@things-factory/auth-base 10.1.192 → 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.
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/controllers/signin.d.ts +5 -0
- package/dist-server/controllers/signin.js +25 -0
- package/dist-server/controllers/signin.js.map +1 -1
- package/dist-server/index.js +7 -0
- package/dist-server/index.js.map +1 -1
- package/dist-server/middlewares/audit-scope-middleware.d.ts +7 -0
- package/dist-server/middlewares/audit-scope-middleware.js +15 -0
- package/dist-server/middlewares/audit-scope-middleware.js.map +1 -0
- package/dist-server/middlewares/index.js +3 -0
- package/dist-server/middlewares/index.js.map +1 -1
- package/dist-server/role-tasks.d.ts +2 -0
- package/dist-server/role-tasks.js +20 -0
- package/dist-server/role-tasks.js.map +1 -0
- package/dist-server/router/auth-checkin-router.js +3 -5
- package/dist-server/router/auth-checkin-router.js.map +1 -1
- package/dist-server/router/auth-public-process-router.js +7 -1
- package/dist-server/router/auth-public-process-router.js.map +1 -1
- package/dist-server/router/site-root-router.d.ts +2 -0
- package/dist-server/router/site-root-router.js +2 -0
- package/dist-server/router/site-root-router.js.map +1 -1
- package/dist-server/service/access-request/index.d.ts +1 -1
- package/dist-server/service/audit/audit-action.d.ts +36 -0
- package/dist-server/service/audit/audit-action.js +137 -0
- package/dist-server/service/audit/audit-action.js.map +1 -0
- package/dist-server/service/audit/audit-kinds.d.ts +19 -0
- package/dist-server/service/audit/audit-kinds.js +70 -0
- package/dist-server/service/audit/audit-kinds.js.map +1 -0
- package/dist-server/service/audit/audit-query.d.ts +11 -0
- package/dist-server/service/audit/audit-query.js +295 -0
- package/dist-server/service/audit/audit-query.js.map +1 -0
- package/dist-server/service/audit/audit-recorder.d.ts +77 -0
- package/dist-server/service/audit/audit-recorder.js +247 -0
- package/dist-server/service/audit/audit-recorder.js.map +1 -0
- package/dist-server/service/audit/audit-request.d.ts +28 -0
- package/dist-server/service/audit/audit-request.js +96 -0
- package/dist-server/service/audit/audit-request.js.map +1 -0
- package/dist-server/service/audit/audit-retention.d.ts +12 -0
- package/dist-server/service/audit/audit-retention.js +42 -0
- package/dist-server/service/audit/audit-retention.js.map +1 -0
- package/dist-server/service/audit/audit-subscriber.d.ts +39 -0
- package/dist-server/service/audit/audit-subscriber.js +191 -0
- package/dist-server/service/audit/audit-subscriber.js.map +1 -0
- package/dist-server/service/audit/audit-target.d.ts +19 -0
- package/dist-server/service/audit/audit-target.js +46 -0
- package/dist-server/service/audit/audit-target.js.map +1 -0
- package/dist-server/service/audit/audit-types.d.ts +44 -0
- package/dist-server/service/audit/audit-types.js +161 -0
- package/dist-server/service/audit/audit-types.js.map +1 -0
- package/dist-server/service/audit/index.d.ts +6 -0
- package/dist-server/service/audit/index.js +11 -0
- package/dist-server/service/audit/index.js.map +1 -0
- package/dist-server/service/domain-owner/domain-owner-mutation.js +3 -0
- package/dist-server/service/domain-owner/domain-owner-mutation.js.map +1 -1
- package/dist-server/service/index.d.ts +9 -2
- package/dist-server/service/index.js +19 -4
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/open-fields.js +1 -1
- package/dist-server/service/open-fields.js.map +1 -1
- package/dist-server/service/privilege/privilege-directive.js +25 -1
- package/dist-server/service/privilege/privilege-directive.js.map +1 -1
- package/dist-server/service/role/role-mutation.js +8 -1
- package/dist-server/service/role/role-mutation.js.map +1 -1
- package/dist-server/service/role-template/index.d.ts +1 -1
- package/dist-server/service/role-template/role-task.d.ts +74 -0
- package/dist-server/service/role-template/role-task.js +113 -0
- package/dist-server/service/role-template/role-task.js.map +1 -0
- package/dist-server/service/role-template/role-template-gap-resolver.d.ts +9 -5
- package/dist-server/service/role-template/role-template-gap-resolver.js +41 -13
- package/dist-server/service/role-template/role-template-gap-resolver.js.map +1 -1
- package/dist-server/service/role-template/role-template-mutation.js +10 -1
- package/dist-server/service/role-template/role-template-mutation.js.map +1 -1
- package/dist-server/service/role-template/role-template-query.js +4 -2
- package/dist-server/service/role-template/role-template-query.js.map +1 -1
- package/dist-server/service/role-template/role-template-types.d.ts +24 -0
- package/dist-server/service/role-template/role-template-types.js +63 -1
- package/dist-server/service/role-template/role-template-types.js.map +1 -1
- package/dist-server/service/role-template/role-template.d.ts +63 -10
- package/dist-server/service/role-template/role-template.js +92 -18
- package/dist-server/service/role-template/role-template.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/dist-server/utils/check-permission.d.ts +6 -0
- package/dist-server/utils/check-permission.js +23 -8
- package/dist-server/utils/check-permission.js.map +1 -1
- package/package.json +3 -2
- package/tests/audit-db.test.ts +394 -0
- package/tests/auth-base-role-tasks.test.ts +58 -0
- package/tests/list-inheritance-db.test.ts +0 -7
- package/tests/open-doors.test.ts +2 -2
- package/tests/refusal-cause.test.ts +1 -1
- package/tests/role-task.test.ts +168 -0
- package/tests/role-template-gap-db.test.ts +77 -2
- package/translations/en.json +13 -1
- package/translations/es.json +13 -1
- package/translations/ja.json +13 -1
- package/translations/ko.json +13 -1
- package/translations/ms.json +13 -1
- package/translations/zh.json +13 -1
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runInAuditScope = runInAuditScope;
|
|
4
|
+
exports.currentAuditScope = currentAuditScope;
|
|
5
|
+
exports.noteAuditAction = noteAuditAction;
|
|
6
|
+
exports.recordAuditAction = recordAuditAction;
|
|
7
|
+
exports.addressOf = addressOf;
|
|
8
|
+
exports.operationOf = operationOf;
|
|
9
|
+
exports.auditHealth = auditHealth;
|
|
10
|
+
exports.flushAudit = flushAudit;
|
|
11
|
+
const async_hooks_1 = require("async_hooks");
|
|
12
|
+
const crypto_1 = require("crypto");
|
|
13
|
+
const graphql_1 = require("graphql");
|
|
14
|
+
const env_1 = require("@things-factory/env");
|
|
15
|
+
const shell_1 = require("@things-factory/shell");
|
|
16
|
+
const audit_action_js_1 = require("./audit-action.js");
|
|
17
|
+
const audit_kinds_js_1 = require("./audit-kinds.js");
|
|
18
|
+
const audit_request_js_1 = require("./audit-request.js");
|
|
19
|
+
/**
|
|
20
|
+
* ── The audit recorder (ADR-0141) ──────────────────────────────────────────
|
|
21
|
+
*
|
|
22
|
+
* A request runs inside a scope (`runInAuditScope`, installed once by the auth middleware). Door
|
|
23
|
+
* checks and results are merged into the scope while the request runs and written with the request
|
|
24
|
+
* row when it ends - one write per request, apart from the business transaction, and never in the
|
|
25
|
+
* way of the answer (§4-4). Events (`recordAuditAction`) are written when they happen: inside the
|
|
26
|
+
* caller's transaction when it hands one (§4-1, they fail together), on their own otherwise (§4-2 ·
|
|
27
|
+
* §4-3, a failure is counted and does not undo what already happened).
|
|
28
|
+
*
|
|
29
|
+
* Who and where are read from the request's context at write time, so a door checked before the
|
|
30
|
+
* user was known still lands under them once they are.
|
|
31
|
+
*/
|
|
32
|
+
const MAX_TARGETS = 100;
|
|
33
|
+
const storage = new async_hooks_1.AsyncLocalStorage();
|
|
34
|
+
/** Runs `work` as one audited request; the request row and merged actions are written when it ends. */
|
|
35
|
+
async function runInAuditScope(context, channel, work) {
|
|
36
|
+
const scope = {
|
|
37
|
+
requestId: (0, crypto_1.randomUUID)(),
|
|
38
|
+
channel,
|
|
39
|
+
context,
|
|
40
|
+
startedAt: Date.now(),
|
|
41
|
+
merged: new Map(),
|
|
42
|
+
recorded: false
|
|
43
|
+
};
|
|
44
|
+
try {
|
|
45
|
+
return await storage.run(scope, work);
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
track(finishAuditScope(scope));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function currentAuditScope() {
|
|
52
|
+
return storage.getStore();
|
|
53
|
+
}
|
|
54
|
+
/* ── merging door checks and results ───────────────────────────────────── */
|
|
55
|
+
function keyOf(input) {
|
|
56
|
+
return [input.kind, input.door, input.decision, input.basis, input.outcome, input.code, input.targetKind, input.targetRole].join('|');
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Notes a door check or result in the running request. Equal ones become one row with a count; their
|
|
60
|
+
* targets are kept together, up to 100 (ADR-0141 §3.2). Outside any request this does nothing - a
|
|
61
|
+
* subscription stream or a boot task is not audited at the door (§9 step 4).
|
|
62
|
+
*/
|
|
63
|
+
function noteAuditAction(input) {
|
|
64
|
+
const scope = currentAuditScope();
|
|
65
|
+
if (!scope)
|
|
66
|
+
return;
|
|
67
|
+
(0, audit_kinds_js_1.allowedDetail)(input.kind, input.detail);
|
|
68
|
+
const key = keyOf(input);
|
|
69
|
+
let merged = scope.merged.get(key);
|
|
70
|
+
if (!merged) {
|
|
71
|
+
merged = { ...input, count: 0, targets: new Set(), truncated: false, returned: undefined };
|
|
72
|
+
scope.merged.set(key, merged);
|
|
73
|
+
}
|
|
74
|
+
merged.count++;
|
|
75
|
+
if (typeof input.returned === 'number') {
|
|
76
|
+
merged.returned = (merged.returned ?? 0) + input.returned;
|
|
77
|
+
}
|
|
78
|
+
for (const id of input.targetIds || []) {
|
|
79
|
+
if (merged.targets.has(id))
|
|
80
|
+
continue;
|
|
81
|
+
if (merged.targets.size >= MAX_TARGETS) {
|
|
82
|
+
merged.truncated = true;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
merged.targets.add(id);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Writes one event now. With `manager` it shares the caller's transaction and throws with it;
|
|
90
|
+
* without, it is written on its own and a failure is only counted (ADR-0141 §4-2 · §4-3 · §4-5).
|
|
91
|
+
*/
|
|
92
|
+
async function recordAuditAction(input, options = {}) {
|
|
93
|
+
const scope = currentAuditScope();
|
|
94
|
+
const row = rowOf(input, scope, options);
|
|
95
|
+
if (scope) {
|
|
96
|
+
scope.recorded = true;
|
|
97
|
+
if (options.actorId)
|
|
98
|
+
scope.eventActorId ??= options.actorId;
|
|
99
|
+
if (options.domainId)
|
|
100
|
+
scope.eventDomainId ??= options.domainId;
|
|
101
|
+
}
|
|
102
|
+
if (options.manager) {
|
|
103
|
+
await options.manager.getRepository(audit_action_js_1.AuditAction).insert(row);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const writing = (0, shell_1.getRepository)(audit_action_js_1.AuditAction)
|
|
107
|
+
.insert(row)
|
|
108
|
+
.then(() => undefined, error => countFailure(error, input.kind));
|
|
109
|
+
track(writing);
|
|
110
|
+
await writing;
|
|
111
|
+
}
|
|
112
|
+
function rowOf(input, scope, options, count = 1, targets, truncated = false) {
|
|
113
|
+
const state = scope?.context?.state || {};
|
|
114
|
+
const actorId = options.actorId !== undefined ? options.actorId : state.user?.id;
|
|
115
|
+
const domainId = options.domainId !== undefined ? options.domainId : state.domain?.id;
|
|
116
|
+
const ids = targets ?? (input.targetIds || []).slice(0, MAX_TARGETS);
|
|
117
|
+
return {
|
|
118
|
+
requestId: scope?.requestId ?? null,
|
|
119
|
+
domain: domainId ? { id: domainId } : null,
|
|
120
|
+
actor: actorId ? { id: actorId } : null,
|
|
121
|
+
kind: input.kind,
|
|
122
|
+
door: input.door ?? null,
|
|
123
|
+
decision: input.decision ?? null,
|
|
124
|
+
basis: input.basis ?? null,
|
|
125
|
+
outcome: input.outcome ?? null,
|
|
126
|
+
code: input.code ? String(input.code).slice(0, 64) : null,
|
|
127
|
+
count,
|
|
128
|
+
returned: input.returned ?? null,
|
|
129
|
+
targetKind: input.targetKind ?? null,
|
|
130
|
+
targetIds: ids.length ? ids : null,
|
|
131
|
+
truncated: truncated || (input.targetIds || []).length > MAX_TARGETS,
|
|
132
|
+
targetRole: input.targetRole ?? null,
|
|
133
|
+
objectKind: input.objectKind ?? null,
|
|
134
|
+
objectId: input.objectId ? String(input.objectId).slice(0, 128) : null,
|
|
135
|
+
detail: (0, audit_kinds_js_1.allowedDetail)(input.kind, input.detail) ?? null
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/* ── the request row ────────────────────────────────────────────────────── */
|
|
139
|
+
/** The address a request came from - the first `x-forwarded-for` value, as the check-in record always took it. */
|
|
140
|
+
function addressOf(context) {
|
|
141
|
+
const forwarded = context?.req?.headers?.['x-forwarded-for'];
|
|
142
|
+
const address = forwarded ? forwarded.split(',')[0].trim() : context?.req?.socket?.remoteAddress;
|
|
143
|
+
return address ? String(address).slice(0, 64) : undefined;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* What a GraphQL request asked: its operation type and the root fields by their schema names - never
|
|
147
|
+
* an argument or a variable (ADR-0141 §3.4). A field with no `@privilege` leaves no action, so these
|
|
148
|
+
* names are what says which open door a request used. The parsed document is Apollo's and is not
|
|
149
|
+
* reachable from here; parsing again is the cost of knowing.
|
|
150
|
+
*/
|
|
151
|
+
function operationOf(body) {
|
|
152
|
+
const first = Array.isArray(body) ? body[0] : body;
|
|
153
|
+
if (!first || typeof first !== 'object' || typeof first.query !== 'string')
|
|
154
|
+
return {};
|
|
155
|
+
const name = typeof first.operationName === 'string' ? first.operationName.slice(0, 128) : undefined;
|
|
156
|
+
try {
|
|
157
|
+
const document = (0, graphql_1.parse)(first.query);
|
|
158
|
+
const operations = document.definitions.filter((one) => one.kind === graphql_1.Kind.OPERATION_DEFINITION);
|
|
159
|
+
const operation = operations.find(one => !name || one.name?.value === name) ?? operations[0];
|
|
160
|
+
if (!operation)
|
|
161
|
+
return { operationName: name };
|
|
162
|
+
const fragments = new Map(document.definitions.flatMap(one => (one.kind === graphql_1.Kind.FRAGMENT_DEFINITION ? [[one.name.value, one.selectionSet]] : [])));
|
|
163
|
+
const names = new Set();
|
|
164
|
+
const walk = (set, seen) => {
|
|
165
|
+
for (const selection of set.selections) {
|
|
166
|
+
if (selection.kind === graphql_1.Kind.FIELD)
|
|
167
|
+
names.add(selection.name.value);
|
|
168
|
+
else if (selection.kind === graphql_1.Kind.INLINE_FRAGMENT)
|
|
169
|
+
walk(selection.selectionSet, seen);
|
|
170
|
+
else if (selection.kind === graphql_1.Kind.FRAGMENT_SPREAD && !seen.has(selection.name.value)) {
|
|
171
|
+
const spread = fragments.get(selection.name.value);
|
|
172
|
+
if (spread)
|
|
173
|
+
walk(spread, new Set([...seen, selection.name.value]));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
walk(operation.selectionSet, new Set());
|
|
178
|
+
return { operationType: operation.operation, operationName: name, fields: [...names].join(',').slice(0, 256) || undefined };
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return { operationName: name };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async function finishAuditScope(scope) {
|
|
185
|
+
if (scope.channel === 'http' && !scope.recorded && scope.merged.size === 0)
|
|
186
|
+
return;
|
|
187
|
+
const context = scope.context;
|
|
188
|
+
const state = context?.state || {};
|
|
189
|
+
const domainId = state.domain?.id ?? scope.eventDomainId;
|
|
190
|
+
const actorId = state.user?.id ?? scope.eventActorId;
|
|
191
|
+
const request = {
|
|
192
|
+
id: scope.requestId,
|
|
193
|
+
domain: domainId ? { id: domainId } : null,
|
|
194
|
+
actor: actorId ? { id: actorId } : null,
|
|
195
|
+
ip: addressOf(context) ?? null,
|
|
196
|
+
userAgent: context?.request?.header?.['user-agent']?.slice(0, 256) ?? null,
|
|
197
|
+
durationMs: Date.now() - scope.startedAt,
|
|
198
|
+
channel: scope.channel,
|
|
199
|
+
route: scope.channel === 'http' ? (context?._matchedRoute ? String(context._matchedRoute).slice(0, 128) : null) : null,
|
|
200
|
+
...(scope.channel === 'graphql' ? operationOf(context?.request?.body) : {})
|
|
201
|
+
};
|
|
202
|
+
const actions = [...scope.merged.values()].map(merged => rowOf(merged, scope, {}, merged.count, [...merged.targets], merged.truncated));
|
|
203
|
+
/*
|
|
204
|
+
* Two plain inserts, no transaction of their own: a sqlite database has one connection, and a
|
|
205
|
+
* transaction opened here collided with the requests' own (「cannot start a transaction within a
|
|
206
|
+
* transaction」, seen on Office's development database). The line and its actions are both best
|
|
207
|
+
* effort (§4-4); one failing is counted like the other.
|
|
208
|
+
*/
|
|
209
|
+
try {
|
|
210
|
+
await (0, shell_1.getRepository)(audit_request_js_1.AuditRequest).insert(request);
|
|
211
|
+
if (actions.length)
|
|
212
|
+
await (0, shell_1.getRepository)(audit_action_js_1.AuditAction).insert(actions);
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
countFailure(error, 'request');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/* ── failures, counted apart from the audit store (ADR-0141 §4-5) ──────── */
|
|
219
|
+
const health = {
|
|
220
|
+
since: new Date(),
|
|
221
|
+
failures: 0,
|
|
222
|
+
firstFailedAt: null,
|
|
223
|
+
lastFailedAt: null
|
|
224
|
+
};
|
|
225
|
+
function countFailure(error, what) {
|
|
226
|
+
const now = new Date();
|
|
227
|
+
health.failures++;
|
|
228
|
+
health.firstFailedAt ??= now;
|
|
229
|
+
health.lastFailedAt = now;
|
|
230
|
+
env_1.logger.error(`[audit] not written (${what}): ${error?.message}`);
|
|
231
|
+
}
|
|
232
|
+
/** Failures this server process detected since it started. Not those it never got to try. */
|
|
233
|
+
function auditHealth() {
|
|
234
|
+
return { ...health };
|
|
235
|
+
}
|
|
236
|
+
/* ── for tests: wait for every write started so far ────────────────────── */
|
|
237
|
+
const pending = new Set();
|
|
238
|
+
function track(promise) {
|
|
239
|
+
pending.add(promise);
|
|
240
|
+
promise.finally(() => pending.delete(promise));
|
|
241
|
+
}
|
|
242
|
+
async function flushAudit() {
|
|
243
|
+
while (pending.size) {
|
|
244
|
+
await Promise.allSettled([...pending]);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
//# sourceMappingURL=audit-recorder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-recorder.js","sourceRoot":"","sources":["../../../server/service/audit/audit-recorder.ts"],"names":[],"mappings":"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","sourcesContent":["import { AsyncLocalStorage } from 'async_hooks'\nimport { randomUUID } from 'crypto'\nimport { Kind, OperationDefinitionNode, parse, SelectionSetNode } from 'graphql'\nimport { EntityManager } from 'typeorm'\n\nimport { logger } from '@things-factory/env'\nimport { getRepository } from '@things-factory/shell'\n\nimport { AuditAction } from './audit-action.js'\nimport { allowedDetail, AuditDetailValue } from './audit-kinds.js'\nimport { AuditRequest } from './audit-request.js'\n\n/**\n * ── The audit recorder (ADR-0141) ──────────────────────────────────────────\n *\n * A request runs inside a scope (`runInAuditScope`, installed once by the auth middleware). Door\n * checks and results are merged into the scope while the request runs and written with the request\n * row when it ends - one write per request, apart from the business transaction, and never in the\n * way of the answer (§4-4). Events (`recordAuditAction`) are written when they happen: inside the\n * caller's transaction when it hands one (§4-1, they fail together), on their own otherwise (§4-2 ·\n * §4-3, a failure is counted and does not undo what already happened).\n *\n * Who and where are read from the request's context at write time, so a door checked before the\n * user was known still lands under them once they are.\n */\n\nconst MAX_TARGETS = 100\n\nexport type AuditChannel = 'graphql' | 'http'\n\nexport interface AuditActionInput {\n kind: string\n door?: string\n decision?: 'granted' | 'denied'\n basis?: string\n outcome?: 'ok' | 'error'\n code?: string\n returned?: number\n targetKind?: string\n targetIds?: string[]\n targetRole?: 'requested' | 'handled'\n objectKind?: string\n objectId?: string\n detail?: Record<string, AuditDetailValue>\n}\n\ninterface MergedAction extends AuditActionInput {\n count: number\n targets: Set<string>\n truncated: boolean\n}\n\nexport interface AuditScope {\n requestId: string\n channel: AuditChannel\n context: any\n startedAt: number\n merged: Map<string, MergedAction>\n /* An http request is kept only when something in it was recorded. */\n recorded: boolean\n /* Who and where its events named, for a request that ran before the context knew (a sign-in, a check-in). */\n eventActorId?: string\n eventDomainId?: string\n}\n\nconst storage = new AsyncLocalStorage<AuditScope>()\n\n/** Runs `work` as one audited request; the request row and merged actions are written when it ends. */\nexport async function runInAuditScope<T>(context: any, channel: AuditChannel, work: () => Promise<T>): Promise<T> {\n const scope: AuditScope = {\n requestId: randomUUID(),\n channel,\n context,\n startedAt: Date.now(),\n merged: new Map(),\n recorded: false\n }\n\n try {\n return await storage.run(scope, work)\n } finally {\n track(finishAuditScope(scope))\n }\n}\n\nexport function currentAuditScope(): AuditScope | undefined {\n return storage.getStore()\n}\n\n/* ── merging door checks and results ───────────────────────────────────── */\n\nfunction keyOf(input: AuditActionInput) {\n return [input.kind, input.door, input.decision, input.basis, input.outcome, input.code, input.targetKind, input.targetRole].join('|')\n}\n\n/**\n * Notes a door check or result in the running request. Equal ones become one row with a count; their\n * targets are kept together, up to 100 (ADR-0141 §3.2). Outside any request this does nothing - a\n * subscription stream or a boot task is not audited at the door (§9 step 4).\n */\nexport function noteAuditAction(input: AuditActionInput) {\n const scope = currentAuditScope()\n if (!scope) return\n\n allowedDetail(input.kind, input.detail)\n\n const key = keyOf(input)\n let merged = scope.merged.get(key)\n if (!merged) {\n merged = { ...input, count: 0, targets: new Set(), truncated: false, returned: undefined }\n scope.merged.set(key, merged)\n }\n\n merged.count++\n if (typeof input.returned === 'number') {\n merged.returned = (merged.returned ?? 0) + input.returned\n }\n for (const id of input.targetIds || []) {\n if (merged.targets.has(id)) continue\n if (merged.targets.size >= MAX_TARGETS) {\n merged.truncated = true\n break\n }\n merged.targets.add(id)\n }\n}\n\n/* ── events ─────────────────────────────────────────────────────────────── */\n\nexport interface RecordOptions {\n /** The business transaction. Given, the event is written in it and a failure fails the change (§4-1). */\n manager?: EntityManager\n /** Who did it, when the context does not know yet (a sign-in). `null` says nobody is known. */\n actorId?: string | null\n /** Where, when it is not the request's domain (a role of another domain, a sign-in with no domain). */\n domainId?: string | null\n}\n\n/**\n * Writes one event now. With `manager` it shares the caller's transaction and throws with it;\n * without, it is written on its own and a failure is only counted (ADR-0141 §4-2 · §4-3 · §4-5).\n */\nexport async function recordAuditAction(input: AuditActionInput, options: RecordOptions = {}): Promise<void> {\n const scope = currentAuditScope()\n const row = rowOf(input, scope, options)\n\n if (scope) {\n scope.recorded = true\n if (options.actorId) scope.eventActorId ??= options.actorId\n if (options.domainId) scope.eventDomainId ??= options.domainId\n }\n\n if (options.manager) {\n await options.manager.getRepository(AuditAction).insert(row)\n return\n }\n\n const writing = getRepository(AuditAction)\n .insert(row)\n .then(\n () => undefined,\n error => countFailure(error, input.kind)\n )\n track(writing)\n await writing\n}\n\nfunction rowOf(input: AuditActionInput, scope: AuditScope | undefined, options: RecordOptions, count = 1, targets?: string[], truncated = false) {\n const state = scope?.context?.state || {}\n const actorId = options.actorId !== undefined ? options.actorId : state.user?.id\n const domainId = options.domainId !== undefined ? options.domainId : state.domain?.id\n const ids = targets ?? (input.targetIds || []).slice(0, MAX_TARGETS)\n\n return {\n requestId: scope?.requestId ?? null,\n domain: domainId ? { id: domainId } : null,\n actor: actorId ? { id: actorId } : null,\n kind: input.kind,\n door: input.door ?? null,\n decision: input.decision ?? null,\n basis: input.basis ?? null,\n outcome: input.outcome ?? null,\n code: input.code ? String(input.code).slice(0, 64) : null,\n count,\n returned: input.returned ?? null,\n targetKind: input.targetKind ?? null,\n targetIds: ids.length ? ids : null,\n truncated: truncated || (input.targetIds || []).length > MAX_TARGETS,\n targetRole: input.targetRole ?? null,\n objectKind: input.objectKind ?? null,\n objectId: input.objectId ? String(input.objectId).slice(0, 128) : null,\n detail: allowedDetail(input.kind, input.detail) ?? null\n } as any\n}\n\n/* ── the request row ────────────────────────────────────────────────────── */\n\n/** The address a request came from - the first `x-forwarded-for` value, as the check-in record always took it. */\nexport function addressOf(context: any): string | undefined {\n const forwarded = context?.req?.headers?.['x-forwarded-for'] as string | undefined\n const address = forwarded ? forwarded.split(',')[0].trim() : context?.req?.socket?.remoteAddress\n return address ? String(address).slice(0, 64) : undefined\n}\n\n/**\n * What a GraphQL request asked: its operation type and the root fields by their schema names - never\n * an argument or a variable (ADR-0141 §3.4). A field with no `@privilege` leaves no action, so these\n * names are what says which open door a request used. The parsed document is Apollo's and is not\n * reachable from here; parsing again is the cost of knowing.\n */\nexport function operationOf(body: any): { operationType?: string; operationName?: string; fields?: string } {\n const first = Array.isArray(body) ? body[0] : body\n if (!first || typeof first !== 'object' || typeof first.query !== 'string') return {}\n\n const name = typeof first.operationName === 'string' ? first.operationName.slice(0, 128) : undefined\n\n try {\n const document = parse(first.query)\n const operations = document.definitions.filter((one): one is OperationDefinitionNode => one.kind === Kind.OPERATION_DEFINITION)\n const operation = operations.find(one => !name || one.name?.value === name) ?? operations[0]\n if (!operation) return { operationName: name }\n\n const fragments = new Map(\n document.definitions.flatMap(one => (one.kind === Kind.FRAGMENT_DEFINITION ? [[one.name.value, one.selectionSet] as const] : []))\n )\n const names = new Set<string>()\n const walk = (set: SelectionSetNode, seen: Set<string>) => {\n for (const selection of set.selections) {\n if (selection.kind === Kind.FIELD) names.add(selection.name.value)\n else if (selection.kind === Kind.INLINE_FRAGMENT) walk(selection.selectionSet, seen)\n else if (selection.kind === Kind.FRAGMENT_SPREAD && !seen.has(selection.name.value)) {\n const spread = fragments.get(selection.name.value)\n if (spread) walk(spread, new Set([...seen, selection.name.value]))\n }\n }\n }\n walk(operation.selectionSet, new Set())\n\n return { operationType: operation.operation, operationName: name, fields: [...names].join(',').slice(0, 256) || undefined }\n } catch {\n return { operationName: name }\n }\n}\n\nasync function finishAuditScope(scope: AuditScope): Promise<void> {\n if (scope.channel === 'http' && !scope.recorded && scope.merged.size === 0) return\n\n const context = scope.context\n const state = context?.state || {}\n\n const domainId = state.domain?.id ?? scope.eventDomainId\n const actorId = state.user?.id ?? scope.eventActorId\n\n const request = {\n id: scope.requestId,\n domain: domainId ? { id: domainId } : null,\n actor: actorId ? { id: actorId } : null,\n ip: addressOf(context) ?? null,\n userAgent: (context?.request?.header?.['user-agent'] as string | undefined)?.slice(0, 256) ?? null,\n durationMs: Date.now() - scope.startedAt,\n channel: scope.channel,\n route: scope.channel === 'http' ? (context?._matchedRoute ? String(context._matchedRoute).slice(0, 128) : null) : null,\n ...(scope.channel === 'graphql' ? operationOf(context?.request?.body) : {})\n } as any\n\n const actions = [...scope.merged.values()].map(merged =>\n rowOf(merged, scope, {}, merged.count, [...merged.targets], merged.truncated)\n )\n\n /*\n * Two plain inserts, no transaction of their own: a sqlite database has one connection, and a\n * transaction opened here collided with the requests' own (「cannot start a transaction within a\n * transaction」, seen on Office's development database). The line and its actions are both best\n * effort (§4-4); one failing is counted like the other.\n */\n try {\n await getRepository(AuditRequest).insert(request)\n if (actions.length) await getRepository(AuditAction).insert(actions)\n } catch (error) {\n countFailure(error, 'request')\n }\n}\n\n/* ── failures, counted apart from the audit store (ADR-0141 §4-5) ──────── */\n\nconst health = {\n since: new Date(),\n failures: 0,\n firstFailedAt: null as Date | null,\n lastFailedAt: null as Date | null\n}\n\nfunction countFailure(error: unknown, what: string) {\n const now = new Date()\n health.failures++\n health.firstFailedAt ??= now\n health.lastFailedAt = now\n logger.error(`[audit] not written (${what}): ${(error as Error)?.message}`)\n}\n\n/** Failures this server process detected since it started. Not those it never got to try. */\nexport function auditHealth() {\n return { ...health }\n}\n\n/* ── for tests: wait for every write started so far ────────────────────── */\n\nconst pending = new Set<Promise<unknown>>()\n\nfunction track(promise: Promise<unknown>) {\n pending.add(promise)\n promise.finally(() => pending.delete(promise))\n}\n\nexport async function flushAudit(): Promise<void> {\n while (pending.size) {\n await Promise.allSettled([...pending])\n }\n}\n"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Domain } from '@things-factory/shell';
|
|
2
|
+
import { User } from '../user/user.js';
|
|
3
|
+
/**
|
|
4
|
+
* One request that reached the server, as the audit keeps it (ADR-0141 §3.1).
|
|
5
|
+
*
|
|
6
|
+
* Every GraphQL request has one; any other request has one when it left at least one action
|
|
7
|
+
* (a sign-in, a check-in). What happened inside is in `AuditAction` rows carrying this id.
|
|
8
|
+
*
|
|
9
|
+
* The row is never changed or deleted by the application (ADR-0141 §4-6), so the entity has no
|
|
10
|
+
* updater, `updatedAt` or `deletedAt`. Its relations carry no foreign key: an audit row must
|
|
11
|
+
* neither stop a user or domain from being removed nor go with them.
|
|
12
|
+
*/
|
|
13
|
+
export declare class AuditRequest {
|
|
14
|
+
id: string;
|
|
15
|
+
domain?: Domain;
|
|
16
|
+
domainId?: string;
|
|
17
|
+
actor?: User;
|
|
18
|
+
actorId?: string;
|
|
19
|
+
createdAt: Date;
|
|
20
|
+
ip?: string;
|
|
21
|
+
userAgent?: string;
|
|
22
|
+
durationMs?: number;
|
|
23
|
+
channel: string;
|
|
24
|
+
operationType?: string;
|
|
25
|
+
operationName?: string;
|
|
26
|
+
fields?: string;
|
|
27
|
+
route?: string;
|
|
28
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AuditRequest = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const type_graphql_1 = require("type-graphql");
|
|
6
|
+
const typeorm_1 = require("typeorm");
|
|
7
|
+
const shell_1 = require("@things-factory/shell");
|
|
8
|
+
const user_js_1 = require("../user/user.js");
|
|
9
|
+
/**
|
|
10
|
+
* One request that reached the server, as the audit keeps it (ADR-0141 §3.1).
|
|
11
|
+
*
|
|
12
|
+
* Every GraphQL request has one; any other request has one when it left at least one action
|
|
13
|
+
* (a sign-in, a check-in). What happened inside is in `AuditAction` rows carrying this id.
|
|
14
|
+
*
|
|
15
|
+
* The row is never changed or deleted by the application (ADR-0141 §4-6), so the entity has no
|
|
16
|
+
* updater, `updatedAt` or `deletedAt`. Its relations carry no foreign key: an audit row must
|
|
17
|
+
* neither stop a user or domain from being removed nor go with them.
|
|
18
|
+
*/
|
|
19
|
+
let AuditRequest = class AuditRequest {
|
|
20
|
+
};
|
|
21
|
+
exports.AuditRequest = AuditRequest;
|
|
22
|
+
tslib_1.__decorate([
|
|
23
|
+
(0, typeorm_1.PrimaryColumn)({ type: 'varchar', length: 36 }),
|
|
24
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.ID, { description: 'Request id, shared by the actions of this request.' }),
|
|
25
|
+
tslib_1.__metadata("design:type", String)
|
|
26
|
+
], AuditRequest.prototype, "id", void 0);
|
|
27
|
+
tslib_1.__decorate([
|
|
28
|
+
(0, typeorm_1.ManyToOne)(type => shell_1.Domain, { nullable: true, createForeignKeyConstraints: false }),
|
|
29
|
+
(0, type_graphql_1.Field)(type => shell_1.Domain, { nullable: true, description: 'The domain the request was made in.' }),
|
|
30
|
+
tslib_1.__metadata("design:type", shell_1.Domain)
|
|
31
|
+
], AuditRequest.prototype, "domain", void 0);
|
|
32
|
+
tslib_1.__decorate([
|
|
33
|
+
(0, typeorm_1.RelationId)((request) => request.domain),
|
|
34
|
+
tslib_1.__metadata("design:type", String)
|
|
35
|
+
], AuditRequest.prototype, "domainId", void 0);
|
|
36
|
+
tslib_1.__decorate([
|
|
37
|
+
(0, typeorm_1.ManyToOne)(type => user_js_1.User, { nullable: true, createForeignKeyConstraints: false }),
|
|
38
|
+
(0, type_graphql_1.Field)(type => user_js_1.User, { nullable: true, description: 'Who made the request. Empty when nobody was signed in.' }),
|
|
39
|
+
tslib_1.__metadata("design:type", user_js_1.User)
|
|
40
|
+
], AuditRequest.prototype, "actor", void 0);
|
|
41
|
+
tslib_1.__decorate([
|
|
42
|
+
(0, typeorm_1.RelationId)((request) => request.actor),
|
|
43
|
+
tslib_1.__metadata("design:type", String)
|
|
44
|
+
], AuditRequest.prototype, "actorId", void 0);
|
|
45
|
+
tslib_1.__decorate([
|
|
46
|
+
(0, typeorm_1.CreateDateColumn)(),
|
|
47
|
+
(0, type_graphql_1.Field)({ description: 'When the server finished the request.' }),
|
|
48
|
+
tslib_1.__metadata("design:type", Date)
|
|
49
|
+
], AuditRequest.prototype, "createdAt", void 0);
|
|
50
|
+
tslib_1.__decorate([
|
|
51
|
+
(0, typeorm_1.Column)({ nullable: true, length: 64 }),
|
|
52
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'Address the request came from.' }),
|
|
53
|
+
tslib_1.__metadata("design:type", String)
|
|
54
|
+
], AuditRequest.prototype, "ip", void 0);
|
|
55
|
+
tslib_1.__decorate([
|
|
56
|
+
(0, typeorm_1.Column)({ nullable: true, length: 256 }),
|
|
57
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'User-Agent header, cut at 256 characters.' }),
|
|
58
|
+
tslib_1.__metadata("design:type", String)
|
|
59
|
+
], AuditRequest.prototype, "userAgent", void 0);
|
|
60
|
+
tslib_1.__decorate([
|
|
61
|
+
(0, typeorm_1.Column)({ type: 'int', nullable: true }),
|
|
62
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Int, { nullable: true, description: 'Milliseconds the server spent on the request.' }),
|
|
63
|
+
tslib_1.__metadata("design:type", Number)
|
|
64
|
+
], AuditRequest.prototype, "durationMs", void 0);
|
|
65
|
+
tslib_1.__decorate([
|
|
66
|
+
(0, typeorm_1.Column)({ length: 16 }),
|
|
67
|
+
(0, type_graphql_1.Field)({ description: 'graphql, or http for any other request.' }),
|
|
68
|
+
tslib_1.__metadata("design:type", String)
|
|
69
|
+
], AuditRequest.prototype, "channel", void 0);
|
|
70
|
+
tslib_1.__decorate([
|
|
71
|
+
(0, typeorm_1.Column)({ nullable: true, length: 16 }),
|
|
72
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'query, mutation or subscription - for a GraphQL request.' }),
|
|
73
|
+
tslib_1.__metadata("design:type", String)
|
|
74
|
+
], AuditRequest.prototype, "operationType", void 0);
|
|
75
|
+
tslib_1.__decorate([
|
|
76
|
+
(0, typeorm_1.Column)({ nullable: true, length: 128 }),
|
|
77
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'The operation name the client gave. For reference only - the client chooses it.' }),
|
|
78
|
+
tslib_1.__metadata("design:type", String)
|
|
79
|
+
], AuditRequest.prototype, "operationName", void 0);
|
|
80
|
+
tslib_1.__decorate([
|
|
81
|
+
(0, typeorm_1.Column)({ nullable: true, length: 256 }),
|
|
82
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'The root fields a GraphQL request asked, by their schema names - never an argument.' }),
|
|
83
|
+
tslib_1.__metadata("design:type", String)
|
|
84
|
+
], AuditRequest.prototype, "fields", void 0);
|
|
85
|
+
tslib_1.__decorate([
|
|
86
|
+
(0, typeorm_1.Column)({ nullable: true, length: 128 }),
|
|
87
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'The route pattern of a request outside GraphQL. Never the path itself.' }),
|
|
88
|
+
tslib_1.__metadata("design:type", String)
|
|
89
|
+
], AuditRequest.prototype, "route", void 0);
|
|
90
|
+
exports.AuditRequest = AuditRequest = tslib_1.__decorate([
|
|
91
|
+
(0, typeorm_1.Entity)(),
|
|
92
|
+
(0, typeorm_1.Index)('ix_audit_request_0', (request) => [request.domain, request.createdAt]),
|
|
93
|
+
(0, typeorm_1.Index)('ix_audit_request_1', (request) => [request.actor, request.createdAt]),
|
|
94
|
+
(0, type_graphql_1.ObjectType)({ description: 'A request that reached the server, as the audit keeps it.' })
|
|
95
|
+
], AuditRequest);
|
|
96
|
+
//# sourceMappingURL=audit-request.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-request.js","sourceRoot":"","sources":["../../../server/service/audit/audit-request.ts"],"names":[],"mappings":"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","sourcesContent":["import { Field, ID, Int, ObjectType } from 'type-graphql'\nimport { Column, CreateDateColumn, Entity, Index, ManyToOne, PrimaryColumn, RelationId } from 'typeorm'\n\nimport { Domain } from '@things-factory/shell'\n\nimport { User } from '../user/user.js'\n\n/**\n * One request that reached the server, as the audit keeps it (ADR-0141 §3.1).\n *\n * Every GraphQL request has one; any other request has one when it left at least one action\n * (a sign-in, a check-in). What happened inside is in `AuditAction` rows carrying this id.\n *\n * The row is never changed or deleted by the application (ADR-0141 §4-6), so the entity has no\n * updater, `updatedAt` or `deletedAt`. Its relations carry no foreign key: an audit row must\n * neither stop a user or domain from being removed nor go with them.\n */\n@Entity()\n@Index('ix_audit_request_0', (request: AuditRequest) => [request.domain, request.createdAt])\n@Index('ix_audit_request_1', (request: AuditRequest) => [request.actor, request.createdAt])\n@ObjectType({ description: 'A request that reached the server, as the audit keeps it.' })\nexport class AuditRequest {\n /* Given by the recorder at the start of the request, so actions written mid-request can name it. */\n @PrimaryColumn({ type: 'varchar', length: 36 })\n @Field(type => ID, { description: 'Request id, shared by the actions of this request.' })\n id: string\n\n @ManyToOne(type => Domain, { nullable: true, createForeignKeyConstraints: false })\n @Field(type => Domain, { nullable: true, description: 'The domain the request was made in.' })\n domain?: Domain\n\n @RelationId((request: AuditRequest) => request.domain)\n domainId?: string\n\n @ManyToOne(type => User, { nullable: true, createForeignKeyConstraints: false })\n @Field(type => User, { nullable: true, description: 'Who made the request. Empty when nobody was signed in.' })\n actor?: User\n\n @RelationId((request: AuditRequest) => request.actor)\n actorId?: string\n\n @CreateDateColumn()\n @Field({ description: 'When the server finished the request.' })\n createdAt: Date\n\n @Column({ nullable: true, length: 64 })\n @Field({ nullable: true, description: 'Address the request came from.' })\n ip?: string\n\n @Column({ nullable: true, length: 256 })\n @Field({ nullable: true, description: 'User-Agent header, cut at 256 characters.' })\n userAgent?: string\n\n @Column({ type: 'int', nullable: true })\n @Field(type => Int, { nullable: true, description: 'Milliseconds the server spent on the request.' })\n durationMs?: number\n\n @Column({ length: 16 })\n @Field({ description: 'graphql, or http for any other request.' })\n channel: string\n\n @Column({ nullable: true, length: 16 })\n @Field({ nullable: true, description: 'query, mutation or subscription - for a GraphQL request.' })\n operationType?: string\n\n @Column({ nullable: true, length: 128 })\n @Field({ nullable: true, description: 'The operation name the client gave. For reference only - the client chooses it.' })\n operationName?: string\n\n @Column({ nullable: true, length: 256 })\n @Field({ nullable: true, description: 'The root fields a GraphQL request asked, by their schema names - never an argument.' })\n fields?: string\n\n @Column({ nullable: true, length: 128 })\n @Field({ nullable: true, description: 'The route pattern of a request outside GraphQL. Never the path itself.' })\n route?: string\n}\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit rows older than the retention are removed by this task and by nothing else - the
|
|
3
|
+
* application has no door that changes or deletes them (ADR-0141 §4-6 · §7). One year unless
|
|
4
|
+
* `audit/retentionDays` says otherwise.
|
|
5
|
+
*/
|
|
6
|
+
export declare function auditRetentionDays(): number;
|
|
7
|
+
/** Removes what is older than the retention, counted from `now`. */
|
|
8
|
+
export declare function purgeExpiredAudit(now?: Date): Promise<{
|
|
9
|
+
requests: number;
|
|
10
|
+
actions: number;
|
|
11
|
+
}>;
|
|
12
|
+
export declare function startAuditRetention(): import("@things-factory/task-lease").PeriodicTask;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.auditRetentionDays = auditRetentionDays;
|
|
4
|
+
exports.purgeExpiredAudit = purgeExpiredAudit;
|
|
5
|
+
exports.startAuditRetention = startAuditRetention;
|
|
6
|
+
const typeorm_1 = require("typeorm");
|
|
7
|
+
const env_1 = require("@things-factory/env");
|
|
8
|
+
const shell_1 = require("@things-factory/shell");
|
|
9
|
+
const task_lease_1 = require("@things-factory/task-lease");
|
|
10
|
+
const audit_action_js_1 = require("./audit-action.js");
|
|
11
|
+
const audit_request_js_1 = require("./audit-request.js");
|
|
12
|
+
/**
|
|
13
|
+
* Audit rows older than the retention are removed by this task and by nothing else - the
|
|
14
|
+
* application has no door that changes or deletes them (ADR-0141 §4-6 · §7). One year unless
|
|
15
|
+
* `audit/retentionDays` says otherwise.
|
|
16
|
+
*/
|
|
17
|
+
function auditRetentionDays() {
|
|
18
|
+
const days = Number(env_1.config.get('audit/retentionDays'));
|
|
19
|
+
return Number.isFinite(days) && days > 0 ? days : 365;
|
|
20
|
+
}
|
|
21
|
+
/** Removes what is older than the retention, counted from `now`. */
|
|
22
|
+
async function purgeExpiredAudit(now = new Date()) {
|
|
23
|
+
const before = new Date(now.getTime() - auditRetentionDays() * 24 * 60 * 60 * 1000);
|
|
24
|
+
const actions = await (0, shell_1.getRepository)(audit_action_js_1.AuditAction).delete({ createdAt: (0, typeorm_1.LessThan)(before) });
|
|
25
|
+
const requests = await (0, shell_1.getRepository)(audit_request_js_1.AuditRequest).delete({ createdAt: (0, typeorm_1.LessThan)(before) });
|
|
26
|
+
return { requests: requests.affected ?? 0, actions: actions.affected ?? 0 };
|
|
27
|
+
}
|
|
28
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
29
|
+
function startAuditRetention() {
|
|
30
|
+
return (0, task_lease_1.startPeriodicTask)({
|
|
31
|
+
name: 'audit-retention',
|
|
32
|
+
intervalMs: DAY_MS,
|
|
33
|
+
work: async () => {
|
|
34
|
+
const removed = await purgeExpiredAudit();
|
|
35
|
+
if (removed.requests || removed.actions) {
|
|
36
|
+
env_1.logger.info(`[audit] removed ${removed.requests} requests and ${removed.actions} actions past ${auditRetentionDays()} days`);
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
onFailure: failure => env_1.logger.error(`[audit] retention round failed: ${failure.error}`)
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=audit-retention.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-retention.js","sourceRoot":"","sources":["../../../server/service/audit/audit-retention.ts"],"names":[],"mappings":";;AAcA,gDAGC;AAGD,8CAOC;AAID,kDAYC;AA3CD,qCAAkC;AAElC,6CAAoD;AACpD,iDAAqD;AACrD,2DAA8D;AAE9D,uDAA+C;AAC/C,yDAAiD;AAEjD;;;;GAIG;AACH,SAAgB,kBAAkB;IAChC,MAAM,IAAI,GAAG,MAAM,CAAC,YAAM,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC,CAAA;IACtD,OAAO,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAA;AACvD,CAAC;AAED,oEAAoE;AAC7D,KAAK,UAAU,iBAAiB,CAAC,GAAG,GAAG,IAAI,IAAI,EAAE;IACtD,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,kBAAkB,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAA;IAEnF,MAAM,OAAO,GAAG,MAAM,IAAA,qBAAa,EAAC,6BAAW,CAAC,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,IAAA,kBAAQ,EAAC,MAAM,CAAC,EAAE,CAAC,CAAA;IACxF,MAAM,QAAQ,GAAG,MAAM,IAAA,qBAAa,EAAC,+BAAY,CAAC,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,IAAA,kBAAQ,EAAC,MAAM,CAAC,EAAE,CAAC,CAAA;IAE1F,OAAO,EAAE,QAAQ,EAAE,QAAQ,CAAC,QAAQ,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,QAAQ,IAAI,CAAC,EAAE,CAAA;AAC7E,CAAC;AAED,MAAM,MAAM,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA;AAElC,SAAgB,mBAAmB;IACjC,OAAO,IAAA,8BAAiB,EAAC;QACvB,IAAI,EAAE,iBAAiB;QACvB,UAAU,EAAE,MAAM;QAClB,IAAI,EAAE,KAAK,IAAI,EAAE;YACf,MAAM,OAAO,GAAG,MAAM,iBAAiB,EAAE,CAAA;YACzC,IAAI,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;gBACxC,YAAM,CAAC,IAAI,CAAC,mBAAmB,OAAO,CAAC,QAAQ,iBAAiB,OAAO,CAAC,OAAO,iBAAiB,kBAAkB,EAAE,OAAO,CAAC,CAAA;YAC9H,CAAC;QACH,CAAC;QACD,SAAS,EAAE,OAAO,CAAC,EAAE,CAAC,YAAM,CAAC,KAAK,CAAC,mCAAmC,OAAO,CAAC,KAAK,EAAE,CAAC;KACvF,CAAC,CAAA;AACJ,CAAC","sourcesContent":["import { LessThan } from 'typeorm'\n\nimport { config, logger } from '@things-factory/env'\nimport { getRepository } from '@things-factory/shell'\nimport { startPeriodicTask } from '@things-factory/task-lease'\n\nimport { AuditAction } from './audit-action.js'\nimport { AuditRequest } from './audit-request.js'\n\n/**\n * Audit rows older than the retention are removed by this task and by nothing else - the\n * application has no door that changes or deletes them (ADR-0141 §4-6 · §7). One year unless\n * `audit/retentionDays` says otherwise.\n */\nexport function auditRetentionDays(): number {\n const days = Number(config.get('audit/retentionDays'))\n return Number.isFinite(days) && days > 0 ? days : 365\n}\n\n/** Removes what is older than the retention, counted from `now`. */\nexport async function purgeExpiredAudit(now = new Date()): Promise<{ requests: number; actions: number }> {\n const before = new Date(now.getTime() - auditRetentionDays() * 24 * 60 * 60 * 1000)\n\n const actions = await getRepository(AuditAction).delete({ createdAt: LessThan(before) })\n const requests = await getRepository(AuditRequest).delete({ createdAt: LessThan(before) })\n\n return { requests: requests.affected ?? 0, actions: actions.affected ?? 0 }\n}\n\nconst DAY_MS = 24 * 60 * 60 * 1000\n\nexport function startAuditRetention() {\n return startPeriodicTask({\n name: 'audit-retention',\n intervalMs: DAY_MS,\n work: async () => {\n const removed = await purgeExpiredAudit()\n if (removed.requests || removed.actions) {\n logger.info(`[audit] removed ${removed.requests} requests and ${removed.actions} actions past ${auditRetentionDays()} days`)\n }\n },\n onFailure: failure => logger.error(`[audit] retention round failed: ${failure.error}`)\n })\n}\n"]}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { EntitySubscriberInterface, InsertEvent, RemoveEvent, UpdateEvent } from 'typeorm';
|
|
2
|
+
import { DomainOwner } from '../domain-owner/domain-owner.js';
|
|
3
|
+
import { Invitation } from '../invitation/invitation.js';
|
|
4
|
+
import { User } from '../user/user.js';
|
|
5
|
+
import { WebAuthCredential } from '../web-auth-credential/web-auth-credential.js';
|
|
6
|
+
/**
|
|
7
|
+
* ── Grants are junction rows ──────────────────────────────────────────────
|
|
8
|
+
*
|
|
9
|
+
* A role given to a user is a `users_roles` row; a privilege given to a role, a `roles_privileges`
|
|
10
|
+
* row. When only those change, TypeORM updates no entity and sends no update event - it inserts and
|
|
11
|
+
* removes junction rows, and those it does announce. So grants are read here, from the junction, for
|
|
12
|
+
* every path that saves a user's roles or a role's privileges, a new user's or role's included.
|
|
13
|
+
*
|
|
14
|
+
* This listens to every entity (a junction has no class to name) and leaves at once on anything
|
|
15
|
+
* that is not one of the two junctions.
|
|
16
|
+
*/
|
|
17
|
+
export declare class GrantAuditSubscriber implements EntitySubscriberInterface<any> {
|
|
18
|
+
afterInsert(event: InsertEvent<any>): Promise<void>;
|
|
19
|
+
afterRemove(event: RemoveEvent<any>): Promise<void>;
|
|
20
|
+
private record;
|
|
21
|
+
}
|
|
22
|
+
export declare class UserAuditSubscriber implements EntitySubscriberInterface<User> {
|
|
23
|
+
listenTo(): typeof User;
|
|
24
|
+
afterUpdate(event: UpdateEvent<User>): Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
export declare class DomainOwnerAuditSubscriber implements EntitySubscriberInterface<DomainOwner> {
|
|
27
|
+
listenTo(): typeof DomainOwner;
|
|
28
|
+
afterInsert(event: InsertEvent<DomainOwner>): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
export declare class InvitationAuditSubscriber implements EntitySubscriberInterface<Invitation> {
|
|
31
|
+
listenTo(): typeof Invitation;
|
|
32
|
+
afterInsert(event: InsertEvent<Invitation>): Promise<void>;
|
|
33
|
+
afterUpdate(event: UpdateEvent<Invitation>): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
export declare class WebAuthCredentialAuditSubscriber implements EntitySubscriberInterface<WebAuthCredential> {
|
|
36
|
+
listenTo(): typeof WebAuthCredential;
|
|
37
|
+
afterInsert(event: InsertEvent<WebAuthCredential>): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
export declare const auditSubscribers: (typeof GrantAuditSubscriber | typeof UserAuditSubscriber | typeof DomainOwnerAuditSubscriber | typeof InvitationAuditSubscriber | typeof WebAuthCredentialAuditSubscriber)[];
|