@zudojs/tenancy 1.3.2 → 1.4.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.
Files changed (78) hide show
  1. package/README.md +10 -0
  2. package/dist/context/contextManager.core.d.ts +0 -1
  3. package/dist/context/contextManager.core.js +0 -1
  4. package/dist/context/contextStorage.core.d.ts +0 -1
  5. package/dist/context/contextStorage.core.js +0 -1
  6. package/dist/context/index.d.ts +0 -1
  7. package/dist/context/index.js +0 -1
  8. package/dist/http/httpHelpers.d.ts +30 -7
  9. package/dist/http/httpHelpers.js +33 -12
  10. package/dist/http/httpResolverContext.d.ts +0 -1
  11. package/dist/http/httpResolverContext.js +0 -1
  12. package/dist/http/httpSupport/httpRequest.helper.d.ts +0 -1
  13. package/dist/http/httpSupport/httpRequest.helper.js +0 -1
  14. package/dist/http/httpSupport/index.d.ts +0 -1
  15. package/dist/http/httpSupport/index.js +0 -1
  16. package/dist/http/httpSupport/tenancyMiddleware.lookup.d.ts +0 -1
  17. package/dist/http/httpSupport/tenancyMiddleware.lookup.js +0 -1
  18. package/dist/http/httpSupport/tenancyMiddleware.resolver.d.ts +0 -1
  19. package/dist/http/httpSupport/tenancyMiddleware.resolver.js +0 -1
  20. package/dist/http/httpTypes.d.ts +0 -1
  21. package/dist/http/httpTypes.js +0 -1
  22. package/dist/http/index.d.ts +2 -2
  23. package/dist/http/index.js +1 -2
  24. package/dist/http/tenancyMiddleware.core.d.ts +4 -2
  25. package/dist/http/tenancyMiddleware.core.js +2 -3
  26. package/dist/http/tenancyMiddleware.guard.d.ts +0 -1
  27. package/dist/http/tenancyMiddleware.guard.js +2 -3
  28. package/dist/index.d.ts +0 -1
  29. package/dist/index.js +0 -1
  30. package/dist/repository/index.d.ts +0 -1
  31. package/dist/repository/index.js +0 -1
  32. package/dist/repository/repository.core.d.ts +0 -1
  33. package/dist/repository/repository.core.js +0 -1
  34. package/dist/repository/tenantManager.core.d.ts +0 -1
  35. package/dist/repository/tenantManager.core.js +0 -1
  36. package/dist/resolver/index.d.ts +0 -1
  37. package/dist/resolver/index.js +0 -1
  38. package/dist/resolver/resolverChain.core.d.ts +0 -1
  39. package/dist/resolver/resolverChain.core.js +0 -1
  40. package/dist/resolver/resolvers/domainResolver.core.d.ts +0 -1
  41. package/dist/resolver/resolvers/domainResolver.core.js +0 -1
  42. package/dist/resolver/resolvers/headerResolver.core.d.ts +0 -1
  43. package/dist/resolver/resolvers/headerResolver.core.js +0 -1
  44. package/dist/resolver/resolvers/index.d.ts +0 -1
  45. package/dist/resolver/resolvers/index.js +0 -1
  46. package/dist/resolver/resolvers/jwtResolver.core.d.ts +0 -1
  47. package/dist/resolver/resolvers/jwtResolver.core.js +0 -1
  48. package/dist/resolver/resolvers/pathResolver.core.d.ts +0 -1
  49. package/dist/resolver/resolvers/pathResolver.core.js +0 -1
  50. package/dist/resolver/resolvers/subdomainResolver.core.d.ts +0 -1
  51. package/dist/resolver/resolvers/subdomainResolver.core.js +0 -1
  52. package/dist/security/guard.core.d.ts +0 -1
  53. package/dist/security/guard.core.js +0 -1
  54. package/dist/security/index.d.ts +1 -1
  55. package/dist/security/index.js +1 -1
  56. package/dist/security/tenantCache.helper.d.ts +49 -0
  57. package/dist/security/tenantCache.helper.js +54 -0
  58. package/dist/tenancyErrors/index.d.ts +0 -1
  59. package/dist/tenancyErrors/index.js +0 -1
  60. package/dist/tenancyErrors/tenancyError.base.d.ts +0 -1
  61. package/dist/tenancyErrors/tenancyError.base.js +0 -1
  62. package/dist/tenancyErrors/tenancyError.types.d.ts +0 -1
  63. package/dist/tenancyErrors/tenancyError.types.js +0 -1
  64. package/dist/tenancyTypes/index.d.ts +0 -1
  65. package/dist/tenancyTypes/index.js +0 -1
  66. package/dist/tenancyTypes/repositoryTypes.d.ts +0 -1
  67. package/dist/tenancyTypes/repositoryTypes.js +0 -1
  68. package/dist/tenancyTypes/resolverTypes.d.ts +0 -1
  69. package/dist/tenancyTypes/resolverTypes.js +0 -1
  70. package/dist/tenancyTypes/tenantIdentity.d.ts +0 -1
  71. package/dist/tenancyTypes/tenantIdentity.js +0 -1
  72. package/dist/tenancyTypes/tenantInterface.d.ts +0 -1
  73. package/dist/tenancyTypes/tenantInterface.js +0 -1
  74. package/dist/utils/index.d.ts +0 -1
  75. package/dist/utils/index.js +0 -1
  76. package/dist/utils/utils.helper.d.ts +0 -1
  77. package/dist/utils/utils.helper.js +0 -1
  78. package/package.json +4 -4
package/README.md CHANGED
@@ -149,8 +149,18 @@ explicitly on routes where something else ties the tenant to the principal.
149
149
  - Tenant ids are normalized (NFKC, trimmed, lowercased) and constrained to
