@vielzeug/archive 26.10.6 → 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 (47) hide show
  1. package/dist/_crypto.cjs +2 -0
  2. package/dist/_crypto.cjs.map +1 -0
  3. package/dist/_crypto.d.ts +7 -0
  4. package/dist/_crypto.d.ts.map +1 -0
  5. package/dist/_crypto.js +2 -0
  6. package/dist/_crypto.js.map +1 -0
  7. package/dist/auth/worker.cjs +4 -4
  8. package/dist/auth/worker.cjs.map +1 -1
  9. package/dist/auth/worker.d.ts.map +1 -1
  10. package/dist/auth/worker.js +4 -4
  11. package/dist/auth/worker.js.map +1 -1
  12. package/dist/auth.cjs +1 -1
  13. package/dist/auth.d.ts +0 -1
  14. package/dist/auth.d.ts.map +1 -1
  15. package/dist/auth.js +1 -1
  16. package/dist/collection.cjs +1 -1
  17. package/dist/collection.cjs.map +1 -1
  18. package/dist/collection.d.ts +9 -1
  19. package/dist/collection.d.ts.map +1 -1
  20. package/dist/collection.js +1 -1
  21. package/dist/collection.js.map +1 -1
  22. package/dist/contract.cjs.map +1 -1
  23. package/dist/contract.d.ts +4 -3
  24. package/dist/contract.d.ts.map +1 -1
  25. package/dist/contract.js.map +1 -1
  26. package/dist/errors.cjs.map +1 -1
  27. package/dist/errors.d.ts +1 -1
  28. package/dist/errors.d.ts.map +1 -1
  29. package/dist/errors.js.map +1 -1
  30. package/dist/worker/_pagination.cjs +1 -1
  31. package/dist/worker/_pagination.cjs.map +1 -1
  32. package/dist/worker/_pagination.d.ts +20 -7
  33. package/dist/worker/_pagination.d.ts.map +1 -1
  34. package/dist/worker/_pagination.js +1 -1
  35. package/dist/worker/_pagination.js.map +1 -1
  36. package/dist/worker/publications.cjs +1 -1
  37. package/dist/worker/publications.cjs.map +1 -1
  38. package/dist/worker/publications.d.ts.map +1 -1
  39. package/dist/worker/publications.js +1 -1
  40. package/dist/worker/publications.js.map +1 -1
  41. package/dist/worker/sql.cjs +4 -4
  42. package/dist/worker/sql.cjs.map +1 -1
  43. package/dist/worker/sql.d.ts +3 -2
  44. package/dist/worker/sql.d.ts.map +1 -1
  45. package/dist/worker/sql.js +4 -4
  46. package/dist/worker/sql.js.map +1 -1
  47. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"publications.js","names":[],"sources":["../../src/worker/publications.ts"],"sourcesContent":["import { s } from '@vielzeug/spell';\nimport type { ArchiveCollection } from '../collection';\nimport {\n ARCHIVE_CONTRACT_VERSION,\n ARCHIVE_LIMITS,\n ARCHIVE_SCHEMA_VERSION,\n type ArchiveModerationAction,\n archiveAliasSchema,\n archiveCollectionMetaSchema,\n archiveHealthSchema,\n archiveIdentitySchema,\n archiveLikedIdsSchema,\n archiveLikeStateSchema,\n archiveLikesPageSchema,\n archiveModerationLogSchema,\n archiveOwnReportsSchema,\n archiveReportsSchema,\n archiveSortSchema,\n isWellFormedText,\n SESSION_MAX_AGE_SECONDS,\n uuidSchema,\n} from '../contract';\nimport { decodeCursorValue, encodeCursorValue, forwardPage, nextCursor } from './_pagination';\nimport { JWT_CLOCK_TOLERANCE_SECONDS, type Publisher, type Session } from './auth';\nimport type { ApiConfig, ArchiveWorkerEnv } from './env';\nimport {\n ApiError,\n badRequest,\n conflict,\n forbidden,\n invalid,\n json,\n noStore,\n notFound,\n queryParams,\n readJsonBody,\n} from './http';\nimport {\n likedPublicationsSql,\n likesPageSql,\n listPublicationsSql,\n moderationLogSql,\n type PublicationCursor,\n type PublicationFilter,\n reportsPageSql,\n SQL,\n} from './sql';\n\n/**\n * Route handlers for /api/v1, parameterized by the registered collection. All SQL lives\n * in sql.ts and is bound positionally here in the authored order; the DTO boundary\n * re-validates every stored row before it leaves, content validation lives in the\n * collection's derive step, and moderation state (hidden entries) is visible only\n * through the owner and operator surfaces.\n */\n\ninterface PublicationRow {\n author_name: string;\n client_key: string;\n collection: string;\n content_hash: string;\n content_version: number;\n filter_value: string | null;\n /** Null while visible; set while an operator has removed the entry from public view. */\n hidden_at: string | null;\n id: string;\n like_count: number;\n owner_id: string;\n payload_json: string;\n published_at: string;\n revision: number;\n schema_version: number;\n tags_json: string;\n title: string;\n updated_at: string;\n}\n\nasync function databaseCall<T>(operation: () => Promise<T>): Promise<T> {\n try {\n return await operation();\n } catch (cause) {\n throw new ApiError(500, 'internal', 'The service is not available.', { cause, diagnostic: { kind: 'database' } });\n }\n}\nconst first = <T>(env: ArchiveWorkerEnv, sql: string, ...params: (string | number | null)[]) =>\n databaseCall(() =>\n env.DB.prepare(sql)\n .bind(...params)\n .first<T>(),\n );\nconst all = <T>(env: ArchiveWorkerEnv, sql: string, ...params: (string | number | null)[]) =>\n databaseCall(async () => {\n const result = await env.DB.prepare(sql)\n .bind(...params)\n .all<T>();\n if (!Array.isArray(result.results)) throw new Error('Missing database query results');\n return result.results;\n });\nconst run = (env: ArchiveWorkerEnv, sql: string, ...params: (string | number | null)[]) =>\n databaseCall(() =>\n env.DB.prepare(sql)\n .bind(...params)\n .run(),\n );\n\nconst uuid = (): string => crypto.randomUUID();\nconst now = (): string => new Date().toISOString();\n\n/** A stored row that fails the emitted-DTO contract is unavailable, not malformed data. */\nconst recordUnavailable = (): ApiError =>\n new ApiError(503, 'record-unavailable', 'The record is not available.', { diagnostic: { kind: 'record' } });\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/**\n * Maps a joined row to the public DTO through the collection's strict entry schema:\n * stored JSON is parsed and re-validated, stored versions are emitted as-is, and any\n * drift — unknown fields, corrupt payload, mismatched versions — surfaces as 503\n * `record-unavailable` rather than leaking or fabricating data.\n */\nconst rowToEntry = (collection: ArchiveCollection, row: PublicationRow) => {\n let payloadJson: unknown;\n try {\n payloadJson = JSON.parse(row.payload_json);\n } catch {\n throw recordUnavailable();\n }\n const parsed = collection.contract.entry.safeParse({\n author: { id: row.owner_id, name: row.author_name },\n contentHash: row.content_hash,\n contentVersion: row.content_version,\n id: row.id,\n likeCount: row.like_count,\n payload: payloadJson,\n publishedAt: row.published_at,\n revision: row.revision,\n schemaVersion: row.schema_version,\n updatedAt: row.updated_at,\n });\n if (!parsed.success) throw recordUnavailable();\n return parsed.data;\n};\n\nconst rowToOwnedEntry = (collection: ArchiveCollection, row: PublicationRow) => {\n const entry = rowToEntry(collection, row);\n // The private owner DTO adds the create retry key and the moderation state;\n // public routes never emit either.\n const key = uuidSchema.safeParse(row.client_key);\n if (!key.success) throw recordUnavailable();\n return { ...entry, clientKey: row.client_key, hidden: row.hidden_at !== null };\n};\n\nconst entryById = async (collection: ArchiveCollection, env: ArchiveWorkerEnv, id: string) => {\n const row = await first<PublicationRow>(env, SQL.publication, id, collection.name);\n return row === null ? null : rowToEntry(collection, row);\n};\n\n/* ----------------------------------- list --------------------------------- */\n\ntype CursorDirection = 'next' | 'previous';\n\n/** Normalizes free text into the bounded FTS prefix-match expression the SQL expects. */\nconst searchMatchFor = (search: string | null): string | undefined => {\n if (search === null) return undefined;\n if (search.length > ARCHIVE_LIMITS.searchChars) throw badRequest();\n // Unicode word extraction: punctuation splits terms rather than poisoning them,\n // so \"Hunter's\" and \"fire-ice\" match the words their indexed titles carry.\n const terms = (search.match(/[\\p{L}\\p{N}]+/gu) ?? []).slice(0, ARCHIVE_LIMITS.searchTerms);\n if (terms.length === 0) return undefined;\n return terms.map((t) => `\"${t}\"*`).join(' ');\n};\n\n/** The cursor carries direction plus filter context so a replayed/mismatched cursor is 400. */\ninterface CursorPayload {\n c: PublicationCursor;\n d: CursorDirection;\n q: {\n a: string | null;\n e: string | null;\n g: string;\n m: string | null;\n s: 'liked' | 'recent';\n t: string | null;\n };\n}\n\nconst cursorPayloadSchema = s.object({\n c: s.object({\n id: s.string().uuid(),\n likeCount: s.number().int().min(0).max(Number.MAX_SAFE_INTEGER),\n publishedAt: s.string().isoDateTime(),\n }),\n d: s.enum(['next', 'previous'] as const),\n q: s.object({\n a: s.string().uuid().nullable(),\n e: s.string().max(ARCHIVE_LIMITS.vocabChars).nullable(),\n g: s.string().max(ARCHIVE_LIMITS.vocabChars),\n m: s\n .string()\n .max(ARCHIVE_LIMITS.searchChars * ARCHIVE_LIMITS.searchTerms)\n .nullable(),\n s: s.enum(['liked', 'recent'] as const),\n t: s.string().max(ARCHIVE_LIMITS.vocabChars).nullable(),\n }),\n});\n\nconst cursorFromRow = (row: PublicationRow): PublicationCursor => ({\n id: row.id,\n likeCount: row.like_count,\n publishedAt: row.published_at,\n});\n\nconst encodeCursor = (row: PublicationRow, direction: CursorDirection, query: PublicationFilter): string =>\n encodeCursorValue({\n c: cursorFromRow(row),\n d: direction,\n q: {\n a: query.author ?? null,\n e: query.filter ?? null,\n g: query.collection,\n m: query.searchMatch ?? null,\n s: query.sort,\n t: query.tag ?? null,\n },\n } satisfies CursorPayload);\n\nconst decodeCursor = (raw: string, query: PublicationFilter): CursorPayload => {\n const payload = decodeCursorValue(raw, cursorPayloadSchema);\n const { q } = payload;\n if (\n (q.a ?? undefined) !== query.author ||\n q.s !== query.sort ||\n q.g !== query.collection ||\n (q.e ?? undefined) !== query.filter ||\n (q.m ?? undefined) !== query.searchMatch ||\n (q.t ?? undefined) !== query.tag\n ) {\n throw badRequest();\n }\n return payload;\n};\n\nexport async function listEntries(\n collection: ArchiveCollection,\n request: Request,\n env: ArchiveWorkerEnv,\n): Promise<Response> {\n const url = new URL(request.url);\n const params = queryParams(url, ['author', 'cursor', 'filter', 'limit', 'search', 'sort', 'tag']);\n\n const sortRaw = params.get('sort') ?? 'recent';\n const sortParsed = archiveSortSchema.safeParse(sortRaw);\n if (!sortParsed.success) throw badRequest();\n\n const limitRaw = params.get('limit');\n const limit = limitRaw === null ? ARCHIVE_LIMITS.pageSize : Number(limitRaw);\n if (!Number.isInteger(limit) || limit < 1 || limit > ARCHIVE_LIMITS.maxPageSize) throw badRequest();\n\n const filter = params.get('filter');\n if (filter !== null && !collection.filters.includes(filter)) throw badRequest();\n const tag = params.get('tag');\n if (tag !== null && !collection.tags.includes(tag)) throw badRequest();\n const author = params.get('author');\n if (author !== null && !UUID_RE.test(author)) throw badRequest();\n\n const searchMatch = searchMatchFor(params.get('search'));\n const query: PublicationFilter = {\n author: author ?? undefined,\n collection: collection.name,\n filter: filter ?? undefined,\n limit: limit + 1,\n searchMatch,\n sort: sortParsed.data,\n tag: tag ?? undefined,\n };\n\n // The cursor tells the fetch direction; the first page always walks forward.\n const cursorRaw = params.get('cursor');\n const decoded = cursorRaw === null ? null : decodeCursor(cursorRaw, query);\n const direction: CursorDirection = decoded?.d ?? 'next';\n if (decoded !== null) query.cursor = decoded.c;\n query.direction = direction;\n\n const fetchRows = async (f: PublicationFilter): Promise<PublicationRow[]> => {\n const bound = listPublicationsSql(f);\n return all<PublicationRow>(env, bound.sql, ...bound.parameters);\n };\n\n const fetched = await fetchRows(query);\n const hasMore = fetched.length > limit;\n // 'previous' reads ASC off the cursor; display flips back to normal DESC order.\n const page = fetched.slice(0, limit);\n if (direction === 'previous') page.reverse();\n\n // Bounded index probes — never a count query — decide has-more in the opposite\n // direction. A first (uncursored) page needs no backward probe: it IS the top.\n let hasOpposite = false;\n if (decoded !== null && page.length > 0) {\n const probe: PublicationFilter = { ...query, limit: 1 };\n if (direction === 'next') {\n probe.cursor = cursorFromRow(page[0]);\n probe.direction = 'previous';\n } else {\n probe.cursor = cursorFromRow(page[page.length - 1]);\n probe.direction = 'next';\n }\n hasOpposite = (await fetchRows(probe)).length > 0;\n }\n\n const next =\n page.length > 0 && ((direction === 'next' && hasMore) || (direction === 'previous' && hasOpposite))\n ? encodeCursor(page[page.length - 1], 'next', query)\n : null;\n const previous =\n page.length > 0 && ((direction === 'previous' && hasMore) || (direction === 'next' && hasOpposite))\n ? encodeCursor(page[0], 'previous', query)\n : null;\n\n // Entries are validated per row through the collection's entry schema; the cursors\n // are internally generated, so the page composes validated parts directly.\n const pageDto = { cursor: next, entries: page.map((row) => rowToEntry(collection, row)), previous };\n return json(200, pageDto, noStore);\n}\n\n/** The collection's public self-description: name plus the declared query vocabularies. */\nexport function collectionMeta(collection: ArchiveCollection): Response {\n const parsed = archiveCollectionMetaSchema.safeParse({\n filters: [...collection.filters].sort(),\n name: collection.name,\n tags: [...collection.tags].sort(),\n });\n if (!parsed.success) throw recordUnavailable();\n return json(200, parsed.data, noStore);\n}\n\nexport async function getEntry(collection: ArchiveCollection, env: ArchiveWorkerEnv, id: string): Promise<Response> {\n const entry = await entryById(collection, env, id);\n if (entry === null) throw notFound();\n return json(200, entry, noStore);\n}\n\n/* ---------------------------------- health --------------------------------- */\n\n/**\n * Service health: the feature flags, the wire limits, and the storage schema stamp\n * actually present in the database. A database whose migrations lag the package\n * answers `degraded` instead of pretending to be fine — the most likely deployment\n * fault becomes a one-glance diagnosis.\n */\nexport async function healthJson(config: ApiConfig, env: ArchiveWorkerEnv): Promise<Response> {\n let dbSchemaVersion: number | null = null;\n let reason: 'database-unavailable' | 'schema-mismatch' | null = null;\n try {\n const row = await first<{ applied: number }>(env, SQL.schemaVersion);\n dbSchemaVersion = row?.applied ?? 0;\n } catch {\n reason = 'database-unavailable';\n }\n if (reason === null && dbSchemaVersion !== ARCHIVE_SCHEMA_VERSION) reason = 'schema-mismatch';\n const parsed = archiveHealthSchema.safeParse({\n dbSchemaVersion,\n features: { likes: config.likesEnabled, publishing: config.publishingEnabled },\n limits: ARCHIVE_LIMITS,\n reason,\n schemaVersion: ARCHIVE_CONTRACT_VERSION,\n status: reason === null ? 'ok' : 'degraded',\n });\n if (!parsed.success) throw recordUnavailable();\n return json(reason === null ? 200 : 503, parsed.data, noStore);\n}\n\n/* --------------------------------- identity -------------------------------- */\n\n/** Emits the private identity DTO through the strict shared schema. */\nconst identityJson = (status: number, identity: unknown): Response => {\n const parsed = archiveIdentitySchema.safeParse(identity);\n if (!parsed.success) throw recordUnavailable();\n return json(status, parsed.data, noStore);\n};\n\nexport function getMe(session: Session | null): Response {\n return identityJson(\n 200,\n session?.publisher\n ? { id: session.publisher.id, name: session.publisher.displayName, operator: session.operator }\n : null,\n );\n}\n\nexport async function registerMe(request: Request, env: ArchiveWorkerEnv, session: Session): Promise<Response> {\n const body = await readJsonBody(request);\n const parsed = registerBodySchema.safeParse(body);\n if (!parsed.success) throw invalid();\n const name = parsed.data.name ?? '';\n\n const existing = await first<{ display_name: string; id: string }>(\n env,\n SQL.publisher,\n session.subject.issuer,\n session.subject.subject,\n );\n if (existing !== null) {\n return identityJson(200, { id: existing.id, name: existing.display_name, operator: session.operator });\n }\n const id = uuid();\n await run(\n env,\n SQL.insertPublisher,\n id,\n session.subject.issuer,\n session.subject.subject,\n name,\n now(),\n session.subject.issuer,\n session.subject.subject,\n Date.now(),\n );\n const row = await first<{ display_name: string; id: string }>(\n env,\n SQL.publisher,\n session.subject.issuer,\n session.subject.subject,\n );\n if (row === null) {\n if ((await first(env, SQL.revokedSubject, session.subject.issuer, session.subject.subject, Date.now())) !== null) {\n throw new ApiError(401, 'unauthenticated', 'Sign-in is required.');\n }\n throw conflict();\n }\n return identityJson(201, { id: row.id, name: row.display_name, operator: session.operator });\n}\n\nexport async function renameMe(request: Request, env: ArchiveWorkerEnv, session: Session): Promise<Response> {\n const publisher = requirePublisherRow(session);\n const body = await readJsonBody(request);\n const parsed = renameBodySchema.safeParse(body);\n if (!parsed.success) throw invalid();\n if (parsed.data.name !== publisher.displayName) {\n await run(env, SQL.renamePublisher, parsed.data.name, publisher.id, parsed.data.name);\n }\n return identityJson(200, { id: publisher.id, name: parsed.data.name, operator: session.operator });\n}\n\n/**\n * Archive-profile erasure, independent of the identity provider's account.\n * The credential survives so an issuer can retry coordinated account erasure.\n */\nexport async function deleteProfile(env: ArchiveWorkerEnv, session: Session): Promise<Response> {\n if (session.publisher !== null) await run(env, SQL.deletePublisher, session.publisher.id);\n return new Response(null, {\n headers: {\n 'cache-control': 'no-store',\n },\n status: 204,\n });\n}\n\n/** Revocation and erasure share a transaction, preventing a stale session from recreating the profile. */\nexport async function deleteAccountProfile(env: ArchiveWorkerEnv, session: Session): Promise<Response> {\n const { issuer, subject } = session.subject;\n // Issuance may be one tolerance ahead; expiry verification permits another.\n await databaseCall(() =>\n env.DB.batch([\n env.DB.prepare(SQL.revokeSubject).bind(\n issuer,\n subject,\n Date.now() + (SESSION_MAX_AGE_SECONDS + 2 * JWT_CLOCK_TOLERANCE_SECONDS) * 1000,\n ),\n env.DB.prepare(SQL.deleteSubjectPublisher).bind(issuer, subject),\n ]),\n );\n return new Response(null, { headers: noStore, status: 204 });\n}\n\n/**\n * Operator-side GDPR erasure: removes any publisher's row; FK cascades take its\n * publications, likes and retry keys. Idempotent for the same reason the self-erase\n * is: an already-erased publisher is a 204 with nothing left to remove.\n */\nexport async function erasePublisher(env: ArchiveWorkerEnv, session: Session, publisherId: string): Promise<Response> {\n const operator = requireOperatorRow(session);\n // The terminal moderation action: erasing a publisher removes their row, and FK\n // cascades take the publications, likes and retry keys with it — the same cascade a\n // self-erase walks, applied to any publisher the operator names. The guarded audit\n // and deletion share a transaction, so retries never record fictitious actions.\n await databaseCall(() =>\n env.DB.batch([\n env.DB.prepare(SQL.auditErasure).bind(uuid(), operator.id, now(), publisherId),\n env.DB.prepare(SQL.deletePublisher).bind(publisherId),\n ]),\n );\n return new Response(null, { headers: noStore, status: 204 });\n}\n\n/** The operator's accountability log: newest actions first, bounded, ids only. */\nexport async function listModerationLog(request: Request, env: ArchiveWorkerEnv, session: Session): Promise<Response> {\n requireOperatorRow(session);\n const context = 'moderation-log';\n const { cursor, limit } = forwardPage(request, context);\n const bound = moderationLogSql({ cursor, limit: limit + 1 });\n const rows = await all<{\n id: string;\n action: ArchiveModerationAction;\n collection: string | null;\n created_at: string;\n operator_id: string;\n target_id: string;\n }>(env, bound.sql, ...bound.parameters);\n const page = rows.slice(0, limit);\n const last = page.at(-1);\n const parsed = archiveModerationLogSchema.safeParse({\n actions: page.map((row) => ({\n action: row.action,\n collection: row.collection,\n createdAt: row.created_at,\n id: row.id,\n operatorId: row.operator_id,\n targetId: row.target_id,\n })),\n cursor:\n rows.length > limit && last !== undefined\n ? nextCursor({ createdAt: last.created_at, id: last.id }, context)\n : null,\n });\n if (!parsed.success) throw recordUnavailable();\n return json(200, parsed.data, noStore);\n}\n\nexport const requirePublisherRow = (session: Session): Publisher => {\n if (session.publisher === null) throw forbidden('Register before publishing.');\n return session.publisher;\n};\n\n/** Operator authority plus a registered publisher row: moderation routes need both,\n * the role claim for authority and the row for attribution. */\nexport const requireOperatorRow = (session: Session): Publisher => {\n if (!session.operator) throw forbidden('Operator access is required.');\n if (session.publisher === null) throw forbidden('Register before moderating.');\n return session.publisher;\n};\n\n// Private identity payloads share the one alias rule with the emitted DTOs.\nconst registerBodySchema = s.object({ name: archiveAliasSchema.optional() });\nconst renameBodySchema = s.object({ name: archiveAliasSchema });\n\nexport async function myPublications(\n collection: ArchiveCollection,\n env: ArchiveWorkerEnv,\n session: Session,\n): Promise<Response> {\n if (session.publisher === null) return json(200, { entries: [] }, noStore);\n const rows = await all<PublicationRow>(\n env,\n SQL.ownPublications,\n session.publisher.id,\n collection.name,\n ARCHIVE_LIMITS.maxPublications,\n );\n // The private owner DTO adds the create retry key; public routes never emit it.\n return json(200, { entries: rows.map((row) => rowToOwnedEntry(collection, row)) }, noStore);\n}\n\nexport async function myLikes(request: Request, env: ArchiveWorkerEnv, session: Session): Promise<Response> {\n const params = new URL(request.url).searchParams;\n const raw = params.get('ids');\n if (raw === null) {\n const context = `likes:${session.publisher?.id ?? 'unregistered'}`;\n const { cursor, limit } = forwardPage(request, context);\n if (session.publisher === null) return json(200, { cursor: null, ids: [] }, noStore);\n const bound = likesPageSql(session.publisher.id, { cursor, limit: limit + 1 });\n const rows = await all<{ created_at: string; publication_id: string }>(env, bound.sql, ...bound.parameters);\n const page = rows.slice(0, limit);\n const last = page.at(-1);\n const parsed = archiveLikesPageSchema.safeParse({\n cursor:\n rows.length > limit && last !== undefined\n ? nextCursor({ createdAt: last.created_at, id: last.publication_id }, context)\n : null,\n ids: page.map((row) => row.publication_id),\n });\n if (!parsed.success) throw recordUnavailable();\n return json(200, parsed.data, noStore);\n }\n queryParams(new URL(request.url), ['ids']);\n const ids = [...new Set(raw.split(','))];\n if (ids.length > ARCHIVE_LIMITS.likeStateIds || ids.some((id) => !UUID_RE.test(id))) throw badRequest();\n if (session.publisher === null) return json(200, { ids: [] }, noStore);\n const bound = likedPublicationsSql(session.publisher.id, ids);\n const rows = await all<{ publication_id: string }>(env, bound.sql, ...bound.parameters);\n const liked = new Set(rows.map((r) => r.publication_id));\n const dto = { ids: ids.filter((id) => liked.has(id)) };\n const validated = archiveLikedIdsSchema.safeParse(dto);\n if (!validated.success) throw recordUnavailable();\n return json(200, validated.data, noStore);\n}\n\n/* --------------------------------- mutations ------------------------------- */\n\n// The core validates the request envelope; the collection's derive step validates the payload.\nconst publishBodySchema = s.object({ clientKey: uuidSchema, payload: s.unknown() });\nconst updateBodySchema = s.object({\n expectedRevision: s.number().int().min(1).max(Number.MAX_SAFE_INTEGER),\n payload: s.unknown(),\n});\n\nexport async function publishEntry(\n collection: ArchiveCollection,\n request: Request,\n env: ArchiveWorkerEnv,\n config: ApiConfig,\n session: Session,\n): Promise<Response> {\n if (!config.publishingEnabled) throw new ApiError(503, 'publishing-disabled', 'Publishing is currently disabled.');\n const publisher = requirePublisherRow(session);\n const body = await readJsonBody(request);\n const parsed = publishBodySchema.safeParse(body);\n if (!parsed.success) throw invalid();\n const { clientKey } = parsed.data;\n // The derive step owns every content rejection: schema, vocabulary, title, byte bound.\n const derivation = await collection.derive(parsed.data.payload);\n if (derivation === null) throw invalid();\n\n const payloadJson = derivation.canonicalJson;\n const hash = derivation.contentHash;\n const owner = publisher.id;\n const name = collection.name;\n\n // Retry-key inspection first: the request record decides replay vs conflict vs gone.\n // A logically expired record is ignored entirely — retention is enforced by time,\n // not just by the periodic sweep.\n const nowMs = Date.now();\n const retry = await first<{ expires_at: number; payload_hash: string; publication_id: string | null }>(\n env,\n SQL.request,\n owner,\n name,\n clientKey,\n );\n if (retry !== null && retry.expires_at > nowMs) {\n if (retry.payload_hash !== hash) throw conflict('That key was used for a different payload.');\n if (retry.publication_id === null) {\n throw new ApiError(410, 'gone', 'That publication was removed.');\n }\n const entry = await entryById(collection, env, retry.publication_id);\n if (entry !== null) return json(200, entry, noStore);\n }\n\n const existing = await first<PublicationRow>(env, SQL.publicationByKey, owner, name, clientKey);\n if (existing !== null) {\n // A hidden entry is frozen for its owner too: replaying the create key must not\n // read as a live publication.\n if (existing.hidden_at !== null) throw forbidden('This entry is under moderation review.');\n if (existing.content_hash !== hash) throw conflict('That key was used for a different payload.');\n return json(200, rowToEntry(collection, existing), noStore);\n }\n\n const timestamp = now();\n const expiresAt = Date.now() + ARCHIVE_LIMITS.idempotencyRetentionMs;\n const tagsJson = JSON.stringify(derivation.tags);\n const results = await databaseCall(() =>\n env.DB.batch([\n env.DB.prepare(SQL.expireRequests).bind(Date.now()),\n env.DB.prepare(SQL.insertPublication).bind(\n uuid(),\n owner,\n name,\n clientKey,\n ARCHIVE_CONTRACT_VERSION,\n derivation.contentVersion,\n payloadJson,\n hash,\n derivation.filter,\n derivation.title,\n tagsJson,\n timestamp,\n timestamp,\n owner,\n name,\n ARCHIVE_LIMITS.maxPublications,\n owner,\n name,\n clientKey,\n Date.now(),\n ),\n env.DB.prepare(SQL.insertRequest).bind(\n owner,\n name,\n clientKey,\n hash,\n timestamp,\n expiresAt,\n owner,\n name,\n clientKey,\n hash,\n ),\n ]),\n );\n if ((results[1]?.meta?.changes ?? 0) === 0) {\n // The guarded insert was skipped: a race already wrote the key, the key's\n // publication was removed, or the owner's live-publication quota is full.\n // Re-reading the request record first distinguishes a tombstone from a race.\n const request = await first<{ expires_at: number; payload_hash: string; publication_id: string | null }>(\n env,\n SQL.request,\n owner,\n name,\n clientKey,\n );\n if (request !== null && request.expires_at > Date.now()) {\n if (request.publication_id === null) throw new ApiError(410, 'gone', 'That publication was removed.');\n if (request.payload_hash === hash) {\n const entry = await entryById(collection, env, request.publication_id);\n if (entry !== null) return json(200, entry, noStore);\n // The publication vanished between checks: the key is a tombstone.\n throw new ApiError(410, 'gone', 'That publication was removed.');\n }\n throw conflict('That key was used for a different payload.');\n }\n const raced = await first<PublicationRow>(env, SQL.publicationByKey, owner, name, clientKey);\n if (raced !== null && raced.hidden_at !== null) throw forbidden('This entry is under moderation review.');\n if (raced !== null && raced.content_hash === hash) return json(200, rowToEntry(collection, raced), noStore);\n if (raced !== null) throw conflict('That key was used for a different payload.');\n throw new ApiError(429, 'quota', 'Publication quota reached.');\n }\n\n const created = await first<PublicationRow>(env, SQL.publicationByKey, owner, name, clientKey);\n if (created === null) throw conflict();\n return json(201, rowToEntry(collection, created), noStore);\n}\n\nexport async function updateEntry(\n collection: ArchiveCollection,\n request: Request,\n env: ArchiveWorkerEnv,\n config: ApiConfig,\n session: Session,\n id: string,\n): Promise<Response> {\n if (!config.publishingEnabled) throw new ApiError(503, 'publishing-disabled', 'Publishing is currently disabled.');\n const publisher = requirePublisherRow(session);\n // The owner's row regardless of moderation state: the author must be told their\n // entry is frozen, not that it vanished.\n const row = await first<PublicationRow>(env, SQL.publicationAny, id, collection.name);\n // Replacing an entry is the author's alone; anyone else sees the entry as absent.\n if (row === null || row.owner_id !== publisher.id) throw notFound();\n // Hidden entries are frozen: an edit must not bypass the operator's decision.\n if (row.hidden_at !== null) throw forbidden('This entry is under moderation review.');\n\n const body = await readJsonBody(request);\n const parsed = updateBodySchema.safeParse(body);\n if (!parsed.success) throw invalid();\n const { expectedRevision } = parsed.data;\n\n if (expectedRevision !== row.revision) throw conflict();\n\n const derivation = await collection.derive(parsed.data.payload);\n if (derivation === null) throw invalid();\n const payloadJson = derivation.canonicalJson;\n const hash = derivation.contentHash;\n if (hash === row.content_hash) {\n const current = await first<PublicationRow>(env, SQL.publicationAny, id, collection.name);\n if (current === null || current.owner_id !== publisher.id) throw notFound();\n if (current.hidden_at !== null) throw forbidden('This entry is under moderation review.');\n if (current.revision !== expectedRevision) throw conflict();\n return json(200, rowToEntry(collection, current), noStore);\n }\n\n const tagsJson = JSON.stringify(derivation.tags);\n const result = await run(\n env,\n SQL.updatePublication,\n ARCHIVE_CONTRACT_VERSION,\n derivation.contentVersion,\n payloadJson,\n hash,\n derivation.filter,\n derivation.title,\n tagsJson,\n now(),\n id,\n publisher.id,\n expectedRevision,\n hash,\n );\n if ((result.meta?.changes ?? 0) === 0) {\n const current = await first<PublicationRow>(env, SQL.publicationAny, id, collection.name);\n if (current === null) throw notFound();\n if (current.hidden_at !== null) throw forbidden('This entry is under moderation review.');\n throw conflict();\n }\n const updated = await entryById(collection, env, id);\n if (updated === null) throw notFound();\n return json(200, updated, noStore);\n}\n\nexport async function deleteEntry(\n collection: ArchiveCollection,\n env: ArchiveWorkerEnv,\n session: Session,\n id: string,\n): Promise<Response> {\n // The owner's row regardless of moderation state: the author may still remove a\n // hidden entry (their content, their call) — hiding freezes edits, not removal.\n // Removing an entry is the author's alone; an operator may remove ANY entry — the\n // terminal moderation action for abusive content. The operator path carries the\n // same registered-publisher-row requirement every moderation route shares: one\n // accountable identity per action. Everyone else sees the entry as absent.\n if (session.operator) {\n const operator = requireOperatorRow(session);\n await databaseCall(() =>\n env.DB.batch([\n env.DB.prepare(SQL.auditRemoval).bind(uuid(), operator.id, now(), id, collection.name),\n env.DB.prepare(SQL.removePublicationAny).bind(id, collection.name),\n ]),\n );\n } else {\n const publisher = requirePublisherRow(session);\n const row = await first<PublicationRow>(env, SQL.publicationAny, id, collection.name);\n if (row === null) return new Response(null, { headers: noStore, status: 204 });\n if (row.owner_id !== publisher.id) throw notFound();\n await run(env, SQL.removePublication, id, publisher.id);\n }\n return new Response(null, { headers: noStore, status: 204 });\n}\n\nexport async function setLike(\n collection: ArchiveCollection,\n env: ArchiveWorkerEnv,\n config: ApiConfig,\n session: Session,\n id: string,\n liked: boolean,\n): Promise<Response> {\n const publisher = requirePublisherRow(session);\n if (liked && !config.likesEnabled) throw new ApiError(503, 'likes-disabled', 'Likes are currently disabled.');\n const row = await first<PublicationRow>(env, SQL.publication, id, collection.name);\n if (row === null) throw notFound();\n if (liked) {\n await run(env, SQL.insertLike, publisher.id, now(), id);\n } else {\n await run(env, SQL.removeLike, id, publisher.id);\n }\n // One consistent snapshot: the count and the viewer's flag read together.\n const state = await first<{ id: string; like_count: number; liked: number }>(env, SQL.likeState, publisher.id, id);\n if (state === null) throw notFound();\n const dto = { id: state.id, likeCount: state.like_count, liked: state.liked === 1 };\n const validated = archiveLikeStateSchema.safeParse(dto);\n if (!validated.success) throw recordUnavailable();\n return json(200, validated.data, noStore);\n}\n\n/* -------------------------------- moderation ------------------------------- */\n\n// One free-text report reason: trimmed, non-empty, well-formed, bounded.\nconst reportBodySchema = s.object({\n reason: s.string().check(isWellFormedText).trim().min(1).max(ARCHIVE_LIMITS.reportReasonChars),\n});\n\n/**\n * A reader's abuse report against one visible entry. Idempotent per (entry, reporter):\n * reporting again is the same 204, never a duplicate row. Hidden or missing entries\n * are 404 — they cannot be reported, they are already actioned or gone.\n */\nexport async function reportEntry(\n collection: ArchiveCollection,\n request: Request,\n env: ArchiveWorkerEnv,\n session: Session,\n id: string,\n): Promise<Response> {\n const reporter = requirePublisherRow(session);\n const body = await readJsonBody(request);\n const parsed = reportBodySchema.safeParse(body);\n if (!parsed.success) throw invalid();\n const row = await first<PublicationRow>(env, SQL.publication, id, collection.name);\n if (row === null) throw notFound();\n // Bound in the SQL's authored placeholder order: reporter, reason, created_at,\n // then the WHERE's entry id and collection.\n await run(env, SQL.insertReport, uuid(), reporter.id, parsed.data.reason, now(), id, collection.name);\n return new Response(null, { headers: noStore, status: 204 });\n}\n\n/**\n * The operator soft-hide: removes an entry from every public surface while keeping\n * the row, its likes and its retry key intact. Idempotent — already in the requested\n * state answers the same 204. Operator authority is the session's verified role\n * claim; a registered publisher row is still required (one accountable identity).\n */\nexport async function setHidden(\n collection: ArchiveCollection,\n env: ArchiveWorkerEnv,\n session: Session,\n id: string,\n hidden: boolean,\n): Promise<Response> {\n const operator = requireOperatorRow(session);\n // Any collection row, visible or hidden: the operator acts on the storage row. The\n // pre-read establishes existence only. The transactional audit guard decides\n // whether the requested transition is an effective action.\n const row = await first<PublicationRow>(env, SQL.publicationAny, id, collection.name);\n if (row === null) throw notFound();\n const statement = hidden\n ? env.DB.prepare(SQL.hidePublication).bind(now(), id, collection.name)\n : env.DB.prepare(SQL.unhidePublication).bind(id, collection.name);\n await databaseCall(() =>\n env.DB.batch([\n env.DB.prepare(SQL.auditHide).bind(\n uuid(),\n operator.id,\n hidden ? 'hide' : 'unhide',\n now(),\n id,\n collection.name,\n Number(hidden),\n ),\n statement,\n ]),\n );\n return new Response(null, { headers: noStore, status: 204 });\n}\n\n/** The operator's report queue for one collection: newest first, bounded. */\nexport async function listReports(\n collection: ArchiveCollection,\n request: Request,\n env: ArchiveWorkerEnv,\n session: Session,\n): Promise<Response> {\n requireOperatorRow(session);\n const status = new URL(request.url).searchParams.get('status') ?? 'unresolved';\n if (status !== 'unresolved' && status !== 'resolved') throw badRequest();\n const context = `reports:${collection.name}:${status}`;\n const { cursor, limit } = forwardPage(request, context, ['status']);\n const bound = reportsPageSql(\n { collection: collection.name, resolved: status === 'resolved' },\n { cursor, limit: limit + 1 },\n );\n const rows = await all<ReportRow>(env, bound.sql, ...bound.parameters);\n const page = rows.slice(0, limit);\n const last = page.at(-1);\n const parsed = archiveReportsSchema.safeParse({\n cursor:\n rows.length > limit && last !== undefined\n ? nextCursor({ createdAt: last.created_at, id: last.id }, context)\n : null,\n reports: page.map((row) => ({\n createdAt: row.created_at,\n entryId: row.publication_id,\n entryTitle: row.entry_title,\n id: row.id,\n reason: row.reason,\n reporter: { id: row.reporter_id, name: row.reporter_name },\n resolvedAt: row.resolved_at,\n })),\n });\n if (!parsed.success) throw recordUnavailable();\n return json(200, parsed.data, noStore);\n}\n\ninterface ReportRow {\n collection: string;\n created_at: string;\n entry_title: string;\n id: string;\n publication_id: string;\n reason: string;\n reporter_id: string;\n reporter_name: string;\n resolved_at: string | null;\n}\n\nexport async function resolveReport(\n collection: ArchiveCollection,\n env: ArchiveWorkerEnv,\n session: Session,\n id: string,\n): Promise<Response> {\n requireOperatorRow(session);\n const row = await first<{ id: string }>(env, SQL.reportById, id, collection.name);\n if (row === null) throw notFound();\n await run(env, SQL.resolveReport, now(), id, collection.name);\n return new Response(null, { headers: noStore, status: 204 });\n}\n\nexport async function myReports(request: Request, env: ArchiveWorkerEnv, session: Session): Promise<Response> {\n const context = `own-reports:${session.publisher?.id ?? 'unregistered'}`;\n const { cursor, limit } = forwardPage(request, context);\n if (session.publisher === null) return json(200, { cursor: null, reports: [] }, noStore);\n const bound = reportsPageSql({ publisherId: session.publisher.id }, { cursor, limit: limit + 1 });\n const rows = await all<ReportRow>(env, bound.sql, ...bound.parameters);\n const page = rows.slice(0, limit);\n const last = page.at(-1);\n const parsed = archiveOwnReportsSchema.safeParse({\n cursor:\n rows.length > limit && last !== undefined\n ? nextCursor({ createdAt: last.created_at, id: last.id }, context)\n : null,\n reports: page.map((row) => ({\n collection: row.collection,\n createdAt: row.created_at,\n entryId: row.publication_id,\n id: row.id,\n reason: row.reason,\n resolvedAt: row.resolved_at,\n })),\n });\n if (!parsed.success) throw recordUnavailable();\n return json(200, parsed.data, noStore);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"publications.js","names":[],"sources":["../../src/worker/publications.ts"],"sourcesContent":["import { s } from '@vielzeug/spell';\nimport type { ArchiveCollection } from '../collection';\nimport {\n ARCHIVE_CONTRACT_VERSION,\n ARCHIVE_LIMITS,\n ARCHIVE_SCHEMA_VERSION,\n type ArchiveModerationAction,\n archiveAliasSchema,\n archiveCollectionMetaSchema,\n archiveHealthSchema,\n archiveIdentitySchema,\n archiveLikedIdsSchema,\n archiveLikeStateSchema,\n archiveLikesPageSchema,\n archiveModerationLogSchema,\n archiveOwnReportsSchema,\n archiveReportsSchema,\n archiveSortSchema,\n isWellFormedText,\n SESSION_MAX_AGE_SECONDS,\n uuidSchema,\n} from '../contract';\nimport { forwardPage, forwardPageParams, nextCursor } from './_pagination';\nimport { JWT_CLOCK_TOLERANCE_SECONDS, type Publisher, type Session } from './auth';\nimport type { ApiConfig, ArchiveWorkerEnv } from './env';\nimport {\n ApiError,\n badRequest,\n conflict,\n forbidden,\n invalid,\n json,\n noStore,\n notFound,\n queryParams,\n readJsonBody,\n} from './http';\nimport {\n likedPublicationsSql,\n likesPageSql,\n listPublicationsSql,\n moderationLogSql,\n type PublicationCursor,\n type PublicationFilter,\n reportsPageSql,\n SQL,\n} from './sql';\n\n/**\n * Route handlers for /api/v1, parameterized by the registered collection. All SQL lives\n * in sql.ts and is bound positionally here in the authored order; the DTO boundary\n * re-validates every stored row before it leaves, content validation lives in the\n * collection's derive step, and moderation state (hidden entries) is visible only\n * through the owner and operator surfaces.\n */\n\ninterface PublicationRow {\n author_name: string;\n client_key: string;\n collection: string;\n content_hash: string;\n content_version: number;\n filter_value: string | null;\n /** Null while visible; set while an operator has removed the entry from public view. */\n hidden_at: string | null;\n id: string;\n like_count: number;\n owner_id: string;\n payload_json: string;\n published_at: string;\n revision: number;\n schema_version: number;\n tags_json: string;\n title: string;\n updated_at: string;\n}\n\nasync function databaseCall<T>(operation: () => Promise<T>): Promise<T> {\n try {\n return await operation();\n } catch (cause) {\n throw new ApiError(500, 'internal', 'The service is not available.', { cause, diagnostic: { kind: 'database' } });\n }\n}\nconst first = <T>(env: ArchiveWorkerEnv, sql: string, ...params: (string | number | null)[]) =>\n databaseCall(() =>\n env.DB.prepare(sql)\n .bind(...params)\n .first<T>(),\n );\nconst all = <T>(env: ArchiveWorkerEnv, sql: string, ...params: (string | number | null)[]) =>\n databaseCall(async () => {\n const result = await env.DB.prepare(sql)\n .bind(...params)\n .all<T>();\n if (!Array.isArray(result.results)) throw new Error('Missing database query results');\n return result.results;\n });\nconst run = (env: ArchiveWorkerEnv, sql: string, ...params: (string | number | null)[]) =>\n databaseCall(() =>\n env.DB.prepare(sql)\n .bind(...params)\n .run(),\n );\n\nconst uuid = (): string => crypto.randomUUID();\nconst now = (): string => new Date().toISOString();\n\n/** A stored row that fails the emitted-DTO contract is unavailable, not malformed data. */\nconst recordUnavailable = (): ApiError =>\n new ApiError(503, 'record-unavailable', 'The record is not available.', { diagnostic: { kind: 'record' } });\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/**\n * Maps a joined row to the public DTO through the collection's strict entry schema:\n * stored JSON is parsed and re-validated, stored versions are emitted as-is, and any\n * drift — unknown fields, corrupt payload, mismatched versions — surfaces as 503\n * `record-unavailable` rather than leaking or fabricating data.\n */\nconst rowToEntry = (collection: ArchiveCollection, row: PublicationRow) => {\n let payloadJson: unknown;\n try {\n payloadJson = JSON.parse(row.payload_json);\n } catch {\n throw recordUnavailable();\n }\n const parsed = collection.contract.entry.safeParse({\n author: { id: row.owner_id, name: row.author_name },\n contentHash: row.content_hash,\n contentVersion: row.content_version,\n id: row.id,\n likeCount: row.like_count,\n payload: payloadJson,\n publishedAt: row.published_at,\n revision: row.revision,\n schemaVersion: row.schema_version,\n updatedAt: row.updated_at,\n });\n if (!parsed.success) throw recordUnavailable();\n return parsed.data;\n};\n\nconst rowToOwnedEntry = (collection: ArchiveCollection, row: PublicationRow) => {\n const entry = rowToEntry(collection, row);\n // The private owner DTO adds the create retry key and the moderation state;\n // public routes never emit either.\n const key = uuidSchema.safeParse(row.client_key);\n if (!key.success) throw recordUnavailable();\n return { ...entry, clientKey: row.client_key, hidden: row.hidden_at !== null };\n};\n\nconst entryById = async (collection: ArchiveCollection, env: ArchiveWorkerEnv, id: string) => {\n const row = await first<PublicationRow>(env, SQL.publication, id, collection.name);\n return row === null ? null : rowToEntry(collection, row);\n};\n\n/* ----------------------------------- list --------------------------------- */\n\n/** Normalizes free text into the bounded FTS prefix-match expression the SQL expects. */\nconst searchMatchFor = (search: string | null): string | undefined => {\n if (search === null) return undefined;\n if (search.length > ARCHIVE_LIMITS.searchChars) throw badRequest();\n // Unicode word extraction: punctuation splits terms rather than poisoning them,\n // so \"Hunter's\" and \"fire-ice\" match the words their indexed titles carry.\n const terms = (search.match(/[\\p{L}\\p{N}]+/gu) ?? []).slice(0, ARCHIVE_LIMITS.searchTerms);\n if (terms.length === 0) return undefined;\n return terms.map((t) => `\"${t}\"*`).join(' ');\n};\n\n/**\n * The entry-list cursor context: every query dimension the page's boundary depends\n * on, so a cursor minted under one filter/sort/search and replayed under another is\n * a 400 — the same forward-cursor guard every other list in the package uses.\n */\nconst entryCursorContext = (query: {\n author?: string;\n collection: string;\n filter?: string;\n searchMatch?: string;\n sort: 'liked' | 'recent';\n tag?: string;\n}): string =>\n JSON.stringify([\n query.collection,\n query.sort,\n query.author ?? '',\n query.filter ?? '',\n query.tag ?? '',\n query.searchMatch ?? '',\n ]);\n\nconst cursorFromRow = (row: PublicationRow): PublicationCursor => ({\n createdAt: row.published_at,\n id: row.id,\n likeCount: row.like_count,\n});\n\nexport async function listEntries(\n collection: ArchiveCollection,\n request: Request,\n env: ArchiveWorkerEnv,\n): Promise<Response> {\n const url = new URL(request.url);\n const params = queryParams(url, ['author', 'cursor', 'filter', 'limit', 'search', 'sort', 'tag']);\n\n const sortRaw = params.get('sort') ?? 'recent';\n const sortParsed = archiveSortSchema.safeParse(sortRaw);\n if (!sortParsed.success) throw badRequest();\n\n const filter = params.get('filter');\n if (filter !== null && !collection.filters.includes(filter)) throw badRequest();\n const tag = params.get('tag');\n if (tag !== null && !collection.tags.includes(tag)) throw badRequest();\n const author = params.get('author');\n if (author !== null && !UUID_RE.test(author)) throw badRequest();\n\n const searchMatch = searchMatchFor(params.get('search'));\n const context = entryCursorContext({\n author: author ?? undefined,\n collection: collection.name,\n filter: filter ?? undefined,\n searchMatch,\n sort: sortParsed.data,\n tag: tag ?? undefined,\n });\n const { cursor, limit } = forwardPageParams(params, context);\n\n const query: PublicationFilter = {\n author: author ?? undefined,\n collection: collection.name,\n cursor:\n cursor === null ? undefined : { createdAt: cursor.createdAt, id: cursor.id, likeCount: cursor.likeCount ?? 0 },\n filter: filter ?? undefined,\n limit: limit + 1,\n searchMatch,\n sort: sortParsed.data,\n tag: tag ?? undefined,\n };\n\n // One bounded query — never a count: the over-read row is the next page's proof.\n const bound = listPublicationsSql(query);\n const fetched = await all<PublicationRow>(env, bound.sql, ...bound.parameters);\n const page = fetched.slice(0, limit);\n const last = page.at(-1);\n\n // Entries are validated per row through the collection's entry schema; the cursor\n // is internally generated, so the page composes validated parts directly.\n const pageDto = {\n cursor: fetched.length > limit && last !== undefined ? nextCursor(cursorFromRow(last), context) : null,\n entries: page.map((row) => rowToEntry(collection, row)),\n };\n return json(200, pageDto, noStore);\n}\n\n/** The collection's public self-description: name plus the declared query vocabularies. */\nexport function collectionMeta(collection: ArchiveCollection): Response {\n const parsed = archiveCollectionMetaSchema.safeParse({\n filters: [...collection.filters].sort(),\n name: collection.name,\n tags: [...collection.tags].sort(),\n });\n if (!parsed.success) throw recordUnavailable();\n return json(200, parsed.data, noStore);\n}\n\nexport async function getEntry(collection: ArchiveCollection, env: ArchiveWorkerEnv, id: string): Promise<Response> {\n const entry = await entryById(collection, env, id);\n if (entry === null) throw notFound();\n return json(200, entry, noStore);\n}\n\n/* ---------------------------------- health --------------------------------- */\n\n/**\n * Service health: the feature flags, the wire limits, and the storage schema stamp\n * actually present in the database. A database whose migrations lag the package\n * answers `degraded` instead of pretending to be fine — the most likely deployment\n * fault becomes a one-glance diagnosis.\n */\nexport async function healthJson(config: ApiConfig, env: ArchiveWorkerEnv): Promise<Response> {\n let dbSchemaVersion: number | null = null;\n let reason: 'database-unavailable' | 'schema-mismatch' | null = null;\n try {\n const row = await first<{ applied: number }>(env, SQL.schemaVersion);\n dbSchemaVersion = row?.applied ?? 0;\n } catch {\n reason = 'database-unavailable';\n }\n if (reason === null && dbSchemaVersion !== ARCHIVE_SCHEMA_VERSION) reason = 'schema-mismatch';\n const parsed = archiveHealthSchema.safeParse({\n dbSchemaVersion,\n features: { likes: config.likesEnabled, publishing: config.publishingEnabled },\n limits: ARCHIVE_LIMITS,\n reason,\n schemaVersion: ARCHIVE_CONTRACT_VERSION,\n status: reason === null ? 'ok' : 'degraded',\n });\n if (!parsed.success) throw recordUnavailable();\n return json(reason === null ? 200 : 503, parsed.data, noStore);\n}\n\n/* --------------------------------- identity -------------------------------- */\n\n/** Emits the private identity DTO through the strict shared schema. */\nconst identityJson = (status: number, identity: unknown): Response => {\n const parsed = archiveIdentitySchema.safeParse(identity);\n if (!parsed.success) throw recordUnavailable();\n return json(status, parsed.data, noStore);\n};\n\nexport function getMe(session: Session | null): Response {\n return identityJson(\n 200,\n session?.publisher\n ? { id: session.publisher.id, name: session.publisher.displayName, operator: session.operator }\n : null,\n );\n}\n\nexport async function registerMe(request: Request, env: ArchiveWorkerEnv, session: Session): Promise<Response> {\n const body = await readJsonBody(request);\n const parsed = registerBodySchema.safeParse(body);\n if (!parsed.success) throw invalid();\n const name = parsed.data.name ?? '';\n\n const existing = await first<{ display_name: string; id: string }>(\n env,\n SQL.publisher,\n session.subject.issuer,\n session.subject.subject,\n );\n if (existing !== null) {\n return identityJson(200, { id: existing.id, name: existing.display_name, operator: session.operator });\n }\n const id = uuid();\n await run(\n env,\n SQL.insertPublisher,\n id,\n session.subject.issuer,\n session.subject.subject,\n name,\n now(),\n session.subject.issuer,\n session.subject.subject,\n Date.now(),\n );\n const row = await first<{ display_name: string; id: string }>(\n env,\n SQL.publisher,\n session.subject.issuer,\n session.subject.subject,\n );\n if (row === null) {\n if ((await first(env, SQL.revokedSubject, session.subject.issuer, session.subject.subject, Date.now())) !== null) {\n throw new ApiError(401, 'unauthenticated', 'Sign-in is required.');\n }\n throw conflict();\n }\n return identityJson(201, { id: row.id, name: row.display_name, operator: session.operator });\n}\n\nexport async function renameMe(request: Request, env: ArchiveWorkerEnv, session: Session): Promise<Response> {\n const publisher = requirePublisherRow(session);\n const body = await readJsonBody(request);\n const parsed = renameBodySchema.safeParse(body);\n if (!parsed.success) throw invalid();\n if (parsed.data.name !== publisher.displayName) {\n await run(env, SQL.renamePublisher, parsed.data.name, publisher.id, parsed.data.name);\n }\n return identityJson(200, { id: publisher.id, name: parsed.data.name, operator: session.operator });\n}\n\n/**\n * Archive-profile erasure, independent of the identity provider's account.\n * The credential survives so an issuer can retry coordinated account erasure.\n */\nexport async function deleteProfile(env: ArchiveWorkerEnv, session: Session): Promise<Response> {\n if (session.publisher !== null) await run(env, SQL.deletePublisher, session.publisher.id);\n return new Response(null, {\n headers: {\n 'cache-control': 'no-store',\n },\n status: 204,\n });\n}\n\n/** Revocation and erasure share a transaction, preventing a stale session from recreating the profile. */\nexport async function deleteAccountProfile(env: ArchiveWorkerEnv, session: Session): Promise<Response> {\n const { issuer, subject } = session.subject;\n // Issuance may be one tolerance ahead; expiry verification permits another.\n await databaseCall(() =>\n env.DB.batch([\n env.DB.prepare(SQL.revokeSubject).bind(\n issuer,\n subject,\n Date.now() + (SESSION_MAX_AGE_SECONDS + 2 * JWT_CLOCK_TOLERANCE_SECONDS) * 1000,\n ),\n env.DB.prepare(SQL.deleteSubjectPublisher).bind(issuer, subject),\n ]),\n );\n return new Response(null, { headers: noStore, status: 204 });\n}\n\n/**\n * Operator-side GDPR erasure: removes any publisher's row; FK cascades take its\n * publications, likes and retry keys. Idempotent for the same reason the self-erase\n * is: an already-erased publisher is a 204 with nothing left to remove.\n */\nexport async function erasePublisher(env: ArchiveWorkerEnv, session: Session, publisherId: string): Promise<Response> {\n const operator = requireOperatorRow(session);\n // The terminal moderation action: erasing a publisher removes their row, and FK\n // cascades take the publications, likes and retry keys with it — the same cascade a\n // self-erase walks, applied to any publisher the operator names. The guarded audit\n // and deletion share a transaction, so retries never record fictitious actions.\n await databaseCall(() =>\n env.DB.batch([\n env.DB.prepare(SQL.auditErasure).bind(uuid(), operator.id, now(), publisherId),\n env.DB.prepare(SQL.deletePublisher).bind(publisherId),\n ]),\n );\n return new Response(null, { headers: noStore, status: 204 });\n}\n\n/** The operator's accountability log: newest actions first, bounded, ids only. */\nexport async function listModerationLog(request: Request, env: ArchiveWorkerEnv, session: Session): Promise<Response> {\n requireOperatorRow(session);\n const context = 'moderation-log';\n const { cursor, limit } = forwardPage(request, context);\n const bound = moderationLogSql({ cursor, limit: limit + 1 });\n const rows = await all<{\n id: string;\n action: ArchiveModerationAction;\n collection: string | null;\n created_at: string;\n operator_id: string;\n target_id: string;\n }>(env, bound.sql, ...bound.parameters);\n const page = rows.slice(0, limit);\n const last = page.at(-1);\n const parsed = archiveModerationLogSchema.safeParse({\n actions: page.map((row) => ({\n action: row.action,\n collection: row.collection,\n createdAt: row.created_at,\n id: row.id,\n operatorId: row.operator_id,\n targetId: row.target_id,\n })),\n cursor:\n rows.length > limit && last !== undefined\n ? nextCursor({ createdAt: last.created_at, id: last.id }, context)\n : null,\n });\n if (!parsed.success) throw recordUnavailable();\n return json(200, parsed.data, noStore);\n}\n\nexport const requirePublisherRow = (session: Session): Publisher => {\n if (session.publisher === null) throw forbidden('Register before publishing.');\n return session.publisher;\n};\n\n/** Operator authority plus a registered publisher row: moderation routes need both,\n * the role claim for authority and the row for attribution. */\nexport const requireOperatorRow = (session: Session): Publisher => {\n if (!session.operator) throw forbidden('Operator access is required.');\n if (session.publisher === null) throw forbidden('Register before moderating.');\n return session.publisher;\n};\n\n// Private identity payloads share the one alias rule with the emitted DTOs.\nconst registerBodySchema = s.object({ name: archiveAliasSchema.optional() });\nconst renameBodySchema = s.object({ name: archiveAliasSchema });\n\nexport async function myPublications(\n collection: ArchiveCollection,\n env: ArchiveWorkerEnv,\n session: Session,\n): Promise<Response> {\n if (session.publisher === null) return json(200, { entries: [] }, noStore);\n const rows = await all<PublicationRow>(\n env,\n SQL.ownPublications,\n session.publisher.id,\n collection.name,\n ARCHIVE_LIMITS.maxPublications,\n );\n // The private owner DTO adds the create retry key; public routes never emit it.\n return json(200, { entries: rows.map((row) => rowToOwnedEntry(collection, row)) }, noStore);\n}\n\nexport async function myLikes(request: Request, env: ArchiveWorkerEnv, session: Session): Promise<Response> {\n const params = new URL(request.url).searchParams;\n const raw = params.get('ids');\n if (raw === null) {\n const context = `likes:${session.publisher?.id ?? 'unregistered'}`;\n const { cursor, limit } = forwardPage(request, context);\n if (session.publisher === null) return json(200, { cursor: null, ids: [] }, noStore);\n const bound = likesPageSql(session.publisher.id, { cursor, limit: limit + 1 });\n const rows = await all<{ created_at: string; publication_id: string }>(env, bound.sql, ...bound.parameters);\n const page = rows.slice(0, limit);\n const last = page.at(-1);\n const parsed = archiveLikesPageSchema.safeParse({\n cursor:\n rows.length > limit && last !== undefined\n ? nextCursor({ createdAt: last.created_at, id: last.publication_id }, context)\n : null,\n ids: page.map((row) => row.publication_id),\n });\n if (!parsed.success) throw recordUnavailable();\n return json(200, parsed.data, noStore);\n }\n queryParams(new URL(request.url), ['ids']);\n const ids = [...new Set(raw.split(','))];\n if (ids.length > ARCHIVE_LIMITS.likeStateIds || ids.some((id) => !UUID_RE.test(id))) throw badRequest();\n if (session.publisher === null) return json(200, { ids: [] }, noStore);\n const bound = likedPublicationsSql(session.publisher.id, ids);\n const rows = await all<{ publication_id: string }>(env, bound.sql, ...bound.parameters);\n const liked = new Set(rows.map((r) => r.publication_id));\n const dto = { ids: ids.filter((id) => liked.has(id)) };\n const validated = archiveLikedIdsSchema.safeParse(dto);\n if (!validated.success) throw recordUnavailable();\n return json(200, validated.data, noStore);\n}\n\n/* --------------------------------- mutations ------------------------------- */\n\n// The core validates the request envelope; the collection's derive step validates the payload.\nconst publishBodySchema = s.object({ clientKey: uuidSchema, payload: s.unknown() });\nconst updateBodySchema = s.object({\n expectedRevision: s.number().int().min(1).max(Number.MAX_SAFE_INTEGER),\n payload: s.unknown(),\n});\n\ninterface RetryRecord {\n expires_at: number;\n payload_hash: string;\n publication_id: string | null;\n}\n\n/**\n * The one replay/conflict/gone decision for a create retry key, shared by the\n * pre-read and the post-insert race re-read: a live record for a different payload\n * is a 409, a tombstone (null publication id) is a 410, and a live record for the\n * same payload replays the entry. `'vanished'` means the recorded publication is\n * no longer readable — hidden or raced-deleted — and only the caller knows which:\n * the pre-read falls through to the publication-by-key lookup (a hidden entry is\n * 403 there), the post-insert re-read answers 410.\n */\nasync function resolveRetryRecord(\n collection: ArchiveCollection,\n env: ArchiveWorkerEnv,\n record: RetryRecord | null,\n hash: string,\n): Promise<Response | 'vanished' | null> {\n if (record === null || record.expires_at <= Date.now()) return null;\n if (record.payload_hash !== hash) throw conflict('That key was used for a different payload.');\n if (record.publication_id === null) throw new ApiError(410, 'gone', 'That publication was removed.');\n const entry = await entryById(collection, env, record.publication_id);\n if (entry === null) return 'vanished';\n return json(200, entry, noStore);\n}\n\nexport async function publishEntry(\n collection: ArchiveCollection,\n request: Request,\n env: ArchiveWorkerEnv,\n config: ApiConfig,\n session: Session,\n): Promise<Response> {\n if (!config.publishingEnabled) throw new ApiError(503, 'publishing-disabled', 'Publishing is currently disabled.');\n const publisher = requirePublisherRow(session);\n const body = await readJsonBody(request);\n const parsed = publishBodySchema.safeParse(body);\n if (!parsed.success) throw invalid();\n const { clientKey } = parsed.data;\n // The derive step owns every content rejection: schema, vocabulary, title, byte bound.\n const derivation = await collection.derive(parsed.data.payload);\n if (derivation === null) throw invalid();\n\n const payloadJson = derivation.canonicalJson;\n const hash = derivation.contentHash;\n const owner = publisher.id;\n const name = collection.name;\n\n // Retry-key inspection first: the request record decides replay vs conflict vs gone.\n // A logically expired record is ignored entirely — retention is enforced by time,\n // not just by the periodic sweep.\n const retry = await first<RetryRecord>(env, SQL.request, owner, name, clientKey);\n const replay = await resolveRetryRecord(collection, env, retry, hash);\n if (replay !== null && replay !== 'vanished') return replay;\n\n const existing = await first<PublicationRow>(env, SQL.publicationByKey, owner, name, clientKey);\n if (existing !== null) {\n // A hidden entry is frozen for its owner too: replaying the create key must not\n // read as a live publication.\n if (existing.hidden_at !== null) throw forbidden('This entry is under moderation review.');\n if (existing.content_hash !== hash) throw conflict('That key was used for a different payload.');\n return json(200, rowToEntry(collection, existing), noStore);\n }\n\n const timestamp = now();\n const expiresAt = Date.now() + ARCHIVE_LIMITS.idempotencyRetentionMs;\n const tagsJson = JSON.stringify(derivation.tags);\n const results = await databaseCall(() =>\n env.DB.batch([\n env.DB.prepare(SQL.expireRequests).bind(Date.now()),\n env.DB.prepare(SQL.insertPublication).bind(\n uuid(),\n owner,\n name,\n clientKey,\n ARCHIVE_CONTRACT_VERSION,\n derivation.contentVersion,\n payloadJson,\n hash,\n derivation.filter,\n derivation.title,\n tagsJson,\n timestamp,\n timestamp,\n owner,\n name,\n ARCHIVE_LIMITS.maxPublications,\n owner,\n name,\n clientKey,\n Date.now(),\n ),\n env.DB.prepare(SQL.insertRequest).bind(\n owner,\n name,\n clientKey,\n hash,\n timestamp,\n expiresAt,\n owner,\n name,\n clientKey,\n hash,\n ),\n ]),\n );\n if ((results[1]?.meta?.changes ?? 0) === 0) {\n // The guarded insert was skipped: a race already wrote the key, the key's\n // publication was removed, or the owner's live-publication quota is full.\n // Re-reading the request record first distinguishes a tombstone from a race.\n const racedRecord = await first<RetryRecord>(env, SQL.request, owner, name, clientKey);\n const racedReplay = await resolveRetryRecord(collection, env, racedRecord, hash);\n if (racedReplay === 'vanished') throw new ApiError(410, 'gone', 'That publication was removed.');\n if (racedReplay !== null) return racedReplay;\n const raced = await first<PublicationRow>(env, SQL.publicationByKey, owner, name, clientKey);\n if (raced !== null && raced.hidden_at !== null) throw forbidden('This entry is under moderation review.');\n if (raced !== null && raced.content_hash === hash) return json(200, rowToEntry(collection, raced), noStore);\n if (raced !== null) throw conflict('That key was used for a different payload.');\n throw new ApiError(429, 'quota', 'Publication quota reached.');\n }\n\n const created = await first<PublicationRow>(env, SQL.publicationByKey, owner, name, clientKey);\n if (created === null) throw conflict();\n return json(201, rowToEntry(collection, created), noStore);\n}\n\nexport async function updateEntry(\n collection: ArchiveCollection,\n request: Request,\n env: ArchiveWorkerEnv,\n config: ApiConfig,\n session: Session,\n id: string,\n): Promise<Response> {\n if (!config.publishingEnabled) throw new ApiError(503, 'publishing-disabled', 'Publishing is currently disabled.');\n const publisher = requirePublisherRow(session);\n // The owner's row regardless of moderation state: the author must be told their\n // entry is frozen, not that it vanished.\n const row = await first<PublicationRow>(env, SQL.publicationAny, id, collection.name);\n // Replacing an entry is the author's alone; anyone else sees the entry as absent.\n if (row === null || row.owner_id !== publisher.id) throw notFound();\n // Hidden entries are frozen: an edit must not bypass the operator's decision.\n if (row.hidden_at !== null) throw forbidden('This entry is under moderation review.');\n\n const body = await readJsonBody(request);\n const parsed = updateBodySchema.safeParse(body);\n if (!parsed.success) throw invalid();\n const { expectedRevision } = parsed.data;\n\n if (expectedRevision !== row.revision) throw conflict();\n\n const derivation = await collection.derive(parsed.data.payload);\n if (derivation === null) throw invalid();\n const payloadJson = derivation.canonicalJson;\n const hash = derivation.contentHash;\n if (hash === row.content_hash) {\n const current = await first<PublicationRow>(env, SQL.publicationAny, id, collection.name);\n if (current === null || current.owner_id !== publisher.id) throw notFound();\n if (current.hidden_at !== null) throw forbidden('This entry is under moderation review.');\n if (current.revision !== expectedRevision) throw conflict();\n return json(200, rowToEntry(collection, current), noStore);\n }\n\n const tagsJson = JSON.stringify(derivation.tags);\n const result = await run(\n env,\n SQL.updatePublication,\n ARCHIVE_CONTRACT_VERSION,\n derivation.contentVersion,\n payloadJson,\n hash,\n derivation.filter,\n derivation.title,\n tagsJson,\n now(),\n id,\n publisher.id,\n expectedRevision,\n hash,\n );\n if ((result.meta?.changes ?? 0) === 0) {\n const current = await first<PublicationRow>(env, SQL.publicationAny, id, collection.name);\n if (current === null) throw notFound();\n if (current.hidden_at !== null) throw forbidden('This entry is under moderation review.');\n throw conflict();\n }\n const updated = await entryById(collection, env, id);\n if (updated === null) throw notFound();\n return json(200, updated, noStore);\n}\n\nexport async function deleteEntry(\n collection: ArchiveCollection,\n env: ArchiveWorkerEnv,\n session: Session,\n id: string,\n): Promise<Response> {\n // The owner's row regardless of moderation state: the author may still remove a\n // hidden entry (their content, their call) — hiding freezes edits, not removal.\n // Removing an entry is the author's alone; an operator may remove ANY entry — the\n // terminal moderation action for abusive content. The operator path carries the\n // same registered-publisher-row requirement every moderation route shares: one\n // accountable identity per action. Everyone else sees the entry as absent.\n if (session.operator) {\n const operator = requireOperatorRow(session);\n await databaseCall(() =>\n env.DB.batch([\n env.DB.prepare(SQL.auditRemoval).bind(uuid(), operator.id, now(), id, collection.name),\n env.DB.prepare(SQL.removePublicationAny).bind(id, collection.name),\n ]),\n );\n } else {\n const publisher = requirePublisherRow(session);\n const row = await first<PublicationRow>(env, SQL.publicationAny, id, collection.name);\n if (row === null) return new Response(null, { headers: noStore, status: 204 });\n if (row.owner_id !== publisher.id) throw notFound();\n await run(env, SQL.removePublication, id, publisher.id);\n }\n return new Response(null, { headers: noStore, status: 204 });\n}\n\nexport async function setLike(\n collection: ArchiveCollection,\n env: ArchiveWorkerEnv,\n config: ApiConfig,\n session: Session,\n id: string,\n liked: boolean,\n): Promise<Response> {\n const publisher = requirePublisherRow(session);\n if (liked && !config.likesEnabled) throw new ApiError(503, 'likes-disabled', 'Likes are currently disabled.');\n const row = await first<PublicationRow>(env, SQL.publication, id, collection.name);\n if (row === null) throw notFound();\n if (liked) {\n await run(env, SQL.insertLike, publisher.id, now(), id);\n } else {\n await run(env, SQL.removeLike, id, publisher.id);\n }\n // One consistent snapshot: the count and the viewer's flag read together.\n const state = await first<{ id: string; like_count: number; liked: number }>(env, SQL.likeState, publisher.id, id);\n if (state === null) throw notFound();\n const dto = { id: state.id, likeCount: state.like_count, liked: state.liked === 1 };\n const validated = archiveLikeStateSchema.safeParse(dto);\n if (!validated.success) throw recordUnavailable();\n return json(200, validated.data, noStore);\n}\n\n/* -------------------------------- moderation ------------------------------- */\n\n// One free-text report reason: trimmed, non-empty, well-formed, bounded.\nconst reportBodySchema = s.object({\n reason: s.string().check(isWellFormedText).trim().min(1).max(ARCHIVE_LIMITS.reportReasonChars),\n});\n\n/**\n * A reader's abuse report against one visible entry. Idempotent per (entry, reporter):\n * reporting again is the same 204, never a duplicate row. Hidden or missing entries\n * are 404 — they cannot be reported, they are already actioned or gone.\n */\nexport async function reportEntry(\n collection: ArchiveCollection,\n request: Request,\n env: ArchiveWorkerEnv,\n session: Session,\n id: string,\n): Promise<Response> {\n const reporter = requirePublisherRow(session);\n const body = await readJsonBody(request);\n const parsed = reportBodySchema.safeParse(body);\n if (!parsed.success) throw invalid();\n const row = await first<PublicationRow>(env, SQL.publication, id, collection.name);\n if (row === null) throw notFound();\n // Bound in the SQL's authored placeholder order: reporter, reason, created_at,\n // then the WHERE's entry id and collection.\n await run(env, SQL.insertReport, uuid(), reporter.id, parsed.data.reason, now(), id, collection.name);\n return new Response(null, { headers: noStore, status: 204 });\n}\n\n/**\n * The operator soft-hide: removes an entry from every public surface while keeping\n * the row, its likes and its retry key intact. Idempotent — already in the requested\n * state answers the same 204. Operator authority is the session's verified role\n * claim; a registered publisher row is still required (one accountable identity).\n */\nexport async function setHidden(\n collection: ArchiveCollection,\n env: ArchiveWorkerEnv,\n session: Session,\n id: string,\n hidden: boolean,\n): Promise<Response> {\n const operator = requireOperatorRow(session);\n // Any collection row, visible or hidden: the operator acts on the storage row. The\n // pre-read establishes existence only. The transactional audit guard decides\n // whether the requested transition is an effective action.\n const row = await first<PublicationRow>(env, SQL.publicationAny, id, collection.name);\n if (row === null) throw notFound();\n const statement = hidden\n ? env.DB.prepare(SQL.hidePublication).bind(now(), id, collection.name)\n : env.DB.prepare(SQL.unhidePublication).bind(id, collection.name);\n await databaseCall(() =>\n env.DB.batch([\n env.DB.prepare(SQL.auditHide).bind(\n uuid(),\n operator.id,\n hidden ? 'hide' : 'unhide',\n now(),\n id,\n collection.name,\n Number(hidden),\n ),\n statement,\n ]),\n );\n return new Response(null, { headers: noStore, status: 204 });\n}\n\n/** The operator's report queue for one collection: newest first, bounded. */\nexport async function listReports(\n collection: ArchiveCollection,\n request: Request,\n env: ArchiveWorkerEnv,\n session: Session,\n): Promise<Response> {\n requireOperatorRow(session);\n const status = new URL(request.url).searchParams.get('status') ?? 'unresolved';\n if (status !== 'unresolved' && status !== 'resolved') throw badRequest();\n const context = `reports:${collection.name}:${status}`;\n const { cursor, limit } = forwardPage(request, context, ['status']);\n const bound = reportsPageSql(\n { collection: collection.name, resolved: status === 'resolved' },\n { cursor, limit: limit + 1 },\n );\n const rows = await all<ReportRow>(env, bound.sql, ...bound.parameters);\n const page = rows.slice(0, limit);\n const last = page.at(-1);\n const parsed = archiveReportsSchema.safeParse({\n cursor:\n rows.length > limit && last !== undefined\n ? nextCursor({ createdAt: last.created_at, id: last.id }, context)\n : null,\n reports: page.map((row) => ({\n createdAt: row.created_at,\n entryId: row.publication_id,\n entryTitle: row.entry_title,\n id: row.id,\n reason: row.reason,\n reporter: { id: row.reporter_id, name: row.reporter_name },\n resolvedAt: row.resolved_at,\n })),\n });\n if (!parsed.success) throw recordUnavailable();\n return json(200, parsed.data, noStore);\n}\n\ninterface ReportRow {\n collection: string;\n created_at: string;\n entry_title: string;\n id: string;\n publication_id: string;\n reason: string;\n reporter_id: string;\n reporter_name: string;\n resolved_at: string | null;\n}\n\nexport async function resolveReport(\n collection: ArchiveCollection,\n env: ArchiveWorkerEnv,\n session: Session,\n id: string,\n): Promise<Response> {\n requireOperatorRow(session);\n const row = await first<{ id: string }>(env, SQL.reportById, id, collection.name);\n if (row === null) throw notFound();\n await run(env, SQL.resolveReport, now(), id, collection.name);\n return new Response(null, { headers: noStore, status: 204 });\n}\n\nexport async function myReports(request: Request, env: ArchiveWorkerEnv, session: Session): Promise<Response> {\n const context = `own-reports:${session.publisher?.id ?? 'unregistered'}`;\n const { cursor, limit } = forwardPage(request, context);\n if (session.publisher === null) return json(200, { cursor: null, reports: [] }, noStore);\n const bound = reportsPageSql({ publisherId: session.publisher.id }, { cursor, limit: limit + 1 });\n const rows = await all<ReportRow>(env, bound.sql, ...bound.parameters);\n const page = rows.slice(0, limit);\n const last = page.at(-1);\n const parsed = archiveOwnReportsSchema.safeParse({\n cursor:\n rows.length > limit && last !== undefined\n ? nextCursor({ createdAt: last.created_at, id: last.id }, context)\n : null,\n reports: page.map((row) => ({\n collection: row.collection,\n createdAt: row.created_at,\n entryId: row.publication_id,\n id: row.id,\n reason: row.reason,\n resolvedAt: row.resolved_at,\n })),\n });\n if (!parsed.success) throw recordUnavailable();\n return json(200, parsed.data, noStore);\n}\n"],"mappings":"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"}
@@ -50,11 +50,11 @@ var e={auditErasure:`INSERT INTO moderation_actions(id, operator_id, action, col
50
50
  SELECT 1 FROM publication_tags t WHERE t.publication_id = p.id AND t.tag = ?
51
51
  )`),n.push(e.tag)),e.searchMatch&&(t.push(`p.rowid IN (
52
52
  SELECT rowid FROM publication_search WHERE publication_search MATCH ?
53
- )`),n.push(e.searchMatch));let r=e.cursor,i=e.direction===`previous`?`>`:`<`;r&&(e.sort===`liked`?(t.push(`(p.like_count ${i} ? OR (p.like_count = ? AND (
54
- p.published_at ${i} ? OR (p.published_at = ? AND p.id ${i} ?)
55
- )))`),n.push(r.likeCount,r.likeCount,r.publishedAt,r.publishedAt,r.id)):(t.push(`(p.published_at ${i} ? OR (p.published_at = ? AND p.id ${i} ?))`),n.push(r.publishedAt,r.publishedAt,r.id)));let a=e.direction===`previous`?`ASC`:`DESC`,o=e.sort===`liked`?`p.like_count ${a}, p.published_at ${a}, p.id ${a}`:`p.published_at ${a}, p.id ${a}`;return n.push(e.limit),{parameters:n,sql:`SELECT p.*, u.display_name AS author_name
53
+ )`),n.push(e.searchMatch));let r=e.cursor;r&&(e.sort===`liked`?(t.push(`(p.like_count < ? OR (p.like_count = ? AND (
54
+ p.published_at < ? OR (p.published_at = ? AND p.id < ?)
55
+ )))`),n.push(r.likeCount,r.likeCount,r.createdAt,r.createdAt,r.id)):(t.push(`(p.published_at < ? OR (p.published_at = ? AND p.id < ?))`),n.push(r.createdAt,r.createdAt,r.id)));let i=e.sort===`liked`?`p.like_count DESC, p.published_at DESC, p.id DESC`:`p.published_at DESC, p.id DESC`;return n.push(e.limit),{parameters:n,sql:`SELECT p.*, u.display_name AS author_name
56
56
  FROM publications p JOIN publishers u ON u.id = p.owner_id
57
57
  ${t.length?`WHERE ${t.join(` AND `)}`:``}
58
- ORDER BY ${o} LIMIT ?`}}function likedPublicationsSql(e,t){return{parameters:[e,...t],sql:`SELECT publication_id FROM likes WHERE publisher_id = ?
58
+ ORDER BY ${i} LIMIT ?`}}function likedPublicationsSql(e,t){return{parameters:[e,...t],sql:`SELECT publication_id FROM likes WHERE publisher_id = ?
59
59
  AND publication_id IN (${t.map(()=>`?`).join(`, `)||`NULL`})`}}exports.SQL=e,exports.likedPublicationsSql=likedPublicationsSql,exports.likesPageSql=likesPageSql,exports.listPublicationsSql=listPublicationsSql,exports.moderationLogSql=moderationLogSql,exports.reportsPageSql=reportsPageSql;
