@vielzeug/archive 26.10.14 → 26.10.16

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 (68) hide show
  1. package/dist/_crypto.cjs.map +1 -1
  2. package/dist/_crypto.js +5 -1
  3. package/dist/_crypto.js.map +1 -1
  4. package/dist/_json.cjs.map +1 -1
  5. package/dist/_json.js +51 -1
  6. package/dist/_json.js.map +1 -1
  7. package/dist/auth/_confirmation.cjs +5 -1
  8. package/dist/auth/_confirmation.cjs.map +1 -1
  9. package/dist/auth/_confirmation.d.ts.map +1 -1
  10. package/dist/auth/_confirmation.js +38 -14
  11. package/dist/auth/_confirmation.js.map +1 -1
  12. package/dist/auth/config.cjs.map +1 -1
  13. package/dist/auth/config.js +156 -1
  14. package/dist/auth/config.js.map +1 -1
  15. package/dist/auth/errors.cjs.map +1 -1
  16. package/dist/auth/errors.js +18 -1
  17. package/dist/auth/errors.js.map +1 -1
  18. package/dist/auth/store.cjs.map +1 -1
  19. package/dist/auth/store.js +102 -18
  20. package/dist/auth/store.js.map +1 -1
  21. package/dist/auth/worker.cjs.map +1 -1
  22. package/dist/auth/worker.js +360 -1
  23. package/dist/auth/worker.js.map +1 -1
  24. package/dist/auth.js +1 -1
  25. package/dist/collection.cjs.map +1 -1
  26. package/dist/collection.js +108 -1
  27. package/dist/collection.js.map +1 -1
  28. package/dist/contract.cjs.map +1 -1
  29. package/dist/contract.js +144 -1
  30. package/dist/contract.js.map +1 -1
  31. package/dist/errors.cjs.map +1 -1
  32. package/dist/errors.js +14 -1
  33. package/dist/errors.js.map +1 -1
  34. package/dist/index.js +4 -1
  35. package/dist/worker/_pagination.cjs.map +1 -1
  36. package/dist/worker/_pagination.js +55 -1
  37. package/dist/worker/_pagination.js.map +1 -1
  38. package/dist/worker/_service.cjs.map +1 -1
  39. package/dist/worker/_service.js +16 -1
  40. package/dist/worker/_service.js.map +1 -1
  41. package/dist/worker/auth.cjs.map +1 -1
  42. package/dist/worker/auth.js +68 -1
  43. package/dist/worker/auth.js.map +1 -1
  44. package/dist/worker/env.cjs.map +1 -1
  45. package/dist/worker/env.js +76 -1
  46. package/dist/worker/env.js.map +1 -1
  47. package/dist/worker/http.cjs +1 -1
  48. package/dist/worker/http.cjs.map +1 -1
  49. package/dist/worker/http.d.ts +3 -1
  50. package/dist/worker/http.d.ts.map +1 -1
  51. package/dist/worker/http.js +118 -1
  52. package/dist/worker/http.js.map +1 -1
  53. package/dist/worker/publications.cjs +1 -1
  54. package/dist/worker/publications.cjs.map +1 -1
  55. package/dist/worker/publications.d.ts.map +1 -1
  56. package/dist/worker/publications.js +470 -1
  57. package/dist/worker/publications.js.map +1 -1
  58. package/dist/worker/sql.cjs +3 -3
  59. package/dist/worker/sql.cjs.map +1 -1
  60. package/dist/worker/sql.d.ts +4 -2
  61. package/dist/worker/sql.d.ts.map +1 -1
  62. package/dist/worker/sql.js +100 -56
  63. package/dist/worker/sql.js.map +1 -1
  64. package/dist/worker/worker.cjs.map +1 -1
  65. package/dist/worker/worker.js +352 -1
  66. package/dist/worker/worker.js.map +1 -1
  67. package/dist/worker.js +1 -1
  68. package/package.json +6 -6
