@zudojs/tenancy 1.2.1 → 1.3.1
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 +49 -2
- package/dist/http/httpHelpers.d.ts +16 -26
- package/dist/http/httpHelpers.js +15 -6
- package/dist/http/httpResolverContext.d.ts +11 -1
- package/dist/http/httpSupport/index.d.ts +4 -2
- package/dist/http/httpSupport/index.js +4 -2
- package/dist/http/httpSupport/tenancyMiddleware.resolver.d.ts +30 -0
- package/dist/http/httpSupport/tenancyMiddleware.resolver.js +25 -0
- package/dist/http/httpTypes.d.ts +17 -2
- package/dist/http/index.d.ts +3 -2
- package/dist/http/index.js +1 -1
- package/dist/http/tenancyMiddleware.core.d.ts +23 -9
- package/dist/http/tenancyMiddleware.core.js +6 -13
- package/dist/resolver/index.d.ts +1 -1
- package/dist/resolver/resolverChain.core.d.ts +18 -0
- package/dist/resolver/resolverChain.core.js +0 -9
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -45,15 +45,62 @@ const resolver = createResolverChain([
|
|
|
45
45
|
// (default "verified"), then runs the rest of the request inside the tenant
|
|
46
46
|
// context. acme.example.com reaches t-1001 through its slug.
|
|
47
47
|
const middleware = createResolveTenantMiddleware({
|
|
48
|
-
resolver
|
|
48
|
+
resolver, // a chain as it is, or a single resolver
|
|
49
49
|
repository,
|
|
50
50
|
storage,
|
|
51
51
|
});
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
+
`resolver` takes a chain directly. Before 1.3.1 it took only a single
|
|
55
|
+
`TenantResolver`, so a chain had to go through `chain.asResolver()`: passing
|
|
56
|
+
the chain itself was a type error, and cast through, every request was
|
|
57
|
+
refused, because a chain's `resolve` returns `{ resolution, candidates,
|
|
58
|
+
conflict }` rather than a resolution. The middleware now recognises a chain by
|
|
59
|
+
its `asResolver` method and adapts it. `resolver.asResolver()` still works.
|
|
60
|
+
|
|
61
|
+
The chain's context type is inferred from its resolvers — what the JWT,
|
|
62
|
+
domain and subdomain resolvers read, which `HttpResolverContext` provides — so
|
|
63
|
+
the call needs no type argument and no casts. (Before 1.3 it failed with
|
|
64
|
+
TS2322 unless you wrote `createResolverChain<HttpResolverContext>`; that form
|
|
65
|
+
still works.)
|
|
66
|
+
|
|
54
67
|
The middleware runs inside the real `@zudojs/http` pipeline without
|
|
55
68
|
depending on it: headers are read through `request.getHeader()` when present,
|
|
56
|
-
and otherwise from a plain object or a `Map`, case-insensitively.
|
|
69
|
+
and otherwise from a plain object or a `Map`, case-insensitively. Its
|
|
70
|
+
`HttpMiddleware` type is assignable to `@zudojs/http`'s, so it goes straight
|
|
71
|
+
into a route's `middleware` list:
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
router.get("/projects", listProjects, { middleware: [middleware] });
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`getClaims` reads verified token claims for the JWT resolver (by default they
|
|
78
|
+
come from the `tenancy:claims` state key). It may be typed with
|
|
79
|
+
`@zudojs/http`'s own `HttpMiddlewareContext`, so a helper your auth layer
|
|
80
|
+
already has plugs in without a cast:
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import type { HttpMiddlewareContext } from "@zudojs/http";
|
|
84
|
+
import type { TenantClaims } from "@zudojs/tenancy";
|
|
85
|
+
|
|
86
|
+
const claimsOf = (context: HttpMiddlewareContext) =>
|
|
87
|
+
context.state.get<TenantClaims>("auth:claims");
|
|
88
|
+
|
|
89
|
+
createResolveTenantMiddleware({ resolver, repository, storage, getClaims: claimsOf });
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The option is generic over the context it reads, bounded by this package's
|
|
93
|
+
structural mirror, which the real context satisfies. A reader for something
|
|
94
|
+
that is not a middleware context, such as a bare request, is still refused.
|
|
95
|
+
|
|
96
|
+
A refusal — 400, 401, 403, 404 — is a `GuardResponse` (`createGuardResponse`
|
|
97
|
+
from `@zudojs/middleware`), which `@zudojs/http` sends with that status. The
|
|
98
|
+
helpers `createBadRequest`, `createUnauthorized`, `createForbidden`,
|
|
99
|
+
`createNotFound`, `createJsonErrorResponse` and `createJsonResponse` all
|
|
100
|
+
return one. They used to return plain `{ status, body, headers }` objects,
|
|
101
|
+
which `@zudojs/http` did not treat as a response, so a request carrying only
|
|
102
|
+
an `x-tenant-id` header was refused (the handler never ran) but answered
|
|
103
|
+
`200` instead of `403`.
|
|
57
104
|
|
|
58
105
|
Read the current tenant anywhere downstream:
|
|
59
106
|
|
|
@@ -1,46 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HTTP response helpers for tenancy middleware.
|
|
3
3
|
*
|
|
4
|
+
* Every helper returns a `GuardResponse` from `@zudojs/middleware`, which
|
|
5
|
+
* `@zudojs/http` sends with its own status. They used to return plain
|
|
6
|
+
* `{ status, body, headers }` objects, which a route middleware's caller
|
|
7
|
+
* ignored: a refused request reached the client as `200`.
|
|
8
|
+
*
|
|
4
9
|
* @module http/httpHelpers
|
|
5
10
|
*/
|
|
11
|
+
import { type GuardResponse } from "@zudojs/middleware";
|
|
12
|
+
/**
|
|
13
|
+
* Create a JSON response with a caller-built body.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createJsonResponse(status: number, body: unknown): GuardResponse;
|
|
6
16
|
/**
|
|
7
|
-
* Create a JSON error response
|
|
17
|
+
* Create a JSON error response: `{ "error": message }`.
|
|
8
18
|
*/
|
|
9
|
-
export declare function createJsonErrorResponse(status: number, message: string):
|
|
10
|
-
readonly status: number;
|
|
11
|
-
readonly body: unknown;
|
|
12
|
-
readonly headers: Record<string, string>;
|
|
13
|
-
};
|
|
19
|
+
export declare function createJsonErrorResponse(status: number, message: string): GuardResponse;
|
|
14
20
|
/**
|
|
15
21
|
* Create a 400 Bad Request response.
|
|
16
22
|
*/
|
|
17
|
-
export declare function createBadRequest(message: string):
|
|
18
|
-
readonly status: number;
|
|
19
|
-
readonly body: unknown;
|
|
20
|
-
readonly headers: Record<string, string>;
|
|
21
|
-
};
|
|
23
|
+
export declare function createBadRequest(message: string): GuardResponse;
|
|
22
24
|
/**
|
|
23
25
|
* Create a 401 Unauthorized response.
|
|
24
26
|
*/
|
|
25
|
-
export declare function createUnauthorized(message: string):
|
|
26
|
-
readonly status: number;
|
|
27
|
-
readonly body: unknown;
|
|
28
|
-
readonly headers: Record<string, string>;
|
|
29
|
-
};
|
|
27
|
+
export declare function createUnauthorized(message: string): GuardResponse;
|
|
30
28
|
/**
|
|
31
29
|
* Create a 403 Forbidden response.
|
|
32
30
|
*/
|
|
33
|
-
export declare function createForbidden(message: string):
|
|
34
|
-
readonly status: number;
|
|
35
|
-
readonly body: unknown;
|
|
36
|
-
readonly headers: Record<string, string>;
|
|
37
|
-
};
|
|
31
|
+
export declare function createForbidden(message: string): GuardResponse;
|
|
38
32
|
/**
|
|
39
33
|
* Create a 404 Not Found response.
|
|
40
34
|
*/
|
|
41
|
-
export declare function createNotFound(message: string):
|
|
42
|
-
readonly status: number;
|
|
43
|
-
readonly body: unknown;
|
|
44
|
-
readonly headers: Record<string, string>;
|
|
45
|
-
};
|
|
35
|
+
export declare function createNotFound(message: string): GuardResponse;
|
|
46
36
|
//# sourceMappingURL=httpHelpers.d.ts.map
|
package/dist/http/httpHelpers.js
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HTTP response helpers for tenancy middleware.
|
|
3
3
|
*
|
|
4
|
+
* Every helper returns a `GuardResponse` from `@zudojs/middleware`, which
|
|
5
|
+
* `@zudojs/http` sends with its own status. They used to return plain
|
|
6
|
+
* `{ status, body, headers }` objects, which a route middleware's caller
|
|
7
|
+
* ignored: a refused request reached the client as `200`.
|
|
8
|
+
*
|
|
4
9
|
* @module http/httpHelpers
|
|
5
10
|
*/
|
|
11
|
+
import { createGuardResponse } from "@zudojs/middleware";
|
|
12
|
+
const JSON_HEADERS = { "content-type": "application/json" };
|
|
13
|
+
/**
|
|
14
|
+
* Create a JSON response with a caller-built body.
|
|
15
|
+
*/
|
|
16
|
+
export function createJsonResponse(status, body) {
|
|
17
|
+
return createGuardResponse({ status, body, headers: JSON_HEADERS });
|
|
18
|
+
}
|
|
6
19
|
/**
|
|
7
|
-
* Create a JSON error response
|
|
20
|
+
* Create a JSON error response: `{ "error": message }`.
|
|
8
21
|
*/
|
|
9
22
|
export function createJsonErrorResponse(status, message) {
|
|
10
|
-
return
|
|
11
|
-
status,
|
|
12
|
-
body: { error: message },
|
|
13
|
-
headers: { "content-type": "application/json" },
|
|
14
|
-
});
|
|
23
|
+
return createJsonResponse(status, { error: message });
|
|
15
24
|
}
|
|
16
25
|
/**
|
|
17
26
|
* Create a 400 Bad Request response.
|
|
@@ -20,6 +20,16 @@ export interface HttpResolverContext {
|
|
|
20
20
|
getPath(): string | undefined;
|
|
21
21
|
getClaims(): TenantClaims | undefined;
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Reads verified token claims for a request.
|
|
25
|
+
*
|
|
26
|
+
* Generic over the middleware context it reads, so a helper written against
|
|
27
|
+
* `@zudojs/http`'s own `HttpMiddlewareContext` is accepted as it is. The
|
|
28
|
+
* constraint is this package's structural mirror, which the real context
|
|
29
|
+
* satisfies; tenancy cannot depend on http (a higher tier), so the mirror is
|
|
30
|
+
* the only shape it can name.
|
|
31
|
+
*/
|
|
32
|
+
export type TenantClaimsReader<Context extends HttpMiddlewareContext = HttpMiddlewareContext> = (context: Context) => TenantClaims | undefined;
|
|
23
33
|
/** State key under which upstream auth middleware publishes token claims. */
|
|
24
34
|
export declare const TENANT_CLAIMS_STATE_KEY = "tenancy:claims";
|
|
25
35
|
/**
|
|
@@ -31,5 +41,5 @@ export declare const TENANT_CLAIMS_STATE_KEY = "tenancy:claims";
|
|
|
31
41
|
* an authentication middleware is expected to publish them.
|
|
32
42
|
* @returns An accessor object every shipped resolver understands.
|
|
33
43
|
*/
|
|
34
|
-
export declare function createHttpResolverContext
|
|
44
|
+
export declare function createHttpResolverContext<Context extends HttpMiddlewareContext = HttpMiddlewareContext>(context: Context, getClaims?: TenantClaimsReader<Context>): HttpResolverContext;
|
|
35
45
|
//# sourceMappingURL=httpResolverContext.d.ts.map
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Support for the tenancy HTTP middleware: portable header reads,
|
|
3
|
-
* the tenant a resolution names (by id, then by slug)
|
|
2
|
+
* Support for the tenancy HTTP middleware: portable header reads, loading
|
|
3
|
+
* the tenant a resolution names (by id, then by slug), and adapting a
|
|
4
|
+
* resolver chain passed where a resolver is expected.
|
|
4
5
|
*
|
|
5
6
|
* @module http/httpSupport
|
|
6
7
|
*/
|
|
7
8
|
export { readRequestHeader } from "./httpRequest.helper.js";
|
|
8
9
|
export { loadResolvedTenant } from "./tenancyMiddleware.lookup.js";
|
|
10
|
+
export { toTenantResolver, type TenantResolverSource, } from "./tenancyMiddleware.resolver.js";
|
|
9
11
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Support for the tenancy HTTP middleware: portable header reads,
|
|
3
|
-
* the tenant a resolution names (by id, then by slug)
|
|
2
|
+
* Support for the tenancy HTTP middleware: portable header reads, loading
|
|
3
|
+
* the tenant a resolution names (by id, then by slug), and adapting a
|
|
4
|
+
* resolver chain passed where a resolver is expected.
|
|
4
5
|
*
|
|
5
6
|
* @module http/httpSupport
|
|
6
7
|
*/
|
|
7
8
|
export { readRequestHeader } from "./httpRequest.helper.js";
|
|
8
9
|
export { loadResolvedTenant } from "./tenancyMiddleware.lookup.js";
|
|
10
|
+
export { toTenantResolver, } from "./tenancyMiddleware.resolver.js";
|
|
9
11
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalises what `createResolveTenantMiddleware` accepts as its resolver.
|
|
3
|
+
*
|
|
4
|
+
* @module http/httpSupport/tenancyMiddleware.resolver
|
|
5
|
+
*/
|
|
6
|
+
import type { TenantResolver } from "../../tenancyTypes/resolverTypes.js";
|
|
7
|
+
import type { TenantResolverChain } from "../../resolver/resolverChain.core.js";
|
|
8
|
+
/**
|
|
9
|
+
* A single resolver, or a resolver chain passed as it is.
|
|
10
|
+
*
|
|
11
|
+
* A chain's `resolve` returns the full `TenantResolutionResult`
|
|
12
|
+
* (`{ resolution, candidates, conflict }`), not a `TenantResolution`, so it
|
|
13
|
+
* is not itself a `TenantResolver`. The middleware used to take only the
|
|
14
|
+
* resolver form: passing a chain was a type error, and cast through at
|
|
15
|
+
* runtime every request was refused because the result object carries no
|
|
16
|
+
* `trust`. Both forms are accepted now.
|
|
17
|
+
*/
|
|
18
|
+
export type TenantResolverSource<Context> = TenantResolver<Context> | TenantResolverChain<Context>;
|
|
19
|
+
/**
|
|
20
|
+
* Turn a resolver or a resolver chain into a `TenantResolver`.
|
|
21
|
+
*
|
|
22
|
+
* A chain is recognised structurally, by its `asResolver` method, so a chain
|
|
23
|
+
* built by another copy of this package is adapted too. Anything else is
|
|
24
|
+
* used as it is.
|
|
25
|
+
*
|
|
26
|
+
* @param source - A resolver, or a chain from `createResolverChain`.
|
|
27
|
+
* @returns A resolver that yields the winning `TenantResolution`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function toTenantResolver<Context>(source: TenantResolverSource<Context>): TenantResolver<Context>;
|
|
30
|
+
//# sourceMappingURL=tenancyMiddleware.resolver.d.ts.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalises what `createResolveTenantMiddleware` accepts as its resolver.
|
|
3
|
+
*
|
|
4
|
+
* @module http/httpSupport/tenancyMiddleware.resolver
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Turn a resolver or a resolver chain into a `TenantResolver`.
|
|
8
|
+
*
|
|
9
|
+
* A chain is recognised structurally, by its `asResolver` method, so a chain
|
|
10
|
+
* built by another copy of this package is adapted too. Anything else is
|
|
11
|
+
* used as it is.
|
|
12
|
+
*
|
|
13
|
+
* @param source - A resolver, or a chain from `createResolverChain`.
|
|
14
|
+
* @returns A resolver that yields the winning `TenantResolution`.
|
|
15
|
+
*/
|
|
16
|
+
export function toTenantResolver(source) {
|
|
17
|
+
if (isResolverChain(source))
|
|
18
|
+
return source.asResolver();
|
|
19
|
+
return source;
|
|
20
|
+
}
|
|
21
|
+
function isResolverChain(source) {
|
|
22
|
+
return (typeof source.asResolver ===
|
|
23
|
+
"function");
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=tenancyMiddleware.resolver.js.map
|
package/dist/http/httpTypes.d.ts
CHANGED
|
@@ -7,8 +7,23 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @module http/httpTypes
|
|
9
9
|
*/
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
import type { GuardResponse } from "@zudojs/middleware";
|
|
11
|
+
/**
|
|
12
|
+
* What a tenancy middleware returns: nothing, a web `Response`, a
|
|
13
|
+
* `GuardResponse` refusing the request, or whatever `next()` produced.
|
|
14
|
+
*/
|
|
15
|
+
export type HttpMiddlewareOutcome<Downstream> = void | Response | GuardResponse | Downstream;
|
|
16
|
+
/**
|
|
17
|
+
* HTTP middleware signature, structurally assignable to `@zudojs/http`'s
|
|
18
|
+
* `HttpMiddleware` without a cast.
|
|
19
|
+
*
|
|
20
|
+
* Generic over what `next()` resolves to, so a middleware hands back the
|
|
21
|
+
* real pipeline's response unchanged. A refusal is a `GuardResponse`
|
|
22
|
+
* (`createGuardResponse` from `@zudojs/middleware`), which `@zudojs/http`
|
|
23
|
+
* sends with its own status; a plain `{ status, body, headers }` object is
|
|
24
|
+
* not a response.
|
|
25
|
+
*/
|
|
26
|
+
export type HttpMiddleware = <Downstream extends HttpResponseContext>(context: HttpMiddlewareContext, next: () => Promise<Downstream>) => HttpMiddlewareOutcome<Downstream> | Promise<HttpMiddlewareOutcome<Downstream>>;
|
|
12
27
|
/** HTTP middleware context from @zudojs/http. */
|
|
13
28
|
export interface HttpMiddlewareContext {
|
|
14
29
|
readonly request: HttpRequestContext;
|
package/dist/http/index.d.ts
CHANGED
|
@@ -6,8 +6,9 @@ export type { RequireTenantMiddlewareOptions, ResolveTenantMiddlewareOptions, }
|
|
|
6
6
|
export { createTenantGuardMiddleware, createTenantPropagationMiddleware, } from "./tenancyMiddleware.guard.js";
|
|
7
7
|
export type { TenantGuardMiddlewareOptions } from "./tenancyMiddleware.guard.js";
|
|
8
8
|
export { TENANT_CLAIMS_STATE_KEY, createHttpResolverContext, } from "./httpResolverContext.js";
|
|
9
|
-
export type { HttpResolverContext, TenantClaims, } from "./httpResolverContext.js";
|
|
9
|
+
export type { HttpResolverContext, TenantClaims, TenantClaimsReader, } from "./httpResolverContext.js";
|
|
10
10
|
export { readRequestHeader } from "./httpSupport/index.js";
|
|
11
|
-
export {
|
|
11
|
+
export type { TenantResolverSource } from "./httpSupport/index.js";
|
|
12
|
+
export { createBadRequest, createForbidden, createJsonErrorResponse, createJsonResponse, createNotFound, createUnauthorized, } from "./httpHelpers.js";
|
|
12
13
|
export type * from "./httpTypes.js";
|
|
13
14
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/http/index.js
CHANGED
|
@@ -5,5 +5,5 @@ export { TENANT_CONTEXT_STATE_KEY, TENANT_STATE_KEY, createRequireTenantMiddlewa
|
|
|
5
5
|
export { createTenantGuardMiddleware, createTenantPropagationMiddleware, } from "./tenancyMiddleware.guard.js";
|
|
6
6
|
export { TENANT_CLAIMS_STATE_KEY, createHttpResolverContext, } from "./httpResolverContext.js";
|
|
7
7
|
export { readRequestHeader } from "./httpSupport/index.js";
|
|
8
|
-
export { createBadRequest, createForbidden, createJsonErrorResponse, createNotFound, createUnauthorized, } from "./httpHelpers.js";
|
|
8
|
+
export { createBadRequest, createForbidden, createJsonErrorResponse, createJsonResponse, createNotFound, createUnauthorized, } from "./httpHelpers.js";
|
|
9
9
|
//# sourceMappingURL=index.js.map
|
|
@@ -8,19 +8,30 @@
|
|
|
8
8
|
* Composes with the @zudojs/http pipeline structurally; no dependency on it.
|
|
9
9
|
*/
|
|
10
10
|
import type { Tenant, TenantRequirement, TenantTrustLevel } from "../tenancyTypes/tenantInterface.js";
|
|
11
|
-
import type {
|
|
11
|
+
import type { 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
14
|
import type { HttpMiddleware, HttpMiddlewareContext } from "./httpTypes.js";
|
|
15
|
-
import type { HttpResolverContext,
|
|
15
|
+
import type { HttpResolverContext, TenantClaimsReader } from "./httpResolverContext.js";
|
|
16
|
+
import { type TenantResolverSource } from "./httpSupport/index.js";
|
|
16
17
|
/** State key for the resolved tenant. */
|
|
17
18
|
export declare const TENANT_STATE_KEY = "tenancy:tenant";
|
|
18
19
|
/** State key for the tenant context. */
|
|
19
20
|
export declare const TENANT_CONTEXT_STATE_KEY = "tenancy:context";
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Options for the resolve tenant middleware.
|
|
23
|
+
*
|
|
24
|
+
* `Context` is the middleware context `getClaims` reads. It defaults to this
|
|
25
|
+
* package's mirror; a `getClaims` typed with `@zudojs/http`'s
|
|
26
|
+
* `HttpMiddlewareContext` sets it to that, with no cast.
|
|
27
|
+
*/
|
|
28
|
+
export interface ResolveTenantMiddlewareOptions<Context extends HttpMiddlewareContext = HttpMiddlewareContext> {
|
|
29
|
+
/**
|
|
30
|
+
* A single resolver, or a resolver chain from `createResolverChain` passed
|
|
31
|
+
* as it is. A chain is adapted with its `asResolver()`, so calling that
|
|
32
|
+
* yourself is no longer needed (it still works).
|
|
33
|
+
*/
|
|
34
|
+
readonly resolver: TenantResolverSource<HttpResolverContext>;
|
|
24
35
|
/** Repository to load the full tenant after resolution. */
|
|
25
36
|
readonly repository: TenantRepository;
|
|
26
37
|
/** Tenant context storage for propagation. */
|
|
@@ -58,8 +69,11 @@ export interface ResolveTenantMiddlewareOptions {
|
|
|
58
69
|
* suspended tenant is refused whether or not this is set.
|
|
59
70
|
*/
|
|
60
71
|
readonly optional?: boolean;
|
|
61
|
-
/**
|
|
62
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Reads verified token claims for the JWT resolver. Defaults to the
|
|
74
|
+
* `tenancy:claims` state key. May be typed with `@zudojs/http`'s context.
|
|
75
|
+
*/
|
|
76
|
+
readonly getClaims?: TenantClaimsReader<Context>;
|
|
63
77
|
/** Custom error response for missing tenant. */
|
|
64
78
|
readonly notFoundResponse?: (resolution: TenantResolution | undefined) => unknown;
|
|
65
79
|
}
|
|
@@ -77,7 +91,7 @@ export interface RequireTenantMiddlewareOptions {
|
|
|
77
91
|
* Enforces trust and tenant status itself rather than relying on a second
|
|
78
92
|
* middleware being installed: the safe behaviour has to be the default.
|
|
79
93
|
*/
|
|
80
|
-
export declare function createResolveTenantMiddleware(options: ResolveTenantMiddlewareOptions): HttpMiddleware;
|
|
94
|
+
export declare function createResolveTenantMiddleware<Context extends HttpMiddlewareContext = HttpMiddlewareContext>(options: ResolveTenantMiddlewareOptions<Context>): HttpMiddleware;
|
|
81
95
|
/**
|
|
82
96
|
* Create middleware that enforces tenant presence.
|
|
83
97
|
*
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
* Composes with the @zudojs/http pipeline structurally; no dependency on it.
|
|
9
9
|
*/
|
|
10
10
|
import { createHttpResolverContext } from "./httpResolverContext.js";
|
|
11
|
-
import { createBadRequest, createForbidden, createNotFound, createUnauthorized, } from "./httpHelpers.js";
|
|
11
|
+
import { createBadRequest, createForbidden, createJsonResponse, createNotFound, createUnauthorized, } from "./httpHelpers.js";
|
|
12
12
|
import { meetsTrustLevel } from "../security/guard.core.js";
|
|
13
|
-
import { loadResolvedTenant } from "./httpSupport/index.js";
|
|
13
|
+
import { loadResolvedTenant, toTenantResolver, } from "./httpSupport/index.js";
|
|
14
14
|
import { TenantResolutionConflictError, TenantResolutionError, } from "../tenancyErrors/tenancyError.types.js";
|
|
15
15
|
// ─── State Keys ───────────────────────────────────────────────────────────
|
|
16
16
|
/** State key for the resolved tenant. */
|
|
@@ -26,21 +26,18 @@ export const TENANT_CONTEXT_STATE_KEY = "tenancy:context";
|
|
|
26
26
|
* middleware being installed: the safe behaviour has to be the default.
|
|
27
27
|
*/
|
|
28
28
|
export function createResolveTenantMiddleware(options) {
|
|
29
|
+
const resolver = toTenantResolver(options.resolver);
|
|
29
30
|
const minimumTrust = options.minimumTrust ?? "verified";
|
|
30
31
|
const slugLookup = options.slugLookup !== false;
|
|
31
32
|
// Unknown and unavailable tenants get the same answer, so a caller cannot
|
|
32
33
|
// learn which tenant ids exist or which of them are suspended.
|
|
33
34
|
const notFound = (resolution) => options.notFoundResponse
|
|
34
|
-
?
|
|
35
|
-
status: 404,
|
|
36
|
-
body: options.notFoundResponse(resolution),
|
|
37
|
-
headers: { "content-type": "application/json" },
|
|
38
|
-
}
|
|
35
|
+
? createJsonResponse(404, options.notFoundResponse(resolution))
|
|
39
36
|
: createNotFound("Tenant not found");
|
|
40
37
|
return async (context, next) => {
|
|
41
38
|
let resolution;
|
|
42
39
|
try {
|
|
43
|
-
resolution = await
|
|
40
|
+
resolution = await resolver.resolve(createHttpResolverContext(context, options.getClaims));
|
|
44
41
|
}
|
|
45
42
|
catch (error) {
|
|
46
43
|
// A resolver chain throws when a credential was rejected or when two
|
|
@@ -98,11 +95,7 @@ export function createRequireTenantMiddleware(options) {
|
|
|
98
95
|
return next();
|
|
99
96
|
if (!tenant) {
|
|
100
97
|
return options?.deniedResponse
|
|
101
|
-
?
|
|
102
|
-
status: 401,
|
|
103
|
-
body: options.deniedResponse(undefined),
|
|
104
|
-
headers: { "content-type": "application/json" },
|
|
105
|
-
}
|
|
98
|
+
? createJsonResponse(401, options.deniedResponse(undefined))
|
|
106
99
|
: createUnauthorized("Tenant context is required");
|
|
107
100
|
}
|
|
108
101
|
return next();
|
package/dist/resolver/index.d.ts
CHANGED
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
* @module resolver
|
|
5
5
|
*/
|
|
6
6
|
export { createResolverChain } from "./resolverChain.core.js";
|
|
7
|
-
export type { TenantResolverChain } from "./resolverChain.core.js";
|
|
7
|
+
export type { ResolverChainContext, TenantResolverChain, } from "./resolverChain.core.js";
|
|
8
8
|
export * from "./resolvers/index.js";
|
|
9
9
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -18,14 +18,32 @@ export interface TenantResolverChain<Context = unknown> {
|
|
|
18
18
|
resolveTenant(context: Context): Promise<TenantResolution | undefined>;
|
|
19
19
|
asResolver(): TenantResolver<Context>;
|
|
20
20
|
}
|
|
21
|
+
/** The context type one resolver reads. */
|
|
22
|
+
type ResolverContextOf<Resolver> = Resolver extends TenantResolver<infer Context> ? Context : never;
|
|
23
|
+
type UnionToIntersection<Union> = (Union extends unknown ? (value: Union) => void : never) extends (value: infer Intersection) => void ? Intersection : never;
|
|
24
|
+
/**
|
|
25
|
+
* The context a chain of these resolvers needs: everything each of them
|
|
26
|
+
* reads. For the JWT, domain and subdomain resolvers that is
|
|
27
|
+
* `JwtContext & DomainContext & SubdomainContext`, which
|
|
28
|
+
* `HttpResolverContext` satisfies.
|
|
29
|
+
*/
|
|
30
|
+
export type ResolverChainContext<Resolvers extends readonly TenantResolver<never>[]> = UnionToIntersection<ResolverContextOf<Resolvers[number]>>;
|
|
21
31
|
/**
|
|
22
32
|
* Create a resolver chain that tries resolvers in priority order.
|
|
23
33
|
*
|
|
34
|
+
* The chain's context is inferred from the resolvers — each reads a
|
|
35
|
+
* different part of the request, so it is the intersection of what they
|
|
36
|
+
* read — and `createResolverChain([createJwtResolver(), createDomainResolver(
|
|
37
|
+
* { repository })])` needs no type argument. Passing one explicitly
|
|
38
|
+
* (`createResolverChain<HttpResolverContext>([...])`) still works.
|
|
39
|
+
*
|
|
24
40
|
* A resolver that returns `undefined` found nothing, and the chain moves on.
|
|
25
41
|
* A resolver that *throws* rejected a credential — an expired JWT, a bad
|
|
26
42
|
* signature — and the chain stops there. Continuing would let a lower-trust
|
|
27
43
|
* source such as a client-supplied header decide the tenant for a request
|
|
28
44
|
* whose credential was just refused.
|
|
29
45
|
*/
|
|
46
|
+
export declare function createResolverChain<const Resolvers extends readonly TenantResolver<never>[]>(resolvers: Resolvers, options?: ResolverChainOptions): TenantResolverChain<ResolverChainContext<Resolvers>>;
|
|
30
47
|
export declare function createResolverChain<Context = unknown>(resolvers: readonly TenantResolver<Context>[], options?: ResolverChainOptions): TenantResolverChain<Context>;
|
|
48
|
+
export {};
|
|
31
49
|
//# sourceMappingURL=resolverChain.core.d.ts.map
|
|
@@ -4,15 +4,6 @@
|
|
|
4
4
|
* @module resolver/resolverChain
|
|
5
5
|
*/
|
|
6
6
|
import { TenantResolutionConflictError, TenantResolutionError, } from "../tenancyErrors/tenancyError.types.js";
|
|
7
|
-
/**
|
|
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.
|
|
15
|
-
*/
|
|
16
7
|
export function createResolverChain(resolvers, options) {
|
|
17
8
|
// Sort by priority descending (higher priority first)
|
|
18
9
|
const sorted = [...resolvers].sort((a, b) => b.priority - a.priority);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/tenancy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
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
6
|
"author": {
|
|
@@ -25,15 +25,16 @@
|
|
|
25
25
|
"!dist/.tsbuildinfo"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@zudojs/errors": "1.
|
|
29
|
-
"@zudojs/constants": "1.1.
|
|
28
|
+
"@zudojs/errors": "1.3.0",
|
|
29
|
+
"@zudojs/constants": "1.1.2",
|
|
30
|
+
"@zudojs/middleware": "1.1.0"
|
|
30
31
|
},
|
|
31
32
|
"engines": {
|
|
32
33
|
"node": ">=24.0.0"
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
|
35
36
|
"typescript": "7.0.2",
|
|
36
|
-
"vitest": "^
|
|
37
|
+
"vitest": "^5.0.1"
|
|
37
38
|
},
|
|
38
39
|
"publishConfig": {
|
|
39
40
|
"access": "public"
|
|
@@ -44,7 +45,7 @@
|
|
|
44
45
|
"multi-tenant",
|
|
45
46
|
"saas"
|
|
46
47
|
],
|
|
47
|
-
"homepage": "https://
|
|
48
|
+
"homepage": "https://zudojs.oyinlola.site/docs/packages-tenancy",
|
|
48
49
|
"bugs": {
|
|
49
50
|
"url": "https://github.com/oyinlola-tech/zudo/issues"
|
|
50
51
|
},
|