Browse documentation

notabenda-cf

On this page 14 sections

Optional, dependency-free Cloudflare integration for read-only evidence inspection and immutable legal-document archives. The host supplies authentication and authorization. Recording helpers run through host-owned calls, separately from public document rendering.

Start a project

After installing the local preview package, run npx notabenda-cf init --out-dir ./privacy-backend --site-id your-site. It creates a new standalone Worker project with SQLite storage, the responsive admin, local setup assets and a host authorization stub that denies access until implemented. Existing directories are never replaced, and initialization performs no install, login or deployment.

The generated project supplies build, doctor (read-only local checks), check (Wrangler dry run), dev and explicit deploy scripts. The packages remain unpublished; its README explains installing local tarballs. See the starter guide for project files, permission separation, private archive RPC, deployment and removal.

npx notabenda-cf doctor [--directory PATH] [--config wrangler.jsonc] [--env staging] [--json] reports bounded local starter issues and separate manual reviews. It never executes project code, loads credentials, installs, builds or deploys. A successful check still needs to be followed by host authorization and lifecycle testing. See doctor scope and report format.

Host-owned authorization

Every worker request calls a mandatory authorize hook. The host verifies the caller and returns an already scoped RecordStore, or null to deny access. Missing hooks throw at construction; denied, malformed, or failed grants return a generic 403. No URL accepts a Durable Object ID, namespace, or authorization scope. The host selects those before returning the store.

import { createAdminWorker, createDurableObjectStore } from 'notabenda-cf';
import type { DurableStorage } from 'notabenda-cf';

// Inside an existing Durable Object, use its ctx.storage.
export function mountEvidence(
  storage: DurableStorage,
  authorizeHostRequest: (request: Request) => Promise<{ storagePrefix: string; label: string } | null>,
) {
  return createAdminWorker({
    basePath: '/backoffice/evidence',
    workspaceLabel: 'Evidence studio',
    navigation: [
      {label: 'Evidence', href: '/backoffice/evidence'},
      {label: 'Document revisions', href: '/backoffice/history'},
      {label: 'Guided setup', href: '/backoffice/setup'},
    ],
    async authorize(request) {
      const permission = await authorizeHostRequest(request);
      if (!permission) return null;
      return {
        store: createDurableObjectStore(storage, { prefix: permission.storagePrefix }),
        scopeLabel: permission.label,
      };
    },
  });
}

storagePrefix must come from trusted host permission resolution, never from a URL or an unverified forwarded identity header. Prefixes are nonempty, end in /, and use letters, digits, _, :, /, or -. Use disjoint prefixes for independently authorized stores. The Worker has a standard fetch(request, env, context?) method suitable for mounting in a host Worker/DO or a local preview server. The host remains responsible for its session security, authorization policy, access logging, rate limits, and Cloudflare routing.

RecordStore has two operations:

interface RecordStore {
  list(query: RecordQuery): Promise<RecordPage>;
  get(id: string): Promise<EvidenceRecord | undefined>;
}
// EvidenceRecord: { id: string, data: JsonValue }
// RecordQuery: { query, kind, from, to, keyPrefix, limit, after? }
// RecordPage: { records, nextAfter?, scanned? }

The built-in adapter reads JSON-compatible values with ctx.storage.list/get. Values may be objects, arrays, strings, numbers, booleans, or null. Cycles, accessors, dates, Maps, buffers, and other structured-clone types need a host adapter projecting them to JSON. The adapter never writes evidence records. Cloudflare documents its asynchronous KV storage API, key ordering and prefix/cursor options here.

Search uses AND matching of whitespace-separated words across record keys and serialized JSON. Additional filters select record kind, key prefix, and UTC event-date range. Useful optional fields are:

Field Presentation
kind or type Record classification
occurredAt, timestamp, or createdAt Recorded event time; use an ISO timestamp with a timezone
subjectId, siteId Stored identity and site references; never authorization inputs
documentVersion or document.version Document version
purposeChoices or purposes Readable stored choices
provenance Origin/context declared in the record
retention or expiresAt Declared retention information

No field is required, and absent facts are not invented. Results are cards with context and a detail view; raw JSON sits under a disclosure. A stored record does not establish lawful processing, valid consent, successful delivery, or completeness.

The responsive workspace uses a navigation rail on desktop and an explicit menu on smaller screens. Search begins with one field; advanced filters expand when needed and remain open when populated. Agreement cards lead with the stored subject reference. workspaceLabel, workspaceNote and up to twelve unique same-origin navigation links are optional presentation settings, shared with createSetupWorker. They neither authorize a destination nor select its store; every destination still needs its own host permission check. No external fonts, scripts or UI service are needed for inspection pages.

