@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
package/AGENTS.md ADDED
@@ -0,0 +1,71 @@
1
+ # @happyvertical/smrt-tenancy
2
+
3
+ Multi-tenancy via AsyncLocalStorage context propagation with automatic query filtering and tenant ID population.
4
+
5
+ ## Context Propagation
6
+
7
+ ```typescript
8
+ import { withTenant, getTenantId, withSystemContext } from '@happyvertical/smrt-tenancy';
9
+
10
+ await withTenant({ tenantId: 'tenant-123' }, async () => {
11
+ // All SmrtCollection queries auto-filtered by tenantId
12
+ // All creates auto-populate tenantId
13
+ const docs = await collection.list({}); // WHERE tenant_id = 'tenant-123'
14
+ });
15
+
16
+ await withSystemContext(async () => { /* bypasses all tenant checks */ });
17
+ ```
18
+
19
+ **Critical distinction**: `withSystemContext()` sets a SYSTEM_CONTEXT_MARKER sentinel — different from "no context" (undefined). Interceptor can distinguish intentional bypass from missing context.
20
+
21
+ ## Interceptor System
22
+
23
+ Hooks into SmrtCollection via `GlobalInterceptors.register()` (priority 100, runs first):
24
+
25
+ | Hook | Behavior |
26
+ |------|----------|
27
+ | `beforeList` | Injects `tenantId` into WHERE clause; validates existing filters match context |
28
+ | `beforeGet` | Same — converts ID lookup to `{ id, tenantId }` |
29
+ | `beforeSave` | Auto-populates tenantId if empty + `autoPopulate: true`; validates if already set |
30
+ | `beforeDelete` | Validates instance.tenantId matches context |
31
+ | `beforeQuery` | Enforces raw SQL policy on tenant-scoped classes (`throw`/`warn`/`allow`) |
32
+ | `afterSave` | Emits `directory.<class>.created`/`updated` via `dispatchBus` for configured `directoryClasses` |
33
+ | `afterDelete` | Emits `directory.<class>.deleted` via `dispatchBus` for configured `directoryClasses` |
34
+
35
+ Mismatches throw `TenantIsolationError`. Missing required context throws `TenantContextError`.
36
+
37
+ ## Registration — Two Patterns
38
+
39
+ ```typescript
40
+ // Pattern 1: Tenancy decorator
41
+ @TenantScoped({ mode: 'optional' })
42
+ class Doc extends SmrtObject { @tenantId({ nullable: true }) tenantId: string | null = null; }
43
+
44
+ // Pattern 2: Core decorator (tenancy package reads this too)
45
+ @smrt({ tenantScoped: { mode: 'optional' } })
46
+ class Doc extends SmrtObject { tenantId: string | null = null; }
47
+ ```
48
+
49
+ Modes: `'required'` (default — throws without context) or `'optional'` (passes through if no context).
50
+
51
+ ## Adapters
52
+
53
+ - **Express**: `createExpressMiddleware()` — uses `enterTenantContext()` (not withTenant, because middleware returns before handlers run)
54
+ - **SvelteKit**: `createSvelteKitHandle()` — stores context in `event.locals`
55
+ - **CLI**: `createCliContext()` — `run()`, `runWithTenant()`, `runAsSystem()`, `runAsSuperAdmin()`
56
+
57
+ ## Super Admin Bypass
58
+
59
+ `withSuperAdminBypass()` keeps tenant context but disables auto-filtering. Different from `withSystemContext()` which removes context entirely.
60
+
61
+ ## Gotchas
62
+
63
+ - **Context lost in callbacks**: `setTimeout(() => getTenantId(), 100)` → undefined. Fix: `TenantContext.bind(fn)`
64
+ - **Nested contexts override**: inner `withTenant()` overrides outer; restores on exit
65
+ - **Auto-populate only if empty**: if tenantId already set, interceptor validates (not overwrites)
66
+ - **Isolation checked at query time**: `list({ where: { tenantId: 'other' } })` throws immediately
67
+ - **Testing**: `resetTenancy()` + `setupTestTenancy()` in beforeEach; `testTenantIsolation()` helper
68
+
69
+ ## Known exceptions to monorepo standards
70
+
71
+ - **`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.
package/CLAUDE.md ADDED
@@ -0,0 +1 @@
1
+ @AGENTS.md
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright <2025> <Happy Vertical Corporation>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,122 @@
1
+ # @happyvertical/smrt-tenancy
2
+
3
+ Multi-tenancy for SMRT with AsyncLocalStorage context propagation, automatic query filtering, and tenant ID population.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm install @happyvertical/smrt-tenancy
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { enableTenancy, TenantScoped, tenantId, withTenant } from '@happyvertical/smrt-tenancy';
15
+ import { smrt, SmrtObject } from '@happyvertical/smrt-core';
16
+
17
+ // 1. Enable tenancy globally (once at app startup)
18
+ enableTenancy();
19
+
20
+ // 2. Mark classes as tenant-scoped
21
+ @smrt()
22
+ @TenantScoped({ mode: 'optional' })
23
+ class Document extends SmrtObject {
24
+ @tenantId({ nullable: true })
25
+ tenantId: string | null = null;
26
+
27
+ title: string = '';
28
+ }
29
+
30
+ // 3. Wrap operations in tenant context
31
+ await withTenant({ tenantId: 'tenant-123' }, async () => {
32
+ const docs = await collection.list({ where: { status: 'active' } });
33
+ // Executes: WHERE tenant_id = 'tenant-123' AND status = 'active'
34
+ });
35
+ ```
36
+
37
+ ## API
38
+
39
+ ### Decorators
40
+
41
+ | Export | Description |
42
+ |--------|-------------|
43
+ | `TenantScoped(options?)` | Class decorator. Modes: `'required'` (default) or `'optional'` |
44
+ | `tenantId(options?)` | Property decorator for the tenant ID field |
45
+
46
+ ### Context Runners
47
+
48
+ | Export | Description |
49
+ |--------|-------------|
50
+ | `withTenant(ctx, fn)` | Run code scoped to a tenant |
51
+ | `withTenantSync(ctx, fn)` | Synchronous variant |
52
+ | `withSystemContext(fn)` | Bypass all tenant checks (admin/migrations) |
53
+ | `withSuperAdminBypass(fn)` | Keep tenant context but disable auto-filtering |
54
+ | `enterTenantContext(ctx)` | Enter context without callback (for middleware) |
55
+
56
+ ### Context Accessors
57
+
58
+ | Export | Description |
59
+ |--------|-------------|
60
+ | `getCurrentTenant()` | Get current tenant context (may be undefined) |
61
+ | `getTenantId()` | Get tenant ID string (may be undefined) |
62
+ | `requireTenant()` | Get tenant context or throw |
63
+ | `requireTenantId()` | Get tenant ID or throw |
64
+ | `hasTenantContext()` | Check if in tenant context |
65
+ | `isSystemContext()` | Check if in system context |
66
+ | `isSuperAdminBypass()` | Check if super admin bypass is active |
67
+ | `TenantContext` | AsyncLocalStorage instance (advanced use) |
68
+
69
+ ### Errors
70
+
71
+ `TenantContextError` (missing required context), `TenantIsolationError` (tenant mismatch).
72
+
73
+ ### Interceptor
74
+
75
+ | Export | Description |
76
+ |--------|-------------|
77
+ | `enableTenancy()` | Register tenant interceptor globally |
78
+ | `disableTenancy()` | Remove tenant interceptor |
79
+ | `isTenancyEnabled()` | Check if tenancy is active |
80
+ | `createTenantInterceptor(options?)` | Create interceptor manually |
81
+
82
+ ### Framework Adapters
83
+
84
+ | Export | Description |
85
+ |--------|-------------|
86
+ | `createSvelteKitHandle(options)` | SvelteKit hooks.server.ts handler |
87
+ | `createExpressMiddleware(options)` | Express middleware |
88
+ | `createCliContext(options)` | CLI context with `run()`, `runWithTenant()`, `runAsSystem()` |
89
+
90
+ ### Registry (Advanced)
91
+
92
+ | Export | Description |
93
+ |--------|-------------|
94
+ | `isTenantScopedClass(name)` | Check if a class is tenant-scoped |
95
+ | `getTenantScopedConfig(name)` | Get tenant config for a class |
96
+ | `getAllTenantScopedClasses()` | List all registered tenant-scoped classes |
97
+ | `registerTenantScopedClass()` | Register a class programmatically |
98
+ | `unregisterTenantScopedClass()` | Remove a class from registry |
99
+ | `clearTenantScopedRegistry()` | Clear all registrations |
100
+
101
+ ### Testing
102
+
103
+ | Export | Description |
104
+ |--------|-------------|
105
+ | `setupTestTenancy(options?)` | Enable tenancy for tests |
106
+ | `resetTenancy()` | Clean up tenancy state between tests |
107
+ | `createTestTenantContext(ctx, fn)` | Run test code in tenant context |
108
+ | `testTenantIsolation(tenantIds, fn)` | Verify isolation between tenants |
109
+ | `assertTenantContextRequired(fn)` | Assert operation requires context |
110
+ | `assertTenantIsolationViolation(fn)` | Assert operation violates isolation |
111
+
112
+ ## Dependencies
113
+
114
+ - `@happyvertical/smrt-core` -- SmrtObject, SmrtCollection, GlobalInterceptors
115
+ - `@happyvertical/sql` -- database operations
116
+ - `@happyvertical/utils` -- utility functions
117
+
118
+ Optional peers: `svelte`, `@happyvertical/smrt-users`, `@happyvertical/smrt-svelte`
119
+
120
+ ## License
121
+
122
+ MIT
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=__smrt-register__.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"__smrt-register__.d.ts","sourceRoot":"","sources":["../src/__smrt-register__.ts"],"names":[],"mappings":""}
@@ -0,0 +1,178 @@
1
+ /**
2
+ * CLI Adapter for smrt-tenancy
3
+ *
4
+ * Provides utilities for setting up tenant context in CLI tools.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { createCliContext } from '@happyvertical/smrt-tenancy/adapters';
9
+ *
10
+ * // Create context helper for CLI
11
+ * const cliContext = createCliContext({
12
+ * resolveTenantId: () => process.env.TENANT_ID,
13
+ * });
14
+ *
15
+ * // Run command in tenant context
16
+ * await cliContext.run(async () => {
17
+ * await documentCollection.list({});
18
+ * });
19
+ * ```
20
+ */
21
+ /**
22
+ * Configuration for the CLI context runner created by `createCliContext()`.
23
+ *
24
+ * `resolveTenantId` is optional — when omitted (or when it returns
25
+ * `null`/`undefined`) the `run()` method falls back to `withSystemContext()`.
26
+ *
27
+ * @see createCliContext
28
+ * @see CliContextRunner
29
+ */
30
+ export interface CliContextOptions {
31
+ /**
32
+ * Resolve tenant ID
33
+ *
34
+ * Common sources:
35
+ * - Environment variable: () => process.env.TENANT_ID
36
+ * - Command line argument: () => argv.tenant
37
+ * - Config file: () => config.tenantId
38
+ */
39
+ resolveTenantId?: () => Promise<string | null | undefined> | string | null | undefined;
40
+ /**
41
+ * Resolve user ID (optional)
42
+ */
43
+ resolveUserId?: () => Promise<string | null | undefined> | string | null | undefined;
44
+ /**
45
+ * Default permissions for CLI operations
46
+ */
47
+ defaultPermissions?: Set<string>;
48
+ /**
49
+ * Run CLI as super admin by default
50
+ * @default false
51
+ */
52
+ superAdminByDefault?: boolean;
53
+ }
54
+ /**
55
+ * Context runner returned by `createCliContext()`.
56
+ *
57
+ * Provides four execution modes suitable for different CLI scenarios:
58
+ * - `run()` — resolves the tenant from the configured options and runs code in
59
+ * that context; falls back to system context if no tenant is available.
60
+ * - `runWithTenant()` — explicitly specify a tenant ID for this invocation.
61
+ * - `runAsSystem()` — bypass all tenant checks (migration scripts, admin tools).
62
+ * - `runAsSuperAdmin()` — tenant context with bypass flag enabled.
63
+ *
64
+ * @see createCliContext
65
+ */
66
+ export interface CliContextRunner {
67
+ /**
68
+ * Run `fn` inside the tenant context resolved from the `CliContextOptions`.
69
+ *
70
+ * Falls back to `withSystemContext()` when no tenant ID is available.
71
+ *
72
+ * @param fn - Async function to execute.
73
+ * @returns Promise resolving to the return value of `fn`.
74
+ */
75
+ run<T>(fn: () => Promise<T>): Promise<T>;
76
+ /**
77
+ * Run `fn` inside the context of the specified tenant.
78
+ *
79
+ * @param tenantId - Tenant ID to set as context.
80
+ * @param fn - Async function to execute.
81
+ * @returns Promise resolving to the return value of `fn`.
82
+ */
83
+ runWithTenant<T>(tenantId: string, fn: () => Promise<T>): Promise<T>;
84
+ /**
85
+ * Run `fn` in system context, bypassing all tenant checks.
86
+ *
87
+ * @param fn - Async function to execute.
88
+ * @returns Promise resolving to the return value of `fn`.
89
+ */
90
+ runAsSystem<T>(fn: () => Promise<T>): Promise<T>;
91
+ /**
92
+ * Run `fn` with a tenant context and super admin bypass enabled.
93
+ *
94
+ * @param tenantId - Tenant ID to set as context.
95
+ * @param fn - Async function to execute.
96
+ * @returns Promise resolving to the return value of `fn`.
97
+ */
98
+ runAsSuperAdmin<T>(tenantId: string, fn: () => Promise<T>): Promise<T>;
99
+ }
100
+ /**
101
+ * Create a CLI context runner
102
+ *
103
+ * @param options - Configuration options
104
+ * @returns CLI context runner with various execution modes
105
+ *
106
+ * @example
107
+ * ```typescript
108
+ * const cli = createCliContext({
109
+ * resolveTenantId: () => process.env.TENANT_ID,
110
+ * superAdminByDefault: true,
111
+ * });
112
+ *
113
+ * // Use resolved tenant
114
+ * await cli.run(async () => {
115
+ * const docs = await collection.list({});
116
+ * console.log(`Found ${docs.length} documents`);
117
+ * });
118
+ *
119
+ * // Override tenant
120
+ * await cli.runWithTenant('other-tenant', async () => {
121
+ * // Operations in other-tenant context
122
+ * });
123
+ *
124
+ * // System operations (no tenant)
125
+ * await cli.runAsSystem(async () => {
126
+ * // Can access all data
127
+ * const allDocs = await collection.list({});
128
+ * });
129
+ * ```
130
+ */
131
+ export declare function createCliContext(options?: CliContextOptions): CliContextRunner;
132
+ /**
133
+ * Run an async function with a specific tenant ID set as context.
134
+ *
135
+ * Convenience wrapper around `withTenant()` for one-off CLI operations where
136
+ * a full `CliContextRunner` is not needed.
137
+ *
138
+ * @param tenantId - Tenant ID to set as the active context.
139
+ * @param fn - Async function to execute in the tenant context.
140
+ * @returns Promise resolving to the return value of `fn`.
141
+ *
142
+ * @example
143
+ * ```typescript
144
+ * import { runWithTenant } from '@happyvertical/smrt-tenancy/adapters';
145
+ *
146
+ * await runWithTenant('tenant-123', async () => {
147
+ * await collection.list({});
148
+ * });
149
+ * ```
150
+ *
151
+ * @see createCliContext
152
+ * @see runAsSystem
153
+ */
154
+ export declare function runWithTenant<T>(tenantId: string, fn: () => Promise<T>): Promise<T>;
155
+ /**
156
+ * Run an async function in system context, bypassing all tenant checks.
157
+ *
158
+ * Convenience wrapper around `withSystemContext()` for one-off CLI operations
159
+ * such as migration scripts or admin tooling that needs cross-tenant access.
160
+ *
161
+ * @param fn - Async function to execute in system context.
162
+ * @returns Promise resolving to the return value of `fn`.
163
+ *
164
+ * @example
165
+ * ```typescript
166
+ * import { runAsSystem } from '@happyvertical/smrt-tenancy/adapters';
167
+ *
168
+ * await runAsSystem(async () => {
169
+ * const all = await collection.list({});
170
+ * console.log(`Total records: ${all.length}`);
171
+ * });
172
+ * ```
173
+ *
174
+ * @see runWithTenant
175
+ * @see createCliContext
176
+ */
177
+ export declare function runAsSystem<T>(fn: () => Promise<T>): Promise<T>;
178
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/adapters/cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAQH;;;;;;;;GAQG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;;;;OAOG;IACH,eAAe,CAAC,EAAE,MACd,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,GAClC,MAAM,GACN,IAAI,GACJ,SAAS,CAAC;IAEd;;OAEG;IACH,aAAa,CAAC,EAAE,MACZ,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,GAClC,MAAM,GACN,IAAI,GACJ,SAAS,CAAC;IAEd;;OAEG;IACH,kBAAkB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAEjC;;;OAGG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;;;;OAOG;IACH,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAEzC;;;;;;OAMG;IACH,aAAa,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAErE;;;;;OAKG;IACH,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAEjD;;;;;;OAMG;IACH,eAAe,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CACxE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,GAAE,iBAAsB,GAC9B,gBAAgB,CA8DlB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,aAAa,CAAC,CAAC,EACnC,QAAQ,EAAE,MAAM,EAChB,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,CAAC,CAAC,CAEZ;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAErE"}
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Express Adapter for smrt-tenancy
3
+ *
4
+ * Provides Express middleware that sets up tenant context for each request.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import express from 'express';
9
+ * import { createExpressMiddleware } from '@happyvertical/smrt-tenancy/adapters';
10
+ *
11
+ * const app = express();
12
+ *
13
+ * app.use(createExpressMiddleware({
14
+ * resolveTenantId: (req) => req.headers['x-tenant-id'] as string,
15
+ * }));
16
+ * ```
17
+ */
18
+ /**
19
+ * Express Request interface (minimal to avoid direct dependency)
20
+ */
21
+ interface ExpressRequest {
22
+ headers: Record<string, string | string[] | undefined>;
23
+ url: string;
24
+ path: string;
25
+ query: Record<string, unknown>;
26
+ cookies?: Record<string, string>;
27
+ }
28
+ /**
29
+ * Express Response interface
30
+ */
31
+ interface ExpressResponse {
32
+ status(code: number): ExpressResponse;
33
+ json(data: unknown): ExpressResponse;
34
+ send(data: unknown): ExpressResponse;
35
+ }
36
+ /**
37
+ * Express NextFunction
38
+ */
39
+ type ExpressNext = (error?: unknown) => void;
40
+ /**
41
+ * Configuration options for the Express tenancy middleware created by
42
+ * `createExpressMiddleware()`.
43
+ *
44
+ * Only `resolveTenantId` is required. All callback options receive the raw
45
+ * Express `Request` object so you can extract tenant information from headers,
46
+ * subdomains, cookies, or any other request property.
47
+ *
48
+ * @see createExpressMiddleware
49
+ */
50
+ export interface ExpressMiddlewareOptions {
51
+ /**
52
+ * Resolve tenant ID from the request
53
+ */
54
+ resolveTenantId: (req: ExpressRequest) => Promise<string | null | undefined> | string | null | undefined;
55
+ /**
56
+ * Resolve user ID from the request (optional)
57
+ */
58
+ resolveUserId?: (req: ExpressRequest) => Promise<string | null | undefined> | string | null | undefined;
59
+ /**
60
+ * Resolve permissions (optional)
61
+ */
62
+ resolvePermissions?: (req: ExpressRequest, tenantId: string, userId?: string) => Promise<Set<string>> | Set<string>;
63
+ /**
64
+ * Check if user is super admin (optional)
65
+ */
66
+ isSuperAdmin?: (req: ExpressRequest, tenantId: string, userId?: string) => Promise<boolean> | boolean;
67
+ /**
68
+ * Called when no tenant ID could be resolved
69
+ * Return true to continue, false to stop with 400 error.
70
+ */
71
+ onNoTenant?: (req: ExpressRequest, res: ExpressResponse) => Promise<boolean> | boolean;
72
+ /**
73
+ * Paths to exclude from tenant context
74
+ */
75
+ excludePaths?: string[];
76
+ }
77
+ /**
78
+ * Create an Express middleware function that establishes tenant context for
79
+ * every incoming request.
80
+ *
81
+ * Uses `enterTenantContext()` (rather than `withTenant()`) because Express
82
+ * middleware returns before route handlers execute. `enterWith()` sets the
83
+ * context on the current async resource so it propagates to handlers that run
84
+ * after `next()` is called.
85
+ *
86
+ * The resolved context is also attached directly to the request object for
87
+ * convenience:
88
+ * - `req.tenantContext` — full `TenantContextData`
89
+ * - `req.tenantId` — string tenant ID shortcut
90
+ *
91
+ * When no tenant ID can be resolved, the default behaviour returns a `400`
92
+ * JSON response. Customise this with the `onNoTenant` option.
93
+ *
94
+ * @param options - Middleware configuration including the required
95
+ * `resolveTenantId` callback.
96
+ * @returns An Express-compatible middleware function `(req, res, next) => void`.
97
+ *
98
+ * @example
99
+ * ```typescript
100
+ * import express from 'express';
101
+ * import { createExpressMiddleware } from '@happyvertical/smrt-tenancy/adapters';
102
+ *
103
+ * const app = express();
104
+ * app.use(createExpressMiddleware({
105
+ * resolveTenantId: (req) => req.headers['x-tenant-id'] as string,
106
+ * excludePaths: ['/health', '/public/*'],
107
+ * }));
108
+ * ```
109
+ *
110
+ * @see ExpressMiddlewareOptions
111
+ * @see createSvelteKitHandle
112
+ */
113
+ export declare function createExpressMiddleware(options: ExpressMiddlewareOptions): (req: ExpressRequest, res: ExpressResponse, next: ExpressNext) => Promise<void>;
114
+ export {};
115
+ //# sourceMappingURL=express.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"express.d.ts","sourceRoot":"","sources":["../../src/adapters/express.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH;;GAEG;AACH,UAAU,cAAc;IACtB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;IACvD,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED;;GAEG;AACH,UAAU,eAAe;IACvB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,CAAC;IACtC,IAAI,CAAC,IAAI,EAAE,OAAO,GAAG,eAAe,CAAC;IACrC,IAAI,CAAC,IAAI,EAAE,OAAO,GAAG,eAAe,CAAC;CACtC;AAED;;GAEG;AACH,KAAK,WAAW,GAAG,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;AAE7C;;;;;;;;;GASG;AACH,MAAM,WAAW,wBAAwB;IACvC;;OAEG;IACH,eAAe,EAAE,CACf,GAAG,EAAE,cAAc,KAChB,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAEpE;;OAEG;IACH,aAAa,CAAC,EAAE,CACd,GAAG,EAAE,cAAc,KAChB,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAEpE;;OAEG;IACH,kBAAkB,CAAC,EAAE,CACnB,GAAG,EAAE,cAAc,EACnB,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,MAAM,KACZ,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;IAExC;;OAEG;IACH,YAAY,CAAC,EAAE,CACb,GAAG,EAAE,cAAc,EACnB,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,MAAM,KACZ,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;IAEhC;;;OAGG;IACH,UAAU,CAAC,EAAE,CACX,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,eAAe,KACjB,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;IAEhC;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,wBAAwB,IAWrE,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,MAAM,WAAW,KAChB,OAAO,CAAC,IAAI,CAAC,CA2DjB"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Framework Adapters for smrt-tenancy
3
+ *
4
+ * Provides middleware/hooks for popular frameworks to set up tenant context.
5
+ *
6
+ * @example SvelteKit
7
+ * ```typescript
8
+ * // hooks.server.ts
9
+ * import { createSvelteKitHandle } from '@happyvertical/smrt-tenancy/adapters';
10
+ *
11
+ * export const handle = createSvelteKitHandle({
12
+ * resolveTenantId: async (event) => {
13
+ * // Get from subdomain, header, cookie, etc.
14
+ * return event.request.headers.get('x-tenant-id');
15
+ * }
16
+ * });
17
+ * ```
18
+ */
19
+ export { type CliContextOptions, createCliContext } from './cli.js';
20
+ export { createExpressMiddleware, type ExpressMiddlewareOptions, } from './express.js';
21
+ export { createSvelteKitHandle, type SvelteKitHandleOptions, } from './sveltekit.js';
22
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,KAAK,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACpE,OAAO,EACL,uBAAuB,EACvB,KAAK,wBAAwB,GAC9B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,qBAAqB,EACrB,KAAK,sBAAsB,GAC5B,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,7 @@
1
+ import { c, a, b } from "../chunks/sveltekit-9eRH1RLw.js";
2
+ export {
3
+ c as createCliContext,
4
+ a as createExpressMiddleware,
5
+ b as createSvelteKitHandle
6
+ };
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
@@ -0,0 +1,123 @@
1
+ /**
2
+ * SvelteKit Adapter for smrt-tenancy
3
+ *
4
+ * Provides a SvelteKit Handle that sets up tenant context for each request.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * // hooks.server.ts
9
+ * import { createSvelteKitHandle } from '@happyvertical/smrt-tenancy/adapters';
10
+ *
11
+ * export const handle = createSvelteKitHandle({
12
+ * resolveTenantId: async (event) => {
13
+ * // From subdomain
14
+ * const host = event.request.headers.get('host');
15
+ * const subdomain = host?.split('.')[0];
16
+ * return subdomain;
17
+ *
18
+ * // Or from header
19
+ * // return event.request.headers.get('x-tenant-id');
20
+ *
21
+ * // Or from cookie
22
+ * // return event.cookies.get('tenant_id');
23
+ * }
24
+ * });
25
+ * ```
26
+ */
27
+ /**
28
+ * SvelteKit RequestEvent (minimal interface to avoid direct dependency)
29
+ */
30
+ interface SvelteKitEvent {
31
+ request: Request;
32
+ url: URL;
33
+ cookies: {
34
+ get(name: string): string | undefined;
35
+ set(name: string, value: string, opts?: unknown): void;
36
+ };
37
+ locals: Record<string, unknown>;
38
+ }
39
+ /**
40
+ * SvelteKit resolve function
41
+ */
42
+ type SvelteKitResolve = (event: SvelteKitEvent) => Promise<Response>;
43
+ /**
44
+ * Configuration options for the SvelteKit tenancy handle created by
45
+ * `createSvelteKitHandle()`.
46
+ *
47
+ * Only `resolveTenantId` is required; all other fields are optional callbacks
48
+ * used to enrich the context or customise missing-tenant behaviour.
49
+ *
50
+ * @see createSvelteKitHandle
51
+ */
52
+ export interface SvelteKitHandleOptions {
53
+ /**
54
+ * Resolve tenant ID from the request
55
+ *
56
+ * Return the tenant ID string, or null/undefined if no tenant context should be set.
57
+ */
58
+ resolveTenantId: (event: SvelteKitEvent) => Promise<string | null | undefined> | string | null | undefined;
59
+ /**
60
+ * Resolve user ID from the request (optional)
61
+ */
62
+ resolveUserId?: (event: SvelteKitEvent) => Promise<string | null | undefined> | string | null | undefined;
63
+ /**
64
+ * Resolve permissions for the user in this tenant (optional)
65
+ */
66
+ resolvePermissions?: (event: SvelteKitEvent, tenantId: string, userId?: string) => Promise<Set<string>> | Set<string>;
67
+ /**
68
+ * Check if user is a super admin (optional)
69
+ * If true and super admin bypass is enabled on the class, tenant filtering is skipped.
70
+ */
71
+ isSuperAdmin?: (event: SvelteKitEvent, tenantId: string, userId?: string) => Promise<boolean> | boolean;
72
+ /**
73
+ * Called when no tenant ID could be resolved
74
+ * Return a Response to short-circuit, or undefined to continue without tenant context.
75
+ */
76
+ onNoTenant?: (event: SvelteKitEvent) => Promise<Response | undefined> | Response | undefined;
77
+ /**
78
+ * Paths to exclude from tenant context (e.g., public APIs, health checks)
79
+ * Supports glob patterns.
80
+ */
81
+ excludePaths?: string[];
82
+ }
83
+ /**
84
+ * Create a SvelteKit `Handle` function that establishes tenant context for
85
+ * every server-side request.
86
+ *
87
+ * The returned handle wraps the `resolve` call in `withTenant()` so that all
88
+ * server-side load functions, API routes, and `+server.ts` handlers within the
89
+ * request share the same tenant context via `AsyncLocalStorage`.
90
+ *
91
+ * The resolved context is also stored in `event.locals` under two keys:
92
+ * - `event.locals.tenantContext` — full `TenantContextData`
93
+ * - `event.locals.tenantId` — string tenant ID shortcut
94
+ *
95
+ * When no tenant ID can be resolved (and no custom `onNoTenant` handler
96
+ * returns a `Response`), the request continues without any tenant context.
97
+ *
98
+ * @param options - Configuration options including the required
99
+ * `resolveTenantId` callback.
100
+ * @returns A SvelteKit `Handle` function suitable for use in `hooks.server.ts`.
101
+ *
102
+ * @example
103
+ * ```typescript
104
+ * // src/hooks.server.ts
105
+ * import { createSvelteKitHandle } from '@happyvertical/smrt-tenancy/adapters';
106
+ *
107
+ * export const handle = createSvelteKitHandle({
108
+ * resolveTenantId: (event) =>
109
+ * event.request.headers.get('x-tenant-id'),
110
+ * onNoTenant: () =>
111
+ * new Response('Tenant required', { status: 400 }),
112
+ * });
113
+ * ```
114
+ *
115
+ * @see SvelteKitHandleOptions
116
+ * @see createExpressMiddleware
117
+ */
118
+ export declare function createSvelteKitHandle(options: SvelteKitHandleOptions): ({ event, resolve, }: {
119
+ event: SvelteKitEvent;
120
+ resolve: SvelteKitResolve;
121
+ }) => Promise<Response>;
122
+ export {};
123
+ //# sourceMappingURL=sveltekit.d.ts.map