@happyvertical/smrt-core 0.37.6 → 0.37.8

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 (71) hide show
  1. package/AGENTS.md +17 -6
  2. package/dist/collection.d.ts +7 -0
  3. package/dist/collection.d.ts.map +1 -1
  4. package/dist/collection.js +2 -0
  5. package/dist/collection.js.map +1 -1
  6. package/dist/consumer-plugin/index.js +26 -3
  7. package/dist/consumer-plugin/index.js.map +1 -1
  8. package/dist/dispatch/index.d.ts +1 -1
  9. package/dist/dispatch/index.d.ts.map +1 -1
  10. package/dist/dispatch/index.js +2 -2
  11. package/dist/dispatch/tenant-resolver.d.ts +23 -0
  12. package/dist/dispatch/tenant-resolver.d.ts.map +1 -1
  13. package/dist/dispatch/tenant-resolver.js +33 -1
  14. package/dist/dispatch/tenant-resolver.js.map +1 -1
  15. package/dist/generators/conditional-get.d.ts +120 -0
  16. package/dist/generators/conditional-get.d.ts.map +1 -0
  17. package/dist/generators/conditional-get.js +217 -0
  18. package/dist/generators/conditional-get.js.map +1 -0
  19. package/dist/generators/index.d.ts +1 -0
  20. package/dist/generators/index.d.ts.map +1 -1
  21. package/dist/generators/index.js +2 -1
  22. package/dist/generators/rest.d.ts +52 -0
  23. package/dist/generators/rest.d.ts.map +1 -1
  24. package/dist/generators/rest.js +146 -10
  25. package/dist/generators/rest.js.map +1 -1
  26. package/dist/generators.js +2 -1
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +4 -2
  30. package/dist/manifest/manifest-loader.d.ts +10 -1
  31. package/dist/manifest/manifest-loader.d.ts.map +1 -1
  32. package/dist/manifest/manifest-loader.js +18 -5
  33. package/dist/manifest/manifest-loader.js.map +1 -1
  34. package/dist/manifest/static-manifest.d.ts.map +1 -1
  35. package/dist/manifest/static-manifest.js +10 -2
  36. package/dist/manifest/static-manifest.js.map +1 -1
  37. package/dist/manifest/store.js +1 -1
  38. package/dist/manifest/store.js.map +1 -1
  39. package/dist/manifest/test-manifest-stub.d.ts.map +1 -1
  40. package/dist/manifest/test-manifest-stub.js +2108 -223
  41. package/dist/manifest/test-manifest-stub.js.map +1 -1
  42. package/dist/manifest.json +10 -2
  43. package/dist/object.d.ts +19 -0
  44. package/dist/object.d.ts.map +1 -1
  45. package/dist/object.js +24 -2
  46. package/dist/object.js.map +1 -1
  47. package/dist/registry/index.d.ts +1 -1
  48. package/dist/registry/index.d.ts.map +1 -1
  49. package/dist/registry/shared-state.d.ts +8 -1
  50. package/dist/registry/shared-state.d.ts.map +1 -1
  51. package/dist/registry/shared-state.js +11 -3
  52. package/dist/registry/shared-state.js.map +1 -1
  53. package/dist/registry/types.d.ts +34 -0
  54. package/dist/registry/types.d.ts.map +1 -1
  55. package/dist/smrt-knowledge.json +7 -6
  56. package/dist/sync/apply.d.ts +234 -0
  57. package/dist/sync/apply.d.ts.map +1 -0
  58. package/dist/sync/apply.js +378 -0
  59. package/dist/sync/apply.js.map +1 -0
  60. package/dist/utils/stack-frames.d.ts +50 -0
  61. package/dist/utils/stack-frames.d.ts.map +1 -0
  62. package/dist/utils/stack-frames.js +64 -0
  63. package/dist/utils/stack-frames.js.map +1 -0
  64. package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
  65. package/dist/vite-plugin/sveltekit-generator.js +60 -27
  66. package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
  67. package/dist/vite-plugin/sync-apply-route.d.ts +40 -0
  68. package/dist/vite-plugin/sync-apply-route.d.ts.map +1 -0
  69. package/dist/vite-plugin/sync-apply-route.js +240 -0
  70. package/dist/vite-plugin/sync-apply-route.js.map +1 -0
  71. package/package.json +4 -4
@@ -1,3 +1,5 @@
1
+ import { generateConditionalGetRouteHelper } from "../generators/conditional-get.js";
2
+ import { generateSyncApplyRoute } from "./sync-apply-route.js";
1
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
2
4
  import { join, relative } from "node:path";
3
5
  //#region src/vite-plugin/sveltekit-generator.ts
@@ -269,24 +271,28 @@ function isTenantScoped(objectDef) {
269
271
  return !!objectDef.decoratorConfig?.tenantScoped;
270
272
  }
