@vielzeug/codex 26.10.9 → 26.10.10

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.
@@ -1,6 +1,6 @@
1
1
  # Vielzeug: Full Documentation
2
2
 
3
- > Complete documentation for 42 packages. Version: 26.10.8
3
+ > Complete documentation for 42 packages. Version: 26.10.9
4
4
 
5
5
  ---
6
6
 
@@ -129,26 +129,26 @@ console.log(parsed.entries);
129
129
 
130
130
  ## API Overview
131
131
 
132
- | Symbol | Purpose | Execution mode | Common gotcha |
133
- | --- | --- | --- | --- |
134
- | `defineArchiveCollection` | Compose validation, indexing, identity and wire schemas | Sync | Invalid content derives to `null`; callback faults throw |
135
- | `createArchiveWorker` | Build the catalog Worker | Sync | Collection names must be unique |
136
- | `createAuthWorker` | Build the passwordless issuer | Sync | `AUTH_ARCHIVE` is required |
137
- | `worker.tap`, `issuer.tap` | Observe sanitized runtime outcomes | Sync | Neither core logs by default; observer exceptions are swallowed |
138
- | `ArchiveDerivationError` | Identify a collection implementation/runtime fault | Sync | Faults become sanitized 500s, not content-validation 422s |
139
- | `ARCHIVE_LIMITS` | Shared application bounds | Sync | Page bounds do not cap total exports |
140
- | `archiveErrorResponseSchema`, `archiveAuthHealthSchema` | Validate error and issuer-health responses | Sync | Unknown error codes and health states reject |
141
- | `ArchiveError`, `ArchiveDefinitionError` | Public base and invalid-definition errors | Sync | Thrown locally, not transmitted as error objects |
142
- | `sessionCookieValue` | Compose shared session-cookie attributes | Sync | Deployments must choose `secure` from their environment |
143
- | `isWellFormedText` | Check control characters and lone surrogates | Sync | Does not trim or impose a length bound |
132
+ | Symbol | Purpose | Execution mode | Common gotcha |
133
+ | ------------------------------------------------------- | ------------------------------------------------------- | -------------- | --------------------------------------------------------------- |
134
+ | `defineArchiveCollection` | Compose validation, indexing, identity and wire schemas | Sync | Invalid content derives to `null`; callback faults throw |
135
+ | `createArchiveWorker` | Build the catalog Worker | Sync | Collection names must be unique |
136
+ | `createAuthWorker` | Build the passwordless issuer | Sync | `AUTH_ARCHIVE` is required |
137
+ | `worker.tap`, `issuer.tap` | Observe sanitized runtime outcomes | Sync | Neither core logs by default; observer exceptions are swallowed |
138
+ | `ArchiveDerivationError` | Identify a collection implementation/runtime fault | Sync | Faults become sanitized 500s, not content-validation 422s |
139
+ | `ARCHIVE_LIMITS` | Shared application bounds | Sync | Page bounds do not cap total exports |
140
+ | `archiveErrorResponseSchema`, `archiveAuthHealthSchema` | Validate error and issuer-health responses | Sync | Unknown error codes and health states reject |
141
+ | `ArchiveError`, `ArchiveDefinitionError` | Public base and invalid-definition errors | Sync | Thrown locally, not transmitted as error objects |
142
+ | `sessionCookieValue` | Compose shared session-cookie attributes | Sync | Deployments must choose `secure` from their environment |
143
+ | `isWellFormedText` | Check control characters and lone surrogates | Sync | Does not trim or impose a length bound |
144
144
 
145
145
  ## Package Entry Point
146
146
 
147
- | Import | Purpose |
148
- | --- | --- |
149
- | `@vielzeug/archive` | Browser-safe collections, constants, schemas and errors |
147
+ | Import | Purpose |
148
+ | -------------------------- | --------------------------------------------------------------------------- |
149
+ | `@vielzeug/archive` | Browser-safe collections, constants, schemas and errors |
150
150
  | `@vielzeug/archive/worker` | Catalog factory, issuer signing-key/session service binding and event types |
151
- | `@vielzeug/archive/auth` | Issuer factory and binding types |
151
+ | `@vielzeug/archive/auth` | Issuer factory and binding types |
152
152
 
153
153
  ## Collections
154
154
 
@@ -160,14 +160,14 @@ Creates a frozen collection and its contract; malformed names, versions or vocab
160
160
  function defineArchiveCollection(definition: ArchiveCollectionDefinition): ArchiveCollection;
161
161
  ```
162
162
 
163
- | Parameter | Contract |
164
- | --- | --- |
165
- | `name` | Lowercase `[a-z0-9-]{1,32}` |
166
- | `contentVersion` | Positive safe integer |
167
- | `payloadSchema` | Spell schema whose output is a stable JSON object |
168
- | `filters`, `tags` | Optional readonly arrays; omitted means empty vocabulary |
169
- | `canonicalize` | Optional pure, idempotent, schema-valid normalization |
170
- | `index` | Derive `filter`, `tags` and searchable `title` without mutating the payload |
163
+ | Parameter | Contract |
164
+ | ----------------- | --------------------------------------------------------------------------- |
165
+ | `name` | Lowercase `[a-z0-9-]{1,32}` |
166
+ | `contentVersion` | Positive safe integer |
167
+ | `payloadSchema` | Spell schema whose output is a stable JSON object |
168
+ | `filters`, `tags` | Optional readonly arrays; omitted means empty vocabulary |
169
+ | `canonicalize` | Optional pure, idempotent, schema-valid normalization |
170
+ | `index` | Derive `filter`, `tags` and searchable `title` without mutating the payload |
171
171
 
172
172
  **Returns:** `ArchiveCollection`.
173
173
 
@@ -188,14 +188,14 @@ if (derived === null) throw new Error('Invalid recipe');
188
188
  console.log(derived.contentHash);
189
189
  ```
190
190
 
