@things-factory/auth-base 10.1.188 → 10.1.198

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 (105) hide show
  1. package/dist-client/tsconfig.tsbuildinfo +1 -1
  2. package/dist-server/controllers/signin.d.ts +5 -0
  3. package/dist-server/controllers/signin.js +25 -0
  4. package/dist-server/controllers/signin.js.map +1 -1
  5. package/dist-server/controllers/verification.d.ts +5 -1
  6. package/dist-server/controllers/verification.js +11 -6
  7. package/dist-server/controllers/verification.js.map +1 -1
  8. package/dist-server/index.js +7 -0
  9. package/dist-server/index.js.map +1 -1
  10. package/dist-server/middlewares/audit-scope-middleware.d.ts +7 -0
  11. package/dist-server/middlewares/audit-scope-middleware.js +15 -0
  12. package/dist-server/middlewares/audit-scope-middleware.js.map +1 -0
  13. package/dist-server/middlewares/authenticate-401-middleware.js +4 -0
  14. package/dist-server/middlewares/authenticate-401-middleware.js.map +1 -1
  15. package/dist-server/middlewares/index.js +3 -0
  16. package/dist-server/middlewares/index.js.map +1 -1
  17. package/dist-server/role-tasks.d.ts +2 -0
  18. package/dist-server/role-tasks.js +20 -0
  19. package/dist-server/role-tasks.js.map +1 -0
  20. package/dist-server/router/auth-checkin-router.js +3 -5
  21. package/dist-server/router/auth-checkin-router.js.map +1 -1
  22. package/dist-server/router/auth-public-process-router.js +26 -14
  23. package/dist-server/router/auth-public-process-router.js.map +1 -1
  24. package/dist-server/router/site-root-router.d.ts +2 -0
  25. package/dist-server/router/site-root-router.js +2 -0
  26. package/dist-server/router/site-root-router.js.map +1 -1
  27. package/dist-server/service/access-request/index.d.ts +1 -1
  28. package/dist-server/service/audit/audit-action.d.ts +36 -0
  29. package/dist-server/service/audit/audit-action.js +137 -0
  30. package/dist-server/service/audit/audit-action.js.map +1 -0
  31. package/dist-server/service/audit/audit-kinds.d.ts +19 -0
  32. package/dist-server/service/audit/audit-kinds.js +70 -0
  33. package/dist-server/service/audit/audit-kinds.js.map +1 -0
  34. package/dist-server/service/audit/audit-query.d.ts +11 -0
  35. package/dist-server/service/audit/audit-query.js +295 -0
  36. package/dist-server/service/audit/audit-query.js.map +1 -0
  37. package/dist-server/service/audit/audit-recorder.d.ts +77 -0
  38. package/dist-server/service/audit/audit-recorder.js +247 -0
  39. package/dist-server/service/audit/audit-recorder.js.map +1 -0
  40. package/dist-server/service/audit/audit-request.d.ts +28 -0
  41. package/dist-server/service/audit/audit-request.js +96 -0
  42. package/dist-server/service/audit/audit-request.js.map +1 -0
  43. package/dist-server/service/audit/audit-retention.d.ts +12 -0
  44. package/dist-server/service/audit/audit-retention.js +42 -0
  45. package/dist-server/service/audit/audit-retention.js.map +1 -0
  46. package/dist-server/service/audit/audit-subscriber.d.ts +39 -0
  47. package/dist-server/service/audit/audit-subscriber.js +191 -0
  48. package/dist-server/service/audit/audit-subscriber.js.map +1 -0
  49. package/dist-server/service/audit/audit-target.d.ts +19 -0
  50. package/dist-server/service/audit/audit-target.js +46 -0
  51. package/dist-server/service/audit/audit-target.js.map +1 -0
  52. package/dist-server/service/audit/audit-types.d.ts +44 -0
  53. package/dist-server/service/audit/audit-types.js +161 -0
  54. package/dist-server/service/audit/audit-types.js.map +1 -0
  55. package/dist-server/service/audit/index.d.ts +6 -0
  56. package/dist-server/service/audit/index.js +11 -0
  57. package/dist-server/service/audit/index.js.map +1 -0
  58. package/dist-server/service/domain-owner/domain-owner-mutation.js +3 -0
  59. package/dist-server/service/domain-owner/domain-owner-mutation.js.map +1 -1
  60. package/dist-server/service/index.d.ts +9 -2
  61. package/dist-server/service/index.js +19 -4
  62. package/dist-server/service/index.js.map +1 -1
  63. package/dist-server/service/open-fields.js +1 -1
  64. package/dist-server/service/open-fields.js.map +1 -1
  65. package/dist-server/service/privilege/privilege-directive.js +25 -1
  66. package/dist-server/service/privilege/privilege-directive.js.map +1 -1
  67. package/dist-server/service/role/role-mutation.js +8 -1
  68. package/dist-server/service/role/role-mutation.js.map +1 -1
  69. package/dist-server/service/role-template/index.d.ts +1 -1
  70. package/dist-server/service/role-template/role-task.d.ts +74 -0
  71. package/dist-server/service/role-template/role-task.js +113 -0
  72. package/dist-server/service/role-template/role-task.js.map +1 -0
  73. package/dist-server/service/role-template/role-template-gap-resolver.d.ts +9 -5
  74. package/dist-server/service/role-template/role-template-gap-resolver.js +41 -13
  75. package/dist-server/service/role-template/role-template-gap-resolver.js.map +1 -1
  76. package/dist-server/service/role-template/role-template-mutation.js +10 -1
  77. package/dist-server/service/role-template/role-template-mutation.js.map +1 -1
  78. package/dist-server/service/role-template/role-template-query.js +4 -2
  79. package/dist-server/service/role-template/role-template-query.js.map +1 -1
  80. package/dist-server/service/role-template/role-template-types.d.ts +24 -0
  81. package/dist-server/service/role-template/role-template-types.js +63 -1
  82. package/dist-server/service/role-template/role-template-types.js.map +1 -1
  83. package/dist-server/service/role-template/role-template.d.ts +63 -10
  84. package/dist-server/service/role-template/role-template.js +92 -18
  85. package/dist-server/service/role-template/role-template.js.map +1 -1
  86. package/dist-server/tsconfig.tsbuildinfo +1 -1
  87. package/dist-server/utils/check-permission.d.ts +6 -0
  88. package/dist-server/utils/check-permission.js +23 -8
  89. package/dist-server/utils/check-permission.js.map +1 -1
  90. package/package.json +3 -2
  91. package/tests/activate-page-case.test.ts +53 -0
  92. package/tests/audit-db.test.ts +394 -0
  93. package/tests/auth-base-role-tasks.test.ts +58 -0
  94. package/tests/list-inheritance-db.test.ts +0 -7
  95. package/tests/mail-request-answers-db.test.ts +139 -0
  96. package/tests/open-doors.test.ts +2 -2
  97. package/tests/refusal-cause.test.ts +1 -1
  98. package/tests/role-task.test.ts +168 -0
  99. package/tests/role-template-gap-db.test.ts +77 -2
  100. package/translations/en.json +16 -3
  101. package/translations/es.json +16 -3
  102. package/translations/ja.json +16 -3
  103. package/translations/ko.json +16 -3
  104. package/translations/ms.json +16 -3
  105. package/translations/zh.json +16 -3
