@zudojs/tenancy 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -51,9 +51,30 @@ const middleware = createResolveTenantMiddleware({
51
51
  });
52
52
  ```
53
53
 
54
+ The chain's context type is inferred from its resolvers — what the JWT,
55
+ domain and subdomain resolvers read, which `HttpResolverContext` provides — so
56
+ the call needs no type argument and no casts. (Before 1.3 it failed with
57
+ TS2322 unless you wrote `createResolverChain<HttpResolverContext>`; that form
58
+ still works.)
59
+
54
60
  The middleware runs inside the real `@zudojs/http` pipeline without
55
61
  depending on it: headers are read through `request.getHeader()` when present,
56
- and otherwise from a plain object or a `Map`, case-insensitively.
62
+ and otherwise from a plain object or a `Map`, case-insensitively. Its
63
+ `HttpMiddleware` type is assignable to `@zudojs/http`'s, so it goes straight
64
+ into a route's `middleware` list:
65
+
66
+ ```typescript
67
+ router.get("/projects", listProjects, { middleware: [middleware] });
68
+ ```
69
+
70
+ A refusal — 400, 401, 403, 404 — is a `GuardResponse` (`createGuardResponse`
71
+ from `@zudojs/middleware`), which `@zudojs/http` sends with that status. The
72
+ helpers `createBadRequest`, `createUnauthorized`, `createForbidden`,
73
+ `createNotFound`, `createJsonErrorResponse` and `createJsonResponse` all
74
+ return one. They used to return plain `{ status, body, headers }` objects,
75
+ which `@zudojs/http` did not treat as a response, so a request carrying only
76
+ an `x-tenant-id` header was refused (the handler never ran) but answered
77
+ `200` instead of `403`.
57
78
 
58
79
  Read the current tenant anywhere downstream:
59
80
 
@@ -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
@@ -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 Object.freeze({
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.
@@ -7,8 +7,23 @@
7
7
  *
8
8
  * @module http/httpTypes
9
9
  */
10
- /** HTTP middleware signature from @zudojs/http. */
11
- export type HttpMiddleware = (context: HttpMiddlewareContext, next: () => Promise<HttpResponseContext>) => void | Response | HttpResponseContext | Promise<void | Response | HttpResponseContext>;
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;
@@ -8,6 +8,6 @@ export type { TenantGuardMiddlewareOptions } from "./tenancyMiddleware.guard.js"
8
8
  export { TENANT_CLAIMS_STATE_KEY, createHttpResolverContext, } from "./httpResolverContext.js";
9
9
  export type { HttpResolverContext, TenantClaims, } from "./httpResolverContext.js";
10
10
  export { readRequestHeader } from "./httpSupport/index.js";
11
- export { createBadRequest, createForbidden, createJsonErrorResponse, createNotFound, createUnauthorized, } from "./httpHelpers.js";
11
+ export { createBadRequest, createForbidden, createJsonErrorResponse, createJsonResponse, createNotFound, createUnauthorized, } from "./httpHelpers.js";
12
12
  export type * from "./httpTypes.js";
13
13
  //# sourceMappingURL=index.d.ts.map
@@ -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,7 +8,7 @@
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
13
  import { loadResolvedTenant } from "./httpSupport/index.js";
14
14
  import { TenantResolutionConflictError, TenantResolutionError, } from "../tenancyErrors/tenancyError.types.js";
@@ -31,11 +31,7 @@ export function createResolveTenantMiddleware(options) {
31
31
  // Unknown and unavailable tenants get the same answer, so a caller cannot
32
32
  // learn which tenant ids exist or which of them are suspended.
33
33
  const notFound = (resolution) => options.notFoundResponse
34
- ? {
35
- status: 404,
36
- body: options.notFoundResponse(resolution),
37
- headers: { "content-type": "application/json" },
38
- }
34
+ ? createJsonResponse(404, options.notFoundResponse(resolution))
39
35
  : createNotFound("Tenant not found");
40
36
  return async (context, next) => {
41
37
  let resolution;
@@ -98,11 +94,7 @@ export function createRequireTenantMiddleware(options) {
98
94
  return next();
99
95
  if (!tenant) {
100
96
  return options?.deniedResponse
101
- ? {
102
- status: 401,
103
- body: options.deniedResponse(undefined),
104
- headers: { "content-type": "application/json" },
105
- }
97
+ ? createJsonResponse(401, options.deniedResponse(undefined))
106
98
  : createUnauthorized("Tenant context is required");
107
99
  }
108
100
  return next();
@@ -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.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Multi-tenant context and isolation with tenant resolution, AsyncLocalStorage propagation, resolver chains, trust levels, and guard middleware.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -25,15 +25,16 @@
25
25
  "!dist/.tsbuildinfo"
26
26
  ],
27
27
  "dependencies": {
28
- "@zudojs/errors": "1.1.0",
29
- "@zudojs/constants": "1.1.0"
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": "^4.1.11"
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://github.com/oyinlola-tech/zudo#readme",
48
+ "homepage": "https://zudojs.oyinlola.site/docs/packages-tenancy",
48
49
  "bugs": {
49
50
  "url": "https://github.com/oyinlola-tech/zudo/issues"
50
51
  },