feat(mosaic): mechanically authorize lease promotion
This commit is contained in:
@@ -152,7 +152,11 @@ describe('mosaic promote', () => {
|
||||
};
|
||||
const output = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
const program = new Command().exitOverride();
|
||||
registerPromoteCommand(program, { store, transport: promotionTransport });
|
||||
registerPromoteCommand(program, {
|
||||
mintAuthorization: async () => {},
|
||||
store,
|
||||
transport: promotionTransport,
|
||||
});
|
||||
|
||||
try {
|
||||
await program.parseAsync(['node', 'mosaic', 'promote', 'claude-seat']);
|
||||
@@ -179,7 +183,11 @@ describe('mosaic promote', () => {
|
||||
};
|
||||
const output = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
const program = new Command().exitOverride();
|
||||
registerPromoteCommand(program, { store, transport: promotionTransport });
|
||||
registerPromoteCommand(program, {
|
||||
mintAuthorization: async () => {},
|
||||
store,
|
||||
transport: promotionTransport,
|
||||
});
|
||||
|
||||
try {
|
||||
process.exitCode = undefined;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { constants } from 'node:fs';
|
||||
import { open } from 'node:fs/promises';
|
||||
import { mkdir, open, rename } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import type { Command } from 'commander';
|
||||
import {
|
||||
@@ -17,6 +18,8 @@ const DEFAULT_POLL_INTERVAL_MS = 250;
|
||||
const DEFAULT_TIMEOUT_MS = 30_000;
|
||||
const SUBPROCESS_TIMEOUT_MS = 4_500;
|
||||
const PENDING_DIRECTORY = 'mosaic-lease';
|
||||
const AUTHORIZATION_DIRECTORY = 'authorizations';
|
||||
const AUTHORIZATION_TTL_SECONDS = 60;
|
||||
const RESULT_FILE = 'last-result.json';
|
||||
|
||||
export interface PromotionBreadcrumb {
|
||||
@@ -47,11 +50,13 @@ export interface PromoteSeatOptions {
|
||||
pollIntervalMs?: number;
|
||||
sleep?: (milliseconds: number) => Promise<void>;
|
||||
store: PromotionBreadcrumbStore;
|
||||
target?: PromotionTarget;
|
||||
timeoutMs?: number;
|
||||
transport: PromotionTransport;
|
||||
}
|
||||
|
||||
export interface PromoteCommandDeps {
|
||||
mintAuthorization?: (target: PromotionTarget) => Promise<void>;
|
||||
mosaicHome?: string;
|
||||
runner?: CommandRunner;
|
||||
store?: PromotionBreadcrumbStore;
|
||||
@@ -95,7 +100,7 @@ export async function promoteSeat(
|
||||
const pollIntervalMs = normalizePollInterval(options.pollIntervalMs);
|
||||
let target: PromotionTarget;
|
||||
try {
|
||||
target = await options.transport.resolve(seat);
|
||||
target = options.target ?? (await options.transport.resolve(seat));
|
||||
} catch (error: unknown) {
|
||||
return unverifiedUnresolvedSeat(seat, `RESOLVE_FAILED: ${errorMessage(error)}`);
|
||||
}
|
||||
@@ -154,8 +159,26 @@ export function registerPromoteCommand(program: Command, deps: PromoteCommandDep
|
||||
`Bounded result wait in milliseconds (default: ${DEFAULT_TIMEOUT_MS})`,
|
||||
)
|
||||
.action(async (seat: string, opts: { timeout?: string }) => {
|
||||
if (
|
||||
process.env['MOSAIC_LEASE_SESSION_ID'] !== undefined &&
|
||||
deps.mintAuthorization === undefined
|
||||
) {
|
||||
console.error('mosaic promote must run outside a lease-gated seat.');
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
let target: PromotionTarget;
|
||||
try {
|
||||
target = await transport.resolve(seat);
|
||||
await (deps.mintAuthorization ?? mintAuthorization)(target);
|
||||
} catch (error: unknown) {
|
||||
console.error(`mosaic promote authorization failed: ${errorMessage(error)}`);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
const result = await promoteSeat(seat, {
|
||||
store,
|
||||
target,
|
||||
timeoutMs: parseOptionTimeout(opts.timeout),
|
||||
transport,
|
||||
});
|
||||
@@ -171,6 +194,33 @@ export function registerPromoteCommand(program: Command, deps: PromoteCommandDep
|
||||
});
|
||||
}
|
||||
|
||||
async function mintAuthorization(target: PromotionTarget): Promise<void> {
|
||||
const directory = join(defaultRuntimeDirectory(), PENDING_DIRECTORY, AUTHORIZATION_DIRECTORY);
|
||||
await mkdir(directory, { mode: 0o700, recursive: true });
|
||||
const token = {
|
||||
expires_at: wallClockSeconds() + AUTHORIZATION_TTL_SECONDS,
|
||||
nonce: randomBytes(32).toString('hex'),
|
||||
seat: target.seat,
|
||||
session_id: target.sessionId,
|
||||
ts: wallClockSeconds(),
|
||||
};
|
||||
const destination = join(directory, `${target.sessionId}.auth`);
|
||||
const temporary = join(directory, `.${target.sessionId}.${randomBytes(8).toString('hex')}.tmp`);
|
||||
const handle = await open(
|
||||
temporary,
|
||||
constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL,
|
||||
0o600,
|
||||
);
|
||||
try {
|
||||
await handle.chmod(0o600);
|
||||
await handle.writeFile(JSON.stringify(token));
|
||||
await handle.sync();
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
await rename(temporary, destination);
|
||||
}
|
||||
|
||||
function defaultRuntimeDirectory(): string {
|
||||
const configured = process.env['XDG_RUNTIME_DIR'];
|
||||
if (configured) return configured;
|
||||
|
||||
Reference in New Issue
Block a user