@@ -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\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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"}
1
+ {"version":3,"file":"sql.js","names":["SQL","filter","column","id","cursor","publisherId","operator","scope","where","parameters","order","publicationIds"],"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 /** Declared filter values matched with OR; empty matches every entry. */\n filters?: readonly string[];\n limit: number;\n /** Quoted word-prefix terms, normalized before reaching the SQL boundary. */\n searchMatch?: string;\n sort: 'liked' | 'recent';\n /** Declared tag values matched with OR; empty matches every entry. */\n tags?: readonly 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.filters?.length) {\n where.push(`p.filter_value IN (${filter.filters.map(() => '?').join(', ')})`);\n parameters.push(...filter.filters);\n }\n if (filter.tags?.length) {\n where.push(`EXISTS (\n SELECT 1 FROM publication_tags t WHERE t.publication_id = p.id AND t.tag IN (${filter.tags.map(() => '?').join(', ')})\n )`);\n parameters.push(...filter.tags);\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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"}
@@ -1 +1 @@
1
- {"version":3,"file":"worker.cjs","names":[],"sources":["../../src/worker/worker.ts"],"sourcesContent":["import type { ArchiveCollection } from '../collection';\nimport { ARCHIVE_LIMITS, type ArchiveErrorCode, archiveHealthSchema, PUBLISHER_HEADER } from '../contract';\nimport { ArchiveDefinitionError, ArchiveDerivationError } from '../errors';\nimport { requireSession, type Session, verifySession } from './auth';\nimport { type ApiConfig, type ArchiveWorkerEnv, apiConfig } from './env';\nimport {\n ApiError,\n type ArchiveDiagnostic,\n badRequest,\n corsHeaders,\n expiredSessionCookie,\n json,\n preflight,\n queryParams,\n requireMutationGuard,\n toErrorResponse,\n} from './http';\nimport {\n collectionMeta,\n deleteAccountProfile,\n deleteEntry,\n deleteProfile,\n erasePublisher,\n getEntry,\n getMe,\n healthJson,\n listEntries,\n listModerationLog,\n listReports,\n myLikes,\n myPublications,\n myReports,\n publishEntry,\n registerMe,\n renameMe,\n reportEntry,\n resolveReport,\n setHidden,\n setLike,\n updateEntry,\n} from './publications';\nimport { SQL } from './sql';\n\n/**\n * The community-archive Worker factory: anonymous public reads, cookie-authenticated\n * mutations behind strict Origin/request-marker guards, IP and subject rate limits,\n * operator moderation over the registered collections, and a daily expiry sweep for\n * retired publish retry keys.\n *\n * Trust model: configuration and signing keys come only from validated Env bindings;\n * the session cookie is the single credential; every response is either no-store\n * (private/error) or a 30s anonymous public cache entry. Runtime observability\n * flows exclusively through `tap()` — the core itself never logs.\n */\n\n/** The minimal execution-context surface the core uses; the runtime's satisfies it. */\nexport interface ArchiveExecutionContext {\n waitUntil(promise: Promise<unknown>): void;\n}\n\n/** One sanitized request-completion event: ids, codes, route classes and duration only. */\nexport interface ArchiveRequestEvent {\n /** 'ok' on success; otherwise the route's `ARCHIVE_ERROR_CODES` value. */\n code: ArchiveErrorCode | 'ok';\n diagnostic?: ArchiveDiagnostic;\n /** Whole-request wall-clock duration in milliseconds. */\n ms: number;\n requestId: string;\n /** A finite route class like `/collections/:collection/entries`, never the raw path. */\n route: string;\n status: number;\n type: 'request';\n}\n\n/** The daily retry-key sweep, observable like every other runtime behavior. */\nexport type ArchiveScheduledEvent =\n | {\n /** Retired retry-key rows this run removed. */\n removed: number;\n type: 'scheduled';\n status: 'ok';\n }\n | { type: 'scheduled'; status: 'failed'; diagnostic: ArchiveDiagnostic };\n\nexport type ArchiveEvent =\n | ArchiveRequestEvent\n | ArchiveScheduledEvent\n | { readonly type: 'cache-error'; readonly diagnostic: ArchiveDiagnostic };\n\n/**\n * The Worker's exported-handler shape plus its observability side channel. Declared\n * structurally so the package needs no `@cloudflare/workers-types`: a deployment's\n * generated `ExportedHandler<Env>` accepts the factory's result as-is.\n */\nexport interface ArchiveWorkerHandler<E extends ArchiveWorkerEnv = ArchiveWorkerEnv> {\n fetch(request: Request, env: E, ctx: ArchiveExecutionContext): Promise<Response>;\n scheduled?(controller: unknown, env: E, ctx: ArchiveExecutionContext): void | Promise<void>;\n /**\n * Observes every request's outcome and every scheduled sweep. The handler owns\n * logging policy (console, a structured logger, or nothing); handler errors are\n * swallowed so observability can never affect the Worker, and with no handlers\n * registered there is zero work.\n */\n tap(handler: (event: ArchiveEvent) => void, options?: { readonly signal?: AbortSignal }): () => void;\n}\n\n/** One dispatchable route: the id both the switch and the event labels derive from. */\ntype RouteId =\n | 'health'\n | 'signOut'\n | 'meta'\n | 'reports'\n | 'resolveReport'\n | 'entries'\n | 'entry'\n | 'like'\n | 'report'\n | 'hidden'\n | 'me'\n | 'accountErase'\n | 'mePublications'\n | 'meLikes'\n | 'meReports'\n | 'modLog'\n | 'publisherErase';\n\n/** One matched route: its id (for dispatch) and label (for events), plus captures. */\ninterface RouteMatch {\n id: RouteId;\n /** The finite event label, never the raw path. */\n label: string;\n /** The methods the route serves; anything else is 405 before any guard runs. */\n methods: readonly string[];\n /** The path's captured segments, in pattern order: collection first, then id. */\n segments: readonly string[];\n}\n\n/**\n * The one route table: dispatch, methods, and event labels all read from here, so a\n * new route can never miss its observability class or its method set. Methods are\n * enforced before any guard runs — a wrong-method request never spends rate-limit\n * budget, verifies a session, or answers an auth-shaped error.\n */\nconst ROUTES: readonly { id: RouteId; label: string; methods: readonly string[]; pattern: RegExp }[] = [\n { id: 'accountErase', label: '/me/account', methods: ['DELETE'], pattern: /^\\/api\\/v1\\/me\\/account$/ },\n { id: 'health', label: '/health', methods: ['GET'], pattern: /^\\/api\\/v1\\/health$/ },\n { id: 'signOut', label: '/sign-out', methods: ['POST'], pattern: /^\\/api\\/v1\\/sign-out$/ },\n {\n id: 'resolveReport',\n label: '/collections/:collection/reports/:id/resolved',\n methods: ['PUT'],\n pattern: /^\\/api\\/v1\\/collections\\/([a-z0-9-]+)\\/reports\\/([0-9a-f-]{36})\\/resolved$/i,\n },\n {\n id: 'meta',\n label: '/collections/:collection',\n methods: ['GET'],\n pattern: /^\\/api\\/v1\\/collections\\/([a-z0-9-]+)$/,\n },\n {\n id: 'reports',\n label: '/collections/:collection/reports',\n methods: ['GET'],\n pattern: /^\\/api\\/v1\\/collections\\/([a-z0-9-]+)\\/reports$/,\n },\n {\n id: 'entries',\n label: '/collections/:collection/entries',\n methods: ['GET', 'POST'],\n pattern: /^\\/api\\/v1\\/collections\\/([a-z0-9-]+)\\/entries$/,\n },\n {\n id: 'entry',\n label: '/collections/:collection/entries/:id',\n methods: ['GET', 'PUT', 'DELETE'],\n pattern: /^\\/api\\/v1\\/collections\\/([a-z0-9-]+)\\/entries\\/([0-9a-f-]{36})$/i,\n },\n {\n id: 'like',\n label: '/collections/:collection/entries/:id/like',\n methods: ['PUT', 'DELETE'],\n pattern: /^\\/api\\/v1\\/collections\\/([a-z0-9-]+)\\/entries\\/([0-9a-f-]{36})\\/like$/i,\n },\n {\n id: 'report',\n label: '/collections/:collection/entries/:id/report',\n methods: ['POST'],\n pattern: /^\\/api\\/v1\\/collections\\/([a-z0-9-]+)\\/entries\\/([0-9a-f-]{36})\\/report$/i,\n },\n {\n id: 'hidden',\n label: '/collections/:collection/entries/:id/hidden',\n methods: ['PUT', 'DELETE'],\n pattern: /^\\/api\\/v1\\/collections\\/([a-z0-9-]+)\\/entries\\/([0-9a-f-]{36})\\/hidden$/i,\n },\n {\n id: 'modLog',\n label: '/moderation-log',\n methods: ['GET'],\n pattern: /^\\/api\\/v1\\/moderation-log$/,\n },\n {\n id: 'publisherErase',\n label: '/publishers/:id',\n methods: ['DELETE'],\n pattern: /^\\/api\\/v1\\/publishers\\/([0-9a-f-]{36})$/i,\n },\n { id: 'me', label: '/me', methods: ['GET', 'POST', 'PUT', 'DELETE'], pattern: /^\\/api\\/v1\\/me$/ },\n { id: 'mePublications', label: '/me/publications', methods: ['GET'], pattern: /^\\/api\\/v1\\/me\\/publications$/ },\n { id: 'meLikes', label: '/me/likes', methods: ['GET'], pattern: /^\\/api\\/v1\\/me\\/likes$/ },\n { id: 'meReports', label: '/me/reports', methods: ['GET'], pattern: /^\\/api\\/v1\\/me\\/reports$/ },\n];\n\nconst matchRoute = (pathname: string): RouteMatch | null => {\n for (const route of ROUTES) {\n const found = pathname.match(route.pattern);\n if (found) return { id: route.id, label: route.label, methods: route.methods, segments: found.slice(1) };\n }\n return null;\n};\n\n/** The matched route's event label; unmatched paths stay 'unknown', never the raw path. */\nconst routeLabel = (pathname: string): string => matchRoute(pathname)?.label ?? 'unknown';\n\nconst UUID_PATH = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/** The routes whose second path segment is an entry id: they share the one id rule. */\nconst ID_SEGMENT_ROUTES: ReadonlySet<RouteId> = new Set(['entry', 'like', 'report', 'hidden', 'resolveReport']);\n\nconst notAllowed = (): ApiError => new ApiError(405, 'method-not-allowed', 'That method is not supported here.');\n\nconst hmacKeys = new WeakMap<ApiConfig, Promise<CryptoKey>>();\n/** Peppered HMAC: rate-limit keys cannot be reversed to IPs without the deployment secret. */\nconst rateLimitHash = async (config: ApiConfig, text: string): Promise<string> => {\n let key = hmacKeys.get(config);\n if (key === undefined) {\n key = crypto.subtle.importKey(\n 'raw',\n new TextEncoder().encode(config.rateLimitPepper),\n { hash: 'SHA-256', name: 'HMAC' },\n false,\n ['sign'],\n );\n hmacKeys.set(config, key);\n }\n const mac = await crypto.subtle.sign('HMAC', await key, new TextEncoder().encode(text));\n return [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, '0')).join('');\n};\n\n/**\n * The Retry-After the 429s carry. The platform limiter does not expose its period at\n * runtime, so deployments must configure 60-second windows — a longer window would\n * make the header lie.\n */\nconst RATE_LIMIT_RETRY_AFTER_SECONDS = 60;\n\n/** The connecting IP, peppered-HMAC'd, keys the read limit; loopback only when APP_ENV=local. */\nasync function readLimit(config: ApiConfig, env: ArchiveWorkerEnv, request: Request): Promise<void> {\n const ip = request.headers.get('cf-connecting-ip') ?? (config.appEnv === 'local' ? '127.0.0.1' : null);\n if (ip === null) throw new ApiError(503, 'unavailable', 'The service is temporarily unavailable.');\n const { success } = await env.READ_LIMIT.limit({ key: await rateLimitHash(config, ip) });\n if (!success) {\n throw new ApiError(429, 'rate-limited', 'Too many requests. Try again later.', {\n retryAfterSeconds: RATE_LIMIT_RETRY_AFTER_SECONDS,\n });\n }\n}\n\n/** The verified issuer+subject, peppered-HMAC'd, keys the write limit ahead of every mutation. */\nasync function writeLimit(config: ApiConfig, env: ArchiveWorkerEnv, issuer: string, subject: string): Promise<void> {\n const { success } = await env.WRITE_LIMIT.limit({\n key: await rateLimitHash(config, JSON.stringify([issuer, subject])),\n });\n if (!success) {\n throw new ApiError(429, 'rate-limited', 'Too many requests. Try again later.', {\n retryAfterSeconds: RATE_LIMIT_RETRY_AFTER_SECONDS,\n });\n }\n}\n\n/** Only anonymous list reads may serve from the edge cache. */\nasync function cachedPublicGet(\n request: Request,\n ctx: ArchiveExecutionContext,\n produce: () => Promise<Response>,\n onFailure: () => void,\n): Promise<Response> {\n const anonymous = request.headers.get('cookie') === null && request.headers.get('authorization') === null;\n if (!anonymous) return produce();\n const url = new URL(request.url);\n url.search = new URLSearchParams(\n [...url.searchParams.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),\n ).toString();\n const edgeCache = caches.default;\n const key = new Request(url.toString(), { method: 'GET' });\n const cached = await edgeCache.match(key).catch(() => {\n onFailure();\n return undefined;\n });\n if (cached) return cached;\n const response = await produce();\n response.headers.set(\n 'cache-control',\n `public, max-age=${ARCHIVE_LIMITS.publicCacheSeconds}, s-maxage=${ARCHIVE_LIMITS.publicCacheSeconds}`,\n );\n ctx.waitUntil(edgeCache.put(key, response.clone()).catch(onFailure));\n return response;\n}\n\nconst withCors = (response: Response, request: Request, config: ApiConfig): Response => {\n const headers = corsHeaders(request, config);\n if (Object.keys(headers).length === 0) return response;\n const next = new Response(response.body, response);\n for (const [key, value] of Object.entries(headers)) next.headers.set(key, value);\n return next;\n};\n\n/**\n * Fail-closed precondition for registered publishers: the UI pins the opaque\n * publisher UUID it believes the session belongs to, so a queued write from one\n * tab can never land under another account that took over the shared cookie.\n * The header is never an identity source — every check still resolves the\n * publisher from the verified JWT — it only vetoes mismatched writes.\n */\nfunction requireExpectedPublisher(request: Request, session: Session, exempt: boolean): void {\n if (exempt || session.publisher === null) return;\n const expected = request.headers.get(PUBLISHER_HEADER);\n if (expected === null || !UUID_PATH.test(expected)) {\n throw new ApiError(400, 'bad-request', 'The request is not valid.');\n }\n if (expected !== session.publisher.id) {\n throw new ApiError(409, 'publisher-changed', 'The signed-in publisher changed since this request was prepared.');\n }\n}\n\n/**\n * Builds the Worker for one deployment's registered collections. Each collection owns\n * one URL + storage dimension; a duplicate registration is a deployment bug and\n * fails fast.\n */\nexport function createArchiveWorker<E extends ArchiveWorkerEnv>(\n collections: readonly ArchiveCollection[],\n): ArchiveWorkerHandler<E> {\n const registered = new Map<string, ArchiveCollection>();\n for (const collection of collections) {\n if (registered.has(collection.name))\n throw new ArchiveDefinitionError(`Duplicate archive collection: ${collection.name}`);\n registered.set(collection.name, collection);\n }\n\n /** Resolves the route's collection segment; an unregistered collection is a 404, not a 500. */\n const collectionFor = (segment: string): ArchiveCollection => {\n const collection = registered.get(segment);\n if (collection === undefined) throw new ApiError(404, 'not-found', 'No such route.');\n return collection;\n };\n\n async function handle(request: Request, env: E, ctx: ArchiveExecutionContext): Promise<Response> {\n const config = apiConfig(env);\n const url = new URL(request.url);\n const { pathname } = url;\n const method = request.method;\n\n if (!pathname.startsWith('/api/v1/')) throw new ApiError(404, 'not-found', 'No such route.');\n if (method === 'OPTIONS') return preflight(request, config);\n\n const match = matchRoute(pathname);\n // An unknown path inside the mount answers 404 like the paths outside it, and a\n // known route under an unsupported method answers 405 — both with no side\n // effects: no rate-limit spend, no session verification, no auth state leaked,\n // no guard-shaped error for a method the route does not serve.\n if (match === null) throw new ApiError(404, 'not-found', 'No such route.');\n if (!match.methods.includes(method)) throw notAllowed();\n\n // A malformed entry id is the same class of client bug: it answers 400 before\n // any rate-limit spend, session verification or auth state.\n if (ID_SEGMENT_ROUTES.has(match.id) && !UUID_PATH.test(match.segments[1] ?? '')) throw badRequest();\n\n // Health sits deliberately outside the rate-limit and session pipelines: uptime\n // probes must never burn the anonymous read budget or couple to authentication,\n // and it touches the database exactly once (the schema-version read).\n if (match.id === 'health') {\n return withCors(await healthJson(config, env), request, config);\n }\n if (match.id === 'reports') queryParams(url, ['cursor', 'limit', 'status']);\n\n await readLimit(config, env, request);\n\n // The same-origin mutation contract is proven BEFORE any identity work: a\n // foreign-origin write or missing marker must fail with 403 even while carrying a\n // garbage token — JWKS/JWT cost and 401 semantics stay behind the origin fence.\n const isMutation = method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS';\n if (isMutation) requireMutationGuard(request, config);\n\n // Session verification runs only where identity matters: private routes, the\n // moderation surfaces and every mutation. Public reads (list, detail, meta) never\n // invoke JWKS or the publisher lookup — browsing must not couple to authentication\n // availability. Sign-out is a cookie-clearing reply, not a session operation.\n const isPublicGet = method === 'GET' && (match.id === 'meta' || match.id === 'entries' || match.id === 'entry');\n const isSignOut = match.id === 'signOut' && method === 'POST';\n const session =\n isPublicGet || isSignOut ? null : await verifySession(request, config, env, match.id === 'accountErase');\n\n if (isMutation) {\n if (isSignOut) {\n // The shared `archiveOkSchema` body; every session cookie attribute is the\n // contract's, never ad hoc.\n const response = json(\n 200,\n { ok: true },\n {\n 'cache-control': 'no-store',\n 'set-cookie': expiredSessionCookie(config.appEnv === 'production'),\n },\n );\n return withCors(response, request, config);\n }\n const verified = requireSession(session);\n // The free fail-closed header precondition runs before the quota token is spent:\n // a stale tab's rejected writes must not consume the current account's budget.\n requireExpectedPublisher(request, verified, match.id === 'me' && method === 'POST');\n await writeLimit(config, env, verified.subject.issuer, verified.subject.subject);\n }\n\n const jsonWithCors = (response: Response) => withCors(response, request, config);\n\n switch (match.id) {\n case 'meta': {\n queryParams(url, []);\n return jsonWithCors(collectionMeta(collectionFor(match.segments[0] ?? '')));\n }\n case 'entries': {\n const collection = collectionFor(match.segments[0] ?? '');\n if (method === 'GET') {\n const response = await cachedPublicGet(\n request,\n ctx,\n () => listEntries(collection, request, env),\n () => emit({ diagnostic: { kind: 'cache' }, type: 'cache-error' }),\n );\n return jsonWithCors(response);\n }\n if (method === 'POST') {\n return jsonWithCors(await publishEntry(collection, request, env, config, requireSession(session)));\n }\n throw notAllowed();\n }\n case 'entry': {\n const collection = collectionFor(match.segments[0] ?? '');\n const id = match.segments[1] ?? '';\n if (method === 'GET') {\n queryParams(url, []);\n return jsonWithCors(await getEntry(collection, env, id));\n }\n if (method === 'PUT') {\n return jsonWithCors(await updateEntry(collection, request, env, config, requireSession(session), id));\n }\n if (method === 'DELETE') {\n return jsonWithCors(await deleteEntry(collection, env, requireSession(session), id));\n }\n throw notAllowed();\n }\n case 'like': {\n const collection = collectionFor(match.segments[0] ?? '');\n const id = match.segments[1] ?? '';\n if (method === 'PUT') {\n return jsonWithCors(await setLike(collection, env, config, requireSession(session), id, true));\n }\n if (method === 'DELETE') {\n return jsonWithCors(await setLike(collection, env, config, requireSession(session), id, false));\n }\n throw notAllowed();\n }\n case 'report': {\n if (method !== 'POST') throw notAllowed();\n const collection = collectionFor(match.segments[0] ?? '');\n return jsonWithCors(\n await reportEntry(collection, request, env, requireSession(session), match.segments[1] ?? ''),\n );\n }\n case 'hidden': {\n if (method !== 'PUT' && method !== 'DELETE') throw notAllowed();\n const collection = collectionFor(match.segments[0] ?? '');\n return jsonWithCors(\n await setHidden(collection, env, requireSession(session), match.segments[1] ?? '', method === 'PUT'),\n );\n }\n case 'reports': {\n // Private reads carry the same fail-closed precondition as writes: a stale tab\n // must not read another publisher's moderation queue after an account switch.\n const verified = requireSession(session);\n requireExpectedPublisher(request, verified, false);\n return jsonWithCors(await listReports(collectionFor(match.segments[0] ?? ''), request, env, verified));\n }\n case 'resolveReport': {\n queryParams(url, []);\n return jsonWithCors(\n await resolveReport(\n collectionFor(match.segments[0] ?? ''),\n env,\n requireSession(session),\n match.segments[1] ?? '',\n ),\n );\n }\n case 'modLog': {\n // The accountability read carries the same fail-closed precondition as every\n // private surface: a stale tab must not read the moderation history after an\n // account switch.\n const verified = requireSession(session);\n requireExpectedPublisher(request, verified, false);\n return jsonWithCors(await listModerationLog(request, env, verified));\n }\n case 'publisherErase': {\n // The terminal moderation action: erasing a publisher removes their row, and\n // FK cascades take the publications, likes and retry keys with it — the same\n // cascade a self-erase walks, applied to any publisher the operator names.\n if (method !== 'DELETE') throw notAllowed();\n return jsonWithCors(await erasePublisher(env, requireSession(session), match.segments[0] ?? ''));\n }\n case 'accountErase': {\n queryParams(url, []);\n return jsonWithCors(await deleteAccountProfile(env, requireSession(session)));\n }\n case 'me': {\n if (method === 'GET') return jsonWithCors(getMe(session));\n if (method === 'POST') return jsonWithCors(await registerMe(request, env, requireSession(session)));\n if (method === 'PUT') return jsonWithCors(await renameMe(request, env, requireSession(session)));\n if (method === 'DELETE') return jsonWithCors(await deleteProfile(env, requireSession(session)));\n throw notAllowed();\n }\n case 'mePublications':\n case 'meLikes':\n case 'meReports': {\n // Private reads carry the same fail-closed precondition as writes: a stale tab\n // must not consume another publisher's data after a shared-cookie account switch.\n const verified = requireSession(session);\n requireExpectedPublisher(request, verified, false);\n if (match.id === 'mePublications') {\n // The owner's entries are per-collection: the collection param names the\n // registered collection, and an absent or unregistered value is a malformed\n // request, not an empty list.\n const params = queryParams(url, ['collection']);\n const requested = params.get('collection');\n if (requested === null) throw badRequest();\n const collection = registered.get(requested);\n if (collection === undefined) throw badRequest();\n return jsonWithCors(await myPublications(collection, env, verified));\n }\n return jsonWithCors(\n await (match.id === 'meReports' ? myReports(request, env, verified) : myLikes(request, env, verified)),\n );\n }\n default:\n throw notAllowed();\n }\n }\n\n const tappers = new Set<(event: ArchiveEvent) => void>();\n const emit = (event: ArchiveEvent): void => {\n if (tappers.size === 0) return;\n for (const tapper of tappers) {\n try {\n tapper(event);\n } catch {\n // Observability must never affect the Worker.\n }\n }\n };\n\n return {\n async fetch(request: Request, env: E, ctx: ArchiveExecutionContext): Promise<Response> {\n const requestId = crypto.randomUUID();\n const route = routeLabel(new URL(request.url).pathname);\n const startedAt = Date.now();\n try {\n const response = await handle(request, env, ctx);\n let diagnostic: ArchiveDiagnostic | undefined;\n if (response.status === 503 && route === '/health') {\n const health = archiveHealthSchema.parse(await response.clone().json());\n diagnostic = { kind: health.reason === 'database-unavailable' ? 'database' : 'schema' };\n }\n emit({\n code: response.ok ? 'ok' : 'unavailable',\n ms: Date.now() - startedAt,\n requestId,\n route,\n status: response.status,\n type: 'request',\n ...(diagnostic === undefined ? {} : { diagnostic }),\n });\n return response;\n } catch (error) {\n const response = toErrorResponse(error, requestId);\n emit({\n code: error instanceof ApiError ? error.code : 'internal',\n diagnostic:\n error instanceof ApiError\n ? error.diagnostic\n : { kind: error instanceof ArchiveDerivationError ? 'collection' : 'internal' },\n ms: Date.now() - startedAt,\n requestId,\n route,\n status: response.status,\n type: 'request',\n });\n // Allowed origins see the same CORS contract on errors as on successes.\n try {\n return withCors(response, request, apiConfig(env));\n } catch {\n return response; // the config error itself is the response\n }\n }\n },\n\n async scheduled(_controller: unknown, env: E, _ctx: ArchiveExecutionContext): Promise<void> {\n try {\n const timestamp = Date.now();\n const results = await env.DB.batch([\n env.DB.prepare(SQL.expireRequests).bind(timestamp),\n env.DB.prepare(SQL.expireRevocations).bind(timestamp),\n ]);\n emit({\n removed: results.reduce((sum, result) => sum + (result.meta?.changes ?? 0), 0),\n status: 'ok',\n type: 'scheduled',\n });\n } catch (error) {\n emit({ diagnostic: { kind: 'database' }, status: 'failed', type: 'scheduled' });\n throw error;\n }\n },\n\n tap(handler, options) {\n const signal = options?.signal;\n if (signal?.aborted) return () => {};\n const off = () => {\n tappers.delete(handler);\n signal?.removeEventListener('abort', off);\n };\n tappers.add(handler);\n signal?.addEventListener('abort', off, { once: true });\n return off;\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"worker.cjs","names":["ApiError","ARCHIVE_LIMITS","corsHeaders","PUBLISHER_HEADER","ArchiveDefinitionError","apiConfig","preflight","badRequest","healthJson","verifySession","json","expiredSessionCookie","requireSession","collectionMeta","listEntries","publishEntry","getEntry","updateEntry","deleteEntry","setLike","reportEntry","setHidden","listReports","resolveReport","listModerationLog","erasePublisher","deleteAccountProfile","getMe","registerMe","renameMe","deleteProfile","queryParams","myPublications","myReports","myLikes","archiveHealthSchema","toErrorResponse","ArchiveDerivationError","SQL","require_contract","require_http","require_errors","require_sql","require_auth","require_env","require_publications","ROUTES","route","found","pathname","UUID_PATH","ID_SEGMENT_ROUTES","hmacKeys","key","config","mac","text","b","RATE_LIMIT_RETRY_AFTER_SECONDS","ip","request","env","success","issuer","subject","produce","url","a","edgeCache","cached","onFailure","response","ctx","headers","next","value","exempt","session","expected","registered","collection","collections","segment","method","match","isMutation","isPublicGet","isSignOut","verified","id","requested","tappers","tapper","event","requestId","startedAt","diagnostic","error","timestamp","results","sum","result","signal","options","handler"],"sources":["../../src/worker/worker.ts"],"sourcesContent":["import type { ArchiveCollection } from '../collection';\nimport { ARCHIVE_LIMITS, type ArchiveErrorCode, archiveHealthSchema, PUBLISHER_HEADER } from '../contract';\nimport { ArchiveDefinitionError, ArchiveDerivationError } from '../errors';\nimport { requireSession, type Session, verifySession } from './auth';\nimport { type ApiConfig, type ArchiveWorkerEnv, apiConfig } from './env';\nimport {\n ApiError,\n type ArchiveDiagnostic,\n badRequest,\n corsHeaders,\n expiredSessionCookie,\n json,\n preflight,\n queryParams,\n requireMutationGuard,\n toErrorResponse,\n} from './http';\nimport {\n collectionMeta,\n deleteAccountProfile,\n deleteEntry,\n deleteProfile,\n erasePublisher,\n getEntry,\n getMe,\n healthJson,\n listEntries,\n listModerationLog,\n listReports,\n myLikes,\n myPublications,\n myReports,\n publishEntry,\n registerMe,\n renameMe,\n reportEntry,\n resolveReport,\n setHidden,\n setLike,\n updateEntry,\n} from './publications';\nimport { SQL } from './sql';\n\n/**\n * The community-archive Worker factory: anonymous public reads, cookie-authenticated\n * mutations behind strict Origin/request-marker guards, IP and subject rate limits,\n * operator moderation over the registered collections, and a daily expiry sweep for\n * retired publish retry keys.\n *\n * Trust model: configuration and signing keys come only from validated Env bindings;\n * the session cookie is the single credential; every response is either no-store\n * (private/error) or a 30s anonymous public cache entry. Runtime observability\n * flows exclusively through `tap()` — the core itself never logs.\n */\n\n/** The minimal execution-context surface the core uses; the runtime's satisfies it. */\nexport interface ArchiveExecutionContext {\n waitUntil(promise: Promise<unknown>): void;\n}\n\n/** One sanitized request-completion event: ids, codes, route classes and duration only. */\nexport interface ArchiveRequestEvent {\n /** 'ok' on success; otherwise the route's `ARCHIVE_ERROR_CODES` value. */\n code: ArchiveErrorCode | 'ok';\n diagnostic?: ArchiveDiagnostic;\n /** Whole-request wall-clock duration in milliseconds. */\n ms: number;\n requestId: string;\n /** A finite route class like `/collections/:collection/entries`, never the raw path. */\n route: string;\n status: number;\n type: 'request';\n}\n\n/** The daily retry-key sweep, observable like every other runtime behavior. */\nexport type ArchiveScheduledEvent =\n | {\n /** Retired retry-key rows this run removed. */\n removed: number;\n type: 'scheduled';\n status: 'ok';\n }\n | { type: 'scheduled'; status: 'failed'; diagnostic: ArchiveDiagnostic };\n\nexport type ArchiveEvent =\n | ArchiveRequestEvent\n | ArchiveScheduledEvent\n | { readonly type: 'cache-error'; readonly diagnostic: ArchiveDiagnostic };\n\n/**\n * The Worker's exported-handler shape plus its observability side channel. Declared\n * structurally so the package needs no `@cloudflare/workers-types`: a deployment's\n * generated `ExportedHandler<Env>` accepts the factory's result as-is.\n */\nexport interface ArchiveWorkerHandler<E extends ArchiveWorkerEnv = ArchiveWorkerEnv> {\n fetch(request: Request, env: E, ctx: ArchiveExecutionContext): Promise<Response>;\n scheduled?(controller: unknown, env: E, ctx: ArchiveExecutionContext): void | Promise<void>;\n /**\n * Observes every request's outcome and every scheduled sweep. The handler owns\n * logging policy (console, a structured logger, or nothing); handler errors are\n * swallowed so observability can never affect the Worker, and with no handlers\n * registered there is zero work.\n */\n tap(handler: (event: ArchiveEvent) => void, options?: { readonly signal?: AbortSignal }): () => void;\n}\n\n/** One dispatchable route: the id both the switch and the event labels derive from. */\ntype RouteId =\n | 'health'\n | 'signOut'\n | 'meta'\n | 'reports'\n | 'resolveReport'\n | 'entries'\n | 'entry'\n | 'like'\n | 'report'\n | 'hidden'\n | 'me'\n | 'accountErase'\n | 'mePublications'\n | 'meLikes'\n | 'meReports'\n | 'modLog'\n | 'publisherErase';\n\n/** One matched route: its id (for dispatch) and label (for events), plus captures. */\ninterface RouteMatch {\n id: RouteId;\n /** The finite event label, never the raw path. */\n label: string;\n /** The methods the route serves; anything else is 405 before any guard runs. */\n methods: readonly string[];\n /** The path's captured segments, in pattern order: collection first, then id. */\n segments: readonly string[];\n}\n\n/**\n * The one route table: dispatch, methods, and event labels all read from here, so a\n * new route can never miss its observability class or its method set. Methods are\n * enforced before any guard runs — a wrong-method request never spends rate-limit\n * budget, verifies a session, or answers an auth-shaped error.\n */\nconst ROUTES: readonly { id: RouteId; label: string; methods: readonly string[]; pattern: RegExp }[] = [\n { id: 'accountErase', label: '/me/account', methods: ['DELETE'], pattern: /^\\/api\\/v1\\/me\\/account$/ },\n { id: 'health', label: '/health', methods: ['GET'], pattern: /^\\/api\\/v1\\/health$/ },\n { id: 'signOut', label: '/sign-out', methods: ['POST'], pattern: /^\\/api\\/v1\\/sign-out$/ },\n {\n id: 'resolveReport',\n label: '/collections/:collection/reports/:id/resolved',\n methods: ['PUT'],\n pattern: /^\\/api\\/v1\\/collections\\/([a-z0-9-]+)\\/reports\\/([0-9a-f-]{36})\\/resolved$/i,\n },\n {\n id: 'meta',\n label: '/collections/:collection',\n methods: ['GET'],\n pattern: /^\\/api\\/v1\\/collections\\/([a-z0-9-]+)$/,\n },\n {\n id: 'reports',\n label: '/collections/:collection/reports',\n methods: ['GET'],\n pattern: /^\\/api\\/v1\\/collections\\/([a-z0-9-]+)\\/reports$/,\n },\n {\n id: 'entries',\n label: '/collections/:collection/entries',\n methods: ['GET', 'POST'],\n pattern: /^\\/api\\/v1\\/collections\\/([a-z0-9-]+)\\/entries$/,\n },\n {\n id: 'entry',\n label: '/collections/:collection/entries/:id',\n methods: ['GET', 'PUT', 'DELETE'],\n pattern: /^\\/api\\/v1\\/collections\\/([a-z0-9-]+)\\/entries\\/([0-9a-f-]{36})$/i,\n },\n {\n id: 'like',\n label: '/collections/:collection/entries/:id/like',\n methods: ['PUT', 'DELETE'],\n pattern: /^\\/api\\/v1\\/collections\\/([a-z0-9-]+)\\/entries\\/([0-9a-f-]{36})\\/like$/i,\n },\n {\n id: 'report',\n label: '/collections/:collection/entries/:id/report',\n methods: ['POST'],\n pattern: /^\\/api\\/v1\\/collections\\/([a-z0-9-]+)\\/entries\\/([0-9a-f-]{36})\\/report$/i,\n },\n {\n id: 'hidden',\n label: '/collections/:collection/entries/:id/hidden',\n methods: ['PUT', 'DELETE'],\n pattern: /^\\/api\\/v1\\/collections\\/([a-z0-9-]+)\\/entries\\/([0-9a-f-]{36})\\/hidden$/i,\n },\n {\n id: 'modLog',\n label: '/moderation-log',\n methods: ['GET'],\n pattern: /^\\/api\\/v1\\/moderation-log$/,\n },\n {\n id: 'publisherErase',\n label: '/publishers/:id',\n methods: ['DELETE'],\n pattern: /^\\/api\\/v1\\/publishers\\/([0-9a-f-]{36})$/i,\n },\n { id: 'me', label: '/me', methods: ['GET', 'POST', 'PUT', 'DELETE'], pattern: /^\\/api\\/v1\\/me$/ },\n { id: 'mePublications', label: '/me/publications', methods: ['GET'], pattern: /^\\/api\\/v1\\/me\\/publications$/ },\n { id: 'meLikes', label: '/me/likes', methods: ['GET'], pattern: /^\\/api\\/v1\\/me\\/likes$/ },\n { id: 'meReports', label: '/me/reports', methods: ['GET'], pattern: /^\\/api\\/v1\\/me\\/reports$/ },\n];\n\nconst matchRoute = (pathname: string): RouteMatch | null => {\n for (const route of ROUTES) {\n const found = pathname.match(route.pattern);\n if (found) return { id: route.id, label: route.label, methods: route.methods, segments: found.slice(1) };\n }\n return null;\n};\n\n/** The matched route's event label; unmatched paths stay 'unknown', never the raw path. */\nconst routeLabel = (pathname: string): string => matchRoute(pathname)?.label ?? 'unknown';\n\nconst UUID_PATH = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/** The routes whose second path segment is an entry id: they share the one id rule. */\nconst ID_SEGMENT_ROUTES: ReadonlySet<RouteId> = new Set(['entry', 'like', 'report', 'hidden', 'resolveReport']);\n\nconst notAllowed = (): ApiError => new ApiError(405, 'method-not-allowed', 'That method is not supported here.');\n\nconst hmacKeys = new WeakMap<ApiConfig, Promise<CryptoKey>>();\n/** Peppered HMAC: rate-limit keys cannot be reversed to IPs without the deployment secret. */\nconst rateLimitHash = async (config: ApiConfig, text: string): Promise<string> => {\n let key = hmacKeys.get(config);\n if (key === undefined) {\n key = crypto.subtle.importKey(\n 'raw',\n new TextEncoder().encode(config.rateLimitPepper),\n { hash: 'SHA-256', name: 'HMAC' },\n false,\n ['sign'],\n );\n hmacKeys.set(config, key);\n }\n const mac = await crypto.subtle.sign('HMAC', await key, new TextEncoder().encode(text));\n return [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, '0')).join('');\n};\n\n/**\n * The Retry-After the 429s carry. The platform limiter does not expose its period at\n * runtime, so deployments must configure 60-second windows — a longer window would\n * make the header lie.\n */\nconst RATE_LIMIT_RETRY_AFTER_SECONDS = 60;\n\n/** The connecting IP, peppered-HMAC'd, keys the read limit; loopback only when APP_ENV=local. */\nasync function readLimit(config: ApiConfig, env: ArchiveWorkerEnv, request: Request): Promise<void> {\n const ip = request.headers.get('cf-connecting-ip') ?? (config.appEnv === 'local' ? '127.0.0.1' : null);\n if (ip === null) throw new ApiError(503, 'unavailable', 'The service is temporarily unavailable.');\n const { success } = await env.READ_LIMIT.limit({ key: await rateLimitHash(config, ip) });\n if (!success) {\n throw new ApiError(429, 'rate-limited', 'Too many requests. Try again later.', {\n retryAfterSeconds: RATE_LIMIT_RETRY_AFTER_SECONDS,\n });\n }\n}\n\n/** The verified issuer+subject, peppered-HMAC'd, keys the write limit ahead of every mutation. */\nasync function writeLimit(config: ApiConfig, env: ArchiveWorkerEnv, issuer: string, subject: string): Promise<void> {\n const { success } = await env.WRITE_LIMIT.limit({\n key: await rateLimitHash(config, JSON.stringify([issuer, subject])),\n });\n if (!success) {\n throw new ApiError(429, 'rate-limited', 'Too many requests. Try again later.', {\n retryAfterSeconds: RATE_LIMIT_RETRY_AFTER_SECONDS,\n });\n }\n}\n\n/** Only anonymous list reads may serve from the edge cache. */\nasync function cachedPublicGet(\n request: Request,\n ctx: ArchiveExecutionContext,\n produce: () => Promise<Response>,\n onFailure: () => void,\n): Promise<Response> {\n const anonymous = request.headers.get('cookie') === null && request.headers.get('authorization') === null;\n if (!anonymous) return produce();\n const url = new URL(request.url);\n url.search = new URLSearchParams(\n [...url.searchParams.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),\n ).toString();\n const edgeCache = caches.default;\n const key = new Request(url.toString(), { method: 'GET' });\n const cached = await edgeCache.match(key).catch(() => {\n onFailure();\n return undefined;\n });\n if (cached) return cached;\n const response = await produce();\n response.headers.set(\n 'cache-control',\n `public, max-age=${ARCHIVE_LIMITS.publicCacheSeconds}, s-maxage=${ARCHIVE_LIMITS.publicCacheSeconds}`,\n );\n ctx.waitUntil(edgeCache.put(key, response.clone()).catch(onFailure));\n return response;\n}\n\nconst withCors = (response: Response, request: Request, config: ApiConfig): Response => {\n const headers = corsHeaders(request, config);\n if (Object.keys(headers).length === 0) return response;\n const next = new Response(response.body, response);\n for (const [key, value] of Object.entries(headers)) next.headers.set(key, value);\n return next;\n};\n\n/**\n * Fail-closed precondition for registered publishers: the UI pins the opaque\n * publisher UUID it believes the session belongs to, so a queued write from one\n * tab can never land under another account that took over the shared cookie.\n * The header is never an identity source — every check still resolves the\n * publisher from the verified JWT — it only vetoes mismatched writes.\n */\nfunction requireExpectedPublisher(request: Request, session: Session, exempt: boolean): void {\n if (exempt || session.publisher === null) return;\n const expected = request.headers.get(PUBLISHER_HEADER);\n if (expected === null || !UUID_PATH.test(expected)) {\n throw new ApiError(400, 'bad-request', 'The request is not valid.');\n }\n if (expected !== session.publisher.id) {\n throw new ApiError(409, 'publisher-changed', 'The signed-in publisher changed since this request was prepared.');\n }\n}\n\n/**\n * Builds the Worker for one deployment's registered collections. Each collection owns\n * one URL + storage dimension; a duplicate registration is a deployment bug and\n * fails fast.\n */\nexport function createArchiveWorker<E extends ArchiveWorkerEnv>(\n collections: readonly ArchiveCollection[],\n): ArchiveWorkerHandler<E> {\n const registered = new Map<string, ArchiveCollection>();\n for (const collection of collections) {\n if (registered.has(collection.name))\n throw new ArchiveDefinitionError(`Duplicate archive collection: ${collection.name}`);\n registered.set(collection.name, collection);\n }\n\n /** Resolves the route's collection segment; an unregistered collection is a 404, not a 500. */\n const collectionFor = (segment: string): ArchiveCollection => {\n const collection = registered.get(segment);\n if (collection === undefined) throw new ApiError(404, 'not-found', 'No such route.');\n return collection;\n };\n\n async function handle(request: Request, env: E, ctx: ArchiveExecutionContext): Promise<Response> {\n const config = apiConfig(env);\n const url = new URL(request.url);\n const { pathname } = url;\n const method = request.method;\n\n if (!pathname.startsWith('/api/v1/')) throw new ApiError(404, 'not-found', 'No such route.');\n if (method === 'OPTIONS') return preflight(request, config);\n\n const match = matchRoute(pathname);\n // An unknown path inside the mount answers 404 like the paths outside it, and a\n // known route under an unsupported method answers 405 — both with no side\n // effects: no rate-limit spend, no session verification, no auth state leaked,\n // no guard-shaped error for a method the route does not serve.\n if (match === null) throw new ApiError(404, 'not-found', 'No such route.');\n if (!match.methods.includes(method)) throw notAllowed();\n\n // A malformed entry id is the same class of client bug: it answers 400 before\n // any rate-limit spend, session verification or auth state.\n if (ID_SEGMENT_ROUTES.has(match.id) && !UUID_PATH.test(match.segments[1] ?? '')) throw badRequest();\n\n // Health sits deliberately outside the rate-limit and session pipelines: uptime\n // probes must never burn the anonymous read budget or couple to authentication,\n // and it touches the database exactly once (the schema-version read).\n if (match.id === 'health') {\n return withCors(await healthJson(config, env), request, config);\n }\n if (match.id === 'reports') queryParams(url, ['cursor', 'limit', 'status']);\n\n await readLimit(config, env, request);\n\n // The same-origin mutation contract is proven BEFORE any identity work: a\n // foreign-origin write or missing marker must fail with 403 even while carrying a\n // garbage token — JWKS/JWT cost and 401 semantics stay behind the origin fence.\n const isMutation = method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS';\n if (isMutation) requireMutationGuard(request, config);\n\n // Session verification runs only where identity matters: private routes, the\n // moderation surfaces and every mutation. Public reads (list, detail, meta) never\n // invoke JWKS or the publisher lookup — browsing must not couple to authentication\n // availability. Sign-out is a cookie-clearing reply, not a session operation.\n const isPublicGet = method === 'GET' && (match.id === 'meta' || match.id === 'entries' || match.id === 'entry');\n const isSignOut = match.id === 'signOut' && method === 'POST';\n const session =\n isPublicGet || isSignOut ? null : await verifySession(request, config, env, match.id === 'accountErase');\n\n if (isMutation) {\n if (isSignOut) {\n // The shared `archiveOkSchema` body; every session cookie attribute is the\n // contract's, never ad hoc.\n const response = json(\n 200,\n { ok: true },\n {\n 'cache-control': 'no-store',\n 'set-cookie': expiredSessionCookie(config.appEnv === 'production'),\n },\n );\n return withCors(response, request, config);\n }\n const verified = requireSession(session);\n // The free fail-closed header precondition runs before the quota token is spent:\n // a stale tab's rejected writes must not consume the current account's budget.\n requireExpectedPublisher(request, verified, match.id === 'me' && method === 'POST');\n await writeLimit(config, env, verified.subject.issuer, verified.subject.subject);\n }\n\n const jsonWithCors = (response: Response) => withCors(response, request, config);\n\n switch (match.id) {\n case 'meta': {\n queryParams(url, []);\n return jsonWithCors(collectionMeta(collectionFor(match.segments[0] ?? '')));\n }\n case 'entries': {\n const collection = collectionFor(match.segments[0] ?? '');\n if (method === 'GET') {\n const response = await cachedPublicGet(\n request,\n ctx,\n () => listEntries(collection, request, env),\n () => emit({ diagnostic: { kind: 'cache' }, type: 'cache-error' }),\n );\n return jsonWithCors(response);\n }\n if (method === 'POST') {\n return jsonWithCors(await publishEntry(collection, request, env, config, requireSession(session)));\n }\n throw notAllowed();\n }\n case 'entry': {\n const collection = collectionFor(match.segments[0] ?? '');\n const id = match.segments[1] ?? '';\n if (method === 'GET') {\n queryParams(url, []);\n return jsonWithCors(await getEntry(collection, env, id));\n }\n if (method === 'PUT') {\n return jsonWithCors(await updateEntry(collection, request, env, config, requireSession(session), id));\n }\n if (method === 'DELETE') {\n return jsonWithCors(await deleteEntry(collection, env, requireSession(session), id));\n }\n throw notAllowed();\n }\n case 'like': {\n const collection = collectionFor(match.segments[0] ?? '');\n const id = match.segments[1] ?? '';\n if (method === 'PUT') {\n return jsonWithCors(await setLike(collection, env, config, requireSession(session), id, true));\n }\n if (method === 'DELETE') {\n return jsonWithCors(await setLike(collection, env, config, requireSession(session), id, false));\n }\n throw notAllowed();\n }\n case 'report': {\n if (method !== 'POST') throw notAllowed();\n const collection = collectionFor(match.segments[0] ?? '');\n return jsonWithCors(\n await reportEntry(collection, request, env, requireSession(session), match.segments[1] ?? ''),\n );\n }\n case 'hidden': {\n if (method !== 'PUT' && method !== 'DELETE') throw notAllowed();\n const collection = collectionFor(match.segments[0] ?? '');\n return jsonWithCors(\n await setHidden(collection, env, requireSession(session), match.segments[1] ?? '', method === 'PUT'),\n );\n }\n case 'reports': {\n // Private reads carry the same fail-closed precondition as writes: a stale tab\n // must not read another publisher's moderation queue after an account switch.\n const verified = requireSession(session);\n requireExpectedPublisher(request, verified, false);\n return jsonWithCors(await listReports(collectionFor(match.segments[0] ?? ''), request, env, verified));\n }\n case 'resolveReport': {\n queryParams(url, []);\n return jsonWithCors(\n await resolveReport(\n collectionFor(match.segments[0] ?? ''),\n env,\n requireSession(session),\n match.segments[1] ?? '',\n ),\n );\n }\n case 'modLog': {\n // The accountability read carries the same fail-closed precondition as every\n // private surface: a stale tab must not read the moderation history after an\n // account switch.\n const verified = requireSession(session);\n requireExpectedPublisher(request, verified, false);\n return jsonWithCors(await listModerationLog(request, env, verified));\n }\n case 'publisherErase': {\n // The terminal moderation action: erasing a publisher removes their row, and\n // FK cascades take the publications, likes and retry keys with it — the same\n // cascade a self-erase walks, applied to any publisher the operator names.\n if (method !== 'DELETE') throw notAllowed();\n return jsonWithCors(await erasePublisher(env, requireSession(session), match.segments[0] ?? ''));\n }\n case 'accountErase': {\n queryParams(url, []);\n return jsonWithCors(await deleteAccountProfile(env, requireSession(session)));\n }\n case 'me': {\n if (method === 'GET') return jsonWithCors(getMe(session));\n if (method === 'POST') return jsonWithCors(await registerMe(request, env, requireSession(session)));\n if (method === 'PUT') return jsonWithCors(await renameMe(request, env, requireSession(session)));\n if (method === 'DELETE') return jsonWithCors(await deleteProfile(env, requireSession(session)));\n throw notAllowed();\n }\n case 'mePublications':\n case 'meLikes':\n case 'meReports': {\n // Private reads carry the same fail-closed precondition as writes: a stale tab\n // must not consume another publisher's data after a shared-cookie account switch.\n const verified = requireSession(session);\n requireExpectedPublisher(request, verified, false);\n if (match.id === 'mePublications') {\n // The owner's entries are per-collection: the collection param names the\n // registered collection, and an absent or unregistered value is a malformed\n // request, not an empty list.\n const params = queryParams(url, ['collection']);\n const requested = params.get('collection');\n if (requested === null) throw badRequest();\n const collection = registered.get(requested);\n if (collection === undefined) throw badRequest();\n return jsonWithCors(await myPublications(collection, env, verified));\n }\n return jsonWithCors(\n await (match.id === 'meReports' ? myReports(request, env, verified) : myLikes(request, env, verified)),\n );\n }\n default:\n throw notAllowed();\n }\n }\n\n const tappers = new Set<(event: ArchiveEvent) => void>();\n const emit = (event: ArchiveEvent): void => {\n if (tappers.size === 0) return;\n for (const tapper of tappers) {\n try {\n tapper(event);\n } catch {\n // Observability must never affect the Worker.\n }\n }\n };\n\n return {\n async fetch(request: Request, env: E, ctx: ArchiveExecutionContext): Promise<Response> {\n const requestId = crypto.randomUUID();\n const route = routeLabel(new URL(request.url).pathname);\n const startedAt = Date.now();\n try {\n const response = await handle(request, env, ctx);\n let diagnostic: ArchiveDiagnostic | undefined;\n if (response.status === 503 && route === '/health') {\n const health = archiveHealthSchema.parse(await response.clone().json());\n diagnostic = { kind: health.reason === 'database-unavailable' ? 'database' : 'schema' };\n }\n emit({\n code: response.ok ? 'ok' : 'unavailable',\n ms: Date.now() - startedAt,\n requestId,\n route,\n status: response.status,\n type: 'request',\n ...(diagnostic === undefined ? {} : { diagnostic }),\n });\n return response;\n } catch (error) {\n const response = toErrorResponse(error, requestId);\n emit({\n code: error instanceof ApiError ? error.code : 'internal',\n diagnostic:\n error instanceof ApiError\n ? error.diagnostic\n : { kind: error instanceof ArchiveDerivationError ? 'collection' : 'internal' },\n ms: Date.now() - startedAt,\n requestId,\n route,\n status: response.status,\n type: 'request',\n });\n // Allowed origins see the same CORS contract on errors as on successes.\n try {\n return withCors(response, request, apiConfig(env));\n } catch {\n return response; // the config error itself is the response\n }\n }\n },\n\n async scheduled(_controller: unknown, env: E, _ctx: ArchiveExecutionContext): Promise<void> {\n try {\n const timestamp = Date.now();\n const results = await env.DB.batch([\n env.DB.prepare(SQL.expireRequests).bind(timestamp),\n env.DB.prepare(SQL.expireRevocations).bind(timestamp),\n ]);\n emit({\n removed: results.reduce((sum, result) => sum + (result.meta?.changes ?? 0), 0),\n status: 'ok',\n type: 'scheduled',\n });\n } catch (error) {\n emit({ diagnostic: { kind: 'database' }, status: 'failed', type: 'scheduled' });\n throw error;\n }\n },\n\n tap(handler, options) {\n const signal = options?.signal;\n if (signal?.aborted) return () => {};\n const off = () => {\n tappers.delete(handler);\n signal?.removeEventListener('abort', off);\n };\n tappers.add(handler);\n signal?.addEventListener('abort', off, { once: true });\n return off;\n },\n };\n}\n"],"mappings":"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"}
@@ -1,2 +1,353 @@
1
- import{ARCHIVE_LIMITS as e,PUBLISHER_HEADER as t,archiveHealthSchema as n}from"../contract.js";import{ApiError as r,badRequest as i,corsHeaders as a,expiredSessionCookie as o,json as s,preflight as c,queryParams as l,requireMutationGuard as u,toErrorResponse as d}from"./http.js";import{ArchiveDefinitionError as f,ArchiveDerivationError as p}from"../errors.js";import{SQL as m}from"./sql.js";import{requireSession as h,verifySession as g}from"./auth.js";import{apiConfig as _}from"./env.js";import{collectionMeta as v,deleteAccountProfile as y,deleteEntry as b,deleteProfile as x,erasePublisher as S,getEntry as C,getMe as w,healthJson as T,listEntries as E,listModerationLog as D,listReports as O,myLikes as k,myPublications as A,myReports as j,publishEntry as M,registerMe as N,renameMe as P,reportEntry as F,resolveReport as I,setHidden as L,setLike as R,updateEntry as z}from"./publications.js";var B=[{id:`accountErase`,label:`/me/account`,methods:[`DELETE`],pattern:/^\/api\/v1\/me\/account$/},{id:`health`,label:`/health`,methods:[`GET`],pattern:/^\/api\/v1\/health$/},{id:`signOut`,label:`/sign-out`,methods:[`POST`],pattern:/^\/api\/v1\/sign-out$/},{id:`resolveReport`,label:`/collections/:collection/reports/:id/resolved`,methods:[`PUT`],pattern:/^\/api\/v1\/collections\/([a-z0-9-]+)\/reports\/([0-9a-f-]{36})\/resolved$/i},{id:`meta`,label:`/collections/:collection`,methods:[`GET`],pattern:/^\/api\/v1\/collections\/([a-z0-9-]+)$/},{id:`reports`,label:`/collections/:collection/reports`,methods:[`GET`],pattern:/^\/api\/v1\/collections\/([a-z0-9-]+)\/reports$/},{id:`entries`,label:`/collections/:collection/entries`,methods:[`GET`,`POST`],pattern:/^\/api\/v1\/collections\/([a-z0-9-]+)\/entries$/},{id:`entry`,label:`/collections/:collection/entries/:id`,methods:[`GET`,`PUT`,`DELETE`],pattern:/^\/api\/v1\/collections\/([a-z0-9-]+)\/entries\/([0-9a-f-]{36})$/i},{id:`like`,label:`/collections/:collection/entries/:id/like`,methods:[`PUT`,`DELETE`],pattern:/^\/api\/v1\/collections\/([a-z0-9-]+)\/entries\/([0-9a-f-]{36})\/like$/i},{id:`report`,label:`/collections/:collection/entries/:id/report`,methods:[`POST`],pattern:/^\/api\/v1\/collections\/([a-z0-9-]+)\/entries\/([0-9a-f-]{36})\/report$/i},{id:`hidden`,label:`/collections/:collection/entries/:id/hidden`,methods:[`PUT`,`DELETE`],pattern:/^\/api\/v1\/collections\/([a-z0-9-]+)\/entries\/([0-9a-f-]{36})\/hidden$/i},{id:`modLog`,label:`/moderation-log`,methods:[`GET`],pattern:/^\/api\/v1\/moderation-log$/},{id:`publisherErase`,label:`/publishers/:id`,methods:[`DELETE`],pattern:/^\/api\/v1\/publishers\/([0-9a-f-]{36})$/i},{id:`me`,label:`/me`,methods:[`GET`,`POST`,`PUT`,`DELETE`],pattern:/^\/api\/v1\/me$/},{id:`mePublications`,label:`/me/publications`,methods:[`GET`],pattern:/^\/api\/v1\/me\/publications$/},{id:`meLikes`,label:`/me/likes`,methods:[`GET`],pattern:/^\/api\/v1\/me\/likes$/},{id:`meReports`,label:`/me/reports`,methods:[`GET`],pattern:/^\/api\/v1\/me\/reports$/}],matchRoute=e=>{for(let t of B){let n=e.match(t.pattern);if(n)return{id:t.id,label:t.label,methods:t.methods,segments:n.slice(1)}}return null},routeLabel=e=>matchRoute(e)?.label??`unknown`,V=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,H=new Set([`entry`,`like`,`report`,`hidden`,`resolveReport`]),notAllowed=()=>new r(405,`method-not-allowed`,`That method is not supported here.`),U=new WeakMap,rateLimitHash=async(e,t)=>{let n=U.get(e);n===void 0&&(n=crypto.subtle.importKey(`raw`,new TextEncoder().encode(e.rateLimitPepper),{hash:`SHA-256`,name:`HMAC`},!1,[`sign`]),U.set(e,n));let r=await crypto.subtle.sign(`HMAC`,await n,new TextEncoder().encode(t));return[...new Uint8Array(r)].map(e=>e.toString(16).padStart(2,`0`)).join(``)},W=60;async function readLimit(e,t,n){let i=n.headers.get(`cf-connecting-ip`)??(e.appEnv===`local`?`127.0.0.1`:null);if(i===null)throw new r(503,`unavailable`,`The service is temporarily unavailable.`);let{success:a}=await t.READ_LIMIT.limit({key:await rateLimitHash(e,i)});if(!a)throw new r(429,`rate-limited`,`Too many requests. Try again later.`,{retryAfterSeconds:W})}async function writeLimit(e,t,n,i){let{success:a}=await t.WRITE_LIMIT.limit({key:await rateLimitHash(e,JSON.stringify([n,i]))});if(!a)throw new r(429,`rate-limited`,`Too many requests. Try again later.`,{retryAfterSeconds:W})}async function cachedPublicGet(t,n,r,i){if(t.headers.get(`cookie`)!==null||t.headers.get(`authorization`)!==null)return r();let a=new URL(t.url);a.search=new URLSearchParams([...a.searchParams.entries()].sort(([e],[t])=>e<t?-1:+(e>t))).toString();let o=caches.default,s=new Request(a.toString(),{method:`GET`}),c=await o.match(s).catch(()=>{i()});if(c)return c;let l=await r();return l.headers.set(`cache-control`,`public, max-age=${e.publicCacheSeconds}, s-maxage=${e.publicCacheSeconds}`),n.waitUntil(o.put(s,l.clone()).catch(i)),l}var withCors=(e,t,n)=>{let r=a(t,n);if(Object.keys(r).length===0)return e;let i=new Response(e.body,e);for(let[e,t]of Object.entries(r))i.headers.set(e,t);return i};function requireExpectedPublisher(e,n,i){if(i||n.publisher===null)return;let a=e.headers.get(t);if(a===null||!V.test(a))throw new r(400,`bad-request`,`The request is not valid.`);if(a!==n.publisher.id)throw new r(409,`publisher-changed`,`The signed-in publisher changed since this request was prepared.`)}function createArchiveWorker(e){let t=new Map;for(let n of e){if(t.has(n.name))throw new f(`Duplicate archive collection: ${n.name}`);t.set(n.name,n)}let collectionFor=e=>{let n=t.get(e);if(n===void 0)throw new r(404,`not-found`,`No such route.`);return n};async function handle(e,n,a){let d=_(n),f=new URL(e.url),{pathname:p}=f,m=e.method;if(!p.startsWith(`/api/v1/`))throw new r(404,`not-found`,`No such route.`);if(m===`OPTIONS`)return c(e,d);let B=matchRoute(p);if(B===null)throw new r(404,`not-found`,`No such route.`);if(!B.methods.includes(m))throw notAllowed();if(H.has(B.id)&&!V.test(B.segments[1]??``))throw i();if(B.id===`health`)return withCors(await T(d,n),e,d);B.id===`reports`&&l(f,[`cursor`,`limit`,`status`]),await readLimit(d,n,e);let U=m!==`GET`&&m!==`HEAD`&&m!==`OPTIONS`;U&&u(e,d);let W=m===`GET`&&(B.id===`meta`||B.id===`entries`||B.id===`entry`),G=B.id===`signOut`&&m===`POST`,K=W||G?null:await g(e,d,n,B.id===`accountErase`);if(U){if(G)return withCors(s(200,{ok:!0},{"cache-control":`no-store`,"set-cookie":o(d.appEnv===`production`)}),e,d);let t=h(K);requireExpectedPublisher(e,t,B.id===`me`&&m===`POST`),await writeLimit(d,n,t.subject.issuer,t.subject.subject)}let jsonWithCors=t=>withCors(t,e,d);switch(B.id){case`meta`:return l(f,[]),jsonWithCors(v(collectionFor(B.segments[0]??``)));case`entries`:{let t=collectionFor(B.segments[0]??``);if(m===`GET`)return jsonWithCors(await cachedPublicGet(e,a,()=>E(t,e,n),()=>emit({diagnostic:{kind:`cache`},type:`cache-error`})));if(m===`POST`)return jsonWithCors(await M(t,e,n,d,h(K)));throw notAllowed()}case`entry`:{let t=collectionFor(B.segments[0]??``),r=B.segments[1]??``;if(m===`GET`)return l(f,[]),jsonWithCors(await C(t,n,r));if(m===`PUT`)return jsonWithCors(await z(t,e,n,d,h(K),r));if(m===`DELETE`)return jsonWithCors(await b(t,n,h(K),r));throw notAllowed()}case`like`:{let e=collectionFor(B.segments[0]??``),t=B.segments[1]??``;if(m===`PUT`)return jsonWithCors(await R(e,n,d,h(K),t,!0));if(m===`DELETE`)return jsonWithCors(await R(e,n,d,h(K),t,!1));throw notAllowed()}case`report`:{if(m!==`POST`)throw notAllowed();let t=collectionFor(B.segments[0]??``);return jsonWithCors(await F(t,e,n,h(K),B.segments[1]??``))}case`hidden`:{if(m!==`PUT`&&m!==`DELETE`)throw notAllowed();let e=collectionFor(B.segments[0]??``);return jsonWithCors(await L(e,n,h(K),B.segments[1]??``,m===`PUT`))}case`reports`:{let t=h(K);return requireExpectedPublisher(e,t,!1),jsonWithCors(await O(collectionFor(B.segments[0]??``),e,n,t))}case`resolveReport`:return l(f,[]),jsonWithCors(await I(collectionFor(B.segments[0]??``),n,h(K),B.segments[1]??``));case`modLog`:{let t=h(K);return requireExpectedPublisher(e,t,!1),jsonWithCors(await D(e,n,t))}case`publisherErase`:if(m!==`DELETE`)throw notAllowed();return jsonWithCors(await S(n,h(K),B.segments[0]??``));case`accountErase`:return l(f,[]),jsonWithCors(await y(n,h(K)));case`me`:if(m===`GET`)return jsonWithCors(w(K));if(m===`POST`)return jsonWithCors(await N(e,n,h(K)));if(m===`PUT`)return jsonWithCors(await P(e,n,h(K)));if(m===`DELETE`)return jsonWithCors(await x(n,h(K)));throw notAllowed();case`mePublications`:case`meLikes`:case`meReports`:{let r=h(K);if(requireExpectedPublisher(e,r,!1),B.id===`mePublications`){let e=l(f,[`collection`]).get(`collection`);if(e===null)throw i();let a=t.get(e);if(a===void 0)throw i();return jsonWithCors(await A(a,n,r))}return jsonWithCors(await(B.id===`meReports`?j(e,n,r):k(e,n,r)))}default:throw notAllowed()}}let a=new Set,emit=e=>{if(a.size!==0)for(let t of a)try{t(e)}catch{}};return{async fetch(e,t,i){let a=crypto.randomUUID(),o=routeLabel(new URL(e.url).pathname),s=Date.now();try{let r=await handle(e,t,i),c;return r.status===503&&o===`/health`&&(c={kind:n.parse(await r.clone().json()).reason===`database-unavailable`?`database`:`schema`}),emit({code:r.ok?`ok`:`unavailable`,ms:Date.now()-s,requestId:a,route:o,status:r.status,type:`request`,...c===void 0?{}:{diagnostic:c}}),r}catch(n){let i=d(n,a);emit({code:n instanceof r?n.code:`internal`,diagnostic:n instanceof r?n.diagnostic:{kind:n instanceof p?`collection`:`internal`},ms:Date.now()-s,requestId:a,route:o,status:i.status,type:`request`});try{return withCors(i,e,_(t))}catch{return i}}},async scheduled(e,t,n){try{let e=Date.now(),n=await t.DB.batch([t.DB.prepare(m.expireRequests).bind(e),t.DB.prepare(m.expireRevocations).bind(e)]);emit({removed:n.reduce((e,t)=>e+(t.meta?.changes??0),0),status:`ok`,type:`scheduled`})}catch(e){throw emit({diagnostic:{kind:`database`},status:`failed`,type:`scheduled`}),e}},tap(e,t){let n=t?.signal;if(n?.aborted)return()=>{};let off=()=>{a.delete(e),n?.removeEventListener(`abort`,off)};return a.add(e),n?.addEventListener(`abort`,off,{once:!0}),off}}}export{createArchiveWorker};
1
+ import { ARCHIVE_LIMITS as e, PUBLISHER_HEADER as t, archiveHealthSchema as n } from "../contract.js";
2
+ import { ApiError as r, badRequest as i, corsHeaders as a, expiredSessionCookie as o, json as s, preflight as c, queryParams as l, requireMutationGuard as u, toErrorResponse as d } from "./http.js";
3
+ import { ArchiveDefinitionError as f, ArchiveDerivationError as p } from "../errors.js";
4
+ import { SQL as m } from "./sql.js";
5
+ import { requireSession as h, verifySession as g } from "./auth.js";
6
+ import { apiConfig as _ } from "./env.js";
7
+ import { collectionMeta as v, deleteAccountProfile as y, deleteEntry as b, deleteProfile as x, erasePublisher as S, getEntry as C, getMe as w, healthJson as T, listEntries as E, listModerationLog as D, listReports as O, myLikes as k, myPublications as A, myReports as j, publishEntry as M, registerMe as N, renameMe as P, reportEntry as F, resolveReport as I, setHidden as L, setLike as R, updateEntry as z } from "./publications.js";
8
+ //#region src/worker/worker.ts
9
+ var B = [
10
+ {
11
+ id: "accountErase",
12
+ label: "/me/account",
13
+ methods: ["DELETE"],
14
+ pattern: /^\/api\/v1\/me\/account$/
15
+ },
16
+ {
17
+ id: "health",
18
+ label: "/health",
19
+ methods: ["GET"],
20
+ pattern: /^\/api\/v1\/health$/
21
+ },
22
+ {
23
+ id: "signOut",
24
+ label: "/sign-out",
25
+ methods: ["POST"],
26
+ pattern: /^\/api\/v1\/sign-out$/
27
+ },
28
+ {
29
+ id: "resolveReport",
30
+ label: "/collections/:collection/reports/:id/resolved",
31
+ methods: ["PUT"],
32
+ pattern: /^\/api\/v1\/collections\/([a-z0-9-]+)\/reports\/([0-9a-f-]{36})\/resolved$/i
33
+ },
34
+ {
35
+ id: "meta",
36
+ label: "/collections/:collection",
37
+ methods: ["GET"],
38
+ pattern: /^\/api\/v1\/collections\/([a-z0-9-]+)$/
39
+ },
40
+ {
41
+ id: "reports",
42
+ label: "/collections/:collection/reports",
43
+ methods: ["GET"],
44
+ pattern: /^\/api\/v1\/collections\/([a-z0-9-]+)\/reports$/
45
+ },
46
+ {
47
+ id: "entries",
48
+ label: "/collections/:collection/entries",
49
+ methods: ["GET", "POST"],
50
+ pattern: /^\/api\/v1\/collections\/([a-z0-9-]+)\/entries$/
51
+ },
52
+ {
53
+ id: "entry",
54
+ label: "/collections/:collection/entries/:id",
55
+ methods: [
56
+ "GET",
57
+ "PUT",
58
+ "DELETE"
59
+ ],
60
+ pattern: /^\/api\/v1\/collections\/([a-z0-9-]+)\/entries\/([0-9a-f-]{36})$/i
61
+ },
62
+ {
63
+ id: "like",
64
+ label: "/collections/:collection/entries/:id/like",
65
+ methods: ["PUT", "DELETE"],
66
+ pattern: /^\/api\/v1\/collections\/([a-z0-9-]+)\/entries\/([0-9a-f-]{36})\/like$/i
67
+ },
68
+ {
69
+ id: "report",
70
+ label: "/collections/:collection/entries/:id/report",
71
+ methods: ["POST"],
72
+ pattern: /^\/api\/v1\/collections\/([a-z0-9-]+)\/entries\/([0-9a-f-]{36})\/report$/i
73
+ },
74
+ {
75
+ id: "hidden",
76
+ label: "/collections/:collection/entries/:id/hidden",
77
+ methods: ["PUT", "DELETE"],
78
+ pattern: /^\/api\/v1\/collections\/([a-z0-9-]+)\/entries\/([0-9a-f-]{36})\/hidden$/i
79
+ },
80
+ {
81
+ id: "modLog",
82
+ label: "/moderation-log",
83
+ methods: ["GET"],
84
+ pattern: /^\/api\/v1\/moderation-log$/
85
+ },
86
+ {
87
+ id: "publisherErase",
88
+ label: "/publishers/:id",
89
+ methods: ["DELETE"],
90
+ pattern: /^\/api\/v1\/publishers\/([0-9a-f-]{36})$/i
91
+ },
92
+ {
93
+ id: "me",
94
+ label: "/me",
95
+ methods: [
96
+ "GET",
97
+ "POST",
98
+ "PUT",
99
+ "DELETE"
100
+ ],
101
+ pattern: /^\/api\/v1\/me$/
102
+ },
103
+ {
104
+ id: "mePublications",
105
+ label: "/me/publications",
106
+ methods: ["GET"],
107
+ pattern: /^\/api\/v1\/me\/publications$/
108
+ },
109
+ {
110
+ id: "meLikes",
111
+ label: "/me/likes",
112
+ methods: ["GET"],
113
+ pattern: /^\/api\/v1\/me\/likes$/
114
+ },
115
+ {
116
+ id: "meReports",
117
+ label: "/me/reports",
118
+ methods: ["GET"],
119
+ pattern: /^\/api\/v1\/me\/reports$/
120
+ }
121
+ ], matchRoute = (e) => {
122
+ for (let t of B) {
123
+ let n = e.match(t.pattern);
124
+ if (n) return {
125
+ id: t.id,
126
+ label: t.label,
127
+ methods: t.methods,
128
+ segments: n.slice(1)
129
+ };
130
+ }
131
+ return null;
132
+ }, routeLabel = (e) => matchRoute(e)?.label ?? "unknown", V = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, H = /* @__PURE__ */ new Set([
133
+ "entry",
134
+ "like",
135
+ "report",
136
+ "hidden",
137
+ "resolveReport"
138
+ ]), notAllowed = () => new r(405, "method-not-allowed", "That method is not supported here."), U = /* @__PURE__ */ new WeakMap(), rateLimitHash = async (e, t) => {
139
+ let n = U.get(e);
140
+ n === void 0 && (n = crypto.subtle.importKey("raw", new TextEncoder().encode(e.rateLimitPepper), {
141
+ hash: "SHA-256",
142
+ name: "HMAC"
143
+ }, !1, ["sign"]), U.set(e, n));
144
+ let r = await crypto.subtle.sign("HMAC", await n, new TextEncoder().encode(t));
145
+ return [...new Uint8Array(r)].map((e) => e.toString(16).padStart(2, "0")).join("");
146
+ }, W = 60;
147
+ async function readLimit(e, t, n) {
148
+ let i = n.headers.get("cf-connecting-ip") ?? (e.appEnv === "local" ? "127.0.0.1" : null);
149
+ if (i === null) throw new r(503, "unavailable", "The service is temporarily unavailable.");
150
+ let { success: a } = await t.READ_LIMIT.limit({ key: await rateLimitHash(e, i) });
151
+ if (!a) throw new r(429, "rate-limited", "Too many requests. Try again later.", { retryAfterSeconds: W });
152
+ }
153
+ async function writeLimit(e, t, n, i) {
154
+ let { success: a } = await t.WRITE_LIMIT.limit({ key: await rateLimitHash(e, JSON.stringify([n, i])) });
155
+ if (!a) throw new r(429, "rate-limited", "Too many requests. Try again later.", { retryAfterSeconds: W });
156
+ }
157
+ async function cachedPublicGet(t, n, r, i) {
158
+ if (t.headers.get("cookie") !== null || t.headers.get("authorization") !== null) return r();
159
+ let a = new URL(t.url);
160
+ a.search = new URLSearchParams([...a.searchParams.entries()].sort(([e], [t]) => e < t ? -1 : +(e > t))).toString();
161
+ let o = caches.default, s = new Request(a.toString(), { method: "GET" }), c = await o.match(s).catch(() => {
162
+ i();
163
+ });
164
+ if (c) return c;
165
+ let l = await r();
166
+ return l.headers.set("cache-control", `public, max-age=${e.publicCacheSeconds}, s-maxage=${e.publicCacheSeconds}`), n.waitUntil(o.put(s, l.clone()).catch(i)), l;
167
+ }
168
+ var withCors = (e, t, n) => {
169
+ let r = a(t, n);
170
+ if (Object.keys(r).length === 0) return e;
171
+ let i = new Response(e.body, e);
172
+ for (let [e, t] of Object.entries(r)) i.headers.set(e, t);
173
+ return i;
174
+ };
175
+ function requireExpectedPublisher(e, n, i) {
176
+ if (i || n.publisher === null) return;
177
+ let a = e.headers.get(t);
178
+ if (a === null || !V.test(a)) throw new r(400, "bad-request", "The request is not valid.");
179
+ if (a !== n.publisher.id) throw new r(409, "publisher-changed", "The signed-in publisher changed since this request was prepared.");
180
+ }
181
+ function createArchiveWorker(e) {
182
+ let t = /* @__PURE__ */ new Map();
183
+ for (let n of e) {
184
+ if (t.has(n.name)) throw new f(`Duplicate archive collection: ${n.name}`);
185
+ t.set(n.name, n);
186
+ }
187
+ let collectionFor = (e) => {
188
+ let n = t.get(e);
189
+ if (n === void 0) throw new r(404, "not-found", "No such route.");
190
+ return n;
191
+ };
192
+ async function handle(e, n, a) {
193
+ let d = _(n), f = new URL(e.url), { pathname: p } = f, m = e.method;
194
+ if (!p.startsWith("/api/v1/")) throw new r(404, "not-found", "No such route.");
195
+ if (m === "OPTIONS") return c(e, d);
196
+ let B = matchRoute(p);
197
+ if (B === null) throw new r(404, "not-found", "No such route.");
198
+ if (!B.methods.includes(m)) throw notAllowed();
199
+ if (H.has(B.id) && !V.test(B.segments[1] ?? "")) throw i();
200
+ if (B.id === "health") return withCors(await T(d, n), e, d);
201
+ B.id === "reports" && l(f, [
202
+ "cursor",
203
+ "limit",
204
+ "status"
205
+ ]), await readLimit(d, n, e);
206
+ let U = m !== "GET" && m !== "HEAD" && m !== "OPTIONS";
207
+ U && u(e, d);
208
+ let W = m === "GET" && (B.id === "meta" || B.id === "entries" || B.id === "entry"), G = B.id === "signOut" && m === "POST", K = W || G ? null : await g(e, d, n, B.id === "accountErase");
209
+ if (U) {
210
+ if (G) return withCors(s(200, { ok: !0 }, {
211
+ "cache-control": "no-store",
212
+ "set-cookie": o(d.appEnv === "production")
213
+ }), e, d);
214
+ let t = h(K);
215
+ requireExpectedPublisher(e, t, B.id === "me" && m === "POST"), await writeLimit(d, n, t.subject.issuer, t.subject.subject);
216
+ }
217
+ let jsonWithCors = (t) => withCors(t, e, d);
218
+ switch (B.id) {
219
+ case "meta": return l(f, []), jsonWithCors(v(collectionFor(B.segments[0] ?? "")));
220
+ case "entries": {
221
+ let t = collectionFor(B.segments[0] ?? "");
222
+ if (m === "GET") return jsonWithCors(await cachedPublicGet(e, a, () => E(t, e, n), () => emit({
223
+ diagnostic: { kind: "cache" },
224
+ type: "cache-error"
225
+ })));
226
+ if (m === "POST") return jsonWithCors(await M(t, e, n, d, h(K)));
227
+ throw notAllowed();
228
+ }
229
+ case "entry": {
230
+ let t = collectionFor(B.segments[0] ?? ""), r = B.segments[1] ?? "";
231
+ if (m === "GET") return l(f, []), jsonWithCors(await C(t, n, r));
232
+ if (m === "PUT") return jsonWithCors(await z(t, e, n, d, h(K), r));
233
+ if (m === "DELETE") return jsonWithCors(await b(t, n, h(K), r));
234
+ throw notAllowed();
235
+ }
236
+ case "like": {
237
+ let e = collectionFor(B.segments[0] ?? ""), t = B.segments[1] ?? "";
238
+ if (m === "PUT") return jsonWithCors(await R(e, n, d, h(K), t, !0));
239
+ if (m === "DELETE") return jsonWithCors(await R(e, n, d, h(K), t, !1));
240
+ throw notAllowed();
241
+ }
242
+ case "report": {
243
+ if (m !== "POST") throw notAllowed();
244
+ let t = collectionFor(B.segments[0] ?? "");
245
+ return jsonWithCors(await F(t, e, n, h(K), B.segments[1] ?? ""));
246
+ }
247
+ case "hidden": {
248
+ if (m !== "PUT" && m !== "DELETE") throw notAllowed();
249
+ let e = collectionFor(B.segments[0] ?? "");
250
+ return jsonWithCors(await L(e, n, h(K), B.segments[1] ?? "", m === "PUT"));
251
+ }
252
+ case "reports": {
253
+ let t = h(K);
254
+ return requireExpectedPublisher(e, t, !1), jsonWithCors(await O(collectionFor(B.segments[0] ?? ""), e, n, t));
255
+ }
256
+ case "resolveReport": return l(f, []), jsonWithCors(await I(collectionFor(B.segments[0] ?? ""), n, h(K), B.segments[1] ?? ""));
257
+ case "modLog": {
258
+ let t = h(K);
259
+ return requireExpectedPublisher(e, t, !1), jsonWithCors(await D(e, n, t));
260
+ }
261
+ case "publisherErase":
262
+ if (m !== "DELETE") throw notAllowed();
263
+ return jsonWithCors(await S(n, h(K), B.segments[0] ?? ""));
264
+ case "accountErase": return l(f, []), jsonWithCors(await y(n, h(K)));
265
+ case "me":
266
+ if (m === "GET") return jsonWithCors(w(K));
267
+ if (m === "POST") return jsonWithCors(await N(e, n, h(K)));
268
+ if (m === "PUT") return jsonWithCors(await P(e, n, h(K)));
269
+ if (m === "DELETE") return jsonWithCors(await x(n, h(K)));
270
+ throw notAllowed();
271
+ case "mePublications":
272
+ case "meLikes":
273
+ case "meReports": {
274
+ let r = h(K);
275
+ if (requireExpectedPublisher(e, r, !1), B.id === "mePublications") {
276
+ let e = l(f, ["collection"]).get("collection");
277
+ if (e === null) throw i();
278
+ let a = t.get(e);
279
+ if (a === void 0) throw i();
280
+ return jsonWithCors(await A(a, n, r));
281
+ }
282
+ return jsonWithCors(await (B.id === "meReports" ? j(e, n, r) : k(e, n, r)));
283
+ }
284
+ default: throw notAllowed();
285
+ }
286
+ }
287
+ let a = /* @__PURE__ */ new Set(), emit = (e) => {
288
+ if (a.size !== 0) for (let t of a) try {
289
+ t(e);
290
+ } catch {}
291
+ };
292
+ return {
293
+ async fetch(e, t, i) {
294
+ let a = crypto.randomUUID(), o = routeLabel(new URL(e.url).pathname), s = Date.now();
295
+ try {
296
+ let r = await handle(e, t, i), c;
297
+ return r.status === 503 && o === "/health" && (c = { kind: n.parse(await r.clone().json()).reason === "database-unavailable" ? "database" : "schema" }), emit({
298
+ code: r.ok ? "ok" : "unavailable",
299
+ ms: Date.now() - s,
300
+ requestId: a,
301
+ route: o,
302
+ status: r.status,
303
+ type: "request",
304
+ ...c === void 0 ? {} : { diagnostic: c }
305
+ }), r;
306
+ } catch (n) {
307
+ let i = d(n, a);
308
+ emit({
309
+ code: n instanceof r ? n.code : "internal",
310
+ diagnostic: n instanceof r ? n.diagnostic : { kind: n instanceof p ? "collection" : "internal" },
311
+ ms: Date.now() - s,
312
+ requestId: a,
313
+ route: o,
314
+ status: i.status,
315
+ type: "request"
316
+ });
317
+ try {
318
+ return withCors(i, e, _(t));
319
+ } catch {
320
+ return i;
321
+ }
322
+ }
323
+ },
324
+ async scheduled(e, t, n) {
325
+ try {
326
+ let e = Date.now(), n = await t.DB.batch([t.DB.prepare(m.expireRequests).bind(e), t.DB.prepare(m.expireRevocations).bind(e)]);
327
+ emit({
328
+ removed: n.reduce((e, t) => e + (t.meta?.changes ?? 0), 0),
329
+ status: "ok",
330
+ type: "scheduled"
331
+ });
332
+ } catch (e) {
333
+ throw emit({
334
+ diagnostic: { kind: "database" },
335
+ status: "failed",
336
+ type: "scheduled"
337
+ }), e;
338
+ }
339
+ },
340
+ tap(e, t) {
341
+ let n = t?.signal;
342
+ if (n?.aborted) return () => {};
343
+ let off = () => {
344
+ a.delete(e), n?.removeEventListener("abort", off);
345
+ };
346
+ return a.add(e), n?.addEventListener("abort", off, { once: !0 }), off;
347
+ }
348
+ };
349
+ }
350
+ //#endregion
351
+ export { createArchiveWorker };
352
+
2
353
  //# sourceMappingURL=worker.js.map