@happyvertical/smrt-core 0.40.59 → 0.40.61

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 (56) hide show
  1. package/dist/generated-client-runtime.d.ts +6 -5
  2. package/dist/generated-client-runtime.d.ts.map +1 -1
  3. package/dist/generated-client-runtime.js +49 -14
  4. package/dist/generated-client-runtime.js.map +1 -1
  5. package/dist/generators/index.d.ts +3 -2
  6. package/dist/generators/index.d.ts.map +1 -1
  7. package/dist/generators/index.js +3 -2
  8. package/dist/generators/mcp-runtime-template.d.ts +13 -0
  9. package/dist/generators/mcp-runtime-template.d.ts.map +1 -1
  10. package/dist/generators/mcp-runtime-template.js +83 -32
  11. package/dist/generators/mcp-runtime-template.js.map +1 -1
  12. package/dist/generators/mcp.d.ts +79 -18
  13. package/dist/generators/mcp.d.ts.map +1 -1
  14. package/dist/generators/mcp.js +429 -177
  15. package/dist/generators/mcp.js.map +1 -1
  16. package/dist/generators/rest.d.ts.map +1 -1
  17. package/dist/generators/rest.js +3 -0
  18. package/dist/generators/rest.js.map +1 -1
  19. package/dist/generators/tool-schema.d.ts +32 -1
  20. package/dist/generators/tool-schema.d.ts.map +1 -1
  21. package/dist/generators/tool-schema.js +108 -36
  22. package/dist/generators/tool-schema.js.map +1 -1
  23. package/dist/generators/typed-http-error.d.ts +16 -0
  24. package/dist/generators/typed-http-error.d.ts.map +1 -0
  25. package/dist/generators/typed-http-error.js +17 -0
  26. package/dist/generators/typed-http-error.js.map +1 -0
  27. package/dist/generators.js +3 -2
  28. package/dist/index.js +3 -2
  29. package/dist/manifest/static-manifest.js +1 -1
  30. package/dist/manifest/static-manifest.js.map +1 -1
  31. package/dist/manifest/store.js +1 -1
  32. package/dist/manifest.json +1 -1
  33. package/dist/prebuild/index.d.ts.map +1 -1
  34. package/dist/prebuild/index.js +14 -1
  35. package/dist/prebuild/index.js.map +1 -1
  36. package/dist/registry/types.d.ts +15 -0
  37. package/dist/registry/types.d.ts.map +1 -1
  38. package/dist/smrt-knowledge.json +3 -3
  39. package/dist/vite-plugin/index.d.ts +2 -0
  40. package/dist/vite-plugin/index.d.ts.map +1 -1
  41. package/dist/vite-plugin/index.js +25 -11
  42. package/dist/vite-plugin/index.js.map +1 -1
  43. package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
  44. package/dist/vite-plugin/sveltekit-generator.js +94 -26
  45. package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
  46. package/dist/vite-plugin/web-collections.d.ts +6 -3
  47. package/dist/vite-plugin/web-collections.d.ts.map +1 -1
  48. package/dist/vite-plugin/web-collections.js +24 -6
  49. package/dist/vite-plugin/web-collections.js.map +1 -1
  50. package/package.json +5 -5
  51. package/dist/manifest/test-manifest-loader.d.ts +0 -3
  52. package/dist/manifest/test-manifest-loader.d.ts.map +0 -1
  53. package/dist/manifest/test-manifest-stub.d.ts +0 -4
  54. package/dist/manifest/test-manifest-stub.d.ts.map +0 -1
  55. package/dist/manifest/test-manifest-stub.js +0 -75816
  56. package/dist/manifest/test-manifest-stub.js.map +0 -1
@@ -399,6 +399,17 @@ function isTenantScoped(objectDef) {
399
399
  return !!objectDef.decoratorConfig?.tenantScoped;
400
400
  }