The default adapter scans at most 200 records per request (scanLimit accepts 1–500), returning at most 50 matches. A bounded segment can contain zero matches and still have a continuation. Results follow UTF-8 key order, not event-time order, and are not a snapshot across requests. A continuation can lead to a final empty page. Counts describe only the current segment. For large stores, implement RecordStore using an appropriate host-owned index; preserve its authorization scope and pagination contract.

Records are limited to 64 KiB, 20 nesting levels, and 10,000 JSON nodes; keys to 512 UTF-8 bytes. Oversized or unsupported records cause a generic inspection error. A custom adapter can instead return a bounded descriptive projection. Page size, filters, query length, and returned adapter data are also bounded.

Filtered evidence export

Return allowExport: true in the host's authorization grant to enable the export review and JSON attachment routes. It defaults to false and is checked on every request, separately from ordinary inspection. The review screen preserves the current filters, explains the disclosure boundary and shows a live preview; the download reads the store again. Each versioned JSON segment contains exact normalized records, filters, read times, counts and continuation links. Empty segments can still have continuation.

All fields returned by the scoped store are included without redaction. Source hooks and external artifact links are not followed. Downloads are bounded, unsigned live reads, without snapshot or completeness guarantees, and do not export the application's other data. Use the downloaded file's continuation links and save each segment separately. Hosts own export auditing, access policy, secure handling and retention of copies. The trusted application API createEvidenceExportSegment(store, query, {scopeLabel}) supplies the same one-segment envelope without HTTP links. See the export guide for the schema, limits and operational contract.

Immutable release history and readable differences

import { recordLegalRelease, createDocumentHistory, createAdminWorker } from 'notabenda-cf';

// artifact is the JSON artifact produced by notabenda/content, including
// manifest, publicConfig, pages, and structured documents.
await recordLegalRelease(ctx.storage, artifact, {
  recordedAt: '2026-10-18T12:00:00Z',
  changeNote: 'Explain the processing-purpose clarification.',
  actor: 'host-provided-operator-reference',
}, { prefix: 'history/site-one/' });

const historyWorker = createAdminWorker({
  basePath: '/backoffice/history',
  title: 'Document history',
  async authorize(request) {
    if (!await hostCanReadHistory(request)) return null;
    return { store: createDocumentHistory(ctx.storage, { prefix: 'history/site-one/' }) };
  },
});

The default archive prefix is notabenda/releases/; use a separate host-selected prefix for each separately authorized site. Recording is an explicit host operation. It does not publish, notify, accept terms, or change consent.

recordLegalRelease requires the DO's transactional storage API. It checks artifact identity, public-config hash, rendered-page hashes, and structured-content hashes, then atomically inserts by artifact ID. Repeating the same artifact and metadata returns { id, created: false }. A conflicting artifact or metadata is rejected, never overwritten. Metadata has no automatic defaults: pass a stable timestamp for retryable writes. Direct writes outside this API can still alter the underlying storage; this is not WORM storage or a cryptographic audit service.

Each archive preserves exact HTML, structured documents, and public configuration. The JSON archive limit is 128 KiB; storage-backend serialization limits can be stricter. Larger archives need a future/custom blob-storage adapter. The backoffice projects metadata and structured documents only; it never executes or embeds archived HTML. Projected records must also fit the 64 KiB inspection limit.

When a results page contains at least two document releases, a comparison form offers readable version/date/change-note choices. Defaults use the two latest distinct stored timestamps on that page; absent or tied timestamps leave the choice to the reviewer. These selectors do not change key ordering or search beyond the current page. The release detail also provides a manual-ID comparison form.

/diff?before=ID&after=ID&kind=privacy compares two records within the same authorized store/site (kind also accepts cookies and terms). Side-by-side section blocks identify removed/replaced and added content. Title and section-order changes are identified. The view distinguishes:

Version, effective date, review declarations, and full structured documents remain inspectable. Differences do not determine legal materiality, require new acceptance, or invalidate consent. Release history preserves document versions. Cookie-consent evidence capture remains the host's job; the optional terms helpers below bind agreement records to an exact release.

Version-bound terms agreement

The host can opt into registerTermsOffer, setActiveTermsOffer, recordTermsAcceptance, and createTermsAcceptanceStore. These require transactional DO storage and supply no public ingestion or authentication endpoint.

import { registerTermsOffer, setActiveTermsOffer, recordTermsAcceptance,
  createTermsAcceptanceStore } from 'notabenda-cf';

// build-time offer from notabenda/content; release already archived.
const scope = {siteId: 'site-one', scopeId: 'account-terms'};
await registerTermsOffer(ctx.storage, offer, scope);
await setActiveTermsOffer(ctx.storage, offer.offerId, scope);

