@zudojs/tenancy 0.0.1 → 1.0.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 +64 -13
  3. package/dist/context/contextManager.core.d.ts +10 -0
  4. package/dist/context/contextManager.core.js +7 -0
  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 +23 -3
  10. package/dist/http/tenancyMiddleware.core.js +55 -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 +17 -1
  18. package/dist/repository/tenantManager.core.js +30 -5
  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 +1 -2
  29. package/dist/resolver/resolvers/pathResolver.core.js +5 -7
  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 +22 -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,77 @@ 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.
30
82
 
31
83
  ## Use Cases
32
84
 
33
- - SaaS multi-tenant applications
34
- - Data isolation per tenant
35
- - Tenant-aware routing
36
- - Compliance and data separation
85
+ - SaaS multi-tenancy
86
+ - Per-tenant data isolation
87
+ - 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.
@@ -32,6 +39,9 @@ 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
46
  run<T>(tenant: Tenant, callback: () => T): T;
37
47
  /**
@@ -4,11 +4,13 @@
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;
12
14
  return {
13
15
  /**
14
16
  * Get the current execution context.
@@ -43,8 +45,13 @@ export function createContextManager(options) {
43
45
  },
44
46
  /**
45
47
  * Run a callback within a tenant context.
48
+ *
49
+ * @throws {TenantUnavailableError} when the tenant is not active and
50
+ * `allowInactive` was not set.
46
51
  */
