@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.
- package/LICENSE +21 -0
- package/README.md +64 -13
- package/dist/context/contextManager.core.d.ts +10 -0
- package/dist/context/contextManager.core.js +7 -0
- 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 +23 -3
- package/dist/http/tenancyMiddleware.core.js +55 -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 +17 -1
- package/dist/repository/tenantManager.core.js +30 -5
- 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 +1 -2
- package/dist/resolver/resolvers/pathResolver.core.js +5 -7
- 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 +22 -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,40 +3,83 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module repository/repository
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { TenantAlreadyExistsError } from "../tenancyErrors/tenancyError.types.js";
|
|
7
|
+
/** Normalizes a domain for case-insensitive lookup. */
|
|
8
|
+
function normalizeDomain(domain) {
|
|
9
|
+
return domain.trim().toLowerCase();
|
|
10
|
+
}
|
|
7
11
|
/**
|
|
8
12
|
* Create an in-memory tenant repository.
|
|
13
|
+
*
|
|
14
|
+
* Secondary indexes are rebuilt on every write. A tenant whose slug or domain
|
|
15
|
+
* changed must not stay reachable under its previous one: the stale entry
|
|
16
|
+
* would keep serving the pre-update record, including its pre-suspension
|
|
17
|
+
* status.
|
|
9
18
|
*/
|
|
10
19
|
export function createMemoryTenantRepository() {
|
|
11
20
|
const tenants = new Map();
|
|
12
21
|
const bySlug = new Map();
|
|
13
22
|
const byDomain = new Map();
|
|
23
|
+
function unindex(id) {
|
|
24
|
+
for (const [slug, owner] of bySlug) {
|
|
25
|
+
if (owner === id)
|
|
26
|
+
bySlug.delete(slug);
|
|
27
|
+
}
|
|
28
|
+
for (const [domain, owner] of byDomain) {
|
|
29
|
+
if (owner === id)
|
|
30
|
+
byDomain.delete(domain);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
14
33
|
return {
|
|
15
34
|
async findById(id) {
|
|
16
35
|
return tenants.get(id);
|
|
17
36
|
},
|
|
18
37
|
async findBySlug(slug) {
|
|
19
|
-
|
|
38
|
+
const id = bySlug.get(slug.trim().toLowerCase());
|
|
39
|
+
return id ? tenants.get(id) : undefined;
|
|
20
40
|
},
|
|
21
41
|
async findByDomain(domain) {
|
|
22
|
-
|
|
42
|
+
const id = byDomain.get(normalizeDomain(domain));
|
|
43
|
+
return id ? tenants.get(id) : undefined;
|
|
23
44
|
},
|
|
24
|
-
add(tenant) {
|
|
45
|
+
add(tenant, domains) {
|
|
46
|
+
// A slug or a domain already pointing at a different tenant must not be
|
|
47
|
+
// silently reassigned: `findByDomain` is a tenant resolution path, so a
|
|
48
|
+
// silent steal is a cross-tenant takeover, not a bookkeeping detail.
|
|
49
|
+
const slug = tenant.slug?.trim().toLowerCase();
|
|
50
|
+
if (slug) {
|
|
51
|
+
const owner = bySlug.get(slug);
|
|
52
|
+
if (owner !== undefined && owner !== tenant.id) {
|
|
53
|
+
throw new TenantAlreadyExistsError(`slug "${slug}" is already registered to tenant "${owner}"`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const normalizedDomains = (domains ?? []).map(normalizeDomain);
|
|
57
|
+
for (const domain of normalizedDomains) {
|
|
58
|
+
const owner = byDomain.get(domain);
|
|
59
|
+
if (owner !== undefined && owner !== tenant.id) {
|
|
60
|
+
throw new TenantAlreadyExistsError(`domain "${domain}" is already registered to tenant "${owner}"`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
unindex(tenant.id);
|
|
25
64
|
tenants.set(tenant.id, tenant);
|
|
26
|
-
if (
|
|
27
|
-
bySlug.set(
|
|
65
|
+
if (slug)
|
|
66
|
+
bySlug.set(slug, tenant.id);
|
|
67
|
+
for (const domain of normalizedDomains) {
|
|
68
|
+
byDomain.set(domain, tenant.id);
|
|
69
|
+
}
|
|
28
70
|
},
|
|
29
71
|
remove(id) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
tenants.delete(id);
|
|
33
|
-
if (tenant.slug)
|
|
34
|
-
bySlug.delete(tenant.slug);
|
|
35
|
-
}
|
|
72
|
+
unindex(id);
|
|
73
|
+
tenants.delete(id);
|
|
36
74
|
},
|
|
37
75
|
all() {
|
|
38
76
|
return Array.from(tenants.values());
|
|
39
77
|
},
|
|
78
|
+
domainsOf(id) {
|
|
79
|
+
return Array.from(byDomain.entries())
|
|
80
|
+
.filter(([, owner]) => owner === id)
|
|
81
|
+
.map(([domain]) => domain);
|
|
82
|
+
},
|
|
40
83
|
};
|
|
41
84
|
}
|
|
42
85
|
/**
|
|
@@ -45,14 +88,25 @@ export function createMemoryTenantRepository() {
|
|
|
45
88
|
export function createDomainRegistry() {
|
|
46
89
|
const domains = new Map();
|
|
47
90
|
return {
|
|
91
|
+
/**
|
|
92
|
+
* Map a domain to a tenant.
|
|
93
|
+
*
|
|
94
|
+
* @throws {TenantAlreadyExistsError} when the domain already resolves to
|
|
95
|
+
* a different tenant. Re-registering the same pair is a no-op.
|
|
96
|
+
*/
|
|
48
97
|
register(domain, tenantId) {
|
|
49
|
-
|
|
98
|
+
const normalized = normalizeDomain(domain);
|
|
99
|
+
const owner = domains.get(normalized);
|
|
100
|
+
if (owner !== undefined && owner !== tenantId) {
|
|
101
|
+
throw new TenantAlreadyExistsError(`domain "${normalized}" is already registered to tenant "${owner}"`);
|
|
102
|
+
}
|
|
103
|
+
domains.set(normalized, tenantId);
|
|
50
104
|
},
|
|
51
105
|
unregister(domain) {
|
|
52
|
-
domains.delete(domain);
|
|
106
|
+
domains.delete(normalizeDomain(domain));
|
|
53
107
|
},
|
|
54
108
|
resolve(domain) {
|
|
55
|
-
return domains.get(domain);
|
|
109
|
+
return domains.get(normalizeDomain(domain));
|
|
56
110
|
},
|
|
57
111
|
all() {
|
|
58
112
|
return Array.from(domains.entries()).map(([domain, tenantId]) => ({
|
|
@@ -6,13 +6,22 @@
|
|
|
6
6
|
import type { TenantId } from "../tenancyTypes/tenantIdentity.js";
|
|
7
7
|
import type { Tenant } from "../tenancyTypes/tenantInterface.js";
|
|
8
8
|
import type { TenantResolution } from "../tenancyTypes/resolverTypes.js";
|
|
9
|
-
import type {
|
|
9
|
+
import type { TenantCache, TenantRepository } from "../tenancyTypes/repositoryTypes.js";
|
|
10
10
|
import type { TenantContextStorage } from "../context/contextStorage.core.js";
|
|
11
|
+
/** Default lifetime of a cached tenant record, in milliseconds. */
|
|
12
|
+
export declare const DEFAULT_TENANT_CACHE_TTL_MS = 30000;
|
|
11
13
|
/** Options for the tenant manager. */
|
|
12
14
|
export interface TenantManagerOptions {
|
|
13
15
|
readonly repository: TenantRepository;
|
|
14
16
|
readonly cache?: TenantCache;
|
|
15
17
|
readonly storage: TenantContextStorage;
|
|
18
|
+
/**
|
|
19
|
+
* How long a cached tenant may be served before it is re-read.
|
|
20
|
+
*
|
|
21
|
+
* Bounds the window in which a suspended tenant keeps being served from
|
|
22
|
+
* cache. Set to 0 to disable caching entirely.
|
|
23
|
+
*/
|
|
24
|
+
readonly cacheTtlMs?: number;
|
|
16
25
|
}
|
|
17
26
|
/**
|
|
18
27
|
* Create a tenant manager.
|
|
@@ -30,6 +39,10 @@ export declare function createTenantManager(options: TenantManagerOptions): {
|
|
|
30
39
|
* Require a tenant by ID — throws if not found.
|
|
31
40
|
*/
|
|
32
41
|
require(id: TenantId): Promise<Tenant>;
|
|
42
|
+
/**
|
|
43
|
+
* Require a tenant that is also in an active state.
|
|
44
|
+
*/
|
|
45
|
+
requireActive(id: TenantId): Promise<Tenant>;
|
|
33
46
|
/**
|
|
34
47
|
* Validate that a tenant is in an active state.
|
|
35
48
|
*/
|
|
@@ -40,6 +53,9 @@ export declare function createTenantManager(options: TenantManagerOptions): {
|
|
|
40
53
|
getCurrent(): Tenant | undefined;
|
|
41
54
|
/**
|
|
42
55
|
* Invalidate a cached tenant.
|
|
56
|
+
*
|
|
57
|
+
* Call after any write that changes a tenant's status, so the change is
|
|
58
|
+
* visible before the TTL would have expired.
|
|
43
59
|
*/
|
|
44
60
|
invalidate(id: TenantId): Promise<void>;
|
|
45
61
|
};
|
|
@@ -4,22 +4,35 @@
|
|
|
4
4
|
* @module repository/tenantManager
|
|
5
5
|
*/
|
|
6
6
|
import { TenantNotFoundError, TenantUnavailableError, } from "../tenancyErrors/tenancyError.types.js";
|
|
7
|
+
/** Default lifetime of a cached tenant record, in milliseconds. */
|
|
8
|
+
export const DEFAULT_TENANT_CACHE_TTL_MS = 30_000;
|
|
7
9
|
/**
|
|
8
10
|
* Create a tenant manager.
|
|
9
11
|
*/
|
|
10
12
|
export function createTenantManager(options) {
|
|
11
13
|
const { repository, cache, storage } = options;
|
|
14
|
+
const ttl = options.cacheTtlMs ?? DEFAULT_TENANT_CACHE_TTL_MS;
|
|
15
|
+
const cachedAt = new Map();
|
|
16
|
+
function isFresh(id) {
|
|
17
|
+
const at = cachedAt.get(id);
|
|
18
|
+
return at !== undefined && Date.now() - at < ttl;
|
|
19
|
+
}
|
|
12
20
|
async function loadTenant(id) {
|
|
13
|
-
|
|
14
|
-
if (cache) {
|
|
21
|
+
if (cache && ttl > 0 && isFresh(id)) {
|
|
15
22
|
const cached = await cache.get(id);
|
|
16
23
|
if (cached)
|
|
17
24
|
return cached;
|
|
18
25
|
}
|
|
19
|
-
// Load from repository
|
|
20
26
|
const tenant = await repository.findById(id);
|
|
21
|
-
if (
|
|
22
|
-
|
|
27
|
+
if (cache && ttl > 0) {
|
|
28
|
+
if (tenant) {
|
|
29
|
+
await cache.set(tenant);
|
|
30
|
+
cachedAt.set(id, Date.now());
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
await cache.delete(id);
|
|
34
|
+
cachedAt.delete(id);
|
|
35
|
+
}
|
|
23
36
|
}
|
|
24
37
|
return tenant;
|
|
25
38
|
}
|
|
@@ -45,6 +58,14 @@ export function createTenantManager(options) {
|
|
|
45
58
|
throw new TenantNotFoundError(id);
|
|
46
59
|
return tenant;
|
|
47
60
|
},
|
|
61
|
+
/**
|
|
62
|
+
* Require a tenant that is also in an active state.
|
|
63
|
+
*/
|
|
64
|
+
async requireActive(id) {
|
|
65
|
+
const tenant = await this.require(id);
|
|
66
|
+
this.assertActive(tenant);
|
|
67
|
+
return tenant;
|
|
68
|
+
},
|
|
48
69
|
/**
|
|
49
70
|
* Validate that a tenant is in an active state.
|
|
50
71
|
*/
|
|
@@ -64,8 +85,12 @@ export function createTenantManager(options) {
|
|
|
64
85
|
},
|
|
65
86
|
/**
|
|
66
87
|
* Invalidate a cached tenant.
|
|
88
|
+
*
|
|
89
|
+
* Call after any write that changes a tenant's status, so the change is
|
|
90
|
+
* visible before the TTL would have expired.
|
|
67
91
|
*/
|
|
68
92
|
async invalidate(id) {
|
|
93
|
+
cachedAt.delete(id);
|
|
69
94
|
if (cache)
|
|
70
95
|
await cache.delete(id);
|
|
71
96
|
},
|
package/dist/resolver/index.d.ts
CHANGED
|
@@ -3,14 +3,29 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module resolver/resolverChain
|
|
5
5
|
*/
|
|
6
|
-
import type { TenantResolver, TenantResolutionResult, ResolverChainOptions } from "../tenancyTypes/resolverTypes.js";
|
|
6
|
+
import type { TenantResolver, TenantResolution, TenantResolutionResult, ResolverChainOptions } from "../tenancyTypes/resolverTypes.js";
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* A resolver chain.
|
|
9
|
+
*
|
|
10
|
+
* Exposes the full result via `resolve`, and views itself as a single
|
|
11
|
+
* `TenantResolver` via `asResolver()` so chains compose and can be passed
|
|
12
|
+
* directly to the HTTP middleware.
|
|
9
13
|
*/
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
*/
|
|
14
|
+
export interface TenantResolverChain<Context = unknown> {
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly priority: number;
|
|
14
17
|
resolve(context: Context): Promise<TenantResolutionResult>;
|
|
15
|
-
|
|
18
|
+
resolveTenant(context: Context): Promise<TenantResolution | undefined>;
|
|
19
|
+
asResolver(): TenantResolver<Context>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Create a resolver chain that tries resolvers in priority order.
|
|
23
|
+
*
|
|
24
|
+
* A resolver that returns `undefined` found nothing, and the chain moves on.
|
|
25
|
+
* A resolver that *throws* rejected a credential — an expired JWT, a bad
|
|
26
|
+
* signature — and the chain stops there. Continuing would let a lower-trust
|
|
27
|
+
* source such as a client-supplied header decide the tenant for a request
|
|
28
|
+
* whose credential was just refused.
|
|
29
|
+
*/
|
|
30
|
+
export declare function createResolverChain<Context = unknown>(resolvers: readonly TenantResolver<Context>[], options?: ResolverChainOptions): TenantResolverChain<Context>;
|
|
16
31
|
//# sourceMappingURL=resolverChain.core.d.ts.map
|
|
@@ -3,46 +3,78 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module resolver/resolverChain
|
|
5
5
|
*/
|
|
6
|
-
import { TenantResolutionConflictError } from "../tenancyErrors/tenancyError.types.js";
|
|
6
|
+
import { TenantResolutionConflictError, TenantResolutionError, } from "../tenancyErrors/tenancyError.types.js";
|
|
7
7
|
/**
|
|
8
8
|
* Create a resolver chain that tries resolvers in priority order.
|
|
9
|
+
*
|
|
10
|
+
* A resolver that returns `undefined` found nothing, and the chain moves on.
|
|
11
|
+
* A resolver that *throws* rejected a credential — an expired JWT, a bad
|
|
12
|
+
* signature — and the chain stops there. Continuing would let a lower-trust
|
|
13
|
+
* source such as a client-supplied header decide the tenant for a request
|
|
14
|
+
* whose credential was just refused.
|
|
9
15
|
*/
|
|
10
16
|
export function createResolverChain(resolvers, options) {
|
|
11
17
|
// Sort by priority descending (higher priority first)
|
|
12
18
|
const sorted = [...resolvers].sort((a, b) => b.priority - a.priority);
|
|
13
|
-
|
|
19
|
+
const collectAll = options?.detectConflicts ?? false;
|
|
20
|
+
const chain = {
|
|
21
|
+
name: options?.name ?? "chain",
|
|
22
|
+
priority: options?.priority ??
|
|
23
|
+
sorted.reduce((highest, r) => Math.max(highest, r.priority), 0),
|
|
14
24
|
/**
|
|
15
25
|
* Resolve tenant from context using the chain.
|
|
26
|
+
*
|
|
27
|
+
* @throws {TenantResolutionError} when a resolver rejects a credential.
|
|
28
|
+
* @throws {TenantResolutionConflictError} when sources disagree and
|
|
29
|
+
* `throwOnConflict` is set.
|
|
16
30
|
*/
|
|
17
31
|
async resolve(context) {
|
|
18
32
|
const candidates = [];
|
|
19
33
|
for (const resolver of sorted) {
|
|
34
|
+
let result;
|
|
20
35
|
try {
|
|
21
|
-
|
|
22
|
-
if (result) {
|
|
23
|
-
candidates.push(result);
|
|
24
|
-
}
|
|
36
|
+
result = await resolver.resolve(context);
|
|
25
37
|
}
|
|
26
|
-
catch {
|
|
27
|
-
|
|
38
|
+
catch (error) {
|
|
39
|
+
throw new TenantResolutionError(`Tenant resolver "${resolver.name}" rejected the request`, error);
|
|
28
40
|
}
|
|
41
|
+
if (!result)
|
|
42
|
+
continue;
|
|
43
|
+
candidates.push(result);
|
|
44
|
+
if (!collectAll)
|
|
45
|
+
break;
|
|
29
46
|
}
|
|
30
47
|
if (candidates.length === 0) {
|
|
31
48
|
return { resolution: undefined, candidates: [], conflict: false };
|
|
32
49
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const hasConflict = uniqueTenantIds.size > 1;
|
|
36
|
-
if (hasConflict && options?.throwOnConflict) {
|
|
50
|
+
const hasConflict = new Set(candidates.map((candidate) => candidate.tenantId)).size > 1;
|
|
51
|
+
if (hasConflict && (options?.throwOnConflict ?? true)) {
|
|
37
52
|
throw new TenantResolutionConflictError(candidates.map((c) => `${c.source}:${c.tenantId}`));
|
|
38
53
|
}
|
|
39
|
-
// Use highest priority (first in sorted list)
|
|
40
54
|
return {
|
|
41
55
|
resolution: candidates[0],
|
|
42
56
|
candidates,
|
|
43
57
|
conflict: hasConflict,
|
|
44
58
|
};
|
|
45
59
|
},
|
|
60
|
+
/**
|
|
61
|
+
* Resolve the winning tenant, discarding the candidate detail.
|
|
62
|
+
*
|
|
63
|
+
* This is what makes a chain a `TenantResolver`, so chains compose with
|
|
64
|
+
* each other and can be handed straight to the HTTP middleware.
|
|
65
|
+
*/
|
|
66
|
+
async resolveTenant(context) {
|
|
67
|
+
return (await chain.resolve(context)).resolution;
|
|
68
|
+
},
|
|
69
|
+
/** View the chain as a single resolver. */
|
|
70
|
+
asResolver() {
|
|
71
|
+
return {
|
|
72
|
+
name: chain.name,
|
|
73
|
+
priority: chain.priority,
|
|
74
|
+
resolve: (context) => chain.resolveTenant(context),
|
|
75
|
+
};
|
|
76
|
+
},
|
|
46
77
|
};
|
|
78
|
+
return chain;
|
|
47
79
|
}
|
|
48
80
|
//# sourceMappingURL=resolverChain.core.js.map
|
|
@@ -4,18 +4,26 @@
|
|
|
4
4
|
* @module resolvers/headerResolver
|
|
5
5
|
*/
|
|
6
6
|
import type { TenantResolver } from "../../tenancyTypes/resolverTypes.js";
|
|
7
|
+
import type { TenantTrustLevel } from "../../tenancyTypes/tenantInterface.js";
|
|
7
8
|
/** Context type with a getHeader method. */
|
|
8
|
-
interface HeaderContext {
|
|
9
|
+
export interface HeaderContext {
|
|
9
10
|
getHeader(name: string): string | undefined;
|
|
10
11
|
}
|
|
11
12
|
/** Options for the header resolver. */
|
|
12
13
|
export interface HeaderResolverOptions {
|
|
13
14
|
readonly headerName?: string;
|
|
14
15
|
readonly priority?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Trust to assign to a tenant resolved from this header.
|
|
18
|
+
*
|
|
19
|
+
* Defaults to `untrusted`, because the header is client-supplied on the
|
|
20
|
+
* wire. Raise it to `verified` only when a trusted proxy strips and re-sets
|
|
21
|
+
* the header at the edge, so a client cannot forge it.
|
|
22
|
+
*/
|
|
23
|
+
readonly trust?: TenantTrustLevel;
|
|
15
24
|
}
|
|
16
25
|
/**
|
|
17
26
|
* Create a tenant resolver that reads from an HTTP header.
|
|
18
27
|
*/
|
|
19
28
|
export declare function createHeaderResolver(options?: HeaderResolverOptions): TenantResolver<HeaderContext>;
|
|
20
|
-
export {};
|
|
21
29
|
//# sourceMappingURL=headerResolver.core.d.ts.map
|
|
@@ -3,25 +3,22 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module resolvers/headerResolver
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { tryCreateTenantId } from "../../tenancyTypes/tenantIdentity.js";
|
|
7
7
|
/**
|
|
8
8
|
* Create a tenant resolver that reads from an HTTP header.
|
|
9
9
|
*/
|
|
10
10
|
export function createHeaderResolver(options) {
|
|
11
11
|
const headerName = options?.headerName ?? "x-tenant-id";
|
|
12
12
|
const priority = options?.priority ?? 80;
|
|
13
|
+
const trust = options?.trust ?? "untrusted";
|
|
13
14
|
return {
|
|
14
15
|
name: "header",
|
|
15
16
|
priority,
|
|
16
17
|
async resolve(context) {
|
|
17
|
-
const
|
|
18
|
-
if (!
|
|
18
|
+
const tenantId = tryCreateTenantId(context.getHeader(headerName));
|
|
19
|
+
if (!tenantId)
|
|
19
20
|
return undefined;
|
|
20
|
-
return {
|
|
21
|
-
tenantId: createTenantId(value),
|
|
22
|
-
source: "header",
|
|
23
|
-
trust: "verified",
|
|
24
|
-
};
|
|
21
|
+
return { tenantId, source: "header", trust };
|
|
25
22
|
},
|
|
26
23
|
};
|
|
27
24
|
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Built-in tenant resolvers.
|
|
3
3
|
*
|
|
4
|
-
* @module resolvers
|
|
4
|
+
* @module resolver/resolvers
|
|
5
5
|
*/
|
|
6
|
-
export { createHeaderResolver
|
|
7
|
-
export {
|
|
8
|
-
export {
|
|
9
|
-
export {
|
|
6
|
+
export { createHeaderResolver } from "./headerResolver.core.js";
|
|
7
|
+
export type { HeaderContext, HeaderResolverOptions, } from "./headerResolver.core.js";
|
|
8
|
+
export { createSubdomainResolver } from "./subdomainResolver.core.js";
|
|
9
|
+
export type { SubdomainContext, SubdomainResolverOptions, } from "./subdomainResolver.core.js";
|
|
10
|
+
export { createPathResolver } from "./pathResolver.core.js";
|
|
11
|
+
export type { PathContext, PathResolverOptions } from "./pathResolver.core.js";
|
|
12
|
+
export { createJwtResolver } from "./jwtResolver.core.js";
|
|
13
|
+
export type { JwtContext, JwtResolverOptions } from "./jwtResolver.core.js";
|
|
10
14
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Built-in tenant resolvers.
|
|
3
3
|
*
|
|
4
|
-
* @module resolvers
|
|
4
|
+
* @module resolver/resolvers
|
|
5
5
|
*/
|
|
6
|
-
export { createHeaderResolver
|
|
7
|
-
export { createSubdomainResolver
|
|
8
|
-
export { createPathResolver
|
|
9
|
-
export { createJwtResolver
|
|
6
|
+
export { createHeaderResolver } from "./headerResolver.core.js";
|
|
7
|
+
export { createSubdomainResolver } from "./subdomainResolver.core.js";
|
|
8
|
+
export { createPathResolver } from "./pathResolver.core.js";
|
|
9
|
+
export { createJwtResolver } from "./jwtResolver.core.js";
|
|
10
10
|
//# sourceMappingURL=index.js.map
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module resolvers/jwtResolver
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { tryCreateTenantId } from "../../tenancyTypes/tenantIdentity.js";
|
|
7
7
|
/**
|
|
8
8
|
* Create a tenant resolver that reads from JWT claims.
|
|
9
9
|
*/
|
|
@@ -17,14 +17,10 @@ export function createJwtResolver(options) {
|
|
|
17
17
|
const claims = context.getClaims();
|
|
18
18
|
if (!claims)
|
|
19
19
|
return undefined;
|
|
20
|
-
const tenantId = claims[claimKey];
|
|
21
|
-
if (
|
|
20
|
+
const tenantId = tryCreateTenantId(claims[claimKey]);
|
|
21
|
+
if (!tenantId)
|
|
22
22
|
return undefined;
|
|
23
|
-
return {
|
|
24
|
-
tenantId: createTenantId(tenantId),
|
|
25
|
-
source: "jwt",
|
|
26
|
-
trust: "trusted",
|
|
27
|
-
};
|
|
23
|
+
return { tenantId, source: "jwt", trust: "trusted" };
|
|
28
24
|
},
|
|
29
25
|
};
|
|
30
26
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { TenantResolver } from "../../tenancyTypes/resolverTypes.js";
|
|
7
7
|
/** Context with getPath method. */
|
|
8
|
-
interface PathContext {
|
|
8
|
+
export interface PathContext {
|
|
9
9
|
getPath(): string | undefined;
|
|
10
10
|
}
|
|
11
11
|
/** Options for the path resolver. */
|
|
@@ -20,5 +20,4 @@ export interface PathResolverOptions {
|
|
|
20
20
|
* Example: "/acme/users" → tenant "acme"
|
|
21
21
|
*/
|
|
22
22
|
export declare function createPathResolver(options?: PathResolverOptions): TenantResolver<PathContext>;
|
|
23
|
-
export {};
|
|
24
23
|
//# sourceMappingURL=pathResolver.core.d.ts.map
|
|
@@ -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
|
*
|
|
@@ -29,12 +29,10 @@ export function createPathResolver(options) {
|
|
|
29
29
|
}
|
|
30
30
|
if (segments.length === 0)
|
|
31
31
|
return undefined;
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
trust: "untrusted",
|
|
37
|
-
};
|
|
32
|
+
const tenantId = tryCreateTenantId(segments[0]);
|
|
33
|
+
if (!tenantId)
|
|
34
|
+
return undefined;
|
|
35
|
+
return { tenantId, source: "path", trust: "untrusted" };
|
|
38
36
|
},
|
|
39
37
|
};
|
|
40
38
|
}
|
|
@@ -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
|
}
|