@@ -0,0 +1,191 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.auditSubscribers = exports.WebAuthCredentialAuditSubscriber = exports.InvitationAuditSubscriber = exports.DomainOwnerAuditSubscriber = exports.UserAuditSubscriber = exports.GrantAuditSubscriber = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const typeorm_1 = require("typeorm");
6
+ const domain_owner_js_1 = require("../domain-owner/domain-owner.js");
7
+ const invitation_js_1 = require("../invitation/invitation.js");
8
+ const privilege_js_1 = require("../privilege/privilege.js");
9
+ const role_js_1 = require("../role/role.js");
10
+ const user_js_1 = require("../user/user.js");
11
+ const web_auth_credential_js_1 = require("../web-auth-credential/web-auth-credential.js");
12
+ const audit_recorder_js_1 = require("./audit-recorder.js");
13
+ /**
14
+ * ── Who may do what, recorded where it is saved (ADR-0141 §3.3 · §4-1) ───────
15
+ *
16
+ * A role is granted in a dozen places - the user screen, an access request, an invitation, a
17
+ * template, Office's role plan. Each saves through TypeORM, so the change is caught here, once, and
18
+ * written in the same transaction as the save: the grant and its record stand or fall together.
19
+ *
20
+ * A write that bypasses the entity manager's events (a query builder, `delete()`) is not seen here;
21
+ * those few paths record themselves.
22
+ */
23
+ const idOf = (one) => (typeof one === 'string' ? one : one?.id);
24
+ /** The id a junction row holds for `target`, read from what the event carries. */
25
+ function junctionIdOf(event, target) {
26
+ const source = event.entityId ?? event.entity ?? event.databaseEntity;
27
+ if (!source)
28
+ return undefined;
29
+ const column = event.metadata.columns.find(one => one.referencedColumn?.entityMetadata.target === target);
30
+ if (!column)
31
+ return undefined;
32
+ const value = column.getEntityValue(source) ?? source[column.propertyName] ?? source[column.databaseName];
33
+ return idOf(value);
34
+ }
35
+ /** The domain of a role, read in the same transaction - a role being deleted has none left to read. */
36
+ async function domainOfRole(event, roleId) {
37
+ const role = await event.manager.getRepository(role_js_1.Role).findOne({ where: { id: roleId }, relations: ['domain'] });
38
+ return role?.domain?.id;
39
+ }
40
+ /**
41
+ * ── Grants are junction rows ──────────────────────────────────────────────
42
+ *
43
+ * A role given to a user is a `users_roles` row; a privilege given to a role, a `roles_privileges`
44
+ * row. When only those change, TypeORM updates no entity and sends no update event - it inserts and
45
+ * removes junction rows, and those it does announce. So grants are read here, from the junction, for
46
+ * every path that saves a user's roles or a role's privileges, a new user's or role's included.
47
+ *
48
+ * This listens to every entity (a junction has no class to name) and leaves at once on anything
49
+ * that is not one of the two junctions.
50
+ */
51
+ let GrantAuditSubscriber = class GrantAuditSubscriber {
52
+ async afterInsert(event) {
53
+ await this.record(event, 'insert');
54
+ }
55
+ async afterRemove(event) {
56
+ await this.record(event, 'remove');
57
+ }
58
+ async record(event, change) {
59
+ if (!event.metadata.isJunction)
60
+ return;
61
+ const roleId = junctionIdOf(event, role_js_1.Role);
62
+ const userId = junctionIdOf(event, user_js_1.User);
63
+ const privilegeId = junctionIdOf(event, privilege_js_1.Privilege);
64
+ if (!roleId)
65
+ return;
66
+ const domainId = await domainOfRole(event, roleId);
67
+ const where = { manager: event.manager, ...(domainId ? { domainId } : {}) };
68
+ if (userId) {
69
+ await (0, audit_recorder_js_1.recordAuditAction)({
70
+ kind: change === 'insert' ? 'role.grant' : 'role.revoke',
71
+ targetKind: 'User',
72
+ targetIds: [userId],
73
+ targetRole: 'handled',
74
+ objectKind: 'Role',
75
+ objectId: roleId
76
+ }, where);
77
+ }
78
+ else if (privilegeId) {
79
+ await (0, audit_recorder_js_1.recordAuditAction)({
80
+ kind: change === 'insert' ? 'role.privilege.add' : 'role.privilege.remove',
81
+ targetKind: 'Role',
82
+ targetIds: [roleId],
83
+ targetRole: 'handled',
84
+ objectKind: 'Privilege',
85
+ objectId: privilegeId
86
+ }, where);
87
+ }
88
+ }
89
+ };
90
+ exports.GrantAuditSubscriber = GrantAuditSubscriber;
91
+ exports.GrantAuditSubscriber = GrantAuditSubscriber = tslib_1.__decorate([
92
+ (0, typeorm_1.EventSubscriber)()
93
+ ], GrantAuditSubscriber);
94
+ let UserAuditSubscriber = class UserAuditSubscriber {
95
+ listenTo() {
96
+ return user_js_1.User;
97
+ }
98
+ async afterUpdate(event) {
99
+ const user = event.entity;
100
+ const before = event.databaseEntity;
101
+ const userId = user?.id ?? before?.id;
102
+ if (!user || !before || !userId)
103
+ return;
104
+ const target = { targetKind: 'User', targetIds: [userId], targetRole: 'handled' };
105
+ if (user.status !== undefined && user.status !== before.status) {
106
+ await (0, audit_recorder_js_1.recordAuditAction)({ kind: 'user.status', ...target, detail: { from: before.status ?? null, to: user.status } }, { manager: event.manager });
107
+ }
108
+ if (user.password !== undefined && user.password !== before.password) {
109
+ /* One's own change, or somebody else setting it - the actor tells which. */
110
+ const actorId = (0, audit_recorder_js_1.currentAuditScope)()?.context?.state?.user?.id;
111
+ const kind = actorId && actorId !== userId ? 'password.reset-by-admin' : 'password.change';
112
+ await (0, audit_recorder_js_1.recordAuditAction)({ kind, ...target }, { manager: event.manager });
113
+ }
114
+ if (user.credentialSerial !== undefined && user.credentialSerial !== before.credentialSerial) {
115
+ await (0, audit_recorder_js_1.recordAuditAction)({ kind: 'credential.issue', ...target }, { manager: event.manager });
116
+ }
117
+ }
118
+ };
119
+ exports.UserAuditSubscriber = UserAuditSubscriber;
120
+ exports.UserAuditSubscriber = UserAuditSubscriber = tslib_1.__decorate([
121
+ (0, typeorm_1.EventSubscriber)()
122
+ ], UserAuditSubscriber);
123
+ let DomainOwnerAuditSubscriber = class DomainOwnerAuditSubscriber {
124
+ listenTo() {
125
+ return domain_owner_js_1.DomainOwner;
126
+ }
127
+ async afterInsert(event) {
128
+ const owner = event.entity;
129
+ const userId = idOf(owner?.user);
130
+ const domainId = idOf(owner?.domain);
131
+ if (!userId)
132
+ return;
133
+ await (0, audit_recorder_js_1.recordAuditAction)({ kind: 'domain.owner.add', targetKind: 'User', targetIds: [userId], targetRole: 'handled' }, { manager: event.manager, ...(domainId ? { domainId } : {}) });
134
+ }
135
+ };
136
+ exports.DomainOwnerAuditSubscriber = DomainOwnerAuditSubscriber;
137
+ exports.DomainOwnerAuditSubscriber = DomainOwnerAuditSubscriber = tslib_1.__decorate([
138
+ (0, typeorm_1.EventSubscriber)()
139
+ ], DomainOwnerAuditSubscriber);
140
+ let InvitationAuditSubscriber = class InvitationAuditSubscriber {
141
+ listenTo() {
142
+ return invitation_js_1.Invitation;
143
+ }
144
+ async afterInsert(event) {
145
+ const invitation = event.entity;
146
+ if (!invitation?.id)
147
+ return;
148
+ const domainId = idOf(invitation.domain);
149
+ await (0, audit_recorder_js_1.recordAuditAction)({ kind: 'invitation.create', targetKind: 'Invitation', targetIds: [invitation.id], targetRole: 'handled' }, { manager: event.manager, ...(domainId ? { domainId } : {}) });
150
+ }
151
+ async afterUpdate(event) {
152
+ const invitation = event.entity;
153
+ const before = event.databaseEntity;
154
+ if (!invitation?.id || !before || invitation.status === undefined || invitation.status === before.status)
155
+ return;
156
+ await (0, audit_recorder_js_1.recordAuditAction)({
157
+ kind: 'invitation.status',
158
+ targetKind: 'Invitation',
159
+ targetIds: [invitation.id],
160
+ targetRole: 'handled',
161
+ detail: { from: before.status ?? null, to: invitation.status }
162
+ }, { manager: event.manager });
163
+ }
164
+ };
165
+ exports.InvitationAuditSubscriber = InvitationAuditSubscriber;
166
+ exports.InvitationAuditSubscriber = InvitationAuditSubscriber = tslib_1.__decorate([
167
+ (0, typeorm_1.EventSubscriber)()
168
+ ], InvitationAuditSubscriber);
169
+ let WebAuthCredentialAuditSubscriber = class WebAuthCredentialAuditSubscriber {
170
+ listenTo() {
171
+ return web_auth_credential_js_1.WebAuthCredential;
172
+ }
173
+ async afterInsert(event) {
174
+ const userId = idOf(event.entity?.user);
175
+ if (!userId)
176
+ return;
177
+ await (0, audit_recorder_js_1.recordAuditAction)({ kind: 'webauthn.register', targetKind: 'User', targetIds: [userId], targetRole: 'handled' }, { manager: event.manager, actorId: userId });
178
+ }
179
+ };
180
+ exports.WebAuthCredentialAuditSubscriber = WebAuthCredentialAuditSubscriber;
181
+ exports.WebAuthCredentialAuditSubscriber = WebAuthCredentialAuditSubscriber = tslib_1.__decorate([
182
+ (0, typeorm_1.EventSubscriber)()
183
+ ], WebAuthCredentialAuditSubscriber);
184
+ exports.auditSubscribers = [
185
+ GrantAuditSubscriber,
186
+ UserAuditSubscriber,
187
+ DomainOwnerAuditSubscriber,
188
+ InvitationAuditSubscriber,
189
+ WebAuthCredentialAuditSubscriber
190
+ ];
191
+ //# sourceMappingURL=audit-subscriber.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-subscriber.js","sourceRoot":"","sources":["../../../server/service/audit/audit-subscriber.ts"],"names":[],"mappings":"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","sourcesContent":["import { EntitySubscriberInterface, EventSubscriber, InsertEvent, RemoveEvent, UpdateEvent } from 'typeorm'\n\nimport { DomainOwner } from '../domain-owner/domain-owner.js'\nimport { Invitation } from '../invitation/invitation.js'\nimport { Privilege } from '../privilege/privilege.js'\nimport { Role } from '../role/role.js'\nimport { User } from '../user/user.js'\nimport { WebAuthCredential } from '../web-auth-credential/web-auth-credential.js'\nimport { currentAuditScope, recordAuditAction } from './audit-recorder.js'\n\n/**\n * ── Who may do what, recorded where it is saved (ADR-0141 §3.3 · §4-1) ───────\n *\n * A role is granted in a dozen places - the user screen, an access request, an invitation, a\n * template, Office's role plan. Each saves through TypeORM, so the change is caught here, once, and\n * written in the same transaction as the save: the grant and its record stand or fall together.\n *\n * A write that bypasses the entity manager's events (a query builder, `delete()`) is not seen here;\n * those few paths record themselves.\n */\n\nconst idOf = (one: any): string | undefined => (typeof one === 'string' ? one : one?.id)\n\n/** The id a junction row holds for `target`, read from what the event carries. */\nfunction junctionIdOf(event: InsertEvent<any> | RemoveEvent<any>, target: Function): string | undefined {\n const source = (event as any).entityId ?? event.entity ?? (event as RemoveEvent<any>).databaseEntity\n if (!source) return undefined\n const column = event.metadata.columns.find(one => one.referencedColumn?.entityMetadata.target === target)\n if (!column) return undefined\n const value = column.getEntityValue(source) ?? source[column.propertyName] ?? source[column.databaseName]\n return idOf(value)\n}\n\n/** The domain of a role, read in the same transaction - a role being deleted has none left to read. */\nasync function domainOfRole(event: InsertEvent<any> | RemoveEvent<any>, roleId: string) {\n const role = await event.manager.getRepository(Role).findOne({ where: { id: roleId }, relations: ['domain'] })\n return role?.domain?.id\n}\n\n/**\n * ── Grants are junction rows ──────────────────────────────────────────────\n *\n * A role given to a user is a `users_roles` row; a privilege given to a role, a `roles_privileges`\n * row. When only those change, TypeORM updates no entity and sends no update event - it inserts and\n * removes junction rows, and those it does announce. So grants are read here, from the junction, for\n * every path that saves a user's roles or a role's privileges, a new user's or role's included.\n *\n * This listens to every entity (a junction has no class to name) and leaves at once on anything\n * that is not one of the two junctions.\n */\n@EventSubscriber()\nexport class GrantAuditSubscriber implements EntitySubscriberInterface<any> {\n async afterInsert(event: InsertEvent<any>) {\n await this.record(event, 'insert')\n }\n\n async afterRemove(event: RemoveEvent<any>) {\n await this.record(event, 'remove')\n }\n\n private async record(event: InsertEvent<any> | RemoveEvent<any>, change: 'insert' | 'remove') {\n if (!event.metadata.isJunction) return\n\n const roleId = junctionIdOf(event, Role)\n const userId = junctionIdOf(event, User)\n const privilegeId = junctionIdOf(event, Privilege)\n if (!roleId) return\n\n const domainId = await domainOfRole(event, roleId)\n const where = { manager: event.manager, ...(domainId ? { domainId } : {}) }\n\n if (userId) {\n await recordAuditAction(\n {\n kind: change === 'insert' ? 'role.grant' : 'role.revoke',\n targetKind: 'User',\n targetIds: [userId],\n targetRole: 'handled',\n objectKind: 'Role',\n objectId: roleId\n },\n where\n )\n } else if (privilegeId) {\n await recordAuditAction(\n {\n kind: change === 'insert' ? 'role.privilege.add' : 'role.privilege.remove',\n targetKind: 'Role',\n targetIds: [roleId],\n targetRole: 'handled',\n objectKind: 'Privilege',\n objectId: privilegeId\n },\n where\n )\n }\n }\n}\n\n@EventSubscriber()\nexport class UserAuditSubscriber implements EntitySubscriberInterface<User> {\n listenTo() {\n return User\n }\n\n async afterUpdate(event: UpdateEvent<User>) {\n const user = event.entity as User | undefined\n const before = event.databaseEntity as User | undefined\n const userId = user?.id ?? before?.id\n if (!user || !before || !userId) return\n\n const target = { targetKind: 'User', targetIds: [userId], targetRole: 'handled' as const }\n\n if (user.status !== undefined && user.status !== before.status) {\n await recordAuditAction(\n { kind: 'user.status', ...target, detail: { from: before.status ?? null, to: user.status } },\n { manager: event.manager }\n )\n }\n\n if (user.password !== undefined && user.password !== before.password) {\n /* One's own change, or somebody else setting it - the actor tells which. */\n const actorId = currentAuditScope()?.context?.state?.user?.id\n const kind = actorId && actorId !== userId ? 'password.reset-by-admin' : 'password.change'\n await recordAuditAction({ kind, ...target }, { manager: event.manager })\n }\n\n if (user.credentialSerial !== undefined && user.credentialSerial !== before.credentialSerial) {\n await recordAuditAction({ kind: 'credential.issue', ...target }, { manager: event.manager })\n }\n }\n}\n\n@EventSubscriber()\nexport class DomainOwnerAuditSubscriber implements EntitySubscriberInterface<DomainOwner> {\n listenTo() {\n return DomainOwner\n }\n\n async afterInsert(event: InsertEvent<DomainOwner>) {\n const owner = event.entity\n const userId = idOf(owner?.user)\n const domainId = idOf(owner?.domain)\n if (!userId) return\n\n await recordAuditAction(\n { kind: 'domain.owner.add', targetKind: 'User', targetIds: [userId], targetRole: 'handled' },\n { manager: event.manager, ...(domainId ? { domainId } : {}) }\n )\n }\n}\n\n@EventSubscriber()\nexport class InvitationAuditSubscriber implements EntitySubscriberInterface<Invitation> {\n listenTo() {\n return Invitation\n }\n\n async afterInsert(event: InsertEvent<Invitation>) {\n const invitation = event.entity\n if (!invitation?.id) return\n const domainId = idOf((invitation as any).domain)\n\n await recordAuditAction(\n { kind: 'invitation.create', targetKind: 'Invitation', targetIds: [invitation.id], targetRole: 'handled' },\n { manager: event.manager, ...(domainId ? { domainId } : {}) }\n )\n }\n\n async afterUpdate(event: UpdateEvent<Invitation>) {\n const invitation = event.entity as Invitation | undefined\n const before = event.databaseEntity as Invitation | undefined\n if (!invitation?.id || !before || invitation.status === undefined || invitation.status === before.status) return\n\n await recordAuditAction(\n {\n kind: 'invitation.status',\n targetKind: 'Invitation',\n targetIds: [invitation.id],\n targetRole: 'handled',\n detail: { from: before.status ?? null, to: invitation.status }\n },\n { manager: event.manager }\n )\n }\n}\n\n@EventSubscriber()\nexport class WebAuthCredentialAuditSubscriber implements EntitySubscriberInterface<WebAuthCredential> {\n listenTo() {\n return WebAuthCredential\n }\n\n async afterInsert(event: InsertEvent<WebAuthCredential>) {\n const userId = idOf(event.entity?.user)\n if (!userId) return\n\n await recordAuditAction(\n { kind: 'webauthn.register', targetKind: 'User', targetIds: [userId], targetRole: 'handled' },\n { manager: event.manager, actorId: userId }\n )\n }\n}\n\nexport const auditSubscribers = [\n GrantAuditSubscriber,\n UserAuditSubscriber,\n DomainOwnerAuditSubscriber,\n InvitationAuditSubscriber,\n WebAuthCredentialAuditSubscriber\n]\n"]}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `@auditTarget(kind: "User", arg: "id")` - which argument of a door names what it acts on
3
+ * (ADR-0141 §3.3). The server declares it beside the field; the audit does not guess from argument
4
+ * names. What it reads is the **requested** target: what the caller named. What a service actually
5
+ * handled - an id created, a set changed in bulk - the service records itself.
6
+ *
7
+ * Read by the `@privilege` wrapper, so it only means something on a field that carries `@privilege`.
8
+ */
9
+ export declare const auditTargetDirectiveTypeDefs: import("graphql").DocumentNode;
10
+ /** Ids named by `args[arg]`: a string, a list of strings, an object with `id`, or a list of those. */
11
+ export declare function targetIdsOf(args: Record<string, any> | undefined, arg: string): string[];
12
+ /** How many items a list answer holds: an array, or `{ items }`. Not a list - nothing to count. */
13
+ export declare function returnedCountOf(result: any): number | undefined;
14
+ /**
15
+ * A fixed code for an error - never its message (ADR-0141 §3.4). Codes are the names a program gave
16
+ * in a code field (`FORBIDDEN`, `password-not-matched`, `user not found`, `SQLITE_CONSTRAINT`); the
17
+ * message is never read, and a code field that does not look like a name is `error`.
18
+ */
19
+ export declare function errorCodeOf(error: any): string;
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.auditTargetDirectiveTypeDefs = void 0;
4
+ exports.targetIdsOf = targetIdsOf;
5
+ exports.returnedCountOf = returnedCountOf;
6
+ exports.errorCodeOf = errorCodeOf;
7
+ const tslib_1 = require("tslib");
8
+ const graphql_tag_1 = tslib_1.__importDefault(require("graphql-tag"));
9
+ /**
10
+ * `@auditTarget(kind: "User", arg: "id")` - which argument of a door names what it acts on
11
+ * (ADR-0141 §3.3). The server declares it beside the field; the audit does not guess from argument
12
+ * names. What it reads is the **requested** target: what the caller named. What a service actually
13
+ * handled - an id created, a set changed in bulk - the service records itself.
14
+ *
15
+ * Read by the `@privilege` wrapper, so it only means something on a field that carries `@privilege`.
16
+ */
17
+ exports.auditTargetDirectiveTypeDefs = (0, graphql_tag_1.default) `
18
+ directive @auditTarget(kind: String!, arg: String!) on FIELD_DEFINITION
19
+ `;
20
+ /** Ids named by `args[arg]`: a string, a list of strings, an object with `id`, or a list of those. */
21
+ function targetIdsOf(args, arg) {
22
+ const value = args?.[arg];
23
+ const values = Array.isArray(value) ? value : value === undefined || value === null ? [] : [value];
24
+ return values
25
+ .map(one => (typeof one === 'string' || typeof one === 'number' ? String(one) : one?.id ? String(one.id) : null))
26
+ .filter((id) => !!id);
27
+ }
28
+ /** How many items a list answer holds: an array, or `{ items }`. Not a list - nothing to count. */
29
+ function returnedCountOf(result) {
30
+ if (Array.isArray(result))
31
+ return result.length;
32
+ if (result && Array.isArray(result.items))
33
+ return result.items.length;
34
+ return undefined;
35
+ }
36
+ /**
37
+ * A fixed code for an error - never its message (ADR-0141 §3.4). Codes are the names a program gave
38
+ * in a code field (`FORBIDDEN`, `password-not-matched`, `user not found`, `SQLITE_CONSTRAINT`); the
39
+ * message is never read, and a code field that does not look like a name is `error`.
40
+ */
41
+ function errorCodeOf(error) {
42
+ const candidates = [error?.errorCode, error?.extensions?.code, error?.code];
43
+ const code = candidates.find(one => typeof one === 'string' && /^[A-Za-z][A-Za-z0-9 _.-]{0,63}$/.test(one));
44
+ return code ?? 'error';
45
+ }
46
+ //# sourceMappingURL=audit-target.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-target.js","sourceRoot":"","sources":["../../../server/service/audit/audit-target.ts"],"names":[],"mappings":";;;AAeA,kCAOC;AAGD,0CAIC;AAOD,kCAIC;;AAxCD,sEAA6B;AAE7B;;;;;;;GAOG;AACU,QAAA,4BAA4B,GAAG,IAAA,qBAAG,EAAA;;CAE9C,CAAA;AAED,sGAAsG;AACtG,SAAgB,WAAW,CAAC,IAAqC,EAAE,GAAW;IAC5E,MAAM,KAAK,GAAG,IAAI,EAAE,CAAC,GAAG,CAAC,CAAA;IACzB,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAA;IAElG,OAAO,MAAM;SACV,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,OAAO,GAAG,KAAK,QAAQ,IAAI,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;SAChH,MAAM,CAAC,CAAC,EAAE,EAAgB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;AACvC,CAAC;AAED,mGAAmG;AACnG,SAAgB,eAAe,CAAC,MAAW;IACzC,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QAAE,OAAO,MAAM,CAAC,MAAM,CAAA;IAC/C,IAAI,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC;QAAE,OAAO,MAAM,CAAC,KAAK,CAAC,MAAM,CAAA;IACrE,OAAO,SAAS,CAAA;AAClB,CAAC;AAED;;;;GAIG;AACH,SAAgB,WAAW,CAAC,KAAU;IACpC,MAAM,UAAU,GAAG,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,CAAA;IAC3E,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,OAAO,GAAG,KAAK,QAAQ,IAAI,iCAAiC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;IAC3G,OAAO,IAAI,IAAI,OAAO,CAAA;AACxB,CAAC","sourcesContent":["import gql from 'graphql-tag'\n\n/**\n * `@auditTarget(kind: \"User\", arg: \"id\")` - which argument of a door names what it acts on\n * (ADR-0141 §3.3). The server declares it beside the field; the audit does not guess from argument\n * names. What it reads is the **requested** target: what the caller named. What a service actually\n * handled - an id created, a set changed in bulk - the service records itself.\n *\n * Read by the `@privilege` wrapper, so it only means something on a field that carries `@privilege`.\n */\nexport const auditTargetDirectiveTypeDefs = gql`\n directive @auditTarget(kind: String!, arg: String!) on FIELD_DEFINITION\n`\n\n/** Ids named by `args[arg]`: a string, a list of strings, an object with `id`, or a list of those. */\nexport function targetIdsOf(args: Record<string, any> | undefined, arg: string): string[] {\n const value = args?.[arg]\n const values = Array.isArray(value) ? value : value === undefined || value === null ? [] : [value]\n\n return values\n .map(one => (typeof one === 'string' || typeof one === 'number' ? String(one) : one?.id ? String(one.id) : null))\n .filter((id): id is string => !!id)\n}\n\n/** How many items a list answer holds: an array, or `{ items }`. Not a list - nothing to count. */\nexport function returnedCountOf(result: any): number | undefined {\n if (Array.isArray(result)) return result.length\n if (result && Array.isArray(result.items)) return result.items.length\n return undefined\n}\n\n/**\n * A fixed code for an error - never its message (ADR-0141 §3.4). Codes are the names a program gave\n * in a code field (`FORBIDDEN`, `password-not-matched`, `user not found`, `SQLITE_CONSTRAINT`); the\n * message is never read, and a code field that does not look like a name is `error`.\n */\nexport function errorCodeOf(error: any): string {\n const candidates = [error?.errorCode, error?.extensions?.code, error?.code]\n const code = candidates.find(one => typeof one === 'string' && /^[A-Za-z][A-Za-z0-9 _.-]{0,63}$/.test(one))\n return code ?? 'error'\n}\n"]}
@@ -0,0 +1,44 @@
1
+ import { User } from '../user/user.js';
2
+ import { AuditAction } from './audit-action.js';
3
+ import { AuditRequest } from './audit-request.js';
4
+ export declare class AuditRecordFilter {
5
+ from: Date;
6
+ to: Date;
7
+ actorId?: string;
8
+ door?: string;
9
+ kind?: string;
10
+ outcome?: string;
11
+ page?: number;
12
+ limit?: number;
13
+ }
14
+ export declare class AuditRecord {
15
+ request: AuditRequest;
16
+ actions: AuditAction[];
17
+ }
18
+ export declare class AuditRecordList {
19
+ items: AuditRecord[];
20
+ total: number;
21
+ }
22
+ export declare class AuditPerson {
23
+ user: User;
24
+ roles: string[];
25
+ lastCheckinAt?: Date;
26
+ checkins: number;
27
+ failedSignins: number;
28
+ granted: number;
29
+ denied: number;
30
+ }
31
+ export declare class AuditDoor {
32
+ door: string;
33
+ category?: string;
34
+ privilege?: string;
35
+ people: number;
36
+ granted: number;
37
+ denied: number;
38
+ }
39
+ export declare class AuditHealth {
40
+ since: Date;
41
+ failures: number;
42
+ firstFailedAt?: Date;
43
+ lastFailedAt?: Date;
44
+ }
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AuditHealth = exports.AuditDoor = exports.AuditPerson = exports.AuditRecordList = exports.AuditRecord = exports.AuditRecordFilter = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const type_graphql_1 = require("type-graphql");
6
+ const user_js_1 = require("../user/user.js");
7
+ const audit_action_js_1 = require("./audit-action.js");
8
+ const audit_request_js_1 = require("./audit-request.js");
9
+ let AuditRecordFilter = class AuditRecordFilter {
10
+ };
11
+ exports.AuditRecordFilter = AuditRecordFilter;
12
+ tslib_1.__decorate([
13
+ (0, type_graphql_1.Field)({ description: 'From this moment.' }),
14
+ tslib_1.__metadata("design:type", Date)
15
+ ], AuditRecordFilter.prototype, "from", void 0);
16
+ tslib_1.__decorate([
17
+ (0, type_graphql_1.Field)({ description: 'Up to this moment.' }),
18
+ tslib_1.__metadata("design:type", Date)
19
+ ], AuditRecordFilter.prototype, "to", void 0);
20
+ tslib_1.__decorate([
21
+ (0, type_graphql_1.Field)({ nullable: true, description: 'Only requests by this user.' }),
22
+ tslib_1.__metadata("design:type", String)
23
+ ], AuditRecordFilter.prototype, "actorId", void 0);
24
+ tslib_1.__decorate([
25
+ (0, type_graphql_1.Field)({ nullable: true, description: 'Only requests that passed through this door (category:privilege).' }),
26
+ tslib_1.__metadata("design:type", String)
27
+ ], AuditRecordFilter.prototype, "door", void 0);
28
+ tslib_1.__decorate([
29
+ (0, type_graphql_1.Field)({ nullable: true, description: 'Only requests with an action of this kind.' }),
30
+ tslib_1.__metadata("design:type", String)
31
+ ], AuditRecordFilter.prototype, "kind", void 0);
32
+ tslib_1.__decorate([
33
+ (0, type_graphql_1.Field)({ nullable: true, description: 'denied, error or failed - only requests where that happened.' }),
34
+ tslib_1.__metadata("design:type", String)
35
+ ], AuditRecordFilter.prototype, "outcome", void 0);
36
+ tslib_1.__decorate([
37
+ (0, type_graphql_1.Field)(type => type_graphql_1.Int, { nullable: true, description: 'Page, from 1.' }),
38
+ tslib_1.__metadata("design:type", Number)
39
+ ], AuditRecordFilter.prototype, "page", void 0);
40
+ tslib_1.__decorate([
41
+ (0, type_graphql_1.Field)(type => type_graphql_1.Int, { nullable: true, description: 'Rows per page, up to 200.' }),
42
+ tslib_1.__metadata("design:type", Number)
43
+ ], AuditRecordFilter.prototype, "limit", void 0);
44
+ exports.AuditRecordFilter = AuditRecordFilter = tslib_1.__decorate([
45
+ (0, type_graphql_1.InputType)({ description: 'What to narrow the audit records to.' })
46
+ ], AuditRecordFilter);
47
+ let AuditRecord = class AuditRecord {
48
+ };
49
+ exports.AuditRecord = AuditRecord;
50
+ tslib_1.__decorate([
51
+ (0, type_graphql_1.Field)(type => audit_request_js_1.AuditRequest, { description: 'The request.' }),
52
+ tslib_1.__metadata("design:type", audit_request_js_1.AuditRequest)
53
+ ], AuditRecord.prototype, "request", void 0);
54
+ tslib_1.__decorate([
55
+ (0, type_graphql_1.Field)(type => [audit_action_js_1.AuditAction], { description: 'Its actions in this domain.' }),
56
+ tslib_1.__metadata("design:type", Array)
57
+ ], AuditRecord.prototype, "actions", void 0);
58
+ exports.AuditRecord = AuditRecord = tslib_1.__decorate([
59
+ (0, type_graphql_1.ObjectType)({ description: 'A request with what was done in it.' })
60
+ ], AuditRecord);
61
+ let AuditRecordList = class AuditRecordList {
62
+ };
63
+ exports.AuditRecordList = AuditRecordList;
64
+ tslib_1.__decorate([
65
+ (0, type_graphql_1.Field)(type => [AuditRecord], { description: 'The records on this page.' }),
66
+ tslib_1.__metadata("design:type", Array)
67
+ ], AuditRecordList.prototype, "items", void 0);
68
+ tslib_1.__decorate([
69
+ (0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'How many requests match.' }),
70
+ tslib_1.__metadata("design:type", Number)
71
+ ], AuditRecordList.prototype, "total", void 0);
72
+ exports.AuditRecordList = AuditRecordList = tslib_1.__decorate([
73
+ (0, type_graphql_1.ObjectType)({ description: 'A page of audit records.' })
74
+ ], AuditRecordList);
75
+ let AuditPerson = class AuditPerson {
76
+ };
77
+ exports.AuditPerson = AuditPerson;
78
+ tslib_1.__decorate([
79
+ (0, type_graphql_1.Field)(type => user_js_1.User, { description: 'The member.' }),
80
+ tslib_1.__metadata("design:type", user_js_1.User)
81
+ ], AuditPerson.prototype, "user", void 0);
82
+ tslib_1.__decorate([
83
+ (0, type_graphql_1.Field)(type => [String], { description: 'Names of the roles held in this domain.' }),
84
+ tslib_1.__metadata("design:type", Array)
85
+ ], AuditPerson.prototype, "roles", void 0);
86
+ tslib_1.__decorate([
87
+ (0, type_graphql_1.Field)({ nullable: true, description: 'The last check-in to this domain, at any time.' }),
88
+ tslib_1.__metadata("design:type", Date)
89
+ ], AuditPerson.prototype, "lastCheckinAt", void 0);
90
+ tslib_1.__decorate([
91
+ (0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'Check-ins in the period.' }),
92
+ tslib_1.__metadata("design:type", Number)
93
+ ], AuditPerson.prototype, "checkins", void 0);
94
+ tslib_1.__decorate([
95
+ (0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'Failed sign-ins in the period.' }),
96
+ tslib_1.__metadata("design:type", Number)
97
+ ], AuditPerson.prototype, "failedSignins", void 0);
98
+ tslib_1.__decorate([
99
+ (0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'Door checks passed in the period, audit reads left out.' }),
100
+ tslib_1.__metadata("design:type", Number)
101
+ ], AuditPerson.prototype, "granted", void 0);
102
+ tslib_1.__decorate([
103
+ (0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'Door checks refused in the period.' }),
104
+ tslib_1.__metadata("design:type", Number)
105
+ ], AuditPerson.prototype, "denied", void 0);
106
+ exports.AuditPerson = AuditPerson = tslib_1.__decorate([
107
+ (0, type_graphql_1.ObjectType)({ description: "One member's audit summary over a period." })
108
+ ], AuditPerson);
109
+ let AuditDoor = class AuditDoor {
110
+ };
111
+ exports.AuditDoor = AuditDoor;
112
+ tslib_1.__decorate([
113
+ (0, type_graphql_1.Field)({ description: 'The door, as category:privilege - or owner / super.' }),
114
+ tslib_1.__metadata("design:type", String)
115
+ ], AuditDoor.prototype, "door", void 0);
116
+ tslib_1.__decorate([
117
+ (0, type_graphql_1.Field)({ nullable: true, description: 'The category, when the door names a privilege.' }),
118
+ tslib_1.__metadata("design:type", String)
119
+ ], AuditDoor.prototype, "category", void 0);
120
+ tslib_1.__decorate([
121
+ (0, type_graphql_1.Field)({ nullable: true, description: 'The privilege, when the door names one.' }),
122
+ tslib_1.__metadata("design:type", String)
123
+ ], AuditDoor.prototype, "privilege", void 0);
124
+ tslib_1.__decorate([
125
+ (0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'People who passed or were refused here in the period.' }),
126
+ tslib_1.__metadata("design:type", Number)
127
+ ], AuditDoor.prototype, "people", void 0);
128
+ tslib_1.__decorate([
129
+ (0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'Checks passed in the period.' }),
130
+ tslib_1.__metadata("design:type", Number)
131
+ ], AuditDoor.prototype, "granted", void 0);
132
+ tslib_1.__decorate([
133
+ (0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'Checks refused in the period.' }),
134
+ tslib_1.__metadata("design:type", Number)
135
+ ], AuditDoor.prototype, "denied", void 0);
136
+ exports.AuditDoor = AuditDoor = tslib_1.__decorate([
137
+ (0, type_graphql_1.ObjectType)({ description: "One door's audit summary over a period." })
138
+ ], AuditDoor);
139
+ let AuditHealth = class AuditHealth {
140
+ };
141
+ exports.AuditHealth = AuditHealth;
142
+ tslib_1.__decorate([
143
+ (0, type_graphql_1.Field)({ description: 'When this process started counting.' }),
144
+ tslib_1.__metadata("design:type", Date)
145
+ ], AuditHealth.prototype, "since", void 0);
146
+ tslib_1.__decorate([
147
+ (0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'Writes that failed since then. Writes never attempted are not counted.' }),
148
+ tslib_1.__metadata("design:type", Number)
149
+ ], AuditHealth.prototype, "failures", void 0);
150
+ tslib_1.__decorate([
151
+ (0, type_graphql_1.Field)({ nullable: true, description: 'The first failure.' }),
152
+ tslib_1.__metadata("design:type", Date)
153
+ ], AuditHealth.prototype, "firstFailedAt", void 0);
154
+ tslib_1.__decorate([
155
+ (0, type_graphql_1.Field)({ nullable: true, description: 'The last failure.' }),
156
+ tslib_1.__metadata("design:type", Date)
157
+ ], AuditHealth.prototype, "lastFailedAt", void 0);
158
+ exports.AuditHealth = AuditHealth = tslib_1.__decorate([
159
+ (0, type_graphql_1.ObjectType)({ description: 'Audit writes this server process failed, counted apart from the audit store.' })
160
+ ], AuditHealth);
161
+ //# sourceMappingURL=audit-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-types.js","sourceRoot":"","sources":["../../../server/service/audit/audit-types.ts"],"names":[],"mappings":";;;;AAAA,+CAAgE;AAEhE,6CAAsC;AACtC,uDAA+C;AAC/C,yDAAiD;AAG1C,IAAM,iBAAiB,GAAvB,MAAM,iBAAiB;CAwB7B,CAAA;AAxBY,8CAAiB;AAE5B;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,mBAAmB,EAAE,CAAC;sCACtC,IAAI;+CAAA;AAGV;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,oBAAoB,EAAE,CAAC;sCACzC,IAAI;6CAAA;AAGR;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,6BAA6B,EAAE,CAAC;;kDACtD;AAGhB;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,mEAAmE,EAAE,CAAC;;+CAC/F;AAGb;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,4CAA4C,EAAE,CAAC;;+CACxE;AAGb;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,8DAA8D,EAAE,CAAC;;kDACvF;AAGhB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,eAAe,EAAE,CAAC;;+CACxD;AAGb;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,2BAA2B,EAAE,CAAC;;gDACnE;4BAvBH,iBAAiB;IAD7B,IAAA,wBAAS,EAAC,EAAE,WAAW,EAAE,sCAAsC,EAAE,CAAC;GACtD,iBAAiB,CAwB7B;AAGM,IAAM,WAAW,GAAjB,MAAM,WAAW;CAMvB,CAAA;AANY,kCAAW;AAEtB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,+BAAY,EAAE,EAAE,WAAW,EAAE,cAAc,EAAE,CAAC;sCACpD,+BAAY;4CAAA;AAGrB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,CAAC,6BAAW,CAAC,EAAE,EAAE,WAAW,EAAE,6BAA6B,EAAE,CAAC;;4CACvD;sBALX,WAAW;IADvB,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,qCAAqC,EAAE,CAAC;GACtD,WAAW,CAMvB;AAGM,IAAM,eAAe,GAArB,MAAM,eAAe;CAM3B,CAAA;AANY,0CAAe;AAE1B;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,CAAC,WAAW,CAAC,EAAE,EAAE,WAAW,EAAE,2BAA2B,EAAE,CAAC;;8CACvD;AAGpB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,0BAA0B,EAAE,CAAC;;8CACnD;0BALF,eAAe;IAD3B,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,0BAA0B,EAAE,CAAC;GAC3C,eAAe,CAM3B;AAGM,IAAM,WAAW,GAAjB,MAAM,WAAW;CAqBvB,CAAA;AArBY,kCAAW;AAEtB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,cAAI,EAAE,EAAE,WAAW,EAAE,aAAa,EAAE,CAAC;sCAC9C,cAAI;yCAAA;AAGV;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,WAAW,EAAE,yCAAyC,EAAE,CAAC;;0CACrE;AAGf;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,gDAAgD,EAAE,CAAC;sCACzE,IAAI;kDAAA;AAGpB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,0BAA0B,EAAE,CAAC;;6CAChD;AAGhB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,gCAAgC,EAAE,CAAC;;kDACjD;AAGrB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,yDAAyD,EAAE,CAAC;;4CAChF;AAGf;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,oCAAoC,EAAE,CAAC;;2CAC5D;sBApBH,WAAW;IADvB,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,2CAA2C,EAAE,CAAC;GAC5D,WAAW,CAqBvB;AAGM,IAAM,SAAS,GAAf,MAAM,SAAS;CAkBrB,CAAA;AAlBY,8BAAS;AAEpB;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,qDAAqD,EAAE,CAAC;;uCAClE;AAGZ;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,gDAAgD,EAAE,CAAC;;2CACxE;AAGjB;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,yCAAyC,EAAE,CAAC;;4CAChE;AAGlB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,uDAAuD,EAAE,CAAC;;yCAC/E;AAGd;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,8BAA8B,EAAE,CAAC;;0CACrD;AAGf;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,+BAA+B,EAAE,CAAC;;yCACvD;oBAjBH,SAAS;IADrB,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,yCAAyC,EAAE,CAAC;GAC1D,SAAS,CAkBrB;AAGM,IAAM,WAAW,GAAjB,MAAM,WAAW;CAYvB,CAAA;AAZY,kCAAW;AAEtB;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,qCAAqC,EAAE,CAAC;sCACvD,IAAI;0CAAA;AAGX;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,wEAAwE,EAAE,CAAC;;6CAC9F;AAGhB;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,oBAAoB,EAAE,CAAC;sCAC7C,IAAI;kDAAA;AAGpB;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,mBAAmB,EAAE,CAAC;sCAC7C,IAAI;iDAAA;sBAXR,WAAW;IADvB,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,8EAA8E,EAAE,CAAC;GAC/F,WAAW,CAYvB","sourcesContent":["import { Field, InputType, Int, ObjectType } from 'type-graphql'\n\nimport { User } from '../user/user.js'\nimport { AuditAction } from './audit-action.js'\nimport { AuditRequest } from './audit-request.js'\n\n@InputType({ description: 'What to narrow the audit records to.' })\nexport class AuditRecordFilter {\n @Field({ description: 'From this moment.' })\n from: Date\n\n @Field({ description: 'Up to this moment.' })\n to: Date\n\n @Field({ nullable: true, description: 'Only requests by this user.' })\n actorId?: string\n\n @Field({ nullable: true, description: 'Only requests that passed through this door (category:privilege).' })\n door?: string\n\n @Field({ nullable: true, description: 'Only requests with an action of this kind.' })\n kind?: string\n\n @Field({ nullable: true, description: 'denied, error or failed - only requests where that happened.' })\n outcome?: string\n\n @Field(type => Int, { nullable: true, description: 'Page, from 1.' })\n page?: number\n\n @Field(type => Int, { nullable: true, description: 'Rows per page, up to 200.' })\n limit?: number\n}\n\n@ObjectType({ description: 'A request with what was done in it.' })\nexport class AuditRecord {\n @Field(type => AuditRequest, { description: 'The request.' })\n request: AuditRequest\n\n @Field(type => [AuditAction], { description: 'Its actions in this domain.' })\n actions: AuditAction[]\n}\n\n@ObjectType({ description: 'A page of audit records.' })\nexport class AuditRecordList {\n @Field(type => [AuditRecord], { description: 'The records on this page.' })\n items: AuditRecord[]\n\n @Field(type => Int, { description: 'How many requests match.' })\n total: number\n}\n\n@ObjectType({ description: \"One member's audit summary over a period.\" })\nexport class AuditPerson {\n @Field(type => User, { description: 'The member.' })\n user: User\n\n @Field(type => [String], { description: 'Names of the roles held in this domain.' })\n roles: string[]\n\n @Field({ nullable: true, description: 'The last check-in to this domain, at any time.' })\n lastCheckinAt?: Date\n\n @Field(type => Int, { description: 'Check-ins in the period.' })\n checkins: number\n\n @Field(type => Int, { description: 'Failed sign-ins in the period.' })\n failedSignins: number\n\n @Field(type => Int, { description: 'Door checks passed in the period, audit reads left out.' })\n granted: number\n\n @Field(type => Int, { description: 'Door checks refused in the period.' })\n denied: number\n}\n\n@ObjectType({ description: \"One door's audit summary over a period.\" })\nexport class AuditDoor {\n @Field({ description: 'The door, as category:privilege - or owner / super.' })\n door: string\n\n @Field({ nullable: true, description: 'The category, when the door names a privilege.' })\n category?: string\n\n @Field({ nullable: true, description: 'The privilege, when the door names one.' })\n privilege?: string\n\n @Field(type => Int, { description: 'People who passed or were refused here in the period.' })\n people: number\n\n @Field(type => Int, { description: 'Checks passed in the period.' })\n granted: number\n\n @Field(type => Int, { description: 'Checks refused in the period.' })\n denied: number\n}\n\n@ObjectType({ description: 'Audit writes this server process failed, counted apart from the audit store.' })\nexport class AuditHealth {\n @Field({ description: 'When this process started counting.' })\n since: Date\n\n @Field(type => Int, { description: 'Writes that failed since then. Writes never attempted are not counted.' })\n failures: number\n\n @Field({ nullable: true, description: 'The first failure.' })\n firstFailedAt?: Date\n\n @Field({ nullable: true, description: 'The last failure.' })\n lastFailedAt?: Date\n}\n"]}
@@ -0,0 +1,6 @@
1
+ import { AuditAction } from './audit-action.js';
2
+ import { AuditQuery } from './audit-query.js';
3
+ import { AuditRequest } from './audit-request.js';
4
+ export declare const entities: (typeof AuditRequest | typeof AuditAction)[];
5
+ export declare const resolvers: (typeof AuditQuery)[];
6
+ export declare const subscribers: (typeof import("./audit-subscriber.js").GrantAuditSubscriber | typeof import("./audit-subscriber.js").UserAuditSubscriber | typeof import("./audit-subscriber.js").DomainOwnerAuditSubscriber | typeof import("./audit-subscriber.js").InvitationAuditSubscriber | typeof import("./audit-subscriber.js").WebAuthCredentialAuditSubscriber)[];
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.subscribers = exports.resolvers = exports.entities = void 0;
4
+ const audit_action_js_1 = require("./audit-action.js");
5
+ const audit_query_js_1 = require("./audit-query.js");
6
+ const audit_request_js_1 = require("./audit-request.js");
7
+ const audit_subscriber_js_1 = require("./audit-subscriber.js");
8
+ exports.entities = [audit_request_js_1.AuditRequest, audit_action_js_1.AuditAction];
9
+ exports.resolvers = [audit_query_js_1.AuditQuery];
10
+ exports.subscribers = audit_subscriber_js_1.auditSubscribers;
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/audit/index.ts"],"names":[],"mappings":";;;AAAA,uDAA+C;AAC/C,qDAA6C;AAC7C,yDAAiD;AACjD,+DAAwD;AAE3C,QAAA,QAAQ,GAAG,CAAC,+BAAY,EAAE,6BAAW,CAAC,CAAA;AACtC,QAAA,SAAS,GAAG,CAAC,2BAAU,CAAC,CAAA;AACxB,QAAA,WAAW,GAAG,sCAAgB,CAAA","sourcesContent":["import { AuditAction } from './audit-action.js'\nimport { AuditQuery } from './audit-query.js'\nimport { AuditRequest } from './audit-request.js'\nimport { auditSubscribers } from './audit-subscriber.js'\n\nexport const entities = [AuditRequest, AuditAction]\nexport const resolvers = [AuditQuery]\nexport const subscribers = auditSubscribers\n"]}
@@ -6,6 +6,7 @@ const type_graphql_1 = require("type-graphql");
6
6
  const shell_1 = require("@things-factory/shell");
