@zudojs/tenancy 0.1.0 → 1.1.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 (113) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +70 -13
  3. package/dist/context/contextManager.core.d.ts +12 -2
  4. package/dist/context/contextManager.core.js +34 -22
  5. package/dist/http/httpResolverContext.d.ts +35 -0
  6. package/dist/http/httpResolverContext.js +40 -0
  7. package/dist/http/index.d.ts +8 -8
  8. package/dist/http/index.js +4 -5
  9. package/dist/http/tenancyMiddleware.core.d.ts +34 -3
  10. package/dist/http/tenancyMiddleware.core.js +57 -35
  11. package/dist/http/tenancyMiddleware.guard.d.ts +13 -5
  12. package/dist/http/tenancyMiddleware.guard.js +11 -3
  13. package/dist/repository/index.d.ts +2 -1
  14. package/dist/repository/index.js +1 -1
  15. package/dist/repository/repository.core.d.ts +38 -7
  16. package/dist/repository/repository.core.js +69 -15
  17. package/dist/repository/tenantManager.core.d.ts +21 -3
  18. package/dist/repository/tenantManager.core.js +43 -14
  19. package/dist/resolver/index.d.ts +1 -0
  20. package/dist/resolver/resolverChain.core.d.ts +22 -7
  21. package/dist/resolver/resolverChain.core.js +45 -13
  22. package/dist/resolver/resolvers/headerResolver.core.d.ts +10 -2
  23. package/dist/resolver/resolvers/headerResolver.core.js +5 -8
  24. package/dist/resolver/resolvers/index.d.ts +9 -5
  25. package/dist/resolver/resolvers/index.js +5 -5
  26. package/dist/resolver/resolvers/jwtResolver.core.d.ts +1 -1
  27. package/dist/resolver/resolvers/jwtResolver.core.js +4 -8
  28. package/dist/resolver/resolvers/pathResolver.core.d.ts +7 -3
  29. package/dist/resolver/resolvers/pathResolver.core.js +12 -9
  30. package/dist/resolver/resolvers/subdomainResolver.core.d.ts +11 -2
  31. package/dist/resolver/resolvers/subdomainResolver.core.js +31 -14
  32. package/dist/security/guard.core.d.ts +19 -0
  33. package/dist/security/guard.core.js +58 -9
  34. package/dist/security/index.d.ts +1 -1
  35. package/dist/security/index.js +1 -1
  36. package/dist/tenancyErrors/index.d.ts +1 -1
  37. package/dist/tenancyErrors/index.js +1 -1
  38. package/dist/tenancyErrors/tenancyError.types.d.ts +11 -6
  39. package/dist/tenancyErrors/tenancyError.types.js +15 -12
  40. package/dist/tenancyTypes/index.d.ts +3 -3
  41. package/dist/tenancyTypes/index.js +2 -2
  42. package/dist/tenancyTypes/repositoryTypes.d.ts +6 -21
  43. package/dist/tenancyTypes/resolverTypes.d.ts +14 -2
  44. package/dist/tenancyTypes/tenantIdentity.d.ts +24 -5
  45. package/dist/tenancyTypes/tenantIdentity.js +54 -4
  46. package/package.json +26 -15
  47. package/dist/.tsbuildinfo +0 -1
  48. package/dist/context/contextManager.core.d.ts.map +0 -1
  49. package/dist/context/contextManager.core.js.map +0 -1
  50. package/dist/context/contextStorage.core.d.ts.map +0 -1
  51. package/dist/context/contextStorage.core.js.map +0 -1
  52. package/dist/context/index.d.ts.map +0 -1
  53. package/dist/context/index.js.map +0 -1
  54. package/dist/http/httpHelpers.d.ts.map +0 -1
  55. package/dist/http/httpHelpers.js.map +0 -1
  56. package/dist/http/httpTypes.d.ts.map +0 -1
  57. package/dist/http/httpTypes.js.map +0 -1
  58. package/dist/http/index.d.ts.map +0 -1
  59. package/dist/http/index.js.map +0 -1
  60. package/dist/http/tenancyMiddleware.core.d.ts.map +0 -1
  61. package/dist/http/tenancyMiddleware.core.js.map +0 -1
  62. package/dist/http/tenancyMiddleware.guard.d.ts.map +0 -1
  63. package/dist/http/tenancyMiddleware.guard.js.map +0 -1
  64. package/dist/index.d.ts.map +0 -1
  65. package/dist/index.js.map +0 -1
  66. package/dist/repository/index.d.ts.map +0 -1
  67. package/dist/repository/index.js.map +0 -1
  68. package/dist/repository/repository.core.d.ts.map +0 -1
  69. package/dist/repository/repository.core.js.map +0 -1
  70. package/dist/repository/tenantManager.core.d.ts.map +0 -1
  71. package/dist/repository/tenantManager.core.js.map +0 -1
  72. package/dist/resolver/index.d.ts.map +0 -1
  73. package/dist/resolver/index.js.map +0 -1
  74. package/dist/resolver/resolverChain.core.d.ts.map +0 -1
  75. package/dist/resolver/resolverChain.core.js.map +0 -1
  76. package/dist/resolver/resolvers/headerResolver.core.d.ts.map +0 -1
  77. package/dist/resolver/resolvers/headerResolver.core.js.map +0 -1
  78. package/dist/resolver/resolvers/index.d.ts.map +0 -1
  79. package/dist/resolver/resolvers/index.js.map +0 -1
  80. package/dist/resolver/resolvers/jwtResolver.core.d.ts.map +0 -1
  81. package/dist/resolver/resolvers/jwtResolver.core.js.map +0 -1
  82. package/dist/resolver/resolvers/pathResolver.core.d.ts.map +0 -1
  83. package/dist/resolver/resolvers/pathResolver.core.js.map +0 -1
  84. package/dist/resolver/resolvers/subdomainResolver.core.d.ts.map +0 -1
  85. package/dist/resolver/resolvers/subdomainResolver.core.js.map +0 -1
  86. package/dist/security/guard.core.d.ts.map +0 -1
  87. package/dist/security/guard.core.js.map +0 -1
  88. package/dist/security/index.d.ts.map +0 -1
  89. package/dist/security/index.js.map +0 -1
  90. package/dist/tenancyErrors/index.d.ts.map +0 -1
  91. package/dist/tenancyErrors/index.js.map +0 -1
  92. package/dist/tenancyErrors/tenancyError.base.d.ts.map +0 -1
  93. package/dist/tenancyErrors/tenancyError.base.js.map +0 -1
  94. package/dist/tenancyErrors/tenancyError.types.d.ts.map +0 -1
  95. package/dist/tenancyErrors/tenancyError.types.js.map +0 -1
  96. package/dist/tenancyTypes/index.d.ts.map +0 -1
  97. package/dist/tenancyTypes/index.js.map +0 -1
  98. package/dist/tenancyTypes/repositoryTypes.d.ts.map +0 -1
  99. package/dist/tenancyTypes/repositoryTypes.js.map +0 -1
  100. package/dist/tenancyTypes/resolverTypes.d.ts.map +0 -1
  101. package/dist/tenancyTypes/resolverTypes.js.map +0 -1
  102. package/dist/tenancyTypes/tenancyOptions.d.ts +0 -37
  103. package/dist/tenancyTypes/tenancyOptions.d.ts.map +0 -1
  104. package/dist/tenancyTypes/tenancyOptions.js +0 -7
  105. package/dist/tenancyTypes/tenancyOptions.js.map +0 -1
  106. package/dist/tenancyTypes/tenantIdentity.d.ts.map +0 -1
  107. package/dist/tenancyTypes/tenantIdentity.js.map +0 -1
  108. package/dist/tenancyTypes/tenantInterface.d.ts.map +0 -1
  109. package/dist/tenancyTypes/tenantInterface.js.map +0 -1
  110. package/dist/utils/index.d.ts.map +0 -1
  111. package/dist/utils/index.js.map +0 -1
  112. package/dist/utils/utils.helper.d.ts.map +0 -1
  113. package/dist/utils/utils.helper.js.map +0 -1
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zudojs Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @zudojs/tenancy
2
2
 
