Files
stack/packages/mosaic/src/commands/cred.spec.ts
T
2026-08-05 14:25:55 -05:00

420 lines
15 KiB
TypeScript

import {
chmod,
mkdtemp,
mkdir,
open,
readFile,
readdir,
rename,
rm,
writeFile,
} from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
CredentialAuditJournal,
CredentialJournalError,
listCredentialJournals,
} from '../credentials/audit-journal.js';
import { parseCredentialEstateRegistry } from '../credentials/estate-registry.js';
import { FileCredentialStore } from '../credentials/file-credential-store.js';
import { executeCredentialRotate, executeCredentialWire } from './cred.js';
let cleanup: string | undefined;
afterEach(async (): Promise<void> => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
if (cleanup !== undefined) await rm(cleanup, { recursive: true, force: true });
cleanup = undefined;
});
async function fixture(): Promise<{
readonly mosaicHome: string;
readonly registryPath: string;
readonly tokenDirectory: string;
readonly stateRoot: string;
}> {
cleanup = await mkdtemp(join(tmpdir(), 'mosaic-cred-command-'));
await chmod(cleanup, 0o700);
const mosaicHome = join(cleanup, 'mosaic');
const credentialDirectory = join(mosaicHome, 'cred');
await mkdir(credentialDirectory, { recursive: true, mode: 0o700 });
const registryPath = join(credentialDirectory, 'estates.json');
await writeFile(
registryPath,
JSON.stringify({
version: 1,
estates: [
{
name: 'homelab',
hosts: [
{
host: 'git.example.invalid',
provider: 'gitea',
apiBaseUrl: 'https://git.example.invalid',
tokenPrefix: 'gitea-example',
},
],
},
],
}),
{ mode: 0o600 },
);
return {
mosaicHome,
registryPath,
tokenDirectory: join(mosaicHome, 'secrets', 'gitea-tokens'),
stateRoot: join(cleanup, 'state'),
};
}
describe('credential lifecycle command controls', (): void => {
it('returns the visible open rotation journal when protected authority resolution fails', async (): Promise<void> => {
const paths = await fixture();
const registry = parseCredentialEstateRegistry(await readFile(paths.registryPath, 'utf8'));
await mkdir(join(paths.mosaicHome, 'secrets'), { mode: 0o700 });
const store = new FileCredentialStore(paths.tokenDirectory, registry);
await store.put(
{
identity: 'seat-name',
estate: 'homelab',
host: 'git.example.invalid',
providerLogin: 'seat-name',
tokenName: 'old-generation',
scopes: ['write:repository'],
createdAt: '2026-08-05T00:00:00.000Z',
},
new TextEncoder().encode('old-token-canary'),
);
const result = await executeCredentialRotate('seat-name', {
estate: 'homelab',
host: 'git.example.invalid',
actor: 'seat-name',
authorityFd: '999',
tokenName: 'new-generation',
mosaicHome: paths.mosaicHome,
registry: paths.registryPath,
tokenDir: paths.tokenDirectory,
stateDir: paths.stateRoot,
});
expect(result.outcome).toBe('error');
expect(result.mutation).toBe('none');
expect(result.audit.state).toBe('open');
expect(result.audit.journalId).not.toBeNull();
await expect(listCredentialJournals(paths.stateRoot)).resolves.toContainEqual(
expect.objectContaining({ id: result.audit.journalId, state: 'open' }),
);
});
it('refuses an unauthenticated actor before rewriting another seat environment', async (): Promise<void> => {
const paths = await fixture();
const agents = join(paths.mosaicHome, 'fleet', 'agents');
await mkdir(agents, { recursive: true, mode: 0o700 });
const seatEnvironment = join(agents, 'seat-name.env.generated');
const before = 'MOSAIC_AGENT_NAME=seat-name\nMOSAIC_AGENT_CLASS=coder\n';
await writeFile(seatEnvironment, before, { mode: 0o600 });
const result = await executeCredentialWire('seat-name', {
estate: 'homelab',
host: 'git.example.invalid',
actor: 'intruder-seat',
seatEnv: seatEnvironment,
mosaicHome: paths.mosaicHome,
registry: paths.registryPath,
tokenDir: paths.tokenDirectory,
stateDir: paths.stateRoot,
});
expect(result.outcome).toBe('refused');
expect(result.mutation).toBe('none');
await expect(readFile(seatEnvironment, 'utf8')).resolves.toBe(before);
});
it('authenticates the exact seat and rewrites its roster-derived projection idempotently', async (): Promise<void> => {
const paths = await fixture();
const agents = join(paths.mosaicHome, 'fleet', 'agents');
await mkdir(agents, { recursive: true, mode: 0o700 });
const seatEnvironment = join(agents, 'seat-name.env.generated');
await writeFile(seatEnvironment, 'MOSAIC_AGENT_NAME=seat-name\nMOSAIC_AGENT_CLASS=coder\n', {
mode: 0o600,
});
const authorityPath = join(cleanup!, 'authority.json');
await writeFile(
authorityPath,
JSON.stringify({
identity: 'seat-name',
estate: 'homelab',
host: 'git.example.invalid',
secret: 'authority-canary',
}),
{ mode: 0o600 },
);
vi.stubGlobal(
'fetch',
async (): Promise<Response> =>
new Response(JSON.stringify({ id: 7, login: 'seat-name' }), {
status: 200,
headers: { 'content-type': 'application/json' },
}),
);
const invoke = async () => {
const authority = await open(authorityPath, 'r');
try {
return await executeCredentialWire('seat-name', {
estate: 'homelab',
host: 'git.example.invalid',
actor: 'seat-name',
authorityFd: authority.fd.toString(),
seatEnv: seatEnvironment,
mosaicHome: paths.mosaicHome,
registry: paths.registryPath,
tokenDir: paths.tokenDirectory,
stateDir: paths.stateRoot,
});
} finally {
await authority.close();
}
};
const first = await invoke();
const afterFirst = await readFile(seatEnvironment, 'utf8');
const second = await invoke();
const afterSecond = await readFile(seatEnvironment, 'utf8');
expect(first.outcome).toBe('ok');
expect(second.outcome).toBe('ok');
expect(afterSecond).toBe(afterFirst);
expect(afterSecond).toContain('MOSAIC_GIT_IDENTITY=seat-name\n');
expect(afterSecond).toContain('MOSAIC_CREDENTIAL_ESTATE=homelab\n');
expect(afterSecond).toContain('GITEA_LOGIN=seat-name--git.example.invalid\n');
});
it.each(['recordMutation', 'seal'] as const)(
'reports an applied wire as indeterminate when audit %s fails after rename',
async (method): Promise<void> => {
const paths = await fixture();
const agents = join(paths.mosaicHome, 'fleet', 'agents');
await mkdir(agents, { recursive: true, mode: 0o700 });
const seatEnvironment = join(agents, 'seat-name.env.generated');
await writeFile(seatEnvironment, 'MOSAIC_AGENT_NAME=seat-name\n', { mode: 0o600 });
const authorityPath = join(cleanup!, 'authority.json');
await writeFile(
authorityPath,
JSON.stringify({
identity: 'seat-name',
estate: 'homelab',
host: 'git.example.invalid',
secret: 'authority-canary',
}),
{ mode: 0o600 },
);
vi.stubGlobal(
'fetch',
async (): Promise<Response> =>
new Response(JSON.stringify({ id: 7, login: 'seat-name' }), {
status: 200,
headers: { 'content-type': 'application/json' },
}),
);
vi.spyOn(CredentialAuditJournal.prototype, method).mockRejectedValueOnce(
new CredentialJournalError('journal-unavailable', 'injected audit failure'),
);
const authority = await open(authorityPath, 'r');
try {
const result = await executeCredentialWire('seat-name', {
estate: 'homelab',
host: 'git.example.invalid',
actor: 'seat-name',
authorityFd: authority.fd.toString(),
seatEnv: seatEnvironment,
mosaicHome: paths.mosaicHome,
registry: paths.registryPath,
tokenDir: paths.tokenDirectory,
stateDir: paths.stateRoot,
});
expect(result.outcome).toBe('indeterminate');
expect(result.mutation).toBe('applied');
expect(await readFile(seatEnvironment, 'utf8')).toContain('MOSAIC_GIT_IDENTITY=seat-name');
} finally {
await authority.close();
}
},
);
it('refuses to overwrite a roster projection replaced after validation', async (): Promise<void> => {
const paths = await fixture();
const agents = join(paths.mosaicHome, 'fleet', 'agents');
await mkdir(agents, { recursive: true, mode: 0o700 });
const seatEnvironment = join(agents, 'seat-name.env.generated');
await writeFile(seatEnvironment, 'MOSAIC_AGENT_NAME=seat-name\n', { mode: 0o600 });
const authorityPath = join(cleanup!, 'authority.json');
await writeFile(
authorityPath,
JSON.stringify({
identity: 'seat-name',
estate: 'homelab',
host: 'git.example.invalid',
secret: 'authority-canary',
}),
{ mode: 0o600 },
);
vi.stubGlobal(
'fetch',
async (): Promise<Response> =>
new Response(JSON.stringify({ id: 7, login: 'seat-name' }), {
status: 200,
headers: { 'content-type': 'application/json' },
}),
);
const authority = await open(authorityPath, 'r');
try {
const result = await executeCredentialWire('seat-name', {
estate: 'homelab',
host: 'git.example.invalid',
actor: 'seat-name',
authorityFd: authority.fd.toString(),
seatEnv: seatEnvironment,
mosaicHome: paths.mosaicHome,
registry: paths.registryPath,
tokenDir: paths.tokenDirectory,
stateDir: paths.stateRoot,
wireBeforeRename: async (): Promise<void> => {
const replacement = join(agents, 'replacement');
await writeFile(replacement, 'MOSAIC_AGENT_NAME=seat-name\nNEW=value\n', { mode: 0o600 });
await rename(replacement, seatEnvironment);
},
});
expect(result.outcome).toBe('error');
expect(result.mutation).toBe('none');
expect(await readFile(seatEnvironment, 'utf8')).toBe(
'MOSAIC_AGENT_NAME=seat-name\nNEW=value\n',
);
} finally {
await authority.close();
}
});
it('reports directory-sync failure after rename as applied and indeterminate', async (): Promise<void> => {
const paths = await fixture();
const agents = join(paths.mosaicHome, 'fleet', 'agents');
await mkdir(agents, { recursive: true, mode: 0o700 });
const seatEnvironment = join(agents, 'seat-name.env.generated');
await writeFile(seatEnvironment, 'MOSAIC_AGENT_NAME=seat-name\n', { mode: 0o600 });
const authorityPath = join(cleanup!, 'authority.json');
await writeFile(
authorityPath,
JSON.stringify({
identity: 'seat-name',
estate: 'homelab',
host: 'git.example.invalid',
secret: 'authority-canary',
}),
{ mode: 0o600 },
);
vi.stubGlobal(
'fetch',
async (): Promise<Response> =>
new Response(JSON.stringify({ id: 7, login: 'seat-name' }), {
status: 200,
headers: { 'content-type': 'application/json' },
}),
);
const authority = await open(authorityPath, 'r');
try {
const result = await executeCredentialWire('seat-name', {
estate: 'homelab',
host: 'git.example.invalid',
actor: 'seat-name',
authorityFd: authority.fd.toString(),
seatEnv: seatEnvironment,
mosaicHome: paths.mosaicHome,
registry: paths.registryPath,
tokenDir: paths.tokenDirectory,
stateDir: paths.stateRoot,
wireDirectorySync: async (): Promise<void> => {
throw new Error('injected directory sync failure');
},
});
expect(result.outcome).toBe('indeterminate');
expect(result.mutation).toBe('applied');
expect(await readFile(seatEnvironment, 'utf8')).toContain('MOSAIC_GIT_IDENTITY=seat-name');
} finally {
await authority.close();
}
});
it('removes a temporary projection when directory revalidation fails before rename', async (): Promise<void> => {
const paths = await fixture();
const agents = join(paths.mosaicHome, 'fleet', 'agents');
await mkdir(agents, { recursive: true, mode: 0o700 });
const seatEnvironment = join(agents, 'seat-name.env.generated');
await writeFile(seatEnvironment, 'MOSAIC_AGENT_NAME=seat-name\n', { mode: 0o600 });
const authorityPath = join(cleanup!, 'authority.json');
await writeFile(
authorityPath,
JSON.stringify({
identity: 'seat-name',
estate: 'homelab',
host: 'git.example.invalid',
secret: 'authority-canary',
}),
{ mode: 0o600 },
);
vi.stubGlobal('fetch', async (): Promise<Response> => {
await chmod(agents, 0o777);
return new Response(JSON.stringify({ id: 7, login: 'seat-name' }), {
status: 200,
headers: { 'content-type': 'application/json' },
});
});
const authority = await open(authorityPath, 'r');
try {
const result = await executeCredentialWire('seat-name', {
estate: 'homelab',
host: 'git.example.invalid',
actor: 'seat-name',
authorityFd: authority.fd.toString(),
seatEnv: seatEnvironment,
mosaicHome: paths.mosaicHome,
registry: paths.registryPath,
tokenDir: paths.tokenDirectory,
stateDir: paths.stateRoot,
});
expect(result.outcome).toBe('error');
expect(await readdir(agents)).toEqual(['seat-name.env.generated']);
} finally {
await authority.close();
await chmod(agents, 0o700);
}
});
it('refuses a caller-selected seat filename that is not bound to the requested identity', async (): Promise<void> => {
const paths = await fixture();
const agents = join(paths.mosaicHome, 'fleet', 'agents');
await mkdir(agents, { recursive: true, mode: 0o700 });
const seatEnvironment = join(agents, 'other-seat.env.generated');
const before = 'MOSAIC_AGENT_NAME=other-seat\nMOSAIC_AGENT_CLASS=coder\n';
await writeFile(seatEnvironment, before, { mode: 0o600 });
const result = await executeCredentialWire('seat-name', {
estate: 'homelab',
host: 'git.example.invalid',
actor: 'seat-name',
authorityFd: '999',
seatEnv: seatEnvironment,
mosaicHome: paths.mosaicHome,
registry: paths.registryPath,
tokenDir: paths.tokenDirectory,
stateDir: paths.stateRoot,
});
expect(result.outcome).toBe('refused');
expect(result.reason.code).toBe('credential-binding-mismatch');
await expect(readFile(seatEnvironment, 'utf8')).resolves.toBe(before);
});
});