@astram/identity 0.13.0 → 0.14.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.
Files changed (88) hide show
  1. package/dist/features/authz/grant-admin.service.d.ts +1 -1
  2. package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
  3. package/dist/features/authz/grant-admin.service.js +8 -0
  4. package/dist/features/authz/grant-ceiling.d.ts +11 -3
  5. package/dist/features/authz/grant-ceiling.d.ts.map +1 -1
  6. package/dist/features/authz/grant-ceiling.js +20 -3
  7. package/dist/features/authz/record-authorization.d.ts +1 -1
  8. package/dist/features/authz/record-authorization.d.ts.map +1 -1
  9. package/dist/features/authz/record-authorization.js +3 -2
  10. package/dist/features/company/company-hierarchy.service.d.ts +6 -0
  11. package/dist/features/company/company-hierarchy.service.d.ts.map +1 -1
  12. package/dist/features/company/company-hierarchy.service.js +19 -0
  13. package/dist/features/identity-feature.module.d.ts.map +1 -1
  14. package/dist/features/identity-feature.module.js +13 -0
  15. package/dist/features/membership/membership.service.d.ts +9 -0
  16. package/dist/features/membership/membership.service.d.ts.map +1 -1
  17. package/dist/features/membership/membership.service.js +40 -0
  18. package/dist/features/membership/posting.service.d.ts +18 -0
  19. package/dist/features/membership/posting.service.d.ts.map +1 -1
  20. package/dist/features/membership/posting.service.js +85 -27
  21. package/dist/features/org-bootstrap/org-bootstrap-error.d.ts +20 -0
  22. package/dist/features/org-bootstrap/org-bootstrap-error.d.ts.map +1 -0
  23. package/dist/features/org-bootstrap/org-bootstrap-error.js +24 -0
  24. package/dist/features/org-bootstrap/org-bootstrap-file.d.ts +58 -0
  25. package/dist/features/org-bootstrap/org-bootstrap-file.d.ts.map +1 -0
  26. package/dist/features/org-bootstrap/org-bootstrap-file.js +323 -0
  27. package/dist/features/org-bootstrap/org-bootstrap-plan.d.ts +79 -0
  28. package/dist/features/org-bootstrap/org-bootstrap-plan.d.ts.map +1 -0
  29. package/dist/features/org-bootstrap/org-bootstrap-plan.js +288 -0
  30. package/dist/features/org-bootstrap/org-bootstrap-runner.harness.d.ts +259 -0
  31. package/dist/features/org-bootstrap/org-bootstrap-runner.harness.d.ts.map +1 -0
  32. package/dist/features/org-bootstrap/org-bootstrap-runner.harness.js +604 -0
  33. package/dist/features/org-bootstrap/org-bootstrap-runner.mocks.d.ts +24 -0
  34. package/dist/features/org-bootstrap/org-bootstrap-runner.mocks.d.ts.map +1 -0
  35. package/dist/features/org-bootstrap/org-bootstrap-runner.mocks.js +39 -0
  36. package/dist/features/org-bootstrap/org-bootstrap.manifest.d.ts +19 -0
  37. package/dist/features/org-bootstrap/org-bootstrap.manifest.d.ts.map +1 -0
  38. package/dist/features/org-bootstrap/org-bootstrap.manifest.js +30 -0
  39. package/dist/features/org-bootstrap/org-bootstrap.tokens.d.ts +8 -0
  40. package/dist/features/org-bootstrap/org-bootstrap.tokens.d.ts.map +1 -0
  41. package/dist/features/org-bootstrap/org-bootstrap.tokens.js +10 -0
  42. package/dist/features/org-bootstrap/org-names.d.ts +12 -0
  43. package/dist/features/org-bootstrap/org-names.d.ts.map +1 -0
  44. package/dist/features/org-bootstrap/org-names.js +26 -0
  45. package/dist/features/org-bootstrap/org-state.reader.d.ts +15 -0
  46. package/dist/features/org-bootstrap/org-state.reader.d.ts.map +1 -0
  47. package/dist/features/org-bootstrap/org-state.reader.js +225 -0
  48. package/dist/features/org-bootstrap/root-password-source.d.ts +9 -0
  49. package/dist/features/org-bootstrap/root-password-source.d.ts.map +1 -0
  50. package/dist/features/org-bootstrap/root-password-source.js +57 -0
  51. package/dist/features/org-bootstrap/run-org-bootstrap.d.ts +42 -0
  52. package/dist/features/org-bootstrap/run-org-bootstrap.d.ts.map +1 -0
  53. package/dist/features/org-bootstrap/run-org-bootstrap.js +688 -0
  54. package/dist/features/org-bootstrap/set-password-links.d.ts +30 -0
  55. package/dist/features/org-bootstrap/set-password-links.d.ts.map +1 -0
  56. package/dist/features/org-bootstrap/set-password-links.js +55 -0
  57. package/dist/features/org-structure/org-catalog.service.d.ts +13 -0
  58. package/dist/features/org-structure/org-catalog.service.d.ts.map +1 -1
  59. package/dist/features/org-structure/org-catalog.service.js +40 -0
  60. package/dist/features/role/role-owner-transition.d.ts +17 -0
  61. package/dist/features/role/role-owner-transition.d.ts.map +1 -0
  62. package/dist/features/role/role-owner-transition.js +23 -0
  63. package/dist/features/role/role.controller.d.ts +1 -1
  64. package/dist/features/role/role.controller.d.ts.map +1 -1
  65. package/dist/features/role/role.controller.js +6 -4
  66. package/dist/features/role/role.service.d.ts +8 -2
  67. package/dist/features/role/role.service.d.ts.map +1 -1
  68. package/dist/features/role/role.service.js +13 -3
  69. package/dist/features/user/credential-provisioner.token.d.ts +8 -0
  70. package/dist/features/user/credential-provisioner.token.d.ts.map +1 -0
  71. package/dist/features/user/credential-provisioner.token.js +10 -0
  72. package/dist/features/user/user.controller.d.ts +1 -3
  73. package/dist/features/user/user.controller.d.ts.map +1 -1
  74. package/dist/features/user/user.controller.js +2 -2
  75. package/dist/features/user/user.service.d.ts +28 -5
  76. package/dist/features/user/user.service.d.ts.map +1 -1
  77. package/dist/features/user/user.service.js +45 -12
  78. package/dist/index.d.ts +6 -0
  79. package/dist/index.d.ts.map +1 -1
  80. package/dist/index.js +9 -1
  81. package/dist/internal.d.ts +2 -0
  82. package/dist/internal.d.ts.map +1 -1
  83. package/dist/internal.js +3 -1
  84. package/dist/role.object.d.ts +1 -0
  85. package/dist/role.object.d.ts.map +1 -1
  86. package/dist/role.object.js +8 -0
  87. package/migrations/1710000000101-AddRoleOwnerCompany.cjs +98 -0
  88. package/package.json +3 -3
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OrgBootstrapError = void 0;
4
+ /**
5
+ * A refusal or failure of the org bootstrap run. The message is fixed text plus names from the
6
+ * operator's own file; it never carries a password, a link or token, or a driver message.
7
+ */
8
+ class OrgBootstrapError extends Error {
9
+ code;
10
+ /** `step_failed` only: the manifest step key that failed. */
11
+ step;
12
+ /** `step_failed` only: what the run had written before it stopped. */
13
+ written;
14
+ constructor(code, message, extra = {}) {
15
+ super(message);
16
+ this.name = 'OrgBootstrapError';
17
+ this.code = code;
18
+ if (extra.step !== undefined)
19
+ this.step = extra.step;
20
+ if (extra.written !== undefined)
21
+ this.written = extra.written;
22
+ }
23
+ }
24
+ exports.OrgBootstrapError = OrgBootstrapError;
@@ -0,0 +1,58 @@
1
+ export interface OrgBootstrapRoot {
2
+ username: string;
3
+ email: string;
4
+ /** Tenant path: `A` or `A/B` (tenant name, then child names). */
5
+ homeCompany: string;
6
+ }
7
+ export interface OrgBootstrapRole {
8
+ name: string;
9
+ permissions: string[];
10
+ }
11
+ export interface OrgBootstrapPosting {
12
+ role: string;
13
+ department?: string;
14
+ branch?: string;
15
+ }
16
+ export interface OrgBootstrapUser {
17
+ username: string;
18
+ email: string;
19
+ /** Path of child names relative to the tenant (`A/B` means tenant > A > B). */
20
+ company?: string;
21
+ postings?: OrgBootstrapPosting[];
22
+ }
23
+ export interface OrgBootstrapCompany {
24
+ name: string;
25
+ departments?: string[];
26
+ branches?: string[];
27
+ roles?: OrgBootstrapRole[];
28
+ users?: OrgBootstrapUser[];
29
+ children?: OrgBootstrapCompany[];
30
+ }
31
+ export interface OrgBootstrapFile {
32
+ version: 1;
33
+ roots: OrgBootstrapRoot[];
34
+ tenants: OrgBootstrapCompany[];
35
+ }
36
+ export interface OrgBootstrapFileIssue {
37
+ path: string;
38
+ message: string;
39
+ }
40
+ export interface OrgBootstrapFileOptions {
41
+ /** `GrantAdminService.listCatalog()` keys: catalog keys plus computed wildcards. */
42
+ grantableKeys: ReadonlySet<string>;
43
+ }
44
+ /** The closed key sets, exported so the schema parity test compares against the parser's own. */
45
+ export declare const ORG_FILE_KEYS: {
46
+ readonly file: readonly ["version", "roots", "tenants"];
47
+ readonly root: readonly ["username", "email", "homeCompany"];
48
+ readonly company: readonly ["name", "departments", "branches", "roles", "users", "children"];
49
+ readonly role: readonly ["name", "permissions"];
50
+ readonly user: readonly ["username", "email", "company", "postings"];
51
+ readonly posting: readonly ["role", "department", "branch"];
52
+ };
53
+ export declare class OrgBootstrapFileError extends Error {
54
+ readonly issues: OrgBootstrapFileIssue[];
55
+ constructor(issues: OrgBootstrapFileIssue[]);
56
+ }
57
+ export declare function parseOrgBootstrapFile(input: unknown, options: OrgBootstrapFileOptions): OrgBootstrapFile;
58
+ //# sourceMappingURL=org-bootstrap-file.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"org-bootstrap-file.d.ts","sourceRoot":"","sources":["../../../src/features/org-bootstrap/org-bootstrap-file.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,iEAAiE;IACjE,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,mBAAmB,EAAE,CAAC;CAClC;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC3B,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC3B,QAAQ,CAAC,EAAE,mBAAmB,EAAE,CAAC;CAClC;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,CAAC,CAAC;IACX,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAC1B,OAAO,EAAE,mBAAmB,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,uBAAuB;IACtC,oFAAoF;IACpF,aAAa,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CACpC;AAED,iGAAiG;AACjG,eAAO,MAAM,aAAa;;;;;;;CAOhB,CAAC;AAEX,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,QAAQ,CAAC,MAAM,EAAE,qBAAqB,EAAE,CAAC;gBAC7B,MAAM,EAAE,qBAAqB,EAAE;CAO5C;AAmBD,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,uBAAuB,GAC/B,gBAAgB,CAqRlB"}
@@ -0,0 +1,323 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OrgBootstrapFileError = exports.ORG_FILE_KEYS = void 0;
4
+ exports.parseOrgBootstrapFile = parseOrgBootstrapFile;
5
+ /**
6
+ * G-009 T8: strict, pure parser for the org bootstrap file (version 1).
7
+ *
8
+ * Hand-written (the repo carries no validator library) with closed key sets at every level;
9
+ * `org-bootstrap.schema.json` documents the same shape and a parity test keeps them in step.
10
+ * No I/O. Every problem is collected as `{ path, message }`; messages never echo input values, so a
11
+ * stray `password` field is reported by name only.
12
+ */
13
+ const guard_1 = require("@astram/guard");
14
+ const org_names_js_1 = require("./org-names.js");
15
+ /** The closed key sets, exported so the schema parity test compares against the parser's own. */
16
+ exports.ORG_FILE_KEYS = {
17
+ file: ['version', 'roots', 'tenants'],
18
+ root: ['username', 'email', 'homeCompany'],
19
+ company: ['name', 'departments', 'branches', 'roles', 'users', 'children'],
20
+ role: ['name', 'permissions'],
21
+ user: ['username', 'email', 'company', 'postings'],
22
+ posting: ['role', 'department', 'branch'],
23
+ };
24
+ class OrgBootstrapFileError extends Error {
25
+ issues;
26
+ constructor(issues) {
27
+ super(`Org bootstrap file is invalid (${issues.length} issue${issues.length === 1 ? '' : 's'}).`);
28
+ this.name = 'OrgBootstrapFileError';
29
+ this.issues = issues;
30
+ }
31
+ }
32
+ exports.OrgBootstrapFileError = OrgBootstrapFileError;
33
+ const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
34
+ const join = (base, key) => (base === '' ? key : `${base}.${key}`);
35
+ function parseOrgBootstrapFile(input, options) {
36
+ const issues = [];
37
+ const add = (path, message) => issues.push({ path, message });
38
+ /** A record with only the allowed keys; unknown keys are reported by name, never by value. */
39
+ const closed = (v, path, allowed) => {
40
+ if (!isRecord(v)) {
41
+ add(path === '' ? '(file)' : path, 'must be an object');
42
+ return undefined;
43
+ }
44
+ for (const k of Object.keys(v)) {
45
+ if (!allowed.includes(k))
46
+ add(join(path, k), 'unknown field');
47
+ }
48
+ return v;
49
+ };
50
+ const text = (v, path) => {
51
+ if (typeof v !== 'string' || v.trim() === '') {
52
+ add(path, 'must be a non-empty string');
53
+ return undefined;
54
+ }
55
+ return v.trim();
56
+ };
57
+ const optionalArray = (v, path) => {
58
+ if (v === undefined)
59
+ return undefined;
60
+ if (!Array.isArray(v)) {
61
+ add(path, 'must be an array');
62
+ return undefined;
63
+ }
64
+ return v;
65
+ };
66
+ const nameList = (v, path) => {
67
+ const out = [];
68
+ const seen = new Set();
69
+ (optionalArray(v, path) ?? []).forEach((item, i) => {
70
+ const p = `${path}[${i}]`;
71
+ const s = text(item, p);
72
+ if (s === undefined)
73
+ return;
74
+ // R3-1: duplicates compare like the DB collation (case, accents, trailing space).
75
+ if (seen.has((0, org_names_js_1.nameKey)(s)))
76
+ add(p, 'duplicate within this company');
77
+ seen.add((0, org_names_js_1.nameKey)(s));
78
+ out.push(s);
79
+ });
80
+ return out;
81
+ };
82
+ const f = closed(input, '', exports.ORG_FILE_KEYS.file);
83
+ if (!f)
84
+ throw new OrgBootstrapFileError(issues);
85
+ if (f.version !== 1)
86
+ add('version', 'must be the number 1');
87
+ // ---- pass 1: companies (structure, roles, name uniqueness), building the lookup tree ----
88
+ const globalRoles = new Set();
89
+ const tenantNodes = new Map();
90
+ const nodes = []; // depth-first, parent before children
91
+ const walkCompany = (raw, path, parent, siblings) => {
92
+ const c = closed(raw, path, exports.ORG_FILE_KEYS.company);
93
+ if (!c)
94
+ return undefined;
95
+ const name = text(c.name, `${path}.name`);
96
+ if (name !== undefined && name.includes('/'))
97
+ add(`${path}.name`, 'must not contain "/"');
98
+ const departments = nameList(c.departments, `${path}.departments`);
99
+ const branches = nameList(c.branches, `${path}.branches`);
100
+ const roles = [];
101
+ const roleNames = new Set();
102
+ (optionalArray(c.roles, `${path}.roles`) ?? []).forEach((rr, i) => {
103
+ const rp = `${path}.roles[${i}]`;
104
+ const r = closed(rr, rp, exports.ORG_FILE_KEYS.role);
105
+ if (!r)
106
+ return;
107
+ const rname = text(r.name, `${rp}.name`);
108
+ const perms = [];
109
+ if (!Array.isArray(r.permissions)) {
110
+ add(`${rp}.permissions`, 'must be an array of permission keys');
111
+ }
112
+ else {
113
+ r.permissions.forEach((k, j) => {
114
+ const kp = `${rp}.permissions[${j}]`;
115
+ if (typeof k !== 'string')
116
+ add(kp, 'must be a string');
117
+ else if (!options.grantableKeys.has(k))
118
+ add(kp, 'not a grantable permission key');
119
+ else
120
+ perms.push(k);
121
+ });
122
+ }
123
+ if (rname !== undefined) {
124
+ if (globalRoles.has((0, org_names_js_1.nameKey)(rname)))
125
+ add(`${rp}.name`, 'duplicate role name (roles are global by name)');
126
+ globalRoles.add((0, org_names_js_1.nameKey)(rname));
127
+ roleNames.add(rname);
128
+ roles.push({ name: rname, permissions: perms });
129
+ }
130
+ });
131
+ const node = {
132
+ name: name ?? '',
133
+ path,
134
+ parent,
135
+ departments: new Set(departments),
136
+ branches: new Set(branches),
137
+ roles: roleNames,
138
+ children: new Map(),
139
+ rawUsers: c.users,
140
+ usersPath: `${path}.users`,
141
+ };
142
+ if (name !== undefined) {
143
+ const clash = [...siblings.keys()].some((k) => (0, org_names_js_1.nameKey)(k) === (0, org_names_js_1.nameKey)(name));
144
+ if (clash)
145
+ add(`${path}.name`, 'duplicate company name among siblings');
146
+ else
147
+ siblings.set(name, node);
148
+ }
149
+ nodes.push(node);
150
+ const children = [];
151
+ (optionalArray(c.children, `${path}.children`) ?? []).forEach((cc, i) => {
152
+ const parsed = walkCompany(cc, `${path}.children[${i}]`, node, node.children);
153
+ if (parsed)
154
+ children.push(parsed);
155
+ });
156
+ const out = { name: name ?? '' };
157
+ if (c.departments !== undefined)
158
+ out.departments = departments;
159
+ if (c.branches !== undefined)
160
+ out.branches = branches;
161
+ if (c.roles !== undefined)
162
+ out.roles = roles;
163
+ if (children.length > 0 || c.children !== undefined)
164
+ out.children = children;
165
+ return out;
166
+ };
167
+ const tenants = [];
168
+ const tenantsRaw = f.tenants;
169
+ if (!Array.isArray(tenantsRaw))
170
+ add('tenants', 'must be an array');
171
+ else if (tenantsRaw.length === 0)
172
+ add('tenants', 'must declare at least one tenant');
173
+ else
174
+ tenantsRaw.forEach((t, i) => {
175
+ const parsed = walkCompany(t, `tenants[${i}]`, undefined, tenantNodes);
176
+ if (parsed)
177
+ tenants.push(parsed);
178
+ });
179
+ const resolvePath = (start, raw) => {
180
+ let level = start;
181
+ let node;
182
+ for (const seg of raw.split('/')) {
183
+ node = level.get(seg.trim());
184
+ if (!node)
185
+ return undefined;
186
+ level = node.children;
187
+ }
188
+ return node;
189
+ };
190
+ // ---- pass 2: roots, then users in depth-first order (first occurrence wins on duplicates) ----
191
+ const usernames = new Set();
192
+ const emails = new Set();
193
+ const claim = (username, email, path) => {
194
+ if (username !== undefined) {
195
+ const k = (0, org_names_js_1.nameKey)(username);
196
+ if (usernames.has(k))
197
+ add(`${path}.username`, 'duplicate username (case-insensitive)');
198
+ usernames.add(k);
199
+ }
200
+ if (email !== undefined) {
201
+ const k = (0, org_names_js_1.nameKey)(email);
202
+ if (emails.has(k))
203
+ add(`${path}.email`, 'duplicate email (case-insensitive)');
204
+ emails.add(k);
205
+ }
206
+ };
207
+ const emailOf = (v, path) => {
208
+ if (typeof v !== 'string' || !(0, guard_1.isEmailShaped)(v.trim())) {
209
+ add(path, 'must be a valid email address');
210
+ return undefined;
211
+ }
212
+ return (0, guard_1.canonicalEmail)(v);
213
+ };
214
+ const roots = [];
215
+ (optionalArray(f.roots, 'roots') ?? []).forEach((rr, i) => {
216
+ const p = `roots[${i}]`;
217
+ const r = closed(rr, p, exports.ORG_FILE_KEYS.root);
218
+ if (!r)
219
+ return;
220
+ const username = text(r.username, `${p}.username`);
221
+ const email = emailOf(r.email, `${p}.email`);
222
+ const home = text(r.homeCompany, `${p}.homeCompany`);
223
+ if (home !== undefined && !resolvePath(tenantNodes, home)) {
224
+ add(`${p}.homeCompany`, 'does not name a company declared in this file');
225
+ }
226
+ claim(username, email, p);
227
+ if (username !== undefined && email !== undefined && home !== undefined) {
228
+ roots.push({ username, email, homeCompany: home });
229
+ }
230
+ });
231
+ const inScope = (start, pick, value) => {
232
+ for (let n = start; n; n = n.parent)
233
+ if (pick(n).has(value))
234
+ return true;
235
+ return false;
236
+ };
237
+ const usersByNode = new Map();
238
+ for (const node of nodes) {
239
+ const users = [];
240
+ usersByNode.set(node, users);
241
+ (optionalArray(node.rawUsers, node.usersPath) ?? []).forEach((uu, i) => {
242
+ const p = `${node.usersPath}[${i}]`;
243
+ const u = closed(uu, p, exports.ORG_FILE_KEYS.user);
244
+ if (!u)
245
+ return;
246
+ const username = text(u.username, `${p}.username`);
247
+ const email = emailOf(u.email, `${p}.email`);
248
+ claim(username, email, p);
249
+ let home = node;
250
+ let companyRaw;
251
+ if (u.company !== undefined) {
252
+ companyRaw = text(u.company, `${p}.company`);
253
+ if (companyRaw !== undefined) {
254
+ let top = node;
255
+ while (top.parent)
256
+ top = top.parent;
257
+ const found = resolvePath(new Map([[top.name, top]]), `${top.name}/${companyRaw}`);
258
+ if (found)
259
+ home = found;
260
+ else
261
+ add(`${p}.company`, 'does not exist under this tenant');
262
+ }
263
+ }
264
+ const postings = [];
265
+ const tuples = new Set();
266
+ (optionalArray(u.postings, `${p}.postings`) ?? []).forEach((pp, j) => {
267
+ const q = `${p}.postings[${j}]`;
268
+ const po = closed(pp, q, exports.ORG_FILE_KEYS.posting);
269
+ if (!po)
270
+ return;
271
+ const role = text(po.role, `${q}.role`);
272
+ if (role !== undefined && !inScope(home, (n) => n.roles, role)) {
273
+ add(`${q}.role`, 'not declared on this company or an ancestor');
274
+ }
275
+ const posting = { role: role ?? '' };
276
+ for (const [key, pick] of [
277
+ ['department', (n) => n.departments],
278
+ ['branch', (n) => n.branches],
279
+ ]) {
280
+ if (po[key] === undefined)
281
+ continue;
282
+ const v = text(po[key], `${q}.${key}`);
283
+ if (v === undefined)
284
+ continue;
285
+ if (!inScope(home, pick, v))
286
+ add(`${q}.${key}`, 'not declared on this company or an ancestor');
287
+ posting[key] = v;
288
+ }
289
+ const tuple = JSON.stringify([role, posting.department ?? null, posting.branch ?? null]);
290
+ if (tuples.has(tuple))
291
+ add(q, 'duplicate posting');
292
+ tuples.add(tuple);
293
+ if (role !== undefined)
294
+ postings.push(posting);
295
+ });
296
+ if (username !== undefined && email !== undefined) {
297
+ const user = { username, email };
298
+ if (companyRaw !== undefined)
299
+ user.company = companyRaw;
300
+ if (u.postings !== undefined)
301
+ user.postings = postings;
302
+ users.push(user);
303
+ }
304
+ });
305
+ }
306
+ if (issues.length > 0)
307
+ throw new OrgBootstrapFileError(issues);
308
+ // Attach parsed users to the parsed companies (same depth-first order as `nodes`).
309
+ const attach = (list, map) => {
310
+ list.forEach((c, i) => {
311
+ const n = map[i];
312
+ if (!n)
313
+ return;
314
+ const users = usersByNode.get(n) ?? [];
315
+ if (users.length > 0)
316
+ c.users = users;
317
+ if (c.children)
318
+ attach(c.children, [...n.children.values()]);
319
+ });
320
+ };
321
+ attach(tenants, [...tenantNodes.values()]);
322
+ return { version: 1, roots, tenants };
323
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * G-009 T9: the pure org bootstrap planner (SPEC R4, R8, R10, R13).
3
+ *
4
+ * `(file, state) -> { entries, conflicts }`. No I/O, no mutation of its inputs. Additive only: an
5
+ * entry that is in the state but not in the file is reported (`not-in-file`), a differing field is
6
+ * reported (`drift`), nothing is ever planned as a delete or an overwrite. Only the file's tenants'
7
+ * subtrees are looked at, so onboarding tenant B never reports tenant A.
8
+ */
9
+ import type { OrgBootstrapFile } from './org-bootstrap-file.js';
10
+ export type CredentialState = 'none' | 'locked' | 'usable';
11
+ export declare const PLAN_KIND_ORDER: readonly ["company", "department", "branch", "company_department", "company_branch", "role", "role_claim", "grant", "user", "membership", "posting", "credential", "root_flag"];
12
+ export type PlanKind = (typeof PLAN_KIND_ORDER)[number];
13
+ export type PlanStatus = 'create' | 'unchanged' | 'drift' | 'not-in-file' | 'conflict';
14
+ export interface PlanEntry {
15
+ kind: PlanKind;
16
+ path: string;
17
+ status: PlanStatus;
18
+ detail?: string;
19
+ }
20
+ export interface OrgPlan {
21
+ entries: PlanEntry[];
22
+ /** The entries with status `conflict`; non-empty means the run refuses before any write. */
23
+ conflicts: PlanEntry[];
24
+ }
25
+ export interface OrgStatePosting {
26
+ role: string;
27
+ department?: string;
28
+ branch?: string;
29
+ }
30
+ export interface OrgStateMembership {
31
+ /** `Tenant/Child/...`, names as stored. */
32
+ companyPath: string;
33
+ active: boolean;
34
+ postings: OrgStatePosting[];
35
+ }
36
+ export interface OrgStateUser {
37
+ username: string;
38
+ email: string;
39
+ credential: CredentialState;
40
+ isPlatformSuperuser: boolean;
41
+ /** Memberships INSIDE the file's tenants' subtrees only. */
42
+ memberships: OrgStateMembership[];
43
+ /** Holds a membership OUTSIDE the file's tenants' subtrees (another tenant's user). */
44
+ membershipsOutside: boolean;
45
+ }
46
+ export interface OrgStateRole {
47
+ name: string;
48
+ /** Name of the owner's top-level tenant; null = no owner. */
49
+ ownerTenant: string | null;
50
+ /** Held by a posting outside the file's tenants' subtrees. */
51
+ heldOutside: boolean;
52
+ permissions: string[];
53
+ }
54
+ export interface OrgStateCompany {
55
+ path: string;
56
+ departments: string[];
57
+ branches: string[];
58
+ }
59
+ export interface OrgState {
60
+ companies: OrgStateCompany[];
61
+ departments: string[];
62
+ branches: string[];
63
+ roles: OrgStateRole[];
64
+ users: OrgStateUser[];
65
+ emailHolders: {
66
+ email: string;
67
+ username: string;
68
+ }[];
69
+ /**
70
+ * File company paths whose name matches more than one stored company (several top-level companies
71
+ * of the tenant's name, or several children of one parent with the child's name): never guessed.
72
+ */
73
+ ambiguousCompanies: string[];
74
+ }
75
+ export interface PlanOptions {
76
+ claimUnownedRoles: boolean;
77
+ }
78
+ export declare function planOrgBootstrap(file: OrgBootstrapFile, state: OrgState, options: PlanOptions): OrgPlan;
79
+ //# sourceMappingURL=org-bootstrap-plan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"org-bootstrap-plan.d.ts","sourceRoot":"","sources":["../../../src/features/org-bootstrap/org-bootstrap-plan.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAEV,gBAAgB,EAEjB,MAAM,yBAAyB,CAAC;AAGjC,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE3D,eAAO,MAAM,eAAe,iLAclB,CAAC;AAEX,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AACxD,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,WAAW,GAAG,OAAO,GAAG,aAAa,GAAG,UAAU,CAAC;AAEvF,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,UAAU,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,SAAS,EAAE,CAAC;IACrB,4FAA4F;IAC5F,SAAS,EAAE,SAAS,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,2CAA2C;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,eAAe,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,eAAe,CAAC;IAC5B,mBAAmB,EAAE,OAAO,CAAC;IAC7B,4DAA4D;IAC5D,WAAW,EAAE,kBAAkB,EAAE,CAAC;IAClC,uFAAuF;IACvF,kBAAkB,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,8DAA8D;IAC9D,WAAW,EAAE,OAAO,CAAC;IACrB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,eAAe,EAAE,CAAC;IAC7B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,YAAY,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACpD;;;OAGG;IACH,kBAAkB,EAAE,MAAM,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,WAAW;IAC1B,iBAAiB,EAAE,OAAO,CAAC;CAC5B;AAQD,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,gBAAgB,EACtB,KAAK,EAAE,QAAQ,EACf,OAAO,EAAE,WAAW,GACnB,OAAO,CA+WT"}