feat(pwa): logicsrc credentials app — real auth + Turso, redesigned; retire commandboard-api credshare
Some checks failed
CI / build (push) Has been cancelled
test / test (push) Has been cancelled

Adds apps/pwa: an Express + libSQL/Turso app that is now the home of team
credential sharing, with the moshcode-style auth stack ported and reskinned to
match logicsrc.com (light theme, Inter, green accent).

apps/pwa
- auth: email/password (scrypt), passkeys (WebAuthn), CoinPay OAuth, cookie
  sessions, and lsk_ API keys for the CLI via a loopback OAuth-PKCE flow
  (/cli/authorize + /cli/token). Ported from the moshcode PWA.
- credshare API (/api/credshare/*): teams, members, invites, vaults, sealed
  grants, ciphertext secrets, audit — authed by session OR Bearer lsk_ key.
  Zero-knowledge: only ciphertext + sealed vault keys + public keys stored.
- teams dashboard, accept-invite, and settings (API keys) pages, server-rendered
  in the LogicSRC brand (lib/html.mjs).
- migrations (libSQL) 001_auth + 002_credshare, migrate-on-boot; Turso via
  TURSO_DATABASE_URL / TURSO_AUTH_TOKEN, or a local file db for dev.
- trimmed moshcode-specific approvals/credits/push/deliver.

CLI
- `logicsrc login` now does browser loopback OAuth-PKCE against the app and
  stores an lsk_ token (email-OTP removed); --token for CI. Client repointed.

Distribution
- install.sh (served at logicsrc.com/install.sh) installs the CLI from the
  GitHub repo: tarball -> npm install -> `npm run build:cli` -> logicsrc wrapper.
- root build:cli builds only the CLI's workspace chain (skips web/api/next).

Cleanup
- removed the commandboard-api credshare backend (superseded by the PWA) and its
  Supabase/Turso stores + libsql dep; commandboard-api tests green (40).
- removed the Next.js /teams page (the PWA is the web UI now).

Verified end-to-end: two accounts register on the PWA, mint lsk_ keys, CLI login
uploads identity keys, owner pushes an encrypted .env, teammate invited ->
accepted -> granted -> pulls the exact file. Server stores ciphertext only.
Full workspace build + tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Anthony Ettinger 2026-07-13 14:29:29 +00:00
parent f057589d66
commit 9ba044577f
46 changed files with 2785 additions and 1730 deletions

View file

@ -11,10 +11,6 @@ import { listSocialAccountProviders, socialAccountsPlugin } from "@logicsrc/plug
import { uGigPlugin } from "@logicsrc/plugin-ugig";
import { schemas, validate } from "@logicsrc/validators";
import { buildAgentMailService, mailIdentity } from "./agentmail.js";
import { createCredShareApi, type CredShareRequest } from "./credshare/router.js";
import { createMemoryCredShareStore } from "./credshare/store.js";
import { createSupabaseCredShareStore } from "./credshare/supabase-store.js";
import { createResendEmailSender } from "./credshare/email.js";
const registry = createPluginRegistry([coinPayPlugin, uGigPlugin, sh1ptPlugin, c0mputePlugin, feedDiscoveryPlugin, socialAccountsPlugin, emailAccountsPlugin, agentMailPlugin]);
@ -61,42 +57,6 @@ const c0mputeWorkers = [
{ id: "worker_pool_1", region: "us-west", status: "preview", capacity: "wip" }
];
// Credential-sharing API: Supabase-backed when SUPABASE_URL + service key are
// present, else an in-process memory store (local dev / tests). Zero-knowledge:
// the server only ever relays ciphertext, wrapped keys, and public keys.
const credShareApi = createCredShareApi({
store: createSupabaseCredShareStore() ?? createMemoryCredShareStore(),
email: createResendEmailSender(),
webBaseUrl: process.env.LOGICSRC_WEB_URL || "https://logicsrc.com"
});
const CREDSHARE_PREFIX = "/api/credshare";
async function handleCredShare(request: IncomingMessage, response: ServerResponse, url: URL) {
const method = request.method ?? "GET";
let body: unknown;
if (method === "POST" || method === "PUT" || method === "PATCH") {
try {
body = await readJson(request);
} catch {
json(response, 400, { error: "Invalid JSON body" });
return;
}
}
const authHeader = request.headers["authorization"];
const header = Array.isArray(authHeader) ? authHeader[0] : authHeader;
const token = header?.toLowerCase().startsWith("bearer ") ? header.slice(7).trim() : undefined;
const req: CredShareRequest = {
method,
path: url.pathname.slice(CREDSHARE_PREFIX.length) || "/",
query: url.searchParams,
body,
token
};
const result = await credShareApi.handle(req);
json(response, result.status, result.body);
}
class InvalidJsonBodyError extends Error {
constructor() {
super("Invalid JSON body");
@ -125,7 +85,7 @@ async function route(request: IncomingMessage, response: ServerResponse) {
json(response, 200, {
ok: true,
service: "commandboard-api",
endpoints: ["/health", "/api/boards", "/api/tasks", "/api/plugins", "/api/schemas", "/api/accounts/providers", "/api/accounts", "/api/social/providers", "/api/email/providers", "/api/feeds/discover", "/api/feeds/providers", "/api/credshare/auth/request", "/api/credshare/auth/verify", "/api/credshare/keys", "/api/credshare/teams", "/api/credshare/teams/:slug/members", "/api/credshare/teams/:slug/invites", "/api/credshare/teams/:slug/vaults", "/api/credshare/invites/accept", "/api/credshare/vaults/:id/secrets", "/api/credshare/vaults/:id/grant", "/api/credshare/vaults/:id/grants", "/api/credshare/vaults/:id/audit", "/api/plugins/agentmail/mailboxes", "/api/plugins/agentmail/mailboxes/:mailbox/messages", "/api/plugins/agentmail/mailboxes/:mailbox/messages/:uid", "/api/plugins/agentmail/search", "/api/plugins/agentmail/messages", "/rss/discover/:keyword.xml", "/opml/discover/:keyword.xml", "/atom/discover/:keyword.xml", "/json-feed/discover/:keyword.json"]
endpoints: ["/health", "/api/boards", "/api/tasks", "/api/plugins", "/api/schemas", "/api/accounts/providers", "/api/accounts", "/api/social/providers", "/api/email/providers", "/api/feeds/discover", "/api/feeds/providers", "/api/plugins/agentmail/mailboxes", "/api/plugins/agentmail/mailboxes/:mailbox/messages", "/api/plugins/agentmail/mailboxes/:mailbox/messages/:uid", "/api/plugins/agentmail/search", "/api/plugins/agentmail/messages", "/rss/discover/:keyword.xml", "/opml/discover/:keyword.xml", "/atom/discover/:keyword.xml", "/json-feed/discover/:keyword.json"]
});
return;
}
@ -260,11 +220,6 @@ async function route(request: IncomingMessage, response: ServerResponse) {
return;
}
if (url.pathname === CREDSHARE_PREFIX || url.pathname.startsWith(`${CREDSHARE_PREFIX}/`)) {
await handleCredShare(request, response, url);
return;
}
if (url.pathname === "/api/plugins/agentmail" || url.pathname.startsWith("/api/plugins/agentmail/")) {
await handleAgentMail(request, response, url);
return;