@zerotal/tenancy 1.4.0 → 1.5.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/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: `beta`**
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. **Beta** — APIs are stable but rough
11
- edges remain.
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.4.0",
3
+ "version": "1.5.0",
4
4
  "license": "MIT",
5
- "maturity": "beta",
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.4.0",
33
- "@zerotal/orm": "1.4.0"
32
+ "@zerotal/core": "1.5.0",
33
+ "@zerotal/orm": "1.5.0"
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();
@@ -45,7 +45,7 @@ function _authUserId(http: HttpContext): number | null {
45
45
  }
46
46
 
47
47
  export class TenancyMiddleware extends BaseMiddleware {
48
- protected options: {} = {};
48
+ protected options: Record<string, never> = {};
49
49
 
50
50
  private static _config?: TenancyConfigShape;
51
51
 
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<Function>();
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: Function): void {
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.
@@ -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 {