@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.
- package/AGENTS.md +17 -6
- package/dist/collection.d.ts +7 -0
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +2 -0
- package/dist/collection.js.map +1 -1
- package/dist/consumer-plugin/index.js +26 -3
- package/dist/consumer-plugin/index.js.map +1 -1
- package/dist/dispatch/index.d.ts +1 -1
- package/dist/dispatch/index.d.ts.map +1 -1
- package/dist/dispatch/index.js +2 -2
- package/dist/dispatch/tenant-resolver.d.ts +23 -0
- package/dist/dispatch/tenant-resolver.d.ts.map +1 -1
- package/dist/dispatch/tenant-resolver.js +33 -1
- package/dist/dispatch/tenant-resolver.js.map +1 -1
- package/dist/generators/conditional-get.d.ts +120 -0
- package/dist/generators/conditional-get.d.ts.map +1 -0
- package/dist/generators/conditional-get.js +217 -0
- package/dist/generators/conditional-get.js.map +1 -0
- package/dist/generators/index.d.ts +1 -0
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +2 -1
- package/dist/generators/rest.d.ts +52 -0
- package/dist/generators/rest.d.ts.map +1 -1
- package/dist/generators/rest.js +146 -10
- package/dist/generators/rest.js.map +1 -1
- package/dist/generators.js +2 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/manifest/manifest-loader.d.ts +10 -1
- package/dist/manifest/manifest-loader.d.ts.map +1 -1
- package/dist/manifest/manifest-loader.js +18 -5
- package/dist/manifest/manifest-loader.js.map +1 -1
- package/dist/manifest/static-manifest.d.ts.map +1 -1
- package/dist/manifest/static-manifest.js +10 -2
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest/store.js.map +1 -1
- package/dist/manifest/test-manifest-stub.d.ts.map +1 -1
- package/dist/manifest/test-manifest-stub.js +2108 -223
- package/dist/manifest/test-manifest-stub.js.map +1 -1
- package/dist/manifest.json +10 -2
- package/dist/object.d.ts +19 -0
- package/dist/object.d.ts.map +1 -1
- package/dist/object.js +24 -2
- package/dist/object.js.map +1 -1
- package/dist/registry/index.d.ts +1 -1
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/shared-state.d.ts +8 -1
- package/dist/registry/shared-state.d.ts.map +1 -1
- package/dist/registry/shared-state.js +11 -3
- package/dist/registry/shared-state.js.map +1 -1
- package/dist/registry/types.d.ts +34 -0
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +7 -6
- package/dist/sync/apply.d.ts +234 -0
- package/dist/sync/apply.d.ts.map +1 -0
- package/dist/sync/apply.js +378 -0
- package/dist/sync/apply.js.map +1 -0
- package/dist/utils/stack-frames.d.ts +50 -0
- package/dist/utils/stack-frames.d.ts.map +1 -0
- package/dist/utils/stack-frames.js +64 -0
- package/dist/utils/stack-frames.js.map +1 -0
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +60 -27
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/sync-apply-route.d.ts +40 -0
- package/dist/vite-plugin/sync-apply-route.d.ts.map +1 -0
- package/dist/vite-plugin/sync-apply-route.js +240 -0
- package/dist/vite-plugin/sync-apply-route.js.map +1 -0
- 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
|
|
273
|
-
* (#1540
|
|
274
|
-
* after the auth guard so every
|
|
275
|
-
* derived from the authenticated principal — engaging
|
|
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
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
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
|
-
|
|
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
|
|
1034
|
+
return conditionalJson(request, { items: serializedItems, count, limit, offset });` : `
|
|
1009
1035
|
const items_public = items.map((item) => item.toPublicJSON());
|
|
1010
|
-
return
|
|
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
|
-
|
|
1124
|
+
${getForRead}
|
|
1092
1125
|
${generateNotFoundError(className)}
|
|
1093
1126
|
${serializers.itemSerializerName ? `
|
|
1094
1127
|
const serializedItem = await ${serializers.itemSerializerName}(item);
|
|
1095
1128
|
|
|
1096
|
-
return
|
|
1097
|
-
return
|
|
1129
|
+
return conditionalJson(request, serializedItem);` : `
|
|
1130
|
+
return conditionalJson(request, item.toPublicJSON());`}
|
|
1098
1131
|
};
|
|
1099
1132
|
` : "";
|
|
1100
1133
|
const putHandler = hasPut ? `
|