@vielzeug/archive 26.10.6 → 26.10.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/_crypto.cjs +2 -0
- package/dist/_crypto.cjs.map +1 -0
- package/dist/_crypto.d.ts +7 -0
- package/dist/_crypto.d.ts.map +1 -0
- package/dist/_crypto.js +2 -0
- package/dist/_crypto.js.map +1 -0
- package/dist/auth/worker.cjs +4 -4
- package/dist/auth/worker.cjs.map +1 -1
- package/dist/auth/worker.d.ts.map +1 -1
- package/dist/auth/worker.js +4 -4
- package/dist/auth/worker.js.map +1 -1
- package/dist/auth.cjs +1 -1
- package/dist/auth.d.ts +0 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +1 -1
- package/dist/collection.cjs +1 -1
- package/dist/collection.cjs.map +1 -1
- package/dist/collection.d.ts +9 -1
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +1 -1
- package/dist/collection.js.map +1 -1
- package/dist/contract.cjs.map +1 -1
- package/dist/contract.d.ts +4 -3
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js.map +1 -1
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/worker/_pagination.cjs +1 -1
- package/dist/worker/_pagination.cjs.map +1 -1
- package/dist/worker/_pagination.d.ts +20 -7
- package/dist/worker/_pagination.d.ts.map +1 -1
- package/dist/worker/_pagination.js +1 -1
- package/dist/worker/_pagination.js.map +1 -1
- package/dist/worker/env.cjs +1 -1
- package/dist/worker/env.cjs.map +1 -1
- package/dist/worker/env.d.ts.map +1 -1
- package/dist/worker/env.js +1 -1
- package/dist/worker/env.js.map +1 -1
- package/dist/worker/publications.cjs +1 -1
- package/dist/worker/publications.cjs.map +1 -1
- package/dist/worker/publications.d.ts.map +1 -1
- package/dist/worker/publications.js +1 -1
- package/dist/worker/publications.js.map +1 -1
- package/dist/worker/sql.cjs +4 -4
- package/dist/worker/sql.cjs.map +1 -1
- package/dist/worker/sql.d.ts +3 -2
- package/dist/worker/sql.d.ts.map +1 -1
- package/dist/worker/sql.js +4 -4
- package/dist/worker/sql.js.map +1 -1
- package/package.json +2 -2
package/dist/contract.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract.cjs","names":[],"sources":["../src/contract.ts"],"sourcesContent":["import { type InferOutput, type Schema, s } from '@vielzeug/spell';\n\n/** Shared naming: one place so the client, Worker and issuer cannot drift. */\nexport const SESSION_COOKIE_NAME = 'archive_session';\nexport const SESSION_COOKIE_PATH = '/api/v1';\n/**\n * The maximum age a session JWT may carry, and the cookie Max-Age an issuer sets:\n * a long-lived signed-in state. The Worker rejects older tokens at this bound.\n */\nexport const SESSION_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;\nexport const OPERATOR_SESSION_MAX_AGE_SECONDS = 15 * 60;\n\n/**\n * The full Set-Cookie value for the session cookie: name, token and the security\n * attributes every issuer and the Worker must agree on — HttpOnly, SameSite=Strict,\n * the contract Path, Max-Age, and Secure in production. Issuers mint with the\n * session's max age; the Worker and issuers clear with `maxAgeSeconds: 0`. Never\n * hand-compose the attributes: the cookie's CSRF and theft posture is contract,\n * not per-issuer choice.\n */\nexport const sessionCookieValue = (token: string, options: { maxAgeSeconds: number; secure: boolean }): string =>\n `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Path=${SESSION_COOKIE_PATH}; Max-Age=${options.maxAgeSeconds}${options.secure ? '; Secure' : ''}`;\n\nexport const PUBLISHER_HEADER = 'x-archive-publisher';\n/** Pins account erasure to the issuer account shown when confirmation began. */\nexport const ACCOUNT_HEADER = 'X-Archive-Account';\nexport const REQUEST_MARKER_HEADER = 'x-archive-request';\n/** The wire-contract version advertised by health and stamped on every entry envelope. */\nexport const ARCHIVE_CONTRACT_VERSION = 1;\n/**\n * The storage schema version: the `archive_schema_meta` stamp the package's migration\n * writes and the Worker reads back through health. A mismatch answers `degraded`\n * instead of `ok` — a stale database is the most likely deployment fault.\n */\nexport const ARCHIVE_SCHEMA_VERSION = 1;\n/** Canonical issuer database initialization stamp. */\nexport const AUTH_SCHEMA_VERSION = 1;\n/** The `roles` claim value a session JWT carries for operator authority (moderation). */\nexport const ARCHIVE_OPERATOR_ROLE = 'archive-operator';\n/** The routable collection-name rule: lowercase `[a-z0-9-]`, at most 32 chars. */\nexport const ARCHIVE_COLLECTION_NAME_PATTERN = /^[a-z0-9-]{1,32}$/;\n\n// Exported schemas carry explicit public `Schema<Output>` annotations: the concrete\n// spell schema classes are not part of spell's public export surface, so inferred\n// types would not be portable across package declaration files.\nexport const uuidSchema: Schema<string> = s.string().uuid();\nexport const isoTimestampSchema: Schema<string> = s.string().isoDateTime();\nexport const hex64Schema: Schema<string> = s.string().regex(/^[0-9a-f]{64}$/);\n/** Well-formed user text: no lone UTF-16 surrogates, no C0/C1/DEL control characters. */\nexport const ILL_FORMED_TEXT = /[\\p{Cs}\\p{Cc}]/u;\nexport const isWellFormedText = (value: string): boolean => !ILL_FORMED_TEXT.test(value);\nconst textString = () => s.string().check(isWellFormedText);\n/** The one alias rule, shared by identity mutations and every emitted DTO. */\nexport const archiveAliasSchema: Schema<string> = textString().trim().max(24);\n\nexport const archiveAuthorSchema: Schema<{ id: string; name: string }> = s.object({\n id: uuidSchema,\n name: archiveAliasSchema,\n});\nexport const archiveIdentitySchema: Schema<{ id: string; name: string; operator: boolean } | null> = s\n .object({ id: uuidSchema, name: archiveAliasSchema, operator: s.boolean() })\n .nullable();\nexport type ArchiveIdentity = InferOutput<typeof archiveIdentitySchema>;\n/** The issuer account metadata export; its id is the session subject, not a publisher id. */\nexport const archiveAccountSchema: Schema<{\n createdAt: string;\n email: string;\n id: string;\n lastSignInAt: string | null;\n}> = s.object({\n createdAt: isoTimestampSchema,\n email: s.string().email(),\n id: uuidSchema,\n lastSignInAt: isoTimestampSchema.nullable(),\n});\nexport type ArchiveAccount = InferOutput<typeof archiveAccountSchema>;\nexport const archiveLikedIdsSchema: Schema<{ ids: string[] }> = s.object({ ids: s.array(uuidSchema) });\nexport const archiveLikesPageSchema: Schema<{ cursor: string | null; ids: string[] }> = s.object({\n cursor: s.string().nullable(),\n ids: s.array(uuidSchema),\n});\nexport type ArchiveLikesPage = InferOutput<typeof archiveLikesPageSchema>;\nexport const archiveLikeStateSchema: Schema<{ id: string; likeCount: number; liked: boolean }> = s.object({\n id: uuidSchema,\n likeCount: s.number().int().min(0).max(Number.MAX_SAFE_INTEGER),\n liked: s.boolean(),\n});\n\n/** Application bounds shared by handlers, the health route, and clients. */\nexport const ARCHIVE_LIMITS = Object.freeze({\n aliasChars: 24,\n cursorChars: 1024,\n idempotencyRetentionMs: 48 * 60 * 60 * 1000,\n likeStateIds: 50,\n maxPageSize: 50,\n maxPublications: 50,\n pageSize: 10,\n payloadBytes: 8192,\n publicCacheSeconds: 30,\n /** One free-text moderation reason: trimmed, non-empty, at most this many chars. */\n reportReasonChars: 256,\n requestBytes: 12 * 1024,\n searchChars: 64,\n searchTerms: 6,\n tagsPerEntry: 16,\n titleChars: 128,\n /** One declared filter or tag value: well-formed, non-empty, at most this many chars. */\n vocabChars: 64,\n});\nexport const archiveLimitsSchema: Schema<{\n aliasChars: number;\n cursorChars: number;\n idempotencyRetentionMs: number;\n likeStateIds: number;\n maxPageSize: number;\n maxPublications: number;\n pageSize: number;\n payloadBytes: number;\n publicCacheSeconds: number;\n reportReasonChars: number;\n requestBytes: number;\n searchChars: number;\n searchTerms: number;\n tagsPerEntry: number;\n titleChars: number;\n vocabChars: number;\n}> = s.object({\n aliasChars: s.number().int().min(1),\n cursorChars: s.number().int().min(1),\n idempotencyRetentionMs: s.number().int().min(1),\n likeStateIds: s.number().int().min(1),\n maxPageSize: s.number().int().min(1),\n maxPublications: s.number().int().min(1),\n pageSize: s.number().int().min(1),\n payloadBytes: s.number().int().min(1),\n publicCacheSeconds: s.number().int().min(0),\n reportReasonChars: s.number().int().min(1),\n requestBytes: s.number().int().min(1),\n searchChars: s.number().int().min(1),\n searchTerms: s.number().int().min(1),\n tagsPerEntry: s.number().int().min(1),\n titleChars: s.number().int().min(1),\n vocabChars: s.number().int().min(1),\n});\nexport const archiveHealthSchema: Schema<{\n /** The storage schema stamp (`archive_schema_meta`) the database actually carries. */\n dbSchemaVersion: number | null;\n features: { likes: boolean; publishing: boolean };\n limits: InferOutput<typeof archiveLimitsSchema>;\n schemaVersion: 1;\n /** 'degraded' when the database's storage schema does not match the Worker's. */\n status: 'ok' | 'degraded';\n reason: 'database-unavailable' | 'schema-mismatch' | null;\n}> = s.object({\n dbSchemaVersion: s.number().int().min(0).nullable(),\n features: s.object({ likes: s.boolean(), publishing: s.boolean() }),\n limits: archiveLimitsSchema,\n reason: s.enum(['database-unavailable', 'schema-mismatch'] as const).nullable(),\n schemaVersion: s.literal(ARCHIVE_CONTRACT_VERSION),\n status: s.enum(['ok', 'degraded'] as const),\n});\nexport type ArchiveHealth = InferOutput<typeof archiveHealthSchema>;\n\nexport const archiveAuthHealthSchema: Schema<{\n dbSchemaVersion: number | null;\n schemaVersion: 1;\n status: 'ok' | 'degraded';\n reason: 'configuration-unavailable' | 'database-unavailable' | 'schema-mismatch' | null;\n}> = s.object({\n dbSchemaVersion: s.number().int().min(0).nullable(),\n reason: s.enum(['configuration-unavailable', 'database-unavailable', 'schema-mismatch'] as const).nullable(),\n schemaVersion: s.literal(AUTH_SCHEMA_VERSION),\n status: s.enum(['ok', 'degraded'] as const),\n});\nexport type AuthHealth = InferOutput<typeof archiveAuthHealthSchema>;\n\n/** The Worker's full error-code vocabulary — the contract both sides test against. */\nexport const ARCHIVE_ERROR_CODES = [\n 'account-changed',\n 'auth-unavailable',\n 'bad-request',\n 'conflict',\n 'forbidden',\n 'gone',\n 'internal',\n 'invalid-payload',\n 'likes-disabled',\n 'method-not-allowed',\n 'not-found',\n 'publisher-changed',\n 'publishing-disabled',\n 'quota',\n 'rate-limited',\n 'record-unavailable',\n 'timeout',\n 'too-large',\n 'unauthenticated',\n 'unavailable',\n 'unsupported-media',\n] as const;\nexport type ArchiveErrorCode = (typeof ARCHIVE_ERROR_CODES)[number];\n\n/**\n * The error envelope every route answers failures with: `{ error: { code, message,\n * requestId } }`. Clients validate against this one schema — an envelope carrying an\n * unknown code or malformed fields is not an archive error at all.\n */\nexport const archiveErrorResponseSchema: Schema<{\n error: { code: ArchiveErrorCode; message: string; requestId: string };\n}> = s\n .object({\n error: s\n .object({\n code: s.enum(ARCHIVE_ERROR_CODES),\n message: s.string().min(1).max(256),\n requestId: uuidSchema,\n })\n .strict(),\n })\n .strict();\nexport type ArchiveErrorResponse = InferOutput<typeof archiveErrorResponseSchema>;\n\n/** The sign-out acknowledgement: the only non-collection response body, shared by both sides. */\nexport const archiveOkSchema: Schema<{ ok: true }> = s.object({ ok: s.literal(true) }).strict();\n\n/** One collection's public self-description: its name and declared query vocabularies. */\nexport const archiveCollectionMetaSchema: Schema<{\n filters: string[];\n name: string;\n tags: string[];\n}> = s.object({\n filters: s.array(s.string().min(1).max(ARCHIVE_LIMITS.vocabChars)),\n name: s.string().regex(ARCHIVE_COLLECTION_NAME_PATTERN),\n tags: s.array(s.string().min(1).max(ARCHIVE_LIMITS.vocabChars)),\n});\nexport type ArchiveCollectionMeta = InferOutput<typeof archiveCollectionMetaSchema>;\n\n/** One reader report an operator reviews: what was reported, why, and by whom. */\nexport const archiveReportsSchema: Schema<{\n cursor: string | null;\n reports: {\n id: string;\n createdAt: string;\n entryId: string;\n entryTitle: string;\n reason: string;\n reporter: { id: string; name: string };\n resolvedAt: string | null;\n }[];\n}> = s.object({\n cursor: s.string().nullable(),\n reports: s.array(\n s.object({\n createdAt: isoTimestampSchema,\n entryId: uuidSchema,\n entryTitle: s.string().min(1).max(ARCHIVE_LIMITS.titleChars),\n id: uuidSchema,\n reason: s.string().min(1).max(ARCHIVE_LIMITS.reportReasonChars),\n reporter: archiveAuthorSchema,\n resolvedAt: isoTimestampSchema.nullable(),\n }),\n ),\n});\nexport type ArchiveReports = InferOutput<typeof archiveReportsSchema>;\nexport const archiveOwnReportsSchema: Schema<{\n cursor: string | null;\n reports: {\n collection: string;\n createdAt: string;\n entryId: string;\n id: string;\n reason: string;\n resolvedAt: string | null;\n }[];\n}> = s.object({\n cursor: s.string().nullable(),\n reports: s.array(\n s.object({\n collection: s.string().regex(ARCHIVE_COLLECTION_NAME_PATTERN),\n createdAt: isoTimestampSchema,\n entryId: uuidSchema,\n id: uuidSchema,\n reason: s.string().min(1).max(ARCHIVE_LIMITS.reportReasonChars),\n resolvedAt: isoTimestampSchema.nullable(),\n }),\n ),\n});\nexport type ArchiveOwnReports = InferOutput<typeof archiveOwnReportsSchema>;\n\n/** The moderation actions an operator can take; the audit log's whole vocabulary. */\nexport const ARCHIVE_MODERATION_ACTIONS = ['hide', 'unhide', 'remove', 'erase-publisher'] as const;\nexport type ArchiveModerationAction = (typeof ARCHIVE_MODERATION_ACTIONS)[number];\n\n/**\n * The operator accountability log: one row per effective moderation action, newest\n * first. Operator and target travel as opaque ids only — no names (they change), no\n * content — so the log stays honest after renames and survives the erasures it records.\n */\nexport const archiveModerationLogSchema: Schema<{\n cursor: string | null;\n actions: {\n id: string;\n action: ArchiveModerationAction;\n collection: string | null;\n createdAt: string;\n operatorId: string;\n targetId: string;\n }[];\n}> = s.object({\n actions: s.array(\n s.object({\n action: s.enum(ARCHIVE_MODERATION_ACTIONS),\n collection: s.string().regex(ARCHIVE_COLLECTION_NAME_PATTERN).nullable(),\n createdAt: isoTimestampSchema,\n id: uuidSchema,\n operatorId: uuidSchema,\n targetId: uuidSchema,\n }),\n ),\n cursor: s.string().nullable(),\n});\nexport type ArchiveModerationLog = InferOutput<typeof archiveModerationLogSchema>;\n\n/** How the online list ranks entries: explicit modes only, no vague composites. */\nexport type ArchiveSort = 'liked' | 'recent';\nexport const archiveSortSchema: Schema<ArchiveSort> = s.enum(['liked', 'recent'] as const);\n\n/** One page of archive entries plus forward/backward cursors around it. */\nexport interface ArchivePage<T> {\n /** Cursor for the next page; null after the last. */\n cursor: string | null;\n entries: readonly T[];\n /** Cursor for the previous page; null when already at the top. */\n previous: string | null;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"contract.cjs","names":[],"sources":["../src/contract.ts"],"sourcesContent":["import { type InferOutput, type Schema, s } from '@vielzeug/spell';\n\n/** Shared naming: one place so the client, Worker and issuer cannot drift. */\nexport const SESSION_COOKIE_NAME = 'archive_session';\nexport const SESSION_COOKIE_PATH = '/api/v1';\n/**\n * The maximum age a session JWT may carry, and the cookie Max-Age an issuer sets:\n * a long-lived signed-in state. The Worker rejects older tokens at this bound.\n */\nexport const SESSION_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;\nexport const OPERATOR_SESSION_MAX_AGE_SECONDS = 15 * 60;\n\n/**\n * The full Set-Cookie value for the session cookie: name, token and the security\n * attributes every issuer and the Worker must agree on — HttpOnly, SameSite=Strict,\n * the contract Path, Max-Age, and Secure in production. Issuers mint with the\n * session's max age; the Worker and issuers clear with `maxAgeSeconds: 0`. Never\n * hand-compose the attributes: the cookie's CSRF and theft posture is contract,\n * not per-issuer choice.\n */\nexport const sessionCookieValue = (token: string, options: { maxAgeSeconds: number; secure: boolean }): string =>\n `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Path=${SESSION_COOKIE_PATH}; Max-Age=${options.maxAgeSeconds}${options.secure ? '; Secure' : ''}`;\n\nexport const PUBLISHER_HEADER = 'x-archive-publisher';\n/** Pins account erasure to the issuer account shown when confirmation began. */\nexport const ACCOUNT_HEADER = 'X-Archive-Account';\nexport const REQUEST_MARKER_HEADER = 'x-archive-request';\n/** The wire-contract version advertised by health and stamped on every entry envelope. */\nexport const ARCHIVE_CONTRACT_VERSION = 1;\n/**\n * The storage schema version: the `archive_schema_meta` stamp the package's migration\n * writes and the Worker reads back through health. A mismatch answers `degraded`\n * instead of `ok` — a stale database is the most likely deployment fault.\n */\nexport const ARCHIVE_SCHEMA_VERSION = 1;\n/** Canonical issuer database initialization stamp. */\nexport const AUTH_SCHEMA_VERSION = 1;\n/** The `roles` claim value a session JWT carries for operator authority (moderation). */\nexport const ARCHIVE_OPERATOR_ROLE = 'archive-operator';\n/** The routable collection-name rule: lowercase `[a-z0-9-]`, at most 32 chars. */\nexport const ARCHIVE_COLLECTION_NAME_PATTERN = /^[a-z0-9-]{1,32}$/;\n\n// Exported schemas carry explicit public `Schema<Output>` annotations: the concrete\n// spell schema classes are not part of spell's public export surface, so inferred\n// types would not be portable across package declaration files.\nexport const uuidSchema: Schema<string> = s.string().uuid();\nexport const isoTimestampSchema: Schema<string> = s.string().isoDateTime();\nexport const hex64Schema: Schema<string> = s.string().regex(/^[0-9a-f]{64}$/);\n/** Well-formed user text: no lone UTF-16 surrogates, no C0/C1/DEL control characters. */\nexport const ILL_FORMED_TEXT = /[\\p{Cs}\\p{Cc}]/u;\nexport const isWellFormedText = (value: string): boolean => !ILL_FORMED_TEXT.test(value);\nconst textString = () => s.string().check(isWellFormedText);\n/** The one alias rule, shared by identity mutations and every emitted DTO. */\nexport const archiveAliasSchema: Schema<string> = textString().trim().max(24);\n\nexport const archiveAuthorSchema: Schema<{ id: string; name: string }> = s.object({\n id: uuidSchema,\n name: archiveAliasSchema,\n});\nexport const archiveIdentitySchema: Schema<{ id: string; name: string; operator: boolean } | null> = s\n .object({ id: uuidSchema, name: archiveAliasSchema, operator: s.boolean() })\n .nullable();\nexport type ArchiveIdentity = InferOutput<typeof archiveIdentitySchema>;\n/** The issuer account metadata export; its id is the session subject, not a publisher id. */\nexport const archiveAccountSchema: Schema<{\n createdAt: string;\n email: string;\n id: string;\n lastSignInAt: string | null;\n}> = s.object({\n createdAt: isoTimestampSchema,\n email: s.string().email(),\n id: uuidSchema,\n lastSignInAt: isoTimestampSchema.nullable(),\n});\nexport type ArchiveAccount = InferOutput<typeof archiveAccountSchema>;\nexport const archiveLikedIdsSchema: Schema<{ ids: string[] }> = s.object({ ids: s.array(uuidSchema) });\nexport const archiveLikesPageSchema: Schema<{ cursor: string | null; ids: string[] }> = s.object({\n cursor: s.string().nullable(),\n ids: s.array(uuidSchema),\n});\nexport type ArchiveLikesPage = InferOutput<typeof archiveLikesPageSchema>;\nexport const archiveLikeStateSchema: Schema<{ id: string; likeCount: number; liked: boolean }> = s.object({\n id: uuidSchema,\n likeCount: s.number().int().min(0).max(Number.MAX_SAFE_INTEGER),\n liked: s.boolean(),\n});\n\n/** Application bounds shared by handlers, the health route, and clients. */\nexport const ARCHIVE_LIMITS = Object.freeze({\n aliasChars: 24,\n cursorChars: 1024,\n idempotencyRetentionMs: 48 * 60 * 60 * 1000,\n likeStateIds: 50,\n maxPageSize: 50,\n maxPublications: 50,\n pageSize: 10,\n payloadBytes: 8192,\n publicCacheSeconds: 30,\n /** One free-text moderation reason: trimmed, non-empty, at most this many chars. */\n reportReasonChars: 256,\n requestBytes: 12 * 1024,\n searchChars: 64,\n searchTerms: 6,\n tagsPerEntry: 16,\n titleChars: 128,\n /** One declared filter or tag value: well-formed, non-empty, at most this many chars. */\n vocabChars: 64,\n});\nexport const archiveLimitsSchema: Schema<{\n aliasChars: number;\n cursorChars: number;\n idempotencyRetentionMs: number;\n likeStateIds: number;\n maxPageSize: number;\n maxPublications: number;\n pageSize: number;\n payloadBytes: number;\n publicCacheSeconds: number;\n reportReasonChars: number;\n requestBytes: number;\n searchChars: number;\n searchTerms: number;\n tagsPerEntry: number;\n titleChars: number;\n vocabChars: number;\n}> = s.object({\n aliasChars: s.number().int().min(1),\n cursorChars: s.number().int().min(1),\n idempotencyRetentionMs: s.number().int().min(1),\n likeStateIds: s.number().int().min(1),\n maxPageSize: s.number().int().min(1),\n maxPublications: s.number().int().min(1),\n pageSize: s.number().int().min(1),\n payloadBytes: s.number().int().min(1),\n publicCacheSeconds: s.number().int().min(0),\n reportReasonChars: s.number().int().min(1),\n requestBytes: s.number().int().min(1),\n searchChars: s.number().int().min(1),\n searchTerms: s.number().int().min(1),\n tagsPerEntry: s.number().int().min(1),\n titleChars: s.number().int().min(1),\n vocabChars: s.number().int().min(1),\n});\nexport const archiveHealthSchema: Schema<{\n /** The storage schema stamp (`archive_schema_meta`) the database actually carries. */\n dbSchemaVersion: number | null;\n features: { likes: boolean; publishing: boolean };\n limits: InferOutput<typeof archiveLimitsSchema>;\n schemaVersion: 1;\n /** 'degraded' when the database's storage schema does not match the Worker's. */\n status: 'ok' | 'degraded';\n reason: 'database-unavailable' | 'schema-mismatch' | null;\n}> = s.object({\n dbSchemaVersion: s.number().int().min(0).nullable(),\n features: s.object({ likes: s.boolean(), publishing: s.boolean() }),\n limits: archiveLimitsSchema,\n reason: s.enum(['database-unavailable', 'schema-mismatch'] as const).nullable(),\n schemaVersion: s.literal(ARCHIVE_CONTRACT_VERSION),\n status: s.enum(['ok', 'degraded'] as const),\n});\nexport type ArchiveHealth = InferOutput<typeof archiveHealthSchema>;\n\nexport const archiveAuthHealthSchema: Schema<{\n dbSchemaVersion: number | null;\n schemaVersion: 1;\n status: 'ok' | 'degraded';\n reason: 'configuration-unavailable' | 'database-unavailable' | 'schema-mismatch' | null;\n}> = s.object({\n dbSchemaVersion: s.number().int().min(0).nullable(),\n reason: s.enum(['configuration-unavailable', 'database-unavailable', 'schema-mismatch'] as const).nullable(),\n schemaVersion: s.literal(AUTH_SCHEMA_VERSION),\n status: s.enum(['ok', 'degraded'] as const),\n});\nexport type AuthHealth = InferOutput<typeof archiveAuthHealthSchema>;\n\n/** The Worker's full error-code vocabulary — the contract both sides test against. */\nexport const ARCHIVE_ERROR_CODES = [\n 'account-changed',\n 'auth-unavailable',\n 'bad-request',\n 'conflict',\n 'forbidden',\n 'gone',\n 'internal',\n 'invalid-payload',\n 'likes-disabled',\n 'method-not-allowed',\n 'not-found',\n 'publisher-changed',\n 'publishing-disabled',\n 'quota',\n 'rate-limited',\n 'record-unavailable',\n 'timeout',\n 'too-large',\n 'unauthenticated',\n 'unavailable',\n 'unsupported-media',\n] as const;\nexport type ArchiveErrorCode = (typeof ARCHIVE_ERROR_CODES)[number];\n\n/**\n * The error envelope every route answers failures with: `{ error: { code, message,\n * requestId } }`. Clients validate against this one schema — an envelope carrying an\n * unknown code or malformed fields is not an archive error at all.\n */\nexport const archiveErrorResponseSchema: Schema<{\n error: { code: ArchiveErrorCode; message: string; requestId: string };\n}> = s\n .object({\n error: s\n .object({\n code: s.enum(ARCHIVE_ERROR_CODES),\n message: s.string().min(1).max(256),\n requestId: uuidSchema,\n })\n .strict(),\n })\n .strict();\nexport type ArchiveErrorResponse = InferOutput<typeof archiveErrorResponseSchema>;\n\n/** The sign-out acknowledgement: the only non-collection response body, shared by both sides. */\nexport const archiveOkSchema: Schema<{ ok: true }> = s.object({ ok: s.literal(true) }).strict();\n\n/** One collection's public self-description: its name and declared query vocabularies. */\nexport const archiveCollectionMetaSchema: Schema<{\n filters: string[];\n name: string;\n tags: string[];\n}> = s.object({\n filters: s.array(s.string().min(1).max(ARCHIVE_LIMITS.vocabChars)),\n name: s.string().regex(ARCHIVE_COLLECTION_NAME_PATTERN),\n tags: s.array(s.string().min(1).max(ARCHIVE_LIMITS.vocabChars)),\n});\nexport type ArchiveCollectionMeta = InferOutput<typeof archiveCollectionMetaSchema>;\n\n/** One reader report an operator reviews: what was reported, why, and by whom. */\nexport const archiveReportsSchema: Schema<{\n cursor: string | null;\n reports: {\n id: string;\n createdAt: string;\n entryId: string;\n entryTitle: string;\n reason: string;\n reporter: { id: string; name: string };\n resolvedAt: string | null;\n }[];\n}> = s.object({\n cursor: s.string().nullable(),\n reports: s.array(\n s.object({\n createdAt: isoTimestampSchema,\n entryId: uuidSchema,\n entryTitle: s.string().min(1).max(ARCHIVE_LIMITS.titleChars),\n id: uuidSchema,\n reason: s.string().min(1).max(ARCHIVE_LIMITS.reportReasonChars),\n reporter: archiveAuthorSchema,\n resolvedAt: isoTimestampSchema.nullable(),\n }),\n ),\n});\nexport type ArchiveReports = InferOutput<typeof archiveReportsSchema>;\nexport const archiveOwnReportsSchema: Schema<{\n cursor: string | null;\n reports: {\n collection: string;\n createdAt: string;\n entryId: string;\n id: string;\n reason: string;\n resolvedAt: string | null;\n }[];\n}> = s.object({\n cursor: s.string().nullable(),\n reports: s.array(\n s.object({\n collection: s.string().regex(ARCHIVE_COLLECTION_NAME_PATTERN),\n createdAt: isoTimestampSchema,\n entryId: uuidSchema,\n id: uuidSchema,\n reason: s.string().min(1).max(ARCHIVE_LIMITS.reportReasonChars),\n resolvedAt: isoTimestampSchema.nullable(),\n }),\n ),\n});\nexport type ArchiveOwnReports = InferOutput<typeof archiveOwnReportsSchema>;\n\n/** The moderation actions an operator can take; the audit log's whole vocabulary. */\nexport const ARCHIVE_MODERATION_ACTIONS = ['hide', 'unhide', 'remove', 'erase-publisher'] as const;\nexport type ArchiveModerationAction = (typeof ARCHIVE_MODERATION_ACTIONS)[number];\n\n/**\n * The operator accountability log: one row per effective moderation action, newest\n * first. Operator and target travel as opaque ids only — no names (they change), no\n * content — so the log stays honest after renames and survives the erasures it records.\n */\nexport const archiveModerationLogSchema: Schema<{\n cursor: string | null;\n actions: {\n id: string;\n action: ArchiveModerationAction;\n collection: string | null;\n createdAt: string;\n operatorId: string;\n targetId: string;\n }[];\n}> = s.object({\n actions: s.array(\n s.object({\n action: s.enum(ARCHIVE_MODERATION_ACTIONS),\n collection: s.string().regex(ARCHIVE_COLLECTION_NAME_PATTERN).nullable(),\n createdAt: isoTimestampSchema,\n id: uuidSchema,\n operatorId: uuidSchema,\n targetId: uuidSchema,\n }),\n ),\n cursor: s.string().nullable(),\n});\nexport type ArchiveModerationLog = InferOutput<typeof archiveModerationLogSchema>;\n\n/** How the online list ranks entries: explicit modes only, no vague composites. */\nexport type ArchiveSort = 'liked' | 'recent';\nexport const archiveSortSchema: Schema<ArchiveSort> = s.enum(['liked', 'recent'] as const);\n\n/**\n * One page of archive entries. Every list in the package pages forward-only: the\n * cursor walks toward the oldest entry and is null after the last page.\n */\nexport interface ArchivePage<T> {\n /** Cursor for the next page; null after the last. */\n cursor: string | null;\n entries: readonly T[];\n}\n"],"mappings":"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"}
|
package/dist/contract.d.ts
CHANGED
|
@@ -216,12 +216,13 @@ export type ArchiveModerationLog = InferOutput<typeof archiveModerationLogSchema
|
|
|
216
216
|
/** How the online list ranks entries: explicit modes only, no vague composites. */
|
|
217
217
|
export type ArchiveSort = 'liked' | 'recent';
|
|
218
218
|
export declare const archiveSortSchema: Schema<ArchiveSort>;
|
|
219
|
-
/**
|
|
219
|
+
/**
|
|
220
|
+
* One page of archive entries. Every list in the package pages forward-only: the
|
|
221
|
+
* cursor walks toward the oldest entry and is null after the last page.
|
|
222
|
+
*/
|
|
220
223
|
export interface ArchivePage<T> {
|
|
221
224
|
/** Cursor for the next page; null after the last. */
|
|
222
225
|
cursor: string | null;
|
|
223
226
|
entries: readonly T[];
|
|
224
|
-
/** Cursor for the previous page; null when already at the top. */
|
|
225
|
-
previous: string | null;
|
|
226
227
|
}
|
|
227
228
|
//# sourceMappingURL=contract.d.ts.map
|
package/dist/contract.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,MAAM,EAAK,MAAM,iBAAiB,CAAC;AAEnE,8EAA8E;AAC9E,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AACrD,eAAO,MAAM,mBAAmB,YAAY,CAAC;AAC7C;;;GAGG;AACH,eAAO,MAAM,uBAAuB,QAAoB,CAAC;AACzD,eAAO,MAAM,gCAAgC,QAAU,CAAC;AAExD;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,MAAM,EAAE,SAAS;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,KAAG,MACwD,CAAC;AAEjK,eAAO,MAAM,gBAAgB,wBAAwB,CAAC;AACtD,gFAAgF;AAChF,eAAO,MAAM,cAAc,sBAAsB,CAAC;AAClD,eAAO,MAAM,qBAAqB,sBAAsB,CAAC;AACzD,0FAA0F;AAC1F,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAC1C;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,IAAI,CAAC;AACxC,sDAAsD;AACtD,eAAO,MAAM,mBAAmB,IAAI,CAAC;AACrC,yFAAyF;AACzF,eAAO,MAAM,qBAAqB,qBAAqB,CAAC;AACxD,kFAAkF;AAClF,eAAO,MAAM,+BAA+B,QAAsB,CAAC;AAKnE,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,MAAM,CAAqB,CAAC;AAC5D,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,MAAM,CAA4B,CAAC;AAC3E,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,CAAsC,CAAC;AAC9E,yFAAyF;AACzF,eAAO,MAAM,eAAe,QAAoB,CAAC;AACjD,eAAO,MAAM,gBAAgB,GAAI,OAAO,MAAM,KAAG,OAAuC,CAAC;AAEzF,8EAA8E;AAC9E,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,MAAM,CAA+B,CAAC;AAE9E,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAGnE,CAAC;AACH,eAAO,MAAM,qBAAqB,EAAE,MAAM,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAEpF,CAAC;AACd,MAAM,MAAM,eAAe,GAAG,WAAW,CAAC,OAAO,qBAAqB,CAAC,CAAC;AACxE,6FAA6F;AAC7F,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC;IACxC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B,CAKC,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,WAAW,CAAC,OAAO,oBAAoB,CAAC,CAAC;AACtE,eAAO,MAAM,qBAAqB,EAAE,MAAM,CAAC;IAAE,GAAG,EAAE,MAAM,EAAE,CAAA;CAAE,CAA0C,CAAC;AACvG,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC;IAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,GAAG,EAAE,MAAM,EAAE,CAAA;CAAE,CAGlF,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,WAAW,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAC1E,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAI3F,CAAC;AAEH,4EAA4E;AAC5E,eAAO,MAAM,cAAc;;;;;;;;;;IAUzB,oFAAoF;;;;;;;IAOpF,yFAAyF;;EAEzF,CAAC;AACH,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,sBAAsB,EAAE,MAAM,CAAC;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB,CAiBC,CAAC;AACH,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC;IACvC,sFAAsF;IACtF,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,EAAE;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,UAAU,EAAE,OAAO,CAAA;KAAE,CAAC;IAClD,MAAM,EAAE,WAAW,CAAC,OAAO,mBAAmB,CAAC,CAAC;IAChD,aAAa,EAAE,CAAC,CAAC;IACjB,iFAAiF;IACjF,MAAM,EAAE,IAAI,GAAG,UAAU,CAAC;IAC1B,MAAM,EAAE,sBAAsB,GAAG,iBAAiB,GAAG,IAAI,CAAC;CAC3D,CAOC,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,WAAW,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEpE,eAAO,MAAM,uBAAuB,EAAE,MAAM,CAAC;IAC3C,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,aAAa,EAAE,CAAC,CAAC;IACjB,MAAM,EAAE,IAAI,GAAG,UAAU,CAAC;IAC1B,MAAM,EAAE,2BAA2B,GAAG,sBAAsB,GAAG,iBAAiB,GAAG,IAAI,CAAC;CACzF,CAKC,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,WAAW,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAErE,sFAAsF;AACtF,eAAO,MAAM,mBAAmB,gWAsBtB,CAAC;AACX,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEpE;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,EAAE,MAAM,CAAC;IAC9C,KAAK,EAAE;QAAE,IAAI,EAAE,gBAAgB,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;CACvE,CAUU,CAAC;AACZ,MAAM,MAAM,oBAAoB,GAAG,WAAW,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAElF,iGAAiG;AACjG,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,CAA8C,CAAC;AAEhG,0FAA0F;AAC1F,eAAO,MAAM,2BAA2B,EAAE,MAAM,CAAC;IAC/C,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB,CAIC,CAAC;AACH,MAAM,MAAM,qBAAqB,GAAG,WAAW,CAAC,OAAO,2BAA2B,CAAC,CAAC;AAEpF,kFAAkF;AAClF,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC;IACxC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,EAAE;QACP,EAAE,EAAE,MAAM,CAAC;QACX,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC;QACvC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;KAC3B,EAAE,CAAC;CACL,CAaC,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,WAAW,CAAC,OAAO,oBAAoB,CAAC,CAAC;AACtE,eAAO,MAAM,uBAAuB,EAAE,MAAM,CAAC;IAC3C,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,EAAE;QACP,UAAU,EAAE,MAAM,CAAC;QACnB,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,MAAM,CAAC;QAChB,EAAE,EAAE,MAAM,CAAC;QACX,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;KAC3B,EAAE,CAAC;CACL,CAYC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,WAAW,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAE5E,qFAAqF;AACrF,eAAO,MAAM,0BAA0B,0DAA2D,CAAC;AACnG,MAAM,MAAM,uBAAuB,GAAG,CAAC,OAAO,0BAA0B,CAAC,CAAC,MAAM,CAAC,CAAC;AAElF;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,EAAE,MAAM,CAAC;IAC9C,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,EAAE;QACP,EAAE,EAAE,MAAM,CAAC;QACX,MAAM,EAAE,uBAAuB,CAAC;QAChC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;QAC1B,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;KAClB,EAAE,CAAC;CACL,CAYC,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,WAAW,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAElF,mFAAmF;AACnF,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,QAAQ,CAAC;AAC7C,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,WAAW,CAAwC,CAAC;AAE3F;;;GAGG;AACH,MAAM,WAAW,WAAW,CAAC,CAAC;IAC5B,qDAAqD;IACrD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,EAAE,SAAS,CAAC,EAAE,CAAC;CACvB"}
|
package/dist/contract.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract.js","names":[],"sources":["../src/contract.ts"],"sourcesContent":["import { type InferOutput, type Schema, s } from '@vielzeug/spell';\n\n/** Shared naming: one place so the client, Worker and issuer cannot drift. */\nexport const SESSION_COOKIE_NAME = 'archive_session';\nexport const SESSION_COOKIE_PATH = '/api/v1';\n/**\n * The maximum age a session JWT may carry, and the cookie Max-Age an issuer sets:\n * a long-lived signed-in state. The Worker rejects older tokens at this bound.\n */\nexport const SESSION_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;\nexport const OPERATOR_SESSION_MAX_AGE_SECONDS = 15 * 60;\n\n/**\n * The full Set-Cookie value for the session cookie: name, token and the security\n * attributes every issuer and the Worker must agree on — HttpOnly, SameSite=Strict,\n * the contract Path, Max-Age, and Secure in production. Issuers mint with the\n * session's max age; the Worker and issuers clear with `maxAgeSeconds: 0`. Never\n * hand-compose the attributes: the cookie's CSRF and theft posture is contract,\n * not per-issuer choice.\n */\nexport const sessionCookieValue = (token: string, options: { maxAgeSeconds: number; secure: boolean }): string =>\n `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Path=${SESSION_COOKIE_PATH}; Max-Age=${options.maxAgeSeconds}${options.secure ? '; Secure' : ''}`;\n\nexport const PUBLISHER_HEADER = 'x-archive-publisher';\n/** Pins account erasure to the issuer account shown when confirmation began. */\nexport const ACCOUNT_HEADER = 'X-Archive-Account';\nexport const REQUEST_MARKER_HEADER = 'x-archive-request';\n/** The wire-contract version advertised by health and stamped on every entry envelope. */\nexport const ARCHIVE_CONTRACT_VERSION = 1;\n/**\n * The storage schema version: the `archive_schema_meta` stamp the package's migration\n * writes and the Worker reads back through health. A mismatch answers `degraded`\n * instead of `ok` — a stale database is the most likely deployment fault.\n */\nexport const ARCHIVE_SCHEMA_VERSION = 1;\n/** Canonical issuer database initialization stamp. */\nexport const AUTH_SCHEMA_VERSION = 1;\n/** The `roles` claim value a session JWT carries for operator authority (moderation). */\nexport const ARCHIVE_OPERATOR_ROLE = 'archive-operator';\n/** The routable collection-name rule: lowercase `[a-z0-9-]`, at most 32 chars. */\nexport const ARCHIVE_COLLECTION_NAME_PATTERN = /^[a-z0-9-]{1,32}$/;\n\n// Exported schemas carry explicit public `Schema<Output>` annotations: the concrete\n// spell schema classes are not part of spell's public export surface, so inferred\n// types would not be portable across package declaration files.\nexport const uuidSchema: Schema<string> = s.string().uuid();\nexport const isoTimestampSchema: Schema<string> = s.string().isoDateTime();\nexport const hex64Schema: Schema<string> = s.string().regex(/^[0-9a-f]{64}$/);\n/** Well-formed user text: no lone UTF-16 surrogates, no C0/C1/DEL control characters. */\nexport const ILL_FORMED_TEXT = /[\\p{Cs}\\p{Cc}]/u;\nexport const isWellFormedText = (value: string): boolean => !ILL_FORMED_TEXT.test(value);\nconst textString = () => s.string().check(isWellFormedText);\n/** The one alias rule, shared by identity mutations and every emitted DTO. */\nexport const archiveAliasSchema: Schema<string> = textString().trim().max(24);\n\nexport const archiveAuthorSchema: Schema<{ id: string; name: string }> = s.object({\n id: uuidSchema,\n name: archiveAliasSchema,\n});\nexport const archiveIdentitySchema: Schema<{ id: string; name: string; operator: boolean } | null> = s\n .object({ id: uuidSchema, name: archiveAliasSchema, operator: s.boolean() })\n .nullable();\nexport type ArchiveIdentity = InferOutput<typeof archiveIdentitySchema>;\n/** The issuer account metadata export; its id is the session subject, not a publisher id. */\nexport const archiveAccountSchema: Schema<{\n createdAt: string;\n email: string;\n id: string;\n lastSignInAt: string | null;\n}> = s.object({\n createdAt: isoTimestampSchema,\n email: s.string().email(),\n id: uuidSchema,\n lastSignInAt: isoTimestampSchema.nullable(),\n});\nexport type ArchiveAccount = InferOutput<typeof archiveAccountSchema>;\nexport const archiveLikedIdsSchema: Schema<{ ids: string[] }> = s.object({ ids: s.array(uuidSchema) });\nexport const archiveLikesPageSchema: Schema<{ cursor: string | null; ids: string[] }> = s.object({\n cursor: s.string().nullable(),\n ids: s.array(uuidSchema),\n});\nexport type ArchiveLikesPage = InferOutput<typeof archiveLikesPageSchema>;\nexport const archiveLikeStateSchema: Schema<{ id: string; likeCount: number; liked: boolean }> = s.object({\n id: uuidSchema,\n likeCount: s.number().int().min(0).max(Number.MAX_SAFE_INTEGER),\n liked: s.boolean(),\n});\n\n/** Application bounds shared by handlers, the health route, and clients. */\nexport const ARCHIVE_LIMITS = Object.freeze({\n aliasChars: 24,\n cursorChars: 1024,\n idempotencyRetentionMs: 48 * 60 * 60 * 1000,\n likeStateIds: 50,\n maxPageSize: 50,\n maxPublications: 50,\n pageSize: 10,\n payloadBytes: 8192,\n publicCacheSeconds: 30,\n /** One free-text moderation reason: trimmed, non-empty, at most this many chars. */\n reportReasonChars: 256,\n requestBytes: 12 * 1024,\n searchChars: 64,\n searchTerms: 6,\n tagsPerEntry: 16,\n titleChars: 128,\n /** One declared filter or tag value: well-formed, non-empty, at most this many chars. */\n vocabChars: 64,\n});\nexport const archiveLimitsSchema: Schema<{\n aliasChars: number;\n cursorChars: number;\n idempotencyRetentionMs: number;\n likeStateIds: number;\n maxPageSize: number;\n maxPublications: number;\n pageSize: number;\n payloadBytes: number;\n publicCacheSeconds: number;\n reportReasonChars: number;\n requestBytes: number;\n searchChars: number;\n searchTerms: number;\n tagsPerEntry: number;\n titleChars: number;\n vocabChars: number;\n}> = s.object({\n aliasChars: s.number().int().min(1),\n cursorChars: s.number().int().min(1),\n idempotencyRetentionMs: s.number().int().min(1),\n likeStateIds: s.number().int().min(1),\n maxPageSize: s.number().int().min(1),\n maxPublications: s.number().int().min(1),\n pageSize: s.number().int().min(1),\n payloadBytes: s.number().int().min(1),\n publicCacheSeconds: s.number().int().min(0),\n reportReasonChars: s.number().int().min(1),\n requestBytes: s.number().int().min(1),\n searchChars: s.number().int().min(1),\n searchTerms: s.number().int().min(1),\n tagsPerEntry: s.number().int().min(1),\n titleChars: s.number().int().min(1),\n vocabChars: s.number().int().min(1),\n});\nexport const archiveHealthSchema: Schema<{\n /** The storage schema stamp (`archive_schema_meta`) the database actually carries. */\n dbSchemaVersion: number | null;\n features: { likes: boolean; publishing: boolean };\n limits: InferOutput<typeof archiveLimitsSchema>;\n schemaVersion: 1;\n /** 'degraded' when the database's storage schema does not match the Worker's. */\n status: 'ok' | 'degraded';\n reason: 'database-unavailable' | 'schema-mismatch' | null;\n}> = s.object({\n dbSchemaVersion: s.number().int().min(0).nullable(),\n features: s.object({ likes: s.boolean(), publishing: s.boolean() }),\n limits: archiveLimitsSchema,\n reason: s.enum(['database-unavailable', 'schema-mismatch'] as const).nullable(),\n schemaVersion: s.literal(ARCHIVE_CONTRACT_VERSION),\n status: s.enum(['ok', 'degraded'] as const),\n});\nexport type ArchiveHealth = InferOutput<typeof archiveHealthSchema>;\n\nexport const archiveAuthHealthSchema: Schema<{\n dbSchemaVersion: number | null;\n schemaVersion: 1;\n status: 'ok' | 'degraded';\n reason: 'configuration-unavailable' | 'database-unavailable' | 'schema-mismatch' | null;\n}> = s.object({\n dbSchemaVersion: s.number().int().min(0).nullable(),\n reason: s.enum(['configuration-unavailable', 'database-unavailable', 'schema-mismatch'] as const).nullable(),\n schemaVersion: s.literal(AUTH_SCHEMA_VERSION),\n status: s.enum(['ok', 'degraded'] as const),\n});\nexport type AuthHealth = InferOutput<typeof archiveAuthHealthSchema>;\n\n/** The Worker's full error-code vocabulary — the contract both sides test against. */\nexport const ARCHIVE_ERROR_CODES = [\n 'account-changed',\n 'auth-unavailable',\n 'bad-request',\n 'conflict',\n 'forbidden',\n 'gone',\n 'internal',\n 'invalid-payload',\n 'likes-disabled',\n 'method-not-allowed',\n 'not-found',\n 'publisher-changed',\n 'publishing-disabled',\n 'quota',\n 'rate-limited',\n 'record-unavailable',\n 'timeout',\n 'too-large',\n 'unauthenticated',\n 'unavailable',\n 'unsupported-media',\n] as const;\nexport type ArchiveErrorCode = (typeof ARCHIVE_ERROR_CODES)[number];\n\n/**\n * The error envelope every route answers failures with: `{ error: { code, message,\n * requestId } }`. Clients validate against this one schema — an envelope carrying an\n * unknown code or malformed fields is not an archive error at all.\n */\nexport const archiveErrorResponseSchema: Schema<{\n error: { code: ArchiveErrorCode; message: string; requestId: string };\n}> = s\n .object({\n error: s\n .object({\n code: s.enum(ARCHIVE_ERROR_CODES),\n message: s.string().min(1).max(256),\n requestId: uuidSchema,\n })\n .strict(),\n })\n .strict();\nexport type ArchiveErrorResponse = InferOutput<typeof archiveErrorResponseSchema>;\n\n/** The sign-out acknowledgement: the only non-collection response body, shared by both sides. */\nexport const archiveOkSchema: Schema<{ ok: true }> = s.object({ ok: s.literal(true) }).strict();\n\n/** One collection's public self-description: its name and declared query vocabularies. */\nexport const archiveCollectionMetaSchema: Schema<{\n filters: string[];\n name: string;\n tags: string[];\n}> = s.object({\n filters: s.array(s.string().min(1).max(ARCHIVE_LIMITS.vocabChars)),\n name: s.string().regex(ARCHIVE_COLLECTION_NAME_PATTERN),\n tags: s.array(s.string().min(1).max(ARCHIVE_LIMITS.vocabChars)),\n});\nexport type ArchiveCollectionMeta = InferOutput<typeof archiveCollectionMetaSchema>;\n\n/** One reader report an operator reviews: what was reported, why, and by whom. */\nexport const archiveReportsSchema: Schema<{\n cursor: string | null;\n reports: {\n id: string;\n createdAt: string;\n entryId: string;\n entryTitle: string;\n reason: string;\n reporter: { id: string; name: string };\n resolvedAt: string | null;\n }[];\n}> = s.object({\n cursor: s.string().nullable(),\n reports: s.array(\n s.object({\n createdAt: isoTimestampSchema,\n entryId: uuidSchema,\n entryTitle: s.string().min(1).max(ARCHIVE_LIMITS.titleChars),\n id: uuidSchema,\n reason: s.string().min(1).max(ARCHIVE_LIMITS.reportReasonChars),\n reporter: archiveAuthorSchema,\n resolvedAt: isoTimestampSchema.nullable(),\n }),\n ),\n});\nexport type ArchiveReports = InferOutput<typeof archiveReportsSchema>;\nexport const archiveOwnReportsSchema: Schema<{\n cursor: string | null;\n reports: {\n collection: string;\n createdAt: string;\n entryId: string;\n id: string;\n reason: string;\n resolvedAt: string | null;\n }[];\n}> = s.object({\n cursor: s.string().nullable(),\n reports: s.array(\n s.object({\n collection: s.string().regex(ARCHIVE_COLLECTION_NAME_PATTERN),\n createdAt: isoTimestampSchema,\n entryId: uuidSchema,\n id: uuidSchema,\n reason: s.string().min(1).max(ARCHIVE_LIMITS.reportReasonChars),\n resolvedAt: isoTimestampSchema.nullable(),\n }),\n ),\n});\nexport type ArchiveOwnReports = InferOutput<typeof archiveOwnReportsSchema>;\n\n/** The moderation actions an operator can take; the audit log's whole vocabulary. */\nexport const ARCHIVE_MODERATION_ACTIONS = ['hide', 'unhide', 'remove', 'erase-publisher'] as const;\nexport type ArchiveModerationAction = (typeof ARCHIVE_MODERATION_ACTIONS)[number];\n\n/**\n * The operator accountability log: one row per effective moderation action, newest\n * first. Operator and target travel as opaque ids only — no names (they change), no\n * content — so the log stays honest after renames and survives the erasures it records.\n */\nexport const archiveModerationLogSchema: Schema<{\n cursor: string | null;\n actions: {\n id: string;\n action: ArchiveModerationAction;\n collection: string | null;\n createdAt: string;\n operatorId: string;\n targetId: string;\n }[];\n}> = s.object({\n actions: s.array(\n s.object({\n action: s.enum(ARCHIVE_MODERATION_ACTIONS),\n collection: s.string().regex(ARCHIVE_COLLECTION_NAME_PATTERN).nullable(),\n createdAt: isoTimestampSchema,\n id: uuidSchema,\n operatorId: uuidSchema,\n targetId: uuidSchema,\n }),\n ),\n cursor: s.string().nullable(),\n});\nexport type ArchiveModerationLog = InferOutput<typeof archiveModerationLogSchema>;\n\n/** How the online list ranks entries: explicit modes only, no vague composites. */\nexport type ArchiveSort = 'liked' | 'recent';\nexport const archiveSortSchema: Schema<ArchiveSort> = s.enum(['liked', 'recent'] as const);\n\n/** One page of archive entries plus forward/backward cursors around it. */\nexport interface ArchivePage<T> {\n /** Cursor for the next page; null after the last. */\n cursor: string | null;\n entries: readonly T[];\n /** Cursor for the previous page; null when already at the top. */\n previous: string | null;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"contract.js","names":[],"sources":["../src/contract.ts"],"sourcesContent":["import { type InferOutput, type Schema, s } from '@vielzeug/spell';\n\n/** Shared naming: one place so the client, Worker and issuer cannot drift. */\nexport const SESSION_COOKIE_NAME = 'archive_session';\nexport const SESSION_COOKIE_PATH = '/api/v1';\n/**\n * The maximum age a session JWT may carry, and the cookie Max-Age an issuer sets:\n * a long-lived signed-in state. The Worker rejects older tokens at this bound.\n */\nexport const SESSION_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;\nexport const OPERATOR_SESSION_MAX_AGE_SECONDS = 15 * 60;\n\n/**\n * The full Set-Cookie value for the session cookie: name, token and the security\n * attributes every issuer and the Worker must agree on — HttpOnly, SameSite=Strict,\n * the contract Path, Max-Age, and Secure in production. Issuers mint with the\n * session's max age; the Worker and issuers clear with `maxAgeSeconds: 0`. Never\n * hand-compose the attributes: the cookie's CSRF and theft posture is contract,\n * not per-issuer choice.\n */\nexport const sessionCookieValue = (token: string, options: { maxAgeSeconds: number; secure: boolean }): string =>\n `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Path=${SESSION_COOKIE_PATH}; Max-Age=${options.maxAgeSeconds}${options.secure ? '; Secure' : ''}`;\n\nexport const PUBLISHER_HEADER = 'x-archive-publisher';\n/** Pins account erasure to the issuer account shown when confirmation began. */\nexport const ACCOUNT_HEADER = 'X-Archive-Account';\nexport const REQUEST_MARKER_HEADER = 'x-archive-request';\n/** The wire-contract version advertised by health and stamped on every entry envelope. */\nexport const ARCHIVE_CONTRACT_VERSION = 1;\n/**\n * The storage schema version: the `archive_schema_meta` stamp the package's migration\n * writes and the Worker reads back through health. A mismatch answers `degraded`\n * instead of `ok` — a stale database is the most likely deployment fault.\n */\nexport const ARCHIVE_SCHEMA_VERSION = 1;\n/** Canonical issuer database initialization stamp. */\nexport const AUTH_SCHEMA_VERSION = 1;\n/** The `roles` claim value a session JWT carries for operator authority (moderation). */\nexport const ARCHIVE_OPERATOR_ROLE = 'archive-operator';\n/** The routable collection-name rule: lowercase `[a-z0-9-]`, at most 32 chars. */\nexport const ARCHIVE_COLLECTION_NAME_PATTERN = /^[a-z0-9-]{1,32}$/;\n\n// Exported schemas carry explicit public `Schema<Output>` annotations: the concrete\n// spell schema classes are not part of spell's public export surface, so inferred\n// types would not be portable across package declaration files.\nexport const uuidSchema: Schema<string> = s.string().uuid();\nexport const isoTimestampSchema: Schema<string> = s.string().isoDateTime();\nexport const hex64Schema: Schema<string> = s.string().regex(/^[0-9a-f]{64}$/);\n/** Well-formed user text: no lone UTF-16 surrogates, no C0/C1/DEL control characters. */\nexport const ILL_FORMED_TEXT = /[\\p{Cs}\\p{Cc}]/u;\nexport const isWellFormedText = (value: string): boolean => !ILL_FORMED_TEXT.test(value);\nconst textString = () => s.string().check(isWellFormedText);\n/** The one alias rule, shared by identity mutations and every emitted DTO. */\nexport const archiveAliasSchema: Schema<string> = textString().trim().max(24);\n\nexport const archiveAuthorSchema: Schema<{ id: string; name: string }> = s.object({\n id: uuidSchema,\n name: archiveAliasSchema,\n});\nexport const archiveIdentitySchema: Schema<{ id: string; name: string; operator: boolean } | null> = s\n .object({ id: uuidSchema, name: archiveAliasSchema, operator: s.boolean() })\n .nullable();\nexport type ArchiveIdentity = InferOutput<typeof archiveIdentitySchema>;\n/** The issuer account metadata export; its id is the session subject, not a publisher id. */\nexport const archiveAccountSchema: Schema<{\n createdAt: string;\n email: string;\n id: string;\n lastSignInAt: string | null;\n}> = s.object({\n createdAt: isoTimestampSchema,\n email: s.string().email(),\n id: uuidSchema,\n lastSignInAt: isoTimestampSchema.nullable(),\n});\nexport type ArchiveAccount = InferOutput<typeof archiveAccountSchema>;\nexport const archiveLikedIdsSchema: Schema<{ ids: string[] }> = s.object({ ids: s.array(uuidSchema) });\nexport const archiveLikesPageSchema: Schema<{ cursor: string | null; ids: string[] }> = s.object({\n cursor: s.string().nullable(),\n ids: s.array(uuidSchema),\n});\nexport type ArchiveLikesPage = InferOutput<typeof archiveLikesPageSchema>;\nexport const archiveLikeStateSchema: Schema<{ id: string; likeCount: number; liked: boolean }> = s.object({\n id: uuidSchema,\n likeCount: s.number().int().min(0).max(Number.MAX_SAFE_INTEGER),\n liked: s.boolean(),\n});\n\n/** Application bounds shared by handlers, the health route, and clients. */\nexport const ARCHIVE_LIMITS = Object.freeze({\n aliasChars: 24,\n cursorChars: 1024,\n idempotencyRetentionMs: 48 * 60 * 60 * 1000,\n likeStateIds: 50,\n maxPageSize: 50,\n maxPublications: 50,\n pageSize: 10,\n payloadBytes: 8192,\n publicCacheSeconds: 30,\n /** One free-text moderation reason: trimmed, non-empty, at most this many chars. */\n reportReasonChars: 256,\n requestBytes: 12 * 1024,\n searchChars: 64,\n searchTerms: 6,\n tagsPerEntry: 16,\n titleChars: 128,\n /** One declared filter or tag value: well-formed, non-empty, at most this many chars. */\n vocabChars: 64,\n});\nexport const archiveLimitsSchema: Schema<{\n aliasChars: number;\n cursorChars: number;\n idempotencyRetentionMs: number;\n likeStateIds: number;\n maxPageSize: number;\n maxPublications: number;\n pageSize: number;\n payloadBytes: number;\n publicCacheSeconds: number;\n reportReasonChars: number;\n requestBytes: number;\n searchChars: number;\n searchTerms: number;\n tagsPerEntry: number;\n titleChars: number;\n vocabChars: number;\n}> = s.object({\n aliasChars: s.number().int().min(1),\n cursorChars: s.number().int().min(1),\n idempotencyRetentionMs: s.number().int().min(1),\n likeStateIds: s.number().int().min(1),\n maxPageSize: s.number().int().min(1),\n maxPublications: s.number().int().min(1),\n pageSize: s.number().int().min(1),\n payloadBytes: s.number().int().min(1),\n publicCacheSeconds: s.number().int().min(0),\n reportReasonChars: s.number().int().min(1),\n requestBytes: s.number().int().min(1),\n searchChars: s.number().int().min(1),\n searchTerms: s.number().int().min(1),\n tagsPerEntry: s.number().int().min(1),\n titleChars: s.number().int().min(1),\n vocabChars: s.number().int().min(1),\n});\nexport const archiveHealthSchema: Schema<{\n /** The storage schema stamp (`archive_schema_meta`) the database actually carries. */\n dbSchemaVersion: number | null;\n features: { likes: boolean; publishing: boolean };\n limits: InferOutput<typeof archiveLimitsSchema>;\n schemaVersion: 1;\n /** 'degraded' when the database's storage schema does not match the Worker's. */\n status: 'ok' | 'degraded';\n reason: 'database-unavailable' | 'schema-mismatch' | null;\n}> = s.object({\n dbSchemaVersion: s.number().int().min(0).nullable(),\n features: s.object({ likes: s.boolean(), publishing: s.boolean() }),\n limits: archiveLimitsSchema,\n reason: s.enum(['database-unavailable', 'schema-mismatch'] as const).nullable(),\n schemaVersion: s.literal(ARCHIVE_CONTRACT_VERSION),\n status: s.enum(['ok', 'degraded'] as const),\n});\nexport type ArchiveHealth = InferOutput<typeof archiveHealthSchema>;\n\nexport const archiveAuthHealthSchema: Schema<{\n dbSchemaVersion: number | null;\n schemaVersion: 1;\n status: 'ok' | 'degraded';\n reason: 'configuration-unavailable' | 'database-unavailable' | 'schema-mismatch' | null;\n}> = s.object({\n dbSchemaVersion: s.number().int().min(0).nullable(),\n reason: s.enum(['configuration-unavailable', 'database-unavailable', 'schema-mismatch'] as const).nullable(),\n schemaVersion: s.literal(AUTH_SCHEMA_VERSION),\n status: s.enum(['ok', 'degraded'] as const),\n});\nexport type AuthHealth = InferOutput<typeof archiveAuthHealthSchema>;\n\n/** The Worker's full error-code vocabulary — the contract both sides test against. */\nexport const ARCHIVE_ERROR_CODES = [\n 'account-changed',\n 'auth-unavailable',\n 'bad-request',\n 'conflict',\n 'forbidden',\n 'gone',\n 'internal',\n 'invalid-payload',\n 'likes-disabled',\n 'method-not-allowed',\n 'not-found',\n 'publisher-changed',\n 'publishing-disabled',\n 'quota',\n 'rate-limited',\n 'record-unavailable',\n 'timeout',\n 'too-large',\n 'unauthenticated',\n 'unavailable',\n 'unsupported-media',\n] as const;\nexport type ArchiveErrorCode = (typeof ARCHIVE_ERROR_CODES)[number];\n\n/**\n * The error envelope every route answers failures with: `{ error: { code, message,\n * requestId } }`. Clients validate against this one schema — an envelope carrying an\n * unknown code or malformed fields is not an archive error at all.\n */\nexport const archiveErrorResponseSchema: Schema<{\n error: { code: ArchiveErrorCode; message: string; requestId: string };\n}> = s\n .object({\n error: s\n .object({\n code: s.enum(ARCHIVE_ERROR_CODES),\n message: s.string().min(1).max(256),\n requestId: uuidSchema,\n })\n .strict(),\n })\n .strict();\nexport type ArchiveErrorResponse = InferOutput<typeof archiveErrorResponseSchema>;\n\n/** The sign-out acknowledgement: the only non-collection response body, shared by both sides. */\nexport const archiveOkSchema: Schema<{ ok: true }> = s.object({ ok: s.literal(true) }).strict();\n\n/** One collection's public self-description: its name and declared query vocabularies. */\nexport const archiveCollectionMetaSchema: Schema<{\n filters: string[];\n name: string;\n tags: string[];\n}> = s.object({\n filters: s.array(s.string().min(1).max(ARCHIVE_LIMITS.vocabChars)),\n name: s.string().regex(ARCHIVE_COLLECTION_NAME_PATTERN),\n tags: s.array(s.string().min(1).max(ARCHIVE_LIMITS.vocabChars)),\n});\nexport type ArchiveCollectionMeta = InferOutput<typeof archiveCollectionMetaSchema>;\n\n/** One reader report an operator reviews: what was reported, why, and by whom. */\nexport const archiveReportsSchema: Schema<{\n cursor: string | null;\n reports: {\n id: string;\n createdAt: string;\n entryId: string;\n entryTitle: string;\n reason: string;\n reporter: { id: string; name: string };\n resolvedAt: string | null;\n }[];\n}> = s.object({\n cursor: s.string().nullable(),\n reports: s.array(\n s.object({\n createdAt: isoTimestampSchema,\n entryId: uuidSchema,\n entryTitle: s.string().min(1).max(ARCHIVE_LIMITS.titleChars),\n id: uuidSchema,\n reason: s.string().min(1).max(ARCHIVE_LIMITS.reportReasonChars),\n reporter: archiveAuthorSchema,\n resolvedAt: isoTimestampSchema.nullable(),\n }),\n ),\n});\nexport type ArchiveReports = InferOutput<typeof archiveReportsSchema>;\nexport const archiveOwnReportsSchema: Schema<{\n cursor: string | null;\n reports: {\n collection: string;\n createdAt: string;\n entryId: string;\n id: string;\n reason: string;\n resolvedAt: string | null;\n }[];\n}> = s.object({\n cursor: s.string().nullable(),\n reports: s.array(\n s.object({\n collection: s.string().regex(ARCHIVE_COLLECTION_NAME_PATTERN),\n createdAt: isoTimestampSchema,\n entryId: uuidSchema,\n id: uuidSchema,\n reason: s.string().min(1).max(ARCHIVE_LIMITS.reportReasonChars),\n resolvedAt: isoTimestampSchema.nullable(),\n }),\n ),\n});\nexport type ArchiveOwnReports = InferOutput<typeof archiveOwnReportsSchema>;\n\n/** The moderation actions an operator can take; the audit log's whole vocabulary. */\nexport const ARCHIVE_MODERATION_ACTIONS = ['hide', 'unhide', 'remove', 'erase-publisher'] as const;\nexport type ArchiveModerationAction = (typeof ARCHIVE_MODERATION_ACTIONS)[number];\n\n/**\n * The operator accountability log: one row per effective moderation action, newest\n * first. Operator and target travel as opaque ids only — no names (they change), no\n * content — so the log stays honest after renames and survives the erasures it records.\n */\nexport const archiveModerationLogSchema: Schema<{\n cursor: string | null;\n actions: {\n id: string;\n action: ArchiveModerationAction;\n collection: string | null;\n createdAt: string;\n operatorId: string;\n targetId: string;\n }[];\n}> = s.object({\n actions: s.array(\n s.object({\n action: s.enum(ARCHIVE_MODERATION_ACTIONS),\n collection: s.string().regex(ARCHIVE_COLLECTION_NAME_PATTERN).nullable(),\n createdAt: isoTimestampSchema,\n id: uuidSchema,\n operatorId: uuidSchema,\n targetId: uuidSchema,\n }),\n ),\n cursor: s.string().nullable(),\n});\nexport type ArchiveModerationLog = InferOutput<typeof archiveModerationLogSchema>;\n\n/** How the online list ranks entries: explicit modes only, no vague composites. */\nexport type ArchiveSort = 'liked' | 'recent';\nexport const archiveSortSchema: Schema<ArchiveSort> = s.enum(['liked', 'recent'] as const);\n\n/**\n * One page of archive entries. Every list in the package pages forward-only: the\n * cursor walks toward the oldest entry and is null after the last page.\n */\nexport interface ArchivePage<T> {\n /** Cursor for the next page; null after the last. */\n cursor: string | null;\n entries: readonly T[];\n}\n"],"mappings":"oCAGA,IAAa,EAAsB,kBACtB,EAAsB,UAKtB,EAA0B,OAC1B,EAAmC,IAUnC,oBAAsB,EAAe,IAChD,GAAG,EAAoB,GAAG,EAAM,oCAAoC,EAAoB,YAAY,EAAQ,gBAAgB,EAAQ,OAAS,WAAa,KAE/I,EAAmB,sBAEnB,EAAiB,oBACjB,EAAwB,oBAExB,EAA2B,EAM3B,EAAyB,EAEzB,EAAsB,EAEtB,EAAwB,mBAExB,EAAkC,oBAKlC,EAA6B,EAAE,OAAO,CAAC,CAAC,KAAK,EAC7C,EAAqC,EAAE,OAAO,CAAC,CAAC,YAAY,EAC5D,EAA8B,EAAE,OAAO,CAAC,CAAC,MAAM,gBAAgB,EAE/D,EAAkB,kBAClB,iBAAoB,GAA2B,CAAC,EAAgB,KAAK,CAAK,EACjF,eAAmB,EAAE,OAAO,CAAC,CAAC,MAAM,gBAAgB,EAE7C,EAAqC,WAAW,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAE/D,EAA4D,EAAE,OAAO,CAChF,GAAI,EACJ,KAAM,CACR,CAAC,EACY,EAAwF,EAClG,OAAO,CAAE,GAAI,EAAY,KAAM,EAAoB,SAAU,EAAE,QAAQ,CAAE,CAAC,CAAC,CAC3E,SAAS,EAGC,EAKR,EAAE,OAAO,CACZ,UAAW,EACX,MAAO,EAAE,OAAO,CAAC,CAAC,MAAM,EACxB,GAAI,EACJ,aAAc,EAAmB,SAAS,CAC5C,CAAC,EAEY,EAAmD,EAAE,OAAO,CAAE,IAAK,EAAE,MAAM,CAAU,CAAE,CAAC,EACxF,EAA2E,EAAE,OAAO,CAC/F,OAAQ,EAAE,OAAO,CAAC,CAAC,SAAS,EAC5B,IAAK,EAAE,MAAM,CAAU,CACzB,CAAC,EAEY,EAAoF,EAAE,OAAO,CACxG,GAAI,EACJ,UAAW,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,WAA2B,EAC9D,MAAO,EAAE,QAAQ,CACnB,CAAC,EAGY,EAAiB,OAAO,OAAO,CAC1C,WAAY,GACZ,YAAa,KACb,uBAAwB,OACxB,aAAc,GACd,YAAa,GACb,gBAAiB,GACjB,SAAU,GACV,aAAc,KACd,mBAAoB,GAEpB,kBAAmB,IACnB,aAAc,MACd,YAAa,GACb,YAAa,EACb,aAAc,GACd,WAAY,IAEZ,WAAY,EACd,CAAC,EACY,EAiBR,EAAE,OAAO,CACZ,WAAY,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EAClC,YAAa,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EACnC,uBAAwB,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EAC9C,aAAc,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EACpC,YAAa,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EACnC,gBAAiB,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EACvC,SAAU,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EAChC,aAAc,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EACpC,mBAAoB,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EAC1C,kBAAmB,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EACzC,aAAc,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EACpC,YAAa,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EACnC,YAAa,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EACnC,aAAc,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EACpC,WAAY,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EAClC,WAAY,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CACpC,CAAC,EACY,EASR,EAAE,OAAO,CACZ,gBAAiB,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,EAClD,SAAU,EAAE,OAAO,CAAE,MAAO,EAAE,QAAQ,EAAG,WAAY,EAAE,QAAQ,CAAE,CAAC,EAClE,OAAQ,EACR,OAAQ,EAAE,KAAK,CAAC,uBAAwB,iBAAiB,CAAU,CAAC,CAAC,SAAS,EAC9E,cAAe,EAAE,QAAA,CAAgC,EACjD,OAAQ,EAAE,KAAK,CAAC,KAAM,UAAU,CAAU,CAC5C,CAAC,EAGY,EAKR,EAAE,OAAO,CACZ,gBAAiB,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,EAClD,OAAQ,EAAE,KAAK,CAAC,4BAA6B,uBAAwB,iBAAiB,CAAU,CAAC,CAAC,SAAS,EAC3G,cAAe,EAAE,QAAA,CAA2B,EAC5C,OAAQ,EAAE,KAAK,CAAC,KAAM,UAAU,CAAU,CAC5C,CAAC,EAIY,EAAsB,CACjC,kBACA,mBACA,cACA,WACA,YACA,OACA,WACA,kBACA,iBACA,qBACA,YACA,oBACA,sBACA,QACA,eACA,qBACA,UACA,YACA,kBACA,cACA,mBACF,EAQa,EAER,EACF,OAAO,CACN,MAAO,EACJ,OAAO,CACN,KAAM,EAAE,KAAK,CAAmB,EAChC,QAAS,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,EAClC,UAAW,CACb,CAAC,CAAC,CACD,OAAO,CACZ,CAAC,CAAC,CACD,OAAO,EAIG,EAAwC,EAAE,OAAO,CAAE,GAAI,EAAE,QAAQ,EAAI,CAAE,CAAC,CAAC,CAAC,OAAO,EAGjF,EAIR,EAAE,OAAO,CACZ,QAAS,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAe,UAAU,CAAC,EACjE,KAAM,EAAE,OAAO,CAAC,CAAC,MAAM,CAA+B,EACtD,KAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAe,UAAU,CAAC,CAChE,CAAC,EAIY,EAWR,EAAE,OAAO,CACZ,OAAQ,EAAE,OAAO,CAAC,CAAC,SAAS,EAC5B,QAAS,EAAE,MACT,EAAE,OAAO,CACP,UAAW,EACX,QAAS,EACT,WAAY,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAe,UAAU,EAC3D,GAAI,EACJ,OAAQ,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAe,iBAAiB,EAC9D,SAAU,EACV,WAAY,EAAmB,SAAS,CAC1C,CAAC,CACH,CACF,CAAC,EAEY,EAUR,EAAE,OAAO,CACZ,OAAQ,EAAE,OAAO,CAAC,CAAC,SAAS,EAC5B,QAAS,EAAE,MACT,EAAE,OAAO,CACP,WAAY,EAAE,OAAO,CAAC,CAAC,MAAM,CAA+B,EAC5D,UAAW,EACX,QAAS,EACT,GAAI,EACJ,OAAQ,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAe,iBAAiB,EAC9D,WAAY,EAAmB,SAAS,CAC1C,CAAC,CACH,CACF,CAAC,EAIY,EAA6B,CAAC,OAAQ,SAAU,SAAU,iBAAiB,EAQ3E,EAUR,EAAE,OAAO,CACZ,QAAS,EAAE,MACT,EAAE,OAAO,CACP,OAAQ,EAAE,KAAK,CAA0B,EACzC,WAAY,EAAE,OAAO,CAAC,CAAC,MAAM,CAA+B,CAAC,CAAC,SAAS,EACvE,UAAW,EACX,GAAI,EACJ,WAAY,EACZ,SAAU,CACZ,CAAC,CACH,EACA,OAAQ,EAAE,OAAO,CAAC,CAAC,SAAS,CAC9B,CAAC,EAKY,EAAyC,EAAE,KAAK,CAAC,QAAS,QAAQ,CAAU"}
|
package/dist/errors.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.cjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["export class ArchiveError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\nexport class ArchiveDefinitionError extends ArchiveError {}\n\nexport type ArchiveDerivationStage = '
|
|
1
|
+
{"version":3,"file":"errors.cjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["export class ArchiveError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\nexport class ArchiveDefinitionError extends ArchiveError {}\n\nexport type ArchiveDerivationStage = 'canonicalize' | 'hash' | 'index' | 'schema';\n\n/** A collection callback or core runtime fault, not invalid submitted content. */\nexport class ArchiveDerivationError extends ArchiveError {\n readonly stage: ArchiveDerivationStage;\n\n constructor(stage: ArchiveDerivationStage, message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.stage = stage;\n }\n}\n"],"mappings":"AAAA,IAAa,aAAb,cAAkC,KAAM,CACtC,YAAY,EAAiB,EAAqB,CAChD,MAAM,EAAS,CAAI,EACnB,KAAK,KAAO,WAAW,KACvB,OAAO,eAAe,KAAM,WAAW,SAAS,CAClD,CACF,EAEa,uBAAb,cAA4C,YAAa,CAAC,EAK7C,uBAAb,cAA4C,YAAa,CACvD,MAEA,YAAY,EAA+B,EAAiB,EAAqB,CAC/E,MAAM,EAAS,CAAI,EACnB,KAAK,MAAQ,CACf,CACF"}
|
package/dist/errors.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export declare class ArchiveError extends Error {
|
|
|
3
3
|
}
|
|
4
4
|
export declare class ArchiveDefinitionError extends ArchiveError {
|
|
5
5
|
}
|
|
6
|
-
export type ArchiveDerivationStage = '
|
|
6
|
+
export type ArchiveDerivationStage = 'canonicalize' | 'hash' | 'index' | 'schema';
|
|
7
7
|
/** A collection callback or core runtime fault, not invalid submitted content. */
|
|
8
8
|
export declare class ArchiveDerivationError extends ArchiveError {
|
|
9
9
|
readonly stage: ArchiveDerivationStage;
|
package/dist/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,qBAAa,YAAa,SAAQ,KAAK;gBACzB,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,YAAY;CAKjD;AAED,qBAAa,sBAAuB,SAAQ,YAAY;CAAG;AAE3D,MAAM,MAAM,sBAAsB,GAAG,
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,qBAAa,YAAa,SAAQ,KAAK;gBACzB,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,YAAY;CAKjD;AAED,qBAAa,sBAAuB,SAAQ,YAAY;CAAG;AAE3D,MAAM,MAAM,sBAAsB,GAAG,cAAc,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;AAElF,kFAAkF;AAClF,qBAAa,sBAAuB,SAAQ,YAAY;IACtD,QAAQ,CAAC,KAAK,EAAE,sBAAsB,CAAC;gBAE3B,KAAK,EAAE,sBAAsB,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,YAAY;CAIhF"}
|
package/dist/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","names":[],"sources":["../src/errors.ts"],"sourcesContent":["export class ArchiveError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\nexport class ArchiveDefinitionError extends ArchiveError {}\n\nexport type ArchiveDerivationStage = '
|
|
1
|
+
{"version":3,"file":"errors.js","names":[],"sources":["../src/errors.ts"],"sourcesContent":["export class ArchiveError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\nexport class ArchiveDefinitionError extends ArchiveError {}\n\nexport type ArchiveDerivationStage = 'canonicalize' | 'hash' | 'index' | 'schema';\n\n/** A collection callback or core runtime fault, not invalid submitted content. */\nexport class ArchiveDerivationError extends ArchiveError {\n readonly stage: ArchiveDerivationStage;\n\n constructor(stage: ArchiveDerivationStage, message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.stage = stage;\n }\n}\n"],"mappings":"AAAA,IAAa,aAAb,cAAkC,KAAM,CACtC,YAAY,EAAiB,EAAqB,CAChD,MAAM,EAAS,CAAI,EACnB,KAAK,KAAO,WAAW,KACvB,OAAO,eAAe,KAAM,WAAW,SAAS,CAClD,CACF,EAEa,uBAAb,cAA4C,YAAa,CAAC,EAK7C,uBAAb,cAA4C,YAAa,CACvD,MAEA,YAAY,EAA+B,EAAiB,EAAqB,CAC/E,MAAM,EAAS,CAAI,EACnB,KAAK,MAAQ,CACf,CACF"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../contract.cjs"),t=require("./http.cjs");let n=require("@vielzeug/spell");var encodeCursorValue=e=>{let t=new TextEncoder().encode(JSON.stringify(e));return btoa(Array.from(t,e=>String.fromCharCode(e)).join(``)).replaceAll(`+`,`-`).replaceAll(`/`,`_`).replaceAll(`=`,``)};function decodeCursorValue(n,r){if(n.length>e.ARCHIVE_LIMITS.cursorChars||!/^[A-Za-z0-9_-]+$/.test(n))throw t.badRequest();let i;try{let e=n.replaceAll(`-`,`+`).replaceAll(`_`,`/`),t=Uint8Array.from(atob(e+`=`.repeat((4-e.length%4)%4)),e=>e.charCodeAt(0));i=JSON.parse(new TextDecoder(`utf-8`,{fatal:!0}).decode(t))}catch{throw t.badRequest()}let a=r.safeParse(i);if(!a.success||encodeCursorValue(i)!==n)throw t.badRequest();return a.data}var r=n.s.object({context:n.s.string(),createdAt:e.isoTimestampSchema,id:e.uuidSchema});function
|
|
1
|
+
const e=require("../contract.cjs"),t=require("./http.cjs");let n=require("@vielzeug/spell");var encodeCursorValue=e=>{let t=new TextEncoder().encode(JSON.stringify(e));return btoa(Array.from(t,e=>String.fromCharCode(e)).join(``)).replaceAll(`+`,`-`).replaceAll(`/`,`_`).replaceAll(`=`,``)};function decodeCursorValue(n,r){if(n.length>e.ARCHIVE_LIMITS.cursorChars||!/^[A-Za-z0-9_-]+$/.test(n))throw t.badRequest();let i;try{let e=n.replaceAll(`-`,`+`).replaceAll(`_`,`/`),t=Uint8Array.from(atob(e+`=`.repeat((4-e.length%4)%4)),e=>e.charCodeAt(0));i=JSON.parse(new TextDecoder(`utf-8`,{fatal:!0}).decode(t))}catch{throw t.badRequest()}let a=r.safeParse(i);if(!a.success||encodeCursorValue(i)!==n)throw t.badRequest();return a.data}var r=n.s.object({context:n.s.string(),createdAt:e.isoTimestampSchema,id:e.uuidSchema,likeCount:n.s.number().int().min(0).max(2**53-1).optional()});function forwardPageParams(n,i){let a=n.get(`limit`),o=a===null?e.ARCHIVE_LIMITS.pageSize:Number(a);if(!Number.isInteger(o)||o<1||o>e.ARCHIVE_LIMITS.maxPageSize)throw t.badRequest();let s=n.get(`cursor`),c=s===null?null:decodeCursorValue(s,r);if(c!==null&&c.context!==i)throw t.badRequest();return{cursor:c,limit:o}}function forwardPage(e,n,r=[]){let i=t.queryParams(new URL(e.url),[`cursor`,`limit`,...r]);return{...forwardPageParams(i,n),params:i}}var nextCursor=(e,t)=>encodeCursorValue({...e,context:t});exports.decodeCursorValue=decodeCursorValue,exports.encodeCursorValue=encodeCursorValue,exports.forwardPage=forwardPage,exports.forwardPageParams=forwardPageParams,exports.nextCursor=nextCursor;
|
|
2
2
|
//# sourceMappingURL=_pagination.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"_pagination.cjs","names":[],"sources":["../../src/worker/_pagination.ts"],"sourcesContent":["import { type Schema, s } from '@vielzeug/spell';\nimport { ARCHIVE_LIMITS, isoTimestampSchema, uuidSchema } from '../contract';\nimport { badRequest, queryParams } from './http';\n\nexport const encodeCursorValue = (value: unknown): string => {\n const bytes = new TextEncoder().encode(JSON.stringify(value));\n return btoa(Array.from(bytes, (byte) => String.fromCharCode(byte)).join(''))\n .replaceAll('+', '-')\n .replaceAll('/', '_')\n .replaceAll('=', '');\n};\n\nexport function decodeCursorValue<T>(raw: string, schema: Schema<T>): T {\n if (raw.length > ARCHIVE_LIMITS.cursorChars || !/^[A-Za-z0-9_-]+$/.test(raw)) throw badRequest();\n let value: unknown;\n try {\n const base64 = raw.replaceAll('-', '+').replaceAll('_', '/');\n const bytes = Uint8Array.from(atob(base64 + '='.repeat((4 - (base64.length % 4)) % 4)), (char) =>\n char.charCodeAt(0),\n );\n value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));\n } catch {\n throw badRequest();\n }\n const parsed = schema.safeParse(value);\n if (!parsed.success || encodeCursorValue(value) !== raw) throw badRequest();\n return parsed.data;\n}\n\nconst forwardCursorSchema = s.object({\n context: s.string(),\n createdAt: isoTimestampSchema,\n id: uuidSchema,\n
|
|
1
|
+
{"version":3,"file":"_pagination.cjs","names":[],"sources":["../../src/worker/_pagination.ts"],"sourcesContent":["import { type Schema, s } from '@vielzeug/spell';\nimport { ARCHIVE_LIMITS, isoTimestampSchema, uuidSchema } from '../contract';\nimport { badRequest, queryParams } from './http';\n\n/**\n * The package's one pagination idiom: forward-only keyset pages. A cursor is an\n * opaque envelope of `{ context, createdAt, id, likeCount }` — `createdAt`/`id`\n * are the sort boundary (a timestamp and the row id), `likeCount` is the optional\n * extra sort field (the boundary count of the liked-sorted entry list), and\n * `context` names the query the cursor was minted for, so a cursor replayed under\n * a different query is a 400, never a silently wrong page.\n */\nexport const encodeCursorValue = (value: unknown): string => {\n const bytes = new TextEncoder().encode(JSON.stringify(value));\n return btoa(Array.from(bytes, (byte) => String.fromCharCode(byte)).join(''))\n .replaceAll('+', '-')\n .replaceAll('/', '_')\n .replaceAll('=', '');\n};\n\nexport function decodeCursorValue<T>(raw: string, schema: Schema<T>): T {\n if (raw.length > ARCHIVE_LIMITS.cursorChars || !/^[A-Za-z0-9_-]+$/.test(raw)) throw badRequest();\n let value: unknown;\n try {\n const base64 = raw.replaceAll('-', '+').replaceAll('_', '/');\n const bytes = Uint8Array.from(atob(base64 + '='.repeat((4 - (base64.length % 4)) % 4)), (char) =>\n char.charCodeAt(0),\n );\n value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));\n } catch {\n throw badRequest();\n }\n const parsed = schema.safeParse(value);\n if (!parsed.success || encodeCursorValue(value) !== raw) throw badRequest();\n return parsed.data;\n}\n\n/** One keyset boundary: timestamp, row id, and the liked sort's boundary count. */\nexport interface ForwardCursor {\n context: string;\n createdAt: string;\n id: string;\n likeCount?: number;\n}\n\nconst forwardCursorSchema: Schema<ForwardCursor> = s.object({\n context: s.string(),\n createdAt: isoTimestampSchema,\n id: uuidSchema,\n // Only the liked-sorted entry list carries a boundary count; every other list\n // omits the key entirely.\n likeCount: s.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional(),\n});\n\n/** Parses `limit` and the forward cursor from an already allow-listed parameter set. */\nexport function forwardPageParams(params: URLSearchParams, context: string) {\n const rawLimit = params.get('limit');\n const limit = rawLimit === null ? ARCHIVE_LIMITS.pageSize : Number(rawLimit);\n if (!Number.isInteger(limit) || limit < 1 || limit > ARCHIVE_LIMITS.maxPageSize) throw badRequest();\n const raw = params.get('cursor');\n const cursor = raw === null ? null : decodeCursorValue(raw, forwardCursorSchema);\n if (cursor !== null && cursor.context !== context) throw badRequest();\n return { cursor, limit };\n}\n\n/** Allow-lists the query, then parses `limit` and the forward cursor. */\nexport function forwardPage(request: Request, context: string, extra: readonly string[] = []) {\n const params = queryParams(new URL(request.url), ['cursor', 'limit', ...extra]);\n return { ...forwardPageParams(params, context), params };\n}\n\nexport const nextCursor = (cursor: Omit<ForwardCursor, 'context'>, context: string): string =>\n encodeCursorValue({ ...cursor, context });\n"],"mappings":"4FAYA,IAAa,kBAAqB,GAA2B,CAC3D,IAAM,EAAQ,IAAI,YAAY,CAAC,CAAC,OAAO,KAAK,UAAU,CAAK,CAAC,EAC5D,OAAO,KAAK,MAAM,KAAK,EAAQ,GAAS,OAAO,aAAa,CAAI,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CACzE,WAAW,IAAK,GAAG,CAAC,CACpB,WAAW,IAAK,GAAG,CAAC,CACpB,WAAW,IAAK,EAAE,CACvB,EAEA,SAAgB,kBAAqB,EAAa,EAAsB,CACtE,GAAI,EAAI,OAAS,EAAA,eAAe,aAAe,CAAC,mBAAmB,KAAK,CAAG,EAAG,MAAM,EAAA,WAAW,EAC/F,IAAI,EACJ,GAAI,CACF,IAAM,EAAS,EAAI,WAAW,IAAK,GAAG,CAAC,CAAC,WAAW,IAAK,GAAG,EACrD,EAAQ,WAAW,KAAK,KAAK,EAAS,IAAI,QAAQ,EAAK,EAAO,OAAS,GAAM,CAAC,CAAC,EAAI,GACvF,EAAK,WAAW,CAAC,CACnB,EACA,EAAQ,KAAK,MAAM,IAAI,YAAY,QAAS,CAAE,MAAO,EAAK,CAAC,CAAC,CAAC,OAAO,CAAK,CAAC,CAC5E,MAAQ,CACN,MAAM,EAAA,WAAW,CACnB,CACA,IAAM,EAAS,EAAO,UAAU,CAAK,EACrC,GAAI,CAAC,EAAO,SAAW,kBAAkB,CAAK,IAAM,EAAK,MAAM,EAAA,WAAW,EAC1E,OAAO,EAAO,IAChB,CAUA,IAAM,EAA6C,EAAA,EAAE,OAAO,CAC1D,QAAS,EAAA,EAAE,OAAO,EAClB,UAAW,EAAA,mBACX,GAAI,EAAA,WAGJ,UAAW,EAAA,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,WAA2B,CAAC,CAAC,SAAS,CAC3E,CAAC,EAGD,SAAgB,kBAAkB,EAAyB,EAAiB,CAC1E,IAAM,EAAW,EAAO,IAAI,OAAO,EAC7B,EAAQ,IAAa,KAAO,EAAA,eAAe,SAAW,OAAO,CAAQ,EAC3E,GAAI,CAAC,OAAO,UAAU,CAAK,GAAK,EAAQ,GAAK,EAAQ,EAAA,eAAe,YAAa,MAAM,EAAA,WAAW,EAClG,IAAM,EAAM,EAAO,IAAI,QAAQ,EACzB,EAAS,IAAQ,KAAO,KAAO,kBAAkB,EAAK,CAAmB,EAC/E,GAAI,IAAW,MAAQ,EAAO,UAAY,EAAS,MAAM,EAAA,WAAW,EACpE,MAAO,CAAE,SAAQ,OAAM,CACzB,CAGA,SAAgB,YAAY,EAAkB,EAAiB,EAA2B,CAAC,EAAG,CAC5F,IAAM,EAAS,EAAA,YAAY,IAAI,IAAI,EAAQ,GAAG,EAAG,CAAC,SAAU,QAAS,GAAG,CAAK,CAAC,EAC9E,MAAO,CAAE,GAAG,kBAAkB,EAAQ,CAAO,EAAG,QAAO,CACzD,CAEA,IAAa,YAAc,EAAwC,IACjE,kBAAkB,CAAE,GAAG,EAAQ,SAAQ,CAAC"}
|
|
@@ -1,18 +1,31 @@
|
|
|
1
1
|
import { type Schema } from '@vielzeug/spell';
|
|
2
|
+
/**
|
|
3
|
+
* The package's one pagination idiom: forward-only keyset pages. A cursor is an
|
|
4
|
+
* opaque envelope of `{ context, createdAt, id, likeCount }` — `createdAt`/`id`
|
|
5
|
+
* are the sort boundary (a timestamp and the row id), `likeCount` is the optional
|
|
6
|
+
* extra sort field (the boundary count of the liked-sorted entry list), and
|
|
7
|
+
* `context` names the query the cursor was minted for, so a cursor replayed under
|
|
8
|
+
* a different query is a 400, never a silently wrong page.
|
|
9
|
+
*/
|
|
2
10
|
export declare const encodeCursorValue: (value: unknown) => string;
|
|
3
11
|
export declare function decodeCursorValue<T>(raw: string, schema: Schema<T>): T;
|
|
12
|
+
/** One keyset boundary: timestamp, row id, and the liked sort's boundary count. */
|
|
4
13
|
export interface ForwardCursor {
|
|
14
|
+
context: string;
|
|
5
15
|
createdAt: string;
|
|
6
16
|
id: string;
|
|
17
|
+
likeCount?: number;
|
|
7
18
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
createdAt: string;
|
|
12
|
-
context: string;
|
|
13
|
-
} | null;
|
|
19
|
+
/** Parses `limit` and the forward cursor from an already allow-listed parameter set. */
|
|
20
|
+
export declare function forwardPageParams(params: URLSearchParams, context: string): {
|
|
21
|
+
cursor: ForwardCursor | null;
|
|
14
22
|
limit: number;
|
|
23
|
+
};
|
|
24
|
+
/** Allow-lists the query, then parses `limit` and the forward cursor. */
|
|
25
|
+
export declare function forwardPage(request: Request, context: string, extra?: readonly string[]): {
|
|
15
26
|
params: URLSearchParams;
|
|
27
|
+
cursor: ForwardCursor | null;
|
|
28
|
+
limit: number;
|
|
16
29
|
};
|
|
17
|
-
export declare const nextCursor: (
|
|
30
|
+
export declare const nextCursor: (cursor: Omit<ForwardCursor, "context">, context: string) => string;
|
|
18
31
|
//# sourceMappingURL=_pagination.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"_pagination.d.ts","sourceRoot":"","sources":["../../src/worker/_pagination.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAK,MAAM,iBAAiB,CAAC;AAIjD,eAAO,MAAM,iBAAiB,GAAI,OAAO,OAAO,KAAG,MAMlD,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAetE;
|
|
1
|
+
{"version":3,"file":"_pagination.d.ts","sourceRoot":"","sources":["../../src/worker/_pagination.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAK,MAAM,iBAAiB,CAAC;AAIjD;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,GAAI,OAAO,OAAO,KAAG,MAMlD,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAetE;AAED,mFAAmF;AACnF,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAWD,wFAAwF;AACxF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM;;;EAQzE;AAED,yEAAyE;AACzE,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,GAAE,SAAS,MAAM,EAAO;;;;EAG3F;AAED,eAAO,MAAM,UAAU,GAAI,QAAQ,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,EAAE,SAAS,MAAM,KAAG,MAC1C,CAAC"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{ARCHIVE_LIMITS as e,isoTimestampSchema as t,uuidSchema as n}from"../contract.js";import{badRequest as r,queryParams as i}from"./http.js";import{s as a}from"@vielzeug/spell";var encodeCursorValue=e=>{let t=new TextEncoder().encode(JSON.stringify(e));return btoa(Array.from(t,e=>String.fromCharCode(e)).join(``)).replaceAll(`+`,`-`).replaceAll(`/`,`_`).replaceAll(`=`,``)};function decodeCursorValue(t,n){if(t.length>e.cursorChars||!/^[A-Za-z0-9_-]+$/.test(t))throw r();let i;try{let e=t.replaceAll(`-`,`+`).replaceAll(`_`,`/`),n=Uint8Array.from(atob(e+`=`.repeat((4-e.length%4)%4)),e=>e.charCodeAt(0));i=JSON.parse(new TextDecoder(`utf-8`,{fatal:!0}).decode(n))}catch{throw r()}let a=n.safeParse(i);if(!a.success||encodeCursorValue(i)!==t)throw r();return a.data}var o=a.object({context:a.string(),createdAt:t,id:n});function
|
|
1
|
+
import{ARCHIVE_LIMITS as e,isoTimestampSchema as t,uuidSchema as n}from"../contract.js";import{badRequest as r,queryParams as i}from"./http.js";import{s as a}from"@vielzeug/spell";var encodeCursorValue=e=>{let t=new TextEncoder().encode(JSON.stringify(e));return btoa(Array.from(t,e=>String.fromCharCode(e)).join(``)).replaceAll(`+`,`-`).replaceAll(`/`,`_`).replaceAll(`=`,``)};function decodeCursorValue(t,n){if(t.length>e.cursorChars||!/^[A-Za-z0-9_-]+$/.test(t))throw r();let i;try{let e=t.replaceAll(`-`,`+`).replaceAll(`_`,`/`),n=Uint8Array.from(atob(e+`=`.repeat((4-e.length%4)%4)),e=>e.charCodeAt(0));i=JSON.parse(new TextDecoder(`utf-8`,{fatal:!0}).decode(n))}catch{throw r()}let a=n.safeParse(i);if(!a.success||encodeCursorValue(i)!==t)throw r();return a.data}var o=a.object({context:a.string(),createdAt:t,id:n,likeCount:a.number().int().min(0).max(2**53-1).optional()});function forwardPageParams(t,n){let i=t.get(`limit`),a=i===null?e.pageSize:Number(i);if(!Number.isInteger(a)||a<1||a>e.maxPageSize)throw r();let s=t.get(`cursor`),c=s===null?null:decodeCursorValue(s,o);if(c!==null&&c.context!==n)throw r();return{cursor:c,limit:a}}function forwardPage(e,t,n=[]){let r=i(new URL(e.url),[`cursor`,`limit`,...n]);return{...forwardPageParams(r,t),params:r}}var nextCursor=(e,t)=>encodeCursorValue({...e,context:t});export{decodeCursorValue,encodeCursorValue,forwardPage,forwardPageParams,nextCursor};
|
|
2
2
|
//# sourceMappingURL=_pagination.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"_pagination.js","names":[],"sources":["../../src/worker/_pagination.ts"],"sourcesContent":["import { type Schema, s } from '@vielzeug/spell';\nimport { ARCHIVE_LIMITS, isoTimestampSchema, uuidSchema } from '../contract';\nimport { badRequest, queryParams } from './http';\n\nexport const encodeCursorValue = (value: unknown): string => {\n const bytes = new TextEncoder().encode(JSON.stringify(value));\n return btoa(Array.from(bytes, (byte) => String.fromCharCode(byte)).join(''))\n .replaceAll('+', '-')\n .replaceAll('/', '_')\n .replaceAll('=', '');\n};\n\nexport function decodeCursorValue<T>(raw: string, schema: Schema<T>): T {\n if (raw.length > ARCHIVE_LIMITS.cursorChars || !/^[A-Za-z0-9_-]+$/.test(raw)) throw badRequest();\n let value: unknown;\n try {\n const base64 = raw.replaceAll('-', '+').replaceAll('_', '/');\n const bytes = Uint8Array.from(atob(base64 + '='.repeat((4 - (base64.length % 4)) % 4)), (char) =>\n char.charCodeAt(0),\n );\n value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));\n } catch {\n throw badRequest();\n }\n const parsed = schema.safeParse(value);\n if (!parsed.success || encodeCursorValue(value) !== raw) throw badRequest();\n return parsed.data;\n}\n\nconst forwardCursorSchema = s.object({\n context: s.string(),\n createdAt: isoTimestampSchema,\n id: uuidSchema,\n
|
|
1
|
+
{"version":3,"file":"_pagination.js","names":[],"sources":["../../src/worker/_pagination.ts"],"sourcesContent":["import { type Schema, s } from '@vielzeug/spell';\nimport { ARCHIVE_LIMITS, isoTimestampSchema, uuidSchema } from '../contract';\nimport { badRequest, queryParams } from './http';\n\n/**\n * The package's one pagination idiom: forward-only keyset pages. A cursor is an\n * opaque envelope of `{ context, createdAt, id, likeCount }` — `createdAt`/`id`\n * are the sort boundary (a timestamp and the row id), `likeCount` is the optional\n * extra sort field (the boundary count of the liked-sorted entry list), and\n * `context` names the query the cursor was minted for, so a cursor replayed under\n * a different query is a 400, never a silently wrong page.\n */\nexport const encodeCursorValue = (value: unknown): string => {\n const bytes = new TextEncoder().encode(JSON.stringify(value));\n return btoa(Array.from(bytes, (byte) => String.fromCharCode(byte)).join(''))\n .replaceAll('+', '-')\n .replaceAll('/', '_')\n .replaceAll('=', '');\n};\n\nexport function decodeCursorValue<T>(raw: string, schema: Schema<T>): T {\n if (raw.length > ARCHIVE_LIMITS.cursorChars || !/^[A-Za-z0-9_-]+$/.test(raw)) throw badRequest();\n let value: unknown;\n try {\n const base64 = raw.replaceAll('-', '+').replaceAll('_', '/');\n const bytes = Uint8Array.from(atob(base64 + '='.repeat((4 - (base64.length % 4)) % 4)), (char) =>\n char.charCodeAt(0),\n );\n value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));\n } catch {\n throw badRequest();\n }\n const parsed = schema.safeParse(value);\n if (!parsed.success || encodeCursorValue(value) !== raw) throw badRequest();\n return parsed.data;\n}\n\n/** One keyset boundary: timestamp, row id, and the liked sort's boundary count. */\nexport interface ForwardCursor {\n context: string;\n createdAt: string;\n id: string;\n likeCount?: number;\n}\n\nconst forwardCursorSchema: Schema<ForwardCursor> = s.object({\n context: s.string(),\n createdAt: isoTimestampSchema,\n id: uuidSchema,\n // Only the liked-sorted entry list carries a boundary count; every other list\n // omits the key entirely.\n likeCount: s.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional(),\n});\n\n/** Parses `limit` and the forward cursor from an already allow-listed parameter set. */\nexport function forwardPageParams(params: URLSearchParams, context: string) {\n const rawLimit = params.get('limit');\n const limit = rawLimit === null ? ARCHIVE_LIMITS.pageSize : Number(rawLimit);\n if (!Number.isInteger(limit) || limit < 1 || limit > ARCHIVE_LIMITS.maxPageSize) throw badRequest();\n const raw = params.get('cursor');\n const cursor = raw === null ? null : decodeCursorValue(raw, forwardCursorSchema);\n if (cursor !== null && cursor.context !== context) throw badRequest();\n return { cursor, limit };\n}\n\n/** Allow-lists the query, then parses `limit` and the forward cursor. */\nexport function forwardPage(request: Request, context: string, extra: readonly string[] = []) {\n const params = queryParams(new URL(request.url), ['cursor', 'limit', ...extra]);\n return { ...forwardPageParams(params, context), params };\n}\n\nexport const nextCursor = (cursor: Omit<ForwardCursor, 'context'>, context: string): string =>\n encodeCursorValue({ ...cursor, context });\n"],"mappings":"oLAYA,IAAa,kBAAqB,GAA2B,CAC3D,IAAM,EAAQ,IAAI,YAAY,CAAC,CAAC,OAAO,KAAK,UAAU,CAAK,CAAC,EAC5D,OAAO,KAAK,MAAM,KAAK,EAAQ,GAAS,OAAO,aAAa,CAAI,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CACzE,WAAW,IAAK,GAAG,CAAC,CACpB,WAAW,IAAK,GAAG,CAAC,CACpB,WAAW,IAAK,EAAE,CACvB,EAEA,SAAgB,kBAAqB,EAAa,EAAsB,CACtE,GAAI,EAAI,OAAS,EAAe,aAAe,CAAC,mBAAmB,KAAK,CAAG,EAAG,MAAM,EAAW,EAC/F,IAAI,EACJ,GAAI,CACF,IAAM,EAAS,EAAI,WAAW,IAAK,GAAG,CAAC,CAAC,WAAW,IAAK,GAAG,EACrD,EAAQ,WAAW,KAAK,KAAK,EAAS,IAAI,QAAQ,EAAK,EAAO,OAAS,GAAM,CAAC,CAAC,EAAI,GACvF,EAAK,WAAW,CAAC,CACnB,EACA,EAAQ,KAAK,MAAM,IAAI,YAAY,QAAS,CAAE,MAAO,EAAK,CAAC,CAAC,CAAC,OAAO,CAAK,CAAC,CAC5E,MAAQ,CACN,MAAM,EAAW,CACnB,CACA,IAAM,EAAS,EAAO,UAAU,CAAK,EACrC,GAAI,CAAC,EAAO,SAAW,kBAAkB,CAAK,IAAM,EAAK,MAAM,EAAW,EAC1E,OAAO,EAAO,IAChB,CAUA,IAAM,EAA6C,EAAE,OAAO,CAC1D,QAAS,EAAE,OAAO,EAClB,UAAW,EACX,GAAI,EAGJ,UAAW,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,WAA2B,CAAC,CAAC,SAAS,CAC3E,CAAC,EAGD,SAAgB,kBAAkB,EAAyB,EAAiB,CAC1E,IAAM,EAAW,EAAO,IAAI,OAAO,EAC7B,EAAQ,IAAa,KAAO,EAAe,SAAW,OAAO,CAAQ,EAC3E,GAAI,CAAC,OAAO,UAAU,CAAK,GAAK,EAAQ,GAAK,EAAQ,EAAe,YAAa,MAAM,EAAW,EAClG,IAAM,EAAM,EAAO,IAAI,QAAQ,EACzB,EAAS,IAAQ,KAAO,KAAO,kBAAkB,EAAK,CAAmB,EAC/E,GAAI,IAAW,MAAQ,EAAO,UAAY,EAAS,MAAM,EAAW,EACpE,MAAO,CAAE,SAAQ,OAAM,CACzB,CAGA,SAAgB,YAAY,EAAkB,EAAiB,EAA2B,CAAC,EAAG,CAC5F,IAAM,EAAS,EAAY,IAAI,IAAI,EAAQ,GAAG,EAAG,CAAC,SAAU,QAAS,GAAG,CAAK,CAAC,EAC9E,MAAO,CAAE,GAAG,kBAAkB,EAAQ,CAAO,EAAG,QAAO,CACzD,CAEA,IAAa,YAAc,EAAwC,IACjE,kBAAkB,CAAE,GAAG,EAAQ,SAAQ,CAAC"}
|
package/dist/worker/env.cjs
CHANGED
|
@@ -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),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("./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
|
package/dist/worker/env.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env.cjs","names":[],"sources":["../../src/worker/env.ts"],"sourcesContent":["import { createRemoteJWKSet } 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 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":[],"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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"}
|
package/dist/worker/env.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../src/worker/env.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,
|
|
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"}
|
package/dist/worker/env.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{ApiError as e}from"./http.js";import{createRemoteJWKSet as t}from"jose";var
|
|
1
|
+
import{ApiError as e}from"./http.js";import{createRemoteJWKSet as t,customFetch as n}from"jose";var r=new Set([`127.0.0.1`,`localhost`,`[::1]`]),isLoopback=e=>r.has(e.hostname),configError=t=>new e(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)},i=`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 i;throw configError(`RATE_LIMIT_KEY_SECRET`)}if(t===`production`&&n.length<32)throw configError(`RATE_LIMIT_KEY_SECRET`);return n},a=new WeakMap;function apiConfig(e){let r=a.get(e);if(r)return r;let i=requireVar(e,`APP_ENV`);if(i!==`local`&&i!==`production`)throw configError(`APP_ENV`);let o=i;validateUrl(requireUrl(requireVar(e,`JWT_ISSUER`),`JWT_ISSUER`),o,`JWT_ISSUER`);let s=validateUrl(requireUrl(requireVar(e,`JWT_JWKS_URL`),`JWT_JWKS_URL`),o,`JWT_JWKS_URL`),c=requireVar(e,`JWT_AUDIENCE`),l=new Set(requireVar(e,`ALLOWED_ORIGINS`).split(`,`).map(e=>e.trim()).filter(e=>e.length>0).map(e=>validateOrigin(e,o,`ALLOWED_ORIGINS`)));if(l.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 u={allowedOrigins:l,appEnv:o,audience:c,issuer:requireVar(e,`JWT_ISSUER`),jwks:t(s,{[n]:(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,o)};return a.set(e,u),u}export{apiConfig};
|
|
2
2
|
//# sourceMappingURL=env.js.map
|
package/dist/worker/env.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env.js","names":[],"sources":["../../src/worker/env.ts"],"sourcesContent":["import { createRemoteJWKSet } 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 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":[],"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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"}
|