401
401
  /**
402
+ * Whether generated routes must publish the authenticated principal into the
403
+ * tenancy ALS without making the model itself tenant-scoped. This is an
404
+ * explicit model-level opt-in: body data is never an identity source.
405
+ */
406
+ function usesPrincipalContext(objectDef) {
407
+ return getApiConfigObject(objectDef.decoratorConfig?.api)?.principalContext === true;
408
+ }
409
+ function needsRouteTenantContext(objectDef) {
410
+ return isTenantScoped(objectDef) || usesPrincipalContext(objectDef);
411
+ }
412
+ /**
402
413
  * Emit the tenant-context establishment + fail-closed read-scope helpers for
403
414
  * `@TenantScoped` objects (#1540 facet 2; #1782). Generated handlers call
404
415
  * `establishTenantContext(locals)` after the auth guard so every query runs
@@ -418,9 +429,9 @@ function isTenantScoped(objectDef) {
418
429
  * active. Both helpers are imported only for tenant-scoped routes, which already
419
430
  * depend on `@happyvertical/smrt-tenancy`.
420
431
  */
421
- function generateTenantContextHelper() {
432
+ function generateTenantContextHelper(requirePrincipal, includeReadScope) {
422
433
  return `
423
- import { enterTenantContext, getCurrentTenant, hasTenantContext, isSuperAdminBypass, isTenancyEnabled } from '@happyvertical/smrt-tenancy';
434
+ import { ${includeReadScope ? "enterTenantContext, getCurrentTenant, hasTenantContext, isSuperAdminBypass, isTenancyEnabled" : "enterTenantContext, hasTenantContext"} } from '@happyvertical/smrt-tenancy';
424
435
 
425
436
  function establishTenantContext(locals: unknown): void {
426
437
  if (hasTenantContext()) return;
@@ -429,11 +440,27 @@ function establishTenantContext(locals: unknown): void {
429
440
  const user = l.user as Record<string, unknown> | undefined;
430
441
  const session = l.session as Record<string, unknown> | undefined;
431
442
  const tenantId = l.tenantId ?? user?.tenantId ?? session?.tenantId;
443
+ const userId = l.userId ?? user?.id ?? user?.userId ?? session?.userId;
444
+ const rawPermissions = l.permissions;
445
+ const permissions =
446
+ rawPermissions instanceof Set && [...rawPermissions].every((value) => typeof value === 'string')
447
+ ? rawPermissions
448
+ : Array.isArray(rawPermissions) && rawPermissions.every((value) => typeof value === 'string')
449
+ ? new Set(rawPermissions)
450
+ : undefined;
451
+ ${requirePrincipal ? ` if (
452
+ typeof tenantId !== 'string' || !tenantId ||
453
+ typeof userId !== 'string' || !userId ||
454
+ !permissions
455
+ ) return;
456
+ ` : ""}
432
457
  if (typeof tenantId === 'string' && tenantId) {
433
- enterTenantContext({ tenantId });
458
+ enterTenantContext(
459
+ ${requirePrincipal ? "{ tenantId, userId, permissions }" : "{ tenantId }"},
460
+ );
434
461
  }
435
462
  }
436
-
463
+ ${includeReadScope ? `
437
464
  // Fail-closed read scope (#1782): a public/anonymous read on a @TenantScoped
438
465
  // model has no tenant context, so the tenancy interceptor (optional mode) would
439
466
  // pass the query through UNFILTERED and return every tenant's rows. When tenancy
@@ -458,6 +485,7 @@ function tenantReadOptionsScope(): { tenantId: string | null } | undefined {
458
485
  }
459
486
  return { tenantId: getCurrentTenant()?.tenantId ?? null };
460
487
  }
488
+ ` : ""}
461
489
  `;
462
490
  }
463
491
  /**
@@ -466,7 +494,7 @@ function tenantReadOptionsScope(): { tenantId: string | null } | undefined {
466
494
  */
467
495
  function routeGuardPreamble(objectDef, mutating) {
468
496
  const lines = [` requireRouteAuth(locals, ${mutating});`];
469
- if (isTenantScoped(objectDef)) lines.push(" establishTenantContext(locals);");
497
+ if (needsRouteTenantContext(objectDef)) lines.push(" establishTenantContext(locals);");
470
498
  return lines.join("\n");
471
499
  }
