@usebetterdev/audit-cli 0.5.0 → 0.6.1

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.
@@ -7,60 +7,12 @@ import {
7
7
 
8
8
  // src/purge.ts
9
9
  import pc from "picocolors";
10
-
11
- // src/config-loader.ts
12
- import { stat } from "fs/promises";
13
- import { resolve } from "path";
14
- function importFile(path) {
15
- return import(
16
- /* webpackIgnore: true */
17
- path
18
- );
19
- }
20
- function validateRetention(retention) {
21
- if (typeof retention !== "object" || retention === null) {
22
- throw new Error("retention must be an object with a 'days' property");
23
- }
24
- const r = retention;
25
- if (!Number.isInteger(r["days"]) || !Number.isFinite(r["days"]) || r["days"] <= 0) {
26
- throw new Error(`retention.days must be a positive integer, got ${String(r["days"])}`);
27
- }
28
- if (r["tables"] !== void 0) {
29
- if (!Array.isArray(r["tables"]) || r["tables"].length === 0 || r["tables"].some((t) => typeof t !== "string" || t === "")) {
30
- throw new Error("retention.tables must be a non-empty array of non-empty strings");
31
- }
32
- }
33
- }
34
- async function loadConfig(configPath) {
35
- const resolved = resolve(process.cwd(), configPath);
36
- let mod;
37
- try {
38
- mod = await importFile(resolved);
39
- } catch (err) {
40
- const exists = await stat(resolved).then(() => true, () => false);
41
- if (!exists) {
42
- return null;
43
- }
44
- const message = err instanceof Error ? err.message : String(err);
45
- throw new Error(
46
- `Failed to load config file "${configPath}": ${message}
47
- Hint: TypeScript config files require a TypeScript loader. Run with "tsx" or "ts-node", or compile your config first.`
48
- );
49
- }
50
- const config = mod.default;
51
- if (typeof config !== "object" || config === null) {
52
- throw new Error(
53
- `Config file "${configPath}" must export a BetterAuditConfig as the default export.`
54
- );
55
- }
56
- const configObj = config;
57
- if (configObj["retention"] !== void 0) {
58
- validateRetention(configObj["retention"]);
59
- }
60
- return config;
61
- }
62
-
63
- // src/purge.ts
10
+ import {
11
+ loadBetterConfig,
12
+ BetterConfigNotFoundError,
13
+ ConfigValidationError,
14
+ requireAuditConfig
15
+ } from "@usebetterdev/plugin/config";
64
16
  var ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}(T[\w:.+-]+)?$/;
65
17
  var DURATION_REGEX = /^(\d+)(d|w|m|y)$/i;
66
18
  var DEFAULT_BATCH_SIZE = 1e3;
