Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,12 @@ filtering flags as search.
To skip paths inside that root for one search, pass `--exclude` with a gitignore pattern
relative to the root, for example `--exclude '**/*.test.ts' --exclude 'src/generated/'`.

The default credential-name filter excludes `.env`, `.env.*`, and path components
ending in `.env`, case-insensitively. This includes files such as `keys.env` and
directories such as `samples.env` and their contents. Use `--include-sensitive`
to include them; hidden paths also need `--hidden`. Explicit `--exclude` patterns
still apply.

`jg auth` asks for your provider, then saves its key in an owner-only config file.
Re-running auth replaces that setup; searches always use the saved provider.
`jg doctor` checks it with synthetic input. Existing saved keys without a provider
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export const filesystemDefaults = Object.freeze({
".npmrc",
".pypirc",
] as readonly string[],
sensitiveSuffixes: [".pem", ".key", ".p12", ".pfx"] as readonly string[],
sensitiveSuffixes: [".env", ".pem", ".key", ".p12", ".pfx"] as readonly string[],
pageSize: 128,
maxOpenDirectories: 64,
maxFileBytes: 16 * 1024 * 1024,
Expand Down
108 changes: 108 additions & 0 deletions packages/core/test/filesystem.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,114 @@ async function fixture(files: Record<string, string | Uint8Array>) {
return root;
}

test("env-suffixed files are excluded case-insensitively at any depth", async () => {
const root = await fixture({
"keys.env": "ROOT_ENV_FIXTURE",
"nested/prod.env": "NESTED_ENV_FIXTURE",
"nested/foo.ENV": "UPPERCASE_ENV_FIXTURE",
"source.ts": "export const visible = true;\n",
});
const reader = await createFilesystem({ root });
try {
for (const path of ["keys.env", "nested/prod.env", "nested/foo.ENV"])
expect(await reader.readSnapshot(path)).toMatchObject({
status: "excluded",
reason: "sensitive_name",
});
expect((await reader.listPage()).entries.map((entry) => entry.path).sort()).toEqual([
"nested",
"source.ts",
]);
expect((await reader.listPage("nested")).entries).toEqual([]);
} finally {
await reader.close();
}
});

test("hidden env files require independent hidden and sensitive overrides", async () => {
const root = await fixture({
".env": "DOT_ENV_FIXTURE",
".env.local": "LOCAL_ENV_FIXTURE",
"keys.env": "SUFFIX_ENV_FIXTURE",
});
for (const policy of [
{},
{ hidden: true },
{ includeSensitive: true },
{ hidden: true, includeSensitive: true },
]) {
const reader = await createFilesystem({ root, policy });
try {
expect((await reader.readSnapshot("keys.env")).status).toBe(
"includeSensitive" in policy ? "ok" : "excluded",
);
for (const path of [".env", ".env.local"])
expect((await reader.readSnapshot(path)).status).toBe(
"hidden" in policy && "includeSensitive" in policy ? "ok" : "excluded",
);
} finally {
await reader.close();
}
}
});

test("env-like and ordinary configuration filenames remain eligible", async () => {
const paths = [
"keys.env.example",
"config.env.json",
"settings.json",
"config.toml",
"keys.json",
];
const root = await fixture(
Object.fromEntries(paths.map((path) => [path, "CONFIGURATION_FIXTURE"])),
);
const reader = await createFilesystem({ root });
try {
for (const path of paths)
expect(await reader.readSnapshot(path)).toMatchObject({
status: "ok",
snapshot: { path, source: "CONFIGURATION_FIXTURE" },
});
expect((await reader.listPage()).entries.map((entry) => entry.path).sort()).toEqual(
paths.sort(),
);
} finally {
await reader.close();
}
});

test("env-suffixed directories are pruned unless sensitive paths are included", async () => {
const root = await fixture({
"samples.env/fixture.ts": "export const sample = true;\n",
"source.ts": "export const visible = true;\n",
});
const reader = await createFilesystem({ root });
try {
expect((await reader.listPage()).entries.map((entry) => entry.path)).toEqual(["source.ts"]);
expect((await reader.listPage("samples.env")).entries).toEqual([]);
expect(await reader.readSnapshot("samples.env/fixture.ts")).toMatchObject({
status: "excluded",
reason: "sensitive_name",
});
} finally {
await reader.close();
}
const included = await createFilesystem({ root, policy: { includeSensitive: true } });
try {
expect((await included.listPage()).entries.map((entry) => entry.path).sort()).toEqual([
"samples.env",
"source.ts",
]);
expect(await included.readSnapshot("samples.env/fixture.ts")).toMatchObject({
status: "ok",
snapshot: { path: "samples.env/fixture.ts", source: "export const sample = true;\n" },
});
} finally {
await included.close();
}
});

test("one reader excludes ignored, hidden, dependent, sensitive and binary content from plain roots", async () => {
const root = await fixture({
".gitignore": "ignored.txt\n",
Expand Down
107 changes: 107 additions & 0 deletions test/retrieval.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,113 @@ import { testIfDocker } from "./helpers/docker";
import { retrieve } from "../packages/core/src/retrieve";
import { createEvaluator, EvaluationFailure } from "../packages/core/src/evaluator";

testIfDocker(
"env-suffixed files never reach the provider by default",
async () => {
const root = await mkdtemp(join(tmpdir(), "jg-env-suffix-"));
const sent: string[] = [];
const server = Bun.serve({
port: 0,
async fetch(request) {
const body = (await request.json()) as { questions: Record<string, unknown> };
sent.push(JSON.stringify(body));
return Response.json({
answers: Object.fromEntries(
Object.keys(body.questions).map((id) => [id, { type: "noul", noul: 0.9 }]),
),
});
},
});
try {
await writeFile(join(root, "keys.env"), "FIXTURE_VALUE=ENV_SUFFIX_EXCLUDED_FILE_MARKER\n");
await writeFile(
join(root, "source.ts"),
'export const sourceMarker = "ENV_SUFFIX_NORMAL_SOURCE_MARKER";\n',
);
const signal = new AbortController().signal;
const evaluator = createEvaluator({
apiKey: "fixture",
provider: "vercel",
fetch: routeProviderFetch(fetch, `http://127.0.0.1:${server.port}`),
signal,
});
const result = await retrieve({ root, query: "find fixture source", signal }, evaluator);
expect(result.status).toBe("complete");
expect(sent.length).toBeGreaterThan(0);
expect(result.counts.requests).toBe(sent.length);
expect(sent.join("\n")).toContain("ENV_SUFFIX_NORMAL_SOURCE_MARKER");
expect(sent.join("\n")).not.toContain("ENV_SUFFIX_EXCLUDED_FILE_MARKER");
expect(result.files.map((file) => file.path)).toEqual(["source.ts"]);
} finally {
server.stop(true);
await rm(root, { recursive: true, force: true });
}
},
120_000,
);

testIfDocker(
"sensitive overrides admit env suffixes without bypassing explicit excludes",
async () => {
const root = await mkdtemp(join(tmpdir(), "jg-env-override-"));
const sent: string[] = [];
const server = Bun.serve({
port: 0,
async fetch(request) {
const body = (await request.json()) as { questions: Record<string, unknown> };
sent.push(JSON.stringify(body));
return Response.json({
answers: Object.fromEntries(
Object.keys(body.questions).map((id) => [id, { type: "noul", noul: 0.9 }]),
),
});
},
});
try {
await writeFile(join(root, "keys.env"), "FIXTURE_VALUE=ENV_SUFFIX_OVERRIDE_MARKER\n");
for (const path of [".env", ".env.local"])
await writeFile(join(root, path), "FIXTURE_VALUE=ENV_SUFFIX_HIDDEN_MARKER\n");
await writeFile(
join(root, "source.ts"),
'export const sourceMarker = "ENV_OVERRIDE_NORMAL_SOURCE_MARKER";\n',
);
for (const policy of [
{ includeSensitive: true },
{ includeSensitive: true, exclude: ["keys.env"] },
]) {
sent.length = 0;
const signal = new AbortController().signal;
const evaluator = createEvaluator({
apiKey: "fixture",
provider: "vercel",
fetch: routeProviderFetch(fetch, `http://127.0.0.1:${server.port}`),
signal,
});
const result = await retrieve(
{ root, query: "find fixture source", policy, signal },
evaluator,
);
expect(result.status).toBe("complete");
expect(sent.length).toBeGreaterThan(0);
expect(result.counts.requests).toBe(sent.length);
expect(sent.join("\n")).toContain("ENV_OVERRIDE_NORMAL_SOURCE_MARKER");
expect(sent.join("\n")).not.toContain("ENV_SUFFIX_HIDDEN_MARKER");
if (policy.exclude) {
expect(sent.join("\n")).not.toContain("ENV_SUFFIX_OVERRIDE_MARKER");
expect(result.files.map((file) => file.path)).toEqual(["source.ts"]);
} else {
expect(sent.join("\n")).toContain("ENV_SUFFIX_OVERRIDE_MARKER");
expect(result.files.map((file) => file.path).sort()).toEqual(["keys.env", "source.ts"]);
}
}
} finally {
server.stop(true);
await rm(root, { recursive: true, force: true });
}
},
120_000,
);

testIfDocker(
"hierarchical retrieval returns source from multiple files without uploading excluded data",
async () => {
Expand Down