3
- Multi-tenant context and isolation with tenant resolution, AsyncLocalStorage propagation, resolver chains, trust levels, and guard middleware.
3
+ Multi-tenant context and isolation: tenant resolution, context propagation, resolver chains, and guard middleware.
4
4
 
5
5
  ## Installation
6
6
 
@@ -11,26 +11,83 @@ npm install @zudojs/tenancy
11
11
  ## Quick Start
12
12
 
13
13
  ```typescript
14
- import { createTenantContextManager } from "@zudojs/tenancy";
14
+ import {
15
+ createJwtResolver,
16
+ createMemoryTenantRepository,
17
+ createResolveTenantMiddleware,
18
+ createResolverChain,
19
+ createSubdomainResolver,
20
+ createTenantContextStorage,
21
+ } from "@zudojs/tenancy";
15
22
 
16
- const manager = createTenantContextManager({
17
- resolvers: [headerResolver, subdomainResolver, jwtResolver],
23
+ const storage = createTenantContextStorage();
24
+ const repository = createMemoryTenantRepository();
25
+
26
+ const resolver = createResolverChain([
27
+ createJwtResolver(), // priority 100, trusted
28
+ createSubdomainResolver({ baseDomain: "example.com" }), // priority 70
29
+ ]);
30
+
31
+ // Resolves the tenant, enforces its status and the route's trust floor,
32
+ // then runs the rest of the request inside the tenant context.
33
+ const middleware = createResolveTenantMiddleware({
34
+ resolver: resolver.asResolver(),
35
+ repository,
36
+ storage,
37
+ minimumTrust: "verified",
18
38
  });
39
+ ```
40
+
41
+ Read the current tenant anywhere downstream:
42
+
43
+ ```typescript
44
+ import { createContextManager } from "@zudojs/tenancy";
19
45
 
20
- const tenant = await manager.resolve(request);
46
+ const context = createContextManager({ storage });
47
+ const tenant = context.requireCurrentTenant();
21
48
  ```