191
- | Method / property | Result |
192
- | --- | --- |
193
- | `derive(raw)` | `Promise` |
194
- | `contract.entry` | Public entry schema |
195
- | `contract.ownedEntry`, `ownedEntries` | Owner view with `clientKey` and `hidden` |
196
- | `contract.page` | Entries and the forward cursor |
191
+ | Method / property | Result |
192
+ | -------------------------------------- | --------------------------------------------------- |
193
+ | `derive(raw)` | `Promise` |
194
+ | `contract.entry` | Public entry schema |
195
+ | `contract.ownedEntry`, `ownedEntries` | Owner view with `clientKey` and `hidden` |
196
+ | `contract.page` | Entries and the forward cursor |
197
197
  | `contract.publishInput`, `updateInput` | Publish and revision-guarded update request schemas |
198
- | `filters`, `tags` | Frozen vocabulary snapshots |
198
+ | `filters`, `tags` | Frozen vocabulary snapshots |
199
199
 
200
200
  `derive` returns `null` for invalid JSON objects, schema-invalid input, invalid titles, too many tags and oversized canonical payloads. Unexpected schema, callback or hashing failures throw `ArchiveDerivationError`. Non-idempotent or schema-invalid canonicalization, unstable schema transformations, index mutation and out-of-vocabulary projections are also derivation faults. Routes map `null` to 422 `invalid-payload` and faults to a sanitized 500 `internal`.
201
201
  Schema output that is lossy or no longer JSON is a derivation fault, not invalid
@@ -229,18 +229,19 @@ import { createArchiveWorker, type ArchiveWorkerEnv } from '@vielzeug/archive/wo
229
229
  import { s } from '@vielzeug/spell';
230
230
 
231
231
  const notes = defineArchiveCollection({
232
- name: 'notes', contentVersion: 1,
232
+ name: 'notes',
233
+ contentVersion: 1,
233
234
  payloadSchema: s.object({ title: s.string().min(1).max(128) }),
234
235
  index: ({ title }) => ({ title, filter: null, tags: [] }),
235
236
  });
236
237
  export default createArchiveWorker([notes]);
237
238
  ```
238
239
 
239
- | Method | Contract |
240
- | --- | --- |
241
- | `fetch(request, env, ctx)` | Returns `Promise` |
240
+ | Method | Contract |
241
+ | --------------------------------- | -------------------------------------------------------------------------------- |
242
+ | `fetch(request, env, ctx)` | Returns `Promise` |
242
243
  | `scheduled(controller, env, ctx)` | Sweeps expired retry keys and revocation tombstones; emits failures and rethrows |
243
- | `tap(handler, { signal? }?)` | Returns unsubscribe; a pre-aborted signal registers nothing |
244
+ | `tap(handler, { signal? }?)` | Returns unsubscribe; a pre-aborted signal registers nothing |
244
245
 
245
246
  `AUTH_ISSUER_SERVICE` is a required `fetch`-shaped service binding to the issuer.
246
247
  The catalog fetches signing keys from the configured `JWT_JWKS_URL` through that
@@ -300,11 +301,11 @@ import { createAuthWorker, type AuthWorkerEnv } from '@vielzeug/archive/auth';
300
301
  export default createAuthWorker();
301
302
  ```
302
303
 
303
- | Method | Contract |
304
- | --- | --- |
305
- | `fetch(request, env, ctx?)` | Returns `Promise` |
304
+ | Method | Contract |
305
+ | ---------------------------------- | ------------------------------------------------------------------------------- |
306
+ | `fetch(request, env, ctx?)` | Returns `Promise` |
306
307
  | `scheduled(controller, env, ctx?)` | Removes expired links, closed send windows and unverified unreferenced accounts |
307
- | `tap(handler, { signal? }?)` | Observes typed `AuthEvent`; returns unsubscribe; pre-aborted signals are inert |
308
+ | `tap(handler, { signal? }?)` | Observes typed `AuthEvent`; returns unsubscribe; pre-aborted signals are inert |
308
309
 
309
310
  Magic links expire after 15 minutes and work once. The durable per-email budget allows three sends per hour; consumption, expiration and send failure do not refund it. Ordinary sessions last at most 30 days. Operator sessions use `OPERATOR_SESSION_MAX_AGE_SECONDS` (900 seconds). `OPERATOR_EMAILS` controls newly minted role claims.
310
311
 
@@ -331,12 +332,44 @@ contract; `/auth` exports the factory and issuer binding types.
331
332
  Opening a magic link performs a safe GET that displays a click-confirmation page,
332
333
  not redemption. Only a marked POST from an exact allowed origin or
333
334
  `PUBLIC_ORIGIN` consumes `{ token }` and returns 204 with the session cookie. The
334
- confirmation page navigates to `/signed-in` after success; the POST is not a
335
- redirect. Confirmation responses are `no-store` with no-referrer and restrictive
336
- CSP; the page removes the token from the address bar and loads no external assets.
335
+ confirmation page navigates to the fixed `${PUBLIC_ORIGIN}/signed-in` destination;
336
+ your application must serve that page because the issuer serves only the
337
+ confirmation endpoint. The POST is not a redirect. Confirmation responses are
338
+ `no-store` with no-referrer and a restrictive CSP; inline script and styles
339
+ require a fresh response nonce, with no
340
+ `unsafe-inline`. The page removes the token from the address bar before rendering
341
+ the controls and loads no external assets. `APP_NAME` supplies escaped page and
342
+ email branding; the confirmation has keyboard focus, live status announcements,
343
+ 44-pixel minimum action heights, a warm near-white paper and copper light scheme,
344
+ a deep neutral dark scheme, serif headings, sans-serif body text and reduced-motion
345
+ styles. Without JavaScript, it links to `${PUBLIC_ORIGIN}/account` as “Request a
346
+ new sign-in link” but cannot redeem a link.
337
347
  GET displays confirmation for any syntactically valid token, including a used
338
348
  token; only POST establishes whether the link is still redeemable. POST accepts
