Skip to content

Node-side support for lofi's opt-in store provisioning (lofi#109): provision-scoped tickets, store-status preflight, gate parity #2

Description

@FelineStateMachine

Context

lofi#109 (FelineStateMachine/lofi#109) defines opt-in, slice-scoped store provisioning on top of the user-supplied sync location: a lofi app classifies the store as ok | no_schema | schema_out_of_date | schema_drift, and — only on explicit user opt-in — creates/updates its own slice via a merge deploy (fetch stored head schema verbatim, append/update only its own appslug__* tables, publish schema + createTables/dropTables/renameTables migration + union permissions). The safety invariant is client-side: an app may never touch tables outside its declared namespaces.

The whole flow was validated against a live JazzServer on jazz-tools@2.0.0-alpha.53 (slice A deployed → union deployed with a createTables migration → permissions head advances as a chained bundle → old-hash clients keep working, new-hash clients use both slices). lofi-node already proxies everything the flow needs (…/t/<secret>/apps/<appId>/admin/… reaches the loopback Jazz server; X-Jazz-Admin-Secret still required on top). This issue covers what the node should add so the lofi-side UX matches #109's opt-in framing.

1. Provision-scoped app tickets

Today the opt-in "create/update store" flow requires the user to paste the raw admin secret alongside their sync ticket. lofi#109 tracks this as an open question pointed here: a ticket kind that carries provisioning capability, so possession of one credential is the opt-in.

  • Extend the lofisync1. format (v1 fields are {v, appId, url, label?, node?} — optional-field additions are forward-compatible per docs/app-ticket.md) with an optional scope, e.g. scope: "sync" | "provision", default "sync" when absent so every existing ticket keeps meaning transport-only.
  • ticket issue --provision --label laptop-admin mints one; ticket list shows scope; revocation semantics unchanged (401 / WS 4001).
  • The gate enforces scope by path: admin/catalogue-mutating routes (admin/schemas, admin/migrations, admin/permissions, …) require a provision-scoped ticket. For a provision-scoped ticket the gate injects the X-Jazz-Admin-Secret header from config itself, so the secret never leaves the node. Sync-scoped tickets hitting admin paths get the same 401 shape as invalid tickets (nothing to enumerate).
  • Decide and document whether a provision ticket also grants sync (probably yes — it is a superset), and recommend issuing it per provisioning context, not per device.

2. Ticket-gated store-status preflight

The worst failure surface found experimentally: against a store with no deployed schema, client writes hang indefinitely — no error. lofi's classifier therefore needs a preflight, but the Jazz catalogue read endpoints (/schemas, /schema/<hash>) require the admin secret, which sync-only clients rightly never hold.

Add a lofi-node-owned, ticket-gated (any valid ticket, sync scope included) endpoint, e.g. GET /t/<secret>/store-status, answered by the node itself (it holds the admin secret in config and can query its loopback Jazz server):

{
  "v": 1,
  "appId": "",
  "schema": { "deployed": true, "headHash": "", "permissionsVersion": 3 } // or { "deployed": false }
}
  • Metadata only — never schema contents, never policies, never the admin secret.
  • This is what lets a sync-only lofi client render no_schema / hash-mismatch diagnostics instead of hanging, and prompt the user toward the provisioning opt-in.
  • Document it in docs/app-ticket.md (or a new docs/store-provisioning.md) as part of the contract the lofi side implements against.

3. Gate and tunnel parity for the merge-deploy flow

  • E2E coverage: run the full lofi#109 second-app-joins-an-occupied-store scenario through the gate (ticket transport, provision scope): deploy slice A, merge slice B with its createTables migration, verify the old-hash client keeps syncing and the union client works — mirroring the validated direct-server experiment.
  • Same scenario once through an iroh-paired leaf: provisioning against the leaf must land on the root via the existing HTTP proxy (deploys already do; pin it for the migration + permissions-head sequence).
  • docs/hosting-lofi-apps.md §2 ("deploy the app's schema") gets rewritten around this: runtime provisioning via provision ticket becomes the primary path; the deploy-schema.ts script and the proposed lofi-node deploy CLI verb become the operator-side fallback.

Non-goals

  • Enforcing lofi#109's appslug invariant server-side. The node cannot know which namespaces an app legitimately owns; honesty is a lofi-framework property. A provision ticket holder is trusted with the store — same trust boundary as the admin secret today, minus secret exposure.
  • Multiple Jazz apps per node. One node, one appId, one store whose schema grows slice-by-slice, remains the model.

Acceptance criteria

  • Ticket format v1 + optional scope; issuance/list/revoke CLI; existing tickets unchanged in meaning; fixture strings added for the cross-repo contract test.
  • Gate enforces scope by path and injects the admin secret for provision-scoped requests; sync-scope on admin paths indistinguishable from invalid (401 shape).
  • store-status endpoint, ticket-gated, metadata-only, documented in the app-ticket contract.
  • E2E: slice-merge provisioning through the gate and through an iroh-paired leaf, with old-hash-client continuity asserted.
  • docs/app-ticket.md + docs/hosting-lofi-apps.md updated; lofi#109 open-question checkbox for admin-scoped tickets resolvable against this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions