@hasna/hooks 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.
Files changed (37) hide show
  1. package/README.md +81 -8
  2. package/bin/index.js +1756 -382
  3. package/bin/serve.js +5258 -0
  4. package/dist/cf/provision.d.ts +24 -0
  5. package/dist/config.d.ts +18 -0
  6. package/dist/db/legacy-import.d.ts +1 -1
  7. package/dist/db/migrations/004_hooks_table.d.ts +9 -0
  8. package/dist/db/pg-migrations.d.ts +1 -1
  9. package/dist/db/storage-sync.d.ts +26 -6
  10. package/dist/index.d.ts +18 -2
  11. package/dist/index.js +5351 -286
  12. package/dist/lib/custom-install.d.ts +19 -0
  13. package/dist/lib/manifest.d.ts +70 -0
  14. package/dist/lib/resolve.d.ts +19 -0
  15. package/dist/lib/run.d.ts +37 -0
  16. package/dist/lib/store.d.ts +69 -0
  17. package/dist/lib/sync.d.ts +35 -0
  18. package/dist/serve.d.ts +36 -0
  19. package/dist/storage.d.ts +2 -2
  20. package/dist/storage.js +133 -42
  21. package/hooks/codewith-native-common.test.ts +15 -2
  22. package/hooks/hook-scanoutput/README.md +151 -0
  23. package/hooks/hook-scanoutput/package.json +12 -0
  24. package/hooks/hook-scanoutput/src/hook.test.ts +217 -0
  25. package/hooks/hook-scanoutput/src/hook.ts +319 -0
  26. package/hooks/hook-workspace-repos-guard/README.md +63 -0
  27. package/hooks/hook-workspace-repos-guard/package.json +12 -0
  28. package/hooks/hook-workspace-repos-guard/src/hook.test.ts +312 -0
  29. package/hooks/hook-workspace-repos-guard/src/hook.ts +352 -0
  30. package/hooks/hook-workspace-repos-guard/tsconfig.json +21 -0
  31. package/hooks/mention-context/README.md +109 -0
  32. package/hooks/mention-context/package.json +9 -0
  33. package/hooks/mention-context/src/hasna-mention-context.py +1218 -0
  34. package/hooks/mention-context/src/hasna-mention-warm.py +521 -0
  35. package/hooks/mention-context/src/hook.test.ts +68 -0
  36. package/hooks/mention-context/src/test_run_capture.py +345 -0
  37. package/package.json +9 -5
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Custom hook install — sources outside the bundled registry.
3
+ *
4
+ * git URL -> shallow clone, read manifest, copy into the custom dir
5
+ * local -> copy a directory containing manifest.json
6
+ * https -> fetch manifest.json (+ relative script file when needed)
7
+ */
8
+ export type CustomSourceKind = "git" | "local" | "url";
9
+ export interface CustomInstallResult {
10
+ name: string;
11
+ version: string;
12
+ kind: CustomSourceKind;
13
+ source: string;
14
+ dir: string;
15
+ scriptPath: string;
16
+ }
17
+ export declare function isCustomSource(value: string): boolean;
18
+ export declare function installCustomSource(source: string): Promise<CustomInstallResult>;
19
+ export declare function ensureCustomDir(): string;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Hook manifest model — the contract for custom/remote hooks.
3
+ */
4
+ import { z } from "zod";
5
+ export declare const HOOK_NAME_RE: RegExp;
6
+ export declare const manifestSchema: z.ZodObject<{
7
+ name: z.ZodString;
8
+ version: z.ZodString;
9
+ description: z.ZodOptional<z.ZodString>;
10
+ events: z.ZodArray<z.ZodString, "many">;
11
+ script: z.ZodString;
12
+ args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
13
+ timeout_ms: z.ZodOptional<z.ZodNumber>;
14
+ }, "strip", z.ZodTypeAny, {
15
+ version: string;
16
+ events: string[];
17
+ name: string;
18
+ script: string;
19
+ description?: string | undefined;
20
+ args?: string[] | undefined;
21
+ timeout_ms?: number | undefined;
22
+ }, {
23
+ version: string;
24
+ events: string[];
25
+ name: string;
26
+ script: string;
27
+ description?: string | undefined;
28
+ args?: string[] | undefined;
29
+ timeout_ms?: number | undefined;
30
+ }>;
31
+ export type HookManifest = z.infer<typeof manifestSchema>;
32
+ export interface ParsedManifest {
33
+ manifest: HookManifest;
34
+ scriptPath: string;
35
+ scriptContent: string;
36
+ scriptIsInline: boolean;
37
+ }
38
+ export declare function parseManifest(json: string): HookManifest;
39
+ export declare function normalizeManifestName(name: string): string;
40
+ export declare function shortManifestName(name: string): string;
41
+ /**
42
+ * A manifest script path resolved outside its hook's own directory.
43
+ * Carries the offending path so callers can surface it verbatim.
44
+ */
45
+ export declare class ScriptContainmentError extends Error {
46
+ constructor(path: string, hookDir: string, raw?: string);
47
+ }
48
+ /**
49
+ * Full containment check (lexical + symlink resolution). Requires `root` to
50
+ * exist. For write targets that do not exist yet, call
51
+ * assertContainedLexical first (before creating anything), then create the
52
+ * root, then call assertContainedReal.
53
+ */
54
+ export declare function assertContained(target: string, root: string, raw?: string): string;
55
+ /**
56
+ * Resolve a manifest's script to (relative path, content).
57
+ * A script value containing a newline is inline content; anything else is a
58
+ * relative path resolved against the manifest directory.
59
+ */
60
+ export declare function resolveScript(manifest: HookManifest, manifestDir: string): {
61
+ path: string;
62
+ content: string;
63
+ };
64
+ export declare function customHookDir(name: string): string;
65
+ export declare function readCustomManifest(name: string): ParsedManifest | undefined;
66
+ export declare function listCustomHooks(): ParsedManifest[];
67
+ export declare function writeCustomHook(name: string, manifest: HookManifest, scriptContent: string, scriptRelPath: string): {
68
+ dir: string;
69
+ scriptPath: string;
70
+ };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Hook resolution — custom directory first, bundled registry second.
3
+ * A custom hook that collides with a bundled name shadows it.
4
+ */
5
+ import { type HookMeta } from "./registry.js";
6
+ export type HookSource = "custom" | "bundled";
7
+ export interface ResolvedHook {
8
+ name: string;
9
+ source: HookSource;
10
+ version: string;
11
+ events: string[];
12
+ description: string;
13
+ scriptPath: string;
14
+ meta: HookMeta;
15
+ }
16
+ export declare function resolveHookDir(name: string): string | undefined;
17
+ export declare function resolveScriptPath(name: string): string | undefined;
18
+ export declare function resolveHookMeta(name: string): HookMeta | undefined;
19
+ export declare function resolveHook(name: string): ResolvedHook | undefined;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Verified hook execution — the verified bytes are the executed bytes.
3
+ *
4
+ * The trust check hashes the script content once; executing a PATH afterwards
5
+ * would re-open the file and could run different bytes (TOCTOU). So the
6
+ * verified content is written to a runner-owned temp file next to the original
7
+ * script and that file is executed. The original path is never opened for
8
+ * execution after verification.
9
+ *
10
+ * The temp file sits in the script's own directory so the hook's relative
11
+ * imports resolve exactly as they do today (bundled hooks import shared
12
+ * modules like ../../../src/lib/db-writer). A temp file in the system tmp dir
13
+ * would break that resolution.
14
+ */
15
+ export interface VerifiedRunOptions {
16
+ /** Registered hook name (for error messages) */
17
+ name: string;
18
+ /**
19
+ * The hook's on-disk script path. Only its directory and basename are used:
20
+ * the path itself is never re-opened for execution.
21
+ */
22
+ scriptPath: string;
23
+ /** The verified bytes to execute — exactly what was hashed and trusted */
24
+ content: Buffer;
25
+ /** Args passed through to the hook script */
26
+ args?: string[];
27
+ /** Input passed to the hook on stdin */
28
+ stdin: string;
29
+ env?: Record<string, string | undefined>;
30
+ timeout?: number;
31
+ }
32
+ export interface VerifiedRunResult {
33
+ stdout: string;
34
+ stderr: string;
35
+ exitCode: number;
36
+ }
37
+ export declare function executeVerifiedScript(options: VerifiedRunOptions): Promise<VerifiedRunResult>;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Hook store — SQLite hooks table + hooks.lock pin file.
3
+ *
4
+ * The DB record and the lock pin are written together so a verified hash is
5
+ * never trusted on one surface only. The lock file is the portable pin that a
6
+ * remote registry sync compares against.
7
+ */
8
+ import type { Database } from "bun:sqlite";
9
+ export interface HookRecord {
10
+ id: string;
11
+ name: string;
12
+ version: string;
13
+ sha256: string;
14
+ source_type: string;
15
+ source_ref: string | null;
16
+ installed_at: string;
17
+ enabled: number;
18
+ last_verified_at: string | null;
19
+ }
20
+ export interface LockEntry {
21
+ version: string;
22
+ sha256: string;
23
+ source: string;
24
+ }
25
+ export interface LockFile {
26
+ hooks: Record<string, LockEntry>;
27
+ }
28
+ export declare function sha256Of(input: Buffer | string): string;
29
+ export declare function sha256File(path: string): Promise<string>;
30
+ export declare function getHookRecord(db: Database, name: string): HookRecord | null;
31
+ export declare function listHookRecords(db: Database): HookRecord[];
32
+ export declare function upsertHookRecord(db: Database, record: {
33
+ name: string;
34
+ version: string;
35
+ sha256: string;
36
+ source_type: string;
37
+ source_ref?: string | null;
38
+ enabled?: number;
39
+ last_verified_at?: string | null;
40
+ }): void;
41
+ export declare function removeHookRecord(db: Database, name: string): boolean;
42
+ export declare function readLock(): LockFile;
43
+ export declare function writeLock(lock: LockFile): string;
44
+ export declare function setPinnedHook(name: string, entry: LockEntry): string;
45
+ export declare function getPinnedHook(name: string): LockEntry | undefined;
46
+ export declare function removePinnedHook(name: string): boolean;
47
+ export interface TrustCheck {
48
+ ok: boolean;
49
+ pinned: boolean;
50
+ expected: string | null;
51
+ actual: string;
52
+ name: string;
53
+ }
54
+ /**
55
+ * Check a hook script's content hash against the trusted record.
56
+ * No record exists (first run) => pin the current hash and pass.
57
+ * Record or pin exists and differs => refuse.
58
+ *
59
+ * Takes a precomputed hash so callers can verify the exact bytes they are
60
+ * about to execute (content-based verification), instead of re-reading a path.
61
+ */
62
+ export declare function checkScriptHash(name: string, actual: string): TrustCheck;
63
+ /**
64
+ * Verify a hook script's content hash against the trusted record, reading the
65
+ * script from disk. Execution paths should prefer checkScriptHash with the
66
+ * bytes they already hold so the verified bytes are the executed bytes.
67
+ */
68
+ export declare function verifyScriptHash(name: string, scriptPath: string): Promise<TrustCheck>;
69
+ export declare function retrustHook(name: string, scriptPath: string, version: string, source: string): TrustCheck;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * hooks sync — reconcile the local store against the remote registry (when an
3
+ * API URL is configured) or the bundled registry (otherwise).
4
+ *
5
+ * Fail-closed: every network read happens before anything is written, so a
6
+ * failure mid-sync leaves the local store untouched. Local-only hooks are
7
+ * never deleted by a remote sync.
8
+ */
9
+ export interface SyncDiff {
10
+ added: string[];
11
+ updated: string[];
12
+ unchanged: string[];
13
+ skipped: string[];
14
+ }
15
+ export interface SyncPlan {
16
+ apiUrl: string | null;
17
+ dryRun: boolean;
18
+ diff: SyncDiff;
19
+ }
20
+ export interface ArtifactResponse {
21
+ manifest: {
22
+ name: string;
23
+ version: string;
24
+ description?: string;
25
+ events: string[];
26
+ script: string;
27
+ args?: string[];
28
+ timeout_ms?: number;
29
+ };
30
+ script: string;
31
+ }
32
+ export declare function planSync(): Promise<SyncPlan>;
33
+ export declare function syncHooks(options?: {
34
+ dryRun?: boolean;
35
+ }): Promise<SyncPlan>;
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * hooks serve — the local registry HTTP API.
4
+ *
5
+ * Catalog and artifact reads are open; publishing (PUT) requires the API key.
6
+ * The published set is hooks.lock, the same pin file the client syncs from.
7
+ */
8
+ export declare const DEFAULT_SERVE_PORT = 39428;
9
+ export declare const SERVE_HOST = "127.0.0.1";
10
+ export declare const SERVE_SERVICE_NAME = "hooks-registry";
11
+ export interface CatalogEntry {
12
+ name: string;
13
+ version: string;
14
+ sha256: string;
15
+ events: string[];
16
+ description: string;
17
+ source: string;
18
+ }
19
+ export interface ArtifactPayload {
20
+ manifest: {
21
+ name: string;
22
+ version: string;
23
+ description?: string;
24
+ events: string[];
25
+ script: string;
26
+ args?: string[];
27
+ timeout_ms?: number;
28
+ };
29
+ script: string;
30
+ }
31
+ export declare function handleServeRequest(req: Request, apiKey: string | undefined): Promise<Response>;
32
+ export declare function startServeServer(options: {
33
+ port?: number;
34
+ host?: string;
35
+ apiKey?: string;
36
+ }): ReturnType<typeof Bun.serve>;
package/dist/storage.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { HOOKS_STORAGE_ENV, HOOKS_STORAGE_FALLBACK_ENV, HOOKS_STORAGE_MODE_ENV, HOOKS_STORAGE_MODE_FALLBACK_ENV, HOOKS_STORAGE_TABLES, STORAGE_DATABASE_ENV, STORAGE_MODE_ENV, STORAGE_TABLES, getStorageDatabaseEnv, getStorageDatabaseEnvName, getStorageDatabaseUrl, getStorageMode, getStoragePg, getStorageStatus, getSyncMetaAll, parseStorageTables, resolveTables, runStorageMigrations, storagePull, storagePush, storageSync, } from "./db/storage-sync.js";
2
- export type { StorageEnv, StorageMode, StorageStatus, SyncMeta, SyncResult } from "./db/storage-sync.js";
1
+ export { HOOKS_STORAGE_ENV, HOOKS_STORAGE_FALLBACK_ENV, HOOKS_STORAGE_BACKEND_ENV, HOOKS_STORAGE_BACKEND_FALLBACK_ENV, HOOKS_STORAGE_TABLES, RETIRED_STORAGE_MODE_ENV, STORAGE_BACKENDS, STORAGE_BACKEND_ENV, STORAGE_DATABASE_ENV, STORAGE_TABLES, getStorageBackend, getStorageDatabaseEnv, getStorageDatabaseEnvName, getStorageDatabaseUrl, getStoragePg, getStorageStatus, getSyncMetaAll, parseStorageTables, resolveTables, runStorageMigrations, storagePull, storagePush, storageSync, } from "./db/storage-sync.js";
2
+ export type { StorageBackend, StorageEnv, StorageStatus, SyncMeta, SyncResult } from "./db/storage-sync.js";
3
3
  export { PgAdapterAsync } from "./db/remote-storage.js";
