@happyvertical/smrt-tenancy 0.30.0

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.
Files changed (70) hide show
  1. package/AGENTS.md +71 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/README.md +122 -0
  5. package/dist/__smrt-register__.d.ts +2 -0
  6. package/dist/__smrt-register__.d.ts.map +1 -0
  7. package/dist/adapters/cli.d.ts +178 -0
  8. package/dist/adapters/cli.d.ts.map +1 -0
  9. package/dist/adapters/express.d.ts +115 -0
  10. package/dist/adapters/express.d.ts.map +1 -0
  11. package/dist/adapters/index.d.ts +22 -0
  12. package/dist/adapters/index.d.ts.map +1 -0
  13. package/dist/adapters/index.js +7 -0
  14. package/dist/adapters/index.js.map +1 -0
  15. package/dist/adapters/sveltekit.d.ts +123 -0
  16. package/dist/adapters/sveltekit.d.ts.map +1 -0
  17. package/dist/chunks/context-B5CKsmMi.js +190 -0
  18. package/dist/chunks/context-B5CKsmMi.js.map +1 -0
  19. package/dist/chunks/sveltekit-9eRH1RLw.js +153 -0
  20. package/dist/chunks/sveltekit-9eRH1RLw.js.map +1 -0
  21. package/dist/chunks/testing-C_tV23JW.js +487 -0
  22. package/dist/chunks/testing-C_tV23JW.js.map +1 -0
  23. package/dist/context.d.ts +435 -0
  24. package/dist/context.d.ts.map +1 -0
  25. package/dist/decorators.d.ts +126 -0
  26. package/dist/decorators.d.ts.map +1 -0
  27. package/dist/enabled-state.d.ts +25 -0
  28. package/dist/enabled-state.d.ts.map +1 -0
  29. package/dist/entry-point.d.ts +83 -0
  30. package/dist/entry-point.d.ts.map +1 -0
  31. package/dist/fields.d.ts +104 -0
  32. package/dist/fields.d.ts.map +1 -0
  33. package/dist/index.d.ts +9 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +108 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/interceptor.d.ts +156 -0
  38. package/dist/interceptor.d.ts.map +1 -0
  39. package/dist/manifest.json +11 -0
  40. package/dist/playground.d.ts +2 -0
  41. package/dist/playground.d.ts.map +1 -0
  42. package/dist/playground.js +80 -0
  43. package/dist/playground.js.map +1 -0
  44. package/dist/registry.d.ts +145 -0
  45. package/dist/registry.d.ts.map +1 -0
  46. package/dist/smrt-knowledge.json +65 -0
  47. package/dist/svelte/components/TenantCard.svelte +272 -0
  48. package/dist/svelte/components/TenantCard.svelte.d.ts +18 -0
  49. package/dist/svelte/components/TenantCard.svelte.d.ts.map +1 -0
  50. package/dist/svelte/components/TenantSwitcher.svelte +68 -0
  51. package/dist/svelte/components/TenantSwitcher.svelte.d.ts +11 -0
  52. package/dist/svelte/components/TenantSwitcher.svelte.d.ts.map +1 -0
  53. package/dist/svelte/i18n.d.ts +5 -0
  54. package/dist/svelte/i18n.d.ts.map +1 -0
  55. package/dist/svelte/i18n.js +9 -0
  56. package/dist/svelte/index.d.ts +15 -0
  57. package/dist/svelte/index.d.ts.map +1 -0
  58. package/dist/svelte/index.js +19 -0
  59. package/dist/svelte/playground.d.ts +70 -0
  60. package/dist/svelte/playground.d.ts.map +1 -0
  61. package/dist/svelte/playground.js +75 -0
  62. package/dist/testing.d.ts +145 -0
  63. package/dist/testing.d.ts.map +1 -0
  64. package/dist/testing.js +11 -0
  65. package/dist/testing.js.map +1 -0
  66. package/dist/ui.d.ts +21 -0
  67. package/dist/ui.d.ts.map +1 -0
  68. package/dist/ui.js +33 -0
  69. package/dist/ui.js.map +1 -0
  70. package/package.json +99 -0
