@happyvertical/smrt-template-sveltekit 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 (49) hide show
  1. package/AGENTS.md +60 -27
  2. package/README.md +23 -16
  3. package/mcp-apps-template/package.dependencies.json +2 -2
  4. package/mcp-apps-template/src/routes/.well-known/oauth-protected-resource/api/mcp/+server.ts +10 -6
  5. package/mcp-apps-template/src/routes/api/mcp/+server.ts +68 -12
  6. package/package.json +19 -15
  7. package/template/AGENTS.md +20 -5
  8. package/template/Dockerfile +0 -1
  9. package/template/INSTALL_PROMPT.md +2 -2
  10. package/template/README.md +86 -40
  11. package/template/compose.yaml +2 -2
  12. package/template/package.json +28 -27
  13. package/template/smrt.config.ts +16 -34
  14. package/template/src/app.d.ts +4 -11
  15. package/template/src/hooks.server.ts +10 -51
  16. package/template/src/lib/server/smrt.ts +22 -98
  17. package/template/src/routes/+layout.server.ts +4 -12
  18. package/template/src/routes/+layout.svelte +27 -99
  19. package/template/src/routes/+page.server.ts +4 -4
  20. package/template/src/routes/api/_runtime/diagnostics/+server.ts +17 -3
  21. package/template/src/routes/api/_runtime/health/+server.ts +4 -28
  22. package/template/src/routes/settings/+page.svelte +3 -34
  23. package/template/src/routes/setup/+page.server.ts +7 -94
  24. package/template/src/routes/setup/+page.svelte +3 -23
  25. package/template/vite.config.ts +5 -43
  26. package/template.config.js +11 -11
  27. package/mcp-apps-template/src/lib/McpAppsBridge.svelte +0 -37
  28. package/mcp-apps-template/src/lib/server/mcp-hosted-principal.ts +0 -12
  29. package/mcp-apps-template/src/lib/server/mcp-hosted.ts +0 -28
  30. package/mcp-apps-template/src/lib/server/mcp.ts +0 -85
  31. package/template/scripts/smrt-app.mjs +0 -885
  32. package/template/scripts/smrt-mcp-apps.mjs +0 -18
  33. package/template/scripts/smrt-operation-lock.mjs +0 -102
  34. package/template/scripts/smrt-portability-assets.mjs +0 -707
  35. package/template/scripts/smrt-portability.mjs +0 -470
  36. package/template/scripts/smrt-prepare-migration.mjs +0 -93
  37. package/template/scripts/smrt-process.mjs +0 -82
  38. package/template/scripts/smrt-provider-readiness.mjs +0 -33
  39. package/template/scripts/smrt-runtime-identity.mjs +0 -245
  40. package/template/scripts/smrt-vite.mjs +0 -45
  41. package/template/scripts/smrt-web.mjs +0 -10
  42. package/template/scripts/smrt-worker.mjs +0 -100
  43. package/template/scripts/smrt-writer-lease.mjs +0 -199
  44. package/template/src/lib/RuntimeDiagnosticsWebMcp.svelte +0 -11
  45. package/template/src/lib/runtime-diagnostics-webmcp.ts +0 -108
  46. package/template/src/lib/server/application-runtime.ts +0 -262
  47. package/template/src/lib/server/runtime-diagnostics.ts +0 -114
  48. package/template/src/lib/server/tenancy.ts +0 -114
  49. package/template/src/smrt-webmcp.d.ts +0 -6
@@ -22,7 +22,9 @@ re-running install, setup, or recovery; each operation is repeatable from that
22
22
  stopped state. `pnpm app:doctor`
23
23
  prints secret-free JSON diagnostics and recovery steps. Individual
24
24
  setup/start/doctor/open/stop/backup/export/import operations are available as
25
- `pnpm app:<operation>`.
25
+ `pnpm app:<operation>`. Every lifecycle script is a one-line call to
26
+ `smrt app <operation>` from `@happyvertical/smrt-cli`; the app ships no copied
27
+ operator scripts, so upgrading the CLI upgrades them.
26
28
 
27
29
  `app:start` defaults adapter-node `ORIGIN` to its loopback HTTP URL (including
28
30
  the selected `PORT`) and preserves an explicitly configured `ORIGIN`. Origin