4
4
  export { PG_MIGRATIONS } from "./db/pg-migrations.js";
package/dist/storage.js CHANGED
@@ -15,12 +15,6 @@ var __export = (target, all) => {
15
15
  };
16
16
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
 
18
- // src/db/index.ts
19
- import { Database } from "bun:sqlite";
20
- import { existsSync as existsSync2, mkdirSync, cpSync } from "fs";
21
- import { join as join2 } from "path";
22
- import { homedir as homedir2 } from "os";
23
-
24
18
  // src/db/schema.ts
25
19
  var CREATE_HOOK_EVENTS_TABLE = `
26
20
  CREATE TABLE IF NOT EXISTS hook_events (
@@ -37,14 +31,16 @@ var CREATE_HOOK_EVENTS_TABLE = `
37
31
  project_dir TEXT,
38
32
  metadata TEXT
39
33
  )
40
- `;
41
- var CREATE_INDEXES = [
42
- `CREATE INDEX IF NOT EXISTS idx_hook_events_timestamp ON hook_events (timestamp)`,
43
- `CREATE INDEX IF NOT EXISTS idx_hook_events_session_id ON hook_events (session_id)`,
44
- `CREATE INDEX IF NOT EXISTS idx_hook_events_hook_name ON hook_events (hook_name)`,
45
- `CREATE INDEX IF NOT EXISTS idx_hook_events_event_type ON hook_events (event_type)`,
46
- `CREATE INDEX IF NOT EXISTS idx_hook_events_errors ON hook_events (timestamp) WHERE error IS NOT NULL`
47
- ];
34
+ `, CREATE_INDEXES;
35
+ var init_schema = __esm(() => {
36
+ CREATE_INDEXES = [
37
+ `CREATE INDEX IF NOT EXISTS idx_hook_events_timestamp ON hook_events (timestamp)`,
38
+ `CREATE INDEX IF NOT EXISTS idx_hook_events_session_id ON hook_events (session_id)`,
39
+ `CREATE INDEX IF NOT EXISTS idx_hook_events_hook_name ON hook_events (hook_name)`,
40
+ `CREATE INDEX IF NOT EXISTS idx_hook_events_event_type ON hook_events (event_type)`,
41
+ `CREATE INDEX IF NOT EXISTS idx_hook_events_errors ON hook_events (timestamp) WHERE error IS NOT NULL`
42
+ ];
43
+ });
48
44
 