150
150
  `^[a-z0-9][a-z0-9_-]*$`, so an id cannot forge a separator in a cache key,
151
151
  a schema name, or a path. `tenantKey` escapes its segments as well.
152
+ - For `@zudojs/cache`, use `createTenantCacheScope(tenantId, key)` rather than
153
+ `createTenantCacheKey`: the cache refuses `:` inside a key part (that is how
154
+ it stops a raw key from forging a namespace), so `tenant:acme:totals` throws
155
+ `ERR_INVALID_INPUT` there. The scope helper returns
156
+ `{ namespace: "tenant.acme", key: "totals" }`, and the cache's namespace is
157
+ the isolation boundary:
158
+ `await cache.get(scope.key, { namespace: scope.namespace })`.
152
159
  - Non-active tenants are refused during resolution. Pass `allowInactive` on
153
160
  routes that exist to serve suspended tenants.
161
+ - Every refusal body is `{ "error": message, "code": ERR_TENANT_* }`, the
162
+ shape `@zudojs/http` uses, so a client can tell `ERR_TENANT_NOT_FOUND` from
163
+ an application 404. The codes are exported as `TENANCY_RESPONSE_CODE`.
154
164
  - An unknown tenant and a non-active one get the same `404 Tenant not found`,
155
165
  and the guard middleware answers `403 Tenant is not available` without the
156
166
  id or status, so a caller cannot enumerate tenants or learn which are
@@ -53,4 +53,3 @@ export declare function createContextManager(options: ContextManagerOptions): {
53
53
  */
54
54
  runAs<T>(tenant: Tenant, callback: () => T): T;
55
55
  };
56
- //# sourceMappingURL=contextManager.core.d.ts.map
@@ -85,4 +85,3 @@ export function createContextManager(options) {
85
85
  },
86
86
  };
87
87
  }
88
- //# sourceMappingURL=contextManager.core.js.map
@@ -21,4 +21,3 @@ export declare function getDefaultStorage(): TenantContextStorage;
21
21
  * Reset the default storage (useful for testing).
22
22
  */
23
23
  export declare function resetDefaultStorage(): void;