47
52
  run(tenant, callback) {
53
+ if (!allowInactive)
54
+ assertTenantUsable(tenant);
48
55
  const context = {
49
56
  mode: "tenant",
50
57
  tenant,
@@ -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,27 @@ 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
+ /** Reads verified token claims for the JWT resolver. */
43
+ readonly getClaims?: (context: HttpMiddlewareContext) => TenantClaims | undefined;
27
44
  /** Custom error response for missing tenant. */
28
45
  readonly notFoundResponse?: (resolution: TenantResolution | undefined) => unknown;
29
46
  }
@@ -37,6 +54,9 @@ export interface RequireTenantMiddlewareOptions {
37
54
  /**
38
55
  * Create middleware that resolves the tenant from the request
39
56
  * and creates a tenant context.
57
+ *
58
+ * Enforces trust and tenant status itself rather than relying on a second
59
+ * middleware being installed: the safe behaviour has to be the default.
40
60
  */
41
61
  export declare function createResolveTenantMiddleware(options: ResolveTenantMiddlewareOptions): HttpMiddleware;
42
62
  /**
@@ -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,54 @@ 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
+ return options.notFoundResponse
49
+ ? {
50
+ status: 404,
51
+ body: options.notFoundResponse(resolution),
52
+ headers: { "content-type": "application/json" },
53
+ }
54
+ : createNotFound("Tenant not found");
55
+ }
56
+ if (!meetsTrustLevel(resolution.trust, minimumTrust)) {
57
+ return createForbidden("Tenant could not be established for this route");
33
58
  }
34
59
  const tenant = await options.repository.findById(resolution.tenantId);
35
60
  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
- };
61
+ return options.notFoundResponse
62
+ ? {
63
+ status: 404,
64
+ body: options.notFoundResponse(resolution),
65
+ headers: { "content-type": "application/json" },
66
+ }
67
+ : createNotFound("Tenant not found");
68
+ }
69
+ if (!options.allowInactive && tenant.status !== "active") {
70
+ return createForbidden("Tenant is not available");
44
71
  }
45
72
  const tenantContext = {
46
73
  tenantId: tenant.id,
@@ -51,11 +78,7 @@ export function createResolveTenantMiddleware(options) {
51
78
  };
52
79
  context.state.set(TENANT_STATE_KEY, tenant);
53
80
  context.state.set(TENANT_CONTEXT_STATE_KEY, tenantContext);
54
- return options.storage.run({
55
- mode: "tenant",
56
- tenant,
57
- context: tenantContext,
58
- }, () => next());
81
+ return options.storage.run({ mode: "tenant", tenant, context: tenantContext }, () => next());
59
82
  };
60
83
  }
61
84
  /**
@@ -66,26 +89,23 @@ export function createResolveTenantMiddleware(options) {
66
89
  export function createRequireTenantMiddleware(options) {
67
90
  const requirement = options?.requirement ?? "required";
68
91
  return async (context, next) => {
92
+ const tenant = context.state.get(TENANT_STATE_KEY);
69
93
  if (requirement === "forbidden") {
70
- const tenant = context.state.get(TENANT_STATE_KEY);
71
94
  if (tenant) {
72
95
  return createForbidden("Tenant context is not allowed for this route");
73
96
  }
74
97
  return next();
75
98
  }
76
- if (requirement === "optional") {
99
+ if (requirement === "optional")
77
100
  return next();
78
- }
79
- const tenant = context.state.get(TENANT_STATE_KEY);
80
101
  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
- };
102
+ return options?.deniedResponse
103
+ ? {
104
+ status: 401,
105
+ body: options.deniedResponse(undefined),
106
+ headers: { "content-type": "application/json" },
107
+ }
108
+ : createUnauthorized("Tenant context is required");
89
109
  }
90
110
  return next();
91
111
  };
@@ -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
@@ -5,20 +5,51 @@
5
5
  */
6
6
  import type { TenantId } from "../tenancyTypes/tenantIdentity.js";
7
7
  import type { Tenant } from "../tenancyTypes/tenantInterface.js";
8
- import type { TenantRepository } from "../tenancyTypes/repositoryTypes.js";
9
- import type { TenantDomain } from "../tenancyTypes/tenancyOptions.js";
8
+ import type { TenantDomain, TenantRepository } from "../tenancyTypes/repositoryTypes.js";
9
+ /** A tenant plus the custom domains that resolve to it. */
10
+ export interface TenantWithDomains {
11
+ readonly tenant: Tenant;
12
+ readonly domains?: readonly string[];
13
+ }
14
+ /** The in-memory repository plus its write surface. */
15
+ export interface MemoryTenantRepository extends TenantRepository {
16
+ /** Look a tenant up by its slug. Always present on this implementation. */
17
+ findBySlug(slug: string): Promise<Tenant | undefined>;
18
+ /** Look a tenant up by a registered custom domain. */
19
+ findByDomain(domain: string): Promise<Tenant | undefined>;
20
+ /**
21
+ * Insert or replace a tenant, re-indexing its slug and domains.
22
+ *
23
+ * @throws {TenantAlreadyExistsError} when the slug or one of the domains is
24
+ * already indexed to a different tenant.
25
+ */
26
+ add(tenant: Tenant, domains?: readonly string[]): void;
27
+ /** Remove a tenant and every index entry pointing at it. */
28
+ remove(id: TenantId): void;
29
+ /** Every tenant currently stored. */
30
+ all(): readonly Tenant[];
31
+ /** Domains mapped to the given tenant. */
32
+ domainsOf(id: TenantId): readonly string[];
33
+ }
10
34
  /**
11
35
  * Create an in-memory tenant repository.
36
+ *
37
+ * Secondary indexes are rebuilt on every write. A tenant whose slug or domain
38
+ * changed must not stay reachable under its previous one: the stale entry
39
+ * would keep serving the pre-update record, including its pre-suspension
40
+ * status.
12
41
  */
13
- export declare function createMemoryTenantRepository(): TenantRepository & {
14
- add(tenant: Tenant): void;
15
- remove(id: TenantId): void;
16
- all(): readonly Tenant[];
17
- };
42
+ export declare function createMemoryTenantRepository(): MemoryTenantRepository;
18
43
  /**
19
44
  * Create a domain-to-tenant registry.
20
45
  */
21
46
  export declare function createDomainRegistry(): {
47
+ /**
48
+ * Map a domain to a tenant.
49
+ *
50
+ * @throws {TenantAlreadyExistsError} when the domain already resolves to
51
+ * a different tenant. Re-registering the same pair is a no-op.
52
+ */
22
53
  register(domain: string, tenantId: TenantId): void;
23
54
  unregister(domain: string): void;
24
55
  resolve(domain: string): TenantId | undefined;