@@ -0,0 +1,435 @@
1
+ import { DatabaseInterface } from '@happyvertical/sql';
2
+ /**
3
+ * Full data stored in tenant context for the current async execution scope.
4
+ *
5
+ * Created by `withTenant()` / `enterTenantContext()` and read by `getCurrentTenant()`,
6
+ * `getTenantId()`, and the interceptor hooks. All fields except `tenantId` and
7
+ * `permissions` are optional and may be populated lazily by higher-level packages
8
+ * (e.g., `smrt-users`).
9
+ *
10
+ * @see withTenant
11
+ * @see MinimalTenantContext
12
+ */
13
+ export interface TenantContextData {
14
+ /** Current tenant ID (required) */
15
+ tenantId: string;
16
+ /** Current tenant object (lazy-loaded if smrt-users is available) */
17
+ tenant?: unknown;
18
+ /** Current user ID (optional) */
19
+ userId?: string;
20
+ /** Current user object (lazy-loaded if smrt-users is available) */
21
+ user?: unknown;
22
+ /** Resolved permissions for this user in this tenant */
23
+ permissions: Set<string>;
24
+ /** Database connection for this tenant (if database-per-tenant strategy) */
25
+ database?: DatabaseInterface;
26
+ /** Super admin bypass enabled - allows cross-tenant operations */
27
+ superAdminBypass?: boolean;
28
+ /** Custom metadata for application-specific data */
29
+ metadata?: Record<string, unknown>;
30
+ }
31
+ /**
32
+ * Minimal context accepted by `withTenant()` and `withTenantSync()` when only a
33
+ * tenant ID is known.
34
+ *
35
+ * `permissions` defaults to an empty `Set` when omitted. Use `TenantContextData`
36
+ * when you also need to carry user info, database handles, or resolved permissions.
37
+ *
38
+ * @see TenantContextData
39
+ * @see withTenant
40
+ */
41
+ export interface MinimalTenantContext {
42
+ /** Tenant identifier. */
43
+ tenantId: string;
44
+ /** Resolved permissions; defaults to an empty Set when omitted. */
45
+ permissions?: Set<string>;
46
+ /** When `true`, tenant auto-filtering is skipped for classes that allow super admin bypass. */
47
+ superAdminBypass?: boolean;
48
+ /** Arbitrary application-specific metadata to carry through the context. */
49
+ metadata?: Record<string, unknown>;
50
+ }
51
+ /**
52
+ * Get the current tenant context for this async execution scope.
53
+ *
54
+ * Returns `undefined` when called outside any tenant scope or inside a
55
+ * `withSystemContext()` block (the system context marker is treated as "no
56
+ * tenant data"). Prefer `requireTenant()` when a context is mandatory.
57
+ *
58
+ * @returns The active `TenantContextData`, or `undefined` if none is set.
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * const ctx = getCurrentTenant();
63
+ * if (ctx) {
64
+ * console.log('Current tenant:', ctx.tenantId);
65
+ * }
66
+ * ```
67
+ *
68
+ * @see requireTenant
69
+ * @see hasTenantContext
70
+ */
71
+ export declare function getCurrentTenant(): TenantContextData | undefined;
72
+ /**
73
+ * Get the current tenant context or throw if one is not available.
74
+ *
75
+ * Use this in business-logic code that must run inside a tenant scope.
76
+ * For a non-throwing alternative use `getCurrentTenant()`.
77
+ *
78
+ * @returns The active `TenantContextData`.
79
+ * @throws {TenantContextError} When no tenant context is set (code is outside
80
+ * any `withTenant()` call or the enclosing middleware has not run).
81
+ *
82
+ * @example
83
+ * ```typescript
84
+ * const { tenantId, permissions } = requireTenant();
85
+ * ```
86
+ *
87
+ * @see getCurrentTenant
88
+ * @see requireTenantId
89
+ */
90
+ export declare function requireTenant(): TenantContextData;
91
+ /**
92
+ * Get the current tenant ID or throw if no tenant context is available.
93
+ *
94
+ * Shorthand for `requireTenant().tenantId`.
95
+ *
96
+ * @returns The active tenant ID string.
97
+ * @throws {TenantContextError} When no tenant context is set.
98
+ *
99
+ * @example
100
+ * ```typescript
101
+ * const tenantId = requireTenantId();
102
+ * const rows = await db.query(`SELECT * FROM docs WHERE tenant_id = ?`, [tenantId]);
103
+ * ```
104
+ *
105
+ * @see getTenantId
106
+ * @see requireTenant
107
+ */
108
+ export declare function requireTenantId(): string;
109
+ /**
110
+ * Get the current tenant ID without throwing.
111
+ *
112
+ * Returns `undefined` when called outside any tenant scope or inside a
113
+ * `withSystemContext()` block. Use `requireTenantId()` when a missing context
114
+ * should be treated as an error.
115
+ *
116
+ * @returns The active tenant ID, or `undefined` if none is set.
117
+ *
118
+ * @example
119
+ * ```typescript
120
+ * const tenantId = getTenantId();
121
+ * if (tenantId) {
122
+ * // Optional tenant-scoped logic
123
+ * }
124
+ * ```
125
+ *
126
+ * @see requireTenantId
127
+ * @see hasTenantContext
128
+ */
129
+ export declare function getTenantId(): string | undefined;
130
+ /**
131
+ * Check whether the current async execution scope has an active tenant context.
132
+ *
133
+ * Returns `false` both when there is no context at all and when code is running
134
+ * inside `withSystemContext()` (the system marker is not a tenant context).
135
+ *
136
+ * @returns `true` if a `TenantContextData` is active, `false` otherwise.
137
+ *
138
+ * @example
139
+ * ```typescript
140
+ * if (hasTenantContext()) {
141
+ * console.log('Tenant:', getTenantId());
142
+ * }
143
+ * ```
144
+ *
145
+ * @see getTenantId
146
+ * @see isSystemContext
147
+ */
148
+ export declare function hasTenantContext(): boolean;
149
+ /**
150
+ * Check whether the current async execution scope was entered via `withSystemContext()`.
151
+ *
152
+ * A system context is explicitly set to bypass all tenant checks; it is distinct
153
+ * from "no context" (undefined store). When the store is undefined the
154
+ * interceptor enforces tenant requirements; when it holds the system marker the
155
+ * interceptor skips all checks.
156
+ *
157
+ * @returns `true` if inside a `withSystemContext()` call, `false` otherwise.
158
+ *
159
+ * @see withSystemContext
160
+ * @see hasTenantContext
161
+ */
162
+ export declare function isSystemContext(): boolean;
163
+ /**
164
+ * Check whether the super admin bypass flag is set in the current tenant context.
165
+ *
166
+ * When `true`, the interceptor skips tenant auto-filtering for classes that have
167
+ * `allowSuperAdminBypass: true` in their `@TenantScoped()` config. Returns
168
+ * `false` inside a system context (no tenant data is available).
169
+ *
170
+ * @returns `true` if super admin bypass is active, `false` otherwise.
171
+ *
172
+ * @see withSuperAdminBypass
173
+ * @see TenantScopedOptions.allowSuperAdminBypass
174
+ */
175
+ export declare function isSuperAdminBypass(): boolean;
176
+ /**
177
+ * Run code within a tenant context (async version)
178
+ *
179
+ * @param context - Tenant context data (at minimum, tenantId)
180
+ * @param fn - Async function to run within the tenant context
181
+ * @returns Promise resolving to the function's return value
182
+ *
183
+ * @example
184
+ * ```typescript
185
+ * await withTenant({ tenantId: 'tenant-123' }, async () => {
186
+ * const id = requireTenantId(); // 'tenant-123'
187
+ * await doSomething();
188
+ * });
189
+ * ```
190
+ */
191
+ export declare function withTenant<T>(context: TenantContextData | MinimalTenantContext, fn: () => Promise<T>): Promise<T>;
192
+ /**
193
+ * Run synchronous code within a tenant context.
194
+ *
195
+ * Prefer `withTenant()` for async code. Use this variant only when the
196
+ * callback must be synchronous (e.g., initializing a module-level value that
197
+ * is consumed synchronously downstream).
198
+ *
199
+ * @param context - Tenant context data (at minimum, `tenantId`).
200
+ * @param fn - Synchronous function to run within the tenant context.
201
+ * @returns The return value of `fn`.
202
+ *
203
+ * @example
204
+ * ```typescript
205
+ * const result = withTenantSync({ tenantId: 'tenant-123' }, () => {
206
+ * return computeSomethingSync();
207
+ * });
208
+ * ```
209
+ *
210
+ * @see withTenant
211
+ */
212
+ export declare function withTenantSync<T>(context: TenantContextData | MinimalTenantContext, fn: () => T): T;
213
+ /**
214
+ * Enter tenant context for the remainder of the current async execution
215
+ *
216
+ * This uses AsyncLocalStorage.enterWith() to establish context that persists
217
+ * until the async resource completes. Useful for Express middleware where
218
+ * the route handler executes after the middleware returns.
219
+ *
220
+ * @param context - Tenant context data
221
+ *
222
+ * @example Express middleware
223
+ * ```typescript
224
+ * app.use((req, res, next) => {
225
+ * const tenantId = req.headers['x-tenant-id'] as string;
226
+ * enterTenantContext({ tenantId });
227
+ * next(); // Route handlers now have tenant context
228
+ * });
229
+ * ```
230
+ */
231
+ export declare function enterTenantContext(context: TenantContextData | MinimalTenantContext): void;
232
+ /**
233
+ * Run code in system context (bypasses tenant checks)
234
+ *
235
+ * Use this for:
236
+ * - Migration scripts
237
+ * - Admin tools that need cross-tenant access
238
+ * - Background jobs that process multiple tenants
239
+ *
240
+ * System context is explicitly different from "no context" - it signals
241
+ * that tenant checks should be bypassed, while no context means the
242
+ * interceptor should enforce tenant requirements.
243
+ *
244
+ * @param fn - Async function to run without tenant context
245
+ *
246
+ * @example
247
+ * ```typescript
248
+ * await withSystemContext(async () => {
249
+ * // No tenant context - can access all data
250
+ * const allDocuments = await documentCollection.list({});
251
+ * });
252
+ * ```
253
+ */
254
+ export declare function withSystemContext<T>(fn: () => Promise<T>): Promise<T>;
255
+ /**
256
+ * Run async code with the super admin bypass flag enabled on the current
257
+ * tenant context.
258
+ *
259
+ * Unlike `withSystemContext()`, this does **not** remove the tenant context —
260
+ * the caller's `tenantId` remains intact. The interceptor skips
261
+ * auto-filtering only for classes that have `allowSuperAdminBypass: true` in
262
+ * their `@TenantScoped()` config.
263
+ *
264
+ * A tenant context must already be active (i.e., this must be called from
265
+ * within a `withTenant()` scope). Use `withSystemContext()` if no tenant
266
+ * context is available at all.
267
+ *
268
+ * @param fn - Async function to run with super admin bypass enabled.
269
+ * @returns Promise resolving to the return value of `fn`.
270
+ * @throws {TenantContextError} If called outside any tenant context.
271
+ *
272
+ * @example
273
+ * ```typescript
274
+ * await withTenant({ tenantId: 'admin-tenant' }, async () => {
275
+ * await withSuperAdminBypass(async () => {
276
+ * // Can read any tenant's AuditLog (if allowSuperAdminBypass: true)
277
+ * const logs = await auditLogCollection.list({});
278
+ * });
279
+ * });
280
+ * ```
281
+ *
282
+ * @see withSystemContext
283
+ * @see isSuperAdminBypass
284
+ */
285
+ export declare function withSuperAdminBypass<T>(fn: () => Promise<T>): Promise<T>;
286
+ /**
287
+ * Namespace object providing advanced tenant context utilities.
288
+ *
289
+ * Contains helpers for binding callbacks, inspecting context state, and
290
+ * running code with the context stored in a queued job payload. These
291
+ * utilities supplement the standalone exported functions for situations where
292
+ * async context might otherwise be lost (e.g., `setTimeout`, event emitters,
293
+ * message queue consumers).
294
+ *
295
+ * @example
296
+ * ```typescript
297
+ * import { TenantContext } from '@happyvertical/smrt-tenancy';
298
+ *
299
+ * // Preserve context across a setTimeout
300
+ * setTimeout(TenantContext.bind(() => {
301
+ * console.log(getTenantId()); // context is intact
302
+ * }), 500);
303
+ *
304
+ * // Process a queued job
305
+ * await TenantContext.runWithJobContext(job, async () => {
306
+ * await processJob(job);
307
+ * });
308
+ * ```
309
+ */
310
+ export declare const TenantContext: {
311
+ /**
312
+ * Bind a callback to the current tenant context
313
+ *
314
+ * Use this when passing callbacks to setTimeout, event emitters,
315
+ * or other APIs that might lose the async context.
316
+ *
317
+ * @param fn - Function to bind to current context
318
+ * @returns Wrapped function that will run in the original context
319
+ *
320
+ * @example
321
+ * ```typescript
322
+ * // Without bind - context is lost
323
+ * setTimeout(() => {
324
+ * console.log(getTenantId()); // undefined!
325
+ * }, 1000);
326
+ *
327
+ * // With bind - context is preserved
328
+ * setTimeout(TenantContext.bind(() => {
329
+ * console.log(getTenantId()); // 'tenant-123'
330
+ * }), 1000);
331
+ * ```
332
+ */
333
+ bind<T extends (...args: unknown[]) => unknown>(fn: T): T;
334
+ /**
335
+ * Get the current context data (or undefined for system/no context)
336
+ */
337
+ readonly current: TenantContextData | undefined;
338
+ /**
339
+ * Check if we're in system context
340
+ */
341
+ readonly isSystem: boolean;
342
+ /**
343
+ * Run code with context from a job/message payload
344
+ *
345
+ * Useful for processing queued jobs that include tenant metadata.
346
+ *
347
+ * @param job - Job object with tenantId in metadata
348
+ * @param fn - Function to run in the job's tenant context
349
+ *
350
+ * @example
351
+ * ```typescript
352
+ * const job = await queue.pop();
353
+ * await TenantContext.runWithJobContext(job, async () => {
354
+ * await processJob(job);
355
+ * });
356
+ * ```
357
+ */
358
+ runWithJobContext<T>(job: {
359
+ metadata?: {
360
+ tenantId?: string;
361
+ };
362
+ tenantId?: string;
363
+ }, fn: () => Promise<T>): Promise<T>;
364
+ };
365
+ /**
366
+ * Error thrown when a tenant context is required but not available.
367
+ *
368
+ * Raised by `requireTenant()`, `requireTenantId()`, and the tenant interceptor
369
+ * when a `@TenantScoped({ mode: 'required' })` operation is attempted outside
370
+ * any `withTenant()` scope.
371
+ *
372
+ * The `code` property is always `'TENANT_CONTEXT_REQUIRED'` and can be used for
373
+ * programmatic error handling.
374
+ *
375
+ * @example
376
+ * ```typescript
377
+ * try {
378
+ * const tenantId = requireTenantId();
379
+ * } catch (err) {
380
+ * if (err instanceof TenantContextError) {
381
+ * // err.code === 'TENANT_CONTEXT_REQUIRED'
382
+ * }
383
+ * }
384
+ * ```
385
+ *
386
+ * @see requireTenant
387
+ * @see requireTenantId
388
+ * @see TenantIsolationError
389
+ */
390
+ export declare class TenantContextError extends Error {
391
+ /** Stable error code; always `'TENANT_CONTEXT_REQUIRED'`. */
392
+ readonly code = "TENANT_CONTEXT_REQUIRED";
393
+ constructor(message: string);
394
+ }
395
+ /**
396
+ * Error thrown when a tenant isolation boundary is crossed.
397
+ *
398
+ * Raised by the tenant interceptor when:
399
+ * - A `list()` or `get()` query explicitly filters by a tenant ID that does not
400
+ * match the current context tenant.
401
+ * - A `save()` or `delete()` is attempted on an object whose `tenantId` field
402
+ * belongs to a different tenant than the current context.
403
+ * - A raw SQL query is executed against a tenant-scoped class without an
404
+ * explicit bypass (when `rawQueryPolicy` is `'throw'`).
405
+ *
406
+ * The `code` property is always `'TENANT_ISOLATION_VIOLATION'`.
407
+ *
408
+ * @example
409
+ * ```typescript
410
+ * try {
411
+ * await collection.list({ where: { tenantId: 'other-tenant' } });
412
+ * } catch (err) {
413
+ * if (err instanceof TenantIsolationError) {
414
+ * // err.tenantId — context tenant
415
+ * // err.attemptedTenantId — tenant that was attempted
416
+ * }
417
+ * }
418
+ * ```
419
+ *
420
+ * @see TenantContextError
421
+ * @see createTenantInterceptor
422
+ */
423
+ export declare class TenantIsolationError extends Error {
424
+ /** Stable error code; always `'TENANT_ISOLATION_VIOLATION'`. */
425
+ readonly code = "TENANT_ISOLATION_VIOLATION";
426
+ /** The tenant ID that is active in the current context. */
427
+ readonly tenantId?: string;
428
+ /** The tenant ID that was attempted (and rejected). */
429
+ readonly attemptedTenantId?: string;
430
+ constructor(message: string, details?: {
431
+ tenantId?: string;
432
+ attemptedTenantId?: string;
433
+ });
434
+ }
435
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,126 @@
1
+ import { CompatiblePropertyDecorator } from '@happyvertical/smrt-core';
2
+ import { TenantIdFieldOptions } from './fields.js';
3
+ /**
4
+ * Options accepted by the `@TenantScoped()` class decorator.
5
+ *
6
+ * All fields are optional; defaults match the most restrictive safe behaviour
7
+ * (required mode, auto-filter and auto-populate enabled, no super-admin bypass).
8
+ *
9
+ * @see TenantScoped
10
+ * @see TenantScopedConfig
11
+ */
12
+ export interface TenantScopedOptions {
13
+ /**
14
+ * Tenancy mode for this class
15
+ * - 'required': Must have tenant context for all operations (default)
16
+ * - 'optional': Works with or without tenant context
17
+ */
18
+ mode?: 'required' | 'optional';
19
+ /**
20
+ * Field name containing tenant ID
21
+ * @default 'tenantId'
22
+ */
23
+ field?: string;
24
+ /**
25
+ * Auto-filter all queries by tenant
26
+ * @default true
27
+ */
28
+ autoFilter?: boolean;
29
+ /**
30
+ * Auto-populate tenant ID from context on create
31
+ * @default true
32
+ */
33
+ autoPopulate?: boolean;
34
+ /**
35
+ * Allow super admin bypass for this class
36
+ * @default false - must be explicitly enabled
37
+ */
38
+ allowSuperAdminBypass?: boolean;
39
+ }
40
+ /**
41
+ * Mark a class as tenant-scoped
42
+ *
43
+ * This decorator registers the class with the tenancy system so that:
44
+ * - list()/get() queries are automatically filtered by tenant
45
+ * - save() validates tenant ID matches current context
46
+ * - delete() validates tenant ownership
47
+ * - Raw SQL queries trigger policy enforcement
48
+ *
49
+ * @param options - Configuration options
50
+ *
51
+ * @example Basic usage (required tenancy)
52
+ * ```typescript
53
+ * @smrt()
54
+ * @TenantScoped()
55
+ * class Document extends SmrtObject {
56
+ * @tenantId()
57
+ * tenantId: string = '';
58
+ *
59
+ * title: string = '';
60
+ * }
61
+ * ```
62
+ *
63
+ * @example With super admin bypass enabled
64
+ * ```typescript
65
+ * @smrt()
66
+ * @TenantScoped({ allowSuperAdminBypass: true })
67
+ * class AuditLog extends SmrtObject {
68
+ * @tenantId()
69
+ * tenantId: string = '';
70
+ *
71
+ * action: string = '';
72
+ * }
73
+ * ```
74
+ *
75
+ * @example Optional tenancy (works with or without context)
76
+ * ```typescript
77
+ * @smrt()
78
+ * @TenantScoped({ mode: 'optional' })
79
+ * class GlobalConfig extends SmrtObject {
80
+ * @tenantId({ nullable: true })
81
+ * tenantId: string | null = null; // null = global, string = tenant-specific
82
+ *
83
+ * key: string = '';
84
+ * value: string = '';
85
+ * }
86
+ * ```
87
+ */
88
+ export declare function TenantScoped(options?: TenantScopedOptions): <T extends Function>(target: T, decoratorContext?: ClassDecoratorContext) => T;
89
+ /**
90
+ * Tenant ID property decorator
91
+ *
92
+ * Marks a property as the tenant identifier field. This decorator registers
93
+ * the field metadata with ObjectRegistry, keeping the property value clean
94
+ * (no descriptor objects that could be accidentally saved to the database).
95
+ *
96
+ * @param options - Field options (nullable, autoFilter, autoPopulate, etc.)
97
+ * @returns Property decorator
98
+ *
99
+ * @example Basic usage (required tenancy)
100
+ * ```typescript
101
+ * @smrt()
102
+ * @TenantScoped()
103
+ * class Document extends SmrtObject {
104
+ * @tenantId()
105
+ * tenantId: string = '';
106
+ *
107
+ * title: string = '';
108
+ * }
109
+ * ```
110
+ *
111
+ * @example Nullable tenant ID (for global resources)
112
+ * ```typescript
113
+ * @smrt()
114
+ * @TenantScoped({ mode: 'optional' })
115
+ * class GlobalConfig extends SmrtObject {
116
+ * @tenantId({ nullable: true })
117
+ * tenantId: string | null = null; // null = global, string = tenant-specific
118
+ *
119
+ * key: string = '';
120
+ * }
121
+ * ```
122
+ *
123
+ * @see https://github.com/happyvertical/smrt/issues/829 - Why decorators over field helpers
124
+ */
125
+ export declare function tenantId(options?: TenantIdFieldOptions): CompatiblePropertyDecorator;
126
+ //# sourceMappingURL=decorators.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decorators.d.ts","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAEL,KAAK,2BAA2B,EAKjC,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAMxD;;;;;;;;GAQG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,IAAI,CAAC,EAAE,UAAU,GAAG,UAAU,CAAC;IAE/B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;;OAGG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,wBAAgB,YAAY,CAAC,OAAO,GAAE,mBAAwB,IACpD,CAAC,SAAS,QAAQ,EACxB,QAAQ,CAAC,EACT,mBAAmB,qBAAqB,KACvC,CAAC,CAoBL;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,QAAQ,CAAC,OAAO,GAAE,oBAAyB,GA8BnD,2BAA2B,CAClC"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Shared tenancy-enabled flag.
3
+ *
4
+ * Holds the single boolean toggled by `enableTenancy()` / `disableTenancy()`.
5
+ * It lives in its own leaf module (importing nothing from the package) so that
6
+ * both `interceptor.ts` and `entry-point.ts` can read it without forming a
7
+ * circular import: `interceptor.ts` imports `runTenantScopedEntryPoint` from
8
+ * `entry-point.ts`, and `entry-point.ts` needs the enabled flag — routing the
9
+ * flag through here keeps that dependency one-directional.
10
+ */
11
+ /**
12
+ * Set the global tenancy-enabled flag. Internal — called by `enableTenancy()` /
13
+ * `disableTenancy()` in `interceptor.ts`.
14
+ *
15
+ * @param value - `true` to mark tenancy enabled, `false` to clear it.
16
+ */
17
+ export declare function setTenancyEnabled(value: boolean): void;
18
+ /**
19
+ * Return `true` if tenant enforcement is currently active.
20
+ *
21
+ * @returns Whether `enableTenancy()` has been called without a later
22
+ * `disableTenancy()`.
23
+ */
24
+ export declare function isTenancyEnabled(): boolean;
25
+ //# sourceMappingURL=enabled-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enabled-state.d.ts","sourceRoot":"","sources":["../src/enabled-state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAEtD;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAE1C"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Fail-closed tenant-context establishment for non-web entry points (#1554).
3
+ *
4
+ * The SvelteKit/Express adapters establish tenant context from the authenticated
5
+ * request principal, so the web surface of a `@TenantScoped({ mode: 'optional' })`
6
+ * model never reads across tenants without an active context. The generated
7
+ * **CLI** and **MCP** entry points have no request principal, so an invocation
8
+ * with no active context would fall through the interceptor's optional-mode
9
+ * pass-through and return rows across **all** tenants.
10
+ *
11
+ * `runTenantScopedEntryPoint()` closes that gap. It is the single fail-closed
12
+ * gate both generated surfaces wrap their per-command/per-tool execution in.
13
+ *
14
+ * @see createCliContext for the richer CLI runner (resolveTenantId, super-admin).
15
+ */
16
+ /**
17
+ * Inputs for {@link runTenantScopedEntryPoint}.
18
+ *
19
+ * Provide **either** `className` (the gate resolves tenant-scoping from the
20
+ * authoritative tenancy registry — the same source the interceptor uses, so it
21
+ * covers both `@TenantScoped` and `@smrt({ tenantScoped })` registrations) or an
22
+ * explicit `tenantScoped` boolean (when the caller already resolved it, e.g. a
23
+ * build-time generated surface). An explicit boolean wins when both are given.
24
+ */
25
+ export interface TenantEntryPointOptions {
26
+ /**
27
+ * Class name of the target model. When provided, tenant-scoping is resolved
28
+ * via `isTenantScopedClass(className)`.
29
+ */
30
+ className?: string;
31
+ /**
32
+ * Explicit tenant-scoping decision. Overrides `className` resolution when set.
33
+ * Non-scoped models always pass through unchanged — the gate is a no-op.
34
+ */
35
+ tenantScoped?: boolean;
36
+ /**
37
+ * Explicit operator-provided tenant selector (CLI `--tenant <id>`, MCP
38
+ * `context.tenantId`). When present (and no context is already active) the
39
+ * function runs inside this tenant's context.
40
+ */
41
+ tenantId?: string | null;
42
+ /**
43
+ * Explicit operator opt-in to cross-tenant / system access (CLI
44
+ * `--all-tenants`, an MCP host that trusts the caller as an operator). When
45
+ * set the function runs in system context, bypassing tenant filtering.
46
+ *
47
+ * @default false
48
+ */
49
+ allowCrossTenant?: boolean;
50
+ /**
51
+ * Human-facing surface name used in the fail-closed error message, e.g.
52
+ * `'CLI'` or `'MCP'`.
53
+ *
54
+ * @default 'entry point'
55
+ */
56
+ surface?: string;
57
+ }
58
+ /**
59
+ * Run `fn` inside an appropriate tenant context for a generated CLI/MCP entry
60
+ * point, failing closed for tenant-scoped models when no authorized context can
61
+ * be established.
62
+ *
63
+ * Resolution order (tenant-scoped models only):
64
+ * 1. A tenant context is already active, or an explicit `withSystemContext()`
65
+ * bypass is in effect (e.g. `runAsSystem()`, migrations) → run as-is.
66
+ * 2. `allowCrossTenant` was explicitly set → run in system context. Checked
67
+ * before `tenantId` so an explicit cross-tenant opt-in wins over a default
68
+ * principal/host tenant rather than being silently scoped.
69
+ * 3. An explicit `tenantId` was provided → run inside that tenant.
70
+ * 4. Tenancy is enabled but none of the above → **throw** `TenantContextError`
71
+ * (the fail-closed branch — never silently read across tenants).
72
+ * 5. Tenancy is disabled (single-/no-tenant deployment) → pass through.
73
+ *
74
+ * Non-tenant-scoped models always pass straight through.
75
+ *
76
+ * @param options - {@link TenantEntryPointOptions}.
77
+ * @param fn - The command/tool body to execute.
78
+ * @returns The resolved value of `fn`.
79
+ * @throws {TenantContextError} When a tenant-scoped model is reached with
80
+ * tenancy enabled and no tenant/cross-tenant selector.
81
+ */
82
+ export declare function runTenantScopedEntryPoint<T>(options: TenantEntryPointOptions, fn: () => Promise<T>): Promise<T>;
83
+ //# sourceMappingURL=entry-point.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry-point.d.ts","sourceRoot":"","sources":["../src/entry-point.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAYH;;;;;;;;GAQG;AACH,MAAM,WAAW,uBAAuB;IACtC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAEzB;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,yBAAyB,CAAC,CAAC,EAC/C,OAAO,EAAE,uBAAuB,EAChC,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,CAAC,CAAC,CAgDZ"}