24
- //# sourceMappingURL=contextStorage.core.d.ts.map
@@ -35,4 +35,3 @@ export function getDefaultStorage() {
35
35
  export function resetDefaultStorage() {
36
36
  defaultStorage = undefined;
37
37
  }
38
- //# sourceMappingURL=contextStorage.core.js.map
@@ -5,4 +5,3 @@
5
5
  */
6
6
  export { createTenantContextStorage, getDefaultStorage, resetDefaultStorage, type TenantContextStorage, } from "./contextStorage.core.js";
7
7
  export { createContextManager, type ContextManagerOptions, } from "./contextManager.core.js";
8
- //# sourceMappingURL=index.d.ts.map
@@ -5,4 +5,3 @@
5
5
  */
6
6
  export { createTenantContextStorage, getDefaultStorage, resetDefaultStorage, } from "./contextStorage.core.js";
7
7
  export { createContextManager, } from "./contextManager.core.js";
8
- //# sourceMappingURL=index.js.map
@@ -9,28 +9,51 @@
9
9
  * @module http/httpHelpers
10
10
  */
11
11
  import { type GuardResponse } from "@zudojs/middleware";
12
+ /**
13
+ * Machine-readable `code` carried by every refusal body the tenancy
14
+ * middleware produces, alongside `error`. They are tenant-specific on
15
+ * purpose: a client (or a test) can tell "no tenant" from an application's
16
+ * own `404`, which `{ "error": "Tenant not found" }` alone could not.
17
+ */
18
+ export declare const TENANCY_RESPONSE_CODE: Readonly<{
19
+ /** The request names no tenant, or an unknown or non-active one. */
20
+ readonly NOT_FOUND: "ERR_TENANT_NOT_FOUND";
21
+ /** A tenant was resolved but the route requires one it lacks. */
22
+ readonly REQUIRED: "ERR_TENANT_REQUIRED";
23
+ /** The route forbids tenant context, or the resolution is not trusted. */
24
+ readonly FORBIDDEN: "ERR_TENANT_FORBIDDEN";
25
+ /** The tenant exists but is not active (guard middleware). */
26
+ readonly UNAVAILABLE: "ERR_TENANT_UNAVAILABLE";
27
+ /** Two request sources named different tenants. */
28
+ readonly RESOLUTION_CONFLICT: "ERR_TENANT_RESOLUTION_CONFLICT";
29
+ /** A resolver rejected a credential or could not run. */
30
+ readonly RESOLUTION_FAILED: "ERR_TENANT_RESOLUTION_FAILED";
31
+ }>;
32
+ /** One of the {@link TENANCY_RESPONSE_CODE} values. */
33
+ export type TenancyResponseCode = (typeof TENANCY_RESPONSE_CODE)[keyof typeof TENANCY_RESPONSE_CODE];
12
34
  /**
13
35
  * Create a JSON response with a caller-built body.
14
36
  */
15
37
  export declare function createJsonResponse(status: number, body: unknown): GuardResponse;
16
38
  /**
17
- * Create a JSON error response: `{ "error": message }`.
39
+ * Create a JSON error response: `{ "error": message, "code": code }`, the
40
+ * shape `@zudojs/http` uses for thrown errors. `code` is omitted only when
41
+ * not given.
18
42
  */
19
- export declare function createJsonErrorResponse(status: number, message: string): GuardResponse;
43
+ export declare function createJsonErrorResponse(status: number, message: string, code?: string): GuardResponse;
20
44
  /**
21
45
  * Create a 400 Bad Request response.
22
46
  */
23
- export declare function createBadRequest(message: string): GuardResponse;
47
+ export declare function createBadRequest(message: string, code?: string): GuardResponse;
24
48
  /**
25
49
  * Create a 401 Unauthorized response.
26
50
  */
27
- export declare function createUnauthorized(message: string): GuardResponse;
51
+ export declare function createUnauthorized(message: string, code?: string): GuardResponse;
28
52
  /**
29
53
  * Create a 403 Forbidden response.
30
54
  */
31
- export declare function createForbidden(message: string): GuardResponse;
55
+ export declare function createForbidden(message: string, code?: string): GuardResponse;
32
56
  /**
33
57
  * Create a 404 Not Found response.
34
58
  */
35
- export declare function createNotFound(message: string): GuardResponse;
36
- //# sourceMappingURL=httpHelpers.d.ts.map
59
+ export declare function createNotFound(message: string, code?: string): GuardResponse;
@@ -10,6 +10,26 @@
10
10
  */
11
11
  import { createGuardResponse } from "@zudojs/middleware";
12
12
  const JSON_HEADERS = { "content-type": "application/json" };
13
+ /**
14
+ * Machine-readable `code` carried by every refusal body the tenancy
15
+ * middleware produces, alongside `error`. They are tenant-specific on
16
+ * purpose: a client (or a test) can tell "no tenant" from an application's
17
+ * own `404`, which `{ "error": "Tenant not found" }` alone could not.
18
+ */
19
+ export const TENANCY_RESPONSE_CODE = Object.freeze({
20
+ /** The request names no tenant, or an unknown or non-active one. */
21
+ NOT_FOUND: "ERR_TENANT_NOT_FOUND",
22
+ /** A tenant was resolved but the route requires one it lacks. */
23
+ REQUIRED: "ERR_TENANT_REQUIRED",
24
+ /** The route forbids tenant context, or the resolution is not trusted. */
25
+ FORBIDDEN: "ERR_TENANT_FORBIDDEN",
26
+ /** The tenant exists but is not active (guard middleware). */
27
+ UNAVAILABLE: "ERR_TENANT_UNAVAILABLE",
28
+ /** Two request sources named different tenants. */
29
+ RESOLUTION_CONFLICT: "ERR_TENANT_RESOLUTION_CONFLICT",
30
+ /** A resolver rejected a credential or could not run. */
31
+ RESOLUTION_FAILED: "ERR_TENANT_RESOLUTION_FAILED",
32
+ });
13
33
  /**
14
34
  * Create a JSON response with a caller-built body.
15
35
  */
@@ -17,33 +37,34 @@ export function createJsonResponse(status, body) {
17
37
  return createGuardResponse({ status, body, headers: JSON_HEADERS });
18
38
  }
19
39
  /**
20
- * Create a JSON error response: `{ "error": message }`.
40
+ * Create a JSON error response: `{ "error": message, "code": code }`, the
41
+ * shape `@zudojs/http` uses for thrown errors. `code` is omitted only when
42
+ * not given.
21
43
  */
22
- export function createJsonErrorResponse(status, message) {
23
- return createJsonResponse(status, { error: message });
44
+ export function createJsonErrorResponse(status, message, code) {
45
+ return createJsonResponse(status, code === undefined ? { error: message } : { error: message, code });
24
46
  }
25
47
  /**
26
48
  * Create a 400 Bad Request response.
27
49
  */
28
- export function createBadRequest(message) {
29
- return createJsonErrorResponse(400, message);
50
+ export function createBadRequest(message, code = TENANCY_RESPONSE_CODE.RESOLUTION_FAILED) {
51
+ return createJsonErrorResponse(400, message, code);
30
52
  }
31
53
  /**
32
54
  * Create a 401 Unauthorized response.
33
55
  */
34
- export function createUnauthorized(message) {
35
- return createJsonErrorResponse(401, message);
56
+ export function createUnauthorized(message, code = TENANCY_RESPONSE_CODE.REQUIRED) {
57
+ return createJsonErrorResponse(401, message, code);
36
58
  }
37
59
  /**
38
60
  * Create a 403 Forbidden response.
39
61
  */
40
- export function createForbidden(message) {
41
- return createJsonErrorResponse(403, message);
62
+ export function createForbidden(message, code = TENANCY_RESPONSE_CODE.FORBIDDEN) {
63
+ return createJsonErrorResponse(403, message, code);
42
64
  }
43
65
  /**
44
66
  * Create a 404 Not Found response.
45
67
  */
46
- export function createNotFound(message) {
47
- return createJsonErrorResponse(404, message);
68
+ export function createNotFound(message, code = TENANCY_RESPONSE_CODE.NOT_FOUND) {
69
+ return createJsonErrorResponse(404, message, code);
48
70
  }
49
- //# sourceMappingURL=httpHelpers.js.map
@@ -42,4 +42,3 @@ export declare const TENANT_CLAIMS_STATE_KEY = "tenancy:claims";
42
42
  * @returns An accessor object every shipped resolver understands.
43
43
  */
44
44
  export declare function createHttpResolverContext<Context extends HttpMiddlewareContext = HttpMiddlewareContext>(context: Context, getClaims?: TenantClaimsReader<Context>): HttpResolverContext;
45
- //# sourceMappingURL=httpResolverContext.d.ts.map
@@ -38,4 +38,3 @@ export function createHttpResolverContext(context, getClaims) {
38
38
  },
39
39
  };
40
40
  }
41
- //# sourceMappingURL=httpResolverContext.js.map
@@ -14,4 +14,3 @@ import type { HttpRequestContext } from "../httpTypes.js";
14
14
  * then a plain object — each case-insensitive.
15
15
  */
16
16
  export declare function readRequestHeader(request: HttpRequestContext, name: string): string | undefined;
17
- //# sourceMappingURL=httpRequest.helper.d.ts.map
@@ -45,4 +45,3 @@ export function readRequestHeader(request, name) {
45
45
  }
46
46
  return readBag(request.headers, name);
47
47
  }
48
- //# sourceMappingURL=httpRequest.helper.js.map
@@ -8,4 +8,3 @@
8
8
  export { readRequestHeader } from "./httpRequest.helper.js";
9
9
  export { loadResolvedTenant } from "./tenancyMiddleware.lookup.js";
10
10
  export { toTenantResolver, type TenantResolverSource, } from "./tenancyMiddleware.resolver.js";
11
- //# sourceMappingURL=index.d.ts.map
@@ -8,4 +8,3 @@
8
8
  export { readRequestHeader } from "./httpRequest.helper.js";
9
9
  export { loadResolvedTenant } from "./tenancyMiddleware.lookup.js";
10
10
  export { toTenantResolver, } from "./tenancyMiddleware.resolver.js";
11
- //# sourceMappingURL=index.js.map
@@ -16,4 +16,3 @@ import type { TenantRepository } from "../../tenancyTypes/repositoryTypes.js";
16
16
  * every tenant whose id differs from its slug was unreachable.
17
17
  */
18
18
  export declare function loadResolvedTenant(repository: TenantRepository, resolution: TenantResolution, slugLookup: boolean): Promise<Tenant | undefined>;
19
- //# sourceMappingURL=tenancyMiddleware.lookup.d.ts.map
@@ -22,4 +22,3 @@ export async function loadResolvedTenant(repository, resolution, slugLookup) {
22
22
  return undefined;
23
23
  return repository.findBySlug?.(resolution.tenantId);
24
24
  }
25
- //# sourceMappingURL=tenancyMiddleware.lookup.js.map
@@ -27,4 +27,3 @@ export type TenantResolverSource<Context> = TenantResolver<Context> | TenantReso
27
27
  * @returns A resolver that yields the winning `TenantResolution`.
28
28
  */
29
29
  export declare function toTenantResolver<Context>(source: TenantResolverSource<Context>): TenantResolver<Context>;
30
- //# sourceMappingURL=tenancyMiddleware.resolver.d.ts.map
@@ -22,4 +22,3 @@ function isResolverChain(source) {
22
22
  return (typeof source.asResolver ===
23
23
  "function");
24
24
  }
25
- //# sourceMappingURL=tenancyMiddleware.resolver.js.map
@@ -65,4 +65,3 @@ export interface HttpMiddlewareState {
65
65
  get<T = unknown>(key: string): T | undefined;
66
66
  set<T = unknown>(key: string, value: T): void;
67
67
  }
68
- //# sourceMappingURL=httpTypes.d.ts.map
@@ -8,4 +8,3 @@
8
8
  * @module http/httpTypes
9
9
  */
10
10
  export {};
11
- //# sourceMappingURL=httpTypes.js.map
@@ -9,6 +9,6 @@ export { TENANT_CLAIMS_STATE_KEY, createHttpResolverContext, } from "./httpResol
9
9
  export type { HttpResolverContext, TenantClaims, TenantClaimsReader, } from "./httpResolverContext.js";
10
10
  export { readRequestHeader } from "./httpSupport/index.js";
11
11
  export type { TenantResolverSource } from "./httpSupport/index.js";
12
- export { createBadRequest, createForbidden, createJsonErrorResponse, createJsonResponse, createNotFound, createUnauthorized, } from "./httpHelpers.js";
12
+ export { createBadRequest, createForbidden, createJsonErrorResponse, createJsonResponse, createNotFound, createUnauthorized, TENANCY_RESPONSE_CODE, } from "./httpHelpers.js";
13
+ export type { TenancyResponseCode } from "./httpHelpers.js";
13
14
  export type * from "./httpTypes.js";
14
- //# sourceMappingURL=index.d.ts.map
@@ -5,5 +5,4 @@ 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, createJsonResponse, createNotFound, createUnauthorized, } from "./httpHelpers.js";
9
- //# sourceMappingURL=index.js.map
8
+ export { createBadRequest, createForbidden, createJsonErrorResponse, createJsonResponse, createNotFound, createUnauthorized, TENANCY_RESPONSE_CODE, } from "./httpHelpers.js";
@@ -74,7 +74,10 @@ export interface ResolveTenantMiddlewareOptions<Context extends HttpMiddlewareCo
74
74
  * `tenancy:claims` state key. May be typed with `@zudojs/http`'s context.
75
75
  */
76
76
  readonly getClaims?: TenantClaimsReader<Context>;
77
- /** Custom error response for missing tenant. */
77
+ /**
78
+ * Custom body for the 404 answered when no usable tenant resolves. The
79
+ * default is `{ error: "Tenant not found", code: "ERR_TENANT_NOT_FOUND" }`.
80
+ */
78
81
  readonly notFoundResponse?: (resolution: TenantResolution | undefined) => unknown;
79
82
  }
80
83
  /** Options for the require tenant middleware. */
@@ -98,4 +101,3 @@ export declare function createResolveTenantMiddleware<Context extends HttpMiddle
98
101
  * Must run after `createResolveTenantMiddleware`.
99
102
  */
100
103
  export declare function createRequireTenantMiddleware(options?: RequireTenantMiddlewareOptions): HttpMiddleware;
101
- //# sourceMappingURL=tenancyMiddleware.core.d.ts.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, createJsonResponse, createNotFound, createUnauthorized, } from "./httpHelpers.js";
11
+ import { createBadRequest, createForbidden, createJsonResponse, createNotFound, createUnauthorized, TENANCY_RESPONSE_CODE, } from "./httpHelpers.js";
12
12
  import { meetsTrustLevel } from "../security/guard.core.js";
13
13
  import { loadResolvedTenant, toTenantResolver, } from "./httpSupport/index.js";
14
14
  import { TenantResolutionConflictError, TenantResolutionError, } from "../tenancyErrors/tenancyError.types.js";
@@ -45,7 +45,7 @@ export function createResolveTenantMiddleware(options) {
45
45
  // carrying the framework's own error text; both cases are a refusal
46
46
  // the caller caused, and neither may fall through to `next()`.
47
47
  if (error instanceof TenantResolutionConflictError) {
48
- return createForbidden("Tenant could not be established: request sources name different tenants");
48
+ return createForbidden("Tenant could not be established: request sources name different tenants", TENANCY_RESPONSE_CODE.RESOLUTION_CONFLICT);
49
49
  }
50
50
  if (error instanceof TenantResolutionError) {
51
51
  return createBadRequest("Tenant could not be resolved for this request");
@@ -101,4 +101,3 @@ export function createRequireTenantMiddleware(options) {
101
101
  return next();
102
102
  };
103
103
  }
104
- //# sourceMappingURL=tenancyMiddleware.core.js.map
@@ -32,4 +32,3 @@ export declare function createTenantGuardMiddleware(options?: TenantGuardMiddlew
32
32
  * into AsyncLocalStorage for downstream handlers.
33
33
  */
34
34
  export declare function createTenantPropagationMiddleware(storage: TenantContextStorage): HttpMiddleware;
35
- //# sourceMappingURL=tenancyMiddleware.guard.d.ts.map
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * @module http/tenancyMiddleware.guard
5
5
  */
6
- import { createForbidden } from "./httpHelpers.js";
6
+ import { createForbidden, TENANCY_RESPONSE_CODE } from "./httpHelpers.js";
7
7
  import { TENANT_STATE_KEY, TENANT_CONTEXT_STATE_KEY, } from "./tenancyMiddleware.core.js";
8
8
  // ─── Middleware Factories ──────────────────────────────────────────────────
9
9
  /**
@@ -24,7 +24,7 @@ export function createTenantGuardMiddleware(options = {}) {
24
24
  // One answer for "gone" and "not active", naming neither the tenant nor
25
25
  // its status: the body goes to the client, which must not learn either.
26
26
  if (!current || current.status !== "active") {
27
- return createForbidden("Tenant is not available");
27
+ return createForbidden("Tenant is not available", TENANCY_RESPONSE_CODE.UNAVAILABLE);
28
28
  }
29
29
  return next();
30
30
  };
@@ -47,4 +47,3 @@ export function createTenantPropagationMiddleware(storage) {
47
47
  return next();
48
48
  };
49
49
  }
50
- //# sourceMappingURL=tenancyMiddleware.guard.js.map
package/dist/index.d.ts CHANGED
@@ -16,4 +16,3 @@ export * from "./repository/index.js";
16
16
  export * from "./security/index.js";
17
17
  export * from "./utils/index.js";
18
18
  export * from "./http/index.js";
19
- //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -16,4 +16,3 @@ export * from "./repository/index.js";
16
16
  export * from "./security/index.js";
17
17
  export * from "./utils/index.js";
18
18
  export * from "./http/index.js";
19
- //# sourceMappingURL=index.js.map
@@ -6,4 +6,3 @@
6
6
  export { createMemoryTenantRepository, createDomainRegistry, } from "./repository.core.js";
7
7
  export type { MemoryTenantRepository, TenantWithDomains, } from "./repository.core.js";
8
8
  export { DEFAULT_TENANT_CACHE_TTL_MS, createTenantManager, type TenantManagerOptions, } from "./tenantManager.core.js";
9
- //# sourceMappingURL=index.d.ts.map
@@ -5,4 +5,3 @@
5
5
  */
6
6
  export { createMemoryTenantRepository, createDomainRegistry, } from "./repository.core.js";
7
7
  export { DEFAULT_TENANT_CACHE_TTL_MS, createTenantManager, } from "./tenantManager.core.js";
8
- //# sourceMappingURL=index.js.map
@@ -55,4 +55,3 @@ export declare function createDomainRegistry(): {
55
55
  resolve(domain: string): TenantId | undefined;
56
56
  all(): readonly TenantDomain[];
57
57
  };
58
- //# sourceMappingURL=repository.core.d.ts.map
@@ -116,4 +116,3 @@ export function createDomainRegistry() {
116
116
  },
117
117
  };
118
118
  }
119
- //# sourceMappingURL=repository.core.js.map
@@ -61,4 +61,3 @@ export declare function createTenantManager(options: TenantManagerOptions): {
61
61
  */
62
62
  invalidate(id: TenantId): Promise<void>;
63
63
  };
64
- //# sourceMappingURL=tenantManager.core.d.ts.map
@@ -100,4 +100,3 @@ export function createTenantManager(options) {
100
100
  },
101
101
  };