60
60
  //# sourceMappingURL=sql.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"sql.cjs","names":[],"sources":["../../src/worker/sql.ts"],"sourcesContent":["/**\n * The runtime SQL the package-owned migration (`packages/archive/migrations/`) must\n * serve. The collection dimension is data (`collection` column, `publication_tags`),\n * never DDL. The migration also stamps the storage schema (`archive_schema_meta`,\n * equal to `ARCHIVE_SCHEMA_VERSION`), which health reads back.\n */\n\n/** SQL is centralized so every mutation's ownership and no-op predicates are reviewable. */\nexport const SQL = {\n auditErasure: `INSERT INTO moderation_actions(id, operator_id, action, collection, target_id, created_at)\n SELECT ?, ?, 'erase-publisher', NULL, id, ? FROM publishers WHERE id = ?`,\n auditHide: `INSERT INTO moderation_actions(id, operator_id, action, collection, target_id, created_at)\n SELECT ?, ?, ?, collection, id, ? FROM publications\n WHERE id = ? AND collection = ? AND (hidden_at IS NOT NULL) <> ?`,\n auditRemoval: `INSERT INTO moderation_actions(id, operator_id, action, collection, target_id, created_at)\n SELECT ?, ?, 'remove', collection, id, ? FROM publications WHERE id = ? AND collection = ?`,\n deletePublisher: `DELETE FROM publishers WHERE id = ?`,\n deleteSubjectPublisher: `DELETE FROM publishers WHERE issuer = ? AND subject = ?`,\n expireRequests: `DELETE FROM publication_requests WHERE expires_at <= ?`,\n expireRevocations: `DELETE FROM revoked_subjects WHERE expires_at <= ?`,\n hidePublication: `UPDATE publications SET hidden_at = ?\n WHERE id = ? AND collection = ? AND hidden_at IS NULL`,\n insertLike: `INSERT INTO likes(publication_id, publisher_id, created_at)\n SELECT id, ?, ? FROM publications WHERE id = ? AND hidden_at IS NULL\n ON CONFLICT(publication_id, publisher_id) DO NOTHING`,\n insertPublication: `INSERT INTO publications(\n id, owner_id, collection, client_key, schema_version, content_version, payload_json,\n content_hash, filter_value, title, tags_json, published_at, updated_at\n ) SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?\n WHERE (SELECT count(*) FROM publications WHERE owner_id = ? AND collection = ?) < ?\n AND NOT EXISTS (\n SELECT 1 FROM publication_requests\n WHERE owner_id = ? AND collection = ? AND client_key = ? AND expires_at > ?\n )\n ON CONFLICT(collection, owner_id, client_key) DO NOTHING`,\n insertPublisher: `INSERT INTO publishers(id, issuer, subject, display_name, created_at)\n SELECT ?, ?, ?, ?, ?\n WHERE NOT EXISTS (SELECT 1 FROM revoked_subjects WHERE issuer = ? AND subject = ? AND expires_at > ?)\n ON CONFLICT(issuer, subject) DO NOTHING`,\n insertReport: `INSERT INTO publication_reports(id, publication_id, reporter_id, reason, created_at)\n SELECT ?, id, ?, ?, ? FROM publications WHERE id = ? AND collection = ? AND hidden_at IS NULL\n ON CONFLICT(publication_id, reporter_id) DO NOTHING`,\n insertRequest: `INSERT INTO publication_requests(\n owner_id, collection, client_key, payload_hash, publication_id, created_at, expires_at\n ) SELECT ?, ?, ?, ?, p.id, ?, ? FROM publications p\n WHERE p.owner_id = ? AND p.collection = ? AND p.client_key = ? AND p.content_hash = ?\n ON CONFLICT(owner_id, collection, client_key) DO NOTHING`,\n likeState: `SELECT p.id, p.like_count, EXISTS(SELECT 1 FROM likes l WHERE l.publication_id = p.id AND l.publisher_id = ?) AS liked FROM publications p WHERE p.id = ? AND p.hidden_at IS NULL`,\n ownPublications: `SELECT p.*, u.display_name AS author_name\n FROM publications p JOIN publishers u ON u.id = p.owner_id\n WHERE p.owner_id = ? AND p.collection = ? ORDER BY p.published_at DESC, p.id DESC LIMIT ?`,\n /** The public detail row: a hidden entry is invisible to everyone but its owner. */\n publication: `SELECT p.*, u.display_name AS author_name\n FROM publications p JOIN publishers u ON u.id = p.owner_id\n WHERE p.id = ? AND p.collection = ? AND p.hidden_at IS NULL`,\n /** The owner's row regardless of moderation state: update/delete decisions need it. */\n publicationAny: `SELECT p.*, u.display_name AS author_name\n FROM publications p JOIN publishers u ON u.id = p.owner_id WHERE p.id = ? AND p.collection = ?`,\n publicationByKey: `SELECT p.*, u.display_name AS author_name\n FROM publications p JOIN publishers u ON u.id = p.owner_id\n WHERE p.owner_id = ? AND p.collection = ? AND p.client_key = ?`,\n publisher: `SELECT id, display_name, created_at FROM publishers WHERE issuer = ? AND subject = ?`,\n publisherById: `SELECT id FROM publishers WHERE id = ?`,\n removeLike: `DELETE FROM likes WHERE publication_id = ? AND publisher_id = ?\n AND EXISTS (SELECT 1 FROM publications p WHERE p.id = likes.publication_id AND p.hidden_at IS NULL)`,\n removePublication: `DELETE FROM publications WHERE id = ? AND owner_id = ?`,\n removePublicationAny: `DELETE FROM publications WHERE id = ? AND collection = ?`,\n renamePublisher: `UPDATE publishers SET display_name = ? WHERE id = ? AND display_name <> ?`,\n reportById: `SELECT r.id FROM publication_reports r JOIN publications p ON p.id = r.publication_id\n WHERE r.id = ? AND p.collection = ?`,\n request: `SELECT payload_hash, publication_id, expires_at FROM publication_requests\n WHERE owner_id = ? AND collection = ? AND client_key = ?`,\n resolveReport: `UPDATE publication_reports SET resolved_at = ?\n WHERE id = ? AND resolved_at IS NULL AND publication_id IN\n (SELECT id FROM publications WHERE collection = ?)`,\n revokedSubject: `SELECT 1 AS revoked FROM revoked_subjects WHERE issuer = ? AND subject = ? AND expires_at > ?`,\n revokeSubject: `INSERT INTO revoked_subjects(issuer, subject, expires_at) VALUES (?, ?, ?)\n ON CONFLICT(issuer, subject) DO UPDATE SET expires_at = MAX(expires_at, excluded.expires_at)`,\n /** The storage schema stamp the final migration writes; health reads it back. */\n schemaVersion: `SELECT applied FROM archive_schema_meta`,\n unhidePublication: `UPDATE publications SET hidden_at = NULL\n WHERE id = ? AND collection = ? AND hidden_at IS NOT NULL`,\n updatePublication: `UPDATE publications SET\n schema_version = ?, content_version = ?, payload_json = ?, content_hash = ?,\n filter_value = ?, title = ?, tags_json = ?, updated_at = ?, revision = revision + 1\n WHERE id = ? AND owner_id = ? AND revision = ? AND content_hash <> ? AND hidden_at IS NULL`,\n} as const;\n\ninterface ForwardFilter {\n cursor: { createdAt: string; id: string } | null;\n limit: number;\n}\n\nconst forwardWhere = (column: string, id: string, filter: ForwardFilter): BoundSql => ({\n parameters: filter.cursor === null ? [] : [filter.cursor.createdAt, filter.cursor.createdAt, filter.cursor.id],\n sql: filter.cursor === null ? '' : ` AND (${column} < ? OR (${column} = ? AND ${id} < ?))`,\n});\n\nexport function likesPageSql(publisherId: string, filter: ForwardFilter): BoundSql {\n const cursor = forwardWhere('created_at', 'publication_id', filter);\n return {\n parameters: [publisherId, ...cursor.parameters, filter.limit],\n sql: `SELECT publication_id, created_at FROM likes WHERE publisher_id = ?${cursor.sql}\n ORDER BY created_at DESC, publication_id DESC LIMIT ?`,\n };\n}\n\nexport function moderationLogSql(filter: ForwardFilter): BoundSql {\n const cursor = forwardWhere('created_at', 'id', filter);\n return {\n parameters: [...cursor.parameters, filter.limit],\n sql: `SELECT id, action, collection, operator_id, target_id, created_at FROM moderation_actions\n WHERE 1=1${cursor.sql} ORDER BY created_at DESC, id DESC LIMIT ?`,\n };\n}\n\nexport function reportsPageSql(\n scope: { collection: string; resolved: boolean } | { publisherId: string },\n filter: ForwardFilter,\n): BoundSql {\n const cursor = forwardWhere('r.created_at', 'r.id', filter);\n const operator = 'collection' in scope;\n return {\n parameters: [operator ? scope.collection : scope.publisherId, ...cursor.parameters, filter.limit],\n sql: `SELECT r.*, p.collection, p.title AS entry_title, u.display_name AS reporter_name\n FROM publication_reports r JOIN publications p ON p.id = r.publication_id\n JOIN publishers u ON u.id = r.reporter_id\n WHERE ${operator ? `p.collection = ? AND r.resolved_at IS ${scope.resolved ? 'NOT ' : ''}NULL` : 'r.reporter_id = ?'}\n ${cursor.sql} ORDER BY r.created_at DESC, r.id DESC LIMIT ?`,\n };\n}\n\nexport interface PublicationCursor {\n id: string;\n likeCount: number;\n publishedAt: string;\n}\n\nexport interface PublicationFilter {\n /** Restrict to one author's publisher id; omit for all authors. */\n author?: string;\n collection: string;\n cursor?: PublicationCursor;\n direction?: 'next' | 'previous';\n filter?: string;\n limit: number;\n /** Quoted word-prefix terms, normalized before reaching the SQL boundary. */\n searchMatch?: string;\n sort: 'liked' | 'recent';\n tag?: string;\n}\n\nexport interface BoundSql {\n parameters: (string | number)[];\n sql: string;\n}\n\n/** Only fixed SQL fragments are combined; all filters and cursor values remain bindings. */\nexport function listPublicationsSql(filter: PublicationFilter): BoundSql {\n const where: string[] = [];\n const parameters: (string | number)[] = [];\n where.push('p.collection = ?');\n parameters.push(filter.collection);\n // Public lists never surface operator-hidden entries.\n where.push('p.hidden_at IS NULL');\n if (filter.author) {\n where.push('p.owner_id = ?');\n parameters.push(filter.author);\n }\n if (filter.filter) {\n where.push('p.filter_value = ?');\n parameters.push(filter.filter);\n }\n if (filter.tag) {\n where.push(`EXISTS (\n SELECT 1 FROM publication_tags t WHERE t.publication_id = p.id AND t.tag = ?\n )`);\n parameters.push(filter.tag);\n }\n if (filter.searchMatch) {\n where.push(`p.rowid IN (\n SELECT rowid FROM publication_search WHERE publication_search MATCH ?\n )`);\n parameters.push(filter.searchMatch);\n }\n const cursor = filter.cursor;\n const comparison = filter.direction === 'previous' ? '>' : '<';\n if (cursor) {\n if (filter.sort === 'liked') {\n where.push(`(p.like_count ${comparison} ? OR (p.like_count = ? AND (\n p.published_at ${comparison} ? OR (p.published_at = ? AND p.id ${comparison} ?)\n )))`);\n parameters.push(cursor.likeCount, cursor.likeCount, cursor.publishedAt, cursor.publishedAt, cursor.id);\n } else {\n where.push(`(p.published_at ${comparison} ? OR (p.published_at = ? AND p.id ${comparison} ?))`);\n parameters.push(cursor.publishedAt, cursor.publishedAt, cursor.id);\n }\n }\n const direction = filter.direction === 'previous' ? 'ASC' : 'DESC';\n const order =\n filter.sort === 'liked'\n ? `p.like_count ${direction}, p.published_at ${direction}, p.id ${direction}`\n : `p.published_at ${direction}, p.id ${direction}`;\n parameters.push(filter.limit);\n return {\n parameters,\n sql: `SELECT p.*, u.display_name AS author_name\n FROM publications p JOIN publishers u ON u.id = p.owner_id\n ${where.length ? `WHERE ${where.join(' AND ')}` : ''}\n ORDER BY ${order} LIMIT ?`,\n };\n}\n\nexport function likedPublicationsSql(publisherId: string, publicationIds: readonly string[]): BoundSql {\n return {\n parameters: [publisherId, ...publicationIds],\n sql: `SELECT publication_id FROM likes WHERE publisher_id = ?\n AND publication_id IN (${publicationIds.map(() => '?').join(', ') || 'NULL'})`,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sql.cjs","names":[],"sources":["../../src/worker/sql.ts"],"sourcesContent":["/**\n * The runtime SQL the package-owned migration (`packages/archive/migrations/`) must\n * serve. The collection dimension is data (`collection` column, `publication_tags`),\n * never DDL. The migration also stamps the storage schema (`archive_schema_meta`,\n * equal to `ARCHIVE_SCHEMA_VERSION`), which health reads back.\n */\n\n/** SQL is centralized so every mutation's ownership and no-op predicates are reviewable. */\nexport const SQL = {\n auditErasure: `INSERT INTO moderation_actions(id, operator_id, action, collection, target_id, created_at)\n SELECT ?, ?, 'erase-publisher', NULL, id, ? FROM publishers WHERE id = ?`,\n auditHide: `INSERT INTO moderation_actions(id, operator_id, action, collection, target_id, created_at)\n SELECT ?, ?, ?, collection, id, ? FROM publications\n WHERE id = ? AND collection = ? AND (hidden_at IS NOT NULL) <> ?`,\n auditRemoval: `INSERT INTO moderation_actions(id, operator_id, action, collection, target_id, created_at)\n SELECT ?, ?, 'remove', collection, id, ? FROM publications WHERE id = ? AND collection = ?`,\n deletePublisher: `DELETE FROM publishers WHERE id = ?`,\n deleteSubjectPublisher: `DELETE FROM publishers WHERE issuer = ? AND subject = ?`,\n expireRequests: `DELETE FROM publication_requests WHERE expires_at <= ?`,\n expireRevocations: `DELETE FROM revoked_subjects WHERE expires_at <= ?`,\n hidePublication: `UPDATE publications SET hidden_at = ?\n WHERE id = ? AND collection = ? AND hidden_at IS NULL`,\n insertLike: `INSERT INTO likes(publication_id, publisher_id, created_at)\n SELECT id, ?, ? FROM publications WHERE id = ? AND hidden_at IS NULL\n ON CONFLICT(publication_id, publisher_id) DO NOTHING`,\n insertPublication: `INSERT INTO publications(\n id, owner_id, collection, client_key, schema_version, content_version, payload_json,\n content_hash, filter_value, title, tags_json, published_at, updated_at\n ) SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?\n WHERE (SELECT count(*) FROM publications WHERE owner_id = ? AND collection = ?) < ?\n AND NOT EXISTS (\n SELECT 1 FROM publication_requests\n WHERE owner_id = ? AND collection = ? AND client_key = ? AND expires_at > ?\n )\n ON CONFLICT(collection, owner_id, client_key) DO NOTHING`,\n insertPublisher: `INSERT INTO publishers(id, issuer, subject, display_name, created_at)\n SELECT ?, ?, ?, ?, ?\n WHERE NOT EXISTS (SELECT 1 FROM revoked_subjects WHERE issuer = ? AND subject = ? AND expires_at > ?)\n ON CONFLICT(issuer, subject) DO NOTHING`,\n insertReport: `INSERT INTO publication_reports(id, publication_id, reporter_id, reason, created_at)\n SELECT ?, id, ?, ?, ? FROM publications WHERE id = ? AND collection = ? AND hidden_at IS NULL\n ON CONFLICT(publication_id, reporter_id) DO NOTHING`,\n insertRequest: `INSERT INTO publication_requests(\n owner_id, collection, client_key, payload_hash, publication_id, created_at, expires_at\n ) SELECT ?, ?, ?, ?, p.id, ?, ? FROM publications p\n WHERE p.owner_id = ? AND p.collection = ? AND p.client_key = ? AND p.content_hash = ?\n ON CONFLICT(owner_id, collection, client_key) DO NOTHING`,\n likeState: `SELECT p.id, p.like_count, EXISTS(SELECT 1 FROM likes l WHERE l.publication_id = p.id AND l.publisher_id = ?) AS liked FROM publications p WHERE p.id = ? AND p.hidden_at IS NULL`,\n ownPublications: `SELECT p.*, u.display_name AS author_name\n FROM publications p JOIN publishers u ON u.id = p.owner_id\n WHERE p.owner_id = ? AND p.collection = ? ORDER BY p.published_at DESC, p.id DESC LIMIT ?`,\n /** The public detail row: a hidden entry is invisible to everyone but its owner. */\n publication: `SELECT p.*, u.display_name AS author_name\n FROM publications p JOIN publishers u ON u.id = p.owner_id\n WHERE p.id = ? AND p.collection = ? AND p.hidden_at IS NULL`,\n /** The owner's row regardless of moderation state: update/delete decisions need it. */\n publicationAny: `SELECT p.*, u.display_name AS author_name\n FROM publications p JOIN publishers u ON u.id = p.owner_id WHERE p.id = ? AND p.collection = ?`,\n publicationByKey: `SELECT p.*, u.display_name AS author_name\n FROM publications p JOIN publishers u ON u.id = p.owner_id\n WHERE p.owner_id = ? AND p.collection = ? AND p.client_key = ?`,\n publisher: `SELECT id, display_name, created_at FROM publishers WHERE issuer = ? AND subject = ?`,\n publisherById: `SELECT id FROM publishers WHERE id = ?`,\n removeLike: `DELETE FROM likes WHERE publication_id = ? AND publisher_id = ?\n AND EXISTS (SELECT 1 FROM publications p WHERE p.id = likes.publication_id AND p.hidden_at IS NULL)`,\n removePublication: `DELETE FROM publications WHERE id = ? AND owner_id = ?`,\n removePublicationAny: `DELETE FROM publications WHERE id = ? AND collection = ?`,\n renamePublisher: `UPDATE publishers SET display_name = ? WHERE id = ? AND display_name <> ?`,\n reportById: `SELECT r.id FROM publication_reports r JOIN publications p ON p.id = r.publication_id\n WHERE r.id = ? AND p.collection = ?`,\n request: `SELECT payload_hash, publication_id, expires_at FROM publication_requests\n WHERE owner_id = ? AND collection = ? AND client_key = ?`,\n resolveReport: `UPDATE publication_reports SET resolved_at = ?\n WHERE id = ? AND resolved_at IS NULL AND publication_id IN\n (SELECT id FROM publications WHERE collection = ?)`,\n revokedSubject: `SELECT 1 AS revoked FROM revoked_subjects WHERE issuer = ? AND subject = ? AND expires_at > ?`,\n revokeSubject: `INSERT INTO revoked_subjects(issuer, subject, expires_at) VALUES (?, ?, ?)\n ON CONFLICT(issuer, subject) DO UPDATE SET expires_at = MAX(expires_at, excluded.expires_at)`,\n /** The storage schema stamp the final migration writes; health reads it back. */\n schemaVersion: `SELECT applied FROM archive_schema_meta`,\n unhidePublication: `UPDATE publications SET hidden_at = NULL\n WHERE id = ? AND collection = ? AND hidden_at IS NOT NULL`,\n updatePublication: `UPDATE publications SET\n schema_version = ?, content_version = ?, payload_json = ?, content_hash = ?,\n filter_value = ?, title = ?, tags_json = ?, updated_at = ?, revision = revision + 1\n WHERE id = ? AND owner_id = ? AND revision = ? AND content_hash <> ? AND hidden_at IS NULL`,\n} as const;\n\ninterface ForwardFilter {\n cursor: { createdAt: string; id: string } | null;\n limit: number;\n}\n\nconst forwardWhere = (column: string, id: string, filter: ForwardFilter): BoundSql => ({\n parameters: filter.cursor === null ? [] : [filter.cursor.createdAt, filter.cursor.createdAt, filter.cursor.id],\n sql: filter.cursor === null ? '' : ` AND (${column} < ? OR (${column} = ? AND ${id} < ?))`,\n});\n\nexport function likesPageSql(publisherId: string, filter: ForwardFilter): BoundSql {\n const cursor = forwardWhere('created_at', 'publication_id', filter);\n return {\n parameters: [publisherId, ...cursor.parameters, filter.limit],\n sql: `SELECT publication_id, created_at FROM likes WHERE publisher_id = ?${cursor.sql}\n ORDER BY created_at DESC, publication_id DESC LIMIT ?`,\n };\n}\n\nexport function moderationLogSql(filter: ForwardFilter): BoundSql {\n const cursor = forwardWhere('created_at', 'id', filter);\n return {\n parameters: [...cursor.parameters, filter.limit],\n sql: `SELECT id, action, collection, operator_id, target_id, created_at FROM moderation_actions\n WHERE 1=1${cursor.sql} ORDER BY created_at DESC, id DESC LIMIT ?`,\n };\n}\n\nexport function reportsPageSql(\n scope: { collection: string; resolved: boolean } | { publisherId: string },\n filter: ForwardFilter,\n): BoundSql {\n const cursor = forwardWhere('r.created_at', 'r.id', filter);\n const operator = 'collection' in scope;\n return {\n parameters: [operator ? scope.collection : scope.publisherId, ...cursor.parameters, filter.limit],\n sql: `SELECT r.*, p.collection, p.title AS entry_title, u.display_name AS reporter_name\n FROM publication_reports r JOIN publications p ON p.id = r.publication_id\n JOIN publishers u ON u.id = r.reporter_id\n WHERE ${operator ? `p.collection = ? AND r.resolved_at IS ${scope.resolved ? 'NOT ' : ''}NULL` : 'r.reporter_id = ?'}\n ${cursor.sql} ORDER BY r.created_at DESC, r.id DESC LIMIT ?`,\n };\n}\n\n/** The entry-list keyset boundary: the shared cursor with its sort field resolved. */\nexport interface PublicationCursor {\n createdAt: string;\n id: string;\n likeCount: number;\n}\n\nexport interface PublicationFilter {\n /** Restrict to one author's publisher id; omit for all authors. */\n author?: string;\n collection: string;\n /** Forward keyset boundary: `createdAt`/`id` plus the liked sort's `likeCount`. */\n cursor?: PublicationCursor;\n filter?: string;\n limit: number;\n /** Quoted word-prefix terms, normalized before reaching the SQL boundary. */\n searchMatch?: string;\n sort: 'liked' | 'recent';\n tag?: string;\n}\n\nexport interface BoundSql {\n parameters: (string | number)[];\n sql: string;\n}\n\n/** Only fixed SQL fragments are combined; all filters and cursor values remain bindings. */\nexport function listPublicationsSql(filter: PublicationFilter): BoundSql {\n const where: string[] = [];\n const parameters: (string | number)[] = [];\n where.push('p.collection = ?');\n parameters.push(filter.collection);\n // Public lists never surface operator-hidden entries.\n where.push('p.hidden_at IS NULL');\n if (filter.author) {\n where.push('p.owner_id = ?');\n parameters.push(filter.author);\n }\n if (filter.filter) {\n where.push('p.filter_value = ?');\n parameters.push(filter.filter);\n }\n if (filter.tag) {\n where.push(`EXISTS (\n SELECT 1 FROM publication_tags t WHERE t.publication_id = p.id AND t.tag = ?\n )`);\n parameters.push(filter.tag);\n }\n if (filter.searchMatch) {\n where.push(`p.rowid IN (\n SELECT rowid FROM publication_search WHERE publication_search MATCH ?\n )`);\n parameters.push(filter.searchMatch);\n }\n // Forward-only keyset walk: every list orders DESC and the cursor is the last\n // row of the previous page.\n const cursor = filter.cursor;\n if (cursor) {\n if (filter.sort === 'liked') {\n where.push(`(p.like_count < ? OR (p.like_count = ? AND (\n p.published_at < ? OR (p.published_at = ? AND p.id < ?)\n )))`);\n parameters.push(cursor.likeCount, cursor.likeCount, cursor.createdAt, cursor.createdAt, cursor.id);\n } else {\n where.push('(p.published_at < ? OR (p.published_at = ? AND p.id < ?))');\n parameters.push(cursor.createdAt, cursor.createdAt, cursor.id);\n }\n }\n const order =\n filter.sort === 'liked' ? 'p.like_count DESC, p.published_at DESC, p.id DESC' : 'p.published_at DESC, p.id DESC';\n parameters.push(filter.limit);\n return {\n parameters,\n sql: `SELECT p.*, u.display_name AS author_name\n FROM publications p JOIN publishers u ON u.id = p.owner_id\n ${where.length ? `WHERE ${where.join(' AND ')}` : ''}\n ORDER BY ${order} LIMIT ?`,\n };\n}\n\nexport function likedPublicationsSql(publisherId: string, publicationIds: readonly string[]): BoundSql {\n return {\n parameters: [publisherId, ...publicationIds],\n sql: `SELECT publication_id FROM likes WHERE publisher_id = ?\n AND publication_id IN (${publicationIds.map(() => '?').join(', ') || 'NULL'})`,\n };\n}\n"],"mappings":"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"}
