@jini-ai/user-management 0.3.3 → 0.3.4
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/CHANGELOG.md +4 -0
- package/dist/admin/adapters/memory.d.ts +3 -5
- package/dist/admin/adapters/memory.js +27 -28
- package/dist/admin/conformance/index.js +1 -0
- package/dist/admin/controllers/auth.controller.js +8 -6
- package/dist/admin/controllers/members.controller.js +9 -6
- package/dist/admin/controllers/roles.controller.js +99 -105
- package/dist/admin/controllers/users.controller.js +56 -36
- package/dist/admin/error-copy.d.ts +11 -0
- package/dist/admin/error-copy.js +10 -0
- package/dist/admin/people.rules.js +37 -31
- package/dist/admin/react/components/MemberRow.d.ts +1 -1
- package/dist/admin/react/components/MemberRow.js +12 -2
- package/dist/admin/react/hooks/AuthBinding.hooks.d.ts +6 -21
- package/dist/admin/react/hooks/AuthBinding.hooks.js +3 -8
- package/dist/admin/react/hooks/AuthScope.hooks.d.ts +22 -0
- package/dist/admin/react/hooks/AuthScope.hooks.js +10 -0
- package/dist/admin/react/hooks/LoginPage.hooks.d.ts +1 -1
- package/dist/admin/react/hooks/LoginPage.hooks.js +1 -1
- package/dist/admin/react/hooks/MembersBinding.hooks.d.ts +6 -17
- package/dist/admin/react/hooks/MembersBinding.hooks.js +3 -8
- package/dist/admin/react/hooks/MembersPage.hooks.d.ts +1 -1
- package/dist/admin/react/hooks/MembersPage.hooks.js +1 -1
- package/dist/admin/react/hooks/MembersScope.hooks.d.ts +17 -0
- package/dist/admin/react/hooks/MembersScope.hooks.js +10 -0
- package/dist/admin/react/hooks/RolesBinding.hooks.d.ts +7 -21
- package/dist/admin/react/hooks/RolesBinding.hooks.js +3 -8
- package/dist/admin/react/hooks/RolesController.hooks.js +1 -1
- package/dist/admin/react/hooks/RolesPage.hooks.d.ts +2 -2
- package/dist/admin/react/hooks/RolesPage.hooks.js +22 -8
- package/dist/admin/react/hooks/RolesScope.hooks.d.ts +21 -0
- package/dist/admin/react/hooks/RolesScope.hooks.js +10 -0
- package/dist/admin/react/hooks/UsersBinding.hooks.d.ts +6 -18
- package/dist/admin/react/hooks/UsersBinding.hooks.js +3 -8
- package/dist/admin/react/hooks/UsersPage.hooks.d.ts +1 -1
- package/dist/admin/react/hooks/UsersPage.hooks.js +1 -1
- package/dist/admin/react/hooks/UsersScope.hooks.d.ts +19 -0
- package/dist/admin/react/hooks/UsersScope.hooks.js +10 -0
- package/dist/admin/react/hooks/UsersTab.hooks.d.ts +6 -0
- package/dist/admin/react/hooks/UsersTab.hooks.js +34 -22
- package/dist/admin/react/index.d.ts +4 -4
- package/dist/admin/react/pages/LoginPage.d.ts +1 -1
- package/dist/admin/react/pages/MembersPage.d.ts +1 -1
- package/dist/admin/react/pages/RolesPage.d.ts +2 -2
- package/dist/admin/react/pages/RolesPage.js +6 -2
- package/dist/admin/react/pages/UsersPage.d.ts +2 -2
- package/dist/admin/react/pages/UsersPage.js +6 -2
- package/dist/admin/rules.js +14 -12
- package/dist/react/errors.js +16 -9
- package/dist/react/index.d.ts +1 -1
- package/dist/react/members/hooks/use-members.hooks.js +21 -12
- package/dist/react/testing.js +4 -6
- package/dist/react/users/UserRowParts.d.ts +53 -0
- package/dist/react/users/UserRowParts.js +39 -0
- package/dist/react/users/Users.d.ts +31 -2
- package/dist/react/users/Users.js +41 -36
- package/dist/react/users/hooks/use-users.hooks.js +57 -30
- package/dist/server/admin-crud-service.js +25 -13
- package/dist/server/agent-tool-input.js +62 -36
- package/dist/server/agent-tool-registrations.d.ts +46 -0
- package/dist/server/agent-tool-registrations.js +28 -20
- package/dist/server/auth-service.js +35 -20
- package/dist/server/authorize.js +7 -9
- package/dist/server/permission-migrations.js +35 -25
- package/dist/server/repo.memory-transactions.js +5 -6
- package/dist/server/repo.memory.js +35 -81
- package/dist/server/seed.js +100 -84
- package/package.json +8 -8
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { matchesRecordFields, upsertRecord } from "@jini-ai/core/records";
|
|
1
2
|
/**
|
|
2
3
|
* @file In-memory adapters for the `user-management` repo ports.
|
|
3
4
|
*
|
|
@@ -33,18 +34,13 @@ export class InMemoryPrincipalRepo extends InMemoryIdentityRows {
|
|
|
33
34
|
super(_required, { initialRows });
|
|
34
35
|
}
|
|
35
36
|
async findById(required) {
|
|
36
|
-
return
|
|
37
|
+
return this.rows.find(record => matchesRecordFields({ record, fields: { workspaceId: required.workspaceId, id: required.id } })) ?? null;
|
|
37
38
|
}
|
|
38
39
|
async list(required) {
|
|
39
|
-
return this.rows.filter(
|
|
40
|
+
return this.rows.filter(record => matchesRecordFields({ record, fields: { workspaceId: required.workspaceId } }));
|
|
40
41
|
}
|
|
41
42
|
async save(record) {
|
|
42
|
-
|
|
43
|
-
if (index === -1) {
|
|
44
|
-
this.rows.push(record);
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
this.rows[index] = record;
|
|
43
|
+
upsertRecord({ records: this.rows, record, identity: { workspaceId: record.workspaceId, id: record.id } });
|
|
48
44
|
}
|
|
49
45
|
}
|
|
50
46
|
export class InMemoryUserRepo extends InMemoryIdentityRows {
|
|
@@ -52,21 +48,16 @@ export class InMemoryUserRepo extends InMemoryIdentityRows {
|
|
|
52
48
|
super(_required, { initialRows });
|
|
53
49
|
}
|
|
54
50
|
async findByPrincipalId(required) {
|
|
55
|
-
return
|
|
51
|
+
return this.rows.find(record => matchesRecordFields({ record, fields: required })) ?? null;
|
|
56
52
|
}
|
|
57
53
|
async findByUsername(required) {
|
|
58
|
-
return
|
|
54
|
+
return this.rows.find(record => matchesRecordFields({ record, fields: required })) ?? null;
|
|
59
55
|
}
|
|
60
56
|
async list(required) {
|
|
61
|
-
return this.rows.filter(
|
|
57
|
+
return this.rows.filter(record => matchesRecordFields({ record, fields: { workspaceId: required.workspaceId } }));
|
|
62
58
|
}
|
|
63
59
|
async save(record) {
|
|
64
|
-
|
|
65
|
-
if (index === -1) {
|
|
66
|
-
this.rows.push(record);
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
this.rows[index] = record;
|
|
60
|
+
upsertRecord({ records: this.rows, record, identity: { workspaceId: record.workspaceId, principalId: record.principalId } });
|
|
70
61
|
}
|
|
71
62
|
}
|
|
72
63
|
export class InMemorySessionRepo extends InMemoryIdentityRows {
|
|
@@ -74,18 +65,13 @@ export class InMemorySessionRepo extends InMemoryIdentityRows {
|
|
|
74
65
|
super(_required, { initialRows });
|
|
75
66
|
}
|
|
76
67
|
async findById(required) {
|
|
77
|
-
return
|
|
68
|
+
return this.rows.find(record => matchesRecordFields({ record, fields: { workspaceId: required.workspaceId, id: required.id } })) ?? null;
|
|
78
69
|
}
|
|
79
70
|
async findByTokenHash(required) {
|
|
80
|
-
return
|
|
71
|
+
return this.rows.find(record => matchesRecordFields({ record, fields: required })) ?? null;
|
|
81
72
|
}
|
|
82
73
|
async save(record) {
|
|
83
|
-
|
|
84
|
-
if (index === -1) {
|
|
85
|
-
this.rows.push(record);
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
this.rows[index] = record;
|
|
74
|
+
upsertRecord({ records: this.rows, record, identity: { workspaceId: record.workspaceId, id: record.id } });
|
|
89
75
|
}
|
|
90
76
|
async revoke(required) {
|
|
91
77
|
const row = this.rows.find((candidate) => candidate.workspaceId === required.workspaceId && candidate.id === required.id);
|
|
@@ -93,7 +79,7 @@ export class InMemorySessionRepo extends InMemoryIdentityRows {
|
|
|
93
79
|
row.revokedAt = required.revokedAt;
|
|
94
80
|
}
|
|
95
81
|
async listByPrincipalId(required) {
|
|
96
|
-
return this.rows.filter(
|
|
82
|
+
return this.rows.filter(record => matchesRecordFields({ record, fields: required }));
|
|
97
83
|
}
|
|
98
84
|
}
|
|
99
85
|
export class InMemoryRoleRepo extends InMemoryIdentityRows {
|
|
@@ -101,25 +87,19 @@ export class InMemoryRoleRepo extends InMemoryIdentityRows {
|
|
|
101
87
|
super(_required, { initialRows });
|
|
102
88
|
}
|
|
103
89
|
async findById(required) {
|
|
104
|
-
return
|
|
90
|
+
return this.rows.find(record => matchesRecordFields({ record, fields: { workspaceId: required.workspaceId, id: required.id } })) ?? null;
|
|
105
91
|
}
|
|
106
92
|
async findByName(required) {
|
|
107
|
-
return
|
|
108
|
-
null);
|
|
93
|
+
return this.rows.find(record => matchesRecordFields({ record, fields: { workspaceId: required.workspaceId, name: required.name } })) ?? null;
|
|
109
94
|
}
|
|
110
95
|
async list(required) {
|
|
111
|
-
return this.rows.filter(
|
|
96
|
+
return this.rows.filter(record => matchesRecordFields({ record, fields: { workspaceId: required.workspaceId } }));
|
|
112
97
|
}
|
|
113
98
|
async save(record) {
|
|
114
|
-
|
|
115
|
-
if (index === -1) {
|
|
116
|
-
this.rows.push(record);
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
this.rows[index] = record;
|
|
99
|
+
upsertRecord({ records: this.rows, record, identity: { workspaceId: record.workspaceId, id: record.id } });
|
|
120
100
|
}
|
|
121
101
|
async delete(required) {
|
|
122
|
-
this.rows = this.rows.filter(
|
|
102
|
+
this.rows = this.rows.filter(record => !matchesRecordFields({ record, fields: { workspaceId: required.workspaceId, id: required.id } }));
|
|
123
103
|
}
|
|
124
104
|
}
|
|
125
105
|
export class InMemoryPolicyRepo extends InMemoryIdentityRows {
|
|
@@ -127,25 +107,19 @@ export class InMemoryPolicyRepo extends InMemoryIdentityRows {
|
|
|
127
107
|
super(_required, { initialRows });
|
|
128
108
|
}
|
|
129
109
|
async findById(required) {
|
|
130
|
-
return
|
|
110
|
+
return this.rows.find(record => matchesRecordFields({ record, fields: { workspaceId: required.workspaceId, id: required.id } })) ?? null;
|
|
131
111
|
}
|
|
132
112
|
async findByName(required) {
|
|
133
|
-
return
|
|
134
|
-
null);
|
|
113
|
+
return this.rows.find(record => matchesRecordFields({ record, fields: { workspaceId: required.workspaceId, name: required.name } })) ?? null;
|
|
135
114
|
}
|
|
136
115
|
async list(required) {
|
|
137
|
-
return this.rows.filter(
|
|
116
|
+
return this.rows.filter(record => matchesRecordFields({ record, fields: { workspaceId: required.workspaceId } }));
|
|
138
117
|
}
|
|
139
118
|
async save(record) {
|
|
140
|
-
|
|
141
|
-
if (index === -1) {
|
|
142
|
-
this.rows.push(record);
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
this.rows[index] = record;
|
|
119
|
+
upsertRecord({ records: this.rows, record, identity: { workspaceId: record.workspaceId, id: record.id } });
|
|
146
120
|
}
|
|
147
121
|
async delete(required) {
|
|
148
|
-
this.rows = this.rows.filter(
|
|
122
|
+
this.rows = this.rows.filter(record => !matchesRecordFields({ record, fields: { workspaceId: required.workspaceId, id: required.id } }));
|
|
149
123
|
}
|
|
150
124
|
}
|
|
151
125
|
export class InMemoryPolicyPermissionRepo extends InMemoryIdentityRows {
|
|
@@ -153,21 +127,16 @@ export class InMemoryPolicyPermissionRepo extends InMemoryIdentityRows {
|
|
|
153
127
|
super(_required, { initialRows });
|
|
154
128
|
}
|
|
155
129
|
async listByPolicyId(required) {
|
|
156
|
-
return this.rows.filter(
|
|
130
|
+
return this.rows.filter(record => matchesRecordFields({ record, fields: required }));
|
|
157
131
|
}
|
|
158
132
|
async save(record) {
|
|
159
|
-
|
|
160
|
-
if (index === -1) {
|
|
161
|
-
this.rows.push(record);
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
this.rows[index] = record;
|
|
133
|
+
upsertRecord({ records: this.rows, record, identity: { workspaceId: record.workspaceId, id: record.id } });
|
|
165
134
|
}
|
|
166
135
|
async deleteByPolicyId(required) {
|
|
167
|
-
this.rows = this.rows.filter(
|
|
136
|
+
this.rows = this.rows.filter(record => !matchesRecordFields({ record, fields: { workspaceId: required.workspaceId, policyId: required.policyId } }));
|
|
168
137
|
}
|
|
169
138
|
async delete(required) {
|
|
170
|
-
this.rows = this.rows.filter(
|
|
139
|
+
this.rows = this.rows.filter(record => !matchesRecordFields({ record, fields: { workspaceId: required.workspaceId, id: required.id } }));
|
|
171
140
|
}
|
|
172
141
|
}
|
|
173
142
|
export class InMemoryRolePolicyRepo extends InMemoryIdentityRows {
|
|
@@ -175,18 +144,13 @@ export class InMemoryRolePolicyRepo extends InMemoryIdentityRows {
|
|
|
175
144
|
super(_required, { initialRows });
|
|
176
145
|
}
|
|
177
146
|
async listByRoleId(required) {
|
|
178
|
-
return this.rows.filter(
|
|
147
|
+
return this.rows.filter(record => matchesRecordFields({ record, fields: { workspaceId: required.workspaceId, roleId: required.roleId } }));
|
|
179
148
|
}
|
|
180
149
|
async listByPolicyId(required) {
|
|
181
|
-
return this.rows.filter(
|
|
150
|
+
return this.rows.filter(record => matchesRecordFields({ record, fields: { workspaceId: required.workspaceId, policyId: required.policyId } }));
|
|
182
151
|
}
|
|
183
152
|
async save(record) {
|
|
184
|
-
|
|
185
|
-
if (index === -1) {
|
|
186
|
-
this.rows.push(record);
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
this.rows[index] = record;
|
|
153
|
+
upsertRecord({ records: this.rows, record, identity: { workspaceId: record.workspaceId, id: record.id } });
|
|
190
154
|
}
|
|
191
155
|
}
|
|
192
156
|
export class InMemoryPrincipalRoleRepo extends InMemoryIdentityRows {
|
|
@@ -194,18 +158,13 @@ export class InMemoryPrincipalRoleRepo extends InMemoryIdentityRows {
|
|
|
194
158
|
super(_required, { initialRows });
|
|
195
159
|
}
|
|
196
160
|
async listByPrincipalId(required) {
|
|
197
|
-
return this.rows.filter(
|
|
161
|
+
return this.rows.filter(record => matchesRecordFields({ record, fields: required }));
|
|
198
162
|
}
|
|
199
163
|
async listByRoleId(required) {
|
|
200
|
-
return this.rows.filter(
|
|
164
|
+
return this.rows.filter(record => matchesRecordFields({ record, fields: { workspaceId: required.workspaceId, roleId: required.roleId } }));
|
|
201
165
|
}
|
|
202
166
|
async save(record) {
|
|
203
|
-
|
|
204
|
-
if (index === -1) {
|
|
205
|
-
this.rows.push(record);
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
this.rows[index] = record;
|
|
167
|
+
upsertRecord({ records: this.rows, record, identity: { workspaceId: record.workspaceId, id: record.id } });
|
|
209
168
|
}
|
|
210
169
|
}
|
|
211
170
|
export class InMemoryPrincipalPolicyRepo extends InMemoryIdentityRows {
|
|
@@ -213,18 +172,13 @@ export class InMemoryPrincipalPolicyRepo extends InMemoryIdentityRows {
|
|
|
213
172
|
super(_required, { initialRows });
|
|
214
173
|
}
|
|
215
174
|
async listByPrincipalId(required) {
|
|
216
|
-
return this.rows.filter(
|
|
175
|
+
return this.rows.filter(record => matchesRecordFields({ record, fields: required }));
|
|
217
176
|
}
|
|
218
177
|
async listByPolicyId(required) {
|
|
219
|
-
return this.rows.filter(
|
|
178
|
+
return this.rows.filter(record => matchesRecordFields({ record, fields: required }));
|
|
220
179
|
}
|
|
221
180
|
async save(record) {
|
|
222
|
-
|
|
223
|
-
if (index === -1) {
|
|
224
|
-
this.rows.push(record);
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
this.rows[index] = record;
|
|
181
|
+
upsertRecord({ records: this.rows, record, identity: { workspaceId: record.workspaceId, id: record.id } });
|
|
228
182
|
}
|
|
229
183
|
}
|
|
230
184
|
//# sourceMappingURL=repo.memory.js.map
|
package/dist/server/seed.js
CHANGED
|
@@ -240,7 +240,7 @@ async function ensurePolicyPermissions(required) {
|
|
|
240
240
|
}
|
|
241
241
|
/** Builds one built-in role + its 1:1 policy + policy_permissions rows, converging on what exists. */
|
|
242
242
|
async function seedBuiltinRoleWithPolicy(required) {
|
|
243
|
-
const { deps, workspaceId, name, permissions
|
|
243
|
+
const { deps, workspaceId, name, permissions } = required;
|
|
244
244
|
const roleId = await ensureBuiltinRole({ deps, workspaceId, name });
|
|
245
245
|
const policyId = await ensureBuiltinPolicy({ deps, workspaceId, name });
|
|
246
246
|
// `role_policies` has no unique index, so a replay would otherwise stack a second identical link.
|
|
@@ -249,7 +249,6 @@ async function seedBuiltinRoleWithPolicy(required) {
|
|
|
249
249
|
await deps.repos.rolePolicies.save({ id: deps.idGen.newId(), workspaceId, roleId, policyId });
|
|
250
250
|
}
|
|
251
251
|
await ensurePolicyPermissions({ deps, workspaceId, policyId, permissions });
|
|
252
|
-
void nowIso; // reserved for a future createdAt column on roles/policies
|
|
253
252
|
return { roleId, policyId };
|
|
254
253
|
}
|
|
255
254
|
/**
|
|
@@ -279,7 +278,7 @@ async function seedBuiltinRoleWithPolicy(required) {
|
|
|
279
278
|
* returns early), plus the fixed converge pass for the owner role only when the repair fires.
|
|
280
279
|
*/
|
|
281
280
|
async function ensureOwnerRoleBinding(required) {
|
|
282
|
-
const { deps, workspaceId, ownerPrincipalId
|
|
281
|
+
const { deps, workspaceId, ownerPrincipalId } = required;
|
|
283
282
|
const heldRoles = await deps.repos.principalRoles.listByPrincipalId({
|
|
284
283
|
workspaceId,
|
|
285
284
|
principalId: ownerPrincipalId,
|
|
@@ -291,7 +290,6 @@ async function ensureOwnerRoleBinding(required) {
|
|
|
291
290
|
workspaceId,
|
|
292
291
|
name: OWNER_ROLE_NAME,
|
|
293
292
|
permissions: OWNER_ROLE_PERMISSIONS,
|
|
294
|
-
nowIso,
|
|
295
293
|
});
|
|
296
294
|
await deps.repos.principalRoles.save({
|
|
297
295
|
id: deps.idGen.newId(),
|
|
@@ -300,64 +298,41 @@ async function ensureOwnerRoleBinding(required) {
|
|
|
300
298
|
roleId,
|
|
301
299
|
});
|
|
302
300
|
}
|
|
301
|
+
/** The seeded built-in roles below the owner, in seed order, each with its permission list. */
|
|
302
|
+
const BUILTIN_NON_OWNER_ROLES = [
|
|
303
|
+
{ name: "admin", permissions: BUILTIN_ADMIN_PERMISSIONS },
|
|
304
|
+
{ name: "editor", permissions: BUILTIN_EDITOR_PERMISSIONS },
|
|
305
|
+
{ name: "viewer", permissions: BUILTIN_VIEWER_PERMISSIONS },
|
|
306
|
+
];
|
|
307
|
+
/** The workspace's non-legacy `system` principal, if one exists (first in roster order). */
|
|
308
|
+
async function findSystemPrincipal(required) {
|
|
309
|
+
const { deps, workspaceId } = required;
|
|
310
|
+
return (await deps.repos.principals.list({ workspaceId })).find((row) => row.kind === "system" && row.id !== LEGACY_USER_LOCAL_PRINCIPAL_ID);
|
|
311
|
+
}
|
|
303
312
|
/**
|
|
304
|
-
*
|
|
313
|
+
* The owner user a previous seed already wrote, or `null` on a first (or interrupted-before-owner) seed.
|
|
305
314
|
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
* confirming its role binding (`ensureOwnerRoleBinding`) and writes nothing. (This path does not
|
|
309
|
-
* reconcile a built-in policy whose permission list has since grown; that is the host's job — see
|
|
310
|
-
* the host's `applyBuiltinRoleGrants` / `migrateDeprecatedPermissionGrants`, chained onto
|
|
311
|
-
* `identityReady`.)
|
|
312
|
-
* - "Ran to completion" is judged on the owner user AND its role link, not the user alone. The link
|
|
313
|
-
* is the seed's last write, so the user is not on its own evidence the seed finished — see
|
|
314
|
-
* `ensureOwnerRoleBinding` for what an interrupt between those two writes costs.
|
|
315
|
-
* - A seed that was INTERRUPTED is resumed, not replayed: every step converges onto the rows a
|
|
316
|
-
* previous attempt already wrote and fills in only what is missing. See the block comment above
|
|
317
|
-
* `ensureBuiltinRole` for why this function must survive being killed halfway.
|
|
318
|
-
*
|
|
319
|
-
* @complexity O(1) owner lookup plus the existing principal roster read; fresh seeds make a
|
|
320
|
-
* fixed small number of inserts (4 roles/policies, ~2 dozen policy_permissions,
|
|
321
|
-
* 3 principals, 1 user, 1 role assignment).
|
|
322
|
-
* @overallScore 100
|
|
315
|
+
* An explicit binding is authoritative, even when somebody has reused its former login name.
|
|
316
|
+
* A missing bound row is an error, never permission to mint a replacement owner.
|
|
323
317
|
*/
|
|
324
|
-
|
|
325
|
-
const { deps } = required;
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const ownerUsername = normalizeUsername({ raw: input.ownerUsername });
|
|
329
|
-
const ownerPassword = input.ownerPassword;
|
|
330
|
-
const nowIso = kernelNowIso({ clock: deps.clock });
|
|
331
|
-
// An explicit binding is authoritative, even when somebody has reused its former login name.
|
|
332
|
-
// A missing bound row is an error, never permission to mint a replacement owner.
|
|
333
|
-
const seededOwnerUser = input.seededOwnerPrincipalId !== undefined
|
|
334
|
-
? await deps.repos.users.findByPrincipalId({ workspaceId, principalId: input.seededOwnerPrincipalId }) : null;
|
|
335
|
-
if (input.seededOwnerPrincipalId !== undefined && !seededOwnerUser) {
|
|
336
|
-
throw new IdentityNotFoundError({ message: "the bound seeded owner user was not found; restore the owner identity before booting" });
|
|
318
|
+
async function findExistingOwnerUser(required) {
|
|
319
|
+
const { deps, workspaceId, ownerUsername, seededOwnerPrincipalId } = required;
|
|
320
|
+
if (seededOwnerPrincipalId === undefined) {
|
|
321
|
+
return deps.repos.users.findByUsername({ workspaceId, username: ownerUsername });
|
|
337
322
|
}
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
});
|
|
342
|
-
if (existingOwnerUser) {
|
|
343
|
-
// The owner user alone is NOT proof the seed finished — its role link is written after it. See
|
|
344
|
-
// `ensureOwnerRoleBinding`: on a healthy workspace this is one read and no write.
|
|
345
|
-
await ensureOwnerRoleBinding({
|
|
346
|
-
deps,
|
|
347
|
-
workspaceId,
|
|
348
|
-
ownerPrincipalId: existingOwnerUser.principalId,
|
|
349
|
-
nowIso,
|
|
350
|
-
});
|
|
351
|
-
const existingSystem = (await deps.repos.principals.list({ workspaceId })).find((row) => row.kind === "system" && row.id !== LEGACY_USER_LOCAL_PRINCIPAL_ID);
|
|
352
|
-
return {
|
|
353
|
-
ownerPrincipalId: existingOwnerUser.principalId,
|
|
354
|
-
systemPrincipalId: existingSystem?.id ?? LEGACY_USER_LOCAL_PRINCIPAL_ID,
|
|
355
|
-
};
|
|
323
|
+
const seededOwnerUser = await deps.repos.users.findByPrincipalId({ workspaceId, principalId: seededOwnerPrincipalId });
|
|
324
|
+
if (!seededOwnerUser) {
|
|
325
|
+
throw new IdentityNotFoundError({ message: "the bound seeded owner user was not found; restore the owner identity before booting" });
|
|
356
326
|
}
|
|
327
|
+
return seededOwnerUser;
|
|
328
|
+
}
|
|
329
|
+
/** Write the `system` principal (reusing an interrupted seed's) and the disabled legacy actor; returns the system id. */
|
|
330
|
+
async function ensureSystemPrincipals(required) {
|
|
331
|
+
const { deps, workspaceId, nowIso } = required;
|
|
357
332
|
// Reuse the system principal a prior interrupted seed may already have written: `principals` has
|
|
358
333
|
// no unique index on kind, so a blind re-insert would leave TWO non-legacy system principals and
|
|
359
|
-
//
|
|
360
|
-
const existingSystem =
|
|
334
|
+
// `seedIdentity`'s early-return branch picking between them by list order.
|
|
335
|
+
const existingSystem = await findSystemPrincipal({ deps, workspaceId });
|
|
361
336
|
const systemPrincipalId = existingSystem?.id ?? deps.idGen.newId();
|
|
362
337
|
if (!existingSystem) {
|
|
363
338
|
await deps.repos.principals.save({
|
|
@@ -380,34 +355,11 @@ export async function seedIdentity(required, optional = {}) {
|
|
|
380
355
|
disabledAt: nowIso,
|
|
381
356
|
createdAt: nowIso,
|
|
382
357
|
});
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
nowIso,
|
|
389
|
-
});
|
|
390
|
-
await seedBuiltinRoleWithPolicy({
|
|
391
|
-
deps,
|
|
392
|
-
workspaceId,
|
|
393
|
-
name: "admin",
|
|
394
|
-
permissions: BUILTIN_ADMIN_PERMISSIONS,
|
|
395
|
-
nowIso,
|
|
396
|
-
});
|
|
397
|
-
await seedBuiltinRoleWithPolicy({
|
|
398
|
-
deps,
|
|
399
|
-
workspaceId,
|
|
400
|
-
name: "editor",
|
|
401
|
-
permissions: BUILTIN_EDITOR_PERMISSIONS,
|
|
402
|
-
nowIso,
|
|
403
|
-
});
|
|
404
|
-
await seedBuiltinRoleWithPolicy({
|
|
405
|
-
deps,
|
|
406
|
-
workspaceId,
|
|
407
|
-
name: "viewer",
|
|
408
|
-
permissions: BUILTIN_VIEWER_PERMISSIONS,
|
|
409
|
-
nowIso,
|
|
410
|
-
});
|
|
358
|
+
return systemPrincipalId;
|
|
359
|
+
}
|
|
360
|
+
/** Write the owner principal, its user row, and its owner-role link — the seed's final writes, in that order. */
|
|
361
|
+
async function createOwner(required) {
|
|
362
|
+
const { deps, workspaceId, ownerUsername, ownerPassword, ownerEmail, ownerRoleId, nowIso } = required;
|
|
411
363
|
// Hashed BEFORE the owner principal row is written, not inline in the `users.save` below.
|
|
412
364
|
// argon2id is deliberately slow, which made it by far the widest window in this un-awaited,
|
|
413
365
|
// non-transactional sequence for a process to exit between the principal insert and the user
|
|
@@ -427,7 +379,7 @@ export async function seedIdentity(required, optional = {}) {
|
|
|
427
379
|
principalId: ownerPrincipalId,
|
|
428
380
|
workspaceId,
|
|
429
381
|
username: ownerUsername,
|
|
430
|
-
email:
|
|
382
|
+
email: ownerEmail,
|
|
431
383
|
passwordHash: ownerPasswordHash,
|
|
432
384
|
});
|
|
433
385
|
// `principal_roles` carries no unique index either — same converge-don't-duplicate rule as the
|
|
@@ -444,6 +396,70 @@ export async function seedIdentity(required, optional = {}) {
|
|
|
444
396
|
roleId: ownerRoleId,
|
|
445
397
|
});
|
|
446
398
|
}
|
|
399
|
+
return ownerPrincipalId;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Seed first-boot identity data.
|
|
403
|
+
*
|
|
404
|
+
* Idempotent in two distinct senses, and both are load-bearing:
|
|
405
|
+
* - A seed that RAN TO COMPLETION is a no-op — the owner lookup below early-returns after
|
|
406
|
+
* confirming its role binding (`ensureOwnerRoleBinding`) and writes nothing. (This path does not
|
|
407
|
+
* reconcile a built-in policy whose permission list has since grown; that is the host's job — see
|
|
408
|
+
* the host's `applyBuiltinRoleGrants` / `migrateDeprecatedPermissionGrants`, chained onto
|
|
409
|
+
* `identityReady`.)
|
|
410
|
+
* - "Ran to completion" is judged on the owner user AND its role link, not the user alone. The link
|
|
411
|
+
* is the seed's last write, so the user is not on its own evidence the seed finished — see
|
|
412
|
+
* `ensureOwnerRoleBinding` for what an interrupt between those two writes costs.
|
|
413
|
+
* - A seed that was INTERRUPTED is resumed, not replayed: every step converges onto the rows a
|
|
414
|
+
* previous attempt already wrote and fills in only what is missing. See the block comment above
|
|
415
|
+
* `ensureBuiltinRole` for why this function must survive being killed halfway.
|
|
416
|
+
*
|
|
417
|
+
* @complexity O(1) owner lookup plus the existing principal roster read; fresh seeds make a
|
|
418
|
+
* fixed small number of inserts (4 roles/policies, ~2 dozen policy_permissions,
|
|
419
|
+
* 3 principals, 1 user, 1 role assignment).
|
|
420
|
+
* @overallScore 100
|
|
421
|
+
*/
|
|
422
|
+
export async function seedIdentity(required, optional = {}) {
|
|
423
|
+
const { deps } = required;
|
|
424
|
+
const input = { ...required.input, ...optional };
|
|
425
|
+
const workspaceId = input.workspaceId;
|
|
426
|
+
const ownerUsername = normalizeUsername({ raw: input.ownerUsername });
|
|
427
|
+
const nowIso = kernelNowIso({ clock: deps.clock });
|
|
428
|
+
const existingOwnerUser = await findExistingOwnerUser({
|
|
429
|
+
deps,
|
|
430
|
+
workspaceId,
|
|
431
|
+
ownerUsername,
|
|
432
|
+
seededOwnerPrincipalId: input.seededOwnerPrincipalId,
|
|
433
|
+
});
|
|
434
|
+
if (existingOwnerUser) {
|
|
435
|
+
// The owner user alone is NOT proof the seed finished — its role link is written after it. See
|
|
436
|
+
// `ensureOwnerRoleBinding`: on a healthy workspace this is one read and no write.
|
|
437
|
+
await ensureOwnerRoleBinding({ deps, workspaceId, ownerPrincipalId: existingOwnerUser.principalId });
|
|
438
|
+
const existingSystem = await findSystemPrincipal({ deps, workspaceId });
|
|
439
|
+
return {
|
|
440
|
+
ownerPrincipalId: existingOwnerUser.principalId,
|
|
441
|
+
systemPrincipalId: existingSystem?.id ?? LEGACY_USER_LOCAL_PRINCIPAL_ID,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
const systemPrincipalId = await ensureSystemPrincipals({ deps, workspaceId, nowIso });
|
|
445
|
+
const { roleId: ownerRoleId } = await seedBuiltinRoleWithPolicy({
|
|
446
|
+
deps,
|
|
447
|
+
workspaceId,
|
|
448
|
+
name: OWNER_ROLE_NAME,
|
|
449
|
+
permissions: OWNER_ROLE_PERMISSIONS,
|
|
450
|
+
});
|
|
451
|
+
for (const role of BUILTIN_NON_OWNER_ROLES) {
|
|
452
|
+
await seedBuiltinRoleWithPolicy({ deps, workspaceId, name: role.name, permissions: role.permissions });
|
|
453
|
+
}
|
|
454
|
+
const ownerPrincipalId = await createOwner({
|
|
455
|
+
deps,
|
|
456
|
+
workspaceId,
|
|
457
|
+
ownerUsername,
|
|
458
|
+
ownerPassword: input.ownerPassword,
|
|
459
|
+
ownerEmail: input.ownerEmail,
|
|
460
|
+
ownerRoleId,
|
|
461
|
+
nowIso,
|
|
462
|
+
});
|
|
447
463
|
return { ownerPrincipalId, systemPrincipalId };
|
|
448
464
|
}
|
|
449
465
|
//# sourceMappingURL=seed.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/user-management",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Runtime-separated user management contracts and Node authentication, authorization, and administration services.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"registry": "https://registry.npmjs.org"
|
|
88
88
|
},
|
|
89
89
|
"dependencies": {
|
|
90
|
-
"@jini-ai/core": "^0.4.
|
|
90
|
+
"@jini-ai/core": "^0.4.9"
|
|
91
91
|
},
|
|
92
92
|
"peerDependencies": {
|
|
93
93
|
"argon2": "^0.44.0",
|
|
@@ -134,12 +134,12 @@
|
|
|
134
134
|
"@testing-library/user-event": "^14.5.0",
|
|
135
135
|
"@testing-library/jest-dom": "^6.6.3",
|
|
136
136
|
"jsdom": "^25.0.1",
|
|
137
|
-
"@jini-ai/ui": "0.5.
|
|
138
|
-
"@jini-ai/
|
|
139
|
-
"@jini-ai/
|
|
140
|
-
"@jini-ai/
|
|
141
|
-
"@jini-ai/
|
|
142
|
-
"@jini-ai/
|
|
137
|
+
"@jini-ai/ui": "0.5.6",
|
|
138
|
+
"@jini-ai/agentic": "0.4.5",
|
|
139
|
+
"@jini-ai/core": "^0.4.9",
|
|
140
|
+
"@jini-ai/ui-kit": "^0.1.5",
|
|
141
|
+
"@jini-ai/admin": "0.4.13",
|
|
142
|
+
"@jini-ai/cms": "0.5.14"
|
|
143
143
|
},
|
|
144
144
|
"scripts": {
|
|
145
145
|
"build": "tsc -p tsconfig.json",
|