@bookedsolid/rea 0.22.0 → 0.23.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 (55) hide show
  1. package/README.md +15 -0
  2. package/THREAT_MODEL.md +582 -0
  3. package/dist/audit/append.js +1 -1
  4. package/dist/cli/doctor.js +11 -12
  5. package/dist/cli/hook.d.ts +37 -3
  6. package/dist/cli/hook.js +167 -5
  7. package/dist/cli/init.js +14 -26
  8. package/dist/cli/install/canonical.js +18 -3
  9. package/dist/cli/install/commit-msg.js +1 -2
  10. package/dist/cli/install/copy.js +4 -13
  11. package/dist/cli/install/fs-safe.js +5 -16
  12. package/dist/cli/install/gitignore.js +1 -5
  13. package/dist/cli/install/pre-push.js +3 -8
  14. package/dist/cli/install/settings-merge.js +79 -16
  15. package/dist/cli/upgrade.js +14 -10
  16. package/dist/gateway/downstream.js +1 -2
  17. package/dist/gateway/live-state.js +3 -1
  18. package/dist/gateway/log.js +1 -3
  19. package/dist/gateway/middleware/audit.js +1 -1
  20. package/dist/gateway/middleware/injection.js +3 -9
  21. package/dist/gateway/middleware/policy.js +3 -1
  22. package/dist/gateway/middleware/redact.js +1 -1
  23. package/dist/gateway/observability/codex-telemetry.js +1 -2
  24. package/dist/gateway/reviewers/claude-self.js +10 -6
  25. package/dist/hooks/bash-scanner/blocked-scan.d.ts +26 -0
  26. package/dist/hooks/bash-scanner/blocked-scan.js +467 -0
  27. package/dist/hooks/bash-scanner/index.d.ts +41 -0
  28. package/dist/hooks/bash-scanner/index.js +62 -0
  29. package/dist/hooks/bash-scanner/parse-fail-closed.d.ts +31 -0
  30. package/dist/hooks/bash-scanner/parse-fail-closed.js +27 -0
  31. package/dist/hooks/bash-scanner/parser.d.ts +42 -0
  32. package/dist/hooks/bash-scanner/parser.js +92 -0
  33. package/dist/hooks/bash-scanner/protected-scan.d.ts +76 -0
  34. package/dist/hooks/bash-scanner/protected-scan.js +815 -0
  35. package/dist/hooks/bash-scanner/verdict.d.ts +80 -0
  36. package/dist/hooks/bash-scanner/verdict.js +49 -0
  37. package/dist/hooks/bash-scanner/walker.d.ts +165 -0
  38. package/dist/hooks/bash-scanner/walker.js +7954 -0
  39. package/dist/hooks/push-gate/base.js +2 -6
  40. package/dist/hooks/push-gate/codex-runner.js +3 -1
  41. package/dist/hooks/push-gate/index.js +9 -10
  42. package/dist/policy/loader.js +4 -1
  43. package/dist/registry/tofu-gate.js +2 -2
  44. package/hooks/blocked-paths-bash-gate.sh +142 -272
  45. package/hooks/protected-paths-bash-gate.sh +227 -511
  46. package/package.json +3 -2
  47. package/profiles/bst-internal-no-codex.yaml +1 -1
  48. package/profiles/bst-internal.yaml +1 -1
  49. package/profiles/client-engagement.yaml +1 -1
  50. package/profiles/lit-wc.yaml +1 -1
  51. package/profiles/minimal.yaml +1 -1
  52. package/profiles/open-source-no-codex.yaml +1 -1
  53. package/profiles/open-source.yaml +1 -1
  54. package/scripts/postinstall.mjs +1 -2
  55. package/scripts/run-vitest.mjs +117 -0
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Thin wrapper around `mvdan-sh` — the GopherJS-compiled JS port of
3
+ * the upstream Go bash parser at `mvdan.cc/sh/v3/syntax`.
4
+ *
5
+ * Why a wrapper:
6
+ * 1. The parser instance is mutable (it tracks state across calls
7
+ * to `Parse`). Construct once per process; serialize all parses
8
+ * through it. Multiple concurrent Node CLI invocations get one
9
+ * parser each — fine, it's cheap.
10
+ * 2. The library throws Go-style error objects with `.Error()`
11
+ * methods, not native JS Errors. Normalize them to native Error
12
+ * with a clean message.
13
+ * 3. Callers care only about three outcomes: parsed, parse-failed,
14
+ * or unexpected JS-level throw. Collapse the Go-vs-JS-error
15
+ * ambiguity to a single discriminated union.
16
+ *
17
+ * 0.23.0 — first release of this module. Pinned to mvdan-sh@0.10.1
18
+ * (deprecated upstream but functionally complete; see issue 1145).
19
+ * If we ever migrate to `sh-syntax` (the WASM successor), this
20
+ * wrapper is the only file that changes — everything downstream
21
+ * works against `BashFile`/`BashNode` from our local d.ts shim.
22
+ */
23
+ import mvdanSh from 'mvdan-sh';
24
+ /**
25
+ * Singleton parser. mvdan-sh's `NewParser` is documented as reusable
26
+ * across calls; spinning up a fresh one per scan adds 1-2ms with no
27
+ * correctness benefit.
28
+ *
29
+ * Imported via the default export — the package ships CommonJS with
30
+ * a single `module.exports = { syntax: ... }` shape; under ESM the
31
+ * named-export `{ syntax }` form fails because the property is
32
+ * dynamically assigned in the GopherJS-generated body and Node's
33
+ * named-exports synthesis cannot see it.
34
+ */
35
+ const parser = mvdanSh.syntax.NewParser();
36
+ /**
37
+ * Parse a bash command string into an AST. Returns a tagged union so
38
+ * the caller never has to wrap this in try/catch — every failure mode
39
+ * (Go parse error, JS throw, weird native return) is collapsed to
40
+ * `{ ok: false, error }`.
41
+ *
42
+ * Empty / whitespace-only input is a no-op success — the parser
43
+ * returns a `File` with zero `Stmts`, which the walker will yield
44
+ * zero detections for. Equivalent to the bash gates' `[[ -z "$CMD" ]]
45
+ * && exit 0` guard.
46
+ */
47
+ export function parseBashCommand(src) {
48
+ try {
49
+ const file = parser.Parse(src, 'rea-bash-scanner.sh');
50
+ // Defensive: GopherJS sometimes returns a node missing the Stmts
51
+ // field on certain edge-case inputs (we have not reproduced one,
52
+ // but the file-IO layer the original Go uses is mocked out and
53
+ // failure modes are not perfectly characterized). Treat that as
54
+ // "parse failure" rather than "empty file" so we fail closed.
55
+ if (file === null || file === undefined) {
56
+ return { ok: false, error: 'parser returned null file' };
57
+ }
58
+ return { ok: true, file };
59
+ }
60
+ catch (e) {
61
+ // Go errors come through as objects with `.Error()` methods. Native
62
+ // JS errors have `.message`. Anything else gets stringified.
63
+ const error = goErrorMessage(e) ?? jsErrorMessage(e) ?? String(e);
64
+ return { ok: false, error };
65
+ }
66
+ }
67
+ function goErrorMessage(e) {
68
+ if (typeof e === 'object' && e !== null && 'Error' in e) {
69
+ const fn = e.Error;
70
+ if (typeof fn === 'function') {
71
+ try {
72
+ const msg = fn.call(e);
73
+ if (typeof msg === 'string')
74
+ return msg;
75
+ }
76
+ catch {
77
+ // Fall through to other strategies.
78
+ }
79
+ }
80
+ }
81
+ return null;
82
+ }
83
+ function jsErrorMessage(e) {
84
+ if (e instanceof Error)
85
+ return e.message;
86
+ if (typeof e === 'object' && e !== null && 'message' in e) {
87
+ const m = e.message;
88
+ if (typeof m === 'string')
89
+ return m;
90
+ }
91
+ return null;
92
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Protected-paths policy composition. Mirrors the bash semantics in
3
+ * `hooks/_lib/protected-paths.sh` byte-for-byte:
4
+ *
5
+ * 1. Build the effective protected set:
6
+ * - If policy.protected_writes is set: that list, plus kill-switch
7
+ * invariants always added.
8
+ * - Else: the historical default (REA_PROTECTED_PATTERNS_FULL).
9
+ * Then subtract policy.protected_paths_relax — but kill-switch
10
+ * invariants in the relax list are silently dropped from the
11
+ * relax set with a stderr advisory (not from the protected set).
12
+ *
13
+ * 2. The match check:
14
+ * a. Explicit `protected_writes` overrides win FIRST (helix-020 G2).
15
+ * Matched against the path BEFORE the extension-surface check.
16
+ * b. Extension-surface paths (`.husky/{commit-msg,pre-push,
17
+ * pre-commit}.d/<fragment>`) are NOT protected by default
18
+ * (helix-018 Option B / 0.16.4).
19
+ * c. Default protected list applies, with kill-switch invariants
20
+ * always enforced.
21
+ *
22
+ * 3. Pattern matching:
23
+ * - case-insensitive (macOS APFS — helix-015 #2)
24
+ * - trailing `/` is a prefix-match
25
+ * - everything else is exact-match
26
+ *
27
+ * 4. Path normalization runs BEFORE matching:
28
+ * - URL decode, backslash → slash, leading `./` strip
29
+ * - `..` walk-up via the parser-friendly equivalent of
30
+ * `cd -P / pwd -P` (we rely on `node:fs.realpathSync` for the
31
+ * symlink resolution; non-existent parents walk up to the
32
+ * nearest existing ancestor — helix-022 #1)
33
+ * - case-insensitive lowercase comparison
34
+ * - sentinel `__rea_unresolved_expansion__` for $-substitution
35
+ * - sentinel `__rea_outside_root__` for paths escaping REA_ROOT
36
+ */
37
+ import type { Policy } from '../../policy/types.js';
38
+ import type { DetectedWrite } from './walker.js';
39
+ import { type Verdict } from './verdict.js';
40
+ /**
41
+ * Inputs the protected-scan composer needs. The caller collects these
42
+ * from the environment + policy file once and passes them in. Keeps
43
+ * the scanner function pure / testable.
44
+ */
45
+ export interface ProtectedScanContext {
46
+ reaRoot: string;
47
+ policy: Pick<Policy, 'protected_writes' | 'protected_paths_relax'>;
48
+ /**
49
+ * Stderr sink for advisory messages (e.g. "kill-switch invariant in
50
+ * protected_paths_relax"). Defaults to no-op so unit tests don't
51
+ * pollute stdout.
52
+ */
53
+ stderr?: (line: string) => void;
54
+ }
55
+ interface EffectivePatterns {
56
+ /** Full effective protected set (default OR override + invariants − relax). */
57
+ full: string[];
58
+ /** Subset that came from explicit `protected_writes` (overrides the extension-surface allow-list). */
59
+ override: string[];
60
+ }
61
+ /**
62
+ * Compute the effective protected pattern sets from policy. Pure — no
63
+ * filesystem access.
64
+ */
65
+ export declare function computeEffectivePatterns(ctx: ProtectedScanContext): EffectivePatterns;
66
+ /**
67
+ * Run a list of detected writes against the protected-paths policy.
68
+ * Returns the FIRST blocking verdict, or allow if every detection is
69
+ * clean.
70
+ *
71
+ * Order: walk detections in order of appearance. The walker emits
72
+ * them in source order, so the operator sees the EARLIEST violation
73
+ * in the error message.
74
+ */
75
+ export declare function scanForProtectedViolations(ctx: ProtectedScanContext, detections: readonly DetectedWrite[]): Verdict;
76
+ export {};