49
45
  // src/db/migrations/001_initial.ts
50
46
  function up(db) {
@@ -53,6 +49,9 @@ function up(db) {
53
49
  db.exec(idx);
54
50
  }
55
51
  }
52
+ var init_001_initial = __esm(() => {
53
+ init_schema();
54
+ });
56
55
 
57
56
  // src/db/migrations/002_session_events.ts
58
57
  function up2(db) {
@@ -79,6 +78,9 @@ function up2(db) {
79
78
  throw err;
80
79
  }
81
80
  }
81
+ var init_002_session_events = __esm(() => {
82
+ init_schema();
83
+ });
82
84
 
83
85
  // src/db/migrations/003_user_prompt_submit_event.ts
84
86
  function up3(db) {
@@ -105,13 +107,37 @@ function up3(db) {
105
107
  throw err;
106
108
  }
107
109
  }
110
+ var init_003_user_prompt_submit_event = __esm(() => {
111
+ init_schema();
112
+ });
113
+
114
+ // src/db/migrations/004_hooks_table.ts
115
+ function up4(db) {
116
+ db.exec(CREATE_HOOKS_TABLE);
117
+ for (const idx of CREATE_HOOKS_INDEXES) {
118
+ db.exec(idx);
119
+ }
120
+ }
121
+ var CREATE_HOOKS_TABLE = `
122
+ CREATE TABLE IF NOT EXISTS hooks (
123
+ id TEXT PRIMARY KEY,
124
+ name TEXT NOT NULL,
125
+ version TEXT NOT NULL,
126
+ sha256 TEXT NOT NULL,
127
+ source_type TEXT NOT NULL,
128
+ source_ref TEXT,
129
+ installed_at TEXT NOT NULL,
130
+ enabled INTEGER NOT NULL DEFAULT 1,
131
+ last_verified_at TEXT
132
+ )
133
+ `, CREATE_HOOKS_INDEXES;
134
+ var init_004_hooks_table = __esm(() => {
135
+ CREATE_HOOKS_INDEXES = [
136
+ `CREATE INDEX IF NOT EXISTS idx_hooks_name ON hooks (name)`
137
+ ];
138
+ });
108
139
 
