import test from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, writeFileSync, chmodSync, symlinkSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; const { Credentials } = await import('../src/credentials.mjs').catch((e) => { if (e.code === 'ERR_MODULE_NOT_FOUND') return {}; throw e; }); function fixture(t) { assert.equal(typeof Credentials, 'function'); const root = mkdtempSync(join(tmpdir(), 'bus-token-')); t.after(() => rmSync(root, { recursive: true, force: true })); const token = 'fixture_only_0123456789abcdef'; const path = join(root, 'token'); writeFileSync(path, token + '\n', { mode: 0o600 }); return { root, path, token, ref: { file: path, rotateBy: '2099-01-01' } }; } test('only validated broker references load; returned data and exceptions cannot expose a known token', async (t) => { const { ref, token } = fixture(t); const c = new Credentials({ references: { coder: { gitea: ref } }, repoRoots: [], dataRoot: '/nonexistent-data', }); assert.equal( await c.use('coder', 'gitea', async (value) => ({ matches: value === token })).then((x) => x.matches), true, ); await assert.rejects( c.use('coder', 'gitea', async (value) => ({ echo: value })), /credential-leak/, ); await assert.rejects( c.use('coder', 'gitea', async (value) => { throw Error(value); }), /service-failed/, ); assert.throws(() => c.assertClean({ [token]: 'x' }), /credential-leak/); assert.throws(() => c.assertClean({ nested: ['prefix ' + token] }), /credential-leak/); c.close(); await assert.rejects( c.use('coder', 'gitea', () => true), /credential-unavailable/, ); }); test('bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse', (t) => { const { path, root, ref } = fixture(t); const create = (r = ref, extra = {}) => new Credentials({ references: { coder: { gitea: r } }, repoRoots: [], dataRoot: '/no-data', ...extra }); chmodSync(path, 0o644); assert.throws(() => create(), /credential-file/); chmodSync(path, 0o600); const link = join(root, 'link'); symlinkSync(path, link); assert.throws(() => create({ ...ref, file: link }), /credential-file/); assert.throws(() => create(ref, { repoRoots: [root] }), /credential-location/); assert.throws(() => create(ref, { dataRoot: root }), /credential-location/); assert.throws(() => create({ file: path }), /credential-date/); for (const value of ['', 'value\nsecond', 'value\r\n', 'space value', '"value"']) { writeFileSync(path, value); assert.throws(() => create(), /credential-format/); } }); test('expiry refuses use and env references never become client data', async (t) => { fixture(t); const c = new Credentials({ references: { coder: { vikunja: { env: 'FIXTURE_BUS_TOKEN', expires: '2000-01-01' } } }, env: { FIXTURE_BUS_TOKEN: 'fixture_expired_0123456789' }, repoRoots: [], dataRoot: '/no-data', }); await assert.rejects( c.use('coder', 'vikunja', () => true), /credential-expired/, ); await assert.rejects( c.use('pm', 'vikunja', () => true), /credential-unavailable/, ); }); test('S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state', async (t) => { const { ref } = fixture(t); const c = new Credentials({ references: { coder: { gitea: { ...ref, service: 'gitea', rotateBy: '2000-01-01' } } }, }); assert.equal(await c.use('coder', 'gitea', () => true), true); assert.deepEqual(c.status(), [ { instance: 'coder', service: 'gitea', date: '2000-01-01', state: 'rotation-due' }, ]); assert.throws( () => new Credentials({ references: { coder: { gitea: { ...ref, service: 'vikunja' } } } }), /credential-reference/, ); }); test('opaque tokens shorter than 16 characters refuse before use', () => { for (const value of ['a', 'a'.repeat(15), 'a'.repeat(15) + '\n']) assert.throws( () => new Credentials({ references: { coder: { gitea: { env: 'FIXTURE_TOKEN', rotateBy: '2099-01-01' } } }, env: { FIXTURE_TOKEN: value }, }), /credential-format/, ); const c = new Credentials({ references: { coder: { gitea: { env: 'FIXTURE_TOKEN', rotateBy: '2099-01-01' } } }, env: { FIXTURE_TOKEN: 'a'.repeat(16) }, }); c.close(); });