@forestadmin/agent 1.95.1 → 1.96.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/agent.d.ts +4 -0
  2. package/dist/agent.js +56 -5
  3. package/dist/audit-trail/action-capture.d.ts +17 -0
  4. package/dist/audit-trail/action-capture.js +97 -0
  5. package/dist/audit-trail/index.d.ts +8 -0
  6. package/dist/audit-trail/index.js +44 -0
  7. package/dist/audit-trail/instrument.d.ts +12 -0
  8. package/dist/audit-trail/instrument.js +393 -0
  9. package/dist/audit-trail/migrations.d.ts +34 -0
  10. package/dist/audit-trail/migrations.js +213 -0
  11. package/dist/audit-trail/revert.d.ts +8 -0
  12. package/dist/audit-trail/revert.js +81 -0
  13. package/dist/audit-trail/scope.d.ts +20 -0
  14. package/dist/audit-trail/scope.js +33 -0
  15. package/dist/audit-trail/sql-store.d.ts +30 -0
  16. package/dist/audit-trail/sql-store.js +316 -0
  17. package/dist/audit-trail/types.d.ts +129 -0
  18. package/dist/audit-trail/types.js +3 -0
  19. package/dist/routes/access/audit-trail-correlation.d.ts +18 -0
  20. package/dist/routes/access/audit-trail-correlation.js +84 -0
  21. package/dist/routes/access/audit-trail.d.ts +23 -0
  22. package/dist/routes/access/audit-trail.js +293 -0
  23. package/dist/routes/capabilities.js +5 -1
  24. package/dist/routes/index.js +14 -1
  25. package/dist/routes/modification/action/action.d.ts +3 -0
  26. package/dist/routes/modification/action/action.js +106 -2
  27. package/dist/types.d.ts +39 -1
  28. package/dist/types.js +1 -1
  29. package/dist/utils/correlation-id.d.ts +5 -0
  30. package/dist/utils/correlation-id.js +26 -0
  31. package/dist/utils/options-validator.js +19 -1
  32. package/dist/utils/query-string.d.ts +3 -1
  33. package/dist/utils/query-string.js +5 -5
  34. package/package.json +5 -4
