diff --git a/lib/ai/secret.js b/lib/ai/secret.js new file mode 100644 index 0000000..a07ae7d --- /dev/null +++ b/lib/ai/secret.js @@ -0,0 +1,18 @@ +import { readFileSync } from 'node:fs'; + +// Resolve a vault_path-style secret reference. v1 supports: +// env:NAME -> process.env.NAME +// file:/path -> file contents (trimmed) +// -> returned as-is +// Vaultwarden item-id resolution is a future swap (see spec). +export function resolveSecret(spec) { + if (!spec) return null; + if (spec.startsWith('env:')) { + return process.env[spec.slice(4)] ?? null; + } + if (spec.startsWith('file:')) { + try { return readFileSync(spec.slice(5), 'utf8').trim(); } + catch { return null; } + } + return spec; +} diff --git a/tests/ai/secret.test.js b/tests/ai/secret.test.js new file mode 100644 index 0000000..5200fa4 --- /dev/null +++ b/tests/ai/secret.test.js @@ -0,0 +1,30 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { writeFileSync, mkdtempSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { resolveSecret } from '../../lib/ai/secret.js'; + +describe('resolveSecret', () => { + beforeEach(() => { delete process.env.__SECRET_TEST; }); + + it('resolves env: specs', () => { + process.env.__SECRET_TEST = 'sk-from-env'; + expect(resolveSecret('env:__SECRET_TEST')).toBe('sk-from-env'); + }); + + it('resolves file: specs (trimmed)', () => { + const dir = mkdtempSync(join(tmpdir(), 'sec-')); + const f = join(dir, 'key'); + writeFileSync(f, 'sk-from-file\n'); + expect(resolveSecret('file:' + f)).toBe('sk-from-file'); + }); + + it('returns a raw value unchanged', () => { + expect(resolveSecret('sk-raw')).toBe('sk-raw'); + }); + + it('returns null for empty/missing', () => { + expect(resolveSecret('')).toBeNull(); + expect(resolveSecret('env:__SECRET_TEST')).toBeNull(); + }); +});