@vielzeug/archive 26.11.12 → 26.11.13

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 (90) hide show
  1. package/dist/_server/d1.d.ts +23 -0
  2. package/dist/_server/d1.d.ts.map +1 -0
  3. package/dist/_server/execution.d.ts +5 -0
  4. package/dist/_server/execution.d.ts.map +1 -0
  5. package/dist/_server/http.cjs +2 -0
  6. package/dist/_server/http.cjs.map +1 -0
  7. package/dist/{worker → _server}/http.d.ts +6 -11
  8. package/dist/_server/http.d.ts.map +1 -0
  9. package/dist/{worker → _server}/http.js +18 -44
  10. package/dist/_server/http.js.map +1 -0
  11. package/dist/{worker/_service.cjs → _server/service.cjs} +1 -1
  12. package/dist/{worker/_service.js.map → _server/service.cjs.map} +1 -1
  13. package/dist/{worker/_service.d.ts → _server/service.d.ts} +1 -1
  14. package/dist/_server/service.d.ts.map +1 -0
  15. package/dist/{worker/_service.js → _server/service.js} +2 -2
  16. package/dist/_server/service.js.map +1 -0
  17. package/dist/_server/uuid.cjs +2 -0
  18. package/dist/_server/uuid.cjs.map +1 -0
  19. package/dist/_server/uuid.d.ts +3 -0
  20. package/dist/_server/uuid.d.ts.map +1 -0
  21. package/dist/_server/uuid.js +5 -0
  22. package/dist/_server/uuid.js.map +1 -0
  23. package/dist/auth/_confirmation.cjs +3 -3
  24. package/dist/auth/_confirmation.cjs.map +1 -1
  25. package/dist/auth/_confirmation.js +4 -4
  26. package/dist/auth/_confirmation.js.map +1 -1
  27. package/dist/auth/config.cjs.map +1 -1
  28. package/dist/auth/config.d.ts +1 -1
  29. package/dist/auth/config.js.map +1 -1
  30. package/dist/auth/errors.cjs +1 -1
  31. package/dist/auth/errors.cjs.map +1 -1
  32. package/dist/auth/errors.d.ts +1 -1
  33. package/dist/auth/errors.d.ts.map +1 -1
  34. package/dist/auth/errors.js +1 -1
  35. package/dist/auth/errors.js.map +1 -1
  36. package/dist/auth/store.cjs.map +1 -1
  37. package/dist/auth/store.d.ts +1 -1
  38. package/dist/auth/store.js.map +1 -1
  39. package/dist/auth/worker.cjs +1 -1
  40. package/dist/auth/worker.cjs.map +1 -1
  41. package/dist/auth/worker.d.ts +1 -1
  42. package/dist/auth/worker.d.ts.map +1 -1
  43. package/dist/auth/worker.js +153 -152
  44. package/dist/auth/worker.js.map +1 -1
  45. package/dist/errors.cjs +1 -1
  46. package/dist/errors.cjs.map +1 -1
  47. package/dist/errors.d.ts.map +1 -1
  48. package/dist/errors.js +1 -1
  49. package/dist/errors.js.map +1 -1
  50. package/dist/worker/_cors.cjs +2 -0
  51. package/dist/worker/_cors.cjs.map +1 -0
  52. package/dist/worker/_cors.d.ts +13 -0
  53. package/dist/worker/_cors.d.ts.map +1 -0
  54. package/dist/worker/_cors.js +32 -0
  55. package/dist/worker/_cors.js.map +1 -0
  56. package/dist/worker/_pagination.cjs +1 -1
  57. package/dist/worker/_pagination.cjs.map +1 -1
  58. package/dist/worker/_pagination.js +18 -18
  59. package/dist/worker/_pagination.js.map +1 -1
  60. package/dist/worker/auth.cjs +1 -1
  61. package/dist/worker/auth.cjs.map +1 -1
  62. package/dist/worker/auth.d.ts.map +1 -1
  63. package/dist/worker/auth.js +18 -18
  64. package/dist/worker/auth.js.map +1 -1
  65. package/dist/worker/env.cjs +1 -1
  66. package/dist/worker/env.cjs.map +1 -1
  67. package/dist/worker/env.d.ts +1 -22
  68. package/dist/worker/env.d.ts.map +1 -1
  69. package/dist/worker/env.js +1 -1
  70. package/dist/worker/env.js.map +1 -1
  71. package/dist/worker/publications.cjs +1 -1
  72. package/dist/worker/publications.cjs.map +1 -1
  73. package/dist/worker/publications.d.ts.map +1 -1
  74. package/dist/worker/publications.js +263 -262
  75. package/dist/worker/publications.js.map +1 -1
  76. package/dist/worker/worker.cjs +1 -1
  77. package/dist/worker/worker.cjs.map +1 -1
  78. package/dist/worker/worker.d.ts +2 -4
  79. package/dist/worker/worker.d.ts.map +1 -1
  80. package/dist/worker/worker.js +124 -122
  81. package/dist/worker/worker.js.map +1 -1
  82. package/dist/worker.d.ts +5 -3
  83. package/dist/worker.d.ts.map +1 -1
  84. package/package.json +2 -2
  85. package/dist/worker/_service.cjs.map +0 -1
  86. package/dist/worker/_service.d.ts.map +0 -1
  87. package/dist/worker/http.cjs +0 -2
  88. package/dist/worker/http.cjs.map +0 -1
  89. package/dist/worker/http.d.ts.map +0 -1
  90. package/dist/worker/http.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"auth.js","names":["JWT_MAX_AGE_SECONDS","SESSION_MAX_AGE_SECONDS","SUBJECT_MAX_CHARS","ApiError","token","sessionToken","request","payload","jwtVerify","config","error","errors","unauthenticated","subject","issuer","iat","roles","operator","isWellFormedText","allowErasureRetry","env","SQL","serviceRequest","body","response","archiveOkSchema","failure","archiveErrorResponseSchema","row","session"],"sources":["../../src/worker/auth.ts"],"sourcesContent":["import { errors, type JWTPayload, jwtVerify } from 'jose';\nimport {\n ARCHIVE_OPERATOR_ROLE,\n archiveErrorResponseSchema,\n archiveOkSchema,\n isWellFormedText,\n OPERATOR_SESSION_MAX_AGE_SECONDS,\n SESSION_MAX_AGE_SECONDS,\n} from '../contract';\nimport { serviceRequest } from './_service';\nimport type { ApiConfig, ArchiveWorkerEnv } from './env';\nimport { ApiError, sessionToken, unauthenticated } from './http';\nimport { SQL } from './sql';\n\n/**\n * Session authentication. The Worker trusts exactly one source: the archive_session\n * HttpOnly cookie holding an RS256 JWT from the configured issuer. Tokens are verified\n * before any database read and are never echoed into DTOs; the publisher row maps the\n * opaque (issuer, subject) pair to the public publisher id. An optional `roles` claim\n * from the same trusted issuer grants operator authority (moderation routes).\n */\nexport interface VerifiedSubject {\n issuer: string;\n subject: string;\n}\n\nexport interface Publisher {\n displayName: string;\n id: string;\n}\n\nexport interface Session {\n /** True only when the verified token's `roles` claim carries the operator role. */\n operator: boolean;\n /** The registered publisher row; null while the verified user has not registered. */\n publisher: Publisher | null;\n subject: VerifiedSubject;\n}\n\nconst JWT_MAX_AGE_SECONDS = SESSION_MAX_AGE_SECONDS;\nexport const JWT_CLOCK_TOLERANCE_SECONDS = 5;\nconst SUBJECT_MAX_CHARS = 256;\n\nconst authUnavailable = (): ApiError => new ApiError(503, 'auth-unavailable', 'Sign-in is currently unavailable.');\n\n/**\n * Verifies the session cookie; returns null for anonymous requests.\n * Token/signature/claim problems are 401; JWKS provider faults (fetch failure,\n * timeout, undecodable document) are 503 `auth-unavailable` — an outage is not a\n * reason to treat the caller as signed out.\n */\nexport async function verifySession(\n request: Request,\n config: ApiConfig,\n env: ArchiveWorkerEnv,\n allowErasureRetry = false,\n): Promise<Session | null> {\n const token = sessionToken(request);\n if (token === null) return null;\n\n let payload: JWTPayload;\n try {\n const result = await jwtVerify(token, config.jwks, {\n algorithms: ['RS256'],\n audience: config.audience,\n clockTolerance: JWT_CLOCK_TOLERANCE_SECONDS,\n issuer: config.issuer,\n maxTokenAge: JWT_MAX_AGE_SECONDS,\n requiredClaims: ['sub', 'iss', 'aud', 'iat', 'exp', 'sessionVersion'],\n });\n payload = result.payload;\n } catch (error) {\n // Provider faults are 503: a JWKS outage must never read as a signed-out user.\n if (\n error instanceof errors.JWKSTimeout ||\n error instanceof errors.JWKSInvalid ||\n error instanceof errors.JWKSMultipleMatchingKeys\n ) {\n throw authUnavailable();\n }\n // Everything else in the JOSE family — malformed, expired, wrong alg/aud/iss,\n // bad signature, no matching key — is a token problem.\n if (error instanceof errors.JOSEError) throw unauthenticated();\n // Network fetch failures surface outside the JOSE error family.\n throw authUnavailable();\n }\n const subject = payload.sub;\n const issuer = payload.iss;\n const iat = payload.iat;\n const roles = payload.roles;\n if (!Number.isSafeInteger(payload.sessionVersion) || (payload.sessionVersion as number) < 0) throw unauthenticated();\n // Least authority: a malformed roles claim grants nothing, and operator authority\n // only ever comes from this verified, audience- and issuer-checked token.\n const operator = Array.isArray(roles) && roles.includes(ARCHIVE_OPERATOR_ROLE);\n // Malformed subjects must reject outright: lone surrogates/control characters\n // could otherwise collapse distinct claims into the same stored identity.\n if (\n typeof subject !== 'string' ||\n subject.length === 0 ||\n subject.length > SUBJECT_MAX_CHARS ||\n !isWellFormedText(subject)\n ) {\n throw unauthenticated();\n }\n if (typeof issuer !== 'string') throw unauthenticated();\n // A token claiming to be issued in the future beyond clock tolerance is a claims failure.\n if (typeof iat === 'number' && iat * 1000 > Date.now() + JWT_CLOCK_TOLERANCE_SECONDS * 1000) {\n throw unauthenticated();\n }\n if (\n operator &&\n (typeof iat !== 'number' ||\n typeof payload.exp !== 'number' ||\n payload.exp - iat > OPERATOR_SESSION_MAX_AGE_SECONDS ||\n Date.now() / 1000 - iat > OPERATOR_SESSION_MAX_AGE_SECONDS + JWT_CLOCK_TOLERANCE_SECONDS)\n )\n throw unauthenticated();\n\n // Only a previously tombstoned principal can bypass live validation to finish\n // partial erasure. Its profile cannot be re-created while the tombstone lives.\n const erasureRetry =\n allowErasureRetry && (await env.DB.prepare(SQL.revokedSubject).bind(issuer, subject, Date.now()).first()) !== null;\n if (!erasureRetry) {\n try {\n await serviceRequest(\n env.AUTH_ISSUER_SERVICE,\n new Request(new URL('/api/v1/auth/session', config.issuer), {\n headers: { cookie: `archive_session=${token}` },\n redirect: 'manual',\n }),\n async (response) => {\n const body: unknown = await response.json();\n if (response.status === 200 && archiveOkSchema.safeParse(body).success) return;\n const failure = archiveErrorResponseSchema.safeParse(body);\n if (response.status === 401 && failure.success && failure.data.error.code === 'unauthenticated')\n throw unauthenticated();\n throw authUnavailable();\n },\n );\n } catch (error) {\n if (error instanceof ApiError && error.status === 401) throw error;\n throw authUnavailable();\n }\n }\n if (!allowErasureRetry) {\n const revoked = await env.DB.prepare(SQL.revokedSubject).bind(issuer, subject, Date.now()).first();\n if (revoked !== null) throw unauthenticated();\n }\n const row = await env.DB.prepare(SQL.publisher).bind(issuer, subject).first<{\n display_name: string;\n id: string;\n }>();\n return {\n operator,\n publisher: row === null ? null : { displayName: row.display_name, id: row.id },\n subject: { issuer, subject },\n };\n}\n\n/** Session required for private routes; anonymous gets 401. */\nexport function requireSession(session: Session | null): Session {\n if (session === null) throw unauthenticated();\n return session;\n}\n"],"mappings":";;;;;;AAuCA,IAAMA,IAAsBC,GAEtBC,IAAoB,KAEpB,wBAAkC,IAAIC,EAAS,KAAK,oBAAoB,mCAAmC;AAQjH,eAAsB,cACpB,GACA,GACA,GACA,IAAoB,IACK;CACzB,IAAMC,IAAQC,EAAaC,CAAO;CAClC,IAAIF,MAAU,MAAM,OAAO;CAE3B,IAAIG;CACJ,IAAI;EASF,KAAU,MARWC,EAAUJ,GAAOK,EAAO,MAAM;GACjD,YAAY,CAAC,OAAO;GACpB,UAAUA,EAAO;GACjB,gBAAA;GACA,QAAQA,EAAO;GACf,aAAaT;GACb,gBAAgB;IAAC;IAAO;IAAO;IAAO;IAAO;IAAO;GAAgB;EACtE,CAAC,EAAA,CACgB;CACnB,SAASU,GAAO;EAad,MAVEA,aAAiBC,EAAO,eACxBD,aAAiBC,EAAO,eACxBD,aAAiBC,EAAO,2BAElB,gBAAgB,IAIpBD,aAAiBC,EAAO,YAAiBC,EAAgB,IAEvD,gBAAgB;CACxB;CACA,IAAMC,IAAUN,EAAQ,KAClBO,IAASP,EAAQ,KACjBQ,IAAMR,EAAQ,KACdS,IAAQT,EAAQ;CACtB,IAAI,CAAC,OAAO,cAAcA,EAAQ,cAAc,KAAMA,EAAQ,iBAA4B,GAAG,MAAMK,EAAgB;CAGnH,IAAMK,IAAW,MAAM,QAAQD,CAAK,KAAKA,EAAM,SAAA,kBAA8B;CAgB7E,IAZE,OAAOH,KAAY,YACnBA,EAAQ,WAAW,KACnBA,EAAQ,SAASX,KACjB,CAACgB,EAAiBL,CAAO,KAIvB,OAAOC,KAAW,YAElB,OAAOC,KAAQ,YAAYA,IAAM,MAAO,KAAK,IAAI,IAAA,OAInDE,MACC,OAAOF,KAAQ,YACd,OAAOR,EAAQ,OAAQ,YACvBA,EAAQ,MAAMQ,IAAA,QACd,KAAK,IAAI,IAAI,MAAOA,IAAA,OAEtB,MAAMH,EAAgB;CAMxB,IAAI,EADFO,KAAsB,MAAMC,EAAI,GAAG,QAAQC,EAAI,cAAc,CAAC,CAAC,KAAKP,GAAQD,GAAS,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,MAAO,OAE9G,IAAI;EACF,MAAMS,EACJF,EAAI,qBACJ,IAAI,QAAQ,IAAI,IAAI,wBAAwBX,EAAO,MAAM,GAAG;GAC1D,SAAS,EAAE,QAAQ,mBAAmBL,IAAQ;GAC9C,UAAU;EACZ,CAAC,GACD,OAAO,MAAa;GAClB,IAAMmB,IAAgB,MAAMC,EAAS,KAAK;GAC1C,IAAIA,EAAS,WAAW,OAAOC,EAAgB,UAAUF,CAAI,CAAC,CAAC,SAAS;GACxE,IAAMG,IAAUC,EAA2B,UAAUJ,CAAI;GAGzD,MAFIC,EAAS,WAAW,OAAOE,EAAQ,WAAWA,EAAQ,KAAK,MAAM,SAAS,oBACtEd,EAAgB,IAClB,gBAAgB;EACxB,CACF;CACF,SAASF,GAAO;EAEd,MADIA,aAAiBP,KAAYO,EAAM,WAAW,MAAWA,IACvD,gBAAgB;CACxB;CAEF,IAAI,CAACS,KAEC,MADkBC,EAAI,GAAG,QAAQC,EAAI,cAAc,CAAC,CAAC,KAAKP,GAAQD,GAAS,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,MACjF,MAAM,MAAMD,EAAgB;CAE9C,IAAMgB,IAAM,MAAMR,EAAI,GAAG,QAAQC,EAAI,SAAS,CAAC,CAAC,KAAKP,GAAQD,CAAO,CAAC,CAAC,MAGnE;CACH,OAAO;EACL;EACA,WAAWe,MAAQ,OAAO,OAAO;GAAE,aAAaA,EAAI;GAAc,IAAIA,EAAI;EAAG;EAC7E,SAAS;GAAE;GAAQ;EAAQ;CAC7B;AACF;AAGA,SAAgB,eAAe,GAAkC;CAC/D,IAAIC,MAAY,MAAM,MAAMjB,EAAgB;CAC5C,OAAOiB;AACT"}