@@ -0,0 +1,316 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_TABLE = exports.DEFAULT_SCHEMA = void 0;
4
+ exports.defineAuditLogModel = defineAuditLogModel;
5
+ exports.ensureAuditStorage = ensureAuditStorage;
6
+ exports.toRow = toRow;
7
+ exports.fieldsChangedCondition = fieldsChangedCondition;
8
+ exports.searchCondition = searchCondition;
9
+ exports.fromRow = fromRow;
10
+ exports.createSqlAuditStore = createSqlAuditStore;
11
+ const sequelize_1 = require("sequelize");
12
+ const migrations_1 = require("./migrations");
13
+ exports.DEFAULT_SCHEMA = 'forest';
14
+ exports.DEFAULT_TABLE = 'audit_logs';
15
+ const DIALECTS_WITH_SCHEMAS = new Set(['postgres', 'mssql']);
16
+ const MODEL_NAME = 'ForestAuditLog';
17
+ function defineAuditLogModel(sequelize, options) {
18
+ return sequelize.define(MODEL_NAME, {
19
+ id: { type: sequelize_1.DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
20
+ // Millisecond precision: a bare DATE truncates to whole seconds on MySQL, which would
21
+ // silently drop the fractional part the `/state` boundary and the `id` tiebreaker both rely
22
+ // on for correct ordering.
23
+ timestamp: { type: sequelize_1.DataTypes.DATE(3), allowNull: false },
24
+ operation: { type: sequelize_1.DataTypes.STRING, allowNull: false },
25
+ collection: { type: sequelize_1.DataTypes.STRING, allowNull: false },
26
+ // TEXT, not STRING (VARCHAR(255)): a packed composite-id can exceed 255 characters, and the
27
+ // migration creates the column as such — this must match. Nullable: a pending create's row
28
+ // has no id yet, since the record doesn't exist until the write resolves.
29
+ recordId: { type: sequelize_1.DataTypes.TEXT, allowNull: true },
30
+ userId: { type: sequelize_1.DataTypes.INTEGER, allowNull: true },
31
+ // Denormalised from the caller at write time — who acted then, not who holds that id today.
32
+ userFirstName: { type: sequelize_1.DataTypes.TEXT, allowNull: true },
33
+ userLastName: { type: sequelize_1.DataTypes.TEXT, allowNull: true },
34
+ userEmail: { type: sequelize_1.DataTypes.TEXT, allowNull: true },
35
+ // `action` / `action_failed` rows only.
36
+ actionName: { type: sequelize_1.DataTypes.TEXT, allowNull: true },
37
+ correlationKey: { type: sequelize_1.DataTypes.STRING, allowNull: true },
38
+ previousValues: { type: sequelize_1.DataTypes.JSON, allowNull: false, defaultValue: {} },
39
+ newValues: { type: sequelize_1.DataTypes.JSON, allowNull: false, defaultValue: {} },
40
+ status: { type: sequelize_1.DataTypes.STRING, allowNull: false, defaultValue: 'done' },
41
+ }, {
42
+ schema: options.schema,
43
+ tableName: options.tableName,
44
+ timestamps: false,
45
+ underscored: true,
46
+ freezeTableName: true,
47
+ });
48
+ }
49
+ async function ensureAuditStorage(sequelize, options) {
50
+ const schema = DIALECTS_WITH_SCHEMAS.has(sequelize.getDialect()) ? options.schema : undefined;
51
+ await (0, migrations_1.runAuditMigrations)(sequelize, { schema, tableName: options.tableName });
52
+ return defineAuditLogModel(sequelize, { schema, tableName: options.tableName });
53
+ }
54
+ function toRow(record) {
55
+ return {
56
+ timestamp: record.timestamp,
57
+ operation: record.operation,
58
+ collection: record.collection,
59
+ recordId: record.recordId,
60
+ userId: record.userId,
61
+ userFirstName: record.userFirstName,
62
+ userLastName: record.userLastName,
63
+ userEmail: record.userEmail,
64
+ actionName: record.actionName,
65
+ correlationKey: record.correlationKey,
66
+ previousValues: record.previousValues,
67
+ newValues: record.newValues,
68
+ status: record.status,
69
+ };
70
+ }
71
+ // A bare `$.${field}` path treats a dot in `field` as a nested-member separator instead of a
72
+ // literal character (so `profile.name` would look up `profile` → `name` instead of the top-level
73
+ // key `"profile.name"` the audit row actually stores under). Quoting the segment makes it literal.
74
+ function jsonPath(field) {
75
+ return `$."${field.replace(/"/g, '\\"')}"`;
76
+ }
77
+ function fieldsChangedCondition(sequelize, fields) {
78
+ const dialect = sequelize.getDialect();
79
+ if (dialect === 'postgres') {
80
+ const keys = fields.map(field => sequelize.escape(field)).join(',');
81
+ return sequelize_1.Sequelize.literal(`(jsonb_exists_any("previous_values"::jsonb, ARRAY[${keys}]) ` +
82
+ `OR jsonb_exists_any("new_values"::jsonb, ARRAY[${keys}]))`);
83
+ }
84
+ if (dialect === 'sqlite') {
85
+ const tests = fields.flatMap(field => {
86
+ const path = sequelize.escape(jsonPath(field));
87
+ return [
88
+ `json_type(previous_values, ${path}) IS NOT NULL`,
89
+ `json_type(new_values, ${path}) IS NOT NULL`,
90
+ ];
91
+ });
92
+ return sequelize_1.Sequelize.literal(`(${tests.join(' OR ')})`);
93
+ }
94
+ if (dialect === 'mysql' || dialect === 'mariadb') {
95
+ const paths = fields.map(field => sequelize.escape(jsonPath(field))).join(',');
96
+ return sequelize_1.Sequelize.literal(`(JSON_CONTAINS_PATH(previous_values, 'one', ${paths}) ` +
97
+ `OR JSON_CONTAINS_PATH(new_values, 'one', ${paths}))`);
98
+ }
99
+ if (dialect === 'mssql') {
100
+ const tests = fields.flatMap(field => {
101
+ const path = sequelize.escape(jsonPath(field));
102
+ return [
103
+ `JSON_PATH_EXISTS(previous_values, ${path}) = 1`,
104
+ `JSON_PATH_EXISTS(new_values, ${path}) = 1`,
105
+ ];
106
+ });
107
+ // JSON_PATH_EXISTS requires SQL Server 2022+.
108
+ return sequelize_1.Sequelize.literal(`(${tests.join(' OR ')})`);
109
+ }
110
+ throw new Error(`Audit-trail "fields" filter is not supported on dialect "${dialect}"`);
111
+ }
112
+ // JSON columns are already stored as plain text on dialects with no native JSON type (sqlite,
113
+ // mssql); only postgres/mysql/mariadb need an explicit cast before a text match can run on them.
114
+ function jsonColumnAsText(sequelize, column) {
115
+ const dialect = sequelize.getDialect();
116
+ if (dialect === 'postgres')
117
+ return `${column}::text`;
118
+ if (dialect === 'mysql' || dialect === 'mariadb')
119
+ return `CAST(${column} AS CHAR)`;
120
+ return column;
121
+ }
122
+ // A free-text search against the *serialized* JSON text naturally covers "keys and scalar values
123
+ // at any depth" without walking the structure by hand: both a key and a value appear as a quoted
124
+ // literal substring of that text. This runs as a SQL WHERE clause (not an in-memory scan of
125
+ // fetched rows), so it composes with pagination and COUNT the same way every other filter does. A
126
+ // redacted value can't match a search for the real value — the real value already isn't in this
127
+ // text, `redactValues` replaced it before the row was ever written.
128
+ function searchCondition(sequelize, term) {
129
+ // `~` rather than the standard `\`: MySQL/MariaDB treat backslash as a string-literal escape
130
+ // character under the default sql_mode (no NO_BACKSLASH_ESCAPES), so a bare `ESCAPE '\'` is
131
+ // itself malformed there — the backslash escapes the closing quote instead of terminating the
132
+ // literal. `~` has no special meaning to any of the four supported dialects' string literals, so
133
+ // it sidesteps that interaction entirely. Escapes the two LIKE wildcards plus itself, so a term
134
+ // containing `%`/`_`/`~` is matched literally instead of behaving like a pattern.
135
+ const escaped = term.replace(/[~%_]/g, char => `~${char}`);
136
+ const pattern = sequelize.escape(`%${escaped}%`);
137
+ const columns = [
138
+ 'action_name',
139
+ 'user_first_name',
140
+ 'user_last_name',
141
+ 'user_email',
142
+ jsonColumnAsText(sequelize, 'previous_values'),
143
+ jsonColumnAsText(sequelize, 'new_values'),
144
+ ];
145
+ return sequelize_1.Sequelize.literal(`(${columns.map(column => `LOWER(${column}) LIKE LOWER(${pattern}) ESCAPE '~'`).join(' OR ')})`);
146
+ }
147
+ function buildHistoryWhereClause({ collection, recordId, userIds, startTimestamp, endTimestamp, fields, search, }, sequelize) {
148
+ const where = { collection, recordId };
149
+ if (userIds)
150
+ where.userId = { [sequelize_1.Op.in]: userIds };
151
+ const timestampRange = {};
152
+ if (startTimestamp)
153
+ timestampRange[sequelize_1.Op.gte] = new Date(startTimestamp);
154
+ if (endTimestamp)
155
+ timestampRange[sequelize_1.Op.lte] = new Date(endTimestamp);
156
+ if (Object.getOwnPropertySymbols(timestampRange).length)
157
+ where.timestamp = timestampRange;
158
+ const andConditions = [];
159
+ if (fields?.length)
160
+ andConditions.push(fieldsChangedCondition(sequelize, fields));
161
+ if (search)
162
+ andConditions.push(searchCondition(sequelize, search));
163
+ if (andConditions.length)
164
+ where[sequelize_1.Op.and] = andConditions;
165
+ return where;
166
+ }
167
+ function fromRow(row) {
168
+ const plain = row.get({ plain: true });
169
+ const { timestamp } = plain;
170
+ return {
171
+ id: Number(plain.id),
172
+ timestamp: timestamp instanceof Date ? timestamp.toISOString() : String(timestamp),
173
+ operation: plain.operation,
174
+ collection: plain.collection,
175
+ recordId: plain.recordId ?? null,
176
+ userId: plain.userId,
177
+ userFirstName: plain.userFirstName ?? null,
178
+ userLastName: plain.userLastName ?? null,
179
+ userEmail: plain.userEmail ?? null,
180
+ actionName: plain.actionName ?? null,
181
+ correlationKey: plain.correlationKey,
182
+ previousValues: plain.previousValues ?? {},
183
+ newValues: plain.newValues ?? {},
184
+ status: plain.status,
185
+ };
186
+ }
187
+ /**
188
+ * Returns a SQL-backed audit store. Construction is synchronous so the store can be passed to
189
+ * `createAgent` at module top level; the connection (and pending migrations) only run when
190
+ * `store.init()` is awaited during `agent.start()`. The store also self-initializes on the first
191
+ * append or read so tests and manual scripts work without an explicit init.
192
+ */
193
+ function createSqlAuditStore(options) {
194
+ const { connectionString, schema = exports.DEFAULT_SCHEMA, tableName = exports.DEFAULT_TABLE } = options;
195
+ let sequelize = null;
196
+ let bootstrap = null;
197
+ const init = () => {
198
+ if (bootstrap)
199
+ return bootstrap;
200
+ const connection = new sequelize_1.Sequelize(connectionString, { logging: false });
201
+ sequelize = connection;
202
+ bootstrap = (async () => {
203
+ try {
204
+ await connection.authenticate();
205
+ const model = await ensureAuditStorage(connection, { schema, tableName });
206
+ return { model, connection };
207
+ }
208
+ catch (error) {
209
+ // Reset so a later call can retry once the database is reachable.
210
+ await connection.close().catch(() => undefined);
211
+ sequelize = null;
212
+ bootstrap = null;
213
+ throw error;
214
+ }
215
+ })();
216
+ return bootstrap;
217
+ };
218
+ // Insertion-order tiebreaker on `id` keeps paging deterministic when timestamps collide.
219
+ const chronologicalOrder = [
220
+ ['timestamp', 'ASC'],
221
+ ['id', 'ASC'],
222
+ ];
223
+ return {
224
+ store: {
225
+ async init() {
226
+ await init();
227
+ },
228
+ async insertPending(record) {
229
+ const { model } = await init();
230
+ const row = await model.create(toRow({ ...record, status: 'pending' }));
231
+ return Number(row.get('id'));
232
+ },
233
+ // Per-row generated ids from one statement are trustworthy on every supported dialect:
234
+ // Postgres/MSSQL return them natively (RETURNING/OUTPUT), Sequelize infers them sequentially
235
+ // from the single last-insert-id on MySQL/MariaDB (safe because a multi-row INSERT holds the
236
+ // table's auto-increment lock for the whole statement under the default lock mode — no other
237
+ // connection's insert can land an id in the middle of this batch), and sqlite returns them
238
+ // directly too (verified empirically).
239
+ async insertPendingBatch(records) {
240
+ const { model } = await init();
241
+ const rows = await model.bulkCreate(records.map(record => toRow({ ...record, status: 'pending' })));
242
+ return rows.map(row => Number(row.get('id')));
243
+ },
244
+ async confirm(id, patch) {
245
+ const { model } = await init();
246
+ await model.update({ ...patch, status: 'done' }, { where: { id } });
247
+ },
248
+ async listByRecord(query) {
249
+ const { model, connection } = await init();
250
+ const { skip = 0, limit, order = 'asc' } = query;
251
+ const direction = order === 'desc' ? 'DESC' : 'ASC';
252
+ const rows = await model.findAll({
253
+ where: buildHistoryWhereClause(query, connection),
254
+ order: [
255
+ ['timestamp', direction],
256
+ ['id', direction],
257
+ ],
258
+ offset: skip,
259
+ limit,
260
+ });
261
+ return rows.map(fromRow);
262
+ },
263
+ async countByRecord(query) {
264
+ const { model, connection } = await init();
265
+ return model.count({ where: buildHistoryWhereClause(query, connection) });
266
+ },
267
+ async listDistinctUsers(query) {
268
+ const { model, connection } = await init();
269
+ // MAX() rather than a bare column: grouping by `user_id` alone is invalid in strict SQL
270
+ // unless every selected column is either grouped or aggregated.
271
+ const rows = (await model.findAll({
272
+ where: buildHistoryWhereClause(query, connection),
273
+ attributes: [
274
+ 'userId',
275
+ [sequelize_1.Sequelize.fn('MAX', sequelize_1.Sequelize.col('user_first_name')), 'userFirstName'],
276
+ [sequelize_1.Sequelize.fn('MAX', sequelize_1.Sequelize.col('user_last_name')), 'userLastName'],
277
+ [sequelize_1.Sequelize.fn('MAX', sequelize_1.Sequelize.col('user_email')), 'userEmail'],
278
+ ],
279
+ group: ['userId'],
280
+ raw: true,
281
+ }));
282
+ return rows.map(row => ({
283
+ id: row.userId,
284
+ firstName: row.userFirstName ?? null,
285
+ lastName: row.userLastName ?? null,
286
+ email: row.userEmail ?? null,
287
+ }));
288
+ },
289
+ async listByCorrelation({ collection, recordId, correlationKey }) {
290
+ const { model } = await init();
291
+ const rows = await model.findAll({
292
+ where: { collection, recordId, correlationKey },
293
+ order: chronologicalOrder,
294
+ });
295
+ return rows.map(fromRow);
296
+ },
297
+ async listByCorrelations({ collection, recordId, correlationKeys }) {
298
+ if (!correlationKeys.length)
299
+ return [];
300
+ const { model } = await init();
301
+ const rows = await model.findAll({
302
+ where: { collection, recordId, correlationKey: { [sequelize_1.Op.in]: correlationKeys } },
303
+ order: chronologicalOrder,
304
+ });
305
+ return rows.map(fromRow);
306
+ },
307
+ },
308
+ close: async () => {
309
+ if (sequelize)
310
+ await sequelize.close();
311
+ sequelize = null;
312
+ bootstrap = null;
313
+ },
314
+ };
315
+ }
316
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3FsLXN0b3JlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2F1ZGl0LXRyYWlsL3NxbC1zdG9yZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFxQkEsa0RBc0NDO0FBRUQsZ0RBU0M7QUFFRCxzQkFrQkM7QUFTRCx3REFpREM7QUFtQkQsMENBc0JDO0FBK0JELDBCQW9CQztBQVFELGtEQW1KQztBQWpZRCx5Q0FBcUQ7QUFFckQsNkNBQWtEO0FBRXJDLFFBQUEsY0FBYyxHQUFHLFFBQVEsQ0FBQztBQUMxQixRQUFBLGFBQWEsR0FBRyxZQUFZLENBQUM7QUFFMUMsTUFBTSxxQkFBcUIsR0FBRyxJQUFJLEdBQUcsQ0FBQyxDQUFDLFVBQVUsRUFBRSxPQUFPLENBQUMsQ0FBQyxDQUFDO0FBRTdELE1BQU0sVUFBVSxHQUFHLGdCQUFnQixDQUFDO0FBRXBDLFNBQWdCLG1CQUFtQixDQUNqQyxTQUFvQixFQUNwQixPQUErQztJQUUvQyxPQUFPLFNBQVMsQ0FBQyxNQUFNLENBQ3JCLFVBQVUsRUFDVjtRQUNFLEVBQUUsRUFBRSxFQUFFLElBQUksRUFBRSxxQkFBUyxDQUFDLE1BQU0sRUFBRSxVQUFVLEVBQUUsSUFBSSxFQUFFLGFBQWEsRUFBRSxJQUFJLEVBQUU7UUFDckUsc0ZBQXNGO1FBQ3RGLDRGQUE0RjtRQUM1RiwyQkFBMkI7UUFDM0IsU0FBUyxFQUFFLEVBQUUsSUFBSSxFQUFFLHFCQUFTLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxFQUFFLFNBQVMsRUFBRSxLQUFLLEVBQUU7UUFDeEQsU0FBUyxFQUFFLEVBQUUsSUFBSSxFQUFFLHFCQUFTLENBQUMsTUFBTSxFQUFFLFNBQVMsRUFBRSxLQUFLLEVBQUU7UUFDdkQsVUFBVSxFQUFFLEVBQUUsSUFBSSxFQUFFLHFCQUFTLENBQUMsTUFBTSxFQUFFLFNBQVMsRUFBRSxLQUFLLEVBQUU7UUFDeEQsNEZBQTRGO1FBQzVGLDJGQUEyRjtRQUMzRiwwRUFBMEU7UUFDMUUsUUFB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
@@ -0,0 +1,129 @@
1
+ import type { Logger } from '@forestadmin/datasource-toolkit';
2
+ export type AuditOperation = 'create' | 'update' | 'delete' | 'action' | 'action_failed';
3
+ export type AuditStatus = 'pending' | 'done';
4
+ export type AuditRecord = {
5
+ id: number;
6
+ timestamp: string;
7
+ operation: AuditOperation;
8
+ collection: string;
9
+ /** Null for a pending create — the record's primary key isn't assigned yet. */
10
+ recordId: string | null;
11
+ userId: number;
12
+ /** Denormalised from the caller at write time: who acted then, not who holds that id today. */
13
+ userFirstName: string | null;
14
+ userLastName: string | null;
15
+ userEmail: string | null;
16
+ /** `action` / `action_failed` rows only. */
17
+ actionName: string | null;
18
+ correlationKey: string;
19
+ previousValues: Record<string, unknown>;
20
+ newValues: Record<string, unknown>;
21
+ /**
22
+ * `pending` from the moment the row is inserted (before the write runs) until it's confirmed
23
+ * `done` afterward. A row left `pending` means the write may or may not have landed — that
24
+ * residue is evidence a write was attempted and never confirmed, which is the point.
25
+ */
26
+ status: AuditStatus;
27
+ };
28
+ /** The subset known before the write runs, when the pending row is first inserted. */
29
+ export type PendingAuditRecord = Omit<AuditRecord, 'id' | 'status'>;
30
+ /** What `confirm` updates once the write (or action) has resolved. */
31
+ export type AuditRecordConfirmation = Pick<AuditRecord, 'operation' | 'recordId' | 'previousValues' | 'newValues'>;
32
+ export type AuditHistoryQuery = {
33
+ collection: string;
34
+ recordId: string;
35
+ skip?: number;
36
+ limit?: number;
37
+ userIds?: number[];
38
+ /** Inclusive lower bound on `timestamp` as a UTC ISO instant. */
39
+ startTimestamp?: string;
40
+ /** Inclusive upper bound on `timestamp` as a UTC ISO instant. */
41
+ endTimestamp?: string;
42
+ /** Keep only entries whose change touched at least one of these fields. */
43
+ fields?: string[];
44
+ /**
45
+ * Free-text, case-insensitive substring match against `actionName`, `userFirstName`,
46
+ * `userLastName`, `userEmail`, and the keys and scalar values of `previousValues`/`newValues` at
47
+ * any depth. Never matches `operation`, `correlationKey`, `recordId`, `collection`, `status` or
48
+ * `timestamp` — those are machine identifiers a user would never search, and matching them
49
+ * produces confusing hits. A redacted value can't match a search for the real value: the real
50
+ * value was never stored in the first place.
51
+ *
52
+ * The match runs against the *serialized JSON text* of `previousValues`/`newValues`, not a
53
+ * structural walk of the parsed value — cheap, and correct for "keys and scalar values", but two
54
+ * things fall out of that: a punctuation-only term (`,`, `:`, `{`) matches almost any row whose
55
+ * diff has more than one key, since those characters are JSON structure rather than content; and
56
+ * a value containing a double quote or a backslash can't be found by searching for it literally
57
+ * (`5"` is stored as `5\"`, so searching `5"` never matches the row that holds it). Neither is
58
+ * severe, and both are inherent to matching the serialized form rather than the parsed value.
59
+ */
60
+ search?: string;
61
+ /** Sort direction on `timestamp` (ties broken by insertion order). Defaults to `'asc'`. */
62
+ order?: 'asc' | 'desc';
63
+ };
64
+ export type AuditCorrelationQuery = {
65
+ collection: string;
66
+ recordId: string;
67
+ correlationKey: string;
68
+ };
69
+ export type AuditCorrelationsQuery = {
70
+ collection: string;
71
+ recordId: string;
72
+ correlationKeys: string[];
73
+ };
74
+ export type AuditUserSummary = {
75
+ id: number;
76
+ firstName: string | null;
77
+ lastName: string | null;
78
+ email: string | null;
79
+ };
80
+ export interface AuditStore {
81
+ /** Inserts one `status: 'pending'` row before the write runs; returns its id. */
82
+ insertPending(record: PendingAuditRecord): Promise<number>;
83
+ /** Batched form of `insertPending`, for a bulk operation touching several records at once. */
84
+ insertPendingBatch(records: PendingAuditRecord[]): Promise<number[]>;
85
+ /** Updates a pending row to `status: 'done'` with the values known once the write resolved. */
86
+ confirm(id: number, patch: AuditRecordConfirmation): Promise<void>;
87
+ listByRecord(query: AuditHistoryQuery): AuditRecord[] | Promise<AuditRecord[]>;
88
+ /** Total entries matching the query filters, ignoring `skip` / `limit`. */
89
+ countByRecord(query: AuditHistoryQuery): number | Promise<number>;
90
+ /** Entries recorded under one `correlationKey` for a record, oldest first. */
91
+ listByCorrelation(query: AuditCorrelationQuery): AuditRecord[] | Promise<AuditRecord[]>;
92
+ /** Flat list of entries recorded under any of `correlationKeys` for a record, oldest first. */
93
+ listByCorrelations(query: AuditCorrelationsQuery): AuditRecord[] | Promise<AuditRecord[]>;
94
+ /** Distinct authors matching the query filters, independent of pagination. */
95
+ listDistinctUsers(query: Omit<AuditHistoryQuery, 'skip' | 'limit' | 'order'>): AuditUserSummary[] | Promise<AuditUserSummary[]>;
96
+ /** One-shot bootstrap awaited by `agent.start()`. Must be idempotent. */
97
+ init?(): Promise<void>;
98
+ }
99
+ export type AuditSink = (record: PendingAuditRecord) => void | Promise<void>;
100
+ export type AuditTrailInstrumentOptions = {
101
+ sink?: AuditSink;
102
+ store?: AuditStore;
103
+ /**
104
+ * Fields to mask, keyed by collection name. Redacted fields still produce an audit entry when
105
+ * they change, but their value is replaced with a sentinel instead of being stored.
106
+ */
107
+ redact?: Record<string, string[]>;
108
+ /**
109
+ * When true, a failure inserting a write's pending audit row refuses the write itself — no
110
+ * unaudited write ever happens, though a row left `pending` doesn't guarantee its values are
111
+ * exact. When false (default), the same failure is logged and the write proceeds unaudited,
112
+ * matching today's behavior.
113
+ */
114
+ critical?: boolean;
115
+ logger?: Logger;
116
+ };
117
+ export type AuditStorageOptions = {
118
+ /**
119
+ * Postgres / SQL connection string for the audit database. May point at an empty database, the
120
+ * database already used by the agent, or one that already contains the `forest` schema — the
121
+ * schema and table are created on the fly when missing.
122
+ */
123
+ connectionString: string;
124
+ /** Defaults to `forest`. */
125
+ schema?: string;
126
+ /** Defaults to `audit_logs`. */
127
+ tableName?: string;
128
+ };
129
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHlwZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvYXVkaXQtdHJhaWwvdHlwZXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiJ9
@@ -0,0 +1,18 @@
1
+ import type { ForestAdminHttpDriverServices } from '../../services';
2
+ import type { AgentOptionsWithDefaults } from '../../types';
3
+ import type { DataSource } from '@forestadmin/datasource-toolkit';
4
+ import type Router from '@koa/router';
5
+ import type { Context } from 'koa';
6
+ import { RouteType } from '../../types';
7
+ import BaseRoute from '../base-route';
8
+ export default class AuditTrailCorrelationRoute extends BaseRoute {
9
+ readonly type = RouteType.PrivateRoute;
10
+ private readonly dataSource;
11
+ constructor(services: ForestAdminHttpDriverServices, options: AgentOptionsWithDefaults, dataSource: DataSource);
12
+ setupRoutes(router: Router): void;
13
+ handleHistory(context: Context): Promise<void>;
14
+ handleBatch(context: Context): Promise<void>;
15
+ private assertRecordReadable;
16
+ private static parseCorrelationKeys;
17
+ }
18
+ //# sourceMappingURL=audit-trail-correlation.d.ts.map
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const datasource_toolkit_1 = require("@forestadmin/datasource-toolkit");
7
+ const scope_1 = __importDefault(require("../../audit-trail/scope"));
8
+ const types_1 = require("../../types");
9
+ const base_route_1 = __importDefault(require("../base-route"));
10
+ class AuditTrailCorrelationRoute extends base_route_1.default {
11
+ constructor(services, options, dataSource) {
12
+ super(services, options);
13
+ this.type = types_1.RouteType.PrivateRoute;
14
+ this.dataSource = dataSource;
15
+ }
16
+ setupRoutes(router) {
17
+ router.get('/_audit-trail/correlation/:correlationKey', this.handleHistory.bind(this));
18
+ // POST accepts the keys in a JSON body to dodge any URL length limit.
19
+ router.get('/_audit-trail/correlations', this.handleBatch.bind(this));
20
+ router.post('/_audit-trail/correlations', this.handleBatch.bind(this));
21
+ }
22
+ async handleHistory(context) {
23
+ const target = await this.assertRecordReadable(context);
24
+ if (!target)
25
+ return;
26
+ const { collection, recordId } = target;
27
+ const { store } = this.options.auditTrail;
28
+ const history = await store.listByCorrelation({
29
+ collection,
30
+ recordId,
31
+ correlationKey: context.params.correlationKey,
32
+ });
33
+ context.response.body = { data: history };
34
+ }
35
+ async handleBatch(context) {
36
+ const target = await this.assertRecordReadable(context);
37
+ if (!target)
38
+ return;
39
+ const { collection, recordId } = target;
40
+ const correlationKeys = AuditTrailCorrelationRoute.parseCorrelationKeys(context);
41
+ const { store } = this.options.auditTrail;
42
+ const history = correlationKeys.length
43
+ ? await store.listByCorrelations({ collection, recordId, correlationKeys })
44
+ : [];
45
+ context.response.body = { data: history };
46
+ }
47
+ // Returns null (after issuing the 404) when a configured record-level scope excludes the id —
48
+ // same rule as the per-collection route: a scope can't be evaluated retroactively for a
49
+ // now-deleted record, so a scoped caller cannot look up correlations for an out-of-scope id.
50
+ async assertRecordReadable(context) {
51
+ const query = context.request.query;
52
+ const body = (context.request.body ?? {});
53
+ const collectionName = (query.collection ?? body.collection)?.toString();
54
+ const recordId = (query.recordId ?? body.recordId)?.toString();
55
+ if (!collectionName)
56
+ throw new datasource_toolkit_1.ValidationError('Missing collection');
57
+ if (!recordId)
58
+ throw new datasource_toolkit_1.ValidationError('Missing recordId');
59
+ const collection = this.dataSource.getCollection(collectionName);
60
+ await this.services.authorization.assertCanRead(context, collectionName);
61
+ const { visible } = await (0, scope_1.default)(this.services, collection, recordId, context);
62
+ if (!visible) {
63
+ context.throw(types_1.HttpCode.NotFound, 'Record does not exists');
64
+ return null;
65
+ }
66
+ return { collection: collectionName, recordId };
67
+ }
68
+ // Body array (POST) takes precedence over the comma-separated query param (GET).
69
+ static parseCorrelationKeys(context) {
70
+ const body = (context.request.body ?? {});
71
+ if (Array.isArray(body.correlationKeys)) {
72
+ return body.correlationKeys.map(key => String(key).trim()).filter(Boolean);
73
+ }
74
+ const raw = context.request.query.correlationKeys?.toString();
75
+ return raw
76
+ ? raw
77
+ .split(',')
78
+ .map(key => key.trim())
79
+ .filter(Boolean)
80
+ : [];
81
+ }
82
+ }
83
+ exports.default = AuditTrailCorrelationRoute;
84
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXVkaXQtdHJhaWwtY29ycmVsYXRpb24uanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvcm91dGVzL2FjY2Vzcy9hdWRpdC10cmFpbC1jb3JyZWxhdGlvbi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7OztBQU1BLHdFQUFrRTtBQUVsRSxvRUFBNEQ7QUFDNUQsdUNBQWtEO0FBQ2xELCtEQUFzQztBQUV0QyxNQUFxQiwwQkFBMkIsU0FBUSxvQkFBUztJQUsvRCxZQUNFLFFBQXVDLEVBQ3ZDLE9BQWlDLEVBQ2pDLFVBQXNCO1FBRXRCLEtBQUssQ0FBQyxRQUFRLEVBQUUsT0FBTyxDQUFDLENBQUM7UUFUbEIsU0FBSSxHQUFHLGlCQUFTLENBQUMsWUFBWSxDQUFDO1FBVXJDLElBQUksQ0FBQyxVQUFVLEdBQUcsVUFBVSxDQUFDO0lBQy9CLENBQUM7SUFFRCxXQUFXLENBQUMsTUFBYztRQUN4QixNQUFNLENBQUMsR0FBRyxDQUFDLDJDQUEyQyxFQUFFLElBQUksQ0FBQyxhQUFhLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUM7UUFDdkYsc0VBQXNFO1FBQ3RFLE1BQU0sQ0FBQyxHQUFHLENBQUMsNEJBQTRCLEVBQUUsSUFBSSxDQUFDLFdBQVcsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQztRQUN0RSxNQUFNLENBQUMsSUFBSSxDQUFDLDRCQUE0QixFQUFFLElBQUksQ0FBQyxXQUFXLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUM7SUFDekUsQ0FBQztJQUVNLEtBQUssQ0FBQyxhQUFhLENBQUMsT0FBZ0I7UUFDekMsTUFBTSxNQUFNLEdBQUcsTUFBTSxJQUFJLENBQUMsb0JBQW9CLENBQUMsT0FBTyxDQUFDLENBQUM7UUFDeEQsSUFBSSxDQUFDLE1BQU07WUFBRSxPQUFPO1FBRXBCLE1BQU0sRUFBRSxVQUFVLEVBQUUsUUFBUSxFQUFFLEdBQUcsTUFBTSxDQUFDO1FBQ3hDLE1B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
@@ -0,0 +1,23 @@
1
+ import type Router from '@koa/router';
2
+ import type { Context } from 'koa';
3
+ import CollectionRoute from '../collection-route';
4
+ export default class AuditTrailRoute extends CollectionRoute {
5
+ setupRoutes(router: Router): void;
6
+ handleHistory(context: Context): Promise<void>;
7
+ private withholdOutOfScopeValues;
8
+ handleStateAt(context: Context): Promise<void>;
9
+ private fetchCurrentRecord;
10
+ private static auditedColumns;
11
+ private static parseAt;
12
+ private static parseSort;
13
+ private static parsePagination;
14
+ private static parsePageSize;
15
+ private static parsePageNumber;
16
+ private static parseFilters;
17
+ private static parseUserIds;
18
+ private static parseFields;
19
+ private static parseSearch;
20
+ private static parseDateBoundary;
21
+ private static toLocalInstant;
22
+ }
23
+ //# sourceMappingURL=audit-trail.d.ts.map