339
- exactly `{ token }` and rejects query parameters.
349
+ exactly `{ token }` and rejects query parameters. With valid issuer configuration,
350
+ invalid or missing GET tokens, duplicate token parameters and unsupported query
351
+ parameters return HTTP 400 with branded HTML recovery, not a JSON error. POST
352
+ errors retain the JSON `ArchiveErrorResponse` schema and their HTTP status.
353
+
354
+ The button announces pending sign-in and blocks duplicate submissions. HTTP 410
355
+ replaces it with a new-link action; HTTP 400 also disables resubmission. Every
356
+ failure offers the fixed `${PUBLIC_ORIGIN}/account` recovery link labelled
357
+ “Request a new sign-in link”. Like `/signed-in`, `/account` is a fixed reference
358
+ issuer convention: the application must serve its sign-in entry there, not the issuer.
359
+ Recovery is secondary while retry remains possible, and becomes the primary action
360
+ for invalid or expired links. It replaces the footer exit link to avoid duplicate
361
+ navigation. The initial “Back to app” footer links to `${PUBLIC_ORIGIN}/`.
362
+ HTTP 429 delays manual retry using unsigned integer seconds or a syntactically valid
363
+ HTTP-date `Retry-After`, rounded up
364
+ and bounded to 1-300 seconds (30 seconds for missing or invalid values). HTTP 503
365
+ and network failures offer explicit retry; no failure starts an automatic request.
366
+ All returned response bodies, including discarded stale responses, are cancelled
367
+ without delaying recovery; cancellation failures are ignored. Each attempt aborts
368
+ its controller on completion. Stalled requests abort after 15 seconds with a
369
+ timeout-specific message, distinct from connection failures. Leaving the page aborts its request and
370
+ clears timers; bfcache restoration resumes only the remaining cooldown, not a
371
+ request. Navigation after exact 204 success is fixed at
372
+ `${PUBLIC_ORIGIN}/signed-in`; query input never controls either destination.
340
373
 
341
374
  The issuer never logs directly. `tap` emits sanitized request and scheduled
342
375
  outcomes; observer failures are swallowed, abort detaches the observer and failed
@@ -346,39 +379,39 @@ scheduled work emits then rethrows. `AuthDiagnostic` reports only a category.
346
379
 
347
380
  Every route below is mounted under `/api/v1`. Mutation guards require an exact allowed `Origin` and `x-archive-request: 1`. Registered publishers pin `x-archive-publisher` to the identity captured when preparing private requests. The header is a freshness precondition, not authority.
348
381
 