1
+ {"version":3,"file":"auth.js","names":["JWT_MAX_AGE_SECONDS","SESSION_MAX_AGE_SECONDS","SUBJECT_MAX_CHARS","ApiError","token","sessionToken","request","payload","jwtVerify","config","error","errors","unauthenticated","subject","issuer","iat","roles","operator","isWellFormedText","allowErasureRetry","env","SQL","serviceRequest","body","response","archiveOkSchema","failure","archiveErrorResponseSchema","row","session"],"sources":["../../src/worker/auth.ts"],"sourcesContent":["import { errors, type JWTPayload, jwtVerify } from 'jose';\nimport { ApiError, sessionToken, unauthenticated } from '../_server/http';\nimport { serviceRequest } from '../_server/service';\nimport {\n ARCHIVE_OPERATOR_ROLE,\n archiveErrorResponseSchema,\n archiveOkSchema,\n isWellFormedText,\n OPERATOR_SESSION_MAX_AGE_SECONDS,\n SESSION_MAX_AGE_SECONDS,\n} from '../contract';\nimport type { ApiConfig, ArchiveWorkerEnv } from './env';\nimport { SQL } from './sql';\n\n/**\n * Session authentication. The Worker trusts exactly one source: the archive_session\n * HttpOnly cookie holding an RS256 JWT from the configured issuer. Tokens are verified\n * before any database read and are never echoed into DTOs; the publisher row maps the\n * opaque (issuer, subject) pair to the public publisher id. An optional `roles` claim\n * from the same trusted issuer grants operator authority (moderation routes).\n */\nexport interface VerifiedSubject {\n issuer: string;\n subject: string;\n}\n\nexport interface Publisher {\n displayName: string;\n id: string;\n}\n\nexport interface Session {\n /** True only when the verified token's `roles` claim carries the operator role. */\n operator: boolean;\n /** The registered publisher row; null while the verified user has not registered. */\n publisher: Publisher | null;\n subject: VerifiedSubject;\n}\n\nconst JWT_MAX_AGE_SECONDS = SESSION_MAX_AGE_SECONDS;\nexport const JWT_CLOCK_TOLERANCE_SECONDS = 5;\nconst SUBJECT_MAX_CHARS = 256;\n\nconst authUnavailable = (): ApiError => new ApiError(503, 'auth-unavailable', 'Sign-in is currently unavailable.');\n\n/**\n * Verifies the session cookie; returns null for anonymous requests.\n * Token/signature/claim problems are 401; JWKS provider faults (fetch failure,\n * timeout, undecodable document) are 503 `auth-unavailable` — an outage is not a\n * reason to treat the caller as signed out.\n */\nexport async function verifySession(\n request: Request,\n config: ApiConfig,\n env: ArchiveWorkerEnv,\n allowErasureRetry = false,\n): Promise<Session | null> {\n const token = sessionToken(request);\n if (token === null) return null;\n\n let payload: JWTPayload;\n try {\n const result = await jwtVerify(token, config.jwks, {\n algorithms: ['RS256'],\n audience: config.audience,\n clockTolerance: JWT_CLOCK_TOLERANCE_SECONDS,\n issuer: config.issuer,\n maxTokenAge: JWT_MAX_AGE_SECONDS,\n requiredClaims: ['sub', 'iss', 'aud', 'iat', 'exp', 'sessionVersion'],\n });\n payload = result.payload;\n } catch (error) {\n // Provider faults are 503: a JWKS outage must never read as a signed-out user.\n if (\n error instanceof errors.JWKSTimeout ||\n error instanceof errors.JWKSInvalid ||\n error instanceof errors.JWKSMultipleMatchingKeys\n ) {\n throw authUnavailable();\n }\n // Everything else in the JOSE family — malformed, expired, wrong alg/aud/iss,\n // bad signature, no matching key — is a token problem.\n if (error instanceof errors.JOSEError) throw unauthenticated();\n // Network fetch failures surface outside the JOSE error family.\n throw authUnavailable();\n }\n const subject = payload.sub;\n const issuer = payload.iss;\n const iat = payload.iat;\n const roles = payload.roles;\n if (!Number.isSafeInteger(payload.sessionVersion) || (payload.sessionVersion as number) < 0) throw unauthenticated();\n // Least authority: a malformed roles claim grants nothing, and operator authority\n // only ever comes from this verified, audience- and issuer-checked token.\n const operator = Array.isArray(roles) && roles.includes(ARCHIVE_OPERATOR_ROLE);\n // Malformed subjects must reject outright: lone surrogates/control characters\n // could otherwise collapse distinct claims into the same stored identity.\n if (\n typeof subject !== 'string' ||\n subject.length === 0 ||\n subject.length > SUBJECT_MAX_CHARS ||\n !isWellFormedText(subject)\n ) {\n throw unauthenticated();\n }\n if (typeof issuer !== 'string') throw unauthenticated();\n // A token claiming to be issued in the future beyond clock tolerance is a claims failure.\n if (typeof iat === 'number' && iat * 1000 > Date.now() + JWT_CLOCK_TOLERANCE_SECONDS * 1000) {\n throw unauthenticated();\n }\n if (\n operator &&\n (typeof iat !== 'number' ||\n typeof payload.exp !== 'number' ||\n payload.exp - iat > OPERATOR_SESSION_MAX_AGE_SECONDS ||\n Date.now() / 1000 - iat > OPERATOR_SESSION_MAX_AGE_SECONDS + JWT_CLOCK_TOLERANCE_SECONDS)\n )\n throw unauthenticated();\n\n // Only a previously tombstoned principal can bypass live validation to finish\n // partial erasure. Its profile cannot be re-created while the tombstone lives.\n const erasureRetry =\n allowErasureRetry && (await env.DB.prepare(SQL.revokedSubject).bind(issuer, subject, Date.now()).first()) !== null;\n if (!erasureRetry) {\n try {\n await serviceRequest(\n env.AUTH_ISSUER_SERVICE,\n new Request(new URL('/api/v1/auth/session', config.issuer), {\n headers: { cookie: `archive_session=${token}` },\n redirect: 'manual',\n }),\n async (response) => {\n const body: unknown = await response.json();\n if (response.status === 200 && archiveOkSchema.safeParse(body).success) return;\n const failure = archiveErrorResponseSchema.safeParse(body);\n if (response.status === 401 && failure.success && failure.data.error.code === 'unauthenticated')\n throw unauthenticated();\n throw authUnavailable();\n },\n );\n } catch (error) {\n if (error instanceof ApiError && error.status === 401) throw error;\n throw authUnavailable();\n }\n }\n if (!allowErasureRetry) {\n const revoked = await env.DB.prepare(SQL.revokedSubject).bind(issuer, subject, Date.now()).first();\n if (revoked !== null) throw unauthenticated();\n }\n const row = await env.DB.prepare(SQL.publisher).bind(issuer, subject).first<{\n display_name: string;\n id: string;\n }>();\n return {\n operator,\n publisher: row === null ? null : { displayName: row.display_name, id: row.id },\n subject: { issuer, subject },\n };\n}\n\n/** Session required for private routes; anonymous gets 401. */\nexport function requireSession(session: Session | null): Session {\n if (session === null) throw unauthenticated();\n return session;\n}\n"],"mappings":";;;;;;AAuCA,IAAMA,IAAsBC,GAEtBC,IAAoB,KAEpB,wBAAkC,IAAIC,EAAS,KAAK,oBAAoB,mCAAmC;AAQjH,eAAsB,cACpB,GACA,GACA,GACA,IAAoB,IACK;CACzB,IAAMC,IAAQC,EAAaC,CAAO;CAClC,IAAIF,MAAU,MAAM,OAAO;CAE3B,IAAIG;CACJ,IAAI;EASF,KAAU,MARWC,EAAUJ,GAAOK,EAAO,MAAM;GACjD,YAAY,CAAC,OAAO;GACpB,UAAUA,EAAO;GACjB,gBAAA;GACA,QAAQA,EAAO;GACf,aAAaT;GACb,gBAAgB;IAAC;IAAO;IAAO;IAAO;IAAO;IAAO;GAAgB;EACtE,CAAC,EAAA,CACgB;CACnB,SAASU,GAAO;EAad,MAVEA,aAAiBC,EAAO,eACxBD,aAAiBC,EAAO,eACxBD,aAAiBC,EAAO,2BAElB,gBAAgB,IAIpBD,aAAiBC,EAAO,YAAiBC,EAAgB,IAEvD,gBAAgB;CACxB;CACA,IAAMC,IAAUN,EAAQ,KAClBO,IAASP,EAAQ,KACjBQ,IAAMR,EAAQ,KACdS,IAAQT,EAAQ;CACtB,IAAI,CAAC,OAAO,cAAcA,EAAQ,cAAc,KAAMA,EAAQ,iBAA4B,GAAG,MAAMK,EAAgB;CAGnH,IAAMK,IAAW,MAAM,QAAQD,CAAK,KAAKA,EAAM,SAAA,kBAA8B;CAgB7E,IAZE,OAAOH,KAAY,YACnBA,EAAQ,WAAW,KACnBA,EAAQ,SAASX,KACjB,CAACgB,EAAiBL,CAAO,KAIvB,OAAOC,KAAW,YAElB,OAAOC,KAAQ,YAAYA,IAAM,MAAO,KAAK,IAAI,IAAA,OAInDE,MACC,OAAOF,KAAQ,YACd,OAAOR,EAAQ,OAAQ,YACvBA,EAAQ,MAAMQ,IAAA,QACd,KAAK,IAAI,IAAI,MAAOA,IAAA,OAEtB,MAAMH,EAAgB;CAMxB,IAAI,EADFO,KAAsB,MAAMC,EAAI,GAAG,QAAQC,EAAI,cAAc,CAAC,CAAC,KAAKP,GAAQD,GAAS,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,MAAO,OAE9G,IAAI;EACF,MAAMS,EACJF,EAAI,qBACJ,IAAI,QAAQ,IAAI,IAAI,wBAAwBX,EAAO,MAAM,GAAG;GAC1D,SAAS,EAAE,QAAQ,mBAAmBL,IAAQ;GAC9C,UAAU;EACZ,CAAC,GACD,OAAO,MAAa;GAClB,IAAMmB,IAAgB,MAAMC,EAAS,KAAK;GAC1C,IAAIA,EAAS,WAAW,OAAOC,EAAgB,UAAUF,CAAI,CAAC,CAAC,SAAS;GACxE,IAAMG,IAAUC,EAA2B,UAAUJ,CAAI;GAGzD,MAFIC,EAAS,WAAW,OAAOE,EAAQ,WAAWA,EAAQ,KAAK,MAAM,SAAS,oBACtEd,EAAgB,IAClB,gBAAgB;EACxB,CACF;CACF,SAASF,GAAO;EAEd,MADIA,aAAiBP,KAAYO,EAAM,WAAW,MAAWA,IACvD,gBAAgB;CACxB;CAEF,IAAI,CAACS,KAEC,MADkBC,EAAI,GAAG,QAAQC,EAAI,cAAc,CAAC,CAAC,KAAKP,GAAQD,GAAS,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,MACjF,MAAM,MAAMD,EAAgB;CAE9C,IAAMgB,IAAM,MAAMR,EAAI,GAAG,QAAQC,EAAI,SAAS,CAAC,CAAC,KAAKP,GAAQD,CAAO,CAAC,CAAC,MAGnE;CACH,OAAO;EACL;EACA,WAAWe,MAAQ,OAAO,OAAO;GAAE,aAAaA,EAAI;GAAc,IAAIA,EAAI;EAAG;EAC7E,SAAS;GAAE;GAAQ;EAAQ;CAC7B;AACF;AAGA,SAAgB,eAAe,GAAkC;CAC/D,IAAIC,MAAY,MAAM,MAAMjB,EAAgB;CAC5C,OAAOiB;AACT"}
@@ -1,2 +1,2 @@
1
- const e=require("./http.cjs");let t=require("jose");var n=new Set([`127.0.0.1`,`localhost`,`[::1]`]),isLoopback=e=>n.has(e.hostname),configError=t=>new e.ApiError(503,`unavailable`,`The service is temporarily unavailable.`,{diagnostic:{field:t,kind:`configuration`}}),requireVar=(e,t)=>{let n=e[t];if(typeof n!=`string`||n.length===0)throw configError(t);return n},requireUrl=(e,t)=>{try{return new URL(e)}catch{throw configError(t)}},validateUrl=(e,t,n)=>{if(e.username!==``||e.password!==``||e.hash!==``)throw configError(n);if(t===`local`){if(e.protocol!==`http:`||!isLoopback(e))throw configError(n);return e}if(e.protocol!==`https:`||isLoopback(e))throw configError(n);return e},validateOrigin=(e,t,n)=>{if(e===`null`||e===`*`)throw configError(n);let r=validateUrl(requireUrl(e,n),t,n);if(r.pathname!==`/`||r.search!==``)throw configError(n);let i=r.origin;if(i!==e)throw configError(n);return i},flag=(e,t)=>{if(e===`true`)return!0;if(e===`false`)return!1;throw configError(t)},r=`dev-only-insecure-pepper`,rateLimitPepperFor=(e,t)=>{let n=e.RATE_LIMIT_KEY_SECRET;if(typeof n!=`string`||n.length===0){if(t===`local`)return r;throw configError(`RATE_LIMIT_KEY_SECRET`)}if(t===`production`&&n.length<32)throw configError(`RATE_LIMIT_KEY_SECRET`);return n},i=new WeakMap;function apiConfig(e){let n=i.get(e);if(n)return n;let r=requireVar(e,`APP_ENV`);if(r!==`local`&&r!==`production`)throw configError(`APP_ENV`);let a=r;validateUrl(requireUrl(requireVar(e,`JWT_ISSUER`),`JWT_ISSUER`),a,`JWT_ISSUER`);let o=validateUrl(requireUrl(requireVar(e,`JWT_JWKS_URL`),`JWT_JWKS_URL`),a,`JWT_JWKS_URL`),s=requireVar(e,`JWT_AUDIENCE`),c=new Set(requireVar(e,`ALLOWED_ORIGINS`).split(`,`).map(e=>e.trim()).filter(e=>e.length>0).map(e=>validateOrigin(e,a,`ALLOWED_ORIGINS`)));if(c.size===0)throw configError(`ALLOWED_ORIGINS`);if(typeof e.DB?.prepare!=`function`||typeof e.DB?.batch!=`function`)throw configError(`DB`);if(typeof e.READ_LIMIT?.limit!=`function`)throw configError(`READ_LIMIT`);if(typeof e.WRITE_LIMIT?.limit!=`function`)throw configError(`WRITE_LIMIT`);if(typeof e.AUTH_ISSUER_SERVICE?.fetch!=`function`)throw configError(`AUTH_ISSUER_SERVICE`);let l={allowedOrigins:c,appEnv:a,audience:s,issuer:requireVar(e,`JWT_ISSUER`),jwks:(0,t.createRemoteJWKSet)(o,{[t.customFetch]:(t,n)=>e.AUTH_ISSUER_SERVICE.fetch(new Request(t,n))}),likesEnabled:flag(requireVar(e,`LIKES_ENABLED`),`LIKES_ENABLED`),publishingEnabled:flag(requireVar(e,`PUBLISHING_ENABLED`),`PUBLISHING_ENABLED`),rateLimitPepper:rateLimitPepperFor(e,a)};return i.set(e,l),l}exports.apiConfig=apiConfig;
1
+ const e=require("../_server/http.cjs");let t=require("jose");var n=new Set([`127.0.0.1`,`localhost`,`[::1]`]),isLoopback=e=>n.has(e.hostname),configError=t=>new e.ApiError(503,`unavailable`,`The service is temporarily unavailable.`,{diagnostic:{field:t,kind:`configuration`}}),requireVar=(e,t)=>{let n=e[t];if(typeof n!=`string`||n.length===0)throw configError(t);return n},requireUrl=(e,t)=>{try{return new URL(e)}catch{throw configError(t)}},validateUrl=(e,t,n)=>{if(e.username!==``||e.password!==``||e.hash!==``)throw configError(n);if(t===`local`){if(e.protocol!==`http:`||!isLoopback(e))throw configError(n);return e}if(e.protocol!==`https:`||isLoopback(e))throw configError(n);return e},validateOrigin=(e,t,n)=>{if(e===`null`||e===`*`)throw configError(n);let r=validateUrl(requireUrl(e,n),t,n);if(r.pathname!==`/`||r.search!==``)throw configError(n);let i=r.origin;if(i!==e)throw configError(n);return i},flag=(e,t)=>{if(e===`true`)return!0;if(e===`false`)return!1;throw configError(t)},r=`dev-only-insecure-pepper`,rateLimitPepperFor=(e,t)=>{let n=e.RATE_LIMIT_KEY_SECRET;if(typeof n!=`string`||n.length===0){if(t===`local`)return r;throw configError(`RATE_LIMIT_KEY_SECRET`)}if(t===`production`&&n.length<32)throw configError(`RATE_LIMIT_KEY_SECRET`);return n},i=new WeakMap;function apiConfig(e){let n=i.get(e);if(n)return n;let r=requireVar(e,`APP_ENV`);if(r!==`local`&&r!==`production`)throw configError(`APP_ENV`);let a=r;validateUrl(requireUrl(requireVar(e,`JWT_ISSUER`),`JWT_ISSUER`),a,`JWT_ISSUER`);let o=validateUrl(requireUrl(requireVar(e,`JWT_JWKS_URL`),`JWT_JWKS_URL`),a,`JWT_JWKS_URL`),s=requireVar(e,`JWT_AUDIENCE`),c=new Set(requireVar(e,`ALLOWED_ORIGINS`).split(`,`).map(e=>e.trim()).filter(e=>e.length>0).map(e=>validateOrigin(e,a,`ALLOWED_ORIGINS`)));if(c.size===0)throw configError(`ALLOWED_ORIGINS`);if(typeof e.DB?.prepare!=`function`||typeof e.DB?.batch!=`function`)throw configError(`DB`);if(typeof e.READ_LIMIT?.limit!=`function`)throw configError(`READ_LIMIT`);if(typeof e.WRITE_LIMIT?.limit!=`function`)throw configError(`WRITE_LIMIT`);if(typeof e.AUTH_ISSUER_SERVICE?.fetch!=`function`)throw configError(`AUTH_ISSUER_SERVICE`);let l={allowedOrigins:c,appEnv:a,audience:s,issuer:requireVar(e,`JWT_ISSUER`),jwks:(0,t.createRemoteJWKSet)(o,{[t.customFetch]:(t,n)=>e.AUTH_ISSUER_SERVICE.fetch(new Request(t,n))}),likesEnabled:flag(requireVar(e,`LIKES_ENABLED`),`LIKES_ENABLED`),publishingEnabled:flag(requireVar(e,`PUBLISHING_ENABLED`),`PUBLISHING_ENABLED`),rateLimitPepper:rateLimitPepperFor(e,a)};return i.set(e,l),l}exports.apiConfig=apiConfig;
2
2
  //# sourceMappingURL=env.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"env.cjs","names":["ApiError","createRemoteJWKSet","customFetch","require_http","jose","LOOPBACK_HOSTS","url","value","env","name","raw","appEnv","canonical","DEV_RATE_LIMIT_PEPPER","configs","cached","appEnvRaw","jwksUrl","audience","origins","o","config","input","init"],"sources":["../../src/worker/env.ts"],"sourcesContent":["import { createRemoteJWKSet, customFetch } from 'jose';\nimport { ApiError } from './http';\n\n/**\n * Worker configuration. Every value the worker trusts comes from the validated Env —\n * never from request-supplied URLs or headers. Anything missing or malformed fails\n * closed (503) rather than falling back to a permissive default.\n */\nexport type AppEnv = 'local' | 'production';\n\nexport interface ApiConfig {\n allowedOrigins: ReadonlySet<string>;\n appEnv: AppEnv;\n audience: string;\n issuer: string;\n jwks: ReturnType<typeof createRemoteJWKSet>;\n likesEnabled: boolean;\n publishingEnabled: boolean;\n rateLimitPepper: string;\n}\n\n/** The rate-limit binding subset the core calls; the runtime's RateLimit satisfies it. */\nexport interface RateLimiterBinding {\n limit(input: { key: string }): Promise<{ success: boolean }>;\n}\n\n/** The D1 statement subset the core binds; null params model nullable columns. */\nexport interface D1Statement {\n all<T>(): Promise<{ results?: T[] }>;\n bind(...params: (string | number | null)[]): D1Statement;\n first<T>(): Promise<T | null>;\n run(): Promise<{ meta?: { changes?: number } }>;\n}\n\n/** The D1 database subset the core uses; a deployment's generated D1Database satisfies it. */\nexport interface D1Like {\n batch(statements: D1Statement[]): Promise<{ meta?: { changes?: number } }[]>;\n prepare(sql: string): D1Statement;\n}\n\n/**\n * The bindings the archive Worker reads. The package declares its own minimal\n * structural types instead of `@cloudflare/workers-types`: a deployment's generated\n * Env (with real D1Database/RateLimit bindings) satisfies this shape structurally.\n */\nexport interface ArchiveWorkerEnv {\n ALLOWED_ORIGINS: string;\n APP_ENV: string;\n /** Required issuer session-validation service; every private request checks live authority. */\n AUTH_ISSUER_SERVICE: AuthIssuerServiceBinding;\n DB: D1Like;\n JWT_AUDIENCE: string;\n JWT_ISSUER: string;\n JWT_JWKS_URL: string;\n LIKES_ENABLED: string;\n PUBLISHING_ENABLED: string;\n RATE_LIMIT_KEY_SECRET: string;\n READ_LIMIT: RateLimiterBinding;\n WRITE_LIMIT: RateLimiterBinding;\n}\n\nexport interface AuthIssuerServiceBinding {\n fetch(request: Request): Promise<Response>;\n}\n\nconst LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '[::1]']);\n\nconst isLoopback = (url: URL): boolean => LOOPBACK_HOSTS.has(url.hostname);\n\n// Configuration faults never name the offending field — the response must not\n// leak Env names or framework details, only a generic temporary unavailability.\nconst configError = (field: string): ApiError =>\n new ApiError(503, 'unavailable', 'The service is temporarily unavailable.', {\n diagnostic: { field, kind: 'configuration' },\n });\n\nconst requireVar = (env: ArchiveWorkerEnv, name: string): string => {\n const value = env[name as keyof ArchiveWorkerEnv];\n if (typeof value !== 'string' || value.length === 0) throw configError(name);\n return value;\n};\n\nconst requireUrl = (raw: string, name: string): URL => {\n try {\n return new URL(raw);\n } catch {\n throw configError(name);\n }\n};\n\n/** Validates one trusted endpoint URL against the environment's transport rules. */\nconst validateUrl = (url: URL, appEnv: AppEnv, name: string): URL => {\n // No embedded credentials or fragments can ever be part of a trusted endpoint.\n if (url.username !== '' || url.password !== '' || url.hash !== '') throw configError(name);\n if (appEnv === 'local') {\n // Loopback HTTP is permitted only in local development.\n if (url.protocol !== 'http:' || !isLoopback(url)) throw configError(name);\n return url;\n }\n if (url.protocol !== 'https:' || isLoopback(url)) throw configError(name);\n return url;\n};\n\n/**\n * An allowed origin is an exact canonical origin: scheme + host (+port) and\n * nothing else — no wildcard, `null` origin, credentials, path, query or fragment.\n * Local permits loopback HTTP; production requires non-loopback HTTPS.\n */\nconst validateOrigin = (raw: string, appEnv: AppEnv, name: string): string => {\n if (raw === 'null' || raw === '*') throw configError(name);\n const url = validateUrl(requireUrl(raw, name), appEnv, name);\n if (url.pathname !== '/' || url.search !== '') throw configError(name);\n const canonical = url.origin;\n if (canonical !== raw) throw configError(name);\n return canonical;\n};\n\nconst flag = (raw: string, name: string): boolean => {\n if (raw === 'true') return true;\n if (raw === 'false') return false;\n throw configError(name);\n};\n\n/** Local-only fallback pepper for rate-limit keys; never a production secret. */\nconst DEV_RATE_LIMIT_PEPPER = 'dev-only-insecure-pepper';\n\n/**\n * The deployment secret behind rate-limit key HMACs: a deployment-specific\n * pepper makes the hashed keys useless outside this deployment. Production\n * fails closed on a missing, empty or too-short value; local development only\n * may fall back to the constant insecure pepper — local only, never production.\n */\nconst rateLimitPepperFor = (env: ArchiveWorkerEnv, appEnv: AppEnv): string => {\n const raw = env.RATE_LIMIT_KEY_SECRET;\n if (typeof raw !== 'string' || raw.length === 0) {\n if (appEnv === 'local') return DEV_RATE_LIMIT_PEPPER;\n throw configError('RATE_LIMIT_KEY_SECRET');\n }\n if (appEnv === 'production' && raw.length < 32) throw configError('RATE_LIMIT_KEY_SECRET');\n return raw;\n};\n\nconst configs = new WeakMap<ArchiveWorkerEnv, ApiConfig>();\n\n/** Parses and validates the env once per bindings object; throws ApiError(503). */\nexport function apiConfig(env: ArchiveWorkerEnv): ApiConfig {\n const cached = configs.get(env);\n if (cached) return cached;\n\n const appEnvRaw = requireVar(env, 'APP_ENV');\n if (appEnvRaw !== 'local' && appEnvRaw !== 'production') throw configError('APP_ENV');\n const appEnv = appEnvRaw;\n\n // The issuer URL is validated for transport/safety but kept as the exact\n // configured string for JWT claim matching.\n validateUrl(requireUrl(requireVar(env, 'JWT_ISSUER'), 'JWT_ISSUER'), appEnv, 'JWT_ISSUER');\n const jwksUrl = validateUrl(requireUrl(requireVar(env, 'JWT_JWKS_URL'), 'JWT_JWKS_URL'), appEnv, 'JWT_JWKS_URL');\n const audience = requireVar(env, 'JWT_AUDIENCE');\n\n const origins = new Set(\n requireVar(env, 'ALLOWED_ORIGINS')\n .split(',')\n .map((o) => o.trim())\n .filter((o) => o.length > 0)\n .map((o) => validateOrigin(o, appEnv, 'ALLOWED_ORIGINS')),\n );\n if (origins.size === 0) throw configError('ALLOWED_ORIGINS');\n\n if (typeof env.DB?.prepare !== 'function' || typeof env.DB?.batch !== 'function') throw configError('DB');\n if (typeof env.READ_LIMIT?.limit !== 'function') throw configError('READ_LIMIT');\n if (typeof env.WRITE_LIMIT?.limit !== 'function') throw configError('WRITE_LIMIT');\n if (typeof env.AUTH_ISSUER_SERVICE?.fetch !== 'function') throw configError('AUTH_ISSUER_SERVICE');\n\n const config: ApiConfig = {\n allowedOrigins: origins,\n appEnv,\n audience,\n // The exact configured issuer string is the match target — a trailing slash\n // is meaningful, never normalized away.\n issuer: requireVar(env, 'JWT_ISSUER'),\n jwks: createRemoteJWKSet(jwksUrl, {\n [customFetch]: (input, init) => env.AUTH_ISSUER_SERVICE.fetch(new Request(input, init)),\n }),\n likesEnabled: flag(requireVar(env, 'LIKES_ENABLED'), 'LIKES_ENABLED'),\n publishingEnabled: flag(requireVar(env, 'PUBLISHING_ENABLED'), 'PUBLISHING_ENABLED'),\n rateLimitPepper: rateLimitPepperFor(env, appEnv),\n };\n configs.set(env, config);\n return config;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"env.cjs","names":["ApiError","createRemoteJWKSet","customFetch","require_http","jose","LOOPBACK_HOSTS","url","value","env","name","raw","appEnv","canonical","DEV_RATE_LIMIT_PEPPER","configs","cached","appEnvRaw","jwksUrl","audience","origins","o","config","input","init"],"sources":["../../src/worker/env.ts"],"sourcesContent":["import { createRemoteJWKSet, customFetch } from 'jose';\nimport type { D1Like } from '../_server/d1';\nimport { ApiError } from '../_server/http';\n\n/**\n * Worker configuration. Every value the worker trusts comes from the validated Env —\n * never from request-supplied URLs or headers. Anything missing or malformed fails\n * closed (503) rather than falling back to a permissive default.\n */\nexport type AppEnv = 'local' | 'production';\n\nexport interface ApiConfig {\n allowedOrigins: ReadonlySet<string>;\n appEnv: AppEnv;\n audience: string;\n issuer: string;\n jwks: ReturnType<typeof createRemoteJWKSet>;\n likesEnabled: boolean;\n publishingEnabled: boolean;\n rateLimitPepper: string;\n}\n\n/** The rate-limit binding subset the core calls; the runtime's RateLimit satisfies it. */\nexport interface RateLimiterBinding {\n limit(input: { key: string }): Promise<{ success: boolean }>;\n}\n\n/**\n * The bindings the archive Worker reads. The package declares its own minimal\n * structural types instead of `@cloudflare/workers-types`: a deployment's generated\n * Env (with real D1Database/RateLimit bindings) satisfies this shape structurally.\n */\nexport interface ArchiveWorkerEnv {\n ALLOWED_ORIGINS: string;\n APP_ENV: string;\n /** Required issuer session-validation service; every private request checks live authority. */\n AUTH_ISSUER_SERVICE: AuthIssuerServiceBinding;\n DB: D1Like;\n JWT_AUDIENCE: string;\n JWT_ISSUER: string;\n JWT_JWKS_URL: string;\n LIKES_ENABLED: string;\n PUBLISHING_ENABLED: string;\n RATE_LIMIT_KEY_SECRET: string;\n READ_LIMIT: RateLimiterBinding;\n WRITE_LIMIT: RateLimiterBinding;\n}\n\nexport interface AuthIssuerServiceBinding {\n fetch(request: Request): Promise<Response>;\n}\n\nconst LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '[::1]']);\n\nconst isLoopback = (url: URL): boolean => LOOPBACK_HOSTS.has(url.hostname);\n\n// Configuration faults never name the offending field — the response must not\n// leak Env names or framework details, only a generic temporary unavailability.\nconst configError = (field: string): ApiError =>\n new ApiError(503, 'unavailable', 'The service is temporarily unavailable.', {\n diagnostic: { field, kind: 'configuration' },\n });\n\nconst requireVar = (env: ArchiveWorkerEnv, name: string): string => {\n const value = env[name as keyof ArchiveWorkerEnv];\n if (typeof value !== 'string' || value.length === 0) throw configError(name);\n return value;\n};\n\nconst requireUrl = (raw: string, name: string): URL => {\n try {\n return new URL(raw);\n } catch {\n throw configError(name);\n }\n};\n\n/** Validates one trusted endpoint URL against the environment's transport rules. */\nconst validateUrl = (url: URL, appEnv: AppEnv, name: string): URL => {\n // No embedded credentials or fragments can ever be part of a trusted endpoint.\n if (url.username !== '' || url.password !== '' || url.hash !== '') throw configError(name);\n if (appEnv === 'local') {\n // Loopback HTTP is permitted only in local development.\n if (url.protocol !== 'http:' || !isLoopback(url)) throw configError(name);\n return url;\n }\n if (url.protocol !== 'https:' || isLoopback(url)) throw configError(name);\n return url;\n};\n\n/**\n * An allowed origin is an exact canonical origin: scheme + host (+port) and\n * nothing else — no wildcard, `null` origin, credentials, path, query or fragment.\n * Local permits loopback HTTP; production requires non-loopback HTTPS.\n */\nconst validateOrigin = (raw: string, appEnv: AppEnv, name: string): string => {\n if (raw === 'null' || raw === '*') throw configError(name);\n const url = validateUrl(requireUrl(raw, name), appEnv, name);\n if (url.pathname !== '/' || url.search !== '') throw configError(name);\n const canonical = url.origin;\n if (canonical !== raw) throw configError(name);\n return canonical;\n};\n\nconst flag = (raw: string, name: string): boolean => {\n if (raw === 'true') return true;\n if (raw === 'false') return false;\n throw configError(name);\n};\n\n/** Local-only fallback pepper for rate-limit keys; never a production secret. */\nconst DEV_RATE_LIMIT_PEPPER = 'dev-only-insecure-pepper';\n\n/**\n * The deployment secret behind rate-limit key HMACs: a deployment-specific\n * pepper makes the hashed keys useless outside this deployment. Production\n * fails closed on a missing, empty or too-short value; local development only\n * may fall back to the constant insecure pepper — local only, never production.\n */\nconst rateLimitPepperFor = (env: ArchiveWorkerEnv, appEnv: AppEnv): string => {\n const raw = env.RATE_LIMIT_KEY_SECRET;\n if (typeof raw !== 'string' || raw.length === 0) {\n if (appEnv === 'local') return DEV_RATE_LIMIT_PEPPER;\n throw configError('RATE_LIMIT_KEY_SECRET');\n }\n if (appEnv === 'production' && raw.length < 32) throw configError('RATE_LIMIT_KEY_SECRET');\n return raw;\n};\n\nconst configs = new WeakMap<ArchiveWorkerEnv, ApiConfig>();\n\n/** Parses and validates the env once per bindings object; throws ApiError(503). */\nexport function apiConfig(env: ArchiveWorkerEnv): ApiConfig {\n const cached = configs.get(env);\n if (cached) return cached;\n\n const appEnvRaw = requireVar(env, 'APP_ENV');\n if (appEnvRaw !== 'local' && appEnvRaw !== 'production') throw configError('APP_ENV');\n const appEnv = appEnvRaw;\n\n // The issuer URL is validated for transport/safety but kept as the exact\n // configured string for JWT claim matching.\n validateUrl(requireUrl(requireVar(env, 'JWT_ISSUER'), 'JWT_ISSUER'), appEnv, 'JWT_ISSUER');\n const jwksUrl = validateUrl(requireUrl(requireVar(env, 'JWT_JWKS_URL'), 'JWT_JWKS_URL'), appEnv, 'JWT_JWKS_URL');\n const audience = requireVar(env, 'JWT_AUDIENCE');\n\n const origins = new Set(\n requireVar(env, 'ALLOWED_ORIGINS')\n .split(',')\n .map((o) => o.trim())\n .filter((o) => o.length > 0)\n .map((o) => validateOrigin(o, appEnv, 'ALLOWED_ORIGINS')),\n );\n if (origins.size === 0) throw configError('ALLOWED_ORIGINS');\n\n if (typeof env.DB?.prepare !== 'function' || typeof env.DB?.batch !== 'function') throw configError('DB');\n if (typeof env.READ_LIMIT?.limit !== 'function') throw configError('READ_LIMIT');\n if (typeof env.WRITE_LIMIT?.limit !== 'function') throw configError('WRITE_LIMIT');\n if (typeof env.AUTH_ISSUER_SERVICE?.fetch !== 'function') throw configError('AUTH_ISSUER_SERVICE');\n\n const config: ApiConfig = {\n allowedOrigins: origins,\n appEnv,\n audience,\n // The exact configured issuer string is the match target — a trailing slash\n // is meaningful, never normalized away.\n issuer: requireVar(env, 'JWT_ISSUER'),\n jwks: createRemoteJWKSet(jwksUrl, {\n [customFetch]: (input, init) => env.AUTH_ISSUER_SERVICE.fetch(new Request(input, init)),\n }),\n likesEnabled: flag(requireVar(env, 'LIKES_ENABLED'), 'LIKES_ENABLED'),\n publishingEnabled: flag(requireVar(env, 'PUBLISHING_ENABLED'), 'PUBLISHING_ENABLED'),\n rateLimitPepper: rateLimitPepperFor(env, appEnv),\n };\n configs.set(env, config);\n return config;\n}\n"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { createRemoteJWKSet } from 'jose';
