@zudojs/tenancy 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +64 -13
  3. package/dist/context/contextManager.core.d.ts +10 -0
  4. package/dist/context/contextManager.core.js +7 -0
  5. package/dist/http/httpResolverContext.d.ts +35 -0
  6. package/dist/http/httpResolverContext.js +40 -0
  7. package/dist/http/index.d.ts +8 -8
  8. package/dist/http/index.js +4 -5
  9. package/dist/http/tenancyMiddleware.core.d.ts +23 -3
  10. package/dist/http/tenancyMiddleware.core.js +55 -35
  11. package/dist/http/tenancyMiddleware.guard.d.ts +13 -5
  12. package/dist/http/tenancyMiddleware.guard.js +11 -3
  13. package/dist/repository/index.d.ts +2 -1
  14. package/dist/repository/index.js +1 -1
  15. package/dist/repository/repository.core.d.ts +38 -7
  16. package/dist/repository/repository.core.js +69 -15
  17. package/dist/repository/tenantManager.core.d.ts +17 -1
  18. package/dist/repository/tenantManager.core.js +30 -5
  19. package/dist/resolver/index.d.ts +1 -0
  20. package/dist/resolver/resolverChain.core.d.ts +22 -7
  21. package/dist/resolver/resolverChain.core.js +45 -13
  22. package/dist/resolver/resolvers/headerResolver.core.d.ts +10 -2
  23. package/dist/resolver/resolvers/headerResolver.core.js +5 -8
  24. package/dist/resolver/resolvers/index.d.ts +9 -5
  25. package/dist/resolver/resolvers/index.js +5 -5
  26. package/dist/resolver/resolvers/jwtResolver.core.d.ts +1 -1
  27. package/dist/resolver/resolvers/jwtResolver.core.js +4 -8
  28. package/dist/resolver/resolvers/pathResolver.core.d.ts +1 -2
  29. package/dist/resolver/resolvers/pathResolver.core.js +5 -7
  30. package/dist/resolver/resolvers/subdomainResolver.core.d.ts +11 -2
  31. package/dist/resolver/resolvers/subdomainResolver.core.js +31 -14
  32. package/dist/security/guard.core.d.ts +19 -0
  33. package/dist/security/guard.core.js +58 -9
  34. package/dist/security/index.d.ts +1 -1
  35. package/dist/security/index.js +1 -1
  36. package/dist/tenancyErrors/index.d.ts +1 -1
  37. package/dist/tenancyErrors/index.js +1 -1
  38. package/dist/tenancyErrors/tenancyError.types.d.ts +11 -6
  39. package/dist/tenancyErrors/tenancyError.types.js +15 -12
  40. package/dist/tenancyTypes/index.d.ts +3 -3
  41. package/dist/tenancyTypes/index.js +2 -2
  42. package/dist/tenancyTypes/repositoryTypes.d.ts +6 -21
  43. package/dist/tenancyTypes/resolverTypes.d.ts +14 -2
  44. package/dist/tenancyTypes/tenantIdentity.d.ts +24 -5
  45. package/dist/tenancyTypes/tenantIdentity.js +54 -4
  46. package/package.json +22 -15
  47. package/dist/.tsbuildinfo +0 -1
  48. package/dist/context/contextManager.core.d.ts.map +0 -1
  49. package/dist/context/contextManager.core.js.map +0 -1
  50. package/dist/context/contextStorage.core.d.ts.map +0 -1
  51. package/dist/context/contextStorage.core.js.map +0 -1
  52. package/dist/context/index.d.ts.map +0 -1
  53. package/dist/context/index.js.map +0 -1
  54. package/dist/http/httpHelpers.d.ts.map +0 -1
  55. package/dist/http/httpHelpers.js.map +0 -1
  56. package/dist/http/httpTypes.d.ts.map +0 -1
  57. package/dist/http/httpTypes.js.map +0 -1
  58. package/dist/http/index.d.ts.map +0 -1
  59. package/dist/http/index.js.map +0 -1
  60. package/dist/http/tenancyMiddleware.core.d.ts.map +0 -1
  61. package/dist/http/tenancyMiddleware.core.js.map +0 -1
  62. package/dist/http/tenancyMiddleware.guard.d.ts.map +0 -1
  63. package/dist/http/tenancyMiddleware.guard.js.map +0 -1
  64. package/dist/index.d.ts.map +0 -1
  65. package/dist/index.js.map +0 -1
  66. package/dist/repository/index.d.ts.map +0 -1
  67. package/dist/repository/index.js.map +0 -1
  68. package/dist/repository/repository.core.d.ts.map +0 -1
  69. package/dist/repository/repository.core.js.map +0 -1
  70. package/dist/repository/tenantManager.core.d.ts.map +0 -1
  71. package/dist/repository/tenantManager.core.js.map +0 -1
  72. package/dist/resolver/index.d.ts.map +0 -1
  73. package/dist/resolver/index.js.map +0 -1
  74. package/dist/resolver/resolverChain.core.d.ts.map +0 -1
  75. package/dist/resolver/resolverChain.core.js.map +0 -1
  76. package/dist/resolver/resolvers/headerResolver.core.d.ts.map +0 -1
  77. package/dist/resolver/resolvers/headerResolver.core.js.map +0 -1
  78. package/dist/resolver/resolvers/index.d.ts.map +0 -1
  79. package/dist/resolver/resolvers/index.js.map +0 -1
  80. package/dist/resolver/resolvers/jwtResolver.core.d.ts.map +0 -1
  81. package/dist/resolver/resolvers/jwtResolver.core.js.map +0 -1
  82. package/dist/resolver/resolvers/pathResolver.core.d.ts.map +0 -1
  83. package/dist/resolver/resolvers/pathResolver.core.js.map +0 -1
  84. package/dist/resolver/resolvers/subdomainResolver.core.d.ts.map +0 -1
  85. package/dist/resolver/resolvers/subdomainResolver.core.js.map +0 -1
  86. package/dist/security/guard.core.d.ts.map +0 -1
  87. package/dist/security/guard.core.js.map +0 -1
  88. package/dist/security/index.d.ts.map +0 -1
  89. package/dist/security/index.js.map +0 -1
  90. package/dist/tenancyErrors/index.d.ts.map +0 -1
  91. package/dist/tenancyErrors/index.js.map +0 -1
  92. package/dist/tenancyErrors/tenancyError.base.d.ts.map +0 -1
  93. package/dist/tenancyErrors/tenancyError.base.js.map +0 -1
  94. package/dist/tenancyErrors/tenancyError.types.d.ts.map +0 -1
  95. package/dist/tenancyErrors/tenancyError.types.js.map +0 -1
  96. package/dist/tenancyTypes/index.d.ts.map +0 -1
  97. package/dist/tenancyTypes/index.js.map +0 -1
  98. package/dist/tenancyTypes/repositoryTypes.d.ts.map +0 -1
  99. package/dist/tenancyTypes/repositoryTypes.js.map +0 -1
  100. package/dist/tenancyTypes/resolverTypes.d.ts.map +0 -1
  101. package/dist/tenancyTypes/resolverTypes.js.map +0 -1
  102. package/dist/tenancyTypes/tenancyOptions.d.ts +0 -37
  103. package/dist/tenancyTypes/tenancyOptions.d.ts.map +0 -1
  104. package/dist/tenancyTypes/tenancyOptions.js +0 -7
  105. package/dist/tenancyTypes/tenancyOptions.js.map +0 -1
  106. package/dist/tenancyTypes/tenantIdentity.d.ts.map +0 -1
  107. package/dist/tenancyTypes/tenantIdentity.js.map +0 -1
  108. package/dist/tenancyTypes/tenantInterface.d.ts.map +0 -1
  109. package/dist/tenancyTypes/tenantInterface.js.map +0 -1
  110. package/dist/utils/index.d.ts.map +0 -1
  111. package/dist/utils/index.js.map +0 -1
  112. package/dist/utils/utils.helper.d.ts.map +0 -1
  113. package/dist/utils/utils.helper.js.map +0 -1
