0595794385
- Default export is { id, setup, server }: OpenCode 2 calls setup(ctx), OpenCode 1 (1.18.29+) calls server()
- Tools defined once in lib/tools.ts and registered for both versions (V2 with codemode: false)
- Blocking covers path args of any tool, patch/apply_patch envelopes, shell commands naming env files, grep output, and V2 permission checks
- Secret-looking values from the project's env files are replaced in every non-envsitter tool's output
- Protected files come from envsitter's path policy; example/template files stay readable
- npm run test:e2e drives the real OpenCode 1 and 2 CLIs with a scripted model
- @opencode-ai/plugin ^1.19.3, @opencode/plugin ^2.1.0 (peer >=2.0.32), envsitter ^0.1.0, TypeScript 6
BREAKING CHANGE: the named export EnvSitterGuard is now the plugin definition object; call EnvSitterGuard.server(input) instead of EnvSitterGuard(input).
73 lines
3.2 KiB
TypeScript
73 lines
3.2 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import test from "node:test";
|
|
|
|
import { createScrubLoader, findSensitiveEnvFiles, scrubToolResult } from "../lib/scrub.js";
|
|
|
|
const TOKEN = "tok_live_9f8a7b6c5d4e";
|
|
const DB_PASSWORD = "s3cretPassw0rd";
|
|
|
|
async function createProject(files: Record<string, string>): Promise<string> {
|
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "envsitter-guard-scrub-"));
|
|
for (const [name, contents] of Object.entries(files)) {
|
|
await fs.mkdir(path.dirname(path.join(dir, name)), { recursive: true });
|
|
await fs.writeFile(path.join(dir, name), contents);
|
|
}
|
|
return dir;
|
|
}
|
|
|
|
test("finds sensitive env files in the project, skipping examples and dependency folders", async () => {
|
|
const root = await createProject({
|
|
".env": `API_TOKEN=${TOKEN}\n`,
|
|
".env.example": "API_TOKEN=replace-me\n",
|
|
"apps/web/.env.local": `DATABASE_URL=postgres://app:${DB_PASSWORD}@db:5432/app\n`,
|
|
"node_modules/pkg/.env": "X=1\n",
|
|
"a/b/c/d/.env": "TOO_DEEP=1\n",
|
|
});
|
|
|
|
const files = (await findSensitiveEnvFiles(root)).map((file) => path.relative(root, file));
|
|
assert.deepEqual(files, [".env", path.join("apps", "web", ".env.local")]);
|
|
});
|
|
|
|
test("scrub replaces secret values from every env file and leaves other text alone", async () => {
|
|
const root = await createProject({
|
|
".env": `API_TOKEN=${TOKEN}\nNODE_ENV=development\n`,
|
|
"apps/web/.env.local": `DATABASE_URL=postgres://app:${DB_PASSWORD}@db:5432/app\n`,
|
|
});
|
|
|
|
const scrub = await createScrubLoader(root)();
|
|
const output = scrub(`Bearer ${TOKEN}\npassword=${DB_PASSWORD}\nNODE_ENV=development`);
|
|
assert.equal(output, "Bearer [envsitter: API_TOKEN redacted]\npassword=[envsitter: DATABASE_URL redacted]\nNODE_ENV=development");
|
|
});
|
|
|
|
test("scrub picks up env file changes", async () => {
|
|
const root = await createProject({ ".env": `API_TOKEN=${TOKEN}\n` });
|
|
const loadScrub = createScrubLoader(root);
|
|
assert.ok(!(await loadScrub())(TOKEN).includes(TOKEN));
|
|
|
|
const rotated = "tok_live_0a1b2c3d4e5f6a7b8c";
|
|
await fs.writeFile(path.join(root, ".env"), `API_TOKEN=${rotated}\n`);
|
|
const scrub = await loadScrub();
|
|
assert.equal(scrub(rotated), "[envsitter: API_TOKEN redacted]");
|
|
assert.equal(scrub(TOKEN), TOKEN);
|
|
});
|
|
|
|
test("scrubToolResult scrubs text content and structured output, and keeps untouched results identical", () => {
|
|
const scrub = (text: string) => text.replaceAll(TOKEN, "[x]");
|
|
|
|
const result = {
|
|
content: [{ type: "text", text: `token ${TOKEN}` }, { type: "image", data: TOKEN }],
|
|
output: { stdout: `ok ${TOKEN}`, exitCode: 0, lines: [TOKEN, "fine"] },
|
|
metadata: { title: "t" },
|
|
};
|
|
const scrubbed = scrubToolResult(result, scrub);
|
|
assert.deepEqual(scrubbed.content, [{ type: "text", text: "token [x]" }, { type: "image", data: TOKEN }]);
|
|
assert.deepEqual(scrubbed.output, { stdout: "ok [x]", exitCode: 0, lines: ["[x]", "fine"] });
|
|
assert.equal(scrubbed.metadata, result.metadata);
|
|
|
|
const clean = { content: "nothing secret", output: { exitCode: 0 } };
|
|
assert.equal(scrubToolResult(clean, scrub), clean);
|
|
});
|