@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
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module resolvers/pathResolver
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { tryCreateTenantId } from "../../tenancyTypes/tenantIdentity.js";
|
|
7
7
|
/**
|
|
8
8
|
* Create a tenant resolver that extracts tenant from URL path.
|
|
9
9
|
*
|
|
@@ -21,20 +21,23 @@ 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;
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
trust: "untrusted",
|
|
37
|
-
};
|
|
37
|
+
const tenantId = tryCreateTenantId(segments[0]);
|
|
38
|
+
if (!tenantId)
|
|
39
|
+
return undefined;
|
|
40
|
+
return { tenantId, source: "path", trust: "untrusted" };
|
|
38
41
|
},
|
|
39
42
|
};
|
|
40
43
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { TenantResolver } from "../../tenancyTypes/resolverTypes.js";
|
|
7
7
|
/** Context type with a getHost method. */
|
|
8
|
-
interface SubdomainContext {
|
|
8
|
+
export interface SubdomainContext {
|
|
9
9
|
getHost(): string | undefined;
|
|
10
10
|
}
|
|
11
11
|
/** Options for the subdomain resolver. */
|
|
@@ -13,6 +13,16 @@ export interface SubdomainResolverOptions {
|
|
|
13
13
|
/** Base domain to strip (e.g. "example.com"). If not set, uses first segment. */
|
|
14
14
|
readonly baseDomain?: string;
|
|
15
15
|
readonly priority?: number;
|
|
16
|
+
/** Subdomains that never name a tenant. Defaults to `www`. */
|
|
17
|
+
readonly reserved?: readonly string[];
|
|
18
|
+
/**
|
|
19
|
+
* Whether a multi-label subdomain may name a tenant.
|
|
20
|
+
*
|
|
21
|
+
* Off by default: with a base domain of `example.com`, `a.b.example.com`
|
|
22
|
+
* would otherwise resolve to the tenant `a.b`, silently creating a second
|
|
23
|
+
* identity for a host nobody provisioned.
|
|
24
|
+
*/
|
|
25
|
+
readonly allowMultiLabel?: boolean;
|
|
16
26
|
}
|
|
17
27
|
/**
|
|
18
28
|
* Create a tenant resolver that extracts tenant from subdomain.
|
|
@@ -20,5 +30,4 @@ export interface SubdomainResolverOptions {
|
|
|
20
30
|
* Example: "acme.example.com" → tenant "acme"
|
|
21
31
|
*/
|
|
22
32
|
export declare function createSubdomainResolver(options?: SubdomainResolverOptions): TenantResolver<SubdomainContext>;
|
|
23
|
-
export {};
|
|
24
33
|
//# sourceMappingURL=subdomainResolver.core.d.ts.map
|
|
@@ -3,7 +3,21 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module resolvers/subdomainResolver
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { tryCreateTenantId } from "../../tenancyTypes/tenantIdentity.js";
|
|
7
|
+
/**
|
|
8
|
+
* Strip the port from an authority, handling bracketed IPv6 literals.
|
|
9
|
+
*/
|
|
10
|
+
function hostnameOf(host) {
|
|
11
|
+
const trimmed = host.trim();
|
|
12
|
+
if (trimmed.length === 0)
|
|
13
|
+
return undefined;
|
|
14
|
+
if (trimmed.startsWith("[")) {
|
|
15
|
+
const close = trimmed.indexOf("]");
|
|
16
|
+
return close === -1 ? undefined : trimmed.slice(0, close + 1);
|
|
17
|
+
}
|
|
18
|
+
const [name] = trimmed.split(":");
|
|
19
|
+
return name && name.length > 0 ? name : undefined;
|
|
20
|
+
}
|
|
7
21
|
/**
|
|
8
22
|
* Create a tenant resolver that extracts tenant from subdomain.
|
|
9
23
|
*
|
|
@@ -11,6 +25,7 @@ import { createTenantId } from "../../tenancyTypes/tenantIdentity.js";
|
|
|
11
25
|
*/
|
|
12
26
|
export function createSubdomainResolver(options) {
|
|
13
27
|
const priority = options?.priority ?? 70;
|
|
28
|
+
const reserved = new Set((options?.reserved ?? ["www"]).map((entry) => entry.toLowerCase()));
|
|
14
29
|
return {
|
|
15
30
|
name: "subdomain",
|
|
16
31
|
priority,
|
|
@@ -18,30 +33,32 @@ export function createSubdomainResolver(options) {
|
|
|
18
33
|
const host = context.getHost();
|
|
19
34
|
if (!host)
|
|
20
35
|
return undefined;
|
|
21
|
-
|
|
22
|
-
const hostname = host.split(":")[0];
|
|
36
|
+
const hostname = hostnameOf(host)?.toLowerCase();
|
|
23
37
|
if (!hostname)
|
|
24
38
|
return undefined;
|
|
25
39
|
let subdomain;
|
|
26
40
|
if (options?.baseDomain) {
|
|
27
|
-
|
|
28
|
-
|
|
41
|
+
const base = options.baseDomain.toLowerCase();
|
|
42
|
+
if (hostname.endsWith(`.${base}`)) {
|
|
43
|
+
subdomain = hostname.slice(0, -(base.length + 1));
|
|
29
44
|
}
|
|
30
45
|
}
|
|
31
46
|
else {
|
|
32
|
-
// Use first segment
|
|
33
47
|
const parts = hostname.split(".");
|
|
34
|
-
if (parts.length > 2)
|
|
48
|
+
if (parts.length > 2)
|
|
35
49
|
subdomain = parts[0];
|
|
36
|
-
}
|
|
37
50
|
}
|
|
38
|
-
if (!subdomain
|
|
51
|
+
if (!subdomain)
|
|
52
|
+
return undefined;
|
|
53
|
+
if (reserved.has(subdomain))
|
|
54
|
+
return undefined;
|
|
55
|
+
if (!options?.allowMultiLabel && subdomain.includes(".")) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
const tenantId = tryCreateTenantId(subdomain);
|
|
59
|
+
if (!tenantId)
|
|
39
60
|
return undefined;
|
|
40
|
-
return {
|
|
41
|
-
tenantId: createTenantId(subdomain),
|
|
42
|
-
source: "subdomain",
|
|
43
|
-
trust: "verified",
|
|
44
|
-
};
|
|
61
|
+
return { tenantId, source: "subdomain", trust: "verified" };
|
|
45
62
|
},
|
|
46
63
|
};
|
|
47
64
|
}
|
|
@@ -10,6 +10,12 @@ import type { TenantResolutionSource, TenantTrustLevel } from "../tenancyTypes/t
|
|
|
10
10
|
* Get the default trust level for a resolution source.
|
|
11
11
|
*/
|
|
12
12
|
export declare function getDefaultTrust(source: TenantResolutionSource): TenantTrustLevel;
|
|
13
|
+
/**
|
|
14
|
+
* Compare two trust levels.
|
|
15
|
+
*
|
|
16
|
+
* @returns True when `actual` is at least as trusted as `required`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function meetsTrustLevel(actual: TenantTrustLevel, required: TenantTrustLevel): boolean;
|
|
13
19
|
/**
|
|
14
20
|
* Validate that a tenant is usable.
|
|
15
21
|
*/
|
|
@@ -20,10 +26,23 @@ export declare function assertTenantUsable(tenant: Tenant): void;
|
|
|
20
26
|
export declare function assertTenantOwnership(resource: TenantResource, tenantId: TenantId): void;
|
|
21
27
|
/**
|
|
22
28
|
* Validate that a trust level is sufficient.
|
|
29
|
+
*
|
|
30
|
+
* @throws {TenantTrustLevelError} when the resolution is not trusted enough.
|
|
23
31
|
*/
|
|
24
32
|
export declare function assertTrustLevel(actual: TenantTrustLevel, required: TenantTrustLevel, source: TenantResolutionSource): void;
|
|
33
|
+
/**
|
|
34
|
+
* Assert that a caller may act on a tenant other than its own.
|
|
35
|
+
*
|
|
36
|
+
* @throws {TenantAccessDeniedError} when the two tenants differ.
|
|
37
|
+
*/
|
|
38
|
+
export declare function assertSameTenant(actual: TenantId, expected: TenantId): void;
|
|
25
39
|
/**
|
|
26
40
|
* Create a tenant key for scoped resources.
|
|
41
|
+
*
|
|
42
|
+
* @param tenantId - The owning tenant.
|
|
43
|
+
* @param key - The resource key within that tenant.
|
|
44
|
+
* @param separator - Segment separator. Defaults to ":".
|
|
45
|
+
* @returns A key that no other tenant's key can collide with.
|
|
27
46
|
*/
|
|
28
47
|
export declare function tenantKey(tenantId: TenantId, key: string, separator?: string): string;
|
|
29
48
|
/**
|
|
@@ -3,25 +3,46 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module security/guard
|
|
5
5
|
*/
|
|
6
|
-
import { TenantAccessDeniedError, TenantIsolationError, TenantUnavailableError, } from "../tenancyErrors/tenancyError.types.js";
|
|
7
|
-
/**
|
|
6
|
+
import { TenantAccessDeniedError, TenantIsolationError, TenantTrustLevelError, TenantUnavailableError, } from "../tenancyErrors/tenancyError.types.js";
|
|
7
|
+
/**
|
|
8
|
+
* Trust levels for resolution sources.
|
|
9
|
+
*
|
|
10
|
+
* `header` is untrusted: the value arrives on the wire under the client's
|
|
11
|
+
* control unless a trusted proxy overwrites it, and this package cannot know
|
|
12
|
+
* whether one does. Applications that can attest to their edge raise it
|
|
13
|
+
* explicitly via `createHeaderResolver({ trust: "verified" })`.
|
|
14
|
+
*/
|
|
8
15
|
const TRUST_MAP = {
|
|
9
16
|
jwt: "trusted",
|
|
10
17
|
"api-key": "trusted",
|
|
11
18
|
manual: "trusted",
|
|
12
19
|
system: "trusted",
|
|
13
|
-
header: "verified",
|
|
14
20
|
subdomain: "verified",
|
|
15
21
|
domain: "verified",
|
|
22
|
+
header: "untrusted",
|
|
16
23
|
path: "untrusted",
|
|
17
24
|
custom: "untrusted",
|
|
18
25
|
};
|
|
26
|
+
/** Trust levels in ascending order of confidence. */
|
|
27
|
+
const TRUST_ORDER = [
|
|
28
|
+
"untrusted",
|
|
29
|
+
"verified",
|
|
30
|
+
"trusted",
|
|
31
|
+
];
|
|
19
32
|
/**
|
|
20
33
|
* Get the default trust level for a resolution source.
|
|
21
34
|
*/
|
|
22
35
|
export function getDefaultTrust(source) {
|
|
23
36
|
return TRUST_MAP[source] ?? "untrusted";
|
|
24
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Compare two trust levels.
|
|
40
|
+
*
|
|
41
|
+
* @returns True when `actual` is at least as trusted as `required`.
|
|
42
|
+
*/
|
|
43
|
+
export function meetsTrustLevel(actual, required) {
|
|
44
|
+
return TRUST_ORDER.indexOf(actual) >= TRUST_ORDER.indexOf(required);
|
|
45
|
+
}
|
|
25
46
|
/**
|
|
26
47
|
* Validate that a tenant is usable.
|
|
27
48
|
*/
|
|
@@ -40,20 +61,48 @@ export function assertTenantOwnership(resource, tenantId) {
|
|
|
40
61
|
}
|
|
41
62
|
/**
|
|
42
63
|
* Validate that a trust level is sufficient.
|
|
64
|
+
*
|
|
65
|
+
* @throws {TenantTrustLevelError} when the resolution is not trusted enough.
|
|
43
66
|
*/
|
|
44
67
|
export function assertTrustLevel(actual, required, source) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
68
|
+
if (!meetsTrustLevel(actual, required)) {
|
|
69
|
+
throw new TenantTrustLevelError(source, required, actual);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Assert that a caller may act on a tenant other than its own.
|
|
74
|
+
*
|
|
75
|
+
* @throws {TenantAccessDeniedError} when the two tenants differ.
|
|
76
|
+
*/
|
|
77
|
+
export function assertSameTenant(actual, expected) {
|
|
78
|
+
if (actual !== expected) {
|
|
79
|
+
throw new TenantAccessDeniedError(actual);
|
|
50
80
|
}
|
|
51
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Escape a key segment so it cannot forge a separator boundary.
|
|
84
|
+
*
|
|
85
|
+
* Tenant ids are already constrained by `createTenantId`, but arbitrary keys
|
|
86
|
+
* are not; without this, a key of `"cache:k"` under tenant `a` collides with
|
|
87
|
+
* key `"k"` under a tenant literally named `a:cache`.
|
|
88
|
+
*/
|
|
89
|
+
function escapeSegment(segment, separator) {
|
|
90
|
+
return segment.split(separator).join(`\\${separator}`);
|
|
91
|
+
}
|
|
52
92
|
/**
|
|
53
93
|
* Create a tenant key for scoped resources.
|
|
94
|
+
*
|
|
95
|
+
* @param tenantId - The owning tenant.
|
|
96
|
+
* @param key - The resource key within that tenant.
|
|
97
|
+
* @param separator - Segment separator. Defaults to ":".
|
|
98
|
+
* @returns A key that no other tenant's key can collide with.
|
|
54
99
|
*/
|
|
55
100
|
export function tenantKey(tenantId, key, separator = ":") {
|
|
56
|
-
return
|
|
101
|
+
return [
|
|
102
|
+
"tenant",
|
|
103
|
+
escapeSegment(tenantId, separator),
|
|
104
|
+
escapeSegment(key, separator),
|
|
105
|
+
].join(separator);
|
|
57
106
|
}
|
|
58
107
|
/**
|
|
59
108
|
* Create a tenant cache key.
|
package/dist/security/index.d.ts
CHANGED
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module security
|
|
5
5
|
*/
|
|
6
|
-
export { getDefaultTrust, assertTenantUsable, assertTenantOwnership, assertTrustLevel, tenantKey, createTenantCacheKey, } from "./guard.core.js";
|
|
6
|
+
export { getDefaultTrust, meetsTrustLevel, assertTenantUsable, assertTenantOwnership, assertTrustLevel, assertSameTenant, tenantKey, createTenantCacheKey, } from "./guard.core.js";
|
|
7
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/security/index.js
CHANGED
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module security
|
|
5
5
|
*/
|
|
6
|
-
export { getDefaultTrust, assertTenantUsable, assertTenantOwnership, assertTrustLevel, tenantKey, createTenantCacheKey, } from "./guard.core.js";
|
|
6
|
+
export { getDefaultTrust, meetsTrustLevel, assertTenantUsable, assertTenantOwnership, assertTrustLevel, assertSameTenant, tenantKey, createTenantCacheKey, } from "./guard.core.js";
|
|
7
7
|
//# sourceMappingURL=index.js.map
|
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
* @module tenancyErrors
|
|
5
5
|
*/
|
|
6
6
|
export { TenantError } from "./tenancyError.base.js";
|
|
7
|
-
export { InvalidTenantIdError, TenantNotFoundError, TenantContextMissingError, TenantResolutionError, TenantResolutionConflictError, TenantUnavailableError, TenantAccessDeniedError,
|
|
7
|
+
export { InvalidTenantIdError, TenantNotFoundError, TenantContextMissingError, TenantResolutionError, TenantResolutionConflictError, TenantUnavailableError, TenantAccessDeniedError, TenantTrustLevelError, TenantAlreadyExistsError, TenantIsolationError, } from "./tenancyError.types.js";
|
|
8
8
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
* @module tenancyErrors
|
|
5
5
|
*/
|
|
6
6
|
export { TenantError } from "./tenancyError.base.js";
|
|
7
|
-
export { InvalidTenantIdError, TenantNotFoundError, TenantContextMissingError, TenantResolutionError, TenantResolutionConflictError, TenantUnavailableError, TenantAccessDeniedError,
|
|
7
|
+
export { InvalidTenantIdError, TenantNotFoundError, TenantContextMissingError, TenantResolutionError, TenantResolutionConflictError, TenantUnavailableError, TenantAccessDeniedError, TenantTrustLevelError, TenantAlreadyExistsError, TenantIsolationError, } from "./tenancyError.types.js";
|
|
8
8
|
//# sourceMappingURL=index.js.map
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { TenantError } from "./tenancyError.base.js";
|
|
7
7
|
import type { TenantId, TenantStatus } from "../tenancyTypes/tenantIdentity.js";
|
|
8
|
+
import type { TenantResolutionSource, TenantTrustLevel } from "../tenancyTypes/tenantInterface.js";
|
|
8
9
|
/**
|
|
9
10
|
* The provided tenant ID is invalid.
|
|
10
11
|
*/
|
|
@@ -48,16 +49,20 @@ export declare class TenantAccessDeniedError extends TenantError {
|
|
|
48
49
|
constructor(tenantId: string);
|
|
49
50
|
}
|
|
50
51
|
/**
|
|
51
|
-
*
|
|
52
|
+
* The tenant was resolved from a source that is not trusted enough.
|
|
53
|
+
*
|
|
54
|
+
* Distinct from {@link TenantAccessDeniedError} so the resolution source and
|
|
55
|
+
* the two trust levels stay in structured metadata instead of being formatted
|
|
56
|
+
* into a field that is supposed to hold a tenant id.
|
|
52
57
|
*/
|
|
53
|
-
export declare class
|
|
54
|
-
constructor(
|
|
58
|
+
export declare class TenantTrustLevelError extends TenantError {
|
|
59
|
+
constructor(source: TenantResolutionSource, required: TenantTrustLevel, actual: TenantTrustLevel);
|
|
55
60
|
}
|
|
56
61
|
/**
|
|
57
|
-
* Tenant
|
|
62
|
+
* Tenant already exists.
|
|
58
63
|
*/
|
|
59
|
-
export declare class
|
|
60
|
-
constructor(
|
|
64
|
+
export declare class TenantAlreadyExistsError extends TenantError {
|
|
65
|
+
constructor(identifier: string);
|
|
61
66
|
}
|
|
62
67
|
/**
|
|
63
68
|
* Tenant isolation violation detected.
|
|
@@ -81,6 +81,21 @@ export class TenantAccessDeniedError extends TenantError {
|
|
|
81
81
|
});
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* The tenant was resolved from a source that is not trusted enough.
|
|
86
|
+
*
|
|
87
|
+
* Distinct from {@link TenantAccessDeniedError} so the resolution source and
|
|
88
|
+
* the two trust levels stay in structured metadata instead of being formatted
|
|
89
|
+
* into a field that is supposed to hold a tenant id.
|
|
90
|
+
*/
|
|
91
|
+
export class TenantTrustLevelError extends TenantError {
|
|
92
|
+
constructor(source, required, actual) {
|
|
93
|
+
super(`Insufficient trust level from "${source}": need ${required}, got ${actual}`, {
|
|
94
|
+
code: ErrorCode.ACCESS_DENIED,
|
|
95
|
+
metadata: { source, required, actual },
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
84
99
|
/**
|
|
85
100
|
* Tenant already exists.
|
|
86
101
|
*/
|
|
@@ -92,18 +107,6 @@ export class TenantAlreadyExistsError extends TenantError {
|
|
|
92
107
|
});
|
|
93
108
|
}
|
|
94
109
|
}
|
|
95
|
-
/**
|
|
96
|
-
* Tenant provisioning failed.
|
|
97
|
-
*/
|
|
98
|
-
export class TenantProvisioningError extends TenantError {
|
|
99
|
-
constructor(tenantId, cause) {
|
|
100
|
-
super(`Provisioning failed for tenant "${tenantId}"`, {
|
|
101
|
-
code: ErrorCode.OPERATION_FAILED,
|
|
102
|
-
cause,
|
|
103
|
-
metadata: { tenantId },
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
110
|
/**
|
|
108
111
|
* Tenant isolation violation detected.
|
|
109
112
|
*/
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module tenancyTypes
|
|
5
5
|
*/
|
|
6
|
-
export { type TenantId, createTenantId,
|
|
6
|
+
export { type TenantId, MAX_TENANT_ID_LENGTH, createTenantId, tryCreateTenantId, isValidTenantId, type TenantStatus, } from "./tenantIdentity.js";
|
|
7
7
|
export { type Tenant, type TenantResolutionSource, type TenantTrustLevel, type TenantContext, type SystemContext, type TenantExecutionContext, type ExecutionTenantContext, type TenantRequirement, type TenantResource, } from "./tenantInterface.js";
|
|
8
8
|
export { type TenantResolution, type TenantResolutionResult, type TenantResolver, type ResolverChainOptions, } from "./resolverTypes.js";
|
|
9
|
-
export { type TenantRepository, type TenantCache,
|
|
10
|
-
export { type
|
|
9
|
+
export { type TenantRepository, type TenantCache, } from "./repositoryTypes.js";
|
|
10
|
+
export { type TenantDomain } from "./repositoryTypes.js";
|
|
11
11
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module tenancyTypes
|
|
5
5
|
*/
|
|
6
|
-
export { createTenantId, } from "./tenantIdentity.js";
|
|
6
|
+
export { MAX_TENANT_ID_LENGTH, createTenantId, tryCreateTenantId, isValidTenantId, } from "./tenantIdentity.js";
|
|
7
7
|
export {} from "./tenantInterface.js";
|
|
8
8
|
export {} from "./resolverTypes.js";
|
|
9
9
|
export {} from "./repositoryTypes.js";
|
|
10
|
-
export {} from "./
|
|
10
|
+
export {} from "./repositoryTypes.js";
|
|
11
11
|
//# sourceMappingURL=index.js.map
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module tenancyTypes/repositoryTypes
|
|
5
5
|
*/
|
|
6
|
-
import type { TenantId
|
|
6
|
+
import type { TenantId } from "./tenantIdentity.js";
|
|
7
7
|
import type { Tenant } from "./tenantInterface.js";
|
|
8
8
|
/** Repository for loading tenants. */
|
|
9
9
|
export interface TenantRepository {
|
|
@@ -11,30 +11,15 @@ export interface TenantRepository {
|
|
|
11
11
|
findBySlug?(slug: string): Promise<Tenant | undefined>;
|
|
12
12
|
findByDomain?(domain: string): Promise<Tenant | undefined>;
|
|
13
13
|
}
|
|
14
|
+
/** A custom domain mapped to the tenant it serves. */
|
|
15
|
+
export interface TenantDomain {
|
|
16
|
+
readonly domain: string;
|
|
17
|
+
readonly tenantId: TenantId;
|
|
18
|
+
}
|
|
14
19
|
/** Cache for tenant data. */
|
|
15
20
|
export interface TenantCache {
|
|
16
21
|
get(id: TenantId): Promise<Tenant | undefined>;
|
|
17
22
|
set(tenant: Tenant): Promise<void>;
|
|
18
23
|
delete(id: TenantId): Promise<void>;
|
|
19
24
|
}
|
|
20
|
-
/** High-level tenant orchestration. */
|
|
21
|
-
export interface TenantManager {
|
|
22
|
-
resolve(context: unknown): Promise<Tenant | undefined>;
|
|
23
|
-
get(id: TenantId): Promise<Tenant | undefined>;
|
|
24
|
-
require(id: TenantId): Promise<Tenant>;
|
|
25
|
-
}
|
|
26
|
-
/** Handles tenant setup operations. */
|
|
27
|
-
export interface TenantProvisioner {
|
|
28
|
-
provision(tenant: Tenant): Promise<void>;
|
|
29
|
-
}
|
|
30
|
-
/** Isolation configuration for a tenant. */
|
|
31
|
-
export interface TenantIsolationConfig {
|
|
32
|
-
readonly strategy: TenantIsolationStrategy;
|
|
33
|
-
readonly identifier?: string;
|
|
34
|
-
}
|
|
35
|
-
/** Tenant configuration provider. */
|
|
36
|
-
export interface TenantConfigurationProvider {
|
|
37
|
-
get<T = unknown>(tenantId: TenantId, key: string): Promise<T | undefined>;
|
|
38
|
-
set<T = unknown>(tenantId: TenantId, key: string, value: T): Promise<void>;
|
|
39
|
-
}
|
|
40
25
|
//# sourceMappingURL=repositoryTypes.d.ts.map
|
|
@@ -26,9 +26,21 @@ export interface TenantResolver<Context = unknown> {
|
|
|
26
26
|
}
|
|
27
27
|
/** Options for resolver chain. */
|
|
28
28
|
export interface ResolverChainOptions {
|
|
29
|
-
/**
|
|
29
|
+
/** Name reported when the chain is used as a resolver. Defaults to "chain". */
|
|
30
|
+
readonly name?: string;
|
|
31
|
+
/** Priority when nested in another chain. Defaults to the highest member's. */
|
|
32
|
+
readonly priority?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Run every resolver and collect all candidates, rather than stopping at the
|
|
35
|
+
* first match. Required for conflict detection.
|
|
36
|
+
*/
|
|
30
37
|
readonly detectConflicts?: boolean;
|
|
31
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Throw when candidates name different tenants. Defaults to true.
|
|
40
|
+
*
|
|
41
|
+
* Disagreement between a verified credential and a client-supplied header is
|
|
42
|
+
* the signature of an attempted cross-tenant request.
|
|
43
|
+
*/
|
|
32
44
|
readonly throwOnConflict?: boolean;
|
|
33
45
|
}
|
|
34
46
|
//# sourceMappingURL=resolverTypes.d.ts.map
|
|
@@ -9,13 +9,32 @@ declare const TenantIdBrand: unique symbol;
|
|
|
9
9
|
export type TenantId = string & {
|
|
10
10
|
readonly [TenantIdBrand]: true;
|
|
11
11
|
};
|
|
12
|
-
/**
|
|
12
|
+
/** Maximum accepted tenant id length. */
|
|
13
|
+
export declare const MAX_TENANT_ID_LENGTH = 64;
|
|
14
|
+
/**
|
|
15
|
+
* Create a validated TenantId.
|
|
16
|
+
*
|
|
17
|
+
* Input is trimmed, Unicode-normalized and lowercased before validation, so
|
|
18
|
+
* two spellings of the same identifier cannot become two tenants.
|
|
19
|
+
*
|
|
20
|
+
* @param value - The candidate identifier.
|
|
21
|
+
* @returns The normalized, validated tenant id.
|
|
22
|
+
* @throws {InvalidTenantIdError} when the value is not a valid tenant id.
|
|
23
|
+
*/
|
|
13
24
|
export declare function createTenantId(value: string): TenantId;
|
|
25
|
+
/**
|
|
26
|
+
* Create a validated TenantId, or undefined when the value is unusable.
|
|
27
|
+
*
|
|
28
|
+
* Resolvers use this: an unparseable candidate means "this resolver found
|
|
29
|
+
* nothing", not "the request is malformed".
|
|
30
|
+
*
|
|
31
|
+
* @param value - The candidate identifier.
|
|
32
|
+
* @returns The tenant id, or undefined.
|
|
33
|
+
*/
|
|
34
|
+
export declare function tryCreateTenantId(value: unknown): TenantId | undefined;
|
|
35
|
+
/** Whether a value is a well-formed tenant id. */
|
|
36
|
+
export declare function isValidTenantId(value: unknown): value is TenantId;
|
|
14
37
|
/** Tenant lifecycle status. */
|
|
15
38
|
export type TenantStatus = "provisioning" | "active" | "inactive" | "suspended" | "deleting" | "deleted";
|
|
16
|
-
/** Database isolation strategy for tenant data. */
|
|
17
|
-
export type TenantIsolationStrategy = "shared" | "schema" | "database" | "hybrid";
|
|
18
|
-
/** Tenancy execution mode. */
|
|
19
|
-
export type TenancyMode = "tenant" | "system";
|
|
20
39
|
export {};
|
|
21
40
|
//# sourceMappingURL=tenantIdentity.d.ts.map
|
|
@@ -3,11 +3,61 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module tenancyTypes/tenantIdentity
|
|
5
5
|
*/
|
|
6
|
-
|
|
6
|
+
import { InvalidTenantIdError } from "../tenancyErrors/tenancyError.types.js";
|
|
7
|
+
/**
|
|
8
|
+
* Characters a tenant id may contain.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately narrow. A tenant id is concatenated into cache keys, log lines,
|
|
11
|
+
* schema names and file paths, so anything that could act as a separator or a
|
|
12
|
+
* path segment in one of those contexts is rejected here rather than escaped
|
|
13
|
+
* at every use site.
|
|
14
|
+
*/
|
|
15
|
+
const TENANT_ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/u;
|
|
16
|
+
/** Maximum accepted tenant id length. */
|
|
17
|
+
export const MAX_TENANT_ID_LENGTH = 64;
|
|
18
|
+
/**
|
|
19
|
+
* Create a validated TenantId.
|
|
20
|
+
*
|
|
21
|
+
* Input is trimmed, Unicode-normalized and lowercased before validation, so
|
|
22
|
+
* two spellings of the same identifier cannot become two tenants.
|
|
23
|
+
*
|
|
24
|
+
* @param value - The candidate identifier.
|
|
25
|
+
* @returns The normalized, validated tenant id.
|
|
26
|
+
* @throws {InvalidTenantIdError} when the value is not a valid tenant id.
|
|
27
|
+
*/
|
|
7
28
|
export function createTenantId(value) {
|
|
8
|
-
if (
|
|
9
|
-
throw new
|
|
29
|
+
if (typeof value !== "string")
|
|
30
|
+
throw new InvalidTenantIdError(String(value));
|
|
31
|
+
const normalized = value.normalize("NFKC").trim().toLowerCase();
|
|
32
|
+
if (normalized.length === 0 || normalized.length > MAX_TENANT_ID_LENGTH) {
|
|
33
|
+
throw new InvalidTenantIdError(value);
|
|
34
|
+
}
|
|
35
|
+
if (!TENANT_ID_PATTERN.test(normalized)) {
|
|
36
|
+
throw new InvalidTenantIdError(value);
|
|
10
37
|
}
|
|
11
|
-
return
|
|
38
|
+
return normalized;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Create a validated TenantId, or undefined when the value is unusable.
|
|
42
|
+
*
|
|
43
|
+
* Resolvers use this: an unparseable candidate means "this resolver found
|
|
44
|
+
* nothing", not "the request is malformed".
|
|
45
|
+
*
|
|
46
|
+
* @param value - The candidate identifier.
|
|
47
|
+
* @returns The tenant id, or undefined.
|
|
48
|
+
*/
|
|
49
|
+
export function tryCreateTenantId(value) {
|
|
50
|
+
if (typeof value !== "string")
|
|
51
|
+
return undefined;
|
|
52
|
+
try {
|
|
53
|
+
return createTenantId(value);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Whether a value is a well-formed tenant id. */
|
|
60
|
+
export function isValidTenantId(value) {
|
|
61
|
+
return tryCreateTenantId(value) !== undefined;
|
|
12
62
|
}
|
|
13
63
|
//# sourceMappingURL=tenantIdentity.js.map
|