@@ -10,6 +10,12 @@ import type { TenantResolutionSource, TenantTrustLevel } from "../tenancyTypes/t
10
10
  * Get the default trust level for a resolution source.
11
11
  */
12
12
  export declare function getDefaultTrust(source: TenantResolutionSource): TenantTrustLevel;
13
+ /**
14
+ * Compare two trust levels.
15
+ *
16
+ * @returns True when `actual` is at least as trusted as `required`.
17
+ */
18
+ export declare function meetsTrustLevel(actual: TenantTrustLevel, required: TenantTrustLevel): boolean;
13
19
  /**
14
20
  * Validate that a tenant is usable.
15
21
  */
@@ -20,10 +26,23 @@ export declare function assertTenantUsable(tenant: Tenant): void;
20
26
  export declare function assertTenantOwnership(resource: TenantResource, tenantId: TenantId): void;
21
27
  /**
22
28
  * Validate that a trust level is sufficient.
29
+ *
30
+ * @throws {TenantTrustLevelError} when the resolution is not trusted enough.
23
31
  */
24
32
  export declare function assertTrustLevel(actual: TenantTrustLevel, required: TenantTrustLevel, source: TenantResolutionSource): void;
33
+ /**
34
+ * Assert that a caller may act on a tenant other than its own.
35
+ *
36
+ * @throws {TenantAccessDeniedError} when the two tenants differ.
37
+ */
38
+ export declare function assertSameTenant(actual: TenantId, expected: TenantId): void;
25
39
  /**
26
40
  * Create a tenant key for scoped resources.
41
+ *
42
+ * @param tenantId - The owning tenant.
43
+ * @param key - The resource key within that tenant.
44
+ * @param separator - Segment separator. Defaults to ":".
45
+ * @returns A key that no other tenant's key can collide with.
27
46
  */