@@ -58,9 +60,12 @@ The source of truth is `src/lib/objects`. Running `pnpm dev`, `pnpm build`, or
58
60
  | `src/lib/types/smrt-generated/` | Virtual-module and consumer declarations | No |
59
61
  | `src/routes/api/**/+server.ts` | Generated SvelteKit REST routes | No |
60
62
 
61
- Do not edit generated files. `smrtPlugin()` owns local scanning, manifests,
62
- types, and routes. `smrtConsumer()` explicitly consumes the profiles, tenancy,
63
- and users manifests so those models are available to setup and tooling.
63
+ Do not edit generated files. The `smrt()` plugin in `vite.config.ts` owns
64
+ local scanning, manifests, types, routes, and the decorator transform. It
65
+ consumes exactly the packages listed in `smrt.config.ts` under
66
+ `consumer.packages` (profiles, tenancy, and users) so those models are
67
+ available to setup and tooling; add a SMRT package there when the app uses its
68
+ objects.
64
69
 
65
70
  ## 3. Define the first object
66
71
 
@@ -130,22 +135,37 @@ migration command, not by adding schema creation to a request handler.
130
135
 
131
136
  ## 5. Understand tenant context
132
137
 
133
- `src/hooks.server.ts` keeps tenant selection separate from authorization:
138
+ `src/hooks.server.ts` mounts the runtime configured in `src/lib/server/smrt.ts`
139
+ (`createSmrtSvelteKitRuntime()` from `@happyvertical/smrt-app-runtime/sveltekit`).
140
+ It keeps tenant selection separate from authorization:
134
141
 
135
- 1. `src/lib/server/tenancy.ts` reads a subdomain slug and looks up an active
136
- Tenant UUID. It stores the candidate in `locals.selectedTenantId` and
137
- `locals.selectedTenantSlug`.
142
+ 1. A subdomain slug is looked up as an active Tenant UUID and stored only as a
143
+ candidate in `locals.selectedTenantId` and `locals.selectedTenantSlug`.
138
144
  2. That candidate does not enter AsyncLocalStorage and cannot scope queries.
139
- 3. `createSessionHandler({ enterTenantContext: true })` loads the signed session,
140
- resolves its membership and permissions, and establishes the authorized
141
- `locals.tenantId` context.
142
- 4. `enableTenancy()` makes `@TenantScoped` collections honor that context.
143
-
144
- The default resolver ignores `x-tenant-id`. If a gateway supplies a tenant
145
- header, validate the gateway identity/signature before mapping it to a tenant,
146
- and still use `switchSessionTenant()` for browser session changes. That helper
147
- checks active membership and rotates the session ID; never copy an untrusted
148
- header directly into `locals.tenantId` or `enterTenantContext()`.
145
+ 3. The signed session is loaded. Its tenant is accepted only for an active (or
146
+ legitimately inherited) membership, which establishes the authorized
147
+ `locals.tenantId`, the permission set, and the tenant context.
148
+ 4. The runtime enables tenancy, so `@TenantScoped` collections honor that
149
+ context.
150
+
151
+ The default selector ignores `x-tenant-id`. To select tenants from a path,
152
+ signed cookie, or trusted gateway, pass `selectTenant` to
153
+ `createSmrtSvelteKitRuntime()`; selection must still never establish
154
+ authorization. Validate a gateway's identity/signature before mapping its
155
+ header to a tenant, and use `switchSessionTenant()` for browser session
156
+ changes. That helper checks active membership and rotates the session ID; never
157
+ copy an untrusted header directly into `locals.tenantId` or
158
+ `enterTenantContext()`.
159
+
160
+ `src/lib/server/smrt.ts` holds only the runtime and its options. The `smrt()`
161
+ plugin registers the generated objects before it runs, and each generated API
162
+ route imports this module and resolves collections through its exported
163
+ `runtime.getCollection()` (a `getCollection` export here would take precedence,
164
+ deprecated for one release, so do not add one). In the local profile
165
+ the runtime holds the single-writer lease (`acquireWriterLease: false` opts
166
+ out). `runtime.getCollection()` and `runtime.classOptions()` are
167
+ request-scoped (under `database-rls` isolation they carry the request
168
+ transaction): call them inside a request and never keep the result.
149
169
 
150
170
  Set `TENANT_BASE_DOMAIN` for deployed subdomain routing. The fallback parser is
151
171
  only for local shapes such as `acme.demo.local`.
