@happyvertical/smrt-core 0.37.7 → 0.37.9
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 +25 -6
- package/dist/change-feed.d.ts +261 -0
- package/dist/change-feed.d.ts.map +1 -0
- package/dist/change-feed.js +482 -0
- package/dist/change-feed.js.map +1 -0
- package/dist/class.d.ts.map +1 -1
- package/dist/class.js +4 -2
- package/dist/class.js.map +1 -1
- package/dist/collection.js +2 -2
- package/dist/consumer-plugin/index.d.ts.map +1 -1
- package/dist/consumer-plugin/index.js +24 -19
- package/dist/consumer-plugin/index.js.map +1 -1
- package/dist/dispatch/bus.js +2 -2
- 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/generated-client-runtime.d.ts +26 -0
- package/dist/generated-client-runtime.d.ts.map +1 -0
- package/dist/generated-client-runtime.js +88 -0
- package/dist/generated-client-runtime.js.map +1 -0
- package/dist/generators/changes-route.d.ts +52 -0
- package/dist/generators/changes-route.d.ts.map +1 -0
- package/dist/generators/changes-route.js +111 -0
- package/dist/generators/changes-route.js.map +1 -0
- package/dist/generators/conditional-get.d.ts +12 -0
- package/dist/generators/conditional-get.d.ts.map +1 -1
- package/dist/generators/conditional-get.js +30 -2
- package/dist/generators/conditional-get.js.map +1 -1
- package/dist/generators/rest.d.ts +58 -0
- package/dist/generators/rest.d.ts.map +1 -1
- package/dist/generators/rest.js +114 -21
- package/dist/generators/rest.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -5
- 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.js +2 -2
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest/test-manifest-stub.d.ts.map +1 -1
- package/dist/manifest/test-manifest-stub.js +880 -105
- package/dist/manifest/test-manifest-stub.js.map +1 -1
- package/dist/manifest.json +2 -2
- package/dist/object.js +2 -2
- package/dist/prebuild/index.d.ts.map +1 -1
- package/dist/prebuild/index.js +14 -7
- package/dist/prebuild/index.js.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/smrt-knowledge.json +6 -6
- package/dist/system/schema.d.ts +24 -1
- package/dist/system/schema.d.ts.map +1 -1
- package/dist/system/schema.js +45 -3
- package/dist/system/schema.js.map +1 -1
- package/dist/testing/database.js +1 -1
- 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/changes-route.d.ts +10 -0
- package/dist/vite-plugin/changes-route.d.ts.map +1 -0
- package/dist/vite-plugin/changes-route.js +174 -0
- package/dist/vite-plugin/changes-route.js.map +1 -0
- package/dist/vite-plugin/index.d.ts +7 -0
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +38 -20
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/route-header.d.ts +10 -0
- package/dist/vite-plugin/route-header.d.ts.map +1 -0
- package/dist/vite-plugin/route-header.js +14 -0
- package/dist/vite-plugin/route-header.js.map +1 -0
- package/dist/vite-plugin/sveltekit-generator.d.ts +7 -0
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +46 -20
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/sync-apply-route.d.ts.map +1 -1
- package/dist/vite-plugin/sync-apply-route.js +2 -2
- package/dist/vite-plugin/sync-apply-route.js.map +1 -1
- package/package.json +4 -4
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { generateConditionalGetRouteHelper } from "../generators/conditional-get.js";
|
|
2
|
+
import { AUTO_GENERATED_ROUTE_HEADER } from "./route-header.js";
|
|
2
3
|
import { generateSyncApplyRoute } from "./sync-apply-route.js";
|
|
4
|
+
import { generateChangesRoute } from "./changes-route.js";
|
|
3
5
|
import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
6
|
import { join, relative } from "node:path";
|
|
5
7
|
//#region src/vite-plugin/sveltekit-generator.ts
|
|
@@ -8,7 +10,6 @@ import { join, relative } from "node:path";
|
|
|
8
10
|
* Generates real TypeScript files in src/routes/api/ based on discovered SMRT objects
|
|
9
11
|
*/
|
|
10
12
|
var BIOME_LINE_WIDTH = 80;
|
|
11
|
-
var AUTO_GENERATED_ROUTE_HEADER = "// Auto-generated by @smrt/core vite plugin";
|
|
12
13
|
var STANDARD_API_ACTIONS = [
|
|
13
14
|
"list",
|
|
14
15
|
"get",
|
|
@@ -271,24 +272,28 @@ function isTenantScoped(objectDef) {
|
|
|
271
272
|
return !!objectDef.decoratorConfig?.tenantScoped;
|
|
272
273
|
}
|
|
273
274
|
/**
|
|
274
|
-
* Emit the tenant-context establishment
|
|
275
|
-
* (#1540
|
|
276
|
-
* after the auth guard so every
|
|
277
|
-
* derived from the authenticated principal — engaging
|
|
278
|
-
* interceptors that auto-filter by tenant.
|
|
275
|
+
* Emit the tenant-context establishment + fail-closed read-scope helpers for
|
|
276
|
+
* `@TenantScoped` objects (#1540 facet 2; #1782). Generated handlers call
|
|
277
|
+
* `establishTenantContext(locals)` after the auth guard so every query runs
|
|
278
|
+
* inside the tenant context derived from the authenticated principal — engaging
|
|
279
|
+
* the `@TenantScoped` interceptors that auto-filter by tenant.
|
|
279
280
|
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
281
|
+
* `establishTenantContext` reads the canonical `locals.tenantId` set by the
|
|
282
|
+
* smrt-users auth hook (with `user`/`session` fallbacks). It is a no-op when a
|
|
283
|
+
* context is already active (e.g. an upstream tenancy handle) or when the
|
|
284
|
+
* request carries no tenant (anonymous / global principal).
|
|
285
|
+
*
|
|
286
|
+
* When no context could be established, an `optional`-mode read would otherwise
|
|
287
|
+
* fall through the interceptor UNFILTERED and return rows from ALL tenants (the
|
|
288
|
+
* interceptor only hard-fails `required` mode) — the #1782 leak. So read
|
|
289
|
+
* handlers additionally call `tenantReadScope()`, which restricts reads to
|
|
290
|
+
* NULL-tenant (global) rows only when tenancy is enabled but no context is
|
|
291
|
+
* active. Both helpers are imported only for tenant-scoped routes, which already
|
|
292
|
+
* depend on `@happyvertical/smrt-tenancy`.
|
|
288
293
|
*/
|
|
289
294
|
function generateTenantContextHelper() {
|
|
290
295
|
return `
|
|
291
|
-
import { enterTenantContext, hasTenantContext } from '@happyvertical/smrt-tenancy';
|
|
296
|
+
import { enterTenantContext, hasTenantContext, isTenancyEnabled } from '@happyvertical/smrt-tenancy';
|
|
292
297
|
|
|
293
298
|
function establishTenantContext(locals: unknown): void {
|
|
294
299
|
if (hasTenantContext()) return;
|
|
@@ -301,6 +306,19 @@ function establishTenantContext(locals: unknown): void {
|
|
|
301
306
|
enterTenantContext({ tenantId });
|
|
302
307
|
}
|
|
303
308
|
}
|
|
309
|
+
|
|
310
|
+
// Fail-closed read scope (#1782): a public/anonymous read on a @TenantScoped
|
|
311
|
+
// model has no tenant context, so the tenancy interceptor (optional mode) would
|
|
312
|
+
// pass the query through UNFILTERED and return every tenant's rows. When tenancy
|
|
313
|
+
// is enabled but no context was established, restrict reads to NULL-tenant
|
|
314
|
+
// (global) rows only — mirroring the dispatch resolver + _changes convention:
|
|
315
|
+
// tenancy enforced with no context => global rows only. Returns undefined when a
|
|
316
|
+
// context is active (the interceptor filters by it) or tenancy is disabled.
|
|
317
|
+
function tenantReadScope(): { tenantId: null } | undefined {
|
|
318
|
+
return isTenancyEnabled() && !hasTenantContext()
|
|
319
|
+
? { tenantId: null }
|
|
320
|
+
: undefined;
|
|
321
|
+
}
|
|
304
322
|
`;
|
|
305
323
|
}
|
|
306
324
|
/**
|
|
@@ -479,6 +497,7 @@ async function generateSvelteKitRoutes(projectRoot, manifest, options) {
|
|
|
479
497
|
}
|
|
480
498
|
if (options.knowledge?.api?.enabled) generateKnowledgeRoute(projectRoot, options);
|
|
481
499
|
generateSyncApplyRoute(projectRoot, manifest, options);
|
|
500
|
+
generateChangesRoute(projectRoot, manifest, options);
|
|
482
501
|
updateGitignore(projectRoot, options);
|
|
483
502
|
const skippedMsg = skippedCollections > 0 ? ` (skipped ${skippedCollections} collection classes)` : "";
|
|
484
503
|
console.log(`[smrt] Generated routes for ${generatedCount} SMRT objects${skippedMsg}`);
|
|
@@ -492,7 +511,7 @@ function clearGeneratedRouteFiles(routesRoot) {
|
|
|
492
511
|
continue;
|
|
493
512
|
}
|
|
494
513
|
if (!entry.isFile() || entry.name !== "+server.ts") continue;
|
|
495
|
-
if (readFileSync(entryPath, "utf-8").startsWith(
|
|
514
|
+
if (readFileSync(entryPath, "utf-8").startsWith("// Auto-generated by @smrt/core vite plugin")) unlinkSync(entryPath);
|
|
496
515
|
}
|
|
497
516
|
}
|
|
498
517
|
function knowledgeRouteDir(projectRoot, options) {
|
|
@@ -509,7 +528,7 @@ function svelteKitRouteRoot(routesDir) {
|
|
|
509
528
|
function clearGeneratedKnowledgeRoute(projectRoot, options) {
|
|
510
529
|
const routePath = join(knowledgeRouteDir(projectRoot, options), "+server.ts");
|
|
511
530
|
if (!existsSync(routePath)) return;
|
|
512
|
-
if (readFileSync(routePath, "utf-8").startsWith(
|
|
531
|
+
if (readFileSync(routePath, "utf-8").startsWith("// Auto-generated by @smrt/core vite plugin")) unlinkSync(routePath);
|
|
513
532
|
}
|
|
514
533
|
function generateKnowledgeRoute(projectRoot, options) {
|
|
515
534
|
writeRoute(knowledgeRouteDir(projectRoot, options), "+server.ts", generateKnowledgeRouteTemplate(options.knowledge ?? {}, readKnowledgeRouteArtifact(projectRoot)));
|
|
@@ -996,6 +1015,10 @@ ${generateAuthGuardHelper(objectDef)}${isTenantScoped(objectDef) ? generateTenan
|
|
|
996
1015
|
tenantScoped: isTenantScoped(objectDef),
|
|
997
1016
|
modelName: className
|
|
998
1017
|
}) : ""}`;
|
|
1018
|
+
const listAndCount = isTenantScoped(objectDef) ? ` const readScope = tenantReadScope();
|
|
1019
|
+
const items = await collection.list({ limit, offset, where: readScope });
|
|
1020
|
+
const count = await collection.count({ where: readScope });` : ` const items = await collection.list({ limit, offset });
|
|
1021
|
+
const count = await collection.count();`;
|
|
999
1022
|
const getHandler = hasGet ? `
|
|
1000
1023
|
// List all ${className.toLowerCase()}s
|
|
1001
1024
|
export const GET: RequestHandler = async ({ locals, url, request }) => {
|
|
@@ -1004,8 +1027,7 @@ ${routeGuardPreamble(objectDef, false)}
|
|
|
1004
1027
|
const offset = Number(url.searchParams.get('offset')) || 0;
|
|
1005
1028
|
|
|
1006
1029
|
${generateCollectionLoad(className, { typeName: modelType.typeName })}
|
|
1007
|
-
|
|
1008
|
-
const count = await collection.count();
|
|
1030
|
+
${listAndCount}
|
|
1009
1031
|
${serializers.listItemSerializerName ? `
|
|
1010
1032
|
const serializedItems = await Promise.all(
|
|
1011
1033
|
items.map((item) => ${serializers.listItemSerializerName}(item)),
|
|
@@ -1092,12 +1114,16 @@ ${generateAuthGuardHelper(objectDef)}${isTenantScoped(objectDef) ? generateTenan
|
|
|
1092
1114
|
tenantScoped: isTenantScoped(objectDef),
|
|
1093
1115
|
modelName: className
|
|
1094
1116
|
}) : ""}`;
|
|
1117
|
+
const getForRead = isTenantScoped(objectDef) ? ` const readScope = tenantReadScope();
|
|
1118
|
+
const item = await collection.get(
|
|
1119
|
+
readScope ? { id: params.id, ...readScope } : params.id,
|
|
1120
|
+
);` : ` const item = await collection.get(params.id);`;
|
|
1095
1121
|
const getHandler = hasGet ? `
|
|
1096
1122
|
// Get single ${simpleClassName.toLowerCase()}
|
|
1097
1123
|
export const GET: RequestHandler = async ({ locals, params, request }) => {
|
|
1098
1124
|
${routeGuardPreamble(objectDef, false)}
|
|
1099
1125
|
${generateCollectionLoad(className, { typeName: modelType.typeName })}
|
|
1100
|
-
|
|
1126
|
+
${getForRead}
|
|
1101
1127
|
${generateNotFoundError(className)}
|
|
1102
1128
|
${serializers.itemSerializerName ? `
|
|
1103
1129
|
const serializedItem = await ${serializers.itemSerializerName}(item);
|