271
273
  /**
272
- * Emit the tenant-context establishment helper for `@TenantScoped` objects
273
- * (#1540, facet 2). Generated handlers call `establishTenantContext(locals)`
274
- * after the auth guard so every collection query runs inside the tenant context
275
- * derived from the authenticated principal — engaging the `@TenantScoped`
276
- * interceptors that auto-filter by tenant.
274
+ * Emit the tenant-context establishment + fail-closed read-scope helpers for
275
+ * `@TenantScoped` objects (#1540 facet 2; #1782). Generated handlers call
276
+ * `establishTenantContext(locals)` after the auth guard so every query runs
277
+ * inside the tenant context derived from the authenticated principal — engaging
278
+ * the `@TenantScoped` interceptors that auto-filter by tenant.
277
279
  *
278
- * Without this, an `optional`-mode scoped object queried over a generated route
279
- * with no active context returns rows from ALL tenants (the interceptor only
280
- * hard-fails `required` mode). We read the canonical `locals.tenantId` set by
281
- * the smrt-users auth hook (with `user`/`session` fallbacks). It is a no-op when
282
- * a context is already active (e.g. an upstream tenancy handle) or when the
283
- * principal carries no tenant (global/admin preserving optional-mode global
284
- * access). Imported only for tenant-scoped routes, which already depend on
285
- * `@happyvertical/smrt-tenancy`.
280
+ * `establishTenantContext` reads the canonical `locals.tenantId` set by the
281
+ * smrt-users auth hook (with `user`/`session` fallbacks). It is a no-op when a
282
+ * context is already active (e.g. an upstream tenancy handle) or when the
283
+ * request carries no tenant (anonymous / global principal).
284
+ *
285
+ * When no context could be established, an `optional`-mode read would otherwise
286
+ * fall through the interceptor UNFILTERED and return rows from ALL tenants (the
287
+ * interceptor only hard-fails `required` mode) — the #1782 leak. So read
288
+ * handlers additionally call `tenantReadScope()`, which restricts reads to
289
+ * NULL-tenant (global) rows only when tenancy is enabled but no context is
290
+ * active. Both helpers are imported only for tenant-scoped routes, which already
291
+ * depend on `@happyvertical/smrt-tenancy`.
286
292
  */
287
293
  function generateTenantContextHelper() {
288
294
  return `
289
- import { enterTenantContext, hasTenantContext } from '@happyvertical/smrt-tenancy';
295
+ import { enterTenantContext, hasTenantContext, isTenancyEnabled } from '@happyvertical/smrt-tenancy';
290
296
 
291
297
  function establishTenantContext(locals: unknown): void {
292
298
  if (hasTenantContext()) return;
@@ -299,6 +305,19 @@ function establishTenantContext(locals: unknown): void {
299
305
  enterTenantContext({ tenantId });
300
306
  }
301
307
  }
308
+
309
+ // Fail-closed read scope (#1782): a public/anonymous read on a @TenantScoped
310
+ // model has no tenant context, so the tenancy interceptor (optional mode) would
311
+ // pass the query through UNFILTERED and return every tenant's rows. When tenancy
312
+ // is enabled but no context was established, restrict reads to NULL-tenant
313
+ // (global) rows only — mirroring the dispatch resolver + _changes convention:
314
+ // tenancy enforced with no context => global rows only. Returns undefined when a
315
+ // context is active (the interceptor filters by it) or tenancy is disabled.
316
+ function tenantReadScope(): { tenantId: null } | undefined {
317
+ return isTenancyEnabled() && !hasTenantContext()
318
+ ? { tenantId: null }
319
+ : undefined;
320
+ }
302
321
  `;
303
322
  }
