@astram/identity 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/dist/auth-facts-keys.d.ts +3 -0
  2. package/dist/auth-facts-keys.d.ts.map +1 -0
  3. package/dist/auth-facts-keys.js +24 -0
  4. package/dist/cross-tenant-grant.object.d.ts +28 -0
  5. package/dist/cross-tenant-grant.object.d.ts.map +1 -0
  6. package/dist/cross-tenant-grant.object.js +64 -0
  7. package/dist/features/access/access-admin.access.d.ts +3 -0
  8. package/dist/features/access/access-admin.access.d.ts.map +1 -0
  9. package/dist/features/access/access-admin.access.js +27 -0
  10. package/dist/features/access/company-admin.access.d.ts +3 -0
  11. package/dist/features/access/company-admin.access.d.ts.map +1 -0
  12. package/dist/features/access/company-admin.access.js +31 -0
  13. package/dist/features/access/index.d.ts +15 -0
  14. package/dist/features/access/index.d.ts.map +1 -0
  15. package/dist/features/access/index.js +26 -0
  16. package/dist/features/access/user-management.access.d.ts +3 -0
  17. package/dist/features/access/user-management.access.d.ts.map +1 -0
  18. package/dist/features/access/user-management.access.js +35 -0
  19. package/dist/features/audit/audit-read.service.d.ts +92 -0
  20. package/dist/features/audit/audit-read.service.d.ts.map +1 -0
  21. package/dist/features/audit/audit-read.service.js +97 -0
  22. package/dist/features/audit/audit.controller.d.ts +35 -0
  23. package/dist/features/audit/audit.controller.d.ts.map +1 -0
  24. package/dist/features/audit/audit.controller.js +73 -0
  25. package/dist/features/audit/directory-label-resolver.d.ts +14 -0
  26. package/dist/features/audit/directory-label-resolver.d.ts.map +1 -0
  27. package/dist/features/audit/directory-label-resolver.js +46 -0
  28. package/dist/features/audit/mariadb-audit-reader.d.ts +46 -0
  29. package/dist/features/audit/mariadb-audit-reader.d.ts.map +1 -0
  30. package/dist/features/audit/mariadb-audit-reader.js +116 -0
  31. package/dist/features/authz/access-bundle-reconcile.job.d.ts +35 -0
  32. package/dist/features/authz/access-bundle-reconcile.job.d.ts.map +1 -0
  33. package/dist/features/authz/access-bundle-reconcile.job.js +84 -0
  34. package/dist/features/authz/access-bundle-store.d.ts +65 -0
  35. package/dist/features/authz/access-bundle-store.d.ts.map +1 -0
  36. package/dist/features/authz/access-bundle-store.js +154 -0
  37. package/dist/features/authz/catalog-bootstrap.provider.d.ts +28 -0
  38. package/dist/features/authz/catalog-bootstrap.provider.d.ts.map +1 -0
  39. package/dist/features/authz/catalog-bootstrap.provider.js +85 -0
  40. package/dist/features/authz/catalog-reconcile.job.d.ts +23 -0
  41. package/dist/features/authz/catalog-reconcile.job.d.ts.map +1 -0
  42. package/dist/features/authz/catalog-reconcile.job.js +47 -0
  43. package/dist/features/authz/db-permission-facts.adapter.d.ts +60 -0
  44. package/dist/features/authz/db-permission-facts.adapter.d.ts.map +1 -0
  45. package/dist/features/authz/db-permission-facts.adapter.js +116 -0
  46. package/dist/features/authz/grant-admin.controller.d.ts +96 -0
  47. package/dist/features/authz/grant-admin.controller.d.ts.map +1 -0
  48. package/dist/features/authz/grant-admin.controller.js +209 -0
  49. package/dist/features/authz/grant-admin.service.d.ts +107 -0
  50. package/dist/features/authz/grant-admin.service.d.ts.map +1 -0
  51. package/dist/features/authz/grant-admin.service.js +297 -0
  52. package/dist/features/authz/mariadb-catalog-store.d.ts +16 -0
  53. package/dist/features/authz/mariadb-catalog-store.d.ts.map +1 -0
  54. package/dist/features/authz/mariadb-catalog-store.js +44 -0
  55. package/dist/features/authz/permission-version.bumper.d.ts +47 -0
  56. package/dist/features/authz/permission-version.bumper.d.ts.map +1 -0
  57. package/dist/features/authz/permission-version.bumper.js +68 -0
  58. package/dist/features/authz/record-authorization.d.ts +31 -0
  59. package/dist/features/authz/record-authorization.d.ts.map +1 -0
  60. package/dist/features/authz/record-authorization.js +59 -0
  61. package/dist/features/authz/session-facts.reader.d.ts +41 -0
  62. package/dist/features/authz/session-facts.reader.d.ts.map +1 -0
  63. package/dist/features/authz/session-facts.reader.js +27 -0
  64. package/dist/features/company/company.controller.d.ts +37 -0
  65. package/dist/features/company/company.controller.d.ts.map +1 -0
  66. package/dist/features/company/company.controller.js +100 -0
  67. package/dist/features/company/company.service.d.ts +25 -0
  68. package/dist/features/company/company.service.d.ts.map +1 -0
  69. package/dist/features/company/company.service.js +53 -0
  70. package/dist/features/cross-tenant/cross-tenant-grant-read.service.d.ts +126 -0
  71. package/dist/features/cross-tenant/cross-tenant-grant-read.service.d.ts.map +1 -0
  72. package/dist/features/cross-tenant/cross-tenant-grant-read.service.js +276 -0
  73. package/dist/features/cross-tenant/cross-tenant-grant.controller.d.ts +65 -0
  74. package/dist/features/cross-tenant/cross-tenant-grant.controller.d.ts.map +1 -0
  75. package/dist/features/cross-tenant/cross-tenant-grant.controller.js +250 -0
  76. package/dist/features/cross-tenant/grant-authorize.d.ts +42 -0
  77. package/dist/features/cross-tenant/grant-authorize.d.ts.map +1 -0
  78. package/dist/features/cross-tenant/grant-authorize.js +17 -0
  79. package/dist/features/identity-feature.module.d.ts +26 -0
  80. package/dist/features/identity-feature.module.d.ts.map +1 -0
  81. package/dist/features/identity-feature.module.js +247 -0
  82. package/dist/features/membership/membership.controller.d.ts +77 -0
  83. package/dist/features/membership/membership.controller.d.ts.map +1 -0
  84. package/dist/features/membership/membership.controller.js +174 -0
  85. package/dist/features/membership/membership.service.d.ts +73 -0
  86. package/dist/features/membership/membership.service.d.ts.map +1 -0
  87. package/dist/features/membership/membership.service.js +173 -0
  88. package/dist/features/membership/posting-version-bump.d.ts +10 -0
  89. package/dist/features/membership/posting-version-bump.d.ts.map +1 -0
  90. package/dist/features/membership/posting-version-bump.js +4 -0
  91. package/dist/features/membership/posting.service.d.ts +59 -0
  92. package/dist/features/membership/posting.service.d.ts.map +1 -0
  93. package/dist/features/membership/posting.service.js +181 -0
  94. package/dist/features/org-structure/org-catalog.controller.d.ts +14 -0
  95. package/dist/features/org-structure/org-catalog.controller.d.ts.map +1 -0
  96. package/dist/features/org-structure/org-catalog.controller.js +62 -0
  97. package/dist/features/org-structure/org-catalog.service.d.ts +19 -0
  98. package/dist/features/org-structure/org-catalog.service.d.ts.map +1 -0
  99. package/dist/features/org-structure/org-catalog.service.js +53 -0
  100. package/dist/features/role/role.controller.d.ts +37 -0
  101. package/dist/features/role/role.controller.d.ts.map +1 -0
  102. package/dist/features/role/role.controller.js +104 -0
  103. package/dist/features/role/role.service.d.ts +24 -0
  104. package/dist/features/role/role.service.d.ts.map +1 -0
  105. package/dist/features/role/role.service.js +53 -0
  106. package/dist/features/shared/http.d.ts +15 -0
  107. package/dist/features/shared/http.d.ts.map +1 -0
  108. package/dist/features/shared/http.js +25 -0
  109. package/dist/features/shared/list-query.d.ts +44 -0
  110. package/dist/features/shared/list-query.d.ts.map +1 -0
  111. package/dist/features/shared/list-query.js +53 -0
  112. package/dist/features/shared/named-option.d.ts +6 -0
  113. package/dist/features/shared/named-option.d.ts.map +1 -0
  114. package/dist/features/shared/named-option.js +2 -0
  115. package/dist/features/shared/record-resolver.d.ts +11 -0
  116. package/dist/features/shared/record-resolver.d.ts.map +1 -0
  117. package/dist/features/shared/record-resolver.js +32 -0
  118. package/dist/features/user/user.controller.d.ts +39 -0
  119. package/dist/features/user/user.controller.d.ts.map +1 -0
  120. package/dist/features/user/user.controller.js +104 -0
  121. package/dist/features/user/user.service.d.ts +26 -0
  122. package/dist/features/user/user.service.d.ts.map +1 -0
  123. package/dist/features/user/user.service.js +53 -0
  124. package/dist/index.d.ts +34 -3
  125. package/dist/index.d.ts.map +1 -1
  126. package/dist/index.js +66 -33
  127. package/dist/objects.d.ts +13 -0
  128. package/dist/objects.d.ts.map +1 -0
  129. package/dist/objects.js +20 -0
  130. package/dist/shared-resource.object.d.ts +28 -0
  131. package/dist/shared-resource.object.d.ts.map +1 -0
  132. package/dist/shared-resource.object.js +63 -0
  133. package/migrations/1710000000010-CreateAuditLog.cjs +42 -0
  134. package/migrations/1710000000011-CreateCrossTenantGrant.cjs +66 -0
  135. package/migrations/1710000000012-SeedCrossTenantGrantPerms.cjs +86 -0
  136. package/migrations/1710000000013-CreateSharedResource.cjs +30 -0
  137. package/migrations/1710000000099-SeedDevIdentity.cjs +130 -0
  138. package/migrations/1710000000100-SeedDevTenantBAndRoot.cjs +212 -0
  139. package/package.json +4 -3
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+ /**
3
+ * MembershipService — the membership layer's write/read logic, built ENTIRELY on the
4
+ * injected RecordsService facade (which supplies the request principal and runs each op
5
+ * through the engine pipe). It never touches a pool, query runner, or raw SQL — every op
6
+ * is one-or-more facade calls, so the permission gate + anti-forgery write-context +
7
+ * audit + cross-tenant isolation are applied automatically.
8
+ *
9
+ * A `company_membership` is the first-class `(user, company)` scope with `is_active`
10
+ * (exactly one active per user) + `status`. This service owns the ONE invariant the DB
11
+ * cannot express — one-active-per-user — because there is no partial-unique on
12
+ * `cm_is_active`; `setActive` is the guard.
13
+ */
14
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
15
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
16
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
17
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
18
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
19
+ };
20
+ var __metadata = (this && this.__metadata) || function (k, v) {
21
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
22
+ };
23
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
24
+ return function (target, key) { decorator(target, key, paramIndex); }
25
+ };
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.MembershipService = void 0;
28
+ const common_1 = require("@nestjs/common");
29
+ const engine_1 = require("@astram/engine");
30
+ const posting_version_bump_js_1 = require("./posting-version-bump.js");
31
+ let MembershipService = class MembershipService {
32
+ records;
33
+ versionBump;
34
+ constructor(records, versionBump) {
35
+ this.records = records;
36
+ this.versionBump = versionBump;
37
+ }
38
+ /**
39
+ * Read a membership's tenant (its `companyUuid`) through the pipe, or `null` when
40
+ * unresolvable (→ no bump; fail-safe, never guesses a tenant). Used to target the
41
+ * permission-version bump at the company whose coarse grants a mutation changed.
42
+ */
43
+ async tenantOf(membershipUuid) {
44
+ try {
45
+ const m = await this.records.get('company_membership', membershipUuid);
46
+ const companyUuid = m?.fields?.companyUuid;
47
+ return typeof companyUuid === 'string' && companyUuid.length > 0 ? companyUuid : null;
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ }
53
+ /**
54
+ * Bump the permission version for each DISTINCT tenant in `tenantUuids` (best-effort +
55
+ * fail-safe): the mutation already succeeded, so a bump hiccup must NOT surface as an
56
+ * error that hides a successful membership change — the cache is invalidated by the next
57
+ * legit bump / the facts' natural TTL instead. No collaborator wired → no-op (prior
58
+ * behavior). `null`/empty entries are skipped (never bump a guessed tenant).
59
+ */
60
+ async bumpTenants(tenantUuids) {
61
+ const bump = this.versionBump;
62
+ if (!bump)
63
+ return;
64
+ for (const tenant of new Set(tenantUuids.filter((t) => !!t))) {
65
+ try {
66
+ await bump.bumpTenant(tenant);
67
+ }
68
+ catch {
69
+ // Fail-safe: never fail an applied membership change on a version-bump hiccup.
70
+ }
71
+ }
72
+ }
73
+ /**
74
+ * Add a membership card for `(userUuid, companyUuid)`. New memberships are created
75
+ * INACTIVE (`isActive:false`) — activation is an explicit `setActive` call so the
76
+ * one-active invariant is never violated by a fresh add. A duplicate `(user, company)`
77
+ * hits the DB's UNIQUE constraint and the engine's error surfaces to the caller (NOT
78
+ * swallowed here). `createdBy` is stamped server-side (actorId-trusted).
79
+ */
80
+ addMembership(userUuid, companyUuid) {
81
+ return this.records.create('company_membership', {
82
+ fields: { userUuid, companyUuid, isActive: false, status: 'active' },
83
+ });
84
+ }
85
+ /** List a user's membership cards (tenant-scoped by the pipe). */
86
+ async listMemberships(userUuid) {
87
+ const page = await this.records.list('company_membership', { where: { userUuid } });
88
+ return page.rows;
89
+ }
90
+ /**
91
+ * Flip which of the user's memberships is active — the ONE-ACTIVE invariant. The service
92
+ * is the guard because the DB has no partial-unique on `cm_is_active`.
93
+ *
94
+ * ⚠️ ORDER IS LOAD-BEARING (fail-safe against a LOGIN LOCKOUT). The engine's `list` has
95
+ * no ORDER BY (PKs are random uuids → arbitrary row order) and each `update` is its OWN
96
+ * transaction (no cross-call atomicity). So this MUST activate the TARGET FIRST, then
97
+ * deactivate the others:
98
+ * - Worst case of a mid-loop failure is then a transient TWO-active window, which is
99
+ * RECOVERABLE — the credential resolver reads `is_active=1 LIMIT 1`, so it still
100
+ * resolves a tenant and login works.
101
+ * - The forbidden alternative (deactivate-old-then-activate-target) can leave a
102
+ * ZERO-active window on a mid-failure → the resolver returns null → the user is
103
+ * LOCKED OUT of login until an admin re-activates. Never do that.
104
+ *
105
+ * Guard: if `membershipUuid` is NOT one of the user's memberships, throw — otherwise the
106
+ * loop would deactivate everything and activate nothing (also a zero-active lockout).
107
+ */
108
+ async setActive(membershipUuid, userUuid) {
109
+ const page = await this.records.list('company_membership', { where: { userUuid } });
110
+ const target = page.rows.find((m) => m.id === membershipUuid);
111
+ if (!target) {
112
+ // Not this user's membership → refuse rather than leave the user with ZERO active
113
+ // (a login lockout). Fail-closed.
114
+ throw new common_1.NotFoundException(`membership ${membershipUuid} does not belong to user ${userUuid}`);
115
+ }
116
+ // 1) TARGET FIRST — guarantees an active membership exists before any deactivation, so
117
+ // a mid-failure below leaves TWO active (recoverable), never ZERO (lockout).
118
+ await this.records.update('company_membership', target.id, { fields: { isActive: true } });
119
+ // 2) Deactivate every OTHER membership (skip the target we just activated).
120
+ const deactivated = [];
121
+ for (const m of page.rows) {
122
+ if (m.id === membershipUuid)
123
+ continue;
124
+ await this.records.update('company_membership', m.id, { fields: { isActive: false } });
125
+ deactivated.push(m);
126
+ }
127
+ // ADR-0011 STALENESS FIX: activation ADDS the target company's role grants and each
128
+ // deactivation REMOVES that company's grants — a coarse-grant change in BOTH directions.
129
+ // A user's memberships can span DIFFERENT companies, so setActive may affect MULTIPLE
130
+ // tenants: bump the target's tenant AND every deactivated membership's tenant. Read each
131
+ // tenant from the already-loaded rows (companyUuid is a field on the membership) — no
132
+ // extra pipe reads. bumpTenants de-dupes + is best-effort/fail-safe (the flip already
133
+ // succeeded; a bump hiccup must not error).
134
+ const asCompanyUuid = (m) => {
135
+ const c = m.fields?.companyUuid;
136
+ return typeof c === 'string' && c.length > 0 ? c : null;
137
+ };
138
+ await this.bumpTenants([asCompanyUuid(target), ...deactivated.map(asCompanyUuid)]);
139
+ }
140
+ /**
141
+ * Update a membership's mutable non-activity fields (currently `status`). Activity is
142
+ * flipped only via `setActive` to preserve the one-active invariant, so `isActive` is
143
+ * intentionally NOT a patch key here.
144
+ */
145
+ updateMembership(membershipUuid, patch) {
146
+ // NO version bump here (ADR-0011): this patches `status` ONLY — `isActive` is
147
+ // deliberately not a patch key (activity flips solely via `setActive`, which bumps),
148
+ // and the controller only ever passes `{ status }`. Coarse-grant resolution gates on
149
+ // `cm_is_active = 1`, NOT on `cm_status`, so a status change does NOT alter which roles
150
+ // resolve → the warm cache stays correct → no invalidation needed. If a future patch key
151
+ // is ever added here that CAN change which roles/postings resolve, it MUST bump the
152
+ // membership's tenant (mirror `setActive`).
153
+ return this.records.update('company_membership', membershipUuid, { fields: patch });
154
+ }
155
+ /** Remove a membership card (its postings cascade at the DB via FK ON DELETE CASCADE). */
156
+ async removeMembership(membershipUuid) {
157
+ // ADR-0011 STALENESS FIX: resolve the membership's tenant BEFORE the delete (it's
158
+ // unreadable afterwards). If the membership was active, deleting it (postings cascade)
159
+ // removes the user's role grants in that company — a coarse-grant change — so bump that
160
+ // tenant AFTER the delete succeeds. Best-effort/fail-safe (bumpTenants swallows). A
161
+ // membership belongs to exactly one company → one tenant bump.
162
+ const tenant = await this.tenantOf(membershipUuid);
163
+ await this.records.delete('company_membership', membershipUuid);
164
+ await this.bumpTenants([tenant]);
165
+ }
166
+ };
167
+ exports.MembershipService = MembershipService;
168
+ exports.MembershipService = MembershipService = __decorate([
169
+ (0, common_1.Injectable)(),
170
+ __param(1, (0, common_1.Optional)()),
171
+ __param(1, (0, common_1.Inject)(posting_version_bump_js_1.POSTING_VERSION_BUMP)),
172
+ __metadata("design:paramtypes", [engine_1.RecordsService, Object])
173
+ ], MembershipService);
@@ -0,0 +1,10 @@
1
+ /**
2
+ * PostingVersionBump — the seam the membership/posting services call after a change to bump the
3
+ * tenant's permission version (ADR-0011), so a role gain/loss lands in authorization WITHOUT
4
+ * re-login. Injected @Optional() — absent (unit tests) = no bump = prior behavior.
5
+ */
6
+ export interface PostingVersionBump {
7
+ bumpTenant(tenantUuid: string): Promise<void>;
8
+ }
9
+ export declare const POSTING_VERSION_BUMP: unique symbol;
10
+ //# sourceMappingURL=posting-version-bump.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"posting-version-bump.d.ts","sourceRoot":"","sources":["../../../src/features/membership/posting-version-bump.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/C;AAED,eAAO,MAAM,oBAAoB,eAAiC,CAAC"}
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.POSTING_VERSION_BUMP = void 0;
4
+ exports.POSTING_VERSION_BUMP = Symbol('POSTING_VERSION_BUMP');
@@ -0,0 +1,59 @@
1
+ /**
2
+ * PostingService — the ATOMIC replace-set of a membership's postings (Phase 3, Task 4),
3
+ * built on the injected RecordsService facade (→ the engine pipe). A posting is a bundled, time-bounded assignment:
4
+ * a role (+ optional department + optional branch) with an optional effective window.
5
+ * The Assignments editor submits the FULL desired row set for a membership; this service
6
+ * diffs current-vs-desired and applies the minimal change — delete only what's removed,
7
+ * create only what's added, keep the rest untouched.
8
+ *
9
+ * ── Atomicity / mid-op safety (why delete-then-create is safe here) ──────────────────
10
+ * The engine's feature-facing surface (`RecordsService`) exposes NO cross-call transaction
11
+ * — each of its methods (create/update/delete/list/get) runs in its OWN transaction, and
12
+ * no `externalQueryRunner` seam is handed to features. So `replacePostings` cannot wrap
13
+ * the whole diff in a single DB transaction. Instead it is safe BY CONSTRUCTION:
14
+ * - Deletes target ONLY rows absent from `desired` (the "removed" set). A KEPT posting
15
+ * is never a delete target, so it is never removed.
16
+ * - Creates target ONLY rows absent from `current` (the "added" set).
17
+ * Therefore the membership NEVER lands empty mid-op: even if a create fails after the
18
+ * delete phase, every kept posting is still present (never deleted). Ordering is
19
+ * delete-first, then create; a failure surfaces to the caller (fail-closed) with the
20
+ * kept set intact. (Covered by the mid-op-safety tests in posting.service.spec.ts.)
21
+ */
22
+ import { RecordsService, type RecordView } from '@astram/engine';
23
+ import { type PostingVersionBump } from './posting-version-bump.js';
24
+ /**
25
+ * The shape of one desired posting row submitted by the editor. The effective-window
26
+ * bounds arrive from the client as a bare `YYYY-MM-DD` date string (native `<input
27
+ * type=date>`), an ISO datetime, an epoch-ms number, or `null` (open-ended). They are
28
+ * normalized to epoch-ms integers before the engine writes them (see `toEpochMs`).
29
+ */
30
+ export interface PostingInput {
31
+ roleUuid: string;
32
+ departmentUuid?: string | null;
33
+ branchUuid?: string | null;
34
+ effectiveFrom?: string | number | null;
35
+ effectiveUntil?: string | number | null;
36
+ }
37
+ export declare class PostingService {
38
+ private readonly records;
39
+ private readonly versionBump?;
40
+ constructor(records: RecordsService, versionBump?: (PostingVersionBump | null) | undefined);
41
+ /** List a membership's postings (tenant-scoped by the pipe). */
42
+ listPostings(membershipUuid: string): Promise<readonly RecordView[]>;
43
+ /**
44
+ * Replace a membership's full posting set with `desired`, applying the minimal diff.
45
+ * Compares current vs desired by the natural key `roleUuid|departmentUuid|branchUuid`:
46
+ * deletes removed rows, creates added rows, leaves kept rows untouched. See the file
47
+ * header for the atomicity / mid-op-safety reasoning (delete-then-create is safe because
48
+ * kept postings are never delete targets, so the membership never lands empty).
49
+ */
50
+ replacePostings(membershipUuid: string, desired: readonly PostingInput[]): Promise<void>;
51
+ /**
52
+ * Resolve the tenant (company uuid) a membership belongs to — the tenant whose permission
53
+ * version a posting change must bump. Reads the parent `company_membership` through the
54
+ * pipe (tenant-scoped, no raw SQL), returning `companyUuid` or `null` if unresolvable
55
+ * (then no bump — fail-safe; never guesses a tenant).
56
+ */
57
+ private resolveMembershipTenant;
58
+ }
59
+ //# sourceMappingURL=posting.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"posting.service.d.ts","sourceRoot":"","sources":["../../../src/features/membership/posting.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,EAAE,cAAc,EAAE,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjE,OAAO,EAAwB,KAAK,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAE1F;;;;;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;AA4DD,qBACa,cAAc;IAEvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAKxB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;gBALZ,OAAO,EAAE,cAAc,EAKvB,WAAW,CAAC,GAAE,kBAAkB,GAAG,IAAI,aAAA;IAG1D,gEAAgE;IAC1D,YAAY,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,UAAU,EAAE,CAAC;IAK1E;;;;;;OAMG;IACG,eAAe,CAAC,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,YAAY,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IA4D9F;;;;;OAKG;YACW,uBAAuB;CAStC"}
@@ -0,0 +1,181 @@
1
+ "use strict";
2
+ /**
3
+ * PostingService — the ATOMIC replace-set of a membership's postings (Phase 3, Task 4),
4
+ * built on the injected RecordsService facade (→ the engine pipe). A posting is a bundled, time-bounded assignment:
5
+ * a role (+ optional department + optional branch) with an optional effective window.
6
+ * The Assignments editor submits the FULL desired row set for a membership; this service
7
+ * diffs current-vs-desired and applies the minimal change — delete only what's removed,
8
+ * create only what's added, keep the rest untouched.
9
+ *
10
+ * ── Atomicity / mid-op safety (why delete-then-create is safe here) ──────────────────
11
+ * The engine's feature-facing surface (`RecordsService`) exposes NO cross-call transaction
12
+ * — each of its methods (create/update/delete/list/get) runs in its OWN transaction, and
13
+ * no `externalQueryRunner` seam is handed to features. So `replacePostings` cannot wrap
14
+ * the whole diff in a single DB transaction. Instead it is safe BY CONSTRUCTION:
15
+ * - Deletes target ONLY rows absent from `desired` (the "removed" set). A KEPT posting
16
+ * is never a delete target, so it is never removed.
17
+ * - Creates target ONLY rows absent from `current` (the "added" set).
18
+ * Therefore the membership NEVER lands empty mid-op: even if a create fails after the
19
+ * delete phase, every kept posting is still present (never deleted). Ordering is
20
+ * delete-first, then create; a failure surfaces to the caller (fail-closed) with the
21
+ * kept set intact. (Covered by the mid-op-safety tests in posting.service.spec.ts.)
22
+ */
23
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
24
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
25
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
26
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
27
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
28
+ };
29
+ var __metadata = (this && this.__metadata) || function (k, v) {
30
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
31
+ };
32
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
33
+ return function (target, key) { decorator(target, key, paramIndex); }
34
+ };
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.PostingService = void 0;
37
+ const common_1 = require("@nestjs/common");
38
+ const engine_1 = require("@astram/engine");
39
+ const posting_version_bump_js_1 = require("./posting-version-bump.js");
40
+ /** The field keys whose values are effective-window epoch-ms bounds (normalized on write). */
41
+ const WINDOW_KEYS = new Set(['effectiveFrom', 'effectiveUntil']);
42
+ /**
43
+ * Normalize an effective-window bound to epoch-ms (a `number`), or `null` if empty/unparseable.
44
+ * The membership_posting object declares these fields as `number` (BIGINT epoch-ms) so the
45
+ * engine binds an INTEGER, never a driver-serialized `Date` — the timezone-immune fix. A bare
46
+ * `YYYY-MM-DD` (the native date input) is parsed by `Date.parse` as UTC midnight (per the ES
47
+ * spec's date-only form), so the stored instant is timezone-stable regardless of host tz.
48
+ */
49
+ function toEpochMs(value) {
50
+ if (value === null || value === undefined || value === '')
51
+ return null;
52
+ if (typeof value === 'number')
53
+ return Number.isFinite(value) ? value : null;
54
+ if (typeof value === 'string') {
55
+ const ms = Date.parse(value);
56
+ return Number.isNaN(ms) ? null : ms;
57
+ }
58
+ return null;
59
+ }
60
+ /** The identity a human calls "the same posting": role + optional dept + optional branch. */
61
+ function naturalKey(p) {
62
+ return `${p.roleUuid}|${p.departmentUuid ?? ''}|${p.branchUuid ?? ''}`;
63
+ }
64
+ /**
65
+ * Build the engine field map from a desired posting, dropping empty optionals and
66
+ * normalizing the effective-window bounds to epoch-ms integers.
67
+ *
68
+ * Two concerns:
69
+ * 1. Drop `undefined`/`null`/`''` optional keys. This is load-bearing: the editor submits
70
+ * `departmentUuid`/`branchUuid`/`effectiveFrom`/`effectiveUntil` as `null`/`''` for an
71
+ * empty (company-wide / open-ended) posting, but the engine's validate stage runs its
72
+ * type-coercer on any PRESENT column and REJECTS `null`. Omitting the key entirely makes
73
+ * the stage skip it (absent + optional → no coercion), so the column defaults to SQL NULL
74
+ * — exactly what a company-wide / no-expiry posting means.
75
+ * 2. Convert the window bounds (`effectiveFrom`/`effectiveUntil`) from the client's date
76
+ * string / ISO / number to epoch-ms via `toEpochMs`, because the object declares them as
77
+ * `number` (BIGINT epoch-ms) — the timezone-immune storage. An unparseable/empty bound
78
+ * is dropped (→ SQL NULL = open-ended), never coerced to a bogus number.
79
+ */
80
+ function stripEmpty(input) {
81
+ const out = {};
82
+ for (const [k, v] of Object.entries(input)) {
83
+ if (WINDOW_KEYS.has(k)) {
84
+ const ms = toEpochMs(v);
85
+ if (ms !== null)
86
+ out[k] = ms; // else omit → SQL NULL (open-ended)
87
+ continue;
88
+ }
89
+ if (v !== undefined && v !== null && v !== '')
90
+ out[k] = v;
91
+ }
92
+ return out;
93
+ }
94
+ let PostingService = class PostingService {
95
+ records;
96
+ versionBump;
97
+ constructor(records, versionBump) {
98
+ this.records = records;
99
+ this.versionBump = versionBump;
100
+ }
101
+ /** List a membership's postings (tenant-scoped by the pipe). */
102
+ async listPostings(membershipUuid) {
103
+ const page = await this.records.list('membership_posting', { where: { membershipUuid } });
104
+ return page.rows;
105
+ }
106
+ /**
107
+ * Replace a membership's full posting set with `desired`, applying the minimal diff.
108
+ * Compares current vs desired by the natural key `roleUuid|departmentUuid|branchUuid`:
109
+ * deletes removed rows, creates added rows, leaves kept rows untouched. See the file
110
+ * header for the atomicity / mid-op-safety reasoning (delete-then-create is safe because
111
+ * kept postings are never delete targets, so the membership never lands empty).
112
+ */
113
+ async replacePostings(membershipUuid, desired) {
114
+ const current = (await this.records.list('membership_posting', { where: { membershipUuid } })).rows;
115
+ const desiredKeys = new Set(desired.map(naturalKey));
116
+ const currentKeys = new Set(current.map((r) => naturalKey(r.fields)));
117
+ // Track whether the diff actually changed anything — only a real add/remove of a role
118
+ // assignment need invalidate cached permission facts (a no-op replace must not bump).
119
+ let changed = false;
120
+ // Delete removed: rows in current whose key is not in desired. Kept rows are skipped.
121
+ for (const r of current) {
122
+ const key = naturalKey(r.fields);
123
+ if (!desiredKeys.has(key)) {
124
+ await this.records.delete('membership_posting', r.id);
125
+ changed = true;
126
+ }
127
+ }
128
+ // Create added: desired rows whose key is not already present. Only real fields are
129
+ // sent (undefined/null/empty optionals are dropped so the engine gets a clean field map).
130
+ for (const d of desired) {
131
+ if (!currentKeys.has(naturalKey(d))) {
132
+ await this.records.create('membership_posting', {
133
+ fields: { membershipUuid, ...stripEmpty(d) },
134
+ });
135
+ changed = true;
136
+ }
137
+ }
138
+ // ADR-0011 STALENESS FIX: a posting add/remove changes the effective roles (→ coarse
139
+ // grants) of the membership's user in that company, so bump the tenant's permission
140
+ // version — every live session in that tenant then reloads its cached facts on the next
141
+ // request (a role gain/loss lands within the version bound). Only when something actually
142
+ // changed, and only when the bump collaborator is wired (absent in unit tests → no-op).
143
+ // Best-effort + fail-safe: the posting write already succeeded, so a bump failure must
144
+ // NOT surface as a 500 that hides a successful assignment change — the cache is instead
145
+ // invalidated by the next legit bump / the facts' natural TTL. We log-swallow it.
146
+ if (changed && this.versionBump) {
147
+ const tenantUuid = await this.resolveMembershipTenant(membershipUuid);
148
+ if (tenantUuid) {
149
+ try {
150
+ await this.versionBump.bumpTenant(tenantUuid);
151
+ }
152
+ catch {
153
+ // Fail-safe: never fail an applied posting change on a version-bump hiccup.
154
+ }
155
+ }
156
+ }
157
+ }
158
+ /**
159
+ * Resolve the tenant (company uuid) a membership belongs to — the tenant whose permission
160
+ * version a posting change must bump. Reads the parent `company_membership` through the
161
+ * pipe (tenant-scoped, no raw SQL), returning `companyUuid` or `null` if unresolvable
162
+ * (then no bump — fail-safe; never guesses a tenant).
163
+ */
164
+ async resolveMembershipTenant(membershipUuid) {
165
+ try {
166
+ const m = await this.records.get('company_membership', membershipUuid);
167
+ const companyUuid = m?.fields?.companyUuid;
168
+ return typeof companyUuid === 'string' && companyUuid.length > 0 ? companyUuid : null;
169
+ }
170
+ catch {
171
+ return null;
172
+ }
173
+ }
174
+ };
175
+ exports.PostingService = PostingService;
176
+ exports.PostingService = PostingService = __decorate([
177
+ (0, common_1.Injectable)(),
178
+ __param(1, (0, common_1.Optional)()),
179
+ __param(1, (0, common_1.Inject)(posting_version_bump_js_1.POSTING_VERSION_BUMP)),
180
+ __metadata("design:paramtypes", [engine_1.RecordsService, Object])
181
+ ], PostingService);
@@ -0,0 +1,14 @@
1
+ import { OrgCatalogService } from './org-catalog.service.js';
2
+ export declare class OrgCatalogController {
3
+ private readonly catalog;
4
+ constructor(catalog: OrgCatalogService);
5
+ /** A company's departments as `{ uuid, name }` options for the posting editor. */
6
+ listCompanyDepartments(companyUuid: string): Promise<{
7
+ rows: import("../shared/named-option.js").NamedOption[];
8
+ }>;
9
+ /** A company's branches as `{ uuid, name }` options for the posting editor. */
10
+ listCompanyBranches(companyUuid: string): Promise<{
11
+ rows: import("../shared/named-option.js").NamedOption[];
12
+ }>;
13
+ }
14
+ //# sourceMappingURL=org-catalog.controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"org-catalog.controller.d.ts","sourceRoot":"","sources":["../../../src/features/org-structure/org-catalog.controller.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAU7D,qBACa,oBAAoB;IACnB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,iBAAiB;IAEvD,kFAAkF;IAG5E,sBAAsB,CAAuB,WAAW,EAAE,MAAM;;;IAKtE,+EAA+E;IAGzE,mBAAmB,CAAuB,WAAW,EAAE,MAAM;;;CAIpE"}
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.OrgCatalogController = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const guard_1 = require("@astram/guard");
18
+ const org_catalog_service_js_1 = require("./org-catalog.service.js");
19
+ // ── Company-scoped catalogs (dept/branch option lists for the posting editor) ──
20
+ //
21
+ // These back the Assignments editor's company-scoped Department/Branch dropdowns.
22
+ // Each returns `{ rows: [{ uuid, name }] }` — the uuid is the option VALUE the client
23
+ // stores, the name is the display LABEL. Read through the pipe (list the
24
+ // company_department/company_branch junction → resolve each catalog uuid to a name in
25
+ // the service), so no raw uuid is ever a label.
26
+ let OrgCatalogController = class OrgCatalogController {
27
+ catalog;
28
+ constructor(catalog) {
29
+ this.catalog = catalog;
30
+ }
31
+ /** A company's departments as `{ uuid, name }` options for the posting editor. */
32
+ async listCompanyDepartments(companyUuid) {
33
+ const rows = await this.catalog.listCompanyDepartments(companyUuid);
34
+ return { rows };
35
+ }
36
+ /** A company's branches as `{ uuid, name }` options for the posting editor. */
37
+ async listCompanyBranches(companyUuid) {
38
+ const rows = await this.catalog.listCompanyBranches(companyUuid);
39
+ return { rows };
40
+ }
41
+ };
42
+ exports.OrgCatalogController = OrgCatalogController;
43
+ __decorate([
44
+ (0, common_1.Get)('companies/:companyUuid/departments'),
45
+ (0, guard_1.Classified)({ typeKey: 'company_department', action: 'read' }),
46
+ __param(0, (0, common_1.Param)('companyUuid')),
47
+ __metadata("design:type", Function),
48
+ __metadata("design:paramtypes", [String]),
49
+ __metadata("design:returntype", Promise)
50
+ ], OrgCatalogController.prototype, "listCompanyDepartments", null);
51
+ __decorate([
52
+ (0, common_1.Get)('companies/:companyUuid/branches'),
53
+ (0, guard_1.Classified)({ typeKey: 'company_branch', action: 'read' }),
54
+ __param(0, (0, common_1.Param)('companyUuid')),
55
+ __metadata("design:type", Function),
56
+ __metadata("design:paramtypes", [String]),
57
+ __metadata("design:returntype", Promise)
58
+ ], OrgCatalogController.prototype, "listCompanyBranches", null);
59
+ exports.OrgCatalogController = OrgCatalogController = __decorate([
60
+ (0, common_1.Controller)('identity'),
61
+ __metadata("design:paramtypes", [org_catalog_service_js_1.OrgCatalogService])
62
+ ], OrgCatalogController);
@@ -0,0 +1,19 @@
1
+ /**
2
+ * OrgCatalogService — company-scoped Department/Branch OPTION lists for the Assignments
3
+ * editor dropdowns, via the RecordsService facade (no actor threading). Reads the
4
+ * company_department/company_branch junctions and resolves each catalog uuid → display
5
+ * name (never a raw uuid label). No raw SQL.
6
+ */
7
+ import { RecordsService } from '@astram/engine';
8
+ import { type NamedOption } from '../shared/named-option.js';
9
+ export declare class OrgCatalogService {
10
+ private readonly records;
11
+ constructor(records: RecordsService);
12
+ /** A company's departments as `{ uuid, name }` options for the posting editor. */
13
+ listCompanyDepartments(companyUuid: string): Promise<NamedOption[]>;
14
+ /** A company's branches as `{ uuid, name }` options for the posting editor. */
15
+ listCompanyBranches(companyUuid: string): Promise<NamedOption[]>;
16
+ /** Shared reader for a company-scoped catalog junction; lists then name-resolves. */
17
+ private listCompanyCatalog;
18
+ }
19
+ //# sourceMappingURL=org-catalog.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"org-catalog.service.d.ts","sourceRoot":"","sources":["../../../src/features/org-structure/org-catalog.service.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAE7D,qBACa,iBAAiB;IAChB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,cAAc;IAEpD,kFAAkF;IAClF,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAInE,+EAA+E;IAC/E,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAIhE,qFAAqF;YACvE,kBAAkB;CAiBjC"}
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ /**
3
+ * OrgCatalogService — company-scoped Department/Branch OPTION lists for the Assignments
4
+ * editor dropdowns, via the RecordsService facade (no actor threading). Reads the
5
+ * company_department/company_branch junctions and resolves each catalog uuid → display
6
+ * name (never a raw uuid label). No raw SQL.
7
+ */
8
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
9
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
10
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
11
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
12
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
13
+ };
14
+ var __metadata = (this && this.__metadata) || function (k, v) {
15
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
16
+ };
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.OrgCatalogService = void 0;
19
+ const common_1 = require("@nestjs/common");
20
+ const engine_1 = require("@astram/engine");
21
+ let OrgCatalogService = class OrgCatalogService {
22
+ records;
23
+ constructor(records) {
24
+ this.records = records;
25
+ }
26
+ /** A company's departments as `{ uuid, name }` options for the posting editor. */
27
+ listCompanyDepartments(companyUuid) {
28
+ return this.listCompanyCatalog('company_department', 'departmentUuid', 'department', companyUuid);
29
+ }
30
+ /** A company's branches as `{ uuid, name }` options for the posting editor. */
31
+ listCompanyBranches(companyUuid) {
32
+ return this.listCompanyCatalog('company_branch', 'branchUuid', 'branch', companyUuid);
33
+ }
34
+ /** Shared reader for a company-scoped catalog junction; lists then name-resolves. */
35
+ async listCompanyCatalog(junctionTypeKey, refFieldKey, catalogTypeKey, companyUuid) {
36
+ const page = await this.records.list(junctionTypeKey, { where: { companyUuid } });
37
+ const options = [];
38
+ for (const row of page.rows) {
39
+ const refUuid = row.fields[refFieldKey];
40
+ if (typeof refUuid !== 'string' || refUuid.length === 0)
41
+ continue;
42
+ const view = await this.records.get(catalogTypeKey, refUuid);
43
+ const name = view?.fields?.name;
44
+ options.push({ uuid: refUuid, name: typeof name === 'string' && name.length > 0 ? name : '(unknown)' });
45
+ }
46
+ return options;
47
+ }
48
+ };
49
+ exports.OrgCatalogService = OrgCatalogService;
50
+ exports.OrgCatalogService = OrgCatalogService = __decorate([
51
+ (0, common_1.Injectable)(),
52
+ __metadata("design:paramtypes", [engine_1.RecordsService])
53
+ ], OrgCatalogService);
@@ -0,0 +1,37 @@
1
+ import { RoleService } from './role.service.js';
2
+ import { type ListQueryDto } from '../shared/list-query.js';
3
+ export declare class RoleController {
4
+ private readonly roles;
5
+ constructor(roles: RoleService);
6
+ /**
7
+ * List roles — gated on `identity:role:read` (see class doc). Server-side DataTable:
8
+ * `?page&limit&search&sortBy&sortOrder`. Returns `{ total, rows, page, limit }` —
9
+ * `total` reflects the FILTERED set so the client can page the true result count.
10
+ */
11
+ listRoles(query: ListQueryDto): Promise<{
12
+ total: number;
13
+ rows: readonly import("packages/engine/dist/types.js").RecordView[];
14
+ page: number;
15
+ limit: number;
16
+ }>;
17
+ /** Create a role — gated on `identity:role:create`. */
18
+ createRole(body: {
19
+ name?: string;
20
+ }): Promise<{
21
+ id: import("packages/engine/dist/types.js").RecordId;
22
+ }>;
23
+ /** Read one role — gated on `identity:role:read` (CONTROL-plane; root, the identity
24
+ * administrator, may read roles). */
25
+ getRole(uuid: string): Promise<import("packages/engine/dist/types.js").RecordView | null>;
26
+ /** Update a role — gated on `identity:role:update`. */
27
+ updateRole(uuid: string, body: {
28
+ name?: string;
29
+ }): Promise<{
30
+ success: boolean;
31
+ }>;
32
+ /** Delete a role — gated on `identity:role:delete`. */
33
+ deleteRole(uuid: string): Promise<{
34
+ success: boolean;
35
+ }>;
36
+ }
37
+ //# sourceMappingURL=role.controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"role.controller.d.ts","sourceRoot":"","sources":["../../../src/features/role/role.controller.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAK5E,qBACa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,WAAW;IAE/C;;;;OAIG;IAGG,SAAS,CAAU,KAAK,EAAE,YAAY;;;;;;IAM5C,uDAAuD;IAGjD,UAAU,CAAS,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAKhD;0CACsC;IAGtC,OAAO,CAAgB,IAAI,EAAE,MAAM;IAInC,uDAAuD;IAGjD,UAAU,CAAgB,IAAI,EAAE,MAAM,EAAU,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAK7E,uDAAuD;IAGjD,UAAU,CAAgB,IAAI,EAAE,MAAM;;;CAI7C"}