@vielzeug/codex 26.10.10 → 26.10.12

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/data/search.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "description": "community content publishing for cloudflare workers + d1: a hardened worker core over per collection payload schemas",
5
5
  "docs": {
6
6
  "index": " \ntitle: archive\ndescription: \"community content publishing for cloudflare workers + d1: a hardened worker core over per collection payload schemas\"\npackage: archive\ncategory: utilities\nkeywords: [cloudflare, worker, d1, community, publishing, content, catalog, gdpr]\nrelated: [spell, courier, rune]\nexports: [definearchivecollection, createarchiveworker, createauthworker, archiveauthhealthschema, archivederivationerror]\nenvironments: [browser, node]\n \n\n<! markdownlint disable md025 md033 md060 >\n\n<packagehero package=\"archive\" />\n\n## why archive?\n\nyou can share content entries with ranked browsing, filters, likes and account\nerasure on cloudflare workers + d1. `definearchivecollection` supplies your json\npayload schema and indexing rules; the worker supplies sessions, guards,\nidempotent publishing, cursors and rate limits.\n\n```ts\n// before: each deployment repeats validation, indexing and route plumbing.\n\n// after: the deployment is one collection definition plus one factory call.\nimport { definearchivecollection } from '@vielzeug/archive';\nimport { createarchiveworker } from '@vielzeug/archive/worker';\nimport { s } from '@vielzeug/spell';\n\nconst notes = definearchivecollection({\n name: 'notes', contentversion: 1,\n payloadschema: s.object({ title: s.string().min(1).max(128) }),\n index: ({ title }) => ({ title, filter: null, tags: [] }),\n});\nexport default createarchiveworker([notes]);\n```\n\n| feature | archive | hand rolled worker | baas (firebase/supabase) |\n| | | | |\n| bundle size (core) | <packageinfo package=\"archive\" type=\"size\" /> | depends on deployment code | depends on selected sdk |\n| zero dependencies | no — spell and jose | depends on implementation | no — client sdk |\n| content knowledge in the core | <ore icon name=\"check\" size=\"16\"></ore icon> none — one collection seam | <ore icon name=\"x\" size=\"16\"></ore icon> baked in | <ore icon name=\"x\" size=\"16\"></ore icon> rules everywhere |\n| canonical fresh database d1 schema | <ore icon name=\"check\" size=\"16\"></ore icon> in the package | deployment owned | not a d1 backend |\n| idempotent publish w/ tombstones | <ore icon name=\"check\" size=\"16\"></ore icon> built in | <ore icon name=\"x\" size=\"16\"></ore icon> rebuild it | <ore icon name=\"x\" size=\"16\"></ore icon> rebuild it |\n| gdpr erasure / export endpoints | <ore icon name=\"check\" size=\"16\"></ore icon> built in | <ore icon name=\"x\" size=\"16\"></ore icon> rebuild it | <ore icon name=\"triangle alert\" size=\"16\"></ore icon> partial |\n| strict dto re validation on read | <ore icon name=\"check\" size=\"16\"></ore icon> every entry | <ore icon name=\"x\" size=\"16\"></ore icon> optional | <ore icon name=\"x\" size=\"16\"></ore icon> optional |\n\n<div class=\"decision callout\">\n\n**use archive when** an app needs a shareable, ranked, liked community space for\ncontent entries on cloudflare workers + d1, and can express its content as one strict\npayload schema.\n**consider alternatives when** you need rich queries beyond filter/tag/search, live\ncollaboration, or a backend you do not control — archive is deliberately one narrow,\nhardened surface.\n\n</div>\n\n## installation\n\n::: code group\n\n```sh [pnpm]\npnpm add @vielzeug/archive @vielzeug/spell\n```\n\n```sh [npm]\nnpm install @vielzeug/archive @vielzeug/spell\n```\n\n```sh [yarn]\nyarn add @vielzeug/archive @vielzeug/spell\n```\n\n:::\n\n## quick start\n\na deployment is one collection definition plus one factory call. the definition\nbrings the payload schema and what to index; everything else — the emitted entry\nenvelope, the content hash, the wire contract, the routes — is composed for you.\n\n```ts\nimport { definearchivecollection } from '@vielzeug/archive';\nimport { createarchiveworker } from '@vielzeug/archive/worker';\nimport { s } from '@vielzeug/spell';\n\n// the collection's payload: one strict schema is the whole content contract.\nconst payloadschema = s.object({\n cuisine: s.enum(['italian', 'japanese']),\n ingredients: s.array(s.string().trim().min(1).max(64)).min(1).max(20),\n name: s.string().trim().min(1).max(32),\n});\n\nconst recipescollection = definearchivecollection({\n canonicalize: (payload) => ({ ...payload, ingredients: [...payload.ingredients].sort() }),\n contentversion: 1,\n filters: ['italian', 'japanese'],\n index: (payload) => ({\n filter: payload.cuisine,\n tags: [payload.ingredients.length <= 5 ? 'quick' : 'involved'],\n title: payload.name,\n }),\n name: 'recipes',\n payloadschema,\n tags: ['quick', 'involved'],\n});\n\n// the whole worker: routing, auth, guards, limits, sql and dto disciplines included.\nexport default createarchiveworker([recipescollection]);\n```\n\nthe browser side never imports worker machinery. the same collection's composed\nschemas parse what the routes emit:\n\n```ts\nimport { definearchivecollection } from '@vielzeug/archive';\nimport { s } from '@vielzeug/spell';\n\nconst notes = definearchivecollection({\n name: 'notes', contentversion: 1,\n payloadschema: s.object({ title: s.string().min(1).max(128) }),\n index: ({ title }) => ({ title, filter: null, tags: [] }),\n});\nconst response = await fetch('/api/v1/collections/notes/entries?sort=liked');\nif (!response.ok) throw new error('archive unavailable');\nconst parsed = notes.contract.page.parse(await response.json());\nconsole.log(parsed.entries);\n```\n\n## features\n\n<div class=\"features grid\">\n\n `definearchivecollection` — the one content seam: payload schema, canonicalization, filter/tag/title derivation, union typed vocabularies\n `createarchiveworker` — the whole routes/auth/limits/sql stack from registered collections\n `collection.contract` — entry, page, publish/update input schemas shared by client and worker\n `archive_limits` + `archivehealthschema` — the application bounds, advertised by health and validated on both sides\n `archiveauthhealthschema` + `auth_schema_version` — typed issuer readiness checked against the issuer ddl stamp\n `archiveerrorresponseschema` — the one error envelope, code typed across the wire\n `createauthworker` — scanner safe confirmation, live session version enforcement, durable budgets, metadata export and coordinated erasure\n `auth_issuer_service` — signing key retrieval and an uncached issuer check after local catalog jwt verification\n `archivederivationerror` — distinguishes faulty collection code from invalid submitted content\n moderation — reader reports, the operator soft hide and the report queue, keyed on a verified `roles` claim\n `worker.tap()` / `issuer.tap()` — typed sanitized runtime events; neither core logs by default\n `migrations/` — canonical fresh database ddl with the storage stamp health reads back; no upgrade framework\n\n</div>\n\n## documentation\n\n<div class=\"doc links\">\n\n [usage guide](./usage.md) — stand up a collection, deploy the worker, wire the client.\n [api reference](./api.md) — every export, all three entry points, the full route table.\n [examples](./examples.md) — recipes from first deployment to gdpr erasure.\n [migration](./migration.md) — breaking changes after the first release and\n fresh database scope.\n\n</div>\n\n## see also\n\n<div class=\"see also\">\n\n [spell](/spell/) — every payload schema here is a spell schema; the core composes envelopes from it.\n [courier](/courier/) — the http client shape an app's frontend mirrors to consume these routes.\n [rune](/rune/) — the structured logger a deployment points `worker.tap()` events at.\n\n</div>\n\n<! markdownlint enable md060 md033 md025 >\n",
7
- "api": " \ntitle: 'archive: api reference'\ndescription: 'collection definitions, browser safe wire schemas, worker deployment, and identity issuance.'\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution mode | common gotcha |\n| | | | |\n| `definearchivecollection` | compose validation, indexing, identity and wire schemas | sync | invalid content derives to `null`; callback faults throw |\n| `createarchiveworker` | build the catalog worker | sync | collection names must be unique |\n| `createauthworker` | build the passwordless issuer | sync | `auth_archive` is required |\n| `worker.tap`, `issuer.tap` | observe sanitized runtime outcomes | sync | neither core logs by default; observer exceptions are swallowed |\n| `archivederivationerror` | identify a collection implementation/runtime fault | sync | faults become sanitized 500s, not content validation 422s |\n| `archive_limits` | shared application bounds | sync | page bounds do not cap total exports |\n| `archiveerrorresponseschema`, `archiveauthhealthschema` | validate error and issuer health responses | sync | unknown error codes and health states reject |\n| `archiveerror`, `archivedefinitionerror` | public base and invalid definition errors | sync | thrown locally, not transmitted as error objects |\n| `sessioncookievalue` | compose shared session cookie attributes | sync | deployments must choose `secure` from their environment |\n| `iswellformedtext` | check control characters and lone surrogates | sync | does not trim or impose a length bound |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/archive` | browser safe collections, constants, schemas and errors |\n| `@vielzeug/archive/worker` | catalog factory, issuer signing key/session service binding and event types |\n| `@vielzeug/archive/auth` | issuer factory and binding types |\n\n## collections\n\n### `definearchivecollection`\n\ncreates a frozen collection and its contract; malformed names, versions or vocabularies throw `archivedefinitionerror`.\n\n```ts\nfunction definearchivecollection<\n p extends object,\n const c extends number,\n const f extends string = never,\n const t extends string = never,\n>(definition: archivecollectiondefinition<p, c, f, t>): archivecollection<p, c>;\n```\n\n| parameter | contract |\n| | |\n| `name` | lowercase `[a z0 9 ]{1,32}` |\n| `contentversion` | positive safe integer |\n| `payloadschema` | spell schema whose output is a stable json object |\n| `filters`, `tags` | optional readonly arrays; omitted means empty vocabulary |\n| `canonicalize` | optional pure, idempotent, schema valid normalization |\n| `index` | derive `filter`, `tags` and searchable `title` without mutating the payload |\n\n**returns:** `archivecollection<p, c>`.\n\n**example:** define and derive one collection without worker bindings.\n\n```ts\nimport { definearchivecollection } from '@vielzeug/archive';\nimport { s } from '@vielzeug/spell';\n\nconst recipes = definearchivecollection({\n name: 'recipes',\n contentversion: 1,\n payloadschema: s.object({ name: s.string().min(1).max(32) }),\n index: (payload) => ({ filter: null, tags: [], title: payload.name }),\n});\nconst derived = await recipes.derive({ name: 'rice' });\nif (derived === null) throw new error('invalid recipe');\nconsole.log(derived.contenthash);\n```\n\n| method / property | result |\n| | |\n| `derive(raw)` | `promise<archivederivation \\| null>` |\n| `contract.entry` | public entry schema |\n| `contract.ownedentry`, `ownedentries` | owner view with `clientkey` and `hidden` |\n| `contract.page` | entries and the forward cursor |\n| `contract.publishinput`, `updateinput` | publish and revision guarded update request schemas |\n| `filters`, `tags` | frozen vocabulary snapshots |\n\n`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`.\nschema output that is lossy or no longer json is a derivation fault, not invalid\nsubmitted content. invalid index result shapes also throw.\n\nconst generics preserve the content version literal and infer filter/tag unions from vocabulary arrays without `as const`. `noinfer` prevents `index` from widening those unions; omitted arrays infer `never`, permitting only `filter: null` and empty tags.\n\nobjects serialize with recursively sorted keys; arrays preserve order unless your canonicalizer changes it. sha 256 hashes the core owned `{contentversion, payload}` envelope. stored payloads are canonical by construction, so the emitted entry schemas embed the plain payload schema.\n\n`filter`, tags and searchable title are persisted projections, not computed during reads. changing `index` or its vocabulary does not update stored rows. the active definition and persisted dataset must remain consistent; archive has no projection update or historical schema api.\n\n## worker and maintenance\n\n### `createarchiveworker`\n\nreturns the catalog request/scheduled handler and its observation side channel.\n\n```ts\nfunction createarchiveworker<e extends archiveworkerenv>(\n collections: readonly archivecollection[],\n): archiveworkerhandler<e>;\n```\n\n**returns:** `archiveworkerhandler<e>`; duplicate names throw `archivedefinitionerror` at registration.\n\n**example:** export a deployable worker using the package's structural bindings.\n\n```ts\nimport { definearchivecollection } from '@vielzeug/archive';\nimport { createarchiveworker, type archiveworkerenv } from '@vielzeug/archive/worker';\nimport { s } from '@vielzeug/spell';\n\nconst notes = definearchivecollection({\n name: 'notes',\n contentversion: 1,\n payloadschema: s.object({ title: s.string().min(1).max(128) }),\n index: ({ title }) => ({ title, filter: null, tags: [] }),\n});\nexport default createarchiveworker<archiveworkerenv>([notes]);\n```\n\n| method | contract |\n| | |\n| `fetch(request, env, ctx)` | returns `promise<response>` |\n| `scheduled(controller, env, ctx)` | sweeps expired retry keys and revocation tombstones; emits failures and rethrows |\n| `tap(handler, { signal? }?)` | returns unsubscribe; a pre aborted signal registers nothing |\n\n`auth_issuer_service` is a required `fetch` shaped service binding to the issuer.\nthe catalog fetches signing keys from the configured `jwt_jwks_url` through that\nbinding, not through global `fetch`. the url still selects the issuer's jwks\nroute and must satisfy the environment's transport rules. key requests use get\nwithout a session cookie and do not follow redirects.\nthe catalog verifies the signed jwt locally, then asks the issuer to validate its\ncurrent account session version. jose caches signing keys; the catalog does not\ncache successful issuer session verdicts.\nif the service exchange fails or exceeds its fixed 5000 ms timeout (body decoding\nincluded), the catalog returns `503 auth unavailable`; the timeout does not cancel\nwork already started by the issuer.\n\nonly anonymous entry lists can use the 30 second edge cache. any `cookie` or `authorization` header disables caching, even if invalid or empty. detail, metadata, private and error responses are `no store`. public reads do not verify sessions.\n\n \n\n### `worker.tap`\n\nobserves typed request, scheduled and cache failure events without affecting request behavior.\n\n```ts\ntap(handler: (event: archiveevent) => void, options?: { readonly signal?: abortsignal }): () => void;\n```\n\n**returns:** an unsubscribe function; abort detaches the observer. diagnostics contain categories and trusted configuration field names, never raw errors, sql, subjects, aliases, tokens or urls.\n\n**example:** observe a worker and detach explicitly.\n\n```ts\nimport { createarchiveworker, type archiveworkerenv } from '@vielzeug/archive/worker';\n\nconst worker = createarchiveworker<archiveworkerenv>([]);\nconst stop = worker.tap((event) => console.log(json.stringify(event)));\nstop();\n```\n\n \n\n## identity issuer\n\n### `createauthworker`\n\nreturns the passwordless email issuer with account export and coordinated erasure.\n\n```ts\nfunction createauthworker<e extends authworkerenv>(): authworkerhandler<e>;\n```\n\n**returns:** `authworkerhandler<e>`.\n\n**example:** export an issuer configured through deployment bindings.\n\n```ts\nimport { createauthworker, type authworkerenv } from '@vielzeug/archive/auth';\n\nexport default createauthworker<authworkerenv>();\n```\n\n| method | contract |\n| | |\n| `fetch(request, env, ctx?)` | returns `promise<response>` |\n| `scheduled(controller, env, ctx?)` | removes expired links, closed send windows and unverified unreferenced accounts |\n| `tap(handler, { signal? }?)` | observes typed `authevent`; returns unsubscribe; pre aborted signals are inert |\n\nmagic 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.\n\nthe issuer global admission budget is durable d1 state in `auth_sign_in_budget`:\n20 requests per 60 second window before account creation or email delivery.\nthis supplements, rather than replaces, the durable per email budget. no external\nrate limiter binding is required.\nadmission spends are not refunded after later database or email failures.\nrequest emails are trimmed and lowercased before validation. `sender_address`\nmust already be canonical; `operator_emails` entries are trimmed and lowercased\nbefore validation.\n\nsign in requests create pending account rows with `lastsigninat: null`. successful\nredemption atomically claims the one use link and stamps the account's sign in\ntime. the sweep deletes expired links and closed per user send windows, then\nunverified accounts with neither remaining links nor send budget references.\nverified accounts are retained until explicit account deletion.\n\n`operator_session_max_age_seconds = 900` is part of the browser safe root\ncontract; `/auth` exports the factory and issuer binding types.\n\n`auth_jwt_previous_public_jwks` optionally publishes retired public rs256 keys and verifies sessions minted before rotation; private members reject configuration. the package has no demo sign in routes.\n\nopening a magic link performs a safe get that displays a click confirmation page,\nnot redemption. only a marked post from an exact allowed origin or\n`public_origin` consumes `{ token }` and returns 204 with the session cookie. the\nconfirmation page navigates to the fixed `${public_origin}/signed in` destination;\nyour application must serve that page because the issuer serves only the\nconfirmation endpoint. the post is not a redirect. confirmation responses are\n`no store` with no referrer and a restrictive csp; inline script and styles\nrequire a fresh response nonce, with no\n`unsafe inline`. the page removes the token from the address bar before rendering\nthe controls and loads no external assets. `app_name` supplies escaped page and\nemail branding; the confirmation has keyboard focus, live status announcements,\n44 pixel minimum action heights, a warm near white paper and copper light scheme,\na deep neutral dark scheme, serif headings, sans serif body text and reduced motion\nstyles. without javascript, it links to `${public_origin}/account` as “request a\nnew sign in link” but cannot redeem a link.\nget displays confirmation for any syntactically valid token, including a used\ntoken; only post establishes whether the link is still redeemable. post accepts\nexactly `{ token }` and rejects query parameters. with valid issuer configuration,\ninvalid or missing get tokens, duplicate token parameters and unsupported query\nparameters return http 400 with branded html recovery, not a json error. post\nerrors retain the json `archiveerrorresponse` schema and their http status.\n\nthe button announces pending sign in and blocks duplicate submissions. http 410\nreplaces it with a new link action; http 400 also disables resubmission. every\nfailure offers the fixed `${public_origin}/account` recovery link labelled\n“request a new sign in link”. like `/signed in`, `/account` is a fixed reference\nissuer convention: the application must serve its sign in entry there, not the issuer.\nrecovery is secondary while retry remains possible, and becomes the primary action\nfor invalid or expired links. it replaces the footer exit link to avoid duplicate\nnavigation. the initial “back to app” footer links to `${public_origin}/`.\nhttp 429 delays manual retry using unsigned integer seconds or a syntactically valid\nhttp date `retry after`, rounded up\nand bounded to 1 300 seconds (30 seconds for missing or invalid values). http 503\nand network failures offer explicit retry; no failure starts an automatic request.\nall returned response bodies, including discarded stale responses, are cancelled\nwithout delaying recovery; cancellation failures are ignored. each attempt aborts\nits controller on completion. stalled requests abort after 15 seconds with a\ntimeout specific message, distinct from connection failures. leaving the page aborts its request and\nclears timers; bfcache restoration resumes only the remaining cooldown, not a\nrequest. navigation after exact 204 success is fixed at\n`${public_origin}/signed in`; query input never controls either destination.\n\nthe issuer never logs directly. `tap` emits sanitized request and scheduled\noutcomes; observer failures are swallowed, abort detaches the observer and failed\nscheduled work emits then rethrows. `authdiagnostic` reports only a category.\n\n## http routes\n\nevery 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.\n\n| route | contract |\n| | |\n| `get /health` | public catalog readiness; 200 `ok` or 503 `degraded` |\n| `post /sign out` | catalog local sign out; `{ ok: true }` and expired session cookie, without issuer access |\n| `get /collections/:collection` | collection metadata |\n| `get /collections/:collection/entries` | `author`, `filter`, `tag`, `search`, `sort`, `cursor`, `limit`; forward entry page |\n| `post /collections/:collection/entries` | `{ clientkey, payload }`; 201 create or 200 replay |\n| `get /collections/:collection/entries/:id` | public entry; always `no store` |\n| `put /collections/:collection/entries/:id` | owner only `{ expectedrevision, payload }` |\n| `delete /collections/:collection/entries/:id` | owner or operator removal; 204 |\n| `put` / `delete /collections/:collection/entries/:id/like` | like / unlike visible entries only; guarded write and response; like state response |\n| `post /collections/:collection/entries/:id/report` | `{ reason }`; idempotent per entry/reporter; 204 |\n| `put` / `delete /collections/:collection/entries/:id/hidden` | operator hide / unhide; 204 |\n| `get /collections/:collection/reports` | operator page; `status=unresolved` (default) or `resolved`, `cursor`, `limit` |\n| `put /collections/:collection/reports/:id/resolved` | operator resolves report by its unique report id; idempotent 204 |\n| `delete /publishers/:id` | operator erases archive profile; not issuer account |\n| `get /moderation log` | operator forward page; `cursor`, `limit` |\n| `get` / `post` / `put /me` | identity, register with optional `{ name }`, rename with required `{ name }` |\n| `delete /me` | profile only cascade; 204; does not clear cookie |\n| `delete /me/account` | revoke issuer/subject and atomically erase profile; 204; coordinated issuer erasure seam |\n| `get /me/publications?collection=…` | owner entries including hidden state and retry keys |\n| `get /me/likes` | forward page `{ ids, cursor }`; `cursor`, `limit` |\n| `get /me/likes?ids=…` | subset lookup `{ ids }`; at most `likestateids` input ids |\n| `get /me/reports` | own forward report page across collections; `cursor`, `limit` |\n| `get /auth/health` | public issuer readiness; typed `authhealth`, 200 `ok` or 503 `degraded`; no email or peer probe |\n| `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 |\n| `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 |\n| `get /auth/jwks.json` | active and configured retired public verification keys |\n| `post /auth/sign in` | `{ email }` (trimmed and lowercased); 202 `{ ok: true }` |\n| `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 |\n| `post /auth/verify` | json `{ token }`, marker and exact allowed origin or `public_origin`; consume once; 204 with session cookie |\n| `get /auth/account` | `{ id, email, createdat, lastsigninat }`; no tokens or budget data |\n| `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 |\n\nforward 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`.\n\nfull erasure creates an issuer/subject revocation tombstone for 30 days plus ten\nseconds and deletes the profile in the same transaction. catalog authentication\nrejects that subject with 401 on ordinary private routes; registration also checks\nrevocation in sql to prevent resurrection races. only `delete /me/account` may\nbypass the issuer session lookup, and only to retry erasure with the original\nsigned session of the same principal while its existing revocation tombstone is\nvalid; jwt signature and claims are still verified. it is not a general\nstale session exception: initial erasure validates the issuer session. the\nissuer facing `delete /auth/account` always checks the current session version\nbefore calling the catalog, so retrying it after completed erasure returns 401\nbecause the account is absent.\ndirect profile only `delete /me` allows later registration using the same session.\nthe sweep removes expired tombstones. re enrollment through the reference issuer\ncreates a fresh account uuid subject; an external issuer must issue a new subject\nto re enroll during the revocation window.\n\nthe issuer stores `auth_users.session_version` as a safe non negative integer,\ndefaulting to `0`. every session jwt carries the current value as the required\n`sessionversion` claim. each ordinary issuer authenticated route compares that\nclaim to the current account row; a mismatch is unauthenticated. `post\n/api/v1/auth/sign out all` checks the captured issuer account uuid against the\nverified subject before atomically incrementing the version and deleting pending\nlinks. a changed account answers 409 `account changed`; a failed request does not\nclear the cookie. this revokes all sessions, including the caller's, after a\nsuccessful response. requests authorized before revocation may finish; there is\nno cross database transactional or in flight cancellation guarantee.\n\nthe catalog requires `auth_issuer_service`, a service binding to the issuer for\nboth jwks retrieval and live session validation. signing key retrieval uses\n`jwt_jwks_url` through the binding, so it does not depend on a public same zone\nworker to worker fetch. after local jwt signature/claims verification, it requests\n`get /api/v1/auth/session` through that binding on every private request. only a\nstrictly validated 401 `unauthenticated` remains a 401. malformed, unexpected,\nprovider and 5xx responses become 503 `auth unavailable`; there is no authority\ncache. both service directions use a fixed 5000 ms timeout that includes body\ndecoding. the issuer's `/api/v1/auth/session` is an internal\nservice contract, not a frontend session api.\n\nissuer readiness is independent of catalog readiness. `get\n/api/v1/auth/health` returns `archiveauthhealthschema` as `authhealth`; its\nread only probe checks configuration, a signing and verification key proof,\nbinding shapes, the singleton `auth_schema_meta` stamp against\n`auth_schema_version`, and readability of expected tables, columns and triggers.\nit does not write, send email or probe the catalog service. responses are\nsanitized.\nconfiguration failure, database failure and schema mismatch return http 503\nwith `status: 'degraded'` and the corresponding reason.\n\neffective hide, unhide, operator removal and publisher erasure append audit rows atomically with the state transition. concurrent idempotent no ops append nothing. audit rows contain opaque ids, not names or content, and have no foreign keys so they outlive erasure. resolving a report sets `resolvedat`; it is not one of the four audit action kinds.\n\n## constants and schemas\n\n| export | value or parsed output |\n| | |\n| `archive_contract_version`, `archive_schema_version` | both debut at `1`; wire and storage versions are independent |\n| `auth_schema_version` | `1`; issuer ddl stamp checked by `/auth/health` |\n| `archive_collection_name_pattern` | `/^[a z0 9 ]{1,32}$/` |\n| `archive_operator_role` | `'archive operator'` |\n| `session_cookie_name`, `session_cookie_path` | `'archive_session'`, `'/api/v1'` |\n| `session_max_age_seconds` | `2592000` |\n| `operator_session_max_age_seconds` | `900`; shared root contract |\n| `account_header` | `'x archive account'`; required issuer account uuid on coordinated erasure |\n| `publisher_header`, `request_marker_header` | `'x archive publisher'`, `'x archive request'` |\n| `ill_formed_text`, `iswellformedtext(value: string): boolean` | reject control characters and lone utf 16 surrogates |\n| `uuidschema`, `isotimestampschema`, `hex64schema` | uuid, iso timestamp, lowercase sha 256 hex strings |\n| `archivealiasschema` | trimmed, well formed alias of at most 24 characters |\n| `archiveauthorschema` | `{ id: string; name: string }` |\n| `archiveidentityschema` | `archiveidentity` |\n| `archiveaccountschema` | `archiveaccount`; issuer metadata, excluding private fields |\n| `archivelimitsschema` | all numeric `archive_limits` fields |\n| `archivehealthschema` | `archivehealth` |\n| `archiveauthhealthschema` | `authhealth` |\n| `archiveerrorresponseschema` | `archiveerrorresponse` |\n| `archiveokschema` | `{ ok: true }` |\n| `archivecollectionmetaschema` | `archivecollectionmeta` |\n| `archivelikedidsschema` | `{ ids: string[] }` subset response |\n| `archivelikespageschema` | `archivelikespage` |\n| `archivelikestateschema` | `{ id: string; likecount: number; liked: boolean }` |\n| `archivereportsschema`, `archiveownreportsschema` | `archivereports`, `archiveownreports` |\n| `archivemoderationlogschema` | `archivemoderationlog` |\n| `archivesortschema` | `archivesort` |\n| `archive_error_codes` | values of `archiveerrorcode` below |\n| `archive_moderation_actions` | `['hide', 'unhide', 'remove', 'erase publisher']` |\n\n### `sessioncookievalue`\n\ncomposes a session cookie using the shared security attributes.\n\n```ts\nfunction sessioncookievalue(token: string, options: { maxageseconds: number; secure: boolean }): string;\n```\n\n| parameter | contract |\n| | |\n| `token` | session token, or empty string when clearing |\n| `options.maxageseconds` | cookie lifetime; `0` clears it |\n| `options.secure` | include `secure` for production https |\n\n**returns:** the `set cookie` value with `httponly`, `samesite=strict`, the shared path and optional `secure`.\n\n**example:** clear the production cookie.\n\n```ts\nimport { sessioncookievalue } from '@vielzeug/archive';\n\nconst expiredcookie = sessioncookievalue('', { maxageseconds: 0, secure: true });\nconsole.log(expiredcookie);\n```\n\n \n\n### `iswellformedtext`\n\nchecks text for control characters and lone utf 16 surrogates.\n\n```ts\nfunction iswellformedtext(value: string): boolean;\n```\n\n**returns:** `true` when `ill_formed_text` does not match; it does not trim or impose bounds.\n\n**example:** validate text independently of the alias schema.\n\n```ts\nimport { iswellformedtext } from '@vielzeug/archive';\n\nconsole.log(iswellformedtext('rice'), iswellformedtext('\\u0000'));\n```\n\n### `archive_limits`\n\n`archive_limits` is frozen with these values:\n\n```ts\nconst limits = {\n aliaschars: 24,\n cursorchars: 1024,\n idempotencyretentionms: 172800000,\n likestateids: 50,\n maxpagesize: 50,\n maxpublications: 50,\n pagesize: 10,\n payloadbytes: 8192,\n publiccacheseconds: 30,\n reportreasonchars: 256,\n requestbytes: 12288,\n searchchars: 64,\n searchterms: 6,\n tagsperentry: 16,\n titlechars: 128,\n vocabchars: 64,\n};\n```\n\n## types\n\nthe following definitions use `schema` from `@vielzeug/spell`. schema derived aliases show their complete output shape.\n\n```ts\ninterface archiveindexing<f extends string = string, t extends string = string> {\n filter: f | null;\n tags: readonly t[];\n title: string;\n}\ninterface archivecollectiondefinition<\n p extends object,\n c extends number,\n f extends string = never,\n t extends string = never,\n> {\n canonicalize?(payload: p): p;\n contentversion: c;\n filters?: readonly f[];\n index(payload: p): archiveindexing<noinfer<f>, noinfer<t>>;\n name: string;\n payloadschema: schema<p, unknown>;\n tags?: readonly t[];\n}\ninterface archivederivation {\n canonicaljson: string;\n contenthash: string;\n contentversion: number;\n filter: string | null;\n tags: readonly string[];\n title: string;\n}\ninterface archiveentry<p = unknown, c extends number = number> {\n author: { id: string; name: string };\n contenthash: string;\n contentversion: c;\n id: string;\n likecount: number;\n payload: p;\n publishedat: string;\n revision: number;\n schemaversion: typeof archive_contract_version;\n updatedat: string;\n}\ninterface archiveownedentry<p = unknown, c extends number = number> extends archiveentry<p, c> {\n clientkey: string;\n hidden: boolean;\n}\ninterface archivecontract<p = unknown, c extends number = number> {\n entry: schema<archiveentry<p, c>>;\n ownedentries: schema<{ entries: archiveownedentry<p, c>[] }>;\n ownedentry: schema<archiveownedentry<p, c>>;\n page: schema<archivepage<archiveentry<p, c>>>;\n publishinput: schema<{ clientkey: string; payload: p }, unknown>;\n updateinput: schema<{ expectedrevision: number; payload: p }, unknown>;\n}\ninterface archivecollection<p extends object = object, c extends number = number> {\n readonly contentversion: c;\n readonly contract: archivecontract<p, c>;\n readonly derive: (raw: unknown) => promise<archivederivation | null>;\n readonly filters: readonly string[];\n readonly name: string;\n readonly tags: readonly string[];\n}\ninterface archivepage<t> {\n cursor: string | null;\n entries: readonly t[];\n}\ntype archivesort = 'liked' | 'recent';\ntype archivederivationstage = 'canonicalize' | 'hash' | 'index' | 'schema';\ntype archiveidentity = { id: string; name: string; operator: boolean } | null;\ntype archiveaccount = {\n id: string;\n email: string;\n createdat: string;\n lastsigninat: string | null;\n};\ntype archivecollectionmeta = { filters: string[]; name: string; tags: string[] };\ntype archivelikespage = { cursor: string | null; ids: string[] };\ntype archiveerrorcode =\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';\ntype archiveerrorresponse = {\n error: { code: archiveerrorcode; message: string; requestid: string };\n};\ntype archivehealth = {\n dbschemaversion: number | null;\n features: { likes: boolean; publishing: boolean };\n limits: {\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 };\n schemaversion: 1;\n status: 'ok' | 'degraded';\n reason: 'database unavailable' | 'schema mismatch' | null;\n};\ntype authhealth = {\n dbschemaversion: number | null;\n schemaversion: 1;\n status: 'ok' | 'degraded';\n reason: 'configuration unavailable' | 'database unavailable' | 'schema mismatch' | null;\n};\ntype archivereports = {\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};\ntype archiveownreports = {\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};\ntype archivemoderationaction = 'hide' | 'unhide' | 'remove' | 'erase publisher';\ntype archivemoderationlog = {\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};\n```\n\nworker subpath types model only the binding methods the catalog calls.\n\n```ts\ninterface 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}\ninterface d1like {\n batch(statements: d1statement[]): promise<{ meta?: { changes?: number } }[]>;\n prepare(sql: string): d1statement;\n}\ninterface ratelimiterbinding {\n limit(input: { key: string }): promise<{ success: boolean }>;\n}\ninterface archiveworkerenv {\n auth_issuer_service: authissuerservicebinding;\n allowed_origins: string;\n app_env: string;\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}\ninterface authissuerservicebinding {\n fetch(request: request): promise<response>;\n}\ninterface archiveexecutioncontext {\n waituntil(promise: promise<unknown>): void;\n}\ntype archivediagnostic =\n | { readonly kind: 'configuration'; readonly field: string }\n | { readonly kind: 'database' | 'record' | 'schema' | 'internal' | 'cache' | 'collection' };\ninterface archiverequestevent {\n code: archiveerrorcode | 'ok';\n diagnostic?: archivediagnostic;\n ms: number;\n requestid: string;\n route: string;\n status: number;\n type: 'request';\n}\ntype archivescheduledevent =\n | { removed: number; type: 'scheduled'; status: 'ok' }\n | { type: 'scheduled'; status: 'failed'; diagnostic: archivediagnostic };\ntype archiveevent =\n | archiverequestevent\n | archivescheduledevent\n | { readonly type: 'cache error'; readonly diagnostic: archivediagnostic };\ninterface archiveworkerhandler<e extends archiveworkerenv = archiveworkerenv> {\n fetch(request: request, env: e, ctx: archiveexecutioncontext): promise<response>;\n scheduled?(controller: unknown, env: e, ctx: archiveexecutioncontext): void | promise<void>;\n tap(handler: (event: archiveevent) => void, options?: { readonly signal?: abortsignal }): () => void;\n}\n```\n\nscheduled `removed` counts the combined expired retry key and revocation rows.\n\nauth subpath types describe the sibling issuer and its required service binding.\n\n```ts\ninterface sendemail {\n send(message: { from: string; html?: string; subject: string; text: string; to: string }): promise<unknown>;\n}\ninterface autharchivebinding {\n fetch(request: request): promise<response>;\n}\ninterface authworkerenv {\n allowed_origins: string;\n app_env: string;\n app_name: string;\n audience: string;\n auth_archive: autharchivebinding;\n auth_db: d1like;\n auth_email: sendemail;\n auth_issuer: string;\n auth_jwt_previous_public_jwks?: string;\n auth_jwt_private_jwk: string;\n operator_emails: string;\n public_origin: string;\n sender_address: string;\n}\ninterface authworkerhandler<e extends authworkerenv = authworkerenv> {\n fetch(request: request, env: e, ctx?: archiveexecutioncontext): promise<response>;\n scheduled(controller: unknown, env: e, ctx?: archiveexecutioncontext): promise<void>;\n tap(handler: (event: authevent) => void, options?: { readonly signal?: abortsignal }): () => void;\n}\ntype authdiagnostic = {\n readonly kind: 'configuration' | 'database' | 'schema' | 'email' | 'catalog' | 'internal';\n};\ntype authevent =\n | {\n readonly type: 'request';\n readonly requestid: string;\n readonly route:\n | '/auth/account'\n | '/auth/health'\n | '/auth/jwks.json'\n | '/auth/session'\n | '/auth/sign in'\n | '/auth/sign out all'\n | '/auth/verify'\n | 'unmatched';\n readonly status: number;\n readonly code: archiveerrorcode | 'ok';\n readonly ms: number;\n readonly diagnostic?: authdiagnostic;\n }\n | { readonly type: 'scheduled'; readonly status: 'ok'; readonly removed: number }\n | { readonly type: 'scheduled'; readonly status: 'failed'; readonly diagnostic: authdiagnostic };\n```\n\nissuer scheduled `removed` counts expired token rows, closed per user send windows\nand deleted unverified accounts. the singleton admission row is retained and its\nwindow resets atomically on a later sign in request.\n\n## errors\n\n| exported class | trigger | properties |\n| | | |\n| `archiveerror` | base for public package errors | `name`, `message`, optional `cause` |\n| `archivedefinitionerror` | invalid collection definition or duplicate registered collection names | extends `archiveerror` |\n| `archivederivationerror` | unexpected schema/canonicalization/index/hash fault or violated collection invariant | extends `archiveerror`; readonly `stage: archivederivationstage`; optional `cause` |\n\n`archiveerror` and `archivedefinitionerror` accept `(message: string, opts?: erroroptions)`.\n`archivederivationerror` accepts `(stage: archivederivationstage, message: string, opts?: erroroptions)`.\nits message and cause are local diagnostics, not response content. worker request\nfailures use the wire envelope, not these thrown classes. internal `apierror` and\n`authoperationerror` are not exported.\n\nstored entries that fail the current wire schema return 503 `record unavailable`.\nstored payloads are canonical by construction, so the emitted entry schemas embed\nthe plain payload schema: parsing a response re runs that schema, and an\nexception from one of its callbacks becomes a sanitized 500.\n\nthe complete public error signatures are:\n\n```ts\nclass archiveerror extends error {\n constructor(message: string, opts?: erroroptions);\n}\nclass archivedefinitionerror extends archiveerror {}\nclass archivederivationerror extends archiveerror {\n readonly stage: archivederivationstage;\n constructor(stage: archivederivationstage, message: string, opts?: erroroptions);\n}\n```\n\ncatalog rate limit responses use `retry after: 60`; configure catalog limiter periods to 60 seconds. issuer admission/send budget responses instead carry seconds until their durable window reopens. health's degraded response is the health dto, not an error envelope: schema mismatch reports the observed version; database unavailability reports `dbschemaversion: null`.\n",
7
+ "api": " \ntitle: 'archive: api reference'\ndescription: 'collection definitions, browser safe wire schemas, worker deployment, and identity issuance.'\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution mode | common gotcha |\n| | | | |\n| `definearchivecollection` | compose validation, indexing, identity and wire schemas | sync | invalid content derives to `null`; callback faults throw |\n| `createarchiveworker` | build the catalog worker | sync | collection names must be unique |\n| `createauthworker` | build the passwordless issuer | sync | `auth_archive` is required |\n| `worker.tap`, `issuer.tap` | observe sanitized runtime outcomes | sync | neither core logs by default; observer exceptions are swallowed |\n| `archivederivationerror` | identify a collection implementation/runtime fault | sync | faults become sanitized 500s, not content validation 422s |\n| `archive_limits` | shared application bounds | sync | page bounds do not cap total exports |\n| `archiveerrorresponseschema`, `archiveauthhealthschema` | validate error and issuer health responses | sync | unknown error codes and health states reject |\n| `archiveerror`, `archivedefinitionerror` | public base and invalid definition errors | sync | thrown locally, not transmitted as error objects |\n| `sessioncookievalue` | compose shared session cookie attributes | sync | deployments must choose `secure` from their environment |\n| `iswellformedtext` | check control characters and lone surrogates | sync | does not trim or impose a length bound |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/archive` | browser safe collections, constants, schemas and errors |\n| `@vielzeug/archive/worker` | catalog factory, issuer signing key/session service binding and event types |\n| `@vielzeug/archive/auth` | issuer factory and binding types |\n\n## collections\n\n### `definearchivecollection`\n\ncreates a frozen collection and its contract; malformed names, versions or vocabularies throw `archivedefinitionerror`.\n\n```ts\nfunction definearchivecollection<\n p extends object,\n const c extends number,\n const f extends string = never,\n const t extends string = never,\n>(definition: archivecollectiondefinition<p, c, f, t>): archivecollection<p, c>;\n```\n\n| parameter | contract |\n| | |\n| `name` | lowercase `[a z0 9 ]{1,32}` |\n| `contentversion` | positive safe integer |\n| `payloadschema` | spell schema whose output is a stable json object |\n| `filters`, `tags` | optional readonly arrays; omitted means empty vocabulary |\n| `canonicalize` | optional pure, idempotent, schema valid normalization |\n| `index` | derive `filter`, `tags` and searchable `title` without mutating the payload |\n\n**returns:** `archivecollection<p, c>`.\n\n**example:** define and derive one collection without worker bindings.\n\n```ts\nimport { definearchivecollection } from '@vielzeug/archive';\nimport { s } from '@vielzeug/spell';\n\nconst recipes = definearchivecollection({\n name: 'recipes',\n contentversion: 1,\n payloadschema: s.object({ name: s.string().min(1).max(32) }),\n index: (payload) => ({ filter: null, tags: [], title: payload.name }),\n});\nconst derived = await recipes.derive({ name: 'rice' });\nif (derived === null) throw new error('invalid recipe');\nconsole.log(derived.contenthash);\n```\n\n| method / property | result |\n| | |\n| `derive(raw)` | `promise<archivederivation \\| null>` |\n| `contract.entry` | public entry schema |\n| `contract.ownedentry`, `ownedentries` | owner view with `clientkey` and `hidden` |\n| `contract.page` | entries and the forward cursor |\n| `contract.publishinput`, `updateinput` | publish and revision guarded update request schemas |\n| `filters`, `tags` | frozen vocabulary snapshots |\n\n`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`.\nschema output that is lossy or no longer json is a derivation fault, not invalid\nsubmitted content. invalid index result shapes also throw.\n\nconst generics preserve the content version literal and infer filter/tag unions from vocabulary arrays without `as const`. `noinfer` prevents `index` from widening those unions; omitted arrays infer `never`, permitting only `filter: null` and empty tags.\n\nobjects serialize with recursively sorted keys; arrays preserve order unless your canonicalizer changes it. sha 256 hashes the core owned `{contentversion, payload}` envelope. stored payloads are canonical by construction, so the emitted entry schemas embed the plain payload schema.\n\n`filter`, tags and searchable title are persisted projections, not computed during reads. changing `index` or its vocabulary does not update stored rows. the active definition and persisted dataset must remain consistent; archive has no projection update or historical schema api.\n\n## worker and maintenance\n\n### `createarchiveworker`\n\nreturns the catalog request/scheduled handler and its observation side channel.\n\n```ts\nfunction createarchiveworker<e extends archiveworkerenv>(\n collections: readonly archivecollection[],\n): archiveworkerhandler<e>;\n```\n\n**returns:** `archiveworkerhandler<e>`; duplicate names throw `archivedefinitionerror` at registration.\n\n**example:** export a deployable worker using the package's structural bindings.\n\n```ts\nimport { definearchivecollection } from '@vielzeug/archive';\nimport { createarchiveworker, type archiveworkerenv } from '@vielzeug/archive/worker';\nimport { s } from '@vielzeug/spell';\n\nconst notes = definearchivecollection({\n name: 'notes',\n contentversion: 1,\n payloadschema: s.object({ title: s.string().min(1).max(128) }),\n index: ({ title }) => ({ title, filter: null, tags: [] }),\n});\nexport default createarchiveworker<archiveworkerenv>([notes]);\n```\n\n| method | contract |\n| | |\n| `fetch(request, env, ctx)` | returns `promise<response>` |\n| `scheduled(controller, env, ctx)` | sweeps expired retry keys and revocation tombstones; emits failures and rethrows |\n| `tap(handler, { signal? }?)` | returns unsubscribe; a pre aborted signal registers nothing |\n\n`auth_issuer_service` is a required `fetch` shaped service binding to the issuer.\nthe catalog fetches signing keys from the configured `jwt_jwks_url` through that\nbinding, not through global `fetch`. the url still selects the issuer's jwks\nroute and must satisfy the environment's transport rules. key requests use get\nwithout a session cookie and do not follow redirects.\nthe catalog verifies the signed jwt locally, then asks the issuer to validate its\ncurrent account session version. jose caches signing keys; the catalog does not\ncache successful issuer session verdicts.\nif the service exchange fails or exceeds its fixed 5000 ms timeout (body decoding\nincluded), the catalog returns `503 auth unavailable`; the timeout does not cancel\nwork already started by the issuer.\n\nonly anonymous entry lists can use the 30 second edge cache. any `cookie` or `authorization` header disables caching, even if invalid or empty. detail, metadata, private and error responses are `no store`. public reads do not verify sessions.\n\n \n\n### `worker.tap`\n\nobserves typed request, scheduled and cache failure events without affecting request behavior.\n\n```ts\ntap(handler: (event: archiveevent) => void, options?: { readonly signal?: abortsignal }): () => void;\n```\n\n**returns:** an unsubscribe function; abort detaches the observer. diagnostics contain categories and trusted configuration field names, never raw errors, sql, subjects, aliases, tokens or urls.\n\n**example:** observe a worker and detach explicitly.\n\n```ts\nimport { createarchiveworker, type archiveworkerenv } from '@vielzeug/archive/worker';\n\nconst worker = createarchiveworker<archiveworkerenv>([]);\nconst stop = worker.tap((event) => console.log(json.stringify(event)));\nstop();\n```\n\n \n\n## identity issuer\n\n### `createauthworker`\n\nreturns the passwordless email issuer with account export and coordinated erasure.\n\n```ts\nfunction createauthworker<e extends authworkerenv>(): authworkerhandler<e>;\n```\n\n**returns:** `authworkerhandler<e>`.\n\n**example:** export an issuer configured through deployment bindings.\n\n```ts\nimport { createauthworker, type authworkerenv } from '@vielzeug/archive/auth';\n\nexport default createauthworker<authworkerenv>();\n```\n\n| method | contract |\n| | |\n| `fetch(request, env, ctx?)` | returns `promise<response>` |\n| `scheduled(controller, env, ctx?)` | removes expired links, closed send windows and unverified unreferenced accounts |\n| `tap(handler, { signal? }?)` | observes typed `authevent`; returns unsubscribe; pre aborted signals are inert |\n\nmagic 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.\n\nthe issuer global admission budget is durable d1 state in `auth_sign_in_budget`:\n20 requests per 60 second window before account creation or email delivery.\nthis supplements, rather than replaces, the durable per email budget. no external\nrate limiter binding is required.\nadmission spends are not refunded after later database or email failures.\nrequest emails are trimmed and lowercased before validation. `sender_address`\nmust already be canonical; `operator_emails` entries are trimmed and lowercased\nbefore validation.\n\nsign in requests create pending account rows with `lastsigninat: null`. successful\nredemption atomically claims the one use link and stamps the account's sign in\ntime. the sweep deletes expired links and closed per user send windows, then\nunverified accounts with neither remaining links nor send budget references.\nverified accounts are retained until explicit account deletion.\n\n`operator_session_max_age_seconds = 900` is part of the browser safe root\ncontract; `/auth` exports the factory and issuer binding types.\n\n`auth_jwt_previous_public_jwks` optionally publishes retired public rs256 keys and verifies sessions minted before rotation; private members reject configuration. the package has no demo sign in routes.\n\nopening a magic link performs a safe get that displays a click confirmation page,\nnot redemption. only a marked post from an exact allowed origin or\n`public_origin` consumes `{ token }` and returns 204 with the session cookie. the\nconfirmation page navigates to the fixed `${public_origin}/signed in` destination;\nyour application must serve that page because the issuer serves only the\nconfirmation endpoint. the post is not a redirect. confirmation responses are\n`no store` with no referrer and a restrictive csp; inline script and styles\nrequire a fresh response nonce, with no\n`unsafe inline`. the page removes the token from the address bar before rendering\nthe controls and loads no external assets. `app_name` supplies escaped page and\nemail branding; the confirmation has keyboard focus, live status announcements,\n44 pixel minimum action heights, a warm near white paper and copper light scheme,\na deep neutral dark scheme, serif headings, sans serif body text and reduced motion\nstyles. without javascript, it links to `${public_origin}/account` as “request a\nnew sign in link” but cannot redeem a link.\nget displays confirmation for any syntactically valid token, including a used\ntoken; only post establishes whether the link is still redeemable. post accepts\nexactly `{ token }` and rejects query parameters. with valid issuer configuration,\ninvalid or missing get tokens, duplicate token parameters and unsupported query\nparameters return http 400 with branded html recovery, not a json error. post\nerrors retain the json `archiveerrorresponse` schema and their http status.\n\nthe button announces pending sign in and blocks duplicate submissions. http 410\nreplaces it with a new link action; http 400 also disables resubmission. every\nfailure offers the fixed `${public_origin}/account` recovery link labelled\n“request a new sign in link”. like `/signed in`, `/account` is a fixed reference\nissuer convention: the application must serve its sign in entry there, not the issuer.\nrecovery is secondary while retry remains possible, and becomes the primary action\nfor invalid or expired links. it replaces the footer exit link to avoid duplicate\nnavigation. the initial “back to app” footer links to `${public_origin}/`.\nhttp 429 delays manual retry using unsigned integer seconds or a syntactically valid\nhttp date `retry after`, rounded up\nand bounded to 1 300 seconds (30 seconds for missing or invalid values). http 503\nand network failures offer explicit retry; no failure starts an automatic request.\nall returned response bodies, including discarded stale responses, are cancelled\nwithout delaying recovery; cancellation failures are ignored. each attempt aborts\nits controller on completion. stalled requests abort after 15 seconds with a\ntimeout specific message, distinct from connection failures. leaving the page aborts its request and\nclears timers; bfcache restoration resumes only the remaining cooldown, not a\nrequest. navigation after exact 204 success is fixed at\n`${public_origin}/signed in`; query input never controls either destination.\n\nthe issuer never logs directly. `tap` emits sanitized request and scheduled\noutcomes; observer failures are swallowed, abort detaches the observer and failed\nscheduled work emits then rethrows. `authdiagnostic` reports only a category.\n\n## http routes\n\nevery 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.\n\n| route | contract |\n| | |\n| `get /health` | public catalog readiness; 200 `ok` or 503 `degraded` |\n| `post /sign out` | catalog local sign out; `{ ok: true }` and expired session cookie, without issuer access |\n| `get /collections/:collection` | collection metadata |\n| `get /collections/:collection/entries` | `author`, `filter`, `tag`, `search`, `sort`, `cursor`, `limit`; forward entry page |\n| `post /collections/:collection/entries` | `{ clientkey, payload }`; 201 create or 200 replay |\n| `get /collections/:collection/entries/:id` | public entry; always `no store` |\n| `put /collections/:collection/entries/:id` | owner only `{ expectedrevision, payload }` |\n| `delete /collections/:collection/entries/:id` | owner or operator removal; 204 |\n| `put` / `delete /collections/:collection/entries/:id/like` | like / unlike visible entries only; guarded write and response; like state response |\n| `post /collections/:collection/entries/:id/report` | `{ reason }`; idempotent per entry/reporter; 204 |\n| `put` / `delete /collections/:collection/entries/:id/hidden` | operator hide / unhide; 204 |\n| `get /collections/:collection/reports` | operator page; `status=unresolved` (default) or `resolved`, `cursor`, `limit` |\n| `put /collections/:collection/reports/:id/resolved` | operator resolves report by its unique report id; idempotent 204 |\n| `delete /publishers/:id` | operator erases archive profile; not issuer account |\n| `get /moderation log` | operator forward page; `cursor`, `limit` |\n| `get` / `post` / `put /me` | identity, register with optional `{ name }`, rename with required `{ name }` |\n| `delete /me` | profile only cascade; 204; does not clear cookie |\n| `delete /me/account` | revoke issuer/subject and atomically erase profile; 204; coordinated issuer erasure seam |\n| `get /me/publications?collection=…` | owner entries including hidden state and retry keys |\n| `get /me/likes` | forward page `{ ids, cursor }`; `cursor`, `limit` |\n| `get /me/likes?ids=…` | subset lookup `{ ids }`; at most `likestateids` input ids |\n| `get /me/reports` | own forward report page across collections; `cursor`, `limit` |\n| `get /auth/health` | public issuer readiness; typed `authhealth`, 200 `ok` or 503 `degraded`; no email or peer probe |\n| `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 |\n| `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 |\n| `get /auth/jwks.json` | active and configured retired public verification keys |\n| `post /auth/sign in` | `{ email }` (trimmed and lowercased); 202 `{ ok: true }` |\n| `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 |\n| `post /auth/verify` | json `{ token }`, marker and exact allowed origin or `public_origin`; consume once; 204 with session cookie |\n| `get /auth/account` | `{ id, email, createdat, lastsigninat }`; no tokens or budget data |\n| `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 |\n\nforward 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`.\n\nfull erasure creates an issuer/subject revocation tombstone for 30 days plus ten\nseconds and deletes the profile in the same transaction. catalog authentication\nrejects that subject with 401 on ordinary private routes; registration also checks\nrevocation in sql to prevent resurrection races. only `delete /me/account` may\nbypass the issuer session lookup, and only to retry erasure with the original\nsigned session of the same principal while its existing revocation tombstone is\nvalid; jwt signature and claims are still verified. it is not a general\nstale session exception: initial erasure validates the issuer session. the\nissuer facing `delete /auth/account` always checks the current session version\nbefore calling the catalog, so retrying it after completed erasure returns 401\nbecause the account is absent.\ndirect profile only `delete /me` allows later registration using the same session.\nthe sweep removes expired tombstones. re enrollment through the reference issuer\ncreates a fresh account uuid subject; an external issuer must issue a new subject\nto re enroll during the revocation window.\n\nthe issuer stores `auth_users.session_version` as a safe non negative integer,\ndefaulting to `0`. every session jwt carries the current value as the required\n`sessionversion` claim. each ordinary issuer authenticated route compares that\nclaim to the current account row; a mismatch is unauthenticated. `post\n/api/v1/auth/sign out all` checks the captured issuer account uuid against the\nverified subject before atomically incrementing the version and deleting pending\nlinks. a changed account answers 409 `account changed`; a failed request does not\nclear the cookie. this revokes all sessions, including the caller's, after a\nsuccessful response. requests authorized before revocation may finish; there is\nno cross database transactional or in flight cancellation guarantee.\n\nthe catalog requires `auth_issuer_service`, a service binding to the issuer for\nboth jwks retrieval and live session validation. signing key retrieval uses\n`jwt_jwks_url` through the binding, so it does not depend on a public same zone\nworker to worker fetch. after local jwt signature/claims verification, it requests\n`get /api/v1/auth/session` through that binding on every private request. only a\nstrictly validated 401 `unauthenticated` remains a 401. malformed, unexpected,\nprovider and 5xx responses become 503 `auth unavailable`; there is no authority\ncache. both service directions use a fixed 5000 ms timeout that includes body\ndecoding. the issuer's `/api/v1/auth/session` is an internal\nservice contract, not a frontend session api.\n\nissuer readiness is independent of catalog readiness. `get\n/api/v1/auth/health` returns `archiveauthhealthschema` as `authhealth`; its\nread only probe checks configuration, a signing and verification key proof,\nbinding shapes, the singleton `auth_schema_meta` stamp against\n`auth_schema_version`, and readability of expected tables, columns and triggers.\nit does not write, send email or probe the catalog service. responses are\nsanitized.\nconfiguration failure, database failure and schema mismatch return http 503\nwith `status: 'degraded'` and the corresponding reason.\n\neffective hide, unhide, operator removal and publisher erasure append audit rows atomically with the state transition. concurrent idempotent no ops append nothing. audit rows contain opaque ids, not names or content, and have no foreign keys so they outlive erasure. resolving a report sets `resolvedat`; it is not one of the four audit action kinds.\n\n## constants and schemas\n\n| export | value or parsed output |\n| | |\n| `archive_contract_version`, `archive_schema_version` | both debut at `1`; wire and storage versions are independent |\n| `auth_schema_version` | `1`; issuer ddl stamp checked by `/auth/health` |\n| `archive_collection_name_pattern` | `/^[a z0 9 ]{1,32}$/` |\n| `archive_operator_role` | `'archive operator'` |\n| `session_cookie_name`, `session_cookie_path` | `'archive_session'`, `'/api/v1'` |\n| `session_max_age_seconds` | `2592000` |\n| `operator_session_max_age_seconds` | `900`; shared root contract |\n| `account_header` | `'x archive account'`; required issuer account uuid on coordinated erasure |\n| `publisher_header`, `request_marker_header` | `'x archive publisher'`, `'x archive request'` |\n| `ill_formed_text`, `iswellformedtext(value: string): boolean` | reject control characters and lone utf 16 surrogates |\n| `uuidschema`, `isotimestampschema`, `hex64schema` | uuid, iso timestamp, lowercase sha 256 hex strings |\n| `archivealiasschema` | trimmed, well formed alias of at most 24 characters |\n| `archiveauthorschema` | `{ id: string; name: string }` |\n| `archiveidentityschema` | `archiveidentity` |\n| `archiveaccountschema` | `archiveaccount`; issuer metadata, excluding private fields |\n| `archivelimitsschema` | all numeric `archive_limits` fields |\n| `archivehealthschema` | `archivehealth` |\n| `archiveauthhealthschema` | `authhealth` |\n| `archiveerrorresponseschema` | `archiveerrorresponse` |\n| `archiveokschema` | `{ ok: true }` |\n| `archivecollectionmetaschema` | `archivecollectionmeta` |\n| `archivelikedidsschema` | `{ ids: string[] }` subset response |\n| `archivelikespageschema` | `archivelikespage` |\n| `archivelikestateschema` | `{ id: string; likecount: number; liked: boolean }` |\n| `archivereportsschema`, `archiveownreportsschema` | `archivereports`, `archiveownreports` |\n| `archivemoderationlogschema` | `archivemoderationlog` |\n| `archivesortschema` | `archivesort` |\n| `archive_error_codes` | values of `archiveerrorcode` below |\n| `archive_moderation_actions` | `['hide', 'unhide', 'remove', 'erase publisher']` |\n\n### `sessioncookievalue`\n\ncomposes a session cookie using the shared security attributes.\n\n```ts\nfunction sessioncookievalue(token: string, options: { maxageseconds: number; secure: boolean }): string;\n```\n\n| parameter | contract |\n| | |\n| `token` | session token, or empty string when clearing |\n| `options.maxageseconds` | cookie lifetime; `0` clears it |\n| `options.secure` | include `secure` for production https |\n\n**returns:** the `set cookie` value with `httponly`, `samesite=strict`, the shared path and optional `secure`.\n\n**example:** clear the production cookie.\n\n```ts\nimport { sessioncookievalue } from '@vielzeug/archive';\n\nconst expiredcookie = sessioncookievalue('', { maxageseconds: 0, secure: true });\nconsole.log(expiredcookie);\n```\n\n \n\n### `iswellformedtext`\n\nchecks text for control characters and lone utf 16 surrogates.\n\n```ts\nfunction iswellformedtext(value: string): boolean;\n```\n\n**returns:** `true` when `ill_formed_text` does not match; it does not trim or impose bounds.\n\n**example:** validate text independently of the alias schema.\n\n```ts\nimport { iswellformedtext } from '@vielzeug/archive';\n\nconsole.log(iswellformedtext('rice'), iswellformedtext('\\u0000'));\n```\n\n### `archive_limits`\n\n`archive_limits` is frozen with these values:\n\n```ts\nconst limits = {\n aliaschars: 24,\n cursorchars: 1024,\n idempotencyretentionms: 172800000,\n likestateids: 50,\n maxpagesize: 50,\n maxpublications: 50,\n pagesize: 10,\n payloadbytes: 8192,\n publiccacheseconds: 30,\n reportreasonchars: 256,\n requestbytes: 12288,\n searchchars: 64,\n searchterms: 6,\n tagsperentry: 16,\n titlechars: 128,\n vocabchars: 64,\n};\n```\n\n## types\n\nthe following definitions use `schema` from `@vielzeug/spell`. schema derived aliases show their complete output shape.\n\n```ts\ninterface archiveindexing<f extends string = string, t extends string = string> {\n filter: f | null;\n tags: readonly t[];\n title: string;\n}\ninterface archivecollectiondefinition<\n p extends object,\n c extends number,\n f extends string = never,\n t extends string = never,\n> {\n canonicalize?(payload: p): p;\n contentversion: c;\n filters?: readonly f[];\n index(payload: p): archiveindexing<noinfer<f>, noinfer<t>>;\n name: string;\n payloadschema: schema<p, unknown>;\n tags?: readonly t[];\n}\ninterface archivederivation {\n canonicaljson: string;\n contenthash: string;\n contentversion: number;\n filter: string | null;\n tags: readonly string[];\n title: string;\n}\ninterface archiveentry<p = unknown, c extends number = number> {\n author: { id: string; name: string };\n contenthash: string;\n contentversion: c;\n id: string;\n likecount: number;\n payload: p;\n publishedat: string;\n revision: number;\n schemaversion: typeof archive_contract_version;\n updatedat: string;\n}\ninterface archiveownedentry<p = unknown, c extends number = number> extends archiveentry<p, c> {\n clientkey: string;\n hidden: boolean;\n}\ninterface archivecontract<p = unknown, c extends number = number> {\n entry: schema<archiveentry<p, c>>;\n ownedentries: schema<{ entries: archiveownedentry<p, c>[] }>;\n ownedentry: schema<archiveownedentry<p, c>>;\n page: schema<archivepage<archiveentry<p, c>>>;\n publishinput: schema<{ clientkey: string; payload: p }, unknown>;\n updateinput: schema<{ expectedrevision: number; payload: p }, unknown>;\n}\ninterface archivecollection<p extends object = object, c extends number = number> {\n readonly contentversion: c;\n readonly contract: archivecontract<p, c>;\n readonly derive: (raw: unknown) => promise<archivederivation | null>;\n readonly filters: readonly string[];\n readonly name: string;\n readonly tags: readonly string[];\n}\ninterface archivepage<t> {\n cursor: string | null;\n entries: readonly t[];\n}\ntype archivesort = 'liked' | 'recent';\ntype archivederivationstage = 'canonicalize' | 'hash' | 'index' | 'schema';\ntype archiveidentity = { id: string; name: string; operator: boolean } | null;\ntype archiveaccount = {\n id: string;\n email: string;\n createdat: string;\n lastsigninat: string | null;\n};\ntype archivecollectionmeta = { filters: string[]; name: string; tags: string[] };\ntype archivelikespage = { cursor: string | null; ids: string[] };\ntype archiveerrorcode =\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';\ntype archiveerrorresponse = {\n error: { code: archiveerrorcode; message: string; requestid: string };\n};\ntype archivehealth = {\n dbschemaversion: number | null;\n features: { likes: boolean; publishing: boolean };\n limits: {\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 };\n schemaversion: 1;\n status: 'ok' | 'degraded';\n reason: 'database unavailable' | 'schema mismatch' | null;\n};\ntype authhealth = {\n dbschemaversion: number | null;\n schemaversion: 1;\n status: 'ok' | 'degraded';\n reason: 'configuration unavailable' | 'database unavailable' | 'schema mismatch' | null;\n};\ntype archivereports = {\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};\ntype archiveownreports = {\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};\ntype archivemoderationaction = 'hide' | 'unhide' | 'remove' | 'erase publisher';\ntype archivemoderationlog = {\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};\n```\n\nworker subpath types model only the binding methods the catalog calls.\n\n```ts\ninterface 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}\ninterface d1like {\n batch(statements: d1statement[]): promise<{ meta?: { changes?: number } }[]>;\n prepare(sql: string): d1statement;\n}\ninterface ratelimiterbinding {\n limit(input: { key: string }): promise<{ success: boolean }>;\n}\ninterface archiveworkerenv {\n auth_issuer_service: authissuerservicebinding;\n allowed_origins: string;\n app_env: string;\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}\ninterface authissuerservicebinding {\n fetch(request: request): promise<response>;\n}\ninterface archiveexecutioncontext {\n waituntil(promise: promise<unknown>): void;\n}\ntype archivediagnostic =\n | { readonly kind: 'configuration'; readonly field: string }\n | { readonly kind: 'database' | 'record' | 'schema' | 'internal' | 'cache' | 'collection' };\ninterface archiverequestevent {\n code: archiveerrorcode | 'ok';\n diagnostic?: archivediagnostic;\n ms: number;\n requestid: string;\n route: string;\n status: number;\n type: 'request';\n}\ntype archivescheduledevent =\n | { removed: number; type: 'scheduled'; status: 'ok' }\n | { type: 'scheduled'; status: 'failed'; diagnostic: archivediagnostic };\ntype archiveevent =\n | archiverequestevent\n | archivescheduledevent\n | { readonly type: 'cache error'; readonly diagnostic: archivediagnostic };\ninterface archiveworkerhandler<e extends archiveworkerenv = archiveworkerenv> {\n fetch(request: request, env: e, ctx: archiveexecutioncontext): promise<response>;\n scheduled?(controller: unknown, env: e, ctx: archiveexecutioncontext): void | promise<void>;\n tap(handler: (event: archiveevent) => void, options?: { readonly signal?: abortsignal }): () => void;\n}\n```\n\nscheduled `removed` counts the combined expired retry key and revocation rows.\n\nauth subpath types describe the sibling issuer and its required service binding.\n\n```ts\ninterface sendemail {\n send(message: { from: string; html?: string; subject: string; text: string; to: string }): promise<unknown>;\n}\ninterface autharchivebinding {\n fetch(request: request): promise<response>;\n}\ninterface authworkerenv {\n allowed_origins: string;\n app_env: string;\n app_name: string;\n audience: string;\n auth_archive: autharchivebinding;\n auth_db: d1like;\n auth_email: sendemail;\n auth_issuer: string;\n auth_jwt_previous_public_jwks?: string;\n auth_jwt_private_jwk: string;\n operator_emails: string;\n public_origin: string;\n sender_address: string;\n}\ninterface authworkerhandler<e extends authworkerenv = authworkerenv> {\n fetch(request: request, env: e, ctx?: archiveexecutioncontext): promise<response>;\n scheduled(controller: unknown, env: e, ctx?: archiveexecutioncontext): promise<void>;\n tap(handler: (event: authevent) => void, options?: { readonly signal?: abortsignal }): () => void;\n}\ntype authdiagnostic = {\n readonly kind: 'configuration' | 'database' | 'schema' | 'email' | 'catalog' | 'internal';\n};\ntype authevent =\n | {\n readonly type: 'request';\n readonly requestid: string;\n readonly route:\n | '/auth/account'\n | '/auth/health'\n | '/auth/jwks.json'\n | '/auth/session'\n | '/auth/sign in'\n | '/auth/sign out all'\n | '/auth/verify'\n | 'unmatched';\n readonly status: number;\n readonly code: archiveerrorcode | 'ok';\n readonly ms: number;\n readonly diagnostic?: authdiagnostic;\n }\n | { readonly type: 'scheduled'; readonly status: 'ok'; readonly removed: number }\n | { readonly type: 'scheduled'; readonly status: 'failed'; readonly diagnostic: authdiagnostic };\n```\n\nissuer scheduled `removed` counts expired token rows, closed per user send windows\nand deleted unverified accounts. the singleton admission row is retained and its\nwindow resets atomically on a later sign in request.\n\n## errors\n\n| exported class | trigger | properties |\n| | | |\n| `archiveerror` | base for public package errors | `name`, `message`, optional `cause` |\n| `archivedefinitionerror` | invalid collection definition or duplicate registered collection names | extends `archiveerror` |\n| `archivederivationerror` | unexpected schema/canonicalization/index/hash fault or violated collection invariant | extends `archiveerror`; readonly `stage: archivederivationstage`; optional `cause` |\n\n`archiveerror` and `archivedefinitionerror` accept `(message: string, opts?: erroroptions)`.\n`archivederivationerror` accepts `(stage: archivederivationstage, message: string, opts?: erroroptions)`.\nits message and cause are local diagnostics, not response content. worker request\nfailures use the wire envelope, not these thrown classes. internal `apierror` and\n`authoperationerror` are not exported.\n\nstored entries that fail the current wire schema return 503 `record unavailable`.\nstored payloads are canonical by construction, so the emitted entry schemas embed\nthe plain payload schema: parsing a response re runs that schema, and an\nexception from one of its callbacks becomes a sanitized 500.\n\nthe complete public error signatures are:\n\n```ts\nclass archiveerror extends error {\n constructor(message: string, opts?: erroroptions);\n}\nclass archivedefinitionerror extends archiveerror {}\nclass archivederivationerror extends archiveerror {\n readonly stage: archivederivationstage;\n constructor(stage: archivederivationstage, message: string, opts?: erroroptions);\n}\n```\n\ncatalog rate limit responses use `retry after: 60`; configure catalog limiter periods to 60 seconds. issuer admission/send budget responses instead carry seconds until their durable window reopens. health's degraded response is the health dto, not an error envelope: schema mismatch reports the observed version; database unavailability reports `dbschemaversion: null`.\n",
8
8
  "usage": " \ntitle: 'archive: usage guide'\ndescription: 'define a collection, initialize fresh d1 databases, deploy the workers, and wire your client and observation policy.'\n \n\n[[toc]]\n\n## basic usage\n\na deployment is a collection definition plus one factory call. put the definition in a\nmodule both sides import — the browser needs the composed schemas, the worker needs\nthe collection itself.\n\n```ts\n// recipes collection.ts — the one content contract, shared by both sides.\nimport { definearchivecollection } from '@vielzeug/archive';\nimport { s } from '@vielzeug/spell';\n\nconst payloadschema = s.object({\n cuisine: s.enum(['italian', 'japanese']),\n ingredients: s.array(s.string().trim().min(1).max(64)).min(1).max(20),\n name: s.string().trim().min(1).max(32),\n});\n\nexport const recipescollection = definearchivecollection({\n // ingredient order is not identity: the canonical form sorts, so reordered\n // content hashes identically and updates as a no op.\n canonicalize: (payload) => ({ ...payload, ingredients: [...payload.ingredients].sort() }),\n contentversion: 1,\n filters: ['italian', 'japanese'],\n index: (payload) => ({\n filter: payload.cuisine,\n tags: [payload.ingredients.length <= 5 ? 'quick' : 'involved'],\n title: payload.name,\n }),\n name: 'recipes',\n payloadschema,\n tags: ['quick', 'involved'],\n});\n```\n\nthe worker entry imports the collection and exports the built handler:\n\n```ts\n// backend/src/index.ts — the whole deployment.\nimport { createarchiveworker } from '@vielzeug/archive/worker';\nimport { recipescollection } from './recipes collection';\n\nconst worker = createarchiveworker([recipescollection]);\n\n// one sanitized log line per request: ids, codes and route classes only.\nworker.tap((event) => console.log(json.stringify(event)));\n\nexport default worker;\n```\n\nthe browser imports the same collection for its schemas — never the worker subpath:\n\n```ts\nimport { recipescollection } from './recipes collection';\nimport { archiveidentityschema, publisher_header } from '@vielzeug/archive';\n\nconst response = await fetch('/api/v1/collections/recipes/entries');\nif (!response.ok) throw new error('archive unavailable');\nconst page = recipescollection.contract.page.parse(await response.json());\nconst meresponse = await fetch('/api/v1/me', { credentials: 'same origin' });\nif (!meresponse.ok) throw new error('identity unavailable');\nconst me = archiveidentityschema.parse(await meresponse.json());\nif (me === null) throw new error('register a publisher first');\nconst mineresponse = await fetch('/api/v1/me/publications?collection=recipes', {\n credentials: 'same origin',\n headers: { [publisher_header]: me.id },\n});\nif (!mineresponse.ok) throw new error('own publications unavailable');\nconst owned = recipescollection.contract.ownedentries.parse(await mineresponse.json());\nconsole.log(page.entries, owned.entries);\n```\n\n## the collection contract\n\n`definearchivecollection` is the only place content knowledge lives. the definition is\nvalidated at registration — a bad `name`, `contentversion`, or any vocabulary value\n(well formed text, 1..`vocabchars` chars) throws there, never mid request — and every\nordinary invalid content returns `null` → `422 invalid payload`. collection\nimplementation faults instead throw `archivederivationerror` → sanitized\n`500 internal`:\n\n **parse** — accept stable json objects only; schema outputs must remain json.\n **canonicalize** — optional; a result that leaves the payload schema throws, so a\n broken normalizer can never store invalid content.\n **index** — the returned `filter` must be declared in `filters`, every `tag` in\n `tags` (duplicates collapse), and the `title` is trimmed and bounded\n (`titlechars`, well formed text). a derivation outside the declared vocabulary\n would store a row no list query can reach — it throws instead.\n **size** — the canonical json must fit `payloadbytes`.\n **identity** — the content hash is sha 256 over `{contentversion, payload}`:\n identical content hashes identically, a real edit always re identifies, and the\n core owns the envelope so collections cannot drift it.\n\ndeclare vocabularies with readonly arrays (`['fire', 'ice']`) for automatic\nliteral union inference. const generics retain the version literal and vocabulary\nunions; `noinfer` prevents the index callback from widening them. omitted arrays\nmean empty vocabularies: return `null` for\nthe filter and no tags. object keys are recursively sorted by the core; schema\ntransformations must stabilize and `canonicalize` must be pure and idempotent.\nnon json input returns `null`. unexpected callback, schema or hashing failures,\nunstable transformations, non idempotent canonicalization and index mutation throw\n`archivederivationerror`; its `stage` identifies the failing phase.\nlossy or non json schema output and invalid index result shapes also throw.\n\n`contentversion` participates in content identity. request handling validates\nonly the current collection definition; it does not interpret historical schemas.\narchive provides no content upgrade apis: a new definition never reinterprets\nstored rows.\n\nfilters, tags and searchable titles are persisted data derived at publish/update\ntime. editing `index` or its vocabularies does not recompute existing projections.\nkeep the active definition and dataset consistent. for a development\ndeployment, deliberately reset and reseed a fresh database when you change payload\nor projection semantics; the package does not supply a migration framework.\nresetting destroys existing data. perform it only as an explicit, deliberate\ndevelopment operation, not automatically during startup or deployment.\n\n**payloads carry decisions, not content.** the payload schema should accept\nuser authored choices expressed as ids plus user written text — never the content's\nown copyrighted expression (card text, artwork, rules), never more than the game's\nown identifiers. for games with paid or dlc content this is the whole answer:\nownership resolves on each viewer's device against their own library, the service\nnever learns what anyone owns, and the strict schema is the enforcement seam —\nuse a strict schema that rejects unknown fields, so a collection that only models ids cannot\naccidentally start shipping paid content. for example, published game builds can\nreference cards, equipment and potions by id plus a user written name. the client\nrenders pieces the viewer does not own as unavailable. keep the browser safe\npublication contract in a shared domain module consumed by both the frontend and\nits backend, without importing frontend application code into the worker.\n\n## the worker and its environment\n\n`createarchiveworker(collections)` returns the full handler: anonymous public reads,\ncookie authenticated mutations behind exact origin and request marker guards, local\nrs256/jwks verification followed by an uncached issuer session version check,\nper ip read limits and per subject write limits keyed by peppered hmacs, the\n30 second anonymous cache on public entry lists only, operator moderation, and a\nscheduled expiry sweep for retired publish retry keys and revocation tombstones.\nhealth sits outside the rate limit and session pipelines: probes never burn the\nanonymous read budget or call the issuer.\n\nthe worker reads its environment fail closed — every binding must be present and\nwell formed, or the route answers 503 without naming what is missing:\n\n| variable | meaning |\n| | |\n| `app_env` | `local` or `production` (transport rules and the pepper policy) |\n| `allowed_origins` | exact canonical origins; never wildcards |\n| `jwt_issuer` / `jwt_audience` / `jwt_jwks_url` | exact issuer claim, audience and issuer jwks endpoint url |\n| `auth_issuer_service` | required issuer service binding for signing key retrieval and live session version checks |\n| `likes_enabled` / `publishing_enabled` | feature flags |\n| `rate_limit_key_secret` | hmac pepper. required ≥ 32 chars in production; local may fall back |\n| `db` / `read_limit` / `write_limit` | d1 + rate limit bindings |\n\nboth limiter bindings must run 60 second windows (`simple` limiters:\n`period: 60`): the platform binding does not expose its period at runtime, so the\nworker's 429 `retry after` header is fixed at 60 seconds — a longer window would\nmake the header lie.\n\ngenerated env types (wrangler's `worker configuration.d.ts`) satisfy the package's\nstructural `archiveworkerenv` — no `@cloudflare/workers types` dependency, no casts.\n\nsigning keys are fetched from `jwt_jwks_url` through `auth_issuer_service`, not\nglobal `fetch`. point the url at the bound issuer's `/api/v1/auth/jwks.json`\nroute; retain the https production or loopback http local configuration rules.\nthis transport supports workers on the same cloudflare zone without a public\nworker to worker fetch. jose caches signing keys, but every private request still\nchecks the current issuer session version. a signing key provider failure is\n`503 auth unavailable`, not a reason to clear the browser's signed in state.\n\n## the auth issuer\n\n`createauthworker()` from `@vielzeug/archive/auth` is the reference passwordless\nidentity issuer a deployment runs beside the catalog worker: one post with an email\nsends a single use magic link, and explicitly confirming it sets the `archive_session`\ncookie. the catalog verifies the jwt locally, then calls the issuer through\n`auth_issuer_service` to check the account's current session version. requests create\npending account rows; explicit link redemption atomically stamps the account's\nsign in time and claims the token. the issuer's d1 schema ships at\n`node_modules/@vielzeug/archive/migrations/auth/`; point the issuer's\n`migrations_dir` there. the catalog's `jwt_issuer` and `jwt_jwks_url` must identify\nthe issuer, and its audience must match the issuer's `audience`.\n\n| variable | meaning |\n| | |\n| `app_name` | the app's display name in magic link email and escaped confirmation page branding |\n| `allowed_origins` | exact origins allowed to trigger unsafe routes (the app origin(s)) |\n| `app_env` | `local` or `production` — production requires https and sets secure cookies |\n| `audience` / `auth_issuer` | the claims every minted session carries; the catalog verifies the same pair |\n| `auth_db` / `auth_email` | the issuer's d1 and send email bindings |\n| `auth_archive` | required service binding to the catalog worker; deletion calls its profile erasure route |\n| `auth_jwt_private_jwk` | the rs256 private key as a jwk json string: a wrangler secret in production |\n| `auth_jwt_previous_public_jwks` | optional retired public rs256 jwks; publish during rotation until old sessions expire |\n| `operator_emails` | comma separated emails whose newly minted sessions carry the operator role |\n| `public_origin` | app origin for links, confirmation post origin checks and client navigation after sign in |\n| `sender_address` | the address magic links come from: a domain onboarded to cloudflare email service |\n\nunder `wrangler dev` the send email binding is simulated — the link prints to the\nissuer's console, so that console is the inbox. demo only alice/bob routes are\ndeployment wrappers, not package apis.\n\nthe issuer first admits sign in requests through its singleton durable\n`auth_sign_in_budget` table: 20 requests per 60 seconds before creating accounts\nor delivering email. no rate limiter binding is required. it then\npermits three sign in emails per email in an hourly durable window.\nlater database or delivery failures do not refund global admission.\nusing or expiring a link never refunds sends; failed delivery also spends the\nallowance. links expire after 15 minutes. operator sessions expire after 900\nseconds, ordinary sessions after at most 30 days. retain retired public keys during\nrotation; the active private key signs every new session. sign in input trims and\nlowercases email before validation. `sender_address` must already be canonical;\n`operator_emails` entries are trimmed and lowercased before validation.\n\nthe scheduled sweep deletes expired links and closed per user send windows, then\npending accounts only when neither a link nor send budget row still references\nthem. failed delivery deletes the unusable token but retains the spent allowance;\nit does not immediately remove the pending account. verified accounts stay until\nexplicit erasure. the singleton admission row remains and resets its window on a\nlater admitted request.\n\nopening `get /auth/verify?token=…` renders a no store confirmation page and does\nnot consume the link or change the session, so email scanners cannot sign in by\nfetching it. a deliberate click sends `post /auth/verify` with json `{ token }`,\nthe request marker and an exact allowed origin or `public_origin`.\nsuccess is 204 with set cookie; the page then navigates to the fixed\n`${public_origin}/signed in` destination. your application must serve that page;\nthe issuer only serves the confirmation endpoint. the redemption update and\naccount sign in stamp are trigger backed and atomic.\nthe page removes the token from the address bar, loads no external assets, and\nsets a restrictive csp and `referrer policy: no referrer`. its inline css and\nscript use a fresh nonce, never `unsafe inline`. the page follows light/dark\npreferences, supports reduced motion, focuses the confirmation action and\nannounces pending, success and recovery states. without javascript it links to\n`${public_origin}/account` as “request a new sign in link” and does not pretend\nto complete sign in.\n\narchive only serves the confirmation endpoint. your application must serve its\nsign in entry at the fixed `${public_origin}/account` recovery destination and\nthe success page at `${public_origin}/signed in`. these are reference issuer\nroute conventions, not configurable destinations. the initial “back to app”\nexit links to `${public_origin}/`. no query parameter supplies a destination.\nexpired or already used links (post 410) require a new link rather than a retry.\nhttp 429 enables manual retry after a `retry after` cooldown bounded to 1 300\nseconds, accepting unsigned integer seconds or valid http date syntax and\ndefaulting to 30 seconds for missing or invalid values, including negative or\nfractional numbers. http 503 and network failures offer a retry button and a\nsecondary “request a new sign in link” action; invalid or expired links make\nthat recovery action primary. the page never\nautomatically retries, aborts stalled requests after 15 seconds and releases\nrequests/timers when you leave it. response bodies, including stale responses,\nare cancelled without waiting, and completed attempts abort their controllers.\n\nwith valid issuer configuration, malformed get links return http 400 with branded\nrecovery html rather than the json error envelope. post errors retain the json\nerror schema. a well formed expired token still gets get 200 confirmation: its\nredemption status is determined only by the deliberate post.\n\n`get /auth/account` exports account metadata only. for full erasure, call\n`delete /auth/account` with `account_header` (`x archive account`) set to the\nissuer account uuid captured from `get /auth/account` before confirmation.\nthe issuer rejects missing or invalid uuids with 400 and a different signed in\naccount with 409 `account changed`, before any erasure. this guard applies even\nbefore publisher registration and must stay pinned across retries.\nthe issuer forwards the caller's session and publisher\nprecondition through `auth_archive` to `delete /me/account`, requires exactly 204,\nthen deletes its\nown user and clears the cookie. failure preserves the session for retry. direct\n`delete /me` removes only the archive profile and leaves the cookie intact.\n\nseveral game frontends can share one issuer and catalog. keep the cookie\nhost only on the central api host, use `credentials: 'include'` from same site\nhttps game subdomains, and allow each exact frontend origin in both workers.\ndo not share cookies through a parent domain `domain` attribute. the\n[multi collection recipe](./examples/multi collection deployment.md) describes\nthe topology, session revalidation and platform wide account boundary.\n\n`delete /me/account` atomically revokes the issuer/subject and cascades profile\ndeletion. its tombstone lasts 30 days plus ten seconds; old sessions cannot read\nprivate data or register again, including a racing registration. the dedicated\naccount erasure route alone may bypass the issuer lookup, and only to retry with\nthe same principal's original signed session while its existing tombstone is\nvalid; signature and claims are still verified, and initial erasure validates the\nissuer. the issuer's `delete /auth/account` checks the current version before\nstarting erasure; after completed erasure a retry there returns 401. a fresh\nissuer account gets a new uuid subject. external issuers must use a new subject to\nre enroll during the revocation window. profile only deletion does not revoke the\nsubject and permits re registration with existing credentials.\n\neach jwt also carries the issuer account's required safe integer `sessionversion`.\nthe issuer compares it with `auth_users.session_version` on every ordinary\nauthenticated route. a jwt with an old version is unauthenticated even before its\nexpiry; the catalog asks the issuer on every private request and does not cache a\nsuccessful verdict. a valid local jwt with an unavailable, malformed, 5xx or\ntimed out (fixed 5000 ms, including body decoding) issuer response becomes\n`503 auth unavailable`, not anonymous identity.\n\n## the database\n\nthe package ships the canonical catalog migration:\n`node_modules/@vielzeug/archive/migrations/0001_community_archive.sql` — the whole schema,\nmoderation included, as one designed file. the runtime sql in the package is\nwritten against exactly this schema, and the package's own integration suite runs\nagainst the same file. point wrangler at the directory:\n\n```jsonc\n// wrangler.jsonc — the deployment owns its d1 database, not its ddl.\n{\n \"d1_databases\": [\n {\n \"binding\": \"db\",\n \"database_name\": \"my app\",\n \"migrations_dir\": \"./node_modules/@vielzeug/archive/migrations\",\n },\n ],\n}\n```\n\nthe issuer points to\n`node_modules/@vielzeug/archive/migrations/auth/0001_auth.sql`. both files are\nfresh database initialization definitions, not upgrades. the issuer ddl initializes\n`auth_users.session_version` to `0` and stamps `auth_schema_meta.applied` as\n`auth_schema_version` (`1`). neither file automatically deletes or resets data.\narchive's canonical initial schemas are edited directly. reset and reseed a\ndevelopment database deliberately when its schema or collection semantics no\nlonger match; reapplying the initial file does not update an existing dataset.\nthe initialization file also stamps the storage schema\n(`archive_schema_meta`) at `archive_schema_version`, and health reads it back: a\ndatabase with a mismatched stamp answers `status: 'degraded'` with the\nobserved `dbschemaversion` and http 503 (`reason: 'schema mismatch'`).\nan unavailable database produces http 503 with `dbschemaversion: null` and\n`reason: 'database unavailable'`. the debut storage stamp is `1`. a unit\ntest pins the shipped ddl's `check` bounds to `archive_limits`, so the two can never\ndrift silently. the stamp does not detect changes in your collection's payload\nschema, vocabulary or indexing function; you own that dataset consistency.\n\n## routes and wire names\n\n```\nget /api/v1/health\npost /api/v1/sign out\nget /api/v1/auth/health\nget /api/v1/auth/session (internal issuer check)\npost /api/v1/auth/sign out all (revoke every session)\nget /api/v1/collections/:collection (self description)\nget /api/v1/collections/:collection/entries (author, filter, tag, search, sort, cursor, limit)\npost /api/v1/collections/:collection/entries\nget /api/v1/collections/:collection/entries/:id\nput /api/v1/collections/:collection/entries/:id (expectedrevision)\ndelete /api/v1/collections/:collection/entries/:id\nput /api/v1/collections/:collection/entries/:id/like\ndelete /api/v1/collections/:collection/entries/:id/like\npost /api/v1/collections/:collection/entries/:id/report (reason)\nput /api/v1/collections/:collection/entries/:id/hidden (operator)\ndelete /api/v1/collections/:collection/entries/:id/hidden (operator)\nget /api/v1/collections/:collection/reports (operator: status, cursor, limit)\nput /api/v1/collections/:collection/reports/:id/resolved (operator: report id)\ndelete /api/v1/publishers/:id (operator: erase any publisher)\nget /api/v1/moderation log (operator: cursor, limit)\nget /api/v1/me post /me put /me delete /me\nget /api/v1/me/publications?collection=:collection get /api/v1/me/likes[?ids=…]\nget /api/v1/me/reports (cursor, limit)\nget /api/v1/auth/account\nget /api/v1/auth/jwks.json\npost /api/v1/auth/sign in (email)\ndelete /api/v1/auth/account (coordinated full erasure)\ndelete /api/v1/me/account (issuer erasure seam; revokes subject)\nget /api/v1/auth/verify?token=… (confirmation only)\npost /api/v1/auth/verify (guarded {token} redemption)\n```\n\npublishing is bounded by `maxpublications` live entries per `(collection, owner)`.\nthe quota check runs inside the guarded insert.\n\nevery list — entry pages, `/me/likes` without `ids`, `/me/reports`, operator\nreports and `/moderation log` — pages forward only: follow `cursor` until `null`,\npreserving query context; a cursor minted for another query is a 400. `limit`\ndefaults to `pagesize` and is at most `maxpagesize`; there is no total export\nbound. `/me/likes?ids=…` remains a subset lookup returning `{ ids }`, not a\npage, and cannot combine `ids` with `cursor` or `limit`.\n\nshared wire names live in the package — `archive_session` cookie,\n`x archive publisher` and `x archive request` headers, `archive_error_codes`, and\n`archive_limits` — so a client and the worker cannot drift. parse failures with the\ncollection's composed schemas; parse errors with `archiveerrorresponseschema`, the\nsign out body with `archiveokschema`, a collection's self description with\n`archivecollectionmetaschema` and the operator queue with `archivereportsschema`.\nevery entry's `schemaversion` is the archive contract's version, not the\ncollection's; the collection's own format version lives in its hash envelope and in\neach entry's `contentversion`.\n\nonly anonymous entry lists carry a 30 second edge cache contract\n(`publiccacheseconds`). detail is always `no store`. any cookie or authorization\nheader disables caching, regardless of validity; metadata, private and error\nresponses also use `no store`.\n\nimplement ui ownership checks in your application by comparing the current\npublisher id to the entry's `author.id`; there is no package action policy helper.\nserver mutations remain authoritative.\n\n`post /api/v1/sign out` is deliberately local: it clears only this browser's\ncookie and does not contact the issuer or revoke copied sessions. to revoke every\nsession, post to `/api/v1/auth/sign out all` with the mutation marker, an exact\nallowed `origin`, and the issuer account uuid captured from\n`get /api/v1/auth/account` in `x archive account`. the issuer returns 409\n`account changed` if the active subject differs. on success it atomically increments\n`session_version`, deletes outstanding links, preserves account and catalog data,\nand returns `{ ok: true }` with an expired cookie. requests authorized before\nrevocation may finish; there is no cross database cancellation guarantee.\n\n`get /api/v1/auth/health` is public and read only. it validates issuer\nconfiguration, key signing/verification, binding shapes, the `auth_schema_meta`\nstamp and expected tables, columns and triggers, returns `authhealth` through\n`archiveauthhealthschema`, and answers 503 `degraded` for configuration,\ndatabase or schema failures. it never writes, sends email or probes the catalog.\n\n## moderation\n\noperator authority is the session token's optional `roles` claim carrying\n`archive_operator_role` (`'archive operator'`) — minted by your issuer, verified with\nthe same signature, audience and issuer checks as any session, and never a separate\ncredential. moderation routes additionally require a registered publisher row: one\naccountable identity per action.\n\n **report** — any registered publisher reports a visible entry with a bounded\n free text reason (`reportreasonchars`). idempotent per (entry, reporter): the same\n report twice is the same 204. missing or hidden entries answer 404 — there is\n nothing left to report.\n **hide** — an operator removes an entry from every public surface (list, detail,\n likes) with a soft hide: the row, its likes and its retry key stay intact. the\n owner still sees the entry in `/me/publications` with `hidden: true`, edits and\n key replay are frozen (403), removal stays allowed, and unhiding restores\n everything. already hidden or already visible answer the same 204.\n **queue** — the operator pages newest reports with entry title and reporter\n identity; `status` is `unresolved` (default) or `resolved`. each report has its\n own `id` and nullable `resolvedat`. resolve it with\n `put /collections/:collection/reports/:id/resolved`; repeating is a 204 no op.\n **log** — every effective moderation action (hide, unhide, removal, publisher\n erasure) appends one accountability row in the same transaction that applies it,\n and `get /api/v1/moderation log` pages the newest actions to\n operators. rows carry only ids, the action kind and the time — no names (they\n change), no content — and no foreign keys: the audit must outlive the erasures it\n records. idempotent repeats (hiding an already hidden entry, erasing an\n already erased publisher) answer the same 204 with no new row, including\n concurrent repeats: audit insertion tests the effective state inside the batch.\n\nlike and unlike both require a visible entry. their sql guards visibility during\nthe mutation, and the response checks it again; hiding or deleting an entry\ncannot turn a racing unlike into a successful response for unavailable content.\n\n## observability\n\nthe core never logs. `worker.tap(handler, { signal? })` emits one `archiveevent` per\nrequest — `{ type: 'request', code, ms, requestid, route, status, diagnostic? }` —\nand successful sweep — `{ type: 'scheduled', status: 'ok', removed }`. failed\nsweeps emit `{ type: 'scheduled', status: 'failed', diagnostic }` and rethrow so\nthe runtime sees failure. cache failures emit `{ type: 'cache error', diagnostic }`.\nthe worker swallows observer errors so\nobservability cannot affect the worker. with no handlers registered there is zero\nwork. route is a finite class like `/collections/:collection/entries`, never a\nraw path; no event carries a url, cookie, subject, alias or token. diagnostics are\ntyped sanitized categories, not raw exceptions. a pre aborted tap signal is inert.\nthe sweep's `removed` counts expired retry keys and revocation tombstones together.\n\nthe issuer also exposes `tap()` with `authevent` from `@vielzeug/archive/auth`.\nit never logs directly. events contain finite route labels, codes, counts,\nduration and sanitized `authdiagnostic` categories, never emails, tokens or raw\nexceptions. scheduled failures emit then rethrow; pre aborted taps are inert.\nissuer sweep counts include expired tokens, closed per user send windows and\nremoved pending accounts; the singleton admission row is not removed.\n\n```ts\nimport { createlogger } from '@vielzeug/rune';\nimport { createarchiveworker } from '@vielzeug/archive/worker';\nimport { recipescollection } from './recipes collection';\n\nconst log = createlogger({ namespace: 'recipes' });\nconst worker = createarchiveworker([recipescollection]);\nworker.tap((event) => log.debug(event, `archive:${event.type}`));\nexport default worker;\n```\n\n## testing\n\nthe package proves itself against real workerd + local d1:\n\n```sh\npnpm vitest run packages/archive/src/__tests__/ # fast unit suite\npnpm filter @vielzeug/archive test:integration # real workerd + local d1 + issuer coverage\npnpm filter @vielzeug/archive test:types # strict types over src and the integration suite\n```\n\nprove your own collection the way the reference deployment does: a slim deployment\nsuite that publishes over your real vocabulary and asserts the derivation, plus the\npackage's generic suite for the core behavior. a collection with an `index` that can\ndrift out of vocabulary should unit test `derive` directly: ordinary invalid input\nreturns `null`, while implementation faults throw `archivederivationerror`.\ndirect ddl tests exercise the shipped schema rather than a duplicate schema.\n\n## framework integration\n\nthe root entry is plain typescript with no framework coupling; vue, react and svelte\napps all consume the composed schemas the same way. a vue 3 composable that pages a\ncollection:\n\n```ts\n// userecipes.ts — one collection's typed client in any component tree.\nimport { recipescollection } from './recipes collection';\nimport { ref, shallowref } from 'vue';\n\nexport function userecipes() {\n const entries = shallowref<returntype<typeof recipescollection.contract.entry.parse>[]>([]);\n const cursor = ref<string | null>(null);\n async function load(sort: 'liked' | 'recent') {\n const params = new urlsearchparams({ sort });\n if (cursor.value !== null) params.set('cursor', cursor.value);\n const response = await fetch(`/api/v1/collections/recipes/entries?${params}`);\n if (!response.ok) throw new error('the archive is unavailable.');\n const page = recipescollection.contract.page.parse(await response.json());\n entries.value = [...entries.value, ...page.entries];\n cursor.value = page.cursor;\n }\n return { cursor, entries, load };\n}\n```\n\n## working with other vielzeug libraries\n\n [spell](/spell/) — the payload schema a collection declares; the core composes the\n envelope schemas from it, so the wire and the types cannot drift.\n [courier](/courier/) — the http client shape an app's frontend mirrors to consume\n these routes: strict response schemas over one typed port.\n [rune](/rune/) — the structured logger a deployment points `tap()` events at.\n\n## best practices\n\n compose client schemas from `collection.contract`; never hand write the envelope.\n declare exactly the vocabulary your `index` derives, with union types where you\n have them; a value outside it rejects loudly.\n keep `canonicalize` pure and schema true: reordered lists in, canonical form out.\n initialize fresh databases with the shipped ddl and deliberately reset/reseed\n development data when collection or storage semantics change.\n treat `archive_error_codes` and `archiveerrorresponseschema` as one tested contract.\n pin the expected publisher header from the identity you captured at sign in, never from storage alone.\n grant the operator role claim only through your issuer, and only to accounts that\n are also registered publishers.\n wire a sanitized `tap()` sink and register the rate limit pepper as a production secret.\n",
9
9
  "examples": " \ntitle: \"archive: examples\"\ndescription: \"task oriented recipes for @vielzeug/archive, from first deployment to erasure.\"\n \n\n [stand up a content archive](./examples/stand up a content archive.md)\n [content identity and versions](./examples/content identity and versions.md)\n [multi collection deployment](./examples/multi collection deployment.md)\n [moderate content](./examples/moderate content.md)\n [client error mapping](./examples/client error mapping.md)\n [gdpr erasure and export](./examples/gdpr erasure and export.md)\n"
10
10
  },
