@zudojs/tenancy 1.3.0 → 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 CHANGED
@@ -45,12 +45,19 @@ 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: resolver.asResolver(),
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
+
54
61
  The chain's context type is inferred from its resolvers — what the JWT,
55
62
  domain and subdomain resolvers read, which `HttpResolverContext` provides — so
56
63
  the call needs no type argument and no casts. (Before 1.3 it failed with
@@ -67,6 +74,25 @@ into a route's `middleware` list:
67
74
  router.get("/projects", listProjects, { middleware: [middleware] });
68
75
  ```
69
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
+
70
96
  A refusal — 400, 401, 403, 404 — is a `GuardResponse` (`createGuardResponse`
71
97
  from `@zudojs/middleware`), which `@zudojs/http` sends with that status. The
72
98
  helpers `createBadRequest`, `createUnauthorized`, `createForbidden`,
@@ -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(context: HttpMiddlewareContext, getClaims?: (context: HttpMiddlewareContext) => TenantClaims | undefined): HttpResolverContext;
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, and loading
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, and loading
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
@@ -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 type { TenantResolverSource } from "./httpSupport/index.js";
11
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
@@ -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 { TenantResolver, TenantResolution } from "../tenancyTypes/resolverTypes.js";
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, TenantClaims } from "./httpResolverContext.js";
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
- /** Options for the resolve tenant middleware. */
21
- export interface ResolveTenantMiddlewareOptions {
22
- /** Resolver chain or single resolver to determine tenant. */
23
- readonly resolver: TenantResolver<HttpResolverContext>;
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
- /** Reads verified token claims for the JWT resolver. */
62
- readonly getClaims?: (context: HttpMiddlewareContext) => TenantClaims | undefined;
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
  *
@@ -10,7 +10,7 @@
10
10
  import { createHttpResolverContext } from "./httpResolverContext.js";
11
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,6 +26,7 @@ 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
@@ -36,7 +37,7 @@ export function createResolveTenantMiddleware(options) {
36
37
  return async (context, next) => {
37
38
  let resolution;
38
39
  try {
39
- resolution = await options.resolver.resolve(createHttpResolverContext(context, options.getClaims));
40
+ resolution = await resolver.resolve(createHttpResolverContext(context, options.getClaims));
40
41
  }
41
42
  catch (error) {
42
43
  // A resolver chain throws when a credential was rejected or when two
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zudojs/tenancy",
3
- "version": "1.3.0",
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": {