28
47
  export declare function tenantKey(tenantId: TenantId, key: string, separator?: string): string;
29
48
  /**
@@ -3,25 +3,46 @@
3
3
  *
4
4
  * @module security/guard
5
5
  */
6
- import { TenantAccessDeniedError, TenantIsolationError, TenantUnavailableError, } from "../tenancyErrors/tenancyError.types.js";
7
- /** Trust levels for resolution sources. */
6
+ import { TenantAccessDeniedError, TenantIsolationError, TenantTrustLevelError, TenantUnavailableError, } from "../tenancyErrors/tenancyError.types.js";
7
+ /**
8
+ * Trust levels for resolution sources.
9
+ *
10
+ * `header` is untrusted: the value arrives on the wire under the client's
11
+ * control unless a trusted proxy overwrites it, and this package cannot know
12
+ * whether one does. Applications that can attest to their edge raise it
13
+ * explicitly via `createHeaderResolver({ trust: "verified" })`.
14
+ */
8
15
  const TRUST_MAP = {
9
16
  jwt: "trusted",
10
17
  "api-key": "trusted",
11
18
  manual: "trusted",
12
19
  system: "trusted",
13
- header: "verified",
14
20
  subdomain: "verified",
15
21
  domain: "verified",
22
+ header: "untrusted",
16
23
  path: "untrusted",
17
24
  custom: "untrusted",
18
25
  };
26
+ /** Trust levels in ascending order of confidence. */
27
+ const TRUST_ORDER = [
28
+ "untrusted",
29
+ "verified",
30
+ "trusted",
31
+ ];
19
32
  /**
20
33
  * Get the default trust level for a resolution source.
21
34
  */
22
35
  export function getDefaultTrust(source) {
23
36
  return TRUST_MAP[source] ?? "untrusted";
24
37
  }
38
+ /**
39
+ * Compare two trust levels.
40
+ *
41
+ * @returns True when `actual` is at least as trusted as `required`.
42
+ */
43
+ export function meetsTrustLevel(actual, required) {
44
+ return TRUST_ORDER.indexOf(actual) >= TRUST_ORDER.indexOf(required);
45
+ }
25
46
  /**
26
47
  * Validate that a tenant is usable.
27
48
  */
@@ -40,20 +61,48 @@ export function assertTenantOwnership(resource, tenantId) {
40
61
  }
