@happyvertical/smrt-core 0.37.7 → 0.37.9

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 (88) hide show
  1. package/AGENTS.md +25 -6
  2. package/dist/change-feed.d.ts +261 -0
  3. package/dist/change-feed.d.ts.map +1 -0
  4. package/dist/change-feed.js +482 -0
  5. package/dist/change-feed.js.map +1 -0
  6. package/dist/class.d.ts.map +1 -1
  7. package/dist/class.js +4 -2
  8. package/dist/class.js.map +1 -1
  9. package/dist/collection.js +2 -2
  10. package/dist/consumer-plugin/index.d.ts.map +1 -1
  11. package/dist/consumer-plugin/index.js +24 -19
  12. package/dist/consumer-plugin/index.js.map +1 -1
  13. package/dist/dispatch/bus.js +2 -2
  14. package/dist/dispatch/index.d.ts +1 -1
  15. package/dist/dispatch/index.d.ts.map +1 -1
  16. package/dist/dispatch/index.js +2 -2
  17. package/dist/dispatch/tenant-resolver.d.ts +23 -0
  18. package/dist/dispatch/tenant-resolver.d.ts.map +1 -1
  19. package/dist/dispatch/tenant-resolver.js +33 -1
  20. package/dist/dispatch/tenant-resolver.js.map +1 -1
  21. package/dist/generated-client-runtime.d.ts +26 -0
  22. package/dist/generated-client-runtime.d.ts.map +1 -0
  23. package/dist/generated-client-runtime.js +88 -0
  24. package/dist/generated-client-runtime.js.map +1 -0
  25. package/dist/generators/changes-route.d.ts +52 -0
  26. package/dist/generators/changes-route.d.ts.map +1 -0
  27. package/dist/generators/changes-route.js +111 -0
  28. package/dist/generators/changes-route.js.map +1 -0
  29. package/dist/generators/conditional-get.d.ts +12 -0
  30. package/dist/generators/conditional-get.d.ts.map +1 -1
  31. package/dist/generators/conditional-get.js +30 -2
  32. package/dist/generators/conditional-get.js.map +1 -1
  33. package/dist/generators/rest.d.ts +58 -0
  34. package/dist/generators/rest.d.ts.map +1 -1
  35. package/dist/generators/rest.js +114 -21
  36. package/dist/generators/rest.js.map +1 -1
  37. package/dist/index.d.ts +1 -0
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +6 -5
  40. package/dist/manifest/manifest-loader.d.ts +10 -1
  41. package/dist/manifest/manifest-loader.d.ts.map +1 -1
  42. package/dist/manifest/manifest-loader.js +18 -5
  43. package/dist/manifest/manifest-loader.js.map +1 -1
  44. package/dist/manifest/static-manifest.js +2 -2
  45. package/dist/manifest/static-manifest.js.map +1 -1
  46. package/dist/manifest/store.js +1 -1
  47. package/dist/manifest/test-manifest-stub.d.ts.map +1 -1
  48. package/dist/manifest/test-manifest-stub.js +880 -105
  49. package/dist/manifest/test-manifest-stub.js.map +1 -1
  50. package/dist/manifest.json +2 -2
  51. package/dist/object.js +2 -2
  52. package/dist/prebuild/index.d.ts.map +1 -1
  53. package/dist/prebuild/index.js +14 -7
  54. package/dist/prebuild/index.js.map +1 -1
  55. package/dist/registry/shared-state.d.ts +8 -1
  56. package/dist/registry/shared-state.d.ts.map +1 -1
  57. package/dist/registry/shared-state.js +11 -3
  58. package/dist/registry/shared-state.js.map +1 -1
  59. package/dist/smrt-knowledge.json +6 -6
  60. package/dist/system/schema.d.ts +24 -1
  61. package/dist/system/schema.d.ts.map +1 -1
  62. package/dist/system/schema.js +45 -3
  63. package/dist/system/schema.js.map +1 -1
  64. package/dist/testing/database.js +1 -1
  65. package/dist/utils/stack-frames.d.ts +50 -0
  66. package/dist/utils/stack-frames.d.ts.map +1 -0
  67. package/dist/utils/stack-frames.js +64 -0
  68. package/dist/utils/stack-frames.js.map +1 -0
  69. package/dist/vite-plugin/changes-route.d.ts +10 -0
  70. package/dist/vite-plugin/changes-route.d.ts.map +1 -0
  71. package/dist/vite-plugin/changes-route.js +174 -0
  72. package/dist/vite-plugin/changes-route.js.map +1 -0
  73. package/dist/vite-plugin/index.d.ts +7 -0
  74. package/dist/vite-plugin/index.d.ts.map +1 -1
  75. package/dist/vite-plugin/index.js +38 -20
  76. package/dist/vite-plugin/index.js.map +1 -1
  77. package/dist/vite-plugin/route-header.d.ts +10 -0
  78. package/dist/vite-plugin/route-header.d.ts.map +1 -0
  79. package/dist/vite-plugin/route-header.js +14 -0
  80. package/dist/vite-plugin/route-header.js.map +1 -0
  81. package/dist/vite-plugin/sveltekit-generator.d.ts +7 -0
  82. package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
  83. package/dist/vite-plugin/sveltekit-generator.js +46 -20
  84. package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
  85. package/dist/vite-plugin/sync-apply-route.d.ts.map +1 -1
  86. package/dist/vite-plugin/sync-apply-route.js +2 -2
  87. package/dist/vite-plugin/sync-apply-route.js.map +1 -1
  88. package/package.json +4 -4
