af471c9821
- parseDotenv, validate, and every edit share the round-trip document parser - Path policy helpers (isEnvFile, isSensitiveEnvFile, isEnvSitterPepperFile) used by envsitter-guard - CLI file commands run through the library file operations - EnvSitter#redactor() and the redact command replace secret-looking values in text - Writes keep file permissions and go through symlinks; new files are 0600 - An existing pepper is never replaced; a new .envsitter/ gets a .gitignore - set/add/copy into empty files write clean output - Regexes with g/y flags no longer skip keys - TypeScript 6, Node 22+ engines, prepack build, dist/test excluded from the package
151 lines
5.6 KiB
TypeScript
151 lines
5.6 KiB
TypeScript
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 matchKey supports matcher operators', async () => {
|
|
const filePath = await makeTempDotenv(
|
|
[
|
|
'EMPTY=',
|
|
'SPACE=" "',
|
|
'TOKEN=sk-test-123',
|
|
'N1=42',
|
|
'N2=3.14',
|
|
'N3= -2 ',
|
|
'B1=true',
|
|
'B2=FALSE',
|
|
'S=hello'
|
|
].join('\n') + '\n'
|
|
);
|
|
const es = EnvSitter.fromDotenvFile(filePath);
|
|
|
|
assert.equal(await es.matchKey('TOKEN', { op: 'exists' }), true);
|
|
assert.equal(await es.matchKey('MISSING', { op: 'exists' }), false);
|
|
|
|
assert.equal(await es.matchKey('EMPTY', { op: 'is_empty' }), true);
|
|
assert.equal(await es.matchKey('SPACE', { op: 'is_empty' }), false);
|
|
|
|
assert.equal(await es.matchKey('TOKEN', { op: 'is_equal', candidate: 'sk-test-123' }), true);
|
|
assert.equal(await es.matchKey('TOKEN', { op: 'is_equal', candidate: 'nope' }), false);
|
|
|
|
assert.equal(await es.matchKey('TOKEN', { op: 'partial_match_prefix', prefix: 'sk-' }), true);
|
|
assert.equal(await es.matchKey('TOKEN', { op: 'partial_match_suffix', suffix: '123' }), true);
|
|
assert.equal(await es.matchKey('TOKEN', { op: 'partial_match_regex', regex: /^sk-[a-z]+-\d+$/ }), true);
|
|
|
|
assert.equal(await es.matchKey('N1', { op: 'is_number' }), true);
|
|
assert.equal(await es.matchKey('N2', { op: 'is_number' }), true);
|
|
assert.equal(await es.matchKey('N3', { op: 'is_number' }), true);
|
|
assert.equal(await es.matchKey('S', { op: 'is_number' }), false);
|
|
|
|
assert.equal(await es.matchKey('B1', { op: 'is_boolean' }), true);
|
|
assert.equal(await es.matchKey('B2', { op: 'is_boolean' }), true);
|
|
assert.equal(await es.matchKey('N1', { op: 'is_boolean' }), false);
|
|
|
|
assert.equal(await es.matchKey('S', { op: 'is_string' }), true);
|
|
assert.equal(await es.matchKey('N1', { op: 'is_string' }), false);
|
|
assert.equal(await es.matchKey('B1', { op: 'is_string' }), false);
|
|
});
|
|
|
|
test('EnvSitter matchKeyBulk supports matcher operators', async () => {
|
|
const filePath = await makeTempDotenv('K1=V1\nK2=\nK3=123\n');
|
|
const es = EnvSitter.fromDotenvFile(filePath);
|
|
|
|
assert.deepEqual(await es.matchKeyBulk(['K1', 'K2', 'MISSING'], { op: 'exists' }), [
|
|
{ key: 'K1', match: true },
|
|
{ key: 'K2', match: true },
|
|
{ key: 'MISSING', match: false }
|
|
]);
|
|
|
|
assert.deepEqual(await es.matchKeyBulk(['K1', 'K2', 'K3'], { op: 'is_empty' }), [
|
|
{ key: 'K1', match: false },
|
|
{ key: 'K2', match: true },
|
|
{ key: 'K3', match: false }
|
|
]);
|
|
|
|
assert.deepEqual(await es.matchKeyBulk(['K1', 'K2', 'K3'], { op: 'is_number' }), [
|
|
{ key: 'K1', match: false },
|
|
{ key: 'K2', match: false },
|
|
{ key: 'K3', match: true }
|
|
]);
|
|
});
|
|
|
|
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'] }
|
|
]);
|
|
});
|
|
|
|
test('EnvSitter regex filters and matchers are stable with the global flag', async () => {
|
|
const filePath = await makeTempDotenv('API_A=https://a.example\nAPI_B=https://b.example\nAPI_C=https://c.example\nOTHER=https://x.example\n');
|
|
const es = EnvSitter.fromDotenvFile(filePath);
|
|
|
|
assert.deepEqual(await es.listKeys({ filter: /^API_/g }), ['API_A', 'API_B', 'API_C']);
|
|
|
|
const matches = await es.matchKeyBulk(['API_A', 'API_B', 'API_C'], { op: 'partial_match_regex', regex: /^https:/g });
|
|
assert.deepEqual(
|
|
matches.map((m) => m.match),
|
|
[true, true, true]
|
|
);
|
|
|
|
const findings = await es.scan({ keysFilter: /^API_/gy, detect: ['url'] });
|
|
assert.deepEqual(
|
|
findings.map((f) => f.key),
|
|
['API_A', 'API_B', 'API_C']
|
|
);
|
|
});
|