@bookedsolid/rea 0.31.0 → 0.33.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.
Files changed (43) hide show
  1. package/.husky/prepare-commit-msg +80 -6
  2. package/MIGRATING.md +24 -15
  3. package/dist/cli/hook.js +60 -22
  4. package/dist/hooks/_lib/halt-check.d.ts +78 -0
  5. package/dist/hooks/_lib/halt-check.js +106 -0
  6. package/dist/hooks/_lib/payload.d.ts +124 -0
  7. package/dist/hooks/_lib/payload.js +245 -0
  8. package/dist/hooks/_lib/segments.d.ts +125 -0
  9. package/dist/hooks/_lib/segments.js +766 -0
  10. package/dist/hooks/architecture-review-gate/index.d.ts +58 -0
  11. package/dist/hooks/architecture-review-gate/index.js +250 -0
  12. package/dist/hooks/attribution-advisory/index.d.ts +72 -0
  13. package/dist/hooks/attribution-advisory/index.js +233 -0
  14. package/dist/hooks/bash-scanner/protected-scan.js +14 -2
  15. package/dist/hooks/changeset-security-gate/index.d.ts +71 -0
  16. package/dist/hooks/changeset-security-gate/index.js +330 -0
  17. package/dist/hooks/dependency-audit-gate/index.d.ts +91 -0
  18. package/dist/hooks/dependency-audit-gate/index.js +294 -0
  19. package/dist/hooks/env-file-protection/index.d.ts +55 -0
  20. package/dist/hooks/env-file-protection/index.js +159 -0
  21. package/dist/hooks/pr-issue-link-gate/index.d.ts +91 -0
  22. package/dist/hooks/pr-issue-link-gate/index.js +127 -0
  23. package/dist/hooks/security-disclosure-gate/index.d.ts +91 -0
  24. package/dist/hooks/security-disclosure-gate/index.js +502 -0
  25. package/hooks/_lib/protected-paths.sh +10 -3
  26. package/hooks/architecture-review-gate.sh +92 -77
  27. package/hooks/attribution-advisory.sh +139 -131
  28. package/hooks/changeset-security-gate.sh +114 -149
  29. package/hooks/dependency-audit-gate.sh +115 -156
  30. package/hooks/env-file-protection.sh +130 -97
  31. package/hooks/pr-issue-link-gate.sh +114 -45
  32. package/hooks/security-disclosure-gate.sh +148 -316
  33. package/hooks/settings-protection.sh +13 -9
  34. package/package.json +1 -1
  35. package/templates/architecture-review-gate.dogfood-staged.sh +116 -0
  36. package/templates/attribution-advisory.dogfood-staged.sh +170 -0
  37. package/templates/changeset-security-gate.dogfood-staged.sh +137 -0
  38. package/templates/dependency-audit-gate.dogfood-staged.sh +138 -0
  39. package/templates/env-file-protection.dogfood-staged.sh +157 -0
  40. package/templates/pr-issue-link-gate.dogfood-staged.sh +134 -0
  41. package/templates/prepare-commit-msg.husky.sh +80 -6
  42. package/templates/security-disclosure-gate.dogfood-staged.sh +171 -0
  43. package/templates/settings-protection.dogfood.patch +58 -0
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Node-binary port of `hooks/changeset-security-gate.sh`.
3
+ *
4
+ * 0.33.0 Phase 1 port #3.
5
+ *
6
+ * Guards `.changeset/*.md` files against two failure modes:
7
+ *
8
+ * 1. SECURITY DISCLOSURE LEAK — a GHSA or CVE identifier in a
9
+ * changeset file becomes public via CHANGELOG.md when the
10
+ * release ships. Block the write.
11
+ * 2. MISSING OR MALFORMED FRONTMATTER — a changeset without a
12
+ * proper frontmatter block is silently ignored by the
13
+ * changesets tool, wasting the release entry. Block the write.
14
+ *
15
+ * Behavioral contract preserves the bash hook byte-for-byte:
16
+ *
17
+ * 1. HALT check → exit 2 with shared banner.
18
+ * 2. Tool filter: only `Write`, `Edit`, `MultiEdit`, `NotebookEdit`.
19
+ * Any other tool exits 0.
20
+ * 3. File-path filter: only `.changeset/*.md` files. The
21
+ * `.changeset/README.md` companion is excluded (it's metadata
22
+ * for the changesets tool itself).
23
+ * 4. Security disclosure scan on the resolved content. The
24
+ * ordered pattern list is reproduced verbatim. First match wins;
25
+ * emit the `MATCHED_PATTERN` placeholder.
26
+ * 5. MultiEdit short-circuit for frontmatter: MultiEdit's
27
+ * `edits[].new_string` is a list of replacement FRAGMENTS, not
28
+ * a full file. Running frontmatter validation against the
29
+ * concatenated fragments would reject every legitimate edit.
30
+ * The bash hook added this exemption in 0.15.0; we mirror it.
31
+ * The disclosure scan still runs on the fragments because
32
+ * GHSA/CVE patterns match per-fragment without structural
33
+ * assumption.
34
+ * 6. Frontmatter validation:
35
+ * a. Must start with `---`.
36
+ * b. Must contain at least one `<pkg>: (patch|minor|major)`
37
+ * entry inside the first `---`/`---` block. Accepts
38
+ * single-quoted, double-quoted, and unquoted package
39
+ * names — same explicit alternation form as the bash hook
40
+ * (0.15.0 codex round-1 P2-1 fix).
41
+ * c. Must have a non-empty description after the closing
42
+ * `---`.
43
+ *
44
+ * Block emissions use the Claude Code PreToolUse JSON-on-stdout
45
+ * protocol via `emitJsonBlock`, mirroring `_lib/common.sh::json_output`
46
+ * — JSON on stdout AND the human reason on stderr, exit 2.
47
+ */
48
+ import type { Buffer } from 'node:buffer';
49
+ export interface ChangesetSecurityGateOptions {
50
+ reaRoot?: string;
51
+ stdinOverride?: string | Buffer;
52
+ stderrWrite?: (s: string) => void;
53
+ stdoutWrite?: (s: string) => void;
54
+ }
55
+ export interface ChangesetSecurityGateResult {
56
+ exitCode: number;
57
+ stderr: string;
58
+ stdout: string;
59
+ }
60
+ export declare function runChangesetSecurityGate(options?: ChangesetSecurityGateOptions): Promise<ChangesetSecurityGateResult>;
61
+ /**
62
+ * CLI entry — `rea hook changeset-security-gate`.
63
+ */
64
+ export declare function runHookChangesetSecurityGate(options?: ChangesetSecurityGateOptions): Promise<void>;
65
+ export declare const __INTERNAL_DISCLOSURE_PATTERNS_FOR_TESTS: readonly string[];
66
+ export declare const __INTERNAL_FRONTMATTER_PATTERN_FOR_TESTS: RegExp;
67
+ export declare const __INTERNAL_BANNERS_FOR_TESTS: {
68
+ MISSING_FRONTMATTER_BANNER: string;
69
+ INVALID_FRONTMATTER_BANNER: string;
70
+ MISSING_DESCRIPTION_BANNER: string;
71
+ };
@@ -0,0 +1,330 @@
1
+ /**
2
+ * Node-binary port of `hooks/changeset-security-gate.sh`.
3
+ *
4
+ * 0.33.0 Phase 1 port #3.
5
+ *
6
+ * Guards `.changeset/*.md` files against two failure modes:
7
+ *
8
+ * 1. SECURITY DISCLOSURE LEAK — a GHSA or CVE identifier in a
9
+ * changeset file becomes public via CHANGELOG.md when the
10
+ * release ships. Block the write.
11
+ * 2. MISSING OR MALFORMED FRONTMATTER — a changeset without a
12
+ * proper frontmatter block is silently ignored by the
13
+ * changesets tool, wasting the release entry. Block the write.
14
+ *
15
+ * Behavioral contract preserves the bash hook byte-for-byte:
16
+ *
17
+ * 1. HALT check → exit 2 with shared banner.
18
+ * 2. Tool filter: only `Write`, `Edit`, `MultiEdit`, `NotebookEdit`.
19
+ * Any other tool exits 0.
20
+ * 3. File-path filter: only `.changeset/*.md` files. The
21
+ * `.changeset/README.md` companion is excluded (it's metadata
22
+ * for the changesets tool itself).
23
+ * 4. Security disclosure scan on the resolved content. The
24
+ * ordered pattern list is reproduced verbatim. First match wins;
25
+ * emit the `MATCHED_PATTERN` placeholder.
26
+ * 5. MultiEdit short-circuit for frontmatter: MultiEdit's
27
+ * `edits[].new_string` is a list of replacement FRAGMENTS, not
28
+ * a full file. Running frontmatter validation against the
29
+ * concatenated fragments would reject every legitimate edit.
30
+ * The bash hook added this exemption in 0.15.0; we mirror it.
31
+ * The disclosure scan still runs on the fragments because
32
+ * GHSA/CVE patterns match per-fragment without structural
33
+ * assumption.
34
+ * 6. Frontmatter validation:
35
+ * a. Must start with `---`.
36
+ * b. Must contain at least one `<pkg>: (patch|minor|major)`
37
+ * entry inside the first `---`/`---` block. Accepts
38
+ * single-quoted, double-quoted, and unquoted package
39
+ * names — same explicit alternation form as the bash hook
40
+ * (0.15.0 codex round-1 P2-1 fix).
41
+ * c. Must have a non-empty description after the closing
42
+ * `---`.
43
+ *
44
+ * Block emissions use the Claude Code PreToolUse JSON-on-stdout
45
+ * protocol via `emitJsonBlock`, mirroring `_lib/common.sh::json_output`
46
+ * — JSON on stdout AND the human reason on stderr, exit 2.
47
+ */
48
+ import { checkHalt, formatHaltBanner } from '../_lib/halt-check.js';
49
+ import { parseWriteHookPayload, MalformedPayloadError, TypePayloadError, readStdinWithTimeout, } from '../_lib/payload.js';
50
+ /**
51
+ * Tool names accepted by this gate. Mirrors the bash hook's
52
+ * `[[ "$TOOL_NAME" != "Write" && ... ]]` chain.
53
+ */
54
+ const ACCEPTED_TOOLS = new Set([
55
+ 'Write',
56
+ 'Edit',
57
+ 'MultiEdit',
58
+ 'NotebookEdit',
59
+ ]);
60
+ /**
61
+ * Pattern list for the disclosure scan. Order matters — first match
62
+ * wins, and the matched pattern string lands in the operator banner.
63
+ */
64
+ const DISCLOSURE_PATTERNS = [
65
+ /GHSA-[0-9A-Za-z]{4}-[0-9A-Za-z]{4}-[0-9A-Za-z]{4}/,
66
+ /CVE-[0-9]{4}-[0-9]+/,
67
+ ];
68
+ /**
69
+ * Source strings for the disclosure patterns — these are what the
70
+ * bash hook emitted in its `MATCHED_PATTERN` placeholder so the
71
+ * operator banner matches byte-for-byte.
72
+ */
73
+ const DISCLOSURE_PATTERN_SOURCES = [
74
+ 'GHSA-[0-9A-Za-z]{4}-[0-9A-Za-z]{4}-[0-9A-Za-z]{4}',
75
+ 'CVE-[0-9]{4}-[0-9]+',
76
+ ];
77
+ /**
78
+ * Frontmatter package-bump line. Accepts:
79
+ * - "@scope/name": patch
80
+ * - '@scope/name': minor
81
+ * - @scope/name : major (unquoted)
82
+ * Mirrors the bash hook's explicit-alternation form (codex P2-1).
83
+ */
84
+ const FRONTMATTER_BUMP_PATTERN = /^("[^"]+"|'[^']+'|[^"'\s]+): (patch|minor|major)/;
85
+ function emitJsonBlock(reason) {
86
+ const obj = {
87
+ hookSpecificOutput: {
88
+ hookEventName: 'PreToolUse',
89
+ permissionDecision: 'deny',
90
+ permissionDecisionReason: reason,
91
+ },
92
+ };
93
+ return { json: JSON.stringify(obj) + '\n', stderr: reason + '\n' };
94
+ }
95
+ /**
96
+ * Test the file path against the `.changeset/*.md` predicate. Mirrors
97
+ * the bash hook's two grep calls:
98
+ * - must match `\.changeset/[^/]+\.md$`
99
+ * - must NOT match `\.changeset/README\.md$`
100
+ */
101
+ function isChangesetFile(filePath) {
102
+ if (!/\.changeset\/[^/]+\.md$/.test(filePath))
103
+ return false;
104
+ if (/\.changeset\/README\.md$/.test(filePath))
105
+ return false;
106
+ return true;
107
+ }
108
+ /**
109
+ * Find the first matching disclosure pattern. Returns the source
110
+ * string (for the operator banner) or `null` when none match.
111
+ */
112
+ function firstDisclosureMatch(content) {
113
+ for (let i = 0; i < DISCLOSURE_PATTERNS.length; i += 1) {
114
+ const re = DISCLOSURE_PATTERNS[i];
115
+ if (re !== undefined && re.test(content)) {
116
+ return DISCLOSURE_PATTERN_SOURCES[i] ?? null;
117
+ }
118
+ }
119
+ return null;
120
+ }
121
+ /**
122
+ * Extract the frontmatter block (between the first `---` and the
123
+ * second `---`). Returns the lines BETWEEN those delimiters, NOT
124
+ * including the delimiters themselves. Mirrors the bash hook's
125
+ * `awk '/^---/{count++; if(count==2){exit} next} count==1{print}'`.
126
+ *
127
+ * When the second `---` is missing the function returns whatever was
128
+ * captured after the first `---`; the frontmatter validation regex
129
+ * then fails for lack of a bump entry, exactly as bash awk would.
130
+ */
131
+ function extractFrontmatter(content) {
132
+ const lines = content.split('\n');
133
+ let dashCount = 0;
134
+ const out = [];
135
+ for (const line of lines) {
136
+ if (/^---/.test(line)) {
137
+ dashCount += 1;
138
+ if (dashCount === 2)
139
+ break;
140
+ continue;
141
+ }
142
+ if (dashCount === 1)
143
+ out.push(line);
144
+ }
145
+ return out.join('\n');
146
+ }
147
+ /**
148
+ * Extract the first non-empty line AFTER the closing `---`. Mirrors
149
+ * the bash hook's `awk 'BEGIN{count=0} /^---/{count++; next} count>=2{print}'
150
+ * | grep -v '^[[:space:]]*$' | head -1`.
151
+ */
152
+ function extractDescription(content) {
153
+ const lines = content.split('\n');
154
+ let dashCount = 0;
155
+ for (const line of lines) {
156
+ if (/^---/.test(line)) {
157
+ dashCount += 1;
158
+ continue;
159
+ }
160
+ if (dashCount < 2)
161
+ continue;
162
+ if (line.trim().length === 0)
163
+ continue;
164
+ return line;
165
+ }
166
+ return '';
167
+ }
168
+ function buildDisclosureBanner(matched) {
169
+ return `CHANGESET SECURITY GATE: This changeset contains a security advisory identifier (matched: '${matched}').
170
+
171
+ Do NOT reference GHSA IDs or CVE numbers in changeset files before the advisory is published.
172
+ Changeset files are committed to git — this creates pre-disclosure in public history and CHANGELOG.
173
+
174
+ CORRECT approach for security fix changesets:
175
+ Use vague language only — no identifiers, no vulnerability details.
176
+
177
+ WRONG: 'fix(hooks): patch GHSA-3w3m-7gg4-f82g — symlink-guard now covers Edit tool'
178
+ RIGHT: 'security: extend symlink protection to cover all write-capable tools'
179
+
180
+ WRONG: 'security: fix CVE-2026-1234 prompt injection via tool descriptions'
181
+ RIGHT: 'security: harden middleware chain against indirect instruction attacks'
182
+
183
+ After the release ships:
184
+ 1. Publish the GitHub Security Advisory (Security tab → Advisories → Publish)
185
+ 2. The GHSA becomes the detailed public disclosure document
186
+ 3. Optionally update CHANGELOG.md post-publish to add the GHSA reference`;
187
+ }
188
+ const MISSING_FRONTMATTER_BANNER = `CHANGESET FORMAT GATE: Missing frontmatter block.
189
+
190
+ Every changeset must start with a frontmatter block specifying which package to bump:
191
+
192
+ ---
193
+ '@bookedsolid/rea': patch
194
+ ---
195
+
196
+ Brief description of what changed and why (close #N if applicable).
197
+
198
+ Bump types: patch (bug fix/security), minor (new feature), major (breaking change)`;
199
+ const INVALID_FRONTMATTER_BANNER = `CHANGESET FORMAT GATE: Frontmatter does not contain a valid package bump entry.
200
+
201
+ The frontmatter must include at least one package/bump pair:
202
+
203
+ ---
204
+ '@bookedsolid/rea': patch
205
+ ---
206
+
207
+ Valid bump types: patch | minor | major`;
208
+ const MISSING_DESCRIPTION_BANNER = `CHANGESET FORMAT GATE: Missing description after frontmatter.
209
+
210
+ Add a meaningful description explaining what changed and why:
211
+
212
+ ---
213
+ '@bookedsolid/rea': patch
214
+ ---
215
+
216
+ fix(gateway): policy-loader now uses async I/O with 500ms TTL cache
217
+
218
+ Previously, loadPolicy used fs.readFileSync on every tool invocation, blocking
219
+ the event loop under concurrency. Closes #34.`;
220
+ export async function runChangesetSecurityGate(options = {}) {
221
+ const reaRoot = options.reaRoot ?? process.env['CLAUDE_PROJECT_DIR'] ?? process.cwd();
222
+ let stderr = '';
223
+ let stdout = '';
224
+ const writeStderr = (s) => {
225
+ stderr += s;
226
+ if (options.stderrWrite)
227
+ options.stderrWrite(s);
228
+ };
229
+ const writeStdout = (s) => {
230
+ stdout += s;
231
+ if (options.stdoutWrite)
232
+ options.stdoutWrite(s);
233
+ };
234
+ // 1. HALT.
235
+ const halt = checkHalt(reaRoot);
236
+ if (halt.halted) {
237
+ writeStderr(formatHaltBanner(halt.reason));
238
+ return { exitCode: 2, stderr, stdout };
239
+ }
240
+ // 2. Stdin.
241
+ const stdinRaw = options.stdinOverride !== undefined
242
+ ? options.stdinOverride
243
+ : await readStdinWithTimeout(5_000);
244
+ let toolName = '';
245
+ let filePath = '';
246
+ let content = '';
247
+ try {
248
+ const payload = parseWriteHookPayload(stdinRaw);
249
+ toolName = payload.toolName;
250
+ filePath = payload.filePath;
251
+ content = payload.content;
252
+ }
253
+ catch (err) {
254
+ if (err instanceof MalformedPayloadError || err instanceof TypePayloadError) {
255
+ writeStderr(`changeset-security-gate: ${err.message} — refusing on uncertainty.\n`);
256
+ return { exitCode: 2, stderr, stdout };
257
+ }
258
+ throw err;
259
+ }
260
+ // 3. Tool filter.
261
+ if (toolName !== '' && !ACCEPTED_TOOLS.has(toolName)) {
262
+ return { exitCode: 0, stderr, stdout };
263
+ }
264
+ // 4. Path filter.
265
+ if (filePath.length === 0 || !isChangesetFile(filePath)) {
266
+ return { exitCode: 0, stderr, stdout };
267
+ }
268
+ // 5. Disclosure scan (runs for ALL accepted tools incl. MultiEdit).
269
+ const matched = firstDisclosureMatch(content);
270
+ if (matched !== null) {
271
+ const out = emitJsonBlock(buildDisclosureBanner(matched));
272
+ writeStdout(out.json);
273
+ writeStderr(out.stderr);
274
+ return { exitCode: 2, stderr, stdout };
275
+ }
276
+ // 6. MultiEdit short-circuit for frontmatter validation. The bash
277
+ // hook exits 0 here — the disclosure scan above is the only
278
+ // enforcement for fragment-style writes.
279
+ if (toolName === 'MultiEdit') {
280
+ return { exitCode: 0, stderr, stdout };
281
+ }
282
+ // 7. Frontmatter validation.
283
+ const firstLine = content.split('\n', 1)[0] ?? '';
284
+ if (!/^---/.test(firstLine)) {
285
+ const out = emitJsonBlock(MISSING_FRONTMATTER_BANNER);
286
+ writeStdout(out.json);
287
+ writeStderr(out.stderr);
288
+ return { exitCode: 2, stderr, stdout };
289
+ }
290
+ const frontmatter = extractFrontmatter(content);
291
+ let hasBump = false;
292
+ for (const line of frontmatter.split('\n')) {
293
+ if (FRONTMATTER_BUMP_PATTERN.test(line)) {
294
+ hasBump = true;
295
+ break;
296
+ }
297
+ }
298
+ if (!hasBump) {
299
+ const out = emitJsonBlock(INVALID_FRONTMATTER_BANNER);
300
+ writeStdout(out.json);
301
+ writeStderr(out.stderr);
302
+ return { exitCode: 2, stderr, stdout };
303
+ }
304
+ const description = extractDescription(content);
305
+ if (description.length === 0) {
306
+ const out = emitJsonBlock(MISSING_DESCRIPTION_BANNER);
307
+ writeStdout(out.json);
308
+ writeStderr(out.stderr);
309
+ return { exitCode: 2, stderr, stdout };
310
+ }
311
+ return { exitCode: 0, stderr, stdout };
312
+ }
313
+ /**
314
+ * CLI entry — `rea hook changeset-security-gate`.
315
+ */
316
+ export async function runHookChangesetSecurityGate(options = {}) {
317
+ const result = await runChangesetSecurityGate({
318
+ ...options,
319
+ stderrWrite: (s) => process.stderr.write(s),
320
+ stdoutWrite: (s) => process.stdout.write(s),
321
+ });
322
+ process.exit(result.exitCode);
323
+ }
324
+ export const __INTERNAL_DISCLOSURE_PATTERNS_FOR_TESTS = DISCLOSURE_PATTERN_SOURCES;
325
+ export const __INTERNAL_FRONTMATTER_PATTERN_FOR_TESTS = FRONTMATTER_BUMP_PATTERN;
326
+ export const __INTERNAL_BANNERS_FOR_TESTS = {
327
+ MISSING_FRONTMATTER_BANNER,
328
+ INVALID_FRONTMATTER_BANNER,
329
+ MISSING_DESCRIPTION_BANNER,
330
+ };
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Node-binary port of `hooks/dependency-audit-gate.sh`.
3
+ *
4
+ * 0.33.0 Phase 1 port #2.
5
+ *
6
+ * Detects npm/pnpm/yarn `install|i|add` invocations and verifies that
7
+ * every named package exists on the npm registry before allowing the
8
+ * install. The original bash hook is the LARGEST member of the 0.33.0
9
+ * tier-1 batch (179 LOC) and the only one in this tier that makes a
10
+ * NETWORK call (spawning `npm view <pkg> name`).
11
+ *
12
+ * Behavioral contract preserves the bash hook byte-for-byte:
13
+ *
14
+ * 1. HALT check → exit 2 with shared banner.
15
+ * 2. Read stdin → `tool_input.command`. Non-Bash tool → exit 0.
16
+ * 3. Empty command → exit 0.
17
+ * 4. Use the shared quote-aware segmenter to split on shell command
18
+ * separators (`;`, `&&`, `||`, `|`, `&`, newline). For each
19
+ * segment whose prefix-stripped head matches the install pattern
20
+ * (`(npm install|i|add) | (pnpm add|install|i) | (yarn add)`),
21
+ * extract the package-name tokens after the install command.
22
+ * Skip tokens that:
23
+ * - start with `-` (flags)
24
+ * - start with `./`, `/`, `../` (path installs)
25
+ * - contain shell metacharacters (`=`, `>`, `<`, `&`, `|`, `;`,
26
+ * `$`, backtick, quotes)
27
+ * - use workspace/link/file/git+ prefixes
28
+ * Strip trailing `@version` so `lodash@^4.0` → `lodash`.
29
+ * 5. For each extracted package (capped at 5 per command — same
30
+ * cap as the bash hook), spawn `npm view <pkg> name` with a 5s
31
+ * timeout (when GNU `timeout` is available; falls back to a
32
+ * JS-side timeout otherwise). Failed lookups accumulate.
33
+ * 6. If any failures, emit the same multi-line banner to stderr
34
+ * and exit 2. Otherwise exit 0.
35
+ *
36
+ * Key fidelity choices:
37
+ * - Segment-anchored: heredoc bodies / commit-message text that
38
+ * happens to contain `pnpm install` does NOT trigger; the bash
39
+ * hook's 0.15.0 fix is reproduced here via `splitSegments` +
40
+ * anchor-on-segment-head.
41
+ * - Env-prefix strip: `CI=1 pnpm add foo` → `pnpm add foo` for
42
+ * matching purposes. The segments helper strips leading
43
+ * `VAR=value` env-var assignments and shell prefixes (`sudo`,
44
+ * `exec`, `time`).
45
+ * - Network failure is a registry-not-found verdict, same as the
46
+ * bash hook's `npm view` exit≠0 → "package missing" — we don't
47
+ * distinguish ECONNREFUSED from "package not found", matching the
48
+ * bash hook's fail-closed posture.
49
+ */
50
+ import type { Buffer } from 'node:buffer';
51
+ export interface DependencyAuditGateOptions {
52
+ reaRoot?: string;
53
+ stdinOverride?: string | Buffer;
54
+ stderrWrite?: (s: string) => void;
55
+ /**
56
+ * Test seam — replaces the live `npm view` spawn. Returns `true`
57
+ * when the package is verified to exist, `false` otherwise. The
58
+ * production caller binds this to the real `npm view <pkg> name`
59
+ * spawn with a 5s timeout.
60
+ */
61
+ verifyPackage?: (pkg: string) => Promise<boolean>;
62
+ }
63
+ export interface DependencyAuditGateResult {
64
+ exitCode: number;
65
+ stderr: string;
66
+ /**
67
+ * Test seam — packages this run attempted to verify, in order.
68
+ * Useful for assertion-driven tests without grepping stderr.
69
+ */
70
+ checkedPackages: string[];
71
+ failedPackages: string[];
72
+ }
73
+ /**
74
+ * Walk every segment of the command. For each segment whose stripped
75
+ * head matches the install pattern, contribute its package tokens.
76
+ */
77
+ export declare function extractPackages(cmd: string): string[];
78
+ /**
79
+ * Real verifier — spawns `npm view <pkg> name`. Resolves `true` when
80
+ * the registry confirms the package exists; `false` on timeout,
81
+ * non-zero exit, or spawn failure. The bash hook treats all three
82
+ * the same (`npm view` exit ≠ 0 → fail).
83
+ */
84
+ export declare function verifyPackageReal(pkg: string): Promise<boolean>;
85
+ export declare function runDependencyAuditGate(options?: DependencyAuditGateOptions): Promise<DependencyAuditGateResult>;
86
+ /**
87
+ * CLI entry — `rea hook dependency-audit-gate`.
88
+ */
89
+ export declare function runHookDependencyAuditGate(options?: DependencyAuditGateOptions): Promise<void>;
90
+ export declare const __INTERNAL_INSTALL_PATTERN_FOR_TESTS: RegExp;
91
+ export declare const __INTERNAL_MAX_PACKAGES_FOR_TESTS = 5;