349
- | Route | Contract |
350
- | --- | --- |
351
- | `GET /health` | Public catalog readiness; 200 `ok` or 503 `degraded` |
352
- | `POST /sign-out` | Catalog-local sign-out; `{ ok: true }` and expired session cookie, without issuer access |
353
- | `GET /collections/:collection` | Collection metadata |
354
- | `GET /collections/:collection/entries` | `author`, `filter`, `tag`, `search`, `sort`, `cursor`, `limit`; forward entry page |
355
- | `POST /collections/:collection/entries` | `{ clientKey, payload }`; 201 create or 200 replay |
356
- | `GET /collections/:collection/entries/:id` | Public entry; always `no-store` |
357
- | `PUT /collections/:collection/entries/:id` | Owner-only `{ expectedRevision, payload }` |
358
- | `DELETE /collections/:collection/entries/:id` | Owner or operator removal; 204 |
359
- | `PUT` / `DELETE /collections/:collection/entries/:id/like` | Like / unlike visible entries only; guarded write and response; like-state response |
360
- | `POST /collections/:collection/entries/:id/report` | `{ reason }`; idempotent per entry/reporter; 204 |
361
- | `PUT` / `DELETE /collections/:collection/entries/:id/hidden` | Operator hide / unhide; 204 |
362
- | `GET /collections/:collection/reports` | Operator page; `status=unresolved` (default) or `resolved`, `cursor`, `limit` |
363
- | `PUT /collections/:collection/reports/:id/resolved` | Operator resolves report by its unique report ID; idempotent 204 |
364
- | `DELETE /publishers/:id` | Operator erases Archive profile; not issuer account |
365
- | `GET /moderation-log` | Operator forward page; `cursor`, `limit` |
366
- | `GET` / `POST` / `PUT /me` | Identity, register with optional `{ name }`, rename with required `{ name }` |
367
- | `DELETE /me` | Profile-only cascade; 204; does not clear cookie |
368
- | `DELETE /me/account` | Revoke issuer/subject and atomically erase profile; 204; coordinated issuer-erasure seam |
369
- | `GET /me/publications?collection=…` | Owner entries including hidden state and retry keys |
370
- | `GET /me/likes` | Forward page `{ ids, cursor }`; `cursor`, `limit` |
371
- | `GET /me/likes?ids=…` | Subset lookup `{ ids }`; at most `likeStateIds` input IDs |
372
- | `GET /me/reports` | Own forward report page across collections; `cursor`, `limit` |
373
- | `GET /auth/health` | Public issuer readiness; typed `AuthHealth`, 200 `ok` or 503 `degraded`; no email or peer probe |
374
- | `GET /auth/session` | Internal issuer-session check for the catalog service binding; exact `{ ok: true }`, `no-store`, no PII; 401 for an invalid, stale or absent-account session |
375
- | `POST /auth/sign-out-all` | Requires marker, exact origin, session and captured `ACCOUNT_HEADER`; atomically increments `session_version`, deletes outstanding links, preserves account and catalog data; 200 `{ ok: true }` with an expired cookie only on durable success |
376
- | `GET /auth/jwks.json` | Active and configured retired public verification keys |
377
- | `POST /auth/sign-in` | `{ email }` (trimmed and lowercased); 202 `{ ok: true }` |
378
- | `GET /auth/verify?token=…` | Safe no-store click-confirmation page; no token consumption or session change |
379
- | `POST /auth/verify` | JSON `{ token }`, marker and exact allowed origin or `PUBLIC_ORIGIN`; consume once; 204 with session cookie |
380
- | `GET /auth/account` | `{ id, email, createdAt, lastSignInAt }`; no tokens or budget data |
381
- | `DELETE /auth/account` | Requires `ACCOUNT_HEADER` with the captured issuer account UUID; catalog `/me/account` via `AUTH_ARCHIVE`, require exact 204, then issuer user and cookie clear; 204 |
382
+ | Route | Contract |
383
+ | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
384
+ | `GET /health` | Public catalog readiness; 200 `ok` or 503 `degraded` |
385
+ | `POST /sign-out` | Catalog-local sign-out; `{ ok: true }` and expired session cookie, without issuer access |
386
+ | `GET /collections/:collection` | Collection metadata |
387
+ | `GET /collections/:collection/entries` | `author`, `filter`, `tag`, `search`, `sort`, `cursor`, `limit`; forward entry page |
388
+ | `POST /collections/:collection/entries` | `{ clientKey, payload }`; 201 create or 200 replay |
389
+ | `GET /collections/:collection/entries/:id` | Public entry; always `no-store` |
390
+ | `PUT /collections/:collection/entries/:id` | Owner-only `{ expectedRevision, payload }` |
391
+ | `DELETE /collections/:collection/entries/:id` | Owner or operator removal; 204 |
392
+ | `PUT` / `DELETE /collections/:collection/entries/:id/like` | Like / unlike visible entries only; guarded write and response; like-state response |
393
+ | `POST /collections/:collection/entries/:id/report` | `{ reason }`; idempotent per entry/reporter; 204 |
394
+ | `PUT` / `DELETE /collections/:collection/entries/:id/hidden` | Operator hide / unhide; 204 |
395
+ | `GET /collections/:collection/reports` | Operator page; `status=unresolved` (default) or `resolved`, `cursor`, `limit` |
396
+ | `PUT /collections/:collection/reports/:id/resolved` | Operator resolves report by its unique report ID; idempotent 204 |
397
+ | `DELETE /publishers/:id` | Operator erases Archive profile; not issuer account |
398
+ | `GET /moderation-log` | Operator forward page; `cursor`, `limit` |
399
+ | `GET` / `POST` / `PUT /me` | Identity, register with optional `{ name }`, rename with required `{ name }` |
400
+ | `DELETE /me` | Profile-only cascade; 204; does not clear cookie |
401
+ | `DELETE /me/account` | Revoke issuer/subject and atomically erase profile; 204; coordinated issuer-erasure seam |
402
+ | `GET /me/publications?collection=…` | Owner entries including hidden state and retry keys |
403
+ | `GET /me/likes` | Forward page `{ ids, cursor }`; `cursor`, `limit` |
404
+ | `GET /me/likes?ids=…` | Subset lookup `{ ids }`; at most `likeStateIds` input IDs |
405
+ | `GET /me/reports` | Own forward report page across collections; `cursor`, `limit` |
406
+ | `GET /auth/health` | Public issuer readiness; typed `AuthHealth`, 200 `ok` or 503 `degraded`; no email or peer probe |
407
+ | `GET /auth/session` | Internal issuer-session check for the catalog service binding; exact `{ ok: true }`, `no-store`, no PII; 401 for an invalid, stale or absent-account session |
408
+ | `POST /auth/sign-out-all` | Requires marker, exact origin, session and captured `ACCOUNT_HEADER`; atomically increments `session_version`, deletes outstanding links, preserves account and catalog data; 200 `{ ok: true }` with an expired cookie only on durable success |
409
+ | `GET /auth/jwks.json` | Active and configured retired public verification keys |
410
+ | `POST /auth/sign-in` | `{ email }` (trimmed and lowercased); 202 `{ ok: true }` |
411
+ | `GET /auth/verify?token=…` | Safe no-store click-confirmation HTML; with valid issuer configuration, invalid queries return 400 recovery HTML; no token consumption or session change |
412
+ | `POST /auth/verify` | JSON `{ token }`, marker and exact allowed origin or `PUBLIC_ORIGIN`; consume once; 204 with session cookie |
413
+ | `GET /auth/account` | `{ id, email, createdAt, lastSignInAt }`; no tokens or budget data |
414
+ | `DELETE /auth/account` | Requires `ACCOUNT_HEADER` with the captured issuer account UUID; catalog `/me/account` via `AUTH_ARCHIVE`, require exact 204, then issuer user and cookie clear; 204 |
382
415
 
383
416
  Forward pages are newest first with opaque context-bound keyset cursors. Keep the same query context when following a cursor; end at `cursor: null`. `maxPageSize` bounds each page, not the total number of liked IDs, reports or audit actions you can export. The subset-like query is not a page and cannot combine `ids` with `cursor` or `limit`.
384
417
 
@@ -434,37 +467,37 @@ Effective hide, unhide, operator removal and publisher erasure append audit rows
434
467
 
435
468
  ## Constants and Schemas
436
469
 
