@astram/identity 0.3.1 → 0.5.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 (118) hide show
  1. package/dist/company-membership.object.d.ts.map +1 -1
  2. package/dist/company-membership.object.js +3 -2
  3. package/dist/company.object.d.ts.map +1 -1
  4. package/dist/company.object.js +8 -3
  5. package/dist/features/access/index.d.ts.map +1 -1
  6. package/dist/features/access/index.js +1 -5
  7. package/dist/features/audit/audit-read.service.d.ts.map +1 -1
  8. package/dist/features/audit/audit-read.service.js +13 -6
  9. package/dist/features/audit/audit.controller.d.ts.map +1 -1
  10. package/dist/features/audit/audit.controller.js +2 -0
  11. package/dist/features/audit/mariadb-audit-reader.d.ts.map +1 -1
  12. package/dist/features/authz/access-bundle-reconcile.job.d.ts.map +1 -1
  13. package/dist/features/authz/catalog-reconcile.job.d.ts.map +1 -1
  14. package/dist/features/authz/catalog-reconcile.job.js +1 -3
  15. package/dist/features/authz/db-permission-facts.adapter.d.ts +24 -4
  16. package/dist/features/authz/db-permission-facts.adapter.d.ts.map +1 -1
  17. package/dist/features/authz/db-permission-facts.adapter.js +92 -5
  18. package/dist/features/authz/grant-admin.controller.d.ts +8 -5
  19. package/dist/features/authz/grant-admin.controller.d.ts.map +1 -1
  20. package/dist/features/authz/grant-admin.controller.js +22 -15
  21. package/dist/features/authz/grant-admin.service.d.ts +57 -11
  22. package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
  23. package/dist/features/authz/grant-admin.service.js +163 -85
  24. package/dist/features/authz/grant-ceiling.d.ts +111 -0
  25. package/dist/features/authz/grant-ceiling.d.ts.map +1 -0
  26. package/dist/features/authz/grant-ceiling.js +248 -0
  27. package/dist/features/authz/mariadb-catalog-store.d.ts.map +1 -1
  28. package/dist/features/authz/mariadb-catalog-store.js +15 -3
  29. package/dist/features/authz/permission-version.bumper.d.ts +45 -1
  30. package/dist/features/authz/permission-version.bumper.d.ts.map +1 -1
  31. package/dist/features/authz/permission-version.bumper.js +106 -16
  32. package/dist/features/authz/record-authorization.d.ts.map +1 -1
  33. package/dist/features/authz/role-binding.d.ts +36 -0
  34. package/dist/features/authz/role-binding.d.ts.map +1 -0
  35. package/dist/features/authz/role-binding.js +53 -0
  36. package/dist/features/company/company-hierarchy.service.d.ts +46 -0
  37. package/dist/features/company/company-hierarchy.service.d.ts.map +1 -0
  38. package/dist/features/company/company-hierarchy.service.js +100 -0
  39. package/dist/features/company/company.controller.d.ts +23 -5
  40. package/dist/features/company/company.controller.d.ts.map +1 -1
  41. package/dist/features/company/company.controller.js +47 -12
  42. package/dist/features/company/company.service.d.ts +28 -12
  43. package/dist/features/company/company.service.d.ts.map +1 -1
  44. package/dist/features/company/company.service.js +61 -14
  45. package/dist/features/cross-tenant/cross-tenant-grant.controller.d.ts.map +1 -1
  46. package/dist/features/cross-tenant/cross-tenant-grant.controller.js +2 -0
  47. package/dist/features/identity-feature.module.d.ts.map +1 -1
  48. package/dist/features/identity-feature.module.js +123 -6
  49. package/dist/features/membership/membership.controller.d.ts +6 -5
  50. package/dist/features/membership/membership.controller.d.ts.map +1 -1
  51. package/dist/features/membership/membership.controller.js +34 -22
  52. package/dist/features/membership/membership.service.d.ts +20 -14
  53. package/dist/features/membership/membership.service.d.ts.map +1 -1
  54. package/dist/features/membership/membership.service.js +62 -29
  55. package/dist/features/membership/posting-version-bump.d.ts +5 -0
  56. package/dist/features/membership/posting-version-bump.d.ts.map +1 -1
  57. package/dist/features/membership/posting.service.d.ts +20 -24
  58. package/dist/features/membership/posting.service.d.ts.map +1 -1
  59. package/dist/features/membership/posting.service.js +101 -64
  60. package/dist/features/org-structure/org-catalog.controller.d.ts.map +1 -1
  61. package/dist/features/org-structure/org-catalog.controller.js +2 -0
  62. package/dist/features/org-structure/org-catalog.service.d.ts.map +1 -1
  63. package/dist/features/org-structure/org-catalog.service.js +4 -1
  64. package/dist/features/policy/policy.controller.d.ts.map +1 -1
  65. package/dist/features/policy/policy.controller.js +2 -0
  66. package/dist/features/role/role.controller.d.ts +3 -2
  67. package/dist/features/role/role.controller.d.ts.map +1 -1
  68. package/dist/features/role/role.controller.js +15 -10
  69. package/dist/features/role/role.service.d.ts +16 -8
  70. package/dist/features/role/role.service.d.ts.map +1 -1
  71. package/dist/features/role/role.service.js +24 -10
  72. package/dist/features/scope/scope-closure.service.d.ts +44 -4
  73. package/dist/features/scope/scope-closure.service.d.ts.map +1 -1
  74. package/dist/features/scope/scope-closure.service.js +215 -82
  75. package/dist/features/service-account/service-account.controller.d.ts +63 -0
  76. package/dist/features/service-account/service-account.controller.d.ts.map +1 -0
  77. package/dist/features/service-account/service-account.controller.js +295 -0
  78. package/dist/features/service-account/service-account.service.d.ts +209 -0
  79. package/dist/features/service-account/service-account.service.d.ts.map +1 -0
  80. package/dist/features/service-account/service-account.service.js +528 -0
  81. package/dist/features/service-account/service-account.store.d.ts +91 -0
  82. package/dist/features/service-account/service-account.store.d.ts.map +1 -0
  83. package/dist/features/service-account/service-account.store.js +158 -0
  84. package/dist/features/share/share.controller.d.ts.map +1 -1
  85. package/dist/features/share/share.controller.js +2 -0
  86. package/dist/features/share/share.service.d.ts.map +1 -1
  87. package/dist/features/shared/identity-error.interceptor.d.ts +24 -0
  88. package/dist/features/shared/identity-error.interceptor.d.ts.map +1 -0
  89. package/dist/features/shared/identity-error.interceptor.js +82 -0
  90. package/dist/features/shared/identity-errors.d.ts +23 -0
  91. package/dist/features/shared/identity-errors.d.ts.map +1 -0
  92. package/dist/features/shared/identity-errors.js +41 -0
  93. package/dist/features/user/session-revocation-check.d.ts +11 -0
  94. package/dist/features/user/session-revocation-check.d.ts.map +1 -0
  95. package/dist/features/user/session-revocation-check.js +21 -0
  96. package/dist/features/user/user.controller.d.ts +11 -6
  97. package/dist/features/user/user.controller.d.ts.map +1 -1
  98. package/dist/features/user/user.controller.js +35 -17
  99. package/dist/features/user/user.service.d.ts +55 -14
  100. package/dist/features/user/user.service.d.ts.map +1 -1
  101. package/dist/features/user/user.service.js +143 -16
  102. package/dist/index.d.ts +1 -0
  103. package/dist/index.d.ts.map +1 -1
  104. package/dist/index.js +3 -1
  105. package/dist/membership-posting.object.d.ts.map +1 -1
  106. package/dist/membership-posting.object.js +4 -2
  107. package/dist/objects.d.ts.map +1 -1
  108. package/dist/objects.js +14 -3
  109. package/dist/role.object.d.ts.map +1 -1
  110. package/dist/role.object.js +3 -0
  111. package/dist/service-account.object.d.ts +31 -0
  112. package/dist/service-account.object.d.ts.map +1 -0
  113. package/dist/service-account.object.js +75 -0
  114. package/dist/user.object.d.ts.map +1 -1
  115. package/dist/user.object.js +3 -0
  116. package/migrations/1710000000018-CreateServiceAccount.cjs +53 -0
  117. package/migrations/1710000000019-AddSessionMustReenroll.cjs +35 -0
  118. package/package.json +4 -4