41
62
  /**
42
63
  * Validate that a trust level is sufficient.
64
+ *
65
+ * @throws {TenantTrustLevelError} when the resolution is not trusted enough.
43
66
  */
44
67
  export function assertTrustLevel(actual, required, source) {
45
- const levels = ["untrusted", "verified", "trusted"];
46
- const actualIdx = levels.indexOf(actual);
47
- const requiredIdx = levels.indexOf(required);
48
- if (actualIdx < requiredIdx) {
49
- throw new TenantAccessDeniedError(`Insufficient trust level from "${source}": need ${required}, got ${actual}`);
68
+ if (!meetsTrustLevel(actual, required)) {
69
+ throw new TenantTrustLevelError(source, required, actual);
70
+ }
71
+ }
72
+ /**
73
+ * Assert that a caller may act on a tenant other than its own.
74
+ *
75
+ * @throws {TenantAccessDeniedError} when the two tenants differ.
76
+ */
77
+ export function assertSameTenant(actual, expected) {
78
+ if (actual !== expected) {
79
+ throw new TenantAccessDeniedError(actual);
50
80
  }
51
81
  }
82
+ /**
83
+ * Escape a key segment so it cannot forge a separator boundary.
84
+ *
85
+ * Tenant ids are already constrained by `createTenantId`, but arbitrary keys
86
+ * are not; without this, a key of `"cache:k"` under tenant `a` collides with
87
+ * key `"k"` under a tenant literally named `a:cache`.
88
+ */
89
+ function escapeSegment(segment, separator) {
90
+ return segment.split(separator).join(`\\${separator}`);
91
+ }
52
92
  /**
53
93
  * Create a tenant key for scoped resources.
94
+ *
95
+ * @param tenantId - The owning tenant.
96
+ * @param key - The resource key within that tenant.
97
+ * @param separator - Segment separator. Defaults to ":".
98
+ * @returns A key that no other tenant's key can collide with.
54
99
  */
55
100
  export function tenantKey(tenantId, key, separator = ":") {
56
- return `tenant${separator}${tenantId}${separator}${key}`;
101
+ return [
102
+ "tenant",
103
+ escapeSegment(tenantId, separator),
104
+ escapeSegment(key, separator),
105
+ ].join(separator);
57
106
  }
58
107
  /**
59
108
  * Create a tenant cache key.
@@ -3,5 +3,5 @@
3
3
  *
4
4
  * @module security
5
5
  */
6
- export { getDefaultTrust, assertTenantUsable, assertTenantOwnership, assertTrustLevel, tenantKey, createTenantCacheKey, } from "./guard.core.js";
6
+ export { getDefaultTrust, meetsTrustLevel, assertTenantUsable, assertTenantOwnership, assertTrustLevel, assertSameTenant, tenantKey, createTenantCacheKey, } from "./guard.core.js";
7
7
  //# sourceMappingURL=index.d.ts.map
@@ -3,5 +3,5 @@
3
3
  *
4
4
  * @module security
5
5
  */
6
- export { getDefaultTrust, assertTenantUsable, assertTenantOwnership, assertTrustLevel, tenantKey, createTenantCacheKey, } from "./guard.core.js";
6
+ export { getDefaultTrust, meetsTrustLevel, assertTenantUsable, assertTenantOwnership, assertTrustLevel, assertSameTenant, tenantKey, createTenantCacheKey, } from "./guard.core.js";
7
7
  //# sourceMappingURL=index.js.map
@@ -4,5 +4,5 @@
4
4
  * @module tenancyErrors
5
5
  */
6
6
  export { TenantError } from "./tenancyError.base.js";
7
- export { InvalidTenantIdError, TenantNotFoundError, TenantContextMissingError, TenantResolutionError, TenantResolutionConflictError, TenantUnavailableError, TenantAccessDeniedError, TenantAlreadyExistsError, TenantProvisioningError, TenantIsolationError, } from "./tenancyError.types.js";
7
+ export { InvalidTenantIdError, TenantNotFoundError, TenantContextMissingError, TenantResolutionError, TenantResolutionConflictError, TenantUnavailableError, TenantAccessDeniedError, TenantTrustLevelError, TenantAlreadyExistsError, TenantIsolationError, } from "./tenancyError.types.js";
8
8
  //# sourceMappingURL=index.d.ts.map
@@ -4,5 +4,5 @@
4
4
  * @module tenancyErrors
5
5
  */
6
6
  export { TenantError } from "./tenancyError.base.js";
7
- export { InvalidTenantIdError, TenantNotFoundError, TenantContextMissingError, TenantResolutionError, TenantResolutionConflictError, TenantUnavailableError, TenantAccessDeniedError, TenantAlreadyExistsError, TenantProvisioningError, TenantIsolationError, } from "./tenancyError.types.js";
7
+ export { InvalidTenantIdError, TenantNotFoundError, TenantContextMissingError, TenantResolutionError, TenantResolutionConflictError, TenantUnavailableError, TenantAccessDeniedError, TenantTrustLevelError, TenantAlreadyExistsError, TenantIsolationError, } from "./tenancyError.types.js";
8
8
  //# sourceMappingURL=index.js.map
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { TenantError } from "./tenancyError.base.js";
7
7
  import type { TenantId, TenantStatus } from "../tenancyTypes/tenantIdentity.js";
8
+ import type { TenantResolutionSource, TenantTrustLevel } from "../tenancyTypes/tenantInterface.js";
8
9
  /**
9
10
  * The provided tenant ID is invalid.
10
11
  */
@@ -48,16 +49,20 @@ export declare class TenantAccessDeniedError extends TenantError {
48
49
  constructor(tenantId: string);
49
50
  }
50
51
  /**
51
- * Tenant already exists.
52
+ * The tenant was resolved from a source that is not trusted enough.
53
+ *
54
+ * Distinct from {@link TenantAccessDeniedError} so the resolution source and
55
+ * the two trust levels stay in structured metadata instead of being formatted
56
+ * into a field that is supposed to hold a tenant id.
52
57
  */
53
- export declare class TenantAlreadyExistsError extends TenantError {
54
- constructor(identifier: string);
58
+ export declare class TenantTrustLevelError extends TenantError {
59
+ constructor(source: TenantResolutionSource, required: TenantTrustLevel, actual: TenantTrustLevel);
55
60
  }
56
61
  /**
57
- * Tenant provisioning failed.
62
+ * Tenant already exists.
58
63
  */
59
- export declare class TenantProvisioningError extends TenantError {
60
- constructor(tenantId: string, cause?: unknown);
64
+ export declare class TenantAlreadyExistsError extends TenantError {
65
+ constructor(identifier: string);
61
66
  }
62
67
  /**
63
68
  * Tenant isolation violation detected.
@@ -81,6 +81,21 @@ export class TenantAccessDeniedError extends TenantError {
81
81
  });
82
82
  }
83
83
  }
84
+ /**
85
+ * The tenant was resolved from a source that is not trusted enough.
86
+ *
87
+ * Distinct from {@link TenantAccessDeniedError} so the resolution source and
88
+ * the two trust levels stay in structured metadata instead of being formatted
89
+ * into a field that is supposed to hold a tenant id.
90
+ */
91
+ export class TenantTrustLevelError extends TenantError {
92
+ constructor(source, required, actual) {
93
+ super(`Insufficient trust level from "${source}": need ${required}, got ${actual}`, {
94
+ code: ErrorCode.ACCESS_DENIED,
95
+ metadata: { source, required, actual },
96
+ });
97
+ }
98
+ }
84
99
  /**
85
100
  * Tenant already exists.
86
101
  */
@@ -92,18 +107,6 @@ export class TenantAlreadyExistsError extends TenantError {
92
107
  });
93
108
  }
94
109
  }