102
102
  }
103
- //# sourceMappingURL=tenantManager.core.js.map
@@ -6,4 +6,3 @@
6
6
  export { createResolverChain } from "./resolverChain.core.js";
7
7
  export type { ResolverChainContext, TenantResolverChain, } from "./resolverChain.core.js";
8
8
  export * from "./resolvers/index.js";
9
- //# sourceMappingURL=index.d.ts.map
@@ -5,4 +5,3 @@
5
5
  */
6
6
  export { createResolverChain } from "./resolverChain.core.js";
7
7
  export * from "./resolvers/index.js";
8
- //# sourceMappingURL=index.js.map
@@ -46,4 +46,3 @@ export type ResolverChainContext<Resolvers extends readonly TenantResolver<never
46
46
  export declare function createResolverChain<const Resolvers extends readonly TenantResolver<never>[]>(resolvers: Resolvers, options?: ResolverChainOptions): TenantResolverChain<ResolverChainContext<Resolvers>>;
47
47
  export declare function createResolverChain<Context = unknown>(resolvers: readonly TenantResolver<Context>[], options?: ResolverChainOptions): TenantResolverChain<Context>;
48
48
  export {};
49
- //# sourceMappingURL=resolverChain.core.d.ts.map
@@ -68,4 +68,3 @@ export function createResolverChain(resolvers, options) {
68
68
  };