437
- | Export | Value or parsed output |
438
- | --- | --- |
439
- | `ARCHIVE_CONTRACT_VERSION`, `ARCHIVE_SCHEMA_VERSION` | Both debut at `1`; wire and storage versions are independent |
440
- | `AUTH_SCHEMA_VERSION` | `1`; issuer DDL stamp checked by `/auth/health` |
441
- | `ARCHIVE_COLLECTION_NAME_PATTERN` | `/^[a-z0-9-]{1,32}$/` |
442
- | `ARCHIVE_OPERATOR_ROLE` | `'archive-operator'` |
443
- | `SESSION_COOKIE_NAME`, `SESSION_COOKIE_PATH` | `'archive_session'`, `'/api/v1'` |
444
- | `SESSION_MAX_AGE_SECONDS` | `2592000` |
445
- | `OPERATOR_SESSION_MAX_AGE_SECONDS` | `900`; shared root contract |
446
- | `ACCOUNT_HEADER` | `'X-Archive-Account'`; required issuer-account UUID on coordinated erasure |
447
- | `PUBLISHER_HEADER`, `REQUEST_MARKER_HEADER` | `'x-archive-publisher'`, `'x-archive-request'` |
448
- | `ILL_FORMED_TEXT`, `isWellFormedText(value: string): boolean` | Reject control characters and lone UTF-16 surrogates |
449
- | `uuidSchema`, `isoTimestampSchema`, `hex64Schema` | UUID, ISO timestamp, lowercase SHA-256 hex strings |
450
- | `archiveAliasSchema` | Trimmed, well-formed alias of at most 24 characters |
451
- | `archiveAuthorSchema` | `{ id: string; name: string }` |
452
- | `archiveIdentitySchema` | `ArchiveIdentity` |
453
- | `archiveAccountSchema` | `ArchiveAccount`; issuer metadata, excluding private fields |
454
- | `archiveLimitsSchema` | All numeric `ARCHIVE_LIMITS` fields |
455
- | `archiveHealthSchema` | `ArchiveHealth` |
456
- | `archiveAuthHealthSchema` | `AuthHealth` |
457
- | `archiveErrorResponseSchema` | `ArchiveErrorResponse` |
458
- | `archiveOkSchema` | `{ ok: true }` |
459
- | `archiveCollectionMetaSchema` | `ArchiveCollectionMeta` |
460
- | `archiveLikedIdsSchema` | `{ ids: string[] }` subset response |
461
- | `archiveLikesPageSchema` | `ArchiveLikesPage` |
462
- | `archiveLikeStateSchema` | `{ id: string; likeCount: number; liked: boolean }` |
463
- | `archiveReportsSchema`, `archiveOwnReportsSchema` | `ArchiveReports`, `ArchiveOwnReports` |
464
- | `archiveModerationLogSchema` | `ArchiveModerationLog` |
465
- | `archiveSortSchema` | `ArchiveSort` |
466
- | `ARCHIVE_ERROR_CODES` | Values of `ArchiveErrorCode` below |
467
- | `ARCHIVE_MODERATION_ACTIONS` | `['hide', 'unhide', 'remove', 'erase-publisher']` |
470
+ | Export | Value or parsed output |
471
+ | ------------------------------------------------------------- | -------------------------------------------------------------------------- |
472
+ | `ARCHIVE_CONTRACT_VERSION`, `ARCHIVE_SCHEMA_VERSION` | Both debut at `1`; wire and storage versions are independent |
473
+ | `AUTH_SCHEMA_VERSION` | `1`; issuer DDL stamp checked by `/auth/health` |
474
+ | `ARCHIVE_COLLECTION_NAME_PATTERN` | `/^[a-z0-9-]{1,32}$/` |
475
+ | `ARCHIVE_OPERATOR_ROLE` | `'archive-operator'` |
476
+ | `SESSION_COOKIE_NAME`, `SESSION_COOKIE_PATH` | `'archive_session'`, `'/api/v1'` |
477
+ | `SESSION_MAX_AGE_SECONDS` | `2592000` |
478
+ | `OPERATOR_SESSION_MAX_AGE_SECONDS` | `900`; shared root contract |
479
+ | `ACCOUNT_HEADER` | `'X-Archive-Account'`; required issuer-account UUID on coordinated erasure |
480
+ | `PUBLISHER_HEADER`, `REQUEST_MARKER_HEADER` | `'x-archive-publisher'`, `'x-archive-request'` |
481
+ | `ILL_FORMED_TEXT`, `isWellFormedText(value: string): boolean` | Reject control characters and lone UTF-16 surrogates |
482
+ | `uuidSchema`, `isoTimestampSchema`, `hex64Schema` | UUID, ISO timestamp, lowercase SHA-256 hex strings |
483
+ | `archiveAliasSchema` | Trimmed, well-formed alias of at most 24 characters |
484
+ | `archiveAuthorSchema` | `{ id: string; name: string }` |
485
+ | `archiveIdentitySchema` | `ArchiveIdentity` |
486
+ | `archiveAccountSchema` | `ArchiveAccount`; issuer metadata, excluding private fields |
487
+ | `archiveLimitsSchema` | All numeric `ARCHIVE_LIMITS` fields |
488
+ | `archiveHealthSchema` | `ArchiveHealth` |
489
+ | `archiveAuthHealthSchema` | `AuthHealth` |
490
+ | `archiveErrorResponseSchema` | `ArchiveErrorResponse` |
491
+ | `archiveOkSchema` | `{ ok: true }` |
492
+ | `archiveCollectionMetaSchema` | `ArchiveCollectionMeta` |
493
+ | `archiveLikedIdsSchema` | `{ ids: string[] }` subset response |
494
+ | `archiveLikesPageSchema` | `ArchiveLikesPage` |
495
+ | `archiveLikeStateSchema` | `{ id: string; likeCount: number; liked: boolean }` |
496
+ | `archiveReportsSchema`, `archiveOwnReportsSchema` | `ArchiveReports`, `ArchiveOwnReports` |
497
+ | `archiveModerationLogSchema` | `ArchiveModerationLog` |
498
+ | `archiveSortSchema` | `ArchiveSort` |
499
+ | `ARCHIVE_ERROR_CODES` | Values of `ArchiveErrorCode` below |
500
+ | `ARCHIVE_MODERATION_ACTIONS` | `['hide', 'unhide', 'remove', 'erase-publisher']` |
468
501
 
469
502
  ### `sessionCookieValue`
470
503
 
@@ -474,11 +507,11 @@ Composes a session cookie using the shared security attributes.
474
507
  function sessionCookieValue(token: string, options: { maxAgeSeconds: number; secure: boolean }): string;
475
508
  ```
476
509
 
477
- | Parameter | Contract |
478
- | --- | --- |
479
- | `token` | Session token, or empty string when clearing |
480
- | `options.maxAgeSeconds` | Cookie lifetime; `0` clears it |
481
- | `options.secure` | Include `Secure` for production HTTPS |
510
+ | Parameter | Contract |
511
+ | ----------------------- | -------------------------------------------- |
512
+ | `token` | Session token, or empty string when clearing |
513
+ | `options.maxAgeSeconds` | Cookie lifetime; `0` clears it |
514
+ | `options.secure` | Include `Secure` for production HTTPS |
482
515
 
483
516
  **Returns:** the `Set-Cookie` value with `HttpOnly`, `SameSite=Strict`, the shared path and optional `Secure`.
484
517
 
@@ -517,11 +550,22 @@ console.log(isWellFormedText('Rice'), isWellFormedText('\u0000'));
517
550
 
518
551
  ```ts