@@ -0,0 +1,295 @@
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.ServiceAccountController = void 0;
16
+ /**
17
+ * ServiceAccountController — the Service Accounts admin API (ADR-0033). Seven routes over
18
+ * {@link ServiceAccountService}, all `@Classified({ typeKey: 'service_account', … })` so the ONE
19
+ * `decide()` gates them (no handler-level root check): list/get need `read`, create needs `create`,
20
+ * edit needs `update`, and the three key routes (issue/rotate/revoke) need `manage_keys` — the more-
21
+ * sensitive credential capability, grantable separately from editing the account.
22
+ *
23
+ * ⛔ The actor + tenant are ALWAYS `req.principal` (the enforcement guard attached it) — the account
24
+ * is created in the ADMIN's own tenant, never a body-supplied one. ⛔ Own-tenant wall + ADR-0029
25
+ * ceiling live in the service; a cross-tenant id is 404 (no existence leak), an over-reach is 403.
26
+ * ⛔ Responses carry LABELS + names (never a raw secret; the id is a route handle only). The raw
27
+ * `ask_…` key is returned EXACTLY ONCE by issue/rotate.
28
+ */
29
+ const common_1 = require("@nestjs/common");
30
+ const guard_1 = require("@astram/guard");
31
+ const service_account_service_js_1 = require("./service-account.service.js");
32
+ const identity_error_interceptor_js_1 = require("../shared/identity-error.interceptor.js");
33
+ /** Project the resolved Principal to the AdminPrincipal the service consumes. */
34
+ function adminOf(req) {
35
+ const p = req.principal;
36
+ return {
37
+ id: p.id,
38
+ tenantId: p.tenantId,
39
+ scopeSubtreeNodeIds: p.scopeSubtreeNodeIds ?? [],
40
+ // No roleIds: the principal's are a login-time credential snapshot. The service resolves the
41
+ // admin's in-effect roles itself (HeldRoleResolver → readRoleIdsForTenant).
42
+ isRoot: p.isRoot === true,
43
+ };
44
+ }
45
+ /** Map a service error to the right HTTP status (404 / 403 / 400); rethrow the rest (→ 503). */
46
+ function mapError(e) {
47
+ if (e instanceof service_account_service_js_1.ServiceAccountNotFoundError)
48
+ throw new common_1.NotFoundException(e.message);
49
+ if (e instanceof service_account_service_js_1.ServiceAccountCeilingError)
50
+ throw new common_1.ForbiddenException(e.message);
51
+ if (e instanceof service_account_service_js_1.ServiceAccountInputError)
52
+ throw new common_1.BadRequestException(e.message);
53
+ if (e instanceof service_account_service_js_1.ServiceAccountConflictError)
54
+ throw new common_1.ConflictException(e.message);
55
+ throw e;
56
+ }
57
+ /** Narrow the OPTIONAL string[] fields of a body (scope/roles) — reject a non-string-array. */
58
+ function optStringArray(v, field) {
59
+ if (v === undefined)
60
+ return undefined;
61
+ if (!Array.isArray(v) || v.some((x) => typeof x !== 'string')) {
62
+ throw new common_1.BadRequestException(`${field} must be an array of strings`);
63
+ }
64
+ return v;
65
+ }
66
+ /**
67
+ * Narrow a key-issue/rotate body to its expiry choice (ADR-0033 §4): `expiresAtMs` (epoch-ms) or
68
+ * `neverExpires: true`. Shape errors are 400 here; "neither", "both" and "not in the future" are
69
+ * decided by the service (it owns the clock) and also answer 400.
70
+ */
71
+ function expiryOf(body) {
72
+ const out = {};
73
+ const v = body.expiresAtMs;
74
+ if (v !== undefined && v !== null) {
75
+ if (typeof v !== 'number' || !Number.isFinite(v) || v <= 0) {
76
+ throw new common_1.BadRequestException('expiresAtMs must be a positive epoch-ms number');
77
+ }
78
+ out.expiresAtMs = v;
79
+ }
80
+ if (body.neverExpires !== undefined) {
81
+ if (typeof body.neverExpires !== 'boolean') {
82
+ throw new common_1.BadRequestException('neverExpires must be true or false');
83
+ }
84
+ out.neverExpires = body.neverExpires;
85
+ }
86
+ return out;
87
+ }
88
+ let ServiceAccountController = class ServiceAccountController {
89
+ svc;
90
+ constructor(svc) {
91
+ this.svc = svc;
92
+ }
93
+ /** List the caller's own-tenant service accounts (by label). Gated `service_account:read`. */
94
+ async list(req) {
95
+ try {
96
+ return { accounts: await this.svc.list(adminOf(req)) };
97
+ }
98
+ catch (e) {
99
+ mapError(e);
100
+ }
101
+ }
102
+ /**
103
+ * What the caller may grant an agent — the create/edit picker source (Phase 4). Gated
104
+ * `service_account:create` (only someone who can create agents needs the list). Computed from the
105
+ * SAME ceiling the create/update write check enforces: `{ roles: [{id,name}], scopeNodes:
106
+ * [{id,name,parentId|null}] }`. ⚠ Declared BEFORE `service-accounts/:id` so the static path wins.
107
+ */
108
+ async assignable(req) {
109
+ try {
110
+ return await this.svc.assignable(adminOf(req));
111
+ }
112
+ catch (e) {
113
+ mapError(e);
114
+ }
115
+ }
116
+ /** Fetch one own-tenant account. Gated `service_account:read`. Cross-tenant/unknown → 404. */
117
+ async get(req, id) {
118
+ try {
119
+ return { account: await this.svc.get(adminOf(req), id) };
120
+ }
121
+ catch (e) {
122
+ mapError(e);
123
+ }
124
+ }
125
+ /**
126
+ * Create an account in the caller's own tenant. Gated `service_account:create`.
127
+ * Body: `{ label, scopeSubtreeNodeIds?, roleIds? }`. Scope/roles must pass the ADR-0029 ceiling.
128
+ */
129
+ async create(req, body) {
130
+ const b = (body ?? {});
131
+ if (typeof b.label !== 'string')
132
+ throw new common_1.BadRequestException('label is required');
133
+ const scope = optStringArray(b.scopeSubtreeNodeIds, 'scopeSubtreeNodeIds');
134
+ const roles = optStringArray(b.roleIds, 'roleIds');
135
+ try {
136
+ const account = await this.svc.create(adminOf(req), {
137
+ label: b.label,
138
+ ...(scope !== undefined ? { scopeSubtreeNodeIds: scope } : {}),
139
+ ...(roles !== undefined ? { roleIds: roles } : {}),
140
+ });
141
+ return { account };
142
+ }
143
+ catch (e) {
144
+ mapError(e);
145
+ }
146
+ }
147
+ /**
148
+ * Edit an account (label/scope/roles/status). Gated `service_account:update`. Own-tenant only;
149
+ * scope/roles re-checked against the ceiling (an edit cannot escalate).
150
+ */
151
+ async update(req, id, body) {
152
+ const b = (body ?? {});
153
+ if (b.label !== undefined && typeof b.label !== 'string') {
154
+ throw new common_1.BadRequestException('label must be a string');
155
+ }
156
+ if (b.status !== undefined && b.status !== 'active' && b.status !== 'disabled') {
157
+ throw new common_1.BadRequestException("status must be 'active' or 'disabled'");
158
+ }
159
+ const scope = optStringArray(b.scopeSubtreeNodeIds, 'scopeSubtreeNodeIds');
160
+ const roles = optStringArray(b.roleIds, 'roleIds');
161
+ try {
162
+ const account = await this.svc.update(adminOf(req), id, {
163
+ ...(b.label !== undefined ? { label: b.label } : {}),
164
+ ...(scope !== undefined ? { scopeSubtreeNodeIds: scope } : {}),
165
+ ...(roles !== undefined ? { roleIds: roles } : {}),
166
+ ...(b.status !== undefined ? { status: b.status } : {}),
167
+ });
168
+ return { account };
169
+ }
170
+ catch (e) {
171
+ mapError(e);
172
+ }
173
+ }
174
+ /**
175
+ * Issue a NEW key for an account. Gated `service_account:manage_keys`. Body: `{ expiresAtMs }`
176
+ * (a future epoch-ms) or `{ neverExpires: true }` — one of the two is required (400 otherwise).
177
+ * Returns the raw `ask_…` key EXACTLY ONCE — copy it now, it is never retrievable again.
178
+ */
179
+ async issueKey(req, id, body) {
180
+ const expiry = expiryOf((body ?? {}));
181
+ try {
182
+ const key = await this.svc.issueKey(adminOf(req), id, expiry);
183
+ return { key };
184
+ }
185
+ catch (e) {
186
+ mapError(e);
187
+ }
188
+ }
189
+ /**
190
+ * Rotate an account's key. Gated `service_account:manage_keys`. The previous key stops
191
+ * authenticating; the new raw key is returned EXACTLY ONCE. Same expiry rule as issue.
192
+ */
193
+ async rotateKey(req, id, body) {
194
+ const expiry = expiryOf((body ?? {}));
195
+ try {
196
+ const key = await this.svc.rotateKey(adminOf(req), id, expiry);
197
+ return { key };
198
+ }
199
+ catch (e) {
200
+ mapError(e);
201
+ }
202
+ }
203
+ /**
204
+ * Revoke ALL of an account's keys. Gated `service_account:manage_keys`. Fail-closed on the next
205
+ * request; idempotent.
206
+ */
207
+ async revokeKeys(req, id) {
208
+ try {
209
+ await this.svc.revokeKeys(adminOf(req), id);
210
+ return { success: true };
211
+ }
212
+ catch (e) {
213
+ mapError(e);
214
+ }
215
+ }
216
+ };
217
+ exports.ServiceAccountController = ServiceAccountController;
218
+ __decorate([
219
+ (0, common_1.Get)('service-accounts'),
220
+ (0, guard_1.Classified)({ typeKey: 'service_account', action: 'read' }),
221
+ __param(0, (0, common_1.Req)()),
222
+ __metadata("design:type", Function),
223
+ __metadata("design:paramtypes", [Object]),
224
+ __metadata("design:returntype", Promise)
225
+ ], ServiceAccountController.prototype, "list", null);
226
+ __decorate([
227
+ (0, common_1.Get)('service-accounts/assignable'),
228
+ (0, guard_1.Classified)({ typeKey: 'service_account', action: 'create' }),
229
+ __param(0, (0, common_1.Req)()),
230
+ __metadata("design:type", Function),
231
+ __metadata("design:paramtypes", [Object]),
232
+ __metadata("design:returntype", Promise)
233
+ ], ServiceAccountController.prototype, "assignable", null);
234
+ __decorate([
235
+ (0, common_1.Get)('service-accounts/:id'),
236
+ (0, guard_1.Classified)({ typeKey: 'service_account', action: 'read' }),
237
+ __param(0, (0, common_1.Req)()),
238
+ __param(1, (0, common_1.Param)('id')),
239
+ __metadata("design:type", Function),
240
+ __metadata("design:paramtypes", [Object, String]),
241
+ __metadata("design:returntype", Promise)
242
+ ], ServiceAccountController.prototype, "get", null);
243
+ __decorate([
244
+ (0, common_1.Post)('service-accounts'),
245
+ (0, guard_1.Classified)({ typeKey: 'service_account', action: 'create' }),
246
+ __param(0, (0, common_1.Req)()),
247
+ __param(1, (0, common_1.Body)()),
248
+ __metadata("design:type", Function),
249
+ __metadata("design:paramtypes", [Object, Object]),
250
+ __metadata("design:returntype", Promise)
251
+ ], ServiceAccountController.prototype, "create", null);
252
+ __decorate([
253
+ (0, common_1.Patch)('service-accounts/:id'),
254
+ (0, guard_1.Classified)({ typeKey: 'service_account', action: 'update' }),
255
+ __param(0, (0, common_1.Req)()),
256
+ __param(1, (0, common_1.Param)('id')),
257
+ __param(2, (0, common_1.Body)()),
258
+ __metadata("design:type", Function),
259
+ __metadata("design:paramtypes", [Object, String, Object]),
260
+ __metadata("design:returntype", Promise)
261
+ ], ServiceAccountController.prototype, "update", null);
262
+ __decorate([
263
+ (0, common_1.Post)('service-accounts/:id/keys'),
264
+ (0, guard_1.Classified)({ typeKey: 'service_account', action: 'manage_keys' }),
265
+ __param(0, (0, common_1.Req)()),
266
+ __param(1, (0, common_1.Param)('id')),
267
+ __param(2, (0, common_1.Body)()),
268
+ __metadata("design:type", Function),
269
+ __metadata("design:paramtypes", [Object, String, Object]),
270
+ __metadata("design:returntype", Promise)
271
+ ], ServiceAccountController.prototype, "issueKey", null);
272
+ __decorate([
273
+ (0, common_1.Post)('service-accounts/:id/keys/rotate'),
274
+ (0, guard_1.Classified)({ typeKey: 'service_account', action: 'manage_keys' }),
275
+ __param(0, (0, common_1.Req)()),
276
+ __param(1, (0, common_1.Param)('id')),
277
+ __param(2, (0, common_1.Body)()),
278
+ __metadata("design:type", Function),
279
+ __metadata("design:paramtypes", [Object, String, Object]),
280
+ __metadata("design:returntype", Promise)
281
+ ], ServiceAccountController.prototype, "rotateKey", null);
282
+ __decorate([
283
+ (0, common_1.Post)('service-accounts/:id/keys/revoke'),
284
+ (0, guard_1.Classified)({ typeKey: 'service_account', action: 'manage_keys' }),
285
+ __param(0, (0, common_1.Req)()),
286
+ __param(1, (0, common_1.Param)('id')),
287
+ __metadata("design:type", Function),
288
+ __metadata("design:paramtypes", [Object, String]),
289
+ __metadata("design:returntype", Promise)
290
+ ], ServiceAccountController.prototype, "revokeKeys", null);
291
+ exports.ServiceAccountController = ServiceAccountController = __decorate([
292
+ (0, common_1.Controller)('identity'),
293
+ (0, common_1.UseInterceptors)(identity_error_interceptor_js_1.IdentityErrorInterceptor),
294
+ __metadata("design:paramtypes", [service_account_service_js_1.ServiceAccountService])
295
+ ], ServiceAccountController);
@@ -0,0 +1,209 @@
1
+ import type { PasswordHasher, ApiKeyStore } from '@astram/core';
2
+ import type { ServiceAccountStore, RoleOption, ScopeNodeOption } from './service-account.store.js';
3
+ /** The account id was not in the caller's subtree (unknown or cross-tenant) → 404 (no leak). */
4
+ export declare class ServiceAccountNotFoundError extends Error {
5
+ constructor();
6
+ }
7
+ /** The requested scope/roles exceed the caller's own authority (ADR-0029) → 403. */
8
+ export declare class ServiceAccountCeilingError extends Error {
9
+ constructor(message: string);
10
+ }
11
+ /** The account changed between this edit's read and its write (a concurrent edit) → 409. */
12
+ export declare class ServiceAccountConflictError extends Error {
13
+ constructor();
14
+ }
15
+ /** A malformed request (empty label, no fields to update, etc.) → 400. */
16
+ export declare class ServiceAccountInputError extends Error {
17
+ constructor(message: string);
18
+ }
19
+ /** An account view for the API (never carries a raw secret; `id` is a handle, shown by label). */
20
+ export interface ServiceAccountView {
21
+ readonly id: string;
22
+ readonly label: string;
23
+ readonly scopeSubtreeNodeIds: readonly string[];
24
+ /** Resolved company names, same order as `scopeSubtreeNodeIds` (ADR-0012 — show these). */
25
+ readonly scopeLabels: readonly string[];
26
+ readonly roleIds: readonly string[];
27
+ /** Resolved role names, same order as `roleIds` (ADR-0012 — show these). */
28
+ readonly roleLabels: readonly string[];
29
+ readonly status: string;
30
+ readonly createdByName: string | null;
31
+ readonly createdAtMs: number;
32
+ readonly updatedAtMs: number | null;
33
+ }
34
+ /** The ONE-TIME key reveal returned by issue/rotate — the only time the raw key exists. */
35
+ export interface IssuedKey {
36
+ readonly apiKey: string;
37
+ readonly accountLabel: string;
38
+ readonly version: number;
39
+ readonly expiresAtMs: number | null;
40
+ }
41
+ /** A minimal audit sink (best-effort). The module adapts it to the shield_audit sink. */
42
+ export interface ServiceAccountAudit {
43
+ record(event: {
44
+ action: string;
45
+ actorId: string;
46
+ tenantId: string;
47
+ occurredAtMs: number;
48
+ attributes: Readonly<Record<string, unknown>>;
49
+ }): Promise<void>;
50
+ }
51
+ /** Resolve a principal id → display name (no raw uuid in the view). Returns null when unknown. */
52
+ export type NameResolver = (id: string) => Promise<string | null>;
53
+ /** The caller (an admin) as the service needs it — the enforcement guard's resolved principal. */
54
+ export interface AdminPrincipal {
55
+ readonly id: string;
56
+ readonly tenantId: string;
57
+ readonly scopeSubtreeNodeIds: readonly string[];
58
+ readonly isRoot: boolean;
59
+ }
60
+ /**
61
+ * The roles an admin is IN EFFECT holding in a tenant — the ceiling's role source. Wired to
62
+ * `readRoleIdsForTenant` (the single source shared with gate 5 and `GrantCeiling`). Deliberately
63
+ * NOT the request principal's `roleIds`: that is the credential's login-time snapshot, which goes
64
+ * stale when postings change (a demoted admin could otherwise mint an agent with the role they lost).
65
+ */
66
+ export type HeldRoleResolver = (principalId: string, tenantId: string) => Promise<readonly string[]>;
67
+ /**
68
+ * "What may I grant" — the ADR-0029 ceiling of ONE admin, computed in ONE place. Both the write
69
+ * check ({@link ServiceAccountService} create/update) and the picker source
70
+ * ({@link ServiceAccountService.assignable}) read it, so the picker can never offer what the write
71
+ * would reject. Root (strict `isRoot === true`) spans everything; a non-root admin may grant only
72
+ * nodes in their reachable subtree (their tenant when the subtree is empty) and roles they hold.
73
+ */
74
+ export interface ServiceAccountCeiling {
75
+ readonly isRoot: boolean;
76
+ readonly nodeIds: ReadonlySet<string>;
77
+ readonly roleIds: ReadonlySet<string>;
78
+ }
79
+ export declare function ceilingOf(admin: AdminPrincipal, heldRoleIds: readonly string[]): ServiceAccountCeiling;
80
+ export declare function ceilingAllowsNode(c: ServiceAccountCeiling, nodeId: string): boolean;
81
+ export declare function ceilingAllowsRole(c: ServiceAccountCeiling, roleId: string): boolean;
82
+ /** What the caller may grant an agent (the picker source) — names, never shown as ids. */
83
+ export interface AssignableAccess {
84
+ readonly roles: readonly RoleOption[];
85
+ readonly scopeNodes: readonly ScopeNodeOption[];
86
+ }
87
+ /** Key expiry on issue/rotate: a future `expiresAtMs`, or an explicit `neverExpires: true`. */
88
+ export interface KeyExpiryOptions {
89
+ readonly expiresAtMs?: number;
90
+ readonly neverExpires?: boolean;
91
+ }
92
+ export declare class ServiceAccountService {
93
+ private readonly accounts;
94
+ private readonly keys;
95
+ private readonly hasher;
96
+ private readonly audit;
97
+ private readonly resolveName;
98
+ private readonly heldRoles;
99
+ private readonly now;
100
+ constructor(accounts: ServiceAccountStore, keys: ApiKeyStore & {
101
+ putVersion(input: {
102
+ accountId: string;
103
+ tenantId: string;
104
+ scopeSubtreeNodeIds: readonly string[];
105
+ roleIds: readonly string[];
106
+ secretHash: string;
107
+ version: number;
108
+ expiresAtMs?: number;
109
+ createdAtMs: number;
110
+ }): Promise<void>;
111
+ currentVersion(accountId: string): Promise<number>;
112
+ revoke(accountId: string, atMs: number): Promise<void>;
113
+ }, hasher: PasswordHasher, audit: ServiceAccountAudit, resolveName: NameResolver, heldRoles: HeldRoleResolver, now?: () => number);
114
+ /**
115
+ * Rows → views, resolving role + company NAMES in two batched reads (no N+1). Labels follow the
116
+ * id order; an id that no longer resolves gets a fallback label, never the id (ADR-0012).
117
+ */
118
+ private toViews;
119
+ /**
120
+ * The caller's ceiling with roles resolved LIVE ({@link HeldRoleResolver}). Root needs no role
121
+ * read. A resolver failure propagates (→ 503): fail closed, never an empty-or-stale ceiling.
122
+ */
123
+ private ceilingFor;
124
+ /**
125
+ * Enforce the ADR-0029 ceiling ({@link ceilingOf}): every requested node must be within the
126
+ * ceiling's subtree and every requested role one the admin holds. Root bypasses. On violation
127
+ * throws a 403 — never with an id (ADR-0012), and NAMING an offender only when it is already on
128
+ * the agent being edited (`onAgent`, whose labels the caller sees anyway). Any other offender is
129
+ * described generically: the role/company catalog reads are not caller-scoped, so resolving a
130
+ * name for an arbitrary id would turn the 403 into a cross-tenant name/existence oracle.
131
+ */
132
+ private assertWithinCeiling;
133
+ /**
134
+ * "the role ‘A’", "‘A’ and 2 other roles", "a role" … — names only offenders already on the agent.
135
+ */
136
+ private describeOffenders;
137
+ /**
138
+ * Resolve names for audit (ids already authorised for this caller: within ceiling, or on the
139
+ * agent). Best-effort like the audit itself: it runs AFTER the write, so a lookup failure falls
140
+ * back to placeholder names instead of failing a write that already happened.
141
+ */
142
+ private auditNames;
143
+ /**
144
+ * What the caller may grant (the picker source for create/edit), computed from the SAME
145
+ * {@link ceilingOf} the write check enforces. Root → every role + every company; non-root → the
146
+ * roles they hold + the nodes of their reachable subtree. Nodes come back tree-ordered with a
147
+ * `parentId` only when the parent is also assignable.
148
+ */
149
+ assignable(admin: AdminPrincipal): Promise<AssignableAccess>;
150
+ /** List the admin's own-tenant accounts (by label; ids are handles). */
151
+ list(admin: AdminPrincipal): Promise<ServiceAccountView[]>;
152
+ /** Fetch one own-tenant account; cross-tenant/unknown → 404. */
153
+ get(admin: AdminPrincipal, id: string): Promise<ServiceAccountView>;
154
+ /**
155
+ * Create an account. `tenantId` is the admin's (server-stamped) — an account is created in the
156
+ * admin's OWN tenant only (a non-root admin cannot pick another tenant). Scope defaults to the
157
+ * admin's whole subtree when omitted; whatever is requested must pass the ceiling.
158
+ */
159
+ create(admin: AdminPrincipal, input: {
160
+ label: string;
161
+ scopeSubtreeNodeIds?: readonly string[];
162
+ roleIds?: readonly string[];
163
+ }): Promise<ServiceAccountView>;
164
+ /**
165
+ * Edit an account's label/scope/roles/status (own-tenant; ceiling re-checked).
166
+ *
167
+ * Scope/roles semantics (Phase 4, confirmed 2026-10-08):
168
+ * - `scopeSubtreeNodeIds: []` → 400 (an agent needs at least one scope node).
169
+ * - What is REQUESTED must be ⊆ the editor's ceiling, else 403 — whatever the request shape
170
+ * (duplicates, mixed lists, or re-sending an item the agent already has but the editor can't
171
+ * grant). Nothing is written on a 403.
172
+ * - NON-ROOT: the agent's EXISTING items outside the editor's ceiling are PRESERVED —
173
+ * stored = requested ∪ (existing \ ceiling). A lower-ceiling admin can't strip access they could
174
+ * never grant back; the preserved items come from the stored row, never the request, so the
175
+ * merge can't add anything new. ROOT replaces with exactly what it sends.
176
+ */
177
+ update(admin: AdminPrincipal, id: string, patch: {
178
+ label?: string;
179
+ scopeSubtreeNodeIds?: readonly string[];
180
+ roleIds?: readonly string[];
181
+ status?: 'active' | 'disabled';
182
+ }): Promise<ServiceAccountView>;
183
+ /**
184
+ * Issue a NEW key for an account (own-tenant). Mints `ask_<id>_<secret>`, hashes the secret, and
185
+ * persists the hash as the next version — carrying the ACCOUNT's current scope/roles onto the key
186
+ * (so the key's Principal reflects the account as it stands). Returns the raw key ONCE.
187
+ * ADR-0033 §4 (security hardening): the caller must give a FUTURE `expiresAtMs`, or opt in to a
188
+ * non-expiring key explicitly with `neverExpires: true` — otherwise 400, nothing minted.
189
+ */
190
+ issueKey(admin: AdminPrincipal, id: string, opts?: KeyExpiryOptions): Promise<IssuedKey>;
191
+ /**
192
+ * The key's expiry from the request: a future epoch-ms, or `undefined` when the caller said
193
+ * `neverExpires: true`. Neither, both, or a past/invalid time → ServiceAccountInputError (400).
194
+ */
195
+ private resolveExpiry;
196
+ /**
197
+ * Rotate an account's key: mint a new version (the old version stops being the current one —
198
+ * `getByAccountId` returns the highest, so the previous key no longer authenticates). Returns the
199
+ * new raw key ONCE.
200
+ */
201
+ rotateKey(admin: AdminPrincipal, id: string, opts?: KeyExpiryOptions): Promise<IssuedKey>;
202
+ /** Revoke ALL of an account's keys (own-tenant) — fail-closed on the next request. Idempotent. */
203
+ revokeKeys(admin: AdminPrincipal, id: string): Promise<void>;
204
+ /** Shared mint path for issue + rotate: version bump, hash, putVersion, audit, one-time reveal. */
205
+ private mintFor;
206
+ /** Best-effort audit — a sink throw must never fail a legitimate operation. */
207
+ private safeAudit;
208
+ }
209
+ //# sourceMappingURL=service-account.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-account.service.d.ts","sourceRoot":"","sources":["../../../src/features/service-account/service-account.service.ts"],"names":[],"mappings":"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"}