@ultimat3/entity 6.0.0 → 7.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.
package/CLAUDE.md CHANGED
@@ -568,9 +568,24 @@ Columns + invariants; the row type is derived from the columns. Tier 2.
568
568
  the tenant is a request-time value, so the seam is the enforcement.
569
569
  - **`crossTenant(reason, fn)` (`cross-tenant.ts`) is the ONE way to read across tenants**, for the
570
570
  three cases that have no single one: an admin surface over every org, background reconciliation,
571
- support tooling. An `AsyncLocalStorage` scope with a written reason, the same shape
571
+ support tooling. An async-context scope with a written reason, the same shape
572
572
  `@ultimat3/db`'s `expectedQueryLoop` has, never a boolean argument on a repository call — which
573
573
  reads exactly like forgetting the tenant — and never a config list of exempt entities (axiom 1).
574
+ The scope opens through `asyncContext<string>('the cross-tenant reason')` from `@ultimat3/core`,
575
+ **never a `new AsyncLocalStorage` here, and that is a build error rather than a convention `As of
576
+ 2026-08`** — `scripts/async-context-guard.ts` refuses the construction *and* the import that
577
+ binds the class, anywhere but `packages/core/src/async-context.ts`, and
578
+ `scripts/async-context-guard.test.ts` runs it over the tree in the gate's `unit` step. The
579
+ module-scope `new` this replaced threw `TypeError: undefined is not a constructor` at module
580
+ **evaluation** in a browser bundle, where the bundler stubs `node:async_hooks` to `{}`, taking
581
+ every importer of `cross-tenant.ts` with it. Now the module evaluates and `crossTenantReason()`
582
+ answers `undefined` there — in a browser nothing IS in flight, so that is the true answer. A
583
+ write is the case that names itself: `storage.run` throws `X_ASYNC_CONTEXT_UNAVAILABLE` instead
584
+ of a bare `TypeError`, though `crossTenant()` reaches it only past `assertCrossTenant`, which
585
+ wants a request context a browser does not have. A server saves no allocation — the store is
586
+ built on the first `get()` **or** `run()`, so a read constructs it too; what the laziness costs
587
+ is nothing observable, since `getStore()` outside a scope answers `undefined` whether the storage
588
+ existed or not.
574
589
  **The capability is proven twice**: `CROSS_TENANT_SCOPE` (`tenancy:cross`) on the actor, at the
575
590
  call and again at every plan built inside it, because `withChildContext({ actor })` swaps the
576
591
  actor without closing the scope and an impersonated caller must not inherit it —
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/entity",
3
- "version": "6.0.0",
3
+ "version": "7.0.0",
4
4
  "description": "A table + its domain type + invariants the database also enforces",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -31,9 +31,9 @@
31
31
  "test": "bun test"
32
32
  },
33
33
  "dependencies": {
34
- "@ultimat3/core": "6.0.0",
35
- "@ultimat3/db": "6.0.0",
36
- "@ultimat3/schema": "6.0.0",
37
- "@ultimat3/time": "6.0.0"
34
+ "@ultimat3/core": "7.0.0",
35
+ "@ultimat3/db": "7.0.0",
36
+ "@ultimat3/schema": "7.0.0",
37
+ "@ultimat3/time": "7.0.0"
38
38
  }
39
39
  }
@@ -3,11 +3,11 @@
3
3
  // reads exactly like forgetting the tenant, and never a config list of exempt entities (axiom 1):
4
4
  // both put the argument somewhere other than the read it defends.
5
5
 
6
- // `node:` because Bun exposes no native async-context primitive: the scope has to outlive every
7
- // `await` inside it, and `AsyncLocalStorage` is the only thing that carries a value across them.
8
- // A module-scope flag would be shared by two concurrent requests one of them ordinary.
9
- import { AsyncLocalStorage } from 'node:async_hooks';
10
- import { actorLabel, assert, hasScope, tryUseContext } from '@ultimat3/core';
6
+ // The scope has to outlive every `await` inside it and a module-scope flag would be shared by two
7
+ // concurrent requests one of them ordinary so it needs an async context. Opened through core's
8
+ // one lazy seam rather than a `node:async_hooks` construction here, which threw at module
9
+ // EVALUATION in a browser bundle (the bundler stubs the module to `{}`).
10
+ import { actorLabel, assert, asyncContext, hasScope, tryUseContext } from '@ultimat3/core';
11
11
  import { crossTenantDenied } from './errors';
12
12
 
13
13
  /**
@@ -18,7 +18,7 @@ import { crossTenantDenied } from './errors';
18
18
  */
19
19
  export const CROSS_TENANT_SCOPE = 'tenancy:cross';
20
20
 
21
- const storage = new AsyncLocalStorage<string>();
21
+ const storage = asyncContext<string>('the cross-tenant reason');
22
22
 
23
23
  /**
24
24
  * Run `fn` with the tenant guard lifted — every read and write it issues, at any depth and across
@@ -54,7 +54,7 @@ export function crossTenant<T>(reason: string, fn: () => T): T {
54
54
  * The innermost enclosing reason, or `undefined` outside every scope — which is every query in an
55
55
  * app that never calls `crossTenant`. Read by the tenant guard, and by nothing else.
56
56
  */
57
- export const crossTenantReason = (): string | undefined => storage.getStore();
57
+ export const crossTenantReason = (): string | undefined => storage.get();
58
58
 
59
59
  /**
60
60
  * The capability check itself, run at `crossTenant()` and again for every plan built inside it.