519
552
  const limits = {
520
- aliasChars: 24, cursorChars: 1024, idempotencyRetentionMs: 172800000,
521
- likeStateIds: 50, maxPageSize: 50, maxPublications: 50, pageSize: 10,
522
- payloadBytes: 8192, publicCacheSeconds: 30, reportReasonChars: 256,
523
- requestBytes: 12288, searchChars: 64, searchTerms: 6, tagsPerEntry: 16,
524
- titleChars: 128, vocabChars: 64,
553
+ aliasChars: 24,
554
+ cursorChars: 1024,
555
+ idempotencyRetentionMs: 172800000,
556
+ likeStateIds: 50,
557
+ maxPageSize: 50,
558
+ maxPublications: 50,
559
+ pageSize: 10,
560
+ payloadBytes: 8192,
561
+ publicCacheSeconds: 30,
562
+ reportReasonChars: 256,
563
+ requestBytes: 12288,
564
+ searchChars: 64,
565
+ searchTerms: 6,
566
+ tagsPerEntry: 16,
567
+ titleChars: 128,
568
+ vocabChars: 64,
525
569
  };
526
570
  ```
527
571
 
@@ -592,16 +636,35 @@ type ArchiveSort = 'liked' | 'recent';
592
636
  type ArchiveDerivationStage = 'canonicalize' | 'hash' | 'index' | 'schema';
593
637
  type ArchiveIdentity = { id: string; name: string; operator: boolean } | null;
594
638
  type ArchiveAccount = {
595
- id: string; email: string; createdAt: string; lastSignInAt: string | null;
639
+ id: string;
640
+ email: string;
641
+ createdAt: string;
642
+ lastSignInAt: string | null;
596
643
  };
597
644
  type ArchiveCollectionMeta = { filters: string[]; name: string; tags: string[] };
598
645
  type ArchiveLikesPage = { cursor: string | null; ids: string[] };
599
646
  type ArchiveErrorCode =
600
- | 'account-changed' | 'auth-unavailable' | 'bad-request' | 'conflict' | 'forbidden' | 'gone'
601
- | 'internal' | 'invalid-payload' | 'likes-disabled' | 'method-not-allowed'
602
- | 'not-found' | 'publisher-changed' | 'publishing-disabled' | 'quota'
603
- | 'rate-limited' | 'record-unavailable' | 'timeout' | 'too-large'
604
- | 'unauthenticated' | 'unavailable' | 'unsupported-media';
647
+ | 'account-changed'
648
+ | 'auth-unavailable'
649
+ | 'bad-request'
650
+ | 'conflict'
651
+ | 'forbidden'
652
+ | 'gone'
653
+ | 'internal'
654
+ | 'invalid-payload'
655
+ | 'likes-disabled'
656
+ | 'method-not-allowed'
657
+ | 'not-found'
658
+ | 'publisher-changed'
659
+ | 'publishing-disabled'
660
+ | 'quota'
661
+ | 'rate-limited'
662
+ | 'record-unavailable'
663
+ | 'timeout'
664
+ | 'too-large'
665
+ | 'unauthenticated'
666
+ | 'unavailable'
667
+ | 'unsupported-media';
605
668
  type ArchiveErrorResponse = {
606
669
  error: { code: ArchiveErrorCode; message: string; requestId: string };
607
670
  };
@@ -609,11 +672,22 @@ type ArchiveHealth = {
609
672
  dbSchemaVersion: number | null;
610
673
  features: { likes: boolean; publishing: boolean };
611
674
  limits: {
612
- aliasChars: number; cursorChars: number; idempotencyRetentionMs: number;
613
- likeStateIds: number; maxPageSize: number; maxPublications: number;
614
- pageSize: number; payloadBytes: number; publicCacheSeconds: number;
615
- reportReasonChars: number; requestBytes: number; searchChars: number;
616
- searchTerms: number; tagsPerEntry: number; titleChars: number; vocabChars: number;
675
+ aliasChars: number;
676
+ cursorChars: number;
677
+ idempotencyRetentionMs: number;
678
+ likeStateIds: number;
679
+ maxPageSize: number;
680
+ maxPublications: number;
681
+ pageSize: number;
682
+ payloadBytes: number;
683
+ publicCacheSeconds: number;
684
+ reportReasonChars: number;
685
+ requestBytes: number;
686
+ searchChars: number;
687
+ searchTerms: number;
688
+ tagsPerEntry: number;
689
+ titleChars: number;
690
+ vocabChars: number;
617
691
  };
618
692
  schemaVersion: 1;
619
693
  status: 'ok' | 'degraded';
@@ -628,23 +702,36 @@ type AuthHealth = {
628
702
  type ArchiveReports = {
629
703
  cursor: string | null;
630
704
  reports: {
631
- id: string; createdAt: string; entryId: string; entryTitle: string;
632
- reason: string; reporter: { id: string; name: string }; resolvedAt: string | null;
705
+ id: string;
706
+ createdAt: string;
707
+ entryId: string;
708
+ entryTitle: string;
709
+ reason: string;
710
+ reporter: { id: string; name: string };
711
+ resolvedAt: string | null;
633
712
  }[];
634
713
  };
635
714
  type ArchiveOwnReports = {
636
715
  cursor: string | null;
637
716
  reports: {
638
- collection: string; createdAt: string; entryId: string; id: string;
639
- reason: string; resolvedAt: string | null;
717
+ collection: string;
718
+ createdAt: string;
719
+ entryId: string;
720
+ id: string;
721
+ reason: string;
722
+ resolvedAt: string | null;
640
723
  }[];
641
724
  };
642
725
  type ArchiveModerationAction = 'hide' | 'unhide' | 'remove' | 'erase-publisher';
643
726
  type ArchiveModerationLog = {
644
727
  cursor: string | null;
645
728
  actions: {
646
- id: string; action: ArchiveModerationAction; collection: string | null;
647
- createdAt: string; operatorId: string; targetId: string;
729
+ id: string;
730
+ action: ArchiveModerationAction;
731
+ collection: string | null;
732
+ createdAt: string;
733
+ operatorId: string;
734
+ targetId: string;
648
735
  }[];
649
736
  };
650
737
  ```