@@ -57,17 +57,18 @@ export declare function reportsPageSql(scope: {
57
57
  } | {
58
58
  publisherId: string;
59
59
  }, filter: ForwardFilter): BoundSql;
60
+ /** The entry-list keyset boundary: the shared cursor with its sort field resolved. */
60
61
  export interface PublicationCursor {
62
+ createdAt: string;
61
63
  id: string;
62
64
  likeCount: number;
63
- publishedAt: string;
64
65
  }
65
66
  export interface PublicationFilter {
66
67
  /** Restrict to one author's publisher id; omit for all authors. */
67
68
  author?: string;
68
69
  collection: string;
70
+ /** Forward keyset boundary: `createdAt`/`id` plus the liked sort's `likeCount`. */
69
71
  cursor?: PublicationCursor;
70
- direction?: 'next' | 'previous';
71
72
  filter?: string;
72
73
  limit: number;
73
74
  /** Quoted word-prefix terms, normalized before reaching the SQL boundary. */
@@ -1 +1 @@
1
- {"version":3,"file":"sql.d.ts","sourceRoot":"","sources":["../../src/worker/sql.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,4FAA4F;AAC5F,eAAO,MAAM,GAAG;;;;;;;;;;;;;;;;IA2Cd,oFAAoF;;IAIpF,uFAAuF;;;;;;;;;;;;;;IAuBvF,iFAAiF;;;;CAQzE,CAAC;AAEX,UAAU,aAAa;IACrB,MAAM,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACjD,KAAK,EAAE,MAAM,CAAC;CACf;AAOD,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,QAAQ,CAOjF;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,aAAa,GAAG,QAAQ,CAOhE;AAED,wBAAgB,cAAc,CAC5B,KAAK,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,WAAW,EAAE,MAAM,CAAA;CAAE,EAC1E,MAAM,EAAE,aAAa,GACpB,QAAQ,CAWV;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,OAAO,GAAG,QAAQ,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,QAAQ;IACvB,UAAU,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IAChC,GAAG,EAAE,MAAM,CAAC;CACb;AAED,4FAA4F;AAC5F,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,iBAAiB,GAAG,QAAQ,CAqDvE;AAED,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,cAAc,EAAE,SAAS,MAAM,EAAE,GAAG,QAAQ,CAMrG"}
1
+ {"version":3,"file":"sql.d.ts","sourceRoot":"","sources":["../../src/worker/sql.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,4FAA4F;AAC5F,eAAO,MAAM,GAAG;;;;;;;;;;;;;;;;IA2Cd,oFAAoF;;IAIpF,uFAAuF;;;;;;;;;;;;;;IAuBvF,iFAAiF;;;;CAQzE,CAAC;AAEX,UAAU,aAAa;IACrB,MAAM,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACjD,KAAK,EAAE,MAAM,CAAC;CACf;AAOD,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,QAAQ,CAOjF;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,aAAa,GAAG,QAAQ,CAOhE;AAED,wBAAgB,cAAc,CAC5B,KAAK,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,WAAW,EAAE,MAAM,CAAA;CAAE,EAC1E,MAAM,EAAE,aAAa,GACpB,QAAQ,CAWV;AAED,sFAAsF;AACtF,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,mFAAmF;IACnF,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,OAAO,GAAG,QAAQ,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,QAAQ;IACvB,UAAU,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IAChC,GAAG,EAAE,MAAM,CAAC;CACb;AAED,4FAA4F;AAC5F,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,iBAAiB,GAAG,QAAQ,CAmDvE;AAED,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,cAAc,EAAE,SAAS,MAAM,EAAE,GAAG,QAAQ,CAMrG"}
@@ -50,11 +50,11 @@ var e={auditErasure:`INSERT INTO moderation_actions(id, operator_id, action, col
50
50
  SELECT 1 FROM publication_tags t WHERE t.publication_id = p.id AND t.tag = ?
51
51
  )`),n.push(e.tag)),e.searchMatch&&(t.push(`p.rowid IN (
52
52
  SELECT rowid FROM publication_search WHERE publication_search MATCH ?
53
- )`),n.push(e.searchMatch));let r=e.cursor,i=e.direction===`previous`?`>`:`<`;r&&(e.sort===`liked`?(t.push(`(p.like_count ${i} ? OR (p.like_count = ? AND (
54
- p.published_at ${i} ? OR (p.published_at = ? AND p.id ${i} ?)
55
- )))`),n.push(r.likeCount,r.likeCount,r.publishedAt,r.publishedAt,r.id)):(t.push(`(p.published_at ${i} ? OR (p.published_at = ? AND p.id ${i} ?))`),n.push(r.publishedAt,r.publishedAt,r.id)));let a=e.direction===`previous`?`ASC`:`DESC`,o=e.sort===`liked`?`p.like_count ${a}, p.published_at ${a}, p.id ${a}`:`p.published_at ${a}, p.id ${a}`;return n.push(e.limit),{parameters:n,sql:`SELECT p.*, u.display_name AS author_name
53
+ )`),n.push(e.searchMatch));let r=e.cursor;r&&(e.sort===`liked`?(t.push(`(p.like_count < ? OR (p.like_count = ? AND (
54
+ p.published_at < ? OR (p.published_at = ? AND p.id < ?)
55
+ )))`),n.push(r.likeCount,r.likeCount,r.createdAt,r.createdAt,r.id)):(t.push(`(p.published_at < ? OR (p.published_at = ? AND p.id < ?))`),n.push(r.createdAt,r.createdAt,r.id)));let i=e.sort===`liked`?`p.like_count DESC, p.published_at DESC, p.id DESC`:`p.published_at DESC, p.id DESC`;return n.push(e.limit),{parameters:n,sql:`SELECT p.*, u.display_name AS author_name
56
56
  FROM publications p JOIN publishers u ON u.id = p.owner_id
57
57
  ${t.length?`WHERE ${t.join(` AND `)}`:``}
58
- ORDER BY ${o} LIMIT ?`}}function likedPublicationsSql(e,t){return{parameters:[e,...t],sql:`SELECT publication_id FROM likes WHERE publisher_id = ?
58
+ ORDER BY ${i} LIMIT ?`}}function likedPublicationsSql(e,t){return{parameters:[e,...t],sql:`SELECT publication_id FROM likes WHERE publisher_id = ?
59
59
  AND publication_id IN (${t.map(()=>`?`).join(`, `)||`NULL`})`}}export{e as SQL,likedPublicationsSql,likesPageSql,listPublicationsSql,moderationLogSql,reportsPageSql};