69
69
  return chain;
70
70
  }
71
- //# sourceMappingURL=resolverChain.core.js.map
@@ -32,4 +32,3 @@ export interface DomainResolverOptions {
32
32
  * so the chain moves on.
33
33
  */
34
34
  export declare function createDomainResolver(options: DomainResolverOptions): TenantResolver<DomainContext>;
35
- //# sourceMappingURL=domainResolver.core.d.ts.map
@@ -30,4 +30,3 @@ export function createDomainResolver(options) {
30
30
  },
31
31
  };
32
32
  }
33
- //# sourceMappingURL=domainResolver.core.js.map
@@ -26,4 +26,3 @@ export interface HeaderResolverOptions {
26
26
  * Create a tenant resolver that reads from an HTTP header.
27
27
  */
28
28
  export declare function createHeaderResolver(options?: HeaderResolverOptions): TenantResolver<HeaderContext>;
29
- //# sourceMappingURL=headerResolver.core.d.ts.map
@@ -22,4 +22,3 @@ export function createHeaderResolver(options) {
22
22
  },
23
23
  };
24
24
  }
25
- //# sourceMappingURL=headerResolver.core.js.map
@@ -13,4 +13,3 @@ export { createPathResolver } from "./pathResolver.core.js";
13
13
  export type { PathContext, PathResolverOptions } from "./pathResolver.core.js";
