@venturekit-pro/audit 0.0.0-dev.20260602192622

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.
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Read helpers over the `audit_events` table.
3
+ *
4
+ * Pure audit — no domain aggregation. The package can:
5
+ * - `listEvents()` — paginated newest-first list.
6
+ * - `countEvents()` — count matching events, with an
7
+ * optional per-prefix breakdown.
8
+ * - `monthlyEventCounts()` — events per month, optionally
9
+ * per-prefix split. Useful for
10
+ * "activity volume" dashboards.
11
+ *
12
+ * Anything more specific (cost-in-USD, payment totals, token usage,
13
+ * etc.) belongs in the package that owns that concern. The audit
14
+ * package only references those via `correlation_id` / `target_id`.
15
+ */
16
+ import type { AuditEvent, EventCount, Querier } from './types.js';
17
+ export interface ListEventsArgs {
18
+ tenantId: string;
19
+ /** Filter by `kind` (exact match). */
20
+ kind?: string;
21
+ /** Filter by `kind` prefix (matches every event whose kind starts with the value). */
22
+ kindPrefix?: string;
23
+ /** Filter by target row. Pass both `targetType` AND `targetId`. */
24
+ targetType?: string;
25
+ targetId?: string;
26
+ /** Filter by `correlationId`. */
27
+ correlationId?: string;
28
+ /** Filter by `actorType`. */
29
+ actorType?: string;
30
+ /** Filter by `status`. */
31
+ status?: string;
32
+ /** Page size; default 50, capped at 500. */
33
+ limit?: number;
34
+ /**
35
+ * `id` boundary; rows with `id < beforeId` (strictly older when
36
+ * sorted by created_at DESC) are returned. Stable cursor; works
37
+ * even when many rows share the same `created_at`.
38
+ */
39
+ beforeId?: string;
40
+ }
41
+ /**
42
+ * Paginated event list, newest first.
43
+ */
44
+ export declare function listEvents(querier: Querier, args: ListEventsArgs): Promise<AuditEvent[]>;
45
+ export interface CountEventsArgs {
46
+ tenantId: string;
47
+ kind?: string;
48
+ kindPrefix?: string;
49
+ targetType?: string;
50
+ targetId?: string;
51
+ correlationId?: string;
52
+ actorType?: string;
53
+ status?: string;
54
+ /** Inclusive lower bound on `created_at`. */
55
+ since?: Date;
56
+ /** Exclusive upper bound on `created_at`. */
57
+ until?: Date;
58
+ /**
59
+ * Optional kind-prefixes to split the count by. Each prefix is
60
+ * matched as `kind LIKE '<prefix>%'`. Empty / omitted = no split,
61
+ * only `count`.
62
+ */
63
+ byKindPrefix?: string[];
64
+ }
65
+ /**
66
+ * Count events matching the filter, optionally split by kind-prefix.
67
+ *
68
+ * The package's only aggregation surface — domain-specific rollups
69
+ * (cost, payment totals, token usage) live in their own packages.
70
+ */
71
+ export declare function countEvents(querier: Querier, args: CountEventsArgs): Promise<EventCount>;
72
+ export interface MonthlyEventCountsArgs {
73
+ tenantId: string;
74
+ /**
75
+ * Number of trailing months to return (current month + the last
76
+ * `monthCount - 1`). Default 12, capped at 36.
77
+ */
78
+ monthCount?: number;
79
+ /** Optional kind-prefixes to split each month's count by. */
80
+ byKindPrefix?: string[];
81
+ }
82
+ export interface MonthlyCountBucket extends EventCount {
83
+ /** ISO date (`YYYY-MM-01`) for the start of the bucket's month. */
84
+ month: string;
85
+ }
86
+ /**
87
+ * Events per trailing month — empty months emit a zero row so the
88
+ * consumer can render a flat chart without gap handling.
89
+ */
90
+ export declare function monthlyEventCounts(querier: Querier, args: MonthlyEventCountsArgs): Promise<MonthlyCountBucket[]>;
91
+ //# sourceMappingURL=query.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAoElE,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,sCAAsC;IACtC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sFAAsF;IACtF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mEAAmE;IACnE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iCAAiC;IACjC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,6BAA6B;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0BAA0B;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,cAAc,GACnB,OAAO,CAAC,UAAU,EAAE,CAAC,CAiBvB;AAID,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6CAA6C;IAC7C,KAAK,CAAC,EAAE,IAAI,CAAC;IACb,6CAA6C;IAC7C,KAAK,CAAC,EAAE,IAAI,CAAC;IACb;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAC/B,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,UAAU,CAAC,CAoBrB;AAID,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6DAA6D;IAC7D,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,kBAAmB,SAAQ,UAAU;IACpD,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,sBAAsB,GAC3B,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAyB/B"}
package/dist/query.js ADDED
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Read helpers over the `audit_events` table.
3
+ *
4
+ * Pure audit — no domain aggregation. The package can:
5
+ * - `listEvents()` — paginated newest-first list.
6
+ * - `countEvents()` — count matching events, with an
7
+ * optional per-prefix breakdown.
8
+ * - `monthlyEventCounts()` — events per month, optionally
9
+ * per-prefix split. Useful for
10
+ * "activity volume" dashboards.
11
+ *
12
+ * Anything more specific (cost-in-USD, payment totals, token usage,
13
+ * etc.) belongs in the package that owns that concern. The audit
14
+ * package only references those via `correlation_id` / `target_id`.
15
+ */
16
+ // ─── Internal helpers ──────────────────────────────────────────────────
17
+ /**
18
+ * Validate a prefix list: non-empty strings, deduplicated, no SQL
19
+ * pattern characters. We embed prefixes as SQL identifiers (not
20
+ * parameters) because `FILTER (WHERE kind LIKE $N)` would emit a
21
+ * dynamic column per row otherwise, which we don't want.
22
+ */
23
+ function normalizePrefixes(prefixes) {
24
+ if (!prefixes || prefixes.length === 0)
25
+ return [];
26
+ const out = new Set();
27
+ for (const raw of prefixes) {
28
+ if (typeof raw !== 'string' || raw.length === 0)
29
+ continue;
30
+ // Reject LIKE-significant characters so a caller can't smuggle
31
+ // wildcard semantics into the rollup. A trailing dot is the
32
+ // expected pattern (`'order.'` matches `'order.refunded'` etc).
33
+ if (/[%_\\']/.test(raw)) {
34
+ throw new Error(`[audit/query] Invalid prefix '${raw}' — must not contain %, _, \\ or '`);
35
+ }
36
+ out.add(raw);
37
+ }
38
+ return Array.from(out);
39
+ }
40
+ function toNumber(v) {
41
+ if (v === null || v === undefined)
42
+ return 0;
43
+ const n = typeof v === 'number' ? v : Number(v);
44
+ return Number.isFinite(n) ? n : 0;
45
+ }
46
+ function rowToCount(row, prefixes) {
47
+ const byKindPrefix = {};
48
+ prefixes.forEach((p, i) => {
49
+ byKindPrefix[p] = Math.trunc(toNumber(row?.[`split_${i}`]));
50
+ });
51
+ return {
52
+ count: Math.trunc(toNumber(row?.count)),
53
+ byKindPrefix,
54
+ };
55
+ }
56
+ /**
57
+ * Build the SELECT clause for a count query, adding one
58
+ * `COUNT(...) FILTER (WHERE kind LIKE 'prefix%')` column per
59
+ * requested prefix.
60
+ */
61
+ function buildCountSelect(prefixes, rowAlias = '') {
62
+ const prefix = rowAlias ? `${rowAlias}.` : '';
63
+ const baseCols = `COUNT(${prefix}id) AS "count"`;
64
+ if (prefixes.length === 0)
65
+ return baseCols;
66
+ const splitCols = prefixes.map((p, i) => `,\n COUNT(${prefix}id) FILTER (WHERE ${prefix}kind LIKE '${p}%') AS "split_${i}"`);
67
+ return baseCols + splitCols.join('');
68
+ }
69
+ /**
70
+ * Paginated event list, newest first.
71
+ */
72
+ export async function listEvents(querier, args) {
73
+ const { conditions, params } = buildEventFilter(args);
74
+ const limit = Math.min(Math.max(args.limit ?? 50, 1), 500);
75
+ if (args.beforeId !== undefined) {
76
+ params.push(args.beforeId);
77
+ conditions.push(`id < $${params.length}`);
78
+ }
79
+ params.push(limit);
80
+ return querier(`SELECT * FROM audit_events
81
+ WHERE ${conditions.join(' AND ')}
82
+ ORDER BY created_at DESC, id DESC
83
+ LIMIT $${params.length}`, params);
84
+ }
85
+ /**
86
+ * Count events matching the filter, optionally split by kind-prefix.
87
+ *
88
+ * The package's only aggregation surface — domain-specific rollups
89
+ * (cost, payment totals, token usage) live in their own packages.
90
+ */
91
+ export async function countEvents(querier, args) {
92
+ const { conditions, params } = buildEventFilter(args);
93
+ if (args.since) {
94
+ params.push(args.since);
95
+ conditions.push(`created_at >= $${params.length}`);
96
+ }
97
+ if (args.until) {
98
+ params.push(args.until);
99
+ conditions.push(`created_at < $${params.length}`);
100
+ }
101
+ const prefixes = normalizePrefixes(args.byKindPrefix);
102
+ const rows = await querier(`SELECT ${buildCountSelect(prefixes)}
103
+ FROM audit_events
104
+ WHERE ${conditions.join(' AND ')}`, params);
105
+ return rowToCount(rows[0], prefixes);
106
+ }
107
+ /**
108
+ * Events per trailing month — empty months emit a zero row so the
109
+ * consumer can render a flat chart without gap handling.
110
+ */
111
+ export async function monthlyEventCounts(querier, args) {
112
+ const monthCount = Math.max(1, Math.min(args.monthCount ?? 12, 36));
113
+ const prefixes = normalizePrefixes(args.byKindPrefix);
114
+ const rows = await querier(`WITH months AS (
115
+ SELECT generate_series(
116
+ date_trunc('month', now()) - (($2::int - 1) * interval '1 month'),
117
+ date_trunc('month', now()),
118
+ interval '1 month'
119
+ ) AS month_start
120
+ )
121
+ SELECT
122
+ to_char(months.month_start, 'YYYY-MM-DD') AS "month",
123
+ ${buildCountSelect(prefixes, 'a')}
124
+ FROM months
125
+ LEFT JOIN audit_events a
126
+ ON a.tenant_id = $1
127
+ AND a.created_at >= months.month_start
128
+ AND a.created_at < months.month_start + interval '1 month'
129
+ GROUP BY months.month_start
130
+ ORDER BY months.month_start ASC`, [args.tenantId, monthCount]);
131
+ return rows.map((r) => ({ month: r.month, ...rowToCount(r, prefixes) }));
132
+ }
133
+ function buildEventFilter(args) {
134
+ const conditions = ['tenant_id = $1'];
135
+ const params = [args.tenantId];
136
+ if (args.kind !== undefined) {
137
+ params.push(args.kind);
138
+ conditions.push(`kind = $${params.length}`);
139
+ }
140
+ if (args.kindPrefix !== undefined) {
141
+ if (/[%_\\']/.test(args.kindPrefix)) {
142
+ throw new Error(`[audit/query] Invalid kindPrefix '${args.kindPrefix}' — must not contain %, _, \\ or '`);
143
+ }
144
+ params.push(`${args.kindPrefix}%`);
145
+ conditions.push(`kind LIKE $${params.length}`);
146
+ }
147
+ if (args.targetType !== undefined) {
148
+ params.push(args.targetType);
149
+ conditions.push(`target_type = $${params.length}`);
150
+ }
151
+ if (args.targetId !== undefined) {
152
+ params.push(args.targetId);
153
+ conditions.push(`target_id = $${params.length}`);
154
+ }
155
+ if (args.correlationId !== undefined) {
156
+ params.push(args.correlationId);
157
+ conditions.push(`correlation_id = $${params.length}`);
158
+ }
159
+ if (args.actorType !== undefined) {
160
+ params.push(args.actorType);
161
+ conditions.push(`actor_type = $${params.length}`);
162
+ }
163
+ if (args.status !== undefined) {
164
+ params.push(args.status);
165
+ conditions.push(`status = $${params.length}`);
166
+ }
167
+ return { conditions, params };
168
+ }
169
+ //# sourceMappingURL=query.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.js","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * `record()` — write one audit event.
3
+ *
4
+ * Idempotent when `idempotencyKey` is set: the partial unique index
5
+ * `audit_events_idempotency_unique` rejects duplicates with Postgres
6
+ * error code `23505`; we catch that and return the original row
7
+ * instead of letting the caller see a transactional error.
8
+ *
9
+ * Append-only by contract: there is no `update()` / `delete()`
10
+ * exported from this package. Finalizing a long-running event ("step
11
+ * succeeded after running for 2 min") writes a NEW row referencing
12
+ * the same `correlationId`, not an UPDATE on the original.
13
+ */
14
+ import type { AuditEvent, AuditEventInput, Querier } from './types.js';
15
+ /**
16
+ * Insert one audit event. Returns the persisted row.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * await record(query, {
21
+ * tenantId,
22
+ * actor: { type: 'user', id: cognitoSub },
23
+ * kind: 'blog.save',
24
+ * target: { type: 'blog_post', id: post.id, slug: post.slug },
25
+ * payload: { locale: 'fr', versionNumber: 4 },
26
+ * });
27
+ * ```
28
+ */
29
+ export declare function record(querier: Querier, input: AuditEventInput): Promise<AuditEvent>;
30
+ //# sourceMappingURL=record.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"record.d.ts","sourceRoot":"","sources":["../src/record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EACV,UAAU,EACV,eAAe,EACf,OAAO,EACR,MAAM,YAAY,CAAC;AAEpB;;;;;;;;;;;;;GAaG;AACH,wBAAsB,MAAM,CAC1B,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,eAAe,GACrB,OAAO,CAAC,UAAU,CAAC,CAoErB"}
package/dist/record.js ADDED
@@ -0,0 +1,136 @@
1
+ /**
2
+ * `record()` — write one audit event.
3
+ *
4
+ * Idempotent when `idempotencyKey` is set: the partial unique index
5
+ * `audit_events_idempotency_unique` rejects duplicates with Postgres
6
+ * error code `23505`; we catch that and return the original row
7
+ * instead of letting the caller see a transactional error.
8
+ *
9
+ * Append-only by contract: there is no `update()` / `delete()`
10
+ * exported from this package. Finalizing a long-running event ("step
11
+ * succeeded after running for 2 min") writes a NEW row referencing
12
+ * the same `correlationId`, not an UPDATE on the original.
13
+ */
14
+ /**
15
+ * Insert one audit event. Returns the persisted row.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * await record(query, {
20
+ * tenantId,
21
+ * actor: { type: 'user', id: cognitoSub },
22
+ * kind: 'blog.save',
23
+ * target: { type: 'blog_post', id: post.id, slug: post.slug },
24
+ * payload: { locale: 'fr', versionNumber: 4 },
25
+ * });
26
+ * ```
27
+ */
28
+ export async function record(querier, input) {
29
+ // Payload is rendered as JSON by the driver. We freeze the
30
+ // top-level shape to a plain object here so a caller-side
31
+ // `Map`/`Set`/instance leaks as `{}` rather than throwing inside
32
+ // node-postgres.
33
+ const payload = sanitizePayload(input.payload);
34
+ const sql = `
35
+ INSERT INTO audit_events (
36
+ tenant_id,
37
+ actor_type,
38
+ actor_id,
39
+ kind,
40
+ target_type,
41
+ target_id,
42
+ target_slug,
43
+ status,
44
+ payload,
45
+ correlation_id,
46
+ idempotency_key,
47
+ error_message
48
+ )
49
+ VALUES (
50
+ $1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10, $11, $12
51
+ )
52
+ RETURNING *
53
+ `;
54
+ const params = [
55
+ input.tenantId,
56
+ input.actor.type,
57
+ input.actor.id ?? null,
58
+ input.kind,
59
+ input.target?.type ?? null,
60
+ input.target?.id ?? null,
61
+ input.target?.slug ?? null,
62
+ input.status ?? 'succeeded',
63
+ JSON.stringify(payload),
64
+ input.correlationId ?? null,
65
+ input.idempotencyKey ?? null,
66
+ input.errorMessage ?? null,
67
+ ];
68
+ try {
69
+ const rows = await querier(sql, params);
70
+ const row = rows[0];
71
+ if (!row) {
72
+ // pg always returns the inserted row from RETURNING; this
73
+ // branch is defensive against custom queriers that strip it.
74
+ throw new Error('[audit/record] INSERT ... RETURNING returned no rows');
75
+ }
76
+ return row;
77
+ }
78
+ catch (err) {
79
+ if (input.idempotencyKey &&
80
+ isUniqueViolation(err) &&
81
+ isIdempotencyConstraint(err)) {
82
+ // Retried call with the same idempotency key — return the
83
+ // pre-existing row so the caller sees a stable outcome.
84
+ const existing = await querier(`SELECT * FROM audit_events WHERE idempotency_key = $1 LIMIT 1`, [input.idempotencyKey]);
85
+ if (existing[0])
86
+ return existing[0];
87
+ }
88
+ throw err;
89
+ }
90
+ }
91
+ // ─── Helpers ────────────────────────────────────────────────────────
92
+ function sanitizePayload(payload) {
93
+ if (payload === undefined || payload === null)
94
+ return {};
95
+ if (typeof payload !== 'object') {
96
+ return { value: payload };
97
+ }
98
+ // Best-effort: a plain object passes through; everything else
99
+ // (Map / Set / class instance) loses its identity but at least
100
+ // survives JSON.stringify.
101
+ try {
102
+ return JSON.parse(JSON.stringify(payload));
103
+ }
104
+ catch {
105
+ return {};
106
+ }
107
+ }
108
+ /**
109
+ * Pg unique-violation SQLSTATE. Driver-agnostic check — both
110
+ * `node-postgres` and Aurora-DSQL surface this under `err.code`.
111
+ */
112
+ function isUniqueViolation(err) {
113
+ if (!err || typeof err !== 'object')
114
+ return false;
115
+ const code = err.code;
116
+ return code === '23505';
117
+ }
118
+ /**
119
+ * Distinguish "duplicate idempotency_key" (intentional, return existing
120
+ * row) from a true 23505 on some other index — would mean a schema
121
+ * bug we shouldn't swallow. We check the constraint name via pg's
122
+ * error metadata when available; fall back to the SQL detail string
123
+ * if not.
124
+ */
125
+ function isIdempotencyConstraint(err) {
126
+ if (!err || typeof err !== 'object')
127
+ return false;
128
+ const e = err;
129
+ if (e.constraint === 'audit_events_idempotency_unique')
130
+ return true;
131
+ if (typeof e.detail === 'string' && e.detail.includes('(idempotency_key)')) {
132
+ return true;
133
+ }
134
+ return false;
135
+ }
136
+ //# sourceMappingURL=record.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"record.js","sourceRoot":"","sources":["../src/record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAQH;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,MAAM,CAC1B,OAAgB,EAChB,KAAsB;IAEtB,2DAA2D;IAC3D,0DAA0D;IAC1D,iEAAiE;IACjE,iBAAiB;IACjB,MAAM,OAAO,GAAG,eAAe,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAE/C,MAAM,GAAG,GAAG;;;;;;;;;;;;;;;;;;;GAmBX,CAAC;IAEF,MAAM,MAAM,GAAc;QACxB,KAAK,CAAC,QAAQ;QACd,KAAK,CAAC,KAAK,CAAC,IAAI;QAChB,KAAK,CAAC,KAAK,CAAC,EAAE,IAAI,IAAI;QACtB,KAAK,CAAC,IAAI;QACV,KAAK,CAAC,MAAM,EAAE,IAAI,IAAI,IAAI;QAC1B,KAAK,CAAC,MAAM,EAAE,EAAE,IAAI,IAAI;QACxB,KAAK,CAAC,MAAM,EAAE,IAAI,IAAI,IAAI;QAC1B,KAAK,CAAC,MAAM,IAAI,WAAW;QAC3B,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;QACvB,KAAK,CAAC,aAAa,IAAI,IAAI;QAC3B,KAAK,CAAC,cAAc,IAAI,IAAI;QAC5B,KAAK,CAAC,YAAY,IAAI,IAAI;KAC3B,CAAC;IAEF,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,OAAO,CAAe,GAAG,EAAE,MAAM,CAAC,CAAC;QACtD,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACpB,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,0DAA0D;YAC1D,6DAA6D;YAC7D,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC;QAC1E,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IACE,KAAK,CAAC,cAAc;YACpB,iBAAiB,CAAC,GAAG,CAAC;YACtB,uBAAuB,CAAC,GAAG,CAAC,EAC5B,CAAC;YACD,0DAA0D;YAC1D,wDAAwD;YACxD,MAAM,QAAQ,GAAG,MAAM,OAAO,CAC5B,+DAA+D,EAC/D,CAAC,KAAK,CAAC,cAAc,CAAC,CACvB,CAAC;YACF,IAAI,QAAQ,CAAC,CAAC,CAAC;gBAAE,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC;QACtC,CAAC;QACD,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC;AAED,uEAAuE;AAEvE,SAAS,eAAe,CAAC,OAAgB;IACvC,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,EAAE,CAAC;IACzD,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;QAChC,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;IAC5B,CAAC;IACD,8DAA8D;IAC9D,+DAA+D;IAC/D,2BAA2B;IAC3B,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC;IAC7C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,SAAS,iBAAiB,CAAC,GAAY;IACrC,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAClD,MAAM,IAAI,GAAI,GAAyB,CAAC,IAAI,CAAC;IAC7C,OAAO,IAAI,KAAK,OAAO,CAAC;AAC1B,CAAC;AAED;;;;;;GAMG;AACH,SAAS,uBAAuB,CAAC,GAAY;IAC3C,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAClD,MAAM,CAAC,GAAG,GAA+C,CAAC;IAC1D,IAAI,CAAC,CAAC,UAAU,KAAK,iCAAiC;QAAE,OAAO,IAAI,CAAC;IACpE,IAAI,OAAO,CAAC,CAAC,MAAM,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAC,EAAE,CAAC;QAC3E,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Retention pruning — destructive, runs as the privileged migration
3
+ * role (NOT the application role, which has DELETE revoked).
4
+ *
5
+ * Apps typically wire `pruneAuditEvents()` into a daily cron with a
6
+ * conservative `olderThanDays` (180+) and call `archiveAuditEvents()`
7
+ * BEFORE pruning so cold history lands in S3 / Glacier / whatever
8
+ * the compliance team uses.
9
+ *
10
+ * **Default retention is open-ended** — this package never auto-prunes.
11
+ * The caller picks a policy.
12
+ */
13
+ import type { Querier } from './types.js';
14
+ export interface PruneOptions {
15
+ /** Delete events older than this many days. */
16
+ olderThanDays: number;
17
+ /** Apply only within a specific tenant; null = all tenants. */
18
+ tenantId?: string | null;
19
+ /**
20
+ * Limit each invocation. Defaults to 1000 — high enough to drain
21
+ * a steady backlog within a few cron ticks without holding a long
22
+ * transaction.
23
+ */
24
+ batchSize?: number;
25
+ }
26
+ /**
27
+ * DELETE events older than the threshold. Returns the number of rows
28
+ * pruned.
29
+ *
30
+ * The caller's database role MUST have DELETE privileges on
31
+ * `audit_events`. The application role typically does NOT (that's the
32
+ * point of the migration's `REVOKE`).
33
+ */
34
+ export declare function pruneAuditEvents(querier: Querier, options: PruneOptions): Promise<number>;
35
+ /**
36
+ * Stub for archival — returns the rows that WOULD be pruned, without
37
+ * deleting. Apps stream this to S3 / Glacier / SIEM before calling
38
+ * `pruneAuditEvents()`.
39
+ *
40
+ * Pure-SQL SELECT; works as the application role (no DELETE needed).
41
+ */
42
+ export declare function previewPrune(querier: Querier, options: PruneOptions): Promise<Array<{
43
+ id: string;
44
+ createdAt: string;
45
+ tenantId: string | null;
46
+ kind: string;
47
+ }>>;
48
+ //# sourceMappingURL=retention.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retention.d.ts","sourceRoot":"","sources":["../src/retention.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,MAAM,WAAW,YAAY;IAC3B,+CAA+C;IAC/C,aAAa,EAAE,MAAM,CAAC;IACtB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,MAAM,CAAC,CA6BjB;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAChC,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC,CAoB1F"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Retention pruning — destructive, runs as the privileged migration
3
+ * role (NOT the application role, which has DELETE revoked).
4
+ *
5
+ * Apps typically wire `pruneAuditEvents()` into a daily cron with a
6
+ * conservative `olderThanDays` (180+) and call `archiveAuditEvents()`
7
+ * BEFORE pruning so cold history lands in S3 / Glacier / whatever
8
+ * the compliance team uses.
9
+ *
10
+ * **Default retention is open-ended** — this package never auto-prunes.
11
+ * The caller picks a policy.
12
+ */
13
+ /**
14
+ * DELETE events older than the threshold. Returns the number of rows
15
+ * pruned.
16
+ *
17
+ * The caller's database role MUST have DELETE privileges on
18
+ * `audit_events`. The application role typically does NOT (that's the
19
+ * point of the migration's `REVOKE`).
20
+ */
21
+ export async function pruneAuditEvents(querier, options) {
22
+ const days = Math.max(1, Math.floor(options.olderThanDays));
23
+ const batchSize = Math.max(1, Math.min(options.batchSize ?? 1000, 100_000));
24
+ const conditions = [`created_at < now() - ($1::int * interval '1 day')`];
25
+ const params = [days];
26
+ if (options.tenantId !== undefined && options.tenantId !== null) {
27
+ params.push(options.tenantId);
28
+ conditions.push(`tenant_id = $${params.length}`);
29
+ }
30
+ params.push(batchSize);
31
+ // DELETE ... WHERE id IN (SELECT … LIMIT) so a runaway prune
32
+ // doesn't blow out a single transaction on a giant table.
33
+ const sql = `
34
+ WITH victims AS (
35
+ SELECT id FROM audit_events
36
+ WHERE ${conditions.join(' AND ')}
37
+ ORDER BY created_at ASC
38
+ LIMIT $${params.length}
39
+ )
40
+ DELETE FROM audit_events
41
+ USING victims
42
+ WHERE audit_events.id = victims.id
43
+ RETURNING audit_events.id
44
+ `;
45
+ const rows = await querier(sql, params);
46
+ return rows.length;
47
+ }
48
+ /**
49
+ * Stub for archival — returns the rows that WOULD be pruned, without
50
+ * deleting. Apps stream this to S3 / Glacier / SIEM before calling
51
+ * `pruneAuditEvents()`.
52
+ *
53
+ * Pure-SQL SELECT; works as the application role (no DELETE needed).
54
+ */
55
+ export async function previewPrune(querier, options) {
56
+ const days = Math.max(1, Math.floor(options.olderThanDays));
57
+ const batchSize = Math.max(1, Math.min(options.batchSize ?? 1000, 100_000));
58
+ const conditions = [`created_at < now() - ($1::int * interval '1 day')`];
59
+ const params = [days];
60
+ if (options.tenantId !== undefined && options.tenantId !== null) {
61
+ params.push(options.tenantId);
62
+ conditions.push(`tenant_id = $${params.length}`);
63
+ }
64
+ params.push(batchSize);
65
+ return querier(`SELECT id, created_at AS "createdAt", tenant_id AS "tenantId", kind
66
+ FROM audit_events
67
+ WHERE ${conditions.join(' AND ')}
68
+ ORDER BY created_at ASC
69
+ LIMIT $${params.length}`, params);
70
+ }
71
+ //# sourceMappingURL=retention.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retention.js","sourceRoot":"","sources":["../src/retention.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAiBH;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,OAAgB,EAChB,OAAqB;IAErB,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC;IAC5D,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,SAAS,IAAI,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;IAE5E,MAAM,UAAU,GAAa,CAAC,mDAAmD,CAAC,CAAC;IACnF,MAAM,MAAM,GAAc,CAAC,IAAI,CAAC,CAAC;IACjC,IAAI,OAAO,CAAC,QAAQ,KAAK,SAAS,IAAI,OAAO,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;QAChE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC9B,UAAU,CAAC,IAAI,CAAC,gBAAgB,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;IACnD,CAAC;IACD,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAEvB,6DAA6D;IAC7D,0DAA0D;IAC1D,MAAM,GAAG,GAAG;;;eAGC,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC;;gBAEvB,MAAM,CAAC,MAAM;;;;;;GAM1B,CAAC;IAEF,MAAM,IAAI,GAAG,MAAM,OAAO,CAAwB,GAAG,EAAE,MAAM,CAAC,CAAC;IAC/D,OAAO,IAAI,CAAC,MAAM,CAAC;AACrB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,OAAgB,EAChB,OAAqB;IAErB,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC;IAC5D,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,SAAS,IAAI,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;IAE5E,MAAM,UAAU,GAAa,CAAC,mDAAmD,CAAC,CAAC;IACnF,MAAM,MAAM,GAAc,CAAC,IAAI,CAAC,CAAC;IACjC,IAAI,OAAO,CAAC,QAAQ,KAAK,SAAS,IAAI,OAAO,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;QAChE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC9B,UAAU,CAAC,IAAI,CAAC,gBAAgB,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;IACnD,CAAC;IACD,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAEvB,OAAO,OAAO,CACZ;;cAEU,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC;;eAEvB,MAAM,CAAC,MAAM,EAAE,EAC1B,MAAM,CACP,CAAC;AACJ,CAAC"}