22
49
 
50
+ ## Resolution Trust
51
+
52
+ | Source | Default trust | Note |
53
+ | --------------------- | ------------- | --------------------------- |
54
+ | `jwt`, `api-key` | `trusted` | verified credential |
55
+ | `subdomain`, `domain` | `verified` | host-derived |
56
+ | `header`, `path` | `untrusted` | client-supplied on the wire |
57
+
58
+ `x-tenant-id` is untrusted by default. Raise it with
59
+ `createHeaderResolver({ trust: "verified" })` only where a trusted proxy
60
+ strips and re-sets the header at the edge.
61
+
23
62
  ## Features
24
63
 
25
- - Tenant resolution from headers, subdomains, JWT, or custom resolvers
64
+ - Tenant resolution from JWT claims, subdomain, custom domain, header, or path
65
+ - Resolver chains with trust grading and conflict detection
26
66
  - AsyncLocalStorage context propagation
27
- - Tenant isolation and data scoping
28
- - Trust levels (trusted, untrusted, isolated)
29
- - Guard middleware for route protection
67
+ - Tenant repository with slug and custom-domain indexes
68
+ - Guard middleware and key scoping helpers
69
+
70
+ ## Safety Notes
71
+
72
+ - A resolver that **throws** rejected a credential, and aborts the chain. Only
73
+ returning `undefined` means "found nothing" and advances to the next
74
+ resolver — so a failed JWT verification can never fall through to a
75
+ client-supplied header.
76
+ - Resolvers disagreeing about the tenant throws by default.
77
+ - Tenant ids are normalized (NFKC, trimmed, lowercased) and constrained to
78
+ `^[a-z0-9][a-z0-9_-]*$`, so an id cannot forge a separator in a cache key,
79
+ a schema name, or a path. `tenantKey` escapes its segments as well.
80
+ - Non-active tenants are refused during resolution. Pass `allowInactive` on
81
+ routes that exist to serve suspended tenants.
82
+ - A request that resolves to no tenant at all is answered `404`. Pass
83
+ `optional: true` to `createResolveTenantMiddleware` on routes where a tenant
84
+ may be absent; a tenant that _was_ named but is unknown, untrusted or
85
+ suspended is still refused.
86
+ - `createPathResolver({ prefix })` only names a tenant for paths under the
87
+ prefix; `/health` never resolves to a tenant called `health`.
30
88
 
31
89
  ## Use Cases
32
90
 
33
- - SaaS multi-tenant applications
34
- - Data isolation per tenant
35
- - Tenant-aware routing
36
- - Compliance and data separation
91
+ - SaaS multi-tenancy
92
+ - Per-tenant data isolation
93
+ - Tenant-scoped caching and configuration
@@ -9,6 +9,13 @@ import type { ExecutionTenantContext } from "../tenancyTypes/tenantInterface.js"
9
9
  /** Options for the context manager. */
10
10
  export interface ContextManagerOptions {
11
11
  readonly storage: TenantContextStorage;
12
+ /**
13
+ * Allow entering a context for a non-active tenant. Defaults to false.
14
+ *
15
+ * This is the entry point for background jobs and scripts, where no HTTP
16
+ * guard runs, so a suspended tenant is refused unless explicitly permitted.
17
+ */
18
+ readonly allowInactive?: boolean;
12
19
  }
13
20
  /**
14
21
  * Create a tenant context manager.
@@ -21,7 +28,7 @@ export declare function createContextManager(options: ContextManagerOptions): {
21
28
  /**
22
29
  * Get the current tenant, if any.
23
30
  */
24
- getCurrentTenant(): Tenant | undefined;
31
+ getCurrentTenant: () => Tenant | undefined;
25
32
  /**
26
33
  * Require a current tenant — throws if missing.
27
34
  */