// In the host's authenticated, CSRF-protected, operation-authorized handler:
const receipt = await recordTermsAcceptance(ctx.storage, body, {
  ...scope, subjectId: authorizedSubject.id, operationId: authorizedOperation.id,
});

// Return this store only from an admin authorization hook:
const agreements = createTermsAcceptanceStore(ctx.storage, {siteId: 'site-one'});

TermsStoreOptions accepts trusted siteId, optional prefix (default notabenda/terms/SITE_ID/), releasePrefix (default notabenda/releases/), and allowPreview (default false). Use disjoint prefixes for separately authorized sites/stores. Registration verifies the archive, exact terms metadata, and offer hash; it never activates or publishes. Activation/deactivation and recording share a transaction boundary. setActiveTermsOffer(storage, null, scope) disables new agreements for that scope.

A request accepts exactly {schemaVersion:1, offerId, requestId, accepted:true}. The host independently supplies subject, intended scope, operation and optional server now: () => Date. Do not copy those authority fields from browser JSON. New attempts require the currently active offer; already committed retries return their original receipt after activation changes. The event ID hashes [siteId, subjectId, requestId], with operation/scope/offer conflicts rejected. Preview needs independent explicit server opt-in, regardless of frontend flags.

Registration payloads are limited to 16 KiB, requests to 2 KiB and events to 64 KiB. Each event stores exact offer wording, document references/digests, host subject/operation, server time and host-received provenance. The event is also its idempotency record; host deletion/retention must coordinate replay protection and operation expiry. No cleanup/deletion service is supplied. Receipt storage is separate from actual signup, purchase or fulfillment.

createTermsAcceptanceStore exposes only matching site events to the existing bounded search/detail UI. Agreement cards and detail context support review by subject, operation, document version and statement. The UI does not infer a human click, identity, contract validity, delivery or operation completion. The host must retain and serve pinned document bytes; the admin Worker does not execute or serve archive HTML. See the repository's terms integration guide for frontend/retry/retention details and legal boundaries.

Optional host-callable helpers include registerConsentNotice, setActiveConsentNotice, recordConsentChoice, getConsentHead and createConsentEvidenceStore. They bind browser reports to exact notice wording and archived documents, use transactional revisions for grants, accept restrictive old-notice revoke-all, and return historical receipts together with the current head on retries. Host authorization and stream resolution remain mandatory. The frontend can opt into a required-receipt gate at construction. The package provides no public ingestion/authentication endpoint or retention service.

See the cookie evidence guide for the complete contract, limits, deployment obligations and remaining work. Run the local example and open the fictional history to inspect fixed reports with purpose cards, notice wording and version references.

The optional notabenda-cf/orders entry retains complete reviews and original producer files, activates quotes with compare-and-swap revisions, and records one admission per host-selected checkout intent. A mandatory local decision returns host reservation writes; those commit atomically with the receipt and replay guard. Exact outcomes remain recoverable after quote expiry or replacement. External payments, fulfillment, contract formation and confirmation need separate host workflows.

createPaidOrderStore supplies small searchable summaries. Return allowOrderArchive:true from your admin authorization hook to allow the full retained-review page and original-file inspection; allowExport remains independent and never follows these large archives. Downloads are attachments, image views are sandboxed, and every request is authorized again. Retirement can fence and purge one intent’s private reviews, files, attempts and event after host eligibility/hold review and external-job fencing. See the paid-order guide for API examples, bounds and recovery requirements.

Routes, security and previews

The optional notabenda-authoring build package accepts Markdown and MDX with YAML frontmatter. Register its private release.archive envelope, rather than the public release.artifact, to retain original source atomically with the rendered release. Source attachments must match declared hashes and byte counts. Ordinary history search does not return raw source. See the authoring guide for integration and size limits.

createDocumentHistory also supplies getSource(releaseId, kind). Authenticated /source?id=RELEASE_ID&kind=terms and /source-diff?before=RELEASE_ID&after=RELEASE_ID&kind=terms routes display escaped original text. They never evaluate stored MDX. Expose this hook only when the caller is authorized to inspect private source. A custom authorized store may omit getSource; source routes then return 404 and their entry links are omitted. Release comparison distinguishes source changes from rendered-body changes, so a component update or source-only edit is visible to reviewers.

The default mount is /admin/notabenda. Routes are the base list, /record?id=..., /diff?before=...&after=...&kind=..., source inspection/comparison routes, and explicitly permitted /export and /export.json routes. Paid-order stores add separately permitted /order-review?id=..., /order-review.json?id=..., /order-asset?id=...&sha256=... and /order-image?id=...&sha256=.... Only GET/HEAD are accepted. Every route is authorized first; errors expose no backend exception or record payload. Responses use no-store/private caching, no-referrer, noindex, framing restrictions, and a restrictive CSP. The UI has no JavaScript or external assets; CSS uses a fresh nonce. All stored text is escaped. Archived record HTML is never executed; original order images are served only through their authorized sandboxed file route.

renderRecordList, renderRecordDetail, and renderRecordDiff are pure presentation exports. They do not authorize requests. Prefer mounting createAdminWorker; direct renderer callers must validate record data with normalizeRecord, supply a clean base path and a 32-character hexadecimal nonce, and set corresponding response security headers.

examples/synthetic.mjs exports syntheticRecords and createSyntheticStorage(records?, prefix?) for a local preview or tests. The fake supports get, put, list, and serialized test transactions. It is not persistent storage and is not an authentication provider. Never replace production authorization with an unconditional demo grant.

npm run build --workspace notabenda-cf
npm run test --workspace notabenda-cf

Tests simulate Worker requests and DO storage, including denied/failed authorization, scope isolation, search pagination, XSS, sanitized failures, atomic history, idempotence, hash validation, and escaped differences. No Cloudflare deployment is performed by these tests.

Recorded processing opt-outs expose registerProcessingNotice, recordProcessingRestriction, getProcessingHead and createProcessingRestrictionStore. Events add restrictions transactionally and retries return the current head. The host supplies stable context, provenance, authorization, retention and downstream enforcement. The admin view shows requested activities, historical cumulative restrictions, current retained state and pinned notice wording.

For guided operator setup, createSetupWorker serves the host-authorized local questionnaire with your same-origin entry module and stylesheet. It accepts GET/HEAD, adds no account system or server answer storage, and exports private notes separately. See guided setup.

Evidence lifecycle

Use createEvidenceLifecycle from trusted host code to retire a fixed consent/processing stream or whole terms subject, apply revisioned holds, and purge matching event payloads in bounded transactional steps. Retirement blocks new commands and historical retries before deletion begins. createEvidenceLifecycleStore plugs hold, purge and restore progress into the read-only admin and export UI.

The host owns authorization, eligibility, scheduling and authoritative retirement manifests outside the backup rollback boundary. Reapply those manifests before restored storage receives traffic. Shared archives and minimal pseudonymous metadata remain; downloaded copies and external data need separate handling. No full-erasure or legal sufficiency claim is implied. Map trusted terminal responses to the frontend context-retired error classes so browser workflows close without retry loops. See the lifecycle guide for target scope, API examples and restore requirements.

Cookie-choice recording also enforces the archived scope guard policy. A new grant for an unavailable purpose fails with scope-restricted; historical receipts remain historical and known older notices can still withdraw. Inspection distinguishes a purpose held off by a guard from a reported decline. Scope declarations and receipts do not establish specialized authorization or legal validity.

Form permissions

Register and activate exact purpose/channel offers with registerFormPermissionOffer and setActiveFormPermissionOffer. Trusted application code records grant/withdrawal commands with recordFormPermission; downstream use reads getFormPermissionState. createFormPermissionStore provides scoped history, search and export with historical action separated from current evaluation. The existing evidence lifecycle supports the permissions family. See the integration guide for host contact-generation binding and remaining limits.

Contract declarations and delivery jobs

registerContractActionDefinition, recordContractDeclaration and createContractDeclarationStore provide a separate archive-bound declaration ledger. Capture commits an exact savable receipt and independent application/acknowledgement jobs atomically. claimContractDelivery / settleContractDelivery expose bounded leased delivery to a host runner; they send nothing themselves. Provider acceptance, recipient delivery and contract fulfillment remain distinct. Declaration-scoped lifecycle waits for draining or explicit host quiescence. See the initial contract-action API and limits.

dispatchContractDeclaration starts both host-configured delivery callbacks independently; dispatchContractDelivery runs one channel for an explicit retry. They bound asynchronous attempts, preserve stable downstream IDs, and separate provider acceptance from settlement uncertainty. They add no provider, auth, scheduler or automatic retry. See delivery runners for the API, result meanings and rehearsal boundaries.

Retained catalog review

An explicitly finalized release can include a digest-bound selected-reference attachment alongside optional Markdown/MDX sources. Pass its archive to recordLegalRelease; public artifacts alone cannot satisfy declared private attachments. createDocumentHistory keeps these snapshots out of ordinary records and exports and exposes getCatalogReview separately. Grant allowCatalogReview: true only after your host authorizes this inspection; source/export permissions do not imply it. The generated starter accepts the independent host grant catalogReview: true. See the release workflow.

Customer confirmations

The optional notabenda-cf/order-confirmations subpath retains complete personally addressed host files and queues a recoverable delivery job. Hosts supply contract facts, reviewed content, a fixed destination version, authentication, provider and scheduling. Admin inspection uses a separate allowOrderConfirmation permission; original files download as inert attachments. Provider acceptance remains distinct from recipient delivery. See the confirmation guide.