95
- /**
96
- * Tenant provisioning failed.
97
- */
98
- export class TenantProvisioningError extends TenantError {
99
- constructor(tenantId, cause) {
100
- super(`Provisioning failed for tenant "${tenantId}"`, {
101
- code: ErrorCode.OPERATION_FAILED,
102
- cause,
103
- metadata: { tenantId },
104
- });
105
- }
106
- }
107
110
  /**
108
111
  * Tenant isolation violation detected.
109
112
  */
@@ -3,9 +3,9 @@
3
3
  *
4
4
  * @module tenancyTypes
5
5
  */
6
- export { type TenantId, createTenantId, type TenantStatus, type TenantIsolationStrategy, type TenancyMode, } from "./tenantIdentity.js";
6
+ export { type TenantId, MAX_TENANT_ID_LENGTH, createTenantId, tryCreateTenantId, isValidTenantId, type TenantStatus, } from "./tenantIdentity.js";
7
7
  export { type Tenant, type TenantResolutionSource, type TenantTrustLevel, type TenantContext, type SystemContext, type TenantExecutionContext, type ExecutionTenantContext, type TenantRequirement, type TenantResource, } from "./tenantInterface.js";
8
8
  export { type TenantResolution, type TenantResolutionResult, type TenantResolver, type ResolverChainOptions, } from "./resolverTypes.js";
