<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>cat/lofi</title>
<subtitle>a deno framework for local-first web apps that open instantly, work offline and sync when users choose</subtitle>
<id>https://src.rest/cat/lofi/</id>
<link href="https://src.rest/cat/lofi/"/>
<updated>2026-10-17T17:05:18-06:00</updated><entry>
<id>dd2de6800e5ad93a4acb1f5a41a17d4f07d89133</id>
<title>chore(ci): run the release gate as src.rest CI</title>
<link href="https://src.rest/cat/lofi/commit/dd2de6800e5ad93a4acb1f5a41a17d4f07d89133/"/>
<author><name>cat</name></author>
<updated>2026-10-17T17:05:18-06:00</updated>
<content type="text">forge attest runs .forge/ci in a clean worktree of each commit and signs the
result into refs/notes/ci; forge push sends it with the branch. It runs what the
pull request gate runs: deno task check, the reference production build, the
publish dry run, and the generated-app golden journey in Chromium.</content>
</entry>
<entry>
<id>683e6fcd51e9c139e9b9c0c99b792efe8f0a42a1</id>
<title>chore(site): build the docs and the reference app under a forge path</title>
<link href="https://src.rest/cat/lofi/commit/683e6fcd51e9c139e9b9c0c99b792efe8f0a42a1/"/>
<author><name>cat</name></author>
<updated>2026-10-17T17:00:50-06:00</updated>
<content type="text">forge site deploy sets FORGE_SITE_URL and FORGE_SITE_BASE; without them the
docs still build for the root of lofi.host exactly as before. Under a path the
docs emit each route as a directory with an index.html, since that host is a
plain file server, and the PWA manifest and head links follow the base.

