@happyvertical/smrt-app-runtime 0.53.3 → 0.53.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/AGENTS.md +83 -0
  2. package/README.md +111 -0
  3. package/dist/application-identity.d.ts +43 -0
  4. package/dist/application-identity.d.ts.map +1 -0
  5. package/dist/chunks/src-D7cM0zHW.js +2552 -0
  6. package/dist/chunks/src-D7cM0zHW.js.map +1 -0
  7. package/dist/deployed-runtime.d.ts +11 -0
  8. package/dist/deployed-runtime.d.ts.map +1 -1
  9. package/dist/direct-membership.d.ts +39 -0
  10. package/dist/direct-membership.d.ts.map +1 -0
  11. package/dist/error-code.d.ts +3 -0
  12. package/dist/error-code.d.ts.map +1 -0
  13. package/dist/index.d.ts +34 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +2 -1831
  16. package/dist/local-mcp-tokens.d.ts +123 -0
  17. package/dist/local-mcp-tokens.d.ts.map +1 -0
  18. package/dist/manifest.json +1 -1
  19. package/dist/operation-lock.d.ts +28 -0
  20. package/dist/operation-lock.d.ts.map +1 -0
  21. package/dist/plain-data.d.ts +27 -0
  22. package/dist/plain-data.d.ts.map +1 -0
  23. package/dist/provider-readiness.d.ts +48 -0
  24. package/dist/provider-readiness.d.ts.map +1 -0
  25. package/dist/smrt-knowledge.json +9 -6
  26. package/dist/stale-reclaim.d.ts +33 -0
  27. package/dist/stale-reclaim.d.ts.map +1 -0
  28. package/dist/state-root.d.ts +53 -0
  29. package/dist/state-root.d.ts.map +1 -0
  30. package/dist/sveltekit/handlers.d.ts +83 -0
  31. package/dist/sveltekit/handlers.d.ts.map +1 -0
  32. package/dist/sveltekit/index.d.ts +20 -0
  33. package/dist/sveltekit/index.d.ts.map +1 -0
  34. package/dist/sveltekit/owner-setup.d.ts +109 -0
  35. package/dist/sveltekit/owner-setup.d.ts.map +1 -0
  36. package/dist/sveltekit/runtime.d.ts +363 -0
  37. package/dist/sveltekit/runtime.d.ts.map +1 -0
  38. package/dist/sveltekit/tenancy.d.ts +59 -0
  39. package/dist/sveltekit/tenancy.d.ts.map +1 -0
  40. package/dist/sveltekit.d.ts +2 -0
  41. package/dist/sveltekit.d.ts.map +1 -0
  42. package/dist/sveltekit.js +807 -0
  43. package/dist/sveltekit.js.map +1 -0
  44. package/dist/writer-lease.d.ts +36 -0
  45. package/dist/writer-lease.d.ts.map +1 -0
  46. package/package.json +23 -9
  47. package/dist/index.js.map +0 -1
package/AGENTS.md CHANGED
@@ -33,6 +33,30 @@ Application infrastructure composition for the validated runtime profiles in
33
33
  A token that cannot claim fails a read-only preflight before role/catalog