@@ -743,7 +830,7 @@ interface AuthWorkerHandler {
743
830
  tap(handler: (event: AuthEvent) => void, options?: { readonly signal?: AbortSignal }): () => void;
744
831
  }
745
832
  type AuthDiagnostic = {
746
- readonly kind: 'configuration' | 'database' | 'email' | 'catalog' | 'internal';
833
+ readonly kind: 'configuration' | 'database' | 'schema' | 'email' | 'catalog' | 'internal';
747
834
  };
748
835
  type AuthEvent =
749
836
  | {
@@ -773,10 +860,10 @@ window resets atomically on a later sign-in request.
773
860
 
774
861
  ## Errors
775
862
 
776
- | Exported class | Trigger | Properties |
777
- | --- | --- | --- |
778
- | `ArchiveError` | Base for public package errors | `name`, `message`, optional `cause` |
779
- | `ArchiveDefinitionError` | Invalid collection definition or duplicate registered collection names | Extends `ArchiveError` |
863
+ | Exported class | Trigger | Properties |
864
+ | ------------------------ | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- |
865
+ | `ArchiveError` | Base for public package errors | `name`, `message`, optional `cause` |
866
+ | `ArchiveDefinitionError` | Invalid collection definition or duplicate registered collection names | Extends `ArchiveError` |
780
867
  | `ArchiveDerivationError` | Unexpected schema/canonicalization/index/hash fault or violated collection invariant | Extends `ArchiveError`; readonly `stage: ArchiveDerivationStage`; optional `cause` |
781
868
 
782
869
  `ArchiveError` and `ArchiveDefinitionError` accept `(message: string, opts?: ErrorOptions)`.
@@ -851,7 +938,8 @@ if (!meResponse.ok) throw new Error('Identity unavailable');
851
938
  const me = archiveIdentitySchema.parse(await meResponse.json());
852
939
  if (me === null) throw new Error('Register a publisher first');
853
940
  const mineResponse = await fetch('/api/v1/me/publications?collection=recipes', {
854
- credentials: 'same-origin', headers: { [PUBLISHER_HEADER]: me.id },
941
+ credentials: 'same-origin',
942
+ headers: { [PUBLISHER_HEADER]: me.id },
855
943
  });
856
944
  if (!mineResponse.ok) throw new Error('Own publications unavailable');
857
945
  const owned = recipesCollection.contract.ownedEntries.parse(await mineResponse.json());
@@ -930,15 +1018,15 @@ anonymous read budget or call the issuer.
930
1018
  The Worker reads its environment fail-closed — every binding must be present and
931
1019
  well-formed, or the route answers 503 without naming what is missing:
932
1020
 
933
- | Variable | Meaning |
934
- | ------------------------ | ---------------------------------------------------------------- |
935
- | `APP_ENV` | `local` or `production` (transport rules and the pepper policy) |
936
- | `ALLOWED_ORIGINS` | Exact canonical origins; never wildcards |
937
- | `JWT_ISSUER` / `JWT_AUDIENCE` / `JWT_JWKS_URL` | Exact issuer claim, audience and issuer JWKS endpoint URL |
938
- | `AUTH_ISSUER_SERVICE` | Required issuer service binding for signing-key retrieval and live session-version checks |
939
- | `LIKES_ENABLED` / `PUBLISHING_ENABLED` | Feature flags |
940
- | `RATE_LIMIT_KEY_SECRET` | HMAC pepper. Required ≥ 32 chars in production; local may fall back |
941
- | `DB` / `READ_LIMIT` / `WRITE_LIMIT` | D1 + rate-limit bindings |
1021
+ | Variable | Meaning |
1022
+ | ---------------------------------------------- | ----------------------------------------------------------------------------------------- |
1023
+ | `APP_ENV` | `local` or `production` (transport rules and the pepper policy) |
1024
+ | `ALLOWED_ORIGINS` | Exact canonical origins; never wildcards |
1025
+ | `JWT_ISSUER` / `JWT_AUDIENCE` / `JWT_JWKS_URL` | Exact issuer claim, audience and issuer JWKS endpoint URL |
1026
+ | `AUTH_ISSUER_SERVICE` | Required issuer service binding for signing-key retrieval and live session-version checks |
1027
+ | `LIKES_ENABLED` / `PUBLISHING_ENABLED` | Feature flags |
1028
+ | `RATE_LIMIT_KEY_SECRET` | HMAC pepper. Required ≥ 32 chars in production; local may fall back |
1029
+ | `DB` / `READ_LIMIT` / `WRITE_LIMIT` | D1 + rate-limit bindings |
942
1030
 
943
1031
  Both limiter bindings must run 60-second windows (`simple` limiters:
944
1032
  `period: 60`): the platform binding does not expose its period at runtime, so the
@@ -969,19 +1057,19 @@ sign-in time and claims the token. The issuer's D1 schema ships at
969
1057
  `migrations_dir` there. The catalog's `JWT_ISSUER` and `JWT_JWKS_URL` must identify
970
1058
  the issuer, and its audience must match the issuer's `AUDIENCE`.
971
1059
 
972
- | Variable | Meaning |
973
- | --- | --- |
974
- | `APP_NAME` | The app's display name: the magic-link email's subject and copy name it |
975
- | `ALLOWED_ORIGINS` | Exact origins allowed to trigger unsafe routes (the app origin(s)) |
976
- | `APP_ENV` | `local` or `production` — production requires HTTPS and sets Secure cookies |
977
- | `AUDIENCE` / `AUTH_ISSUER` | The claims every minted session carries; the catalog verifies the same pair |
978
- | `AUTH_DB` / `AUTH_EMAIL` | The issuer's D1 and send-email bindings |
979
- | `AUTH_ARCHIVE` | Required service binding to the catalog Worker; deletion calls its profile-erasure route |
980
- | `AUTH_JWT_PRIVATE_JWK` | The RS256 private key as a JWK JSON string: a wrangler secret in production |
981
- | `AUTH_JWT_PREVIOUS_PUBLIC_JWKS` | Optional retired public RS256 JWKS; publish during rotation until old sessions expire |
982
- | `OPERATOR_EMAILS` | Comma-separated emails whose newly minted sessions carry the operator role |
983
- | `PUBLIC_ORIGIN` | App origin for links, confirmation POST origin checks and client navigation after sign-in |
984
- | `SENDER_ADDRESS` | The address magic links come from: a domain onboarded to Cloudflare Email Service |
1060
+ | Variable | Meaning |
1061
+ | ------------------------------- | ----------------------------------------------------------------------------------------- |
1062
+ | `APP_NAME` | The app's display name in magic-link email and escaped confirmation-page branding |
1063
+ | `ALLOWED_ORIGINS` | Exact origins allowed to trigger unsafe routes (the app origin(s)) |
1064
+ | `APP_ENV` | `local` or `production` — production requires HTTPS and sets Secure cookies |
1065
+ | `AUDIENCE` / `AUTH_ISSUER` | The claims every minted session carries; the catalog verifies the same pair |
1066
+ | `AUTH_DB` / `AUTH_EMAIL` | The issuer's D1 and send-email bindings |
1067
+ | `AUTH_ARCHIVE` | Required service binding to the catalog Worker; deletion calls its profile-erasure route |
1068
+ | `AUTH_JWT_PRIVATE_JWK` | The RS256 private key as a JWK JSON string: a wrangler secret in production |
1069
+ | `AUTH_JWT_PREVIOUS_PUBLIC_JWKS` | Optional retired public RS256 JWKS; publish during rotation until old sessions expire |
1070
+ | `OPERATOR_EMAILS` | Comma-separated emails whose newly minted sessions carry the operator role |
1071
+ | `PUBLIC_ORIGIN` | App origin for links, confirmation POST origin checks and client navigation after sign-in |
1072
+ | `SENDER_ADDRESS` | The address magic links come from: a domain onboarded to Cloudflare Email Service |
985
1073
 
986
1074
  Under `wrangler dev` the send-email binding is simulated — the link prints to the
987
1075
  issuer's console, so that console is the inbox. Demo-only Alice/Bob routes are
@@ -1010,10 +1098,38 @@ Opening `GET /auth/verify?token=…` renders a no-store confirmation page and do
1010
1098
  not consume the link or change the session, so email scanners cannot sign in by
1011
1099
  fetching it. A deliberate click sends `POST /auth/verify` with JSON `{ token }`,
1012
1100
  the request marker and an exact allowed origin or `PUBLIC_ORIGIN`.
1013
- Success is 204 with Set-Cookie; the page then navigates to `/signed-in`. The
1014
- redemption update and account sign-in stamp are trigger-backed and atomic.
1101
+ Success is 204 with Set-Cookie; the page then navigates to the fixed
1102
+ `${PUBLIC_ORIGIN}/signed-in` destination. Your application must serve that page;
1103
+ the issuer only serves the confirmation endpoint. The redemption update and
1104
+ account sign-in stamp are trigger-backed and atomic.
1015
1105
  The page removes the token from the address bar, loads no external assets, and
1016
- sets a restrictive CSP and `Referrer-Policy: no-referrer`.
1106
+ sets a restrictive CSP and `Referrer-Policy: no-referrer`. Its inline CSS and
1107
+ script use a fresh nonce, never `unsafe-inline`. The page follows light/dark
1108
+ preferences, supports reduced motion, focuses the confirmation action and
1109
+ announces pending, success and recovery states. Without JavaScript it links to
1110
+ `${PUBLIC_ORIGIN}/account` as “Request a new sign-in link” and does not pretend
1111
+ to complete sign-in.
1112
+
1113
+ Archive only serves the confirmation endpoint. Your application must serve its
1114
+ sign-in entry at the fixed `${PUBLIC_ORIGIN}/account` recovery destination and
1115
+ the success page at `${PUBLIC_ORIGIN}/signed-in`. These are reference issuer
1116
+ route conventions, not configurable destinations. The initial “Back to app”
1117
+ exit links to `${PUBLIC_ORIGIN}/`. No query parameter supplies a destination.
1118
+ Expired or already-used links (POST 410) require a new link rather than a retry.
1119
+ HTTP 429 enables manual retry after a `Retry-After` cooldown bounded to 1-300
1120
+ seconds, accepting unsigned integer seconds or valid HTTP-date syntax and
1121
+ defaulting to 30 seconds for missing or invalid values, including negative or
1122
+ fractional numbers. HTTP 503 and network failures offer a retry button and a
1123
+ secondary “Request a new sign-in link” action; invalid or expired links make
1124
+ that recovery action primary. The page never
1125
+ automatically retries, aborts stalled requests after 15 seconds and releases
1126
+ requests/timers when you leave it. Response bodies, including stale responses,
1127
+ are cancelled without waiting, and completed attempts abort their controllers.
1128
+
1129
+ With valid issuer configuration, malformed GET links return HTTP 400 with branded
1130
+ recovery HTML rather than the JSON error envelope. POST errors retain the JSON
1131
+ error schema. A well-formed expired token still gets GET 200 confirmation: its
1132
+ redemption status is determined only by the deliberate POST.
1017
1133
 
1018
1134
  `GET /auth/account` exports account metadata only. For full erasure, call
1019
1135
  `DELETE /auth/account` with `ACCOUNT_HEADER` (`X-Archive-Account`) set to the
@@ -1069,9 +1185,9 @@ against the same file. Point wrangler at the directory:
1069
1185
  {
1070
1186
  "binding": "DB",
1071
1187
  "database_name": "my-app",
1072
- "migrations_dir": "./node_modules/@vielzeug/archive/migrations"
1073
- }
1074
- ]
1188
+ "migrations_dir": "./node_modules/@vielzeug/archive/migrations",
1189
+ },
1190
+ ],
1075
1191
  }
1076
1192
  ```
1077
1193