7
7
  const domain_owner_js_1 = require("./domain-owner.js");
8
8
  const user_js_1 = require("../user/user.js");
9
+ const audit_recorder_js_1 = require("../audit/audit-recorder.js");
9
10
  let DomainOwnerMutation = class DomainOwnerMutation {
10
11
  /**
11
12
  * 현재 도메인에 새 owner를 추가한다. 기존 도메인 오너 또는 수퍼유저만 호출 가능.
@@ -111,6 +112,8 @@ let DomainOwnerMutation = class DomainOwnerMutation {
111
112
  if (entry) {
112
113
  await ownerRepo.delete(entry.id);
113
114
  }
115
+ /* `delete()` passes no subscriber - the removal records itself, in the same transaction (ADR-0141 §4-1). */
116
+ await (0, audit_recorder_js_1.recordAuditAction)({ kind: 'domain.owner.remove', targetKind: 'User', targetIds: [targetUser.id], targetRole: 'handled' }, { manager: tx, domainId: domain.id });
114
117
  if (hasLegacyOwner) {
115
118
  // 캐시 재설정: 남은 DomainOwner 중 가장 먼저 추가된 사람
116
119
  const next = await ownerRepo
@@ -1 +1 @@
1
- {"version":3,"file":"domain-owner-mutation.js","sourceRoot":"","sources":["../../../server/service/domain-owner/domain-owner-mutation.ts"],"names":[],"mappings":"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","sourcesContent":["import { Resolver, Mutation, Arg, Ctx, Directive } from 'type-graphql'\nimport { Domain, getRepository } from '@things-factory/shell'\n\nimport { DomainOwner } from './domain-owner.js'\nimport { User } from '../user/user.js'\n\n@Resolver(of => DomainOwner)\nexport class DomainOwnerMutation {\n /**\n * 현재 도메인에 새 owner를 추가한다. 기존 도메인 오너 또는 수퍼유저만 호출 가능.\n *\n * 하위 호환: `Domain.owner` 캐시 필드가 비어 있으면 추가된 user를 세팅한다.\n */\n @Directive('@privilege(domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => DomainOwner, { description: 'Add a user as owner of the current domain.' })\n async addDomainOwner(\n @Arg('username') username: string,\n @Ctx() context: any,\n @Arg('reason', { nullable: true }) reason?: string\n ): Promise<DomainOwner> {\n const { domain, user: actor } = context.state\n\n return (getRepository(Domain) as any).manager.transaction(async (tx: any) => {\n const userRepo = tx.getRepository(User)\n const ownerRepo = tx.getRepository(DomainOwner)\n const domainRepo = tx.getRepository(Domain)\n\n // username 컬럼이 nullable — 사용자는 username 을 가지지 않고\n // email 만 갖는 경우가 많음. User.get username() 도 `username || email`\n // 을 리턴하므로 UI 가 email 을 보낼 수 있음. 둘 다 매칭한다.\n const targetUser = await userRepo\n .createQueryBuilder('u')\n .innerJoin('u.domains', 'd')\n .where('(u.username = :key OR u.email = :key) AND d.id = :domainId', {\n key: username,\n domainId: domain.id\n })\n .getOne()\n if (!targetUser) {\n throw new Error(`User '${username}' is not a member of this domain.`)\n }\n\n const existing = await ownerRepo.findOne({\n where: { domain: { id: domain.id }, user: { id: targetUser.id } }\n })\n if (existing) {\n throw new Error(`User '${username}' is already an owner of this domain.`)\n }\n\n const entry = ownerRepo.create({\n domain: { id: domain.id } as Domain,\n user: { id: targetUser.id } as User,\n grantedBy: actor,\n reason\n })\n const saved = await ownerRepo.save(entry)\n\n // 하위 호환 캐시: domain.owner가 비어 있으면 세팅\n if (!domain.owner) {\n await domainRepo.update(domain.id, { owner: targetUser.id })\n }\n\n // GraphQL 응답을 위해 relations 포함해 재조회.\n // (save() 반환값은 { user: {id}, grantedBy: actor } 인데 user/grantedBy 의\n // non-nullable 필드 email 등이 채워져 있지 않아 직렬화 에러 발생)\n return ownerRepo.findOneOrFail({\n where: { id: saved.id },\n relations: ['user', 'grantedBy', 'domain']\n })\n })\n }\n\n /**\n * 현재 도메인의 owner를 제거한다.\n * - 기존 도메인 오너 또는 수퍼유저만 호출 가능\n * - 마지막 남은 owner는 제거 불가 (최소 1명 유지)\n * - `Domain.owner` 캐시가 제거되는 user였다면, 남은 owner 중 하나로 재설정\n */\n @Directive('@privilege(domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => Boolean, { description: 'Remove a user from the owners of the current domain.' })\n async removeDomainOwner(\n @Arg('username') username: string,\n @Ctx() context: any,\n @Arg('reason', { nullable: true }) _reason?: string\n ): Promise<boolean> {\n const { domain } = context.state\n\n return (getRepository(Domain) as any).manager.transaction(async (tx: any) => {\n const userRepo = tx.getRepository(User)\n const ownerRepo = tx.getRepository(DomainOwner)\n const domainRepo = tx.getRepository(Domain)\n\n // username 또는 email 둘 다 허용 — addDomainOwner 와 대칭.\n const targetUser = await userRepo.findOne({\n where: [{ username }, { email: username }]\n })\n if (!targetUser) {\n throw new Error(`User '${username}' not found.`)\n }\n\n const entry = await ownerRepo.findOne({\n where: { domain: { id: domain.id }, user: { id: targetUser.id } }\n })\n const hasLegacyOwner = domain.owner === targetUser.id\n\n if (!entry && !hasLegacyOwner) {\n throw new Error(`User '${username}' is not an owner of this domain.`)\n }\n\n // 마지막 1인 보호: DomainOwner 테이블 기준 count + legacy owner 포함 여부\n const ownerCount = await ownerRepo\n .createQueryBuilder('ow')\n .where('ow.domain_id = :d', { d: domain.id })\n .getCount()\n\n // 남을 owner 수 계산:\n // - DomainOwner 테이블에서 제거 대상이 있으면 ownerCount - 1\n // - legacy owner가 별도로 있고 DomainOwner에 없다면 +1\n let remaining = ownerCount\n if (entry) remaining -= 1\n // legacy owner가 지금 제거 대상과 다른 user라면, 그 user도 하나의 owner로 카운트\n if (domain.owner && domain.owner !== targetUser.id) {\n // legacy owner가 DomainOwner 테이블에도 이미 있는지\n const legacyInTable = await ownerRepo\n .createQueryBuilder('ow')\n .where('ow.domain_id = :d AND ow.user_id = :u', { d: domain.id, u: domain.owner })\n .getExists()\n if (!legacyInTable) remaining += 1\n }\n\n if (remaining < 1) {\n throw new Error(\n 'Cannot remove the last owner of a domain. Add another owner first.'\n )\n }\n\n if (entry) {\n await ownerRepo.delete(entry.id)\n }\n\n if (hasLegacyOwner) {\n // 캐시 재설정: 남은 DomainOwner 중 가장 먼저 추가된 사람\n const next = await ownerRepo\n .createQueryBuilder('ow')\n .where('ow.domain_id = :d', { d: domain.id })\n .orderBy('ow.grantedAt', 'ASC')\n .getOne()\n await domainRepo.update(domain.id, { owner: next?.userId ?? null })\n }\n\n return true\n })\n }\n}\n"]}
1
+ {"version":3,"file":"domain-owner-mutation.js","sourceRoot":"","sources":["../../../server/service/domain-owner/domain-owner-mutation.ts"],"names":[],"mappings":"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","sourcesContent":["import { Resolver, Mutation, Arg, Ctx, Directive } from 'type-graphql'\nimport { Domain, getRepository } from '@things-factory/shell'\n\nimport { DomainOwner } from './domain-owner.js'\nimport { User } from '../user/user.js'\nimport { recordAuditAction } from '../audit/audit-recorder.js'\n\n@Resolver(of => DomainOwner)\nexport class DomainOwnerMutation {\n /**\n * 현재 도메인에 새 owner를 추가한다. 기존 도메인 오너 또는 수퍼유저만 호출 가능.\n *\n * 하위 호환: `Domain.owner` 캐시 필드가 비어 있으면 추가된 user를 세팅한다.\n */\n @Directive('@privilege(domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => DomainOwner, { description: 'Add a user as owner of the current domain.' })\n async addDomainOwner(\n @Arg('username') username: string,\n @Ctx() context: any,\n @Arg('reason', { nullable: true }) reason?: string\n ): Promise<DomainOwner> {\n const { domain, user: actor } = context.state\n\n return (getRepository(Domain) as any).manager.transaction(async (tx: any) => {\n const userRepo = tx.getRepository(User)\n const ownerRepo = tx.getRepository(DomainOwner)\n const domainRepo = tx.getRepository(Domain)\n\n // username 컬럼이 nullable — 사용자는 username 을 가지지 않고\n // email 만 갖는 경우가 많음. User.get username() 도 `username || email`\n // 을 리턴하므로 UI 가 email 을 보낼 수 있음. 둘 다 매칭한다.\n const targetUser = await userRepo\n .createQueryBuilder('u')\n .innerJoin('u.domains', 'd')\n .where('(u.username = :key OR u.email = :key) AND d.id = :domainId', {\n key: username,\n domainId: domain.id\n })\n .getOne()\n if (!targetUser) {\n throw new Error(`User '${username}' is not a member of this domain.`)\n }\n\n const existing = await ownerRepo.findOne({\n where: { domain: { id: domain.id }, user: { id: targetUser.id } }\n })\n if (existing) {\n throw new Error(`User '${username}' is already an owner of this domain.`)\n }\n\n const entry = ownerRepo.create({\n domain: { id: domain.id } as Domain,\n user: { id: targetUser.id } as User,\n grantedBy: actor,\n reason\n })\n const saved = await ownerRepo.save(entry)\n\n // 하위 호환 캐시: domain.owner가 비어 있으면 세팅\n if (!domain.owner) {\n await domainRepo.update(domain.id, { owner: targetUser.id })\n }\n\n // GraphQL 응답을 위해 relations 포함해 재조회.\n // (save() 반환값은 { user: {id}, grantedBy: actor } 인데 user/grantedBy 의\n // non-nullable 필드 email 등이 채워져 있지 않아 직렬화 에러 발생)\n return ownerRepo.findOneOrFail({\n where: { id: saved.id },\n relations: ['user', 'grantedBy', 'domain']\n })\n })\n }\n\n /**\n * 현재 도메인의 owner를 제거한다.\n * - 기존 도메인 오너 또는 수퍼유저만 호출 가능\n * - 마지막 남은 owner는 제거 불가 (최소 1명 유지)\n * - `Domain.owner` 캐시가 제거되는 user였다면, 남은 owner 중 하나로 재설정\n */\n @Directive('@privilege(domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => Boolean, { description: 'Remove a user from the owners of the current domain.' })\n async removeDomainOwner(\n @Arg('username') username: string,\n @Ctx() context: any,\n @Arg('reason', { nullable: true }) _reason?: string\n ): Promise<boolean> {\n const { domain } = context.state\n\n return (getRepository(Domain) as any).manager.transaction(async (tx: any) => {\n const userRepo = tx.getRepository(User)\n const ownerRepo = tx.getRepository(DomainOwner)\n const domainRepo = tx.getRepository(Domain)\n\n // username 또는 email 둘 다 허용 — addDomainOwner 와 대칭.\n const targetUser = await userRepo.findOne({\n where: [{ username }, { email: username }]\n })\n if (!targetUser) {\n throw new Error(`User '${username}' not found.`)\n }\n\n const entry = await ownerRepo.findOne({\n where: { domain: { id: domain.id }, user: { id: targetUser.id } }\n })\n const hasLegacyOwner = domain.owner === targetUser.id\n\n if (!entry && !hasLegacyOwner) {\n throw new Error(`User '${username}' is not an owner of this domain.`)\n }\n\n // 마지막 1인 보호: DomainOwner 테이블 기준 count + legacy owner 포함 여부\n const ownerCount = await ownerRepo\n .createQueryBuilder('ow')\n .where('ow.domain_id = :d', { d: domain.id })\n .getCount()\n\n // 남을 owner 수 계산:\n // - DomainOwner 테이블에서 제거 대상이 있으면 ownerCount - 1\n // - legacy owner가 별도로 있고 DomainOwner에 없다면 +1\n let remaining = ownerCount\n if (entry) remaining -= 1\n // legacy owner가 지금 제거 대상과 다른 user라면, 그 user도 하나의 owner로 카운트\n if (domain.owner && domain.owner !== targetUser.id) {\n // legacy owner가 DomainOwner 테이블에도 이미 있는지\n const legacyInTable = await ownerRepo\n .createQueryBuilder('ow')\n .where('ow.domain_id = :d AND ow.user_id = :u', { d: domain.id, u: domain.owner })\n .getExists()\n if (!legacyInTable) remaining += 1\n }\n\n if (remaining < 1) {\n throw new Error(\n 'Cannot remove the last owner of a domain. Add another owner first.'\n )\n }\n\n if (entry) {\n await ownerRepo.delete(entry.id)\n }\n\n /* `delete()` passes no subscriber - the removal records itself, in the same transaction (ADR-0141 §4-1). */\n await recordAuditAction(\n { kind: 'domain.owner.remove', targetKind: 'User', targetIds: [targetUser.id], targetRole: 'handled' },\n { manager: tx, domainId: domain.id }\n )\n\n if (hasLegacyOwner) {\n // 캐시 재설정: 남은 DomainOwner 중 가장 먼저 추가된 사람\n const next = await ownerRepo\n .createQueryBuilder('ow')\n .where('ow.domain_id = :d', { d: domain.id })\n .orderBy('ow.grantedAt', 'ASC')\n .getOne()\n await domainRepo.update(domain.id, { owner: next?.userId ?? null })\n }\n\n return true\n })\n }\n}\n"]}