@i4e/invest4edu-access-core 0.4.0 → 0.4.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@i4e/invest4edu-access-core",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Shared access-control primitives (Track 3: tenant keystone, role capabilities, reportee tree, feature flags) for NeoFindesk backends.",
5
5
  "type": "module",
6
6
  "exports": {
package/src/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * Tenant isolation (D1 keystone) + role capabilities (G7).
4
4
  */
5
5
  export { default as als, runWithTenant, runAsSystem, getTenantStore } from "./tenant-context.js";
6
- export { default as tenantPlugin } from "./tenant-plugin.js";
6
+ export { default as tenantPlugin, setTenantMode, getTenantMode } from "./tenant-plugin.js";
7
7
  export {
8
8
  CAPABILITY_FLAGS,
9
9
  DEFAULT_ROLE_CAPABILITIES,
@@ -5,9 +5,11 @@
5
5
  * injects `account_id` from the request's ALS store (tenant-context.js), so a query cannot
6
6
  * reach Mongo without the caller's tenant boundary — even if the controller forgot it.
7
7
  *
8
- * Modes (env `TENANT_ENFORCEMENT`, default `warn`):
9
- * - identity present → inject `account_id` unconditionally (BOTH modes)
10
- * - identity absent → `enforce`: THROW (fail-closed) · `warn`: log + do not scope (burn-in)
8
+ * Modes (env `TENANT_ENFORCEMENT`, default `off`):
9
+ * - `off` (DEFAULT) → literal no-op; behaviour identical to pre-Track-3. Safe merge baseline.
10
+ * - `warn` → inject `account_id` when identity present; log identity-less reads.
11
+ * - `enforce` → inject when present; THROW on identity-less reads (fail-closed).
12
+ * Rollout ladder: off → warn (burn-in, watch `[tenant] identity-less`) → enforce.
11
13
  *
12
14
  * Bypass: `runAsSystem(fn)` or `.setOptions({ skipTenant: true })` for legitimately
13
15
  * cross-tenant reads. NOT covered: aggregation pipelines and write ops (reads are C0 scope).
@@ -18,9 +20,31 @@
18
20
  import als from "./tenant-context.js";
19
21
 
20
22
  const READ_OPS = ["find", "findOne", "countDocuments"];
21
- const mode = () => (process.env.TENANT_ENFORCEMENT || "warn").toLowerCase();
23
+ const VALID = new Set(["off", "warn", "enforce"]);
24
+
25
+ // Enforcement mode is resolvable from TWO places, so it can be flipped from the admin UI
26
+ // (DB) WITHOUT a restart, while the env var stays the infra-level emergency override.
27
+ // Precedence: env TENANT_ENFORCEMENT (if valid) > DB value (setTenantMode) > "off"
28
+ let dbMode = null;
29
+
30
+ /** Called by the backend from the `accessconfig` DB doc so a UI toggle takes effect (~refresh). */
31
+ export function setTenantMode(m) {
32
+ const v = m ? String(m).toLowerCase() : "";
33
+ dbMode = VALID.has(v) ? v : null;
34
+ }
35
+
36
+ /** Effective mode. */
37
+ export function getTenantMode() {
38
+ const env = (process.env.TENANT_ENFORCEMENT || "").toLowerCase();
39
+ if (VALID.has(env)) return env; // explicit env wins (emergency / infra)
40
+ return dbMode || "off";
41
+ }
42
+
43
+ const mode = getTenantMode;
22
44
 
23
45
  function tenantHook() {
46
+ if (mode() === "off") return; // literal no-op — identical to pre-Track-3
47
+
24
48
  const opts = typeof this.getOptions === "function" ? this.getOptions() : this.options || {};
25
49
  if (opts && opts.skipTenant) return;
26
50