9
- export { type TenantRepository, type TenantCache, type TenantManager, type TenantProvisioner, type TenantIsolationConfig, type TenantConfigurationProvider, } from "./repositoryTypes.js";
10
- export { type TenancyOptions, type TenantDomain, type TenantScopeOptions, } from "./tenancyOptions.js";
9
+ export { type TenantRepository, type TenantCache, } from "./repositoryTypes.js";
10
+ export { type TenantDomain } from "./repositoryTypes.js";
11
11
  //# sourceMappingURL=index.d.ts.map
@@ -3,9 +3,9 @@
3
3
  *
4
4
  * @module tenancyTypes
5
5
  */
6
- export { createTenantId, } from "./tenantIdentity.js";
6
+ export { MAX_TENANT_ID_LENGTH, createTenantId, tryCreateTenantId, isValidTenantId, } from "./tenantIdentity.js";
7
7
  export {} from "./tenantInterface.js";
8
8
  export {} from "./resolverTypes.js";
9
9
  export {} from "./repositoryTypes.js";
10
- export {} from "./tenancyOptions.js";
10
+ export {} from "./repositoryTypes.js";
11
11
  //# sourceMappingURL=index.js.map
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * @module tenancyTypes/repositoryTypes
5
5
  */
6
- import type { TenantId, TenantIsolationStrategy } from "./tenantIdentity.js";
6
+ import type { TenantId } from "./tenantIdentity.js";
7
7
  import type { Tenant } from "./tenantInterface.js";
8
8
  /** Repository for loading tenants. */