@@ -1,5 +1,7 @@
1
1
  import { generateConditionalGetRouteHelper } from "../generators/conditional-get.js";
2
+ import { AUTO_GENERATED_ROUTE_HEADER } from "./route-header.js";
2
3
  import { generateSyncApplyRoute } from "./sync-apply-route.js";
4
+ import { generateChangesRoute } from "./changes-route.js";
3
5
  import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
4
6
  import { join, relative } from "node:path";
5
7
  //#region src/vite-plugin/sveltekit-generator.ts
@@ -8,7 +10,6 @@ import { join, relative } from "node:path";
8
10
  * Generates real TypeScript files in src/routes/api/ based on discovered SMRT objects
9
11
  */
10
12
  var BIOME_LINE_WIDTH = 80;
11
- var AUTO_GENERATED_ROUTE_HEADER = "// Auto-generated by @smrt/core vite plugin";
12
13
  var STANDARD_API_ACTIONS = [
13
14
  "list",
14
15
  "get",
@@ -271,24 +272,28 @@ function isTenantScoped(objectDef) {
271
272
  return !!objectDef.decoratorConfig?.tenantScoped;
272
273
  }
273
274
  /**
274
- * Emit the tenant-context establishment helper for `@TenantScoped` objects
275
- * (#1540, facet 2). Generated handlers call `establishTenantContext(locals)`
276
- * after the auth guard so every collection query runs inside the tenant context
277
- * derived from the authenticated principal — engaging the `@TenantScoped`
278
- * interceptors that auto-filter by tenant.
275
+ * Emit the tenant-context establishment + fail-closed read-scope helpers for
276
+ * `@TenantScoped` objects (#1540 facet 2; #1782). Generated handlers call
277
+ * `establishTenantContext(locals)` after the auth guard so every query runs
278
+ * inside the tenant context derived from the authenticated principal — engaging
279
+ * the `@TenantScoped` interceptors that auto-filter by tenant.
279
280
  *
280
- * Without this, an `optional`-mode scoped object queried over a generated route
281
- * with no active context returns rows from ALL tenants (the interceptor only
282
- * hard-fails `required` mode). We read the canonical `locals.tenantId` set by
283
- * the smrt-users auth hook (with `user`/`session` fallbacks). It is a no-op when
284
- * a context is already active (e.g. an upstream tenancy handle) or when the
285
- * principal carries no tenant (global/admin preserving optional-mode global
286
- * access). Imported only for tenant-scoped routes, which already depend on
287
- * `@happyvertical/smrt-tenancy`.
281
+ * `establishTenantContext` reads the canonical `locals.tenantId` set by the
282
+ * smrt-users auth hook (with `user`/`session` fallbacks). It is a no-op when a
283
+ * context is already active (e.g. an upstream tenancy handle) or when the
284
+ * request carries no tenant (anonymous / global principal).
285
+ *
286
+ * When no context could be established, an `optional`-mode read would otherwise
287
+ * fall through the interceptor UNFILTERED and return rows from ALL tenants (the
288
+ * interceptor only hard-fails `required` mode) — the #1782 leak. So read
289
+ * handlers additionally call `tenantReadScope()`, which restricts reads to
290
+ * NULL-tenant (global) rows only when tenancy is enabled but no context is
291
+ * active. Both helpers are imported only for tenant-scoped routes, which already
292
+ * depend on `@happyvertical/smrt-tenancy`.
288
293
  */
289
294
  function generateTenantContextHelper() {
290
295
  return `
291
- import { enterTenantContext, hasTenantContext } from '@happyvertical/smrt-tenancy';
296
+ import { enterTenantContext, hasTenantContext, isTenancyEnabled } from '@happyvertical/smrt-tenancy';
292
297
 
293
298
  function establishTenantContext(locals: unknown): void {
294
299
  if (hasTenantContext()) return;
@@ -301,6 +306,19 @@ function establishTenantContext(locals: unknown): void {
301
306
  enterTenantContext({ tenantId });
302
307
  }
303
308
  }
309
+
310
+ // Fail-closed read scope (#1782): a public/anonymous read on a @TenantScoped
311
+ // model has no tenant context, so the tenancy interceptor (optional mode) would
312
+ // pass the query through UNFILTERED and return every tenant's rows. When tenancy
313
+ // is enabled but no context was established, restrict reads to NULL-tenant
314
+ // (global) rows only — mirroring the dispatch resolver + _changes convention:
315
+ // tenancy enforced with no context => global rows only. Returns undefined when a
316
+ // context is active (the interceptor filters by it) or tenancy is disabled.
317
+ function tenantReadScope(): { tenantId: null } | undefined {
318
+ return isTenancyEnabled() && !hasTenantContext()
319
+ ? { tenantId: null }
320
+ : undefined;
321
+ }
304
322
  `;
305
323
  }
306
324
  /**
@@ -479,6 +497,7 @@ async function generateSvelteKitRoutes(projectRoot, manifest, options) {
479
497
  }
480
498
  if (options.knowledge?.api?.enabled) generateKnowledgeRoute(projectRoot, options);
481
499
  generateSyncApplyRoute(projectRoot, manifest, options);
500
+ generateChangesRoute(projectRoot, manifest, options);
482
501
  updateGitignore(projectRoot, options);
483
502
  const skippedMsg = skippedCollections > 0 ? ` (skipped ${skippedCollections} collection classes)` : "";
484
503
  console.log(`[smrt] Generated routes for ${generatedCount} SMRT objects${skippedMsg}`);
@@ -492,7 +511,7 @@ function clearGeneratedRouteFiles(routesRoot) {
492
511
  continue;
493
512
  }
494
513
  if (!entry.isFile() || entry.name !== "+server.ts") continue;
495
- if (readFileSync(entryPath, "utf-8").startsWith(AUTO_GENERATED_ROUTE_HEADER)) unlinkSync(entryPath);
514
+ if (readFileSync(entryPath, "utf-8").startsWith("// Auto-generated by @smrt/core vite plugin")) unlinkSync(entryPath);
496
515
  }
497
516
  }
498
517
  function knowledgeRouteDir(projectRoot, options) {
@@ -509,7 +528,7 @@ function svelteKitRouteRoot(routesDir) {
509
528
  function clearGeneratedKnowledgeRoute(projectRoot, options) {
510
529
  const routePath = join(knowledgeRouteDir(projectRoot, options), "+server.ts");
511
530
  if (!existsSync(routePath)) return;
512
- if (readFileSync(routePath, "utf-8").startsWith(AUTO_GENERATED_ROUTE_HEADER)) unlinkSync(routePath);
531
+ if (readFileSync(routePath, "utf-8").startsWith("// Auto-generated by @smrt/core vite plugin")) unlinkSync(routePath);
513
532
  }
514
533
  function generateKnowledgeRoute(projectRoot, options) {
515
534
  writeRoute(knowledgeRouteDir(projectRoot, options), "+server.ts", generateKnowledgeRouteTemplate(options.knowledge ?? {}, readKnowledgeRouteArtifact(projectRoot)));
@@ -996,6 +1015,10 @@ ${generateAuthGuardHelper(objectDef)}${isTenantScoped(objectDef) ? generateTenan
996
1015
  tenantScoped: isTenantScoped(objectDef),
997
1016
  modelName: className
998
1017
  }) : ""}`;
1018
+ const listAndCount = isTenantScoped(objectDef) ? ` const readScope = tenantReadScope();
1019
+ const items = await collection.list({ limit, offset, where: readScope });
1020
+ const count = await collection.count({ where: readScope });` : ` const items = await collection.list({ limit, offset });
1021
+ const count = await collection.count();`;
999
1022
  const getHandler = hasGet ? `
1000
1023
  // List all ${className.toLowerCase()}s
1001
1024
  export const GET: RequestHandler = async ({ locals, url, request }) => {
@@ -1004,8 +1027,7 @@ ${routeGuardPreamble(objectDef, false)}
1004
1027
  const offset = Number(url.searchParams.get('offset')) || 0;
1005
1028
 
1006
1029
  ${generateCollectionLoad(className, { typeName: modelType.typeName })}
1007
- const items = await collection.list({ limit, offset });
1008
- const count = await collection.count();
1030
+ ${listAndCount}
1009
1031
  ${serializers.listItemSerializerName ? `
1010
1032
  const serializedItems = await Promise.all(
1011
1033
  items.map((item) => ${serializers.listItemSerializerName}(item)),
@@ -1092,12 +1114,16 @@ ${generateAuthGuardHelper(objectDef)}${isTenantScoped(objectDef) ? generateTenan
1092
1114
  tenantScoped: isTenantScoped(objectDef),
1093
1115
  modelName: className
1094
1116
  }) : ""}`;
1117
+ const getForRead = isTenantScoped(objectDef) ? ` const readScope = tenantReadScope();
1118
+ const item = await collection.get(
1119
+ readScope ? { id: params.id, ...readScope } : params.id,
1120
+ );` : ` const item = await collection.get(params.id);`;
1095
1121
  const getHandler = hasGet ? `
1096
1122
  // Get single ${simpleClassName.toLowerCase()}
1097
1123
  export const GET: RequestHandler = async ({ locals, params, request }) => {
1098
1124
  ${routeGuardPreamble(objectDef, false)}
1099
1125
  ${generateCollectionLoad(className, { typeName: modelType.typeName })}
1100
- const item = await collection.get(params.id);
1126
+ ${getForRead}
1101
1127
  ${generateNotFoundError(className)}
1102
1128
  ${serializers.itemSerializerName ? `
1103
1129
  const serializedItem = await ${serializers.itemSerializerName}(item);