@sorb/juice 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -0
- package/dist/cli.js +1209 -0
- package/dist/cli.js.map +7 -0
- package/dist/index.js +989 -0
- package/dist/index.js.map +7 -0
- package/dist/migrations/001_init.sql +106 -0
- package/package.json +49 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/store/redis.js", "../src/index.js", "../src/server.js", "../src/auth.js", "../src/entitlements.js", "../src/store/memory.js", "../src/store/index.js", "../src/config.js", "../src/db/migrate.js", "../src/db/index.js", "../src/watch.js", "../src/transform.js", "../src/github.js"],
|
|
4
|
+
"sourcesContent": ["// Redis-backed Store implementation.\n//\n// IMPORTANT: ioredis is imported at MODULE TOP \u2014 this is ALLOWED *only* because\n// this file is reachable solely via a dynamic `await import('./redis.js')` from\n// store/index.js, gated on REDIS_URL being set. Nothing reachable from\n// src/index.js or src/cli.js may static-import this file. Free local users who\n// never set REDIS_URL never load this module and never need ioredis installed.\nimport Redis from 'ioredis'\n\n// Key layout:\n// preview:{id} -> JSON { tokens, createdAt } (PX = config.previewTtlMs)\n// verify:{id} -> JSON { storyId, bbox, meta, createdAt } (PX = config.previewTtlMs)\n// verify:latest -> the newest verify id (PX = config.previewTtlMs)\n//\n// Counting uses SCAN over the namespaced key prefix (DBSIZE/KEYS avoided so we\n// don't block Redis and don't miscount when sharing a db). Redis' own PX TTL\n// handles expiry, so counts are naturally \"active\" (expired keys are gone).\nconst PREVIEW_PREFIX = 'preview:'\nconst VERIFY_PREFIX = 'verify:'\nconst VERIFY_LATEST_KEY = 'verify:latest'\n\nconst previewKey = (id) => PREVIEW_PREFIX + id\nconst verifyKey = (id) => VERIFY_PREFIX + id\n\n/**\n * Count keys matching a glob pattern via non-blocking SCAN.\n * @param {import('ioredis').Redis} client\n * @param {string} match\n * @returns {Promise<number>}\n */\nconst scanCount = async (client, match) => {\n let cursor = '0'\n let total = 0\n do {\n // COUNT is a hint; 100 keeps each round-trip small.\n const [next, keys] = await client.scan(cursor, 'MATCH', match, 'COUNT', 100)\n cursor = next\n total += keys.length\n } while (cursor !== '0')\n return total\n}\n\n/**\n * Create the Redis-backed Store.\n *\n * @param {import('../types').Config} config\n * @returns {Promise<import('../types').Store>}\n */\nexport const createRedisStore = async (config) => {\n const ttlMs = config.previewTtlMs\n\n // lazyConnect so construction never throws on an unreachable Redis; we connect\n // explicitly and surface failures through ping() / the /ready check.\n const client = new Redis(config.redisUrl, {\n lazyConnect: true,\n maxRetriesPerRequest: 2,\n })\n\n // ioredis emits 'error' asynchronously; without a listener an unreachable\n // server would crash the process with an unhandled 'error' event.\n client.on('error', () => {\n // Swallowed \u2014 surfaced via ping(); reconnection is handled by ioredis.\n })\n\n try {\n await client.connect()\n } catch (e) {\n // Defer hard failure to ping()/usage rather than crashing the factory.\n void e\n }\n\n // \u2500\u2500\u2500 PREVIEWS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /** @type {import('../types').Store['putPreview']} */\n const putPreview = async (id, tokens) => {\n const entry = { tokens, createdAt: Date.now() }\n await client.set(previewKey(id), JSON.stringify(entry), 'PX', ttlMs)\n }\n\n /** @type {import('../types').Store['getPreview']} */\n const getPreview = async (id) => {\n const raw = await client.get(previewKey(id))\n if (raw == null) return null\n return JSON.parse(raw)\n }\n\n /** @type {import('../types').Store['hasPreview']} */\n const hasPreview = async (id) => {\n return (await client.exists(previewKey(id))) === 1\n }\n\n /** @type {import('../types').Store['updatePreview']} */\n const updatePreview = async (id, tokens) => {\n // Match today's PUT semantics: refuse to create, only refresh an existing\n // entry. Re-setting with a fresh PX refreshes the TTL.\n if (!(await hasPreview(id))) return false\n const entry = { tokens, createdAt: Date.now() }\n await client.set(previewKey(id), JSON.stringify(entry), 'PX', ttlMs)\n return true\n }\n\n /** @type {import('../types').Store['deletePreview']} */\n const deletePreview = async (id) => {\n await client.del(previewKey(id))\n }\n\n /** @type {import('../types').Store['countPreviews']} */\n const countPreviews = async () => {\n return scanCount(client, PREVIEW_PREFIX + '*')\n }\n\n // \u2500\u2500\u2500 VERIFICATIONS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /** @type {import('../types').Store['putVerification']} */\n const putVerification = async (id, { storyId, bbox, meta }) => {\n const entry = { storyId, bbox, meta, createdAt: Date.now() }\n // Set the entry and advance the latest pointer atomically-ish; both carry\n // the same TTL so a stale \"latest\" pointer expires alongside its entry.\n const pipeline = client.multi()\n pipeline.set(verifyKey(id), JSON.stringify(entry), 'PX', ttlMs)\n pipeline.set(VERIFY_LATEST_KEY, id, 'PX', ttlMs)\n await pipeline.exec()\n }\n\n /** @type {import('../types').Store['getVerification']} */\n const getVerification = async (id) => {\n const raw = await client.get(verifyKey(id))\n if (raw == null) return null\n return JSON.parse(raw)\n }\n\n /** @type {import('../types').Store['getLatestVerification']} */\n const getLatestVerification = async () => {\n const id = await client.get(VERIFY_LATEST_KEY)\n if (id == null) return null\n // The pointer may outlive nothing here (same TTL), but the entry could have\n // been explicitly deleted \u2014 fall through to null in that case.\n return getVerification(id)\n }\n\n /** @type {import('../types').Store['countVerifications']} */\n const countVerifications = async () => {\n // verify:latest shares the verify: prefix; subtract it so the count matches\n // the number of real verification entries (memory store counts entries only).\n const total = await scanCount(client, VERIFY_PREFIX + '*')\n const hasLatest = (await client.exists(VERIFY_LATEST_KEY)) === 1\n return hasLatest ? Math.max(0, total - 1) : total\n }\n\n // \u2500\u2500\u2500 LIFECYCLE / HEALTH \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /** @type {import('../types').Store['ping']} */\n const ping = async () => {\n try {\n return (await client.ping()) === 'PONG'\n } catch (e) {\n void e\n return false\n }\n }\n\n /** @type {import('../types').Store['close']} */\n const close = async () => {\n // Idempotent: quit() on an already-ended client throws \"Connection is\n // closed.\" \u2014 swallow it so close() can be called repeatedly.\n try {\n await client.quit()\n } catch (e) {\n void e\n }\n }\n\n return {\n putPreview,\n getPreview,\n hasPreview,\n updatePreview,\n deletePreview,\n countPreviews,\n putVerification,\n getVerification,\n getLatestVerification,\n countVerifications,\n ping,\n close,\n }\n}\n\nexport default createRedisStore\n", "export { createServer } from './server'\nexport { createStore } from './store/index'\nexport { createMemoryStore } from './store/memory'\nexport { loadConfig } from './config'\nexport { createDb } from './db/index'\nexport { watchTokenFile } from './watch'\nexport { runStyleDictionary } from './transform'\nexport { openTokenPR } from './github'\n", "import { Hono } from 'hono'\nimport { cors } from 'hono/cors'\nimport { nanoid } from 'nanoid'\nimport { resolveApiKey, SCOPE } from './auth.js'\nimport { getEntitlements, effectiveEntitlements, FREE } from './entitlements.js'\n\n/**\n * Build the Hono bridge app. All preview/verify state lives in the injected\n * `store` (see storeInterface) \u2014 this module keeps NO in-memory Maps and no\n * prune timers (pruning now lives in the store). Id generation (nanoid(8))\n * stays here; the store never mints ids.\n *\n * ## Two modes \u2014 gated entirely on `config.databaseUrl`\n *\n * **LOCAL mode** (`config.databaseUrl` UNSET): behaves EXACTLY as the free local\n * bridge always has \u2014 no auth, open CORS ('*' or CORS_ORIGINS), in-memory store,\n * all routes open and un-tenant-scoped. Zero new behavior is registered.\n *\n * **HOSTED mode** (`config.databaseUrl` SET): the bridge reads sorb-cloud's\n * shared Postgres for auth + entitlements:\n * - scoped CORS sourced from the project's `allowed_origins`,\n * - a Bearer API key is required on every route except `/health` + `/ready`,\n * - publishable keys are read-only (GET /preview/:id only; writes \u2192 403),\n * - entitlements are enforced (maxActivePreviews \u2192 402, preview TTL,\n * sharing-gated actions \u2192 402), with past_due/canceled orgs degraded to Free.\n *\n * @param {Object} options\n * @param {import('./types').Store} options.store\n * Required. A Store instance from src/store/index.js. All preview + verify\n * handlers delegate to it (awaited).\n * @param {import('./types').Config} options.config\n * Required. Config from src/config.js. Supplies `namespace` (surfaced in\n * /health), `corsOrigins`, `databaseUrl` (the hosted-mode switch) and the\n * TTL/prune windows the store honors.\n * @param {import('./types').DbHandle | null} [options.db]\n * Optional Postgres handle (or null in local mode). /ready pings it only when\n * it is non-null. In hosted mode it is the source for auth + entitlements +\n * the per-project active-preview count, and MUST be non-null.\n * @param {() => import('./types').TokenSet} [options.getLatestTokens]\n * Latest committed tokens for /tokens/latest. Defaults to () => ({}).\n * @param {() => (Array<{id:string,cssVar:string,value:string,tier:string,type:string}> | null)} [options.getResolvedTokens]\n * Resolved bindable token map (.sorb/resolved.json), or null. Defaults to () => null.\n * @param {() => (object | null)} [options.getArtifactIndex]\n * Captured-component index (.sorb/index.json), or null. Defaults to () => null.\n * @param {(storyId: string) => (object | null)} [options.getArtifact]\n * Artifact JSON for a story id (looked up via the index \u2014 never a raw path),\n * or null. Defaults to () => null.\n * @param {(err: unknown, context?: Record<string, unknown>) => void} [options.onError]\n * Optional error sink for the DB-error / best-effort catch paths. Threaded in\n * (dependency-injection style, mirroring db/store) so this module stays\n * Sentry-import-free and fake-db testable. Defaults to a no-op; cli.js `serve`\n * passes `captureError` from src/sentry.js (itself a no-op when SENTRY_DSN is\n * unset). The test harness omits it, so capture stays a no-op under `node --test`.\n * @returns {import('hono').Hono} the Hono app (synchronous \u2014 /ready does its\n * async backend checks per-request).\n */\nexport const createServer = ({\n store,\n config,\n db = null,\n getLatestTokens = () => ({}),\n getResolvedTokens = () => null,\n getArtifactIndex = () => null,\n getArtifact = () => null,\n onError = () => {},\n}) => {\n const namespace = config.namespace\n const corsOrigin =\n config.corsOrigins && config.corsOrigins !== '*' ? config.corsOrigins : '*'\n\n // The ONE switch. Everything new lives behind `hosted`; when it is false the\n // server is byte-for-byte the free local bridge it has always been.\n const hosted = Boolean(config.databaseUrl)\n\n // Where to send a user who hits a paid gate (402). Relative '/billing' by\n // default; overridable for the hosted control-plane host.\n const upgradeUrl = process.env.SORB_UPGRADE_URL || '/billing'\n\n // Preview TTL for persistent (paid) previews. null = no expiry; otherwise the\n // configured (default 24h) window for Free orgs.\n const previewTtlMs = config.previewTtlMs || 86_400_000\n\n const app = new Hono()\n\n // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // HOSTED middleware (registered ONLY when a database is configured)\n // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if (hosted) {\n // 1) Auth \u2014 resolve the Bearer key on every route except infra probes and\n // CORS preflight. MUST run BEFORE the CORS middleware: Hono's `cors`\n // evaluates its `origin` callback at the TOP of its handler (before\n // `next()`), so the authed context it scopes against has to already be on\n // the request. Registering auth first guarantees `c.get('auth')` is\n // populated by the time CORS decides the Access-Control-Allow-Origin\n // header for real (keyed) requests. Preflight (OPTIONS) carries no\n // Authorization header, so we skip auth for it and let CORS answer it.\n app.use('*', async (c, next) => {\n const path = c.req.path\n if (path === '/health' || path === '/ready') return next()\n // Preflight: no body, no key. Let the CORS middleware (registered next)\n // answer it; never 401 a preflight.\n if (c.req.method === 'OPTIONS') return next()\n\n let ctx\n try {\n ctx = await resolveApiKey(db, c.req.header('Authorization'))\n } catch (e) {\n // DB outage during auth lookup \u2014 fail CLOSED, never fall through to\n // open/local behavior.\n onError(e, { at: 'auth' })\n return c.json({ error: 'Service unavailable', code: 'db_unavailable' }, 503)\n }\n if (!ctx) {\n return c.json({ error: 'Unauthorized', code: 'unauthorized' }, 401)\n }\n c.set('auth', ctx)\n\n // Resolve + degrade entitlements once per request, behind the same DB.\n let ent\n try {\n ent = effectiveEntitlements(await getEntitlements(db, ctx.orgId))\n } catch (e) {\n onError(e, { at: 'entitlements' })\n return c.json({ error: 'Service unavailable', code: 'db_unavailable' }, 503)\n }\n c.set('ent', ent)\n\n return next()\n })\n\n // 2) Scoped CORS \u2014 defense-in-depth on top of auth + tenant scoping.\n // NEVER '*' in hosted mode. Preflight (OPTIONS) carries no Authorization\n // header so it cannot be project-scoped; we echo the Origin permissively\n // for preflight (it reveals nothing \u2014 the real request is still auth +\n // tenant gated). For real (keyed) requests auth has already run, so we\n // echo the Origin only when it is a member of the authed project's\n // allowed_origins.\n app.use(\n '*',\n cors({\n origin: (origin, c) => {\n if (!origin) return origin\n // Preflight: no body, no key \u2014 echo so the browser proceeds to the\n // real (authoritative) request.\n if (c.req.method === 'OPTIONS') return origin\n const ctx = c.get('auth')\n if (ctx && Array.isArray(ctx.allowedOrigins) && ctx.allowedOrigins.includes(origin)) {\n return origin\n }\n // Deny: returning undefined omits the ACAO header. Empty\n // allowed_origins \u21D2 deny all cross-origin (never '*').\n return undefined\n },\n }),\n )\n } else {\n // LOCAL mode \u2014 today's open CORS, unchanged.\n app.use('*', cors({ origin: corsOrigin }))\n }\n\n // Scope guard: WRITE ops require a 'secret' key. Returns a Response (the 403)\n // when the key is read-only, or null when the caller may proceed. In local\n // mode there is no auth context, so this is a no-op.\n const requireWrite = (c) => {\n if (!hosted) return null\n const ctx = c.get('auth')\n if (ctx.scope !== SCOPE.WRITE) {\n return c.json({ error: 'Publishable keys are read-only', code: 'read_only' }, 403)\n }\n return null\n }\n\n // Count the authed project's currently-active previews from the DB previews\n // table (project-scoped + TTL-aware). Owned by juice (001_core.sql).\n const activePreviewCount = async (projectId) => {\n const res = await db.query(\n 'SELECT count(*)::int AS n FROM previews WHERE project_id = $1 AND (expires_at IS NULL OR expires_at > now())',\n [projectId],\n )\n const row = res && res.rows && res.rows[0]\n return row ? Number(row.n) || 0 : 0\n }\n\n // Sharing gate (frozen for the future share route). Returns the 402 Response\n // when sharing is locked, else null.\n const requireSharing = (c) => {\n const ent = c.get('ent')\n if (!ent || ent.previewSharing !== true) {\n return c.json({ error: 'Preview sharing is not available on your plan', code: 'sharing_locked', upgradeUrl }, 402)\n }\n return null\n }\n\n // \u2500\u2500\u2500 POST /preview \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // Figma plugin POSTs a proposed token set here.\n // Returns a short preview ID the React app can use via ?preview=<id>\n app.post('/preview', async (c) => {\n if (hosted) {\n const denied = requireWrite(c)\n if (denied) return denied\n\n const ctx = c.get('auth')\n const ent = c.get('ent')\n\n // Sharing-gated when explicitly requested via ?share=1 (frozen contract).\n if (c.req.query('share') === '1') {\n const locked = requireSharing(c)\n if (locked) return locked\n }\n\n // Active-preview cap. -1 = unlimited (Free gate is TTL+sharing, not count).\n if (ent.maxActivePreviews !== -1) {\n let active\n try {\n active = await activePreviewCount(ctx.projectId)\n } catch (e) {\n onError(e, { at: 'preview.count' })\n return c.json({ error: 'Service unavailable', code: 'db_unavailable' }, 503)\n }\n if (active >= ent.maxActivePreviews) {\n return c.json(\n {\n error: `Active preview limit reached (${ent.maxActivePreviews}). Upgrade for more concurrent previews.`,\n code: 'preview_limit',\n upgradeUrl,\n },\n 402,\n )\n }\n }\n\n const tokens = await c.req.json()\n const id = nanoid(8)\n // Persistent (paid) \u21D2 no expiry; otherwise 24h TTL.\n const ttlMs = ent.previewPersistence ? null : previewTtlMs\n await store.putPreview(id, tokens, ttlMs)\n\n // Bookkeeping row for the per-project count + TTL (juice owns previews).\n const expiresAt = ttlMs == null ? null : new Date(Date.now() + ttlMs)\n try {\n await db.query(\n 'INSERT INTO previews (id, project_id, expires_at) VALUES ($1, $2, $3)',\n [id, ctx.projectId, expiresAt],\n )\n } catch (e) {\n // Best-effort bookkeeping: the preview is already in the store. A failed\n // insert only affects future counting; do not fail the request. Report\n // it so silent bookkeeping drift is observable.\n onError(e, { at: 'preview.bookkeeping.insert', id })\n }\n\n const url = `?preview=${id}`\n return c.json({ id, url })\n }\n\n // LOCAL mode \u2014 unchanged.\n const tokens = await c.req.json()\n const id = nanoid(8)\n await store.putPreview(id, tokens)\n const url = `?preview=${id}`\n return c.json({ id, url })\n })\n\n // \u2500\u2500\u2500 GET /preview/:id \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // React app polls this while a preview is active. Allowed for BOTH read\n // (publishable) and write (secret) scope in hosted mode.\n app.get('/preview/:id', async (c) => {\n const id = c.req.param('id')\n const entry = await store.getPreview(id)\n if (!entry) {\n return c.json({ error: 'Preview not found or expired' }, 404)\n }\n if (hosted) {\n // Tenant isolation: never reveal a cross-tenant preview's existence.\n const ctx = c.get('auth')\n let owned = false\n try {\n const res = await db.query(\n 'SELECT 1 FROM previews WHERE id = $1 AND project_id = $2 LIMIT 1',\n [id, ctx.projectId],\n )\n owned = Boolean(res && res.rows && res.rows.length)\n } catch (e) {\n onError(e, { at: 'preview.owner', method: 'GET', id })\n return c.json({ error: 'Service unavailable', code: 'db_unavailable' }, 503)\n }\n if (!owned) {\n return c.json({ error: 'Preview not found or expired' }, 404)\n }\n }\n return c.json(entry.tokens)\n })\n\n // \u2500\u2500\u2500 PUT /preview/:id \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // Plugin updates an existing preview in-place (live edit mode). WRITE op.\n app.put('/preview/:id', async (c) => {\n const id = c.req.param('id')\n if (hosted) {\n const denied = requireWrite(c)\n if (denied) return denied\n // Tenant scope: a cross-tenant id is \"not found\".\n const ctx = c.get('auth')\n let owned = false\n try {\n const res = await db.query(\n 'SELECT 1 FROM previews WHERE id = $1 AND project_id = $2 LIMIT 1',\n [id, ctx.projectId],\n )\n owned = Boolean(res && res.rows && res.rows.length)\n } catch (e) {\n onError(e, { at: 'preview.owner', method: 'PUT', id })\n return c.json({ error: 'Service unavailable', code: 'db_unavailable' }, 503)\n }\n if (!owned) {\n return c.json({ error: 'Preview not found' }, 404)\n }\n }\n const tokens = await c.req.json()\n const updated = await store.updatePreview(id, tokens)\n if (!updated) {\n return c.json({ error: 'Preview not found' }, 404)\n }\n return c.json({ id, updated: true })\n })\n\n // \u2500\u2500\u2500 DELETE /preview/:id \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // Plugin clears a preview when designer exits without committing. WRITE op.\n app.delete('/preview/:id', async (c) => {\n const id = c.req.param('id')\n if (hosted) {\n const denied = requireWrite(c)\n if (denied) return denied\n // Tenant scope: only delete (and report deleted) when it belongs to the\n // tenant. A cross-tenant id is treated as already-absent.\n const ctx = c.get('auth')\n let owned = false\n try {\n const res = await db.query(\n 'SELECT 1 FROM previews WHERE id = $1 AND project_id = $2 LIMIT 1',\n [id, ctx.projectId],\n )\n owned = Boolean(res && res.rows && res.rows.length)\n } catch (e) {\n onError(e, { at: 'preview.owner', method: 'DELETE', id })\n return c.json({ error: 'Service unavailable', code: 'db_unavailable' }, 503)\n }\n if (!owned) {\n return c.json({ error: 'Preview not found' }, 404)\n }\n await store.deletePreview(id)\n try {\n await db.query('DELETE FROM previews WHERE id = $1 AND project_id = $2', [id, ctx.projectId])\n } catch (e) {\n // Best-effort: the store entry is gone; a stale bookkeeping row only\n // affects future counting and will TTL out. Report so drift is visible.\n onError(e, { at: 'preview.bookkeeping.delete', id })\n }\n return c.json({ deleted: true })\n }\n await store.deletePreview(id)\n return c.json({ deleted: true })\n })\n\n // \u2500\u2500\u2500 POST /verify \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // Plugin posts the post-layout geometry of an inserted component so the\n // canvas can be reconciled against the captured artifact. Returns a short id.\n // WRITE op.\n app.post('/verify', async (c) => {\n if (hosted) {\n const denied = requireWrite(c)\n if (denied) return denied\n }\n const { storyId, bbox, meta } = await c.req.json()\n const id = nanoid(8)\n await store.putVerification(id, { storyId, bbox, meta })\n return c.json({ id })\n })\n\n // \u2500\u2500\u2500 GET /verify/latest \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // The most recently reported verification. MUST be registered before\n // /verify/:id or Hono treats \"latest\" as an :id param.\n app.get('/verify/latest', async (c) => {\n const entry = await store.getLatestVerification()\n if (!entry) {\n return c.json({ error: 'No verification reported yet' }, 404)\n }\n return c.json(entry)\n })\n\n // \u2500\u2500\u2500 GET /verify/:id \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // A specific verification by id.\n app.get('/verify/:id', async (c) => {\n const entry = await store.getVerification(c.req.param('id'))\n if (!entry) {\n return c.json({ error: 'Verification not found or expired' }, 404)\n }\n return c.json(entry)\n })\n\n // \u2500\u2500\u2500 GET /tokens/latest \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // Returns the latest committed tokens from disk.\n // Plugin fetches this on open to pre-populate the editor.\n app.get('/tokens/latest', (c) => {\n return c.json(getLatestTokens())\n })\n\n // \u2500\u2500\u2500 GET /tokens/resolved \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // The resolved *bindable* token map produced by Style Dictionary \u2014 one entry\n // per token: { id, cssVar, value, tier, type }. The plugin uses it to create\n // grouped Variables and to bind captured values; capture annotates against\n // it. 404 if it hasn't been built yet.\n app.get('/tokens/resolved', (c) => {\n const resolved = getResolvedTokens()\n if (!resolved) {\n return c.json(\n { error: 'No resolved token map. Run `sorb-seed resolve` (Style Dictionary build).' },\n 404,\n )\n }\n return c.json(resolved)\n })\n\n // \u2500\u2500\u2500 GET /artifacts \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // The captured-component index \u2014 list of components/stories with hashes,\n // produced by `sorb-seed capture`. 404 until seed has run.\n app.get('/artifacts', (c) => {\n const idx = getArtifactIndex()\n if (!idx) {\n return c.json(\n { error: 'No artifact index. Run `sorb-seed capture`.' },\n 404,\n )\n }\n return c.json(idx)\n })\n\n // \u2500\u2500\u2500 GET /artifact?id=<storyId> \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // Lookup by id in the index \u2014 never accepts an arbitrary filesystem path.\n app.get('/artifact', (c) => {\n const storyId = c.req.query('id')\n if (!storyId) return c.json({ error: 'Missing ?id=' }, 400)\n const art = getArtifact(storyId)\n if (!art) return c.json({ error: 'Artifact not found for id: ' + storyId }, 404)\n return c.json(art)\n })\n\n // \u2500\u2500\u2500 GET /health \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // Infra probe \u2014 open in ALL modes (no auth, no tenant scope).\n app.get('/health', async (c) => {\n return c.json({\n ok: true,\n namespace,\n activePreviews: await store.countPreviews(),\n verifications: await store.countVerifications(),\n })\n })\n\n // \u2500\u2500\u2500 GET /ready \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // Readiness probe: 200 only when every *configured* backend is reachable.\n // Open in ALL modes (no auth). The in-memory store always pings true; a null\n // db (local mode) is \"not configured\" and skipped. Returns 503 with\n // { ok:false, checks } when any configured backend is unreachable.\n app.get('/ready', async (c) => {\n /** @type {Record<string, boolean>} */\n const checks = {}\n\n try {\n checks.store = await store.ping()\n } catch (e) {\n checks.store = false\n }\n\n if (db) {\n try {\n checks.db = await db.ping()\n } catch (e) {\n checks.db = false\n }\n }\n\n const ok = Object.values(checks).every(Boolean)\n return c.json({ ok, checks }, ok ? 200 : 503)\n })\n\n return app\n}\n", "/**\n * Hosted-mode API-key authentication for the bridge.\n *\n * BACK-COMPAT IS SACRED: this module is imported only inside the hosted branch\n * of createServer (when config.databaseUrl is set). It has ZERO new deps \u2014 it\n * needs only `node:crypto` and takes the db handle as an injected argument, so\n * it stays pure/testable with a FAKE db and never reaches Postgres in local\n * mode.\n *\n * Keys are validated by sha256-hashing the raw bearer token and looking up\n * api_keys.hash (which stores the sha256 hex, UNIQUE) where revoked_at IS NULL.\n * The raw key is never stored anywhere \u2014 only its hash.\n *\n * @module auth\n */\n\nimport { createHash } from 'node:crypto'\n\n/**\n * Authorization scope derived from an API key's type.\n *\n * `publishable` keys are read-only (`read`); `secret` keys may write (`write`).\n * @type {Readonly<{ READ: 'read', WRITE: 'write' }>}\n */\nexport const SCOPE = Object.freeze({ READ: 'read', WRITE: 'write' })\n\n/**\n * The authenticated tenant context resolved from a valid API key. Frozen so\n * downstream middleware/handlers can't mutate the tenant identity mid-request.\n *\n * `scope` is DERIVED from `type`: publishable \u2192 'read', secret \u2192 'write'.\n * `allowedOrigins` is always an array (defaults to [] when the column is null).\n *\n * @typedef {Object} AuthContext\n * @property {string} keyId The api_keys.id (uuid) of the matched key.\n * @property {'secret' | 'publishable'} type The key type.\n * @property {string} projectId projects.id (uuid) the key belongs to.\n * @property {string} orgId projects.org_id (uuid) \u2014 the owning organization.\n * @property {string} namespace projects.namespace.\n * @property {string[]} allowedOrigins projects.allowed_origins (text[]); [] if null.\n * @property {'read' | 'write'} scope Derived from `type`.\n */\n\n/**\n * sha256-hash a raw API key to its hex digest.\n *\n * Matches the storage format of api_keys.hash (sha256 hex). Pure and\n * synchronous \u2014 no db, no I/O.\n *\n * @param {string} raw The raw bearer token (the user-supplied key).\n * @returns {string} Lowercase hex sha256 digest of `raw`.\n */\nexport function hashKey(raw) {\n return createHash('sha256').update(raw, 'utf8').digest('hex')\n}\n\n/**\n * Parse an `Authorization` header value into its raw bearer token.\n *\n * Accepts a case-insensitive `Bearer` scheme and trims surrounding whitespace.\n * Returns null when the header is missing, malformed, or carries an empty token.\n *\n * @param {string | null | undefined} authHeader The raw Authorization header.\n * @returns {string | null} The raw token, or null when absent/malformed.\n */\nfunction parseBearer(authHeader) {\n if (typeof authHeader !== 'string') return null\n const trimmed = authHeader.trim()\n if (trimmed === '') return null\n // Split on the first run of whitespace into scheme + remainder.\n const match = /^(\\S+)\\s+(.*)$/.exec(trimmed)\n if (!match) return null\n const scheme = match[1]\n if (scheme.toLowerCase() !== 'bearer') return null\n const token = match[2].trim()\n if (token === '') return null\n return token\n}\n\n/**\n * Resolve an `Authorization` header to an {@link AuthContext}, or null.\n *\n * Parses `Bearer <raw>` (case-insensitive scheme), hashes the token, and runs a\n * single JOIN of api_keys against projects, filtering out revoked keys. An\n * unknown OR a revoked key both yield null (the two cases are never\n * distinguished, so a revoked key reveals nothing).\n *\n * db errors are NOT swallowed \u2014 they propagate so the server can map them to a\n * 503 (fail-closed) rather than silently falling through to open behavior.\n *\n * @param {import('./types.js').DbHandle} db Injected db handle (FAKE in tests).\n * @param {string | null | undefined} authHeader The raw Authorization header.\n * @returns {Promise<AuthContext | null>} The tenant context, or null when the\n * header is missing/malformed/empty or the key is unknown/revoked.\n */\nexport async function resolveApiKey(db, authHeader) {\n const raw = parseBearer(authHeader)\n if (raw === null) return null\n\n const hash = hashKey(raw)\n\n const result = await db.query(\n `SELECT k.id AS key_id, k.type, k.project_id, p.org_id, p.namespace, p.allowed_origins\n FROM api_keys k\n JOIN projects p ON p.id = k.project_id\n WHERE k.hash = $1 AND k.revoked_at IS NULL\n LIMIT 1`,\n [hash]\n )\n\n const rows = (result && result.rows) || []\n if (rows.length === 0) return null\n\n const row = rows[0]\n const type = row.type\n const scope = type === 'publishable' ? SCOPE.READ : SCOPE.WRITE\n const allowedOrigins = Array.isArray(row.allowed_origins) ? row.allowed_origins : []\n\n return Object.freeze({\n keyId: row.key_id,\n type,\n projectId: row.project_id,\n orgId: row.org_id,\n namespace: row.namespace,\n allowedOrigins,\n scope\n })\n}\n", "// Entitlements lookup + normalization for @sorb/juice (hosted mode).\n//\n// Pure JS (JSDoc types). The db handle is INJECTED as the first arg so tests\n// can pass a fake db = { async query(text, params) { return { rows: [...] } } }\n// \u2014 no real Postgres required. This module imports nothing (no pg, no crypto):\n// it only reads sorb-cloud's `entitlements` table via the injected handle.\n//\n// BACK-COMPAT: never reached in local mode (DATABASE_URL unset). The server\n// registers entitlement checks only inside the hosted branch.\n//\n// Spec: payment-subscription-spec.md \u00A74 (entitlements object, Free defaults,\n// degrade rule). See src/types.js for the Entitlements typedef.\n\n/**\n * @typedef {object} DbHandle\n * @property {(text: string, params?: any[]) => Promise<{ rows: any[] }>} query\n */\n\n/**\n * The frozen entitlements object, EXACT shape from payment-spec \u00A74.\n * @typedef {object} Entitlements\n * @property {'free'|'team'|'enterprise'} plan\n * @property {'active'|'trialing'|'past_due'|'canceled'} status\n * @property {number} seats\n * @property {boolean} previewPersistence\n * @property {boolean} previewSharing\n * @property {number} maxProjects -1 = unlimited\n * @property {number} maxActivePreviews -1 = unlimited\n * @property {boolean} captureEnabled\n */\n\n/**\n * The FREE baseline (payment-spec \u00A74 Free row). Hosted Free = 1 project,\n * 1 seat, previews expire at 24h (no persistence), no sharing, no capture.\n * maxActivePreviews is -1 (unlimited COUNT on Free \u2014 the Free gate is the 24h\n * TTL + no sharing, not a count cap). Workers MUST treat -1 as unlimited.\n * @type {Readonly<Entitlements>}\n */\nexport const FREE = Object.freeze({\n plan: 'free',\n status: 'active',\n seats: 1,\n previewPersistence: false,\n previewSharing: false,\n maxProjects: 1,\n maxActivePreviews: -1,\n captureEnabled: false,\n})\n\n/**\n * Coerce a value to a finite Number, falling back to `fallback` on NaN/invalid.\n * @param {unknown} v\n * @param {number} fallback\n * @returns {number}\n */\nconst numOr = (v, fallback) => {\n const n = Number(v)\n return Number.isFinite(n) ? n : fallback\n}\n\n/**\n * Normalize a raw `entitlements` row into a frozen {@link Entitlements} object.\n *\n * Merge order (later wins):\n * 1. FREE baseline.\n * 2. row.data jsonb \u2014 field-by-field, only KNOWN keys; each value coerced\n * (booleans \u2192 Boolean, numeric caps \u2192 Number with NaN \u2192 FREE value).\n * 3. row.plan / row.status from the columns (columns win for plan + status).\n *\n * Unknown / missing fields fall back to the FREE value. Pure; no db.\n *\n * @param {{ plan?: unknown, status?: unknown, data?: unknown } | null | undefined} row\n * @returns {Readonly<Entitlements>}\n */\nexport function normalizeEntitlements(row) {\n const r = row || {}\n // row.data may arrive as parsed jsonb (object) or as a JSON string.\n let data = r.data\n if (typeof data === 'string') {\n try {\n data = JSON.parse(data)\n } catch (e) {\n data = null\n }\n }\n if (data === null || data === undefined || typeof data !== 'object') data = {}\n\n /** @type {Entitlements} */\n const out = {\n plan: FREE.plan,\n status: FREE.status,\n seats: 'seats' in data ? Math.max(1, numOr(data.seats, FREE.seats)) : FREE.seats,\n previewPersistence:\n 'previewPersistence' in data ? Boolean(data.previewPersistence) : FREE.previewPersistence,\n previewSharing: 'previewSharing' in data ? Boolean(data.previewSharing) : FREE.previewSharing,\n maxProjects: 'maxProjects' in data ? numOr(data.maxProjects, FREE.maxProjects) : FREE.maxProjects,\n maxActivePreviews:\n 'maxActivePreviews' in data\n ? numOr(data.maxActivePreviews, FREE.maxActivePreviews)\n : FREE.maxActivePreviews,\n captureEnabled: 'captureEnabled' in data ? Boolean(data.captureEnabled) : FREE.captureEnabled,\n }\n\n // Columns win for plan + status.\n if (r.plan !== undefined && r.plan !== null && String(r.plan).trim() !== '') {\n out.plan = /** @type {Entitlements['plan']} */ (String(r.plan))\n }\n if (r.status !== undefined && r.status !== null && String(r.status).trim() !== '') {\n out.status = /** @type {Entitlements['status']} */ (String(r.status))\n }\n\n return Object.freeze(out)\n}\n\n/**\n * Resolve the entitlements object for an org from sorb-cloud's `entitlements`\n * table. Returns {@link FREE} when no row exists; otherwise the normalized row.\n *\n * On a db.query throw this RETHROWS \u2014 the server maps it to 503. We never fall\n * back to FREE on a DB error, so a Postgres outage can't silently unlock paid\n * gates (fail closed).\n *\n * @param {DbHandle} db Injected db handle (fake in tests).\n * @param {string} orgId uuid string.\n * @returns {Promise<Readonly<Entitlements>>}\n */\nexport async function getEntitlements(db, orgId) {\n const { rows } = await db.query(\n 'SELECT plan, status, data FROM entitlements WHERE org_id = $1 LIMIT 1',\n [orgId],\n )\n if (!rows || rows.length === 0) return FREE\n return normalizeEntitlements(rows[0])\n}\n\n/**\n * Apply the payment-spec \u00A74 degrade rule. If status is 'past_due' or\n * 'canceled', collapse the paid GATE fields back to Free values (24h TTL, no\n * sharing/persistence, Free caps + seats) while KEEPING plan + status from the\n * original so the upgrade CTA can explain why access lapsed. 'active' and\n * 'trialing' pass through unchanged.\n *\n * Pure. The server calls this on the result of {@link getEntitlements} before\n * enforcing any gate.\n *\n * @param {Readonly<Entitlements>} ent\n * @returns {Readonly<Entitlements>}\n */\nexport function effectiveEntitlements(ent) {\n if (ent && (ent.status === 'past_due' || ent.status === 'canceled')) {\n return Object.freeze({\n ...ent,\n // Overlay FREE's gate fields; keep plan + status from ent.\n seats: FREE.seats,\n previewPersistence: FREE.previewPersistence,\n previewSharing: FREE.previewSharing,\n maxProjects: FREE.maxProjects,\n maxActivePreviews: FREE.maxActivePreviews,\n captureEnabled: FREE.captureEnabled,\n plan: ent.plan,\n status: ent.status,\n })\n }\n return ent\n}\n\nexport default getEntitlements\n", "// In-memory Store implementation \u2014 the zero-dependency default backend.\n//\n// Preserves today's exact behavior from server.js:\n// - two Maps (previews, verifications) + a `latestVerifyId` string\n// - 24h-default TTL via config.previewTtlMs\n// - hourly prune via setInterval (config.pruneIntervalMs), .unref()'d so it\n// never holds the process open or hangs vitest\n//\n// All methods are async (return Promises) so this is drop-in interchangeable\n// with the Redis-backed store from the server's perspective.\n\n/**\n * Create the in-memory Store.\n *\n * @param {import('../types').Config} config\n * @returns {import('../types').Store}\n */\nexport const createMemoryStore = (config) => {\n const ttlMs = config.previewTtlMs\n const pruneIntervalMs = config.pruneIntervalMs\n\n /** @type {Map<string, import('../types').PreviewEntry>} */\n const previews = new Map()\n\n // Plugin self-reports each inserted component's post-layout geometry here:\n // { storyId, bbox, meta, createdAt }. `latestVerifyId` tracks the newest so\n // the canvas verifier can poll /verify/latest without knowing the id.\n /** @type {Map<string, import('../types').VerificationEntry>} */\n const verifications = new Map()\n /** @type {string | null} */\n let latestVerifyId = null\n\n /** True once an entry's TTL has elapsed relative to now. */\n const isExpired = (entry, now) => now - entry.createdAt > ttlMs\n\n // Single hourly prune sweep across both maps (same TTL as today). Mirrors the\n // two setInterval sweeps in the original server.js, collapsed into one timer.\n const pruneTimer = setInterval(() => {\n const now = Date.now()\n for (const [id, entry] of previews) {\n if (isExpired(entry, now)) previews.delete(id)\n }\n for (const [id, entry] of verifications) {\n if (isExpired(entry, now)) {\n verifications.delete(id)\n if (id === latestVerifyId) latestVerifyId = null\n }\n }\n }, pruneIntervalMs)\n // Never keep the event loop alive just for pruning (matters for the CLI\n // shutdown path and for vitest, which would otherwise hang on a live timer).\n if (typeof pruneTimer.unref === 'function') pruneTimer.unref()\n\n // \u2500\u2500\u2500 PREVIEWS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /** @type {import('../types').Store['putPreview']} */\n const putPreview = async (id, tokens) => {\n previews.set(id, { tokens, createdAt: Date.now() })\n }\n\n /** @type {import('../types').Store['getPreview']} */\n const getPreview = async (id) => {\n const entry = previews.get(id)\n if (!entry) return null\n if (isExpired(entry, Date.now())) {\n previews.delete(id)\n return null\n }\n return entry\n }\n\n /** @type {import('../types').Store['hasPreview']} */\n const hasPreview = async (id) => {\n return (await getPreview(id)) !== null\n }\n\n /** @type {import('../types').Store['updatePreview']} */\n const updatePreview = async (id, tokens) => {\n // Match today's PUT semantics: refuse to create, only refresh an existing\n // (and non-expired) entry. Refreshes createdAt \u2192 refreshes TTL.\n if (!(await hasPreview(id))) return false\n previews.set(id, { tokens, createdAt: Date.now() })\n return true\n }\n\n /** @type {import('../types').Store['deletePreview']} */\n const deletePreview = async (id) => {\n previews.delete(id)\n }\n\n /** @type {import('../types').Store['countPreviews']} */\n const countPreviews = async () => {\n const now = Date.now()\n let n = 0\n for (const entry of previews.values()) {\n if (!isExpired(entry, now)) n++\n }\n return n\n }\n\n // \u2500\u2500\u2500 VERIFICATIONS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /** @type {import('../types').Store['putVerification']} */\n const putVerification = async (id, { storyId, bbox, meta }) => {\n verifications.set(id, { storyId, bbox, meta, createdAt: Date.now() })\n latestVerifyId = id\n }\n\n /** @type {import('../types').Store['getVerification']} */\n const getVerification = async (id) => {\n const entry = verifications.get(id)\n if (!entry) return null\n if (isExpired(entry, Date.now())) {\n verifications.delete(id)\n if (id === latestVerifyId) latestVerifyId = null\n return null\n }\n return entry\n }\n\n /** @type {import('../types').Store['getLatestVerification']} */\n const getLatestVerification = async () => {\n if (!latestVerifyId) return null\n return getVerification(latestVerifyId)\n }\n\n /** @type {import('../types').Store['countVerifications']} */\n const countVerifications = async () => {\n const now = Date.now()\n let n = 0\n for (const entry of verifications.values()) {\n if (!isExpired(entry, now)) n++\n }\n return n\n }\n\n // \u2500\u2500\u2500 LIFECYCLE / HEALTH \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /** @type {import('../types').Store['ping']} */\n const ping = async () => true\n\n /** @type {import('../types').Store['close']} */\n const close = async () => {\n // Idempotent: clearInterval is a no-op if already cleared.\n clearInterval(pruneTimer)\n }\n\n return {\n putPreview,\n getPreview,\n hasPreview,\n updatePreview,\n deletePreview,\n countPreviews,\n putVerification,\n getVerification,\n getLatestVerification,\n countVerifications,\n ping,\n close,\n }\n}\n\nexport default createMemoryStore\n", "// Store factory.\n//\n// Picks the backend from `config`:\n// - config.redisUrl set \u2192 DYNAMICALLY `await import('./redis.js')` and return\n// createRedisStore(config). The dynamic import is what keeps ioredis out of\n// the dependency graph for free local users.\n// - config.redisUrl absent \u2192 return createMemoryStore(config), the\n// statically-imported zero-dependency default (today's behavior).\n//\n// NEVER static-import './redis.js' here \u2014 that would pull ioredis into every\n// build reachable from src/index.js / src/cli.js and break the free local mode.\nimport { createMemoryStore } from './memory.js'\n\n/**\n * Create the appropriate Store for the given config.\n *\n * @param {import('../types').Config} config\n * @returns {Promise<import('../types').Store>}\n */\nexport const createStore = async (config) => {\n if (config.redisUrl) {\n const { createRedisStore } = await import('./redis.js')\n return createRedisStore(config)\n }\n return createMemoryStore(config)\n}\n", "// 12-factor environment config loader for @sorb/juice.\n//\n// Pure module: reads process.env and returns a frozen config object with safe\n// local defaults so the FREE local bridge runs with ZERO new env and ZERO new\n// deps. NO Redis/Postgres imports here \u2014 this module only decides *whether*\n// hosted backends are configured (via redisUrl / databaseUrl presence) so the\n// store factory and /ready can pick which checks to run.\n//\n// JavaScript only (JSDoc types). See src/types.js for the Config typedef.\n\n/**\n * Canonical default values for every env var the bridge understands.\n * These reproduce today's single-process behavior exactly (port 7777,\n * in-memory store, 24h TTL, hourly prune, open CORS).\n * @type {{\n * PORT: number,\n * SORB_NAMESPACE: string,\n * REDIS_URL: undefined,\n * DATABASE_URL: undefined,\n * CORS_ORIGINS: '*',\n * PREVIEW_TTL_MS: number,\n * PRUNE_INTERVAL_MS: number,\n * NODE_ENV: string,\n * }}\n */\nexport const DEFAULTS = Object.freeze({\n PORT: 7777,\n SORB_NAMESPACE: 'sorb-local',\n REDIS_URL: undefined,\n DATABASE_URL: undefined,\n CORS_ORIGINS: '*', // open by default = free local mode\n PREVIEW_TTL_MS: 86_400_000, // 24h \u2014 preserves today's preview/verify lifetime\n PRUNE_INTERVAL_MS: 3_600_000, // 1h \u2014 preserves today's in-memory prune cadence\n NODE_ENV: 'development',\n})\n\n/**\n * Parse a positive integer from an env string, falling back to `fallback`\n * when the value is missing, empty, or not a finite positive number.\n * @param {string | undefined} raw\n * @param {number} fallback\n * @returns {number}\n */\nconst intOr = (raw, fallback) => {\n if (raw === undefined || raw === null || String(raw).trim() === '') return fallback\n const n = Number.parseInt(String(raw), 10)\n return Number.isFinite(n) && n > 0 ? n : fallback\n}\n\n/**\n * Coerce an env value to a trimmed non-empty string, or `undefined`.\n * @param {string | undefined} raw\n * @returns {string | undefined}\n */\nconst strOrUndef = (raw) => {\n if (raw === undefined || raw === null) return undefined\n const s = String(raw).trim()\n return s === '' ? undefined : s\n}\n\n/**\n * Parse CORS_ORIGINS. Empty/unset \u2192 '*' (today's open default). Otherwise a\n * comma-separated list becomes a de-duped array of trimmed origins. A literal\n * '*' (alone or among entries) collapses back to '*'.\n * @param {string | undefined} raw\n * @returns {string[] | '*'}\n */\nconst parseCorsOrigins = (raw) => {\n const s = strOrUndef(raw)\n if (s === undefined) return '*'\n const parts = s\n .split(',')\n .map((o) => o.trim())\n .filter((o) => o !== '')\n if (parts.length === 0 || parts.includes('*')) return '*'\n return Array.from(new Set(parts))\n}\n\n/**\n * Load the bridge configuration from `env` (defaults to process.env), applying\n * the safe local defaults from {@link DEFAULTS}. The result is deeply frozen so\n * downstream units (store, db, server) can treat it as immutable.\n *\n * Local mode (no REDIS_URL / DATABASE_URL) yields:\n * { redisUrl: undefined, databaseUrl: undefined, hosted: false, ... }\n * which keeps the in-memory store + no-DB path active.\n *\n * @param {NodeJS.ProcessEnv} [env] Environment source. Defaults to process.env.\n * @returns {import('./types').Config}\n */\nexport const loadConfig = (env = process.env) => {\n const redisUrl = strOrUndef(env.REDIS_URL)\n const databaseUrl = strOrUndef(env.DATABASE_URL)\n\n /** @type {import('./types').Config} */\n const config = {\n port: intOr(env.PORT, DEFAULTS.PORT),\n namespace: strOrUndef(env.SORB_NAMESPACE) ?? DEFAULTS.SORB_NAMESPACE,\n\n // Presence of these URLs is the switch that activates each hosted backend.\n redisUrl,\n databaseUrl,\n\n corsOrigins: parseCorsOrigins(env.CORS_ORIGINS),\n previewTtlMs: intOr(env.PREVIEW_TTL_MS, DEFAULTS.PREVIEW_TTL_MS),\n pruneIntervalMs: intOr(env.PRUNE_INTERVAL_MS, DEFAULTS.PRUNE_INTERVAL_MS),\n nodeEnv: strOrUndef(env.NODE_ENV) ?? DEFAULTS.NODE_ENV,\n\n // Convenience flags so the store factory + /ready don't re-derive presence.\n redisEnabled: redisUrl !== undefined,\n databaseEnabled: databaseUrl !== undefined,\n hosted: redisUrl !== undefined || databaseUrl !== undefined,\n }\n\n return Object.freeze(config)\n}\n\nexport default loadConfig\n", "/**\n * Thin SQL migration runner \u2014 no external migration framework.\n *\n * Reads `src/db/migrations/*.sql` in lexical (NNNN_*) order, applies each\n * inside a single transaction, and records applied filenames in a\n * `schema_migrations` table so re-runs are idempotent. JS only; uses the pool\n * carried by the {@link DbHandle} passed in.\n *\n * @module db/migrate\n */\n\nimport { readdir, readFile } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n/**\n * Module URL used to locate sibling files.\n *\n * In raw ESM source (dev / `node src/cli.js`) this is the real\n * `import.meta.url`. In the esbuild **cjs** bundle (`dist/`) `import.meta` would\n * be empty, so build.mjs `define`s `import.meta.url` to a `__dirname`-derived\n * file URL \u2014 replacing the token outright, so the bundle carries no\n * `import.meta` reference (no esbuild warning) and the path still resolves to\n * `dist/migrations` at runtime.\n *\n * @type {string}\n */\nconst SELF_URL = import.meta.url\n\n/**\n * Absolute path to the directory holding the `.sql` migration files. Resolved\n * relative to this module so it works from both `src/` (dev) and `dist/`\n * (bundled) \u2014 the build copies the `.sql` files alongside the bundle.\n * @type {string}\n */\nexport const MIGRATIONS_DIR = join(dirname(fileURLToPath(SELF_URL)), 'migrations')\n\nconst SCHEMA_MIGRATIONS_DDL = `\nCREATE TABLE IF NOT EXISTS schema_migrations (\n filename text PRIMARY KEY,\n applied_at timestamptz NOT NULL DEFAULT now()\n)\n`\n\n/**\n * Apply all pending migrations in lexical order.\n *\n * Each unapplied file is run in its own transaction together with the\n * bookkeeping INSERT, so a failed migration rolls back cleanly and leaves\n * `schema_migrations` consistent.\n *\n * @param {import('../types.js').DbHandle} db A DbHandle from {@link createDb}.\n * @param {object} [opts]\n * @param {string} [opts.dir] Override the migrations directory (testing).\n * @returns {Promise<string[]>} The filenames that were applied this run (in\n * order). Empty array when the schema is already up to date.\n */\nexport async function runMigrations(db, opts) {\n if (!db) {\n throw new Error('runMigrations: no DbHandle (DATABASE_URL not configured?)')\n }\n const dir = (opts && opts.dir) || MIGRATIONS_DIR\n\n // Ensure the ledger table exists before we read it.\n await db.query(SCHEMA_MIGRATIONS_DDL)\n\n let entries\n try {\n entries = await readdir(dir)\n } catch (e) {\n if (e && e.code === 'ENOENT') return []\n throw e\n }\n\n const files = entries\n .filter((f) => f.toLowerCase().endsWith('.sql'))\n .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))\n\n const appliedRes = await db.query('SELECT filename FROM schema_migrations')\n const already = new Set((appliedRes.rows || []).map((r) => r.filename))\n\n /** @type {string[]} */\n const applied = []\n\n for (const file of files) {\n if (already.has(file)) continue\n const sql = await readFile(join(dir, file), 'utf8')\n\n // Apply the migration body + its ledger row atomically.\n await db.tx(async (client) => {\n await client.query(sql)\n await client.query('INSERT INTO schema_migrations (filename) VALUES ($1)', [file])\n })\n\n applied.push(file)\n // eslint-disable-next-line no-console\n console.log(`[db] applied migration ${file}`)\n }\n\n return applied\n}\n\nexport default { runMigrations, MIGRATIONS_DIR }\n", "/**\n * Postgres durable access layer for the hosted bridge.\n *\n * ZERO-NEW-DEPS LOCAL MODE IS SACRED: `pg` is imported **dynamically**, and\n * ONLY when `config.databaseUrl` is set. When no DATABASE_URL is configured,\n * {@link createDb} returns `null` \u2014 exactly today's local-mode behavior with no\n * Postgres and no `pg` dependency required. Never static-import `pg` at module\n * top: this file is reachable from src/index.js / src/cli.js and must keep\n * working for free users who never install `pg`.\n *\n * @module db/index\n */\n\nimport { runMigrations } from './migrate.js'\n\n/**\n * Create the Postgres durable layer over a connection pool.\n *\n * @param {import('../types.js').Config} config Loaded config (env + sorb.config.json).\n * @returns {Promise<import('../types.js').DbHandle | null>} A DbHandle when\n * `config.databaseUrl` is set; `null` in local mode (no Postgres).\n */\nexport async function createDb(config) {\n const databaseUrl = config && config.databaseUrl\n if (!databaseUrl) {\n // Local mode: no Postgres. Caller (cli.js / server.js) treats null as\n // \"not configured\" and skips DB-dependent checks.\n return null\n }\n\n // Dynamic import \u2014 `pg` is only loaded when DATABASE_URL is present.\n const pgModule = await import('pg')\n // `pg` is a CommonJS module; the default export carries { Pool, Client, ... }.\n const pg = pgModule.default || pgModule\n const { Pool } = pg\n\n const pool = new Pool({\n connectionString: databaseUrl,\n // Keep the pool conservative; the bridge is mostly Redis-bound. These can\n // be tuned later via config if needed.\n max: 10,\n idleTimeoutMillis: 30000,\n connectionTimeoutMillis: 10000,\n })\n\n // Surface idle-client errors instead of crashing the process. catch-binding\n // style (no bare catch) per sandbox rule.\n pool.on('error', (err) => {\n // eslint-disable-next-line no-console\n console.error('[db] idle pool client error:', err && err.message ? err.message : err)\n })\n\n /**\n * Run a parameterized query against a pooled client.\n * @param {string} text SQL with `$1`-style placeholders.\n * @param {Array<*>} [params] Bound parameters.\n * @returns {Promise<import('pg').QueryResult>}\n */\n async function query(text, params) {\n return pool.query(text, params)\n }\n\n /**\n * Acquire a dedicated client for a multi-statement unit of work. The caller\n * MUST `client.release()` when done.\n * @returns {Promise<import('pg').PoolClient>}\n */\n async function getClient() {\n return pool.connect()\n }\n\n /**\n * Run `fn` inside a single transaction. Commits on success, rolls back on\n * throw, and always releases the client.\n * @template T\n * @param {(client: import('pg').PoolClient) => Promise<T>} fn\n * @returns {Promise<T>}\n */\n async function tx(fn) {\n const client = await pool.connect()\n try {\n await client.query('BEGIN')\n const result = await fn(client)\n await client.query('COMMIT')\n return result\n } catch (e) {\n try {\n await client.query('ROLLBACK')\n } catch (rollbackErr) {\n // eslint-disable-next-line no-console\n console.error('[db] rollback failed:', rollbackErr && rollbackErr.message ? rollbackErr.message : rollbackErr)\n }\n throw e\n } finally {\n client.release()\n }\n }\n\n /**\n * Readiness ping. Round-trips `SELECT 1`. Returns false (never throws) on\n * failure so /ready can report a 503 cleanly.\n * @returns {Promise<boolean>}\n */\n async function ping() {\n try {\n const res = await pool.query('SELECT 1 AS ok')\n return Boolean(res && res.rows && res.rows.length === 1)\n } catch (e) {\n // eslint-disable-next-line no-console\n console.error('[db] ping failed:', e && e.message ? e.message : e)\n return false\n }\n }\n\n /** Close the pool. Idempotent. @returns {Promise<void>} */\n let closed = false\n async function close() {\n if (closed) return\n closed = true\n try {\n await pool.end()\n } catch (e) {\n // eslint-disable-next-line no-console\n console.error('[db] close failed:', e && e.message ? e.message : e)\n }\n }\n\n /** @type {import('../types.js').DbHandle} */\n const handle = {\n pool,\n query,\n getClient,\n tx,\n ping,\n close,\n /** Apply pending SQL migrations in order. @returns {Promise<string[]>} */\n runMigrations: () => runMigrations(handle),\n }\n\n return handle\n}\n\nexport default { createDb }\n", "import chokidar from 'chokidar'\nimport { readFileSync, existsSync } from 'fs'\nimport { resolve } from 'path'\nimport pc from 'picocolors'\n\n/**\n * @typedef {Object} Watcher\n * @property {() => import('./types').TokenSet} read\n * @property {() => void} stop\n */\n\n/**\n * @param {string} tokenPath\n * @param {(tokens: import('./types').TokenSet) => void} onChange\n * @returns {Watcher}\n */\nexport const watchTokenFile = (tokenPath, onChange) => {\n const abs = resolve(process.cwd(), tokenPath)\n\n if (!existsSync(abs)) {\n console.error(pc.red(` \u2717 Token file not found: ${abs}`))\n process.exit(1)\n }\n\n const read = () => {\n try {\n return JSON.parse(readFileSync(abs, 'utf-8'))\n } catch (err) {\n console.error(pc.red(` \u2717 Failed to parse token file: ${abs}`))\n return {}\n }\n }\n\n const watcher = chokidar.watch(abs, {\n ignoreInitial: true,\n awaitWriteFinish: { stabilityThreshold: 100 },\n })\n\n watcher.on('change', () => {\n console.log(pc.cyan(` \u2192 Token file changed`))\n onChange(read())\n })\n\n watcher.on('error', (err) => {\n console.error(pc.red(` \u2717 Watcher error:`), err)\n })\n\n return {\n read,\n stop: () => watcher.close(),\n }\n}\n\n/**\n * Watch multiple token *source* files (the DTCG sets) and fire `onChange` when\n * any changes \u2014 used to re-run Style Dictionary. Unlike watchTokenFile this\n * doesn't read/serve a file and doesn't hard-exit on a missing path (it watches\n * whichever paths exist).\n *\n * @param {string[]} paths\n * @param {(changedPath: string) => void} onChange\n * @returns {{ stop: () => void }}\n */\nexport const watchSources = (paths, onChange) => {\n const abs = (paths || [])\n .map((p) => resolve(process.cwd(), p))\n .filter((p) => existsSync(p))\n\n if (!abs.length) {\n console.warn(pc.yellow(' \u26A0 No token sources found to watch.'))\n return { stop: () => {} }\n }\n\n const watcher = chokidar.watch(abs, {\n ignoreInitial: true,\n awaitWriteFinish: { stabilityThreshold: 100 },\n })\n\n watcher.on('change', (p) => {\n console.log(pc.cyan(' \u2192 Token source changed') + pc.dim(` (${p})`))\n onChange(p)\n })\n watcher.on('error', (err) => console.error(pc.red(' \u2717 Watcher error:'), err))\n\n return { stop: () => watcher.close() }\n}\n", "import { execSync } from 'child_process'\nimport { existsSync } from 'fs'\nimport { resolve } from 'path'\nimport pc from 'picocolors'\n\n/**\n * Runs a Style Dictionary build after tokens change.\n * Only runs if a config file exists \u2014 non-fatal if it doesn't.\n *\n * @param {string} configPath\n * @returns {boolean}\n */\nexport const runStyleDictionary = (configPath) => {\n const abs = resolve(process.cwd(), configPath)\n\n if (!existsSync(abs)) {\n console.warn(\n pc.yellow(` \u26A0 style-dictionary config not found at ${configPath}, skipping`),\n )\n return false\n }\n\n try {\n console.log(pc.dim(' \u2192 Running Style Dictionary...'))\n execSync(`npx style-dictionary build --config ${abs}`, {\n stdio: 'inherit',\n cwd: process.cwd(),\n })\n console.log(pc.green(' \u2713 Style Dictionary build complete'))\n return true\n } catch (e) {\n console.error(pc.red(' \u2717 Style Dictionary build failed'))\n return false\n }\n}\n", "import pc from 'picocolors'\n\n/**\n * @typedef {Object} CommitOptions\n * @property {string} owner\n * @property {string} repo\n * @property {string} tokenPath\n * @property {string} content\n * @property {string} message\n * @property {string} pat\n */\n\n/**\n * Creates a branch with the updated token file and opens a PR.\n * Called by the CLI when the designer hits \"commit\" in the Figma plugin.\n *\n * @param {CommitOptions} opts\n * @returns {Promise<string>}\n */\nexport const openTokenPR = async (opts) => {\n const base = `https://api.github.com/repos/${opts.owner}/${opts.repo}`\n const headers = {\n Authorization: `Bearer ${opts.pat}`,\n 'Content-Type': 'application/json',\n Accept: 'application/vnd.github+json',\n }\n\n // 1. Resolve HEAD SHA of main\n const mainRef = await fetch(`${base}/git/ref/heads/main`, { headers })\n .then((r) => r.json())\n const sha = mainRef.object.sha\n\n // 2. Create a branch named tokens/<timestamp>\n const branchName = `tokens/update-${Date.now()}`\n await fetch(`${base}/git/refs`, {\n method: 'POST',\n headers,\n body: JSON.stringify({\n ref: `refs/heads/${branchName}`,\n sha,\n }),\n })\n\n // 3. Get current file SHA so we can update rather than create\n const fileRes = await fetch(`${base}/contents/${opts.tokenPath}`, {\n headers,\n })\n const fileData = fileRes.ok ? await fileRes.json() : null\n\n // 4. Commit the updated token file\n await fetch(`${base}/contents/${opts.tokenPath}`, {\n method: 'PUT',\n headers,\n body: JSON.stringify({\n message: opts.message,\n content: Buffer.from(opts.content).toString('base64'),\n branch: branchName,\n ...(fileData?.sha ? { sha: fileData.sha } : {}),\n }),\n })\n\n // 5. Open PR\n const pr = await fetch(`${base}/pulls`, {\n method: 'POST',\n headers,\n body: JSON.stringify({\n title: opts.message,\n head: branchName,\n base: 'main',\n body: [\n '> \uD83C\uDFA8 Created by **Sorb**',\n '',\n 'This PR was generated from the Sorb Figma plugin.',\n 'Review the token diff and merge to apply changes to the app.',\n ].join('\\n'),\n }),\n }).then((r) => r.json())\n\n return pr.html_url\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["Redis", "tokens", "id", "url", "createRedisStore", "pc", "chokidar", "import_fs", "import_path", "import_picocolors", "pc", "import_picocolors"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
-- 001_init.sql — initial hosted-bridge schema.
|
|
2
|
+
-- Mirrors hosted-bridge-spec.md §5 "Data model (Postgres, first cut)".
|
|
3
|
+
--
|
|
4
|
+
-- Notes:
|
|
5
|
+
-- * Preview *payloads* live in Redis (TTL); this table holds metadata only.
|
|
6
|
+
-- * UUID primary keys via pgcrypto's gen_random_uuid(). Enabled below so the
|
|
7
|
+
-- migration is self-contained on a fresh database.
|
|
8
|
+
-- * Applied atomically by src/db/migrate.js inside one transaction.
|
|
9
|
+
|
|
10
|
+
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
11
|
+
|
|
12
|
+
-- organizations (id, name, created_at, stripe_customer_id, plan, status)
|
|
13
|
+
CREATE TABLE IF NOT EXISTS organizations (
|
|
14
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
15
|
+
name text NOT NULL,
|
|
16
|
+
stripe_customer_id text,
|
|
17
|
+
plan text NOT NULL DEFAULT 'free',
|
|
18
|
+
status text NOT NULL DEFAULT 'active',
|
|
19
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
-- users (id, email, name, created_at)
|
|
23
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
24
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
25
|
+
email text NOT NULL UNIQUE,
|
|
26
|
+
name text,
|
|
27
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
-- memberships (org_id, user_id, role) -- owner | admin | member
|
|
31
|
+
CREATE TABLE IF NOT EXISTS memberships (
|
|
32
|
+
org_id uuid NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
|
33
|
+
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
34
|
+
role text NOT NULL DEFAULT 'member'
|
|
35
|
+
CHECK (role IN ('owner', 'admin', 'member')),
|
|
36
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
37
|
+
PRIMARY KEY (org_id, user_id)
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
CREATE INDEX IF NOT EXISTS memberships_user_id_idx ON memberships(user_id);
|
|
41
|
+
|
|
42
|
+
-- projects (id, org_id, namespace, name, allowed_origins[], created_at)
|
|
43
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
44
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
45
|
+
org_id uuid NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
|
46
|
+
namespace text NOT NULL,
|
|
47
|
+
name text NOT NULL,
|
|
48
|
+
allowed_origins text[] NOT NULL DEFAULT '{}',
|
|
49
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
50
|
+
UNIQUE (org_id, namespace)
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
CREATE INDEX IF NOT EXISTS projects_org_id_idx ON projects(org_id);
|
|
54
|
+
|
|
55
|
+
-- api_keys (id, project_id, type, hash, last4, created_at, revoked_at)
|
|
56
|
+
-- type: secret | publishable
|
|
57
|
+
CREATE TABLE IF NOT EXISTS api_keys (
|
|
58
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
59
|
+
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
60
|
+
type text NOT NULL CHECK (type IN ('secret', 'publishable')),
|
|
61
|
+
hash text NOT NULL,
|
|
62
|
+
last4 text,
|
|
63
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
64
|
+
revoked_at timestamptz
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
CREATE INDEX IF NOT EXISTS api_keys_project_id_idx ON api_keys(project_id);
|
|
68
|
+
CREATE UNIQUE INDEX IF NOT EXISTS api_keys_hash_idx ON api_keys(hash);
|
|
69
|
+
|
|
70
|
+
-- token_commits (id, project_id, resolved_json, version, committed_by, created_at)
|
|
71
|
+
CREATE TABLE IF NOT EXISTS token_commits (
|
|
72
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
73
|
+
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
74
|
+
resolved_json jsonb NOT NULL,
|
|
75
|
+
version integer NOT NULL,
|
|
76
|
+
committed_by uuid REFERENCES users(id) ON DELETE SET NULL,
|
|
77
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
78
|
+
UNIQUE (project_id, version)
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
CREATE INDEX IF NOT EXISTS token_commits_project_id_idx ON token_commits(project_id);
|
|
82
|
+
|
|
83
|
+
-- previews (id, project_id, created_by, expires_at, created_at) -- metadata; payload in Redis
|
|
84
|
+
CREATE TABLE IF NOT EXISTS previews (
|
|
85
|
+
id text PRIMARY KEY, -- nanoid(8) minted in server.js
|
|
86
|
+
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
87
|
+
created_by uuid REFERENCES users(id) ON DELETE SET NULL,
|
|
88
|
+
expires_at timestamptz,
|
|
89
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
CREATE INDEX IF NOT EXISTS previews_project_id_idx ON previews(project_id);
|
|
93
|
+
CREATE INDEX IF NOT EXISTS previews_expires_at_idx ON previews(expires_at);
|
|
94
|
+
|
|
95
|
+
-- audit_log (id, org_id, actor, action, target, created_at)
|
|
96
|
+
CREATE TABLE IF NOT EXISTS audit_log (
|
|
97
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
98
|
+
org_id uuid REFERENCES organizations(id) ON DELETE CASCADE,
|
|
99
|
+
actor text,
|
|
100
|
+
action text NOT NULL,
|
|
101
|
+
target text,
|
|
102
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
CREATE INDEX IF NOT EXISTS audit_log_org_id_idx ON audit_log(org_id);
|
|
106
|
+
CREATE INDEX IF NOT EXISTS audit_log_created_at_idx ON audit_log(created_at);
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sorb/juice",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local token bridge server and dev tooling for Sorb — the live conduit carrying tokens across (CLI command: sorb)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"sorb",
|
|
8
|
+
"design-tokens",
|
|
9
|
+
"figma",
|
|
10
|
+
"cli",
|
|
11
|
+
"tokens"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/metatoy/sorb-juice#readme",
|
|
14
|
+
"bugs": "https://github.com/metatoy/sorb-juice/issues",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/metatoy/sorb-juice.git"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"bin": {
|
|
23
|
+
"sorb": "dist/cli.js"
|
|
24
|
+
},
|
|
25
|
+
"main": "dist/index.js",
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "node build.mjs",
|
|
31
|
+
"dev": "node build.mjs --watch",
|
|
32
|
+
"test": "node --test",
|
|
33
|
+
"prepublishOnly": "node build.mjs"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@hono/node-server": "^1.12.0",
|
|
37
|
+
"@sentry/node": "^8",
|
|
38
|
+
"chokidar": "^3.6.0",
|
|
39
|
+
"commander": "^12.0.0",
|
|
40
|
+
"hono": "^4.4.0",
|
|
41
|
+
"ioredis": "^5.4.0",
|
|
42
|
+
"nanoid": "^5.0.0",
|
|
43
|
+
"pg": "^8.13.0",
|
|
44
|
+
"picocolors": "^1.0.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"esbuild": "^0.21.0"
|
|
48
|
+
}
|
|
49
|
+
}
|