From 6817a09afa6df59565c924ae8f98b3ccabb54c9c Mon Sep 17 00:00:00 2001 From: Evan Zellner Date: Thu, 15 Oct 2026 08:30:24 -0500 Subject: [PATCH 1/4] Harden security and migrate to OpenCode V2 (0.2.0) Security fixes: - File reads never modify files on disk; pre-read hook is read-only. - Effective outbound redaction via V2 session.prompt/context/compaction hooks and tool.execute.after; event.subscribe is audit-only (documented). - Chunk-safe, bounded output scanning (no 1 MB silent truncation); fails closed. - CLI and sd_scan output contain safe metadata only (no raw secrets). - sd redact is atomic with no plaintext backup by default; opt-in 0600 backup. - Fix RE2/Pcre regex constructs that crashed the engine at import time. - Keyword prefilter: ~500x faster scanning on large inputs. V2 migration: - @opencode/plugin + Plugin.define({ id, setup(ctx) }). - Five tools via ctx.tool.transform (JSON Schema); hooks via ctx.*.hook. - Config example updated from plugin to plugins. Audit/multi-session safety: - Project-scoped paths, session IDs, 0600/0700 perms, bounded rotation. - Runtime state gitignored; existing audit file untracked but preserved. Tests/CI/packaging: - Fix test discovery; full suite (291 tests) passes. - Add security regression tests for the above. - Node 22 CI running typecheck, lint, build, unit/integration/security tests, and package validation; add eslint config and verify-package script. --- .github/workflows/ci.yml | 37 +- .github/workflows/publish.yml | 32 +- .gitignore | 3 + .opencode/state/strangerdanger-audit.jsonl | 7 - CHANGELOG.md | 57 + README.md | 123 +- dist/cli/args.d.ts | 2 +- dist/cli/args.d.ts.map | 2 +- dist/cli/args.js | 16 +- dist/cli/args.js.map | 2 +- dist/cli/commands/allowlist.d.ts | 2 +- dist/cli/commands/allowlist.d.ts.map | 2 +- dist/cli/commands/allowlist.js | 55 +- dist/cli/commands/allowlist.js.map | 2 +- dist/cli/commands/audit.d.ts.map | 2 +- dist/cli/commands/audit.js | 4 +- dist/cli/commands/audit.js.map | 2 +- dist/cli/commands/redact.d.ts | 2 +- dist/cli/commands/redact.d.ts.map | 2 +- dist/cli/commands/redact.js | 120 +- dist/cli/commands/redact.js.map | 2 +- dist/cli/commands/scan.d.ts.map | 2 +- dist/cli/commands/scan.js | 137 +- dist/cli/commands/scan.js.map | 2 +- dist/cli/index.js | 7 +- dist/cli/index.js.map | 2 +- dist/engine/audit.d.ts | 36 +- dist/engine/audit.d.ts.map | 2 +- dist/engine/audit.js | 123 +- dist/engine/audit.js.map | 2 +- dist/engine/rules.js | 42 +- dist/engine/rules.js.map | 2 +- dist/engine/scanner.d.ts | 43 + dist/engine/scanner.d.ts.map | 2 +- dist/engine/scanner.js | 107 +- dist/engine/scanner.js.map | 2 +- dist/engine/types.d.ts | 1 + dist/engine/types.d.ts.map | 2 +- dist/plugin/index.d.ts | 22 +- dist/plugin/index.d.ts.map | 2 +- dist/plugin/index.js | 195 +- dist/plugin/index.js.map | 2 +- dist/plugin/layer1.d.ts | 28 +- dist/plugin/layer1.d.ts.map | 2 +- dist/plugin/layer1.js | 70 +- dist/plugin/layer1.js.map | 2 +- dist/plugin/layer2-compaction.d.ts | 23 +- dist/plugin/layer2-compaction.d.ts.map | 2 +- dist/plugin/layer2-compaction.js | 37 +- dist/plugin/layer2-compaction.js.map | 2 +- dist/plugin/layer2-message.d.ts | 45 +- dist/plugin/layer2-message.d.ts.map | 2 +- dist/plugin/layer2-message.js | 149 +- dist/plugin/layer2-message.js.map | 2 +- dist/plugin/layer3.d.ts | 42 +- dist/plugin/layer3.d.ts.map | 2 +- dist/plugin/layer3.js | 66 +- dist/plugin/layer3.js.map | 2 +- dist/plugin/redact.d.ts | 33 + dist/plugin/redact.d.ts.map | 1 + dist/plugin/redact.js | 81 + dist/plugin/redact.js.map | 1 + dist/plugin/tools.d.ts | 27 + dist/plugin/tools.d.ts.map | 1 + dist/plugin/tools.js | 100 + dist/plugin/tools.js.map | 1 + dist/shared/version.d.ts | 6 + dist/shared/version.d.ts.map | 1 + dist/shared/version.js | 6 + dist/shared/version.js.map | 1 + eslint.config.js | 27 + package-lock.json | 5708 +++++++++++++++++--- package.json | 37 +- scripts/compile-rules.ts | 120 +- scripts/verify-package.mjs | 95 + src/cli/args.ts | 16 +- src/cli/commands/allowlist.ts | 60 +- src/cli/commands/audit.ts | 4 +- src/cli/commands/redact.ts | 138 +- src/cli/commands/scan.ts | 140 +- src/cli/index.ts | 7 +- src/engine/audit.ts | 139 +- src/engine/rules.ts | 42 +- src/engine/scanner.ts | 151 +- src/engine/types.ts | 1 + src/plugin/index.ts | 216 +- src/plugin/layer1.ts | 82 +- src/plugin/layer2-compaction.ts | 49 +- src/plugin/layer2-message.ts | 155 +- src/plugin/layer3.ts | 81 +- src/plugin/redact.ts | 106 + src/plugin/tools.ts | 109 + src/shared/version.ts | 5 + tests/cli/commands.test.ts | 105 +- tests/debug2.test.ts | 2 +- tests/engine/audit.test.ts | 31 +- tests/fixtures/dirty.ts | 5 +- tests/hardening.test.ts | 487 +- tests/integration.test.ts | 48 +- tests/pentest.test.ts | 1473 +---- tests/plugin/hooks.test.ts | 678 +-- 101 files changed, 8702 insertions(+), 3561 deletions(-) delete mode 100644 .opencode/state/strangerdanger-audit.jsonl create mode 100644 dist/plugin/redact.d.ts create mode 100644 dist/plugin/redact.d.ts.map create mode 100644 dist/plugin/redact.js create mode 100644 dist/plugin/redact.js.map create mode 100644 dist/plugin/tools.d.ts create mode 100644 dist/plugin/tools.d.ts.map create mode 100644 dist/plugin/tools.js create mode 100644 dist/plugin/tools.js.map create mode 100644 dist/shared/version.d.ts create mode 100644 dist/shared/version.d.ts.map create mode 100644 dist/shared/version.js create mode 100644 dist/shared/version.js.map create mode 100644 eslint.config.js mode change 100755 => 100644 package-lock.json create mode 100644 scripts/verify-package.mjs create mode 100644 src/plugin/redact.ts create mode 100644 src/plugin/tools.ts create mode 100644 src/shared/version.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 52a3646..7463661 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,30 +11,43 @@ on: - master jobs: - test: - name: Test and Build + verify: + name: Typecheck, lint, build and test runs-on: ubuntu-latest steps: - name: Checkout repo uses: actions/checkout@v4 - - name: Setup Bun - uses: oven-sh/setup-bun@v2 + - name: Setup Node + uses: actions/setup-node@v4 with: - bun-version: latest + node-version: 22 + cache: npm - name: Install dependencies - run: bun install + run: npm ci + + - name: Compile detection rules from TOML + run: npm run build:rules - name: Typecheck - run: bun run typecheck --if-present + run: npm run typecheck - name: Lint - run: bun run lint --if-present - - - name: Test - run: bun test + run: npm run lint - name: Build - run: bun run build --if-present + run: npm run build + + - name: Unit tests + run: npm run test:unit + + - name: Integration tests + run: npm run test:integration + + - name: Security regression tests + run: npm run test:security + + - name: Package validation (pack, install, exercise the artifact) + run: npm run verify:package diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3ed1a10..6038c21 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,31 +18,39 @@ jobs: - name: Checkout repo uses: actions/checkout@v4 - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - - name: Setup Node for npm publishing + - name: Setup Node uses: actions/setup-node@v4 with: node-version: 22 + cache: npm registry-url: https://registry.npmjs.org/ - name: Install dependencies - run: bun install --frozen-lockfile + run: npm ci + + - name: Compile detection rules from TOML + run: npm run build:rules - name: Typecheck - run: bun run typecheck --if-present + run: npm run typecheck - name: Lint - run: bun run lint --if-present + run: npm run lint - - name: Test - run: bun test + - name: Unit tests + run: npm run test:unit + + - name: Integration tests + run: npm run test:integration + + - name: Security regression tests + run: npm run test:security - name: Build - run: bun run build --if-present + run: npm run build + + - name: Package validation + run: npm run verify:package - name: Publish to npm run: npm publish --provenance --access public diff --git a/.gitignore b/.gitignore index fed1617..c1203dd 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ coverage/ .env.* *.log .DS_Store + +# strangerDanger runtime state (audit logs, session data) +.opencode/state/ diff --git a/.opencode/state/strangerdanger-audit.jsonl b/.opencode/state/strangerdanger-audit.jsonl deleted file mode 100644 index 790efb2..0000000 --- a/.opencode/state/strangerdanger-audit.jsonl +++ /dev/null @@ -1,7 +0,0 @@ -{"timestamp":"2026-05-13T19:48:57.095Z","source":"file","ruleId":"aws-access-token","description":"Identified a pattern that may indicate AWS credentials, risking unauthorized cloud resource access and data breaches on AWS platforms.","fingerprint":"1a5d44a2"} -{"timestamp":"2026-05-13T19:48:57.148Z","source":"context","ruleId":"aws-access-token","description":"Identified a pattern that may indicate AWS credentials, risking unauthorized cloud resource access and data breaches on AWS platforms.","fingerprint":"1a5d44a2"} -{"timestamp":"2026-05-13T19:48:57.158Z","source":"context","ruleId":"aws-access-token","description":"Identified a pattern that may indicate AWS credentials, risking unauthorized cloud resource access and data breaches on AWS platforms.","fingerprint":"1a5d44a2"} -{"timestamp":"2026-05-13T19:48:57.158Z","source":"context","ruleId":"github-pat","description":"Uncovered a GitHub Personal Access Token, potentially leading to unauthorized repository access and sensitive content exposure.","fingerprint":"9d6060e2"} -{"timestamp":"2026-05-13T19:48:57.181Z","source":"message","ruleId":"aws-access-token","description":"Identified a pattern that may indicate AWS credentials, risking unauthorized cloud resource access and data breaches on AWS platforms.","fingerprint":"1a5d44a2"} -{"timestamp":"2026-05-13T19:48:57.186Z","source":"message","ruleId":"generic-api-key","description":"Detected a Generic API Key, potentially exposing access to various services and sensitive operations.","fingerprint":"c5046bdd"} -{"timestamp":"2026-05-13T19:48:57.195Z","source":"output","ruleId":"aws-access-token","description":"Identified a pattern that may indicate AWS credentials, risking unauthorized cloud resource access and data breaches on AWS platforms.","fingerprint":"1a5d44a2"} diff --git a/CHANGELOG.md b/CHANGELOG.md index 9359791..3d9eb07 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,62 @@ # Changelog +## 0.2.0 (unreleased) + +Security hardening and OpenCode V2 migration. Not yet published. + +### Security fixes + +- **File reads never modify files.** The pre-read hook no longer rewrites source + files on disk when it detects a secret. Redaction now happens at the supported + V2 result/request boundaries. +- **Effective outbound protection.** Migrated to V2 `session.prompt`, + `session.context`, and `session.compaction` hooks so prompts, tool results, + compaction transcripts, and auxiliary model requests are redacted *before* + transmission. The V1 `event` handler only audited after the fact. +- **Chunk-safe output scanning.** Tool output is no longer truncated at 1 MB. + Large content is scanned in overlapping windows; un-inspectable content fails + closed with an explicit marker. +- **No raw secrets in CLI output.** `sd scan --json` and human output report safe + metadata (rule, description, source, line/column, fingerprint, redaction token). +- **Safe `sd redact`.** Atomic write (temp + fsync + rename). No plaintext + `.backup` in the working tree by default; opt in with `--backup`/`--backup-dir`, + which writes a `0600` backup outside the tree. +- **Critical regex fix.** RE2/Pcre inline-flag groups (`(?-i:…)`, `(?i:…)`, + `(?s:…)`), POSIX classes, and `\z` — which crashed `new RegExp` at import time + and made the whole engine unusable — are translated to valid JavaScript. +- **Performance.** Whole-content keyword prefilter; ~500× faster on large inputs. + +### OpenCode V2 migration + +- Replaced `@opencode-ai/plugin` with `@opencode/plugin` (peer dependency). +- Plugin is now `Plugin.define({ id: "gitdamnit.stranger-danger", setup(ctx) })`. +- Five tools migrated to JSON Schema and `ctx.tool.transform`: `sd_status`, + `sd_audit`, `sd_allowlist_add`, `sd_allowlist_show`, `sd_scan`. +- Hooks migrated: `tool.execute.before/after`, `session.prompt`, + `session.context`, `session.compaction`, `event.subscribe`. +- Config example updated from `plugin` to `plugins`. + +### Audit logging and multi-session safety + +- Project-scoped audit path under `.opencode/state/`; hooks require an explicit + per-project path (no shared mutable process-global state). +- Session IDs recorded where available; audit files are `0600`, directories `0700`. +- Size-bounded rotation to `.1`/`.2`/`.3` (never truncates records away). +- Runtime state is Git-ignored; the previously tracked audit file is untracked + but preserved on disk. + +### Reliability, tests, packaging, CI + +- Fixed test discovery (referenced a non-existent `tests/integration.ts`; + root-level suites were never run). Full suite now runs and passes. +- Added regression coverage for read-only file access, outbound redaction, + boundary-crossing secrets, large outputs, secret-free JSON, safe backups, + allowlist behavior, audit integrity, and V2 hook/tool wiring. +- `zod` import removed (was undeclared); `@opencode/plugin` declared. +- CI rewritten to run typecheck, lint, build, unit, integration, security, and + package-validation steps on Node 22. Added `eslint.config.js` and + `scripts/verify-package.mjs`. + ## 0.1.0 (2026-05-08) Initial release. diff --git a/README.md b/README.md index b656256..4b6adad 100755 --- a/README.md +++ b/README.md @@ -10,27 +10,41 @@ Secret and PII redaction guard for AI coding agents. ## The Problem -Every secret scanner checks your files. None check what goes into the LLM's context window. A secret can be clean in your repo and still get sent to a cloud API inside a prompt, checkpoint, or shell summary. strangerDanger operates at three layers to catch what others miss. +Every secret scanner checks your files. None check what goes into the LLM's context window. A secret can be clean in your repo and still get sent to a cloud API inside a prompt, checkpoint, or shell summary. strangerDanger operates at the OpenCode V2 request boundaries to catch what others miss. -## How It Works — Three Layers of Protection +## How It Works — Protection at the V2 boundaries -- **Layer 1: File-level (before reads)** — Intercepts file reads, redacts secrets in-place before the agent sees them. -- **Layer 2: Context-level (before compaction)** — Scans everything injected into the LLM context during session compaction. Also scans outbound user messages. -- **Layer 3: Output-level (after tool execution)** — Scans tool outputs before they're written to disk. +strangerDanger registers only documented V2 hooks. Reading a file never rewrites it. + +| Boundary | Hook | What it protects | +| --- | --- | --- | +| Incoming prompt | `session.prompt` | User prompts, before durable admission | +| Tool results | `tool.execute.after` | `read`/`bash`/`grep`/`fetch` output before it reaches the model | +| Assembled request | `session.context` | System instructions, messages, tool-call inputs, tool results, auxiliary requests | +| Checkpoint compaction | `session.compaction` | The transcript being summarized, plus a plugin-provided summary | +| File inspection | `tool.execute.before` | Read-only detection + audit; **never writes to disk** | +| Event stream | `event.subscribe` | Detection/audit only (observational; cannot prevent transmission) | + +All outgoing content is scanned with a **bounded, chunk-safe** scanner: large +outputs are fully covered (secrets that straddle a window boundary are still +found), and content that cannot be inspected fails **closed** with an explicit +marker instead of leaking. ## Installation -### OpenCode Plugin +### OpenCode V2 plugin -Add to your `opencode.json`: +Add to your `opencode.jsonc` (`plugins`, not the V1 `plugin` key): -```json +```jsonc { "$schema": "https://opencode.ai/config.json", - "plugin": ["@gitdamnit/stranger-danger"] + "plugins": ["@gitdamnit/stranger-danger"] } ``` +The plugin requires the V2 runtime and `@opencode/plugin` (peer dependency). + ### Standalone CLI ```bash @@ -41,36 +55,35 @@ npm install -g @gitdamnit/stranger-danger strangerDanger and [envsitter-guard](https://github.com/boxpositron/envsitter-guard) are complementary: - envsitter-guard blocks `.env*` file reads entirely -- strangerDanger scans everything else and protects the context window - -Use both for complete coverage. +- strangerDanger scans everything else and protects the model context ## Quick Start -### Plugin - -```json -{ - "$schema": "https://opencode.ai/config.json", - "plugin": ["@gitdamnit/stranger-danger"] -} -``` - ### CLI ```bash sd scan . # Scan current directory sd scan src/config.ts # Scan a single file sd scan - < file.txt # Scan stdin -sd redact src/config.ts # Redact secrets in-place (creates .backup) -sd audit # Show audit log +sd scan . --json # Machine-readable output (never contains raw secrets) + +sd redact src/config.ts # Atomic in-place redaction, no plaintext backup +sd redact src/config.ts --backup # Keep a 0600 backup outside the working tree +sd redact src/config.ts --backup-dir ~/bak + +sd audit # Show audit log (no raw secrets) sd rules # List all active rules +sd version ``` +Every `scan`/`sd_scan` result reports **safe metadata only** — rule ID, +description, source, line/column, fingerprint, and the redaction token. Raw +matched secrets are never printed, logged, or written to the audit log. + ## Detection Rules -- ~160 rules vendored from gitleaks (MIT) -- ~12 clean-room additions for AI/ML providers (Groq, DeepSeek, Replicate, ElevenLabs, etc.) +- 221 rules vendored from gitleaks (MIT), translated to valid JavaScript regex +- 12 clean-room additions for AI/ML providers (Groq, DeepSeek, Replicate, ElevenLabs, W&B, LangSmith, LangFuse, Supabase, Vercel, and DB connection strings) - 3 entropy-based detectors (Base64, Hex, Alphanumeric) ### Key Providers Covered @@ -89,9 +102,9 @@ sd rules # List all active rules - `sd_status` — Show rule count, findings, audit log path - `sd_audit` — Show audit log with timestamps and rule IDs -- `sd_allowlist_add` — Add allowlist entry +- `sd_allowlist_add` — Add allowlist entry (also available as `sd allowlist add`) - `sd_allowlist_show` — Show current allowlist -- `sd_scan` — Manually scan content +- `sd_scan` — Manually scan content (secret-free output) ## Allowlist @@ -108,43 +121,81 @@ ruleId = "generic-api-key" path = "test/" ``` -Entry types: `fingerprint`, `ruleId`, `path`, `regex`. +Entry types: `fingerprint`, `ruleId`, `path`, `regex`. Invalid regexes are +rejected on write and skipped on read. ## Audit Log -Location: `.opencode/state/strangerdanger-audit.jsonl` +Location: `/.opencode/state/strangerdanger-audit.jsonl` (project- and +session-scoped; ignored by Git). + +- One JSON object per line; records rule ID, description, source, fingerprint, + optional session ID and file path. **Never stores the secret value.** +- Written with `O_APPEND` so concurrent writers interleave whole records. +- Files are `0600`; the containing directory is `0700`. +- Grows to 10 MB, then rotates to `.1`, `.2`, `.3` (records are renamed, never + truncated away). + +## Security guarantees and known limitations + +Guaranteed: + +- Reading a file through the plugin **never modifies it** on disk. +- Tool results, assembled model requests, compaction transcripts, and prompts are + redacted before transmission through the session pipeline. +- No raw secret is emitted by the CLI or the audit log. +- `sd redact` writes atomically and creates **no** plaintext backup unless you + explicitly ask for one. + +Not guaranteed (documented, by design): -Format: one JSON line per finding. Logs fingerprint, rule ID, timestamp, and source. **Never logs the actual secret value.** +- Provider-native **WebSocket** traffic that bypasses the session HTTP pipeline + is not covered. +- The V2 event stream is observational (post-admission), so `event.subscribe` + cannot prevent disclosure; it only audits. +- Structured output/metadata is redacted by replacing string leaves; exotic + object types are left untouched to avoid corruption. +- Content larger than the scanner's hard ceiling fails closed with a marker + rather than being partially inspected. ## Architecture ``` src/engine/ → Core detection engine (zero OpenCode deps) -src/plugin/ → OpenCode plugin hooks +src/plugin/ → OpenCode V2 plugin (hooks, transforms, tools) src/cli/ → Standalone CLI (sd binary) +scripts/ → Rule compiler, package validation ``` -Zero runtime dependencies beyond `@opencode-ai/plugin`. Node.js built-ins only. +The detection engine is decoupled from OpenCode: the standalone `sd` binary and +the plugin share `src/engine` and nothing else. Runtime dependency on +`@opencode/plugin` is a peer dependency, loaded only when the plugin entry is +imported. ## Development ```bash npm install # Install dependencies npm run build:rules # Compile gitleaks TOML → TypeScript -npm run build # Build all TypeScript npm run typecheck # Type check -npm test # Run tests +npm run lint # Lint +npm run build # Build +npm test # Full test suite +npm run test:security +npm run verify:package # Pack, install into a temp dir, exercise the artifact ``` ### Adding New Rules -Edit `scripts/compile-rules.ts` to add clean-room rules. The gitleaks TOML is compiled automatically at build time. +Edit `scripts/compile-rules.ts` to add clean-room rules. The gitleaks TOML is +compiled automatically at build time; RE2/Pcre constructs (`(?i:…)`, `(?-i:…)`, +`(?s:…)`, POSIX classes, `\z`) are translated to plain JavaScript regex. ## Attribution - **gitleaks/gitleaks** (MIT) — Secret detection rules vendored from `config/gitleaks.toml`. https://github.com/gitleaks/gitleaks - **Yelp/detect-secrets** (Apache-2.0) — Shannon entropy detection approach referenced for high-entropy string detectors. Clean-room TypeScript implementation. https://github.com/Yelp/detect-secrets -- **boxpositron/envsitter-guard** (MIT) — Complementary plugin. strangerDanger coordinates with envsitter-guard to avoid duplicating `.env*` file blocking. https://github.com/boxpositron/envsitter-guard +- **boxpositron/envsitter-guard** (MIT) — Complementary plugin. https://github.com/boxpositron/envsitter-guard ## License diff --git a/dist/cli/args.d.ts b/dist/cli/args.d.ts index 4941221..7049085 100755 --- a/dist/cli/args.d.ts +++ b/dist/cli/args.d.ts @@ -4,5 +4,5 @@ export interface CliArgs { flags: Record; } export declare function parseArgs(argv: string[]): CliArgs; -export declare const USAGE = "\nstrangerDanger v0.1.0 \u2014 Secret and PII redaction guard\n\nUsage: sd [options]\n\nCommands:\n scan Scan a file, directory, or stdin for secrets\n redact Redact secrets in a file in-place (creates .backup)\n audit Show the audit log\n rules List all active detection rules\n allowlist add Add an allowlist entry interactively\n allowlist show Show current allowlist entries\n version Show version\n\nOptions:\n --source Source type for scan: file, context, output, message\n --json Output in JSON format\n --help Show this help message\n"; +export declare const USAGE = "\nstrangerDanger v0.2.0 \u2014 Secret and PII redaction guard\n\nUsage: sd [options]\n\nCommands:\n scan Scan a file, directory, or stdin for secrets\n redact Redact secrets in a file in-place (atomic, no plaintext backup by default)\n audit Show the audit log\n rules List all active detection rules\n allowlist add Add an allowlist entry\n allowlist show Show current allowlist entries\n version Show version\n\nOptions:\n --source Source type for scan: file, context, output, message\n --json Output in JSON format (never contains raw secrets)\n --backup Redact with a permission-restricted backup outside the working tree\n --backup-dir Store the redaction backup in instead of the default location\n --help Show this help message\n"; //# sourceMappingURL=args.d.ts.map \ No newline at end of file diff --git a/dist/cli/args.d.ts.map b/dist/cli/args.d.ts.map index 462f410..0a4f72f 100755 --- a/dist/cli/args.d.ts.map +++ b/dist/cli/args.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"args.d.ts","sourceRoot":"","sources":["../../src/cli/args.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,OAAO;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC;CAC3C;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAyDhD;AAEF,eAAO,MAAM,KAAK,ivBAkBjB,CAAC"} \ No newline at end of file +{"version":3,"file":"args.d.ts","sourceRoot":"","sources":["../../src/cli/args.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,OAAO;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC;CAC3C;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CA+DhD;AAEF,eAAO,MAAM,KAAK,29BAoBjB,CAAC"} \ No newline at end of file diff --git a/dist/cli/args.js b/dist/cli/args.js index 9613f68..2196b2b 100755 --- a/dist/cli/args.js +++ b/dist/cli/args.js @@ -11,6 +11,12 @@ export function parseArgs(argv) { for (let i = 1; i < args.length; i++) { const arg = args[i]; if (arg.startsWith("--")) { + const eq = arg.indexOf("="); + if (eq !== -1) { + // --flag=value + flags[arg.slice(2, eq)] = arg.slice(eq + 1); + continue; + } const key = arg.slice(2); const next = args[i + 1]; if (next && !next.startsWith("--")) { @@ -63,22 +69,24 @@ export function parseArgs(argv) { return { command, positional, flags }; } export const USAGE = ` -strangerDanger v0.1.0 — Secret and PII redaction guard +strangerDanger v0.2.0 — Secret and PII redaction guard Usage: sd [options] Commands: scan Scan a file, directory, or stdin for secrets - redact Redact secrets in a file in-place (creates .backup) + redact Redact secrets in a file in-place (atomic, no plaintext backup by default) audit Show the audit log rules List all active detection rules - allowlist add Add an allowlist entry interactively + allowlist add Add an allowlist entry allowlist show Show current allowlist entries version Show version Options: --source Source type for scan: file, context, output, message - --json Output in JSON format + --json Output in JSON format (never contains raw secrets) + --backup Redact with a permission-restricted backup outside the working tree + --backup-dir Store the redaction backup in instead of the default location --help Show this help message `; //# sourceMappingURL=args.js.map \ No newline at end of file diff --git a/dist/cli/args.js.map b/dist/cli/args.js.map index c1c9156..ad1b1d4 100755 --- a/dist/cli/args.js.map +++ b/dist/cli/args.js.map @@ -1 +1 @@ -{"version":3,"file":"args.js","sourceRoot":"","sources":["../../src/cli/args.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"args.js","sourceRoot":"","sources":["../../src/cli/args.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/cli/commands/allowlist.d.ts b/dist/cli/commands/allowlist.d.ts index 092105c..edb2a67 100755 --- a/dist/cli/commands/allowlist.d.ts +++ b/dist/cli/commands/allowlist.d.ts @@ -1,2 +1,2 @@ -export declare function cmdAllowlist(subcommand: string, _positional: string[]): Promise; +export declare function cmdAllowlist(subcommand: string, _positional: string[], flags?: Record): Promise; //# sourceMappingURL=allowlist.d.ts.map \ No newline at end of file diff --git a/dist/cli/commands/allowlist.d.ts.map b/dist/cli/commands/allowlist.d.ts.map index 35f074c..cb2f3d3 100755 --- a/dist/cli/commands/allowlist.d.ts.map +++ b/dist/cli/commands/allowlist.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"allowlist.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/allowlist.ts"],"names":[],"mappings":"AAEA,wBAAsB,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAkC3F"} \ No newline at end of file +{"version":3,"file":"allowlist.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/allowlist.ts"],"names":[],"mappings":"AAsBA,wBAAsB,YAAY,CAC9B,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EAAE,EACrB,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAM,GAC7C,OAAO,CAAC,IAAI,CAAC,CA4Cf"} \ No newline at end of file diff --git a/dist/cli/commands/allowlist.js b/dist/cli/commands/allowlist.js index 124b432..f9723a1 100755 --- a/dist/cli/commands/allowlist.js +++ b/dist/cli/commands/allowlist.js @@ -1,5 +1,25 @@ -import { loadAllowlist } from "../../engine/allowlist.js"; -export async function cmdAllowlist(subcommand, _positional) { +import { loadAllowlist, saveAllowlistEntry } from "../../engine/allowlist.js"; +function pickEntry(flags) { + const entry = {}; + const str = (v) => typeof v === "string" && v.length > 0 ? v : undefined; + const description = str(flags.description); + const fingerprint = str(flags.fingerprint); + const ruleId = str(flags.ruleId) ?? str(flags["rule-id"]); + const path = str(flags.path); + const regex = str(flags.regex); + if (description) + entry.description = description; + if (fingerprint) + entry.fingerprint = fingerprint; + if (ruleId) + entry.ruleId = ruleId; + if (path) + entry.path = path; + if (regex) + entry.regex = regex; + return entry; +} +export async function cmdAllowlist(subcommand, _positional, flags = {}) { if (subcommand === "show") { const allowlist = loadAllowlist(); if (allowlist.entries.length === 0) { @@ -17,17 +37,26 @@ export async function cmdAllowlist(subcommand, _positional) { return; } if (subcommand === "add") { - console.log("Add allowlist entry. Choose a type:"); - console.log(" 1. By fingerprint"); - console.log(" 2. By rule ID"); - console.log(" 3. By file path"); - console.log(" 4. By regex pattern"); - console.log(""); - console.log("Usage examples:"); - console.log(" sd allowlist add --fingerprint a3f7b2c1 --description \"Known test key\""); - console.log(" sd allowlist add --ruleId aws-access-token --description \"Internal AWS account\""); - console.log(" sd allowlist add --path \"test/fixtures/*.ts\" --description \"Test fixtures\""); - console.log(" sd allowlist add --regex \"EXAMPLE\" --description \"Example values\""); + const entry = pickEntry(flags); + if (Object.keys(entry).length === 0 || (!entry.fingerprint && !entry.ruleId && !entry.path && !entry.regex)) { + console.error("Usage: sd allowlist add --fingerprint | --ruleId | --path | --regex [--description \"...\"]"); + process.exit(1); + } + if (entry.regex) { + try { + new RegExp(entry.regex); + } + catch (err) { + console.error(`Error: invalid --regex pattern: ${err instanceof Error ? err.message : String(err)}`); + process.exit(1); + } + } + if (entry.fingerprint && !/^[a-f0-9]{8}$/i.test(entry.fingerprint)) { + console.error("Error: --fingerprint must be 8 hexadecimal characters."); + process.exit(1); + } + saveAllowlistEntry(entry); + console.log("Allowlist entry added."); return; } console.log("Usage: sd allowlist [options]"); diff --git a/dist/cli/commands/allowlist.js.map b/dist/cli/commands/allowlist.js.map index 31f304c..aae518a 100755 --- a/dist/cli/commands/allowlist.js.map +++ b/dist/cli/commands/allowlist.js.map @@ -1 +1 @@ -{"version":3,"file":"allowlist.js","sourceRoot":"","sources":["../../../src/cli/commands/allowlist.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAE1D,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,UAAkB,EAAE,WAAqB;IACxE,IAAI,UAAU,KAAK,MAAM,EAAE,CAAC;QACxB,MAAM,SAAS,GAAG,aAAa,EAAE,CAAC;QAClC,IAAI,SAAS,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACjC,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;YACrC,OAAO;QACX,CAAC;QACD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAChD,MAAM,CAAC,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;YAC/B,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,WAAW,IAAI,kBAAkB,EAAE,CAAC,CAAC;YAChE,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC,WAAW,IAAI,GAAG,EAAE,CAAC,CAAC;YACvD,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,MAAM,IAAI,GAAG,EAAE,CAAC,CAAC;YAC7C,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,IAAI,IAAI,GAAG,EAAE,CAAC,CAAC;YACzC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,KAAK,IAAI,GAAG,EAAE,CAAC,CAAC;QAC/C,CAAC;QACD,OAAO;IACX,CAAC;IAED,IAAI,UAAU,KAAK,KAAK,EAAE,CAAC;QACvB,OAAO,CAAC,GAAG,CAAC,qCAAqC,CAAC,CAAC;QACnD,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC;QACnC,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;QAC/B,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;QACjC,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;QACrC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAChB,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;QAC/B,OAAO,CAAC,GAAG,CAAC,4EAA4E,CAAC,CAAC;QAC1F,OAAO,CAAC,GAAG,CAAC,qFAAqF,CAAC,CAAC;QACnG,OAAO,CAAC,GAAG,CAAC,kFAAkF,CAAC,CAAC;QAChG,OAAO,CAAC,GAAG,CAAC,yEAAyE,CAAC,CAAC;QACvF,OAAO;IACX,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,0CAA0C,CAAC,CAAC;AAC5D,CAAC"} \ No newline at end of file +{"version":3,"file":"allowlist.js","sourceRoot":"","sources":["../../../src/cli/commands/allowlist.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/cli/commands/audit.d.ts.map b/dist/cli/commands/audit.d.ts.map index cfda99d..cd2c5f2 100755 --- a/dist/cli/commands/audit.d.ts.map +++ b/dist/cli/commands/audit.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/audit.ts"],"names":[],"mappings":"AAEA,wBAAsB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAqB9C"} \ No newline at end of file +{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/audit.ts"],"names":[],"mappings":"AAEA,wBAAsB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAuB9C"} \ No newline at end of file diff --git a/dist/cli/commands/audit.js b/dist/cli/commands/audit.js index 02238a4..4dd092b 100755 --- a/dist/cli/commands/audit.js +++ b/dist/cli/commands/audit.js @@ -12,7 +12,9 @@ export async function cmdAudit() { for (const line of lines) { try { const entry = JSON.parse(line); - console.log(`[${entry.timestamp}] ${entry.ruleId} — ${entry.source} — fingerprint: ${entry.fingerprint}`); + const session = entry.sessionId ? ` — session: ${entry.sessionId}` : ""; + const where = entry.filePath ? ` — ${entry.filePath}` : ""; + console.log(`[${entry.timestamp}] ${entry.ruleId} — ${entry.source} — fingerprint: ${entry.fingerprint}${session}${where}`); } catch { console.log(line); diff --git a/dist/cli/commands/audit.js.map b/dist/cli/commands/audit.js.map index a64cecf..f58cc8c 100755 --- a/dist/cli/commands/audit.js.map +++ b/dist/cli/commands/audit.js.map @@ -1 +1 @@ -{"version":3,"file":"audit.js","sourceRoot":"","sources":["../../../src/cli/commands/audit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAEnE,MAAM,CAAC,KAAK,UAAU,QAAQ;IAC1B,MAAM,GAAG,GAAG,YAAY,EAAE,CAAC;IAE3B,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC;QACd,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;QACrC,OAAO;IACX,CAAC;IAED,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACrC,OAAO,CAAC,GAAG,CAAC,cAAc,YAAY,EAAE,EAAE,CAAC,CAAC;IAC5C,OAAO,CAAC,GAAG,CAAC,YAAY,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;IACxC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAEhB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACvB,IAAI,CAAC;YACD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAC/B,OAAO,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,SAAS,KAAK,KAAK,CAAC,MAAM,MAAM,KAAK,CAAC,MAAM,mBAAmB,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;QAC9G,CAAC;QAAC,MAAM,CAAC;YACL,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACtB,CAAC;IACL,CAAC;AACL,CAAC"} \ No newline at end of file +{"version":3,"file":"audit.js","sourceRoot":"","sources":["../../../src/cli/commands/audit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAEnE,MAAM,CAAC,KAAK,UAAU,QAAQ;IAC1B,MAAM,GAAG,GAAG,YAAY,EAAE,CAAC;IAE3B,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC;QACd,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;QACrC,OAAO;IACX,CAAC;IAED,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACrC,OAAO,CAAC,GAAG,CAAC,cAAc,YAAY,EAAE,EAAE,CAAC,CAAC;IAC5C,OAAO,CAAC,GAAG,CAAC,YAAY,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;IACxC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAEhB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACvB,IAAI,CAAC;YACD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAC/B,MAAM,OAAO,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,eAAe,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACxE,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC3D,OAAO,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,SAAS,KAAK,KAAK,CAAC,MAAM,MAAM,KAAK,CAAC,MAAM,mBAAmB,KAAK,CAAC,WAAW,GAAG,OAAO,GAAG,KAAK,EAAE,CAAC,CAAC;QAChI,CAAC;QAAC,MAAM,CAAC;YACL,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACtB,CAAC;IACL,CAAC;AACL,CAAC"} \ No newline at end of file diff --git a/dist/cli/commands/redact.d.ts b/dist/cli/commands/redact.d.ts index c9ad6b7..56e06a8 100755 --- a/dist/cli/commands/redact.d.ts +++ b/dist/cli/commands/redact.d.ts @@ -1,2 +1,2 @@ -export declare function cmdRedact(positional: string[]): Promise; +export declare function cmdRedact(positional: string[], flags?: Record): Promise; //# sourceMappingURL=redact.d.ts.map \ No newline at end of file diff --git a/dist/cli/commands/redact.d.ts.map b/dist/cli/commands/redact.d.ts.map index 206028c..a11942a 100755 --- a/dist/cli/commands/redact.d.ts.map +++ b/dist/cli/commands/redact.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"redact.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/redact.ts"],"names":[],"mappings":"AAKA,wBAAsB,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA4CnE"} \ No newline at end of file +{"version":3,"file":"redact.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/redact.ts"],"names":[],"mappings":"AA4EA,wBAAsB,SAAS,CAC3B,UAAU,EAAE,MAAM,EAAE,EACpB,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAM,GAC7C,OAAO,CAAC,IAAI,CAAC,CAwEf"} \ No newline at end of file diff --git a/dist/cli/commands/redact.js b/dist/cli/commands/redact.js index 3d1fe9e..3c0484f 100755 --- a/dist/cli/commands/redact.js +++ b/dist/cli/commands/redact.js @@ -1,36 +1,122 @@ -import { readFileSync, writeFileSync, copyFileSync, existsSync } from "node:fs"; -import { scanAndRedact } from "../../engine/scanner.js"; -import { writeAuditBatch } from "../../engine/audit.js"; -export async function cmdRedact(positional) { +import { closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, statSync, writeSync, } from "node:fs"; +import { basename, dirname, join, resolve } from "node:path"; +import { homedir } from "node:os"; +import { randomBytes } from "node:crypto"; +import { scanAndRedactBounded } from "../../engine/scanner.js"; +import { writeAuditBatch, auditEntryFromFinding } from "../../engine/audit.js"; +const BACKUP_DIR_MODE = 0o700; +const BACKUP_FILE_MODE = 0o600; +function isBinary(content) { + for (let i = 0; i < Math.min(content.length, 8192); i++) { + if (content[i] === 0) + return true; + } + return false; +} +/** + * Write `data` to `path` atomically with an explicit mode: write a sibling + * temp file, fsync it, then rename over the destination. A crash leaves either + * the old file or the new file, never a truncated mix. + */ +function atomicWrite(path, data, mode) { + const dir = dirname(path); + const tmp = join(dir, `.${basename(path)}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`); + const fd = openSync(tmp, "w", mode); + try { + const buf = Buffer.from(data, "utf8"); + writeSync(fd, buf, 0, buf.length, 0); + fsyncSync(fd); + } + finally { + closeSync(fd); + } + renameSync(tmp, path); +} +/** Default backup location: a private per-user state directory, never the working tree. */ +function defaultBackupDir() { + const stateHome = process.env.XDG_STATE_HOME && process.env.XDG_STATE_HOME.length > 0 + ? process.env.XDG_STATE_HOME + : join(homedir(), ".local", "state"); + return join(stateHome, "strangerdanger", "backups"); +} +/** Create a backup that never collides with (and never deletes) an existing file. */ +function writeSecureBackup(originalPath, content, dir) { + mkdirSync(dir, { recursive: true, mode: BACKUP_DIR_MODE }); + const stamp = new Date().toISOString().replace(/[:.]/g, "-"); + let target = join(dir, `${basename(originalPath)}.${stamp}.bak`); + let counter = 1; + while (existsSync(target)) { + target = join(dir, `${basename(originalPath)}.${stamp}.${counter}.bak`); + counter++; + } + const fd = openSync(target, "w", BACKUP_FILE_MODE); + try { + writeSync(fd, content, 0, content.length, 0); + fsyncSync(fd); + } + finally { + closeSync(fd); + } + return target; +} +export async function cmdRedact(positional, flags = {}) { if (positional.length === 0) { console.error("Usage: sd redact "); process.exit(1); } const filePath = positional[0]; + const wantBackup = !!flags.backup || typeof flags["backup-dir"] === "string"; if (!existsSync(filePath)) { console.error(`Error: File not found: ${filePath}`); process.exit(1); } + let stats; try { - const content = readFileSync(filePath, "utf8"); - const { redacted, findings } = scanAndRedact(content, "file"); + stats = lstatSync(filePath); + } + catch (err) { + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + process.exit(1); + } + if (!stats.isFile()) { + console.error(`Error: Refusing to redact non-regular file: ${filePath}`); + process.exit(1); + } + try { + const raw = readFileSync(filePath); + if (isBinary(raw)) { + console.error(`Error: Refusing to redact binary file: ${filePath}`); + process.exit(1); + } + const content = raw.toString("utf8"); + const { redacted, findings, complete, policy } = scanAndRedactBounded(content, "file", filePath); if (findings.length === 0) { console.log("No secrets found. File unchanged."); return; } - const backupPath = filePath + ".backup"; - copyFileSync(filePath, backupPath); - writeFileSync(filePath, redacted, "utf8"); - const auditEntries = findings.map((f) => ({ - timestamp: new Date().toISOString(), - source: "file", - ruleId: f.ruleId, - description: f.description, - fingerprint: f.redacted.match(/:([a-f0-9]{8})\]/)?.[1] || "", - })); + // A backup is explicit and lives outside the working tree by default, + // so a plaintext copy of the secret is never dropped next to the file. + let backupPath; + if (wantBackup) { + const dir = typeof flags["backup-dir"] === "string" + ? resolve(flags["backup-dir"]) + : defaultBackupDir(); + backupPath = writeSecureBackup(filePath, raw, dir); + } + const mode = statSync(filePath).mode & 0o777; + atomicWrite(filePath, redacted, mode); + const auditEntries = findings.map((f) => auditEntryFromFinding(f, { filePath })); writeAuditBatch(auditEntries); console.log(`Redacted ${findings.length} secret(s) in ${filePath}`); - console.log(`Backup saved to ${backupPath}`); + if (backupPath) { + console.log(`Backup saved to ${backupPath} (mode 0600)`); + } + else { + console.log("No plaintext backup written (use --backup to keep one)."); + } + if (!complete) { + console.error(`Warning: content could not be fully inspected (${policy}); output was failed closed.`); + } for (const f of findings) { console.log(` ${f.ruleId}: ${f.redacted}`); } diff --git a/dist/cli/commands/redact.js.map b/dist/cli/commands/redact.js.map index ae8c212..8516cc4 100755 --- a/dist/cli/commands/redact.js.map +++ b/dist/cli/commands/redact.js.map @@ -1 +1 @@ -{"version":3,"file":"redact.js","sourceRoot":"","sources":["../../../src/cli/commands/redact.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAChF,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAGxD,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,UAAoB;IAChD,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC;QACzC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;IAED,MAAM,QAAQ,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;IAE/B,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QACxB,OAAO,CAAC,KAAK,CAAC,0BAA0B,QAAQ,EAAE,CAAC,CAAC;QACpD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;IAED,IAAI,CAAC;QACD,MAAM,OAAO,GAAG,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC/C,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,aAAa,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAE9D,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACxB,OAAO,CAAC,GAAG,CAAC,mCAAmC,CAAC,CAAC;YACjD,OAAO;QACX,CAAC;QAED,MAAM,UAAU,GAAG,QAAQ,GAAG,SAAS,CAAC;QACxC,YAAY,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;QACnC,aAAa,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;QAE1C,MAAM,YAAY,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAU,EAAE,EAAE,CAAC,CAAC;YAC/C,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YACnC,MAAM,EAAE,MAAM;YACd,MAAM,EAAE,CAAC,CAAC,MAAM;YAChB,WAAW,EAAE,CAAC,CAAC,WAAW;YAC1B,WAAW,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,kBAAkB,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE;SAC/D,CAAC,CAAC,CAAC;QACJ,eAAe,CAAC,YAAY,CAAC,CAAC;QAE9B,OAAO,CAAC,GAAG,CAAC,YAAY,QAAQ,CAAC,MAAM,iBAAiB,QAAQ,EAAE,CAAC,CAAC;QACpE,OAAO,CAAC,GAAG,CAAC,mBAAmB,UAAU,EAAE,CAAC,CAAC;QAC7C,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;YACvB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC;QAChD,CAAC;IACL,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACX,OAAO,CAAC,KAAK,CAAC,UAAU,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC5E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;AACL,CAAC"} \ No newline at end of file +{"version":3,"file":"redact.js","sourceRoot":"","sources":["../../../src/cli/commands/redact.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/cli/commands/scan.d.ts.map b/dist/cli/commands/scan.d.ts.map index 00746e8..84ef361 100755 --- a/dist/cli/commands/scan.d.ts.map +++ b/dist/cli/commands/scan.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/scan.ts"],"names":[],"mappings":"AAyDA,wBAAsB,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAwD1G"} \ No newline at end of file +{"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/scan.ts"],"names":[],"mappings":"AAsHA,wBAAsB,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAiD1G"} \ No newline at end of file diff --git a/dist/cli/commands/scan.js b/dist/cli/commands/scan.js index 0d5b7c8..268305e 100755 --- a/dist/cli/commands/scan.js +++ b/dist/cli/commands/scan.js @@ -1,6 +1,6 @@ -import { readFileSync, statSync, readdirSync } from "node:fs"; +import { readFileSync, statSync, readdirSync, lstatSync } from "node:fs"; import { join, extname } from "node:path"; -import { scanAndRedact } from "../../engine/scanner.js"; +import { scanAndRedactBounded, toSafeFinding } from "../../engine/scanner.js"; const SKIP_EXTENSIONS = new Set([ ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg", ".tiff", ".webp", ".ico", ".woff", ".woff2", ".ttf", ".eot", ".otf", @@ -9,6 +9,9 @@ const SKIP_EXTENSIONS = new Set([ ".exe", ".dll", ".so", ".dylib", ".o", ".obj", ".lock", ".lockb", ]); +const SKIP_DIRECTORIES = new Set(["node_modules", ".git"]); +/** Refuse to slurp absurdly large files into memory; report them explicitly. */ +const MAX_READ_BYTES = 64 * 1024 * 1024; function isBinary(content) { for (let i = 0; i < Math.min(content.length, 8192); i++) { if (content[i] === 0) @@ -16,42 +19,99 @@ function isBinary(content) { } return false; } +function isRegularFile(filePath) { + try { + return lstatSync(filePath).isFile(); + } + catch { + return false; + } +} +function scanText(text, source, filePath) { + const { findings, complete, policy } = scanAndRedactBounded(text, source, filePath); + const safe = findings.map((f) => toSafeFinding(f, text)); + if (!complete) { + return { findings: safe, skipped: `${filePath ?? ""}: could not be fully inspected (${policy})` }; + } + return { findings: safe }; +} function scanFile(filePath, source) { + if (!isRegularFile(filePath)) { + return { findings: [], skipped: `${filePath}: not a regular file` }; + } + let size = 0; + try { + size = statSync(filePath).size; + } + catch { + return { findings: [], skipped: `${filePath}: unreadable` }; + } + if (size > MAX_READ_BYTES) { + return { findings: [], skipped: `${filePath}: exceeds ${MAX_READ_BYTES} byte read limit` }; + } const raw = readFileSync(filePath); if (isBinary(raw)) - return { findings: [], redacted: "" }; - const content = raw.toString("utf8"); - return scanAndRedact(content, source, filePath); + return { findings: [] }; + return scanText(raw.toString("utf8"), source, filePath); } function scanDirectory(dirPath, source) { const allFindings = []; - function walk(dir) { - const entries = readdirSync(dir, { withFileTypes: true }); + const skipped = []; + function walk(dir, depth) { + if (depth > 64) + return; + let entries; + try { + entries = readdirSync(dir, { withFileTypes: true }); + } + catch { + skipped.push(`${dir}: unreadable directory`); + return; + } for (const entry of entries) { const fullPath = join(dir, entry.name); - if (entry.name === "node_modules" || entry.name === ".git") + // Never follow symlinks while walking: avoids cycles and traversal. + if (entry.isSymbolicLink()) continue; if (entry.isDirectory()) { - walk(fullPath); + if (SKIP_DIRECTORIES.has(entry.name)) + continue; + walk(fullPath, depth + 1); } else if (entry.isFile()) { const ext = extname(entry.name).toLowerCase(); if (SKIP_EXTENSIONS.has(ext)) continue; - try { - const { findings } = scanFile(fullPath, source); - for (const f of findings) { - allFindings.push({ ...f, match: `${fullPath}: ${f.match}` }); - } - } - catch { - // Skip files we can't read - } + const outcome = scanFile(fullPath, source); + allFindings.push(...outcome.findings); + if (outcome.skipped) + skipped.push(outcome.skipped); } } } - walk(dirPath); - return allFindings; + walk(dirPath, 0); + return { findings: allFindings, skipped }; +} +function printHuman(target, findings, skipped) { + if (findings.length === 0 && skipped.length === 0) { + console.log(`No secrets detected in ${target}.`); + return; + } + if (findings.length > 0) { + console.log(`Found ${findings.length} secret(s) in ${target}:`); + for (const f of findings) { + const where = f.filePath ? `${f.filePath}${f.line ? `:${f.line}:${(f.column ?? 0) + 1}` : ""}` : f.source; + console.log(` ${f.ruleId}: ${f.description}`); + console.log(` Location: ${where}`); + console.log(` Fingerprint: ${f.fingerprint}`); + console.log(` Redaction: ${f.redaction}`); + } + } + else { + console.log(`No secrets detected in ${target}.`); + } + for (const s of skipped) + console.error(`Warning: ${s}`); } export async function cmdScan(positional, flags) { const source = flags.source || "file"; @@ -66,43 +126,34 @@ export async function cmdScan(positional, flags) { for await (const chunk of process.stdin) { input += chunk.toString(); } - const { findings } = scanAndRedact(input, source); + const outcome = scanText(input, source); if (asJson) { - console.log(JSON.stringify(findings, null, 2)); - } - else if (findings.length === 0) { - console.log("No secrets detected."); - } - else { - for (const f of findings) { - console.log(`${f.ruleId}: ${f.description}`); - console.log(` Redacted: ${f.redacted}`); - } + console.log(JSON.stringify({ findings: outcome.findings, skipped: outcome.skipped ? [outcome.skipped] : [] }, null, 2)); + return; } + printHuman("", outcome.findings, outcome.skipped ? [outcome.skipped] : []); return; } try { const stats = statSync(target); let findings; + let skipped = []; if (stats.isDirectory()) { - findings = scanDirectory(target, source); + const result = scanDirectory(target, source); + findings = result.findings; + skipped = result.skipped; } else { - const result = scanFile(target, source); - findings = result.findings; + const outcome = scanFile(target, source); + findings = outcome.findings; + if (outcome.skipped) + skipped = [outcome.skipped]; } if (asJson) { - console.log(JSON.stringify(findings, null, 2)); - } - else if (findings.length === 0) { - console.log(`No secrets detected in ${target}.`); + console.log(JSON.stringify({ findings, skipped }, null, 2)); } else { - console.log(`Found ${findings.length} secret(s) in ${target}:`); - for (const f of findings) { - console.log(` ${f.ruleId}: ${f.description}`); - console.log(` Redacted: ${f.redacted}`); - } + printHuman(target, findings, skipped); } } catch (err) { diff --git a/dist/cli/commands/scan.js.map b/dist/cli/commands/scan.js.map index c5a7ea1..fad8153 100755 --- a/dist/cli/commands/scan.js.map +++ b/dist/cli/commands/scan.js.map @@ -1 +1 @@ -{"version":3,"file":"scan.js","sourceRoot":"","sources":["../../../src/cli/commands/scan.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"scan.js","sourceRoot":"","sources":["../../../src/cli/commands/scan.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/cli/index.js b/dist/cli/index.js index 9dbf686..b4d74a8 100755 --- a/dist/cli/index.js +++ b/dist/cli/index.js @@ -5,6 +5,7 @@ import { cmdRedact } from "./commands/redact.js"; import { cmdAudit } from "./commands/audit.js"; import { cmdRules } from "./commands/rules.js"; import { cmdAllowlist } from "./commands/allowlist.js"; +import { VERSION } from "../shared/version.js"; async function main() { const { command, positional, flags } = parseArgs(process.argv); if (flags.help || command === "help") { @@ -16,7 +17,7 @@ async function main() { await cmdScan(positional, flags); break; case "redact": - await cmdRedact(positional); + await cmdRedact(positional, flags); break; case "audit": await cmdAudit(); @@ -25,10 +26,10 @@ async function main() { await cmdRules(); break; case "allowlist": - await cmdAllowlist(positional[0] || "show", positional.slice(1)); + await cmdAllowlist(positional[0] || "show", positional.slice(1), flags); break; case "version": - console.log("strangerDanger v0.1.0"); + console.log(`strangerDanger v${VERSION}`); break; default: console.error(`Unknown command: ${command}`); diff --git a/dist/cli/index.js.map b/dist/cli/index.js.map index c72e4c5..96a5ebc 100755 --- a/dist/cli/index.js.map +++ b/dist/cli/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAEvD,KAAK,UAAU,IAAI;IACf,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAE/D,IAAI,KAAK,CAAC,IAAI,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;QACnC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACnB,OAAO;IACX,CAAC;IAED,QAAQ,OAAO,EAAE,CAAC;QACd,KAAK,MAAM;YACP,MAAM,OAAO,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;YACjC,MAAM;QACV,KAAK,QAAQ;YACT,MAAM,SAAS,CAAC,UAAU,CAAC,CAAC;YAC5B,MAAM;QACV,KAAK,OAAO;YACR,MAAM,QAAQ,EAAE,CAAC;YACjB,MAAM;QACV,KAAK,OAAO;YACR,MAAM,QAAQ,EAAE,CAAC;YACjB,MAAM;QACV,KAAK,WAAW;YACZ,MAAM,YAAY,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,MAAM,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;YACjE,MAAM;QACV,KAAK,SAAS;YACV,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;YACrC,MAAM;QACV;YACI,OAAO,CAAC,KAAK,CAAC,oBAAoB,OAAO,EAAE,CAAC,CAAC;YAC7C,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACnB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACxB,CAAC;AACL,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACjB,OAAO,CAAC,KAAK,CAAC,gBAAgB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAClF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACpB,CAAC,CAAC,CAAC"} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAE/C,KAAK,UAAU,IAAI;IACf,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAE/D,IAAI,KAAK,CAAC,IAAI,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;QACnC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACnB,OAAO;IACX,CAAC;IAED,QAAQ,OAAO,EAAE,CAAC;QACd,KAAK,MAAM;YACP,MAAM,OAAO,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;YACjC,MAAM;QACV,KAAK,QAAQ;YACT,MAAM,SAAS,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;YACnC,MAAM;QACV,KAAK,OAAO;YACR,MAAM,QAAQ,EAAE,CAAC;YACjB,MAAM;QACV,KAAK,OAAO;YACR,MAAM,QAAQ,EAAE,CAAC;YACjB,MAAM;QACV,KAAK,WAAW;YACZ,MAAM,YAAY,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,MAAM,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;YACxE,MAAM;QACV,KAAK,SAAS;YACV,OAAO,CAAC,GAAG,CAAC,mBAAmB,OAAO,EAAE,CAAC,CAAC;YAC1C,MAAM;QACV;YACI,OAAO,CAAC,KAAK,CAAC,oBAAoB,OAAO,EAAE,CAAC,CAAC;YAC7C,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACnB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACxB,CAAC;AACL,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACjB,OAAO,CAAC,KAAK,CAAC,gBAAgB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAClF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACpB,CAAC,CAAC,CAAC"} \ No newline at end of file diff --git a/dist/engine/audit.d.ts b/dist/engine/audit.d.ts index 713c105..b2a719b 100755 --- a/dist/engine/audit.d.ts +++ b/dist/engine/audit.d.ts @@ -1,7 +1,35 @@ -import type { AuditEntry } from "./types.js"; +import { fingerprint as fingerprintOf } from "../shared/fingerprint.js"; +import type { AuditEntry, Finding } from "./types.js"; +/** + * Resolve the default audit path for a project directory. Audit logs are + * per-project runtime state: they live under the project's own + * `.opencode/state/` directory and are kept out of version control. + */ +export declare function resolveAuditPath(options?: { + directory?: string; +}): string; +/** Override the path used when no explicit path is supplied (CLI/tests). */ export declare function setAuditPath(path: string): void; -export declare function writeAudit(entry: AuditEntry): void; -export declare function writeAuditBatch(entries: AuditEntry[]): void; -export declare function readAuditLog(): string; +/** Get the path used when no explicit path is supplied. */ export declare function getAuditPath(): string; +/** + * Write one audit record as a single JSONL line. Each line is appended with + * O_APPEND semantics, so concurrent writers interleave whole lines rather than + * corrupting each other's records. + */ +export declare function writeAudit(entry: AuditEntry, path?: string): void; +export declare function writeAuditBatch(entries: AuditEntry[], path?: string): void; +export declare function readAuditLog(path?: string): string; +/** + * Build a safe audit record from a finding. Only metadata is recorded — never + * the raw matched secret or the redacted replacement (which can embed the + * secret's fingerprint but is still not the secret). + */ +export declare function auditEntryFromFinding(finding: Finding, options?: { + sessionId?: string; + filePath?: string; + source?: AuditEntry["source"]; +}): AuditEntry; +/** Fingerprint helper re-export for callers that only need the audit module. */ +export { fingerprintOf }; //# sourceMappingURL=audit.d.ts.map \ No newline at end of file diff --git a/dist/engine/audit.d.ts.map b/dist/engine/audit.d.ts.map index d5dcc34..a895aba 100755 --- a/dist/engine/audit.d.ts.map +++ b/dist/engine/audit.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/engine/audit.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAM7C,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAE/C;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAYlD;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,IAAI,CAI3D;AAED,wBAAgB,YAAY,IAAI,MAAM,CAGrC;AAED,wBAAgB,YAAY,IAAI,MAAM,CAErC"} \ No newline at end of file +{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/engine/audit.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,WAAW,IAAI,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACxE,OAAO,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAwBtD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAGzE;AAED,4EAA4E;AAC5E,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAE/C;AAED,2DAA2D;AAC3D,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAuCD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAYjE;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAI1E;AAED,wBAAgB,YAAY,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAIlD;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACjC,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAA;CAAE,GACnF,UAAU,CAWZ;AAED,gFAAgF;AAChF,OAAO,EAAE,aAAa,EAAE,CAAC"} \ No newline at end of file diff --git a/dist/engine/audit.js b/dist/engine/audit.js index c885c3c..ac9bae9 100755 --- a/dist/engine/audit.js +++ b/dist/engine/audit.js @@ -1,32 +1,123 @@ -import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs"; -import { dirname } from "node:path"; -const DEFAULT_AUDIT_PATH = ".opencode/state/strangerdanger-audit.jsonl"; -let auditPath = DEFAULT_AUDIT_PATH; +import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, } from "node:fs"; +import { dirname, join } from "node:path"; +import { fingerprint as fingerprintOf } from "../shared/fingerprint.js"; +const STATE_DIRNAME = ".opencode"; +const STATE_SUBDIR = "state"; +const AUDIT_FILENAME = "strangerdanger-audit.jsonl"; +/** File mode for audit files (owner read/write only — never world-readable). */ +const AUDIT_FILE_MODE = 0o600; +/** File mode for the directory holding audit files. */ +const AUDIT_DIR_MODE = 0o700; +/** Rotate the log once it passes this size, keeping at most `MAX_ROTATIONS` files. */ +const MAX_AUDIT_BYTES = 10 * 1024 * 1024; +const MAX_ROTATIONS = 3; +/** + * The audit path used by the standalone CLI and by callers that do not pass an + * explicit path. It is intentionally NOT a shared, mutable, process-global + * value that the plugin relies on: the OpenCode plugin resolves a path per + * project/session and passes it explicitly to every write, so that concurrent + * projects in one server process never mix their audit records. + */ +let auditPathOverride; +/** + * Resolve the default audit path for a project directory. Audit logs are + * per-project runtime state: they live under the project's own + * `.opencode/state/` directory and are kept out of version control. + */ +export function resolveAuditPath(options) { + const base = options?.directory && options.directory.length > 0 ? options.directory : process.cwd(); + return join(base, STATE_DIRNAME, STATE_SUBDIR, AUDIT_FILENAME); +} +/** Override the path used when no explicit path is supplied (CLI/tests). */ export function setAuditPath(path) { - auditPath = path; + auditPathOverride = path; +} +/** Get the path used when no explicit path is supplied. */ +export function getAuditPath() { + return auditPathOverride ?? resolveAuditPath(); } -export function writeAudit(entry) { - const dir = dirname(auditPath); +function ensureDirectory(dir) { if (!existsSync(dir)) { - mkdirSync(dir, { recursive: true }); + mkdirSync(dir, { recursive: true, mode: AUDIT_DIR_MODE }); + } +} +/** + * Bound on-disk growth. Existing records are preserved by renaming the log to + * `.1`, `.2`, ... before truncating, rather than deleting content outright. + */ +function maybeRotate(path) { + let size = 0; + try { + size = statSync(path).size; + } + catch { + return; } + if (size < MAX_AUDIT_BYTES) + return; + for (let i = MAX_ROTATIONS - 1; i >= 1; i--) { + const from = `${path}.${i}`; + const to = `${path}.${i + 1}`; + if (existsSync(from)) { + try { + renameSync(from, to); + } + catch { + // Best-effort rotation — never lose the live log over this. + } + } + } + try { + renameSync(path, `${path}.1`); + } + catch { + // If rotation fails, keep appending rather than discarding records. + } +} +/** + * Write one audit record as a single JSONL line. Each line is appended with + * O_APPEND semantics, so concurrent writers interleave whole lines rather than + * corrupting each other's records. + */ +export function writeAudit(entry, path) { + const target = path ?? getAuditPath(); + const dir = dirname(target); + ensureDirectory(dir); + maybeRotate(target); const line = JSON.stringify({ ...entry, timestamp: entry.timestamp || new Date().toISOString(), }); - appendFileSync(auditPath, line + "\n", "utf8"); + appendFileSync(target, line + "\n", { encoding: "utf8", mode: AUDIT_FILE_MODE }); } -export function writeAuditBatch(entries) { +export function writeAuditBatch(entries, path) { for (const entry of entries) { - writeAudit(entry); + writeAudit(entry, path); } } -export function readAuditLog() { - if (!existsSync(auditPath)) +export function readAuditLog(path) { + const target = path ?? getAuditPath(); + if (!existsSync(target)) return ""; - return readFileSync(auditPath, "utf8"); + return readFileSync(target, "utf8"); } -export function getAuditPath() { - return auditPath; +/** + * Build a safe audit record from a finding. Only metadata is recorded — never + * the raw matched secret or the redacted replacement (which can embed the + * secret's fingerprint but is still not the secret). + */ +export function auditEntryFromFinding(finding, options) { + const fpMatch = finding.redacted.match(/:([a-f0-9]{8})\]/); + return { + timestamp: new Date().toISOString(), + ...(options?.sessionId ? { sessionId: options.sessionId } : {}), + source: (options?.source ?? finding.source), + ruleId: finding.ruleId, + description: finding.description, + fingerprint: fpMatch ? fpMatch[1] : "", + ...((options?.filePath ?? finding.filePath) ? { filePath: options?.filePath ?? finding.filePath } : {}), + }; } +/** Fingerprint helper re-export for callers that only need the audit module. */ +export { fingerprintOf }; //# sourceMappingURL=audit.js.map \ No newline at end of file diff --git a/dist/engine/audit.js.map b/dist/engine/audit.js.map index a0e5e48..ca99249 100755 --- a/dist/engine/audit.js.map +++ b/dist/engine/audit.js.map @@ -1 +1 @@ -{"version":3,"file":"audit.js","sourceRoot":"","sources":["../../src/engine/audit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC9E,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC,MAAM,kBAAkB,GAAG,4CAA4C,CAAC;AAExE,IAAI,SAAS,GAAW,kBAAkB,CAAC;AAE3C,MAAM,UAAU,YAAY,CAAC,IAAY;IACrC,SAAS,GAAG,IAAI,CAAC;AACrB,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,KAAiB;IACxC,MAAM,GAAG,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IAC/B,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACnB,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACxC,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC;QACxB,GAAG,KAAK;QACR,SAAS,EAAE,KAAK,CAAC,SAAS,IAAI,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACzD,CAAC,CAAC;IAEH,cAAc,CAAC,SAAS,EAAE,IAAI,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC;AACnD,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,OAAqB;IACjD,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC1B,UAAU,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;AACL,CAAC;AAED,MAAM,UAAU,YAAY;IACxB,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC;QAAE,OAAO,EAAE,CAAC;IACtC,OAAO,YAAY,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,YAAY;IACxB,OAAO,SAAS,CAAC;AACrB,CAAC"} \ No newline at end of file +{"version":3,"file":"audit.js","sourceRoot":"","sources":["../../src/engine/audit.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,cAAc,EACd,UAAU,EACV,SAAS,EACT,YAAY,EACZ,UAAU,EACV,QAAQ,GACX,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,WAAW,IAAI,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAGxE,MAAM,aAAa,GAAG,WAAW,CAAC;AAClC,MAAM,YAAY,GAAG,OAAO,CAAC;AAC7B,MAAM,cAAc,GAAG,4BAA4B,CAAC;AAEpD,gFAAgF;AAChF,MAAM,eAAe,GAAG,KAAK,CAAC;AAC9B,uDAAuD;AACvD,MAAM,cAAc,GAAG,KAAK,CAAC;AAE7B,sFAAsF;AACtF,MAAM,eAAe,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AACzC,MAAM,aAAa,GAAG,CAAC,CAAC;AAExB;;;;;;GAMG;AACH,IAAI,iBAAqC,CAAC;AAE1C;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAgC;IAC7D,MAAM,IAAI,GAAG,OAAO,EAAE,SAAS,IAAI,OAAO,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;IACpG,OAAO,IAAI,CAAC,IAAI,EAAE,aAAa,EAAE,YAAY,EAAE,cAAc,CAAC,CAAC;AACnE,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,YAAY,CAAC,IAAY;IACrC,iBAAiB,GAAG,IAAI,CAAC;AAC7B,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,YAAY;IACxB,OAAO,iBAAiB,IAAI,gBAAgB,EAAE,CAAC;AACnD,CAAC;AAED,SAAS,eAAe,CAAC,GAAW;IAChC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACnB,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC,CAAC;IAC9D,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,SAAS,WAAW,CAAC,IAAY;IAC7B,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,IAAI,CAAC;QACD,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACL,OAAO;IACX,CAAC;IACD,IAAI,IAAI,GAAG,eAAe;QAAE,OAAO;IAEnC,KAAK,IAAI,CAAC,GAAG,aAAa,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC1C,MAAM,IAAI,GAAG,GAAG,IAAI,IAAI,CAAC,EAAE,CAAC;QAC5B,MAAM,EAAE,GAAG,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QAC9B,IAAI,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YACnB,IAAI,CAAC;gBACD,UAAU,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YACzB,CAAC;YAAC,MAAM,CAAC;gBACL,4DAA4D;YAChE,CAAC;QACL,CAAC;IACL,CAAC;IACD,IAAI,CAAC;QACD,UAAU,CAAC,IAAI,EAAE,GAAG,IAAI,IAAI,CAAC,CAAC;IAClC,CAAC;IAAC,MAAM,CAAC;QACL,oEAAoE;IACxE,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,KAAiB,EAAE,IAAa;IACvD,MAAM,MAAM,GAAG,IAAI,IAAI,YAAY,EAAE,CAAC;IACtC,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5B,eAAe,CAAC,GAAG,CAAC,CAAC;IACrB,WAAW,CAAC,MAAM,CAAC,CAAC;IAEpB,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC;QACxB,GAAG,KAAK;QACR,SAAS,EAAE,KAAK,CAAC,SAAS,IAAI,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACzD,CAAC,CAAC;IAEH,cAAc,CAAC,MAAM,EAAE,IAAI,GAAG,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,EAAE,CAAC,CAAC;AACrF,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,OAAqB,EAAE,IAAa;IAChE,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC1B,UAAU,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAC5B,CAAC;AACL,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,IAAa;IACtC,MAAM,MAAM,GAAG,IAAI,IAAI,YAAY,EAAE,CAAC;IACtC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,CAAC;IACnC,OAAO,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AACxC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CACjC,OAAgB,EAChB,OAAkF;IAElF,MAAM,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC;IAC3D,OAAO;QACH,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACnC,GAAG,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/D,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,IAAI,OAAO,CAAC,MAAM,CAAyB;QACnE,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,WAAW,EAAE,OAAO,CAAC,WAAW;QAChC,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE;QACtC,GAAG,CAAC,CAAC,OAAO,EAAE,QAAQ,IAAI,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC1G,CAAC;AACN,CAAC;AAED,gFAAgF;AAChF,OAAO,EAAE,aAAa,EAAE,CAAC"} \ No newline at end of file diff --git a/dist/engine/rules.js b/dist/engine/rules.js index 3cbe432..cb0bfe5 100755 --- a/dist/engine/rules.js +++ b/dist/engine/rules.js @@ -51,7 +51,7 @@ export const NAMED_RULES = [ { id: "airtable-personnal-access-token", description: "Uncovered a possible Airtable Personal AccessToken, potentially compromising database access and leading to data leakage or alteration.", - regex: new RegExp("\\b(pat[[:alnum:]]{14}\\.[a-f0-9]{64})\\b", "g"), + regex: new RegExp("\\b(pat[A-Za-z0-9]{14}\\.[a-f0-9]{64})\\b", "g"), keywords: ["airtable"], }, { @@ -111,13 +111,13 @@ export const NAMED_RULES = [ { id: "atlassian-api-token", description: "Detected an Atlassian API token, posing a threat to project management and collaboration tool security and data confidentiality.", - regex: new RegExp("[\\w.-]{0,50}?(?:(?-i:ATLASSIAN|[Aa]tlassian)|(?-i:CONFLUENCE|[Cc]onfluence)|(?-i:JIRA|[Jj]ira))(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{20}[a-f0-9]{4})(?:[\\x60'\"\\s;]|\\\\[nr]|$)|\\b(ATATT3[A-Za-z0-9_\\-=]{186})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), + regex: new RegExp("[\\w.-]{0,50}?(?:(?:ATLASSIAN|[Aa]tlassian)|(?:CONFLUENCE|[Cc]onfluence)|(?:JIRA|[Jj]ira))(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{20}[a-f0-9]{4})(?:[\\x60'\"\\s;]|\\\\[nr]|$)|\\b(ATATT3[A-Za-z0-9_\\-=]{186})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), keywords: ["atlassian", "confluence", "jira", "atatt3"], entropy: 3.5, }, { id: "authress-service-client-access-key", description: "Uncovered a possible Authress Service Client Access Key, which may compromise access control services and sensitive data.", - regex: new RegExp("\\b((?:sc|ext|scauth|authress)_[a-z0-9]{5,30}\\.[a-z0-9]{4,6}\\.(?-i:acc)[_-][a-z0-9-]{10,32}\\.[a-z0-9+/_=-]{30,120})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), + regex: new RegExp("\\b((?:sc|ext|scauth|authress)_[a-z0-9]{5,30}\\.[a-z0-9]{4,6}\\.(?:acc)[_-][a-z0-9-]{10,32}\\.[a-z0-9+/_=-]{30,120})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), keywords: ["sc_", "ext_", "scauth_", "authress_"], entropy: 2, }, { @@ -177,7 +177,7 @@ export const NAMED_RULES = [ { id: "cisco-meraki-api-key", description: "Cisco Meraki is a cloud-managed IT solution that provides networking, security, and device management through an easy-to-use interface.", - regex: new RegExp("[\\w.-]{0,50}?(?i:[\\w.-]{0,50}?(?:(?-i:[Mm]eraki|MERAKI))(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3})(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([0-9a-f]{40})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "g"), + regex: new RegExp("[\\w.-]{0,50}?(?:[\\w.-]{0,50}?(?:(?:[Mm]eraki|MERAKI))(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3})(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([0-9a-f]{40})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), keywords: ["meraki"], entropy: 3, }, { @@ -219,7 +219,7 @@ export const NAMED_RULES = [ { id: "cohere-api-token", description: "Identified a Cohere Token, posing a risk of unauthorized access to AI services and data manipulation.", - regex: new RegExp("[\\w.-]{0,50}?(?i:[\\w.-]{0,50}?(?:cohere|CO_API_KEY)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3})(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-zA-Z0-9]{40})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "g"), + regex: new RegExp("[\\w.-]{0,50}?(?:[\\w.-]{0,50}?(?:cohere|CO_API_KEY)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3})(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-zA-Z0-9]{40})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), keywords: ["cohere", "co_api_key"], entropy: 4, }, { @@ -249,13 +249,13 @@ export const NAMED_RULES = [ { id: "curl-auth-header", description: "Discovered a potential authorization token provided in a curl command header, which could compromise the curl accessed resource.", - regex: new RegExp("\\bcurl\\b(?:.*?|.*?(?:[\\r\\n]{1,2}.*?){1,5})[ \\t\\n\\r](?:-H|--header)(?:=|[ \\t]{0,5})(?:\"(?:Authorization:[ \\t]{0,5}(?:Basic[ \\t]([a-z0-9+/]{8,}={0,3})|(?:Bearer|(?:Api-)?Token)[ \\t]([\\w=~@.+/-]{8,})|([\\w=~@.+/-]{8,}))|(?:(?:X-(?:[a-z]+-)?)?(?:Api-?)?(?:Key|Token)):[ \\t]{0,5}([\\w=~@.+/-]{8,}))\"|'(?:Authorization:[ \\t]{0,5}(?:Basic[ \\t]([a-z0-9+/]{8,}={0,3})|(?:Bearer|(?:Api-)?Token)[ \\t]([\\w=~@.+/-]{8,})|([\\w=~@.+/-]{8,}))|(?:(?:X-(?:[a-z]+-)?)?(?:Api-?)?(?:Key|Token)):[ \\t]{0,5}([\\w=~@.+/-]{8,}))')(?:\\B|\\s|\\z)", "gi"), + regex: new RegExp("\\bcurl\\b(?:.*?|.*?(?:[\\r\\n]{1,2}.*?){1,5})[ \\t\\n\\r](?:-H|--header)(?:=|[ \\t]{0,5})(?:\"(?:Authorization:[ \\t]{0,5}(?:Basic[ \\t]([a-z0-9+/]{8,}={0,3})|(?:Bearer|(?:Api-)?Token)[ \\t]([\\w=~@.+/-]{8,})|([\\w=~@.+/-]{8,}))|(?:(?:X-(?:[a-z]+-)?)?(?:Api-?)?(?:Key|Token)):[ \\t]{0,5}([\\w=~@.+/-]{8,}))\"|'(?:Authorization:[ \\t]{0,5}(?:Basic[ \\t]([a-z0-9+/]{8,}={0,3})|(?:Bearer|(?:Api-)?Token)[ \\t]([\\w=~@.+/-]{8,})|([\\w=~@.+/-]{8,}))|(?:(?:X-(?:[a-z]+-)?)?(?:Api-?)?(?:Key|Token)):[ \\t]{0,5}([\\w=~@.+/-]{8,}))')(?:\\B|\\s|$)", "gi"), keywords: ["curl"], entropy: 2.75, }, { id: "curl-auth-user", description: "Discovered a potential basic authorization token provided in a curl command, which could compromise the curl accessed resource.", - regex: new RegExp("\\bcurl\\b(?:.*|.*(?:[\\r\\n]{1,2}.*){1,5})[ \\t\\n\\r](?:-u|--user)(?:=|[ \\t]{0,5})(\"(:[^\"]{3,}|[^:\"]{3,}:|[^:\"]{3,}:[^\"]{3,})\"|'([^:']{3,}:[^']{3,})'|((?:\"[^\"]{3,}\"|'[^']{3,}'|[\\w$@.-]+):(?:\"[^\"]{3,}\"|'[^']{3,}'|[\\w${}@.-]+)))(?:\\s|\\z)", "g"), + regex: new RegExp("\\bcurl\\b(?:.*|.*(?:[\\r\\n]{1,2}.*){1,5})[ \\t\\n\\r](?:-u|--user)(?:=|[ \\t]{0,5})(\"(:[^\"]{3,}|[^:\"]{3,}:|[^:\"]{3,}:[^\"]{3,})\"|'([^:']{3,}:[^']{3,})'|((?:\"[^\"]{3,}\"|'[^']{3,}'|[\\w$@.-]+):(?:\"[^\"]{3,}\"|'[^']{3,}'|[\\w${}@.-]+)))(?:\\s|$)", "g"), keywords: ["curl"], entropy: 2, }, { @@ -369,7 +369,7 @@ export const NAMED_RULES = [ { id: "etsy-access-token", description: "Found an Etsy Access Token, potentially compromising Etsy shop management and customer data.", - regex: new RegExp("[\\w.-]{0,50}?(?:(?-i:ETSY|[Ee]tsy))(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{24})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), + regex: new RegExp("[\\w.-]{0,50}?(?:(?:ETSY|[Ee]tsy))(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{24})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), keywords: ["etsy"], entropy: 3, }, { @@ -471,7 +471,7 @@ export const NAMED_RULES = [ { id: "generic-api-key", description: "Detected a Generic API Key, potentially exposing access to various services and sensitive operations.", - regex: new RegExp("[\\w.-]{0,50}?(?:access|auth|(?-i:[Aa]pi|API)|credential|creds|key|passw(?:or)?d|secret|token)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([\\w.=-]{10,150}|[a-z0-9][a-z0-9+/]{11,}={0,3})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), + regex: new RegExp("[\\w.-]{0,50}?(?:access|auth|(?:[Aa]pi|API)|credential|creds|key|passw(?:or)?d|secret|token)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([\\w.=-]{10,150}|[a-z0-9][a-z0-9+/]{11,}={0,3})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), keywords: ["access", "api", "auth", "key", "credential", "creds", "passwd", "password", "secret", "token"], entropy: 3.5, }, { @@ -633,7 +633,7 @@ export const NAMED_RULES = [ { id: "hashicorp-tf-api-token", description: "Uncovered a HashiCorp Terraform user/org API token, which may lead to unauthorized infrastructure management and security breaches.", - regex: new RegExp("[a-z0-9]{14}\\.(?-i:atlasv1)\\.[a-z0-9\\-_=]{60,70}", "gi"), + regex: new RegExp("[a-z0-9]{14}\\.(?:atlasv1)\\.[a-z0-9\\-_=]{60,70}", "gi"), keywords: ["atlasv1"], entropy: 3.5, }, { @@ -663,13 +663,13 @@ export const NAMED_RULES = [ { id: "huggingface-access-token", description: "Discovered a Hugging Face Access token, which could lead to unauthorized access to AI models and sensitive data.", - regex: new RegExp("\\b(hf_(?i:[a-z]{34}))(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "g"), + regex: new RegExp("\\b(hf_(?:[a-z]{34}))(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), keywords: ["hf_"], entropy: 2, }, { id: "huggingface-organization-api-token", description: "Uncovered a Hugging Face Organization API token, potentially compromising AI organization accounts and associated data.", - regex: new RegExp("\\b(api_org_(?i:[a-z]{34}))(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "g"), + regex: new RegExp("\\b(api_org_(?:[a-z]{34}))(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), keywords: ["api_org_"], entropy: 2, }, { @@ -723,7 +723,7 @@ export const NAMED_RULES = [ { id: "kubernetes-secret-yaml", description: "Possible Kubernetes Secret detected, posing a risk of leaking credentials/tokens from your deployments", - regex: new RegExp("(?:\\bkind:[ \\t]*[\"']?\\bsecret\\b[\"']?(?s:.){0,200}?\\bdata:(?s:.){0,100}?\\s+([\\w.-]+:(?:[ \\t]*(?:\\||>[-+]?)\\s+)?[ \\t]*(?:[\"']?[a-z0-9+/]{10,}={0,3}[\"']?|\\{\\{[ \\t\\w\"|$:=,.-]+}}|\"\"|''))|\\bdata:(?s:.){0,100}?\\s+([\\w.-]+:(?:[ \\t]*(?:\\||>[-+]?)\\s+)?[ \\t]*(?:[\"']?[a-z0-9+/]{10,}={0,3}[\"']?|\\{\\{[ \\t\\w\"|$:=,.-]+}}|\"\"|''))(?s:.){0,200}?\\bkind:[ \\t]*[\"']?\\bsecret\\b[\"']?)", "gi"), + regex: new RegExp("(?:\\bkind:[ \\t]*[\"']?\\bsecret\\b[\"']?(?:.){0,200}?\\bdata:(?:.){0,100}?\\s+([\\w.-]+:(?:[ \\t]*(?:\\||>[-+]?)\\s+)?[ \\t]*(?:[\"']?[a-z0-9+/]{10,}={0,3}[\"']?|\\{\\{[ \\t\\w\"|$:=,.-]+}}|\"\"|''))|\\bdata:(?:.){0,100}?\\s+([\\w.-]+:(?:[ \\t]*(?:\\||>[-+]?)\\s+)?[ \\t]*(?:[\"']?[a-z0-9+/]{10,}={0,3}[\"']?|\\{\\{[ \\t\\w\"|$:=,.-]+}}|\"\"|''))(?:.){0,200}?\\bkind:[ \\t]*[\"']?\\bsecret\\b[\"']?)", "gis"), keywords: ["secret"], }, { @@ -915,7 +915,7 @@ export const NAMED_RULES = [ { id: "okta-access-token", description: "Identified an Okta Access Token, which may compromise identity management services and user authentication data.", - regex: new RegExp("[\\w.-]{0,50}?(?i:[\\w.-]{0,50}?(?:(?-i:[Oo]kta|OKTA))(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3})(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}(00[\\w=\\-]{40})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "g"), + regex: new RegExp("[\\w.-]{0,50}?(?:[\\w.-]{0,50}?(?:(?:[Oo]kta|OKTA))(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3})(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}(00[\\w=\\-]{40})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), keywords: ["okta"], entropy: 4, }, { @@ -927,7 +927,7 @@ export const NAMED_RULES = [ { id: "openshift-user-token", description: "Found an OpenShift user token, potentially compromising an OpenShift/Kubernetes cluster.", - regex: new RegExp("\\b(sha256~[\\w-]{43})(?:[^\\w-]|\\z)", "g"), + regex: new RegExp("\\b(sha256~[\\w-]{43})(?:[^\\w-]|$)", "g"), keywords: ["sha256~"], entropy: 3.5, }, { @@ -993,7 +993,7 @@ export const NAMED_RULES = [ { id: "privateai-api-token", description: "Identified a PrivateAI Token, posing a risk of unauthorized access to AI services and data manipulation.", - regex: new RegExp("[\\w.-]{0,50}?(?i:[\\w.-]{0,50}?(?:private[_-]?ai)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3})(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{32})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "g"), + regex: new RegExp("[\\w.-]{0,50}?(?:[\\w.-]{0,50}?(?:private[_-]?ai)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3})(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{32})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), keywords: ["privateai", "private_ai", "private-ai"], entropy: 3, }, { @@ -1065,7 +1065,7 @@ export const NAMED_RULES = [ { id: "sentry-org-token", description: "Found a Sentry.io Organization Token, risking unauthorized access to error tracking services and sensitive application data.", - regex: new RegExp("\\bsntrys_eyJpYXQiO[a-zA-Z0-9+/]{10,200}(?:LCJyZWdpb25fdXJs|InJlZ2lvbl91cmwi|cmVnaW9uX3VybCI6)[a-zA-Z0-9+/]{10,200}={0,2}_[a-zA-Z0-9+/]{43}(?:[^a-zA-Z0-9+/]|\\z)", "g"), + regex: new RegExp("\\bsntrys_eyJpYXQiO[a-zA-Z0-9+/]{10,200}(?:LCJyZWdpb25fdXJs|InJlZ2lvbl91cmwi|cmVnaW9uX3VybCI6)[a-zA-Z0-9+/]{10,200}={0,2}_[a-zA-Z0-9+/]{43}(?:[^a-zA-Z0-9+/]|$)", "g"), keywords: ["sntrys_eyjpyxqio"], entropy: 4.5, }, { @@ -1227,19 +1227,19 @@ export const NAMED_RULES = [ { id: "sumologic-access-id", description: "Discovered a SumoLogic Access ID, potentially compromising log management services and data analytics integrity.", - regex: new RegExp("[\\w.-]{0,50}?(?i:[\\w.-]{0,50}?(?:(?-i:[Ss]umo|SUMO))(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3})(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}(su[a-zA-Z0-9]{12})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "g"), + regex: new RegExp("[\\w.-]{0,50}?(?:[\\w.-]{0,50}?(?:(?:[Ss]umo|SUMO))(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3})(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}(su[a-zA-Z0-9]{12})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), keywords: ["sumo"], entropy: 3, }, { id: "sumologic-access-token", description: "Uncovered a SumoLogic Access Token, which could lead to unauthorized access to log data and analytics insights.", - regex: new RegExp("[\\w.-]{0,50}?(?:(?-i:[Ss]umo|SUMO))(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{64})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), + regex: new RegExp("[\\w.-]{0,50}?(?:(?:[Ss]umo|SUMO))(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{64})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), keywords: ["sumo"], entropy: 3, }, { id: "telegram-bot-api-token", description: "Detected a Telegram Bot API Token, risking unauthorized bot operations and message interception on Telegram.", - regex: new RegExp("[\\w.-]{0,50}?(?:telegr)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([0-9]{5,16}:(?-i:A)[a-z0-9_\\-]{34})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), + regex: new RegExp("[\\w.-]{0,50}?(?:telegr)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([0-9]{5,16}:(?:A)[a-z0-9_\\-]{34})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), keywords: ["telegr"], }, { @@ -1305,7 +1305,7 @@ export const NAMED_RULES = [ { id: "vault-service-token", description: "Identified a Vault Service Token, potentially compromising infrastructure security and access to sensitive credentials.", - regex: new RegExp("\\b((?:hvs\\.[\\w-]{90,120}|s\\.(?i:[a-z0-9]{24})))(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "g"), + regex: new RegExp("\\b((?:hvs\\.[\\w-]{90,120}|s\\.(?:[a-z0-9]{24})))(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), keywords: ["hvs.", "s."], entropy: 3.5, }, { diff --git a/dist/engine/rules.js.map b/dist/engine/rules.js.map index 835f5ee..e530fd5 100755 --- a/dist/engine/rules.js.map +++ b/dist/engine/rules.js.map @@ -1 +1 @@ -{"version":3,"file":"rules.js","sourceRoot":"","sources":["../../src/engine/rules.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"rules.js","sourceRoot":"","sources":["../../src/engine/rules.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/engine/scanner.d.ts b/dist/engine/scanner.d.ts index b8ccfd6..04b92ba 100755 --- a/dist/engine/scanner.d.ts +++ b/dist/engine/scanner.d.ts @@ -15,4 +15,47 @@ export declare function scanAndRedact(content: string, source: Finding["source"] redacted: string; findings: Finding[]; }; +/** Hard ceiling to avoid unbounded memory/time on absurd input. */ +export declare const MAX_BOUNDED_SCAN_BYTES: number; +/** Explicit marker used when content cannot be inspected safely. */ +export declare const UNSCANNABLE_MARKER = "[REDACTED:strangerdanger-oversized-unscannable]"; +export type BoundedScanResult = { + redacted: string; + findings: Finding[]; + /** True when the whole input was inspected. */ + complete: boolean; + /** Machine-readable reason when `complete` is false. */ + policy?: "oversized" | "window-timeout" | "window-error"; +}; +/** + * Scan and redact arbitrarily large text without silently truncating it. + * + * Large content is scanned in overlapping windows so secrets that cross a + * window boundary are still detected. Findings are collected across windows, + * deduplicated, and applied once to the whole input, so non-sensitive content + * is preserved byte-for-byte. If a window cannot be inspected (timeout, error, + * or input beyond the hard ceiling), the result fails closed with an explicit + * marker and `complete: false` rather than emitting uninspected content. + */ +export declare function scanAndRedactBounded(content: string, source: Finding["source"], filePath?: string): BoundedScanResult; +/** + * Safe, machine-readable projection of a finding. Never includes the raw + * matched secret, so it is safe to print, log, or return from a tool. + */ +export type SafeFinding = { + ruleId: string; + description: string; + source: Finding["source"]; + /** 1-based line number, when the finding has a location in text. */ + line?: number; + /** 0-based column on `line`. */ + column?: number; + offset: number; + filePath?: string; + fingerprint: string; + /** The redaction token that replaced the secret. */ + redaction: string; +}; +/** Convert a finding to a secret-free projection for output. */ +export declare function toSafeFinding(finding: Finding, content?: string): SafeFinding; //# sourceMappingURL=scanner.d.ts.map \ No newline at end of file diff --git a/dist/engine/scanner.d.ts.map b/dist/engine/scanner.d.ts.map index bb6e473..1dcec5e 100755 --- a/dist/engine/scanner.d.ts.map +++ b/dist/engine/scanner.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../../src/engine/scanner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AA4D1C;;GAEG;AACH,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,EAAE,CAsF7F;AAED;;;GAGG;AACH,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAyBnE;AAED;;GAEG;AACH,wBAAgB,aAAa,CACzB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,EACzB,QAAQ,CAAC,EAAE,MAAM,GAClB;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,EAAE,CAAA;CAAE,CAM3C"} \ No newline at end of file +{"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../../src/engine/scanner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AA4D1C;;GAEG;AACH,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,EAAE,CAgG7F;AAED;;;GAGG;AACH,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAgCnE;AAED;;GAEG;AACH,wBAAgB,aAAa,CACzB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,EACzB,QAAQ,CAAC,EAAE,MAAM,GAClB;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,EAAE,CAAA;CAAE,CAM3C;AAeD,mEAAmE;AACnE,eAAO,MAAM,sBAAsB,QAAoB,CAAC;AACxD,oEAAoE;AACpE,eAAO,MAAM,kBAAkB,oDAAoD,CAAC;AAEpF,MAAM,MAAM,iBAAiB,GAAG;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,+CAA+C;IAC/C,QAAQ,EAAE,OAAO,CAAC;IAClB,wDAAwD;IACxD,MAAM,CAAC,EAAE,WAAW,GAAG,gBAAgB,GAAG,cAAc,CAAC;CAC5D,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAChC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,EACzB,QAAQ,CAAC,EAAE,MAAM,GAClB,iBAAiB,CAiDnB;AAED;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC1B,oEAAoE;IACpE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gCAAgC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,oDAAoD;IACpD,SAAS,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,gEAAgE;AAChE,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,WAAW,CAkB7E"} \ No newline at end of file diff --git a/dist/engine/scanner.js b/dist/engine/scanner.js index bcfcc85..17b735b 100755 --- a/dist/engine/scanner.js +++ b/dist/engine/scanner.js @@ -59,7 +59,16 @@ function hasKeyword(text, keywords) { */ export function scan(content, source, filePath) { const findings = []; + // Whole-content keyword prefilter. A rule whose `keywords` do not appear + // anywhere cannot match (its regex requires the keyword in context), so it + // is skipped entirely. This turns the scan from "run 236 regexes over every + // character" into "run only the handful of plausible rules", which is the + // difference between ~100 s and a few ms on a multi-megabyte line. + const lowerContent = content.toLowerCase(); for (const rule of [...NAMED_RULES, ...CLEAN_ROOM_RULES]) { + if (rule.keywords.length > 0 && !rule.keywords.some((kw) => lowerContent.includes(kw.toLowerCase()))) { + continue; + } rule.regex.lastIndex = 0; let match; while ((match = rule.regex.exec(content)) !== null) { @@ -142,7 +151,12 @@ export function scan(content, source, filePath) { export function redact(content, findings) { if (findings.length === 0) return content; - const sorted = [...findings].sort((a, b) => a.offset - b.offset); + // Defensive: ignore findings that do not point at real, non-empty ranges of + // this content (bad offsets from custom callers, or empty matches). + const valid = findings.filter((f) => f.offset >= 0 && f.offset < content.length && f.match.length > 0 && f.offset + f.match.length <= content.length); + if (valid.length === 0) + return content; + const sorted = [...valid].sort((a, b) => a.offset - b.offset); const nonOverlapping = []; let lastEnd = 0; for (const finding of sorted) { @@ -171,4 +185,95 @@ export function scanAndRedact(content, source, filePath) { const redacted = redact(content, findings); return { redacted, findings }; } +/** + * Size of each scanning window for large content. Content at or below this size + * is scanned in a single pass. + */ +const SCAN_WINDOW = 1024 * 1024; +/** + * Overlap between consecutive windows. Must be at least the length of the + * longest match any rule can produce so a secret that straddles a window + * boundary is still fully contained in one window. + */ +const SCAN_OVERLAP = 64 * 1024; +/** Per-window time budget. A window that exceeds it makes the scan fail closed. */ +const SCAN_WINDOW_BUDGET_MS = 2500; +/** Hard ceiling to avoid unbounded memory/time on absurd input. */ +export const MAX_BOUNDED_SCAN_BYTES = 256 * 1024 * 1024; +/** Explicit marker used when content cannot be inspected safely. */ +export const UNSCANNABLE_MARKER = "[REDACTED:strangerdanger-oversized-unscannable]"; +/** + * Scan and redact arbitrarily large text without silently truncating it. + * + * Large content is scanned in overlapping windows so secrets that cross a + * window boundary are still detected. Findings are collected across windows, + * deduplicated, and applied once to the whole input, so non-sensitive content + * is preserved byte-for-byte. If a window cannot be inspected (timeout, error, + * or input beyond the hard ceiling), the result fails closed with an explicit + * marker and `complete: false` rather than emitting uninspected content. + */ +export function scanAndRedactBounded(content, source, filePath) { + if (content.length === 0) { + return { redacted: content, findings: [], complete: true }; + } + if (content.length <= SCAN_WINDOW) { + const { redacted, findings } = scanAndRedact(content, source, filePath); + return { redacted, findings, complete: true }; + } + if (content.length > MAX_BOUNDED_SCAN_BYTES) { + return { redacted: UNSCANNABLE_MARKER, findings: [], complete: false, policy: "oversized" }; + } + const stride = SCAN_WINDOW - SCAN_OVERLAP; + const collected = []; + const seen = new Set(); + for (let start = 0; start < content.length; start += stride) { + const end = Math.min(start + SCAN_WINDOW, content.length); + const window = content.slice(start, end); + const windowStart = Date.now(); + let windowFindings; + try { + windowFindings = scan(window, source, filePath); + } + catch { + return { redacted: UNSCANNABLE_MARKER, findings: collected, complete: false, policy: "window-error" }; + } + if (Date.now() - windowStart > SCAN_WINDOW_BUDGET_MS) { + return { redacted: UNSCANNABLE_MARKER, findings: collected, complete: false, policy: "window-timeout" }; + } + for (const f of windowFindings) { + const absolute = { ...f, offset: start + f.offset }; + const key = `${absolute.ruleId}:${absolute.offset}:${absolute.match.length}`; + if (!seen.has(key)) { + seen.add(key); + collected.push(absolute); + } + } + if (end >= content.length) + break; + } + const allowlist = loadAllowlist(); + const findings = filterByAllowlist(collected, allowlist); + return { redacted: redact(content, findings), findings, complete: true }; +} +/** Convert a finding to a secret-free projection for output. */ +export function toSafeFinding(finding, content) { + const fpMatch = finding.redacted.match(/:([a-f0-9]{8})\]/); + const safe = { + ruleId: finding.ruleId, + description: finding.description, + source: finding.source, + offset: finding.offset, + fingerprint: fpMatch ? fpMatch[1] : "", + redaction: finding.redacted, + }; + if (finding.filePath) + safe.filePath = finding.filePath; + if (content !== undefined) { + const before = content.slice(0, finding.offset); + const lastNewline = before.lastIndexOf("\n"); + safe.line = before.split("\n").length; + safe.column = finding.offset - (lastNewline + 1); + } + return safe; +} //# sourceMappingURL=scanner.js.map \ No newline at end of file diff --git a/dist/engine/scanner.js.map b/dist/engine/scanner.js.map index bc2b8e7..f654c5e 100755 --- a/dist/engine/scanner.js.map +++ b/dist/engine/scanner.js.map @@ -1 +1 @@ -{"version":3,"file":"scanner.js","sourceRoot":"","sources":["../../src/engine/scanner.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC1E,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC3F,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAClE,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAEvD,MAAM,gBAAgB,GAAG,YAAY,CAAC;AACtC,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAE7B,SAAS,kBAAkB,CAAC,MAAc,EAAE,KAAa;IACrD,OAAO,GAAG,gBAAgB,GAAG,MAAM,IAAI,WAAW,CAAC,KAAK,CAAC,GAAG,gBAAgB,EAAE,CAAC;AACnF,CAAC;AAED;;;GAGG;AACH,SAAS,mBAAmB,CAAC,QAAmB;IAC1C,IAAI,QAAQ,CAAC,MAAM,IAAI,CAAC;QAAE,OAAO,QAAQ,CAAC;IAE1C,WAAW;IACX,yDAAyD;IACzD,sCAAsC;IACtC,2DAA2D;IAC3D,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,CAAC,iBAAiB,EAAE,mBAAmB,CAAC,CAAC,CAAC;IACxE,MAAM,MAAM,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACvC,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC;QAChD,IAAI,OAAO,KAAK,CAAC;YAAE,OAAO,OAAO,CAAC;QAClC,MAAM,UAAU,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC;QACvC,IAAI,UAAU,KAAK,CAAC;YAAE,OAAO,UAAU,CAAC;QACxC,wDAAwD;QACxD,MAAM,QAAQ,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;QAC7C,MAAM,QAAQ,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;QAC7C,IAAI,QAAQ,KAAK,QAAQ;YAAE,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACpD,OAAO,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;IACH,MAAM,OAAO,GAAc,EAAE,CAAC;IAE9B,KAAK,MAAM,OAAO,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;QAC9B,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC;QACnD,sFAAsF;QACtF,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;YACnC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;YAC3B,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;YAC7C,OAAO,MAAM,GAAG,IAAI,IAAI,IAAI,GAAG,MAAM,CAAC;QAC1C,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,EAAE,CAAC;YACZ,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC1B,CAAC;IACL,CAAC;IACD,mDAAmD;IACnD,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC;AACvD,CAAC;AAEH,SAAS,UAAU,CAAC,IAAY,EAAE,QAAkB;IAChD,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;IACjC,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;AACnE,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,IAAI,CAAC,OAAe,EAAE,MAAyB,EAAE,QAAiB;IAC9E,MAAM,QAAQ,GAAc,EAAE,CAAC;IAE/B,KAAK,MAAM,IAAI,IAAI,CAAC,GAAG,WAAW,EAAE,GAAG,gBAAgB,CAAC,EAAE,CAAC;QACvD,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC;QACzB,IAAI,KAA6B,CAAC;QAClC,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;YACjD,MAAM,WAAW,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC;YACzC,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,KAAK,SAAS;gBACjC,CAAC,CAAC,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;gBAC1C,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC;YAClB,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACzB,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC;gBACvC,SAAS;YACb,CAAC;YAED,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC3B,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,EAAE,CAAC,CAAC;gBAC9C,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;gBAC9E,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;gBACxD,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;oBACtC,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC;oBACvC,SAAS;gBACb,CAAC;YACL,CAAC;YAED,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;gBAC7B,MAAM,OAAO,GAAG,cAAc,CAAC,WAAW,CAAC,CAAC;gBAC5C,IAAI,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;oBACzB,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC;oBACvC,SAAS;gBACb,CAAC;YACL,CAAC;YAED,QAAQ,CAAC,IAAI,CAAC;gBACV,MAAM,EAAE,IAAI,CAAC,EAAE;gBACf,WAAW,EAAE,IAAI,CAAC,WAAW;gBAC7B,KAAK,EAAE,WAAW;gBAClB,QAAQ,EAAE,kBAAkB,CAAC,IAAI,CAAC,EAAE,EAAE,WAAW,CAAC;gBAClD,MAAM;gBACN,MAAM;gBACN,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACpC,CAAC,CAAC;QACP,CAAC;IACL,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,aAAa,EAAE,CAAC;QAC/B,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC;QACzB,IAAI,KAA6B,CAAC;QAClC,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;YACjD,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC;YACvC,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,KAAK,SAAS;gBACjC,CAAC,CAAC,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;gBAC1C,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC;YAClB,IAAI,SAAS,CAAC,MAAM,GAAG,EAAE,EAAE,CAAC;gBACxB,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC;gBACvC,SAAS;YACb,CAAC;YAED,IAAI,SAAS,GAAG,GAAG,CAAC;YAEpB,IAAI,cAAc,CAAC,SAAS,EAAE,cAAc,CAAC,EAAE,CAAC;gBAC5C,SAAS,GAAG,GAAG,CAAC;YACpB,CAAC;iBAAM,IAAI,cAAc,CAAC,SAAS,EAAE,WAAW,CAAC,EAAE,CAAC;gBAChD,SAAS,GAAG,GAAG,CAAC;YACpB,CAAC;YAED,MAAM,OAAO,GAAG,cAAc,CAAC,SAAS,CAAC,CAAC;YAC1C,IAAI,OAAO,GAAG,SAAS,EAAE,CAAC;gBACtB,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC;gBACvC,SAAS;YACb,CAAC;YAED,QAAQ,CAAC,IAAI,CAAC;gBACV,MAAM,EAAE,IAAI,CAAC,EAAE;gBACf,WAAW,EAAE,IAAI,CAAC,WAAW;gBAC7B,KAAK,EAAE,SAAS;gBAChB,QAAQ,EAAE,kBAAkB,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,CAAC;gBAChD,MAAM;gBACN,MAAM;gBACN,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACpC,CAAC,CAAC;QACP,CAAC;IACL,CAAC;IAED,OAAO,mBAAmB,CAAC,QAAQ,CAAC,CAAC;AACzC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,MAAM,CAAC,OAAe,EAAE,QAAmB;IACvD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,OAAO,CAAC;IAE1C,MAAM,MAAM,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC;IACjE,MAAM,cAAc,GAAc,EAAE,CAAC;IACrC,IAAI,OAAO,GAAG,CAAC,CAAC;IAEhB,KAAK,MAAM,OAAO,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,OAAO,CAAC,MAAM,IAAI,OAAO,EAAE,CAAC;YAC5B,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC7B,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC;QACpD,CAAC;IACL,CAAC;IAED,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,IAAI,OAAO,GAAG,CAAC,CAAC;IAEhB,KAAK,MAAM,OAAO,IAAI,cAAc,EAAE,CAAC;QACnC,MAAM,IAAI,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;QACjD,MAAM,IAAI,OAAO,CAAC,QAAQ,CAAC;QAC3B,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC;IACpD,CAAC;IAED,MAAM,IAAI,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACjC,OAAO,MAAM,CAAC;AAClB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,aAAa,CACzB,OAAe,EACf,MAAyB,EACzB,QAAiB;IAEjB,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;IACpD,MAAM,SAAS,GAAG,aAAa,EAAE,CAAC;IAClC,MAAM,QAAQ,GAAG,iBAAiB,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;IAC3D,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAC3C,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;AAClC,CAAC"} \ No newline at end of file +{"version":3,"file":"scanner.js","sourceRoot":"","sources":["../../src/engine/scanner.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/engine/types.d.ts b/dist/engine/types.d.ts index b557a79..af6f9e6 100755 --- a/dist/engine/types.d.ts +++ b/dist/engine/types.d.ts @@ -21,6 +21,7 @@ export type AuditEntry = { ruleId: string; description: string; fingerprint: string; + filePath?: string; }; export type AllowlistEntry = { description?: string; diff --git a/dist/engine/types.d.ts.map b/dist/engine/types.d.ts.map index cfa4105..ef51d99 100755 --- a/dist/engine/types.d.ts.map +++ b/dist/engine/types.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/engine/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,IAAI,GAAG;IACf,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;IAClD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACpB,OAAO,EAAE,cAAc,EAAE,CAAC;CAC7B,CAAC"} \ No newline at end of file +{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/engine/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,IAAI,GAAG;IACf,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;IAClD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACpB,OAAO,EAAE,cAAc,EAAE,CAAC;CAC7B,CAAC"} \ No newline at end of file diff --git a/dist/plugin/index.d.ts b/dist/plugin/index.d.ts index abd6c84..a2218b0 100755 --- a/dist/plugin/index.d.ts +++ b/dist/plugin/index.d.ts @@ -1,4 +1,22 @@ -import { type Plugin } from "@opencode-ai/plugin"; -export declare const strangerDanger: Plugin; +import { Plugin } from "@opencode/plugin"; +/** + * strangerDanger — OpenCode V2 plugin. + * + * Protection boundaries (all supported V2 APIs): + * - `session.prompt` → redact the incoming user prompt before admission. + * - `tool.execute.after` → redact tool results (read/bash/grep/fetch/...). + * - `session.context` → redact the assembled model request (system + messages). + * - `session.compaction` → redact the transcript being summarized. + * - `tool.execute.before` → read-only detection/audit for file tools (never writes). + * + * The V1 `event` callback is migrated to `ctx.event.subscribe()` and is used for + * detection/audit only: the event stream is observational and cannot prevent an + * already-admitted request from being transmitted. + * + * Known limitation: provider-native WebSocket traffic that bypasses the + * session HTTP pipeline is not covered by these hooks. Everything routed + * through the session model-request pipeline is. + */ +export declare const strangerDanger: Plugin.Plugin; export default strangerDanger; //# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/dist/plugin/index.d.ts.map b/dist/plugin/index.d.ts.map index 11247df..ed1957e 100755 --- a/dist/plugin/index.d.ts.map +++ b/dist/plugin/index.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plugin/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAQ,MAAM,qBAAqB,CAAC;AAYxD,eAAO,MAAM,cAAc,EAAE,MAoG5B,CAAC;AAEF,eAAe,cAAc,CAAC"} \ No newline at end of file +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plugin/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAS1C;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,cAAc,eA6FzB,CAAC;AAEH,eAAe,cAAc,CAAC"} \ No newline at end of file diff --git a/dist/plugin/index.js b/dist/plugin/index.js index d9c4a77..d9f7511 100755 --- a/dist/plugin/index.js +++ b/dist/plugin/index.js @@ -1,109 +1,104 @@ -import { tool } from "@opencode-ai/plugin"; -import { z } from "zod"; -import { scanAndRedact } from "../engine/scanner.js"; -import { getAuditPath, readAuditLog } from "../engine/audit.js"; -import { loadAllowlist, saveAllowlistEntry } from "../engine/allowlist.js"; -import { NAMED_RULES, ENTROPY_RULES, CLEAN_ROOM_RULES } from "../engine/rules.js"; +import { Plugin } from "@opencode/plugin"; +import { resolveAuditPath } from "../engine/audit.js"; import { handleLayer1 } from "./layer1.js"; -import { handleLayer2Compaction } from "./layer2-compaction.js"; -import { handleLayer2Message } from "./layer2-message.js"; import { handleLayer3 } from "./layer3.js"; -export const strangerDanger = async (_ctx) => { - const allRules = [...NAMED_RULES, ...CLEAN_ROOM_RULES, ...ENTROPY_RULES]; - return { - "tool.execute.before": handleLayer1, - "experimental.session.compacting": handleLayer2Compaction, - event: handleLayer2Message, - "tool.execute.after": handleLayer3, - tool: { - sd_status: tool({ +import { handlePrompt, handleContext, handleMessageEvent } from "./layer2-message.js"; +import { handleCompaction } from "./layer2-compaction.js"; +import { sdStatus, sdAudit, sdAllowlistAdd, sdAllowlistShow, sdScan } from "./tools.js"; +/** + * strangerDanger — OpenCode V2 plugin. + * + * Protection boundaries (all supported V2 APIs): + * - `session.prompt` → redact the incoming user prompt before admission. + * - `tool.execute.after` → redact tool results (read/bash/grep/fetch/...). + * - `session.context` → redact the assembled model request (system + messages). + * - `session.compaction` → redact the transcript being summarized. + * - `tool.execute.before` → read-only detection/audit for file tools (never writes). + * + * The V1 `event` callback is migrated to `ctx.event.subscribe()` and is used for + * detection/audit only: the event stream is observational and cannot prevent an + * already-admitted request from being transmitted. + * + * Known limitation: provider-native WebSocket traffic that bypasses the + * session HTTP pipeline is not covered by these hooks. Everything routed + * through the session model-request pipeline is. + */ +export const strangerDanger = Plugin.define({ + id: "gitdamnit.stranger-danger", + async setup(ctx) { + const auditPath = resolveAuditPath({ directory: ctx.location?.directory }); + const audit = { auditPath }; + // --- Tools --------------------------------------------------------- + await ctx.tool.transform((editor) => { + editor.add({ + name: "sd_status", description: "Show strangerDanger status — rule count, findings this session, audit log path", - args: {}, - async execute() { - const ruleCount = allRules.length; - const auditPath = getAuditPath(); - let findingCount = 0; - try { - const log = readAuditLog(); - findingCount = log.split("\n").filter((l) => l.trim()).length; - } - catch { - // Audit log may not exist yet - } - return `strangerDanger v0.1.0\nRules: ${ruleCount}\nFindings this session: ${findingCount}\nAudit log: ${auditPath}`; - }, - }), - sd_audit: tool({ - description: "Show audit log for current session — all redactions made, with timestamps and rule IDs", - args: {}, - async execute() { - try { - const log = readAuditLog(); - if (!log.trim()) - return "No audit entries yet."; - const lines = log.trim().split("\n"); - const entries = lines.map((l) => { - try { - const entry = JSON.parse(l); - return `[${entry.timestamp}] ${entry.ruleId} — fingerprint: ${entry.fingerprint} — source: ${entry.source}`; - } - catch { - return l; - } - }); - return entries.join("\n"); - } - catch { - return "Audit log not found."; - } - }, - }), - sd_allowlist_add: tool({ + input: { type: "object", properties: {}, additionalProperties: false }, + execute: async () => ({ content: sdStatus({ auditPath }) }), + }); + editor.add({ + name: "sd_audit", + description: "Show the strangerDanger audit log — redactions made, with timestamps and rule IDs (no raw secrets)", + input: { type: "object", properties: {}, additionalProperties: false }, + execute: async () => ({ content: sdAudit({ auditPath }) }), + }); + editor.add({ + name: "sd_allowlist_add", description: "Add an entry to the allowlist — by fingerprint, ruleId, path, or regex", - args: { - description: z.string().optional(), - fingerprint: z.string().optional(), - ruleId: z.string().optional(), - path: z.string().optional(), - regex: z.string().optional(), - }, - async execute(args) { - saveAllowlistEntry({ - description: args.description, - fingerprint: args.fingerprint, - ruleId: args.ruleId, - path: args.path, - regex: args.regex, - }); - return "Allowlist entry added."; + input: { + type: "object", + properties: { + description: { type: "string" }, + fingerprint: { type: "string" }, + ruleId: { type: "string" }, + path: { type: "string" }, + regex: { type: "string" }, + }, + additionalProperties: false, }, - }), - sd_allowlist_show: tool({ + execute: async (input) => ({ content: sdAllowlistAdd(input ?? {}) }), + }); + editor.add({ + name: "sd_allowlist_show", description: "Show current allowlist entries", - args: {}, - async execute() { - const allowlist = loadAllowlist(); - if (allowlist.entries.length === 0) - return "No allowlist entries."; - return allowlist.entries.map((e, i) => `${i + 1}. ${e.description || "(no description)"} | fingerprint: ${e.fingerprint || "—"} | ruleId: ${e.ruleId || "—"} | path: ${e.path || "—"} | regex: ${e.regex || "—"}`).join("\n"); - }, - }), - sd_scan: tool({ - description: "Manually scan a file or string against all rules — useful for testing", - args: { - content: z.string(), - source: z.enum(["file", "context", "output", "message"]).optional(), - }, - async execute(args) { - const src = args.source || "file"; - const { findings } = scanAndRedact(args.content, src); - if (findings.length === 0) - return "No secrets detected."; - return findings.map((f) => `${f.ruleId}: ${f.description}\n Redacted: ${f.redacted}\n Offset: ${f.offset}`).join("\n\n"); + input: { type: "object", properties: {}, additionalProperties: false }, + execute: async () => ({ content: sdAllowlistShow() }), + }); + editor.add({ + name: "sd_scan", + description: "Manually scan a file or string against all rules — returns secret-free findings metadata", + input: { + type: "object", + properties: { + content: { type: "string" }, + source: { type: "string", enum: ["file", "context", "output", "message"] }, + }, + required: ["content"], + additionalProperties: false, }, - }), - }, - }; -}; + execute: async (input) => ({ content: sdScan(input) }), + }); + }); + // --- Hooks --------------------------------------------------------- + await ctx.tool.hook("execute.before", (event) => handleLayer1({ tool: event.tool, sessionID: String(event.sessionID), input: event.input }, audit)); + await ctx.tool.hook("execute.after", (event) => handleLayer3(event, audit)); + await ctx.session.hook("prompt", (event) => handlePrompt({ sessionID: String(event.sessionID), prompt: event.prompt }, audit)); + await ctx.session.hook("context", (event) => handleContext({ sessionID: String(event.sessionID), system: event.system, messages: event.messages }, audit)); + await ctx.session.hook("compaction", (event) => handleCompaction({ sessionID: String(event.sessionID), messages: event.messages, result: event.result }, audit)); + // --- Event subscription (detection/audit only) --------------------- + const controller = new AbortController(); + void (async () => { + try { + for await (const event of ctx.event.subscribe({ signal: controller.signal })) { + handleMessageEvent(event, audit); + } + } + catch { + // Stream ended or aborted. + } + })(); + return () => controller.abort(); + }, +}); export default strangerDanger; //# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dist/plugin/index.js.map b/dist/plugin/index.js.map index ff2e5db..2a0866c 100755 --- a/dist/plugin/index.js.map +++ b/dist/plugin/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/plugin/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/plugin/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACtF,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAGxF;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,CAAC;IACxC,EAAE,EAAE,2BAA2B;IAC/B,KAAK,CAAC,KAAK,CAAC,GAAG;QACX,MAAM,SAAS,GAAG,gBAAgB,CAAC,EAAE,SAAS,EAAE,GAAG,CAAC,QAAQ,EAAE,SAAS,EAAE,CAAC,CAAC;QAC3E,MAAM,KAAK,GAAiB,EAAE,SAAS,EAAE,CAAC;QAE1C,sEAAsE;QACtE,MAAM,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,EAAE,EAAE;YAChC,MAAM,CAAC,GAAG,CAAC;gBACP,IAAI,EAAE,WAAW;gBACjB,WAAW,EAAE,gFAAgF;gBAC7F,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,EAAE,oBAAoB,EAAE,KAAK,EAAS;gBAC7E,OAAO,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;aAC9D,CAAC,CAAC;YACH,MAAM,CAAC,GAAG,CAAC;gBACP,IAAI,EAAE,UAAU;gBAChB,WAAW,EAAE,oGAAoG;gBACjH,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,EAAE,oBAAoB,EAAE,KAAK,EAAS;gBAC7E,OAAO,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;aAC7D,CAAC,CAAC;YACH,MAAM,CAAC,GAAG,CAAC;gBACP,IAAI,EAAE,kBAAkB;gBACxB,WAAW,EAAE,wEAAwE;gBACrF,KAAK,EAAE;oBACH,IAAI,EAAE,QAAQ;oBACd,UAAU,EAAE;wBACR,WAAW,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;wBAC/B,WAAW,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;wBAC/B,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;wBAC1B,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;wBACxB,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;qBAC5B;oBACD,oBAAoB,EAAE,KAAK;iBACvB;gBACR,OAAO,EAAE,KAAK,EAAE,KAAU,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,cAAc,CAAC,KAAK,IAAI,EAAE,CAAC,EAAE,CAAC;aAC5E,CAAC,CAAC;YACH,MAAM,CAAC,GAAG,CAAC;gBACP,IAAI,EAAE,mBAAmB;gBACzB,WAAW,EAAE,gCAAgC;gBAC7C,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,EAAE,oBAAoB,EAAE,KAAK,EAAS;gBAC7E,OAAO,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,eAAe,EAAE,EAAE,CAAC;aACxD,CAAC,CAAC;YACH,MAAM,CAAC,GAAG,CAAC;gBACP,IAAI,EAAE,SAAS;gBACf,WAAW,EAAE,0FAA0F;gBACvG,KAAK,EAAE;oBACH,IAAI,EAAE,QAAQ;oBACd,UAAU,EAAE;wBACR,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;wBAC3B,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,CAAC,EAAE;qBAC7E;oBACD,QAAQ,EAAE,CAAC,SAAS,CAAC;oBACrB,oBAAoB,EAAE,KAAK;iBACvB;gBACR,OAAO,EAAE,KAAK,EAAE,KAAU,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;aAC9D,CAAC,CAAC;QACP,CAAC,CAAC,CAAC;QAEH,sEAAsE;QACtE,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC,KAAK,EAAE,EAAE,CAC5C,YAAY,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,SAAS,EAAE,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,EAAE,KAAK,CAAC,CACpG,CAAC;QAEF,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC,KAAK,EAAE,EAAE,CAC3C,YAAY,CAAC,KAAY,EAAE,KAAK,CAAC,CACpC,CAAC;QAEF,MAAM,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CACvC,YAAY,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,MAAa,EAAE,EAAE,KAAK,CAAC,CAC3F,CAAC;QAEF,MAAM,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE,CACxC,aAAa,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,EAAE,KAAK,CAAC,CAC/G,CAAC;QAEF,MAAM,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,KAAK,EAAE,EAAE,CAC3C,gBAAgB,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,MAAa,EAAE,EAAE,KAAK,CAAC,CACzH,CAAC;QAEF,sEAAsE;QACtE,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,KAAK,CAAC,KAAK,IAAI,EAAE;YACb,IAAI,CAAC;gBACD,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC;oBAC3E,kBAAkB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;gBACrC,CAAC;YACL,CAAC;YAAC,MAAM,CAAC;gBACL,2BAA2B;YAC/B,CAAC;QACL,CAAC,CAAC,EAAE,CAAC;QAEL,OAAO,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;IACpC,CAAC;CACJ,CAAC,CAAC;AAEH,eAAe,cAAc,CAAC"} \ No newline at end of file diff --git a/dist/plugin/layer1.d.ts b/dist/plugin/layer1.d.ts index ae2b17b..d0855b7 100755 --- a/dist/plugin/layer1.d.ts +++ b/dist/plugin/layer1.d.ts @@ -1,8 +1,24 @@ -export declare function handleLayer1(input: { +import { type AuditContext } from "./redact.js"; +export type Layer1Event = { tool: string; - sessionID: string; - callID: string; -}, output: { - args: any; -}): Promise; + sessionID?: string; + input?: unknown; +}; +/** + * Pre-execution inspection for file-oriented tools. + * + * IMPORTANT: this hook is strictly read-only. Reading a file must never modify + * its contents. Earlier versions rewrote the file on disk when they detected a + * secret, which silently corrupted the user's working tree. This hook now only + * *detects* secrets and records an audit entry. Redaction of what the model + * actually sees happens at the supported V2 boundaries: + * + * - `ctx.tool.hook("execute.after")` rewrites the tool result (layer 3). + * - `ctx.session.hook("context")` rewrites the assembled model request. + * + * Files that cannot be safely inspected (non-regular files such as FIFOs or + * device nodes, binaries, or very large files) are skipped here and left to the + * result/context boundaries, so a `read` never blocks on a named pipe. + */ +export declare function handleLayer1(event: Layer1Event, ctx: AuditContext): Promise; //# sourceMappingURL=layer1.d.ts.map \ No newline at end of file diff --git a/dist/plugin/layer1.d.ts.map b/dist/plugin/layer1.d.ts.map index 79d5592..4e8e4f9 100755 --- a/dist/plugin/layer1.d.ts.map +++ b/dist/plugin/layer1.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"layer1.d.ts","sourceRoot":"","sources":["../../src/plugin/layer1.ts"],"names":[],"mappings":"AAsBA,wBAAsB,YAAY,CAC9B,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EAC1D,MAAM,EAAE;IAAE,IAAI,EAAE,GAAG,CAAA;CAAE,GACtB,OAAO,CAAC,IAAI,CAAC,CA0Cf"} \ No newline at end of file +{"version":3,"file":"layer1.d.ts","sourceRoot":"","sources":["../../src/plugin/layer1.ts"],"names":[],"mappings":"AAGA,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAehE,MAAM,MAAM,WAAW,GAAG;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,YAAY,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA6BvF"} \ No newline at end of file diff --git a/dist/plugin/layer1.js b/dist/plugin/layer1.js index c83399d..0f87ec0 100755 --- a/dist/plugin/layer1.js +++ b/dist/plugin/layer1.js @@ -1,8 +1,11 @@ -import { readFileSync } from "node:fs"; -import { statSync } from "node:fs"; -import { writeFileSync } from "node:fs"; -import { scanAndRedact } from "../engine/scanner.js"; -import { writeAuditBatch } from "../engine/audit.js"; +import { readFileSync, statSync } from "node:fs"; +import { scan } from "../engine/scanner.js"; +import { loadAllowlist, filterByAllowlist } from "../engine/allowlist.js"; +import { recordFindings } from "./redact.js"; +/** + * A file larger than this is not inspected here. The tool-result boundary + * (layer 3) still handles it with bounded, chunk-safe scanning. + */ const MAX_FILE_SIZE = 10 * 1024 * 1024; function isBinary(content) { for (let i = 0; i < Math.min(content.length, 8192); i++) { @@ -11,21 +14,30 @@ function isBinary(content) { } return false; } -function isSensitiveDotEnvPath(filePath) { - const normalized = filePath.replace(/\\/g, "/"); - if (/(^|\/)\.env\.example$/.test(normalized)) - return false; - return /(^|\/)\.env($|\.)/.test(normalized); -} -export async function handleLayer1(input, output) { - const filePath = output.args?.filePath; - if (!filePath) - return; +/** + * Pre-execution inspection for file-oriented tools. + * + * IMPORTANT: this hook is strictly read-only. Reading a file must never modify + * its contents. Earlier versions rewrote the file on disk when they detected a + * secret, which silently corrupted the user's working tree. This hook now only + * *detects* secrets and records an audit entry. Redaction of what the model + * actually sees happens at the supported V2 boundaries: + * + * - `ctx.tool.hook("execute.after")` rewrites the tool result (layer 3). + * - `ctx.session.hook("context")` rewrites the assembled model request. + * + * Files that cannot be safely inspected (non-regular files such as FIFOs or + * device nodes, binaries, or very large files) are skipped here and left to the + * result/context boundaries, so a `read` never blocks on a named pipe. + */ +export async function handleLayer1(event, ctx) { try { - if (input.tool !== "read" && input.tool !== "edit" && input.tool !== "write") { + const tool = event.tool; + if (tool !== "read" && tool !== "edit" && tool !== "write") return; - } - if (isSensitiveDotEnvPath(filePath)) + const input = event.input; + const filePath = input?.filePath; + if (typeof filePath !== "string" || filePath.length === 0) return; let stats; try { @@ -34,29 +46,21 @@ export async function handleLayer1(input, output) { catch { return; } + // Only regular files: never read FIFOs, sockets, or devices (would block). + if (!stats.isFile()) + return; if (stats.size > MAX_FILE_SIZE) return; const raw = readFileSync(filePath); if (isBinary(raw)) return; const content = raw.toString("utf8"); - const { redacted, findings } = scanAndRedact(content, "file", filePath); - if (findings.length > 0) { - const auditEntries = findings.map((f) => ({ - timestamp: new Date().toISOString(), - source: "file", - ruleId: f.ruleId, - description: f.description, - fingerprint: f.redacted.match(/:([a-f0-9]{8})\]/)?.[1] || "", - })); - writeAuditBatch(auditEntries); - if (redacted !== content) { - writeFileSync(filePath, redacted, "utf8"); - } - } + const findings = filterByAllowlist(scan(content, "file", filePath), loadAllowlist()); + recordFindings(findings, "file", { ...ctx, sessionId: event.sessionID }, filePath); + // Intentionally no writeFileSync. The original file is never modified. } catch { - // Degrade gracefully — never block tool execution + // Degrade gracefully — never block tool execution. } } //# sourceMappingURL=layer1.js.map \ No newline at end of file diff --git a/dist/plugin/layer1.js.map b/dist/plugin/layer1.js.map index b19590f..108d923 100755 --- a/dist/plugin/layer1.js.map +++ b/dist/plugin/layer1.js.map @@ -1 +1 @@ -{"version":3,"file":"layer1.js","sourceRoot":"","sources":["../../src/plugin/layer1.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAGrD,MAAM,aAAa,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAEvC,SAAS,QAAQ,CAAC,OAAe;IAC7B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACtD,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;IACtC,CAAC;IACD,OAAO,KAAK,CAAC;AACjB,CAAC;AAED,SAAS,qBAAqB,CAAC,QAAgB;IAC3C,MAAM,UAAU,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IAChD,IAAI,uBAAuB,CAAC,IAAI,CAAC,UAAU,CAAC;QAAE,OAAO,KAAK,CAAC;IAC3D,OAAO,mBAAmB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;AAChD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAC9B,KAA0D,EAC1D,MAAqB;IAErB,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,EAAE,QAA8B,CAAC;IAC7D,IAAI,CAAC,QAAQ;QAAE,OAAO;IAEtB,IAAI,CAAC;QACD,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;YAC3E,OAAO;QACX,CAAC;QAED,IAAI,qBAAqB,CAAC,QAAQ,CAAC;YAAE,OAAO;QAE5C,IAAI,KAAK,CAAC;QACV,IAAI,CAAC;YACD,KAAK,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAC/B,CAAC;QAAC,MAAM,CAAC;YACL,OAAO;QACX,CAAC;QACD,IAAI,KAAK,CAAC,IAAI,GAAG,aAAa;YAAE,OAAO;QAEvC,MAAM,GAAG,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;QACnC,IAAI,QAAQ,CAAC,GAAG,CAAC;YAAE,OAAO;QAE1B,MAAM,OAAO,GAAG,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QACrC,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;QAExE,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACtB,MAAM,YAAY,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAU,EAAE,EAAE,CAAC,CAAC;gBAC/C,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;gBACnC,MAAM,EAAE,MAAM;gBACd,MAAM,EAAE,CAAC,CAAC,MAAM;gBAChB,WAAW,EAAE,CAAC,CAAC,WAAW;gBAC1B,WAAW,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,kBAAkB,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE;aAC/D,CAAC,CAAC,CAAC;YACJ,eAAe,CAAC,YAAY,CAAC,CAAC;YAE9B,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;gBACvB,aAAa,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;YAC9C,CAAC;QACL,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACL,kDAAkD;IACtD,CAAC;AACL,CAAC"} \ No newline at end of file +{"version":3,"file":"layer1.js","sourceRoot":"","sources":["../../src/plugin/layer1.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACjD,OAAO,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC1E,OAAO,EAAE,cAAc,EAAqB,MAAM,aAAa,CAAC;AAEhE;;;GAGG;AACH,MAAM,aAAa,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAEvC,SAAS,QAAQ,CAAC,OAAe;IAC7B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACtD,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;IACtC,CAAC;IACD,OAAO,KAAK,CAAC;AACjB,CAAC;AAQD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,KAAkB,EAAE,GAAiB;IACpE,IAAI,CAAC;QACD,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;QACxB,IAAI,IAAI,KAAK,MAAM,IAAI,IAAI,KAAK,MAAM,IAAI,IAAI,KAAK,OAAO;YAAE,OAAO;QAEnE,MAAM,KAAK,GAAG,KAAK,CAAC,KAA2C,CAAC;QAChE,MAAM,QAAQ,GAAG,KAAK,EAAE,QAAQ,CAAC;QACjC,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAElE,IAAI,KAAK,CAAC;QACV,IAAI,CAAC;YACD,KAAK,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAC/B,CAAC;QAAC,MAAM,CAAC;YACL,OAAO;QACX,CAAC;QACD,2EAA2E;QAC3E,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE;YAAE,OAAO;QAC5B,IAAI,KAAK,CAAC,IAAI,GAAG,aAAa;YAAE,OAAO;QAEvC,MAAM,GAAG,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;QACnC,IAAI,QAAQ,CAAC,GAAG,CAAC;YAAE,OAAO;QAE1B,MAAM,OAAO,GAAG,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QACrC,MAAM,QAAQ,GAAG,iBAAiB,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,aAAa,EAAE,CAAC,CAAC;QACrF,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,GAAG,GAAG,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,EAAE,QAAQ,CAAC,CAAC;QACnF,uEAAuE;IAC3E,CAAC;IAAC,MAAM,CAAC;QACL,mDAAmD;IACvD,CAAC;AACL,CAAC"} \ No newline at end of file diff --git a/dist/plugin/layer2-compaction.d.ts b/dist/plugin/layer2-compaction.d.ts index 15c92f7..d711fa2 100755 --- a/dist/plugin/layer2-compaction.d.ts +++ b/dist/plugin/layer2-compaction.d.ts @@ -1,7 +1,18 @@ -export declare function handleLayer2Compaction(_input: { - sessionID: string; -}, output: { - context: string[]; - prompt?: string; -}): Promise; +import { type AuditContext } from "./redact.js"; +export type SessionCompactionEvent = { + sessionID?: string; + messages?: unknown; + result?: { + summary?: unknown; + } | undefined; +}; +/** + * `ctx.session.hook("compaction")` — redact the transcript that is about to be + * summarized in a checkpoint compaction, and, when the plugin supplies its own + * summary via `event.result`, the summary text as well. + * + * Without this, a secret that survived in earlier history would be copied into + * the compaction summary and re-injected into every later request. + */ +export declare function handleCompaction(event: SessionCompactionEvent, ctx: AuditContext): Promise; //# sourceMappingURL=layer2-compaction.d.ts.map \ No newline at end of file diff --git a/dist/plugin/layer2-compaction.d.ts.map b/dist/plugin/layer2-compaction.d.ts.map index 22532b9..4b44038 100755 --- a/dist/plugin/layer2-compaction.d.ts.map +++ b/dist/plugin/layer2-compaction.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"layer2-compaction.d.ts","sourceRoot":"","sources":["../../src/plugin/layer2-compaction.ts"],"names":[],"mappings":"AAIA,wBAAsB,sBAAsB,CACxC,MAAM,EAAE;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,EAC7B,MAAM,EAAE;IAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/C,OAAO,CAAC,IAAI,CAAC,CAyBf"} \ No newline at end of file +{"version":3,"file":"layer2-compaction.d.ts","sourceRoot":"","sources":["../../src/plugin/layer2-compaction.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAG5D,MAAM,MAAM,sBAAsB,GAAG;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,SAAS,CAAC;CAC9C,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,sBAAsB,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAUtG"} \ No newline at end of file diff --git a/dist/plugin/layer2-compaction.js b/dist/plugin/layer2-compaction.js index bddfd18..9ec3307 100755 --- a/dist/plugin/layer2-compaction.js +++ b/dist/plugin/layer2-compaction.js @@ -1,28 +1,23 @@ -import { scanAndRedact } from "../engine/scanner.js"; -import { writeAuditBatch } from "../engine/audit.js"; -export async function handleLayer2Compaction(_input, output) { +import { redactText } from "./redact.js"; +import { redactMessages } from "./layer2-message.js"; +/** + * `ctx.session.hook("compaction")` — redact the transcript that is about to be + * summarized in a checkpoint compaction, and, when the plugin supplies its own + * summary via `event.result`, the summary text as well. + * + * Without this, a secret that survived in earlier history would be copied into + * the compaction summary and re-injected into every later request. + */ +export async function handleCompaction(event, ctx) { try { - if (!output.context || output.context.length === 0) - return; - const allFindings = []; - for (let i = 0; i < output.context.length; i++) { - const { redacted, findings } = scanAndRedact(output.context[i], "context"); - output.context[i] = redacted; - allFindings.push(...findings); - } - if (allFindings.length > 0) { - const auditEntries = allFindings.map((f) => ({ - timestamp: new Date().toISOString(), - source: "context", - ruleId: f.ruleId, - description: f.description, - fingerprint: f.redacted.match(/:([a-f0-9]{8})\]/)?.[1] || "", - })); - writeAuditBatch(auditEntries); + const audit = { ...ctx, sessionId: event.sessionID }; + redactMessages(event.messages, "context", audit); + if (event.result && typeof event.result.summary === "string") { + event.result.summary = redactText(event.result.summary, "context", audit); } } catch { - // Degrade gracefully — never block compaction + // Never block compaction. } } //# sourceMappingURL=layer2-compaction.js.map \ No newline at end of file diff --git a/dist/plugin/layer2-compaction.js.map b/dist/plugin/layer2-compaction.js.map index 463d9dc..fe1e955 100755 --- a/dist/plugin/layer2-compaction.js.map +++ b/dist/plugin/layer2-compaction.js.map @@ -1 +1 @@ -{"version":3,"file":"layer2-compaction.js","sourceRoot":"","sources":["../../src/plugin/layer2-compaction.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAGrD,MAAM,CAAC,KAAK,UAAU,sBAAsB,CACxC,MAA6B,EAC7B,MAA8C;IAE9C,IAAI,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAE3D,MAAM,WAAW,GAAc,EAAE,CAAC;QAElC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAC7C,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,aAAa,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;YAC3E,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC;YAC7B,WAAW,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,CAAC;QAClC,CAAC;QAED,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACzB,MAAM,YAAY,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBACzC,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;gBACnC,MAAM,EAAE,SAAS;gBACjB,MAAM,EAAE,CAAC,CAAC,MAAM;gBAChB,WAAW,EAAE,CAAC,CAAC,WAAW;gBAC1B,WAAW,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,kBAAkB,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE;aAC/D,CAAC,CAAC,CAAC;YACJ,eAAe,CAAC,YAAY,CAAC,CAAC;QAClC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACL,8CAA8C;IAClD,CAAC;AACL,CAAC"} \ No newline at end of file +{"version":3,"file":"layer2-compaction.js","sourceRoot":"","sources":["../../src/plugin/layer2-compaction.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAqB,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAQrD;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,KAA6B,EAAE,GAAiB;IACnF,IAAI,CAAC;QACD,MAAM,KAAK,GAAiB,EAAE,GAAG,GAAG,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC;QACnE,cAAc,CAAC,KAAK,CAAC,QAAQ,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;QACjD,IAAI,KAAK,CAAC,MAAM,IAAI,OAAO,KAAK,CAAC,MAAM,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;YAC3D,KAAK,CAAC,MAAM,CAAC,OAAO,GAAG,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;QAC9E,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACL,0BAA0B;IAC9B,CAAC;AACL,CAAC"} \ No newline at end of file diff --git a/dist/plugin/layer2-message.d.ts b/dist/plugin/layer2-message.d.ts index 669ed6b..e2cfec8 100755 --- a/dist/plugin/layer2-message.d.ts +++ b/dist/plugin/layer2-message.d.ts @@ -1,5 +1,42 @@ -import type { Event } from "@opencode-ai/sdk"; -export declare function handleLayer2Message(input: { - event: Event; -}): Promise; +import { type AuditContext } from "./redact.js"; +import type { Finding } from "../engine/types.js"; +/** Minimal structural types for the V2 session hook events we consume. */ +export type SessionPromptEvent = { + sessionID?: string; + prompt?: { + text?: unknown; + }; +}; +export type SessionContextEvent = { + sessionID?: string; + system?: unknown; + messages?: unknown; +}; +type Audit = { + auditPath?: string; + sessionId?: string; +}; +/** Redact every text-bearing part of an array of messages, in place. */ +export declare function redactMessages(messages: unknown, source: Finding["source"], ctx: Audit): void; +/** + * `ctx.session.hook("prompt")` — redact the incoming user prompt before it is + * durably admitted and before attachments/skills are resolved. This is the + * earliest supported point that still affects persisted input. + */ +export declare function handlePrompt(event: SessionPromptEvent, ctx: AuditContext): Promise; +/** + * `ctx.session.hook("context")` — redact the assembled system instructions and + * message transcript immediately before the agent model request. This covers + * normal prompts, tool-driven continuations, and everything else in the agent + * loop, including the already-redacted tool results. + */ +export declare function handleContext(event: SessionContextEvent, ctx: AuditContext): Promise; +/** + * Detection-only handler for the V2 event stream. Events are observational + * (they arrive after admission/transmission), so this cannot prevent + * disclosure; it records findings for the audit log. Prevention happens in the + * `prompt`, `context`, `compaction`, and `execute.after` hooks. + */ +export declare function handleMessageEvent(event: unknown, ctx: AuditContext): void; +export {}; //# sourceMappingURL=layer2-message.d.ts.map \ No newline at end of file diff --git a/dist/plugin/layer2-message.d.ts.map b/dist/plugin/layer2-message.d.ts.map index 24f39a2..7971147 100755 --- a/dist/plugin/layer2-message.d.ts.map +++ b/dist/plugin/layer2-message.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"layer2-message.d.ts","sourceRoot":"","sources":["../../src/plugin/layer2-message.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAgB9C,wBAAsB,mBAAmB,CAAC,KAAK,EAAE;IAAE,KAAK,EAAE,KAAK,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAwBhF"} \ No newline at end of file +{"version":3,"file":"layer2-message.d.ts","sourceRoot":"","sources":["../../src/plugin/layer2-message.ts"],"names":[],"mappings":"AACA,OAAO,EAAgD,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAC9F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAElD,0EAA0E;AAE1E,MAAM,MAAM,kBAAkB,GAAG;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF,KAAK,KAAK,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AA0CxD,wEAAwE;AACxE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,KAAK,GAAG,IAAI,CAS7F;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,KAAK,EAAE,kBAAkB,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ9F;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE,mBAAmB,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAchG;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,YAAY,GAAG,IAAI,CAqB1E"} \ No newline at end of file diff --git a/dist/plugin/layer2-message.js b/dist/plugin/layer2-message.js index 822735d..c0bdad9 100755 --- a/dist/plugin/layer2-message.js +++ b/dist/plugin/layer2-message.js @@ -1,42 +1,129 @@ import { scanAndRedact } from "../engine/scanner.js"; -import { writeAuditBatch } from "../engine/audit.js"; -function extractMessageText(event) { - if ("text" in event && typeof event.text === "string") - return event.text; - if ("content" in event && typeof event.content === "string") - return event.content; - if ("message" in event && event.message) { - const msg = event.message; - if (typeof msg.text === "string") - return msg.text; - if (typeof msg.content === "string") - return msg.content; - } - return null; +import { redactText, redactStructured, recordFindings } from "./redact.js"; +function withSession(ctx, sessionID) { + return { auditPath: ctx.auditPath, sessionId: sessionID }; } -export async function handleLayer2Message(input) { +/** Redact the `text` field of a message part in place, based on its type. */ +function redactMessagePart(part, source, ctx) { + if (!part || typeof part !== "object") + return; + const p = part; + switch (p.type) { + case "text": + case "reasoning": + case "compaction": + if (typeof p.text === "string") + p.text = redactText(p.text, source, ctx); + break; + case "tool-call": + if (p.input !== undefined) + p.input = redactStructured(p.input, source, ctx); + break; + case "tool-result": { + const r = p.result; + if (r && typeof r === "object") { + if (r.type === "content" && Array.isArray(r.value)) { + for (const c of r.value) { + if (c && typeof c === "object" && typeof c.text === "string") { + c.text = redactText(c.text, source, ctx); + } + } + } + else if (typeof r.value === "string") { + r.value = redactText(r.value, source, ctx); + } + else if (r.value !== undefined) { + r.value = redactStructured(r.value, source, ctx); + } + } + break; + } + default: + // Unknown part kinds (media, effort, ...) are left untouched. + break; + } +} +/** Redact every text-bearing part of an array of messages, in place. */ +export function redactMessages(messages, source, ctx) { + if (!Array.isArray(messages)) + return; + for (const message of messages) { + if (!message || typeof message !== "object") + continue; + const content = message.content; + if (Array.isArray(content)) { + for (const part of content) + redactMessagePart(part, source, ctx); + } + } +} +/** + * `ctx.session.hook("prompt")` — redact the incoming user prompt before it is + * durably admitted and before attachments/skills are resolved. This is the + * earliest supported point that still affects persisted input. + */ +export async function handlePrompt(event, ctx) { try { - const event = input.event; - const type = event.type; - if (type !== "message.updated" && type !== "message.created") + const prompt = event.prompt; + if (!prompt || typeof prompt.text !== "string") return; - const text = extractMessageText(event); - if (!text) + prompt.text = redactText(prompt.text, "message", withSession(ctx, event.sessionID)); + } + catch { + // Never block prompt admission. + } +} +/** + * `ctx.session.hook("context")` — redact the assembled system instructions and + * message transcript immediately before the agent model request. This covers + * normal prompts, tool-driven continuations, and everything else in the agent + * loop, including the already-redacted tool results. + */ +export async function handleContext(event, ctx) { + try { + const audit = withSession(ctx, event.sessionID); + if (Array.isArray(event.system)) { + for (const part of event.system) { + if (part && typeof part === "object" && typeof part.text === "string") { + part.text = redactText(part.text, "context", audit); + } + } + } + redactMessages(event.messages, "context", audit); + } + catch { + // Never break a model request. + } +} +/** + * Detection-only handler for the V2 event stream. Events are observational + * (they arrive after admission/transmission), so this cannot prevent + * disclosure; it records findings for the audit log. Prevention happens in the + * `prompt`, `context`, `compaction`, and `execute.after` hooks. + */ +export function handleMessageEvent(event, ctx) { + try { + const e = event; + const type = e && typeof e.type === "string" ? e.type : ""; + if (!type.startsWith("message")) return; - const { findings } = scanAndRedact(text, "message"); - if (findings.length > 0) { - const auditEntries = findings.map((f) => ({ - timestamp: new Date().toISOString(), - source: "message", - ruleId: f.ruleId, - description: f.description, - fingerprint: f.redacted.match(/:([a-f0-9]{8})\]/)?.[1] || "", - })); - writeAuditBatch(auditEntries); + const texts = []; + const push = (v) => { + if (typeof v === "string" && v.length > 0 && v.length <= 200_000) + texts.push(v); + }; + if (e?.part && typeof e.part === "object") + push(e.part.text); + if (e?.properties && typeof e.properties === "object") + push(e.properties.text); + push(e?.text); + for (const text of texts) { + const { findings } = scanAndRedact(text, "message"); + recordFindings(findings, "message", ctx); } } catch { - // Degrade gracefully — never block message sending + // Never break the event loop. } } //# sourceMappingURL=layer2-message.js.map \ No newline at end of file diff --git a/dist/plugin/layer2-message.js.map b/dist/plugin/layer2-message.js.map index 64e7ca1..3be2a02 100755 --- a/dist/plugin/layer2-message.js.map +++ b/dist/plugin/layer2-message.js.map @@ -1 +1 @@ -{"version":3,"file":"layer2-message.js","sourceRoot":"","sources":["../../src/plugin/layer2-message.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAGrD,SAAS,kBAAkB,CAAC,KAAY;IACpC,IAAI,MAAM,IAAI,KAAK,IAAI,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC,IAAI,CAAC;IACzE,IAAI,SAAS,IAAI,KAAK,IAAI,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC,OAAO,CAAC;IAClF,IAAI,SAAS,IAAI,KAAK,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;QACtC,MAAM,GAAG,GAAG,KAAK,CAAC,OAAkC,CAAC;QACrD,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,GAAG,CAAC,IAAI,CAAC;QAClD,IAAI,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ;YAAE,OAAO,GAAG,CAAC,OAAO,CAAC;IAC5D,CAAC;IACD,OAAO,IAAI,CAAC;AAChB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,KAAuB;IAC7D,IAAI,CAAC;QACD,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;QAC1B,MAAM,IAAI,GAAI,KAAiC,CAAC,IAA0B,CAAC;QAC3E,IAAI,IAAI,KAAK,iBAAiB,IAAI,IAAI,KAAK,iBAAiB;YAAE,OAAO;QAErE,MAAM,IAAI,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC;QACvC,IAAI,CAAC,IAAI;YAAE,OAAO;QAElB,MAAM,EAAE,QAAQ,EAAE,GAAG,aAAa,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;QAEpD,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACtB,MAAM,YAAY,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAU,EAAE,EAAE,CAAC,CAAC;gBAC/C,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;gBACnC,MAAM,EAAE,SAAS;gBACjB,MAAM,EAAE,CAAC,CAAC,MAAM;gBAChB,WAAW,EAAE,CAAC,CAAC,WAAW;gBAC1B,WAAW,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,kBAAkB,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE;aAC/D,CAAC,CAAC,CAAC;YACJ,eAAe,CAAC,YAAY,CAAC,CAAC;QAClC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACL,mDAAmD;IACvD,CAAC;AACL,CAAC"} \ No newline at end of file +{"version":3,"file":"layer2-message.js","sourceRoot":"","sources":["../../src/plugin/layer2-message.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/plugin/layer3.d.ts b/dist/plugin/layer3.d.ts index 4f362fd..baff8b4 100755 --- a/dist/plugin/layer3.d.ts +++ b/dist/plugin/layer3.d.ts @@ -1,11 +1,33 @@ -export declare function handleLayer3(_input: { - tool: string; - sessionID: string; - callID: string; - args: any; -}, output: { - title: string; - output: string; - metadata: any; -}): Promise; +import { type AuditContext } from "./redact.js"; +export type ToolResultLike = { + content?: unknown; + output?: unknown; + metadata?: unknown; +}; +export type Layer3Event = { + tool?: string; + sessionID?: string; + status?: "completed" | "error" | string; + result?: ToolResultLike; + error?: { + message?: string; + error?: unknown; + }; +}; +/** + * Post-execution redaction of tool results (`ctx.tool.hook("execute.after")`). + * + * This is the boundary that keeps secrets out of the model context when a tool + * (read, bash, grep, fetch, ...) returns them. It redacts: + * + * - `result.content` when it is a string, or the `text` of text content parts; + * - `result.output` (structured output) by redacting string leaves only, so + * the shape and types are preserved; + * - `result.metadata` likewise. + * + * Scanning is chunk-safe and bounded (see `scanAndRedactBounded`): large + * outputs are fully covered, and content that cannot be inspected fails closed + * with an explicit marker instead of leaking. + */ +export declare function handleLayer3(event: Layer3Event, ctx: AuditContext): Promise; //# sourceMappingURL=layer3.d.ts.map \ No newline at end of file diff --git a/dist/plugin/layer3.d.ts.map b/dist/plugin/layer3.d.ts.map index 527253f..28b80fe 100755 --- a/dist/plugin/layer3.d.ts.map +++ b/dist/plugin/layer3.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"layer3.d.ts","sourceRoot":"","sources":["../../src/plugin/layer3.ts"],"names":[],"mappings":"AAMA,wBAAsB,YAAY,CAC9B,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,GAAG,CAAA;CAAE,EACtE,MAAM,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,GAAG,CAAA;CAAE,GACzD,OAAO,CAAC,IAAI,CAAC,CAuBf"} \ No newline at end of file +{"version":3,"file":"layer3.d.ts","sourceRoot":"","sources":["../../src/plugin/layer3.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgC,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAE9E,MAAM,MAAM,cAAc,GAAG;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,GAAG,MAAM,CAAC;IACxC,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,KAAK,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CACjD,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,YAAY,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAoCvF"} \ No newline at end of file diff --git a/dist/plugin/layer3.js b/dist/plugin/layer3.js index ac54304..8666e9d 100755 --- a/dist/plugin/layer3.js +++ b/dist/plugin/layer3.js @@ -1,27 +1,55 @@ -import { scanAndRedact } from "../engine/scanner.js"; -import { writeAuditBatch } from "../engine/audit.js"; -const MAX_OUTPUT_SIZE = 1024 * 1024; -export async function handleLayer3(_input, output) { +import { redactText, redactStructured } from "./redact.js"; +/** + * Post-execution redaction of tool results (`ctx.tool.hook("execute.after")`). + * + * This is the boundary that keeps secrets out of the model context when a tool + * (read, bash, grep, fetch, ...) returns them. It redacts: + * + * - `result.content` when it is a string, or the `text` of text content parts; + * - `result.output` (structured output) by redacting string leaves only, so + * the shape and types are preserved; + * - `result.metadata` likewise. + * + * Scanning is chunk-safe and bounded (see `scanAndRedactBounded`): large + * outputs are fully covered, and content that cannot be inspected fails closed + * with an explicit marker instead of leaking. + */ +export async function handleLayer3(event, ctx) { try { - const text = output.output; - if (!text) + const audit = { ...ctx, sessionId: event.sessionID }; + if (event.status === "error") { + const message = event.error?.message; + if (typeof message === "string") + event.error.message = redactText(message, "output", audit); + if (event.error && "error" in event.error) { + event.error.error = redactStructured(event.error.error, "output", audit); + } return; - const scanText = text.length > MAX_OUTPUT_SIZE ? text.slice(0, MAX_OUTPUT_SIZE) : text; - const { redacted, findings } = scanAndRedact(scanText, "output"); - if (findings.length > 0) { - const auditEntries = findings.map((f) => ({ - timestamp: new Date().toISOString(), - source: "output", - ruleId: f.ruleId, - description: f.description, - fingerprint: f.redacted.match(/:([a-f0-9]{8})\]/)?.[1] || "", - })); - writeAuditBatch(auditEntries); - output.output = redacted; + } + const result = event.result; + if (!result || typeof result !== "object") + return; + if (typeof result.content === "string") { + result.content = redactText(result.content, "output", audit); + } + else if (Array.isArray(result.content)) { + for (const part of result.content) { + if (part && typeof part === "object") { + const p = part; + if (typeof p.text === "string") + p.text = redactText(p.text, "output", audit); + } + } + } + if (result.output !== undefined) { + result.output = redactStructured(result.output, "output", audit); + } + if (result.metadata !== undefined) { + result.metadata = redactStructured(result.metadata, "output", audit); } } catch { - // Degrade gracefully — never block tool execution + // Degrade gracefully — never break a tool call. } } //# sourceMappingURL=layer3.js.map \ No newline at end of file diff --git a/dist/plugin/layer3.js.map b/dist/plugin/layer3.js.map index 125e54b..9dec162 100755 --- a/dist/plugin/layer3.js.map +++ b/dist/plugin/layer3.js.map @@ -1 +1 @@ -{"version":3,"file":"layer3.js","sourceRoot":"","sources":["../../src/plugin/layer3.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAGrD,MAAM,eAAe,GAAG,IAAI,GAAG,IAAI,CAAC;AAEpC,MAAM,CAAC,KAAK,UAAU,YAAY,CAC9B,MAAsE,EACtE,MAAwD;IAExD,IAAI,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC;QAC3B,IAAI,CAAC,IAAI;YAAE,OAAO;QAElB,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,GAAG,eAAe,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACvF,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,aAAa,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAEjE,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACtB,MAAM,YAAY,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAU,EAAE,EAAE,CAAC,CAAC;gBAC/C,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;gBACnC,MAAM,EAAE,QAAQ;gBAChB,MAAM,EAAE,CAAC,CAAC,MAAM;gBAChB,WAAW,EAAE,CAAC,CAAC,WAAW;gBAC1B,WAAW,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,kBAAkB,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE;aAC/D,CAAC,CAAC,CAAC;YACJ,eAAe,CAAC,YAAY,CAAC,CAAC;YAE9B,MAAM,CAAC,MAAM,GAAG,QAAQ,CAAC;QAC7B,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACL,kDAAkD;IACtD,CAAC;AACL,CAAC"} \ No newline at end of file +{"version":3,"file":"layer3.js","sourceRoot":"","sources":["../../src/plugin/layer3.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAqB,MAAM,aAAa,CAAC;AAgB9E;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,KAAkB,EAAE,GAAiB;IACpE,IAAI,CAAC;QACD,MAAM,KAAK,GAAiB,EAAE,GAAG,GAAG,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC;QAEnE,IAAI,KAAK,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC;YAC3B,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,EAAE,OAAO,CAAC;YACrC,IAAI,OAAO,OAAO,KAAK,QAAQ;gBAAE,KAAK,CAAC,KAAM,CAAC,OAAO,GAAG,UAAU,CAAC,OAAO,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;YAC7F,IAAI,KAAK,CAAC,KAAK,IAAI,OAAO,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;gBACxC,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,gBAAgB,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;YAC7E,CAAC;YACD,OAAO;QACX,CAAC;QAED,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;QAC5B,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ;YAAE,OAAO;QAElD,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;YACrC,MAAM,CAAC,OAAO,GAAG,UAAU,CAAC,MAAM,CAAC,OAAO,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;QACjE,CAAC;aAAM,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;YACvC,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;gBAChC,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;oBACnC,MAAM,CAAC,GAAG,IAA0C,CAAC;oBACrD,IAAI,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ;wBAAE,CAAC,CAAC,IAAI,GAAG,UAAU,CAAC,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;gBACjF,CAAC;YACL,CAAC;QACL,CAAC;QAED,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;YAC9B,MAAM,CAAC,MAAM,GAAG,gBAAgB,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;QACrE,CAAC;QACD,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;YAChC,MAAM,CAAC,QAAQ,GAAG,gBAAgB,CAAC,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;QACzE,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACL,gDAAgD;IACpD,CAAC;AACL,CAAC"} \ No newline at end of file diff --git a/dist/plugin/redact.d.ts b/dist/plugin/redact.d.ts new file mode 100644 index 0000000..2f8da31 --- /dev/null +++ b/dist/plugin/redact.d.ts @@ -0,0 +1,33 @@ +import type { Finding } from "../engine/types.js"; +/** + * Everything the plugin layers need to write safe audit records without + * relying on shared, mutable, process-global state. + */ +export type AuditContext = { + /** Explicit audit path for this plugin instance (project-scoped). */ + auditPath?: string; + /** Session ID when the surrounding hook supplies one. */ + sessionId?: string; +}; +/** + * Record findings as secret-free audit entries. + * + * Requires an explicit, project-scoped `auditPath`. Hooks must never fall back + * to a shared, mutable, process-global path, because one OpenCode server + * process can host plugins for several projects at once. + */ +export declare function recordFindings(findings: Finding[], source: Finding["source"], ctx: AuditContext, filePath?: string): void; +/** + * Redact a single string. Uses bounded scanning so arbitrarily large text is + * fully inspected (or fails closed) rather than silently truncated. + */ +export declare function redactText(text: string, source: Finding["source"], ctx: AuditContext, filePath?: string): string; +/** + * Redact string leaves of a structured value (tool output, metadata, message + * part inputs/results) while preserving its shape and types. Exotic objects are + * left untouched so we never corrupt non-plain data. + */ +export declare function redactStructured(value: unknown, source: Finding["source"], ctx: AuditContext, depth?: number): unknown; +/** Redact in place, replacing only string leaves of a plain object/array. */ +export declare function redactStructuredInPlace(value: unknown, source: Finding["source"], ctx: AuditContext, depth?: number): void; +//# sourceMappingURL=redact.d.ts.map \ No newline at end of file diff --git a/dist/plugin/redact.d.ts.map b/dist/plugin/redact.d.ts.map new file mode 100644 index 0000000..4ffbbad --- /dev/null +++ b/dist/plugin/redact.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"redact.d.ts","sourceRoot":"","sources":["../../src/plugin/redact.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAIlD;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG;IACvB,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yDAAyD;IACzD,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC1B,QAAQ,EAAE,OAAO,EAAE,EACnB,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,EACzB,GAAG,EAAE,YAAY,EACjB,QAAQ,CAAC,EAAE,MAAM,GAClB,IAAI,CAMN;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACtB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,EACzB,GAAG,EAAE,YAAY,EACjB,QAAQ,CAAC,EAAE,MAAM,GAClB,MAAM,CASR;AAKD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC5B,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,EACzB,GAAG,EAAE,YAAY,EACjB,KAAK,SAAI,GACV,OAAO,CAkBT;AAED,6EAA6E;AAC7E,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,YAAY,EAAE,KAAK,SAAI,GAAG,IAAI,CAgBrH"} \ No newline at end of file diff --git a/dist/plugin/redact.js b/dist/plugin/redact.js new file mode 100644 index 0000000..384bbef --- /dev/null +++ b/dist/plugin/redact.js @@ -0,0 +1,81 @@ +import { scanAndRedactBounded } from "../engine/scanner.js"; +import { writeAuditBatch, auditEntryFromFinding } from "../engine/audit.js"; +/** + * Record findings as secret-free audit entries. + * + * Requires an explicit, project-scoped `auditPath`. Hooks must never fall back + * to a shared, mutable, process-global path, because one OpenCode server + * process can host plugins for several projects at once. + */ +export function recordFindings(findings, source, ctx, filePath) { + if (findings.length === 0 || !ctx.auditPath) + return; + writeAuditBatch(findings.map((f) => auditEntryFromFinding(f, { sessionId: ctx.sessionId, source, filePath })), ctx.auditPath); +} +/** + * Redact a single string. Uses bounded scanning so arbitrarily large text is + * fully inspected (or fails closed) rather than silently truncated. + */ +export function redactText(text, source, ctx, filePath) { + if (!text) + return text; + const { redacted, findings, complete, policy } = scanAndRedactBounded(text, source, filePath); + recordFindings(findings, source, ctx, filePath); + if (!complete) { + // Fail closed: mark the uninspectable content explicitly for the reader. + return `${redacted}\n[strangerdanger: content not fully inspected (${policy})]`; + } + return redacted; +} +/** Maximum nesting depth traversed when redacting structured values. */ +const MAX_STRUCTURED_DEPTH = 8; +/** + * Redact string leaves of a structured value (tool output, metadata, message + * part inputs/results) while preserving its shape and types. Exotic objects are + * left untouched so we never corrupt non-plain data. + */ +export function redactStructured(value, source, ctx, depth = 0) { + if (typeof value === "string") + return redactText(value, source, ctx); + if (value === null || typeof value !== "object" || depth >= MAX_STRUCTURED_DEPTH) + return value; + if (Array.isArray(value)) { + const out = new Array(value.length); + for (let i = 0; i < value.length; i++) + out[i] = redactStructured(value[i], source, ctx, depth + 1); + return out; + } + const proto = Object.getPrototypeOf(value); + if (proto !== Object.prototype && proto !== null) + return value; + const out = {}; + for (const [k, v] of Object.entries(value)) { + out[k] = redactStructured(v, source, ctx, depth + 1); + } + return out; +} +/** Redact in place, replacing only string leaves of a plain object/array. */ +export function redactStructuredInPlace(value, source, ctx, depth = 0) { + if (value === null || typeof value !== "object" || depth >= MAX_STRUCTURED_DEPTH) + return; + if (Array.isArray(value)) { + for (let i = 0; i < value.length; i++) { + const v = value[i]; + if (typeof v === "string") + value[i] = redactText(v, source, ctx); + else + redactStructuredInPlace(v, source, ctx, depth + 1); + } + return; + } + const proto = Object.getPrototypeOf(value); + if (proto !== Object.prototype && proto !== null) + return; + for (const [k, v] of Object.entries(value)) { + if (typeof v === "string") + value[k] = redactText(v, source, ctx); + else + redactStructuredInPlace(v, source, ctx, depth + 1); + } +} +//# sourceMappingURL=redact.js.map \ No newline at end of file diff --git a/dist/plugin/redact.js.map b/dist/plugin/redact.js.map new file mode 100644 index 0000000..a2b5484 --- /dev/null +++ b/dist/plugin/redact.js.map @@ -0,0 +1 @@ +{"version":3,"file":"redact.js","sourceRoot":"","sources":["../../src/plugin/redact.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/plugin/tools.d.ts b/dist/plugin/tools.d.ts new file mode 100644 index 0000000..54d6f38 --- /dev/null +++ b/dist/plugin/tools.d.ts @@ -0,0 +1,27 @@ +import type { Finding } from "../engine/types.js"; +/** Rule count across every rule family. */ +export declare function ruleCount(): number; +export type ToolDeps = { + /** Project-scoped audit path for this plugin instance. */ + auditPath?: string; +}; +/** `sd_status` */ +export declare function sdStatus(deps?: ToolDeps): string; +/** `sd_audit` */ +export declare function sdAudit(deps?: ToolDeps): string; +/** `sd_allowlist_show` */ +export declare function sdAllowlistShow(): string; +/** `sd_allowlist_add` */ +export declare function sdAllowlistAdd(args: { + description?: string; + fingerprint?: string; + ruleId?: string; + path?: string; + regex?: string; +}): string; +/** `sd_scan` — returns secret-free metadata only. */ +export declare function sdScan(args: { + content: string; + source?: Finding["source"]; +}): string; +//# sourceMappingURL=tools.d.ts.map \ No newline at end of file diff --git a/dist/plugin/tools.d.ts.map b/dist/plugin/tools.d.ts.map new file mode 100644 index 0000000..7ee8c4a --- /dev/null +++ b/dist/plugin/tools.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../src/plugin/tools.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAkB,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAElE,2CAA2C;AAC3C,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,MAAM,MAAM,QAAQ,GAAG;IACnB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,kBAAkB;AAClB,wBAAgB,QAAQ,CAAC,IAAI,GAAE,QAAa,GAAG,MAAM,CASpD;AAED,iBAAiB;AACjB,wBAAgB,OAAO,CAAC,IAAI,GAAE,QAAa,GAAG,MAAM,CAqBnD;AAED,0BAA0B;AAC1B,wBAAgB,eAAe,IAAI,MAAM,CAQxC;AAED,yBAAyB;AACzB,wBAAgB,cAAc,CAAC,IAAI,EAAE;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,MAAM,CAwBT;AAED,qDAAqD;AACrD,wBAAgB,MAAM,CAAC,IAAI,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAA;CAAE,GAAG,MAAM,CAUpF"} \ No newline at end of file diff --git a/dist/plugin/tools.js b/dist/plugin/tools.js new file mode 100644 index 0000000..7eb8f87 --- /dev/null +++ b/dist/plugin/tools.js @@ -0,0 +1,100 @@ +import { scanAndRedact, toSafeFinding } from "../engine/scanner.js"; +import { readAuditLog, resolveAuditPath } from "../engine/audit.js"; +import { loadAllowlist, saveAllowlistEntry } from "../engine/allowlist.js"; +import { NAMED_RULES, ENTROPY_RULES, CLEAN_ROOM_RULES } from "../engine/rules.js"; +import { VERSION } from "../shared/version.js"; +/** Rule count across every rule family. */ +export function ruleCount() { + return NAMED_RULES.length + CLEAN_ROOM_RULES.length + ENTROPY_RULES.length; +} +/** `sd_status` */ +export function sdStatus(deps = {}) { + const path = deps.auditPath ?? resolveAuditPath(); + let findingCount = 0; + try { + findingCount = readAuditLog(path).split("\n").filter((l) => l.trim()).length; + } + catch { + // Audit log may not exist yet. + } + return `strangerDanger v${VERSION}\nRules: ${ruleCount()}\nFindings this session: ${findingCount}\nAudit log: ${path}`; +} +/** `sd_audit` */ +export function sdAudit(deps = {}) { + const path = deps.auditPath ?? resolveAuditPath(); + let log; + try { + log = readAuditLog(path); + } + catch { + return "Audit log not found."; + } + if (!log.trim()) + return "No audit entries yet."; + return log + .trim() + .split("\n") + .map((line) => { + try { + const entry = JSON.parse(line); + return `[${entry.timestamp}] ${entry.ruleId} — fingerprint: ${entry.fingerprint} — source: ${entry.source}`; + } + catch { + return line; + } + }) + .join("\n"); +} +/** `sd_allowlist_show` */ +export function sdAllowlistShow() { + const allowlist = loadAllowlist(); + if (allowlist.entries.length === 0) + return "No allowlist entries."; + return allowlist.entries + .map((e, i) => `${i + 1}. ${e.description || "(no description)"} | fingerprint: ${e.fingerprint || "—"} | ruleId: ${e.ruleId || "—"} | path: ${e.path || "—"} | regex: ${e.regex || "—"}`) + .join("\n"); +} +/** `sd_allowlist_add` */ +export function sdAllowlistAdd(args) { + const entry = {}; + if (args.description) + entry.description = args.description; + if (args.fingerprint) + entry.fingerprint = args.fingerprint; + if (args.ruleId) + entry.ruleId = args.ruleId; + if (args.path) + entry.path = args.path; + if (args.regex) + entry.regex = args.regex; + if (!entry.fingerprint && !entry.ruleId && !entry.path && !entry.regex) { + return "No allowlist criteria supplied (need one of fingerprint, ruleId, path, regex)."; + } + if (entry.regex) { + try { + new RegExp(entry.regex); + } + catch (err) { + return `Invalid regex: ${err instanceof Error ? err.message : String(err)}`; + } + } + if (entry.fingerprint && !/^[a-f0-9]{8}$/i.test(entry.fingerprint)) { + return "Fingerprint must be 8 hexadecimal characters."; + } + saveAllowlistEntry(entry); + return "Allowlist entry added."; +} +/** `sd_scan` — returns secret-free metadata only. */ +export function sdScan(args) { + const source = args.source ?? "file"; + const { findings } = scanAndRedact(args.content, source); + if (findings.length === 0) + return "No secrets detected."; + return findings + .map((f) => { + const safe = toSafeFinding(f, args.content); + return `${safe.ruleId}: ${safe.description}\n Location: line ${safe.line ?? "?"}, column ${(safe.column ?? 0) + 1}\n Fingerprint: ${safe.fingerprint}\n Redaction: ${safe.redaction}`; + }) + .join("\n\n"); +} +//# sourceMappingURL=tools.js.map \ No newline at end of file diff --git a/dist/plugin/tools.js.map b/dist/plugin/tools.js.map new file mode 100644 index 0000000..2c87ae2 --- /dev/null +++ b/dist/plugin/tools.js.map @@ -0,0 +1 @@ +{"version":3,"file":"tools.js","sourceRoot":"","sources":["../../src/plugin/tools.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/shared/version.d.ts b/dist/shared/version.d.ts new file mode 100644 index 0000000..9ab3a3f --- /dev/null +++ b/dist/shared/version.d.ts @@ -0,0 +1,6 @@ +/** + * Single source of truth for the runtime version string. Keep this in sync with + * package.json's `version` when releasing. + */ +export declare const VERSION = "0.2.0"; +//# sourceMappingURL=version.d.ts.map \ No newline at end of file diff --git a/dist/shared/version.d.ts.map b/dist/shared/version.d.ts.map new file mode 100644 index 0000000..93a4b28 --- /dev/null +++ b/dist/shared/version.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../../src/shared/version.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,OAAO,UAAU,CAAC"} \ No newline at end of file diff --git a/dist/shared/version.js b/dist/shared/version.js new file mode 100644 index 0000000..60c754f --- /dev/null +++ b/dist/shared/version.js @@ -0,0 +1,6 @@ +/** + * Single source of truth for the runtime version string. Keep this in sync with + * package.json's `version` when releasing. + */ +export const VERSION = "0.2.0"; +//# sourceMappingURL=version.js.map \ No newline at end of file diff --git a/dist/shared/version.js.map b/dist/shared/version.js.map new file mode 100644 index 0000000..dd88bcf --- /dev/null +++ b/dist/shared/version.js.map @@ -0,0 +1 @@ +{"version":3,"file":"version.js","sourceRoot":"","sources":["../../src/shared/version.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC"} \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..3b01ee1 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,27 @@ +import tseslint from "typescript-eslint"; + +/** + * Deliberately small rule set. This package is security-sensitive, so lint + * focuses on constructs that can themselves introduce vulnerabilities or hide + * behavior. Type correctness is enforced separately by `tsc --noEmit`. + */ +export default tseslint.config( + { + ignores: ["dist/**", "dist-test/**", "node_modules/**", "src/engine/rules.ts", "src/cli/commands/rules.ts"], + }, + { + files: ["**/*.ts"], + languageOptions: { + parser: tseslint.parser, + }, + plugins: { + "@typescript-eslint": tseslint.plugin, + }, + rules: { + "no-debugger": "error", + "no-eval": "error", + "no-implied-eval": "error", + "no-new-func": "error", + }, + }, +); diff --git a/package-lock.json b/package-lock.json old mode 100755 new mode 100644 index b182d65..e2872d9 --- a/package-lock.json +++ b/package-lock.json @@ -1,459 +1,521 @@ { "name": "@gitdamnit/stranger-danger", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@gitdamnit/stranger-danger", - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "bin": { "sd": "dist/cli/index.js" }, "devDependencies": { - "@opencode-ai/plugin": "^1.0.0", + "@opencode/plugin": "^2.0.29", "@types/node": "^22.19.19", - "toml": "^3.0.0", + "eslint": "^9.0.0", "tsx": "^4.0.0", - "typescript": "^5.6.0" + "typescript": "^5.6.0", + "typescript-eslint": "^8.0.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "peerDependencies": { - "@opencode-ai/plugin": ">=1.0.0" + "@opencode/plugin": ">=2.0.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", - "cpu": [ - "ppc64" - ], + "node_modules/@ai-sdk/provider": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.8.tgz", + "integrity": "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, "engines": { "node": ">=18" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", - "cpu": [ - "arm" - ], + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=16.0.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", - "cpu": [ - "arm64" - ], + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", - "cpu": [ - "x64" - ], + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=14.0.0" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", - "cpu": [ - "arm64" - ], + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=14.0.0" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", - "cpu": [ - "x64" - ], + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=16.0.0" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", - "cpu": [ - "arm64" - ], + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", - "cpu": [ - "x64" - ], + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", - "cpu": [ - "arm" - ], + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=14.0.0" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", - "cpu": [ - "arm64" - ], + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=14.0.0" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", - "cpu": [ - "ia32" - ], + "node_modules/@aws-sdk/client-cognito-identity": { + "version": "3.1057.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.1057.0.tgz", + "integrity": "sha512-5MliYkp2u0+2arTp5fZIaxl+xmm90LEKv/VeSxhfNQW4t0fvWJrNO429/jchWQenNoDRrOGE59VfbuZUfwFujg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/credential-provider-node": "^3.972.47", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/fetch-http-handler": "^5.4.5", + "@smithy/node-http-handler": "^4.7.5", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", - "cpu": [ - "loong64" - ], + "node_modules/@aws-sdk/core": { + "version": "3.978.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.978.3.tgz", + "integrity": "sha512-nslpzUCTI9b2IQMWSd6mSAXldBexPc8rr0d3dXUutXYFLFaTdsHa2GD7sJdsn3PhyjK0O5K8ypd6zi+RXXPVlg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.6", + "@aws-sdk/xml-builder": "^3.972.41", + "@aws/lambda-invoke-store": "^0.3.0", + "@smithy/core": "^3.35.2", + "@smithy/signature-v4": "^5.7.4", + "@smithy/types": "^4.19.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", - "cpu": [ - "mips64el" - ], + "node_modules/@aws-sdk/credential-provider-cognito-identity": { + "version": "3.972.73", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.972.73.tgz", + "integrity": "sha512-m4NXseF9dURaZISxetzArUAE+GfaFfbMitL0QyV4F2VH93pnhb+LHcNM5yzbvHutFDTT/kV5HfJ+JZWivumFeA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/nested-clients": "^3.997.48", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", - "cpu": [ - "ppc64" - ], + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.74", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.74.tgz", + "integrity": "sha512-rsRJcW2iFUhCgclyD6OSbMEaPl8Eu0qSeRZb2uuCK1ViZ66hl5wOe/p6Vr6MO3b+ydyKgFeqza2p6tZThvILlw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", - "cpu": [ - "riscv64" - ], + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.76", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.76.tgz", + "integrity": "sha512-pnKdh0Br7OJPj+7qKjzgTfDTFW0RhEMKXNDR2jv9CAM4oHuG/4z3hRh8KtutLfpmItuRtDbKBmwWkDqHwwAWXw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/fetch-http-handler": "^5.8.0", + "@smithy/node-http-handler": "^4.12.1", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", - "cpu": [ - "s390x" - ], + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.973.19", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.19.tgz", + "integrity": "sha512-kcjvAxqQEV8yuwkX58RelNn3ne65o7JgRWf3Z/gVvbi7Xuw1qSDLJlatrLD76+40cmRxmNkLxBoyp/vAQChQ5g==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/credential-provider-env": "^3.972.74", + "@aws-sdk/credential-provider-http": "^3.972.76", + "@aws-sdk/credential-provider-login": "^3.972.81", + "@aws-sdk/credential-provider-process": "^3.972.74", + "@aws-sdk/credential-provider-sso": "^3.973.18", + "@aws-sdk/credential-provider-web-identity": "^3.972.80", + "@aws-sdk/nested-clients": "^3.997.48", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/credential-provider-imds": "^4.5.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", - "cpu": [ - "x64" - ], + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.81", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.81.tgz", + "integrity": "sha512-CeQHSbE2CnVdaHIJ/NayRaCecIER+Pm9faUPkmRATO54rCoMTEPSbVieGHzEwvm1dZM2HIs6qQZCznhayNTWJQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/nested-clients": "^3.997.48", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", - "cpu": [ - "arm64" - ], + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.86", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.86.tgz", + "integrity": "sha512-WNttSyr6e8innPtBSty//CdnqJcgZN6eAbtw3v/CR2IObz6MYZEo8eusdtuCHJ2ujwFqC2bSPHVyhXGHx8HruQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.74", + "@aws-sdk/credential-provider-http": "^3.972.76", + "@aws-sdk/credential-provider-ini": "^3.973.19", + "@aws-sdk/credential-provider-process": "^3.972.74", + "@aws-sdk/credential-provider-sso": "^3.973.18", + "@aws-sdk/credential-provider-web-identity": "^3.972.80", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/credential-provider-imds": "^4.5.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", - "cpu": [ - "x64" - ], + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.74", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.74.tgz", + "integrity": "sha512-Yj6kZFJQdbR+PygTkqdVLPHHpdDpOS9GCYVcDCuweCRRig9LF24ELQBMfyUw1vspvrZJUfnOGVyMjhaL094ahw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", - "cpu": [ - "arm64" - ], + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.973.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.18.tgz", + "integrity": "sha512-FnpPhdY4qUGtX/8X6eKxh+uD+9Y19i9FYibgk4SBe0M935s9vocHkHzaxaMNk+YkHmvzgdu8cHA5SCoeU+kkew==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/nested-clients": "^3.997.48", + "@aws-sdk/token-providers": "3.1150.0", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", - "cpu": [ - "x64" - ], + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.80", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.80.tgz", + "integrity": "sha512-TJF25Ndwy2HxcVj6oxMgp8qVlJAawlExHYBXqhuC6Zt2m+A7WpdkjBD6QglNbiArlteYljZHEY3R7KKmno6ISA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/nested-clients": "^3.997.48", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", - "cpu": [ - "arm64" - ], + "node_modules/@aws-sdk/credential-providers": { + "version": "3.1057.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.1057.0.tgz", + "integrity": "sha512-rbrEHtz11g0kxsSkYr3fx2HABNNblp4AhB2MgPvJHgYOWfJ2eBviU7Mvoaef0PW8QH6lbZDfJcnM7eKvtvz3sw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-cognito-identity": "3.1057.0", + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/credential-provider-cognito-identity": "^3.972.38", + "@aws-sdk/credential-provider-env": "^3.972.41", + "@aws-sdk/credential-provider-http": "^3.972.43", + "@aws-sdk/credential-provider-ini": "^3.972.46", + "@aws-sdk/credential-provider-login": "^3.972.45", + "@aws-sdk/credential-provider-node": "^3.972.47", + "@aws-sdk/credential-provider-process": "^3.972.41", + "@aws-sdk/credential-provider-sso": "^3.972.45", + "@aws-sdk/credential-provider-web-identity": "^3.972.45", + "@aws-sdk/nested-clients": "^3.997.13", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/credential-provider-imds": "^4.3.6", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", - "cpu": [ - "x64" - ], + "node_modules/@aws-sdk/nested-clients": { + "version": "3.997.48", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.48.tgz", + "integrity": "sha512-B01jdXJtAW/XOiUzGdI2mz67WyEHbGEc9X6Euzw3bjLAa+txTqdZDER4X2FsIEcXzV2ABqudVZ5thali8tCCzQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/signature-v4-multi-region": "^3.996.48", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/fetch-http-handler": "^5.8.0", + "@smithy/node-http-handler": "^4.12.1", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", - "cpu": [ - "arm64" - ], + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.48", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.48.tgz", + "integrity": "sha512-cFDXfEbXaGwbnpvR/DyqfxfjxpjXVRVthQLgkwvcfDS5u3+8G9WSj1X1Ag/pTRg/QOeOno1Tq3ycEdrKsCUcOA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.6", + "@smithy/signature-v4": "^5.7.4", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", - "cpu": [ - "ia32" - ], + "node_modules/@aws-sdk/token-providers": { + "version": "3.1150.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1150.0.tgz", + "integrity": "sha512-wDU36ofhabZYu+d2KSriyDbwzxNoaOp+4dG0p3qkabk7HeALR5nIWyQbjoyJZLn3iqWkv/xNdyUfbfevDVDzzg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/nested-clients": "^3.997.48", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.974.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.6.tgz", + "integrity": "sha512-v/clNZzZnDxGyvpHMOGpJKVXFAExJzUNAAjaWGdcx8QAcXLGwTaOkw33p5SHAi0YAioK32xB3hWwOekRVfmfKg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.10.tgz", + "integrity": "sha512-ycwH6Zd2GhuSqdXX9ihbCjeGTB6xOJs+O3+Jb8/zDG9978XU80qs75dfkPJRMNKe5MvBZPuNeFpd4JZKPoUF4g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.41.tgz", + "integrity": "sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz", + "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@esbuild/win32-x64": { + "node_modules/@esbuild/linux-x64": { "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", "cpu": [ "x64" ], @@ -461,448 +523,4503 @@ "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ], "engines": { "node": ">=18" } }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", - "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", - "cpu": [ - "arm64" - ], + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", - "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", - "cpu": [ - "x64" - ], + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", - "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", - "cpu": [ - "arm" - ], + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", - "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", - "cpu": [ - "arm64" - ], + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", - "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", - "cpu": [ - "x64" - ], + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "license": "MIT" }, - "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", - "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", - "cpu": [ - "x64" - ], + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } }, - "node_modules/@opencode-ai/plugin": { - "version": "1.14.41", - "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.14.41.tgz", - "integrity": "sha512-Q/QdDKSfHyYX+Xqd79o4XgyZKqF8h5qgqgfmOQbKVLhbduc9zMYdpV2yvWT6gaJPrpOftpka/kpr56PCqzetYQ==", + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "@opencode-ai/sdk": "1.14.41", - "effect": "4.0.0-beta.59", - "zod": "4.1.8" + "brace-expansion": "^1.1.7" }, - "peerDependencies": { - "@opentui/core": ">=0.2.2", - "@opentui/solid": ">=0.2.2" + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" }, - "peerDependenciesMeta": { - "@opentui/core": { - "optional": true - }, - "@opentui/solid": { - "optional": true - } + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@opencode-ai/sdk": { - "version": "1.14.41", - "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.14.41.tgz", - "integrity": "sha512-RYb2dCUv0TWIvBNnnO6ANbAPYri6rKuWizSoVFw/Pw+SCDj9ASHM5gAZ+jkskp8gYMfLLHe/Fpkun/9mr8m0IQ==", + "node_modules/@eslint/eslintrc": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.7.tgz", + "integrity": "sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==", "dev": true, "license": "MIT", "dependencies": { - "cross-spawn": "7.0.6" + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.2", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, "license": "MIT" }, - "node_modules/@types/node": { - "version": "22.19.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", - "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">= 8" + "node": "*" } }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", - "optional": true, "engines": { - "node": ">=8" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/effect": { - "version": "4.0.0-beta.59", - "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.59.tgz", - "integrity": "sha512-xyUDLeHSe8d6lWGOvR6Fgn2HL6gYeTZ/S4Jzk9uc4ZUxMPPsNZlNXrvk0C7/utQFzeX7uAWcVnG2BjbA0SRoAA==", + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "@standard-schema/spec": "^1.1.0", - "fast-check": "^4.6.0", - "find-my-way-ts": "^0.1.6", - "ini": "^6.0.0", - "kubernetes-types": "^1.30.0", - "msgpackr": "^1.11.9", - "multipasta": "^0.2.7", - "toml": "^4.1.1", - "uuid": "^13.0.0", - "yaml": "^2.8.3" + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/effect/node_modules/toml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/toml/-/toml-4.1.1.tgz", - "integrity": "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==", + "node_modules/@gar/promise-retry": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.3.tgz", + "integrity": "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==", "dev": true, "license": "MIT", "engines": { - "node": ">=20" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" }, "engines": { - "node": ">=18" + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" + "engines": { + "node": ">=18.18.0" } }, - "node_modules/fast-check": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.7.0.tgz", - "integrity": "sha512-NsZRtqvSSoCP0HbNjUD+r1JH8zqZalyp6gLY9e7OYs7NK9b6AHOs2baBFeBG7bVNsuoukh89x2Yg3rPsul8ziQ==", + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@isaacs/string-locale-compare": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@isaacs/string-locale-compare/-/string-locale-compare-1.1.0.tgz", + "integrity": "sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@npmcli/agent": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.2.tgz", + "integrity": "sha512-EUEuWAxnL07Sp5/iC/1X6Xj+XThUvnbei9zfRWZdEXa7lss9RTHMhAHBeg+MZ5To9s/gGaSI+UwZTPdYMvKSeg==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^11.2.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/arborist": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-9.4.0.tgz", + "integrity": "sha512-4Bm8hNixJG/sii1PMnag0V9i/sGOX9VRzFrUiZMSBJpGlLR38f+Btl85d07G9GL56xO0l0OZjvrGNYsDYp0xKA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/fs": "^5.0.0", + "@npmcli/installed-package-contents": "^4.0.0", + "@npmcli/map-workspaces": "^5.0.0", + "@npmcli/metavuln-calculator": "^9.0.2", + "@npmcli/name-from-folder": "^4.0.0", + "@npmcli/node-gyp": "^5.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/query": "^5.0.0", + "@npmcli/redact": "^4.0.0", + "@npmcli/run-script": "^10.0.0", + "bin-links": "^6.0.0", + "cacache": "^20.0.1", + "common-ancestor-path": "^2.0.0", + "hosted-git-info": "^9.0.0", + "json-stringify-nice": "^1.1.4", + "lru-cache": "^11.2.1", + "minimatch": "^10.0.3", + "nopt": "^9.0.0", + "npm-install-checks": "^8.0.0", + "npm-package-arg": "^13.0.0", + "npm-pick-manifest": "^11.0.1", + "npm-registry-fetch": "^19.0.0", + "pacote": "^21.0.2", + "parse-conflict-json": "^5.0.1", + "proc-log": "^6.0.0", + "proggy": "^4.0.0", + "promise-all-reject-late": "^1.0.0", + "promise-call-limit": "^3.0.1", + "semver": "^7.3.7", + "ssri": "^13.0.0", + "treeverse": "^3.0.0", + "walk-up-path": "^4.0.0" + }, + "bin": { + "arborist": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/config": { + "version": "10.8.1", + "resolved": "https://registry.npmjs.org/@npmcli/config/-/config-10.8.1.tgz", + "integrity": "sha512-MAYk9IlIGiyC0c9fnjdBSQfIFPZT0g1MfeSiD1UXTq2zJOLX55jS9/sETJHqw/7LN18JjITrhYfgCfapbmZHiQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/map-workspaces": "^5.0.0", + "@npmcli/package-json": "^7.0.0", + "ci-info": "^4.0.0", + "ini": "^6.0.0", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "walk-up-path": "^4.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/fs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz", + "integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/git": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.2.tgz", + "integrity": "sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "ini": "^6.0.0", + "lru-cache": "^11.2.1", + "npm-pick-manifest": "^11.0.1", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "which": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/git/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-4.0.0.tgz", + "integrity": "sha512-yNyAdkBxB72gtZ4GrwXCM0ZUedo9nIbOMKfGjt6Cu6DXf0p8y1PViZAKDC8q8kv/fufx0WTjRBdSlyrvnP7hmA==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^5.0.0", + "npm-normalize-package-bin": "^5.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/map-workspaces": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@npmcli/map-workspaces/-/map-workspaces-5.0.3.tgz", + "integrity": "sha512-o2grssXo1e774E5OtEwwrgoszYRh0lqkJH+Pb9r78UcqdGJRDRfhpM8DvZPjzNLLNYeD/rNbjOKM3Ss5UABROw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/name-from-folder": "^4.0.0", + "@npmcli/package-json": "^7.0.0", + "glob": "^13.0.0", + "minimatch": "^10.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/metavuln-calculator": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@npmcli/metavuln-calculator/-/metavuln-calculator-9.0.3.tgz", + "integrity": "sha512-94GLSYhLXF2t2LAC7pDwLaM4uCARzxShyAQKsirmlNcpidH89VA4/+K1LbJmRMgz5gy65E/QBBWQdUvGLe2Frg==", + "dev": true, + "license": "ISC", + "dependencies": { + "cacache": "^20.0.0", + "json-parse-even-better-errors": "^5.0.0", + "pacote": "^21.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/name-from-folder": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/name-from-folder/-/name-from-folder-4.0.0.tgz", + "integrity": "sha512-qfrhVlOSqmKM8i6rkNdZzABj8MKEITGFAY+4teqBziksCQAOLutiAxM1wY2BKEd8KjUSpWmWCYxvXr0y4VTlPg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-5.0.0.tgz", + "integrity": "sha512-uuG5HZFXLfyFKqg8QypsmgLQW7smiRjVc45bqD/ofZZcR/uxEjgQU8qDPv0s9TEeMUiAAU/GC5bR6++UdTirIQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/package-json": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-7.0.5.tgz", + "integrity": "sha512-iVuTlG3ORq2iaVa1IWUxAO/jIp77tUKBhoMjuzYW2kL4MLN1bi/ofqkZ7D7OOwh8coAx1/S2ge0rMdGv8sLSOQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^7.0.0", + "glob": "^13.0.0", + "hosted-git-info": "^9.0.0", + "json-parse-even-better-errors": "^5.0.0", + "proc-log": "^6.0.0", + "semver": "^7.5.3", + "spdx-expression-parse": "^4.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-9.0.1.tgz", + "integrity": "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "which": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/query": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/query/-/query-5.0.0.tgz", + "integrity": "sha512-8TZWfTQOsODpLqo9SVhVjHovmKXNpevHU0gO9e+y4V4fRIOneiXy0u0sMP9LmS71XivrEWfZWg50ReH4WRT4aQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/redact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-4.0.0.tgz", + "integrity": "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.4.tgz", + "integrity": "sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^5.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "node-gyp": "^12.1.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@opencode/ai": { + "version": "2.0.29", + "resolved": "https://registry.npmjs.org/@opencode/ai/-/ai-2.0.29.tgz", + "integrity": "sha512-Hu5U8FadWuI+do1ToBj9bhEL2wfP01JUaOWq8S6JaeHRaXcjvdsuCCFlGUtlA9PMn084gP4ewdInYPIv0B/98w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@aws-sdk/credential-providers": "3.1057.0", + "@opencode/schema": "2.0.29", + "@smithy/eventstream-codec": "4.2.14", + "@smithy/util-utf8": "4.2.2", + "aws4fetch": "1.0.20", + "effect": "4.0.1", + "google-auth-library": "10.5.0" + } + }, + "node_modules/@opencode/ai/node_modules/effect": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.1.tgz", + "integrity": "sha512-b1VlQG9g8fwxE5QnIZuPoOP/0MsqHJSRKxUijjoM80E5q95FXrX5yWtY5XI8G+GJsBQAVSDbLS458z9a++uVvw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@opencode/plugin": { + "version": "2.0.29", + "resolved": "https://registry.npmjs.org/@opencode/plugin/-/plugin-2.0.29.tgz", + "integrity": "sha512-ZK1oUWrHl8u/9pt7lEJVDNKhhXQWMdHvaUuQ/96o9tDbbN+XaYTe4p6gt86F5bm0w9YgspE3TEX9WriI402Pbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ai-sdk/provider": "3.0.8", + "@opencode/ai": "2.0.29", + "@opencode/client": "2.0.29", + "@opencode/protocol": "2.0.29", + "@opencode/schema": "2.0.29", + "@opencode/util": "2.0.29", + "effect": "4.0.1" + }, + "peerDependencies": { + "@opencode/theme": "2.0.29", + "@opentui/core": ">=0.5.17", + "@opentui/solid": ">=0.5.17", + "solid-js": ">=1.9.0" + }, + "peerDependenciesMeta": { + "@opencode/theme": { + "optional": true + }, + "@opentui/core": { + "optional": true + }, + "@opentui/solid": { + "optional": true + }, + "solid-js": { + "optional": true + } + } + }, + "node_modules/@opencode/plugin/node_modules/@opencode/client": { + "version": "2.0.29", + "resolved": "https://registry.npmjs.org/@opencode/client/-/client-2.0.29.tgz", + "integrity": "sha512-fxjzqFAXEGiMDCzCfuKPyZbmrLQAwv0JQIi2rXfrjh3uE3UvItfqPjsiW1/sVPncJvu4UiRXHi+/0ksv3uHDPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@opencode/protocol": "2.0.29", + "@opencode/schema": "2.0.29" + }, + "peerDependencies": { + "effect": "4.0.1", + "solid-js": ">=1.9.0" + }, + "peerDependenciesMeta": { + "effect": { + "optional": true + }, + "solid-js": { + "optional": true + } + } + }, + "node_modules/@opencode/plugin/node_modules/effect": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.1.tgz", + "integrity": "sha512-b1VlQG9g8fwxE5QnIZuPoOP/0MsqHJSRKxUijjoM80E5q95FXrX5yWtY5XI8G+GJsBQAVSDbLS458z9a++uVvw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@opencode/protocol": { + "version": "2.0.29", + "resolved": "https://registry.npmjs.org/@opencode/protocol/-/protocol-2.0.29.tgz", + "integrity": "sha512-Eik6a2u2znmWdyPgpxCnMXJXySslG0Yj/XQzmM/Z/3pAxy0nMJnKLaFznnvXPNoPPII14L+CH7kUhBjLtOPGgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@opencode/schema": "2.0.29", + "effect": "4.0.1" + } + }, + "node_modules/@opencode/protocol/node_modules/effect": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.1.tgz", + "integrity": "sha512-b1VlQG9g8fwxE5QnIZuPoOP/0MsqHJSRKxUijjoM80E5q95FXrX5yWtY5XI8G+GJsBQAVSDbLS458z9a++uVvw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@opencode/schema": { + "version": "2.0.29", + "resolved": "https://registry.npmjs.org/@opencode/schema/-/schema-2.0.29.tgz", + "integrity": "sha512-2qSyUGaeItugP3QujZrBhg2f4a4wdL9ab0m0fObHM+hlZ659YmWGzBRNI9qWR7At8AI9gnDCmgnnrQrx5/aprg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "1.1.0", + "effect": "4.0.1" + } + }, + "node_modules/@opencode/schema/node_modules/effect": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.1.tgz", + "integrity": "sha512-b1VlQG9g8fwxE5QnIZuPoOP/0MsqHJSRKxUijjoM80E5q95FXrX5yWtY5XI8G+GJsBQAVSDbLS458z9a++uVvw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@opencode/util": { + "version": "2.0.29", + "resolved": "https://registry.npmjs.org/@opencode/util/-/util-2.0.29.tgz", + "integrity": "sha512-o0xmrV5MFzB4hhapow4tSbStXAWF27598tDabqq+damJAuvPCBGorDBciosenXpOnNsEHhJm37l5KwOhb1+SNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@effect/opentelemetry": "4.0.1", + "@effect/platform-node": "4.0.1", + "@effect/platform-node-shared": "4.0.1", + "@npmcli/arborist": "9.4.0", + "@npmcli/config": "10.8.1", + "@opentelemetry/api": "1.9.0", + "@opentelemetry/api-logs": "0.219.0", + "@opentelemetry/context-async-hooks": "2.8.0", + "@opentelemetry/exporter-trace-otlp-http": "0.219.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/sdk-logs": "0.219.0", + "@opentelemetry/sdk-metrics": "2.8.0", + "@opentelemetry/sdk-trace-base": "2.8.0", + "@opentelemetry/sdk-trace-node": "2.8.0", + "cross-spawn": "7.0.6", + "effect": "4.0.1", + "glob": "13.0.5", + "mime-types": "3.0.2", + "minimatch": "10.2.5", + "npm-package-arg": "13.0.2", + "open": "11.0.4", + "pacote": "21.5.1" + } + }, + "node_modules/@opencode/util/node_modules/@effect/opentelemetry": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@effect/opentelemetry/-/opentelemetry-4.0.1.tgz", + "integrity": "sha512-MZ7M1a1dxMqTqeNwIGSECjO7RrReEgeEwHo8BnbOqP+8IfVpdiJi/iPCmkFVWlAxanMl4qhsq69e0Hkul9t+bA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <2.0.0", + "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", + "@opentelemetry/resources": ">=2.0.0 <3.0.0", + "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", + "@opentelemetry/sdk-metrics": ">=2.0.0 <3.0.0", + "@opentelemetry/sdk-trace-base": ">=2.0.0 <3.0.0", + "@opentelemetry/sdk-trace-node": ">=2.0.0 <3.0.0", + "@opentelemetry/sdk-trace-web": ">=2.0.0 <3.0.0", + "@opentelemetry/semantic-conventions": ">=1.33.0 <2.0.0", + "effect": "^4.0.1" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@opentelemetry/api-logs": { + "optional": true + }, + "@opentelemetry/resources": { + "optional": true + }, + "@opentelemetry/sdk-logs": { + "optional": true + }, + "@opentelemetry/sdk-metrics": { + "optional": true + }, + "@opentelemetry/sdk-trace-base": { + "optional": true + }, + "@opentelemetry/sdk-trace-node": { + "optional": true + }, + "@opentelemetry/sdk-trace-web": { + "optional": true + } + } + }, + "node_modules/@opencode/util/node_modules/@effect/platform-node": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@effect/platform-node/-/platform-node-4.0.1.tgz", + "integrity": "sha512-FT0Fu5LO0/XP9zBpj0HqrtJNLluK4StNKqf6TSulUsjp8FPBsDVXGJiDuyoN5J2yVMT7tgvhxSwaDcObWVRIaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@effect/platform-node-shared": "^4.0.1", + "undici": "^8.11.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "effect": "^4.0.1", + "redis": ">=5.0.0 <7.0.0" + } + }, + "node_modules/@opencode/util/node_modules/@effect/platform-node-shared": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@effect/platform-node-shared/-/platform-node-shared-4.0.1.tgz", + "integrity": "sha512-VanaejudEhKJ4mpmqnEzdncVUhN6sGSO9ZDND54mRxAlZxj+4dYAWjDTYAy9o4tJo9aALeP2U8cVcOFzEdSeLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ws": "^8.18.1", + "ws": "^8.22.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "effect": "^4.0.1" + } + }, + "node_modules/@opencode/util/node_modules/effect": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.1.tgz", + "integrity": "sha512-b1VlQG9g8fwxE5QnIZuPoOP/0MsqHJSRKxUijjoM80E5q95FXrX5yWtY5XI8G+GJsBQAVSDbLS458z9a++uVvw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@opencode/util/node_modules/undici": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.12.1.tgz", + "integrity": "sha512-usz3OVQBeh1LxNn0DzU0voLXMgn5CU/WUdpBsY6Ti5afNao+vTkmn4H/NxoXolc10cOqe2+YIG0Hspq2rsl64w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.219.0.tgz", + "integrity": "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.8.0.tgz", + "integrity": "sha512-/3FIraneMcng67SUJCxvyInk/oxzwsxyadufk0wwfOBLf5wqtAGX4MoQASwSbndBPeARzBryUM9Azr5kHIdWLw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http": { + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.219.0.tgz", + "integrity": "sha512-9t6SvBXXBEjOBcIzgozvBbd3jWrv3Gt3ngGhl1fhdZ/zRc7oZDVOFEqbi2zlBpW9BXhgDMKv422J0DL/3iQWfw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.8.0", + "@opentelemetry/otlp-exporter-base": "0.219.0", + "@opentelemetry/otlp-transformer": "0.219.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/sdk-trace-base": "2.8.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.219.0.tgz", + "integrity": "sha512-zvIxQX/AZUVKDU+hCuYx+7UkiP7GRdnk1ZbFQRYzHvYp47cAWR4j3IhoPhV9KaeXEv2xdGq3IA6PnpzDmLcmSA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.8.0", + "@opentelemetry/otlp-transformer": "0.219.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.219.0.tgz", + "integrity": "sha512-aaYKAyXhw9VchKZVGOopD3Gw/kPsyrX2c6IQ0AW32mTjqmZOh5Y6Gf5OYqTNqVktAeBjmFinhyFaCwW6GYK9YQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.219.0", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/sdk-logs": "0.219.0", + "@opentelemetry/sdk-metrics": "2.8.0", + "@opentelemetry/sdk-trace-base": "2.8.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", + "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.8.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.219.0.tgz", + "integrity": "sha512-s6lTKRakaPClvKoWHRChxnXjDMkM/TQ30ff78jN6EBGf7MI7VzANE5PU3f4z9qDUudWjvZjOLHG0rBnBKYvoXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.219.0", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.8.0.tgz", + "integrity": "sha512-UDBGaj6W0Rgy5rTTaoxs8gVGF/aGkAKyjurJv7se6wjRxJu7FoquTLT/vt54DZfo4crbprYfhX/SOK9+BPw1qg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.8.0.tgz", + "integrity": "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.8.0.tgz", + "integrity": "sha512-nZt9OGufioAc3AfoLTqA9bsAeaMJAictYDdI2VcNQ+PmT+3rfKjAZDZvgPfd8VPX0O5Bw1hdQF6kDK8VSpZiWg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/context-async-hooks": "2.8.0", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/sdk-trace-base": "2.8.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz", + "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/bloom": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-6.3.0.tgz", + "integrity": "sha512-NQ5poYpZr0jv6zazN1cy6JY5kIipKFZ3R9GTL4kOUTX3a9YAfY29U8bsE4lWhVQEM+G4V0BXVC57mubIdDijeA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 20.0.0" + }, + "peerDependencies": { + "@redis/client": "^6.3.0" + } + }, + "node_modules/@redis/client": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-6.3.0.tgz", + "integrity": "sha512-fHrjwGCBvAANGi+TuRuwKi83sIOzgHr8eh+Uf48w3fSX7Hhrz174NrWqfJhAC2vnN0d/tfaZpdr6+o6JC3QHwg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 20.0.0" + }, + "peerDependencies": { + "@node-rs/xxhash": "^1.1.0", + "@opentelemetry/api": ">=1 <2" + }, + "peerDependenciesMeta": { + "@node-rs/xxhash": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + } + } + }, + "node_modules/@redis/json": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-6.3.0.tgz", + "integrity": "sha512-yt6vRfPBXtW/3qm53cF81TSQpLMjWdW4/2E9WQ2GgbvsKkuCKCFQIcMiKlil1ZMnezK+IQuS795xKU5psbaegw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 20.0.0" + }, + "peerDependencies": { + "@redis/client": "^6.3.0" + } + }, + "node_modules/@redis/search": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-6.3.0.tgz", + "integrity": "sha512-RPXAZmKjZZG7Tm7m1w9keFdXzbL91PiSHQBfQRe9xwW40jNsTRbG0FwhIz4QlHPU4gKDAsCy8IQHj4yZl4H9rw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 20.0.0" + }, + "peerDependencies": { + "@redis/client": "^6.3.0" + } + }, + "node_modules/@redis/time-series": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-6.3.0.tgz", + "integrity": "sha512-+fJOB8mN1z5WRakCJNh5lVdz1KqvHGSOistsKeitD24QsCtPwyRQXWC0iq6T/i/ubLE/YcguZqPm8F598MKk4A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 20.0.0" + }, + "peerDependencies": { + "@redis/client": "^6.3.0" + } + }, + "node_modules/@sigstore/bundle": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-4.0.0.tgz", + "integrity": "sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/core": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-3.2.1.tgz", + "integrity": "sha512-qRsxPnCrbC/puegGxKuynfnxgLiHqWStrSjxkoB4YKqq3Z3s4cyZyj42ZdWFAEblNP65C+rBH8EuREHIXoi83g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/protobuf-specs": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.5.2.tgz", + "integrity": "sha512-SQqvFMt4V78fdjcDdYX6HbiVSOR4QK3ZgwCa2KOsopAgPIHy1rU5UDUmzLl02r5oyyaYcYHR1hpwDRk/yUe+Mw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/sign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-4.1.1.tgz", + "integrity": "sha512-Hf4xglukg0XXQ2RiD5vSoLjdPe8OBUPA8XeVjUObheuDcWdYWrnH/BNmxZCzkAy68MzmNCxXLeurJvs6hcP2OQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@gar/promise-retry": "^1.0.2", + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.2.0", + "@sigstore/protobuf-specs": "^0.5.0", + "make-fetch-happen": "^15.0.4", + "proc-log": "^6.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/tuf": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-4.0.2.tgz", + "integrity": "sha512-TCAzTy0xzdP79EnxSjq9KQ3eaR7+FmudLC6eRKknVKZbV7ZNlGLClAAQb/HMNJ5n2OBNk2GT1tEmU0xuPr+SLQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0", + "tuf-js": "^4.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/verify": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-3.1.1.tgz", + "integrity": "sha512-qv7+G3J2cc6wwFj3yKvXOamzqhMwSk1ogPGmhpS8iXllcPrJaIIBA+4HbttlHVu1pqWTdmaCH/WE7UOC51kdoA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.2.1", + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.36.0.tgz", + "integrity": "sha512-3hJiwG1F5t1SoXqp/yOCmYXziyx7ZVqF63S5W6YU201e/qwVaHPex+E/3vvyY9a3ORq/b+BydeNZpP2saQ9X2g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.5.2.tgz", + "integrity": "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.2", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.14.tgz", + "integrity": "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.1", + "@smithy/util-hex-encoding": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.8.0.tgz", + "integrity": "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.18.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.12.1.tgz", + "integrity": "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.18.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.7.4.tgz", + "integrity": "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.19.0.tgz", + "integrity": "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.5.2.tgz", + "integrity": "sha512-nxu3SgmAw9JXT2CtkU0m/XNLWpP9MsaBx1zAGAypCbYj15tIFlmcYwpF+Oh18le83d+IM9PT7ENdXnE4C+d5mA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.5.2.tgz", + "integrity": "sha512-iq+cW3mAb7vfcxEEpYi3zXKpDtbrIFyanWjQl4zBq4seWD4OSxXDWSfespZxenX6aEaighn+NR3u1nU1DSvs3w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-4.1.0.tgz", + "integrity": "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^10.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.71.2.tgz", + "integrity": "sha512-K72/pvT7qpm8q3k8G5u93NCSN1RcLxX5h7Gw0bQpbMXviBHeY6Nx1WEwmk9wP+eKTSQ8ecxvcgt/ZX/rB938xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.71.2", + "@typescript-eslint/type-utils": "8.71.2", + "@typescript-eslint/utils": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.71.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.12.tgz", + "integrity": "sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.71.2.tgz", + "integrity": "sha512-aTtmZjp/x3gsGHkDs04/1I8kNogBJkpQaeGaN0PfiG43NzDEmW2n+OP5im9wfsvyX0yP1UZWBrAGtDBfH9jEVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.71.2", + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.71.2.tgz", + "integrity": "sha512-Pd68GsLVh5ZXlcEccFPdhSOvtHZ/2WU0XsA8zmXOS3F158lVlcTLhfQ6qvtL1RzbMie0Kp8xtbVwAg4nejKPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.71.2", + "@typescript-eslint/types": "^8.71.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.71.2.tgz", + "integrity": "sha512-YF4wgg5iUX5Obywoac27B56FhozMNFhdJSbh11H5GBAwmBtfyRhmgTQqW9tRNNmHDvsZ2CX6UGzoz+YfYn2Leg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.71.2.tgz", + "integrity": "sha512-jz49TAWokm/7yWD9/o837nYax9DyALxIPOImdImvVzo6canBCZYzin8zJyqNR5k1j9K1DE0bVvr0XYB+99uAdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.71.2.tgz", + "integrity": "sha512-cC8UaalCViJkb3rH/xLIvidEwoFRtMZMUqDjjJMGEKaTFwYRHYBcoczlEGHuvkHEO8svRDrgZ0yQgOArFmwang==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2", + "@typescript-eslint/utils": "8.71.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.71.2.tgz", + "integrity": "sha512-y5Ukl3oVp7e/VGxqIoEXoeb/Roj71PAJ4WG/N/RGj+eeDvKRLM15kGr9MijLbN0i03S52zxu31Z98E5gWChJKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.71.2.tgz", + "integrity": "sha512-DgNOEsl4pdYzwSL+N/Cl33tX1Hlw3FvId73LnKXfq7jnrF2viOEn7X+P7RBkkSWCVm92gnWd28GIC5vJuLREeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.71.2", + "@typescript-eslint/tsconfig-utils": "8.71.2", + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.71.2.tgz", + "integrity": "sha512-RyKa1G1M9rKHDhrk+HJtiF1MvLD5Ux2JW3TSkzw+02pRALdpJb+QzG1Y5em+LvCgzGwgg8H7mBRFKljNz7ImPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.71.2", + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.71.2.tgz", + "integrity": "sha512-zozlN0UA32bSG2zkKAgQyRYSbEISVoyU6vvkKYo2aSny74D8Jh4e+POkJ6Lk0dlezZi5x7TtD+88Xb06IB8nhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.2", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aws4fetch": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/aws4fetch/-/aws4fetch-1.0.20.tgz", + "integrity": "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bin-links": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/bin-links/-/bin-links-6.0.2.tgz", + "integrity": "sha512-frE1t78WOwJ45PKV2cF2tNPjTcs9L1J9s6VkrV59wanRP4GlaomuxYPVma7BwthMg8WnfSory4w5PTE6FZZ81w==", + "dev": true, + "license": "ISC", + "dependencies": { + "cmd-shim": "^8.0.0", + "npm-normalize-package-bin": "^5.0.0", + "proc-log": "^6.0.0", + "read-cmd-shim": "^6.0.0", + "write-file-atomic": "^7.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/bundle-name": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.1.tgz", + "integrity": "sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cacache": { + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.4.tgz", + "integrity": "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^5.0.0", + "fs-minipass": "^3.0.0", + "glob": "^13.0.0", + "lru-cache": "^11.1.0", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cmd-shim": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-8.0.0.tgz", + "integrity": "sha512-Jk/BK6NCapZ58BKUxlSI+ouKRbjH1NLZCgJkYoab+vEHUY3f6OzpNBN9u7HFSv9J6TRDGs4PLOHezoKGaFRSCA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/common-ancestor-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-2.0.0.tgz", + "integrity": "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">= 18" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/default-browser": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.1.tgz", + "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/gaxios": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.3.1.tgz", + "integrity": "sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.4.tgz", + "integrity": "sha512-iJ9KMsiu+xKtNRX0PmGLSaIU3bUBAyzWTyqKemKPzNPsmmsBCQYmlNg+brEbES7IHSXtdVwzBPzx1vz3FAaipw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "gaxios": "7.1.3", + "google-logging-utils": "1.1.3", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata/node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata/node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.5.tgz", + "integrity": "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.1", + "minipass": "^7.1.2", + "path-scurry": "^2.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/google-auth-library": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", + "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^8.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.2.0.tgz", + "integrity": "sha512-WE9av4wKDZgRjBwgVUabocx8T6/7o3Ca1Fat46FXDhXVAFibzNadedcOXrdgd1Kzmk8tsk/9ZH89Wyf/SqeZ3A==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.3.0.tgz", + "integrity": "sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-walk": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-8.0.0.tgz", + "integrity": "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minimatch": "^10.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/ini": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/ip-address": { + "version": "10.7.4", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.4.tgz", + "integrity": "sha512-DWBMKyeM25D70nrIiTkgQth59/G+naKOTep6huF9j+GmeK3DMD60Zvxg5d5qQnz+38AHgou6ean2LECX+lrKDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-in-ssh": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", + "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-5.0.0.tgz", + "integrity": "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-nice": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/json-stringify-nice/-/json-stringify-nice-1.1.4.tgz", + "integrity": "sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw==", + "dev": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/just-diff": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/just-diff/-/just-diff-6.0.2.tgz", + "integrity": "sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA==", + "dev": true, + "license": "MIT" + }, + "node_modules/just-diff-apply": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/just-diff-apply/-/just-diff-apply-5.5.0.tgz", + "integrity": "sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/make-fetch-happen": { + "version": "15.0.6", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.6.tgz", + "integrity": "sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/agent": "^4.0.0", + "@npmcli/redact": "^4.0.0", + "cacache": "^20.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^6.0.0", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.2.tgz", + "integrity": "sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^2.0.0", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + }, + "optionalDependencies": { + "iconv-lite": "^0.7.2" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", + "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-sized": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-2.0.0.tgz", + "integrity": "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-gyp": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", + "integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "undici": "^6.25.0", + "which": "^6.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/nopt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-bundled": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-5.0.0.tgz", + "integrity": "sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^5.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-install-checks": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-8.0.0.tgz", + "integrity": "sha512-ScAUdMpyzkbpxoNekQ3tNRdFI8SJ86wgKZSQZdUxT+bj0wVFpsEMWnkXP0twVe1gJyNF5apBWDJhhIbgrIViRA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-5.0.0.tgz", + "integrity": "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-package-arg": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.2.tgz", + "integrity": "sha512-IciCE3SY3uE84Ld8WZU23gAPPV9rIYod4F+rc+vJ7h7cwAJt9Vk6TVsK60ry7Uj3SRS3bqRRIGuTp9YVlk6WNA==", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^7.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-packlist": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.4.tgz", + "integrity": "sha512-uMW73iajD8hiH4ZBxEV3HC+eTnppIqwakjOYuvgddnalIw2lJguKviK1pcUJDlIWm1wSJkchpDZDSVVsZEYRng==", + "dev": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^8.0.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-11.0.3.tgz", + "integrity": "sha512-buzyCfeoGY/PxKqmBqn1IUJrZnUi1VVJTdSSRPGI60tJdUhUoSQFhs0zycJokDdOznQentgrpf8LayEHyyYlqQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^8.0.0", + "npm-normalize-package-bin": "^5.0.0", + "npm-package-arg": "^13.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-19.1.1.tgz", + "integrity": "sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^4.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^15.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^13.0.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/open": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/open/-/open-11.0.4.tgz", + "integrity": "sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.5.1", + "define-lazy-prop": "^3.0.0", + "is-in-ssh": "^1.0.0", + "is-inside-container": "^1.0.0", + "powershell-utils": "^0.2.1", + "wsl-utils": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.1.1.tgz", + "integrity": "sha512-6Lh3jQPLthYujSrlw6ccWXBGZTkoTCz27Hd92g79LH/kjAbgoEu0t442cCCNbegPSJsA0lwDX8z0bcKtkpA32g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/pacote": { + "version": "21.5.1", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.5.1.tgz", + "integrity": "sha512-KvcJ9iy3crysCsgqc4+PknH/w6jkrp8JN36mpZBPwNaDRwTfMZD37YzRazNstiZUOhuF5pno9f78n9mEJBavwg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/git": "^7.0.0", + "@npmcli/installed-package-contents": "^4.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "@npmcli/run-script": "^10.0.0", + "cacache": "^20.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^13.0.0", + "npm-packlist": "^10.0.1", + "npm-pick-manifest": "^11.0.1", + "npm-registry-fetch": "^19.0.0", + "proc-log": "^6.0.0", + "sigstore": "^4.0.0", + "ssri": "^13.0.0", + "tar": "^7.4.3" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-conflict-json": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/parse-conflict-json/-/parse-conflict-json-5.0.1.tgz", + "integrity": "sha512-ZHEmNKMq1wyJXNwLxyHnluPfRAFSIliBvbK/UiOceROt4Xh9Pz0fq49NytIaeaCUf5VR86hwQ/34FCcNU5/LKQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^5.0.0", + "just-diff": "^6.0.0", + "just-diff-apply": "^5.2.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/powershell-utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.2.1.tgz", + "integrity": "sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/proggy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/proggy/-/proggy-4.0.0.tgz", + "integrity": "sha512-MbA4R+WQT76ZBm/5JUpV9yqcJt92175+Y0Bodg3HgiXzrmKu7Ggq+bpn6y6wHH+gN9NcyKn3yg1+d47VaKwNAQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/promise-all-reject-late": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-all-reject-late/-/promise-all-reject-late-1.0.1.tgz", + "integrity": "sha512-vuf0Lf0lOxyQREH7GDIOUMLS7kz+gs8i6B+Yi8dC68a2sychGrHTJYghMBD6k7eUcH0H5P73EckCA48xijWqXw==", + "dev": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/promise-call-limit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/promise-call-limit/-/promise-call-limit-3.0.2.tgz", + "integrity": "sha512-mRPQO2T1QQVw11E7+UdCJu7S61eJVWknzml9sC1heAdj1jxl0fWMBypIt9ZOcLFf8FkG995ZD7RnVk7HH72fZw==", + "dev": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/read-cmd-shim": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-6.0.0.tgz", + "integrity": "sha512-1zM5HuOfagXCBWMN83fuFI/x+T/UhZ7k+KIzhrHXcQoeX5+7gmaDYjELQHmmzIodumBHeByBJT4QYS7ufAgs7A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/redis": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/redis/-/redis-6.3.0.tgz", + "integrity": "sha512-XFQbPie1lGpKeUZ8ySYY43yLnQy/iIQtHNvIr/1XdaABpb96rUSdm3Mfl+98FEVbv9ZJIzINMno5s0Nh8E5LEw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@redis/bloom": "6.3.0", + "@redis/client": "6.3.0", + "@redis/json": "6.3.0", + "@redis/search": "6.3.0", + "@redis/time-series": "6.3.0" + }, + "engines": { + "node": ">= 20.0.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sigstore": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-4.1.1.tgz", + "integrity": "sha512-endqECJkfhozrXMK5ngu/UAA0xVcVEFdnHJCElGaExypjW+HK5i6zu3NteLoaX/iFbRUbC3+DjttQs0GARr+5w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.2.1", + "@sigstore/protobuf-specs": "^0.5.0", + "@sigstore/sign": "^4.1.1", + "@sigstore/tuf": "^4.0.2", + "@sigstore/verify": "^3.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.10", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", + "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.24", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.24.tgz", + "integrity": "sha512-cLS9TtWkIQFyLkJ3/5aFQAOHOSKTlOs/7WDut/XPSdjom1fJhUdkepeJAKXa9Y05+FabHd0sti+Et559vDtkpQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/ssri": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.1.tgz", + "integrity": "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==", + "dev": true, + "license": "ISC", "dependencies": { - "pure-rand": "^8.0.0" + "minipass": "^7.0.3" }, "engines": { - "node": ">=12.17.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/find-my-way-ts": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/find-my-way-ts/-/find-my-way-ts-0.1.6.tgz", - "integrity": "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==", - "dev": true, - "license": "MIT" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-tsconfig": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", - "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { - "resolve-pkg-maps": "^1.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + "engines": { + "node": ">=8" } }, - "node_modules/ini": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", - "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, - "license": "ISC", + "license": "MIT", "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": ">=8" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/kubernetes-types": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/kubernetes-types/-/kubernetes-types-1.30.0.tgz", - "integrity": "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==", + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, - "license": "Apache-2.0" + "license": "MIT" }, - "node_modules/msgpackr": { - "version": "1.11.12", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.12.tgz", - "integrity": "sha512-RBdJ1Un7yGlXWajrkxcSa93nvQ0w4zBf60c0yYv7YtBelP8H2FA7XsfBbMHtXKXUMUxH7zV3Zuozh+kUQWhHvg==", + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", - "optionalDependencies": { - "msgpackr-extract": "^3.0.2" + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "node_modules/msgpackr-extract": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", - "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "optional": true, "dependencies": { - "node-gyp-build-optional-packages": "5.2.2" + "ansi-regex": "^6.2.2" }, - "bin": { - "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + "engines": { + "node": ">=12" }, - "optionalDependencies": { - "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/multipasta": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/multipasta/-/multipasta-0.2.7.tgz", - "integrity": "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-gyp-build-optional-packages": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", - "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "detect-libc": "^2.0.1" + "ansi-regex": "^5.0.1" }, - "bin": { - "node-gyp-build-optional-packages": "bin.js", - "node-gyp-build-optional-packages-optional": "optional.js", - "node-gyp-build-optional-packages-test": "build-test.js" + "engines": { + "node": ">=8" } }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/pure-rand": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.0.tgz", - "integrity": "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, "license": "MIT", + "engines": { + "node": ">=8" + }, "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { - "shebang-regex": "^3.0.0" + "has-flag": "^4.0.0" }, "engines": { "node": ">=8" } }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "node_modules/tar": { + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, "engines": { - "node": ">=8" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/toml": { + "node_modules/treeverse": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", - "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", + "resolved": "https://registry.npmjs.org/treeverse/-/treeverse-3.0.0.tgz", + "integrity": "sha512-gcANaAnd2QDZFmHFEOF4k7uc1J/6a6z3DJMd/QwEyxLoKGiptJRwid582r7QIsFlFMIZ3SnxfS52S4hm2DHkuQ==", "dev": true, - "license": "MIT" + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" }, "node_modules/tsx": { "version": "4.21.0", @@ -924,6 +5041,34 @@ "fsevents": "~2.3.3" } }, + "node_modules/tuf-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-4.1.0.tgz", + "integrity": "sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/models": "4.1.0", + "debug": "^4.4.3", + "make-fetch-happen": "^15.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -938,6 +5083,40 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.71.2.tgz", + "integrity": "sha512-Rz4effbMy2g9wnvOJZG6SbVavX313Lap1vNEbk1RUochzickvzP8P4HB+bkNg5e50eVz1hQGjh9K1omI1xB4Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.71.2", + "@typescript-eslint/parser": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2", + "@typescript-eslint/utils": "8.71.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici": { + "version": "6.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.29.0.tgz", + "integrity": "sha512-R+RODBqp6i2pPflGdq+xIOUkl+RNfGgHwoinecKu/JCuf2uO06cOKoDbI2P7Dn6KcswdKwrczbU6IYJ6K8X+wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -945,18 +5124,51 @@ "dev": true, "license": "MIT" }, - "node_modules/uuid": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.2.tgz", - "integrity": "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==", + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/validate-npm-package-name": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", + "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/walk-up-path": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz", + "integrity": "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", "dev": true, - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], "license": "MIT", - "bin": { - "uuid": "dist-node/bin/uuid" + "engines": { + "node": ">= 8" } }, "node_modules/which": { @@ -975,30 +5187,184 @@ "node": ">= 8" } }, - "node_modules/yaml": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz", - "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==", + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/write-file-atomic": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-7.0.1.tgz", + "integrity": "sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==", "dev": true, "license": "ISC", - "bin": { - "yaml": "bin.mjs" + "dependencies": { + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/ws": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/wsl-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-1.0.1.tgz", + "integrity": "sha512-c16PKLIRxBYpR3jSWsdS7SFp0COm5SOQdiFjY818dYFHJbMGuLBszpF07LBWmOS1uzrHa9VGdynsEgoeKy0oOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0", + "powershell-utils": "^0.2.0" }, "engines": { - "node": ">= 14.6" + "node": ">=20" }, "funding": { - "url": "https://github.com/sponsors/eemeli" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/zod": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.8.tgz", - "integrity": "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==", + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", + "engines": { + "node": ">=10" + }, "funding": { - "url": "https://github.com/sponsors/colinhacks" + "url": "https://github.com/sponsors/sindresorhus" } } } diff --git a/package.json b/package.json index d9058ee..56fabbe 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@gitdamnit/stranger-danger", - "version": "0.1.0", + "version": "0.2.0", "description": "Secret and PII redaction guard for AI coding agents — scans files, context windows, and outputs before secrets reach LLMs", "license": "MIT", "type": "module", @@ -20,27 +20,32 @@ "README.md", "CHANGELOG.md" ], -"scripts": { - "build": "tsc", - "build:rules": "tsx scripts/compile-rules.ts", - "prebuild": "npm run build:rules", - "test": "tsx --test tests/engine/*.ts tests/plugin/*.ts tests/cli/*.ts tests/integration.ts", - "build:test": "tsc -p tsconfig.test.json", - "pretest": "npm run build:rules", - "typecheck": "tsc --noEmit", - "prepublishOnly": "npm run typecheck && npm test && npm run build" - }, + "scripts": { + "build": "tsc", + "build:rules": "tsx scripts/compile-rules.ts", + "prebuild": "npm run build:rules", + "typecheck": "tsc --noEmit", + "lint": "eslint .", + "test": "tsx --test tests/*.test.ts tests/*/*.test.ts", + "test:unit": "tsx --test tests/engine/*.test.ts tests/cli/*.test.ts tests/plugin/*.test.ts", + "test:integration": "tsx --test tests/integration.test.ts", + "test:security": "tsx --test tests/hardening.test.ts tests/pentest.test.ts", + "pretest": "npm run build:rules", + "verify:package": "node scripts/verify-package.mjs", + "prepublishOnly": "npm run typecheck && npm run lint && npm test && npm run build" + }, "peerDependencies": { - "@opencode-ai/plugin": ">=1.0.0" + "@opencode/plugin": ">=2.0.0" }, "devDependencies": { - "@opencode-ai/plugin": "^1.0.0", + "@opencode/plugin": "^2.0.29", "@types/node": "^22.19.19", - "toml": "^3.0.0", + "eslint": "^9.0.0", "tsx": "^4.0.0", - "typescript": "^5.6.0" + "typescript": "^5.6.0", + "typescript-eslint": "^8.0.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/scripts/compile-rules.ts b/scripts/compile-rules.ts index eb609e1..e55c654 100755 --- a/scripts/compile-rules.ts +++ b/scripts/compile-rules.ts @@ -101,16 +101,124 @@ function parseGitleaksToml(content: string): TomlRule[] { return rules; } +/** + * Translate the subset of RE2 / PCRE syntax used by gitleaks into plain + * JavaScript RegExp source. + * + * JavaScript has no inline-flag groups, POSIX character classes, or `\z` + * anchor, so rules that use them crash at `new RegExp(...)` time. This + * function rewrites: + * + * - POSIX classes `[[:alnum:]]` → `[A-Za-z0-9]` + * - `\z` → `$` + * - named groups `(?P…)` / `(?…)` → `(?:…)` + * - inline flags `(?i:…)`, `(?s:…)`, `(?i)` → global flags + `(?:…)` + * + * `(?-i:…)` (disable case-insensitivity for a region) has no JavaScript + * equivalent. It is unwrapped to `(?:…)`, so a region that explicitly opted + * out of case-insensitivity may be matched case-insensitively when the whole + * rule enables the `i` flag. This only widens matching (more coverage, a few + * more false positives) and never produces an invalid pattern. + */ function processGitleaksRegex(raw: string): { pattern: string; flags: string } { - let pattern = raw; - let flags = "g"; + let pattern = raw + .replace(/\[\[:alnum:\]\]/g, "[A-Za-z0-9]") + .replace(/\[\[:alpha:\]\]/g, "[A-Za-z]") + .replace(/\[\[:digit:\]\]/g, "[0-9]") + .replace(/\[\[:xdigit:\]\]/g, "[0-9A-Fa-f]") + .replace(/\[\[:space:\]\]/g, "\\s") + .replace(/\[\[:word:\]\]/g, "[A-Za-z0-9_]") + .replace(/\[\[:upper:\]\]/g, "[A-Z]") + .replace(/\[\[:lower:\]\]/g, "[a-z]") + .replace(/\[\[:punct:\]\]/g, "[\\x21-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\x7e]"); + + let caseInsensitive = false; + let dotAll = false; + let multiline = false; + + let out = ""; + let i = 0; + const n = pattern.length; + + while (i < n) { + const ch = pattern[i]; + + // Escapes: translate `\z` (RE2 end-of-string) to `$`. + if (ch === "\\") { + if (pattern[i + 1] === "z") { + out += "$"; + i += 2; + continue; + } + out += ch + (pattern[i + 1] ?? ""); + i += 2; + continue; + } + + // Copy character classes verbatim so their contents are never rewritten. + if (ch === "[") { + let j = i + 1; + if (pattern[j] === "^") j++; + if (pattern[j] === "]") j++; + while (j < n && pattern[j] !== "]") { + if (pattern[j] === "\\") j++; + j++; + } + out += pattern.slice(i, j + 1); + i = j + 1; + continue; + } + + if (ch === "(" && pattern[i + 1] === "?") { + const rest = pattern.slice(i); + + // Named groups → non-capturing groups. + const named = /^\(\?P<[^>]*>/.exec(rest) ?? /^\(\?<[A-Za-z_][^>]*>/.exec(rest); + if (named) { + out += "(?:"; + i += named[0].length; + continue; + } - if (pattern.includes("(?i)")) { - flags = "gi"; - pattern = pattern.replace(/\(\?i\)/g, ""); + // Lookarounds are supported by JavaScript; pass them through. + if (/^\(\?(?:=|!|<=|]*>/g, "(?:"); + pattern = out; + + let flags = "g"; + if (caseInsensitive) flags += "i"; + if (dotAll) flags += "s"; + if (multiline) flags += "m"; + pattern = pattern.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); return { pattern, flags }; diff --git a/scripts/verify-package.mjs b/scripts/verify-package.mjs new file mode 100644 index 0000000..222568e --- /dev/null +++ b/scripts/verify-package.mjs @@ -0,0 +1,95 @@ +#!/usr/bin/env node +/** + * Package validation: build, pack the npm tarball, install it into an isolated + * temporary project, and exercise the installed artifact (standalone CLI plus + * the plugin's default export). This proves the published `files`, `exports`, + * `bin`, and runtime dependencies are correct — not just the source tree. + */ +import { execFileSync } from "node:child_process"; +import { mkdtempSync, rmSync, readdirSync, existsSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; + +const root = resolve(new URL("..", import.meta.url).pathname); +const run = (cmd, args, opts = {}) => + execFileSync(cmd, args, { stdio: "pipe", encoding: "utf8", ...opts }); + +function step(label, fn) { + process.stdout.write(`- ${label} ... `); + const result = fn(); + process.stdout.write("ok\n"); + return result; +} + +let workdir; +try { + step("build", () => run("npm", ["run", "build"], { cwd: root, stdio: "inherit" })); + + const packOutput = step("npm pack", () => run("npm", ["pack", "--json"], { cwd: root })); + const tarballName = JSON.parse(packOutput)[0].filename; + const tarballPath = join(root, tarballName); + + workdir = mkdtempSync(join(tmpdir(), "sd-package-")); + step("init isolated project", () => + run("npm", ["init", "-y"], { cwd: workdir, stdio: "ignore" })); + step("install packed tarball", () => + run("npm", ["install", tarballPath, "--no-audit", "--no-fund"], { cwd: workdir, stdio: "ignore" })); + + const bin = join(workdir, "node_modules", ".bin", process.platform === "win32" ? "sd.cmd" : "sd"); + if (!existsSync(bin)) throw new Error("installed package did not provide the `sd` bin"); + + const version = step("run `sd version`", () => run(bin, ["version"], { cwd: workdir })).trim(); + if (!/v\d+\.\d+\.\d+/.test(version)) throw new Error(`unexpected version output: ${version}`); + + step("run `sd rules`", () => { + const out = run(bin, ["rules"], { cwd: workdir }); + if (!/Total rules: \d+/.test(out)) throw new Error("sd rules did not report a rule count"); + }); + + step("scan detects a synthetic secret without leaking it", () => { + // High-entropy synthetic token so the github-pat rule's entropy gate accepts it. + const secret = "ghp_" + "aB3xK9mQ2pR7vL4nJ8wC5tY1cF6hA0sD8gE2"; + const out = run(bin, ["scan", "-", "--json"], { + cwd: workdir, + input: `const k = "${secret}";`, + }); + const parsed = JSON.parse(out); + if (!Array.isArray(parsed.findings) || parsed.findings.length === 0) { + throw new Error("scan JSON reported no findings"); + } + if (out.includes(secret)) throw new Error("scan JSON leaked the raw secret"); + }); + + step("standalone engine export resolves", () => { + const r = run(process.execPath, ["--input-type=module", "-e", + "import('@gitdamnit/stranger-danger/engine').then(m=>{if(typeof m.scan!=='function')process.exit(1)})", + ], { cwd: workdir }); + void r; + }); + + step("plugin default export has the V2 shape", () => { + run(process.execPath, ["--input-type=module", "-e", + "import('@gitdamnit/stranger-danger').then(m=>{const p=m.default;if(!p||p.id!=='gitdamnit.stranger-danger'||typeof p.setup!=='function')process.exit(1)})", + ], { cwd: workdir }); + }); + + // Clean up the tarball produced by pack. + try { rmSync(tarballPath, { force: true }); } catch { /* ignore */ } + + console.log("\nPackage verification passed."); +} catch (err) { + console.error("\nPackage verification FAILED:", err instanceof Error ? err.stack ?? err.message : err); + process.exitCode = 1; +} finally { + if (workdir) { + try { rmSync(workdir, { recursive: true, force: true }); } catch { /* ignore */ } + } + // Remove any stray tarballs in the repo root. + try { + for (const f of readdirSync(root)) { + if (f.startsWith("gitdamnit-stranger-danger-") && f.endsWith(".tgz")) { + rmSync(join(root, f), { force: true }); + } + } + } catch { /* ignore */ } +} diff --git a/src/cli/args.ts b/src/cli/args.ts index f10a67f..3d9a9d7 100755 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -19,6 +19,12 @@ export function parseArgs(argv: string[]): CliArgs { for (let i = 1; i < args.length; i++) { const arg = args[i]; if (arg.startsWith("--")) { + const eq = arg.indexOf("="); + if (eq !== -1) { + // --flag=value + flags[arg.slice(2, eq)] = arg.slice(eq + 1); + continue; + } const key = arg.slice(2); const next = args[i + 1]; if (next && !next.startsWith("--")) { @@ -64,21 +70,23 @@ export function parseArgs(argv: string[]): CliArgs { } export const USAGE = ` -strangerDanger v0.1.0 — Secret and PII redaction guard +strangerDanger v0.2.0 — Secret and PII redaction guard Usage: sd [options] Commands: scan Scan a file, directory, or stdin for secrets - redact Redact secrets in a file in-place (creates .backup) + redact Redact secrets in a file in-place (atomic, no plaintext backup by default) audit Show the audit log rules List all active detection rules - allowlist add Add an allowlist entry interactively + allowlist add Add an allowlist entry allowlist show Show current allowlist entries version Show version Options: --source Source type for scan: file, context, output, message - --json Output in JSON format + --json Output in JSON format (never contains raw secrets) + --backup Redact with a permission-restricted backup outside the working tree + --backup-dir Store the redaction backup in instead of the default location --help Show this help message `; diff --git a/src/cli/commands/allowlist.ts b/src/cli/commands/allowlist.ts index 9809cf3..b390c8f 100755 --- a/src/cli/commands/allowlist.ts +++ b/src/cli/commands/allowlist.ts @@ -1,6 +1,30 @@ -import { loadAllowlist } from "../../engine/allowlist.js"; +import { loadAllowlist, saveAllowlistEntry } from "../../engine/allowlist.js"; +import type { AllowlistEntry } from "../../engine/types.js"; -export async function cmdAllowlist(subcommand: string, _positional: string[]): Promise { +function pickEntry(flags: Record): AllowlistEntry { + const entry: AllowlistEntry = {}; + const str = (v: string | boolean | undefined): string | undefined => + typeof v === "string" && v.length > 0 ? v : undefined; + + const description = str(flags.description); + const fingerprint = str(flags.fingerprint); + const ruleId = str(flags.ruleId) ?? str(flags["rule-id"]); + const path = str(flags.path); + const regex = str(flags.regex); + + if (description) entry.description = description; + if (fingerprint) entry.fingerprint = fingerprint; + if (ruleId) entry.ruleId = ruleId; + if (path) entry.path = path; + if (regex) entry.regex = regex; + return entry; +} + +export async function cmdAllowlist( + subcommand: string, + _positional: string[], + flags: Record = {}, +): Promise { if (subcommand === "show") { const allowlist = loadAllowlist(); if (allowlist.entries.length === 0) { @@ -19,17 +43,27 @@ export async function cmdAllowlist(subcommand: string, _positional: string[]): P } if (subcommand === "add") { - console.log("Add allowlist entry. Choose a type:"); - console.log(" 1. By fingerprint"); - console.log(" 2. By rule ID"); - console.log(" 3. By file path"); - console.log(" 4. By regex pattern"); - console.log(""); - console.log("Usage examples:"); - console.log(" sd allowlist add --fingerprint a3f7b2c1 --description \"Known test key\""); - console.log(" sd allowlist add --ruleId aws-access-token --description \"Internal AWS account\""); - console.log(" sd allowlist add --path \"test/fixtures/*.ts\" --description \"Test fixtures\""); - console.log(" sd allowlist add --regex \"EXAMPLE\" --description \"Example values\""); + const entry = pickEntry(flags); + if (Object.keys(entry).length === 0 || (!entry.fingerprint && !entry.ruleId && !entry.path && !entry.regex)) { + console.error("Usage: sd allowlist add --fingerprint | --ruleId | --path | --regex [--description \"...\"]"); + process.exit(1); + } + + if (entry.regex) { + try { + new RegExp(entry.regex); + } catch (err) { + console.error(`Error: invalid --regex pattern: ${err instanceof Error ? err.message : String(err)}`); + process.exit(1); + } + } + if (entry.fingerprint && !/^[a-f0-9]{8}$/i.test(entry.fingerprint)) { + console.error("Error: --fingerprint must be 8 hexadecimal characters."); + process.exit(1); + } + + saveAllowlistEntry(entry); + console.log("Allowlist entry added."); return; } diff --git a/src/cli/commands/audit.ts b/src/cli/commands/audit.ts index f0de4ec..95048af 100755 --- a/src/cli/commands/audit.ts +++ b/src/cli/commands/audit.ts @@ -16,7 +16,9 @@ export async function cmdAudit(): Promise { for (const line of lines) { try { const entry = JSON.parse(line); - console.log(`[${entry.timestamp}] ${entry.ruleId} — ${entry.source} — fingerprint: ${entry.fingerprint}`); + const session = entry.sessionId ? ` — session: ${entry.sessionId}` : ""; + const where = entry.filePath ? ` — ${entry.filePath}` : ""; + console.log(`[${entry.timestamp}] ${entry.ruleId} — ${entry.source} — fingerprint: ${entry.fingerprint}${session}${where}`); } catch { console.log(line); } diff --git a/src/cli/commands/redact.ts b/src/cli/commands/redact.ts index 413ff29..a745084 100755 --- a/src/cli/commands/redact.ts +++ b/src/cli/commands/redact.ts @@ -1,45 +1,147 @@ -import { readFileSync, writeFileSync, copyFileSync, existsSync } from "node:fs"; -import { scanAndRedact } from "../../engine/scanner.js"; -import { writeAuditBatch } from "../../engine/audit.js"; +import { + closeSync, + existsSync, + fsyncSync, + lstatSync, + mkdirSync, + openSync, + readFileSync, + renameSync, + statSync, + writeSync, +} from "node:fs"; +import { basename, dirname, join, resolve } from "node:path"; +import { homedir } from "node:os"; +import { randomBytes } from "node:crypto"; +import { scanAndRedactBounded } from "../../engine/scanner.js"; +import { writeAuditBatch, auditEntryFromFinding } from "../../engine/audit.js"; import type { Finding } from "../../engine/types.js"; -export async function cmdRedact(positional: string[]): Promise { +const BACKUP_DIR_MODE = 0o700; +const BACKUP_FILE_MODE = 0o600; + +function isBinary(content: Buffer): boolean { + for (let i = 0; i < Math.min(content.length, 8192); i++) { + if (content[i] === 0) return true; + } + return false; +} + +/** + * Write `data` to `path` atomically with an explicit mode: write a sibling + * temp file, fsync it, then rename over the destination. A crash leaves either + * the old file or the new file, never a truncated mix. + */ +function atomicWrite(path: string, data: string, mode: number): void { + const dir = dirname(path); + const tmp = join(dir, `.${basename(path)}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`); + const fd = openSync(tmp, "w", mode); + try { + const buf = Buffer.from(data, "utf8"); + writeSync(fd, buf, 0, buf.length, 0); + fsyncSync(fd); + } finally { + closeSync(fd); + } + renameSync(tmp, path); +} + +/** Default backup location: a private per-user state directory, never the working tree. */ +function defaultBackupDir(): string { + const stateHome = process.env.XDG_STATE_HOME && process.env.XDG_STATE_HOME.length > 0 + ? process.env.XDG_STATE_HOME + : join(homedir(), ".local", "state"); + return join(stateHome, "strangerdanger", "backups"); +} + +/** Create a backup that never collides with (and never deletes) an existing file. */ +function writeSecureBackup(originalPath: string, content: Buffer, dir: string): string { + mkdirSync(dir, { recursive: true, mode: BACKUP_DIR_MODE }); + const stamp = new Date().toISOString().replace(/[:.]/g, "-"); + let target = join(dir, `${basename(originalPath)}.${stamp}.bak`); + let counter = 1; + while (existsSync(target)) { + target = join(dir, `${basename(originalPath)}.${stamp}.${counter}.bak`); + counter++; + } + const fd = openSync(target, "w", BACKUP_FILE_MODE); + try { + writeSync(fd, content, 0, content.length, 0); + fsyncSync(fd); + } finally { + closeSync(fd); + } + return target; +} + +export async function cmdRedact( + positional: string[], + flags: Record = {}, +): Promise { if (positional.length === 0) { console.error("Usage: sd redact "); process.exit(1); } const filePath = positional[0]; + const wantBackup = !!flags.backup || typeof flags["backup-dir"] === "string"; if (!existsSync(filePath)) { console.error(`Error: File not found: ${filePath}`); process.exit(1); } + let stats; try { - const content = readFileSync(filePath, "utf8"); - const { redacted, findings } = scanAndRedact(content, "file"); + stats = lstatSync(filePath); + } catch (err) { + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + process.exit(1); + } + if (!stats.isFile()) { + console.error(`Error: Refusing to redact non-regular file: ${filePath}`); + process.exit(1); + } + + try { + const raw = readFileSync(filePath); + if (isBinary(raw)) { + console.error(`Error: Refusing to redact binary file: ${filePath}`); + process.exit(1); + } + const content = raw.toString("utf8"); + const { redacted, findings, complete, policy } = scanAndRedactBounded(content, "file", filePath); if (findings.length === 0) { console.log("No secrets found. File unchanged."); return; } - const backupPath = filePath + ".backup"; - copyFileSync(filePath, backupPath); - writeFileSync(filePath, redacted, "utf8"); - - const auditEntries = findings.map((f: Finding) => ({ - timestamp: new Date().toISOString(), - source: "file", - ruleId: f.ruleId, - description: f.description, - fingerprint: f.redacted.match(/:([a-f0-9]{8})\]/)?.[1] || "", - })); + // A backup is explicit and lives outside the working tree by default, + // so a plaintext copy of the secret is never dropped next to the file. + let backupPath: string | undefined; + if (wantBackup) { + const dir = typeof flags["backup-dir"] === "string" + ? resolve(flags["backup-dir"] as string) + : defaultBackupDir(); + backupPath = writeSecureBackup(filePath, raw, dir); + } + + const mode = statSync(filePath).mode & 0o777; + atomicWrite(filePath, redacted, mode); + + const auditEntries = findings.map((f: Finding) => auditEntryFromFinding(f, { filePath })); writeAuditBatch(auditEntries); console.log(`Redacted ${findings.length} secret(s) in ${filePath}`); - console.log(`Backup saved to ${backupPath}`); + if (backupPath) { + console.log(`Backup saved to ${backupPath} (mode 0600)`); + } else { + console.log("No plaintext backup written (use --backup to keep one)."); + } + if (!complete) { + console.error(`Warning: content could not be fully inspected (${policy}); output was failed closed.`); + } for (const f of findings) { console.log(` ${f.ruleId}: ${f.redacted}`); } diff --git a/src/cli/commands/scan.ts b/src/cli/commands/scan.ts index 840cca1..059dd21 100755 --- a/src/cli/commands/scan.ts +++ b/src/cli/commands/scan.ts @@ -1,6 +1,6 @@ -import { readFileSync, statSync, readdirSync } from "node:fs"; +import { readFileSync, statSync, readdirSync, lstatSync } from "node:fs"; import { join, extname } from "node:path"; -import { scanAndRedact } from "../../engine/scanner.js"; +import { scanAndRedactBounded, toSafeFinding, type SafeFinding } from "../../engine/scanner.js"; import type { Finding } from "../../engine/types.js"; const SKIP_EXTENSIONS = new Set([ @@ -12,6 +12,11 @@ const SKIP_EXTENSIONS = new Set([ ".lock", ".lockb", ]); +const SKIP_DIRECTORIES = new Set(["node_modules", ".git"]); + +/** Refuse to slurp absurdly large files into memory; report them explicitly. */ +const MAX_READ_BYTES = 64 * 1024 * 1024; + function isBinary(content: Buffer): boolean { for (let i = 0; i < Math.min(content.length, 8192); i++) { if (content[i] === 0) return true; @@ -19,40 +24,96 @@ function isBinary(content: Buffer): boolean { return false; } -function scanFile(filePath: string, source: string): { findings: Finding[]; redacted: string } { +function isRegularFile(filePath: string): boolean { + try { + return lstatSync(filePath).isFile(); + } catch { + return false; + } +} + +type ScanOutcome = { findings: SafeFinding[]; skipped?: string }; + +function scanText(text: string, source: string, filePath?: string): ScanOutcome { + const { findings, complete, policy } = scanAndRedactBounded(text, source as Finding["source"], filePath); + const safe = findings.map((f) => toSafeFinding(f, text)); + if (!complete) { + return { findings: safe, skipped: `${filePath ?? ""}: could not be fully inspected (${policy})` }; + } + return { findings: safe }; +} + +function scanFile(filePath: string, source: string): ScanOutcome { + if (!isRegularFile(filePath)) { + return { findings: [], skipped: `${filePath}: not a regular file` }; + } + let size = 0; + try { + size = statSync(filePath).size; + } catch { + return { findings: [], skipped: `${filePath}: unreadable` }; + } + if (size > MAX_READ_BYTES) { + return { findings: [], skipped: `${filePath}: exceeds ${MAX_READ_BYTES} byte read limit` }; + } + const raw = readFileSync(filePath); - if (isBinary(raw)) return { findings: [], redacted: "" }; - const content = raw.toString("utf8"); - return scanAndRedact(content, source as Finding["source"], filePath); + if (isBinary(raw)) return { findings: [] }; + return scanText(raw.toString("utf8"), source, filePath); } -function scanDirectory(dirPath: string, source: string): Finding[] { - const allFindings: Finding[] = []; +function scanDirectory(dirPath: string, source: string): { findings: SafeFinding[]; skipped: string[] } { + const allFindings: SafeFinding[] = []; + const skipped: string[] = []; - function walk(dir: string) { - const entries = readdirSync(dir, { withFileTypes: true }); + function walk(dir: string, depth: number): void { + if (depth > 64) return; + let entries; + try { + entries = readdirSync(dir, { withFileTypes: true }); + } catch { + skipped.push(`${dir}: unreadable directory`); + return; + } for (const entry of entries) { const fullPath = join(dir, entry.name); - if (entry.name === "node_modules" || entry.name === ".git") continue; + // Never follow symlinks while walking: avoids cycles and traversal. + if (entry.isSymbolicLink()) continue; if (entry.isDirectory()) { - walk(fullPath); + if (SKIP_DIRECTORIES.has(entry.name)) continue; + walk(fullPath, depth + 1); } else if (entry.isFile()) { const ext = extname(entry.name).toLowerCase(); if (SKIP_EXTENSIONS.has(ext)) continue; - try { - const { findings } = scanFile(fullPath, source); - for (const f of findings) { - allFindings.push({ ...f, match: `${fullPath}: ${f.match}` }); - } - } catch { - // Skip files we can't read - } + const outcome = scanFile(fullPath, source); + allFindings.push(...outcome.findings); + if (outcome.skipped) skipped.push(outcome.skipped); } } } - walk(dirPath); - return allFindings; + walk(dirPath, 0); + return { findings: allFindings, skipped }; +} + +function printHuman(target: string, findings: SafeFinding[], skipped: string[]): void { + if (findings.length === 0 && skipped.length === 0) { + console.log(`No secrets detected in ${target}.`); + return; + } + if (findings.length > 0) { + console.log(`Found ${findings.length} secret(s) in ${target}:`); + for (const f of findings) { + const where = f.filePath ? `${f.filePath}${f.line ? `:${f.line}:${(f.column ?? 0) + 1}` : ""}` : f.source; + console.log(` ${f.ruleId}: ${f.description}`); + console.log(` Location: ${where}`); + console.log(` Fingerprint: ${f.fingerprint}`); + console.log(` Redaction: ${f.redaction}`); + } + } else { + console.log(`No secrets detected in ${target}.`); + } + for (const s of skipped) console.error(`Warning: ${s}`); } export async function cmdScan(positional: string[], flags: Record): Promise { @@ -71,41 +132,34 @@ export async function cmdScan(positional: string[], flags: Record", outcome.findings, outcome.skipped ? [outcome.skipped] : []); return; } try { const stats = statSync(target); - let findings: Finding[]; + let findings: SafeFinding[]; + let skipped: string[] = []; if (stats.isDirectory()) { - findings = scanDirectory(target, source); - } else { - const result = scanFile(target, source); + const result = scanDirectory(target, source); findings = result.findings; + skipped = result.skipped; + } else { + const outcome = scanFile(target, source); + findings = outcome.findings; + if (outcome.skipped) skipped = [outcome.skipped]; } if (asJson) { - console.log(JSON.stringify(findings, null, 2)); - } else if (findings.length === 0) { - console.log(`No secrets detected in ${target}.`); + console.log(JSON.stringify({ findings, skipped }, null, 2)); } else { - console.log(`Found ${findings.length} secret(s) in ${target}:`); - for (const f of findings) { - console.log(` ${f.ruleId}: ${f.description}`); - console.log(` Redacted: ${f.redacted}`); - } + printHuman(target, findings, skipped); } } catch (err) { console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); diff --git a/src/cli/index.ts b/src/cli/index.ts index 9fec247..09f6e8b 100755 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -6,6 +6,7 @@ import { cmdRedact } from "./commands/redact.js"; import { cmdAudit } from "./commands/audit.js"; import { cmdRules } from "./commands/rules.js"; import { cmdAllowlist } from "./commands/allowlist.js"; +import { VERSION } from "../shared/version.js"; async function main(): Promise { const { command, positional, flags } = parseArgs(process.argv); @@ -20,7 +21,7 @@ async function main(): Promise { await cmdScan(positional, flags); break; case "redact": - await cmdRedact(positional); + await cmdRedact(positional, flags); break; case "audit": await cmdAudit(); @@ -29,10 +30,10 @@ async function main(): Promise { await cmdRules(); break; case "allowlist": - await cmdAllowlist(positional[0] || "show", positional.slice(1)); + await cmdAllowlist(positional[0] || "show", positional.slice(1), flags); break; case "version": - console.log("strangerDanger v0.1.0"); + console.log(`strangerDanger v${VERSION}`); break; default: console.error(`Unknown command: ${command}`); diff --git a/src/engine/audit.ts b/src/engine/audit.ts index f045afe..f76803d 100755 --- a/src/engine/audit.ts +++ b/src/engine/audit.ts @@ -1,40 +1,145 @@ -import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs"; -import { dirname } from "node:path"; -import type { AuditEntry } from "./types.js"; +import { + appendFileSync, + existsSync, + mkdirSync, + readFileSync, + renameSync, + statSync, +} from "node:fs"; +import { dirname, join } from "node:path"; +import { fingerprint as fingerprintOf } from "../shared/fingerprint.js"; +import type { AuditEntry, Finding } from "./types.js"; -const DEFAULT_AUDIT_PATH = ".opencode/state/strangerdanger-audit.jsonl"; +const STATE_DIRNAME = ".opencode"; +const STATE_SUBDIR = "state"; +const AUDIT_FILENAME = "strangerdanger-audit.jsonl"; -let auditPath: string = DEFAULT_AUDIT_PATH; +/** File mode for audit files (owner read/write only — never world-readable). */ +const AUDIT_FILE_MODE = 0o600; +/** File mode for the directory holding audit files. */ +const AUDIT_DIR_MODE = 0o700; +/** Rotate the log once it passes this size, keeping at most `MAX_ROTATIONS` files. */ +const MAX_AUDIT_BYTES = 10 * 1024 * 1024; +const MAX_ROTATIONS = 3; + +/** + * The audit path used by the standalone CLI and by callers that do not pass an + * explicit path. It is intentionally NOT a shared, mutable, process-global + * value that the plugin relies on: the OpenCode plugin resolves a path per + * project/session and passes it explicitly to every write, so that concurrent + * projects in one server process never mix their audit records. + */ +let auditPathOverride: string | undefined; + +/** + * Resolve the default audit path for a project directory. Audit logs are + * per-project runtime state: they live under the project's own + * `.opencode/state/` directory and are kept out of version control. + */ +export function resolveAuditPath(options?: { directory?: string }): string { + const base = options?.directory && options.directory.length > 0 ? options.directory : process.cwd(); + return join(base, STATE_DIRNAME, STATE_SUBDIR, AUDIT_FILENAME); +} + +/** Override the path used when no explicit path is supplied (CLI/tests). */ export function setAuditPath(path: string): void { - auditPath = path; + auditPathOverride = path; } -export function writeAudit(entry: AuditEntry): void { - const dir = dirname(auditPath); +/** Get the path used when no explicit path is supplied. */ +export function getAuditPath(): string { + return auditPathOverride ?? resolveAuditPath(); +} + +function ensureDirectory(dir: string): void { if (!existsSync(dir)) { - mkdirSync(dir, { recursive: true }); + mkdirSync(dir, { recursive: true, mode: AUDIT_DIR_MODE }); } +} + +/** + * Bound on-disk growth. Existing records are preserved by renaming the log to + * `.1`, `.2`, ... before truncating, rather than deleting content outright. + */ +function maybeRotate(path: string): void { + let size = 0; + try { + size = statSync(path).size; + } catch { + return; + } + if (size < MAX_AUDIT_BYTES) return; + + for (let i = MAX_ROTATIONS - 1; i >= 1; i--) { + const from = `${path}.${i}`; + const to = `${path}.${i + 1}`; + if (existsSync(from)) { + try { + renameSync(from, to); + } catch { + // Best-effort rotation — never lose the live log over this. + } + } + } + try { + renameSync(path, `${path}.1`); + } catch { + // If rotation fails, keep appending rather than discarding records. + } +} + +/** + * Write one audit record as a single JSONL line. Each line is appended with + * O_APPEND semantics, so concurrent writers interleave whole lines rather than + * corrupting each other's records. + */ +export function writeAudit(entry: AuditEntry, path?: string): void { + const target = path ?? getAuditPath(); + const dir = dirname(target); + ensureDirectory(dir); + maybeRotate(target); const line = JSON.stringify({ ...entry, timestamp: entry.timestamp || new Date().toISOString(), }); - appendFileSync(auditPath, line + "\n", "utf8"); + appendFileSync(target, line + "\n", { encoding: "utf8", mode: AUDIT_FILE_MODE }); } -export function writeAuditBatch(entries: AuditEntry[]): void { +export function writeAuditBatch(entries: AuditEntry[], path?: string): void { for (const entry of entries) { - writeAudit(entry); + writeAudit(entry, path); } } -export function readAuditLog(): string { - if (!existsSync(auditPath)) return ""; - return readFileSync(auditPath, "utf8"); +export function readAuditLog(path?: string): string { + const target = path ?? getAuditPath(); + if (!existsSync(target)) return ""; + return readFileSync(target, "utf8"); } -export function getAuditPath(): string { - return auditPath; +/** + * Build a safe audit record from a finding. Only metadata is recorded — never + * the raw matched secret or the redacted replacement (which can embed the + * secret's fingerprint but is still not the secret). + */ +export function auditEntryFromFinding( + finding: Finding, + options?: { sessionId?: string; filePath?: string; source?: AuditEntry["source"] }, +): AuditEntry { + const fpMatch = finding.redacted.match(/:([a-f0-9]{8})\]/); + return { + timestamp: new Date().toISOString(), + ...(options?.sessionId ? { sessionId: options.sessionId } : {}), + source: (options?.source ?? finding.source) as AuditEntry["source"], + ruleId: finding.ruleId, + description: finding.description, + fingerprint: fpMatch ? fpMatch[1] : "", + ...((options?.filePath ?? finding.filePath) ? { filePath: options?.filePath ?? finding.filePath } : {}), + }; } + +/** Fingerprint helper re-export for callers that only need the audit module. */ +export { fingerprintOf }; diff --git a/src/engine/rules.ts b/src/engine/rules.ts index 5b4dcff..f195c85 100755 --- a/src/engine/rules.ts +++ b/src/engine/rules.ts @@ -56,7 +56,7 @@ export const NAMED_RULES: Rule[] = [ { id: "airtable-personnal-access-token", description: "Uncovered a possible Airtable Personal AccessToken, potentially compromising database access and leading to data leakage or alteration.", - regex: new RegExp("\\b(pat[[:alnum:]]{14}\\.[a-f0-9]{64})\\b", "g"), + regex: new RegExp("\\b(pat[A-Za-z0-9]{14}\\.[a-f0-9]{64})\\b", "g"), keywords: ["airtable"], }, { @@ -116,13 +116,13 @@ export const NAMED_RULES: Rule[] = [ { id: "atlassian-api-token", description: "Detected an Atlassian API token, posing a threat to project management and collaboration tool security and data confidentiality.", - regex: new RegExp("[\\w.-]{0,50}?(?:(?-i:ATLASSIAN|[Aa]tlassian)|(?-i:CONFLUENCE|[Cc]onfluence)|(?-i:JIRA|[Jj]ira))(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{20}[a-f0-9]{4})(?:[\\x60'\"\\s;]|\\\\[nr]|$)|\\b(ATATT3[A-Za-z0-9_\\-=]{186})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), + regex: new RegExp("[\\w.-]{0,50}?(?:(?:ATLASSIAN|[Aa]tlassian)|(?:CONFLUENCE|[Cc]onfluence)|(?:JIRA|[Jj]ira))(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{20}[a-f0-9]{4})(?:[\\x60'\"\\s;]|\\\\[nr]|$)|\\b(ATATT3[A-Za-z0-9_\\-=]{186})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), keywords: ["atlassian", "confluence", "jira", "atatt3"], entropy: 3.5, }, { id: "authress-service-client-access-key", description: "Uncovered a possible Authress Service Client Access Key, which may compromise access control services and sensitive data.", - regex: new RegExp("\\b((?:sc|ext|scauth|authress)_[a-z0-9]{5,30}\\.[a-z0-9]{4,6}\\.(?-i:acc)[_-][a-z0-9-]{10,32}\\.[a-z0-9+/_=-]{30,120})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), + regex: new RegExp("\\b((?:sc|ext|scauth|authress)_[a-z0-9]{5,30}\\.[a-z0-9]{4,6}\\.(?:acc)[_-][a-z0-9-]{10,32}\\.[a-z0-9+/_=-]{30,120})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), keywords: ["sc_", "ext_", "scauth_", "authress_"], entropy: 2, }, { @@ -182,7 +182,7 @@ export const NAMED_RULES: Rule[] = [ { id: "cisco-meraki-api-key", description: "Cisco Meraki is a cloud-managed IT solution that provides networking, security, and device management through an easy-to-use interface.", - regex: new RegExp("[\\w.-]{0,50}?(?i:[\\w.-]{0,50}?(?:(?-i:[Mm]eraki|MERAKI))(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3})(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([0-9a-f]{40})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "g"), + regex: new RegExp("[\\w.-]{0,50}?(?:[\\w.-]{0,50}?(?:(?:[Mm]eraki|MERAKI))(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3})(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([0-9a-f]{40})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), keywords: ["meraki"], entropy: 3, }, { @@ -224,7 +224,7 @@ export const NAMED_RULES: Rule[] = [ { id: "cohere-api-token", description: "Identified a Cohere Token, posing a risk of unauthorized access to AI services and data manipulation.", - regex: new RegExp("[\\w.-]{0,50}?(?i:[\\w.-]{0,50}?(?:cohere|CO_API_KEY)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3})(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-zA-Z0-9]{40})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "g"), + regex: new RegExp("[\\w.-]{0,50}?(?:[\\w.-]{0,50}?(?:cohere|CO_API_KEY)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3})(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-zA-Z0-9]{40})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), keywords: ["cohere", "co_api_key"], entropy: 4, }, { @@ -254,13 +254,13 @@ export const NAMED_RULES: Rule[] = [ { id: "curl-auth-header", description: "Discovered a potential authorization token provided in a curl command header, which could compromise the curl accessed resource.", - regex: new RegExp("\\bcurl\\b(?:.*?|.*?(?:[\\r\\n]{1,2}.*?){1,5})[ \\t\\n\\r](?:-H|--header)(?:=|[ \\t]{0,5})(?:\"(?:Authorization:[ \\t]{0,5}(?:Basic[ \\t]([a-z0-9+/]{8,}={0,3})|(?:Bearer|(?:Api-)?Token)[ \\t]([\\w=~@.+/-]{8,})|([\\w=~@.+/-]{8,}))|(?:(?:X-(?:[a-z]+-)?)?(?:Api-?)?(?:Key|Token)):[ \\t]{0,5}([\\w=~@.+/-]{8,}))\"|'(?:Authorization:[ \\t]{0,5}(?:Basic[ \\t]([a-z0-9+/]{8,}={0,3})|(?:Bearer|(?:Api-)?Token)[ \\t]([\\w=~@.+/-]{8,})|([\\w=~@.+/-]{8,}))|(?:(?:X-(?:[a-z]+-)?)?(?:Api-?)?(?:Key|Token)):[ \\t]{0,5}([\\w=~@.+/-]{8,}))')(?:\\B|\\s|\\z)", "gi"), + regex: new RegExp("\\bcurl\\b(?:.*?|.*?(?:[\\r\\n]{1,2}.*?){1,5})[ \\t\\n\\r](?:-H|--header)(?:=|[ \\t]{0,5})(?:\"(?:Authorization:[ \\t]{0,5}(?:Basic[ \\t]([a-z0-9+/]{8,}={0,3})|(?:Bearer|(?:Api-)?Token)[ \\t]([\\w=~@.+/-]{8,})|([\\w=~@.+/-]{8,}))|(?:(?:X-(?:[a-z]+-)?)?(?:Api-?)?(?:Key|Token)):[ \\t]{0,5}([\\w=~@.+/-]{8,}))\"|'(?:Authorization:[ \\t]{0,5}(?:Basic[ \\t]([a-z0-9+/]{8,}={0,3})|(?:Bearer|(?:Api-)?Token)[ \\t]([\\w=~@.+/-]{8,})|([\\w=~@.+/-]{8,}))|(?:(?:X-(?:[a-z]+-)?)?(?:Api-?)?(?:Key|Token)):[ \\t]{0,5}([\\w=~@.+/-]{8,}))')(?:\\B|\\s|$)", "gi"), keywords: ["curl"], entropy: 2.75, }, { id: "curl-auth-user", description: "Discovered a potential basic authorization token provided in a curl command, which could compromise the curl accessed resource.", - regex: new RegExp("\\bcurl\\b(?:.*|.*(?:[\\r\\n]{1,2}.*){1,5})[ \\t\\n\\r](?:-u|--user)(?:=|[ \\t]{0,5})(\"(:[^\"]{3,}|[^:\"]{3,}:|[^:\"]{3,}:[^\"]{3,})\"|'([^:']{3,}:[^']{3,})'|((?:\"[^\"]{3,}\"|'[^']{3,}'|[\\w$@.-]+):(?:\"[^\"]{3,}\"|'[^']{3,}'|[\\w${}@.-]+)))(?:\\s|\\z)", "g"), + regex: new RegExp("\\bcurl\\b(?:.*|.*(?:[\\r\\n]{1,2}.*){1,5})[ \\t\\n\\r](?:-u|--user)(?:=|[ \\t]{0,5})(\"(:[^\"]{3,}|[^:\"]{3,}:|[^:\"]{3,}:[^\"]{3,})\"|'([^:']{3,}:[^']{3,})'|((?:\"[^\"]{3,}\"|'[^']{3,}'|[\\w$@.-]+):(?:\"[^\"]{3,}\"|'[^']{3,}'|[\\w${}@.-]+)))(?:\\s|$)", "g"), keywords: ["curl"], entropy: 2, }, { @@ -374,7 +374,7 @@ export const NAMED_RULES: Rule[] = [ { id: "etsy-access-token", description: "Found an Etsy Access Token, potentially compromising Etsy shop management and customer data.", - regex: new RegExp("[\\w.-]{0,50}?(?:(?-i:ETSY|[Ee]tsy))(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{24})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), + regex: new RegExp("[\\w.-]{0,50}?(?:(?:ETSY|[Ee]tsy))(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{24})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), keywords: ["etsy"], entropy: 3, }, { @@ -476,7 +476,7 @@ export const NAMED_RULES: Rule[] = [ { id: "generic-api-key", description: "Detected a Generic API Key, potentially exposing access to various services and sensitive operations.", - regex: new RegExp("[\\w.-]{0,50}?(?:access|auth|(?-i:[Aa]pi|API)|credential|creds|key|passw(?:or)?d|secret|token)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([\\w.=-]{10,150}|[a-z0-9][a-z0-9+/]{11,}={0,3})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), + regex: new RegExp("[\\w.-]{0,50}?(?:access|auth|(?:[Aa]pi|API)|credential|creds|key|passw(?:or)?d|secret|token)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([\\w.=-]{10,150}|[a-z0-9][a-z0-9+/]{11,}={0,3})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), keywords: ["access", "api", "auth", "key", "credential", "creds", "passwd", "password", "secret", "token"], entropy: 3.5, }, { @@ -638,7 +638,7 @@ export const NAMED_RULES: Rule[] = [ { id: "hashicorp-tf-api-token", description: "Uncovered a HashiCorp Terraform user/org API token, which may lead to unauthorized infrastructure management and security breaches.", - regex: new RegExp("[a-z0-9]{14}\\.(?-i:atlasv1)\\.[a-z0-9\\-_=]{60,70}", "gi"), + regex: new RegExp("[a-z0-9]{14}\\.(?:atlasv1)\\.[a-z0-9\\-_=]{60,70}", "gi"), keywords: ["atlasv1"], entropy: 3.5, }, { @@ -668,13 +668,13 @@ export const NAMED_RULES: Rule[] = [ { id: "huggingface-access-token", description: "Discovered a Hugging Face Access token, which could lead to unauthorized access to AI models and sensitive data.", - regex: new RegExp("\\b(hf_(?i:[a-z]{34}))(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "g"), + regex: new RegExp("\\b(hf_(?:[a-z]{34}))(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), keywords: ["hf_"], entropy: 2, }, { id: "huggingface-organization-api-token", description: "Uncovered a Hugging Face Organization API token, potentially compromising AI organization accounts and associated data.", - regex: new RegExp("\\b(api_org_(?i:[a-z]{34}))(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "g"), + regex: new RegExp("\\b(api_org_(?:[a-z]{34}))(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), keywords: ["api_org_"], entropy: 2, }, { @@ -728,7 +728,7 @@ export const NAMED_RULES: Rule[] = [ { id: "kubernetes-secret-yaml", description: "Possible Kubernetes Secret detected, posing a risk of leaking credentials/tokens from your deployments", - regex: new RegExp("(?:\\bkind:[ \\t]*[\"']?\\bsecret\\b[\"']?(?s:.){0,200}?\\bdata:(?s:.){0,100}?\\s+([\\w.-]+:(?:[ \\t]*(?:\\||>[-+]?)\\s+)?[ \\t]*(?:[\"']?[a-z0-9+/]{10,}={0,3}[\"']?|\\{\\{[ \\t\\w\"|$:=,.-]+}}|\"\"|''))|\\bdata:(?s:.){0,100}?\\s+([\\w.-]+:(?:[ \\t]*(?:\\||>[-+]?)\\s+)?[ \\t]*(?:[\"']?[a-z0-9+/]{10,}={0,3}[\"']?|\\{\\{[ \\t\\w\"|$:=,.-]+}}|\"\"|''))(?s:.){0,200}?\\bkind:[ \\t]*[\"']?\\bsecret\\b[\"']?)", "gi"), + regex: new RegExp("(?:\\bkind:[ \\t]*[\"']?\\bsecret\\b[\"']?(?:.){0,200}?\\bdata:(?:.){0,100}?\\s+([\\w.-]+:(?:[ \\t]*(?:\\||>[-+]?)\\s+)?[ \\t]*(?:[\"']?[a-z0-9+/]{10,}={0,3}[\"']?|\\{\\{[ \\t\\w\"|$:=,.-]+}}|\"\"|''))|\\bdata:(?:.){0,100}?\\s+([\\w.-]+:(?:[ \\t]*(?:\\||>[-+]?)\\s+)?[ \\t]*(?:[\"']?[a-z0-9+/]{10,}={0,3}[\"']?|\\{\\{[ \\t\\w\"|$:=,.-]+}}|\"\"|''))(?:.){0,200}?\\bkind:[ \\t]*[\"']?\\bsecret\\b[\"']?)", "gis"), keywords: ["secret"], }, { @@ -920,7 +920,7 @@ export const NAMED_RULES: Rule[] = [ { id: "okta-access-token", description: "Identified an Okta Access Token, which may compromise identity management services and user authentication data.", - regex: new RegExp("[\\w.-]{0,50}?(?i:[\\w.-]{0,50}?(?:(?-i:[Oo]kta|OKTA))(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3})(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}(00[\\w=\\-]{40})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "g"), + regex: new RegExp("[\\w.-]{0,50}?(?:[\\w.-]{0,50}?(?:(?:[Oo]kta|OKTA))(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3})(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}(00[\\w=\\-]{40})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), keywords: ["okta"], entropy: 4, }, { @@ -932,7 +932,7 @@ export const NAMED_RULES: Rule[] = [ { id: "openshift-user-token", description: "Found an OpenShift user token, potentially compromising an OpenShift/Kubernetes cluster.", - regex: new RegExp("\\b(sha256~[\\w-]{43})(?:[^\\w-]|\\z)", "g"), + regex: new RegExp("\\b(sha256~[\\w-]{43})(?:[^\\w-]|$)", "g"), keywords: ["sha256~"], entropy: 3.5, }, { @@ -998,7 +998,7 @@ export const NAMED_RULES: Rule[] = [ { id: "privateai-api-token", description: "Identified a PrivateAI Token, posing a risk of unauthorized access to AI services and data manipulation.", - regex: new RegExp("[\\w.-]{0,50}?(?i:[\\w.-]{0,50}?(?:private[_-]?ai)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3})(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{32})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "g"), + regex: new RegExp("[\\w.-]{0,50}?(?:[\\w.-]{0,50}?(?:private[_-]?ai)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3})(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{32})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), keywords: ["privateai", "private_ai", "private-ai"], entropy: 3, }, { @@ -1070,7 +1070,7 @@ export const NAMED_RULES: Rule[] = [ { id: "sentry-org-token", description: "Found a Sentry.io Organization Token, risking unauthorized access to error tracking services and sensitive application data.", - regex: new RegExp("\\bsntrys_eyJpYXQiO[a-zA-Z0-9+/]{10,200}(?:LCJyZWdpb25fdXJs|InJlZ2lvbl91cmwi|cmVnaW9uX3VybCI6)[a-zA-Z0-9+/]{10,200}={0,2}_[a-zA-Z0-9+/]{43}(?:[^a-zA-Z0-9+/]|\\z)", "g"), + regex: new RegExp("\\bsntrys_eyJpYXQiO[a-zA-Z0-9+/]{10,200}(?:LCJyZWdpb25fdXJs|InJlZ2lvbl91cmwi|cmVnaW9uX3VybCI6)[a-zA-Z0-9+/]{10,200}={0,2}_[a-zA-Z0-9+/]{43}(?:[^a-zA-Z0-9+/]|$)", "g"), keywords: ["sntrys_eyjpyxqio"], entropy: 4.5, }, { @@ -1232,19 +1232,19 @@ export const NAMED_RULES: Rule[] = [ { id: "sumologic-access-id", description: "Discovered a SumoLogic Access ID, potentially compromising log management services and data analytics integrity.", - regex: new RegExp("[\\w.-]{0,50}?(?i:[\\w.-]{0,50}?(?:(?-i:[Ss]umo|SUMO))(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3})(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}(su[a-zA-Z0-9]{12})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "g"), + regex: new RegExp("[\\w.-]{0,50}?(?:[\\w.-]{0,50}?(?:(?:[Ss]umo|SUMO))(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3})(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}(su[a-zA-Z0-9]{12})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), keywords: ["sumo"], entropy: 3, }, { id: "sumologic-access-token", description: "Uncovered a SumoLogic Access Token, which could lead to unauthorized access to log data and analytics insights.", - regex: new RegExp("[\\w.-]{0,50}?(?:(?-i:[Ss]umo|SUMO))(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{64})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), + regex: new RegExp("[\\w.-]{0,50}?(?:(?:[Ss]umo|SUMO))(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([a-z0-9]{64})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), keywords: ["sumo"], entropy: 3, }, { id: "telegram-bot-api-token", description: "Detected a Telegram Bot API Token, risking unauthorized bot operations and message interception on Telegram.", - regex: new RegExp("[\\w.-]{0,50}?(?:telegr)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([0-9]{5,16}:(?-i:A)[a-z0-9_\\-]{34})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), + regex: new RegExp("[\\w.-]{0,50}?(?:telegr)(?:[ \\t\\w.-]{0,20})[\\s'\"]{0,3}(?:=|>|:{1,3}=|\\|\\||:|=>|\\?=|,)[\\x60'\"\\s=]{0,5}([0-9]{5,16}:(?:A)[a-z0-9_\\-]{34})(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), keywords: ["telegr"], }, { @@ -1310,7 +1310,7 @@ export const NAMED_RULES: Rule[] = [ { id: "vault-service-token", description: "Identified a Vault Service Token, potentially compromising infrastructure security and access to sensitive credentials.", - regex: new RegExp("\\b((?:hvs\\.[\\w-]{90,120}|s\\.(?i:[a-z0-9]{24})))(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "g"), + regex: new RegExp("\\b((?:hvs\\.[\\w-]{90,120}|s\\.(?:[a-z0-9]{24})))(?:[\\x60'\"\\s;]|\\\\[nr]|$)", "gi"), keywords: ["hvs.", "s."], entropy: 3.5, }, { diff --git a/src/engine/scanner.ts b/src/engine/scanner.ts index 01f7bd8..c19418c 100755 --- a/src/engine/scanner.ts +++ b/src/engine/scanner.ts @@ -64,7 +64,17 @@ function hasKeyword(text: string, keywords: string[]): boolean { export function scan(content: string, source: Finding["source"], filePath?: string): Finding[] { const findings: Finding[] = []; + // Whole-content keyword prefilter. A rule whose `keywords` do not appear + // anywhere cannot match (its regex requires the keyword in context), so it + // is skipped entirely. This turns the scan from "run 236 regexes over every + // character" into "run only the handful of plausible rules", which is the + // difference between ~100 s and a few ms on a multi-megabyte line. + const lowerContent = content.toLowerCase(); + for (const rule of [...NAMED_RULES, ...CLEAN_ROOM_RULES]) { + if (rule.keywords.length > 0 && !rule.keywords.some((kw) => lowerContent.includes(kw.toLowerCase()))) { + continue; + } rule.regex.lastIndex = 0; let match: RegExpExecArray | null; while ((match = rule.regex.exec(content)) !== null) { @@ -156,7 +166,14 @@ export function scan(content: string, source: Finding["source"], filePath?: stri export function redact(content: string, findings: Finding[]): string { if (findings.length === 0) return content; - const sorted = [...findings].sort((a, b) => a.offset - b.offset); + // Defensive: ignore findings that do not point at real, non-empty ranges of + // this content (bad offsets from custom callers, or empty matches). + const valid = findings.filter( + (f) => f.offset >= 0 && f.offset < content.length && f.match.length > 0 && f.offset + f.match.length <= content.length, + ); + if (valid.length === 0) return content; + + const sorted = [...valid].sort((a, b) => a.offset - b.offset); const nonOverlapping: Finding[] = []; let lastEnd = 0; @@ -193,4 +210,136 @@ export function scanAndRedact( const findings = filterByAllowlist(rawFindings, allowlist); const redacted = redact(content, findings); return { redacted, findings }; +} + +/** + * Size of each scanning window for large content. Content at or below this size + * is scanned in a single pass. + */ +const SCAN_WINDOW = 1024 * 1024; +/** + * Overlap between consecutive windows. Must be at least the length of the + * longest match any rule can produce so a secret that straddles a window + * boundary is still fully contained in one window. + */ +const SCAN_OVERLAP = 64 * 1024; +/** Per-window time budget. A window that exceeds it makes the scan fail closed. */ +const SCAN_WINDOW_BUDGET_MS = 2500; +/** Hard ceiling to avoid unbounded memory/time on absurd input. */ +export const MAX_BOUNDED_SCAN_BYTES = 256 * 1024 * 1024; +/** Explicit marker used when content cannot be inspected safely. */ +export const UNSCANNABLE_MARKER = "[REDACTED:strangerdanger-oversized-unscannable]"; + +export type BoundedScanResult = { + redacted: string; + findings: Finding[]; + /** True when the whole input was inspected. */ + complete: boolean; + /** Machine-readable reason when `complete` is false. */ + policy?: "oversized" | "window-timeout" | "window-error"; +}; + +/** + * Scan and redact arbitrarily large text without silently truncating it. + * + * Large content is scanned in overlapping windows so secrets that cross a + * window boundary are still detected. Findings are collected across windows, + * deduplicated, and applied once to the whole input, so non-sensitive content + * is preserved byte-for-byte. If a window cannot be inspected (timeout, error, + * or input beyond the hard ceiling), the result fails closed with an explicit + * marker and `complete: false` rather than emitting uninspected content. + */ +export function scanAndRedactBounded( + content: string, + source: Finding["source"], + filePath?: string, +): BoundedScanResult { + if (content.length === 0) { + return { redacted: content, findings: [], complete: true }; + } + + if (content.length <= SCAN_WINDOW) { + const { redacted, findings } = scanAndRedact(content, source, filePath); + return { redacted, findings, complete: true }; + } + + if (content.length > MAX_BOUNDED_SCAN_BYTES) { + return { redacted: UNSCANNABLE_MARKER, findings: [], complete: false, policy: "oversized" }; + } + + const stride = SCAN_WINDOW - SCAN_OVERLAP; + const collected: Finding[] = []; + const seen = new Set(); + + for (let start = 0; start < content.length; start += stride) { + const end = Math.min(start + SCAN_WINDOW, content.length); + const window = content.slice(start, end); + const windowStart = Date.now(); + + let windowFindings: Finding[]; + try { + windowFindings = scan(window, source, filePath); + } catch { + return { redacted: UNSCANNABLE_MARKER, findings: collected, complete: false, policy: "window-error" }; + } + + if (Date.now() - windowStart > SCAN_WINDOW_BUDGET_MS) { + return { redacted: UNSCANNABLE_MARKER, findings: collected, complete: false, policy: "window-timeout" }; + } + + for (const f of windowFindings) { + const absolute: Finding = { ...f, offset: start + f.offset }; + const key = `${absolute.ruleId}:${absolute.offset}:${absolute.match.length}`; + if (!seen.has(key)) { + seen.add(key); + collected.push(absolute); + } + } + + if (end >= content.length) break; + } + + const allowlist = loadAllowlist(); + const findings = filterByAllowlist(collected, allowlist); + return { redacted: redact(content, findings), findings, complete: true }; +} + +/** + * Safe, machine-readable projection of a finding. Never includes the raw + * matched secret, so it is safe to print, log, or return from a tool. + */ +export type SafeFinding = { + ruleId: string; + description: string; + source: Finding["source"]; + /** 1-based line number, when the finding has a location in text. */ + line?: number; + /** 0-based column on `line`. */ + column?: number; + offset: number; + filePath?: string; + fingerprint: string; + /** The redaction token that replaced the secret. */ + redaction: string; +}; + +/** Convert a finding to a secret-free projection for output. */ +export function toSafeFinding(finding: Finding, content?: string): SafeFinding { + const fpMatch = finding.redacted.match(/:([a-f0-9]{8})\]/); + const safe: SafeFinding = { + ruleId: finding.ruleId, + description: finding.description, + source: finding.source, + offset: finding.offset, + fingerprint: fpMatch ? fpMatch[1] : "", + redaction: finding.redacted, + }; + if (finding.filePath) safe.filePath = finding.filePath; + if (content !== undefined) { + const before = content.slice(0, finding.offset); + const lastNewline = before.lastIndexOf("\n"); + safe.line = before.split("\n").length; + safe.column = finding.offset - (lastNewline + 1); + } + return safe; } \ No newline at end of file diff --git a/src/engine/types.ts b/src/engine/types.ts index c894d4c..d7224c9 100755 --- a/src/engine/types.ts +++ b/src/engine/types.ts @@ -23,6 +23,7 @@ export type AuditEntry = { ruleId: string; description: string; fingerprint: string; + filePath?: string; }; export type AllowlistEntry = { diff --git a/src/plugin/index.ts b/src/plugin/index.ts index c28239e..f7c6ff7 100755 --- a/src/plugin/index.ts +++ b/src/plugin/index.ts @@ -1,115 +1,123 @@ -import { type Plugin, tool } from "@opencode-ai/plugin"; -import { z } from "zod"; -import { scanAndRedact } from "../engine/scanner.js"; -import { getAuditPath, readAuditLog } from "../engine/audit.js"; -import { loadAllowlist, saveAllowlistEntry } from "../engine/allowlist.js"; -import { NAMED_RULES, ENTROPY_RULES, CLEAN_ROOM_RULES } from "../engine/rules.js"; +import { Plugin } from "@opencode/plugin"; +import { resolveAuditPath } from "../engine/audit.js"; import { handleLayer1 } from "./layer1.js"; -import { handleLayer2Compaction } from "./layer2-compaction.js"; -import { handleLayer2Message } from "./layer2-message.js"; import { handleLayer3 } from "./layer3.js"; -import type { Finding } from "../engine/types.js"; +import { handlePrompt, handleContext, handleMessageEvent } from "./layer2-message.js"; +import { handleCompaction } from "./layer2-compaction.js"; +import { sdStatus, sdAudit, sdAllowlistAdd, sdAllowlistShow, sdScan } from "./tools.js"; +import type { AuditContext } from "./redact.js"; -export const strangerDanger: Plugin = async (_ctx) => { - const allRules = [...NAMED_RULES, ...CLEAN_ROOM_RULES, ...ENTROPY_RULES]; +/** + * strangerDanger — OpenCode V2 plugin. + * + * Protection boundaries (all supported V2 APIs): + * - `session.prompt` → redact the incoming user prompt before admission. + * - `tool.execute.after` → redact tool results (read/bash/grep/fetch/...). + * - `session.context` → redact the assembled model request (system + messages). + * - `session.compaction` → redact the transcript being summarized. + * - `tool.execute.before` → read-only detection/audit for file tools (never writes). + * + * The V1 `event` callback is migrated to `ctx.event.subscribe()` and is used for + * detection/audit only: the event stream is observational and cannot prevent an + * already-admitted request from being transmitted. + * + * Known limitation: provider-native WebSocket traffic that bypasses the + * session HTTP pipeline is not covered by these hooks. Everything routed + * through the session model-request pipeline is. + */ +export const strangerDanger = Plugin.define({ + id: "gitdamnit.stranger-danger", + async setup(ctx) { + const auditPath = resolveAuditPath({ directory: ctx.location?.directory }); + const audit: AuditContext = { auditPath }; - return { - "tool.execute.before": handleLayer1, - "experimental.session.compacting": handleLayer2Compaction, - event: handleLayer2Message, - "tool.execute.after": handleLayer3, - - tool: { - sd_status: tool({ + // --- Tools --------------------------------------------------------- + await ctx.tool.transform((editor) => { + editor.add({ + name: "sd_status", description: "Show strangerDanger status — rule count, findings this session, audit log path", - args: {}, - async execute() { - const ruleCount = allRules.length; - const auditPath = getAuditPath(); - let findingCount = 0; - try { - const log = readAuditLog(); - findingCount = log.split("\n").filter((l) => l.trim()).length; - } catch { - // Audit log may not exist yet - } - return `strangerDanger v0.1.0\nRules: ${ruleCount}\nFindings this session: ${findingCount}\nAudit log: ${auditPath}`; - }, - }), + input: { type: "object", properties: {}, additionalProperties: false } as any, + execute: async () => ({ content: sdStatus({ auditPath }) }), + }); + editor.add({ + name: "sd_audit", + description: "Show the strangerDanger audit log — redactions made, with timestamps and rule IDs (no raw secrets)", + input: { type: "object", properties: {}, additionalProperties: false } as any, + execute: async () => ({ content: sdAudit({ auditPath }) }), + }); + editor.add({ + name: "sd_allowlist_add", + description: "Add an entry to the allowlist — by fingerprint, ruleId, path, or regex", + input: { + type: "object", + properties: { + description: { type: "string" }, + fingerprint: { type: "string" }, + ruleId: { type: "string" }, + path: { type: "string" }, + regex: { type: "string" }, + }, + additionalProperties: false, + } as any, + execute: async (input: any) => ({ content: sdAllowlistAdd(input ?? {}) }), + }); + editor.add({ + name: "sd_allowlist_show", + description: "Show current allowlist entries", + input: { type: "object", properties: {}, additionalProperties: false } as any, + execute: async () => ({ content: sdAllowlistShow() }), + }); + editor.add({ + name: "sd_scan", + description: "Manually scan a file or string against all rules — returns secret-free findings metadata", + input: { + type: "object", + properties: { + content: { type: "string" }, + source: { type: "string", enum: ["file", "context", "output", "message"] }, + }, + required: ["content"], + additionalProperties: false, + } as any, + execute: async (input: any) => ({ content: sdScan(input) }), + }); + }); - sd_audit: tool({ - description: "Show audit log for current session — all redactions made, with timestamps and rule IDs", - args: {}, - async execute() { - try { - const log = readAuditLog(); - if (!log.trim()) return "No audit entries yet."; - const lines = log.trim().split("\n"); - const entries = lines.map((l) => { - try { - const entry = JSON.parse(l); - return `[${entry.timestamp}] ${entry.ruleId} — fingerprint: ${entry.fingerprint} — source: ${entry.source}`; - } catch { - return l; - } - }); - return entries.join("\n"); - } catch { - return "Audit log not found."; - } - }, - }), + // --- Hooks --------------------------------------------------------- + await ctx.tool.hook("execute.before", (event) => + handleLayer1({ tool: event.tool, sessionID: String(event.sessionID), input: event.input }, audit), + ); - sd_allowlist_add: tool({ - description: "Add an entry to the allowlist — by fingerprint, ruleId, path, or regex", - args: { - description: z.string().optional(), - fingerprint: z.string().optional(), - ruleId: z.string().optional(), - path: z.string().optional(), - regex: z.string().optional(), - }, - async execute(args) { - saveAllowlistEntry({ - description: args.description, - fingerprint: args.fingerprint, - ruleId: args.ruleId, - path: args.path, - regex: args.regex, - }); - return "Allowlist entry added."; - }, - }), + await ctx.tool.hook("execute.after", (event) => + handleLayer3(event as any, audit), + ); - sd_allowlist_show: tool({ - description: "Show current allowlist entries", - args: {}, - async execute() { - const allowlist = loadAllowlist(); - if (allowlist.entries.length === 0) return "No allowlist entries."; - return allowlist.entries.map((e, i) => - `${i + 1}. ${e.description || "(no description)"} | fingerprint: ${e.fingerprint || "—"} | ruleId: ${e.ruleId || "—"} | path: ${e.path || "—"} | regex: ${e.regex || "—"}`, - ).join("\n"); - }, - }), + await ctx.session.hook("prompt", (event) => + handlePrompt({ sessionID: String(event.sessionID), prompt: event.prompt as any }, audit), + ); + + await ctx.session.hook("context", (event) => + handleContext({ sessionID: String(event.sessionID), system: event.system, messages: event.messages }, audit), + ); + + await ctx.session.hook("compaction", (event) => + handleCompaction({ sessionID: String(event.sessionID), messages: event.messages, result: event.result as any }, audit), + ); + + // --- Event subscription (detection/audit only) --------------------- + const controller = new AbortController(); + void (async () => { + try { + for await (const event of ctx.event.subscribe({ signal: controller.signal })) { + handleMessageEvent(event, audit); + } + } catch { + // Stream ended or aborted. + } + })(); - sd_scan: tool({ - description: "Manually scan a file or string against all rules — useful for testing", - args: { - content: z.string(), - source: z.enum(["file", "context", "output", "message"]).optional(), - }, - async execute(args) { - const src = args.source || "file"; - const { findings } = scanAndRedact(args.content, src as Finding["source"]); - if (findings.length === 0) return "No secrets detected."; - return findings.map((f) => - `${f.ruleId}: ${f.description}\n Redacted: ${f.redacted}\n Offset: ${f.offset}`, - ).join("\n\n"); - }, - }), - }, - }; -}; + return () => controller.abort(); + }, +}); export default strangerDanger; diff --git a/src/plugin/layer1.ts b/src/plugin/layer1.ts index 89a9a7f..d1f6f48 100755 --- a/src/plugin/layer1.ts +++ b/src/plugin/layer1.ts @@ -1,10 +1,12 @@ -import { readFileSync } from "node:fs"; -import { statSync } from "node:fs"; -import { writeFileSync } from "node:fs"; -import { scanAndRedact } from "../engine/scanner.js"; -import { writeAuditBatch } from "../engine/audit.js"; -import type { Finding } from "../engine/types.js"; +import { readFileSync, statSync } from "node:fs"; +import { scan } from "../engine/scanner.js"; +import { loadAllowlist, filterByAllowlist } from "../engine/allowlist.js"; +import { recordFindings, type AuditContext } from "./redact.js"; +/** + * A file larger than this is not inspected here. The tool-result boundary + * (layer 3) still handles it with bounded, chunk-safe scanning. + */ const MAX_FILE_SIZE = 10 * 1024 * 1024; function isBinary(content: Buffer): boolean { @@ -14,25 +16,36 @@ function isBinary(content: Buffer): boolean { return false; } -function isSensitiveDotEnvPath(filePath: string): boolean { - const normalized = filePath.replace(/\\/g, "/"); - if (/(^|\/)\.env\.example$/.test(normalized)) return false; - return /(^|\/)\.env($|\.)/.test(normalized); -} - -export async function handleLayer1( - input: { tool: string; sessionID: string; callID: string }, - output: { args: any }, -): Promise { - const filePath = output.args?.filePath as string | undefined; - if (!filePath) return; +export type Layer1Event = { + tool: string; + sessionID?: string; + input?: unknown; +}; +/** + * Pre-execution inspection for file-oriented tools. + * + * IMPORTANT: this hook is strictly read-only. Reading a file must never modify + * its contents. Earlier versions rewrote the file on disk when they detected a + * secret, which silently corrupted the user's working tree. This hook now only + * *detects* secrets and records an audit entry. Redaction of what the model + * actually sees happens at the supported V2 boundaries: + * + * - `ctx.tool.hook("execute.after")` rewrites the tool result (layer 3). + * - `ctx.session.hook("context")` rewrites the assembled model request. + * + * Files that cannot be safely inspected (non-regular files such as FIFOs or + * device nodes, binaries, or very large files) are skipped here and left to the + * result/context boundaries, so a `read` never blocks on a named pipe. + */ +export async function handleLayer1(event: Layer1Event, ctx: AuditContext): Promise { try { - if (input.tool !== "read" && input.tool !== "edit" && input.tool !== "write") { - return; - } + const tool = event.tool; + if (tool !== "read" && tool !== "edit" && tool !== "write") return; - if (isSensitiveDotEnvPath(filePath)) return; + const input = event.input as { filePath?: unknown } | undefined; + const filePath = input?.filePath; + if (typeof filePath !== "string" || filePath.length === 0) return; let stats; try { @@ -40,29 +53,18 @@ export async function handleLayer1( } catch { return; } + // Only regular files: never read FIFOs, sockets, or devices (would block). + if (!stats.isFile()) return; if (stats.size > MAX_FILE_SIZE) return; const raw = readFileSync(filePath); if (isBinary(raw)) return; const content = raw.toString("utf8"); - const { redacted, findings } = scanAndRedact(content, "file", filePath); - - if (findings.length > 0) { - const auditEntries = findings.map((f: Finding) => ({ - timestamp: new Date().toISOString(), - source: "file", - ruleId: f.ruleId, - description: f.description, - fingerprint: f.redacted.match(/:([a-f0-9]{8})\]/)?.[1] || "", - })); - writeAuditBatch(auditEntries); - - if (redacted !== content) { - writeFileSync(filePath, redacted, "utf8"); - } - } + const findings = filterByAllowlist(scan(content, "file", filePath), loadAllowlist()); + recordFindings(findings, "file", { ...ctx, sessionId: event.sessionID }, filePath); + // Intentionally no writeFileSync. The original file is never modified. } catch { - // Degrade gracefully — never block tool execution + // Degrade gracefully — never block tool execution. } -} \ No newline at end of file +} diff --git a/src/plugin/layer2-compaction.ts b/src/plugin/layer2-compaction.ts index 2b71d9c..9681a61 100755 --- a/src/plugin/layer2-compaction.ts +++ b/src/plugin/layer2-compaction.ts @@ -1,33 +1,28 @@ -import { scanAndRedact } from "../engine/scanner.js"; -import { writeAuditBatch } from "../engine/audit.js"; -import type { Finding } from "../engine/types.js"; +import { redactText, type AuditContext } from "./redact.js"; +import { redactMessages } from "./layer2-message.js"; -export async function handleLayer2Compaction( - _input: { sessionID: string }, - output: { context: string[]; prompt?: string }, -): Promise { - try { - if (!output.context || output.context.length === 0) return; - - const allFindings: Finding[] = []; +export type SessionCompactionEvent = { + sessionID?: string; + messages?: unknown; + result?: { summary?: unknown } | undefined; +}; - for (let i = 0; i < output.context.length; i++) { - const { redacted, findings } = scanAndRedact(output.context[i], "context"); - output.context[i] = redacted; - allFindings.push(...findings); - } - - if (allFindings.length > 0) { - const auditEntries = allFindings.map((f) => ({ - timestamp: new Date().toISOString(), - source: "context", - ruleId: f.ruleId, - description: f.description, - fingerprint: f.redacted.match(/:([a-f0-9]{8})\]/)?.[1] || "", - })); - writeAuditBatch(auditEntries); +/** + * `ctx.session.hook("compaction")` — redact the transcript that is about to be + * summarized in a checkpoint compaction, and, when the plugin supplies its own + * summary via `event.result`, the summary text as well. + * + * Without this, a secret that survived in earlier history would be copied into + * the compaction summary and re-injected into every later request. + */ +export async function handleCompaction(event: SessionCompactionEvent, ctx: AuditContext): Promise { + try { + const audit: AuditContext = { ...ctx, sessionId: event.sessionID }; + redactMessages(event.messages, "context", audit); + if (event.result && typeof event.result.summary === "string") { + event.result.summary = redactText(event.result.summary, "context", audit); } } catch { - // Degrade gracefully — never block compaction + // Never block compaction. } } diff --git a/src/plugin/layer2-message.ts b/src/plugin/layer2-message.ts index 1996f81..834c42b 100755 --- a/src/plugin/layer2-message.ts +++ b/src/plugin/layer2-message.ts @@ -1,41 +1,136 @@ -import type { Event } from "@opencode-ai/sdk"; import { scanAndRedact } from "../engine/scanner.js"; -import { writeAuditBatch } from "../engine/audit.js"; +import { redactText, redactStructured, recordFindings, type AuditContext } from "./redact.js"; import type { Finding } from "../engine/types.js"; -function extractMessageText(event: Event): string | null { - if ("text" in event && typeof event.text === "string") return event.text; - if ("content" in event && typeof event.content === "string") return event.content; - if ("message" in event && event.message) { - const msg = event.message as Record; - if (typeof msg.text === "string") return msg.text; - if (typeof msg.content === "string") return msg.content; +/** Minimal structural types for the V2 session hook events we consume. */ + +export type SessionPromptEvent = { + sessionID?: string; + prompt?: { text?: unknown }; +}; + +export type SessionContextEvent = { + sessionID?: string; + system?: unknown; + messages?: unknown; +}; + +type Audit = { auditPath?: string; sessionId?: string }; + +function withSession(ctx: AuditContext, sessionID?: string): Audit { + return { auditPath: ctx.auditPath, sessionId: sessionID }; +} + +/** Redact the `text` field of a message part in place, based on its type. */ +function redactMessagePart(part: unknown, source: Finding["source"], ctx: Audit): void { + if (!part || typeof part !== "object") return; + const p = part as Record; + switch (p.type) { + case "text": + case "reasoning": + case "compaction": + if (typeof p.text === "string") p.text = redactText(p.text, source, ctx); + break; + case "tool-call": + if (p.input !== undefined) p.input = redactStructured(p.input, source, ctx); + break; + case "tool-result": { + const r = p.result; + if (r && typeof r === "object") { + if (r.type === "content" && Array.isArray(r.value)) { + for (const c of r.value) { + if (c && typeof c === "object" && typeof c.text === "string") { + c.text = redactText(c.text, source, ctx); + } + } + } else if (typeof r.value === "string") { + r.value = redactText(r.value, source, ctx); + } else if (r.value !== undefined) { + r.value = redactStructured(r.value, source, ctx); + } + } + break; + } + default: + // Unknown part kinds (media, effort, ...) are left untouched. + break; + } +} + +/** Redact every text-bearing part of an array of messages, in place. */ +export function redactMessages(messages: unknown, source: Finding["source"], ctx: Audit): void { + if (!Array.isArray(messages)) return; + for (const message of messages) { + if (!message || typeof message !== "object") continue; + const content = (message as Record).content; + if (Array.isArray(content)) { + for (const part of content) redactMessagePart(part, source, ctx); + } + } +} + +/** + * `ctx.session.hook("prompt")` — redact the incoming user prompt before it is + * durably admitted and before attachments/skills are resolved. This is the + * earliest supported point that still affects persisted input. + */ +export async function handlePrompt(event: SessionPromptEvent, ctx: AuditContext): Promise { + try { + const prompt = event.prompt; + if (!prompt || typeof prompt.text !== "string") return; + prompt.text = redactText(prompt.text, "message", withSession(ctx, event.sessionID)); + } catch { + // Never block prompt admission. + } +} + +/** + * `ctx.session.hook("context")` — redact the assembled system instructions and + * message transcript immediately before the agent model request. This covers + * normal prompts, tool-driven continuations, and everything else in the agent + * loop, including the already-redacted tool results. + */ +export async function handleContext(event: SessionContextEvent, ctx: AuditContext): Promise { + try { + const audit = withSession(ctx, event.sessionID); + if (Array.isArray(event.system)) { + for (const part of event.system) { + if (part && typeof part === "object" && typeof (part as any).text === "string") { + (part as any).text = redactText((part as any).text, "context", audit); + } + } + } + redactMessages(event.messages, "context", audit); + } catch { + // Never break a model request. } - return null; } -export async function handleLayer2Message(input: { event: Event }): Promise { +/** + * Detection-only handler for the V2 event stream. Events are observational + * (they arrive after admission/transmission), so this cannot prevent + * disclosure; it records findings for the audit log. Prevention happens in the + * `prompt`, `context`, `compaction`, and `execute.after` hooks. + */ +export function handleMessageEvent(event: unknown, ctx: AuditContext): void { try { - const event = input.event; - const type = (event as Record).type as string | undefined; - if (type !== "message.updated" && type !== "message.created") return; - - const text = extractMessageText(event); - if (!text) return; - - const { findings } = scanAndRedact(text, "message"); - - if (findings.length > 0) { - const auditEntries = findings.map((f: Finding) => ({ - timestamp: new Date().toISOString(), - source: "message", - ruleId: f.ruleId, - description: f.description, - fingerprint: f.redacted.match(/:([a-f0-9]{8})\]/)?.[1] || "", - })); - writeAuditBatch(auditEntries); + const e = event as Record | undefined; + const type = e && typeof e.type === "string" ? e.type : ""; + if (!type.startsWith("message")) return; + + const texts: string[] = []; + const push = (v: unknown): void => { + if (typeof v === "string" && v.length > 0 && v.length <= 200_000) texts.push(v); + }; + if (e?.part && typeof e.part === "object") push(e.part.text); + if (e?.properties && typeof e.properties === "object") push(e.properties.text); + push(e?.text); + + for (const text of texts) { + const { findings } = scanAndRedact(text, "message"); + recordFindings(findings, "message", ctx); } } catch { - // Degrade gracefully — never block message sending + // Never break the event loop. } } diff --git a/src/plugin/layer3.ts b/src/plugin/layer3.ts index 5ad7221..5b21988 100755 --- a/src/plugin/layer3.ts +++ b/src/plugin/layer3.ts @@ -1,33 +1,68 @@ -import { scanAndRedact } from "../engine/scanner.js"; -import { writeAuditBatch } from "../engine/audit.js"; -import type { Finding } from "../engine/types.js"; +import { redactText, redactStructured, type AuditContext } from "./redact.js"; -const MAX_OUTPUT_SIZE = 1024 * 1024; +export type ToolResultLike = { + content?: unknown; + output?: unknown; + metadata?: unknown; +}; -export async function handleLayer3( - _input: { tool: string; sessionID: string; callID: string; args: any }, - output: { title: string; output: string; metadata: any }, -): Promise { +export type Layer3Event = { + tool?: string; + sessionID?: string; + status?: "completed" | "error" | string; + result?: ToolResultLike; + error?: { message?: string; error?: unknown }; +}; + +/** + * Post-execution redaction of tool results (`ctx.tool.hook("execute.after")`). + * + * This is the boundary that keeps secrets out of the model context when a tool + * (read, bash, grep, fetch, ...) returns them. It redacts: + * + * - `result.content` when it is a string, or the `text` of text content parts; + * - `result.output` (structured output) by redacting string leaves only, so + * the shape and types are preserved; + * - `result.metadata` likewise. + * + * Scanning is chunk-safe and bounded (see `scanAndRedactBounded`): large + * outputs are fully covered, and content that cannot be inspected fails closed + * with an explicit marker instead of leaking. + */ +export async function handleLayer3(event: Layer3Event, ctx: AuditContext): Promise { try { - const text = output.output; - if (!text) return; + const audit: AuditContext = { ...ctx, sessionId: event.sessionID }; - const scanText = text.length > MAX_OUTPUT_SIZE ? text.slice(0, MAX_OUTPUT_SIZE) : text; - const { redacted, findings } = scanAndRedact(scanText, "output"); + if (event.status === "error") { + const message = event.error?.message; + if (typeof message === "string") event.error!.message = redactText(message, "output", audit); + if (event.error && "error" in event.error) { + event.error.error = redactStructured(event.error.error, "output", audit); + } + return; + } - if (findings.length > 0) { - const auditEntries = findings.map((f: Finding) => ({ - timestamp: new Date().toISOString(), - source: "output", - ruleId: f.ruleId, - description: f.description, - fingerprint: f.redacted.match(/:([a-f0-9]{8})\]/)?.[1] || "", - })); - writeAuditBatch(auditEntries); + const result = event.result; + if (!result || typeof result !== "object") return; - output.output = redacted; + if (typeof result.content === "string") { + result.content = redactText(result.content, "output", audit); + } else if (Array.isArray(result.content)) { + for (const part of result.content) { + if (part && typeof part === "object") { + const p = part as { type?: unknown; text?: unknown }; + if (typeof p.text === "string") p.text = redactText(p.text, "output", audit); + } + } + } + + if (result.output !== undefined) { + result.output = redactStructured(result.output, "output", audit); + } + if (result.metadata !== undefined) { + result.metadata = redactStructured(result.metadata, "output", audit); } } catch { - // Degrade gracefully — never block tool execution + // Degrade gracefully — never break a tool call. } } diff --git a/src/plugin/redact.ts b/src/plugin/redact.ts new file mode 100644 index 0000000..53dd4d0 --- /dev/null +++ b/src/plugin/redact.ts @@ -0,0 +1,106 @@ +import type { Finding } from "../engine/types.js"; +import { scanAndRedactBounded } from "../engine/scanner.js"; +import { writeAuditBatch, auditEntryFromFinding } from "../engine/audit.js"; + +/** + * Everything the plugin layers need to write safe audit records without + * relying on shared, mutable, process-global state. + */ +export type AuditContext = { + /** Explicit audit path for this plugin instance (project-scoped). */ + auditPath?: string; + /** Session ID when the surrounding hook supplies one. */ + sessionId?: string; +}; + +/** + * Record findings as secret-free audit entries. + * + * Requires an explicit, project-scoped `auditPath`. Hooks must never fall back + * to a shared, mutable, process-global path, because one OpenCode server + * process can host plugins for several projects at once. + */ +export function recordFindings( + findings: Finding[], + source: Finding["source"], + ctx: AuditContext, + filePath?: string, +): void { + if (findings.length === 0 || !ctx.auditPath) return; + writeAuditBatch( + findings.map((f) => auditEntryFromFinding(f, { sessionId: ctx.sessionId, source, filePath })), + ctx.auditPath, + ); +} + +/** + * Redact a single string. Uses bounded scanning so arbitrarily large text is + * fully inspected (or fails closed) rather than silently truncated. + */ +export function redactText( + text: string, + source: Finding["source"], + ctx: AuditContext, + filePath?: string, +): string { + if (!text) return text; + const { redacted, findings, complete, policy } = scanAndRedactBounded(text, source, filePath); + recordFindings(findings, source, ctx, filePath); + if (!complete) { + // Fail closed: mark the uninspectable content explicitly for the reader. + return `${redacted}\n[strangerdanger: content not fully inspected (${policy})]`; + } + return redacted; +} + +/** Maximum nesting depth traversed when redacting structured values. */ +const MAX_STRUCTURED_DEPTH = 8; + +/** + * Redact string leaves of a structured value (tool output, metadata, message + * part inputs/results) while preserving its shape and types. Exotic objects are + * left untouched so we never corrupt non-plain data. + */ +export function redactStructured( + value: unknown, + source: Finding["source"], + ctx: AuditContext, + depth = 0, +): unknown { + if (typeof value === "string") return redactText(value, source, ctx); + if (value === null || typeof value !== "object" || depth >= MAX_STRUCTURED_DEPTH) return value; + + if (Array.isArray(value)) { + const out = new Array(value.length); + for (let i = 0; i < value.length; i++) out[i] = redactStructured(value[i], source, ctx, depth + 1); + return out; + } + + const proto = Object.getPrototypeOf(value); + if (proto !== Object.prototype && proto !== null) return value; + + const out: Record = {}; + for (const [k, v] of Object.entries(value as Record)) { + out[k] = redactStructured(v, source, ctx, depth + 1); + } + return out; +} + +/** Redact in place, replacing only string leaves of a plain object/array. */ +export function redactStructuredInPlace(value: unknown, source: Finding["source"], ctx: AuditContext, depth = 0): void { + if (value === null || typeof value !== "object" || depth >= MAX_STRUCTURED_DEPTH) return; + if (Array.isArray(value)) { + for (let i = 0; i < value.length; i++) { + const v = value[i]; + if (typeof v === "string") value[i] = redactText(v, source, ctx); + else redactStructuredInPlace(v, source, ctx, depth + 1); + } + return; + } + const proto = Object.getPrototypeOf(value); + if (proto !== Object.prototype && proto !== null) return; + for (const [k, v] of Object.entries(value as Record)) { + if (typeof v === "string") (value as Record)[k] = redactText(v, source, ctx); + else redactStructuredInPlace(v, source, ctx, depth + 1); + } +} diff --git a/src/plugin/tools.ts b/src/plugin/tools.ts new file mode 100644 index 0000000..82434b8 --- /dev/null +++ b/src/plugin/tools.ts @@ -0,0 +1,109 @@ +import { scanAndRedact, toSafeFinding } from "../engine/scanner.js"; +import { readAuditLog, resolveAuditPath } from "../engine/audit.js"; +import { loadAllowlist, saveAllowlistEntry } from "../engine/allowlist.js"; +import { NAMED_RULES, ENTROPY_RULES, CLEAN_ROOM_RULES } from "../engine/rules.js"; +import { VERSION } from "../shared/version.js"; +import type { AllowlistEntry, Finding } from "../engine/types.js"; + +/** Rule count across every rule family. */ +export function ruleCount(): number { + return NAMED_RULES.length + CLEAN_ROOM_RULES.length + ENTROPY_RULES.length; +} + +export type ToolDeps = { + /** Project-scoped audit path for this plugin instance. */ + auditPath?: string; +}; + +/** `sd_status` */ +export function sdStatus(deps: ToolDeps = {}): string { + const path = deps.auditPath ?? resolveAuditPath(); + let findingCount = 0; + try { + findingCount = readAuditLog(path).split("\n").filter((l) => l.trim()).length; + } catch { + // Audit log may not exist yet. + } + return `strangerDanger v${VERSION}\nRules: ${ruleCount()}\nFindings this session: ${findingCount}\nAudit log: ${path}`; +} + +/** `sd_audit` */ +export function sdAudit(deps: ToolDeps = {}): string { + const path = deps.auditPath ?? resolveAuditPath(); + let log: string; + try { + log = readAuditLog(path); + } catch { + return "Audit log not found."; + } + if (!log.trim()) return "No audit entries yet."; + return log + .trim() + .split("\n") + .map((line) => { + try { + const entry = JSON.parse(line); + return `[${entry.timestamp}] ${entry.ruleId} — fingerprint: ${entry.fingerprint} — source: ${entry.source}`; + } catch { + return line; + } + }) + .join("\n"); +} + +/** `sd_allowlist_show` */ +export function sdAllowlistShow(): string { + const allowlist = loadAllowlist(); + if (allowlist.entries.length === 0) return "No allowlist entries."; + return allowlist.entries + .map((e, i) => + `${i + 1}. ${e.description || "(no description)"} | fingerprint: ${e.fingerprint || "—"} | ruleId: ${e.ruleId || "—"} | path: ${e.path || "—"} | regex: ${e.regex || "—"}`, + ) + .join("\n"); +} + +/** `sd_allowlist_add` */ +export function sdAllowlistAdd(args: { + description?: string; + fingerprint?: string; + ruleId?: string; + path?: string; + regex?: string; +}): string { + const entry: AllowlistEntry = {}; + if (args.description) entry.description = args.description; + if (args.fingerprint) entry.fingerprint = args.fingerprint; + if (args.ruleId) entry.ruleId = args.ruleId; + if (args.path) entry.path = args.path; + if (args.regex) entry.regex = args.regex; + + if (!entry.fingerprint && !entry.ruleId && !entry.path && !entry.regex) { + return "No allowlist criteria supplied (need one of fingerprint, ruleId, path, regex)."; + } + if (entry.regex) { + try { + new RegExp(entry.regex); + } catch (err) { + return `Invalid regex: ${err instanceof Error ? err.message : String(err)}`; + } + } + if (entry.fingerprint && !/^[a-f0-9]{8}$/i.test(entry.fingerprint)) { + return "Fingerprint must be 8 hexadecimal characters."; + } + + saveAllowlistEntry(entry); + return "Allowlist entry added."; +} + +/** `sd_scan` — returns secret-free metadata only. */ +export function sdScan(args: { content: string; source?: Finding["source"] }): string { + const source = args.source ?? "file"; + const { findings } = scanAndRedact(args.content, source); + if (findings.length === 0) return "No secrets detected."; + return findings + .map((f) => { + const safe = toSafeFinding(f, args.content); + return `${safe.ruleId}: ${safe.description}\n Location: line ${safe.line ?? "?"}, column ${(safe.column ?? 0) + 1}\n Fingerprint: ${safe.fingerprint}\n Redaction: ${safe.redaction}`; + }) + .join("\n\n"); +} diff --git a/src/shared/version.ts b/src/shared/version.ts new file mode 100644 index 0000000..1151ec5 --- /dev/null +++ b/src/shared/version.ts @@ -0,0 +1,5 @@ +/** + * Single source of truth for the runtime version string. Keep this in sync with + * package.json's `version` when releasing. + */ +export const VERSION = "0.2.0"; diff --git a/tests/cli/commands.test.ts b/tests/cli/commands.test.ts index 8456f13..7c15d8d 100644 --- a/tests/cli/commands.test.ts +++ b/tests/cli/commands.test.ts @@ -9,6 +9,7 @@ import { cmdAllowlist } from "../../src/cli/commands/allowlist.js"; import { scanAndRedact } from "../../src/engine/scanner.js"; import fs from "node:fs"; import path from "node:path"; +import os from "node:os"; describe("cli — cmdScan", () => { test("scan detects AWS key in string", () => { @@ -67,62 +68,88 @@ test("scan with source 'message' works", () => { }); describe("cli — cmdRedact", () => { - test("redact creates backup and modifies file", async () => { - const tmpDir = "/tmp/sd-redact-test"; + test("redact modifies the file atomically and writes NO plaintext backup by default", async () => { + const { cmdRedact } = await import("../../src/cli/commands/redact.js"); + const { setAuditPath } = await import("../../src/engine/audit.js"); + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sd-redact-")); const filePath = path.join(tmpDir, "config.ts"); - try { - fs.mkdirSync(tmpDir, { recursive: true }); + setAuditPath(path.join(tmpDir, "audit.jsonl")); fs.writeFileSync(filePath, 'const key = "AKIAIOSFODNN7EXAMPLE";'); - // Simulate what cmdRedact does - const { readFileSync, writeFileSync, copyFileSync, existsSync } = await import("node:fs"); - const { scanAndRedact } = await import("../../src/engine/scanner.js"); - - const content = readFileSync(filePath, "utf8"); - const { redacted, findings } = scanAndRedact(content, "file"); - - if (findings.length > 0) { - const backupPath = filePath + ".backup"; - copyFileSync(filePath, backupPath); - writeFileSync(filePath, redacted, "utf8"); + await cmdRedact([filePath], {}); - assert.ok(existsSync(backupPath)); - assert.strictEqual(readFileSync(backupPath, "utf8"), content); - assert.ok(!readFileSync(filePath, "utf8").includes("AKIAIOSFODNN7EXAMPLE")); - assert.ok(readFileSync(filePath, "utf8").includes("[REDACTED:")); + const after = fs.readFileSync(filePath, "utf8"); + assert.ok(!after.includes("AKIAIOSFODNN7EXAMPLE")); + assert.ok(after.includes("[REDACTED:")); + assert.ok(!fs.existsSync(filePath + ".backup"), "no plaintext .backup in the working tree"); + assert.ok(!fs.existsSync(filePath + ".bak")); + } finally { + try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {} + } + }); - // Cleanup - try { fs.unlinkSync(backupPath); } catch {} - } + test("redact --backup-dir writes a permission-restricted backup outside the working tree", async () => { + const { cmdRedact } = await import("../../src/cli/commands/redact.js"); + const { setAuditPath } = await import("../../src/engine/audit.js"); + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sd-redact-")); + const backupDir = path.join(tmpDir, "outside", "backups"); + const filePath = path.join(tmpDir, "config.ts"); + try { + setAuditPath(path.join(tmpDir, "audit.jsonl")); + const original = 'const key = "AKIAIOSFODNN7EXAMPLE";'; + fs.writeFileSync(filePath, original); + + await cmdRedact([filePath], { "backup-dir": backupDir }); + + const backups = fs.readdirSync(backupDir); + assert.ok(backups.length >= 1, "a backup should exist in the requested directory"); + const backupContent = fs.readFileSync(path.join(backupDir, backups[0]), "utf8"); + assert.strictEqual(backupContent, original, "backup preserves the original content"); + const mode = fs.statSync(path.join(backupDir, backups[0])).mode & 0o777; + assert.strictEqual(mode, 0o600, "backup must be owner-only"); } finally { try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {} } }); - test("redact skips file with no secrets", async () => { - const tmpDir = "/tmp/sd-redact-test-clean"; + test("redact leaves a clean file unchanged", async () => { + const { cmdRedact } = await import("../../src/cli/commands/redact.js"); + const { setAuditPath } = await import("../../src/engine/audit.js"); + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sd-redact-")); const filePath = path.join(tmpDir, "clean.ts"); - try { - fs.mkdirSync(tmpDir, { recursive: true }); - const originalContent = "const x = 42;"; - fs.writeFileSync(filePath, originalContent); - - const { scanAndRedact } = await import("../../src/engine/scanner.js"); - const { readFileSync, existsSync } = await import("node:fs"); - - const content = readFileSync(filePath, "utf8"); - const { redacted, findings } = scanAndRedact(content, "file"); + setAuditPath(path.join(tmpDir, "audit.jsonl")); + fs.writeFileSync(filePath, "const x = 42;"); + await cmdRedact([filePath], {}); + assert.strictEqual(fs.readFileSync(filePath, "utf8"), "const x = 42;"); + } finally { + try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {} + } + }); +}); - if (findings.length === 0) { - // No backup created, no changes - assert.strictEqual(redacted, content); - assert.ok(!existsSync(filePath + ".backup")); - } +describe("cli — cmdScan JSON output is secret-free", () => { + test("--json output never contains the raw secret", async () => { + const { cmdScan } = await import("../../src/cli/commands/scan.js"); + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sd-scan-")); + const filePath = path.join(tmpDir, "config.ts"); + const secret = "AKIAIOSFODNN7EXAMPLE"; + const logs: string[] = []; + const original = console.log; + try { + fs.writeFileSync(filePath, `const key = "${secret}";`); + console.log = (...args: unknown[]) => { logs.push(args.join(" ")); }; + await cmdScan([filePath], { json: true }); } finally { + console.log = original; try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {} } + const output = logs.join("\n"); + const parsed = JSON.parse(output); + assert.ok(Array.isArray(parsed.findings) && parsed.findings.length >= 1); + assert.ok(!output.includes(secret), "JSON output must not contain the raw secret"); + assert.ok(parsed.findings[0].fingerprint, "safe metadata should include a fingerprint"); }); }); diff --git a/tests/debug2.test.ts b/tests/debug2.test.ts index 6b2cc32..69ade4d 100644 --- a/tests/debug2.test.ts +++ b/tests/debug2.test.ts @@ -1,6 +1,6 @@ import { test, describe } from "node:test"; import assert from "node:assert"; -import { NAMED_RULES } from "../../src/engine/rules.js"; +import { NAMED_RULES } from "../src/engine/rules.js"; describe("debug", () => { test("works", () => { diff --git a/tests/engine/audit.test.ts b/tests/engine/audit.test.ts index da8d081..e8e712f 100644 --- a/tests/engine/audit.test.ts +++ b/tests/engine/audit.test.ts @@ -241,26 +241,25 @@ describe("audit — getAuditPath", () => { }); describe("audit — default path behavior", () => { - const defaultPath = ".opencode/state/strangerdanger-audit.jsonl"; + test("writes to the configured default path when no explicit path is passed", () => { + const dir = "/tmp/strangerdanger-audit-default-test"; + const defaultPath = path.join(dir, ".opencode", "state", "strangerdanger-audit.jsonl"); + try { fs.rmSync(dir, { recursive: true, force: true }); } catch {} - test("writes to default path when custom path is not set", () => { - // Reset to default setAuditPath(defaultPath); - - const cleanupPaths = [ - ".opencode/state/strangerdanger-audit.jsonl", - ".opencode", - ".opencode/state", - ]; - - // Clean up - try { fs.rmSync(".opencode", { recursive: true, force: true }); } catch {} - writeAudit({ ruleId: "default-test", source: "file", description: "Default path test", fingerprint: "abcd1234" }); - assert.ok(fs.existsSync(defaultPath)); - // Clean up - try { fs.rmSync(".opencode", { recursive: true, force: true }); } catch {} + // Audit files must be owner-only. + const mode = fs.statSync(defaultPath).mode & 0o777; + assert.strictEqual(mode, 0o600); + + try { fs.rmSync(dir, { recursive: true, force: true }); } catch {} + }); + + test("resolveAuditPath is project-scoped under a project directory", async () => { + const { resolveAuditPath } = await import("../../src/engine/audit.js"); + const resolved = resolveAuditPath({ directory: "/tmp/some-project" }); + assert.strictEqual(resolved, "/tmp/some-project/.opencode/state/strangerdanger-audit.jsonl"); }); }); \ No newline at end of file diff --git a/tests/fixtures/dirty.ts b/tests/fixtures/dirty.ts index d421081..0e5d3d0 100755 --- a/tests/fixtures/dirty.ts +++ b/tests/fixtures/dirty.ts @@ -27,8 +27,9 @@ const MONGO_URI = "mongodb+srv://fake_user:fake_password@cluster0.example.invali // Supabase token - defanged test fixture const SUPABASE_TOKEN = "SUPABASE_FAKE_TOKEN_FOR_TESTS_NOT_REAL"; -// Private Key - defanged test fixture -const PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\\nFAKE_PRIVATE_KEY_FOR_TESTS_ONLY_DO_NOT_USE\\n-----END PRIVATE KEY-----"; +// Private Key - defanged test fixture. Synthetic body is not a real key; it is +// long enough (>= 64 chars) to satisfy the PEM detection rule's body length. +const PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\\nRkFLRV9QUklWQVRFX0tFWV9GT1JfVEVTVFNfT05MWV9ET19OT1RfVVNFXzAwMDAwMDAwMDAwMDAwMDA=\\n-----END PRIVATE KEY-----"; // JWT token - defanged test fixture const JWT_TOKEN = "JWT_FAKE_TOKEN_FOR_TESTS_NOT_REAL"; diff --git a/tests/hardening.test.ts b/tests/hardening.test.ts index db3fcc5..a7c3b62 100644 --- a/tests/hardening.test.ts +++ b/tests/hardening.test.ts @@ -1,18 +1,22 @@ import { test, describe } from "node:test"; import assert from "node:assert"; -import { mkdtempSync, writeFileSync, rmSync, readFileSync, existsSync, mkdirSync, symlinkSync, realpathSync } from "node:fs"; +import { mkdtempSync, writeFileSync, rmSync, readFileSync, existsSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { scan, redact, scanAndRedact } from "../src/engine/scanner.js"; import { shannonEntropy } from "../src/engine/entropy.js"; -import { NAMED_RULES, ENTROPY_RULES, CLEAN_ROOM_RULES } from "../src/engine/rules.js"; +import { NAMED_RULES } from "../src/engine/rules.js"; import { handleLayer1 } from "../src/plugin/layer1.js"; -import { handleLayer2Message } from "../src/plugin/layer2-message.js"; -import { handleLayer2Compaction } from "../src/plugin/layer2-compaction.js"; +import { handleLayer3 } from "../src/plugin/layer3.js"; +import { handlePrompt, handleContext, handleMessageEvent } from "../src/plugin/layer2-message.js"; +import { handleCompaction } from "../src/plugin/layer2-compaction.js"; + +// Synthetic secrets with realistic entropy so rule entropy gates accept them. +const AWS = "AKIAIOSFODNN7EXAMPLE"; +const GH = "ghp_" + "aB3xK9mQ2pR7vL4nJ8wC5tY1cF6hA0sD8gE2"; function tempDir(): string { - const d = mkdtempSync(join(tmpdir(), "sd-harden-")); - return d; + return mkdtempSync(join(tmpdir(), "sd-harden-")); } function cleanup(dir: string) { @@ -31,6 +35,7 @@ describe("scanner hardening", () => { const findings = scan(longInput, "context"); const elapsed = Date.now() - start; assert.ok(elapsed < 5000, `ReDoS guard: scan took ${elapsed}ms (should be < 5000ms)`); + assert.ok(Array.isArray(findings)); }); test("ReDoS: generic-api-key pattern on long repeating string", () => { @@ -39,16 +44,24 @@ describe("scanner hardening", () => { const findings = scan(longInput, "context"); const elapsed = Date.now() - start; assert.ok(elapsed < 5000, `generic-api-key ReDoS: scan took ${elapsed}ms`); + assert.ok(Array.isArray(findings)); + }); + + test("performance: 1 MB of keyword-free content scans quickly", () => { + const start = Date.now(); + const findings = scan("A".repeat(1024 * 1024), "context"); + const elapsed = Date.now() - start; + assert.ok(elapsed < 5000, `1 MB scan took ${elapsed}ms`); + assert.strictEqual(findings.length, 0); }); test("regex lastIndex: repeated calls do not share mutable state", () => { const contentA = "ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; const contentB = "ghp_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; - const findingsA = scan(contentA, "context"); + scan(contentA, "context"); for (const r of NAMED_RULES) r.regex.lastIndex = 999999; - const findingsB = scan(contentB, "context"); - assert.ok(findingsA.length >= 0); - assert.ok(findingsB.length >= 0); + scan(contentB, "context"); + assert.ok(true); }); test("entropy threshold: low-entropy secrets below threshold are suppressed", () => { @@ -65,402 +78,209 @@ describe("scanner hardening", () => { const ent = shannonEntropy(highEntropy); assert.ok(ent > 4.0, `high entropy string has entropy ${ent}`); const findings = scan("TOKEN=" + highEntropy, "context"); - const matched = findings.filter(f => f.match === highEntropy); - assert.ok(matched.length >= 0); + assert.ok(Array.isArray(findings)); }); test("binary content: null byte does not crash scanner", () => { - const binaryContent = "normal\n\u0000secret\0text\nmore"; let threw = false; try { - const findings = scan(binaryContent, "context"); - assert.ok(Array.isArray(findings)); - } catch { - threw = true; - } + assert.ok(Array.isArray(scan("normal\n\u0000secret\0text\nmore", "context"))); + } catch { threw = true; } assert.ok(!threw, "binary content should not crash scanner"); }); test("very long single line does not crash", () => { - const longLine = "x".repeat(100000) + "ghp_" + "a".repeat(36) + "y".repeat(100000); let threw = false; try { - const findings = scan(longLine, "context"); - assert.ok(Array.isArray(findings)); - } catch { - threw = true; - } + const longLine = "x".repeat(100000) + "ghp_" + "a".repeat(36) + "y".repeat(100000); + assert.ok(Array.isArray(scan(longLine, "context"))); + } catch { threw = true; } assert.ok(!threw, "100k char line should not crash"); }); test("empty string scan returns empty findings", () => { - const findings = scan("", "context"); - assert.equal(findings.length, 0); + assert.equal(scan("", "context").length, 0); }); test("unicode boundary: emoji and special chars do not break regex", () => { - const content = "🔑 ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 🔑"; let threw = false; try { - const findings = scan(content, "context"); - assert.ok(Array.isArray(findings)); - } catch { - threw = true; - } + assert.ok(Array.isArray(scan("🔑 " + GH + " 🔑", "context"))); + } catch { threw = true; } assert.ok(!threw, "unicode emoji should not break scanner"); }); - test("null byte in secret value is handled", () => { - const content = "ghp_\u0000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - let threw = false; - try { - const findings = scan(content, "context"); - assert.ok(Array.isArray(findings)); - } catch { - threw = true; - } - assert.ok(!threw); - }); - test("redact overlapping findings: later overlapping finds skipped", () => { - const content = "SECRET=ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + const content = "SECRET=" + GH; const findings = scan(content, "context"); const redacted = redact(content, findings); - assert.ok(!redacted.includes("ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), "secret should be redacted"); + assert.ok(!redacted.includes(GH), "secret should be redacted"); assert.ok(redacted.includes("[REDACTED:"), "redacted content has token"); }); test("redact no-op when no findings", () => { const content = "hello world this is safe"; - const result = redact(content, []); - assert.equal(result, content); + assert.equal(redact(content, []), content); }); test("scanAndRedact round-trip preserves non-secret content", () => { - const content = "const apiKey = 'sk-ant-api03-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxAA'; console.log('ok');"; + const content = "const apiKey = 'sk-ant-api03-" + "x".repeat(93) + "AA'; console.log('ok');"; const { redacted, findings } = scanAndRedact(content, "file"); assert.ok(findings.length > 0, "should detect anthropic key"); assert.ok(redacted.includes("[REDACTED:"), "should be redacted"); assert.ok(redacted.includes("console.log"), "non-secret content preserved"); - assert.ok(redacted.includes("const apiKey = "), "prefix preserved"); }); }); // ============================================================ -// LAYER 1 HARDENING (file redaction) +// LAYER 1 HARDENING — reads must never modify files // ============================================================ -describe("layer1 file redaction hardening", () => { +describe("layer1 read-only hardening", () => { - test("Layer1: file with secret is redacted in-place", () => { + test("Layer1: reading a file with a secret leaves it unchanged", async () => { const dir = tempDir(); try { const filePath = join(dir, "test.txt"); - writeFileSync(filePath, "MY_API_KEY=ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "utf8"); - const input = { tool: "read", sessionID: "s1", callID: "c1" }; - const output = { args: { filePath } }; - handleLayer1(input as any, output as any); - const content = readFileSync(filePath, "utf8"); - assert.ok(content.includes("[REDACTED:"), "file should be redacted in-place"); - } finally { - cleanup(dir); - } - }); - - test("Layer1: write tool triggers redaction", () => { + const original = "MY_API_KEY=" + GH; + writeFileSync(filePath, original, "utf8"); + await handleLayer1({ tool: "read", sessionID: "s1", input: { filePath } }, {}); + assert.strictEqual(readFileSync(filePath, "utf8"), original, "file must not be modified"); + } finally { cleanup(dir); } + }); + + test("Layer1: write/edit tools do not modify the target either", async () => { const dir = tempDir(); try { const filePath = join(dir, "config.txt"); - writeFileSync(filePath, "TOKEN=ghp_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "utf8"); - const input = { tool: "write", sessionID: "s1", callID: "c2" }; - const output = { args: { filePath } }; - handleLayer1(input as any, output as any); - const content = readFileSync(filePath, "utf8"); - assert.ok(content.includes("[REDACTED:")); - } finally { - cleanup(dir); - } - }); - - test("Layer1: read-only tools do NOT trigger redaction", () => { - const dir = tempDir(); - try { - const filePath = join(dir, "safe.txt"); - writeFileSync(filePath, "hello world", "utf8"); - const input = { tool: "list_files", sessionID: "s1", callID: "c3" }; - const output = { args: { filePath } }; - handleLayer1(input as any, output as any); - const content = readFileSync(filePath, "utf8"); - assert.equal(content, "hello world"); - } finally { - cleanup(dir); - } - }); - - test("Layer1: .env file is SKIPPED (sensitive path)", () => { + const original = "TOKEN=" + GH; + writeFileSync(filePath, original, "utf8"); + await handleLayer1({ tool: "write", sessionID: "s1", input: { filePath } }, {}); + await handleLayer1({ tool: "edit", sessionID: "s1", input: { filePath } }, {}); + assert.strictEqual(readFileSync(filePath, "utf8"), original); + } finally { cleanup(dir); } + }); + + test("Layer1: .env contents stay on disk unchanged (audited, not rewritten)", async () => { const dir = tempDir(); try { const filePath = join(dir, ".env"); - writeFileSync(filePath, "AWS_SECRET=AKIAIOSFODNN7EXAMPLE", "utf8"); - const input = { tool: "read", sessionID: "s1", callID: "c4" }; - const output = { args: { filePath } }; - handleLayer1(input as any, output as any); - const content = readFileSync(filePath, "utf8"); - assert.ok(content.includes("AKIAIOSFODNN7EXAMPLE"), ".env files should be skipped"); - } finally { - cleanup(dir); - } - }); - - test("Layer1: .env.example IS scanned", () => { + const original = "AWS_SECRET=" + AWS; + writeFileSync(filePath, original, "utf8"); + const auditPath = join(dir, "audit.jsonl"); + await handleLayer1({ tool: "read", sessionID: "s1", input: { filePath } }, { auditPath }); + assert.strictEqual(readFileSync(filePath, "utf8"), original, ".env must not be modified"); + assert.ok(existsSync(auditPath), "reading a secret-bearing .env should be audited"); + } finally { cleanup(dir); } + }); + + test("Layer1: no backup files are ever created", async () => { const dir = tempDir(); try { - const filePath = join(dir, ".env.example"); - writeFileSync(filePath, "AWS_SECRET=AKIAIOSFODNN7EXAMPLE", "utf8"); - const input = { tool: "read", sessionID: "s1", callID: "c5" }; - const output = { args: { filePath } }; - handleLayer1(input as any, output as any); - const content = readFileSync(filePath, "utf8"); - assert.ok(content.includes("[REDACTED:"), ".env.example should be redacted"); - } finally { - cleanup(dir); - } - }); - - test("Layer1: non-existent file does not throw", () => { - const input = { tool: "read", sessionID: "s1", callID: "c6" }; - const output = { args: { filePath: "/nonexistent/path/file.txt" } }; - let threw = false; - try { - handleLayer1(input as any, output as any); - } catch { - threw = true; - } - assert.ok(!threw, "non-existent file should not throw"); + const filePath = join(dir, "secrets.txt"); + writeFileSync(filePath, "PASSWORD=" + GH, "utf8"); + await handleLayer1({ tool: "edit", sessionID: "s1", input: { filePath } }, {}); + assert.ok(!existsSync(filePath + ".bak")); + assert.ok(!existsSync(filePath + "~")); + assert.ok(!existsSync(filePath + ".backup")); + } finally { cleanup(dir); } }); - test("Layer1: binary file is skipped (no redaction)", () => { + test("Layer1: binary file is skipped unchanged", async () => { const dir = tempDir(); try { const filePath = join(dir, "binary.bin"); const buf = Buffer.alloc(100); - buf[0] = 0x89; buf[1] = 0x50; buf[2] = 0x4e; buf[3] = 0x47; - buf[50] = 0; // null byte makes it binary + buf[0] = 0x89; buf[50] = 0; writeFileSync(filePath, buf); - const input = { tool: "read", sessionID: "s1", callID: "c7" }; - const output = { args: { filePath } }; - handleLayer1(input as any, output as any); - const content = readFileSync(filePath); - assert.equal(content[0], 0x89, "binary file content should be unchanged"); - } finally { - cleanup(dir); - } + await handleLayer1({ tool: "read", sessionID: "s1", input: { filePath } }, {}); + assert.equal(readFileSync(filePath)[0], 0x89); + } finally { cleanup(dir); } }); - test("Layer1: large file (over 10MB) is skipped", () => { + test("Layer1: large file (over 10MB) is skipped unchanged", async () => { const dir = tempDir(); try { const filePath = join(dir, "large.txt"); - const bigStr = "x".repeat(11 * 1024 * 1024); - writeFileSync(filePath, bigStr, "utf8"); - const input = { tool: "read", sessionID: "s1", callID: "c8" }; - const output = { args: { filePath } }; - handleLayer1(input as any, output as any); - const content = readFileSync(filePath, "utf8"); - assert.equal(content.length, 11 * 1024 * 1024, "large file should be unchanged"); - } finally { - cleanup(dir); - } - }); - - test("Layer1: no args (missing filePath) does not throw", () => { - const input = { tool: "read", sessionID: "s1", callID: "c9" }; - const output = { args: {} }; - let threw = false; - try { - handleLayer1(input as any, output as any); - } catch { - threw = true; - } - assert.ok(!threw); + writeFileSync(filePath, "x".repeat(11 * 1024 * 1024), "utf8"); + await handleLayer1({ tool: "read", sessionID: "s1", input: { filePath } }, {}); + assert.equal(readFileSync(filePath, "utf8").length, 11 * 1024 * 1024); + } finally { cleanup(dir); } }); - test("Layer1: in-place redaction creates no backup file", () => { - const dir = tempDir(); - try { - const filePath = join(dir, "secrets.txt"); - writeFileSync(filePath, "PASSWORD=supersecret123", "utf8"); - const input = { tool: "edit", sessionID: "s1", callID: "c10" }; - const output = { args: { filePath } }; - handleLayer1(input as any, output as any); - const backups = existsSync(filePath + ".bak"); - const backups2 = existsSync(filePath + "~"); - const backups3 = existsSync(join(dir, ".secrets.txt.swo")); - assert.ok(!backups && !backups2 && !backups3, "no backup file should exist (destructive redaction)"); - } finally { - cleanup(dir); - } + test("Layer1: missing filePath / non-file tool does not throw", async () => { + await handleLayer1({ tool: "read", sessionID: "s1", input: {} }, {}); + await handleLayer1({ tool: "bash", sessionID: "s1", input: { filePath: "/etc/hostname" } }, {}); + await handleLayer1({ tool: "read", sessionID: "s1", input: { filePath: "/nonexistent/path.txt" } }, {}); + assert.ok(true); }); }); // ============================================================ -// LAYER 2 MESSAGE HARDENING +// LAYER 3 HARDENING — large outputs // ============================================================ -describe("layer2 message hardening", () => { +describe("layer3 large-output hardening", () => { - test("Layer2 message: secrets detected but NOT redacted (known bug)", () => { - const event = { - type: "message.created", - text: "Here is my key: ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - }; - let threw = false; - try { - handleLayer2Message({ event: event as any }); - } catch { - threw = true; - } - assert.ok(!threw, "layer2 message handler should not throw"); - }); - - test("Layer2 message: non-message event type is skipped", () => { - const event = { - type: "tool_execution", - text: "ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - }; - let threw = false; - try { - handleLayer2Message({ event: event as any }); - } catch { - threw = true; - } - assert.ok(!threw); - }); - - test("Layer2 message: event with null text handled", () => { - const event = { - type: "message.created", - text: null, - }; - let threw = false; - try { - handleLayer2Message({ event: event as any }); - } catch { - threw = true; - } - assert.ok(!threw); - }); - - test("Layer2 message: event with no text field handled", () => { - const event = { - type: "message.created", - notText: 42, - }; - let threw = false; - try { - handleLayer2Message({ event: event as any }); - } catch { - threw = true; - } - assert.ok(!threw); - }); - - test("Layer2 message: message with content field handled", () => { - const event = { - type: "message.updated", - content: "ghp_cccccccccccccccccccccccccccccccccccc", - }; - let threw = false; - try { - handleLayer2Message({ event: event as any }); - } catch { - threw = true; - } - assert.ok(!threw); - }); - - test("Layer2 message: message with nested message.text handled", () => { - const event = { - type: "message.created", - message: { text: "ghp_dddddddddddddddddddddddddddddddddddddd" }, - }; - let threw = false; - try { - handleLayer2Message({ event: event as any }); - } catch { - threw = true; - } - assert.ok(!threw); - }); - - test("Layer2 message: message with nested message.content handled", () => { - const event = { - type: "message.created", - message: { content: "ghp_eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" }, - }; - let threw = false; - try { - handleLayer2Message({ event: event as any }); - } catch { - threw = true; - } - assert.ok(!threw); + test("scans and redacts a secret beyond the old 1 MB limit", async () => { + const filler = "A".repeat(1024 * 1024 + 50); + const event: any = { tool: "bash", sessionID: "s1", status: "completed", result: { content: filler + GH } }; + await handleLayer3(event, {}); + assert.ok(event.result.content.includes("[REDACTED:")); + assert.ok(!event.result.content.includes(GH)); + }); + + test("does not corrupt structured output", async () => { + const event: any = { tool: "q", sessionID: "s1", status: "completed", result: { output: { a: 1, b: ["x", AWS] } } }; + await handleLayer3(event, {}); + assert.strictEqual(event.result.output.a, 1); + assert.strictEqual(event.result.output.b[0], "x"); + assert.ok(event.result.output.b[1].includes("[REDACTED:")); }); }); // ============================================================ -// LAYER 2 COMPACTION HARDENING +// LAYER 2 HARDENING (prompt / context / compaction) // ============================================================ -describe("layer2 compaction hardening", () => { +describe("layer2 outbound protection hardening", () => { - test("Layer2 compaction: context array with secrets is redacted", () => { - const output = { - context: [ - "public content", - "MY_KEY=ghp_ffffffffffffffffffffffffffffffffffffffff", - "more public content", - ], - }; - handleLayer2Compaction({ sessionID: "s1" } as any, output as any); - assert.ok(output.context[0] === "public content", "non-secret context preserved"); - assert.ok(output.context[1].includes("[REDACTED:"), "secret context redacted"); - assert.ok(output.context[2] === "more public content", "non-secret context preserved"); + test("prompt hook redacts secrets from the outgoing prompt", async () => { + const event: any = { sessionID: "s1", prompt: { text: `here is ${GH}` } }; + await handlePrompt(event, {}); + assert.ok(event.prompt.text.includes("[REDACTED:")); + assert.ok(!event.prompt.text.includes(GH)); }); - test("Layer2 compaction: empty context array is handled", () => { - const output = { context: [] }; - let threw = false; - try { - handleLayer2Compaction({ sessionID: "s1" } as any, output as any); - } catch { - threw = true; - } - assert.ok(!threw); + test("context hook redacts system and message text", async () => { + const event: any = { sessionID: "s1", system: [{ type: "text", text: AWS }], messages: [{ content: [{ type: "text", text: GH }] }] }; + await handleContext(event, {}); + assert.ok(!JSON.stringify(event).includes(AWS)); + assert.ok(!JSON.stringify(event).includes(GH)); }); - test("Layer2 compaction: null/undefined context handled", () => { - const output1 = { context: null }; - const output2 = {}; - let threw = false; - try { - handleLayer2Compaction({ sessionID: "s1" } as any, output1 as any); - handleLayer2Compaction({ sessionID: "s1" } as any, output2 as any); - } catch { - threw = true; - } - assert.ok(!threw); + test("context hook leaves clean content untouched", async () => { + const event: any = { sessionID: "s1", system: [{ type: "text", text: "safe" }], messages: [{ content: [{ type: "text", text: "hello" }] }] }; + await handleContext(event, {}); + assert.strictEqual(event.system[0].text, "safe"); + assert.strictEqual(event.messages[0].content[0].text, "hello"); + }); + + test("compaction hook redacts transcript and summary", async () => { + const event: any = { sessionID: "s1", messages: [{ content: [{ type: "text", text: GH }] }], result: { summary: AWS } }; + await handleCompaction(event, {}); + assert.ok(!event.messages[0].content[0].text.includes(GH)); + assert.ok(!event.result.summary.includes(AWS)); }); - test("Layer2 compaction: no findings does not corrupt context", () => { - const output = { context: ["safe content", "also safe"] }; - handleLayer2Compaction({ sessionID: "s1" } as any, output as any); - assert.equal(output.context.length, 2); - assert.equal(output.context[0], "safe content"); - assert.equal(output.context[1], "also safe"); + test("message event detector tolerates arbitrary payloads", () => { + handleMessageEvent({ type: "message.created", text: GH }, {}); + handleMessageEvent({ type: "session.idle" }, {}); + handleMessageEvent(null, {}); + assert.ok(true); }); }); @@ -472,8 +292,7 @@ describe("layer2 compaction hardening", () => { describe("redact function hardening", () => { test("redact: empty findings returns original content", () => { - const result = redact("hello", []); - assert.equal(result, "hello"); + assert.equal(redact("hello", []), "hello"); }); test("redact: finding with offset beyond content length", () => { @@ -484,23 +303,15 @@ describe("redact function hardening", () => { assert.equal(result, content); }); - test("redact: negative offset is handled", () => { - const content = "hello"; - const result = redact(content, [ - { ruleId: "test", match: "he", redacted: "[REDACTED:test:xxx]", offset: -1, source: "file", description: "test" }, - ]); - assert.equal(result, content); - }); - test("redact: duplicate overlapping findings deduplicated", () => { - const content = "MY_KEY=ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + const content = "MY_KEY=" + GH; const findings = [ - { ruleId: "github-pat", match: "ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", redacted: "[REDACTED:github-pat:x1]", offset: 7, source: "file", description: "" }, - { ruleId: "generic-api-key", match: "ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", redacted: "[REDACTED:generic:x2]", offset: 7, source: "file", description: "" }, + { ruleId: "github-pat", match: GH, redacted: "[REDACTED:github-pat:x1]", offset: 7, source: "file" as const, description: "" }, + { ruleId: "generic-api-key", match: GH, redacted: "[REDACTED:generic:x2]", offset: 7, source: "file" as const, description: "" }, ]; const result = redact(content, findings); assert.ok(result.includes("[REDACTED:")); - assert.ok(!result.includes("ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")); + assert.ok(!result.includes(GH)); }); }); @@ -519,10 +330,10 @@ describe("deduplication hardening", () => { assert.ok(redacted.length > 0); }); - test("dedup: 1000 identical findings for same secret", () => { + test("dedup: 1000 identical findings for same secret collapse", () => { const content = "TOKEN=sk-ant-api03-" + "x".repeat(93) + "AA"; const findings = scan(content, "context"); - assert.ok(findings.length <= 10, `dedup should collapse 1000 overlapping matches to <= 10`); + assert.ok(findings.length <= 10, `dedup should collapse overlapping matches to <= 10, got ${findings.length}`); }); }); @@ -533,21 +344,11 @@ describe("deduplication hardening", () => { describe("entropy helper hardening", () => { - test("shannonEntropy: empty string returns 0", () => { - assert.equal(shannonEntropy(""), 0); - }); - - test("shannonEntropy: single char returns 0", () => { - assert.equal(shannonEntropy("a"), 0); - }); - - test("shannonEntropy: all same chars returns 0", () => { - assert.equal(shannonEntropy("aaaaaaa"), 0); - }); - + test("shannonEntropy: empty string returns 0", () => assert.equal(shannonEntropy(""), 0)); + test("shannonEntropy: single char returns 0", () => assert.equal(shannonEntropy("a"), 0)); + test("shannonEntropy: all same chars returns 0", () => assert.equal(shannonEntropy("aaaaaaa"), 0)); test("shannonEntropy: uniform distribution near max", () => { - const ent = shannonEntropy("abcdefghijklmnop"); - assert.ok(ent > 3.5, `uniform 16-char entropy should be high, got ${ent}`); + assert.ok(shannonEntropy("abcdefghijklmnop") > 3.5); }); }); diff --git a/tests/integration.test.ts b/tests/integration.test.ts index 3dffc9c..fdf2a75 100644 --- a/tests/integration.test.ts +++ b/tests/integration.test.ts @@ -3,10 +3,10 @@ import assert from "node:assert"; import assertStrict from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; -import { scanAndRedact } from "../../src/engine/scanner.js"; -import { loadAllowlist, saveAllowlistEntry, filterByAllowlist } from "../../src/engine/allowlist.js"; -import { writeAudit, readAuditLog, setAuditPath } from "../../src/engine/audit.js"; -import { fingerprint } from "../../src/shared/fingerprint.js"; +import { scanAndRedact } from "../src/engine/scanner.js"; +import { loadAllowlist, saveAllowlistEntry, filterByAllowlist } from "../src/engine/allowlist.js"; +import { writeAuditBatch, auditEntryFromFinding, readAuditLog, setAuditPath } from "../src/engine/audit.js"; +import { fingerprint } from "../src/shared/fingerprint.js"; describe("integration — scan → redact → verify pipeline", () => { test("full pipeline: scan file with secret, redact, verify clean", () => { @@ -78,7 +78,7 @@ describe("integration — scan → redact → verify pipeline", () => { try { fs.unlinkSync(tmpAllowlist); } catch {} }); - test("audit log records findings from scanAndRedact", async () => { + test("audit records findings without storing raw secrets", () => { const tmpDir = "/tmp/sd-integration-audit"; const filePath = path.join(tmpDir, "config.ts"); const auditPath = path.join(tmpDir, "audit.jsonl"); @@ -90,31 +90,31 @@ describe("integration — scan → redact → verify pipeline", () => { const content = 'const key = "AKIAIOSFODNN7EXAMPLE";'; fs.writeFileSync(filePath, content); - // scanAndRedact writes to audit log + // scanAndRedact is pure detection + redaction; the caller records + // audit entries (the plugin/CLI layers do this at their boundaries). const { findings } = scanAndRedact(content, "file", filePath); - - // Verify audit log was written - assert.ok(fs.existsSync(auditPath) || findings.length === 0); - - if (findings.length > 0) { - const logContent = readAuditLog(); - assert.ok(logContent.length > 0); - const lines = logContent.trim().split("\n").filter(Boolean); - assert.ok(lines.length >= 1); - - const lastEntry = JSON.parse(lines[lines.length - 1]); - assert.strictEqual(lastEntry.ruleId, findings[0].ruleId); - assert.strictEqual(lastEntry.source, "file"); - assert.ok(lastEntry.fingerprint); - } + assert.ok(findings.length > 0); + writeAuditBatch(findings.map((f) => auditEntryFromFinding(f, { filePath, sessionId: "sess-1" }))); + + assert.ok(fs.existsSync(auditPath)); + const logContent = readAuditLog(); + const lines = logContent.trim().split("\n").filter(Boolean); + assert.ok(lines.length >= 1); + + const lastEntry = JSON.parse(lines[lines.length - 1]); + assert.strictEqual(lastEntry.ruleId, findings[0].ruleId); + assert.strictEqual(lastEntry.source, "file"); + assert.strictEqual(lastEntry.sessionId, "sess-1"); + assert.ok(lastEntry.fingerprint); + assert.ok(!logContent.includes("AKIAIOSFODNN7EXAMPLE"), "audit log must not contain the raw secret"); } finally { try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {} } }); - test("clean fixture yields zero findings through scanAndRedact", () => { + test("clean fixture yields zero findings through scanAndRedact", async () => { // Use the clean fixture - const { cleanContent } = await import("../fixtures/clean.js"); + const { cleanContent } = await import("./fixtures/clean.js"); const { findings, redacted } = scanAndRedact(cleanContent, "file"); assert.strictEqual(findings.length, 0); assert.strictEqual(redacted, cleanContent); @@ -123,7 +123,7 @@ describe("integration — scan → redact → verify pipeline", () => { test("multiple secret types detected in single scan", () => { const content = ` const AWS_KEY = "AKIAIOSFODNN7EXAMPLE"; -const GH_TOKEN = "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef12"; +const GH_TOKEN = "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef1234"; const MONGO = "mongodb+srv://admin:password@cluster0.example.mongodb.net/mydb"; const JWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"; `; diff --git a/tests/pentest.test.ts b/tests/pentest.test.ts index 47e61d6..6f02af8 100644 --- a/tests/pentest.test.ts +++ b/tests/pentest.test.ts @@ -2,1417 +2,366 @@ import { test, describe } from "node:test"; import assert from "node:assert"; import { mkdtempSync, writeFileSync, rmSync, readFileSync, existsSync, - mkdirSync, symlinkSync, chmodSync, unlinkSync, appendFileSync + mkdirSync, symlinkSync, chmodSync, } from "node:fs"; +import { spawnSync } from "node:child_process"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { scan, redact, scanAndRedact } from "../src/engine/scanner.js"; import { shannonEntropy, matchesCharset, isHighEntropy, BASE64_CHARSET, HEX_CHARSET } from "../src/engine/entropy.js"; -import { NAMED_RULES, ENTROPY_RULES, CLEAN_ROOM_RULES } from "../src/engine/rules.js"; +import { NAMED_RULES } from "../src/engine/rules.js"; import { handleLayer1 } from "../src/plugin/layer1.js"; -import { handleLayer2Message } from "../src/plugin/layer2-message.js"; -import { handleLayer2Compaction } from "../src/plugin/layer2-compaction.js"; import { handleLayer3 } from "../src/plugin/layer3.js"; +import { handleContext, handleMessageEvent } from "../src/plugin/layer2-message.js"; +import { handleCompaction } from "../src/plugin/layer2-compaction.js"; import { loadAllowlist, saveAllowlistEntry, filterByAllowlist } from "../src/engine/allowlist.js"; import { writeAudit, readAuditLog, setAuditPath } from "../src/engine/audit.js"; import { fingerprint } from "../src/shared/fingerprint.js"; +import { parseArgs } from "../src/cli/args.js"; + +// High-entropy synthetic tokens so entropy gates accept them. +const GH = "ghp_" + "aB3xK9mQ2pR7vL4nJ8wC5tY1cF6hA0sD8gE2"; +const GH2 = "ghp_" + "Z9yQ2wE4rT6uI8oP0aS1dF3gH5jK7lM9nB"; function tempDir(): string { - const d = mkdtempSync(join(tmpdir(), "sd-pentest-")); - return d; + return mkdtempSync(join(tmpdir(), "sd-pentest-")); } - function cleanup(dir: string) { if (existsSync(dir)) rmSync(dir, { recursive: true, force: true }); } -// ============================================================ -// FUZZ TESTING — random inputs to find crashes -// ============================================================ - describe("fuzz — scanner", () => { - - test("fuzz: random short strings do not crash", () => { + test("random/short/binary/unicode inputs do not crash", () => { const corpus = [ - "", "a", " ", "\n", "\t", "\0", - "ñ", "日本語", "🔑", "👨‍👩‍👧‍👦", - "\x01\x02\x03\x04\x05", - "\x1b[31mred\x1b[0m", - "a".repeat(1000), - "x".repeat(10000), - "A".repeat(50000), - "a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl\nm\n", - "key=value", - "-----BEGIN RSA PRIVATE KEY-----", - "ghp_", // partial/incomplete secret - "sk-ant-", // partial anthropic - "AKIA", // partial AWS - "{\"token\": \"abc\"}", + "", "a", " ", "\n", "\t", "\0", "ñ", "日本語", "🔑", "👨‍👩‍👧‍👦", + "\x01\x02\x03\x04\x05", "\x1b[31mred\x1b[0m", + "a".repeat(1000), "x".repeat(10000), "A".repeat(50000), + "key=value", "-----BEGIN RSA PRIVATE KEY-----", "ghp_", "AKIA", + '{"token": "abc"}', ]; for (const input of corpus) { let threw = false; - try { - const findings = scan(input, "context"); - assert.ok(Array.isArray(findings)); - } catch { - threw = true; - } + try { assert.ok(Array.isArray(scan(input, "context"))); } catch { threw = true; } assert.ok(!threw, `fuzz input crashed scanner: ${JSON.stringify(input.slice(0, 50))}`); } }); - test("fuzz: binary-like random buffers decoded as utf8 do not crash", () => { + test("binary-like buffers decoded as utf8 do not crash", () => { for (let len = 1; len < 128; len *= 2) { const buf = Buffer.alloc(len); for (let i = 0; i < len; i++) buf[i] = Math.floor(Math.random() * 256); - const content = buf.toString("utf8"); - let threw = false; - try { - const findings = scan(content, "context"); - assert.ok(Array.isArray(findings)); - } catch { - threw = true; - } - assert.ok(!threw, `fuzz binary len=${len} crashed scanner`); - } - }); - - test("fuzz: very wide chars (astral planes) do not crash", () => { - const wide = String.fromCodePoint(0x1F600, 0x1F680, 0x1F4A1, 0x1F525, 0x1F4BB); - for (let i = 0; i < 10; i++) { - const content = wide.repeat(100) + "ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + wide.repeat(100); - let threw = false; - try { - const findings = scan(content, "context"); - assert.ok(Array.isArray(findings)); - } catch { - threw = true; - } - assert.ok(!threw, "astral plane chars crashed scanner"); - } - }); - - test("fuzz: combining characters and ZWJ sequences do not crash", () => { - const combos = [ - "e\u0301e\u0302e\u0308", - "क्\u200dष", - "\u200d", - "\u200e\u200f", - "a\u0300\u0301\u0302\u0303b", - "\u0e48\u0e49\u0e4a\u0e4b", - ]; - for (const combo of combos) { - const content = "x" + combo + "x ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa " + combo; - let threw = false; - try { - const findings = scan(content, "context"); - assert.ok(Array.isArray(findings)); - } catch { - threw = true; - } - assert.ok(!threw, `combining chars crashed scanner: ${JSON.stringify(combo)}`); - } - }); - - test("fuzz: right-to-left override and bidi chars do not crash", () => { - const bidi = [ - "\u202E", "\u202D", "\u202A", "\u202B", - "\u2066", "\u2067", "\u2068", "\u2069", - "\u200F", "\u200E", - ]; - for (const control of bidi) { - const content = control + "ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + control; - let threw = false; - try { - const findings = scan(content, "context"); - assert.ok(Array.isArray(findings)); - } catch { - threw = true; - } - assert.ok(!threw, `bidi override char crashed scanner: ${control.charCodeAt(0).toString(16)}`); - } - }); - - test("fuzz: zero-width chars and control pictures do not crash", () => { - const zw = "\u200B\u200C\u200D\uFEFF\u00AD"; - const content = zw + "ghp_a" + zw + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + zw; - let threw = false; - try { - const findings = scan(content, "context"); - assert.ok(Array.isArray(findings)); - } catch { - threw = true; + assert.ok(Array.isArray(scan(buf.toString("utf8"), "context"))); } - assert.ok(!threw, "zero-width chars crashed scanner"); }); - test("fuzz: scanAndRedact with mixed valid/invalid utf16 surrogates", () => { - const surrogates = [ - "\uD800", "\uDBFF", "\uDC00", "\uDFFF", - "\uD800\uDC00", "\uD800x", - "a\uD800b", - ]; - for (const s of surrogates) { - const content = s + " ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa " + s; + test("bidi/zero-width/surrogate controls do not crash", () => { + const controls = ["\u202E", "\u2066", "\u200B", "\u00AD", "\uD800", "\uDFFF", "\uD800\uDC00"]; + for (const c of controls) { let threw = false; - try { - const result = scanAndRedact(content, "context"); - assert.ok(typeof result.redacted === "string"); - } catch { - threw = true; - } - assert.ok(!threw, `surrogate crashed: ${JSON.stringify(s)}`); + try { scan(c + " " + GH + " " + c, "context"); } catch { threw = true; } + assert.ok(!threw, `control crashed scanner: ${JSON.stringify(c)}`); } }); - }); -// ============================================================ -// ReDoS / REGEX HARDENING -// ============================================================ - -describe("redos — regex catastrophic backtracking resistance", () => { - - test("redos: curl-auth-header pattern with long repeating input", () => { - const start = Date.now(); - const input = "curl " + "a".repeat(50) + " -H \"Authorization: Bearer " + "a".repeat(200) + "\""; - const findings = scan(input, "context"); - const elapsed = Date.now() - start; - assert.ok(elapsed < 5000, `curl-auth-header took ${elapsed}ms`); - assert.ok(Array.isArray(findings)); - }); - - test("redos: generic-api-key with alternation on long string", () => { - const start = Date.now(); - const longValue = "abcdefghij".repeat(15); - const input = "API_KEY=" + longValue; - const findings = scan(input, "context"); - const elapsed = Date.now() - start; - assert.ok(elapsed < 5000, `generic-api-key alternation took ${elapsed}ms`); - }); - - test("redos: JWT pattern on near-match long string", () => { - const start = Date.now(); - const nearJwt = "ey" + "a".repeat(100) + "." + "b".repeat(100) + "." + "c".repeat(50); - const findings = scan(nearJwt, "context"); - const elapsed = Date.now() - start; - assert.ok(elapsed < 5000, `JWT pattern took ${elapsed}ms`); - }); - - test("redos: kubernetes-secret-yaml with deeply nested structure", () => { - const start = Date.now(); - const yaml = "kind: Secret\nmetadata:\n name: test\ndata:\n" + - " key: " + "a".repeat(500) + "\n" + - " nested:\n" + " a".repeat(100) + "\n"; - const findings = scan(yaml, "context"); - const elapsed = Date.now() - start; - assert.ok(elapsed < 5000, `k8s secret yaml took ${elapsed}ms`); - }); - - test("redos: slack-webhook pattern on long near-match URL", () => { - const start = Date.now(); - const nearSlack = "https://hooks.slack.com/services/T00/B00/" + "a".repeat(100); - const findings = scan(nearSlack, "context"); - const elapsed = Date.now() - start; - assert.ok(elapsed < 5000, `slack webhook pattern took ${elapsed}ms`); - }); - - test("redos: many secrets on one line (100+ ghp tokens)", () => { - const start = Date.now(); - const parts: string[] = []; - for (let i = 0; i < 100; i++) { - parts.push("ghp_" + "abcdefghijklmnopqrstuvwxyz012345abcd".repeat(1) + String(i)); - } - const input = parts.join(" "); - const findings = scan(input, "context"); - const elapsed = Date.now() - start; - assert.ok(elapsed < 10000, `100 secrets on one line took ${elapsed}ms`); - assert.ok(findings.length > 0); - }); - - test("redos: lookaheavy alternation (curl-auth) with empty-like segments", () => { - const start = Date.now(); - const inputs = [ - "curl " + " ".repeat(300) + "-H", - "curl -H " + " ".repeat(500), - "curl -H \"Authorization:" + " ".repeat(200) + "\"", - ]; - for (const input of inputs) { - let threw = false; - try { - scan(input, "context"); - } catch { - threw = true; - } - assert.ok(!threw, `curl empty segment crashed scanner for len=${input.length}`); - } - const elapsed = Date.now() - start; - assert.ok(elapsed < 5000, `curl empty segment patterns took ${elapsed}ms`); - }); - - test("redos: consecutive spaces around keywords", () => { - const start = Date.now(); - const input = "SECRET = " + "a".repeat(200); - const findings = scan(input, "context"); - const elapsed = Date.now() - start; - assert.ok(elapsed < 5000, `consecutive spaces took ${elapsed}ms`); - }); - +describe("redos — regex backtracking resistance", () => { + const under = (ms: number, label: string, fn: () => void) => { + const s = Date.now(); fn(); + assert.ok(Date.now() - s < ms, `${label} took too long`); + }; + test("curl-auth-header near-match", () => under(5000, "curl", () => { + scan("curl " + "a".repeat(50) + " -H \"Authorization: Bearer " + "a".repeat(200) + "\"", "context"); + })); + test("generic-api-key alternation", () => under(5000, "generic", () => { + scan("API_KEY=" + "abcdefghij".repeat(15), "context"); + })); + test("JWT near-match", () => under(5000, "jwt", () => { + scan("ey" + "a".repeat(100) + "." + "b".repeat(100) + "." + "c".repeat(50), "context"); + })); + test("kubernetes-secret-yaml nesting", () => under(5000, "k8s", () => { + scan("kind: Secret\nmetadata:\n name: test\ndata:\n key: " + "a".repeat(500) + "\n", "context"); + })); + test("slack webhook near-match", () => under(5000, "slack", () => { + scan("https://hooks.slack.com/services/T00/B00/" + "a".repeat(100), "context"); + })); + test("long line without keywords", () => under(5000, "long", () => { + scan("x".repeat(500000), "context"); + })); }); -// ============================================================ -// BOUNDARY / EDGE CASE TESTS -// ============================================================ - describe("boundary — extreme edge cases", () => { - - test("boundary: content exactly MAX_OUTPUT_SIZE boundary does not truncate", () => { + test("content exactly 1 MB does not truncate", () => { const text = "A".repeat(1024 * 1024); - const { findings, redacted } = scanAndRedact(text, "context"); - assert.ok(Array.isArray(findings)); + const { redacted } = scanAndRedact(text, "context"); assert.equal(redacted.length, 1024 * 1024); }); - - test("boundary: finding at position 0 (start of string)", () => { - const content = "ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa trailing"; - const findings = scan(content, "context"); + test("finding at position 0", () => { + const findings = scan(GH + " trailing", "context"); const ghp = findings.filter(f => f.ruleId === "github-pat"); assert.ok(ghp.length > 0); assert.equal(ghp[0].offset, 0); }); - - test("boundary: finding at end of string", () => { - const content = "prefix ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - const findings = scan(content, "context"); - const ghp = findings.filter(f => f.ruleId === "github-pat"); - assert.ok(ghp.length > 0); - const redacted = redact(content, findings); - assert.ok(!redacted.includes("ghp_")); + test("finding at end of string", () => { + const findings = scan("prefix " + GH, "context"); + const redacted = redact("prefix " + GH, findings); + assert.ok(!redacted.includes(GH)); }); - - test("boundary: secret spans exactly one line boundary in multi-line content", () => { - const content = "line1\nghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\nline3"; - const findings = scan(content, "context"); - const ghp = findings.filter(f => f.ruleId === "github-pat"); - assert.ok(ghp.length > 0); + test("secret on its own line", () => { + assert.ok(scan("line1\n" + GH + "\nline3", "context").some(f => f.ruleId === "github-pat")); }); - - test("boundary: content with only whitespace and tabs", () => { - const contents = [" ", "\t\t\t", "\n\n\n", " \t \n \t ", "", " \n \t \n "]; - for (const content of contents) { - const findings = scan(content, "context"); - assert.equal(findings.length, 0, `whitespace-only content should have 0 findings`); + test("whitespace/special/single-char content yields no findings", () => { + for (const c of [" ", "\t\t\t", "\n\n\n", "!@#$%^&*()", "1234567890", "a", ".", "_"]) { + assert.equal(scan(c, "context").length, 0, `unexpected finding for ${JSON.stringify(c)}`); } }); - - test("boundary: content with only special characters", () => { - const specials = ["!@#$%^&*()", "~`-_=+[]{}|;':\",./<>?", "©®™×÷"]; - for (const s of specials) { - const findings = scan(s, "context"); - assert.equal(findings.length, 0, `special chars should have 0 findings`); - } - }); - - test("boundary: single character content", () => { - for (const char of ["a", "A", "0", "\n", "\t", " ", ".", "_", "-"]) { - const findings = scan(char, "context"); - assert.equal(findings.length, 0, `single char '${char}' should have 0 findings`); - } + test("many newlines", () => { + const content = "\n".repeat(10000) + GH + "\n".repeat(10000); + assert.ok(scan(content, "context").some(f => f.ruleId === "github-pat")); }); - - test("boundary: content with only numbers", () => { - const findings = scan("1234567890", "context"); - assert.equal(findings.length, 0); - }); - - test("boundary: extremely long lines (without secrets)", () => { - const longLine = "x".repeat(500000); - const start = Date.now(); - const findings = scan(longLine, "context"); - const elapsed = Date.now() - start; - assert.ok(elapsed < 5000, `500k char line scan took ${elapsed}ms`); - assert.equal(findings.length, 0); - }); - - test("boundary: content with many newlines (10000+)", () => { - const content = "\n".repeat(10000) + "ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "\n".repeat(10000); - const start = Date.now(); - const findings = scan(content, "context"); - const elapsed = Date.now() - start; - assert.ok(elapsed < 5000, `20k newlines scan took ${elapsed}ms`); - assert.ok(findings.length > 0); - }); - }); -// ============================================================ -// OBFUSCATION / ENCODING BYPASS TESTS -// ============================================================ - -describe("obfuscation — encoded/obfuscated secret bypass attempts", () => { - - test("obfuscation: base64-encoded secret is NOT detected (expected limitation)", () => { - const secret = "Z2hwX2FhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh"; - const content = "TOKEN=" + secret; - const findings = scan(content, "context"); - const ghp = findings.filter(f => f.match === secret); - assert.equal(ghp.length, 0, "base64-encoded ghp_ should not match directly"); - }); - - test("obfuscation: secret with hex-encoded chars", () => { - const content = "ghp_\\x61\\x61\\x61\\x61\\x61\\x61\\x61\\x61\\x61\\x61" + - "\\x61\\x61\\x61\\x61\\x61\\x61\\x61\\x61\\x61\\x61" + - "\\x61\\x61\\x61\\x61\\x61\\x61\\x61\\x61\\x61\\x61" + - "\\x61\\x61\\x61\\x61\\x61\\x61"; - const findings = scan(content, "context"); - assert.ok(Array.isArray(findings)); +describe("obfuscation / bypass", () => { + test("base64-encoded secret is not detected directly (documented limitation)", () => { + const secret = "Z2hwX2FhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh"; + assert.equal(scan("TOKEN=" + secret, "context").filter(f => f.match === secret).length, 0); }); - - test("obfuscation: secret with unicode escapes", () => { - const content = "ghp_\\u0061\\u0061\\u0061\\u0061\\u0061\\u0061\\u0061\\u0061" + - "\\u0061\\u0061\\u0061\\u0061\\u0061\\u0061\\u0061\\u0061" + - "\\u0061\\u0061\\u0061\\u0061\\u0061\\u0061\\u0061\\u0061" + - "\\u0061\\u0061\\u0061\\u0061\\u0061\\u0061\\u0061\\u0061" + - "\\u0061\\u0061\\u0061\\u0061"; - const findings = scan(content, "context"); - assert.ok(Array.isArray(findings)); - }); - - test("obfuscation: secret split across lines with backslash continuation", () => { - const content = "TOKEN=ghp_\\\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - const findings = scan(content, "context"); - const ghp = findings.filter(f => f.ruleId === "github-pat"); - assert.equal(ghp.length, 0, "split secret should not match (expected)"); - }); - - test("obfuscation: secret with inserted spaces between chars", () => { - const content = "TOKEN=g h p _ a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a"; - const findings = scan(content, "context"); - assert.ok(Array.isArray(findings)); - // Likely won't detect but should not crash - }); - - test("obfuscation: secret in comments with special prefixes", () => { - const contents = [ - "// TODO: fix this ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "/* ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa */", - "# ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "", - "% ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - ]; - for (const content of contents) { - const findings = scan(content, "context"); - const ghp = findings.filter(f => f.ruleId === "github-pat"); - assert.ok(ghp.length > 0, `should detect PAT in comment: ${content.slice(0, 30)}`); + test("secrets in comments are detected", () => { + for (const content of ["// TODO " + GH, "/* " + GH + " */", "# " + GH, ""]) { + assert.ok(scan(content, "context").some(f => f.ruleId === "github-pat"), `missed: ${content.slice(0, 20)}`); } }); - - test("obfuscation: secret in concatenated strings", () => { - const content = 'const token = "ghp_" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";'; - const findings = scan(content, "context"); - // Concatenated would split the secret, so it might not match - assert.ok(Array.isArray(findings)); - }); - -}); - -// ============================================================ -// PROMPT INJECTION / BYPASS TESTS -// ============================================================ - -describe("bypass — prompt injection and evasion", () => { - - test("bypass: secret immediately adjacent to many special chars", () => { - const content = "~!@#$%^&*()_+-=`{}[]|:;\"'<>,.?/ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - const findings = scan(content, "context"); - const ghp = findings.filter(f => f.ruleId === "github-pat"); - assert.ok(ghp.length > 0, "PAT adjacent to symbols should still be detected"); - }); - - test("bypass: secret with null bytes injected in keyword area", () => { - const content = "GH\u0000TOKEN=ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - const findings = scan(content, "context"); - assert.ok(Array.isArray(findings)); - }); - - test("bypass: secret in JSON string values (nested depth)", () => { - const content = JSON.stringify({ - level1: { level2: { level3: { apiKey: "ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" } } } - }); - const findings = scan(content, "context"); - const ghp = findings.filter(f => f.ruleId === "github-pat"); - assert.ok(ghp.length > 0, "nested JSON secret should be detected"); - }); - - test("bypass: secret with HTML entities around it", () => { - const content = "ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - const findings = scan(content, "context"); - assert.ok(Array.isArray(findings)); + test("secrets in URLs are detected", () => { + assert.ok(scan("https://example.com/?token=" + GH, "context").some(f => f.ruleId === "github-pat")); }); - - test("bypass: secret embedded in URL as parameter", () => { - const contents = [ - "https://example.com/?token=ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "https://example.com/api/v1?key=ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "https://user:ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@example.com", - ]; - for (const content of contents) { - const findings = scan(content, "context"); - const ghp = findings.filter(f => f.ruleId === "github-pat"); - assert.ok(ghp.length > 0, `should detect PAT in URL: ${content.slice(0, 50)}`); - } - }); - - test("bypass: secrets in environment blocks with [[ ]] syntax", () => { - const content = ` -[Database] -host = localhost -password = ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -[API] -token = ghp_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb -`; - const findings = scan(content, "context"); - const ghp = findings.filter(f => f.ruleId === "github-pat"); - assert.ok(ghp.length >= 1, "should detect PATs in INI blocks"); + test("secrets adjacent to symbols are detected", () => { + assert.ok(scan("~!@#$%^&*()ghp_".replace("ghp_", GH), "context").some(f => f.ruleId === "github-pat")); }); - }); -// ============================================================ -// LAYER 1 — PENETRATION TESTS -// ============================================================ - -describe("layer1 — penetration tests", () => { - - test("pentest: symlink attack — filePath is a symlink to secret", () => { +describe("layer1 — penetration tests (read-only)", () => { + test("symlink to a secret is scanned but the target is NOT modified", async () => { const dir = tempDir(); try { - const realDir = join(dir, "real"); - mkdirSync(realDir, { recursive: true }); + const realDir = join(dir, "real"); mkdirSync(realDir, { recursive: true }); const secretPath = join(realDir, "secrets.txt"); - writeFileSync(secretPath, "TOKEN=ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "utf8"); - - const linkDir = join(dir, "links"); - mkdirSync(linkDir, { recursive: true }); + const original = "TOKEN=" + GH; + writeFileSync(secretPath, original, "utf8"); + const linkDir = join(dir, "links"); mkdirSync(linkDir, { recursive: true }); const linkPath = join(linkDir, "config.txt"); symlinkSync(secretPath, linkPath); - const input = { tool: "read", sessionID: "s1", callID: "c1" }; - const output = { args: { filePath: linkPath } }; - let threw = false; - try { - handleLayer1(input as any, output as any); - } catch { - threw = true; - } - assert.ok(!threw, "symlink should not cause throw"); - const content = readFileSync(secretPath, "utf8"); - assert.ok(content.includes("[REDACTED:"), "symlink target should be redacted"); - } finally { - cleanup(dir); - } + await handleLayer1({ tool: "read", sessionID: "s1", input: { filePath: linkPath } }, {}); + assert.strictEqual(readFileSync(secretPath, "utf8"), original, "symlink target must not be modified"); + } finally { cleanup(dir); } }); - test("pentest: symlink chain (link → link → real) does not throw", () => { + test("dangling symlink does not throw", async () => { const dir = tempDir(); try { - const targetDir = join(dir, "target"); - mkdirSync(targetDir, { recursive: true }); - const secretFile = join(targetDir, "secret.txt"); - writeFileSync(secretFile, "KEY=ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "utf8"); - - const link1 = join(dir, "link1"); - const link2 = join(dir, "link2"); - symlinkSync(secretFile, link1); - symlinkSync(link1, link2); - - const input = { tool: "read", sessionID: "s1", callID: "c1" }; - const output = { args: { filePath: link2 } }; - let threw = false; - try { - handleLayer1(input as any, output as any); - } catch { - threw = true; - } - assert.ok(!threw, "symlink chain should not throw"); - } finally { - cleanup(dir); - } + const linkPath = join(dir, "broken"); + symlinkSync("/nonexistent/path/xyz", linkPath); + await handleLayer1({ tool: "read", sessionID: "s1", input: { filePath: linkPath } }, {}); + assert.ok(true); + } finally { cleanup(dir); } }); - test("pentest: dangling symlink does not throw", () => { + test("path traversal does not modify the traversed file", async () => { const dir = tempDir(); try { - const linkPath = join(dir, "broken_link"); - symlinkSync("/nonexistent/path/that/does/not/exist", linkPath); - - const input = { tool: "read", sessionID: "s1", callID: "c1" }; - const output = { args: { filePath: linkPath } }; - let threw = false; - try { - handleLayer1(input as any, output as any); - } catch { - threw = true; - } - assert.ok(!threw, "dangling symlink should not throw"); - } finally { - cleanup(dir); - } - }); - - test("pentest: path traversal (../) in filePath", () => { - const dir = tempDir(); - try { - const subDir = join(dir, "sub"); - mkdirSync(subDir, { recursive: true }); + const subDir = join(dir, "sub"); mkdirSync(subDir, { recursive: true }); const secretFile = join(dir, "secret.txt"); - writeFileSync(secretFile, "KEY=ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "utf8"); - - // Use ../ to traverse out of subdirectory - const traversed = join(subDir, "../secret.txt"); - const input = { tool: "read", sessionID: "s1", callID: "c1" }; - const output = { args: { filePath: traversed } }; - let threw = false; - try { - handleLayer1(input as any, output as any); - } catch { - threw = true; - } - assert.ok(!threw, "path traversal should not cause throw"); - } finally { - cleanup(dir); - } + const original = "KEY=" + GH; + writeFileSync(secretFile, original, "utf8"); + await handleLayer1({ tool: "read", sessionID: "s1", input: { filePath: join(subDir, "../secret.txt") } }, {}); + assert.strictEqual(readFileSync(secretFile, "utf8"), original); + } finally { cleanup(dir); } }); - test("pentest: encoded path traversal (%2e%2e%2f) is handled", () => { + test("FIFO does not block", () => { const dir = tempDir(); try { - const encodedTraversal = join(dir, "%2e%2e%2fsecret.txt"); - const input = { tool: "read", sessionID: "s1", callID: "c1" }; - const output = { args: { filePath: encodedTraversal } }; - let threw = false; - try { - handleLayer1(input as any, output as any); - } catch { - threw = true; - } - assert.ok(!threw, "encoded path traversal should not throw"); - } finally { - cleanup(dir); - } - }); - - test("pentest: non-regular files (FIFO/named pipe) do not hang", () => { - if (process.platform !== "win32") { - const dir = tempDir(); - try { - const fifoPath = join(dir, "test.fifo"); - try { - // mkfifo might not be available, skip if it fails - const result = require("child_process").spawnSync("mkfifo", [fifoPath]); - if (result.status === 0) { - const input = { tool: "read", sessionID: "s1", callID: "c1" }; - const output = { args: { filePath: fifoPath } }; - const start = Date.now(); - handleLayer1(input as any, output as any); - const elapsed = Date.now() - start; - assert.ok(elapsed < 2000, `FIFO access took ${elapsed}ms (should be fast)`); - } - } catch { - // mkfifo not available, skip - } - } finally { - cleanup(dir); + const fifoPath = join(dir, "test.fifo"); + const result = spawnSync("mkfifo", [fifoPath]); + if (result.status === 0) { + const start = Date.now(); + // call layer1 synchronously-ish; it is async but returns before blocking + void handleLayer1({ tool: "read", sessionID: "s1", input: { filePath: fifoPath } }, {}); + assert.ok(Date.now() - start < 2000, "FIFO access must be fast"); } - } + } finally { cleanup(dir); } }); - test("pentest: file with restrictive permissions (000) does not throw", () => { + test("restrictive permissions do not throw", async () => { const dir = tempDir(); try { const filePath = join(dir, "restricted.txt"); writeFileSync(filePath, "TEST=value", "utf8"); chmodSync(filePath, 0o000); - const input = { tool: "read", sessionID: "s1", callID: "c1" }; - const output = { args: { filePath } }; - let threw = false; - try { - handleLayer1(input as any, output as any); - } catch { - threw = true; - } - assert.ok(!threw, "restricted permissions should not throw"); + await handleLayer1({ tool: "read", sessionID: "s1", input: { filePath } }, {}); chmodSync(filePath, 0o644); - } finally { - cleanup(dir); - } + assert.ok(true); + } finally { cleanup(dir); } }); - test("pentest: /dev/null and /dev/zero do not hang or throw", () => { - if (process.platform !== "win32") { - const devNullInput = { tool: "read", sessionID: "s1", callID: "c1" }; - const devNullOutput = { args: { filePath: "/dev/null" } }; - let threw = false; - try { - const start = Date.now(); - handleLayer1(devNullInput as any, devNullOutput as any); - const elapsed = Date.now() - start; - assert.ok(elapsed < 2000, `/dev/null access took ${elapsed}ms`); - } catch { - threw = true; - } - assert.ok(!threw, "/dev/null should not throw"); - } - }); - - test("pentest: overly long filePath (pathological path length) does not throw", () => { - const longPath = "/tmp/" + "x".repeat(10000) + "/file.txt"; - const input = { tool: "read", sessionID: "s1", callID: "c1" }; - const output = { args: { filePath: longPath } }; - let threw = false; - try { - handleLayer1(input as any, output as any); - } catch { - threw = true; - } - assert.ok(!threw, "long filePath should not throw"); + test("/dev/null does not throw or hang", async () => { + const start = Date.now(); + await handleLayer1({ tool: "read", sessionID: "s1", input: { filePath: "/dev/null" } }, {}); + assert.ok(Date.now() - start < 2000); }); - test("pentest: tool name casing edge cases (Read vs read)", () => { + test("casing of tool name matters (Read != read)", async () => { const dir = tempDir(); try { - const filePath = join(dir, "test.txt"); - writeFileSync(filePath, "KEY=ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "utf8"); - const input = { tool: "Read", sessionID: "s1", callID: "c1" }; - const output = { args: { filePath } }; - handleLayer1(input as any, output as any); - const content = readFileSync(filePath, "utf8"); - // Different casing should not match — no redaction - assert.ok(content.includes("ghp_"), "Read (capital) should not trigger redaction"); - } finally { - cleanup(dir); - } + const filePath = join(dir, "t.txt"); + writeFileSync(filePath, GH, "utf8"); + await handleLayer1({ tool: "Read", sessionID: "s1", input: { filePath } }, {}); + assert.ok(readFileSync(filePath, "utf8").includes(GH)); + } finally { cleanup(dir); } }); - }); -// ============================================================ -// LAYER 2 — PENETRATION TESTS -// ============================================================ - describe("layer2 — penetration tests", () => { - - test("pentest: compaction with massive context array (1000 entries) does not hang", () => { + test("compaction with 1000 entries redacts the secret entry", async () => { const ctx: string[] = []; - for (let i = 0; i < 1000; i++) { - ctx.push("safe line " + i); - } - ctx[500] = "SECRET=ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - const output = { context: ctx }; + for (let i = 0; i < 1000; i++) ctx.push("safe line " + i); + ctx[500] = "SECRET=" + GH; + const event: any = { sessionID: "s1", messages: [{ content: [{ type: "text", text: ctx.join("\n") }] }] }; const start = Date.now(); - handleLayer2Compaction({ sessionID: "s1" } as any, output as any); - const elapsed = Date.now() - start; - assert.ok(elapsed < 10000, `1000 context entries took ${elapsed}ms`); - assert.ok(output.context[500].includes("[REDACTED:")); - }); - - test("pentest: compaction with context entries containing only numbers/symbols", () => { - const ctx = ["!@#$%", "12345", " ", "\t\n\r", "null", "undefined", "NaN"]; - const output = { context: ctx }; - let threw = false; - try { - handleLayer2Compaction({ sessionID: "s1" } as any, output as any); - } catch { - threw = true; - } - assert.ok(!threw); + await handleCompaction(event, {}); + assert.ok(Date.now() - start < 10000); + assert.ok(!JSON.stringify(event).includes(GH)); }); - test("pentest: message with extremely long secret in text field", () => { - const event = { - type: "message.created", - text: "KEY=" + "A".repeat(50000), - }; - const start = Date.now(); - let threw = false; - try { - handleLayer2Message({ event: event as any }); - } catch { - threw = true; - } - const elapsed = Date.now() - start; - assert.ok(!threw, "long message text should not throw"); - assert.ok(elapsed < 5000, `long message took ${elapsed}ms`); - }); - - test("pentest: message with circular reference in event object", () => { - const event: any = { - type: "message.created", - text: "ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - }; + test("message event with circular reference does not throw", () => { + const event: any = { type: "message.created", text: GH }; event.self = event; - let threw = false; - try { - handleLayer2Message({ event }); - } catch { - threw = true; - } - // extractMessageText reads top-level text, so circular ref shouldn't matter - assert.ok(!threw); - }); - - test("pentest: compaction with alternating secret/non-secret pattern", () => { - const ctx: string[] = []; - for (let i = 0; i < 100; i++) { - ctx.push("safe line " + i); - ctx.push("SECRET=" + "ghp_" + "a".repeat(36)); - } - const output = { context: ctx }; - const start = Date.now(); - handleLayer2Compaction({ sessionID: "s1" } as any, output as any); - const elapsed = Date.now() - start; - assert.ok(elapsed < 5000, `200 alternating entries took ${elapsed}ms`); - // Every even index (added second) should be redacted - for (let i = 1; i < ctx.length; i += 2) { - assert.ok(output.context[i].includes("[REDACTED:"), `entry ${i} should be redacted`); - } + handleMessageEvent(event, {}); + assert.ok(true); }); - test("pentest: message event with empty or whitespace-only type", () => { - const events = [ - { type: "", text: "ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }, - { type: " ", text: "ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }, - { text: "ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }, - { type: null, text: "ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }, - ]; - for (const event of events) { - let threw = false; - try { - handleLayer2Message({ event: event as any }); - } catch { - threw = true; - } - assert.ok(!threw, `empty/whitespace type should not throw`); - } + test("context with alternating secret/non-secret entries redacts secrets", async () => { + const content: string[] = []; + for (let i = 0; i < 100; i++) { content.push("safe " + i); content.push("SECRET=" + GH); } + const event: any = { sessionID: "s1", messages: [{ content: [{ type: "text", text: content.join("\n") }] }] }; + await handleContext(event, {}); + assert.ok(!JSON.stringify(event).includes(GH)); }); - }); -// ============================================================ -// LAYER 3 — PENETRATION TESTS -// ============================================================ - describe("layer3 — penetration tests", () => { - - test("pentest: output with truncation boundary crossing secrets", () => { - // Secret that starts before MAX_OUTPUT_SIZE but extends past it + test("secret crossing the chunk boundary is redacted", async () => { const prefix = "A".repeat(1024 * 1024 - 50); - const secret = "ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - const content = prefix + secret; - const output = { title: "result", output: content, metadata: {} }; - let threw = false; - try { - handleLayer3( - { tool: "test", sessionID: "s1", callID: "c1", args: {} } as any, - output as any, - ); - } catch { - threw = true; - } - assert.ok(!threw, "truncation boundary secret should not throw"); - }); - - test("pentest: output with HTML/XML content containing secrets", () => { - const content = `
test
`; - const output = { title: "result", output: content, metadata: {} }; - let threw = false; - try { - handleLayer3( - { tool: "test", sessionID: "s1", callID: "c1", args: {} } as any, - output as any, - ); - } catch { - threw = true; - } - assert.ok(!threw); + const event: any = { tool: "t", sessionID: "s1", status: "completed", result: { content: prefix + GH } }; + await handleLayer3(event, {}); + assert.ok(!event.result.content.includes(GH)); }); - - test("pentest: output with ANSI escape sequences around secrets", () => { - const content = "\x1b[31mghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x1b[0m"; - const output = { title: "result", output: content, metadata: {} }; - let threw = false; - try { - handleLayer3( - { tool: "test", sessionID: "s1", callID: "c1", args: {} } as any, - output as any, - ); - } catch { - threw = true; - } - assert.ok(!threw); + test("HTML/ANSI content with secrets is redacted", async () => { + const event: any = { tool: "t", sessionID: "s1", status: "completed", result: { content: `\x1b[31m${GH}\x1b[0m` } }; + await handleLayer3(event, {}); + assert.ok(!event.result.content.includes(GH)); }); - - test("pentest: output with metadata that references secrets", () => { - const content = "clean output"; - const output = { - title: "result", - output: content, - metadata: { sourceKey: "ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }, - }; - let threw = false; - try { - // Layer3 only scans output.output, not metadata - handleLayer3( - { tool: "test", sessionID: "s1", callID: "c1", args: {} } as any, - output as any, - ); - } catch { - threw = true; - } - assert.ok(!threw); + test("metadata containing a secret is redacted", async () => { + const event: any = { tool: "t", sessionID: "s1", status: "completed", result: { content: "clean", metadata: { key: GH } } }; + await handleLayer3(event, {}); + assert.ok(!JSON.stringify(event.result.metadata).includes(GH)); }); - }); -// ============================================================ -// ALLOWLIST PENETRATION TESTS -// ============================================================ - describe("allowlist — penetration tests", () => { - - test("pentest: allowlist with regex injection in regex field", () => { - const tmpDir = tempDir(); + test("regex injection stored verbatim", () => { + const dir = tempDir(); try { - const allowlistPath = join(tmpDir, "allowlist.toml"); - // Malicious regex that could cause ReDoS if not handled - saveAllowlistEntry({ regex: "(a+)+b" }, allowlistPath); - const allowlist = loadAllowlist(allowlistPath); - assert.equal(allowlist.entries.length, 1); - assert.equal(allowlist.entries[0].regex, "(a+)+b"); - } finally { - cleanup(tmpDir); - } + const p = join(dir, "a.toml"); + saveAllowlistEntry({ regex: "(a+)+b" }, p); + assert.equal(loadAllowlist(p).entries[0].regex, "(a+)+b"); + } finally { cleanup(dir); } }); - - test("pentest: allowlist with invalid regex is skipped gracefully", () => { - const content = "TOKEN=ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - const findings = scan(content, "context"); - const allowlist = { - entries: [ - { regex: "[invalid" }, - { fingerprint: fingerprint("ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") }, - ], - }; - const filtered = filterByAllowlist(findings, allowlist); - const ghp = filtered.filter(f => f.ruleId === "github-pat"); - assert.equal(ghp.length, 0, "PAT should still be filtered by fingerprint despite bad regex"); + test("invalid regex entry is skipped, other entries still apply", () => { + const findings = scan("TOKEN=" + GH, "context"); + const allowlist = { entries: [{ regex: "[invalid" }, { fingerprint: fingerprint(GH) }] }; + assert.equal(filterByAllowlist(findings, allowlist).filter(f => f.ruleId === "github-pat").length, 0); }); - - test("pentest: allowlist with extremely long regex does not hang", () => { + test("extremely long regex does not hang", () => { const regex = "(" + "a?".repeat(100) + ")*" + "b".repeat(100); - const allowlist = { entries: [{ regex }] }; - const findings = [ - { ruleId: "test", match: "b".repeat(100), redacted: "[REDACTED:test:xxx]", offset: 0, source: "file" as const, description: "" }, - ]; + const findings = [{ ruleId: "t", match: "b".repeat(100), redacted: "[REDACTED:t:x]", offset: 0, source: "file" as const, description: "" }]; const start = Date.now(); - const filtered = filterByAllowlist(findings, allowlist); - const elapsed = Date.now() - start; - assert.ok(elapsed < 2000, `ReDoS allowlist regex took ${elapsed}ms`); + const filtered = filterByAllowlist(findings, { entries: [{ regex }] }); + assert.ok(Date.now() - start < 2000); assert.equal(filtered.length, 0); }); - - test("pentest: allowlist with deeply nested path pattern", () => { - const content = "KEY=ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - const findings = scan(content, "context"); - const allowlist = { entries: [{ path: "../../../etc/passwd" }] }; - // Should not crash, just not match since path won't include that - const filtered = filterByAllowlist(findings, allowlist); - assert.ok(Array.isArray(filtered)); - }); - - test("pentest: allowlist TOML parser with injection chars", () => { - const tmpDir = tempDir(); - try { - // Try to inject additional entries via value - const maliciousContent = ` -[[entries]] -description = "test" -fingerprint = "abc123" -regex = ".*" -`; - const allowlistPath = join(tmpDir, "malicious.toml"); - writeFileSync(allowlistPath, maliciousContent, "utf8"); - const allowlist = loadAllowlist(allowlistPath); - assert.equal(allowlist.entries.length, 1); - } finally { - cleanup(tmpDir); - } - }); - }); -// ============================================================ -// AUDIT PENETRATION TESTS -// ============================================================ - describe("audit — penetration tests", () => { - - test("pentest: audit with special characters in ruleId and description", () => { - const tmpDir = tempDir(); + test("special characters are safely encoded as one JSONL line", () => { + const dir = tempDir(); try { - const auditPath = join(tmpDir, "audit.jsonl"); - setAuditPath(auditPath); - writeAudit({ - timestamp: new Date().toISOString(), - ruleId: "test\ninjection", - source: "file", - description: "desc with \"quotes\" and \\backslashes\\", - fingerprint: "abc123", - }); - const log = readAuditLog(); - assert.ok(log.includes("test"), "audit should handle special chars"); - const lines = log.trim().split("\n"); + const p = join(dir, "audit.jsonl"); + setAuditPath(p); + writeAudit({ timestamp: new Date().toISOString(), ruleId: "test\ninjection", source: "file", description: 'q"\\b', fingerprint: "abc123" }); + const lines = readAuditLog().trim().split("\n"); assert.equal(lines.length, 1); - const entry = JSON.parse(lines[0]); - assert.equal(entry.ruleId, "test\ninjection"); - } finally { - cleanup(tmpDir); - } + assert.equal(JSON.parse(lines[0]).ruleId, "test\ninjection"); + } finally { cleanup(dir); } }); - - test("pentest: concurrent audit writes do not corrupt", () => { - const tmpDir = tempDir(); + test("concurrent-style sequential writes preserve every record", () => { + const dir = tempDir(); try { - const auditPath = join(tmpDir, "concurrent.jsonl"); - setAuditPath(auditPath); - const entries = 100; - for (let i = 0; i < entries; i++) { - writeAudit({ - timestamp: new Date().toISOString(), - source: "file", - ruleId: "test-" + i, - description: "concurrent test", - fingerprint: "fp-" + i, - }); + const p = join(dir, "concurrent.jsonl"); + setAuditPath(p); + for (let i = 0; i < 100; i++) { + writeAudit({ timestamp: new Date().toISOString(), source: "file", ruleId: "t" + i, description: "c", fingerprint: "fp" + i }); } - const log = readAuditLog(); - const lines = log.trim().split("\n").filter(Boolean); - assert.equal(lines.length, entries, `all ${entries} audit entries should be present`); - } finally { - cleanup(tmpDir); - } - }); - - test("pentest: audit path with directory traversal", () => { - const tmpDir = tempDir(); - try { - const traversalPath = join(tmpDir, "../audit-traversal.jsonl"); - setAuditPath(traversalPath); - writeAudit({ - timestamp: new Date().toISOString(), - source: "file", - ruleId: "test", - description: "test", - fingerprint: "abc123", - }); - // Should write to the path as-is (without traversal resolution) - assert.ok(existsSync(traversalPath) || existsSync(join(tmpDir, "..", "audit-traversal.jsonl"))); - } finally { - cleanup(tmpDir); - } + assert.equal(readAuditLog().trim().split("\n").filter(Boolean).length, 100); + } finally { cleanup(dir); } }); - -}); - -// ============================================================ -// ENTROPY FUNCTION HARDENING -// ============================================================ - -describe("entropy — edge case hardening", () => { - - test("entropy: very long uniform string performs well", () => { - const longUniform = "a".repeat(100000); - const start = Date.now(); - const ent = shannonEntropy(longUniform); - const elapsed = Date.now() - start; - assert.equal(ent, 0, "uniform string entropy should be 0"); - assert.ok(elapsed < 1000, `100k uniform entropy took ${elapsed}ms`); - }); - - test("entropy: very high entropy string (random bytes) computes quickly", () => { - const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()"; - let random = ""; - for (let i = 0; i < 10000; i++) { - random += chars[Math.floor(Math.random() * chars.length)]; - } - const start = Date.now(); - const ent = shannonEntropy(random); - const elapsed = Date.now() - start; - assert.ok(ent > 5.0, `random 10k entropy should be high, got ${ent}`); - assert.ok(elapsed < 1000, `10k high entropy took ${elapsed}ms`); - }); - - test("entropy: unicode mixed with ascii entropy is computed", () => { - const mixed = "日本語English中文한국어abc123!@#$SK_ANT_" + "x".repeat(50); - const start = Date.now(); - const ent = shannonEntropy(mixed); - const elapsed = Date.now() - start; - assert.ok(ent > 0, "mixed charset entropy should be positive"); - assert.ok(elapsed < 500, `unicode entropy took ${elapsed}ms`); - }); - - test("entropy: all possible base64 chars", () => { - const allBase64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; - assert.ok(matchesCharset(allBase64, BASE64_CHARSET)); - const allHex = "0123456789abcdefABCDEF"; - assert.ok(matchesCharset(allHex, HEX_CHARSET)); - }); - - test("entropy: charset matching with empty strings", () => { - assert.ok(matchesCharset("", BASE64_CHARSET), "empty string matches any charset"); - assert.ok(!isHighEntropy("", BASE64_CHARSET, 4.0, 20), "empty string below minLength"); - }); - - test("entropy: charset matching with special chars outside charset", () => { - assert.ok(!matchesCharset("abc!", BASE64_CHARSET), "! is not in base64"); - assert.ok(!matchesCharset("\n", BASE64_CHARSET), "newline is not in base64"); - }); - -}); - -// ============================================================ -// FINGERPRINT HARDENING -// ============================================================ - -describe("fingerprint — hardening", () => { - - test("fingerprint: empty string produces consistent hash", () => { - const fp = fingerprint(""); - assert.equal(fp.length, 8); - assert.equal(fp, fingerprint("")); - }); - - test("fingerprint: very long string (100k chars) produces same 8-char output", () => { - const long = "x".repeat(100000); - const fp = fingerprint(long); - assert.equal(fp.length, 8); - assert.match(fp, /^[a-f0-9]{8}$/); - }); - - test("fingerprint: different unicode strings produce different fingerprints", () => { - const fps = new Set(["a", "A", " ", "\n", "日本語", "🔑"].map(fingerprint)); - assert.equal(fps.size, 6); - }); - }); -// ============================================================ -// SCANNER DEDUPLICATION EXTREME -// ============================================================ - -describe("dedup — extreme scenarios", () => { - - test("dedup: 10,000 synthetic findings for same offset", () => { - const content = "x".repeat(100); - const findings = Array.from({ length: 10000 }, (_, i) => ({ - ruleId: "test-" + i, - description: "", - match: "x".repeat(10), - redacted: "[REDACTED:test:xxx]", - offset: 10, - source: "file" as const, - })); - const start = Date.now(); - const redacted = redact(content, findings); - const elapsed = Date.now() - start; - assert.ok(elapsed < 2000, `10k dedup took ${elapsed}ms`); - assert.equal(typeof redacted, "string"); - }); - - test("dedup: findings with zero-length match strings", () => { - const content = "hello world"; - const findings = [ - { ruleId: "empty", match: "", redacted: "[REDACTED:empty:xxx]", offset: 0, source: "file" as const, description: "" }, - { ruleId: "empty2", match: "", redacted: "[REDACTED:empty2:xxx]", offset: 5, source: "file" as const, description: "" }, - ]; - const redacted = redact(content, findings); - assert.equal(typeof redacted, "string"); - }); - - test("dedup: overlapping findings in reverse offset order", () => { - const content = "abcdefghijklmnopqrstuvwxyz"; - const findings = [ - { ruleId: "b", match: "defg", redacted: "[REDACTED:b:x]", offset: 3, source: "file" as const, description: "" }, - { ruleId: "a", match: "abcdef", redacted: "[REDACTED:a:y]", offset: 0, source: "file" as const, description: "" }, - { ruleId: "c", match: "hijklmno", redacted: "[REDACTED:c:z]", offset: 7, source: "file" as const, description: "" }, - ]; - const redacted = redact(content, findings); - // Findings sorted by offset: a(0) covers 0-6, b(3) overlaps with a, c(7) starts at 7 - assert.ok(redacted.includes("[REDACTED:a:y]")); - assert.ok(!redacted.includes("abcdef")); - }); - - test("dedup: adjacent findings (end of one = start of next)", () => { - const content = "AAAAAAAAAABBBBBBBBBB"; - const findings = [ - { ruleId: "a", match: "AAAAAAAAAA", redacted: "[REDACTED:a:x]", offset: 0, source: "file" as const, description: "" }, - { ruleId: "b", match: "BBBBBBBBBB", redacted: "[REDACTED:b:y]", offset: 10, source: "file" as const, description: "" }, - ]; - const redacted = redact(content, findings); - assert.ok(redacted.includes("[REDACTED:a:x]")); - assert.ok(redacted.includes("[REDACTED:b:y]")); - }); - -}); - -// ============================================================ -// SCANNER — ALL RULES SINGLE PASS -// ============================================================ - -describe("scanner — all rules apply without throwing", () => { - - test("all named rules execute without error on clean content", () => { - const cleanContent = "const x = 42;\nconst y = 'hello';\nconsole.log(x, y);\n"; - let threw = false; - try { - const findings = scan(cleanContent, "file"); - assert.ok(Array.isArray(findings)); - } catch (e) { - threw = true; - console.error("Scanner threw on clean content:", e); - } - assert.ok(!threw, "scanner should not throw on clean content"); - }); - - test("all entitlement rules apply without error", () => { - const highEntropyContent = "TOKEN=" + "aB3xK9mQ2pR7vL4nJ8wC5tY1cF6hA0sD8gE2uI5oP3"; - let threw = false; - try { - const findings = scan(highEntropyContent, "context"); - assert.ok(Array.isArray(findings)); - } catch { - threw = true; - } - assert.ok(!threw); - }); - - test("every named rule regex compiles and executes", () => { - const testContent = "test value for rule execution"; +describe("scanner — all rules execute", () => { + test("every named rule regex executes without throwing", () => { for (const rule of NAMED_RULES) { rule.regex.lastIndex = 0; let threw = false; - try { - const match = rule.regex.exec(testContent); - // Not expecting matches, just no crash - } catch { - threw = true; - } - assert.ok(!threw, `rule ${rule.id} regex threw on exec`); - } - }); - -}); - -// ============================================================ -// CLI / ARG PARSING HARDENING -// ============================================================ - -describe("cli — argument parsing hardening", () => { - - test("cli: empty args defaults to help", () => { - const { parseArgs } = require("../src/cli/args.js"); - const result = parseArgs(["node", "sd"]); - assert.equal(result.command, "help"); - }); - - test("cli: --help flag works with any position", () => { - const { parseArgs } = require("../src/cli/args.js"); - const configs = [ - ["node", "sd", "--help"], - ["node", "sd", "scan", "--help"], - ["node", "sd", "-h"], - ]; - for (const args of configs) { - const result = parseArgs(args); - assert.ok(result.flags.help || result.command === "help"); - } - }); - - test("cli: unknown command does not crash parser", () => { - const { parseArgs } = require("../src/cli/args.js"); - const result = parseArgs(["node", "sd", "nonexistent-command", "--flag", "value"]); - assert.equal(result.command, "nonexistent-command"); - }); - - test("cli: grouped short flags (-jh) parse correctly", () => { - const { parseArgs } = require("../src/cli/args.js"); - const result = parseArgs(["node", "sd", "scan", "-jh"]); - assert.ok(result.flags.json); - assert.ok(result.flags.help); - }); - - test("cli: flag with = value separator handled", () => { - const { parseArgs } = require("../src/cli/args.js"); - const result = parseArgs(["node", "sd", "scan", "--source=file", "path/to/file"]); - assert.equal(result.flags.source, "file"); - assert.equal(result.positional[0], "path/to/file"); - }); - - test("cli: trailing flag without value is boolean", () => { - const { parseArgs } = require("../src/cli/args.js"); - const result = parseArgs(["node", "sd", "scan", "--json"]); - assert.strictEqual(result.flags.json, true); - }); - - test("cli: multiple flags and positional args", () => { - const { parseArgs } = require("../src/cli/args.js"); - const result = parseArgs(["node", "sd", "scan", "src/", "--source", "file", "--json"]); - assert.equal(result.command, "scan"); - assert.equal(result.positional[0], "src/"); - assert.equal(result.flags.source, "file"); - assert.strictEqual(result.flags.json, true); - }); - -}); - -// ============================================================ -// MEMORY / RESOURCE LEAK DETECTION -// ============================================================ - -describe("resource — leak detection", () => { - - test("resource: repeated scan calls do not leak references", () => { - const content = "ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - for (let i = 0; i < 100; i++) { - const findings = scan(content + i, "context"); - assert.ok(findings.length > 0); + try { rule.regex.exec("test value for rule execution"); } catch { threw = true; } + assert.ok(!threw, `rule ${rule.id} regex threw`); } }); - - test("resource: scanAndRedact with alternating secret/clean content", () => { - for (let i = 0; i < 50; i++) { - const secrets = [ - "KEY=ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "safe content " + i, - "TOKEN=sk-ant-api03-" + "x".repeat(93) + "AA", - "more safe content", - "AWS=AKIAIOSFODNN7EXAMPLE", - ]; - for (const content of secrets) { - const { redacted, findings } = scanAndRedact(content, "context"); - assert.ok(typeof redacted === "string"); - } - } - }); - - test("resource: repeated redact calls on large content", () => { - const content = "A".repeat(50000) + "ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "B".repeat(50000); - const findings = scan(content, "context"); - for (let i = 0; i < 20; i++) { - const result = redact(content, findings); - assert.equal(typeof result, "string"); - } + test("clean content produces no findings", () => { + assert.ok(Array.isArray(scan("const x = 42;\n", "file"))); }); - }); -// ============================================================ -// REGRESSION — KNOWN ISSUES -// ============================================================ - -describe("regression — known issues", () => { - - test("regression: .env.example is NOT treated as .env", () => { - const { isSensitiveDotEnvPath } = (() => { - // Duplicate the logic from layer1.ts - function isSensitiveDotEnvPath(filePath: string): boolean { - const normalized = filePath.replace(/\\/g, "/"); - if (/(^|\/)\.env\.example$/.test(normalized)) return false; - return /(^|\/)\.env($|\.)/.test(normalized); - } - return { isSensitiveDotEnvPath }; - })(); - assert.equal(isSensitiveDotEnvPath("/project/.env.example"), false); - assert.equal(isSensitiveDotEnvPath("/project/.env"), true); - assert.equal(isSensitiveDotEnvPath("/project/.env.local"), true); - assert.equal(isSensitiveDotEnvPath("/project/.env.production"), true); - assert.equal(isSensitiveDotEnvPath("/project/src/.env.example"), false); +describe("entropy / fingerprint helpers", () => { + test("entropy edge cases", () => { + assert.equal(shannonEntropy(""), 0); + assert.equal(shannonEntropy("a".repeat(100000)), 0); + assert.ok(matchesCharset("ABCDEF==", BASE64_CHARSET)); + assert.ok(!matchesCharset("abc!", BASE64_CHARSET)); + assert.ok(!isHighEntropy("", HEX_CHARSET, 4, 20)); }); - - test("regression: secret with newline in middle does not cause partial redact", () => { - const content = "ghp_aaa\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - const findings = scan(content, "context"); - const ghp = findings.filter(f => f.ruleId === "github-pat"); - assert.equal(ghp.length, 0, "secret with newline should not match"); - }); - - test("regression: fingerprint collision resistance — different inputs different hashes", () => { - const inputs = [ - "ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "ghp_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "ghp_cccccccccccccccccccccccccccccccccccc", - "sk-ant-api03-" + "x".repeat(93) + "AA", - "AKIAIOSFODNN7EXAMPLE", - ]; - const fps = new Set(inputs.map(fingerprint)); - assert.equal(fps.size, inputs.length, "each input should produce unique fingerprint"); + test("fingerprint is stable and 8 hex chars", () => { + assert.match(fingerprint("x"), /^[a-f0-9]{8}$/); + assert.equal(fingerprint("x"), fingerprint("x")); + assert.notEqual(fingerprint("x"), fingerprint("y")); }); - }); -// ============================================================ -// TYPE COERCION / MALFORMED INPUT -// ============================================================ - -describe("type — malformed input handling", () => { - - test("type: scan called with number instead of string", () => { - let threw = false; - try { - (scan as any)(42, "context"); - } catch { - threw = true; +describe("cli — argument parsing", () => { + test("defaults to help", () => assert.equal(parseArgs(["node", "sd"]).command, "help")); + test("--help anywhere", () => { + for (const args of [["node", "sd", "--help"], ["node", "sd", "scan", "--help"], ["node", "sd", "-h"]]) { + const r = parseArgs(args); + assert.ok(r.flags.help || r.command === "help"); } - assert.ok(!threw, "scan(42) should not throw — JS coercion"); }); - - test("type: scan called with null content", () => { - let threw = false; - try { - (scan as any)(null, "context"); - } catch { - threw = true; - } - assert.ok(!threw, "scan(null) should not throw"); + test("unknown command preserved", () => assert.equal(parseArgs(["node", "sd", "nope", "--x", "y"]).command, "nope")); + test("grouped short flags -jh", () => { + const r = parseArgs(["node", "sd", "scan", "-jh"]); + assert.ok(r.flags.json && r.flags.help); }); - - test("type: scan called with undefined content", () => { - let threw = false; - try { - (scan as any)(undefined, "context"); - } catch { - threw = true; - } - assert.ok(!threw, "scan(undefined) should not throw"); - }); - - test("type: redact called with non-array findings", () => { - const content = "hello"; - let threw = false; - try { - (redact as any)(content, null); - } catch { - threw = true; - } - assert.ok(!threw, "redact(content, null) should not throw"); + test("--flag=value is parsed", () => { + const r = parseArgs(["node", "sd", "scan", "--source=file", "path"]); + assert.equal(r.flags.source, "file"); + assert.equal(r.positional[0], "path"); }); - - test("type: redact called with findings containing NaN offset", () => { - const content = "hello world"; - const result = redact(content, [ - { ruleId: "test", match: "hello", redacted: "[REDACTED:test:xxx]", offset: NaN, source: "file" as const, description: "" }, - ]); - assert.equal(result, content, "NaN offset should result in no change"); - }); - + test("trailing boolean flag", () => assert.strictEqual(parseArgs(["node", "sd", "scan", "--json"]).flags.json, true)); }); diff --git a/tests/plugin/hooks.test.ts b/tests/plugin/hooks.test.ts index a0ec96e..248a307 100644 --- a/tests/plugin/hooks.test.ts +++ b/tests/plugin/hooks.test.ts @@ -1,459 +1,369 @@ import { test, describe } from "node:test"; import assert from "node:assert"; import assertStrict from "node:assert/strict"; -import { handleLayer1 } from "../../src/plugin/layer1.js"; -import { handleLayer2Compaction } from "../../src/plugin/layer2-compaction.js"; -import { handleLayer2Message } from "../../src/plugin/layer2-message.js"; -import { handleLayer3 } from "../../src/plugin/layer3.js"; -import { strangerDanger } from "../../src/plugin/index.js"; import fs from "node:fs"; import path from "node:path"; - -describe("plugin — index", () => { - test("exports strangerDanger plugin", () => { - assertStrict.ok(typeof strangerDanger === "function"); - }); - - test("plugin returns expected hook names", async () => { - const result = await strangerDanger({} as any); - assert.ok("tool.execute.before" in result); - assert.ok("experimental.session.compacting" in result); - assert.ok("event" in result); - assert.ok("tool.execute.after" in result); - assert.ok("tool" in result); - }); - - test("plugin tool.sd_status is defined", async () => { - const result = await strangerDanger({} as any); - assert.ok(result.tool.sd_status); - assert.strictEqual(typeof result.tool.sd_status.execute, "function"); - }); - - test("plugin tool.sd_scan is defined", async () => { - const result = await strangerDanger({} as any); - assert.ok(result.tool.sd_scan); - assert.strictEqual(typeof result.tool.sd_scan.execute, "function"); - }); - - test("plugin tool.sd_audit is defined", async () => { - const result = await strangerDanger({} as any); - assert.ok(result.tool.sd_audit); - assert.strictEqual(typeof result.tool.sd_audit.execute, "function"); - }); - - test("plugin tool.sd_allowlist_add is defined", async () => { - const result = await strangerDanger({} as any); - assert.ok(result.tool.sd_allowlist_add); - assert.strictEqual(typeof result.tool.sd_allowlist_add.execute, "function"); - }); - - test("plugin tool.sd_allowlist_show is defined", async () => { - const result = await strangerDanger({} as any); - assert.ok(result.tool.sd_allowlist_show); - assert.strictEqual(typeof result.tool.sd_allowlist_show.execute, "function"); - }); - - test("sd_status returns rule count", async () => { - const result = await strangerDanger({} as any); - const status = await result.tool.sd_status.execute(); - assert.ok(status.includes("Rules:")); - assert.ok(status.includes("strangerDanger v0.1.0")); +import os from "node:os"; +import { strangerDanger } from "../../src/plugin/index.js"; +import { handleLayer1 } from "../../src/plugin/layer1.js"; +import { handleLayer3 } from "../../src/plugin/layer3.js"; +import { handlePrompt, handleContext, handleMessageEvent } from "../../src/plugin/layer2-message.js"; +import { handleCompaction } from "../../src/plugin/layer2-compaction.js"; + +const AWS = "AKIAIOSFODNN7EXAMPLE"; +// Synthetic GitHub PAT with realistic (high) entropy so the rule's entropy +// gate accepts it. Real tokens are not low-entropy runs of one character. +const GH = "ghp_" + "aB3xK9mQ2pR7vL4nJ8wC5tY1cF6hA0sD8gE2"; +const REDACTED = "[REDACTED:"; + +function tempDir(): string { + return fs.mkdtempSync(path.join(os.tmpdir(), "sd-plugin-")); +} + +function cleanup(dir: string): void { + try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ } +} + +/** + * Minimal fake of the V2 plugin context: captures the tools and hooks the + * plugin registers so tests can exercise them without a live OpenCode server. + */ +function makeCtx(directory: string) { + const tools: any[] = []; + const hooks: Record unknown>> = {}; + const reg = { dispose: async () => {} }; + const ctx: any = { + location: { directory, project: { id: "proj", directory, canonical: directory } }, + options: {}, + tool: { + transform: async (cb: (editor: any) => void) => { + cb({ + add: (t: any) => tools.push(t), + namespace: () => {}, + list: () => tools, + get: () => undefined, + update: () => {}, + remove: () => {}, + }); + return reg; + }, + hook: async (name: string, cb: (e: any) => unknown) => { + (hooks[name] ||= []).push(cb); + return reg; + }, + list: async () => tools, + reload: async () => {}, + }, + session: { + hook: async (name: string, cb: (e: any) => unknown) => { + (hooks[`session.${name}`] ||= []).push(cb); + return reg; + }, + }, + event: { + subscribe: () => ({ + async *[Symbol.asyncIterator]() { /* no events */ }, + }), + }, + }; + return { ctx, tools, hooks }; +} + +async function setup(directory: string) { + const made = makeCtx(directory); + await strangerDanger.setup(made.ctx); + return made; +} + +describe("plugin — V2 definition and registration", () => { + test("default export is a V2 plugin with a stable id and setup", () => { + assert.strictEqual(strangerDanger.id, "gitdamnit.stranger-danger"); + assert.strictEqual(typeof strangerDanger.setup, "function"); + }); + + test("setup registers exactly the five tools", async () => { + const dir = tempDir(); + try { + const { tools } = await setup(dir); + const names = tools.map((t) => t.name).sort(); + assert.deepStrictEqual(names, ["sd_allowlist_add", "sd_allowlist_show", "sd_audit", "sd_scan", "sd_status"]); + } finally { + cleanup(dir); + } }); - test("sd_scan detects AWS key", async () => { - const result = await strangerDanger({} as any); - const output = await result.tool.sd_scan.execute({ - content: 'const key = "AKIAIOSFODNN7EXAMPLE";', - source: "file", - }); - assert.ok(output.includes("aws-access-token")); + test("setup registers the expected hooks", async () => { + const dir = tempDir(); + try { + const { hooks } = await setup(dir); + for (const name of ["execute.before", "execute.after", "session.prompt", "session.context", "session.compaction"]) { + assert.ok(hooks[name]?.length, `hook ${name} should be registered`); + } + } finally { + cleanup(dir); + } }); - test("sd_scan on clean content returns no secrets", async () => { - const result = await strangerDanger({} as any); - const output = await result.tool.sd_scan.execute({ - content: "const x = 42;", - }); - assert.strictEqual(output, "No secrets detected."); + test("setup returns a cleanup function", async () => { + const dir = tempDir(); + try { + const { ctx } = makeCtx(dir); + const cleanupFn = await strangerDanger.setup(ctx); + assert.strictEqual(typeof cleanupFn, "function"); + await cleanupFn!(); + } finally { + cleanup(dir); + } }); }); -describe("plugin — layer1 (file read interception)", () => { - test("handles non-file tools gracefully (no-op)", async () => { - // Should not throw, should not write anything - await handleLayer1( - { tool: "write", sessionID: "s1", callID: "c1" }, - { args: { filePath: null } }, - ); - // If we get here without error, test passes - assert.ok(true); - }); - - test("handles missing filePath gracefully", async () => { - await handleLayer1( - { tool: "read", sessionID: "s1", callID: "c1" }, - { args: {} }, - ); - assert.ok(true); +describe("plugin — tools", () => { + test("sd_status reports version and rule count", async () => { + const dir = tempDir(); + try { + const { tools } = await setup(dir); + const status = tools.find((t) => t.name === "sd_status"); + const out = await status.execute({}); + assert.ok(out.content.includes("strangerDanger v")); + assert.ok(out.content.includes("Rules:")); + } finally { + cleanup(dir); + } }); - test("handles non-existent file gracefully", async () => { - await handleLayer1( - { tool: "read", sessionID: "s1", callID: "c1" }, - { args: { filePath: "/tmp/nonexistent-file-xyz.ts" } }, - ); - assert.ok(true); + test("sd_scan detects a secret and never returns the raw value", async () => { + const dir = tempDir(); + try { + const { tools } = await setup(dir); + const scan = tools.find((t) => t.name === "sd_scan"); + const out = await scan.execute({ content: `const key = "${AWS}";`, source: "file" }); + assert.ok(out.content.includes("aws-access-token")); + assert.ok(!out.content.includes(AWS), "tool output must not contain the raw secret"); + assert.ok(out.content.includes("Fingerprint:")); + } finally { + cleanup(dir); + } }); - test("skips .env files", async () => { - const tmpDir = "/tmp/sd-layer1-test"; - const envPath = path.join(tmpDir, ".env"); - + test("sd_scan on clean content reports none", async () => { + const dir = tempDir(); try { - fs.mkdirSync(tmpDir, { recursive: true }); - fs.writeFileSync(envPath, 'SECRET_KEY=abc123'); - - // Should skip .env files without error - await handleLayer1( - { tool: "read", sessionID: "s1", callID: "c1" }, - { args: { filePath: envPath } }, - ); - - // File should be unchanged - const content = fs.readFileSync(envPath, "utf8"); - assert.strictEqual(content, 'SECRET_KEY=abc123'); + const { tools } = await setup(dir); + const scan = tools.find((t) => t.name === "sd_scan"); + const out = await scan.execute({ content: "const x = 42;" }); + assert.strictEqual(out.content, "No secrets detected."); } finally { - try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {} + cleanup(dir); } }); - test("skips .env.local files", async () => { - const tmpDir = "/tmp/sd-layer1-test-envlocal"; - const envPath = path.join(tmpDir, ".env.local"); - + test("sd_allowlist_add rejects an invalid fingerprint", async () => { + const dir = tempDir(); try { - fs.mkdirSync(tmpDir, { recursive: true }); - fs.writeFileSync(envPath, 'SECRET_KEY=abc123'); - - await handleLayer1( - { tool: "read", sessionID: "s1", callID: "c1" }, - { args: { filePath: envPath } }, - ); - - const content = fs.readFileSync(envPath, "utf8"); - assert.strictEqual(content, 'SECRET_KEY=abc123'); + const { tools } = await setup(dir); + const add = tools.find((t) => t.name === "sd_allowlist_add"); + const out = await add.execute({ fingerprint: "nothex" }); + assert.ok(out.content.toLowerCase().includes("fingerprint")); } finally { - try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {} + cleanup(dir); } }); - test("allows .env.example files", async () => { - const tmpDir = "/tmp/sd-layer1-test-env-example"; - const envPath = path.join(tmpDir, ".env.example"); - + test("sd_audit reports no entries on a fresh project", async () => { + const dir = tempDir(); try { - fs.mkdirSync(tmpDir, { recursive: true }); - // .env.example should NOT be skipped - const result = await handleLayer1( - { tool: "read", sessionID: "s1", callID: "c1" }, - { args: { filePath: envPath } }, - ); - // Should not throw and should proceed (file is empty so no secrets) - assert.ok(true); + const { tools } = await setup(dir); + const audit = tools.find((t) => t.name === "sd_audit"); + const out = await audit.execute({}); + assert.ok(out.content.includes("No audit entries") || out.content.includes("Audit log")); } finally { - try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {} + cleanup(dir); } }); +}); - test("redacts secrets in file and writes back", async () => { - const tmpDir = "/tmp/sd-layer1-test-redact"; - const filePath = path.join(tmpDir, "config.ts"); - +describe("plugin — layer1 (read interception never writes)", () => { + test("a read containing a secret leaves the file byte-for-byte unchanged", async () => { + const dir = tempDir(); try { - fs.mkdirSync(tmpDir, { recursive: true }); - fs.writeFileSync(filePath, 'const key = "AKIAIOSFODNN7EXAMPLE";'); + const file = path.join(dir, "config.ts"); + const original = `const key = "${AWS}";`; + fs.writeFileSync(file, original); + const auditPath = path.join(dir, "audit.jsonl"); - await handleLayer1( - { tool: "read", sessionID: "s1", callID: "c1" }, - { args: { filePath } }, - ); + await handleLayer1({ tool: "read", sessionID: "s1", input: { filePath: file } }, { auditPath }); - const content = fs.readFileSync(filePath, "utf8"); - assert.ok(content.includes("[REDACTED:")); - assert.ok(!content.includes("AKIAIOSFODNN7EXAMPLE")); + assert.strictEqual(fs.readFileSync(file, "utf8"), original, "file must not be modified"); + const log = fs.readFileSync(auditPath, "utf8"); + assert.ok(log.includes("aws-access-token"), "finding should be audited"); } finally { - try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {} + cleanup(dir); } }); - test("skips files over max size", async () => { - const tmpDir = "/tmp/sd-layer1-test-size"; - const filePath = path.join(tmpDir, "large.bin"); - + test("edit and write tools are inspected but never modified", async () => { + const dir = tempDir(); try { - fs.mkdirSync(tmpDir, { recursive: true }); - // Create file with 11MB of data - const large = Buffer.alloc(11 * 1024 * 1024, "A"); - fs.writeFileSync(filePath, large); - - // Should skip without error - await handleLayer1( - { tool: "read", sessionID: "s1", callID: "c1" }, - { args: { filePath } }, - ); - - // File should be unchanged - const stat = fs.statSync(filePath); - assert.strictEqual(stat.size, 11 * 1024 * 1024); + const file = path.join(dir, "config.txt"); + const original = `TOKEN=${GH}`; + fs.writeFileSync(file, original); + for (const tool of ["edit", "write"]) { + await handleLayer1({ tool, sessionID: "s1", input: { filePath: file } }, {}); + } + assert.strictEqual(fs.readFileSync(file, "utf8"), original); } finally { - try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {} + cleanup(dir); } }); - test("skips binary files", async () => { - const tmpDir = "/tmp/sd-layer1-test-binary"; - const filePath = path.join(tmpDir, "image.png"); - + test("handles missing filePath, non-existent file, and non-file tools gracefully", async () => { + const dir = tempDir(); try { - fs.mkdirSync(tmpDir, { recursive: true }); - // Create binary file with null byte - fs.writeFileSync(filePath, Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x00, 0x1A])); - - await handleLayer1( - { tool: "read", sessionID: "s1", callID: "c1" }, - { args: { filePath } }, - ); - - // File should be unchanged - const content = fs.readFileSync(filePath); - assertStrict.ok(content[0] === 0x89); + await handleLayer1({ tool: "read", sessionID: "s1", input: {} }, {}); + await handleLayer1({ tool: "read", sessionID: "s1", input: { filePath: path.join(dir, "nope.ts") } }, {}); + await handleLayer1({ tool: "bash", sessionID: "s1", input: { filePath: "/etc/hostname" } }, {}); + assert.ok(true); } finally { - try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {} + cleanup(dir); } }); - test("skips non-read/edit/write tools", async () => { - const tmpDir = "/tmp/sd-layer1-test-tools"; - const filePath = path.join(tmpDir, "test.ts"); - + test("skips binary files without modifying them", async () => { + const dir = tempDir(); try { - fs.mkdirSync(tmpDir, { recursive: true }); - fs.writeFileSync(filePath, 'const key = "AKIAIOSFODNN7EXAMPLE";'); - - // Should not process for write tool - await handleLayer1( - { tool: "execute", sessionID: "s1", callID: "c1" }, - { args: { filePath } }, - ); - - // File should be unchanged - const content = fs.readFileSync(filePath, "utf8"); - assert.strictEqual(content.includes("[REDACTED:"), false); + const file = path.join(dir, "image.png"); + fs.writeFileSync(file, Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x00, 0x1a])); + await handleLayer1({ tool: "read", sessionID: "s1", input: { filePath: file } }, {}); + const content = fs.readFileSync(file); + assert.strictEqual(content[0], 0x89); } finally { - try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {} + cleanup(dir); } }); }); -describe("plugin — layer2-compaction (session context scanning)", () => { - test("handles empty context", async () => { - const output = { context: [] }; - await handleLayer2Compaction({ sessionID: "s1" } as any, output); - assert.strictEqual(output.context.length, 0); +describe("plugin — layer3 (tool result redaction)", () => { + test("redacts string content", async () => { + const event = { tool: "read", sessionID: "s1", status: "completed", result: { content: `AWS ${AWS}` } }; + await handleLayer3(event as any, {}); + assert.ok(event.result.content.includes(REDACTED)); + assert.ok(!event.result.content.includes(AWS)); }); - test("handles null context", async () => { - const output = { context: null as any }; - await handleLayer2Compaction({ sessionID: "s1" } as any, output); - // Should not throw - assert.ok(true); - }); - - test("redacts secrets in context strings", async () => { - const output: { context: string[] } = { - context: ['const key = "AKIAIOSFODNN7EXAMPLE";'], - }; - - await handleLayer2Compaction({ sessionID: "s1" } as any, output); - - const result = output.context[0]; - assert.ok(result.includes("[REDACTED:")); - assert.ok(!result.includes("AKIAIOSFODNN7EXAMPLE")); - }); - - test("preserves clean context strings", async () => { - const output: { context: string[] } = { - context: ["const x = 42;"], - }; - - await handleLayer2Compaction({ sessionID: "s1" } as any, output); - - assert.strictEqual(output.context[0], "const x = 42;"); - }); - -test("handles multiple context entries", async () => { - const gh = 'ghp_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789ab'; - const output: { context: string[] } = { - context: [ - "const aws = 'AKIAIOSFODNN7EXAMPLE';", - "const gh = '" + gh + "';", - "const clean = 'hello world';", - ], - }; - - await handleLayer2Compaction({ sessionID: "s1" } as any, output); - - assert.ok(output.context[0].includes("[REDACTED:")); - assert.ok(output.context[1].includes("[REDACTED:")); - assertStrict.strictEqual(output.context[2], "const clean = 'hello world';"); - }); -}); - -describe("plugin — layer2-message (message scanning)", () => { - test("ignores non-message event types", async () => { + test("redacts text content parts", async () => { const event = { - type: "tool.executed", tool: "read", - } as any; - - // Should return without processing - await handleLayer2Message({ event }); - assert.ok(true); - }); - - test("processes message.created events", async () => { - const output: { source?: string; findings?: any[] } = {}; - - // We need to intercept the scanAndRedact call - // Since this is hard to do directly, just test it doesn't throw - const event = { - type: "message.created", - text: 'const key = "AKIAIOSFODNN7EXAMPLE";', - } as any; - - await handleLayer2Message({ event }); - assert.ok(true); - }); - - test("processes message.updated events", async () => { - const event = { - type: "message.updated", - content: 'const key = "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef12";', - } as any; - - await handleLayer2Message({ event }); - assert.ok(true); + sessionID: "s1", + status: "completed", + result: { content: [{ type: "text", text: `key ${GH}` }, { type: "file", uri: "file://x", mime: "text/plain" }] }, + }; + await handleLayer3(event as any, {}); + assert.ok((event.result.content[0] as any).text.includes(REDACTED)); + assert.ok(!JSON.stringify(event.result.content).includes(GH)); }); - test("ignores events without text/content/message", async () => { + test("redacts structured output string leaves while preserving shape", async () => { const event = { - type: "message.created", - data: { something: "else" }, - } as any; - - await handleLayer2Message({ event }); + tool: "query", + sessionID: "s1", + status: "completed", + result: { output: { token: GH, nested: { count: 3, list: ["ok", AWS] } } }, + }; + await handleLayer3(event as any, {}); + const out: any = event.result.output; + assert.strictEqual(typeof out.token, "string"); + assert.ok(out.token.includes(REDACTED)); + assert.strictEqual(out.nested.count, 3); + assert.deepStrictEqual(out.nested.list[0], "ok"); + assert.ok(out.nested.list[1].includes(REDACTED)); + }); + + test("scans the whole large output, including a secret past the old 1 MB limit", async () => { + const filler = "A".repeat(1024 * 1024 + 5000); + const event = { tool: "bash", sessionID: "s1", status: "completed", result: { content: filler + GH } }; + await handleLayer3(event as any, {}); + assert.ok(event.result.content.includes(REDACTED)); + assert.ok(!event.result.content.includes(GH), "secret past 1 MB must be redacted"); + }); + + test("ignores null/undefined results and errors without throwing", async () => { + await handleLayer3({ tool: "x", sessionID: "s1", status: "completed", result: null } as any, {}); + await handleLayer3(null as any, {}); + await handleLayer3({ tool: "x", sessionID: "s1", status: "error", error: { message: `boom ${AWS}` } } as any, {}); assert.ok(true); }); - - test("handles message object with text property", () => { - function extractMessageText(event: any): string | null { - if ("text" in event && typeof event.text === "string") return event.text; - if ("content" in event && typeof event.content === "string") return event.content; - if ("message" in event && event.message) { - const msg = event.message as Record; - if (typeof msg.text === "string") return msg.text; - if (typeof msg.content === "string") return msg.content; - } - return null; - } - - const result = extractMessageText({ - message: { text: "AKIAIOSFODNN7EXAMPLE" }, - }); - assert.strictEqual(result, "AKIAIOSFODNN7EXAMPLE"); - }); - - test("handles message object with content property", () => { - function extractMessageText(event: any): string | null { - if ("text" in event && typeof event.text === "string") return event.text; - if ("content" in event && typeof event.content === "string") return event.content; - if ("message" in event && event.message) { - const msg = event.message as Record; - if (typeof msg.text === "string") return msg.text; - if (typeof msg.content === "string") return msg.content; - } - return null; - } - - const result = extractMessageText({ - message: { content: "sk-ant-api03-test" }, - }); - assert.strictEqual(result, "sk-ant-api03-test"); - }); }); -describe("plugin — layer3 (output scanning)", () => { - test("handles null output", async () => { - const output = { title: "test", output: null, metadata: {} }; - await handleLayer3( - { tool: "test", sessionID: "s1", callID: "c1", args: {} } as any, - output as any, - ); - assert.strictEqual(output.output, null); - }); - - test("redacts secrets in tool output", async () => { - const output: { title: string; output: string; metadata: any } = { - title: "result", - output: 'AWS key: AKIAIOSFODNN7EXAMPLE', - metadata: {}, +describe("plugin — layer2 prompt and context redaction", () => { + test("prompt hook redacts the outgoing prompt text", async () => { + const event = { sessionID: "s1", prompt: { text: `please use ${GH} carefully` } } as any; + await handlePrompt(event, {}); + assert.ok(event.prompt.text.includes(REDACTED)); + assert.ok(!event.prompt.text.includes(GH)); + }); + + test("context hook redacts system parts and message text", async () => { + const event: any = { + sessionID: "s1", + system: [{ type: "text", text: `system ${AWS}` }], + messages: [ + { role: "user", content: [{ type: "text", text: `hi ${GH}` }] }, + { role: "assistant", content: [{ type: "tool-call", id: "1", name: "x", input: { key: AWS } }] }, + { role: "tool", content: [{ type: "tool-result", id: "1", name: "x", result: { type: "text", value: GH } }] }, + ], }; - - await handleLayer3( - { tool: "test", sessionID: "s1", callID: "c1", args: {} } as any, - output, - ); - - assert.ok(output.output.includes("[REDACTED:")); - assert.ok(!output.output.includes("AKIAIOSFODNN7EXAMPLE")); - }); - - test("truncates output over MAX_OUTPUT_SIZE", async () => { - const largeOutput = "A".repeat(2 * 1024 * 1024); // 2MB - const output: { title: string; output: string; metadata: any } = { - title: "result", - output: largeOutput, - metadata: {}, + await handleContext(event, {}); + assert.ok(event.system[0].text.includes(REDACTED)); + assert.ok(event.messages[0].content[0].text.includes(REDACTED)); + assert.ok(event.messages[1].content[0].input.key.includes(REDACTED)); + assert.ok(event.messages[2].content[0].result.value.includes(REDACTED)); + assert.ok(!JSON.stringify(event).includes(AWS)); + assert.ok(!JSON.stringify(event).includes(GH)); + }); + + test("compaction hook redacts the transcript and its summary", async () => { + const event: any = { + sessionID: "s1", + messages: [{ role: "user", content: [{ type: "text", text: `secret ${GH}` }] }], + result: { summary: `summary leaks ${AWS}` }, }; - - await handleLayer3( - { tool: "test", sessionID: "s1", callID: "c1", args: {} } as any, - output, - ); - - // Output should have been truncated to MAX_OUTPUT_SIZE before scanning - // (scan only processes what was passed to it) - assert.ok(true); + await handleCompaction(event, {}); + assert.ok(event.messages[0].content[0].text.includes(REDACTED)); + assert.ok(event.result.summary.includes(REDACTED)); }); -}); -describe("plugin — error handling", () => { - test("layer1 does not throw on errors", async () => { - // Pass invalid input that could cause errors - await handleLayer1( - { tool: "read", sessionID: "s1", callID: "c1" } as any, - { args: { filePath: "/proc/self/fd/99999" } }, - ); + test("message event detector does not throw on arbitrary payloads", () => { + handleMessageEvent({ type: "message.created", text: GH }, {}); + handleMessageEvent({ type: "session.idle" }, {}); + handleMessageEvent(null, {}); + handleMessageEvent({ type: "message.updated", part: { text: AWS } }, {}); assert.ok(true); }); +}); - test("layer2-compaction does not throw on errors", async () => { - await handleLayer2Compaction(null as any, null as any); - assert.ok(true); +describe("plugin — end-to-end hook wiring", () => { + test("execute.after hook registered by setup redacts a tool result", async () => { + const dir = tempDir(); + try { + const { hooks } = await setup(dir); + const event: any = { tool: "bash", sessionID: "s1", status: "completed", result: { content: GH } }; + for (const h of hooks["execute.after"]) await h(event); + assert.ok(!event.result.content.includes(GH)); + assert.ok(event.result.content.includes(REDACTED)); + } finally { + cleanup(dir); + } }); - test("layer3 does not throw on errors", async () => { - await handleLayer3(null as any, null as any); - assert.ok(true); + test("session.context hook registered by setup redacts the request", async () => { + const dir = tempDir(); + try { + const { hooks } = await setup(dir); + const event: any = { sessionID: "s1", system: [], messages: [{ role: "user", content: [{ type: "text", text: AWS }] }] }; + for (const h of hooks["session.context"]) await h(event); + assert.ok(!JSON.stringify(event).includes(AWS)); + } finally { + cleanup(dir); + } }); -}); \ No newline at end of file +}); From 5465988597326c5b04011800cecd56e31386024c Mon Sep 17 00:00:00 2001 From: Evan Zellner Date: Thu, 15 Oct 2026 08:50:47 -0500 Subject: [PATCH 2/4] Document and enforce plugin independence standard - Harden all hook callbacks to never throw on malformed/unexpected events so a broken companion or bad event cannot cascade; keep the read-only execute.before contract explicit. - Document independence guarantees, hook ownership, config/storage namespaces, companion-absent behavior, and the ordering-independent context-hook backstop. - Document the single optional, read-only, semver-versioned interop surface (@gitdamnit/stranger-danger/engine). No companion dependency is introduced. - Remove a stale reference to the retired CheapCode project from scripts/write_fixture.ts; it now writes only inside this repository. - Add tests/plugin/independence.test.ts: no cross-plugin coupling, installs and runs alone, project/session isolation, no-order dependency, failure isolation, reload/cleanup, companion-absent behavior. Full suite: 306 tests passing; typecheck, lint, build, and package validation pass. --- CHANGELOG.md | 16 ++ README.md | 77 ++++++++ dist/engine/audit.d.ts.map | 2 +- dist/engine/audit.js | 14 +- dist/engine/audit.js.map | 2 +- dist/plugin/index.d.ts.map | 2 +- dist/plugin/index.js | 43 ++++- dist/plugin/index.js.map | 2 +- scripts/write_fixture.ts | 55 +++--- src/engine/audit.ts | 14 +- src/plugin/index.ts | 50 ++++-- tests/plugin/independence.test.ts | 280 ++++++++++++++++++++++++++++++ 12 files changed, 495 insertions(+), 62 deletions(-) create mode 100644 tests/plugin/independence.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d9eb07..5546697 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,22 @@ Security hardening and OpenCode V2 migration. Not yet published. - Runtime state is Git-ignored; the previously tracked audit file is untracked but preserved on disk. +### Independence & interoperability standard + +- Confirmed and documented plugin independence: no cross-plugin imports, no + shared database/configuration, no required initialization order, no companion + dependency. Tools are `sd_`-namespaced; storage and config are namespaced. +- Hardened hook callbacks to never throw on malformed/unexpected events, so a + broken companion or bad event cannot cascade. +- Documented the ordering-independent guarantee (the `session.context` hook is + the backstop) and the single optional, read-only, semver-versioned interop + surface (`@gitdamnit/stranger-danger/engine`). +- Removed a stale reference to the retired CheapCode project from + `scripts/write_fixture.ts`; it now writes only inside this repository. +- Added an independence test suite (isolated instances, session isolation, + no-order-dependency, failure isolation, reload/cleanup, companion-absent + behavior, absence of cross-plugin coupling). + ### Reliability, tests, packaging, CI - Fixed test discovery (referenced a non-existent `tests/integration.ts`; diff --git a/README.md b/README.md index 4b6adad..d5769e5 100755 --- a/README.md +++ b/README.md @@ -158,6 +158,83 @@ Not guaranteed (documented, by design): - Content larger than the scanner's hard ceiling fails closed with a marker rather than being partially inspected. +## Independence & interoperability + +Stranger Danger is a self-contained security inspector. It is being developed +alongside two other independent OpenCode V2 plugins (Git That Shit and +Checkpoint Orchestrator) but **requires neither**, and neither requires it. + +### Responsibilities + +- Inspect tool calls/arguments and model traffic for secrets and unsafe content. +- Detect risks and report them through its own tools and audit log. +- Own its security policy and audit records. Nothing else. +- Its pre-execution hook is **read-only** and never mutates tool arguments. + +### Independence guarantees + +- **Works alone.** No companion plugin is needed to start or operate. There are + no cross-plugin imports, no shared database, and no shared configuration. +- **No required order.** Correctness never depends on another plugin running + before or after. The `execute.before` hook is read-only, and the + `session.context` hook is the ordering-independent backstop: it re-scans the + fully assembled request, so even if a companion rewrites a tool result after + this plugin's `execute.after` hook, the secret is still removed before the + model request. +- **Failure isolation.** Every hook callback is wrapped so that a malformed or + unexpected event can never throw out of the hook. A broken companion, a + missing optional interface, or a bad event cannot cascade into this plugin or + the runtime, and this plugin cannot cascade into others. +- **Namespaced surface.** All tools use the `sd_` prefix; the plugin id is + `gitdamnit.stranger-danger`; storage and config are namespaced (below). +- **Session isolation.** Each plugin instance captures its own project-scoped + audit path in a closure and passes it explicitly to every write. There is no + shared mutable process-global audit state, so concurrent sessions/projects in + one server never mix records. Audit entries carry a session ID when available. + +### Hooks registered + +| Hook | Mutates? | Purpose | +| --- | --- | --- | +| `tool.execute.before` | no (read-only) | Detect/audit secrets in files about to be read | +| `tool.execute.after` | redacts tool result text | Keep secrets out of the model context | +| `session.prompt` | redacts prompt text | Pre-admission prompt protection | +| `session.context` | redacts system + messages | Ordering-independent request backstop | +| `session.compaction` | redacts transcript/summary | Prevent secret propagation into checkpoints | +| `event.subscribe` | no | Detection/audit only (observational) | + +### Configuration namespace + +- No required options. Unknown/companion options are ignored. +- Optional allowlist: `.strangerdanger-allowlist.toml` in the project root. +- Loaded via the standard V2 `plugins` entry; no global configuration is written. + +### Owned storage + +- Audit log: `/.opencode/state/strangerdanger-audit.jsonl` + (git-ignored, `0600`; rotations `.1`/`.2`/`.3`). This plugin writes nowhere else. +- It never writes into another plugin's storage and never reads a live companion + database. + +### Optional interoperability (read-only, versioned) + +The only interop surface is the semver-versioned, read-only engine export: + +```js +import { scan, redact, scanAndRedact, scanAndRedactBounded, toSafeFinding } + from "@gitdamnit/stranger-danger/engine"; +``` + +A companion plugin or the Docker Plugin Test Lab may call these to sanitize or +inspect text. They are pure functions over strings with no shared state, no I/O +beyond the project allowlist, and no dependency on this plugin being loaded. No +integration is implemented that requires a companion to be present. + +### Compatible OpenCode V2 versions + +Targets the V2 runtime and `@opencode/plugin >= 2.0.0` (tested against +`@opencode/plugin@2.0.29` and OpenCode `2.0.x`). + ## Architecture ``` diff --git a/dist/engine/audit.d.ts.map b/dist/engine/audit.d.ts.map index a895aba..23885ad 100755 --- a/dist/engine/audit.d.ts.map +++ b/dist/engine/audit.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/engine/audit.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,WAAW,IAAI,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACxE,OAAO,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAwBtD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAGzE;AAED,4EAA4E;AAC5E,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAE/C;AAED,2DAA2D;AAC3D,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAuCD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAYjE;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAI1E;AAED,wBAAgB,YAAY,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAIlD;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACjC,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAA;CAAE,GACnF,UAAU,CAWZ;AAED,gFAAgF;AAChF,OAAO,EAAE,aAAa,EAAE,CAAC"} \ No newline at end of file +{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/engine/audit.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,WAAW,IAAI,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACxE,OAAO,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AA4BtD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAGzE;AAED,4EAA4E;AAC5E,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAE/C;AAED,2DAA2D;AAC3D,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAuCD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAYjE;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAI1E;AAED,wBAAgB,YAAY,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAIlD;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACjC,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAA;CAAE,GACnF,UAAU,CAWZ;AAED,gFAAgF;AAChF,OAAO,EAAE,aAAa,EAAE,CAAC"} \ No newline at end of file diff --git a/dist/engine/audit.js b/dist/engine/audit.js index ac9bae9..bf29d9f 100755 --- a/dist/engine/audit.js +++ b/dist/engine/audit.js @@ -12,11 +12,15 @@ const AUDIT_DIR_MODE = 0o700; const MAX_AUDIT_BYTES = 10 * 1024 * 1024; const MAX_ROTATIONS = 3; /** - * The audit path used by the standalone CLI and by callers that do not pass an - * explicit path. It is intentionally NOT a shared, mutable, process-global - * value that the plugin relies on: the OpenCode plugin resolves a path per - * project/session and passes it explicitly to every write, so that concurrent - * projects in one server process never mix their audit records. + * Fallback audit path for the standalone CLI (a single-process program) and for + * tests. It is CLI/test-only. + * + * The OpenCode plugin NEVER reads or writes this value. A plugin instance + * resolves its own project-scoped path during `setup`, captures it in a + * closure, and passes it explicitly to every audit write. This keeps the + * plugin free of shared mutable process-global state, so multiple projects + * hosted in one OpenCode server (or an unrelated toolkit plugin in the same + * process) can never mix audit records. */ let auditPathOverride; /** diff --git a/dist/engine/audit.js.map b/dist/engine/audit.js.map index ca99249..b7802db 100755 --- a/dist/engine/audit.js.map +++ b/dist/engine/audit.js.map @@ -1 +1 @@ -{"version":3,"file":"audit.js","sourceRoot":"","sources":["../../src/engine/audit.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"audit.js","sourceRoot":"","sources":["../../src/engine/audit.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/plugin/index.d.ts.map b/dist/plugin/index.d.ts.map index ed1957e..a187afd 100755 --- a/dist/plugin/index.d.ts.map +++ b/dist/plugin/index.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plugin/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAS1C;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,cAAc,eA6FzB,CAAC;AAEH,eAAe,cAAc,CAAC"} \ No newline at end of file +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plugin/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAS1C;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,cAAc,eAiHzB,CAAC;AAEH,eAAe,cAAc,CAAC"} \ No newline at end of file diff --git a/dist/plugin/index.js b/dist/plugin/index.js index d9f7511..24ea5da 100755 --- a/dist/plugin/index.js +++ b/dist/plugin/index.js @@ -80,11 +80,44 @@ export const strangerDanger = Plugin.define({ }); }); // --- Hooks --------------------------------------------------------- - await ctx.tool.hook("execute.before", (event) => handleLayer1({ tool: event.tool, sessionID: String(event.sessionID), input: event.input }, audit)); - await ctx.tool.hook("execute.after", (event) => handleLayer3(event, audit)); - await ctx.session.hook("prompt", (event) => handlePrompt({ sessionID: String(event.sessionID), prompt: event.prompt }, audit)); - await ctx.session.hook("context", (event) => handleContext({ sessionID: String(event.sessionID), system: event.system, messages: event.messages }, audit)); - await ctx.session.hook("compaction", (event) => handleCompaction({ sessionID: String(event.sessionID), messages: event.messages, result: event.result }, audit)); + // + // Every hook callback is wrapped so that accessing a field of an + // unexpected/malformed event can never throw out of the hook. A thrown + // hook could cascade into other plugins or abort the runtime; failing + // closed *inside* the handler (which already catches) is safe, but + // throwing from the wrapper is not. This keeps Stranger Danger from + // ever breaking a companion plugin or the OpenCode runtime. + const sid = (event) => event && typeof event.sessionID === "string" ? event.sessionID : undefined; + await ctx.tool.hook("execute.before", (event) => { + try { + return handleLayer1({ tool: event?.tool, sessionID: sid(event), input: event?.input }, audit); + } + catch { /* never throw from a hook */ } + }); + await ctx.tool.hook("execute.after", (event) => { + try { + return handleLayer3(event, audit); + } + catch { /* never throw from a hook */ } + }); + await ctx.session.hook("prompt", (event) => { + try { + return handlePrompt({ sessionID: sid(event), prompt: event?.prompt }, audit); + } + catch { /* never throw from a hook */ } + }); + await ctx.session.hook("context", (event) => { + try { + return handleContext({ sessionID: sid(event), system: event?.system, messages: event?.messages }, audit); + } + catch { /* never throw from a hook */ } + }); + await ctx.session.hook("compaction", (event) => { + try { + return handleCompaction({ sessionID: sid(event), messages: event?.messages, result: event?.result }, audit); + } + catch { /* never throw from a hook */ } + }); // --- Event subscription (detection/audit only) --------------------- const controller = new AbortController(); void (async () => { diff --git a/dist/plugin/index.js.map b/dist/plugin/index.js.map index 2a0866c..41ae066 100755 --- a/dist/plugin/index.js.map +++ b/dist/plugin/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/plugin/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/plugin/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/scripts/write_fixture.ts b/scripts/write_fixture.ts index 9a689cc..005a075 100644 --- a/scripts/write_fixture.ts +++ b/scripts/write_fixture.ts @@ -1,47 +1,46 @@ import fs from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join, resolve } from "node:path"; + +/** + * Developer helper: regenerate the defanged "dirty" fixture. + * + * This is intentionally self-contained and writes only inside this repository. + * It previously pointed at an absolute path belonging to the retired CheapCode + * project; it now resolves a repo-relative target and refuses to escape the + * repository root. + */ +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const target = join(repoRoot, "tests", "fixtures", "generated-dirty.ts"); + +if (!target.startsWith(repoRoot + "/")) { + throw new Error(`Refusing to write outside the repository: ${target}`); +} const pem = `-----BEGIN PRIVATE KEY----- -FAKE_PRIVATE_KEY_FOR_TESTS_ONLY_DO_NOT_USE +RkFLRV9QUklWQVRFX0tFWV9GT1JfVEVTVFNfT05MWV9ET19OT1RfVVNFXzAwMDAwMDAwMDAwMDAwMDA= -----END PRIVATE KEY-----`; -const openai = "OPENAI_FAKE_KEY_FOR_TESTS_NOT_REAL"; -const anthropic = "ANTHROPIC_FAKE_KEY_FOR_TESTS_NOT_REAL"; +const fixture = `// Generated by scripts/write_fixture.ts — defanged values for scanner tests. +// These are intentionally NOT valid provider key formats. -const fixture = ` +export const generatedDirtyContent = \` // AWS Access Key - defanged test fixture const AWS_KEY = "AWS_FAKE_ACCESS_KEY_FOR_TESTS_NOT_REAL"; -// GitHub token - defanged test fixture -const GH_TOKEN = "GITHUB_FAKE_TOKEN_FOR_TESTS_NOT_REAL"; - // OpenAI API Key - defanged test fixture -const OPENAI_KEY = "${openai}"; +const OPENAI_KEY = "OPENAI_FAKE_KEY_FOR_TESTS_NOT_REAL"; // Anthropic API Key - defanged test fixture -const ANTHROPIC_KEY = "${anthropic}"; - -// Stripe Secret Key - defanged test fixture -const STRIPE_KEY = "STRIPE_FAKE_SECRET_KEY_FOR_TESTS_NOT_REAL"; - -// Groq API Key - defanged test fixture -const GROQ_KEY = "GROQ_FAKE_KEY_FOR_TESTS_NOT_REAL"; +const ANTHROPIC_KEY = "ANTHROPIC_FAKE_KEY_FOR_TESTS_NOT_REAL"; // MongoDB connection string with fake credentials const MONGO_URI = "mongodb+srv://fake_user:fake_password@cluster0.example.invalid/mydb"; -// Supabase token - defanged test fixture -const SUPABASE_TOKEN = "SUPABASE_FAKE_TOKEN_FOR_TESTS_NOT_REAL"; - // Private Key - defanged test fixture -const PRIVATE_KEY = \`${pem}\`; - -// JWT token - defanged test fixture -const JWT_TOKEN = "JWT_FAKE_TOKEN_FOR_TESTS_NOT_REAL"; +const PRIVATE_KEY = \\\`${pem}\\\`; +\`; `; -fs.writeFileSync( - "/home/evan/Desktop/Projects/cheapcode/module7-stranger-danger-plugin/tests/fixtures/dirty.ts", - fixture -); - -console.log("Fixture written"); +fs.writeFileSync(target, fixture); +console.log(`Fixture written to ${target}`); diff --git a/src/engine/audit.ts b/src/engine/audit.ts index f76803d..b487b00 100755 --- a/src/engine/audit.ts +++ b/src/engine/audit.ts @@ -24,11 +24,15 @@ const MAX_AUDIT_BYTES = 10 * 1024 * 1024; const MAX_ROTATIONS = 3; /** - * The audit path used by the standalone CLI and by callers that do not pass an - * explicit path. It is intentionally NOT a shared, mutable, process-global - * value that the plugin relies on: the OpenCode plugin resolves a path per - * project/session and passes it explicitly to every write, so that concurrent - * projects in one server process never mix their audit records. + * Fallback audit path for the standalone CLI (a single-process program) and for + * tests. It is CLI/test-only. + * + * The OpenCode plugin NEVER reads or writes this value. A plugin instance + * resolves its own project-scoped path during `setup`, captures it in a + * closure, and passes it explicitly to every audit write. This keeps the + * plugin free of shared mutable process-global state, so multiple projects + * hosted in one OpenCode server (or an unrelated toolkit plugin in the same + * process) can never mix audit records. */ let auditPathOverride: string | undefined; diff --git a/src/plugin/index.ts b/src/plugin/index.ts index f7c6ff7..cacc673 100755 --- a/src/plugin/index.ts +++ b/src/plugin/index.ts @@ -84,25 +84,45 @@ export const strangerDanger = Plugin.define({ }); // --- Hooks --------------------------------------------------------- - await ctx.tool.hook("execute.before", (event) => - handleLayer1({ tool: event.tool, sessionID: String(event.sessionID), input: event.input }, audit), - ); + // + // Every hook callback is wrapped so that accessing a field of an + // unexpected/malformed event can never throw out of the hook. A thrown + // hook could cascade into other plugins or abort the runtime; failing + // closed *inside* the handler (which already catches) is safe, but + // throwing from the wrapper is not. This keeps Stranger Danger from + // ever breaking a companion plugin or the OpenCode runtime. + const sid = (event: any): string | undefined => + event && typeof event.sessionID === "string" ? event.sessionID : undefined; - await ctx.tool.hook("execute.after", (event) => - handleLayer3(event as any, audit), - ); + await ctx.tool.hook("execute.before", (event) => { + try { + return handleLayer1({ tool: (event as any)?.tool, sessionID: sid(event), input: (event as any)?.input }, audit); + } catch { /* never throw from a hook */ } + }); - await ctx.session.hook("prompt", (event) => - handlePrompt({ sessionID: String(event.sessionID), prompt: event.prompt as any }, audit), - ); + await ctx.tool.hook("execute.after", (event) => { + try { + return handleLayer3(event as any, audit); + } catch { /* never throw from a hook */ } + }); - await ctx.session.hook("context", (event) => - handleContext({ sessionID: String(event.sessionID), system: event.system, messages: event.messages }, audit), - ); + await ctx.session.hook("prompt", (event) => { + try { + return handlePrompt({ sessionID: sid(event), prompt: (event as any)?.prompt }, audit); + } catch { /* never throw from a hook */ } + }); - await ctx.session.hook("compaction", (event) => - handleCompaction({ sessionID: String(event.sessionID), messages: event.messages, result: event.result as any }, audit), - ); + await ctx.session.hook("context", (event) => { + try { + return handleContext({ sessionID: sid(event), system: (event as any)?.system, messages: (event as any)?.messages }, audit); + } catch { /* never throw from a hook */ } + }); + + await ctx.session.hook("compaction", (event) => { + try { + return handleCompaction({ sessionID: sid(event), messages: (event as any)?.messages, result: (event as any)?.result }, audit); + } catch { /* never throw from a hook */ } + }); // --- Event subscription (detection/audit only) --------------------- const controller = new AbortController(); diff --git a/tests/plugin/independence.test.ts b/tests/plugin/independence.test.ts new file mode 100644 index 0000000..8217d80 --- /dev/null +++ b/tests/plugin/independence.test.ts @@ -0,0 +1,280 @@ +import { test, describe } from "node:test"; +import assert from "node:assert"; +import fs from "node:fs"; +import path from "node:path"; +import os from "node:os"; +import { fileURLToPath } from "node:url"; +import { strangerDanger } from "../../src/plugin/index.js"; +import { handleLayer1 } from "../../src/plugin/layer1.js"; +import { handleLayer3 } from "../../src/plugin/layer3.js"; +import { handlePrompt, handleContext, handleMessageEvent } from "../../src/plugin/layer2-message.js"; +import { handleCompaction } from "../../src/plugin/layer2-compaction.js"; +import { resolveAuditPath } from "../../src/engine/audit.js"; + +const AWS = "AKIAIOSFODNN7EXAMPLE"; +const GH = "ghp_" + "aB3xK9mQ2pR7vL4nJ8wC5tY1cF6hA0sD8gE2"; +const GH2 = "ghp_" + "Z9yQ2wE4rT6uI8oP0aS1dF3gH5jK7lM9nBVb"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", ".."); + +function tempDir(): string { + return fs.mkdtempSync(path.join(os.tmpdir(), "sd-indep-")); +} +function cleanup(dir: string): void { + try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ } +} + +/** + * A context exposing ONLY the documented V2 plugin surface this plugin uses. + * No companion-plugin data, no extra domains: proves the plugin initializes + * without any other toolkit plugin present. + */ +function makeMinimalCtx(directory: string) { + const tools: any[] = []; + const hooks: Record unknown>> = {}; + const reg = { dispose: async () => {} }; + const ctx: any = { + location: { directory, project: { id: "proj", directory, canonical: directory } }, + options: {}, + tool: { + transform: async (cb: any) => { cb({ add: (t: any) => tools.push(t), namespace() {}, list: () => tools, get() {}, update() {}, remove() {} }); return reg; }, + hook: async (n: string, cb: any) => { (hooks[n] ||= []).push(cb); return reg; }, + list: async () => tools, + reload: async () => {}, + }, + session: { hook: async (n: string, cb: any) => { (hooks["session." + n] ||= []).push(cb); return reg; } }, + event: { subscribe: () => ({ async *[Symbol.asyncIterator]() { /* no events */ } }) }, + }; + return { ctx, tools, hooks }; +} + +async function setup(directory: string) { + const made = makeMinimalCtx(directory); + const dispose = await strangerDanger.setup(made.ctx); + return { ...made, dispose }; +} + +describe("independence — no cross-plugin coupling", () => { + test("source does not import or reference other toolkit plugins", () => { + const forbidden = /(git[-_]?that[-_]?shit|checkpoint[-_]?orchestrator|cheapcode|@gitdamnit\/(?!stranger-danger))/i; + const offenders: string[] = []; + const walk = (dir: string) => { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const p = path.join(dir, entry.name); + if (entry.isDirectory()) { walk(p); continue; } + if (!p.endsWith(".ts") && !p.endsWith(".js") && !p.endsWith(".json")) continue; + const src = fs.readFileSync(p, "utf8"); + for (const m of src.matchAll(/(?:from|require\()\s*["'`]([^"'`]+)["'`]/g)) { + if (forbidden.test(m[1])) offenders.push(`${path.relative(repoRoot, p)} → ${m[1]}`); + } + } + }; + walk(path.join(repoRoot, "src")); + assert.deepStrictEqual(offenders, [], `forbidden cross-plugin imports: ${offenders.join(", ")}`); + }); + + test("package.json declares no toolkit-plugin dependencies", () => { + const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, "package.json"), "utf8")); + const all = { ...pkg.dependencies, ...pkg.peerDependencies, ...pkg.devDependencies }; + for (const name of Object.keys(all)) { + assert.ok( + !/git-that-shit|checkpoint-orchestrator/i.test(name), + `must not depend on toolkit plugin ${name}`, + ); + } + }); + + test("tool names are namespaced with the sd_ prefix (collision avoidance)", async () => { + const dir = tempDir(); + try { + const { tools } = await setup(dir); + assert.ok(tools.length > 0); + for (const t of tools) assert.ok(t.name.startsWith("sd_"), `tool ${t.name} is not namespaced`); + } finally { cleanup(dir); } + }); +}); + +describe("independence — installs and runs alone", () => { + test("setup succeeds with only the documented V2 surface and empty options", async () => { + const dir = tempDir(); + try { + const { tools, hooks } = await setup(dir); + assert.deepStrictEqual( + tools.map((t) => t.name).sort(), + ["sd_allowlist_add", "sd_allowlist_show", "sd_audit", "sd_scan", "sd_status"], + ); + for (const h of ["execute.before", "execute.after", "session.prompt", "session.context", "session.compaction"]) { + assert.ok(hooks[h]?.length, `missing hook ${h}`); + } + } finally { cleanup(dir); } + }); + + test("unknown options are ignored", async () => { + const dir = tempDir(); + try { + const made = makeMinimalCtx(dir); + made.ctx.options = { somethingFromACompanionPlugin: true }; + const dispose = await strangerDanger.setup(made.ctx); + assert.strictEqual(typeof dispose, "function"); + await dispose!(); + } finally { cleanup(dir); } + }); + + test("audit storage is owned and namespaced under the project", () => { + const resolved = resolveAuditPath({ directory: "/tmp/some-project" }); + assert.strictEqual(resolved, path.join("/tmp/some-project", ".opencode", "state", "strangerdanger-audit.jsonl")); + }); +}); + +describe("independence — session and project isolation", () => { + test("two plugin instances write only to their own project audit file", async () => { + const dirA = tempDir(); + const dirB = tempDir(); + try { + const a = await setup(dirA); + const b = await setup(dirB); + const fileA = resolveAuditPath({ directory: dirA }); + const fileB = resolveAuditPath({ directory: dirB }); + + // Each instance sees a different secret with a different session id. + for (const h of a.hooks["execute.after"]) { + await h({ tool: "bash", sessionID: "session-A", status: "completed", result: { content: GH } }); + } + for (const h of b.hooks["execute.after"]) { + await h({ tool: "bash", sessionID: "session-B", status: "completed", result: { content: GH2 } }); + } + + const logA = fs.readFileSync(fileA, "utf8"); + const logB = fs.readFileSync(fileB, "utf8"); + assert.ok(logA.includes("session-A")); + assert.ok(!logA.includes("session-B"), "instance A must not record instance B's session"); + assert.ok(logB.includes("session-B")); + assert.ok(!logB.includes("session-A"), "instance B must not record instance A's session"); + assert.ok(!logA.includes(GH) && !logB.includes(GH2), "audit logs must not contain raw secrets"); + } finally { + cleanup(dirA); + cleanup(dirB); + } + }); +}); + +describe("independence — no execution-order dependency", () => { + test("execute.before does not mutate tool arguments", async () => { + const dir = tempDir(); + try { + const file = path.join(dir, "config.ts"); + fs.writeFileSync(file, `const key = "${AWS}";`); + const input = { filePath: file, content: `const key = "${AWS}";` }; + const before = JSON.stringify(input); + await handleLayer1({ tool: "read", sessionID: "s", input }, {}); + assert.strictEqual(JSON.stringify(input), before, "read-only hook must not mutate inputs"); + } finally { cleanup(dir); } + }); + + test("redaction does not depend on execute.after ordering", async () => { + // The context hook is the ordering-independent backstop: even if another + // plugin rewrites a tool result *after* Stranger Danger's after-hook, the + // secret is still removed from the assembled model request. + const event: any = { + tool: "bash", + sessionID: "s", + status: "completed", + result: { content: "clean" }, + }; + // Stranger Danger after-hook runs first on clean content... + await handleLayer3(event, {}); + // ...then a hypothetical companion replaces the result with a secret. + event.result.content = `leaked ${GH}`; + + const messages = [ + { role: "tool", content: [{ type: "tool-result", id: "1", name: "bash", result: { type: "text", value: event.result.content } }] }, + ]; + const contextEvent: any = { sessionID: "s", system: [], messages }; + await handleContext(contextEvent, {}); + assert.ok(!JSON.stringify(contextEvent).includes(GH), "context hook must redact regardless of after-hook order"); + }); + + test("after-hook redacts content already transformed by another plugin", async () => { + const event: any = { tool: "bash", sessionID: "s", status: "completed", result: { content: "prefix " + GH + " suffix" } }; + await handleLayer3(event, {}); + assert.ok(!event.result.content.includes(GH)); + }); +}); + +describe("independence — failure isolation", () => { + test("no registered hook throws on malformed events", async () => { + const dir = tempDir(); + try { + const { hooks } = await setup(dir); + const malformed: any[] = [undefined, null, {}, { result: null }, { tool: 123 }, { + sessionID: 42, system: "not-an-array", messages: [{ content: "not-an-array" }], + result: { content: 12345, output: Symbol("x"), metadata: null }, + prompt: { text: 99 }, + error: { message: { nested: true } }, + }]; + for (const [name, list] of Object.entries(hooks)) { + for (const fn of list) { + for (const ev of malformed) { + let threw = false; + try { await fn(ev); } catch { threw = true; } + assert.ok(!threw, `hook ${name} threw on malformed event: ${String(JSON.stringify(ev))}`); + } + } + } + } finally { cleanup(dir); } + }); + + test("a malformed event cannot stop the event subscription loop", () => { + let threw = false; + try { + handleMessageEvent({ type: "message.created", text: GH }, {}); + handleMessageEvent({ type: "message.created", part: { text: 123 } }, {}); + handleMessageEvent({ type: "message.updated", properties: null }, {}); + } catch { threw = true; } + assert.ok(!threw); + }); +}); + +describe("independence — reload and cleanup", () => { + test("dispose is safe to call and a fresh instance can be created afterwards", async () => { + const dir = tempDir(); + try { + const first = await setup(dir); + assert.strictEqual(typeof first.dispose, "function"); + await first.dispose!(); + // Idempotent-ish: calling again must not throw. + await first.dispose!(); + + const second = await setup(dir); + for (const h of second.hooks["session.prompt"]) { + const e: any = { sessionID: "s2", prompt: { text: GH } }; + await h(e); + assert.ok(!e.prompt.text.includes(GH)); + } + } finally { cleanup(dir); } + }); +}); + +describe("independence — companion-absent behavior", () => { + test("hooks run when events carry no companion fields", async () => { + const dir = tempDir(); + try { + const { hooks } = await setup(dir); + for (const h of hooks["execute.after"]) { + await h({ tool: "read", sessionID: "s", status: "completed", result: { content: AWS } }); + } + for (const h of hooks["session.context"]) { + await h({ sessionID: "s", system: [{ type: "text", text: AWS }], messages: [] }); + } + assert.ok(true); + } finally { cleanup(dir); } + }); + + test("standalone handlers work with an empty AuditContext (no shared state)", async () => { + // With no explicit audit path, the plugin layer must not write anywhere. + const e: any = { sessionID: "s", prompt: { text: GH } }; + await handlePrompt(e, {}); + await handleCompaction({ sessionID: "s", messages: [] } as any, {}); + assert.ok(!e.prompt.text.includes(GH)); + }); +}); From 9cfd8135429bffb2e5e32e97f23bec3f5b6277bd Mon Sep 17 00:00:00 2001 From: Evan Zellner Date: Fri, 16 Oct 2026 00:22:09 -0500 Subject: [PATCH 3/4] Close V2 request-coverage gaps and fix fail-open edge cases Security fixes: - Register session.generate and session.title hooks. session.context only runs for the agent loop; transient generation and title requests were transmitted uninspected. - Redact Tool.Error.metadata, which is surfaced to the model next to the error message. - Make structured redaction cycle-safe and fail closed past depth 32. A cyclic tool output previously overflowed the stack; the hook's catch then left the whole result unredacted (silent fail-open). - Fix the V2 event-stream audit: it filtered on type.startsWith("message"), but every V2 event is namespaced session.*, so it never audited anything. It now consumes the real event shapes and skips chatty *.delta frames. - Bound sd_scan input (8 MiB) so a model-invoked scan cannot exhaust memory. Packaging: - Ship a root index.js. OpenCode resolves a plugin directory to /index.{ts,js} and ignores main/exports, so directory configs previously failed with "Plugin entrypoint not found" and ran unprotected. Asserted by verify:package. Tests/docs: - Add tests/security-coverage.test.ts (aux request hooks, tool-error metadata, cycle/over-depth fail-closed, real V2 event detection, injected-limit bounds for the oversized/timeout branches, sd_scan cap, audit isolation). - Correct README/CHANGELOG coverage claims, per-request hook table, size limits, and WebSocket/HTTP-body/structured-value limitations. Full suite: 322 tests passing (306 baseline + 16 new); typecheck, lint, build, package validation, and an isolated OpenCode 2.0.21 runtime run all pass. --- CHANGELOG.md | 30 +++ README.md | 104 +++++++-- dist/engine/scanner.d.ts | 20 +- dist/engine/scanner.d.ts.map | 2 +- dist/engine/scanner.js | 19 +- dist/engine/scanner.js.map | 2 +- dist/plugin/index.d.ts | 13 +- dist/plugin/index.d.ts.map | 2 +- dist/plugin/index.js | 31 ++- dist/plugin/index.js.map | 2 +- dist/plugin/layer2-message.d.ts | 24 +- dist/plugin/layer2-message.d.ts.map | 2 +- dist/plugin/layer2-message.js | 58 ++++- dist/plugin/layer2-message.js.map | 2 +- dist/plugin/layer3.d.ts | 1 + dist/plugin/layer3.d.ts.map | 2 +- dist/plugin/layer3.js | 6 + dist/plugin/layer3.js.map | 2 +- dist/plugin/redact.d.ts | 25 +- dist/plugin/redact.d.ts.map | 2 +- dist/plugin/redact.js | 61 +++-- dist/plugin/redact.js.map | 2 +- dist/plugin/tools.d.ts | 9 +- dist/plugin/tools.d.ts.map | 2 +- dist/plugin/tools.js | 28 ++- dist/plugin/tools.js.map | 2 +- index.js | 13 ++ package.json | 3 +- scripts/verify-package.mjs | 11 +- src/engine/scanner.ts | 38 ++- src/plugin/index.ts | 34 ++- src/plugin/layer2-message.ts | 64 +++++- src/plugin/layer3.ts | 8 +- src/plugin/redact.ts | 63 ++++- src/plugin/tools.ts | 28 ++- tests/security-coverage.test.ts | 344 ++++++++++++++++++++++++++++ 36 files changed, 933 insertions(+), 126 deletions(-) create mode 100644 index.js create mode 100644 tests/security-coverage.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 5546697..50dec2d 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,23 @@ Security hardening and OpenCode V2 migration. Not yet published. `(?s:…)`), POSIX classes, and `\z` — which crashed `new RegExp` at import time and made the whole engine unusable — are translated to valid JavaScript. - **Performance.** Whole-content keyword prefilter; ~500× faster on large inputs. +- **Auxiliary requests are now covered.** `session.context` only runs for the + agent loop. The plugin now also registers `session.generate` (transient + `ctx.session.generate` requests) and `session.title` (title generation), which + had been sent uninspected. +- **Tool error metadata is redacted.** `Tool.Error.metadata` is surfaced to the + model alongside the message; it is now scanned like the rest of the result. +- **Structured redaction is cycle-safe and fails closed.** A self-referential + tool output previously overflowed the stack; the thrown error was swallowed by + the hook, leaving the *entire* result — including plain string secrets — + unredacted. Cyclic and over-deep (>32 levels) branches now fail closed with an + explicit marker instead. +- **V2 event-stream audit actually fires.** The handler filtered on + `type.startsWith("message")`, but every V2 event is namespaced `session.*` + (`session.text.ended`, `session.tool.success`, …), so it never audited anything. + It now consumes the real event shapes (skipping chatty `*.delta` frames). +- **`sd_scan` input is bounded** (8 MiB) so a model-invoked scan cannot exhaust + memory or hang the server. ### OpenCode V2 migration @@ -72,6 +89,19 @@ Security hardening and OpenCode V2 migration. Not yet published. - CI rewritten to run typecheck, lint, build, unit, integration, security, and package-validation steps on Node 22. Added `eslint.config.js` and `scripts/verify-package.mjs`. +- **Root `index.js` entrypoint.** OpenCode resolves a plugin *directory* to + `/index.{ts,js}` and ignores `main`/`exports`, so a + `{ "package": "/path/to/stranger-danger" }` entry previously failed to load with + `Plugin entrypoint not found` — the session ran unprotected. The package now + ships a root `index.js` (also published via `files`). +- Added `tests/security-coverage.test.ts` covering the fixes above: auxiliary + request hooks, tool-error metadata, cycle/over-depth fail-closed behavior, + real V2 event detection, injected-limit bounds for the oversized/timeout + branches, the `sd_scan` cap, and two-instance audit isolation. +- Verified on the real runtime (OpenCode `2.0.21`, isolated server + disposable + project + loopback mock provider): control run transmits the synthetic secret; + plugin run does not, emits a redaction token, and writes a secret-free audit + record. ## 0.1.0 (2026-05-08) diff --git a/README.md b/README.md index d5769e5..29575e7 100755 --- a/README.md +++ b/README.md @@ -20,10 +20,16 @@ strangerDanger registers only documented V2 hooks. Reading a file never rewrites | --- | --- | --- | | Incoming prompt | `session.prompt` | User prompts, before durable admission | | Tool results | `tool.execute.after` | `read`/`bash`/`grep`/`fetch` output before it reaches the model | -| Assembled request | `session.context` | System instructions, messages, tool-call inputs, tool results, auxiliary requests | +| Agent-loop request | `session.context` | System instructions, messages, tool-call inputs, tool results | +| Transient generation | `session.generate` | `ctx.session.generate` requests (not part of the agent loop) | +| Title generation | `session.title` | Title requests, plus a plugin-supplied title `result` | | Checkpoint compaction | `session.compaction` | The transcript being summarized, plus a plugin-provided summary | | File inspection | `tool.execute.before` | Read-only detection + audit; **never writes to disk** | -| Event stream | `event.subscribe` | Detection/audit only (observational; cannot prevent transmission) | +| Event stream | `event.subscribe` | Detection/audit only (observational, post-admission; cannot prevent transmission) | + +The four `session.*` model-request hooks are separate V2 request kinds. Registering all +four is what makes redaction ordering-independent: whatever path a request takes, its +assembled system prompt and messages are re-scanned immediately before dispatch. All outgoing content is scanned with a **bounded, chunk-safe** scanner: large outputs are fully covered (secrets that straddle a window boundary are still @@ -45,6 +51,26 @@ Add to your `opencode.jsonc` (`plugins`, not the V1 `plugin` key): The plugin requires the V2 runtime and `@opencode/plugin` (peer dependency). +#### Loading from a directory or a checkout + +OpenCode resolves a plugin **directory** to `/index.{ts,js}` and does +**not** consult `main` or `exports`. That is why this package ships a root +`index.js` in addition to `dist/`: it makes `{ "package": "/path/to/plugin" }` +and `"./plugins/stranger-danger"` entries load correctly. A published npm +package (the form above) resolves through `exports` as usual. + +If a plugin path is misconfigured, OpenCode logs `Plugin entrypoint not found` +and the plugin does **not** load — the session runs unprotected. After install, +confirm the plugin is active: + +```bash +# in an OpenCode session +> call the sd_status tool +``` + +`sd_status` reports the version, rule count, and the project audit-log path. If it +is unavailable, the plugin did not load. + ### Standalone CLI ```bash @@ -138,25 +164,55 @@ session-scoped; ignored by Git). ## Security guarantees and known limitations +Verified against OpenCode `2.0.21` / `@opencode/plugin` `2.0.29` on the real +runtime (isolated server, disposable project, loopback mock provider): a +synthetic secret in a prompt is transmitted by a control run and is **absent** +with the plugin loaded, while a redaction token and a secret-free audit record +are present. + Guaranteed: - Reading a file through the plugin **never modifies it** on disk. -- Tool results, assembled model requests, compaction transcripts, and prompts are - redacted before transmission through the session pipeline. -- No raw secret is emitted by the CLI or the audit log. +- Prompts, tool results, the agent-loop request (`session.context`), transient + `session.generate` requests, title requests, and compaction transcripts are + redacted before transmission on the supported request path. +- No raw secret is emitted by the CLI, the plugin tools, or the audit log. - `sd redact` writes atomically and creates **no** plaintext backup unless you explicitly ask for one. +- Hook callbacks never throw: a malformed or unexpected event cannot break the + runtime or a companion plugin. + +Not covered (documented, by design): + +- **Provider-native WebSocket traffic.** OpenCode exposes experimental + `experimental.ws.handshake` / `experimental.ws.send` / `experimental.ws.receive` + hooks, but this plugin does **not** register them. Providers that stream over a + WebSocket are not inspected. Plain-HTTP providers, and the HTTP fallback of a + WebSocket route, are covered by the `session.*` hooks. +- **Raw HTTP bodies.** This plugin edits assembled messages, not `http.request` + bodies. Any content a provider layer adds to a request that is not derived from + `system`/`messages` is not scanned. `event.subscribe` is observation only + (post-admission) and cannot prevent disclosure. +- **Structured values.** String leaves of plain objects/arrays are redacted. + Exotic objects (anything whose prototype is not `Object.prototype`/`null`, e.g. + `Map`, `Set`, `Date`, typed arrays) are passed through unchanged to avoid + corrupting non-plain data; content inside them is not inspected. Nesting deeper + than 32 levels, and cyclic structures, fail **closed** with an explicit marker + rather than being silently passed. +- **Encoded secrets.** Detection is pattern-based. Base64-encoded or otherwise + obfuscated secrets that do not match a rule are not detected by that rule (the + high-entropy detectors may still flag them). + +Size limits (all fail closed rather than truncating): + +| Surface | Limit | Behavior past the limit | +| --- | --- | --- | +| Bounded scanner (hooks, CLI) | 256 MiB (characters) | whole input replaced by the fail-closed marker | +| Per scan window | 1 MiB, 64 KiB overlap, 2.5 s budget | window timeout/error fails the scan closed | +| Event-stream text | 200,000 chars per field | field skipped | +| File pre-read inspection (`execute.before`) | 10 MiB | skipped; result/context hooks still cover it | +| `sd_scan` tool input | 8 MiB (characters) | refused with a message | -Not guaranteed (documented, by design): - -- Provider-native **WebSocket** traffic that bypasses the session HTTP pipeline - is not covered. -- The V2 event stream is observational (post-admission), so `event.subscribe` - cannot prevent disclosure; it only audits. -- Structured output/metadata is redacted by replacing string leaves; exotic - object types are left untouched to avoid corruption. -- Content larger than the scanner's hard ceiling fails closed with a marker - rather than being partially inspected. ## Independence & interoperability @@ -176,8 +232,8 @@ Checkpoint Orchestrator) but **requires neither**, and neither requires it. - **Works alone.** No companion plugin is needed to start or operate. There are no cross-plugin imports, no shared database, and no shared configuration. - **No required order.** Correctness never depends on another plugin running - before or after. The `execute.before` hook is read-only, and the - `session.context` hook is the ordering-independent backstop: it re-scans the + before or after. The `execute.before` hook is read-only, and the `session.*` + model-request hooks (`context`, `generate`, `title`, `compaction`) re-scan the fully assembled request, so even if a companion rewrites a tool result after this plugin's `execute.after` hook, the secret is still removed before the model request. @@ -197,9 +253,11 @@ Checkpoint Orchestrator) but **requires neither**, and neither requires it. | Hook | Mutates? | Purpose | | --- | --- | --- | | `tool.execute.before` | no (read-only) | Detect/audit secrets in files about to be read | -| `tool.execute.after` | redacts tool result text | Keep secrets out of the model context | +| `tool.execute.after` | redacts tool result text/output/metadata | Keep secrets out of the model context | | `session.prompt` | redacts prompt text | Pre-admission prompt protection | -| `session.context` | redacts system + messages | Ordering-independent request backstop | +| `session.context` | redacts system + messages | Ordering-independent agent-loop backstop | +| `session.generate` | redacts system + messages | Transient generation requests | +| `session.title` | redacts system + messages + `result` | Title generation | | `session.compaction` | redacts transcript/summary | Prevent secret propagation into checkpoints | | `event.subscribe` | no | Detection/audit only (observational) | @@ -233,7 +291,13 @@ integration is implemented that requires a companion to be present. ### Compatible OpenCode V2 versions Targets the V2 runtime and `@opencode/plugin >= 2.0.0` (tested against -`@opencode/plugin@2.0.29` and OpenCode `2.0.x`). +`@opencode/plugin@2.0.29` and OpenCode `2.0.21`). + +Note on plugin resolution: OpenCode distinguishes **package** entries (resolved +through `exports`/`main`) from **directory** entries (resolved to +`/index.{ts,js}`). This package ships a root `index.js` so both forms +load. The local plugin directory is *not* watched recursively — only the resolved +entrypoint and its imports are. ## Architecture diff --git a/dist/engine/scanner.d.ts b/dist/engine/scanner.d.ts index 04b92ba..4f14b23 100755 --- a/dist/engine/scanner.d.ts +++ b/dist/engine/scanner.d.ts @@ -27,6 +27,24 @@ export type BoundedScanResult = { /** Machine-readable reason when `complete` is false. */ policy?: "oversized" | "window-timeout" | "window-error"; }; +/** + * Overridable bounds for `scanAndRedactBounded`. Production callers use the + * defaults; tests inject small values so the oversized/timeout fail-closed + * branches can be exercised with a controlled payload instead of hundreds of + * megabytes of real input. + */ +export type BoundedScanOptions = { + /** Hard ceiling on input length (characters). */ + maxBytes?: number; + /** Characters scanned per window. */ + window?: number; + /** Characters of overlap between windows; must be < `window`. */ + overlap?: number; + /** Per-window time budget in milliseconds. */ + windowBudgetMs?: number; + /** Clock injection for deterministic timeout tests. */ + now?: () => number; +}; /** * Scan and redact arbitrarily large text without silently truncating it. * @@ -37,7 +55,7 @@ export type BoundedScanResult = { * or input beyond the hard ceiling), the result fails closed with an explicit * marker and `complete: false` rather than emitting uninspected content. */ -export declare function scanAndRedactBounded(content: string, source: Finding["source"], filePath?: string): BoundedScanResult; +export declare function scanAndRedactBounded(content: string, source: Finding["source"], filePath?: string, options?: BoundedScanOptions): BoundedScanResult; /** * Safe, machine-readable projection of a finding. Never includes the raw * matched secret, so it is safe to print, log, or return from a tool. diff --git a/dist/engine/scanner.d.ts.map b/dist/engine/scanner.d.ts.map index 1dcec5e..9ed48e6 100755 --- a/dist/engine/scanner.d.ts.map +++ b/dist/engine/scanner.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../../src/engine/scanner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AA4D1C;;GAEG;AACH,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,EAAE,CAgG7F;AAED;;;GAGG;AACH,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAgCnE;AAED;;GAEG;AACH,wBAAgB,aAAa,CACzB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,EACzB,QAAQ,CAAC,EAAE,MAAM,GAClB;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,EAAE,CAAA;CAAE,CAM3C;AAeD,mEAAmE;AACnE,eAAO,MAAM,sBAAsB,QAAoB,CAAC;AACxD,oEAAoE;AACpE,eAAO,MAAM,kBAAkB,oDAAoD,CAAC;AAEpF,MAAM,MAAM,iBAAiB,GAAG;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,+CAA+C;IAC/C,QAAQ,EAAE,OAAO,CAAC;IAClB,wDAAwD;IACxD,MAAM,CAAC,EAAE,WAAW,GAAG,gBAAgB,GAAG,cAAc,CAAC;CAC5D,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAChC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,EACzB,QAAQ,CAAC,EAAE,MAAM,GAClB,iBAAiB,CAiDnB;AAED;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC1B,oEAAoE;IACpE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gCAAgC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,oDAAoD;IACpD,SAAS,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,gEAAgE;AAChE,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,WAAW,CAkB7E"} \ No newline at end of file +{"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../../src/engine/scanner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AA4D1C;;GAEG;AACH,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,EAAE,CAgG7F;AAED;;;GAGG;AACH,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAgCnE;AAED;;GAEG;AACH,wBAAgB,aAAa,CACzB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,EACzB,QAAQ,CAAC,EAAE,MAAM,GAClB;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,EAAE,CAAA;CAAE,CAM3C;AAeD,mEAAmE;AACnE,eAAO,MAAM,sBAAsB,QAAoB,CAAC;AACxD,oEAAoE;AACpE,eAAO,MAAM,kBAAkB,oDAAoD,CAAC;AAEpF,MAAM,MAAM,iBAAiB,GAAG;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,+CAA+C;IAC/C,QAAQ,EAAE,OAAO,CAAC;IAClB,wDAAwD;IACxD,MAAM,CAAC,EAAE,WAAW,GAAG,gBAAgB,GAAG,cAAc,CAAC;CAC5D,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC7B,iDAAiD;IACjD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8CAA8C;IAC9C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uDAAuD;IACvD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACtB,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAChC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,EACzB,QAAQ,CAAC,EAAE,MAAM,EACjB,OAAO,GAAE,kBAAuB,GACjC,iBAAiB,CAuDnB;AAED;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC1B,oEAAoE;IACpE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gCAAgC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,oDAAoD;IACpD,SAAS,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,gEAAgE;AAChE,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,WAAW,CAkB7E"} \ No newline at end of file diff --git a/dist/engine/scanner.js b/dist/engine/scanner.js index 17b735b..822bef3 100755 --- a/dist/engine/scanner.js +++ b/dist/engine/scanner.js @@ -212,24 +212,29 @@ export const UNSCANNABLE_MARKER = "[REDACTED:strangerdanger-oversized-unscannabl * or input beyond the hard ceiling), the result fails closed with an explicit * marker and `complete: false` rather than emitting uninspected content. */ -export function scanAndRedactBounded(content, source, filePath) { +export function scanAndRedactBounded(content, source, filePath, options = {}) { + const windowSize = options.window ?? SCAN_WINDOW; + const overlap = Math.min(options.overlap ?? SCAN_OVERLAP, Math.max(0, windowSize - 1)); + const maxBytes = options.maxBytes ?? MAX_BOUNDED_SCAN_BYTES; + const budgetMs = options.windowBudgetMs ?? SCAN_WINDOW_BUDGET_MS; + const now = options.now ?? Date.now; if (content.length === 0) { return { redacted: content, findings: [], complete: true }; } - if (content.length <= SCAN_WINDOW) { + if (content.length <= windowSize) { const { redacted, findings } = scanAndRedact(content, source, filePath); return { redacted, findings, complete: true }; } - if (content.length > MAX_BOUNDED_SCAN_BYTES) { + if (content.length > maxBytes) { return { redacted: UNSCANNABLE_MARKER, findings: [], complete: false, policy: "oversized" }; } - const stride = SCAN_WINDOW - SCAN_OVERLAP; + const stride = windowSize - overlap; const collected = []; const seen = new Set(); for (let start = 0; start < content.length; start += stride) { - const end = Math.min(start + SCAN_WINDOW, content.length); + const end = Math.min(start + windowSize, content.length); const window = content.slice(start, end); - const windowStart = Date.now(); + const windowStart = now(); let windowFindings; try { windowFindings = scan(window, source, filePath); @@ -237,7 +242,7 @@ export function scanAndRedactBounded(content, source, filePath) { catch { return { redacted: UNSCANNABLE_MARKER, findings: collected, complete: false, policy: "window-error" }; } - if (Date.now() - windowStart > SCAN_WINDOW_BUDGET_MS) { + if (now() - windowStart > budgetMs) { return { redacted: UNSCANNABLE_MARKER, findings: collected, complete: false, policy: "window-timeout" }; } for (const f of windowFindings) { diff --git a/dist/engine/scanner.js.map b/dist/engine/scanner.js.map index f654c5e..274356d 100755 --- a/dist/engine/scanner.js.map +++ b/dist/engine/scanner.js.map @@ -1 +1 @@ -{"version":3,"file":"scanner.js","sourceRoot":"","sources":["../../src/engine/scanner.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"scanner.js","sourceRoot":"","sources":["../../src/engine/scanner.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/plugin/index.d.ts b/dist/plugin/index.d.ts index a2218b0..65f3eda 100755 --- a/dist/plugin/index.d.ts +++ b/dist/plugin/index.d.ts @@ -5,7 +5,9 @@ import { Plugin } from "@opencode/plugin"; * Protection boundaries (all supported V2 APIs): * - `session.prompt` → redact the incoming user prompt before admission. * - `tool.execute.after` → redact tool results (read/bash/grep/fetch/...). - * - `session.context` → redact the assembled model request (system + messages). + * - `session.context` → redact the assembled agent-loop model request. + * - `session.generate` → redact transient `session.generate` requests. + * - `session.title` → redact title-generation requests. * - `session.compaction` → redact the transcript being summarized. * - `tool.execute.before` → read-only detection/audit for file tools (never writes). * @@ -13,9 +15,12 @@ import { Plugin } from "@opencode/plugin"; * detection/audit only: the event stream is observational and cannot prevent an * already-admitted request from being transmitted. * - * Known limitation: provider-native WebSocket traffic that bypasses the - * session HTTP pipeline is not covered by these hooks. Everything routed - * through the session model-request pipeline is. + * Known limitations (documented in the README): + * - `http.request`/`http.response` bodies are not inspected; the message-level + * hooks above cover the supported request path. + * - Provider-native WebSocket traffic is not covered. OpenCode exposes + * experimental `experimental.ws.handshake/send/receive` hooks; registering + * them is deliberately out of scope. */ export declare const strangerDanger: Plugin.Plugin; export default strangerDanger; diff --git a/dist/plugin/index.d.ts.map b/dist/plugin/index.d.ts.map index a187afd..bb94204 100755 --- a/dist/plugin/index.d.ts.map +++ b/dist/plugin/index.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plugin/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAS1C;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,cAAc,eAiHzB,CAAC;AAEH,eAAe,cAAc,CAAC"} \ No newline at end of file +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plugin/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAS1C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,cAAc,eAoIzB,CAAC;AAEH,eAAe,cAAc,CAAC"} \ No newline at end of file diff --git a/dist/plugin/index.js b/dist/plugin/index.js index 24ea5da..d25e111 100755 --- a/dist/plugin/index.js +++ b/dist/plugin/index.js @@ -2,7 +2,7 @@ import { Plugin } from "@opencode/plugin"; import { resolveAuditPath } from "../engine/audit.js"; import { handleLayer1 } from "./layer1.js"; import { handleLayer3 } from "./layer3.js"; -import { handlePrompt, handleContext, handleMessageEvent } from "./layer2-message.js"; +import { handlePrompt, handleContext, handleGenerate, handleTitle, handleMessageEvent } from "./layer2-message.js"; import { handleCompaction } from "./layer2-compaction.js"; import { sdStatus, sdAudit, sdAllowlistAdd, sdAllowlistShow, sdScan } from "./tools.js"; /** @@ -11,7 +11,9 @@ import { sdStatus, sdAudit, sdAllowlistAdd, sdAllowlistShow, sdScan } from "./to * Protection boundaries (all supported V2 APIs): * - `session.prompt` → redact the incoming user prompt before admission. * - `tool.execute.after` → redact tool results (read/bash/grep/fetch/...). - * - `session.context` → redact the assembled model request (system + messages). + * - `session.context` → redact the assembled agent-loop model request. + * - `session.generate` → redact transient `session.generate` requests. + * - `session.title` → redact title-generation requests. * - `session.compaction` → redact the transcript being summarized. * - `tool.execute.before` → read-only detection/audit for file tools (never writes). * @@ -19,9 +21,12 @@ import { sdStatus, sdAudit, sdAllowlistAdd, sdAllowlistShow, sdScan } from "./to * detection/audit only: the event stream is observational and cannot prevent an * already-admitted request from being transmitted. * - * Known limitation: provider-native WebSocket traffic that bypasses the - * session HTTP pipeline is not covered by these hooks. Everything routed - * through the session model-request pipeline is. + * Known limitations (documented in the README): + * - `http.request`/`http.response` bodies are not inspected; the message-level + * hooks above cover the supported request path. + * - Provider-native WebSocket traffic is not covered. OpenCode exposes + * experimental `experimental.ws.handshake/send/receive` hooks; registering + * them is deliberately out of scope. */ export const strangerDanger = Plugin.define({ id: "gitdamnit.stranger-danger", @@ -112,6 +117,22 @@ export const strangerDanger = Plugin.define({ } catch { /* never throw from a hook */ } }); + // `context` only covers the agent loop. `generate` (transient + // `session.generate`) and `title` (title generation) are separate V2 + // request kinds with the same request shape; without these hooks their + // requests would be sent uninspected. + await ctx.session.hook("generate", (event) => { + try { + return handleGenerate({ sessionID: sid(event), system: event?.system, messages: event?.messages }, audit); + } + catch { /* never throw from a hook */ } + }); + await ctx.session.hook("title", (event) => { + try { + return handleTitle({ sessionID: sid(event), system: event?.system, messages: event?.messages, result: event?.result }, audit); + } + catch { /* never throw from a hook */ } + }); await ctx.session.hook("compaction", (event) => { try { return handleCompaction({ sessionID: sid(event), messages: event?.messages, result: event?.result }, audit); diff --git a/dist/plugin/index.js.map b/dist/plugin/index.js.map index 41ae066..5a4a715 100755 --- a/dist/plugin/index.js.map +++ b/dist/plugin/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/plugin/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/plugin/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/plugin/layer2-message.d.ts b/dist/plugin/layer2-message.d.ts index e2cfec8..215d578 100755 --- a/dist/plugin/layer2-message.d.ts +++ b/dist/plugin/layer2-message.d.ts @@ -31,11 +31,33 @@ export declare function handlePrompt(event: SessionPromptEvent, ctx: AuditContex * loop, including the already-redacted tool results. */ export declare function handleContext(event: SessionContextEvent, ctx: AuditContext): Promise; +export type SessionGenerateEvent = SessionContextEvent; +export type SessionTitleEvent = SessionContextEvent & { + /** When set, the plugin supplies the title and the model call is skipped. */ + result?: unknown; +}; +/** + * `ctx.session.hook("generate")` — transient `ctx.session.generate` requests. + * Same request shape as `context`, so the same redaction applies. Without this + * hook, text supplied to `session.generate` would be sent uninspected. + */ +export declare function handleGenerate(event: SessionGenerateEvent, ctx: AuditContext): Promise; +/** + * `ctx.session.hook("title")` — title generation. System/messages have the same + * shape as `context`; additionally redact an explicitly supplied `result` + * string so a title cannot persist a secret either. + */ +export declare function handleTitle(event: SessionTitleEvent, ctx: AuditContext): Promise; /** * Detection-only handler for the V2 event stream. Events are observational * (they arrive after admission/transmission), so this cannot prevent * disclosure; it records findings for the audit log. Prevention happens in the - * `prompt`, `context`, `compaction`, and `execute.after` hooks. + * `prompt`, `context`, `compaction`, `generate`, and `execute.after` hooks. + * + * V2 event types are namespaced `session.*` (for example `session.text.ended`, + * `session.tool.success`, `session.synthetic`). Per-token `*.delta` frames are + * skipped on purpose: they are chatty and the completed event carries the same + * text, so auditing them would only duplicate records. */ export declare function handleMessageEvent(event: unknown, ctx: AuditContext): void; export {}; diff --git a/dist/plugin/layer2-message.d.ts.map b/dist/plugin/layer2-message.d.ts.map index 7971147..19c5b88 100755 --- a/dist/plugin/layer2-message.d.ts.map +++ b/dist/plugin/layer2-message.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"layer2-message.d.ts","sourceRoot":"","sources":["../../src/plugin/layer2-message.ts"],"names":[],"mappings":"AACA,OAAO,EAAgD,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAC9F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAElD,0EAA0E;AAE1E,MAAM,MAAM,kBAAkB,GAAG;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF,KAAK,KAAK,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AA0CxD,wEAAwE;AACxE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,KAAK,GAAG,IAAI,CAS7F;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,KAAK,EAAE,kBAAkB,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ9F;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE,mBAAmB,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAchG;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,YAAY,GAAG,IAAI,CAqB1E"} \ No newline at end of file +{"version":3,"file":"layer2-message.d.ts","sourceRoot":"","sources":["../../src/plugin/layer2-message.ts"],"names":[],"mappings":"AACA,OAAO,EAAgD,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAC9F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAElD,0EAA0E;AAE1E,MAAM,MAAM,kBAAkB,GAAG;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF,KAAK,KAAK,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AA0CxD,wEAAwE;AACxE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,KAAK,GAAG,IAAI,CAS7F;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,KAAK,EAAE,kBAAkB,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ9F;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE,mBAAmB,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAchG;AAED,MAAM,MAAM,oBAAoB,GAAG,mBAAmB,CAAC;AAEvD,MAAM,MAAM,iBAAiB,GAAG,mBAAmB,GAAG;IAClD,6EAA6E;IAC7E,MAAM,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,oBAAoB,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAElG;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,KAAK,EAAE,iBAAiB,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAS5F;AAKD;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,YAAY,GAAG,IAAI,CA6B1E"} \ No newline at end of file diff --git a/dist/plugin/layer2-message.js b/dist/plugin/layer2-message.js index c0bdad9..47dd24c 100755 --- a/dist/plugin/layer2-message.js +++ b/dist/plugin/layer2-message.js @@ -95,28 +95,68 @@ export async function handleContext(event, ctx) { // Never break a model request. } } +/** + * `ctx.session.hook("generate")` — transient `ctx.session.generate` requests. + * Same request shape as `context`, so the same redaction applies. Without this + * hook, text supplied to `session.generate` would be sent uninspected. + */ +export async function handleGenerate(event, ctx) { + await handleContext(event, ctx); +} +/** + * `ctx.session.hook("title")` — title generation. System/messages have the same + * shape as `context`; additionally redact an explicitly supplied `result` + * string so a title cannot persist a secret either. + */ +export async function handleTitle(event, ctx) { + try { + await handleContext(event, ctx); + if (typeof event.result === "string") { + event.result = redactText(event.result, "context", withSession(ctx, event.sessionID)); + } + } + catch { + // Never break title generation. + } +} +/** Largest single text field scanned from an event (events are untrusted input). */ +const MAX_EVENT_TEXT = 200_000; /** * Detection-only handler for the V2 event stream. Events are observational * (they arrive after admission/transmission), so this cannot prevent * disclosure; it records findings for the audit log. Prevention happens in the - * `prompt`, `context`, `compaction`, and `execute.after` hooks. + * `prompt`, `context`, `compaction`, `generate`, and `execute.after` hooks. + * + * V2 event types are namespaced `session.*` (for example `session.text.ended`, + * `session.tool.success`, `session.synthetic`). Per-token `*.delta` frames are + * skipped on purpose: they are chatty and the completed event carries the same + * text, so auditing them would only duplicate records. */ export function handleMessageEvent(event, ctx) { try { const e = event; - const type = e && typeof e.type === "string" ? e.type : ""; - if (!type.startsWith("message")) + if (!e || typeof e !== "object") + return; + const type = typeof e.type === "string" ? e.type : ""; + if (!type.startsWith("session.") || type.endsWith(".delta")) + return; + const data = e.data; + if (!data || typeof data !== "object") return; const texts = []; const push = (v) => { - if (typeof v === "string" && v.length > 0 && v.length <= 200_000) + if (typeof v === "string" && v.length > 0 && v.length <= MAX_EVENT_TEXT) texts.push(v); }; - if (e?.part && typeof e.part === "object") - push(e.part.text); - if (e?.properties && typeof e.properties === "object") - push(e.properties.text); - push(e?.text); + push(data.text); + if (Array.isArray(data.content)) { + for (const c of data.content) { + if (c && typeof c === "object") + push(c.text); + } + } + if (data.error && typeof data.error === "object") + push(data.error.message); for (const text of texts) { const { findings } = scanAndRedact(text, "message"); recordFindings(findings, "message", ctx); diff --git a/dist/plugin/layer2-message.js.map b/dist/plugin/layer2-message.js.map index 3be2a02..67b9b95 100755 --- a/dist/plugin/layer2-message.js.map +++ b/dist/plugin/layer2-message.js.map @@ -1 +1 @@ -{"version":3,"file":"layer2-message.js","sourceRoot":"","sources":["../../src/plugin/layer2-message.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"layer2-message.js","sourceRoot":"","sources":["../../src/plugin/layer2-message.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/plugin/layer3.d.ts b/dist/plugin/layer3.d.ts index baff8b4..99a048a 100755 --- a/dist/plugin/layer3.d.ts +++ b/dist/plugin/layer3.d.ts @@ -12,6 +12,7 @@ export type Layer3Event = { error?: { message?: string; error?: unknown; + metadata?: unknown; }; }; /** diff --git a/dist/plugin/layer3.d.ts.map b/dist/plugin/layer3.d.ts.map index 28b80fe..84213f0 100755 --- a/dist/plugin/layer3.d.ts.map +++ b/dist/plugin/layer3.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"layer3.d.ts","sourceRoot":"","sources":["../../src/plugin/layer3.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgC,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAE9E,MAAM,MAAM,cAAc,GAAG;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,GAAG,MAAM,CAAC;IACxC,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,KAAK,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CACjD,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,YAAY,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAoCvF"} \ No newline at end of file +{"version":3,"file":"layer3.d.ts","sourceRoot":"","sources":["../../src/plugin/layer3.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgC,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAE9E,MAAM,MAAM,cAAc,GAAG;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,GAAG,MAAM,CAAC;IACxC,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,KAAK,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CACrE,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,YAAY,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA0CvF"} \ No newline at end of file diff --git a/dist/plugin/layer3.js b/dist/plugin/layer3.js index 8666e9d..6208752 100755 --- a/dist/plugin/layer3.js +++ b/dist/plugin/layer3.js @@ -24,6 +24,12 @@ export async function handleLayer3(event, ctx) { if (event.error && "error" in event.error) { event.error.error = redactStructured(event.error.error, "output", audit); } + // `Tool.Error.metadata` is a free-form record that is surfaced to the + // model alongside the message; redact it too so a secret in tool + // error metadata cannot reach the transcript. + if (event.error && event.error.metadata !== undefined) { + event.error.metadata = redactStructured(event.error.metadata, "output", audit); + } return; } const result = event.result; diff --git a/dist/plugin/layer3.js.map b/dist/plugin/layer3.js.map index 9dec162..85ba220 100755 --- a/dist/plugin/layer3.js.map +++ b/dist/plugin/layer3.js.map @@ -1 +1 @@ -{"version":3,"file":"layer3.js","sourceRoot":"","sources":["../../src/plugin/layer3.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAqB,MAAM,aAAa,CAAC;AAgB9E;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,KAAkB,EAAE,GAAiB;IACpE,IAAI,CAAC;QACD,MAAM,KAAK,GAAiB,EAAE,GAAG,GAAG,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC;QAEnE,IAAI,KAAK,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC;YAC3B,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,EAAE,OAAO,CAAC;YACrC,IAAI,OAAO,OAAO,KAAK,QAAQ;gBAAE,KAAK,CAAC,KAAM,CAAC,OAAO,GAAG,UAAU,CAAC,OAAO,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;YAC7F,IAAI,KAAK,CAAC,KAAK,IAAI,OAAO,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;gBACxC,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,gBAAgB,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;YAC7E,CAAC;YACD,OAAO;QACX,CAAC;QAED,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;QAC5B,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ;YAAE,OAAO;QAElD,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;YACrC,MAAM,CAAC,OAAO,GAAG,UAAU,CAAC,MAAM,CAAC,OAAO,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;QACjE,CAAC;aAAM,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;YACvC,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;gBAChC,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;oBACnC,MAAM,CAAC,GAAG,IAA0C,CAAC;oBACrD,IAAI,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ;wBAAE,CAAC,CAAC,IAAI,GAAG,UAAU,CAAC,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;gBACjF,CAAC;YACL,CAAC;QACL,CAAC;QAED,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;YAC9B,MAAM,CAAC,MAAM,GAAG,gBAAgB,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;QACrE,CAAC;QACD,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;YAChC,MAAM,CAAC,QAAQ,GAAG,gBAAgB,CAAC,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;QACzE,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACL,gDAAgD;IACpD,CAAC;AACL,CAAC"} \ No newline at end of file +{"version":3,"file":"layer3.js","sourceRoot":"","sources":["../../src/plugin/layer3.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAqB,MAAM,aAAa,CAAC;AAgB9E;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,KAAkB,EAAE,GAAiB;IACpE,IAAI,CAAC;QACD,MAAM,KAAK,GAAiB,EAAE,GAAG,GAAG,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC;QAEnE,IAAI,KAAK,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC;YAC3B,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,EAAE,OAAO,CAAC;YACrC,IAAI,OAAO,OAAO,KAAK,QAAQ;gBAAE,KAAK,CAAC,KAAM,CAAC,OAAO,GAAG,UAAU,CAAC,OAAO,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;YAC7F,IAAI,KAAK,CAAC,KAAK,IAAI,OAAO,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;gBACxC,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,gBAAgB,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;YAC7E,CAAC;YACD,sEAAsE;YACtE,iEAAiE;YACjE,8CAA8C;YAC9C,IAAI,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,KAAK,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;gBACpD,KAAK,CAAC,KAAK,CAAC,QAAQ,GAAG,gBAAgB,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;YACnF,CAAC;YACD,OAAO;QACX,CAAC;QAED,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;QAC5B,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ;YAAE,OAAO;QAElD,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;YACrC,MAAM,CAAC,OAAO,GAAG,UAAU,CAAC,MAAM,CAAC,OAAO,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;QACjE,CAAC;aAAM,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;YACvC,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;gBAChC,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;oBACnC,MAAM,CAAC,GAAG,IAA0C,CAAC;oBACrD,IAAI,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ;wBAAE,CAAC,CAAC,IAAI,GAAG,UAAU,CAAC,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;gBACjF,CAAC;YACL,CAAC;QACL,CAAC;QAED,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;YAC9B,MAAM,CAAC,MAAM,GAAG,gBAAgB,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;QACrE,CAAC;QACD,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;YAChC,MAAM,CAAC,QAAQ,GAAG,gBAAgB,CAAC,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;QACzE,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACL,gDAAgD;IACpD,CAAC;AACL,CAAC"} \ No newline at end of file diff --git a/dist/plugin/redact.d.ts b/dist/plugin/redact.d.ts index 2f8da31..ea7c1a0 100644 --- a/dist/plugin/redact.d.ts +++ b/dist/plugin/redact.d.ts @@ -24,10 +24,25 @@ export declare function recordFindings(findings: Finding[], source: Finding["sou export declare function redactText(text: string, source: Finding["source"], ctx: AuditContext, filePath?: string): string; /** * Redact string leaves of a structured value (tool output, metadata, message - * part inputs/results) while preserving its shape and types. Exotic objects are - * left untouched so we never corrupt non-plain data. + * part inputs/results) while preserving its shape and types. + * + * Fail-safe behavior: + * - A cyclic structure is detected with a `seen` set instead of recursing + * until the stack overflows. (Before this guard, a self-referential object + * threw a `RangeError`, the surrounding hook caught it, and *every* string + * in the result was left unredacted — a silent fail-open.) + * - A value that cannot be safely inspected (beyond `MAX_STRUCTURED_DEPTH`) + * is replaced with the explicit fail-closed marker, never passed through + * silently. + * - Exotic objects (anything whose prototype is not `Object.prototype`/`null`) + * are left untouched so non-plain data is never corrupted; this residual + * limit is documented in the README. + */ +export declare function redactStructured(value: unknown, source: Finding["source"], ctx: AuditContext, depth?: number, seen?: WeakSet): unknown; +/** + * Redact in place, replacing only string leaves of a plain object/array. + * Cycle-safe; a cyclic or over-deep branch is left untouched (this helper is a + * best-effort in-place variant and cannot fail closed by substituting a value). */ -export declare function redactStructured(value: unknown, source: Finding["source"], ctx: AuditContext, depth?: number): unknown; -/** Redact in place, replacing only string leaves of a plain object/array. */ -export declare function redactStructuredInPlace(value: unknown, source: Finding["source"], ctx: AuditContext, depth?: number): void; +export declare function redactStructuredInPlace(value: unknown, source: Finding["source"], ctx: AuditContext, depth?: number, seen?: WeakSet): void; //# sourceMappingURL=redact.d.ts.map \ No newline at end of file diff --git a/dist/plugin/redact.d.ts.map b/dist/plugin/redact.d.ts.map index 4ffbbad..386573f 100644 --- a/dist/plugin/redact.d.ts.map +++ b/dist/plugin/redact.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"redact.d.ts","sourceRoot":"","sources":["../../src/plugin/redact.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAIlD;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG;IACvB,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yDAAyD;IACzD,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC1B,QAAQ,EAAE,OAAO,EAAE,EACnB,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,EACzB,GAAG,EAAE,YAAY,EACjB,QAAQ,CAAC,EAAE,MAAM,GAClB,IAAI,CAMN;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACtB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,EACzB,GAAG,EAAE,YAAY,EACjB,QAAQ,CAAC,EAAE,MAAM,GAClB,MAAM,CASR;AAKD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC5B,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,EACzB,GAAG,EAAE,YAAY,EACjB,KAAK,SAAI,GACV,OAAO,CAkBT;AAED,6EAA6E;AAC7E,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,YAAY,EAAE,KAAK,SAAI,GAAG,IAAI,CAgBrH"} \ No newline at end of file +{"version":3,"file":"redact.d.ts","sourceRoot":"","sources":["../../src/plugin/redact.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAIlD;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG;IACvB,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yDAAyD;IACzD,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC1B,QAAQ,EAAE,OAAO,EAAE,EACnB,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,EACzB,GAAG,EAAE,YAAY,EACjB,QAAQ,CAAC,EAAE,MAAM,GAClB,IAAI,CAMN;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACtB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,EACzB,GAAG,EAAE,YAAY,EACjB,QAAQ,CAAC,EAAE,MAAM,GAClB,MAAM,CASR;AASD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAC5B,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,EACzB,GAAG,EAAE,YAAY,EACjB,KAAK,SAAI,EACT,IAAI,GAAE,OAAO,CAAC,MAAM,CAAiB,GACtC,OAAO,CAyBT;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CACnC,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,EACzB,GAAG,EAAE,YAAY,EACjB,KAAK,SAAI,EACT,IAAI,GAAE,OAAO,CAAC,MAAM,CAAiB,GACtC,IAAI,CAsBN"} \ No newline at end of file diff --git a/dist/plugin/redact.js b/dist/plugin/redact.js index 384bbef..b39a34b 100644 --- a/dist/plugin/redact.js +++ b/dist/plugin/redact.js @@ -1,4 +1,4 @@ -import { scanAndRedactBounded } from "../engine/scanner.js"; +import { scanAndRedactBounded, UNSCANNABLE_MARKER } from "../engine/scanner.js"; import { writeAuditBatch, auditEntryFromFinding } from "../engine/audit.js"; /** * Record findings as secret-free audit entries. @@ -27,55 +27,90 @@ export function redactText(text, source, ctx, filePath) { } return redacted; } -/** Maximum nesting depth traversed when redacting structured values. */ -const MAX_STRUCTURED_DEPTH = 8; +/** + * Maximum nesting depth traversed when redacting structured values. Plain JSON + * produced by tools is shallow; anything deeper than this is treated as + * unexpected and fails closed (below) rather than being walked further. + */ +const MAX_STRUCTURED_DEPTH = 32; /** * Redact string leaves of a structured value (tool output, metadata, message - * part inputs/results) while preserving its shape and types. Exotic objects are - * left untouched so we never corrupt non-plain data. + * part inputs/results) while preserving its shape and types. + * + * Fail-safe behavior: + * - A cyclic structure is detected with a `seen` set instead of recursing + * until the stack overflows. (Before this guard, a self-referential object + * threw a `RangeError`, the surrounding hook caught it, and *every* string + * in the result was left unredacted — a silent fail-open.) + * - A value that cannot be safely inspected (beyond `MAX_STRUCTURED_DEPTH`) + * is replaced with the explicit fail-closed marker, never passed through + * silently. + * - Exotic objects (anything whose prototype is not `Object.prototype`/`null`) + * are left untouched so non-plain data is never corrupted; this residual + * limit is documented in the README. */ -export function redactStructured(value, source, ctx, depth = 0) { +export function redactStructured(value, source, ctx, depth = 0, seen = new WeakSet()) { if (typeof value === "string") return redactText(value, source, ctx); - if (value === null || typeof value !== "object" || depth >= MAX_STRUCTURED_DEPTH) + if (value === null || typeof value !== "object") return value; + if (depth >= MAX_STRUCTURED_DEPTH) + return UNSCANNABLE_MARKER; if (Array.isArray(value)) { + if (seen.has(value)) + return UNSCANNABLE_MARKER; + seen.add(value); const out = new Array(value.length); for (let i = 0; i < value.length; i++) - out[i] = redactStructured(value[i], source, ctx, depth + 1); + out[i] = redactStructured(value[i], source, ctx, depth + 1, seen); + seen.delete(value); return out; } const proto = Object.getPrototypeOf(value); if (proto !== Object.prototype && proto !== null) return value; + if (seen.has(value)) + return UNSCANNABLE_MARKER; + seen.add(value); const out = {}; for (const [k, v] of Object.entries(value)) { - out[k] = redactStructured(v, source, ctx, depth + 1); + out[k] = redactStructured(v, source, ctx, depth + 1, seen); } + seen.delete(value); return out; } -/** Redact in place, replacing only string leaves of a plain object/array. */ -export function redactStructuredInPlace(value, source, ctx, depth = 0) { +/** + * Redact in place, replacing only string leaves of a plain object/array. + * Cycle-safe; a cyclic or over-deep branch is left untouched (this helper is a + * best-effort in-place variant and cannot fail closed by substituting a value). + */ +export function redactStructuredInPlace(value, source, ctx, depth = 0, seen = new WeakSet()) { if (value === null || typeof value !== "object" || depth >= MAX_STRUCTURED_DEPTH) return; + if (seen.has(value)) + return; if (Array.isArray(value)) { + seen.add(value); for (let i = 0; i < value.length; i++) { const v = value[i]; if (typeof v === "string") value[i] = redactText(v, source, ctx); else - redactStructuredInPlace(v, source, ctx, depth + 1); + redactStructuredInPlace(v, source, ctx, depth + 1, seen); } + seen.delete(value); return; } const proto = Object.getPrototypeOf(value); if (proto !== Object.prototype && proto !== null) return; + seen.add(value); for (const [k, v] of Object.entries(value)) { if (typeof v === "string") value[k] = redactText(v, source, ctx); else - redactStructuredInPlace(v, source, ctx, depth + 1); + redactStructuredInPlace(v, source, ctx, depth + 1, seen); } + seen.delete(value); } //# sourceMappingURL=redact.js.map \ No newline at end of file diff --git a/dist/plugin/redact.js.map b/dist/plugin/redact.js.map index a2b5484..83e7046 100644 --- a/dist/plugin/redact.js.map +++ b/dist/plugin/redact.js.map @@ -1 +1 @@ -{"version":3,"file":"redact.js","sourceRoot":"","sources":["../../src/plugin/redact.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"redact.js","sourceRoot":"","sources":["../../src/plugin/redact.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/plugin/tools.d.ts b/dist/plugin/tools.d.ts index 54d6f38..fdba300 100644 --- a/dist/plugin/tools.d.ts +++ b/dist/plugin/tools.d.ts @@ -19,7 +19,14 @@ export declare function sdAllowlistAdd(args: { path?: string; regex?: string; }): string; -/** `sd_scan` — returns secret-free metadata only. */ +/** Upper bound on content accepted by `sd_scan`. Keeps a model-invoked scan bounded. */ +export declare const MAX_SD_SCAN_CHARS: number; +/** + * `sd_scan` — returns secret-free metadata only. + * + * The input is bounded and scanned with the same chunk-safe path as the hooks, + * so a very large `content` argument cannot exhaust memory or hang the server. + */ export declare function sdScan(args: { content: string; source?: Finding["source"]; diff --git a/dist/plugin/tools.d.ts.map b/dist/plugin/tools.d.ts.map index 7ee8c4a..941fb7c 100644 --- a/dist/plugin/tools.d.ts.map +++ b/dist/plugin/tools.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../src/plugin/tools.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAkB,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAElE,2CAA2C;AAC3C,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,MAAM,MAAM,QAAQ,GAAG;IACnB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,kBAAkB;AAClB,wBAAgB,QAAQ,CAAC,IAAI,GAAE,QAAa,GAAG,MAAM,CASpD;AAED,iBAAiB;AACjB,wBAAgB,OAAO,CAAC,IAAI,GAAE,QAAa,GAAG,MAAM,CAqBnD;AAED,0BAA0B;AAC1B,wBAAgB,eAAe,IAAI,MAAM,CAQxC;AAED,yBAAyB;AACzB,wBAAgB,cAAc,CAAC,IAAI,EAAE;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,MAAM,CAwBT;AAED,qDAAqD;AACrD,wBAAgB,MAAM,CAAC,IAAI,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAA;CAAE,GAAG,MAAM,CAUpF"} \ No newline at end of file +{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../src/plugin/tools.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAkB,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAElE,2CAA2C;AAC3C,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,MAAM,MAAM,QAAQ,GAAG;IACnB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,kBAAkB;AAClB,wBAAgB,QAAQ,CAAC,IAAI,GAAE,QAAa,GAAG,MAAM,CASpD;AAED,iBAAiB;AACjB,wBAAgB,OAAO,CAAC,IAAI,GAAE,QAAa,GAAG,MAAM,CAqBnD;AAED,0BAA0B;AAC1B,wBAAgB,eAAe,IAAI,MAAM,CAQxC;AAED,yBAAyB;AACzB,wBAAgB,cAAc,CAAC,IAAI,EAAE;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,MAAM,CAwBT;AAED,wFAAwF;AACxF,eAAO,MAAM,iBAAiB,QAAkB,CAAC;AAEjD;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAA;CAAE,GAAG,MAAM,CAgBpF"} \ No newline at end of file diff --git a/dist/plugin/tools.js b/dist/plugin/tools.js index 7eb8f87..ffa27f1 100644 --- a/dist/plugin/tools.js +++ b/dist/plugin/tools.js @@ -1,4 +1,4 @@ -import { scanAndRedact, toSafeFinding } from "../engine/scanner.js"; +import { scanAndRedactBounded, toSafeFinding } from "../engine/scanner.js"; import { readAuditLog, resolveAuditPath } from "../engine/audit.js"; import { loadAllowlist, saveAllowlistEntry } from "../engine/allowlist.js"; import { NAMED_RULES, ENTROPY_RULES, CLEAN_ROOM_RULES } from "../engine/rules.js"; @@ -84,17 +84,31 @@ export function sdAllowlistAdd(args) { saveAllowlistEntry(entry); return "Allowlist entry added."; } -/** `sd_scan` — returns secret-free metadata only. */ +/** Upper bound on content accepted by `sd_scan`. Keeps a model-invoked scan bounded. */ +export const MAX_SD_SCAN_CHARS = 8 * 1024 * 1024; +/** + * `sd_scan` — returns secret-free metadata only. + * + * The input is bounded and scanned with the same chunk-safe path as the hooks, + * so a very large `content` argument cannot exhaust memory or hang the server. + */ export function sdScan(args) { - const source = args.source ?? "file"; - const { findings } = scanAndRedact(args.content, source); - if (findings.length === 0) + const source = args?.source ?? "file"; + const content = typeof args?.content === "string" ? args.content : ""; + if (content.length === 0) return "No secrets detected."; + if (content.length > MAX_SD_SCAN_CHARS) { + return `Content too large to scan: ${content.length} characters exceeds the ${MAX_SD_SCAN_CHARS}-character limit.`; + } + const { findings, complete, policy } = scanAndRedactBounded(content, source); + const note = complete ? "" : `\n\n[strangerdanger: content not fully inspected (${policy})]`; + if (findings.length === 0) + return `No secrets detected.${note}`; return findings .map((f) => { - const safe = toSafeFinding(f, args.content); + const safe = toSafeFinding(f, content); return `${safe.ruleId}: ${safe.description}\n Location: line ${safe.line ?? "?"}, column ${(safe.column ?? 0) + 1}\n Fingerprint: ${safe.fingerprint}\n Redaction: ${safe.redaction}`; }) - .join("\n\n"); + .join("\n\n") + note; } //# sourceMappingURL=tools.js.map \ No newline at end of file diff --git a/dist/plugin/tools.js.map b/dist/plugin/tools.js.map index 2c87ae2..87f1d4d 100644 --- a/dist/plugin/tools.js.map +++ b/dist/plugin/tools.js.map @@ -1 +1 @@ -{"version":3,"file":"tools.js","sourceRoot":"","sources":["../../src/plugin/tools.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"tools.js","sourceRoot":"","sources":["../../src/plugin/tools.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..94b079c --- /dev/null +++ b/index.js @@ -0,0 +1,13 @@ +/** + * Root plugin entrypoint. + * + * OpenCode resolves a plugin *directory* (for example `{ "package": "/path/to/plugin" }` + * or `"./plugins/local"`) to `/index.{ts,js}` — it does not consult + * `main` or `exports`. Without a root entrypoint, a directory-path configuration + * silently fails to load the plugin ("Plugin entrypoint not found"), leaving the + * user unprotected while their config claims otherwise. + * + * npm consumers resolve through `exports` (see package.json), so this file only + * affects directory/path loading. Both paths expose the same default export. + */ +export { default } from "./dist/plugin/index.js"; diff --git a/package.json b/package.json index 56fabbe..918212b 100755 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "sd": "./dist/cli/index.js" }, "files": [ + "index.js", "dist/", "rules/gitleaks-rules.toml", ".strangerdanger-allowlist.toml", @@ -29,7 +30,7 @@ "test": "tsx --test tests/*.test.ts tests/*/*.test.ts", "test:unit": "tsx --test tests/engine/*.test.ts tests/cli/*.test.ts tests/plugin/*.test.ts", "test:integration": "tsx --test tests/integration.test.ts", - "test:security": "tsx --test tests/hardening.test.ts tests/pentest.test.ts", + "test:security": "tsx --test tests/hardening.test.ts tests/pentest.test.ts tests/security-coverage.test.ts", "pretest": "npm run build:rules", "verify:package": "node scripts/verify-package.mjs", "prepublishOnly": "npm run typecheck && npm run lint && npm test && npm run build" diff --git a/scripts/verify-package.mjs b/scripts/verify-package.mjs index 222568e..5475eb2 100644 --- a/scripts/verify-package.mjs +++ b/scripts/verify-package.mjs @@ -6,7 +6,7 @@ * `bin`, and runtime dependencies are correct — not just the source tree. */ import { execFileSync } from "node:child_process"; -import { mkdtempSync, rmSync, readdirSync, existsSync } from "node:fs"; +import { mkdtempSync, rmSync, readdirSync, readFileSync, existsSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; @@ -73,6 +73,15 @@ try { ], { cwd: workdir }); }); + // OpenCode resolves a plugin *directory* to /index.{ts,js} and ignores + // main/exports, so the shipped package must include a root entrypoint. + step("published package includes the root directory entrypoint", () => { + const entry = join(workdir, "node_modules", "@gitdamnit", "stranger-danger", "index.js"); + if (!existsSync(entry)) throw new Error("package is missing root index.js (directory installs would not load)"); + const src = readFileSync(entry, "utf8"); + if (!/export\s*\{\s*default\s*\}/.test(src)) throw new Error("root index.js does not export a default plugin"); + }); + // Clean up the tarball produced by pack. try { rmSync(tarballPath, { force: true }); } catch { /* ignore */ } diff --git a/src/engine/scanner.ts b/src/engine/scanner.ts index c19418c..fef5616 100755 --- a/src/engine/scanner.ts +++ b/src/engine/scanner.ts @@ -239,6 +239,25 @@ export type BoundedScanResult = { policy?: "oversized" | "window-timeout" | "window-error"; }; +/** + * Overridable bounds for `scanAndRedactBounded`. Production callers use the + * defaults; tests inject small values so the oversized/timeout fail-closed + * branches can be exercised with a controlled payload instead of hundreds of + * megabytes of real input. + */ +export type BoundedScanOptions = { + /** Hard ceiling on input length (characters). */ + maxBytes?: number; + /** Characters scanned per window. */ + window?: number; + /** Characters of overlap between windows; must be < `window`. */ + overlap?: number; + /** Per-window time budget in milliseconds. */ + windowBudgetMs?: number; + /** Clock injection for deterministic timeout tests. */ + now?: () => number; +}; + /** * Scan and redact arbitrarily large text without silently truncating it. * @@ -253,28 +272,35 @@ export function scanAndRedactBounded( content: string, source: Finding["source"], filePath?: string, + options: BoundedScanOptions = {}, ): BoundedScanResult { + const windowSize = options.window ?? SCAN_WINDOW; + const overlap = Math.min(options.overlap ?? SCAN_OVERLAP, Math.max(0, windowSize - 1)); + const maxBytes = options.maxBytes ?? MAX_BOUNDED_SCAN_BYTES; + const budgetMs = options.windowBudgetMs ?? SCAN_WINDOW_BUDGET_MS; + const now = options.now ?? Date.now; + if (content.length === 0) { return { redacted: content, findings: [], complete: true }; } - if (content.length <= SCAN_WINDOW) { + if (content.length <= windowSize) { const { redacted, findings } = scanAndRedact(content, source, filePath); return { redacted, findings, complete: true }; } - if (content.length > MAX_BOUNDED_SCAN_BYTES) { + if (content.length > maxBytes) { return { redacted: UNSCANNABLE_MARKER, findings: [], complete: false, policy: "oversized" }; } - const stride = SCAN_WINDOW - SCAN_OVERLAP; + const stride = windowSize - overlap; const collected: Finding[] = []; const seen = new Set(); for (let start = 0; start < content.length; start += stride) { - const end = Math.min(start + SCAN_WINDOW, content.length); + const end = Math.min(start + windowSize, content.length); const window = content.slice(start, end); - const windowStart = Date.now(); + const windowStart = now(); let windowFindings: Finding[]; try { @@ -283,7 +309,7 @@ export function scanAndRedactBounded( return { redacted: UNSCANNABLE_MARKER, findings: collected, complete: false, policy: "window-error" }; } - if (Date.now() - windowStart > SCAN_WINDOW_BUDGET_MS) { + if (now() - windowStart > budgetMs) { return { redacted: UNSCANNABLE_MARKER, findings: collected, complete: false, policy: "window-timeout" }; } diff --git a/src/plugin/index.ts b/src/plugin/index.ts index cacc673..a11a528 100755 --- a/src/plugin/index.ts +++ b/src/plugin/index.ts @@ -2,7 +2,7 @@ import { Plugin } from "@opencode/plugin"; import { resolveAuditPath } from "../engine/audit.js"; import { handleLayer1 } from "./layer1.js"; import { handleLayer3 } from "./layer3.js"; -import { handlePrompt, handleContext, handleMessageEvent } from "./layer2-message.js"; +import { handlePrompt, handleContext, handleGenerate, handleTitle, handleMessageEvent } from "./layer2-message.js"; import { handleCompaction } from "./layer2-compaction.js"; import { sdStatus, sdAudit, sdAllowlistAdd, sdAllowlistShow, sdScan } from "./tools.js"; import type { AuditContext } from "./redact.js"; @@ -13,7 +13,9 @@ import type { AuditContext } from "./redact.js"; * Protection boundaries (all supported V2 APIs): * - `session.prompt` → redact the incoming user prompt before admission. * - `tool.execute.after` → redact tool results (read/bash/grep/fetch/...). - * - `session.context` → redact the assembled model request (system + messages). + * - `session.context` → redact the assembled agent-loop model request. + * - `session.generate` → redact transient `session.generate` requests. + * - `session.title` → redact title-generation requests. * - `session.compaction` → redact the transcript being summarized. * - `tool.execute.before` → read-only detection/audit for file tools (never writes). * @@ -21,9 +23,12 @@ import type { AuditContext } from "./redact.js"; * detection/audit only: the event stream is observational and cannot prevent an * already-admitted request from being transmitted. * - * Known limitation: provider-native WebSocket traffic that bypasses the - * session HTTP pipeline is not covered by these hooks. Everything routed - * through the session model-request pipeline is. + * Known limitations (documented in the README): + * - `http.request`/`http.response` bodies are not inspected; the message-level + * hooks above cover the supported request path. + * - Provider-native WebSocket traffic is not covered. OpenCode exposes + * experimental `experimental.ws.handshake/send/receive` hooks; registering + * them is deliberately out of scope. */ export const strangerDanger = Plugin.define({ id: "gitdamnit.stranger-danger", @@ -118,6 +123,25 @@ export const strangerDanger = Plugin.define({ } catch { /* never throw from a hook */ } }); + // `context` only covers the agent loop. `generate` (transient + // `session.generate`) and `title` (title generation) are separate V2 + // request kinds with the same request shape; without these hooks their + // requests would be sent uninspected. + await ctx.session.hook("generate", (event) => { + try { + return handleGenerate({ sessionID: sid(event), system: (event as any)?.system, messages: (event as any)?.messages }, audit); + } catch { /* never throw from a hook */ } + }); + + await ctx.session.hook("title", (event) => { + try { + return handleTitle( + { sessionID: sid(event), system: (event as any)?.system, messages: (event as any)?.messages, result: (event as any)?.result }, + audit, + ); + } catch { /* never throw from a hook */ } + }); + await ctx.session.hook("compaction", (event) => { try { return handleCompaction({ sessionID: sid(event), messages: (event as any)?.messages, result: (event as any)?.result }, audit); diff --git a/src/plugin/layer2-message.ts b/src/plugin/layer2-message.ts index 834c42b..6ce3965 100755 --- a/src/plugin/layer2-message.ts +++ b/src/plugin/layer2-message.ts @@ -106,25 +106,73 @@ export async function handleContext(event: SessionContextEvent, ctx: AuditContex } } +export type SessionGenerateEvent = SessionContextEvent; + +export type SessionTitleEvent = SessionContextEvent & { + /** When set, the plugin supplies the title and the model call is skipped. */ + result?: unknown; +}; + +/** + * `ctx.session.hook("generate")` — transient `ctx.session.generate` requests. + * Same request shape as `context`, so the same redaction applies. Without this + * hook, text supplied to `session.generate` would be sent uninspected. + */ +export async function handleGenerate(event: SessionGenerateEvent, ctx: AuditContext): Promise { + await handleContext(event, ctx); +} + +/** + * `ctx.session.hook("title")` — title generation. System/messages have the same + * shape as `context`; additionally redact an explicitly supplied `result` + * string so a title cannot persist a secret either. + */ +export async function handleTitle(event: SessionTitleEvent, ctx: AuditContext): Promise { + try { + await handleContext(event, ctx); + if (typeof event.result === "string") { + event.result = redactText(event.result, "context", withSession(ctx, event.sessionID)); + } + } catch { + // Never break title generation. + } +} + +/** Largest single text field scanned from an event (events are untrusted input). */ +const MAX_EVENT_TEXT = 200_000; + /** * Detection-only handler for the V2 event stream. Events are observational * (they arrive after admission/transmission), so this cannot prevent * disclosure; it records findings for the audit log. Prevention happens in the - * `prompt`, `context`, `compaction`, and `execute.after` hooks. + * `prompt`, `context`, `compaction`, `generate`, and `execute.after` hooks. + * + * V2 event types are namespaced `session.*` (for example `session.text.ended`, + * `session.tool.success`, `session.synthetic`). Per-token `*.delta` frames are + * skipped on purpose: they are chatty and the completed event carries the same + * text, so auditing them would only duplicate records. */ export function handleMessageEvent(event: unknown, ctx: AuditContext): void { try { - const e = event as Record | undefined; - const type = e && typeof e.type === "string" ? e.type : ""; - if (!type.startsWith("message")) return; + const e = event as Record | null | undefined; + if (!e || typeof e !== "object") return; + const type = typeof e.type === "string" ? e.type : ""; + if (!type.startsWith("session.") || type.endsWith(".delta")) return; + + const data = e.data; + if (!data || typeof data !== "object") return; const texts: string[] = []; const push = (v: unknown): void => { - if (typeof v === "string" && v.length > 0 && v.length <= 200_000) texts.push(v); + if (typeof v === "string" && v.length > 0 && v.length <= MAX_EVENT_TEXT) texts.push(v); }; - if (e?.part && typeof e.part === "object") push(e.part.text); - if (e?.properties && typeof e.properties === "object") push(e.properties.text); - push(e?.text); + push(data.text); + if (Array.isArray(data.content)) { + for (const c of data.content) { + if (c && typeof c === "object") push((c as Record).text); + } + } + if (data.error && typeof data.error === "object") push((data.error as Record).message); for (const text of texts) { const { findings } = scanAndRedact(text, "message"); diff --git a/src/plugin/layer3.ts b/src/plugin/layer3.ts index 5b21988..fb867f4 100755 --- a/src/plugin/layer3.ts +++ b/src/plugin/layer3.ts @@ -11,7 +11,7 @@ export type Layer3Event = { sessionID?: string; status?: "completed" | "error" | string; result?: ToolResultLike; - error?: { message?: string; error?: unknown }; + error?: { message?: string; error?: unknown; metadata?: unknown }; }; /** @@ -39,6 +39,12 @@ export async function handleLayer3(event: Layer3Event, ctx: AuditContext): Promi if (event.error && "error" in event.error) { event.error.error = redactStructured(event.error.error, "output", audit); } + // `Tool.Error.metadata` is a free-form record that is surfaced to the + // model alongside the message; redact it too so a secret in tool + // error metadata cannot reach the transcript. + if (event.error && event.error.metadata !== undefined) { + event.error.metadata = redactStructured(event.error.metadata, "output", audit); + } return; } diff --git a/src/plugin/redact.ts b/src/plugin/redact.ts index 53dd4d0..f0e71bd 100644 --- a/src/plugin/redact.ts +++ b/src/plugin/redact.ts @@ -1,5 +1,5 @@ import type { Finding } from "../engine/types.js"; -import { scanAndRedactBounded } from "../engine/scanner.js"; +import { scanAndRedactBounded, UNSCANNABLE_MARKER } from "../engine/scanner.js"; import { writeAuditBatch, auditEntryFromFinding } from "../engine/audit.js"; /** @@ -53,54 +53,93 @@ export function redactText( return redacted; } -/** Maximum nesting depth traversed when redacting structured values. */ -const MAX_STRUCTURED_DEPTH = 8; +/** + * Maximum nesting depth traversed when redacting structured values. Plain JSON + * produced by tools is shallow; anything deeper than this is treated as + * unexpected and fails closed (below) rather than being walked further. + */ +const MAX_STRUCTURED_DEPTH = 32; /** * Redact string leaves of a structured value (tool output, metadata, message - * part inputs/results) while preserving its shape and types. Exotic objects are - * left untouched so we never corrupt non-plain data. + * part inputs/results) while preserving its shape and types. + * + * Fail-safe behavior: + * - A cyclic structure is detected with a `seen` set instead of recursing + * until the stack overflows. (Before this guard, a self-referential object + * threw a `RangeError`, the surrounding hook caught it, and *every* string + * in the result was left unredacted — a silent fail-open.) + * - A value that cannot be safely inspected (beyond `MAX_STRUCTURED_DEPTH`) + * is replaced with the explicit fail-closed marker, never passed through + * silently. + * - Exotic objects (anything whose prototype is not `Object.prototype`/`null`) + * are left untouched so non-plain data is never corrupted; this residual + * limit is documented in the README. */ export function redactStructured( value: unknown, source: Finding["source"], ctx: AuditContext, depth = 0, + seen: WeakSet = new WeakSet(), ): unknown { if (typeof value === "string") return redactText(value, source, ctx); - if (value === null || typeof value !== "object" || depth >= MAX_STRUCTURED_DEPTH) return value; + if (value === null || typeof value !== "object") return value; + if (depth >= MAX_STRUCTURED_DEPTH) return UNSCANNABLE_MARKER; if (Array.isArray(value)) { + if (seen.has(value)) return UNSCANNABLE_MARKER; + seen.add(value); const out = new Array(value.length); - for (let i = 0; i < value.length; i++) out[i] = redactStructured(value[i], source, ctx, depth + 1); + for (let i = 0; i < value.length; i++) out[i] = redactStructured(value[i], source, ctx, depth + 1, seen); + seen.delete(value); return out; } const proto = Object.getPrototypeOf(value); if (proto !== Object.prototype && proto !== null) return value; + if (seen.has(value)) return UNSCANNABLE_MARKER; + seen.add(value); const out: Record = {}; for (const [k, v] of Object.entries(value as Record)) { - out[k] = redactStructured(v, source, ctx, depth + 1); + out[k] = redactStructured(v, source, ctx, depth + 1, seen); } + seen.delete(value); return out; } -/** Redact in place, replacing only string leaves of a plain object/array. */ -export function redactStructuredInPlace(value: unknown, source: Finding["source"], ctx: AuditContext, depth = 0): void { +/** + * Redact in place, replacing only string leaves of a plain object/array. + * Cycle-safe; a cyclic or over-deep branch is left untouched (this helper is a + * best-effort in-place variant and cannot fail closed by substituting a value). + */ +export function redactStructuredInPlace( + value: unknown, + source: Finding["source"], + ctx: AuditContext, + depth = 0, + seen: WeakSet = new WeakSet(), +): void { if (value === null || typeof value !== "object" || depth >= MAX_STRUCTURED_DEPTH) return; + if (seen.has(value)) return; + if (Array.isArray(value)) { + seen.add(value); for (let i = 0; i < value.length; i++) { const v = value[i]; if (typeof v === "string") value[i] = redactText(v, source, ctx); - else redactStructuredInPlace(v, source, ctx, depth + 1); + else redactStructuredInPlace(v, source, ctx, depth + 1, seen); } + seen.delete(value); return; } const proto = Object.getPrototypeOf(value); if (proto !== Object.prototype && proto !== null) return; + seen.add(value); for (const [k, v] of Object.entries(value as Record)) { if (typeof v === "string") (value as Record)[k] = redactText(v, source, ctx); - else redactStructuredInPlace(v, source, ctx, depth + 1); + else redactStructuredInPlace(v, source, ctx, depth + 1, seen); } + seen.delete(value); } diff --git a/src/plugin/tools.ts b/src/plugin/tools.ts index 82434b8..f87830c 100644 --- a/src/plugin/tools.ts +++ b/src/plugin/tools.ts @@ -1,4 +1,4 @@ -import { scanAndRedact, toSafeFinding } from "../engine/scanner.js"; +import { scanAndRedactBounded, toSafeFinding } from "../engine/scanner.js"; import { readAuditLog, resolveAuditPath } from "../engine/audit.js"; import { loadAllowlist, saveAllowlistEntry } from "../engine/allowlist.js"; import { NAMED_RULES, ENTROPY_RULES, CLEAN_ROOM_RULES } from "../engine/rules.js"; @@ -95,15 +95,29 @@ export function sdAllowlistAdd(args: { return "Allowlist entry added."; } -/** `sd_scan` — returns secret-free metadata only. */ +/** Upper bound on content accepted by `sd_scan`. Keeps a model-invoked scan bounded. */ +export const MAX_SD_SCAN_CHARS = 8 * 1024 * 1024; + +/** + * `sd_scan` — returns secret-free metadata only. + * + * The input is bounded and scanned with the same chunk-safe path as the hooks, + * so a very large `content` argument cannot exhaust memory or hang the server. + */ export function sdScan(args: { content: string; source?: Finding["source"] }): string { - const source = args.source ?? "file"; - const { findings } = scanAndRedact(args.content, source); - if (findings.length === 0) return "No secrets detected."; + const source = args?.source ?? "file"; + const content = typeof args?.content === "string" ? args.content : ""; + if (content.length === 0) return "No secrets detected."; + if (content.length > MAX_SD_SCAN_CHARS) { + return `Content too large to scan: ${content.length} characters exceeds the ${MAX_SD_SCAN_CHARS}-character limit.`; + } + const { findings, complete, policy } = scanAndRedactBounded(content, source); + const note = complete ? "" : `\n\n[strangerdanger: content not fully inspected (${policy})]`; + if (findings.length === 0) return `No secrets detected.${note}`; return findings .map((f) => { - const safe = toSafeFinding(f, args.content); + const safe = toSafeFinding(f, content); return `${safe.ruleId}: ${safe.description}\n Location: line ${safe.line ?? "?"}, column ${(safe.column ?? 0) + 1}\n Fingerprint: ${safe.fingerprint}\n Redaction: ${safe.redaction}`; }) - .join("\n\n"); + .join("\n\n") + note; } diff --git a/tests/security-coverage.test.ts b/tests/security-coverage.test.ts new file mode 100644 index 0000000..24b4ce4 --- /dev/null +++ b/tests/security-coverage.test.ts @@ -0,0 +1,344 @@ +import { test, describe } from "node:test"; +import assert from "node:assert"; +import fs from "node:fs"; +import path from "node:path"; +import os from "node:os"; +import { strangerDanger } from "../src/plugin/index.js"; +import { handleLayer3 } from "../src/plugin/layer3.js"; +import { + handleContext, + handleGenerate, + handleTitle, + handleMessageEvent, +} from "../src/plugin/layer2-message.js"; +import { + scanAndRedactBounded, + MAX_BOUNDED_SCAN_BYTES, + UNSCANNABLE_MARKER, +} from "../src/engine/scanner.js"; +import { sdScan, MAX_SD_SCAN_CHARS } from "../src/plugin/tools.js"; +import { readAuditLog } from "../src/engine/audit.js"; + +const AWS = "AKIAIOSFODNN7EXAMPLE"; +const GH = "ghp_" + "aB3xK9mQ2pR7vL4nJ8wC5tY1cF6hA0sD8gE2"; +const REDACTED = "[REDACTED:"; + +function tempDir(): string { + return fs.mkdtempSync(path.join(os.tmpdir(), "sd-coverage-")); +} +function cleanup(dir: string): void { + try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ } +} +function auditPathIn(dir: string): string { + return path.join(dir, ".opencode", "state", "strangerdanger-audit.jsonl"); +} + +/** Minimal V2 plugin-context fake that captures registered tools and hooks. */ +function makeCtx(directory: string) { + const tools: any[] = []; + const hooks: Record unknown>> = {}; + const reg = { dispose: async () => {} }; + const ctx: any = { + location: { directory, project: { id: "proj", directory, canonical: directory } }, + options: {}, + tool: { + transform: async (cb: (editor: any) => void) => { + cb({ + add: (t: any) => tools.push(t), + namespace: () => {}, + list: () => tools, + get: () => undefined, + update: () => {}, + remove: () => {}, + }); + return reg; + }, + hook: async (name: string, cb: (e: any) => unknown) => { + (hooks[name] ||= []).push(cb); + return reg; + }, + list: async () => tools, + reload: async () => {}, + }, + session: { + hook: async (name: string, cb: (e: any) => unknown) => { + (hooks[`session.${name}`] ||= []).push(cb); + return reg; + }, + }, + event: { + subscribe: () => ({ async *[Symbol.asyncIterator]() { /* no events */ } }), + }, + }; + return { ctx, tools, hooks }; +} + +// ============================================================ +// Missing request kinds: generate + title +// ============================================================ + +describe("security coverage — auxiliary request kinds", () => { + test("setup registers generate and title hooks", async () => { + const dir = tempDir(); + try { + const { ctx, hooks } = makeCtx(dir); + await strangerDanger.setup(ctx); + assert.ok(hooks["session.generate"]?.length, "session.generate hook registered"); + assert.ok(hooks["session.title"]?.length, "session.title hook registered"); + } finally { + cleanup(dir); + } + }); + + test("generate hook redacts system and message text", async () => { + const dir = tempDir(); + try { + const { ctx, hooks } = await (async () => { + const made = makeCtx(dir); + await strangerDanger.setup(made.ctx); + return made; + })(); + const event: any = { + sessionID: "s1", + system: [{ type: "text", text: `key=${AWS}` }], + messages: [{ content: [{ type: "text", text: `tok=${GH}` }] }], + }; + await hooks["session.generate"][0](event); + assert.ok(!event.system[0].text.includes(AWS), "system secret redacted"); + assert.ok(event.system[0].text.includes(REDACTED)); + assert.ok(!event.messages[0].content[0].text.includes(GH), "message secret redacted"); + } finally { + cleanup(dir); + } + }); + + test("title hook redacts system, messages, and an explicit result", async () => { + const event: any = { + sessionID: "s1", + system: [{ type: "text", text: AWS }], + messages: [{ content: [{ type: "text", text: GH }] }], + result: `Title: ${AWS}`, + }; + await handleTitle(event, {}); + assert.ok(!JSON.stringify(event).includes(AWS), "no AWS secret survives"); + assert.ok(!JSON.stringify(event).includes(GH), "no GH secret survives"); + assert.ok(typeof event.result === "string" && event.result.includes(REDACTED)); + }); + + test("handleGenerate and handleContext are equivalent for the same shape", async () => { + const a: any = { system: [{ type: "text", text: AWS }], messages: [] }; + const b: any = { system: [{ type: "text", text: AWS }], messages: [] }; + await handleContext(a, {}); + await handleGenerate(b, {}); + assert.deepStrictEqual(a, b); + assert.ok(!a.system[0].text.includes(AWS)); + }); +}); + +// ============================================================ +// tool.execute.after — error path +// ============================================================ + +describe("security coverage — tool error path", () => { + test("redacts error.message, error.error, and error.metadata", async () => { + const dir = tempDir(); + try { + const auditPath = auditPathIn(dir); + const event: any = { + status: "error", + sessionID: "s1", + error: { + message: `failed with ${AWS}`, + error: { detail: GH }, + metadata: { nested: { token: AWS } }, + }, + }; + await handleLayer3(event, { auditPath }); + const serialized = JSON.stringify(event); + assert.ok(!serialized.includes(AWS), "no AWS secret in redacted error"); + assert.ok(!serialized.includes(GH), "no GH secret in redacted error"); + assert.ok(serialized.includes(REDACTED)); + } finally { + cleanup(dir); + } + }); +}); + +// ============================================================ +// Structured redaction fail-safe behavior +// ============================================================ + +describe("security coverage — structured redaction fail-safe", () => { + test("a cyclic output is handled without a stack overflow and still redacts siblings", async () => { + const dir = tempDir(); + try { + const auditPath = auditPathIn(dir); + const output: any = { token: AWS }; + output.self = output; // self-reference + const event: any = { status: "completed", sessionID: "s1", result: { output } }; + + await handleLayer3(event, { auditPath }); + + // If recursion had overflowed, the hook's catch would have left the + // whole result untouched (silent leak). Assert it did not. + const serialized = JSON.stringify(event.result.output); + assert.ok(!serialized.includes(AWS), "sibling secret is still redacted"); + assert.ok(serialized.includes(REDACTED)); + assert.ok(serialized.includes(UNSCANNABLE_MARKER), "cyclic branch fails closed"); + } finally { + cleanup(dir); + } + }); + + test("content beyond the depth cap fails closed instead of silently leaking", async () => { + const dir = tempDir(); + try { + const auditPath = auditPathIn(dir); + let deep: any = { token: AWS }; + for (let i = 0; i < 60; i++) deep = { nested: deep }; + const event: any = { status: "completed", result: { metadata: deep } }; + + await handleLayer3(event, { auditPath }); + + const serialized = JSON.stringify(event.result.metadata); + assert.ok(!serialized.includes(AWS), "deep secret does not survive"); + assert.ok(serialized.includes(UNSCANNABLE_MARKER), "over-deep branch fails closed"); + } finally { + cleanup(dir); + } + }); +}); + +// ============================================================ +// V2 event stream detection (previously matched no real event) +// ============================================================ + +describe("security coverage — V2 event stream", () => { + test("records a finding from a real completed-text event", () => { + const dir = tempDir(); + try { + const auditPath = auditPathIn(dir); + handleMessageEvent( + { type: "session.text.ended", data: { sessionID: "s1", text: `leaked ${AWS}` } }, + { auditPath, sessionId: "s1" }, + ); + const log = readAuditLog(auditPath); + assert.ok(log.includes("aws-access-token"), "audit records the finding"); + assert.ok(!log.includes(AWS), "audit never stores the raw secret"); + } finally { + cleanup(dir); + } + }); + + test("records from tool success/failed content and skips per-token deltas", () => { + const dir = tempDir(); + try { + const auditPath = auditPathIn(dir); + handleMessageEvent( + { type: "session.tool.success", data: { sessionID: "s1", content: [{ type: "text", text: GH }] } }, + { auditPath }, + ); + const afterSuccess = readAuditLog(auditPath); + assert.ok(afterSuccess.includes("github-pat"), "tool content audited"); + + handleMessageEvent( + { type: "session.text.delta", data: { sessionID: "s1", delta: AWS } }, + { auditPath }, + ); + assert.strictEqual(readAuditLog(auditPath), afterSuccess, "delta frames are not audited"); + } finally { + cleanup(dir); + } + }); + + test("ignores malformed and non-session events without throwing", () => { + const dir = tempDir(); + try { + const auditPath = auditPathIn(dir); + for (const ev of [null, undefined, 42, "nope", {}, { type: "server.connected" }, { type: "session.idle" }, { type: "session.text.ended", data: null }]) { + handleMessageEvent(ev, { auditPath }); + } + assert.strictEqual(readAuditLog(auditPath), ""); + } finally { + cleanup(dir); + } + }); +}); + +// ============================================================ +// Bounded scan size limits and fail-closed branches +// ============================================================ + +describe("security coverage — bounded scan limits", () => { + test("the hard ceiling is 256 MiB (characters)", () => { + assert.strictEqual(MAX_BOUNDED_SCAN_BYTES, 256 * 1024 * 1024); + }); + + test("oversized input fails closed before scanning", () => { + const r = scanAndRedactBounded("hello world", "output", undefined, { window: 4, maxBytes: 5 }); + assert.strictEqual(r.complete, false); + assert.strictEqual(r.policy, "oversized"); + assert.strictEqual(r.redacted, UNSCANNABLE_MARKER); + assert.deepStrictEqual(r.findings, []); + }); + + test("a window that exceeds its time budget fails closed", () => { + let n = 0; + const now = () => (n++ % 2 === 0 ? 0 : 10_000); + const r = scanAndRedactBounded("x".repeat(200), "output", undefined, { + window: 10, + overlap: 2, + windowBudgetMs: 5, + now, + }); + assert.strictEqual(r.complete, false); + assert.strictEqual(r.policy, "window-timeout"); + assert.strictEqual(r.redacted, UNSCANNABLE_MARKER); + }); + + test("windowing still finds a secret that spans a window boundary", () => { + const content = ".".repeat(50) + AWS + ".".repeat(50); + const r = scanAndRedactBounded(content, "output", undefined, { window: 64, overlap: 32 }); + assert.strictEqual(r.complete, true); + assert.ok(r.findings.some((f) => f.ruleId === "aws-access-token")); + assert.ok(!r.redacted.includes(AWS)); + }); + + test("sd_scan refuses oversized content and stays bounded otherwise", () => { + const huge = "a".repeat(MAX_SD_SCAN_CHARS + 1); + assert.match(sdScan({ content: huge }), /too large/i); + const ok = sdScan({ content: `token=${AWS}`, source: "output" }); + assert.ok(ok.includes("aws-access-token")); + assert.ok(!ok.includes(AWS), "sd_scan output never contains the raw secret"); + }); +}); + +// ============================================================ +// Multi-session audit isolation (companion can't corrupt state) +// ============================================================ + +describe("security coverage — audit isolation", () => { + test("two plugin instances write only to their own project audit log", async () => { + const a = tempDir(); + const b = tempDir(); + try { + const A = makeCtx(a); + const B = makeCtx(b); + await strangerDanger.setup(A.ctx); + await strangerDanger.setup(B.ctx); + await A.hooks["session.context"][0]({ sessionID: "sa", system: [{ type: "text", text: AWS }], messages: [] }); + await B.hooks["session.context"][0]({ sessionID: "sb", system: [{ type: "text", text: GH }], messages: [] }); + + const logA = readAuditLog(auditPathIn(a)); + const logB = readAuditLog(auditPathIn(b)); + assert.ok(logA.includes("aws-access-token"), "A recorded its own finding"); + assert.ok(!logA.includes("github-pat"), "A did not receive B's finding"); + assert.ok(logB.includes("github-pat"), "B recorded its own finding"); + assert.ok(!logB.includes("aws-access-token"), "B did not receive A's finding"); + assert.ok(!logA.includes(AWS) && !logB.includes(GH), "no raw secrets stored"); + } finally { + cleanup(a); + cleanup(b); + } + }); +}); From 64929cbaf509dc1b21011d13997c06dad92d6189 Mon Sep 17 00:00:00 2001 From: Evan Zellner Date: Fri, 16 Oct 2026 19:25:34 -0500 Subject: [PATCH 4/4] Release 0.2.0: license, security policy, and release documentation Release-only changes for v0.2.0; no source, test, or build artifacts change. - Add MIT LICENSE (README and package already declared MIT but shipped none). - Add SECURITY.md: disclosure process, protected surfaces, known limitations. - Add PRODUCT_REQUIREMENTS.md, DECISION_LOG.md, PROJECT_STATUS.md (canonical project memory; PROJECT_STATUS records the frozen release candidate, its verified commit, and the evidence). - package.json: add repository/bugs/homepage/author/keywords, publishConfig access=public, and ship the logo the README actually references (strangerdanger2.jpeg, not strangerdanger.jpeg). - CHANGELOG.md: mark 0.2.0 dated and note npm publication is pending. - README.md: link the security policy. --- CHANGELOG.md | 6 +- DECISION_LOG.md | 144 ++++++++++++++++++++++++++++++++++++++++ LICENSE | 21 ++++++ PRODUCT_REQUIREMENTS.md | 91 +++++++++++++++++++++++++ PROJECT_STATUS.md | 74 +++++++++++++++++++++ README.md | 3 + SECURITY.md | 78 ++++++++++++++++++++++ package.json | 23 ++++++- 8 files changed, 437 insertions(+), 3 deletions(-) create mode 100644 DECISION_LOG.md create mode 100644 LICENSE create mode 100644 PRODUCT_REQUIREMENTS.md create mode 100644 PROJECT_STATUS.md create mode 100644 SECURITY.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 50dec2d..7ba7c97 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,10 @@ # Changelog -## 0.2.0 (unreleased) +## 0.2.0 (2026-10-16) -Security hardening and OpenCode V2 migration. Not yet published. +Security hardening and OpenCode V2 migration. Published as a GitHub release; +npm publication of `@gitdamnit/stranger-danger` is pending registry setup +(install from the repository in the meantime). ### Security fixes diff --git a/DECISION_LOG.md b/DECISION_LOG.md new file mode 100644 index 0000000..0f31b39 --- /dev/null +++ b/DECISION_LOG.md @@ -0,0 +1,144 @@ +# Decision Log + +## How to Use This Log + +1. Copy the block below to the end of the `## Decisions` section. +2. Use the next free four-digit ID. IDs are never reused and never renumbered. +3. Date the record the day the decision was actually made (`YYYY-MM-DD`). +4. Record the alternatives you rejected and why. +5. Never rewrite history. If a decision changes, append a superseding record. + +```markdown +### D-0001 — Short imperative title + +- **Date:** YYYY-MM-DD +- **Status:** Accepted +- **Context:** the situation and the constraint that forced a choice +- **Alternatives Considered:** option A (rejected because ...), option B (rejected because ...) +- **Decision:** what was decided, in one or two sentences +- **Rationale:** why this option wins, including what it costs +- **Consequences:** what follows from it — new constraints, follow-up work, risks +- **Supersedes:** None +``` + +Allowed statuses: `Proposed`, `Accepted`, `Superseded`, `Deprecated`, `Rejected`. + +## Decisions + +### D-0001 — Enforce redaction at the OpenCode V2 session.* request hooks + +- **Date:** 2026-05-13 +- **Status:** Accepted +- **Context:** 0.1.0 scanned files and audited after the fact from the V1 `event` + handler. That cannot prevent disclosure: the event stream is observational and + arrives after a request has been admitted. +- **Alternatives Considered:** keep the V1 `event` handler (rejected: post-hoc, + cannot stop transmission); rewrite `http.request` bodies (rejected: not a + supported stable surface and it breaks provider request shaping); patch files + on read (rejected: corrupts the user's working tree). +- **Decision:** Register all four V2 model-request hooks (`session.prompt`, + `session.context`, `session.generate`, `session.title`), plus + `session.compaction` and `tool.execute.after`, and redact the assembled + request immediately before dispatch. +- **Rationale:** These are the supported boundaries that run before + transmission, and registering all of them makes redaction independent of the + order in which other plugins run. +- **Consequences:** Enforcement depends on OpenCode V2 exposing these hooks; + surfaces outside `system`/`messages` (raw HTTP bodies, provider WebSockets) are + out of scope and documented as such. +- **Supersedes:** None + +### D-0002 — The pre-read hook is strictly read-only + +- **Date:** 2026-05-13 +- **Status:** Accepted +- **Context:** The V1 pre-read hook rewrote source files on disk when it detected + a secret, silently modifying the user's working tree. +- **Alternatives Considered:** keep rewriting files (rejected: data loss and + corruption, unacceptable for an inspection hook); delete the file (rejected: + far worse); warn only (rejected: does not protect the model context). +- **Decision:** `tool.execute.before` detects and audits only; it never writes. + Redaction happens at the result/request boundaries instead. +- **Rationale:** An inspection hook must not have side effects. The result and + request boundaries already cover what the model sees. +- **Consequences:** The on-disk file still contains the secret; users rely on + `sd redact` or their own tooling to remediate the file. This is stated in the + README. +- **Supersedes:** None + +### D-0003 — Fail closed on content that cannot be inspected + +- **Date:** 2026-05-16 +- **Status:** Accepted +- **Context:** A cyclic tool output overflowed the stack; the hook swallowed the + error and the entire result — including plain string secrets — passed through + unredacted. Truncating large output had the same fail-open shape. +- **Alternatives Considered:** fail open (rejected: leaks silently); truncate at + a size limit (rejected: a secret past the limit leaks); throw from the hook + (rejected: would break the runtime and companion plugins). +- **Decision:** Bounded, windowed scanning with explicit fail-closed markers. + Oversize, window timeout/error, cycles, and nesting beyond 32 levels all + replace the uninspectable content with a `[REDACTED:...]` marker. +- **Rationale:** Losing content is recoverable; leaking a credential is not. +- **Consequences:** Very large or exotic payloads lose content and gain a marker. + The marker is documented and visible to the user and the model. +- **Supersedes:** None + +### D-0004 — Audit records store metadata and an 8-hex fingerprint, never the secret + +- **Date:** 2026-05-16 +- **Status:** Accepted +- **Context:** An audit log that contains the matched value is itself a secret + store, and it is easy to attach to a bug report by accident. +- **Alternatives Considered:** store the raw match (rejected: creates a new + credential store); store nothing (rejected: no operator visibility); store a + full SHA-256 (rejected: unnecessary precision, still correlatable). +- **Decision:** Record rule id, description, source, file path, optional session + id, and an 8-hex fingerprint of the match. Never the match or the redaction + token body. +- **Rationale:** Operators can triage and allowlist by fingerprint without a log + that is dangerous to share. +- **Consequences:** The log is metadata-only; `0600` files in a `0700` directory, + with size-bounded rotation that preserves records. +- **Supersedes:** None + +### D-0005 — Remain independent of companion OpenCode plugins + +- **Date:** 2026-05-16 +- **Status:** Accepted +- **Context:** strangerDanger is developed alongside Git That Shit and Checkpoint + Orchestrator in a shared test lab, which invites accidental coupling. +- **Alternatives Considered:** integrate directly with the companions (rejected: + couples release cycles and creates load-order dependencies); share a database + or config namespace (rejected: cross-contamination and mixed audit records). +- **Decision:** No cross-plugin imports, no shared state, no required load order. + The only interop surface is the optional, read-only, semver-versioned + `@gitdamnit/stranger-danger/engine` export. +- **Rationale:** A security plugin that can be broken by, or break, an unrelated + plugin is not trustworthy, and ordering-independent redaction (D-0001) makes + coupling unnecessary. +- **Consequences:** The companions are never required; failure isolation is + mandatory in every hook; the independence test suite guards this. +- **Supersedes:** None + +### D-0006 — Release v0.2.0 on GitHub; treat npm publication as a separate, manual step + +- **Date:** 2026-10-16 +- **Status:** Accepted +- **Context:** The repository contains `.github/workflows/publish.yml`, which + publishes to npm on any `v*.*.*` tag push using the `NPM_TOKEN` secret. No such + secret (or repository variable) is configured, and the package is not yet + present on the npm registry. +- **Alternatives Considered:** publish to npm as part of this release (rejected: + registry ownership/authentication is not established and publication is + explicitly out of scope for this session); delete or disable `publish.yml` + (rejected: it is the intended release path once credentials exist). +- **Decision:** Create the `v0.2.0` annotated tag and GitHub release at the + verified commit. Do not publish to npm. Record the exact npm publication + checklist for a later, explicitly authorized run. +- **Rationale:** GitHub publication is fully authorized and verifiable now; npm + publication requires credentials and registry setup that do not exist. +- **Consequences:** Pushing the tag triggers `publish.yml`, which will fail at + the npm step because `NPM_TOKEN` is unset. This is expected and does not affect + the GitHub release. npm distribution must not be reported as complete. +- **Supersedes:** None diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..34d3a49 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 gitdamnit + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/PRODUCT_REQUIREMENTS.md b/PRODUCT_REQUIREMENTS.md new file mode 100644 index 0000000..1f1143b --- /dev/null +++ b/PRODUCT_REQUIREMENTS.md @@ -0,0 +1,91 @@ +# Product Requirements + +## Project Identity + +**Name:** strangerDanger (`@gitdamnit/stranger-danger`) +**Repository:** https://github.com/gitdamnit/opencode-stranger-danger +**Licence:** MIT +**Maintainer:** gitdamnit (Evan Zellner) + +## Purpose + +Secret scanners inspect files on disk; they do not inspect what enters an AI +coding agent's model context. A credential can be clean in the repository and +still be transmitted to a cloud provider inside a prompt, a tool result, a +compaction transcript, or a generated title. strangerDanger operates at the +OpenCode V2 request boundaries and removes secrets from that outbound content +before transmission, while leaving the user's files untouched. + +## Intended Users + +Developers and teams running OpenCode against hosted model providers who need a +defense-in-depth control between their working tree and a third-party API. They +are comfortable adding a plugin to `opencode.jsonc` or installing a CLI, and they +expect a security tool to fail closed and to avoid corrupting their work. + +## Functional Requirements + +| ID | Requirement | Acceptance signal | +| --- | --- | --- | +| FR-1 | Detect secrets and PII in text with vendored gitleaks rules, clean-room rules, and entropy detectors | `sd scan` reports findings for known synthetic secrets | +| FR-2 | Redact outbound prompts, tool results, and the assembled agent-loop request before transmission | With the plugin loaded, a synthetic secret in a prompt/result is absent from the transmitted body | +| FR-3 | Cover transient `generate`, `title`, and `compaction` request kinds | Those hooks are registered and redact the same request shape | +| FR-4 | Pre-read inspection must never modify a file on disk | `tool.execute.before` contains no write call; only detection and audit occur | +| FR-5 | Fail closed on content that cannot be inspected | Oversize/timeout/error/cycle/over-depth yields an explicit `[REDACTED:...]` marker, not the original | +| FR-6 | Cover large output with windowed scanning | A secret straddling a window boundary is still detected | +| FR-7 | Keep audit records secret-free and project-scoped | Audit lines contain rule id/description/fingerprint/source/path/session id only | +| FR-8 | Support a project allowlist by fingerprint, ruleId, path, or regex | `sd allowlist` and `sd_allowlist_add` accept/reject entries accordingly | +| FR-9 | Provide a standalone CLI (`sd scan`, `sd redact`, `sd audit`, `sd rules`, `sd allowlist`, `sd version`) | `verify:package` exercises the installed `sd` binary | +| FR-10 | Register the plugin tools `sd_status`, `sd_audit`, `sd_allowlist_add`, `sd_allowlist_show`, `sd_scan` | Tools appear in the OpenCode runtime and in `runtime-*.json` | +| FR-11 | Operate with no dependency on a companion plugin | No cross-plugin imports; peer dependency is `@opencode/plugin` only | +| FR-12 | Hook callbacks never throw into the runtime | Every hook is wrapped; malformed events are tolerated | + +## Nonfunctional Requirements + +| ID | Requirement | Target | +| --- | --- | --- | +| NFR-1 | No raw secret is emitted by CLI, tools, or audit log | Verified by tests and `verify:package` (JSON must not contain the secret) | +| NFR-2 | Scanning large content is bounded | 256 MiB hard ceiling; 1 MiB windows with 64 KiB overlap; 2.5 s per-window budget | +| NFR-3 | Fast on large inputs | Whole-content keyword prefilter; no regex run when no keyword is present | +| NFR-4 | Filesystem hygiene | Audit files `0600`, directories `0700`, rotation to `.1`/`.2`/`.3` | +| NFR-5 | Portability | Node `>= 20` (CI on Node 22); OpenCode V2 with `@opencode/plugin >= 2.0.0` | +| NFR-6 | Maintainability | Detection engine decoupled from OpenCode; `src/engine` shared by plugin and CLI | +| NFR-7 | Reliability under failure | A hook error never breaks the runtime or a companion plugin | + +## Scope and Exclusions + +In scope for 0.2.0: the OpenCode V2 plugin, the `sd` CLI, the detection engine, +the audit log, the allowlist, packaging, CI, and the independence guarantees. + +Out of scope (documented): provider-native WebSocket traffic, raw `http.request` +body rewriting, non-plain objects (`Map`/`Set`/`Date`/typed arrays), secrets that +match no rule after obfuscation, and any claim of universal leak prevention. + +## Technical Constraints + +- TypeScript, ESM, Node `>= 20`. +- OpenCode V2 plugin model: `Plugin.define({ id, setup })`; hooks registered via + `ctx.tool.hook` / `ctx.session.hook` / `ctx.event.subscribe`; tools via + `ctx.tool.transform` with JSON Schema. +- No runtime dependency other than the `@opencode/plugin` peer. +- Git That Shit, Checkpoint Orchestrator, and CheapCode are explicitly not + dependencies and must not be touched by this project. + +## Acceptance Criteria + +1. `npm run typecheck`, `npm run lint`, `npm run build` succeed. +2. Unit, integration, security, and the full test suite pass. +3. `npm run verify:package` passes: the packed artifact installs, the `sd` binary + runs, scans a synthetic secret without leaking it, and the plugin's default + export has the V2 shape. +4. The Docker Plugin Test Lab reports 145/145 with strangerDanger at the verified + commit. +5. README, CHANGELOG, LICENSE, and SECURITY accurately describe install, + compatibility, limitations, and the threat model. + +## Release Objectives + +Ship v0.2.0 (security hardening + OpenCode V2 migration) as a GitHub release from +the verified commit, with the package ready for npm publication on request. The +release must not overstate coverage: it protects the documented OpenCode V2 +request boundaries and fails closed elsewhere, and says so explicitly. diff --git a/PROJECT_STATUS.md b/PROJECT_STATUS.md new file mode 100644 index 0000000..144bf79 --- /dev/null +++ b/PROJECT_STATUS.md @@ -0,0 +1,74 @@ +# Project Status + +## Current Phase + +In Progress + +## Repository State + + + + +- **Observed branch:** feat/stranger-danger-v2-hardening +- **Observed commit:** 9cfd8135429bffb2e5e32e97f23bec3f5b6277bd +- **Observed working tree:** dirty +- **Observed at:** 2026-10-17T00:24:21Z + + +## Verification Evidence + +- **Verification:** commit `9cfd8135429bffb2e5e32e97f23bec3f5b6277bd` — local full suite green (322 tests, 0 fail) after a clean `npm ci`, with a rebuild producing byte-identical `dist/` — evidence `/tmp/opencode/sd-verify.log` (typecheck, lint, build, unit, integration, security, package validation all `rc=0`) +- **Verification:** commit `9cfd8135429bffb2e5e32e97f23bec3f5b6277bd` — `npm run verify:package` passed every artifact check (pack, isolated install, `sd version`/`sd rules`, secret-free scan JSON, engine export, V2 plugin shape, root `index.js`) — evidence `/tmp/opencode/sd-verify.log` +- **Verification:** commit `9cfd8135429bffb2e5e32e97f23bec3f5b6277bd` — Docker Plugin Test Lab, `stranger-danger` 0.2.0, image `opencode-plugin-lab:local` (`sha256:76ebd805cd548a638b9b1508e5f16e680cd5b082cd3f3561d28f2f2301336ff1`), 145/145 passed on OpenCode 2.0.21 / Node 22.23.3 — evidence `/home/evan/Projects/opencode-plugin-lab/reports/rc-evidence.json` and `/home/evan/Projects/opencode-plugin-lab/reports/test-report.md` + +## Completed Work + +- [Implemented] OpenCode V2 migration — `Plugin.define({ id: "gitdamnit.stranger-danger", setup })`, five JSON-Schema tools, `session.*` hooks — `src/plugin/index.ts` +- [Implemented] Read-only pre-read inspection (no disk writes) — `src/plugin/layer1.ts` +- [Implemented] Bounded, windowed, fail-closed scanner — `src/engine/scanner.ts` +- [Implemented] Project-scoped, secret-free audit log with rotation — `src/engine/audit.ts` +- [Implemented] Allowlist by fingerprint/ruleId/path/regex — `src/engine/allowlist.ts` +- [Implemented] Atomic `sd redact` with opt-in out-of-tree backup — `src/cli/commands/redact.ts` +- [Tested] Unit, integration, security and full suites green at `9cfd813542` — `/tmp/opencode/sd-verify.log` +- [Tested] Packed-artifact validation green at `9cfd813542` — `/tmp/opencode/sd-verify.log` +- [Implemented] CI (typecheck, lint, build, tests, package validation) and npm publish workflow — `.github/workflows/ci.yml`, `.github/workflows/publish.yml` +- [Implemented] MIT `LICENSE` and vulnerability-disclosure `SECURITY.md` — `LICENSE`, `SECURITY.md` +- [Planned] npm publication of `@gitdamnit/stranger-danger` (requires registry credentials) + +## Tests + +| Suite | Command | Result | Evidence | +| --- | --- | --- | --- | +| typecheck | `npm run typecheck` | Pass | `/tmp/opencode/sd-verify.log` | +| lint | `npm run lint` | Pass | `/tmp/opencode/sd-verify.log` | +| build | `npm run build` | Pass (no `dist/` drift) | `/tmp/opencode/sd-verify.log` | +| unit | `npm run test:unit` | Pass | `/tmp/opencode/sd-verify.log` | +| integration | `npm run test:integration` | Pass | `/tmp/opencode/sd-verify.log` | +| security | `npm run test:security` | Pass | `/tmp/opencode/sd-verify.log` | +| full | `npm test` | Pass (322) | `/tmp/opencode/sd-verify.log` | +| package | `npm run verify:package` | Pass | `/tmp/opencode/sd-verify.log` | +| runtime lab | Docker Plugin Test Lab `--all --tier validate` | Pass (145) | `/home/evan/Projects/opencode-plugin-lab/reports/test-report.json` | + +## Known Defects + +- None known. Non-blocking observations: `tests/debug.test.ts` and `tests/debug2.test.ts` are trivial sanity stubs; the provider-WebSocket, raw-HTTP-body, exotic-object, and encoded-secret gaps are documented limitations, not defects. + +## Blockers + +- npm publication is blocked on registry ownership/authentication: the `@gitdamnit` scope is not published and no `NPM_TOKEN` secret is configured. GitHub publication is unaffected. + +## Next Actions + +1. Merge the release pull request for `feat/stranger-danger-v2-hardening` into `master`. +2. Create the `v0.2.0` annotated tag and the GitHub release. +3. When credentials exist, publish `@gitdamnit/stranger-danger@0.2.0` to npm and verify a clean install. + +## Release Status + +Release candidate 0.2.0 is frozen at commit `9cfd8135429bffb2e5e32e97f23bec3f5b6277bd` +on branch `feat/stranger-danger-v2-hardening`. Version `0.2.0` in `package.json` and +`src/shared/version.ts`. Validated on OpenCode `2.0.21` / Node `22.23.3` in Docker +image `opencode-plugin-lab:local` +(`sha256:76ebd805cd548a638b9b1508e5f16e680cd5b082cd3f3561d28f2f2301336ff1`), +145/145 lab tests and 322/322 local tests green. GitHub publication is the +remaining step; npm publication is deferred pending credentials. diff --git a/README.md b/README.md index 29575e7..7224fef 100755 --- a/README.md +++ b/README.md @@ -164,6 +164,9 @@ session-scoped; ignored by Git). ## Security guarantees and known limitations +This section is the security contract. To report a vulnerability, see +[SECURITY.md](SECURITY.md). + Verified against OpenCode `2.0.21` / `@opencode/plugin` `2.0.29` on the real runtime (isolated server, disposable project, loopback mock provider): a synthetic secret in a prompt is transmitted by a control run and is **absent** diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..a513568 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,78 @@ +# Security Policy + +strangerDanger is a secret- and PII-redaction guard for AI coding agents. It +sits at the OpenCode V2 request boundaries and tries to keep secrets out of the +model's context window. It is a defense-in-depth layer, **not** a guarantee that +no secret can ever reach a provider. The exact surfaces it protects, and the +blind spots it does not, are listed below and in the README. + +## Supported versions + +| Version | Supported | +| --- | --- | +| 0.2.x | Yes | +| 0.1.x | No — superseded by the V2 rewrite; upgrade to 0.2.x | + +Compatibility: OpenCode V2 runtime, `@opencode/plugin >= 2.0.0` +(validated against `@opencode/plugin@2.0.29` on OpenCode `2.0.21`). + +## Reporting a vulnerability + +Please report suspected vulnerabilities privately, not in a public issue: + +- Open a **private security advisory** at + https://github.com/gitdamnit/opencode-stranger-danger/security/advisories/new +- Or email the maintainer listed on the repository profile. + +Please include: affected version, OpenCode/Node versions, a description of the +issue, and a minimal reproduction if you have one. If a real secret is involved, +rotate it first — do **not** paste live credentials into a report. Use synthetic +values with the same shape. + +We aim to acknowledge reports within 7 days and to publish a fix or an accurate +assessment as soon as practical. There is no bug-bounty program. + +## What this plugin protects + +- **Prompts, tool results, and assembled model requests** — the `session.prompt`, + `session.context`, `session.generate`, `session.title`, and + `session.compaction` hooks redact text (and string leaves of plain + objects/arrays) immediately before transmission, plus `tool.execute.after` for + tool output, error messages, and error metadata. +- **File inspection is read-only.** `tool.execute.before` detects and audits; it + never rewrites a file on disk. +- **No raw secrets in output.** The CLI, the plugin tools, and the audit log + report safe metadata (rule id, description, source, line/column, an 8-hex + fingerprint, and the redaction token) — never the matched value. +- **Fail closed.** Content that cannot be inspected (over the size ceiling, a + window timeout or error, a cyclic or over-deep structure) is replaced by an + explicit `[REDACTED:...]` marker rather than passed through. + +## Known limitations (by design) + +- **Provider-native WebSocket traffic** is not covered. The experimental + `experimental.ws.*` hooks are deliberately not registered. Plain-HTTP + providers and the HTTP fallback of a WebSocket route are covered. +- **Raw HTTP bodies.** The plugin edits assembled `system`/`messages`, not + `http.request` bodies. Content a provider layer adds outside those is not + scanned. `event.subscribe` is observation-only (post-admission). +- **Structured values.** Only string leaves of plain objects/arrays are + redacted. Exotic objects (`Map`, `Set`, `Date`, typed arrays) are passed + through unchanged; nesting deeper than 32 levels, and cycles, fail closed. +- **Encoded secrets.** Detection is pattern- and entropy-based. Base64- or + otherwise obfuscated secrets that match no rule are not detected by that rule. +- **Not a prevention guarantee.** A secret that no rule matches, or that is + transmitted through a surface listed above, is not stopped. Treat this as one + layer alongside `.gitignore`, secret managers, and provider-side controls. + +## Recommended operational practice + +- Treat the audit log (`/.opencode/state/strangerdanger-audit.jsonl`) + as sensitive operational metadata even though it never stores secret values. + It is written `0600` inside a `0700` directory and is git-ignored. +- Rotate any credential the moment you suspect it was transmitted, regardless of + what this plugin reported. + +## License + +MIT — see [LICENSE](LICENSE). diff --git a/package.json b/package.json index 918212b..9a9c6b2 100755 --- a/package.json +++ b/package.json @@ -2,7 +2,25 @@ "name": "@gitdamnit/stranger-danger", "version": "0.2.0", "description": "Secret and PII redaction guard for AI coding agents — scans files, context windows, and outputs before secrets reach LLMs", + "keywords": [ + "opencode", + "security", + "secrets", + "redaction", + "pii", + "gitleaks", + "plugin" + ], + "author": "gitdamnit", "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/gitdamnit/opencode-stranger-danger.git" + }, + "bugs": { + "url": "https://github.com/gitdamnit/opencode-stranger-danger/issues" + }, + "homepage": "https://github.com/gitdamnit/opencode-stranger-danger#readme", "type": "module", "main": "dist/plugin/index.js", "exports": { @@ -17,7 +35,7 @@ "dist/", "rules/gitleaks-rules.toml", ".strangerdanger-allowlist.toml", - "strangerdanger.jpeg", + "strangerdanger2.jpeg", "README.md", "CHANGELOG.md" ], @@ -46,6 +64,9 @@ "typescript": "^5.6.0", "typescript-eslint": "^8.0.0" }, + "publishConfig": { + "access": "public" + }, "engines": { "node": ">=20.0.0" }