mirror of
https://github.com/block/buzz.git
synced 2026-10-29 13:31:44 +00:00
64 lines
6.7 KiB
TypeScript
64 lines
6.7 KiB
TypeScript
import { createInterface } from 'node:readline/promises';
|
|
import { stdin, stdout } from 'node:process';
|
|
import { join } from 'node:path';
|
|
import { hostname } from 'node:os';
|
|
import { existsSync } from 'node:fs';
|
|
import { randomUUID } from 'node:crypto';
|
|
import { bootstrapHostIdentity, enrollHostIdentity, readHostIdentityPublic } from './host-identity.ts';
|
|
import { hostPairing, pairingFingerprint, registerHost, verifyHostRegistration } from './host-registration.ts';
|
|
import { readPrivate, writePrivate } from './storage.ts';
|
|
import { readAgentSecret } from './key-input.ts';
|
|
import { text } from './protocol.ts';
|
|
import { setupPath } from './setup-path.ts';
|
|
|
|
const quote = (s: string) => `'${s.replaceAll("'", "'\"'\"'")}'`;
|
|
/** Standalone offline enrollment UI. File exchange avoids circular relay bootstrap. */
|
|
export async function enrollmentInput(directory: string): Promise<void> {
|
|
const retained = existsSync(join(directory, 'host-identity.json')) ? readHostIdentityPublic(directory) : undefined;
|
|
const exchange = join(directory, 'exchange');
|
|
const ui = createInterface({ input: stdin, output: stdout });
|
|
try {
|
|
console.log('Prepare this computer → owner approval → import. No relay connection, agent authorization, or Start. Approve on the trusted owner computer, never enter the owner private key on the host.');
|
|
if (retained) console.log(`Existing computer identity retained: ${retained.pairing.label}. Enter export to continue without creating or reading a key, or import when approval returns.`);
|
|
console.log('create/export save requests automatically; export-to/approve-to let you choose a new output path.');
|
|
const action = (await ui.question(`Enrollment [${retained ? 'export (Enter) / import / export-to / cancel' : 'create / approve / approve-to / cancel'}]: `)) || (retained ? 'export' : '');
|
|
if (action === 'cancel') return;
|
|
if (action === 'create' || action === 'export' || action === 'export-to') {
|
|
if (action === 'create' && retained) throw Error('Host identity already exists. Rerun setup and choose export or import; do not reset it.');
|
|
const identity = action === 'create' ? bootstrapHostIdentity(directory,
|
|
(await ui.question(`Computer name [${hostname()}]: `)) || hostname(),
|
|
text(await ui.question('Owner PUBLIC key (hex, from your existing owner identity; never the private key): ')),
|
|
text(await ui.question('Management relay URL (wss://… from your community operator; private transport): '))) : readHostIdentityPublic(directory);
|
|
const fingerprint = pairingFingerprint(identity.pairing);
|
|
console.log(`Pairing fingerprint: ${fingerprint}`);
|
|
const file = action === 'export-to' ? setupPath(text(await ui.question('New private request output path (~ supported): '))) : join(exchange, `request-${fingerprint}-${randomUUID()}.json`);
|
|
writePrivate(file, identity.pairing, true);
|
|
console.log(`Request saved: ${file}\nTransfer this file privately to the owner computer (contains public keys and private infrastructure metadata, no secrets). There run beehive legacy-enrollment ~/.beehive/owner-exchange, choose approve and supply the transferred file. Compare the full fingerprint above. Re-export is safe; older files are never replaced.`);
|
|
} else if (action === 'approve' || action === 'approve-to') {
|
|
if (retained) throw Error('This folder belongs to a host. Approve on the trusted owner computer using beehive legacy-enrollment ~/.beehive/owner-exchange; no owner key requested here.');
|
|
const request = hostPairing(readPrivate(setupPath(text(await ui.question('Private pairing file (transferred from host, ~ supported): ')))));
|
|
console.log(`Host: ${request.host}\nLabel: ${request.label}\nOwner: ${request.owner}\nRelay: ${request.relay}\nFingerprint: ${pairingFingerprint(request)}`);
|
|
if (await ui.question('Compare full fingerprint with host. Register ONLY this infrastructure (not agents or relay delegation)? [yes/no]: ') !== 'yes') return;
|
|
const duration = await ui.question('Approval validity (e.g. 7d for seven days, or future Unix seconds; choose how long to trust this host): ');
|
|
const expires = /^\d+d$/.test(duration) ? Math.floor(Date.now() / 1000) + Number(duration.slice(0, -1)) * 86400 : Number(duration);
|
|
if (!Number.isSafeInteger(expires) || expires <= Math.floor(Date.now() / 1000)) throw Error('Expiration must be in the future; use e.g. 7d or future Unix seconds');
|
|
console.log(`Approval expires: ${new Date(expires * 1000).toISOString()}`);
|
|
const file = action === 'approve-to' ? setupPath(text(await ui.question('New private approval output path (~ supported): '))) : join(exchange, `approval-${pairingFingerprint(request)}-${randomUUID()}.json`);
|
|
ui.close();
|
|
const secret = await readAgentSecret('Owner');
|
|
writePrivate(file, registerHost(request, secret, expires), true);
|
|
console.log(`Approval saved: ${file}\nOwner secret not persisted. Transfer privately back to host, then run beehive legacy-enrollment and choose import. You may put it at the host's displayed default approval path.\nOn this owner computer next: beehive catalog ${quote(file)}\nPrivate transport opens at startup.`);
|
|
} else if (action === 'import') {
|
|
if (!retained) throw Error('No host identity here. Run setup on the original host folder; do not create a replacement identity.');
|
|
const expected = join(exchange, `approval-${pairingFingerprint(retained.pairing)}.json`);
|
|
console.log(`Transfer the owner approval here for an Enter default: ${expected}. Or enter its actual transferred path. Filename is not authorization; exact request and owner signature are checked.`);
|
|
const supplied = await ui.question(`Private approval file (~ supported${existsSync(expected) ? ', Enter for displayed path' : '; transferred from owner'}): `);
|
|
if (!supplied && !existsSync(expected)) throw Error('No approval at the displayed path. Transfer the owner-signed approval, then rerun setup and choose import.');
|
|
const approval = readPrivate(supplied ? setupPath(supplied) : expected);
|
|
verifyHostRegistration(approval, retained.pairing);
|
|
enrollHostIdentity(directory, approval);
|
|
console.log(`Host registered offline. Private transport opens at startup. No agent authorized or started.\nNext: prepare binding and owner-approved agent genesis as in FIRST_DEMO.md, then beehive provision-agent ${quote(directory)} <binding-file> <genesis-file>. Provider login is separate; beehive auth-info ${quote(directory)} prints its context. Then beehive host ${quote(directory)} --owner-present.`);
|
|
} else throw Error('Choose create to prepare a new computer, export to resume, approve on the owner computer, or import a returned approval');
|
|
} finally { ui.close(); }
|
|
}
|