@happyvertical/smrt-tenancy 0.55.7 → 0.55.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/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +8 -1
- package/dist/smrt-knowledge.json +11 -4
- package/package.json +8 -8
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { n as createExpressMiddleware, r as createCliContext, t as createSvelteK
|
|
|
3
3
|
import { C as unregisterTenantScopedClass, S as registerTenantScopedConstructor, _ as clearTenantScopedRegistry, a as setupTestTenancy, b as isTenantScopedClass, c as disableTenancy, d as queryGlobal, f as queryWithGlobals, g as auditTenantScopedRegistrations, h as isTenancyEnabled, i as resetTenancy, l as enableTenancy, m as runTenantScopedEntryPoint, n as assertTenantIsolationViolation, o as testTenantIsolation, p as withTenantGlobalRead, r as createTestTenantContext, s as createTenantInterceptor, t as assertTenantContextRequired, u as assertTenantReadAllowed, v as getAllTenantScopedClasses, x as registerTenantScopedClass, y as getTenantScopedConfig } from "./chunks/testing-Cs_C_Q-y.js";
|
|
4
4
|
import { ObjectRegistry, SmrtCollection, SmrtObject, applyPendingDecoratorRegistrations, crossPackageRef, field, isEmbeddedDatabase, isPostgresDatabase, registerCompatibleFieldDecorator, smrt, withEmbeddedWriteTransaction } from "@happyvertical/smrt-core";
|
|
5
5
|
//#region src/__smrt-register__.ts
|
|
6
|
-
ObjectRegistry.registerPackageManifest(JSON.parse(`{"version":"1.0.0","timestamp":0,"packageName":"@happyvertical/smrt-tenancy","packageVersion":"0.55.
|
|
6
|
+
ObjectRegistry.registerPackageManifest(JSON.parse(`{"version":"1.0.0","timestamp":0,"packageName":"@happyvertical/smrt-tenancy","packageVersion":"0.55.8","objects":{"@happyvertical/smrt-tenancy:BillingRelationship":{"name":"billingrelationship","className":"BillingRelationship","qualifiedName":"@happyvertical/smrt-tenancy:BillingRelationship","collection":"billingrelationships","filePath":"/home/runner/work/smrt/smrt/packages/tenancy/src/billing-relationship.ts","packageName":"@happyvertical/smrt-tenancy","fields":{"childTenantId":{"type":"crossPackageRef","required":true,"related":"@happyvertical/smrt-users:Tenant","_meta":{"required":true}},"resellerTenantId":{"type":"crossPackageRef","required":true,"related":"@happyvertical/smrt-users:Tenant","_meta":{"required":true}},"billingOwnerMode":{"type":"text","required":false,"default":"self","enum":["self","reseller"]}},"methods":{},"decoratorConfig":{"tableName":"_smrt_billing_relationships","conflictColumns":["child_tenant_id"],"api":false,"cli":false,"mcp":false},"extends":"SmrtObject","exportName":"BillingRelationship","collectionExportName":"BillingRelationshipCollection","validationRules":[{"field":"childTenantId","rule":"required","fieldType":"crossPackageRef"},{"field":"resellerTenantId","rule":"required","fieldType":"crossPackageRef"}],"schema":{"tableName":"_smrt_billing_relationships","ddl":"CREATE TABLE IF NOT EXISTS \\"_smrt_billing_relationships\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"child_tenant_id\\" UUID NOT NULL,\\n \\"reseller_tenant_id\\" UUID NOT NULL,\\n \\"billing_owner_mode\\" TEXT DEFAULT 'self'\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"child_tenant_id":{"type":"UUID","referenceKind":"crossPackageRef","notNull":true,"unique":false},"reseller_tenant_id":{"type":"UUID","referenceKind":"crossPackageRef","notNull":true,"unique":false},"billing_owner_mode":{"type":"TEXT","notNull":false,"unique":false,"default":"self"}},"indexes":[{"name":"_smrt_billing_relationships_child_tenant_id_idx","columns":["child_tenant_id"],"unique":true},{"name":"_smrt_billing_relationships_slug_context_idx","columns":["slug","context"]},{"name":"_smrt_billing_relationships_created_at_idx","columns":["created_at"]},{"name":"_smrt_billing_relationships_reseller_tenant_id_idx","columns":["reseller_tenant_id"]}],"version":"4d7f410a"},"importPath":"@happyvertical/smrt-tenancy"},"@happyvertical/smrt-tenancy:BillingRelationshipCollection":{"name":"billingrelationshipcollection","className":"BillingRelationshipCollection","qualifiedName":"@happyvertical/smrt-tenancy:BillingRelationshipCollection","collection":"billingrelationships","filePath":"/home/runner/work/smrt/smrt/packages/tenancy/src/billing-relationship.ts","packageName":"@happyvertical/smrt-tenancy","fields":{},"methods":{},"decoratorConfig":{"tableName":"_smrt_billing_relationships","api":false,"mcp":false,"cli":false},"extends":"SmrtCollection","extendsTypeArg":"BillingRelationship","exportName":"BillingRelationshipCollection","collectionExportName":"BillingRelationshipCollectionCollection","schema":{"tableName":"_smrt_billing_relationships","ddl":"CREATE TABLE IF NOT EXISTS \\"_smrt_billing_relationships\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"child_tenant_id\\" UUID NOT NULL,\\n \\"reseller_tenant_id\\" UUID NOT NULL,\\n \\"billing_owner_mode\\" TEXT DEFAULT 'self'\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"child_tenant_id":{"type":"UUID","referenceKind":"crossPackageRef","notNull":true,"unique":false},"reseller_tenant_id":{"type":"UUID","referenceKind":"crossPackageRef","notNull":true,"unique":false},"billing_owner_mode":{"type":"TEXT","notNull":false,"unique":false,"default":"self"}},"indexes":[{"name":"_smrt_billing_relationships_child_tenant_id_idx","columns":["child_tenant_id"],"unique":true},{"name":"_smrt_billing_relationships_slug_context_idx","columns":["slug","context"]},{"name":"_smrt_billing_relationships_created_at_idx","columns":["created_at"]},{"name":"_smrt_billing_relationships_reseller_tenant_id_idx","columns":["reseller_tenant_id"]}],"version":"4d7f410a"},"importPath":"@happyvertical/smrt-tenancy"}},"moduleType":"smrt","smrtDependencies":["@happyvertical/smrt-core"],"browser":{"status":"server-only","issues":["#3624"],"reason":"node:async_hooks"},"stampsConstructors":true}`));
|
|
7
7
|
//#endregion
|
|
8
8
|
//#region src/billing-relationship.ts
|
|
9
9
|
var __defProp = Object.defineProperty;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/__smrt-register__.ts","../src/billing-relationship.ts","../src/decorators.ts","../src/fields.ts"],"sourcesContent":["// Generated by @happyvertical/smrt-core smrtPlugin at library build time.\n// The package manifest is inlined so self-registration cannot miss when a\n// downstream bundler relocates this module away from dist/manifest.json\n// (https://github.com/happyvertical/smrt/issues/1506,\n// https://github.com/happyvertical/smrt/issues/1507).\nimport { ObjectRegistry } from '@happyvertical/smrt-core';\n\nObjectRegistry.registerPackageManifest(JSON.parse(\"{\\\"version\\\":\\\"1.0.0\\\",\\\"timestamp\\\":0,\\\"packageName\\\":\\\"@happyvertical/smrt-tenancy\\\",\\\"packageVersion\\\":\\\"0.55.7\\\",\\\"objects\\\":{\\\"@happyvertical/smrt-tenancy:BillingRelationship\\\":{\\\"name\\\":\\\"billingrelationship\\\",\\\"className\\\":\\\"BillingRelationship\\\",\\\"qualifiedName\\\":\\\"@happyvertical/smrt-tenancy:BillingRelationship\\\",\\\"collection\\\":\\\"billingrelationships\\\",\\\"filePath\\\":\\\"/home/runner/work/smrt/smrt/packages/tenancy/src/billing-relationship.ts\\\",\\\"packageName\\\":\\\"@happyvertical/smrt-tenancy\\\",\\\"fields\\\":{\\\"childTenantId\\\":{\\\"type\\\":\\\"crossPackageRef\\\",\\\"required\\\":true,\\\"related\\\":\\\"@happyvertical/smrt-users:Tenant\\\",\\\"_meta\\\":{\\\"required\\\":true}},\\\"resellerTenantId\\\":{\\\"type\\\":\\\"crossPackageRef\\\",\\\"required\\\":true,\\\"related\\\":\\\"@happyvertical/smrt-users:Tenant\\\",\\\"_meta\\\":{\\\"required\\\":true}},\\\"billingOwnerMode\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":false,\\\"default\\\":\\\"self\\\",\\\"enum\\\":[\\\"self\\\",\\\"reseller\\\"]}},\\\"methods\\\":{},\\\"decoratorConfig\\\":{\\\"tableName\\\":\\\"_smrt_billing_relationships\\\",\\\"conflictColumns\\\":[\\\"child_tenant_id\\\"],\\\"api\\\":false,\\\"cli\\\":false,\\\"mcp\\\":false},\\\"extends\\\":\\\"SmrtObject\\\",\\\"exportName\\\":\\\"BillingRelationship\\\",\\\"collectionExportName\\\":\\\"BillingRelationshipCollection\\\",\\\"validationRules\\\":[{\\\"field\\\":\\\"childTenantId\\\",\\\"rule\\\":\\\"required\\\",\\\"fieldType\\\":\\\"crossPackageRef\\\"},{\\\"field\\\":\\\"resellerTenantId\\\",\\\"rule\\\":\\\"required\\\",\\\"fieldType\\\":\\\"crossPackageRef\\\"}],\\\"schema\\\":{\\\"tableName\\\":\\\"_smrt_billing_relationships\\\",\\\"ddl\\\":\\\"CREATE TABLE IF NOT EXISTS \\\\\\\"_smrt_billing_relationships\\\\\\\" (\\\\n \\\\\\\"id\\\\\\\" UUID PRIMARY KEY NOT NULL,\\\\n \\\\\\\"slug\\\\\\\" TEXT NOT NULL,\\\\n \\\\\\\"context\\\\\\\" TEXT NOT NULL DEFAULT '',\\\\n \\\\\\\"created_at\\\\\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\\\n \\\\\\\"updated_at\\\\\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\\\n \\\\\\\"child_tenant_id\\\\\\\" UUID NOT NULL,\\\\n \\\\\\\"reseller_tenant_id\\\\\\\" UUID NOT NULL,\\\\n \\\\\\\"billing_owner_mode\\\\\\\" TEXT DEFAULT 'self'\\\\n);\\\",\\\"columns\\\":{\\\"id\\\":{\\\"type\\\":\\\"UUID\\\",\\\"primaryKey\\\":true,\\\"referenceKind\\\":\\\"id\\\",\\\"notNull\\\":true},\\\"slug\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true},\\\"context\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"\\\"},\\\"created_at\\\":{\\\"type\\\":\\\"TIMESTAMP\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"current_timestamp\\\"},\\\"updated_at\\\":{\\\"type\\\":\\\"TIMESTAMP\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"current_timestamp\\\"},\\\"child_tenant_id\\\":{\\\"type\\\":\\\"UUID\\\",\\\"referenceKind\\\":\\\"crossPackageRef\\\",\\\"notNull\\\":true,\\\"unique\\\":false},\\\"reseller_tenant_id\\\":{\\\"type\\\":\\\"UUID\\\",\\\"referenceKind\\\":\\\"crossPackageRef\\\",\\\"notNull\\\":true,\\\"unique\\\":false},\\\"billing_owner_mode\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false,\\\"default\\\":\\\"self\\\"}},\\\"indexes\\\":[{\\\"name\\\":\\\"_smrt_billing_relationships_child_tenant_id_idx\\\",\\\"columns\\\":[\\\"child_tenant_id\\\"],\\\"unique\\\":true},{\\\"name\\\":\\\"_smrt_billing_relationships_slug_context_idx\\\",\\\"columns\\\":[\\\"slug\\\",\\\"context\\\"]},{\\\"name\\\":\\\"_smrt_billing_relationships_created_at_idx\\\",\\\"columns\\\":[\\\"created_at\\\"]},{\\\"name\\\":\\\"_smrt_billing_relationships_reseller_tenant_id_idx\\\",\\\"columns\\\":[\\\"reseller_tenant_id\\\"]}],\\\"version\\\":\\\"4d7f410a\\\"},\\\"importPath\\\":\\\"@happyvertical/smrt-tenancy\\\"},\\\"@happyvertical/smrt-tenancy:BillingRelationshipCollection\\\":{\\\"name\\\":\\\"billingrelationshipcollection\\\",\\\"className\\\":\\\"BillingRelationshipCollection\\\",\\\"qualifiedName\\\":\\\"@happyvertical/smrt-tenancy:BillingRelationshipCollection\\\",\\\"collection\\\":\\\"billingrelationships\\\",\\\"filePath\\\":\\\"/home/runner/work/smrt/smrt/packages/tenancy/src/billing-relationship.ts\\\",\\\"packageName\\\":\\\"@happyvertical/smrt-tenancy\\\",\\\"fields\\\":{},\\\"methods\\\":{},\\\"decoratorConfig\\\":{\\\"tableName\\\":\\\"_smrt_billing_relationships\\\",\\\"api\\\":false,\\\"mcp\\\":false,\\\"cli\\\":false},\\\"extends\\\":\\\"SmrtCollection\\\",\\\"extendsTypeArg\\\":\\\"BillingRelationship\\\",\\\"exportName\\\":\\\"BillingRelationshipCollection\\\",\\\"collectionExportName\\\":\\\"BillingRelationshipCollectionCollection\\\",\\\"schema\\\":{\\\"tableName\\\":\\\"_smrt_billing_relationships\\\",\\\"ddl\\\":\\\"CREATE TABLE IF NOT EXISTS \\\\\\\"_smrt_billing_relationships\\\\\\\" (\\\\n \\\\\\\"id\\\\\\\" UUID PRIMARY KEY NOT NULL,\\\\n \\\\\\\"slug\\\\\\\" TEXT NOT NULL,\\\\n \\\\\\\"context\\\\\\\" TEXT NOT NULL DEFAULT '',\\\\n \\\\\\\"created_at\\\\\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\\\n \\\\\\\"updated_at\\\\\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\\\n \\\\\\\"child_tenant_id\\\\\\\" UUID NOT NULL,\\\\n \\\\\\\"reseller_tenant_id\\\\\\\" UUID NOT NULL,\\\\n \\\\\\\"billing_owner_mode\\\\\\\" TEXT DEFAULT 'self'\\\\n);\\\",\\\"columns\\\":{\\\"id\\\":{\\\"type\\\":\\\"UUID\\\",\\\"primaryKey\\\":true,\\\"referenceKind\\\":\\\"id\\\",\\\"notNull\\\":true},\\\"slug\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true},\\\"context\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"\\\"},\\\"created_at\\\":{\\\"type\\\":\\\"TIMESTAMP\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"current_timestamp\\\"},\\\"updated_at\\\":{\\\"type\\\":\\\"TIMESTAMP\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"current_timestamp\\\"},\\\"child_tenant_id\\\":{\\\"type\\\":\\\"UUID\\\",\\\"referenceKind\\\":\\\"crossPackageRef\\\",\\\"notNull\\\":true,\\\"unique\\\":false},\\\"reseller_tenant_id\\\":{\\\"type\\\":\\\"UUID\\\",\\\"referenceKind\\\":\\\"crossPackageRef\\\",\\\"notNull\\\":true,\\\"unique\\\":false},\\\"billing_owner_mode\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false,\\\"default\\\":\\\"self\\\"}},\\\"indexes\\\":[{\\\"name\\\":\\\"_smrt_billing_relationships_child_tenant_id_idx\\\",\\\"columns\\\":[\\\"child_tenant_id\\\"],\\\"unique\\\":true},{\\\"name\\\":\\\"_smrt_billing_relationships_slug_context_idx\\\",\\\"columns\\\":[\\\"slug\\\",\\\"context\\\"]},{\\\"name\\\":\\\"_smrt_billing_relationships_created_at_idx\\\",\\\"columns\\\":[\\\"created_at\\\"]},{\\\"name\\\":\\\"_smrt_billing_relationships_reseller_tenant_id_idx\\\",\\\"columns\\\":[\\\"reseller_tenant_id\\\"]}],\\\"version\\\":\\\"4d7f410a\\\"},\\\"importPath\\\":\\\"@happyvertical/smrt-tenancy\\\"}},\\\"moduleType\\\":\\\"smrt\\\",\\\"smrtDependencies\\\":[\\\"@happyvertical/smrt-core\\\"],\\\"stampsConstructors\\\":true}\"));\n","/** Reseller parentage and billing ownership, independent of user permissions. */\nimport {\n crossPackageRef,\n field,\n isEmbeddedDatabase,\n isPostgresDatabase,\n type SmrtClassOptions,\n SmrtCollection,\n SmrtObject,\n smrt,\n withEmbeddedWriteTransaction,\n} from '@happyvertical/smrt-core';\nimport {\n getTenantId,\n isSuperAdminBypass,\n isSystemContext,\n TenantIsolationError,\n} from './context.js';\n\nexport type BillingOwnerMode = 'self' | 'reseller';\n\nexport interface BillingRelationshipView {\n childTenantId: string;\n resellerTenantId: string;\n billingOwnerMode: BillingOwnerMode;\n billingOwnerTenantId: string;\n}\n\nexport type BillingRelationshipAction = 'read' | 'manage';\n\nexport interface BillingRelationshipServiceOptions extends SmrtClassOptions {\n /** Resolve tenant IDs against the host tenant directory. */\n tenantExists: (tenantId: string) => Promise<boolean>;\n /** Optional host permission check for access beyond the built-in context rules. */\n authorize?: (request: {\n action: BillingRelationshipAction;\n childTenantId: string;\n resellerTenantId?: string;\n }) => Promise<boolean>;\n}\n\nexport class BillingRelationshipError extends Error {\n constructor(\n message: string,\n public readonly code:\n | 'INVALID_TENANT'\n | 'INVALID_OWNER_MODE'\n | 'TENANT_NOT_FOUND'\n | 'CIRCULAR_RELATIONSHIP',\n ) {\n super(message);\n this.name = 'BillingRelationshipError';\n }\n}\n\n@smrt({\n tableName: '_smrt_billing_relationships',\n conflictColumns: ['child_tenant_id'],\n api: false,\n cli: false,\n mcp: false,\n})\nexport class BillingRelationship extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-tenancy\";\n @crossPackageRef('@happyvertical/smrt-users:Tenant')\n @field({ required: true })\n childTenantId: string = '';\n @crossPackageRef('@happyvertical/smrt-users:Tenant')\n @field({ required: true })\n resellerTenantId: string = '';\n billingOwnerMode: BillingOwnerMode = 'self';\n\n protected async validateBeforeSave(): Promise<void> {\n await super.validateBeforeSave();\n this.childTenantId = canonicalTenantId(this.childTenantId);\n this.resellerTenantId = canonicalTenantId(this.resellerTenantId);\n assertMode(this.billingOwnerMode);\n if (this.childTenantId === this.resellerTenantId) {\n throw new BillingRelationshipError(\n 'A tenant cannot resell to itself.',\n 'CIRCULAR_RELATIONSHIP',\n );\n }\n }\n}\n\nexport class BillingRelationshipCollection extends SmrtCollection<BillingRelationship> {\n static readonly _itemClass = BillingRelationship;\n}\n\nconst UUID_PATTERN =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\nfunction assertTenantId(id: string): void {\n if (typeof id !== 'string' || !UUID_PATTERN.test(id)) {\n throw new BillingRelationshipError(\n 'Tenant IDs must be UUIDs.',\n 'INVALID_TENANT',\n );\n }\n}\n\nfunction canonicalTenantId(id: string): string {\n assertTenantId(id);\n return id.toLowerCase();\n}\n\nfunction assertMode(mode: BillingOwnerMode): void {\n if (mode !== 'self' && mode !== 'reseller') {\n throw new BillingRelationshipError(\n 'Billing owner mode must be self or reseller.',\n 'INVALID_OWNER_MODE',\n );\n }\n}\n\nfunction view(row: BillingRelationship): BillingRelationshipView {\n const childTenantId = canonicalTenantId(row.childTenantId);\n const resellerTenantId = canonicalTenantId(row.resellerTenantId);\n assertMode(row.billingOwnerMode);\n return {\n childTenantId,\n resellerTenantId,\n billingOwnerMode: row.billingOwnerMode,\n billingOwnerTenantId:\n row.billingOwnerMode === 'reseller' ? resellerTenantId : childTenantId,\n };\n}\n\n/**\n * The one-row-per-child billing graph. The host supplies tenant existence and\n * any additional authorization; absent a host authorizer, only an explicit\n * system context can mutate relationships.\n */\nexport class BillingRelationshipService {\n private constructor(\n private readonly relationships: BillingRelationshipCollection,\n private readonly options: BillingRelationshipServiceOptions,\n ) {}\n\n static async create(\n options: BillingRelationshipServiceOptions,\n ): Promise<BillingRelationshipService> {\n if (typeof options.tenantExists !== 'function') {\n throw new Error('Billing relationships require a tenantExists resolver.');\n }\n const relationships = await BillingRelationshipCollection.create(options);\n if (/^(?:https?|libsql):\\/\\//iu.test(relationships.db.url ?? '')) {\n throw new Error(\n 'Billing relationships require a local SQL or PostgreSQL database with serialized transactions.',\n );\n }\n if (!relationships.db.transaction) {\n throw new Error('Billing relationships require database transactions.');\n }\n return new BillingRelationshipService(relationships, options);\n }\n\n /** A tenant without a relationship always pays for itself. */\n async resolveBillingOwner(childTenantId: string): Promise<string> {\n childTenantId = canonicalTenantId(childTenantId);\n const row = await this.relationships.get({ childTenantId });\n await this.assertReadable(childTenantId, row?.resellerTenantId);\n await this.assertExists(childTenantId);\n if (row) await this.assertExists(row.resellerTenantId);\n return row ? view(row).billingOwnerTenantId : childTenantId;\n }\n\n async getRelationship(\n childTenantId: string,\n ): Promise<BillingRelationshipView | null> {\n childTenantId = canonicalTenantId(childTenantId);\n const row = await this.relationships.get({ childTenantId });\n await this.assertReadable(childTenantId, row?.resellerTenantId);\n await this.assertExists(childTenantId);\n if (row) await this.assertExists(row.resellerTenantId);\n return row ? view(row) : null;\n }\n\n /** List direct children billed to an owner, with no collection page limit. */\n async listChildrenBilledTo(ownerTenantId: string): Promise<string[]> {\n ownerTenantId = canonicalTenantId(ownerTenantId);\n await this.assertReadable(ownerTenantId);\n await this.assertExists(ownerTenantId);\n const result = await this.relationships.db.query(\n 'SELECT child_tenant_id FROM _smrt_billing_relationships WHERE reseller_tenant_id = ? AND billing_owner_mode = ? ORDER BY child_tenant_id',\n ownerTenantId,\n 'reseller',\n );\n return result.rows.map((row) => {\n return canonicalTenantId(String(row.child_tenant_id));\n });\n }\n\n /** Create or replace parentage and its default billing owner in one row. */\n async setRelationship(input: {\n childTenantId: string;\n resellerTenantId: string;\n billingOwnerMode: BillingOwnerMode;\n }): Promise<BillingRelationshipView> {\n const childTenantId = canonicalTenantId(input.childTenantId);\n const resellerTenantId = canonicalTenantId(input.resellerTenantId);\n const { billingOwnerMode } = input;\n assertMode(billingOwnerMode);\n await this.assertManage(childTenantId, resellerTenantId);\n await this.assertExists(childTenantId);\n await this.assertExists(resellerTenantId);\n if (childTenantId === resellerTenantId) {\n throw new BillingRelationshipError(\n 'A tenant cannot resell to itself.',\n 'CIRCULAR_RELATIONSHIP',\n );\n }\n return withEmbeddedWriteTransaction(\n this.relationships.db,\n isEmbeddedDatabase(this.relationships.db),\n async (db) => {\n if (isPostgresDatabase(db)) {\n await db.query(\n \"SELECT pg_advisory_xact_lock(hashtext('smrt-billing-relationships'))\",\n );\n }\n const relationships = await BillingRelationshipCollection.create({\n db,\n });\n await this.assertAcyclic(\n relationships,\n childTenantId,\n resellerTenantId,\n );\n const existing = await relationships.get({ childTenantId });\n if (existing && existing.resellerTenantId !== resellerTenantId) {\n await this.assertManage(childTenantId, existing.resellerTenantId);\n }\n const row = existing\n ? Object.assign(existing, { resellerTenantId, billingOwnerMode })\n : await relationships.create({\n childTenantId,\n resellerTenantId,\n billingOwnerMode,\n });\n if (existing) await row.save();\n return view(row);\n },\n );\n }\n\n /** Remove reseller parentage. The child becomes self-billed. */\n async clearRelationship(childTenantId: string): Promise<void> {\n childTenantId = canonicalTenantId(childTenantId);\n await withEmbeddedWriteTransaction(\n this.relationships.db,\n isEmbeddedDatabase(this.relationships.db),\n async (db) => {\n if (isPostgresDatabase(db)) {\n await db.query(\n \"SELECT pg_advisory_xact_lock(hashtext('smrt-billing-relationships'))\",\n );\n }\n const relationships = await BillingRelationshipCollection.create({\n db,\n });\n const current = await relationships.get({ childTenantId });\n await this.assertManage(childTenantId, current?.resellerTenantId);\n if (current?.id) {\n // SmrtObject.delete() starts its own cascade transaction, which\n // DuckDB cannot nest. This row has no dependents, so delete it on\n // the already-bound transaction handle.\n await db.delete('_smrt_billing_relationships', { id: current.id });\n }\n },\n );\n }\n\n private async assertAcyclic(\n relationships: BillingRelationshipCollection,\n childTenantId: string,\n resellerTenantId: string,\n ): Promise<void> {\n const visited = new Set([childTenantId]);\n let cursor: string | undefined = resellerTenantId;\n while (cursor) {\n if (visited.has(cursor)) {\n throw new BillingRelationshipError(\n 'Reseller relationships cannot form a cycle.',\n 'CIRCULAR_RELATIONSHIP',\n );\n }\n visited.add(cursor);\n const row = await relationships.get({ childTenantId: cursor });\n cursor = row ? canonicalTenantId(row.resellerTenantId) : undefined;\n }\n }\n\n private async assertExists(tenantId: string): Promise<void> {\n assertTenantId(tenantId);\n if (!(await this.options.tenantExists(tenantId))) {\n throw new BillingRelationshipError(\n `Tenant not found: ${tenantId}`,\n 'TENANT_NOT_FOUND',\n );\n }\n }\n\n private async assertReadable(\n childTenantId: string,\n resellerTenantId?: string,\n ): Promise<void> {\n if (\n isSystemContext() ||\n isSuperAdminBypass() ||\n getTenantId()?.toLowerCase() === childTenantId ||\n (resellerTenantId && getTenantId()?.toLowerCase() === resellerTenantId) ||\n (await this.options.authorize?.({\n action: 'read',\n childTenantId,\n resellerTenantId,\n }))\n ) {\n return;\n }\n throw new TenantIsolationError('Billing relationship access denied.');\n }\n\n private async assertManage(\n childTenantId: string,\n resellerTenantId?: string,\n ): Promise<void> {\n if (\n isSystemContext() ||\n isSuperAdminBypass() ||\n (await this.options.authorize?.({\n action: 'manage',\n childTenantId,\n resellerTenantId,\n }))\n ) {\n return;\n }\n throw new TenantIsolationError('Billing relationship mutation denied.');\n }\n}\n","/**\n * Tenancy Decorators\n *\n * Provides class and property decorators for tenant-scoped SMRT objects.\n *\n * @example\n * ```typescript\n * import { smrt, SmrtObject } from '@happyvertical/smrt-core';\n * import { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\n *\n * @smrt()\n * @TenantScoped({ mode: 'optional' })\n * class Document extends SmrtObject {\n * @tenantId({ nullable: true })\n * tenantId: string | null = null; // null = global document\n *\n * title: string = '';\n * }\n * ```\n *\n * @see https://github.com/happyvertical/smrt/issues/675\n * @see https://github.com/happyvertical/smrt/issues/829\n */\n\nimport {\n applyPendingDecoratorRegistrations,\n type CompatiblePropertyDecorator,\n type CompatiblePropertyDecoratorContext,\n type LegacyPropertyDecoratorTarget,\n ObjectRegistry,\n registerCompatibleFieldDecorator,\n} from '@happyvertical/smrt-core';\nimport type { TenantIdFieldOptions } from './fields.js';\nimport {\n registerTenantScopedConstructor,\n type TenantScopedConfig,\n} from './registry.js';\n\n/**\n * Options accepted by the `@TenantScoped()` class decorator.\n *\n * All fields are optional; defaults match the most restrictive safe behaviour\n * (required mode, auto-filter and auto-populate enabled, no super-admin bypass).\n *\n * @see TenantScoped\n * @see TenantScopedConfig\n */\nexport interface TenantScopedOptions {\n /**\n * Tenancy mode for this class\n * - 'required': Must have tenant context for all operations (default)\n * - 'optional': Works with or without tenant context\n */\n mode?: 'required' | 'optional';\n\n /**\n * Field name containing tenant ID\n * @default 'tenantId'\n */\n field?: string;\n\n /**\n * Auto-filter all queries by tenant\n * @default true\n */\n autoFilter?: boolean;\n\n /**\n * Auto-populate tenant ID from context on create\n * @default true\n */\n autoPopulate?: boolean;\n\n /**\n * Allow super admin bypass for this class\n * @default false - must be explicitly enabled\n */\n allowSuperAdminBypass?: boolean;\n}\n\n/**\n * Mark a class as tenant-scoped\n *\n * This decorator registers the class with the tenancy system so that:\n * - list()/get() queries are automatically filtered by tenant\n * - save() validates tenant ID matches current context\n * - delete() validates tenant ownership\n * - Raw SQL queries trigger policy enforcement\n *\n * @param options - Configuration options\n *\n * @example Basic usage (required tenancy)\n * ```typescript\n * @smrt()\n * @TenantScoped()\n * class Document extends SmrtObject {\n * @tenantId()\n * tenantId: string = '';\n *\n * title: string = '';\n * }\n * ```\n *\n * @example With super admin bypass enabled\n * ```typescript\n * @smrt()\n * @TenantScoped({ allowSuperAdminBypass: true })\n * class AuditLog extends SmrtObject {\n * @tenantId()\n * tenantId: string = '';\n *\n * action: string = '';\n * }\n * ```\n *\n * @example Optional tenancy (works with or without context)\n * ```typescript\n * @smrt()\n * @TenantScoped({ mode: 'optional' })\n * class GlobalConfig extends SmrtObject {\n * @tenantId({ nullable: true })\n * tenantId: string | null = null; // null = global, string = tenant-specific\n *\n * key: string = '';\n * value: string = '';\n * }\n * ```\n */\nexport function TenantScoped(options: TenantScopedOptions = {}) {\n return <T extends Function>(\n target: T,\n decoratorContext?: ClassDecoratorContext,\n ): T => {\n applyPendingDecoratorRegistrations(target, decoratorContext);\n\n // Merge with defaults\n const config: Partial<TenantScopedConfig> = {\n mode: options.mode ?? 'required',\n field: options.field ?? 'tenantId',\n autoFilter: options.autoFilter ?? true,\n autoPopulate: options.autoPopulate ?? true,\n allowSuperAdminBypass: options.allowSuperAdminBypass ?? false,\n };\n\n // Register with the tenancy system\n registerTenantScopedConstructor(target, config);\n\n // Support either class-decorator order. This declaration replaces only a\n // provisional field fallback; explicit @smrt and manifest policy retain\n // their documented precedence.\n ObjectRegistry.reconcileTenantScopedConfig(target, {\n mode: config.mode ?? 'required',\n field: config.field ?? 'tenantId',\n autoFilter: config.autoFilter ?? true,\n autoPopulate: config.autoPopulate ?? true,\n allowSuperAdminBypass: config.allowSuperAdminBypass ?? false,\n });\n\n // Return the class unchanged\n return target;\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Property Decorator: @tenantId\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Tenant ID property decorator\n *\n * Marks a property as the tenant identifier field. This decorator registers\n * the field metadata with ObjectRegistry, keeping the property value clean\n * (no descriptor objects that could be accidentally saved to the database).\n *\n * @param options - Field options (nullable, autoFilter, autoPopulate, etc.)\n * @returns Property decorator\n *\n * @example Basic usage (required tenancy)\n * ```typescript\n * @smrt()\n * @TenantScoped()\n * class Document extends SmrtObject {\n * @tenantId()\n * tenantId: string = '';\n *\n * title: string = '';\n * }\n * ```\n *\n * @example Nullable tenant ID (for global resources)\n * ```typescript\n * @smrt()\n * @TenantScoped({ mode: 'optional' })\n * class GlobalConfig extends SmrtObject {\n * @tenantId({ nullable: true })\n * tenantId: string | null = null; // null = global, string = tenant-specific\n *\n * key: string = '';\n * }\n * ```\n *\n * @see https://github.com/happyvertical/smrt/issues/829 - Why decorators over field helpers\n */\nexport function tenantId(options: TenantIdFieldOptions = {}) {\n const opts = {\n autoFilter: true,\n required: true,\n autoPopulate: true,\n nullable: false,\n ...options,\n };\n\n return ((\n targetOrValue: LegacyPropertyDecoratorTarget | undefined,\n propertyKeyOrContext: CompatiblePropertyDecoratorContext<unknown, unknown>,\n ) => {\n registerCompatibleFieldDecorator(\n targetOrValue,\n propertyKeyOrContext,\n (className, propertyKey, ctor) => {\n const fieldOptions: Parameters<\n typeof ObjectRegistry.registerFieldDecorator\n >[2] = {\n type: 'foreignKey',\n related: 'Tenant',\n sqlType: 'UUID',\n required: opts.required,\n nullable: opts.nullable,\n __tenancy: {\n ...opts,\n isTenantIdField: true,\n },\n };\n ObjectRegistry.registerFieldDecorator(\n className,\n propertyKey,\n fieldOptions,\n ctor,\n );\n },\n );\n }) as CompatiblePropertyDecorator;\n}\n","/**\n * Tenancy Field Types and Utilities\n *\n * This module provides types and utility functions for tenant ID fields.\n * The actual field decorator is in decorators.ts.\n *\n * @see https://github.com/happyvertical/smrt/issues/675\n * @see https://github.com/happyvertical/smrt/issues/829\n */\n\n/**\n * Options for the `@tenantId()` property decorator.\n *\n * Controls how the decorated field interacts with the tenancy interceptor.\n * All options default to the strictest safe values: auto-filter on, required,\n * auto-populate on, not nullable.\n *\n * @see tenantId\n * @see TenantScopedOptions\n */\nexport interface TenantIdFieldOptions {\n /**\n * Auto-filter queries by this field\n * @default true\n */\n autoFilter?: boolean;\n\n /**\n * Require this field to have a value on save\n * @default true\n */\n required?: boolean;\n\n /**\n * Auto-populate from context on create if not set\n * @default true\n */\n autoPopulate?: boolean;\n\n /**\n * Allow null values (for global resources)\n * @default false\n */\n nullable?: boolean;\n}\n\n// Symbol to identify tenantId fields\nexport const TENANT_ID_SYMBOL = Symbol('tenantId');\n\n/**\n * Internal field descriptor stored in `ObjectRegistry` when `@tenantId()` is\n * applied to a property.\n *\n * Consumers should use `isTenantIdField()` and `getTenantIdFieldOptions()`\n * to inspect these descriptors rather than reading the raw properties directly.\n *\n * @see isTenantIdField\n * @see getTenantIdFieldOptions\n */\nexport interface TenantIdFieldDefinition {\n /** Field type marker */\n type: 'foreignKey';\n /** Reference to Tenant class (placeholder - actual class resolved at runtime) */\n reference: 'Tenant';\n /** SQL type */\n sqlType: 'UUID';\n /** Field is required */\n required: boolean;\n /** Field allows null */\n nullable: boolean;\n /** Tenancy-specific options */\n __tenancy: TenantIdFieldOptions & { isTenantIdField: true };\n}\n\n/**\n * Return `true` if the given field definition was produced by the `@tenantId()`\n * decorator (i.e., it has an `__tenancy.isTenantIdField` marker).\n *\n * Used internally by the interceptor and code generators to locate the tenant\n * ID field on a class without knowing its property name in advance.\n *\n * @param field - A raw field definition object, typically from `ObjectRegistry`.\n * @returns `true` if `field` is a tenant ID field definition, `false` otherwise.\n *\n * @example\n * ```typescript\n * const fields = ObjectRegistry.getFields('Document');\n * const tenantField = Object.entries(fields).find(([, def]) => isTenantIdField(def));\n * ```\n *\n * @see getTenantIdFieldOptions\n * @see TenantIdFieldDefinition\n */\nexport function isTenantIdField(field: unknown): boolean {\n if (!field || typeof field !== 'object') {\n return false;\n }\n const def = field as Record<string, unknown>;\n const tenancy = def.__tenancy as Record<string, unknown> | undefined;\n return tenancy?.isTenantIdField === true;\n}\n\n/**\n * Extract the `TenantIdFieldOptions` from a field definition.\n *\n * Returns the tenancy-specific options (autoFilter, required, autoPopulate,\n * nullable) stored inside the field descriptor's `__tenancy` property.\n * Returns `null` if the field was not produced by `@tenantId()`.\n *\n * @param field - A raw field definition object, typically from `ObjectRegistry`.\n * @returns The `TenantIdFieldOptions` if the field is a tenant ID field,\n * `null` otherwise.\n *\n * @see isTenantIdField\n * @see TenantIdFieldOptions\n */\nexport function getTenantIdFieldOptions(\n field: unknown,\n): TenantIdFieldOptions | null {\n if (!isTenantIdField(field)) {\n return null;\n }\n const def = field as { __tenancy: TenantIdFieldOptions };\n return def.__tenancy;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/__smrt-register__.ts","../src/billing-relationship.ts","../src/decorators.ts","../src/fields.ts"],"sourcesContent":["// Generated by @happyvertical/smrt-core smrtPlugin at library build time.\n// The package manifest is inlined so self-registration cannot miss when a\n// downstream bundler relocates this module away from dist/manifest.json\n// (https://github.com/happyvertical/smrt/issues/1506,\n// https://github.com/happyvertical/smrt/issues/1507).\nimport { ObjectRegistry } from '@happyvertical/smrt-core';\n\nObjectRegistry.registerPackageManifest(JSON.parse(\"{\\\"version\\\":\\\"1.0.0\\\",\\\"timestamp\\\":0,\\\"packageName\\\":\\\"@happyvertical/smrt-tenancy\\\",\\\"packageVersion\\\":\\\"0.55.8\\\",\\\"objects\\\":{\\\"@happyvertical/smrt-tenancy:BillingRelationship\\\":{\\\"name\\\":\\\"billingrelationship\\\",\\\"className\\\":\\\"BillingRelationship\\\",\\\"qualifiedName\\\":\\\"@happyvertical/smrt-tenancy:BillingRelationship\\\",\\\"collection\\\":\\\"billingrelationships\\\",\\\"filePath\\\":\\\"/home/runner/work/smrt/smrt/packages/tenancy/src/billing-relationship.ts\\\",\\\"packageName\\\":\\\"@happyvertical/smrt-tenancy\\\",\\\"fields\\\":{\\\"childTenantId\\\":{\\\"type\\\":\\\"crossPackageRef\\\",\\\"required\\\":true,\\\"related\\\":\\\"@happyvertical/smrt-users:Tenant\\\",\\\"_meta\\\":{\\\"required\\\":true}},\\\"resellerTenantId\\\":{\\\"type\\\":\\\"crossPackageRef\\\",\\\"required\\\":true,\\\"related\\\":\\\"@happyvertical/smrt-users:Tenant\\\",\\\"_meta\\\":{\\\"required\\\":true}},\\\"billingOwnerMode\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":false,\\\"default\\\":\\\"self\\\",\\\"enum\\\":[\\\"self\\\",\\\"reseller\\\"]}},\\\"methods\\\":{},\\\"decoratorConfig\\\":{\\\"tableName\\\":\\\"_smrt_billing_relationships\\\",\\\"conflictColumns\\\":[\\\"child_tenant_id\\\"],\\\"api\\\":false,\\\"cli\\\":false,\\\"mcp\\\":false},\\\"extends\\\":\\\"SmrtObject\\\",\\\"exportName\\\":\\\"BillingRelationship\\\",\\\"collectionExportName\\\":\\\"BillingRelationshipCollection\\\",\\\"validationRules\\\":[{\\\"field\\\":\\\"childTenantId\\\",\\\"rule\\\":\\\"required\\\",\\\"fieldType\\\":\\\"crossPackageRef\\\"},{\\\"field\\\":\\\"resellerTenantId\\\",\\\"rule\\\":\\\"required\\\",\\\"fieldType\\\":\\\"crossPackageRef\\\"}],\\\"schema\\\":{\\\"tableName\\\":\\\"_smrt_billing_relationships\\\",\\\"ddl\\\":\\\"CREATE TABLE IF NOT EXISTS \\\\\\\"_smrt_billing_relationships\\\\\\\" (\\\\n \\\\\\\"id\\\\\\\" UUID PRIMARY KEY NOT NULL,\\\\n \\\\\\\"slug\\\\\\\" TEXT NOT NULL,\\\\n \\\\\\\"context\\\\\\\" TEXT NOT NULL DEFAULT '',\\\\n \\\\\\\"created_at\\\\\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\\\n \\\\\\\"updated_at\\\\\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\\\n \\\\\\\"child_tenant_id\\\\\\\" UUID NOT NULL,\\\\n \\\\\\\"reseller_tenant_id\\\\\\\" UUID NOT NULL,\\\\n \\\\\\\"billing_owner_mode\\\\\\\" TEXT DEFAULT 'self'\\\\n);\\\",\\\"columns\\\":{\\\"id\\\":{\\\"type\\\":\\\"UUID\\\",\\\"primaryKey\\\":true,\\\"referenceKind\\\":\\\"id\\\",\\\"notNull\\\":true},\\\"slug\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true},\\\"context\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"\\\"},\\\"created_at\\\":{\\\"type\\\":\\\"TIMESTAMP\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"current_timestamp\\\"},\\\"updated_at\\\":{\\\"type\\\":\\\"TIMESTAMP\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"current_timestamp\\\"},\\\"child_tenant_id\\\":{\\\"type\\\":\\\"UUID\\\",\\\"referenceKind\\\":\\\"crossPackageRef\\\",\\\"notNull\\\":true,\\\"unique\\\":false},\\\"reseller_tenant_id\\\":{\\\"type\\\":\\\"UUID\\\",\\\"referenceKind\\\":\\\"crossPackageRef\\\",\\\"notNull\\\":true,\\\"unique\\\":false},\\\"billing_owner_mode\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false,\\\"default\\\":\\\"self\\\"}},\\\"indexes\\\":[{\\\"name\\\":\\\"_smrt_billing_relationships_child_tenant_id_idx\\\",\\\"columns\\\":[\\\"child_tenant_id\\\"],\\\"unique\\\":true},{\\\"name\\\":\\\"_smrt_billing_relationships_slug_context_idx\\\",\\\"columns\\\":[\\\"slug\\\",\\\"context\\\"]},{\\\"name\\\":\\\"_smrt_billing_relationships_created_at_idx\\\",\\\"columns\\\":[\\\"created_at\\\"]},{\\\"name\\\":\\\"_smrt_billing_relationships_reseller_tenant_id_idx\\\",\\\"columns\\\":[\\\"reseller_tenant_id\\\"]}],\\\"version\\\":\\\"4d7f410a\\\"},\\\"importPath\\\":\\\"@happyvertical/smrt-tenancy\\\"},\\\"@happyvertical/smrt-tenancy:BillingRelationshipCollection\\\":{\\\"name\\\":\\\"billingrelationshipcollection\\\",\\\"className\\\":\\\"BillingRelationshipCollection\\\",\\\"qualifiedName\\\":\\\"@happyvertical/smrt-tenancy:BillingRelationshipCollection\\\",\\\"collection\\\":\\\"billingrelationships\\\",\\\"filePath\\\":\\\"/home/runner/work/smrt/smrt/packages/tenancy/src/billing-relationship.ts\\\",\\\"packageName\\\":\\\"@happyvertical/smrt-tenancy\\\",\\\"fields\\\":{},\\\"methods\\\":{},\\\"decoratorConfig\\\":{\\\"tableName\\\":\\\"_smrt_billing_relationships\\\",\\\"api\\\":false,\\\"mcp\\\":false,\\\"cli\\\":false},\\\"extends\\\":\\\"SmrtCollection\\\",\\\"extendsTypeArg\\\":\\\"BillingRelationship\\\",\\\"exportName\\\":\\\"BillingRelationshipCollection\\\",\\\"collectionExportName\\\":\\\"BillingRelationshipCollectionCollection\\\",\\\"schema\\\":{\\\"tableName\\\":\\\"_smrt_billing_relationships\\\",\\\"ddl\\\":\\\"CREATE TABLE IF NOT EXISTS \\\\\\\"_smrt_billing_relationships\\\\\\\" (\\\\n \\\\\\\"id\\\\\\\" UUID PRIMARY KEY NOT NULL,\\\\n \\\\\\\"slug\\\\\\\" TEXT NOT NULL,\\\\n \\\\\\\"context\\\\\\\" TEXT NOT NULL DEFAULT '',\\\\n \\\\\\\"created_at\\\\\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\\\n \\\\\\\"updated_at\\\\\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\\\n \\\\\\\"child_tenant_id\\\\\\\" UUID NOT NULL,\\\\n \\\\\\\"reseller_tenant_id\\\\\\\" UUID NOT NULL,\\\\n \\\\\\\"billing_owner_mode\\\\\\\" TEXT DEFAULT 'self'\\\\n);\\\",\\\"columns\\\":{\\\"id\\\":{\\\"type\\\":\\\"UUID\\\",\\\"primaryKey\\\":true,\\\"referenceKind\\\":\\\"id\\\",\\\"notNull\\\":true},\\\"slug\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true},\\\"context\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"\\\"},\\\"created_at\\\":{\\\"type\\\":\\\"TIMESTAMP\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"current_timestamp\\\"},\\\"updated_at\\\":{\\\"type\\\":\\\"TIMESTAMP\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"current_timestamp\\\"},\\\"child_tenant_id\\\":{\\\"type\\\":\\\"UUID\\\",\\\"referenceKind\\\":\\\"crossPackageRef\\\",\\\"notNull\\\":true,\\\"unique\\\":false},\\\"reseller_tenant_id\\\":{\\\"type\\\":\\\"UUID\\\",\\\"referenceKind\\\":\\\"crossPackageRef\\\",\\\"notNull\\\":true,\\\"unique\\\":false},\\\"billing_owner_mode\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false,\\\"default\\\":\\\"self\\\"}},\\\"indexes\\\":[{\\\"name\\\":\\\"_smrt_billing_relationships_child_tenant_id_idx\\\",\\\"columns\\\":[\\\"child_tenant_id\\\"],\\\"unique\\\":true},{\\\"name\\\":\\\"_smrt_billing_relationships_slug_context_idx\\\",\\\"columns\\\":[\\\"slug\\\",\\\"context\\\"]},{\\\"name\\\":\\\"_smrt_billing_relationships_created_at_idx\\\",\\\"columns\\\":[\\\"created_at\\\"]},{\\\"name\\\":\\\"_smrt_billing_relationships_reseller_tenant_id_idx\\\",\\\"columns\\\":[\\\"reseller_tenant_id\\\"]}],\\\"version\\\":\\\"4d7f410a\\\"},\\\"importPath\\\":\\\"@happyvertical/smrt-tenancy\\\"}},\\\"moduleType\\\":\\\"smrt\\\",\\\"smrtDependencies\\\":[\\\"@happyvertical/smrt-core\\\"],\\\"browser\\\":{\\\"status\\\":\\\"server-only\\\",\\\"issues\\\":[\\\"#3624\\\"],\\\"reason\\\":\\\"node:async_hooks\\\"},\\\"stampsConstructors\\\":true}\"));\n","/** Reseller parentage and billing ownership, independent of user permissions. */\nimport {\n crossPackageRef,\n field,\n isEmbeddedDatabase,\n isPostgresDatabase,\n type SmrtClassOptions,\n SmrtCollection,\n SmrtObject,\n smrt,\n withEmbeddedWriteTransaction,\n} from '@happyvertical/smrt-core';\nimport {\n getTenantId,\n isSuperAdminBypass,\n isSystemContext,\n TenantIsolationError,\n} from './context.js';\n\nexport type BillingOwnerMode = 'self' | 'reseller';\n\nexport interface BillingRelationshipView {\n childTenantId: string;\n resellerTenantId: string;\n billingOwnerMode: BillingOwnerMode;\n billingOwnerTenantId: string;\n}\n\nexport type BillingRelationshipAction = 'read' | 'manage';\n\nexport interface BillingRelationshipServiceOptions extends SmrtClassOptions {\n /** Resolve tenant IDs against the host tenant directory. */\n tenantExists: (tenantId: string) => Promise<boolean>;\n /** Optional host permission check for access beyond the built-in context rules. */\n authorize?: (request: {\n action: BillingRelationshipAction;\n childTenantId: string;\n resellerTenantId?: string;\n }) => Promise<boolean>;\n}\n\nexport class BillingRelationshipError extends Error {\n constructor(\n message: string,\n public readonly code:\n | 'INVALID_TENANT'\n | 'INVALID_OWNER_MODE'\n | 'TENANT_NOT_FOUND'\n | 'CIRCULAR_RELATIONSHIP',\n ) {\n super(message);\n this.name = 'BillingRelationshipError';\n }\n}\n\n@smrt({\n tableName: '_smrt_billing_relationships',\n conflictColumns: ['child_tenant_id'],\n api: false,\n cli: false,\n mcp: false,\n})\nexport class BillingRelationship extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-tenancy\";\n @crossPackageRef('@happyvertical/smrt-users:Tenant')\n @field({ required: true })\n childTenantId: string = '';\n @crossPackageRef('@happyvertical/smrt-users:Tenant')\n @field({ required: true })\n resellerTenantId: string = '';\n billingOwnerMode: BillingOwnerMode = 'self';\n\n protected async validateBeforeSave(): Promise<void> {\n await super.validateBeforeSave();\n this.childTenantId = canonicalTenantId(this.childTenantId);\n this.resellerTenantId = canonicalTenantId(this.resellerTenantId);\n assertMode(this.billingOwnerMode);\n if (this.childTenantId === this.resellerTenantId) {\n throw new BillingRelationshipError(\n 'A tenant cannot resell to itself.',\n 'CIRCULAR_RELATIONSHIP',\n );\n }\n }\n}\n\nexport class BillingRelationshipCollection extends SmrtCollection<BillingRelationship> {\n static readonly _itemClass = BillingRelationship;\n}\n\nconst UUID_PATTERN =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\nfunction assertTenantId(id: string): void {\n if (typeof id !== 'string' || !UUID_PATTERN.test(id)) {\n throw new BillingRelationshipError(\n 'Tenant IDs must be UUIDs.',\n 'INVALID_TENANT',\n );\n }\n}\n\nfunction canonicalTenantId(id: string): string {\n assertTenantId(id);\n return id.toLowerCase();\n}\n\nfunction assertMode(mode: BillingOwnerMode): void {\n if (mode !== 'self' && mode !== 'reseller') {\n throw new BillingRelationshipError(\n 'Billing owner mode must be self or reseller.',\n 'INVALID_OWNER_MODE',\n );\n }\n}\n\nfunction view(row: BillingRelationship): BillingRelationshipView {\n const childTenantId = canonicalTenantId(row.childTenantId);\n const resellerTenantId = canonicalTenantId(row.resellerTenantId);\n assertMode(row.billingOwnerMode);\n return {\n childTenantId,\n resellerTenantId,\n billingOwnerMode: row.billingOwnerMode,\n billingOwnerTenantId:\n row.billingOwnerMode === 'reseller' ? resellerTenantId : childTenantId,\n };\n}\n\n/**\n * The one-row-per-child billing graph. The host supplies tenant existence and\n * any additional authorization; absent a host authorizer, only an explicit\n * system context can mutate relationships.\n */\nexport class BillingRelationshipService {\n private constructor(\n private readonly relationships: BillingRelationshipCollection,\n private readonly options: BillingRelationshipServiceOptions,\n ) {}\n\n static async create(\n options: BillingRelationshipServiceOptions,\n ): Promise<BillingRelationshipService> {\n if (typeof options.tenantExists !== 'function') {\n throw new Error('Billing relationships require a tenantExists resolver.');\n }\n const relationships = await BillingRelationshipCollection.create(options);\n if (/^(?:https?|libsql):\\/\\//iu.test(relationships.db.url ?? '')) {\n throw new Error(\n 'Billing relationships require a local SQL or PostgreSQL database with serialized transactions.',\n );\n }\n if (!relationships.db.transaction) {\n throw new Error('Billing relationships require database transactions.');\n }\n return new BillingRelationshipService(relationships, options);\n }\n\n /** A tenant without a relationship always pays for itself. */\n async resolveBillingOwner(childTenantId: string): Promise<string> {\n childTenantId = canonicalTenantId(childTenantId);\n const row = await this.relationships.get({ childTenantId });\n await this.assertReadable(childTenantId, row?.resellerTenantId);\n await this.assertExists(childTenantId);\n if (row) await this.assertExists(row.resellerTenantId);\n return row ? view(row).billingOwnerTenantId : childTenantId;\n }\n\n async getRelationship(\n childTenantId: string,\n ): Promise<BillingRelationshipView | null> {\n childTenantId = canonicalTenantId(childTenantId);\n const row = await this.relationships.get({ childTenantId });\n await this.assertReadable(childTenantId, row?.resellerTenantId);\n await this.assertExists(childTenantId);\n if (row) await this.assertExists(row.resellerTenantId);\n return row ? view(row) : null;\n }\n\n /** List direct children billed to an owner, with no collection page limit. */\n async listChildrenBilledTo(ownerTenantId: string): Promise<string[]> {\n ownerTenantId = canonicalTenantId(ownerTenantId);\n await this.assertReadable(ownerTenantId);\n await this.assertExists(ownerTenantId);\n const result = await this.relationships.db.query(\n 'SELECT child_tenant_id FROM _smrt_billing_relationships WHERE reseller_tenant_id = ? AND billing_owner_mode = ? ORDER BY child_tenant_id',\n ownerTenantId,\n 'reseller',\n );\n return result.rows.map((row) => {\n return canonicalTenantId(String(row.child_tenant_id));\n });\n }\n\n /** Create or replace parentage and its default billing owner in one row. */\n async setRelationship(input: {\n childTenantId: string;\n resellerTenantId: string;\n billingOwnerMode: BillingOwnerMode;\n }): Promise<BillingRelationshipView> {\n const childTenantId = canonicalTenantId(input.childTenantId);\n const resellerTenantId = canonicalTenantId(input.resellerTenantId);\n const { billingOwnerMode } = input;\n assertMode(billingOwnerMode);\n await this.assertManage(childTenantId, resellerTenantId);\n await this.assertExists(childTenantId);\n await this.assertExists(resellerTenantId);\n if (childTenantId === resellerTenantId) {\n throw new BillingRelationshipError(\n 'A tenant cannot resell to itself.',\n 'CIRCULAR_RELATIONSHIP',\n );\n }\n return withEmbeddedWriteTransaction(\n this.relationships.db,\n isEmbeddedDatabase(this.relationships.db),\n async (db) => {\n if (isPostgresDatabase(db)) {\n await db.query(\n \"SELECT pg_advisory_xact_lock(hashtext('smrt-billing-relationships'))\",\n );\n }\n const relationships = await BillingRelationshipCollection.create({\n db,\n });\n await this.assertAcyclic(\n relationships,\n childTenantId,\n resellerTenantId,\n );\n const existing = await relationships.get({ childTenantId });\n if (existing && existing.resellerTenantId !== resellerTenantId) {\n await this.assertManage(childTenantId, existing.resellerTenantId);\n }\n const row = existing\n ? Object.assign(existing, { resellerTenantId, billingOwnerMode })\n : await relationships.create({\n childTenantId,\n resellerTenantId,\n billingOwnerMode,\n });\n if (existing) await row.save();\n return view(row);\n },\n );\n }\n\n /** Remove reseller parentage. The child becomes self-billed. */\n async clearRelationship(childTenantId: string): Promise<void> {\n childTenantId = canonicalTenantId(childTenantId);\n await withEmbeddedWriteTransaction(\n this.relationships.db,\n isEmbeddedDatabase(this.relationships.db),\n async (db) => {\n if (isPostgresDatabase(db)) {\n await db.query(\n \"SELECT pg_advisory_xact_lock(hashtext('smrt-billing-relationships'))\",\n );\n }\n const relationships = await BillingRelationshipCollection.create({\n db,\n });\n const current = await relationships.get({ childTenantId });\n await this.assertManage(childTenantId, current?.resellerTenantId);\n if (current?.id) {\n // SmrtObject.delete() starts its own cascade transaction, which\n // DuckDB cannot nest. This row has no dependents, so delete it on\n // the already-bound transaction handle.\n await db.delete('_smrt_billing_relationships', { id: current.id });\n }\n },\n );\n }\n\n private async assertAcyclic(\n relationships: BillingRelationshipCollection,\n childTenantId: string,\n resellerTenantId: string,\n ): Promise<void> {\n const visited = new Set([childTenantId]);\n let cursor: string | undefined = resellerTenantId;\n while (cursor) {\n if (visited.has(cursor)) {\n throw new BillingRelationshipError(\n 'Reseller relationships cannot form a cycle.',\n 'CIRCULAR_RELATIONSHIP',\n );\n }\n visited.add(cursor);\n const row = await relationships.get({ childTenantId: cursor });\n cursor = row ? canonicalTenantId(row.resellerTenantId) : undefined;\n }\n }\n\n private async assertExists(tenantId: string): Promise<void> {\n assertTenantId(tenantId);\n if (!(await this.options.tenantExists(tenantId))) {\n throw new BillingRelationshipError(\n `Tenant not found: ${tenantId}`,\n 'TENANT_NOT_FOUND',\n );\n }\n }\n\n private async assertReadable(\n childTenantId: string,\n resellerTenantId?: string,\n ): Promise<void> {\n if (\n isSystemContext() ||\n isSuperAdminBypass() ||\n getTenantId()?.toLowerCase() === childTenantId ||\n (resellerTenantId && getTenantId()?.toLowerCase() === resellerTenantId) ||\n (await this.options.authorize?.({\n action: 'read',\n childTenantId,\n resellerTenantId,\n }))\n ) {\n return;\n }\n throw new TenantIsolationError('Billing relationship access denied.');\n }\n\n private async assertManage(\n childTenantId: string,\n resellerTenantId?: string,\n ): Promise<void> {\n if (\n isSystemContext() ||\n isSuperAdminBypass() ||\n (await this.options.authorize?.({\n action: 'manage',\n childTenantId,\n resellerTenantId,\n }))\n ) {\n return;\n }\n throw new TenantIsolationError('Billing relationship mutation denied.');\n }\n}\n","/**\n * Tenancy Decorators\n *\n * Provides class and property decorators for tenant-scoped SMRT objects.\n *\n * @example\n * ```typescript\n * import { smrt, SmrtObject } from '@happyvertical/smrt-core';\n * import { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\n *\n * @smrt()\n * @TenantScoped({ mode: 'optional' })\n * class Document extends SmrtObject {\n * @tenantId({ nullable: true })\n * tenantId: string | null = null; // null = global document\n *\n * title: string = '';\n * }\n * ```\n *\n * @see https://github.com/happyvertical/smrt/issues/675\n * @see https://github.com/happyvertical/smrt/issues/829\n */\n\nimport {\n applyPendingDecoratorRegistrations,\n type CompatiblePropertyDecorator,\n type CompatiblePropertyDecoratorContext,\n type LegacyPropertyDecoratorTarget,\n ObjectRegistry,\n registerCompatibleFieldDecorator,\n} from '@happyvertical/smrt-core';\nimport type { TenantIdFieldOptions } from './fields.js';\nimport {\n registerTenantScopedConstructor,\n type TenantScopedConfig,\n} from './registry.js';\n\n/**\n * Options accepted by the `@TenantScoped()` class decorator.\n *\n * All fields are optional; defaults match the most restrictive safe behaviour\n * (required mode, auto-filter and auto-populate enabled, no super-admin bypass).\n *\n * @see TenantScoped\n * @see TenantScopedConfig\n */\nexport interface TenantScopedOptions {\n /**\n * Tenancy mode for this class\n * - 'required': Must have tenant context for all operations (default)\n * - 'optional': Works with or without tenant context\n */\n mode?: 'required' | 'optional';\n\n /**\n * Field name containing tenant ID\n * @default 'tenantId'\n */\n field?: string;\n\n /**\n * Auto-filter all queries by tenant\n * @default true\n */\n autoFilter?: boolean;\n\n /**\n * Auto-populate tenant ID from context on create\n * @default true\n */\n autoPopulate?: boolean;\n\n /**\n * Allow super admin bypass for this class\n * @default false - must be explicitly enabled\n */\n allowSuperAdminBypass?: boolean;\n}\n\n/**\n * Mark a class as tenant-scoped\n *\n * This decorator registers the class with the tenancy system so that:\n * - list()/get() queries are automatically filtered by tenant\n * - save() validates tenant ID matches current context\n * - delete() validates tenant ownership\n * - Raw SQL queries trigger policy enforcement\n *\n * @param options - Configuration options\n *\n * @example Basic usage (required tenancy)\n * ```typescript\n * @smrt()\n * @TenantScoped()\n * class Document extends SmrtObject {\n * @tenantId()\n * tenantId: string = '';\n *\n * title: string = '';\n * }\n * ```\n *\n * @example With super admin bypass enabled\n * ```typescript\n * @smrt()\n * @TenantScoped({ allowSuperAdminBypass: true })\n * class AuditLog extends SmrtObject {\n * @tenantId()\n * tenantId: string = '';\n *\n * action: string = '';\n * }\n * ```\n *\n * @example Optional tenancy (works with or without context)\n * ```typescript\n * @smrt()\n * @TenantScoped({ mode: 'optional' })\n * class GlobalConfig extends SmrtObject {\n * @tenantId({ nullable: true })\n * tenantId: string | null = null; // null = global, string = tenant-specific\n *\n * key: string = '';\n * value: string = '';\n * }\n * ```\n */\nexport function TenantScoped(options: TenantScopedOptions = {}) {\n return <T extends Function>(\n target: T,\n decoratorContext?: ClassDecoratorContext,\n ): T => {\n applyPendingDecoratorRegistrations(target, decoratorContext);\n\n // Merge with defaults\n const config: Partial<TenantScopedConfig> = {\n mode: options.mode ?? 'required',\n field: options.field ?? 'tenantId',\n autoFilter: options.autoFilter ?? true,\n autoPopulate: options.autoPopulate ?? true,\n allowSuperAdminBypass: options.allowSuperAdminBypass ?? false,\n };\n\n // Register with the tenancy system\n registerTenantScopedConstructor(target, config);\n\n // Support either class-decorator order. This declaration replaces only a\n // provisional field fallback; explicit @smrt and manifest policy retain\n // their documented precedence.\n ObjectRegistry.reconcileTenantScopedConfig(target, {\n mode: config.mode ?? 'required',\n field: config.field ?? 'tenantId',\n autoFilter: config.autoFilter ?? true,\n autoPopulate: config.autoPopulate ?? true,\n allowSuperAdminBypass: config.allowSuperAdminBypass ?? false,\n });\n\n // Return the class unchanged\n return target;\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Property Decorator: @tenantId\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Tenant ID property decorator\n *\n * Marks a property as the tenant identifier field. This decorator registers\n * the field metadata with ObjectRegistry, keeping the property value clean\n * (no descriptor objects that could be accidentally saved to the database).\n *\n * @param options - Field options (nullable, autoFilter, autoPopulate, etc.)\n * @returns Property decorator\n *\n * @example Basic usage (required tenancy)\n * ```typescript\n * @smrt()\n * @TenantScoped()\n * class Document extends SmrtObject {\n * @tenantId()\n * tenantId: string = '';\n *\n * title: string = '';\n * }\n * ```\n *\n * @example Nullable tenant ID (for global resources)\n * ```typescript\n * @smrt()\n * @TenantScoped({ mode: 'optional' })\n * class GlobalConfig extends SmrtObject {\n * @tenantId({ nullable: true })\n * tenantId: string | null = null; // null = global, string = tenant-specific\n *\n * key: string = '';\n * }\n * ```\n *\n * @see https://github.com/happyvertical/smrt/issues/829 - Why decorators over field helpers\n */\nexport function tenantId(options: TenantIdFieldOptions = {}) {\n const opts = {\n autoFilter: true,\n required: true,\n autoPopulate: true,\n nullable: false,\n ...options,\n };\n\n return ((\n targetOrValue: LegacyPropertyDecoratorTarget | undefined,\n propertyKeyOrContext: CompatiblePropertyDecoratorContext<unknown, unknown>,\n ) => {\n registerCompatibleFieldDecorator(\n targetOrValue,\n propertyKeyOrContext,\n (className, propertyKey, ctor) => {\n const fieldOptions: Parameters<\n typeof ObjectRegistry.registerFieldDecorator\n >[2] = {\n type: 'foreignKey',\n related: 'Tenant',\n sqlType: 'UUID',\n required: opts.required,\n nullable: opts.nullable,\n __tenancy: {\n ...opts,\n isTenantIdField: true,\n },\n };\n ObjectRegistry.registerFieldDecorator(\n className,\n propertyKey,\n fieldOptions,\n ctor,\n );\n },\n );\n }) as CompatiblePropertyDecorator;\n}\n","/**\n * Tenancy Field Types and Utilities\n *\n * This module provides types and utility functions for tenant ID fields.\n * The actual field decorator is in decorators.ts.\n *\n * @see https://github.com/happyvertical/smrt/issues/675\n * @see https://github.com/happyvertical/smrt/issues/829\n */\n\n/**\n * Options for the `@tenantId()` property decorator.\n *\n * Controls how the decorated field interacts with the tenancy interceptor.\n * All options default to the strictest safe values: auto-filter on, required,\n * auto-populate on, not nullable.\n *\n * @see tenantId\n * @see TenantScopedOptions\n */\nexport interface TenantIdFieldOptions {\n /**\n * Auto-filter queries by this field\n * @default true\n */\n autoFilter?: boolean;\n\n /**\n * Require this field to have a value on save\n * @default true\n */\n required?: boolean;\n\n /**\n * Auto-populate from context on create if not set\n * @default true\n */\n autoPopulate?: boolean;\n\n /**\n * Allow null values (for global resources)\n * @default false\n */\n nullable?: boolean;\n}\n\n// Symbol to identify tenantId fields\nexport const TENANT_ID_SYMBOL = Symbol('tenantId');\n\n/**\n * Internal field descriptor stored in `ObjectRegistry` when `@tenantId()` is\n * applied to a property.\n *\n * Consumers should use `isTenantIdField()` and `getTenantIdFieldOptions()`\n * to inspect these descriptors rather than reading the raw properties directly.\n *\n * @see isTenantIdField\n * @see getTenantIdFieldOptions\n */\nexport interface TenantIdFieldDefinition {\n /** Field type marker */\n type: 'foreignKey';\n /** Reference to Tenant class (placeholder - actual class resolved at runtime) */\n reference: 'Tenant';\n /** SQL type */\n sqlType: 'UUID';\n /** Field is required */\n required: boolean;\n /** Field allows null */\n nullable: boolean;\n /** Tenancy-specific options */\n __tenancy: TenantIdFieldOptions & { isTenantIdField: true };\n}\n\n/**\n * Return `true` if the given field definition was produced by the `@tenantId()`\n * decorator (i.e., it has an `__tenancy.isTenantIdField` marker).\n *\n * Used internally by the interceptor and code generators to locate the tenant\n * ID field on a class without knowing its property name in advance.\n *\n * @param field - A raw field definition object, typically from `ObjectRegistry`.\n * @returns `true` if `field` is a tenant ID field definition, `false` otherwise.\n *\n * @example\n * ```typescript\n * const fields = ObjectRegistry.getFields('Document');\n * const tenantField = Object.entries(fields).find(([, def]) => isTenantIdField(def));\n * ```\n *\n * @see getTenantIdFieldOptions\n * @see TenantIdFieldDefinition\n */\nexport function isTenantIdField(field: unknown): boolean {\n if (!field || typeof field !== 'object') {\n return false;\n }\n const def = field as Record<string, unknown>;\n const tenancy = def.__tenancy as Record<string, unknown> | undefined;\n return tenancy?.isTenantIdField === true;\n}\n\n/**\n * Extract the `TenantIdFieldOptions` from a field definition.\n *\n * Returns the tenancy-specific options (autoFilter, required, autoPopulate,\n * nullable) stored inside the field descriptor's `__tenancy` property.\n * Returns `null` if the field was not produced by `@tenantId()`.\n *\n * @param field - A raw field definition object, typically from `ObjectRegistry`.\n * @returns The `TenantIdFieldOptions` if the field is a tenant ID field,\n * `null` otherwise.\n *\n * @see isTenantIdField\n * @see TenantIdFieldOptions\n */\nexport function getTenantIdFieldOptions(\n field: unknown,\n): TenantIdFieldOptions | null {\n if (!isTenantIdField(field)) {\n return null;\n }\n const def = field as { __tenancy: TenantIdFieldOptions };\n return def.__tenancy;\n}\n"],"mappings":"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"}
|
package/dist/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"version": "1.0.0",
|
|
3
3
|
"timestamp": 0,
|
|
4
4
|
"packageName": "@happyvertical/smrt-tenancy",
|
|
5
|
-
"packageVersion": "0.55.
|
|
5
|
+
"packageVersion": "0.55.8",
|
|
6
6
|
"objects": {
|
|
7
7
|
"@happyvertical/smrt-tenancy:BillingRelationship": {
|
|
8
8
|
"name": "billingrelationship",
|
|
@@ -247,5 +247,12 @@
|
|
|
247
247
|
"smrtDependencies": [
|
|
248
248
|
"@happyvertical/smrt-core"
|
|
249
249
|
],
|
|
250
|
+
"browser": {
|
|
251
|
+
"status": "server-only",
|
|
252
|
+
"issues": [
|
|
253
|
+
"#3624"
|
|
254
|
+
],
|
|
255
|
+
"reason": "node:async_hooks"
|
|
256
|
+
},
|
|
250
257
|
"stampsConstructors": true
|
|
251
258
|
}
|
package/dist/smrt-knowledge.json
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
"sensitiveFieldsExcluded": true,
|
|
4
4
|
"generatedAt": "1970-01-01T00:00:00.000Z",
|
|
5
5
|
"packageName": "@happyvertical/smrt-tenancy",
|
|
6
|
-
"packageVersion": "0.55.
|
|
6
|
+
"packageVersion": "0.55.8",
|
|
7
7
|
"sourceManifestPath": "dist/manifest.json",
|
|
8
8
|
"agentDocPath": "AGENTS.md",
|
|
9
9
|
"sourceHashes": {
|
|
10
|
-
"manifest": "
|
|
11
|
-
"packageJson": "
|
|
10
|
+
"manifest": "f73dae11bba183f850a52fd6254aec919526c616c0ef2e7f80262ecfc3c8e73b",
|
|
11
|
+
"packageJson": "2bfebe7d03c3fe10011f8849c388cb8b90382f7537fc059e0c26a614d6677fa1",
|
|
12
12
|
"agents": "400de55ccbb88e4fba9f29a8d0e9c104488f166e232182057fece627914d05e1"
|
|
13
13
|
},
|
|
14
14
|
"exports": [
|
|
@@ -149,5 +149,12 @@
|
|
|
149
149
|
"polymorphicAssociations": 0,
|
|
150
150
|
"uuidColumns": 6
|
|
151
151
|
},
|
|
152
|
-
"agentDoc": "# @happyvertical/smrt-tenancy\n\nMulti-tenancy via AsyncLocalStorage context propagation with automatic query filtering and tenant ID population.\n\n## Context Propagation\n\n```typescript\nimport { withTenant, getTenantId, withSystemContext } from '@happyvertical/smrt-tenancy';\n\nawait withTenant({ tenantId: 'tenant-123' }, async () => {\n // All SmrtCollection queries auto-filtered by tenantId\n // All creates auto-populate tenantId\n const docs = await collection.list({}); // WHERE tenant_id = 'tenant-123'\n});\n\nawait withSystemContext(async () => { /* bypasses all tenant checks */ });\n```\n\n**Critical distinction**: `withSystemContext()` sets a SYSTEM_CONTEXT_MARKER sentinel — different from \"no context\" (undefined). Interceptor can distinguish intentional bypass from missing context.\n\n**Duplication-safe storage**: the underlying `AsyncLocalStorage` is a `Symbol.for`-keyed singleton on `globalThis`, so context survives Vite/vitest/SvelteKit pipelines that evaluate the module more than once — context entered through one module instance is visible to guards in another (#2077).\n\n## Billing relationships (#3058)\n\n`src/billing-relationship.ts` owns a separate reseller parent graph. It never\nupdates `smrt-users` `Tenant.parentTenantId` or tenant-scoped records. One row\nper child stores the reseller and `self`/`reseller` owner mode; owner queries\nderive one tenant ID. Mutations go through `BillingRelationshipService`, which\nserializes graph writes and rejects cycles. The host supplies tenant existence\nand optional authorization; no generated write surface is exposed. The model\nstays a root export because generated consumer registration imports every\npublic non-collection manifest object from the root; withholding it breaks\nevery dependent's `register.js` (publish dry run, smrt#3074). The collection\nstays unexported, as in smrt-chat. See the README for setup, access rules, and\nmigration guidance.\n\n## Interceptor System\n\nHooks into SmrtCollection via `GlobalInterceptors.register()` (priority 100, runs first):\n\n| Hook | Behavior |\n|------|----------|\n| `beforeList` | Injects `tenantId` into WHERE clause; validates existing filters match context |\n| `beforeGet` | Same — resolves string lookups via core's `resolveGetStringFilter()` (UUID → `{ id }`, else `{ slug, context: '' }`) and adds the tenant predicate (#2365) |\n| `beforeSave` | Auto-populates tenantId if empty + `autoPopulate: true`; validates if already set |\n| `beforeDelete` | Validates instance.tenantId matches context |\n| `beforeQuery` | Enforces raw SQL policy on tenant-scoped classes (`throw`/`warn`/`allow`) |\n| `afterSave` | Emits `directory.<class>.created`/`updated` via `dispatchBus` for configured `directoryClasses` |\n| `afterDelete` | Emits `directory.<class>.deleted` via `dispatchBus` for configured `directoryClasses` |\n\nMismatches throw `TenantIsolationError`. Missing required context throws `TenantContextError`.\n\n**Optional-mode reads with no context pass through UNFILTERED at the interceptor.** That is intentional for trusted/admin call paths, but it means the interceptor alone does not protect a tenant-scoped model exposed as `@smrt({ api: { public } })`: an anonymous HTTP read has no context, so the interceptor would return every tenant's rows. The generated REST + SvelteKit read routes close this by injecting a `{ tenantId: null }` filter when tenancy is enabled but no context is active, so public/anonymous reads fail closed to **global (NULL-tenant) rows only** — mirroring the dispatch resolver's *enforced, no active tenant → global rows only* rule (#1782). Authenticated reads still scope to the caller's tenant via the interceptor.\n\n## Read-Path Coverage (#2365)\n\nTenant scoping is a whole-path property — every read path is interceptor-aware,\nnot only collection list/get:\n\n- **Get-by-slug**: `collection.get('<slug>')` works under a tenant context. The\n interceptor resolves string filters with core's `resolveGetStringFilter()`\n instead of assuming they are ids. Any custom `beforeGet` interceptor that\n rewrites a string filter must do the same.\n- **Hydration and identity**: `new Model({ id | slug }).initialize()`,\n `loadFromId()`, `loadFromSlug()`, `getSavedId()` and `getId()` run their\n filters through the `beforeGet` pipeline, so constructor hydration cannot\n read another tenant's row and `getId()` can never adopt another tenant's\n same-slug row id (which would steer a later `save()` onto the foreign row).\n Required-mode classes fail closed (`TenantContextError`) when hydrated\n outside a tenant context; `withSystemContext()` / super-admin bypass remain\n the explicit cross-tenant paths.\n- **Vector search**: `semanticSearch()` / `findSimilarToEmbedding()` restrict\n candidates to the tenant's rows BEFORE top-K ranking (the tenant predicate is\n resolved through the `beforeList` pipeline), so results are never starved by\n — and similarity ranks never leak — other tenants' content.\n- **Collection memory**: `remember()`/`recall()`/`recallAll()`/`forget()` on a\n tenant-scoped collection key `_smrt_contexts.owner_id` per tenant\n (`__collection__:<tenantId>`) under an active tenant context. Isolation is\n strict: tenant-keyed memory never falls back to the shared `__collection__`\n key, and memory learned outside a tenant context is invisible inside one.\n Two edge semantics to know: under `withSuperAdminBypass()` reads skip\n filtering but memory still keys to the active tenant (scoped tighter, not a\n leak), and an empty-string tenant id resolves to the shared key (an\n empty-string tenant is a misconfiguration — real tenant ids are uuids).\n\n## Registration — Two Patterns\n\n```typescript\n// Pattern 1: Tenancy decorator\n@TenantScoped({ mode: 'optional' })\nclass Doc extends SmrtObject { @tenantId({ nullable: true }) tenantId: string | null = null; }\n\n// Pattern 2: Core decorator (tenancy package reads this too)\n@smrt({ tenantScoped: { mode: 'optional' } })\nclass Doc extends SmrtObject { tenantId: string | null = null; }\n```\n\nThe exported `registerTenantScopedClass()` also accepts a simple or qualified\nstring selector for third-party classes and test doubles. A simple selector is\nbound to the one exact core constructor when that name is uniquely resolvable;\nit may be registered before core. Once bound, a later same-name package peer\ndoes not inherit its policy. If two core classes exist before the simple\nselector can bind, operations fail closed until the caller registers an exact\nqualified name (for example, `@package/name:Doc`). No lookup strips a namespace\nto guess an owner. If core clears a bound constructor and reuses its qualified\nname, unregister and register the selector again; it will not silently transfer\npolicy to the replacement constructor.\n\nModes: `'required'` (default — throws without context) or `'optional'` (passes through if no context).\n\n## Adapters\n\n- **Express**: `createExpressMiddleware()` — uses `enterTenantContext()` (not withTenant, because middleware returns before handlers run)\n- **SvelteKit**: `createSvelteKitHandle()` — stores context in `event.locals`\n- **CLI**: `createCliContext()` — `run()`, `runWithTenant()`, `runAsSystem()`, `runAsSuperAdmin()`\n\n## Super Admin Bypass\n\n`withSuperAdminBypass()` keeps tenant context but disables auto-filtering. Different from `withSystemContext()` which removes context entirely.\n\n## Gotchas\n\n- **Context lost in callbacks**: `setTimeout(() => getTenantId(), 100)` → undefined. Fix: `TenantContext.bind(fn)`\n- **Nested contexts override**: inner `withTenant()` overrides outer; restores on exit\n- **Auto-populate only if empty**: if tenantId already set, interceptor validates (not overwrites)\n- **Isolation checked at query time**: `list({ where: { tenantId: 'other' } })` throws immediately\n- **Testing**: `resetTenancy()` + `setupTestTenancy()` in beforeEach; `testTenantIsolation()` helper\n- **Natural keys are per tenant (smrt#2360)**: a tenant-scoped class with no explicit `conflictColumns` upserts on, and indexes, `(tenant_id, slug, context[, _meta_type])` — `save()` from tenant B with tenant A's slug is a second row, never an overwrite; within a tenant the natural key still dedups; NULL-tenant (`optional` mode, no context) rows dedup among themselves through the SDK's null-aware upsert but not through the index (NULLs are distinct), so raw SQL `ON CONFLICT (slug, context…)` on such a table no longer binds — use `WHERE NOT EXISTS`, and on PostgreSQL an advisory lock, as `ProfileTypeCollection.getOrCreateGlobalBySlug()` does. Core resolves tenant schema policy in order: explicit `@smrt`, manifest (including an omitted `tenantScoped`), exact-constructor `@TenantScoped()` reconciliation, then marked-field fallback; it never reads the standalone tenancy registry. This keeps schema and upsert behavior aligned before `enableTenancy()` runs. Rollout: deploy the code and `smrt db:migrate` together (neither version's create works against the other's index), and backfill `tenant_id` on legacy NULL-tenant rows first — a tenant-context save no longer adopts a `(NULL, slug)` row, it inserts beside it and that tenant stops seeing the legacy one (details in `packages/core/agents/schema-paths.md`).\n- **Runtime-only registration is audited, and ownership keys follow the field**: a class registered with `registerTenantScopedClass()` whose manifest declares no tenancy still gets the tenant-led default key when it has a `tenantId` field (core infers OWNERSHIP from the field, never the read policy). `auditTenantScopedRegistrations()` reports per selector: `missing_tenant_field` (error — neither the model nor a registered ancestor declares the field; an STI child registered before its manifest loads inherits it from the parent, #3623), `natural_key_not_tenant_scoped` (warning — an explicit key omits the tenant column, so a second tenant's same-key save is refused with `TENANT_ISOLATION_VIOLATION`), `undeclared_tenant_scope` (info). Registration (or first use, when it precedes core) logs the error/warning findings once. Core's save guard, not the interceptor, is what stops a tenant-filtered lookup from upserting onto another tenant's row (see core `agents/schema-paths.md`).\n- **Manifest/runtime mismatch fails closed (smrt#2763)**: if a cached manifest omits or sets `tenantScoped: false` while the exact runtime constructor carries `@TenantScoped()`, registration is rejected; regenerate the manifest. A rejected late manifest or conflicting promotion preserves a previously valid scoped registration. Applying `@TenantScoped()` to an already-global manifest registration that omits or disables tenancy instead marks that class unavailable to conflict-key, schema, and tenancy interceptor operations until a corrected manifest explicitly declares tenancy. This prevents a global unique key from conflicting with tenant-enforced reads or writes.\n\n## Known exceptions to monorepo standards\n\n- **`serializeInstance()` in `src/interceptor.ts` calls `instance.toJSON()` directly** (standards.md §7 forbids this in favor of `transformJSON()`). The interceptor must serialize arbitrary instances handed to it — including workspace stubs and plain-object test doubles whose classes may not extend `SmrtObject` and therefore have no `transformJSON()` hook. The call is duck-typed and falls back to manual key iteration when `toJSON` is absent. See the inline comment at the call site for the full rationale.\n"
|
|
152
|
+
"agentDoc": "# @happyvertical/smrt-tenancy\n\nMulti-tenancy via AsyncLocalStorage context propagation with automatic query filtering and tenant ID population.\n\n## Context Propagation\n\n```typescript\nimport { withTenant, getTenantId, withSystemContext } from '@happyvertical/smrt-tenancy';\n\nawait withTenant({ tenantId: 'tenant-123' }, async () => {\n // All SmrtCollection queries auto-filtered by tenantId\n // All creates auto-populate tenantId\n const docs = await collection.list({}); // WHERE tenant_id = 'tenant-123'\n});\n\nawait withSystemContext(async () => { /* bypasses all tenant checks */ });\n```\n\n**Critical distinction**: `withSystemContext()` sets a SYSTEM_CONTEXT_MARKER sentinel — different from \"no context\" (undefined). Interceptor can distinguish intentional bypass from missing context.\n\n**Duplication-safe storage**: the underlying `AsyncLocalStorage` is a `Symbol.for`-keyed singleton on `globalThis`, so context survives Vite/vitest/SvelteKit pipelines that evaluate the module more than once — context entered through one module instance is visible to guards in another (#2077).\n\n## Billing relationships (#3058)\n\n`src/billing-relationship.ts` owns a separate reseller parent graph. It never\nupdates `smrt-users` `Tenant.parentTenantId` or tenant-scoped records. One row\nper child stores the reseller and `self`/`reseller` owner mode; owner queries\nderive one tenant ID. Mutations go through `BillingRelationshipService`, which\nserializes graph writes and rejects cycles. The host supplies tenant existence\nand optional authorization; no generated write surface is exposed. The model\nstays a root export because generated consumer registration imports every\npublic non-collection manifest object from the root; withholding it breaks\nevery dependent's `register.js` (publish dry run, smrt#3074). The collection\nstays unexported, as in smrt-chat. See the README for setup, access rules, and\nmigration guidance.\n\n## Interceptor System\n\nHooks into SmrtCollection via `GlobalInterceptors.register()` (priority 100, runs first):\n\n| Hook | Behavior |\n|------|----------|\n| `beforeList` | Injects `tenantId` into WHERE clause; validates existing filters match context |\n| `beforeGet` | Same — resolves string lookups via core's `resolveGetStringFilter()` (UUID → `{ id }`, else `{ slug, context: '' }`) and adds the tenant predicate (#2365) |\n| `beforeSave` | Auto-populates tenantId if empty + `autoPopulate: true`; validates if already set |\n| `beforeDelete` | Validates instance.tenantId matches context |\n| `beforeQuery` | Enforces raw SQL policy on tenant-scoped classes (`throw`/`warn`/`allow`) |\n| `afterSave` | Emits `directory.<class>.created`/`updated` via `dispatchBus` for configured `directoryClasses` |\n| `afterDelete` | Emits `directory.<class>.deleted` via `dispatchBus` for configured `directoryClasses` |\n\nMismatches throw `TenantIsolationError`. Missing required context throws `TenantContextError`.\n\n**Optional-mode reads with no context pass through UNFILTERED at the interceptor.** That is intentional for trusted/admin call paths, but it means the interceptor alone does not protect a tenant-scoped model exposed as `@smrt({ api: { public } })`: an anonymous HTTP read has no context, so the interceptor would return every tenant's rows. The generated REST + SvelteKit read routes close this by injecting a `{ tenantId: null }` filter when tenancy is enabled but no context is active, so public/anonymous reads fail closed to **global (NULL-tenant) rows only** — mirroring the dispatch resolver's *enforced, no active tenant → global rows only* rule (#1782). Authenticated reads still scope to the caller's tenant via the interceptor.\n\n## Read-Path Coverage (#2365)\n\nTenant scoping is a whole-path property — every read path is interceptor-aware,\nnot only collection list/get:\n\n- **Get-by-slug**: `collection.get('<slug>')` works under a tenant context. The\n interceptor resolves string filters with core's `resolveGetStringFilter()`\n instead of assuming they are ids. Any custom `beforeGet` interceptor that\n rewrites a string filter must do the same.\n- **Hydration and identity**: `new Model({ id | slug }).initialize()`,\n `loadFromId()`, `loadFromSlug()`, `getSavedId()` and `getId()` run their\n filters through the `beforeGet` pipeline, so constructor hydration cannot\n read another tenant's row and `getId()` can never adopt another tenant's\n same-slug row id (which would steer a later `save()` onto the foreign row).\n Required-mode classes fail closed (`TenantContextError`) when hydrated\n outside a tenant context; `withSystemContext()` / super-admin bypass remain\n the explicit cross-tenant paths.\n- **Vector search**: `semanticSearch()` / `findSimilarToEmbedding()` restrict\n candidates to the tenant's rows BEFORE top-K ranking (the tenant predicate is\n resolved through the `beforeList` pipeline), so results are never starved by\n — and similarity ranks never leak — other tenants' content.\n- **Collection memory**: `remember()`/`recall()`/`recallAll()`/`forget()` on a\n tenant-scoped collection key `_smrt_contexts.owner_id` per tenant\n (`__collection__:<tenantId>`) under an active tenant context. Isolation is\n strict: tenant-keyed memory never falls back to the shared `__collection__`\n key, and memory learned outside a tenant context is invisible inside one.\n Two edge semantics to know: under `withSuperAdminBypass()` reads skip\n filtering but memory still keys to the active tenant (scoped tighter, not a\n leak), and an empty-string tenant id resolves to the shared key (an\n empty-string tenant is a misconfiguration — real tenant ids are uuids).\n\n## Registration — Two Patterns\n\n```typescript\n// Pattern 1: Tenancy decorator\n@TenantScoped({ mode: 'optional' })\nclass Doc extends SmrtObject { @tenantId({ nullable: true }) tenantId: string | null = null; }\n\n// Pattern 2: Core decorator (tenancy package reads this too)\n@smrt({ tenantScoped: { mode: 'optional' } })\nclass Doc extends SmrtObject { tenantId: string | null = null; }\n```\n\nThe exported `registerTenantScopedClass()` also accepts a simple or qualified\nstring selector for third-party classes and test doubles. A simple selector is\nbound to the one exact core constructor when that name is uniquely resolvable;\nit may be registered before core. Once bound, a later same-name package peer\ndoes not inherit its policy. If two core classes exist before the simple\nselector can bind, operations fail closed until the caller registers an exact\nqualified name (for example, `@package/name:Doc`). No lookup strips a namespace\nto guess an owner. If core clears a bound constructor and reuses its qualified\nname, unregister and register the selector again; it will not silently transfer\npolicy to the replacement constructor.\n\nModes: `'required'` (default — throws without context) or `'optional'` (passes through if no context).\n\n## Adapters\n\n- **Express**: `createExpressMiddleware()` — uses `enterTenantContext()` (not withTenant, because middleware returns before handlers run)\n- **SvelteKit**: `createSvelteKitHandle()` — stores context in `event.locals`\n- **CLI**: `createCliContext()` — `run()`, `runWithTenant()`, `runAsSystem()`, `runAsSuperAdmin()`\n\n## Super Admin Bypass\n\n`withSuperAdminBypass()` keeps tenant context but disables auto-filtering. Different from `withSystemContext()` which removes context entirely.\n\n## Gotchas\n\n- **Context lost in callbacks**: `setTimeout(() => getTenantId(), 100)` → undefined. Fix: `TenantContext.bind(fn)`\n- **Nested contexts override**: inner `withTenant()` overrides outer; restores on exit\n- **Auto-populate only if empty**: if tenantId already set, interceptor validates (not overwrites)\n- **Isolation checked at query time**: `list({ where: { tenantId: 'other' } })` throws immediately\n- **Testing**: `resetTenancy()` + `setupTestTenancy()` in beforeEach; `testTenantIsolation()` helper\n- **Natural keys are per tenant (smrt#2360)**: a tenant-scoped class with no explicit `conflictColumns` upserts on, and indexes, `(tenant_id, slug, context[, _meta_type])` — `save()` from tenant B with tenant A's slug is a second row, never an overwrite; within a tenant the natural key still dedups; NULL-tenant (`optional` mode, no context) rows dedup among themselves through the SDK's null-aware upsert but not through the index (NULLs are distinct), so raw SQL `ON CONFLICT (slug, context…)` on such a table no longer binds — use `WHERE NOT EXISTS`, and on PostgreSQL an advisory lock, as `ProfileTypeCollection.getOrCreateGlobalBySlug()` does. Core resolves tenant schema policy in order: explicit `@smrt`, manifest (including an omitted `tenantScoped`), exact-constructor `@TenantScoped()` reconciliation, then marked-field fallback; it never reads the standalone tenancy registry. This keeps schema and upsert behavior aligned before `enableTenancy()` runs. Rollout: deploy the code and `smrt db:migrate` together (neither version's create works against the other's index), and backfill `tenant_id` on legacy NULL-tenant rows first — a tenant-context save no longer adopts a `(NULL, slug)` row, it inserts beside it and that tenant stops seeing the legacy one (details in `packages/core/agents/schema-paths.md`).\n- **Runtime-only registration is audited, and ownership keys follow the field**: a class registered with `registerTenantScopedClass()` whose manifest declares no tenancy still gets the tenant-led default key when it has a `tenantId` field (core infers OWNERSHIP from the field, never the read policy). `auditTenantScopedRegistrations()` reports per selector: `missing_tenant_field` (error — neither the model nor a registered ancestor declares the field; an STI child registered before its manifest loads inherits it from the parent, #3623), `natural_key_not_tenant_scoped` (warning — an explicit key omits the tenant column, so a second tenant's same-key save is refused with `TENANT_ISOLATION_VIOLATION`), `undeclared_tenant_scope` (info). Registration (or first use, when it precedes core) logs the error/warning findings once. Core's save guard, not the interceptor, is what stops a tenant-filtered lookup from upserting onto another tenant's row (see core `agents/schema-paths.md`).\n- **Manifest/runtime mismatch fails closed (smrt#2763)**: if a cached manifest omits or sets `tenantScoped: false` while the exact runtime constructor carries `@TenantScoped()`, registration is rejected; regenerate the manifest. A rejected late manifest or conflicting promotion preserves a previously valid scoped registration. Applying `@TenantScoped()` to an already-global manifest registration that omits or disables tenancy instead marks that class unavailable to conflict-key, schema, and tenancy interceptor operations until a corrected manifest explicitly declares tenancy. This prevents a global unique key from conflicting with tenant-enforced reads or writes.\n\n## Known exceptions to monorepo standards\n\n- **`serializeInstance()` in `src/interceptor.ts` calls `instance.toJSON()` directly** (standards.md §7 forbids this in favor of `transformJSON()`). The interceptor must serialize arbitrary instances handed to it — including workspace stubs and plain-object test doubles whose classes may not extend `SmrtObject` and therefore have no `transformJSON()` hook. The call is duck-typed and falls back to manual key iteration when `toJSON` is absent. See the inline comment at the call site for the full rationale.\n",
|
|
153
|
+
"browser": {
|
|
154
|
+
"status": "server-only",
|
|
155
|
+
"issues": [
|
|
156
|
+
"#3624"
|
|
157
|
+
],
|
|
158
|
+
"reason": "node:async_hooks"
|
|
159
|
+
}
|
|
153
160
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-tenancy",
|
|
3
|
-
"version": "0.55.
|
|
3
|
+
"version": "0.55.8",
|
|
4
4
|
"smrtJsdoc": "strict",
|
|
5
5
|
"description": "Production-ready multi-tenancy framework for SMRT with automatic tenant isolation and enforcement",
|
|
6
6
|
"type": "module",
|
|
@@ -42,12 +42,12 @@
|
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@happyvertical/logger": "^0.
|
|
46
|
-
"@happyvertical/smrt-core": "0.55.
|
|
47
|
-
"@happyvertical/smrt-types": "0.55.
|
|
48
|
-
"@happyvertical/smrt-ui": "0.55.
|
|
49
|
-
"@happyvertical/sql": "^0.
|
|
50
|
-
"@happyvertical/utils": "^0.
|
|
45
|
+
"@happyvertical/logger": "^0.103.1",
|
|
46
|
+
"@happyvertical/smrt-core": "0.55.8",
|
|
47
|
+
"@happyvertical/smrt-types": "0.55.8",
|
|
48
|
+
"@happyvertical/smrt-ui": "0.55.8",
|
|
49
|
+
"@happyvertical/sql": "^0.103.1",
|
|
50
|
+
"@happyvertical/utils": "^0.103.1"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
53
|
"svelte": "^5.57.0"
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
}
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@happyvertical/smrt-vitest": "0.55.
|
|
61
|
+
"@happyvertical/smrt-vitest": "0.55.8",
|
|
62
62
|
"@sveltejs/package": "^2.5.8",
|
|
63
63
|
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
|
64
64
|
"@types/node": "24.13.6",
|