9
9
  export interface TenantRepository {
@@ -11,30 +11,15 @@ export interface TenantRepository {
11
11
  findBySlug?(slug: string): Promise<Tenant | undefined>;
12
12
  findByDomain?(domain: string): Promise<Tenant | undefined>;
13
13
  }
14
+ /** A custom domain mapped to the tenant it serves. */
15
+ export interface TenantDomain {
16
+ readonly domain: string;
17
+ readonly tenantId: TenantId;
18
+ }
14
19
  /** Cache for tenant data. */
15
20
  export interface TenantCache {
16
21
  get(id: TenantId): Promise<Tenant | undefined>;
17
22
  set(tenant: Tenant): Promise<void>;
18
23
  delete(id: TenantId): Promise<void>;
19
24
  }
20
- /** High-level tenant orchestration. */
21
- export interface TenantManager {
22
- resolve(context: unknown): Promise<Tenant | undefined>;
23
- get(id: TenantId): Promise<Tenant | undefined>;
24
- require(id: TenantId): Promise<Tenant>;
25
- }
26
- /** Handles tenant setup operations. */
27
- export interface TenantProvisioner {
28
- provision(tenant: Tenant): Promise<void>;
29
- }
30
- /** Isolation configuration for a tenant. */
31
- export interface TenantIsolationConfig {
32
- readonly strategy: TenantIsolationStrategy;
33
- readonly identifier?: string;
34
- }
35
- /** Tenant configuration provider. */
36
- export interface TenantConfigurationProvider {
37
- get<T = unknown>(tenantId: TenantId, key: string): Promise<T | undefined>;
38
- set<T = unknown>(tenantId: TenantId, key: string, value: T): Promise<void>;
39
- }
40
25
  //# sourceMappingURL=repositoryTypes.d.ts.map
@@ -26,9 +26,21 @@ export interface TenantResolver<Context = unknown> {
26
26
  }
27
27
  /** Options for resolver chain. */
28
28
  export interface ResolverChainOptions {
29
- /** Whether to detect conflicts between resolver results. */
29
+ /** Name reported when the chain is used as a resolver. Defaults to "chain". */
30
+ readonly name?: string;
31
+ /** Priority when nested in another chain. Defaults to the highest member's. */
32
+ readonly priority?: number;
33
+ /**
34
+ * Run every resolver and collect all candidates, rather than stopping at the
35
+ * first match. Required for conflict detection.
36
+ */
30
37
  readonly detectConflicts?: boolean;
31
- /** Throw on conflict instead of using highest priority. */
38
+ /**
39
+ * Throw when candidates name different tenants. Defaults to true.
40
+ *
41
+ * Disagreement between a verified credential and a client-supplied header is
42
+ * the signature of an attempted cross-tenant request.
43
+ */
32
44
  readonly throwOnConflict?: boolean;
33
45
  }
34
46
  //# sourceMappingURL=resolverTypes.d.ts.map
@@ -9,13 +9,32 @@ declare const TenantIdBrand: unique symbol;
9
9
  export type TenantId = string & {
10
10
  readonly [TenantIdBrand]: true;
11
11
  };
12
- /** Create a validated TenantId. */
12
+ /** Maximum accepted tenant id length. */
13
+ export declare const MAX_TENANT_ID_LENGTH = 64;
14
+ /**
15
+ * Create a validated TenantId.
16
+ *
17
+ * Input is trimmed, Unicode-normalized and lowercased before validation, so
18
+ * two spellings of the same identifier cannot become two tenants.
19
+ *
20
+ * @param value - The candidate identifier.
21
+ * @returns The normalized, validated tenant id.
22
+ * @throws {InvalidTenantIdError} when the value is not a valid tenant id.
23
+ */
13
24
  export declare function createTenantId(value: string): TenantId;
25
+ /**
26
+ * Create a validated TenantId, or undefined when the value is unusable.
27
+ *
28
+ * Resolvers use this: an unparseable candidate means "this resolver found
29
+ * nothing", not "the request is malformed".
30
+ *
31
+ * @param value - The candidate identifier.
32
+ * @returns The tenant id, or undefined.
33
+ */
34
+ export declare function tryCreateTenantId(value: unknown): TenantId | undefined;
35
+ /** Whether a value is a well-formed tenant id. */
36
+ export declare function isValidTenantId(value: unknown): value is TenantId;
14
37
  /** Tenant lifecycle status. */
15
38
  export type TenantStatus = "provisioning" | "active" | "inactive" | "suspended" | "deleting" | "deleted";
16
- /** Database isolation strategy for tenant data. */
17
- export type TenantIsolationStrategy = "shared" | "schema" | "database" | "hybrid";
18
- /** Tenancy execution mode. */
19
- export type TenancyMode = "tenant" | "system";
20
39
  export {};
21
40
  //# sourceMappingURL=tenantIdentity.d.ts.map
@@ -3,11 +3,61 @@
3
3
  *
4
4
  * @module tenancyTypes/tenantIdentity
5
5
  */
6
- /** Create a validated TenantId. */
6
+ import { InvalidTenantIdError } from "../tenancyErrors/tenancyError.types.js";
7
+ /**
8
+ * Characters a tenant id may contain.
9
+ *
10
+ * Deliberately narrow. A tenant id is concatenated into cache keys, log lines,
11
+ * schema names and file paths, so anything that could act as a separator or a
12
+ * path segment in one of those contexts is rejected here rather than escaped
13
+ * at every use site.
14
+ */
15
+ const TENANT_ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/u;
16
+ /** Maximum accepted tenant id length. */
17
+ export const MAX_TENANT_ID_LENGTH = 64;
18
+ /**
19
+ * Create a validated TenantId.
20
+ *
21
+ * Input is trimmed, Unicode-normalized and lowercased before validation, so
22
+ * two spellings of the same identifier cannot become two tenants.
23
+ *
24
+ * @param value - The candidate identifier.
25
+ * @returns The normalized, validated tenant id.
26
+ * @throws {InvalidTenantIdError} when the value is not a valid tenant id.
27
+ */
7
28
  export function createTenantId(value) {
8
- if (!value || value.trim().length === 0) {
9
- throw new Error("TenantId cannot be empty");
29
+ if (typeof value !== "string")
30
+ throw new InvalidTenantIdError(String(value));
31
+ const normalized = value.normalize("NFKC").trim().toLowerCase();
32
+ if (normalized.length === 0 || normalized.length > MAX_TENANT_ID_LENGTH) {
33
+ throw new InvalidTenantIdError(value);
34
+ }
35
+ if (!TENANT_ID_PATTERN.test(normalized)) {
36
+ throw new InvalidTenantIdError(value);
10
37
  }
11
- return value;
38
+ return normalized;
39
+ }
40
+ /**
41
+ * Create a validated TenantId, or undefined when the value is unusable.
42
+ *
43
+ * Resolvers use this: an unparseable candidate means "this resolver found
44
+ * nothing", not "the request is malformed".
45
+ *
46
+ * @param value - The candidate identifier.
47
+ * @returns The tenant id, or undefined.
48
+ */
49
+ export function tryCreateTenantId(value) {
50
+ if (typeof value !== "string")
51
+ return undefined;
52
+ try {
53
+ return createTenantId(value);
54
+ }
55
+ catch {
56
+ return undefined;
57
+ }
58
+ }
59
+ /** Whether a value is a well-formed tenant id. */
60
+ export function isValidTenantId(value) {
61
+ return tryCreateTenantId(value) !== undefined;
12
62
  }
13
63
  //# sourceMappingURL=tenantIdentity.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zudojs/tenancy",
3
- "version": "0.1.0",
3
+ "version": "1.0.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
  "type": "module",
@@ -15,21 +15,17 @@
15
15
  }
16
16
  },
