Files
stack/packages/mosaic/src/credentials/file-credential-store.spec.ts
T

161 lines
5.8 KiB
TypeScript

import { chmod, copyFile, mkdir, symlink, unlink, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { mkdtemp } from 'node:fs/promises';
import { afterEach, describe, expect, it } from 'vitest';
import { rm } from 'node:fs/promises';
import { parseCredentialEstateRegistry } from './estate-registry.js';
import { FileCredentialResolver, FileCredentialStore } from './file-credential-store.js';
let cleanup: string | undefined;
async function fixtureRoot(): Promise<string> {
cleanup = await mkdtemp(join(tmpdir(), 'mosaic-cred-store-'));
const root = join(cleanup, 'tokens');
await mkdir(root, { mode: 0o700 });
return root;
}
function registry(): ReturnType<typeof parseCredentialEstateRegistry> {
return parseCredentialEstateRegistry(
JSON.stringify({
version: 1,
estates: [
{
name: 'homelab',
hosts: [
{
host: 'git.example.invalid',
provider: 'gitea',
apiBaseUrl: 'https://git.example.invalid',
tokenPrefix: 'gitea-example',
},
],
},
],
}),
);
}
afterEach(async (): Promise<void> => {
if (cleanup !== undefined) await rm(cleanup, { recursive: true, force: true });
cleanup = undefined;
});
describe('phase-1 governed file credential resolver', (): void => {
it('resolves only the exact estate/host/identity token at a test-overridable root', async (): Promise<void> => {
const root = await fixtureRoot();
await writeFile(join(root, 'gitea-example-seat.token'), 'canary-token', { mode: 0o600 });
const resolver = new FileCredentialResolver(root, registry());
const resolved = await resolver.resolve('seat', 'homelab', 'git.example.invalid');
const wrongEstate = await resolver.resolve('seat', 'usc', 'git.example.invalid');
expect(resolved?.identity).toBe('seat');
expect(Buffer.from(resolved?.secret ?? []).toString('utf8')).toBe('canary-token');
expect(wrongEstate).toBeUndefined();
});
it('rejects a group-writable token directory even when the token file is private', async (): Promise<void> => {
const root = await fixtureRoot();
await writeFile(join(root, 'gitea-example-seat-name.token'), 'private-token', {
mode: 0o600,
});
await chmod(root, 0o770);
const resolver = new FileCredentialResolver(root, registry());
await expect(resolver.resolve('seat-name', 'homelab', 'git.example.invalid')).rejects.toThrow(
/insecure-token-owner/,
);
});
it('rejects a token file with group or other permissions', async (): Promise<void> => {
const root = await fixtureRoot();
const path = join(root, 'gitea-example-seat.token');
await writeFile(path, 'canary-token', { mode: 0o600 });
await chmod(path, 0o640);
const resolver = new FileCredentialResolver(root, registry());
await expect(resolver.resolve('seat', 'homelab', 'git.example.invalid')).rejects.toThrow(
/insecure-token-mode/,
);
});
it('rejects a symlinked token instead of following it', async (): Promise<void> => {
const root = await fixtureRoot();
const target = join(cleanup ?? root, 'outside-token');
await writeFile(target, 'canary-token', { mode: 0o600 });
await symlink(target, join(root, 'gitea-example-seat.token'));
const resolver = new FileCredentialResolver(root, registry());
await expect(resolver.resolve('seat', 'homelab', 'git.example.invalid')).rejects.toThrow(
/symbolic link|unavailable/,
);
});
it('rejects traversal-shaped identities before touching storage', async (): Promise<void> => {
const root = await fixtureRoot();
const resolver = new FileCredentialResolver(root, registry());
await expect(resolver.resolve('../other', 'homelab', 'git.example.invalid')).rejects.toThrow(
/invalid-identity/,
);
});
it('atomically stores, lists, reads binding metadata, and removes a governed credential', async (): Promise<void> => {
const root = await fixtureRoot();
const store = new FileCredentialStore(root, registry());
await store.put(
{
identity: 'seat',
estate: 'homelab',
host: 'git.example.invalid',
providerLogin: 'seat',
tokenName: 'mosaic-seat-1',
scopes: ['write:repository'],
createdAt: '2026-08-05T00:00:00.000Z',
},
new TextEncoder().encode('new-private-token'),
);
await expect(store.list('homelab', 'git.example.invalid')).resolves.toEqual(['seat']);
await expect(
store.readBinding('seat', 'homelab', 'git.example.invalid'),
).resolves.toMatchObject({
providerLogin: 'seat',
tokenName: 'mosaic-seat-1',
});
await expect(
new FileCredentialResolver(root, registry()).resolve(
'seat',
'homelab',
'git.example.invalid',
),
).resolves.toMatchObject({ identity: 'seat' });
await copyFile(
join(root, 'gitea-example-seat.credential.json'),
join(root, 'gitea-example-other.credential.json'),
);
await expect(
new FileCredentialResolver(root, registry()).resolve(
'other',
'homelab',
'git.example.invalid',
),
).rejects.toThrow(/credential-binding-mismatch/);
await unlink(join(root, 'gitea-example-other.credential.json'));
await store.remove('seat', 'homelab', 'git.example.invalid');
await expect(store.list('homelab', 'git.example.invalid')).resolves.toEqual([]);
});
it('returns undefined for an absent token without borrowing another identity', async (): Promise<void> => {
const root = await fixtureRoot();
await writeFile(join(root, 'gitea-example-shared.token'), 'shared-canary', { mode: 0o600 });
const resolver = new FileCredentialResolver(root, registry());
const resolved = await resolver.resolve('missing-seat', 'homelab', 'git.example.invalid');
expect(resolved).toBeUndefined();
});
});