472
500
  function normalizeApiHttpMethod(method) {
@@ -1176,6 +1204,15 @@ function isLocalObject(projectRoot, objectDef) {
1176
1204
  /**
1177
1205
  * Generates collection route template (GET list, POST create)
1178
1206
  */
1207
+ function generateTypedRouteErrorHelper() {
1208
+ return `
1209
+ function smrtRouteErrorResponse(cause: unknown) {
1210
+ const failure = normalizeTypedHttpError(cause);
1211
+ if (failure) return json({ error: failure }, { status: failure.status });
1212
+ return json({ error: 'Internal server error' }, { status: 500 });
1213
+ }
1214
+ `;
1215
+ }
1179
1216
  function generateCollectionRouteTemplate(projectRoot, className, objectDef, manifest, includedActions, options, routeDir) {
1180
1217
  const hasGet = includedActions.includes("list");
1181
1218
  const hasPost = includedActions.includes("create");
@@ -1191,9 +1228,10 @@ function generateCollectionRouteTemplate(projectRoot, className, objectDef, mani
1191
1228
 
1192
1229
  import { error${hasPost ? ", json" : ""} } from '@sveltejs/kit';
1193
1230
  ${serializerImports ? `${serializerImports}\n` : ""}import { getCollection } from '$lib/server/smrt';
1231
+ ${hasPost ? "import { normalizeTypedHttpError } from '@happyvertical/smrt-core';\n" : ""}
1194
1232
  ${modelType.importStatement ? `${modelType.importStatement}\n` : ""}import type { RequestHandler } from './$types';
1195
1233
  // Note: ${className} is auto-registered by the Vite plugin scanner
1196
- ${generateAuthGuardHelper(objectDef, manifest)}${isTenantScoped(objectDef) ? generateTenantContextHelper() : ""}${hasPost ? generateWritablePolicyHelper(objectDef) : ""}${hasGet ? generateConditionalGetRouteHelper(objectDef.decoratorConfig?.api, {
1234
+ ${generateAuthGuardHelper(objectDef, manifest)}${needsRouteTenantContext(objectDef) ? generateTenantContextHelper(usesPrincipalContext(objectDef), isTenantScoped(objectDef)) : ""}${hasPost ? generateWritablePolicyHelper(objectDef) : ""}${hasPost ? generateTypedRouteErrorHelper() : ""}${hasGet ? generateConditionalGetRouteHelper(objectDef.decoratorConfig?.api, {
1197
1235
  tenantScoped: isTenantScoped(objectDef),
1198
1236
  permissionScoped: listUsesPermissionScopedBody,
1199
1237
  modelName: className,
@@ -1244,16 +1282,19 @@ ${routeGuardPreamble(objectDef, true)}
1244
1282
  const data = applyWritablePolicy(body);
1245
1283
 
1246
1284
  ${generateCollectionLoad(className, { typeName: modelType.typeName })}
1247
- const item = await collection.create(data);
1248
- await item.save();
1285
+ try {
1286
+ const item = await collection.create(data);
1249
1287
  ${serializers.itemSerializerName ? `
1250
- const serializedItem = await ${serializers.itemSerializerName}(item);
1251
-
1252
- return json(
1253
- applyReadPermissionRedaction(serializedItem, publicJsonOptions),
1254
- { status: 201 },
1255
- );` : `
1256
- return json(item.toPublicJSON(publicJsonOptions), { status: 201 });`}
1288
+ const serializedItem = await ${serializers.itemSerializerName}(item);
1289
+
1290
+ return json(
1291
+ applyReadPermissionRedaction(serializedItem, publicJsonOptions),
1292
+ { status: 201 },
1293
+ );` : `
1294
+ return json(item.toPublicJSON(publicJsonOptions), { status: 201 });`}
1295
+ } catch (cause) {
1296
+ return smrtRouteErrorResponse(cause);
1297
+ }
1257
1298
  };
1258
1299
  ` : "";
1259
1300
  return imports + getHandler + postHandler;
@@ -1314,8 +1355,9 @@ function generateItemRouteTemplate(projectRoot, className, objectDef, manifest,
1314
1355
 
1315
1356
  import { error${hasPut || hasDelete ? ", json" : ""} } from '@sveltejs/kit';
1316
1357
  ${serializerImports ? `${serializerImports}\n` : ""}import { getCollection } from '$lib/server/smrt';
1358
+ ${hasPut || hasDelete ? "import { normalizeTypedHttpError } from '@happyvertical/smrt-core';\n" : ""}
1317
1359
  ${modelType.importStatement ? `${modelType.importStatement}\n` : ""}import type { RequestHandler } from './$types';
1318
- ${generateAuthGuardHelper(objectDef, manifest)}${isTenantScoped(objectDef) ? generateTenantContextHelper() : ""}${hasPut ? generateWritablePolicyHelper(objectDef) : ""}${hasGet ? generateConditionalGetRouteHelper(objectDef.decoratorConfig?.api, {
1360
+ ${generateAuthGuardHelper(objectDef, manifest)}${needsRouteTenantContext(objectDef) ? generateTenantContextHelper(usesPrincipalContext(objectDef), isTenantScoped(objectDef)) : ""}${hasPut ? generateWritablePolicyHelper(objectDef) : ""}${hasPut || hasDelete ? generateTypedRouteErrorHelper() : ""}${hasGet ? generateConditionalGetRouteHelper(objectDef.decoratorConfig?.api, {
1319
1361
  tenantScoped: isTenantScoped(objectDef),
1320
1362
  permissionScoped: getUsesPermissionScopedBody,
1321
1363
  modelName: className,
@@ -1364,7 +1406,11 @@ ${generateNotFoundError(className)}
1364
1406
  const body: unknown = await request.json();
1365
1407
  const data = applyWritablePolicy(body);
1366
1408
  Object.assign(item, data);
1367
- await item.save();
1409
+ try {
1410
+ await item.save();
1411
+ } catch (cause) {
1412
+ return smrtRouteErrorResponse(cause);
1413
+ }
1368
1414
  ${serializers.itemSerializerName ? `
1369
1415
  const serializedItem = await ${serializers.itemSerializerName}(item);
1370
1416
 
@@ -1380,7 +1426,11 @@ ${generateCollectionLoad(className, { typeName: modelType.typeName })}
1380
1426
  const item = await collection.get(params.id);
1381
1427
  ${generateNotFoundError(className)}
1382
1428
 
1383
- await item.delete();
1429
+ try {
1430
+ await item.delete();
1431
+ } catch (cause) {
1432
+ return smrtRouteErrorResponse(cause);
1433
+ }
1384
1434
  return json({ success: true });
1385
1435
  };
1386
1436
  ` : "";
@@ -1399,24 +1449,27 @@ function generateActionRouteTemplate(projectRoot, routeDir, routeSpecs, objectDe
1399
1449
  if (routeSpecs.some((spec) => spec.hostType !== hostType || spec.lookupClassName !== lookupClassName || spec.routeConfig.scope !== routeConfig.scope)) throw new Error(`Cannot generate mixed custom route handlers for ${lookupClassName}. All handlers sharing a route path must target the same host type and scope.`);
1400
1450
  const importBlock = [
1401
1451
  "import { error, json } from '@sveltejs/kit';",
1402
- "import { normalizeCustomActionFailure } from '@happyvertical/smrt-core';",
1452
+ "import { normalizeCustomActionFailure, normalizeTypedHttpError } from '@happyvertical/smrt-core';",
1403
1453
  hostType === "collection" || routeConfig.scope !== "collection" ? "import { getCollection } from '$lib/server/smrt';" : "import { ObjectRegistry } from '@happyvertical/smrt-core';",
1404
1454
  typeImports,
1405
1455
  "import type { RequestHandler } from './$types';"
1406
1456
  ].filter(Boolean).join("\n");
1407
1457
  const tenantScoped = isTenantScoped(objectDef) || routeSpecs.some((spec) => !!spec.lookupObjectDef && isTenantScoped(spec.lookupObjectDef));
1408
- const handlers = routeSpecs.map((spec) => generateActionRouteHandler(spec.lookupClassName, spec.actionName, spec.actionDef, spec.routeConfig, spec.hostType, tenantScoped, lookupModelType.typeName, hostModelType.typeName)).join("\n");
1458
+ const principalContext = usesPrincipalContext(objectDef) || routeSpecs.some((spec) => !!spec.lookupObjectDef && usesPrincipalContext(spec.lookupObjectDef));
1459
+ const needsTenantContext = tenantScoped || principalContext;
1460
+ const handlers = routeSpecs.map((spec) => generateActionRouteHandler(spec.lookupClassName, spec.actionName, spec.actionDef, spec.routeConfig, spec.hostType, tenantScoped, needsTenantContext, lookupModelType.typeName, hostModelType.typeName)).join("\n");
1409
1461
  return `${AUTO_GENERATED_ROUTE_HEADER}
1410
1462
  // DO NOT EDIT - changes will be overwritten
1411
1463
 
1412
1464
  ${importBlock}
1413
- ${generateAuthGuardHelper(objectDef, manifest)}${tenantScoped ? generateTenantContextHelper() : ""}
1465
+ ${generateAuthGuardHelper(objectDef, manifest)}${needsTenantContext ? generateTenantContextHelper(principalContext, tenantScoped) : ""}
1466
+ ${generateTypedRouteErrorHelper()}
1414
1467
  ${handlers}`;
1415
1468
  }
1416
- function generateActionRouteHandler(lookupClassName, actionName, actionDef, routeConfig, hostType, tenantScoped, lookupTypeName, hostTypeName) {
1469
+ function generateActionRouteHandler(lookupClassName, actionName, actionDef, routeConfig, hostType, tenantScoped, needsTenantContext, lookupTypeName, hostTypeName) {
1417
1470
  const handlerName = routeConfig.method;
1418
1471
  const guardLines = [` requireRouteAuth(locals, ${routeConfig.method !== "GET"});`];
1419
- if (tenantScoped) guardLines.push(" establishTenantContext(locals);");
1472
+ if (needsTenantContext) guardLines.push(" establishTenantContext(locals);");
1420
1473
  const authGuardLine = guardLines.join("\n");
1421
1474
  const needsRequest = actionDef.parameters.length > 0;
1422
1475
  const collectionHandlerArgs = buildRouteHandlerArgs(routeConfig.pathParamNames.length > 0, needsRequest);
@@ -1436,7 +1489,12 @@ ${generateCollectionLoad(lookupClassName, { typeName: lookupTypeName })}
1436
1489
  ${generateCollectionNotRegisteredError(lookupClassName)}
1437
1490
  ${staticTargetLoad}
1438
1491
 
1439
- ${optionsLoad}${scopedOptions.source} const result = ${buildActionInvocationExpression(receiverExpression, actionName, invocationArgs)};
1492
+ ${optionsLoad}${scopedOptions.source} let result: unknown;
1493
+ try {
1494
+ result = ${buildActionInvocationExpression(receiverExpression, actionName, invocationArgs)};
1495
+ } catch (cause) {
1496
+ return smrtRouteErrorResponse(cause);
1497
+ }
1440
1498
 
1441
1499
  const failure = normalizeCustomActionFailure(result);
1442
1500
  if (failure) {
@@ -1462,7 +1520,12 @@ ${authGuardLine}
1462
1520
  ${generateClassNotRegisteredError(lookupClassName)}
1463
1521
 
1464
1522
  ${optionsLoad}${scopedOptions.source} const ClassRef = registered.constructor as typeof ${hostTypeName};
1465
- const result = ${buildActionInvocationExpression("ClassRef", actionName, invocationArgs)};
1523
+ let result: unknown;
1524
+ try {
1525
+ result = ${buildActionInvocationExpression("ClassRef", actionName, invocationArgs)};
1526
+ } catch (cause) {
1527
+ return smrtRouteErrorResponse(cause);
1528
+ }
1466
1529
 
1467
1530
  const failure = normalizeCustomActionFailure(result);
1468
1531
  if (failure) {
@@ -1490,7 +1553,12 @@ ${generateCollectionLoad(lookupClassName, { typeName: lookupTypeName })}
1490
1553
  ${scopedItemLoad}
1491
1554
  ${generateNotFoundError(lookupClassName)}
1492
1555
 
1493
- ${optionsLoad} const result = ${buildActionInvocationExpression("item", actionName, invocationArgs)};
1556
+ ${optionsLoad} let result: unknown;
1557
+ try {
1558
+ result = ${buildActionInvocationExpression("item", actionName, invocationArgs)};
1559
+ } catch (cause) {
1560
+ return smrtRouteErrorResponse(cause);
1561
+ }
1494
1562
 
1495
1563
  const failure = normalizeCustomActionFailure(result);
1496
1564
  if (failure) {