@zerotal/tenancy 1.4.0 → 1.5.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/CHANGELOG.md +22 -1
- package/README.md +2 -2
- package/package.json +4 -4
- package/src/EnsureTenancyMiddleware.ts +1 -1
- package/src/TenancyMiddleware.ts +1 -1
- package/src/Tenantable.ts +7 -2
- package/src/tenantStorage.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,10 +4,31 @@ All notable changes to this package are documented here. The format is
|
|
|
4
4
|
based on [Keep a Changelog](https://keepachangelog.com/); this package
|
|
5
5
|
follows the Zerotal monorepo's unified versioning.
|
|
6
6
|
|
|
7
|
-
**Maturity: `
|
|
7
|
+
**Maturity: `stable`**
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [1.5.0] — 2026-08-15
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`EnsureTenancyMiddleware` and `TenantStoragePathError` are documented.** Both were
|
|
16
|
+
public and neither appeared in the guide. `EnsureTenancyMiddleware` is the difference
|
|
17
|
+
between _resolving_ a tenant and _insisting_ on one — the guide now shows it composed
|
|
18
|
+
after `TenancyMiddleware`, and says why registering it alone fails every request.
|
|
19
|
+
`TenantStoragePathError` is the cross-tenant traversal guard: `LocalDriver` confines
|
|
20
|
+
paths to the disk root rather than the tenant directory, so a key containing `..`
|
|
21
|
+
reached a sibling tenant's folder and passed the driver's own check. The rejection
|
|
22
|
+
happens at the prefixing layer, and that is now written down where someone handling
|
|
23
|
+
upload errors will find it.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- **Maturity is now `stable`** — the public API follows SemVer strictly for the rest of
|
|
28
|
+
the 1.x line. All 22 promised exports are documented, and the property that actually
|
|
29
|
+
matters here — that one tenant cannot read another's rows, files or cache — is covered
|
|
30
|
+
by a dedicated cross-tenant security suite rather than inferred from unit tests.
|
|
31
|
+
|
|
11
32
|
## [1.0.3] — 2026-08-07
|
|
12
33
|
|
|
13
34
|
### Changed
|
package/README.md
CHANGED
|
@@ -7,8 +7,8 @@ everywhere in the call stack — ORM queries, storage paths, and cache keys —
|
|
|
7
7
|
no manual thread-through. Supports both single-database (a `tenant_id` column,
|
|
8
8
|
scoped automatically by `Tenantable`) and multi-database (a connection per tenant,
|
|
9
9
|
routed automatically from a one-line `connect` factory) strategies — in both, your
|
|
10
|
-
models "just work" with no per-query wiring. **
|
|
11
|
-
|
|
10
|
+
models "just work" with no per-query wiring. **Stable** — the public API follows
|
|
11
|
+
SemVer strictly for the rest of the 1.x line.
|
|
12
12
|
|
|
13
13
|
Part of the [Zerotal](../../README.md) framework. Requires **Bun ≥ 1.3.14**.
|
|
14
14
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zerotal/tenancy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"maturity": "
|
|
5
|
+
"maturity": "stable",
|
|
6
6
|
"private": false,
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "./src/index.ts",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"typecheck": "tsc --noEmit"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@zerotal/core": "1.
|
|
33
|
-
"@zerotal/orm": "1.
|
|
32
|
+
"@zerotal/core": "1.5.1",
|
|
33
|
+
"@zerotal/orm": "1.5.1"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"typescript": "^5.8.0"
|
|
@@ -23,7 +23,7 @@ import { TenantContext } from "./TenantContext.ts";
|
|
|
23
23
|
import { TenantNotFoundError } from "./errors.ts";
|
|
24
24
|
|
|
25
25
|
export class EnsureTenancyMiddleware extends BaseMiddleware {
|
|
26
|
-
protected options:
|
|
26
|
+
protected options: Record<string, never> = {};
|
|
27
27
|
|
|
28
28
|
async handle(_http: HttpContext, next: NextFn): Promise<Response | void> {
|
|
29
29
|
if (TenantContext.tryGet() == null) throw new TenantNotFoundError();
|
package/src/TenancyMiddleware.ts
CHANGED
package/src/Tenantable.ts
CHANGED
|
@@ -35,6 +35,7 @@ import { TenantContext } from "./TenantContext.ts";
|
|
|
35
35
|
import { _globalScopeRegistry, ModelQueryBuilder as MQB, HookRegistry } from "@zerotal/orm";
|
|
36
36
|
import type { ModelQueryBuilder } from "@zerotal/orm";
|
|
37
37
|
import type { BaseModel } from "@zerotal/orm";
|
|
38
|
+
import type { ClassRef } from "@zerotal/core";
|
|
38
39
|
|
|
39
40
|
const SCOPE_NAME = "__tenant__";
|
|
40
41
|
const DEFAULT_COLUMN = "tenant_id";
|
|
@@ -52,7 +53,7 @@ function _tenantColumn(ctor: unknown): string {
|
|
|
52
53
|
* module-load; `TenancyProvider.onBooted()` drains it once the ORM context is live.
|
|
53
54
|
* After boot, new classes are registered immediately via `_markTenantProviderBooted()`.
|
|
54
55
|
*/
|
|
55
|
-
export const _pendingTenantable = new Set<
|
|
56
|
+
export const _pendingTenantable = new Set<ClassRef>();
|
|
56
57
|
let _tenantProviderBooted = false;
|
|
57
58
|
export function _markTenantProviderBooted(): void {
|
|
58
59
|
_tenantProviderBooted = true;
|
|
@@ -62,7 +63,7 @@ export function _markTenantProviderBooted(): void {
|
|
|
62
63
|
* Wire the global query scope and beforeCreate hook onto a model class.
|
|
63
64
|
* Called by `TenancyProvider.onBooted()` for each `Tenantable`-composed class.
|
|
64
65
|
*/
|
|
65
|
-
export function registerTenantScoping(cls:
|
|
66
|
+
export function registerTenantScoping(cls: ClassRef): void {
|
|
66
67
|
// ── 1. Global query scope ────────────────────────────────────────────────
|
|
67
68
|
const registry = _globalScopeRegistry();
|
|
68
69
|
let scopes = registry.get(cls as unknown as typeof BaseModel);
|
|
@@ -118,6 +119,10 @@ export function Tenantable<TBase extends Constructor>(Base: TBase) {
|
|
|
118
119
|
// ── Augment ModelQueryBuilder with withoutTenancy() ──────────────────────────
|
|
119
120
|
|
|
120
121
|
declare module "@zerotal/orm" {
|
|
122
|
+
// Declaration merging requires the type parameter list to match the merged
|
|
123
|
+
// declaration exactly, so `M` cannot be renamed or dropped even though this
|
|
124
|
+
// augmentation's body never reads it.
|
|
125
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
121
126
|
interface ModelQueryBuilder<M extends BaseModel> {
|
|
122
127
|
/**
|
|
123
128
|
* Remove the tenant global scope for this query only.
|
package/src/tenantStorage.ts
CHANGED
|
@@ -169,6 +169,9 @@ export function tenantCache(store?: string, cacheResolver?: CacheResolverLike):
|
|
|
169
169
|
|
|
170
170
|
function _resolveCache(): CacheResolverLike {
|
|
171
171
|
try {
|
|
172
|
+
// Optional peer probed synchronously — a static import would fail when the
|
|
173
|
+
// package is absent, and this resolver cannot be async.
|
|
174
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
172
175
|
const { Cache } = require("@zerotal/cache") as { Cache: CacheResolverLike };
|
|
173
176
|
return Cache;
|
|
174
177
|
} catch {
|