@@ -102,16 +54,24 @@ async function resolveCutoffDate(options) {
102
54
  if (options.since !== void 0) {
103
55
  return parseSinceValue(options.since);
104
56
  }
105
- if (options.config !== void 0) {
106
- const config = await loadConfig(options.config);
107
- if (config?.retention !== void 0) {
57
+ try {
58
+ const config = await loadBetterConfig(
59
+ options.cwd !== void 0 ? { cwd: options.cwd } : {}
60
+ );
61
+ const auditConfig = requireAuditConfig(config);
62
+ if (auditConfig.retention !== void 0) {
108
63
  const cutoff = /* @__PURE__ */ new Date();
109
- cutoff.setDate(cutoff.getDate() - config.retention.days);
64
+ cutoff.setDate(cutoff.getDate() - auditConfig.retention.days);
110
65
  return cutoff;
111
66
  }
67
+ } catch (err) {
68
+ if (err instanceof BetterConfigNotFoundError || err instanceof ConfigValidationError) {
69
+ } else {
70
+ throw err;
71
+ }
112
72
  }
113
73
  throw new Error(
114
- "No retention policy configured. Pass --since <date|duration> or set retention.days in your config file."
74
+ "No retention policy configured. Pass --since <date|duration> or set audit.retention.days in your better.config file."
115
75
  );
116
76
  }
117
77
  async function countEligibleRows(db, before) {
@@ -139,13 +99,10 @@ async function purge(options = {}) {
139
99
  "DATABASE_URL is required. Set the DATABASE_URL environment variable or pass --database-url."
140
100
  );
141
101
  }
142
- const resolvOpts = {};
143
- if (options.since !== void 0) {
144
- resolvOpts.since = options.since;
145
- } else if (options.config !== void 0) {
146
- resolvOpts.config = options.config;
147
- }
148
- const before = await resolveCutoffDate(resolvOpts);
102
+ const before = await resolveCutoffDate({
103
+ ...options.since !== void 0 ? { since: options.since } : {},
104
+ cwd: process.cwd()
105
+ });
149
106
  if (before > /* @__PURE__ */ new Date()) {
150
107
  throw new Error(
151
108
  `Cutoff date ${before.toISOString().split("T")[0]} is in the future. Purge only accepts past dates.`
@@ -222,4 +179,4 @@ export {
222
179
  formatDuration,
223
180
  purge
224
181
  };
225
- //# sourceMappingURL=chunk-SJSGTCG4.js.map
182
+ //# sourceMappingURL=chunk-BKCJJOQN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/purge.ts"],"sourcesContent":["/**\n * `better-audit purge` — Delete audit logs older than the configured retention period.\n *\n * Connects to the database directly via Kysely (no ORM adapter needed).\n * Executes batched DELETEs to avoid holding long row-level locks on large tables.\n *\n * Always run with --dry-run first to preview what will be deleted.\n */\n\nimport pc from \"picocolors\";\nimport { createKyselyInstance } from \"./sql-executor.js\";\nimport { detectDialect } from \"./detect-adapter.js\";\nimport {\n loadBetterConfig,\n BetterConfigNotFoundError,\n ConfigValidationError,\n requireAuditConfig,\n} from \"@usebetterdev/plugin/config\";\nimport type { Database } from \"./sql-executor.js\";\nimport type { Kysely } from \"kysely\";\n\nconst ISO_DATE_REGEX = /^\\d{4}-\\d{2}-\\d{2}(T[\\w:.+-]+)?$/;\nconst DURATION_REGEX = /^(\\d+)(d|w|m|y)$/i;\nconst DEFAULT_BATCH_SIZE = 1000;\nconst MAX_BATCHES = 100_000;\n\nexport interface PurgeOptions {\n /** Preview rows to be deleted without deleting them. */\n dryRun?: boolean;\n /** ISO date string (e.g. \"2025-01-01\") or duration shorthand (e.g. \"90d\", \"1y\"). */\n since?: string;\n /** Rows per DELETE batch. Default: 1000. */\n batchSize?: number;\n /** Database URL (default: DATABASE_URL env). */\n databaseUrl?: string;\n /** Skip confirmation prompt (required for live deletion). */\n yes?: boolean;\n}\n\n/**\n * Parse a `--since` value to an absolute cutoff `Date`.\n *\n * Accepts:\n * - ISO-8601 date strings: \"2025-01-01\" or \"2025-01-01T00:00:00Z\"\n * - Duration shorthands: \"90d\", \"4w\", \"3m\", \"1y\"\n *\n * Exported for testing.\n */\nexport function parseSinceValue(value: string): Date {\n if (ISO_DATE_REGEX.test(value)) {\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) {\n throw new Error(`Invalid date \"${value}\". Expected ISO-8601 format (e.g. \"2025-01-01\").`);\n }\n return date;\n }\n\n const match = DURATION_REGEX.exec(value);\n if (match !== null) {\n const amount = parseInt(match[1]!, 10);\n if (amount <= 0) {\n throw new Error(\n `Invalid --since value \"${value}\". Duration amount must be greater than zero.`,\n );\n }\n const unit = match[2]!.toLowerCase();\n const now = new Date();\n if (unit === \"d\") {\n now.setDate(now.getDate() - amount);\n } else if (unit === \"w\") {\n now.setDate(now.getDate() - amount * 7);\n } else if (unit === \"m\") {\n now.setMonth(now.getMonth() - amount);\n } else {\n // y\n now.setFullYear(now.getFullYear() - amount);\n }\n return now;\n }\n\n throw new Error(\n `Invalid --since value \"${value}\". ` +\n `Expected an ISO date (e.g. \"2025-01-01\") or duration shorthand (e.g. \"90d\", \"4w\", \"3m\", \"1y\").`,\n );\n}\n\n/**\n * Resolve the cutoff date from options.\n *\n * Priority: `--since` flag > config `retention.days`.\n * Throws if neither is available.\n *\n * Exported for testing.\n */\nexport async function resolveCutoffDate(options: {\n since?: string;\n cwd?: string;\n}): Promise<Date> {\n if (options.since !== undefined) {\n return parseSinceValue(options.since);\n }\n\n try {\n const config = await loadBetterConfig(\n options.cwd !== undefined ? { cwd: options.cwd } : {},\n );\n const auditConfig = requireAuditConfig(config);\n if (auditConfig.retention !== undefined) {\n const cutoff = new Date();\n cutoff.setDate(cutoff.getDate() - auditConfig.retention.days);\n return cutoff;\n }\n } catch (err) {\n if (\n err instanceof BetterConfigNotFoundError ||\n err instanceof ConfigValidationError\n ) {\n // Config file not found or audit section missing/invalid — fall through\n } else {\n throw err;\n }\n }\n\n throw new Error(\n \"No retention policy configured. \" +\n \"Pass --since <date|duration> or set audit.retention.days in your better.config file.\",\n );\n}\n\n/**\n * Count rows eligible for deletion (for --dry-run).\n */\nasync function countEligibleRows(\n db: Kysely<Database>,\n before: Date,\n): Promise<number> {\n const result = await db\n .selectFrom(\"audit_logs\")\n .select((eb) => eb.fn.countAll<string>().as(\"count\"))\n .where(\"timestamp\", \"<\", before)\n .executeTakeFirst();\n return result !== undefined ? Number(result.count) : 0;\n}\n\n/**\n * Delete one batch of rows older than `before`.\n *\n * Uses `DELETE … WHERE id IN (SELECT id … LIMIT n)` on all dialects.\n * This avoids the lack of LIMIT support on DELETE in Postgres and SQLite.\n *\n * Returns the number of rows deleted in this batch.\n */\nasync function deleteBatch(\n db: Kysely<Database>,\n before: Date,\n batchSize: number,\n): Promise<number> {\n const subquery = db\n .selectFrom(\"audit_logs\")\n .select(\"id\")\n .where(\"timestamp\", \"<\", before)\n .limit(batchSize);\n\n const result = await db\n .deleteFrom(\"audit_logs\")\n .where(\"id\", \"in\", subquery)\n .executeTakeFirst();\n\n return Number(result.numDeletedRows);\n}\n\n/**\n * Format a number with locale-aware thousands separators.\n */\nfunction formatCount(n: number): string {\n return n.toLocaleString(\"en-US\");\n}\n\n/**\n * Format elapsed milliseconds to a human-readable string.\n *\n * Exported for testing.\n */\nexport function formatDuration(ms: number): string {\n if (ms < 1000) {\n return `${ms}ms`;\n }\n return `${(ms / 1000).toFixed(1)}s`;\n}\n\nexport async function purge(options: PurgeOptions = {}): Promise<void> {\n // 1. Resolve database URL\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 // 2. Resolve the cutoff date\n // Only consult config when --since is absent (short-circuit when --since is set)\n const before = await resolveCutoffDate({\n ...(options.since !== undefined ? { since: options.since } : {}),\n cwd: process.cwd(),\n });\n\n // 3. Validate cutoff is in the past\n if (before > new Date()) {\n throw new Error(\n `Cutoff date ${before.toISOString().split(\"T\")[0]!} is in the future. ` +\n `Purge only accepts past dates.`,\n );\n }\n\n const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;\n if (!Number.isInteger(batchSize) || batchSize <= 0) {\n throw new Error(`--batch-size must be a positive integer, got ${String(batchSize)}`);\n }\n\n // 4. Connect\n const dialect = detectDialect(databaseUrl);\n const db = await createKyselyInstance(databaseUrl, dialect);\n\n const startTime = Date.now();\n\n try {\n // 4. Dry run — count only, no confirmation needed\n if (options.dryRun) {\n const count = await countEligibleRows(db, before);\n process.stderr.write(\n `${pc.cyan(\"→\")} ${pc.bold(formatCount(count))} rows would be deleted` +\n ` (cutoff: ${pc.dim(before.toISOString().split(\"T\")[0]!)})\\n`,\n );\n process.stderr.write(`${pc.dim(\" Dry run — no changes made.\")}\\n`);\n return;\n }\n\n // 5. Confirmation guard — require --yes for live deletion\n if (!options.yes) {\n const count = await countEligibleRows(db, before);\n process.stderr.write(\n `${pc.yellow(\"!\")} ${pc.bold(formatCount(count))} rows will be deleted` +\n ` (cutoff: ${pc.dim(before.toISOString().split(\"T\")[0]!)})\\n` +\n ` Run with ${pc.bold(\"--yes\")} to confirm deletion, or ${pc.bold(\"--dry-run\")} to preview.\\n`,\n );\n return;\n }\n\n // 6. Batched delete loop\n let totalDeleted = 0;\n let batchNumber = 0;\n\n while (batchNumber < MAX_BATCHES) {\n const deleted = await deleteBatch(db, before, batchSize);\n totalDeleted += deleted;\n batchNumber++;\n\n if (batchNumber % 10 === 0) {\n process.stderr.write(\n ` ${pc.dim(`batch ${batchNumber}: ${formatCount(totalDeleted)} rows deleted so far...`)}\\n`,\n );\n }\n\n if (deleted < batchSize) {\n break;\n }\n }\n\n if (batchNumber >= MAX_BATCHES) {\n process.stderr.write(\n pc.yellow(\n ` Warning: reached max batch limit (${MAX_BATCHES.toLocaleString(\"en-US\")}). Some rows may remain.\\n`,\n ),\n );\n }\n\n const elapsed = Date.now() - startTime;\n\n // 7. Summary\n process.stderr.write(`${pc.green(\"✓\")} Purge complete\\n`);\n process.stderr.write(` Rows deleted: ${pc.bold(formatCount(totalDeleted))}\\n`);\n process.stderr.write(` Cutoff date: ${pc.dim(before.toISOString().split(\"T\")[0]!)}\\n`);\n process.stderr.write(` Time taken: ${pc.dim(formatDuration(elapsed))}\\n`);\n } finally {\n await db.destroy();\n }\n}\n"],"mappings":"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","names":[]}
@@ -7,4 +7,4 @@ async function stats(options = {}) {
7
7
  export {
8
8
  stats
9
9
  };
10
- //# sourceMappingURL=chunk-AGFBL646.js.map
10
+ //# sourceMappingURL=chunk-UASMKVFP.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/stats.ts"],"sourcesContent":["/**\n * `better-audit stats` — Show audit log statistics.\n *\n * Outputs:\n * - Total log count\n * - Events per day (last 7 / 30 days)\n * - Top actors (by event count)\n * - Top tables (by event count)\n * - Severity distribution\n *\n * TODO: Connect to DB and run aggregation queries.\n */\n\nexport interface StatsOptions {\n config?: string;\n since?: string;\n}\n\nexport async function stats(options: StatsOptions = {}): Promise<void> {\n console.log(\"[better-audit] stats — not yet implemented\");\n void options;\n}\n"],"mappings":";AAkBA,eAAsB,MAAM,UAAwB,CAAC,GAAkB;AACrE,UAAQ,IAAI,iDAA4C;AACxD,OAAK;AACP;","names":[]}
1
+ {"version":3,"sources":["../src/stats.ts"],"sourcesContent":["/**\n * `better-audit stats` — Show audit log statistics.\n *\n * Outputs:\n * - Total log count\n * - Events per day (last 7 / 30 days)\n * - Top actors (by event count)\n * - Top tables (by event count)\n * - Severity distribution\n *\n * TODO: Connect to DB and run aggregation queries.\n */\n\nexport interface StatsOptions {\n since?: string;\n}\n\nexport async function stats(options: StatsOptions = {}): Promise<void> {\n console.log(\"[better-audit] stats — not yet implemented\");\n void options;\n}\n"],"mappings":";AAiBA,eAAsB,MAAM,UAAwB,CAAC,GAAkB;AACrE,UAAQ,IAAI,iDAA4C;AACxD,OAAK;AACP;","names":[]}
package/dist/cli.js CHANGED
@@ -12,12 +12,12 @@ import {
12
12
  import "./chunk-O5LHE2AC.js";
13
13
  import {
14
14
  purge
15
- } from "./chunk-SJSGTCG4.js";
15
+ } from "./chunk-BKCJJOQN.js";
16
16
  import "./chunk-7GSN73TA.js";
17
17
  import "./chunk-HDO5P6X7.js";
18
18
  import {
19
19
  stats
20
- } from "./chunk-AGFBL646.js";
20
+ } from "./chunk-UASMKVFP.js";
21
21
 
22
22
  // src/cli.ts
23
23
  import { readFileSync } from "fs";
@@ -94,10 +94,10 @@ program.command("check").description("Verify the audit_logs table and ORM adapte
94
94
  () => check({ verbose: opts.verbose, databaseUrl: opts.databaseUrl })
95
95
  );
96
96
  });
97
- program.command("stats").description("Show audit log statistics").option("-c, --config <path>", "Config file path", "better-audit.config.ts").option("--since <period>", "Time window (e.g. 30d)", "30d").action(async (opts) => {
98
- await runAction(() => stats({ config: opts.config, since: opts.since }));
97
+ program.command("stats").description("Show audit log statistics").option("--since <period>", "Time window (e.g. 30d)", "30d").action(async (opts) => {
98
+ await runAction(() => stats({ since: opts.since }));
99
99
  });
100
- program.command("purge").description("Delete audit logs older than the retention period").option("-c, --config <path>", "Config file path", "better-audit.config.ts").option("--dry-run", "Preview rows to be deleted without deleting them").option(
100
+ program.command("purge").description("Delete audit logs older than the retention period").option("--dry-run", "Preview rows to be deleted without deleting them").option(
101
101
  "--since <value>",
102
102
  'ISO date (e.g. "2025-01-01") or duration shorthand (e.g. "90d") \u2014 overrides config'
103
103
  ).option("--batch-size <n>", "Rows per DELETE batch (default: 1000)").option(
@@ -106,7 +106,7 @@ program.command("purge").description("Delete audit logs older than the retention
106
106
  ).option("-y, --yes", "Skip confirmation prompt (required for live deletion)").action(
107
107
  async (opts) => {
108
108
  await runAction(async () => {
109
- const purgeOpts = { config: opts.config };
109
+ const purgeOpts = {};
110
110
  if (opts.dryRun !== void 0) {
111
111
  purgeOpts.dryRun = opts.dryRun;
112
112
  }
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { Command } from \"commander\";\nimport pc from \"picocolors\";\nimport { migrate } from \"./migrate.js\";\nimport { check, CheckFailedError } from \"./check.js\";\nimport { stats } from \"./stats.js\";\nimport { purge } from \"./purge.js\";\nimport { exportLogs } from \"./export.js\";\n\n// Read version from package.json at the package root\nconst cliDir = dirname(fileURLToPath(import.meta.url));\nconst pkgRaw: unknown = JSON.parse(\n readFileSync(join(cliDir, \"..\", \"package.json\"), \"utf-8\"),\n);\nconst cliVersion =\n typeof pkgRaw === \"object\" &&\n pkgRaw !== null &&\n \"version\" in pkgRaw &&\n typeof pkgRaw.version === \"string\"\n ? pkgRaw.version\n : \"0.0.0\";\n\n/** Validate and narrow the --adapter flag value. */\nfunction parseAdapter(\n value: string | undefined,\n): \"drizzle\" | \"prisma\" | undefined {\n if (value === \"drizzle\" || value === \"prisma\" || value === undefined) {\n return value;\n }\n throw new Error(\"--adapter must be 'drizzle' or 'prisma'\");\n}\n\n/** Validate and narrow the --dialect flag value. */\nfunction parseDialect(\n value: string | undefined,\n): \"postgres\" | \"mysql\" | \"sqlite\" | undefined {\n if (\n value === \"postgres\" ||\n value === \"mysql\" ||\n value === \"sqlite\" ||\n value === undefined\n ) {\n return value;\n }\n throw new Error(\"--dialect must be 'postgres', 'mysql', or 'sqlite'\");\n}\n\n/** Extract a human-readable message from an error, including AggregateError nested errors. */\nfunction formatErrorMessage(err: unknown): string {\n if (err instanceof AggregateError && err.errors.length > 0) {\n // AggregateError (e.g. from pg connection failure) has an empty .message\n // but contains nested errors with the actual details\n const nested = err.errors\n .map((e: unknown) => (e instanceof Error ? e.message : String(e)))\n .join(\"; \");\n return err.message !== \"\" ? `${err.message}: ${nested}` : nested;\n }\n if (err instanceof Error) {\n return err.message;\n }\n return String(err);\n}\n\n/** Wrap a subcommand action with consistent error handling. */\nasync function runAction(fn: () => Promise<void>): Promise<void> {\n try {\n await fn();\n } catch (err) {\n // CheckFailedError already printed its output — just exit\n if (err instanceof CheckFailedError) {\n process.exit(1);\n }\n const message = formatErrorMessage(err);\n console.error(pc.red(message));\n process.exit(1);\n }\n}\n\nconst program = new Command()\n .name(\"@usebetterdev/audit-cli\")\n .description(\"CLI for @usebetterdev/audit — compliance-ready audit logging\")\n .version(cliVersion);\n\nprogram\n .command(\"migrate\")\n .description(\"Generate the audit_logs table migration\")\n .option(\n \"--dry-run\",\n \"Print SQL to stdout without writing a file\",\n )\n .option(\n \"--adapter <adapter>\",\n \"ORM adapter: drizzle or prisma (auto-detected from package.json)\",\n )\n .option(\n \"--dialect <dialect>\",\n \"Database dialect: postgres, mysql, or sqlite (auto-detected from DATABASE_URL)\",\n )\n .option(\"-o, --output <path>\", \"Output directory or .sql file path\")\n .action(\n async (opts: {\n dryRun?: boolean;\n adapter?: string;\n dialect?: string;\n output?: string;\n }) => {\n await runAction(() =>\n migrate({\n dryRun: opts.dryRun,\n adapter: parseAdapter(opts.adapter),\n dialect: parseDialect(opts.dialect),\n output: opts.output,\n }),\n );\n },\n );\n\nprogram\n .command(\"check\")\n .description(\"Verify the audit_logs table and ORM adapter are working\")\n .option(\"--verbose\", \"Show detailed results for each check\")\n .option(\n \"--database-url <url>\",\n \"Database URL (default: DATABASE_URL env)\",\n )\n .action(async (opts: { verbose?: boolean; databaseUrl?: string }) => {\n await runAction(() =>\n check({ verbose: opts.verbose, databaseUrl: opts.databaseUrl }),\n );\n });\n\nprogram\n .command(\"stats\")\n .description(\"Show audit log statistics\")\n .option(\"-c, --config <path>\", \"Config file path\", \"better-audit.config.ts\")\n .option(\"--since <period>\", \"Time window (e.g. 30d)\", \"30d\")\n .action(async (opts: { config: string; since: string }) => {\n await runAction(() => stats({ config: opts.config, since: opts.since }));\n });\n\nprogram\n .command(\"purge\")\n .description(\"Delete audit logs older than the retention period\")\n .option(\"-c, --config <path>\", \"Config file path\", \"better-audit.config.ts\")\n .option(\"--dry-run\", \"Preview rows to be deleted without deleting them\")\n .option(\n \"--since <value>\",\n 'ISO date (e.g. \"2025-01-01\") or duration shorthand (e.g. \"90d\") — overrides config',\n )\n .option(\"--batch-size <n>\", \"Rows per DELETE batch (default: 1000)\")\n .option(\n \"--database-url <url>\",\n \"Database URL (default: DATABASE_URL env)\",\n )\n .option(\"-y, --yes\", \"Skip confirmation prompt (required for live deletion)\")\n .action(\n async (opts: {\n config: string;\n dryRun?: boolean;\n since?: string;\n batchSize?: string;\n databaseUrl?: string;\n yes?: boolean;\n }) => {\n await runAction(async () => {\n const purgeOpts: Parameters<typeof purge>[0] = { config: opts.config };\n if (opts.dryRun !== undefined) {\n purgeOpts.dryRun = opts.dryRun;\n }\n if (opts.since !== undefined) {\n purgeOpts.since = opts.since;\n }\n if (opts.batchSize !== undefined) {\n const n = Number(opts.batchSize);\n if (!Number.isInteger(n) || n <= 0) {\n throw new Error(\n `Invalid --batch-size \"${opts.batchSize}\". Expected a positive integer.`,\n );\n }\n purgeOpts.batchSize = n;\n }\n if (opts.databaseUrl !== undefined) {\n purgeOpts.databaseUrl = opts.databaseUrl;\n }\n if (opts.yes !== undefined) {\n purgeOpts.yes = opts.yes;\n }\n await purge(purgeOpts);\n });\n },\n );\n\nprogram\n .command(\"export\")\n .description(\"Export audit log entries as CSV or JSON\")\n .option(\"--format <format>\", \"Output format: csv or json (default: csv)\")\n .option(\"-o, --output <path>\", \"Output file path (default: stdout)\")\n .option(\n \"--since <value>\",\n 'Duration (e.g. \"90d\") or ISO date (e.g. \"2025-01-01\")',\n )\n .option(\n \"--severity <level>\",\n \"Filter by severity: low, medium, high, or critical\",\n )\n .option(\n \"--compliance <tags>\",\n 'Comma-separated compliance tags (e.g. \"soc2:access-control,gdpr\")',\n )\n .option(\"--actor <id>\", \"Filter by actor ID\")\n .option(\"--limit <n>\", \"Maximum number of rows to export\")\n .option(\n \"--database-url <url>\",\n \"Database URL (default: DATABASE_URL env)\",\n )\n .action(\n async (opts: {\n format?: string;\n output?: string;\n since?: string;\n severity?: string;\n compliance?: string;\n actor?: string;\n limit?: string;\n databaseUrl?: string;\n }) => {\n await runAction(() => exportLogs(opts));\n },\n );\n\nprogram.parseAsync(process.argv).catch((err: unknown) => {\n console.error(err);\n process.exit(1);\n});\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { Command } from \"commander\";\nimport pc from \"picocolors\";\nimport { migrate } from \"./migrate.js\";\nimport { check, CheckFailedError } from \"./check.js\";\nimport { stats } from \"./stats.js\";\nimport { purge } from \"./purge.js\";\nimport { exportLogs } from \"./export.js\";\n\n// Read version from package.json at the package root\nconst cliDir = dirname(fileURLToPath(import.meta.url));\nconst pkgRaw: unknown = JSON.parse(\n readFileSync(join(cliDir, \"..\", \"package.json\"), \"utf-8\"),\n);\nconst cliVersion =\n typeof pkgRaw === \"object\" &&\n pkgRaw !== null &&\n \"version\" in pkgRaw &&\n typeof pkgRaw.version === \"string\"\n ? pkgRaw.version\n : \"0.0.0\";\n\n/** Validate and narrow the --adapter flag value. */\nfunction parseAdapter(\n value: string | undefined,\n): \"drizzle\" | \"prisma\" | undefined {\n if (value === \"drizzle\" || value === \"prisma\" || value === undefined) {\n return value;\n }\n throw new Error(\"--adapter must be 'drizzle' or 'prisma'\");\n}\n\n/** Validate and narrow the --dialect flag value. */\nfunction parseDialect(\n value: string | undefined,\n): \"postgres\" | \"mysql\" | \"sqlite\" | undefined {\n if (\n value === \"postgres\" ||\n value === \"mysql\" ||\n value === \"sqlite\" ||\n value === undefined\n ) {\n return value;\n }\n throw new Error(\"--dialect must be 'postgres', 'mysql', or 'sqlite'\");\n}\n\n/** Extract a human-readable message from an error, including AggregateError nested errors. */\nfunction formatErrorMessage(err: unknown): string {\n if (err instanceof AggregateError && err.errors.length > 0) {\n // AggregateError (e.g. from pg connection failure) has an empty .message\n // but contains nested errors with the actual details\n const nested = err.errors\n .map((e: unknown) => (e instanceof Error ? e.message : String(e)))\n .join(\"; \");\n return err.message !== \"\" ? `${err.message}: ${nested}` : nested;\n }\n if (err instanceof Error) {\n return err.message;\n }\n return String(err);\n}\n\n/** Wrap a subcommand action with consistent error handling. */\nasync function runAction(fn: () => Promise<void>): Promise<void> {\n try {\n await fn();\n } catch (err) {\n // CheckFailedError already printed its output — just exit\n if (err instanceof CheckFailedError) {\n process.exit(1);\n }\n const message = formatErrorMessage(err);\n console.error(pc.red(message));\n process.exit(1);\n }\n}\n\nconst program = new Command()\n .name(\"@usebetterdev/audit-cli\")\n .description(\"CLI for @usebetterdev/audit — compliance-ready audit logging\")\n .version(cliVersion);\n\nprogram\n .command(\"migrate\")\n .description(\"Generate the audit_logs table migration\")\n .option(\n \"--dry-run\",\n \"Print SQL to stdout without writing a file\",\n )\n .option(\n \"--adapter <adapter>\",\n \"ORM adapter: drizzle or prisma (auto-detected from package.json)\",\n )\n .option(\n \"--dialect <dialect>\",\n \"Database dialect: postgres, mysql, or sqlite (auto-detected from DATABASE_URL)\",\n )\n .option(\"-o, --output <path>\", \"Output directory or .sql file path\")\n .action(\n async (opts: {\n dryRun?: boolean;\n adapter?: string;\n dialect?: string;\n output?: string;\n }) => {\n await runAction(() =>\n migrate({\n dryRun: opts.dryRun,\n adapter: parseAdapter(opts.adapter),\n dialect: parseDialect(opts.dialect),\n output: opts.output,\n }),\n );\n },\n );\n\nprogram\n .command(\"check\")\n .description(\"Verify the audit_logs table and ORM adapter are working\")\n .option(\"--verbose\", \"Show detailed results for each check\")\n .option(\n \"--database-url <url>\",\n \"Database URL (default: DATABASE_URL env)\",\n )\n .action(async (opts: { verbose?: boolean; databaseUrl?: string }) => {\n await runAction(() =>\n check({ verbose: opts.verbose, databaseUrl: opts.databaseUrl }),\n );\n });\n\nprogram\n .command(\"stats\")\n .description(\"Show audit log statistics\")\n .option(\"--since <period>\", \"Time window (e.g. 30d)\", \"30d\")\n .action(async (opts: { since: string }) => {\n await runAction(() => stats({ since: opts.since }));\n });\n\nprogram\n .command(\"purge\")\n .description(\"Delete audit logs older than the retention period\")\n .option(\"--dry-run\", \"Preview rows to be deleted without deleting them\")\n .option(\n \"--since <value>\",\n 'ISO date (e.g. \"2025-01-01\") or duration shorthand (e.g. \"90d\") — overrides config',\n )\n .option(\"--batch-size <n>\", \"Rows per DELETE batch (default: 1000)\")\n .option(\n \"--database-url <url>\",\n \"Database URL (default: DATABASE_URL env)\",\n )\n .option(\"-y, --yes\", \"Skip confirmation prompt (required for live deletion)\")\n .action(\n async (opts: {\n dryRun?: boolean;\n since?: string;\n batchSize?: string;\n databaseUrl?: string;\n yes?: boolean;\n }) => {\n await runAction(async () => {\n const purgeOpts: Parameters<typeof purge>[0] = {};\n if (opts.dryRun !== undefined) {\n purgeOpts.dryRun = opts.dryRun;\n }\n if (opts.since !== undefined) {\n purgeOpts.since = opts.since;\n }\n if (opts.batchSize !== undefined) {\n const n = Number(opts.batchSize);\n if (!Number.isInteger(n) || n <= 0) {\n throw new Error(\n `Invalid --batch-size \"${opts.batchSize}\". Expected a positive integer.`,\n );\n }\n purgeOpts.batchSize = n;\n }\n if (opts.databaseUrl !== undefined) {\n purgeOpts.databaseUrl = opts.databaseUrl;\n }\n if (opts.yes !== undefined) {\n purgeOpts.yes = opts.yes;\n }\n await purge(purgeOpts);\n });\n },\n );\n\nprogram\n .command(\"export\")\n .description(\"Export audit log entries as CSV or JSON\")\n .option(\"--format <format>\", \"Output format: csv or json (default: csv)\")\n .option(\"-o, --output <path>\", \"Output file path (default: stdout)\")\n .option(\n \"--since <value>\",\n 'Duration (e.g. \"90d\") or ISO date (e.g. \"2025-01-01\")',\n )\n .option(\n \"--severity <level>\",\n \"Filter by severity: low, medium, high, or critical\",\n )\n .option(\n \"--compliance <tags>\",\n 'Comma-separated compliance tags (e.g. \"soc2:access-control,gdpr\")',\n )\n .option(\"--actor <id>\", \"Filter by actor ID\")\n .option(\"--limit <n>\", \"Maximum number of rows to export\")\n .option(\n \"--database-url <url>\",\n \"Database URL (default: DATABASE_URL env)\",\n )\n .action(\n async (opts: {\n format?: string;\n output?: string;\n since?: string;\n severity?: string;\n compliance?: string;\n actor?: string;\n limit?: string;\n databaseUrl?: string;\n }) => {\n await runAction(() => exportLogs(opts));\n },\n );\n\nprogram.parseAsync(process.argv).catch((err: unknown) => {\n console.error(err);\n process.exit(1);\n});\n"],"mappings":"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","names":[]}
package/dist/purge.d.ts CHANGED
@@ -7,7 +7,6 @@
7
7
  * Always run with --dry-run first to preview what will be deleted.
8
8
  */
9
9
  interface PurgeOptions {
10
- config?: string;
11
10
  /** Preview rows to be deleted without deleting them. */
12
11
  dryRun?: boolean;
13
12
  /** ISO date string (e.g. "2025-01-01") or duration shorthand (e.g. "90d", "1y"). */
@@ -39,7 +38,7 @@ declare function parseSinceValue(value: string): Date;
39
38
  */
40
39
  declare function resolveCutoffDate(options: {
41
40
  since?: string;
42
- config?: string;
41
+ cwd?: string;
43
42
  }): Promise<Date>;
44
43
  /**
45
44
  * Format elapsed milliseconds to a human-readable string.
package/dist/purge.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  parseSinceValue,
4
4
  purge,
5
5
  resolveCutoffDate
6
- } from "./chunk-SJSGTCG4.js";
6
+ } from "./chunk-BKCJJOQN.js";
7
7
  import "./chunk-7GSN73TA.js";
8
8
  import "./chunk-HDO5P6X7.js";
9
9
  export {
package/dist/stats.d.ts CHANGED
@@ -11,7 +11,6 @@
11
11
  * TODO: Connect to DB and run aggregation queries.
12
12
  */
13
13
  interface StatsOptions {
14
- config?: string;
15
14
  since?: string;
16
15
  }
17
16
  declare function stats(options?: StatsOptions): Promise<void>;
package/dist/stats.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  stats
3
- } from "./chunk-AGFBL646.js";
3
+ } from "./chunk-UASMKVFP.js";
4
4
  export {
5
5
  stats
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usebetterdev/audit-cli",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "repository": "github:usebetter-dev/usebetter",
5
5
  "bugs": "https://github.com/usebetter-dev/usebetter/issues",
6
6
  "homepage": "https://github.com/usebetter-dev/usebetter#readme",
@@ -22,7 +22,8 @@
22
22
  "commander": "^12.1.0",
23
23
  "kysely": "^0.28.11",
24
24
  "picocolors": "^1.1.0",
25
- "@usebetterdev/audit-core": "0.5.0"
25
+ "@usebetterdev/audit-core": "0.6.1",
26
+ "@usebetterdev/plugin": "0.7.0"
26
27
  },
27
28
  "peerDependencies": {
28
29
  "pg": ">=8.0.0",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/purge.ts","../src/config-loader.ts"],"sourcesContent":["/**\n * `better-audit purge` — Delete audit logs older than the configured retention period.\n *\n * Connects to the database directly via Kysely (no ORM adapter needed).\n * Executes batched DELETEs to avoid holding long row-level locks on large tables.\n *\n * Always run with --dry-run first to preview what will be deleted.\n */\n\nimport pc from \"picocolors\";\nimport { createKyselyInstance } from \"./sql-executor.js\";\nimport { detectDialect } from \"./detect-adapter.js\";\nimport { loadConfig } from \"./config-loader.js\";\nimport type { Database } from \"./sql-executor.js\";\nimport type { Kysely } from \"kysely\";\n\nconst ISO_DATE_REGEX = /^\\d{4}-\\d{2}-\\d{2}(T[\\w:.+-]+)?$/;\nconst DURATION_REGEX = /^(\\d+)(d|w|m|y)$/i;\nconst DEFAULT_BATCH_SIZE = 1000;\nconst MAX_BATCHES = 100_000;\n\nexport interface PurgeOptions {\n config?: string;\n /** Preview rows to be deleted without deleting them. */\n dryRun?: boolean;\n /** ISO date string (e.g. \"2025-01-01\") or duration shorthand (e.g. \"90d\", \"1y\"). */\n since?: string;\n /** Rows per DELETE batch. Default: 1000. */\n batchSize?: number;\n /** Database URL (default: DATABASE_URL env). */\n databaseUrl?: string;\n /** Skip confirmation prompt (required for live deletion). */\n yes?: boolean;\n}\n\n/**\n * Parse a `--since` value to an absolute cutoff `Date`.\n *\n * Accepts:\n * - ISO-8601 date strings: \"2025-01-01\" or \"2025-01-01T00:00:00Z\"\n * - Duration shorthands: \"90d\", \"4w\", \"3m\", \"1y\"\n *\n * Exported for testing.\n */\nexport function parseSinceValue(value: string): Date {\n if (ISO_DATE_REGEX.test(value)) {\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) {\n throw new Error(`Invalid date \"${value}\". Expected ISO-8601 format (e.g. \"2025-01-01\").`);\n }\n return date;\n }\n\n const match = DURATION_REGEX.exec(value);\n if (match !== null) {\n const amount = parseInt(match[1]!, 10);\n if (amount <= 0) {\n throw new Error(\n `Invalid --since value \"${value}\". Duration amount must be greater than zero.`,\n );\n }\n const unit = match[2]!.toLowerCase();\n const now = new Date();\n if (unit === \"d\") {\n now.setDate(now.getDate() - amount);\n } else if (unit === \"w\") {\n now.setDate(now.getDate() - amount * 7);\n } else if (unit === \"m\") {\n now.setMonth(now.getMonth() - amount);\n } else {\n // y\n now.setFullYear(now.getFullYear() - amount);\n }\n return now;\n }\n\n throw new Error(\n `Invalid --since value \"${value}\". ` +\n `Expected an ISO date (e.g. \"2025-01-01\") or duration shorthand (e.g. \"90d\", \"4w\", \"3m\", \"1y\").`,\n );\n}\n\n/**\n * Resolve the cutoff date from options.\n *\n * Priority: `--since` flag > config `retention.days`.\n * Throws if neither is available.\n *\n * Exported for testing.\n */\nexport async function resolveCutoffDate(options: {\n since?: string;\n config?: string;\n}): Promise<Date> {\n if (options.since !== undefined) {\n return parseSinceValue(options.since);\n }\n\n if (options.config !== undefined) {\n const config = await loadConfig(options.config);\n if (config?.retention !== undefined) {\n const cutoff = new Date();\n cutoff.setDate(cutoff.getDate() - config.retention.days);\n return cutoff;\n }\n }\n\n throw new Error(\n \"No retention policy configured. \" +\n \"Pass --since <date|duration> or set retention.days in your config file.\",\n );\n}\n\n/**\n * Count rows eligible for deletion (for --dry-run).\n */\nasync function countEligibleRows(\n db: Kysely<Database>,\n before: Date,\n): Promise<number> {\n const result = await db\n .selectFrom(\"audit_logs\")\n .select((eb) => eb.fn.countAll<string>().as(\"count\"))\n .where(\"timestamp\", \"<\", before)\n .executeTakeFirst();\n return result !== undefined ? Number(result.count) : 0;\n}\n\n/**\n * Delete one batch of rows older than `before`.\n *\n * Uses `DELETE … WHERE id IN (SELECT id … LIMIT n)` on all dialects.\n * This avoids the lack of LIMIT support on DELETE in Postgres and SQLite.\n *\n * Returns the number of rows deleted in this batch.\n */\nasync function deleteBatch(\n db: Kysely<Database>,\n before: Date,\n batchSize: number,\n): Promise<number> {\n const subquery = db\n .selectFrom(\"audit_logs\")\n .select(\"id\")\n .where(\"timestamp\", \"<\", before)\n .limit(batchSize);\n\n const result = await db\n .deleteFrom(\"audit_logs\")\n .where(\"id\", \"in\", subquery)\n .executeTakeFirst();\n\n return Number(result.numDeletedRows);\n}\n\n/**\n * Format a number with locale-aware thousands separators.\n */\nfunction formatCount(n: number): string {\n return n.toLocaleString(\"en-US\");\n}\n\n/**\n * Format elapsed milliseconds to a human-readable string.\n *\n * Exported for testing.\n */\nexport function formatDuration(ms: number): string {\n if (ms < 1000) {\n return `${ms}ms`;\n }\n return `${(ms / 1000).toFixed(1)}s`;\n}\n\nexport async function purge(options: PurgeOptions = {}): Promise<void> {\n // 1. Resolve database URL\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 // 2. Resolve the cutoff date\n // Only consult config when --since is absent (short-circuit when --since is set)\n const resolvOpts: { since?: string; config?: string } = {};\n if (options.since !== undefined) {\n resolvOpts.since = options.since;\n } else if (options.config !== undefined) {\n resolvOpts.config = options.config;\n }\n const before = await resolveCutoffDate(resolvOpts);\n\n // 3. Validate cutoff is in the past\n if (before > new Date()) {\n throw new Error(\n `Cutoff date ${before.toISOString().split(\"T\")[0]!} is in the future. ` +\n `Purge only accepts past dates.`,\n );\n }\n\n const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;\n if (!Number.isInteger(batchSize) || batchSize <= 0) {\n throw new Error(`--batch-size must be a positive integer, got ${String(batchSize)}`);\n }\n\n // 4. Connect\n const dialect = detectDialect(databaseUrl);\n const db = await createKyselyInstance(databaseUrl, dialect);\n\n const startTime = Date.now();\n\n try {\n // 4. Dry run — count only, no confirmation needed\n if (options.dryRun) {\n const count = await countEligibleRows(db, before);\n process.stderr.write(\n `${pc.cyan(\"→\")} ${pc.bold(formatCount(count))} rows would be deleted` +\n ` (cutoff: ${pc.dim(before.toISOString().split(\"T\")[0]!)})\\n`,\n );\n process.stderr.write(`${pc.dim(\" Dry run — no changes made.\")}\\n`);\n return;\n }\n\n // 5. Confirmation guard — require --yes for live deletion\n if (!options.yes) {\n const count = await countEligibleRows(db, before);\n process.stderr.write(\n `${pc.yellow(\"!\")} ${pc.bold(formatCount(count))} rows will be deleted` +\n ` (cutoff: ${pc.dim(before.toISOString().split(\"T\")[0]!)})\\n` +\n ` Run with ${pc.bold(\"--yes\")} to confirm deletion, or ${pc.bold(\"--dry-run\")} to preview.\\n`,\n );\n return;\n }\n\n // 6. Batched delete loop\n let totalDeleted = 0;\n let batchNumber = 0;\n\n while (batchNumber < MAX_BATCHES) {\n const deleted = await deleteBatch(db, before, batchSize);\n totalDeleted += deleted;\n batchNumber++;\n\n if (batchNumber % 10 === 0) {\n process.stderr.write(\n ` ${pc.dim(`batch ${batchNumber}: ${formatCount(totalDeleted)} rows deleted so far...`)}\\n`,\n );\n }\n\n if (deleted < batchSize) {\n break;\n }\n }\n\n if (batchNumber >= MAX_BATCHES) {\n process.stderr.write(\n pc.yellow(\n ` Warning: reached max batch limit (${MAX_BATCHES.toLocaleString(\"en-US\")}). Some rows may remain.\\n`,\n ),\n );\n }\n\n const elapsed = Date.now() - startTime;\n\n // 7. Summary\n process.stderr.write(`${pc.green(\"✓\")} Purge complete\\n`);\n process.stderr.write(` Rows deleted: ${pc.bold(formatCount(totalDeleted))}\\n`);\n process.stderr.write(` Cutoff date: ${pc.dim(before.toISOString().split(\"T\")[0]!)}\\n`);\n process.stderr.write(` Time taken: ${pc.dim(formatDuration(elapsed))}\\n`);\n } finally {\n await db.destroy();\n }\n}\n","/**\n * Loader for `BetterAuditConfig` from a user-supplied config file.\n *\n * Dynamically imports the config file and validates the `retention` policy.\n * Returns `null` when the file does not exist — callers may fall back to CLI flags.\n *\n * TypeScript config files (`.ts`) require the Node process to be started with\n * `--import tsx` or an equivalent TypeScript loader. Compiled JS config files\n * (`.js`, `.mjs`, `.cjs`) work without any loader.\n */\n\nimport { stat } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport type { BetterAuditConfig } from \"@usebetterdev/audit-core\";\n\n/**\n * Dynamic module import helper — uses a variable to prevent bundlers from\n * resolving the path at compile time.\n */\nfunction importFile(path: string): Promise<{ default?: unknown }> {\n return import(/* webpackIgnore: true */ path) as Promise<{ default?: unknown }>;\n}\n\nfunction validateRetention(retention: unknown): void {\n if (typeof retention !== \"object\" || retention === null) {\n throw new Error(\"retention must be an object with a 'days' property\");\n }\n const r = retention as Record<string, unknown>;\n if (!Number.isInteger(r[\"days\"]) || !Number.isFinite(r[\"days\"] as number) || (r[\"days\"] as number) <= 0) {\n throw new Error(`retention.days must be a positive integer, got ${String(r[\"days\"])}`);\n }\n if (r[\"tables\"] !== undefined) {\n if (\n !Array.isArray(r[\"tables\"]) ||\n r[\"tables\"].length === 0 ||\n r[\"tables\"].some((t: unknown) => typeof t !== \"string\" || t === \"\")\n ) {\n throw new Error(\"retention.tables must be a non-empty array of non-empty strings\");\n }\n }\n}\n\n/**\n * Loads a `BetterAuditConfig` from the given path.\n *\n * Returns `null` if the file does not exist.\n * Throws if the file exists but cannot be loaded or has an invalid shape.\n */\nexport async function loadConfig(configPath: string): Promise<BetterAuditConfig | null> {\n const resolved = resolve(process.cwd(), configPath);\n\n let mod: { default?: unknown };\n try {\n mod = await importFile(resolved);\n } catch (err) {\n // Categorise the error: did the file simply not exist?\n // We stat after the failed import to avoid TOCTOU — the stat result only\n // classifies an error that already occurred; it does not gate the import.\n const exists = await stat(resolved).then(() => true, () => false);\n if (!exists) {\n // File does not exist — caller may fall back to CLI flags\n return null;\n }\n const message = err instanceof Error ? err.message : String(err);\n throw new Error(\n `Failed to load config file \"${configPath}\": ${message}\\n` +\n `Hint: TypeScript config files require a TypeScript loader. ` +\n `Run with \"tsx\" or \"ts-node\", or compile your config first.`,\n );\n }\n\n const config = mod.default;\n\n if (typeof config !== \"object\" || config === null) {\n throw new Error(\n `Config file \"${configPath}\" must export a BetterAuditConfig as the default export.`,\n );\n }\n\n const configObj = config as Record<string, unknown>;\n if (configObj[\"retention\"] !== undefined) {\n validateRetention(configObj[\"retention\"]);\n }\n\n return config as BetterAuditConfig;\n}\n"],"mappings":";;;;;;;;AASA,OAAO,QAAQ;;;ACEf,SAAS,YAAY;AACrB,SAAS,eAAe;AAOxB,SAAS,WAAW,MAA8C;AAChE,SAAO;AAAA;AAAA,IAAiC;AAAA;AAC1C;AAEA,SAAS,kBAAkB,WAA0B;AACnD,MAAI,OAAO,cAAc,YAAY,cAAc,MAAM;AACvD,UAAM,IAAI,MAAM,oDAAoD;AAAA,EACtE;AACA,QAAM,IAAI;AACV,MAAI,CAAC,OAAO,UAAU,EAAE,MAAM,CAAC,KAAK,CAAC,OAAO,SAAS,EAAE,MAAM,CAAW,KAAM,EAAE,MAAM,KAAgB,GAAG;AACvG,UAAM,IAAI,MAAM,kDAAkD,OAAO,EAAE,MAAM,CAAC,CAAC,EAAE;AAAA,EACvF;AACA,MAAI,EAAE,QAAQ,MAAM,QAAW;AAC7B,QACE,CAAC,MAAM,QAAQ,EAAE,QAAQ,CAAC,KAC1B,EAAE,QAAQ,EAAE,WAAW,KACvB,EAAE,QAAQ,EAAE,KAAK,CAAC,MAAe,OAAO,MAAM,YAAY,MAAM,EAAE,GAClE;AACA,YAAM,IAAI,MAAM,iEAAiE;AAAA,IACnF;AAAA,EACF;AACF;AAQA,eAAsB,WAAW,YAAuD;AACtF,QAAM,WAAW,QAAQ,QAAQ,IAAI,GAAG,UAAU;AAElD,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,WAAW,QAAQ;AAAA,EACjC,SAAS,KAAK;AAIZ,UAAM,SAAS,MAAM,KAAK,QAAQ,EAAE,KAAK,MAAM,MAAM,MAAM,KAAK;AAChE,QAAI,CAAC,QAAQ;AAEX,aAAO;AAAA,IACT;AACA,UAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,UAAM,IAAI;AAAA,MACR,+BAA+B,UAAU,MAAM,OAAO;AAAA;AAAA,IAGxD;AAAA,EACF;AAEA,QAAM,SAAS,IAAI;AAEnB,MAAI,OAAO,WAAW,YAAY,WAAW,MAAM;AACjD,UAAM,IAAI;AAAA,MACR,gBAAgB,UAAU;AAAA,IAC5B;AAAA,EACF;AAEA,QAAM,YAAY;AAClB,MAAI,UAAU,WAAW,MAAM,QAAW;AACxC,sBAAkB,UAAU,WAAW,CAAC;AAAA,EAC1C;AAEA,SAAO;AACT;;;ADrEA,IAAM,iBAAiB;AACvB,IAAM,iBAAiB;AACvB,IAAM,qBAAqB;AAC3B,IAAM,cAAc;AAyBb,SAAS,gBAAgB,OAAqB;AACnD,MAAI,eAAe,KAAK,KAAK,GAAG;AAC9B,UAAM,OAAO,IAAI,KAAK,KAAK;AAC3B,QAAI,OAAO,MAAM,KAAK,QAAQ,CAAC,GAAG;AAChC,YAAM,IAAI,MAAM,iBAAiB,KAAK,kDAAkD;AAAA,IAC1F;AACA,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,eAAe,KAAK,KAAK;AACvC,MAAI,UAAU,MAAM;AAClB,UAAM,SAAS,SAAS,MAAM,CAAC,GAAI,EAAE;AACrC,QAAI,UAAU,GAAG;AACf,YAAM,IAAI;AAAA,QACR,0BAA0B,KAAK;AAAA,MACjC;AAAA,IACF;AACA,UAAM,OAAO,MAAM,CAAC,EAAG,YAAY;AACnC,UAAM,MAAM,oBAAI,KAAK;AACrB,QAAI,SAAS,KAAK;AAChB,UAAI,QAAQ,IAAI,QAAQ,IAAI,MAAM;AAAA,IACpC,WAAW,SAAS,KAAK;AACvB,UAAI,QAAQ,IAAI,QAAQ,IAAI,SAAS,CAAC;AAAA,IACxC,WAAW,SAAS,KAAK;AACvB,UAAI,SAAS,IAAI,SAAS,IAAI,MAAM;AAAA,IACtC,OAAO;AAEL,UAAI,YAAY,IAAI,YAAY,IAAI,MAAM;AAAA,IAC5C;AACA,WAAO;AAAA,EACT;AAEA,QAAM,IAAI;AAAA,IACR,0BAA0B,KAAK;AAAA,EAEjC;AACF;AAUA,eAAsB,kBAAkB,SAGtB;AAChB,MAAI,QAAQ,UAAU,QAAW;AAC/B,WAAO,gBAAgB,QAAQ,KAAK;AAAA,EACtC;AAEA,MAAI,QAAQ,WAAW,QAAW;AAChC,UAAM,SAAS,MAAM,WAAW,QAAQ,MAAM;AAC9C,QAAI,QAAQ,cAAc,QAAW;AACnC,YAAM,SAAS,oBAAI,KAAK;AACxB,aAAO,QAAQ,OAAO,QAAQ,IAAI,OAAO,UAAU,IAAI;AACvD,aAAO;AAAA,IACT;AAAA,EACF;AAEA,QAAM,IAAI;AAAA,IACR;AAAA,EAEF;AACF;AAKA,eAAe,kBACb,IACA,QACiB;AACjB,QAAM,SAAS,MAAM,GAClB,WAAW,YAAY,EACvB,OAAO,CAAC,OAAO,GAAG,GAAG,SAAiB,EAAE,GAAG,OAAO,CAAC,EACnD,MAAM,aAAa,KAAK,MAAM,EAC9B,iBAAiB;AACpB,SAAO,WAAW,SAAY,OAAO,OAAO,KAAK,IAAI;AACvD;AAUA,eAAe,YACb,IACA,QACA,WACiB;AACjB,QAAM,WAAW,GACd,WAAW,YAAY,EACvB,OAAO,IAAI,EACX,MAAM,aAAa,KAAK,MAAM,EAC9B,MAAM,SAAS;AAElB,QAAM,SAAS,MAAM,GAClB,WAAW,YAAY,EACvB,MAAM,MAAM,MAAM,QAAQ,EAC1B,iBAAiB;AAEpB,SAAO,OAAO,OAAO,cAAc;AACrC;AAKA,SAAS,YAAY,GAAmB;AACtC,SAAO,EAAE,eAAe,OAAO;AACjC;AAOO,SAAS,eAAe,IAAoB;AACjD,MAAI,KAAK,KAAM;AACb,WAAO,GAAG,EAAE;AAAA,EACd;AACA,SAAO,IAAI,KAAK,KAAM,QAAQ,CAAC,CAAC;AAClC;AAEA,eAAsB,MAAM,UAAwB,CAAC,GAAkB;AAErE,QAAM,cAAc,QAAQ,eAAe,QAAQ,IAAI,cAAc;AACrE,MAAI,gBAAgB,UAAa,gBAAgB,IAAI;AACnD,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAIA,QAAM,aAAkD,CAAC;AACzD,MAAI,QAAQ,UAAU,QAAW;AAC/B,eAAW,QAAQ,QAAQ;AAAA,EAC7B,WAAW,QAAQ,WAAW,QAAW;AACvC,eAAW,SAAS,QAAQ;AAAA,EAC9B;AACA,QAAM,SAAS,MAAM,kBAAkB,UAAU;AAGjD,MAAI,SAAS,oBAAI,KAAK,GAAG;AACvB,UAAM,IAAI;AAAA,MACR,eAAe,OAAO,YAAY,EAAE,MAAM,GAAG,EAAE,CAAC,CAAE;AAAA,IAEpD;AAAA,EACF;AAEA,QAAM,YAAY,QAAQ,aAAa;AACvC,MAAI,CAAC,OAAO,UAAU,SAAS,KAAK,aAAa,GAAG;AAClD,UAAM,IAAI,MAAM,gDAAgD,OAAO,SAAS,CAAC,EAAE;AAAA,EACrF;AAGA,QAAM,UAAU,cAAc,WAAW;AACzC,QAAM,KAAK,MAAM,qBAAqB,aAAa,OAAO;AAE1D,QAAM,YAAY,KAAK,IAAI;AAE3B,MAAI;AAEF,QAAI,QAAQ,QAAQ;AAClB,YAAM,QAAQ,MAAM,kBAAkB,IAAI,MAAM;AAChD,cAAQ,OAAO;AAAA,QACb,GAAG,GAAG,KAAK,QAAG,CAAC,IAAI,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,mCACjC,GAAG,IAAI,OAAO,YAAY,EAAE,MAAM,GAAG,EAAE,CAAC,CAAE,CAAC;AAAA;AAAA,MAC1D;AACA,cAAQ,OAAO,MAAM,GAAG,GAAG,IAAI,mCAA8B,CAAC;AAAA,CAAI;AAClE;AAAA,IACF;AAGA,QAAI,CAAC,QAAQ,KAAK;AAChB,YAAM,QAAQ,MAAM,kBAAkB,IAAI,MAAM;AAChD,cAAQ,OAAO;AAAA,QACb,GAAG,GAAG,OAAO,GAAG,CAAC,IAAI,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,kCACnC,GAAG,IAAI,OAAO,YAAY,EAAE,MAAM,GAAG,EAAE,CAAC,CAAE,CAAC;AAAA,aAC1C,GAAG,KAAK,OAAO,CAAC,4BAA4B,GAAG,KAAK,WAAW,CAAC;AAAA;AAAA,MAChF;AACA;AAAA,IACF;AAGA,QAAI,eAAe;AACnB,QAAI,cAAc;AAElB,WAAO,cAAc,aAAa;AAChC,YAAM,UAAU,MAAM,YAAY,IAAI,QAAQ,SAAS;AACvD,sBAAgB;AAChB;AAEA,UAAI,cAAc,OAAO,GAAG;AAC1B,gBAAQ,OAAO;AAAA,UACb,KAAK,GAAG,IAAI,SAAS,WAAW,KAAK,YAAY,YAAY,CAAC,yBAAyB,CAAC;AAAA;AAAA,QAC1F;AAAA,MACF;AAEA,UAAI,UAAU,WAAW;AACvB;AAAA,MACF;AAAA,IACF;AAEA,QAAI,eAAe,aAAa;AAC9B,cAAQ,OAAO;AAAA,QACb,GAAG;AAAA,UACD,uCAAuC,YAAY,eAAe,OAAO,CAAC;AAAA;AAAA,QAC5E;AAAA,MACF;AAAA,IACF;AAEA,UAAM,UAAU,KAAK,IAAI,IAAI;AAG7B,YAAQ,OAAO,MAAM,GAAG,GAAG,MAAM,QAAG,CAAC;AAAA,CAAmB;AACxD,YAAQ,OAAO,MAAM,oBAAoB,GAAG,KAAK,YAAY,YAAY,CAAC,CAAC;AAAA,CAAI;AAC/E,YAAQ,OAAO,MAAM,oBAAoB,GAAG,IAAI,OAAO,YAAY,EAAE,MAAM,GAAG,EAAE,CAAC,CAAE,CAAC;AAAA,CAAI;AACxF,YAAQ,OAAO,MAAM,oBAAoB,GAAG,IAAI,eAAe,OAAO,CAAC,CAAC;AAAA,CAAI;AAAA,EAC9E,UAAE;AACA,UAAM,GAAG,QAAQ;AAAA,EACnB;AACF;","names":[]}