14
14
  export { createJwtResolver } from "./jwtResolver.core.js";
15
15
  export type { JwtContext, JwtResolverOptions } from "./jwtResolver.core.js";
16
- //# sourceMappingURL=index.d.ts.map
@@ -8,4 +8,3 @@ export { createSubdomainResolver } from "./subdomainResolver.core.js";
8
8
  export { createDomainResolver } from "./domainResolver.core.js";
9
9
  export { createPathResolver } from "./pathResolver.core.js";
10
10
  export { createJwtResolver } from "./jwtResolver.core.js";
11
- //# sourceMappingURL=index.js.map
@@ -24,4 +24,3 @@ export interface JwtResolverOptions {
24
24
  */
25
25
  export declare function createJwtResolver(options?: JwtResolverOptions): TenantResolver<JwtContext>;
26
26
  export {};
27
- //# sourceMappingURL=jwtResolver.core.d.ts.map
@@ -24,4 +24,3 @@ export function createJwtResolver(options) {
24
24
  },
25
25
  };
26
26
  }
27
- //# sourceMappingURL=jwtResolver.core.js.map
@@ -25,4 +25,3 @@ export interface PathResolverOptions {
25
25
  * Example: "/acme/users" → tenant "acme"
26
26
  */
27
27
  export declare function createPathResolver(options?: PathResolverOptions): TenantResolver<PathContext>;
28
- //# sourceMappingURL=pathResolver.core.d.ts.map
@@ -41,4 +41,3 @@ export function createPathResolver(options) {
41
41
  },
42
42
  };
43
43
  }
44
- //# sourceMappingURL=pathResolver.core.js.map
@@ -36,4 +36,3 @@ export declare function hostnameOf(host: string): string | undefined;
36
36
  * Example: "acme.example.com" → tenant "acme"
37
37
  */
38
38
  export declare function createSubdomainResolver(options?: SubdomainResolverOptions): TenantResolver<SubdomainContext>;
39
- //# sourceMappingURL=subdomainResolver.core.d.ts.map
@@ -64,4 +64,3 @@ export function createSubdomainResolver(options) {
64
64
  },
65
65
  };
66
66
  }
