@vielzeug/archive 26.10.6 → 26.10.9
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 +5 -3
- package/dist/_crypto.cjs +2 -0
- package/dist/_crypto.cjs.map +1 -0
- package/dist/_crypto.d.ts +7 -0
- package/dist/_crypto.d.ts.map +1 -0
- package/dist/_crypto.js +2 -0
- package/dist/_crypto.js.map +1 -0
- package/dist/auth/worker.cjs +4 -4
- package/dist/auth/worker.cjs.map +1 -1
- package/dist/auth/worker.d.ts.map +1 -1
- package/dist/auth/worker.js +4 -4
- package/dist/auth/worker.js.map +1 -1
- package/dist/auth.cjs +1 -1
- package/dist/auth.d.ts +0 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +1 -1
- package/dist/collection.cjs +1 -1
- package/dist/collection.cjs.map +1 -1
- package/dist/collection.d.ts +9 -1
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +1 -1
- package/dist/collection.js.map +1 -1
- package/dist/contract.cjs.map +1 -1
- package/dist/contract.d.ts +4 -3
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js.map +1 -1
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/worker/_pagination.cjs +1 -1
- package/dist/worker/_pagination.cjs.map +1 -1
- package/dist/worker/_pagination.d.ts +20 -7
- package/dist/worker/_pagination.d.ts.map +1 -1
- package/dist/worker/_pagination.js +1 -1
- package/dist/worker/_pagination.js.map +1 -1
- package/dist/worker/env.cjs +1 -1
- package/dist/worker/env.cjs.map +1 -1
- package/dist/worker/env.d.ts.map +1 -1
- package/dist/worker/env.js +1 -1
- package/dist/worker/env.js.map +1 -1
- package/dist/worker/publications.cjs +1 -1
- package/dist/worker/publications.cjs.map +1 -1
- package/dist/worker/publications.d.ts.map +1 -1
- package/dist/worker/publications.js +1 -1
- package/dist/worker/publications.js.map +1 -1
- package/dist/worker/sql.cjs +4 -4
- package/dist/worker/sql.cjs.map +1 -1
- package/dist/worker/sql.d.ts +3 -2
- package/dist/worker/sql.d.ts.map +1 -1
- package/dist/worker/sql.js +4 -4
- package/dist/worker/sql.js.map +1 -1
- package/package.json +2 -2
package/dist/auth/worker.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker.js","names":[],"sources":["../../src/auth/worker.ts"],"sourcesContent":["import { errors, jwtVerify, SignJWT } from 'jose';\nimport {\n ACCOUNT_HEADER,\n ARCHIVE_OPERATOR_ROLE,\n type ArchiveErrorCode,\n AUTH_SCHEMA_VERSION,\n archiveAuthHealthSchema,\n archiveErrorResponseSchema,\n isWellFormedText,\n OPERATOR_SESSION_MAX_AGE_SECONDS,\n PUBLISHER_HEADER,\n REQUEST_MARKER_HEADER,\n SESSION_COOKIE_NAME,\n SESSION_MAX_AGE_SECONDS,\n sessionCookieValue,\n uuidSchema,\n} from '../contract';\nimport { serviceRequest } from '../worker/_service';\nimport {\n ApiError,\n badRequest,\n expiredSessionCookie,\n forbidden,\n json,\n noStore,\n queryParams,\n readJsonBody,\n sessionToken,\n toErrorResponse,\n unauthenticated,\n} from '../worker/http';\nimport type { ArchiveExecutionContext } from '../worker/worker';\nimport { type AuthConfig, type AuthWorkerEnv, authConfig, isWellFormedEmail } from './config';\nimport { type AuthDiagnostic, AuthOperationError } from './errors';\nimport {\n authReadiness,\n consumeToken,\n deleteAccount,\n deleteToken,\n ensureUser,\n insertToken,\n LINK_TTL_MS,\n readAccount,\n revokeSessions,\n spendLinkBudget,\n spendSignInBudget,\n sweep,\n} from './store';\n\n/**\n * The reference identity issuer: passwordless email accounts. A POST with an email\n * address ensures the account row and sends a single-use magic link; consuming it\n * signs the browser in by setting the `archive_session` cookie a catalog Worker\n * verifies — an RS256 JWT minted by the active private key, verifiable through\n * `/jwks.json` alongside any configured retired public keys. `OPERATOR_EMAILS` is\n * the one operator-membership truth; operator sessions are short-lived so a revoked\n * operator loses authority within minutes. The account routes export the issuer's\n * own record and erase it together with the catalog profile.\n */\nexport interface AuthWorkerHandler<E extends AuthWorkerEnv = AuthWorkerEnv> {\n fetch(request: Request, env: E, ctx?: ArchiveExecutionContext): Promise<Response>;\n /** The cron sweep: expired links, closed send windows and never-verified accounts. */\n scheduled(controller: unknown, env: E, ctx?: ArchiveExecutionContext): Promise<void>;\n tap(handler: (event: AuthEvent) => void, options?: { readonly signal?: AbortSignal }): () => void;\n}\n\nexport type AuthEvent =\n | {\n readonly type: 'request';\n readonly requestId: string;\n readonly route:\n | '/auth/account'\n | '/auth/jwks.json'\n | '/auth/sign-in'\n | '/auth/verify'\n | '/auth/session'\n | '/auth/sign-out-all'\n | '/auth/health'\n | 'unmatched';\n readonly status: number;\n readonly code: ArchiveErrorCode | 'ok';\n readonly ms: number;\n readonly diagnostic?: AuthDiagnostic;\n }\n | { readonly type: 'scheduled'; readonly status: 'ok'; readonly removed: number }\n | { readonly type: 'scheduled'; readonly status: 'failed'; readonly diagnostic: AuthDiagnostic };\n\nconst ROUTES = {\n account: /^\\/api\\/v1\\/auth\\/account$/,\n health: /^\\/api\\/v1\\/auth\\/health$/,\n jwks: /^\\/api\\/v1\\/auth\\/jwks\\.json$/,\n session: /^\\/api\\/v1\\/auth\\/session$/,\n signIn: /^\\/api\\/v1\\/auth\\/sign-in$/,\n signOutAll: /^\\/api\\/v1\\/auth\\/sign-out-all$/,\n verify: /^\\/api\\/v1\\/auth\\/verify$/,\n} as const;\n\nconst ROUTE_METHODS: Record<keyof typeof ROUTES, readonly string[]> = {\n account: ['GET', 'DELETE'],\n health: ['GET'],\n jwks: ['GET'],\n session: ['GET'],\n signIn: ['POST'],\n signOutAll: ['POST'],\n verify: ['GET', 'POST'],\n};\n\nconst ROUTE_LABELS = {\n account: '/auth/account',\n health: '/auth/health',\n jwks: '/auth/jwks.json',\n session: '/auth/session',\n signIn: '/auth/sign-in',\n signOutAll: '/auth/sign-out-all',\n verify: '/auth/verify',\n} as const;\n\nconst matchRoute = (request: Request): keyof typeof ROUTES | undefined =>\n (Object.keys(ROUTES) as (keyof typeof ROUTES)[]).find((id) => ROUTES[id].test(new URL(request.url).pathname));\n\nconst TOKEN_BYTES = 32;\n// Base64url of TOKEN_BYTES random bytes, unpadded.\nconst LINK_TOKEN = /^[A-Za-z0-9_-]{43}$/;\nconst PUBLISHER_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;\nconst SUBJECT_MAX = 256;\nconst CLOCK_TOLERANCE_SECONDS = 5;\n\nconst hex = (bytes: ArrayBuffer): string =>\n [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, '0')).join('');\n\nconst hashToken = async (token: string): Promise<string> =>\n hex(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(token)));\n\nconst randomToken = (): string =>\n btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(TOKEN_BYTES))))\n .replaceAll('+', '-')\n .replaceAll('/', '_')\n .replace(/=+$/, '');\n\nconst escapeHtml = (value: string): string => value.replace(/[&<>\"']/g, (char) => `&#${char.charCodeAt(0)};`);\n\nconst unavailable = (): ApiError => new AuthOperationError(503, 'catalog');\n\n/** Exact allowed Origin plus the request marker, like the catalog's mutation guard. */\nfunction requireMutationGuard(request: Request, config: AuthConfig, allowPublicOrigin = false): string {\n const origin = request.headers.get('origin');\n if (origin === null || (!config.allowedOrigins.has(origin) && !(allowPublicOrigin && origin === config.publicOrigin)))\n throw forbidden();\n if (request.headers.get(REQUEST_MARKER_HEADER) !== '1') throw forbidden();\n return origin;\n}\n\nfunction corsHeaders(request: Request, config: AuthConfig): Record<string, string> {\n const origin = request.headers.get('origin');\n if (\n origin === null ||\n (!config.allowedOrigins.has(origin) && !(matchRoute(request) === 'verify' && origin === config.publicOrigin))\n )\n return {};\n return { 'access-control-allow-credentials': 'true', 'access-control-allow-origin': origin, vary: 'Origin' };\n}\n\nfunction preflight(request: Request, config: AuthConfig, methods: readonly string[]): Response {\n const cors = corsHeaders(request, config);\n if (Object.keys(cors).length === 0) throw forbidden();\n return new Response(null, {\n headers: {\n ...cors,\n 'access-control-allow-headers': `Content-Type, ${ACCOUNT_HEADER}, ${PUBLISHER_HEADER}, ${REQUEST_MARKER_HEADER}`,\n 'access-control-allow-methods': [...methods, 'OPTIONS'].join(', '),\n 'access-control-max-age': '600',\n },\n status: 204,\n });\n}\n\nconst secure = (config: AuthConfig): boolean => config.appEnv === 'production';\n\nasync function mintSession(\n config: AuthConfig,\n subject: string,\n operator: boolean,\n sessionVersion: number,\n): Promise<{ maxAge: number; token: string }> {\n const maxAge = operator ? OPERATOR_SESSION_MAX_AGE_SECONDS : SESSION_MAX_AGE_SECONDS;\n const token = await new SignJWT({ sessionVersion, ...(operator ? { roles: [ARCHIVE_OPERATOR_ROLE] } : {}) })\n .setProtectedHeader({ alg: 'RS256', kid: config.signing.kid, typ: 'JWT' })\n .setIssuedAt()\n .setJti(crypto.randomUUID())\n .setIssuer(config.issuer)\n .setAudience(config.audience)\n .setSubject(subject)\n .setExpirationTime(`${maxAge}s`)\n .sign(config.signing.key);\n return { maxAge, token };\n}\n\n/** Verifies the request's own session cookie against every published key; returns the token and subject. */\nasync function requireSession(\n request: Request,\n config: AuthConfig,\n env: AuthWorkerEnv,\n): Promise<{ subject: string; token: string; sessionVersion: number }> {\n const token = sessionToken(request);\n if (token === null) throw unauthenticated();\n try {\n const { payload } = await jwtVerify(token, config.verifier, {\n algorithms: ['RS256'],\n audience: config.audience,\n clockTolerance: CLOCK_TOLERANCE_SECONDS,\n issuer: config.issuer,\n maxTokenAge: SESSION_MAX_AGE_SECONDS,\n requiredClaims: ['sub', 'iss', 'aud', 'iat', 'exp', 'sessionVersion'],\n });\n const subject = payload.sub ?? '';\n if (subject.length === 0 || subject.length > SUBJECT_MAX || !isWellFormedText(subject)) throw unauthenticated();\n const version = payload.sessionVersion;\n if (typeof version !== 'number' || !Number.isSafeInteger(version) || version < 0) throw unauthenticated();\n if (typeof payload.iat !== 'number' || payload.iat > Date.now() / 1000 + CLOCK_TOLERANCE_SECONDS)\n throw unauthenticated();\n if (\n Array.isArray(payload.roles) &&\n payload.roles.includes(ARCHIVE_OPERATOR_ROLE) &&\n (typeof payload.exp !== 'number' ||\n payload.exp - payload.iat > OPERATOR_SESSION_MAX_AGE_SECONDS ||\n Date.now() / 1000 - payload.iat > OPERATOR_SESSION_MAX_AGE_SECONDS + CLOCK_TOLERANCE_SECONDS)\n )\n throw unauthenticated();\n const account = await readAccount(env.AUTH_DB, subject);\n if (account === null || account.session_version !== version) throw unauthenticated();\n return { sessionVersion: version, subject, token };\n } catch (error) {\n if (error instanceof errors.JOSEError) throw unauthenticated();\n throw error;\n }\n}\n\nasync function sendLink(env: AuthWorkerEnv, config: AuthConfig, to: string, link: string): Promise<void> {\n const name = config.appName;\n const minutes = LINK_TTL_MS / 60_000;\n const notice = `The link works once and expires in ${minutes} minutes. If you did not request it, ignore this email.`;\n await env.AUTH_EMAIL.send({\n from: config.senderAddress,\n html: `<p>Open this link to sign in to ${escapeHtml(name)}:</p><p><a href=\"${escapeHtml(link)}\">${escapeHtml(link)}</a></p><p>${notice}</p>`,\n subject: `Your ${name} sign-in link`,\n text: `Open this link to sign in to ${name}:\\n\\n${link}\\n\\n${notice}`,\n to,\n });\n}\n\nasync function handleSignIn(request: Request, env: AuthWorkerEnv, config: AuthConfig): Promise<Response> {\n requireMutationGuard(request, config);\n const body = await readJsonBody(request);\n if (typeof body !== 'object' || body === null || Array.isArray(body)) throw badRequest();\n const keys = Object.keys(body);\n const requestedEmail: unknown = Reflect.get(body, 'email');\n if (keys.length !== 1 || keys[0] !== 'email' || typeof requestedEmail !== 'string') {\n throw badRequest();\n }\n const email = requestedEmail.trim().toLowerCase();\n if (!isWellFormedEmail(email)) {\n throw badRequest();\n }\n const nowMs = Date.now();\n const admissionRetry = await spendSignInBudget(env.AUTH_DB, nowMs);\n if (admissionRetry !== null) {\n throw new ApiError(429, 'rate-limited', 'Too many sign-in requests. Try again later.', {\n retryAfterSeconds: admissionRetry,\n });\n }\n const user = await ensureUser(env.AUTH_DB, email);\n const retryAfterSeconds = await spendLinkBudget(env.AUTH_DB, user.id, nowMs);\n if (retryAfterSeconds !== null) {\n throw new ApiError(429, 'rate-limited', 'Too many sign-in emails. Try again later.', { retryAfterSeconds });\n }\n const token = randomToken();\n const tokenHash = await hashToken(token);\n await insertToken(env.AUTH_DB, tokenHash, user.id, nowMs);\n try {\n await sendLink(env, config, user.email, `${config.publicOrigin}/api/v1/auth/verify?token=${token}`);\n } catch (error) {\n // An unsent link is useless; the spent allowance is deliberately not refunded.\n await deleteToken(env.AUTH_DB, tokenHash).catch(() => undefined);\n throw new AuthOperationError(503, 'email', { cause: error });\n }\n return json(202, { ok: true }, noStore);\n}\n\nasync function handleVerify(request: Request, env: AuthWorkerEnv, config: AuthConfig): Promise<Response> {\n if (request.method === 'GET') {\n const token = queryParams(new URL(request.url), ['token']).get('token') ?? '';\n if (!LINK_TOKEN.test(token)) throw badRequest('The link is not valid.');\n const nonce = randomToken();\n // No automatic redemption, external assets or referrers. Only a deliberate\n // button click makes the same-origin marked POST; scanners can safely GET.\n const html = `<!doctype html><html lang=\"en\"><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width\"><title>Confirm sign-in</title><h1>Sign in to ${escapeHtml(config.appName)}</h1><p>Continue only if you requested this sign-in link.</p><button type=\"button\" id=\"confirm\">Confirm sign-in</button><p role=\"status\" id=\"status\"></p><script nonce=\"${nonce}\">\nhistory.replaceState(null, '', location.pathname);\ndocument.getElementById('confirm').addEventListener('click', async () => {\n const button = document.getElementById('confirm');\n button.disabled = true;\n try {\n const response = await fetch(location.pathname, { method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/json', '${REQUEST_MARKER_HEADER}': '1' }, body: JSON.stringify({ token: '${token}' }) });\n if (!response.ok) throw new Error();\n location.replace(${JSON.stringify(`${config.publicOrigin}/signed-in`)});\n } catch {\n document.getElementById('status').textContent = 'Sign-in failed. Request a new link or try again.';\n button.disabled = false;\n }\n});\n</script></html>`;\n return new Response(html, {\n headers: {\n ...noStore,\n 'content-security-policy': `default-src 'none'; script-src 'nonce-${nonce}'; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'`,\n 'content-type': 'text/html; charset=utf-8',\n 'referrer-policy': 'no-referrer',\n 'x-content-type-options': 'nosniff',\n },\n });\n }\n requireMutationGuard(request, config, true);\n queryParams(new URL(request.url), []);\n const body = await readJsonBody(request);\n if (typeof body !== 'object' || body === null || Array.isArray(body) || Object.keys(body).length !== 1) {\n throw badRequest('The link is not valid.');\n }\n const token: unknown = Reflect.get(body, 'token');\n if (typeof token !== 'string') throw badRequest('The link is not valid.');\n if (!LINK_TOKEN.test(token)) throw badRequest('The link is not valid.');\n const user = await consumeToken(env.AUTH_DB, await hashToken(token), Date.now());\n if (user === null) throw new ApiError(410, 'gone', 'That link has expired or was already used.');\n const session = await mintSession(config, user.id, config.operatorEmails.has(user.email), user.sessionVersion);\n return new Response(null, {\n headers: {\n ...noStore,\n 'set-cookie': sessionCookieValue(session.token, { maxAgeSeconds: session.maxAge, secure: secure(config) }),\n },\n status: 204,\n });\n}\n\n/** The issuer's half of a complete export: the account record only, never tokens or budget state. */\nasync function handleExport(request: Request, env: AuthWorkerEnv, config: AuthConfig): Promise<Response> {\n const { subject } = await requireSession(request, config, env);\n const row = await readAccount(env.AUTH_DB, subject);\n if (row === null) throw unauthenticated();\n return json(\n 200,\n { createdAt: row.created_at, email: row.email, id: row.id, lastSignInAt: row.last_sign_in_at },\n noStore,\n );\n}\n\n/** Re-serializes a failed catalog response into the public envelope; anything unrecognizable is 503. */\nasync function archiveFailure(response: Response): Promise<Response> {\n const text = await response.text().catch(() => '');\n let parsed: unknown = null;\n try {\n parsed = JSON.parse(text);\n } catch {\n parsed = null;\n }\n const envelope = archiveErrorResponseSchema.safeParse(parsed);\n if (!envelope.success || response.status < 400 || response.status > 599) throw unavailable();\n const { code, message, requestId } = envelope.data.error;\n const retryAfter = response.headers.get('retry-after');\n return json(\n response.status,\n { error: { code, message, requestId } },\n retryAfter !== null && /^\\d{1,6}$/.test(retryAfter) ? { ...noStore, 'retry-after': retryAfter } : noStore,\n );\n}\n\n/**\n * Full erasure: catalog session revocation and profile erasure first (through the AUTH_ARCHIVE service binding,\n * presenting the caller's own session), then the issuer's account row, and only then\n * the cookie is expired. Any failure keeps the browser session so the same request\n * can be retried; both halves are idempotent, so a retry after a partial success\n * re-runs the catalog no-op and finishes the issuer side. The account precondition\n * pins every attempt to the confirmed issuer subject, even without a publisher.\n */\nasync function handleErase(request: Request, env: AuthWorkerEnv, config: AuthConfig): Promise<Response> {\n const origin = requireMutationGuard(request, config);\n const { subject, token } = await requireSession(request, config, env);\n const account = request.headers.get(ACCOUNT_HEADER);\n if (account === null || !uuidSchema.safeParse(account).success) throw badRequest();\n if (account !== subject) {\n throw new ApiError(409, 'account-changed', 'The signed-in account changed since this request was prepared.');\n }\n const headers = new Headers({\n cookie: `${SESSION_COOKIE_NAME}=${token}`,\n origin,\n [REQUEST_MARKER_HEADER]: '1',\n });\n const publisher = request.headers.get(PUBLISHER_HEADER);\n if (publisher !== null) {\n if (!PUBLISHER_ID.test(publisher)) throw badRequest();\n headers.set(PUBLISHER_HEADER, publisher);\n }\n let archived: Response;\n try {\n archived = await serviceRequest(\n env.AUTH_ARCHIVE,\n new Request(`${config.publicOrigin}/api/v1/me/account`, { headers, method: 'DELETE', redirect: 'manual' }),\n async (response) => {\n if (response.status === 204) {\n await response.body?.cancel();\n return response;\n }\n return archiveFailure(response);\n },\n );\n } catch (error) {\n throw new AuthOperationError(503, 'catalog', { cause: error });\n }\n if (archived.status !== 204) return archived;\n await deleteAccount(env.AUTH_DB, subject);\n return new Response(null, {\n headers: { ...noStore, 'set-cookie': expiredSessionCookie(secure(config)) },\n status: 204,\n });\n}\n\nasync function handleSignOutAll(request: Request, env: AuthWorkerEnv, config: AuthConfig): Promise<Response> {\n requireMutationGuard(request, config);\n const { subject, sessionVersion } = await requireSession(request, config, env);\n const account = request.headers.get(ACCOUNT_HEADER);\n if (account === null || !uuidSchema.safeParse(account).success) throw badRequest();\n if (account !== subject)\n throw new ApiError(409, 'account-changed', 'The signed-in account changed since this request was prepared.');\n if ((await revokeSessions(env.AUTH_DB, subject, sessionVersion)) === null) throw unauthenticated();\n return json(200, { ok: true }, { ...noStore, 'set-cookie': expiredSessionCookie(secure(config)) });\n}\n\nasync function handleHealth(env: AuthWorkerEnv): Promise<Response> {\n let reason: 'configuration-unavailable' | 'database-unavailable' | 'schema-mismatch' | null = null;\n let dbSchemaVersion: number | null = null;\n try {\n await authConfig(env);\n } catch {\n reason = 'configuration-unavailable';\n }\n if (reason === null) {\n try {\n dbSchemaVersion = await authReadiness(env.AUTH_DB);\n if (dbSchemaVersion !== AUTH_SCHEMA_VERSION) reason = 'schema-mismatch';\n } catch {\n reason = 'database-unavailable';\n }\n }\n return json(\n reason === null ? 200 : 503,\n archiveAuthHealthSchema.parse({\n dbSchemaVersion,\n reason,\n schemaVersion: AUTH_SCHEMA_VERSION,\n status: reason === null ? 'ok' : 'degraded',\n }),\n noStore,\n );\n}\nasync function route(request: Request, env: AuthWorkerEnv): Promise<Response> {\n const method = request.method.toUpperCase();\n const routeId = matchRoute(request);\n if (routeId === undefined) throw new ApiError(404, 'not-found', 'No such route.');\n if (routeId === 'health' && method === 'GET') return handleHealth(env);\n const config = await authConfig(env);\n const cors = corsHeaders(request, config);\n if (method === 'OPTIONS') return preflight(request, config, ROUTE_METHODS[routeId]);\n if (!ROUTE_METHODS[routeId].includes(method)) {\n throw new ApiError(405, 'method-not-allowed', 'That method is not allowed here.');\n }\n let response: Response;\n switch (routeId) {\n case 'health':\n response = await handleHealth(env);\n break;\n case 'session':\n await requireSession(request, config, env);\n response = json(200, { ok: true }, noStore);\n break;\n case 'signOutAll':\n response = await handleSignOutAll(request, env, config);\n break;\n case 'jwks':\n response = json(200, { keys: config.jwks }, noStore);\n break;\n case 'signIn':\n response = await handleSignIn(request, env, config);\n break;\n case 'verify':\n response = await handleVerify(request, env, config);\n break;\n case 'account':\n response = method === 'GET' ? await handleExport(request, env, config) : await handleErase(request, env, config);\n break;\n }\n return withCors(response, cors);\n}\n\nfunction withCors(response: Response, cors: Record<string, string>): Response {\n if (Object.keys(cors).length === 0) return response;\n const next = new Response(response.body, response);\n for (const [key, value] of Object.entries(cors)) next.headers.set(key, value);\n return next;\n}\n\nconst diagnostic = (error: unknown): AuthDiagnostic =>\n error instanceof AuthOperationError ? error.authDiagnostic : { kind: 'internal' };\n\n/**\n * Builds the reference identity issuer. Route behavior is fixed; every deployment\n * difference is validated configuration — the app name, trusted origins, audience and\n * issuer claims, signing keys, operator roster, the catalog service binding, and\n * where links and redirects land.\n */\nexport function createAuthWorker<E extends AuthWorkerEnv>(): AuthWorkerHandler<E> {\n const tappers = new Set<(event: AuthEvent) => void>();\n const emit = (event: AuthEvent): void => {\n for (const handler of tappers) {\n try {\n handler(event);\n } catch {\n // Observers never change request or scheduled outcomes.\n }\n }\n };\n return {\n async fetch(request: Request, env: E): Promise<Response> {\n const requestId = crypto.randomUUID();\n const started = Date.now();\n let response: Response;\n let failure: unknown;\n try {\n response = await route(request, env);\n } catch (error) {\n failure = error;\n response = toErrorResponse(error, requestId);\n // Failures stay readable by allowed origins; config faults have no trusted origin set.\n const config = await authConfig(env).catch(() => null);\n if (config !== null) response = withCors(response, corsHeaders(request, config));\n }\n if (matchRoute(request) === 'verify') {\n response.headers.set('cache-control', 'no-store');\n response.headers.set('referrer-policy', 'no-referrer');\n response.headers.set('x-content-type-options', 'nosniff');\n if (!response.headers.has('content-security-policy')) {\n response.headers.set(\n 'content-security-policy',\n \"default-src 'none'; base-uri 'none'; frame-ancestors 'none'\",\n );\n }\n }\n if (tappers.size > 0) {\n const routeId = matchRoute(request);\n const code =\n response.status < 400\n ? 'ok'\n : routeId === 'health'\n ? 'unavailable'\n : failure instanceof ApiError\n ? failure.code\n : 'internal';\n // Account erasure may forward a catalog error response rather than throw.\n const forwardedCode =\n failure === undefined && response.status >= 400\n ? archiveErrorResponseSchema.safeParse(\n await response\n .clone()\n .json()\n .catch(() => null),\n )\n : null;\n const health =\n routeId === 'health' && response.status === 503\n ? archiveAuthHealthSchema.safeParse(\n await response\n .clone()\n .json()\n .catch(() => null),\n )\n : null;\n emit({\n code: forwardedCode?.success ? forwardedCode.data.error.code : code,\n ms: Math.max(0, Date.now() - started),\n requestId,\n route: routeId === undefined ? 'unmatched' : ROUTE_LABELS[routeId],\n status: response.status,\n type: 'request',\n ...(response.status >= 500\n ? {\n diagnostic: health?.success\n ? {\n kind:\n health.data.reason === 'configuration-unavailable'\n ? 'configuration'\n : health.data.reason === 'schema-mismatch'\n ? 'schema'\n : 'database',\n }\n : failure === undefined\n ? { kind: 'catalog' as const }\n : diagnostic(failure),\n }\n : {}),\n });\n }\n return response;\n },\n\n async scheduled(_controller: unknown, env: E): Promise<void> {\n try {\n await authConfig(env);\n const removed = await sweep(env.AUTH_DB, Date.now());\n if (tappers.size > 0) emit({ removed, status: 'ok', type: 'scheduled' });\n } catch (error) {\n if (tappers.size > 0) emit({ diagnostic: diagnostic(error), status: 'failed', type: 'scheduled' });\n throw error;\n }\n },\n tap(handler, options) {\n if (options?.signal?.aborted) return () => undefined;\n const observer = (event: AuthEvent): void => handler(event);\n const unsubscribe = (): void => {\n tappers.delete(observer);\n options?.signal?.removeEventListener('abort', unsubscribe);\n };\n tappers.add(observer);\n options?.signal?.addEventListener('abort', unsubscribe, { once: true });\n return unsubscribe;\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"worker.js","names":[],"sources":["../../src/auth/worker.ts"],"sourcesContent":["import { errors, jwtVerify, SignJWT } from 'jose';\nimport { sha256Hex } from '../_crypto';\nimport {\n ACCOUNT_HEADER,\n ARCHIVE_OPERATOR_ROLE,\n type ArchiveErrorCode,\n AUTH_SCHEMA_VERSION,\n archiveAuthHealthSchema,\n archiveErrorResponseSchema,\n isWellFormedText,\n OPERATOR_SESSION_MAX_AGE_SECONDS,\n PUBLISHER_HEADER,\n REQUEST_MARKER_HEADER,\n SESSION_COOKIE_NAME,\n SESSION_MAX_AGE_SECONDS,\n sessionCookieValue,\n uuidSchema,\n} from '../contract';\nimport { serviceRequest } from '../worker/_service';\nimport {\n ApiError,\n badRequest,\n expiredSessionCookie,\n forbidden,\n json,\n noStore,\n queryParams,\n readJsonBody,\n sessionToken,\n toErrorResponse,\n unauthenticated,\n} from '../worker/http';\nimport type { ArchiveExecutionContext } from '../worker/worker';\nimport { type AuthConfig, type AuthWorkerEnv, authConfig, isWellFormedEmail } from './config';\nimport { type AuthDiagnostic, AuthOperationError } from './errors';\nimport {\n authReadiness,\n consumeToken,\n deleteAccount,\n deleteToken,\n ensureUser,\n insertToken,\n LINK_TTL_MS,\n readAccount,\n revokeSessions,\n spendLinkBudget,\n spendSignInBudget,\n sweep,\n} from './store';\n\n/**\n * The reference identity issuer: passwordless email accounts. A POST with an email\n * address ensures the account row and sends a single-use magic link; consuming it\n * signs the browser in by setting the `archive_session` cookie a catalog Worker\n * verifies — an RS256 JWT minted by the active private key, verifiable through\n * `/jwks.json` alongside any configured retired public keys. `OPERATOR_EMAILS` is\n * the one operator-membership truth; operator sessions are short-lived so a revoked\n * operator loses authority within minutes. The account routes export the issuer's\n * own record and erase it together with the catalog profile.\n */\nexport interface AuthWorkerHandler<E extends AuthWorkerEnv = AuthWorkerEnv> {\n fetch(request: Request, env: E, ctx?: ArchiveExecutionContext): Promise<Response>;\n /** The cron sweep: expired links, closed send windows and never-verified accounts. */\n scheduled(controller: unknown, env: E, ctx?: ArchiveExecutionContext): Promise<void>;\n tap(handler: (event: AuthEvent) => void, options?: { readonly signal?: AbortSignal }): () => void;\n}\n\nexport type AuthEvent =\n | {\n readonly type: 'request';\n readonly requestId: string;\n readonly route:\n | '/auth/account'\n | '/auth/jwks.json'\n | '/auth/sign-in'\n | '/auth/verify'\n | '/auth/session'\n | '/auth/sign-out-all'\n | '/auth/health'\n | 'unmatched';\n readonly status: number;\n readonly code: ArchiveErrorCode | 'ok';\n readonly ms: number;\n readonly diagnostic?: AuthDiagnostic;\n }\n | { readonly type: 'scheduled'; readonly status: 'ok'; readonly removed: number }\n | { readonly type: 'scheduled'; readonly status: 'failed'; readonly diagnostic: AuthDiagnostic };\n\nconst ROUTES = {\n account: /^\\/api\\/v1\\/auth\\/account$/,\n health: /^\\/api\\/v1\\/auth\\/health$/,\n jwks: /^\\/api\\/v1\\/auth\\/jwks\\.json$/,\n session: /^\\/api\\/v1\\/auth\\/session$/,\n signIn: /^\\/api\\/v1\\/auth\\/sign-in$/,\n signOutAll: /^\\/api\\/v1\\/auth\\/sign-out-all$/,\n verify: /^\\/api\\/v1\\/auth\\/verify$/,\n} as const;\n\nconst ROUTE_METHODS: Record<keyof typeof ROUTES, readonly string[]> = {\n account: ['GET', 'DELETE'],\n health: ['GET'],\n jwks: ['GET'],\n session: ['GET'],\n signIn: ['POST'],\n signOutAll: ['POST'],\n verify: ['GET', 'POST'],\n};\n\nconst ROUTE_LABELS = {\n account: '/auth/account',\n health: '/auth/health',\n jwks: '/auth/jwks.json',\n session: '/auth/session',\n signIn: '/auth/sign-in',\n signOutAll: '/auth/sign-out-all',\n verify: '/auth/verify',\n} as const;\n\nconst matchRoute = (request: Request): keyof typeof ROUTES | undefined =>\n (Object.keys(ROUTES) as (keyof typeof ROUTES)[]).find((id) => ROUTES[id].test(new URL(request.url).pathname));\n\nconst TOKEN_BYTES = 32;\n// Base64url of TOKEN_BYTES random bytes, unpadded.\nconst LINK_TOKEN = /^[A-Za-z0-9_-]{43}$/;\nconst PUBLISHER_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;\nconst SUBJECT_MAX = 256;\nconst CLOCK_TOLERANCE_SECONDS = 5;\n\nconst randomToken = (): string =>\n btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(TOKEN_BYTES))))\n .replaceAll('+', '-')\n .replaceAll('/', '_')\n .replace(/=+$/, '');\n\nconst escapeHtml = (value: string): string => value.replace(/[&<>\"']/g, (char) => `&#${char.charCodeAt(0)};`);\n\nconst unavailable = (): ApiError => new AuthOperationError(503, 'catalog');\n\n/** Exact allowed Origin plus the request marker, like the catalog's mutation guard. */\nfunction requireMutationGuard(request: Request, config: AuthConfig, allowPublicOrigin = false): string {\n const origin = request.headers.get('origin');\n if (origin === null || (!config.allowedOrigins.has(origin) && !(allowPublicOrigin && origin === config.publicOrigin)))\n throw forbidden();\n if (request.headers.get(REQUEST_MARKER_HEADER) !== '1') throw forbidden();\n return origin;\n}\n\nfunction corsHeaders(request: Request, config: AuthConfig): Record<string, string> {\n const origin = request.headers.get('origin');\n if (\n origin === null ||\n (!config.allowedOrigins.has(origin) && !(matchRoute(request) === 'verify' && origin === config.publicOrigin))\n )\n return {};\n return { 'access-control-allow-credentials': 'true', 'access-control-allow-origin': origin, vary: 'Origin' };\n}\n\nfunction preflight(request: Request, config: AuthConfig, methods: readonly string[]): Response {\n const cors = corsHeaders(request, config);\n if (Object.keys(cors).length === 0) throw forbidden();\n return new Response(null, {\n headers: {\n ...cors,\n 'access-control-allow-headers': `Content-Type, ${ACCOUNT_HEADER}, ${PUBLISHER_HEADER}, ${REQUEST_MARKER_HEADER}`,\n 'access-control-allow-methods': [...methods, 'OPTIONS'].join(', '),\n 'access-control-max-age': '600',\n },\n status: 204,\n });\n}\n\nconst secure = (config: AuthConfig): boolean => config.appEnv === 'production';\n\nasync function mintSession(\n config: AuthConfig,\n subject: string,\n operator: boolean,\n sessionVersion: number,\n): Promise<{ maxAge: number; token: string }> {\n const maxAge = operator ? OPERATOR_SESSION_MAX_AGE_SECONDS : SESSION_MAX_AGE_SECONDS;\n const token = await new SignJWT({ sessionVersion, ...(operator ? { roles: [ARCHIVE_OPERATOR_ROLE] } : {}) })\n .setProtectedHeader({ alg: 'RS256', kid: config.signing.kid, typ: 'JWT' })\n .setIssuedAt()\n .setJti(crypto.randomUUID())\n .setIssuer(config.issuer)\n .setAudience(config.audience)\n .setSubject(subject)\n .setExpirationTime(`${maxAge}s`)\n .sign(config.signing.key);\n return { maxAge, token };\n}\n\n/** Verifies the request's own session cookie against every published key; returns the token and subject. */\nasync function requireSession(\n request: Request,\n config: AuthConfig,\n env: AuthWorkerEnv,\n): Promise<{ subject: string; token: string; sessionVersion: number }> {\n const token = sessionToken(request);\n if (token === null) throw unauthenticated();\n try {\n const { payload } = await jwtVerify(token, config.verifier, {\n algorithms: ['RS256'],\n audience: config.audience,\n clockTolerance: CLOCK_TOLERANCE_SECONDS,\n issuer: config.issuer,\n maxTokenAge: SESSION_MAX_AGE_SECONDS,\n requiredClaims: ['sub', 'iss', 'aud', 'iat', 'exp', 'sessionVersion'],\n });\n const subject = payload.sub ?? '';\n if (subject.length === 0 || subject.length > SUBJECT_MAX || !isWellFormedText(subject)) throw unauthenticated();\n const version = payload.sessionVersion;\n if (typeof version !== 'number' || !Number.isSafeInteger(version) || version < 0) throw unauthenticated();\n if (typeof payload.iat !== 'number' || payload.iat > Date.now() / 1000 + CLOCK_TOLERANCE_SECONDS)\n throw unauthenticated();\n if (\n Array.isArray(payload.roles) &&\n payload.roles.includes(ARCHIVE_OPERATOR_ROLE) &&\n (typeof payload.exp !== 'number' ||\n payload.exp - payload.iat > OPERATOR_SESSION_MAX_AGE_SECONDS ||\n Date.now() / 1000 - payload.iat > OPERATOR_SESSION_MAX_AGE_SECONDS + CLOCK_TOLERANCE_SECONDS)\n )\n throw unauthenticated();\n const account = await readAccount(env.AUTH_DB, subject);\n if (account === null || account.session_version !== version) throw unauthenticated();\n return { sessionVersion: version, subject, token };\n } catch (error) {\n if (error instanceof errors.JOSEError) throw unauthenticated();\n throw error;\n }\n}\n\nasync function sendLink(env: AuthWorkerEnv, config: AuthConfig, to: string, link: string): Promise<void> {\n const name = config.appName;\n const minutes = LINK_TTL_MS / 60_000;\n const notice = `The link works once and expires in ${minutes} minutes. If you did not request it, ignore this email.`;\n await env.AUTH_EMAIL.send({\n from: config.senderAddress,\n html: `<p>Open this link to sign in to ${escapeHtml(name)}:</p><p><a href=\"${escapeHtml(link)}\">${escapeHtml(link)}</a></p><p>${notice}</p>`,\n subject: `Your ${name} sign-in link`,\n text: `Open this link to sign in to ${name}:\\n\\n${link}\\n\\n${notice}`,\n to,\n });\n}\n\nasync function handleSignIn(request: Request, env: AuthWorkerEnv, config: AuthConfig): Promise<Response> {\n requireMutationGuard(request, config);\n const body = await readJsonBody(request);\n if (typeof body !== 'object' || body === null || Array.isArray(body)) throw badRequest();\n const keys = Object.keys(body);\n const requestedEmail: unknown = Reflect.get(body, 'email');\n if (keys.length !== 1 || keys[0] !== 'email' || typeof requestedEmail !== 'string') {\n throw badRequest();\n }\n const email = requestedEmail.trim().toLowerCase();\n if (!isWellFormedEmail(email)) {\n throw badRequest();\n }\n const nowMs = Date.now();\n const admissionRetry = await spendSignInBudget(env.AUTH_DB, nowMs);\n if (admissionRetry !== null) {\n throw new ApiError(429, 'rate-limited', 'Too many sign-in requests. Try again later.', {\n retryAfterSeconds: admissionRetry,\n });\n }\n const user = await ensureUser(env.AUTH_DB, email);\n const retryAfterSeconds = await spendLinkBudget(env.AUTH_DB, user.id, nowMs);\n if (retryAfterSeconds !== null) {\n throw new ApiError(429, 'rate-limited', 'Too many sign-in emails. Try again later.', { retryAfterSeconds });\n }\n const token = randomToken();\n const tokenHash = await sha256Hex(token);\n await insertToken(env.AUTH_DB, tokenHash, user.id, nowMs);\n try {\n await sendLink(env, config, user.email, `${config.publicOrigin}/api/v1/auth/verify?token=${token}`);\n } catch (error) {\n // An unsent link is useless; the spent allowance is deliberately not refunded.\n await deleteToken(env.AUTH_DB, tokenHash).catch(() => undefined);\n throw new AuthOperationError(503, 'email', { cause: error });\n }\n return json(202, { ok: true }, noStore);\n}\n\nasync function handleVerify(request: Request, env: AuthWorkerEnv, config: AuthConfig): Promise<Response> {\n if (request.method === 'GET') {\n const token = queryParams(new URL(request.url), ['token']).get('token') ?? '';\n if (!LINK_TOKEN.test(token)) throw badRequest('The link is not valid.');\n const nonce = randomToken();\n // No automatic redemption, external assets or referrers. Only a deliberate\n // button click makes the same-origin marked POST; scanners can safely GET.\n const html = `<!doctype html><html lang=\"en\"><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width\"><title>Confirm sign-in</title><h1>Sign in to ${escapeHtml(config.appName)}</h1><p>Continue only if you requested this sign-in link.</p><button type=\"button\" id=\"confirm\">Confirm sign-in</button><p role=\"status\" id=\"status\"></p><script nonce=\"${nonce}\">\nhistory.replaceState(null, '', location.pathname);\ndocument.getElementById('confirm').addEventListener('click', async () => {\n const button = document.getElementById('confirm');\n button.disabled = true;\n try {\n const response = await fetch(location.pathname, { method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/json', '${REQUEST_MARKER_HEADER}': '1' }, body: JSON.stringify({ token: '${token}' }) });\n if (!response.ok) throw new Error();\n location.replace(${JSON.stringify(`${config.publicOrigin}/signed-in`)});\n } catch {\n document.getElementById('status').textContent = 'Sign-in failed. Request a new link or try again.';\n button.disabled = false;\n }\n});\n</script></html>`;\n return new Response(html, {\n headers: {\n ...noStore,\n 'content-security-policy': `default-src 'none'; script-src 'nonce-${nonce}'; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'`,\n 'content-type': 'text/html; charset=utf-8',\n 'referrer-policy': 'no-referrer',\n 'x-content-type-options': 'nosniff',\n },\n });\n }\n requireMutationGuard(request, config, true);\n queryParams(new URL(request.url), []);\n const body = await readJsonBody(request);\n if (typeof body !== 'object' || body === null || Array.isArray(body) || Object.keys(body).length !== 1) {\n throw badRequest('The link is not valid.');\n }\n const token: unknown = Reflect.get(body, 'token');\n if (typeof token !== 'string') throw badRequest('The link is not valid.');\n if (!LINK_TOKEN.test(token)) throw badRequest('The link is not valid.');\n const user = await consumeToken(env.AUTH_DB, await sha256Hex(token), Date.now());\n if (user === null) throw new ApiError(410, 'gone', 'That link has expired or was already used.');\n const session = await mintSession(config, user.id, config.operatorEmails.has(user.email), user.sessionVersion);\n return new Response(null, {\n headers: {\n ...noStore,\n 'set-cookie': sessionCookieValue(session.token, { maxAgeSeconds: session.maxAge, secure: secure(config) }),\n },\n status: 204,\n });\n}\n\n/** The issuer's half of a complete export: the account record only, never tokens or budget state. */\nasync function handleExport(request: Request, env: AuthWorkerEnv, config: AuthConfig): Promise<Response> {\n const { subject } = await requireSession(request, config, env);\n const row = await readAccount(env.AUTH_DB, subject);\n if (row === null) throw unauthenticated();\n return json(\n 200,\n { createdAt: row.created_at, email: row.email, id: row.id, lastSignInAt: row.last_sign_in_at },\n noStore,\n );\n}\n\n/** Re-serializes a failed catalog response into the public envelope; anything unrecognizable is 503. */\nasync function archiveFailure(response: Response): Promise<Response> {\n const text = await response.text().catch(() => '');\n let parsed: unknown = null;\n try {\n parsed = JSON.parse(text);\n } catch {\n parsed = null;\n }\n const envelope = archiveErrorResponseSchema.safeParse(parsed);\n if (!envelope.success || response.status < 400 || response.status > 599) throw unavailable();\n const { code, message, requestId } = envelope.data.error;\n const retryAfter = response.headers.get('retry-after');\n return json(\n response.status,\n { error: { code, message, requestId } },\n retryAfter !== null && /^\\d{1,6}$/.test(retryAfter) ? { ...noStore, 'retry-after': retryAfter } : noStore,\n );\n}\n\n/**\n * Full erasure: catalog session revocation and profile erasure first (through the AUTH_ARCHIVE service binding,\n * presenting the caller's own session), then the issuer's account row, and only then\n * the cookie is expired. Any failure keeps the browser session so the same request\n * can be retried; both halves are idempotent, so a retry after a partial success\n * re-runs the catalog no-op and finishes the issuer side. The account precondition\n * pins every attempt to the confirmed issuer subject, even without a publisher.\n */\nasync function handleErase(request: Request, env: AuthWorkerEnv, config: AuthConfig): Promise<Response> {\n const origin = requireMutationGuard(request, config);\n const { subject, token } = await requireSession(request, config, env);\n const account = request.headers.get(ACCOUNT_HEADER);\n if (account === null || !uuidSchema.safeParse(account).success) throw badRequest();\n if (account !== subject) {\n throw new ApiError(409, 'account-changed', 'The signed-in account changed since this request was prepared.');\n }\n const headers = new Headers({\n cookie: `${SESSION_COOKIE_NAME}=${token}`,\n origin,\n [REQUEST_MARKER_HEADER]: '1',\n });\n const publisher = request.headers.get(PUBLISHER_HEADER);\n if (publisher !== null) {\n if (!PUBLISHER_ID.test(publisher)) throw badRequest();\n headers.set(PUBLISHER_HEADER, publisher);\n }\n let archived: Response;\n try {\n archived = await serviceRequest(\n env.AUTH_ARCHIVE,\n new Request(`${config.publicOrigin}/api/v1/me/account`, { headers, method: 'DELETE', redirect: 'manual' }),\n async (response) => {\n if (response.status === 204) {\n await response.body?.cancel();\n return response;\n }\n return archiveFailure(response);\n },\n );\n } catch (error) {\n throw new AuthOperationError(503, 'catalog', { cause: error });\n }\n if (archived.status !== 204) return archived;\n await deleteAccount(env.AUTH_DB, subject);\n return new Response(null, {\n headers: { ...noStore, 'set-cookie': expiredSessionCookie(secure(config)) },\n status: 204,\n });\n}\n\nasync function handleSignOutAll(request: Request, env: AuthWorkerEnv, config: AuthConfig): Promise<Response> {\n requireMutationGuard(request, config);\n const { subject, sessionVersion } = await requireSession(request, config, env);\n const account = request.headers.get(ACCOUNT_HEADER);\n if (account === null || !uuidSchema.safeParse(account).success) throw badRequest();\n if (account !== subject)\n throw new ApiError(409, 'account-changed', 'The signed-in account changed since this request was prepared.');\n if ((await revokeSessions(env.AUTH_DB, subject, sessionVersion)) === null) throw unauthenticated();\n return json(200, { ok: true }, { ...noStore, 'set-cookie': expiredSessionCookie(secure(config)) });\n}\n\nasync function handleHealth(env: AuthWorkerEnv): Promise<Response> {\n let reason: 'configuration-unavailable' | 'database-unavailable' | 'schema-mismatch' | null = null;\n let dbSchemaVersion: number | null = null;\n try {\n await authConfig(env);\n } catch {\n reason = 'configuration-unavailable';\n }\n if (reason === null) {\n try {\n dbSchemaVersion = await authReadiness(env.AUTH_DB);\n if (dbSchemaVersion !== AUTH_SCHEMA_VERSION) reason = 'schema-mismatch';\n } catch {\n reason = 'database-unavailable';\n }\n }\n return json(\n reason === null ? 200 : 503,\n archiveAuthHealthSchema.parse({\n dbSchemaVersion,\n reason,\n schemaVersion: AUTH_SCHEMA_VERSION,\n status: reason === null ? 'ok' : 'degraded',\n }),\n noStore,\n );\n}\nasync function route(request: Request, env: AuthWorkerEnv): Promise<Response> {\n const method = request.method.toUpperCase();\n const routeId = matchRoute(request);\n if (routeId === undefined) throw new ApiError(404, 'not-found', 'No such route.');\n if (routeId === 'health' && method === 'GET') return handleHealth(env);\n const config = await authConfig(env);\n const cors = corsHeaders(request, config);\n if (method === 'OPTIONS') return preflight(request, config, ROUTE_METHODS[routeId]);\n if (!ROUTE_METHODS[routeId].includes(method)) {\n throw new ApiError(405, 'method-not-allowed', 'That method is not allowed here.');\n }\n let response: Response;\n switch (routeId) {\n case 'health':\n response = await handleHealth(env);\n break;\n case 'session':\n await requireSession(request, config, env);\n response = json(200, { ok: true }, noStore);\n break;\n case 'signOutAll':\n response = await handleSignOutAll(request, env, config);\n break;\n case 'jwks':\n response = json(200, { keys: config.jwks }, noStore);\n break;\n case 'signIn':\n response = await handleSignIn(request, env, config);\n break;\n case 'verify':\n response = await handleVerify(request, env, config);\n break;\n case 'account':\n response = method === 'GET' ? await handleExport(request, env, config) : await handleErase(request, env, config);\n break;\n }\n return withCors(response, cors);\n}\n\nfunction withCors(response: Response, cors: Record<string, string>): Response {\n if (Object.keys(cors).length === 0) return response;\n const next = new Response(response.body, response);\n for (const [key, value] of Object.entries(cors)) next.headers.set(key, value);\n return next;\n}\n\nconst diagnostic = (error: unknown): AuthDiagnostic =>\n error instanceof AuthOperationError ? error.authDiagnostic : { kind: 'internal' };\n\n/**\n * Builds the reference identity issuer. Route behavior is fixed; every deployment\n * difference is validated configuration — the app name, trusted origins, audience and\n * issuer claims, signing keys, operator roster, the catalog service binding, and\n * where links and redirects land.\n */\nexport function createAuthWorker<E extends AuthWorkerEnv>(): AuthWorkerHandler<E> {\n const tappers = new Set<(event: AuthEvent) => void>();\n const emit = (event: AuthEvent): void => {\n for (const handler of tappers) {\n try {\n handler(event);\n } catch {\n // Observers never change request or scheduled outcomes.\n }\n }\n };\n return {\n async fetch(request: Request, env: E): Promise<Response> {\n const requestId = crypto.randomUUID();\n const started = Date.now();\n let response: Response;\n let failure: unknown;\n try {\n response = await route(request, env);\n } catch (error) {\n failure = error;\n response = toErrorResponse(error, requestId);\n // Failures stay readable by allowed origins; config faults have no trusted origin set.\n const config = await authConfig(env).catch(() => null);\n if (config !== null) response = withCors(response, corsHeaders(request, config));\n }\n if (matchRoute(request) === 'verify') {\n response.headers.set('cache-control', 'no-store');\n response.headers.set('referrer-policy', 'no-referrer');\n response.headers.set('x-content-type-options', 'nosniff');\n if (!response.headers.has('content-security-policy')) {\n response.headers.set(\n 'content-security-policy',\n \"default-src 'none'; base-uri 'none'; frame-ancestors 'none'\",\n );\n }\n }\n if (tappers.size > 0) {\n const routeId = matchRoute(request);\n const code =\n response.status < 400\n ? 'ok'\n : routeId === 'health'\n ? 'unavailable'\n : failure instanceof ApiError\n ? failure.code\n : 'internal';\n // Account erasure may forward a catalog error response rather than throw.\n const forwardedCode =\n failure === undefined && response.status >= 400\n ? archiveErrorResponseSchema.safeParse(\n await response\n .clone()\n .json()\n .catch(() => null),\n )\n : null;\n const health =\n routeId === 'health' && response.status === 503\n ? archiveAuthHealthSchema.safeParse(\n await response\n .clone()\n .json()\n .catch(() => null),\n )\n : null;\n emit({\n code: forwardedCode?.success ? forwardedCode.data.error.code : code,\n ms: Math.max(0, Date.now() - started),\n requestId,\n route: routeId === undefined ? 'unmatched' : ROUTE_LABELS[routeId],\n status: response.status,\n type: 'request',\n ...(response.status >= 500\n ? {\n diagnostic: health?.success\n ? {\n kind:\n health.data.reason === 'configuration-unavailable'\n ? 'configuration'\n : health.data.reason === 'schema-mismatch'\n ? 'schema'\n : 'database',\n }\n : failure === undefined\n ? { kind: 'catalog' as const }\n : diagnostic(failure),\n }\n : {}),\n });\n }\n return response;\n },\n\n async scheduled(_controller: unknown, env: E): Promise<void> {\n try {\n await authConfig(env);\n const removed = await sweep(env.AUTH_DB, Date.now());\n if (tappers.size > 0) emit({ removed, status: 'ok', type: 'scheduled' });\n } catch (error) {\n if (tappers.size > 0) emit({ diagnostic: diagnostic(error), status: 'failed', type: 'scheduled' });\n throw error;\n }\n },\n tap(handler, options) {\n if (options?.signal?.aborted) return () => undefined;\n const observer = (event: AuthEvent): void => handler(event);\n const unsubscribe = (): void => {\n tappers.delete(observer);\n options?.signal?.removeEventListener('abort', unsubscribe);\n };\n tappers.add(observer);\n options?.signal?.addEventListener('abort', unsubscribe, { once: true });\n return unsubscribe;\n },\n };\n}\n"],"mappings":"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"}
|
package/dist/auth.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./auth/worker.cjs");exports.createAuthWorker=e.createAuthWorker;
|
package/dist/auth.d.ts
CHANGED
|
@@ -2,5 +2,4 @@ export type { AuthArchiveBinding, AuthWorkerEnv, SendEmail } from './auth/config
|
|
|
2
2
|
export type { AuthDiagnostic } from './auth/errors';
|
|
3
3
|
export type { AuthEvent, AuthWorkerHandler } from './auth/worker';
|
|
4
4
|
export { createAuthWorker } from './auth/worker';
|
|
5
|
-
export { OPERATOR_SESSION_MAX_AGE_SECONDS } from './contract';
|
|
6
5
|
//# sourceMappingURL=auth.d.ts.map
|
package/dist/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAGA,YAAY,EAAE,kBAAkB,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAClF,YAAY,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,YAAY,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClE,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAGA,YAAY,EAAE,kBAAkB,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAClF,YAAY,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,YAAY,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClE,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/auth.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{createAuthWorker as e}from"./auth/worker.js";export{e as createAuthWorker};
|
package/dist/collection.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./
|
|
1
|
+
const e=require("./_crypto.cjs"),t=require("./contract.cjs"),n=require("./_json.cjs"),r=require("./errors.cjs");let i=require("@vielzeug/spell");var envelopeShape=(e,n)=>({author:t.archiveAuthorSchema,contentHash:t.hex64Schema,contentVersion:i.s.literal(n),id:t.uuidSchema,likeCount:i.s.number().int().min(0).max(2**53-1),payload:e,publishedAt:t.isoTimestampSchema,revision:i.s.number().int().min(1).max(2**53-1),schemaVersion:i.s.literal(1),updatedAt:t.isoTimestampSchema});function defineArchiveCollection(a){let{canonicalize:o,contentVersion:s,index:c,name:l,payloadSchema:u}=a;if(typeof c!=`function`||!u||typeof u.safeParse!=`function`||o!==void 0&&typeof o!=`function`)throw new r.ArchiveDefinitionError(`Collection requires a payload schema and synchronous indexing/canonicalization functions.`);if(a.filters!==void 0&&!Array.isArray(a.filters)||a.tags!==void 0&&!Array.isArray(a.tags))throw new r.ArchiveDefinitionError(`Collection vocabularies must be arrays.`);let d=Object.freeze([...a.filters??[]]),f=Object.freeze([...a.tags??[]]);if(typeof l!=`string`||!t.ARCHIVE_COLLECTION_NAME_PATTERN.test(l))throw new r.ArchiveDefinitionError(`Archive collection name must match [a-z0-9-]{1,32}: ${JSON.stringify(l)}`);if(!Number.isSafeInteger(s)||s<1)throw new r.ArchiveDefinitionError(`Archive collection ${l}: contentVersion must be a safe integer >= 1.`);let validateVocabulary=(e,n)=>{for(let i of n)if(typeof i!=`string`||i.length===0||i.length>t.ARCHIVE_LIMITS.vocabChars||!t.isWellFormedText(i))throw new r.ArchiveDefinitionError(`Archive collection ${l}: every ${e} value must be well-formed text of 1..${t.ARCHIVE_LIMITS.vocabChars} chars.`)};validateVocabulary(`filters`,d),validateVocabulary(`tags`,f);let p=new Set(d),m=new Set(f),derive=async i=>{let a=`schema`;try{if(n.canonicalObjectJson(i)===null)return null;let l=u.safeParse(i);if(!l.success)return null;let d=n.canonicalObjectJson(l.data,!0,i);if(d===null)throw new r.ArchiveDerivationError(a,`Payload schema produced invalid or lossy JSON.`);a=`canonicalize`;let f=o===void 0?l.data:o(l.data),h=n.canonicalObjectJson(f,!0,JSON.parse(d));if(h===null)throw new r.ArchiveDerivationError(a,`Canonicalization produced invalid JSON.`);a=`schema`;let g=u.safeParse(JSON.parse(h));if(!g.success)throw new r.ArchiveDerivationError(o===void 0?`schema`:`canonicalize`,`Canonical payload is outside the payload schema.`,{cause:g.error});let _=n.canonicalObjectJson(g.data,!0,JSON.parse(h));if(_===null||_!==h)throw new r.ArchiveDerivationError(a,`Payload schema is not stable on canonical JSON.`);if(o!==void 0){let e=u.parse(JSON.parse(_));a=`canonicalize`;let t=o(e);if(n.canonicalObjectJson(t,!0)!==_)throw new r.ArchiveDerivationError(a,`Canonicalization is not idempotent.`)}a=`index`;let{filter:v,tags:y,title:b}=c(g.data);if(n.canonicalObjectJson(g.data,!0)!==_)throw new r.ArchiveDerivationError(a,`Indexing mutated the canonical payload.`);if(typeof b!=`string`||!Array.isArray(y))throw new r.ArchiveDerivationError(a,`Indexing must return a string title and a tags array.`);let x=b.trim();if(x.length===0||x.length>t.ARCHIVE_LIMITS.titleChars||!t.isWellFormedText(x))return null;if(v!==null&&(typeof v!=`string`||!t.isWellFormedText(v)))throw new r.ArchiveDerivationError(a,`Indexing returned an invalid filter.`);let S=[];for(let e of y){if(typeof e!=`string`||!t.isWellFormedText(e))throw new r.ArchiveDerivationError(a,`Indexing returned an invalid tag.`);S.includes(e)||S.push(e)}if(v!==null&&!p.has(v)||S.some(e=>!m.has(e)))throw new r.ArchiveDerivationError(a,`Indexing returned a value outside the declared vocabularies.`);return S.length>t.ARCHIVE_LIMITS.tagsPerEntry||new TextEncoder().encode(_).byteLength>t.ARCHIVE_LIMITS.payloadBytes?null:(a=`hash`,{canonicalJson:_,contentHash:await e.sha256Hex(`{"contentVersion":${s},"payload":${_}}`),contentVersion:s,filter:v,tags:S,title:x})}catch(e){throw e instanceof r.ArchiveDerivationError?e:new r.ArchiveDerivationError(a,`Collection content derivation failed.`,{cause:e})}},h=i.s.object(envelopeShape(u,s)),g=i.s.object({...envelopeShape(u,s),clientKey:t.uuidSchema,hidden:i.s.boolean()}),_={entry:h,ownedEntries:i.s.object({entries:i.s.array(g)}),ownedEntry:g,page:i.s.object({cursor:i.s.string().nullable(),entries:i.s.array(h)}),publishInput:i.s.object({clientKey:t.uuidSchema,payload:u}),updateInput:i.s.object({expectedRevision:i.s.number().int().min(1).max(2**53-1),payload:u})};return Object.freeze({contentVersion:s,contract:Object.freeze(_),derive,filters:d,name:l,tags:f})}exports.defineArchiveCollection=defineArchiveCollection;
|
|
2
2
|
//# sourceMappingURL=collection.cjs.map
|
package/dist/collection.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection.cjs","names":[],"sources":["../src/collection.ts"],"sourcesContent":["import { Schema, s } from '@vielzeug/spell';\nimport { canonicalObjectJson } from './_json';\nimport {\n ARCHIVE_COLLECTION_NAME_PATTERN,\n ARCHIVE_CONTRACT_VERSION,\n ARCHIVE_LIMITS,\n type ArchivePage,\n archiveAuthorSchema,\n hex64Schema,\n isoTimestampSchema,\n isWellFormedText,\n uuidSchema,\n} from './contract';\nimport { ArchiveDefinitionError, ArchiveDerivationError, type ArchiveDerivationStage } from './errors';\n\n/**\n * The one seam between a deployment's content and the archive core. A collection\n * declares its payload schema and derives its list dimensions; the core owns\n * everything else — parsing, canonical serialization, the content hash, the emitted\n * envelope schemas, and every storage bound. Collections never hash, never size,\n * and never hand-write the envelope.\n */\n\n/** What `index` derives from one validated payload: the list dimensions plus the FTS title. */\nexport interface ArchiveIndexing<F extends string = string, T extends string = string> {\n /** One scalar-filter value from the declared vocabulary, or null for \"no filter\". */\n filter: F | null;\n /** Tag values from the declared vocabulary; duplicates collapse in first-seen order. */\n tags: readonly T[];\n /** The searchable title; the core trims and bounds it. */\n title: string;\n}\n\n/** The definition a deployment writes; `defineArchiveCollection` derives the rest. */\nexport interface ArchiveCollectionDefinition<\n P extends object,\n C extends number,\n F extends string = never,\n T extends string = never,\n> {\n /**\n * Normalizes a parsed payload to its storage form — fixed key order, sorted\n * order-insensitive lists — so equivalent content hashes identically. Omit when\n * the payload is already canonical; a result that leaves the payload schema is\n * a derivation fault at derive time, never stored. Must be idempotent.\n */\n canonicalize?(payload: P): P;\n /** The collection's payload-format version; stored and re-emitted with every entry. */\n contentVersion: C;\n /** Valid scalar-filter values; a query param outside the vocabulary is 400. */\n filters?: readonly F[];\n /** Derives the list dimensions and the searchable title from the canonical payload. */\n index(payload: P): ArchiveIndexing<NoInfer<F>, NoInfer<T>>;\n /** The URL + storage dimension this collection owns: lowercase `[a-z0-9-]`, at most 32 chars. */\n name: string;\n /** The strict payload schema: the whole content contract, parsed before anything derives. */\n payloadSchema: Schema<P, unknown>;\n /** Valid tag values. */\n tags?: readonly T[];\n}\n\n/** Everything the archive stores and re-identifies by, derived once per publish and update. */\nexport interface ArchiveDerivation {\n /** The stored bytes: the canonical payload, serialized with fixed key order. */\n canonicalJson: string;\n /** SHA-256 hex over `{contentVersion, payload}`: identical content hashes identically. */\n contentHash: string;\n /** The collection's declared payload-format version. */\n contentVersion: number;\n filter: string | null;\n tags: readonly string[];\n title: string;\n}\n\n/** The envelope fields every emitted entry carries; the payload stays the collection's. */\nexport interface ArchiveEntry<P = unknown, C extends number = number> {\n author: { id: string; name: string };\n contentHash: string;\n /** The collection's payload-format version, literal-pinned by the definition. */\n contentVersion: C;\n id: string;\n likeCount: number;\n payload: P;\n publishedAt: string;\n revision: number;\n /** The archive wire-contract version, pinned by the core. */\n schemaVersion: typeof ARCHIVE_CONTRACT_VERSION;\n updatedAt: string;\n}\n\n/** The private owner view: the emitted entry plus the create retry key and moderation state. */\nexport interface ArchiveOwnedEntry<P = unknown, C extends number = number> extends ArchiveEntry<P, C> {\n /** The create retry key; public routes never emit it. */\n clientKey: string;\n /** True while an operator has removed the entry from public view (still owned, frozen for edits). */\n hidden: boolean;\n}\n\n/**\n * The wire schemas one collection shares with its client: every request and response\n * envelope for this collection's routes, composed from the payload schema, so the\n * client and the Worker parse the same bytes with the same rules.\n */\nexport interface ArchiveContract<P = unknown, C extends number = number> {\n /** The public entry emitted by list, detail and publish/update responses. */\n entry: Schema<ArchiveEntry<P, C>>;\n /** The own-publications response: `{ entries: ArchiveOwnedEntry[] }`. */\n ownedEntries: Schema<{ entries: ArchiveOwnedEntry<P, C>[] }>;\n /** The private owner entry — the public entry plus the create retry key. */\n ownedEntry: Schema<ArchiveOwnedEntry<P, C>>;\n /** One list page: entries plus the forward/backward cursors around it. */\n page: Schema<ArchivePage<ArchiveEntry<P, C>>>;\n /** The publish request: `{ clientKey, payload }`. */\n publishInput: Schema<{ clientKey: string; payload: P }, unknown>;\n /** The update request: `{ expectedRevision, payload }`. */\n updateInput: Schema<{ expectedRevision: number; payload: P }, unknown>;\n}\n\n/** One registered collection: the derived runtime seam the Worker consumes. */\nexport interface ArchiveCollection<P extends object = object, C extends number = number> {\n readonly contentVersion: C;\n /** The collection's wire schemas, shared with its client. */\n readonly contract: ArchiveContract<P, C>;\n /**\n * Validates an untrusted payload and derives everything the archive stores.\n * Null rejects invalid content: JSON/schema failures, title/tag bounds or the\n * canonical payload byte ceiling. Routes answer 422 `invalid-payload` for null.\n * Callback failures and deployment invariant violations throw ArchiveDerivationError.\n */\n readonly derive: (raw: unknown) => Promise<ArchiveDerivation | null>;\n readonly filters: readonly string[];\n readonly name: string;\n readonly tags: readonly string[];\n}\n\nconst sha256Hex = async (text: string): Promise<string> =>\n [...new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text)))]\n .map((byte) => byte.toString(16).padStart(2, '0'))\n .join('');\n\n/** The envelope shape shared by the public and owned compositions. */\nconst envelopeShape = <P, C extends number>(payload: Schema<P>, contentVersion: C) =>\n ({\n author: archiveAuthorSchema,\n contentHash: hex64Schema,\n contentVersion: s.literal(contentVersion),\n id: uuidSchema,\n likeCount: s.number().int().min(0).max(Number.MAX_SAFE_INTEGER),\n payload,\n publishedAt: isoTimestampSchema,\n revision: s.number().int().min(1).max(Number.MAX_SAFE_INTEGER),\n schemaVersion: s.literal(ARCHIVE_CONTRACT_VERSION),\n updatedAt: isoTimestampSchema,\n }) as const;\n\n/**\n * Builds one collection from its definition. Malformed definitions fail here, at\n * registration time — never mid-request: the name must be a routable `[a-z0-9-]`\n * segment, the content version a positive integer, and every declared filter and\n * tag value well-formed text of `1..vocabChars` chars — the bound cursors and the\n * meta route both rely on it. The core derives everything else per payload:\n * parsing, canonical serialization, hashing over the collection's versioned\n * envelope, the emitted-entry schemas, and every storage bound.\n */\nexport function defineArchiveCollection<\n P extends object,\n const C extends number,\n const F extends string = never,\n const T extends string = never,\n>(definition: ArchiveCollectionDefinition<P, C, F, T>): ArchiveCollection<P, C> {\n const { canonicalize, contentVersion, index, name, payloadSchema } = definition;\n if (\n typeof index !== 'function' ||\n !payloadSchema ||\n typeof payloadSchema.safeParse !== 'function' ||\n (canonicalize !== undefined && typeof canonicalize !== 'function')\n ) {\n throw new ArchiveDefinitionError(\n 'Collection requires a payload schema and synchronous indexing/canonicalization functions.',\n );\n }\n if (\n (definition.filters !== undefined && !Array.isArray(definition.filters)) ||\n (definition.tags !== undefined && !Array.isArray(definition.tags))\n ) {\n throw new ArchiveDefinitionError('Collection vocabularies must be arrays.');\n }\n const filters = Object.freeze([...(definition.filters ?? [])]);\n const tags = Object.freeze([...(definition.tags ?? [])]);\n if (typeof name !== 'string' || !ARCHIVE_COLLECTION_NAME_PATTERN.test(name)) {\n throw new ArchiveDefinitionError(`Archive collection name must match [a-z0-9-]{1,32}: ${JSON.stringify(name)}`);\n }\n if (!Number.isSafeInteger(contentVersion) || contentVersion < 1) {\n throw new ArchiveDefinitionError(`Archive collection ${name}: contentVersion must be a safe integer >= 1.`);\n }\n const validateVocabulary = (kind: 'filters' | 'tags', values: readonly string[]): void => {\n for (const value of values) {\n if (\n typeof value !== 'string' ||\n value.length === 0 ||\n value.length > ARCHIVE_LIMITS.vocabChars ||\n !isWellFormedText(value)\n ) {\n throw new ArchiveDefinitionError(\n `Archive collection ${name}: every ${kind} value must be well-formed text of 1..${ARCHIVE_LIMITS.vocabChars} chars.`,\n );\n }\n }\n };\n validateVocabulary('filters', filters);\n validateVocabulary('tags', tags);\n const filterValues = new Set<string>(filters);\n const tagValues = new Set<string>(tags);\n\n const stablePayload = (raw: unknown, stage: ArchiveDerivationStage = 'response'): P | null => {\n try {\n const json = canonicalObjectJson(raw);\n if (json === null) return null;\n const parsed = payloadSchema.safeParse(JSON.parse(json));\n if (!parsed.success || canonicalObjectJson(parsed.data, true, raw) !== json) return null;\n return parsed.data;\n } catch (cause) {\n throw new ArchiveDerivationError(stage, 'Collection stable payload validation failed.', { cause });\n }\n };\n const responsePayload: Schema<P> = new Schema<P>()\n .check((raw) => (stablePayload(raw) === null ? 'Payload is not stable JSON for this collection.' : undefined))\n .transform((raw): P => {\n const parsed = stablePayload(raw);\n if (parsed === null)\n throw new ArchiveDerivationError('response', 'Payload schema changed during response parsing.');\n return parsed;\n });\n\n const derive = async (raw: unknown): Promise<ArchiveDerivation | null> => {\n let stage: ArchiveDerivationStage = 'schema';\n try {\n if (canonicalObjectJson(raw) === null) return null;\n const parsed = payloadSchema.safeParse(raw);\n if (!parsed.success) return null;\n const parsedJson = canonicalObjectJson(parsed.data, true, raw);\n if (parsedJson === null)\n throw new ArchiveDerivationError(stage, 'Payload schema produced invalid or lossy JSON.');\n stage = 'canonicalize';\n const canonical = canonicalize === undefined ? parsed.data : canonicalize(parsed.data);\n // A canonicalize step that leaves the payload schema is a collection bug, not data to store.\n const normalizedJson = canonicalObjectJson(canonical, true, JSON.parse(parsedJson));\n if (normalizedJson === null) throw new ArchiveDerivationError(stage, 'Canonicalization produced invalid JSON.');\n stage = 'schema';\n const revalidated = payloadSchema.safeParse(JSON.parse(normalizedJson));\n if (!revalidated.success)\n throw new ArchiveDerivationError(\n canonicalize === undefined ? 'schema' : 'canonicalize',\n 'Canonical payload is outside the payload schema.',\n { cause: revalidated.error },\n );\n const canonicalJson = canonicalObjectJson(revalidated.data, true, JSON.parse(normalizedJson));\n if (\n canonicalJson === null ||\n canonicalJson !== normalizedJson ||\n stablePayload(JSON.parse(canonicalJson), 'schema') === null\n )\n throw new ArchiveDerivationError(stage, 'Payload schema is not stable on canonical JSON.');\n if (canonicalize !== undefined) {\n const repeatedPayload = payloadSchema.parse(JSON.parse(canonicalJson));\n stage = 'canonicalize';\n const repeated = canonicalize(repeatedPayload);\n if (canonicalObjectJson(repeated, true) !== canonicalJson)\n throw new ArchiveDerivationError(stage, 'Canonicalization is not idempotent.');\n }\n\n stage = 'index';\n const { filter, tags: rawTags, title: rawTitle } = index(revalidated.data);\n // Indexing may read the final parsed value, but must not change the stored content.\n if (canonicalObjectJson(revalidated.data, true) !== canonicalJson)\n throw new ArchiveDerivationError(stage, 'Indexing mutated the canonical payload.');\n if (typeof rawTitle !== 'string' || !Array.isArray(rawTags))\n throw new ArchiveDerivationError(stage, 'Indexing must return a string title and a tags array.');\n const title = rawTitle.trim();\n if (title.length === 0 || title.length > ARCHIVE_LIMITS.titleChars || !isWellFormedText(title)) return null;\n if (filter !== null && (typeof filter !== 'string' || !isWellFormedText(filter)))\n throw new ArchiveDerivationError(stage, 'Indexing returned an invalid filter.');\n const uniqueTags: string[] = [];\n for (const tag of rawTags) {\n if (typeof tag !== 'string' || !isWellFormedText(tag))\n throw new ArchiveDerivationError(stage, 'Indexing returned an invalid tag.');\n if (!uniqueTags.includes(tag)) uniqueTags.push(tag);\n }\n // A derivation outside the declared vocabulary would store a row no list query\n // can reach — the query route 400s the very value the collection derived.\n if ((filter !== null && !filterValues.has(filter)) || uniqueTags.some((tag) => !tagValues.has(tag)))\n throw new ArchiveDerivationError(stage, 'Indexing returned a value outside the declared vocabularies.');\n if (uniqueTags.length > ARCHIVE_LIMITS.tagsPerEntry) return null;\n\n if (new TextEncoder().encode(canonicalJson).byteLength > ARCHIVE_LIMITS.payloadBytes) return null;\n stage = 'hash';\n const contentHash = await sha256Hex(`{\"contentVersion\":${contentVersion},\"payload\":${canonicalJson}}`);\n return { canonicalJson, contentHash, contentVersion, filter, tags: uniqueTags, title };\n } catch (cause) {\n if (cause instanceof ArchiveDerivationError) throw cause;\n throw new ArchiveDerivationError(stage, 'Collection content derivation failed.', { cause });\n }\n };\n\n const entrySchema: Schema<ArchiveEntry<P, C>> = s.object(envelopeShape(responsePayload, contentVersion));\n const ownedEntrySchema: Schema<ArchiveOwnedEntry<P, C>> = s.object({\n ...envelopeShape(responsePayload, contentVersion),\n clientKey: uuidSchema,\n hidden: s.boolean(),\n });\n const contract: ArchiveContract<P, C> = {\n entry: entrySchema,\n ownedEntries: s.object({ entries: s.array(ownedEntrySchema) }),\n ownedEntry: ownedEntrySchema,\n page: s.object({\n cursor: s.string().nullable(),\n entries: s.array(entrySchema),\n previous: s.string().nullable(),\n }),\n publishInput: s.object({ clientKey: uuidSchema, payload: payloadSchema }),\n updateInput: s.object({\n expectedRevision: s.number().int().min(1).max(Number.MAX_SAFE_INTEGER),\n payload: payloadSchema,\n }),\n };\n\n return Object.freeze({ contentVersion, contract: Object.freeze(contract), derive, filters, name, tags });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"collection.cjs","names":[],"sources":["../src/collection.ts"],"sourcesContent":["import { type Schema, s } from '@vielzeug/spell';\nimport { sha256Hex } from './_crypto';\nimport { canonicalObjectJson } from './_json';\nimport {\n ARCHIVE_COLLECTION_NAME_PATTERN,\n ARCHIVE_CONTRACT_VERSION,\n ARCHIVE_LIMITS,\n type ArchivePage,\n archiveAuthorSchema,\n hex64Schema,\n isoTimestampSchema,\n isWellFormedText,\n uuidSchema,\n} from './contract';\nimport { ArchiveDefinitionError, ArchiveDerivationError, type ArchiveDerivationStage } from './errors';\n\n/**\n * Stored payloads are canonical by construction: `derive` validates, normalizes and\n * byte-pins every payload before it is stored, and the package's schema policy is\n * to reset/reseed databases when canonicalization semantics change. The emitted\n * entry schemas therefore embed the plain payload schema — re-parsing a stored\n * payload re-runs its (write-time-stable, hence idempotent) schema, never a\n * parallel re-canonicalization.\n */\n\n/**\n * The one seam between a deployment's content and the archive core. A collection\n * declares its payload schema and derives its list dimensions; the core owns\n * everything else — parsing, canonical serialization, the content hash, the emitted\n * envelope schemas, and every storage bound. Collections never hash, never size,\n * and never hand-write the envelope.\n */\n\n/** What `index` derives from one validated payload: the list dimensions plus the FTS title. */\nexport interface ArchiveIndexing<F extends string = string, T extends string = string> {\n /** One scalar-filter value from the declared vocabulary, or null for \"no filter\". */\n filter: F | null;\n /** Tag values from the declared vocabulary; duplicates collapse in first-seen order. */\n tags: readonly T[];\n /** The searchable title; the core trims and bounds it. */\n title: string;\n}\n\n/** The definition a deployment writes; `defineArchiveCollection` derives the rest. */\nexport interface ArchiveCollectionDefinition<\n P extends object,\n C extends number,\n F extends string = never,\n T extends string = never,\n> {\n /**\n * Normalizes a parsed payload to its storage form — fixed key order, sorted\n * order-insensitive lists — so equivalent content hashes identically. Omit when\n * the payload is already canonical; a result that leaves the payload schema is\n * a derivation fault at derive time, never stored. Must be idempotent.\n */\n canonicalize?(payload: P): P;\n /** The collection's payload-format version; stored and re-emitted with every entry. */\n contentVersion: C;\n /** Valid scalar-filter values; a query param outside the vocabulary is 400. */\n filters?: readonly F[];\n /** Derives the list dimensions and the searchable title from the canonical payload. */\n index(payload: P): ArchiveIndexing<NoInfer<F>, NoInfer<T>>;\n /** The URL + storage dimension this collection owns: lowercase `[a-z0-9-]`, at most 32 chars. */\n name: string;\n /** The strict payload schema: the whole content contract, parsed before anything derives. */\n payloadSchema: Schema<P, unknown>;\n /** Valid tag values. */\n tags?: readonly T[];\n}\n\n/** Everything the archive stores and re-identifies by, derived once per publish and update. */\nexport interface ArchiveDerivation {\n /** The stored bytes: the canonical payload, serialized with fixed key order. */\n canonicalJson: string;\n /** SHA-256 hex over `{contentVersion, payload}`: identical content hashes identically. */\n contentHash: string;\n /** The collection's declared payload-format version. */\n contentVersion: number;\n filter: string | null;\n tags: readonly string[];\n title: string;\n}\n\n/** The envelope fields every emitted entry carries; the payload stays the collection's. */\nexport interface ArchiveEntry<P = unknown, C extends number = number> {\n author: { id: string; name: string };\n contentHash: string;\n /** The collection's payload-format version, literal-pinned by the definition. */\n contentVersion: C;\n id: string;\n likeCount: number;\n payload: P;\n publishedAt: string;\n revision: number;\n /** The archive wire-contract version, pinned by the core. */\n schemaVersion: typeof ARCHIVE_CONTRACT_VERSION;\n updatedAt: string;\n}\n\n/** The private owner view: the emitted entry plus the create retry key and moderation state. */\nexport interface ArchiveOwnedEntry<P = unknown, C extends number = number> extends ArchiveEntry<P, C> {\n /** The create retry key; public routes never emit it. */\n clientKey: string;\n /** True while an operator has removed the entry from public view (still owned, frozen for edits). */\n hidden: boolean;\n}\n\n/**\n * The wire schemas one collection shares with its client: every request and response\n * envelope for this collection's routes, composed from the payload schema, so the\n * client and the Worker parse the same bytes with the same rules.\n */\nexport interface ArchiveContract<P = unknown, C extends number = number> {\n /** The public entry emitted by list, detail and publish/update responses. */\n entry: Schema<ArchiveEntry<P, C>>;\n /** The own-publications response: `{ entries: ArchiveOwnedEntry[] }`. */\n ownedEntries: Schema<{ entries: ArchiveOwnedEntry<P, C>[] }>;\n /** The private owner entry — the public entry plus the create retry key. */\n ownedEntry: Schema<ArchiveOwnedEntry<P, C>>;\n /** One list page: entries plus the forward/backward cursors around it. */\n page: Schema<ArchivePage<ArchiveEntry<P, C>>>;\n /** The publish request: `{ clientKey, payload }`. */\n publishInput: Schema<{ clientKey: string; payload: P }, unknown>;\n /** The update request: `{ expectedRevision, payload }`. */\n updateInput: Schema<{ expectedRevision: number; payload: P }, unknown>;\n}\n\n/** One registered collection: the derived runtime seam the Worker consumes. */\nexport interface ArchiveCollection<P extends object = object, C extends number = number> {\n readonly contentVersion: C;\n /** The collection's wire schemas, shared with its client. */\n readonly contract: ArchiveContract<P, C>;\n /**\n * Validates an untrusted payload and derives everything the archive stores.\n * Null rejects invalid content: JSON/schema failures, title/tag bounds or the\n * canonical payload byte ceiling. Routes answer 422 `invalid-payload` for null.\n * Callback failures and deployment invariant violations throw ArchiveDerivationError.\n */\n readonly derive: (raw: unknown) => Promise<ArchiveDerivation | null>;\n readonly filters: readonly string[];\n readonly name: string;\n readonly tags: readonly string[];\n}\n\n/** The envelope shape shared by the public and owned compositions. */\nconst envelopeShape = <P, C extends number>(payload: Schema<P>, contentVersion: C) =>\n ({\n author: archiveAuthorSchema,\n contentHash: hex64Schema,\n contentVersion: s.literal(contentVersion),\n id: uuidSchema,\n likeCount: s.number().int().min(0).max(Number.MAX_SAFE_INTEGER),\n payload,\n publishedAt: isoTimestampSchema,\n revision: s.number().int().min(1).max(Number.MAX_SAFE_INTEGER),\n schemaVersion: s.literal(ARCHIVE_CONTRACT_VERSION),\n updatedAt: isoTimestampSchema,\n }) as const;\n\n/**\n * Builds one collection from its definition. Malformed definitions fail here, at\n * registration time — never mid-request: the name must be a routable `[a-z0-9-]`\n * segment, the content version a positive integer, and every declared filter and\n * tag value well-formed text of `1..vocabChars` chars — the bound cursors and the\n * meta route both rely on it. The core derives everything else per payload:\n * parsing, canonical serialization, hashing over the collection's versioned\n * envelope, the emitted-entry schemas, and every storage bound.\n */\nexport function defineArchiveCollection<\n P extends object,\n const C extends number,\n const F extends string = never,\n const T extends string = never,\n>(definition: ArchiveCollectionDefinition<P, C, F, T>): ArchiveCollection<P, C> {\n const { canonicalize, contentVersion, index, name, payloadSchema } = definition;\n if (\n typeof index !== 'function' ||\n !payloadSchema ||\n typeof payloadSchema.safeParse !== 'function' ||\n (canonicalize !== undefined && typeof canonicalize !== 'function')\n ) {\n throw new ArchiveDefinitionError(\n 'Collection requires a payload schema and synchronous indexing/canonicalization functions.',\n );\n }\n if (\n (definition.filters !== undefined && !Array.isArray(definition.filters)) ||\n (definition.tags !== undefined && !Array.isArray(definition.tags))\n ) {\n throw new ArchiveDefinitionError('Collection vocabularies must be arrays.');\n }\n const filters = Object.freeze([...(definition.filters ?? [])]);\n const tags = Object.freeze([...(definition.tags ?? [])]);\n if (typeof name !== 'string' || !ARCHIVE_COLLECTION_NAME_PATTERN.test(name)) {\n throw new ArchiveDefinitionError(`Archive collection name must match [a-z0-9-]{1,32}: ${JSON.stringify(name)}`);\n }\n if (!Number.isSafeInteger(contentVersion) || contentVersion < 1) {\n throw new ArchiveDefinitionError(`Archive collection ${name}: contentVersion must be a safe integer >= 1.`);\n }\n const validateVocabulary = (kind: 'filters' | 'tags', values: readonly string[]): void => {\n for (const value of values) {\n if (\n typeof value !== 'string' ||\n value.length === 0 ||\n value.length > ARCHIVE_LIMITS.vocabChars ||\n !isWellFormedText(value)\n ) {\n throw new ArchiveDefinitionError(\n `Archive collection ${name}: every ${kind} value must be well-formed text of 1..${ARCHIVE_LIMITS.vocabChars} chars.`,\n );\n }\n }\n };\n validateVocabulary('filters', filters);\n validateVocabulary('tags', tags);\n const filterValues = new Set<string>(filters);\n const tagValues = new Set<string>(tags);\n\n const derive = async (raw: unknown): Promise<ArchiveDerivation | null> => {\n let stage: ArchiveDerivationStage = 'schema';\n try {\n if (canonicalObjectJson(raw) === null) return null;\n const parsed = payloadSchema.safeParse(raw);\n if (!parsed.success) return null;\n const parsedJson = canonicalObjectJson(parsed.data, true, raw);\n if (parsedJson === null)\n throw new ArchiveDerivationError(stage, 'Payload schema produced invalid or lossy JSON.');\n stage = 'canonicalize';\n const canonical = canonicalize === undefined ? parsed.data : canonicalize(parsed.data);\n // A canonicalize step that leaves the payload schema is a collection bug, not data to store.\n const normalizedJson = canonicalObjectJson(canonical, true, JSON.parse(parsedJson));\n if (normalizedJson === null) throw new ArchiveDerivationError(stage, 'Canonicalization produced invalid JSON.');\n stage = 'schema';\n const revalidated = payloadSchema.safeParse(JSON.parse(normalizedJson));\n if (!revalidated.success)\n throw new ArchiveDerivationError(\n canonicalize === undefined ? 'schema' : 'canonicalize',\n 'Canonical payload is outside the payload schema.',\n { cause: revalidated.error },\n );\n const canonicalJson = canonicalObjectJson(revalidated.data, true, JSON.parse(normalizedJson));\n if (canonicalJson === null || canonicalJson !== normalizedJson)\n throw new ArchiveDerivationError(stage, 'Payload schema is not stable on canonical JSON.');\n if (canonicalize !== undefined) {\n const repeatedPayload = payloadSchema.parse(JSON.parse(canonicalJson));\n stage = 'canonicalize';\n const repeated = canonicalize(repeatedPayload);\n if (canonicalObjectJson(repeated, true) !== canonicalJson)\n throw new ArchiveDerivationError(stage, 'Canonicalization is not idempotent.');\n }\n\n stage = 'index';\n const { filter, tags: rawTags, title: rawTitle } = index(revalidated.data);\n // Indexing may read the final parsed value, but must not change the stored content.\n if (canonicalObjectJson(revalidated.data, true) !== canonicalJson)\n throw new ArchiveDerivationError(stage, 'Indexing mutated the canonical payload.');\n if (typeof rawTitle !== 'string' || !Array.isArray(rawTags))\n throw new ArchiveDerivationError(stage, 'Indexing must return a string title and a tags array.');\n const title = rawTitle.trim();\n if (title.length === 0 || title.length > ARCHIVE_LIMITS.titleChars || !isWellFormedText(title)) return null;\n if (filter !== null && (typeof filter !== 'string' || !isWellFormedText(filter)))\n throw new ArchiveDerivationError(stage, 'Indexing returned an invalid filter.');\n const uniqueTags: string[] = [];\n for (const tag of rawTags) {\n if (typeof tag !== 'string' || !isWellFormedText(tag))\n throw new ArchiveDerivationError(stage, 'Indexing returned an invalid tag.');\n if (!uniqueTags.includes(tag)) uniqueTags.push(tag);\n }\n // A derivation outside the declared vocabulary would store a row no list query\n // can reach — the query route 400s the very value the collection derived.\n if ((filter !== null && !filterValues.has(filter)) || uniqueTags.some((tag) => !tagValues.has(tag)))\n throw new ArchiveDerivationError(stage, 'Indexing returned a value outside the declared vocabularies.');\n if (uniqueTags.length > ARCHIVE_LIMITS.tagsPerEntry) return null;\n\n if (new TextEncoder().encode(canonicalJson).byteLength > ARCHIVE_LIMITS.payloadBytes) return null;\n stage = 'hash';\n const contentHash = await sha256Hex(`{\"contentVersion\":${contentVersion},\"payload\":${canonicalJson}}`);\n return { canonicalJson, contentHash, contentVersion, filter, tags: uniqueTags, title };\n } catch (cause) {\n if (cause instanceof ArchiveDerivationError) throw cause;\n throw new ArchiveDerivationError(stage, 'Collection content derivation failed.', { cause });\n }\n };\n\n // The cast pins the envelope's payload member to the collection's output type:\n // `payloadSchema` is `Schema<P, unknown>` (input unknown), and inference through\n // the generic shape helper would widen the member to `unknown`.\n const entrySchema: Schema<ArchiveEntry<P, C>> = s.object(envelopeShape(payloadSchema as Schema<P>, contentVersion));\n const ownedEntrySchema: Schema<ArchiveOwnedEntry<P, C>> = s.object({\n ...envelopeShape(payloadSchema as Schema<P>, contentVersion),\n clientKey: uuidSchema,\n hidden: s.boolean(),\n });\n const contract: ArchiveContract<P, C> = {\n entry: entrySchema,\n ownedEntries: s.object({ entries: s.array(ownedEntrySchema) }),\n ownedEntry: ownedEntrySchema,\n page: s.object({\n cursor: s.string().nullable(),\n entries: s.array(entrySchema),\n }),\n publishInput: s.object({ clientKey: uuidSchema, payload: payloadSchema }),\n updateInput: s.object({\n expectedRevision: s.number().int().min(1).max(Number.MAX_SAFE_INTEGER),\n payload: payloadSchema,\n }),\n };\n\n return Object.freeze({ contentVersion, contract: Object.freeze(contract), derive, filters, name, tags });\n}\n"],"mappings":"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"}
|
package/dist/collection.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
import { Schema } from '@vielzeug/spell';
|
|
1
|
+
import { type Schema } from '@vielzeug/spell';
|
|
2
2
|
import { ARCHIVE_CONTRACT_VERSION, type ArchivePage } from './contract';
|
|
3
|
+
/**
|
|
4
|
+
* Stored payloads are canonical by construction: `derive` validates, normalizes and
|
|
5
|
+
* byte-pins every payload before it is stored, and the package's schema policy is
|
|
6
|
+
* to reset/reseed databases when canonicalization semantics change. The emitted
|
|
7
|
+
* entry schemas therefore embed the plain payload schema — re-parsing a stored
|
|
8
|
+
* payload re-runs its (write-time-stable, hence idempotent) schema, never a
|
|
9
|
+
* parallel re-canonicalization.
|
|
10
|
+
*/
|
|
3
11
|
/**
|
|
4
12
|
* The one seam between a deployment's content and the archive core. A collection
|
|
5
13
|
* declares its payload schema and derives its list dimensions; the core owns
|
package/dist/collection.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../src/collection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAK,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../src/collection.ts"],"names":[],"mappings":"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"}
|
package/dist/collection.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{ARCHIVE_COLLECTION_NAME_PATTERN as
|
|
1
|
+
import{sha256Hex as e}from"./_crypto.js";import{ARCHIVE_COLLECTION_NAME_PATTERN as t,ARCHIVE_LIMITS as n,archiveAuthorSchema as r,hex64Schema as i,isWellFormedText as a,isoTimestampSchema as o,uuidSchema as s}from"./contract.js";import{canonicalObjectJson as c}from"./_json.js";import{ArchiveDefinitionError as l,ArchiveDerivationError as u}from"./errors.js";import{s as d}from"@vielzeug/spell";var envelopeShape=(e,t)=>({author:r,contentHash:i,contentVersion:d.literal(t),id:s,likeCount:d.number().int().min(0).max(2**53-1),payload:e,publishedAt:o,revision:d.number().int().min(1).max(2**53-1),schemaVersion:d.literal(1),updatedAt:o});function defineArchiveCollection(r){let{canonicalize:i,contentVersion:o,index:f,name:p,payloadSchema:m}=r;if(typeof f!=`function`||!m||typeof m.safeParse!=`function`||i!==void 0&&typeof i!=`function`)throw new l(`Collection requires a payload schema and synchronous indexing/canonicalization functions.`);if(r.filters!==void 0&&!Array.isArray(r.filters)||r.tags!==void 0&&!Array.isArray(r.tags))throw new l(`Collection vocabularies must be arrays.`);let h=Object.freeze([...r.filters??[]]),g=Object.freeze([...r.tags??[]]);if(typeof p!=`string`||!t.test(p))throw new l(`Archive collection name must match [a-z0-9-]{1,32}: ${JSON.stringify(p)}`);if(!Number.isSafeInteger(o)||o<1)throw new l(`Archive collection ${p}: contentVersion must be a safe integer >= 1.`);let validateVocabulary=(e,t)=>{for(let r of t)if(typeof r!=`string`||r.length===0||r.length>n.vocabChars||!a(r))throw new l(`Archive collection ${p}: every ${e} value must be well-formed text of 1..${n.vocabChars} chars.`)};validateVocabulary(`filters`,h),validateVocabulary(`tags`,g);let _=new Set(h),v=new Set(g),derive=async t=>{let r=`schema`;try{if(c(t)===null)return null;let s=m.safeParse(t);if(!s.success)return null;let l=c(s.data,!0,t);if(l===null)throw new u(r,`Payload schema produced invalid or lossy JSON.`);r=`canonicalize`;let d=i===void 0?s.data:i(s.data),p=c(d,!0,JSON.parse(l));if(p===null)throw new u(r,`Canonicalization produced invalid JSON.`);r=`schema`;let h=m.safeParse(JSON.parse(p));if(!h.success)throw new u(i===void 0?`schema`:`canonicalize`,`Canonical payload is outside the payload schema.`,{cause:h.error});let g=c(h.data,!0,JSON.parse(p));if(g===null||g!==p)throw new u(r,`Payload schema is not stable on canonical JSON.`);if(i!==void 0){let e=m.parse(JSON.parse(g));r=`canonicalize`;let t=i(e);if(c(t,!0)!==g)throw new u(r,`Canonicalization is not idempotent.`)}r=`index`;let{filter:y,tags:b,title:x}=f(h.data);if(c(h.data,!0)!==g)throw new u(r,`Indexing mutated the canonical payload.`);if(typeof x!=`string`||!Array.isArray(b))throw new u(r,`Indexing must return a string title and a tags array.`);let S=x.trim();if(S.length===0||S.length>n.titleChars||!a(S))return null;if(y!==null&&(typeof y!=`string`||!a(y)))throw new u(r,`Indexing returned an invalid filter.`);let C=[];for(let e of b){if(typeof e!=`string`||!a(e))throw new u(r,`Indexing returned an invalid tag.`);C.includes(e)||C.push(e)}if(y!==null&&!_.has(y)||C.some(e=>!v.has(e)))throw new u(r,`Indexing returned a value outside the declared vocabularies.`);return C.length>n.tagsPerEntry||new TextEncoder().encode(g).byteLength>n.payloadBytes?null:(r=`hash`,{canonicalJson:g,contentHash:await e(`{"contentVersion":${o},"payload":${g}}`),contentVersion:o,filter:y,tags:C,title:S})}catch(e){throw e instanceof u?e:new u(r,`Collection content derivation failed.`,{cause:e})}},y=d.object(envelopeShape(m,o)),b=d.object({...envelopeShape(m,o),clientKey:s,hidden:d.boolean()}),x={entry:y,ownedEntries:d.object({entries:d.array(b)}),ownedEntry:b,page:d.object({cursor:d.string().nullable(),entries:d.array(y)}),publishInput:d.object({clientKey:s,payload:m}),updateInput:d.object({expectedRevision:d.number().int().min(1).max(2**53-1),payload:m})};return Object.freeze({contentVersion:o,contract:Object.freeze(x),derive,filters:h,name:p,tags:g})}export{defineArchiveCollection};
|
|
2
2
|
//# sourceMappingURL=collection.js.map
|
package/dist/collection.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection.js","names":[],"sources":["../src/collection.ts"],"sourcesContent":["import { Schema, s } from '@vielzeug/spell';\nimport { canonicalObjectJson } from './_json';\nimport {\n ARCHIVE_COLLECTION_NAME_PATTERN,\n ARCHIVE_CONTRACT_VERSION,\n ARCHIVE_LIMITS,\n type ArchivePage,\n archiveAuthorSchema,\n hex64Schema,\n isoTimestampSchema,\n isWellFormedText,\n uuidSchema,\n} from './contract';\nimport { ArchiveDefinitionError, ArchiveDerivationError, type ArchiveDerivationStage } from './errors';\n\n/**\n * The one seam between a deployment's content and the archive core. A collection\n * declares its payload schema and derives its list dimensions; the core owns\n * everything else — parsing, canonical serialization, the content hash, the emitted\n * envelope schemas, and every storage bound. Collections never hash, never size,\n * and never hand-write the envelope.\n */\n\n/** What `index` derives from one validated payload: the list dimensions plus the FTS title. */\nexport interface ArchiveIndexing<F extends string = string, T extends string = string> {\n /** One scalar-filter value from the declared vocabulary, or null for \"no filter\". */\n filter: F | null;\n /** Tag values from the declared vocabulary; duplicates collapse in first-seen order. */\n tags: readonly T[];\n /** The searchable title; the core trims and bounds it. */\n title: string;\n}\n\n/** The definition a deployment writes; `defineArchiveCollection` derives the rest. */\nexport interface ArchiveCollectionDefinition<\n P extends object,\n C extends number,\n F extends string = never,\n T extends string = never,\n> {\n /**\n * Normalizes a parsed payload to its storage form — fixed key order, sorted\n * order-insensitive lists — so equivalent content hashes identically. Omit when\n * the payload is already canonical; a result that leaves the payload schema is\n * a derivation fault at derive time, never stored. Must be idempotent.\n */\n canonicalize?(payload: P): P;\n /** The collection's payload-format version; stored and re-emitted with every entry. */\n contentVersion: C;\n /** Valid scalar-filter values; a query param outside the vocabulary is 400. */\n filters?: readonly F[];\n /** Derives the list dimensions and the searchable title from the canonical payload. */\n index(payload: P): ArchiveIndexing<NoInfer<F>, NoInfer<T>>;\n /** The URL + storage dimension this collection owns: lowercase `[a-z0-9-]`, at most 32 chars. */\n name: string;\n /** The strict payload schema: the whole content contract, parsed before anything derives. */\n payloadSchema: Schema<P, unknown>;\n /** Valid tag values. */\n tags?: readonly T[];\n}\n\n/** Everything the archive stores and re-identifies by, derived once per publish and update. */\nexport interface ArchiveDerivation {\n /** The stored bytes: the canonical payload, serialized with fixed key order. */\n canonicalJson: string;\n /** SHA-256 hex over `{contentVersion, payload}`: identical content hashes identically. */\n contentHash: string;\n /** The collection's declared payload-format version. */\n contentVersion: number;\n filter: string | null;\n tags: readonly string[];\n title: string;\n}\n\n/** The envelope fields every emitted entry carries; the payload stays the collection's. */\nexport interface ArchiveEntry<P = unknown, C extends number = number> {\n author: { id: string; name: string };\n contentHash: string;\n /** The collection's payload-format version, literal-pinned by the definition. */\n contentVersion: C;\n id: string;\n likeCount: number;\n payload: P;\n publishedAt: string;\n revision: number;\n /** The archive wire-contract version, pinned by the core. */\n schemaVersion: typeof ARCHIVE_CONTRACT_VERSION;\n updatedAt: string;\n}\n\n/** The private owner view: the emitted entry plus the create retry key and moderation state. */\nexport interface ArchiveOwnedEntry<P = unknown, C extends number = number> extends ArchiveEntry<P, C> {\n /** The create retry key; public routes never emit it. */\n clientKey: string;\n /** True while an operator has removed the entry from public view (still owned, frozen for edits). */\n hidden: boolean;\n}\n\n/**\n * The wire schemas one collection shares with its client: every request and response\n * envelope for this collection's routes, composed from the payload schema, so the\n * client and the Worker parse the same bytes with the same rules.\n */\nexport interface ArchiveContract<P = unknown, C extends number = number> {\n /** The public entry emitted by list, detail and publish/update responses. */\n entry: Schema<ArchiveEntry<P, C>>;\n /** The own-publications response: `{ entries: ArchiveOwnedEntry[] }`. */\n ownedEntries: Schema<{ entries: ArchiveOwnedEntry<P, C>[] }>;\n /** The private owner entry — the public entry plus the create retry key. */\n ownedEntry: Schema<ArchiveOwnedEntry<P, C>>;\n /** One list page: entries plus the forward/backward cursors around it. */\n page: Schema<ArchivePage<ArchiveEntry<P, C>>>;\n /** The publish request: `{ clientKey, payload }`. */\n publishInput: Schema<{ clientKey: string; payload: P }, unknown>;\n /** The update request: `{ expectedRevision, payload }`. */\n updateInput: Schema<{ expectedRevision: number; payload: P }, unknown>;\n}\n\n/** One registered collection: the derived runtime seam the Worker consumes. */\nexport interface ArchiveCollection<P extends object = object, C extends number = number> {\n readonly contentVersion: C;\n /** The collection's wire schemas, shared with its client. */\n readonly contract: ArchiveContract<P, C>;\n /**\n * Validates an untrusted payload and derives everything the archive stores.\n * Null rejects invalid content: JSON/schema failures, title/tag bounds or the\n * canonical payload byte ceiling. Routes answer 422 `invalid-payload` for null.\n * Callback failures and deployment invariant violations throw ArchiveDerivationError.\n */\n readonly derive: (raw: unknown) => Promise<ArchiveDerivation | null>;\n readonly filters: readonly string[];\n readonly name: string;\n readonly tags: readonly string[];\n}\n\nconst sha256Hex = async (text: string): Promise<string> =>\n [...new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text)))]\n .map((byte) => byte.toString(16).padStart(2, '0'))\n .join('');\n\n/** The envelope shape shared by the public and owned compositions. */\nconst envelopeShape = <P, C extends number>(payload: Schema<P>, contentVersion: C) =>\n ({\n author: archiveAuthorSchema,\n contentHash: hex64Schema,\n contentVersion: s.literal(contentVersion),\n id: uuidSchema,\n likeCount: s.number().int().min(0).max(Number.MAX_SAFE_INTEGER),\n payload,\n publishedAt: isoTimestampSchema,\n revision: s.number().int().min(1).max(Number.MAX_SAFE_INTEGER),\n schemaVersion: s.literal(ARCHIVE_CONTRACT_VERSION),\n updatedAt: isoTimestampSchema,\n }) as const;\n\n/**\n * Builds one collection from its definition. Malformed definitions fail here, at\n * registration time — never mid-request: the name must be a routable `[a-z0-9-]`\n * segment, the content version a positive integer, and every declared filter and\n * tag value well-formed text of `1..vocabChars` chars — the bound cursors and the\n * meta route both rely on it. The core derives everything else per payload:\n * parsing, canonical serialization, hashing over the collection's versioned\n * envelope, the emitted-entry schemas, and every storage bound.\n */\nexport function defineArchiveCollection<\n P extends object,\n const C extends number,\n const F extends string = never,\n const T extends string = never,\n>(definition: ArchiveCollectionDefinition<P, C, F, T>): ArchiveCollection<P, C> {\n const { canonicalize, contentVersion, index, name, payloadSchema } = definition;\n if (\n typeof index !== 'function' ||\n !payloadSchema ||\n typeof payloadSchema.safeParse !== 'function' ||\n (canonicalize !== undefined && typeof canonicalize !== 'function')\n ) {\n throw new ArchiveDefinitionError(\n 'Collection requires a payload schema and synchronous indexing/canonicalization functions.',\n );\n }\n if (\n (definition.filters !== undefined && !Array.isArray(definition.filters)) ||\n (definition.tags !== undefined && !Array.isArray(definition.tags))\n ) {\n throw new ArchiveDefinitionError('Collection vocabularies must be arrays.');\n }\n const filters = Object.freeze([...(definition.filters ?? [])]);\n const tags = Object.freeze([...(definition.tags ?? [])]);\n if (typeof name !== 'string' || !ARCHIVE_COLLECTION_NAME_PATTERN.test(name)) {\n throw new ArchiveDefinitionError(`Archive collection name must match [a-z0-9-]{1,32}: ${JSON.stringify(name)}`);\n }\n if (!Number.isSafeInteger(contentVersion) || contentVersion < 1) {\n throw new ArchiveDefinitionError(`Archive collection ${name}: contentVersion must be a safe integer >= 1.`);\n }\n const validateVocabulary = (kind: 'filters' | 'tags', values: readonly string[]): void => {\n for (const value of values) {\n if (\n typeof value !== 'string' ||\n value.length === 0 ||\n value.length > ARCHIVE_LIMITS.vocabChars ||\n !isWellFormedText(value)\n ) {\n throw new ArchiveDefinitionError(\n `Archive collection ${name}: every ${kind} value must be well-formed text of 1..${ARCHIVE_LIMITS.vocabChars} chars.`,\n );\n }\n }\n };\n validateVocabulary('filters', filters);\n validateVocabulary('tags', tags);\n const filterValues = new Set<string>(filters);\n const tagValues = new Set<string>(tags);\n\n const stablePayload = (raw: unknown, stage: ArchiveDerivationStage = 'response'): P | null => {\n try {\n const json = canonicalObjectJson(raw);\n if (json === null) return null;\n const parsed = payloadSchema.safeParse(JSON.parse(json));\n if (!parsed.success || canonicalObjectJson(parsed.data, true, raw) !== json) return null;\n return parsed.data;\n } catch (cause) {\n throw new ArchiveDerivationError(stage, 'Collection stable payload validation failed.', { cause });\n }\n };\n const responsePayload: Schema<P> = new Schema<P>()\n .check((raw) => (stablePayload(raw) === null ? 'Payload is not stable JSON for this collection.' : undefined))\n .transform((raw): P => {\n const parsed = stablePayload(raw);\n if (parsed === null)\n throw new ArchiveDerivationError('response', 'Payload schema changed during response parsing.');\n return parsed;\n });\n\n const derive = async (raw: unknown): Promise<ArchiveDerivation | null> => {\n let stage: ArchiveDerivationStage = 'schema';\n try {\n if (canonicalObjectJson(raw) === null) return null;\n const parsed = payloadSchema.safeParse(raw);\n if (!parsed.success) return null;\n const parsedJson = canonicalObjectJson(parsed.data, true, raw);\n if (parsedJson === null)\n throw new ArchiveDerivationError(stage, 'Payload schema produced invalid or lossy JSON.');\n stage = 'canonicalize';\n const canonical = canonicalize === undefined ? parsed.data : canonicalize(parsed.data);\n // A canonicalize step that leaves the payload schema is a collection bug, not data to store.\n const normalizedJson = canonicalObjectJson(canonical, true, JSON.parse(parsedJson));\n if (normalizedJson === null) throw new ArchiveDerivationError(stage, 'Canonicalization produced invalid JSON.');\n stage = 'schema';\n const revalidated = payloadSchema.safeParse(JSON.parse(normalizedJson));\n if (!revalidated.success)\n throw new ArchiveDerivationError(\n canonicalize === undefined ? 'schema' : 'canonicalize',\n 'Canonical payload is outside the payload schema.',\n { cause: revalidated.error },\n );\n const canonicalJson = canonicalObjectJson(revalidated.data, true, JSON.parse(normalizedJson));\n if (\n canonicalJson === null ||\n canonicalJson !== normalizedJson ||\n stablePayload(JSON.parse(canonicalJson), 'schema') === null\n )\n throw new ArchiveDerivationError(stage, 'Payload schema is not stable on canonical JSON.');\n if (canonicalize !== undefined) {\n const repeatedPayload = payloadSchema.parse(JSON.parse(canonicalJson));\n stage = 'canonicalize';\n const repeated = canonicalize(repeatedPayload);\n if (canonicalObjectJson(repeated, true) !== canonicalJson)\n throw new ArchiveDerivationError(stage, 'Canonicalization is not idempotent.');\n }\n\n stage = 'index';\n const { filter, tags: rawTags, title: rawTitle } = index(revalidated.data);\n // Indexing may read the final parsed value, but must not change the stored content.\n if (canonicalObjectJson(revalidated.data, true) !== canonicalJson)\n throw new ArchiveDerivationError(stage, 'Indexing mutated the canonical payload.');\n if (typeof rawTitle !== 'string' || !Array.isArray(rawTags))\n throw new ArchiveDerivationError(stage, 'Indexing must return a string title and a tags array.');\n const title = rawTitle.trim();\n if (title.length === 0 || title.length > ARCHIVE_LIMITS.titleChars || !isWellFormedText(title)) return null;\n if (filter !== null && (typeof filter !== 'string' || !isWellFormedText(filter)))\n throw new ArchiveDerivationError(stage, 'Indexing returned an invalid filter.');\n const uniqueTags: string[] = [];\n for (const tag of rawTags) {\n if (typeof tag !== 'string' || !isWellFormedText(tag))\n throw new ArchiveDerivationError(stage, 'Indexing returned an invalid tag.');\n if (!uniqueTags.includes(tag)) uniqueTags.push(tag);\n }\n // A derivation outside the declared vocabulary would store a row no list query\n // can reach — the query route 400s the very value the collection derived.\n if ((filter !== null && !filterValues.has(filter)) || uniqueTags.some((tag) => !tagValues.has(tag)))\n throw new ArchiveDerivationError(stage, 'Indexing returned a value outside the declared vocabularies.');\n if (uniqueTags.length > ARCHIVE_LIMITS.tagsPerEntry) return null;\n\n if (new TextEncoder().encode(canonicalJson).byteLength > ARCHIVE_LIMITS.payloadBytes) return null;\n stage = 'hash';\n const contentHash = await sha256Hex(`{\"contentVersion\":${contentVersion},\"payload\":${canonicalJson}}`);\n return { canonicalJson, contentHash, contentVersion, filter, tags: uniqueTags, title };\n } catch (cause) {\n if (cause instanceof ArchiveDerivationError) throw cause;\n throw new ArchiveDerivationError(stage, 'Collection content derivation failed.', { cause });\n }\n };\n\n const entrySchema: Schema<ArchiveEntry<P, C>> = s.object(envelopeShape(responsePayload, contentVersion));\n const ownedEntrySchema: Schema<ArchiveOwnedEntry<P, C>> = s.object({\n ...envelopeShape(responsePayload, contentVersion),\n clientKey: uuidSchema,\n hidden: s.boolean(),\n });\n const contract: ArchiveContract<P, C> = {\n entry: entrySchema,\n ownedEntries: s.object({ entries: s.array(ownedEntrySchema) }),\n ownedEntry: ownedEntrySchema,\n page: s.object({\n cursor: s.string().nullable(),\n entries: s.array(entrySchema),\n previous: s.string().nullable(),\n }),\n publishInput: s.object({ clientKey: uuidSchema, payload: payloadSchema }),\n updateInput: s.object({\n expectedRevision: s.number().int().min(1).max(Number.MAX_SAFE_INTEGER),\n payload: payloadSchema,\n }),\n };\n\n return Object.freeze({ contentVersion, contract: Object.freeze(contract), derive, filters, name, tags });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"collection.js","names":[],"sources":["../src/collection.ts"],"sourcesContent":["import { type Schema, s } from '@vielzeug/spell';\nimport { sha256Hex } from './_crypto';\nimport { canonicalObjectJson } from './_json';\nimport {\n ARCHIVE_COLLECTION_NAME_PATTERN,\n ARCHIVE_CONTRACT_VERSION,\n ARCHIVE_LIMITS,\n type ArchivePage,\n archiveAuthorSchema,\n hex64Schema,\n isoTimestampSchema,\n isWellFormedText,\n uuidSchema,\n} from './contract';\nimport { ArchiveDefinitionError, ArchiveDerivationError, type ArchiveDerivationStage } from './errors';\n\n/**\n * Stored payloads are canonical by construction: `derive` validates, normalizes and\n * byte-pins every payload before it is stored, and the package's schema policy is\n * to reset/reseed databases when canonicalization semantics change. The emitted\n * entry schemas therefore embed the plain payload schema — re-parsing a stored\n * payload re-runs its (write-time-stable, hence idempotent) schema, never a\n * parallel re-canonicalization.\n */\n\n/**\n * The one seam between a deployment's content and the archive core. A collection\n * declares its payload schema and derives its list dimensions; the core owns\n * everything else — parsing, canonical serialization, the content hash, the emitted\n * envelope schemas, and every storage bound. Collections never hash, never size,\n * and never hand-write the envelope.\n */\n\n/** What `index` derives from one validated payload: the list dimensions plus the FTS title. */\nexport interface ArchiveIndexing<F extends string = string, T extends string = string> {\n /** One scalar-filter value from the declared vocabulary, or null for \"no filter\". */\n filter: F | null;\n /** Tag values from the declared vocabulary; duplicates collapse in first-seen order. */\n tags: readonly T[];\n /** The searchable title; the core trims and bounds it. */\n title: string;\n}\n\n/** The definition a deployment writes; `defineArchiveCollection` derives the rest. */\nexport interface ArchiveCollectionDefinition<\n P extends object,\n C extends number,\n F extends string = never,\n T extends string = never,\n> {\n /**\n * Normalizes a parsed payload to its storage form — fixed key order, sorted\n * order-insensitive lists — so equivalent content hashes identically. Omit when\n * the payload is already canonical; a result that leaves the payload schema is\n * a derivation fault at derive time, never stored. Must be idempotent.\n */\n canonicalize?(payload: P): P;\n /** The collection's payload-format version; stored and re-emitted with every entry. */\n contentVersion: C;\n /** Valid scalar-filter values; a query param outside the vocabulary is 400. */\n filters?: readonly F[];\n /** Derives the list dimensions and the searchable title from the canonical payload. */\n index(payload: P): ArchiveIndexing<NoInfer<F>, NoInfer<T>>;\n /** The URL + storage dimension this collection owns: lowercase `[a-z0-9-]`, at most 32 chars. */\n name: string;\n /** The strict payload schema: the whole content contract, parsed before anything derives. */\n payloadSchema: Schema<P, unknown>;\n /** Valid tag values. */\n tags?: readonly T[];\n}\n\n/** Everything the archive stores and re-identifies by, derived once per publish and update. */\nexport interface ArchiveDerivation {\n /** The stored bytes: the canonical payload, serialized with fixed key order. */\n canonicalJson: string;\n /** SHA-256 hex over `{contentVersion, payload}`: identical content hashes identically. */\n contentHash: string;\n /** The collection's declared payload-format version. */\n contentVersion: number;\n filter: string | null;\n tags: readonly string[];\n title: string;\n}\n\n/** The envelope fields every emitted entry carries; the payload stays the collection's. */\nexport interface ArchiveEntry<P = unknown, C extends number = number> {\n author: { id: string; name: string };\n contentHash: string;\n /** The collection's payload-format version, literal-pinned by the definition. */\n contentVersion: C;\n id: string;\n likeCount: number;\n payload: P;\n publishedAt: string;\n revision: number;\n /** The archive wire-contract version, pinned by the core. */\n schemaVersion: typeof ARCHIVE_CONTRACT_VERSION;\n updatedAt: string;\n}\n\n/** The private owner view: the emitted entry plus the create retry key and moderation state. */\nexport interface ArchiveOwnedEntry<P = unknown, C extends number = number> extends ArchiveEntry<P, C> {\n /** The create retry key; public routes never emit it. */\n clientKey: string;\n /** True while an operator has removed the entry from public view (still owned, frozen for edits). */\n hidden: boolean;\n}\n\n/**\n * The wire schemas one collection shares with its client: every request and response\n * envelope for this collection's routes, composed from the payload schema, so the\n * client and the Worker parse the same bytes with the same rules.\n */\nexport interface ArchiveContract<P = unknown, C extends number = number> {\n /** The public entry emitted by list, detail and publish/update responses. */\n entry: Schema<ArchiveEntry<P, C>>;\n /** The own-publications response: `{ entries: ArchiveOwnedEntry[] }`. */\n ownedEntries: Schema<{ entries: ArchiveOwnedEntry<P, C>[] }>;\n /** The private owner entry — the public entry plus the create retry key. */\n ownedEntry: Schema<ArchiveOwnedEntry<P, C>>;\n /** One list page: entries plus the forward/backward cursors around it. */\n page: Schema<ArchivePage<ArchiveEntry<P, C>>>;\n /** The publish request: `{ clientKey, payload }`. */\n publishInput: Schema<{ clientKey: string; payload: P }, unknown>;\n /** The update request: `{ expectedRevision, payload }`. */\n updateInput: Schema<{ expectedRevision: number; payload: P }, unknown>;\n}\n\n/** One registered collection: the derived runtime seam the Worker consumes. */\nexport interface ArchiveCollection<P extends object = object, C extends number = number> {\n readonly contentVersion: C;\n /** The collection's wire schemas, shared with its client. */\n readonly contract: ArchiveContract<P, C>;\n /**\n * Validates an untrusted payload and derives everything the archive stores.\n * Null rejects invalid content: JSON/schema failures, title/tag bounds or the\n * canonical payload byte ceiling. Routes answer 422 `invalid-payload` for null.\n * Callback failures and deployment invariant violations throw ArchiveDerivationError.\n */\n readonly derive: (raw: unknown) => Promise<ArchiveDerivation | null>;\n readonly filters: readonly string[];\n readonly name: string;\n readonly tags: readonly string[];\n}\n\n/** The envelope shape shared by the public and owned compositions. */\nconst envelopeShape = <P, C extends number>(payload: Schema<P>, contentVersion: C) =>\n ({\n author: archiveAuthorSchema,\n contentHash: hex64Schema,\n contentVersion: s.literal(contentVersion),\n id: uuidSchema,\n likeCount: s.number().int().min(0).max(Number.MAX_SAFE_INTEGER),\n payload,\n publishedAt: isoTimestampSchema,\n revision: s.number().int().min(1).max(Number.MAX_SAFE_INTEGER),\n schemaVersion: s.literal(ARCHIVE_CONTRACT_VERSION),\n updatedAt: isoTimestampSchema,\n }) as const;\n\n/**\n * Builds one collection from its definition. Malformed definitions fail here, at\n * registration time — never mid-request: the name must be a routable `[a-z0-9-]`\n * segment, the content version a positive integer, and every declared filter and\n * tag value well-formed text of `1..vocabChars` chars — the bound cursors and the\n * meta route both rely on it. The core derives everything else per payload:\n * parsing, canonical serialization, hashing over the collection's versioned\n * envelope, the emitted-entry schemas, and every storage bound.\n */\nexport function defineArchiveCollection<\n P extends object,\n const C extends number,\n const F extends string = never,\n const T extends string = never,\n>(definition: ArchiveCollectionDefinition<P, C, F, T>): ArchiveCollection<P, C> {\n const { canonicalize, contentVersion, index, name, payloadSchema } = definition;\n if (\n typeof index !== 'function' ||\n !payloadSchema ||\n typeof payloadSchema.safeParse !== 'function' ||\n (canonicalize !== undefined && typeof canonicalize !== 'function')\n ) {\n throw new ArchiveDefinitionError(\n 'Collection requires a payload schema and synchronous indexing/canonicalization functions.',\n );\n }\n if (\n (definition.filters !== undefined && !Array.isArray(definition.filters)) ||\n (definition.tags !== undefined && !Array.isArray(definition.tags))\n ) {\n throw new ArchiveDefinitionError('Collection vocabularies must be arrays.');\n }\n const filters = Object.freeze([...(definition.filters ?? [])]);\n const tags = Object.freeze([...(definition.tags ?? [])]);\n if (typeof name !== 'string' || !ARCHIVE_COLLECTION_NAME_PATTERN.test(name)) {\n throw new ArchiveDefinitionError(`Archive collection name must match [a-z0-9-]{1,32}: ${JSON.stringify(name)}`);\n }\n if (!Number.isSafeInteger(contentVersion) || contentVersion < 1) {\n throw new ArchiveDefinitionError(`Archive collection ${name}: contentVersion must be a safe integer >= 1.`);\n }\n const validateVocabulary = (kind: 'filters' | 'tags', values: readonly string[]): void => {\n for (const value of values) {\n if (\n typeof value !== 'string' ||\n value.length === 0 ||\n value.length > ARCHIVE_LIMITS.vocabChars ||\n !isWellFormedText(value)\n ) {\n throw new ArchiveDefinitionError(\n `Archive collection ${name}: every ${kind} value must be well-formed text of 1..${ARCHIVE_LIMITS.vocabChars} chars.`,\n );\n }\n }\n };\n validateVocabulary('filters', filters);\n validateVocabulary('tags', tags);\n const filterValues = new Set<string>(filters);\n const tagValues = new Set<string>(tags);\n\n const derive = async (raw: unknown): Promise<ArchiveDerivation | null> => {\n let stage: ArchiveDerivationStage = 'schema';\n try {\n if (canonicalObjectJson(raw) === null) return null;\n const parsed = payloadSchema.safeParse(raw);\n if (!parsed.success) return null;\n const parsedJson = canonicalObjectJson(parsed.data, true, raw);\n if (parsedJson === null)\n throw new ArchiveDerivationError(stage, 'Payload schema produced invalid or lossy JSON.');\n stage = 'canonicalize';\n const canonical = canonicalize === undefined ? parsed.data : canonicalize(parsed.data);\n // A canonicalize step that leaves the payload schema is a collection bug, not data to store.\n const normalizedJson = canonicalObjectJson(canonical, true, JSON.parse(parsedJson));\n if (normalizedJson === null) throw new ArchiveDerivationError(stage, 'Canonicalization produced invalid JSON.');\n stage = 'schema';\n const revalidated = payloadSchema.safeParse(JSON.parse(normalizedJson));\n if (!revalidated.success)\n throw new ArchiveDerivationError(\n canonicalize === undefined ? 'schema' : 'canonicalize',\n 'Canonical payload is outside the payload schema.',\n { cause: revalidated.error },\n );\n const canonicalJson = canonicalObjectJson(revalidated.data, true, JSON.parse(normalizedJson));\n if (canonicalJson === null || canonicalJson !== normalizedJson)\n throw new ArchiveDerivationError(stage, 'Payload schema is not stable on canonical JSON.');\n if (canonicalize !== undefined) {\n const repeatedPayload = payloadSchema.parse(JSON.parse(canonicalJson));\n stage = 'canonicalize';\n const repeated = canonicalize(repeatedPayload);\n if (canonicalObjectJson(repeated, true) !== canonicalJson)\n throw new ArchiveDerivationError(stage, 'Canonicalization is not idempotent.');\n }\n\n stage = 'index';\n const { filter, tags: rawTags, title: rawTitle } = index(revalidated.data);\n // Indexing may read the final parsed value, but must not change the stored content.\n if (canonicalObjectJson(revalidated.data, true) !== canonicalJson)\n throw new ArchiveDerivationError(stage, 'Indexing mutated the canonical payload.');\n if (typeof rawTitle !== 'string' || !Array.isArray(rawTags))\n throw new ArchiveDerivationError(stage, 'Indexing must return a string title and a tags array.');\n const title = rawTitle.trim();\n if (title.length === 0 || title.length > ARCHIVE_LIMITS.titleChars || !isWellFormedText(title)) return null;\n if (filter !== null && (typeof filter !== 'string' || !isWellFormedText(filter)))\n throw new ArchiveDerivationError(stage, 'Indexing returned an invalid filter.');\n const uniqueTags: string[] = [];\n for (const tag of rawTags) {\n if (typeof tag !== 'string' || !isWellFormedText(tag))\n throw new ArchiveDerivationError(stage, 'Indexing returned an invalid tag.');\n if (!uniqueTags.includes(tag)) uniqueTags.push(tag);\n }\n // A derivation outside the declared vocabulary would store a row no list query\n // can reach — the query route 400s the very value the collection derived.\n if ((filter !== null && !filterValues.has(filter)) || uniqueTags.some((tag) => !tagValues.has(tag)))\n throw new ArchiveDerivationError(stage, 'Indexing returned a value outside the declared vocabularies.');\n if (uniqueTags.length > ARCHIVE_LIMITS.tagsPerEntry) return null;\n\n if (new TextEncoder().encode(canonicalJson).byteLength > ARCHIVE_LIMITS.payloadBytes) return null;\n stage = 'hash';\n const contentHash = await sha256Hex(`{\"contentVersion\":${contentVersion},\"payload\":${canonicalJson}}`);\n return { canonicalJson, contentHash, contentVersion, filter, tags: uniqueTags, title };\n } catch (cause) {\n if (cause instanceof ArchiveDerivationError) throw cause;\n throw new ArchiveDerivationError(stage, 'Collection content derivation failed.', { cause });\n }\n };\n\n // The cast pins the envelope's payload member to the collection's output type:\n // `payloadSchema` is `Schema<P, unknown>` (input unknown), and inference through\n // the generic shape helper would widen the member to `unknown`.\n const entrySchema: Schema<ArchiveEntry<P, C>> = s.object(envelopeShape(payloadSchema as Schema<P>, contentVersion));\n const ownedEntrySchema: Schema<ArchiveOwnedEntry<P, C>> = s.object({\n ...envelopeShape(payloadSchema as Schema<P>, contentVersion),\n clientKey: uuidSchema,\n hidden: s.boolean(),\n });\n const contract: ArchiveContract<P, C> = {\n entry: entrySchema,\n ownedEntries: s.object({ entries: s.array(ownedEntrySchema) }),\n ownedEntry: ownedEntrySchema,\n page: s.object({\n cursor: s.string().nullable(),\n entries: s.array(entrySchema),\n }),\n publishInput: s.object({ clientKey: uuidSchema, payload: payloadSchema }),\n updateInput: s.object({\n expectedRevision: s.number().int().min(1).max(Number.MAX_SAFE_INTEGER),\n payload: payloadSchema,\n }),\n };\n\n return Object.freeze({ contentVersion, contract: Object.freeze(contract), derive, filters, name, tags });\n}\n"],"mappings":"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"}
|