@@ -32,8 +39,11 @@ export declare function createContextManager(options: ContextManagerOptions): {
32
39
  isSystemMode(): boolean;
33
40
  /**
34
41
  * Run a callback within a tenant context.
42
+ *
43
+ * @throws {TenantUnavailableError} when the tenant is not active and
44
+ * `allowInactive` was not set.
35
45
  */
36
- run<T>(tenant: Tenant, callback: () => T): T;
46
+ run: <T>(tenant: Tenant, callback: () => T) => T;
37
47
  /**
38
48
  * Run a callback in system mode (no tenant).
39
49
  */
@@ -4,11 +4,38 @@
4
4
  * @module context/contextManager
5
5
  */
6
6
  import { TenantContextMissingError } from "../tenancyErrors/tenancyError.types.js";
7
+ import { assertTenantUsable } from "../security/guard.core.js";
7
8
  /**
8
9
  * Create a tenant context manager.
9
10
  */
10
11
  export function createContextManager(options) {
11
12
  const { storage } = options;
13
+ const allowInactive = options.allowInactive ?? false;
14
+ // Methods close over these rather than reading `this`, so a method pulled
15
+ // off the manager (`const { requireCurrentTenant } = manager`) still works
16
+ // instead of failing with a TypeError that masks the real error.
17
+ function getCurrentTenant() {
18
+ const ctx = storage.get();
19
+ if (ctx?.mode === "tenant")
20
+ return ctx.tenant;
21
+ return undefined;
22
+ }
23
+ function run(tenant, callback) {
24
+ if (!allowInactive)
25
+ assertTenantUsable(tenant);
26
+ const context = {
27
+ mode: "tenant",
28
+ tenant,
29
+ context: {
30
+ tenantId: tenant.id,
31
+ source: "manual",
32
+ trust: "trusted",
33
+ resolvedAt: new Date(),
34
+ metadata: {},
35
+ },
36
+ };
37
+ return storage.run(context, callback);
38
+ }
12
39
  return {
13
40
  /**
14
41
  * Get the current execution context.
@@ -19,17 +46,12 @@ export function createContextManager(options) {
19
46
  /**
20
47
  * Get the current tenant, if any.
21
48
  */
22
- getCurrentTenant() {
23
- const ctx = storage.get();
24
- if (ctx?.mode === "tenant")
25
- return ctx.tenant;
26
- return undefined;
27
- },
49
+ getCurrentTenant,
28
50
  /**
29
51
  * Require a current tenant — throws if missing.
30
52
  */
31
53
  requireCurrentTenant() {
32
- const tenant = this.getCurrentTenant();
54
+ const tenant = getCurrentTenant();
33
55
  if (!tenant)
34
56
  throw new TenantContextMissingError();
35
57
  return tenant;
@@ -43,21 +65,11 @@ export function createContextManager(options) {
43
65
  },
44
66
  /**
45
67
  * Run a callback within a tenant context.
68
+ *
69
+ * @throws {TenantUnavailableError} when the tenant is not active and
70
+ * `allowInactive` was not set.
46
71
  */
47
- run(tenant, callback) {
48
- const context = {
49
- mode: "tenant",
50
- tenant,
51
- context: {
52
- tenantId: tenant.id,
53
- source: "manual",
54
- trust: "trusted",
55
- resolvedAt: new Date(),
56
- metadata: {},
57
- },
58
- };
59
- return storage.run(context, callback);
60
- },
72
+ run,
61
73
  /**
62
74
  * Run a callback in system mode (no tenant).
63
75
  */
@@ -69,7 +81,7 @@ export function createContextManager(options) {
69
81
  * Run a callback with a specific tenant (for switching).
70
82
  */
71
83
  runAs(tenant, callback) {
72
- return this.run(tenant, callback);
84
+ return run(tenant, callback);
73
85
  },
74
86
  };
75
87
  }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Adapts an HTTP middleware context to the shape tenant resolvers expect.
3
+ *
4
+ * Resolvers take a narrow accessor object rather than a framework request, so
5
+ * they can be reused off the HTTP path. The middleware is what bridges the
6
+ * two; without this, passing the raw context type-checks (the resolver's
7
+ * context parameter defaults to `unknown`) and fails on the first request.
8
+ *
9
+ * @module http/httpResolverContext
10
+ */
11
+ import type { HttpMiddlewareContext } from "./httpTypes.js";
12
+ /** Claims extracted from a verified token. */
13
+ export interface TenantClaims {
14
+ readonly [key: string]: unknown;
15
+ }
16
+ /** Everything the resolvers shipped with this package read from a request. */
17
+ export interface HttpResolverContext {
18
+ getHeader(name: string): string | undefined;
19
+ getHost(): string | undefined;
20
+ getPath(): string | undefined;
21
+ getClaims(): TenantClaims | undefined;
22
+ }
23
+ /** State key under which upstream auth middleware publishes token claims. */
24
+ export declare const TENANT_CLAIMS_STATE_KEY = "tenancy:claims";
25
+ /**
26
+ * Build a resolver context from an HTTP middleware context.
27
+ *
28
+ * @param context - The framework request context.
29
+ * @param getClaims - Optional override for reading verified token claims.
30
+ * Defaults to reading `tenancy:claims` from middleware state, which is where
31
+ * an authentication middleware is expected to publish them.
32
+ * @returns An accessor object every shipped resolver understands.
33
+ */
34
+ export declare function createHttpResolverContext(context: HttpMiddlewareContext, getClaims?: (context: HttpMiddlewareContext) => TenantClaims | undefined): HttpResolverContext;
35
+ //# sourceMappingURL=httpResolverContext.d.ts.map
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Adapts an HTTP middleware context to the shape tenant resolvers expect.
3
+ *
4
+ * Resolvers take a narrow accessor object rather than a framework request, so
5
+ * they can be reused off the HTTP path. The middleware is what bridges the
6
+ * two; without this, passing the raw context type-checks (the resolver's
7
+ * context parameter defaults to `unknown`) and fails on the first request.
8
+ *
9
+ * @module http/httpResolverContext
10
+ */
11
+ /** State key under which upstream auth middleware publishes token claims. */
12
+ export const TENANT_CLAIMS_STATE_KEY = "tenancy:claims";
13
+ /**
14
+ * Build a resolver context from an HTTP middleware context.
15
+ *
16
+ * @param context - The framework request context.
17
+ * @param getClaims - Optional override for reading verified token claims.
18
+ * Defaults to reading `tenancy:claims` from middleware state, which is where
19
+ * an authentication middleware is expected to publish them.
20
+ * @returns An accessor object every shipped resolver understands.
21
+ */
22
+ export function createHttpResolverContext(context, getClaims) {
23
+ return {
24
+ getHeader(name) {
25
+ return context.request.headers.get(name.toLowerCase());
26
+ },
27
+ getHost() {
28
+ return context.request.headers.get("host");
29
+ },
30
+ getPath() {
31
+ return context.request.path;
32
+ },
33
+ getClaims() {
34
+ if (getClaims)
35
+ return getClaims(context);
36
+ return context.state.get(TENANT_CLAIMS_STATE_KEY);
37
+ },
38
+ };
39
+ }
40
+ //# sourceMappingURL=httpResolverContext.js.map
@@ -1,12 +1,12 @@
1
1
  /**
2
- * HTTP middleware adapter barrel.
3
- *
4
- * @module http
2
+ * @zudojs/tenancy — HTTP Barrel
5
3
  */
6
- export type { HttpMiddleware, HttpMiddlewareContext } from "./httpTypes.js";
7
- export type { ResolveTenantMiddlewareOptions, RequireTenantMiddlewareOptions, } from "./tenancyMiddleware.core.js";
8
- export { TENANT_STATE_KEY, TENANT_CONTEXT_STATE_KEY, createResolveTenantMiddleware, createRequireTenantMiddleware, } from "./tenancyMiddleware.core.js";
9
- export type { TenantGuardMiddlewareOptions } from "./tenancyMiddleware.guard.js";
4
+ export { TENANT_CONTEXT_STATE_KEY, TENANT_STATE_KEY, createRequireTenantMiddleware, createResolveTenantMiddleware, } from "./tenancyMiddleware.core.js";
5
+ export type { RequireTenantMiddlewareOptions, ResolveTenantMiddlewareOptions, } from "./tenancyMiddleware.core.js";
10
6
  export { createTenantGuardMiddleware, createTenantPropagationMiddleware, } from "./tenancyMiddleware.guard.js";
11
- export { createForbidden, createNotFound } from "./httpHelpers.js";
7
+ export type { TenantGuardMiddlewareOptions } from "./tenancyMiddleware.guard.js";
8
+ export { TENANT_CLAIMS_STATE_KEY, createHttpResolverContext, } from "./httpResolverContext.js";
9
+ export type { HttpResolverContext, TenantClaims, } from "./httpResolverContext.js";
10
+ export { createBadRequest, createForbidden, createJsonErrorResponse, createNotFound, createUnauthorized, } from "./httpHelpers.js";
11
+ export type * from "./httpTypes.js";
12
12
  //# sourceMappingURL=index.d.ts.map
@@ -1,9 +1,8 @@
1
1
  /**
2
- * HTTP middleware adapter barrel.
3
- *
4
- * @module http
2
+ * @zudojs/tenancy — HTTP Barrel
5
3
  */
6
- export { TENANT_STATE_KEY, TENANT_CONTEXT_STATE_KEY, createResolveTenantMiddleware, createRequireTenantMiddleware, } from "./tenancyMiddleware.core.js";
4
+ export { TENANT_CONTEXT_STATE_KEY, TENANT_STATE_KEY, createRequireTenantMiddleware, createResolveTenantMiddleware, } from "./tenancyMiddleware.core.js";
7
5
  export { createTenantGuardMiddleware, createTenantPropagationMiddleware, } from "./tenancyMiddleware.guard.js";
8
- export { createForbidden, createNotFound } from "./httpHelpers.js";
6
+ export { TENANT_CLAIMS_STATE_KEY, createHttpResolverContext, } from "./httpResolverContext.js";
7
+ export { createBadRequest, createForbidden, createJsonErrorResponse, createNotFound, createUnauthorized, } from "./httpHelpers.js";
9
8
  //# sourceMappingURL=index.js.map
@@ -7,11 +7,12 @@
7
7
  *
8
8
  * Requires @zudojs/http as a peer dependency.
9
9
  */
10
- import type { Tenant, TenantRequirement } from "../tenancyTypes/tenantInterface.js";
10
+ import type { Tenant, TenantRequirement, TenantTrustLevel } from "../tenancyTypes/tenantInterface.js";
11
11
  import type { TenantResolver, TenantResolution } from "../tenancyTypes/resolverTypes.js";
12
12
  import type { TenantRepository } from "../tenancyTypes/repositoryTypes.js";
13
13
  import type { TenantContextStorage } from "../context/contextStorage.core.js";
14
- import type { HttpMiddleware } from "./httpTypes.js";
14
+ import type { HttpMiddleware, HttpMiddlewareContext } from "./httpTypes.js";
15
+ import type { HttpResolverContext, TenantClaims } from "./httpResolverContext.js";
15
16
  /** State key for the resolved tenant. */
16
17
  export declare const TENANT_STATE_KEY = "tenancy:tenant";
17
18
  /** State key for the tenant context. */
@@ -19,11 +20,38 @@ export declare const TENANT_CONTEXT_STATE_KEY = "tenancy:context";
19
20
  /** Options for the resolve tenant middleware. */
20
21
  export interface ResolveTenantMiddlewareOptions {
21
22
  /** Resolver chain or single resolver to determine tenant. */
22
- readonly resolver: TenantResolver;
23
+ readonly resolver: TenantResolver<HttpResolverContext>;
23
24
  /** Repository to load the full tenant after resolution. */
24
25
  readonly repository: TenantRepository;
25
26
  /** Tenant context storage for propagation. */
26
27
  readonly storage: TenantContextStorage;
28
+ /**
29
+ * Minimum trust the resolution must carry. Defaults to `untrusted`.
30
+ *
31
+ * Set this on any route where a tenant resolved from a URL path or an
32
+ * unverified header must not be honoured.
33
+ */
34
+ readonly minimumTrust?: TenantTrustLevel;
35
+ /**
36
+ * Whether a non-active tenant may proceed. Defaults to false.
37
+ *
38
+ * Enable only for routes that exist to serve suspended tenants, such as
39
+ * billing or reactivation.
40
+ */
41
+ readonly allowInactive?: boolean;
42
+ /**
43
+ * Let a request that resolves to no tenant at all continue without one.
44
+ * Defaults to false, which answers 404.
45
+ *
46
+ * This is what makes `createRequireTenantMiddleware({ requirement:
47
+ * "optional" })` reachable: without it the resolve middleware refuses every
48
+ * tenant-less request before the requirement is consulted. A resolution
49
+ * that *was* produced is still checked in full — an unknown, untrusted or
50
+ * suspended tenant is refused whether or not this is set.
51
+ */
52
+ readonly optional?: boolean;
53
+ /** Reads verified token claims for the JWT resolver. */
54
+ readonly getClaims?: (context: HttpMiddlewareContext) => TenantClaims | undefined;
27
55
  /** Custom error response for missing tenant. */
28
56
  readonly notFoundResponse?: (resolution: TenantResolution | undefined) => unknown;
29
57
  }
@@ -37,6 +65,9 @@ export interface RequireTenantMiddlewareOptions {
37
65
  /**
38
66
  * Create middleware that resolves the tenant from the request
39
67
  * and creates a tenant context.
68
+ *
69
+ * Enforces trust and tenant status itself rather than relying on a second
70
+ * middleware being installed: the safe behaviour has to be the default.
40
71
  */
41
72
  export declare function createResolveTenantMiddleware(options: ResolveTenantMiddlewareOptions): HttpMiddleware;
42
73
  /**
@@ -7,7 +7,10 @@
7
7
  *
8
8
  * Requires @zudojs/http as a peer dependency.
9
9
  */
10
- import { createForbidden } from "./httpHelpers.js";
10
+ import { createHttpResolverContext } from "./httpResolverContext.js";
11
+ import { createBadRequest, createForbidden, createNotFound, createUnauthorized, } from "./httpHelpers.js";
12
+ import { meetsTrustLevel } from "../security/guard.core.js";
13
+ import { TenantResolutionConflictError, TenantResolutionError, } from "../tenancyErrors/tenancyError.types.js";
11
14
  // ─── State Keys ───────────────────────────────────────────────────────────
12
15
  /** State key for the resolved tenant. */
13
16
  export const TENANT_STATE_KEY = "tenancy:tenant";
@@ -17,30 +20,56 @@ export const TENANT_CONTEXT_STATE_KEY = "tenancy:context";
17
20
  /**
18
21
  * Create middleware that resolves the tenant from the request
19
22
  * and creates a tenant context.
23
+ *
24
+ * Enforces trust and tenant status itself rather than relying on a second
25
+ * middleware being installed: the safe behaviour has to be the default.
20
26
  */
21
27
  export function createResolveTenantMiddleware(options) {
28
+ const minimumTrust = options.minimumTrust ?? "untrusted";
22
29
  return async (context, next) => {
23
- const resolution = await options.resolver.resolve(context);
30
+ let resolution;
31
+ try {
32
+ resolution = await options.resolver.resolve(createHttpResolverContext(context, options.getClaims));
33
+ }
34
+ catch (error) {
35
+ // A resolver chain throws when a credential was rejected or when two
36
+ // sources name different tenants. Letting that escape produced a 500
37
+ // carrying the framework's own error text; both cases are a refusal
38
+ // the caller caused, and neither may fall through to `next()`.
39
+ if (error instanceof TenantResolutionConflictError) {
40
+ return createForbidden("Tenant could not be established: request sources name different tenants");
41
+ }
42
+ if (error instanceof TenantResolutionError) {
43
+ return createBadRequest("Tenant could not be resolved for this request");
44
+ }
45
+ throw error;
46
+ }
24
47
  if (!resolution) {
25
- const body = options.notFoundResponse?.(resolution) ?? {
26
- error: "Tenant not found",
27
- };
28
- return {
29
- status: 404,
30
- body,
31
- headers: { "content-type": "application/json" },
32
- };
48
+ if (options.optional)
49
+ return next();
50
+ return options.notFoundResponse
51
+ ? {
52
+ status: 404,
53
+ body: options.notFoundResponse(resolution),
54
+ headers: { "content-type": "application/json" },
55
+ }
56
+ : createNotFound("Tenant not found");
57
+ }
58
+ if (!meetsTrustLevel(resolution.trust, minimumTrust)) {
59
+ return createForbidden("Tenant could not be established for this route");
33
60
  }
34
61
  const tenant = await options.repository.findById(resolution.tenantId);
35
62
  if (!tenant) {
36
- const body = options.notFoundResponse?.(resolution) ?? {
37
- error: `Tenant "${resolution.tenantId}" not found`,
38
- };
39
- return {
40
- status: 404,
41
- body,
42
- headers: { "content-type": "application/json" },
43
- };
63
+ return options.notFoundResponse
64
+ ? {
65
+ status: 404,
66
+ body: options.notFoundResponse(resolution),
67
+ headers: { "content-type": "application/json" },
68
+ }
69
+ : createNotFound("Tenant not found");
70
+ }
71
+ if (!options.allowInactive && tenant.status !== "active") {
72
+ return createForbidden("Tenant is not available");
44
73
  }
45
74
  const tenantContext = {
46
75
  tenantId: tenant.id,
@@ -51,11 +80,7 @@ export function createResolveTenantMiddleware(options) {
51
80
  };
52
81
  context.state.set(TENANT_STATE_KEY, tenant);
53
82
  context.state.set(TENANT_CONTEXT_STATE_KEY, tenantContext);
54
- return options.storage.run({
55
- mode: "tenant",
56
- tenant,
57
- context: tenantContext,
58
- }, () => next());
83
+ return options.storage.run({ mode: "tenant", tenant, context: tenantContext }, () => next());
59
84
  };
60
85
  }
61
86
  /**
@@ -66,26 +91,23 @@ export function createResolveTenantMiddleware(options) {
66
91
  export function createRequireTenantMiddleware(options) {
67
92
  const requirement = options?.requirement ?? "required";
68
93
  return async (context, next) => {
94
+ const tenant = context.state.get(TENANT_STATE_KEY);
69
95
  if (requirement === "forbidden") {
70
- const tenant = context.state.get(TENANT_STATE_KEY);
71
96
  if (tenant) {
72
97
  return createForbidden("Tenant context is not allowed for this route");
73
98
  }
74
99
  return next();
75
100
  }
76
- if (requirement === "optional") {
101
+ if (requirement === "optional")
77
102
  return next();
78
- }
79
- const tenant = context.state.get(TENANT_STATE_KEY);
80
103
  if (!tenant) {
81
- const body = options?.deniedResponse?.(undefined) ?? {
82
- error: "Tenant context is required",
83
- };
84
- return {
85
- status: 401,
86
- body,
87
- headers: { "content-type": "application/json" },
88
- };
104
+ return options?.deniedResponse
105
+ ? {
106
+ status: 401,
107
+ body: options.deniedResponse(undefined),
108
+ headers: { "content-type": "application/json" },
109
+ }
110
+ : createUnauthorized("Tenant context is required");
89
111
  }
90
112
  return next();
91
113
  };
@@ -8,17 +8,25 @@ import type { TenantContextStorage } from "../context/contextStorage.core.js";
8
8
  import type { HttpMiddleware } from "./httpTypes.js";
9
9
  /** Options for the tenant guard middleware. */
10
10
  export interface TenantGuardMiddlewareOptions {
11
- /** Tenant context storage. */
12
- readonly storage: TenantContextStorage;
13
- /** Repository to validate the tenant. */
14
- readonly repository: TenantRepository;
11
+ /**
12
+ * Repository consulted for the tenant's current status.
13
+ *
14
+ * Optional. Supplied, the guard re-reads the tenant rather than trusting
15
+ * the copy an earlier middleware placed in request state — which is what
16
+ * lets a tenant suspended mid-request be refused. Omitted, the guard checks
17
+ * the state copy only.
18
+ *
19
+ * The previous shape required this field and a `storage` field, and read
20
+ * neither.
21
+ */
22
+ readonly repository?: TenantRepository;
15
23
  }
16
24
  /**
17
25
  * Create middleware that validates tenant status.
18
26
  *
19
27
  * Ensures the resolved tenant is active before proceeding.
20
28
  */
21
- export declare function createTenantGuardMiddleware(options: TenantGuardMiddlewareOptions): HttpMiddleware;
29
+ export declare function createTenantGuardMiddleware(options?: TenantGuardMiddlewareOptions): HttpMiddleware;
22
30
  /**
23
31
  * Create middleware that propagates tenant context from state
24
32
  * into AsyncLocalStorage for downstream handlers.
@@ -11,13 +11,21 @@ import { TENANT_STATE_KEY, TENANT_CONTEXT_STATE_KEY, } from "./tenancyMiddleware
11
11
  *
12
12
  * Ensures the resolved tenant is active before proceeding.
13
13
  */
14
- export function createTenantGuardMiddleware(options) {
14
+ export function createTenantGuardMiddleware(options = {}) {
15
15
  return async (context, next) => {
16
16
  const tenant = context.state.get(TENANT_STATE_KEY);
17
17
  if (!tenant)
18
18
  return next();
19
- if (tenant.status !== "active") {
20
- return createForbidden(`Tenant "${tenant.id}" is not available (status: ${tenant.status})`);
19
+ // A tenant that disappeared between resolution and this check must be
20
+ // refused, not waved through on the stale state copy.
21
+ const current = options.repository
22
+ ? await options.repository.findById(tenant.id)
23
+ : tenant;
24
+ if (!current) {
25
+ return createForbidden(`Tenant "${tenant.id}" is no longer available`);
26
+ }
27
+ if (current.status !== "active") {
28
+ return createForbidden(`Tenant "${current.id}" is not available (status: ${current.status})`);
21
29
  }
22
30
  return next();
23
31
  };
@@ -4,5 +4,6 @@
4
4
  * @module repository
5
5
  */
6
6
  export { createMemoryTenantRepository, createDomainRegistry, } from "./repository.core.js";
7
- export { createTenantManager, type TenantManagerOptions, } from "./tenantManager.core.js";
7
+ export type { MemoryTenantRepository, TenantWithDomains, } from "./repository.core.js";
8
+ export { DEFAULT_TENANT_CACHE_TTL_MS, createTenantManager, type TenantManagerOptions, } from "./tenantManager.core.js";
8
9
  //# sourceMappingURL=index.d.ts.map
@@ -4,5 +4,5 @@
4
4
  * @module repository
5
5
  */
6
6
  export { createMemoryTenantRepository, createDomainRegistry, } from "./repository.core.js";
7
- export { createTenantManager, } from "./tenantManager.core.js";
7
+ export { DEFAULT_TENANT_CACHE_TTL_MS, createTenantManager, } from "./tenantManager.core.js";
8
8
  //# sourceMappingURL=index.js.map