@wtfalch/audit 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -70,6 +70,40 @@ const touched = await ledger.erase(tx, { subject: personId, pseudonym, email });
70
70
  uses: `tenant.invoice_paid` is out, `invoice.paid` is in. The core's
71
71
  namespaces are the trusted base's.
72
72
 
73
+ ## A writer for an embedding service
74
+
75
+ A service that wants a trail (a forum's moderation, a mail admin's writes) gets
76
+ a writer, never the signer. The host binds one per namespace:
77
+
78
+ ```ts
79
+ export const forumAudit = ledger.writer({ namespace: 'thread', handle: db });
80
+ createThreads({ db, gates, audit: forumAudit });
81
+ ```
82
+
83
+ The writer signs `thread.*` and refuses everything else; the actor is whoever
84
+ the service resolved; context, actor class and tenant default to what the
85
+ host bound. A caller passes its transaction as the second argument when the
86
+ row must commit with the change it records.
87
+
88
+ ## A host's own columns
89
+
90
+ A host that needs a column beside the ledger's (a team, a region) declares
91
+ its table over the package's builders and hands it to the ledger:
92
+
93
+ ```ts
94
+ import { AUDIT_COLUMNS, auditIndexes, createLedger } from '@wtfalch/audit';
95
+
96
+ export const auditEvents = pgTable('audit_events', { ...AUDIT_COLUMNS, teamId: uuid('team_id') }, (t) =>
97
+ auditIndexes(t),
98
+ );
99
+ const ledger = createLedger({ vocabulary, table: auditEvents, schemaVersion: core.version });
100
+ await ledger.sign(tx, { ...input, extra: { teamId } });
101
+ ```
102
+
103
+ `extra` takes host columns only: a ledger column there is refused, and so is a
104
+ key the table does not declare. The host's own migration adds the column; the
105
+ package's migration never learns of it.
106
+
73
107
  ## Tests
74
108
 
