@happyvertical/smrt-template-sveltekit 0.53.3 → 0.54.0
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 +60 -27
- package/README.md +23 -16
- package/mcp-apps-template/package.dependencies.json +2 -2
- package/mcp-apps-template/src/routes/.well-known/oauth-protected-resource/api/mcp/+server.ts +10 -6
- package/mcp-apps-template/src/routes/api/mcp/+server.ts +68 -12
- package/package.json +19 -15
- package/template/AGENTS.md +20 -5
- package/template/Dockerfile +0 -1
- package/template/INSTALL_PROMPT.md +2 -2
- package/template/README.md +86 -40
- package/template/compose.yaml +2 -2
- package/template/package.json +28 -27
- package/template/smrt.config.ts +16 -34
- package/template/src/app.d.ts +4 -11
- package/template/src/hooks.server.ts +10 -51
- package/template/src/lib/server/smrt.ts +22 -98
- package/template/src/routes/+layout.server.ts +4 -12
- package/template/src/routes/+layout.svelte +27 -99
- package/template/src/routes/+page.server.ts +4 -4
- package/template/src/routes/api/_runtime/diagnostics/+server.ts +17 -3
- package/template/src/routes/api/_runtime/health/+server.ts +4 -28
- package/template/src/routes/settings/+page.svelte +3 -34
- package/template/src/routes/setup/+page.server.ts +7 -94
- package/template/src/routes/setup/+page.svelte +3 -23
- package/template/vite.config.ts +5 -43
- package/template.config.js +11 -11
- package/mcp-apps-template/src/lib/McpAppsBridge.svelte +0 -37
- package/mcp-apps-template/src/lib/server/mcp-hosted-principal.ts +0 -12
- package/mcp-apps-template/src/lib/server/mcp-hosted.ts +0 -28
- package/mcp-apps-template/src/lib/server/mcp.ts +0 -85
- package/template/scripts/smrt-app.mjs +0 -885
- package/template/scripts/smrt-mcp-apps.mjs +0 -18
- package/template/scripts/smrt-operation-lock.mjs +0 -102
- package/template/scripts/smrt-portability-assets.mjs +0 -707
- package/template/scripts/smrt-portability.mjs +0 -470
- package/template/scripts/smrt-prepare-migration.mjs +0 -93
- package/template/scripts/smrt-process.mjs +0 -82
- package/template/scripts/smrt-provider-readiness.mjs +0 -33
- package/template/scripts/smrt-runtime-identity.mjs +0 -245
- package/template/scripts/smrt-vite.mjs +0 -45
- package/template/scripts/smrt-web.mjs +0 -10
- package/template/scripts/smrt-worker.mjs +0 -100
- package/template/scripts/smrt-writer-lease.mjs +0 -199
- package/template/src/lib/RuntimeDiagnosticsWebMcp.svelte +0 -11
- package/template/src/lib/runtime-diagnostics-webmcp.ts +0 -108
- package/template/src/lib/server/application-runtime.ts +0 -262
- package/template/src/lib/server/runtime-diagnostics.ts +0 -114
- package/template/src/lib/server/tenancy.ts +0 -114
- package/template/src/smrt-webmcp.d.ts +0 -6
package/template/README.md
CHANGED
|
@@ -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. `
|
|
62
|
-
types,
|
|
63
|
-
|
|
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`
|
|
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.
|
|
136
|
-
|
|
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.
|
|
140
|
-
|
|
141
|
-
`locals.tenantId
|
|
142
|
-
4.
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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 {
|
|
194
|
+
import { runtime } from '$lib/server/smrt';
|
|
175
195
|
|
|
176
|
-
await syncPermissionCatalog(
|
|
177
|
-
const roles = await RoleCollection.create(
|
|
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`,
|
|
238
|
-
|
|
239
|
-
`
|
|
240
|
-
|
|
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 {
|
|
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
|
-
|
|
336
|
+
<!-- AppShell passes `webmcp` to its Provider unchanged. -->
|
|
337
|
+
<AppShell title="s-m-r-t app" {webmcp} {nav}>
|
|
313
338
|
{@render children()}
|
|
314
|
-
</
|
|
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`.
|
|
403
|
-
`scripts/smrt-portability.mjs`
|
|
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
|
-
|
|
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.
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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.
|
package/template/compose.yaml
CHANGED
|
@@ -16,7 +16,7 @@ services:
|
|
|
16
16
|
build: .
|
|
17
17
|
env_file: .env.self-hosted
|
|
18
18
|
environment: *runtime-environment
|
|
19
|
-
command:
|
|
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:
|
|
27
|
+
command: pnpm exec smrt app worker schedule
|
|
28
28
|
depends_on:
|
|
29
29
|
migrate:
|
|
30
30
|
condition: service_completed_successfully
|
package/template/package.json
CHANGED
|
@@ -9,25 +9,26 @@
|
|
|
9
9
|
"pnpm": "11.25.0"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
-
"dev": "
|
|
13
|
-
"build": "
|
|
14
|
-
"preview": "
|
|
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
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"app:
|
|
20
|
-
"app:
|
|
21
|
-
"app:
|
|
22
|
-
"app:
|
|
23
|
-
"app:
|
|
24
|
-
"app:
|
|
25
|
-
"app:
|
|
26
|
-
"app:
|
|
27
|
-
"app:
|
|
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": "
|
|
30
|
-
"worker: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.
|
|
42
|
+
"@happyvertical/smrt-cli": "^0.54.0"
|
|
42
43
|
},
|
|
43
44
|
"dependencies": {
|
|
44
|
-
"@happyvertical/smrt-app-runtime": "^0.
|
|
45
|
-
"@happyvertical/smrt-config": "^0.
|
|
46
|
-
"@happyvertical/smrt-core": "^0.
|
|
47
|
-
"@happyvertical/smrt-jobs": "^0.
|
|
48
|
-
"@happyvertical/smrt-profiles": "^0.
|
|
49
|
-
"@happyvertical/smrt-svelte": "^0.
|
|
50
|
-
"@happyvertical/smrt-tenancy": "^0.
|
|
51
|
-
"@happyvertical/smrt-ui": "^0.
|
|
52
|
-
"@happyvertical/smrt-users": "^0.
|
|
53
|
-
"@happyvertical/smrt-web": "^0.
|
|
45
|
+
"@happyvertical/smrt-app-runtime": "^0.54.0",
|
|
46
|
+
"@happyvertical/smrt-config": "^0.54.0",
|
|
47
|
+
"@happyvertical/smrt-core": "^0.54.0",
|
|
48
|
+
"@happyvertical/smrt-jobs": "^0.54.0",
|
|
49
|
+
"@happyvertical/smrt-profiles": "^0.54.0",
|
|
50
|
+
"@happyvertical/smrt-svelte": "^0.54.0",
|
|
51
|
+
"@happyvertical/smrt-tenancy": "^0.54.0",
|
|
52
|
+
"@happyvertical/smrt-ui": "^0.54.0",
|
|
53
|
+
"@happyvertical/smrt-users": "^0.54.0",
|
|
54
|
+
"@happyvertical/smrt-web": "^0.54.0",
|
|
54
55
|
"@happyvertical/sql": "^0.89.12",
|
|
55
56
|
"@modelcontextprotocol/server": "^2.0.0"
|
|
56
57
|
}
|
package/template/smrt.config.ts
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
};
|
package/template/src/app.d.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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 {
|
|
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
|
|
53
|
-
tenantSelectionHandle,
|
|
54
|
-
sessionHandle,
|
|
55
|
-
authorizedTenantLocalsHandle,
|
|
56
|
-
);
|
|
15
|
+
export const { handle, init } = runtime;
|
|
@@ -1,103 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The application's SMRT runtime: app options only.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
1
|
+
import { createSessionLayoutLoad } from '@happyvertical/smrt-app-runtime/sveltekit';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
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();
|