17
17
  "files": [
18
- "dist"
18
+ "dist",
19
+ "!dist/**/*.map",
20
+ "!dist/**/*.tsbuildinfo",
21
+ "!dist/.tsbuildinfo"
19
22
  ],
20
- "scripts": {
21
- "build": "tsc -p tsconfig.json",
22
- "typecheck": "tsc -p tsconfig.json --noEmit",
23
- "clean": "rm -rf dist",
24
- "test": "vitest run",
25
- "test:watch": "vitest"
26
- },
27
23
  "dependencies": {
28
- "@zudojs/errors": "0.1.0",
29
- "@zudojs/constants": "0.1.0"
24
+ "@zudojs/errors": "1.0.0",
25
+ "@zudojs/constants": "1.0.0"
30
26
  },
31
27
  "peerDependencies": {
32
- "@zudojs/http": "0.1.0"
28
+ "@zudojs/http": "1.0.0"
33
29
  },
34
30
  "peerDependenciesMeta": {
35
31
  "@zudojs/http": {
@@ -40,7 +36,7 @@
40
36
  "node": ">=24.0.0"
41
37
  },
42
38
  "devDependencies": {
43
- "typescript": "^7.0.2",
39
+ "typescript": "7.0.2",
44
40
  "vitest": "^4.1.11"
45
41
  },
46
42
  "publishConfig": {
@@ -53,8 +49,19 @@
53
49
  "saas"
54
50
  ],
55
51
  "homepage": "https://github.com/oyinlola-tech/zudo#readme",
52
+ "bugs": {
53
+ "url": "https://github.com/oyinlola-tech/zudo/issues"
54
+ },
56
55
  "repository": {
57
56
  "type": "git",
58
- "url": "https://github.com/oyinlola-tech/zudo"
57
+ "url": "https://github.com/oyinlola-tech/zudo",
58
+ "directory": "packages/tenancy"
59
+ },
60
+ "scripts": {
61
+ "build": "tsc -p tsconfig.json",
62
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit",
63
+ "clean": "rm -rf dist",
64
+ "test": "vitest run",
65
+ "test:watch": "vitest"
59
66
  }
60
- }
67
+ }