@@ -575,8 +575,8 @@
575
575
  "description": "framework neutral two dimensional pointer drag and one axis pan recognition with lifecycle owned handles.",
576
576
  "docs": {
577
577
  "index": " \ntitle: 'gesture: pointer movement primitives'\ndescription: framework neutral two dimensional pointer drag and one axis pan recognition with lifecycle owned handles.\npackage: gesture\ncategory: input\nkeywords: [pointer, pan, swipe, gesture, touch, drag, long press]\nexports: [createdraggesture, createpangesture, createlongpress, gestureconfigerror]\nrelated: [refine, dnd, keymap]\nenvironments: [browser]\n \n\n<! markdownlint disable md025 md033 md060 >\n\n<packagehero package=\"gesture\" />\n\n## why gesture?\n\npointer driven interfaces need reliable one pointer movement tracking without coupling recognition to rendering, drag and drop semantics, or product specific completion policy.\n\n```ts\n// before\nelement.addeventlistener('pointermove', (event) => {\n // coordinate tracking, pointer identity, direction locking, and cleanup\n});\n\n// after\nimport { createpangesture } from '@vielzeug/gesture';\n\nconst pan = createpangesture(element, {\n axis: 'x',\n onmove: ({ distance }) => console.log('distance', distance),\n onend: ({ reason }) => console.log('ended', reason),\n});\n```\n\n| feature | gesture | ad hoc pointer handling |\n| | | |\n| bundle size | <packageinfo package=\"gesture\" type=\"size\" /> | n/a |\n| zero dependencies | <ore icon name=\"check\" size=\"16\"></ore icon> | n/a |\n| two dimensional drag tracking | built in | manual |\n| axis intent recognition | built in | manual |\n| pointer ownership | tracked across the document | manual |\n| lifecycle cleanup | `dispose()` + `disposalsignal` | manual |\n\n<div class=\"decision callout\">\n\n**use gesture when** ui surfaces need consistent free drag or axis locked pan tracking while retaining their own rendering and completion rules.\n\n**consider direct pointer handling when** the interaction is isolated and does not need reusable lifecycle or direction lock behavior.\n\n</div>\n\n## installation\n\n::: code group\n\n```sh [pnpm]\npnpm add @vielzeug/gesture\n```\n\n```sh [npm]\nnpm install @vielzeug/gesture\n```\n\n```sh [yarn]\nyarn add @vielzeug/gesture\n```\n\n:::\n\n## quick start\n\n```ts\nimport { createpangesture } from '@vielzeug/gesture';\n\nconst element = document.queryselector<htmlelement>('[data swipe]');\nif (!element) throw new error('missing [data swipe] element');\n\nconst pan = createpangesture(element, {\n axis: 'x',\n onmove: ({ distance }) => {\n element.style.transform = `translatex(${distance}px)`;\n },\n onend: ({ distance, reason }) => {\n element.style.transform = '';\n if (reason === 'release' && math.abs(distance) >= 48) {\n pan.dispose();\n element.remove();\n }\n },\n});\n\nwindow.addeventlistener('pagehide', () => pan.dispose(), { once: true });\n```\n\n## features\n\n<div class=\"features grid\">\n\n `createdraggesture()`: unrestricted two dimensional pointer movement tracking\n `createpangesture()`: one axis pointer movement with direction intent recognition\n `createlongpress()`: hold recognition with slop cancellation and click swallowing\n direction locking: activates only when movement favors the configured axis\n configurable pointer capture: own the pointer by default or preserve native targeting\n configurable `activationdistance`: tune slop for touch density and component needs\n consumer owned policy: completion thresholds, snapping, and outcomes stay in application code\n stable completion: one `onend` callback for release and cancellation\n lifecycle ownership: `dispose()`, `disposed`, `disposalsignal`, optional owner `signal`, and `[symbol.dispose]()`\n\n</div>\n\n## documentation\n\n<div class=\"doc links\">\n\n [usage guide](./usage.md)\n [api reference](./api.md)\n [examples](./examples.md)\n [migration guide](./migration.md)\n\n</div>\n\n## see also\n\n<div class=\"see also\">\n\n [refine](/refine/): components that use pan recognition for carousel, drawer, toast, and list interactions.\n [dnd](/dnd/): builds touch sorting on `createdraggesture()` and adds files, previews, drop targets, keyboard reordering, and connected list transactions.\n [keymap](/keymap/): keyboard interaction primitives for complementary input paths.\n\n</div>\n\n<! markdownlint enable md025 md033 md060 >\n",
578
- "api": " \ntitle: 'gesture: api reference'\ndescription: api reference for two dimensional drag and one axis pan recognition.\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution mode | common gotcha |\n| | | | |\n| `createdraggesture()` | track unrestricted two dimensional pointer movement | sync | does not provide dom movement or drop semantics |\n| `createpangesture()` | track pointer movement projected onto one axis | sync | cross axis intent ends a pending interaction |\n| `createlongpress()` | recognize a primary pointer held in place | sync | the click that follows a fired hold is swallowed |\n| `draggesture` / `pangesture` / `longpress` | control recognition lifecycle | sync | `dispose()` does not invoke `onend` |\n| `draggestureoptions` | configure free drag recognition | sync | `activationdistance` uses euclidean distance |\n| `pangestureoptions` | configure axis locked recognition | sync | completion thresholds belong in `onend` |\n| `longpressoptions` | configure hold recognition | sync | `durationms` and `sloppx` are validated at construction |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/gesture` | drag and pan factories, handles, callback details, options, axes, points, and end reasons |\n\n## core functions\n\n### `createdraggesture()`\n\n```ts\nfunction createdraggesture(target: element, options?: draggestureoptions): draggesture;\n```\n\ntracks one primary pointer without restricting movement to an axis.\n\n| parameter | type | description |\n| | | |\n| `target` | `element` | element that owns pointer admission and optional capture |\n| `options` | `draggestureoptions` | activation, admission, capture, callback, and lifecycle configuration |\n\n**returns:** a lifecycle owned `draggesture` handle.\n\n**example**\n\n```ts\nimport { createdraggesture } from '@vielzeug/gesture';\n\nconst drag = createdraggesture(element, {\n onmove: ({ delta }) => {\n element.style.translate = `${delta.x}px ${delta.y}px`;\n },\n onend: () => {\n element.style.translate = '';\n },\n});\n```\n\n \n\n### `createpangesture()`\n\n```ts\nfunction createpangesture(target: element, options?: pangestureoptions): pangesture;\n```\n\ntracks one primary pointer after movement establishes intent on the configured axis.\n\n| parameter | type | description |\n| | | |\n| `target` | `element` | element that owns pointer admission and optional capture |\n| `options` | `pangestureoptions` | axis, activation, admission, capture, callback, and lifecycle configuration |\n\n**returns:** a lifecycle owned `pangesture` handle.\n\n**example**\n\n```ts\nimport { createpangesture } from '@vielzeug/gesture';\n\nconst pan = createpangesture(element, {\n axis: 'x',\n onend: ({ distance, reason }) => {\n if (reason === 'release' && math.abs(distance) >= 48) dismiss();\n },\n});\n```\n\nboth handles expose the same members:\n\n| member | type | contract |\n| | | |\n| `active` | `boolean` | `true` after activation and before release or cancellation |\n| `cancel()` | `() => boolean` | clears pending recognition or ends active recognition with `reason: 'cancel'`; returns whether a session existed |\n| `disposalsignal` | `abortsignal` | aborts when the handle is disposed |\n| `dispose()` | `() => void` | aborts `disposalsignal`, clears recognition, releases capture, and detaches listeners; idempotent |\n| `disposed` | `boolean` | `true` after disposal |\n| `[symbol.dispose]()` | `() => void` | calls `dispose()` |\n\n \n\n### `createlongpress()`\n\n```ts\nfunction createlongpress(target: element, options?: longpressoptions): longpress;\n```\n\nrecognizes a primary pointer held in place: `onlongpress` fires with the originating pointerdown once the hold outlasts `durationms`.\n\n| parameter | type | description |\n| | | |\n| `target` | `element` | element that owns pointer admission, document tracking, and click swallowing |\n| `options` | `longpressoptions` | hold time, slop distance, admission, callback, and lifecycle configuration |\n\n**returns:** a lifecycle owned `longpress` handle with the same members as the other gestures; `active` is `true` while a hold is pending.\n\n**example**\n\n```ts\nimport { createlongpress } from '@vielzeug/gesture';\n\nconst hold = createlongpress(list, {\n shouldstart: (event) => event.pointertype !== 'mouse',\n onlongpress: (detail) => {\n const tile = (detail.event.target as htmlelement).closest('.tile');\n opendetail(tile?.dataset.id);\n },\n});\n```\n\nmovement beyond `sloppx` cancels back to a normal press, and window blur or a hidden document cancels a pending hold. the click that follows a fired hold is swallowed through a capture phase listener on `target`, so releasing never commits what the hold opened; bind `target` to a stable ancestor of the pressed controls so the swallow precedes their click handlers.\n\n## types\n\n### drag types\n\n```ts\ntype dragendreason = 'cancel' | 'release';\n\ntype dragpoint = readonly<{\n x: number;\n y: number;\n}>;\n\ntype draggesturedetail = readonly<{\n current: dragpoint;\n delta: dragpoint;\n event: pointerevent;\n pointerid: number;\n pointertype: string;\n start: dragpoint;\n target: element;\n}>;\n\ntype draggestureenddetail = draggesturedetail &\n readonly<{\n reason: dragendreason;\n }>;\n\ntype draggestureoptions = readonly<{\n activationdistance?: number;\n disabled?: boolean | (() => boolean | undefined);\n onend?: (detail: draggestureenddetail) => void;\n onmove?: (detail: draggesturedetail) => void;\n onstart?: (detail: draggesturedetail) => void;\n pointercapture?: boolean;\n shouldstart?: (event: pointerevent) => boolean;\n signal?: abortsignal;\n}>;\n```\n\n`start`, `current`, and `delta` use viewport `clientx` and `clienty` coordinates. activation uses `math.hypot(delta.x, delta.y)`.\n\n### pan types\n\n```ts\ntype panaxis = 'x' | 'y';\ntype panendreason = 'cancel' | 'release';\n\ntype pangesturedetail = readonly<{\n axis: panaxis;\n current: number;\n distance: number;\n event: pointerevent;\n pointerid: number;\n pointertype: string;\n start: number;\n target: element;\n}>;\n\ntype pangestureenddetail = pangesturedetail &\n readonly<{\n reason: panendreason;\n }>;\n\ntype pangestureoptions = readonly<{\n activationdistance?: number;\n axis?: panaxis | (() => panaxis);\n disabled?: boolean | (() => boolean | undefined);\n onend?: (detail: pangestureenddetail) => void;\n onmove?: (detail: pangesturedetail) => void;\n onstart?: (detail: pangesturedetail) => void;\n pointercapture?: boolean;\n shouldstart?: (event: pointerevent) => boolean;\n signal?: abortsignal;\n}>;\n```\n\npan details project `start`, `current`, and `distance` onto `axis`. the default axis is `'x'`.\n\n### long press types\n\n```ts\ntype longpressdetail = readonly<{\n event: pointerevent;\n pointerid: number;\n pointertype: string;\n target: element;\n}>;\n\ntype longpressoptions = readonly<{\n durationms?: number;\n disabled?: boolean | (() => boolean | undefined);\n onlongpress?: (detail: longpressdetail) => void;\n shouldstart?: (event: pointerevent) => boolean;\n sloppx?: number;\n signal?: abortsignal;\n}>;\n```\n\n`durationms` defaults to `500` and `sloppx` to `8`. `event` is the pointerdown that started the hold, so `event.target` is the pressed element.\n\n### handle types\n\n```ts\ntype draggesture = {\n readonly active: boolean;\n [symbol.dispose](): void;\n cancel(): boolean;\n readonly disposalsignal: abortsignal;\n dispose(): void;\n readonly disposed: boolean;\n};\n\ntype pangesture = draggesture;\n```\n\n### shared option behavior\n\n| option | default | contract |\n| | | |\n| `activationdistance` | `6` | non negative finite css pixels; drag uses euclidean distance, pan waits for dominant axis intent |\n| `axis` | `'x'` | pan only; fixed value or getter sampled when each interaction starts |\n| `disabled` | `false` | fixed value or getter; blocks starts and cancels active recognition on the next matching pointer event |\n| `pointercapture` | `true` | best effort capture after activation; document tracking continues when capture is disabled or fails |\n| `shouldstart` | n/a | admission predicate evaluated for the primary pointer before tracking starts |\n| `signal` | n/a | disposes the handle when the external owner aborts |\n| `onstart` | n/a | runs once on the activating movement |\n| `onmove` | n/a | runs on the activating movement and each later matching movement |\n| `onend` | n/a | runs once for active release or cancellation; pending interactions end without callbacks |\n\noptions and callback details are readonly. fixed callbacks and capture policy are snapshotted at construction. function valued `axis` and `disabled` options remain dynamic.\n\ngesture tracks accepted movement with capture phase listeners on the target's current owner document. window blur, hidden document transition, `pointercancel`, and `lostpointercapture` cancel active recognition.\n\n## errors\n\n| error | trigger | notable properties |\n| | | |\n| `gestureerror` | base class for every gesture originated error | `instanceof gestureerror` catches any gesture error |\n| `gestureconfigerror` | invalid option value | extends `gestureerror` |\n\n invalid `activationdistance` values throw `gestureconfigerror` during construction.\n invalid fixed `axis` values throw `gestureconfigerror` during construction.\n invalid values returned by a dynamic `axis` getter throw `gestureconfigerror` when a pointer interaction starts.\n",
579
- "usage": " \ntitle: 'gesture: usage guide'\ndescription: track unrestricted pointer dragging and axis locked pans with application owned rendering and completion.\n \n\n[[toc]]\n\n## basic usage\n\ncreate one handle for the element that owns the interaction. this example creates its surface, applies the required touch policy, and disposes the handle when the page unloads.\n\n```ts\nimport { createpangesture } from '@vielzeug/gesture';\n\nconst row = document.body.appendchild(document.createelement('div'));\nrow.textcontent = 'swipe to archive';\nrow.style.touchaction = 'pan y';\n\nconst pan = createpangesture(row, {\n axis: 'x',\n onmove: ({ distance }) => {\n row.style.transform = `translatex(${distance}px)`;\n },\n onend: ({ distance, reason }) => {\n row.style.transform = '';\n if (reason === 'release' && math.abs(distance) >= 64) {\n pan.dispose();\n row.remove();\n }\n },\n});\n\nwindow.addeventlistener('pagehide', () => pan.dispose(), { once: true });\n```\n\n## track two dimensional dragging\n\nuse `createdraggesture()` when movement must remain unrestricted in both dimensions:\n\n```ts\nimport { createdraggesture } from '@vielzeug/gesture';\n\nconst card = document.queryselector<htmlelement>('[data draggable]');\nif (!card) throw new error('missing [data draggable] element');\n\nconst drag = createdraggesture(card, {\n activationdistance: 6,\n onmove: ({ delta }) => {\n card.style.translate = `${delta.x}px ${delta.y}px`;\n },\n onend: () => {\n card.style.translate = '';\n },\n});\n\nwindow.addeventlistener('pagehide', () => drag.dispose(), { once: true });\n```\n\n`start`, `current`, and `delta` are readonly `{ x, y }` points. set `touch action: none` when the surface owns movement in both dimensions. drag recognition does not provide previews, drop targets, dom movement, or `datatransfer`; use dnd for those semantics.\n\n## recognize long presses\n\nuse `createlongpress()` for hold interactions: the touch counterpart of a keyboard preview key. `onlongpress` receives the originating pointerdown once the hold outlasts `durationms`:\n\n```ts\nimport { createlongpress } from '@vielzeug/gesture';\n\nconst hold = createlongpress(grid, {\n durationms: 500,\n shouldstart: (event) =>\n event.pointertype !== 'mouse' &&\n (event.target as htmlelement).closest('.tile') !== null,\n onlongpress: (detail) => {\n const tile = (detail.event.target as htmlelement).closest<htmlelement>('.tile');\n openpreview(tile?.dataset.id);\n },\n});\n```\n\nmovement beyond `sloppx` cancels back to a normal press, so scrolling never triggers the hold. the click that follows a fired hold is swallowed through a capture phase listener on `target`: bind `target` to a stable ancestor of the pressed controls so the swallow precedes their click handlers, and gate `shouldstart` to the elements the hold belongs to.\n\n## completion rules\n\ngesture reports movement and terminal state but does not decide what constitutes a swipe. apply thresholds and allowed directions in `onend`.\n\n```ts\nconst pan = createpangesture(panel, {\n axis: 'x',\n onend: ({ distance, reason }) => {\n if (reason === 'release' && distance <= 80) {\n opennext();\n } else {\n resetpanel();\n }\n },\n});\n```\n\n## direction recognition\n\nthe gesture remains pending during small movement. it activates only after movement favors the configured axis. cross axis movement ends the pending interaction without invoking callbacks.\n\nthe default `activationdistance` is 6 pixels. tune direction recognition slop for touch density or component specific needs:\n\n```ts\nconst pan = createpangesture(row, {\n axis: 'x',\n activationdistance: 12,\n onmove,\n onend,\n});\n```\n\nset `activationdistance: 0` when the first pointer move with non zero displacement should activate immediately. zero motion events remain pending.\n\nuse the corresponding `touch action` value so the browser retains native scrolling on the other axis.\n\n```css\n.swipe row {\n touch action: pan y;\n}\n```\n\n```ts\nconst pan = createpangesture(row, { axis: 'x', onmove });\n```\n\n## pointer capture\n\npointer capture is enabled by default. after axis intent is accepted, gesture attempts to capture the pointer while continuing to track movement through document level listeners. capture failure (for example after target detachment or pointer termination) falls back to document tracking without stranding the session.\n\ndisable capture when nested or newly revealed controls must retain native pointer up and click targeting:\n\n```ts\nconst pan = createpangesture(row, {\n axis: 'x',\n pointercapture: false,\n onmove: renderreveal,\n onend: settlereveal,\n});\n```\n\ndocument level tracking still keeps the pan active outside the target. disabling capture changes event targeting, not gesture tracking.\n\n## interactive descendants\n\nuse `shouldstart` when buttons, links, or form controls inside the surface must not start a pan.\n\n```ts\nconst pan = createpangesture(notification, {\n axis: 'x',\n pointercapture: false,\n shouldstart: (event) =>\n !event\n .composedpath()\n .some((node) => node instanceof element && node.matches('button, a, input, select, textarea')),\n onmove,\n onend,\n});\n```\n\n`shouldstart` protects controls under the initial pointer. `pointercapture: false` additionally protects controls that appear beneath the pointer during a reveal interaction.\n\n## disabled state\n\na boolean disables the recognizer permanently. a getter supports state that changes while the handle is alive.\n\n```ts\nconst pan = createpangesture(row, {\n disabled: () => islocked,\n onend: ({ reason }) => {\n if (reason === 'cancel') resetrow();\n },\n});\n```\n\nwhen the getter becomes `true`, the next pointer event cancels an active pan. window blur or a hidden owner document also cancels active tracking so a lost release cannot block later gestures.\n\ngesture snapshots fixed callbacks and capture policy at construction. use the `axis` and `disabled` functions for behavior that must remain dynamic instead of mutating the options object.\n\n## lifecycle\n\ndispose the target bound handle when its owning ui scope unmounts.\n\n```ts\nconst element = document.queryselector<htmlelement>('[data pan]');\nif (!element) throw new error('missing [data pan] element');\n\nconst owner = new abortcontroller();\nconst request = new abortcontroller();\nconst pan = createpangesture(element, {\n axis: 'x',\n signal: owner.signal,\n});\n\nconst childsignal = abortsignal.any([pan.disposalsignal, request.signal]);\nwindow.addeventlistener('pagehide', () => owner.abort(), { once: true });\n```\n\npass `{ signal }` to dispose with an existing owner signal. `disposalsignal` always aborts when gesture finishes teardown, so child work can compose with the handle regardless of how disposal starts.\n\nuse `cancel()` to stop a pending or active interaction without disposing the handle. an active interaction emits `onend` with `reason: 'cancel'`. call `cancel()` before `dispose()` when a connected surface needs its `onend` reset path; disposal itself does not invoke callbacks.\n\n## framework integration\n\ncreate the handle after the target element exists and dispose it on unmount.\n\n::: code group\n\n```tsx [react]\nimport { useeffect, useref } from 'react';\nimport { createpangesture } from '@vielzeug/gesture';\n\nfunction swiperow({ ondismiss }: { ondismiss: () => void }) {\n const rowref = useref<htmldivelement>(null);\n\n useeffect(() => {\n const row = rowref.current;\n if (!row) return;\n\n const pan = createpangesture(row, {\n axis: 'x',\n onmove: ({ distance }) => {\n row.style.transform = `translatex(${distance}px)`;\n },\n onend: ({ distance, reason }) => {\n row.style.transform = '';\n if (reason === 'release' && math.abs(distance) >= 64) ondismiss();\n },\n });\n\n return () => pan.dispose();\n }, [ondismiss]);\n\n return <div ref={rowref}>swipe me</div>;\n}\n```\n\n```vue [vue 3]\n<script setup lang=\"ts\">\nimport { onmounted, onunmounted, ref } from 'vue';\nimport { createpangesture, type pangesture } from '@vielzeug/gesture';\n\nconst emit = defineemits<{ dismiss: [] }>();\nconst rowel = ref<htmldivelement | null>(null);\nlet pan: pangesture | undefined;\n\nonmounted(() => {\n const row = rowel.value;\n if (!row) return;\n\n pan = createpangesture(row, {\n axis: 'x',\n onend: ({ distance, reason }) => {\n if (reason === 'release' && math.abs(distance) >= 64) emit('dismiss');\n },\n });\n});\n\nonunmounted(() => pan?.dispose());\n</script>\n\n<template>\n <div ref=\"rowel\">swipe me</div>\n</template>\n```\n\n```svelte [svelte]\n<script lang=\"ts\">\n import { onmount } from 'svelte';\n import { createpangesture } from '@vielzeug/gesture';\n\n let { ondismiss = () => {} }: { ondismiss: () => void } = $props();\n let rowel: htmldivelement;\n\n onmount(() => {\n const pan = createpangesture(rowel, {\n axis: 'x',\n onend: ({ distance, reason }) => {\n if (reason === 'release' && math.abs(distance) >= 64) ondismiss();\n },\n });\n\n return () => pan.dispose();\n });\n</script>\n\n<div bind:this={rowel}>swipe me</div>\n```\n\n:::\n\n## working with other vielzeug libraries\n\n### gesture + refine\n\nrefine uses gesture internally for carousel, drawer, toast, and list item pointer interactions. custom surfaces can use the same pan lifecycle while keeping visual state local.\n\n```ts\nimport { createpangesture } from '@vielzeug/gesture';\n\nconst pan = createpangesture(panel, {\n axis: 'x',\n onmove: ({ distance }) => {\n panel.style.transform = `translatex(${distance}px)`;\n },\n onend: ({ distance, reason }) => {\n panel.style.transform = '';\n if (reason === 'release' && math.abs(distance) >= 80) revealactions();\n },\n});\n```\n\n### gesture + dnd\n\ngesture owns pointer recognition: unrestricted two dimensional drag and one axis pan. dnd builds touch sorting on `createdraggesture()`, then owns `datatransfer`, previews, hit testing, file drop zones, keyboard sorting, dom reordering, and connected list transactions. use dnd when an item is picked up and dropped; use gesture directly when application code owns rendering and completion.\n\n## best practices\n\n **set** `touch action` for the axis the browser should continue scrolling.\n **use** `shouldstart` to exclude nested interactive controls.\n **disable** pointer capture when nested or newly revealed controls must keep native release targeting.\n **apply** completion thresholds and direction rules in `onend`; use `activationdistance` only for recognition slop.\n **treat** `reason: 'cancel'` as a reset path, never a commit path.\n **keep** `onmove` rendering lightweight.\n **dispose** the handle when its target leaves the ui.\n",
578
+ "api": " \ntitle: 'gesture: api reference'\ndescription: api reference for two dimensional drag and one axis pan recognition.\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution mode | common gotcha |\n| | | | |\n| `createdraggesture()` | track unrestricted two dimensional pointer movement | sync | does not provide dom movement or drop semantics |\n| `createpangesture()` | track pointer movement projected onto one axis | sync | cross axis intent ends a pending interaction |\n| `createlongpress()` | recognize a primary pointer held in place | sync | the click that follows a fired hold is swallowed |\n| `draggesture` / `pangesture` / `longpress` | control recognition lifecycle | sync | `dispose()` does not invoke `onend` |\n| `draggestureoptions` | configure free drag recognition | sync | `activationdistance` uses euclidean distance |\n| `pangestureoptions` | configure axis locked recognition | sync | completion thresholds belong in `onend` |\n| `longpressoptions` | configure hold recognition | sync | `durationms` and `sloppx` are validated at construction |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/gesture` | drag and pan factories, handles, callback details, options, axes, points, and end reasons |\n\n## core functions\n\n### `createdraggesture()`\n\n```ts\nfunction createdraggesture(target: element, options?: draggestureoptions): draggesture;\n```\n\ntracks one primary pointer without restricting movement to an axis.\n\n| parameter | type | description |\n| | | |\n| `target` | `element` | element that owns pointer admission and optional capture |\n| `options` | `draggestureoptions` | activation, admission, capture, callback, and lifecycle configuration |\n\n**returns:** a lifecycle owned `draggesture` handle.\n\n**example**\n\n```ts\nimport { createdraggesture } from '@vielzeug/gesture';\n\nconst drag = createdraggesture(element, {\n onmove: ({ delta }) => {\n element.style.translate = `${delta.x}px ${delta.y}px`;\n },\n onend: () => {\n element.style.translate = '';\n },\n});\n```\n\n \n\n### `createpangesture()`\n\n```ts\nfunction createpangesture(target: element, options?: pangestureoptions): pangesture;\n```\n\ntracks one primary pointer after movement establishes intent on the configured axis.\n\n| parameter | type | description |\n| | | |\n| `target` | `element` | element that owns pointer admission and optional capture |\n| `options` | `pangestureoptions` | axis, activation, admission, capture, callback, and lifecycle configuration |\n\n**returns:** a lifecycle owned `pangesture` handle.\n\n**example**\n\n```ts\nimport { createpangesture } from '@vielzeug/gesture';\n\nconst pan = createpangesture(element, {\n axis: 'x',\n onend: ({ distance, reason }) => {\n if (reason === 'release' && math.abs(distance) >= 48) dismiss();\n },\n});\n```\n\nboth handles expose the same members:\n\n| member | type | contract |\n| | | |\n| `active` | `boolean` | `true` after activation and before release or cancellation |\n| `cancel()` | `() => boolean` | clears pending recognition or ends active recognition with `reason: 'cancel'`; returns whether a session existed |\n| `disposalsignal` | `abortsignal` | aborts when the handle is disposed |\n| `dispose()` | `() => void` | aborts `disposalsignal`, clears recognition, releases capture, and detaches listeners; idempotent |\n| `disposed` | `boolean` | `true` after disposal |\n| `[symbol.dispose]()` | `() => void` | calls `dispose()` |\n\n \n\n### `createlongpress()`\n\n```ts\nfunction createlongpress(target: element, options?: longpressoptions): longpress;\n```\n\nrecognizes a primary pointer held in place: `onlongpress` fires with the originating pointerdown once the hold outlasts `durationms`.\n\n| parameter | type | description |\n| | | |\n| `target` | `element` | element that owns pointer admission, document tracking, and click swallowing |\n| `options` | `longpressoptions` | hold time, slop distance, admission, callback, and lifecycle configuration |\n\n**returns:** a lifecycle owned `longpress` handle with the same members as the other gestures; `active` is `true` while a hold is pending.\n\n**example**\n\n```ts\nimport { createlongpress } from '@vielzeug/gesture';\n\nconst hold = createlongpress(list, {\n shouldstart: (event) => event.pointertype !== 'mouse',\n onlongpress: (detail) => {\n const tile = (detail.event.target as htmlelement).closest('.tile');\n opendetail(tile?.dataset.id);\n },\n});\n```\n\nmovement beyond `sloppx` cancels back to a normal press, and window blur or a hidden document cancels a pending hold. the click that follows a fired hold is swallowed through a capture phase listener on `target`, so releasing never commits what the hold opened; bind `target` to a stable ancestor of the pressed controls so the swallow precedes their click handlers.\n\n## types\n\n### drag types\n\n```ts\ntype dragendreason = 'cancel' | 'release';\n\ntype dragpoint = readonly<{\n x: number;\n y: number;\n}>;\n\ntype draggesturedetail = readonly<{\n current: dragpoint;\n delta: dragpoint;\n event: pointerevent;\n pointerid: number;\n pointertype: string;\n start: dragpoint;\n target: element;\n}>;\n\ntype draggestureenddetail = draggesturedetail &\n readonly<{\n reason: dragendreason;\n }>;\n\ntype draggestureoptions = readonly<{\n activationdistance?: number;\n disabled?: boolean | (() => boolean | undefined);\n onend?: (detail: draggestureenddetail) => void;\n onmove?: (detail: draggesturedetail) => void;\n onstart?: (detail: draggesturedetail) => void;\n pointercapture?: boolean;\n shouldstart?: (event: pointerevent) => boolean;\n signal?: abortsignal;\n}>;\n```\n\n`start`, `current`, and `delta` use viewport `clientx` and `clienty` coordinates. activation uses `math.hypot(delta.x, delta.y)`.\n\n### pan types\n\n```ts\ntype panaxis = 'x' | 'y';\ntype panendreason = 'cancel' | 'release';\n\ntype pangesturedetail = readonly<{\n axis: panaxis;\n current: number;\n distance: number;\n event: pointerevent;\n pointerid: number;\n pointertype: string;\n start: number;\n target: element;\n}>;\n\ntype pangestureenddetail = pangesturedetail &\n readonly<{\n reason: panendreason;\n }>;\n\ntype pangestureoptions = readonly<{\n activationdistance?: number;\n axis?: panaxis | (() => panaxis);\n disabled?: boolean | (() => boolean | undefined);\n onend?: (detail: pangestureenddetail) => void;\n onmove?: (detail: pangesturedetail) => void;\n onstart?: (detail: pangesturedetail) => void;\n pointercapture?: boolean;\n shouldstart?: (event: pointerevent) => boolean;\n signal?: abortsignal;\n}>;\n```\n\npan details project `start`, `current`, and `distance` onto `axis`. the default axis is `'x'`.\n\n### long press types\n\n```ts\ntype longpressdetail = readonly<{\n event: pointerevent;\n pointerid: number;\n pointertype: string;\n target: element;\n}>;\n\ntype longpressoptions = readonly<{\n durationms?: number;\n disabled?: boolean | (() => boolean | undefined);\n onlongpress?: (detail: longpressdetail) => void;\n shouldstart?: (event: pointerevent) => boolean;\n sloppx?: number;\n signal?: abortsignal;\n}>;\n```\n\n`durationms` defaults to `500` and `sloppx` to `8`. `event` is the pointerdown that started the hold, so `event.target` is the pressed element.\n\n### handle types\n\n```ts\ntype draggesture = {\n readonly active: boolean;\n [symbol.dispose](): void;\n cancel(): boolean;\n readonly disposalsignal: abortsignal;\n dispose(): void;\n readonly disposed: boolean;\n};\n\ntype pangesture = draggesture;\n```\n\n### shared option behavior\n\n| option | default | contract |\n| | | |\n| `activationdistance` | `6` | non negative finite css pixels; drag uses euclidean distance, pan waits for dominant axis intent |\n| `axis` | `'x'` | pan only; fixed value or getter sampled when each interaction starts |\n| `disabled` | `false` | fixed value or getter; blocks starts and cancels active recognition on the next matching pointer event |\n| `pointercapture` | `true` | best effort capture after activation; document tracking continues when capture is disabled or fails |\n| `shouldstart` | n/a | admission predicate evaluated for the primary pointer before tracking starts |\n| `signal` | n/a | disposes the handle when the external owner aborts |\n| `onstart` | n/a | runs once on the activating movement |\n| `onmove` | n/a | runs on the activating movement and each later matching movement |\n| `onend` | n/a | runs once for active release or cancellation; pending interactions end without callbacks |\n\noptions and callback details are readonly. fixed callbacks and capture policy are snapshotted at construction. function valued `axis` and `disabled` options remain dynamic.\n\ngesture tracks accepted movement with capture phase listeners on the target's current owner document. window blur, hidden document transition, `pointercancel`, and pointer capture loss on the gesture target cancel active recognition. a `lostpointercapture` event bubbling from a descendant does not cancel the target's gesture.\n\n## errors\n\n| error | trigger | notable properties |\n| | | |\n| `gestureerror` | base class for every gesture originated error | `instanceof gestureerror` catches any gesture error |\n| `gestureconfigerror` | invalid option value | extends `gestureerror` |\n\n invalid `activationdistance` values throw `gestureconfigerror` during construction.\n invalid fixed `axis` values throw `gestureconfigerror` during construction.\n invalid values returned by a dynamic `axis` getter throw `gestureconfigerror` when a pointer interaction starts.\n",
579
+ "usage": " \ntitle: 'gesture: usage guide'\ndescription: track unrestricted pointer dragging and axis locked pans with application owned rendering and completion.\n \n\n[[toc]]\n\n## basic usage\n\ncreate one handle for the element that owns the interaction. this example creates its surface, applies the required touch policy, and disposes the handle when the page unloads.\n\n```ts\nimport { createpangesture } from '@vielzeug/gesture';\n\nconst row = document.body.appendchild(document.createelement('div'));\nrow.textcontent = 'swipe to archive';\nrow.style.touchaction = 'pan y';\n\nconst pan = createpangesture(row, {\n axis: 'x',\n onmove: ({ distance }) => {\n row.style.transform = `translatex(${distance}px)`;\n },\n onend: ({ distance, reason }) => {\n row.style.transform = '';\n if (reason === 'release' && math.abs(distance) >= 64) {\n pan.dispose();\n row.remove();\n }\n },\n});\n\nwindow.addeventlistener('pagehide', () => pan.dispose(), { once: true });\n```\n\n## track two dimensional dragging\n\nuse `createdraggesture()` when movement must remain unrestricted in both dimensions:\n\n```ts\nimport { createdraggesture } from '@vielzeug/gesture';\n\nconst card = document.queryselector<htmlelement>('[data draggable]');\nif (!card) throw new error('missing [data draggable] element');\n\nconst drag = createdraggesture(card, {\n activationdistance: 6,\n onmove: ({ delta }) => {\n card.style.translate = `${delta.x}px ${delta.y}px`;\n },\n onend: () => {\n card.style.translate = '';\n },\n});\n\nwindow.addeventlistener('pagehide', () => drag.dispose(), { once: true });\n```\n\n`start`, `current`, and `delta` are readonly `{ x, y }` points. set `touch action: none` when the surface owns movement in both dimensions. drag recognition does not provide previews, drop targets, dom movement, or `datatransfer`; use dnd for those semantics.\n\n## recognize long presses\n\nuse `createlongpress()` for hold interactions: the touch counterpart of a keyboard preview key. `onlongpress` receives the originating pointerdown once the hold outlasts `durationms`:\n\n```ts\nimport { createlongpress } from '@vielzeug/gesture';\n\nconst hold = createlongpress(grid, {\n durationms: 500,\n shouldstart: (event) =>\n event.pointertype !== 'mouse' &&\n (event.target as htmlelement).closest('.tile') !== null,\n onlongpress: (detail) => {\n const tile = (detail.event.target as htmlelement).closest<htmlelement>('.tile');\n openpreview(tile?.dataset.id);\n },\n});\n```\n\nmovement beyond `sloppx` cancels back to a normal press, so scrolling never triggers the hold. the click that follows a fired hold is swallowed through a capture phase listener on `target`: bind `target` to a stable ancestor of the pressed controls so the swallow precedes their click handlers, and gate `shouldstart` to the elements the hold belongs to.\n\n## completion rules\n\ngesture reports movement and terminal state but does not decide what constitutes a swipe. apply thresholds and allowed directions in `onend`.\n\n```ts\nconst pan = createpangesture(panel, {\n axis: 'x',\n onend: ({ distance, reason }) => {\n if (reason === 'release' && distance <= 80) {\n opennext();\n } else {\n resetpanel();\n }\n },\n});\n```\n\n## direction recognition\n\nthe gesture remains pending during small movement. it activates only after movement favors the configured axis. cross axis movement ends the pending interaction without invoking callbacks.\n\nthe default `activationdistance` is 6 pixels. tune direction recognition slop for touch density or component specific needs:\n\n```ts\nconst pan = createpangesture(row, {\n axis: 'x',\n activationdistance: 12,\n onmove,\n onend,\n});\n```\n\nset `activationdistance: 0` when the first pointer move with non zero displacement should activate immediately. zero motion events remain pending.\n\nuse the corresponding `touch action` value so the browser retains native scrolling on the other axis.\n\n```css\n.swipe row {\n touch action: pan y;\n}\n```\n\n```ts\nconst pan = createpangesture(row, { axis: 'x', onmove });\n```\n\n## pointer capture\n\npointer capture is enabled by default. after axis intent is accepted, gesture attempts to capture the pointer while continuing to track movement through document level listeners. capture failure (for example after target detachment or pointer termination) falls back to document tracking without stranding the session.\n\ndisable capture when nested or newly revealed controls must retain native pointer up and click targeting:\n\n```ts\nconst pan = createpangesture(row, {\n axis: 'x',\n pointercapture: false,\n onmove: renderreveal,\n onend: settlereveal,\n});\n```\n\ndocument level tracking still keeps the pan active outside the target. disabling capture changes event targeting, not gesture tracking.\n\n## interactive descendants\n\nuse `shouldstart` when buttons, links, or form controls inside the surface must not start a pan.\n\n```ts\nconst pan = createpangesture(notification, {\n axis: 'x',\n pointercapture: false,\n shouldstart: (event) =>\n !event\n .composedpath()\n .some((node) => node instanceof element && node.matches('button, a, input, select, textarea')),\n onmove,\n onend,\n});\n```\n\n`shouldstart` protects controls under the initial pointer. `pointercapture: false` additionally protects controls that appear beneath the pointer during a reveal interaction.\n\n## disabled state\n\na boolean disables the recognizer permanently. a getter supports state that changes while the handle is alive.\n\n```ts\nconst pan = createpangesture(row, {\n disabled: () => islocked,\n onend: ({ reason }) => {\n if (reason === 'cancel') resetrow();\n },\n});\n```\n\nwhen the getter becomes `true`, the next pointer event cancels an active pan. window blur or a hidden owner document also cancels active tracking so a lost release cannot block later gestures.\n\ngesture snapshots fixed callbacks and capture policy at construction. use the `axis` and `disabled` functions for behavior that must remain dynamic instead of mutating the options object.\n\n## lifecycle\n\ndispose the target bound handle when its owning ui scope unmounts.\n\n```ts\nconst element = document.queryselector<htmlelement>('[data pan]');\nif (!element) throw new error('missing [data pan] element');\n\nconst owner = new abortcontroller();\nconst request = new abortcontroller();\nconst pan = createpangesture(element, {\n axis: 'x',\n signal: owner.signal,\n});\n\nconst childsignal = abortsignal.any([pan.disposalsignal, request.signal]);\nwindow.addeventlistener('pagehide', () => owner.abort(), { once: true });\n```\n\npass `{ signal }` to dispose with an existing owner signal. `disposalsignal` always aborts when gesture finishes teardown, so child work can compose with the handle regardless of how disposal starts.\n\nuse `cancel()` to stop a pending or active interaction without disposing the handle. an active interaction emits `onend` with `reason: 'cancel'`. call `cancel()` before `dispose()` when a connected surface needs its `onend` reset path; disposal itself does not invoke callbacks.\n\n## framework integration\n\ncreate the handle after the target element exists and dispose it on unmount.\n\n::: code group\n\n```tsx [react]\nimport { useeffect, useref } from 'react';\nimport { createpangesture } from '@vielzeug/gesture';\n\nfunction swiperow({ ondismiss }: { ondismiss: () => void }) {\n const rowref = useref<htmldivelement>(null);\n\n useeffect(() => {\n const row = rowref.current;\n if (!row) return;\n\n const pan = createpangesture(row, {\n axis: 'x',\n onmove: ({ distance }) => {\n row.style.transform = `translatex(${distance}px)`;\n },\n onend: ({ distance, reason }) => {\n row.style.transform = '';\n if (reason === 'release' && math.abs(distance) >= 64) ondismiss();\n },\n });\n\n return () => pan.dispose();\n }, [ondismiss]);\n\n return <div ref={rowref}>swipe me</div>;\n}\n```\n\n```vue [vue 3]\n<script setup lang=\"ts\">\nimport { onmounted, onunmounted, ref } from 'vue';\nimport { createpangesture, type pangesture } from '@vielzeug/gesture';\n\nconst emit = defineemits<{ dismiss: [] }>();\nconst rowel = ref<htmldivelement | null>(null);\nlet pan: pangesture | undefined;\n\nonmounted(() => {\n const row = rowel.value;\n if (!row) return;\n\n pan = createpangesture(row, {\n axis: 'x',\n onend: ({ distance, reason }) => {\n if (reason === 'release' && math.abs(distance) >= 64) emit('dismiss');\n },\n });\n});\n\nonunmounted(() => pan?.dispose());\n</script>\n\n<template>\n <div ref=\"rowel\">swipe me</div>\n</template>\n```\n\n```svelte [svelte]\n<script lang=\"ts\">\n import { onmount } from 'svelte';\n import { createpangesture } from '@vielzeug/gesture';\n\n let { ondismiss = () => {} }: { ondismiss: () => void } = $props();\n let rowel: htmldivelement;\n\n onmount(() => {\n const pan = createpangesture(rowel, {\n axis: 'x',\n onend: ({ distance, reason }) => {\n if (reason === 'release' && math.abs(distance) >= 64) ondismiss();\n },\n });\n\n return () => pan.dispose();\n });\n</script>\n\n<div bind:this={rowel}>swipe me</div>\n```\n\n:::\n\n## working with other vielzeug libraries\n\n### gesture + refine\n\nrefine uses gesture internally for carousel, drawer, toast, and list item pointer interactions. custom surfaces can use the same pan lifecycle while keeping visual state local.\n\n```ts\nimport { createpangesture } from '@vielzeug/gesture';\n\nconst pan = createpangesture(panel, {\n axis: 'x',\n onmove: ({ distance }) => {\n panel.style.transform = `translatex(${distance}px)`;\n },\n onend: ({ distance, reason }) => {\n panel.style.transform = '';\n if (reason === 'release' && math.abs(distance) >= 80) revealactions();\n },\n});\n```\n\n### gesture + dnd\n\ngesture owns pointer recognition: unrestricted two dimensional drag and one axis pan. dnd builds touch sorting on `createdraggesture()`, then owns `datatransfer`, previews, hit testing, file drop zones, keyboard sorting, dom reordering, and connected list transactions. use dnd when an item is picked up and dropped; use gesture directly when application code owns rendering and completion.\n\n## best practices\n\n **set** `touch action` for the axis the browser should continue scrolling.\n **use** `shouldstart` to exclude nested interactive controls.\n **disable** pointer capture when nested or newly revealed controls must keep native release targeting.\n **apply** completion thresholds and direction rules in `onend`; use `activationdistance` only for recognition slop.\n **treat** `reason: 'cancel'` as a reset path, never a commit path. pointer capture loss on the gesture target cancels; capture loss bubbling from a descendant does not.\n **keep** `onmove` rendering lightweight.\n **dispose** the handle when its target leaves the ui.\n",
580
580
  "examples": " \ntitle: 'gesture: examples'\ndescription: worked examples for @vielzeug/gesture.\n \n\n## examples\n\n [carousel pan navigation](./examples/carousel swipe navigation.md)\n [swipe to dismiss notifications](./examples/swipe dismiss notifications.md)\n"
581
581
  },