304
323
  /**
@@ -476,6 +495,7 @@ async function generateSvelteKitRoutes(projectRoot, manifest, options) {
476
495
  generatedCount++;
477
496
  }
478
497
  if (options.knowledge?.api?.enabled) generateKnowledgeRoute(projectRoot, options);
498
+ generateSyncApplyRoute(projectRoot, manifest, options);
479
499
  updateGitignore(projectRoot, options);
480
500
  const skippedMsg = skippedCollections > 0 ? ` (skipped ${skippedCollections} collection classes)` : "";
481
501
  console.log(`[smrt] Generated routes for ${generatedCount} SMRT objects${skippedMsg}`);
@@ -985,29 +1005,35 @@ function generateCollectionRouteTemplate(projectRoot, className, objectDef, incl
985
1005
  const imports = `${AUTO_GENERATED_ROUTE_HEADER}
986
1006
  // DO NOT EDIT - changes will be overwritten
987
1007
 
988
- import { error, json } from '@sveltejs/kit';
1008
+ import { error${hasPost ? ", json" : ""} } from '@sveltejs/kit';
989
1009
  ${serializerImports ? `${serializerImports}\n` : ""}import { getCollection } from '$lib/server/smrt';
990
1010
  ${modelType.importStatement ? `${modelType.importStatement}\n` : ""}import type { RequestHandler } from './$types';
991
1011
  // Note: ${className} is auto-registered by the Vite plugin scanner
992
- ${generateAuthGuardHelper(objectDef)}${isTenantScoped(objectDef) ? generateTenantContextHelper() : ""}${hasPost ? generateWritablePolicyHelper(objectDef) : ""}`;
1012
+ ${generateAuthGuardHelper(objectDef)}${isTenantScoped(objectDef) ? generateTenantContextHelper() : ""}${hasPost ? generateWritablePolicyHelper(objectDef) : ""}${hasGet ? generateConditionalGetRouteHelper(objectDef.decoratorConfig?.api, {
1013
+ tenantScoped: isTenantScoped(objectDef),
1014
+ modelName: className
1015
+ }) : ""}`;
1016
+ const listAndCount = isTenantScoped(objectDef) ? ` const readScope = tenantReadScope();
1017
+ const items = await collection.list({ limit, offset, where: readScope });
1018
+ const count = await collection.count({ where: readScope });` : ` const items = await collection.list({ limit, offset });
1019
+ const count = await collection.count();`;
993
1020
  const getHandler = hasGet ? `
994
1021
  // List all ${className.toLowerCase()}s
995
- export const GET: RequestHandler = async ({ locals, url }) => {
1022
+ export const GET: RequestHandler = async ({ locals, url, request }) => {
996
1023
  ${routeGuardPreamble(objectDef, false)}
997
1024
  const limit = Number(url.searchParams.get('limit')) || 50;
998
1025
  const offset = Number(url.searchParams.get('offset')) || 0;
999
1026
 
1000
1027
  ${generateCollectionLoad(className, { typeName: modelType.typeName })}
1001
- const items = await collection.list({ limit, offset });
1002
- const count = await collection.count();
1028
+ ${listAndCount}
1003
1029
  ${serializers.listItemSerializerName ? `
1004
1030
  const serializedItems = await Promise.all(
1005
1031
  items.map((item) => ${serializers.listItemSerializerName}(item)),
1006
1032
  );
1007
1033
 
1008
- return json({ items: serializedItems, count, limit, offset });` : `
1034
+ return conditionalJson(request, { items: serializedItems, count, limit, offset });` : `
1009
1035
  const items_public = items.map((item) => item.toPublicJSON());
1010
- return json({ items: items_public, count, limit, offset });`}
1036
+ return conditionalJson(request, { items: items_public, count, limit, offset });`}
1011
1037
  };
1012
1038
  ` : "";
1013
1039
  const postHandler = hasPost ? `
@@ -1079,22 +1105,29 @@ function generateItemRouteTemplate(projectRoot, className, objectDef, includedAc
1079
1105
  const imports = `${AUTO_GENERATED_ROUTE_HEADER}
1080
1106
  // DO NOT EDIT - changes will be overwritten
1081
1107
 
1082
- import { error, json } from '@sveltejs/kit';
1108
+ import { error${hasPut || hasDelete ? ", json" : ""} } from '@sveltejs/kit';
1083
1109
  ${serializerImports ? `${serializerImports}\n` : ""}import { getCollection } from '$lib/server/smrt';
1084
1110
  ${modelType.importStatement ? `${modelType.importStatement}\n` : ""}import type { RequestHandler } from './$types';
1085
- ${generateAuthGuardHelper(objectDef)}${isTenantScoped(objectDef) ? generateTenantContextHelper() : ""}${hasPut ? generateWritablePolicyHelper(objectDef) : ""}`;
1111
+ ${generateAuthGuardHelper(objectDef)}${isTenantScoped(objectDef) ? generateTenantContextHelper() : ""}${hasPut ? generateWritablePolicyHelper(objectDef) : ""}${hasGet ? generateConditionalGetRouteHelper(objectDef.decoratorConfig?.api, {
1112
+ tenantScoped: isTenantScoped(objectDef),
1113
+ modelName: className
1114
+ }) : ""}`;
1115
+ const getForRead = isTenantScoped(objectDef) ? ` const readScope = tenantReadScope();
1116
+ const item = await collection.get(
1117
+ readScope ? { id: params.id, ...readScope } : params.id,
1118
+ );` : ` const item = await collection.get(params.id);`;
1086
1119
  const getHandler = hasGet ? `
1087
1120
  // Get single ${simpleClassName.toLowerCase()}
1088
- export const GET: RequestHandler = async ({ locals, params }) => {
1121
+ export const GET: RequestHandler = async ({ locals, params, request }) => {
1089
1122
  ${routeGuardPreamble(objectDef, false)}
1090
1123
  ${generateCollectionLoad(className, { typeName: modelType.typeName })}
1091
- const item = await collection.get(params.id);
1124
+ ${getForRead}
1092
1125
  ${generateNotFoundError(className)}
1093
1126
  ${serializers.itemSerializerName ? `
1094
1127
  const serializedItem = await ${serializers.itemSerializerName}(item);
1095
1128
 
1096
- return json(serializedItem);` : `
1097
- return json(item.toPublicJSON());`}
1129
+ return conditionalJson(request, serializedItem);` : `
1130
+ return conditionalJson(request, item.toPublicJSON());`}
1098
1131
  };
1099
1132
  ` : "";
1100
1133
  const putHandler = hasPut ? `