@i4e/invest4edu-access-core 0.1.0 → 0.3.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/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "@i4e/invest4edu-access-core",
3
- "version": "0.1.0",
4
- "description": "Shared tenant-isolation / access-control primitives (Track 3 D1 keystone) for NeoFindesk backends.",
3
+ "version": "0.3.0",
4
+ "description": "Shared tenant-isolation / access-control primitives (Track 3 D1 keystone + role capabilities + reportee tree) for NeoFindesk backends.",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": "./src/index.js",
8
8
  "./tenant-context": "./src/tenant-context.js",
9
- "./tenant-plugin": "./src/tenant-plugin.js"
9
+ "./tenant-plugin": "./src/tenant-plugin.js",
10
+ "./role-capabilities": "./src/role-capabilities.js",
11
+ "./reportee-tree": "./src/reportee-tree.js"
10
12
  },
11
13
  "files": [
12
14
  "src",
package/src/index.js CHANGED
@@ -1,6 +1,13 @@
1
1
  /**
2
- * Access-core barrel (Track 3 C1). CANONICAL SHARED — keep identical in both backends.
3
- * The tenant-enforcement primitives authored once and vendored into v1 + v2.
2
+ * @i4e/invest4edu-access-core — shared access-control primitives for the NFD backends.
3
+ * Tenant isolation (D1 keystone) + role capabilities (G7).
4
4
  */
5
5
  export { default as als, runWithTenant, runAsSystem, getTenantStore } from "./tenant-context.js";
6
6
  export { default as tenantPlugin } from "./tenant-plugin.js";
7
+ export {
8
+ CAPABILITY_FLAGS,
9
+ DEFAULT_ROLE_CAPABILITIES,
10
+ buildCapabilityMap,
11
+ resolveCapabilities,
12
+ } from "./role-capabilities.js";
13
+ export { resolveReporteeUserIds } from "./reportee-tree.js";
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Reportee-tree DFS — @i4e/invest4edu-access-core (Track 3 G3 shared primitive).
3
+ *
4
+ * The one canonical "self + all reportees" traversal, shared by both backends so v1 and v2
5
+ * compose the SAME hierarchy scope. Pure + data-source agnostic: the caller passes a flat
6
+ * employees list `[{ user_id, reporting_manager }]` (v1 feeds the Redis snapshot, v2 the
7
+ * `employees` collection) and one-or-more root user ids (delegation passes `[self, ...givers]`).
8
+ *
9
+ * @param {string|string[]} roots root user id(s)
10
+ * @param {Array<{user_id:*, reporting_manager:*}>} employees flat employee list
11
+ * @returns {string[]} de-duplicated reachable set (roots + all reportees) as STRING ids
12
+ */
13
+ export function resolveReporteeUserIds(roots, employees) {
14
+ const rootIds = (Array.isArray(roots) ? roots : [roots]).filter((id) => id != null).map(String);
15
+
16
+ const byManager = new Map();
17
+ for (const emp of employees || []) {
18
+ if (!emp || !emp.user_id) continue;
19
+ const managerId = String(emp.reporting_manager);
20
+ if (!byManager.has(managerId)) byManager.set(managerId, []);
21
+ byManager.get(managerId).push(String(emp.user_id));
22
+ }
23
+
24
+ const visited = new Set();
25
+ const result = new Set();
26
+ const stack = [...rootIds];
27
+ while (stack.length) {
28
+ const id = stack.pop();
29
+ if (!id || visited.has(id)) continue;
30
+ visited.add(id);
31
+ result.add(id);
32
+ for (const rep of byManager.get(id) || []) stack.push(rep);
33
+ }
34
+ return [...result];
35
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Role capabilities — @i4e/invest4edu-access-core (Track 3 C1 · G7).
3
+ *
4
+ * Turns role classification from hardcoded per-repo Sets into DATA. A `roleCapabilities`
5
+ * collection (one row per role) overrides these defaults; with no rows, `resolveCapabilities`
6
+ * reproduces the exact legacy behaviour, so wiring this in is behaviour-neutral until config
7
+ * is populated.
8
+ *
9
+ * Flags:
10
+ * isFullAccess — sees all data within the account (BROKER_ADMIN-style super-role)
11
+ * isEmployee — eligible for the reportee-tree / 5-field visibility scope
12
+ * canGovernDelegation — may grant / edit / reactivate delegations
13
+ * canCorrectE1 — may run the audited E1 correction
14
+ * canCrossAccount — audited sysadmin cross-account path (default: none)
15
+ * productScoped — pages + records restricted to the user's mapped products
16
+ */
17
+
18
+ export const CAPABILITY_FLAGS = Object.freeze([
19
+ "isFullAccess",
20
+ "isEmployee",
21
+ "canGovernDelegation",
22
+ "canCorrectE1",
23
+ "canCrossAccount",
24
+ "productScoped",
25
+ ]);
26
+
27
+ /**
28
+ * Legacy-parity defaults. MUST mirror the hardcoded Sets they replace:
29
+ * FULL_ACCESS_ROLES = SUPER_ADMIN, SYSTEM_ADMIN, BROKER_ADMIN, BACKOFFICE_USER, HR_USER
30
+ * EMPLOYEE_ROLES = SALES_USER, PAT_SALES
31
+ * GRANT_ROLES (deleg)= SUPER_ADMIN, SYSTEM_ADMIN, BROKER_ADMIN
32
+ * ALLOWED_ROLES (e1) = SUPER_ADMIN, SYSTEM_ADMIN, BROKER_ADMIN, BACKOFFICE_USER
33
+ */
34
+ export const DEFAULT_ROLE_CAPABILITIES = Object.freeze({
35
+ SUPER_ADMIN: { isFullAccess: true, canGovernDelegation: true, canCorrectE1: true },
36
+ SYSTEM_ADMIN: { isFullAccess: true, canGovernDelegation: true, canCorrectE1: true },
37
+ BROKER_ADMIN: { isFullAccess: true, canGovernDelegation: true, canCorrectE1: true },
38
+ BACKOFFICE_USER: { isFullAccess: true, canCorrectE1: true },
39
+ HR_USER: { isFullAccess: true },
40
+ SALES_USER: { isEmployee: true },
41
+ PAT_SALES: { isEmployee: true },
42
+ });
43
+
44
+ function pickFlags(row) {
45
+ const out = {};
46
+ for (const f of CAPABILITY_FLAGS) if (row[f] != null) out[f] = !!row[f];
47
+ return out;
48
+ }
49
+
50
+ /**
51
+ * Merge DB rows over the parity defaults → a role→capabilities map.
52
+ * @param {Array<{role:string} & Record<string, boolean>>} dbRows
53
+ */
54
+ export function buildCapabilityMap(dbRows = []) {
55
+ const map = {};
56
+ for (const [role, caps] of Object.entries(DEFAULT_ROLE_CAPABILITIES)) map[role] = { ...caps };
57
+ for (const row of dbRows) {
58
+ if (!row || !row.role) continue;
59
+ map[row.role] = { ...(map[row.role] || {}), ...pickFlags(row) };
60
+ }
61
+ return map;
62
+ }
63
+
64
+ /**
65
+ * OR-combine the capability flags across all of a caller's roles (multi-role = union).
66
+ * @param {string[]} roles
67
+ * @param {Record<string, Record<string, boolean>>} [capabilityMap] defaults if omitted
68
+ * @returns {Record<string, boolean>} every CAPABILITY_FLAG resolved to a boolean
69
+ */
70
+ export function resolveCapabilities(roles, capabilityMap = DEFAULT_ROLE_CAPABILITIES) {
71
+ const out = {};
72
+ for (const f of CAPABILITY_FLAGS) out[f] = false;
73
+ for (const role of Array.isArray(roles) ? roles : [roles].filter(Boolean)) {
74
+ const caps = capabilityMap[role];
75
+ if (!caps) continue;
76
+ for (const f of CAPABILITY_FLAGS) if (caps[f]) out[f] = true;
77
+ }
78
+ return out;
79
+ }