582
582
  "examples": [
@@ -1026,9 +1026,9 @@
1026
1026
  "category": "ui components",
1027
1027
  "description": "accessible, themeable web components built with ore for framework and vanilla dom apps.",
1028
1028
  "docs": {
1029
- "index": " \ntitle: 'refine: web component library'\ndescription: accessible, themeable web components built with ore for framework and vanilla dom apps.\npackage: refine\ncategory: ui components\nkeywords: [web components, accessible, themeable, ui, components, design system]\nrelated: [ore, orbit, forge, keymap]\nexports:\n [\n ore accordion,\n ore accordion item,\n ore alert,\n ore async,\n ore avatar,\n ore avatar group,\n ore badge,\n ore box,\n ore breadcrumb,\n ore breadcrumb item,\n ore button,\n ore button group,\n ore calendar,\n ore card,\n ore carousel,\n ore carousel slide,\n ore chat message,\n ore checkbox,\n ore checkbox group,\n ore chip,\n ore code window,\n ore column,\n ore combobox,\n ore combobox option,\n ore command palette,\n ore command palette item,\n ore cookie banner,\n ore copy command,\n ore counter,\n ore datagrid,\n ore date picker,\n ore dialog,\n ore drawer,\n ore file input,\n ore grid,\n ore grid item,\n ore icon,\n ore input,\n ore keyboard key,\n ore keyboard shortcut,\n ore list,\n ore list item,\n ore marquee,\n ore menu,\n ore menu item,\n ore menu separator,\n ore message composer,\n ore navbar,\n ore navbar item,\n ore navigation menu,\n ore navigation menu item,\n ore navigation menu panel,\n ore number input,\n ore otp input,\n ore pagination,\n ore password strength,\n ore popover,\n ore progress,\n ore qr code,\n ore qr scanner,\n ore query bar,\n ore radio,\n ore radio group,\n ore rating,\n ore select,\n ore separator,\n ore sidebar,\n ore sidebar group,\n ore sidebar item,\n ore skeleton,\n ore slider,\n ore speech player,\n ore stats,\n ore step,\n ore stepper,\n ore switch,\n ore tab item,\n ore tab panel,\n ore table,\n ore tabs,\n ore td,\n ore text,\n ore th,\n ore tr,\n ore textarea,\n ore time picker,\n ore toast,\n ore tooltip,\n ore typing indicator,\n ]\nenvironments: [browser]\n \n\n<! markdownlint disable md025 md033 md060 >\n\n<packagehero package=\"refine\" />\n\n## why refine?\n\nevery project needs ui primitives. refine provides accessible web components that work natively anywhere html is rendered, no framework required.\n\n```html\n<! before: roll your own button with aria >\n<button class=\"btn btn primary\" role=\"button\" aria pressed=\"false\" tabindex=\"0\">\n <span class=\"btn spinner\" aria hidden=\"true\"></span>\n save\n</button>\n\n<! after: refine >\n<ore button variant=\"solid\" color=\"primary\" loading>save</ore button>\n```\n\n| feature | refine | shoelace | material web |\n| | | | |\n| bundle size | <packageinfo package=\"refine\" type=\"size\" /> | ~145 kb | ~200 kb |\n| built with | ore | lit | lit |\n| runtime dependencies | vielzeug ui primitives + lucide | lit + utilities | lit + material foundations |\n| accessible | wcag aa | wcag aa | wcag aa |\n| framework agnostic | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"check\" size=\"16\"></ore icon> |\n\n<div class=\"decision callout\">\n\n**use refine when** you want accessible web components that match the vielzeug design system without a heavy framework dependency.\n\n**consider shoelace or material web** if your team is already standardized on those ecosystems and you need their established component catalogs.\n\n</div>\n\n## installation\n\n::: code group\n\n```sh [pnpm]\npnpm add @vielzeug/refine\n```\n\n```sh [npm]\nnpm install @vielzeug/refine\n```\n\n```sh [yarn]\nyarn add @vielzeug/refine\n```\n\n:::\n\n## quick start\n\n```ts\n// 1. import global styles once\nimport '@vielzeug/refine/fouc.css'; // hide unupgraded custom elements until first paint\nimport '@vielzeug/refine/tokens.css'; // tokens, animations, cascade layers\n\n// 2. register only the elements you need\nimport '@vielzeug/refine/button';\nimport '@vielzeug/refine/input';\nimport '@vielzeug/refine/card';\n```\n\n```html\n<ore button variant=\"solid\" color=\"primary\">save</ore button>\n<ore input label=\"email\" type=\"email\" required></ore input>\n<ore card padding=\"lg\">\n <span slot=\"header\">account</span>\n <p>card content goes here.</p>\n</ore card>\n```\n\n```ts\n```\n\n### cdn / vanilla html\n\nuse the self contained iife bundle to load refine directly from a cdn in any html page: no build step required:\n\n```html\n<! 1. styles >\n<link rel=\"stylesheet\" href=\"https://unpkg.com/@vielzeug/refine/dist/styles/fouc.css\" />\n<link rel=\"stylesheet\" href=\"https://unpkg.com/@vielzeug/refine/dist/styles/tokens.css\" />\n\n<! 2. all components (iife: registers global refine namespace) >\n<script src=\"https://unpkg.com/@vielzeug/refine/dist/refine.iife.js\"></script>\n```\n\nfor bundler based projects that still want a cdn url, use the esm bundle via an import map:\n\n```html\n<script type=\"importmap\">\n {\n \"imports\": {\n \"@vielzeug/refine\": \"https://esm.sh/@vielzeug/refine\",\n \"@vielzeug/refine/button\": \"https://esm.sh/@vielzeug/refine/button\",\n \"@vielzeug/refine/input\": \"https://esm.sh/@vielzeug/refine/input\"\n }\n }\n</script>\n\n<script type=\"module\">\n import '@vielzeug/refine/button';\n import '@vielzeug/refine/input';\n</script>\n```\n\n### package entry points\n\n| import | purpose |\n| | |\n| `@vielzeug/refine/fouc.css` | fouc suppression for unupgraded custom elements |\n| `@vielzeug/refine/tokens.css` | global design tokens and cascade layers |\n| `@vielzeug/refine/styles/preflight.css` | optional browser default reset (includes fouc suppression) |\n\ncomponent registration happens through side effect imports such as `@vielzeug/refine/button` and `@vielzeug/refine/dialog`.\n\n### components\n\n**content:** `ore avatar`, `ore avatar group`, `ore breadcrumb`, `ore card`, `ore carousel`, `ore carousel slide`, `ore chat message`, `ore icon`, `ore keyboard key`, `ore keyboard shortcut`, `ore list`, `ore list item`, `ore marquee`, `ore pagination`, `ore qr code`, `ore separator`, `ore stats`, `ore step`, `ore stepper`, `ore table`, `ore text`\n\n**disclosure:** `ore accordion`, `ore accordion item`, `ore tabs`, `ore tab item`, `ore tab panel`\n\n**feedback:** `ore alert`, `ore async`, `ore badge`, `ore chip`, `ore cookie banner`, `ore password strength`, `ore progress`, `ore skeleton`, `ore toast`, `ore typing indicator`\n\n**inputs:** `ore button`, `ore button group`, `ore calendar`, `ore checkbox`, `ore checkbox group`, `ore column`, `ore combobox`, `ore counter`, `ore datagrid`, `ore date picker`, `ore file input`, `ore input`, `ore message composer`, `ore number input`, `ore otp input`, `ore qr scanner`, `ore query bar`, `ore radio`, `ore radio group`, `ore rating`, `ore select`, `ore slider`, `ore switch`, `ore textarea`, `ore time picker`\n\n**layout:** `ore box`, `ore grid`, `ore grid item`, `ore navbar`, `ore sidebar`\n\n**overlay:** `ore command palette`, `ore command palette item`, `ore dialog`, `ore drawer`, `ore menu`, `ore popover`, `ore tooltip`\n\n## component portfolio\n\nrefine keeps one public package and classifies components by composition level rather than stability. all listed components remain supported and tested.\n\n **foundations**: buttons, inputs, selection controls, disclosure, dialogs, menus, tooltips, layout primitives, and feedback indicators. start here when building an application specific interface.\n **composed patterns**: data grids, command palettes, navigation menus, steppers, calendars, date/time pickers, file inputs, and message composers. these package recurring accessibility and interaction behavior that is costly to rebuild correctly.\n **presentation patterns**: cards, chat messages, code windows, stats, marquees, avatars, badges, and skeletons. use these when their product vocabulary matches the application; prefer foundations when it does not.\n\nclassification guides discovery and bundle selection. it is not a reason to remove working components or move them into unstable packages; per component subpath imports remain the tree shaking boundary.\n\n## features\n\n<div class=\"features grid\">\n\n **accessible**: keyboard navigation, aria wiring, and focus management across interactive components\n **themeable**: global tokens plus component level css custom properties\n **framework agnostic**: works anywhere html can be rendered\n **tree shakeable**: import only the component entry points you register\n **comprehensive surface**: inputs, content, disclosure, feedback, layout, and overlay primitives\n **native form integration**: form associated controls submit values, validate supported required constraints, and reset with their form\n **typed framework declarations**: dom, react, and vue tags derive from the component property contracts\n\n</div>\n\n### prerequisites\n\n browser runtime with custom elements support.\n import `@vielzeug/refine/fouc.css` and `@vielzeug/refine/tokens.css` before rendering components.\n for ssr, render placeholders server side and hydrate components only on the client.\n\n## documentation\n\n<div class=\"doc links\">\n\n [usage guide](./usage.md)\n [api reference](./api.md)\n [migration guide](./migration.md)\n\n</div>\n\n## see also\n\n<div class=\"see also\">\n\n [ore](/ore/): web component runtime that powers refine\n [orbit](/orbit/): floating ui positioning used in refine's overlays\n [forge](/forge/): form state management for use with refine inputs\n [keymap](/keymap/): keyboard shortcut manager that powers the command palette's global trigger\n\n</div>\n\n<! markdownlint enable md025 md033 md060 >\n",
1030
- "api": " \ntitle: 'refine: api reference'\ndescription: published component, stylesheet, framework type, and error entry points for @vielzeug/refine.\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution mode | common gotcha |\n| | | | |\n| `@vielzeug/refine/<component>` | register one custom element and export its public types | sync | importing a type alone does not register the element |\n| `@vielzeug/refine/tokens.css` | load required theme tokens, animations, and cascade layers | css | import once before components render |\n| `@vielzeug/refine/fouc.css` | hide unregistered `ore *` elements until upgrade | css | load before first paint |\n| `@vielzeug/refine/styles/preflight.css` | apply the optional browser reset | css | the reset affects global elements |\n| `@vielzeug/refine/frameworks/elements` | register typed dom tag mappings | types only | import for side effects in typescript |\n| `@vielzeug/refine/frameworks/react` | register typed react jsx elements | types only | does not provide runtime wrappers |\n| `@vielzeug/refine/frameworks/vue` | register typed vue global components | types only | does not install a vue plugin |\n| `@vielzeug/refine/theme` | `createthemecontroller()`: light/dark/system document theme | sync | does not persist the preference or set an accent color |\n| `eventfieldvalue()` / `eventfieldchecked()` | read a form control's value/checked from `event.currenttarget` | sync | exported from the package root; guards non field targets |\n| `refineerror` | base class for package defined public errors | sync | component configuration warnings do not throw this error |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/refine` | export `refineerror` and the `eventfieldvalue`/`eventfieldchecked` field readers without registering components |\n| `@vielzeug/refine/<component>` | register one component and export its tag constant, props, events, and related types |\n| `@vielzeug/refine/theme` | `createthemecontroller()` and its theme types |\n| `@vielzeug/refine/tokens.css` | required global design contract |\n| `@vielzeug/refine/fouc.css` | optional pre upgrade visibility rule |\n| `@vielzeug/refine/styles/*` | focused theme, animation, layer, and preflight stylesheets |\n| `@vielzeug/refine/frameworks/*` | typescript augmentations for dom, react, and vue |\n\n## styles\n\n```ts\nimport '@vielzeug/refine/fouc.css';\nimport '@vielzeug/refine/tokens.css';\nimport '@vielzeug/refine/styles/preflight.css';\n```\n\n`tokens.css` is required. it defines tokens, animations, and cascade layer order without resetting global elements. `preflight.css` is optional and includes fouc suppression.\n\n| import path | purpose |\n| | |\n| `@vielzeug/refine/fouc.css` | fouc suppression for unupgraded custom elements |\n| `@vielzeug/refine/tokens.css` | tokens, animation helpers, and cascade layers |\n| `@vielzeug/refine/styles/theme.css` | theme token declarations |\n| `@vielzeug/refine/styles/animation.css` | animation helpers |\n| `@vielzeug/refine/styles/layers.css` | cascade layer declarations |\n| `@vielzeug/refine/styles/preflight.css` | optional browser reset and fouc suppression |\n\n## components\n\neach component uses one registration and type entry point:\n\n```ts\nimport '@vielzeug/refine/button';\nimport type { orebuttonprops } from '@vielzeug/refine/button';\n```\n\n| area | components |\n| | |\n| content | `avatar`, `avatar group`, `breadcrumb`, `card`, `carousel`, `chat message`, `code window`, `copy command`, `icon`, `keyboard key`, `list`, `list item`, `marquee`, `pagination`, `separator`, `speech player`, `stats`, `step`, `stepper`, `table`, `text` |\n| disclosure | `accordion`, `accordion item`, `tabs`, `tab item`, `tab panel` |\n| feedback | `alert`, `async`, `badge`, `chip`, `cookie banner`, `password strength`, `progress`, `skeleton`, `toast`, `typing indicator` |\n| inputs | `button`, `button group`, `calendar`, `checkbox`, `checkbox group`, `combobox`, `counter`, `datagrid`, `date picker`, `file input`, `input`, `message composer`, `number input`, `otp input`, `query bar`, `radio`, `radio group`, `rating`, `select`, `slider`, `switch`, `textarea`, `time picker` |\n| layout | `box`, `grid`, `grid item`, `navbar`, `sidebar` |\n| overlays | `command palette`, `dialog`, `drawer`, `menu`, `navigation menu`, `popover`, `tooltip` |\n\neach component page lists its attributes, javascript properties, events, slots, parts, and css custom properties.\n\n## framework types\n\n```ts\nimport type {} from '@vielzeug/refine/frameworks/elements';\nimport type {} from '@vielzeug/refine/frameworks/react';\nimport type {} from '@vielzeug/refine/frameworks/vue';\n```\n\nthe declarations cover every supported tag and derive component properties from refine's authoritative `htmlelementtagnamemap`. they provide types only; component registration still uses component subpaths.\n\n## events and form controls\n\nform controls expose `.value` or `.checked` and dispatch standard `input` and `change` events. read the property from `event.currenttarget`.\n\nfinite choice properties use ore's `prop.oneof()` parsing: values outside a component's documented options resolve to\nthe declared default, or `undefined` when no default is declared.\n\nform associated controls participate in native form submission, validity checks, and reset. a named control's value\nis available through `new formdata(form)`, and supported `required` constraints affect `form.checkvalidity()`,\n`form.reportvalidity()`, and submit blocking. the `error` property renders and announces feedback; it does not by\nitself set native constraint validity.\n\nstateful overlays expose controlled `open`, optional `default open`, and an `open change` custom event with `{ open, reason }` detail. component pages define additional detail fields and reasons.\n\n## utilities\n\n### `createthemecontroller(options?)`\n\n```ts\nimport { createthemecontroller } from '@vielzeug/refine/theme';\n\nconst theme = createthemecontroller(); // starts in 'system' mode\ntheme.setpreference('dark');\ntheme.dispose();\n```\n\nowns the light/dark/system theme for the document: a reactive `preference` signal, the effective `resolved` mode (`'dark' | 'light'` after resolving `system` against the os), and the dom application `styles/theme.css` expects: the `.dark` class plus the `color scheme` property on the root element, so every `light dark()` token resolves to the right branch. while in `system` mode the controller tracks the os `prefers color scheme` via sentinel's `createmediaquery`.\n\nthe controller does **not** persist the preference or expose an accent color: those are consumer concerns (wire `watch(controller.preference, save)` for persistence; set ` color primary hue` yourself). options: `initial` (starting preference, default `'system'`), `root` (element carrying the classes, default `document.documentelement`), `target` (window whose `matchmedia` backs `system`). the handle is disposable: `dispose()`, `disposed`, `disposalsignal`, `[symbol.dispose]`.\n\n### `eventfieldvalue(event)` / `eventfieldchecked(event)`\n\n```ts\nimport { eventfieldchecked, eventfieldvalue } from '@vielzeug/refine';\n\ninput.addeventlistener('input', (event) => {\n const value = eventfieldvalue(event); // string | undefined: never a blind cast\n});\n```\n\nguarded readers for the standard `input`/`change` event pattern above: they read `value`/`checked` from `event.currenttarget` only when the target actually carries it, so a handler wired to the wrong element yields `undefined`/`false` instead of a crash or a lie. exported from the package root, which registers no components.\n\n## types\n\ncomponent props follow the `ore<component>props` naming pattern. event maps follow `ore<component>events`. import both from the component subpath that owns them.\n\n`refineelementmap`, `refinereactintrinsicelements`, and `refinevueglobalcomponents` are exported from their respective `frameworks/*` type entry points.\n\n## errors\n\n### `refineerror`\n\n```ts\nclass refineerror extends error {\n constructor(message: string, opts?: erroroptions);\n}\n```\n\nbase class for public refine errors. it preserves `cause` through `erroroptions`.\n",
1031
- "usage": " \ntitle: 'refine: usage guide'\ndescription: register refine components, configure properties, handle events, compose slots, and integrate frameworks.\n \n\n[[toc]]\n\n## basic usage\n\nload the global design contract once, then register only the components used by the page.\n\n```ts\nimport '@vielzeug/refine/tokens.css';\nimport '@vielzeug/refine/button';\nimport '@vielzeug/refine/input';\nimport '@vielzeug/refine/dialog';\n\nconst input = document.queryselector('ore input');\nconst save = document.queryselector('ore button');\n\nif (!(input instanceof htmlelement) || !(save instanceof htmlelement)) {\n throw new error('expected refine controls');\n}\n\nsave.addeventlistener('click', () => {\n console.log((input as htmlelement & { value: string }).value);\n});\n```\n\nadd the browser reset only when the application owns global element styling:\n\n```ts\nimport '@vielzeug/refine/styles/preflight.css';\n```\n\n## set attributes and properties\n\nuse attributes for serializable html state.\n\n```html\n<ore button variant=\"outline\" color=\"secondary\" size=\"lg\" disabled>\n save changes\n</ore button>\n```\n\nuse javascript properties for arrays, objects, callbacks, and controlled state.\n\n```ts\nimport '@vielzeug/refine/datagrid';\nimport type { oredatagridprops } from '@vielzeug/refine/datagrid';\n\nconst grid = document.queryselector('ore datagrid') as htmlelement & oredatagridprops;\ngrid.columns = [\n { key: 'name', label: 'name', sortable: true },\n { key: 'role', label: 'role' },\n];\ngrid.rows = [\n { id: '1', name: 'ada', role: 'admin' },\n { id: '2', name: 'linus', role: 'editor' },\n];\n```\n\nenumerated properties such as `variant`, `size`, and `color` accept the values listed on each component's reference\npage. an unsupported value resolves to the property's declared default (or `undefined` when it has no default).\n\nin an ore template, `model()` can bind a refine field's `value` to a ripple signal and write user input back:\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { html, model } from '@vielzeug/ore';\nimport '@vielzeug/refine/input';\n\nconst email = signal('');\nconst field = html`<ore input label=\"email\" type=\"email\" value=${model(email)} />`;\n```\n\n## handle events\n\nform controls dispatch standard `input` and `change` events. read their current property from `event.currenttarget`.\n\n```ts\nconst input = document.queryselector('ore input');\ninput?.addeventlistener('input', (event) => {\n const target = event.currenttarget as htmlelement & { value: string };\n console.log(target.value);\n});\n```\n\ncustom events use kebab case names and typed detail objects.\n\n```ts\nimport type { oredialogevents } from '@vielzeug/refine/dialog';\n\nconst dialog = document.queryselector('ore dialog');\ndialog?.addeventlistener('open change', (event: customevent<oredialogevents['open change']>) => {\n console.log(event.detail.open, event.detail.reason);\n});\n```\n\n## native form validation\n\nrefine's form associated controls integrate with native form submission, `formdata`, validity checks, and reset.\nuse `required` for a required value constraint; setting `error` displays and announces feedback but does not, by\nitself, invalidate the form.\n\n```html\n<form id=\"profile form\">\n <ore input name=\"email\" type=\"email\" label=\"email\" required></ore input>\n <ore select name=\"role\" label=\"role\" required>\n <option value=\"\">choose a role…</option>\n <option value=\"editor\">editor</option>\n <option value=\"admin\">admin</option>\n </ore select>\n <ore button type=\"submit\">save</ore button>\n</form>\n\n<script type=\"module\">\n import '@vielzeug/refine/input';\n import '@vielzeug/refine/select';\n import '@vielzeug/refine/button';\n\n const form = document.queryselector('#profile form');\n form.addeventlistener('submit', (event) => {\n event.preventdefault();\n console.log(new formdata(form).get('email'));\n });\n</script>\n```\n\nthe browser runs native validation before dispatching `submit`. call `form.reportvalidity()` to show validation\nfeedback directly, or `form.checkvalidity()` for a boolean check without the browser's validation ui.\n\n## compose slots\n\ndefault slots hold primary content. named slots identify component owned regions.\n\n```html\n<ore card>\n <span slot=\"header\">account</span>\n <p>profile settings</p>\n <div slot=\"footer\">\n <ore button variant=\"ghost\">cancel</ore button>\n <ore button>save</ore button>\n </div>\n</ore card>\n```\n\ninput components commonly expose `prefix`, `suffix`, `label`, `helper`, and `error` slots. each component reference lists its supported slots.\n\n## load typescript framework declarations\n\nimport the declaration matching the framework once in an application type entry point.\n\n::: code group\n\n```ts [dom]\nimport type {} from '@vielzeug/refine/frameworks/elements';\n```\n\n```ts [react]\nimport type {} from '@vielzeug/refine/frameworks/react';\n```\n\n```ts [vue]\nimport type {} from '@vielzeug/refine/frameworks/vue';\n```\n\n:::\n\nthese entry points provide typed tags and component properties. they do not register components or install runtime adapters.\n\n## framework integration\n\nrefine elements use native dom properties, events, and slots. frameworks differ only in how they forward custom element properties and events.\n\n::: code group\n\n```tsx [react 19]\nimport '@vielzeug/refine/button';\n\nexport function savebutton() {\n return <ore button variant=\"solid\">save</ore button>;\n}\n```\n\n```vue [vue 3]\n<script setup lang=\"ts\">\nimport '@vielzeug/refine/button';\n</script>\n\n<template>\n <ore button variant=\"solid\">save</ore button>\n</template>\n```\n\n```svelte [svelte]\n<script lang=\"ts\">\n import '@vielzeug/refine/button';\n</script>\n\n<ore button variant=\"solid\">save</ore button>\n```\n\n:::\n\nsee [framework integration](./frameworks.md) for custom events, controlled properties, ssr guards, and framework specific configuration.\n\n## working with other vielzeug libraries\n\n### ore\n\nuse ore when authoring a custom element that composes refine components.\n\n```ts\nimport '@vielzeug/refine/button';\nimport { define, html } from '@vielzeug/ore';\n\ndefine('save panel', {\n setup() {\n return html`<ore button @click=${() => console.log('save')}>save</ore button>`;\n },\n});\n```\n\n### ripple\n\nuse ripple to drive component properties from application state.\n\n```ts\nimport '@vielzeug/refine/button';\nimport { effect, signal } from '@vielzeug/ripple';\n\nconst loading = signal(false);\nconst button = document.queryselector('ore button') as htmlelement & { loading?: boolean };\nconst loadingeffect = effect(() => {\n button.loading = loading.value;\n});\n\nloadingeffect.dispose();\n```\n\n## best practices\n\n **import** `tokens.css` once before rendering components.\n **register** components through explicit subpaths.\n **set** structured data and callbacks as javascript properties.\n **read** form state from the event target instead of custom event payloads.\n **provide** accessible labels for icon only controls.\n **use** component reference pages for supported slots, parts, and css properties.\n **dispose** application owned ripple effects and service handles.\n **guard** dom dependent component registration during ssr.\n **working with ai agents?** install the `vielzeug` skill (`npx y @vielzeug/codex skills install`) so agents reach for refine components and tokens instead of custom widgets: see the [codex usage guide](../codex/usage.md#agent skill).\n"
1029
+ "index": " \ntitle: 'refine: web component library'\ndescription: accessible, themeable web components built with ore for framework and vanilla dom apps.\npackage: refine\ncategory: ui components\nkeywords: [web components, accessible, themeable, ui, components, design system]\nrelated: [ore, orbit, forge, keymap]\nexports:\n [\n ore accordion,\n ore accordion item,\n ore alert,\n ore async,\n ore avatar,\n ore avatar group,\n ore badge,\n ore box,\n ore breadcrumb,\n ore breadcrumb item,\n ore button,\n ore button group,\n ore calendar,\n ore card,\n ore carousel,\n ore carousel slide,\n ore chat message,\n ore checkbox,\n ore checkbox group,\n ore chip,\n ore code window,\n ore column,\n ore combobox,\n ore combobox option,\n ore command palette,\n ore command palette item,\n ore cookie banner,\n ore copy command,\n ore counter,\n ore datagrid,\n ore date picker,\n ore dialog,\n ore drawer,\n ore file input,\n ore grid,\n ore grid item,\n ore icon,\n ore input,\n ore keyboard key,\n ore keyboard shortcut,\n ore list,\n ore list item,\n ore marquee,\n ore menu,\n ore menu item,\n ore menu separator,\n ore message composer,\n ore navbar,\n ore navbar item,\n ore navigation menu,\n ore navigation menu item,\n ore navigation menu panel,\n ore number input,\n ore otp input,\n ore pagination,\n ore password strength,\n ore popover,\n ore progress,\n ore qr code,\n ore qr scanner,\n ore query bar,\n ore radio,\n ore radio group,\n ore rating,\n ore select,\n ore separator,\n ore sidebar,\n ore sidebar group,\n ore sidebar item,\n ore skeleton,\n ore slider,\n ore speech player,\n ore stats,\n ore step,\n ore stepper,\n ore switch,\n ore tab item,\n ore tab panel,\n ore table,\n ore tabs,\n ore td,\n ore text,\n ore th,\n ore tr,\n ore textarea,\n ore time picker,\n ore toast,\n ore tooltip,\n ore typing indicator,\n ]\nenvironments: [browser]\n \n\n<! markdownlint disable md025 md033 md060 >\n\n<packagehero package=\"refine\" />\n\n## why refine?\n\nevery project needs ui primitives. refine provides accessible web components that work natively anywhere html is rendered, no framework required.\n\n```html\n<! before: roll your own button with aria >\n<button class=\"btn btn primary\" role=\"button\" aria pressed=\"false\" tabindex=\"0\">\n <span class=\"btn spinner\" aria hidden=\"true\"></span>\n save\n</button>\n\n<! after: refine >\n<ore button variant=\"solid\" color=\"primary\" loading>save</ore button>\n```\n\n| feature | refine | shoelace | material web |\n| | | | |\n| bundle size | <packageinfo package=\"refine\" type=\"size\" /> | ~145 kb | ~200 kb |\n| built with | ore | lit | lit |\n| runtime dependencies | vielzeug ui primitives; lucide only for the opt in full library registrar | lit + utilities | lit + material foundations |\n| accessible | wcag aa | wcag aa | wcag aa |\n| framework agnostic | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"check\" size=\"16\"></ore icon> |\n\n<div class=\"decision callout\">\n\n**use refine when** you want accessible web components that match the vielzeug design system without a heavy framework dependency.\n\n**consider shoelace or material web** if your team is already standardized on those ecosystems and you need their established component catalogs.\n\n</div>\n\n## installation\n\n::: code group\n\n```sh [pnpm]\npnpm add @vielzeug/refine\n```\n\n```sh [npm]\nnpm install @vielzeug/refine\n```\n\n```sh [yarn]\nyarn add @vielzeug/refine\n```\n\n:::\n\n## quick start\n\n```ts\n// 1. import global styles once\nimport '@vielzeug/refine/fouc.css'; // hide unupgraded custom elements until first paint\nimport '@vielzeug/refine/tokens.css'; // tokens, animations, cascade layers\n\n// 2. register only the elements you need\nimport '@vielzeug/refine/button';\nimport '@vielzeug/refine/input';\nimport '@vielzeug/refine/card';\n```\n\n```html\n<ore button variant=\"solid\" color=\"primary\">save</ore button>\n<ore input label=\"email\" type=\"email\" required></ore input>\n<ore card padding=\"lg\">\n <span slot=\"header\">account</span>\n <p>card content goes here.</p>\n</ore card>\n```\n\n```ts\n```\n\n### cdn / vanilla html\n\nuse the self contained iife bundle to load refine directly from a cdn in any html page: no build step required:\n\n```html\n<! 1. styles >\n<link rel=\"stylesheet\" href=\"https://unpkg.com/@vielzeug/refine/dist/styles/fouc.css\" />\n<link rel=\"stylesheet\" href=\"https://unpkg.com/@vielzeug/refine/dist/styles/tokens.css\" />\n\n<! 2. all components (iife: registers global refine namespace) >\n<script src=\"https://unpkg.com/@vielzeug/refine/dist/refine.iife.js\"></script>\n```\n\nfor bundler based projects that still want a cdn url, use the esm bundle via an import map:\n\n```html\n<script type=\"importmap\">\n {\n \"imports\": {\n \"@vielzeug/refine\": \"https://esm.sh/@vielzeug/refine\",\n \"@vielzeug/refine/button\": \"https://esm.sh/@vielzeug/refine/button\",\n \"@vielzeug/refine/input\": \"https://esm.sh/@vielzeug/refine/input\"\n }\n }\n</script>\n\n<script type=\"module\">\n import '@vielzeug/refine/button';\n import '@vielzeug/refine/input';\n</script>\n```\n\n### package entry points\n\n| import | purpose |\n| | |\n| `@vielzeug/refine/fouc.css` | fouc suppression for unupgraded custom elements |\n| `@vielzeug/refine/tokens.css` | global design tokens and cascade layers |\n| `@vielzeug/refine/styles/preflight.css` | optional browser default reset (includes fouc suppression) |\n\ncomponent registration happens through side effect imports such as `@vielzeug/refine/button` and `@vielzeug/refine/dialog`.\n\n### components\n\n**content:** `ore avatar`, `ore avatar group`, `ore breadcrumb`, `ore card`, `ore carousel`, `ore carousel slide`, `ore chat message`, `ore icon`, `ore keyboard key`, `ore keyboard shortcut`, `ore list`, `ore list item`, `ore marquee`, `ore pagination`, `ore qr code`, `ore separator`, `ore stats`, `ore step`, `ore stepper`, `ore table`, `ore text`\n\n**disclosure:** `ore accordion`, `ore accordion item`, `ore tabs`, `ore tab item`, `ore tab panel`\n\n**feedback:** `ore alert`, `ore async`, `ore badge`, `ore chip`, `ore cookie banner`, `ore password strength`, `ore progress`, `ore skeleton`, `ore toast`, `ore typing indicator`\n\n**inputs:** `ore button`, `ore button group`, `ore calendar`, `ore checkbox`, `ore checkbox group`, `ore column`, `ore combobox`, `ore counter`, `ore datagrid`, `ore date picker`, `ore file input`, `ore input`, `ore message composer`, `ore number input`, `ore otp input`, `ore qr scanner`, `ore query bar`, `ore radio`, `ore radio group`, `ore rating`, `ore select`, `ore slider`, `ore switch`, `ore textarea`, `ore time picker`\n\n**layout:** `ore box`, `ore grid`, `ore grid item`, `ore navbar`, `ore sidebar`\n\n**overlay:** `ore command palette`, `ore command palette item`, `ore dialog`, `ore drawer`, `ore menu`, `ore popover`, `ore tooltip`\n\n## component portfolio\n\nrefine keeps one public package and classifies components by composition level rather than stability. all listed components remain supported and tested.\n\n **foundations**: buttons, inputs, selection controls, disclosure, dialogs, menus, tooltips, layout primitives, and feedback indicators. start here when building an application specific interface.\n **composed patterns**: data grids, command palettes, navigation menus, steppers, calendars, date/time pickers, file inputs, and message composers. these package recurring accessibility and interaction behavior that is costly to rebuild correctly.\n **presentation patterns**: cards, chat messages, code windows, stats, marquees, avatars, badges, and skeletons. use these when their product vocabulary matches the application; prefer foundations when it does not.\n\nclassification guides discovery and bundle selection. it is not a reason to remove working components or move them into unstable packages; per component subpath imports remain the tree shaking boundary.\n\n## features\n\n<div class=\"features grid\">\n\n **accessible**: keyboard navigation, aria wiring, and focus management across interactive components\n **themeable**: global tokens plus component level css custom properties\n **framework agnostic**: works anywhere html can be rendered\n **tree shakeable**: import only the component entry points you register\n **comprehensive surface**: inputs, content, disclosure, feedback, layout, and overlay primitives\n **native form integration**: form associated controls submit values, validate supported required constraints, and reset with their form\n **typed framework declarations**: dom, react, and vue tags derive from the component property contracts\n\n</div>\n\n### prerequisites\n\n browser runtime with custom elements support.\n import `@vielzeug/refine/fouc.css` and `@vielzeug/refine/tokens.css` before rendering components.\n for ssr, render placeholders server side and hydrate components only on the client.\n\n## documentation\n\n<div class=\"doc links\">\n\n [usage guide](./usage.md)\n [api reference](./api.md)\n [migration guide](./migration.md)\n\n</div>\n\n## see also\n\n<div class=\"see also\">\n\n [ore](/ore/): web component runtime that powers refine\n [orbit](/orbit/): floating ui positioning used in refine's overlays\n [forge](/forge/): form state management for use with refine inputs\n [keymap](/keymap/): keyboard shortcut manager that powers the command palette's global trigger\n\n</div>\n\n<! markdownlint enable md025 md033 md060 >\n",
1030
+ "api": " \ntitle: 'refine: api reference'\ndescription: published component, stylesheet, framework type, and error entry points for @vielzeug/refine.\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution mode | common gotcha |\n| | | | |\n| `@vielzeug/refine/<component>` | register one custom element and export its public types | sync | importing a type alone does not register the element |\n| `@vielzeug/refine/tokens.css` | load required theme tokens, animations, and cascade layers | css | import once before components render |\n| `@vielzeug/refine/fouc.css` | hide unregistered `ore *` elements until upgrade | css | load before first paint |\n| `@vielzeug/refine/styles/preflight.css` | apply the optional browser reset | css | the reset affects global elements |\n| `@vielzeug/refine/frameworks/elements` | register typed dom tag mappings | types only | import for side effects in typescript |\n| `@vielzeug/refine/frameworks/react` | register typed react jsx elements | types only | does not provide runtime wrappers |\n| `@vielzeug/refine/frameworks/vue` | register typed vue global components | types only | does not install a vue plugin |\n| `@vielzeug/refine/theme` | `createthemecontroller()`: light/dark/system document theme | sync | does not persist the preference or set an accent color |\n| `eventfieldvalue()` / `eventfieldchecked()` | read a form control's value/checked from `event.currenttarget` | sync | exported from the package root; guards non field targets |\n| `refineerror` | base class for package defined public errors | sync | component configuration warnings do not throw this error |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/refine` | export `registericons`, icon definition types, `refineerror`, and the field readers without registering components |\n| `@vielzeug/refine/<component>` | register one component and export its tag constant, props, events, and related types |\n| `@vielzeug/refine/theme` | `createthemecontroller()` and its theme types |\n| `@vielzeug/refine/tokens.css` | required global design contract |\n| `@vielzeug/refine/fouc.css` | optional pre upgrade visibility rule |\n| `@vielzeug/refine/styles/*` | focused theme, animation, layer, and preflight stylesheets |\n| `@vielzeug/refine/frameworks/*` | typescript augmentations for dom, react, and vue |\n\n## icon registration and adapters\n\n`registericons(icons: readonly<record<string, icondefinition>>): void` is available from `@vielzeug/refine/icons`, `@vielzeug/refine`, and `@vielzeug/refine/icon`. the common `icons` entry and root remain free of component registration side effects.\n\n`icondefinition`, `iconelement`, and `iconattributes` are exported from all three entries. shared definition and registration modules live in `src/icons/`, alongside adapters and internal svg helpers/defaults. definitions contain a numeric `viewbox` tuple, optional presentation attributes, and recursive svg children; they do not require a provider identity. invalid registrations throw the root exported `refineerror` atomically.\n\n| entry | export | purpose |\n| | | |\n| `@vielzeug/refine/icons` | `registericons`, `icondefinition`, `iconelement`, `iconattributes` | common library neutral registration and definition api |\n| `@vielzeug/refine/icons/lucide` | `lucideicons`, `lucideicondata` | pure adapter for selected structural lucide svg data |\n| `@vielzeug/refine/icons/lucide/all` | side effect registration | explicitly register the full installed lucide library |\n| `@vielzeug/refine/icons/heroicons` | `heroiconsicons`, `iconifyicon` | convert a caller named map of individual heroicons iconify data |\n| `@vielzeug/refine/icons/tabler` | `tablericons`, `iconifyicon` | convert a caller named map of individual tabler iconify data |\n| `@vielzeug/refine/icons/mdi` | `mdiicons`, `iconifyicon` | convert a caller named map of individual mdi iconify data |\n| `@vielzeug/refine/icons/iconify` | `iconifyicon`, `iconifyicons` | convert individual iconify data or selected/complete collections |\n| `@vielzeug/refine/icons/iconify` | `iconifyicon`, `iconifyalias`, `iconifycollection`, `iconifytransform` | structural data types; no iconify dependency required |\n\nlucide is an optional peer, not a mandatory dependency. only `icons/lucide/all` requires an installed `lucide` package; consumers selecting that entry declare it themselves. the structural `icons/lucide` adapter accepts supplied data without importing lucide.\n\nthe standalone iife exposes `registericons`, `lucideicons`, `heroiconsicons`, `tablericons`, `mdiicons`, `iconifyicon`, and `iconifyicons` under `refine.components.content`. neither the iife nor component entries import lucide. iconify conversion drops provider `class` metadata, validates finite numeric offsets and positive finite dimensions before transforms, and retains the strict svg allowlist. see the [icon reference](./components/icon.md) for presentation overrides, local svg id rewriting, supported families, and licenses.\n\nthe three family adapters share this signature:\n\n```ts\nheroiconsicons(icons: readonly<record<string, iconifyicon>>): record<string, icondefinition>;\ntablericons(icons: readonly<record<string, iconifyicon>>): record<string, icondefinition>;\nmdiicons(icons: readonly<record<string, iconifyicon>>): record<string, icondefinition>;\n```\n\neach accepts individual iconify svg data, preserves input keys without adding a namespace, and returns definitions without registering them. they require `domparser`, preserve each icon's geometry/presentation/transforms, and throw `refineerror` for invalid iconify data. they do not accept react/vue components, collection json, or raw svg path strings. use `iconifyicons(collection, names?)` for collections and alias resolution.\n\n## styles\n\n```ts\nimport '@vielzeug/refine/fouc.css';\nimport '@vielzeug/refine/tokens.css';\nimport '@vielzeug/refine/styles/preflight.css';\n```\n\n`tokens.css` is required. it defines tokens, animations, and cascade layer order without resetting global elements. `preflight.css` is optional and includes fouc suppression.\n\n| import path | purpose |\n| | |\n| `@vielzeug/refine/fouc.css` | fouc suppression for unupgraded custom elements |\n| `@vielzeug/refine/tokens.css` | tokens, animation helpers, and cascade layers |\n| `@vielzeug/refine/styles/theme.css` | theme token declarations |\n| `@vielzeug/refine/styles/animation.css` | animation helpers |\n| `@vielzeug/refine/styles/layers.css` | cascade layer declarations |\n| `@vielzeug/refine/styles/preflight.css` | optional browser reset and fouc suppression |\n\n## components\n\neach component uses one registration and type entry point:\n\n```ts\nimport '@vielzeug/refine/button';\nimport type { orebuttonprops } from '@vielzeug/refine/button';\n```\n\n| area | components |\n| | |\n| content | `avatar`, `avatar group`, `breadcrumb`, `card`, `carousel`, `chat message`, `code window`, `copy command`, `icon`, `keyboard key`, `list`, `list item`, `marquee`, `pagination`, `separator`, `speech player`, `stats`, `step`, `stepper`, `table`, `text` |\n| disclosure | `accordion`, `accordion item`, `tabs`, `tab item`, `tab panel` |\n| feedback | `alert`, `async`, `badge`, `chip`, `cookie banner`, `password strength`, `progress`, `skeleton`, `toast`, `typing indicator` |\n| inputs | `button`, `button group`, `calendar`, `checkbox`, `checkbox group`, `combobox`, `counter`, `datagrid`, `date picker`, `file input`, `input`, `message composer`, `number input`, `otp input`, `query bar`, `radio`, `radio group`, `rating`, `select`, `slider`, `switch`, `textarea`, `time picker` |\n| layout | `box`, `grid`, `grid item`, `navbar`, `sidebar` |\n| overlays | `command palette`, `dialog`, `drawer`, `menu`, `navigation menu`, `popover`, `tooltip` |\n\neach component page lists its attributes, javascript properties, events, slots, parts, and css custom properties.\n\n## framework types\n\n```ts\nimport type {} from '@vielzeug/refine/frameworks/elements';\nimport type {} from '@vielzeug/refine/frameworks/react';\nimport type {} from '@vielzeug/refine/frameworks/vue';\n```\n\nthe declarations cover every supported tag and derive component properties from refine's authoritative `htmlelementtagnamemap`. they provide types only; component registration still uses component subpaths.\n\n## events and form controls\n\nform controls expose `.value` or `.checked` and dispatch standard `input` and `change` events. read the property from `event.currenttarget`.\n\nfinite choice properties use ore's `prop.oneof()` parsing: values outside a component's documented options resolve to\nthe declared default, or `undefined` when no default is declared.\n\nform associated controls participate in native form submission, validity checks, and reset. a named control's value\nis available through `new formdata(form)`, and supported `required` constraints affect `form.checkvalidity()`,\n`form.reportvalidity()`, and submit blocking. the `error` property renders and announces feedback; it does not by\nitself set native constraint validity.\n\nstateful overlays expose controlled `open`, optional `default open`, and an `open change` custom event with `{ open, reason }` detail. component pages define additional detail fields and reasons.\n\n## utilities\n\n### `createthemecontroller(options?)`\n\n```ts\nimport { createthemecontroller } from '@vielzeug/refine/theme';\n\nconst theme = createthemecontroller(); // starts in 'system' mode\ntheme.setpreference('dark');\ntheme.dispose();\n```\n\nowns the light/dark/system theme for the document: a reactive `preference` signal, the effective `resolved` mode (`'dark' | 'light'` after resolving `system` against the os), and the dom application `styles/theme.css` expects: the `.dark` class plus the `color scheme` property on the root element, so every `light dark()` token resolves to the right branch. while in `system` mode the controller tracks the os `prefers color scheme` via sentinel's `createmediaquery`.\n\nthe controller does **not** persist the preference or expose an accent color: those are consumer concerns (wire `watch(controller.preference, save)` for persistence; set ` color primary hue` yourself). options: `initial` (starting preference, default `'system'`), `root` (element carrying the classes, default `document.documentelement`), `target` (window whose `matchmedia` backs `system`). the handle is disposable: `dispose()`, `disposed`, `disposalsignal`, `[symbol.dispose]`.\n\n### `eventfieldvalue(event)` / `eventfieldchecked(event)`\n\n```ts\nimport { eventfieldchecked, eventfieldvalue } from '@vielzeug/refine';\n\ninput.addeventlistener('input', (event) => {\n const value = eventfieldvalue(event); // string | undefined: never a blind cast\n});\n```\n\nguarded readers for the standard `input`/`change` event pattern above: they read `value`/`checked` from `event.currenttarget` only when the target actually carries it, so a handler wired to the wrong element yields `undefined`/`false` instead of a crash or a lie. exported from the package root, which registers no components.\n\n## types\n\ncomponent props follow the `ore<component>props` naming pattern. event maps follow `ore<component>events`. import both from the component subpath that owns them.\n\n`refineelementmap`, `refinereactintrinsicelements`, and `refinevueglobalcomponents` are exported from their respective `frameworks/*` type entry points.\n\n## errors\n\n### `refineerror`\n\n```ts\nclass refineerror extends error {\n constructor(message: string, opts?: erroroptions);\n}\n```\n\nbase class for public refine errors. it preserves `cause` through `erroroptions`.\n",
1031
+ "usage": " \ntitle: 'refine: usage guide'\ndescription: register refine components, configure properties, handle events, compose slots, and integrate frameworks.\n \n\n[[toc]]\n\n## basic usage\n\nload the global design contract once, then register only the components used by the page.\n\n```ts\nimport '@vielzeug/refine/tokens.css';\nimport '@vielzeug/refine/button';\nimport '@vielzeug/refine/input';\nimport '@vielzeug/refine/dialog';\n\nconst input = document.queryselector('ore input');\nconst save = document.queryselector('ore button');\n\nif (!(input instanceof htmlelement) || !(save instanceof htmlelement)) {\n throw new error('expected refine controls');\n}\n\nsave.addeventlistener('click', () => {\n console.log((input as htmlelement & { value: string }).value);\n});\n```\n\nadd the browser reset only when the application owns global element styling:\n\n```ts\nimport '@vielzeug/refine/styles/preflight.css';\n```\n\n## choose an icon library\n\nrefine includes only its internal component icons. register application icons before mounting them: use `registericons` and neutral `icondefinition` data from `@vielzeug/refine/icons`, the pure `@vielzeug/refine/icons/lucide` adapter for selected lucide icons, or `@vielzeug/refine/icons/iconify` for local heroicons, tabler, and material design icons data. keep importing the component from `@vielzeug/refine/icon`. lucide is an optional peer: install `lucide` yourself only when choosing its data or `@vielzeug/refine/icons/lucide/all`; core and structural adapters do not require it. the [icon reference](./components/icon.md) covers imports, licenses, and presentation behavior.\n\nfor selected individual iconify data, use `heroiconsicons`, `tablericons`, or `mdiicons` from `@vielzeug/refine/icons/heroicons`, `/tabler`, or `/mdi`. for example, `registericons(heroiconsicons({ search, home }))` preserves your app keys. install the matching `@iconify icons/<family>` data package yourself; these helpers do not accept framework components. keep `iconifyicons` for collection json and aliases.\n\n## set attributes and properties\n\nuse attributes for serializable html state.\n\n```html\n<ore button variant=\"outline\" color=\"secondary\" size=\"lg\" disabled>\n save changes\n</ore button>\n```\n\nuse javascript properties for arrays, objects, callbacks, and controlled state.\n\n```ts\nimport '@vielzeug/refine/datagrid';\nimport type { oredatagridprops } from '@vielzeug/refine/datagrid';\n\nconst grid = document.queryselector('ore datagrid') as htmlelement & oredatagridprops;\ngrid.columns = [\n { key: 'name', label: 'name', sortable: true },\n { key: 'role', label: 'role' },\n];\ngrid.rows = [\n { id: '1', name: 'ada', role: 'admin' },\n { id: '2', name: 'linus', role: 'editor' },\n];\n```\n\nenumerated properties such as `variant`, `size`, and `color` accept the values listed on each component's reference\npage. an unsupported value resolves to the property's declared default (or `undefined` when it has no default).\n\nin an ore template, `model()` can bind a refine field's `value` to a ripple signal and write user input back:\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { html, model } from '@vielzeug/ore';\nimport '@vielzeug/refine/input';\n\nconst email = signal('');\nconst field = html`<ore input label=\"email\" type=\"email\" value=${model(email)} />`;\n```\n\n## handle events\n\nform controls dispatch standard `input` and `change` events. read their current property from `event.currenttarget`.\n\n```ts\nconst input = document.queryselector('ore input');\ninput?.addeventlistener('input', (event) => {\n const target = event.currenttarget as htmlelement & { value: string };\n console.log(target.value);\n});\n```\n\ncustom events use kebab case names and typed detail objects.\n\n```ts\nimport type { oredialogevents } from '@vielzeug/refine/dialog';\n\nconst dialog = document.queryselector('ore dialog');\ndialog?.addeventlistener('open change', (event: customevent<oredialogevents['open change']>) => {\n console.log(event.detail.open, event.detail.reason);\n});\n```\n\n## native form validation\n\nrefine's form associated controls integrate with native form submission, `formdata`, validity checks, and reset.\nuse `required` for a required value constraint; setting `error` displays and announces feedback but does not, by\nitself, invalidate the form.\n\n```html\n<form id=\"profile form\">\n <ore input name=\"email\" type=\"email\" label=\"email\" required></ore input>\n <ore select name=\"role\" label=\"role\" required>\n <option value=\"\">choose a role…</option>\n <option value=\"editor\">editor</option>\n <option value=\"admin\">admin</option>\n </ore select>\n <ore button type=\"submit\">save</ore button>\n</form>\n\n<script type=\"module\">\n import '@vielzeug/refine/input';\n import '@vielzeug/refine/select';\n import '@vielzeug/refine/button';\n\n const form = document.queryselector('#profile form');\n form.addeventlistener('submit', (event) => {\n event.preventdefault();\n console.log(new formdata(form).get('email'));\n });\n</script>\n```\n\nthe browser runs native validation before dispatching `submit`. call `form.reportvalidity()` to show validation\nfeedback directly, or `form.checkvalidity()` for a boolean check without the browser's validation ui.\n\n## compose slots\n\ndefault slots hold primary content. named slots identify component owned regions.\n\n```html\n<ore card>\n <span slot=\"header\">account</span>\n <p>profile settings</p>\n <div slot=\"footer\">\n <ore button variant=\"ghost\">cancel</ore button>\n <ore button>save</ore button>\n </div>\n</ore card>\n```\n\ninput components commonly expose `prefix`, `suffix`, `label`, `helper`, and `error` slots. each component reference lists its supported slots.\n\n## load typescript framework declarations\n\nimport the declaration matching the framework once in an application type entry point.\n\n::: code group\n\n```ts [dom]\nimport type {} from '@vielzeug/refine/frameworks/elements';\n```\n\n```ts [react]\nimport type {} from '@vielzeug/refine/frameworks/react';\n```\n\n```ts [vue]\nimport type {} from '@vielzeug/refine/frameworks/vue';\n```\n\n:::\n\nthese entry points provide typed tags and component properties. they do not register components or install runtime adapters.\n\n## framework integration\n\nrefine elements use native dom properties, events, and slots. frameworks differ only in how they forward custom element properties and events.\n\n::: code group\n\n```tsx [react 19]\nimport '@vielzeug/refine/button';\n\nexport function savebutton() {\n return <ore button variant=\"solid\">save</ore button>;\n}\n```\n\n```vue [vue 3]\n<script setup lang=\"ts\">\nimport '@vielzeug/refine/button';\n</script>\n\n<template>\n <ore button variant=\"solid\">save</ore button>\n</template>\n```\n\n```svelte [svelte]\n<script lang=\"ts\">\n import '@vielzeug/refine/button';\n</script>\n\n<ore button variant=\"solid\">save</ore button>\n```\n\n:::\n\nsee [framework integration](./frameworks.md) for custom events, controlled properties, ssr guards, and framework specific configuration.\n\n## working with other vielzeug libraries\n\n### ore\n\nuse ore when authoring a custom element that composes refine components.\n\n```ts\nimport '@vielzeug/refine/button';\nimport { define, html } from '@vielzeug/ore';\n\ndefine('save panel', {\n setup() {\n return html`<ore button @click=${() => console.log('save')}>save</ore button>`;\n },\n});\n```\n\n### ripple\n\nuse ripple to drive component properties from application state.\n\n```ts\nimport '@vielzeug/refine/button';\nimport { effect, signal } from '@vielzeug/ripple';\n\nconst loading = signal(false);\nconst button = document.queryselector('ore button') as htmlelement & { loading?: boolean };\nconst loadingeffect = effect(() => {\n button.loading = loading.value;\n});\n\nloadingeffect.dispose();\n```\n\n## best practices\n\n **import** `tokens.css` once before rendering components.\n **register** components through explicit subpaths.\n **set** structured data and callbacks as javascript properties.\n **read** form state from the event target instead of custom event payloads.\n **provide** accessible labels for icon only controls.\n **use** component reference pages for supported slots, parts, and css properties.\n **dispose** application owned ripple effects and service handles.\n **guard** dom dependent component registration during ssr.\n **working with ai agents?** install the `vielzeug` skill (`npx y @vielzeug/codex skills install`) so agents reach for refine components and tokens instead of custom widgets: see the [codex usage guide](../codex/usage.md#agent skill).\n"
1032
1032
  },
1033
1033
  "examples": [],
1034
1034
  "exports": "ore accordion ore accordion item ore alert ore async ore avatar ore avatar group ore badge ore box ore breadcrumb ore breadcrumb item ore button ore button group ore calendar ore card ore carousel ore carousel slide ore chat message ore checkbox ore checkbox group ore chip ore code window ore column ore combobox ore combobox option ore command palette ore command palette item ore cookie banner ore copy command ore counter ore datagrid ore date picker ore dialog ore drawer ore file input ore grid ore grid item ore icon ore input ore keyboard key ore keyboard shortcut ore list ore list item ore marquee ore menu ore menu item ore menu separator ore message composer ore navbar ore navbar item ore navigation menu ore navigation menu item ore navigation menu panel ore number input ore otp input ore pagination ore password strength ore popover ore progress ore qr code ore qr scanner ore query bar ore radio ore radio group ore rating ore select ore separator ore sidebar ore sidebar group ore sidebar item ore skeleton ore slider ore speech player ore stats ore step ore stepper ore switch ore tab item ore tab panel ore table ore tabs ore td ore text ore th ore tr ore textarea ore time picker ore toast ore tooltip ore typing indicator",
@@ -1036,7 +1036,7 @@
1036
1036
  "name": "@vielzeug/refine",
1037
1037
  "related": "ore orbit forge keymap",
1038
1038
  "slug": "refine",
1039
- "source": "/**\n * refine components register through their explicit component entry points.\n *\n * keeping the package root free of registration side effects makes dependency\n * ownership and bundle contents obvious to application code.\n */\nexport { refineerror } from './errors';\nexport { eventfieldchecked, eventfieldvalue } from './inputs/shared/native field event';\n"
1039
+ "source": "/**\n * refine components register through their explicit component entry points.\n *\n * keeping the package root free of registration side effects makes dependency\n * ownership and bundle contents obvious to application code.\n */\n\nexport { refineerror } from './errors';\nexport type { iconattributes, icondefinition, iconelement } from './icons';\nexport { registericons } from './icons';\nexport { eventfieldchecked, eventfieldvalue } from './inputs/shared/native field event';\n"
1040
1040
  },
1041
1041
  {
1042
1042
  "category": "state",