@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.
- package/LICENSE +21 -0
- package/README.md +70 -13
- package/dist/context/contextManager.core.d.ts +12 -2
- package/dist/context/contextManager.core.js +34 -22
- package/dist/http/httpResolverContext.d.ts +35 -0
- package/dist/http/httpResolverContext.js +40 -0
- package/dist/http/index.d.ts +8 -8
- package/dist/http/index.js +4 -5
- package/dist/http/tenancyMiddleware.core.d.ts +34 -3
- package/dist/http/tenancyMiddleware.core.js +57 -35
- package/dist/http/tenancyMiddleware.guard.d.ts +13 -5
- package/dist/http/tenancyMiddleware.guard.js +11 -3
- package/dist/repository/index.d.ts +2 -1
- package/dist/repository/index.js +1 -1
- package/dist/repository/repository.core.d.ts +38 -7
- package/dist/repository/repository.core.js +69 -15
- package/dist/repository/tenantManager.core.d.ts +21 -3
- package/dist/repository/tenantManager.core.js +43 -14
- package/dist/resolver/index.d.ts +1 -0
- package/dist/resolver/resolverChain.core.d.ts +22 -7
- package/dist/resolver/resolverChain.core.js +45 -13
- package/dist/resolver/resolvers/headerResolver.core.d.ts +10 -2
- package/dist/resolver/resolvers/headerResolver.core.js +5 -8
- package/dist/resolver/resolvers/index.d.ts +9 -5
- package/dist/resolver/resolvers/index.js +5 -5
- package/dist/resolver/resolvers/jwtResolver.core.d.ts +1 -1
- package/dist/resolver/resolvers/jwtResolver.core.js +4 -8
- package/dist/resolver/resolvers/pathResolver.core.d.ts +7 -3
- package/dist/resolver/resolvers/pathResolver.core.js +12 -9
- package/dist/resolver/resolvers/subdomainResolver.core.d.ts +11 -2
- package/dist/resolver/resolvers/subdomainResolver.core.js +31 -14
- package/dist/security/guard.core.d.ts +19 -0
- package/dist/security/guard.core.js +58 -9
- package/dist/security/index.d.ts +1 -1
- package/dist/security/index.js +1 -1
- package/dist/tenancyErrors/index.d.ts +1 -1
- package/dist/tenancyErrors/index.js +1 -1
- package/dist/tenancyErrors/tenancyError.types.d.ts +11 -6
- package/dist/tenancyErrors/tenancyError.types.js +15 -12
- package/dist/tenancyTypes/index.d.ts +3 -3
- package/dist/tenancyTypes/index.js +2 -2
- package/dist/tenancyTypes/repositoryTypes.d.ts +6 -21
- package/dist/tenancyTypes/resolverTypes.d.ts +14 -2
- package/dist/tenancyTypes/tenantIdentity.d.ts +24 -5
- package/dist/tenancyTypes/tenantIdentity.js +54 -4
- package/package.json +26 -15
- package/dist/.tsbuildinfo +0 -1
- package/dist/context/contextManager.core.d.ts.map +0 -1
- package/dist/context/contextManager.core.js.map +0 -1
- package/dist/context/contextStorage.core.d.ts.map +0 -1
- package/dist/context/contextStorage.core.js.map +0 -1
- package/dist/context/index.d.ts.map +0 -1
- package/dist/context/index.js.map +0 -1
- package/dist/http/httpHelpers.d.ts.map +0 -1
- package/dist/http/httpHelpers.js.map +0 -1
- package/dist/http/httpTypes.d.ts.map +0 -1
- package/dist/http/httpTypes.js.map +0 -1
- package/dist/http/index.d.ts.map +0 -1
- package/dist/http/index.js.map +0 -1
- package/dist/http/tenancyMiddleware.core.d.ts.map +0 -1
- package/dist/http/tenancyMiddleware.core.js.map +0 -1
- package/dist/http/tenancyMiddleware.guard.d.ts.map +0 -1
- package/dist/http/tenancyMiddleware.guard.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/repository/index.d.ts.map +0 -1
- package/dist/repository/index.js.map +0 -1
- package/dist/repository/repository.core.d.ts.map +0 -1
- package/dist/repository/repository.core.js.map +0 -1
- package/dist/repository/tenantManager.core.d.ts.map +0 -1
- package/dist/repository/tenantManager.core.js.map +0 -1
- package/dist/resolver/index.d.ts.map +0 -1
- package/dist/resolver/index.js.map +0 -1
- package/dist/resolver/resolverChain.core.d.ts.map +0 -1
- package/dist/resolver/resolverChain.core.js.map +0 -1
- package/dist/resolver/resolvers/headerResolver.core.d.ts.map +0 -1
- package/dist/resolver/resolvers/headerResolver.core.js.map +0 -1
- package/dist/resolver/resolvers/index.d.ts.map +0 -1
- package/dist/resolver/resolvers/index.js.map +0 -1
- package/dist/resolver/resolvers/jwtResolver.core.d.ts.map +0 -1
- package/dist/resolver/resolvers/jwtResolver.core.js.map +0 -1
- package/dist/resolver/resolvers/pathResolver.core.d.ts.map +0 -1
- package/dist/resolver/resolvers/pathResolver.core.js.map +0 -1
- package/dist/resolver/resolvers/subdomainResolver.core.d.ts.map +0 -1
- package/dist/resolver/resolvers/subdomainResolver.core.js.map +0 -1
- package/dist/security/guard.core.d.ts.map +0 -1
- package/dist/security/guard.core.js.map +0 -1
- package/dist/security/index.d.ts.map +0 -1
- package/dist/security/index.js.map +0 -1
- package/dist/tenancyErrors/index.d.ts.map +0 -1
- package/dist/tenancyErrors/index.js.map +0 -1
- package/dist/tenancyErrors/tenancyError.base.d.ts.map +0 -1
- package/dist/tenancyErrors/tenancyError.base.js.map +0 -1
- package/dist/tenancyErrors/tenancyError.types.d.ts.map +0 -1
- package/dist/tenancyErrors/tenancyError.types.js.map +0 -1
- package/dist/tenancyTypes/index.d.ts.map +0 -1
- package/dist/tenancyTypes/index.js.map +0 -1
- package/dist/tenancyTypes/repositoryTypes.d.ts.map +0 -1
- package/dist/tenancyTypes/repositoryTypes.js.map +0 -1
- package/dist/tenancyTypes/resolverTypes.d.ts.map +0 -1
- package/dist/tenancyTypes/resolverTypes.js.map +0 -1
- package/dist/tenancyTypes/tenancyOptions.d.ts +0 -37
- package/dist/tenancyTypes/tenancyOptions.d.ts.map +0 -1
- package/dist/tenancyTypes/tenancyOptions.js +0 -7
- package/dist/tenancyTypes/tenancyOptions.js.map +0 -1
- package/dist/tenancyTypes/tenantIdentity.d.ts.map +0 -1
- package/dist/tenancyTypes/tenantIdentity.js.map +0 -1
- package/dist/tenancyTypes/tenantInterface.d.ts.map +0 -1
- package/dist/tenancyTypes/tenantInterface.js.map +0 -1
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js.map +0 -1
- package/dist/utils/utils.helper.d.ts.map +0 -1
- 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
|
|
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 {
|
|
14
|
+
import {
|
|
15
|
+
createJwtResolver,
|
|
16
|
+
createMemoryTenantRepository,
|
|
17
|
+
createResolveTenantMiddleware,
|
|
18
|
+
createResolverChain,
|
|
19
|
+
createSubdomainResolver,
|
|
20
|
+
createTenantContextStorage,
|
|
21
|
+
} from "@zudojs/tenancy";
|
|
15
22
|
|
|
16
|
-
const
|
|
17
|
-
|
|
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
|
|
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
|
|
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
|
|
28
|
-
-
|
|
29
|
-
|
|
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-
|
|
34
|
-
-
|
|
35
|
-
- Tenant-
|
|
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()
|
|
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)
|
|
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 =
|
|
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
|
|
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
|
|
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
|
package/dist/http/index.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* @module http
|
|
2
|
+
* @zudojs/tenancy — HTTP Barrel
|
|
5
3
|
*/
|
|
6
|
-
export
|
|
7
|
-
export type {
|
|
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 {
|
|
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
|
package/dist/http/index.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* @module http
|
|
2
|
+
* @zudojs/tenancy — HTTP Barrel
|
|
5
3
|
*/
|
|
6
|
-
export { TENANT_STATE_KEY,
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
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
|
-
|
|
20
|
-
|
|
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 {
|
|
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
|
package/dist/repository/index.js
CHANGED
|
@@ -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
|