60
60
  //# sourceMappingURL=sql.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sql.js","names":[],"sources":["../../src/worker/sql.ts"],"sourcesContent":["/**\n * The runtime SQL the package-owned migration (`packages/archive/migrations/`) must\n * serve. The collection dimension is data (`collection` column, `publication_tags`),\n * never DDL. The migration also stamps the storage schema (`archive_schema_meta`,\n * equal to `ARCHIVE_SCHEMA_VERSION`), which health reads back.\n */\n\n/** SQL is centralized so every mutation's ownership and no-op predicates are reviewable. */\nexport const SQL = {\n auditErasure: `INSERT INTO moderation_actions(id, operator_id, action, collection, target_id, created_at)\n SELECT ?, ?, 'erase-publisher', NULL, id, ? FROM publishers WHERE id = ?`,\n auditHide: `INSERT INTO moderation_actions(id, operator_id, action, collection, target_id, created_at)\n SELECT ?, ?, ?, collection, id, ? FROM publications\n WHERE id = ? AND collection = ? AND (hidden_at IS NOT NULL) <> ?`,\n auditRemoval: `INSERT INTO moderation_actions(id, operator_id, action, collection, target_id, created_at)\n SELECT ?, ?, 'remove', collection, id, ? FROM publications WHERE id = ? AND collection = ?`,\n deletePublisher: `DELETE FROM publishers WHERE id = ?`,\n deleteSubjectPublisher: `DELETE FROM publishers WHERE issuer = ? AND subject = ?`,\n expireRequests: `DELETE FROM publication_requests WHERE expires_at <= ?`,\n expireRevocations: `DELETE FROM revoked_subjects WHERE expires_at <= ?`,\n hidePublication: `UPDATE publications SET hidden_at = ?\n WHERE id = ? AND collection = ? AND hidden_at IS NULL`,\n insertLike: `INSERT INTO likes(publication_id, publisher_id, created_at)\n SELECT id, ?, ? FROM publications WHERE id = ? AND hidden_at IS NULL\n ON CONFLICT(publication_id, publisher_id) DO NOTHING`,\n insertPublication: `INSERT INTO publications(\n id, owner_id, collection, client_key, schema_version, content_version, payload_json,\n content_hash, filter_value, title, tags_json, published_at, updated_at\n ) SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?\n WHERE (SELECT count(*) FROM publications WHERE owner_id = ? AND collection = ?) < ?\n AND NOT EXISTS (\n SELECT 1 FROM publication_requests\n WHERE owner_id = ? AND collection = ? AND client_key = ? AND expires_at > ?\n )\n ON CONFLICT(collection, owner_id, client_key) DO NOTHING`,\n insertPublisher: `INSERT INTO publishers(id, issuer, subject, display_name, created_at)\n SELECT ?, ?, ?, ?, ?\n WHERE NOT EXISTS (SELECT 1 FROM revoked_subjects WHERE issuer = ? AND subject = ? AND expires_at > ?)\n ON CONFLICT(issuer, subject) DO NOTHING`,\n insertReport: `INSERT INTO publication_reports(id, publication_id, reporter_id, reason, created_at)\n SELECT ?, id, ?, ?, ? FROM publications WHERE id = ? AND collection = ? AND hidden_at IS NULL\n ON CONFLICT(publication_id, reporter_id) DO NOTHING`,\n insertRequest: `INSERT INTO publication_requests(\n owner_id, collection, client_key, payload_hash, publication_id, created_at, expires_at\n ) SELECT ?, ?, ?, ?, p.id, ?, ? FROM publications p\n WHERE p.owner_id = ? AND p.collection = ? AND p.client_key = ? AND p.content_hash = ?\n ON CONFLICT(owner_id, collection, client_key) DO NOTHING`,\n likeState: `SELECT p.id, p.like_count, EXISTS(SELECT 1 FROM likes l WHERE l.publication_id = p.id AND l.publisher_id = ?) AS liked FROM publications p WHERE p.id = ? AND p.hidden_at IS NULL`,\n ownPublications: `SELECT p.*, u.display_name AS author_name\n FROM publications p JOIN publishers u ON u.id = p.owner_id\n WHERE p.owner_id = ? AND p.collection = ? ORDER BY p.published_at DESC, p.id DESC LIMIT ?`,\n /** The public detail row: a hidden entry is invisible to everyone but its owner. */\n publication: `SELECT p.*, u.display_name AS author_name\n FROM publications p JOIN publishers u ON u.id = p.owner_id\n WHERE p.id = ? AND p.collection = ? AND p.hidden_at IS NULL`,\n /** The owner's row regardless of moderation state: update/delete decisions need it. */\n publicationAny: `SELECT p.*, u.display_name AS author_name\n FROM publications p JOIN publishers u ON u.id = p.owner_id WHERE p.id = ? AND p.collection = ?`,\n publicationByKey: `SELECT p.*, u.display_name AS author_name\n FROM publications p JOIN publishers u ON u.id = p.owner_id\n WHERE p.owner_id = ? AND p.collection = ? AND p.client_key = ?`,\n publisher: `SELECT id, display_name, created_at FROM publishers WHERE issuer = ? AND subject = ?`,\n publisherById: `SELECT id FROM publishers WHERE id = ?`,\n removeLike: `DELETE FROM likes WHERE publication_id = ? AND publisher_id = ?\n AND EXISTS (SELECT 1 FROM publications p WHERE p.id = likes.publication_id AND p.hidden_at IS NULL)`,\n removePublication: `DELETE FROM publications WHERE id = ? AND owner_id = ?`,\n removePublicationAny: `DELETE FROM publications WHERE id = ? AND collection = ?`,\n renamePublisher: `UPDATE publishers SET display_name = ? WHERE id = ? AND display_name <> ?`,\n reportById: `SELECT r.id FROM publication_reports r JOIN publications p ON p.id = r.publication_id\n WHERE r.id = ? AND p.collection = ?`,\n request: `SELECT payload_hash, publication_id, expires_at FROM publication_requests\n WHERE owner_id = ? AND collection = ? AND client_key = ?`,\n resolveReport: `UPDATE publication_reports SET resolved_at = ?\n WHERE id = ? AND resolved_at IS NULL AND publication_id IN\n (SELECT id FROM publications WHERE collection = ?)`,\n revokedSubject: `SELECT 1 AS revoked FROM revoked_subjects WHERE issuer = ? AND subject = ? AND expires_at > ?`,\n revokeSubject: `INSERT INTO revoked_subjects(issuer, subject, expires_at) VALUES (?, ?, ?)\n ON CONFLICT(issuer, subject) DO UPDATE SET expires_at = MAX(expires_at, excluded.expires_at)`,\n /** The storage schema stamp the final migration writes; health reads it back. */\n schemaVersion: `SELECT applied FROM archive_schema_meta`,\n unhidePublication: `UPDATE publications SET hidden_at = NULL\n WHERE id = ? AND collection = ? AND hidden_at IS NOT NULL`,\n updatePublication: `UPDATE publications SET\n schema_version = ?, content_version = ?, payload_json = ?, content_hash = ?,\n filter_value = ?, title = ?, tags_json = ?, updated_at = ?, revision = revision + 1\n WHERE id = ? AND owner_id = ? AND revision = ? AND content_hash <> ? AND hidden_at IS NULL`,\n} as const;\n\ninterface ForwardFilter {\n cursor: { createdAt: string; id: string } | null;\n limit: number;\n}\n\nconst forwardWhere = (column: string, id: string, filter: ForwardFilter): BoundSql => ({\n parameters: filter.cursor === null ? [] : [filter.cursor.createdAt, filter.cursor.createdAt, filter.cursor.id],\n sql: filter.cursor === null ? '' : ` AND (${column} < ? OR (${column} = ? AND ${id} < ?))`,\n});\n\nexport function likesPageSql(publisherId: string, filter: ForwardFilter): BoundSql {\n const cursor = forwardWhere('created_at', 'publication_id', filter);\n return {\n parameters: [publisherId, ...cursor.parameters, filter.limit],\n sql: `SELECT publication_id, created_at FROM likes WHERE publisher_id = ?${cursor.sql}\n ORDER BY created_at DESC, publication_id DESC LIMIT ?`,\n };\n}\n\nexport function moderationLogSql(filter: ForwardFilter): BoundSql {\n const cursor = forwardWhere('created_at', 'id', filter);\n return {\n parameters: [...cursor.parameters, filter.limit],\n sql: `SELECT id, action, collection, operator_id, target_id, created_at FROM moderation_actions\n WHERE 1=1${cursor.sql} ORDER BY created_at DESC, id DESC LIMIT ?`,\n };\n}\n\nexport function reportsPageSql(\n scope: { collection: string; resolved: boolean } | { publisherId: string },\n filter: ForwardFilter,\n): BoundSql {\n const cursor = forwardWhere('r.created_at', 'r.id', filter);\n const operator = 'collection' in scope;\n return {\n parameters: [operator ? scope.collection : scope.publisherId, ...cursor.parameters, filter.limit],\n sql: `SELECT r.*, p.collection, p.title AS entry_title, u.display_name AS reporter_name\n FROM publication_reports r JOIN publications p ON p.id = r.publication_id\n JOIN publishers u ON u.id = r.reporter_id\n WHERE ${operator ? `p.collection = ? AND r.resolved_at IS ${scope.resolved ? 'NOT ' : ''}NULL` : 'r.reporter_id = ?'}\n ${cursor.sql} ORDER BY r.created_at DESC, r.id DESC LIMIT ?`,\n };\n}\n\nexport interface PublicationCursor {\n id: string;\n likeCount: number;\n publishedAt: string;\n}\n\nexport interface PublicationFilter {\n /** Restrict to one author's publisher id; omit for all authors. */\n author?: string;\n collection: string;\n cursor?: PublicationCursor;\n direction?: 'next' | 'previous';\n filter?: string;\n limit: number;\n /** Quoted word-prefix terms, normalized before reaching the SQL boundary. */\n searchMatch?: string;\n sort: 'liked' | 'recent';\n tag?: string;\n}\n\nexport interface BoundSql {\n parameters: (string | number)[];\n sql: string;\n}\n\n/** Only fixed SQL fragments are combined; all filters and cursor values remain bindings. */\nexport function listPublicationsSql(filter: PublicationFilter): BoundSql {\n const where: string[] = [];\n const parameters: (string | number)[] = [];\n where.push('p.collection = ?');\n parameters.push(filter.collection);\n // Public lists never surface operator-hidden entries.\n where.push('p.hidden_at IS NULL');\n if (filter.author) {\n where.push('p.owner_id = ?');\n parameters.push(filter.author);\n }\n if (filter.filter) {\n where.push('p.filter_value = ?');\n parameters.push(filter.filter);\n }\n if (filter.tag) {\n where.push(`EXISTS (\n SELECT 1 FROM publication_tags t WHERE t.publication_id = p.id AND t.tag = ?\n )`);\n parameters.push(filter.tag);\n }\n if (filter.searchMatch) {\n where.push(`p.rowid IN (\n SELECT rowid FROM publication_search WHERE publication_search MATCH ?\n )`);\n parameters.push(filter.searchMatch);\n }\n const cursor = filter.cursor;\n const comparison = filter.direction === 'previous' ? '>' : '<';\n if (cursor) {\n if (filter.sort === 'liked') {\n where.push(`(p.like_count ${comparison} ? OR (p.like_count = ? AND (\n p.published_at ${comparison} ? OR (p.published_at = ? AND p.id ${comparison} ?)\n )))`);\n parameters.push(cursor.likeCount, cursor.likeCount, cursor.publishedAt, cursor.publishedAt, cursor.id);\n } else {\n where.push(`(p.published_at ${comparison} ? OR (p.published_at = ? AND p.id ${comparison} ?))`);\n parameters.push(cursor.publishedAt, cursor.publishedAt, cursor.id);\n }\n }\n const direction = filter.direction === 'previous' ? 'ASC' : 'DESC';\n const order =\n filter.sort === 'liked'\n ? `p.like_count ${direction}, p.published_at ${direction}, p.id ${direction}`\n : `p.published_at ${direction}, p.id ${direction}`;\n parameters.push(filter.limit);\n return {\n parameters,\n sql: `SELECT p.*, u.display_name AS author_name\n FROM publications p JOIN publishers u ON u.id = p.owner_id\n ${where.length ? `WHERE ${where.join(' AND ')}` : ''}\n ORDER BY ${order} LIMIT ?`,\n };\n}\n\nexport function likedPublicationsSql(publisherId: string, publicationIds: readonly string[]): BoundSql {\n return {\n parameters: [publisherId, ...publicationIds],\n sql: `SELECT publication_id FROM likes WHERE publisher_id = ?\n AND publication_id IN (${publicationIds.map(() => '?').join(', ') || 'NULL'})`,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sql.js","names":[],"sources":["../../src/worker/sql.ts"],"sourcesContent":["/**\n * The runtime SQL the package-owned migration (`packages/archive/migrations/`) must\n * serve. The collection dimension is data (`collection` column, `publication_tags`),\n * never DDL. The migration also stamps the storage schema (`archive_schema_meta`,\n * equal to `ARCHIVE_SCHEMA_VERSION`), which health reads back.\n */\n\n/** SQL is centralized so every mutation's ownership and no-op predicates are reviewable. */\nexport const SQL = {\n auditErasure: `INSERT INTO moderation_actions(id, operator_id, action, collection, target_id, created_at)\n SELECT ?, ?, 'erase-publisher', NULL, id, ? FROM publishers WHERE id = ?`,\n auditHide: `INSERT INTO moderation_actions(id, operator_id, action, collection, target_id, created_at)\n SELECT ?, ?, ?, collection, id, ? FROM publications\n WHERE id = ? AND collection = ? AND (hidden_at IS NOT NULL) <> ?`,\n auditRemoval: `INSERT INTO moderation_actions(id, operator_id, action, collection, target_id, created_at)\n SELECT ?, ?, 'remove', collection, id, ? FROM publications WHERE id = ? AND collection = ?`,\n deletePublisher: `DELETE FROM publishers WHERE id = ?`,\n deleteSubjectPublisher: `DELETE FROM publishers WHERE issuer = ? AND subject = ?`,\n expireRequests: `DELETE FROM publication_requests WHERE expires_at <= ?`,\n expireRevocations: `DELETE FROM revoked_subjects WHERE expires_at <= ?`,\n hidePublication: `UPDATE publications SET hidden_at = ?\n WHERE id = ? AND collection = ? AND hidden_at IS NULL`,\n insertLike: `INSERT INTO likes(publication_id, publisher_id, created_at)\n SELECT id, ?, ? FROM publications WHERE id = ? AND hidden_at IS NULL\n ON CONFLICT(publication_id, publisher_id) DO NOTHING`,\n insertPublication: `INSERT INTO publications(\n id, owner_id, collection, client_key, schema_version, content_version, payload_json,\n content_hash, filter_value, title, tags_json, published_at, updated_at\n ) SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?\n WHERE (SELECT count(*) FROM publications WHERE owner_id = ? AND collection = ?) < ?\n AND NOT EXISTS (\n SELECT 1 FROM publication_requests\n WHERE owner_id = ? AND collection = ? AND client_key = ? AND expires_at > ?\n )\n ON CONFLICT(collection, owner_id, client_key) DO NOTHING`,\n insertPublisher: `INSERT INTO publishers(id, issuer, subject, display_name, created_at)\n SELECT ?, ?, ?, ?, ?\n WHERE NOT EXISTS (SELECT 1 FROM revoked_subjects WHERE issuer = ? AND subject = ? AND expires_at > ?)\n ON CONFLICT(issuer, subject) DO NOTHING`,\n insertReport: `INSERT INTO publication_reports(id, publication_id, reporter_id, reason, created_at)\n SELECT ?, id, ?, ?, ? FROM publications WHERE id = ? AND collection = ? AND hidden_at IS NULL\n ON CONFLICT(publication_id, reporter_id) DO NOTHING`,\n insertRequest: `INSERT INTO publication_requests(\n owner_id, collection, client_key, payload_hash, publication_id, created_at, expires_at\n ) SELECT ?, ?, ?, ?, p.id, ?, ? FROM publications p\n WHERE p.owner_id = ? AND p.collection = ? AND p.client_key = ? AND p.content_hash = ?\n ON CONFLICT(owner_id, collection, client_key) DO NOTHING`,\n likeState: `SELECT p.id, p.like_count, EXISTS(SELECT 1 FROM likes l WHERE l.publication_id = p.id AND l.publisher_id = ?) AS liked FROM publications p WHERE p.id = ? AND p.hidden_at IS NULL`,\n ownPublications: `SELECT p.*, u.display_name AS author_name\n FROM publications p JOIN publishers u ON u.id = p.owner_id\n WHERE p.owner_id = ? AND p.collection = ? ORDER BY p.published_at DESC, p.id DESC LIMIT ?`,\n /** The public detail row: a hidden entry is invisible to everyone but its owner. */\n publication: `SELECT p.*, u.display_name AS author_name\n FROM publications p JOIN publishers u ON u.id = p.owner_id\n WHERE p.id = ? AND p.collection = ? AND p.hidden_at IS NULL`,\n /** The owner's row regardless of moderation state: update/delete decisions need it. */\n publicationAny: `SELECT p.*, u.display_name AS author_name\n FROM publications p JOIN publishers u ON u.id = p.owner_id WHERE p.id = ? AND p.collection = ?`,\n publicationByKey: `SELECT p.*, u.display_name AS author_name\n FROM publications p JOIN publishers u ON u.id = p.owner_id\n WHERE p.owner_id = ? AND p.collection = ? AND p.client_key = ?`,\n publisher: `SELECT id, display_name, created_at FROM publishers WHERE issuer = ? AND subject = ?`,\n publisherById: `SELECT id FROM publishers WHERE id = ?`,\n removeLike: `DELETE FROM likes WHERE publication_id = ? AND publisher_id = ?\n AND EXISTS (SELECT 1 FROM publications p WHERE p.id = likes.publication_id AND p.hidden_at IS NULL)`,\n removePublication: `DELETE FROM publications WHERE id = ? AND owner_id = ?`,\n removePublicationAny: `DELETE FROM publications WHERE id = ? AND collection = ?`,\n renamePublisher: `UPDATE publishers SET display_name = ? WHERE id = ? AND display_name <> ?`,\n reportById: `SELECT r.id FROM publication_reports r JOIN publications p ON p.id = r.publication_id\n WHERE r.id = ? AND p.collection = ?`,\n request: `SELECT payload_hash, publication_id, expires_at FROM publication_requests\n WHERE owner_id = ? AND collection = ? AND client_key = ?`,\n resolveReport: `UPDATE publication_reports SET resolved_at = ?\n WHERE id = ? AND resolved_at IS NULL AND publication_id IN\n (SELECT id FROM publications WHERE collection = ?)`,\n revokedSubject: `SELECT 1 AS revoked FROM revoked_subjects WHERE issuer = ? AND subject = ? AND expires_at > ?`,\n revokeSubject: `INSERT INTO revoked_subjects(issuer, subject, expires_at) VALUES (?, ?, ?)\n ON CONFLICT(issuer, subject) DO UPDATE SET expires_at = MAX(expires_at, excluded.expires_at)`,\n /** The storage schema stamp the final migration writes; health reads it back. */\n schemaVersion: `SELECT applied FROM archive_schema_meta`,\n unhidePublication: `UPDATE publications SET hidden_at = NULL\n WHERE id = ? AND collection = ? AND hidden_at IS NOT NULL`,\n updatePublication: `UPDATE publications SET\n schema_version = ?, content_version = ?, payload_json = ?, content_hash = ?,\n filter_value = ?, title = ?, tags_json = ?, updated_at = ?, revision = revision + 1\n WHERE id = ? AND owner_id = ? AND revision = ? AND content_hash <> ? AND hidden_at IS NULL`,\n} as const;\n\ninterface ForwardFilter {\n cursor: { createdAt: string; id: string } | null;\n limit: number;\n}\n\nconst forwardWhere = (column: string, id: string, filter: ForwardFilter): BoundSql => ({\n parameters: filter.cursor === null ? [] : [filter.cursor.createdAt, filter.cursor.createdAt, filter.cursor.id],\n sql: filter.cursor === null ? '' : ` AND (${column} < ? OR (${column} = ? AND ${id} < ?))`,\n});\n\nexport function likesPageSql(publisherId: string, filter: ForwardFilter): BoundSql {\n const cursor = forwardWhere('created_at', 'publication_id', filter);\n return {\n parameters: [publisherId, ...cursor.parameters, filter.limit],\n sql: `SELECT publication_id, created_at FROM likes WHERE publisher_id = ?${cursor.sql}\n ORDER BY created_at DESC, publication_id DESC LIMIT ?`,\n };\n}\n\nexport function moderationLogSql(filter: ForwardFilter): BoundSql {\n const cursor = forwardWhere('created_at', 'id', filter);\n return {\n parameters: [...cursor.parameters, filter.limit],\n sql: `SELECT id, action, collection, operator_id, target_id, created_at FROM moderation_actions\n WHERE 1=1${cursor.sql} ORDER BY created_at DESC, id DESC LIMIT ?`,\n };\n}\n\nexport function reportsPageSql(\n scope: { collection: string; resolved: boolean } | { publisherId: string },\n filter: ForwardFilter,\n): BoundSql {\n const cursor = forwardWhere('r.created_at', 'r.id', filter);\n const operator = 'collection' in scope;\n return {\n parameters: [operator ? scope.collection : scope.publisherId, ...cursor.parameters, filter.limit],\n sql: `SELECT r.*, p.collection, p.title AS entry_title, u.display_name AS reporter_name\n FROM publication_reports r JOIN publications p ON p.id = r.publication_id\n JOIN publishers u ON u.id = r.reporter_id\n WHERE ${operator ? `p.collection = ? AND r.resolved_at IS ${scope.resolved ? 'NOT ' : ''}NULL` : 'r.reporter_id = ?'}\n ${cursor.sql} ORDER BY r.created_at DESC, r.id DESC LIMIT ?`,\n };\n}\n\n/** The entry-list keyset boundary: the shared cursor with its sort field resolved. */\nexport interface PublicationCursor {\n createdAt: string;\n id: string;\n likeCount: number;\n}\n\nexport interface PublicationFilter {\n /** Restrict to one author's publisher id; omit for all authors. */\n author?: string;\n collection: string;\n /** Forward keyset boundary: `createdAt`/`id` plus the liked sort's `likeCount`. */\n cursor?: PublicationCursor;\n filter?: string;\n limit: number;\n /** Quoted word-prefix terms, normalized before reaching the SQL boundary. */\n searchMatch?: string;\n sort: 'liked' | 'recent';\n tag?: string;\n}\n\nexport interface BoundSql {\n parameters: (string | number)[];\n sql: string;\n}\n\n/** Only fixed SQL fragments are combined; all filters and cursor values remain bindings. */\nexport function listPublicationsSql(filter: PublicationFilter): BoundSql {\n const where: string[] = [];\n const parameters: (string | number)[] = [];\n where.push('p.collection = ?');\n parameters.push(filter.collection);\n // Public lists never surface operator-hidden entries.\n where.push('p.hidden_at IS NULL');\n if (filter.author) {\n where.push('p.owner_id = ?');\n parameters.push(filter.author);\n }\n if (filter.filter) {\n where.push('p.filter_value = ?');\n parameters.push(filter.filter);\n }\n if (filter.tag) {\n where.push(`EXISTS (\n SELECT 1 FROM publication_tags t WHERE t.publication_id = p.id AND t.tag = ?\n )`);\n parameters.push(filter.tag);\n }\n if (filter.searchMatch) {\n where.push(`p.rowid IN (\n SELECT rowid FROM publication_search WHERE publication_search MATCH ?\n )`);\n parameters.push(filter.searchMatch);\n }\n // Forward-only keyset walk: every list orders DESC and the cursor is the last\n // row of the previous page.\n const cursor = filter.cursor;\n if (cursor) {\n if (filter.sort === 'liked') {\n where.push(`(p.like_count < ? OR (p.like_count = ? AND (\n p.published_at < ? OR (p.published_at = ? AND p.id < ?)\n )))`);\n parameters.push(cursor.likeCount, cursor.likeCount, cursor.createdAt, cursor.createdAt, cursor.id);\n } else {\n where.push('(p.published_at < ? OR (p.published_at = ? AND p.id < ?))');\n parameters.push(cursor.createdAt, cursor.createdAt, cursor.id);\n }\n }\n const order =\n filter.sort === 'liked' ? 'p.like_count DESC, p.published_at DESC, p.id DESC' : 'p.published_at DESC, p.id DESC';\n parameters.push(filter.limit);\n return {\n parameters,\n sql: `SELECT p.*, u.display_name AS author_name\n FROM publications p JOIN publishers u ON u.id = p.owner_id\n ${where.length ? `WHERE ${where.join(' AND ')}` : ''}\n ORDER BY ${order} LIMIT ?`,\n };\n}\n\nexport function likedPublicationsSql(publisherId: string, publicationIds: readonly string[]): BoundSql {\n return {\n parameters: [publisherId, ...publicationIds],\n sql: `SELECT publication_id FROM likes WHERE publisher_id = ?\n AND publication_id IN (${publicationIds.map(() => '?').join(', ') || 'NULL'})`,\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vielzeug/archive",
3
- "version": "26.10.6",
3
+ "version": "26.10.7",
4
4
  "description": "Community content publishing for Cloudflare Workers + D1: a hardened Worker core over per-collection payload schemas",
5
5
  "repository": {
6
6
  "type": "git",
@@ -61,7 +61,7 @@
61
61
  "test:types": "tsc -p tsconfig.json --noEmit"
62
62
  },
63
63
  "dependencies": {
64
- "@vielzeug/spell": "26.10.6",
64
+ "@vielzeug/spell": "26.10.7",
65
65
  "jose": "6.2.12"
66
66
  },
67
67
  "devDependencies": {