@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.
Files changed (52) hide show
  1. package/COMMERCIAL_LICENSE.txt +34 -0
  2. package/LICENSE +661 -0
  3. package/README.md +35 -0
  4. package/dist/audit.behavior.d.ts +102 -0
  5. package/dist/audit.behavior.d.ts.map +1 -0
  6. package/dist/audit.behavior.js +262 -0
  7. package/dist/audit.behavior.js.map +1 -0
  8. package/dist/constants/tokens.d.ts +27 -0
  9. package/dist/constants/tokens.d.ts.map +1 -0
  10. package/dist/constants/tokens.js +28 -0
  11. package/dist/constants/tokens.js.map +1 -0
  12. package/dist/helpers/audit.intent.d.ts +15 -0
  13. package/dist/helpers/audit.intent.d.ts.map +1 -0
  14. package/dist/helpers/audit.intent.js +15 -0
  15. package/dist/helpers/audit.intent.js.map +1 -0
  16. package/dist/helpers/build-record.d.ts +42 -0
  17. package/dist/helpers/build-record.d.ts.map +1 -0
  18. package/dist/helpers/build-record.js +85 -0
  19. package/dist/helpers/build-record.js.map +1 -0
  20. package/dist/helpers/json.d.ts +7 -0
  21. package/dist/helpers/json.d.ts.map +1 -0
  22. package/dist/helpers/json.js +102 -0
  23. package/dist/helpers/json.js.map +1 -0
  24. package/dist/helpers/redact.d.ts +2 -0
  25. package/dist/helpers/redact.d.ts.map +1 -0
  26. package/dist/helpers/redact.js +3 -0
  27. package/dist/helpers/redact.js.map +1 -0
  28. package/dist/index.d.ts +12 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +9 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/interfaces/audit-options.interface.d.ts +95 -0
  33. package/dist/interfaces/audit-options.interface.d.ts.map +1 -0
  34. package/dist/interfaces/audit-options.interface.js +3 -0
  35. package/dist/interfaces/audit-options.interface.js.map +1 -0
  36. package/dist/interfaces/audit-record.interface.d.ts +79 -0
  37. package/dist/interfaces/audit-record.interface.d.ts.map +1 -0
  38. package/dist/interfaces/audit-record.interface.js +3 -0
  39. package/dist/interfaces/audit-record.interface.js.map +1 -0
  40. package/dist/interfaces/audit-sink.interface.d.ts +37 -0
  41. package/dist/interfaces/audit-sink.interface.d.ts.map +1 -0
  42. package/dist/interfaces/audit-sink.interface.js +3 -0
  43. package/dist/interfaces/audit-sink.interface.js.map +1 -0
  44. package/dist/sinks/log.sink.d.ts +33 -0
  45. package/dist/sinks/log.sink.d.ts.map +1 -0
  46. package/dist/sinks/log.sink.js +31 -0
  47. package/dist/sinks/log.sink.js.map +1 -0
  48. package/dist/sinks/postgres.sink.d.ts +69 -0
  49. package/dist/sinks/postgres.sink.d.ts.map +1 -0
  50. package/dist/sinks/postgres.sink.js +132 -0
  51. package/dist/sinks/postgres.sink.js.map +1 -0
  52. 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
+ }