109
140
  // src/db/migrations/index.ts
110
- var MIGRATIONS = [
111
- { version: "001_initial", up },
112
- { version: "002_session_events", up: up2 },
113
- { version: "003_user_prompt_submit_event", up: up3 }
114
- ];
115
141
  function ensureMigrationsTable(db) {
116
142
  db.exec(`
117
143
  CREATE TABLE IF NOT EXISTS schema_migrations (
@@ -137,12 +163,24 @@ function runMigrations(db) {
137
163
  ]);
138
164
  }
139
165
  }
166
+ var MIGRATIONS;
167
+ var init_migrations = __esm(() => {
168
+ init_001_initial();
169
+ init_002_session_events();
170
+ init_003_user_prompt_submit_event();
171
+ init_004_hooks_table();
172
+ MIGRATIONS = [
173
+ { version: "001_initial", up },
174
+ { version: "002_session_events", up: up2 },
175
+ { version: "003_user_prompt_submit_event", up: up3 },
176
+ { version: "004_hooks_table", up: up4 }
177
+ ];
178
+ });
140
179
 
141
180
  // src/db/legacy-import.ts
142
181
  import { existsSync, readdirSync, readFileSync } from "fs";
143
182
  import { join } from "path";
144
183
  import { homedir } from "os";
145
- var META_KEY = "legacy_import_done";
146
184
  function ensureMetaTable(db) {
147
185
  db.exec(`
148
186
  CREATE TABLE IF NOT EXISTS _meta (
@@ -216,12 +254,12 @@ function importErrorsLog(db, filePath) {
216
254
  } catch {}
217
255
  return count;
218
256
  }
219
- function runLegacyImport(db) {
257
+ function runLegacyImport(db, homeDir = homedir()) {
220
258
  try {
221
259
  if (isAlreadyDone(db))
222
260
  return;
223
261
  let total = 0;
224
- const claudeProjectsDir = join(homedir(), ".claude", "projects");
262
+ const claudeProjectsDir = join(homeDir, ".claude", "projects");
225
263
  if (existsSync(claudeProjectsDir)) {
226
264
  try {
227
265
  const projectDirs = readdirSync(claudeProjectsDir);
@@ -251,6 +289,8 @@ function runLegacyImport(db) {
251
289
  `);
252
290
  }
253
291
  }
292
+ var META_KEY = "legacy_import_done";
293
+ var init_legacy_import = () => {};
254
294
 
255
295
  // src/db/retention.ts
256
296
  function runRetention(db, days) {
@@ -267,7 +307,10 @@ function runRetention(db, days) {
267
307
  }
268
308
 
269
309
  // src/db/index.ts
270
- var instance = null;
310
+ import { Database } from "bun:sqlite";
311
+ import { existsSync as existsSync2, mkdirSync, cpSync } from "fs";
312
+ import { join as join2 } from "path";
313
+ import { homedir as homedir2 } from "os";
271
314
  function resolveDataDir() {
272
315
  const explicit = process.env.HASNA_HOOKS_DATA_DIR ?? process.env.HOOKS_DATA_DIR;
273
316
  if (explicit)
@@ -318,6 +361,14 @@ function getDb() {
318
361
  }
319
362
  return instance;
320
363
  }
364
+ var instance = null;
365
+ var init_db = __esm(() => {
366
+ init_migrations();
367
+ init_legacy_import();
368
+ });
369
+
370
+ // src/db/storage-sync.ts
371
+ init_db();
321
372
 
322
373
  // src/db/pg-migrations.ts
323
374
  var PG_MIGRATIONS = [
@@ -402,12 +453,14 @@ var STORAGE_TABLES = [
402
453
  "feedback"
403
454
  ];
404
455
  var HOOKS_STORAGE_TABLES = STORAGE_TABLES;
456
+ var STORAGE_BACKENDS = ["sqlite", "postgresql"];
405
457
  var HOOKS_STORAGE_ENV = "HASNA_HOOKS_DATABASE_URL";
406
458
  var HOOKS_STORAGE_FALLBACK_ENV = "HOOKS_DATABASE_URL";
407
- var HOOKS_STORAGE_MODE_ENV = "HASNA_HOOKS_STORAGE_MODE";
408
- var HOOKS_STORAGE_MODE_FALLBACK_ENV = "HOOKS_STORAGE_MODE";
459
+ var HOOKS_STORAGE_BACKEND_ENV = "HASNA_HOOKS_STORAGE_BACKEND";
460
+ var HOOKS_STORAGE_BACKEND_FALLBACK_ENV = "HOOKS_STORAGE_BACKEND";
409
461
  var STORAGE_DATABASE_ENV = [HOOKS_STORAGE_ENV, HOOKS_STORAGE_FALLBACK_ENV];
410
- var STORAGE_MODE_ENV = [HOOKS_STORAGE_MODE_ENV, HOOKS_STORAGE_MODE_FALLBACK_ENV];
462
+ var STORAGE_BACKEND_ENV = [HOOKS_STORAGE_BACKEND_ENV, HOOKS_STORAGE_BACKEND_FALLBACK_ENV];
463
+ var RETIRED_STORAGE_MODE_ENV = ["HASNA_HOOKS_STORAGE_MODE", "HOOKS_STORAGE_MODE"];
411
464
  var PRIMARY_KEYS = {
412
465
  hook_events: ["id"],
413
466
  schema_migrations: ["version"],
@@ -418,11 +471,46 @@ function readEnv(name) {
418
471
  const value = process.env[name]?.trim();
419
472
  return value || undefined;
420
473
  }
421
- function normalizeStorageMode(value) {
422
- const normalized = value?.trim().toLowerCase();
423
- if (normalized === "local" || normalized === "hybrid" || normalized === "remote")
424
- return normalized;
425
- return;
474
+ var RETIRED_MODE_REPLACEMENT = {
475
+ local: "sqlite",
476
+ hybrid: "postgresql",
477
+ remote: "postgresql",
478
+ self_hosted: "postgresql",
479
+ "self-hosted": "postgresql",
480
+ selfhosted: "postgresql",
481
+ cloud: "postgresql"
482
+ };
483
+ var BACKEND_ALIASES = {
484
+ sqlite: "sqlite",
485
+ sqlite3: "sqlite",
486
+ postgresql: "postgresql",
487
+ postgres: "postgresql",
488
+ pg: "postgresql"
489
+ };
490
+ function assertNoRetiredModeEnv() {
491
+ for (const name of RETIRED_STORAGE_MODE_ENV) {
492
+ const value = readEnv(name);
493
+ if (!value)
494
+ continue;
495
+ const replacement = RETIRED_MODE_REPLACEMENT[value.trim().toLowerCase()];
496
+ const mapping = replacement ? `${value} maps to ${replacement}` : `use one of ${STORAGE_BACKENDS.join(", ")}`;
497
+ throw new Error(`${name} is a retired deployment-mode variable and is no longer read. ` + `Hooks storage is a data-backend switch, not a deployment mode: ` + `set ${HOOKS_STORAGE_BACKEND_ENV} to ${STORAGE_BACKENDS.join(" or ")} instead (${mapping}), ` + `then unset ${name}.`);
498
+ }
499
+ }
500
+ function normalizeStorageBackend(value, envName) {
501
+ if (value === undefined)
502
+ return;
503
+ const normalized = value.trim().toLowerCase();
504
+ if (normalized === "")
505
+ return;
506
+ const backend = BACKEND_ALIASES[normalized];
507
+ if (backend)
508
+ return backend;
509
+ const replacement = RETIRED_MODE_REPLACEMENT[normalized];
510
+ if (replacement) {
511
+ throw new Error(`${envName}=${value} names a retired deployment mode. ` + `local/hybrid/remote/self_hosted/cloud were removed: hooks storage now selects a data ` + `backend only. Set ${envName}=${replacement} instead.`);
512
+ }
513
+ throw new Error(`${envName}=${value} is not a known hooks storage backend. ` + `Set ${HOOKS_STORAGE_BACKEND_ENV} to one of ${STORAGE_BACKENDS.join(", ")}.`);
426
514
  }
427
515
  function getStorageDatabaseEnvName() {
428
516
  for (const name of STORAGE_DATABASE_ENV) {
@@ -439,11 +527,12 @@ function getStorageDatabaseUrl() {
439
527
  const env = getStorageDatabaseEnv();
440
528
  return env ? readEnv(env.name) ?? null : null;
441
529
  }
442
- function getStorageMode() {
443
- const mode = normalizeStorageMode(readEnv(HOOKS_STORAGE_MODE_ENV)) ?? normalizeStorageMode(readEnv(HOOKS_STORAGE_MODE_FALLBACK_ENV));
444
- if (mode)
445
- return mode;
446
- return getStorageDatabaseUrl() ? "hybrid" : "local";
530
+ function getStorageBackend() {
531
+ assertNoRetiredModeEnv();
532
+ const backend = normalizeStorageBackend(readEnv(HOOKS_STORAGE_BACKEND_ENV), HOOKS_STORAGE_BACKEND_ENV) ?? normalizeStorageBackend(readEnv(HOOKS_STORAGE_BACKEND_FALLBACK_ENV), HOOKS_STORAGE_BACKEND_FALLBACK_ENV);
533
+ if (backend)
534
+ return backend;
535
+ return getStorageDatabaseUrl() ? "postgresql" : "sqlite";
447
536
  }
448
537
  async function getStoragePg() {
449
538
  const url = getStorageDatabaseUrl();
@@ -501,7 +590,7 @@ function getStorageStatus() {
501
590
  const activeEnv = getStorageDatabaseEnv();
502
591
  return {
503
592
  configured: Boolean(activeEnv),
504
- mode: getStorageMode(),
593
+ backend: getStorageBackend(),
505
594
  env: STORAGE_DATABASE_ENV,
506
595
  activeEnv: activeEnv?.name ?? null,
507
596
  service: "hooks",
@@ -679,18 +768,20 @@ export {
679
768
  getSyncMetaAll,
680
769
  getStorageStatus,
681
770
  getStoragePg,
682
- getStorageMode,
683
771
  getStorageDatabaseUrl,
684
772
  getStorageDatabaseEnvName,
685
773
  getStorageDatabaseEnv,
774
+ getStorageBackend,
686
775
  STORAGE_TABLES,
687
- STORAGE_MODE_ENV,
688
776
  STORAGE_DATABASE_ENV,
777
+ STORAGE_BACKEND_ENV,
778
+ STORAGE_BACKENDS,
779
+ RETIRED_STORAGE_MODE_ENV,
689
780
  PgAdapterAsync,
690
781
  PG_MIGRATIONS,
691
782
  HOOKS_STORAGE_TABLES,
692
- HOOKS_STORAGE_MODE_FALLBACK_ENV,
693
- HOOKS_STORAGE_MODE_ENV,
694
783
  HOOKS_STORAGE_FALLBACK_ENV,
695
- HOOKS_STORAGE_ENV
784
+ HOOKS_STORAGE_ENV,
785
+ HOOKS_STORAGE_BACKEND_FALLBACK_ENV,
786
+ HOOKS_STORAGE_BACKEND_ENV
696
787
  };
@@ -1675,7 +1675,14 @@ describe("destructive shell guard - rm -rf /* incident regression", () => {
1675
1675
  expect(result.block).toBe(true);
1676
1676
  expect(performance.now() - started).toBeLessThan(3000);
1677
1677
  }
1678
- });
1678
+ // Three iterations, each gated at 3000ms above, so this test's own assertions permit up to
1679
+ // 9000ms and still pass. The runner budget therefore has to sit strictly ABOVE 9000 or the
1680
+ // runner kills a run the assertions consider passing, and a genuine regression reads as an
1681
+ // infrastructure timeout rather than as the assertion's message — the same failure the
1682
+ // oversized-command test below was given a budget for. Building the 70k-element flood sits
1683
+ // outside the timed regions and is unmeasured by them. Measured 3.5-4.1s in total alone at
1684
+ // load ~15; 20000 is over twice what its own assertions already allow.
1685
+ }, 20000);
1679
1686
 
1680
1687
  test("an unpinnable target says why it was refused", async () => {
1681
1688
  // Reporting only "targets /" for `rm -rf /var[.]log` is wrong and gets a guard switched
@@ -1779,7 +1786,13 @@ describe("destructive shell guard - rm -rf /* incident regression", () => {
1779
1786
  expect((await classify(`${oversized}; rm -rf /*`)).block).toBe(true);
1780
1787
  expect((await classify(oversized.replace(/cd /g, "echo "))).block).toBe(false);
1781
1788
  expect(performance.now() - started).toBeLessThan(15000);
1782
- });
1789
+ // The 15000ms assertion above is this test's real gate, so the runner budget must sit
1790
+ // strictly ABOVE it: at bun's 5000ms default the runner kills the test at 5s and the
1791
+ // assertion never gets to speak, which reads as an infrastructure timeout rather than
1792
+ // as the regression it is. Measured cost is ~10.0-11.0s on a contended 20-core box
1793
+ // (load ~30); 30000 is double the assertion and ~3x the observed need, so a slow run
1794
+ // still fails on the assertion's message and only a true hang reaches this backstop.
1795
+ }, 30000);
1783
1796
 
1784
1797
  // -------------------------------------------------------------------------------------
1785
1798
  // Adversarial review round 11. All three bounds added the round before turned into root