@happyvertical/smrt-core 0.40.60 → 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.
- package/dist/generated-client-runtime.d.ts +6 -5
- package/dist/generated-client-runtime.d.ts.map +1 -1
- package/dist/generated-client-runtime.js +49 -14
- package/dist/generated-client-runtime.js.map +1 -1
- package/dist/generators/index.d.ts +3 -2
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +3 -2
- package/dist/generators/mcp-runtime-template.d.ts +5 -0
- package/dist/generators/mcp-runtime-template.d.ts.map +1 -1
- package/dist/generators/mcp-runtime-template.js +9 -2
- package/dist/generators/mcp-runtime-template.js.map +1 -1
- package/dist/generators/mcp.d.ts +47 -0
- package/dist/generators/mcp.d.ts.map +1 -1
- package/dist/generators/mcp.js +64 -11
- package/dist/generators/mcp.js.map +1 -1
- package/dist/generators/rest.d.ts.map +1 -1
- package/dist/generators/rest.js +3 -0
- package/dist/generators/rest.js.map +1 -1
- package/dist/generators/typed-http-error.d.ts +16 -0
- package/dist/generators/typed-http-error.d.ts.map +1 -0
- package/dist/generators/typed-http-error.js +17 -0
- package/dist/generators/typed-http-error.js.map +1 -0
- package/dist/generators.js +3 -2
- package/dist/index.js +3 -2
- package/dist/manifest/static-manifest.js +1 -1
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest.json +1 -1
- package/dist/prebuild/index.d.ts.map +1 -1
- package/dist/prebuild/index.js +13 -1
- package/dist/prebuild/index.js.map +1 -1
- package/dist/registry/types.d.ts +15 -0
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +3 -3
- package/dist/vite-plugin/index.d.ts +2 -0
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +24 -11
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +94 -26
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/web-collections.d.ts +1 -0
- package/dist/vite-plugin/web-collections.d.ts.map +1 -1
- package/dist/vite-plugin/web-collections.js +17 -2
- package/dist/vite-plugin/web-collections.js.map +1 -1
- package/package.json +5 -5
- package/dist/manifest/test-manifest-loader.d.ts +0 -3
- package/dist/manifest/test-manifest-loader.d.ts.map +0 -1
- package/dist/manifest/test-manifest-stub.d.ts +0 -4
- package/dist/manifest/test-manifest-stub.d.ts.map +0 -1
- package/dist/manifest/test-manifest-stub.js +0 -77056
- 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(
|
|
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 (
|
|
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)}${
|
|
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
|
-
|
|
1248
|
-
|
|
1285
|
+
try {
|
|
1286
|
+
const item = await collection.create(data);
|
|
1249
1287
|
${serializers.itemSerializerName ? `
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
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)}${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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)}${
|
|
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 (
|
|
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}
|
|
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
|
-
|
|
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}
|
|
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) {
|