@happyvertical/smrt-core 0.49.5 → 0.49.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +49 -0
  2. package/dist/consumer-plugin/index.d.ts +19 -4
  3. package/dist/consumer-plugin/index.d.ts.map +1 -1
  4. package/dist/consumer-plugin/index.js +204 -6
  5. package/dist/consumer-plugin/index.js.map +1 -1
  6. package/dist/manifest/static-manifest.d.ts.map +1 -1
  7. package/dist/manifest/static-manifest.js +13 -7
  8. package/dist/manifest/static-manifest.js.map +1 -1
  9. package/dist/manifest/store.js +1 -1
  10. package/dist/manifest/store.js.map +1 -1
  11. package/dist/manifest.json +13 -7
  12. package/dist/scanner/manifest-generator.d.ts +39 -0
  13. package/dist/scanner/manifest-generator.d.ts.map +1 -1
  14. package/dist/scanner/manifest-generator.js +79 -12
  15. package/dist/scanner/manifest-generator.js.map +1 -1
  16. package/dist/smrt-knowledge.json +3 -3
  17. package/dist/vite-plugin/changes-route.d.ts.map +1 -1
  18. package/dist/vite-plugin/changes-route.js +4 -3
  19. package/dist/vite-plugin/changes-route.js.map +1 -1
  20. package/dist/vite-plugin/dev-plane-route.d.ts +2 -0
  21. package/dist/vite-plugin/dev-plane-route.d.ts.map +1 -1
  22. package/dist/vite-plugin/dev-plane-route.js +8 -5
  23. package/dist/vite-plugin/dev-plane-route.js.map +1 -1
  24. package/dist/vite-plugin/events-route.d.ts.map +1 -1
  25. package/dist/vite-plugin/events-route.js +4 -3
  26. package/dist/vite-plugin/events-route.js.map +1 -1
  27. package/dist/vite-plugin/index.d.ts.map +1 -1
  28. package/dist/vite-plugin/index.js +25 -40
  29. package/dist/vite-plugin/index.js.map +1 -1
  30. package/dist/vite-plugin/resources-route.d.ts +6 -0
  31. package/dist/vite-plugin/resources-route.d.ts.map +1 -1
  32. package/dist/vite-plugin/resources-route.js +30 -7
  33. package/dist/vite-plugin/resources-route.js.map +1 -1
  34. package/dist/vite-plugin/sveltekit-config-import.d.ts +19 -0
  35. package/dist/vite-plugin/sveltekit-config-import.d.ts.map +1 -0
  36. package/dist/vite-plugin/sveltekit-config-import.js +36 -0
  37. package/dist/vite-plugin/sveltekit-config-import.js.map +1 -0
  38. package/dist/vite-plugin/sveltekit-generator.d.ts +33 -2
  39. package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
  40. package/dist/vite-plugin/sveltekit-generator.js +181 -65
  41. package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
  42. package/dist/vite-plugin/sveltekit-path.d.ts +8 -0
  43. package/dist/vite-plugin/sveltekit-path.d.ts.map +1 -0
  44. package/dist/vite-plugin/sveltekit-path.js +25 -0
  45. package/dist/vite-plugin/sveltekit-path.js.map +1 -0
  46. package/dist/vite-plugin/sveltekit-route-coordinator.d.ts +67 -0
  47. package/dist/vite-plugin/sveltekit-route-coordinator.d.ts.map +1 -0
  48. package/dist/vite-plugin/sveltekit-route-coordinator.js +321 -0
  49. package/dist/vite-plugin/sveltekit-route-coordinator.js.map +1 -0
  50. package/dist/vite-plugin/sync-apply-route.d.ts +1 -1
  51. package/dist/vite-plugin/sync-apply-route.d.ts.map +1 -1
  52. package/dist/vite-plugin/sync-apply-route.js +4 -3
  53. package/dist/vite-plugin/sync-apply-route.js.map +1 -1
  54. package/package.json +4 -4
@@ -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.5",
6
+ "packageVersion": "0.49.6",
7
7
  "sourceManifestPath": "dist/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "df5a43cd08e19c90c2c29c9e3a7ea6b0b13e9cd40390fda28f2fcd2ac5b5f2b7",
11
- "packageJson": "cab198084e0c5c6256e246a91f2d3ba86213731b9d9d928e75bd8868541cc824",
10
+ "manifest": "26f48d9d6d3bdb82fd82fa8490d3f1453eb940232ddbf1df2ccd049a6ded9b73",
11
+ "packageJson": "d9b7a0fc698041d6ed70e96d8589fdb2d91170b2e75c23d92e896f6bb1e4b924",
12
12
  "agents": "6c19f8eab9fbcd9c10d0649279f93c8c36c65c18da3402f2a2b49893432125ec",
13
13
  "moduleDoc:agents/object-runtime.md": "34a4e740c109467cd5486c4af0e3bf4b63915697721ab584df0e5506ea9290ca",
14
14
  "moduleDoc:agents/revision-guard.md": "aa6b1ddb5b6b49fa27ebbe575ec7fcd6d5ceb35ee25acc702f877a226eb9a99a",
@@ -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;AAE1B,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,CA0BT"}
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 '$lib/server/smrt';
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 '$lib/server/smrt';\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,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,CACxC;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,cACQ;CAoBR,OAAO,GAAG,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAnBjB,eACjB;;;;;;;;;;;;;;IAeA,GAuCS;;6BAtCM,eAAe,wCAAwC,GAwCpC;;;;;;;;;;;;;;;;;;;;;;;;;OAyBjC,gBAAgB;4CACqB,gBAAgB;;;;;;;;;AAS5D"}
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;AAOH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAEjE,kFAAkF;AAClF,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAUlE;AAED,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,gBAAgB,GACxB,OAAO,CA8BT"}
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 (!consumerHasSmrtDevMcp(projectRoot)) {
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(`$lib/${configPath.replace(/^src\/lib\//, "")}/${configFileName}`), "utf-8");
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 (!consumerHasSmrtDevMcp(projectRoot)) {\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 configPath = options.configPath || 'src/lib/server';\n const configFileName = (options.configFileName || 'smrt.ts').replace(\n /\\.ts$/,\n '',\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 `$lib/${configPath.replace(/^src\\/lib\\//, '')}/${configFileName}`,\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":";;;;;;;;;;;;;;;;;;AAoBA,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;AAEA,SAAgB,sBACd,aACA,SACS;CACT,IAAI,QAAQ,eAAe,YAAY,MACrC,OAAO;CAET,IAAI,CAAC,sBAAsB,WAAW,GAAG;EACvC,QAAQ,IACN,8IAEF;EACA,OAAO;CACT;CACA,MAAM,aAAa,QAAQ,cAAc;CACzC,MAAM,kBAAkB,QAAQ,kBAAkB,UAAA,CAAW,QAC3D,SACA,EACF;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,QAAQ,WAAW,QAAQ,eAAe,EAAE,EAAE,GAAG,gBACnD,GACA,OACF;CACA,QAAQ,IAAI,qBAAqB,UAAU;CAC3C,OAAO;AACT;AAEA,SAAS,8BAA8B,cAA8B;CACnE,OAAO,GAAG,4BAA4B;;;;;;;;;;;;iCAYP,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgC9C"}
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;AAM5D,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,CA8BT"}
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 '$lib/server/smrt';
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":"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"}
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
- await generateSvelteKitRoutes(rootDir, currentManifest, {
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
- return {
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 generateSvelteKitRoutes(projectRoot, manifest, {
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 generateSvelteKitRoutes(projectRoot, manifest, {
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 generateSvelteKitRoutes(projectRoot, manifest, {
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;