@@ -171,10 +191,10 @@ import {
171
191
  RoleCollection,
172
192
  syncPermissionCatalog,
173
193
  } from '@happyvertical/smrt-users';
174
- import { getSmrtConfig } from '$lib/server/smrt';
194
+ import { runtime } from '$lib/server/smrt';
175
195
 
176
- await syncPermissionCatalog(getSmrtConfig('Permission'));
177
- const roles = await RoleCollection.create(getSmrtConfig('Role'));
196
+ await syncPermissionCatalog(runtime.classOptions('Permission'));
197
+ const roles = await RoleCollection.create(runtime.classOptions('Role'));
178
198
  await roles.seedSystemRoles({ seedPermissions: true });
179
199
  ```
180
200
 
@@ -205,7 +225,7 @@ export const load: PageServerLoad = async ({ depends, locals }) => {
205
225
  return { items: [] };
206
226
  }
207
227
 
208
- const items = await getCollection<Item>('Item');
228
+ const items = await runtime.getCollection<Item>('Item');
209
229
  const rows = await items.list({ limit: 50 });
210
230
  return {
211
231
  items: rows.flatMap((item) =>
@@ -234,10 +254,14 @@ non-object or malformed manifests, credential-shaped JSON fields and credential
234
254
  URL userinfo, non-portable schemas, and non-loopback HTTP server URLs. Diagnostics never
235
255
  include malformed manifest content.
236
256
 
237
- The option stages `src/routes/api/mcp/+server.ts`, a session-authorized `mcp`
238
- server, and a bounded static resource with a restrictive CSP. It also includes
239
- `McpAppsBridge.svelte` for an application-configured trusted host origin and
240
- the optional OpenAI display adapter; it stays inert until your UI mounts it.
257
+ The option stages `src/routes/api/mcp/+server.ts`, one `mountMcpAppRoute()`
258
+ call from `@happyvertical/smrt-app-mcp/sveltekit` that publishes `Item` to
259
+ principals holding `items.read` (the signed session's permissions locally),
260
+ refuses browser requests from a foreign `Origin`, and serves a bounded static
261
+ resource with a restrictive CSP and the optional OpenAI display metadata. It
262
+ also stages the protected-resource metadata route. For a view, mount
263
+ `McpAppsBridge` from `@happyvertical/smrt-svelte/mcp-apps` with an
264
+ application-configured trusted host origin.
241
265
  The scaffold does not enable remote MCP tasks, so a deployment that adds them
242
266
  must supply the durable worker's live authorization callback before publication.
243
267
 
@@ -300,7 +324,7 @@ narrower tool set.
300
324
  ```svelte
301
325
  <script lang="ts">
302
326
  import { webMcpToolDefinitions } from '@happyvertical/smrt-virt-web';
303
- import { Provider } from '@happyvertical/smrt-svelte';
327
+ import { AppShell } from '@happyvertical/smrt-svelte/app';
304
328
 
305
329
  const webmcp = $derived(
306
330
  typeof document !== 'undefined' && 'modelContext' in document
@@ -309,11 +333,17 @@ narrower tool set.
309
333
  );
310
334
  </script>
311
335
 
312
- <Provider {webmcp}>
336
+ <!-- AppShell passes `webmcp` to its Provider unchanged. -->
337
+ <AppShell title="s-m-r-t app" {webmcp} {nav}>
313
338
  {@render children()}