67
- //# sourceMappingURL=subdomainResolver.core.js.map
@@ -49,4 +49,3 @@ export declare function tenantKey(tenantId: TenantId, key: string, separator?: s
49
49
  * Create a tenant cache key.
50
50
  */
51
51
  export declare function createTenantCacheKey(tenantId: TenantId, key: string): string;
52
- //# sourceMappingURL=guard.core.d.ts.map
@@ -110,4 +110,3 @@ export function tenantKey(tenantId, key, separator = ":") {
110
110
  export function createTenantCacheKey(tenantId, key) {
111
111
  return tenantKey(tenantId, key, ":");
112
112
  }
113
- //# sourceMappingURL=guard.core.js.map
@@ -4,4 +4,4 @@
4
4
  * @module security
5
5
  */
6
6
  export { getDefaultTrust, meetsTrustLevel, assertTenantUsable, assertTenantOwnership, assertTrustLevel, assertSameTenant, tenantKey, createTenantCacheKey, } from "./guard.core.js";
7
- //# sourceMappingURL=index.d.ts.map
7
+ export { createTenantCacheNamespace, createTenantCacheScope, TENANT_CACHE_PART_PATTERN, type TenantCacheScope, } from "./tenantCache.helper.js";
@@ -4,4 +4,4 @@
4
4
  * @module security
5
5
  */
6
6
  export { getDefaultTrust, meetsTrustLevel, assertTenantUsable, assertTenantOwnership, assertTrustLevel, assertSameTenant, tenantKey, createTenantCacheKey, } from "./guard.core.js";
7
- //# sourceMappingURL=index.js.map
7
+ export { createTenantCacheNamespace, createTenantCacheScope, TENANT_CACHE_PART_PATTERN, } from "./tenantCache.helper.js";
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Tenant scoping for `@zudojs/cache`.
3
+ *
4
+ * @module security/tenantCache
5
+ *
6
+ * `createTenantCacheKey` builds `tenant:<id>:<key>`, which `@zudojs/cache`
7
+ * refuses: its key parts may not contain the `:` separator, precisely so a
8
+ * raw key cannot forge a namespace. The cache's own tenant boundary is the
9
+ * `namespace` option, so the compatible shape is a namespace derived from
10
+ * the tenant id plus the caller's key, passed separately:
11
+ *
12
+ * ```ts
13
+ * const scope = createTenantCacheScope(tenantId, "dashboard.totals");
14
+ * await cache.get(scope.key, { namespace: scope.namespace });
15
+ * ```
16
+ */
17
+ import type { TenantId } from "../tenancyTypes/tenantIdentity.js";
18
+ /**
19
+ * The alphabet `@zudojs/cache` accepts for a key part
20
+ * (`CACHE_KEY_PATTERN` there). Mirrored rather than imported: tenancy sits
21
+ * below cache in the dependency tiers.
22
+ */
23
+ export declare const TENANT_CACHE_PART_PATTERN: RegExp;
24
+ /** A tenant-scoped cache address: pass both halves to `@zudojs/cache`. */
25
+ export interface TenantCacheScope {
26
+ /** Cache namespace for the tenant, e.g. `tenant.kola-motors`. */
27
+ readonly namespace: string;
28
+ /** The caller's key, unchanged. */
29
+ readonly key: string;
30
+ }
31
+ /**
32
+ * Cache namespace for a tenant: `tenant.<tenantId>`.
33
+ *
34
+ * Tenant ids are already constrained to `^[a-z0-9][a-z0-9_-]*$`, so the
35
+ * result always satisfies the cache's part alphabet.
36
+ */
37
+ export declare function createTenantCacheNamespace(tenantId: TenantId): string;
38
+ /**
39
+ * Build a tenant-scoped cache address that `@zudojs/cache` accepts.
40
+ *
41
+ * Isolation comes from the namespace, not from string concatenation, so two
42
+ * tenants can never share a fully-qualified key whatever `key` is.
43
+ *
44
+ * @param tenantId - The owning tenant.
45
+ * @param key - The resource key within that tenant. Must match
46
+ * {@link TENANT_CACHE_PART_PATTERN}; in particular it may not contain `:`.
47
+ * @throws {ValidationError} when `key` would fail the cache's validation.
48
+ */
49
+ export declare function createTenantCacheScope(tenantId: TenantId, key: string): TenantCacheScope;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Tenant scoping for `@zudojs/cache`.
3
+ *
4
+ * @module security/tenantCache
5
+ *
6
+ * `createTenantCacheKey` builds `tenant:<id>:<key>`, which `@zudojs/cache`
7
+ * refuses: its key parts may not contain the `:` separator, precisely so a
8
+ * raw key cannot forge a namespace. The cache's own tenant boundary is the
9
+ * `namespace` option, so the compatible shape is a namespace derived from
10
+ * the tenant id plus the caller's key, passed separately:
11
+ *
12
+ * ```ts
13
+ * const scope = createTenantCacheScope(tenantId, "dashboard.totals");
14
+ * await cache.get(scope.key, { namespace: scope.namespace });
15
+ * ```
16
+ */
17
+ import { ValidationError } from "@zudojs/errors";
18
+ /**
19
+ * The alphabet `@zudojs/cache` accepts for a key part
20
+ * (`CACHE_KEY_PATTERN` there). Mirrored rather than imported: tenancy sits
21
+ * below cache in the dependency tiers.
22
+ */
23
+ export const TENANT_CACHE_PART_PATTERN = /^[a-zA-Z0-9._-]+$/;
24
+ /** Prefix that keeps tenant namespaces apart from any other namespace. */
25
+ const NAMESPACE_PREFIX = "tenant.";
26
+ /**
27
+ * Cache namespace for a tenant: `tenant.<tenantId>`.
28
+ *
29
+ * Tenant ids are already constrained to `^[a-z0-9][a-z0-9_-]*$`, so the
30
+ * result always satisfies the cache's part alphabet.
31
+ */
32
+ export function createTenantCacheNamespace(tenantId) {
33
+ return `${NAMESPACE_PREFIX}${tenantId}`;
34
+ }
35
+ /**
36
+ * Build a tenant-scoped cache address that `@zudojs/cache` accepts.
37
+ *
38
+ * Isolation comes from the namespace, not from string concatenation, so two
39
+ * tenants can never share a fully-qualified key whatever `key` is.
40
+ *
41
+ * @param tenantId - The owning tenant.
42
+ * @param key - The resource key within that tenant. Must match
43
+ * {@link TENANT_CACHE_PART_PATTERN}; in particular it may not contain `:`.
44
+ * @throws {ValidationError} when `key` would fail the cache's validation.
45
+ */
46
+ export function createTenantCacheScope(tenantId, key) {
47
+ if (typeof key !== "string" || !TENANT_CACHE_PART_PATTERN.test(key)) {
48
+ throw new ValidationError(`Tenant cache key ${JSON.stringify(key)} must match ${String(TENANT_CACHE_PART_PATTERN)}; use "." or "-" instead of ":" to separate segments.`);
49
+ }
50
+ return Object.freeze({
51
+ namespace: createTenantCacheNamespace(tenantId),
52
+ key,
53
+ });
54
+ }
@@ -5,4 +5,3 @@
5
5
  */
6
6
  export { TenantError } from "./tenancyError.base.js";
7
7
  export { InvalidTenantIdError, TenantNotFoundError, TenantContextMissingError, TenantResolutionError, TenantResolutionConflictError, TenantUnavailableError, TenantAccessDeniedError, TenantTrustLevelError, TenantAlreadyExistsError, TenantIsolationError, } from "./tenancyError.types.js";
8
- //# sourceMappingURL=index.d.ts.map
@@ -5,4 +5,3 @@
5
5
  */
6
6
  export { TenantError } from "./tenancyError.base.js";
7
7
  export { InvalidTenantIdError, TenantNotFoundError, TenantContextMissingError, TenantResolutionError, TenantResolutionConflictError, TenantUnavailableError, TenantAccessDeniedError, TenantTrustLevelError, TenantAlreadyExistsError, TenantIsolationError, } from "./tenancyError.types.js";
8
- //# sourceMappingURL=index.js.map
@@ -14,4 +14,3 @@ export declare class TenantError extends AuthorizationError {
14
14
  readonly cause?: unknown;
15
15
  });
16
16
  }
