@astram/identity 0.20.0 → 0.22.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/access-change-notifier.d.ts +109 -0
- package/dist/features/authz/access-change-notifier.d.ts.map +1 -0
- package/dist/features/authz/access-change-notifier.js +421 -0
- package/dist/features/authz/grant-admin.service.d.ts +27 -1
- package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
- package/dist/features/authz/grant-admin.service.js +52 -2
- package/dist/features/identity-feature.module.d.ts.map +1 -1
- package/dist/features/identity-feature.module.js +26 -1
- package/dist/features/membership/membership.service.d.ts +19 -2
- package/dist/features/membership/membership.service.d.ts.map +1 -1
- package/dist/features/membership/membership.service.js +57 -5
- package/dist/features/membership/posting.service.d.ts +12 -1
- package/dist/features/membership/posting.service.d.ts.map +1 -1
- package/dist/features/membership/posting.service.js +39 -2
- package/dist/features/permission-announcement/permission-announcement.controller.d.ts +22 -0
- package/dist/features/permission-announcement/permission-announcement.controller.d.ts.map +1 -0
- package/dist/features/permission-announcement/permission-announcement.controller.js +130 -0
- package/dist/features/permission-announcement/permission-announcement.service.d.ts +143 -0
- package/dist/features/permission-announcement/permission-announcement.service.d.ts.map +1 -0
- package/dist/features/permission-announcement/permission-announcement.service.js +389 -0
- package/migrations/1710000000109-CreatePermissionAnnouncementDismissal.cjs +46 -0
- package/package.json +4 -4
|
@@ -44,6 +44,7 @@ class GrantAdminService {
|
|
|
44
44
|
ceiling;
|
|
45
45
|
audit;
|
|
46
46
|
schemas;
|
|
47
|
+
accessNotifier;
|
|
47
48
|
bundles;
|
|
48
49
|
/**
|
|
49
50
|
* @param ceiling The ADR-0029 self-escalation ceiling ("only grant what you hold") plus the
|
|
@@ -55,12 +56,19 @@ class GrantAdminService {
|
|
|
55
56
|
* connection, fail-closed. The identity module ALWAYS wires the engine `MariaDbAuditSink`.
|
|
56
57
|
* @param schemas Resolved framework names (absent ⇒ resolved from the namespace, once, here).
|
|
57
58
|
*/
|
|
58
|
-
constructor(pool, bumper, bundleStore, ceiling, audit, schemas = (0, schema_js_1.frameworkSchemasFromEnv)()
|
|
59
|
+
constructor(pool, bumper, bundleStore, ceiling, audit, schemas = (0, schema_js_1.frameworkSchemasFromEnv)(),
|
|
60
|
+
/**
|
|
61
|
+
* G-027 step 3 (notifying-user SPEC §5B, §5C): personal access-change notices on the grant's
|
|
62
|
+
* own transaction. OPTIONAL (absent ⇒ no notices, no extra reads); the identity module
|
|
63
|
+
* always wires it.
|
|
64
|
+
*/
|
|
65
|
+
accessNotifier) {
|
|
59
66
|
this.pool = pool;
|
|
60
67
|
this.bumper = bumper;
|
|
61
68
|
this.ceiling = ceiling;
|
|
62
69
|
this.audit = audit;
|
|
63
70
|
this.schemas = schemas;
|
|
71
|
+
this.accessNotifier = accessNotifier;
|
|
64
72
|
this.bundles = bundleStore ?? new access_bundle_store_js_1.AccessBundleStore(pool, schemas);
|
|
65
73
|
}
|
|
66
74
|
/** All grantable exact keys (from the catalog) + the computed wildcard set. */
|
|
@@ -167,10 +175,17 @@ class GrantAdminService {
|
|
|
167
175
|
await this.ceiling.assertMayStripRoleFromServiceAccounts(actor, roleUuid, accounts.map((a) => a.sa_tenant_id), 'revoke');
|
|
168
176
|
}
|
|
169
177
|
}
|
|
178
|
+
// G-027 step 3 (SPEC §5B): the holders' effective keys before the write, on this
|
|
179
|
+
// connection, after the lock and every refusal (a refused change never reaches a notice).
|
|
180
|
+
const notice = await this.beginAccessNotice(conn, roleUuid, actor);
|
|
170
181
|
for (const key of keys)
|
|
171
182
|
await statement(conn, key);
|
|
172
183
|
const bumped = await this.bumper.bumpForRole(roleUuid, conn);
|
|
173
|
-
await this.
|
|
184
|
+
const noticeDetail = await this.finishAccessNotice(notice, actor);
|
|
185
|
+
await this.auditGrantChange(conn, action, roleUuid, keys, actor, bumped, {
|
|
186
|
+
...extra,
|
|
187
|
+
...noticeDetail,
|
|
188
|
+
});
|
|
174
189
|
await conn.commit();
|
|
175
190
|
// ADR-0035: the committed versions reach Redis now (gate 5 reads them there).
|
|
176
191
|
await this.bumper.publishCommitted(conn);
|
|
@@ -189,6 +204,36 @@ class GrantAdminService {
|
|
|
189
204
|
await conn.release();
|
|
190
205
|
}
|
|
191
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* G-027 step 3 (SPEC §5B, §5C): read the role's holders and their effective keys on the grant's
|
|
209
|
+
* own transaction `conn`, BEFORE the `role_permission` write. `null` (no reads at all) when no
|
|
210
|
+
* notifier is wired or it is disabled for this actor (guard `auth: false`, the org bootstrap).
|
|
211
|
+
* Never throws: a failed read is carried as an `AccessReadFailure`.
|
|
212
|
+
*/
|
|
213
|
+
async beginAccessNotice(conn, roleUuid, actor) {
|
|
214
|
+
const notifier = this.accessNotifier;
|
|
215
|
+
if (!notifier || !notifier.enabledFor(actor))
|
|
216
|
+
return null;
|
|
217
|
+
const query = (sql, params) => conn.query(sql, params);
|
|
218
|
+
const holders = await notifier.holdersOfRole(query, roleUuid);
|
|
219
|
+
const before = await notifier.snapshot(query, holders);
|
|
220
|
+
return { notifier, query, holders, before };
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* After the write and the bump: the after-snapshot and the notices, on the same connection (a
|
|
224
|
+
* rollback removes them with the grant). Returns the audit detail addition: `noticesDropped: n`
|
|
225
|
+
* when n > 0, `'unknown'` when a holder or snapshot read failed, else nothing (byte-identical).
|
|
226
|
+
*/
|
|
227
|
+
async finishAccessNotice(run, actor) {
|
|
228
|
+
if (run === null)
|
|
229
|
+
return {};
|
|
230
|
+
const after = await run.notifier.snapshot(run.query, run.holders);
|
|
231
|
+
const { dropped } = await run.notifier.notify(run.query, actor, run.before, after);
|
|
232
|
+
const readFailed = !Array.isArray(run.holders) || !run.before.ok || !after.ok;
|
|
233
|
+
if (readFailed)
|
|
234
|
+
return { noticesDropped: 'unknown' };
|
|
235
|
+
return dropped > 0 ? { noticesDropped: dropped } : {};
|
|
236
|
+
}
|
|
192
237
|
/**
|
|
193
238
|
* Write the grant-administration audit row(s) on the transaction connection (D6). One row per
|
|
194
239
|
* distinct tenant among the actor's tenant and every tenant the change bumped, so each affected
|
|
@@ -312,6 +357,9 @@ class GrantAdminService {
|
|
|
312
357
|
if (this.ceiling && actor.isRoot !== true) {
|
|
313
358
|
await this.ceiling.assertMayStripRoleFromServiceAccounts(actor, roleUuid, accounts.map((a) => a.sa_tenant_id), 'delete');
|
|
314
359
|
}
|
|
360
|
+
// G-027 step 3 (SPEC §5C): holders + their keys before the grants go. Holders come from
|
|
361
|
+
// committed postings (the caller's uncommitted role delete is not visible here).
|
|
362
|
+
const notice = await this.beginAccessNotice(conn, roleUuid, actor);
|
|
315
363
|
const keys = (await conn.query(`SELECT rp_perm_key FROM \`${this.schemas.authz}\`.\`role_permission\` WHERE rp_role_uuid = ? ORDER BY rp_perm_key`, [roleUuid])).map((r) => r.rp_perm_key);
|
|
316
364
|
const bumped = await this.bumper.bumpForRole(roleUuid, conn);
|
|
317
365
|
await conn.query(`DELETE FROM \`${this.schemas.authz}\`.\`role_permission\` WHERE rp_role_uuid = ?`, [roleUuid]);
|
|
@@ -321,9 +369,11 @@ class GrantAdminService {
|
|
|
321
369
|
const previous = a.sa_updated_at_ms === null ? 0 : Number(a.sa_updated_at_ms);
|
|
322
370
|
await conn.query(`UPDATE \`${this.schemas.app}\`.\`service_account\` SET sa_role_ids = ?, sa_updated_at_ms = ? WHERE sa_id = ?`, [JSON.stringify(remaining), Math.max(nowMs, previous + 1), a.sa_id]);
|
|
323
371
|
}
|
|
372
|
+
const noticeDetail = await this.finishAccessNotice(notice, actor);
|
|
324
373
|
await this.auditGrantChange(conn, 'role.permission_revoked', roleUuid, keys, actor, bumped, {
|
|
325
374
|
roleDeleted: true,
|
|
326
375
|
serviceAccountsUpdated: accounts.length,
|
|
376
|
+
...noticeDetail,
|
|
327
377
|
});
|
|
328
378
|
await conn.commit();
|
|
329
379
|
await this.bumper.publishCommitted(conn);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"identity-feature.module.d.ts","sourceRoot":"","sources":["../../src/features/identity-feature.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpE,OAAO,EAGL,KAAK,gBAAgB,EAEtB,MAAM,cAAc,CAAC;AACtB,OAAO,EAaL,KAAK,cAAc,EAGnB,KAAK,eAAe,EACrB,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"identity-feature.module.d.ts","sourceRoot":"","sources":["../../src/features/identity-feature.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpE,OAAO,EAGL,KAAK,gBAAgB,EAEtB,MAAM,cAAc,CAAC;AACtB,OAAO,EAaL,KAAK,cAAc,EAGnB,KAAK,eAAe,EACrB,MAAM,gBAAgB,CAAC;AA4DxB,OAAO,EAAoB,KAAK,aAAa,EAAE,MAAM,yBAAyB,CAAC;AA6C/E,OAAO,EAAiB,KAAK,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAkElF;;;;;;GAMG;AACH,wBAAgB,0CAA0C,CAAC,SAAS,EAAE,OAAO,GAAG,IAAI,CAUnF;AAKD;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,gBAAgB,EAC1B,KAAK,EAAE,gBAAgB,GAAG,IAAI,GAAG,SAAS,GACzC,IAAI,CAwBN;AAKD;;;;;;GAMG;AACH,wBAAgB,6BAA6B,CAC3C,gBAAgB,EAAE,MAAM,EACxB,WAAW,EAAE;IAAE,QAAQ,CAAC,EAAE,CAAC,EAAE;QAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,GAC/E,IAAI,CAQN;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,SAAS,eAAe,EAAE,EAClC,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,SAAS,MAAM,EAAE,GAAG,SAAS,GAChE,gBAAgB,EAAE,CAapB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,gBAAgB,EACzB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,cAAc,CAyBhB;AA2BD,qBACa,qBAAqB;IAChC;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,MAAM,CAAC,OAAO,CAAC,EACb,OAAO,EACP,YAAiB,EACjB,OAAO,EAAE,YAAY,EACrB,MAAM,GACP,GAAE;QACD,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QAC5B,YAAY,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;QAC1C,OAAO,CAAC,EAAE,gBAAgB,CAAC;QAC3B,MAAM,CAAC,EAAE,aAAa,CAAC;KACnB,GAAG,aAAa;CA6rBvB"}
|
|
@@ -54,6 +54,9 @@ const access_bundle_store_js_1 = require("./authz/access-bundle-store.js");
|
|
|
54
54
|
const grant_admin_service_js_1 = require("./authz/grant-admin.service.js");
|
|
55
55
|
const grant_admin_controller_js_1 = require("./authz/grant-admin.controller.js");
|
|
56
56
|
const grant_ceiling_js_1 = require("./authz/grant-ceiling.js");
|
|
57
|
+
const permission_announcement_service_js_1 = require("./permission-announcement/permission-announcement.service.js");
|
|
58
|
+
const permission_announcement_controller_js_1 = require("./permission-announcement/permission-announcement.controller.js");
|
|
59
|
+
const access_change_notifier_js_1 = require("./authz/access-change-notifier.js");
|
|
57
60
|
const policy_service_js_1 = require("./policy/policy.service.js");
|
|
58
61
|
const security_policy_service_js_1 = require("./security-policy/security-policy.service.js");
|
|
59
62
|
const security_policy_controller_js_1 = require("./security-policy/security-policy.controller.js");
|
|
@@ -301,6 +304,10 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
301
304
|
// registered for the cross-tenant writer and the policy author too.
|
|
302
305
|
const recordAuthorization = (0, record_authorization_js_1.buildRecordAuthorizationInputs)(authzPool, extraObjects, schemas);
|
|
303
306
|
const ceilingRegistry = recordAuthorization.registry;
|
|
307
|
+
// G-027 step 3: personal access-change notices, keys expanded against the SAME registry the
|
|
308
|
+
// request path grants from. Attached to guard's NotificationService by the provider below
|
|
309
|
+
// (absent with `auth: false` ⇒ a no-op notifier).
|
|
310
|
+
const accessChangeNotifier = new access_change_notifier_js_1.AccessChangeNotifier(ceilingRegistry, schemas);
|
|
304
311
|
const bumper = new permission_version_bumper_js_1.PermissionVersionBumper(undefined, schemas);
|
|
305
312
|
const postingVersionBump = {
|
|
306
313
|
bumpTenant: (tenantUuid) => bumper.bumpForTenant(tenantUuid, authzPool),
|
|
@@ -331,7 +338,11 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
331
338
|
const auditSinkConfig = { schema: schemas.audit };
|
|
332
339
|
// Grant administration is audited on the grant's own transaction (D6, fail-closed).
|
|
333
340
|
const grantAdminAuditSink = new engine_1.MariaDbAuditSink(auditSinkConfig);
|
|
334
|
-
const grantAdminService = new grant_admin_service_js_1.GrantAdminService(authzPool, bumper, accessBundleStore, grantCeiling, grantAdminAuditSink, schemas);
|
|
341
|
+
const grantAdminService = new grant_admin_service_js_1.GrantAdminService(authzPool, bumper, accessBundleStore, grantCeiling, grantAdminAuditSink, schemas, accessChangeNotifier);
|
|
342
|
+
// G-027 step 4: permission-announcement admin tasks, over the SAME authz pool, ceiling and
|
|
343
|
+
// registry as grant administration; dismissals audited on their own transaction through the
|
|
344
|
+
// grant-admin sink (stateless; same fail-closed in-transaction contract).
|
|
345
|
+
const permissionAnnouncementService = new permission_announcement_service_js_1.PermissionAnnouncementService(authzPool, grantCeiling, ceilingRegistry, grantAdminAuditSink, schemas);
|
|
335
346
|
// Scope closure (Task 1.2 of the scoped-hierarchy program): the reconciler + node
|
|
336
347
|
// move/delete maintenance, over the SAME shared authzPool (it reaches BOTH shield_app —
|
|
337
348
|
// companies — and shield_authz — scope_closure). Reuses the SAME `bumper` instance so a
|
|
@@ -661,6 +672,9 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
661
672
|
// Mandatory-2FA policies (auth capabilities R2.9) — `security-policies/*` collides with
|
|
662
673
|
// nothing. Gated on `security_policy:<action>`. Appended last.
|
|
663
674
|
security_policy_controller_js_1.SecurityPolicyController,
|
|
675
|
+
// Permission announcements (G-027 step 4) — `permission-announcements/*` collides with no
|
|
676
|
+
// `/:uuid` route under `identity`; its `:companyUuid/:key/{dismiss,reopen}` are static-suffixed.
|
|
677
|
+
permission_announcement_controller_js_1.PermissionAnnouncementController,
|
|
664
678
|
],
|
|
665
679
|
providers: [
|
|
666
680
|
company_service_js_1.CompanyService,
|
|
@@ -671,6 +685,16 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
671
685
|
org_catalog_service_js_1.OrgCatalogService,
|
|
672
686
|
record_resolver_js_1.RecordResolverService,
|
|
673
687
|
{ provide: posting_version_bump_js_1.POSTING_VERSION_BUMP, useValue: postingVersionBump },
|
|
688
|
+
// G-027 step 3: the one access-change notifier (also held by GrantAdminService), attached
|
|
689
|
+
// here to guard's NotificationService when guard provides it (`auth: true`), else none.
|
|
690
|
+
{
|
|
691
|
+
provide: access_change_notifier_js_1.ACCESS_CHANGE_NOTIFIER,
|
|
692
|
+
useFactory: (service) => {
|
|
693
|
+
accessChangeNotifier.attach(service ?? null);
|
|
694
|
+
return accessChangeNotifier;
|
|
695
|
+
},
|
|
696
|
+
inject: [{ token: guard_1.SHIELD_NOTIFICATIONS, optional: true }],
|
|
697
|
+
},
|
|
674
698
|
// Mandatory-2FA policies (R2.6, R2.9): the service, and its permission-version bump over
|
|
675
699
|
// the ONE shared bumper (bumps on the write's transaction, published after the commit).
|
|
676
700
|
security_policy_service_js_1.SecurityPolicyService,
|
|
@@ -749,6 +773,7 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
|
|
|
749
773
|
// Grant-admin stack — all over the ONE shared authzPool.
|
|
750
774
|
{ provide: access_bundle_store_js_1.AccessBundleStore, useValue: accessBundleStore },
|
|
751
775
|
{ provide: grant_admin_service_js_1.GrantAdminService, useValue: grantAdminService },
|
|
776
|
+
{ provide: permission_announcement_service_js_1.PermissionAnnouncementService, useValue: permissionAnnouncementService },
|
|
752
777
|
// The ONE tenant-boundary authority (ADR-0029 ceiling + block A: role assignment, user
|
|
753
778
|
// dominance, role binding) — injected by RoleService, PostingService and UserService.
|
|
754
779
|
{ provide: grant_ceiling_js_1.GrantCeiling, useValue: grantCeiling },
|
|
@@ -20,11 +20,22 @@ import { RecordsService, type RecordId, type RecordView } from '@astram/engine';
|
|
|
20
20
|
import type { Principal } from '@astram/core';
|
|
21
21
|
import { type PostingVersionBump } from './posting-version-bump.js';
|
|
22
22
|
import { GrantCeiling } from '../authz/grant-ceiling.js';
|
|
23
|
+
import { type AccessChangeNotifier } from '../authz/access-change-notifier.js';
|
|
23
24
|
export declare class MembershipService {
|
|
24
25
|
private readonly records;
|
|
25
26
|
private readonly ceiling;
|
|
26
27
|
private readonly versionBump?;
|
|
27
|
-
|
|
28
|
+
private readonly accessNotifier?;
|
|
29
|
+
constructor(records: RecordsService, ceiling: GrantCeiling, versionBump?: (PostingVersionBump | null) | undefined, accessNotifier?: (AccessChangeNotifier | null) | undefined);
|
|
30
|
+
/**
|
|
31
|
+
* G-027 step 3 (SPEC §5E): the before-snapshot of `pairs` on the engine transaction's own
|
|
32
|
+
* connection (`transactionQueryOf`), so the after-snapshot sees the uncommitted membership
|
|
33
|
+
* writes and the notice commits or rolls back with them. `null` (no reads) when no notifier is
|
|
34
|
+
* wired or it is disabled for this actor (guard `auth: false`, the org bootstrap). Never throws.
|
|
35
|
+
*/
|
|
36
|
+
private beginAccessNotice;
|
|
37
|
+
/** After the last write: after-snapshot + notify (a drop is logged by the notifier only). */
|
|
38
|
+
private finishAccessNotice;
|
|
28
39
|
/**
|
|
29
40
|
* Read a membership through the subtree wall (404 when out of reach) and, unless the actor is
|
|
30
41
|
* acting on their OWN membership, require the actor to dominate its user (security review): a
|
|
@@ -48,7 +59,8 @@ export declare class MembershipService {
|
|
|
48
59
|
*/
|
|
49
60
|
addMembership(actor: Principal, userUuid: string, companyUuid: string): Promise<RecordId>;
|
|
50
61
|
/**
|
|
51
|
-
* Add an ACTIVE membership for `(userUuid, companyUuid)` in one create
|
|
62
|
+
* Add an ACTIVE membership for `(userUuid, companyUuid)` in one create, inside one engine
|
|
63
|
+
* transaction with the user's access notice (G-009 R4; G-027 step 3): a brand-new
|
|
52
64
|
* membership has no other active card to displace, so no create-then-activate pair. A
|
|
53
65
|
* membership for that pair already existing is an `IdentityConflictError`, from the pre-check
|
|
54
66
|
* and from the `(cm_user_uuid, cm_company_uuid)` unique index (the pre-check read is SQL-scoped
|
|
@@ -88,6 +100,11 @@ export declare class MembershipService {
|
|
|
88
100
|
/**
|
|
89
101
|
* Remove a membership card (its postings cascade at the DB via FK ON DELETE CASCADE). A peer's
|
|
90
102
|
* membership needs dominance over the peer (security review); your own does not.
|
|
103
|
+
*
|
|
104
|
+
* Intentionally sends NO access-change notice (notifying-user SPEC §11): the user can no longer
|
|
105
|
+
* open that company's inbox, and the notice store's recipient filter would skip them anyway.
|
|
106
|
+
* `updateMembership` (status only, no key change), user status changes and `deleteUser` are
|
|
107
|
+
* likewise not hooked.
|
|
91
108
|
*/
|
|
92
109
|
removeMembership(actor: Principal, membershipUuid: string): Promise<void>;
|
|
93
110
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"membership.service.d.ts","sourceRoot":"","sources":["../../../src/features/membership/membership.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,EAAE,cAAc,EAAE,KAAK,QAAQ,EAAE,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"membership.service.d.ts","sourceRoot":"","sources":["../../../src/features/membership/membership.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,EAAE,cAAc,EAAE,KAAK,QAAQ,EAAE,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEhF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAwB,KAAK,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAE1F,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAEL,KAAK,oBAAoB,EAI1B,MAAM,oCAAoC,CAAC;AAoB5C,qBACa,iBAAiB;IAE1B,OAAO,CAAC,QAAQ,CAAC,OAAO;IAGxB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAQxB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;IAK7B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC;gBAhBf,OAAO,EAAE,cAAc,EAGvB,OAAO,EAAE,YAAY,EAQrB,WAAW,CAAC,GAAE,kBAAkB,GAAG,IAAI,aAAA,EAKvC,cAAc,CAAC,GAAE,oBAAoB,GAAG,IAAI,aAAA;IAG/D;;;;;OAKG;YACW,iBAAiB;IAY/B,6FAA6F;YAC/E,kBAAkB;IAShC;;;;OAIG;YACW,WAAW;IAQzB;;;;;;OAMG;YACW,WAAW;IAYzB;;;;;;OAMG;IACG,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAkB/F;;;;;;;;OAQG;IACG,mBAAmB,CACvB,KAAK,EAAE,SAAS,EAChB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,QAAQ,CAAC;IAgCpB,kEAAkE;IAC5D,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,UAAU,EAAE,CAAC;IAKvE;;;;;;;;;;;;;;;;;OAiBG;IACG,SAAS,CAAC,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAyE1F;;;;OAIG;IACG,gBAAgB,CACpB,KAAK,EAAE,SAAS,EAChB,cAAc,EAAE,MAAM,EACtB,KAAK,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GACzB,OAAO,CAAC,IAAI,CAAC;IA4BhB;;;;;;;;OAQG;IACG,gBAAgB,CAAC,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAYhF"}
|
|
@@ -33,9 +33,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
33
33
|
exports.MembershipService = void 0;
|
|
34
34
|
const common_1 = require("@nestjs/common");
|
|
35
35
|
const engine_1 = require("@astram/engine");
|
|
36
|
+
const internal_1 = require("@astram/engine/internal");
|
|
36
37
|
const posting_version_bump_js_1 = require("./posting-version-bump.js");
|
|
37
38
|
const identity_errors_js_1 = require("../shared/identity-errors.js");
|
|
38
39
|
const grant_ceiling_js_1 = require("../authz/grant-ceiling.js");
|
|
40
|
+
const access_change_notifier_js_1 = require("../authz/access-change-notifier.js");
|
|
39
41
|
/** The only status the active membership may have (case-sensitive, as the readers' predicate). */
|
|
40
42
|
const ACTIVE_STATUS = 'active';
|
|
41
43
|
/** A driver duplicate-key error (errno 1062), recognised by errno/code only, never its message. */
|
|
@@ -49,13 +51,36 @@ let MembershipService = class MembershipService {
|
|
|
49
51
|
records;
|
|
50
52
|
ceiling;
|
|
51
53
|
versionBump;
|
|
54
|
+
accessNotifier;
|
|
52
55
|
constructor(records,
|
|
53
56
|
// Dominance over the member for destructive ops (security review): REQUIRED, so a missing
|
|
54
57
|
// ceiling fails DI at boot instead of silently skipping the check.
|
|
55
|
-
ceiling, versionBump) {
|
|
58
|
+
ceiling, versionBump, accessNotifier) {
|
|
56
59
|
this.records = records;
|
|
57
60
|
this.ceiling = ceiling;
|
|
58
61
|
this.versionBump = versionBump;
|
|
62
|
+
this.accessNotifier = accessNotifier;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* G-027 step 3 (SPEC §5E): the before-snapshot of `pairs` on the engine transaction's own
|
|
66
|
+
* connection (`transactionQueryOf`), so the after-snapshot sees the uncommitted membership
|
|
67
|
+
* writes and the notice commits or rolls back with them. `null` (no reads) when no notifier is
|
|
68
|
+
* wired or it is disabled for this actor (guard `auth: false`, the org bootstrap). Never throws.
|
|
69
|
+
*/
|
|
70
|
+
async beginAccessNotice(tx, actor, pairs) {
|
|
71
|
+
const notifier = this.accessNotifier;
|
|
72
|
+
if (!notifier || !notifier.enabledFor(actor))
|
|
73
|
+
return null;
|
|
74
|
+
const query = (0, internal_1.transactionQueryOf)(tx);
|
|
75
|
+
const before = await notifier.snapshot(query, pairs);
|
|
76
|
+
return { notifier, query, pairs, before };
|
|
77
|
+
}
|
|
78
|
+
/** After the last write: after-snapshot + notify (a drop is logged by the notifier only). */
|
|
79
|
+
async finishAccessNotice(run, actor) {
|
|
80
|
+
if (run === null)
|
|
81
|
+
return;
|
|
82
|
+
const after = await run.notifier.snapshot(run.query, run.pairs);
|
|
83
|
+
await run.notifier.notify(run.query, actor, run.before, after);
|
|
59
84
|
}
|
|
60
85
|
/**
|
|
61
86
|
* Read a membership through the subtree wall (404 when out of reach) and, unless the actor is
|
|
@@ -117,7 +142,8 @@ let MembershipService = class MembershipService {
|
|
|
117
142
|
});
|
|
118
143
|
}
|
|
119
144
|
/**
|
|
120
|
-
* Add an ACTIVE membership for `(userUuid, companyUuid)` in one create
|
|
145
|
+
* Add an ACTIVE membership for `(userUuid, companyUuid)` in one create, inside one engine
|
|
146
|
+
* transaction with the user's access notice (G-009 R4; G-027 step 3): a brand-new
|
|
121
147
|
* membership has no other active card to displace, so no create-then-activate pair. A
|
|
122
148
|
* membership for that pair already existing is an `IdentityConflictError`, from the pre-check
|
|
123
149
|
* and from the `(cm_user_uuid, cm_company_uuid)` unique index (the pre-check read is SQL-scoped
|
|
@@ -136,8 +162,16 @@ let MembershipService = class MembershipService {
|
|
|
136
162
|
}
|
|
137
163
|
let id;
|
|
138
164
|
try {
|
|
139
|
-
|
|
140
|
-
|
|
165
|
+
// G-027 step 3 (SPEC §5E): the create and the user's access notice in one transaction.
|
|
166
|
+
id = await this.records.transaction(async (tx) => {
|
|
167
|
+
const notice = await this.beginAccessNotice(tx, actor, [
|
|
168
|
+
{ userId: userUuid, tenantId: companyUuid },
|
|
169
|
+
]);
|
|
170
|
+
const created = await tx.create('company_membership', {
|
|
171
|
+
fields: { userUuid, companyUuid, isActive: true, status: 'active' },
|
|
172
|
+
});
|
|
173
|
+
await this.finishAccessNotice(notice, actor);
|
|
174
|
+
return created;
|
|
141
175
|
});
|
|
142
176
|
}
|
|
143
177
|
catch (err) {
|
|
@@ -203,6 +237,16 @@ let MembershipService = class MembershipService {
|
|
|
203
237
|
if (current.fields?.status !== ACTIVE_STATUS) {
|
|
204
238
|
throw new identity_errors_js_1.IdentityConflictError('Only a membership with status active can be made the active membership.');
|
|
205
239
|
}
|
|
240
|
+
// G-027 step 3 (SPEC §5E): the user in every company of their memberships (the target's
|
|
241
|
+
// and each one this flip deactivates), before the first write. One notice per company
|
|
242
|
+
// whose key set changed.
|
|
243
|
+
const companies = new Set();
|
|
244
|
+
for (const m of rows) {
|
|
245
|
+
const c = m.fields?.companyUuid;
|
|
246
|
+
if (typeof c === 'string' && c.length > 0)
|
|
247
|
+
companies.add(c);
|
|
248
|
+
}
|
|
249
|
+
const notice = await this.beginAccessNotice(tx, actor, [...companies].map((tenantId) => ({ userId: userUuid, tenantId })));
|
|
206
250
|
// 1) TARGET FIRST (defence in depth, see the method comment).
|
|
207
251
|
await tx.update('company_membership', target.id, { fields: { isActive: true } });
|
|
208
252
|
// 2) Deactivate every OTHER membership (skip the target we just activated).
|
|
@@ -212,6 +256,7 @@ let MembershipService = class MembershipService {
|
|
|
212
256
|
await tx.update('company_membership', m.id, { fields: { isActive: false } });
|
|
213
257
|
deactivated.push(m);
|
|
214
258
|
}
|
|
259
|
+
await this.finishAccessNotice(notice, actor);
|
|
215
260
|
});
|
|
216
261
|
// ADR-0011 STALENESS FIX: activation ADDS the target company's role grants and each
|
|
217
262
|
// deactivation REMOVES that company's grants — a coarse-grant change in BOTH directions.
|
|
@@ -262,6 +307,11 @@ let MembershipService = class MembershipService {
|
|
|
262
307
|
/**
|
|
263
308
|
* Remove a membership card (its postings cascade at the DB via FK ON DELETE CASCADE). A peer's
|
|
264
309
|
* membership needs dominance over the peer (security review); your own does not.
|
|
310
|
+
*
|
|
311
|
+
* Intentionally sends NO access-change notice (notifying-user SPEC §11): the user can no longer
|
|
312
|
+
* open that company's inbox, and the notice store's recipient filter would skip them anyway.
|
|
313
|
+
* `updateMembership` (status only, no key change), user status changes and `deleteUser` are
|
|
314
|
+
* likewise not hooked.
|
|
265
315
|
*/
|
|
266
316
|
async removeMembership(actor, membershipUuid) {
|
|
267
317
|
// ADR-0011 STALENESS FIX: resolve the membership's tenant BEFORE the delete (it's
|
|
@@ -281,6 +331,8 @@ exports.MembershipService = MembershipService = __decorate([
|
|
|
281
331
|
(0, common_1.Injectable)(),
|
|
282
332
|
__param(2, (0, common_1.Optional)()),
|
|
283
333
|
__param(2, (0, common_1.Inject)(posting_version_bump_js_1.POSTING_VERSION_BUMP)),
|
|
334
|
+
__param(3, (0, common_1.Optional)()),
|
|
335
|
+
__param(3, (0, common_1.Inject)(access_change_notifier_js_1.ACCESS_CHANGE_NOTIFIER)),
|
|
284
336
|
__metadata("design:paramtypes", [engine_1.RecordsService,
|
|
285
|
-
grant_ceiling_js_1.GrantCeiling, Object])
|
|
337
|
+
grant_ceiling_js_1.GrantCeiling, Object, Object])
|
|
286
338
|
], MembershipService);
|
|
@@ -27,6 +27,7 @@ import { RecordsService, type RecordView } from '@astram/engine';
|
|
|
27
27
|
import type { Principal } from '@astram/core';
|
|
28
28
|
import { type PostingVersionBump } from './posting-version-bump.js';
|
|
29
29
|
import { GrantCeiling } from '../authz/grant-ceiling.js';
|
|
30
|
+
import { type AccessChangeNotifier } from '../authz/access-change-notifier.js';
|
|
30
31
|
/**
|
|
31
32
|
* The shape of one desired posting row submitted by the editor. The effective-window
|
|
32
33
|
* bounds arrive from the client as a bare `YYYY-MM-DD` date string (native `<input
|
|
@@ -44,7 +45,8 @@ export declare class PostingService {
|
|
|
44
45
|
private readonly records;
|
|
45
46
|
private readonly ceiling;
|
|
46
47
|
private readonly versionBump?;
|
|
47
|
-
|
|
48
|
+
private readonly accessNotifier?;
|
|
49
|
+
constructor(records: RecordsService, ceiling: GrantCeiling, versionBump?: (PostingVersionBump | null) | undefined, accessNotifier?: (AccessChangeNotifier | null) | undefined);
|
|
48
50
|
/** List a membership's postings (tenant-scoped by the pipe). */
|
|
49
51
|
listPostings(membershipUuid: string): Promise<readonly RecordView[]>;
|
|
50
52
|
/**
|
|
@@ -64,6 +66,15 @@ export declare class PostingService {
|
|
|
64
66
|
addPostings(actor: Principal, membershipUuid: string, toAdd: readonly PostingInput[], opts?: {
|
|
65
67
|
operationRoleIds?: ReadonlySet<string> | readonly string[];
|
|
66
68
|
}): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* G-027 step 3 (SPEC §5D): the before-snapshot of (member, company) on the engine transaction's
|
|
71
|
+
* own connection (`transactionQueryOf`), so the after-snapshot sees the uncommitted posting
|
|
72
|
+
* writes and the notice commits or rolls back with them. `null` (no reads) when no notifier is
|
|
73
|
+
* wired or it is disabled for this actor (guard `auth: false`, the org bootstrap). Never throws.
|
|
74
|
+
*/
|
|
75
|
+
private beginAccessNotice;
|
|
76
|
+
/** After the last write: after-snapshot + notify (a drop is logged by the notifier only). */
|
|
77
|
+
private finishAccessNotice;
|
|
67
78
|
private assertEveryRole;
|
|
68
79
|
/** The membership through the subtree read wall (404 when out of reach) and its company. */
|
|
69
80
|
private reachMembership;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"posting.service.d.ts","sourceRoot":"","sources":["../../../src/features/membership/posting.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAGH,OAAO,EAAE,cAAc,EAAE,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"posting.service.d.ts","sourceRoot":"","sources":["../../../src/features/membership/posting.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAGH,OAAO,EAAE,cAAc,EAAE,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAwB,KAAK,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC1F,OAAO,EAAE,YAAY,EAA6B,MAAM,2BAA2B,CAAC;AACpF,OAAO,EAEL,KAAK,oBAAoB,EAI1B,MAAM,oCAAoC,CAAC;AAO5C;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACvC,cAAc,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;CACzC;AAkFD,qBACa,cAAc;IAEvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAGxB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAKxB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;IAK7B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC;gBAbf,OAAO,EAAE,cAAc,EAGvB,OAAO,EAAE,YAAY,EAKrB,WAAW,CAAC,GAAE,kBAAkB,GAAG,IAAI,aAAA,EAKvC,cAAc,CAAC,GAAE,oBAAoB,GAAG,IAAI,aAAA;IAG/D,gEAAgE;IAC1D,YAAY,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,UAAU,EAAE,CAAC;IAK1E;;;;;OAKG;IACG,eAAe,CACnB,KAAK,EAAE,SAAS,EAChB,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,SAAS,YAAY,EAAE,GAC/B,OAAO,CAAC,IAAI,CAAC;IA6EhB;;;;;;OAMG;IACG,WAAW,CACf,KAAK,EAAE,SAAS,EAChB,cAAc,EAAE,MAAM,EACtB,KAAK,EAAE,SAAS,YAAY,EAAE,EAC9B,IAAI,CAAC,EAAE;QAAE,gBAAgB,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,SAAS,MAAM,EAAE,CAAA;KAAE,GACpE,OAAO,CAAC,IAAI,CAAC;IAiDhB;;;;;OAKG;YACW,iBAAiB;IAiB/B,6FAA6F;YAC/E,kBAAkB;IAShC,OAAO,CAAC,eAAe;IAQvB,4FAA4F;YAC9E,eAAe;IAY7B,iGAAiG;YACnF,YAAY;IAuB1B;;;;OAIG;YACW,cAAc;IAa5B;;;;;;;;;;;;OAYG;YACW,YAAY;YA+BZ,UAAU;CAkBzB"}
|
|
@@ -40,8 +40,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
40
40
|
exports.PostingService = void 0;
|
|
41
41
|
const common_1 = require("@nestjs/common");
|
|
42
42
|
const engine_1 = require("@astram/engine");
|
|
43
|
+
const internal_1 = require("@astram/engine/internal");
|
|
43
44
|
const posting_version_bump_js_1 = require("./posting-version-bump.js");
|
|
44
45
|
const grant_ceiling_js_1 = require("../authz/grant-ceiling.js");
|
|
46
|
+
const access_change_notifier_js_1 = require("../authz/access-change-notifier.js");
|
|
45
47
|
const identity_errors_js_1 = require("../shared/identity-errors.js");
|
|
46
48
|
/** The field keys whose values are effective-window epoch-ms bounds (normalized on write). */
|
|
47
49
|
const WINDOW_KEYS = new Set(['effectiveFrom', 'effectiveUntil']);
|
|
@@ -115,13 +117,15 @@ let PostingService = class PostingService {
|
|
|
115
117
|
records;
|
|
116
118
|
ceiling;
|
|
117
119
|
versionBump;
|
|
120
|
+
accessNotifier;
|
|
118
121
|
constructor(records,
|
|
119
122
|
// The role-assignment ceiling + binding rule (security hardening #11). REQUIRED: a missing
|
|
120
123
|
// ceiling must fail DI at boot, never silently skip the check.
|
|
121
|
-
ceiling, versionBump) {
|
|
124
|
+
ceiling, versionBump, accessNotifier) {
|
|
122
125
|
this.records = records;
|
|
123
126
|
this.ceiling = ceiling;
|
|
124
127
|
this.versionBump = versionBump;
|
|
128
|
+
this.accessNotifier = accessNotifier;
|
|
125
129
|
}
|
|
126
130
|
/** List a membership's postings (tenant-scoped by the pipe). */
|
|
127
131
|
async listPostings(membershipUuid) {
|
|
@@ -170,12 +174,15 @@ let PostingService = class PostingService {
|
|
|
170
174
|
// delete, while we waited is refused before the INSERT (rolled back).
|
|
171
175
|
await this.lockAndRecheck(tx, actor, tenantUuid, addedRoles, undefined);
|
|
172
176
|
await this.lockOrgUnits(tx, tenantUuid, added);
|
|
177
|
+
// G-027 step 3 (SPEC §5D): the member's keys in this company before the first write.
|
|
178
|
+
const notice = await this.beginAccessNotice(tx, actor, memberUuid, tenantUuid);
|
|
173
179
|
for (const d of added) {
|
|
174
180
|
await tx.create('membership_posting', { fields: { ...stripEmpty(d), membershipUuid } });
|
|
175
181
|
}
|
|
176
182
|
for (const r of removed) {
|
|
177
183
|
await tx.delete('membership_posting', r.id);
|
|
178
184
|
}
|
|
185
|
+
await this.finishAccessNotice(notice, actor);
|
|
179
186
|
});
|
|
180
187
|
await this.bumpTenant(tenantUuid);
|
|
181
188
|
}
|
|
@@ -209,15 +216,43 @@ let PostingService = class PostingService {
|
|
|
209
216
|
if (added.length === 0)
|
|
210
217
|
return; // nothing missing: no write, no bump
|
|
211
218
|
const addedRoles = await this.assertMayAdd(actor, tenantUuid, added, ceilingOpts);
|
|
219
|
+
const memberUuid = typeof membership.fields?.userUuid === 'string' ? membership.fields.userUuid : '';
|
|
212
220
|
await this.records.transaction(async (tx) => {
|
|
213
221
|
await this.lockAndRecheck(tx, actor, tenantUuid, addedRoles, ceilingOpts);
|
|
214
222
|
await this.lockOrgUnits(tx, tenantUuid, added);
|
|
223
|
+
// G-027 step 3 (SPEC §5D): the member's keys in this company before the first write.
|
|
224
|
+
const notice = await this.beginAccessNotice(tx, actor, memberUuid, tenantUuid);
|
|
215
225
|
for (const d of added) {
|
|
216
226
|
await tx.create('membership_posting', { fields: { ...stripEmpty(d), membershipUuid } });
|
|
217
227
|
}
|
|
228
|
+
await this.finishAccessNotice(notice, actor);
|
|
218
229
|
});
|
|
219
230
|
await this.bumpTenant(tenantUuid);
|
|
220
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* G-027 step 3 (SPEC §5D): the before-snapshot of (member, company) on the engine transaction's
|
|
234
|
+
* own connection (`transactionQueryOf`), so the after-snapshot sees the uncommitted posting
|
|
235
|
+
* writes and the notice commits or rolls back with them. `null` (no reads) when no notifier is
|
|
236
|
+
* wired or it is disabled for this actor (guard `auth: false`, the org bootstrap). Never throws.
|
|
237
|
+
*/
|
|
238
|
+
async beginAccessNotice(tx, actor, memberUuid, tenantUuid) {
|
|
239
|
+
const notifier = this.accessNotifier;
|
|
240
|
+
if (!notifier || !notifier.enabledFor(actor))
|
|
241
|
+
return null;
|
|
242
|
+
const query = (0, internal_1.transactionQueryOf)(tx);
|
|
243
|
+
const pairs = memberUuid !== '' && tenantUuid !== null
|
|
244
|
+
? [{ userId: memberUuid, tenantId: tenantUuid }]
|
|
245
|
+
: [];
|
|
246
|
+
const before = await notifier.snapshot(query, pairs);
|
|
247
|
+
return { notifier, query, pairs, before };
|
|
248
|
+
}
|
|
249
|
+
/** After the last write: after-snapshot + notify (a drop is logged by the notifier only). */
|
|
250
|
+
async finishAccessNotice(run, actor) {
|
|
251
|
+
if (run === null)
|
|
252
|
+
return;
|
|
253
|
+
const after = await run.notifier.snapshot(run.query, run.pairs);
|
|
254
|
+
await run.notifier.notify(run.query, actor, run.before, after);
|
|
255
|
+
}
|
|
221
256
|
assertEveryRole(postings) {
|
|
222
257
|
for (const d of postings) {
|
|
223
258
|
if (typeof d?.roleUuid !== 'string' || d.roleUuid.length === 0) {
|
|
@@ -327,6 +362,8 @@ exports.PostingService = PostingService = __decorate([
|
|
|
327
362
|
(0, common_1.Injectable)(),
|
|
328
363
|
__param(2, (0, common_1.Optional)()),
|
|
329
364
|
__param(2, (0, common_1.Inject)(posting_version_bump_js_1.POSTING_VERSION_BUMP)),
|
|
365
|
+
__param(3, (0, common_1.Optional)()),
|
|
366
|
+
__param(3, (0, common_1.Inject)(access_change_notifier_js_1.ACCESS_CHANGE_NOTIFIER)),
|
|
330
367
|
__metadata("design:paramtypes", [engine_1.RecordsService,
|
|
331
|
-
grant_ceiling_js_1.GrantCeiling, Object])
|
|
368
|
+
grant_ceiling_js_1.GrantCeiling, Object, Object])
|
|
332
369
|
], PostingService);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Principal } from '@astram/core';
|
|
2
|
+
import { PermissionAnnouncementService, type PermissionAnnouncementQuery } from './permission-announcement.service.js';
|
|
3
|
+
/** The request shape after the enforcement guard attaches the resolved principal. */
|
|
4
|
+
interface AuthedRequest {
|
|
5
|
+
principal: Principal;
|
|
6
|
+
}
|
|
7
|
+
/** Validate the list query (spec §3, §13: bad `status`/`page`/`limit`/uuid ⇒ 400). */
|
|
8
|
+
export declare function parseAnnouncementQuery(query: Record<string, unknown> | undefined): PermissionAnnouncementQuery;
|
|
9
|
+
/** Validate a `:key` route param (spec §5.B), or 400 (the value is never echoed). */
|
|
10
|
+
export declare function parsePermissionKeyParam(value: unknown): string;
|
|
11
|
+
export declare class PermissionAnnouncementController {
|
|
12
|
+
private readonly announcements;
|
|
13
|
+
constructor(announcements: PermissionAnnouncementService);
|
|
14
|
+
/** The caller's tasks, grouped per company: `{ total, rows, page, limit }`. */
|
|
15
|
+
list(req: AuthedRequest, query: Record<string, unknown>): Promise<import("./permission-announcement.service.js").PermissionAnnouncementPage>;
|
|
16
|
+
/** "Reviewed, not needed" (idempotent, audited). */
|
|
17
|
+
dismiss(req: AuthedRequest, companyUuid: string, key: string): Promise<void>;
|
|
18
|
+
/** Undo a dismissal (idempotent, audited). */
|
|
19
|
+
reopen(req: AuthedRequest, companyUuid: string, key: string): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=permission-announcement.controller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permission-announcement.controller.d.ts","sourceRoot":"","sources":["../../../src/features/permission-announcement/permission-announcement.controller.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,EAIL,6BAA6B,EAE7B,KAAK,2BAA2B,EACjC,MAAM,sCAAsC,CAAC;AAE9C,qFAAqF;AACrF,UAAU,aAAa;IACrB,SAAS,EAAE,SAAS,CAAC;CACtB;AAsBD,sFAAsF;AACtF,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GACzC,2BAA2B,CAoB7B;AAED,qFAAqF;AACrF,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAS9D;AAED,qBAEa,gCAAgC;IAC/B,OAAO,CAAC,QAAQ,CAAC,aAAa;gBAAb,aAAa,EAAE,6BAA6B;IAEzE,+EAA+E;IAGzE,IAAI,CAAQ,GAAG,EAAE,aAAa,EAAW,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAI7E,oDAAoD;IAI9C,OAAO,CACJ,GAAG,EAAE,aAAa,EACO,WAAW,EAAE,MAAM,EACrC,GAAG,EAAE,MAAM,GACxB,OAAO,CAAC,IAAI,CAAC;IAIhB,8CAA8C;IAIxC,MAAM,CACH,GAAG,EAAE,aAAa,EACO,WAAW,EAAE,MAAM,EACrC,GAAG,EAAE,MAAM,GACxB,OAAO,CAAC,IAAI,CAAC;CAGjB"}
|