@vielzeug/archive 0.0.0-stage → 26.10.7

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.
Files changed (118) hide show
  1. package/README.md +81 -2
  2. package/dist/_crypto.cjs +2 -0
  3. package/dist/_crypto.cjs.map +1 -0
  4. package/dist/_crypto.d.ts +7 -0
  5. package/dist/_crypto.d.ts.map +1 -0
  6. package/dist/_crypto.js +2 -0
  7. package/dist/_crypto.js.map +1 -0
  8. package/dist/_json.cjs +2 -0
  9. package/dist/_json.cjs.map +1 -0
  10. package/dist/_json.d.ts +4 -0
  11. package/dist/_json.d.ts.map +1 -0
  12. package/dist/_json.js +2 -0
  13. package/dist/_json.js.map +1 -0
  14. package/dist/auth/config.cjs +2 -0
  15. package/dist/auth/config.cjs.map +1 -0
  16. package/dist/auth/config.d.ts +88 -0
  17. package/dist/auth/config.d.ts.map +1 -0
  18. package/dist/auth/config.js +2 -0
  19. package/dist/auth/config.js.map +1 -0
  20. package/dist/auth/errors.cjs +2 -0
  21. package/dist/auth/errors.cjs.map +1 -0
  22. package/dist/auth/errors.d.ts +10 -0
  23. package/dist/auth/errors.d.ts.map +1 -0
  24. package/dist/auth/errors.js +2 -0
  25. package/dist/auth/errors.js.map +1 -0
  26. package/dist/auth/store.cjs +19 -0
  27. package/dist/auth/store.cjs.map +1 -0
  28. package/dist/auth/store.d.ts +45 -0
  29. package/dist/auth/store.d.ts.map +1 -0
  30. package/dist/auth/store.js +19 -0
  31. package/dist/auth/store.js.map +1 -0
  32. package/dist/auth/worker.cjs +16 -0
  33. package/dist/auth/worker.cjs.map +1 -0
  34. package/dist/auth/worker.d.ts +47 -0
  35. package/dist/auth/worker.d.ts.map +1 -0
  36. package/dist/auth/worker.js +16 -0
  37. package/dist/auth/worker.js.map +1 -0
  38. package/dist/auth.cjs +1 -0
  39. package/dist/auth.d.ts +5 -0
  40. package/dist/auth.d.ts.map +1 -0
  41. package/dist/auth.js +1 -0
  42. package/dist/collection.cjs +2 -0
  43. package/dist/collection.cjs.map +1 -0
  44. package/dist/collection.d.ts +139 -0
  45. package/dist/collection.d.ts.map +1 -0
  46. package/dist/collection.js +2 -0
  47. package/dist/collection.js.map +1 -0
  48. package/dist/contract.cjs +2 -0
  49. package/dist/contract.cjs.map +1 -0
  50. package/dist/contract.d.ts +228 -0
  51. package/dist/contract.d.ts.map +1 -0
  52. package/dist/contract.js +2 -0
  53. package/dist/contract.js.map +1 -0
  54. package/dist/errors.cjs +2 -0
  55. package/dist/errors.cjs.map +1 -0
  56. package/dist/errors.d.ts +12 -0
  57. package/dist/errors.d.ts.map +1 -0
  58. package/dist/errors.js +2 -0
  59. package/dist/errors.js.map +1 -0
  60. package/dist/index.cjs +1 -0
  61. package/dist/index.d.ts +4 -0
  62. package/dist/index.d.ts.map +1 -0
  63. package/dist/index.js +1 -0
  64. package/dist/worker/_pagination.cjs +2 -0
  65. package/dist/worker/_pagination.cjs.map +1 -0
  66. package/dist/worker/_pagination.d.ts +31 -0
  67. package/dist/worker/_pagination.d.ts.map +1 -0
  68. package/dist/worker/_pagination.js +2 -0
  69. package/dist/worker/_pagination.js.map +1 -0
  70. package/dist/worker/_service.cjs +2 -0
  71. package/dist/worker/_service.cjs.map +1 -0
  72. package/dist/worker/_service.d.ts +5 -0
  73. package/dist/worker/_service.d.ts.map +1 -0
  74. package/dist/worker/_service.js +2 -0
  75. package/dist/worker/_service.js.map +1 -0
  76. package/dist/worker/auth.cjs +2 -0
  77. package/dist/worker/auth.cjs.map +1 -0
  78. package/dist/worker/auth.d.ts +34 -0
  79. package/dist/worker/auth.d.ts.map +1 -0
  80. package/dist/worker/auth.js +2 -0
  81. package/dist/worker/auth.js.map +1 -0
  82. package/dist/worker/env.cjs +2 -0
  83. package/dist/worker/env.cjs.map +1 -0
  84. package/dist/worker/env.d.ts +73 -0
  85. package/dist/worker/env.d.ts.map +1 -0
  86. package/dist/worker/env.js +2 -0
  87. package/dist/worker/env.js.map +1 -0
  88. package/dist/worker/http.cjs +2 -0
  89. package/dist/worker/http.cjs.map +1 -0
  90. package/dist/worker/http.d.ts +66 -0
  91. package/dist/worker/http.d.ts.map +1 -0
  92. package/dist/worker/http.js +2 -0
  93. package/dist/worker/http.js.map +1 -0
  94. package/dist/worker/publications.cjs +2 -0
  95. package/dist/worker/publications.cjs.map +1 -0
  96. package/dist/worker/publications.d.ts +60 -0
  97. package/dist/worker/publications.d.ts.map +1 -0
  98. package/dist/worker/publications.js +2 -0
  99. package/dist/worker/publications.js.map +1 -0
  100. package/dist/worker/sql.cjs +60 -0
  101. package/dist/worker/sql.cjs.map +1 -0
  102. package/dist/worker/sql.d.ts +87 -0
  103. package/dist/worker/sql.d.ts.map +1 -0
  104. package/dist/worker/sql.js +60 -0
  105. package/dist/worker/sql.js.map +1 -0
  106. package/dist/worker/worker.cjs +2 -0
  107. package/dist/worker/worker.cjs.map +1 -0
  108. package/dist/worker/worker.d.ts +72 -0
  109. package/dist/worker/worker.d.ts.map +1 -0
  110. package/dist/worker/worker.js +2 -0
  111. package/dist/worker/worker.js.map +1 -0
  112. package/dist/worker.cjs +1 -0
  113. package/dist/worker.d.ts +4 -0
  114. package/dist/worker.d.ts.map +1 -0
  115. package/dist/worker.js +1 -0
  116. package/migrations/0001_community_archive.sql +173 -0
  117. package/migrations/auth/0001_auth.sql +65 -0
  118. package/package.json +72 -4
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.cjs","names":[],"sources":["../../src/worker/worker.ts"],"sourcesContent":["import type { ArchiveCollection } from '../collection';\nimport { ARCHIVE_LIMITS, type ArchiveErrorCode, archiveHealthSchema, PUBLISHER_HEADER } from '../contract';\nimport { ArchiveDefinitionError, ArchiveDerivationError } from '../errors';\nimport { requireSession, type Session, verifySession } from './auth';\nimport { type ApiConfig, type ArchiveWorkerEnv, apiConfig } from './env';\nimport {\n ApiError,\n type ArchiveDiagnostic,\n badRequest,\n corsHeaders,\n expiredSessionCookie,\n json,\n preflight,\n queryParams,\n requireMutationGuard,\n toErrorResponse,\n} from './http';\nimport {\n collectionMeta,\n deleteAccountProfile,\n deleteEntry,\n deleteProfile,\n erasePublisher,\n getEntry,\n getMe,\n healthJson,\n listEntries,\n listModerationLog,\n listReports,\n myLikes,\n myPublications,\n myReports,\n publishEntry,\n registerMe,\n renameMe,\n reportEntry,\n resolveReport,\n setHidden,\n setLike,\n updateEntry,\n} from './publications';\nimport { SQL } from './sql';\n\n/**\n * The community-archive Worker factory: anonymous public reads, cookie-authenticated\n * mutations behind strict Origin/request-marker guards, IP and subject rate limits,\n * operator moderation over the registered collections, and a daily expiry sweep for\n * retired publish retry keys.\n *\n * Trust model: configuration and signing keys come only from validated Env bindings;\n * the session cookie is the single credential; every response is either no-store\n * (private/error) or a 30s anonymous public cache entry. Runtime observability\n * flows exclusively through `tap()` — the core itself never logs.\n */\n\n/** The minimal execution-context surface the core uses; the runtime's satisfies it. */\nexport interface ArchiveExecutionContext {\n waitUntil(promise: Promise<unknown>): void;\n}\n\n/** One sanitized request-completion event: ids, codes, route classes and duration only. */\nexport interface ArchiveRequestEvent {\n /** 'ok' on success; otherwise the route's `ARCHIVE_ERROR_CODES` value. */\n code: ArchiveErrorCode | 'ok';\n diagnostic?: ArchiveDiagnostic;\n /** Whole-request wall-clock duration in milliseconds. */\n ms: number;\n requestId: string;\n /** A finite route class like `/collections/:collection/entries`, never the raw path. */\n route: string;\n status: number;\n type: 'request';\n}\n\n/** The daily retry-key sweep, observable like every other runtime behavior. */\nexport type ArchiveScheduledEvent =\n | {\n /** Retired retry-key rows this run removed. */\n removed: number;\n type: 'scheduled';\n status: 'ok';\n }\n | { type: 'scheduled'; status: 'failed'; diagnostic: ArchiveDiagnostic };\n\nexport type ArchiveEvent =\n | ArchiveRequestEvent\n | ArchiveScheduledEvent\n | { readonly type: 'cache-error'; readonly diagnostic: ArchiveDiagnostic };\n\n/**\n * The Worker's exported-handler shape plus its observability side channel. Declared\n * structurally so the package needs no `@cloudflare/workers-types`: a deployment's\n * generated `ExportedHandler<Env>` accepts the factory's result as-is.\n */\nexport interface ArchiveWorkerHandler<E extends ArchiveWorkerEnv = ArchiveWorkerEnv> {\n fetch(request: Request, env: E, ctx: ArchiveExecutionContext): Promise<Response>;\n scheduled?(controller: unknown, env: E, ctx: ArchiveExecutionContext): void | Promise<void>;\n /**\n * Observes every request's outcome and every scheduled sweep. The handler owns\n * logging policy (console, a structured logger, or nothing); handler errors are\n * swallowed so observability can never affect the Worker, and with no handlers\n * registered there is zero work.\n */\n tap(handler: (event: ArchiveEvent) => void, options?: { readonly signal?: AbortSignal }): () => void;\n}\n\n/** One dispatchable route: the id both the switch and the event labels derive from. */\ntype RouteId =\n | 'health'\n | 'signOut'\n | 'meta'\n | 'reports'\n | 'resolveReport'\n | 'entries'\n | 'entry'\n | 'like'\n | 'report'\n | 'hidden'\n | 'me'\n | 'accountErase'\n | 'mePublications'\n | 'meLikes'\n | 'meReports'\n | 'modLog'\n | 'publisherErase';\n\n/** One matched route: its id (for dispatch) and label (for events), plus captures. */\ninterface RouteMatch {\n id: RouteId;\n /** The finite event label, never the raw path. */\n label: string;\n /** The methods the route serves; anything else is 405 before any guard runs. */\n methods: readonly string[];\n /** The path's captured segments, in pattern order: collection first, then id. */\n segments: readonly string[];\n}\n\n/**\n * The one route table: dispatch, methods, and event labels all read from here, so a\n * new route can never miss its observability class or its method set. Methods are\n * enforced before any guard runs — a wrong-method request never spends rate-limit\n * budget, verifies a session, or answers an auth-shaped error.\n */\nconst ROUTES: readonly { id: RouteId; label: string; methods: readonly string[]; pattern: RegExp }[] = [\n { id: 'accountErase', label: '/me/account', methods: ['DELETE'], pattern: /^\\/api\\/v1\\/me\\/account$/ },\n { id: 'health', label: '/health', methods: ['GET'], pattern: /^\\/api\\/v1\\/health$/ },\n { id: 'signOut', label: '/sign-out', methods: ['POST'], pattern: /^\\/api\\/v1\\/sign-out$/ },\n {\n id: 'resolveReport',\n label: '/collections/:collection/reports/:id/resolved',\n methods: ['PUT'],\n pattern: /^\\/api\\/v1\\/collections\\/([a-z0-9-]+)\\/reports\\/([0-9a-f-]{36})\\/resolved$/i,\n },\n {\n id: 'meta',\n label: '/collections/:collection',\n methods: ['GET'],\n pattern: /^\\/api\\/v1\\/collections\\/([a-z0-9-]+)$/,\n },\n {\n id: 'reports',\n label: '/collections/:collection/reports',\n methods: ['GET'],\n pattern: /^\\/api\\/v1\\/collections\\/([a-z0-9-]+)\\/reports$/,\n },\n {\n id: 'entries',\n label: '/collections/:collection/entries',\n methods: ['GET', 'POST'],\n pattern: /^\\/api\\/v1\\/collections\\/([a-z0-9-]+)\\/entries$/,\n },\n {\n id: 'entry',\n label: '/collections/:collection/entries/:id',\n methods: ['GET', 'PUT', 'DELETE'],\n pattern: /^\\/api\\/v1\\/collections\\/([a-z0-9-]+)\\/entries\\/([0-9a-f-]{36})$/i,\n },\n {\n id: 'like',\n label: '/collections/:collection/entries/:id/like',\n methods: ['PUT', 'DELETE'],\n pattern: /^\\/api\\/v1\\/collections\\/([a-z0-9-]+)\\/entries\\/([0-9a-f-]{36})\\/like$/i,\n },\n {\n id: 'report',\n label: '/collections/:collection/entries/:id/report',\n methods: ['POST'],\n pattern: /^\\/api\\/v1\\/collections\\/([a-z0-9-]+)\\/entries\\/([0-9a-f-]{36})\\/report$/i,\n },\n {\n id: 'hidden',\n label: '/collections/:collection/entries/:id/hidden',\n methods: ['PUT', 'DELETE'],\n pattern: /^\\/api\\/v1\\/collections\\/([a-z0-9-]+)\\/entries\\/([0-9a-f-]{36})\\/hidden$/i,\n },\n {\n id: 'modLog',\n label: '/moderation-log',\n methods: ['GET'],\n pattern: /^\\/api\\/v1\\/moderation-log$/,\n },\n {\n id: 'publisherErase',\n label: '/publishers/:id',\n methods: ['DELETE'],\n pattern: /^\\/api\\/v1\\/publishers\\/([0-9a-f-]{36})$/i,\n },\n { id: 'me', label: '/me', methods: ['GET', 'POST', 'PUT', 'DELETE'], pattern: /^\\/api\\/v1\\/me$/ },\n { id: 'mePublications', label: '/me/publications', methods: ['GET'], pattern: /^\\/api\\/v1\\/me\\/publications$/ },\n { id: 'meLikes', label: '/me/likes', methods: ['GET'], pattern: /^\\/api\\/v1\\/me\\/likes$/ },\n { id: 'meReports', label: '/me/reports', methods: ['GET'], pattern: /^\\/api\\/v1\\/me\\/reports$/ },\n];\n\nconst matchRoute = (pathname: string): RouteMatch | null => {\n for (const route of ROUTES) {\n const found = pathname.match(route.pattern);\n if (found) return { id: route.id, label: route.label, methods: route.methods, segments: found.slice(1) };\n }\n return null;\n};\n\n/** The matched route's event label; unmatched paths stay 'unknown', never the raw path. */\nconst routeLabel = (pathname: string): string => matchRoute(pathname)?.label ?? 'unknown';\n\nconst UUID_PATH = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/** The routes whose second path segment is an entry id: they share the one id rule. */\nconst ID_SEGMENT_ROUTES: ReadonlySet<RouteId> = new Set(['entry', 'like', 'report', 'hidden', 'resolveReport']);\n\nconst notAllowed = (): ApiError => new ApiError(405, 'method-not-allowed', 'That method is not supported here.');\n\nconst hmacKeys = new WeakMap<ApiConfig, Promise<CryptoKey>>();\n/** Peppered HMAC: rate-limit keys cannot be reversed to IPs without the deployment secret. */\nconst rateLimitHash = async (config: ApiConfig, text: string): Promise<string> => {\n let key = hmacKeys.get(config);\n if (key === undefined) {\n key = crypto.subtle.importKey(\n 'raw',\n new TextEncoder().encode(config.rateLimitPepper),\n { hash: 'SHA-256', name: 'HMAC' },\n false,\n ['sign'],\n );\n hmacKeys.set(config, key);\n }\n const mac = await crypto.subtle.sign('HMAC', await key, new TextEncoder().encode(text));\n return [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, '0')).join('');\n};\n\n/**\n * The Retry-After the 429s carry. The platform limiter does not expose its period at\n * runtime, so deployments must configure 60-second windows — a longer window would\n * make the header lie.\n */\nconst RATE_LIMIT_RETRY_AFTER_SECONDS = 60;\n\n/** The connecting IP, peppered-HMAC'd, keys the read limit; loopback only when APP_ENV=local. */\nasync function readLimit(config: ApiConfig, env: ArchiveWorkerEnv, request: Request): Promise<void> {\n const ip = request.headers.get('cf-connecting-ip') ?? (config.appEnv === 'local' ? '127.0.0.1' : null);\n if (ip === null) throw new ApiError(503, 'unavailable', 'The service is temporarily unavailable.');\n const { success } = await env.READ_LIMIT.limit({ key: await rateLimitHash(config, ip) });\n if (!success) {\n throw new ApiError(429, 'rate-limited', 'Too many requests. Try again later.', {\n retryAfterSeconds: RATE_LIMIT_RETRY_AFTER_SECONDS,\n });\n }\n}\n\n/** The verified issuer+subject, peppered-HMAC'd, keys the write limit ahead of every mutation. */\nasync function writeLimit(config: ApiConfig, env: ArchiveWorkerEnv, issuer: string, subject: string): Promise<void> {\n const { success } = await env.WRITE_LIMIT.limit({\n key: await rateLimitHash(config, JSON.stringify([issuer, subject])),\n });\n if (!success) {\n throw new ApiError(429, 'rate-limited', 'Too many requests. Try again later.', {\n retryAfterSeconds: RATE_LIMIT_RETRY_AFTER_SECONDS,\n });\n }\n}\n\n/** Only anonymous list reads may serve from the edge cache. */\nasync function cachedPublicGet(\n request: Request,\n ctx: ArchiveExecutionContext,\n produce: () => Promise<Response>,\n onFailure: () => void,\n): Promise<Response> {\n const anonymous = request.headers.get('cookie') === null && request.headers.get('authorization') === null;\n if (!anonymous) return produce();\n const url = new URL(request.url);\n url.search = new URLSearchParams(\n [...url.searchParams.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),\n ).toString();\n const edgeCache = caches.default;\n const key = new Request(url.toString(), { method: 'GET' });\n const cached = await edgeCache.match(key).catch(() => {\n onFailure();\n return undefined;\n });\n if (cached) return cached;\n const response = await produce();\n response.headers.set(\n 'cache-control',\n `public, max-age=${ARCHIVE_LIMITS.publicCacheSeconds}, s-maxage=${ARCHIVE_LIMITS.publicCacheSeconds}`,\n );\n ctx.waitUntil(edgeCache.put(key, response.clone()).catch(onFailure));\n return response;\n}\n\nconst withCors = (response: Response, request: Request, config: ApiConfig): Response => {\n const headers = corsHeaders(request, config);\n if (Object.keys(headers).length === 0) return response;\n const next = new Response(response.body, response);\n for (const [key, value] of Object.entries(headers)) next.headers.set(key, value);\n return next;\n};\n\n/**\n * Fail-closed precondition for registered publishers: the UI pins the opaque\n * publisher UUID it believes the session belongs to, so a queued write from one\n * tab can never land under another account that took over the shared cookie.\n * The header is never an identity source — every check still resolves the\n * publisher from the verified JWT — it only vetoes mismatched writes.\n */\nfunction requireExpectedPublisher(request: Request, session: Session, exempt: boolean): void {\n if (exempt || session.publisher === null) return;\n const expected = request.headers.get(PUBLISHER_HEADER);\n if (expected === null || !UUID_PATH.test(expected)) {\n throw new ApiError(400, 'bad-request', 'The request is not valid.');\n }\n if (expected !== session.publisher.id) {\n throw new ApiError(409, 'publisher-changed', 'The signed-in publisher changed since this request was prepared.');\n }\n}\n\n/**\n * Builds the Worker for one deployment's registered collections. Each collection owns\n * one URL + storage dimension; a duplicate registration is a deployment bug and\n * fails fast.\n */\nexport function createArchiveWorker<E extends ArchiveWorkerEnv>(\n collections: readonly ArchiveCollection[],\n): ArchiveWorkerHandler<E> {\n const registered = new Map<string, ArchiveCollection>();\n for (const collection of collections) {\n if (registered.has(collection.name))\n throw new ArchiveDefinitionError(`Duplicate archive collection: ${collection.name}`);\n registered.set(collection.name, collection);\n }\n\n /** Resolves the route's collection segment; an unregistered collection is a 404, not a 500. */\n const collectionFor = (segment: string): ArchiveCollection => {\n const collection = registered.get(segment);\n if (collection === undefined) throw new ApiError(404, 'not-found', 'No such route.');\n return collection;\n };\n\n async function handle(request: Request, env: E, ctx: ArchiveExecutionContext): Promise<Response> {\n const config = apiConfig(env);\n const url = new URL(request.url);\n const { pathname } = url;\n const method = request.method;\n\n if (!pathname.startsWith('/api/v1/')) throw new ApiError(404, 'not-found', 'No such route.');\n if (method === 'OPTIONS') return preflight(request, config);\n\n const match = matchRoute(pathname);\n // An unknown path inside the mount answers 404 like the paths outside it, and a\n // known route under an unsupported method answers 405 — both with no side\n // effects: no rate-limit spend, no session verification, no auth state leaked,\n // no guard-shaped error for a method the route does not serve.\n if (match === null) throw new ApiError(404, 'not-found', 'No such route.');\n if (!match.methods.includes(method)) throw notAllowed();\n\n // A malformed entry id is the same class of client bug: it answers 400 before\n // any rate-limit spend, session verification or auth state.\n if (ID_SEGMENT_ROUTES.has(match.id) && !UUID_PATH.test(match.segments[1] ?? '')) throw badRequest();\n\n // Health sits deliberately outside the rate-limit and session pipelines: uptime\n // probes must never burn the anonymous read budget or couple to authentication,\n // and it touches the database exactly once (the schema-version read).\n if (match.id === 'health') {\n return withCors(await healthJson(config, env), request, config);\n }\n if (match.id === 'reports') queryParams(url, ['cursor', 'limit', 'status']);\n\n await readLimit(config, env, request);\n\n // The same-origin mutation contract is proven BEFORE any identity work: a\n // foreign-origin write or missing marker must fail with 403 even while carrying a\n // garbage token — JWKS/JWT cost and 401 semantics stay behind the origin fence.\n const isMutation = method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS';\n if (isMutation) requireMutationGuard(request, config);\n\n // Session verification runs only where identity matters: private routes, the\n // moderation surfaces and every mutation. Public reads (list, detail, meta) never\n // invoke JWKS or the publisher lookup — browsing must not couple to authentication\n // availability. Sign-out is a cookie-clearing reply, not a session operation.\n const isPublicGet = method === 'GET' && (match.id === 'meta' || match.id === 'entries' || match.id === 'entry');\n const isSignOut = match.id === 'signOut' && method === 'POST';\n const session =\n isPublicGet || isSignOut ? null : await verifySession(request, config, env, match.id === 'accountErase');\n\n if (isMutation) {\n if (isSignOut) {\n // The shared `archiveOkSchema` body; every session cookie attribute is the\n // contract's, never ad hoc.\n const response = json(\n 200,\n { ok: true },\n {\n 'cache-control': 'no-store',\n 'set-cookie': expiredSessionCookie(config.appEnv === 'production'),\n },\n );\n return withCors(response, request, config);\n }\n const verified = requireSession(session);\n // The free fail-closed header precondition runs before the quota token is spent:\n // a stale tab's rejected writes must not consume the current account's budget.\n requireExpectedPublisher(request, verified, match.id === 'me' && method === 'POST');\n await writeLimit(config, env, verified.subject.issuer, verified.subject.subject);\n }\n\n const jsonWithCors = (response: Response) => withCors(response, request, config);\n\n switch (match.id) {\n case 'meta': {\n queryParams(url, []);\n return jsonWithCors(collectionMeta(collectionFor(match.segments[0] ?? '')));\n }\n case 'entries': {\n const collection = collectionFor(match.segments[0] ?? '');\n if (method === 'GET') {\n const response = await cachedPublicGet(\n request,\n ctx,\n () => listEntries(collection, request, env),\n () => emit({ diagnostic: { kind: 'cache' }, type: 'cache-error' }),\n );\n return jsonWithCors(response);\n }\n if (method === 'POST') {\n return jsonWithCors(await publishEntry(collection, request, env, config, requireSession(session)));\n }\n throw notAllowed();\n }\n case 'entry': {\n const collection = collectionFor(match.segments[0] ?? '');\n const id = match.segments[1] ?? '';\n if (method === 'GET') {\n queryParams(url, []);\n return jsonWithCors(await getEntry(collection, env, id));\n }\n if (method === 'PUT') {\n return jsonWithCors(await updateEntry(collection, request, env, config, requireSession(session), id));\n }\n if (method === 'DELETE') {\n return jsonWithCors(await deleteEntry(collection, env, requireSession(session), id));\n }\n throw notAllowed();\n }\n case 'like': {\n const collection = collectionFor(match.segments[0] ?? '');\n const id = match.segments[1] ?? '';\n if (method === 'PUT') {\n return jsonWithCors(await setLike(collection, env, config, requireSession(session), id, true));\n }\n if (method === 'DELETE') {\n return jsonWithCors(await setLike(collection, env, config, requireSession(session), id, false));\n }\n throw notAllowed();\n }\n case 'report': {\n if (method !== 'POST') throw notAllowed();\n const collection = collectionFor(match.segments[0] ?? '');\n return jsonWithCors(\n await reportEntry(collection, request, env, requireSession(session), match.segments[1] ?? ''),\n );\n }\n case 'hidden': {\n if (method !== 'PUT' && method !== 'DELETE') throw notAllowed();\n const collection = collectionFor(match.segments[0] ?? '');\n return jsonWithCors(\n await setHidden(collection, env, requireSession(session), match.segments[1] ?? '', method === 'PUT'),\n );\n }\n case 'reports': {\n // Private reads carry the same fail-closed precondition as writes: a stale tab\n // must not read another publisher's moderation queue after an account switch.\n const verified = requireSession(session);\n requireExpectedPublisher(request, verified, false);\n return jsonWithCors(await listReports(collectionFor(match.segments[0] ?? ''), request, env, verified));\n }\n case 'resolveReport': {\n queryParams(url, []);\n return jsonWithCors(\n await resolveReport(\n collectionFor(match.segments[0] ?? ''),\n env,\n requireSession(session),\n match.segments[1] ?? '',\n ),\n );\n }\n case 'modLog': {\n // The accountability read carries the same fail-closed precondition as every\n // private surface: a stale tab must not read the moderation history after an\n // account switch.\n const verified = requireSession(session);\n requireExpectedPublisher(request, verified, false);\n return jsonWithCors(await listModerationLog(request, env, verified));\n }\n case 'publisherErase': {\n // The terminal moderation action: erasing a publisher removes their row, and\n // FK cascades take the publications, likes and retry keys with it — the same\n // cascade a self-erase walks, applied to any publisher the operator names.\n if (method !== 'DELETE') throw notAllowed();\n return jsonWithCors(await erasePublisher(env, requireSession(session), match.segments[0] ?? ''));\n }\n case 'accountErase': {\n queryParams(url, []);\n return jsonWithCors(await deleteAccountProfile(env, requireSession(session)));\n }\n case 'me': {\n if (method === 'GET') return jsonWithCors(getMe(session));\n if (method === 'POST') return jsonWithCors(await registerMe(request, env, requireSession(session)));\n if (method === 'PUT') return jsonWithCors(await renameMe(request, env, requireSession(session)));\n if (method === 'DELETE') return jsonWithCors(await deleteProfile(env, requireSession(session)));\n throw notAllowed();\n }\n case 'mePublications':\n case 'meLikes':\n case 'meReports': {\n // Private reads carry the same fail-closed precondition as writes: a stale tab\n // must not consume another publisher's data after a shared-cookie account switch.\n const verified = requireSession(session);\n requireExpectedPublisher(request, verified, false);\n if (match.id === 'mePublications') {\n // The owner's entries are per-collection: the collection param names the\n // registered collection, and an absent or unregistered value is a malformed\n // request, not an empty list.\n const params = queryParams(url, ['collection']);\n const requested = params.get('collection');\n if (requested === null) throw badRequest();\n const collection = registered.get(requested);\n if (collection === undefined) throw badRequest();\n return jsonWithCors(await myPublications(collection, env, verified));\n }\n return jsonWithCors(\n await (match.id === 'meReports' ? myReports(request, env, verified) : myLikes(request, env, verified)),\n );\n }\n default:\n throw notAllowed();\n }\n }\n\n const tappers = new Set<(event: ArchiveEvent) => void>();\n const emit = (event: ArchiveEvent): void => {\n if (tappers.size === 0) return;\n for (const tapper of tappers) {\n try {\n tapper(event);\n } catch {\n // Observability must never affect the Worker.\n }\n }\n };\n\n return {\n async fetch(request: Request, env: E, ctx: ArchiveExecutionContext): Promise<Response> {\n const requestId = crypto.randomUUID();\n const route = routeLabel(new URL(request.url).pathname);\n const startedAt = Date.now();\n try {\n const response = await handle(request, env, ctx);\n let diagnostic: ArchiveDiagnostic | undefined;\n if (response.status === 503 && route === '/health') {\n const health = archiveHealthSchema.parse(await response.clone().json());\n diagnostic = { kind: health.reason === 'database-unavailable' ? 'database' : 'schema' };\n }\n emit({\n code: response.ok ? 'ok' : 'unavailable',\n ms: Date.now() - startedAt,\n requestId,\n route,\n status: response.status,\n type: 'request',\n ...(diagnostic === undefined ? {} : { diagnostic }),\n });\n return response;\n } catch (error) {\n const response = toErrorResponse(error, requestId);\n emit({\n code: error instanceof ApiError ? error.code : 'internal',\n diagnostic:\n error instanceof ApiError\n ? error.diagnostic\n : { kind: error instanceof ArchiveDerivationError ? 'collection' : 'internal' },\n ms: Date.now() - startedAt,\n requestId,\n route,\n status: response.status,\n type: 'request',\n });\n // Allowed origins see the same CORS contract on errors as on successes.\n try {\n return withCors(response, request, apiConfig(env));\n } catch {\n return response; // the config error itself is the response\n }\n }\n },\n\n async scheduled(_controller: unknown, env: E, _ctx: ArchiveExecutionContext): Promise<void> {\n try {\n const timestamp = Date.now();\n const results = await env.DB.batch([\n env.DB.prepare(SQL.expireRequests).bind(timestamp),\n env.DB.prepare(SQL.expireRevocations).bind(timestamp),\n ]);\n emit({\n removed: results.reduce((sum, result) => sum + (result.meta?.changes ?? 0), 0),\n status: 'ok',\n type: 'scheduled',\n });\n } catch (error) {\n emit({ diagnostic: { kind: 'database' }, status: 'failed', type: 'scheduled' });\n throw error;\n }\n },\n\n tap(handler, options) {\n const signal = options?.signal;\n if (signal?.aborted) return () => {};\n const off = () => {\n tappers.delete(handler);\n signal?.removeEventListener('abort', off);\n };\n tappers.add(handler);\n signal?.addEventListener('abort', off, { once: true });\n return off;\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,72 @@
1
+ import type { ArchiveCollection } from '../collection';
2
+ import { type ArchiveErrorCode } from '../contract';
3
+ import { type ArchiveWorkerEnv } from './env';
4
+ import { type ArchiveDiagnostic } from './http';
5
+ /**
6
+ * The community-archive Worker factory: anonymous public reads, cookie-authenticated
7
+ * mutations behind strict Origin/request-marker guards, IP and subject rate limits,
8
+ * operator moderation over the registered collections, and a daily expiry sweep for
9
+ * retired publish retry keys.
10
+ *
11
+ * Trust model: configuration and signing keys come only from validated Env bindings;
12
+ * the session cookie is the single credential; every response is either no-store
13
+ * (private/error) or a 30s anonymous public cache entry. Runtime observability
14
+ * flows exclusively through `tap()` — the core itself never logs.
15
+ */
16
+ /** The minimal execution-context surface the core uses; the runtime's satisfies it. */
17
+ export interface ArchiveExecutionContext {
18
+ waitUntil(promise: Promise<unknown>): void;
19
+ }
20
+ /** One sanitized request-completion event: ids, codes, route classes and duration only. */
21
+ export interface ArchiveRequestEvent {
22
+ /** 'ok' on success; otherwise the route's `ARCHIVE_ERROR_CODES` value. */
23
+ code: ArchiveErrorCode | 'ok';
24
+ diagnostic?: ArchiveDiagnostic;
25
+ /** Whole-request wall-clock duration in milliseconds. */
26
+ ms: number;
27
+ requestId: string;
28
+ /** A finite route class like `/collections/:collection/entries`, never the raw path. */
29
+ route: string;
30
+ status: number;
31
+ type: 'request';
32
+ }
33
+ /** The daily retry-key sweep, observable like every other runtime behavior. */
34
+ export type ArchiveScheduledEvent = {
35
+ /** Retired retry-key rows this run removed. */
36
+ removed: number;
37
+ type: 'scheduled';
38
+ status: 'ok';
39
+ } | {
40
+ type: 'scheduled';
41
+ status: 'failed';
42
+ diagnostic: ArchiveDiagnostic;
43
+ };
44
+ export type ArchiveEvent = ArchiveRequestEvent | ArchiveScheduledEvent | {
45
+ readonly type: 'cache-error';
46
+ readonly diagnostic: ArchiveDiagnostic;
47
+ };
48
+ /**
49
+ * The Worker's exported-handler shape plus its observability side channel. Declared
50
+ * structurally so the package needs no `@cloudflare/workers-types`: a deployment's
51
+ * generated `ExportedHandler<Env>` accepts the factory's result as-is.
52
+ */
53
+ export interface ArchiveWorkerHandler<E extends ArchiveWorkerEnv = ArchiveWorkerEnv> {
54
+ fetch(request: Request, env: E, ctx: ArchiveExecutionContext): Promise<Response>;
55
+ scheduled?(controller: unknown, env: E, ctx: ArchiveExecutionContext): void | Promise<void>;
56
+ /**
57
+ * Observes every request's outcome and every scheduled sweep. The handler owns
58
+ * logging policy (console, a structured logger, or nothing); handler errors are
59
+ * swallowed so observability can never affect the Worker, and with no handlers
60
+ * registered there is zero work.
61
+ */
62
+ tap(handler: (event: ArchiveEvent) => void, options?: {
63
+ readonly signal?: AbortSignal;
64
+ }): () => void;
65
+ }
66
+ /**
67
+ * Builds the Worker for one deployment's registered collections. Each collection owns
68
+ * one URL + storage dimension; a duplicate registration is a deployment bug and
69
+ * fails fast.
70
+ */
71
+ export declare function createArchiveWorker<E extends ArchiveWorkerEnv>(collections: readonly ArchiveCollection[]): ArchiveWorkerHandler<E>;
72
+ //# sourceMappingURL=worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../src/worker/worker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,EAAkB,KAAK,gBAAgB,EAAyC,MAAM,aAAa,CAAC;AAG3G,OAAO,EAAkB,KAAK,gBAAgB,EAAa,MAAM,OAAO,CAAC;AACzE,OAAO,EAEL,KAAK,iBAAiB,EASvB,MAAM,QAAQ,CAAC;AA2BhB;;;;;;;;;;GAUG;AAEH,uFAAuF;AACvF,MAAM,WAAW,uBAAuB;IACtC,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC;CAC5C;AAED,2FAA2F;AAC3F,MAAM,WAAW,mBAAmB;IAClC,0EAA0E;IAC1E,IAAI,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAC9B,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,yDAAyD;IACzD,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,wFAAwF;IACxF,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,SAAS,CAAC;CACjB;AAED,+EAA+E;AAC/E,MAAM,MAAM,qBAAqB,GAC7B;IACE,+CAA+C;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,WAAW,CAAC;IAClB,MAAM,EAAE,IAAI,CAAC;CACd,GACD;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,QAAQ,CAAC;IAAC,UAAU,EAAE,iBAAiB,CAAA;CAAE,CAAC;AAE3E,MAAM,MAAM,YAAY,GACpB,mBAAmB,GACnB,qBAAqB,GACrB;IAAE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,iBAAiB,CAAA;CAAE,CAAC;AAE7E;;;;GAIG;AACH,MAAM,WAAW,oBAAoB,CAAC,CAAC,SAAS,gBAAgB,GAAG,gBAAgB;IACjF,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,uBAAuB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACjF,SAAS,CAAC,CAAC,UAAU,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,uBAAuB,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5F;;;;;OAKG;IACH,GAAG,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GAAG,MAAM,IAAI,CAAC;CACtG;AAuOD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,gBAAgB,EAC5D,WAAW,EAAE,SAAS,iBAAiB,EAAE,GACxC,oBAAoB,CAAC,CAAC,CAAC,CA+SzB"}
@@ -0,0 +1,2 @@
1
+ import{ARCHIVE_LIMITS as e,PUBLISHER_HEADER as t,archiveHealthSchema as n}from"../contract.js";import{ApiError as r,badRequest as i,corsHeaders as a,expiredSessionCookie as o,json as s,preflight as c,queryParams as l,requireMutationGuard as u,toErrorResponse as d}from"./http.js";import{ArchiveDefinitionError as f,ArchiveDerivationError as p}from"../errors.js";import{SQL as m}from"./sql.js";import{requireSession as h,verifySession as g}from"./auth.js";import{apiConfig as _}from"./env.js";import{collectionMeta as v,deleteAccountProfile as y,deleteEntry as b,deleteProfile as x,erasePublisher as S,getEntry as C,getMe as w,healthJson as T,listEntries as E,listModerationLog as D,listReports as O,myLikes as k,myPublications as A,myReports as j,publishEntry as M,registerMe as N,renameMe as P,reportEntry as F,resolveReport as I,setHidden as L,setLike as R,updateEntry as z}from"./publications.js";var B=[{id:`accountErase`,label:`/me/account`,methods:[`DELETE`],pattern:/^\/api\/v1\/me\/account$/},{id:`health`,label:`/health`,methods:[`GET`],pattern:/^\/api\/v1\/health$/},{id:`signOut`,label:`/sign-out`,methods:[`POST`],pattern:/^\/api\/v1\/sign-out$/},{id:`resolveReport`,label:`/collections/:collection/reports/:id/resolved`,methods:[`PUT`],pattern:/^\/api\/v1\/collections\/([a-z0-9-]+)\/reports\/([0-9a-f-]{36})\/resolved$/i},{id:`meta`,label:`/collections/:collection`,methods:[`GET`],pattern:/^\/api\/v1\/collections\/([a-z0-9-]+)$/},{id:`reports`,label:`/collections/:collection/reports`,methods:[`GET`],pattern:/^\/api\/v1\/collections\/([a-z0-9-]+)\/reports$/},{id:`entries`,label:`/collections/:collection/entries`,methods:[`GET`,`POST`],pattern:/^\/api\/v1\/collections\/([a-z0-9-]+)\/entries$/},{id:`entry`,label:`/collections/:collection/entries/:id`,methods:[`GET`,`PUT`,`DELETE`],pattern:/^\/api\/v1\/collections\/([a-z0-9-]+)\/entries\/([0-9a-f-]{36})$/i},{id:`like`,label:`/collections/:collection/entries/:id/like`,methods:[`PUT`,`DELETE`],pattern:/^\/api\/v1\/collections\/([a-z0-9-]+)\/entries\/([0-9a-f-]{36})\/like$/i},{id:`report`,label:`/collections/:collection/entries/:id/report`,methods:[`POST`],pattern:/^\/api\/v1\/collections\/([a-z0-9-]+)\/entries\/([0-9a-f-]{36})\/report$/i},{id:`hidden`,label:`/collections/:collection/entries/:id/hidden`,methods:[`PUT`,`DELETE`],pattern:/^\/api\/v1\/collections\/([a-z0-9-]+)\/entries\/([0-9a-f-]{36})\/hidden$/i},{id:`modLog`,label:`/moderation-log`,methods:[`GET`],pattern:/^\/api\/v1\/moderation-log$/},{id:`publisherErase`,label:`/publishers/:id`,methods:[`DELETE`],pattern:/^\/api\/v1\/publishers\/([0-9a-f-]{36})$/i},{id:`me`,label:`/me`,methods:[`GET`,`POST`,`PUT`,`DELETE`],pattern:/^\/api\/v1\/me$/},{id:`mePublications`,label:`/me/publications`,methods:[`GET`],pattern:/^\/api\/v1\/me\/publications$/},{id:`meLikes`,label:`/me/likes`,methods:[`GET`],pattern:/^\/api\/v1\/me\/likes$/},{id:`meReports`,label:`/me/reports`,methods:[`GET`],pattern:/^\/api\/v1\/me\/reports$/}],matchRoute=e=>{for(let t of B){let n=e.match(t.pattern);if(n)return{id:t.id,label:t.label,methods:t.methods,segments:n.slice(1)}}return null},routeLabel=e=>matchRoute(e)?.label??`unknown`,V=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,H=new Set([`entry`,`like`,`report`,`hidden`,`resolveReport`]),notAllowed=()=>new r(405,`method-not-allowed`,`That method is not supported here.`),U=new WeakMap,rateLimitHash=async(e,t)=>{let n=U.get(e);n===void 0&&(n=crypto.subtle.importKey(`raw`,new TextEncoder().encode(e.rateLimitPepper),{hash:`SHA-256`,name:`HMAC`},!1,[`sign`]),U.set(e,n));let r=await crypto.subtle.sign(`HMAC`,await n,new TextEncoder().encode(t));return[...new Uint8Array(r)].map(e=>e.toString(16).padStart(2,`0`)).join(``)},W=60;async function readLimit(e,t,n){let i=n.headers.get(`cf-connecting-ip`)??(e.appEnv===`local`?`127.0.0.1`:null);if(i===null)throw new r(503,`unavailable`,`The service is temporarily unavailable.`);let{success:a}=await t.READ_LIMIT.limit({key:await rateLimitHash(e,i)});if(!a)throw new r(429,`rate-limited`,`Too many requests. Try again later.`,{retryAfterSeconds:W})}async function writeLimit(e,t,n,i){let{success:a}=await t.WRITE_LIMIT.limit({key:await rateLimitHash(e,JSON.stringify([n,i]))});if(!a)throw new r(429,`rate-limited`,`Too many requests. Try again later.`,{retryAfterSeconds:W})}async function cachedPublicGet(t,n,r,i){if(t.headers.get(`cookie`)!==null||t.headers.get(`authorization`)!==null)return r();let a=new URL(t.url);a.search=new URLSearchParams([...a.searchParams.entries()].sort(([e],[t])=>e<t?-1:+(e>t))).toString();let o=caches.default,s=new Request(a.toString(),{method:`GET`}),c=await o.match(s).catch(()=>{i()});if(c)return c;let l=await r();return l.headers.set(`cache-control`,`public, max-age=${e.publicCacheSeconds}, s-maxage=${e.publicCacheSeconds}`),n.waitUntil(o.put(s,l.clone()).catch(i)),l}var withCors=(e,t,n)=>{let r=a(t,n);if(Object.keys(r).length===0)return e;let i=new Response(e.body,e);for(let[e,t]of Object.entries(r))i.headers.set(e,t);return i};function requireExpectedPublisher(e,n,i){if(i||n.publisher===null)return;let a=e.headers.get(t);if(a===null||!V.test(a))throw new r(400,`bad-request`,`The request is not valid.`);if(a!==n.publisher.id)throw new r(409,`publisher-changed`,`The signed-in publisher changed since this request was prepared.`)}function createArchiveWorker(e){let t=new Map;for(let n of e){if(t.has(n.name))throw new f(`Duplicate archive collection: ${n.name}`);t.set(n.name,n)}let collectionFor=e=>{let n=t.get(e);if(n===void 0)throw new r(404,`not-found`,`No such route.`);return n};async function handle(e,n,a){let d=_(n),f=new URL(e.url),{pathname:p}=f,m=e.method;if(!p.startsWith(`/api/v1/`))throw new r(404,`not-found`,`No such route.`);if(m===`OPTIONS`)return c(e,d);let B=matchRoute(p);if(B===null)throw new r(404,`not-found`,`No such route.`);if(!B.methods.includes(m))throw notAllowed();if(H.has(B.id)&&!V.test(B.segments[1]??``))throw i();if(B.id===`health`)return withCors(await T(d,n),e,d);B.id===`reports`&&l(f,[`cursor`,`limit`,`status`]),await readLimit(d,n,e);let U=m!==`GET`&&m!==`HEAD`&&m!==`OPTIONS`;U&&u(e,d);let W=m===`GET`&&(B.id===`meta`||B.id===`entries`||B.id===`entry`),G=B.id===`signOut`&&m===`POST`,K=W||G?null:await g(e,d,n,B.id===`accountErase`);if(U){if(G)return withCors(s(200,{ok:!0},{"cache-control":`no-store`,"set-cookie":o(d.appEnv===`production`)}),e,d);let t=h(K);requireExpectedPublisher(e,t,B.id===`me`&&m===`POST`),await writeLimit(d,n,t.subject.issuer,t.subject.subject)}let jsonWithCors=t=>withCors(t,e,d);switch(B.id){case`meta`:return l(f,[]),jsonWithCors(v(collectionFor(B.segments[0]??``)));case`entries`:{let t=collectionFor(B.segments[0]??``);if(m===`GET`)return jsonWithCors(await cachedPublicGet(e,a,()=>E(t,e,n),()=>emit({diagnostic:{kind:`cache`},type:`cache-error`})));if(m===`POST`)return jsonWithCors(await M(t,e,n,d,h(K)));throw notAllowed()}case`entry`:{let t=collectionFor(B.segments[0]??``),r=B.segments[1]??``;if(m===`GET`)return l(f,[]),jsonWithCors(await C(t,n,r));if(m===`PUT`)return jsonWithCors(await z(t,e,n,d,h(K),r));if(m===`DELETE`)return jsonWithCors(await b(t,n,h(K),r));throw notAllowed()}case`like`:{let e=collectionFor(B.segments[0]??``),t=B.segments[1]??``;if(m===`PUT`)return jsonWithCors(await R(e,n,d,h(K),t,!0));if(m===`DELETE`)return jsonWithCors(await R(e,n,d,h(K),t,!1));throw notAllowed()}case`report`:{if(m!==`POST`)throw notAllowed();let t=collectionFor(B.segments[0]??``);return jsonWithCors(await F(t,e,n,h(K),B.segments[1]??``))}case`hidden`:{if(m!==`PUT`&&m!==`DELETE`)throw notAllowed();let e=collectionFor(B.segments[0]??``);return jsonWithCors(await L(e,n,h(K),B.segments[1]??``,m===`PUT`))}case`reports`:{let t=h(K);return requireExpectedPublisher(e,t,!1),jsonWithCors(await O(collectionFor(B.segments[0]??``),e,n,t))}case`resolveReport`:return l(f,[]),jsonWithCors(await I(collectionFor(B.segments[0]??``),n,h(K),B.segments[1]??``));case`modLog`:{let t=h(K);return requireExpectedPublisher(e,t,!1),jsonWithCors(await D(e,n,t))}case`publisherErase`:if(m!==`DELETE`)throw notAllowed();return jsonWithCors(await S(n,h(K),B.segments[0]??``));case`accountErase`:return l(f,[]),jsonWithCors(await y(n,h(K)));case`me`:if(m===`GET`)return jsonWithCors(w(K));if(m===`POST`)return jsonWithCors(await N(e,n,h(K)));if(m===`PUT`)return jsonWithCors(await P(e,n,h(K)));if(m===`DELETE`)return jsonWithCors(await x(n,h(K)));throw notAllowed();case`mePublications`:case`meLikes`:case`meReports`:{let r=h(K);if(requireExpectedPublisher(e,r,!1),B.id===`mePublications`){let e=l(f,[`collection`]).get(`collection`);if(e===null)throw i();let a=t.get(e);if(a===void 0)throw i();return jsonWithCors(await A(a,n,r))}return jsonWithCors(await(B.id===`meReports`?j(e,n,r):k(e,n,r)))}default:throw notAllowed()}}let a=new Set,emit=e=>{if(a.size!==0)for(let t of a)try{t(e)}catch{}};return{async fetch(e,t,i){let a=crypto.randomUUID(),o=routeLabel(new URL(e.url).pathname),s=Date.now();try{let r=await handle(e,t,i),c;return r.status===503&&o===`/health`&&(c={kind:n.parse(await r.clone().json()).reason===`database-unavailable`?`database`:`schema`}),emit({code:r.ok?`ok`:`unavailable`,ms:Date.now()-s,requestId:a,route:o,status:r.status,type:`request`,...c===void 0?{}:{diagnostic:c}}),r}catch(n){let i=d(n,a);emit({code:n instanceof r?n.code:`internal`,diagnostic:n instanceof r?n.diagnostic:{kind:n instanceof p?`collection`:`internal`},ms:Date.now()-s,requestId:a,route:o,status:i.status,type:`request`});try{return withCors(i,e,_(t))}catch{return i}}},async scheduled(e,t,n){try{let e=Date.now(),n=await t.DB.batch([t.DB.prepare(m.expireRequests).bind(e),t.DB.prepare(m.expireRevocations).bind(e)]);emit({removed:n.reduce((e,t)=>e+(t.meta?.changes??0),0),status:`ok`,type:`scheduled`})}catch(e){throw emit({diagnostic:{kind:`database`},status:`failed`,type:`scheduled`}),e}},tap(e,t){let n=t?.signal;if(n?.aborted)return()=>{};let off=()=>{a.delete(e),n?.removeEventListener(`abort`,off)};return a.add(e),n?.addEventListener(`abort`,off,{once:!0}),off}}}export{createArchiveWorker};
2
+ //# sourceMappingURL=worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.js","names":[],"sources":["../../src/worker/worker.ts"],"sourcesContent":["import type { ArchiveCollection } from '../collection';\nimport { ARCHIVE_LIMITS, type ArchiveErrorCode, archiveHealthSchema, PUBLISHER_HEADER } from '../contract';\nimport { ArchiveDefinitionError, ArchiveDerivationError } from '../errors';\nimport { requireSession, type Session, verifySession } from './auth';\nimport { type ApiConfig, type ArchiveWorkerEnv, apiConfig } from './env';\nimport {\n ApiError,\n type ArchiveDiagnostic,\n badRequest,\n corsHeaders,\n expiredSessionCookie,\n json,\n preflight,\n queryParams,\n requireMutationGuard,\n toErrorResponse,\n} from './http';\nimport {\n collectionMeta,\n deleteAccountProfile,\n deleteEntry,\n deleteProfile,\n erasePublisher,\n getEntry,\n getMe,\n healthJson,\n listEntries,\n listModerationLog,\n listReports,\n myLikes,\n myPublications,\n myReports,\n publishEntry,\n registerMe,\n renameMe,\n reportEntry,\n resolveReport,\n setHidden,\n setLike,\n updateEntry,\n} from './publications';\nimport { SQL } from './sql';\n\n/**\n * The community-archive Worker factory: anonymous public reads, cookie-authenticated\n * mutations behind strict Origin/request-marker guards, IP and subject rate limits,\n * operator moderation over the registered collections, and a daily expiry sweep for\n * retired publish retry keys.\n *\n * Trust model: configuration and signing keys come only from validated Env bindings;\n * the session cookie is the single credential; every response is either no-store\n * (private/error) or a 30s anonymous public cache entry. Runtime observability\n * flows exclusively through `tap()` — the core itself never logs.\n */\n\n/** The minimal execution-context surface the core uses; the runtime's satisfies it. */\nexport interface ArchiveExecutionContext {\n waitUntil(promise: Promise<unknown>): void;\n}\n\n/** One sanitized request-completion event: ids, codes, route classes and duration only. */\nexport interface ArchiveRequestEvent {\n /** 'ok' on success; otherwise the route's `ARCHIVE_ERROR_CODES` value. */\n code: ArchiveErrorCode | 'ok';\n diagnostic?: ArchiveDiagnostic;\n /** Whole-request wall-clock duration in milliseconds. */\n ms: number;\n requestId: string;\n /** A finite route class like `/collections/:collection/entries`, never the raw path. */\n route: string;\n status: number;\n type: 'request';\n}\n\n/** The daily retry-key sweep, observable like every other runtime behavior. */\nexport type ArchiveScheduledEvent =\n | {\n /** Retired retry-key rows this run removed. */\n removed: number;\n type: 'scheduled';\n status: 'ok';\n }\n | { type: 'scheduled'; status: 'failed'; diagnostic: ArchiveDiagnostic };\n\nexport type ArchiveEvent =\n | ArchiveRequestEvent\n | ArchiveScheduledEvent\n | { readonly type: 'cache-error'; readonly diagnostic: ArchiveDiagnostic };\n\n/**\n * The Worker's exported-handler shape plus its observability side channel. Declared\n * structurally so the package needs no `@cloudflare/workers-types`: a deployment's\n * generated `ExportedHandler<Env>` accepts the factory's result as-is.\n */\nexport interface ArchiveWorkerHandler<E extends ArchiveWorkerEnv = ArchiveWorkerEnv> {\n fetch(request: Request, env: E, ctx: ArchiveExecutionContext): Promise<Response>;\n scheduled?(controller: unknown, env: E, ctx: ArchiveExecutionContext): void | Promise<void>;\n /**\n * Observes every request's outcome and every scheduled sweep. The handler owns\n * logging policy (console, a structured logger, or nothing); handler errors are\n * swallowed so observability can never affect the Worker, and with no handlers\n * registered there is zero work.\n */\n tap(handler: (event: ArchiveEvent) => void, options?: { readonly signal?: AbortSignal }): () => void;\n}\n\n/** One dispatchable route: the id both the switch and the event labels derive from. */\ntype RouteId =\n | 'health'\n | 'signOut'\n | 'meta'\n | 'reports'\n | 'resolveReport'\n | 'entries'\n | 'entry'\n | 'like'\n | 'report'\n | 'hidden'\n | 'me'\n | 'accountErase'\n | 'mePublications'\n | 'meLikes'\n | 'meReports'\n | 'modLog'\n | 'publisherErase';\n\n/** One matched route: its id (for dispatch) and label (for events), plus captures. */\ninterface RouteMatch {\n id: RouteId;\n /** The finite event label, never the raw path. */\n label: string;\n /** The methods the route serves; anything else is 405 before any guard runs. */\n methods: readonly string[];\n /** The path's captured segments, in pattern order: collection first, then id. */\n segments: readonly string[];\n}\n\n/**\n * The one route table: dispatch, methods, and event labels all read from here, so a\n * new route can never miss its observability class or its method set. Methods are\n * enforced before any guard runs — a wrong-method request never spends rate-limit\n * budget, verifies a session, or answers an auth-shaped error.\n */\nconst ROUTES: readonly { id: RouteId; label: string; methods: readonly string[]; pattern: RegExp }[] = [\n { id: 'accountErase', label: '/me/account', methods: ['DELETE'], pattern: /^\\/api\\/v1\\/me\\/account$/ },\n { id: 'health', label: '/health', methods: ['GET'], pattern: /^\\/api\\/v1\\/health$/ },\n { id: 'signOut', label: '/sign-out', methods: ['POST'], pattern: /^\\/api\\/v1\\/sign-out$/ },\n {\n id: 'resolveReport',\n label: '/collections/:collection/reports/:id/resolved',\n methods: ['PUT'],\n pattern: /^\\/api\\/v1\\/collections\\/([a-z0-9-]+)\\/reports\\/([0-9a-f-]{36})\\/resolved$/i,\n },\n {\n id: 'meta',\n label: '/collections/:collection',\n methods: ['GET'],\n pattern: /^\\/api\\/v1\\/collections\\/([a-z0-9-]+)$/,\n },\n {\n id: 'reports',\n label: '/collections/:collection/reports',\n methods: ['GET'],\n pattern: /^\\/api\\/v1\\/collections\\/([a-z0-9-]+)\\/reports$/,\n },\n {\n id: 'entries',\n label: '/collections/:collection/entries',\n methods: ['GET', 'POST'],\n pattern: /^\\/api\\/v1\\/collections\\/([a-z0-9-]+)\\/entries$/,\n },\n {\n id: 'entry',\n label: '/collections/:collection/entries/:id',\n methods: ['GET', 'PUT', 'DELETE'],\n pattern: /^\\/api\\/v1\\/collections\\/([a-z0-9-]+)\\/entries\\/([0-9a-f-]{36})$/i,\n },\n {\n id: 'like',\n label: '/collections/:collection/entries/:id/like',\n methods: ['PUT', 'DELETE'],\n pattern: /^\\/api\\/v1\\/collections\\/([a-z0-9-]+)\\/entries\\/([0-9a-f-]{36})\\/like$/i,\n },\n {\n id: 'report',\n label: '/collections/:collection/entries/:id/report',\n methods: ['POST'],\n pattern: /^\\/api\\/v1\\/collections\\/([a-z0-9-]+)\\/entries\\/([0-9a-f-]{36})\\/report$/i,\n },\n {\n id: 'hidden',\n label: '/collections/:collection/entries/:id/hidden',\n methods: ['PUT', 'DELETE'],\n pattern: /^\\/api\\/v1\\/collections\\/([a-z0-9-]+)\\/entries\\/([0-9a-f-]{36})\\/hidden$/i,\n },\n {\n id: 'modLog',\n label: '/moderation-log',\n methods: ['GET'],\n pattern: /^\\/api\\/v1\\/moderation-log$/,\n },\n {\n id: 'publisherErase',\n label: '/publishers/:id',\n methods: ['DELETE'],\n pattern: /^\\/api\\/v1\\/publishers\\/([0-9a-f-]{36})$/i,\n },\n { id: 'me', label: '/me', methods: ['GET', 'POST', 'PUT', 'DELETE'], pattern: /^\\/api\\/v1\\/me$/ },\n { id: 'mePublications', label: '/me/publications', methods: ['GET'], pattern: /^\\/api\\/v1\\/me\\/publications$/ },\n { id: 'meLikes', label: '/me/likes', methods: ['GET'], pattern: /^\\/api\\/v1\\/me\\/likes$/ },\n { id: 'meReports', label: '/me/reports', methods: ['GET'], pattern: /^\\/api\\/v1\\/me\\/reports$/ },\n];\n\nconst matchRoute = (pathname: string): RouteMatch | null => {\n for (const route of ROUTES) {\n const found = pathname.match(route.pattern);\n if (found) return { id: route.id, label: route.label, methods: route.methods, segments: found.slice(1) };\n }\n return null;\n};\n\n/** The matched route's event label; unmatched paths stay 'unknown', never the raw path. */\nconst routeLabel = (pathname: string): string => matchRoute(pathname)?.label ?? 'unknown';\n\nconst UUID_PATH = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/** The routes whose second path segment is an entry id: they share the one id rule. */\nconst ID_SEGMENT_ROUTES: ReadonlySet<RouteId> = new Set(['entry', 'like', 'report', 'hidden', 'resolveReport']);\n\nconst notAllowed = (): ApiError => new ApiError(405, 'method-not-allowed', 'That method is not supported here.');\n\nconst hmacKeys = new WeakMap<ApiConfig, Promise<CryptoKey>>();\n/** Peppered HMAC: rate-limit keys cannot be reversed to IPs without the deployment secret. */\nconst rateLimitHash = async (config: ApiConfig, text: string): Promise<string> => {\n let key = hmacKeys.get(config);\n if (key === undefined) {\n key = crypto.subtle.importKey(\n 'raw',\n new TextEncoder().encode(config.rateLimitPepper),\n { hash: 'SHA-256', name: 'HMAC' },\n false,\n ['sign'],\n );\n hmacKeys.set(config, key);\n }\n const mac = await crypto.subtle.sign('HMAC', await key, new TextEncoder().encode(text));\n return [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, '0')).join('');\n};\n\n/**\n * The Retry-After the 429s carry. The platform limiter does not expose its period at\n * runtime, so deployments must configure 60-second windows — a longer window would\n * make the header lie.\n */\nconst RATE_LIMIT_RETRY_AFTER_SECONDS = 60;\n\n/** The connecting IP, peppered-HMAC'd, keys the read limit; loopback only when APP_ENV=local. */\nasync function readLimit(config: ApiConfig, env: ArchiveWorkerEnv, request: Request): Promise<void> {\n const ip = request.headers.get('cf-connecting-ip') ?? (config.appEnv === 'local' ? '127.0.0.1' : null);\n if (ip === null) throw new ApiError(503, 'unavailable', 'The service is temporarily unavailable.');\n const { success } = await env.READ_LIMIT.limit({ key: await rateLimitHash(config, ip) });\n if (!success) {\n throw new ApiError(429, 'rate-limited', 'Too many requests. Try again later.', {\n retryAfterSeconds: RATE_LIMIT_RETRY_AFTER_SECONDS,\n });\n }\n}\n\n/** The verified issuer+subject, peppered-HMAC'd, keys the write limit ahead of every mutation. */\nasync function writeLimit(config: ApiConfig, env: ArchiveWorkerEnv, issuer: string, subject: string): Promise<void> {\n const { success } = await env.WRITE_LIMIT.limit({\n key: await rateLimitHash(config, JSON.stringify([issuer, subject])),\n });\n if (!success) {\n throw new ApiError(429, 'rate-limited', 'Too many requests. Try again later.', {\n retryAfterSeconds: RATE_LIMIT_RETRY_AFTER_SECONDS,\n });\n }\n}\n\n/** Only anonymous list reads may serve from the edge cache. */\nasync function cachedPublicGet(\n request: Request,\n ctx: ArchiveExecutionContext,\n produce: () => Promise<Response>,\n onFailure: () => void,\n): Promise<Response> {\n const anonymous = request.headers.get('cookie') === null && request.headers.get('authorization') === null;\n if (!anonymous) return produce();\n const url = new URL(request.url);\n url.search = new URLSearchParams(\n [...url.searchParams.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),\n ).toString();\n const edgeCache = caches.default;\n const key = new Request(url.toString(), { method: 'GET' });\n const cached = await edgeCache.match(key).catch(() => {\n onFailure();\n return undefined;\n });\n if (cached) return cached;\n const response = await produce();\n response.headers.set(\n 'cache-control',\n `public, max-age=${ARCHIVE_LIMITS.publicCacheSeconds}, s-maxage=${ARCHIVE_LIMITS.publicCacheSeconds}`,\n );\n ctx.waitUntil(edgeCache.put(key, response.clone()).catch(onFailure));\n return response;\n}\n\nconst withCors = (response: Response, request: Request, config: ApiConfig): Response => {\n const headers = corsHeaders(request, config);\n if (Object.keys(headers).length === 0) return response;\n const next = new Response(response.body, response);\n for (const [key, value] of Object.entries(headers)) next.headers.set(key, value);\n return next;\n};\n\n/**\n * Fail-closed precondition for registered publishers: the UI pins the opaque\n * publisher UUID it believes the session belongs to, so a queued write from one\n * tab can never land under another account that took over the shared cookie.\n * The header is never an identity source — every check still resolves the\n * publisher from the verified JWT — it only vetoes mismatched writes.\n */\nfunction requireExpectedPublisher(request: Request, session: Session, exempt: boolean): void {\n if (exempt || session.publisher === null) return;\n const expected = request.headers.get(PUBLISHER_HEADER);\n if (expected === null || !UUID_PATH.test(expected)) {\n throw new ApiError(400, 'bad-request', 'The request is not valid.');\n }\n if (expected !== session.publisher.id) {\n throw new ApiError(409, 'publisher-changed', 'The signed-in publisher changed since this request was prepared.');\n }\n}\n\n/**\n * Builds the Worker for one deployment's registered collections. Each collection owns\n * one URL + storage dimension; a duplicate registration is a deployment bug and\n * fails fast.\n */\nexport function createArchiveWorker<E extends ArchiveWorkerEnv>(\n collections: readonly ArchiveCollection[],\n): ArchiveWorkerHandler<E> {\n const registered = new Map<string, ArchiveCollection>();\n for (const collection of collections) {\n if (registered.has(collection.name))\n throw new ArchiveDefinitionError(`Duplicate archive collection: ${collection.name}`);\n registered.set(collection.name, collection);\n }\n\n /** Resolves the route's collection segment; an unregistered collection is a 404, not a 500. */\n const collectionFor = (segment: string): ArchiveCollection => {\n const collection = registered.get(segment);\n if (collection === undefined) throw new ApiError(404, 'not-found', 'No such route.');\n return collection;\n };\n\n async function handle(request: Request, env: E, ctx: ArchiveExecutionContext): Promise<Response> {\n const config = apiConfig(env);\n const url = new URL(request.url);\n const { pathname } = url;\n const method = request.method;\n\n if (!pathname.startsWith('/api/v1/')) throw new ApiError(404, 'not-found', 'No such route.');\n if (method === 'OPTIONS') return preflight(request, config);\n\n const match = matchRoute(pathname);\n // An unknown path inside the mount answers 404 like the paths outside it, and a\n // known route under an unsupported method answers 405 — both with no side\n // effects: no rate-limit spend, no session verification, no auth state leaked,\n // no guard-shaped error for a method the route does not serve.\n if (match === null) throw new ApiError(404, 'not-found', 'No such route.');\n if (!match.methods.includes(method)) throw notAllowed();\n\n // A malformed entry id is the same class of client bug: it answers 400 before\n // any rate-limit spend, session verification or auth state.\n if (ID_SEGMENT_ROUTES.has(match.id) && !UUID_PATH.test(match.segments[1] ?? '')) throw badRequest();\n\n // Health sits deliberately outside the rate-limit and session pipelines: uptime\n // probes must never burn the anonymous read budget or couple to authentication,\n // and it touches the database exactly once (the schema-version read).\n if (match.id === 'health') {\n return withCors(await healthJson(config, env), request, config);\n }\n if (match.id === 'reports') queryParams(url, ['cursor', 'limit', 'status']);\n\n await readLimit(config, env, request);\n\n // The same-origin mutation contract is proven BEFORE any identity work: a\n // foreign-origin write or missing marker must fail with 403 even while carrying a\n // garbage token — JWKS/JWT cost and 401 semantics stay behind the origin fence.\n const isMutation = method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS';\n if (isMutation) requireMutationGuard(request, config);\n\n // Session verification runs only where identity matters: private routes, the\n // moderation surfaces and every mutation. Public reads (list, detail, meta) never\n // invoke JWKS or the publisher lookup — browsing must not couple to authentication\n // availability. Sign-out is a cookie-clearing reply, not a session operation.\n const isPublicGet = method === 'GET' && (match.id === 'meta' || match.id === 'entries' || match.id === 'entry');\n const isSignOut = match.id === 'signOut' && method === 'POST';\n const session =\n isPublicGet || isSignOut ? null : await verifySession(request, config, env, match.id === 'accountErase');\n\n if (isMutation) {\n if (isSignOut) {\n // The shared `archiveOkSchema` body; every session cookie attribute is the\n // contract's, never ad hoc.\n const response = json(\n 200,\n { ok: true },\n {\n 'cache-control': 'no-store',\n 'set-cookie': expiredSessionCookie(config.appEnv === 'production'),\n },\n );\n return withCors(response, request, config);\n }\n const verified = requireSession(session);\n // The free fail-closed header precondition runs before the quota token is spent:\n // a stale tab's rejected writes must not consume the current account's budget.\n requireExpectedPublisher(request, verified, match.id === 'me' && method === 'POST');\n await writeLimit(config, env, verified.subject.issuer, verified.subject.subject);\n }\n\n const jsonWithCors = (response: Response) => withCors(response, request, config);\n\n switch (match.id) {\n case 'meta': {\n queryParams(url, []);\n return jsonWithCors(collectionMeta(collectionFor(match.segments[0] ?? '')));\n }\n case 'entries': {\n const collection = collectionFor(match.segments[0] ?? '');\n if (method === 'GET') {\n const response = await cachedPublicGet(\n request,\n ctx,\n () => listEntries(collection, request, env),\n () => emit({ diagnostic: { kind: 'cache' }, type: 'cache-error' }),\n );\n return jsonWithCors(response);\n }\n if (method === 'POST') {\n return jsonWithCors(await publishEntry(collection, request, env, config, requireSession(session)));\n }\n throw notAllowed();\n }\n case 'entry': {\n const collection = collectionFor(match.segments[0] ?? '');\n const id = match.segments[1] ?? '';\n if (method === 'GET') {\n queryParams(url, []);\n return jsonWithCors(await getEntry(collection, env, id));\n }\n if (method === 'PUT') {\n return jsonWithCors(await updateEntry(collection, request, env, config, requireSession(session), id));\n }\n if (method === 'DELETE') {\n return jsonWithCors(await deleteEntry(collection, env, requireSession(session), id));\n }\n throw notAllowed();\n }\n case 'like': {\n const collection = collectionFor(match.segments[0] ?? '');\n const id = match.segments[1] ?? '';\n if (method === 'PUT') {\n return jsonWithCors(await setLike(collection, env, config, requireSession(session), id, true));\n }\n if (method === 'DELETE') {\n return jsonWithCors(await setLike(collection, env, config, requireSession(session), id, false));\n }\n throw notAllowed();\n }\n case 'report': {\n if (method !== 'POST') throw notAllowed();\n const collection = collectionFor(match.segments[0] ?? '');\n return jsonWithCors(\n await reportEntry(collection, request, env, requireSession(session), match.segments[1] ?? ''),\n );\n }\n case 'hidden': {\n if (method !== 'PUT' && method !== 'DELETE') throw notAllowed();\n const collection = collectionFor(match.segments[0] ?? '');\n return jsonWithCors(\n await setHidden(collection, env, requireSession(session), match.segments[1] ?? '', method === 'PUT'),\n );\n }\n case 'reports': {\n // Private reads carry the same fail-closed precondition as writes: a stale tab\n // must not read another publisher's moderation queue after an account switch.\n const verified = requireSession(session);\n requireExpectedPublisher(request, verified, false);\n return jsonWithCors(await listReports(collectionFor(match.segments[0] ?? ''), request, env, verified));\n }\n case 'resolveReport': {\n queryParams(url, []);\n return jsonWithCors(\n await resolveReport(\n collectionFor(match.segments[0] ?? ''),\n env,\n requireSession(session),\n match.segments[1] ?? '',\n ),\n );\n }\n case 'modLog': {\n // The accountability read carries the same fail-closed precondition as every\n // private surface: a stale tab must not read the moderation history after an\n // account switch.\n const verified = requireSession(session);\n requireExpectedPublisher(request, verified, false);\n return jsonWithCors(await listModerationLog(request, env, verified));\n }\n case 'publisherErase': {\n // The terminal moderation action: erasing a publisher removes their row, and\n // FK cascades take the publications, likes and retry keys with it — the same\n // cascade a self-erase walks, applied to any publisher the operator names.\n if (method !== 'DELETE') throw notAllowed();\n return jsonWithCors(await erasePublisher(env, requireSession(session), match.segments[0] ?? ''));\n }\n case 'accountErase': {\n queryParams(url, []);\n return jsonWithCors(await deleteAccountProfile(env, requireSession(session)));\n }\n case 'me': {\n if (method === 'GET') return jsonWithCors(getMe(session));\n if (method === 'POST') return jsonWithCors(await registerMe(request, env, requireSession(session)));\n if (method === 'PUT') return jsonWithCors(await renameMe(request, env, requireSession(session)));\n if (method === 'DELETE') return jsonWithCors(await deleteProfile(env, requireSession(session)));\n throw notAllowed();\n }\n case 'mePublications':\n case 'meLikes':\n case 'meReports': {\n // Private reads carry the same fail-closed precondition as writes: a stale tab\n // must not consume another publisher's data after a shared-cookie account switch.\n const verified = requireSession(session);\n requireExpectedPublisher(request, verified, false);\n if (match.id === 'mePublications') {\n // The owner's entries are per-collection: the collection param names the\n // registered collection, and an absent or unregistered value is a malformed\n // request, not an empty list.\n const params = queryParams(url, ['collection']);\n const requested = params.get('collection');\n if (requested === null) throw badRequest();\n const collection = registered.get(requested);\n if (collection === undefined) throw badRequest();\n return jsonWithCors(await myPublications(collection, env, verified));\n }\n return jsonWithCors(\n await (match.id === 'meReports' ? myReports(request, env, verified) : myLikes(request, env, verified)),\n );\n }\n default:\n throw notAllowed();\n }\n }\n\n const tappers = new Set<(event: ArchiveEvent) => void>();\n const emit = (event: ArchiveEvent): void => {\n if (tappers.size === 0) return;\n for (const tapper of tappers) {\n try {\n tapper(event);\n } catch {\n // Observability must never affect the Worker.\n }\n }\n };\n\n return {\n async fetch(request: Request, env: E, ctx: ArchiveExecutionContext): Promise<Response> {\n const requestId = crypto.randomUUID();\n const route = routeLabel(new URL(request.url).pathname);\n const startedAt = Date.now();\n try {\n const response = await handle(request, env, ctx);\n let diagnostic: ArchiveDiagnostic | undefined;\n if (response.status === 503 && route === '/health') {\n const health = archiveHealthSchema.parse(await response.clone().json());\n diagnostic = { kind: health.reason === 'database-unavailable' ? 'database' : 'schema' };\n }\n emit({\n code: response.ok ? 'ok' : 'unavailable',\n ms: Date.now() - startedAt,\n requestId,\n route,\n status: response.status,\n type: 'request',\n ...(diagnostic === undefined ? {} : { diagnostic }),\n });\n return response;\n } catch (error) {\n const response = toErrorResponse(error, requestId);\n emit({\n code: error instanceof ApiError ? error.code : 'internal',\n diagnostic:\n error instanceof ApiError\n ? error.diagnostic\n : { kind: error instanceof ArchiveDerivationError ? 'collection' : 'internal' },\n ms: Date.now() - startedAt,\n requestId,\n route,\n status: response.status,\n type: 'request',\n });\n // Allowed origins see the same CORS contract on errors as on successes.\n try {\n return withCors(response, request, apiConfig(env));\n } catch {\n return response; // the config error itself is the response\n }\n }\n },\n\n async scheduled(_controller: unknown, env: E, _ctx: ArchiveExecutionContext): Promise<void> {\n try {\n const timestamp = Date.now();\n const results = await env.DB.batch([\n env.DB.prepare(SQL.expireRequests).bind(timestamp),\n env.DB.prepare(SQL.expireRevocations).bind(timestamp),\n ]);\n emit({\n removed: results.reduce((sum, result) => sum + (result.meta?.changes ?? 0), 0),\n status: 'ok',\n type: 'scheduled',\n });\n } catch (error) {\n emit({ diagnostic: { kind: 'database' }, status: 'failed', type: 'scheduled' });\n throw error;\n }\n },\n\n tap(handler, options) {\n const signal = options?.signal;\n if (signal?.aborted) return () => {};\n const off = () => {\n tappers.delete(handler);\n signal?.removeEventListener('abort', off);\n };\n tappers.add(handler);\n signal?.addEventListener('abort', off, { once: true });\n return off;\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./worker/worker.cjs");exports.createArchiveWorker=e.createArchiveWorker;
@@ -0,0 +1,4 @@
1
+ export type { ArchiveWorkerEnv, AuthIssuerServiceBinding, D1Like, D1Statement, RateLimiterBinding } from './worker/env';
2
+ export type { ArchiveDiagnostic } from './worker/http';
3
+ export { type ArchiveEvent, type ArchiveExecutionContext, type ArchiveRequestEvent, type ArchiveScheduledEvent, type ArchiveWorkerHandler, createArchiveWorker, } from './worker/worker';
4
+ //# sourceMappingURL=worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"AAGA,YAAY,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,MAAM,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACxH,YAAY,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,uBAAuB,EAC5B,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,mBAAmB,GACpB,MAAM,iBAAiB,CAAC"}
package/dist/worker.js ADDED
@@ -0,0 +1 @@
1
+ import{createArchiveWorker as e}from"./worker/worker.js";export{e as createArchiveWorker};
@@ -0,0 +1,173 @@
1
+ -- The community-archive D1 schema: the canonical, collection-agnostic DDL owned by
2
+ -- @vielzeug/archive. Deployments point wrangler's `migrations_dir` here (or copy this
3
+ -- file into their own migration chain) and never edit its shape — the runtime SQL in
4
+ -- the package is written against exactly this schema, and the package's own
5
+ -- integration suite runs against this same file. The collection dimension is data
6
+ -- (the `collection` column and `publication_tags` rows), never DDL.
7
+
8
+ CREATE TABLE publishers (
9
+ id TEXT PRIMARY KEY NOT NULL,
10
+ issuer TEXT NOT NULL,
11
+ subject TEXT NOT NULL,
12
+ display_name TEXT NOT NULL DEFAULT '' CHECK (length(display_name) <= 24),
13
+ created_at TEXT NOT NULL,
14
+ UNIQUE (issuer, subject)
15
+ ) STRICT;
16
+
17
+ CREATE TABLE revoked_subjects (
18
+ issuer TEXT NOT NULL,
19
+ subject TEXT NOT NULL,
20
+ expires_at INTEGER NOT NULL,
21
+ PRIMARY KEY (issuer, subject)
22
+ ) STRICT;
23
+ CREATE INDEX idx_revoked_subjects_expiry ON revoked_subjects(expires_at);
24
+
25
+ CREATE TABLE publications (
26
+ id TEXT PRIMARY KEY NOT NULL,
27
+ owner_id TEXT NOT NULL REFERENCES publishers(id) ON DELETE CASCADE,
28
+ collection TEXT NOT NULL CHECK (length(collection) BETWEEN 1 AND 32),
29
+ client_key TEXT NOT NULL,
30
+ schema_version INTEGER NOT NULL CHECK (schema_version >= 1),
31
+ content_version INTEGER NOT NULL CHECK (content_version >= 1),
32
+ payload_json TEXT NOT NULL CHECK (
33
+ json_valid(payload_json) AND json_type(payload_json) = 'object'
34
+ AND length(CAST(payload_json AS BLOB)) <= 8192
35
+ ),
36
+ content_hash TEXT NOT NULL CHECK (length(content_hash) = 64),
37
+ filter_value TEXT NULL,
38
+ title TEXT NOT NULL CHECK (length(trim(title)) BETWEEN 1 AND 128),
39
+ tags_json TEXT NOT NULL CHECK (
40
+ json_valid(tags_json) AND json_type(tags_json) = 'array'
41
+ AND json_array_length(tags_json) <= 16
42
+ ),
43
+ published_at TEXT NOT NULL,
44
+ updated_at TEXT NOT NULL,
45
+ revision INTEGER NOT NULL DEFAULT 1 CHECK (revision >= 1),
46
+ like_count INTEGER NOT NULL DEFAULT 0 CHECK (like_count >= 0),
47
+ -- Operator moderation: set while an operator has removed the entry from public
48
+ -- view. The row, its likes and its retry key stay intact — only the public
49
+ -- surfaces and owner edits stop serving it.
50
+ hidden_at TEXT NULL,
51
+ UNIQUE (collection, owner_id, client_key)
52
+ ) STRICT;
53
+
54
+ CREATE INDEX publications_collection_recent ON publications(collection, published_at DESC, id DESC);
55
+ CREATE INDEX publications_collection_liked ON publications(collection, like_count DESC, published_at DESC, id DESC);
56
+ CREATE INDEX publications_collection_filter ON publications(collection, filter_value);
57
+ CREATE INDEX publications_owner ON publications(owner_id, collection, published_at DESC, id DESC);
58
+
59
+ CREATE TABLE publication_tags (
60
+ publication_id TEXT NOT NULL REFERENCES publications(id) ON DELETE CASCADE,
61
+ tag TEXT NOT NULL,
62
+ PRIMARY KEY (publication_id, tag)
63
+ ) STRICT;
64
+
65
+ CREATE INDEX publication_tags_filter ON publication_tags(tag, publication_id);
66
+
67
+ CREATE TABLE likes (
68
+ publication_id TEXT NOT NULL REFERENCES publications(id) ON DELETE CASCADE,
69
+ publisher_id TEXT NOT NULL REFERENCES publishers(id) ON DELETE CASCADE,
70
+ created_at TEXT NOT NULL,
71
+ PRIMARY KEY (publication_id, publisher_id)
72
+ ) STRICT;
73
+
74
+ CREATE INDEX likes_publisher ON likes(publisher_id, created_at DESC, publication_id DESC);
75
+
76
+ -- A removed publication retains only its retry key and hash until expiry, not its content.
77
+ CREATE TABLE publication_requests (
78
+ owner_id TEXT NOT NULL REFERENCES publishers(id) ON DELETE CASCADE,
79
+ collection TEXT NOT NULL,
80
+ client_key TEXT NOT NULL,
81
+ payload_hash TEXT NOT NULL CHECK (length(payload_hash) = 64),
82
+ publication_id TEXT REFERENCES publications(id) ON DELETE SET NULL,
83
+ created_at TEXT NOT NULL,
84
+ expires_at INTEGER NOT NULL,
85
+ PRIMARY KEY (owner_id, collection, client_key)
86
+ ) STRICT;
87
+
88
+ CREATE INDEX publication_requests_expiry ON publication_requests(expires_at);
89
+
90
+ -- Reader abuse reports, idempotent per (entry, reporter); the operator queue reads
91
+ -- them newest-first.
92
+ CREATE TABLE publication_reports (
93
+ id TEXT PRIMARY KEY NOT NULL,
94
+ publication_id TEXT NOT NULL REFERENCES publications(id) ON DELETE CASCADE,
95
+ reporter_id TEXT NOT NULL REFERENCES publishers(id) ON DELETE CASCADE,
96
+ reason TEXT NOT NULL CHECK (length(trim(reason)) BETWEEN 1 AND 256),
97
+ created_at TEXT NOT NULL,
98
+ resolved_at TEXT NULL,
99
+ UNIQUE (publication_id, reporter_id)
100
+ ) STRICT;
101
+
102
+ CREATE INDEX publication_reports_recent ON publication_reports(created_at DESC, id DESC);
103
+ CREATE INDEX publication_reports_owner ON publication_reports(reporter_id, created_at DESC, id DESC);
104
+
105
+ -- The operator accountability log: every effective moderation action, appended in the
106
+ -- same transaction that applies it. No foreign keys by design — the audit must
107
+ -- outlive the rows it references (a removal deletes its own target, an operator's
108
+ -- own erasure must not erase their history) — and no content either: operator and
109
+ -- target ids, the action kind, and the time are the whole record. `collection` is
110
+ -- NULL only for the deployment-wide publisher erasure.
111
+ CREATE TABLE moderation_actions (
112
+ id TEXT PRIMARY KEY NOT NULL,
113
+ operator_id TEXT NOT NULL,
114
+ action TEXT NOT NULL CHECK (action IN ('hide', 'unhide', 'remove', 'erase-publisher')),
115
+ collection TEXT NULL CHECK (collection IS NULL OR (length(collection) BETWEEN 1 AND 32)),
116
+ target_id TEXT NOT NULL,
117
+ created_at TEXT NOT NULL
118
+ ) STRICT;
119
+
120
+ CREATE INDEX moderation_actions_recent ON moderation_actions(created_at DESC, id DESC);
121
+
122
+ CREATE VIRTUAL TABLE publication_search USING fts5(
123
+ title,
124
+ content = 'publications',
125
+ content_rowid = 'rowid',
126
+ tokenize = 'unicode61'
127
+ );
128
+
129
+ CREATE TRIGGER publications_tags_insert AFTER INSERT ON publications BEGIN
130
+ INSERT INTO publication_tags(publication_id, tag)
131
+ SELECT NEW.id, value FROM json_each(NEW.tags_json);
132
+ END;
133
+
134
+ CREATE TRIGGER publications_tags_update
135
+ AFTER UPDATE OF tags_json ON publications
136
+ WHEN NEW.tags_json <> OLD.tags_json BEGIN
137
+ DELETE FROM publication_tags WHERE publication_id = NEW.id;
138
+ INSERT INTO publication_tags(publication_id, tag)
139
+ SELECT NEW.id, value FROM json_each(NEW.tags_json);
140
+ END;
141
+
142
+ CREATE TRIGGER publications_search_insert AFTER INSERT ON publications BEGIN
143
+ INSERT INTO publication_search(rowid, title) VALUES (NEW.rowid, NEW.title);
144
+ END;
145
+
146
+ CREATE TRIGGER publications_search_delete AFTER DELETE ON publications BEGIN
147
+ INSERT INTO publication_search(publication_search, rowid, title)
148
+ VALUES ('delete', OLD.rowid, OLD.title);
149
+ END;
150
+
151
+ CREATE TRIGGER publications_search_update
152
+ AFTER UPDATE OF title ON publications
153
+ WHEN NEW.title <> OLD.title BEGIN
154
+ INSERT INTO publication_search(publication_search, rowid, title)
155
+ VALUES ('delete', OLD.rowid, OLD.title);
156
+ INSERT INTO publication_search(rowid, title) VALUES (NEW.rowid, NEW.title);
157
+ END;
158
+
159
+ CREATE TRIGGER likes_insert AFTER INSERT ON likes BEGIN
160
+ UPDATE publications SET like_count = like_count + 1 WHERE id = NEW.publication_id;
161
+ END;
162
+
163
+ CREATE TRIGGER likes_delete AFTER DELETE ON likes BEGIN
164
+ UPDATE publications SET like_count = like_count - 1 WHERE id = OLD.publication_id;
165
+ END;
166
+
167
+ -- The storage schema stamp health reads back: one ordinary row, because D1 does not
168
+ -- authorize PRAGMA writes. The value must equal ARCHIVE_SCHEMA_VERSION in the
169
+ -- package; a database reporting a different stamp answers health 'degraded'.
170
+ CREATE TABLE archive_schema_meta (
171
+ applied INTEGER NOT NULL CHECK (applied >= 1)
172
+ ) STRICT;
173
+ INSERT INTO archive_schema_meta(applied) VALUES (1);
@@ -0,0 +1,65 @@
1
+ -- The auth issuer's own database: accounts by email, single-use magic-link tokens
2
+ -- stored as SHA-256 hashes so a database leak never leaks usable links, and the
3
+ -- durable per-account send budget that is independent of token consumption.
4
+
5
+ -- One row per email that requested a link. The row exists from the first link
6
+ -- request (last_sign_in_at NULL until a link is used); the scheduled sweep removes
7
+ -- never-verified rows once they hold no live token and no open send window.
8
+ CREATE TABLE auth_users (
9
+ id TEXT PRIMARY KEY NOT NULL,
10
+ email TEXT NOT NULL UNIQUE CHECK (email = lower(email) AND length(email) <= 320),
11
+ created_at TEXT NOT NULL,
12
+ last_sign_in_at TEXT,
13
+ session_version INTEGER NOT NULL DEFAULT 0 CHECK (session_version BETWEEN 0 AND 9007199254740991)
14
+ ) STRICT;
15
+
16
+ CREATE TABLE auth_schema_meta (
17
+ id INTEGER PRIMARY KEY NOT NULL CHECK (id = 1),
18
+ applied INTEGER NOT NULL CHECK (applied >= 0)
19
+ ) STRICT;
20
+ INSERT INTO auth_schema_meta (id, applied) VALUES (1, 1);
21
+
22
+ CREATE TABLE auth_tokens (
23
+ -- The SHA-256 hex of the base64url token bytes; never the token itself.
24
+ token_hash TEXT PRIMARY KEY NOT NULL CHECK (length(token_hash) = 64),
25
+ user_id TEXT NOT NULL REFERENCES auth_users(id) ON DELETE CASCADE,
26
+ -- Epoch milliseconds.
27
+ expires_at INTEGER NOT NULL,
28
+ -- Transient redemption timestamp: the trigger stamps the account and removes
29
+ -- this credential within the same statement, including on a trigger failure.
30
+ redeemed_at TEXT
31
+ ) STRICT;
32
+
33
+ CREATE TRIGGER auth_token_redeemed AFTER UPDATE OF redeemed_at ON auth_tokens
34
+ WHEN NEW.redeemed_at IS NOT NULL
35
+ BEGIN
36
+ UPDATE auth_users SET last_sign_in_at = NEW.redeemed_at WHERE id = NEW.user_id;
37
+ DELETE FROM auth_tokens WHERE token_hash = NEW.token_hash;
38
+ END;
39
+
40
+ CREATE INDEX auth_tokens_expiry ON auth_tokens(expires_at);
41
+ CREATE INDEX auth_tokens_user ON auth_tokens(user_id);
42
+
43
+ CREATE TRIGGER auth_sessions_revoked AFTER UPDATE OF session_version ON auth_users
44
+ WHEN NEW.session_version != OLD.session_version
45
+ BEGIN
46
+ DELETE FROM auth_tokens WHERE user_id = NEW.id;
47
+ END;
48
+
49
+ -- The hourly send allowance: a fixed window opened by the first send. Consuming
50
+ -- or expiring a token never refunds it; only the window's end does.
51
+ CREATE TABLE auth_link_budget (
52
+ user_id TEXT PRIMARY KEY NOT NULL REFERENCES auth_users(id) ON DELETE CASCADE,
53
+ -- Epoch milliseconds the current window opened.
54
+ window_start INTEGER NOT NULL,
55
+ sent INTEGER NOT NULL CHECK (sent >= 1)
56
+ ) STRICT;
57
+
58
+ CREATE INDEX auth_link_budget_window ON auth_link_budget(window_start);
59
+
60
+ -- Singleton fixed-window admission budget, shared by every issuer isolate.
61
+ CREATE TABLE auth_sign_in_budget (
62
+ id INTEGER PRIMARY KEY NOT NULL CHECK (id = 1),
63
+ window_start INTEGER NOT NULL,
64
+ admitted INTEGER NOT NULL CHECK (admitted BETWEEN 1 AND 20)
65
+ ) STRICT;
package/package.json CHANGED
@@ -1,6 +1,74 @@
1
1
  {
2
2
  "name": "@vielzeug/archive",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "26.10.7",
4
+ "description": "Community content publishing for Cloudflare Workers + D1: a hardened Worker core over per-collection payload schemas",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/helmuthdu/vielzeug",
8
+ "directory": "packages/archive"
9
+ },
10
+ "type": "module",
11
+ "engines": {
12
+ "node": ">=22"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "migrations"
17
+ ],
18
+ "main": "./dist/index.cjs",
19
+ "module": "./dist/index.js",
20
+ "types": "dist/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "import": "./dist/index.js",
25
+ "require": "./dist/index.cjs"
26
+ },
27
+ "./worker": {
28
+ "types": "./dist/worker.d.ts",
29
+ "import": "./dist/worker.js",
30
+ "require": "./dist/worker.cjs"
31
+ },
32
+ "./auth": {
33
+ "types": "./dist/auth.d.ts",
34
+ "import": "./dist/auth.js",
35
+ "require": "./dist/auth.cjs"
36
+ }
37
+ },
38
+ "typesVersions": {
39
+ "*": {
40
+ "worker": [
41
+ "dist/worker.d.ts"
42
+ ],
43
+ "auth": [
44
+ "dist/auth.d.ts"
45
+ ]
46
+ }
47
+ },
48
+ "sideEffects": false,
49
+ "publishConfig": {
50
+ "access": "public",
51
+ "registry": "https://registry.npmjs.org/"
52
+ },
53
+ "scripts": {
54
+ "build": "vite build && pnpm --silent run build:types",
55
+ "build:types": "tsc -p tsconfig.declarations.json",
56
+ "fix": "biome check --write src test",
57
+ "lint": "biome ci src test",
58
+ "prepublishOnly": "pnpm --silent run build",
59
+ "test": "vitest",
60
+ "test:integration": "vitest run --config vitest.config.integration.ts",
61
+ "test:types": "tsc -p tsconfig.json --noEmit"
62
+ },
63
+ "dependencies": {
64
+ "@vielzeug/spell": "26.10.7",
65
+ "jose": "6.2.12"
66
+ },
67
+ "devDependencies": {
68
+ "@types/node": "^26.6.2",
69
+ "typescript": "^6.0.3",
70
+ "vite": "^8.3.0",
71
+ "vitest": "^5.0.1",
72
+ "wrangler": "4.149.0"
73
+ }
74
+ }