@usebetterdev/audit-cli 0.5.0-beta.1 → 0.6.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.
@@ -0,0 +1,64 @@
1
+ import { ColumnType } from '@usebetterdev/audit-core';
2
+ import { D as DatabaseDialect } from './detect-adapter-DNHcPCKz.js';
3
+
4
+ /**
5
+ * `better-audit check` — Verify audit_logs table schema, indexes, and write path.
6
+ *
7
+ * Performs:
8
+ * 1. Database connectivity test
9
+ * 2. Schema validation: table exists, columns have correct types and nullability
10
+ * 3. Index validation: all expected indexes are present
11
+ * 4. Live integration test: sentinel INSERT → SELECT → verify actor_id → DELETE
12
+ *
13
+ * Supports Postgres, MySQL, and SQLite via dialect-specific introspection queries.
14
+ *
15
+ * Exits with code 0 on success, 1 on failure.
16
+ * All sentinel data is cleaned up regardless of check outcome.
17
+ */
18
+
19
+ interface CheckOptions {
20
+ databaseUrl?: string | undefined;
21
+ verbose?: boolean | undefined;
22
+ }
23
+ interface CheckResult {
24
+ check: string;
25
+ passed: boolean;
26
+ message?: string;
27
+ remediation?: string;
28
+ }
29
+ interface CheckOutput {
30
+ passed: boolean;
31
+ results: CheckResult[];
32
+ warnings: CheckResult[];
33
+ }
34
+ /** Thrown when one or more checks fail. Carries the full output for CLI rendering. */
35
+ declare class CheckFailedError extends Error {
36
+ readonly output: CheckOutput;
37
+ constructor(output: CheckOutput);
38
+ }
39
+ /**
40
+ * Map from core ColumnType to the type string each dialect reports via
41
+ * introspection. Mirrors the sqlType map in generate-sql.ts but in the
42
+ * direction the database returns (lowercase / as-reported).
43
+ *
44
+ * Postgres: information_schema.columns → udt_name
45
+ * MySQL: information_schema.columns → data_type (lowercase)
46
+ * SQLite: PRAGMA table_info → type (uppercase, as declared)
47
+ */
48
+ declare const DIALECT_TYPE_MAP: Record<DatabaseDialect, Record<ColumnType, ReadonlyArray<string>>>;
49
+ interface IntrospectedColumn {
50
+ name: string;
51
+ reportedType: string;
52
+ nullable: boolean;
53
+ }
54
+ declare function checkColumns(columns: ReadonlyArray<IntrospectedColumn>, dialect: DatabaseDialect): CheckResult[];
55
+ declare function checkIndexes(actualIndexes: Set<string>): CheckResult[];
56
+ /**
57
+ * Run all audit_logs checks and return structured results.
58
+ * Does not print to stdout — use `check()` for CLI output.
59
+ */
60
+ declare function runCheck(databaseUrl: string): Promise<CheckOutput>;
61
+ /** CLI action handler for `better-audit check`. */
62
+ declare function check(options?: CheckOptions): Promise<void>;
63
+
64
+ export { CheckFailedError, type CheckOptions, type CheckOutput, type CheckResult, DIALECT_TYPE_MAP, type IntrospectedColumn as _IntrospectedColumn, checkColumns as _checkColumns, checkIndexes as _checkIndexes, check, runCheck };
package/dist/check.js ADDED
@@ -0,0 +1,20 @@
1
+ import {
2
+ CheckFailedError,
3
+ DIALECT_TYPE_MAP,
4
+ check,
5
+ checkColumns,
6
+ checkIndexes,
7
+ runCheck
8
+ } from "./chunk-55KKYFKR.js";
9
+ import "./chunk-O5LHE2AC.js";
10
+ import "./chunk-7GSN73TA.js";
11
+ import "./chunk-HDO5P6X7.js";
12
+ export {
13
+ CheckFailedError,
14
+ DIALECT_TYPE_MAP,
15
+ checkColumns as _checkColumns,
16
+ checkIndexes as _checkIndexes,
17
+ check,
18
+ runCheck
19
+ };
20
+ //# sourceMappingURL=check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,408 @@
1
+ import {
2
+ INDEX_DEFINITIONS
3
+ } from "./chunk-O5LHE2AC.js";
4
+ import {
5
+ createKyselyInstance
6
+ } from "./chunk-7GSN73TA.js";
7
+ import {
8
+ detectDialect
9
+ } from "./chunk-HDO5P6X7.js";
10
+
11
+ // src/check.ts
12
+ import { randomUUID } from "crypto";
13
+ import { sql } from "kysely";
14
+ import pc from "picocolors";
15
+ import { AUDIT_LOG_SCHEMA } from "@usebetterdev/audit-core";
16
+ var CheckFailedError = class extends Error {
17
+ output;
18
+ constructor(output) {
19
+ super("One or more audit checks failed");
20
+ this.name = "CheckFailedError";
21
+ this.output = output;
22
+ }
23
+ };
24
+ var SENTINEL_TABLE_NAME = "__audit_cli_check__";
25
+ var SENTINEL_ACTOR_ID = "__check_actor__";
26
+ var SENTINEL_RECORD_ID = "__sentinel__";
27
+ var DIALECT_TYPE_MAP = {
28
+ postgres: {
29
+ uuid: ["uuid"],
30
+ timestamptz: ["timestamptz"],
31
+ text: ["text"],
32
+ jsonb: ["jsonb"],
33
+ boolean: ["bool"]
34
+ },
35
+ mysql: {
36
+ uuid: ["char"],
37
+ timestamptz: ["datetime"],
38
+ text: ["text", "mediumtext", "longtext"],
39
+ jsonb: ["json"],
40
+ boolean: ["tinyint"]
41
+ },
42
+ sqlite: {
43
+ uuid: ["TEXT"],
44
+ timestamptz: ["TEXT"],
45
+ text: ["TEXT"],
46
+ jsonb: ["TEXT"],
47
+ boolean: ["INTEGER"]
48
+ }
49
+ };
50
+ function normalizeReportedType(raw, dialect) {
51
+ if (dialect === "sqlite") {
52
+ return raw.toUpperCase();
53
+ }
54
+ return raw.toLowerCase();
55
+ }
56
+ function typeMatches(actual, definition, dialect) {
57
+ const normalized = normalizeReportedType(actual, dialect);
58
+ const acceptable = DIALECT_TYPE_MAP[dialect][definition.type];
59
+ return acceptable.includes(normalized);
60
+ }
61
+ function expectedTypeLabel(definition, dialect) {
62
+ const acceptable = DIALECT_TYPE_MAP[dialect][definition.type];
63
+ if (acceptable.length === 1) {
64
+ return acceptable[0] ?? definition.type;
65
+ }
66
+ return acceptable.join(" | ");
67
+ }
68
+ async function checkConnection(db) {
69
+ try {
70
+ await sql`SELECT 1`.execute(db);
71
+ return { check: "database connection", passed: true };
72
+ } catch (err) {
73
+ const message = err instanceof Error ? err.message : String(err);
74
+ return {
75
+ check: "database connection",
76
+ passed: false,
77
+ message: `Cannot connect to database: ${message}`,
78
+ remediation: "Verify DATABASE_URL is correct and the database server is running"
79
+ };
80
+ }
81
+ }
82
+ async function checkTableExists(db) {
83
+ try {
84
+ await sql`SELECT 1 FROM audit_logs WHERE 1 = 0`.execute(db);
85
+ return { check: "audit_logs table exists", passed: true };
86
+ } catch {
87
+ return {
88
+ check: "audit_logs table exists",
89
+ passed: false,
90
+ message: "Table audit_logs not found",
91
+ remediation: "Run `npx @usebetterdev/audit-cli migrate` to create the table"
92
+ };
93
+ }
94
+ }
95
+ async function introspectColumnsPostgres(db) {
96
+ const result = await sql`
97
+ SELECT column_name, udt_name, is_nullable
98
+ FROM information_schema.columns
99
+ WHERE table_schema = 'public' AND table_name = ${AUDIT_LOG_SCHEMA.tableName}
100
+ ORDER BY ordinal_position
101
+ `.execute(db);
102
+ return result.rows.map((row) => ({
103
+ name: row.column_name,
104
+ reportedType: row.udt_name,
105
+ nullable: row.is_nullable === "YES"
106
+ }));
107
+ }
108
+ async function introspectColumnsMysql(db) {
109
+ const result = await sql`
110
+ SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE
111
+ FROM information_schema.columns
112
+ WHERE table_schema = DATABASE() AND table_name = ${AUDIT_LOG_SCHEMA.tableName}
113
+ ORDER BY ORDINAL_POSITION
114
+ `.execute(db);
115
+ return result.rows.map((row) => ({
116
+ name: row.COLUMN_NAME,
117
+ reportedType: row.DATA_TYPE,
118
+ nullable: row.IS_NULLABLE === "YES"
119
+ }));
120
+ }
121
+ async function introspectColumnsSqlite(db) {
122
+ const result = await sql`PRAGMA table_info(audit_logs)`.execute(db);
123
+ return result.rows.map((row) => ({
124
+ name: row.name,
125
+ reportedType: row.type,
126
+ nullable: row.notnull === 0
127
+ }));
128
+ }
129
+ async function introspectColumns(db, dialect) {
130
+ if (dialect === "mysql") {
131
+ return introspectColumnsMysql(db);
132
+ }
133
+ if (dialect === "sqlite") {
134
+ return introspectColumnsSqlite(db);
135
+ }
136
+ return introspectColumnsPostgres(db);
137
+ }
138
+ function checkColumns(columns, dialect) {
139
+ const results = [];
140
+ const actualColumns = /* @__PURE__ */ new Map();
141
+ for (const col of columns) {
142
+ actualColumns.set(col.name, col);
143
+ }
144
+ for (const [name, definition] of Object.entries(AUDIT_LOG_SCHEMA.columns)) {
145
+ const actual = actualColumns.get(name);
146
+ if (actual === void 0) {
147
+ results.push({
148
+ check: `column ${name}`,
149
+ passed: false,
150
+ message: `Missing column: ${name}`,
151
+ remediation: "Run `npx @usebetterdev/audit-cli migrate` to add missing columns"
152
+ });
153
+ continue;
154
+ }
155
+ if (!typeMatches(actual.reportedType, definition, dialect)) {
156
+ const expected = expectedTypeLabel(definition, dialect);
157
+ results.push({
158
+ check: `column ${name} type`,
159
+ passed: false,
160
+ message: `Column ${name} has type "${actual.reportedType}", expected "${expected}"`,
161
+ remediation: `Alter column ${name} to type ${definition.type.toUpperCase()}`
162
+ });
163
+ } else {
164
+ results.push({ check: `column ${name} type`, passed: true });
165
+ }
166
+ if (dialect === "sqlite" && definition.primaryKey === true) {
167
+ results.push({ check: `column ${name} nullability`, passed: true });
168
+ } else if (actual.nullable !== definition.nullable) {
169
+ const actualDesc = actual.nullable ? "nullable" : "NOT NULL";
170
+ const expectedDesc = definition.nullable ? "nullable" : "NOT NULL";
171
+ results.push({
172
+ check: `column ${name} nullability`,
173
+ passed: false,
174
+ message: `Column ${name} is ${actualDesc}, expected ${expectedDesc}`,
175
+ remediation: `ALTER TABLE audit_logs ALTER COLUMN ${name} ${definition.nullable ? "DROP NOT NULL" : "SET NOT NULL"}`
176
+ });
177
+ } else {
178
+ results.push({ check: `column ${name} nullability`, passed: true });
179
+ }
180
+ }
181
+ return results;
182
+ }
183
+ async function introspectIndexNamesPostgres(db) {
184
+ const result = await sql`
185
+ SELECT indexname
186
+ FROM pg_indexes
187
+ WHERE schemaname = 'public' AND tablename = ${AUDIT_LOG_SCHEMA.tableName}
188
+ `.execute(db);
189
+ return new Set(result.rows.map((row) => row.indexname));
190
+ }
191
+ async function introspectIndexNamesMysql(db) {
192
+ const result = await sql`
193
+ SELECT DISTINCT INDEX_NAME
194
+ FROM information_schema.statistics
195
+ WHERE table_schema = DATABASE() AND table_name = ${AUDIT_LOG_SCHEMA.tableName}
196
+ `.execute(db);
197
+ return new Set(result.rows.map((row) => row.INDEX_NAME));
198
+ }
199
+ async function introspectIndexNamesSqlite(db) {
200
+ const result = await sql`
201
+ PRAGMA index_list(audit_logs)
202
+ `.execute(db);
203
+ return new Set(result.rows.map((row) => row.name));
204
+ }
205
+ async function introspectIndexNames(db, dialect) {
206
+ if (dialect === "mysql") {
207
+ return introspectIndexNamesMysql(db);
208
+ }
209
+ if (dialect === "sqlite") {
210
+ return introspectIndexNamesSqlite(db);
211
+ }
212
+ return introspectIndexNamesPostgres(db);
213
+ }
214
+ function checkIndexes(actualIndexes) {
215
+ const results = [];
216
+ for (const idx of INDEX_DEFINITIONS) {
217
+ if (actualIndexes.has(idx.name)) {
218
+ results.push({ check: `index ${idx.name}`, passed: true });
219
+ } else {
220
+ results.push({
221
+ check: `index ${idx.name}`,
222
+ passed: false,
223
+ message: `Missing index ${idx.name} on (${idx.columns.join(", ")})`,
224
+ remediation: "Run `npx @usebetterdev/audit-cli migrate` to create missing indexes"
225
+ });
226
+ }
227
+ }
228
+ return results;
229
+ }
230
+ async function checkLiveWrite(db) {
231
+ const results = [];
232
+ const sentinelId = randomUUID();
233
+ try {
234
+ try {
235
+ await sql`
236
+ INSERT INTO audit_logs (id, table_name, operation, record_id, actor_id)
237
+ VALUES (
238
+ ${sentinelId},
239
+ ${SENTINEL_TABLE_NAME},
240
+ 'INSERT',
241
+ ${SENTINEL_RECORD_ID},
242
+ ${SENTINEL_ACTOR_ID}
243
+ )
244
+ `.execute(db);
245
+ results.push({
246
+ check: "write test: insert sentinel row",
247
+ passed: true
248
+ });
249
+ } catch (err) {
250
+ const message = err instanceof Error ? err.message : String(err);
251
+ results.push({
252
+ check: "write test: insert sentinel row",
253
+ passed: false,
254
+ message: `Failed to insert into audit_logs: ${message}`,
255
+ remediation: "Verify the database user has INSERT permission on audit_logs"
256
+ });
257
+ return results;
258
+ }
259
+ try {
260
+ const readResult = await sql`
261
+ SELECT id, actor_id FROM audit_logs WHERE id = ${sentinelId}
262
+ `.execute(db);
263
+ const row = readResult.rows[0];
264
+ if (row === void 0) {
265
+ results.push({
266
+ check: "write test: read back sentinel row",
267
+ passed: false,
268
+ message: "Sentinel row not found after INSERT \u2014 possible trigger or RLS interference",
269
+ remediation: "Check for DELETE triggers or restrictive RLS policies on audit_logs"
270
+ });
271
+ return results;
272
+ }
273
+ results.push({
274
+ check: "write test: read back sentinel row",
275
+ passed: true
276
+ });
277
+ if (row.actor_id === SENTINEL_ACTOR_ID) {
278
+ results.push({
279
+ check: "write test: actor context captured",
280
+ passed: true
281
+ });
282
+ } else {
283
+ results.push({
284
+ check: "write test: actor context captured",
285
+ passed: false,
286
+ message: `actor_id is "${row.actor_id ?? "NULL"}", expected "${SENTINEL_ACTOR_ID}"`,
287
+ remediation: "Verify AsyncLocalStorage context propagation in your middleware setup"
288
+ });
289
+ }
290
+ } catch (err) {
291
+ const message = err instanceof Error ? err.message : String(err);
292
+ results.push({
293
+ check: "write test: read back sentinel row",
294
+ passed: false,
295
+ message: `Failed to read from audit_logs: ${message}`,
296
+ remediation: "Verify the database user has SELECT permission on audit_logs"
297
+ });
298
+ }
299
+ } finally {
300
+ try {
301
+ await sql`
302
+ DELETE FROM audit_logs WHERE id = ${sentinelId}
303
+ `.execute(db);
304
+ } catch {
305
+ }
306
+ }
307
+ return results;
308
+ }
309
+ async function runCheck(databaseUrl) {
310
+ const dialect = detectDialect(databaseUrl);
311
+ const db = await createKyselyInstance(databaseUrl, dialect);
312
+ const results = [];
313
+ const warnings = [];
314
+ try {
315
+ const connectionCheck = await checkConnection(db);
316
+ results.push(connectionCheck);
317
+ if (!connectionCheck.passed) {
318
+ return { passed: false, results, warnings };
319
+ }
320
+ const tableCheck = await checkTableExists(db);
321
+ results.push(tableCheck);
322
+ if (!tableCheck.passed) {
323
+ return { passed: false, results, warnings };
324
+ }
325
+ const [columns, indexNames] = await Promise.all([
326
+ introspectColumns(db, dialect),
327
+ introspectIndexNames(db, dialect)
328
+ ]);
329
+ results.push(...checkColumns(columns, dialect));
330
+ results.push(...checkIndexes(indexNames));
331
+ results.push(...await checkLiveWrite(db));
332
+ } finally {
333
+ await db.destroy();
334
+ }
335
+ const passed = results.every((r) => r.passed);
336
+ return { passed, results, warnings };
337
+ }
338
+ async function check(options = {}) {
339
+ const databaseUrl = options.databaseUrl ?? process.env["DATABASE_URL"];
340
+ if (databaseUrl === void 0 || databaseUrl === "") {
341
+ throw new Error(
342
+ "DATABASE_URL is required. Set the DATABASE_URL environment variable or pass --database-url."
343
+ );
344
+ }
345
+ const output = await runCheck(databaseUrl);
346
+ const verbose = options.verbose === true;
347
+ printCheckOutput(output, verbose);
348
+ if (!output.passed) {
349
+ throw new CheckFailedError(output);
350
+ }
351
+ }
352
+ function printCheckOutput(output, verbose) {
353
+ const passedCount = output.results.filter((r) => r.passed).length;
354
+ const failedCount = output.results.length - passedCount;
355
+ const failures = output.results.filter((r) => !r.passed);
356
+ if (verbose) {
357
+ console.log("");
358
+ for (const r of output.results) {
359
+ const icon = r.passed ? pc.green("\u2713") : pc.red("\u2717");
360
+ const detail = r.message !== void 0 ? pc.dim(` \u2014 ${r.message}`) : "";
361
+ console.log(` ${icon} ${r.check}${detail}`);
362
+ }
363
+ }
364
+ if (output.warnings.length > 0) {
365
+ console.log("");
366
+ for (const w of output.warnings) {
367
+ console.log(` ${pc.yellow("\u26A0")} ${w.message ?? w.check}`);
368
+ }
369
+ }
370
+ console.log("");
371
+ if (output.passed) {
372
+ console.log(pc.green(`\u2713 All ${passedCount} checks passed`));
373
+ } else {
374
+ console.log(
375
+ pc.red(
376
+ `\u2717 ${failedCount} check${failedCount === 1 ? "" : "s"} failed`
377
+ ) + pc.dim(`, ${passedCount} passed`)
378
+ );
379
+ console.log("");
380
+ if (verbose) {
381
+ for (const f of failures) {
382
+ if (f.remediation !== void 0) {
383
+ console.log(` ${pc.cyan("\u2192")} ${f.check}: ${f.remediation}`);
384
+ }
385
+ }
386
+ } else {
387
+ for (const f of failures) {
388
+ console.log(` ${pc.red("\u2717")} ${f.check}`);
389
+ if (f.message !== void 0) {
390
+ console.log(` ${f.message}`);
391
+ }
392
+ if (f.remediation !== void 0) {
393
+ console.log(` ${pc.cyan("\u2192")} ${f.remediation}`);
394
+ }
395
+ }
396
+ }
397
+ }
398
+ }
399
+
400
+ export {
401
+ CheckFailedError,
402
+ DIALECT_TYPE_MAP,
403
+ checkColumns,
404
+ checkIndexes,
405
+ runCheck,
406
+ check
407
+ };
408
+ //# sourceMappingURL=chunk-55KKYFKR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/check.ts"],"sourcesContent":["/**\n * `better-audit check` — Verify audit_logs table schema, indexes, and write path.\n *\n * Performs:\n * 1. Database connectivity test\n * 2. Schema validation: table exists, columns have correct types and nullability\n * 3. Index validation: all expected indexes are present\n * 4. Live integration test: sentinel INSERT → SELECT → verify actor_id → DELETE\n *\n * Supports Postgres, MySQL, and SQLite via dialect-specific introspection queries.\n *\n * Exits with code 0 on success, 1 on failure.\n * All sentinel data is cleaned up regardless of check outcome.\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport { sql } from \"kysely\";\nimport type { Kysely } from \"kysely\";\nimport pc from \"picocolors\";\nimport { AUDIT_LOG_SCHEMA, type ColumnType } from \"@usebetterdev/audit-core\";\nimport { createKyselyInstance } from \"./sql-executor.js\";\nimport type { Database } from \"./sql-executor.js\";\nimport { detectDialect, type DatabaseDialect } from \"./detect-adapter.js\";\nimport { INDEX_DEFINITIONS } from \"./generate-sql.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface CheckOptions {\n databaseUrl?: string | undefined;\n verbose?: boolean | undefined;\n}\n\nexport interface CheckResult {\n check: string;\n passed: boolean;\n message?: string;\n remediation?: string;\n}\n\nexport interface CheckOutput {\n passed: boolean;\n results: CheckResult[];\n warnings: CheckResult[];\n}\n\n/** Thrown when one or more checks fail. Carries the full output for CLI rendering. */\nexport class CheckFailedError extends Error {\n readonly output: CheckOutput;\n constructor(output: CheckOutput) {\n super(\"One or more audit checks failed\");\n this.name = \"CheckFailedError\";\n this.output = output;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst SENTINEL_TABLE_NAME = \"__audit_cli_check__\";\nconst SENTINEL_ACTOR_ID = \"__check_actor__\";\nconst SENTINEL_RECORD_ID = \"__sentinel__\";\n\n/**\n * Map from core ColumnType to the type string each dialect reports via\n * introspection. Mirrors the sqlType map in generate-sql.ts but in the\n * direction the database returns (lowercase / as-reported).\n *\n * Postgres: information_schema.columns → udt_name\n * MySQL: information_schema.columns → data_type (lowercase)\n * SQLite: PRAGMA table_info → type (uppercase, as declared)\n */\nexport const DIALECT_TYPE_MAP: Record<\n DatabaseDialect,\n Record<ColumnType, ReadonlyArray<string>>\n> = {\n postgres: {\n uuid: [\"uuid\"],\n timestamptz: [\"timestamptz\"],\n text: [\"text\"],\n jsonb: [\"jsonb\"],\n boolean: [\"bool\"],\n },\n mysql: {\n uuid: [\"char\"],\n timestamptz: [\"datetime\"],\n text: [\"text\", \"mediumtext\", \"longtext\"],\n jsonb: [\"json\"],\n boolean: [\"tinyint\"],\n },\n sqlite: {\n uuid: [\"TEXT\"],\n timestamptz: [\"TEXT\"],\n text: [\"TEXT\"],\n jsonb: [\"TEXT\"],\n boolean: [\"INTEGER\"],\n },\n};\n\n// ---------------------------------------------------------------------------\n// Shared helpers\n// ---------------------------------------------------------------------------\n\n/** Normalize a reported type string for comparison against the dialect map. */\nfunction normalizeReportedType(\n raw: string,\n dialect: DatabaseDialect,\n): string {\n if (dialect === \"sqlite\") {\n return raw.toUpperCase();\n }\n // Postgres udt_name and MySQL data_type are already lowercase\n return raw.toLowerCase();\n}\n\n/** Check whether `actual` matches any of the acceptable types for a column. */\nfunction typeMatches(\n actual: string,\n definition: { type: ColumnType },\n dialect: DatabaseDialect,\n): boolean {\n const normalized = normalizeReportedType(actual, dialect);\n const acceptable = DIALECT_TYPE_MAP[dialect][definition.type];\n return acceptable.includes(normalized);\n}\n\n/** Build the \"expected\" label shown in error messages. */\nfunction expectedTypeLabel(\n definition: { type: ColumnType },\n dialect: DatabaseDialect,\n): string {\n const acceptable = DIALECT_TYPE_MAP[dialect][definition.type];\n if (acceptable.length === 1) {\n return acceptable[0] ?? definition.type;\n }\n return acceptable.join(\" | \");\n}\n\n// ---------------------------------------------------------------------------\n// Individual checks\n// ---------------------------------------------------------------------------\n\nasync function checkConnection(\n db: Kysely<Database>,\n): Promise<CheckResult> {\n try {\n await sql`SELECT 1`.execute(db);\n return { check: \"database connection\", passed: true };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return {\n check: \"database connection\",\n passed: false,\n message: `Cannot connect to database: ${message}`,\n remediation:\n \"Verify DATABASE_URL is correct and the database server is running\",\n };\n }\n}\n\nasync function checkTableExists(\n db: Kysely<Database>,\n): Promise<CheckResult> {\n try {\n await sql`SELECT 1 FROM audit_logs WHERE 1 = 0`.execute(db);\n return { check: \"audit_logs table exists\", passed: true };\n } catch {\n return {\n check: \"audit_logs table exists\",\n passed: false,\n message: \"Table audit_logs not found\",\n remediation:\n \"Run `npx @usebetterdev/audit-cli migrate` to create the table\",\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Column checks — per-dialect introspection\n// ---------------------------------------------------------------------------\n\ninterface IntrospectedColumn {\n name: string;\n reportedType: string;\n nullable: boolean;\n}\n\nasync function introspectColumnsPostgres(\n db: Kysely<Database>,\n): Promise<IntrospectedColumn[]> {\n const result = await sql<{\n column_name: string;\n udt_name: string;\n is_nullable: string;\n }>`\n SELECT column_name, udt_name, is_nullable\n FROM information_schema.columns\n WHERE table_schema = 'public' AND table_name = ${AUDIT_LOG_SCHEMA.tableName}\n ORDER BY ordinal_position\n `.execute(db);\n\n return result.rows.map((row) => ({\n name: row.column_name,\n reportedType: row.udt_name,\n nullable: row.is_nullable === \"YES\",\n }));\n}\n\nasync function introspectColumnsMysql(\n db: Kysely<Database>,\n): Promise<IntrospectedColumn[]> {\n const result = await sql<{\n COLUMN_NAME: string;\n DATA_TYPE: string;\n IS_NULLABLE: string;\n }>`\n SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE\n FROM information_schema.columns\n WHERE table_schema = DATABASE() AND table_name = ${AUDIT_LOG_SCHEMA.tableName}\n ORDER BY ORDINAL_POSITION\n `.execute(db);\n\n return result.rows.map((row) => ({\n name: row.COLUMN_NAME,\n reportedType: row.DATA_TYPE,\n nullable: row.IS_NULLABLE === \"YES\",\n }));\n}\n\nasync function introspectColumnsSqlite(\n db: Kysely<Database>,\n): Promise<IntrospectedColumn[]> {\n const result = await sql<{\n name: string;\n type: string;\n notnull: number;\n }>`PRAGMA table_info(audit_logs)`.execute(db);\n\n return result.rows.map((row) => ({\n name: row.name,\n reportedType: row.type,\n nullable: row.notnull === 0,\n }));\n}\n\nasync function introspectColumns(\n db: Kysely<Database>,\n dialect: DatabaseDialect,\n): Promise<IntrospectedColumn[]> {\n if (dialect === \"mysql\") {\n return introspectColumnsMysql(db);\n }\n if (dialect === \"sqlite\") {\n return introspectColumnsSqlite(db);\n }\n return introspectColumnsPostgres(db);\n}\n\nfunction checkColumns(\n columns: ReadonlyArray<IntrospectedColumn>,\n dialect: DatabaseDialect,\n): CheckResult[] {\n const results: CheckResult[] = [];\n\n const actualColumns = new Map<string, IntrospectedColumn>();\n for (const col of columns) {\n actualColumns.set(col.name, col);\n }\n\n for (const [name, definition] of Object.entries(AUDIT_LOG_SCHEMA.columns)) {\n const actual = actualColumns.get(name);\n\n if (actual === undefined) {\n results.push({\n check: `column ${name}`,\n passed: false,\n message: `Missing column: ${name}`,\n remediation:\n \"Run `npx @usebetterdev/audit-cli migrate` to add missing columns\",\n });\n continue;\n }\n\n // Type check\n if (!typeMatches(actual.reportedType, definition, dialect)) {\n const expected = expectedTypeLabel(definition, dialect);\n results.push({\n check: `column ${name} type`,\n passed: false,\n message: `Column ${name} has type \"${actual.reportedType}\", expected \"${expected}\"`,\n remediation: `Alter column ${name} to type ${definition.type.toUpperCase()}`,\n });\n } else {\n results.push({ check: `column ${name} type`, passed: true });\n }\n\n // Nullability check\n // SQLite does not enforce NOT NULL for PRIMARY KEY columns the same way,\n // so skip nullability checks on the primary key in SQLite.\n if (dialect === \"sqlite\" && definition.primaryKey === true) {\n results.push({ check: `column ${name} nullability`, passed: true });\n } else if (actual.nullable !== definition.nullable) {\n const actualDesc = actual.nullable ? \"nullable\" : \"NOT NULL\";\n const expectedDesc = definition.nullable ? \"nullable\" : \"NOT NULL\";\n results.push({\n check: `column ${name} nullability`,\n passed: false,\n message: `Column ${name} is ${actualDesc}, expected ${expectedDesc}`,\n remediation: `ALTER TABLE audit_logs ALTER COLUMN ${name} ${definition.nullable ? \"DROP NOT NULL\" : \"SET NOT NULL\"}`,\n });\n } else {\n results.push({ check: `column ${name} nullability`, passed: true });\n }\n }\n\n return results;\n}\n\n// ---------------------------------------------------------------------------\n// Index checks — per-dialect introspection\n// ---------------------------------------------------------------------------\n\nasync function introspectIndexNamesPostgres(\n db: Kysely<Database>,\n): Promise<Set<string>> {\n const result = await sql<{ indexname: string }>`\n SELECT indexname\n FROM pg_indexes\n WHERE schemaname = 'public' AND tablename = ${AUDIT_LOG_SCHEMA.tableName}\n `.execute(db);\n return new Set(result.rows.map((row) => row.indexname));\n}\n\nasync function introspectIndexNamesMysql(\n db: Kysely<Database>,\n): Promise<Set<string>> {\n const result = await sql<{ INDEX_NAME: string }>`\n SELECT DISTINCT INDEX_NAME\n FROM information_schema.statistics\n WHERE table_schema = DATABASE() AND table_name = ${AUDIT_LOG_SCHEMA.tableName}\n `.execute(db);\n return new Set(result.rows.map((row) => row.INDEX_NAME));\n}\n\nasync function introspectIndexNamesSqlite(\n db: Kysely<Database>,\n): Promise<Set<string>> {\n const result = await sql<{ name: string }>`\n PRAGMA index_list(audit_logs)\n `.execute(db);\n return new Set(result.rows.map((row) => row.name));\n}\n\nasync function introspectIndexNames(\n db: Kysely<Database>,\n dialect: DatabaseDialect,\n): Promise<Set<string>> {\n if (dialect === \"mysql\") {\n return introspectIndexNamesMysql(db);\n }\n if (dialect === \"sqlite\") {\n return introspectIndexNamesSqlite(db);\n }\n return introspectIndexNamesPostgres(db);\n}\n\nfunction checkIndexes(actualIndexes: Set<string>): CheckResult[] {\n const results: CheckResult[] = [];\n\n for (const idx of INDEX_DEFINITIONS) {\n if (actualIndexes.has(idx.name)) {\n results.push({ check: `index ${idx.name}`, passed: true });\n } else {\n results.push({\n check: `index ${idx.name}`,\n passed: false,\n message: `Missing index ${idx.name} on (${idx.columns.join(\", \")})`,\n remediation:\n \"Run `npx @usebetterdev/audit-cli migrate` to create missing indexes\",\n });\n }\n }\n\n return results;\n}\n\n// ---------------------------------------------------------------------------\n// Live write test\n// ---------------------------------------------------------------------------\n\nasync function checkLiveWrite(\n db: Kysely<Database>,\n): Promise<CheckResult[]> {\n const results: CheckResult[] = [];\n const sentinelId = randomUUID();\n\n try {\n // INSERT sentinel row\n try {\n await sql`\n INSERT INTO audit_logs (id, table_name, operation, record_id, actor_id)\n VALUES (\n ${sentinelId},\n ${SENTINEL_TABLE_NAME},\n 'INSERT',\n ${SENTINEL_RECORD_ID},\n ${SENTINEL_ACTOR_ID}\n )\n `.execute(db);\n results.push({\n check: \"write test: insert sentinel row\",\n passed: true,\n });\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n results.push({\n check: \"write test: insert sentinel row\",\n passed: false,\n message: `Failed to insert into audit_logs: ${message}`,\n remediation:\n \"Verify the database user has INSERT permission on audit_logs\",\n });\n return results;\n }\n\n // SELECT sentinel row back\n try {\n const readResult = await sql<{\n id: string;\n actor_id: string | null;\n }>`\n SELECT id, actor_id FROM audit_logs WHERE id = ${sentinelId}\n `.execute(db);\n\n const row = readResult.rows[0];\n\n if (row === undefined) {\n results.push({\n check: \"write test: read back sentinel row\",\n passed: false,\n message:\n \"Sentinel row not found after INSERT — possible trigger or RLS interference\",\n remediation:\n \"Check for DELETE triggers or restrictive RLS policies on audit_logs\",\n });\n return results;\n }\n\n results.push({\n check: \"write test: read back sentinel row\",\n passed: true,\n });\n\n // Verify actor_id round-trip (context propagation proxy)\n if (row.actor_id === SENTINEL_ACTOR_ID) {\n results.push({\n check: \"write test: actor context captured\",\n passed: true,\n });\n } else {\n results.push({\n check: \"write test: actor context captured\",\n passed: false,\n message: `actor_id is \"${row.actor_id ?? \"NULL\"}\", expected \"${SENTINEL_ACTOR_ID}\"`,\n remediation:\n \"Verify AsyncLocalStorage context propagation in your middleware setup\",\n });\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n results.push({\n check: \"write test: read back sentinel row\",\n passed: false,\n message: `Failed to read from audit_logs: ${message}`,\n remediation:\n \"Verify the database user has SELECT permission on audit_logs\",\n });\n }\n } finally {\n // Always clean up sentinel data\n try {\n await sql`\n DELETE FROM audit_logs WHERE id = ${sentinelId}\n `.execute(db);\n } catch {\n // Cleanup failure is not critical — sentinel has a distinctive table_name\n // for manual identification\n }\n }\n\n return results;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Run all audit_logs checks and return structured results.\n * Does not print to stdout — use `check()` for CLI output.\n */\nexport async function runCheck(databaseUrl: string): Promise<CheckOutput> {\n const dialect = detectDialect(databaseUrl);\n const db = await createKyselyInstance(databaseUrl, dialect);\n const results: CheckResult[] = [];\n const warnings: CheckResult[] = [];\n\n try {\n // 0. Connection test\n const connectionCheck = await checkConnection(db);\n results.push(connectionCheck);\n if (!connectionCheck.passed) {\n return { passed: false, results, warnings };\n }\n\n // 1. Table exists\n const tableCheck = await checkTableExists(db);\n results.push(tableCheck);\n if (!tableCheck.passed) {\n return { passed: false, results, warnings };\n }\n\n // 2–3. Column + index validation (independent — run in parallel)\n const [columns, indexNames] = await Promise.all([\n introspectColumns(db, dialect),\n introspectIndexNames(db, dialect),\n ]);\n results.push(...checkColumns(columns, dialect));\n results.push(...checkIndexes(indexNames));\n\n // 4. Live write + read-back + actor context + cleanup\n results.push(...(await checkLiveWrite(db)));\n } finally {\n await db.destroy();\n }\n\n const passed = results.every((r) => r.passed);\n return { passed, results, warnings };\n}\n\n// ---------------------------------------------------------------------------\n// CLI entry point\n// ---------------------------------------------------------------------------\n\n/** CLI action handler for `better-audit check`. */\nexport async function check(options: CheckOptions = {}): Promise<void> {\n const databaseUrl = options.databaseUrl ?? process.env[\"DATABASE_URL\"];\n if (databaseUrl === undefined || databaseUrl === \"\") {\n throw new Error(\n \"DATABASE_URL is required. Set the DATABASE_URL environment variable or pass --database-url.\",\n );\n }\n\n const output = await runCheck(databaseUrl);\n const verbose = options.verbose === true;\n\n printCheckOutput(output, verbose);\n\n if (!output.passed) {\n throw new CheckFailedError(output);\n }\n}\n\n/** Print check results to stdout. Separated for testability. */\nfunction printCheckOutput(output: CheckOutput, verbose: boolean): void {\n const passedCount = output.results.filter((r) => r.passed).length;\n const failedCount = output.results.length - passedCount;\n const failures = output.results.filter((r) => !r.passed);\n\n // Verbose: show all individual checks\n if (verbose) {\n console.log(\"\");\n for (const r of output.results) {\n const icon = r.passed ? pc.green(\"✓\") : pc.red(\"✗\");\n const detail =\n r.message !== undefined ? pc.dim(` — ${r.message}`) : \"\";\n console.log(` ${icon} ${r.check}${detail}`);\n }\n }\n\n // Warnings\n if (output.warnings.length > 0) {\n console.log(\"\");\n for (const w of output.warnings) {\n console.log(` ${pc.yellow(\"⚠\")} ${w.message ?? w.check}`);\n }\n }\n\n // Summary\n console.log(\"\");\n if (output.passed) {\n console.log(pc.green(`✓ All ${passedCount} checks passed`));\n } else {\n console.log(\n pc.red(\n `✗ ${failedCount} check${failedCount === 1 ? \"\" : \"s\"} failed`,\n ) + pc.dim(`, ${passedCount} passed`),\n );\n\n // Failure details\n console.log(\"\");\n if (verbose) {\n // In verbose mode, individual results were already printed — just show remediation\n for (const f of failures) {\n if (f.remediation !== undefined) {\n console.log(` ${pc.cyan(\"→\")} ${f.check}: ${f.remediation}`);\n }\n }\n } else {\n // Default mode: show full failure details\n for (const f of failures) {\n console.log(` ${pc.red(\"✗\")} ${f.check}`);\n if (f.message !== undefined) {\n console.log(` ${f.message}`);\n }\n if (f.remediation !== undefined) {\n console.log(` ${pc.cyan(\"→\")} ${f.remediation}`);\n }\n }\n }\n }\n}\n\n// Re-export internals for unit testing\nexport {\n checkColumns as _checkColumns,\n checkIndexes as _checkIndexes,\n};\nexport type { IntrospectedColumn as _IntrospectedColumn };\n"],"mappings":"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","names":[]}