.forge/sites declares two sites: docs, built by .forge/site-docs the way the
site workflow does (lofi-node at the pinned LOFI_NODE_REF), and app, the
reference app built with LOFI_BASE_PATH set to the deploy path. They deploy to
https://pages.src.rest/sites/cat/lofi/docs/ and .../app/.</content>
</entry>
<entry>
<id>dbd094e2155a7cc2b4c34f6b709336850aa2d0f8</id>
<title>Merge pull request #189 from FelineStateMachine/release/0.9.1</title>
<link href="https://src.rest/cat/lofi/commit/dbd094e2155a7cc2b4c34f6b709336850aa2d0f8/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-07-20T12:38:55-06:00</updated>
<content type="text">release: 0.9.1</content>
</entry>
<entry>
<id>86b5ddcfae429f6eeff29c94fc3ef72bab0e257a</id>
<title>release: 0.9.1</title>
<link href="https://src.rest/cat/lofi/commit/86b5ddcfae429f6eeff29c94fc3ef72bab0e257a/"/>
<author><name>Dami</name></author>
<updated>2026-07-20T12:36:16-06:00</updated>
<content type="text">Publish the enrolled-sync startup fix and pin the site to lofi-node 0.5.1.</content>
</entry>
<entry>
<id>8d803da49be6031420b9a3c813e5cd3445cf6168</id>
<title>fix: restore enrolled sync before runtime startup (#188)</title>
<link href="https://src.rest/cat/lofi/commit/8d803da49be6031420b9a3c813e5cd3445cf6168/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-07-20T12:29:41-06:00</updated>
<content type="text"></content>
</entry>
<entry>
<id>1a7e848ee93aa5b88b78b261247a4689265e9f56</id>
<title>release: 0.9.0 (#187)</title>
<link href="https://src.rest/cat/lofi/commit/1a7e848ee93aa5b88b78b261247a4689265e9f56/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-07-20T04:31:15-06:00</updated>
<content type="text">Bump the package and generated-project contract to 0.9.0.&#92;n&#92;nDocument the encrypted-column type surface so the publication documentation gate passes.</content>
</entry>
<entry>
<id>f6391267d81efbf4c36dda648a1eb91e3f5d5b03</id>
<title>feat(effects): built-in effect library and the custom-unit authoring contract (#184)</title>
<link href="https://src.rest/cat/lofi/commit/f6391267d81efbf4c36dda648a1eb91e3f5d5b03/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-07-20T04:20:31-06:00</updated>
<content type="text">* fix(preact): sync-state quality pass — first-class blocked dispositions, typed enrollment errors, account-gate freshness

The template half of #175, following the runtime half in #180.

DeviceStatus names why nothing is syncing, not merely that it is not:
the one-line Data sync verdict distinguishes owner mismatch, store
refusals (no schema, rejected ticket), an unopenable sink record, and
the absence of any sync location, with owner-mismatch copy naming the
owning account when known. The no-sync hint now leads with ticket
enrollment; the compiled-in env-var path is the stated alternative.

TicketEnrollForm relays typed refusals verbatim — SyncEnrollmentError,
SyncOwnerError, and DataSinkError carry their own remediation, and the
generic paste-again line cannot fix an unprovisioned store or a foreign
sync owner.

AccountGate re-reads the session when the runtime is recreated, gains
the owner-mismatch branch offering exactly the two remediations (stop
sync releases the election, restore adopts the owner), and surfaces a
definite store problem in the backed-up state. Starter mirror and
snapshot regenerated.

* feat(effects): built-in effect library and the custom-unit authoring contract

Closes #141. Grows the #139 effect machinery into a tiered standard library
and formalizes the contract for authoring custom units.

Contract (effects.ts):
- EffectContext gains `op` and `writeCreatedAt` so units can see the operation
  and measure saved→fate latency.
- PermanentEffectError / isPermanentEffectError: a handler throws it to retire
  an obligation immediately (a hopeless failure) instead of re-arming until
  maxAttempts. The ledger honors the severity.
- requireMutationRuntime + anonymousEffectName expose the seams the library is
  built on, keeping every built-in on the public authoring surface.

Built-in units (schema/effect-library.ts), tiered by risk:
- Observation — s.trace (saved→fate span in diagnostics, OTLP-shaped, no vendor
  coupling), s.debug (dev-only timeline, stripped in PROD). s.log stays in the
  core beside the runtime recorder.
- Data-internal — s.notice (durable, UI-agnostic message queue, idempotent by
  journal id; closes &quot;a rejected write flashed success&quot;), s.mark (patch the row
  on fate, convergent, skips a rolled-back insert), s.chain (issue a follow-up
  verb on synced, no saga API).
- External — s.webhook (POST row+fate with journalId as Idempotency-Key; 4xx →
  permanent, 5xx/429/network → retryable; default 24h delivery window).

Runtime:
- notice-queue.ts: durable queue (OPFS→localStorage→memory), idempotent
  enqueue, TTL + dismissal, subscribe; activeNotices diagnostic.
- write-ledger.ts installs recordTrace/recordDebug/enqueueNotice/applyMark and
  owns the queue singleton; public listNotices/subscribeNotices/dismissNotice.
- diagnostics.ts: effectTraces, effectDebugTimeline, activeNotices.

Preact: useNotices hook + Notices component render the durable queue; a toast
stack is a userland wrapper over the same hook.

Docs: docs/effects.md (library reference + authoring contract + worked custom
unit), teaching-path link from nouns-and-verbs, README reference entries.

Tests: per-unit happy/rejection/idempotency, notice-queue idempotence/TTL/
reload, ledger permanent-failure retirement, and a custom unit built purely on
the public contract. Reference app adds s.trace to addTask (dogfood); starter
mirror + snapshot regenerated. Manifest updated for the three new modules.

Deferred, noted for a follow-up: migrating the reference app&#39;s hand-rolled
task-notice to s.notice (a focused island refactor).

* fix(effects): review remediations — durable anonymous-unit identity, notice-queue boot merge, a11y

Addresses findings from the multi-agent + Codex review of #184.

Anonymous-unit identity (HIGH — silent mis-binding across reloads):
- notice/mark/chain no longer take their durable name from a global
  declaration counter (module-load-order dependent, so a re-armed
  obligation could bind to the WRONG unit — for chain, the wrong
  follow-up verb). They are now named `&lt;verb&gt;#&lt;position&gt;` at mutation()
  time, from the author-chosen verb and their slot in its effects — an
  identity independent of which module loads first.
- trace/debug/webhook are content-named and now share one unit per
  identity (cachedBuiltin), so reusing one across verbs aggregates
  instead of throwing a duplicate-name error. webhook keys on url+config.

Notice queue:
- Boot-window clobber (HIGH): load() now MERGES persisted entries with
  any enqueued during the async load window instead of overwriting, so a
  notice an effect enqueues at a boot re-arm is not silently dropped.
- TTL drift (MEDIUM): list() is now a pure read; retirement happens in
  sweep(), wired to a periodic timer in the queue owner; the activeNotices
  count is computed from the live view so it never overcounts expired
  entries.

mark best-effort (MEDIUM): applyMark swallows a denied/vanished-row patch
instead of quarantine-spamming; documented that mark is a convenience over
the status column, not a delivery guarantee (use notice/webhook for that).

Notices a11y (MEDIUM): the aria-live region stays mounted (hidden) when
empty so the first notice is announced; custom children are keyed.

Tests: anonymous-unit verb-scoped naming, content-name sharing across
verbs, notice boot-merge. All green; build refreshed.

* fix(effects): make built-ins replay-safe

* refactor(reference): adopt durable task notices (#185)

* fix(sync): authenticate PoP preflights and align demo notices (#186)

Use proof-of-possession connect URLs for store-status checks during enrollment and boot. Migrate the demo overlay to durable notices and improve recovery/passkey diagnostics.</content>
</entry>
<entry>
<id>0b6a3ec370e662cbe2eb5188c01fb10391b3cf18</id>
<title>fix(preact): sync-state quality pass — first-class blocked dispositions, typed enrollment errors, account-gate freshness (#183)</title>
<link href="https://src.rest/cat/lofi/commit/0b6a3ec370e662cbe2eb5188c01fb10391b3cf18/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-07-19T21:34:00-06:00</updated>
<content type="text">The template half of #175, following the runtime half in #180.

DeviceStatus names why nothing is syncing, not merely that it is not:
the one-line Data sync verdict distinguishes owner mismatch, store
refusals (no schema, rejected ticket), an unopenable sink record, and
the absence of any sync location, with owner-mismatch copy naming the
owning account when known. The no-sync hint now leads with ticket
enrollment; the compiled-in env-var path is the stated alternative.

TicketEnrollForm relays typed refusals verbatim — SyncEnrollmentError,
SyncOwnerError, and DataSinkError carry their own remediation, and the
generic paste-again line cannot fix an unprovisioned store or a foreign
sync owner.

AccountGate re-reads the session when the runtime is recreated, gains
the owner-mismatch branch offering exactly the two remediations (stop
sync releases the election, restore adopts the owner), and surfaces a
definite store problem in the backed-up state. Starter mirror and
snapshot regenerated.</content>
</entry>
<entry>
<id>5409df2b75da162f38dcd46c7f87929dc2dfe840</id>
<title>feat(site): promote the demo to a top-level nav entry in magma dress (#181)</title>
<link href="https://src.rest/cat/lofi/commit/5409df2b75da162f38dcd46c7f87929dc2dfe840/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-07-19T20:55:39-06:00</updated>
<content type="text">The demo moves out of the lofi dropdown to stand beside lofi and
lofi-node, styled after the demo&#39;s own incident-report button: heat
gradient, sharp 2px corners, char text, ember glow on hover, warm focus
ring. Self-contained colors keep it identical across color modes and in
the mobile drawer.</content>
</entry>
<entry>
<id>303cceb2662346acb171f623ae7f317d68e33d87</id>
<title>fix(runtime): sync-state integrity — owner pin, enrollment preflight, guarded stop-sync, reload budget (#180)</title>
<link href="https://src.rest/cat/lofi/commit/303cceb2662346acb171f623ae7f317d68e33d87/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-07-19T20:53:30-06:00</updated>
<content type="text">Per-device sync state (election flag, sealed sink, namespace record) was
scoped to the app alone, with nothing binding it to the account or to the
transport the runtime actually configured. Four fixes close the gaps:

- Sync-owner pin (new sync-owner.ts): electing sync records the electing
  account&#39;s secret fingerprint (the namespace derivation, already
  non-secret). A boot that finds a different account in hand suppresses
  transport instead of writing into the owner&#39;s store, surfacing the
  mismatch through session.syncOwnerMismatch and the syncOwner runtime
  diagnostic; electing under the mismatch throws SyncOwnerError. Stop-sync
  releases the pin; an explicit restore ceremony adopts it.

- Enrollment preflight: enrollSyncTicket now awaits the node&#39;s bounded
  store-status answer before electing. no_schema and ticket_rejected roll
  the sink declaration back, hold no provision capability, and throw
  SyncEnrollmentError; a merely unreachable store enrolls with the warning
  recorded in runtime diagnostics.

- stopSyncBackup only disconnects a transport that was configured
  (runtimeCanReconnect), so a stale-election tab resolves cleanly instead
  of throwing from the vendor client.

- Framework reloads (sync election, principal replacement) draw from a
  per-tab budget (new reload-guard.ts) that a settled boot resets; an
  unbroken reload-without-ready sequence stops with a reload-loop startup
  failure instead of cycling.

Unit tests cover the owner record and adjudication, the reload budget, the
stop-sync repro, and the full enrollment preflight matrix through injected
seams; docs describe the new semantics in sync-and-recovery,
troubleshooting, store-provisioning, and auth-identity.</content>
</entry>
<entry>
<id>ea40241c997fcf0bd99602d43ed46930e59ab4de</id>
<title>chore(deps): clear vite advisories; refresh transitive resolutions (#178)</title>
<link href="https://src.rest/cat/lofi/commit/ea40241c997fcf0bd99602d43ed46930e59ab4de/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-07-19T20:32:05-06:00</updated>
<content type="text">deno audit reported 17 advisories. The five vite findings (path
traversal, server.fs.deny bypasses, WebSocket file read — all dev-server
scoped) came from the direct vite@8.0.1 pin; it now pins 8.1.4, the same
instance the Astro toolchain already resolves, so the vulnerable
duplicate disappears. Regenerating both lockfiles also floated the
in-range OpenTelemetry stable packages.

The remaining 12 findings (protobufjs 8.0.1, @opentelemetry/core 2.7.1,
esbuild 0.27.7) are exact- or range-pinned inside jazz-tools
2.0.0-alpha.53 — the newest alpha — and cannot float: esbuild ^0.27.0
and the OTel experimental ^0.216.0 lines were fixed only in later
majors. None of that code reaches the shipped client bundle (no OTLP
exporter or protobufjs is bundled; esbuild is dev-only), and lofi never
enables the OTLP telemetry path that would load them server-side.
Re-audit on the next jazz-tools bump.</content>
</entry>
<entry>
<id>8c5b010b25a0c5c7fdaa5f5465f45690b585720d</id>
<title>fix: publish-safe generator output, project-name personalization, first-boot secret lock (#177)</title>
<link href="https://src.rest/cat/lofi/commit/8c5b010b25a0c5c7fdaa5f5465f45690b585720d/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-07-19T20:31:47-06:00</updated>
<content type="text">* docs: mark M1 device-matrix deferrals as superseded in spike records

The frozen M1 spike evidence still read as current status: the physical
iPhone/Android install matrix &quot;deferred to M3&quot; and the passkey device
matrix deferred with the rejected alpha mechanism. Both graduated since
(M3 mobile PWA validation; DX-DEVICE-01/DX-DEVICE-UX-01/DX-OFFLINE-01
validated), so each record now carries a supersession note pointing at
the contract instead of inviting re-citation as a live gap.


* fix(create): publish-safe starter sources and project-name personalization

Publishing rewrites the specifiers inside every module in the package, so
the generated starter shipped with direct jsr:/npm: imports that bypassed
the project&#39;s single exact pin and with JSX pragmas collapsed into the
first import line, failing deno fmt --check. The .ts/.tsx starter files
are now also published as verbatim .txt mirrors under package/starter/,
which publishing leaves alone; a test keeps the mirror byte-identical to
the reference app.

createProject now personalizes the copy with the requested project name:
app.ts name and databaseName, the install manifest name/short_name/id,
and the index page title. The reference app&#39;s repository link stays out
of generated projects — the footer renders only when repositoryUrl is
set. The generated README is reflowed to the starter&#39;s fmt style, and
the snapshot test now asserts personalization, rejects direct
jsr:/npm: specifiers in generated sources, and runs deno fmt --check
over the generated tree.


* fix(runtime): serialize account-secret store access behind a Web Lock

The vendor secret store persists the account secret with a localStorage
check-then-write that is not atomic across tabs: two first tabs could
each mint a secret, the last write would win, and the losing tab&#39;s data
would strand under an identity that disappears on reload. First-boot
creation, the session read-through, and recovery&#39;s secret replacement
now run under one exclusive app-scoped Web Lock. The durable capability
gate already requires Web Locks before boot; without a lock manager
(single-process tests) the operation runs directly. The module is
vendored into .lofi/ with the rest of the runtime.


* docs(create): mark the starter mirror as generated

A README in package/starter/ and linguist-generated attributes make the
mirror&#39;s generated status visible to readers and tooling; the byte-
identity test already reverts direct edits. Edits belong in
apps/reference, refreshed via deno task test:update:create.</content>
</entry>
<entry>
<id>879fb711851577b692b233b6de0f1afe8d4ed380</id>
<title>feat(testing): scenario simulation testing — named peers, converge, seeded fuzz (#176)</title>
<link href="https://src.rest/cat/lofi/commit/879fb711851577b692b233b6de0f1afe8d4ed380/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-07-19T19:55:34-06:00</updated>
<content type="text">Expose sync simulation testing as plain test declarations from the
testing entry: scenario() boots a real local sync server, deploys the
app&#39;s schema and permissions, and hands the body real synced peers
(alice/bob) that drive the app&#39;s own tables through offline windows,
concurrent edits, and restarts. converge() settles and diffs replica
views (counter columns by presence; g-set columns order-insensitively);
assertRow/assertNoRow/assertRowCount check one peer&#39;s local view.

scenario.fuzz generates seeded, replayable op sequences across both
peers and asserts convergence against restarted peers and a fresh
canonical reader. scenario.browser carries the same peer controls over
the Playwright two-client fixture; the reference convergence e2e now
dogfoods it.

The scenario sync test pins the live-writer half of update-versus-
delete adjudication: the delete wins for every fresh view while the
session that made the doomed edit keeps it until restart.

New check-gated task test:scenario owns the FFI-needing suite; the pure
fuzz-generator tests run in test:unit.</content>
</entry>
<entry>
<id>0add918b719cb4564ae5781e60d45ba264e51762</id>
<title>release: 0.8.1 — resolve @noble/curves in generated apps (#174)</title>
<link href="https://src.rest/cat/lofi/commit/0add918b719cb4564ae5781e60d45ba264e51762/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-07-19T15:10:10-06:00</updated>
<content type="text">* release: 0.8.1 — resolve @noble/curves in generated apps

The shared-field crypto vendored into generated projects imports
npm:@noble/curves@&lt;v&gt;/ed25519, but the generated Astro config carried no
alias for it, so a clean-room app&#39;s Vite dev server could not resolve the
module and the 0.8.0 registry golden gate failed. The config already
materializes the package at config time; this adds the missing alias line
beside the other noble subpaths. Verified by quarantining the workspace
node_modules copy and running the full golden journey.

* test(astro): pin vendored-import alias coverage statically

Renders the generated Astro config, inventories every import the vendored
.lofi/ tree can produce, resolves each through the root import map, and
asserts two invariants: the alias table matches every rewritten npm:/jsr:
form (both bare and published shapes for self-imports), and every vendored
npm dependency is materialized by a config-time import. Either half missing
previously surfaced only in a clean-room generated app after publish; both
now fail deno task check at PR time.</content>
</entry>
<entry>
<id>f606c9f4005551f9a0093f47e827030a094152f5</id>
<title>release: 0.8.0 — server-blind fields, possession-bound tickets, and the hosted demo (#173)</title>
<link href="https://src.rest/cat/lofi/commit/f606c9f4005551f9a0093f47e827030a094152f5/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-07-19T14:55:59-06:00</updated>
<content type="text"></content>
</entry>
<entry>
<id>6c73e5513cfc7283f51e5e1cae5f3945b5ba3aef</id>
<title>feat: automated demo deploy — demo.lofi.host on each release (#171)</title>
<link href="https://src.rest/cat/lofi/commit/6c73e5513cfc7283f51e5e1cae5f3945b5ba3aef/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-07-19T14:51:58-06:00</updated>
<content type="text">* feat: automated demo deploy to demo.lofi.host on each release

On every successful publish, a workflow generates the starter app from the
exact published JSR version, applies the checked-in demo overlay, builds,
and deploys to Cloudflare Pages. The overlay restyles the starter into its
volcanic opposite and showcases the release surface: s.privateTable
encryption at rest, the strict CSP promoted from _headers.example to a real
response header, and BYOS ticket enrollment on a compiled local-only build.
A guard test pins overlay paths to STARTER_FILES so template renames fail
at PR time. Demo links land in the README, docs, homepage, and site nav.

* feat(demo): restructure the overlay into an incident board

The demo now replaces the starter&#39;s task example the documented way rather
than reskinning it: a new schema (incidents with sealed severity and status
enums over s.privateTable), matching permissions, a three-state board island
with per-row sync fates, and a top-bar strip that measures storage, network,
and unsynced writes live. Account and device panels sit behind disclosures,
provenance moves to the footer, and the page carries no eyebrow or pill
elements. Write confirmations name the store, which is accurate in both
local-only and enrolled-sync modes.

* docs(site): bring both homepages current with the shipped surface

The lofi landing gains three feature cells (server-blind fields, verbs
with effects, the self-defending shell), sink-aware restore in the
lifecycle band, a BYOS cross-link to /node, and an encryption caveat
rewritten from &#39;the server reads unencrypted columns&#39; to the sealing
limits that actually remain. The engine-defect bullet drops its
hardcoded count in favor of the mechanism.

The node landing reframes tickets from &#39;bearer credentials&#39; to the
two-stage posture with device binding, and adds write verdicts and
connection observability to the reference shelf.

* docs(site): align the homepage demo line with the reworked build</content>
</entry>
<entry>
<id>6cdf1ab412769dd8d5d299730317f7d9053767fd</id>
<title>refactor(api): pre-release author-surface pass — entry type closure, internal de-exports, shared-field seam, naming (#169)</title>
<link href="https://src.rest/cat/lofi/commit/6cdf1ab412769dd8d5d299730317f7d9053767fd/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-07-19T14:04:24-06:00</updated>
<content type="text">Every JSR entry now exports every locally-declared type its public
signatures reference, enforced by a new type-closure lint wired into the
check task. Runtime installers, PoP wire internals, shared-field boot
wiring, and test seams leave the author entries; test-facing seams are
re-exported from ./testing instead. The shared-field journey is anchored
across schema/access/runtime with both error families catchable from
./access. WriteHandle&#39;s lifecycle mutators move behind a module-internal
controller so app code cannot forge settlement. Naming consistency:
expiresAfterMs everywhere, TableStore.remove (delete deprecated),
InstalledApp* recipe prefixes, shared-field template kind, completed
is*Error guard family. The schema entries join the entrypoint catalog so
the site&#39;s generated API reference covers them.</content>
</entry>
<entry>
<id>591e3f2497cb781e21cbd1e2ceecc87708f22dd2</id>
<title>docs: correct author-facing docs where they contradict behavior; fix JSR packaging gaps (#167)</title>
<link href="https://src.rest/cat/lofi/commit/591e3f2497cb781e21cbd1e2ceecc87708f22dd2/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-07-19T13:14:05-06:00</updated>
<content type="text">Release documentation pass, first tranche:

- Reattach the `s` JSDoc to its export; it previously attached to a private
  helper and the flagship schema symbol published with no docs.
- Include docs/threat-model.md and docs/store-provisioning.md in the JSR
  publish; published guides already link to both.
- Refresh docs/reference/access.md with the shared encrypted field surface.
- Correct docs that contradicted behavior: passkey enrollment origin gating
  (stable or local-only), astro schemaDir (reserved, src only), testing
  polling option, boot-progress example helper, shared field-key write rules.
- Document the page-reload path of enableSyncBackup / enrollSyncTicket /
  restore flows where the returned promise never settles.
- State the permanent group-creator authority on groupAccess, removeMember&#39;s
  lazy rekey, and share&#39;s upsert semantics.
- Document LofiAppConfig properties, above all credentialOrigins.
- Export AuthenticateOptions from the root entrypoint; remove stray comment
  lines that rendered inside module docs.</content>
</entry>
<entry>
<id>eb6fdba0ccb5424a1ddcea1095cf44c3799b81b5</id>
<title>fix(runtime/preact): surface an unopenable sink record instead of silent local-only (#166)</title>
<link href="https://src.rest/cat/lofi/commit/eb6fdba0ccb5424a1ddcea1095cf44c3799b81b5/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-07-19T12:44:17-06:00</updated>
<content type="text">A persisted sync declaration whose device key is gone restored as
local-only with no signal, unlike a rejected ticket. The runtime now
remembers how the last restore resolved, and DeviceStatus renders the
unopenable state with the same recovery guidance: clear the sync
location and enroll a fresh ticket.</content>
</entry>
<entry>
<id>020549cc84a0888280720a827b9a54320ce06504</id>
<title>feat(runtime): possession-bound sync tickets — device key at enrollment, proof exchange at connect (#165)</title>
<link href="https://src.rest/cat/lofi/commit/020549cc84a0888280720a827b9a54320ce06504/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-07-19T12:36:43-06:00</updated>
<content type="text">Enrollment offers a non-extractable P-256 device key to the node&#39;s
scope-down exchange; a node that accepts binds the derived ticket, and
every boot thereafter answers a fresh challenge before connecting
through a short-lived connect token. A ticket string exfiltrated from
the device then no longer connects on its own. A failed proof boots
without the token and surfaces as a rejected ticket; re-enrollment is
the recovery — key loss is unrecoverable by design.</content>
</entry>
<entry>
<id>e19bbb16848687409e5ddde8f2ccf392f4938778</id>
<title>docs: shared-field encryption — usage, threat model, and decision record (#164)</title>
<link href="https://src.rest/cat/lofi/commit/e19bbb16848687409e5ddde8f2ccf392f4938778/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-07-19T12:25:41-06:00</updated>
<content type="text">Stage 5 of shared-field encryption (#154). The data-and-ui guide gains
the shared-columns section (state-valued reads, the write-path
requirement, lazy rekey, asynchronous key delivery, detection); the
permissions guide covers the two helper tables and their policy touches;
the threat model&#39;s shared-field promise becomes a shipped section with
the residual exposures stated plainly, and the roadmap bullet narrows to
engine-authenticated member keys; the decision record captures the full
construction and its rationale.</content>
</entry>
<entry>
<id>6f4c59107bbac51ba8566b8ae3a5c6a16f23c018</id>
<title>feat(runtime/access): shared-field write path and group key lifecycle (#163)</title>
<link href="https://src.rest/cat/lofi/commit/6f4c59107bbac51ba8566b8ae3a5c6a16f23c018/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-07-19T12:05:59-06:00</updated>
<content type="text">Stage 4 of shared-field encryption (#154). The mutation layer — the only
place that sees the sibling group column naming a row&#39;s key scope — now
seals shared columns with the newest held generation before anything is
journaled or handed to the engine, so the durable journal holds
ciphertext and replayed writes stay sealed; the no-seal path stays fully
synchronous so pending-write observability is unchanged. Writes without
a held key or a resolvable scope refuse loudly.

The group lifecycle mints, delivers, and rotates keys: creation
bootstraps generation 1 wrapped to the creator; an arriving member
receives every held generation, pinned out-of-band by the lofi2
fingerprint their sharing identity carries; removal rotates to a
generation the removed member never receives, wrapped to the remaining
members; reconcileSharedFieldKeys repairs missing wraps. Rotation is
deliberately lazy — the removed member keeps the key material they
already had, and re-encrypting CRDT history is unsound under concurrent
merges; removal protects future content. createGroupOperations wires the
lifecycle in behind optional fieldKeys/directory tables; membership
writes never wait on key delivery.

The lifecycle sync test drives the whole arc against the real server:
bootstrap, sealed generation-1 write, member delivery, watcher unwrap
and ready read, rotation, and the removed member reading old content
while post-rotation content stays pending.</content>
</entry>
<entry>
<id>0c57905cf51886a9b410489e455412896735a4e0</id>
<title>feat(schema/runtime): shared encrypted columns — wrapped keys, keyring, and the read side (#162)</title>
<link href="https://src.rest/cat/lofi/commit/0c57905cf51886a9b410489e455412896735a4e0/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-07-19T11:51:50-06:00</updated>
<content type="text">Stage 3 of shared-field encryption (#154). s.sharedEncryptedText/Json
declare fields sealed under group field keys; reads are state-valued
(ready / pending-key / corrupt) because key-pending is the normal state
of a freshly added member, and a thrown transform would fail the whole
query. The runtime&#39;s wrapped-key watcher verifies each sender through
the pin store, unwraps, and installs keys into the keyring; live queries
resubscribe on keyring change, so pending rows re-materialize into
plaintext with no redefinition — proven end to end against the real
server. Refused wraps and substituted directory keys surface as
diagnostics alerts and install nothing: refusing the key IS the
detection. Writes fail closed at the transform until the mutation layer
seals them (Stage 4).

groupAccess gains the fieldKeys option compiling recipient-or-member
reads and sender-is-self member-scoped writes.

Also pins a significant engine finding uncovered by the read-side test:
on alpha.53, rows whose read policy is an exists-based function
condition never propagate cross-account over sync, while always() and
direct object conditions do (#161). The shared-field design routes
around it — wrapped keys ride a direct recipient condition, the
directory reads always(), and content protection comes from sealing —
and an engine canary fails loudly when a bump fixes propagation.</content>
</entry>
<entry>
<id>0beece2705d2958e52046734f6e89a11f157c8f7</id>
<title>feat(access/runtime): shared-field key directory and account identity (#160)</title>
<link href="https://src.rest/cat/lofi/commit/0beece2705d2958e52046734f6e89a11f157c8f7/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-07-19T11:34:43-06:00</updated>
<content type="text">Stage 2 of shared-field encryption (#154). Every account derives an
x25519 identity from its account secret at boot (distinct HKDF info —
no key material shared with the encrypted-column hierarchy) and can
self-publish its public key in a key-directory table declared with
sharedFieldDirectoryTable and compiled by the sharedFieldAccess
template: world-readable, self-only writes, both enforced against the
real server. A self row holding a different key is surfaced as a
conflict and never overwritten.

Peer fingerprints pin trust-on-first-sight per device, and sharing
identities upgrade to lofi2 with a fingerprint segment once the local
identity exists — members added by identity string are pinned
person-to-person, with no server first-sight window. lofi1 identities
keep decoding.

Also deflakes the encrypted-json no-plaintext probe (short digit runs
occur by chance in base64url ciphertext).</content>
</entry>
<entry>
<id>554498eb174e5a876a7634edbad9dd93a1bad4a4</id>
<title>feat(build): strict Content-Security-Policy for generated apps (#159)</title>
<link href="https://src.rest/cat/lofi/commit/554498eb174e5a876a7634edbad9dd93a1bad4a4/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-07-19T11:26:20-06:00</updated>
<content type="text">Every built page carries a CSP meta tag admitting only the app&#39;s own
hashed scripts — no remote script origins, no inline execution beyond
the hashed island bootstraps — with &#39;wasm-unsafe-eval&#39; for the sync
engine and object-src/base-uri/worker-src locked down. The build unions
the per-page policies into dist/lofi-build.json and a host-header
snippet (dist/_headers.example), reports the shipped policy, and warns
on weakenings without gating; preview serves the union as a real header
on pages and the service worker so the header path is exercised before
deployment.

Tuning rides the environment: LOFI_CSP_SCRIPT_SRC/STYLE_SRC/DIRECTIVES
extend, LOFI_CSP_CONNECT_SRC pins a fixed sink origin (not defaulted —
the sync location is user data), LOFI_CSP=off disables with a report.
CSP is inactive under the Vite dev server by Astro&#39;s design.</content>
</entry>
<entry>
<id>ea2d0afd440a0233668bf8ca07a5327da14059af</id>
<title>feat(schema): shared-field crypto core — authcrypt key wrap and the encs1 sealed format (#158)</title>
<link href="https://src.rest/cat/lofi/commit/ea2d0afd440a0233668bf8ca07a5327da14059af/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-07-19T11:17:32-06:00</updated>
<content type="text">Stage 1 of shared-field encryption (#154). wrap1 seals a random field key
to one recipient&#39;s x25519 public key with the sender&#39;s static key mixed
into the derivation, so the server, which knows every public key, cannot
mint a wrap; forging one requires a member&#39;s secret. encs1 seals column
values under per-column subkeys of the field key, carrying scope and
generation in the stored string so the synchronous open path can pick
the right key from an in-memory keyring. Both formats bind their full
context as associated data; every replay direction refuses.

Adds the pinned @noble/curves dependency to the root and generated-app
import maps and the config-time pin block, and vendors shared-crypto.ts
into generated projects.</content>
</entry>
<entry>
<id>21ae01119d013fff6f30549ed82a569d34dead56</id>
<title>feat(schema): s.privateTable — encrypt-by-default tables with s.plain opt-out (#157)</title>
<link href="https://src.rest/cat/lofi/commit/21ae01119d013fff6f30549ed82a569d34dead56/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-07-19T11:01:57-06:00</updated>
<content type="text">privateTable seals every column by default, deriving labels from the
table&#39;s prefix, and inverts the authoring posture from encrypt-the-
sensitive to expose-what-the-server-needs. Reference columns stay
plaintext automatically (a foreign key the server cannot read cannot
join or gate), byte columns stay plaintext with a report, and s.plain
marks a deliberate plaintext column at the column it affects.

Optionals, defaults, non-lww merge strategies, and transforms on sealed
columns are configuration errors: each would leak plaintext below the
seal boundary or silently discard author intent. Engine conformance
covers the sealed round trip, the plain column&#39;s filterability, and the
zero-match plaintext probe.</content>
</entry>
<entry>
<id>766ae8e65a7a4a03f9a411c1ac568921bbe58f6f</id>
<title>test(access): stage-0 spike for shared-field encryption (#156)</title>
<link href="https://src.rest/cat/lofi/commit/766ae8e65a7a4a03f9a411c1ac568921bbe58f6f/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-07-19T11:01:16-06:00</updated>
<content type="text">Pins the three engine behaviors the shared-field design depends on,
against the real pinned server: a world-readable, self-scoped-write key
directory is expressible (impersonating inserts denied); wrapped-key rows
flow sender to recipient under anyOf/allOf scopes and stay invisible to
non-members (forged senders denied); and column transforms re-run at
materialization, so a sealed read that fails closed before key install
succeeds afterward with no schema redefinition — the pending-to-ready
mechanic shared fields rely on.</content>
</entry>
<entry>
<id>ea54b703e4889e780cedb2dddeddf40f293a6074</id>
<title>feat(schema): compile-time and policy guards for encrypted columns (#155)</title>
<link href="https://src.rest/cat/lofi/commit/ea54b703e4889e780cedb2dddeddf40f293a6074/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-07-19T10:53:23-06:00</updated>
<content type="text">Encrypted columns now carry a branded EncryptedColumn type whose phantom
stored sql type collapses every filter position to never, so a where on an
encrypted column is a compile error; the chaining modifiers are disabled
the same way. The define entry points record encrypted columns into a
registry, and the access layer refuses templates and raw policy rules
that reference one — such a policy would compile but silently never match.

matchDecrypted names the client-side answer: rows reaching the caller are
already decrypted, so predicates run locally after plaintext narrowing.

A type-level test suite pins the branding mechanism against the engine&#39;s
where-input mapping and doubles as the canary for engine bumps.</content>
</entry>
<entry>
<id>ee51c16f8ccbe3c9bc82ffc15febb3cb2e0b85b8</id>
<title>feat(schema): enc2 padded encrypted-column format and sealed number/date scalars (#152)</title>
<link href="https://src.rest/cat/lofi/commit/ee51c16f8ccbe3c9bc82ffc15febb3cb2e0b85b8/"/>
<author><name>FelineStateMachine</name></author>
<updated>2026-07-19T10:41:19-06:00</updated>
<content type="text">Plaintext pads to bucketed sizes (Padmé, 64-byte floor) before sealing, so
stored lengths reveal a size class rather than exact content length. The
format version binds into the associated data, making enc1/enc2 prefix
flips fail authentication in both directions; legacy enc1 values open
indefinitely and re-seal on their next write.

encryptedNumber and encryptedDate seal scalars over the same path; their
TEXT storage sidesteps the pinned i32 integer-column limit. A conformance
finding pins that a plaintext-number where throws in the query adapter
rather than matching zero rows.

Wires encrypted_test.ts into test:unit (previously ran in no CI task) and
adds padding.ts to the vendoring manifest.</content>
</entry>
</feed>
