@i4e/invest4edu-access-core 0.32.0 → 0.33.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/README.md +128 -126
- package/package.json +57 -56
- package/src/access-config.js +68 -68
- package/src/access-resolver.js +269 -269
- package/src/access-schema.js +174 -174
- package/src/credits.js +128 -128
- package/src/entitlement-schema.js +194 -194
- package/src/entitlement-store.d.ts +99 -99
- package/src/entitlement-store.js +610 -610
- package/src/entitlement.js +300 -300
- package/src/grid-schema.js +231 -231
- package/src/index.js +75 -74
- package/src/proration.js +155 -155
- package/src/reportee-tree.js +129 -129
- package/src/role-capabilities.js +93 -93
- package/src/route-features.js +292 -292
- package/src/route-screen.js +45 -0
- package/src/subscription-lifecycle.js +106 -106
- package/src/tenant-context.js +26 -26
- package/src/tenant-plugin.js +177 -177
- package/src/visible-when.js +108 -108
package/README.md
CHANGED
|
@@ -1,126 +1,128 @@
|
|
|
1
|
-
# @i4e/invest4edu-access-core
|
|
2
|
-
|
|
3
|
-
Shared tenant-isolation / access-control primitives for the NeoFindesk backends
|
|
4
|
-
(nfd-api-node v1 + nfd-api-node-v2). The **D1 keystone** of the access-management epic:
|
|
5
|
-
central, unforgettable `account_id` scoping.
|
|
6
|
-
|
|
7
|
-
Both backends are ESM, so this package ships plain ESM source — no build step.
|
|
8
|
-
|
|
9
|
-
## 0.32.0 — BREAKING, and it fails silently
|
|
10
|
-
|
|
11
|
-
`resolveReporteeUserIds` **no longer reads `employees.reporting_manager` as a user id.** It
|
|
12
|
-
reads it only as the manager's employee `_id`, which is the shape BRI-973 migrates the field
|
|
13
|
-
to. This is a breaking change on a 0.x minor, so read the two notes below before upgrading a
|
|
14
|
-
consumer.
|
|
15
|
-
|
|
16
|
-
**1. Against un-migrated data it returns zero reportees, not an error.** The dual-shape
|
|
17
|
-
widening that carried the rollout is gone. A manager whose rows have not been migrated
|
|
18
|
-
resolves to a self-only scope, and because this function feeds record visibility in
|
|
19
|
-
nfd-api-node and the lead access scope in nfd-api-node-v2, that reads as *"this manager has
|
|
20
|
-
no reportees"* — nothing thrown, nothing logged, just a smaller answer.
|
|
21
|
-
|
|
22
|
-
So the deploy order is **migrate → verify → bump**, not the other way round. Run
|
|
23
|
-
nfd-api-node's `migrations/verify-reporting-manager-to-employee-id.js` and get a clean
|
|
24
|
-
verdict before the first instance carrying 0.32.0 serves traffic. In a rolling deploy that
|
|
25
|
-
window is real, so the migration must be complete rather than merely started.
|
|
26
|
-
|
|
27
|
-
Under npm's 0.x caret rules `^0.31.0` will not resolve to 0.32.0, so no consumer is dragged
|
|
28
|
-
across by an install — the ordering risk is entirely a human one.
|
|
29
|
-
|
|
30
|
-
**2. Callers must pass the FULL employee list.** An `_id` reference only resolves if the row
|
|
31
|
-
carrying it is present, so a pre-filtered list (`{ status: 1 }`, an account scope, anything)
|
|
32
|
-
drops link rows — and what you lose is not the filtered person but everyone beneath them.
|
|
33
|
-
Filter the result instead. `resolveReporteeUserIds` warns once when handed a list in which no
|
|
34
|
-
row carries an `_id` at all, which is the one case where the answer is confidently wrong
|
|
35
|
-
rather than merely narrow.
|
|
36
|
-
|
|
37
|
-
0.32.0 also moves the entity/role vocabulary — `ENTITY`, `ENTITY_STATUS`,
|
|
38
|
-
`isEntityLoginAllowed`, `conflictingEntityFor`, `ROLE_TYPE_PRIORITY`,
|
|
39
|
-
`CLIENT_APP_ROLE_NAMES` — into this package. It was previously written out three times
|
|
40
|
-
(nfd-api-node, nfd-api-node-v2, nfdui-nextjs) and held together by parity tests.
|
|
41
|
-
|
|
42
|
-
## Install
|
|
43
|
-
|
|
44
|
-
```
|
|
45
|
-
npm install @i4e/invest4edu-access-core
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
## Usage
|
|
49
|
-
|
|
50
|
-
**1. Carry identity per request** (in the auth middleware, JWT path only):
|
|
51
|
-
|
|
52
|
-
```js
|
|
53
|
-
import als from "@i4e/invest4edu-access-core/tenant-context";
|
|
54
|
-
// after verifying the JWT:
|
|
55
|
-
return als.run({ account_id, userId, roles }, () => next());
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
**2. Enforce on tenant-scoped models:**
|
|
59
|
-
|
|
60
|
-
```js
|
|
61
|
-
import { tenantPlugin } from "@i4e/invest4edu-access-core";
|
|
62
|
-
schema.plugin(tenantPlugin); // before mongoose.model(...)
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
The plugin injects `account_id` from the request's ALS store into the **filter** of every
|
|
66
|
-
operation that selects existing documents:
|
|
67
|
-
|
|
68
|
-
| | ops | switch |
|
|
69
|
-
|---|---|---|
|
|
70
|
-
| reads | `find` `findOne` `countDocuments` `distinct` | `TENANT_ENFORCEMENT` / `accessconfig.tenantEnforcement` |
|
|
71
|
-
| writes | `updateOne` `updateMany` `replaceOne` `deleteOne` `deleteMany` `findOneAndUpdate` `findOneAndReplace` `findOneAndDelete` | `TENANT_ENFORCEMENT_WRITES` / `accessconfig.tenantEnforcementWrites` |
|
|
72
|
-
|
|
73
|
-
Each switch is an independent ladder — `off` → `warn` → `enforce` — and **both default to `off`**,
|
|
74
|
-
so installing the plugin is a literal no-op until you arm it. Reads and writes are separate
|
|
75
|
-
because the blast radius is not comparable: a read that gains a filter returns less data, while a
|
|
76
|
-
write that gains a filter silently modifies **nothing** and still reports success. Arming reads
|
|
77
|
-
must never arm writes by surprise.
|
|
78
|
-
|
|
79
|
-
| mode | identity present | identity absent |
|
|
80
|
-
|---|---|---|
|
|
81
|
-
| `off` (default) | no-op | no-op |
|
|
82
|
-
| `warn` | reads: inject `account_id` · writes: **unchanged** | log `[tenant] identity-less …`, do **not** scope |
|
|
83
|
-
| `enforce` | inject `account_id` | **throw** (fail-closed) |
|
|
84
|
-
|
|
85
|
-
`warn` deliberately leaves writes alone. `warn` means "tell me what `enforce` would do", and for a
|
|
86
|
-
destructive operation that promise is only kept by changing nothing — injecting a filter under a
|
|
87
|
-
mode named `warn` would turn live `UPDATE`s into silent no-ops, the exact failure burn-in exists to
|
|
88
|
-
catch. What you need before promoting is which writes run identity-less, and that is logged.
|
|
89
|
-
|
|
90
|
-
Precedence per ladder: env var (if valid) → DB value → `off`. An unrecognised value is reported
|
|
91
|
-
once and ignored rather than treated as `off`, so a typo cannot silently disarm enforcement.
|
|
92
|
-
|
|
93
|
-
**3. Bypass for legitimately cross-tenant operations** (caches, migrations, scripts):
|
|
94
|
-
|
|
95
|
-
```js
|
|
96
|
-
import { runAsSystem } from "@i4e/invest4edu-access-core";
|
|
97
|
-
await runAsSystem(() => Employee.find({ status: 1 })); // whole scope
|
|
98
|
-
Model.find(q).setOptions({ skipTenant: true }); // one query
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
## Exports
|
|
102
|
-
|
|
103
|
-
- `als` (default of `/tenant-context`) — the AsyncLocalStorage instance
|
|
104
|
-
- `runWithTenant(store, fn)`, `runAsSystem(fn)`, `getTenantStore()`
|
|
105
|
-
- `tenantPlugin` (default of `/tenant-plugin`) — the Mongoose plugin
|
|
106
|
-
- `setTenantMode(m)` / `getTenantMode()` — the read ladder
|
|
107
|
-
- `setTenantWriteMode(m)` / `getTenantWriteMode()` — the write ladder
|
|
108
|
-
- `resolveReporteeUserIds(roots, employees, opts)` (`/reportee-tree`) — self + all reportees
|
|
109
|
-
- `ENTITY`, `ENTITY_STATUS`, `isEntityLoginAllowed`, `entityInactiveMessage`,
|
|
110
|
-
`checkPortalEntityAccess` (`/entity-status`) — which entities permit a login
|
|
111
|
-
- `conflictingEntityFor`, `entityConflictMessage`, `CONFLICTING_ENTITIES`
|
|
112
|
-
(`/user-entity-link`) — which hats may coexist on one person
|
|
113
|
-
- `ROLE_TYPE_PRIORITY`, `CLIENT_APP_ROLE_NAMES`, `pickPortalRoleName` (`/portal-roles`) —
|
|
114
|
-
which role names a portal session
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
**
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
1
|
+
# @i4e/invest4edu-access-core
|
|
2
|
+
|
|
3
|
+
Shared tenant-isolation / access-control primitives for the NeoFindesk backends
|
|
4
|
+
(nfd-api-node v1 + nfd-api-node-v2). The **D1 keystone** of the access-management epic:
|
|
5
|
+
central, unforgettable `account_id` scoping.
|
|
6
|
+
|
|
7
|
+
Both backends are ESM, so this package ships plain ESM source — no build step.
|
|
8
|
+
|
|
9
|
+
## 0.32.0 — BREAKING, and it fails silently
|
|
10
|
+
|
|
11
|
+
`resolveReporteeUserIds` **no longer reads `employees.reporting_manager` as a user id.** It
|
|
12
|
+
reads it only as the manager's employee `_id`, which is the shape BRI-973 migrates the field
|
|
13
|
+
to. This is a breaking change on a 0.x minor, so read the two notes below before upgrading a
|
|
14
|
+
consumer.
|
|
15
|
+
|
|
16
|
+
**1. Against un-migrated data it returns zero reportees, not an error.** The dual-shape
|
|
17
|
+
widening that carried the rollout is gone. A manager whose rows have not been migrated
|
|
18
|
+
resolves to a self-only scope, and because this function feeds record visibility in
|
|
19
|
+
nfd-api-node and the lead access scope in nfd-api-node-v2, that reads as *"this manager has
|
|
20
|
+
no reportees"* — nothing thrown, nothing logged, just a smaller answer.
|
|
21
|
+
|
|
22
|
+
So the deploy order is **migrate → verify → bump**, not the other way round. Run
|
|
23
|
+
nfd-api-node's `migrations/verify-reporting-manager-to-employee-id.js` and get a clean
|
|
24
|
+
verdict before the first instance carrying 0.32.0 serves traffic. In a rolling deploy that
|
|
25
|
+
window is real, so the migration must be complete rather than merely started.
|
|
26
|
+
|
|
27
|
+
Under npm's 0.x caret rules `^0.31.0` will not resolve to 0.32.0, so no consumer is dragged
|
|
28
|
+
across by an install — the ordering risk is entirely a human one.
|
|
29
|
+
|
|
30
|
+
**2. Callers must pass the FULL employee list.** An `_id` reference only resolves if the row
|
|
31
|
+
carrying it is present, so a pre-filtered list (`{ status: 1 }`, an account scope, anything)
|
|
32
|
+
drops link rows — and what you lose is not the filtered person but everyone beneath them.
|
|
33
|
+
Filter the result instead. `resolveReporteeUserIds` warns once when handed a list in which no
|
|
34
|
+
row carries an `_id` at all, which is the one case where the answer is confidently wrong
|
|
35
|
+
rather than merely narrow.
|
|
36
|
+
|
|
37
|
+
0.32.0 also moves the entity/role vocabulary — `ENTITY`, `ENTITY_STATUS`,
|
|
38
|
+
`isEntityLoginAllowed`, `conflictingEntityFor`, `ROLE_TYPE_PRIORITY`,
|
|
39
|
+
`CLIENT_APP_ROLE_NAMES` — into this package. It was previously written out three times
|
|
40
|
+
(nfd-api-node, nfd-api-node-v2, nfdui-nextjs) and held together by parity tests.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
npm install @i4e/invest4edu-access-core
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
**1. Carry identity per request** (in the auth middleware, JWT path only):
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
import als from "@i4e/invest4edu-access-core/tenant-context";
|
|
54
|
+
// after verifying the JWT:
|
|
55
|
+
return als.run({ account_id, userId, roles }, () => next());
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**2. Enforce on tenant-scoped models:**
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
import { tenantPlugin } from "@i4e/invest4edu-access-core";
|
|
62
|
+
schema.plugin(tenantPlugin); // before mongoose.model(...)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The plugin injects `account_id` from the request's ALS store into the **filter** of every
|
|
66
|
+
operation that selects existing documents:
|
|
67
|
+
|
|
68
|
+
| | ops | switch |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| reads | `find` `findOne` `countDocuments` `distinct` | `TENANT_ENFORCEMENT` / `accessconfig.tenantEnforcement` |
|
|
71
|
+
| writes | `updateOne` `updateMany` `replaceOne` `deleteOne` `deleteMany` `findOneAndUpdate` `findOneAndReplace` `findOneAndDelete` | `TENANT_ENFORCEMENT_WRITES` / `accessconfig.tenantEnforcementWrites` |
|
|
72
|
+
|
|
73
|
+
Each switch is an independent ladder — `off` → `warn` → `enforce` — and **both default to `off`**,
|
|
74
|
+
so installing the plugin is a literal no-op until you arm it. Reads and writes are separate
|
|
75
|
+
because the blast radius is not comparable: a read that gains a filter returns less data, while a
|
|
76
|
+
write that gains a filter silently modifies **nothing** and still reports success. Arming reads
|
|
77
|
+
must never arm writes by surprise.
|
|
78
|
+
|
|
79
|
+
| mode | identity present | identity absent |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| `off` (default) | no-op | no-op |
|
|
82
|
+
| `warn` | reads: inject `account_id` · writes: **unchanged** | log `[tenant] identity-less …`, do **not** scope |
|
|
83
|
+
| `enforce` | inject `account_id` | **throw** (fail-closed) |
|
|
84
|
+
|
|
85
|
+
`warn` deliberately leaves writes alone. `warn` means "tell me what `enforce` would do", and for a
|
|
86
|
+
destructive operation that promise is only kept by changing nothing — injecting a filter under a
|
|
87
|
+
mode named `warn` would turn live `UPDATE`s into silent no-ops, the exact failure burn-in exists to
|
|
88
|
+
catch. What you need before promoting is which writes run identity-less, and that is logged.
|
|
89
|
+
|
|
90
|
+
Precedence per ladder: env var (if valid) → DB value → `off`. An unrecognised value is reported
|
|
91
|
+
once and ignored rather than treated as `off`, so a typo cannot silently disarm enforcement.
|
|
92
|
+
|
|
93
|
+
**3. Bypass for legitimately cross-tenant operations** (caches, migrations, scripts):
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
import { runAsSystem } from "@i4e/invest4edu-access-core";
|
|
97
|
+
await runAsSystem(() => Employee.find({ status: 1 })); // whole scope
|
|
98
|
+
Model.find(q).setOptions({ skipTenant: true }); // one query
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Exports
|
|
102
|
+
|
|
103
|
+
- `als` (default of `/tenant-context`) — the AsyncLocalStorage instance
|
|
104
|
+
- `runWithTenant(store, fn)`, `runAsSystem(fn)`, `getTenantStore()`
|
|
105
|
+
- `tenantPlugin` (default of `/tenant-plugin`) — the Mongoose plugin
|
|
106
|
+
- `setTenantMode(m)` / `getTenantMode()` — the read ladder
|
|
107
|
+
- `setTenantWriteMode(m)` / `getTenantWriteMode()` — the write ladder
|
|
108
|
+
- `resolveReporteeUserIds(roots, employees, opts)` (`/reportee-tree`) — self + all reportees
|
|
109
|
+
- `ENTITY`, `ENTITY_STATUS`, `isEntityLoginAllowed`, `entityInactiveMessage`,
|
|
110
|
+
`checkPortalEntityAccess` (`/entity-status`) — which entities permit a login
|
|
111
|
+
- `conflictingEntityFor`, `entityConflictMessage`, `CONFLICTING_ENTITIES`
|
|
112
|
+
(`/user-entity-link`) — which hats may coexist on one person
|
|
113
|
+
- `ROLE_TYPE_PRIORITY`, `CLIENT_APP_ROLE_NAMES`, `pickPortalRoleName` (`/portal-roles`) —
|
|
114
|
+
which role names a portal session
|
|
115
|
+
- `normalizeRoute(route)`, `screenForRoute(features, route)` (`/route-screen`) — the screen row
|
|
116
|
+
a route resolves to, so a `MODULE.SCREEN.ACTION` code is read off the registry, never spelled
|
|
117
|
+
|
|
118
|
+
## Not covered
|
|
119
|
+
**Aggregation pipelines** — add an explicit `{ $match: { account_id } }` stage.
|
|
120
|
+
|
|
121
|
+
**Inserts** (`save`, `create`, `insertMany`) — and they cannot be. They carry no filter, so there
|
|
122
|
+
is nothing to constrain; putting `account_id` *on* a new document is the caller's job. The plugin
|
|
123
|
+
only governs which **existing** documents an operation may reach.
|
|
124
|
+
|
|
125
|
+
`estimatedDocumentCount` takes no filter either, so it is not hooked.
|
|
126
|
+
|
|
127
|
+
## Compatibility
|
|
128
|
+
Node ≥ 18 (AsyncLocalStorage). `mongoose` ≥ 6 is an optional peer (only the plugin needs it).
|
package/package.json
CHANGED
|
@@ -1,56 +1,57 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@i4e/invest4edu-access-core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Shared access-control primitives for NeoFindesk: tenant keystone, role capabilities, reportee tree, entity/role vocabulary, feature flags, and the unified access engine (registry schema, snapshot resolver, visibleWhen).",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"exports": {
|
|
7
|
-
".": "./src/index.js",
|
|
8
|
-
"./tenant-context": "./src/tenant-context.js",
|
|
9
|
-
"./tenant-plugin": "./src/tenant-plugin.js",
|
|
10
|
-
"./role-capabilities": "./src/role-capabilities.js",
|
|
11
|
-
"./reportee-tree": "./src/reportee-tree.js",
|
|
12
|
-
"./entity-status": "./src/entity-status.js",
|
|
13
|
-
"./user-entity-link": "./src/user-entity-link.js",
|
|
14
|
-
"./portal-roles": "./src/portal-roles.js",
|
|
15
|
-
"./access-config": "./src/access-config.js",
|
|
16
|
-
"./access-schema": "./src/access-schema.js",
|
|
17
|
-
"./access-resolver": "./src/access-resolver.js",
|
|
18
|
-
"./visible-when": "./src/visible-when.js",
|
|
19
|
-
"./entitlement": "./src/entitlement.js",
|
|
20
|
-
"./entitlement-schema": "./src/entitlement-schema.js",
|
|
21
|
-
"./grid-schema": "./src/grid-schema.js",
|
|
22
|
-
"./route-features": "./src/route-features.js",
|
|
23
|
-
"./
|
|
24
|
-
"./
|
|
25
|
-
"./
|
|
26
|
-
"./
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
"
|
|
56
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@i4e/invest4edu-access-core",
|
|
3
|
+
"version": "0.33.0",
|
|
4
|
+
"description": "Shared access-control primitives for NeoFindesk: tenant keystone, role capabilities, reportee tree, entity/role vocabulary, feature flags, and the unified access engine (registry schema, snapshot resolver, visibleWhen).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.js",
|
|
8
|
+
"./tenant-context": "./src/tenant-context.js",
|
|
9
|
+
"./tenant-plugin": "./src/tenant-plugin.js",
|
|
10
|
+
"./role-capabilities": "./src/role-capabilities.js",
|
|
11
|
+
"./reportee-tree": "./src/reportee-tree.js",
|
|
12
|
+
"./entity-status": "./src/entity-status.js",
|
|
13
|
+
"./user-entity-link": "./src/user-entity-link.js",
|
|
14
|
+
"./portal-roles": "./src/portal-roles.js",
|
|
15
|
+
"./access-config": "./src/access-config.js",
|
|
16
|
+
"./access-schema": "./src/access-schema.js",
|
|
17
|
+
"./access-resolver": "./src/access-resolver.js",
|
|
18
|
+
"./visible-when": "./src/visible-when.js",
|
|
19
|
+
"./entitlement": "./src/entitlement.js",
|
|
20
|
+
"./entitlement-schema": "./src/entitlement-schema.js",
|
|
21
|
+
"./grid-schema": "./src/grid-schema.js",
|
|
22
|
+
"./route-features": "./src/route-features.js",
|
|
23
|
+
"./route-screen": "./src/route-screen.js",
|
|
24
|
+
"./subscription-lifecycle": "./src/subscription-lifecycle.js",
|
|
25
|
+
"./entitlement-store": "./src/entitlement-store.js",
|
|
26
|
+
"./proration": "./src/proration.js",
|
|
27
|
+
"./credits": "./src/credits.js"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "node --test test/*.test.mjs test/*.test.js"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"src",
|
|
34
|
+
"README.md"
|
|
35
|
+
],
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18"
|
|
38
|
+
},
|
|
39
|
+
"sideEffects": false,
|
|
40
|
+
"keywords": [
|
|
41
|
+
"nfd",
|
|
42
|
+
"access-control",
|
|
43
|
+
"tenant-isolation",
|
|
44
|
+
"mongoose",
|
|
45
|
+
"async-local-storage"
|
|
46
|
+
],
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"mongoose": ">=6"
|
|
49
|
+
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"mongoose": {
|
|
52
|
+
"optional": true
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"license": "UNLICENSED",
|
|
56
|
+
"private": false
|
|
57
|
+
}
|
package/src/access-config.js
CHANGED
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Access feature flags — @i4e/invest4edu-access-core (Track 3 master kill-switch).
|
|
3
|
-
*
|
|
4
|
-
* A single place to toggle the newer, riskier access behaviours on/off WITHOUT a code deploy,
|
|
5
|
-
* so a rollout can ship "off" (legacy behaviour) and be flipped on — or killed instantly if it
|
|
6
|
-
* misbehaves. Precedence (lowest → highest):
|
|
7
|
-
*
|
|
8
|
-
* DEFAULT_ACCESS_FLAGS < DB (`accessconfig` collection) < env var
|
|
9
|
-
*
|
|
10
|
-
* DB toggles propagate in ~60s via the backend helper's background refresh (edit in the admin
|
|
11
|
-
* UI, no restart). The env var is the emergency kill: set `ACCESS_FLAG_<UPPER_SNAKE>=off` on the
|
|
12
|
-
* App Service and restart to force a flag regardless of DB.
|
|
13
|
-
*
|
|
14
|
-
* ALL defaults are the SAFE/legacy value — turning a flag on is an explicit, reversible act.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
export const DEFAULT_ACCESS_FLAGS = Object.freeze({
|
|
18
|
-
// #1 — v2 client-lead reads use the canonical 5-field visibility engine (else legacy lead-scope).
|
|
19
|
-
v2LeadFiveFieldVisibility: false,
|
|
20
|
-
// M1 — send grant/revoke emails via the comms service.
|
|
21
|
-
delegationEmailNotifications: false,
|
|
22
|
-
// H3 — write an access-event when giver-widening actually broadened a delegate's result.
|
|
23
|
-
delegationReadAudit: false,
|
|
24
|
-
// H1 — enable the "acting-as" session path (x-acting-as-giver).
|
|
25
|
-
delegationActingAs: false,
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const ENV_PREFIX = "ACCESS_FLAG_";
|
|
29
|
-
|
|
30
|
-
/** camelCase flag key → env var name, e.g. v2LeadFiveFieldVisibility → ACCESS_FLAG_V2_LEAD_FIVE_FIELD_VISIBILITY */
|
|
31
|
-
export function flagEnvName(key) {
|
|
32
|
-
return ENV_PREFIX + key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toUpperCase();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export const ACCESS_FLAG_ENV_NAMES = Object.freeze(
|
|
36
|
-
Object.fromEntries(Object.keys(DEFAULT_ACCESS_FLAGS).map((k) => [k, flagEnvName(k)])),
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
function toBool(v) {
|
|
40
|
-
if (typeof v === "boolean") return v;
|
|
41
|
-
if (v == null || v === "") return undefined;
|
|
42
|
-
return /^(1|true|on|yes|enabled)$/i.test(String(v).trim());
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Read any recognised flags from a process.env-shaped object. Unset vars are omitted. */
|
|
46
|
-
export function readEnvFlags(env = {}) {
|
|
47
|
-
const out = {};
|
|
48
|
-
for (const key of Object.keys(DEFAULT_ACCESS_FLAGS)) {
|
|
49
|
-
const b = toBool(env[flagEnvName(key)]);
|
|
50
|
-
if (b !== undefined) out[key] = b;
|
|
51
|
-
}
|
|
52
|
-
return out;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Merge sources into the effective flag set. Only known keys are honoured.
|
|
57
|
-
* @param {{ dbFlags?: object, envFlags?: object }} sources
|
|
58
|
-
*/
|
|
59
|
-
export function resolveFlags({ dbFlags = {}, envFlags = {} } = {}) {
|
|
60
|
-
const out = { ...DEFAULT_ACCESS_FLAGS };
|
|
61
|
-
for (const key of Object.keys(DEFAULT_ACCESS_FLAGS)) {
|
|
62
|
-
const d = toBool(dbFlags ? dbFlags[key] : undefined);
|
|
63
|
-
if (d !== undefined) out[key] = d;
|
|
64
|
-
const e = toBool(envFlags ? envFlags[key] : undefined);
|
|
65
|
-
if (e !== undefined) out[key] = e; // env wins — emergency kill
|
|
66
|
-
}
|
|
67
|
-
return out;
|
|
68
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Access feature flags — @i4e/invest4edu-access-core (Track 3 master kill-switch).
|
|
3
|
+
*
|
|
4
|
+
* A single place to toggle the newer, riskier access behaviours on/off WITHOUT a code deploy,
|
|
5
|
+
* so a rollout can ship "off" (legacy behaviour) and be flipped on — or killed instantly if it
|
|
6
|
+
* misbehaves. Precedence (lowest → highest):
|
|
7
|
+
*
|
|
8
|
+
* DEFAULT_ACCESS_FLAGS < DB (`accessconfig` collection) < env var
|
|
9
|
+
*
|
|
10
|
+
* DB toggles propagate in ~60s via the backend helper's background refresh (edit in the admin
|
|
11
|
+
* UI, no restart). The env var is the emergency kill: set `ACCESS_FLAG_<UPPER_SNAKE>=off` on the
|
|
12
|
+
* App Service and restart to force a flag regardless of DB.
|
|
13
|
+
*
|
|
14
|
+
* ALL defaults are the SAFE/legacy value — turning a flag on is an explicit, reversible act.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export const DEFAULT_ACCESS_FLAGS = Object.freeze({
|
|
18
|
+
// #1 — v2 client-lead reads use the canonical 5-field visibility engine (else legacy lead-scope).
|
|
19
|
+
v2LeadFiveFieldVisibility: false,
|
|
20
|
+
// M1 — send grant/revoke emails via the comms service.
|
|
21
|
+
delegationEmailNotifications: false,
|
|
22
|
+
// H3 — write an access-event when giver-widening actually broadened a delegate's result.
|
|
23
|
+
delegationReadAudit: false,
|
|
24
|
+
// H1 — enable the "acting-as" session path (x-acting-as-giver).
|
|
25
|
+
delegationActingAs: false,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const ENV_PREFIX = "ACCESS_FLAG_";
|
|
29
|
+
|
|
30
|
+
/** camelCase flag key → env var name, e.g. v2LeadFiveFieldVisibility → ACCESS_FLAG_V2_LEAD_FIVE_FIELD_VISIBILITY */
|
|
31
|
+
export function flagEnvName(key) {
|
|
32
|
+
return ENV_PREFIX + key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toUpperCase();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const ACCESS_FLAG_ENV_NAMES = Object.freeze(
|
|
36
|
+
Object.fromEntries(Object.keys(DEFAULT_ACCESS_FLAGS).map((k) => [k, flagEnvName(k)])),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
function toBool(v) {
|
|
40
|
+
if (typeof v === "boolean") return v;
|
|
41
|
+
if (v == null || v === "") return undefined;
|
|
42
|
+
return /^(1|true|on|yes|enabled)$/i.test(String(v).trim());
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Read any recognised flags from a process.env-shaped object. Unset vars are omitted. */
|
|
46
|
+
export function readEnvFlags(env = {}) {
|
|
47
|
+
const out = {};
|
|
48
|
+
for (const key of Object.keys(DEFAULT_ACCESS_FLAGS)) {
|
|
49
|
+
const b = toBool(env[flagEnvName(key)]);
|
|
50
|
+
if (b !== undefined) out[key] = b;
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Merge sources into the effective flag set. Only known keys are honoured.
|
|
57
|
+
* @param {{ dbFlags?: object, envFlags?: object }} sources
|
|
58
|
+
*/
|
|
59
|
+
export function resolveFlags({ dbFlags = {}, envFlags = {} } = {}) {
|
|
60
|
+
const out = { ...DEFAULT_ACCESS_FLAGS };
|
|
61
|
+
for (const key of Object.keys(DEFAULT_ACCESS_FLAGS)) {
|
|
62
|
+
const d = toBool(dbFlags ? dbFlags[key] : undefined);
|
|
63
|
+
if (d !== undefined) out[key] = d;
|
|
64
|
+
const e = toBool(envFlags ? envFlags[key] : undefined);
|
|
65
|
+
if (e !== undefined) out[key] = e; // env wins — emergency kill
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|