@happyvertical/smrt-core 0.49.5 → 0.49.7
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/README.md +56 -0
- package/dist/consumer-plugin/index.d.ts +19 -4
- package/dist/consumer-plugin/index.d.ts.map +1 -1
- package/dist/consumer-plugin/index.js +214 -6
- package/dist/consumer-plugin/index.js.map +1 -1
- package/dist/manifest/static-manifest.d.ts.map +1 -1
- package/dist/manifest/static-manifest.js +13 -7
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest/store.js.map +1 -1
- package/dist/manifest.json +13 -7
- package/dist/scanner/manifest-generator.d.ts +39 -0
- package/dist/scanner/manifest-generator.d.ts.map +1 -1
- package/dist/scanner/manifest-generator.js +79 -12
- package/dist/scanner/manifest-generator.js.map +1 -1
- package/dist/smrt-knowledge.json +5 -4
- package/dist/vite-plugin/changes-route.d.ts.map +1 -1
- package/dist/vite-plugin/changes-route.js +4 -3
- package/dist/vite-plugin/changes-route.js.map +1 -1
- package/dist/vite-plugin/dev-plane-route.d.ts +2 -0
- package/dist/vite-plugin/dev-plane-route.d.ts.map +1 -1
- package/dist/vite-plugin/dev-plane-route.js +8 -5
- package/dist/vite-plugin/dev-plane-route.js.map +1 -1
- package/dist/vite-plugin/events-route.d.ts.map +1 -1
- package/dist/vite-plugin/events-route.js +4 -3
- package/dist/vite-plugin/events-route.js.map +1 -1
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +25 -40
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/resources-route.d.ts +6 -0
- package/dist/vite-plugin/resources-route.d.ts.map +1 -1
- package/dist/vite-plugin/resources-route.js +30 -7
- package/dist/vite-plugin/resources-route.js.map +1 -1
- package/dist/vite-plugin/sveltekit-config-import.d.ts +19 -0
- package/dist/vite-plugin/sveltekit-config-import.d.ts.map +1 -0
- package/dist/vite-plugin/sveltekit-config-import.js +36 -0
- package/dist/vite-plugin/sveltekit-config-import.js.map +1 -0
- package/dist/vite-plugin/sveltekit-generator.d.ts +33 -2
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +181 -65
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/sveltekit-path.d.ts +8 -0
- package/dist/vite-plugin/sveltekit-path.d.ts.map +1 -0
- package/dist/vite-plugin/sveltekit-path.js +25 -0
- package/dist/vite-plugin/sveltekit-path.js.map +1 -0
- package/dist/vite-plugin/sveltekit-route-coordinator.d.ts +67 -0
- package/dist/vite-plugin/sveltekit-route-coordinator.d.ts.map +1 -0
- package/dist/vite-plugin/sveltekit-route-coordinator.js +321 -0
- package/dist/vite-plugin/sveltekit-route-coordinator.js.map +1 -0
- package/dist/vite-plugin/sync-apply-route.d.ts +1 -1
- package/dist/vite-plugin/sync-apply-route.d.ts.map +1 -1
- package/dist/vite-plugin/sync-apply-route.js +4 -3
- package/dist/vite-plugin/sync-apply-route.js.map +1 -1
- package/package.json +6 -5
package/dist/smrt-knowledge.json
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
"sensitiveFieldsExcluded": true,
|
|
4
4
|
"generatedAt": "1970-01-01T00:00:00.000Z",
|
|
5
5
|
"packageName": "@happyvertical/smrt-core",
|
|
6
|
-
"packageVersion": "0.49.
|
|
6
|
+
"packageVersion": "0.49.7",
|
|
7
7
|
"sourceManifestPath": "dist/manifest.json",
|
|
8
8
|
"agentDocPath": "AGENTS.md",
|
|
9
9
|
"sourceHashes": {
|
|
10
|
-
"manifest": "
|
|
11
|
-
"packageJson": "
|
|
10
|
+
"manifest": "c9d1fd134ddb71f0c2b88e50fa448fd689fb818c1c5a10468f8b7cd7a8bc1390",
|
|
11
|
+
"packageJson": "c507780460d668ccbfa55391b419f114f8255523754ddf635a343f6355816290",
|
|
12
12
|
"agents": "6c19f8eab9fbcd9c10d0649279f93c8c36c65c18da3402f2a2b49893432125ec",
|
|
13
13
|
"moduleDoc:agents/object-runtime.md": "34a4e740c109467cd5486c4af0e3bf4b63915697721ab584df0e5506ea9290ca",
|
|
14
14
|
"moduleDoc:agents/revision-guard.md": "aa6b1ddb5b6b49fa27ebbe575ec7fcd6d5ceb35ee25acc702f877a226eb9a99a",
|
|
@@ -73,12 +73,13 @@
|
|
|
73
73
|
"yaml": "^2.9.0",
|
|
74
74
|
"@faker-js/faker": "^10.5.0",
|
|
75
75
|
"@huggingface/transformers": ">=3.0.0 <4.0.0",
|
|
76
|
-
"@sveltejs/kit": "^2.69.
|
|
76
|
+
"@sveltejs/kit": "^2.69.3",
|
|
77
77
|
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
|
78
78
|
"@types/node": "24.13.2",
|
|
79
79
|
"@types/pluralize": "^0.0.33",
|
|
80
80
|
"@xenova/transformers": "^2.17.2",
|
|
81
81
|
"svelte": "^5.56.4",
|
|
82
|
+
"svelte-check": "4.7.6",
|
|
82
83
|
"typescript": "5.9.3",
|
|
83
84
|
"vite": "8.1.4",
|
|
84
85
|
"vite-plugin-dts": "4.5.4",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"changes-route.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/changes-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAIH,OAAO,KAAK,EAEV,mBAAmB,EACpB,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"changes-route.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/changes-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAIH,OAAO,KAAK,EAEV,mBAAmB,EACpB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAWjE;;;;;;;;;;;GAWG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,mBAAmB,GAC5B,MAAM,GAAG,IAAI,CASf;AAED,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,mBAAmB,GAC5B,OAAO,CAKT;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CA2BT"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AUTO_GENERATED_ROUTE_HEADER } from "./route-header.js";
|
|
2
|
+
import { resolveSvelteKitConfigImport } from "./sveltekit-config-import.js";
|
|
2
3
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
//#region src/vite-plugin/changes-route.ts
|
|
@@ -72,14 +73,14 @@ function generateChangesRoute(projectRoot, manifest, options) {
|
|
|
72
73
|
return false;
|
|
73
74
|
}
|
|
74
75
|
const routeDir = join(projectRoot, options.routesDir, "_changes");
|
|
75
|
-
const content = generateChangesRouteTemplate(anchorClassName, manifestHasTenantScopedObject(manifest));
|
|
76
|
+
const content = generateChangesRouteTemplate(anchorClassName, manifestHasTenantScopedObject(manifest), resolveSvelteKitConfigImport(projectRoot, routeDir, options));
|
|
76
77
|
if (!existsSync(routeDir)) mkdirSync(routeDir, { recursive: true });
|
|
77
78
|
const filePath = join(routeDir, "+server.ts");
|
|
78
79
|
writeFileSync(filePath, content, "utf-8");
|
|
79
80
|
console.log(`[smrt] Generated: ${filePath}`);
|
|
80
81
|
return true;
|
|
81
82
|
}
|
|
82
|
-
function generateChangesRouteTemplate(anchorClassName, tenantScoped) {
|
|
83
|
+
function generateChangesRouteTemplate(anchorClassName, tenantScoped, configImport) {
|
|
83
84
|
return `${AUTO_GENERATED_ROUTE_HEADER}
|
|
84
85
|
// DO NOT EDIT - changes will be overwritten
|
|
85
86
|
//
|
|
@@ -94,7 +95,7 @@ function generateChangesRouteTemplate(anchorClassName, tenantScoped) {
|
|
|
94
95
|
|
|
95
96
|
import { error, json } from '@sveltejs/kit';
|
|
96
97
|
import { getTenantScopedChangesSince } from '@happyvertical/smrt-core';
|
|
97
|
-
import { getCollection } from '$
|
|
98
|
+
import { getCollection } from '${configImport}';
|
|
98
99
|
import type { RequestHandler } from './$types';
|
|
99
100
|
|
|
100
101
|
// Fail-closed authorization (#1540): the change feed spans every table, so
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"changes-route.js","names":[],"sources":["../../src/vite-plugin/changes-route.ts"],"sourcesContent":["/**\n * SvelteKit `_changes` route generation for the change feed (issue #1758).\n *\n * Emits `{routesDir}/_changes/+server.ts` — an auth-guarded, tenant-scoped\n * GET endpoint over the `_smrt_changes` log, part of the client/mobile sync\n * contract (PRD #1755). Kept in its own module so `sveltekit-generator.ts`\n * only carries a one-line registration.\n *\n * Design notes:\n * - **Fail-closed auth** (#1540 posture): the handler requires an\n * authenticated principal on `locals`. The feed spans every table, so\n * per-model `api: { public }` opt-outs deliberately do not apply.\n * - **Tenant scoping**: when the project has tenant-scoped objects, the\n * route establishes tenant context from `locals` exactly like generated\n * collection routes, then reads through\n * `getTenantScopedChangesSince()` — a tenant only ever sees its own\n * changes plus global rows.\n * - **Database resolution**: the route anchors on the project's first\n * generated collection (alphabetical) via the consumer's existing\n * `getCollection()` helper, inheriting its configuration, request-scoped\n * database support and system-table bootstrap. Multi-database projects\n * (per-object `db` overrides) see the anchor collection's feed.\n * - Cleanup rides the existing generated-route sweep: the emitted file\n * starts with {@link AUTO_GENERATED_ROUTE_HEADER}.\n */\n\nimport { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type {\n SmartObjectDefinition,\n SmartObjectManifest,\n} from '../scanner/types';\nimport { AUTO_GENERATED_ROUTE_HEADER } from './route-header.js';\nimport type { SvelteKitOptions } from './sveltekit-generator.js';\n\n/**\n * Mirrors `isCollectionClass` in `sveltekit-generator.ts` (module-private\n * there): collection classes share route paths with their item class and\n * never anchor routes themselves.\n */\nfunction isCollectionDefinition(objectDef: SmartObjectDefinition): boolean {\n return objectDef.extends === 'SmrtCollection' || !!objectDef.extendsTypeArg;\n}\n\n/**\n * Pick the class the route resolves its database through: the first\n * non-collection object by sorted manifest key, for determinism across\n * builds.\n *\n * Returns the manifest **registry key** verbatim (which may be\n * package-qualified, e.g. `@happyvertical/smrt-ledgers:Account`) — the emitted\n * route passes it straight to `getCollection()`, exactly as the generated CRUD\n * routes do. Collapsing it to a simple class name would resolve ambiguously\n * when two loaded packages declare the same simple name (mirrors the #1778\n * verbatim-key fix and the sync-apply route's `registryKey`).\n */\nexport function resolveAnchorClassName(\n manifest: SmartObjectManifest,\n): string | null {\n const sortedNames = Object.keys(manifest.objects).sort();\n for (const name of sortedNames) {\n const def = manifest.objects[name];\n if (def && !isCollectionDefinition(def)) {\n return name;\n }\n }\n return null;\n}\n\nexport function manifestHasTenantScopedObject(\n manifest: SmartObjectManifest,\n): boolean {\n return Object.values(manifest.objects).some(\n (def) =>\n !isCollectionDefinition(def) && !!def.decoratorConfig?.tenantScoped,\n );\n}\n\n/**\n * Generate the `_changes/+server.ts` route. Returns true when a route was\n * written. Disabled with `sveltekit: { changesRoute: { enabled: false } }`;\n * skipped (with a log line) when the manifest has no objects to anchor the\n * database on.\n */\nexport function generateChangesRoute(\n projectRoot: string,\n manifest: SmartObjectManifest,\n options: SvelteKitOptions,\n): boolean {\n if (options.changesRoute?.enabled === false) {\n return false;\n }\n\n const anchorClassName = resolveAnchorClassName(manifest);\n if (!anchorClassName) {\n console.log(\n '[smrt] Skipping _changes route - no SMRT objects to anchor the database on',\n );\n return false;\n }\n\n const routeDir = join(projectRoot, options.routesDir, '_changes');\n const content = generateChangesRouteTemplate(\n anchorClassName,\n manifestHasTenantScopedObject(manifest),\n );\n\n if (!existsSync(routeDir)) {\n mkdirSync(routeDir, { recursive: true });\n }\n const filePath = join(routeDir, '+server.ts');\n writeFileSync(filePath, content, 'utf-8');\n console.log(`[smrt] Generated: ${filePath}`);\n return true;\n}\n\nfunction generateChangesRouteTemplate(\n anchorClassName: string,\n tenantScoped: boolean,\n): string {\n const tenantHelper = tenantScoped\n ? `\nimport { enterTenantContext, hasTenantContext } from '@happyvertical/smrt-tenancy';\n\nfunction establishTenantContext(locals: unknown): void {\n if (hasTenantContext()) return;\n if (!locals || typeof locals !== 'object') return;\n const l = locals as Record<string, unknown>;\n const user = l.user as Record<string, unknown> | undefined;\n const session = l.session as Record<string, unknown> | undefined;\n const tenantId = l.tenantId ?? user?.tenantId ?? session?.tenantId;\n if (typeof tenantId === 'string' && tenantId) {\n enterTenantContext({ tenantId });\n }\n}\n`\n : '';\n const tenantCall = tenantScoped ? '\\n establishTenantContext(locals);' : '';\n\n return `${AUTO_GENERATED_ROUTE_HEADER}\n// DO NOT EDIT - changes will be overwritten\n//\n// GET /_changes — cursor read over the _smrt_changes change feed (#1758).\n// Part of the client/mobile sync contract: poll with the returned cursor to\n// observe every committed framework save/delete (deletes are tombstones)\n// exactly once. Query params: since (cursor, default 0), tables\n// (comma-separated), limit. A response with resyncRequired: true (still\n// HTTP 200 — protocol state, not an error) means the cursor cannot be\n// served incrementally (pruned or foreign) and the client must re-fetch\n// in full before resuming polling from resyncCursor.\n\nimport { error, json } from '@sveltejs/kit';\nimport { getTenantScopedChangesSince } from '@happyvertical/smrt-core';\nimport { getCollection } from '$
|
|
1
|
+
{"version":3,"file":"changes-route.js","names":[],"sources":["../../src/vite-plugin/changes-route.ts"],"sourcesContent":["/**\n * SvelteKit `_changes` route generation for the change feed (issue #1758).\n *\n * Emits `{routesDir}/_changes/+server.ts` — an auth-guarded, tenant-scoped\n * GET endpoint over the `_smrt_changes` log, part of the client/mobile sync\n * contract (PRD #1755). Kept in its own module so `sveltekit-generator.ts`\n * only carries a one-line registration.\n *\n * Design notes:\n * - **Fail-closed auth** (#1540 posture): the handler requires an\n * authenticated principal on `locals`. The feed spans every table, so\n * per-model `api: { public }` opt-outs deliberately do not apply.\n * - **Tenant scoping**: when the project has tenant-scoped objects, the\n * route establishes tenant context from `locals` exactly like generated\n * collection routes, then reads through\n * `getTenantScopedChangesSince()` — a tenant only ever sees its own\n * changes plus global rows.\n * - **Database resolution**: the route anchors on the project's first\n * generated collection (alphabetical) via the consumer's existing\n * `getCollection()` helper, inheriting its configuration, request-scoped\n * database support and system-table bootstrap. Multi-database projects\n * (per-object `db` overrides) see the anchor collection's feed.\n * - Cleanup rides the existing generated-route sweep: the emitted file\n * starts with {@link AUTO_GENERATED_ROUTE_HEADER}.\n */\n\nimport { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type {\n SmartObjectDefinition,\n SmartObjectManifest,\n} from '../scanner/types';\nimport { AUTO_GENERATED_ROUTE_HEADER } from './route-header.js';\nimport { resolveSvelteKitConfigImport } from './sveltekit-config-import.js';\nimport type { SvelteKitOptions } from './sveltekit-generator.js';\n\n/**\n * Mirrors `isCollectionClass` in `sveltekit-generator.ts` (module-private\n * there): collection classes share route paths with their item class and\n * never anchor routes themselves.\n */\nfunction isCollectionDefinition(objectDef: SmartObjectDefinition): boolean {\n return objectDef.extends === 'SmrtCollection' || !!objectDef.extendsTypeArg;\n}\n\n/**\n * Pick the class the route resolves its database through: the first\n * non-collection object by sorted manifest key, for determinism across\n * builds.\n *\n * Returns the manifest **registry key** verbatim (which may be\n * package-qualified, e.g. `@happyvertical/smrt-ledgers:Account`) — the emitted\n * route passes it straight to `getCollection()`, exactly as the generated CRUD\n * routes do. Collapsing it to a simple class name would resolve ambiguously\n * when two loaded packages declare the same simple name (mirrors the #1778\n * verbatim-key fix and the sync-apply route's `registryKey`).\n */\nexport function resolveAnchorClassName(\n manifest: SmartObjectManifest,\n): string | null {\n const sortedNames = Object.keys(manifest.objects).sort();\n for (const name of sortedNames) {\n const def = manifest.objects[name];\n if (def && !isCollectionDefinition(def)) {\n return name;\n }\n }\n return null;\n}\n\nexport function manifestHasTenantScopedObject(\n manifest: SmartObjectManifest,\n): boolean {\n return Object.values(manifest.objects).some(\n (def) =>\n !isCollectionDefinition(def) && !!def.decoratorConfig?.tenantScoped,\n );\n}\n\n/**\n * Generate the `_changes/+server.ts` route. Returns true when a route was\n * written. Disabled with `sveltekit: { changesRoute: { enabled: false } }`;\n * skipped (with a log line) when the manifest has no objects to anchor the\n * database on.\n */\nexport function generateChangesRoute(\n projectRoot: string,\n manifest: SmartObjectManifest,\n options: SvelteKitOptions,\n): boolean {\n if (options.changesRoute?.enabled === false) {\n return false;\n }\n\n const anchorClassName = resolveAnchorClassName(manifest);\n if (!anchorClassName) {\n console.log(\n '[smrt] Skipping _changes route - no SMRT objects to anchor the database on',\n );\n return false;\n }\n\n const routeDir = join(projectRoot, options.routesDir, '_changes');\n const content = generateChangesRouteTemplate(\n anchorClassName,\n manifestHasTenantScopedObject(manifest),\n resolveSvelteKitConfigImport(projectRoot, routeDir, options),\n );\n\n if (!existsSync(routeDir)) {\n mkdirSync(routeDir, { recursive: true });\n }\n const filePath = join(routeDir, '+server.ts');\n writeFileSync(filePath, content, 'utf-8');\n console.log(`[smrt] Generated: ${filePath}`);\n return true;\n}\n\nfunction generateChangesRouteTemplate(\n anchorClassName: string,\n tenantScoped: boolean,\n configImport: string,\n): string {\n const tenantHelper = tenantScoped\n ? `\nimport { enterTenantContext, hasTenantContext } from '@happyvertical/smrt-tenancy';\n\nfunction establishTenantContext(locals: unknown): void {\n if (hasTenantContext()) return;\n if (!locals || typeof locals !== 'object') return;\n const l = locals as Record<string, unknown>;\n const user = l.user as Record<string, unknown> | undefined;\n const session = l.session as Record<string, unknown> | undefined;\n const tenantId = l.tenantId ?? user?.tenantId ?? session?.tenantId;\n if (typeof tenantId === 'string' && tenantId) {\n enterTenantContext({ tenantId });\n }\n}\n`\n : '';\n const tenantCall = tenantScoped ? '\\n establishTenantContext(locals);' : '';\n\n return `${AUTO_GENERATED_ROUTE_HEADER}\n// DO NOT EDIT - changes will be overwritten\n//\n// GET /_changes — cursor read over the _smrt_changes change feed (#1758).\n// Part of the client/mobile sync contract: poll with the returned cursor to\n// observe every committed framework save/delete (deletes are tombstones)\n// exactly once. Query params: since (cursor, default 0), tables\n// (comma-separated), limit. A response with resyncRequired: true (still\n// HTTP 200 — protocol state, not an error) means the cursor cannot be\n// served incrementally (pruned or foreign) and the client must re-fetch\n// in full before resuming polling from resyncCursor.\n\nimport { error, json } from '@sveltejs/kit';\nimport { getTenantScopedChangesSince } from '@happyvertical/smrt-core';\nimport { getCollection } from '${configImport}';\nimport type { RequestHandler } from './$types';\n\n// Fail-closed authorization (#1540): the change feed spans every table, so\n// it is never public — an authenticated principal on \\`locals\\` is required.\nfunction hasAuthenticatedPrincipal(locals: unknown): boolean {\n if (!locals || typeof locals !== 'object') return false;\n const l = locals as Record<string, unknown>;\n const isResolvedPrincipal = (v: unknown) =>\n typeof v === 'object' && v !== null;\n return (\n isResolvedPrincipal(l.user) ||\n isResolvedPrincipal(l.session) ||\n l.smrtAuth === true\n );\n}\n\nfunction requireRouteAuth(locals: unknown): void {\n if (!hasAuthenticatedPrincipal(locals)) {\n throw error(401, 'Authentication required');\n }\n}\n${tenantHelper}\nexport const GET: RequestHandler = async ({ locals, url }) => {\n requireRouteAuth(locals);${tenantCall}\n\n const since = Number(url.searchParams.get('since') ?? '0');\n if (!Number.isFinite(since) || since < 0) {\n throw error(400, \"'since' must be a non-negative number\");\n }\n\n let limit: number | undefined;\n const limitParam = url.searchParams.get('limit');\n if (limitParam !== null) {\n limit = Number(limitParam);\n if (!Number.isFinite(limit) || limit < 1) {\n throw error(400, \"'limit' must be a positive number\");\n }\n }\n\n const tablesParam = url.searchParams.get('tables');\n const tables = tablesParam\n ? tablesParam\n .split(',')\n .map((table) => table.trim())\n .filter(Boolean)\n : undefined;\n\n // The feed lives in the project's database; anchor on the\n // ${anchorClassName} collection to reuse its configured connection.\n const collection = await getCollection('${anchorClassName}');\n const page = await getTenantScopedChangesSince(collection.db, {\n since,\n tables,\n limit,\n });\n return json(page);\n};\n`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,SAAS,uBAAuB,WAA2C;CACzE,OAAO,UAAU,YAAY,oBAAoB,CAAC,CAAC,UAAU;AAC/D;;;;;;;;;;;;;AAcA,SAAgB,uBACd,UACe;CACf,MAAM,cAAc,OAAO,KAAK,SAAS,OAAO,CAAC,CAAC,KAAK;CACvD,KAAK,MAAM,QAAQ,aAAa;EAC9B,MAAM,MAAM,SAAS,QAAQ;EAC7B,IAAI,OAAO,CAAC,uBAAuB,GAAG,GACpC,OAAO;CAEX;CACA,OAAO;AACT;AAEA,SAAgB,8BACd,UACS;CACT,OAAO,OAAO,OAAO,SAAS,OAAO,CAAC,CAAC,MACpC,QACC,CAAC,uBAAuB,GAAG,KAAK,CAAC,CAAC,IAAI,iBAAiB,YAC3D;AACF;;;;;;;AAQA,SAAgB,qBACd,aACA,UACA,SACS;CACT,IAAI,QAAQ,cAAc,YAAY,OACpC,OAAO;CAGT,MAAM,kBAAkB,uBAAuB,QAAQ;CACvD,IAAI,CAAC,iBAAiB;EACpB,QAAQ,IACN,4EACF;EACA,OAAO;CACT;CAEA,MAAM,WAAW,KAAK,aAAa,QAAQ,WAAW,UAAU;CAChE,MAAM,UAAU,6BACd,iBACA,8BAA8B,QAAQ,GACtC,6BAA6B,aAAa,UAAU,OAAO,CAC7D;CAEA,IAAI,CAAC,WAAW,QAAQ,GACtB,UAAU,UAAU,EAAE,WAAW,KAAK,CAAC;CAEzC,MAAM,WAAW,KAAK,UAAU,YAAY;CAC5C,cAAc,UAAU,SAAS,OAAO;CACxC,QAAQ,IAAI,qBAAqB,UAAU;CAC3C,OAAO;AACT;AAEA,SAAS,6BACP,iBACA,cACA,cACQ;CAoBR,OAAO,GAAG,4BAA4B;;;;;;;;;;;;;;iCAcP,aAAa;;;;;;;;;;;;;;;;;;;;;;EAjCvB,eACjB;;;;;;;;;;;;;;IAeA,GAuCS;;6BAtCM,eAAe,wCAAwC,GAwCpC;;;;;;;;;;;;;;;;;;;;;;;;;OAyBjC,gBAAgB;4CACqB,gBAAgB;;;;;;;;;AAS5D"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { SvelteKitOptions } from './sveltekit-generator.js';
|
|
2
2
|
/** Whether `@happyvertical/smrt-dev-mcp/dev-plane` resolves from the consumer. */
|
|
3
3
|
export declare function consumerHasSmrtDevMcp(projectRoot: string): boolean;
|
|
4
|
+
/** Whether this invocation will own the `_dev/[...tool]` output file. */
|
|
5
|
+
export declare function willGenerateDevPlaneRoute(projectRoot: string, options: SvelteKitOptions): boolean;
|
|
4
6
|
export declare function generateDevPlaneRoute(projectRoot: string, options: SvelteKitOptions): boolean;
|
|
5
7
|
//# sourceMappingURL=dev-plane-route.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev-plane-route.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/dev-plane-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;
|
|
1
|
+
{"version":3,"file":"dev-plane-route.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/dev-plane-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAQH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAEjE,kFAAkF;AAClF,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAUlE;AAED,yEAAyE;AACzE,wBAAgB,yBAAyB,CACvC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAKT;AAED,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAyBT"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AUTO_GENERATED_ROUTE_HEADER } from "./route-header.js";
|
|
2
|
+
import { resolveSvelteKitConfigImport } from "./sveltekit-config-import.js";
|
|
2
3
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { createRequire } from "node:module";
|
|
4
5
|
import { join } from "node:path";
|
|
@@ -24,18 +25,20 @@ function consumerHasSmrtDevMcp(projectRoot) {
|
|
|
24
25
|
return false;
|
|
25
26
|
}
|
|
26
27
|
}
|
|
28
|
+
/** Whether this invocation will own the `_dev/[...tool]` output file. */
|
|
29
|
+
function willGenerateDevPlaneRoute(projectRoot, options) {
|
|
30
|
+
return options.devPlaneRoute?.enabled === true && consumerHasSmrtDevMcp(projectRoot);
|
|
31
|
+
}
|
|
27
32
|
function generateDevPlaneRoute(projectRoot, options) {
|
|
28
33
|
if (options.devPlaneRoute?.enabled !== true) return false;
|
|
29
|
-
if (!
|
|
34
|
+
if (!willGenerateDevPlaneRoute(projectRoot, options)) {
|
|
30
35
|
console.log("[smrt] Skipping _dev route - @happyvertical/smrt-dev-mcp/dev-plane is not resolvable; install @happyvertical/smrt-dev-mcp as a devDependency");
|
|
31
36
|
return false;
|
|
32
37
|
}
|
|
33
|
-
const configPath = options.configPath || "src/lib/server";
|
|
34
|
-
const configFileName = (options.configFileName || "smrt.ts").replace(/\.ts$/, "");
|
|
35
38
|
const routeDir = join(projectRoot, options.routesDir, "_dev", "[...tool]");
|
|
36
39
|
if (!existsSync(routeDir)) mkdirSync(routeDir, { recursive: true });
|
|
37
40
|
const filePath = join(routeDir, "+server.ts");
|
|
38
|
-
writeFileSync(filePath, generateDevPlaneRouteTemplate(
|
|
41
|
+
writeFileSync(filePath, generateDevPlaneRouteTemplate(resolveSvelteKitConfigImport(projectRoot, routeDir, options)), "utf-8");
|
|
39
42
|
console.log(`[smrt] Generated: ${filePath}`);
|
|
40
43
|
return true;
|
|
41
44
|
}
|
|
@@ -86,6 +89,6 @@ export const POST = handle;
|
|
|
86
89
|
`;
|
|
87
90
|
}
|
|
88
91
|
//#endregion
|
|
89
|
-
export { consumerHasSmrtDevMcp, generateDevPlaneRoute };
|
|
92
|
+
export { consumerHasSmrtDevMcp, generateDevPlaneRoute, willGenerateDevPlaneRoute };
|
|
90
93
|
|
|
91
94
|
//# sourceMappingURL=dev-plane-route.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev-plane-route.js","names":[],"sources":["../../src/vite-plugin/dev-plane-route.ts"],"sourcesContent":["/**\n * Generated `_dev/[...tool]/+server.ts` route: the in-app runtime dev-plane\n * (#2782). Opt-in via `sveltekit: { devPlaneRoute: { enabled: true } }`.\n *\n * The route is dev-only twice over: it 404s unless SvelteKit's `dev` flag is\n * set, and it refuses to start without `SMRT_DEV_MCP_TOKEN`. Everything else\n * — loopback checks, bearer auth, the positive read-only catalog, the MCP\n * endpoint — lives in `@happyvertical/smrt-dev-mcp/dev-plane`, which the\n * generated file imports at consumer runtime. Like `_resources`, the route is\n * only emitted when that subpath resolves from the consumer.\n */\n\nimport { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport { join } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { AUTO_GENERATED_ROUTE_HEADER } from './route-header.js';\nimport type { SvelteKitOptions } from './sveltekit-generator.js';\n\n/** Whether `@happyvertical/smrt-dev-mcp/dev-plane` resolves from the consumer. */\nexport function consumerHasSmrtDevMcp(projectRoot: string): boolean {\n try {\n const consumerRequire = createRequire(\n pathToFileURL(join(projectRoot, 'package.json')).href,\n );\n consumerRequire.resolve('@happyvertical/smrt-dev-mcp/dev-plane');\n return true;\n } catch {\n return false;\n }\n}\n\nexport function generateDevPlaneRoute(\n projectRoot: string,\n options: SvelteKitOptions,\n): boolean {\n if (options.devPlaneRoute?.enabled !== true) {\n return false;\n }\n if (!
|
|
1
|
+
{"version":3,"file":"dev-plane-route.js","names":[],"sources":["../../src/vite-plugin/dev-plane-route.ts"],"sourcesContent":["/**\n * Generated `_dev/[...tool]/+server.ts` route: the in-app runtime dev-plane\n * (#2782). Opt-in via `sveltekit: { devPlaneRoute: { enabled: true } }`.\n *\n * The route is dev-only twice over: it 404s unless SvelteKit's `dev` flag is\n * set, and it refuses to start without `SMRT_DEV_MCP_TOKEN`. Everything else\n * — loopback checks, bearer auth, the positive read-only catalog, the MCP\n * endpoint — lives in `@happyvertical/smrt-dev-mcp/dev-plane`, which the\n * generated file imports at consumer runtime. Like `_resources`, the route is\n * only emitted when that subpath resolves from the consumer.\n */\n\nimport { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport { join } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { AUTO_GENERATED_ROUTE_HEADER } from './route-header.js';\nimport { resolveSvelteKitConfigImport } from './sveltekit-config-import.js';\nimport type { SvelteKitOptions } from './sveltekit-generator.js';\n\n/** Whether `@happyvertical/smrt-dev-mcp/dev-plane` resolves from the consumer. */\nexport function consumerHasSmrtDevMcp(projectRoot: string): boolean {\n try {\n const consumerRequire = createRequire(\n pathToFileURL(join(projectRoot, 'package.json')).href,\n );\n consumerRequire.resolve('@happyvertical/smrt-dev-mcp/dev-plane');\n return true;\n } catch {\n return false;\n }\n}\n\n/** Whether this invocation will own the `_dev/[...tool]` output file. */\nexport function willGenerateDevPlaneRoute(\n projectRoot: string,\n options: SvelteKitOptions,\n): boolean {\n return (\n options.devPlaneRoute?.enabled === true &&\n consumerHasSmrtDevMcp(projectRoot)\n );\n}\n\nexport function generateDevPlaneRoute(\n projectRoot: string,\n options: SvelteKitOptions,\n): boolean {\n if (options.devPlaneRoute?.enabled !== true) {\n return false;\n }\n if (!willGenerateDevPlaneRoute(projectRoot, options)) {\n console.log(\n '[smrt] Skipping _dev route - @happyvertical/smrt-dev-mcp/dev-plane is ' +\n 'not resolvable; install @happyvertical/smrt-dev-mcp as a devDependency',\n );\n return false;\n }\n const routeDir = join(projectRoot, options.routesDir, '_dev', '[...tool]');\n if (!existsSync(routeDir)) {\n mkdirSync(routeDir, { recursive: true });\n }\n const filePath = join(routeDir, '+server.ts');\n writeFileSync(\n filePath,\n generateDevPlaneRouteTemplate(\n resolveSvelteKitConfigImport(projectRoot, routeDir, options),\n ),\n 'utf-8',\n );\n console.log(`[smrt] Generated: ${filePath}`);\n return true;\n}\n\nfunction generateDevPlaneRouteTemplate(configImport: string): string {\n return `${AUTO_GENERATED_ROUTE_HEADER}\n// DO NOT EDIT - changes will be overwritten\n//\n// In-app runtime dev-plane (#2782). Dev mode only. Serves the read-only\n// runtime tools over JSON (GET|POST /_dev/<tool>, GET /_dev for the catalog)\n// and MCP (POST /_dev/mcp) against this app's live registry and database.\n// Requires SMRT_DEV_MCP_TOKEN; every request must present it as a bearer\n// token from loopback. Never mounted in production builds.\nimport { dev } from '$app/environment';\nimport { error } from '@sveltejs/kit';\nimport type { RequestHandler } from './$types';\nimport { createDevPlane, type DevPlane } from '@happyvertical/smrt-dev-mcp/dev-plane';\nimport { getSmrtConfig } from '${configImport}';\n\nlet plane: DevPlane | null = null;\n\nfunction getPlane(): DevPlane | null {\n if (!dev) return null;\n const token = process.env.SMRT_DEV_MCP_TOKEN?.trim();\n if (!token) return null;\n if (!plane) {\n // The default config's db is what the app itself opens; passing its\n // url/type makes the SDK connection cache hand the plane the same handle.\n const db = getSmrtConfig('__smrt_dev_plane__').db as\n | { url?: string; type?: string }\n | undefined;\n plane = createDevPlane({ token, projectRoot: process.cwd(), db });\n }\n return plane;\n}\n\nconst handle: RequestHandler = async ({ request, url }) => {\n if (!dev) throw error(404, 'Not found');\n const active = getPlane();\n if (!active) {\n throw error(503, 'SMRT_DEV_MCP_TOKEN is not set; the dev-plane is disabled');\n }\n const base = url.pathname.slice(0, url.pathname.indexOf('/_dev') + '/_dev'.length);\n return active.handleRequest(request, base);\n};\n\nexport const GET = handle;\nexport const POST = handle;\n`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,sBAAsB,aAA8B;CAClE,IAAI;EAIF,cAFE,cAAc,KAAK,aAAa,cAAc,CAAC,CAAC,CAAC,IAEnD,CAAA,CAAgB,QAAQ,uCAAuC;EAC/D,OAAO;CACT,QAAQ;EACN,OAAO;CACT;AACF;;AAGA,SAAgB,0BACd,aACA,SACS;CACT,OACE,QAAQ,eAAe,YAAY,QACnC,sBAAsB,WAAW;AAErC;AAEA,SAAgB,sBACd,aACA,SACS;CACT,IAAI,QAAQ,eAAe,YAAY,MACrC,OAAO;CAET,IAAI,CAAC,0BAA0B,aAAa,OAAO,GAAG;EACpD,QAAQ,IACN,8IAEF;EACA,OAAO;CACT;CACA,MAAM,WAAW,KAAK,aAAa,QAAQ,WAAW,QAAQ,WAAW;CACzE,IAAI,CAAC,WAAW,QAAQ,GACtB,UAAU,UAAU,EAAE,WAAW,KAAK,CAAC;CAEzC,MAAM,WAAW,KAAK,UAAU,YAAY;CAC5C,cACE,UACA,8BACE,6BAA6B,aAAa,UAAU,OAAO,CAC7D,GACA,OACF;CACA,QAAQ,IAAI,qBAAqB,UAAU;CAC3C,OAAO;AACT;AAEA,SAAS,8BAA8B,cAA8B;CACnE,OAAO,GAAG,4BAA4B;;;;;;;;;;;;iCAYP,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgC9C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events-route.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/events-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAIH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"events-route.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/events-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAIH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAO5D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAEjE;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,EAAE,gBAAgB,EACzB,eAAe,CAAC,EAAE,MAAM,GACvB,OAAO,CA+BT"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AUTO_GENERATED_ROUTE_HEADER } from "./route-header.js";
|
|
2
|
+
import { resolveSvelteKitConfigImport } from "./sveltekit-config-import.js";
|
|
2
3
|
import { manifestHasTenantScopedObject, resolveAnchorClassName } from "./changes-route.js";
|
|
3
4
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
4
5
|
import { join } from "node:path";
|
|
@@ -44,7 +45,7 @@ function generateEventsRoute(projectRoot, manifest, options, webManifestHash) {
|
|
|
44
45
|
return false;
|
|
45
46
|
}
|
|
46
47
|
const routeDir = join(projectRoot, options.routesDir, "_events");
|
|
47
|
-
const content = generateEventsRouteTemplate(anchorClassName, manifestHasTenantScopedObject(manifest), webManifestHash, options.eventsRoute?.maxSubscribers, normalizeAllowedOrigins(options.eventsRoute?.allowedOrigins), options.eventsRoute?.allowCredentials === true);
|
|
48
|
+
const content = generateEventsRouteTemplate(anchorClassName, manifestHasTenantScopedObject(manifest), webManifestHash, options.eventsRoute?.maxSubscribers, normalizeAllowedOrigins(options.eventsRoute?.allowedOrigins), options.eventsRoute?.allowCredentials === true, resolveSvelteKitConfigImport(projectRoot, routeDir, options));
|
|
48
49
|
if (!existsSync(routeDir)) mkdirSync(routeDir, { recursive: true });
|
|
49
50
|
const filePath = join(routeDir, "+server.ts");
|
|
50
51
|
writeFileSync(filePath, content, "utf-8");
|
|
@@ -123,7 +124,7 @@ export const OPTIONS: RequestHandler = async ({ request }) => {
|
|
|
123
124
|
`
|
|
124
125
|
};
|
|
125
126
|
}
|
|
126
|
-
function generateEventsRouteTemplate(anchorClassName, tenantScoped, manifestHash, maxSubscribers, allowedOrigins, allowCredentials) {
|
|
127
|
+
function generateEventsRouteTemplate(anchorClassName, tenantScoped, manifestHash, maxSubscribers, allowedOrigins, allowCredentials, configImport) {
|
|
127
128
|
const configuredMaxSubscribers = maxSubscribers !== void 0 && Number.isFinite(maxSubscribers) && maxSubscribers >= 0 ? Math.floor(maxSubscribers) : void 0;
|
|
128
129
|
const maxSubscribersArg = configuredMaxSubscribers === void 0 ? "" : `, ${JSON.stringify(configuredMaxSubscribers)}`;
|
|
129
130
|
const manifestHashLiteral = manifestHash === void 0 ? "undefined" : JSON.stringify(manifestHash);
|
|
@@ -166,7 +167,7 @@ import {
|
|
|
166
167
|
resolveDispatchTenantScope,
|
|
167
168
|
tryReserveChangeEventSubscriberSlot,
|
|
168
169
|
} from '@happyvertical/smrt-core';
|
|
169
|
-
import { getCollection } from '$
|
|
170
|
+
import { getCollection } from '${configImport}';
|
|
170
171
|
import type { RequestHandler } from './$types';
|
|
171
172
|
|
|
172
173
|
const MANIFEST_HASH = ${manifestHashLiteral};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events-route.js","names":[],"sources":["../../src/vite-plugin/events-route.ts"],"sourcesContent":["/**\n * SvelteKit `_events` route generation — live change signals (issue #1763,\n * SERVER half; parent PRD #1755).\n *\n * Emits `{routesDir}/_events/+server.ts` — an auth-guarded, tenant-scoped\n * Server-Sent-Events endpoint that streams coarse change signals (no row\n * payloads). It is the push companion to the generated `_changes` route\n * (#1758). Kept in its own module so `sveltekit-generator.ts` only carries a\n * one-line registration.\n *\n * The stream lifecycle (subscribe, catch-up, heartbeat, teardown) lives in\n * core's `buildChangeEventStream`, imported by the generated file — so the\n * emitted route stays THIN and the stream logic is written and tested once.\n *\n * Design notes (mirror `changes-route.ts`):\n * - **Fail-closed auth** (#1540 posture): the handler requires an authenticated\n * principal on `locals`. The signal stream spans every table, so per-model\n * `api: { public }` opt-outs deliberately do not apply.\n * - **Tenant scoping**: when the project has tenant-scoped objects, the route\n * establishes tenant context from `locals` exactly like the `_changes` route,\n * then captures the resolved scope ONCE at connection open and passes it to\n * the stream (delivery filters against that fixed value — it cannot\n * re-resolve per signal outside the request's ALS context).\n * - **Database resolution**: anchors on the project's first generated\n * collection (alphabetical) via `getCollection()`, exactly as `_changes`.\n * - Cleanup rides the generated-route sweep: the file starts with\n * {@link AUTO_GENERATED_ROUTE_HEADER}.\n */\n\nimport { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { SmartObjectManifest } from '../scanner/types';\nimport {\n manifestHasTenantScopedObject,\n resolveAnchorClassName,\n} from './changes-route.js';\nimport { AUTO_GENERATED_ROUTE_HEADER } from './route-header.js';\nimport type { SvelteKitOptions } from './sveltekit-generator.js';\n\n/**\n * Generate the `_events/+server.ts` route. Returns true when a route was\n * written. Disabled with `sveltekit: { eventsRoute: { enabled: false } }`;\n * skipped (with a log line) when the manifest has no objects to anchor on.\n */\nexport function generateEventsRoute(\n projectRoot: string,\n manifest: SmartObjectManifest,\n options: SvelteKitOptions,\n webManifestHash?: string,\n): boolean {\n if (options.eventsRoute?.enabled === false) {\n return false;\n }\n\n const anchorClassName = resolveAnchorClassName(manifest);\n if (!anchorClassName) {\n console.log(\n '[smrt] Skipping _events route - no SMRT objects to anchor the database on',\n );\n return false;\n }\n\n const routeDir = join(projectRoot, options.routesDir, '_events');\n const content = generateEventsRouteTemplate(\n anchorClassName,\n manifestHasTenantScopedObject(manifest),\n webManifestHash,\n options.eventsRoute?.maxSubscribers,\n normalizeAllowedOrigins(options.eventsRoute?.allowedOrigins),\n options.eventsRoute?.allowCredentials === true,\n );\n\n if (!existsSync(routeDir)) {\n mkdirSync(routeDir, { recursive: true });\n }\n const filePath = join(routeDir, '+server.ts');\n writeFileSync(filePath, content, 'utf-8');\n console.log(`[smrt] Generated: ${filePath}`);\n return true;\n}\n\n/**\n * Trim, de-duplicate, and drop empty entries from a configured origin\n * allowlist. An `undefined`/empty result means \"same-origin only\" — the\n * generated route emits no CORS surface at all (fail-closed default, #1861).\n */\nfunction normalizeAllowedOrigins(\n origins: string[] | undefined,\n): string[] | undefined {\n if (!Array.isArray(origins)) return undefined;\n const cleaned = [\n ...new Set(\n origins\n .filter((o): o is string => typeof o === 'string')\n .map((o) => o.trim())\n .filter((o) => o.length > 0),\n ),\n ];\n return cleaned.length > 0 ? cleaned : undefined;\n}\n\n/**\n * Build the CORS fragments injected into the generated `_events` route (#1861)\n * when an origin allowlist is configured. Mirrors the runtime REST generator's\n * posture: the request `Origin` is echoed only when it is on the allowlist\n * (never `*`), `Vary: Origin` is always set on a matched response, and\n * `Access-Control-Allow-Credentials: true` is added only when credentials are\n * opted in — so cookies flow to an allow-listed cross-origin `EventSource`\n * while an unmatched origin gets no CORS surface (fail-closed).\n */\nfunction buildEventsCorsBlock(\n allowedOrigins: string[],\n allowCredentials: boolean,\n): {\n helpers: string;\n streamHeadersSpread: string;\n capacityReturn: string;\n optionsHandler: string;\n} {\n const originsLiteral = JSON.stringify(allowedOrigins);\n const credentialsLine = allowCredentials\n ? \"\\n headers['Access-Control-Allow-Credentials'] = 'true';\"\n : '';\n const helpers = `\n// Credentialed cross-origin CORS (#1861). Opt-in via\n// \\`sveltekit.eventsRoute.allowedOrigins\\` (+ \\`allowCredentials\\`); an unmatched or\n// absent Origin gets no CORS surface, so this route stays same-origin only by\n// default. The Origin is echoed only when allow-listed (never \\`*\\`), which is\n// what makes credentialed CORS safe. CORS never authorizes — the fail-closed\n// \\`requireRouteAuth\\` guard below is unchanged; CORS only lets an allow-listed\n// browser's cookies reach it.\nconst CORS_ALLOWED_ORIGINS = new Set(${originsLiteral});\n\nfunction eventsCorsHeaders(request: Request): Record<string, string> {\n const origin = request.headers.get('origin');\n if (!origin || !CORS_ALLOWED_ORIGINS.has(origin)) return {};\n const headers: Record<string, string> = {\n 'Access-Control-Allow-Origin': origin,\n Vary: 'Origin',\n };${credentialsLine}\n return headers;\n}\n\nfunction applyEventsCors(response: Response, request: Request): Response {\n for (const [key, value] of Object.entries(eventsCorsHeaders(request))) {\n response.headers.set(key, value);\n }\n return response;\n}\n`;\n const streamHeadersSpread = '...eventsCorsHeaders(request),\\n ';\n const capacityReturn =\n ' return applyEventsCors(eventStreamCapacityExceededResponse(), request);';\n const optionsHandler = `\n// Preflight for a credentialed cross-origin subscription. \\`EventSource\\` GETs\n// are \"simple\" requests (no preflight), but a fetch-based SSE client may\n// preflight — answer it for allow-listed origins only.\nexport const OPTIONS: RequestHandler = async ({ request }) => {\n const headers = eventsCorsHeaders(request);\n if (!('Access-Control-Allow-Origin' in headers)) {\n return new Response(null, { status: 403 });\n }\n return new Response(null, {\n status: 204,\n headers: {\n ...headers,\n 'Access-Control-Allow-Methods': 'GET,OPTIONS',\n 'Access-Control-Allow-Headers': 'Authorization,Last-Event-ID,Content-Type',\n 'Access-Control-Max-Age': '86400',\n },\n });\n};\n`;\n return { helpers, streamHeadersSpread, capacityReturn, optionsHandler };\n}\n\nfunction generateEventsRouteTemplate(\n anchorClassName: string,\n tenantScoped: boolean,\n manifestHash: string | undefined,\n maxSubscribers: number | undefined,\n allowedOrigins: string[] | undefined,\n allowCredentials: boolean,\n): string {\n const configuredMaxSubscribers =\n maxSubscribers !== undefined &&\n Number.isFinite(maxSubscribers) &&\n maxSubscribers >= 0\n ? Math.floor(maxSubscribers)\n : undefined;\n const maxSubscribersArg =\n configuredMaxSubscribers === undefined\n ? ''\n : `, ${JSON.stringify(configuredMaxSubscribers)}`;\n const manifestHashLiteral =\n manifestHash === undefined ? 'undefined' : JSON.stringify(manifestHash);\n // CORS is emitted only when an explicit allowlist is configured (#1861).\n // Without one the generated route carries no CORS surface — same-origin only,\n // byte-for-byte the pre-#1861 output.\n const corsEnabled = allowedOrigins !== undefined;\n const corsBlock = corsEnabled\n ? buildEventsCorsBlock(allowedOrigins, allowCredentials)\n : {\n helpers: '',\n streamHeadersSpread: '',\n capacityReturn: ' return eventStreamCapacityExceededResponse();',\n optionsHandler: '',\n };\n const tenantHelper = tenantScoped\n ? `\nimport { enterTenantContext, hasTenantContext } from '@happyvertical/smrt-tenancy';\n\nfunction establishTenantContext(locals: unknown): void {\n if (hasTenantContext()) return;\n if (!locals || typeof locals !== 'object') return;\n const l = locals as Record<string, unknown>;\n const user = l.user as Record<string, unknown> | undefined;\n const session = l.session as Record<string, unknown> | undefined;\n const tenantId = l.tenantId ?? user?.tenantId ?? session?.tenantId;\n if (typeof tenantId === 'string' && tenantId) {\n enterTenantContext({ tenantId });\n }\n}\n`\n : '';\n const tenantCall = tenantScoped ? '\\n establishTenantContext(locals);' : '';\n\n return `${AUTO_GENERATED_ROUTE_HEADER}\n// DO NOT EDIT - changes will be overwritten\n//\n// GET /_events — Server-Sent-Events stream of coarse change signals (#1763).\n// Part of the client/mobile sync contract: each event names {table, operation,\n// rowId, tenantId} (NEVER a row payload — authorization stays on the read path)\n// and carries a seq cursor in the SSE id: field. A reconnecting EventSource\n// resends Last-Event-ID; the route replays missed changes from it, then streams\n// live. Same-origin only unless an origin allowlist is configured (#1861).\n\nimport { error } from '@sveltejs/kit';\nimport {\n buildChangeEventStream,\n eventStreamCapacityExceededResponse,\n resolveDispatchTenantScope,\n tryReserveChangeEventSubscriberSlot,\n} from '@happyvertical/smrt-core';\nimport { getCollection } from '$lib/server/smrt';\nimport type { RequestHandler } from './$types';\n\nconst MANIFEST_HASH = ${manifestHashLiteral};\n${corsBlock.helpers}\n// Fail-closed authorization (#1540): the signal stream spans every table, so\n// it is never public — an authenticated principal on \\`locals\\` is required.\nfunction hasAuthenticatedPrincipal(locals: unknown): boolean {\n if (!locals || typeof locals !== 'object') return false;\n const l = locals as Record<string, unknown>;\n const isResolvedPrincipal = (v: unknown) =>\n typeof v === 'object' && v !== null;\n return (\n isResolvedPrincipal(l.user) ||\n isResolvedPrincipal(l.session) ||\n l.smrtAuth === true\n );\n}\n\nfunction requireRouteAuth(locals: unknown): void {\n if (!hasAuthenticatedPrincipal(locals)) {\n throw error(401, 'Authentication required');\n }\n}\n\n// Last-Event-ID (auto-reconnect) takes precedence over ?since=; default is\n// live-forward only (no catch-up replay).\nfunction parseCursor(request: Request, url: URL): number | null {\n const lastEventId = request.headers.get('Last-Event-ID');\n if (lastEventId !== null && lastEventId.trim() !== '') {\n const n = Number(lastEventId);\n if (Number.isFinite(n) && n >= 0) return Math.floor(n);\n }\n const since = url.searchParams.get('since');\n if (since !== null && since.trim() !== '') {\n const n = Number(since);\n if (Number.isFinite(n) && n >= 0) return Math.floor(n);\n }\n return null;\n}\n${tenantHelper}\nexport const GET: RequestHandler = async ({ locals, url, request }) => {\n requireRouteAuth(locals);${tenantCall}\n\n const cursor = parseCursor(request, url);\n // Capture the tenant scope ONCE at connection open — signal delivery runs\n // outside this request's tenant context and must filter against a fixed\n // value, not a per-signal re-resolution. resolveDispatchTenantScope() reads\n // the context established above (fail-closed: tenancy on + no tenant → global\n // rows only; tenancy off → unenforced, all rows).\n const tenantScope = resolveDispatchTenantScope();\n\n // The feed lives in the project's database; anchor on the\n // ${anchorClassName} collection to reuse its configured connection.\n const collection = await getCollection('${anchorClassName}');\n const releaseSubscriberSlot = tryReserveChangeEventSubscriberSlot(collection.db${maxSubscribersArg});\n if (!releaseSubscriberSlot) {\n${corsBlock.capacityReturn}\n }\n return new Response(\n buildChangeEventStream(collection.db, {\n cursor,\n tenantScope,\n manifestHash: MANIFEST_HASH,\n releaseSubscriberSlot,\n }),\n {\n status: 200,\n headers: {\n ${corsBlock.streamHeadersSpread}'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n },\n },\n );\n};\n${corsBlock.optionsHandler}`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CA,SAAgB,oBACd,aACA,UACA,SACA,iBACS;CACT,IAAI,QAAQ,aAAa,YAAY,OACnC,OAAO;CAGT,MAAM,kBAAkB,uBAAuB,QAAQ;CACvD,IAAI,CAAC,iBAAiB;EACpB,QAAQ,IACN,2EACF;EACA,OAAO;CACT;CAEA,MAAM,WAAW,KAAK,aAAa,QAAQ,WAAW,SAAS;CAC/D,MAAM,UAAU,4BACd,iBACA,8BAA8B,QAAQ,GACtC,iBACA,QAAQ,aAAa,gBACrB,wBAAwB,QAAQ,aAAa,cAAc,GAC3D,QAAQ,aAAa,qBAAqB,IAC5C;CAEA,IAAI,CAAC,WAAW,QAAQ,GACtB,UAAU,UAAU,EAAE,WAAW,KAAK,CAAC;CAEzC,MAAM,WAAW,KAAK,UAAU,YAAY;CAC5C,cAAc,UAAU,SAAS,OAAO;CACxC,QAAQ,IAAI,qBAAqB,UAAU;CAC3C,OAAO;AACT;;;;;;AAOA,SAAS,wBACP,SACsB;CACtB,IAAI,CAAC,MAAM,QAAQ,OAAO,GAAG,OAAO,KAAA;CACpC,MAAM,UAAU,CACd,GAAG,IAAI,IACL,QACG,QAAQ,MAAmB,OAAO,MAAM,QAAQ,CAAC,CACjD,KAAK,MAAM,EAAE,KAAK,CAAC,CAAC,CACpB,QAAQ,MAAM,EAAE,SAAS,CAAC,CAC/B,CACF;CACA,OAAO,QAAQ,SAAS,IAAI,UAAU,KAAA;AACxC;;;;;;;;;;AAWA,SAAS,qBACP,gBACA,kBAMA;CAuDA,OAAO;EAAE,SAAA;;;;;;;;uCAtDc,KAAK,UAAU,cAYD,EAAe;;;;;;;;MAX5B,mBACpB,8DACA,GAiBgB;;;;;;;;;;;EAkCF,qBAAA;EAAqB,gBAAA;EAAgB,gBAAA;;;;;;;;;;;;;;;;;;;;CAAe;AACxE;AAEA,SAAS,4BACP,iBACA,cACA,cACA,gBACA,gBACA,kBACQ;CACR,MAAM,2BACJ,mBAAmB,KAAA,KACnB,OAAO,SAAS,cAAc,KAC9B,kBAAkB,IACd,KAAK,MAAM,cAAc,IACzB,KAAA;CACN,MAAM,oBACJ,6BAA6B,KAAA,IACzB,KACA,KAAK,KAAK,UAAU,wBAAwB;CAClD,MAAM,sBACJ,iBAAiB,KAAA,IAAY,cAAc,KAAK,UAAU,YAAY;CAKxE,MAAM,YADc,mBAAmB,KAAA,IAEnC,qBAAqB,gBAAgB,gBAAgB,IACrD;EACE,SAAS;EACT,qBAAqB;EACrB,gBAAgB;EAChB,gBAAgB;CAClB;CACJ,MAAM,eAAe,eACjB;;;;;;;;;;;;;;IAeA;CACJ,MAAM,aAAa,eAAe,wCAAwC;CAE1E,OAAO,GAAG,4BAA4B;;;;;;;;;;;;;;;;;;;;wBAoBhB,oBAAoB;EAC1C,UAAU,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoClB,aAAa;;6BAEc,WAAW;;;;;;;;;;;OAWjC,gBAAgB;4CACqB,gBAAgB;mFACuB,kBAAkB;;EAEnG,UAAU,eAAe;;;;;;;;;;;;UAYjB,UAAU,oBAAoB;;;;;;;;EAQtC,UAAU;AACZ"}
|
|
1
|
+
{"version":3,"file":"events-route.js","names":[],"sources":["../../src/vite-plugin/events-route.ts"],"sourcesContent":["/**\n * SvelteKit `_events` route generation — live change signals (issue #1763,\n * SERVER half; parent PRD #1755).\n *\n * Emits `{routesDir}/_events/+server.ts` — an auth-guarded, tenant-scoped\n * Server-Sent-Events endpoint that streams coarse change signals (no row\n * payloads). It is the push companion to the generated `_changes` route\n * (#1758). Kept in its own module so `sveltekit-generator.ts` only carries a\n * one-line registration.\n *\n * The stream lifecycle (subscribe, catch-up, heartbeat, teardown) lives in\n * core's `buildChangeEventStream`, imported by the generated file — so the\n * emitted route stays THIN and the stream logic is written and tested once.\n *\n * Design notes (mirror `changes-route.ts`):\n * - **Fail-closed auth** (#1540 posture): the handler requires an authenticated\n * principal on `locals`. The signal stream spans every table, so per-model\n * `api: { public }` opt-outs deliberately do not apply.\n * - **Tenant scoping**: when the project has tenant-scoped objects, the route\n * establishes tenant context from `locals` exactly like the `_changes` route,\n * then captures the resolved scope ONCE at connection open and passes it to\n * the stream (delivery filters against that fixed value — it cannot\n * re-resolve per signal outside the request's ALS context).\n * - **Database resolution**: anchors on the project's first generated\n * collection (alphabetical) via `getCollection()`, exactly as `_changes`.\n * - Cleanup rides the generated-route sweep: the file starts with\n * {@link AUTO_GENERATED_ROUTE_HEADER}.\n */\n\nimport { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { SmartObjectManifest } from '../scanner/types';\nimport {\n manifestHasTenantScopedObject,\n resolveAnchorClassName,\n} from './changes-route.js';\nimport { AUTO_GENERATED_ROUTE_HEADER } from './route-header.js';\nimport { resolveSvelteKitConfigImport } from './sveltekit-config-import.js';\nimport type { SvelteKitOptions } from './sveltekit-generator.js';\n\n/**\n * Generate the `_events/+server.ts` route. Returns true when a route was\n * written. Disabled with `sveltekit: { eventsRoute: { enabled: false } }`;\n * skipped (with a log line) when the manifest has no objects to anchor on.\n */\nexport function generateEventsRoute(\n projectRoot: string,\n manifest: SmartObjectManifest,\n options: SvelteKitOptions,\n webManifestHash?: string,\n): boolean {\n if (options.eventsRoute?.enabled === false) {\n return false;\n }\n\n const anchorClassName = resolveAnchorClassName(manifest);\n if (!anchorClassName) {\n console.log(\n '[smrt] Skipping _events route - no SMRT objects to anchor the database on',\n );\n return false;\n }\n\n const routeDir = join(projectRoot, options.routesDir, '_events');\n const content = generateEventsRouteTemplate(\n anchorClassName,\n manifestHasTenantScopedObject(manifest),\n webManifestHash,\n options.eventsRoute?.maxSubscribers,\n normalizeAllowedOrigins(options.eventsRoute?.allowedOrigins),\n options.eventsRoute?.allowCredentials === true,\n resolveSvelteKitConfigImport(projectRoot, routeDir, options),\n );\n\n if (!existsSync(routeDir)) {\n mkdirSync(routeDir, { recursive: true });\n }\n const filePath = join(routeDir, '+server.ts');\n writeFileSync(filePath, content, 'utf-8');\n console.log(`[smrt] Generated: ${filePath}`);\n return true;\n}\n\n/**\n * Trim, de-duplicate, and drop empty entries from a configured origin\n * allowlist. An `undefined`/empty result means \"same-origin only\" — the\n * generated route emits no CORS surface at all (fail-closed default, #1861).\n */\nfunction normalizeAllowedOrigins(\n origins: string[] | undefined,\n): string[] | undefined {\n if (!Array.isArray(origins)) return undefined;\n const cleaned = [\n ...new Set(\n origins\n .filter((o): o is string => typeof o === 'string')\n .map((o) => o.trim())\n .filter((o) => o.length > 0),\n ),\n ];\n return cleaned.length > 0 ? cleaned : undefined;\n}\n\n/**\n * Build the CORS fragments injected into the generated `_events` route (#1861)\n * when an origin allowlist is configured. Mirrors the runtime REST generator's\n * posture: the request `Origin` is echoed only when it is on the allowlist\n * (never `*`), `Vary: Origin` is always set on a matched response, and\n * `Access-Control-Allow-Credentials: true` is added only when credentials are\n * opted in — so cookies flow to an allow-listed cross-origin `EventSource`\n * while an unmatched origin gets no CORS surface (fail-closed).\n */\nfunction buildEventsCorsBlock(\n allowedOrigins: string[],\n allowCredentials: boolean,\n): {\n helpers: string;\n streamHeadersSpread: string;\n capacityReturn: string;\n optionsHandler: string;\n} {\n const originsLiteral = JSON.stringify(allowedOrigins);\n const credentialsLine = allowCredentials\n ? \"\\n headers['Access-Control-Allow-Credentials'] = 'true';\"\n : '';\n const helpers = `\n// Credentialed cross-origin CORS (#1861). Opt-in via\n// \\`sveltekit.eventsRoute.allowedOrigins\\` (+ \\`allowCredentials\\`); an unmatched or\n// absent Origin gets no CORS surface, so this route stays same-origin only by\n// default. The Origin is echoed only when allow-listed (never \\`*\\`), which is\n// what makes credentialed CORS safe. CORS never authorizes — the fail-closed\n// \\`requireRouteAuth\\` guard below is unchanged; CORS only lets an allow-listed\n// browser's cookies reach it.\nconst CORS_ALLOWED_ORIGINS = new Set(${originsLiteral});\n\nfunction eventsCorsHeaders(request: Request): Record<string, string> {\n const origin = request.headers.get('origin');\n if (!origin || !CORS_ALLOWED_ORIGINS.has(origin)) return {};\n const headers: Record<string, string> = {\n 'Access-Control-Allow-Origin': origin,\n Vary: 'Origin',\n };${credentialsLine}\n return headers;\n}\n\nfunction applyEventsCors(response: Response, request: Request): Response {\n for (const [key, value] of Object.entries(eventsCorsHeaders(request))) {\n response.headers.set(key, value);\n }\n return response;\n}\n`;\n const streamHeadersSpread = '...eventsCorsHeaders(request),\\n ';\n const capacityReturn =\n ' return applyEventsCors(eventStreamCapacityExceededResponse(), request);';\n const optionsHandler = `\n// Preflight for a credentialed cross-origin subscription. \\`EventSource\\` GETs\n// are \"simple\" requests (no preflight), but a fetch-based SSE client may\n// preflight — answer it for allow-listed origins only.\nexport const OPTIONS: RequestHandler = async ({ request }) => {\n const headers = eventsCorsHeaders(request);\n if (!('Access-Control-Allow-Origin' in headers)) {\n return new Response(null, { status: 403 });\n }\n return new Response(null, {\n status: 204,\n headers: {\n ...headers,\n 'Access-Control-Allow-Methods': 'GET,OPTIONS',\n 'Access-Control-Allow-Headers': 'Authorization,Last-Event-ID,Content-Type',\n 'Access-Control-Max-Age': '86400',\n },\n });\n};\n`;\n return { helpers, streamHeadersSpread, capacityReturn, optionsHandler };\n}\n\nfunction generateEventsRouteTemplate(\n anchorClassName: string,\n tenantScoped: boolean,\n manifestHash: string | undefined,\n maxSubscribers: number | undefined,\n allowedOrigins: string[] | undefined,\n allowCredentials: boolean,\n configImport: string,\n): string {\n const configuredMaxSubscribers =\n maxSubscribers !== undefined &&\n Number.isFinite(maxSubscribers) &&\n maxSubscribers >= 0\n ? Math.floor(maxSubscribers)\n : undefined;\n const maxSubscribersArg =\n configuredMaxSubscribers === undefined\n ? ''\n : `, ${JSON.stringify(configuredMaxSubscribers)}`;\n const manifestHashLiteral =\n manifestHash === undefined ? 'undefined' : JSON.stringify(manifestHash);\n // CORS is emitted only when an explicit allowlist is configured (#1861).\n // Without one the generated route carries no CORS surface — same-origin only,\n // byte-for-byte the pre-#1861 output.\n const corsEnabled = allowedOrigins !== undefined;\n const corsBlock = corsEnabled\n ? buildEventsCorsBlock(allowedOrigins, allowCredentials)\n : {\n helpers: '',\n streamHeadersSpread: '',\n capacityReturn: ' return eventStreamCapacityExceededResponse();',\n optionsHandler: '',\n };\n const tenantHelper = tenantScoped\n ? `\nimport { enterTenantContext, hasTenantContext } from '@happyvertical/smrt-tenancy';\n\nfunction establishTenantContext(locals: unknown): void {\n if (hasTenantContext()) return;\n if (!locals || typeof locals !== 'object') return;\n const l = locals as Record<string, unknown>;\n const user = l.user as Record<string, unknown> | undefined;\n const session = l.session as Record<string, unknown> | undefined;\n const tenantId = l.tenantId ?? user?.tenantId ?? session?.tenantId;\n if (typeof tenantId === 'string' && tenantId) {\n enterTenantContext({ tenantId });\n }\n}\n`\n : '';\n const tenantCall = tenantScoped ? '\\n establishTenantContext(locals);' : '';\n\n return `${AUTO_GENERATED_ROUTE_HEADER}\n// DO NOT EDIT - changes will be overwritten\n//\n// GET /_events — Server-Sent-Events stream of coarse change signals (#1763).\n// Part of the client/mobile sync contract: each event names {table, operation,\n// rowId, tenantId} (NEVER a row payload — authorization stays on the read path)\n// and carries a seq cursor in the SSE id: field. A reconnecting EventSource\n// resends Last-Event-ID; the route replays missed changes from it, then streams\n// live. Same-origin only unless an origin allowlist is configured (#1861).\n\nimport { error } from '@sveltejs/kit';\nimport {\n buildChangeEventStream,\n eventStreamCapacityExceededResponse,\n resolveDispatchTenantScope,\n tryReserveChangeEventSubscriberSlot,\n} from '@happyvertical/smrt-core';\nimport { getCollection } from '${configImport}';\nimport type { RequestHandler } from './$types';\n\nconst MANIFEST_HASH = ${manifestHashLiteral};\n${corsBlock.helpers}\n// Fail-closed authorization (#1540): the signal stream spans every table, so\n// it is never public — an authenticated principal on \\`locals\\` is required.\nfunction hasAuthenticatedPrincipal(locals: unknown): boolean {\n if (!locals || typeof locals !== 'object') return false;\n const l = locals as Record<string, unknown>;\n const isResolvedPrincipal = (v: unknown) =>\n typeof v === 'object' && v !== null;\n return (\n isResolvedPrincipal(l.user) ||\n isResolvedPrincipal(l.session) ||\n l.smrtAuth === true\n );\n}\n\nfunction requireRouteAuth(locals: unknown): void {\n if (!hasAuthenticatedPrincipal(locals)) {\n throw error(401, 'Authentication required');\n }\n}\n\n// Last-Event-ID (auto-reconnect) takes precedence over ?since=; default is\n// live-forward only (no catch-up replay).\nfunction parseCursor(request: Request, url: URL): number | null {\n const lastEventId = request.headers.get('Last-Event-ID');\n if (lastEventId !== null && lastEventId.trim() !== '') {\n const n = Number(lastEventId);\n if (Number.isFinite(n) && n >= 0) return Math.floor(n);\n }\n const since = url.searchParams.get('since');\n if (since !== null && since.trim() !== '') {\n const n = Number(since);\n if (Number.isFinite(n) && n >= 0) return Math.floor(n);\n }\n return null;\n}\n${tenantHelper}\nexport const GET: RequestHandler = async ({ locals, url, request }) => {\n requireRouteAuth(locals);${tenantCall}\n\n const cursor = parseCursor(request, url);\n // Capture the tenant scope ONCE at connection open — signal delivery runs\n // outside this request's tenant context and must filter against a fixed\n // value, not a per-signal re-resolution. resolveDispatchTenantScope() reads\n // the context established above (fail-closed: tenancy on + no tenant → global\n // rows only; tenancy off → unenforced, all rows).\n const tenantScope = resolveDispatchTenantScope();\n\n // The feed lives in the project's database; anchor on the\n // ${anchorClassName} collection to reuse its configured connection.\n const collection = await getCollection('${anchorClassName}');\n const releaseSubscriberSlot = tryReserveChangeEventSubscriberSlot(collection.db${maxSubscribersArg});\n if (!releaseSubscriberSlot) {\n${corsBlock.capacityReturn}\n }\n return new Response(\n buildChangeEventStream(collection.db, {\n cursor,\n tenantScope,\n manifestHash: MANIFEST_HASH,\n releaseSubscriberSlot,\n }),\n {\n status: 200,\n headers: {\n ${corsBlock.streamHeadersSpread}'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n },\n },\n );\n};\n${corsBlock.optionsHandler}`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6CA,SAAgB,oBACd,aACA,UACA,SACA,iBACS;CACT,IAAI,QAAQ,aAAa,YAAY,OACnC,OAAO;CAGT,MAAM,kBAAkB,uBAAuB,QAAQ;CACvD,IAAI,CAAC,iBAAiB;EACpB,QAAQ,IACN,2EACF;EACA,OAAO;CACT;CAEA,MAAM,WAAW,KAAK,aAAa,QAAQ,WAAW,SAAS;CAC/D,MAAM,UAAU,4BACd,iBACA,8BAA8B,QAAQ,GACtC,iBACA,QAAQ,aAAa,gBACrB,wBAAwB,QAAQ,aAAa,cAAc,GAC3D,QAAQ,aAAa,qBAAqB,MAC1C,6BAA6B,aAAa,UAAU,OAAO,CAC7D;CAEA,IAAI,CAAC,WAAW,QAAQ,GACtB,UAAU,UAAU,EAAE,WAAW,KAAK,CAAC;CAEzC,MAAM,WAAW,KAAK,UAAU,YAAY;CAC5C,cAAc,UAAU,SAAS,OAAO;CACxC,QAAQ,IAAI,qBAAqB,UAAU;CAC3C,OAAO;AACT;;;;;;AAOA,SAAS,wBACP,SACsB;CACtB,IAAI,CAAC,MAAM,QAAQ,OAAO,GAAG,OAAO,KAAA;CACpC,MAAM,UAAU,CACd,GAAG,IAAI,IACL,QACG,QAAQ,MAAmB,OAAO,MAAM,QAAQ,CAAC,CACjD,KAAK,MAAM,EAAE,KAAK,CAAC,CAAC,CACpB,QAAQ,MAAM,EAAE,SAAS,CAAC,CAC/B,CACF;CACA,OAAO,QAAQ,SAAS,IAAI,UAAU,KAAA;AACxC;;;;;;;;;;AAWA,SAAS,qBACP,gBACA,kBAMA;CAuDA,OAAO;EAAE,SAAA;;;;;;;;uCAtDc,KAAK,UAAU,cAYD,EAAe;;;;;;;;MAX5B,mBACpB,8DACA,GAiBgB;;;;;;;;;;;EAkCF,qBAAA;EAAqB,gBAAA;EAAgB,gBAAA;;;;;;;;;;;;;;;;;;;;CAAe;AACxE;AAEA,SAAS,4BACP,iBACA,cACA,cACA,gBACA,gBACA,kBACA,cACQ;CACR,MAAM,2BACJ,mBAAmB,KAAA,KACnB,OAAO,SAAS,cAAc,KAC9B,kBAAkB,IACd,KAAK,MAAM,cAAc,IACzB,KAAA;CACN,MAAM,oBACJ,6BAA6B,KAAA,IACzB,KACA,KAAK,KAAK,UAAU,wBAAwB;CAClD,MAAM,sBACJ,iBAAiB,KAAA,IAAY,cAAc,KAAK,UAAU,YAAY;CAKxE,MAAM,YADc,mBAAmB,KAAA,IAEnC,qBAAqB,gBAAgB,gBAAgB,IACrD;EACE,SAAS;EACT,qBAAqB;EACrB,gBAAgB;EAChB,gBAAgB;CAClB;CACJ,MAAM,eAAe,eACjB;;;;;;;;;;;;;;IAeA;CACJ,MAAM,aAAa,eAAe,wCAAwC;CAE1E,OAAO,GAAG,4BAA4B;;;;;;;;;;;;;;;;;iCAiBP,aAAa;;;wBAGtB,oBAAoB;EAC1C,UAAU,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoClB,aAAa;;6BAEc,WAAW;;;;;;;;;;;OAWjC,gBAAgB;4CACqB,gBAAgB;mFACuB,kBAAkB;;EAEnG,UAAU,eAAe;;;;;;;;;;;;UAYjB,UAAU,oBAAoB;;;;;;;;EAQtC,UAAU;AACZ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,KAAK,EACV,2BAA2B,EAC3B,qBAAqB,EAEtB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,MAAM,EAAiC,MAAM,MAAM,CAAC;AAClE,OAAO,EAEL,KAAK,6BAA6B,EACnC,MAAM,2BAA2B,CAAC;AAOnC,OAAO,EAEL,KAAK,mBAAmB,EACzB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EACV,mBAAmB,EACnB,aAAa,EACd,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -9,6 +9,7 @@ import { mergeKnowledgeConfig, resolveFileKnowledgeConfig } from "../knowledge-c
|
|
|
9
9
|
import { renderApiClientCrudType, renderApiClientCustomMethodParameters, selectApiClientEntries } from "./api-client-entries.js";
|
|
10
10
|
import { GENERATED_CLIENT_BASE_METHODS, GENERATED_CLIENT_CRUD_METHODS, generateClientModule } from "./generated-client.js";
|
|
11
11
|
import { importBuildAwareModule } from "./import-build-aware.js";
|
|
12
|
+
import { assertSvelteKitRouteCoordinationComplete, contributeSvelteKitRoutes, expectedSvelteKitRouteOwners, markSvelteKitRouteParticipant } from "./sveltekit-route-coordinator.js";
|
|
12
13
|
import { existsSync, readFileSync } from "node:fs";
|
|
13
14
|
import { dirname, join, resolve } from "node:path";
|
|
14
15
|
import { fileURLToPath } from "node:url";
|
|
@@ -237,8 +238,10 @@ function smrtPlugin(options = {}) {
|
|
|
237
238
|
let hasFreshConfigResolvedManifest = false;
|
|
238
239
|
let configHookManifest = null;
|
|
239
240
|
let generatedRoutesDuringConfig = false;
|
|
241
|
+
let routeLifecycleConfig;
|
|
242
|
+
let routeExpectedOwners = ["producer"];
|
|
240
243
|
async function generateConfiguredSvelteKitRoutes(rootDir, currentManifest) {
|
|
241
|
-
|
|
244
|
+
const routeOptions = {
|
|
242
245
|
enabled: svelteKit.enabled,
|
|
243
246
|
routesDir: svelteKit.routesDir || "src/routes/api",
|
|
244
247
|
objectsDir: svelteKit.objectsDir || "src/lib/objects",
|
|
@@ -249,7 +252,17 @@ function smrtPlugin(options = {}) {
|
|
|
249
252
|
eventsRoute: svelteKit.eventsRoute,
|
|
250
253
|
resourcesRoute: svelteKit.resourcesRoute,
|
|
251
254
|
knowledge: await resolveKnowledgeConfig(rootDir, currentManifest)
|
|
252
|
-
}
|
|
255
|
+
};
|
|
256
|
+
if (routeLifecycleConfig) {
|
|
257
|
+
await contributeSvelteKitRoutes(routeLifecycleConfig, routeExpectedOwners, rootDir, {
|
|
258
|
+
owner: "producer",
|
|
259
|
+
routeManifest: currentManifest,
|
|
260
|
+
semanticManifest: currentManifest,
|
|
261
|
+
options: routeOptions
|
|
262
|
+
});
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
await generateSvelteKitRoutes(rootDir, currentManifest, routeOptions);
|
|
253
266
|
}
|
|
254
267
|
/**
|
|
255
268
|
* Write manifest to .smrt/manifest.json for CLI discovery.
|
|
@@ -360,14 +373,16 @@ function smrtPlugin(options = {}) {
|
|
|
360
373
|
if (validateCliApiCoherence) validateCliIncludeAgainstApi(manifest);
|
|
361
374
|
}
|
|
362
375
|
}
|
|
363
|
-
|
|
376
|
+
const plugin = {
|
|
364
377
|
name: "smrt-auto-service",
|
|
365
378
|
enforce: "pre",
|
|
366
379
|
config: {
|
|
367
380
|
order: "pre",
|
|
368
|
-
async handler(userConfig) {
|
|
381
|
+
async handler(userConfig, env) {
|
|
369
382
|
if (!svelteKit.enabled) return;
|
|
370
383
|
projectRoot = configuredProjectRoot ?? resolve(process.cwd(), userConfig.root ?? ".");
|
|
384
|
+
routeLifecycleConfig = env ?? userConfig;
|
|
385
|
+
routeExpectedOwners = await expectedSvelteKitRouteOwners(userConfig, projectRoot, svelteKit.routesDir || "src/routes/api", env);
|
|
371
386
|
configHookManifest = await scanAndGenerateManifest(projectRoot);
|
|
372
387
|
await generateConfiguredSvelteKitRoutes(projectRoot, configHookManifest);
|
|
373
388
|
generatedRoutesDuringConfig = true;
|
|
@@ -393,6 +408,7 @@ function smrtPlugin(options = {}) {
|
|
|
393
408
|
config = resolvedConfig;
|
|
394
409
|
resolvedPluginNames = (resolvedConfig.plugins || []).map((plugin) => plugin?.name || "");
|
|
395
410
|
projectRoot = configuredProjectRoot ?? resolvedConfig.root;
|
|
411
|
+
if (svelteKit.enabled && routeLifecycleConfig) assertSvelteKitRouteCoordinationComplete(routeLifecycleConfig);
|
|
396
412
|
if (mode === "auto") {
|
|
397
413
|
const isSSRBuild = resolvedConfig.build?.ssr;
|
|
398
414
|
pluginMode = resolvedConfig.plugins.some((p) => p.name?.includes("federation")) || !isSSRBuild && resolvedConfig.build?.target === "esnext" ? "client" : "server";
|
|
@@ -463,18 +479,7 @@ function smrtPlugin(options = {}) {
|
|
|
463
479
|
await writeLocalManifest(manifest, projectRoot);
|
|
464
480
|
warnCliApiCoherenceViolations(manifest);
|
|
465
481
|
}
|
|
466
|
-
if (svelteKit.enabled && manifest && server) await
|
|
467
|
-
enabled: svelteKit.enabled,
|
|
468
|
-
routesDir: svelteKit.routesDir || "src/routes/api",
|
|
469
|
-
objectsDir: svelteKit.objectsDir || "src/lib/objects",
|
|
470
|
-
configPath: svelteKit.configPath || "src/lib/server",
|
|
471
|
-
configFileName: svelteKit.configFileName || "smrt.ts",
|
|
472
|
-
kebabRoutes: svelteKit.kebabRoutes ?? false,
|
|
473
|
-
changesRoute: svelteKit.changesRoute,
|
|
474
|
-
eventsRoute: svelteKit.eventsRoute,
|
|
475
|
-
resourcesRoute: svelteKit.resourcesRoute,
|
|
476
|
-
knowledge: await resolveKnowledgeConfig(projectRoot, manifest)
|
|
477
|
-
});
|
|
482
|
+
if (svelteKit.enabled && manifest && server) await generateConfiguredSvelteKitRoutes(projectRoot, manifest);
|
|
478
483
|
Object.values(VIRTUAL_MODULES).forEach((id) => {
|
|
479
484
|
const module = server?.moduleGraph.getModuleById(id);
|
|
480
485
|
if (module) server?.reloadModule(module);
|
|
@@ -493,18 +498,7 @@ function smrtPlugin(options = {}) {
|
|
|
493
498
|
await writeLocalManifest(manifest, projectRoot);
|
|
494
499
|
warnCliApiCoherenceViolations(manifest);
|
|
495
500
|
}
|
|
496
|
-
if (svelteKit.enabled && manifest && server) await
|
|
497
|
-
enabled: svelteKit.enabled,
|
|
498
|
-
routesDir: svelteKit.routesDir || "src/routes/api",
|
|
499
|
-
objectsDir: svelteKit.objectsDir || "src/lib/objects",
|
|
500
|
-
configPath: svelteKit.configPath || "src/lib/server",
|
|
501
|
-
configFileName: svelteKit.configFileName || "smrt.ts",
|
|
502
|
-
kebabRoutes: svelteKit.kebabRoutes ?? false,
|
|
503
|
-
changesRoute: svelteKit.changesRoute,
|
|
504
|
-
eventsRoute: svelteKit.eventsRoute,
|
|
505
|
-
resourcesRoute: svelteKit.resourcesRoute,
|
|
506
|
-
knowledge: await resolveKnowledgeConfig(projectRoot, manifest)
|
|
507
|
-
});
|
|
501
|
+
if (svelteKit.enabled && manifest && server) await generateConfiguredSvelteKitRoutes(projectRoot, manifest);
|
|
508
502
|
}
|
|
509
503
|
} catch (error) {
|
|
510
504
|
console.error(`[smrt] Failed to rescan after adding ${file}:`, error);
|
|
@@ -519,18 +513,7 @@ function smrtPlugin(options = {}) {
|
|
|
519
513
|
await writeLocalManifest(manifest, projectRoot);
|
|
520
514
|
warnCliApiCoherenceViolations(manifest);
|
|
521
515
|
}
|
|
522
|
-
if (svelteKit.enabled && manifest && server) await
|
|
523
|
-
enabled: svelteKit.enabled,
|
|
524
|
-
routesDir: svelteKit.routesDir || "src/routes/api",
|
|
525
|
-
objectsDir: svelteKit.objectsDir || "src/lib/objects",
|
|
526
|
-
configPath: svelteKit.configPath || "src/lib/server",
|
|
527
|
-
configFileName: svelteKit.configFileName || "smrt.ts",
|
|
528
|
-
kebabRoutes: svelteKit.kebabRoutes ?? false,
|
|
529
|
-
changesRoute: svelteKit.changesRoute,
|
|
530
|
-
eventsRoute: svelteKit.eventsRoute,
|
|
531
|
-
resourcesRoute: svelteKit.resourcesRoute,
|
|
532
|
-
knowledge: await resolveKnowledgeConfig(projectRoot, manifest)
|
|
533
|
-
});
|
|
516
|
+
if (svelteKit.enabled && manifest && server) await generateConfiguredSvelteKitRoutes(projectRoot, manifest);
|
|
534
517
|
}
|
|
535
518
|
} catch (error) {
|
|
536
519
|
console.error(`[smrt] Failed to rescan after removing ${file}:`, error);
|
|
@@ -593,6 +576,8 @@ function smrtPlugin(options = {}) {
|
|
|
593
576
|
}
|
|
594
577
|
}
|
|
595
578
|
};
|
|
579
|
+
markSvelteKitRouteParticipant(plugin, "producer", svelteKit.enabled, svelteKit.routesDir || "src/routes/api", async (rootDir) => resolveKnowledgeConfig(rootDir, { objects: {} }), (userConfig) => configuredProjectRoot ?? resolve(process.cwd(), userConfig?.root ?? "."));
|
|
580
|
+
return plugin;
|
|
596
581
|
function scanAndGenerateManifest(rootDir) {
|
|
597
582
|
const tracked = scanAndGenerateManifestImpl(rootDir).finally(() => {
|
|
598
583
|
if (activeManifestScan === tracked) activeManifestScan = void 0;
|