@zudojs/tenancy 1.0.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/README.md +6 -0
- package/dist/context/contextManager.core.d.ts +2 -2
- package/dist/context/contextManager.core.js +29 -24
- package/dist/http/tenancyMiddleware.core.d.ts +11 -0
- package/dist/http/tenancyMiddleware.core.js +2 -0
- package/dist/repository/tenantManager.core.d.ts +4 -2
- package/dist/repository/tenantManager.core.js +17 -13
- package/dist/resolver/resolvers/pathResolver.core.d.ts +6 -1
- package/dist/resolver/resolvers/pathResolver.core.js +7 -2
- package/package.json +8 -4
package/README.md
CHANGED
|
@@ -79,6 +79,12 @@ strips and re-sets the header at the edge.
|
|
|
79
79
|
a schema name, or a path. `tenantKey` escapes its segments as well.
|
|
80
80
|
- Non-active tenants are refused during resolution. Pass `allowInactive` on
|
|
81
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`.
|
|
82
88
|
|
|
83
89
|
## Use Cases
|
|
84
90
|
|
|
@@ -28,7 +28,7 @@ export declare function createContextManager(options: ContextManagerOptions): {
|
|
|
28
28
|
/**
|
|
29
29
|
* Get the current tenant, if any.
|
|
30
30
|
*/
|
|
31
|
-
getCurrentTenant()
|
|
31
|
+
getCurrentTenant: () => Tenant | undefined;
|
|
32
32
|
/**
|
|
33
33
|
* Require a current tenant — throws if missing.
|
|
34
34
|
*/
|
|
@@ -43,7 +43,7 @@ export declare function createContextManager(options: ContextManagerOptions): {
|
|
|
43
43
|
* @throws {TenantUnavailableError} when the tenant is not active and
|
|
44
44
|
* `allowInactive` was not set.
|
|
45
45
|
*/
|
|
46
|
-
run<T>(tenant: Tenant, callback: () => T)
|
|
46
|
+
run: <T>(tenant: Tenant, callback: () => T) => T;
|
|
47
47
|
/**
|
|
48
48
|
* Run a callback in system mode (no tenant).
|
|
49
49
|
*/
|
|
@@ -11,6 +11,31 @@ import { assertTenantUsable } from "../security/guard.core.js";
|
|
|
11
11
|
export function createContextManager(options) {
|
|
12
12
|
const { storage } = options;
|
|
13
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
|
+
}
|
|
14
39
|
return {
|
|
15
40
|
/**
|
|
16
41
|
* Get the current execution context.
|
|
@@ -21,17 +46,12 @@ export function createContextManager(options) {
|
|
|
21
46
|
/**
|
|
22
47
|
* Get the current tenant, if any.
|
|
23
48
|
*/
|
|
24
|
-
getCurrentTenant
|
|
25
|
-
const ctx = storage.get();
|
|
26
|
-
if (ctx?.mode === "tenant")
|
|
27
|
-
return ctx.tenant;
|
|
28
|
-
return undefined;
|
|
29
|
-
},
|
|
49
|
+
getCurrentTenant,
|
|
30
50
|
/**
|
|
31
51
|
* Require a current tenant — throws if missing.
|
|
32
52
|
*/
|
|
33
53
|
requireCurrentTenant() {
|
|
34
|
-
const tenant =
|
|
54
|
+
const tenant = getCurrentTenant();
|
|
35
55
|
if (!tenant)
|
|
36
56
|
throw new TenantContextMissingError();
|
|
37
57
|
return tenant;
|
|
@@ -49,22 +69,7 @@ export function createContextManager(options) {
|
|
|
49
69
|
* @throws {TenantUnavailableError} when the tenant is not active and
|
|
50
70
|
* `allowInactive` was not set.
|
|
51
71
|
*/
|
|
52
|
-
run
|
|
53
|
-
if (!allowInactive)
|
|
54
|
-
assertTenantUsable(tenant);
|
|
55
|
-
const context = {
|
|
56
|
-
mode: "tenant",
|
|
57
|
-
tenant,
|
|
58
|
-
context: {
|
|
59
|
-
tenantId: tenant.id,
|
|
60
|
-
source: "manual",
|
|
61
|
-
trust: "trusted",
|
|
62
|
-
resolvedAt: new Date(),
|
|
63
|
-
metadata: {},
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
return storage.run(context, callback);
|
|
67
|
-
},
|
|
72
|
+
run,
|
|
68
73
|
/**
|
|
69
74
|
* Run a callback in system mode (no tenant).
|
|
70
75
|
*/
|
|
@@ -76,7 +81,7 @@ export function createContextManager(options) {
|
|
|
76
81
|
* Run a callback with a specific tenant (for switching).
|
|
77
82
|
*/
|
|
78
83
|
runAs(tenant, callback) {
|
|
79
|
-
return
|
|
84
|
+
return run(tenant, callback);
|
|
80
85
|
},
|
|
81
86
|
};
|
|
82
87
|
}
|
|
@@ -39,6 +39,17 @@ export interface ResolveTenantMiddlewareOptions {
|
|
|
39
39
|
* billing or reactivation.
|
|
40
40
|
*/
|
|
41
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;
|
|
42
53
|
/** Reads verified token claims for the JWT resolver. */
|
|
43
54
|
readonly getClaims?: (context: HttpMiddlewareContext) => TenantClaims | undefined;
|
|
44
55
|
/** Custom error response for missing tenant. */
|
|
@@ -38,15 +38,17 @@ export declare function createTenantManager(options: TenantManagerOptions): {
|
|
|
38
38
|
/**
|
|
39
39
|
* Require a tenant by ID — throws if not found.
|
|
40
40
|
*/
|
|
41
|
-
require(id: TenantId)
|
|
41
|
+
require: (id: TenantId) => Promise<Tenant>;
|
|
42
42
|
/**
|
|
43
43
|
* Require a tenant that is also in an active state.
|
|
44
|
+
*
|
|
45
|
+
* Does not read `this`, so it survives being detached from the manager.
|
|
44
46
|
*/
|
|
45
47
|
requireActive(id: TenantId): Promise<Tenant>;
|
|
46
48
|
/**
|
|
47
49
|
* Validate that a tenant is in an active state.
|
|
48
50
|
*/
|
|
49
|
-
assertActive(tenant: Tenant)
|
|
51
|
+
assertActive: (tenant: Tenant) => void;
|
|
50
52
|
/**
|
|
51
53
|
* Get the current tenant from context.
|
|
52
54
|
*/
|
|
@@ -36,6 +36,17 @@ export function createTenantManager(options) {
|
|
|
36
36
|
}
|
|
37
37
|
return tenant;
|
|
38
38
|
}
|
|
39
|
+
function assertActive(tenant) {
|
|
40
|
+
if (tenant.status !== "active") {
|
|
41
|
+
throw new TenantUnavailableError(tenant.id, tenant.status);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function require(id) {
|
|
45
|
+
const tenant = await loadTenant(id);
|
|
46
|
+
if (!tenant)
|
|
47
|
+
throw new TenantNotFoundError(id);
|
|
48
|
+
return tenant;
|
|
49
|
+
}
|
|
39
50
|
return {
|
|
40
51
|
/**
|
|
41
52
|
* Resolve tenant from a resolution.
|
|
@@ -52,28 +63,21 @@ export function createTenantManager(options) {
|
|
|
52
63
|
/**
|
|
53
64
|
* Require a tenant by ID — throws if not found.
|
|
54
65
|
*/
|
|
55
|
-
|
|
56
|
-
const tenant = await loadTenant(id);
|
|
57
|
-
if (!tenant)
|
|
58
|
-
throw new TenantNotFoundError(id);
|
|
59
|
-
return tenant;
|
|
60
|
-
},
|
|
66
|
+
require,
|
|
61
67
|
/**
|
|
62
68
|
* Require a tenant that is also in an active state.
|
|
69
|
+
*
|
|
70
|
+
* Does not read `this`, so it survives being detached from the manager.
|
|
63
71
|
*/
|
|
64
72
|
async requireActive(id) {
|
|
65
|
-
const tenant = await
|
|
66
|
-
|
|
73
|
+
const tenant = await require(id);
|
|
74
|
+
assertActive(tenant);
|
|
67
75
|
return tenant;
|
|
68
76
|
},
|
|
69
77
|
/**
|
|
70
78
|
* Validate that a tenant is in an active state.
|
|
71
79
|
*/
|
|
72
|
-
assertActive
|
|
73
|
-
if (tenant.status !== "active") {
|
|
74
|
-
throw new TenantUnavailableError(tenant.id, tenant.status);
|
|
75
|
-
}
|
|
76
|
-
},
|
|
80
|
+
assertActive,
|
|
77
81
|
/**
|
|
78
82
|
* Get the current tenant from context.
|
|
79
83
|
*/
|
|
@@ -10,7 +10,12 @@ export interface PathContext {
|
|
|
10
10
|
}
|
|
11
11
|
/** Options for the path resolver. */
|
|
12
12
|
export interface PathResolverOptions {
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Prefix to strip (e.g. "/tenant").
|
|
15
|
+
*
|
|
16
|
+
* When set, only paths under the prefix name a tenant; any other path
|
|
17
|
+
* resolves to nothing.
|
|
18
|
+
*/
|
|
14
19
|
readonly prefix?: string;
|
|
15
20
|
readonly priority?: number;
|
|
16
21
|
}
|
|
@@ -21,11 +21,16 @@ export function createPathResolver(options) {
|
|
|
21
21
|
return undefined;
|
|
22
22
|
let segments = path.split("/").filter(Boolean);
|
|
23
23
|
if (prefix) {
|
|
24
|
+
// A prefix scopes this resolver to the routes mounted under it. A
|
|
25
|
+
// path outside the prefix must not name a tenant at all: otherwise
|
|
26
|
+
// `/health` resolves tenant "health" and `/admin/...` resolves
|
|
27
|
+
// whatever tenant happens to be called "admin".
|
|
24
28
|
const prefixSegments = prefix.split("/").filter(Boolean);
|
|
25
|
-
if (segments.slice(0, prefixSegments.length).join("/")
|
|
29
|
+
if (segments.slice(0, prefixSegments.length).join("/") !==
|
|
26
30
|
prefixSegments.join("/")) {
|
|
27
|
-
|
|
31
|
+
return undefined;
|
|
28
32
|
}
|
|
33
|
+
segments = segments.slice(prefixSegments.length);
|
|
29
34
|
}
|
|
30
35
|
if (segments.length === 0)
|
|
31
36
|
return undefined;
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/tenancy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Multi-tenant context and isolation with tenant resolution, AsyncLocalStorage propagation, resolver chains, trust levels, and guard middleware.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Oluwayemi Oyinlola",
|
|
8
|
+
"url": "https://github.com/oyinlola-tech"
|
|
9
|
+
},
|
|
6
10
|
"type": "module",
|
|
7
11
|
"main": "./dist/index.js",
|
|
8
12
|
"module": "./dist/index.js",
|
|
@@ -21,11 +25,11 @@
|
|
|
21
25
|
"!dist/.tsbuildinfo"
|
|
22
26
|
],
|
|
23
27
|
"dependencies": {
|
|
24
|
-
"@zudojs/errors": "1.0.
|
|
25
|
-
"@zudojs/constants": "1.0.
|
|
28
|
+
"@zudojs/errors": "1.0.1",
|
|
29
|
+
"@zudojs/constants": "1.0.1"
|
|
26
30
|
},
|
|
27
31
|
"peerDependencies": {
|
|
28
|
-
"@zudojs/http": "1.
|
|
32
|
+
"@zudojs/http": "1.1.0"
|
|
29
33
|
},
|
|
30
34
|
"peerDependenciesMeta": {
|
|
31
35
|
"@zudojs/http": {
|