34
34
  seeding (#3323); the claim transaction's conditional UPDATE stays the
35
35
  authority for single use, expiry, and concurrency.
36
+ - Local MCP tokens (`mcpTokens`, `openLocalMcpTokenStore`, `smrt app token`):
37
+ owner-bound, scoped, expiring (default 30 d, max 365 d), revocable. Only a
38
+ domain-separated HMAC (application secret) is stored, in the runtime-owned
39
+ `_smrt_local_mcp_tokens` system table (created like the bootstrap table;
40
+ never application schema). Issue refuses scopes the owner lacks; verify
41
+ returns token scopes ∩ live permissions and `null` for anything else.
42
+ - Bearer binding is explicit (`tenantBinding`, set by the auth adapter;
43
+ `direct-membership.ts`): `direct` (local tokens, default hosted resolver)
44
+ needs an active direct membership in exactly the tenant; the default
45
+ `direct-or-inherited` (hosted app mappings) applies
46
+ `isSessionTenantAuthorized`'s rule. The direct row, when present, is pinned
47
+ into `PermissionResolver`, so inheritance never substitutes for it.
48
+ `verify()` principals (and their `scopes` arrays) are frozen;
49
+ `runAsPrincipal` takes a validated, deep-frozen, null-prototype copy of
50
+ the principal before any await (`plain-data.ts`: own data only; poison
51
+ keys `__proto__`/`constructor`/`prototype`, accessors, inherited or
52
+ non-plain data, sparse arrays and functions rejected), authorizes from it,
53
+ and hands `fn` that snapshot. Token `issue()` input is read the same way.
54
+ They carry
55
+ `tenantBinding: 'direct'` and the
56
+ `LOCAL_MCP_TOKEN_PRINCIPAL` brand; `runAsPrincipal` rejects a copy that
57
+ widens the binding, so the two public APIs compose safely.
58
+ Never log or return the token outside `issue()`. The operator store opens an
59
+ uncached connection and takes no writer lease (revocation is per request).
36
60
  - Background jobs and application-defined paid capabilities are default-off.
37
61
  - The embedded runner reuses `TaskRunner`; it is not a second job contract.
38
62
 
@@ -68,6 +92,65 @@ Application infrastructure composition for the validated runtime profiles in
68
92
  - Cloud must keep required tenant context and must never introduce a root or
69
93
  unscoped tenant fallback. RLS remains an explicit deployment/migration choice.
70
94
 
95
+ ## SvelteKit entry (`./sveltekit`)
96
+
97
+ - Server-only subpath; `@sveltejs/kit` is an optional peer and is externalized
98
+ in `vite.config.ts` (kit recognizes `redirect`/`fail` by class identity).
99
+ The root entry must never import kit or Svelte.
100
+ - Without a `runtime` option the profile comes from smrt-config's
101
+ `resolveEffectiveApplicationRuntime(await loadConfig())`, the same rule as
102
+ `smrt app` (#3446): no block → `local`; `null`/`false`/`0`/`''` fail closed.
103
+ - `createSmrtSvelteKitRuntime()` order is fixed: readiness gate → URL tenant
104
+ selection (`locals.selectedTenant*`, never tenant context, headers ignored) →
105
+ verified session (tenant context entered only when
106
+ `isSessionTenantAuthorized()` accepts it, else unauthenticated; never via
107
+ `createSessionHandler`, which re-runs `resolve` on downstream errors) →
108
+ publish `locals.tenantContext` only for that verified tenant.
109
+ - `database-rls` isolation forces `postgresRls: true`, including on
110
+ `skipPaths` (anonymous principal). `resolve` runs at most once;
111
+ session-layer failure before it is a 500 with cleared locals.
112
+ - In an RLS request `classOptions()`/`databaseConfig()` return the
113
+ transaction-bound request db (class `db` overrides win). Anything that
114
+ outlives a request (session service, tenant selector) must use the base
115
+ config, never these.
116
+ - The local writer lease defaults to `defaultWriterLease()` (root
117
+ `acquireWriterLease` over `prepareApplicationStateRoot`, presenting
118
+ `SMRT_OPERATION_INSTANCE`); `acquireWriterLease` replaces it, `false` opts
119
+ out, deployed profiles never take it. Provider readiness stays injected:
120
+ apps pass `createProviderReadinessProbe`; deployed startup fails closed
121
+ without `providerReadiness`.
122
+ - `getCollection()` builds from `classOptions()` on every call (RLS request db
123
+ inside a request); each generated route's embedded prelude imports the
124
+ app's `smrt.ts` and reaches it through core's
125
+ `createGeneratedCollectionAccess()` (a legacy accessor export wins,
126
+ deprecated for one release).
127
+ - Owner setup removes the `smrt app` onboarding hand-off files by default,
128
+ immediately after the claim commits (before cookie serialization or hooks);
129
+ `onOwnerClaimed` is for additional cleanup and
130
+ `removeOnboardingHandoff: false` is the explicit opt-out.
131
+ - Owner setup re-checks loopback peer and loopback URL host per request and
132
+ returns only fixed `{ code, message }` failures.
133
+ - `verifyLocalMcpToken` (local only) and `resolveMcpPrincipal` (hosted only:
134
+ issuer/subject → `oidc_identities` → exactly one active user with exactly
135
+ one active direct membership, else `null`) are the runtime's MCP credential
136
+ bindings; pass the runtime as `createHostedMcpResourceAuth({ runtime })`.
137
+ Token claims never select a tenant.
138
+ - `resolveApplicationId()` / `runtimeConfigurationFingerprint()` must stay
139
+ byte-compatible with process managers (golden vectors in tests).
140
+
141
+ ## Operator state (root entry)
142
+
143
+ - One owner for the state shared by the web process and `smrt app`
144
+ (`@happyvertical/smrt-cli/app` re-exports these): `resolveApplicationStateRoot`
145
+ / `prepareApplicationStateRoot` (private app-bound mode-0700 root and
146
+ marker), `withOperationLock` (`operation.lock`), `acquireWriterLease` /
147
+ `readActiveWriterLease` (`writer.lease`), `reclaimStaleRecord`, and
148
+ `createProviderReadinessProbe` (`SMRT_*_READINESS_MODULE`, resolved from the
149
+ app root with Node's own ESM rules). File names, record formats, and
150
+ messages are a cross-process contract; never change them unilaterally.
151
+ - Tests that hook `node:fs` interleavings must live in this package: from a
152
+ consumer the package is an externalized dependency a mock cannot reach.
153
+
71
154
  ## Public runtime diagnostics
72
155
 
73
156
  - `projectRuntimeDiagnostics()` is the only public diagnostic projection. It
package/README.md CHANGED
@@ -102,6 +102,117 @@ explicitly enabled. With embedded job topology, `createEmbeddedJobRunner()`
102
102
  returns the normal s-m-r-t `TaskRunner`, so the application keeps one enqueue
103
103
  and execution contract without requiring a separate worker service.
104
104
 
105
+ ## SvelteKit entry
106
+
107
+ `@happyvertical/smrt-app-runtime/sveltekit` composes the profile runtime,
108
+ tenant selection, the signed session, and authorized-tenant locals so an app's
109
+ server hooks are a few lines. `@sveltejs/kit` is an optional peer dependency;
110
+ the root entry never imports it.
111
+
112
+ ```ts
113
+ // src/hooks.server.ts
114
+ import { createSmrtSvelteKitRuntime } from '@happyvertical/smrt-app-runtime/sveltekit';
115
+
116
+ export const runtime = createSmrtSvelteKitRuntime({
117
+ prepareDatabase: runApplicationMigrations, // optional, idempotent
118
+ });
119
+ export const { handle, init } = runtime;
120
+ ```
121
+
122
+ ```ts
123
+ // src/app.d.ts
124
+ import type { SmrtRuntimeLocals } from '@happyvertical/smrt-app-runtime/sveltekit';
125
+ declare global {
126
+ namespace App {
127
+ interface Locals extends SmrtRuntimeLocals {}
128
+ }
129
+ }
130
+ ```
131
+
132
+ `init` is the fail-closed startup gate (local runtime or deployed bindings) and
133
+ `handle` waits for it, then runs, in order: URL tenant selection into
134
+ `locals.selectedTenant*` (never tenant context; tenant headers are ignored),
135
+ the signed session, and publication of `locals.tenantContext` only when the
136
+ active context matches the verified session tenant. A session's tenant is
137
+ accepted only for an active direct membership or a legitimately inherited
138
+ one (`isSessionTenantAuthorized()`, the same rule deployed `restoreSession()`
139
+ applies); otherwise the request is unauthenticated (no user, permissions,
140
+ tenant, or tenant context). `database-rls` isolation always runs the request
141
+ RLS transaction (`session.postgresRls` cannot disable it); a
142
+ `session.skipPaths` prefix skips session loading only and still runs inside
143
+ that transaction as an anonymous principal. During such a request
144
+ `runtime.classOptions()` / `runtime.databaseConfig()` return the
145
+ transaction-bound request database (unless the class has its own `db`
146
+ override), so call them per request and never retain the result. Downstream code runs
147
+ at most once: its error propagates unchanged, and a session-layer failure
148
+ before it returns 500 with no authenticated locals. Defaults read `SMRT_APP_ID`, `SMRT_DATA_DIR`, `HOST`, `DATABASE_URL`,
149
+ `TENANT_BASE_DOMAIN`, and `SMRT_BACKGROUND_JOBS`; `smrt.config` `runtime`
150
+ selects the profile (local when absent; a present `null`/`false`/`0`/`''`
151
+ fails closed, as in `smrt app`). Deployed profiles additionally require
152
+ `providerReadiness` probes and fail closed without them. In the local profile
153
+ the runtime holds the single-writer lease shared with `smrt app` operations by
154
+ default (`defaultWriterLease()`: `writer.lease` under
155
+ `prepareApplicationStateRoot()`, presenting `SMRT_OPERATION_INSTANCE`); pass
156
+ `acquireWriterLease` to replace it or `acquireWriterLease: false` to hold none.
157
+ Optional hooks: `onBootstrapInvitation` (present a newly issued setup token),
158
+ `selectTenant`, `session`, and `classOverrides`.
159
+ `runtime.getCollection(className)` returns the registered collection built
160
+ from `runtime.classOptions(className)` on that call. Each generated `/api/*`
161
+ route embeds a prelude that imports the app's `src/lib/server/smrt.ts` and
162
+ resolves collections through its exported `runtime` (core's
163
+ `createGeneratedCollectionAccess()`), so an app's `smrt.ts` is only the
164
+ runtime and its options. A legacy `getCollection`/`getSmrtConfig` export there
165
+ still takes precedence for one release, with a deprecation warning.
166
+
167
+ Mountable routes:
168
+
169
+ ```ts
170
+ // src/routes/api/_runtime/health/+server.ts
171
+ export const GET = createRuntimeHealthHandler(runtime);
172
+ // src/routes/api/_runtime/diagnostics/+server.ts
173
+ export const GET = createRuntimeDiagnosticsHandler({ runtime, toolNames });
174
+ // src/routes/setup/+page.server.ts
175
+ export const { load, actions } = createOwnerSetupPage(runtime);
176
+ // src/routes/+layout.server.ts
177
+ export const load = createSessionLayoutLoad();
178
+ ```
179
+
180
+ Diagnostics authorize (owner role or `runtime_diagnostics.read` on an active,
181
+ session-matching membership) before reading the runtime and return only stable
182
+ `{ schemaVersion: 1, error: { code } }` failures. Owner setup is local-only and
183
+ re-checks on every request that both the peer address and the URL host are
184
+ loopback; its `default` action reads `token`, `name`, `email`, and optional
185
+ `tenantName`, sets the session cookie, and redirects 303, or returns
186
+ `fail(status, { code, message })` with `setup_disabled` (404),
187
+ `setup_unavailable` (403), `setup_invalid_input` (400), or `setup_invalid`
188
+ (400). Claim error text is never returned. After the claim commits, the
189
+ runtime removes the `smrt app setup` / `recover` hand-off files
190
+ (`ONBOARDING_HANDOFF_FILES`: `onboarding.json`, `onboarding-launch.html`) from
191
+ `runtime.applicationStateRoot()`, so `pnpm app:open` stops offering the spent
192
+ invitation; failed claims leave them untouched. `onOwnerClaimed` remains for
193
+ extra app cleanup and `removeOnboardingHandoff: false` opts out.
194
+
195
+ `resolveApplicationId()` and `runtimeConfigurationFingerprint()` (root entry)
196
+ are the canonical app ID and secret-free configuration fingerprint shared by
197
+ the web health route and process managers.
198
+
199
+ The root entry also owns the operator state shared with `smrt app`:
200
+ `prepareApplicationStateRoot()` (private, app-bound state directory),
201
+ `withOperationLock()`, `acquireWriterLease()` / `readActiveWriterLease()`,
202
+ and `createProviderReadinessProbe()`. The SvelteKit runtime takes the writer
203
+ lease itself; a SvelteKit app passes only the readiness probe and its own
204
+ options, without depending on the CLI:
205
+
206
+ ```ts
207
+ // src/lib/server/smrt.ts
208
+ import { createProviderReadinessProbe } from '@happyvertical/smrt-app-runtime';
209
+ import { createSmrtSvelteKitRuntime } from '@happyvertical/smrt-app-runtime/sveltekit';
210
+
211
+ export const runtime = createSmrtSvelteKitRuntime({
212
+ providerReadiness: createProviderReadinessProbe,
213
+ });
214
+ ```
215
+
105
216
  ## Self-hosted and cloud applications
106
217
 
107
218
  The deployed initializer validates the selected profile against concrete,
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Canonical application identity shared by the web process, operator commands,
3
+ * and generated operational surfaces.
4
+ *
5
+ * Every process that manages the same application must derive the same app ID
6
+ * and the same configuration fingerprint, so these helpers live next to
7
+ * `validateApplicationId()` / `encodeApplicationId()` rather than in copied
8
+ * template scripts.
9
+ */
10
+ export interface ResolveApplicationIdOptions {
11
+ /** Directory containing the application's `package.json`. Defaults to cwd. */
12
+ sourceRoot?: string;
13
+ /** Package name to encode instead of reading `package.json`. */
14
+ packageName?: string;
15
+ /** Explicit, strictly validated app ID (for example `SMRT_APP_ID`). */
16
+ explicitId?: string;
17
+ }
18
+ /**
19
+ * Resolve one stable application identity.
20
+ *
21
+ * An explicit ID is validated with {@link validateApplicationId}; otherwise
22
+ * the package name is encoded with {@link encodeApplicationId}.
23
+ */
24
+ export declare function resolveApplicationId(options?: ResolveApplicationIdOptions): string;
25
+ /**
26
+ * Runtime selection the fingerprint covers. Structural so operator tooling
27
+ * can fingerprint a resolved runtime it loaded itself; a
28
+ * {@link ResolvedApplicationRuntime} always satisfies it.
29
+ */
30
+ export interface FingerprintRuntime {
31
+ readonly profile: string;
32
+ readonly providers: unknown;
33
+ }
34
+ /**
35
+ * Secret-safe identity used to reject stale managed processes after profile,
36
+ * provider, database-target, or listener configuration changes.
37
+ *
38
+ * Credentials, query strings, and fragments of `DATABASE_URL` are excluded;
39
+ * only `sslmode` survives. The digest is stable for identical inputs so a
40
+ * process manager and the web process can compare it.
41
+ */
42
+ export declare function runtimeConfigurationFingerprint(runtime: FingerprintRuntime, environment?: Readonly<Record<string, string | undefined>>): string;
43
+ //# sourceMappingURL=application-identity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"application-identity.d.ts","sourceRoot":"","sources":["../src/application-identity.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAQH,MAAM,WAAW,2BAA2B;IAC1C,8EAA8E;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,GAAE,2BAAgC,GACxC,MAAM,CAeR;AAkBD;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED;;;;;;;GAOG;AACH,wBAAgB,+BAA+B,CAC7C,OAAO,EAAE,kBAAkB,EAC3B,WAAW,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAe,GACtE,MAAM,CAyBR"}