Initial envsitter CLI and safe env matching

This commit is contained in:
David Ibia
2026-01-12 10:30:49 +01:00
commit 3993746843
17 changed files with 1001 additions and 0 deletions

View File

@@ -0,0 +1,40 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { parseDotenv } from '../dotenv/parse.js';
test('parseDotenv parses basic assignments and ignores comments', () => {
const input = [
'# comment',
'FOO=bar',
'export BAZ=qux',
'EMPTY=',
'TRAILING=ok # inline',
''
].join('\n');
const parsed = parseDotenv(input);
assert.equal(parsed.errors.length, 0);
assert.equal(parsed.values.get('FOO'), 'bar');
assert.equal(parsed.values.get('BAZ'), 'qux');
assert.equal(parsed.values.get('EMPTY'), '');
assert.equal(parsed.values.get('TRAILING'), 'ok');
});
test('parseDotenv supports quoted values', () => {
const input = [
"SINGLE='a b c'",
'DOUBLE="a\\n\\t\\r\\\\b"'
].join('\n');
const parsed = parseDotenv(input);
assert.equal(parsed.errors.length, 0);
assert.equal(parsed.values.get('SINGLE'), 'a b c');
assert.equal(parsed.values.get('DOUBLE'), 'a\n\t\r\\b');
});
test('parseDotenv reports invalid keys', () => {
const input = 'NOT-OK=value\nOK=value2';
const parsed = parseDotenv(input);
assert.ok(parsed.errors.length >= 1);
assert.equal(parsed.values.get('OK'), 'value2');
});

View File

@@ -0,0 +1,65 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtemp, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { EnvSitter } from '../envsitter.js';
async function makeTempDotenv(contents: string): Promise<string> {
const dir = await mkdtemp(join(tmpdir(), 'envsitter-'));
const filePath = join(dir, '.env');
await writeFile(filePath, contents, 'utf8');
return filePath;
}
test('EnvSitter lists keys and fingerprints without returning values', async () => {
const filePath = await makeTempDotenv('A=1\nB=two\n');
const es = EnvSitter.fromDotenvFile(filePath);
const keys = await es.listKeys();
assert.deepEqual(keys, ['A', 'B']);
const fp = await es.fingerprintKey('B');
assert.equal(fp.key, 'B');
assert.equal(fp.algorithm, 'hmac-sha256');
assert.equal(fp.length, 3);
assert.ok(fp.fingerprint.length > 10);
});
test('EnvSitter matches a candidate for a key (outside-in)', async () => {
const filePath = await makeTempDotenv('OPENAI_API_KEY=sk-test-123\n');
const es = EnvSitter.fromDotenvFile(filePath);
assert.equal(await es.matchCandidate('OPENAI_API_KEY', 'sk-test-123'), true);
assert.equal(await es.matchCandidate('OPENAI_API_KEY', 'nope'), false);
assert.equal(await es.matchCandidate('MISSING', 'sk-test-123'), false);
});
test('EnvSitter bulk matching works across keys and by-key candidates', async () => {
const filePath = await makeTempDotenv('K1=V1\nK2=V2\n');
const es = EnvSitter.fromDotenvFile(filePath);
const bulk = await es.matchCandidateBulk(['K1', 'K2'], 'V2');
assert.deepEqual(bulk, [
{ key: 'K1', match: false },
{ key: 'K2', match: true }
]);
const byKey = await es.matchCandidatesByKey({ K1: 'V1', K2: 'nope' });
assert.deepEqual(byKey, [
{ key: 'K1', match: true },
{ key: 'K2', match: false }
]);
});
test('EnvSitter scan detects JWT-like and URL values without exposing them', async () => {
const jwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMifQ.sgn';
const filePath = await makeTempDotenv(`JWT=${jwt}\nURL=https://example.com\nNOISE=hello\n`);
const es = EnvSitter.fromDotenvFile(filePath);
const findings = await es.scan({ detect: ['jwt', 'url', 'base64'] });
assert.deepEqual(findings, [
{ key: 'JWT', detections: ['jwt'] },
{ key: 'URL', detections: ['url'] }
]);
});

33
src/test/pepper.test.ts Normal file
View File

@@ -0,0 +1,33 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtemp, readFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { resolvePepper } from '../pepper.js';
test('resolvePepper reads from env when set', async () => {
const prev = process.env.ENVSITTER_PEPPER;
process.env.ENVSITTER_PEPPER = 'unit-test-pepper';
try {
const pepper = await resolvePepper({ createIfMissing: false });
assert.equal(pepper.source, 'env');
assert.equal(new TextDecoder().decode(pepper.pepperBytes), 'unit-test-pepper');
} finally {
if (prev === undefined) delete process.env.ENVSITTER_PEPPER;
else process.env.ENVSITTER_PEPPER = prev;
}
});
test('resolvePepper creates a pepper file when missing', async () => {
const dir = await mkdtemp(join(tmpdir(), 'envsitter-'));
const pepperPath = join(dir, 'pepper');
const pepper = await resolvePepper({ pepperFilePath: pepperPath });
assert.equal(pepper.source, 'file');
assert.equal(pepper.pepperFilePath, pepperPath);
assert.ok(pepper.pepperBytes.length >= 16);
const persisted = (await readFile(pepperPath, 'utf8')).trim();
assert.ok(persisted.length > 0);
});