@happyvertical/smrt-core 0.38.2 → 0.38.3

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 (128) hide show
  1. package/AGENTS.md +11 -2
  2. package/dist/change-feed.d.ts +19 -10
  3. package/dist/change-feed.d.ts.map +1 -1
  4. package/dist/change-feed.js +43 -13
  5. package/dist/change-feed.js.map +1 -1
  6. package/dist/change-signals.d.ts +83 -0
  7. package/dist/change-signals.d.ts.map +1 -0
  8. package/dist/change-signals.js +272 -0
  9. package/dist/change-signals.js.map +1 -0
  10. package/dist/collection-cache.d.ts +21 -0
  11. package/dist/collection-cache.d.ts.map +1 -1
  12. package/dist/collection-cache.js +0 -0
  13. package/dist/collection-cache.js.map +1 -1
  14. package/dist/config.d.ts +1 -1
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/config.js.map +1 -1
  17. package/dist/decorators/index.d.ts +8 -0
  18. package/dist/decorators/index.d.ts.map +1 -1
  19. package/dist/decorators/index.js.map +1 -1
  20. package/dist/errors.js +2 -1
  21. package/dist/errors.js.map +1 -1
  22. package/dist/generators/changes-route.d.ts +15 -27
  23. package/dist/generators/changes-route.d.ts.map +1 -1
  24. package/dist/generators/changes-route.js +17 -7
  25. package/dist/generators/changes-route.js.map +1 -1
  26. package/dist/generators/cli.d.ts +3 -0
  27. package/dist/generators/cli.d.ts.map +1 -1
  28. package/dist/generators/cli.js +7 -4
  29. package/dist/generators/cli.js.map +1 -1
  30. package/dist/generators/conditional-get.d.ts +17 -6
  31. package/dist/generators/conditional-get.d.ts.map +1 -1
  32. package/dist/generators/conditional-get.js +19 -13
  33. package/dist/generators/conditional-get.js.map +1 -1
  34. package/dist/generators/events-route.d.ts +72 -0
  35. package/dist/generators/events-route.d.ts.map +1 -0
  36. package/dist/generators/events-route.js +234 -0
  37. package/dist/generators/events-route.js.map +1 -0
  38. package/dist/generators/index.d.ts +1 -0
  39. package/dist/generators/index.d.ts.map +1 -1
  40. package/dist/generators/index.js +2 -1
  41. package/dist/generators/mcp-runtime-template.d.ts.map +1 -1
  42. package/dist/generators/mcp-runtime-template.js +11 -5
  43. package/dist/generators/mcp-runtime-template.js.map +1 -1
  44. package/dist/generators/mcp.d.ts +3 -0
  45. package/dist/generators/mcp.d.ts.map +1 -1
  46. package/dist/generators/mcp.js +18 -9
  47. package/dist/generators/mcp.js.map +1 -1
  48. package/dist/generators/rest.d.ts +7 -1
  49. package/dist/generators/rest.d.ts.map +1 -1
  50. package/dist/generators/rest.js +77 -13
  51. package/dist/generators/rest.js.map +1 -1
  52. package/dist/generators.js +2 -1
  53. package/dist/index.d.ts +1 -0
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +3 -1
  56. package/dist/manifest/generator.js.map +1 -1
  57. package/dist/manifest/manifest-loader.js.map +1 -1
  58. package/dist/manifest/static-manifest.d.ts.map +1 -1
  59. package/dist/manifest/static-manifest.js +7 -3
  60. package/dist/manifest/static-manifest.js.map +1 -1
  61. package/dist/manifest/store.js +1 -1
  62. package/dist/manifest/store.js.map +1 -1
  63. package/dist/manifest/test-manifest-stub.d.ts.map +1 -1
  64. package/dist/manifest/test-manifest-stub.js +1050 -124
  65. package/dist/manifest/test-manifest-stub.js.map +1 -1
  66. package/dist/manifest.json +9 -3
  67. package/dist/mcp-advisor/tools/preview-api-endpoints.d.ts +1 -0
  68. package/dist/mcp-advisor/tools/preview-api-endpoints.d.ts.map +1 -1
  69. package/dist/mcp-advisor/types.d.ts +3 -0
  70. package/dist/mcp-advisor/types.d.ts.map +1 -1
  71. package/dist/migrations/tracker.js +5 -4
  72. package/dist/migrations/tracker.js.map +1 -1
  73. package/dist/migrations/types.d.ts +1 -1
  74. package/dist/migrations/types.d.ts.map +1 -1
  75. package/dist/object.d.ts +17 -8
  76. package/dist/object.d.ts.map +1 -1
  77. package/dist/object.js +42 -26
  78. package/dist/object.js.map +1 -1
  79. package/dist/registry/class-registration.d.ts.map +1 -1
  80. package/dist/registry/class-registration.js +12 -0
  81. package/dist/registry/class-registration.js.map +1 -1
  82. package/dist/registry/shared-state.d.ts +2 -1
  83. package/dist/registry/shared-state.d.ts.map +1 -1
  84. package/dist/registry/shared-state.js.map +1 -1
  85. package/dist/registry/types.d.ts.map +1 -1
  86. package/dist/registry.d.ts.map +1 -1
  87. package/dist/registry.js +1 -1
  88. package/dist/registry.js.map +1 -1
  89. package/dist/runtime/client.d.ts.map +1 -1
  90. package/dist/runtime/client.js.map +1 -1
  91. package/dist/scanner/manifest-generator.js.map +1 -1
  92. package/dist/scanner/types.d.ts +7 -0
  93. package/dist/scanner/types.d.ts.map +1 -1
  94. package/dist/schema/ddl/base-strategy.js +1 -1
  95. package/dist/schema/ddl/base-strategy.js.map +1 -1
  96. package/dist/scripts/migrate-datetime-to-timestamp.ts +1 -1
  97. package/dist/signals/sanitizer.d.ts.map +1 -1
  98. package/dist/signals/sanitizer.js.map +1 -1
  99. package/dist/smrt-knowledge.json +8 -8
  100. package/dist/testing/index.d.ts +1 -0
  101. package/dist/testing/index.d.ts.map +1 -1
  102. package/dist/testing/index.js +2 -1
  103. package/dist/testing/notifications-stub.d.ts +24 -0
  104. package/dist/testing/notifications-stub.d.ts.map +1 -0
  105. package/dist/testing/notifications-stub.js +62 -0
  106. package/dist/testing/notifications-stub.js.map +1 -0
  107. package/dist/testing.js +2 -1
  108. package/dist/tools/tool-generator.js +1 -1
  109. package/dist/tools/tool-generator.js.map +1 -1
  110. package/dist/vite-plugin/changes-route.d.ts +14 -0
  111. package/dist/vite-plugin/changes-route.d.ts.map +1 -1
  112. package/dist/vite-plugin/changes-route.js +2 -2
  113. package/dist/vite-plugin/changes-route.js.map +1 -1
  114. package/dist/vite-plugin/events-route.d.ts +9 -0
  115. package/dist/vite-plugin/events-route.d.ts.map +1 -0
  116. package/dist/vite-plugin/events-route.js +155 -0
  117. package/dist/vite-plugin/events-route.js.map +1 -0
  118. package/dist/vite-plugin/index.d.ts +8 -0
  119. package/dist/vite-plugin/index.d.ts.map +1 -1
  120. package/dist/vite-plugin/index.js +8 -2
  121. package/dist/vite-plugin/index.js.map +1 -1
  122. package/dist/vite-plugin/sveltekit-generator.d.ts +8 -0
  123. package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
  124. package/dist/vite-plugin/sveltekit-generator.js +193 -31
  125. package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
  126. package/dist/vite-plugin/web-collections.js +1 -1
  127. package/dist/vite-plugin/web-collections.js.map +1 -1
  128. package/package.json +12 -12
