@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/types.d.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit-event domain types.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `audit_events` table columns 1:1, with the caller-facing
|
|
5
|
+
* `record()` input shape derived from it. Keep these aligned with the
|
|
6
|
+
* SQL in `migrations/vk_audit_0001_init.sql`.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Actor that performed the action. Mirrors the Postgres
|
|
10
|
+
* `vk_audit_actor_type` enum.
|
|
11
|
+
*
|
|
12
|
+
* user — end-user via session token (Cognito sub, etc).
|
|
13
|
+
* service — internal service identity (workflow runner, worker).
|
|
14
|
+
* cron — scheduled-job Lambda.
|
|
15
|
+
* api_key — programmatic-access key (the actor id carries the
|
|
16
|
+
* key prefix; never the raw key).
|
|
17
|
+
* system — bootstrap / migration / cross-tenant maintenance.
|
|
18
|
+
*/
|
|
19
|
+
export type AuditActorType = 'user' | 'service' | 'cron' | 'api_key' | 'system';
|
|
20
|
+
/**
|
|
21
|
+
* Outcome of the audited action. Mirrors the Postgres
|
|
22
|
+
* `vk_audit_status` enum and matches `venturekit-cms`'s legacy
|
|
23
|
+
* `job_status` shape so apps porting from `jobs_log` get a 1:1
|
|
24
|
+
* mapping.
|
|
25
|
+
*/
|
|
26
|
+
export type AuditStatus = 'queued' | 'running' | 'succeeded' | 'failed' | 'skipped';
|
|
27
|
+
/**
|
|
28
|
+
* Compound identity of the actor.
|
|
29
|
+
*/
|
|
30
|
+
export interface AuditActor {
|
|
31
|
+
type: AuditActorType;
|
|
32
|
+
/**
|
|
33
|
+
* Free-form id. User → auth sub. Service → service name. api_key →
|
|
34
|
+
* key prefix. cron → schedule rule name. Optional for system / bootstrap.
|
|
35
|
+
*/
|
|
36
|
+
id?: string | null;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The thing the action was done to. Optional — events like
|
|
40
|
+
* `cron.tick` don't target a specific row.
|
|
41
|
+
*/
|
|
42
|
+
export interface AuditTarget {
|
|
43
|
+
/** Logical type (e.g. `'blog_post'`, `'social_post'`, `'editorial_run'`). */
|
|
44
|
+
type: string;
|
|
45
|
+
/** Primary key of the target row. */
|
|
46
|
+
id: string;
|
|
47
|
+
/** Optional secondary identifier for human-readable refs (slug, label). */
|
|
48
|
+
slug?: string | null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Caller-facing input to `record()`. Strictly typed on the in-args
|
|
52
|
+
* the package guarantees to preserve; opaque `payload` carries
|
|
53
|
+
* whatever else the event-kind needs.
|
|
54
|
+
*
|
|
55
|
+
* Strictly audit-scoped: WHO did WHAT, TO WHAT, WHEN, HOW IT WENT.
|
|
56
|
+
* Domain-specific fields (cost, tokens, payment amounts, …) live
|
|
57
|
+
* in their own packages' tables and reference this one via
|
|
58
|
+
* `correlationId`.
|
|
59
|
+
*/
|
|
60
|
+
export interface AuditEventInput {
|
|
61
|
+
/** Tenant the event belongs to. Pass `null` only for platform-wide events. */
|
|
62
|
+
tenantId: string | null;
|
|
63
|
+
/** Who did the thing. */
|
|
64
|
+
actor: AuditActor;
|
|
65
|
+
/** Free-form, namespaced event kind (e.g. `'blog.save'`). */
|
|
66
|
+
kind: string;
|
|
67
|
+
/** Optional target row. */
|
|
68
|
+
target?: AuditTarget;
|
|
69
|
+
/** Outcome. Defaults to `'succeeded'`. */
|
|
70
|
+
status?: AuditStatus;
|
|
71
|
+
/** Free-form details. Caller's responsibility to keep small + PII-free. */
|
|
72
|
+
payload?: Record<string, unknown>;
|
|
73
|
+
/** Threads multiple events into one logical operation. */
|
|
74
|
+
correlationId?: string | null;
|
|
75
|
+
/** Optional idempotency token for safe retries. */
|
|
76
|
+
idempotencyKey?: string | null;
|
|
77
|
+
/** Failure summary when `status === 'failed'`. */
|
|
78
|
+
errorMessage?: string | null;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Persisted row shape. Mirrors `audit_events` columns post-mapping
|
|
82
|
+
* (snake_case → camelCase). Returned by `record()` and the query
|
|
83
|
+
* helpers.
|
|
84
|
+
*/
|
|
85
|
+
export interface AuditEvent {
|
|
86
|
+
id: string;
|
|
87
|
+
tenantId: string | null;
|
|
88
|
+
actorType: AuditActorType;
|
|
89
|
+
actorId: string | null;
|
|
90
|
+
kind: string;
|
|
91
|
+
targetType: string | null;
|
|
92
|
+
targetId: string | null;
|
|
93
|
+
targetSlug: string | null;
|
|
94
|
+
status: AuditStatus;
|
|
95
|
+
payload: Record<string, unknown>;
|
|
96
|
+
correlationId: string | null;
|
|
97
|
+
idempotencyKey: string | null;
|
|
98
|
+
errorMessage: string | null;
|
|
99
|
+
createdAt: string;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Minimal querier shape — a function that runs a parameterized SQL
|
|
103
|
+
* statement and returns rows. Matches `@venturekit/data`'s `Querier`
|
|
104
|
+
* type so consumers can pass `query` directly. Kept structural so
|
|
105
|
+
* apps using a different Postgres driver can still inject their own
|
|
106
|
+
* adapter.
|
|
107
|
+
*/
|
|
108
|
+
export type Querier = <T = Record<string, unknown>[]>(sql: string, params?: unknown[]) => Promise<T>;
|
|
109
|
+
/**
|
|
110
|
+
* Counters returned by `countEvents()` / `countEventsByKind()`. The
|
|
111
|
+
* audit package's only aggregation surface — anything more domain-
|
|
112
|
+
* specific (cost rollups, payment totals, token usage) lives in the
|
|
113
|
+
* package that owns those concerns.
|
|
114
|
+
*/
|
|
115
|
+
export interface EventCount {
|
|
116
|
+
/** Number of events matching the filter. */
|
|
117
|
+
count: number;
|
|
118
|
+
/** Per-prefix split when caller passes `byKindPrefix`. */
|
|
119
|
+
byKindPrefix: Record<string, number>;
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;GAUG;AACH,MAAM,MAAM,cAAc,GACtB,MAAM,GACN,SAAS,GACT,MAAM,GACN,SAAS,GACT,QAAQ,CAAC;AAEb;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEpF;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,cAAc,CAAC;IACrB;;;OAGG;IACH,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB;AAED;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb,qCAAqC;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,eAAe;IAC9B,8EAA8E;IAC9E,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,yBAAyB;IACzB,KAAK,EAAE,UAAU,CAAC;IAClB,6DAA6D;IAC7D,IAAI,EAAE,MAAM,CAAC;IACb,2BAA2B;IAC3B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,0CAA0C;IAC1C,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,2EAA2E;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAGlC,0DAA0D;IAC1D,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,mDAAmD;IACnD,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,kDAAkD;IAClD,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,cAAc,CAAC;IAC1B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,MAAM,EAAE,WAAW,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAClD,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,OAAO,EAAE,KACf,OAAO,CAAC,CAAC,CAAC,CAAC;AAEhB;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,0DAA0D;IAC1D,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit-event domain types.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `audit_events` table columns 1:1, with the caller-facing
|
|
5
|
+
* `record()` input shape derived from it. Keep these aligned with the
|
|
6
|
+
* SQL in `migrations/vk_audit_0001_init.sql`.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@venturekit-pro/audit",
|
|
3
|
+
"version": "0.0.0-dev.20260602192622",
|
|
4
|
+
"description": "Append-only audit + cost-tracking log for VentureKit applications",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"src/migrations/*.sql"
|
|
11
|
+
],
|
|
12
|
+
"vk": {
|
|
13
|
+
"migrations": "src/migrations"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/venturekit-dev/venturekit.private.git",
|
|
18
|
+
"directory": "packages/pro/audit"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"registry": "https://registry.npmjs.org",
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"license": "SEE LICENSE IN ../LICENSE",
|
|
25
|
+
"licenseHeader": "VentureKit Pro Commercial License — production use requires a valid VentureKit Pro license key. See https://venturekit.dev/pricing.",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"import": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"venturekit",
|
|
34
|
+
"saas",
|
|
35
|
+
"audit",
|
|
36
|
+
"cost-tracking",
|
|
37
|
+
"compliance"
|
|
38
|
+
],
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@venturekit/core": "0.0.0-dev.20260602192622"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@venturekit/data": "0.0.0-dev.20260602192622"
|
|
44
|
+
},
|
|
45
|
+
"peerDependenciesMeta": {
|
|
46
|
+
"@venturekit/data": {
|
|
47
|
+
"optional": true
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^25.6.0",
|
|
52
|
+
"@venturekit/data": "0.0.0-dev.20260602192622",
|
|
53
|
+
"typescript": "^5.3.0"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "tsc && node -e \"require('fs').cpSync('src/migrations','dist/migrations',{recursive:true})\"",
|
|
57
|
+
"dev": "tsc --watch",
|
|
58
|
+
"clean": "rm -rf dist"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
-- @venturekit-pro/audit — initial schema.
|
|
2
|
+
--
|
|
3
|
+
-- One canonical, append-only `audit_events` table. Stores WHO did
|
|
4
|
+
-- WHAT, TO WHAT, WHEN, and HOW IT WENT. Nothing else.
|
|
5
|
+
--
|
|
6
|
+
-- **Strict scope.** The audit package does not know about LLM costs,
|
|
7
|
+
-- token counts, image generation, payment amounts, or any other
|
|
8
|
+
-- domain concern. Those live in their own packages / tables
|
|
9
|
+
-- (e.g. `@venturekit-pro/ai`'s `llm_cost_events`); audit events
|
|
10
|
+
-- merely reference them by `target_type` / `target_id` /
|
|
11
|
+
-- `correlation_id`.
|
|
12
|
+
--
|
|
13
|
+
-- Append-only by construction:
|
|
14
|
+
-- - No UPDATE / DELETE statements are issued by the package code.
|
|
15
|
+
-- - DB-level REVOKE on `audit_events` from the application role
|
|
16
|
+
-- at the bottom of this migration enforces it physically.
|
|
17
|
+
-- Privileged maintenance (retention pruning) runs as a
|
|
18
|
+
-- separate role.
|
|
19
|
+
--
|
|
20
|
+
-- Idempotency:
|
|
21
|
+
-- - `idempotency_key` is OPTIONAL on insert. When set, the
|
|
22
|
+
-- partial unique index makes a re-insert a no-op (caller
|
|
23
|
+
-- catches the duplicate-key error in `record()` and returns
|
|
24
|
+
-- the original row).
|
|
25
|
+
|
|
26
|
+
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
27
|
+
|
|
28
|
+
-- ─── Status enum ────────────────────────────────────────────────────
|
|
29
|
+
--
|
|
30
|
+
-- Outcome of an audited action. Mirrors the standard `job_status`
|
|
31
|
+
-- shape so callers can map 1:1.
|
|
32
|
+
|
|
33
|
+
DO $$
|
|
34
|
+
BEGIN
|
|
35
|
+
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'vk_audit_status') THEN
|
|
36
|
+
CREATE TYPE vk_audit_status AS ENUM (
|
|
37
|
+
'queued',
|
|
38
|
+
'running',
|
|
39
|
+
'succeeded',
|
|
40
|
+
'failed',
|
|
41
|
+
'skipped'
|
|
42
|
+
);
|
|
43
|
+
END IF;
|
|
44
|
+
END
|
|
45
|
+
$$;
|
|
46
|
+
|
|
47
|
+
DO $$
|
|
48
|
+
BEGIN
|
|
49
|
+
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'vk_audit_actor_type') THEN
|
|
50
|
+
CREATE TYPE vk_audit_actor_type AS ENUM (
|
|
51
|
+
'user', -- end-user via session token
|
|
52
|
+
'service', -- internal service identity
|
|
53
|
+
'cron', -- scheduled-job Lambda
|
|
54
|
+
'api_key', -- programmatic-access key
|
|
55
|
+
'system' -- bootstrap / migration / cross-tenant maintenance
|
|
56
|
+
);
|
|
57
|
+
END IF;
|
|
58
|
+
END
|
|
59
|
+
$$;
|
|
60
|
+
|
|
61
|
+
-- ─── audit_events ───────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
CREATE TABLE IF NOT EXISTS audit_events (
|
|
64
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
65
|
+
-- tenant_id is nullable so we can record platform-wide events
|
|
66
|
+
-- (e.g. cron sweeps that fan out across every tenant). Per-tenant
|
|
67
|
+
-- views filter on `tenant_id = $1`; the rare cross-tenant rows
|
|
68
|
+
-- show up only when querying with `tenant_id IS NULL`.
|
|
69
|
+
tenant_id uuid,
|
|
70
|
+
-- WHO did the thing.
|
|
71
|
+
actor_type vk_audit_actor_type NOT NULL,
|
|
72
|
+
-- Free-form actor id. For users: their auth id (e.g. Cognito sub).
|
|
73
|
+
-- For services: service name. For api_key: key prefix. For cron:
|
|
74
|
+
-- schedule rule name. NULL only for bootstrap / system rows.
|
|
75
|
+
actor_id text,
|
|
76
|
+
-- WHAT happened. Free-form, app-namespaced (e.g. `'blog.save'`,
|
|
77
|
+
-- `'order.refunded'`, `'security.login'`). The package never
|
|
78
|
+
-- enumerates these; the calling app owns its kind taxonomy.
|
|
79
|
+
kind text NOT NULL,
|
|
80
|
+
-- WHAT it was done to (optional). target_type + target_id together
|
|
81
|
+
-- identify the row this event mutated or read.
|
|
82
|
+
target_type text,
|
|
83
|
+
target_id text,
|
|
84
|
+
-- Secondary identifier for human-readable references (slug, label).
|
|
85
|
+
target_slug text,
|
|
86
|
+
-- Outcome of the action. Finalized rows are NEVER UPDATE'd; long
|
|
87
|
+
-- operations write a `running` row, then a separate `succeeded` /
|
|
88
|
+
-- `failed` / `skipped` row with the same `correlation_id`.
|
|
89
|
+
status vk_audit_status NOT NULL DEFAULT 'succeeded',
|
|
90
|
+
-- Free-form payload. Caller's responsibility to keep this small
|
|
91
|
+
-- (~< 32KB) and PII-free. Heavy artifacts go in S3 + are
|
|
92
|
+
-- referenced by URL/key.
|
|
93
|
+
payload jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
94
|
+
|
|
95
|
+
-- ─── Cross-event correlation ─────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
-- Threads multiple events into one logical operation (a workflow
|
|
98
|
+
-- run, an HTTP request, a saga).
|
|
99
|
+
correlation_id text,
|
|
100
|
+
-- Optional idempotency token; supports safe retry of e.g. cron
|
|
101
|
+
-- ticks. NULL is allowed and skips the dedup check.
|
|
102
|
+
idempotency_key text,
|
|
103
|
+
-- Human-readable failure summary when `status = 'failed'`.
|
|
104
|
+
error_message text,
|
|
105
|
+
|
|
106
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
-- ─── Indexes ────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
-- Main page query: "show me the last N events for this tenant".
|
|
112
|
+
CREATE INDEX IF NOT EXISTS audit_events_tenant_created_idx
|
|
113
|
+
ON audit_events (tenant_id, created_at DESC);
|
|
114
|
+
|
|
115
|
+
-- Filtered list: "show me the last N 'order.refunded' events".
|
|
116
|
+
CREATE INDEX IF NOT EXISTS audit_events_tenant_kind_idx
|
|
117
|
+
ON audit_events (tenant_id, kind, created_at DESC);
|
|
118
|
+
|
|
119
|
+
-- Per-entity history: "every event touching blog_post X".
|
|
120
|
+
CREATE INDEX IF NOT EXISTS audit_events_target_idx
|
|
121
|
+
ON audit_events (tenant_id, target_type, target_id, created_at DESC);
|
|
122
|
+
|
|
123
|
+
-- Correlation join: "every event in this workflow run".
|
|
124
|
+
CREATE INDEX IF NOT EXISTS audit_events_correlation_idx
|
|
125
|
+
ON audit_events (correlation_id, created_at DESC)
|
|
126
|
+
WHERE correlation_id IS NOT NULL;
|
|
127
|
+
|
|
128
|
+
-- Idempotency dedup. Partial because the vast majority of rows
|
|
129
|
+
-- have idempotency_key = NULL.
|
|
130
|
+
CREATE UNIQUE INDEX IF NOT EXISTS audit_events_idempotency_unique
|
|
131
|
+
ON audit_events (idempotency_key)
|
|
132
|
+
WHERE idempotency_key IS NOT NULL;
|
|
133
|
+
|
|
134
|
+
-- ─── Append-only enforcement at the storage layer ───────────────────
|
|
135
|
+
--
|
|
136
|
+
-- The application role can INSERT and SELECT but never UPDATE /
|
|
137
|
+
-- DELETE. A privileged maintenance role (whatever the app uses to
|
|
138
|
+
-- run migrations) retains full privileges, and is what retention
|
|
139
|
+
-- sweeps run as.
|
|
140
|
+
--
|
|
141
|
+
-- We don't know the application role's name at migration time — apps
|
|
142
|
+
-- vary (`venturekit_cms_app`, `myapp_runtime`, …). The REVOKE here
|
|
143
|
+
-- targets `PUBLIC` which covers the default no-role-set case AND
|
|
144
|
+
-- means the application role inherits the lockdown unless explicitly
|
|
145
|
+
-- granted UPDATE/DELETE. Apps that grant their runtime role
|
|
146
|
+
-- `ALL PRIVILEGES` on the schema must re-revoke after this migration.
|
|
147
|
+
|
|
148
|
+
REVOKE UPDATE, DELETE ON audit_events FROM PUBLIC;
|
|
149
|
+
|
|
150
|
+
-- A convenience GRANT so the app role still works for inserts and
|
|
151
|
+
-- reads when only PUBLIC's privileges were revoked.
|
|
152
|
+
GRANT SELECT, INSERT ON audit_events TO PUBLIC;
|