@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.
- package/AGENTS.md +83 -0
- package/README.md +111 -0
- package/dist/application-identity.d.ts +43 -0
- package/dist/application-identity.d.ts.map +1 -0
- package/dist/chunks/src-D7cM0zHW.js +2552 -0
- package/dist/chunks/src-D7cM0zHW.js.map +1 -0
- package/dist/deployed-runtime.d.ts +11 -0
- package/dist/deployed-runtime.d.ts.map +1 -1
- package/dist/direct-membership.d.ts +39 -0
- package/dist/direct-membership.d.ts.map +1 -0
- package/dist/error-code.d.ts +3 -0
- package/dist/error-code.d.ts.map +1 -0
- package/dist/index.d.ts +34 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1831
- package/dist/local-mcp-tokens.d.ts +123 -0
- package/dist/local-mcp-tokens.d.ts.map +1 -0
- package/dist/manifest.json +1 -1
- package/dist/operation-lock.d.ts +28 -0
- package/dist/operation-lock.d.ts.map +1 -0
- package/dist/plain-data.d.ts +27 -0
- package/dist/plain-data.d.ts.map +1 -0
- package/dist/provider-readiness.d.ts +48 -0
- package/dist/provider-readiness.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +9 -6
- package/dist/stale-reclaim.d.ts +33 -0
- package/dist/stale-reclaim.d.ts.map +1 -0
- package/dist/state-root.d.ts +53 -0
- package/dist/state-root.d.ts.map +1 -0
- package/dist/sveltekit/handlers.d.ts +83 -0
- package/dist/sveltekit/handlers.d.ts.map +1 -0
- package/dist/sveltekit/index.d.ts +20 -0
- package/dist/sveltekit/index.d.ts.map +1 -0
- package/dist/sveltekit/owner-setup.d.ts +109 -0
- package/dist/sveltekit/owner-setup.d.ts.map +1 -0
- package/dist/sveltekit/runtime.d.ts +363 -0
- package/dist/sveltekit/runtime.d.ts.map +1 -0
- package/dist/sveltekit/tenancy.d.ts +59 -0
- package/dist/sveltekit/tenancy.d.ts.map +1 -0
- package/dist/sveltekit.d.ts +2 -0
- package/dist/sveltekit.d.ts.map +1 -0
- package/dist/sveltekit.js +807 -0
- package/dist/sveltekit.js.map +1 -0
- package/dist/writer-lease.d.ts +36 -0
- package/dist/writer-lease.d.ts.map +1 -0
- package/package.json +23 -9
- 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"}
|