@@ -1,6 +1,7 @@
1
1
  import { generateConditionalGetRouteHelper } from "../generators/conditional-get.js";
2
2
  import { AUTO_GENERATED_ROUTE_HEADER } from "./route-header.js";
3
3
  import { generateChangesRoute } from "./changes-route.js";
4
+ import { generateEventsRoute } from "./events-route.js";
4
5
  import { generateSyncApplyRoute } from "./sync-apply-route.js";
5
6
  import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
6
7
  import { join, relative } from "node:path";
@@ -175,6 +176,59 @@ function getApiPublicAccess(apiConfig) {
175
176
  if (value === true || value === "read") return value;
176
177
  return false;
177
178
  }
179
+ function toFieldColumnAlias(fieldName) {
180
+ return fieldName.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z\d])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toLowerCase();
181
+ }
182
+ function manifestObjectKey(objectDef) {
183
+ return objectDef.qualifiedName ?? objectDef.className;
184
+ }
185
+ function manifestObjectExtends(objectDef, parent) {
186
+ const extendsQualified = objectDef.extendsQualified;
187
+ const extendsName = objectDef.extends;
188
+ return typeof extendsQualified === "string" && extendsQualified === parent.qualifiedName || typeof extendsName === "string" && (extendsName === parent.className || extendsName === parent.qualifiedName);
189
+ }
190
+ function findManifestParent(objectDef, manifest) {
191
+ return Object.values(manifest.objects).find((candidate) => manifestObjectExtends(objectDef, candidate));
192
+ }
193
+ function collectStiHierarchyObjects(objectDef, manifest) {
194
+ if (!manifest) return [objectDef];
195
+ let cursor = objectDef;
196
+ let stiBase = cursor.decoratorConfig?.tableStrategy === "sti" ? cursor : void 0;
197
+ while (cursor) {
198
+ const parent = findManifestParent(cursor, manifest);
199
+ if (!parent) break;
200
+ if (parent.decoratorConfig?.tableStrategy === "sti") stiBase = parent;
201
+ cursor = parent;
202
+ }
203
+ if (!stiBase) return [objectDef];
204
+ const members = /* @__PURE__ */ new Map();
205
+ const visit = (current) => {
206
+ const key = manifestObjectKey(current);
207
+ if (members.has(key)) return;
208
+ members.set(key, current);
209
+ for (const candidate of Object.values(manifest.objects)) {
210
+ if (candidate === current) continue;
211
+ if (manifestObjectExtends(candidate, current)) visit(candidate);
212
+ }
213
+ };
214
+ visit(stiBase);
215
+ return [...members.values()];
216
+ }
217
+ function collectReadPermissionFields(objectDef, manifest) {
218
+ const result = [];
219
+ const seen = /* @__PURE__ */ new Set();
220
+ for (const member of collectStiHierarchyObjects(objectDef, manifest)) for (const [name, def] of Object.entries(member.fields ?? {})) {
221
+ const permission = typeof def.readPermission === "string" ? def.readPermission : typeof def._meta?.readPermission === "string" ? def._meta.readPermission : void 0;
222
+ if (!permission) continue;
223
+ for (const fieldName of [name, toFieldColumnAlias(name)]) {
224
+ const key = `${fieldName}\0${permission}`;
225
+ if (seen.has(key)) continue;
226
+ seen.add(key);
227
+ result.push([fieldName, permission]);
228
+ }
229
+ }
230
+ return result;
231
+ }
178
232
  /**
179
233
  * Emit the fail-closed authorization guard injected into generated route files
180
234
  * (#1540, 2c). Every generated CRUD/action handler calls `requireRouteAuth`,
@@ -182,12 +236,18 @@ function getApiPublicAccess(apiConfig) {
182
236
  * authenticated principal. Mirrors the knowledge-route `isKnowledgeAdmin`
183
237
  * precedent. Mutating verbs require auth even when reads are public.
184
238
  */
