@astram/identity 0.13.0 → 0.15.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/dist/features/authz/grant-admin.service.d.ts +1 -1
- package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
- package/dist/features/authz/grant-admin.service.js +8 -0
- package/dist/features/authz/grant-ceiling.d.ts +11 -3
- package/dist/features/authz/grant-ceiling.d.ts.map +1 -1
- package/dist/features/authz/grant-ceiling.js +20 -3
- package/dist/features/authz/record-authorization.d.ts +1 -1
- package/dist/features/authz/record-authorization.d.ts.map +1 -1
- package/dist/features/authz/record-authorization.js +3 -2
- package/dist/features/company/company-hierarchy.service.d.ts +6 -0
- package/dist/features/company/company-hierarchy.service.d.ts.map +1 -1
- package/dist/features/company/company-hierarchy.service.js +19 -0
- package/dist/features/identity-feature.module.d.ts +7 -1
- package/dist/features/identity-feature.module.d.ts.map +1 -1
- package/dist/features/identity-feature.module.js +46 -1
- package/dist/features/membership/membership.service.d.ts +9 -0
- package/dist/features/membership/membership.service.d.ts.map +1 -1
- package/dist/features/membership/membership.service.js +40 -0
- package/dist/features/membership/posting.service.d.ts +18 -0
- package/dist/features/membership/posting.service.d.ts.map +1 -1
- package/dist/features/membership/posting.service.js +85 -27
- package/dist/features/org-bootstrap/org-bootstrap-error.d.ts +20 -0
- package/dist/features/org-bootstrap/org-bootstrap-error.d.ts.map +1 -0
- package/dist/features/org-bootstrap/org-bootstrap-error.js +24 -0
- package/dist/features/org-bootstrap/org-bootstrap-file.d.ts +58 -0
- package/dist/features/org-bootstrap/org-bootstrap-file.d.ts.map +1 -0
- package/dist/features/org-bootstrap/org-bootstrap-file.js +323 -0
- package/dist/features/org-bootstrap/org-bootstrap-plan.d.ts +79 -0
- package/dist/features/org-bootstrap/org-bootstrap-plan.d.ts.map +1 -0
- package/dist/features/org-bootstrap/org-bootstrap-plan.js +288 -0
- package/dist/features/org-bootstrap/org-bootstrap.manifest.d.ts +19 -0
- package/dist/features/org-bootstrap/org-bootstrap.manifest.d.ts.map +1 -0
- package/dist/features/org-bootstrap/org-bootstrap.manifest.js +30 -0
- package/dist/features/org-bootstrap/org-bootstrap.tokens.d.ts +8 -0
- package/dist/features/org-bootstrap/org-bootstrap.tokens.d.ts.map +1 -0
- package/dist/features/org-bootstrap/org-bootstrap.tokens.js +10 -0
- package/dist/features/org-bootstrap/org-names.d.ts +12 -0
- package/dist/features/org-bootstrap/org-names.d.ts.map +1 -0
- package/dist/features/org-bootstrap/org-names.js +26 -0
- package/dist/features/org-bootstrap/org-state.reader.d.ts +15 -0
- package/dist/features/org-bootstrap/org-state.reader.d.ts.map +1 -0
- package/dist/features/org-bootstrap/org-state.reader.js +225 -0
- package/dist/features/org-bootstrap/root-password-source.d.ts +9 -0
- package/dist/features/org-bootstrap/root-password-source.d.ts.map +1 -0
- package/dist/features/org-bootstrap/root-password-source.js +57 -0
- package/dist/features/org-bootstrap/run-org-bootstrap.d.ts +61 -0
- package/dist/features/org-bootstrap/run-org-bootstrap.d.ts.map +1 -0
- package/dist/features/org-bootstrap/run-org-bootstrap.js +774 -0
- package/dist/features/org-bootstrap/set-password-links.d.ts +42 -0
- package/dist/features/org-bootstrap/set-password-links.d.ts.map +1 -0
- package/dist/features/org-bootstrap/set-password-links.js +76 -0
- package/dist/features/org-structure/org-catalog.service.d.ts +13 -0
- package/dist/features/org-structure/org-catalog.service.d.ts.map +1 -1
- package/dist/features/org-structure/org-catalog.service.js +40 -0
- package/dist/features/role/role-owner-transition.d.ts +17 -0
- package/dist/features/role/role-owner-transition.d.ts.map +1 -0
- package/dist/features/role/role-owner-transition.js +23 -0
- package/dist/features/role/role.controller.d.ts +1 -1
- package/dist/features/role/role.controller.d.ts.map +1 -1
- package/dist/features/role/role.controller.js +6 -4
- package/dist/features/role/role.service.d.ts +8 -2
- package/dist/features/role/role.service.d.ts.map +1 -1
- package/dist/features/role/role.service.js +13 -3
- package/dist/features/user/credential-provisioner.token.d.ts +8 -0
- package/dist/features/user/credential-provisioner.token.d.ts.map +1 -0
- package/dist/features/user/credential-provisioner.token.js +10 -0
- package/dist/features/user/invite-message.d.ts +8 -0
- package/dist/features/user/invite-message.d.ts.map +1 -0
- package/dist/features/user/invite-message.js +23 -0
- package/dist/features/user/invite.config.d.ts +15 -0
- package/dist/features/user/invite.config.d.ts.map +1 -0
- package/dist/features/user/invite.config.js +55 -0
- package/dist/features/user/invite.service.d.ts +62 -0
- package/dist/features/user/invite.service.d.ts.map +1 -0
- package/dist/features/user/invite.service.js +254 -0
- package/dist/features/user/invite.tokens.d.ts +11 -0
- package/dist/features/user/invite.tokens.d.ts.map +1 -0
- package/dist/features/user/invite.tokens.js +13 -0
- package/dist/features/user/user.controller.d.ts +13 -6
- package/dist/features/user/user.controller.d.ts.map +1 -1
- package/dist/features/user/user.controller.js +30 -4
- package/dist/features/user/user.service.d.ts +51 -10
- package/dist/features/user/user.service.d.ts.map +1 -1
- package/dist/features/user/user.service.js +96 -27
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/internal.d.ts +2 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +3 -1
- package/dist/role.object.d.ts +1 -0
- package/dist/role.object.d.ts.map +1 -1
- package/dist/role.object.js +8 -0
- package/dist/user.object.d.ts.map +1 -1
- package/dist/user.object.js +3 -0
- package/migrations/1710000000101-AddRoleOwnerCompany.cjs +98 -0
- package/package.json +4 -4
|
@@ -16,12 +16,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
16
16
|
exports.UserService = void 0;
|
|
17
17
|
const common_1 = require("@nestjs/common");
|
|
18
18
|
const engine_1 = require("@astram/engine");
|
|
19
|
+
const internal_1 = require("@astram/engine/internal");
|
|
19
20
|
const guard_1 = require("@astram/guard");
|
|
20
21
|
const posting_version_bump_js_1 = require("../membership/posting-version-bump.js");
|
|
21
22
|
const list_query_js_1 = require("../shared/list-query.js");
|
|
22
23
|
const grant_ceiling_js_1 = require("../authz/grant-ceiling.js");
|
|
23
24
|
const identity_errors_js_1 = require("../shared/identity-errors.js");
|
|
24
25
|
const email_address_changer_token_js_1 = require("./email-address-changer.token.js");
|
|
26
|
+
const credential_provisioner_token_js_1 = require("./credential-provisioner.token.js");
|
|
25
27
|
const framework_schemas_token_js_1 = require("../framework-schemas.token.js");
|
|
26
28
|
/**
|
|
27
29
|
* KI #87: `users` has two unique keys besides the PK, `IDX_users_username` (migration 001) and
|
|
@@ -43,11 +45,19 @@ async function mappingDuplicates(write) {
|
|
|
43
45
|
return await write();
|
|
44
46
|
}
|
|
45
47
|
catch (err) {
|
|
46
|
-
|
|
48
|
+
// R1-4: an orphan credential already holds this login hint (deleteUser leaves it): same fixed
|
|
49
|
+
// 409 as a duplicate email, never the hint.
|
|
50
|
+
if (isDuplicateKey(err) || err instanceof guard_1.CredentialExistsError) {
|
|
47
51
|
throw new identity_errors_js_1.IdentityConflictError(USERNAME_OR_EMAIL_TAKEN);
|
|
52
|
+
}
|
|
48
53
|
throw err;
|
|
49
54
|
}
|
|
50
55
|
}
|
|
56
|
+
const SIGN_IN_STATUS = {
|
|
57
|
+
locked: 'pending',
|
|
58
|
+
usable: 'active',
|
|
59
|
+
none: 'none',
|
|
60
|
+
};
|
|
51
61
|
const USER_SEARCH_FIELDS = ['username', 'email'];
|
|
52
62
|
const USER_SORT_FIELDS = ['username', 'email', 'status', 'createdAt'];
|
|
53
63
|
/**
|
|
@@ -77,6 +87,8 @@ let UserService = UserService_1 = class UserService {
|
|
|
77
87
|
sessions;
|
|
78
88
|
versionBump;
|
|
79
89
|
emailAddresses;
|
|
90
|
+
provisioner;
|
|
91
|
+
dispatcher;
|
|
80
92
|
logger = new common_1.Logger(UserService_1.name);
|
|
81
93
|
/**
|
|
82
94
|
* The engine's audit sink, used on the email helper's transaction (no pool of its own), on the
|
|
@@ -84,7 +96,7 @@ let UserService = UserService_1 = class UserService {
|
|
|
84
96
|
* service) ⇒ the sink resolves `SHIELD_DB_NAMESPACE` itself.
|
|
85
97
|
*/
|
|
86
98
|
emailAudit;
|
|
87
|
-
constructor(records, ceiling, resetTokens, emailSender, sessions, versionBump, emailAddresses, schemas) {
|
|
99
|
+
constructor(records, ceiling, resetTokens, emailSender, sessions, versionBump, emailAddresses, schemas, provisioner, dispatcher) {
|
|
88
100
|
this.records = records;
|
|
89
101
|
this.ceiling = ceiling;
|
|
90
102
|
this.resetTokens = resetTokens;
|
|
@@ -92,8 +104,17 @@ let UserService = UserService_1 = class UserService {
|
|
|
92
104
|
this.sessions = sessions;
|
|
93
105
|
this.versionBump = versionBump;
|
|
94
106
|
this.emailAddresses = emailAddresses;
|
|
107
|
+
this.provisioner = provisioner;
|
|
108
|
+
this.dispatcher = dispatcher;
|
|
95
109
|
this.emailAudit = new engine_1.MariaDbAuditSink(schemas ? { schema: schemas.audit } : {});
|
|
96
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Create a user; returns its id. Delegates to {@link createUserWithCredential} (same behaviour;
|
|
113
|
+
* the detailed credential outcome is dropped to keep this public signature stable).
|
|
114
|
+
*/
|
|
115
|
+
async createUser(actor, input) {
|
|
116
|
+
return (await this.createUserWithCredential(actor, input)).id;
|
|
117
|
+
}
|
|
97
118
|
/**
|
|
98
119
|
* Create a user. With `companyUuid`, the user is created AND attached (one active membership) in
|
|
99
120
|
* ONE transaction — the onboarding path for a non-root admin, whose membership create is checked
|
|
@@ -103,9 +124,13 @@ let UserService = UserService_1 = class UserService {
|
|
|
103
124
|
*
|
|
104
125
|
* KI #94: an `email` is stored canonical (the ONE address rule, as the email-change flow); a
|
|
105
126
|
* malformed one ⇒ {@link IdentityBadRequestError} (400) before any write; a blank one is no email.
|
|
106
|
-
*
|
|
127
|
+
*
|
|
128
|
+
* G-009 T7 (OD-6): with an email AND a company, a LOCKED credential (no password verifies; set
|
|
129
|
+
* later by reset) is created in that same transaction through the provisioner on the engine
|
|
130
|
+
* transaction's own connection; a provisioner failure rolls everything back. Otherwise no
|
|
131
|
+
* credential is created and the result says why (no email takes precedence).
|
|
107
132
|
*/
|
|
108
|
-
async
|
|
133
|
+
async createUserWithCredential(actor, input) {
|
|
109
134
|
const fields = { username: input.username };
|
|
110
135
|
const email = typeof input.email === 'string' ? (0, guard_1.canonicalEmail)(input.email) : '';
|
|
111
136
|
if (email !== '') {
|
|
@@ -113,25 +138,56 @@ let UserService = UserService_1 = class UserService {
|
|
|
113
138
|
throw new identity_errors_js_1.IdentityBadRequestError('Enter a valid email address.');
|
|
114
139
|
fields.email = email;
|
|
115
140
|
}
|
|
116
|
-
if (!input.companyUuid) {
|
|
117
|
-
return mappingDuplicates(() => this.records.create('user', { fields }));
|
|
118
|
-
}
|
|
119
141
|
const companyUuid = input.companyUuid;
|
|
120
|
-
|
|
121
|
-
const id = await
|
|
142
|
+
if (!companyUuid) {
|
|
143
|
+
const id = await mappingDuplicates(() => this.records.create('user', { fields }));
|
|
144
|
+
return { id, credential: 'none', reason: email === '' ? 'no_email' : 'no_company' };
|
|
145
|
+
}
|
|
146
|
+
const provisioner = this.provisioner;
|
|
147
|
+
const provision = email !== '' && provisioner != null;
|
|
148
|
+
const id = await mappingDuplicates(() => this.records.transaction(async (tx) => {
|
|
149
|
+
const created = await tx.create('user', { fields });
|
|
122
150
|
await tx.create('company_membership', {
|
|
123
|
-
fields: { userUuid:
|
|
151
|
+
fields: { userUuid: created, companyUuid, isActive: true, status: 'active' },
|
|
124
152
|
});
|
|
125
|
-
|
|
153
|
+
if (provision) {
|
|
154
|
+
await provisioner.provisionLocked({ principalId: created, hint: email, tenantId: companyUuid }, (0, internal_1.transactionQueryOf)(tx));
|
|
155
|
+
}
|
|
156
|
+
return created;
|
|
126
157
|
}));
|
|
158
|
+
if (provision)
|
|
159
|
+
return { id, credential: 'locked' };
|
|
160
|
+
return {
|
|
161
|
+
id,
|
|
162
|
+
credential: 'none',
|
|
163
|
+
reason: email === '' ? 'no_email' : 'no_provisioner',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* List users (tenant-scoped by the pipe), with paging/search/sort. Each row gains a top-level
|
|
168
|
+
* `signInStatus` (T10), from ONE batched credential read for the page.
|
|
169
|
+
*/
|
|
170
|
+
async listUsers(params) {
|
|
171
|
+
const page = await this.records.list('user', (0, list_query_js_1.buildListQuery)(params, USER_SEARCH_FIELDS, USER_SORT_FIELDS));
|
|
172
|
+
return { ...page, rows: await this.withSignInStatus(page.rows) };
|
|
127
173
|
}
|
|
128
|
-
/**
|
|
129
|
-
|
|
130
|
-
|
|
174
|
+
/** Read a single user, with its top-level `signInStatus` (T10). */
|
|
175
|
+
async getUser(uuid) {
|
|
176
|
+
const record = await this.records.get('user', uuid);
|
|
177
|
+
if (record === null)
|
|
178
|
+
return null;
|
|
179
|
+
return (await this.withSignInStatus([record]))[0];
|
|
131
180
|
}
|
|
132
|
-
/**
|
|
133
|
-
|
|
134
|
-
|
|
181
|
+
/** One `statesOf` call for the given rows; no provisioner (hand-built service) ⇒ rows unchanged. */
|
|
182
|
+
async withSignInStatus(rows) {
|
|
183
|
+
const provisioner = this.provisioner;
|
|
184
|
+
if (rows.length === 0 || provisioner == null)
|
|
185
|
+
return [...rows];
|
|
186
|
+
const states = await provisioner.statesOf(rows.map((r) => String(r.id)));
|
|
187
|
+
return rows.map((r) => ({
|
|
188
|
+
...r,
|
|
189
|
+
signInStatus: SIGN_IN_STATUS[states.get(String(r.id)) ?? 'none'],
|
|
190
|
+
}));
|
|
135
191
|
}
|
|
136
192
|
/** Update a user (whitelisted patch; username/email/status changes need dominance — class doc). */
|
|
137
193
|
async updateUser(actor, uuid, patch) {
|
|
@@ -170,7 +226,7 @@ let UserService = UserService_1 = class UserService {
|
|
|
170
226
|
if (statusChanged || wantsInactive)
|
|
171
227
|
await this.applyStatusChange(uuid, String(patch.status));
|
|
172
228
|
if (emailChanged && oldEmail)
|
|
173
|
-
await this.notifyEmailChanged(oldEmail);
|
|
229
|
+
await this.notifyEmailChanged(actor, oldEmail);
|
|
174
230
|
}
|
|
175
231
|
/**
|
|
176
232
|
* Phase 12 (KI #14): write a dominated user's new (canonical) email through guard's
|
|
@@ -261,19 +317,28 @@ let UserService = UserService_1 = class UserService {
|
|
|
261
317
|
await this.records.delete('user', uuid);
|
|
262
318
|
}
|
|
263
319
|
/** Tell the OLD address its account email changed (best-effort; never names the new one). */
|
|
264
|
-
async notifyEmailChanged(oldEmail) {
|
|
265
|
-
if (!this.emailSender)
|
|
320
|
+
async notifyEmailChanged(actor, oldEmail) {
|
|
321
|
+
if (!this.dispatcher && !this.emailSender)
|
|
266
322
|
return;
|
|
267
323
|
const text = 'The email address on your account was changed by an administrator. ' +
|
|
268
324
|
'Password-reset links sent before this change no longer work. ' +
|
|
269
325
|
'If you did not expect this, contact your administrator.';
|
|
326
|
+
const message = {
|
|
327
|
+
to: oldEmail,
|
|
328
|
+
subject: 'Your account email address was changed',
|
|
329
|
+
text,
|
|
330
|
+
html: `<p>${text}</p>`,
|
|
331
|
+
};
|
|
270
332
|
try {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
333
|
+
if (this.dispatcher) {
|
|
334
|
+
// G-010: bounded and audited (`message.sent` / `message.failed`); never throws.
|
|
335
|
+
await this.dispatcher.sendEmail('security_notice', message, {
|
|
336
|
+
actorId: actor.id,
|
|
337
|
+
tenantId: actor.tenantId,
|
|
338
|
+
});
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
await this.emailSender.send(message);
|
|
277
342
|
}
|
|
278
343
|
catch {
|
|
279
344
|
// Best-effort: the change is committed; a delivery failure must not fail it.
|
|
@@ -296,6 +361,10 @@ exports.UserService = UserService = UserService_1 = __decorate([
|
|
|
296
361
|
__param(6, (0, common_1.Inject)(email_address_changer_token_js_1.IDENTITY_EMAIL_ADDRESS_CHANGER)),
|
|
297
362
|
__param(7, (0, common_1.Optional)()),
|
|
298
363
|
__param(7, (0, common_1.Inject)(framework_schemas_token_js_1.IDENTITY_FRAMEWORK_SCHEMAS)),
|
|
364
|
+
__param(8, (0, common_1.Optional)()),
|
|
365
|
+
__param(8, (0, common_1.Inject)(credential_provisioner_token_js_1.IDENTITY_CREDENTIAL_PROVISIONER)),
|
|
366
|
+
__param(9, (0, common_1.Optional)()),
|
|
367
|
+
__param(9, (0, common_1.Inject)(guard_1.SHIELD_MESSAGE_DISPATCHER)),
|
|
299
368
|
__metadata("design:paramtypes", [engine_1.RecordsService,
|
|
300
|
-
grant_ceiling_js_1.GrantCeiling, Object, Object, Object, Object, Object, Object])
|
|
369
|
+
grant_ceiling_js_1.GrantCeiling, Object, Object, Object, Object, Object, Object, Object, Object])
|
|
301
370
|
], UserService);
|
package/dist/index.d.ts
CHANGED
|
@@ -40,6 +40,19 @@ export { buildIdentityAuthKeys, type IdentityAuthKeys } from './auth-facts-keys.
|
|
|
40
40
|
* `identityObjects`, and mounts the CRUD controllers — no separate engine setup needed.
|
|
41
41
|
*/
|
|
42
42
|
export { IdentityFeatureModule } from './features/identity-feature.module.js';
|
|
43
|
+
/**
|
|
44
|
+
* G-010: the administrator invite (`POST /identity/users/:uuid/invite`, option
|
|
45
|
+
* `forRoot({ invite })`) and the computed `signInStatus` on user reads (types only).
|
|
46
|
+
*/
|
|
47
|
+
export type { InviteOptions } from './features/user/invite.config.js';
|
|
48
|
+
export type { InviteResult } from './features/user/invite.service.js';
|
|
49
|
+
export type { SignInStatus, UserRecord } from './features/user/user.service.js';
|
|
50
|
+
/**
|
|
51
|
+
* The org bootstrap (G-009): `runOrgBootstrap(app, { file, apply })` applies a declarative org file
|
|
52
|
+
* (roots, company tree, roles, users, postings) through the framework's own services, as the declared
|
|
53
|
+
* operation `shield:org-bootstrap`. Run at deploy time against the app's own `AppModule` context.
|
|
54
|
+
*/
|
|
55
|
+
export { runOrgBootstrap, OrgBootstrapError, type OrgBootstrapReport, type RunOrgBootstrapOptions, } from './features/org-bootstrap/run-org-bootstrap.js';
|
|
43
56
|
/**
|
|
44
57
|
* Stored files (file-storage SPEC §5 I-K): `store` / `open` / `remove` on the engine pipe, provided
|
|
45
58
|
* and exported by `IdentityFeatureModule`. Apps call it from their own `@Classified` routes.
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAE5D;;;;GAIG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C;;;;;;GAMG;AACH,OAAO,EAAE,sBAAsB,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AACxF;0DAC0D;AAC1D,OAAO,EAAE,qBAAqB,EAAE,KAAK,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAEpF;;;;;GAKG;AACH,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAE9E;;;GAGG;AACH,OAAO,EACL,YAAY,EACZ,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,UAAU,GAChB,MAAM,mCAAmC,CAAC;AAU3C;;;GAGG;AACH,OAAO,EAAE,qBAAqB,EAAE,MAAM,0CAA0C,CAAC;AAEjF;;;;;;GAMG;AACH,OAAO,EAAE,sBAAsB,EAAE,MAAM,4CAA4C,CAAC;AAEpF;;;;;;;GAOG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,6CAA6C,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAE5D;;;;GAIG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C;;;;;;GAMG;AACH,OAAO,EAAE,sBAAsB,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AACxF;0DAC0D;AAC1D,OAAO,EAAE,qBAAqB,EAAE,KAAK,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAEpF;;;;;GAKG;AACH,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAE9E;;;GAGG;AACH,YAAY,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AACtE,YAAY,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AACtE,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAEhF;;;;GAIG;AACH,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,GAC5B,MAAM,+CAA+C,CAAC;AAEvD;;;GAGG;AACH,OAAO,EACL,YAAY,EACZ,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,UAAU,GAChB,MAAM,mCAAmC,CAAC;AAU3C;;;GAGG;AACH,OAAO,EAAE,qBAAqB,EAAE,MAAM,0CAA0C,CAAC;AAEjF;;;;;;GAMG;AACH,OAAO,EAAE,sBAAsB,EAAE,MAAM,4CAA4C,CAAC;AAEpF;;;;;;;GAOG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,6CAA6C,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DbMembershipChecker = exports.DbScopeSubtreeResolver = exports.AlsSessionFactsReader = exports.FilesService = exports.IdentityFeatureModule = exports.buildIdentityAuthKeys = exports.identityControlPlaneKeys = exports.identityAuthActionKeys = exports.identityObjects = exports.identityMigrationsDir = exports.StoredFile = exports.ServiceAccount = exports.CompanyBranch = exports.CompanyDepartment = exports.MembershipPosting = exports.CompanyMembership = exports.Branch = exports.Department = exports.Role = exports.User = exports.Company = void 0;
|
|
3
|
+
exports.DbMembershipChecker = exports.DbScopeSubtreeResolver = exports.AlsSessionFactsReader = exports.FilesService = exports.OrgBootstrapError = exports.runOrgBootstrap = exports.IdentityFeatureModule = exports.buildIdentityAuthKeys = exports.identityControlPlaneKeys = exports.identityAuthActionKeys = exports.identityObjects = exports.identityMigrationsDir = exports.StoredFile = exports.ServiceAccount = exports.CompanyBranch = exports.CompanyDepartment = exports.MembershipPosting = exports.CompanyMembership = exports.Branch = exports.Department = exports.Role = exports.User = exports.Company = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* @astram/identity — the framework-owned identity + tenancy model.
|
|
6
6
|
* The 9 identity object classes (relocated from the consumer app in Sub-plan B) +
|
|
@@ -60,6 +60,14 @@ Object.defineProperty(exports, "buildIdentityAuthKeys", { enumerable: true, get:
|
|
|
60
60
|
*/
|
|
61
61
|
var identity_feature_module_js_1 = require("./features/identity-feature.module.js");
|
|
62
62
|
Object.defineProperty(exports, "IdentityFeatureModule", { enumerable: true, get: function () { return identity_feature_module_js_1.IdentityFeatureModule; } });
|
|
63
|
+
/**
|
|
64
|
+
* The org bootstrap (G-009): `runOrgBootstrap(app, { file, apply })` applies a declarative org file
|
|
65
|
+
* (roots, company tree, roles, users, postings) through the framework's own services, as the declared
|
|
66
|
+
* operation `shield:org-bootstrap`. Run at deploy time against the app's own `AppModule` context.
|
|
67
|
+
*/
|
|
68
|
+
var run_org_bootstrap_js_1 = require("./features/org-bootstrap/run-org-bootstrap.js");
|
|
69
|
+
Object.defineProperty(exports, "runOrgBootstrap", { enumerable: true, get: function () { return run_org_bootstrap_js_1.runOrgBootstrap; } });
|
|
70
|
+
Object.defineProperty(exports, "OrgBootstrapError", { enumerable: true, get: function () { return run_org_bootstrap_js_1.OrgBootstrapError; } });
|
|
63
71
|
/**
|
|
64
72
|
* Stored files (file-storage SPEC §5 I-K): `store` / `open` / `remove` on the engine pipe, provided
|
|
65
73
|
* and exported by `IdentityFeatureModule`. Apps call it from their own `@Classified` routes.
|
package/dist/internal.d.ts
CHANGED
|
@@ -8,4 +8,6 @@
|
|
|
8
8
|
* subpath everywhere else. Feature code never holds a pool: it uses the RecordEngine.
|
|
9
9
|
*/
|
|
10
10
|
export { createIdentityAuthzPool } from './features/authz/record-authorization.js';
|
|
11
|
+
export { transitionRoleOwner } from './features/role/role-owner-transition.js';
|
|
12
|
+
export type { RoleOwnerAudit, RoleOwnerQuery } from './features/role/role-owner-transition.js';
|
|
11
13
|
//# sourceMappingURL=internal.d.ts.map
|
package/dist/internal.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["../src/internal.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,uBAAuB,EAAE,MAAM,0CAA0C,CAAC"}
|
|
1
|
+
{"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["../src/internal.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,uBAAuB,EAAE,MAAM,0CAA0C,CAAC;AACnF,OAAO,EAAE,mBAAmB,EAAE,MAAM,0CAA0C,CAAC;AAC/E,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,0CAA0C,CAAC"}
|
package/dist/internal.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createIdentityAuthzPool = void 0;
|
|
3
|
+
exports.transitionRoleOwner = exports.createIdentityAuthzPool = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* @astram/identity/internal — composition-root surface (Phase 8 #5).
|
|
6
6
|
*
|
|
@@ -12,3 +12,5 @@ exports.createIdentityAuthzPool = void 0;
|
|
|
12
12
|
*/
|
|
13
13
|
var record_authorization_js_1 = require("./features/authz/record-authorization.js");
|
|
14
14
|
Object.defineProperty(exports, "createIdentityAuthzPool", { enumerable: true, get: function () { return record_authorization_js_1.createIdentityAuthzPool; } });
|
|
15
|
+
var role_owner_transition_js_1 = require("./features/role/role-owner-transition.js");
|
|
16
|
+
Object.defineProperty(exports, "transitionRoleOwner", { enumerable: true, get: function () { return role_owner_transition_js_1.transitionRoleOwner; } });
|
package/dist/role.object.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role.object.d.ts","sourceRoot":"","sources":["../src/role.object.ts"],"names":[],"mappings":"AAGA,8BAA8B;AAC9B,qBAoBa,IAAI;IAEf,IAAI,EAAG,MAAM,CAAC;IAGd,IAAI,EAAG,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"role.object.d.ts","sourceRoot":"","sources":["../src/role.object.ts"],"names":[],"mappings":"AAGA,8BAA8B;AAC9B,qBAoBa,IAAI;IAEf,IAAI,EAAG,MAAM,CAAC;IAGd,IAAI,EAAG,MAAM,CAAC;IAMd,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAGjC,SAAS,EAAG,MAAM,CAAC;CACpB"}
|
package/dist/role.object.js
CHANGED
|
@@ -16,6 +16,10 @@ const schema_1 = require("./schema");
|
|
|
16
16
|
let Role = class Role {
|
|
17
17
|
uuid;
|
|
18
18
|
name;
|
|
19
|
+
// The company a role was created for (G-009 T14, SPEC R13); NULL = shared (root-created). Create-only
|
|
20
|
+
// and server-set (RoleService stamps it from the actor); NOT `trusted`, so the engine never
|
|
21
|
+
// derives it and root's UI-created roles stay unowned. Changed later only by `transitionRoleOwner`.
|
|
22
|
+
ownerCompanyUuid;
|
|
19
23
|
createdAt;
|
|
20
24
|
};
|
|
21
25
|
exports.Role = Role;
|
|
@@ -27,6 +31,10 @@ __decorate([
|
|
|
27
31
|
(0, engine_1.Field)({ column: 'r_name', type: 'string', required: true }),
|
|
28
32
|
__metadata("design:type", String)
|
|
29
33
|
], Role.prototype, "name", void 0);
|
|
34
|
+
__decorate([
|
|
35
|
+
(0, engine_1.Field)({ column: 'r_owner_company_uuid', type: 'uuid', immutable: true }),
|
|
36
|
+
__metadata("design:type", Object)
|
|
37
|
+
], Role.prototype, "ownerCompanyUuid", void 0);
|
|
30
38
|
__decorate([
|
|
31
39
|
(0, engine_1.Field)({ column: 'r_created_at', type: 'timestamp' }),
|
|
32
40
|
__metadata("design:type", String)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user.object.d.ts","sourceRoot":"","sources":["../src/user.object.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,
|
|
1
|
+
{"version":3,"file":"user.object.d.ts","sourceRoot":"","sources":["../src/user.object.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,qBA0Ba,IAAI;IAEf,IAAI,EAAG,MAAM,CAAC;IAGd,QAAQ,EAAG,MAAM,CAAC;IAGlB,KAAK,EAAG,MAAM,CAAC;IAGf,MAAM,EAAG,MAAM,CAAC;IAGhB,SAAS,EAAG,MAAM,CAAC;CACpB"}
|
package/dist/user.object.js
CHANGED
|
@@ -54,6 +54,9 @@ exports.User = User = __decorate([
|
|
|
54
54
|
// capped root, the identity administrator, may list/read AND create/update/delete users). Uniform
|
|
55
55
|
// across all actions (read reclassified control 2026-07-16), so plane is per-type.
|
|
56
56
|
access: { plane: 'control', system: true },
|
|
57
|
+
// G-010 R11 (ADR-0038): `invite` (`identity:user:invite`, POST /identity/users/:uuid/invite) next
|
|
58
|
+
// to CRUD. Roles holding `identity:*` or `identity:user:*` gain it on upgrade; exact keys do not.
|
|
59
|
+
actions: ['invite'],
|
|
57
60
|
// Read-scope wall (ADR-0027, Task 4.1): a user has no company column (they belong to many
|
|
58
61
|
// companies via `company_membership`) — a user is in-scope iff they have a membership in
|
|
59
62
|
// the caller's reachable subtree. A multi-tenant user correctly appears for each subtree
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework identity migration — `roles.r_owner_company_uuid` (G-009 T14, AC-19; SPEC R13).
|
|
3
|
+
*
|
|
4
|
+
* WHY: `roles` was a global catalog with no owner, so a company delete could not say which roles
|
|
5
|
+
* were its own. The owner is the company (tenant node) a role was created for; NULL = shared
|
|
6
|
+
* (root-created, or unowned). It is create-only at the engine (immutable); the only later change
|
|
7
|
+
* is the audited `transitionRoleOwner` seam (company -> NULL on a company delete, NULL -> company
|
|
8
|
+
* for an owner claim).
|
|
9
|
+
*
|
|
10
|
+
* Steps (idempotent; a half-applied state is completed, nothing is dropped or overwritten):
|
|
11
|
+
* 1. ADD the nullable column when absent.
|
|
12
|
+
* 2. ADD `idx_roles_owner_company` when absent (the FK needs it; the delete's owned-roles read).
|
|
13
|
+
* 3. ADD `FK_roles_owner_company` -> `companies (c_uuid)` ON DELETE SET NULL when absent. The
|
|
14
|
+
* app clears owners itself on a company delete (audited); SET NULL is only the backstop.
|
|
15
|
+
* 4. BACK-FILL unowned roles bound ONLY inside ONE top-level tenant's subtree (postings ->
|
|
16
|
+
* memberships -> companies -> `scope_closure` ancestors with no parent) to that tenant. A role
|
|
17
|
+
* bound across several tenants, or bound nowhere, stays NULL. Only `IS NULL` rows are
|
|
18
|
+
* touched, so a re-run never overwrites an owner. No ids are hardcoded.
|
|
19
|
+
*
|
|
20
|
+
* `down` drops the FK, then the index, then the column, each with IF EXISTS (a second run, or a
|
|
21
|
+
* database that never had the column, does nothing).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const { frameworkSchemasForMigration } = require('@astram/core');
|
|
25
|
+
|
|
26
|
+
const COLUMN = 'r_owner_company_uuid';
|
|
27
|
+
const INDEX = 'idx_roles_owner_company';
|
|
28
|
+
const FK = 'FK_roles_owner_company';
|
|
29
|
+
|
|
30
|
+
const present = async (query, sql, params) => {
|
|
31
|
+
const rows = await query(sql, params);
|
|
32
|
+
return Array.isArray(rows) && rows.length > 0;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
async function up(ctx) {
|
|
36
|
+
const { query } = ctx;
|
|
37
|
+
const schemas = frameworkSchemasForMigration(ctx, process.env);
|
|
38
|
+
const APP = schemas.app;
|
|
39
|
+
const AUTHZ = schemas.authz;
|
|
40
|
+
|
|
41
|
+
const hasColumn = await present(
|
|
42
|
+
query,
|
|
43
|
+
`SELECT 1 AS n FROM information_schema.COLUMNS
|
|
44
|
+
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = 'roles' AND COLUMN_NAME = ? LIMIT 1`,
|
|
45
|
+
[APP, COLUMN],
|
|
46
|
+
);
|
|
47
|
+
if (!hasColumn) {
|
|
48
|
+
await query(`ALTER TABLE \`${APP}\`.\`roles\` ADD COLUMN ${COLUMN} VARCHAR(36) NULL`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const hasIndex = await present(
|
|
52
|
+
query,
|
|
53
|
+
`SELECT 1 AS n FROM information_schema.STATISTICS
|
|
54
|
+
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = 'roles' AND INDEX_NAME = ? LIMIT 1`,
|
|
55
|
+
[APP, INDEX],
|
|
56
|
+
);
|
|
57
|
+
if (!hasIndex) {
|
|
58
|
+
await query(`CREATE INDEX ${INDEX} ON \`${APP}\`.\`roles\` (${COLUMN})`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const hasFk = await present(
|
|
62
|
+
query,
|
|
63
|
+
`SELECT 1 AS n FROM information_schema.REFERENTIAL_CONSTRAINTS
|
|
64
|
+
WHERE CONSTRAINT_SCHEMA = ? AND TABLE_NAME = 'roles' AND CONSTRAINT_NAME = ? LIMIT 1`,
|
|
65
|
+
[APP, FK],
|
|
66
|
+
);
|
|
67
|
+
if (!hasFk) {
|
|
68
|
+
await query(`ALTER TABLE \`${APP}\`.\`roles\`
|
|
69
|
+
ADD CONSTRAINT ${FK} FOREIGN KEY (${COLUMN})
|
|
70
|
+
REFERENCES \`${APP}\`.\`companies\` (c_uuid) ON DELETE SET NULL`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Back-fill: exactly one top-level tenant over every posting of the role, else leave NULL.
|
|
74
|
+
const tenantsOfRole = `SELECT COUNT(DISTINCT sc.ancestor_uuid)
|
|
75
|
+
FROM \`${APP}\`.\`membership_posting\` mp
|
|
76
|
+
JOIN \`${APP}\`.\`company_membership\` cm ON cm.cm_uuid = mp.mp_membership_uuid
|
|
77
|
+
JOIN \`${AUTHZ}\`.\`scope_closure\` sc ON sc.descendant_uuid = cm.cm_company_uuid
|
|
78
|
+
JOIN \`${APP}\`.\`companies\` top ON top.c_uuid = sc.ancestor_uuid AND top.c_parent_uuid IS NULL
|
|
79
|
+
WHERE mp.mp_role_uuid = r.r_uuid`;
|
|
80
|
+
await query(`UPDATE \`${APP}\`.\`roles\` r
|
|
81
|
+
SET r.${COLUMN} = (SELECT MIN(sc.ancestor_uuid)
|
|
82
|
+
FROM \`${APP}\`.\`membership_posting\` mp
|
|
83
|
+
JOIN \`${APP}\`.\`company_membership\` cm ON cm.cm_uuid = mp.mp_membership_uuid
|
|
84
|
+
JOIN \`${AUTHZ}\`.\`scope_closure\` sc ON sc.descendant_uuid = cm.cm_company_uuid
|
|
85
|
+
JOIN \`${APP}\`.\`companies\` top ON top.c_uuid = sc.ancestor_uuid AND top.c_parent_uuid IS NULL
|
|
86
|
+
WHERE mp.mp_role_uuid = r.r_uuid)
|
|
87
|
+
WHERE r.${COLUMN} IS NULL AND (${tenantsOfRole}) = 1`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function down(ctx) {
|
|
91
|
+
const { query } = ctx;
|
|
92
|
+
const APP = frameworkSchemasForMigration(ctx, process.env).app;
|
|
93
|
+
await query(`ALTER TABLE \`${APP}\`.\`roles\` DROP FOREIGN KEY IF EXISTS ${FK}`);
|
|
94
|
+
await query(`DROP INDEX IF EXISTS ${INDEX} ON \`${APP}\`.\`roles\``);
|
|
95
|
+
await query(`ALTER TABLE \`${APP}\`.\`roles\` DROP COLUMN IF EXISTS ${COLUMN}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { up, down };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astram/identity",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Astram framework — the framework-owned identity + tenancy model. Hosts the identity @ObjectType classes (user, company, membership, posting), framework-authored identity migrations, and the identity resolver. Top of the dependency graph: depends on @astram/engine (for @ObjectType/@Field decorators) + @astram/guard / @astram/core (for the auth ports).",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"mariadb": "^3.5.4",
|
|
39
|
-
"@astram/core": "1.
|
|
40
|
-
"@astram/guard": "0.
|
|
41
|
-
"@astram/engine": "0.
|
|
39
|
+
"@astram/core": "1.9.0",
|
|
40
|
+
"@astram/guard": "0.18.0",
|
|
41
|
+
"@astram/engine": "0.10.1"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"@nestjs/common": "^11",
|