@happyvertical/smrt-tenancy 0.55.2 → 0.55.3

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 CHANGED
@@ -126,7 +126,7 @@ Modes: `'required'` (default — throws without context) or `'optional'` (passes
126
126
  - **Isolation checked at query time**: `list({ where: { tenantId: 'other' } })` throws immediately
127
127
  - **Testing**: `resetTenancy()` + `setupTestTenancy()` in beforeEach; `testTenantIsolation()` helper
128
128
  - **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`).
129
- - **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 — the model has no such field), `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`).
129
+ - **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`).
130
130
  - **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.
131
131
 
132
132
  ## Known exceptions to monorepo standards
@@ -167,12 +167,24 @@ function resolveSelectorClass(selector) {
167
167
  const matches = ObjectRegistry.findClassesByName(selector);
168
168
  return matches.length === 1 ? matches[0] : void 0;
169
169
  }
170
+ function declaresFieldInChain(registered, fieldName) {
171
+ if (registered.fields.has(fieldName)) return true;
172
+ if (registered.inheritedFields?.has(fieldName)) return true;
173
+ const visited = /* @__PURE__ */ new Set([registered.constructor]);
174
+ let ctor = Object.getPrototypeOf(registered.constructor);
175
+ while (typeof ctor === "function" && !visited.has(ctor)) {
176
+ visited.add(ctor);
177
+ if (ObjectRegistry.getClassByConstructor(ctor)?.fields.has(fieldName)) return true;
178
+ ctor = Object.getPrototypeOf(ctor);
179
+ }
180
+ return false;
181
+ }
170
182
  function auditSelector(selector, config) {
171
183
  const registered = resolveSelectorClass(selector);
172
184
  if (!registered) return [];
173
185
  const className = registered.qualifiedName || registered.name;
174
186
  const findings = [];
175
- if (!registered.fields.has(config.field)) {
187
+ if (!declaresFieldInChain(registered, config.field)) {
176
188
  findings.push({
177
189
  selector,
178
190
  className,
@@ -616,4 +628,4 @@ async function assertTenantIsolationViolation(fn, messageContains) {
616
628
  //#endregion
617
629
  export { unregisterTenantScopedClass as C, registerTenantScopedConstructor as S, clearTenantScopedRegistry as _, setupTestTenancy as a, isTenantScopedClass as b, disableTenancy as c, queryGlobal as d, queryWithGlobals as f, auditTenantScopedRegistrations as g, isTenancyEnabled as h, resetTenancy as i, enableTenancy as l, runTenantScopedEntryPoint as m, assertTenantIsolationViolation as n, testTenantIsolation as o, withTenantGlobalRead as p, createTestTenantContext as r, createTenantInterceptor as s, assertTenantContextRequired as t, assertTenantReadAllowed as u, getAllTenantScopedClasses as v, registerTenantScopedClass as x, getTenantScopedConfig as y };
618
630
 
619
- //# sourceMappingURL=testing-Bp23qg6E.js.map
631
+ //# sourceMappingURL=testing-Cs_C_Q-y.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing-Cs_C_Q-y.js","names":["tenantField","where"],"sources":["../../src/registry.ts","../../src/enabled-state.ts","../../src/entry-point.ts","../../src/tenant-global-read-scope.ts","../../src/tenant-global-queries.ts","../../src/interceptor.ts","../../src/testing.ts"],"sourcesContent":["/**\n * Tenant-Scoped Class Registry\n *\n * Tracks which classes are tenant-scoped and their configuration.\n * Used by the interceptor to determine how to handle operations.\n *\n * This registry supports two patterns:\n * 1. @TenantScoped() decorator + tenantId field (original pattern)\n * 2. @smrt({ tenantScoped: true }) in smrt-core (Issue #688 pattern)\n *\n * Both patterns are automatically recognized by the interceptor.\n *\n * @see https://github.com/happyvertical/smrt/issues/675\n * @see https://github.com/happyvertical/smrt/issues/688\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport { ObjectRegistry, toSnakeCase } from '@happyvertical/smrt-core';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Resolved tenancy configuration for a single class, as stored in the registry.\n *\n * Every field has a concrete (non-optional) value — defaults are applied by\n * `registerTenantScopedClass()` when the class is registered via `@TenantScoped()`.\n *\n * @see TenantScopedOptions\n * @see registerTenantScopedClass\n */\nexport interface TenantScopedConfig {\n /**\n * Tenancy mode for this class\n * - 'required': Must have tenant context for all operations\n * - 'optional': Works with or without tenant context\n * @default 'required'\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\n */\n allowSuperAdminBypass: boolean;\n}\n\nconst DEFAULT_CONFIG: TenantScopedConfig = {\n mode: 'required',\n field: 'tenantId',\n autoFilter: true,\n autoPopulate: true,\n allowSuperAdminBypass: false,\n};\n\n// Registry snapshot exposed by getAllTenantScopedClasses().\nconst tenantScopedClasses = new Map<string, TenantScopedConfig>();\n\n// Direct callers select a class by string. Keep simple and qualified selectors\n// separate: a simple selector may be bound only after core proves that exactly\n// one constructor owns that name.\nconst directSimpleRegistrations = new Map<string, TenantScopedConfig>();\nconst directQualifiedRegistrations = new Map<string, TenantScopedConfig>();\nconst directSimpleBindings = new Map<\n string,\n { qualifiedName: string; constructor: Function }\n>();\n\n// Decorators retain a simple mirror only until core has registered their\n// authoritative qualified policy. Qualified runtime resolution always defers\n// to core, preserving manifest and explicit-@smrt precedence.\nconst unregisteredDecoratorRegistrations = new Map<\n string,\n TenantScopedConfig\n>();\n\n// Selectors already audited (and warned about) since their last registration.\nconst auditedSelectors = new Set<string>();\n\nfunction isQualifiedClassName(className: string): boolean {\n return className.includes(':');\n}\n\nfunction isCurrentDirectSimpleBinding(binding: {\n qualifiedName: string;\n constructor: Function;\n}): boolean {\n return (\n ObjectRegistry.getClassByQualifiedName(binding.qualifiedName)\n ?.constructor === binding.constructor\n );\n}\n\nfunction bindDirectSimpleRegistration(className: string): void {\n const config = directSimpleRegistrations.get(className);\n if (!config || directSimpleBindings.has(className)) return;\n\n const matches = ObjectRegistry.findClassesByName(className);\n if (matches.length !== 1 || !matches[0].qualifiedName) return;\n\n directSimpleBindings.set(className, {\n qualifiedName: matches[0].qualifiedName,\n constructor: matches[0].constructor,\n });\n warnOnRegistrationMismatch(className);\n}\n\nfunction getDirectSimpleRegistration(\n className: string,\n): TenantScopedConfig | undefined {\n const config = directSimpleRegistrations.get(className);\n if (!config) return undefined;\n\n const binding = directSimpleBindings.get(className);\n if (binding && !isCurrentDirectSimpleBinding(binding)) {\n throw new Error(\n `Stale tenant-scoped class registration '${className}'; ` +\n 'unregister and register it again for the current constructor.',\n );\n }\n\n const matches = ObjectRegistry.findClassesByName(className);\n if (matches.length > 1) {\n throw new Error(\n `Ambiguous tenant-scoped class registration '${className}'; ` +\n 'register an explicit qualified class name instead.',\n );\n }\n\n return config;\n}\n\n/** @internal Used by TenantScoped; direct callers must use the string API. */\nexport function registerTenantScopedConstructor(\n target: Function,\n config: Partial<TenantScopedConfig> = {},\n): void {\n const resolved = { ...DEFAULT_CONFIG, ...config };\n unregisteredDecoratorRegistrations.set(target.name, resolved);\n tenantScopedClasses.set(target.name, resolved);\n}\n\n/**\n * Register a class as tenant-scoped with the given configuration.\n *\n * Call this directly when you cannot use decorators (e.g., third-party classes\n * or plain objects in tests). Defaults from `DEFAULT_CONFIG` are merged over\n * any omitted options. `@TenantScoped()` has its own constructor-aware mirror\n * and reconciles its authoritative policy in core.\n *\n * A simple selector binds to its exact core constructor when one owner is\n * uniquely resolvable, including when registration happens before core. Once\n * bound it remains attached to that constructor if a same-name peer appears.\n * If core clears that constructor and reuses its qualified name, the selector\n * fails closed until the caller explicitly unregisters and re-registers it.\n * If ownership is ambiguous before binding, interception fails closed until a\n * caller registers an explicit qualified selector. Calling this again for the\n * same selector overwrites that selector's previous entry.\n *\n * @param className - A simple class name (e.g., `'Document'`) or exact core\n * qualified name (e.g., `'@package/name:Document'`).\n * @param config - Partial tenancy configuration; omitted fields receive defaults.\n *\n * @example\n * ```typescript\n * // Manually register a class (e.g., for testing)\n * registerTenantScopedClass('Document', { mode: 'optional' });\n * ```\n *\n * @see TenantScoped\n * @see unregisterTenantScopedClass\n */\nexport function registerTenantScopedClass(\n className: string,\n config: Partial<TenantScopedConfig> = {},\n): void {\n const resolved = {\n ...DEFAULT_CONFIG,\n ...config,\n };\n tenantScopedClasses.set(className, resolved);\n\n auditedSelectors.delete(className);\n if (isQualifiedClassName(className)) {\n directQualifiedRegistrations.set(className, resolved);\n warnOnRegistrationMismatch(className);\n return;\n }\n\n directSimpleRegistrations.set(className, resolved);\n const existingBinding = directSimpleBindings.get(className);\n if (existingBinding && !isCurrentDirectSimpleBinding(existingBinding)) {\n directSimpleBindings.delete(className);\n }\n bindDirectSimpleRegistration(className);\n warnOnRegistrationMismatch(className);\n}\n\n/**\n * Remove a class from the tenant-scoped registry.\n *\n * Primarily intended for test teardown — use `clearTenantScopedRegistry()` to\n * reset the entire registry at once.\n *\n * @param className - The class name to remove (e.g., `'Document'`).\n *\n * @see clearTenantScopedRegistry\n * @see registerTenantScopedClass\n */\nexport function unregisterTenantScopedClass(className: string): void {\n auditedSelectors.delete(className);\n tenantScopedClasses.delete(className);\n if (isQualifiedClassName(className)) {\n directQualifiedRegistrations.delete(className);\n return;\n }\n\n directSimpleRegistrations.delete(className);\n directSimpleBindings.delete(className);\n unregisteredDecoratorRegistrations.delete(className);\n}\n\n/**\n * Return a shallow copy of a config so callers can never mutate the stored\n * registration. The same backing object is shared by the base and every\n * inheriting descendant, so handing out the reference would let an accidental\n * caller mutation silently corrupt the base (and all children). (#1598 review)\n */\nfunction cloneConfig(config: TenantScopedConfig): TenantScopedConfig {\n return { ...config };\n}\n\n/**\n * Resolve a class's OWN tenancy configuration — no STI inheritance, EXACT name\n * match only.\n *\n * Checks the registration mechanisms in order: an explicit direct selector,\n * then core's declared policy. `@TenantScoped()` reconciles its policy in core;\n * its simple-name mirror is used only for unregistered test doubles.\n *\n * Lookups are by exact name only — no simple-name fallback — so a qualified\n * lookup (e.g. `@happyvertical/smrt-affiliates:Payout`, explicitly not scoped)\n * can never strip its namespace and match a same-simple-name scoped class in\n * another package (e.g. `@happyvertical/smrt-commerce:Payout`). (#1598 review)\n */\nfunction getDirectTenantScopedConfig(\n className: string,\n): TenantScopedConfig | undefined {\n const direct = getDeclaredTenantScopedConfig(className);\n const qualifiedName = ObjectRegistry.getClass(className)?.qualifiedName;\n if (\n direct &&\n qualifiedName &&\n ObjectRegistry.getRuntimeOverride(qualifiedName)?.tenancy\n ) {\n return { ...direct, mode: 'required' };\n }\n return direct;\n}\n\nfunction getDeclaredTenantScopedConfig(\n className: string,\n): TenantScopedConfig | undefined {\n // Core marks caught silent-manifest/runtime-decorator conflicts invalid.\n // Check before the simple-name decorator mirror so every identity path\n // fails closed rather than falling through to an unscoped operation.\n ObjectRegistry.assertTenantScopedRegistrationValid(className);\n // 1. Explicit direct qualified selector.\n const directQualified = directQualifiedRegistrations.get(className);\n if (directQualified) {\n // A qualified selector registered before core learned the class is\n // audited on first use instead.\n if (!auditedSelectors.has(className)) warnOnRegistrationMismatch(className);\n return cloneConfig(directQualified);\n }\n\n const registered = isQualifiedClassName(className)\n ? ObjectRegistry.getClassByQualifiedName(className)\n : ObjectRegistry.getClass(className);\n\n // A direct simple selector can bind lazily after registration-before-core.\n // It is never inferred from a qualified name when more than one core class\n // owns that simple name.\n if (registered) {\n const simple = registered.name;\n const bound = directSimpleBindings.get(simple);\n if (bound) {\n if (\n bound.qualifiedName === className &&\n bound.constructor === registered.constructor\n ) {\n return cloneConfig(directSimpleRegistrations.get(simple)!);\n }\n // Core can clear and re-register a qualified name with a different\n // constructor. Never transfer the old selector binding to it: the caller\n // must explicitly unregister/re-register after that lifecycle reset.\n if (!isCurrentDirectSimpleBinding(bound)) {\n throw new Error(\n `Stale tenant-scoped class registration '${simple}'; ` +\n 'unregister and register it again for the current constructor.',\n );\n }\n }\n if (!bound && directSimpleRegistrations.has(simple)) {\n const matches = ObjectRegistry.findClassesByName(simple);\n if (matches.length > 1) {\n throw new Error(\n `Ambiguous tenant-scoped class registration '${simple}'; ` +\n 'register an explicit qualified class name instead.',\n );\n }\n bindDirectSimpleRegistration(simple);\n const rebound = directSimpleBindings.get(simple);\n if (\n rebound?.qualifiedName === className &&\n rebound.constructor === registered.constructor\n ) {\n return cloneConfig(directSimpleRegistrations.get(simple)!);\n }\n }\n }\n\n if (!isQualifiedClassName(className)) {\n // Explicit direct selectors retain their established precedence. For\n // plain-object/test-double paths this is the historical simple-selector\n // fallback, subject to the existing ambiguity checks.\n const directSimple = getDirectSimpleRegistration(className);\n if (directSimple) return cloneConfig(directSimple);\n }\n\n // 2. Core registry (@smrt({ tenantScoped: true }) pattern - Issue #688).\n // findClass() resolves qualified names package-safely, so this branch is\n // already disambiguated.\n const coreConfig = ObjectRegistry.getTenantScopedConfig(className);\n if (coreConfig) {\n // Convert core config to TenantScopedConfig format\n return {\n mode: coreConfig.mode,\n field: coreConfig.field,\n autoFilter: coreConfig.autoFilter,\n autoPopulate: coreConfig.autoPopulate,\n allowSuperAdminBypass: coreConfig.allowSuperAdminBypass,\n };\n }\n\n // A decorator mirror is only authoritative when core has no registration.\n // A registered unqualified consumer still has a canonical simple identity.\n if (!registered && !isQualifiedClassName(className)) {\n const decoratorConfig = unregisteredDecoratorRegistrations.get(className);\n if (decoratorConfig) return cloneConfig(decoratorConfig);\n }\n\n return undefined;\n}\n\n/**\n * Resolve tenancy configuration inherited from an STI/ancestor class.\n *\n * `@TenantScoped` (and `@smrt({ tenantScoped })`) register ONLY the exact class\n * decorated — recognition does NOT propagate to subclasses. Before #1596 this\n * meant an STI child with its own collection (the child is the collection's\n * `_itemClass`) was treated as non-tenant-scoped at runtime: the interceptor\n * skipped tenant filtering on its `list()`/`get()` (cross-tenant reads), skipped\n * tenant population in `beforeSave`, and skipped the raw-SQL policy. Manual\n * re-declaration on every child was the fragile pattern that already bit images\n * (#1407) and messages.\n *\n * We now walk the STI inheritance chain so any descendant of a tenant-scoped\n * base is recognized automatically and inherits the base's config. A subclass\n * of a tenant-scoped class is always itself tenant-scoped — there is no safe\n * reason for it to opt out — so the walk intentionally covers any inheritance\n * (the motivating leak is STI child collections, but this is correct for CTI\n * hierarchies too).\n *\n * Ancestors are walked from nearest-to-self toward the root, returning the\n * first tenant-scoped ancestor's config so a closer ancestor wins. The class's\n * OWN declaration is resolved by the direct lookup in `getTenantScopedConfig`\n * and always takes precedence over anything inherited here.\n */\nfunction getInheritedTenantScopedConfig(\n className: string,\n): TenantScopedConfig | undefined {\n // getInheritanceChain returns [root, ..., self] (qualified names where the\n // class has package context). It is cached by core and only reached here when\n // the direct lookup misses, so the per-call cost on non-tenant classes is a\n // cache hit plus this short loop. Returns [] for unregistered classes.\n const chain = ObjectRegistry.getInheritanceChain(className);\n // chain[length - 1] is the class itself (already covered by the direct\n // lookup); walk its ancestors from nearest to root.\n for (let i = chain.length - 2; i >= 0; i--) {\n const ancestor = chain[i];\n\n // Exact, package-safe match first — covers `@smrt({ tenantScoped })` bases\n // (resolved through the core registry by qualified name) and any class\n // whose @TenantScoped key matches the chain entry verbatim.\n const direct = getDirectTenantScopedConfig(ancestor);\n if (direct) {\n return direct;\n }\n\n // Do not bridge a qualified ancestor back to a simple registration here.\n // The exact lookup above reaches the core declaration reconciled by the\n // decorator. Stripping would let an unrelated same-name peer lend its\n // direct or decorator policy to this inheritance chain.\n }\n return undefined;\n}\n\n/**\n * Retrieve the resolved tenancy configuration for a class.\n *\n * Resolution order:\n * 1. The class's OWN declaration — local `@TenantScoped()` registry first, then\n * the core `@smrt({ tenantScoped: true })` registry.\n * 2. STI inheritance — the nearest tenant-scoped ancestor's config (#1596).\n *\n * A class that declares its own tenancy never reaches step 2, so an explicit\n * child `@TenantScoped` always overrides the inherited base config.\n *\n * @param className - The class name to look up.\n * @returns The `TenantScopedConfig` if the class is tenant-scoped directly or\n * by inheritance, or `undefined` if it is not.\n *\n * @see isTenantScopedClass\n * @see getAllTenantScopedClasses\n */\nexport function getTenantScopedConfig(\n className: string,\n): TenantScopedConfig | undefined {\n // A class's own @TenantScoped / @smrt({ tenantScoped }) declaration wins.\n const direct = getDirectTenantScopedConfig(className);\n if (direct) return direct;\n // Otherwise inherit recognition from a tenant-scoped STI ancestor (#1596).\n return getInheritedTenantScopedConfig(className);\n}\n\n/**\n * Tell core which column records a row's owner for every tenant-scoped class,\n * including runtime registrations on a custom field\n * (`registerTenantScopedClass('Team', { field: 'organizationId' })`) that the\n * manifest knows nothing about. Core's natural-key save guard compares these\n * columns, so a save whose key collides with another organization's row is\n * refused (`TENANT_ISOLATION_VIOLATION`) instead of adopting it. Resolved on\n * every lookup, so selectors that bind later or are unregistered are honoured.\n */\nObjectRegistry.registerOwnershipColumnSource((qualifiedName) => {\n const config = getTenantScopedConfig(qualifiedName);\n return config ? [toSnakeCase(config.field)] : undefined;\n});\n\n/**\n * Return `true` if the named class is tenant-scoped — directly (via\n * `@TenantScoped()` / `@smrt({ tenantScoped: true })`) or by inheriting from a\n * tenant-scoped STI ancestor (#1596).\n *\n * @param className - The class name to look up (e.g., `'Document'`).\n * @returns `true` if the class is tenant-scoped by any mechanism.\n *\n * @see getTenantScopedConfig\n * @see registerTenantScopedClass\n */\nexport function isTenantScopedClass(className: string): boolean {\n return getTenantScopedConfig(className) !== undefined;\n}\n\n/**\n * Return a snapshot of all classes registered via `@TenantScoped()`.\n *\n * Returns a new `Map` so mutations to the returned value do not affect the\n * internal registry. Note that classes registered only through the core\n * `ObjectRegistry` (`@smrt({ tenantScoped: true })`) are **not** included in\n * this map.\n *\n * @returns A copy of the local tenant-scoped class registry, keyed by class name.\n *\n * @see isTenantScopedClass\n * @see getTenantScopedConfig\n */\nexport function getAllTenantScopedClasses(): Map<string, TenantScopedConfig> {\n return new Map(tenantScopedClasses);\n}\n\n/**\n * Remove all entries from the local tenant-scoped class registry.\n *\n * Intended for test teardown via `resetTenancy()`. Does not affect\n * registrations held by the core `ObjectRegistry`.\n *\n * @see resetTenancy\n * @see unregisterTenantScopedClass\n */\nexport function clearTenantScopedRegistry(): void {\n auditedSelectors.clear();\n tenantScopedClasses.clear();\n directSimpleRegistrations.clear();\n directQualifiedRegistrations.clear();\n directSimpleBindings.clear();\n unregisteredDecoratorRegistrations.clear();\n}\n\n/**\n * One finding from {@link auditTenantScopedRegistrations}.\n *\n * - `missing_tenant_field` (`error`): the class has no field named by the\n * registration, so the interceptor filters and populates a column that does\n * not exist in the model's manifest/schema.\n * - `natural_key_not_tenant_scoped` (`warning`): the class's upsert conflict\n * target (and so its unique index) omits the tenant column — its natural key\n * is unique ACROSS tenants. The interceptor hides other tenants' rows from\n * lookups, so a second tenant's same-key save collides with a row it cannot\n * see; core refuses that save (`TENANT_ISOLATION_VIOLATION`) instead of\n * overwriting the other tenant's row, but the second tenant still cannot\n * store the key.\n * - `undeclared_tenant_scope` (`info`): tenant scope exists only as this\n * runtime registration; the model's manifest declares no tenancy. Its\n * default natural key is still tenant-led when it has a `tenantId` field,\n * but generated surfaces, knowledge, and schema contracts cannot see the\n * read policy. Declaring `@smrt({ tenantScoped })` makes it visible.\n */\nexport interface TenantRegistrationFinding {\n /** The selector passed to `registerTenantScopedClass()`. */\n selector: string;\n /** The core class it resolved to (qualified name when known). */\n className: string;\n kind:\n | 'missing_tenant_field'\n | 'natural_key_not_tenant_scoped'\n | 'undeclared_tenant_scope';\n severity: 'error' | 'warning' | 'info';\n message: string;\n}\n\nfunction resolveSelectorClass(selector: string) {\n if (isQualifiedClassName(selector)) {\n return ObjectRegistry.getClassByQualifiedName(selector);\n }\n const bound = directSimpleBindings.get(selector);\n if (bound) return ObjectRegistry.getClassByQualifiedName(bound.qualifiedName);\n const matches = ObjectRegistry.findClassesByName(selector);\n return matches.length === 1 ? matches[0] : undefined;\n}\n\ntype RegisteredSmrtClass = NonNullable<ReturnType<typeof resolveSelectorClass>>;\n\n/**\n * Whether the class or one of its registered ancestors declares `fieldName`.\n *\n * A class's own field map holds only its own decorator fields until its\n * manifest entry loads; an STI child registered first (a bundled server) has\n * its inherited tenant field on the registered parent only (#3623). Walk the\n * constructor's prototype chain through public registry lookups — read-only,\n * unlike `getInheritanceChain()`, which caches whatever partial chain exists\n * at registration time.\n */\nfunction declaresFieldInChain(\n registered: RegisteredSmrtClass,\n fieldName: string,\n): boolean {\n if (registered.fields.has(fieldName)) return true;\n if (registered.inheritedFields?.has(fieldName)) return true;\n const visited = new Set<unknown>([registered.constructor]);\n let ctor: unknown = Object.getPrototypeOf(registered.constructor);\n while (typeof ctor === 'function' && !visited.has(ctor)) {\n visited.add(ctor);\n const ancestor = ObjectRegistry.getClassByConstructor(\n ctor as RegisteredSmrtClass['constructor'],\n );\n if (ancestor?.fields.has(fieldName)) return true;\n ctor = Object.getPrototypeOf(ctor);\n }\n return false;\n}\n\nfunction auditSelector(\n selector: string,\n config: TenantScopedConfig,\n): TenantRegistrationFinding[] {\n const registered = resolveSelectorClass(selector);\n if (!registered) return [];\n const className = registered.qualifiedName || registered.name;\n const findings: TenantRegistrationFinding[] = [];\n\n if (!declaresFieldInChain(registered, config.field)) {\n findings.push({\n selector,\n className,\n kind: 'missing_tenant_field',\n severity: 'error',\n message:\n `${className} is registered tenant-scoped on field '${config.field}', ` +\n 'but the model declares no such field. Tenant filtering and population ' +\n 'target a column its manifest and schema do not have. Add the field ' +\n '(or declare `@smrt({ tenantScoped })`) and regenerate the manifest.',\n });\n return findings;\n }\n\n if (!ObjectRegistry.getTenantScopedConfig(className)) {\n findings.push({\n selector,\n className,\n kind: 'undeclared_tenant_scope',\n severity: 'info',\n message:\n `${className} is tenant-scoped only by runtime registration; its ` +\n 'manifest declares no tenancy. Declare `@smrt({ tenantScoped })` so ' +\n 'schema, generated surfaces and knowledge agree with the read policy.',\n });\n }\n\n const tenantColumn = toSnakeCase(config.field);\n const conflictColumns = ObjectRegistry.getConflictColumns(className);\n const keyedOnPrimaryKey =\n conflictColumns.length === 1 && conflictColumns[0] === 'id';\n if (!keyedOnPrimaryKey && !conflictColumns.includes(tenantColumn)) {\n findings.push({\n selector,\n className,\n kind: 'natural_key_not_tenant_scoped',\n severity: 'warning',\n message:\n `${className} is registered tenant-scoped, but its natural key ` +\n `(${conflictColumns.join(', ')}) omits '${tenantColumn}', so it is ` +\n 'unique across tenants: a second tenant cannot store a key another ' +\n 'tenant already uses. A new object saved on such a key is refused ' +\n '(TENANT_ISOLATION_VIOLATION) when the row it collides with has a ' +\n `different '${tenantColumn}'; one whose slug was derived from its ` +\n 'name moves to a free slug instead. ' +\n `Include '${tenantColumn}' in the conflict columns (or drop the ` +\n 'explicit conflictColumns) and run `smrt db:migrate`.',\n });\n }\n return findings;\n}\n\n/**\n * Audit every string-selector registration (`registerTenantScopedClass()`)\n * against the core model it resolves to. Selectors whose class core has not\n * registered yet are skipped (they are audited when they bind).\n *\n * Registration also runs this per selector and logs `error`/`warning`\n * findings once, so a mismatch surfaces at startup; call this directly for a\n * complete report (for example from a health check or a test).\n */\nexport function auditTenantScopedRegistrations(): TenantRegistrationFinding[] {\n const findings: TenantRegistrationFinding[] = [];\n for (const [selector, config] of directQualifiedRegistrations) {\n findings.push(...auditSelector(selector, config));\n }\n for (const [selector, config] of directSimpleRegistrations) {\n findings.push(...auditSelector(selector, config));\n }\n return findings;\n}\n\nfunction warnOnRegistrationMismatch(selector: string): void {\n if (auditedSelectors.has(selector)) return;\n const config =\n directQualifiedRegistrations.get(selector) ??\n directSimpleRegistrations.get(selector);\n if (!config) return;\n let findings: TenantRegistrationFinding[];\n try {\n findings = auditSelector(selector, config);\n } catch {\n // Ambiguous or stale identities fail closed on use; the audit stays quiet.\n return;\n }\n if (!resolveSelectorClass(selector)) return; // not in core yet; retry later\n auditedSelectors.add(selector);\n for (const finding of findings) {\n if (finding.severity === 'info') continue;\n logger.warn(`[smrt-tenancy] ${finding.message}`);\n }\n}\n","/**\n * Shared tenancy-enabled flag.\n *\n * Holds the single boolean toggled by `enableTenancy()` / `disableTenancy()`.\n * It lives in its own leaf module (importing nothing from the package) so that\n * both `interceptor.ts` and `entry-point.ts` can read it without forming a\n * circular import: `interceptor.ts` imports `runTenantScopedEntryPoint` from\n * `entry-point.ts`, and `entry-point.ts` needs the enabled flag — routing the\n * flag through here keeps that dependency one-directional.\n */\n\nlet enabled = false;\n\n/**\n * Set the global tenancy-enabled flag. Internal — called by `enableTenancy()` /\n * `disableTenancy()` in `interceptor.ts`.\n *\n * @param value - `true` to mark tenancy enabled, `false` to clear it.\n */\nexport function setTenancyEnabled(value: boolean): void {\n enabled = value;\n}\n\n/**\n * Return `true` if tenant enforcement is currently active.\n *\n * @returns Whether `enableTenancy()` has been called without a later\n * `disableTenancy()`.\n */\nexport function isTenancyEnabled(): boolean {\n return enabled;\n}\n","/**\n * Fail-closed tenant-context establishment for non-web entry points (#1554).\n *\n * The SvelteKit/Express adapters establish tenant context from the authenticated\n * request principal, so the web surface of a `@TenantScoped({ mode: 'optional' })`\n * model never reads across tenants without an active context. A generated\n * in-process entry point has no request principal, so an invocation with no\n * active context would fall through the interceptor's optional-mode\n * pass-through and return rows across **all** tenants.\n *\n * `runTenantScopedEntryPoint()` closes that gap. `@happyvertical/smrt-core`'s\n * `MCPGenerator` is the only in-repo generated surface that wraps its\n * per-tool execution in this gate today (via `setTenantEntryPointRunner`\n * below). Core's `CLIGenerator`, which used to wrap its per-command execution\n * in the same gate, was retired as unused public API (#2664); the live local\n * CLI transport (`packages/cli/src/cli-generator.ts`, the shipped `smrt\n * <object>:<action>` binary) has never called this gate and is not\n * tenant-isolation fail-closed today.\n *\n * @see createCliContext for a hand-wired CLI runner (resolveTenantId,\n * super-admin) a consuming application can use directly — independent of\n * the generated-surface gate above and unaffected by #2664.\n */\n\nimport {\n hasTenantContext,\n isSystemContext,\n TenantContextError,\n withSystemContext,\n withTenant,\n} from './context.js';\nimport { isTenancyEnabled } from './enabled-state.js';\nimport { isTenantScopedClass } from './registry.js';\n\n/**\n * Inputs for {@link runTenantScopedEntryPoint}.\n *\n * Provide **either** `className` (the gate resolves tenant-scoping from the\n * authoritative tenancy registry — the same source the interceptor uses, so it\n * covers both `@TenantScoped` and `@smrt({ tenantScoped })` registrations) or an\n * explicit `tenantScoped` boolean (when the caller already resolved it, e.g. a\n * build-time generated surface). An explicit boolean wins when both are given.\n */\nexport interface TenantEntryPointOptions {\n /**\n * Class name of the target model. When provided, tenant-scoping is resolved\n * via `isTenantScopedClass(className)`.\n */\n className?: string;\n\n /**\n * Explicit tenant-scoping decision. Overrides `className` resolution when set.\n * Non-scoped models always pass through unchanged — the gate is a no-op.\n */\n tenantScoped?: boolean;\n\n /**\n * Explicit operator-provided tenant selector (CLI `--tenant <id>`, MCP\n * `context.tenantId`). When present (and no context is already active) the\n * function runs inside this tenant's context.\n */\n tenantId?: string | null;\n\n /**\n * Explicit operator opt-in to cross-tenant / system access (CLI\n * `--all-tenants`, an MCP host that trusts the caller as an operator). When\n * set the function runs in system context, bypassing tenant filtering.\n *\n * @default false\n */\n allowCrossTenant?: boolean;\n\n /**\n * Human-facing surface name used in the fail-closed error message, e.g.\n * `'CLI'` or `'MCP'`.\n *\n * @default 'entry point'\n */\n surface?: string;\n}\n\n/**\n * Run `fn` inside an appropriate tenant context for a generated in-process\n * entry point (MCP today, see the module docblock above), failing closed for\n * tenant-scoped models when no authorized context can be established.\n *\n * Resolution order (tenant-scoped models only):\n * 1. A tenant context is already active, or an explicit `withSystemContext()`\n * bypass is in effect (e.g. `runAsSystem()`, migrations) → run as-is.\n * 2. `allowCrossTenant` was explicitly set → run in system context. Checked\n * before `tenantId` so an explicit cross-tenant opt-in wins over a default\n * principal/host tenant rather than being silently scoped.\n * 3. An explicit `tenantId` was provided → run inside that tenant.\n * 4. Tenancy is enabled but none of the above → **throw** `TenantContextError`\n * (the fail-closed branch — never silently read across tenants).\n * 5. Tenancy is disabled (single-/no-tenant deployment) → pass through.\n *\n * Non-tenant-scoped models always pass straight through.\n *\n * @param options - {@link TenantEntryPointOptions}.\n * @param fn - The command/tool body to execute.\n * @returns The resolved value of `fn`.\n * @throws {TenantContextError} When a tenant-scoped model is reached with\n * tenancy enabled and no tenant/cross-tenant selector.\n */\nexport async function runTenantScopedEntryPoint<T>(\n options: TenantEntryPointOptions,\n fn: () => Promise<T>,\n): Promise<T> {\n const {\n className,\n tenantScoped,\n tenantId,\n allowCrossTenant = false,\n surface = 'entry point',\n } = options;\n\n // Resolve tenant-scoping: an explicit boolean wins; otherwise consult the\n // authoritative tenancy registry by class name (matches the interceptor).\n const scoped =\n typeof tenantScoped === 'boolean'\n ? tenantScoped\n : className\n ? isTenantScopedClass(className)\n : false;\n\n // Non-scoped models run as-is. So do calls already inside a tenant context\n // (an upstream handle) or an explicit system-context bypass — the interceptor\n // honors `withSystemContext()` (migrations, `runAsSystem()`), so the gate must\n // not fail-close over it (hasTenantContext() is false for the system marker).\n if (!scoped) return fn();\n if (hasTenantContext() || isSystemContext()) return fn();\n\n // Explicit operator opt-in to cross-tenant access. Checked before the tenant\n // selector so a deliberate `--all-tenants` / `allowCrossTenant` overrides a\n // default host/principal tenant instead of being silently scoped to it.\n if (allowCrossTenant) {\n return withSystemContext(fn);\n }\n\n // Explicit tenant selector.\n if (typeof tenantId === 'string' && tenantId) {\n return withTenant({ tenantId }, fn);\n }\n\n // Fail closed: tenancy is on but the caller gave us nothing to scope by.\n if (isTenancyEnabled()) {\n throw new TenantContextError(\n `Tenant context required for tenant-scoped access via ${surface}. ` +\n 'Pass an explicit tenant (e.g. --tenant <id> / a tenantId) or opt into ' +\n 'cross-tenant access (e.g. --all-tenants) to read across all tenants.',\n );\n }\n\n // Tenancy disabled → single-tenant deployment, pass through.\n return fn();\n}\n","import { AsyncLocalStorage } from 'node:async_hooks';\n\n// Separate from actor context: only the built-in list interceptor consumes it.\n// Share across duplicate module graphs, as with the normal tenant context.\nconst KEY = Symbol.for('smrt:tenant-global-read-scope');\nconst root = globalThis as typeof globalThis & {\n [KEY]?: AsyncLocalStorage<string>;\n};\nroot[KEY] ??= new AsyncLocalStorage<string>();\nconst storage = root[KEY];\n\nexport function getTenantGlobalReadScope(): string | undefined {\n return storage.getStore();\n}\n\nexport function runTenantGlobalReadScope<T>(\n tenantId: string,\n callback: () => Promise<T>,\n): Promise<T> {\n return storage.run(tenantId, callback);\n}\n","/**\n * Shared raw-SQL helpers for tenant-scoped collections' \"global\" and\n * \"tenant + globals\" lookups (#1600).\n *\n * Most domain models are `@TenantScoped`. Their collections historically\n * hand-rolled two helpers the OLD way:\n *\n * ```typescript\n * async findGlobal() { return this.list({ where: { tenantId: null } }); }\n * async findWithGlobals(tid) { return this.query(\n * `SELECT * FROM ${this.tableName} WHERE tenant_id = ? OR tenant_id IS NULL`, [tid]); }\n * ```\n *\n * Under an ACTIVE tenant context with tenancy enabled (default\n * `rawQueryPolicy: 'throw'`) BOTH break:\n * - `findGlobal()` routes an explicit `tenant_id IS NULL` filter through\n * `list()`, which the interceptor flags as an isolation violation → throws.\n * - `findWithGlobals()` issues unflagged raw SQL on a tenant-scoped class,\n * which `beforeQuery` blocks → throws.\n * - `findWithGlobals()` also trusts the caller-supplied `tenantId`, so once the\n * raw bypass is added a caller under tenant-A could read tenant-B by passing\n * B's id.\n *\n * These helpers run raw with `{ allowRawOnTenantScoped: true }` (carrying the\n * tenant predicate themselves), and `queryWithGlobals` re-implements the\n * isolation guard the bypass disables (`assertTenantReadAllowed`): a caller\n * under tenant-A must not read tenant-B's rows by passing tenant-B's id. A\n * system / super-admin-bypass context keeps the deliberate cross-tenant\n * capability for admin paths.\n *\n * STI scoping is derived automatically from the collection's item class via\n * `collection.getStiChildMetaType()` (smrt-core), which mirrors the\n * `_meta_type` scoping `list()` applies: STI **child** collections scope the\n * shared table to their own subtype, while STI **base** and CTI collections do\n * not (a base legitimately spans subtypes; CTI tables have no `_meta_type`).\n * Callers never hand-classify their collection. Promoted from\n * `@happyvertical/smrt-messages` (#1596) so every package shares one\n * implementation.\n */\n\nimport type { SmrtCollection, SmrtObject } from '@happyvertical/smrt-core';\nimport {\n getCurrentTenant,\n isSuperAdminBypass,\n TenantIsolationError,\n} from './context.js';\n\nimport { runTenantGlobalReadScope } from './tenant-global-read-scope.js';\n\n/**\n * Fail closed when an active tenant context requests a different tenant's rows.\n *\n * @param tenantId - The tenant id the caller asked for.\n * @param label - `Class.method` identifier for the error message.\n * @throws {TenantIsolationError} when a non-bypass tenant context is active and\n * does not match `tenantId`.\n */\nexport function assertTenantReadAllowed(tenantId: string, label: string): void {\n const tenantContext = getCurrentTenant();\n if (\n tenantContext &&\n !isSuperAdminBypass() &&\n tenantContext.tenantId !== tenantId\n ) {\n throw new TenantIsolationError(\n `Tenant isolation violation in ${label}: context tenant is ` +\n `'${tenantContext.tenantId}' but query requested '${tenantId}'`,\n { tenantId: tenantContext.tenantId, attemptedTenantId: tenantId },\n );\n }\n}\n\n/**\n * Allow list reads of an authorized tenant and global rows without changing\n * actor identity. Only the built-in tenancy beforeList hook consumes this\n * capability; custom authorization hooks still see the original caller.\n * Other operations (get/query/save/delete) retain their normal guards.\n */\nexport async function withTenantGlobalRead<T>(\n tenantId: string,\n callback: () => Promise<T>,\n): Promise<T> {\n if (typeof tenantId !== 'string' || !tenantId.trim()) {\n throw new Error('withTenantGlobalRead requires a nonempty tenant ID');\n }\n assertTenantReadAllowed(tenantId, 'withTenantGlobalRead');\n return runTenantGlobalReadScope(tenantId, callback);\n}\n\n/**\n * Return all global (tenant-less) rows for a tenant-scoped collection.\n *\n * STI child collections are auto-scoped to their own `_meta_type` (via\n * `collection.getStiChildMetaType()`) so the shared table never returns sibling\n * subtypes; STI base / CTI collections are not scoped.\n *\n * @param collection - The tenant-scoped collection to query.\n */\nexport async function queryGlobal<T, M extends SmrtObject = SmrtObject>(\n collection: SmrtCollection<M>,\n): Promise<T[]> {\n const metaType = collection.getStiChildMetaType();\n const where = metaType\n ? 'WHERE _meta_type = ? AND tenant_id IS NULL'\n : 'WHERE tenant_id IS NULL';\n const params = metaType ? [metaType] : [];\n // Two decoupled type params by design (STI). `M` is inferred from the\n // collection's declared item type — the STI *base* (e.g. `Email`) — and keeps\n // the parameter assignable despite `SmrtCollection`'s contravariant\n // `ModelType` positions. `T` is the caller-declared *row* type: an STI child\n // collection (e.g. `EmailAccountCollection`, statically `SmrtCollection<Email>`)\n // filters by `_meta_type` and hydrates child rows (`EmailAccount`) that differ\n // from `M`. `query()` is statically `M[]` but yields those child instances at\n // runtime, so the bridge cast is required — returning `M[]` would break every\n // STI-child caller.\n return (await collection.query(\n `SELECT * FROM ${collection.tableName} ${where}`,\n params,\n { allowRawOnTenantScoped: true },\n )) as unknown as T[];\n}\n\n/**\n * Return a tenant's rows plus all global rows for a tenant-scoped collection.\n *\n * Fails closed (`assertTenantReadAllowed`) before issuing the bypassed query.\n * STI child collections are auto-scoped to their own `_meta_type` (via\n * `collection.getStiChildMetaType()`); STI base / CTI collections are not.\n *\n * @param collection - The tenant-scoped collection to query.\n * @param tenantId - The tenant id to include alongside globals.\n * @param label - `Class.method` identifier for the isolation error message.\n */\nexport async function queryWithGlobals<T, M extends SmrtObject = SmrtObject>(\n collection: SmrtCollection<M>,\n tenantId: string,\n label: string,\n): Promise<T[]> {\n assertTenantReadAllowed(tenantId, label);\n const metaType = collection.getStiChildMetaType();\n const where = metaType\n ? 'WHERE _meta_type = ? AND (tenant_id = ? OR tenant_id IS NULL)'\n : 'WHERE tenant_id = ? OR tenant_id IS NULL';\n const params = metaType ? [metaType, tenantId] : [tenantId];\n // See `queryGlobal` above: `T` (caller's STI child row type) is intentionally\n // decoupled from `M` (the collection's inferred base type), so the cast\n // bridges `query()`'s static `M[]` to the hydrated child rows.\n return (await collection.query(\n `SELECT * FROM ${collection.tableName} ${where}`,\n params,\n { allowRawOnTenantScoped: true },\n )) as unknown as T[];\n}\n","/**\n * Tenant Interceptor - Core enforcement mechanism\n *\n * Registers with GlobalInterceptors in smrt-core to automatically:\n * - Filter queries by tenant ID\n * - Validate tenant context on save/delete\n * - Block or audit raw SQL on tenant-scoped classes\n *\n * @see https://github.com/happyvertical/smrt/issues/675\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport type { SmrtObject } from '@happyvertical/smrt-core';\nimport {\n type CollectionInterceptor,\n type DispatchBus,\n GlobalInterceptors,\n type InterceptorContext,\n type ListOptions,\n type QueryInterceptResult,\n type QueryOptions,\n resolveGetStringFilter,\n setDispatchTenantResolver,\n setTenantEntryPointRunner,\n setTenantScopedClassResolver,\n} from '@happyvertical/smrt-core';\nimport {\n getCurrentTenant,\n getTenantId,\n isSuperAdminBypass,\n isSystemContext,\n TenantContextError,\n TenantIsolationError,\n} from './context.js';\nimport { isTenancyEnabled, setTenancyEnabled } from './enabled-state.js';\nimport { runTenantScopedEntryPoint } from './entry-point.js';\nimport { getTenantScopedConfig, isTenantScopedClass } from './registry.js';\n\nimport { assertTenantReadAllowed } from './tenant-global-queries.js';\nimport { getTenantGlobalReadScope } from './tenant-global-read-scope.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Policy controlling what happens when raw SQL is executed against a\n * tenant-scoped class without an explicit bypass.\n *\n * - `'throw'` — Raises a `TenantIsolationError` (most secure; default).\n * - `'warn'` — Logs a `console.warn` but allows the query to proceed (useful\n * during migration periods).\n * - `'allow'` — Silently allows the query; not recommended for production.\n *\n * @see TenantInterceptorOptions.rawQueryPolicy\n * @see enableTenancy\n */\nexport type RawQueryPolicy = 'throw' | 'warn' | 'allow';\n\n/**\n * Configuration options accepted by `createTenantInterceptor()` and\n * `enableTenancy()`.\n *\n * All options are optional; reasonable defaults are applied. The callback\n * hooks (`onRawQuery`, `onMissingContext`, `onIsolationViolation`) are useful\n * for logging and alerting without altering the enforcement behaviour.\n *\n * @see createTenantInterceptor\n * @see enableTenancy\n */\nexport interface TenantInterceptorOptions {\n /**\n * Policy for raw SQL queries on tenant-scoped classes\n * - 'throw': Throw error (most secure, default)\n * - 'warn': Log warning but allow (for migration)\n * - 'allow': Silently allow (not recommended for production)\n * @default 'throw'\n */\n rawQueryPolicy?: RawQueryPolicy;\n\n /**\n * Called when a raw query is attempted on a tenant-scoped class\n * Useful for logging/auditing\n */\n onRawQuery?: (\n className: string,\n sql: string,\n context: InterceptorContext,\n ) => void;\n\n /**\n * Called when tenant context is missing for a tenant-scoped operation\n */\n onMissingContext?: (\n className: string,\n operation: string,\n context: InterceptorContext,\n ) => void;\n\n /**\n * Called when an isolation violation is detected\n */\n onIsolationViolation?: (\n className: string,\n expectedTenantId: string,\n actualTenantId: string,\n context: InterceptorContext,\n ) => void;\n\n /**\n * DispatchBus instance for emitting provisioning events on lifecycle changes.\n * When provided along with directoryClasses, afterSave/afterDelete hooks\n * emit dispatches like `directory.membership.created`.\n */\n dispatchBus?: DispatchBus;\n\n /**\n * Class names to emit directory dispatches for on save/delete lifecycle events.\n * Only classes listed here will trigger dispatch emissions.\n * @example ['Tenant', 'Membership', 'User']\n */\n directoryClasses?: string[];\n}\n\nconst DEFAULT_OPTIONS: TenantInterceptorOptions = {\n rawQueryPolicy: 'throw',\n};\n\nfunction getTenancyIdentity(\n className: string,\n context: InterceptorContext,\n): string {\n return context.qualifiedClassName ?? className;\n}\n\n/**\n * Extract a plain-object snapshot of an instance for dispatch payloads.\n *\n * Prefers `toJSON()` when available (all real SmrtObject instances) because\n * it returns only data fields and excludes internal handles like `_db`, `_ai`,\n * and `_fs` which may contain circular references (e.g. connection pools with\n * Timeout objects).\n *\n * @see https://github.com/happyvertical/smrt/issues/946\n */\nfunction serializeInstance(\n instance: SmrtObject,\n className: string,\n): Record<string, unknown> {\n // Documented exception to the \"never call toJSON() directly\" convention\n // (docs/content/standards.md §7): the interceptor must serialize whatever\n // instance is handed to it, including workspace stubs and plain-object\n // doubles used in unit tests whose classes may not extend SmrtObject and\n // therefore have no `transformJSON()` hook. Using `toJSON()` here is a\n // duck-typed fallback — when present, it strips framework-internal handles\n // for us; when absent, we fall through to manual key iteration below.\n const maybeToJSON = (instance as { toJSON?: unknown }).toJSON;\n if (typeof maybeToJSON === 'function') {\n return {\n className,\n ...(maybeToJSON.call(instance) as Record<string, unknown>),\n };\n }\n\n // Fallback for plain-object stubs (e.g. in unit tests):\n // skip functions and framework-internal properties\n const result: Record<string, unknown> = { className };\n const record = instance as unknown as Record<string, unknown>;\n for (const key of Object.keys(instance)) {\n const value = record[key];\n if (typeof value !== 'function') {\n result[key] = value;\n }\n }\n return result;\n}\n\n/**\n * Create a `CollectionInterceptor` that enforces tenant isolation on all\n * `SmrtCollection` operations.\n *\n * The returned interceptor hooks into the smrt-core `GlobalInterceptors`\n * pipeline at priority 100 (runs before all other interceptors) and\n * handles the following lifecycle hooks:\n *\n * | Hook | Behaviour |\n * |---------------|-----------|\n * | `beforeList` | Injects tenant filter into `WHERE`; validates explicit filters. |\n * | `beforeGet` | Resolves string lookups (id vs slug) and adds the tenant predicate. |\n * | `beforeSave` | Auto-populates `tenantId`; validates existing values. |\n * | `beforeDelete`| Validates the instance's `tenantId` matches context. |\n * | `beforeQuery` | Enforces `rawQueryPolicy` on raw SQL calls. |\n * | `afterSave` | Emits `directory.<class>.created/updated` via `dispatchBus`. |\n * | `afterDelete` | Emits `directory.<class>.deleted` via `dispatchBus`. |\n *\n * Use `enableTenancy()` to register the interceptor globally. Call this\n * directly only when you need multiple interceptor instances (e.g., for\n * isolated tests or feature flags).\n *\n * @param options - Configuration for the interceptor.\n * @returns A `CollectionInterceptor` ready to be registered with\n * `GlobalInterceptors.register()`.\n *\n * @example\n * ```typescript\n * import { createTenantInterceptor } from '@happyvertical/smrt-tenancy';\n * import { GlobalInterceptors } from '@happyvertical/smrt-core';\n *\n * const interceptor = createTenantInterceptor({ rawQueryPolicy: 'warn' });\n * GlobalInterceptors.register(interceptor);\n * ```\n *\n * @see enableTenancy\n * @see TenantInterceptorOptions\n */\nexport function createTenantInterceptor(\n options: TenantInterceptorOptions = {},\n): CollectionInterceptor {\n const opts = { ...DEFAULT_OPTIONS, ...options };\n\n return {\n name: 'smrt-tenancy',\n priority: 100, // High priority - should run first\n\n /**\n * Before list: Add tenant filter to queries\n */\n beforeList(\n className: string,\n listOptions: ListOptions,\n context: InterceptorContext,\n ): ListOptions | undefined {\n // Check if this class is tenant-scoped\n const tenancyIdentity = getTenancyIdentity(className, context);\n if (!isTenantScopedClass(tenancyIdentity)) {\n return; // Not tenant-scoped, pass through\n }\n\n // Check for super admin bypass\n if (isSuperAdminBypass()) {\n return; // Bypass enabled, pass through\n }\n\n // Check for system context (explicit bypass via withSystemContext)\n if (isSystemContext()) {\n return; // System context bypasses tenant checks\n }\n\n const config = getTenantScopedConfig(tenancyIdentity);\n const tenantContext = getCurrentTenant();\n const globalReadTenant = getTenantGlobalReadScope();\n if (globalReadTenant !== undefined) {\n // Recheck if nested code changed actors after entering the capability.\n assertTenantReadAllowed(globalReadTenant, 'tenant/global list');\n const tenantField = config?.field || 'tenantId';\n const where = listOptions.where || {};\n const groups = Array.isArray(where) ? where : [[where]];\n if (!groups.length || groups.some((group) => !group.length)) {\n throw new Error('Invalid DNF where clause for tenant/global list');\n }\n return {\n ...listOptions,\n where: groups.flatMap((group) => [\n [...group, { [tenantField]: globalReadTenant }],\n [...group, { [tenantField]: null }],\n ]),\n };\n }\n\n // If no tenant context and mode is 'required', throw\n if (!tenantContext) {\n if (config?.mode === 'required') {\n opts.onMissingContext?.(className, 'list', context);\n throw new TenantContextError(\n `Tenant context required for listing ${className}. ` +\n `Use withTenant() or configure TenantContext middleware.`,\n );\n }\n return; // Mode is 'optional', allow without filtering\n }\n\n // Add tenant filter to where clause\n const tenantField = config?.field || 'tenantId';\n const where = listOptions.where || {};\n\n // Preserve DNF predicates (outer OR, inner AND) by applying the tenant\n // predicate to every branch. Flattening this shape into an object would\n // make one branch escape tenant scope, so validate direct tenant filters\n // branch-by-branch before adding the current tenant where absent.\n if (Array.isArray(where)) {\n if (\n where.length === 0 ||\n where.some((andGroup) => andGroup.length === 0)\n ) {\n throw new Error(\n 'Invalid DNF where clause: every OR branch must contain at least one condition',\n );\n }\n const scopedWhere = where.map((andGroup) => {\n const directValues = andGroup.flatMap((condition) => {\n if (!Object.hasOwn(condition, tenantField)) return [];\n const value = condition[tenantField];\n return Array.isArray(value) ? value : [value];\n });\n const offendingIndex = directValues.findIndex(\n (value) => value !== tenantContext.tenantId,\n );\n if (offendingIndex !== -1) {\n const offending = directValues[offendingIndex];\n opts.onIsolationViolation?.(\n className,\n tenantContext.tenantId,\n String(offending),\n context,\n );\n throw new TenantIsolationError(\n `Tenant isolation violation in ${className} query: ` +\n `context tenant is '${tenantContext.tenantId}' but query filters by '${String(offending)}'`,\n {\n tenantId: tenantContext.tenantId,\n attemptedTenantId: String(offending),\n },\n );\n }\n return directValues.length > 0\n ? andGroup\n : [...andGroup, { [tenantField]: tenantContext.tenantId }];\n });\n return { ...listOptions, where: scopedWhere };\n }\n\n // Check if tenant filter is already present\n if (tenantField in where) {\n // Validate it matches context. The filter may be a scalar\n // (`tenantId: 'x'`) or an IN-style array (`tenantId: ['x']`) —\n // smrt-core auto-converts array values to SQL IN clauses, so an\n // array containing only the context tenant is a valid filter.\n // See https://github.com/happyvertical/smrt/issues/1495\n const existingFilter = where[tenantField];\n const filterValues = Array.isArray(existingFilter)\n ? existingFilter\n : [existingFilter];\n // findIndex (not find) so a literal null/undefined filter value is\n // still flagged as a violation rather than mistaken for \"not found\"\n const offendingIndex = filterValues.findIndex(\n (value) => value !== tenantContext.tenantId,\n );\n if (offendingIndex !== -1) {\n const offending = filterValues[offendingIndex];\n opts.onIsolationViolation?.(\n className,\n tenantContext.tenantId,\n String(offending),\n context,\n );\n throw new TenantIsolationError(\n `Tenant isolation violation in ${className} query: ` +\n `context tenant is '${tenantContext.tenantId}' but query filters by '${String(offending)}'`,\n {\n tenantId: tenantContext.tenantId,\n attemptedTenantId: String(offending),\n },\n );\n }\n return; // Filter already correct\n }\n\n // Inject tenant filter\n return {\n ...listOptions,\n where: {\n ...where,\n [tenantField]: tenantContext.tenantId,\n },\n };\n },\n\n /**\n * Before get: Add tenant filter to single record fetches\n */\n beforeGet(\n className: string,\n filter: string | Record<string, unknown>,\n context: InterceptorContext,\n ): string | Record<string, unknown> | undefined {\n const tenancyIdentity = getTenancyIdentity(className, context);\n if (!isTenantScopedClass(tenancyIdentity)) {\n return;\n }\n\n if (isSuperAdminBypass()) {\n return;\n }\n\n // Check for system context (explicit bypass via withSystemContext)\n if (isSystemContext()) {\n return; // System context bypasses tenant checks\n }\n\n const config = getTenantScopedConfig(tenancyIdentity);\n const tenantContext = getCurrentTenant();\n\n if (!tenantContext) {\n if (config?.mode === 'required') {\n opts.onMissingContext?.(className, 'get', context);\n throw new TenantContextError(\n `Tenant context required for getting ${className}. ` +\n `Use withTenant() or configure TenantContext middleware.`,\n );\n }\n return;\n }\n\n const tenantField = config?.field || 'tenantId';\n\n // If filter is a string, resolve it exactly the way core's `get()` would\n // (UUID -> id lookup, anything else -> slug/context natural key) and add\n // the tenant predicate to whichever shape it resolves to. Rewriting every\n // string to `{ id: filter }` broke get-by-slug under a tenant context:\n // null on SQLite, a uuid cast error on PostgreSQL (#2365).\n if (typeof filter === 'string') {\n return {\n ...resolveGetStringFilter(filter),\n [tenantField]: tenantContext.tenantId,\n };\n }\n\n // Add tenant filter to object\n if (!(tenantField in filter)) {\n return {\n ...filter,\n [tenantField]: tenantContext.tenantId,\n };\n }\n\n // Validate existing filter. Like beforeList, accept scalar or\n // IN-style array filters (smrt-core auto-converts arrays to SQL IN).\n // See https://github.com/happyvertical/smrt/issues/1495\n const existingFilter = filter[tenantField];\n const filterValues = Array.isArray(existingFilter)\n ? existingFilter\n : [existingFilter];\n // findIndex (not find) so a literal null/undefined filter value is\n // still flagged as a violation rather than mistaken for \"not found\"\n const offendingIndex = filterValues.findIndex(\n (value) => value !== tenantContext.tenantId,\n );\n if (offendingIndex !== -1) {\n const offending = filterValues[offendingIndex];\n opts.onIsolationViolation?.(\n className,\n tenantContext.tenantId,\n String(offending),\n context,\n );\n throw new TenantIsolationError(\n `Tenant isolation violation in ${className} get: ` +\n `context tenant is '${tenantContext.tenantId}' but query filters by '${String(offending)}'`,\n {\n tenantId: tenantContext.tenantId,\n attemptedTenantId: String(offending),\n },\n );\n }\n\n return;\n },\n\n /**\n * Before query: Handle raw SQL on tenant-scoped classes\n */\n beforeQuery(\n className: string,\n queryOptions: QueryOptions,\n context: InterceptorContext,\n ): QueryInterceptResult | undefined {\n const tenancyIdentity = getTenancyIdentity(className, context);\n if (!isTenantScopedClass(tenancyIdentity)) {\n return;\n }\n\n // Check for explicit bypass flag\n if (queryOptions.allowRawOnTenantScoped) {\n opts.onRawQuery?.(className, queryOptions.sql, context);\n return; // Explicitly allowed\n }\n\n if (isSuperAdminBypass()) {\n opts.onRawQuery?.(className, queryOptions.sql, context);\n return;\n }\n\n // Check for system context (explicit bypass via withSystemContext)\n if (isSystemContext()) {\n opts.onRawQuery?.(className, queryOptions.sql, context);\n return;\n }\n\n // Handle based on policy\n const message =\n `Raw SQL query attempted on tenant-scoped class ${className}. ` +\n `Use list()/get() for automatic tenant filtering, or call ` +\n `query() with { allowRawOnTenantScoped: true } if you're handling ` +\n `tenant filtering manually.`;\n\n opts.onRawQuery?.(className, queryOptions.sql, context);\n\n switch (opts.rawQueryPolicy) {\n case 'throw':\n throw new TenantIsolationError(message);\n\n case 'warn':\n logger.warn(`[smrt-tenancy] WARNING: ${message}`);\n return;\n default:\n return;\n }\n },\n\n /**\n * Before save: Validate tenant ID is set and matches context\n */\n bulkMutation: {\n // Directory dispatch observes individual writes and remains sequential.\n // Custom error callbacks may observe partial progress, so also fall back.\n compatible: (className: string) =>\n !(opts.dispatchBus && opts.directoryClasses?.includes(className)) &&\n !opts.onMissingContext &&\n !opts.onIsolationViolation,\n },\n\n beforeSave(instance: SmrtObject, context: InterceptorContext): void {\n // Use context.className which is always correct\n // (instance.constructor.name may not match for proxies or plain objects in tests)\n const className = context.className;\n\n // Stash isNew flag for afterSave dispatch detection\n if (opts.directoryClasses?.includes(className)) {\n const id = (instance as unknown as Record<string, unknown>).id;\n context.metadata = {\n ...context.metadata,\n _directoryIsNew: id === undefined || id === null,\n };\n }\n\n const tenancyIdentity = getTenancyIdentity(className, context);\n if (!isTenantScopedClass(tenancyIdentity)) {\n return;\n }\n\n if (isSuperAdminBypass()) {\n return;\n }\n\n // Check for system context (explicit bypass via withSystemContext)\n if (isSystemContext()) {\n return; // System context bypasses tenant checks\n }\n\n const config = getTenantScopedConfig(tenancyIdentity);\n const tenantField = config?.field || 'tenantId';\n const instanceRecord = instance as unknown as Record<string, unknown>;\n const instanceTenantId = instanceRecord[tenantField];\n\n const tenantContext = getCurrentTenant();\n\n // Check if tenant context is required\n if (!tenantContext) {\n if (config?.mode === 'required') {\n opts.onMissingContext?.(className, 'save', context);\n throw new TenantContextError(\n `Tenant context required for saving ${className}. ` +\n `Use withTenant() or configure TenantContext middleware.`,\n );\n }\n return; // Mode is 'optional'\n }\n\n // Auto-populate tenant ID if not set\n if (!instanceTenantId && config?.autoPopulate !== false) {\n instanceRecord[tenantField] = tenantContext.tenantId;\n return;\n }\n\n // Validate tenant ID matches context\n if (instanceTenantId && instanceTenantId !== tenantContext.tenantId) {\n const attemptedTenantId = String(instanceTenantId);\n opts.onIsolationViolation?.(\n className,\n tenantContext.tenantId,\n attemptedTenantId,\n context,\n );\n throw new TenantIsolationError(\n `Tenant isolation violation: cannot save ${className} with ` +\n `tenantId '${attemptedTenantId}' in context of tenant '${tenantContext.tenantId}'`,\n {\n tenantId: tenantContext.tenantId,\n attemptedTenantId,\n },\n );\n }\n },\n\n /**\n * Before delete: Validate instance belongs to current tenant\n */\n beforeDelete(instance: SmrtObject, context: InterceptorContext): void {\n // Use context.className which is always correct\n const className = context.className;\n\n const tenancyIdentity = getTenancyIdentity(className, context);\n if (!isTenantScopedClass(tenancyIdentity)) {\n return;\n }\n\n if (isSuperAdminBypass()) {\n return;\n }\n\n // Check for system context (explicit bypass via withSystemContext)\n if (isSystemContext()) {\n return; // System context bypasses tenant checks\n }\n\n const config = getTenantScopedConfig(tenancyIdentity);\n const tenantField = config?.field || 'tenantId';\n const instanceTenantId = (instance as unknown as Record<string, unknown>)[\n tenantField\n ];\n\n const tenantContext = getCurrentTenant();\n\n if (!tenantContext) {\n if (config?.mode === 'required') {\n opts.onMissingContext?.(className, 'delete', context);\n throw new TenantContextError(\n `Tenant context required for deleting ${className}. ` +\n `Use withTenant() or configure TenantContext middleware.`,\n );\n }\n return;\n }\n\n // Validate tenant ID matches\n if (instanceTenantId && instanceTenantId !== tenantContext.tenantId) {\n const attemptedTenantId = String(instanceTenantId);\n opts.onIsolationViolation?.(\n className,\n tenantContext.tenantId,\n attemptedTenantId,\n context,\n );\n throw new TenantIsolationError(\n `Tenant isolation violation: cannot delete ${className} with ` +\n `tenantId '${attemptedTenantId}' in context of tenant '${tenantContext.tenantId}'`,\n {\n tenantId: tenantContext.tenantId,\n attemptedTenantId,\n },\n );\n }\n },\n\n /**\n * After save: Emit directory dispatch for configured classes\n */\n async afterSave(\n instance: SmrtObject,\n context: InterceptorContext,\n ): Promise<void> {\n if (\n !opts.dispatchBus ||\n !opts.directoryClasses?.includes(context.className)\n )\n return;\n\n const instanceId = (instance as unknown as Record<string, unknown>).id;\n const sourceId = typeof instanceId === 'string' ? instanceId : undefined;\n const rawIsNew = context.metadata?._directoryIsNew;\n const isNew =\n typeof rawIsNew === 'boolean' ? rawIsNew : instanceId == null;\n const event = isNew\n ? `directory.${context.className.toLowerCase()}.created`\n : `directory.${context.className.toLowerCase()}.updated`;\n\n await opts.dispatchBus.emit(\n event,\n serializeInstance(instance, context.className),\n {\n source: 'smrt-tenancy',\n sourceId,\n },\n );\n },\n\n /**\n * After delete: Emit directory dispatch for configured classes\n */\n async afterDelete(\n instance: SmrtObject,\n context: InterceptorContext,\n ): Promise<void> {\n if (\n !opts.dispatchBus ||\n !opts.directoryClasses?.includes(context.className)\n )\n return;\n\n const instanceId = (instance as unknown as Record<string, unknown>).id;\n await opts.dispatchBus.emit(\n `directory.${context.className.toLowerCase()}.deleted`,\n serializeInstance(instance, context.className),\n {\n source: 'smrt-tenancy',\n sourceId: typeof instanceId === 'string' ? instanceId : undefined,\n },\n );\n },\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Registration Functions\n// ─────────────────────────────────────────────────────────────────────────────\n\n// The enabled flag lives in `enabled-state.ts` (a leaf module) so `entry-point.ts`\n// can read it without importing this module — breaking the otherwise-circular\n// interceptor ↔ entry-point dependency.\nlet registeredInterceptor: CollectionInterceptor | null = null;\n\n/**\n * Enable tenant enforcement globally\n *\n * Call this once at application startup to enable automatic tenant isolation.\n *\n * @param options - Configuration options\n *\n * @example\n * ```typescript\n * // In your app initialization\n * import { enableTenancy } from '@happyvertical/smrt-tenancy';\n *\n * enableTenancy({\n * rawQueryPolicy: 'throw',\n * onMissingContext: (className, operation) => {\n * console.error(`Missing tenant context for ${operation} on ${className}`);\n * }\n * });\n * ```\n */\nexport function enableTenancy(options: TenantInterceptorOptions = {}): void {\n if (isTenancyEnabled()) {\n logger.warn(\n '[smrt-tenancy] Tenancy is already enabled. Call disableTenancy() first to reconfigure.',\n );\n return;\n }\n\n registeredInterceptor = createTenantInterceptor(options);\n GlobalInterceptors.register(registeredInterceptor);\n\n // Wire the DispatchBus tenant-scope resolver (S5 #1398). Core cannot depend\n // on tenancy, so it reads the active tenant through this injected hook; the\n // bus stamps/filters dispatches by the active tenant only while tenancy is\n // enabled. Mirrors the GlobalInterceptors inversion above.\n setDispatchTenantResolver(() => getTenantId());\n\n // Wire the fail-closed tenant gate for generated in-process entry points\n // (#1554). Core's MCPGenerator invokes this runner around tenant-scoped MCP\n // execution; without it (tenancy disabled) that surface passes through\n // unchanged. Core's CLIGenerator used to invoke it too but was retired as\n // unused public API (#2664); the shipped local CLI transport\n // (packages/cli/src/cli-generator.ts) has never called it.\n setTenantEntryPointRunner(runTenantScopedEntryPoint);\n\n // Wire the tenant-scoped-class resolver so core-side fail-closed read guards\n // (generated REST read scope, #1782) recognize `@TenantScoped()`-decorated\n // classes, which record their config only in the tenancy registry.\n setTenantScopedClassResolver((className) => isTenantScopedClass(className));\n\n setTenancyEnabled(true);\n}\n\n/**\n * Disable global tenant enforcement.\n *\n * Unregisters the interceptor previously installed by `enableTenancy()` and\n * resets the internal enabled flag so `enableTenancy()` can be called again.\n * Idempotent — safe to call even when tenancy was never enabled.\n *\n * Common use-cases:\n * - Test teardown (via `resetTenancy()`).\n * - Temporarily disabling tenancy before reconfiguring with new options.\n *\n * @example\n * ```typescript\n * afterAll(() => {\n * disableTenancy();\n * });\n * ```\n *\n * @see enableTenancy\n * @see isTenancyEnabled\n * @see resetTenancy\n */\nexport function disableTenancy(): void {\n if (!isTenancyEnabled() || !registeredInterceptor) {\n return;\n }\n\n GlobalInterceptors.unregister(registeredInterceptor);\n // Clear the DispatchBus tenant resolver so the bus reverts to its no-op\n // (pre-tenancy) behavior when tenancy is disabled.\n setDispatchTenantResolver(undefined);\n // Clear the in-process tenant gate so that surface (MCP) passes through\n // (#1554).\n setTenantEntryPointRunner(undefined);\n // Clear the tenant-scoped-class resolver (#1782).\n setTenantScopedClassResolver(undefined);\n registeredInterceptor = null;\n setTenancyEnabled(false);\n}\n\n/**\n * Return `true` if tenant enforcement is currently active.\n *\n * Reflects whether `enableTenancy()` has been called and the interceptor has not\n * yet been removed by `disableTenancy()`. Re-exported from `enabled-state.ts`\n * (the shared leaf module) so the public API surface is unchanged.\n *\n * @see enableTenancy\n * @see disableTenancy\n */\nexport { isTenancyEnabled };\n","/**\n * Testing Utilities for smrt-tenancy\n *\n * Helpers for testing tenant-scoped applications.\n *\n * @example\n * ```typescript\n * import { createTestTenantContext, resetTenancy } from '@happyvertical/smrt-tenancy/testing';\n *\n * beforeEach(() => {\n * resetTenancy(); // Clear all state\n * });\n *\n * it('should filter by tenant', async () => {\n * await createTestTenantContext({ tenantId: 'tenant-1' }, async () => {\n * const docs = await collection.list({});\n * // Only tenant-1 documents\n * });\n * });\n * ```\n */\n\nimport {\n type MinimalTenantContext,\n type TenantContextData,\n withTenant,\n} from './context.js';\nimport { disableTenancy, enableTenancy } from './interceptor.js';\nimport { clearTenantScopedRegistry } from './registry.js';\n\n/**\n * Reset all tenancy state (for use in beforeEach/afterEach)\n *\n * This clears:\n * - Registered interceptors\n * - Tenant-scoped class registry\n *\n * @example\n * ```typescript\n * afterEach(() => {\n * resetTenancy();\n * });\n * ```\n */\nexport function resetTenancy(): void {\n disableTenancy();\n clearTenantScopedRegistry();\n}\n\n/**\n * Create a test tenant context and run code within it\n *\n * Convenience wrapper around withTenant() with sensible defaults for testing.\n *\n * @param context - Tenant context (can be minimal, just tenantId)\n * @param fn - Async function to run in the context\n *\n * @example\n * ```typescript\n * await createTestTenantContext({ tenantId: 'test-tenant' }, async () => {\n * const product = await collection.create({ name: 'Test' });\n * expect(product.tenantId).toBe('test-tenant');\n * });\n * ```\n */\nexport async function createTestTenantContext<T>(\n context: MinimalTenantContext | TenantContextData,\n fn: () => Promise<T>,\n): Promise<T> {\n return withTenant(context, fn);\n}\n\n/**\n * Create multiple tenant contexts for isolation testing\n *\n * @param tenantIds - Array of tenant IDs to create contexts for\n * @param fn - Function that receives an object mapping tenant IDs to context runners\n *\n * @example\n * ```typescript\n * await testTenantIsolation(['tenant-a', 'tenant-b'], async (tenants) => {\n * // Create in tenant A\n * const docA = await tenants['tenant-a'](async () => {\n * return collection.create({ title: 'A doc' });\n * });\n *\n * // Verify not visible in tenant B\n * await tenants['tenant-b'](async () => {\n * const found = await collection.get(docA.id);\n * expect(found).toBeNull();\n * });\n * });\n * ```\n */\nexport async function testTenantIsolation<T>(\n tenantIds: string[],\n fn: (\n tenants: Record<string, <R>(runner: () => Promise<R>) => Promise<R>>,\n ) => Promise<T>,\n): Promise<T> {\n const tenants: Record<string, <R>(runner: () => Promise<R>) => Promise<R>> =\n {};\n\n for (const tenantId of tenantIds) {\n tenants[tenantId] = async <R>(runner: () => Promise<R>) => {\n return withTenant({ tenantId }, runner);\n };\n }\n\n return fn(tenants);\n}\n\n/**\n * Options for `setupTestTenancy()`.\n *\n * @see setupTestTenancy\n */\nexport interface SetupTestTenancyOptions {\n /**\n * Enable tenancy interceptors\n * @default true\n */\n enableInterceptors?: boolean;\n\n /**\n * Raw query policy for tests\n * @default 'throw'\n */\n rawQueryPolicy?: 'throw' | 'warn' | 'allow';\n}\n\n/**\n * Set up tenancy for a test suite\n *\n * Call in beforeAll or at the start of tests to configure tenancy.\n *\n * @param options - Setup options\n *\n * @example\n * ```typescript\n * beforeAll(() => {\n * setupTestTenancy({ enableInterceptors: true });\n * });\n *\n * afterAll(() => {\n * resetTenancy();\n * });\n * ```\n */\nexport function setupTestTenancy(options: SetupTestTenancyOptions = {}): void {\n const { enableInterceptors = true, rawQueryPolicy = 'throw' } = options;\n\n // Clear any existing state\n resetTenancy();\n\n // Enable interceptors if requested\n if (enableInterceptors) {\n enableTenancy({ rawQueryPolicy });\n }\n}\n\n/**\n * Assert that executing `fn` throws a `TenantContextError`.\n *\n * Fails with a descriptive message if `fn` completes without throwing, or if\n * it throws a different error type. Optionally verifies that the error message\n * contains a specific substring.\n *\n * Useful for testing that business-logic code correctly rejects calls that are\n * made outside a tenant context.\n *\n * @param fn - Async function that should throw `TenantContextError`.\n * @param messageContains - Optional substring the error message must include.\n *\n * @example\n * ```typescript\n * await assertTenantContextRequired(async () => {\n * // No withTenant() in scope\n * await documentCollection.list({});\n * });\n * ```\n *\n * @see assertTenantIsolationViolation\n * @see TenantContextError\n */\nexport async function assertTenantContextRequired(\n fn: () => Promise<unknown>,\n messageContains?: string,\n): Promise<void> {\n try {\n await fn();\n throw new Error('Expected TenantContextError but no error was thrown');\n } catch (error: unknown) {\n const err = error as Error & { code?: string };\n if (err.code !== 'TENANT_CONTEXT_REQUIRED') {\n throw new Error(\n `Expected TenantContextError but got ${err.constructor.name}: ${err.message}`,\n );\n }\n if (messageContains && !err.message.includes(messageContains)) {\n throw new Error(\n `Expected error message to contain '${messageContains}' but got: ${err.message}`,\n );\n }\n }\n}\n\n/**\n * Assert that executing `fn` throws a `TenantIsolationError`.\n *\n * Fails with a descriptive message if `fn` completes without throwing, or if\n * it throws a different error type. Optionally verifies that the error message\n * contains a specific substring.\n *\n * Use this to verify that cross-tenant data access attempts are correctly\n * blocked by the interceptor.\n *\n * @param fn - Async function that should throw `TenantIsolationError`.\n * @param messageContains - Optional substring the error message must include.\n *\n * @example\n * ```typescript\n * await withTenant({ tenantId: 'tenant-a' }, async () => {\n * await assertTenantIsolationViolation(async () => {\n * // Attempt to filter by a different tenant\n * await collection.list({ where: { tenantId: 'tenant-b' } });\n * });\n * });\n * ```\n *\n * @see assertTenantContextRequired\n * @see TenantIsolationError\n */\nexport async function assertTenantIsolationViolation(\n fn: () => Promise<unknown>,\n messageContains?: string,\n): Promise<void> {\n try {\n await fn();\n throw new Error('Expected TenantIsolationError but no error was thrown');\n } catch (error: unknown) {\n const err = error as Error & { code?: string };\n if (err.code !== 'TENANT_ISOLATION_VIOLATION') {\n throw new Error(\n `Expected TenantIsolationError but got ${err.constructor.name}: ${err.message}`,\n );\n }\n if (messageContains && !err.message.includes(messageContains)) {\n throw new Error(\n `Expected error message to contain '${messageContains}' but got: ${err.message}`,\n );\n }\n }\n}\n"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { a as getCurrentTenant, c as isSuperAdminBypass, d as requireTenantId, f as withSuperAdminBypass, h as withTenantSync, i as enterTenantContext, l as isSystemContext, m as withTenant, n as TenantContextError, o as getTenantId, p as withSystemContext, r as TenantIsolationError, s as hasTenantContext, t as TenantContext, u as requireTenant } from "./chunks/context-CwbLwyIV.js";
2
2
  import { n as createExpressMiddleware, r as createCliContext, t as createSvelteKitHandle } from "./chunks/adapters-B4hMNcuw.js";
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-Bp23qg6E.js";
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.2","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}`));
6
+ ObjectRegistry.registerPackageManifest(JSON.parse(`{"version":"1.0.0","timestamp":0,"packageName":"@happyvertical/smrt-tenancy","packageVersion":"0.55.3","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}`));
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.2\\\",\\\"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.3\\\",\\\"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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"}
@@ -2,7 +2,7 @@
2
2
  "version": "1.0.0",
3
3
  "timestamp": 0,
4
4
  "packageName": "@happyvertical/smrt-tenancy",
5
- "packageVersion": "0.55.2",
5
+ "packageVersion": "0.55.3",
6
6
  "objects": {
7
7
  "@happyvertical/smrt-tenancy:BillingRelationship": {
8
8
  "name": "billingrelationship",
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAOH;;;;;;;;GAQG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;;;OAKG;IACH,IAAI,EAAE,UAAU,GAAG,UAAU,CAAC;IAE9B;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,UAAU,EAAE,OAAO,CAAC;IAEpB;;;OAGG;IACH,YAAY,EAAE,OAAO,CAAC;IAEtB;;;OAGG;IACH,qBAAqB,EAAE,OAAO,CAAC;CAChC;AAuFD,8EAA8E;AAC9E,wBAAgB,+BAA+B,CAC7C,MAAM,EAAE,QAAQ,EAChB,MAAM,GAAE,OAAO,CAAC,kBAAkB,CAAM,GACvC,IAAI,CAIN;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,MAAM,EACjB,MAAM,GAAE,OAAO,CAAC,kBAAkB,CAAM,GACvC,IAAI,CAqBN;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAWnE;AA4LD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,MAAM,GAChB,kBAAkB,GAAG,SAAS,CAMhC;AAgBD;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAE9D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,IAAI,GAAG,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAE3E;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,IAAI,IAAI,CAOhD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,WAAW,yBAAyB;IACxC,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,iEAAiE;IACjE,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EACA,sBAAsB,GACtB,+BAA+B,GAC/B,yBAAyB,CAAC;IAC9B,QAAQ,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;IACvC,OAAO,EAAE,MAAM,CAAC;CACjB;AA0ED;;;;;;;;GAQG;AACH,wBAAgB,8BAA8B,IAAI,yBAAyB,EAAE,CAS5E"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAOH;;;;;;;;GAQG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;;;OAKG;IACH,IAAI,EAAE,UAAU,GAAG,UAAU,CAAC;IAE9B;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,UAAU,EAAE,OAAO,CAAC;IAEpB;;;OAGG;IACH,YAAY,EAAE,OAAO,CAAC;IAEtB;;;OAGG;IACH,qBAAqB,EAAE,OAAO,CAAC;CAChC;AAuFD,8EAA8E;AAC9E,wBAAgB,+BAA+B,CAC7C,MAAM,EAAE,QAAQ,EAChB,MAAM,GAAE,OAAO,CAAC,kBAAkB,CAAM,GACvC,IAAI,CAIN;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,MAAM,EACjB,MAAM,GAAE,OAAO,CAAC,kBAAkB,CAAM,GACvC,IAAI,CAqBN;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAWnE;AA4LD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,MAAM,GAChB,kBAAkB,GAAG,SAAS,CAMhC;AAgBD;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAE9D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,IAAI,GAAG,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAE3E;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,IAAI,IAAI,CAOhD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,WAAW,yBAAyB;IACxC,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,iEAAiE;IACjE,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EACA,sBAAsB,GACtB,+BAA+B,GAC/B,yBAAyB,CAAC;IAC9B,QAAQ,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;IACvC,OAAO,EAAE,MAAM,CAAC;CACjB;AAyGD;;;;;;;;GAQG;AACH,wBAAgB,8BAA8B,IAAI,yBAAyB,EAAE,CAS5E"}
@@ -3,13 +3,13 @@
3
3
  "sensitiveFieldsExcluded": true,
4
4
  "generatedAt": "1970-01-01T00:00:00.000Z",
5
5
  "packageName": "@happyvertical/smrt-tenancy",
6
- "packageVersion": "0.55.2",
6
+ "packageVersion": "0.55.3",
7
7
  "sourceManifestPath": "dist/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "754e045cabf9d87dd510641e9a6da2a8b9aa343ecdd1b1bc34b619185cb48ae7",
11
- "packageJson": "abff52afd58b22092916980c7359512ef4818cf4f18b7b1de546b89b9513a14b",
12
- "agents": "20e30d6d8792dd857be3004437a3775fcff4d005e37947d765b6a9855ca89bc9"
10
+ "manifest": "db0dfb5eb6eaa6119f218f408b220c9541b9823cbbde6d13264c6d321cf6d66a",
11
+ "packageJson": "a59eae184f5288276e013d7ac60ee4afb34be2b306980794bf8520d1834e45e3",
12
+ "agents": "400de55ccbb88e4fba9f29a8d0e9c104488f166e232182057fece627914d05e1"
13
13
  },
14
14
  "exports": [
15
15
  ".",
@@ -149,5 +149,5 @@
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 — the model has no such field), `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
153
  }
package/dist/testing.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as setupTestTenancy, i as resetTenancy, n as assertTenantIsolationViolation, o as testTenantIsolation, r as createTestTenantContext, t as assertTenantContextRequired } from "./chunks/testing-Bp23qg6E.js";
1
+ import { a as setupTestTenancy, i as resetTenancy, n as assertTenantIsolationViolation, o as testTenantIsolation, r as createTestTenantContext, t as assertTenantContextRequired } from "./chunks/testing-Cs_C_Q-y.js";
2
2
  export { assertTenantContextRequired, assertTenantIsolationViolation, createTestTenantContext, resetTenancy, setupTestTenancy, testTenantIsolation };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-tenancy",
3
- "version": "0.55.2",
3
+ "version": "0.55.3",
4
4
  "smrtJsdoc": "strict",
5
5
  "description": "Production-ready multi-tenancy framework for SMRT with automatic tenant isolation and enforcement",
6
6
  "type": "module",
@@ -43,9 +43,9 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "@happyvertical/logger": "^0.102.2",
46
- "@happyvertical/smrt-core": "0.55.2",
47
- "@happyvertical/smrt-types": "0.55.2",
48
- "@happyvertical/smrt-ui": "0.55.2",
46
+ "@happyvertical/smrt-core": "0.55.3",
47
+ "@happyvertical/smrt-types": "0.55.3",
48
+ "@happyvertical/smrt-ui": "0.55.3",
49
49
  "@happyvertical/sql": "^0.102.2",
50
50
  "@happyvertical/utils": "^0.102.2"
51
51
  },
@@ -58,7 +58,7 @@
58
58
  }
59
59
  },
60
60
  "devDependencies": {
61
- "@happyvertical/smrt-vitest": "0.55.2",
61
+ "@happyvertical/smrt-vitest": "0.55.3",
62
62
  "@sveltejs/package": "^2.5.8",
63
63
  "@sveltejs/vite-plugin-svelte": "^7.3.0",
64
64
  "@types/node": "24.13.6",
@@ -1 +0,0 @@
1
- {"version":3,"file":"testing-Bp23qg6E.js","names":["tenantField","where"],"sources":["../../src/registry.ts","../../src/enabled-state.ts","../../src/entry-point.ts","../../src/tenant-global-read-scope.ts","../../src/tenant-global-queries.ts","../../src/interceptor.ts","../../src/testing.ts"],"sourcesContent":["/**\n * Tenant-Scoped Class Registry\n *\n * Tracks which classes are tenant-scoped and their configuration.\n * Used by the interceptor to determine how to handle operations.\n *\n * This registry supports two patterns:\n * 1. @TenantScoped() decorator + tenantId field (original pattern)\n * 2. @smrt({ tenantScoped: true }) in smrt-core (Issue #688 pattern)\n *\n * Both patterns are automatically recognized by the interceptor.\n *\n * @see https://github.com/happyvertical/smrt/issues/675\n * @see https://github.com/happyvertical/smrt/issues/688\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport { ObjectRegistry, toSnakeCase } from '@happyvertical/smrt-core';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Resolved tenancy configuration for a single class, as stored in the registry.\n *\n * Every field has a concrete (non-optional) value — defaults are applied by\n * `registerTenantScopedClass()` when the class is registered via `@TenantScoped()`.\n *\n * @see TenantScopedOptions\n * @see registerTenantScopedClass\n */\nexport interface TenantScopedConfig {\n /**\n * Tenancy mode for this class\n * - 'required': Must have tenant context for all operations\n * - 'optional': Works with or without tenant context\n * @default 'required'\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\n */\n allowSuperAdminBypass: boolean;\n}\n\nconst DEFAULT_CONFIG: TenantScopedConfig = {\n mode: 'required',\n field: 'tenantId',\n autoFilter: true,\n autoPopulate: true,\n allowSuperAdminBypass: false,\n};\n\n// Registry snapshot exposed by getAllTenantScopedClasses().\nconst tenantScopedClasses = new Map<string, TenantScopedConfig>();\n\n// Direct callers select a class by string. Keep simple and qualified selectors\n// separate: a simple selector may be bound only after core proves that exactly\n// one constructor owns that name.\nconst directSimpleRegistrations = new Map<string, TenantScopedConfig>();\nconst directQualifiedRegistrations = new Map<string, TenantScopedConfig>();\nconst directSimpleBindings = new Map<\n string,\n { qualifiedName: string; constructor: Function }\n>();\n\n// Decorators retain a simple mirror only until core has registered their\n// authoritative qualified policy. Qualified runtime resolution always defers\n// to core, preserving manifest and explicit-@smrt precedence.\nconst unregisteredDecoratorRegistrations = new Map<\n string,\n TenantScopedConfig\n>();\n\n// Selectors already audited (and warned about) since their last registration.\nconst auditedSelectors = new Set<string>();\n\nfunction isQualifiedClassName(className: string): boolean {\n return className.includes(':');\n}\n\nfunction isCurrentDirectSimpleBinding(binding: {\n qualifiedName: string;\n constructor: Function;\n}): boolean {\n return (\n ObjectRegistry.getClassByQualifiedName(binding.qualifiedName)\n ?.constructor === binding.constructor\n );\n}\n\nfunction bindDirectSimpleRegistration(className: string): void {\n const config = directSimpleRegistrations.get(className);\n if (!config || directSimpleBindings.has(className)) return;\n\n const matches = ObjectRegistry.findClassesByName(className);\n if (matches.length !== 1 || !matches[0].qualifiedName) return;\n\n directSimpleBindings.set(className, {\n qualifiedName: matches[0].qualifiedName,\n constructor: matches[0].constructor,\n });\n warnOnRegistrationMismatch(className);\n}\n\nfunction getDirectSimpleRegistration(\n className: string,\n): TenantScopedConfig | undefined {\n const config = directSimpleRegistrations.get(className);\n if (!config) return undefined;\n\n const binding = directSimpleBindings.get(className);\n if (binding && !isCurrentDirectSimpleBinding(binding)) {\n throw new Error(\n `Stale tenant-scoped class registration '${className}'; ` +\n 'unregister and register it again for the current constructor.',\n );\n }\n\n const matches = ObjectRegistry.findClassesByName(className);\n if (matches.length > 1) {\n throw new Error(\n `Ambiguous tenant-scoped class registration '${className}'; ` +\n 'register an explicit qualified class name instead.',\n );\n }\n\n return config;\n}\n\n/** @internal Used by TenantScoped; direct callers must use the string API. */\nexport function registerTenantScopedConstructor(\n target: Function,\n config: Partial<TenantScopedConfig> = {},\n): void {\n const resolved = { ...DEFAULT_CONFIG, ...config };\n unregisteredDecoratorRegistrations.set(target.name, resolved);\n tenantScopedClasses.set(target.name, resolved);\n}\n\n/**\n * Register a class as tenant-scoped with the given configuration.\n *\n * Call this directly when you cannot use decorators (e.g., third-party classes\n * or plain objects in tests). Defaults from `DEFAULT_CONFIG` are merged over\n * any omitted options. `@TenantScoped()` has its own constructor-aware mirror\n * and reconciles its authoritative policy in core.\n *\n * A simple selector binds to its exact core constructor when one owner is\n * uniquely resolvable, including when registration happens before core. Once\n * bound it remains attached to that constructor if a same-name peer appears.\n * If core clears that constructor and reuses its qualified name, the selector\n * fails closed until the caller explicitly unregisters and re-registers it.\n * If ownership is ambiguous before binding, interception fails closed until a\n * caller registers an explicit qualified selector. Calling this again for the\n * same selector overwrites that selector's previous entry.\n *\n * @param className - A simple class name (e.g., `'Document'`) or exact core\n * qualified name (e.g., `'@package/name:Document'`).\n * @param config - Partial tenancy configuration; omitted fields receive defaults.\n *\n * @example\n * ```typescript\n * // Manually register a class (e.g., for testing)\n * registerTenantScopedClass('Document', { mode: 'optional' });\n * ```\n *\n * @see TenantScoped\n * @see unregisterTenantScopedClass\n */\nexport function registerTenantScopedClass(\n className: string,\n config: Partial<TenantScopedConfig> = {},\n): void {\n const resolved = {\n ...DEFAULT_CONFIG,\n ...config,\n };\n tenantScopedClasses.set(className, resolved);\n\n auditedSelectors.delete(className);\n if (isQualifiedClassName(className)) {\n directQualifiedRegistrations.set(className, resolved);\n warnOnRegistrationMismatch(className);\n return;\n }\n\n directSimpleRegistrations.set(className, resolved);\n const existingBinding = directSimpleBindings.get(className);\n if (existingBinding && !isCurrentDirectSimpleBinding(existingBinding)) {\n directSimpleBindings.delete(className);\n }\n bindDirectSimpleRegistration(className);\n warnOnRegistrationMismatch(className);\n}\n\n/**\n * Remove a class from the tenant-scoped registry.\n *\n * Primarily intended for test teardown — use `clearTenantScopedRegistry()` to\n * reset the entire registry at once.\n *\n * @param className - The class name to remove (e.g., `'Document'`).\n *\n * @see clearTenantScopedRegistry\n * @see registerTenantScopedClass\n */\nexport function unregisterTenantScopedClass(className: string): void {\n auditedSelectors.delete(className);\n tenantScopedClasses.delete(className);\n if (isQualifiedClassName(className)) {\n directQualifiedRegistrations.delete(className);\n return;\n }\n\n directSimpleRegistrations.delete(className);\n directSimpleBindings.delete(className);\n unregisteredDecoratorRegistrations.delete(className);\n}\n\n/**\n * Return a shallow copy of a config so callers can never mutate the stored\n * registration. The same backing object is shared by the base and every\n * inheriting descendant, so handing out the reference would let an accidental\n * caller mutation silently corrupt the base (and all children). (#1598 review)\n */\nfunction cloneConfig(config: TenantScopedConfig): TenantScopedConfig {\n return { ...config };\n}\n\n/**\n * Resolve a class's OWN tenancy configuration — no STI inheritance, EXACT name\n * match only.\n *\n * Checks the registration mechanisms in order: an explicit direct selector,\n * then core's declared policy. `@TenantScoped()` reconciles its policy in core;\n * its simple-name mirror is used only for unregistered test doubles.\n *\n * Lookups are by exact name only — no simple-name fallback — so a qualified\n * lookup (e.g. `@happyvertical/smrt-affiliates:Payout`, explicitly not scoped)\n * can never strip its namespace and match a same-simple-name scoped class in\n * another package (e.g. `@happyvertical/smrt-commerce:Payout`). (#1598 review)\n */\nfunction getDirectTenantScopedConfig(\n className: string,\n): TenantScopedConfig | undefined {\n const direct = getDeclaredTenantScopedConfig(className);\n const qualifiedName = ObjectRegistry.getClass(className)?.qualifiedName;\n if (\n direct &&\n qualifiedName &&\n ObjectRegistry.getRuntimeOverride(qualifiedName)?.tenancy\n ) {\n return { ...direct, mode: 'required' };\n }\n return direct;\n}\n\nfunction getDeclaredTenantScopedConfig(\n className: string,\n): TenantScopedConfig | undefined {\n // Core marks caught silent-manifest/runtime-decorator conflicts invalid.\n // Check before the simple-name decorator mirror so every identity path\n // fails closed rather than falling through to an unscoped operation.\n ObjectRegistry.assertTenantScopedRegistrationValid(className);\n // 1. Explicit direct qualified selector.\n const directQualified = directQualifiedRegistrations.get(className);\n if (directQualified) {\n // A qualified selector registered before core learned the class is\n // audited on first use instead.\n if (!auditedSelectors.has(className)) warnOnRegistrationMismatch(className);\n return cloneConfig(directQualified);\n }\n\n const registered = isQualifiedClassName(className)\n ? ObjectRegistry.getClassByQualifiedName(className)\n : ObjectRegistry.getClass(className);\n\n // A direct simple selector can bind lazily after registration-before-core.\n // It is never inferred from a qualified name when more than one core class\n // owns that simple name.\n if (registered) {\n const simple = registered.name;\n const bound = directSimpleBindings.get(simple);\n if (bound) {\n if (\n bound.qualifiedName === className &&\n bound.constructor === registered.constructor\n ) {\n return cloneConfig(directSimpleRegistrations.get(simple)!);\n }\n // Core can clear and re-register a qualified name with a different\n // constructor. Never transfer the old selector binding to it: the caller\n // must explicitly unregister/re-register after that lifecycle reset.\n if (!isCurrentDirectSimpleBinding(bound)) {\n throw new Error(\n `Stale tenant-scoped class registration '${simple}'; ` +\n 'unregister and register it again for the current constructor.',\n );\n }\n }\n if (!bound && directSimpleRegistrations.has(simple)) {\n const matches = ObjectRegistry.findClassesByName(simple);\n if (matches.length > 1) {\n throw new Error(\n `Ambiguous tenant-scoped class registration '${simple}'; ` +\n 'register an explicit qualified class name instead.',\n );\n }\n bindDirectSimpleRegistration(simple);\n const rebound = directSimpleBindings.get(simple);\n if (\n rebound?.qualifiedName === className &&\n rebound.constructor === registered.constructor\n ) {\n return cloneConfig(directSimpleRegistrations.get(simple)!);\n }\n }\n }\n\n if (!isQualifiedClassName(className)) {\n // Explicit direct selectors retain their established precedence. For\n // plain-object/test-double paths this is the historical simple-selector\n // fallback, subject to the existing ambiguity checks.\n const directSimple = getDirectSimpleRegistration(className);\n if (directSimple) return cloneConfig(directSimple);\n }\n\n // 2. Core registry (@smrt({ tenantScoped: true }) pattern - Issue #688).\n // findClass() resolves qualified names package-safely, so this branch is\n // already disambiguated.\n const coreConfig = ObjectRegistry.getTenantScopedConfig(className);\n if (coreConfig) {\n // Convert core config to TenantScopedConfig format\n return {\n mode: coreConfig.mode,\n field: coreConfig.field,\n autoFilter: coreConfig.autoFilter,\n autoPopulate: coreConfig.autoPopulate,\n allowSuperAdminBypass: coreConfig.allowSuperAdminBypass,\n };\n }\n\n // A decorator mirror is only authoritative when core has no registration.\n // A registered unqualified consumer still has a canonical simple identity.\n if (!registered && !isQualifiedClassName(className)) {\n const decoratorConfig = unregisteredDecoratorRegistrations.get(className);\n if (decoratorConfig) return cloneConfig(decoratorConfig);\n }\n\n return undefined;\n}\n\n/**\n * Resolve tenancy configuration inherited from an STI/ancestor class.\n *\n * `@TenantScoped` (and `@smrt({ tenantScoped })`) register ONLY the exact class\n * decorated — recognition does NOT propagate to subclasses. Before #1596 this\n * meant an STI child with its own collection (the child is the collection's\n * `_itemClass`) was treated as non-tenant-scoped at runtime: the interceptor\n * skipped tenant filtering on its `list()`/`get()` (cross-tenant reads), skipped\n * tenant population in `beforeSave`, and skipped the raw-SQL policy. Manual\n * re-declaration on every child was the fragile pattern that already bit images\n * (#1407) and messages.\n *\n * We now walk the STI inheritance chain so any descendant of a tenant-scoped\n * base is recognized automatically and inherits the base's config. A subclass\n * of a tenant-scoped class is always itself tenant-scoped — there is no safe\n * reason for it to opt out — so the walk intentionally covers any inheritance\n * (the motivating leak is STI child collections, but this is correct for CTI\n * hierarchies too).\n *\n * Ancestors are walked from nearest-to-self toward the root, returning the\n * first tenant-scoped ancestor's config so a closer ancestor wins. The class's\n * OWN declaration is resolved by the direct lookup in `getTenantScopedConfig`\n * and always takes precedence over anything inherited here.\n */\nfunction getInheritedTenantScopedConfig(\n className: string,\n): TenantScopedConfig | undefined {\n // getInheritanceChain returns [root, ..., self] (qualified names where the\n // class has package context). It is cached by core and only reached here when\n // the direct lookup misses, so the per-call cost on non-tenant classes is a\n // cache hit plus this short loop. Returns [] for unregistered classes.\n const chain = ObjectRegistry.getInheritanceChain(className);\n // chain[length - 1] is the class itself (already covered by the direct\n // lookup); walk its ancestors from nearest to root.\n for (let i = chain.length - 2; i >= 0; i--) {\n const ancestor = chain[i];\n\n // Exact, package-safe match first — covers `@smrt({ tenantScoped })` bases\n // (resolved through the core registry by qualified name) and any class\n // whose @TenantScoped key matches the chain entry verbatim.\n const direct = getDirectTenantScopedConfig(ancestor);\n if (direct) {\n return direct;\n }\n\n // Do not bridge a qualified ancestor back to a simple registration here.\n // The exact lookup above reaches the core declaration reconciled by the\n // decorator. Stripping would let an unrelated same-name peer lend its\n // direct or decorator policy to this inheritance chain.\n }\n return undefined;\n}\n\n/**\n * Retrieve the resolved tenancy configuration for a class.\n *\n * Resolution order:\n * 1. The class's OWN declaration — local `@TenantScoped()` registry first, then\n * the core `@smrt({ tenantScoped: true })` registry.\n * 2. STI inheritance — the nearest tenant-scoped ancestor's config (#1596).\n *\n * A class that declares its own tenancy never reaches step 2, so an explicit\n * child `@TenantScoped` always overrides the inherited base config.\n *\n * @param className - The class name to look up.\n * @returns The `TenantScopedConfig` if the class is tenant-scoped directly or\n * by inheritance, or `undefined` if it is not.\n *\n * @see isTenantScopedClass\n * @see getAllTenantScopedClasses\n */\nexport function getTenantScopedConfig(\n className: string,\n): TenantScopedConfig | undefined {\n // A class's own @TenantScoped / @smrt({ tenantScoped }) declaration wins.\n const direct = getDirectTenantScopedConfig(className);\n if (direct) return direct;\n // Otherwise inherit recognition from a tenant-scoped STI ancestor (#1596).\n return getInheritedTenantScopedConfig(className);\n}\n\n/**\n * Tell core which column records a row's owner for every tenant-scoped class,\n * including runtime registrations on a custom field\n * (`registerTenantScopedClass('Team', { field: 'organizationId' })`) that the\n * manifest knows nothing about. Core's natural-key save guard compares these\n * columns, so a save whose key collides with another organization's row is\n * refused (`TENANT_ISOLATION_VIOLATION`) instead of adopting it. Resolved on\n * every lookup, so selectors that bind later or are unregistered are honoured.\n */\nObjectRegistry.registerOwnershipColumnSource((qualifiedName) => {\n const config = getTenantScopedConfig(qualifiedName);\n return config ? [toSnakeCase(config.field)] : undefined;\n});\n\n/**\n * Return `true` if the named class is tenant-scoped — directly (via\n * `@TenantScoped()` / `@smrt({ tenantScoped: true })`) or by inheriting from a\n * tenant-scoped STI ancestor (#1596).\n *\n * @param className - The class name to look up (e.g., `'Document'`).\n * @returns `true` if the class is tenant-scoped by any mechanism.\n *\n * @see getTenantScopedConfig\n * @see registerTenantScopedClass\n */\nexport function isTenantScopedClass(className: string): boolean {\n return getTenantScopedConfig(className) !== undefined;\n}\n\n/**\n * Return a snapshot of all classes registered via `@TenantScoped()`.\n *\n * Returns a new `Map` so mutations to the returned value do not affect the\n * internal registry. Note that classes registered only through the core\n * `ObjectRegistry` (`@smrt({ tenantScoped: true })`) are **not** included in\n * this map.\n *\n * @returns A copy of the local tenant-scoped class registry, keyed by class name.\n *\n * @see isTenantScopedClass\n * @see getTenantScopedConfig\n */\nexport function getAllTenantScopedClasses(): Map<string, TenantScopedConfig> {\n return new Map(tenantScopedClasses);\n}\n\n/**\n * Remove all entries from the local tenant-scoped class registry.\n *\n * Intended for test teardown via `resetTenancy()`. Does not affect\n * registrations held by the core `ObjectRegistry`.\n *\n * @see resetTenancy\n * @see unregisterTenantScopedClass\n */\nexport function clearTenantScopedRegistry(): void {\n auditedSelectors.clear();\n tenantScopedClasses.clear();\n directSimpleRegistrations.clear();\n directQualifiedRegistrations.clear();\n directSimpleBindings.clear();\n unregisteredDecoratorRegistrations.clear();\n}\n\n/**\n * One finding from {@link auditTenantScopedRegistrations}.\n *\n * - `missing_tenant_field` (`error`): the class has no field named by the\n * registration, so the interceptor filters and populates a column that does\n * not exist in the model's manifest/schema.\n * - `natural_key_not_tenant_scoped` (`warning`): the class's upsert conflict\n * target (and so its unique index) omits the tenant column — its natural key\n * is unique ACROSS tenants. The interceptor hides other tenants' rows from\n * lookups, so a second tenant's same-key save collides with a row it cannot\n * see; core refuses that save (`TENANT_ISOLATION_VIOLATION`) instead of\n * overwriting the other tenant's row, but the second tenant still cannot\n * store the key.\n * - `undeclared_tenant_scope` (`info`): tenant scope exists only as this\n * runtime registration; the model's manifest declares no tenancy. Its\n * default natural key is still tenant-led when it has a `tenantId` field,\n * but generated surfaces, knowledge, and schema contracts cannot see the\n * read policy. Declaring `@smrt({ tenantScoped })` makes it visible.\n */\nexport interface TenantRegistrationFinding {\n /** The selector passed to `registerTenantScopedClass()`. */\n selector: string;\n /** The core class it resolved to (qualified name when known). */\n className: string;\n kind:\n | 'missing_tenant_field'\n | 'natural_key_not_tenant_scoped'\n | 'undeclared_tenant_scope';\n severity: 'error' | 'warning' | 'info';\n message: string;\n}\n\nfunction resolveSelectorClass(selector: string) {\n if (isQualifiedClassName(selector)) {\n return ObjectRegistry.getClassByQualifiedName(selector);\n }\n const bound = directSimpleBindings.get(selector);\n if (bound) return ObjectRegistry.getClassByQualifiedName(bound.qualifiedName);\n const matches = ObjectRegistry.findClassesByName(selector);\n return matches.length === 1 ? matches[0] : undefined;\n}\n\nfunction auditSelector(\n selector: string,\n config: TenantScopedConfig,\n): TenantRegistrationFinding[] {\n const registered = resolveSelectorClass(selector);\n if (!registered) return [];\n const className = registered.qualifiedName || registered.name;\n const findings: TenantRegistrationFinding[] = [];\n\n if (!registered.fields.has(config.field)) {\n findings.push({\n selector,\n className,\n kind: 'missing_tenant_field',\n severity: 'error',\n message:\n `${className} is registered tenant-scoped on field '${config.field}', ` +\n 'but the model declares no such field. Tenant filtering and population ' +\n 'target a column its manifest and schema do not have. Add the field ' +\n '(or declare `@smrt({ tenantScoped })`) and regenerate the manifest.',\n });\n return findings;\n }\n\n if (!ObjectRegistry.getTenantScopedConfig(className)) {\n findings.push({\n selector,\n className,\n kind: 'undeclared_tenant_scope',\n severity: 'info',\n message:\n `${className} is tenant-scoped only by runtime registration; its ` +\n 'manifest declares no tenancy. Declare `@smrt({ tenantScoped })` so ' +\n 'schema, generated surfaces and knowledge agree with the read policy.',\n });\n }\n\n const tenantColumn = toSnakeCase(config.field);\n const conflictColumns = ObjectRegistry.getConflictColumns(className);\n const keyedOnPrimaryKey =\n conflictColumns.length === 1 && conflictColumns[0] === 'id';\n if (!keyedOnPrimaryKey && !conflictColumns.includes(tenantColumn)) {\n findings.push({\n selector,\n className,\n kind: 'natural_key_not_tenant_scoped',\n severity: 'warning',\n message:\n `${className} is registered tenant-scoped, but its natural key ` +\n `(${conflictColumns.join(', ')}) omits '${tenantColumn}', so it is ` +\n 'unique across tenants: a second tenant cannot store a key another ' +\n 'tenant already uses. A new object saved on such a key is refused ' +\n '(TENANT_ISOLATION_VIOLATION) when the row it collides with has a ' +\n `different '${tenantColumn}'; one whose slug was derived from its ` +\n 'name moves to a free slug instead. ' +\n `Include '${tenantColumn}' in the conflict columns (or drop the ` +\n 'explicit conflictColumns) and run `smrt db:migrate`.',\n });\n }\n return findings;\n}\n\n/**\n * Audit every string-selector registration (`registerTenantScopedClass()`)\n * against the core model it resolves to. Selectors whose class core has not\n * registered yet are skipped (they are audited when they bind).\n *\n * Registration also runs this per selector and logs `error`/`warning`\n * findings once, so a mismatch surfaces at startup; call this directly for a\n * complete report (for example from a health check or a test).\n */\nexport function auditTenantScopedRegistrations(): TenantRegistrationFinding[] {\n const findings: TenantRegistrationFinding[] = [];\n for (const [selector, config] of directQualifiedRegistrations) {\n findings.push(...auditSelector(selector, config));\n }\n for (const [selector, config] of directSimpleRegistrations) {\n findings.push(...auditSelector(selector, config));\n }\n return findings;\n}\n\nfunction warnOnRegistrationMismatch(selector: string): void {\n if (auditedSelectors.has(selector)) return;\n const config =\n directQualifiedRegistrations.get(selector) ??\n directSimpleRegistrations.get(selector);\n if (!config) return;\n let findings: TenantRegistrationFinding[];\n try {\n findings = auditSelector(selector, config);\n } catch {\n // Ambiguous or stale identities fail closed on use; the audit stays quiet.\n return;\n }\n if (!resolveSelectorClass(selector)) return; // not in core yet; retry later\n auditedSelectors.add(selector);\n for (const finding of findings) {\n if (finding.severity === 'info') continue;\n logger.warn(`[smrt-tenancy] ${finding.message}`);\n }\n}\n","/**\n * Shared tenancy-enabled flag.\n *\n * Holds the single boolean toggled by `enableTenancy()` / `disableTenancy()`.\n * It lives in its own leaf module (importing nothing from the package) so that\n * both `interceptor.ts` and `entry-point.ts` can read it without forming a\n * circular import: `interceptor.ts` imports `runTenantScopedEntryPoint` from\n * `entry-point.ts`, and `entry-point.ts` needs the enabled flag — routing the\n * flag through here keeps that dependency one-directional.\n */\n\nlet enabled = false;\n\n/**\n * Set the global tenancy-enabled flag. Internal — called by `enableTenancy()` /\n * `disableTenancy()` in `interceptor.ts`.\n *\n * @param value - `true` to mark tenancy enabled, `false` to clear it.\n */\nexport function setTenancyEnabled(value: boolean): void {\n enabled = value;\n}\n\n/**\n * Return `true` if tenant enforcement is currently active.\n *\n * @returns Whether `enableTenancy()` has been called without a later\n * `disableTenancy()`.\n */\nexport function isTenancyEnabled(): boolean {\n return enabled;\n}\n","/**\n * Fail-closed tenant-context establishment for non-web entry points (#1554).\n *\n * The SvelteKit/Express adapters establish tenant context from the authenticated\n * request principal, so the web surface of a `@TenantScoped({ mode: 'optional' })`\n * model never reads across tenants without an active context. A generated\n * in-process entry point has no request principal, so an invocation with no\n * active context would fall through the interceptor's optional-mode\n * pass-through and return rows across **all** tenants.\n *\n * `runTenantScopedEntryPoint()` closes that gap. `@happyvertical/smrt-core`'s\n * `MCPGenerator` is the only in-repo generated surface that wraps its\n * per-tool execution in this gate today (via `setTenantEntryPointRunner`\n * below). Core's `CLIGenerator`, which used to wrap its per-command execution\n * in the same gate, was retired as unused public API (#2664); the live local\n * CLI transport (`packages/cli/src/cli-generator.ts`, the shipped `smrt\n * <object>:<action>` binary) has never called this gate and is not\n * tenant-isolation fail-closed today.\n *\n * @see createCliContext for a hand-wired CLI runner (resolveTenantId,\n * super-admin) a consuming application can use directly — independent of\n * the generated-surface gate above and unaffected by #2664.\n */\n\nimport {\n hasTenantContext,\n isSystemContext,\n TenantContextError,\n withSystemContext,\n withTenant,\n} from './context.js';\nimport { isTenancyEnabled } from './enabled-state.js';\nimport { isTenantScopedClass } from './registry.js';\n\n/**\n * Inputs for {@link runTenantScopedEntryPoint}.\n *\n * Provide **either** `className` (the gate resolves tenant-scoping from the\n * authoritative tenancy registry — the same source the interceptor uses, so it\n * covers both `@TenantScoped` and `@smrt({ tenantScoped })` registrations) or an\n * explicit `tenantScoped` boolean (when the caller already resolved it, e.g. a\n * build-time generated surface). An explicit boolean wins when both are given.\n */\nexport interface TenantEntryPointOptions {\n /**\n * Class name of the target model. When provided, tenant-scoping is resolved\n * via `isTenantScopedClass(className)`.\n */\n className?: string;\n\n /**\n * Explicit tenant-scoping decision. Overrides `className` resolution when set.\n * Non-scoped models always pass through unchanged — the gate is a no-op.\n */\n tenantScoped?: boolean;\n\n /**\n * Explicit operator-provided tenant selector (CLI `--tenant <id>`, MCP\n * `context.tenantId`). When present (and no context is already active) the\n * function runs inside this tenant's context.\n */\n tenantId?: string | null;\n\n /**\n * Explicit operator opt-in to cross-tenant / system access (CLI\n * `--all-tenants`, an MCP host that trusts the caller as an operator). When\n * set the function runs in system context, bypassing tenant filtering.\n *\n * @default false\n */\n allowCrossTenant?: boolean;\n\n /**\n * Human-facing surface name used in the fail-closed error message, e.g.\n * `'CLI'` or `'MCP'`.\n *\n * @default 'entry point'\n */\n surface?: string;\n}\n\n/**\n * Run `fn` inside an appropriate tenant context for a generated in-process\n * entry point (MCP today, see the module docblock above), failing closed for\n * tenant-scoped models when no authorized context can be established.\n *\n * Resolution order (tenant-scoped models only):\n * 1. A tenant context is already active, or an explicit `withSystemContext()`\n * bypass is in effect (e.g. `runAsSystem()`, migrations) → run as-is.\n * 2. `allowCrossTenant` was explicitly set → run in system context. Checked\n * before `tenantId` so an explicit cross-tenant opt-in wins over a default\n * principal/host tenant rather than being silently scoped.\n * 3. An explicit `tenantId` was provided → run inside that tenant.\n * 4. Tenancy is enabled but none of the above → **throw** `TenantContextError`\n * (the fail-closed branch — never silently read across tenants).\n * 5. Tenancy is disabled (single-/no-tenant deployment) → pass through.\n *\n * Non-tenant-scoped models always pass straight through.\n *\n * @param options - {@link TenantEntryPointOptions}.\n * @param fn - The command/tool body to execute.\n * @returns The resolved value of `fn`.\n * @throws {TenantContextError} When a tenant-scoped model is reached with\n * tenancy enabled and no tenant/cross-tenant selector.\n */\nexport async function runTenantScopedEntryPoint<T>(\n options: TenantEntryPointOptions,\n fn: () => Promise<T>,\n): Promise<T> {\n const {\n className,\n tenantScoped,\n tenantId,\n allowCrossTenant = false,\n surface = 'entry point',\n } = options;\n\n // Resolve tenant-scoping: an explicit boolean wins; otherwise consult the\n // authoritative tenancy registry by class name (matches the interceptor).\n const scoped =\n typeof tenantScoped === 'boolean'\n ? tenantScoped\n : className\n ? isTenantScopedClass(className)\n : false;\n\n // Non-scoped models run as-is. So do calls already inside a tenant context\n // (an upstream handle) or an explicit system-context bypass — the interceptor\n // honors `withSystemContext()` (migrations, `runAsSystem()`), so the gate must\n // not fail-close over it (hasTenantContext() is false for the system marker).\n if (!scoped) return fn();\n if (hasTenantContext() || isSystemContext()) return fn();\n\n // Explicit operator opt-in to cross-tenant access. Checked before the tenant\n // selector so a deliberate `--all-tenants` / `allowCrossTenant` overrides a\n // default host/principal tenant instead of being silently scoped to it.\n if (allowCrossTenant) {\n return withSystemContext(fn);\n }\n\n // Explicit tenant selector.\n if (typeof tenantId === 'string' && tenantId) {\n return withTenant({ tenantId }, fn);\n }\n\n // Fail closed: tenancy is on but the caller gave us nothing to scope by.\n if (isTenancyEnabled()) {\n throw new TenantContextError(\n `Tenant context required for tenant-scoped access via ${surface}. ` +\n 'Pass an explicit tenant (e.g. --tenant <id> / a tenantId) or opt into ' +\n 'cross-tenant access (e.g. --all-tenants) to read across all tenants.',\n );\n }\n\n // Tenancy disabled → single-tenant deployment, pass through.\n return fn();\n}\n","import { AsyncLocalStorage } from 'node:async_hooks';\n\n// Separate from actor context: only the built-in list interceptor consumes it.\n// Share across duplicate module graphs, as with the normal tenant context.\nconst KEY = Symbol.for('smrt:tenant-global-read-scope');\nconst root = globalThis as typeof globalThis & {\n [KEY]?: AsyncLocalStorage<string>;\n};\nroot[KEY] ??= new AsyncLocalStorage<string>();\nconst storage = root[KEY];\n\nexport function getTenantGlobalReadScope(): string | undefined {\n return storage.getStore();\n}\n\nexport function runTenantGlobalReadScope<T>(\n tenantId: string,\n callback: () => Promise<T>,\n): Promise<T> {\n return storage.run(tenantId, callback);\n}\n","/**\n * Shared raw-SQL helpers for tenant-scoped collections' \"global\" and\n * \"tenant + globals\" lookups (#1600).\n *\n * Most domain models are `@TenantScoped`. Their collections historically\n * hand-rolled two helpers the OLD way:\n *\n * ```typescript\n * async findGlobal() { return this.list({ where: { tenantId: null } }); }\n * async findWithGlobals(tid) { return this.query(\n * `SELECT * FROM ${this.tableName} WHERE tenant_id = ? OR tenant_id IS NULL`, [tid]); }\n * ```\n *\n * Under an ACTIVE tenant context with tenancy enabled (default\n * `rawQueryPolicy: 'throw'`) BOTH break:\n * - `findGlobal()` routes an explicit `tenant_id IS NULL` filter through\n * `list()`, which the interceptor flags as an isolation violation → throws.\n * - `findWithGlobals()` issues unflagged raw SQL on a tenant-scoped class,\n * which `beforeQuery` blocks → throws.\n * - `findWithGlobals()` also trusts the caller-supplied `tenantId`, so once the\n * raw bypass is added a caller under tenant-A could read tenant-B by passing\n * B's id.\n *\n * These helpers run raw with `{ allowRawOnTenantScoped: true }` (carrying the\n * tenant predicate themselves), and `queryWithGlobals` re-implements the\n * isolation guard the bypass disables (`assertTenantReadAllowed`): a caller\n * under tenant-A must not read tenant-B's rows by passing tenant-B's id. A\n * system / super-admin-bypass context keeps the deliberate cross-tenant\n * capability for admin paths.\n *\n * STI scoping is derived automatically from the collection's item class via\n * `collection.getStiChildMetaType()` (smrt-core), which mirrors the\n * `_meta_type` scoping `list()` applies: STI **child** collections scope the\n * shared table to their own subtype, while STI **base** and CTI collections do\n * not (a base legitimately spans subtypes; CTI tables have no `_meta_type`).\n * Callers never hand-classify their collection. Promoted from\n * `@happyvertical/smrt-messages` (#1596) so every package shares one\n * implementation.\n */\n\nimport type { SmrtCollection, SmrtObject } from '@happyvertical/smrt-core';\nimport {\n getCurrentTenant,\n isSuperAdminBypass,\n TenantIsolationError,\n} from './context.js';\n\nimport { runTenantGlobalReadScope } from './tenant-global-read-scope.js';\n\n/**\n * Fail closed when an active tenant context requests a different tenant's rows.\n *\n * @param tenantId - The tenant id the caller asked for.\n * @param label - `Class.method` identifier for the error message.\n * @throws {TenantIsolationError} when a non-bypass tenant context is active and\n * does not match `tenantId`.\n */\nexport function assertTenantReadAllowed(tenantId: string, label: string): void {\n const tenantContext = getCurrentTenant();\n if (\n tenantContext &&\n !isSuperAdminBypass() &&\n tenantContext.tenantId !== tenantId\n ) {\n throw new TenantIsolationError(\n `Tenant isolation violation in ${label}: context tenant is ` +\n `'${tenantContext.tenantId}' but query requested '${tenantId}'`,\n { tenantId: tenantContext.tenantId, attemptedTenantId: tenantId },\n );\n }\n}\n\n/**\n * Allow list reads of an authorized tenant and global rows without changing\n * actor identity. Only the built-in tenancy beforeList hook consumes this\n * capability; custom authorization hooks still see the original caller.\n * Other operations (get/query/save/delete) retain their normal guards.\n */\nexport async function withTenantGlobalRead<T>(\n tenantId: string,\n callback: () => Promise<T>,\n): Promise<T> {\n if (typeof tenantId !== 'string' || !tenantId.trim()) {\n throw new Error('withTenantGlobalRead requires a nonempty tenant ID');\n }\n assertTenantReadAllowed(tenantId, 'withTenantGlobalRead');\n return runTenantGlobalReadScope(tenantId, callback);\n}\n\n/**\n * Return all global (tenant-less) rows for a tenant-scoped collection.\n *\n * STI child collections are auto-scoped to their own `_meta_type` (via\n * `collection.getStiChildMetaType()`) so the shared table never returns sibling\n * subtypes; STI base / CTI collections are not scoped.\n *\n * @param collection - The tenant-scoped collection to query.\n */\nexport async function queryGlobal<T, M extends SmrtObject = SmrtObject>(\n collection: SmrtCollection<M>,\n): Promise<T[]> {\n const metaType = collection.getStiChildMetaType();\n const where = metaType\n ? 'WHERE _meta_type = ? AND tenant_id IS NULL'\n : 'WHERE tenant_id IS NULL';\n const params = metaType ? [metaType] : [];\n // Two decoupled type params by design (STI). `M` is inferred from the\n // collection's declared item type — the STI *base* (e.g. `Email`) — and keeps\n // the parameter assignable despite `SmrtCollection`'s contravariant\n // `ModelType` positions. `T` is the caller-declared *row* type: an STI child\n // collection (e.g. `EmailAccountCollection`, statically `SmrtCollection<Email>`)\n // filters by `_meta_type` and hydrates child rows (`EmailAccount`) that differ\n // from `M`. `query()` is statically `M[]` but yields those child instances at\n // runtime, so the bridge cast is required — returning `M[]` would break every\n // STI-child caller.\n return (await collection.query(\n `SELECT * FROM ${collection.tableName} ${where}`,\n params,\n { allowRawOnTenantScoped: true },\n )) as unknown as T[];\n}\n\n/**\n * Return a tenant's rows plus all global rows for a tenant-scoped collection.\n *\n * Fails closed (`assertTenantReadAllowed`) before issuing the bypassed query.\n * STI child collections are auto-scoped to their own `_meta_type` (via\n * `collection.getStiChildMetaType()`); STI base / CTI collections are not.\n *\n * @param collection - The tenant-scoped collection to query.\n * @param tenantId - The tenant id to include alongside globals.\n * @param label - `Class.method` identifier for the isolation error message.\n */\nexport async function queryWithGlobals<T, M extends SmrtObject = SmrtObject>(\n collection: SmrtCollection<M>,\n tenantId: string,\n label: string,\n): Promise<T[]> {\n assertTenantReadAllowed(tenantId, label);\n const metaType = collection.getStiChildMetaType();\n const where = metaType\n ? 'WHERE _meta_type = ? AND (tenant_id = ? OR tenant_id IS NULL)'\n : 'WHERE tenant_id = ? OR tenant_id IS NULL';\n const params = metaType ? [metaType, tenantId] : [tenantId];\n // See `queryGlobal` above: `T` (caller's STI child row type) is intentionally\n // decoupled from `M` (the collection's inferred base type), so the cast\n // bridges `query()`'s static `M[]` to the hydrated child rows.\n return (await collection.query(\n `SELECT * FROM ${collection.tableName} ${where}`,\n params,\n { allowRawOnTenantScoped: true },\n )) as unknown as T[];\n}\n","/**\n * Tenant Interceptor - Core enforcement mechanism\n *\n * Registers with GlobalInterceptors in smrt-core to automatically:\n * - Filter queries by tenant ID\n * - Validate tenant context on save/delete\n * - Block or audit raw SQL on tenant-scoped classes\n *\n * @see https://github.com/happyvertical/smrt/issues/675\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport type { SmrtObject } from '@happyvertical/smrt-core';\nimport {\n type CollectionInterceptor,\n type DispatchBus,\n GlobalInterceptors,\n type InterceptorContext,\n type ListOptions,\n type QueryInterceptResult,\n type QueryOptions,\n resolveGetStringFilter,\n setDispatchTenantResolver,\n setTenantEntryPointRunner,\n setTenantScopedClassResolver,\n} from '@happyvertical/smrt-core';\nimport {\n getCurrentTenant,\n getTenantId,\n isSuperAdminBypass,\n isSystemContext,\n TenantContextError,\n TenantIsolationError,\n} from './context.js';\nimport { isTenancyEnabled, setTenancyEnabled } from './enabled-state.js';\nimport { runTenantScopedEntryPoint } from './entry-point.js';\nimport { getTenantScopedConfig, isTenantScopedClass } from './registry.js';\n\nimport { assertTenantReadAllowed } from './tenant-global-queries.js';\nimport { getTenantGlobalReadScope } from './tenant-global-read-scope.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Policy controlling what happens when raw SQL is executed against a\n * tenant-scoped class without an explicit bypass.\n *\n * - `'throw'` — Raises a `TenantIsolationError` (most secure; default).\n * - `'warn'` — Logs a `console.warn` but allows the query to proceed (useful\n * during migration periods).\n * - `'allow'` — Silently allows the query; not recommended for production.\n *\n * @see TenantInterceptorOptions.rawQueryPolicy\n * @see enableTenancy\n */\nexport type RawQueryPolicy = 'throw' | 'warn' | 'allow';\n\n/**\n * Configuration options accepted by `createTenantInterceptor()` and\n * `enableTenancy()`.\n *\n * All options are optional; reasonable defaults are applied. The callback\n * hooks (`onRawQuery`, `onMissingContext`, `onIsolationViolation`) are useful\n * for logging and alerting without altering the enforcement behaviour.\n *\n * @see createTenantInterceptor\n * @see enableTenancy\n */\nexport interface TenantInterceptorOptions {\n /**\n * Policy for raw SQL queries on tenant-scoped classes\n * - 'throw': Throw error (most secure, default)\n * - 'warn': Log warning but allow (for migration)\n * - 'allow': Silently allow (not recommended for production)\n * @default 'throw'\n */\n rawQueryPolicy?: RawQueryPolicy;\n\n /**\n * Called when a raw query is attempted on a tenant-scoped class\n * Useful for logging/auditing\n */\n onRawQuery?: (\n className: string,\n sql: string,\n context: InterceptorContext,\n ) => void;\n\n /**\n * Called when tenant context is missing for a tenant-scoped operation\n */\n onMissingContext?: (\n className: string,\n operation: string,\n context: InterceptorContext,\n ) => void;\n\n /**\n * Called when an isolation violation is detected\n */\n onIsolationViolation?: (\n className: string,\n expectedTenantId: string,\n actualTenantId: string,\n context: InterceptorContext,\n ) => void;\n\n /**\n * DispatchBus instance for emitting provisioning events on lifecycle changes.\n * When provided along with directoryClasses, afterSave/afterDelete hooks\n * emit dispatches like `directory.membership.created`.\n */\n dispatchBus?: DispatchBus;\n\n /**\n * Class names to emit directory dispatches for on save/delete lifecycle events.\n * Only classes listed here will trigger dispatch emissions.\n * @example ['Tenant', 'Membership', 'User']\n */\n directoryClasses?: string[];\n}\n\nconst DEFAULT_OPTIONS: TenantInterceptorOptions = {\n rawQueryPolicy: 'throw',\n};\n\nfunction getTenancyIdentity(\n className: string,\n context: InterceptorContext,\n): string {\n return context.qualifiedClassName ?? className;\n}\n\n/**\n * Extract a plain-object snapshot of an instance for dispatch payloads.\n *\n * Prefers `toJSON()` when available (all real SmrtObject instances) because\n * it returns only data fields and excludes internal handles like `_db`, `_ai`,\n * and `_fs` which may contain circular references (e.g. connection pools with\n * Timeout objects).\n *\n * @see https://github.com/happyvertical/smrt/issues/946\n */\nfunction serializeInstance(\n instance: SmrtObject,\n className: string,\n): Record<string, unknown> {\n // Documented exception to the \"never call toJSON() directly\" convention\n // (docs/content/standards.md §7): the interceptor must serialize whatever\n // instance is handed to it, including workspace stubs and plain-object\n // doubles used in unit tests whose classes may not extend SmrtObject and\n // therefore have no `transformJSON()` hook. Using `toJSON()` here is a\n // duck-typed fallback — when present, it strips framework-internal handles\n // for us; when absent, we fall through to manual key iteration below.\n const maybeToJSON = (instance as { toJSON?: unknown }).toJSON;\n if (typeof maybeToJSON === 'function') {\n return {\n className,\n ...(maybeToJSON.call(instance) as Record<string, unknown>),\n };\n }\n\n // Fallback for plain-object stubs (e.g. in unit tests):\n // skip functions and framework-internal properties\n const result: Record<string, unknown> = { className };\n const record = instance as unknown as Record<string, unknown>;\n for (const key of Object.keys(instance)) {\n const value = record[key];\n if (typeof value !== 'function') {\n result[key] = value;\n }\n }\n return result;\n}\n\n/**\n * Create a `CollectionInterceptor` that enforces tenant isolation on all\n * `SmrtCollection` operations.\n *\n * The returned interceptor hooks into the smrt-core `GlobalInterceptors`\n * pipeline at priority 100 (runs before all other interceptors) and\n * handles the following lifecycle hooks:\n *\n * | Hook | Behaviour |\n * |---------------|-----------|\n * | `beforeList` | Injects tenant filter into `WHERE`; validates explicit filters. |\n * | `beforeGet` | Resolves string lookups (id vs slug) and adds the tenant predicate. |\n * | `beforeSave` | Auto-populates `tenantId`; validates existing values. |\n * | `beforeDelete`| Validates the instance's `tenantId` matches context. |\n * | `beforeQuery` | Enforces `rawQueryPolicy` on raw SQL calls. |\n * | `afterSave` | Emits `directory.<class>.created/updated` via `dispatchBus`. |\n * | `afterDelete` | Emits `directory.<class>.deleted` via `dispatchBus`. |\n *\n * Use `enableTenancy()` to register the interceptor globally. Call this\n * directly only when you need multiple interceptor instances (e.g., for\n * isolated tests or feature flags).\n *\n * @param options - Configuration for the interceptor.\n * @returns A `CollectionInterceptor` ready to be registered with\n * `GlobalInterceptors.register()`.\n *\n * @example\n * ```typescript\n * import { createTenantInterceptor } from '@happyvertical/smrt-tenancy';\n * import { GlobalInterceptors } from '@happyvertical/smrt-core';\n *\n * const interceptor = createTenantInterceptor({ rawQueryPolicy: 'warn' });\n * GlobalInterceptors.register(interceptor);\n * ```\n *\n * @see enableTenancy\n * @see TenantInterceptorOptions\n */\nexport function createTenantInterceptor(\n options: TenantInterceptorOptions = {},\n): CollectionInterceptor {\n const opts = { ...DEFAULT_OPTIONS, ...options };\n\n return {\n name: 'smrt-tenancy',\n priority: 100, // High priority - should run first\n\n /**\n * Before list: Add tenant filter to queries\n */\n beforeList(\n className: string,\n listOptions: ListOptions,\n context: InterceptorContext,\n ): ListOptions | undefined {\n // Check if this class is tenant-scoped\n const tenancyIdentity = getTenancyIdentity(className, context);\n if (!isTenantScopedClass(tenancyIdentity)) {\n return; // Not tenant-scoped, pass through\n }\n\n // Check for super admin bypass\n if (isSuperAdminBypass()) {\n return; // Bypass enabled, pass through\n }\n\n // Check for system context (explicit bypass via withSystemContext)\n if (isSystemContext()) {\n return; // System context bypasses tenant checks\n }\n\n const config = getTenantScopedConfig(tenancyIdentity);\n const tenantContext = getCurrentTenant();\n const globalReadTenant = getTenantGlobalReadScope();\n if (globalReadTenant !== undefined) {\n // Recheck if nested code changed actors after entering the capability.\n assertTenantReadAllowed(globalReadTenant, 'tenant/global list');\n const tenantField = config?.field || 'tenantId';\n const where = listOptions.where || {};\n const groups = Array.isArray(where) ? where : [[where]];\n if (!groups.length || groups.some((group) => !group.length)) {\n throw new Error('Invalid DNF where clause for tenant/global list');\n }\n return {\n ...listOptions,\n where: groups.flatMap((group) => [\n [...group, { [tenantField]: globalReadTenant }],\n [...group, { [tenantField]: null }],\n ]),\n };\n }\n\n // If no tenant context and mode is 'required', throw\n if (!tenantContext) {\n if (config?.mode === 'required') {\n opts.onMissingContext?.(className, 'list', context);\n throw new TenantContextError(\n `Tenant context required for listing ${className}. ` +\n `Use withTenant() or configure TenantContext middleware.`,\n );\n }\n return; // Mode is 'optional', allow without filtering\n }\n\n // Add tenant filter to where clause\n const tenantField = config?.field || 'tenantId';\n const where = listOptions.where || {};\n\n // Preserve DNF predicates (outer OR, inner AND) by applying the tenant\n // predicate to every branch. Flattening this shape into an object would\n // make one branch escape tenant scope, so validate direct tenant filters\n // branch-by-branch before adding the current tenant where absent.\n if (Array.isArray(where)) {\n if (\n where.length === 0 ||\n where.some((andGroup) => andGroup.length === 0)\n ) {\n throw new Error(\n 'Invalid DNF where clause: every OR branch must contain at least one condition',\n );\n }\n const scopedWhere = where.map((andGroup) => {\n const directValues = andGroup.flatMap((condition) => {\n if (!Object.hasOwn(condition, tenantField)) return [];\n const value = condition[tenantField];\n return Array.isArray(value) ? value : [value];\n });\n const offendingIndex = directValues.findIndex(\n (value) => value !== tenantContext.tenantId,\n );\n if (offendingIndex !== -1) {\n const offending = directValues[offendingIndex];\n opts.onIsolationViolation?.(\n className,\n tenantContext.tenantId,\n String(offending),\n context,\n );\n throw new TenantIsolationError(\n `Tenant isolation violation in ${className} query: ` +\n `context tenant is '${tenantContext.tenantId}' but query filters by '${String(offending)}'`,\n {\n tenantId: tenantContext.tenantId,\n attemptedTenantId: String(offending),\n },\n );\n }\n return directValues.length > 0\n ? andGroup\n : [...andGroup, { [tenantField]: tenantContext.tenantId }];\n });\n return { ...listOptions, where: scopedWhere };\n }\n\n // Check if tenant filter is already present\n if (tenantField in where) {\n // Validate it matches context. The filter may be a scalar\n // (`tenantId: 'x'`) or an IN-style array (`tenantId: ['x']`) —\n // smrt-core auto-converts array values to SQL IN clauses, so an\n // array containing only the context tenant is a valid filter.\n // See https://github.com/happyvertical/smrt/issues/1495\n const existingFilter = where[tenantField];\n const filterValues = Array.isArray(existingFilter)\n ? existingFilter\n : [existingFilter];\n // findIndex (not find) so a literal null/undefined filter value is\n // still flagged as a violation rather than mistaken for \"not found\"\n const offendingIndex = filterValues.findIndex(\n (value) => value !== tenantContext.tenantId,\n );\n if (offendingIndex !== -1) {\n const offending = filterValues[offendingIndex];\n opts.onIsolationViolation?.(\n className,\n tenantContext.tenantId,\n String(offending),\n context,\n );\n throw new TenantIsolationError(\n `Tenant isolation violation in ${className} query: ` +\n `context tenant is '${tenantContext.tenantId}' but query filters by '${String(offending)}'`,\n {\n tenantId: tenantContext.tenantId,\n attemptedTenantId: String(offending),\n },\n );\n }\n return; // Filter already correct\n }\n\n // Inject tenant filter\n return {\n ...listOptions,\n where: {\n ...where,\n [tenantField]: tenantContext.tenantId,\n },\n };\n },\n\n /**\n * Before get: Add tenant filter to single record fetches\n */\n beforeGet(\n className: string,\n filter: string | Record<string, unknown>,\n context: InterceptorContext,\n ): string | Record<string, unknown> | undefined {\n const tenancyIdentity = getTenancyIdentity(className, context);\n if (!isTenantScopedClass(tenancyIdentity)) {\n return;\n }\n\n if (isSuperAdminBypass()) {\n return;\n }\n\n // Check for system context (explicit bypass via withSystemContext)\n if (isSystemContext()) {\n return; // System context bypasses tenant checks\n }\n\n const config = getTenantScopedConfig(tenancyIdentity);\n const tenantContext = getCurrentTenant();\n\n if (!tenantContext) {\n if (config?.mode === 'required') {\n opts.onMissingContext?.(className, 'get', context);\n throw new TenantContextError(\n `Tenant context required for getting ${className}. ` +\n `Use withTenant() or configure TenantContext middleware.`,\n );\n }\n return;\n }\n\n const tenantField = config?.field || 'tenantId';\n\n // If filter is a string, resolve it exactly the way core's `get()` would\n // (UUID -> id lookup, anything else -> slug/context natural key) and add\n // the tenant predicate to whichever shape it resolves to. Rewriting every\n // string to `{ id: filter }` broke get-by-slug under a tenant context:\n // null on SQLite, a uuid cast error on PostgreSQL (#2365).\n if (typeof filter === 'string') {\n return {\n ...resolveGetStringFilter(filter),\n [tenantField]: tenantContext.tenantId,\n };\n }\n\n // Add tenant filter to object\n if (!(tenantField in filter)) {\n return {\n ...filter,\n [tenantField]: tenantContext.tenantId,\n };\n }\n\n // Validate existing filter. Like beforeList, accept scalar or\n // IN-style array filters (smrt-core auto-converts arrays to SQL IN).\n // See https://github.com/happyvertical/smrt/issues/1495\n const existingFilter = filter[tenantField];\n const filterValues = Array.isArray(existingFilter)\n ? existingFilter\n : [existingFilter];\n // findIndex (not find) so a literal null/undefined filter value is\n // still flagged as a violation rather than mistaken for \"not found\"\n const offendingIndex = filterValues.findIndex(\n (value) => value !== tenantContext.tenantId,\n );\n if (offendingIndex !== -1) {\n const offending = filterValues[offendingIndex];\n opts.onIsolationViolation?.(\n className,\n tenantContext.tenantId,\n String(offending),\n context,\n );\n throw new TenantIsolationError(\n `Tenant isolation violation in ${className} get: ` +\n `context tenant is '${tenantContext.tenantId}' but query filters by '${String(offending)}'`,\n {\n tenantId: tenantContext.tenantId,\n attemptedTenantId: String(offending),\n },\n );\n }\n\n return;\n },\n\n /**\n * Before query: Handle raw SQL on tenant-scoped classes\n */\n beforeQuery(\n className: string,\n queryOptions: QueryOptions,\n context: InterceptorContext,\n ): QueryInterceptResult | undefined {\n const tenancyIdentity = getTenancyIdentity(className, context);\n if (!isTenantScopedClass(tenancyIdentity)) {\n return;\n }\n\n // Check for explicit bypass flag\n if (queryOptions.allowRawOnTenantScoped) {\n opts.onRawQuery?.(className, queryOptions.sql, context);\n return; // Explicitly allowed\n }\n\n if (isSuperAdminBypass()) {\n opts.onRawQuery?.(className, queryOptions.sql, context);\n return;\n }\n\n // Check for system context (explicit bypass via withSystemContext)\n if (isSystemContext()) {\n opts.onRawQuery?.(className, queryOptions.sql, context);\n return;\n }\n\n // Handle based on policy\n const message =\n `Raw SQL query attempted on tenant-scoped class ${className}. ` +\n `Use list()/get() for automatic tenant filtering, or call ` +\n `query() with { allowRawOnTenantScoped: true } if you're handling ` +\n `tenant filtering manually.`;\n\n opts.onRawQuery?.(className, queryOptions.sql, context);\n\n switch (opts.rawQueryPolicy) {\n case 'throw':\n throw new TenantIsolationError(message);\n\n case 'warn':\n logger.warn(`[smrt-tenancy] WARNING: ${message}`);\n return;\n default:\n return;\n }\n },\n\n /**\n * Before save: Validate tenant ID is set and matches context\n */\n bulkMutation: {\n // Directory dispatch observes individual writes and remains sequential.\n // Custom error callbacks may observe partial progress, so also fall back.\n compatible: (className: string) =>\n !(opts.dispatchBus && opts.directoryClasses?.includes(className)) &&\n !opts.onMissingContext &&\n !opts.onIsolationViolation,\n },\n\n beforeSave(instance: SmrtObject, context: InterceptorContext): void {\n // Use context.className which is always correct\n // (instance.constructor.name may not match for proxies or plain objects in tests)\n const className = context.className;\n\n // Stash isNew flag for afterSave dispatch detection\n if (opts.directoryClasses?.includes(className)) {\n const id = (instance as unknown as Record<string, unknown>).id;\n context.metadata = {\n ...context.metadata,\n _directoryIsNew: id === undefined || id === null,\n };\n }\n\n const tenancyIdentity = getTenancyIdentity(className, context);\n if (!isTenantScopedClass(tenancyIdentity)) {\n return;\n }\n\n if (isSuperAdminBypass()) {\n return;\n }\n\n // Check for system context (explicit bypass via withSystemContext)\n if (isSystemContext()) {\n return; // System context bypasses tenant checks\n }\n\n const config = getTenantScopedConfig(tenancyIdentity);\n const tenantField = config?.field || 'tenantId';\n const instanceRecord = instance as unknown as Record<string, unknown>;\n const instanceTenantId = instanceRecord[tenantField];\n\n const tenantContext = getCurrentTenant();\n\n // Check if tenant context is required\n if (!tenantContext) {\n if (config?.mode === 'required') {\n opts.onMissingContext?.(className, 'save', context);\n throw new TenantContextError(\n `Tenant context required for saving ${className}. ` +\n `Use withTenant() or configure TenantContext middleware.`,\n );\n }\n return; // Mode is 'optional'\n }\n\n // Auto-populate tenant ID if not set\n if (!instanceTenantId && config?.autoPopulate !== false) {\n instanceRecord[tenantField] = tenantContext.tenantId;\n return;\n }\n\n // Validate tenant ID matches context\n if (instanceTenantId && instanceTenantId !== tenantContext.tenantId) {\n const attemptedTenantId = String(instanceTenantId);\n opts.onIsolationViolation?.(\n className,\n tenantContext.tenantId,\n attemptedTenantId,\n context,\n );\n throw new TenantIsolationError(\n `Tenant isolation violation: cannot save ${className} with ` +\n `tenantId '${attemptedTenantId}' in context of tenant '${tenantContext.tenantId}'`,\n {\n tenantId: tenantContext.tenantId,\n attemptedTenantId,\n },\n );\n }\n },\n\n /**\n * Before delete: Validate instance belongs to current tenant\n */\n beforeDelete(instance: SmrtObject, context: InterceptorContext): void {\n // Use context.className which is always correct\n const className = context.className;\n\n const tenancyIdentity = getTenancyIdentity(className, context);\n if (!isTenantScopedClass(tenancyIdentity)) {\n return;\n }\n\n if (isSuperAdminBypass()) {\n return;\n }\n\n // Check for system context (explicit bypass via withSystemContext)\n if (isSystemContext()) {\n return; // System context bypasses tenant checks\n }\n\n const config = getTenantScopedConfig(tenancyIdentity);\n const tenantField = config?.field || 'tenantId';\n const instanceTenantId = (instance as unknown as Record<string, unknown>)[\n tenantField\n ];\n\n const tenantContext = getCurrentTenant();\n\n if (!tenantContext) {\n if (config?.mode === 'required') {\n opts.onMissingContext?.(className, 'delete', context);\n throw new TenantContextError(\n `Tenant context required for deleting ${className}. ` +\n `Use withTenant() or configure TenantContext middleware.`,\n );\n }\n return;\n }\n\n // Validate tenant ID matches\n if (instanceTenantId && instanceTenantId !== tenantContext.tenantId) {\n const attemptedTenantId = String(instanceTenantId);\n opts.onIsolationViolation?.(\n className,\n tenantContext.tenantId,\n attemptedTenantId,\n context,\n );\n throw new TenantIsolationError(\n `Tenant isolation violation: cannot delete ${className} with ` +\n `tenantId '${attemptedTenantId}' in context of tenant '${tenantContext.tenantId}'`,\n {\n tenantId: tenantContext.tenantId,\n attemptedTenantId,\n },\n );\n }\n },\n\n /**\n * After save: Emit directory dispatch for configured classes\n */\n async afterSave(\n instance: SmrtObject,\n context: InterceptorContext,\n ): Promise<void> {\n if (\n !opts.dispatchBus ||\n !opts.directoryClasses?.includes(context.className)\n )\n return;\n\n const instanceId = (instance as unknown as Record<string, unknown>).id;\n const sourceId = typeof instanceId === 'string' ? instanceId : undefined;\n const rawIsNew = context.metadata?._directoryIsNew;\n const isNew =\n typeof rawIsNew === 'boolean' ? rawIsNew : instanceId == null;\n const event = isNew\n ? `directory.${context.className.toLowerCase()}.created`\n : `directory.${context.className.toLowerCase()}.updated`;\n\n await opts.dispatchBus.emit(\n event,\n serializeInstance(instance, context.className),\n {\n source: 'smrt-tenancy',\n sourceId,\n },\n );\n },\n\n /**\n * After delete: Emit directory dispatch for configured classes\n */\n async afterDelete(\n instance: SmrtObject,\n context: InterceptorContext,\n ): Promise<void> {\n if (\n !opts.dispatchBus ||\n !opts.directoryClasses?.includes(context.className)\n )\n return;\n\n const instanceId = (instance as unknown as Record<string, unknown>).id;\n await opts.dispatchBus.emit(\n `directory.${context.className.toLowerCase()}.deleted`,\n serializeInstance(instance, context.className),\n {\n source: 'smrt-tenancy',\n sourceId: typeof instanceId === 'string' ? instanceId : undefined,\n },\n );\n },\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Registration Functions\n// ─────────────────────────────────────────────────────────────────────────────\n\n// The enabled flag lives in `enabled-state.ts` (a leaf module) so `entry-point.ts`\n// can read it without importing this module — breaking the otherwise-circular\n// interceptor ↔ entry-point dependency.\nlet registeredInterceptor: CollectionInterceptor | null = null;\n\n/**\n * Enable tenant enforcement globally\n *\n * Call this once at application startup to enable automatic tenant isolation.\n *\n * @param options - Configuration options\n *\n * @example\n * ```typescript\n * // In your app initialization\n * import { enableTenancy } from '@happyvertical/smrt-tenancy';\n *\n * enableTenancy({\n * rawQueryPolicy: 'throw',\n * onMissingContext: (className, operation) => {\n * console.error(`Missing tenant context for ${operation} on ${className}`);\n * }\n * });\n * ```\n */\nexport function enableTenancy(options: TenantInterceptorOptions = {}): void {\n if (isTenancyEnabled()) {\n logger.warn(\n '[smrt-tenancy] Tenancy is already enabled. Call disableTenancy() first to reconfigure.',\n );\n return;\n }\n\n registeredInterceptor = createTenantInterceptor(options);\n GlobalInterceptors.register(registeredInterceptor);\n\n // Wire the DispatchBus tenant-scope resolver (S5 #1398). Core cannot depend\n // on tenancy, so it reads the active tenant through this injected hook; the\n // bus stamps/filters dispatches by the active tenant only while tenancy is\n // enabled. Mirrors the GlobalInterceptors inversion above.\n setDispatchTenantResolver(() => getTenantId());\n\n // Wire the fail-closed tenant gate for generated in-process entry points\n // (#1554). Core's MCPGenerator invokes this runner around tenant-scoped MCP\n // execution; without it (tenancy disabled) that surface passes through\n // unchanged. Core's CLIGenerator used to invoke it too but was retired as\n // unused public API (#2664); the shipped local CLI transport\n // (packages/cli/src/cli-generator.ts) has never called it.\n setTenantEntryPointRunner(runTenantScopedEntryPoint);\n\n // Wire the tenant-scoped-class resolver so core-side fail-closed read guards\n // (generated REST read scope, #1782) recognize `@TenantScoped()`-decorated\n // classes, which record their config only in the tenancy registry.\n setTenantScopedClassResolver((className) => isTenantScopedClass(className));\n\n setTenancyEnabled(true);\n}\n\n/**\n * Disable global tenant enforcement.\n *\n * Unregisters the interceptor previously installed by `enableTenancy()` and\n * resets the internal enabled flag so `enableTenancy()` can be called again.\n * Idempotent — safe to call even when tenancy was never enabled.\n *\n * Common use-cases:\n * - Test teardown (via `resetTenancy()`).\n * - Temporarily disabling tenancy before reconfiguring with new options.\n *\n * @example\n * ```typescript\n * afterAll(() => {\n * disableTenancy();\n * });\n * ```\n *\n * @see enableTenancy\n * @see isTenancyEnabled\n * @see resetTenancy\n */\nexport function disableTenancy(): void {\n if (!isTenancyEnabled() || !registeredInterceptor) {\n return;\n }\n\n GlobalInterceptors.unregister(registeredInterceptor);\n // Clear the DispatchBus tenant resolver so the bus reverts to its no-op\n // (pre-tenancy) behavior when tenancy is disabled.\n setDispatchTenantResolver(undefined);\n // Clear the in-process tenant gate so that surface (MCP) passes through\n // (#1554).\n setTenantEntryPointRunner(undefined);\n // Clear the tenant-scoped-class resolver (#1782).\n setTenantScopedClassResolver(undefined);\n registeredInterceptor = null;\n setTenancyEnabled(false);\n}\n\n/**\n * Return `true` if tenant enforcement is currently active.\n *\n * Reflects whether `enableTenancy()` has been called and the interceptor has not\n * yet been removed by `disableTenancy()`. Re-exported from `enabled-state.ts`\n * (the shared leaf module) so the public API surface is unchanged.\n *\n * @see enableTenancy\n * @see disableTenancy\n */\nexport { isTenancyEnabled };\n","/**\n * Testing Utilities for smrt-tenancy\n *\n * Helpers for testing tenant-scoped applications.\n *\n * @example\n * ```typescript\n * import { createTestTenantContext, resetTenancy } from '@happyvertical/smrt-tenancy/testing';\n *\n * beforeEach(() => {\n * resetTenancy(); // Clear all state\n * });\n *\n * it('should filter by tenant', async () => {\n * await createTestTenantContext({ tenantId: 'tenant-1' }, async () => {\n * const docs = await collection.list({});\n * // Only tenant-1 documents\n * });\n * });\n * ```\n */\n\nimport {\n type MinimalTenantContext,\n type TenantContextData,\n withTenant,\n} from './context.js';\nimport { disableTenancy, enableTenancy } from './interceptor.js';\nimport { clearTenantScopedRegistry } from './registry.js';\n\n/**\n * Reset all tenancy state (for use in beforeEach/afterEach)\n *\n * This clears:\n * - Registered interceptors\n * - Tenant-scoped class registry\n *\n * @example\n * ```typescript\n * afterEach(() => {\n * resetTenancy();\n * });\n * ```\n */\nexport function resetTenancy(): void {\n disableTenancy();\n clearTenantScopedRegistry();\n}\n\n/**\n * Create a test tenant context and run code within it\n *\n * Convenience wrapper around withTenant() with sensible defaults for testing.\n *\n * @param context - Tenant context (can be minimal, just tenantId)\n * @param fn - Async function to run in the context\n *\n * @example\n * ```typescript\n * await createTestTenantContext({ tenantId: 'test-tenant' }, async () => {\n * const product = await collection.create({ name: 'Test' });\n * expect(product.tenantId).toBe('test-tenant');\n * });\n * ```\n */\nexport async function createTestTenantContext<T>(\n context: MinimalTenantContext | TenantContextData,\n fn: () => Promise<T>,\n): Promise<T> {\n return withTenant(context, fn);\n}\n\n/**\n * Create multiple tenant contexts for isolation testing\n *\n * @param tenantIds - Array of tenant IDs to create contexts for\n * @param fn - Function that receives an object mapping tenant IDs to context runners\n *\n * @example\n * ```typescript\n * await testTenantIsolation(['tenant-a', 'tenant-b'], async (tenants) => {\n * // Create in tenant A\n * const docA = await tenants['tenant-a'](async () => {\n * return collection.create({ title: 'A doc' });\n * });\n *\n * // Verify not visible in tenant B\n * await tenants['tenant-b'](async () => {\n * const found = await collection.get(docA.id);\n * expect(found).toBeNull();\n * });\n * });\n * ```\n */\nexport async function testTenantIsolation<T>(\n tenantIds: string[],\n fn: (\n tenants: Record<string, <R>(runner: () => Promise<R>) => Promise<R>>,\n ) => Promise<T>,\n): Promise<T> {\n const tenants: Record<string, <R>(runner: () => Promise<R>) => Promise<R>> =\n {};\n\n for (const tenantId of tenantIds) {\n tenants[tenantId] = async <R>(runner: () => Promise<R>) => {\n return withTenant({ tenantId }, runner);\n };\n }\n\n return fn(tenants);\n}\n\n/**\n * Options for `setupTestTenancy()`.\n *\n * @see setupTestTenancy\n */\nexport interface SetupTestTenancyOptions {\n /**\n * Enable tenancy interceptors\n * @default true\n */\n enableInterceptors?: boolean;\n\n /**\n * Raw query policy for tests\n * @default 'throw'\n */\n rawQueryPolicy?: 'throw' | 'warn' | 'allow';\n}\n\n/**\n * Set up tenancy for a test suite\n *\n * Call in beforeAll or at the start of tests to configure tenancy.\n *\n * @param options - Setup options\n *\n * @example\n * ```typescript\n * beforeAll(() => {\n * setupTestTenancy({ enableInterceptors: true });\n * });\n *\n * afterAll(() => {\n * resetTenancy();\n * });\n * ```\n */\nexport function setupTestTenancy(options: SetupTestTenancyOptions = {}): void {\n const { enableInterceptors = true, rawQueryPolicy = 'throw' } = options;\n\n // Clear any existing state\n resetTenancy();\n\n // Enable interceptors if requested\n if (enableInterceptors) {\n enableTenancy({ rawQueryPolicy });\n }\n}\n\n/**\n * Assert that executing `fn` throws a `TenantContextError`.\n *\n * Fails with a descriptive message if `fn` completes without throwing, or if\n * it throws a different error type. Optionally verifies that the error message\n * contains a specific substring.\n *\n * Useful for testing that business-logic code correctly rejects calls that are\n * made outside a tenant context.\n *\n * @param fn - Async function that should throw `TenantContextError`.\n * @param messageContains - Optional substring the error message must include.\n *\n * @example\n * ```typescript\n * await assertTenantContextRequired(async () => {\n * // No withTenant() in scope\n * await documentCollection.list({});\n * });\n * ```\n *\n * @see assertTenantIsolationViolation\n * @see TenantContextError\n */\nexport async function assertTenantContextRequired(\n fn: () => Promise<unknown>,\n messageContains?: string,\n): Promise<void> {\n try {\n await fn();\n throw new Error('Expected TenantContextError but no error was thrown');\n } catch (error: unknown) {\n const err = error as Error & { code?: string };\n if (err.code !== 'TENANT_CONTEXT_REQUIRED') {\n throw new Error(\n `Expected TenantContextError but got ${err.constructor.name}: ${err.message}`,\n );\n }\n if (messageContains && !err.message.includes(messageContains)) {\n throw new Error(\n `Expected error message to contain '${messageContains}' but got: ${err.message}`,\n );\n }\n }\n}\n\n/**\n * Assert that executing `fn` throws a `TenantIsolationError`.\n *\n * Fails with a descriptive message if `fn` completes without throwing, or if\n * it throws a different error type. Optionally verifies that the error message\n * contains a specific substring.\n *\n * Use this to verify that cross-tenant data access attempts are correctly\n * blocked by the interceptor.\n *\n * @param fn - Async function that should throw `TenantIsolationError`.\n * @param messageContains - Optional substring the error message must include.\n *\n * @example\n * ```typescript\n * await withTenant({ tenantId: 'tenant-a' }, async () => {\n * await assertTenantIsolationViolation(async () => {\n * // Attempt to filter by a different tenant\n * await collection.list({ where: { tenantId: 'tenant-b' } });\n * });\n * });\n * ```\n *\n * @see assertTenantContextRequired\n * @see TenantIsolationError\n */\nexport async function assertTenantIsolationViolation(\n fn: () => Promise<unknown>,\n messageContains?: string,\n): Promise<void> {\n try {\n await fn();\n throw new Error('Expected TenantIsolationError but no error was thrown');\n } catch (error: unknown) {\n const err = error as Error & { code?: string };\n if (err.code !== 'TENANT_ISOLATION_VIOLATION') {\n throw new Error(\n `Expected TenantIsolationError but got ${err.constructor.name}: ${err.message}`,\n );\n }\n if (messageContains && !err.message.includes(messageContains)) {\n throw new Error(\n `Expected error message to contain '${messageContains}' but got: ${err.message}`,\n );\n }\n }\n}\n"],"mappings":"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"}