314
- </Provider>
339
+ </AppShell>
315
340
  ```
316
341
 
342
+ The root layout is `AppShell` from `@happyvertical/smrt-svelte/app` (Provider,
343
+ theme and its CSS, the admin shell, and navigation); the app supplies its
344
+ navigation and content. `/setup` and `/settings` use the same package's
345
+ `OwnerSetupForm` and `ShellSettingsPage`.
346
+
317
347
  `registerWebMcpTools()` feature-detects browser support and uses the current
318
348
  authenticated page session. Omitted policy exposes all `read`-effect tools:
319
349
  intrinsic `list`/`get` plus custom actions explicitly declared as reads. To
@@ -399,18 +429,34 @@ Every supported local web
399
429
  entry point (`app:start` or `pnpm dev`) holds a shared writer lease. Direct
400
430
  production startup must set an explicit loopback `HOST`, and `app:start` is the
401
431
  recommended entry point. Stop the app before backup/import. For deployed import, stop
402
- web/workers and set `SMRT_MAINTENANCE_MODE=true`. Extend
403
- `scripts/smrt-portability.mjs` for domain-specific transformations.
432
+ web/workers and set `SMRT_MAINTENANCE_MODE=true`. For domain-specific
433
+ transformations, add a `scripts/smrt-portability.mjs` adapter; when present it
434
+ replaces `smrt app export`/`import`'s built-in one.
435
+
436
+ ### MCP authorization
437
+
438
+ In the `local` profile, the opt-in MCP route accepts two credentials. One is
439
+ the signed browser session. The other is an owner-minted bearer token for a
440
+ local MCP client such as Claude Desktop through `smrt-mcp-bridge`:
441
+
442
+ ```bash
443
+ pnpm exec smrt app token --scopes items.read --label "Claude Desktop"
444
+ pnpm exec smrt app token list
445
+ pnpm exec smrt app token revoke <id>
446
+ ```
404
447
 
405
- ### Hosted MCP authorization
448
+ The token is printed once and only its hash is stored. It is bound to the
449
+ owner and the owner's workspace. It expires after 30 days unless you pass
450
+ `--expires`, up to 365 days. It never carries a permission the owner no longer
451
+ holds. Point the bridge at `/api/mcp` with `<PREFIX>_SERVER_URL` and
452
+ `<PREFIX>_TOKEN`; see the `@happyvertical/smrt-app-cli` README.
406
453
 
407
- The opt-in MCP route uses the local signed session only in the `local` profile.
408
454
  For `self-hosted` and `cloud`, configure HTTPS `SMRT_MCP_RESOURCE`,
409
455
  `SMRT_MCP_ISSUER`, `SMRT_MCP_JWKS_URI`, and space-separated
410
- `SMRT_MCP_SCOPES` values. Bind `resolveHostedMcpPrincipal` in
411
- `src/lib/server/mcp-hosted-principal.ts` to an application-owned lookup that
412
- checks the current account and active tenant membership on every request. It
413
- must return `null` for disabled, revoked, or unmapped identities. The route
414
- does not derive tenant authority from JWT claims, request headers, or tool
415
- arguments; missing configuration or a missing binding fails closed before MCP
416
- dispatch.
456
+ `SMRT_MCP_SCOPES` values. On every request, the runtime maps the verified
457
+ issuer and subject to the user linked through OIDC login. That user must have
458
+ exactly one active tenant membership. Disabled, unmapped, or multi-tenant
459
+ identities are denied. To use a different lookup, pass `resolvePrincipal` to
460
+ `createHostedMcpResourceAuth`. The route never derives tenant authority from
461
+ JWT claims, request headers, or tool arguments. Missing configuration fails
462
+ closed before MCP dispatch.
@@ -16,7 +16,7 @@ services:
16
16
  build: .
17
17
  env_file: .env.self-hosted
18
18
  environment: *runtime-environment
19
- command: node scripts/smrt-worker.mjs task
19
+ command: pnpm exec smrt app worker task
20
20
  depends_on:
21
21
  migrate:
22
22
  condition: service_completed_successfully
@@ -24,7 +24,7 @@ services:
24
24
  build: .
25
25
  env_file: .env.self-hosted
26
26
  environment: *runtime-environment
27
- command: node scripts/smrt-worker.mjs schedule
27
+ command: pnpm exec smrt app worker schedule
28
28
  depends_on:
29
29
  migrate:
30
30
  condition: service_completed_successfully
@@ -9,25 +9,26 @@
9
9
  "pnpm": "11.25.0"
10
10
  },
11
11
  "scripts": {
12
- "dev": "node --env-file-if-exists=.env scripts/smrt-vite.mjs dev",
13
- "build": "node scripts/smrt-mcp-apps.mjs validate-if-present && node --env-file-if-exists=.env scripts/smrt-vite.mjs build",
14
- "preview": "node scripts/smrt-app.mjs start",
12
+ "dev": "smrt app dev",
13
+ "build": "smrt app build",
14
+ "preview": "smrt app start",
15
15
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
16
16
  "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
17
- "db:migrate": "vite build && node scripts/smrt-prepare-migration.mjs",
18
- "app:install": "node scripts/smrt-app.mjs install",
19
- "app:setup": "node scripts/smrt-app.mjs setup",
20
- "app:recover": "node scripts/smrt-app.mjs recover",
21
- "app:start": "node scripts/smrt-app.mjs start",
22
- "app:doctor": "node scripts/smrt-app.mjs doctor",
23
- "app:open": "node scripts/smrt-app.mjs open",
24
- "app:stop": "node scripts/smrt-app.mjs stop",
25
- "app:backup": "node scripts/smrt-app.mjs backup",
26
- "app:export": "node scripts/smrt-app.mjs export",
27
- "app:import": "node scripts/smrt-app.mjs import",
17
+ "typecheck": "svelte-kit sync && tsc --noEmit -p tsconfig.json && svelte-check --tsconfig ./tsconfig.json",
18
+ "db:migrate": "vite build && smrt app migrate",
19
+ "app:install": "smrt app install",
20
+ "app:setup": "smrt app setup",
21
+ "app:recover": "smrt app recover",
22
+ "app:start": "smrt app start",
23
+ "app:doctor": "smrt app doctor",
24
+ "app:open": "smrt app open",
25
+ "app:stop": "smrt app stop",
26
+ "app:backup": "smrt app backup",
27
+ "app:export": "smrt app export",
28
+ "app:import": "smrt app import",
28
29
  "mcp-apps:validate": "pnpm exec smrt mcp-apps validate --output-dir ./mcp-apps",
29
- "worker": "node scripts/smrt-worker.mjs task",
30
- "worker:schedule": "node scripts/smrt-worker.mjs schedule"
30
+ "worker": "smrt app worker task",
31
+ "worker:schedule": "smrt app worker schedule"
31
32
  },
32
33
  "devDependencies": {
33
34
  "@sveltejs/adapter-node": "^5.5.7",
@@ -38,19 +39,19 @@
38
39
  "svelte-check": "^4.7.6",
39
40
  "typescript": "6.0.3",
40
41
  "vite": "8.3.0",
41
- "@happyvertical/smrt-cli": "^0.53.3"
42
+ "@happyvertical/smrt-cli": "^0.53.5"
42
43
  },
43
44
  "dependencies": {
44
- "@happyvertical/smrt-app-runtime": "^0.53.3",
45
- "@happyvertical/smrt-config": "^0.53.3",
46
- "@happyvertical/smrt-core": "^0.53.3",
47
- "@happyvertical/smrt-jobs": "^0.53.3",
48
- "@happyvertical/smrt-profiles": "^0.53.3",
49
- "@happyvertical/smrt-svelte": "^0.53.3",
50
- "@happyvertical/smrt-tenancy": "^0.53.3",
51
- "@happyvertical/smrt-ui": "^0.53.3",
52
- "@happyvertical/smrt-users": "^0.53.3",
53
- "@happyvertical/smrt-web": "^0.53.3",
45
+ "@happyvertical/smrt-app-runtime": "^0.53.5",
46
+ "@happyvertical/smrt-config": "^0.53.5",
47
+ "@happyvertical/smrt-core": "^0.53.5",
48
+ "@happyvertical/smrt-jobs": "^0.53.5",
49
+ "@happyvertical/smrt-profiles": "^0.53.5",
50
+ "@happyvertical/smrt-svelte": "^0.53.5",
51
+ "@happyvertical/smrt-tenancy": "^0.53.5",
52
+ "@happyvertical/smrt-ui": "^0.53.5",
53
+ "@happyvertical/smrt-users": "^0.53.5",
54
+ "@happyvertical/smrt-web": "^0.53.5",
54
55
  "@happyvertical/sql": "^0.89.12",
55
56
  "@modelcontextprotocol/server": "^2.0.0"
56
57
  }
@@ -5,25 +5,10 @@
5
5
  * See: https://github.com/happyvertical/smrt
6
6
  */
7
7
 
8
- import { resolveLocalRuntimePaths } from '@happyvertical/smrt-app-runtime';
9
- import { resolveApplicationId } from './scripts/smrt-runtime-identity.mjs';
10
-
11
8
  const profile = (process.env.SMRT_RUNTIME_PROFILE || 'local') as
12
9
  | 'local'
13
10
  | 'self-hosted'
14
11
  | 'cloud';
15
- const appId = resolveApplicationId({
16
- sourceRoot: process.cwd(),
17
- explicitId: process.env.SMRT_APP_ID,
18
- });
19
- const localDatabase =
20
- profile === 'local'
21
- ? resolveLocalRuntimePaths({
22
- appId,
23
- dataDirectory: process.env.SMRT_DATA_DIR,
24
- sourceRoot: process.cwd(),
25
- }).database
26
- : undefined;
27
12
 
28
13
  export default {
29
14
  // This is the single infrastructure selector for the application. Domain
@@ -32,7 +17,18 @@ export default {
32
17
  profile,
33
18
  },
34
19
 
35
- // Agent/developer knowledge artifacts are generated by smrtPlugin().
20
+ // SMRT packages whose object manifests this app consumes. Keeping the list
21
+ // explicit avoids treating tooling/UI packages as domain model providers
22
+ // and makes generated runtime registration deterministic.
23
+ consumer: {
24
+ packages: [
25
+ '@happyvertical/smrt-profiles',
26
+ '@happyvertical/smrt-tenancy',
27
+ '@happyvertical/smrt-users',
28
+ ],
29
+ },
30
+
31
+ // Agent/developer knowledge artifacts are generated by the smrt() Vite plugin.
36
32
  // HTTP API routes stay off unless explicitly enabled for a dev/admin-only workflow.
37
33
  knowledge: {
38
34
  enabled: true,
@@ -45,22 +41,8 @@ export default {
45
41
  },
46
42
  },
47
43
 
48
- // Package-specific configuration
49
- packages: {
50
- // CLI configuration
51
- cli: {
52
- database: {
53
- type:
54
- profile === 'local' ? 'sqlite' : 'postgres',
55
- // `smrt-app` sets DATABASE_URL to the profile-owned data path before
56
- // migrations. Deployed profiles require an operator-owned URL.
57
- url:
58
- profile === 'local'
59
- ? process.env.DATABASE_URL || localDatabase
60
- : process.env.DATABASE_URL,
61
- },
62
- verbose: false,
63
- },
64
-
65
- },
44
+ // No `packages.cli.database`: `smrt app setup` / `smrt app migrate` hand
45
+ // `smrt db:migrate` the profile-owned database through DATABASE_URL and
46
+ // DATABASE_TYPE (local: the SQLite file in the user data root; deployed: the
47
+ // operator's URL). See the smrt-cli README for the precedence.
66
48
  };
@@ -1,21 +1,14 @@
1
1
  // See https://svelte.dev/docs/kit/types#app.d.ts
2
2
  // for information about these interfaces
3
3
 
4
- import type { SessionLocals } from '@happyvertical/smrt-users/sveltekit';
4
+ import type { SmrtRuntimeLocals } from '@happyvertical/smrt-app-runtime/sveltekit';
5
5
 
6
6
  declare global {
7
7
  namespace App {
8
8
  // interface Error {}
9
- interface Locals extends SessionLocals {
10
- /**
11
- * URL-selected tenant candidate. Never use this as authorization; the
12
- * session-authorized tenant remains `tenantId` from SessionLocals.
13
- */
14
- selectedTenantId: string | null;
15
- selectedTenantSlug: string | null;
16
- /** Full context for the authorized session tenant, when present. */
17
- tenantContext?: import('@happyvertical/smrt-tenancy').MinimalTenantContext;
18
- }
9
+ // Session user, permissions, the authorized `tenantId`/`tenantContext`,
10
+ // and the URL-selected tenant candidate (never authorization).
11
+ interface Locals extends SmrtRuntimeLocals {}
19
12
  // interface PageData {}
20
13
  // interface PageState {}
21
14
  // interface Platform {}
@@ -1,56 +1,15 @@
1
1
  /**
2
- * Request foundation, in order:
2
+ * Request foundation from `@happyvertical/smrt-app-runtime/sveltekit`, in
3
+ * order: the fail-closed startup gate (`init`), URL tenant selection
4
+ * (`locals.selectedTenant*`, never authority; tenant headers are ignored), the
5
+ * signed session (the only source of tenant context and permissions), and
6
+ * `locals.tenantContext` published only for the verified session tenant.
3
7
  *
4
- * 1. Resolve a URL tenant candidate into `locals.selectedTenant*`. This is
5
- * selection only and does not enter AsyncLocalStorage tenant context.
6
- * 2. Load the signed session. `enterTenantContext: true` establishes the
7
- * authorized session tenant and its permission set for downstream code.
8
- * 3. Publish the authorized context on locals when it matches the session.
9
- *
10
- * This ordering prevents a spoofed header or hostname from becoming query
11
- * authority. Membership-gated tenant switching belongs in an explicit action
12
- * using `switchSessionTenant()` from `@happyvertical/smrt-users/sveltekit`.
8
+ * Membership-gated tenant switching belongs in an explicit action using
9
+ * `switchSessionTenant()` from `@happyvertical/smrt-users/sveltekit`.
10
+ * Configure the runtime in `$lib/server/smrt`.
13
11
  */
14
12
 
15
- import { getCurrentTenant, enableTenancy } from '@happyvertical/smrt-tenancy';
16
- import { createSessionHandler } from '@happyvertical/smrt-users/sveltekit';
17
- import type { Handle, ServerInit } from '@sveltejs/kit';
18
- import { sequence } from '@sveltejs/kit/hooks';
19
-
20
- import { getSmrtConfig } from '$lib/server/smrt';
21
- import { ensureApplicationRuntimeReady } from '$lib/server/application-runtime';
22
- import { resolveTenant } from '$lib/server/tenancy';
23
-
24
- enableTenancy();
25
-
26
- export const init: ServerInit = ensureApplicationRuntimeReady;
27
-
28
- const tenantSelectionHandle: Handle = async ({ event, resolve }) => {
29
- const selection = await resolveTenant(event);
30
- event.locals.selectedTenantId = selection.tenantId;
31
- event.locals.selectedTenantSlug = selection.tenantSlug;
32
- return resolve(event);
33
- };
34
-
35
- const sessionHandle = createSessionHandler({
36
- ...getSmrtConfig('Session'),
37
- enterTenantContext: true,
38
- }) as unknown as Handle;
39
-
40
- const authorizedTenantLocalsHandle: Handle = async ({ event, resolve }) => {
41
- const activeContext = getCurrentTenant();
42
- if (
43
- event.locals.user &&
44
- event.locals.tenantId &&
45
- activeContext?.tenantId === event.locals.tenantId
46
- ) {
47
- event.locals.tenantContext = activeContext;
48
- }
49
- return resolve(event);
50
- };
13
+ import { runtime } from '$lib/server/smrt';
51
14
 
52
- export const handle: Handle = sequence(
53
- tenantSelectionHandle,
54
- sessionHandle,
55
- authorizedTenantLocalsHandle,
56
- );
15
+ export const { handle, init } = runtime;
@@ -1,103 +1,27 @@
1
1
  /**
2
- * Centralized SMRT Configuration
2
+ * The application's SMRT runtime: app options only.
3
3
  *
4
- * This file provides configuration for all SMRT objects in your project.
5
- * Import this file in your routes to get properly configured collections.
6
- */
7
-
8
- import { existsSync } from 'node:fs';
9
- import { join } from 'node:path';
10
-
11
- import {
12
- ObjectRegistry,
13
- type SmrtClassOptions,
14
- type SmrtObject,
15
- } from '@happyvertical/smrt-core';
16
- import { loadManifestFromPathSync } from '@happyvertical/smrt-core/manifest';
17
- import { getRequestScopedDatabase } from '@happyvertical/smrt-users';
18
-
19
- import { getApplicationDatabaseConfig } from './application-runtime.js';
20
-
21
- // Import all SMRT objects to register them.
22
- import '../objects/index.js';
23
-
24
- // Upgrade those registrations to package-qualified identities
25
- // (`<package>:<Class>`) — required to link each class to its scanned field
26
- // metadata below. smrtPlugin() generates this module on every dev/build run
27
- // (it is gitignored, like the generated API routes); guard the import so a
28
- // fresh checkout that has not run the plugin yet still boots.
29
- try {
30
- // @ts-ignore generated by smrtPlugin before a consumer build starts
31
- await import('./smrt-register.js');
32
- } catch (error) {
33
- // Not generated yet — the first `vite dev` / `vite build` creates it. Do
34
- // not hide a real import/runtime error from an existing generated module.
35
- const code = (error as NodeJS.ErrnoException).code;
36
- const message = error instanceof Error ? error.message : '';
37
- if (
38
- code !== 'ERR_MODULE_NOT_FOUND' &&
39
- !message.includes('smrt-register.js')
40
- ) {
41
- throw error;
42
- }
43
- }
44
-
45
- // Hydrate field metadata for this app's own objects from the local manifest
46
- // written by smrtPlugin() (`.smrt/manifest.json`). The decorator import above
47
- // registers the classes, but their scanned field metadata (plain properties
48
- // like `title: string = ''`) lives in the manifest — without seeding it, the
49
- // runtime only knows the framework base fields and server-side writes would
50
- // silently drop your domain columns.
51
- const localManifestPath = join(process.cwd(), '.smrt', 'manifest.json');
52
- if (existsSync(localManifestPath)) {
53
- loadManifestFromPathSync(localManifestPath);
54
- }
55
-
56
- /**
57
- * Per-object configuration overrides
58
- *
59
- * Use this to configure specific objects differently from defaults.
60
- * Example:
61
- * AuditLog: { db: { url: process.env.AUDIT_DB_URL!, type: 'postgres' } }
62
- */
63
- const objectOverrides: Record<string, Partial<SmrtClassOptions>> = {
64
- // Add object-specific overrides here
65
- };
66
-
67
- /**
68
- * Get default configuration for SMRT objects
69
- */
70
- function getDefaultConfig(): SmrtClassOptions {
71
- return {
72
- db: getApplicationDatabaseConfig(),
73
- };
74
- }
75
-
76
- /**
77
- * Get configuration for a specific SMRT class
78
- */
79
- export function getSmrtConfig(className: string): SmrtClassOptions {
80
- const defaults = getDefaultConfig();
81
- const override = objectOverrides[className];
82
- return override ? { ...defaults, ...override } : defaults;
83
- }
84
-
85
- /**
86
- * Get a collection instance for a SMRT class
4
+ * `runtime` composes the configured profile (local SQLite or deployed
5
+ * PostgreSQL), storage custody, the owner bootstrap, tenant selection, the
6
+ * signed session, and (local profile) the single-writer lease shared with
7
+ * `smrt app` operations; `src/hooks.server.ts` mounts its `handle` and `init`.
8
+ * The smrt() Vite plugin registers the generated objects before this module
9
+ * runs, and generated API routes resolve collections through
10
+ * `runtime.getCollection()`.
87
11
  *
88
- * Usage in routes:
89
- * const products = await getCollection<Product>('Product');
90
- * const items = await products.list();
12
+ * `runtime.getCollection()` and `runtime.classOptions()` are request-scoped:
13
+ * under `database-rls` isolation they carry the request's transaction, so
14
+ * call them per request and never keep the result.
91
15
  */
92
- export async function getCollection<T extends SmrtObject>(className: string) {
93
- const config = getSmrtConfig(className);
94
- const objectOverride = objectOverrides[className];
95
- const requestScopedDb = objectOverride?.db
96
- ? undefined
97
- : (getRequestScopedDatabase() as SmrtClassOptions['db'] | undefined);
98
16
 
99
- return await ObjectRegistry.getCollection<T>(className, {
100
- ...config,
101
- db: requestScopedDb ?? config.db,
102
- });
103
- }
17
+ import { createProviderReadinessProbe } from '@happyvertical/smrt-app-runtime';
18
+ import { createSmrtSvelteKitRuntime } from '@happyvertical/smrt-app-runtime/sveltekit';
19
+
20
+ export const runtime = createSmrtSvelteKitRuntime({
21
+ // Deployed profiles prove authentication, asset, and secret readiness
22
+ // through the installed modules named by SMRT_*_READINESS_MODULE.
23
+ providerReadiness: createProviderReadinessProbe,
24
+ // Per-object overrides, for example:
25
+ // AuditLog: { db: { type: 'postgres', url: process.env.AUDIT_DB_URL! } }
26
+ classOverrides: {},
27
+ });
@@ -1,15 +1,7 @@
1
- import type { LayoutServerLoad } from './$types';
1
+ import { createSessionLayoutLoad } from '@happyvertical/smrt-app-runtime/sveltekit';
2
2
 
3
3
  /**
4
- * Keep session and tenant state server-owned. The selected tenant is displayed
5
- * separately from the tenant authorized by the active session.
4
+ * Server-owned session summary: whether a user is signed in, the tenant the
5
+ * session authorizes, and (display only) the URL-selected tenant candidate.
6
6
  */
7
- export const load: LayoutServerLoad = async ({ locals }) => {
8
- return {
9
- session: {
10
- authenticated: Boolean(locals.user),
11
- activeTenantId: locals.tenantId,
12
- selectedTenantSlug: locals.selectedTenantSlug,
13
- },
14
- };
15
- };
7
+ export const load = createSessionLayoutLoad();