75
109
  ```sh
package/dist/ledger.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { PgDatabase, PgQueryResultHKT } from 'drizzle-orm/pg-core';
2
- import { type AuditEventRow, auditEvents } from './tables.js';
2
+ import { type AuditEventRow, type AuditTable } from './tables.js';
3
3
  import type { LedgerVocabulary } from './vocabulary.js';
4
4
  /**
5
5
  * The host's drizzle handle or a transaction open on it. postgres-js in the
@@ -42,6 +42,12 @@ export interface SignInput {
42
42
  readonly id: string;
43
43
  } | null;
44
44
  readonly occurredAt?: Date;
45
+ /**
46
+ * Values for the host's own columns, by the drizzle key the host's table
47
+ * declares them under (`{ teamId }`), written beside the ledger's. Never a
48
+ * ledger column: those are validated above and refused here.
49
+ */
50
+ readonly extra?: Readonly<Record<string, unknown>>;
45
51
  }
46
52
  export interface PageOptions {
47
53
  /** One tenant's rows; `null` for rows with no tenant; omit for every tenant. */
@@ -65,6 +71,55 @@ export interface Page {
65
71
  readonly id: number;
66
72
  } | null;
67
73
  }
74
+ /**
75
+ * What an embedding service hands a bound writer for one row. The namespace
76
+ * is fixed when the host binds the writer, so `action` must sit in it; the
77
+ * actor is whoever the service resolved (a forum's `who`, a mail admin's
78
+ * principal), never chosen by the ledger. Everything else is optional and
79
+ * defaults as `SignInput` does.
80
+ */
81
+ export interface WriterEvent {
82
+ readonly action: string;
83
+ readonly actor: {
84
+ readonly id: string;
85
+ readonly display: string;
86
+ readonly class?: string;
87
+ };
88
+ readonly target: {
89
+ readonly type: string;
90
+ readonly id: string;
91
+ };
92
+ readonly tenantId?: string | null;
93
+ readonly outcome?: string;
94
+ readonly reason?: string | null;
95
+ readonly reference?: string | null;
96
+ readonly before?: unknown;
97
+ readonly after?: unknown;
98
+ readonly subject?: {
99
+ readonly class: string;
100
+ readonly id: string;
101
+ } | null;
102
+ readonly request?: SignInput['request'];
103
+ readonly extra?: SignInput['extra'];
104
+ }
105
+ /**
106
+ * A writer the host bound to one namespace. `handle` is the caller's
107
+ * transaction when the row must commit with the change it records;
108
+ * otherwise the handle the host bound.
109
+ */
110
+ export type AuditWriter = (event: WriterEvent, handle?: Handle) => Promise<void>;
111
+ export interface WriterOptions {
112
+ /** The namespace the writer may sign in: `thread` admits `thread.pinned` and refuses `tenant.created`. */
113
+ readonly namespace: string;
114
+ /** The handle used when the caller passes none. */
115
+ readonly handle: Handle;
116
+ /** The context every row carries. Defaults to the vocabulary's first context. */
117
+ readonly context?: string;
118
+ /** The actor class when the event names none. Defaults to the vocabulary's first actor class. */
119
+ readonly actorClass?: string;
120
+ /** The tenant when the event names none. Defaults to null: a row on the estate log. */
121
+ readonly tenantId?: string | null;
122
+ }
68
123
  export interface EraseInput {
69
124
  readonly subject: string;
70
125
  readonly pseudonym: string;
@@ -79,10 +134,17 @@ export interface EraseInput {
79
134
  * or reads a session: the guarantee that a row's actor is real is the host's,
80
135
  * and it holds only while the signer stays private.
81
136
  */
137
+ export interface LedgerOptions {
138
+ readonly vocabulary: LedgerVocabulary;
139
+ /** The host's own table over `AUDIT_COLUMNS`, when it has columns beside the ledger's. Defaults to the package's `auditEvents`. */
140
+ readonly table?: AuditTable;
141
+ /** What `schema_version` every row carries. Defaults to 1; a host whose shared audit words are versioned passes theirs. */
142
+ readonly schemaVersion?: number;
143
+ }
82
144
  export interface Ledger {
83
145
  readonly vocabulary: LedgerVocabulary;
84
146
  readonly tables: {
85
- readonly events: typeof auditEvents;
147
+ readonly events: AuditTable;
86
148
  };
87
149
  /** Validates against the vocabulary and inserts, on the handle given: a transaction when the row must commit with the change it records. */
88
150
  sign(handle: Handle, input: SignInput): Promise<void>;
@@ -92,7 +154,11 @@ export interface Ledger {
92
154
  erase(handle: Handle, input: EraseInput): Promise<number>;
93
155
  /** One tenant's rows, oldest first, for a tenant's export. Deterministic order; no secrets are in this table to omit. */
94
156
  exportRows(handle: Handle, tenantId: string): Promise<readonly AuditEventRow[]>;
157
+ /**
158
+ * A writer for one embedding service, bound to one namespace. This is what
159
+ * a host hands to `createThreads({ audit })` or a postmaster: it can write
160
+ * that namespace's events and nothing else, and it never sees `sign`.
161
+ */
162
+ writer(options: WriterOptions): AuditWriter;
95
163
  }
96
- export declare function createLedger(options: {
97
- vocabulary: LedgerVocabulary;
98
- }): Ledger;
164
+ export declare function createLedger(options: LedgerOptions): Ledger;
package/dist/ledger.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { and, asc, desc, eq, lt, or, sql } from 'drizzle-orm';
2
2
  import { rowSchema } from './schema.js';
3
- import { auditEvents } from './tables.js';
3
+ import { auditEvents as auditEvents_ } from './tables.js';
4
+ const LEDGER_KEYS = new Set(Object.keys(auditEvents_));
4
5
  export function createLedger(options) {
5
- const { vocabulary } = options;
6
- const schema = rowSchema(vocabulary);
6
+ const { vocabulary, table: auditEvents = auditEvents_, schemaVersion = 1 } = options;
7
+ const schema = rowSchema(vocabulary, { schemaVersion });
7
8
  async function sign(handle, input) {
8
9
  const meta = vocabulary.events[input.action];
9
10
  if (!meta) {
@@ -31,11 +32,19 @@ export function createLedger(options) {
31
32
  before: input.before ?? null,
32
33
  after: input.after ?? null,
33
34
  erased_at: null,
34
- schema_version: 1,
35
+ schema_version: schemaVersion,
35
36
  subject_class: input.subject?.class ?? null,
36
37
  subject_id: input.subject?.id ?? null,
37
38
  });
39
+ const extra = input.extra ?? {};
40
+ for (const key of Object.keys(extra)) {
41
+ if (LEDGER_KEYS.has(key))
42
+ throw new Error(`audit: "${key}" is a ledger column, not a host column`);
43
+ if (!(key in auditEvents))
44
+ throw new Error(`audit: the ledger's table has no column "${key}"`);
45
+ }
38
46
  await handle.insert(auditEvents).values({
47
+ ...extra,
39
48
  occurredAt,
40
49
  tenantId: row.tenant_id,
41
50
  actorClass: row.actor_class,
@@ -110,5 +119,40 @@ export function createLedger(options) {
110
119
  .where(eq(auditEvents.tenantId, tenantId))
111
120
  .orderBy(asc(auditEvents.occurredAt), asc(auditEvents.id));
112
121
  }
113
- return { vocabulary, tables: { events: auditEvents }, sign, page, erase, exportRows };
122
+ function writer(options) {
123
+ const { namespace } = options;
124
+ if (!/^[a-z][a-z0-9_]*$/.test(namespace)) {
125
+ throw new Error(`audit: "${namespace}" is not a namespace`);
126
+ }
127
+ const context = options.context ?? vocabulary.contexts[0];
128
+ const actorClass = options.actorClass ?? vocabulary.actorClasses[0];
129
+ if (context === undefined || actorClass === undefined) {
130
+ throw new Error('audit: the vocabulary has no context or actor class to default to');
131
+ }
132
+ return async (event, handle) => {
133
+ if (!event.action.startsWith(`${namespace}.`)) {
134
+ throw new Error(`audit: "${event.action}" is outside the namespace "${namespace}" this writer is bound to`);
135
+ }
136
+ await sign(handle ?? options.handle, {
137
+ action: event.action,
138
+ tenantId: event.tenantId === undefined ? (options.tenantId ?? null) : event.tenantId,
139
+ actor: {
140
+ class: event.actor.class ?? actorClass,
141
+ id: event.actor.id,
142
+ display: event.actor.display,
143
+ },
144
+ context,
145
+ target: event.target,
146
+ outcome: event.outcome,
147
+ reason: event.reason,
148
+ reference: event.reference,
149
+ before: event.before,
150
+ after: event.after,
151
+ subject: event.subject,
152
+ request: event.request,
153
+ extra: event.extra,
154
+ });
155
+ };
156
+ }
157
+ return { vocabulary, tables: { events: auditEvents }, sign, page, erase, exportRows, writer };
114
158
  }
package/dist/schema.d.ts CHANGED
@@ -20,7 +20,9 @@ export declare function isJsonValue(value: unknown, seen?: WeakSet<object>): boo
20
20
  * the break-glass context must carry the session, a reason from the closed
21
21
  * code set and a reference; every other row's reason is free text.
22
22
  */
23
- export declare function rowSchema(vocabulary: LedgerVocabulary): z.ZodObject<{
23
+ export declare function rowSchema(vocabulary: LedgerVocabulary, options?: {
24
+ schemaVersion?: number;
25
+ }): z.ZodObject<{
24
26
  occurred_at: z.ZodISODateTime;
25
27
  tenant_id: z.ZodNullable<z.ZodString>;
26
28
  actor_class: z.ZodEnum<{
@@ -49,7 +51,7 @@ export declare function rowSchema(vocabulary: LedgerVocabulary): z.ZodObject<{
49
51
  before: z.ZodNullable<z.ZodUnknown>;
50
52
  after: z.ZodNullable<z.ZodUnknown>;
51
53
  erased_at: z.ZodNullable<z.ZodISODateTime>;
52
- schema_version: z.ZodLiteral<1>;
54
+ schema_version: z.ZodLiteral<number>;
53
55
  subject_class: z.ZodNullable<z.ZodEnum<{
54
56
  [x: string]: string;
55
57
  }>>;
package/dist/schema.js CHANGED
@@ -60,7 +60,8 @@ const enumOf = (values) => z.enum([...values]);
60
60
  * the break-glass context must carry the session, a reason from the closed
61
61
  * code set and a reference; every other row's reason is free text.
62
62
  */
63
- export function rowSchema(vocabulary) {
63
+ export function rowSchema(vocabulary, options = {}) {
64
+ const schemaVersion = options.schemaVersion ?? 1;
64
65
  const eventNames = Object.keys(vocabulary.events);
65
66
  const reasonCodes = new Set(vocabulary.breakGlassReasonCodes ?? []);
66
67
  return z
@@ -85,7 +86,7 @@ export function rowSchema(vocabulary) {
85
86
  before: jsonColumn.nullable(),
86
87
  after: jsonColumn.nullable(),
87
88
  erased_at: z.iso.datetime({ offset: true }).nullable(),
88
- schema_version: z.literal(1),
89
+ schema_version: z.literal(schemaVersion),
89
90
  subject_class: enumOf(vocabulary.actorClasses).nullable(),
90
91
  subject_id: identifier(AUDIT_LIMITS.id).nullable(),
91
92
  })
package/dist/tables.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type ExtraConfigColumn } from 'drizzle-orm/pg-core';
1
2
  /**
2
3
  * The ledger, mirrored from `migrations/0001_audit.sql`, which is the source
3
4
  * of truth: the SQL carries the CHECKs, the append-only triggers, the partial
@@ -5,6 +6,162 @@
5
6
  * re-exports this from its own schema module so its drizzle instance and its
6
7
  * types know it.
7
8
  */
9
+ /**
10
+ * The ledger's columns, as drizzle builders, so a host that needs a column
11
+ * of its own beside them (a team, a region) declares
12
+ * `pgTable('audit_events', { ...AUDIT_COLUMNS, teamId: uuid('team_id') }, …)`
13
+ * and hands that table to `createLedger`. The package's own `auditEvents`
14
+ * is the same call with nothing added.
15
+ */
16
+ export declare const AUDIT_COLUMNS: {
17
+ id: import("drizzle-orm").IsPrimaryKey<import("drizzle-orm").NotNull<import("drizzle-orm").IsIdentity<import("drizzle-orm/pg-core").PgBigInt53BuilderInitial<"id">, "always">>>;
18
+ occurredAt: import("drizzle-orm").HasDefault<import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTimestampBuilderInitial<"occurred_at">>>;
19
+ tenantId: import("drizzle-orm/pg-core").PgUUIDBuilderInitial<"tenant_id">;
20
+ actorClass: import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTextBuilder<{
21
+ name: "actor_class";
22
+ dataType: "string";
23
+ columnType: "PgText";
24
+ data: string;
25
+ enumValues: [string, ...string[]];
26
+ driverParam: string;
27
+ }>>;
28
+ actorId: import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTextBuilder<{
29
+ name: "actor_id";
30
+ dataType: "string";
31
+ columnType: "PgText";
32
+ data: string;
33
+ enumValues: [string, ...string[]];
34
+ driverParam: string;
35
+ }>>;
36
+ actorDisplay: import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTextBuilder<{
37
+ name: "actor_display";
38
+ dataType: "string";
39
+ columnType: "PgText";
40
+ data: string;
41
+ enumValues: [string, ...string[]];
42
+ driverParam: string;
43
+ }>>;
44
+ action: import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTextBuilder<{
45
+ name: "action";
46
+ dataType: "string";
47
+ columnType: "PgText";
48
+ data: string;
49
+ enumValues: [string, ...string[]];
50
+ driverParam: string;
51
+ }>>;
52
+ targetType: import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTextBuilder<{
53
+ name: "target_type";
54
+ dataType: "string";
55
+ columnType: "PgText";
56
+ data: string;
57
+ enumValues: [string, ...string[]];
58
+ driverParam: string;
59
+ }>>;
60
+ targetId: import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTextBuilder<{
61
+ name: "target_id";
62
+ dataType: "string";
63
+ columnType: "PgText";
64
+ data: string;
65
+ enumValues: [string, ...string[]];
66
+ driverParam: string;
67
+ }>>;
68
+ outcome: import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTextBuilder<{
69
+ name: "outcome";
70
+ dataType: "string";
71
+ columnType: "PgText";
72
+ data: string;
73
+ enumValues: [string, ...string[]];
74
+ driverParam: string;
75
+ }>>;
76
+ context: import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTextBuilder<{
77
+ name: "context";
78
+ dataType: "string";
79
+ columnType: "PgText";
80
+ data: string;
81
+ enumValues: [string, ...string[]];
82
+ driverParam: string;
83
+ }>>;
84
+ sessionId: import("drizzle-orm/pg-core").PgTextBuilder<{
85
+ name: "session_id";
86
+ dataType: "string";
87
+ columnType: "PgText";
88
+ data: string;
89
+ enumValues: [string, ...string[]];
90
+ driverParam: string;
91
+ }>;
92
+ reason: import("drizzle-orm/pg-core").PgTextBuilder<{
93
+ name: "reason";
94
+ dataType: "string";
95
+ columnType: "PgText";
96
+ data: string;
97
+ enumValues: [string, ...string[]];
98
+ driverParam: string;
99
+ }>;
100
+ reference: import("drizzle-orm/pg-core").PgTextBuilder<{
101
+ name: "reference";
102
+ dataType: "string";
103
+ columnType: "PgText";
104
+ data: string;
105
+ enumValues: [string, ...string[]];
106
+ driverParam: string;
107
+ }>;
108
+ requestId: import("drizzle-orm/pg-core").PgTextBuilder<{
109
+ name: "request_id";
110
+ dataType: "string";
111
+ columnType: "PgText";
112
+ data: string;
113
+ enumValues: [string, ...string[]];
114
+ driverParam: string;
115
+ }>;
116
+ ip: import("drizzle-orm/pg-core").PgTextBuilder<{
117
+ name: "ip";
118
+ dataType: "string";
119
+ columnType: "PgText";
120
+ data: string;
121
+ enumValues: [string, ...string[]];
122
+ driverParam: string;
123
+ }>;
124
+ userAgent: import("drizzle-orm/pg-core").PgTextBuilder<{
125
+ name: "user_agent";
126
+ dataType: "string";
127
+ columnType: "PgText";
128
+ data: string;
129
+ enumValues: [string, ...string[]];
130
+ driverParam: string;
131
+ }>;
132
+ tenantVisible: import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgBooleanBuilderInitial<"tenant_visible">>;
133
+ before: import("drizzle-orm/pg-core").PgJsonbBuilderInitial<"before">;
134
+ after: import("drizzle-orm/pg-core").PgJsonbBuilderInitial<"after">;
135
+ erasedAt: import("drizzle-orm/pg-core").PgTimestampBuilderInitial<"erased_at">;
136
+ schemaVersion: import("drizzle-orm").HasDefault<import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgSmallIntBuilderInitial<"schema_version">>>;
137
+ subjectClass: import("drizzle-orm/pg-core").PgTextBuilder<{
138
+ name: "subject_class";
139
+ dataType: "string";
140
+ columnType: "PgText";
141
+ data: string;
142
+ enumValues: [string, ...string[]];
143
+ driverParam: string;
144
+ }>;
145
+ subjectId: import("drizzle-orm/pg-core").PgTextBuilder<{
146
+ name: "subject_id";
147
+ dataType: "string";
148
+ columnType: "PgText";
149
+ data: string;
150
+ enumValues: [string, ...string[]];
151
+ driverParam: string;
152
+ }>;
153
+ };
154
+ /** The indexes every ledger table carries, for a host declaring its own table over `AUDIT_COLUMNS`. */
155
+ export declare function auditIndexes(t: {
156
+ [K in keyof typeof AUDIT_COLUMNS]: ExtraConfigColumn;
157
+ }): import("drizzle-orm/pg-core").IndexBuilder[];
158
+ /**
159
+ * The ledger, mirrored from `migrations/0001_audit.sql`, which is the source
160
+ * of truth: the SQL carries the CHECKs, the append-only triggers, the partial
161
+ * indexes and the erasure function that drizzle-kit does not generate. A host
162
+ * re-exports this from its own schema module so its drizzle instance and its
163
+ * types know it, or declares a wider table over `AUDIT_COLUMNS`.
164
+ */
8
165
  export declare const auditEvents: import("drizzle-orm/pg-core").PgTableWithColumns<{
9
166
  name: "audit_events";
10
167
  schema: undefined;
@@ -420,6 +577,8 @@ export declare const auditEvents: import("drizzle-orm/pg-core").PgTableWithColum
420
577
  };
421
578
  dialect: "pg";
422
579
  }>;
580
+ /** The package's table, or a host's declared over `AUDIT_COLUMNS` with more columns; structurally either. */
581
+ export type AuditTable = typeof auditEvents;
423
582
  export type AuditEventRow = typeof auditEvents.$inferSelect;
424
583
  export type AuditEventInsert = typeof auditEvents.$inferInsert;
425
584
  export declare const tables: {
package/dist/tables.js CHANGED
@@ -7,7 +7,14 @@ import { bigint, boolean, index, jsonb, pgTable, smallint, text, timestamp, uuid
7
7
  * re-exports this from its own schema module so its drizzle instance and its
8
8
  * types know it.
9
9
  */
10
- export const auditEvents = pgTable('audit_events', {
10
+ /**
11
+ * The ledger's columns, as drizzle builders, so a host that needs a column
12
+ * of its own beside them (a team, a region) declares
13
+ * `pgTable('audit_events', { ...AUDIT_COLUMNS, teamId: uuid('team_id') }, …)`
14
+ * and hands that table to `createLedger`. The package's own `auditEvents`
15
+ * is the same call with nothing added.
16
+ */
17
+ export const AUDIT_COLUMNS = {
11
18
  id: bigint('id', { mode: 'number' }).generatedAlwaysAsIdentity().primaryKey(),
12
19
  occurredAt: timestamp('occurred_at', { withTimezone: true }).notNull().default(sql `now()`),
13
20
  tenantId: uuid('tenant_id'),
@@ -32,15 +39,27 @@ export const auditEvents = pgTable('audit_events', {
32
39
  schemaVersion: smallint('schema_version').notNull().default(1),
33
40
  subjectClass: text('subject_class'),
34
41
  subjectId: text('subject_id'),
35
- }, (t) => [
36
- index('audit_events_tenant_time_idx').on(t.tenantId, t.occurredAt.desc(), t.id.desc()),
37
- index('audit_events_actor_time_idx').on(t.actorId, t.occurredAt.desc()),
38
- index('audit_events_action_time_idx').on(t.action, t.occurredAt.desc()),
39
- index('audit_events_subject_time_idx')
40
- .on(t.subjectClass, t.subjectId, t.occurredAt.desc(), t.id.desc())
41
- .where(sql `${t.subjectId} is not null`),
42
- index('audit_events_request_idx')
43
- .on(t.requestId, t.occurredAt.desc())
44
- .where(sql `${t.requestId} is not null`),
45
- ]);
42
+ };
43
+ /** The indexes every ledger table carries, for a host declaring its own table over `AUDIT_COLUMNS`. */
44
+ export function auditIndexes(t) {
45
+ return [
46
+ index('audit_events_tenant_time_idx').on(t.tenantId, t.occurredAt.desc(), t.id.desc()),
47
+ index('audit_events_actor_time_idx').on(t.actorId, t.occurredAt.desc()),
48
+ index('audit_events_action_time_idx').on(t.action, t.occurredAt.desc()),
49
+ index('audit_events_subject_time_idx')
50
+ .on(t.subjectClass, t.subjectId, t.occurredAt.desc(), t.id.desc())
51
+ .where(sql `${t.subjectId} is not null`),
52
+ index('audit_events_request_idx')
53
+ .on(t.requestId, t.occurredAt.desc())
54
+ .where(sql `${t.requestId} is not null`),
55
+ ];
56
+ }
57
+ /**
58
+ * The ledger, mirrored from `migrations/0001_audit.sql`, which is the source
59
+ * of truth: the SQL carries the CHECKs, the append-only triggers, the partial
60
+ * indexes and the erasure function that drizzle-kit does not generate. A host
61
+ * re-exports this from its own schema module so its drizzle instance and its
62
+ * types know it, or declares a wider table over `AUDIT_COLUMNS`.
63
+ */
64
+ export const auditEvents = pgTable('audit_events', AUDIT_COLUMNS, (t) => auditIndexes(t));
46
65
  export const tables = { events: auditEvents };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wtfalch/audit",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "The estate's append-only audit ledger: the table's shape and walls, erasure, export and readers, with a signer a host builds once inside its trusted base and never exports. Per-app Postgres, a host-supplied vocabulary of closed sets, no framework.",
5
5
  "repository": {
6
6
  "type": "git",