2
+ import type { D1Like } from '../_server/d1';
2
3
  /**
3
4
  * Worker configuration. Every value the worker trusts comes from the validated Env —
4
5
  * never from request-supplied URLs or headers. Anything missing or malformed fails
@@ -23,28 +24,6 @@ export interface RateLimiterBinding {
23
24
  success: boolean;
24
25
  }>;
25
26
  }
26
- /** The D1 statement subset the core binds; null params model nullable columns. */
27
- export interface D1Statement {
28
- all<T>(): Promise<{
29
- results?: T[];
30
- }>;
31
- bind(...params: (string | number | null)[]): D1Statement;
32
- first<T>(): Promise<T | null>;
33
- run(): Promise<{
34
- meta?: {
35
- changes?: number;
36
- };
37
- }>;
38
- }
39
- /** The D1 database subset the core uses; a deployment's generated D1Database satisfies it. */
40
- export interface D1Like {
41
- batch(statements: D1Statement[]): Promise<{
42
- meta?: {
43
- changes?: number;
44
- };
45
- }[]>;
46
- prepare(sql: string): D1Statement;
47
- }
48
27
  /**
49
28
  * The bindings the archive Worker reads. The package declares its own minimal
50
29
  * structural types instead of `@cloudflare/workers-types`: a deployment's generated
@@ -1 +1 @@
1
- {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../src/worker/env.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAe,MAAM,MAAM,CAAC;AAGvD;;;;GAIG;AACH,MAAM,MAAM,MAAM,GAAG,OAAO,GAAG,YAAY,CAAC;AAE5C,MAAM,WAAW,SAAS;IACxB,cAAc,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC;IAC5C,YAAY,EAAE,OAAO,CAAC;IACtB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,0FAA0F;AAC1F,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,KAAK,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CAC9D;AAED,kFAAkF;AAClF,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,CAAC,KAAK,OAAO,CAAC;QAAE,OAAO,CAAC,EAAE,CAAC,EAAE,CAAA;KAAE,CAAC,CAAC;IACrC,IAAI,CAAC,GAAG,MAAM,EAAE,CAAC,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC,EAAE,GAAG,WAAW,CAAC;IACzD,KAAK,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAC9B,GAAG,IAAI,OAAO,CAAC;QAAE,IAAI,CAAC,EAAE;YAAE,OAAO,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC,CAAC;CACjD;AAED,8FAA8F;AAC9F,MAAM,WAAW,MAAM;IACrB,KAAK,CAAC,UAAU,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC;QAAE,IAAI,CAAC,EAAE;YAAE,OAAO,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,EAAE,CAAC,CAAC;IAC7E,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAAC;CACnC;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,+FAA+F;IAC/F,mBAAmB,EAAE,wBAAwB,CAAC;IAC9C,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,UAAU,EAAE,kBAAkB,CAAC;IAC/B,WAAW,EAAE,kBAAkB,CAAC;CACjC;AAED,MAAM,WAAW,wBAAwB;IACvC,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC5C;AAiFD,mFAAmF;AACnF,wBAAgB,SAAS,CAAC,GAAG,EAAE,gBAAgB,GAAG,SAAS,CA4C1D"}
1
+ {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../src/worker/env.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAe,MAAM,MAAM,CAAC;AACvD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAG5C;;;;GAIG;AACH,MAAM,MAAM,MAAM,GAAG,OAAO,GAAG,YAAY,CAAC;AAE5C,MAAM,WAAW,SAAS;IACxB,cAAc,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC;IAC5C,YAAY,EAAE,OAAO,CAAC;IACtB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,0FAA0F;AAC1F,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,KAAK,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CAC9D;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,+FAA+F;IAC/F,mBAAmB,EAAE,wBAAwB,CAAC;IAC9C,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,UAAU,EAAE,kBAAkB,CAAC;IAC/B,WAAW,EAAE,kBAAkB,CAAC;CACjC;AAED,MAAM,WAAW,wBAAwB;IACvC,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC5C;AAiFD,mFAAmF;AACnF,wBAAgB,SAAS,CAAC,GAAG,EAAE,gBAAgB,GAAG,SAAS,CA4C1D"}
@@ -1,4 +1,4 @@
1
- import { ApiError as e } from "./http.js";
1
+ import { ApiError as e } from "../_server/http.js";
2
2
  import { createRemoteJWKSet as t, customFetch as n } from "jose";
3
3
  // src/worker/env.ts
4
4
  var r = /* @__PURE__ */ new Set([
@@ -1 +1 @@
1
- {"version":3,"file":"env.js","names":["LOOPBACK_HOSTS","url","ApiError","value","env","name","raw","appEnv","canonical","DEV_RATE_LIMIT_PEPPER","configs","cached","appEnvRaw","jwksUrl","audience","origins","o","config","createRemoteJWKSet","customFetch","input","init"],"sources":["../../src/worker/env.ts"],"sourcesContent":["import { createRemoteJWKSet, customFetch } from 'jose';\nimport { ApiError } from './http';\n\n/**\n * Worker configuration. Every value the worker trusts comes from the validated Env —\n * never from request-supplied URLs or headers. Anything missing or malformed fails\n * closed (503) rather than falling back to a permissive default.\n */\nexport type AppEnv = 'local' | 'production';\n\nexport interface ApiConfig {\n allowedOrigins: ReadonlySet<string>;\n appEnv: AppEnv;\n audience: string;\n issuer: string;\n jwks: ReturnType<typeof createRemoteJWKSet>;\n likesEnabled: boolean;\n publishingEnabled: boolean;\n rateLimitPepper: string;\n}\n\n/** The rate-limit binding subset the core calls; the runtime's RateLimit satisfies it. */\nexport interface RateLimiterBinding {\n limit(input: { key: string }): Promise<{ success: boolean }>;\n}\n\n/** The D1 statement subset the core binds; null params model nullable columns. */\nexport interface D1Statement {\n all<T>(): Promise<{ results?: T[] }>;\n bind(...params: (string | number | null)[]): D1Statement;\n first<T>(): Promise<T | null>;\n run(): Promise<{ meta?: { changes?: number } }>;\n}\n\n/** The D1 database subset the core uses; a deployment's generated D1Database satisfies it. */\nexport interface D1Like {\n batch(statements: D1Statement[]): Promise<{ meta?: { changes?: number } }[]>;\n prepare(sql: string): D1Statement;\n}\n\n/**\n * The bindings the archive Worker reads. The package declares its own minimal\n * structural types instead of `@cloudflare/workers-types`: a deployment's generated\n * Env (with real D1Database/RateLimit bindings) satisfies this shape structurally.\n */\nexport interface ArchiveWorkerEnv {\n ALLOWED_ORIGINS: string;\n APP_ENV: string;\n /** Required issuer session-validation service; every private request checks live authority. */\n AUTH_ISSUER_SERVICE: AuthIssuerServiceBinding;\n DB: D1Like;\n JWT_AUDIENCE: string;\n JWT_ISSUER: string;\n JWT_JWKS_URL: string;\n LIKES_ENABLED: string;\n PUBLISHING_ENABLED: string;\n RATE_LIMIT_KEY_SECRET: string;\n READ_LIMIT: RateLimiterBinding;\n WRITE_LIMIT: RateLimiterBinding;\n}\n\nexport interface AuthIssuerServiceBinding {\n fetch(request: Request): Promise<Response>;\n}\n\nconst LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '[::1]']);\n\nconst isLoopback = (url: URL): boolean => LOOPBACK_HOSTS.has(url.hostname);\n\n// Configuration faults never name the offending field — the response must not\n// leak Env names or framework details, only a generic temporary unavailability.\nconst configError = (field: string): ApiError =>\n new ApiError(503, 'unavailable', 'The service is temporarily unavailable.', {\n diagnostic: { field, kind: 'configuration' },\n });\n\nconst requireVar = (env: ArchiveWorkerEnv, name: string): string => {\n const value = env[name as keyof ArchiveWorkerEnv];\n if (typeof value !== 'string' || value.length === 0) throw configError(name);\n return value;\n};\n\nconst requireUrl = (raw: string, name: string): URL => {\n try {\n return new URL(raw);\n } catch {\n throw configError(name);\n }\n};\n\n/** Validates one trusted endpoint URL against the environment's transport rules. */\nconst validateUrl = (url: URL, appEnv: AppEnv, name: string): URL => {\n // No embedded credentials or fragments can ever be part of a trusted endpoint.\n if (url.username !== '' || url.password !== '' || url.hash !== '') throw configError(name);\n if (appEnv === 'local') {\n // Loopback HTTP is permitted only in local development.\n if (url.protocol !== 'http:' || !isLoopback(url)) throw configError(name);\n return url;\n }\n if (url.protocol !== 'https:' || isLoopback(url)) throw configError(name);\n return url;\n};\n\n/**\n * An allowed origin is an exact canonical origin: scheme + host (+port) and\n * nothing else — no wildcard, `null` origin, credentials, path, query or fragment.\n * Local permits loopback HTTP; production requires non-loopback HTTPS.\n */\nconst validateOrigin = (raw: string, appEnv: AppEnv, name: string): string => {\n if (raw === 'null' || raw === '*') throw configError(name);\n const url = validateUrl(requireUrl(raw, name), appEnv, name);\n if (url.pathname !== '/' || url.search !== '') throw configError(name);\n const canonical = url.origin;\n if (canonical !== raw) throw configError(name);\n return canonical;\n};\n\nconst flag = (raw: string, name: string): boolean => {\n if (raw === 'true') return true;\n if (raw === 'false') return false;\n throw configError(name);\n};\n\n/** Local-only fallback pepper for rate-limit keys; never a production secret. */\nconst DEV_RATE_LIMIT_PEPPER = 'dev-only-insecure-pepper';\n\n/**\n * The deployment secret behind rate-limit key HMACs: a deployment-specific\n * pepper makes the hashed keys useless outside this deployment. Production\n * fails closed on a missing, empty or too-short value; local development only\n * may fall back to the constant insecure pepper — local only, never production.\n */\nconst rateLimitPepperFor = (env: ArchiveWorkerEnv, appEnv: AppEnv): string => {\n const raw = env.RATE_LIMIT_KEY_SECRET;\n if (typeof raw !== 'string' || raw.length === 0) {\n if (appEnv === 'local') return DEV_RATE_LIMIT_PEPPER;\n throw configError('RATE_LIMIT_KEY_SECRET');\n }\n if (appEnv === 'production' && raw.length < 32) throw configError('RATE_LIMIT_KEY_SECRET');\n return raw;\n};\n\nconst configs = new WeakMap<ArchiveWorkerEnv, ApiConfig>();\n\n/** Parses and validates the env once per bindings object; throws ApiError(503). */\nexport function apiConfig(env: ArchiveWorkerEnv): ApiConfig {\n const cached = configs.get(env);\n if (cached) return cached;\n\n const appEnvRaw = requireVar(env, 'APP_ENV');\n if (appEnvRaw !== 'local' && appEnvRaw !== 'production') throw configError('APP_ENV');\n const appEnv = appEnvRaw;\n\n // The issuer URL is validated for transport/safety but kept as the exact\n // configured string for JWT claim matching.\n validateUrl(requireUrl(requireVar(env, 'JWT_ISSUER'), 'JWT_ISSUER'), appEnv, 'JWT_ISSUER');\n const jwksUrl = validateUrl(requireUrl(requireVar(env, 'JWT_JWKS_URL'), 'JWT_JWKS_URL'), appEnv, 'JWT_JWKS_URL');\n const audience = requireVar(env, 'JWT_AUDIENCE');\n\n const origins = new Set(\n requireVar(env, 'ALLOWED_ORIGINS')\n .split(',')\n .map((o) => o.trim())\n .filter((o) => o.length > 0)\n .map((o) => validateOrigin(o, appEnv, 'ALLOWED_ORIGINS')),\n );\n if (origins.size === 0) throw configError('ALLOWED_ORIGINS');\n\n if (typeof env.DB?.prepare !== 'function' || typeof env.DB?.batch !== 'function') throw configError('DB');\n if (typeof env.READ_LIMIT?.limit !== 'function') throw configError('READ_LIMIT');\n if (typeof env.WRITE_LIMIT?.limit !== 'function') throw configError('WRITE_LIMIT');\n if (typeof env.AUTH_ISSUER_SERVICE?.fetch !== 'function') throw configError('AUTH_ISSUER_SERVICE');\n\n const config: ApiConfig = {\n allowedOrigins: origins,\n appEnv,\n audience,\n // The exact configured issuer string is the match target — a trailing slash\n // is meaningful, never normalized away.\n issuer: requireVar(env, 'JWT_ISSUER'),\n jwks: createRemoteJWKSet(jwksUrl, {\n [customFetch]: (input, init) => env.AUTH_ISSUER_SERVICE.fetch(new Request(input, init)),\n }),\n likesEnabled: flag(requireVar(env, 'LIKES_ENABLED'), 'LIKES_ENABLED'),\n publishingEnabled: flag(requireVar(env, 'PUBLISHING_ENABLED'), 'PUBLISHING_ENABLED'),\n rateLimitPepper: rateLimitPepperFor(env, appEnv),\n };\n configs.set(env, config);\n return config;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"env.js","names":["LOOPBACK_HOSTS","url","ApiError","value","env","name","raw","appEnv","canonical","DEV_RATE_LIMIT_PEPPER","configs","cached","appEnvRaw","jwksUrl","audience","origins","o","config","createRemoteJWKSet","customFetch","input","init"],"sources":["../../src/worker/env.ts"],"sourcesContent":["import { createRemoteJWKSet, customFetch } from 'jose';\nimport type { D1Like } from '../_server/d1';\nimport { ApiError } from '../_server/http';\n\n/**\n * Worker configuration. Every value the worker trusts comes from the validated Env —\n * never from request-supplied URLs or headers. Anything missing or malformed fails\n * closed (503) rather than falling back to a permissive default.\n */\nexport type AppEnv = 'local' | 'production';\n\nexport interface ApiConfig {\n allowedOrigins: ReadonlySet<string>;\n appEnv: AppEnv;\n audience: string;\n issuer: string;\n jwks: ReturnType<typeof createRemoteJWKSet>;\n likesEnabled: boolean;\n publishingEnabled: boolean;\n rateLimitPepper: string;\n}\n\n/** The rate-limit binding subset the core calls; the runtime's RateLimit satisfies it. */\nexport interface RateLimiterBinding {\n limit(input: { key: string }): Promise<{ success: boolean }>;\n}\n\n/**\n * The bindings the archive Worker reads. The package declares its own minimal\n * structural types instead of `@cloudflare/workers-types`: a deployment's generated\n * Env (with real D1Database/RateLimit bindings) satisfies this shape structurally.\n */\nexport interface ArchiveWorkerEnv {\n ALLOWED_ORIGINS: string;\n APP_ENV: string;\n /** Required issuer session-validation service; every private request checks live authority. */\n AUTH_ISSUER_SERVICE: AuthIssuerServiceBinding;\n DB: D1Like;\n JWT_AUDIENCE: string;\n JWT_ISSUER: string;\n JWT_JWKS_URL: string;\n LIKES_ENABLED: string;\n PUBLISHING_ENABLED: string;\n RATE_LIMIT_KEY_SECRET: string;\n READ_LIMIT: RateLimiterBinding;\n WRITE_LIMIT: RateLimiterBinding;\n}\n\nexport interface AuthIssuerServiceBinding {\n fetch(request: Request): Promise<Response>;\n}\n\nconst LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '[::1]']);\n\nconst isLoopback = (url: URL): boolean => LOOPBACK_HOSTS.has(url.hostname);\n\n// Configuration faults never name the offending field — the response must not\n// leak Env names or framework details, only a generic temporary unavailability.\nconst configError = (field: string): ApiError =>\n new ApiError(503, 'unavailable', 'The service is temporarily unavailable.', {\n diagnostic: { field, kind: 'configuration' },\n });\n\nconst requireVar = (env: ArchiveWorkerEnv, name: string): string => {\n const value = env[name as keyof ArchiveWorkerEnv];\n if (typeof value !== 'string' || value.length === 0) throw configError(name);\n return value;\n};\n\nconst requireUrl = (raw: string, name: string): URL => {\n try {\n return new URL(raw);\n } catch {\n throw configError(name);\n }\n};\n\n/** Validates one trusted endpoint URL against the environment's transport rules. */\nconst validateUrl = (url: URL, appEnv: AppEnv, name: string): URL => {\n // No embedded credentials or fragments can ever be part of a trusted endpoint.\n if (url.username !== '' || url.password !== '' || url.hash !== '') throw configError(name);\n if (appEnv === 'local') {\n // Loopback HTTP is permitted only in local development.\n if (url.protocol !== 'http:' || !isLoopback(url)) throw configError(name);\n return url;\n }\n if (url.protocol !== 'https:' || isLoopback(url)) throw configError(name);\n return url;\n};\n\n/**\n * An allowed origin is an exact canonical origin: scheme + host (+port) and\n * nothing else — no wildcard, `null` origin, credentials, path, query or fragment.\n * Local permits loopback HTTP; production requires non-loopback HTTPS.\n */\nconst validateOrigin = (raw: string, appEnv: AppEnv, name: string): string => {\n if (raw === 'null' || raw === '*') throw configError(name);\n const url = validateUrl(requireUrl(raw, name), appEnv, name);\n if (url.pathname !== '/' || url.search !== '') throw configError(name);\n const canonical = url.origin;\n if (canonical !== raw) throw configError(name);\n return canonical;\n};\n\nconst flag = (raw: string, name: string): boolean => {\n if (raw === 'true') return true;\n if (raw === 'false') return false;\n throw configError(name);\n};\n\n/** Local-only fallback pepper for rate-limit keys; never a production secret. */\nconst DEV_RATE_LIMIT_PEPPER = 'dev-only-insecure-pepper';\n\n/**\n * The deployment secret behind rate-limit key HMACs: a deployment-specific\n * pepper makes the hashed keys useless outside this deployment. Production\n * fails closed on a missing, empty or too-short value; local development only\n * may fall back to the constant insecure pepper — local only, never production.\n */\nconst rateLimitPepperFor = (env: ArchiveWorkerEnv, appEnv: AppEnv): string => {\n const raw = env.RATE_LIMIT_KEY_SECRET;\n if (typeof raw !== 'string' || raw.length === 0) {\n if (appEnv === 'local') return DEV_RATE_LIMIT_PEPPER;\n throw configError('RATE_LIMIT_KEY_SECRET');\n }\n if (appEnv === 'production' && raw.length < 32) throw configError('RATE_LIMIT_KEY_SECRET');\n return raw;\n};\n\nconst configs = new WeakMap<ArchiveWorkerEnv, ApiConfig>();\n\n/** Parses and validates the env once per bindings object; throws ApiError(503). */\nexport function apiConfig(env: ArchiveWorkerEnv): ApiConfig {\n const cached = configs.get(env);\n if (cached) return cached;\n\n const appEnvRaw = requireVar(env, 'APP_ENV');\n if (appEnvRaw !== 'local' && appEnvRaw !== 'production') throw configError('APP_ENV');\n const appEnv = appEnvRaw;\n\n // The issuer URL is validated for transport/safety but kept as the exact\n // configured string for JWT claim matching.\n validateUrl(requireUrl(requireVar(env, 'JWT_ISSUER'), 'JWT_ISSUER'), appEnv, 'JWT_ISSUER');\n const jwksUrl = validateUrl(requireUrl(requireVar(env, 'JWT_JWKS_URL'), 'JWT_JWKS_URL'), appEnv, 'JWT_JWKS_URL');\n const audience = requireVar(env, 'JWT_AUDIENCE');\n\n const origins = new Set(\n requireVar(env, 'ALLOWED_ORIGINS')\n .split(',')\n .map((o) => o.trim())\n .filter((o) => o.length > 0)\n .map((o) => validateOrigin(o, appEnv, 'ALLOWED_ORIGINS')),\n );\n if (origins.size === 0) throw configError('ALLOWED_ORIGINS');\n\n if (typeof env.DB?.prepare !== 'function' || typeof env.DB?.batch !== 'function') throw configError('DB');\n if (typeof env.READ_LIMIT?.limit !== 'function') throw configError('READ_LIMIT');\n if (typeof env.WRITE_LIMIT?.limit !== 'function') throw configError('WRITE_LIMIT');\n if (typeof env.AUTH_ISSUER_SERVICE?.fetch !== 'function') throw configError('AUTH_ISSUER_SERVICE');\n\n const config: ApiConfig = {\n allowedOrigins: origins,\n appEnv,\n audience,\n // The exact configured issuer string is the match target — a trailing slash\n // is meaningful, never normalized away.\n issuer: requireVar(env, 'JWT_ISSUER'),\n jwks: createRemoteJWKSet(jwksUrl, {\n [customFetch]: (input, init) => env.AUTH_ISSUER_SERVICE.fetch(new Request(input, init)),\n }),\n likesEnabled: flag(requireVar(env, 'LIKES_ENABLED'), 'LIKES_ENABLED'),\n publishingEnabled: flag(requireVar(env, 'PUBLISHING_ENABLED'), 'PUBLISHING_ENABLED'),\n rateLimitPepper: rateLimitPepperFor(env, appEnv),\n };\n configs.set(env, config);\n return config;\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- const e=require("../contract.cjs"),t=require("./_pagination.cjs"),n=require("./http.cjs"),r=require("./sql.cjs");let i=require("@vielzeug/spell");async function databaseCall(e){try{return await e()}catch(e){throw new n.ApiError(500,`internal`,`The service is not available.`,{cause:e,diagnostic:{kind:`database`}})}}var first=(e,t,...n)=>databaseCall(()=>e.DB.prepare(t).bind(...n).first()),all=(e,t,...n)=>databaseCall(async()=>{let r=await e.DB.prepare(t).bind(...n).all();if(!Array.isArray(r.results))throw Error(`Missing database query results`);return r.results}),run=(e,t,...n)=>databaseCall(()=>e.DB.prepare(t).bind(...n).run()),uuid=()=>crypto.randomUUID(),now=()=>new Date().toISOString(),recordUnavailable=()=>new n.ApiError(503,`record-unavailable`,`The record is not available.`,{diagnostic:{kind:`record`}}),a=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,rowToEntry=(e,t)=>{let n;try{n=JSON.parse(t.payload_json)}catch{throw recordUnavailable()}let r=e.contract.entry.safeParse({author:{id:t.owner_id,name:t.author_name},contentHash:t.content_hash,contentVersion:t.content_version,id:t.id,likeCount:t.like_count,payload:n,publishedAt:t.published_at,revision:t.revision,schemaVersion:t.schema_version,updatedAt:t.updated_at});if(!r.success)throw recordUnavailable();return r.data},rowToOwnedEntry=(t,n)=>{let r=rowToEntry(t,n);if(!e.uuidSchema.safeParse(n.client_key).success)throw recordUnavailable();return{...r,clientKey:n.client_key,hidden:n.hidden_at!==null}},entryById=async(e,t,n)=>{let i=await first(t,r.SQL.publication,n,e.name);return i===null?null:rowToEntry(e,i)},searchMatchFor=t=>{if(t===null)return;if(t.length>e.ARCHIVE_LIMITS.searchChars)throw n.badRequest();let r=(t.match(/[\p{L}\p{N}]+/gu)??[]).slice(0,e.ARCHIVE_LIMITS.searchTerms);if(r.length!==0)return r.map(e=>`"${e}"*`).join(` `)},entryCursorContext=e=>JSON.stringify([e.collection,e.sort,e.author??``,e.filters,e.tags,e.searchMatch??``]),cursorFromRow=e=>({createdAt:e.published_at,id:e.id,likeCount:e.like_count}),o=16,vocabValues=(e,t,r)=>{let i=e.getAll(t);if(i.length>o)throw n.badRequest();let a=[...new Set(i)].sort();if(a.some(e=>!r.includes(e)))throw n.badRequest();return a};async function listEntries(i,o,s){let c=new URL(o.url),l=n.queryParams(c,[`author`,`cursor`,`filter`,`limit`,`search`,`sort`,`tag`],[`filter`,`tag`]),u=l.get(`sort`)??`recent`,d=e.archiveSortSchema.safeParse(u);if(!d.success)throw n.badRequest();let f=vocabValues(l,`filter`,i.filters),p=vocabValues(l,`tag`,i.tags),m=l.get(`author`);if(m!==null&&!a.test(m))throw n.badRequest();let h=searchMatchFor(l.get(`search`)),g=entryCursorContext({author:m??void 0,collection:i.name,filters:f,searchMatch:h,sort:d.data,tags:p}),{cursor:_,limit:v}=t.forwardPageParams(l,g),y={author:m??void 0,collection:i.name,cursor:_===null?void 0:{createdAt:_.createdAt,id:_.id,likeCount:_.likeCount??0},filters:f,limit:v+1,searchMatch:h,sort:d.data,tags:p},b=r.listPublicationsSql(y),x=await all(s,b.sql,...b.parameters),S=x.slice(0,v),C=S.at(-1),w={cursor:x.length>v&&C!==void 0?t.nextCursor(cursorFromRow(C),g):null,entries:S.map(e=>rowToEntry(i,e))};return n.json(200,w,n.noStore)}function collectionMeta(t){let r=e.archiveCollectionMetaSchema.safeParse({filters:[...t.filters].sort(),name:t.name,tags:[...t.tags].sort()});if(!r.success)throw recordUnavailable();return n.json(200,r.data,n.noStore)}async function getEntry(e,t,r){let i=await entryById(e,t,r);if(i===null)throw n.notFound();return n.json(200,i,n.noStore)}async function healthJson(t,i){let a=null,o=null;try{a=(await first(i,r.SQL.schemaVersion))?.applied??0}catch{o=`database-unavailable`}o===null&&a!==1&&(o=`schema-mismatch`);let s=e.archiveHealthSchema.safeParse({dbSchemaVersion:a,features:{likes:t.likesEnabled,publishing:t.publishingEnabled},limits:e.ARCHIVE_LIMITS,reason:o,schemaVersion:1,status:o===null?`ok`:`degraded`});if(!s.success)throw recordUnavailable();return n.json(o===null?200:503,s.data,n.noStore)}var identityJson=(t,r)=>{let i=e.archiveIdentitySchema.safeParse(r);if(!i.success)throw recordUnavailable();return n.json(t,i.data,n.noStore)};function getMe(e){return identityJson(200,e?.publisher?{id:e.publisher.id,name:e.publisher.displayName,operator:e.operator}:null)}async function registerMe(e,t,i){let a=await n.readJsonBody(e),o=s.safeParse(a);if(!o.success)throw n.invalid();let c=o.data.name??``,l=await first(t,r.SQL.publisher,i.subject.issuer,i.subject.subject);if(l!==null)return identityJson(200,{id:l.id,name:l.display_name,operator:i.operator});let u=uuid();await run(t,r.SQL.insertPublisher,u,i.subject.issuer,i.subject.subject,c,now(),i.subject.issuer,i.subject.subject,Date.now());let d=await first(t,r.SQL.publisher,i.subject.issuer,i.subject.subject);if(d===null)throw await first(t,r.SQL.revokedSubject,i.subject.issuer,i.subject.subject,Date.now())===null?n.conflict():new n.ApiError(401,`unauthenticated`,`Sign-in is required.`);return identityJson(201,{id:d.id,name:d.display_name,operator:i.operator})}async function renameMe(e,t,i){let a=requirePublisherRow(i),o=await n.readJsonBody(e),s=c.safeParse(o);if(!s.success)throw n.invalid();return s.data.name!==a.displayName&&await run(t,r.SQL.renamePublisher,s.data.name,a.id,s.data.name),identityJson(200,{id:a.id,name:s.data.name,operator:i.operator})}async function deleteProfile(e,t){return t.publisher!==null&&await run(e,r.SQL.deletePublisher,t.publisher.id),new Response(null,{headers:{"cache-control":`no-store`},status:204})}async function deleteAccountProfile(t,i){let{issuer:a,subject:o}=i.subject;return await databaseCall(()=>t.DB.batch([t.DB.prepare(r.SQL.revokeSubject).bind(a,o,Date.now()+(e.SESSION_MAX_AGE_SECONDS+10)*1e3),t.DB.prepare(r.SQL.deleteSubjectPublisher).bind(a,o)])),new Response(null,{headers:n.noStore,status:204})}async function erasePublisher(e,t,i){let a=requireOperatorRow(t);return await databaseCall(()=>e.DB.batch([e.DB.prepare(r.SQL.auditErasure).bind(uuid(),a.id,now(),i),e.DB.prepare(r.SQL.deletePublisher).bind(i)])),new Response(null,{headers:n.noStore,status:204})}async function listModerationLog(i,a,o){requireOperatorRow(o);let s=`moderation-log`,{cursor:c,limit:l}=t.forwardPage(i,s),u=r.moderationLogSql({cursor:c,limit:l+1}),d=await all(a,u.sql,...u.parameters),f=d.slice(0,l),p=f.at(-1),m=e.archiveModerationLogSchema.safeParse({actions:f.map(e=>({action:e.action,collection:e.collection,createdAt:e.created_at,id:e.id,operatorId:e.operator_id,targetId:e.target_id})),cursor:d.length>l&&p!==void 0?t.nextCursor({createdAt:p.created_at,id:p.id},s):null});if(!m.success)throw recordUnavailable();return n.json(200,m.data,n.noStore)}var requirePublisherRow=e=>{if(e.publisher===null)throw n.forbidden(`Register before publishing.`);return e.publisher},requireOperatorRow=e=>{if(!e.operator)throw n.forbidden(`Operator access is required.`);if(e.publisher===null)throw n.forbidden(`Register before moderating.`);return e.publisher},s=i.s.object({name:e.archiveAliasSchema.optional()}),c=i.s.object({name:e.archiveAliasSchema});async function myPublications(t,i,a){if(a.publisher===null)return n.json(200,{entries:[]},n.noStore);let o=await all(i,r.SQL.ownPublications,a.publisher.id,t.name,e.ARCHIVE_LIMITS.maxPublications);return n.json(200,{entries:o.map(e=>rowToOwnedEntry(t,e))},n.noStore)}async function myLikes(i,o,s){let c=new URL(i.url).searchParams.get(`ids`);if(c===null){let a=`likes:${s.publisher?.id??`unregistered`}`,{cursor:c,limit:l}=t.forwardPage(i,a);if(s.publisher===null)return n.json(200,{cursor:null,ids:[]},n.noStore);let u=r.likesPageSql(s.publisher.id,{cursor:c,limit:l+1}),d=await all(o,u.sql,...u.parameters),f=d.slice(0,l),p=f.at(-1),m=e.archiveLikesPageSchema.safeParse({cursor:d.length>l&&p!==void 0?t.nextCursor({createdAt:p.created_at,id:p.publication_id},a):null,ids:f.map(e=>e.publication_id)});if(!m.success)throw recordUnavailable();return n.json(200,m.data,n.noStore)}n.queryParams(new URL(i.url),[`ids`]);let l=[...new Set(c.split(`,`))];if(l.length>e.ARCHIVE_LIMITS.likeStateIds||l.some(e=>!a.test(e)))throw n.badRequest();if(s.publisher===null)return n.json(200,{ids:[]},n.noStore);let u=r.likedPublicationsSql(s.publisher.id,l),d=await all(o,u.sql,...u.parameters),f=new Set(d.map(e=>e.publication_id)),p={ids:l.filter(e=>f.has(e))},m=e.archiveLikedIdsSchema.safeParse(p);if(!m.success)throw recordUnavailable();return n.json(200,m.data,n.noStore)}var l=i.s.object({clientKey:e.uuidSchema,payload:i.s.unknown()}),u=i.s.object({expectedRevision:i.s.number().int().min(1).max(2**53-1),payload:i.s.unknown()});async function resolveRetryRecord(e,t,r,i){if(r===null||r.expires_at<=Date.now())return null;if(r.payload_hash!==i)throw n.conflict(`That key was used for a different payload.`);if(r.publication_id===null)throw new n.ApiError(410,`gone`,`That publication was removed.`);let a=await entryById(e,t,r.publication_id);return a===null?`vanished`:n.json(200,a,n.noStore)}async function publishEntry(t,i,a,o,s){if(!o.publishingEnabled)throw new n.ApiError(503,`publishing-disabled`,`Publishing is currently disabled.`);let c=requirePublisherRow(s),u=await n.readJsonBody(i),d=l.safeParse(u);if(!d.success)throw n.invalid();let{clientKey:f}=d.data,p=await t.derive(d.data.payload);if(p===null)throw n.invalid();let m=p.canonicalJson,h=p.contentHash,g=c.id,_=t.name,v=await resolveRetryRecord(t,a,await first(a,r.SQL.request,g,_,f),h);if(v!==null&&v!==`vanished`)return v;let y=await first(a,r.SQL.publicationByKey,g,_,f);if(y!==null){if(y.hidden_at!==null)throw n.forbidden(`This entry is under moderation review.`);if(y.content_hash!==h)throw n.conflict(`That key was used for a different payload.`);return n.json(200,rowToEntry(t,y),n.noStore)}let b=now(),x=Date.now()+e.ARCHIVE_LIMITS.idempotencyRetentionMs,S=JSON.stringify(p.tags);if(((await databaseCall(()=>a.DB.batch([a.DB.prepare(r.SQL.expireRequests).bind(Date.now()),a.DB.prepare(r.SQL.insertPublication).bind(uuid(),g,_,f,1,p.contentVersion,m,h,p.filter,p.title,S,b,b,g,_,e.ARCHIVE_LIMITS.maxPublications,g,_,f,Date.now()),a.DB.prepare(r.SQL.insertRequest).bind(g,_,f,h,b,x,g,_,f,h)])))[1]?.meta?.changes??0)===0){let e=await resolveRetryRecord(t,a,await first(a,r.SQL.request,g,_,f),h);if(e===`vanished`)throw new n.ApiError(410,`gone`,`That publication was removed.`);if(e!==null)return e;let i=await first(a,r.SQL.publicationByKey,g,_,f);if(i!==null&&i.hidden_at!==null)throw n.forbidden(`This entry is under moderation review.`);if(i!==null&&i.content_hash===h)return n.json(200,rowToEntry(t,i),n.noStore);throw i===null?new n.ApiError(429,`quota`,`Publication quota reached.`):n.conflict(`That key was used for a different payload.`)}let C=await first(a,r.SQL.publicationByKey,g,_,f);if(C===null)throw n.conflict();return n.json(201,rowToEntry(t,C),n.noStore)}async function updateEntry(e,t,i,a,o,s){if(!a.publishingEnabled)throw new n.ApiError(503,`publishing-disabled`,`Publishing is currently disabled.`);let c=requirePublisherRow(o),l=await first(i,r.SQL.publicationAny,s,e.name);if(l===null||l.owner_id!==c.id)throw n.notFound();if(l.hidden_at!==null)throw n.forbidden(`This entry is under moderation review.`);let d=await n.readJsonBody(t),f=u.safeParse(d);if(!f.success)throw n.invalid();let{expectedRevision:p}=f.data;if(p!==l.revision)throw n.conflict();let m=await e.derive(f.data.payload);if(m===null)throw n.invalid();let h=m.canonicalJson,g=m.contentHash;if(g===l.content_hash){let t=await first(i,r.SQL.publicationAny,s,e.name);if(t===null||t.owner_id!==c.id)throw n.notFound();if(t.hidden_at!==null)throw n.forbidden(`This entry is under moderation review.`);if(t.revision!==p)throw n.conflict();return n.json(200,rowToEntry(e,t),n.noStore)}let _=JSON.stringify(m.tags);if(((await run(i,r.SQL.updatePublication,1,m.contentVersion,h,g,m.filter,m.title,_,now(),s,c.id,p,g)).meta?.changes??0)===0){let t=await first(i,r.SQL.publicationAny,s,e.name);throw t===null?n.notFound():t.hidden_at===null?n.conflict():n.forbidden(`This entry is under moderation review.`)}let v=await entryById(e,i,s);if(v===null)throw n.notFound();return n.json(200,v,n.noStore)}async function deleteEntry(e,t,i,a){if(i.operator){let n=requireOperatorRow(i);await databaseCall(()=>t.DB.batch([t.DB.prepare(r.SQL.auditRemoval).bind(uuid(),n.id,now(),a,e.name),t.DB.prepare(r.SQL.removePublicationAny).bind(a,e.name)]))}else{let o=requirePublisherRow(i),s=await first(t,r.SQL.publicationAny,a,e.name);if(s===null)return new Response(null,{headers:n.noStore,status:204});if(s.owner_id!==o.id)throw n.notFound();await run(t,r.SQL.removePublication,a,o.id)}return new Response(null,{headers:n.noStore,status:204})}async function setLike(t,i,a,o,s,c){let l=requirePublisherRow(o);if(c&&!a.likesEnabled)throw new n.ApiError(503,`likes-disabled`,`Likes are currently disabled.`);if(await first(i,r.SQL.publication,s,t.name)===null)throw n.notFound();c?await run(i,r.SQL.insertLike,l.id,now(),s):await run(i,r.SQL.removeLike,s,l.id);let u=await first(i,r.SQL.likeState,l.id,s);if(u===null)throw n.notFound();let d={id:u.id,likeCount:u.like_count,liked:u.liked===1},f=e.archiveLikeStateSchema.safeParse(d);if(!f.success)throw recordUnavailable();return n.json(200,f.data,n.noStore)}var d=i.s.object({reason:i.s.string().check(e.isWellFormedText).trim().min(1).max(e.ARCHIVE_LIMITS.reportReasonChars)});async function reportEntry(e,t,i,a,o){let s=requirePublisherRow(a),c=await n.readJsonBody(t),l=d.safeParse(c);if(!l.success)throw n.invalid();if(await first(i,r.SQL.publication,o,e.name)===null)throw n.notFound();return await run(i,r.SQL.insertReport,uuid(),s.id,l.data.reason,now(),o,e.name),new Response(null,{headers:n.noStore,status:204})}async function setHidden(e,t,i,a,o){let s=requireOperatorRow(i);if(await first(t,r.SQL.publicationAny,a,e.name)===null)throw n.notFound();let c=o?t.DB.prepare(r.SQL.hidePublication).bind(now(),a,e.name):t.DB.prepare(r.SQL.unhidePublication).bind(a,e.name);return await databaseCall(()=>t.DB.batch([t.DB.prepare(r.SQL.auditHide).bind(uuid(),s.id,o?`hide`:`unhide`,now(),a,e.name,Number(o)),c])),new Response(null,{headers:n.noStore,status:204})}async function listReports(i,a,o,s){requireOperatorRow(s);let c=new URL(a.url).searchParams.get(`status`)??`unresolved`;if(c!==`unresolved`&&c!==`resolved`)throw n.badRequest();let l=`reports:${i.name}:${c}`,{cursor:u,limit:d}=t.forwardPage(a,l,[`status`]),f=r.reportsPageSql({collection:i.name,resolved:c===`resolved`},{cursor:u,limit:d+1}),p=await all(o,f.sql,...f.parameters),m=p.slice(0,d),h=m.at(-1),g=e.archiveReportsSchema.safeParse({cursor:p.length>d&&h!==void 0?t.nextCursor({createdAt:h.created_at,id:h.id},l):null,reports:m.map(e=>({createdAt:e.created_at,entryId:e.publication_id,entryTitle:e.entry_title,id:e.id,reason:e.reason,reporter:{id:e.reporter_id,name:e.reporter_name},resolvedAt:e.resolved_at}))});if(!g.success)throw recordUnavailable();return n.json(200,g.data,n.noStore)}async function resolveReport(e,t,i,a){if(requireOperatorRow(i),await first(t,r.SQL.reportById,a,e.name)===null)throw n.notFound();return await run(t,r.SQL.resolveReport,now(),a,e.name),new Response(null,{headers:n.noStore,status:204})}async function myReports(i,a,o){let s=`own-reports:${o.publisher?.id??`unregistered`}`,{cursor:c,limit:l}=t.forwardPage(i,s);if(o.publisher===null)return n.json(200,{cursor:null,reports:[]},n.noStore);let u=r.reportsPageSql({publisherId:o.publisher.id},{cursor:c,limit:l+1}),d=await all(a,u.sql,...u.parameters),f=d.slice(0,l),p=f.at(-1),m=e.archiveOwnReportsSchema.safeParse({cursor:d.length>l&&p!==void 0?t.nextCursor({createdAt:p.created_at,id:p.id},s):null,reports:f.map(e=>({collection:e.collection,createdAt:e.created_at,entryId:e.publication_id,id:e.id,reason:e.reason,resolvedAt:e.resolved_at}))});if(!m.success)throw recordUnavailable();return n.json(200,m.data,n.noStore)}exports.collectionMeta=collectionMeta,exports.deleteAccountProfile=deleteAccountProfile,exports.deleteEntry=deleteEntry,exports.deleteProfile=deleteProfile,exports.erasePublisher=erasePublisher,exports.getEntry=getEntry,exports.getMe=getMe,exports.healthJson=healthJson,exports.listEntries=listEntries,exports.listModerationLog=listModerationLog,exports.listReports=listReports,exports.myLikes=myLikes,exports.myPublications=myPublications,exports.myReports=myReports,exports.publishEntry=publishEntry,exports.registerMe=registerMe,exports.renameMe=renameMe,exports.reportEntry=reportEntry,exports.requireOperatorRow=requireOperatorRow,exports.requirePublisherRow=requirePublisherRow,exports.resolveReport=resolveReport,exports.setHidden=setHidden,exports.setLike=setLike,exports.updateEntry=updateEntry;
1
+ const e=require("../_server/http.cjs"),t=require("../_server/uuid.cjs"),n=require("../contract.cjs"),r=require("./_pagination.cjs"),i=require("./sql.cjs");let a=require("@vielzeug/spell");async function databaseCall(t){try{return await t()}catch(t){throw new e.ApiError(500,`internal`,`The service is not available.`,{cause:t,diagnostic:{kind:`database`}})}}var first=(e,t,...n)=>databaseCall(()=>e.DB.prepare(t).bind(...n).first()),all=(e,t,...n)=>databaseCall(async()=>{let r=await e.DB.prepare(t).bind(...n).all();if(!Array.isArray(r.results))throw Error(`Missing database query results`);return r.results}),run=(e,t,...n)=>databaseCall(()=>e.DB.prepare(t).bind(...n).run()),uuid=()=>crypto.randomUUID(),now=()=>new Date().toISOString(),recordUnavailable=()=>new e.ApiError(503,`record-unavailable`,`The record is not available.`,{diagnostic:{kind:`record`}}),rowToEntry=(e,t)=>{let n;try{n=JSON.parse(t.payload_json)}catch{throw recordUnavailable()}let r=e.contract.entry.safeParse({author:{id:t.owner_id,name:t.author_name},contentHash:t.content_hash,contentVersion:t.content_version,id:t.id,likeCount:t.like_count,payload:n,publishedAt:t.published_at,revision:t.revision,schemaVersion:t.schema_version,updatedAt:t.updated_at});if(!r.success)throw recordUnavailable();return r.data},rowToOwnedEntry=(e,t)=>{let r=rowToEntry(e,t);if(!n.uuidSchema.safeParse(t.client_key).success)throw recordUnavailable();return{...r,clientKey:t.client_key,hidden:t.hidden_at!==null}},entryById=async(e,t,n)=>{let r=await first(t,i.SQL.publication,n,e.name);return r===null?null:rowToEntry(e,r)},searchMatchFor=t=>{if(t===null)return;if(t.length>n.ARCHIVE_LIMITS.searchChars)throw e.badRequest();let r=(t.match(/[\p{L}\p{N}]+/gu)??[]).slice(0,n.ARCHIVE_LIMITS.searchTerms);if(r.length!==0)return r.map(e=>`"${e}"*`).join(` `)},entryCursorContext=e=>JSON.stringify([e.collection,e.sort,e.author??``,e.filters,e.tags,e.searchMatch??``]),cursorFromRow=e=>({createdAt:e.published_at,id:e.id,likeCount:e.like_count}),o=16,vocabValues=(t,n,r)=>{let i=t.getAll(n);if(i.length>o)throw e.badRequest();let a=[...new Set(i)].sort();if(a.some(e=>!r.includes(e)))throw e.badRequest();return a};async function listEntries(a,o,s){let c=new URL(o.url),l=e.queryParams(c,[`author`,`cursor`,`filter`,`limit`,`search`,`sort`,`tag`],[`filter`,`tag`]),u=l.get(`sort`)??`recent`,d=n.archiveSortSchema.safeParse(u);if(!d.success)throw e.badRequest();let f=vocabValues(l,`filter`,a.filters),p=vocabValues(l,`tag`,a.tags),m=l.get(`author`);if(m!==null&&!t.UUID_RE.test(m))throw e.badRequest();let h=searchMatchFor(l.get(`search`)),g=entryCursorContext({author:m??void 0,collection:a.name,filters:f,searchMatch:h,sort:d.data,tags:p}),{cursor:_,limit:v}=r.forwardPageParams(l,g),y={author:m??void 0,collection:a.name,cursor:_===null?void 0:{createdAt:_.createdAt,id:_.id,likeCount:_.likeCount??0},filters:f,limit:v+1,searchMatch:h,sort:d.data,tags:p},b=i.listPublicationsSql(y),x=await all(s,b.sql,...b.parameters),S=x.slice(0,v),C=S.at(-1),w={cursor:x.length>v&&C!==void 0?r.nextCursor(cursorFromRow(C),g):null,entries:S.map(e=>rowToEntry(a,e))};return e.json(200,w,e.noStore)}function collectionMeta(t){let r=n.archiveCollectionMetaSchema.safeParse({filters:[...t.filters].sort(),name:t.name,tags:[...t.tags].sort()});if(!r.success)throw recordUnavailable();return e.json(200,r.data,e.noStore)}async function getEntry(t,n,r){let i=await entryById(t,n,r);if(i===null)throw e.notFound();return e.json(200,i,e.noStore)}async function healthJson(t,r){let a=null,o=null;try{a=(await first(r,i.SQL.schemaVersion))?.applied??0}catch{o=`database-unavailable`}o===null&&a!==1&&(o=`schema-mismatch`);let s=n.archiveHealthSchema.safeParse({dbSchemaVersion:a,features:{likes:t.likesEnabled,publishing:t.publishingEnabled},limits:n.ARCHIVE_LIMITS,reason:o,schemaVersion:1,status:o===null?`ok`:`degraded`});if(!s.success)throw recordUnavailable();return e.json(o===null?200:503,s.data,e.noStore)}var identityJson=(t,r)=>{let i=n.archiveIdentitySchema.safeParse(r);if(!i.success)throw recordUnavailable();return e.json(t,i.data,e.noStore)};function getMe(e){return identityJson(200,e?.publisher?{id:e.publisher.id,name:e.publisher.displayName,operator:e.operator}:null)}async function registerMe(t,n,r){let a=await e.readJsonBody(t),o=s.safeParse(a);if(!o.success)throw e.invalid();let c=o.data.name??``,l=await first(n,i.SQL.publisher,r.subject.issuer,r.subject.subject);if(l!==null)return identityJson(200,{id:l.id,name:l.display_name,operator:r.operator});let u=uuid();await run(n,i.SQL.insertPublisher,u,r.subject.issuer,r.subject.subject,c,now(),r.subject.issuer,r.subject.subject,Date.now());let d=await first(n,i.SQL.publisher,r.subject.issuer,r.subject.subject);if(d===null)throw await first(n,i.SQL.revokedSubject,r.subject.issuer,r.subject.subject,Date.now())===null?e.conflict():new e.ApiError(401,`unauthenticated`,`Sign-in is required.`);return identityJson(201,{id:d.id,name:d.display_name,operator:r.operator})}async function renameMe(t,n,r){let a=requirePublisherRow(r),o=await e.readJsonBody(t),s=c.safeParse(o);if(!s.success)throw e.invalid();return s.data.name!==a.displayName&&await run(n,i.SQL.renamePublisher,s.data.name,a.id,s.data.name),identityJson(200,{id:a.id,name:s.data.name,operator:r.operator})}async function deleteProfile(e,t){return t.publisher!==null&&await run(e,i.SQL.deletePublisher,t.publisher.id),new Response(null,{headers:{"cache-control":`no-store`},status:204})}async function deleteAccountProfile(t,r){let{issuer:a,subject:o}=r.subject;return await databaseCall(()=>t.DB.batch([t.DB.prepare(i.SQL.revokeSubject).bind(a,o,Date.now()+(n.SESSION_MAX_AGE_SECONDS+10)*1e3),t.DB.prepare(i.SQL.deleteSubjectPublisher).bind(a,o)])),new Response(null,{headers:e.noStore,status:204})}async function erasePublisher(t,n,r){let a=requireOperatorRow(n);return await databaseCall(()=>t.DB.batch([t.DB.prepare(i.SQL.auditErasure).bind(uuid(),a.id,now(),r),t.DB.prepare(i.SQL.deletePublisher).bind(r)])),new Response(null,{headers:e.noStore,status:204})}async function listModerationLog(t,a,o){requireOperatorRow(o);let s=`moderation-log`,{cursor:c,limit:l}=r.forwardPage(t,s),u=i.moderationLogSql({cursor:c,limit:l+1}),d=await all(a,u.sql,...u.parameters),f=d.slice(0,l),p=f.at(-1),m=n.archiveModerationLogSchema.safeParse({actions:f.map(e=>({action:e.action,collection:e.collection,createdAt:e.created_at,id:e.id,operatorId:e.operator_id,targetId:e.target_id})),cursor:d.length>l&&p!==void 0?r.nextCursor({createdAt:p.created_at,id:p.id},s):null});if(!m.success)throw recordUnavailable();return e.json(200,m.data,e.noStore)}var requirePublisherRow=t=>{if(t.publisher===null)throw e.forbidden(`Register before publishing.`);return t.publisher},requireOperatorRow=t=>{if(!t.operator)throw e.forbidden(`Operator access is required.`);if(t.publisher===null)throw e.forbidden(`Register before moderating.`);return t.publisher},s=a.s.object({name:n.archiveAliasSchema.optional()}),c=a.s.object({name:n.archiveAliasSchema});async function myPublications(t,r,a){if(a.publisher===null)return e.json(200,{entries:[]},e.noStore);let o=await all(r,i.SQL.ownPublications,a.publisher.id,t.name,n.ARCHIVE_LIMITS.maxPublications);return e.json(200,{entries:o.map(e=>rowToOwnedEntry(t,e))},e.noStore)}async function myLikes(a,o,s){let c=new URL(a.url).searchParams.get(`ids`);if(c===null){let t=`likes:${s.publisher?.id??`unregistered`}`,{cursor:c,limit:l}=r.forwardPage(a,t);if(s.publisher===null)return e.json(200,{cursor:null,ids:[]},e.noStore);let u=i.likesPageSql(s.publisher.id,{cursor:c,limit:l+1}),d=await all(o,u.sql,...u.parameters),f=d.slice(0,l),p=f.at(-1),m=n.archiveLikesPageSchema.safeParse({cursor:d.length>l&&p!==void 0?r.nextCursor({createdAt:p.created_at,id:p.publication_id},t):null,ids:f.map(e=>e.publication_id)});if(!m.success)throw recordUnavailable();return e.json(200,m.data,e.noStore)}e.queryParams(new URL(a.url),[`ids`]);let l=[...new Set(c.split(`,`))];if(l.length>n.ARCHIVE_LIMITS.likeStateIds||l.some(e=>!t.UUID_RE.test(e)))throw e.badRequest();if(s.publisher===null)return e.json(200,{ids:[]},e.noStore);let u=i.likedPublicationsSql(s.publisher.id,l),d=await all(o,u.sql,...u.parameters),f=new Set(d.map(e=>e.publication_id)),p={ids:l.filter(e=>f.has(e))},m=n.archiveLikedIdsSchema.safeParse(p);if(!m.success)throw recordUnavailable();return e.json(200,m.data,e.noStore)}var l=a.s.object({clientKey:n.uuidSchema,payload:a.s.unknown()}),u=a.s.object({expectedRevision:a.s.number().int().min(1).max(2**53-1),payload:a.s.unknown()});async function resolveRetryRecord(t,n,r,i){if(r===null||r.expires_at<=Date.now())return null;if(r.payload_hash!==i)throw e.conflict(`That key was used for a different payload.`);if(r.publication_id===null)throw new e.ApiError(410,`gone`,`That publication was removed.`);let a=await entryById(t,n,r.publication_id);return a===null?`vanished`:e.json(200,a,e.noStore)}async function publishEntry(t,r,a,o,s){if(!o.publishingEnabled)throw new e.ApiError(503,`publishing-disabled`,`Publishing is currently disabled.`);let c=requirePublisherRow(s),u=await e.readJsonBody(r),d=l.safeParse(u);if(!d.success)throw e.invalid();let{clientKey:f}=d.data,p=await t.derive(d.data.payload);if(p===null)throw e.invalid();let m=p.canonicalJson,h=p.contentHash,g=c.id,_=t.name,v=await resolveRetryRecord(t,a,await first(a,i.SQL.request,g,_,f),h);if(v!==null&&v!==`vanished`)return v;let y=await first(a,i.SQL.publicationByKey,g,_,f);if(y!==null){if(y.hidden_at!==null)throw e.forbidden(`This entry is under moderation review.`);if(y.content_hash!==h)throw e.conflict(`That key was used for a different payload.`);return e.json(200,rowToEntry(t,y),e.noStore)}let b=now(),x=Date.now()+n.ARCHIVE_LIMITS.idempotencyRetentionMs,S=JSON.stringify(p.tags);if(((await databaseCall(()=>a.DB.batch([a.DB.prepare(i.SQL.expireRequests).bind(Date.now()),a.DB.prepare(i.SQL.insertPublication).bind(uuid(),g,_,f,1,p.contentVersion,m,h,p.filter,p.title,S,b,b,g,_,n.ARCHIVE_LIMITS.maxPublications,g,_,f,Date.now()),a.DB.prepare(i.SQL.insertRequest).bind(g,_,f,h,b,x,g,_,f,h)])))[1]?.meta?.changes??0)===0){let n=await resolveRetryRecord(t,a,await first(a,i.SQL.request,g,_,f),h);if(n===`vanished`)throw new e.ApiError(410,`gone`,`That publication was removed.`);if(n!==null)return n;let r=await first(a,i.SQL.publicationByKey,g,_,f);if(r!==null&&r.hidden_at!==null)throw e.forbidden(`This entry is under moderation review.`);if(r!==null&&r.content_hash===h)return e.json(200,rowToEntry(t,r),e.noStore);throw r===null?new e.ApiError(429,`quota`,`Publication quota reached.`):e.conflict(`That key was used for a different payload.`)}let C=await first(a,i.SQL.publicationByKey,g,_,f);if(C===null)throw e.conflict();return e.json(201,rowToEntry(t,C),e.noStore)}async function updateEntry(t,n,r,a,o,s){if(!a.publishingEnabled)throw new e.ApiError(503,`publishing-disabled`,`Publishing is currently disabled.`);let c=requirePublisherRow(o),l=await first(r,i.SQL.publicationAny,s,t.name);if(l===null||l.owner_id!==c.id)throw e.notFound();if(l.hidden_at!==null)throw e.forbidden(`This entry is under moderation review.`);let d=await e.readJsonBody(n),f=u.safeParse(d);if(!f.success)throw e.invalid();let{expectedRevision:p}=f.data;if(p!==l.revision)throw e.conflict();let m=await t.derive(f.data.payload);if(m===null)throw e.invalid();let h=m.canonicalJson,g=m.contentHash;if(g===l.content_hash){let n=await first(r,i.SQL.publicationAny,s,t.name);if(n===null||n.owner_id!==c.id)throw e.notFound();if(n.hidden_at!==null)throw e.forbidden(`This entry is under moderation review.`);if(n.revision!==p)throw e.conflict();return e.json(200,rowToEntry(t,n),e.noStore)}let _=JSON.stringify(m.tags);if(((await run(r,i.SQL.updatePublication,1,m.contentVersion,h,g,m.filter,m.title,_,now(),s,c.id,p,g)).meta?.changes??0)===0){let n=await first(r,i.SQL.publicationAny,s,t.name);throw n===null?e.notFound():n.hidden_at===null?e.conflict():e.forbidden(`This entry is under moderation review.`)}let v=await entryById(t,r,s);if(v===null)throw e.notFound();return e.json(200,v,e.noStore)}async function deleteEntry(t,n,r,a){if(r.operator){let e=requireOperatorRow(r);await databaseCall(()=>n.DB.batch([n.DB.prepare(i.SQL.auditRemoval).bind(uuid(),e.id,now(),a,t.name),n.DB.prepare(i.SQL.removePublicationAny).bind(a,t.name)]))}else{let o=requirePublisherRow(r),s=await first(n,i.SQL.publicationAny,a,t.name);if(s===null)return new Response(null,{headers:e.noStore,status:204});if(s.owner_id!==o.id)throw e.notFound();await run(n,i.SQL.removePublication,a,o.id)}return new Response(null,{headers:e.noStore,status:204})}async function setLike(t,r,a,o,s,c){let l=requirePublisherRow(o);if(c&&!a.likesEnabled)throw new e.ApiError(503,`likes-disabled`,`Likes are currently disabled.`);if(await first(r,i.SQL.publication,s,t.name)===null)throw e.notFound();c?await run(r,i.SQL.insertLike,l.id,now(),s):await run(r,i.SQL.removeLike,s,l.id);let u=await first(r,i.SQL.likeState,l.id,s);if(u===null)throw e.notFound();let d={id:u.id,likeCount:u.like_count,liked:u.liked===1},f=n.archiveLikeStateSchema.safeParse(d);if(!f.success)throw recordUnavailable();return e.json(200,f.data,e.noStore)}var d=a.s.object({reason:a.s.string().check(n.isWellFormedText).trim().min(1).max(n.ARCHIVE_LIMITS.reportReasonChars)});async function reportEntry(t,n,r,a,o){let s=requirePublisherRow(a),c=await e.readJsonBody(n),l=d.safeParse(c);if(!l.success)throw e.invalid();if(await first(r,i.SQL.publication,o,t.name)===null)throw e.notFound();return await run(r,i.SQL.insertReport,uuid(),s.id,l.data.reason,now(),o,t.name),new Response(null,{headers:e.noStore,status:204})}async function setHidden(t,n,r,a,o){let s=requireOperatorRow(r);if(await first(n,i.SQL.publicationAny,a,t.name)===null)throw e.notFound();let c=o?n.DB.prepare(i.SQL.hidePublication).bind(now(),a,t.name):n.DB.prepare(i.SQL.unhidePublication).bind(a,t.name);return await databaseCall(()=>n.DB.batch([n.DB.prepare(i.SQL.auditHide).bind(uuid(),s.id,o?`hide`:`unhide`,now(),a,t.name,Number(o)),c])),new Response(null,{headers:e.noStore,status:204})}async function listReports(t,a,o,s){requireOperatorRow(s);let c=new URL(a.url).searchParams.get(`status`)??`unresolved`;if(c!==`unresolved`&&c!==`resolved`)throw e.badRequest();let l=`reports:${t.name}:${c}`,{cursor:u,limit:d}=r.forwardPage(a,l,[`status`]),f=i.reportsPageSql({collection:t.name,resolved:c===`resolved`},{cursor:u,limit:d+1}),p=await all(o,f.sql,...f.parameters),m=p.slice(0,d),h=m.at(-1),g=n.archiveReportsSchema.safeParse({cursor:p.length>d&&h!==void 0?r.nextCursor({createdAt:h.created_at,id:h.id},l):null,reports:m.map(e=>({createdAt:e.created_at,entryId:e.publication_id,entryTitle:e.entry_title,id:e.id,reason:e.reason,reporter:{id:e.reporter_id,name:e.reporter_name},resolvedAt:e.resolved_at}))});if(!g.success)throw recordUnavailable();return e.json(200,g.data,e.noStore)}async function resolveReport(t,n,r,a){if(requireOperatorRow(r),await first(n,i.SQL.reportById,a,t.name)===null)throw e.notFound();return await run(n,i.SQL.resolveReport,now(),a,t.name),new Response(null,{headers:e.noStore,status:204})}async function myReports(t,a,o){let s=`own-reports:${o.publisher?.id??`unregistered`}`,{cursor:c,limit:l}=r.forwardPage(t,s);if(o.publisher===null)return e.json(200,{cursor:null,reports:[]},e.noStore);let u=i.reportsPageSql({publisherId:o.publisher.id},{cursor:c,limit:l+1}),d=await all(a,u.sql,...u.parameters),f=d.slice(0,l),p=f.at(-1),m=n.archiveOwnReportsSchema.safeParse({cursor:d.length>l&&p!==void 0?r.nextCursor({createdAt:p.created_at,id:p.id},s):null,reports:f.map(e=>({collection:e.collection,createdAt:e.created_at,entryId:e.publication_id,id:e.id,reason:e.reason,resolvedAt:e.resolved_at}))});if(!m.success)throw recordUnavailable();return e.json(200,m.data,e.noStore)}exports.collectionMeta=collectionMeta,exports.deleteAccountProfile=deleteAccountProfile,exports.deleteEntry=deleteEntry,exports.deleteProfile=deleteProfile,exports.erasePublisher=erasePublisher,exports.getEntry=getEntry,exports.getMe=getMe,exports.healthJson=healthJson,exports.listEntries=listEntries,exports.listModerationLog=listModerationLog,exports.listReports=listReports,exports.myLikes=myLikes,exports.myPublications=myPublications,exports.myReports=myReports,exports.publishEntry=publishEntry,exports.registerMe=registerMe,exports.renameMe=renameMe,exports.reportEntry=reportEntry,exports.requireOperatorRow=requireOperatorRow,exports.requirePublisherRow=requirePublisherRow,exports.resolveReport=resolveReport,exports.setHidden=setHidden,exports.setLike=setLike,exports.updateEntry=updateEntry;
2
2
  //# sourceMappingURL=publications.cjs.map