@cqrs-ddd/pipeline-audit 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COMMERCIAL_LICENSE.txt +34 -0
- package/LICENSE +661 -0
- package/README.md +35 -0
- package/dist/audit.behavior.d.ts +102 -0
- package/dist/audit.behavior.d.ts.map +1 -0
- package/dist/audit.behavior.js +262 -0
- package/dist/audit.behavior.js.map +1 -0
- package/dist/constants/tokens.d.ts +27 -0
- package/dist/constants/tokens.d.ts.map +1 -0
- package/dist/constants/tokens.js +28 -0
- package/dist/constants/tokens.js.map +1 -0
- package/dist/helpers/audit.intent.d.ts +15 -0
- package/dist/helpers/audit.intent.d.ts.map +1 -0
- package/dist/helpers/audit.intent.js +15 -0
- package/dist/helpers/audit.intent.js.map +1 -0
- package/dist/helpers/build-record.d.ts +42 -0
- package/dist/helpers/build-record.d.ts.map +1 -0
- package/dist/helpers/build-record.js +85 -0
- package/dist/helpers/build-record.js.map +1 -0
- package/dist/helpers/json.d.ts +7 -0
- package/dist/helpers/json.d.ts.map +1 -0
- package/dist/helpers/json.js +102 -0
- package/dist/helpers/json.js.map +1 -0
- package/dist/helpers/redact.d.ts +2 -0
- package/dist/helpers/redact.d.ts.map +1 -0
- package/dist/helpers/redact.js +3 -0
- package/dist/helpers/redact.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/audit-options.interface.d.ts +95 -0
- package/dist/interfaces/audit-options.interface.d.ts.map +1 -0
- package/dist/interfaces/audit-options.interface.js +3 -0
- package/dist/interfaces/audit-options.interface.js.map +1 -0
- package/dist/interfaces/audit-record.interface.d.ts +79 -0
- package/dist/interfaces/audit-record.interface.d.ts.map +1 -0
- package/dist/interfaces/audit-record.interface.js +3 -0
- package/dist/interfaces/audit-record.interface.js.map +1 -0
- package/dist/interfaces/audit-sink.interface.d.ts +37 -0
- package/dist/interfaces/audit-sink.interface.d.ts.map +1 -0
- package/dist/interfaces/audit-sink.interface.js +3 -0
- package/dist/interfaces/audit-sink.interface.js.map +1 -0
- package/dist/sinks/log.sink.d.ts +33 -0
- package/dist/sinks/log.sink.d.ts.map +1 -0
- package/dist/sinks/log.sink.js +31 -0
- package/dist/sinks/log.sink.js.map +1 -0
- package/dist/sinks/postgres.sink.d.ts +69 -0
- package/dist/sinks/postgres.sink.d.ts.map +1 -0
- package/dist/sinks/postgres.sink.js +132 -0
- package/dist/sinks/postgres.sink.js.map +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
2
|
+
import { toPostgresJson } from '@cqrs-ddd/pipeline';
|
|
3
|
+
import { stringifyAuditValue } from '../helpers/json.js';
|
|
4
|
+
/** Allows `table` or `schema.table` made of unquoted SQL identifiers only. */
|
|
5
|
+
const SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$/;
|
|
6
|
+
function assertSafeTable(table) {
|
|
7
|
+
if (!SAFE_IDENTIFIER.test(table)) {
|
|
8
|
+
throw new Error(`Invalid audit table name "${table}". ` +
|
|
9
|
+
'Use an unquoted identifier like "audit_log" or "schema.audit_log".');
|
|
10
|
+
}
|
|
11
|
+
return table;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* SQL to create the audit table. Run once in a migration.
|
|
15
|
+
*
|
|
16
|
+
* A row whose `outcome` is `'pending'` is an operation that started and has no
|
|
17
|
+
* final record: `duration_ms` and `completed_at` stay null. An old pending row
|
|
18
|
+
* marks an attempt interrupted by a process stop, whose outcome is unknown:
|
|
19
|
+
*
|
|
20
|
+
* ```sql
|
|
21
|
+
* SELECT * FROM audit_log
|
|
22
|
+
* WHERE outcome = 'pending' AND occurred_at < now() - interval '1 hour';
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @param table - Table name (validated). Default `'audit_log'`.
|
|
26
|
+
*/
|
|
27
|
+
export function createAuditTableSql(table = 'audit_log') {
|
|
28
|
+
const name = assertSafeTable(table);
|
|
29
|
+
return `CREATE TABLE IF NOT EXISTS ${name} (
|
|
30
|
+
id UUID PRIMARY KEY,
|
|
31
|
+
correlation_id TEXT NOT NULL,
|
|
32
|
+
action TEXT NOT NULL,
|
|
33
|
+
severity TEXT NOT NULL,
|
|
34
|
+
outcome TEXT NOT NULL,
|
|
35
|
+
actor JSONB,
|
|
36
|
+
request_kind TEXT NOT NULL,
|
|
37
|
+
request_name TEXT NOT NULL,
|
|
38
|
+
handler_name TEXT NOT NULL,
|
|
39
|
+
payload JSONB,
|
|
40
|
+
response JSONB,
|
|
41
|
+
error JSONB,
|
|
42
|
+
duration_ms DOUBLE PRECISION,
|
|
43
|
+
metadata JSONB,
|
|
44
|
+
occurred_at TIMESTAMPTZ NOT NULL,
|
|
45
|
+
completed_at TIMESTAMPTZ
|
|
46
|
+
);`;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* {@link AuditSink} backed by **Postgres** (`pg`) — a drop-in replacement for
|
|
50
|
+
* the console sink. {@link begin} inserts a pending row when an operation
|
|
51
|
+
* starts, and {@link write} completes it under the same id (or inserts it, when
|
|
52
|
+
* no pending row exists), so an operation interrupted by a process stop stays
|
|
53
|
+
* visible as `'pending'`.
|
|
54
|
+
*
|
|
55
|
+
* Create the table once with {@link createAuditTableSql}. The table name is
|
|
56
|
+
* validated as a plain SQL identifier (it is interpolated, not parameterized);
|
|
57
|
+
* all record values are passed as bound parameters. A NUL character or a lone
|
|
58
|
+
* surrogate, which `jsonb` rejects, is stored as U+FFFD (see
|
|
59
|
+
* `toPostgresJson`), so one such character cannot lose the whole record.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* import { Pool } from 'pg';
|
|
64
|
+
* const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
65
|
+
* await pool.query(createAuditTableSql());
|
|
66
|
+
* const sink = new PostgresAuditSink(pool);
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export class PostgresAuditSink {
|
|
70
|
+
db;
|
|
71
|
+
beginSql;
|
|
72
|
+
writeSql;
|
|
73
|
+
constructor(db, options = {}) {
|
|
74
|
+
this.db = db;
|
|
75
|
+
const table = assertSafeTable(options.table ?? 'audit_log');
|
|
76
|
+
const columns = `(id, correlation_id, action, severity, outcome, actor, request_kind,
|
|
77
|
+
request_name, handler_name, payload, response, error, duration_ms,
|
|
78
|
+
metadata, occurred_at, completed_at)`;
|
|
79
|
+
this.beginSql = `INSERT INTO ${table} ${columns}
|
|
80
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, NULL)`;
|
|
81
|
+
this.writeSql = `INSERT INTO ${table} ${columns}
|
|
82
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, now())
|
|
83
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
84
|
+
outcome = EXCLUDED.outcome,
|
|
85
|
+
actor = EXCLUDED.actor,
|
|
86
|
+
payload = EXCLUDED.payload,
|
|
87
|
+
response = EXCLUDED.response,
|
|
88
|
+
error = EXCLUDED.error,
|
|
89
|
+
duration_ms = EXCLUDED.duration_ms,
|
|
90
|
+
metadata = EXCLUDED.metadata,
|
|
91
|
+
completed_at = EXCLUDED.completed_at`;
|
|
92
|
+
}
|
|
93
|
+
/** Inserts the pending row of an operation about to run. */
|
|
94
|
+
async begin(record) {
|
|
95
|
+
await this.db.query(this.beginSql, this.values(record));
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Inserts the final row, or completes the pending row that {@link begin}
|
|
99
|
+
* inserted under the same id.
|
|
100
|
+
*/
|
|
101
|
+
async write(record) {
|
|
102
|
+
await this.db.query(this.writeSql, this.values(record));
|
|
103
|
+
}
|
|
104
|
+
values(record) {
|
|
105
|
+
const json = (value) => toPostgresJson(stringifyAuditValue(value));
|
|
106
|
+
const metadata = record.metadata || record.tenantId
|
|
107
|
+
? {
|
|
108
|
+
...(record.metadata ?? {}),
|
|
109
|
+
...(record.tenantId ? { tenantId: record.tenantId } : {}),
|
|
110
|
+
}
|
|
111
|
+
: null;
|
|
112
|
+
const final = record.outcome === 'pending' ? undefined : record;
|
|
113
|
+
return [
|
|
114
|
+
record.id,
|
|
115
|
+
record.correlationId,
|
|
116
|
+
record.action,
|
|
117
|
+
record.severity,
|
|
118
|
+
record.outcome,
|
|
119
|
+
record.actor ? json(record.actor) : null,
|
|
120
|
+
record.requestKind,
|
|
121
|
+
record.requestName,
|
|
122
|
+
record.handlerName,
|
|
123
|
+
record.payload === undefined ? null : json(record.payload),
|
|
124
|
+
final?.response === undefined ? null : json(final.response),
|
|
125
|
+
final?.error ? json(final.error) : null,
|
|
126
|
+
final?.durationMs ?? null,
|
|
127
|
+
metadata ? json(metadata) : null,
|
|
128
|
+
record.timestamp,
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=postgres.sink.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres.sink.js","sourceRoot":"","sources":["../../src/sinks/postgres.sink.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cqrs-ddd/pipeline-audit",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Audit records for @cqrs-ddd/pipeline: who did what, in which tenant, with redacted payloads, to a log or PostgreSQL sink",
|
|
5
|
+
"author": "Aristotelis",
|
|
6
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=22.12.0"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"pipeline",
|
|
13
|
+
"cqrs",
|
|
14
|
+
"audit",
|
|
15
|
+
"audit-trail",
|
|
16
|
+
"audit-log",
|
|
17
|
+
"compliance",
|
|
18
|
+
"security",
|
|
19
|
+
"redaction",
|
|
20
|
+
"behavior"
|
|
21
|
+
],
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/aristoteliss/ddd-cqrs.git",
|
|
25
|
+
"directory": "packages/pipeline-audit"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://aristoteliss.github.io/ddd-cqrs/packages/pipeline-audit/",
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/aristoteliss/ddd-cqrs/issues"
|
|
30
|
+
},
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"default": "./dist/index.js"
|
|
35
|
+
},
|
|
36
|
+
"./package.json": "./package.json"
|
|
37
|
+
},
|
|
38
|
+
"main": "dist/index.js",
|
|
39
|
+
"types": "dist/index.d.ts",
|
|
40
|
+
"files": [
|
|
41
|
+
"dist",
|
|
42
|
+
"LICENSE",
|
|
43
|
+
"COMMERCIAL_LICENSE.txt"
|
|
44
|
+
],
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"@cqrs-ddd/pipeline": "^0.5.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@vitest/coverage-v8": "^5.0.3",
|
|
53
|
+
"vitest": "^5.0.3",
|
|
54
|
+
"@cqrs-ddd/pipeline": "0.5.0"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@cqrs-ddd/safe-stringify": "^0.5.0",
|
|
58
|
+
"@cqrs-ddd/uuidv7": "^0.5.0"
|
|
59
|
+
},
|
|
60
|
+
"scripts": {
|
|
61
|
+
"build": "tsc -p tsconfig.build.json",
|
|
62
|
+
"build:watch": "tsc -p tsconfig.build.json --watch",
|
|
63
|
+
"clean": "rm -rf dist tsconfig.tsbuildinfo",
|
|
64
|
+
"rebuild": "pnpm run clean && pnpm run build",
|
|
65
|
+
"lint": "tsc --noEmit",
|
|
66
|
+
"test": "vitest run",
|
|
67
|
+
"test:watch": "vitest"
|
|
68
|
+
}
|
|
69
|
+
}
|