185
- function generateAuthGuardHelper(objectDef) {
239
+ function generateAuthGuardHelper(objectDef, manifest) {
186
240
  const publicAccess = getApiPublicAccess(objectDef.decoratorConfig?.api);
241
+ const readPermissionFields = collectReadPermissionFields(objectDef, manifest);
187
242
  return `
188
243
  // Fail-closed authorization (#1540): generated routes require an authenticated
189
244
  // principal on \`locals\` unless explicitly marked \`@smrt({ api: { public } })\`.
190
245
  const PUBLIC_ACCESS: boolean | 'read' = ${JSON.stringify(publicAccess)};
246
+ const READ_PERMISSION_FIELDS: Array<[string, string]> = ${JSON.stringify(readPermissionFields)};
247
+
248
+ interface PublicJsonOptions {
249
+ permissions?: Iterable<string>;
250
+ }
191
251
 
192
252
  function hasAuthenticatedPrincipal(locals: unknown): boolean {
193
253
  if (!locals || typeof locals !== 'object') return false;
@@ -215,13 +275,74 @@ function requireRouteAuth(locals: unknown, mutating: boolean): void {
215
275
  }
216
276
  }
217
277
 
278
+ function getPublicJsonOptions(locals: unknown): PublicJsonOptions {
279
+ const l = readJsonRecord(locals);
280
+ const permissions = l.permissions ?? l.permissionSet ?? l.smrtPermissions;
281
+ if (Array.isArray(permissions) || permissions instanceof Set) {
282
+ return { permissions };
283
+ }
284
+ return {};
285
+ }
286
+
287
+ function hasReadPermission(
288
+ options: PublicJsonOptions,
289
+ permission: string,
290
+ ): boolean {
291
+ if (!options.permissions) return false;
292
+ for (const granted of options.permissions) {
293
+ if (granted === permission) return true;
294
+ }
295
+ return false;
296
+ }
297
+
298
+ function applyReadPermissionRedaction(
299
+ value: unknown,
300
+ options: PublicJsonOptions,
301
+ seen: WeakSet<object> = new WeakSet(),
302
+ ): unknown {
303
+ if (value === null || typeof value !== 'object') return value;
304
+ if (seen.has(value)) return null;
305
+ if (hasPublicJson(value)) {
306
+ seen.add(value);
307
+ return applyReadPermissionRedaction(
308
+ value.toPublicJSON(options),
309
+ options,
310
+ seen,
311
+ );
312
+ }
313
+ if (Array.isArray(value)) {
314
+ seen.add(value);
315
+ return value.map((entry) =>
316
+ applyReadPermissionRedaction(entry, options, seen),
317
+ );
318
+ }
319
+ const proto = Object.getPrototypeOf(value);
320
+ if (proto !== Object.prototype && proto !== null) return value;
321
+ seen.add(value);
322
+ const out = { ...(value as Record<string, unknown>) };
323
+ const metaData =
324
+ out._meta_data && typeof out._meta_data === 'object'
325
+ ? { ...(out._meta_data as Record<string, unknown>) }
326
+ : null;
327
+ for (const [fieldName, permission] of READ_PERMISSION_FIELDS) {
328
+ if (hasReadPermission(options, permission)) continue;
329
+ delete out[fieldName];
330
+ if (metaData) delete metaData[fieldName];
331
+ }
332
+ if (metaData) out._meta_data = metaData;
333
+ for (const [key, entry] of Object.entries(out)) {
334
+ out[key] = applyReadPermissionRedaction(entry, options, seen);
335
+ }
336
+ return out;
337
+ }
338
+
218
339
  // Sensitive-field-safe serialization for custom-action results (#1540): a
219
340
  // custom method may return a SmrtObject (or one nested in an array/plain
220
341
  // object), so recurse and route each through toPublicJSON() rather than letting
221
342
  // JSON.stringify call toJSON(). Non-plain instances (Date, etc.) and primitives
222
343
  // pass through; a cycle guard prevents infinite loops.
223
344
  interface PublicJsonSource {
224
- toPublicJSON(): unknown;
345
+ toPublicJSON(options?: PublicJsonOptions): unknown;
225
346
  }
226
347
 
227
348
  function hasPublicJson(value: object): value is PublicJsonSource {
@@ -238,17 +359,18 @@ function readJsonRecord(value: unknown): Record<string, unknown> {
238
359
 
239
360
  function toPublicResult(
240
361
  value: unknown,
362
+ options: PublicJsonOptions,
241
363
  seen: WeakSet<object> = new WeakSet(),
242
364
  ): unknown {
243
365
  if (value === null || typeof value !== 'object') return value;
244
366
  if (seen.has(value)) return null;
245
367
  if (hasPublicJson(value)) {
246
368
  seen.add(value);
247
- return toPublicResult(value.toPublicJSON(), seen);
369
+ return toPublicResult(value.toPublicJSON(options), options, seen);
248
370
  }
249
371
  if (Array.isArray(value)) {
250
372
  seen.add(value);
251
- return value.map((entry) => toPublicResult(entry, seen));
373
+ return value.map((entry) => toPublicResult(entry, options, seen));
252
374
  }
253
375
  const proto = Object.getPrototypeOf(value);
254
376
  if (proto !== Object.prototype && proto !== null) return value;
@@ -257,7 +379,7 @@ function toPublicResult(
257
379
  for (const [key, entry] of Object.entries(
258
380
  value as Record<string, unknown>,
259
381
  )) {
260
- out[key] = toPublicResult(entry, seen);
382
+ out[key] = toPublicResult(entry, options, seen);
261
383
  }
262
384
  return out;
263
385
  }
@@ -492,12 +614,13 @@ async function generateSvelteKitRoutes(projectRoot, manifest, options) {
492
614
  }
493
615
  continue;
494
616
  }
495
- await generateRoutesForObject(projectRoot, className, objectDef, options);
617
+ await generateRoutesForObject(projectRoot, className, objectDef, manifest, options);
496
618
  generatedCount++;
497
619
  }
498
620
  if (options.knowledge?.api?.enabled) generateKnowledgeRoute(projectRoot, options);
499
621
  generateSyncApplyRoute(projectRoot, manifest, options);
500
622
  generateChangesRoute(projectRoot, manifest, options);
623
+ generateEventsRoute(projectRoot, manifest, options);
501
624
  updateGitignore(projectRoot, options);
502
625
  const skippedMsg = skippedCollections > 0 ? ` (skipped ${skippedCollections} collection classes)` : "";
503
626
  console.log(`[smrt] Generated routes for ${generatedCount} SMRT objects${skippedMsg}`);
@@ -780,7 +903,7 @@ export async function getCollection<
780
903
  /**
781
904
  * Generates route files for a single SMRT object
782
905
  */
783
- async function generateRoutesForObject(projectRoot, className, objectDef, options) {
906
+ async function generateRoutesForObject(projectRoot, className, objectDef, manifest, options) {
784
907
  const collectionName = objectDef.collection;
785
908
  const routeDir = join(projectRoot, options.routesDir, collectionName);
786
909
  const apiConfig = objectDef.decoratorConfig?.api;
@@ -789,9 +912,9 @@ async function generateRoutesForObject(projectRoot, className, objectDef, option
789
912
  return;
790
913
  }
791
914
  const includedActions = resolveStandardCrudActions(apiConfig);
792
- if (includedActions.includes("list") || includedActions.includes("create")) writeRoute(routeDir, "+server.ts", generateCollectionRouteTemplate(projectRoot, className, objectDef, includedActions, options, routeDir));
915
+ if (includedActions.includes("list") || includedActions.includes("create")) writeRoute(routeDir, "+server.ts", generateCollectionRouteTemplate(projectRoot, className, objectDef, manifest, includedActions, options, routeDir));
793
916
  if (includedActions.includes("get") || includedActions.includes("update") || includedActions.includes("delete")) {
794
- const itemRoute = generateItemRouteTemplate(projectRoot, className, objectDef, includedActions, options, join(routeDir, "[id]"));
917
+ const itemRoute = generateItemRouteTemplate(projectRoot, className, objectDef, manifest, includedActions, options, join(routeDir, "[id]"));
795
918
  writeRoute(join(routeDir, "[id]"), "+server.ts", itemRoute);
796
919
  }
797
920
  const customActions = Object.entries(objectDef.methods).filter(([name, method]) => !STANDARD_API_ACTIONS.includes(name) && method.isPublic && shouldIncludeInApi(name, apiConfig));
@@ -816,7 +939,7 @@ async function generateRoutesForObject(projectRoot, className, objectDef, option
816
939
  }
817
940
  });
818
941
  }
819
- for (const [actionRouteDir, routeSpecs] of groupCustomActionRoutes(actionSpecs)) writeRoute(actionRouteDir, "+server.ts", generateActionRouteTemplate(projectRoot, actionRouteDir, routeSpecs, objectDef, options));
942
+ for (const [actionRouteDir, routeSpecs] of groupCustomActionRoutes(actionSpecs)) writeRoute(actionRouteDir, "+server.ts", generateActionRouteTemplate(projectRoot, actionRouteDir, routeSpecs, objectDef, manifest, options));
820
943
  }
821
944
  async function generateCollectionRoutesForObject(projectRoot, className, objectDef, manifest, options) {
822
945
  const apiConfig = objectDef.decoratorConfig?.api;
@@ -851,7 +974,7 @@ async function generateCollectionRoutesForObject(projectRoot, className, objectD
851
974
  });
852
975
  }
853
976
  for (const [actionRouteDir, routeSpecs] of groupCustomActionRoutes(actionSpecs)) {
854
- writeRoute(actionRouteDir, "+server.ts", generateActionRouteTemplate(projectRoot, actionRouteDir, routeSpecs, objectDef, options));
977
+ writeRoute(actionRouteDir, "+server.ts", generateActionRouteTemplate(projectRoot, actionRouteDir, routeSpecs, objectDef, manifest, options));
855
978
  generatedAnyRoutes = true;
856
979
  }
857
980
  return generatedAnyRoutes;
@@ -998,13 +1121,15 @@ function isLocalObject(projectRoot, objectDef) {
998
1121
  /**
999
1122
  * Generates collection route template (GET list, POST create)
1000
1123
  */
1001
- function generateCollectionRouteTemplate(projectRoot, className, objectDef, includedActions, options, routeDir) {
1124
+ function generateCollectionRouteTemplate(projectRoot, className, objectDef, manifest, includedActions, options, routeDir) {
1002
1125
  const hasGet = includedActions.includes("list");
1003
1126
  const hasPost = includedActions.includes("create");
1004
1127
  const modelType = resolveObjectTypeReference(projectRoot, className, objectDef, options, routeDir);
1005
1128
  const serializers = resolveStandardRouteSerializers(objectDef.decoratorConfig?.api);
1006
1129
  const serializerImports = serializers.importStatements.join("\n");
1007
1130
  const listUsesSerializer = !!serializers.listItemSerializerName;
1131
+ const listUsesPermissionScopedBody = collectReadPermissionFields(objectDef, manifest).length > 0;
1132
+ const listUsesBodyHash = listUsesSerializer || listUsesPermissionScopedBody;
1008
1133
  const imports = `${AUTO_GENERATED_ROUTE_HEADER}
1009
1134
  // DO NOT EDIT - changes will be overwritten
1010
1135
 
@@ -1012,10 +1137,11 @@ import { error${hasPost ? ", json" : ""} } from '@sveltejs/kit';
1012
1137
  ${serializerImports ? `${serializerImports}\n` : ""}import { getCollection } from '$lib/server/smrt';
1013
1138
  ${modelType.importStatement ? `${modelType.importStatement}\n` : ""}import type { RequestHandler } from './$types';
1014
1139
  // Note: ${className} is auto-registered by the Vite plugin scanner
1015
- ${generateAuthGuardHelper(objectDef)}${isTenantScoped(objectDef) ? generateTenantContextHelper() : ""}${hasPost ? generateWritablePolicyHelper(objectDef) : ""}${hasGet ? generateConditionalGetRouteHelper(objectDef.decoratorConfig?.api, {
1140
+ ${generateAuthGuardHelper(objectDef, manifest)}${isTenantScoped(objectDef) ? generateTenantContextHelper() : ""}${hasPost ? generateWritablePolicyHelper(objectDef) : ""}${hasGet ? generateConditionalGetRouteHelper(objectDef.decoratorConfig?.api, {
1016
1141
  tenantScoped: isTenantScoped(objectDef),
1142
+ permissionScoped: listUsesPermissionScopedBody,
1017
1143
  modelName: className,
1018
- useBodyHash: listUsesSerializer
1144
+ useBodyHash: listUsesBodyHash
1019
1145
  }) : ""}`;
1020
1146
  const listAndCount = isTenantScoped(objectDef) ? ` const readScope = tenantReadScope();
1021
1147
  const items = await collection.list({ limit, offset, where: readScope });
@@ -1025,6 +1151,7 @@ ${generateAuthGuardHelper(objectDef)}${isTenantScoped(objectDef) ? generateTenan
1025
1151
  // List all ${className.toLowerCase()}s
1026
1152
  export const GET: RequestHandler = async ({ locals, url, request }) => {
1027
1153
  ${routeGuardPreamble(objectDef, false)}
1154
+ const publicJsonOptions = getPublicJsonOptions(locals);
1028
1155
  const limit = Number(url.searchParams.get('limit')) || 50;
1029
1156
  const offset = Number(url.searchParams.get('offset')) || 0;
1030
1157
 
@@ -1032,13 +1159,21 @@ ${generateCollectionLoad(className, { typeName: modelType.typeName })}
1032
1159
  ${listUsesSerializer ? `${listAndCount}
1033
1160
  // Custom serializer may render related-table data → v1 body-hash ETag (#1765).
1034
1161
  const serializedItems = await Promise.all(
1035
- items.map((item) => ${serializers.listItemSerializerName}(item)),
1162
+ items.map(async (item) =>
1163
+ applyReadPermissionRedaction(
1164
+ await ${serializers.listItemSerializerName}(item),
1165
+ publicJsonOptions,
1166
+ ),
1167
+ ),
1036
1168
  );
1037
- return conditionalJson(request, { items: serializedItems, count, limit, offset });` : ` // ETag v2 (#1765): the table-version ETag is checked first; on a concrete
1169
+ return conditionalJson(request, { items: serializedItems, count, limit, offset });` : listUsesPermissionScopedBody ? `${listAndCount}
1170
+ // Field read permissions make the body vary by caller permissions → v1 body-hash ETag (#1565).
1171
+ const items_public = items.map((item) => item.toPublicJSON(publicJsonOptions));
1172
+ return conditionalJson(request, { items: items_public, count, limit, offset });` : ` // ETag v2 (#1765): the table-version ETag is checked first; on a concrete
1038
1173
  // If-None-Match the list query below never runs (zero-query 304).
1039
1174
  return conditionalVersionedRead(request, collection.db, collection.tableName, async () => {
1040
1175
  ${listAndCount}
1041
- const items_public = items.map((item) => item.toPublicJSON());
1176
+ const items_public = items.map((item) => item.toPublicJSON(publicJsonOptions));
1042
1177
  return { items: items_public, count, limit, offset };
1043
1178
  });`}
1044
1179
  };
@@ -1047,6 +1182,7 @@ ${listAndCount}
1047
1182
  // Create new ${className.toLowerCase()}
1048
1183
  export const POST: RequestHandler = async ({ locals, request }) => {
1049
1184
  ${routeGuardPreamble(objectDef, true)}
1185
+ const publicJsonOptions = getPublicJsonOptions(locals);
1050
1186
  const body: unknown = await request.json();
1051
1187
  const data = applyWritablePolicy(body);
1052
1188
 
@@ -1056,8 +1192,11 @@ ${generateCollectionLoad(className, { typeName: modelType.typeName })}
1056
1192
  ${serializers.itemSerializerName ? `
1057
1193
  const serializedItem = await ${serializers.itemSerializerName}(item);
1058
1194
 
1059
- return json(serializedItem, { status: 201 });` : `
1060
- return json(item.toPublicJSON(), { status: 201 });`}
1195
+ return json(
1196
+ applyReadPermissionRedaction(serializedItem, publicJsonOptions),
1197
+ { status: 201 },
1198
+ );` : `
1199
+ return json(item.toPublicJSON(publicJsonOptions), { status: 201 });`}
1061
1200
  };
1062
1201
  ` : "";
1063
1202
  return imports + getHandler + postHandler;
@@ -1101,7 +1240,7 @@ function generateClassNotRegisteredError(className) {
1101
1240
  /**
1102
1241
  * Generates item route template (GET, PUT, DELETE)
1103
1242
  */
1104
- function generateItemRouteTemplate(projectRoot, className, objectDef, includedActions, options, routeDir) {
1243
+ function generateItemRouteTemplate(projectRoot, className, objectDef, manifest, includedActions, options, routeDir) {
1105
1244
  const hasGet = includedActions.includes("get");
1106
1245
  const hasPut = includedActions.includes("update");
1107
1246
  const hasDelete = includedActions.includes("delete");
@@ -1110,16 +1249,19 @@ function generateItemRouteTemplate(projectRoot, className, objectDef, includedAc
1110
1249
  const serializers = resolveStandardRouteSerializers(objectDef.decoratorConfig?.api);
1111
1250
  const serializerImports = serializers.importStatements.join("\n");
1112
1251
  const getUsesSerializer = !!serializers.itemSerializerName;
1252
+ const getUsesPermissionScopedBody = collectReadPermissionFields(objectDef, manifest).length > 0;
1253
+ const getUsesBodyHash = getUsesSerializer || getUsesPermissionScopedBody;
1113
1254
  const imports = `${AUTO_GENERATED_ROUTE_HEADER}
1114
1255
  // DO NOT EDIT - changes will be overwritten
1115
1256
 
1116
1257
  import { error${hasPut || hasDelete ? ", json" : ""} } from '@sveltejs/kit';
1117
1258
  ${serializerImports ? `${serializerImports}\n` : ""}import { getCollection } from '$lib/server/smrt';
1118
1259
  ${modelType.importStatement ? `${modelType.importStatement}\n` : ""}import type { RequestHandler } from './$types';
1119
- ${generateAuthGuardHelper(objectDef)}${isTenantScoped(objectDef) ? generateTenantContextHelper() : ""}${hasPut ? generateWritablePolicyHelper(objectDef) : ""}${hasGet ? generateConditionalGetRouteHelper(objectDef.decoratorConfig?.api, {
1260
+ ${generateAuthGuardHelper(objectDef, manifest)}${isTenantScoped(objectDef) ? generateTenantContextHelper() : ""}${hasPut ? generateWritablePolicyHelper(objectDef) : ""}${hasGet ? generateConditionalGetRouteHelper(objectDef.decoratorConfig?.api, {
1120
1261
  tenantScoped: isTenantScoped(objectDef),
1262
+ permissionScoped: getUsesPermissionScopedBody,
1121
1263
  modelName: className,
1122
- useBodyHash: getUsesSerializer
1264
+ useBodyHash: getUsesBodyHash
1123
1265
  }) : ""}`;
1124
1266
  const getForRead = isTenantScoped(objectDef) ? ` const readScope = tenantReadScope();
1125
1267
  const item = await collection.get(
@@ -1129,18 +1271,25 @@ ${generateAuthGuardHelper(objectDef)}${isTenantScoped(objectDef) ? generateTenan
1129
1271
  // Get single ${simpleClassName.toLowerCase()}
1130
1272
  export const GET: RequestHandler = async ({ locals, params, request }) => {
1131
1273
  ${routeGuardPreamble(objectDef, false)}
1274
+ const publicJsonOptions = getPublicJsonOptions(locals);
1132
1275
  ${generateCollectionLoad(className, { typeName: modelType.typeName })}
1133
1276
  ${getUsesSerializer ? `${getForRead}
1134
1277
  ${generateNotFoundError(className)}
1135
1278
  // Custom serializer may render related-table data → v1 body-hash ETag (#1765).
1136
1279
  const serializedItem = await ${serializers.itemSerializerName}(item);
1137
- return conditionalJson(request, serializedItem);` : ` // ETag v2 (#1765): a concrete If-None-Match answers 304 without the row fetch
1280
+ return conditionalJson(
1281
+ request,
1282
+ applyReadPermissionRedaction(serializedItem, publicJsonOptions),
1283
+ );` : getUsesPermissionScopedBody ? `${getForRead}
1284
+ ${generateNotFoundError(className)}
1285
+ // Field read permissions make the body vary by caller permissions → v1 body-hash ETag (#1565).
1286
+ return conditionalJson(request, item.toPublicJSON(publicJsonOptions));` : ` // ETag v2 (#1765): a concrete If-None-Match answers 304 without the row fetch
1138
1287
  // (a delete advances the version, so a since-deleted row can't false-match);
1139
1288
  // a wildcard \`*\` is deferred until the fetch confirms the row exists.
1140
1289
  return conditionalVersionedRead(request, collection.db, collection.tableName, async () => {
1141
1290
  ${getForRead}
1142
1291
  ${generateNotFoundError(className)}
1143
- return item.toPublicJSON();
1292
+ return item.toPublicJSON(publicJsonOptions);
1144
1293
  });`}
1145
1294
  };
1146
1295
  ` : "";
@@ -1148,6 +1297,7 @@ ${generateNotFoundError(className)}
1148
1297
  // Update ${simpleClassName.toLowerCase()}
1149
1298
  export const PUT: RequestHandler = async ({ locals, params, request }) => {
1150
1299
  ${routeGuardPreamble(objectDef, true)}
1300
+ const publicJsonOptions = getPublicJsonOptions(locals);
1151
1301
  ${generateCollectionLoad(className, { typeName: modelType.typeName })}
1152
1302
  const item = await collection.get(params.id);
1153
1303
  ${generateNotFoundError(className)}
@@ -1159,8 +1309,8 @@ ${generateNotFoundError(className)}
1159
1309
  ${serializers.itemSerializerName ? `
1160
1310
  const serializedItem = await ${serializers.itemSerializerName}(item);
1161
1311
 
1162
- return json(serializedItem);` : `
1163
- return json(item.toPublicJSON());`}
1312
+ return json(applyReadPermissionRedaction(serializedItem, publicJsonOptions));` : `
1313
+ return json(item.toPublicJSON(publicJsonOptions));`}
1164
1314
  };
1165
1315
  ` : "";
1166
1316
  const deleteHandler = hasDelete ? `
@@ -1180,7 +1330,7 @@ ${generateNotFoundError(className)}
1180
1330
  /**
1181
1331
  * Generates custom action route template
1182
1332
  */
1183
- function generateActionRouteTemplate(projectRoot, routeDir, routeSpecs, objectDef, options) {
1333
+ function generateActionRouteTemplate(projectRoot, routeDir, routeSpecs, objectDef, manifest, options) {
1184
1334
  if (routeSpecs.length === 0) throw new Error("Cannot generate a custom action route without handlers");
1185
1335
  const [firstSpec] = routeSpecs;
1186
1336
  const { lookupClassName, routeConfig, hostType } = firstSpec;
@@ -1200,7 +1350,7 @@ function generateActionRouteTemplate(projectRoot, routeDir, routeSpecs, objectDe
1200
1350
  // DO NOT EDIT - changes will be overwritten
1201
1351
 
1202
1352
  ${importBlock}
1203
- ${generateAuthGuardHelper(objectDef)}${tenantScoped ? generateTenantContextHelper() : ""}
1353
+ ${generateAuthGuardHelper(objectDef, manifest)}${tenantScoped ? generateTenantContextHelper() : ""}
1204
1354
  ${handlers}`;
1205
1355
  }
1206
1356
  function generateActionRouteHandler(lookupClassName, actionName, actionDef, routeConfig, hostType, tenantScoped, lookupTypeName, hostTypeName) {
@@ -1219,6 +1369,7 @@ function generateActionRouteHandler(lookupClassName, actionName, actionDef, rout
1219
1369
  return `// Custom collection method: ${actionName}
1220
1370
  export const ${handlerName}: RequestHandler = async (${collectionHandlerArgs}) => {
1221
1371
  ${authGuardLine}
1372
+ const publicJsonOptions = getPublicJsonOptions(locals);
1222
1373
  ${generateCollectionLoad(lookupClassName, { typeName: lookupTypeName })}
1223
1374
  const typedCollection = collection as unknown as ${hostTypeName};
1224
1375
  ${generateCollectionNotRegisteredError(lookupClassName)}
@@ -1226,7 +1377,10 @@ ${staticTargetLoad}
1226
1377
 
1227
1378
  ${optionsLoad} const result = ${buildActionInvocationExpression(receiverExpression, actionName, invocationArgs)};
1228
1379
 
1229
- return json({ action: '${actionName}', result: toPublicResult(result) });
1380
+ return json({
1381
+ action: '${actionName}',
1382
+ result: toPublicResult(result, publicJsonOptions),
1383
+ });
1230
1384
  };
1231
1385
  `;
1232
1386
  }
@@ -1235,13 +1389,17 @@ ${optionsLoad} const result = ${buildActionInvocationExpression(receiverExpress
1235
1389
  return `// Custom collection action: ${actionName}
1236
1390
  export const ${handlerName}: RequestHandler = async (${collectionHandlerArgs}) => {
1237
1391
  ${authGuardLine}
1392
+ const publicJsonOptions = getPublicJsonOptions(locals);
1238
1393
  const registered = ObjectRegistry.getClass('${lookupClassName}');
1239
1394
  ${generateClassNotRegisteredError(lookupClassName)}
1240
1395
 
1241
1396
  ${optionsLoad} const ClassRef = registered.constructor as typeof ${hostTypeName};
1242
1397
  const result = ${buildActionInvocationExpression("ClassRef", actionName, invocationArgs)};
1243
1398
 
1244
- return json({ action: '${actionName}', result: toPublicResult(result) });
1399
+ return json({
1400
+ action: '${actionName}',
1401
+ result: toPublicResult(result, publicJsonOptions),
1402
+ });
1245
1403
  };
1246
1404
  `;
1247
1405
  }
@@ -1249,13 +1407,17 @@ ${optionsLoad} const ClassRef = registered.constructor as typeof ${hostTypeName
1249
1407
  return `// Custom action: ${actionName}
1250
1408
  export const ${handlerName}: RequestHandler = async (${itemHandlerArgs}) => {
1251
1409
  ${authGuardLine}
1410
+ const publicJsonOptions = getPublicJsonOptions(locals);
1252
1411
  ${generateCollectionLoad(lookupClassName, { typeName: lookupTypeName })}
1253
1412
  const item = await collection.get(params.id);
1254
1413
  ${generateNotFoundError(lookupClassName)}
1255
1414
 
1256
1415
  ${optionsLoad} const result = ${buildActionInvocationExpression("item", actionName, invocationArgs)};
1257
1416
 
1258
- return json({ action: '${actionName}', result: toPublicResult(result) });
1417
+ return json({
1418
+ action: '${actionName}',
1419
+ result: toPublicResult(result, publicJsonOptions),
1420
+ });
1259
1421
  };
1260
1422
  `;
1261
1423
  }