@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.
- package/LICENSE +191 -0
- package/README.md +124 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/migrations/vk_audit_0001_init.sql +152 -0
- package/dist/path.d.ts +28 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +40 -0
- package/dist/path.js.map +1 -0
- package/dist/query.d.ts +91 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +169 -0
- package/dist/query.js.map +1 -0
- package/dist/record.d.ts +30 -0
- package/dist/record.d.ts.map +1 -0
- package/dist/record.js +136 -0
- package/dist/record.js.map +1 -0
- package/dist/retention.d.ts +48 -0
- package/dist/retention.d.ts.map +1 -0
- package/dist/retention.js +71 -0
- package/dist/retention.js.map +1 -0
- package/dist/types.d.ts +121 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/package.json +60 -0
- package/src/migrations/vk_audit_0001_init.sql +152 -0
package/dist/query.d.ts
ADDED
|
@@ -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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"}
|
package/dist/record.d.ts
ADDED
|
@@ -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":"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"}
|
|
@@ -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"}
|