@parmanasystems/audit-db 1.0.19

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/README.md ADDED
@@ -0,0 +1,170 @@
1
+ # @parmanasystems/audit-db
2
+
3
+ PostgreSQL audit database client for parmanasystems governance decisions, verifications, and security events.
4
+
5
+ ## Overview
6
+
7
+ `audit-db` provides an `AuditDb` class that persists an immutable audit trail of every governance action. All write methods are **fire-and-forget** — they enqueue a database write and return immediately, so they never block or delay server responses.
8
+
9
+ The schema lives in PostgreSQL and is managed by the bundled `runMigrations()` function, which is idempotent and safe to run on every server startup.
10
+
11
+ ## Schema
12
+
13
+ ### Tables
14
+
15
+ | Table | Purpose |
16
+ |-------|---------|
17
+ | `audit_decisions` | One row per `POST /execute` — stores the full `ExecutionAttestation` as JSONB alongside extracted scalar fields for indexed queries. |
18
+ | `audit_verifications` | One row per `POST /verify` — records each check result (`signature_verified`, `runtime_verified`, `schema_compatible`). |
19
+ | `audit_security_events` | Auth failures, replay attempts, malformed requests. Severity: `low \| medium \| high \| critical`. Uses `occurred_at` timestamp column. |
20
+ | `audit_api_access` | Every inbound HTTP request with method, path, status code, response time (`response_time_ms`), user agent, and optional `execution_id`. |
21
+
22
+ ### Views
23
+
24
+ | View | Purpose |
25
+ |------|---------|
26
+ | `view_decision_timeline` | `audit_decisions` LEFT JOINed to `audit_verifications` — one row per decision, with verification outcome if available. |
27
+ | `view_security_dashboard` | Security events aggregated by `(event_type, severity)` with counts and timestamps. |
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ npm install @parmanasystems/audit-db
33
+ ```
34
+
35
+ Requires `pg` (node-postgres) which is listed as a peer dependency.
36
+
37
+ ## Quick start
38
+
39
+ ```typescript
40
+ import { AuditDb } from "@parmanasystems/audit-db";
41
+
42
+ const db = new AuditDb(process.env.AUDIT_DATABASE_URL!);
43
+
44
+ // Run migrations once on startup (idempotent)
45
+ await db.migrate();
46
+
47
+ // Fire-and-forget writes (never await these in the request path)
48
+ db.recordDecision(attestation);
49
+ db.recordVerification(executionId, verificationResult);
50
+ db.recordSecurityEvent({
51
+ event_type: "auth_failure",
52
+ severity: "medium",
53
+ ip_address: req.ip,
54
+ path: "/execute",
55
+ method: "POST",
56
+ user_agent: req.headers["user-agent"],
57
+ });
58
+ db.recordApiAccess({
59
+ method: "POST",
60
+ path: "/execute",
61
+ status_code: 200,
62
+ response_time_ms: 12,
63
+ execution_id: attestation.execution_id,
64
+ });
65
+
66
+ // Async reads
67
+ const stats = await db.getStats();
68
+ const timeline = await db.getDecisionTimeline(100, { policy_id: "claims-approval" });
69
+ const decision = await db.getDecisionById("11111111-...");
70
+ const verifications = await db.getVerificationsByExecution("11111111-...");
71
+ const dashboard = await db.getSecurityDashboard();
72
+
73
+ await db.disconnect();
74
+ ```
75
+
76
+ ## Server integration
77
+
78
+ When `AUDIT_DATABASE_URL` is set, the parmanasystems server automatically:
79
+
80
+ 1. Runs `db.migrate()` on startup (logs failure, continues running).
81
+ 2. Registers an `onResponse` Fastify hook that records every request and emits a security event for 401 responses.
82
+ 3. Calls `db.recordDecision()` after each `POST /execute`.
83
+ 4. Calls `db.recordVerification()` after each `POST /verify`.
84
+ 5. Exposes five read-only audit routes:
85
+
86
+ | Route | Description |
87
+ |-------|-------------|
88
+ | `GET /audit/decisions` | Decision timeline with optional query filters: `limit`, `offset`, `policy_id`, `decision`, `from`, `to` |
89
+ | `GET /audit/decisions/:executionId` | Single decision with full attestation JSONB |
90
+ | `GET /audit/security` | Security event dashboard, with optional `from`, `to`, `limit` filters |
91
+ | `GET /audit/stats` | Aggregate counts: total decisions, verifications, security events, API calls |
92
+ | `GET /audit/verifications/:executionId` | All verification attempts for an execution, newest first |
93
+
94
+ ## Docker Compose
95
+
96
+ The included `docker-compose.yml` wires up a `postgres:16-alpine` service and passes `AUDIT_DATABASE_URL` to the server automatically. Set `POSTGRES_PASSWORD` in `.env` before running:
97
+
98
+ ```bash
99
+ cp .env.example .env
100
+ # Edit .env — set POSTGRES_PASSWORD and other values
101
+ docker compose up
102
+ ```
103
+
104
+ ## API
105
+
106
+ ### `new AuditDb(connectionString: string)`
107
+
108
+ Creates a new pool-backed audit client.
109
+
110
+ ### `ping(): Promise<void>`
111
+
112
+ Sends `SELECT 1` to verify the connection is alive. Used by the server health check.
113
+
114
+ ### `migrate(): Promise<void>`
115
+
116
+ Runs the schema SQL inside a transaction. Safe to call on every startup — all DDL uses `IF NOT EXISTS` / `CREATE OR REPLACE`.
117
+
118
+ ### `recordDecision(attestation: ExecutionAttestation): void`
119
+
120
+ Fire-and-forget. Inserts the attestation into `audit_decisions`. Duplicate `execution_id` is silently ignored (`ON CONFLICT DO NOTHING`).
121
+
122
+ ### `recordVerification(executionId: string, result: VerificationResult): void`
123
+
124
+ Fire-and-forget. Inserts the verification result into `audit_verifications`.
125
+
126
+ ### `recordSecurityEvent(event: SecurityEventInput): void`
127
+
128
+ Fire-and-forget. Inserts into `audit_security_events`. Required: `event_type`, `severity` (`low | medium | high | critical`). Optional: `ip_address`, `path`, `method`, `user_agent`, `details` (JSONB).
129
+
130
+ ### `recordApiAccess(access: ApiAccessInput): void`
131
+
132
+ Fire-and-forget. Inserts into `audit_api_access`. Required: `method`, `path`, `status_code`. Optional: `response_time_ms`, `ip_address`, `user_agent`, `execution_id`.
133
+
134
+ ### `getDecisionTimeline(limit?: number, filter?: DecisionFilter): Promise<DecisionTimelineRow[]>`
135
+
136
+ Queries `view_decision_timeline`. Default limit 100. Optional filter fields: `policy_id`, `decision`, `from_date`, `to_date`.
137
+
138
+ ### `getStats(): Promise<AuditStats>`
139
+
140
+ Returns seven aggregate counts as strings (PostgreSQL `BIGINT` → TypeScript `string`):
141
+
142
+ | Field | Description |
143
+ |---|---|
144
+ | `total_decisions` | Total rows in `audit_decisions` |
145
+ | `decisions_today` | Rows where `executed_at >= CURRENT_DATE` |
146
+ | `total_verifications` | Total rows in `audit_verifications` |
147
+ | `valid_verifications` | Verifications where `valid = true` |
148
+ | `invalid_verifications` | Verifications where `valid = false` |
149
+ | `total_security_events` | Total rows in `audit_security_events` |
150
+ | `total_api_calls` | Total rows in `audit_api_access` |
151
+
152
+ ### `getDecisionById(executionId: string): Promise<AuditDecision | null>`
153
+
154
+ Returns the full decision row including the raw `attestation` JSONB, or `null` if not found.
155
+
156
+ ### `getVerificationsByExecution(executionId: string): Promise<AuditVerification[]>`
157
+
158
+ Returns all verification attempts for a given `execution_id`, newest first.
159
+
160
+ ### `getSecurityDashboard(): Promise<SecurityDashboardRow[]>`
161
+
162
+ Returns `view_security_dashboard` rows ordered by `event_count DESC`. Each row includes `event_type`, `severity`, `event_count` (string), `first_occurrence`, and `last_occurrence`.
163
+
164
+ ### `disconnect(): Promise<void>`
165
+
166
+ Drains the connection pool. Also available as `close()` — both call `pool.end()`.
167
+
168
+ ## License
169
+
170
+ Apache-2.0
@@ -0,0 +1,124 @@
1
+ import { ExecutionAttestation } from '@parmanasystems/execution';
2
+ export { ExecutionAttestation } from '@parmanasystems/execution';
3
+ import { VerificationResult } from '@parmanasystems/verifier';
4
+ export { VerificationResult } from '@parmanasystems/verifier';
5
+ import { PoolClient } from 'pg';
6
+
7
+ interface AuditDecision {
8
+ id: number;
9
+ execution_id: string;
10
+ policy_id: string;
11
+ policy_version: string;
12
+ schema_version: string;
13
+ runtime_version: string;
14
+ runtime_hash: string;
15
+ decision: string;
16
+ signals_hash: string;
17
+ signature: string;
18
+ attestation: ExecutionAttestation;
19
+ executed_at: Date;
20
+ recorded_at: Date;
21
+ }
22
+ interface AuditVerification {
23
+ id: number;
24
+ execution_id: string;
25
+ valid: boolean;
26
+ signature_verified: boolean;
27
+ runtime_verified: boolean;
28
+ schema_compatible: boolean;
29
+ verified_at: Date;
30
+ }
31
+ type SecurityEventSeverity = "low" | "medium" | "high" | "critical";
32
+ interface SecurityEventInput {
33
+ event_type: string;
34
+ severity: SecurityEventSeverity;
35
+ ip_address?: string;
36
+ path?: string;
37
+ method?: string;
38
+ user_agent?: string;
39
+ details?: Record<string, unknown>;
40
+ }
41
+ interface AuditSecurityEvent extends SecurityEventInput {
42
+ id: number;
43
+ occurred_at: Date;
44
+ }
45
+ interface ApiAccessInput {
46
+ method: string;
47
+ path: string;
48
+ status_code: number;
49
+ response_time_ms?: number;
50
+ ip_address?: string;
51
+ user_agent?: string;
52
+ execution_id?: string;
53
+ }
54
+ interface AuditApiAccess extends ApiAccessInput {
55
+ id: number;
56
+ accessed_at: Date;
57
+ }
58
+ interface DecisionTimelineRow {
59
+ execution_id: string;
60
+ policy_id: string;
61
+ policy_version: string;
62
+ decision: string;
63
+ runtime_version: string;
64
+ runtime_hash: string;
65
+ executed_at: Date;
66
+ recorded_at: Date;
67
+ verification_valid: boolean | null;
68
+ signature_verified: boolean | null;
69
+ runtime_verified: boolean | null;
70
+ schema_compatible: boolean | null;
71
+ verified_at: Date | null;
72
+ }
73
+ interface SecurityDashboardRow {
74
+ event_type: string;
75
+ severity: string;
76
+ /** pg returns BIGINT as string */
77
+ event_count: string;
78
+ last_occurrence: Date;
79
+ first_occurrence: Date;
80
+ }
81
+ interface DecisionFilter {
82
+ policy_id?: string;
83
+ decision?: string;
84
+ /** ISO date string — inclusive lower bound on executed_at */
85
+ from_date?: string;
86
+ /** ISO date string — inclusive upper bound on executed_at */
87
+ to_date?: string;
88
+ }
89
+ /** All counts returned as strings because pg serialises BIGINT as string. */
90
+ interface AuditStats {
91
+ total_decisions: string;
92
+ decisions_today: string;
93
+ total_verifications: string;
94
+ valid_verifications: string;
95
+ invalid_verifications: string;
96
+ total_security_events: string;
97
+ total_api_calls: string;
98
+ }
99
+
100
+ declare class AuditDb {
101
+ private readonly pool;
102
+ constructor(connectionString: string);
103
+ ping(): Promise<void>;
104
+ disconnect(): Promise<void>;
105
+ migrate(): Promise<void>;
106
+ /** Fire-and-forget — never throws, never delays the caller. */
107
+ recordDecision(attestation: ExecutionAttestation): void;
108
+ /** Fire-and-forget — never throws, never delays the caller. */
109
+ recordVerification(executionId: string, result: VerificationResult): void;
110
+ /** Fire-and-forget — never throws, never delays the caller. */
111
+ recordSecurityEvent(event: SecurityEventInput): void;
112
+ /** Fire-and-forget — never throws, never delays the caller. */
113
+ recordApiAccess(access: ApiAccessInput): void;
114
+ getDecisionTimeline(limit?: number, filter?: DecisionFilter): Promise<DecisionTimelineRow[]>;
115
+ getStats(): Promise<AuditStats>;
116
+ getDecisionById(executionId: string): Promise<AuditDecision | null>;
117
+ getVerificationsByExecution(executionId: string): Promise<AuditVerification[]>;
118
+ getSecurityDashboard(): Promise<SecurityDashboardRow[]>;
119
+ close(): Promise<void>;
120
+ }
121
+
122
+ declare function runMigrations(client: PoolClient): Promise<void>;
123
+
124
+ export { type ApiAccessInput, type AuditApiAccess, AuditDb, type AuditDecision, type AuditSecurityEvent, type AuditStats, type AuditVerification, type DecisionFilter, type DecisionTimelineRow, type SecurityDashboardRow, type SecurityEventInput, type SecurityEventSeverity, runMigrations };
package/dist/index.js ADDED
@@ -0,0 +1,282 @@
1
+ // src/client.ts
2
+ import { Pool } from "pg";
3
+
4
+ // src/migrations.ts
5
+ var SCHEMA_SQL = `
6
+ CREATE TABLE IF NOT EXISTS audit_decisions (
7
+ id BIGSERIAL PRIMARY KEY,
8
+ execution_id UUID NOT NULL UNIQUE,
9
+ policy_id TEXT NOT NULL,
10
+ policy_version TEXT NOT NULL,
11
+ schema_version TEXT NOT NULL,
12
+ runtime_version TEXT NOT NULL,
13
+ runtime_hash TEXT NOT NULL,
14
+ decision TEXT NOT NULL,
15
+ signals_hash TEXT NOT NULL,
16
+ signature TEXT NOT NULL,
17
+ attestation JSONB NOT NULL,
18
+ executed_at TIMESTAMPTZ NOT NULL,
19
+ recorded_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
20
+ );
21
+
22
+ CREATE INDEX IF NOT EXISTS idx_audit_decisions_policy_id
23
+ ON audit_decisions (policy_id);
24
+ CREATE INDEX IF NOT EXISTS idx_audit_decisions_executed_at
25
+ ON audit_decisions (executed_at DESC);
26
+ CREATE INDEX IF NOT EXISTS idx_audit_decisions_decision
27
+ ON audit_decisions (decision);
28
+
29
+ CREATE TABLE IF NOT EXISTS audit_verifications (
30
+ id BIGSERIAL PRIMARY KEY,
31
+ execution_id UUID NOT NULL,
32
+ valid BOOLEAN NOT NULL,
33
+ signature_verified BOOLEAN NOT NULL,
34
+ runtime_verified BOOLEAN NOT NULL,
35
+ schema_compatible BOOLEAN NOT NULL,
36
+ verified_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
37
+ );
38
+
39
+ CREATE INDEX IF NOT EXISTS idx_audit_verifications_execution_id
40
+ ON audit_verifications (execution_id);
41
+ CREATE INDEX IF NOT EXISTS idx_audit_verifications_valid
42
+ ON audit_verifications (valid);
43
+ CREATE INDEX IF NOT EXISTS idx_audit_verifications_verified_at
44
+ ON audit_verifications (verified_at DESC);
45
+
46
+ CREATE TABLE IF NOT EXISTS audit_security_events (
47
+ id BIGSERIAL PRIMARY KEY,
48
+ event_type TEXT NOT NULL,
49
+ severity TEXT NOT NULL CHECK (severity IN ('low','medium','high','critical')),
50
+ ip_address TEXT,
51
+ path TEXT,
52
+ method TEXT,
53
+ user_agent TEXT,
54
+ details JSONB,
55
+ occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
56
+ );
57
+
58
+ CREATE INDEX IF NOT EXISTS idx_audit_security_events_event_type
59
+ ON audit_security_events (event_type);
60
+ CREATE INDEX IF NOT EXISTS idx_audit_security_events_severity
61
+ ON audit_security_events (severity);
62
+ CREATE INDEX IF NOT EXISTS idx_audit_security_events_occurred_at
63
+ ON audit_security_events (occurred_at DESC);
64
+
65
+ CREATE TABLE IF NOT EXISTS audit_api_access (
66
+ id BIGSERIAL PRIMARY KEY,
67
+ method TEXT NOT NULL,
68
+ path TEXT NOT NULL,
69
+ status_code INTEGER NOT NULL,
70
+ response_time_ms INTEGER,
71
+ ip_address TEXT,
72
+ user_agent TEXT,
73
+ execution_id UUID,
74
+ accessed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
75
+ );
76
+
77
+ CREATE INDEX IF NOT EXISTS idx_audit_api_access_path
78
+ ON audit_api_access (path);
79
+ CREATE INDEX IF NOT EXISTS idx_audit_api_access_status_code
80
+ ON audit_api_access (status_code);
81
+ CREATE INDEX IF NOT EXISTS idx_audit_api_access_accessed_at
82
+ ON audit_api_access (accessed_at DESC);
83
+
84
+ CREATE OR REPLACE VIEW view_decision_timeline AS
85
+ SELECT
86
+ d.execution_id,
87
+ d.policy_id,
88
+ d.policy_version,
89
+ d.decision,
90
+ d.runtime_version,
91
+ d.runtime_hash,
92
+ d.executed_at,
93
+ d.recorded_at,
94
+ v.valid AS verification_valid,
95
+ v.signature_verified,
96
+ v.runtime_verified,
97
+ v.schema_compatible,
98
+ v.verified_at
99
+ FROM audit_decisions d
100
+ LEFT JOIN audit_verifications v ON d.execution_id = v.execution_id;
101
+
102
+ CREATE OR REPLACE VIEW view_security_dashboard AS
103
+ SELECT
104
+ event_type,
105
+ severity,
106
+ COUNT(*) AS event_count,
107
+ MAX(occurred_at) AS last_occurrence,
108
+ MIN(occurred_at) AS first_occurrence
109
+ FROM audit_security_events
110
+ GROUP BY event_type, severity;
111
+ `;
112
+ async function runMigrations(client) {
113
+ await client.query("BEGIN");
114
+ try {
115
+ await client.query(SCHEMA_SQL);
116
+ await client.query("COMMIT");
117
+ } catch (err) {
118
+ await client.query("ROLLBACK");
119
+ throw err;
120
+ }
121
+ }
122
+
123
+ // src/client.ts
124
+ var AuditDb = class {
125
+ pool;
126
+ constructor(connectionString) {
127
+ this.pool = new Pool({ connectionString });
128
+ }
129
+ async ping() {
130
+ await this.pool.query("SELECT 1");
131
+ }
132
+ async disconnect() {
133
+ await this.pool.end();
134
+ }
135
+ async migrate() {
136
+ const client = await this.pool.connect();
137
+ try {
138
+ await runMigrations(client);
139
+ } finally {
140
+ client.release();
141
+ }
142
+ }
143
+ /** Fire-and-forget — never throws, never delays the caller. */
144
+ recordDecision(attestation) {
145
+ this.pool.query(
146
+ `INSERT INTO audit_decisions
147
+ (execution_id, decision, execution_state,
148
+ runtime_hash, signature, attestation, executed_at)
149
+ VALUES ($1,$2,$3,$4,$5,$6,$7)
150
+ ON CONFLICT (execution_id) DO NOTHING`,
151
+ [
152
+ attestation.execution_id,
153
+ attestation.decision,
154
+ attestation.execution_state,
155
+ attestation.runtime_hash,
156
+ attestation.signature,
157
+ JSON.stringify(attestation),
158
+ (/* @__PURE__ */ new Date()).toISOString()
159
+ ]
160
+ ).catch(() => void 0);
161
+ }
162
+ /** Fire-and-forget — never throws, never delays the caller. */
163
+ recordVerification(executionId, result) {
164
+ this.pool.query(
165
+ `INSERT INTO audit_verifications
166
+ (execution_id, valid, signature_verified, runtime_verified, schema_compatible)
167
+ VALUES ($1,$2,$3,$4,$5)`,
168
+ [
169
+ executionId,
170
+ result.valid,
171
+ result.checks.signature_verified,
172
+ result.checks.runtime_verified,
173
+ result.checks.schema_compatible
174
+ ]
175
+ ).catch(() => void 0);
176
+ }
177
+ /** Fire-and-forget — never throws, never delays the caller. */
178
+ recordSecurityEvent(event) {
179
+ this.pool.query(
180
+ `INSERT INTO audit_security_events
181
+ (event_type, severity, ip_address, path, method, user_agent, details)
182
+ VALUES ($1,$2,$3,$4,$5,$6,$7)`,
183
+ [
184
+ event.event_type,
185
+ event.severity,
186
+ event.ip_address ?? null,
187
+ event.path ?? null,
188
+ event.method ?? null,
189
+ event.user_agent ?? null,
190
+ event.details != null ? JSON.stringify(event.details) : null
191
+ ]
192
+ ).catch(() => void 0);
193
+ }
194
+ /** Fire-and-forget — never throws, never delays the caller. */
195
+ recordApiAccess(access) {
196
+ this.pool.query(
197
+ `INSERT INTO audit_api_access
198
+ (method, path, status_code, response_time_ms, ip_address, user_agent, execution_id)
199
+ VALUES ($1,$2,$3,$4,$5,$6,$7)`,
200
+ [
201
+ access.method,
202
+ access.path,
203
+ access.status_code,
204
+ access.response_time_ms ?? null,
205
+ access.ip_address ?? null,
206
+ access.user_agent ?? null,
207
+ access.execution_id ?? null
208
+ ]
209
+ ).catch(() => void 0);
210
+ }
211
+ async getDecisionTimeline(limit = 100, filter) {
212
+ const conditions = [];
213
+ const values = [];
214
+ if (filter?.policy_id) {
215
+ values.push(filter.policy_id);
216
+ conditions.push(`policy_id = $${values.length}`);
217
+ }
218
+ if (filter?.decision) {
219
+ values.push(filter.decision);
220
+ conditions.push(`decision = $${values.length}`);
221
+ }
222
+ if (filter?.from_date) {
223
+ values.push(filter.from_date);
224
+ conditions.push(`executed_at >= $${values.length}`);
225
+ }
226
+ if (filter?.to_date) {
227
+ values.push(filter.to_date);
228
+ conditions.push(`executed_at <= $${values.length}`);
229
+ }
230
+ values.push(limit);
231
+ const limitParam = `$${values.length}`;
232
+ const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
233
+ const { rows } = await this.pool.query(
234
+ `SELECT * FROM view_decision_timeline ${where} ORDER BY executed_at DESC LIMIT ${limitParam}`,
235
+ values
236
+ );
237
+ return rows;
238
+ }
239
+ async getStats() {
240
+ const { rows } = await this.pool.query(`
241
+ SELECT
242
+ (SELECT COUNT(*) FROM audit_decisions)::text AS total_decisions,
243
+ (SELECT COUNT(*) FROM audit_decisions WHERE executed_at >= CURRENT_DATE)::text AS decisions_today,
244
+ (SELECT COUNT(*) FROM audit_verifications)::text AS total_verifications,
245
+ (SELECT COUNT(*) FROM audit_verifications WHERE valid = true)::text AS valid_verifications,
246
+ (SELECT COUNT(*) FROM audit_verifications WHERE valid = false)::text AS invalid_verifications,
247
+ (SELECT COUNT(*) FROM audit_security_events)::text AS total_security_events,
248
+ (SELECT COUNT(*) FROM audit_api_access)::text AS total_api_calls
249
+ `);
250
+ return rows[0];
251
+ }
252
+ async getDecisionById(executionId) {
253
+ const { rows } = await this.pool.query(
254
+ `SELECT * FROM audit_decisions WHERE execution_id = $1`,
255
+ [executionId]
256
+ );
257
+ return rows[0] ?? null;
258
+ }
259
+ async getVerificationsByExecution(executionId) {
260
+ const { rows } = await this.pool.query(
261
+ `SELECT * FROM audit_verifications
262
+ WHERE execution_id = $1
263
+ ORDER BY verified_at DESC`,
264
+ [executionId]
265
+ );
266
+ return rows;
267
+ }
268
+ async getSecurityDashboard() {
269
+ const { rows } = await this.pool.query(
270
+ `SELECT * FROM view_security_dashboard ORDER BY event_count DESC`
271
+ );
272
+ return rows;
273
+ }
274
+ async close() {
275
+ await this.pool.end();
276
+ }
277
+ };
278
+ export {
279
+ AuditDb,
280
+ runMigrations
281
+ };
282
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client.ts","../src/migrations.ts"],"sourcesContent":["import { Pool } from \"pg\";\nimport type { PoolClient } from \"pg\";\n\nimport type { ExecutionAttestation } from \"@parmanasystems/execution\";\nimport type { VerificationResult } from \"@parmanasystems/verifier\";\n\nimport type {\n AuditDecision,\n AuditVerification,\n SecurityEventInput,\n ApiAccessInput,\n DecisionTimelineRow,\n DecisionFilter,\n SecurityDashboardRow,\n AuditStats,\n} from \"./types.js\";\nimport { runMigrations } from \"./migrations.js\";\n\nexport class AuditDb {\n private readonly pool: InstanceType<typeof Pool>;\n\n constructor(connectionString: string) {\n this.pool = new Pool({ connectionString });\n }\n\n async ping(): Promise<void> {\n await this.pool.query(\"SELECT 1\");\n }\n\n async disconnect(): Promise<void> {\n await this.pool.end();\n }\n\n async migrate(): Promise<void> {\n const client: PoolClient = await this.pool.connect();\n try {\n await runMigrations(client);\n } finally {\n client.release();\n }\n }\n\n /** Fire-and-forget — never throws, never delays the caller. */\n recordDecision(attestation: ExecutionAttestation): void {\n this.pool\n .query(\n `INSERT INTO audit_decisions\n (execution_id, decision, execution_state,\n runtime_hash, signature, attestation, executed_at)\n VALUES ($1,$2,$3,$4,$5,$6,$7)\n ON CONFLICT (execution_id) DO NOTHING`,\n [\n attestation.execution_id,\n attestation.decision,\n attestation.execution_state,\n attestation.runtime_hash,\n attestation.signature,\n JSON.stringify(attestation),\n new Date().toISOString(),\n ],\n )\n .catch(() => undefined);\n }\n\n /** Fire-and-forget — never throws, never delays the caller. */\n recordVerification(executionId: string, result: VerificationResult): void {\n this.pool\n .query(\n `INSERT INTO audit_verifications\n (execution_id, valid, signature_verified, runtime_verified, schema_compatible)\n VALUES ($1,$2,$3,$4,$5)`,\n [\n executionId,\n result.valid,\n result.checks.signature_verified,\n result.checks.runtime_verified,\n result.checks.schema_compatible,\n ],\n )\n .catch(() => undefined);\n }\n\n /** Fire-and-forget — never throws, never delays the caller. */\n recordSecurityEvent(event: SecurityEventInput): void {\n this.pool\n .query(\n `INSERT INTO audit_security_events\n (event_type, severity, ip_address, path, method, user_agent, details)\n VALUES ($1,$2,$3,$4,$5,$6,$7)`,\n [\n event.event_type,\n event.severity,\n event.ip_address ?? null,\n event.path ?? null,\n event.method ?? null,\n event.user_agent ?? null,\n event.details != null ? JSON.stringify(event.details) : null,\n ],\n )\n .catch(() => undefined);\n }\n\n /** Fire-and-forget — never throws, never delays the caller. */\n recordApiAccess(access: ApiAccessInput): void {\n this.pool\n .query(\n `INSERT INTO audit_api_access\n (method, path, status_code, response_time_ms, ip_address, user_agent, execution_id)\n VALUES ($1,$2,$3,$4,$5,$6,$7)`,\n [\n access.method,\n access.path,\n access.status_code,\n access.response_time_ms ?? null,\n access.ip_address ?? null,\n access.user_agent ?? null,\n access.execution_id ?? null,\n ],\n )\n .catch(() => undefined);\n }\n\n async getDecisionTimeline(limit = 100, filter?: DecisionFilter): Promise<DecisionTimelineRow[]> {\n const conditions: string[] = [];\n const values: unknown[] = [];\n\n if (filter?.policy_id) {\n values.push(filter.policy_id);\n conditions.push(`policy_id = $${values.length}`);\n }\n if (filter?.decision) {\n values.push(filter.decision);\n conditions.push(`decision = $${values.length}`);\n }\n if (filter?.from_date) {\n values.push(filter.from_date);\n conditions.push(`executed_at >= $${values.length}`);\n }\n if (filter?.to_date) {\n values.push(filter.to_date);\n conditions.push(`executed_at <= $${values.length}`);\n }\n\n values.push(limit);\n const limitParam = `$${values.length}`;\n const where = conditions.length ? `WHERE ${conditions.join(\" AND \")}` : \"\";\n\n const { rows } = await this.pool.query<DecisionTimelineRow>(\n `SELECT * FROM view_decision_timeline ${where} ORDER BY executed_at DESC LIMIT ${limitParam}`,\n values,\n );\n return rows;\n }\n\n async getStats(): Promise<AuditStats> {\n const { rows } = await this.pool.query<AuditStats>(`\n SELECT\n (SELECT COUNT(*) FROM audit_decisions)::text AS total_decisions,\n (SELECT COUNT(*) FROM audit_decisions WHERE executed_at >= CURRENT_DATE)::text AS decisions_today,\n (SELECT COUNT(*) FROM audit_verifications)::text AS total_verifications,\n (SELECT COUNT(*) FROM audit_verifications WHERE valid = true)::text AS valid_verifications,\n (SELECT COUNT(*) FROM audit_verifications WHERE valid = false)::text AS invalid_verifications,\n (SELECT COUNT(*) FROM audit_security_events)::text AS total_security_events,\n (SELECT COUNT(*) FROM audit_api_access)::text AS total_api_calls\n `);\n return rows[0]!;\n }\n\n async getDecisionById(executionId: string): Promise<AuditDecision | null> {\n const { rows } = await this.pool.query<AuditDecision>(\n `SELECT * FROM audit_decisions WHERE execution_id = $1`,\n [executionId],\n );\n return rows[0] ?? null;\n }\n\n async getVerificationsByExecution(executionId: string): Promise<AuditVerification[]> {\n const { rows } = await this.pool.query<AuditVerification>(\n `SELECT * FROM audit_verifications\n WHERE execution_id = $1\n ORDER BY verified_at DESC`,\n [executionId],\n );\n return rows;\n }\n\n async getSecurityDashboard(): Promise<SecurityDashboardRow[]> {\n const { rows } = await this.pool.query<SecurityDashboardRow>(\n `SELECT * FROM view_security_dashboard ORDER BY event_count DESC`,\n );\n return rows;\n }\n\n async close(): Promise<void> {\n await this.pool.end();\n }\n}","import type { PoolClient } from \"pg\";\n\n// Inlined so the bundle has no runtime fs dependency.\n// The canonical reference lives in schema.sql alongside this file.\nconst SCHEMA_SQL = `\nCREATE TABLE IF NOT EXISTS audit_decisions (\n id BIGSERIAL PRIMARY KEY,\n execution_id UUID NOT NULL UNIQUE,\n policy_id TEXT NOT NULL,\n policy_version TEXT NOT NULL,\n schema_version TEXT NOT NULL,\n runtime_version TEXT NOT NULL,\n runtime_hash TEXT NOT NULL,\n decision TEXT NOT NULL,\n signals_hash TEXT NOT NULL,\n signature TEXT NOT NULL,\n attestation JSONB NOT NULL,\n executed_at TIMESTAMPTZ NOT NULL,\n recorded_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n);\n\nCREATE INDEX IF NOT EXISTS idx_audit_decisions_policy_id\n ON audit_decisions (policy_id);\nCREATE INDEX IF NOT EXISTS idx_audit_decisions_executed_at\n ON audit_decisions (executed_at DESC);\nCREATE INDEX IF NOT EXISTS idx_audit_decisions_decision\n ON audit_decisions (decision);\n\nCREATE TABLE IF NOT EXISTS audit_verifications (\n id BIGSERIAL PRIMARY KEY,\n execution_id UUID NOT NULL,\n valid BOOLEAN NOT NULL,\n signature_verified BOOLEAN NOT NULL,\n runtime_verified BOOLEAN NOT NULL,\n schema_compatible BOOLEAN NOT NULL,\n verified_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n);\n\nCREATE INDEX IF NOT EXISTS idx_audit_verifications_execution_id\n ON audit_verifications (execution_id);\nCREATE INDEX IF NOT EXISTS idx_audit_verifications_valid\n ON audit_verifications (valid);\nCREATE INDEX IF NOT EXISTS idx_audit_verifications_verified_at\n ON audit_verifications (verified_at DESC);\n\nCREATE TABLE IF NOT EXISTS audit_security_events (\n id BIGSERIAL PRIMARY KEY,\n event_type TEXT NOT NULL,\n severity TEXT NOT NULL CHECK (severity IN ('low','medium','high','critical')),\n ip_address TEXT,\n path TEXT,\n method TEXT,\n user_agent TEXT,\n details JSONB,\n occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n);\n\nCREATE INDEX IF NOT EXISTS idx_audit_security_events_event_type\n ON audit_security_events (event_type);\nCREATE INDEX IF NOT EXISTS idx_audit_security_events_severity\n ON audit_security_events (severity);\nCREATE INDEX IF NOT EXISTS idx_audit_security_events_occurred_at\n ON audit_security_events (occurred_at DESC);\n\nCREATE TABLE IF NOT EXISTS audit_api_access (\n id BIGSERIAL PRIMARY KEY,\n method TEXT NOT NULL,\n path TEXT NOT NULL,\n status_code INTEGER NOT NULL,\n response_time_ms INTEGER,\n ip_address TEXT,\n user_agent TEXT,\n execution_id UUID,\n accessed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n);\n\nCREATE INDEX IF NOT EXISTS idx_audit_api_access_path\n ON audit_api_access (path);\nCREATE INDEX IF NOT EXISTS idx_audit_api_access_status_code\n ON audit_api_access (status_code);\nCREATE INDEX IF NOT EXISTS idx_audit_api_access_accessed_at\n ON audit_api_access (accessed_at DESC);\n\nCREATE OR REPLACE VIEW view_decision_timeline AS\nSELECT\n d.execution_id,\n d.policy_id,\n d.policy_version,\n d.decision,\n d.runtime_version,\n d.runtime_hash,\n d.executed_at,\n d.recorded_at,\n v.valid AS verification_valid,\n v.signature_verified,\n v.runtime_verified,\n v.schema_compatible,\n v.verified_at\nFROM audit_decisions d\nLEFT JOIN audit_verifications v ON d.execution_id = v.execution_id;\n\nCREATE OR REPLACE VIEW view_security_dashboard AS\nSELECT\n event_type,\n severity,\n COUNT(*) AS event_count,\n MAX(occurred_at) AS last_occurrence,\n MIN(occurred_at) AS first_occurrence\nFROM audit_security_events\nGROUP BY event_type, severity;\n`;\n\nexport async function runMigrations(client: PoolClient): Promise<void> {\n await client.query(\"BEGIN\");\n try {\n await client.query(SCHEMA_SQL);\n await client.query(\"COMMIT\");\n } catch (err) {\n await client.query(\"ROLLBACK\");\n throw err;\n }\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@parmanasystems/audit-db",
3
+ "version": "1.0.19",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "PostgreSQL audit database client for parmanasystems governance decisions, verifications, and security events.",
7
+ "scripts": {
8
+ "build": "tsup"
9
+ },
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "default": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "sideEffects": false,
21
+ "dependencies": {
22
+ "@parmanasystems/execution": "^1.0.19",
23
+ "@parmanasystems/verifier": "^1.0.19",
24
+ "pg": "^8.13.3"
25
+ },
26
+ "devDependencies": {
27
+ "@types/pg": "^8.11.13"
28
+ },
29
+ "engines": {
30
+ "node": ">=20"
31
+ },
32
+ "license": "Apache-2.0",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/pavancharak/parmanasystems-core.git"
36
+ },
37
+ "homepage": "https://github.com/pavancharak/parmanasystems-core",
38
+ "bugs": {
39
+ "url": "https://github.com/pavancharak/parmanasystems-core/issues"
40
+ },
41
+ "main": "./dist/index.js",
42
+ "types": "./dist/index.d.ts"
43
+ }