@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 @@
1
+ {"version":3,"file":"sveltekit.d.ts","sourceRoot":"","sources":["../../src/adapters/sveltekit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAIH;;GAEG;AACH,UAAU,cAAc;IACtB,OAAO,EAAE,OAAO,CAAC;IACjB,GAAG,EAAE,GAAG,CAAC;IACT,OAAO,EAAE;QACP,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;QACtC,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;KACxD,CAAC;IACF,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED;;GAEG;AACH,KAAK,gBAAgB,GAAG,CAAC,KAAK,EAAE,cAAc,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAErE;;;;;;;;GAQG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;;OAIG;IACH,eAAe,EAAE,CACf,KAAK,EAAE,cAAc,KAClB,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAEpE;;OAEG;IACH,aAAa,CAAC,EAAE,CACd,KAAK,EAAE,cAAc,KAClB,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAEpE;;OAEG;IACH,kBAAkB,CAAC,EAAE,CACnB,KAAK,EAAE,cAAc,EACrB,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,MAAM,KACZ,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;IAExC;;;OAGG;IACH,YAAY,CAAC,EAAE,CACb,KAAK,EAAE,cAAc,EACrB,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,MAAM,KACZ,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;IAEhC;;;OAGG;IACH,UAAU,CAAC,EAAE,CACX,KAAK,EAAE,cAAc,KAClB,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,GAAG,QAAQ,GAAG,SAAS,CAAC;IAE1D;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,sBAAsB,IAUtC,qBAG1B;IACD,KAAK,EAAE,cAAc,CAAC;IACtB,OAAO,EAAE,gBAAgB,CAAC;CAC3B,KAAG,OAAO,CAAC,QAAQ,CAAC,CA6CtB"}
@@ -0,0 +1,190 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ const SYSTEM_CONTEXT_MARKER = /* @__PURE__ */ Symbol.for("smrt:system-context");
3
+ const tenantStorage = new AsyncLocalStorage();
4
+ function getCurrentTenant() {
5
+ const store = tenantStorage.getStore();
6
+ if (store === SYSTEM_CONTEXT_MARKER) {
7
+ return void 0;
8
+ }
9
+ return store;
10
+ }
11
+ function requireTenant() {
12
+ const ctx = tenantStorage.getStore();
13
+ if (!ctx || ctx === SYSTEM_CONTEXT_MARKER) {
14
+ throw new TenantContextError(
15
+ "No tenant context available. Ensure request is wrapped in withTenant() or middleware is configured."
16
+ );
17
+ }
18
+ return ctx;
19
+ }
20
+ function requireTenantId() {
21
+ return requireTenant().tenantId;
22
+ }
23
+ function getTenantId() {
24
+ const store = tenantStorage.getStore();
25
+ if (store === SYSTEM_CONTEXT_MARKER) {
26
+ return void 0;
27
+ }
28
+ return store?.tenantId;
29
+ }
30
+ function hasTenantContext() {
31
+ const store = tenantStorage.getStore();
32
+ return store !== void 0 && store !== SYSTEM_CONTEXT_MARKER;
33
+ }
34
+ function isSystemContext() {
35
+ return tenantStorage.getStore() === SYSTEM_CONTEXT_MARKER;
36
+ }
37
+ function isSuperAdminBypass() {
38
+ const store = tenantStorage.getStore();
39
+ if (store === SYSTEM_CONTEXT_MARKER) {
40
+ return false;
41
+ }
42
+ return store?.superAdminBypass === true;
43
+ }
44
+ async function withTenant(context, fn) {
45
+ const fullContext = {
46
+ permissions: /* @__PURE__ */ new Set(),
47
+ ...context
48
+ };
49
+ return tenantStorage.run(fullContext, fn);
50
+ }
51
+ function withTenantSync(context, fn) {
52
+ const fullContext = {
53
+ permissions: /* @__PURE__ */ new Set(),
54
+ ...context
55
+ };
56
+ return tenantStorage.run(fullContext, fn);
57
+ }
58
+ function enterTenantContext(context) {
59
+ const fullContext = {
60
+ permissions: /* @__PURE__ */ new Set(),
61
+ ...context
62
+ };
63
+ tenantStorage.enterWith(fullContext);
64
+ }
65
+ async function withSystemContext(fn) {
66
+ return tenantStorage.run(SYSTEM_CONTEXT_MARKER, fn);
67
+ }
68
+ async function withSuperAdminBypass(fn) {
69
+ const current = tenantStorage.getStore();
70
+ if (!current || current === SYSTEM_CONTEXT_MARKER) {
71
+ throw new TenantContextError(
72
+ "Cannot enable super admin bypass without a tenant context. Use withTenant() first or withSystemContext() instead."
73
+ );
74
+ }
75
+ const bypassContext = {
76
+ ...current,
77
+ superAdminBypass: true
78
+ };
79
+ return tenantStorage.run(bypassContext, fn);
80
+ }
81
+ const TenantContext = {
82
+ /**
83
+ * Bind a callback to the current tenant context
84
+ *
85
+ * Use this when passing callbacks to setTimeout, event emitters,
86
+ * or other APIs that might lose the async context.
87
+ *
88
+ * @param fn - Function to bind to current context
89
+ * @returns Wrapped function that will run in the original context
90
+ *
91
+ * @example
92
+ * ```typescript
93
+ * // Without bind - context is lost
94
+ * setTimeout(() => {
95
+ * console.log(getTenantId()); // undefined!
96
+ * }, 1000);
97
+ *
98
+ * // With bind - context is preserved
99
+ * setTimeout(TenantContext.bind(() => {
100
+ * console.log(getTenantId()); // 'tenant-123'
101
+ * }), 1000);
102
+ * ```
103
+ */
104
+ bind(fn) {
105
+ const store = tenantStorage.getStore();
106
+ if (!store) {
107
+ return fn;
108
+ }
109
+ return ((...args) => {
110
+ return tenantStorage.run(store, () => fn(...args));
111
+ });
112
+ },
113
+ /**
114
+ * Get the current context data (or undefined for system/no context)
115
+ */
116
+ get current() {
117
+ return getCurrentTenant();
118
+ },
119
+ /**
120
+ * Check if we're in system context
121
+ */
122
+ get isSystem() {
123
+ return isSystemContext();
124
+ },
125
+ /**
126
+ * Run code with context from a job/message payload
127
+ *
128
+ * Useful for processing queued jobs that include tenant metadata.
129
+ *
130
+ * @param job - Job object with tenantId in metadata
131
+ * @param fn - Function to run in the job's tenant context
132
+ *
133
+ * @example
134
+ * ```typescript
135
+ * const job = await queue.pop();
136
+ * await TenantContext.runWithJobContext(job, async () => {
137
+ * await processJob(job);
138
+ * });
139
+ * ```
140
+ */
141
+ async runWithJobContext(job, fn) {
142
+ const tenantId = job.metadata?.tenantId ?? job.tenantId;
143
+ if (!tenantId) {
144
+ throw new TenantContextError(
145
+ "Job does not contain tenant information. Ensure jobs include tenantId in metadata or as a top-level field."
146
+ );
147
+ }
148
+ return withTenant({ tenantId }, fn);
149
+ }
150
+ };
151
+ class TenantContextError extends Error {
152
+ /** Stable error code; always `'TENANT_CONTEXT_REQUIRED'`. */
153
+ code = "TENANT_CONTEXT_REQUIRED";
154
+ constructor(message) {
155
+ super(message);
156
+ this.name = "TenantContextError";
157
+ }
158
+ }
159
+ class TenantIsolationError extends Error {
160
+ /** Stable error code; always `'TENANT_ISOLATION_VIOLATION'`. */
161
+ code = "TENANT_ISOLATION_VIOLATION";
162
+ /** The tenant ID that is active in the current context. */
163
+ tenantId;
164
+ /** The tenant ID that was attempted (and rejected). */
165
+ attemptedTenantId;
166
+ constructor(message, details) {
167
+ super(message);
168
+ this.name = "TenantIsolationError";
169
+ this.tenantId = details?.tenantId;
170
+ this.attemptedTenantId = details?.attemptedTenantId;
171
+ }
172
+ }
173
+ export {
174
+ TenantContext as T,
175
+ TenantContextError as a,
176
+ TenantIsolationError as b,
177
+ getTenantId as c,
178
+ isSystemContext as d,
179
+ enterTenantContext as e,
180
+ requireTenantId as f,
181
+ getCurrentTenant as g,
182
+ hasTenantContext as h,
183
+ isSuperAdminBypass as i,
184
+ withSystemContext as j,
185
+ withTenant as k,
186
+ withTenantSync as l,
187
+ requireTenant as r,
188
+ withSuperAdminBypass as w
189
+ };
190
+ //# sourceMappingURL=context-B5CKsmMi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-B5CKsmMi.js","sources":["../../src/context.ts"],"sourcesContent":["/**\n * TenantContext - AsyncLocalStorage-based tenant context propagation\n *\n * Provides request-scoped tenant context that flows through async operations.\n * This is the core of the tenancy system - all tenant isolation depends on\n * having a valid context.\n *\n * @example Basic usage with middleware\n * ```typescript\n * import { withTenant, requireTenantId } from '@happyvertical/smrt-tenancy';\n *\n * // In middleware (SvelteKit, Express, etc.)\n * await withTenant({ tenantId: 'tenant-123' }, async () => {\n * // All code in this async tree has access to tenant context\n * const id = requireTenantId(); // 'tenant-123'\n * });\n * ```\n *\n * @example Background job binding\n * ```typescript\n * import { TenantContext } from '@happyvertical/smrt-tenancy';\n *\n * // Bind a callback to preserve context across async boundaries\n * setTimeout(TenantContext.bind(() => {\n * console.log(requireTenantId()); // Works!\n * }), 1000);\n * ```\n *\n * @see https://github.com/happyvertical/smrt/issues/675\n */\n\nimport { AsyncLocalStorage } from 'node:async_hooks';\nimport type { DatabaseInterface } from '@happyvertical/sql';\n\n/**\n * Full data stored in tenant context for the current async execution scope.\n *\n * Created by `withTenant()` / `enterTenantContext()` and read by `getCurrentTenant()`,\n * `getTenantId()`, and the interceptor hooks. All fields except `tenantId` and\n * `permissions` are optional and may be populated lazily by higher-level packages\n * (e.g., `smrt-users`).\n *\n * @see withTenant\n * @see MinimalTenantContext\n */\nexport interface TenantContextData {\n /** Current tenant ID (required) */\n tenantId: string;\n\n /** Current tenant object (lazy-loaded if smrt-users is available) */\n tenant?: unknown;\n\n /** Current user ID (optional) */\n userId?: string;\n\n /** Current user object (lazy-loaded if smrt-users is available) */\n user?: unknown;\n\n /** Resolved permissions for this user in this tenant */\n permissions: Set<string>;\n\n /** Database connection for this tenant (if database-per-tenant strategy) */\n database?: DatabaseInterface;\n\n /** Super admin bypass enabled - allows cross-tenant operations */\n superAdminBypass?: boolean;\n\n /** Custom metadata for application-specific data */\n metadata?: Record<string, unknown>;\n}\n\n/**\n * Minimal context accepted by `withTenant()` and `withTenantSync()` when only a\n * tenant ID is known.\n *\n * `permissions` defaults to an empty `Set` when omitted. Use `TenantContextData`\n * when you also need to carry user info, database handles, or resolved permissions.\n *\n * @see TenantContextData\n * @see withTenant\n */\nexport interface MinimalTenantContext {\n /** Tenant identifier. */\n tenantId: string;\n /** Resolved permissions; defaults to an empty Set when omitted. */\n permissions?: Set<string>;\n /** When `true`, tenant auto-filtering is skipped for classes that allow super admin bypass. */\n superAdminBypass?: boolean;\n /** Arbitrary application-specific metadata to carry through the context. */\n metadata?: Record<string, unknown>;\n}\n\n// Sentinel symbol to mark system context (distinct from \"no context\")\nconst SYSTEM_CONTEXT_MARKER = Symbol.for('smrt:system-context');\n\n// Storage type includes the marker for system context\ntype ContextStoreValue = TenantContextData | typeof SYSTEM_CONTEXT_MARKER;\n\n// AsyncLocalStorage instance for tenant context\nconst tenantStorage = new AsyncLocalStorage<ContextStoreValue>();\n\n/**\n * Get the current tenant context for this async execution scope.\n *\n * Returns `undefined` when called outside any tenant scope or inside a\n * `withSystemContext()` block (the system context marker is treated as \"no\n * tenant data\"). Prefer `requireTenant()` when a context is mandatory.\n *\n * @returns The active `TenantContextData`, or `undefined` if none is set.\n *\n * @example\n * ```typescript\n * const ctx = getCurrentTenant();\n * if (ctx) {\n * console.log('Current tenant:', ctx.tenantId);\n * }\n * ```\n *\n * @see requireTenant\n * @see hasTenantContext\n */\nexport function getCurrentTenant(): TenantContextData | undefined {\n const store = tenantStorage.getStore();\n // Return undefined for system context marker (no tenant data available)\n if (store === SYSTEM_CONTEXT_MARKER) {\n return undefined;\n }\n return store;\n}\n\n/**\n * Get the current tenant context or throw if one is not available.\n *\n * Use this in business-logic code that must run inside a tenant scope.\n * For a non-throwing alternative use `getCurrentTenant()`.\n *\n * @returns The active `TenantContextData`.\n * @throws {TenantContextError} When no tenant context is set (code is outside\n * any `withTenant()` call or the enclosing middleware has not run).\n *\n * @example\n * ```typescript\n * const { tenantId, permissions } = requireTenant();\n * ```\n *\n * @see getCurrentTenant\n * @see requireTenantId\n */\nexport function requireTenant(): TenantContextData {\n const ctx = tenantStorage.getStore();\n if (!ctx || ctx === SYSTEM_CONTEXT_MARKER) {\n throw new TenantContextError(\n 'No tenant context available. ' +\n 'Ensure request is wrapped in withTenant() or middleware is configured.',\n );\n }\n return ctx;\n}\n\n/**\n * Get the current tenant ID or throw if no tenant context is available.\n *\n * Shorthand for `requireTenant().tenantId`.\n *\n * @returns The active tenant ID string.\n * @throws {TenantContextError} When no tenant context is set.\n *\n * @example\n * ```typescript\n * const tenantId = requireTenantId();\n * const rows = await db.query(`SELECT * FROM docs WHERE tenant_id = ?`, [tenantId]);\n * ```\n *\n * @see getTenantId\n * @see requireTenant\n */\nexport function requireTenantId(): string {\n return requireTenant().tenantId;\n}\n\n/**\n * Get the current tenant ID without throwing.\n *\n * Returns `undefined` when called outside any tenant scope or inside a\n * `withSystemContext()` block. Use `requireTenantId()` when a missing context\n * should be treated as an error.\n *\n * @returns The active tenant ID, or `undefined` if none is set.\n *\n * @example\n * ```typescript\n * const tenantId = getTenantId();\n * if (tenantId) {\n * // Optional tenant-scoped logic\n * }\n * ```\n *\n * @see requireTenantId\n * @see hasTenantContext\n */\nexport function getTenantId(): string | undefined {\n const store = tenantStorage.getStore();\n if (store === SYSTEM_CONTEXT_MARKER) {\n return undefined;\n }\n return store?.tenantId;\n}\n\n/**\n * Check whether the current async execution scope has an active tenant context.\n *\n * Returns `false` both when there is no context at all and when code is running\n * inside `withSystemContext()` (the system marker is not a tenant context).\n *\n * @returns `true` if a `TenantContextData` is active, `false` otherwise.\n *\n * @example\n * ```typescript\n * if (hasTenantContext()) {\n * console.log('Tenant:', getTenantId());\n * }\n * ```\n *\n * @see getTenantId\n * @see isSystemContext\n */\nexport function hasTenantContext(): boolean {\n const store = tenantStorage.getStore();\n // System context marker means no tenant context (even though storage is set)\n return store !== undefined && store !== SYSTEM_CONTEXT_MARKER;\n}\n\n/**\n * Check whether the current async execution scope was entered via `withSystemContext()`.\n *\n * A system context is explicitly set to bypass all tenant checks; it is distinct\n * from \"no context\" (undefined store). When the store is undefined the\n * interceptor enforces tenant requirements; when it holds the system marker the\n * interceptor skips all checks.\n *\n * @returns `true` if inside a `withSystemContext()` call, `false` otherwise.\n *\n * @see withSystemContext\n * @see hasTenantContext\n */\nexport function isSystemContext(): boolean {\n return tenantStorage.getStore() === SYSTEM_CONTEXT_MARKER;\n}\n\n/**\n * Check whether the super admin bypass flag is set in the current tenant context.\n *\n * When `true`, the interceptor skips tenant auto-filtering for classes that have\n * `allowSuperAdminBypass: true` in their `@TenantScoped()` config. Returns\n * `false` inside a system context (no tenant data is available).\n *\n * @returns `true` if super admin bypass is active, `false` otherwise.\n *\n * @see withSuperAdminBypass\n * @see TenantScopedOptions.allowSuperAdminBypass\n */\nexport function isSuperAdminBypass(): boolean {\n const store = tenantStorage.getStore();\n if (store === SYSTEM_CONTEXT_MARKER) {\n return false;\n }\n return store?.superAdminBypass === true;\n}\n\n/**\n * Run code within a tenant context (async version)\n *\n * @param context - Tenant context data (at minimum, tenantId)\n * @param fn - Async function to run within the tenant context\n * @returns Promise resolving to the function's return value\n *\n * @example\n * ```typescript\n * await withTenant({ tenantId: 'tenant-123' }, async () => {\n * const id = requireTenantId(); // 'tenant-123'\n * await doSomething();\n * });\n * ```\n */\nexport async function withTenant<T>(\n context: TenantContextData | MinimalTenantContext,\n fn: () => Promise<T>,\n): Promise<T> {\n const fullContext: TenantContextData = {\n permissions: new Set(),\n ...context,\n };\n return tenantStorage.run(fullContext, fn);\n}\n\n/**\n * Run synchronous code within a tenant context.\n *\n * Prefer `withTenant()` for async code. Use this variant only when the\n * callback must be synchronous (e.g., initializing a module-level value that\n * is consumed synchronously downstream).\n *\n * @param context - Tenant context data (at minimum, `tenantId`).\n * @param fn - Synchronous function to run within the tenant context.\n * @returns The return value of `fn`.\n *\n * @example\n * ```typescript\n * const result = withTenantSync({ tenantId: 'tenant-123' }, () => {\n * return computeSomethingSync();\n * });\n * ```\n *\n * @see withTenant\n */\nexport function withTenantSync<T>(\n context: TenantContextData | MinimalTenantContext,\n fn: () => T,\n): T {\n const fullContext: TenantContextData = {\n permissions: new Set(),\n ...context,\n };\n return tenantStorage.run(fullContext, fn);\n}\n\n/**\n * Enter tenant context for the remainder of the current async execution\n *\n * This uses AsyncLocalStorage.enterWith() to establish context that persists\n * until the async resource completes. Useful for Express middleware where\n * the route handler executes after the middleware returns.\n *\n * @param context - Tenant context data\n *\n * @example Express middleware\n * ```typescript\n * app.use((req, res, next) => {\n * const tenantId = req.headers['x-tenant-id'] as string;\n * enterTenantContext({ tenantId });\n * next(); // Route handlers now have tenant context\n * });\n * ```\n */\nexport function enterTenantContext(\n context: TenantContextData | MinimalTenantContext,\n): void {\n const fullContext: TenantContextData = {\n permissions: new Set(),\n ...context,\n };\n tenantStorage.enterWith(fullContext);\n}\n\n/**\n * Run code in system context (bypasses tenant checks)\n *\n * Use this for:\n * - Migration scripts\n * - Admin tools that need cross-tenant access\n * - Background jobs that process multiple tenants\n *\n * System context is explicitly different from \"no context\" - it signals\n * that tenant checks should be bypassed, while no context means the\n * interceptor should enforce tenant requirements.\n *\n * @param fn - Async function to run without tenant context\n *\n * @example\n * ```typescript\n * await withSystemContext(async () => {\n * // No tenant context - can access all data\n * const allDocuments = await documentCollection.list({});\n * });\n * ```\n */\nexport async function withSystemContext<T>(fn: () => Promise<T>): Promise<T> {\n // Run with system context marker (distinct from undefined/no context)\n return tenantStorage.run(SYSTEM_CONTEXT_MARKER, fn);\n}\n\n/**\n * Run async code with the super admin bypass flag enabled on the current\n * tenant context.\n *\n * Unlike `withSystemContext()`, this does **not** remove the tenant context —\n * the caller's `tenantId` remains intact. The interceptor skips\n * auto-filtering only for classes that have `allowSuperAdminBypass: true` in\n * their `@TenantScoped()` config.\n *\n * A tenant context must already be active (i.e., this must be called from\n * within a `withTenant()` scope). Use `withSystemContext()` if no tenant\n * context is available at all.\n *\n * @param fn - Async function to run with super admin bypass enabled.\n * @returns Promise resolving to the return value of `fn`.\n * @throws {TenantContextError} If called outside any tenant context.\n *\n * @example\n * ```typescript\n * await withTenant({ tenantId: 'admin-tenant' }, async () => {\n * await withSuperAdminBypass(async () => {\n * // Can read any tenant's AuditLog (if allowSuperAdminBypass: true)\n * const logs = await auditLogCollection.list({});\n * });\n * });\n * ```\n *\n * @see withSystemContext\n * @see isSuperAdminBypass\n */\nexport async function withSuperAdminBypass<T>(\n fn: () => Promise<T>,\n): Promise<T> {\n const current = tenantStorage.getStore();\n if (!current || current === SYSTEM_CONTEXT_MARKER) {\n throw new TenantContextError(\n 'Cannot enable super admin bypass without a tenant context. ' +\n 'Use withTenant() first or withSystemContext() instead.',\n );\n }\n\n const bypassContext: TenantContextData = {\n ...current,\n superAdminBypass: true,\n };\n\n return tenantStorage.run(bypassContext, fn);\n}\n\n/**\n * Namespace object providing advanced tenant context utilities.\n *\n * Contains helpers for binding callbacks, inspecting context state, and\n * running code with the context stored in a queued job payload. These\n * utilities supplement the standalone exported functions for situations where\n * async context might otherwise be lost (e.g., `setTimeout`, event emitters,\n * message queue consumers).\n *\n * @example\n * ```typescript\n * import { TenantContext } from '@happyvertical/smrt-tenancy';\n *\n * // Preserve context across a setTimeout\n * setTimeout(TenantContext.bind(() => {\n * console.log(getTenantId()); // context is intact\n * }), 500);\n *\n * // Process a queued job\n * await TenantContext.runWithJobContext(job, async () => {\n * await processJob(job);\n * });\n * ```\n */\nexport const TenantContext = {\n /**\n * Bind a callback to the current tenant context\n *\n * Use this when passing callbacks to setTimeout, event emitters,\n * or other APIs that might lose the async context.\n *\n * @param fn - Function to bind to current context\n * @returns Wrapped function that will run in the original context\n *\n * @example\n * ```typescript\n * // Without bind - context is lost\n * setTimeout(() => {\n * console.log(getTenantId()); // undefined!\n * }, 1000);\n *\n * // With bind - context is preserved\n * setTimeout(TenantContext.bind(() => {\n * console.log(getTenantId()); // 'tenant-123'\n * }), 1000);\n * ```\n */\n bind<T extends (...args: unknown[]) => unknown>(fn: T): T {\n const store = tenantStorage.getStore();\n if (!store) {\n // No context to bind, return function as-is\n return fn;\n }\n\n // Preserve the context (including system context marker)\n return ((...args: unknown[]) => {\n return tenantStorage.run(store, () => fn(...args));\n }) as T;\n },\n\n /**\n * Get the current context data (or undefined for system/no context)\n */\n get current(): TenantContextData | undefined {\n return getCurrentTenant();\n },\n\n /**\n * Check if we're in system context\n */\n get isSystem(): boolean {\n return isSystemContext();\n },\n\n /**\n * Run code with context from a job/message payload\n *\n * Useful for processing queued jobs that include tenant metadata.\n *\n * @param job - Job object with tenantId in metadata\n * @param fn - Function to run in the job's tenant context\n *\n * @example\n * ```typescript\n * const job = await queue.pop();\n * await TenantContext.runWithJobContext(job, async () => {\n * await processJob(job);\n * });\n * ```\n */\n async runWithJobContext<T>(\n job: { metadata?: { tenantId?: string }; tenantId?: string },\n fn: () => Promise<T>,\n ): Promise<T> {\n const tenantId = job.metadata?.tenantId ?? job.tenantId;\n if (!tenantId) {\n throw new TenantContextError(\n 'Job does not contain tenant information. ' +\n 'Ensure jobs include tenantId in metadata or as a top-level field.',\n );\n }\n\n return withTenant({ tenantId }, fn);\n },\n};\n\n/**\n * Error thrown when a tenant context is required but not available.\n *\n * Raised by `requireTenant()`, `requireTenantId()`, and the tenant interceptor\n * when a `@TenantScoped({ mode: 'required' })` operation is attempted outside\n * any `withTenant()` scope.\n *\n * The `code` property is always `'TENANT_CONTEXT_REQUIRED'` and can be used for\n * programmatic error handling.\n *\n * @example\n * ```typescript\n * try {\n * const tenantId = requireTenantId();\n * } catch (err) {\n * if (err instanceof TenantContextError) {\n * // err.code === 'TENANT_CONTEXT_REQUIRED'\n * }\n * }\n * ```\n *\n * @see requireTenant\n * @see requireTenantId\n * @see TenantIsolationError\n */\nexport class TenantContextError extends Error {\n /** Stable error code; always `'TENANT_CONTEXT_REQUIRED'`. */\n readonly code = 'TENANT_CONTEXT_REQUIRED';\n\n constructor(message: string) {\n super(message);\n this.name = 'TenantContextError';\n }\n}\n\n/**\n * Error thrown when a tenant isolation boundary is crossed.\n *\n * Raised by the tenant interceptor when:\n * - A `list()` or `get()` query explicitly filters by a tenant ID that does not\n * match the current context tenant.\n * - A `save()` or `delete()` is attempted on an object whose `tenantId` field\n * belongs to a different tenant than the current context.\n * - A raw SQL query is executed against a tenant-scoped class without an\n * explicit bypass (when `rawQueryPolicy` is `'throw'`).\n *\n * The `code` property is always `'TENANT_ISOLATION_VIOLATION'`.\n *\n * @example\n * ```typescript\n * try {\n * await collection.list({ where: { tenantId: 'other-tenant' } });\n * } catch (err) {\n * if (err instanceof TenantIsolationError) {\n * // err.tenantId — context tenant\n * // err.attemptedTenantId — tenant that was attempted\n * }\n * }\n * ```\n *\n * @see TenantContextError\n * @see createTenantInterceptor\n */\nexport class TenantIsolationError extends Error {\n /** Stable error code; always `'TENANT_ISOLATION_VIOLATION'`. */\n readonly code = 'TENANT_ISOLATION_VIOLATION';\n /** The tenant ID that is active in the current context. */\n readonly tenantId?: string;\n /** The tenant ID that was attempted (and rejected). */\n readonly attemptedTenantId?: string;\n\n constructor(\n message: string,\n details?: { tenantId?: string; attemptedTenantId?: string },\n ) {\n super(message);\n this.name = 'TenantIsolationError';\n this.tenantId = details?.tenantId;\n this.attemptedTenantId = details?.attemptedTenantId;\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,153 @@
1
+ import { k as withTenant, j as withSystemContext, e as enterTenantContext } from "./context-B5CKsmMi.js";
2
+ function createCliContext(options = {}) {
3
+ const {
4
+ resolveTenantId,
5
+ resolveUserId,
6
+ defaultPermissions = /* @__PURE__ */ new Set(),
7
+ superAdminByDefault = false
8
+ } = options;
9
+ return {
10
+ async run(fn) {
11
+ const tenantId = resolveTenantId ? await resolveTenantId() : null;
12
+ if (!tenantId) {
13
+ return withSystemContext(fn);
14
+ }
15
+ const userId = resolveUserId ? await resolveUserId() : void 0;
16
+ const context = {
17
+ tenantId,
18
+ userId: userId ?? void 0,
19
+ permissions: defaultPermissions,
20
+ superAdminBypass: superAdminByDefault
21
+ };
22
+ return withTenant(context, fn);
23
+ },
24
+ async runWithTenant(tenantId, fn) {
25
+ const userId = resolveUserId ? await resolveUserId() : void 0;
26
+ const context = {
27
+ tenantId,
28
+ userId: userId ?? void 0,
29
+ permissions: defaultPermissions,
30
+ superAdminBypass: superAdminByDefault
31
+ };
32
+ return withTenant(context, fn);
33
+ },
34
+ async runAsSystem(fn) {
35
+ return withSystemContext(fn);
36
+ },
37
+ async runAsSuperAdmin(tenantId, fn) {
38
+ const userId = resolveUserId ? await resolveUserId() : void 0;
39
+ const context = {
40
+ tenantId,
41
+ userId: userId ?? void 0,
42
+ permissions: defaultPermissions,
43
+ superAdminBypass: true
44
+ };
45
+ return withTenant(context, fn);
46
+ }
47
+ };
48
+ }
49
+ function createExpressMiddleware(options) {
50
+ const {
51
+ resolveTenantId,
52
+ resolveUserId,
53
+ resolvePermissions,
54
+ isSuperAdmin,
55
+ onNoTenant,
56
+ excludePaths = []
57
+ } = options;
58
+ return async function tenancyMiddleware(req, res, next) {
59
+ try {
60
+ if (excludePaths.some((pattern) => matchPath$1(req.path, pattern))) {
61
+ next();
62
+ return;
63
+ }
64
+ const tenantId = await resolveTenantId(req);
65
+ if (!tenantId) {
66
+ if (onNoTenant) {
67
+ const shouldContinue = await onNoTenant(req, res);
68
+ if (shouldContinue) {
69
+ next();
70
+ return;
71
+ }
72
+ } else {
73
+ res.status(400).json({
74
+ error: "Tenant ID required",
75
+ message: "Please provide a tenant ID via header, subdomain, or query parameter."
76
+ });
77
+ return;
78
+ }
79
+ return;
80
+ }
81
+ const userId = resolveUserId ? await resolveUserId(req) : void 0;
82
+ const permissions = resolvePermissions ? await resolvePermissions(req, tenantId, userId ?? void 0) : /* @__PURE__ */ new Set();
83
+ const superAdminBypass = isSuperAdmin ? await isSuperAdmin(req, tenantId, userId ?? void 0) : false;
84
+ const context = {
85
+ tenantId,
86
+ userId: userId ?? void 0,
87
+ permissions,
88
+ superAdminBypass
89
+ };
90
+ req.tenantContext = context;
91
+ req.tenantId = tenantId;
92
+ enterTenantContext(context);
93
+ next();
94
+ } catch (error) {
95
+ next(error);
96
+ }
97
+ };
98
+ }
99
+ function matchPath$1(path, pattern) {
100
+ const regexPattern = pattern.replace(/\*/g, ".*").replace(/\//g, "\\/").replace(/\?/g, ".");
101
+ return new RegExp(`^${regexPattern}$`).test(path);
102
+ }
103
+ function createSvelteKitHandle(options) {
104
+ const {
105
+ resolveTenantId,
106
+ resolveUserId,
107
+ resolvePermissions,
108
+ isSuperAdmin,
109
+ onNoTenant,
110
+ excludePaths = []
111
+ } = options;
112
+ return async function handle({
113
+ event,
114
+ resolve
115
+ }) {
116
+ const path = event.url.pathname;
117
+ if (excludePaths.some((pattern) => matchPath(path, pattern))) {
118
+ return resolve(event);
119
+ }
120
+ const tenantId = await resolveTenantId(event);
121
+ if (!tenantId) {
122
+ if (onNoTenant) {
123
+ const response = await onNoTenant(event);
124
+ if (response) {
125
+ return response;
126
+ }
127
+ }
128
+ return resolve(event);
129
+ }
130
+ const userId = resolveUserId ? await resolveUserId(event) : void 0;
131
+ const permissions = resolvePermissions ? await resolvePermissions(event, tenantId, userId ?? void 0) : /* @__PURE__ */ new Set();
132
+ const superAdminBypass = isSuperAdmin ? await isSuperAdmin(event, tenantId, userId ?? void 0) : false;
133
+ const context = {
134
+ tenantId,
135
+ userId: userId ?? void 0,
136
+ permissions,
137
+ superAdminBypass
138
+ };
139
+ event.locals.tenantContext = context;
140
+ event.locals.tenantId = tenantId;
141
+ return withTenant(context, () => resolve(event));
142
+ };
143
+ }
144
+ function matchPath(path, pattern) {
145
+ const regexPattern = pattern.replace(/\*/g, ".*").replace(/\//g, "\\/").replace(/\?/g, ".");
146
+ return new RegExp(`^${regexPattern}$`).test(path);
147
+ }
148
+ export {
149
+ createExpressMiddleware as a,
150
+ createSvelteKitHandle as b,
151
+ createCliContext as c
152
+ };
153
+ //# sourceMappingURL=sveltekit-9eRH1RLw.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sveltekit-9eRH1RLw.js","sources":["../../src/adapters/cli.ts","../../src/adapters/express.ts","../../src/adapters/sveltekit.ts"],"sourcesContent":["/**\n * CLI Adapter for smrt-tenancy\n *\n * Provides utilities for setting up tenant context in CLI tools.\n *\n * @example\n * ```typescript\n * import { createCliContext } from '@happyvertical/smrt-tenancy/adapters';\n *\n * // Create context helper for CLI\n * const cliContext = createCliContext({\n * resolveTenantId: () => process.env.TENANT_ID,\n * });\n *\n * // Run command in tenant context\n * await cliContext.run(async () => {\n * await documentCollection.list({});\n * });\n * ```\n */\n\nimport {\n type TenantContextData,\n withSystemContext,\n withTenant,\n} from '../context.js';\n\n/**\n * Configuration for the CLI context runner created by `createCliContext()`.\n *\n * `resolveTenantId` is optional — when omitted (or when it returns\n * `null`/`undefined`) the `run()` method falls back to `withSystemContext()`.\n *\n * @see createCliContext\n * @see CliContextRunner\n */\nexport interface CliContextOptions {\n /**\n * Resolve tenant ID\n *\n * Common sources:\n * - Environment variable: () => process.env.TENANT_ID\n * - Command line argument: () => argv.tenant\n * - Config file: () => config.tenantId\n */\n resolveTenantId?: () =>\n | Promise<string | null | undefined>\n | string\n | null\n | undefined;\n\n /**\n * Resolve user ID (optional)\n */\n resolveUserId?: () =>\n | Promise<string | null | undefined>\n | string\n | null\n | undefined;\n\n /**\n * Default permissions for CLI operations\n */\n defaultPermissions?: Set<string>;\n\n /**\n * Run CLI as super admin by default\n * @default false\n */\n superAdminByDefault?: boolean;\n}\n\n/**\n * Context runner returned by `createCliContext()`.\n *\n * Provides four execution modes suitable for different CLI scenarios:\n * - `run()` — resolves the tenant from the configured options and runs code in\n * that context; falls back to system context if no tenant is available.\n * - `runWithTenant()` — explicitly specify a tenant ID for this invocation.\n * - `runAsSystem()` — bypass all tenant checks (migration scripts, admin tools).\n * - `runAsSuperAdmin()` — tenant context with bypass flag enabled.\n *\n * @see createCliContext\n */\nexport interface CliContextRunner {\n /**\n * Run `fn` inside the tenant context resolved from the `CliContextOptions`.\n *\n * Falls back to `withSystemContext()` when no tenant ID is available.\n *\n * @param fn - Async function to execute.\n * @returns Promise resolving to the return value of `fn`.\n */\n run<T>(fn: () => Promise<T>): Promise<T>;\n\n /**\n * Run `fn` inside the context of the specified tenant.\n *\n * @param tenantId - Tenant ID to set as context.\n * @param fn - Async function to execute.\n * @returns Promise resolving to the return value of `fn`.\n */\n runWithTenant<T>(tenantId: string, fn: () => Promise<T>): Promise<T>;\n\n /**\n * Run `fn` in system context, bypassing all tenant checks.\n *\n * @param fn - Async function to execute.\n * @returns Promise resolving to the return value of `fn`.\n */\n runAsSystem<T>(fn: () => Promise<T>): Promise<T>;\n\n /**\n * Run `fn` with a tenant context and super admin bypass enabled.\n *\n * @param tenantId - Tenant ID to set as context.\n * @param fn - Async function to execute.\n * @returns Promise resolving to the return value of `fn`.\n */\n runAsSuperAdmin<T>(tenantId: string, fn: () => Promise<T>): Promise<T>;\n}\n\n/**\n * Create a CLI context runner\n *\n * @param options - Configuration options\n * @returns CLI context runner with various execution modes\n *\n * @example\n * ```typescript\n * const cli = createCliContext({\n * resolveTenantId: () => process.env.TENANT_ID,\n * superAdminByDefault: true,\n * });\n *\n * // Use resolved tenant\n * await cli.run(async () => {\n * const docs = await collection.list({});\n * console.log(`Found ${docs.length} documents`);\n * });\n *\n * // Override tenant\n * await cli.runWithTenant('other-tenant', async () => {\n * // Operations in other-tenant context\n * });\n *\n * // System operations (no tenant)\n * await cli.runAsSystem(async () => {\n * // Can access all data\n * const allDocs = await collection.list({});\n * });\n * ```\n */\nexport function createCliContext(\n options: CliContextOptions = {},\n): CliContextRunner {\n const {\n resolveTenantId,\n resolveUserId,\n defaultPermissions = new Set<string>(),\n superAdminByDefault = false,\n } = options;\n\n return {\n async run<T>(fn: () => Promise<T>): Promise<T> {\n const tenantId = resolveTenantId ? await resolveTenantId() : null;\n\n if (!tenantId) {\n // No tenant configured - run in system context\n return withSystemContext(fn);\n }\n\n const userId = resolveUserId ? await resolveUserId() : undefined;\n\n const context: TenantContextData = {\n tenantId,\n userId: userId ?? undefined,\n permissions: defaultPermissions,\n superAdminBypass: superAdminByDefault,\n };\n\n return withTenant(context, fn);\n },\n\n async runWithTenant<T>(tenantId: string, fn: () => Promise<T>): Promise<T> {\n const userId = resolveUserId ? await resolveUserId() : undefined;\n\n const context: TenantContextData = {\n tenantId,\n userId: userId ?? undefined,\n permissions: defaultPermissions,\n superAdminBypass: superAdminByDefault,\n };\n\n return withTenant(context, fn);\n },\n\n async runAsSystem<T>(fn: () => Promise<T>): Promise<T> {\n return withSystemContext(fn);\n },\n\n async runAsSuperAdmin<T>(\n tenantId: string,\n fn: () => Promise<T>,\n ): Promise<T> {\n const userId = resolveUserId ? await resolveUserId() : undefined;\n\n const context: TenantContextData = {\n tenantId,\n userId: userId ?? undefined,\n permissions: defaultPermissions,\n superAdminBypass: true,\n };\n\n return withTenant(context, fn);\n },\n };\n}\n\n/**\n * Run an async function with a specific tenant ID set as context.\n *\n * Convenience wrapper around `withTenant()` for one-off CLI operations where\n * a full `CliContextRunner` is not needed.\n *\n * @param tenantId - Tenant ID to set as the active context.\n * @param fn - Async function to execute in the tenant context.\n * @returns Promise resolving to the return value of `fn`.\n *\n * @example\n * ```typescript\n * import { runWithTenant } from '@happyvertical/smrt-tenancy/adapters';\n *\n * await runWithTenant('tenant-123', async () => {\n * await collection.list({});\n * });\n * ```\n *\n * @see createCliContext\n * @see runAsSystem\n */\nexport async function runWithTenant<T>(\n tenantId: string,\n fn: () => Promise<T>,\n): Promise<T> {\n return withTenant({ tenantId }, fn);\n}\n\n/**\n * Run an async function in system context, bypassing all tenant checks.\n *\n * Convenience wrapper around `withSystemContext()` for one-off CLI operations\n * such as migration scripts or admin tooling that needs cross-tenant access.\n *\n * @param fn - Async function to execute in system context.\n * @returns Promise resolving to the return value of `fn`.\n *\n * @example\n * ```typescript\n * import { runAsSystem } from '@happyvertical/smrt-tenancy/adapters';\n *\n * await runAsSystem(async () => {\n * const all = await collection.list({});\n * console.log(`Total records: ${all.length}`);\n * });\n * ```\n *\n * @see runWithTenant\n * @see createCliContext\n */\nexport async function runAsSystem<T>(fn: () => Promise<T>): Promise<T> {\n return withSystemContext(fn);\n}\n","/**\n * Express Adapter for smrt-tenancy\n *\n * Provides Express middleware that sets up tenant context for each request.\n *\n * @example\n * ```typescript\n * import express from 'express';\n * import { createExpressMiddleware } from '@happyvertical/smrt-tenancy/adapters';\n *\n * const app = express();\n *\n * app.use(createExpressMiddleware({\n * resolveTenantId: (req) => req.headers['x-tenant-id'] as string,\n * }));\n * ```\n */\n\nimport { enterTenantContext, type TenantContextData } from '../context.js';\n\n/**\n * Express Request interface (minimal to avoid direct dependency)\n */\ninterface ExpressRequest {\n headers: Record<string, string | string[] | undefined>;\n url: string;\n path: string;\n query: Record<string, unknown>;\n cookies?: Record<string, string>;\n}\n\n/**\n * Express Response interface\n */\ninterface ExpressResponse {\n status(code: number): ExpressResponse;\n json(data: unknown): ExpressResponse;\n send(data: unknown): ExpressResponse;\n}\n\n/**\n * Express NextFunction\n */\ntype ExpressNext = (error?: unknown) => void;\n\n/**\n * Configuration options for the Express tenancy middleware created by\n * `createExpressMiddleware()`.\n *\n * Only `resolveTenantId` is required. All callback options receive the raw\n * Express `Request` object so you can extract tenant information from headers,\n * subdomains, cookies, or any other request property.\n *\n * @see createExpressMiddleware\n */\nexport interface ExpressMiddlewareOptions {\n /**\n * Resolve tenant ID from the request\n */\n resolveTenantId: (\n req: ExpressRequest,\n ) => Promise<string | null | undefined> | string | null | undefined;\n\n /**\n * Resolve user ID from the request (optional)\n */\n resolveUserId?: (\n req: ExpressRequest,\n ) => Promise<string | null | undefined> | string | null | undefined;\n\n /**\n * Resolve permissions (optional)\n */\n resolvePermissions?: (\n req: ExpressRequest,\n tenantId: string,\n userId?: string,\n ) => Promise<Set<string>> | Set<string>;\n\n /**\n * Check if user is super admin (optional)\n */\n isSuperAdmin?: (\n req: ExpressRequest,\n tenantId: string,\n userId?: string,\n ) => Promise<boolean> | boolean;\n\n /**\n * Called when no tenant ID could be resolved\n * Return true to continue, false to stop with 400 error.\n */\n onNoTenant?: (\n req: ExpressRequest,\n res: ExpressResponse,\n ) => Promise<boolean> | boolean;\n\n /**\n * Paths to exclude from tenant context\n */\n excludePaths?: string[];\n}\n\n/**\n * Create an Express middleware function that establishes tenant context for\n * every incoming request.\n *\n * Uses `enterTenantContext()` (rather than `withTenant()`) because Express\n * middleware returns before route handlers execute. `enterWith()` sets the\n * context on the current async resource so it propagates to handlers that run\n * after `next()` is called.\n *\n * The resolved context is also attached directly to the request object for\n * convenience:\n * - `req.tenantContext` — full `TenantContextData`\n * - `req.tenantId` — string tenant ID shortcut\n *\n * When no tenant ID can be resolved, the default behaviour returns a `400`\n * JSON response. Customise this with the `onNoTenant` option.\n *\n * @param options - Middleware configuration including the required\n * `resolveTenantId` callback.\n * @returns An Express-compatible middleware function `(req, res, next) => void`.\n *\n * @example\n * ```typescript\n * import express from 'express';\n * import { createExpressMiddleware } from '@happyvertical/smrt-tenancy/adapters';\n *\n * const app = express();\n * app.use(createExpressMiddleware({\n * resolveTenantId: (req) => req.headers['x-tenant-id'] as string,\n * excludePaths: ['/health', '/public/*'],\n * }));\n * ```\n *\n * @see ExpressMiddlewareOptions\n * @see createSvelteKitHandle\n */\nexport function createExpressMiddleware(options: ExpressMiddlewareOptions) {\n const {\n resolveTenantId,\n resolveUserId,\n resolvePermissions,\n isSuperAdmin,\n onNoTenant,\n excludePaths = [],\n } = options;\n\n return async function tenancyMiddleware(\n req: ExpressRequest,\n res: ExpressResponse,\n next: ExpressNext,\n ): Promise<void> {\n try {\n // Check excluded paths\n if (excludePaths.some((pattern) => matchPath(req.path, pattern))) {\n next();\n return;\n }\n\n // Resolve tenant ID\n const tenantId = await resolveTenantId(req);\n\n if (!tenantId) {\n if (onNoTenant) {\n const shouldContinue = await onNoTenant(req, res);\n if (shouldContinue) {\n next();\n return;\n }\n } else {\n res.status(400).json({\n error: 'Tenant ID required',\n message:\n 'Please provide a tenant ID via header, subdomain, or query parameter.',\n });\n return;\n }\n return;\n }\n\n // Resolve optional context\n const userId = resolveUserId ? await resolveUserId(req) : undefined;\n const permissions = resolvePermissions\n ? await resolvePermissions(req, tenantId, userId ?? undefined)\n : new Set<string>();\n const superAdminBypass = isSuperAdmin\n ? await isSuperAdmin(req, tenantId, userId ?? undefined)\n : false;\n\n // Build context\n const context: TenantContextData = {\n tenantId,\n userId: userId ?? undefined,\n permissions,\n superAdminBypass,\n };\n\n // Attach to request for access in route handlers\n (req as any).tenantContext = context;\n (req as any).tenantId = tenantId;\n\n // Use enterWith() to establish context that persists for the entire\n // request lifecycle. This ensures route handlers have access to\n // tenant context via AsyncLocalStorage.\n enterTenantContext(context);\n next();\n } catch (error) {\n next(error);\n }\n };\n}\n\n/**\n * Simple glob pattern matching for paths\n */\nfunction matchPath(path: string, pattern: string): boolean {\n const regexPattern = pattern\n .replace(/\\*/g, '.*')\n .replace(/\\//g, '\\\\/')\n .replace(/\\?/g, '.');\n\n return new RegExp(`^${regexPattern}$`).test(path);\n}\n","/**\n * SvelteKit Adapter for smrt-tenancy\n *\n * Provides a SvelteKit Handle that sets up tenant context for each request.\n *\n * @example\n * ```typescript\n * // hooks.server.ts\n * import { createSvelteKitHandle } from '@happyvertical/smrt-tenancy/adapters';\n *\n * export const handle = createSvelteKitHandle({\n * resolveTenantId: async (event) => {\n * // From subdomain\n * const host = event.request.headers.get('host');\n * const subdomain = host?.split('.')[0];\n * return subdomain;\n *\n * // Or from header\n * // return event.request.headers.get('x-tenant-id');\n *\n * // Or from cookie\n * // return event.cookies.get('tenant_id');\n * }\n * });\n * ```\n */\n\nimport { type TenantContextData, withTenant } from '../context.js';\n\n/**\n * SvelteKit RequestEvent (minimal interface to avoid direct dependency)\n */\ninterface SvelteKitEvent {\n request: Request;\n url: URL;\n cookies: {\n get(name: string): string | undefined;\n set(name: string, value: string, opts?: unknown): void;\n };\n locals: Record<string, unknown>;\n}\n\n/**\n * SvelteKit resolve function\n */\ntype SvelteKitResolve = (event: SvelteKitEvent) => Promise<Response>;\n\n/**\n * Configuration options for the SvelteKit tenancy handle created by\n * `createSvelteKitHandle()`.\n *\n * Only `resolveTenantId` is required; all other fields are optional callbacks\n * used to enrich the context or customise missing-tenant behaviour.\n *\n * @see createSvelteKitHandle\n */\nexport interface SvelteKitHandleOptions {\n /**\n * Resolve tenant ID from the request\n *\n * Return the tenant ID string, or null/undefined if no tenant context should be set.\n */\n resolveTenantId: (\n event: SvelteKitEvent,\n ) => Promise<string | null | undefined> | string | null | undefined;\n\n /**\n * Resolve user ID from the request (optional)\n */\n resolveUserId?: (\n event: SvelteKitEvent,\n ) => Promise<string | null | undefined> | string | null | undefined;\n\n /**\n * Resolve permissions for the user in this tenant (optional)\n */\n resolvePermissions?: (\n event: SvelteKitEvent,\n tenantId: string,\n userId?: string,\n ) => Promise<Set<string>> | Set<string>;\n\n /**\n * Check if user is a super admin (optional)\n * If true and super admin bypass is enabled on the class, tenant filtering is skipped.\n */\n isSuperAdmin?: (\n event: SvelteKitEvent,\n tenantId: string,\n userId?: string,\n ) => Promise<boolean> | boolean;\n\n /**\n * Called when no tenant ID could be resolved\n * Return a Response to short-circuit, or undefined to continue without tenant context.\n */\n onNoTenant?: (\n event: SvelteKitEvent,\n ) => Promise<Response | undefined> | Response | undefined;\n\n /**\n * Paths to exclude from tenant context (e.g., public APIs, health checks)\n * Supports glob patterns.\n */\n excludePaths?: string[];\n}\n\n/**\n * Create a SvelteKit `Handle` function that establishes tenant context for\n * every server-side request.\n *\n * The returned handle wraps the `resolve` call in `withTenant()` so that all\n * server-side load functions, API routes, and `+server.ts` handlers within the\n * request share the same tenant context via `AsyncLocalStorage`.\n *\n * The resolved context is also stored in `event.locals` under two keys:\n * - `event.locals.tenantContext` — full `TenantContextData`\n * - `event.locals.tenantId` — string tenant ID shortcut\n *\n * When no tenant ID can be resolved (and no custom `onNoTenant` handler\n * returns a `Response`), the request continues without any tenant context.\n *\n * @param options - Configuration options including the required\n * `resolveTenantId` callback.\n * @returns A SvelteKit `Handle` function suitable for use in `hooks.server.ts`.\n *\n * @example\n * ```typescript\n * // src/hooks.server.ts\n * import { createSvelteKitHandle } from '@happyvertical/smrt-tenancy/adapters';\n *\n * export const handle = createSvelteKitHandle({\n * resolveTenantId: (event) =>\n * event.request.headers.get('x-tenant-id'),\n * onNoTenant: () =>\n * new Response('Tenant required', { status: 400 }),\n * });\n * ```\n *\n * @see SvelteKitHandleOptions\n * @see createExpressMiddleware\n */\nexport function createSvelteKitHandle(options: SvelteKitHandleOptions) {\n const {\n resolveTenantId,\n resolveUserId,\n resolvePermissions,\n isSuperAdmin,\n onNoTenant,\n excludePaths = [],\n } = options;\n\n return async function handle({\n event,\n resolve,\n }: {\n event: SvelteKitEvent;\n resolve: SvelteKitResolve;\n }): Promise<Response> {\n // Check excluded paths\n const path = event.url.pathname;\n if (excludePaths.some((pattern) => matchPath(path, pattern))) {\n return resolve(event);\n }\n\n // Resolve tenant ID\n const tenantId = await resolveTenantId(event);\n\n if (!tenantId) {\n // No tenant ID - call handler or continue without context\n if (onNoTenant) {\n const response = await onNoTenant(event);\n if (response) {\n return response;\n }\n }\n return resolve(event);\n }\n\n // Resolve optional context data\n const userId = resolveUserId ? await resolveUserId(event) : undefined;\n const permissions = resolvePermissions\n ? await resolvePermissions(event, tenantId, userId ?? undefined)\n : new Set<string>();\n const superAdminBypass = isSuperAdmin\n ? await isSuperAdmin(event, tenantId, userId ?? undefined)\n : false;\n\n // Build context\n const context: TenantContextData = {\n tenantId,\n userId: userId ?? undefined,\n permissions,\n superAdminBypass,\n };\n\n // Store in locals for access in load functions\n event.locals.tenantContext = context;\n event.locals.tenantId = tenantId;\n\n // Run request in tenant context\n return withTenant(context, () => resolve(event));\n };\n}\n\n/**\n * Simple glob pattern matching for paths\n */\nfunction matchPath(path: string, pattern: string): boolean {\n // Convert glob to regex\n const regexPattern = pattern\n .replace(/\\*/g, '.*')\n .replace(/\\//g, '\\\\/')\n .replace(/\\?/g, '.');\n\n return new RegExp(`^${regexPattern}$`).test(path);\n}\n"],"names":["matchPath"],"mappings":"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"}