@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,127 @@
1
+ /**
2
+ * Node-binary port of `hooks/pr-issue-link-gate.sh`.
3
+ *
4
+ * 0.32.0 Phase 1 Pilot #1 — selected first because the bash original
5
+ * has the smallest dependency surface in the hook tree:
6
+ *
7
+ * - No segment splitter (just substring match on `gh pr create`)
8
+ * - No `--body-file` resolution
9
+ * - No multi-pattern catalog
10
+ * - Advisory only (always exits 0)
11
+ *
12
+ * That makes it the safest place to validate the playbook end-to-end:
13
+ * archive bash → write TS module → wire `rea hook pr-issue-link-gate`
14
+ * subcommand → replace .sh with a 15-line shim → mirror to
15
+ * `.claude/hooks/pr-issue-link-gate.sh` (PROTECTED — staged for git
16
+ * apply) → byte-fidelity test → consumer migration via `rea upgrade`
17
+ * picks up the new shim on next install.
18
+ *
19
+ * Behavioral contract — preserves bash hook byte-for-byte:
20
+ *
21
+ * 1. HALT check — exits 2 with banner when `.rea/HALT` is present.
22
+ * Bash original called `check_halt` from `_lib/halt-check.sh`;
23
+ * Node port calls the shared `checkHalt` primitive in
24
+ * `src/hooks/_lib/halt-check.ts`. Same fail-closed posture.
25
+ * 2. Reads stdin payload, extracts `tool_input.command`. When the
26
+ * tool isn't `Bash`, exits 0 silently (matches bash original
27
+ * `[[ "$TOOL_NAME" != "Bash" ]] && exit 0`).
28
+ * 3. When command does NOT contain `gh\s+pr\s+create`, exits 0.
29
+ * 4. When command DOES contain a closing keyword paired with `#N`
30
+ * (case-insensitive `closes`/`fixes`/`resolves` + whitespace +
31
+ * `#` + digits), exits 0 — the agent has already linked an issue.
32
+ * 5. Otherwise, prints the same advisory banner to stderr and exits
33
+ * 0 (advisory only — never blocks).
34
+ *
35
+ * Wider-net pattern choice: the bash original used `grep -qiE
36
+ * 'gh\s+pr\s+create'` (free `\s` shorthand). The Node port uses the
37
+ * equivalent JavaScript regex `/gh\s+pr\s+create/i` — same byte
38
+ * outcomes for ASCII inputs, which is the only shape `gh` accepts.
39
+ */
40
+ import { checkHalt, formatHaltBanner } from '../_lib/halt-check.js';
41
+ import { parseHookPayload, MalformedPayloadError, TypePayloadError, readStdinWithTimeout, } from '../_lib/payload.js';
42
+ const ADVISORY_BANNER = [
43
+ 'PR ISSUE LINK ADVISORY: This PR does not reference a GitHub issue.\n',
44
+ '\n',
45
+ 'When a PR body includes a closing reference, GitHub automatically:\n',
46
+ ' - Closes the issue when the PR merges to the default branch\n',
47
+ ' - Creates a cross-reference in the issue timeline\n',
48
+ ' - Links the PR in the CHANGELOG context\n',
49
+ '\n',
50
+ 'Add to the --body:\n',
51
+ ' closes #N closes one issue\n',
52
+ ' fixes #N same effect\n',
53
+ ' resolves #N same effect\n',
54
+ ' closes #N, closes #M closes multiple issues\n',
55
+ '\n',
56
+ 'If this is a chore, release, or hotfix PR with no upstream issue, you may proceed.\n',
57
+ ].join('');
58
+ /**
59
+ * Pure executor — no `process.exit`, no stdin read (when
60
+ * `stdinOverride` is set), no HALT-check side effects beyond reading
61
+ * the file. Returns the exit code + full stderr; the CLI wrapper
62
+ * applies them to the actual process.
63
+ */
64
+ export async function runPrIssueLinkGate(options = {}) {
65
+ const reaRoot = options.reaRoot ?? process.env['CLAUDE_PROJECT_DIR'] ?? process.cwd();
66
+ let stderr = '';
67
+ const writeStderr = (s) => {
68
+ stderr += s;
69
+ if (options.stderrWrite)
70
+ options.stderrWrite(s);
71
+ };
72
+ // 1. HALT check — fail-closed (exit 2).
73
+ const halt = checkHalt(reaRoot);
74
+ if (halt.halted) {
75
+ writeStderr(formatHaltBanner(halt.reason));
76
+ return { exitCode: 2, stderr };
77
+ }
78
+ // 2. Read stdin.
79
+ const stdinRaw = options.stdinOverride !== undefined
80
+ ? options.stdinOverride
81
+ : await readStdinWithTimeout(5_000);
82
+ let toolName = '';
83
+ let cmd = '';
84
+ try {
85
+ const payload = parseHookPayload(stdinRaw);
86
+ toolName = payload.toolName;
87
+ cmd = payload.command;
88
+ }
89
+ catch (err) {
90
+ if (err instanceof MalformedPayloadError || err instanceof TypePayloadError) {
91
+ writeStderr(`pr-issue-link-gate: ${err.message} — refusing on uncertainty.\n`);
92
+ return { exitCode: 2, stderr };
93
+ }
94
+ throw err;
95
+ }
96
+ // 3. Only Bash tool calls.
97
+ if (toolName !== '' && toolName !== 'Bash') {
98
+ return { exitCode: 0, stderr };
99
+ }
100
+ // 4. Only `gh pr create`.
101
+ if (!/gh\s+pr\s+create/i.test(cmd)) {
102
+ return { exitCode: 0, stderr };
103
+ }
104
+ // 5. Closing keyword paired with `#N` → satisfied, no advisory.
105
+ if (/(closes|fixes|resolves)\s+#[0-9]+/i.test(cmd)) {
106
+ return { exitCode: 0, stderr };
107
+ }
108
+ // 6. Advisory.
109
+ writeStderr(ADVISORY_BANNER);
110
+ return { exitCode: 0, stderr };
111
+ }
112
+ /**
113
+ * CLI entry — `rea hook pr-issue-link-gate`. Wires the pure executor
114
+ * to `process.stderr.write` + `process.exit`. Mirrors the wiring
115
+ * pattern in `runHookScanBash` / `runHookCodexReview`.
116
+ */
117
+ export async function runHookPrIssueLinkGate(options = {}) {
118
+ const result = await runPrIssueLinkGate({
119
+ ...options,
120
+ stderrWrite: (s) => process.stderr.write(s),
121
+ });
122
+ process.exit(result.exitCode);
123
+ }
124
+ // Internal export — used by the byte-fidelity test to assert the
125
+ // advisory banner string hasn't drifted vs. the bash hook's
126
+ // `printf` lines.
127
+ export const __INTERNAL_ADVISORY_BANNER_FOR_TESTS = ADVISORY_BANNER;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Node-binary port of `hooks/security-disclosure-gate.sh`.
3
+ *
4
+ * 0.32.0 Phase 1 Pilot #2 — env-var-policy + body-file-resolver +
5
+ * mode-aware redirect router for `gh issue create` commands that
6
+ * mention vulnerability-class keywords.
7
+ *
8
+ * Why pilot #2 (and not #3): pilot #2 is the LARGEST of the three
9
+ * (339 LOC bash) and exercises every primitive landed in Phase 0:
10
+ * - `checkHalt` (Phase 0)
11
+ * - `parseHookPayload` (Phase 0)
12
+ * - `splitSegments` / `anySegmentStartsWith` (Phase 0, used by
13
+ * pilot #3 first but in scope here for `gh issue create`)
14
+ * - File-IO resolver for `--body-file` / `-F` paths with `..`
15
+ * traversal refusal, ABSOLUTE-vs-relative resolution, 64 KiB cap.
16
+ * - Read of `REA_DISCLOSURE_MODE` env var with three-state semantics
17
+ * (`advisory` / `issues` / `disabled`).
18
+ *
19
+ * Behavioral contract — preserves bash hook byte-for-byte:
20
+ *
21
+ * 1. HALT check → exit 2 with shared banner.
22
+ * 2. Read `REA_DISCLOSURE_MODE` env var. `disabled` → exit 0
23
+ * immediately (no scan at all).
24
+ * 3. Read stdin. If `tool_name` isn't `Bash`, exit 0.
25
+ * 4. Identify `gh issue create` segments via `anySegmentStartsWith`.
26
+ * Substring fallback when the segment splitter is unreachable is
27
+ * moot in Node — `splitSegments` is always in scope. (The bash
28
+ * hook had a fallback only because `cmd-segments.sh` might be
29
+ * absent in foreign installs.)
30
+ * 5. Resolve `--body-file PATH` and `-F PATH` arguments. The
31
+ * resolver MUST match the bash quote-aware awk tokenizer for the
32
+ * shape `--body-file "path with spaces.md"` — we run our own
33
+ * quote-aware walker that yields each `--body-file` / `-F`
34
+ * value. Stdin form (`-`) is skipped. Paths whose CANONICAL form
35
+ * (after resolving `..` segments) escape REA_ROOT are REFUSED
36
+ * with exit 2 + advisory banner (matches the 0.17.0 helix-019 #1
37
+ * fix). Readable files contribute the first 64 KiB to the scan
38
+ * buffer; unreadable files print a warning and continue.
39
+ * 6. Build `FULL_TEXT` = body-file contents + command text (both
40
+ * lowercased) and scan for SECURITY_PATTERNS (an ordered list of
41
+ * ERE patterns mirroring the bash array). First match wins;
42
+ * `MATCHED_PATTERN` becomes the body-banner placeholder.
43
+ * 7. Route on mode:
44
+ * - `issues` → block banner pointing to `gh issue create
45
+ * --label 'security,internal' …` private form
46
+ * - `advisory` → block banner pointing to `gh api
47
+ * repos/.../security-advisories` private form
48
+ * Both return exit 2.
49
+ *
50
+ * Out-of-scope vs. the bash hook (intentional simplifications):
51
+ *
52
+ * - The bash hook emits `json_output "block" "..."` via
53
+ * `_lib/common.sh`. The JSON format is a Claude Code-specific
54
+ * wrapper that lets the hook present a structured block reason
55
+ * to the agent. In the Node tier, the canonical surface is `{
56
+ * hookSpecificOutput: { hookEventName: 'PreToolUse', ... } }`
57
+ * emitted on STDOUT with exit code 0; the legacy bash hook emits
58
+ * it on stdout. We preserve that exact shape via `emitJsonBlock`.
59
+ * - The bash hook's `require_jq` check is moot — Node parses JSON
60
+ * natively.
61
+ */
62
+ import { Buffer } from 'node:buffer';
63
+ export type DisclosureMode = 'advisory' | 'issues' | 'disabled';
64
+ export interface SecurityDisclosureGateOptions {
65
+ reaRoot?: string;
66
+ stdinOverride?: string | Buffer;
67
+ stderrWrite?: (s: string) => void;
68
+ stdoutWrite?: (s: string) => void;
69
+ /** Override `REA_DISCLOSURE_MODE`. Production reads `process.env`. */
70
+ disclosureModeOverride?: string;
71
+ /**
72
+ * Override `cwd()` for relative `--body-file` path resolution. The
73
+ * bash hook uses `pwd` (the shell's cwd at hook-execution time).
74
+ * Tests inject this so they don't have to `process.chdir`.
75
+ */
76
+ cwdOverride?: string;
77
+ }
78
+ export interface SecurityDisclosureGateResult {
79
+ exitCode: number;
80
+ stderr: string;
81
+ stdout: string;
82
+ }
83
+ /**
84
+ * Pure executor.
85
+ */
86
+ export declare function runSecurityDisclosureGate(options?: SecurityDisclosureGateOptions): Promise<SecurityDisclosureGateResult>;
87
+ /**
88
+ * CLI entry — `rea hook security-disclosure-gate`.
89
+ */
90
+ export declare function runHookSecurityDisclosureGate(options?: SecurityDisclosureGateOptions): Promise<void>;
91
+ export declare const __INTERNAL_SECURITY_PATTERNS_FOR_TESTS: readonly string[];