clone

a deno framework for local-first web apps that open instantly, work offline and sync when users choose

feat: move runtime behind package boundary (#41)

FelineStateMachine committed e9f0f6d
Move storage, sync, identity, PWA, and diagnostics into @nzip/lofi exports; shrink generated apps to author-owned source and package-backed integration.
commit
e9f0f6da511728592378aa8dccf43f3af8e3424b
parent
110e0af90b9c9f607f7d95fcc29edc0b65126e59
author
FelineStateMachine <dami.etoile@gmail.com>
69 files changed, +615 -282
M.gitignore1+
MREADME.md15++-
Mapps/reference/README.md20++--
Dapps/reference/astro.config.ts51------------
Mapps/reference/deno.json3+
Mapps/reference/deno.lock4+-
Dapps/reference/src/_lofi/boot.ts8--
Mapps/reference/src/app.ts9+-
Mapps/reference/src/islands/AccountGate.tsx6+-
Mapps/reference/src/islands/TaskList.tsx6+-
Mapps/reference/src/islands/use-tasks.ts9+-
Rapps/reference/src/_lofi/Shell.astro → apps/reference/src/layouts/Shell.astro6+-
Mapps/reference/src/pages/index.astro8+-
Mapps/reference/tests/author-boundary_test.ts27+++++-
Mapps/reference/tsconfig.json2+-
Mdeno.json6+-
Mdocs/README.md11+-
Mdocs/auth-identity.md6+-
Mdocs/auth.md10+-
Mdocs/data-and-ui.md8+-
Mdocs/devx-contract.md28+++---
Mdocs/getting-started.md24++++-
Mdocs/reference/project-layout.md20+++--
Mdocs/testing.md5+-
Apackage/astro/mod.ts128++++++++++++++++++++++++++++++
Apackage/astro/mod_test.ts23+++++
Mpackage/commands/build.ts21++++-
Mpackage/commands/dev.ts5+-
Mpackage/commands/run_tests.ts2+-
Mpackage/create_core.ts24++---
Mpackage/create_core_test.ts22+++++-
Rapps/reference/src/_lofi/DeviceStatus.tsx → package/preact/DeviceStatus.tsx24+++---
Apackage/preact/mod.ts3+
Rapps/reference/src/_lofi/use-device-capabilities.ts → package/preact/use-device-capabilities.ts10++-
Apackage/runtime/app.ts46+++++++++++
Apackage/runtime/app_test.ts38+++++++++
Rapps/reference/src/_lofi/auth.ts → package/runtime/auth.ts12++-
Rapps/reference/src/_lofi/auth_test.ts → package/runtime/auth_test.ts11+++
Apackage/runtime/boot.ts13+++
Rapps/reference/src/_lofi/config.ts → package/runtime/config.ts10++-
Rapps/reference/src/_lofi/device-capabilities.ts → package/runtime/device-capabilities.ts3+-
Rapps/reference/src/_lofi/device-capabilities_test.ts → package/runtime/device-capabilities_test.ts1+
Rapps/reference/src/_lofi/diagnostics.ts → package/runtime/diagnostics.ts1+
Apackage/runtime/env.d.ts13+++
Rapps/reference/src/_lofi/foreground-recovery.ts → package/runtime/foreground-recovery.ts1+
Rapps/reference/src/_lofi/foreground-recovery_test.ts → package/runtime/foreground-recovery_test.ts1+
Rapps/reference/src/_lofi/inspector.ts → package/runtime/inspector.ts1+
Rapps/reference/src/_lofi/inspector_test.ts → package/runtime/inspector_test.ts1+
Rapps/reference/src/_lofi/lifecycle.ts → package/runtime/lifecycle.ts1+
Apackage/runtime/mod.ts66+++++++++++++++
Rapps/reference/src/_lofi/probe.ts → package/runtime/probe.ts1+
Rapps/reference/src/_lofi/pwa.ts → package/runtime/pwa.ts11+--
Rapps/reference/src/_lofi/pwa_test.ts → package/runtime/pwa_test.ts1+
Rapps/reference/src/_lofi/recovery.ts → package/runtime/recovery.ts1+
Rapps/reference/src/_lofi/recovery_test.ts → package/runtime/recovery_test.ts1+
Rapps/reference/src/_lofi/resource-lifecycle.ts → package/runtime/resource-lifecycle.ts1+
Rapps/reference/src/_lofi/resource-lifecycle_test.ts → package/runtime/resource-lifecycle_test.ts1+
Rapps/reference/src/_lofi/runtime.ts → package/runtime/runtime.ts1+
Rapps/reference/src/_lofi/session.ts → package/runtime/session.ts1+
Rapps/reference/public/sw.js → package/runtime/sw.js1+
Rapps/reference/src/_lofi/table-store.ts → package/runtime/table-store.ts1+
Rapps/reference/src/_lofi/table-store_test.ts → package/runtime/table-store_test.ts1+
Rapps/reference/src/_lofi/test-assert.ts → package/runtime/test-assert.ts1+
Rapps/reference/src/_lofi/ui-mutation.ts → package/runtime/ui-mutation.ts1+
Rapps/reference/src/_lofi/ui-mutation_test.ts → package/runtime/ui-mutation_test.ts1+
Mpackage/starter_template.ts32+-------
Mpackage/testdata/starter.snapshot.json52+++----------
Mpackage/tooling/project.ts6+-
Mpackage/tooling/project_test.ts8+-
M .gitignore +1 -0
M README.md +9 -6
@@ -83,23 +83,26 @@ Keep these current constraints in mind:
8383 8484 ## Project anatomy8585 86-A generated project separates the files you own from the framework runtime:86+A generated project keeps application source separate from the versioned framework package:8787 8888 ```mermaid8989 flowchart TB90- Root["my-app/"] --> Config["deno.json + astro.config.ts"]91- Root --> Public["public/<br/>manifest · service worker · icons"]90+ Root["my-app/"] --> Config["deno.json"]91+ Root --> Public["public/<br/>manifest · icons"]9292 Root --> Src["src/"]9393 Root --> Tests["tests/<br/>your tests + lofi testing contract"]94+ Root --> Framework["@nzip/lofi<br/>versioned framework package"]9495 9596 Src --> Model["schema.ts + permissions.ts<br/>your model and access policy"]9697 Src --> App["app.ts<br/>your application config"]9798 Src --> UI["pages + islands + styles<br/>your product experience"]98- Src --> Runtime["_lofi/<br/>generated runtime—do not edit"]99+ Src --> Layout["layouts/<br/>your document shell"]100+ Framework --> Runtime["storage · identity · sync<br/>PWA · lifecycle · diagnostics"]99101 ```100102 101-Most product work stays in `schema.ts`, `permissions.ts`, `app.ts`, `pages/`, `islands/`, and102-`styles/`. Everything under `src/_lofi/` is generated runtime code.103+Product work stays in `schema.ts`, `permissions.ts`, `app.ts`, `pages/`, `layouts/`, `islands/`, and104+`styles/`. Framework behavior is imported from `@nzip/lofi`; upgrading the package updates runtime105+code without copying it into your source tree.103106 104107 ## Testing local-first behavior105108
M apps/reference/README.md +10 -10
@@ -1,8 +1,7 @@
11 # lofi reference application22 3-This application proves the author-facing lofi workflow while the public `@nzip/lofi` package is4-graduated in layers. M2 extracts the generated-project commands; broader framework API extraction5-remains M4 work. The application is deliberately split into two ownership zones.3+This application proves the author-facing lofi workflow against the public `@nzip/lofi` runtime.4+Framework implementation stays package-owned while the application retains its product source.65 76 ## Author-owned files87
@@ -14,15 +13,16 @@ remains M4 work. The application is deliberately split into two ownership zones.
1413 - `src/styles/` — product styling1514 - `tests/` — product behavior and author-boundary tests1615 17-## Generated and runtime-owned files16+## Package-owned framework1817 19-- `astro.config.ts`, `tsconfig.json`, and `src/env.d.ts` — compatible toolchain wiring20-- `public/manifest.webmanifest`, `public/sw.js`, and `public/favicon.svg` — PWA plumbing21-- `src/_lofi/` — storage, identity, synchronization, lifecycle, diagnostics, and framework adapter18+- `@nzip/lofi` — storage, identity, synchronization, lifecycle, diagnostics, and framework adapter19+- `@nzip/lofi/preact` — optional framework UI such as `DeviceStatus`20+- `.lofi/` — ignored Astro/Jazz integration materialized by framework commands21+- `dist/sw.js` — service worker generated by `deno task build`2222 23-Application work should not require edits in `src/_lofi/`. The M2 generator copies these validated24-runtime-owned files without turning them into public framework APIs. M4 may replace the local seam25-with proven `@nzip/lofi` subpath imports without changing product UI.23+Application work imports supported package APIs and does not carry framework implementation files.24+`public/manifest.webmanifest`, `public/favicon.svg`, `src/layouts/`, and `src/env.d.ts` remain25+application-owned integration and branding surfaces.2626 2727 Browser journeys import readiness, offline, two-client, and sanitized failure-artifact helpers from2828 `@nzip/lofi/testing`. `tests/convergence_e2e_test.ts` is a worked example: two clients add a task
D apps/reference/astro.config.ts +0 -51
@@ -1,51 +0,0 @@
1-import { fileURLToPath } from "node:url";2-import preact from "@astrojs/preact";3-import { defineConfig } from "astro/config";4-import { jazzPlugin } from "jazz-tools/dev/vite";5-6-const workspaceRoot = fileURLToPath(new URL("../../", import.meta.url));7-const schemaDir = fileURLToPath(new URL("./src", import.meta.url));8-const cloudAppId = process.env.JAZZ_APP_ID?.trim();9-const cloudServerUrl = process.env.JAZZ_SERVER_URL?.trim();10-const managedJazzServer = process.env.LOFI_SKIP_JAZZ_MANAGED === "1"11- ? false12- : { allowLocalFirstAuth: true };13-14-if (Boolean(cloudAppId) !== Boolean(cloudServerUrl)) {15- throw new Error(16- "Cloud mode requires both JAZZ_APP_ID and JAZZ_SERVER_URL; set the missing name or remove the partial pair.",17- );18-}19-20-// Jazz's managed Vite plugin reads the VITE_ pair when selecting the server21-// that receives schema, migration, and permission publication. The reference22-// runtime still receives explicit build constants below, so only this public23-// pair enters Vite's client-visible namespace and server secrets remain private.24-if (cloudAppId && cloudServerUrl) {25- process.env.VITE_JAZZ_APP_ID = cloudAppId;26- process.env.VITE_JAZZ_SERVER_URL = cloudServerUrl;27-}28-29-export default defineConfig({30- output: "static",31- integrations: [preact({ compat: true, devtools: true })],32- vite: {33- plugins: [34- jazzPlugin({35- schemaDir,36- inspector: false,37- server: managedJazzServer,38- }),39- ],40- server: {41- fs: { allow: [workspaceRoot] },42- },43- define: {44- __LOFI_JAZZ_APP_ID__: JSON.stringify(cloudAppId ?? ""),45- __LOFI_JAZZ_SERVER_URL__: JSON.stringify(cloudServerUrl ?? ""),46- },47- resolve: {48- noExternal: ["@astrojs/preact"],49- },50- },51-});
M apps/reference/deno.json +3 -0
M apps/reference/deno.lock +2 -2
D apps/reference/src/_lofi/boot.ts +0 -8
M apps/reference/src/app.ts +5 -4
M apps/reference/src/islands/AccountGate.tsx +3 -3
M apps/reference/src/islands/TaskList.tsx +3 -3
M apps/reference/src/islands/use-tasks.ts +4 -5
R apps/reference/src/_lofi/Shell.astro → apps/reference/src/layouts/Shell.astro +5 -1
M apps/reference/src/pages/index.astro +4 -4
M apps/reference/tests/author-boundary_test.ts +22 -5
@@ -7,19 +7,36 @@ const authorUiFiles = [
77 new URL("../src/islands/TaskList.tsx", import.meta.url),88 ];99 10+const packageConsumers = [11+ new URL("../src/app.ts", import.meta.url),12+ new URL("../src/pages/index.astro", import.meta.url),13+ new URL("../src/layouts/Shell.astro", import.meta.url),14+ new URL("../src/islands/AccountGate.tsx", import.meta.url),15+ new URL("../src/islands/TaskList.tsx", import.meta.url),16+ new URL("../src/islands/use-tasks.ts", import.meta.url),17+];18+1019 const forbidden = [1120 { pattern: /jazz-tools/, name: "raw Jazz import" },1221 { pattern: /(?:Shared|Service|Web)?Worker\s*\(/, name: "worker construction" },1322 { pattern: /(?:wss?|https?):\/\//, name: "transport URL" },1423 { pattern: /workbox/i, name: "Workbox configuration" },1524 { pattern: /navigator\.|globalThis\.(?:isSecureContext|SharedWorker)/, name: "browser branch" },16- {17- pattern: /_lofi\/(?:boot|config|device-capabilities|probe|pwa|runtime)/,18- name: "runtime plumbing import",19- },2025 ];2126 22-test("author UI stays behind the lofi checklist boundary", async () => {27+test("author source consumes public lofi package seams and hides plumbing from product UI", async () => {28+ try {29+ await Deno.stat(new URL("../src/_lofi", import.meta.url));30+ throw new Error("generated project still contains a vendored src/_lofi runtime");31+ } catch (error) {32+ if (!(error instanceof Deno.errors.NotFound)) throw error;33+ }34+ for (const file of packageConsumers) {35+ const source = await Deno.readTextFile(file);36+ if (/_lofi\//.test(source)) {37+ throw new Error(`${file.pathname} imports vendored framework code`);38+ }39+ }2340 for (const file of authorUiFiles) {2441 const source = await Deno.readTextFile(file);2542 for (const rule of forbidden) {
M apps/reference/tsconfig.json +1 -1
M deno.json +4 -2
@@ -4,11 +4,14 @@
44 "license": "MIT",55 "description": "A local-first mobile PWA meta-framework for Deno: declare a schema, build Preact islands, and ship an installable offline-capable app whose UI always hydrates from local data.",66 "exports": {7+ ".": "./package/runtime/mod.ts",8+ "./astro": "./package/astro/mod.ts",79 "./build": "./package/commands/build.ts",810 "./create": "./package/create.ts",911 "./dev": "./package/commands/dev.ts",1012 "./doctor": "./package/commands/doctor.ts",1113 "./preview": "./package/commands/preview.ts",14+ "./preact": "./package/preact/mod.ts",1215 "./provision": "./package/commands/provision.ts",1316 "./testing": "./package/testing/mod.ts",1417 "./test": "./package/commands/run_tests.ts"
@@ -38,7 +41,7 @@
3841 },3942 "tasks": {4043 "check": "deno fmt --check . && deno lint && deno task test:unit",41- "test:unit": "deno test --allow-env=LOFI_UPDATE_SNAPSHOT,CI --allow-net=127.0.0.1 --allow-read --allow-write=. --allow-run tools package apps/reference/src/_lofi apps/reference/tests",44+ "test:unit": "deno test --allow-env=LOFI_UPDATE_SNAPSHOT,CI --allow-net=127.0.0.1 --allow-read --allow-write=. --allow-run tools package apps/reference/tests",4245 "test:update:create": "LOFI_UPDATE_SNAPSHOT=1 deno test --allow-env=LOFI_UPDATE_SNAPSHOT --allow-read --allow-write=. --allow-run package/create_core_test.ts",4346 "test": "deno task test:unit",4447 "test:browser:install": "deno run -A npm:playwright@1.61.1 install chromium",
@@ -69,7 +72,6 @@
6972 "docs/sync-and-recovery.md",7073 "docs/testing.md",7174 "docs/troubleshooting.md",72- "apps/reference/astro.config.ts",7375 "apps/reference/deno.json",7476 "apps/reference/public",7577 "apps/reference/src",
M docs/README.md +6 -5
M docs/auth-identity.md +3 -3
@@ -65,7 +65,7 @@ is not handed out without confirming the person is present. Stated honestly: thi
6565 **confirmation**, not encryption. The account secret still lives in device storage so the app can6666 open instantly; the passkey does not encrypt it. A device where WebAuthn is unavailable can still6767 back up, and the UI says the reveal was unconfirmed. (For real at-rest encryption of the secret, the68-PRF primitive in `src/_lofi/auth.ts` is available — see below.)68+PRF primitive exported by `@nzip/lofi` is available — see below.)6969 7070 ### Boot flow7171
@@ -77,7 +77,7 @@ PRF primitive in `src/_lofi/auth.ts` is available — see below.)
7777 - **Stop syncing** → detaches the network and returns to local-only; the account and data are7878 untouched, and electing again resumes against the same account.7979 80-The framework side lives in `src/_lofi/session.ts` (`readSession`, `createBackupPasskey`,80+The framework side is exported by `@nzip/lofi` (`readSession`, `createBackupPasskey`,8181 `confirmPhraseAccess`, `revealRecoveryPhrase`, `enableSyncBackup`, `stopSyncBackup`,8282 `restoreFromRecoveryPhrase`); the gate is the author-owned example you can restyle or replace.8383
@@ -106,7 +106,7 @@ it expires. `.env` holds the server-only secrets and is git-ignored.
106106 107107 ## Optional: protecting the secret at rest108108 109-`src/_lofi/auth.ts` is a standalone device-credential primitive: enroll a WebAuthn passkey and109+The package auth runtime is a standalone device-credential primitive: enroll a WebAuthn passkey and110110 derive a credential-bound key (PRF) to encrypt data — including the account secret — **at rest**. It111111 is feature-detected and never faked. It is _not_ the account identity (deriving the account from a112112 credential was removed, since a derived key is a different account and cannot carry local-only data
M docs/auth.md +5 -5
@@ -3,8 +3,8 @@
33 Status: **primitive validated in reference source; `./auth` subpath deferred to a later milestone**\44 Scope: **device-local WebAuthn + PRF at-rest key derivation**55 6-> **Advanced/internal seam.** Generated applications do not expose this as a supported package7-> subpath, and normal product work should not edit `src/_lofi/auth.ts`. Use this document only when6+> **Advanced runtime seam.** Generated applications consume this through the supported root package7+> export, and normal product work should not reimplement it. Use this document only when88 > deliberately evaluating the optional device-credential primitive. Account sync and recovery are99 > documented separately in [Sync and recovery](sync-and-recovery.md).1010
@@ -14,7 +14,7 @@ against — so this module does exactly three things: enroll a device passkey, a
1414 and derive a credential-bound key to encrypt data **at rest**. The generated session runtime handles1515 accounts, recovery phrases, and optional multi-device sync separately.1616 17-The runtime lives at `apps/reference/src/_lofi/auth.ts`.17+The runtime lives at `package/runtime/auth.ts` and is exposed through `@nzip/lofi`.1818 1919 ## What it guarantees2020
@@ -43,7 +43,7 @@ import {
4343 encryptAtRest,4444 enrollDeviceCredential,4545 getAuthCapability,46-} from "./_lofi/auth.ts";46+} from "@nzip/lofi";4747 4848 // 1. Feature-detect before offering enrollment.4949 const capability = await getAuthCapability();
@@ -68,7 +68,7 @@ console.log(credential.id, blob.iv.length, blob.ciphertext.length);
6868 6969 ## Testing7070 71-- **Unit** (`src/_lofi/auth_test.ts`): origin classification, enroll/authenticate, PRF result71+- **Unit** (`package/runtime/auth_test.ts`): origin classification, enroll/authenticate, PRF result7272 handling, error mapping, and the HKDF -> AES-GCM round-trip run without a browser by injecting a7373 fake `CredentialsContainer` and an explicit `rpId`.7474 - **Browser** (`tests/auth_e2e_test.ts` + `@nzip/lofi/testing`'s `withVirtualAuthenticator`): a CDP
M docs/data-and-ui.md +4 -4
M docs/devx-contract.md +14 -14
@@ -40,8 +40,8 @@ deno task dev
4040 ```4141 4242 The JSR surface is one package, `@nzip/lofi`, with one version and publish operation. Its supported43-public surface is the generator, command subpaths, and `./testing`. Runtime modules remain generated44-under `src/_lofi/` until another application proves a stable package seam.43+public surface includes the configured browser runtime, Preact integration, Astro integration,44+generator, command subpaths, and `./testing`. Framework implementations remain package-owned.4545 4646 From there the developer makes a retained local write, reloads it, works offline, runs checks,4747 builds, previews, and opens the same stable secure origin on a physical device.
@@ -156,7 +156,6 @@ the partial JAZZ_APP_ID/JAZZ_SERVER_URL pair to run local-only.
156156 ```text157157 my-app/158158 ├── deno.json159-├── astro.config.ts160159 ├── .env.example161160 ├── public/162161 ├── src/
@@ -164,25 +163,26 @@ my-app/
164163 │ ├── permissions.ts165164 │ ├── app.ts166165 │ ├── pages/166+│ ├── layouts/167167 │ ├── islands/168-│ ├── styles/169-│ └── _lofi/ # generated; not author-edited168+│ └── styles/170169 └── tests/171170 ```172171 173172 The exact layout may change only when verified toolchain behavior requires it or the change removes174-an author-facing concept. `src/_lofi/` is generated machinery, not an author-editing surface.173+an author-facing concept. Ignored `.lofi/` tooling files and `dist/sw.js` are regenerated from the174+pinned package; they are not author-editing surfaces.175175 176176 ## Author boundary177177 178-| Location | Default allowance | M1 qualification |179-| -------------------------- | ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |180-| Product UI/pages/islands | lofi components/hooks and application types only. | Raw Jazz, worker, transport, Workbox, and capability branching are forbidden. |181-| `schema.ts` | Application schema declarations. | Direct pinned Jazz 2 schema declarations are allowed in the M1 prototype until #7 determines whether a lofi-owned schema facade is honest and affordable. |182-| `permissions.ts` | Application access policy next to the schema. | Direct pinned Jazz 2 permission declarations are allowed; Jazz tooling watches this location. |183-| `app.ts` | Named lofi configuration and composition. | Raw vendor setup is allowed only inside spike controls, not the graduated prototype. |184-| Generated/internal runtime | Vendor clients, workers, storage, sync, auth, and PWA wiring. | May change across alpha pins without changing product UI. |185-| Escape-hatch module | Explicitly isolated unsupported vendor access. | Never generated by default; no compatibility promise before framework extraction. |178+| Location | Default allowance | M1 qualification |179+| ------------------------ | ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |180+| Product UI/pages/islands | lofi components/hooks and application types only. | Raw Jazz, worker, transport, Workbox, and capability branching are forbidden. |181+| `schema.ts` | Application schema declarations. | Direct pinned Jazz 2 schema declarations are allowed in the M1 prototype until #7 determines whether a lofi-owned schema facade is honest and affordable. |182+| `permissions.ts` | Application access policy next to the schema. | Direct pinned Jazz 2 permission declarations are allowed; Jazz tooling watches this location. |183+| `app.ts` | Named lofi configuration and composition. | Raw vendor setup is allowed only inside spike controls, not the graduated prototype. |184+| Package runtime | Vendor clients, workers, storage, sync, auth, and PWA wiring. | May change across package pins without changing product UI. |185+| Escape-hatch module | Explicitly isolated unsupported vendor access. | Never generated by default; package upgrades provide no compatibility promise for vendor internals. |186186 187187 ## Safe defaults188188
M docs/getting-started.md +19 -5
@@ -52,13 +52,27 @@ output at `http://127.0.0.1:4321/` by default.
5252 | `src/pages/` | Astro pages and shell composition |5353 | `src/islands/` | Preact hooks and interactive UI |5454 | `src/styles/` | Application styling |55-| `public/` | Manifest, icon, and service-worker assets |55+| `public/` | Manifest, icon, and other product assets |5656 | `tests/` | Application and local-first browser tests |57-| `src/_lofi/` | Generated framework runtime; do not edit |5857 59-Application hooks can import runtime seams from `src/_lofi/`, as the generated `use-tasks.ts` does.60-The boundary means you should not modify the runtime implementation or import Jazz networking and61-storage machinery directly into product components.58+Application hooks import supported runtime seams from `@nzip/lofi`, as the generated `use-tasks.ts`59+does. The boundary keeps Jazz networking and storage machinery out of product components while60+allowing framework fixes to arrive through a package upgrade.61+62+The final package map is intentionally small:63+64+| Import | Supported use |65+| ----------------------------- | --------------------------------------------------- |66+| `@nzip/lofi` | App configuration, runtime, session, stores, PWA |67+| `@nzip/lofi/preact` | Package-owned Preact hooks and diagnostic UI |68+| `@nzip/lofi/astro` | Package-owned Astro/Vite integration |69+| `@nzip/lofi/testing` | Local-first Playwright helpers |70+| `@nzip/lofi/{create,dev,...}` | Generator and generated-project command entrypoints |71+72+All of these imports resolve through the single version pinned in `deno.json`. Upgrade that pin to73+receive framework fixes; do not copy package files into `src/` or import unpublished internal paths.74+Direct vendor access is an unsupported escape hatch: isolate it from ordinary product files and do75+not expect lofi compatibility guarantees for it.6276 6377 ## Customize the application identity first6478
M docs/reference/project-layout.md +11 -9
@@ -3,9 +3,9 @@
33 ```mermaid44 flowchart TB55 Root["my-app/"]6- Root --> Config["Project config<br/>deno.json · astro.config.ts · tsconfig.json"]6+ Root --> Config["Project config<br/>deno.json · tsconfig.json"]77 Root --> Env[".env.example"]8- Root --> Public["public/<br/>favicon · manifest · service worker"]8+ Root --> Public["public/<br/>favicon · manifest"]99 Root --> Src["src/"]1010 Root --> Tests["tests/"]1111
@@ -15,7 +15,8 @@ flowchart TB
1515 Src --> Pages["pages/"]1616 Src --> Islands["islands/"]1717 Src --> Styles["styles/"]18- Src --> Runtime["_lofi/<br/>generated runtime"]18+ Src --> Layouts["layouts/"]19+ Root --> Runtime["@nzip/lofi<br/>versioned runtime"]1920 ```2021 2122 ## Author-owned files
@@ -27,12 +28,13 @@ flowchart TB
2728 - `public/` contains install and static shell assets.2829 - `tests/` contains application tests and worked local-first browser examples.2930 30-## Generated runtime files31+## Versioned framework runtime3132 32-`src/_lofi/` owns durable storage, the Jazz client, account sessions, recovery, lifecycle handling,33-PWA capability gates, diagnostics, and table stores. Do not edit these files during normal product34-work.33+`@nzip/lofi` owns durable storage, the Jazz client, account sessions, recovery, lifecycle handling,34+PWA capability gates, diagnostics, and table stores. These modules are installed once through the35+pinned package version instead of copied into application source.3536 36-The generated domain hook imports selected `_lofi` runtime seams. Follow that pattern when binding a37+The generated domain hook imports selected public package seams. Follow that pattern when binding a3738 new table, but keep vendor setup, storage selection, transports, service-worker logic, and38-capability branching out of product components.39+capability branching out of product components. `deno task dev` and `build` materialize ignored40+Astro integration files under `.lofi/`; `build` emits the service worker into `dist/`.
M docs/testing.md +3 -2
A package/astro/mod.ts +128 -0
@@ -0,0 +1,128 @@
1+import { join, resolve } from "node:path";2+3+export type LofiAstroOptions = {4+ /** Project root. Defaults to the directory from which Astro was invoked. */5+ root?: string;6+ /** Directory containing the Jazz schema and permissions. */7+ schemaDir?: string;8+};9+10+const runtimeFiles = [11+ "app.ts",12+ "auth.ts",13+ "boot.ts",14+ "config.ts",15+ "device-capabilities.ts",16+ "diagnostics.ts",17+ "env.d.ts",18+ "foreground-recovery.ts",19+ "inspector.ts",20+ "lifecycle.ts",21+ "mod.ts",22+ "probe.ts",23+ "pwa.ts",24+ "recovery.ts",25+ "resource-lifecycle.ts",26+ "runtime.ts",27+ "session.ts",28+ "table-store.ts",29+ "ui-mutation.ts",30+] as const;31+32+const preactFiles = ["DeviceStatus.tsx", "mod.ts", "use-device-capabilities.ts"] as const;33+34+async function readPackageFile(path: string): Promise<string> {35+ const url = new URL(path, import.meta.url);36+ if (url.protocol === "file:") return await Deno.readTextFile(url);37+ const response = await fetch(url);38+ if (!response.ok) {39+ throw new Error(`failed to read package module ${path}: HTTP ${response.status}`);40+ }41+ return await response.text();42+}43+44+function renderConfig(projectRoot: string): string {45+ const runtimeEntry = join(projectRoot, ".lofi", "package", "runtime", "mod.ts");46+ const preactEntry = join(projectRoot, ".lofi", "package", "preact", "mod.ts");47+ return `import preact from "@astrojs/preact";48+import { defineConfig } from "astro/config";49+import { jazzPlugin } from "jazz-tools/dev/vite";50+import { resolve } from "node:path";51+52+const projectRoot = Deno.cwd();53+const schemaDir = resolve(projectRoot, "src");54+const cloudAppId = process.env.JAZZ_APP_ID?.trim();55+const cloudServerUrl = process.env.JAZZ_SERVER_URL?.trim();56+const managedJazzServer = process.env.LOFI_SKIP_JAZZ_MANAGED === "1"57+ ? false58+ : { allowLocalFirstAuth: true };59+60+if (Boolean(cloudAppId) !== Boolean(cloudServerUrl)) {61+ throw new Error(62+ "Cloud mode requires both JAZZ_APP_ID and JAZZ_SERVER_URL; set the missing name or remove the partial pair.",63+ );64+}65+66+if (cloudAppId && cloudServerUrl) {67+ process.env.VITE_JAZZ_APP_ID = cloudAppId;68+ process.env.VITE_JAZZ_SERVER_URL = cloudServerUrl;69+}70+71+export default defineConfig({72+ output: "static",73+ integrations: [preact({ compat: true, devtools: true })],74+ vite: {75+ plugins: [jazzPlugin({ schemaDir, inspector: false, server: managedJazzServer })],76+ server: { fs: { allow: [projectRoot] } },77+ define: {78+ __LOFI_JAZZ_APP_ID__: JSON.stringify(cloudAppId ?? ""),79+ __LOFI_JAZZ_SERVER_URL__: JSON.stringify(cloudServerUrl ?? ""),80+ },81+ resolve: {82+ alias: [83+ { find: "@nzip/lofi/preact", replacement: ${JSON.stringify(preactEntry)} },84+ { find: "@nzip/lofi", replacement: ${JSON.stringify(runtimeEntry)} },85+ ],86+ noExternal: ["@astrojs/preact"],87+ },88+ },89+});90+`;91+}92+93+/**94+ * Materializes the package-owned Astro/Jazz integration into an ignored tooling95+ * directory so Astro's Node-compatible config loader can consume it.96+ */97+export async function prepareLofiAstroConfig(options: LofiAstroOptions = {}): Promise<string> {98+ const projectRoot = resolve(options.root ?? Deno.cwd());99+ if (options.schemaDir && options.schemaDir !== "src") {100+ throw new Error("custom schemaDir is not supported by the generated lofi Astro integration");101+ }102+ const toolingDir = join(projectRoot, ".lofi");103+ const path = join(toolingDir, "astro.config.ts");104+ const packageDir = join(toolingDir, "package");105+ await Deno.mkdir(join(packageDir, "runtime"), { recursive: true });106+ await Deno.mkdir(join(packageDir, "preact"), { recursive: true });107+ for (const file of runtimeFiles) {108+ await Deno.writeTextFile(109+ join(packageDir, "runtime", file),110+ await readPackageFile(`../runtime/${file}`),111+ );112+ }113+ for (const file of preactFiles) {114+ await Deno.writeTextFile(115+ join(packageDir, "preact", file),116+ await readPackageFile(`../preact/${file}`),117+ );118+ }119+ const configSource = renderConfig(projectRoot);120+ let current = "";121+ try {122+ current = await Deno.readTextFile(path);123+ } catch (error) {124+ if (!(error instanceof Deno.errors.NotFound)) throw error;125+ }126+ if (current !== configSource) await Deno.writeTextFile(path, configSource);127+ return path;128+}
A package/astro/mod_test.ts +23 -0
M package/commands/build.ts +18 -3
@@ -8,8 +8,9 @@
88 * @module99 */1010 11-import { extname, join } from "node:path";11+import { extname, join, relative } from "node:path";1212 import { LOFI_VERSION } from "../version.ts";13+import { prepareLofiAstroConfig } from "../astro/mod.ts";1314 import { loadEnvironment, serverEnvironmentNames } from "../tooling/environment.ts";1415 import { precacheUrls, scanSecrets, sourceFingerprint, walkFiles } from "../tooling/project.ts";1516 import { runDeno } from "../tooling/process.ts";
@@ -20,9 +21,23 @@ environment.LOFI_SKIP_JAZZ_MANAGED = "1";
2021 environment.JAZZ_ADMIN_SECRET = "";2122 environment.BACKEND_SECRET = "";2223 const sourceHash = (await sourceFingerprint()).slice(0, 12);24+const astroConfig = relative(Deno.cwd(), await prepareLofiAstroConfig());25+26+async function readPackageAsset(path: string): Promise<string> {27+ const url = new URL(path, import.meta.url);28+ if (url.protocol === "file:") return await Deno.readTextFile(url);29+ const response = await fetch(url);30+ if (!response.ok) {31+ throw new Error(`failed to read package asset ${path}: HTTP ${response.status}`);32+ }33+ return await response.text();34+}2335 2436 exitOnFailure(25- await runDeno(["run", "-A", "npm:astro@7.0.9", "build"], environment),37+ await runDeno(38+ ["run", "-A", "npm:astro@7.0.9", "build", "--config", astroConfig],39+ environment,40+ ),2641 "production build",2742 );2843
@@ -33,7 +48,7 @@ await Deno.writeTextFile(
3348 }\n`,3449 );3550 const serviceWorkerPath = "dist/sw.js";36-const serviceWorker = await Deno.readTextFile(serviceWorkerPath);51+const serviceWorker = await readPackageAsset("../runtime/sw.js");3752 await Deno.writeTextFile(3853 serviceWorkerPath,3954 serviceWorker.replace("__LOFI_BUILD_REVISION__", sourceHash),
M package/commands/dev.ts +4 -1
M package/commands/run_tests.ts +1 -1
M package/create_core.ts +10 -14
@@ -21,6 +21,7 @@ const generatedGitignore = `.env
2121 node_modules/2222 .astro/2323 .vite/24+.lofi/2425 dist/2526 coverage/2627 playwright-report/
@@ -61,7 +62,7 @@ on-device account with no sign-in. To enable managed sync and account backup, ru
6162 6263 With a Jazz app configured, the \`AccountGate\` island lets a user back up and sync their local account6364 and recover it from a 24-word recovery phrase. The account they already have carries over — electing64-to sync never changes its identity. See the framework side in \`src/_lofi/session.ts\`.65+to sync never changes its identity. The framework implementation is provided by \`@nzip/lofi\`.6566 6667 Public tasks: \`dev\`, \`doctor\`, \`test\`, \`build\`, and \`preview\`. Sync/backup and schema tasks:6768 \`jazz:provision\`, \`schema:validate\`, \`schema:deploy\`, \`migrations:create\`, and \`migrations:push\`.
@@ -88,10 +89,13 @@ Point them at any other static host by editing those two tasks.
8889 // — npm imports, tasks (including schema/migration/deploy), fmt, lint — is copied8990 // verbatim, so the template is the single source of truth for project config.9091 const lofiImportTargets: Record<string, { subpath: string; localPath: string }> = {92+ "@nzip/lofi": { subpath: "", localPath: "runtime/mod.ts" },93+ "@nzip/lofi/astro": { subpath: "astro", localPath: "astro/mod.ts" },9194 "@nzip/lofi/build": { subpath: "build", localPath: "commands/build.ts" },9295 "@nzip/lofi/dev": { subpath: "dev", localPath: "commands/dev.ts" },9396 "@nzip/lofi/doctor": { subpath: "doctor", localPath: "commands/doctor.ts" },9497 "@nzip/lofi/preview": { subpath: "preview", localPath: "commands/preview.ts" },98+ "@nzip/lofi/preact": { subpath: "preact", localPath: "preact/mod.ts" },9599 "@nzip/lofi/provision": { subpath: "provision", localPath: "commands/provision.ts" },96100 "@nzip/lofi/test": { subpath: "test", localPath: "commands/run_tests.ts" },97101 "@nzip/lofi/testing": { subpath: "testing", localPath: "testing/mod.ts" },
@@ -110,7 +114,11 @@ async function rewritePortableDenoConfig(root: string, packagePrefix: string): P
110114 if (typeof imports[key] !== "string") {111115 throw new Error(`starter deno.json is missing the ${key} import`);112116 }113- imports[key] = isFileOverride ? `${packagePrefix}${localPath}` : `${packagePrefix}${subpath}`;117+ imports[key] = isFileOverride118+ ? `${packagePrefix}${localPath}`119+ : subpath120+ ? `${packagePrefix}${subpath}`121+ : packagePrefix.slice(0, -1);114122 }115123 // The reference app links the local @nzip/lofi package for its dev loop; a116124 // generated project resolves the pinned JSR version directly.
@@ -179,17 +187,6 @@ async function writeTemplate(destination: string): Promise<void> {
179187 }180188 }181189 182-async function rewritePortableAstroConfig(root: string): Promise<void> {183- const path = join(root, "astro.config.ts");184- const source = await Deno.readTextFile(path);185- const portable = source.replace(186- 'const workspaceRoot = fileURLToPath(new URL("../../", import.meta.url));',187- 'const workspaceRoot = fileURLToPath(new URL("./", import.meta.url));',188- );189- if (portable === source) throw new Error("starter Astro workspace-root marker was not found");190- await Deno.writeTextFile(path, portable);191-}192-193190 export async function createProject(options: CreateProjectOptions): Promise<CreateProjectResult> {194191 const segments = validateName(options.name);195192 const cwd = resolve(options.cwd);
@@ -217,7 +214,6 @@ export async function createProject(options: CreateProjectOptions): Promise<Crea
217214 });218215 try {219216 await writeTemplate(staging);220- await rewritePortableAstroConfig(staging);221217 await rewritePortableDenoConfig(staging, options.packagePrefix ?? LOFI_PACKAGE_PREFIX);222218 await Deno.writeTextFile(join(staging, ".gitignore"), generatedGitignore);223219 await Deno.writeTextFile(join(staging, ".env.example"), generatedEnvironment);
M package/create_core_test.ts +20 -2
@@ -99,14 +99,15 @@ Deno.test("createProject materializes the complete starter snapshot", async () =
9999 );100100 }101101 const config = JSON.parse(await Deno.readTextFile(join(result.destination, "deno.json")));102+ assertEquals(config.imports["@nzip/lofi"], "jsr:@nzip/lofi@0.2.0");102103 assertEquals(config.imports["@nzip/lofi/"], "jsr:@nzip/lofi@0.2.0/");103104 assertEquals(config.imports["@nzip/lofi/testing"], "jsr:@nzip/lofi@0.2.0/testing");104105 const lofiSpecifiers = Object.entries(config.imports)105106 .filter(([name]) => name.startsWith("@nzip/lofi/"))106107 .map(([, specifier]) => String(specifier));107108 assert(108- lofiSpecifiers.length === 8,109- `expected one package prefix, six commands, and testing, received ${lofiSpecifiers.length}`,109+ lofiSpecifiers.length === 10,110+ `expected one package prefix, two integrations, six commands, and testing, received ${lofiSpecifiers.length}`,110111 );111112 assert(112113 lofiSpecifiers.every((specifier) => specifier.startsWith("jsr:@nzip/lofi@0.2.0/")),
@@ -120,6 +121,22 @@ Deno.test("createProject materializes the complete starter snapshot", async () =
120121 Deno.test("package manifest and generated version stay coupled", () => {121122 assertEquals(packageManifest.name, "@nzip/lofi");122123 assertEquals(packageManifest.version, LOFI_VERSION);124+ assertEquals(Object.keys(packageManifest.exports).sort(), [125+ ".",126+ "./astro",127+ "./build",128+ "./create",129+ "./dev",130+ "./doctor",131+ "./preact",132+ "./preview",133+ "./provision",134+ "./test",135+ "./testing",136+ ]);137+ assertEquals(packageManifest.exports["."], "./package/runtime/mod.ts");138+ assertEquals(packageManifest.exports["./astro"], "./package/astro/mod.ts");139+ assertEquals(packageManifest.exports["./preact"], "./package/preact/mod.ts");123140 assertEquals(packageManifest.exports["./testing"], "./package/testing/mod.ts");124141 });125142
@@ -169,6 +186,7 @@ Deno.test("createProject accepts an existing empty directory and a local package
169186 packagePrefix: "file:///clean-room/package/",170187 });171188 const config = JSON.parse(await Deno.readTextFile(join(result.destination, "deno.json")));189+ assertEquals(config.imports["@nzip/lofi"], "file:///clean-room/package/runtime/mod.ts");172190 assertEquals(config.imports["@nzip/lofi/"], "file:///clean-room/package/");173191 assert(174192 !await Array.fromAsync(Deno.readDir(result.destination)).then((entries) =>
R apps/reference/src/_lofi/DeviceStatus.tsx → package/preact/DeviceStatus.tsx +13 -11
@@ -1,10 +1,12 @@
1+import type { JSX } from "preact";12 import { useEffect, useState } from "preact/hooks";2-import { serverUrl } from "./config.ts";3+// Package-owned optional diagnostics UI.4+import { serverUrl } from "../runtime/config.ts";35 import { useDeviceCapabilities } from "./use-device-capabilities.ts";4-import { settleUiMutation } from "./ui-mutation.ts";5-import { getPwaState, type PwaState, subscribePwaState } from "./pwa.ts";6-import { runtimeRecreatedEvent } from "./runtime.ts";7-import { readSession, type Session } from "./session.ts";6+import { settleUiMutation } from "../runtime/ui-mutation.ts";7+import { getPwaState, type PwaState, subscribePwaState } from "../runtime/pwa.ts";8+import { runtimeRecreatedEvent } from "../runtime/runtime.ts";9+import { readSession, type Session } from "../runtime/session.ts";810 911 /**1012 * The Device gate: every subsystem's live status, grouped by the system it
@@ -14,7 +16,7 @@ import { readSession, type Session } from "./session.ts";
1416 */1517 1618 // One label/value row inside a category's definition list.17-function Row({ label, value }: { label: string; value: string }) {19+function Row({ label, value }: { label: string; value: string }): JSX.Element {1820 return (1921 <div>2022 <dt>{label}</dt>
@@ -25,7 +27,7 @@ function Row({ label, value }: { label: string; value: string }) {
2527 2628 const available = (present: boolean) => (present ? "available" : "missing");2729 28-export default function DeviceStatus() {30+export default function DeviceStatus(): JSX.Element {2931 const { report, requestPersistence } = useDeviceCapabilities();3032 const [pwa, setPwa] = useState<PwaState>(getPwaState());3133 const [session, setSession] = useState<Session | null>(null);
@@ -63,7 +65,7 @@ export default function DeviceStatus() {
6365 <div class="device-category">6466 <h3>Shared worker &amp; durable storage</h3>6567 <p class="device-hook">66- Hook in: <code>src/_lofi/device-capabilities.ts</code>, <code>runtime.ts</code>68+ Hook in: <code>@nzip/lofi</code> device capabilities and runtime6769 </p>6870 <dl>6971 <Row
@@ -89,7 +91,7 @@ export default function DeviceStatus() {
8991 <div class="device-category">9092 <h3>Data sync</h3>9193 <p class="device-hook">92- Hook in: <code>src/_lofi/config.ts</code>, <code>runtime.ts</code>94+ Hook in: <code>@nzip/lofi</code> session and runtime9395 </p>9496 <dl>9597 <Row label="Sync" value={syncState} />
@@ -106,7 +108,7 @@ export default function DeviceStatus() {
106108 <div class="device-category">107109 <h3>Auth</h3>108110 <p class="device-hook">109- Hook in: <code>src/_lofi/auth.ts</code>, <code>session.ts</code>111+ Hook in: <code>@nzip/lofi</code> auth and session110112 </p>111113 <dl>112114 <Row label="Identity" value="local-first account" />
@@ -119,7 +121,7 @@ export default function DeviceStatus() {
119121 <div class="device-category">120122 <h3>PWA</h3>121123 <p class="device-hook">122- Hook in: <code>src/_lofi/pwa.ts</code>124+ Hook in: <code>@nzip/lofi</code> PWA state123125 </p>124126 <dl>125127 <Row label="Display mode" value={report.displayMode} />
A package/preact/mod.ts +3 -0
R apps/reference/src/_lofi/use-device-capabilities.ts → package/preact/use-device-capabilities.ts +8 -2
A package/runtime/app.ts +46 -0
@@ -0,0 +1,46 @@
1+/** Author-owned configuration consumed by the package runtime. */2+export type LofiAppConfig<Schema = unknown> = {3+ name: string;4+ databaseName: string;5+ schema: Schema;6+ storage: "durable";7+ credentialOrigins?: readonly string[];8+ sync: { adapter: "jazz" };9+ repositoryUrl?: string;10+};11+12+type AppSlot = { config?: LofiAppConfig };13+14+const slotName = "__LOFI_APP_DEFINITION__";15+const browserGlobal = globalThis as typeof globalThis & { [slotName]?: AppSlot };16+17+function slot(): AppSlot {18+ browserGlobal[slotName] ??= {};19+ return browserGlobal[slotName];20+}21+22+/**23+ * Defines the application-facing values the versioned runtime needs.24+ *25+ * The returned value retains the exact schema type so author code can select26+ * tables without importing or editing framework internals.27+ */28+export function defineLofiApp<const Schema>(29+ config: LofiAppConfig<Schema>,30+): LofiAppConfig<Schema> {31+ if (!config.name.trim()) throw new Error("lofi app name must not be empty");32+ if (!config.databaseName.trim()) throw new Error("lofi database name must not be empty");33+ slot().config = config;34+ return config;35+}36+37+/** Package-internal lookup used when browser resources are opened lazily. */38+export function getLofiApp(): LofiAppConfig {39+ const config = slot().config;40+ if (!config) {41+ throw new Error(42+ "lofi app is not defined; export defineLofiApp({...}) from src/app.ts before booting the runtime",43+ );44+ }45+ return config;46+}
A package/runtime/app_test.ts +38 -0
R apps/reference/src/_lofi/auth.ts → package/runtime/auth.ts +7 -5
@@ -1,4 +1,5 @@
11 /**2+// Package-owned identity and device-credential runtime.23 * Device credential auth for local-first apps.34 *45 * Local-first identity is device-local and cryptographic — there is no central
@@ -14,7 +15,7 @@
1415 * @module1516 */1617 17-import { referenceApp } from "../app.ts";18+import { getLofiApp } from "./app.ts";1819 1920 /** Whether the WebAuthn PRF extension can be used on this client. */2021 export type PrfSupport = "available" | "not-reported" | "unknown" | "unavailable";
@@ -180,7 +181,7 @@ function matchesTrustedOrigin(hostname: string, pattern: string): boolean {
180181 */181182 export function classifyCredentialOrigin(182183 url: URL,183- trustedOrigins: readonly string[] = referenceApp.credentialOrigins ?? [],184+ trustedOrigins: readonly string[] = getLofiApp().credentialOrigins ?? [],184185 ): CredentialOriginReport {185186 const rpId = url.hostname;186187 const hostname = canonicalHostname(rpId);
@@ -219,7 +220,7 @@ function currentOrigin(trustedOrigins?: readonly string[]): CredentialOriginRepo
219220 return { status: "blocked", rpId: "", action: "open the application in a browser" };220221 }221222 const url = new URL(location.href);222- const declared = trustedOrigins ?? referenceApp.credentialOrigins ?? [];223+ const declared = trustedOrigins ?? getLofiApp().credentialOrigins ?? [];223224 // Default trust is the origin you are actually served from: the passkey binds224225 // to this hostname (its RP ID) regardless, so trusting it lets the app work225226 // wherever it is deployed. Pin explicit hosts in `app.ts` `credentialOrigins`
@@ -278,13 +279,14 @@ export async function getAuthCapability(
278279 export async function enrollDeviceCredential(279280 options: EnrollOptions = {},280281 ): Promise<DeviceCredential> {282+ const app = getLofiApp();281283 const rpId = requireStableRpId(options);282284 const credentials = options.credentials ?? browserCredentials();283- const label = options.label ?? referenceApp.name;285+ const label = options.label ?? app.name;284286 try {285287 const credential = await credentials.create({286288 publicKey: {287- rp: { id: rpId, name: referenceApp.name },289+ rp: { id: rpId, name: app.name },288290 user: { id: randomBytes(), name: label, displayName: label },289291 challenge: randomBytes(),290292 pubKeyCredParams: [
R apps/reference/src/_lofi/auth_test.ts → package/runtime/auth_test.ts +11 -0
A package/runtime/boot.ts +13 -0
R apps/reference/src/_lofi/config.ts → package/runtime/config.ts +7 -3
R apps/reference/src/_lofi/device-capabilities.ts → package/runtime/device-capabilities.ts +2 -1
R apps/reference/src/_lofi/device-capabilities_test.ts → package/runtime/device-capabilities_test.ts +1 -0
R apps/reference/src/_lofi/diagnostics.ts → package/runtime/diagnostics.ts +1 -0
A package/runtime/env.d.ts +13 -0
R apps/reference/src/_lofi/foreground-recovery.ts → package/runtime/foreground-recovery.ts +1 -0
R apps/reference/src/_lofi/foreground-recovery_test.ts → package/runtime/foreground-recovery_test.ts +1 -0
R apps/reference/src/_lofi/inspector.ts → package/runtime/inspector.ts +1 -0
R apps/reference/src/_lofi/inspector_test.ts → package/runtime/inspector_test.ts +1 -0
R apps/reference/src/_lofi/lifecycle.ts → package/runtime/lifecycle.ts +1 -0
A package/runtime/mod.ts +66 -0
@@ -0,0 +1,66 @@
1+/**2+ * The supported `@nzip/lofi` browser/runtime surface.3+ *4+ * Application code composes these APIs; storage, identity, sync, PWA, and5+ * lifecycle implementations remain versioned package code.6+ *7+ * @module8+ */9+10+export { defineLofiApp, type LofiAppConfig } from "./app.ts";11+export {12+ type AuthCapability,13+ authenticateDeviceCredential,14+ AuthError,15+ classifyCredentialOrigin,16+ type CredentialOriginReport,17+ decryptAtRest,18+ deriveAtRestKey,19+ derivePrfSecret,20+ encryptAtRest,21+ enrollDeviceCredential,22+ getAuthCapability,23+} from "./auth.ts";24+export { bootLofi } from "./boot.ts";25+export {26+ assertDurableBrowser,27+ type DeviceCapabilityReport,28+ DurableStorageUnsupportedError,29+ readDeviceCapabilityReport,30+ requestPersistentStorage,31+} from "./device-capabilities.ts";32+export {33+ applyPwaUpdate,34+ getPwaState,35+ type PwaState,36+ requestPwaInstall,37+ subscribePwaState,38+} from "./pwa.ts";39+export {40+ getRuntime,41+ getRuntimeDiagnostics,42+ type LofiRuntime,43+ recreateRuntime,44+ runtimeRecreatedEvent,45+ shutdownRuntime,46+ subscribeRuntimeDiagnostics,47+} from "./runtime.ts";48+export {49+ confirmPhraseAccess,50+ createBackupPasskey,51+ enableSyncBackup,52+ isAuthError,53+ isRecoveryError,54+ readSession,55+ restoreFromRecoveryPhrase,56+ revealRecoveryPhrase,57+ type Session,58+ stopSyncBackup,59+} from "./session.ts";60+export {61+ type TableHandle,62+ type TableRow,63+ type TableSnapshot,64+ type TableStore,65+} from "./table-store.ts";66+export { settleUiMutation } from "./ui-mutation.ts";
R apps/reference/src/_lofi/probe.ts → package/runtime/probe.ts +1 -0
R apps/reference/src/_lofi/pwa.ts → package/runtime/pwa.ts +4 -7
R apps/reference/src/_lofi/pwa_test.ts → package/runtime/pwa_test.ts +1 -0
R apps/reference/src/_lofi/recovery.ts → package/runtime/recovery.ts +1 -0
R apps/reference/src/_lofi/recovery_test.ts → package/runtime/recovery_test.ts +1 -0
R apps/reference/src/_lofi/resource-lifecycle.ts → package/runtime/resource-lifecycle.ts +1 -0
R apps/reference/src/_lofi/resource-lifecycle_test.ts → package/runtime/resource-lifecycle_test.ts +1 -0
R apps/reference/src/_lofi/runtime.ts → package/runtime/runtime.ts +1 -0
R apps/reference/src/_lofi/session.ts → package/runtime/session.ts +1 -0
R apps/reference/public/sw.js → package/runtime/sw.js +1 -0
R apps/reference/src/_lofi/table-store.ts → package/runtime/table-store.ts +1 -0
R apps/reference/src/_lofi/table-store_test.ts → package/runtime/table-store_test.ts +1 -0
R apps/reference/src/_lofi/test-assert.ts → package/runtime/test-assert.ts +1 -0
R apps/reference/src/_lofi/ui-mutation.ts → package/runtime/ui-mutation.ts +1 -0
R apps/reference/src/_lofi/ui-mutation_test.ts → package/runtime/ui-mutation_test.ts +1 -0
M package/starter_template.ts +1 -31
@@ -6,11 +6,9 @@
66 */77 export const STARTER_FILES: readonly string[] = [88 "deno.json",9- "astro.config.ts",109 "tsconfig.json",1110 "public/favicon.svg",1211 "public/manifest.webmanifest",13- "public/sw.js",1412 "src/app.ts",1513 "src/env.d.ts",1614 "src/permissions.ts",
@@ -20,35 +18,7 @@ export const STARTER_FILES: readonly string[] = [
2018 "src/islands/AccountGate.tsx",2119 "src/islands/TaskList.tsx",2220 "src/islands/use-tasks.ts",23- "src/_lofi/DeviceStatus.tsx",24- "src/_lofi/Shell.astro",25- "src/_lofi/auth.ts",26- "src/_lofi/auth_test.ts",27- "src/_lofi/boot.ts",28- "src/_lofi/config.ts",29- "src/_lofi/session.ts",30- "src/_lofi/diagnostics.ts",31- "src/_lofi/device-capabilities.ts",32- "src/_lofi/device-capabilities_test.ts",33- "src/_lofi/foreground-recovery.ts",34- "src/_lofi/foreground-recovery_test.ts",35- "src/_lofi/inspector.ts",36- "src/_lofi/inspector_test.ts",37- "src/_lofi/lifecycle.ts",38- "src/_lofi/probe.ts",39- "src/_lofi/pwa.ts",40- "src/_lofi/pwa_test.ts",41- "src/_lofi/recovery.ts",42- "src/_lofi/recovery_test.ts",43- "src/_lofi/resource-lifecycle.ts",44- "src/_lofi/resource-lifecycle_test.ts",45- "src/_lofi/runtime.ts",46- "src/_lofi/table-store.ts",47- "src/_lofi/table-store_test.ts",48- "src/_lofi/test-assert.ts",49- "src/_lofi/ui-mutation.ts",50- "src/_lofi/ui-mutation_test.ts",51- "src/_lofi/use-device-capabilities.ts",21+ "src/layouts/Shell.astro",5222 "tests/auth_e2e_test.ts",5323 "tests/author-boundary_test.ts",5424 "tests/convergence_e2e_test.ts",
M package/testdata/starter.snapshot.json +11 -41
@@ -1,53 +1,23 @@
11 {22 ".env.example": "9b21194b6be854a0c3c14ce03e664a81d7102924c3bc3db00e51464df44a1c84",3- ".gitignore": "9efb15c6a9bfc20f296ed712e74273b6015a0dd9a40f7e861b8711dcf61825f1",4- "astro.config.ts": "8a19cae348af7fcbd9db119d2423dd2e464f0e5ace6e5723d06cd3a1cc9a0d4b",5- "deno.json": "74841f3f60de9ea13ae218818a90e79043db2805cdebabbc02224e5b40ee3765",3+ ".gitignore": "9b9fa4b95f8f728ce80e93fc224d430328786b620096b80f067a3190c410a00e",4+ "deno.json": "663cd027bee884c7e5ac6f5607c0a9ef2a0fac922ab94fe228be4f9eeb15c92d",65 "public/favicon.svg": "23efdb888dae688715faafe3ef94f7b5ad1976afcc715c0a742218b54732c6ad",76 "public/manifest.webmanifest": "7d492285fd982e6e68fa04f12d78cd4f0bef4343f0408e314f1582029f3ea407",8- "public/sw.js": "6fa9f4c4b5ef7baabd48256a91520c766a4ffaf75c261686384d6d13e663d0c2",9- "README.md": "51d08f08312c9a555368e1e57a265dad7cc540016d8d4031b7429e6d577e5471",10- "src/_lofi/auth_test.ts": "771789f0bc48f27c95340e7d8dac452cf87a63e536736a00ac006433aa16e83b",11- "src/_lofi/auth.ts": "19c0cfb08f0c00a05421bdcfcbd01ad099e52997793cbf0e89de0e25d385fc4f",12- "src/_lofi/boot.ts": "0f813fbf75d0ba420dcf893e2762ec84be75869b70ef4f197a367c691773474b",13- "src/_lofi/config.ts": "374e8eba1c08cf80bec19cdb35c6ef242ae15cc859700013fec9c92d900c70fe",14- "src/_lofi/device-capabilities_test.ts": "be099ef5f0b3623dc2768b5b588f7e0eb313fea8cf31e4d86b3a52ed1f85cd5a",15- "src/_lofi/device-capabilities.ts": "6911fbe8ce93cdd79531982fdafd505202141985baec874e4dd965b02e6115d3",16- "src/_lofi/DeviceStatus.tsx": "0b5f4480ae544f3183080dbdcaf7d7b0a1030e5ac3433ace78639412b46c98d6",17- "src/_lofi/diagnostics.ts": "abb2a5f8e4d6b8ab1a86d475db6c374aecf4580b534ba9d7d850e1dbb29bb7f8",18- "src/_lofi/foreground-recovery_test.ts": "a5081aecaa70b71c63d721a0875f4d2b3898a49879eb8045c8e74baf0d867444",19- "src/_lofi/foreground-recovery.ts": "a5ed7a65b9454770de62a4ba8cdac7b0d1a867ec3c703049d2c103791c4d0049",20- "src/_lofi/inspector_test.ts": "cc0d5514cf792cf1627f277794dcb6fd511e2d2c8f1e79bda47b24833ce53394",21- "src/_lofi/inspector.ts": "1638b01913c97e01faf86d721128a8d6e1c8df0fbf446c04b25e0e53f944b424",22- "src/_lofi/lifecycle.ts": "0652ce9d4564b830771522986e9d3d6a2afe0849661b43f2d8b4e47ff16c1a1d",23- "src/_lofi/probe.ts": "20011497e8f6f14825fe76348e5b1e96b5e42bc466d8b9a5598cf8e65653ec16",24- "src/_lofi/pwa_test.ts": "bc9c04ff61e2f5a0dfd4601fbab929aa3815f4b174fad600d68b1f74f070d346",25- "src/_lofi/pwa.ts": "9ee6958a63857bb87f9afb58595eb138ef0865fd7c8a4014ac8f90f36813f39a",26- "src/_lofi/recovery_test.ts": "4fffd6ba780389e1411985377f7037e9007b78174fea5e036dc201575a0aa87c",27- "src/_lofi/recovery.ts": "59b5e0a77dae3650611b7b9b807d1e75f749db934f8b5aae7fc34cda72ec56ea",28- "src/_lofi/resource-lifecycle_test.ts": "9a43053351b8b90f3b67a2953c3ff159e0337ab8009f1d061e9e2bf7382abf43",29- "src/_lofi/resource-lifecycle.ts": "4e017f4391f18854486044709041f163308fa3fc84e08d5ec7f45c39e3ae3cb5",30- "src/_lofi/runtime.ts": "a33d7a802d4ae0a5d34efc11f958a25f55f84db23fa3732a468ed427c74eb3ac",31- "src/_lofi/session.ts": "783dc6d4fff855f7e8486b5a2e22db881b02faac809631cc2c509f31daa4a368",32- "src/_lofi/Shell.astro": "a2817146d1fe62cfdc399669709996b468823a8828257abf63b445c4c8eac912",33- "src/_lofi/table-store_test.ts": "1b8cfd06192b9c0daacc8a8c7811b691338f4ef459b209b3830fb87a0a039955",34- "src/_lofi/table-store.ts": "7ad445cc34c11e0cab3afcf1cde097345f1a53f2583da6c7fd00c7cb3e082df8",35- "src/_lofi/test-assert.ts": "38549889964a4e92a0d63ee514c746cae5a43e6160cea4f2075a9e61543d3744",36- "src/_lofi/ui-mutation_test.ts": "16153da95e080570af254eb0c56433f2db8abacec542984f59f6afd4ebf1c501",37- "src/_lofi/ui-mutation.ts": "3273053a796160556045d06d672f4c79d9597e1f822928f1ee71a7d31865dc32",38- "src/_lofi/use-device-capabilities.ts": "f7c7b2321b400d8076a5347e0f1ff2924623535979d48a2d94a69f825b93841e",39- "src/app.ts": "4b4a5c77b92d59e38649e87c28db5890520c31290de15a438b2b67ff8608553f",7+ "README.md": "777d1e5a00bf4501ac83d584b779467910b26dd8fdfc0e15f37c11c968cadb67",8+ "src/app.ts": "be92b21b00f9b737aef6676928573c02f058700058cdf7d8838df5a4933dd26f",409 "src/env.d.ts": "b44daed05ec5cdfacfd8d8acf7866974b5f6b8db923ab53bd244419093c719da",41- "src/islands/AccountGate.tsx": "7355d496180ee45a6144f10b96d1ec39e2d4427ffeab6e574c9b78a601480138",42- "src/islands/TaskList.tsx": "a3d054078819ea8b5609a014e94878bf1b21b2908c340237ed029e80e58f2349",43- "src/islands/use-tasks.ts": "1df5748b14427755352b8438977ce8720abc42f1e2f69bf1e927ceaa52166246",44- "src/pages/index.astro": "6bcbcbd1c1aad8d55855e5f52473b7a7711c439d4877ef54fe7f6f5d4f88ec3c",10+ "src/islands/AccountGate.tsx": "8dcb98ba3f2f7cb1b0a7494b7ac6cec02d65bab1f78fe6e4be88349e6a8227d2",11+ "src/islands/TaskList.tsx": "b38d32ecc08c60e1bef3c70e3e96914df1a2037419253324c4ac48443531f7e7",12+ "src/islands/use-tasks.ts": "2ad1649b64f677bb042861d988f54736e5442bae8f4e335fc0e50f51b2a260f1",13+ "src/layouts/Shell.astro": "50203c979679cfa1f9af461f053db29d8af3e4705497c80233fb94e4ba99c9c5",14+ "src/pages/index.astro": "de9ad86f9b25f7aff1e293931a785dbe393e4d0a35464ffbb649a0ebb4518281",4515 "src/permissions.ts": "dcb00b7f0576d04ad527b8892b8096573b107c3130ed6d835243251c406a38b7",4616 "src/schema.ts": "f2c555b088ded8eea2653f5d955fc9aae69dfd588e891adf509e99052f1e9949",4717 "src/styles/global.css": "27e98368f3fa1c57befc038cc9bf36f800fef192e368f4c393327ac96bfdd448",4818 "tests/auth_e2e_test.ts": "87dc3eb441eeaa96fc82b15df90bf4108096506217e5bfae907b483a3e75b4bf",49- "tests/author-boundary_test.ts": "529bbd81977294398f4c48a3616d10cdcf0fe5f7c83b705365ad80124ab1a0f1",19+ "tests/author-boundary_test.ts": "3f0a605d1b334577d1ec02cf43073ee0c612cad739a16d6f82360919e5de63fb",5020 "tests/convergence_e2e_test.ts": "71ec0ac935c25ff0722905e5b195c730f32a3b886dd4172e11d9e0e80f19cb38",5121 "tests/testing-contract_test.ts": "4257306060d42566b879d92b3a6c395805b20f53717bee55d670c0ef7a117eec",52- "tsconfig.json": "fe2e5acc66101ed42d893d7e7063d07d298927621b351adcfb1ebde6c3a0a2ac"22+ "tsconfig.json": "c632473513c623f3cc69d7476159fb9421624521220029755695defa265c6cab"5323 }
M package/tooling/project.ts +2 -4
M package/tooling/project_test.ts +4 -4