17
- //# sourceMappingURL=tenancyError.base.d.ts.map
@@ -16,4 +16,3 @@ export class TenantError extends AuthorizationError {
16
16
  });
17
17
  }
18
18
  }
19
- //# sourceMappingURL=tenancyError.base.js.map
@@ -70,4 +70,3 @@ export declare class TenantAlreadyExistsError extends TenantError {
70
70
  export declare class TenantIsolationError extends TenantError {
71
71
  constructor(expected: string, actual: string);
72
72
  }
73
- //# sourceMappingURL=tenancyError.types.d.ts.map
@@ -118,4 +118,3 @@ export class TenantIsolationError extends TenantError {
118
118
  });
119
119
  }
120
120
  }
121
- //# sourceMappingURL=tenancyError.types.js.map
@@ -8,4 +8,3 @@ export { type Tenant, type TenantResolutionSource, type TenantTrustLevel, type T
8
8
  export { type TenantResolution, type TenantResolutionResult, type TenantResolver, type ResolverChainOptions, } from "./resolverTypes.js";
9
9
  export { type TenantRepository, type TenantCache, } from "./repositoryTypes.js";
10
10
  export { type TenantDomain } from "./repositoryTypes.js";
11
- //# sourceMappingURL=index.d.ts.map
@@ -8,4 +8,3 @@ export {} from "./tenantInterface.js";
8
8
  export {} from "./resolverTypes.js";
9
9
  export {} from "./repositoryTypes.js";
10
10
  export {} from "./repositoryTypes.js";
11
- //# sourceMappingURL=index.js.map
@@ -22,4 +22,3 @@ export interface TenantCache {
22
22
  set(tenant: Tenant): Promise<void>;
23
23
  delete(id: TenantId): Promise<void>;
24
24
  }
25
- //# sourceMappingURL=repositoryTypes.d.ts.map
@@ -4,4 +4,3 @@
4
4
  * @module tenancyTypes/repositoryTypes
5
5
  */
6
6
  export {};
7
- //# sourceMappingURL=repositoryTypes.js.map
@@ -43,4 +43,3 @@ export interface ResolverChainOptions {
43
43
  */
44
44
  readonly throwOnConflict?: boolean;
45
45
  }
46
- //# sourceMappingURL=resolverTypes.d.ts.map
@@ -4,4 +4,3 @@
4
4
  * @module tenancyTypes/resolverTypes
5
5
  */
6
6
  export {};
7
- //# sourceMappingURL=resolverTypes.js.map
@@ -48,4 +48,3 @@ export declare function tryCreateTenantId(value: unknown): TenantId | undefined;
48
48
  export declare function isValidTenantId(value: unknown): value is TenantId;
49
49
  /** Tenant lifecycle status. */
50
50
  export type TenantStatus = "provisioning" | "active" | "inactive" | "suspended" | "deleting" | "deleted";
51
- //# sourceMappingURL=tenantIdentity.d.ts.map
@@ -59,4 +59,3 @@ export function tryCreateTenantId(value) {
59
59
  export function isValidTenantId(value) {
60
60
  return tryCreateTenantId(value) !== undefined;
61
61
  }
62
- //# sourceMappingURL=tenantIdentity.js.map
@@ -44,4 +44,3 @@ export type TenantRequirement = "required" | "optional" | "forbidden";
44
44
  export interface TenantResource {
45
45
  readonly tenantId: TenantId;
46
46
  }
47
- //# sourceMappingURL=tenantInterface.d.ts.map
@@ -4,4 +4,3 @@
4
4
  * @module tenancyTypes/tenantInterface
5
5
  */
6
6
  export {};
7
- //# sourceMappingURL=tenantInterface.js.map
@@ -4,4 +4,3 @@
4
4
  * @module utils
5
5
  */
6
6
  export { isTenantActive, sameTenant, summarizeTenant, summarizeContext, } from "./utils.helper.js";
7
- //# sourceMappingURL=index.d.ts.map
@@ -4,4 +4,3 @@
4
4
  * @module utils
5
5
  */
6
6
  export { isTenantActive, sameTenant, summarizeTenant, summarizeContext, } from "./utils.helper.js";
7
- //# sourceMappingURL=index.js.map
@@ -21,4 +21,3 @@ export declare function summarizeTenant(tenant: Tenant): string;
21
21
  * Create a tenant context summary string for logging.
22
22
  */
23
23
  export declare function summarizeContext(ctx: TenantContext): string;
24
- //# sourceMappingURL=utils.helper.d.ts.map
@@ -27,4 +27,3 @@ export function summarizeTenant(tenant) {
27
27
  export function summarizeContext(ctx) {
28
28
  return `TenantContext(${ctx.tenantId}, source=${ctx.source}, trust=${ctx.trust})`;
29
29
  }
30
- //# sourceMappingURL=utils.helper.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zudojs/tenancy",
3
- "version": "1.3.2",
3
+ "version": "1.4.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,9 +25,9 @@
25
25
  "!dist/.tsbuildinfo"
26
26
  ],
27
27
  "dependencies": {
28
- "@zudojs/errors": "1.3.1",
29
- "@zudojs/constants": "1.1.3",
30
- "@zudojs/middleware": "1.1.1"
28
+ "@zudojs/errors": "1.4.0",
29
+ "@zudojs/constants": "1.2.0",
30
+ "@zudojs/middleware": "1.1.3"
31
31
  },
32
32
  "engines": {
33
33
  "node": ">=24.0.0"