mirror of
https://github.com/block/buzz.git
synced 2026-11-13 21:01:53 +00:00
beehive: real beehive command launcher with default host folder
Signed-off-by: Logan Johnson <loganj@squareup.com>
This commit is contained in:
@@ -8,8 +8,20 @@ compatibility. Do not use frozen or existing installations for this demo.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node 24.15.0 and this package's frozen standalone dependencies. In `beehive`,
|
||||
invoke `/absolute/path/to/node src/cli.ts ...`; no root bootstrap is needed.
|
||||
- Node 24.15.0 (any Node.js 22.18+ works; the `beehive` launcher checks the
|
||||
version and says what to do) and this package's frozen standalone
|
||||
dependencies. Install the command once, reversibly, into a user bin directory
|
||||
already on your PATH, matching nothing else:
|
||||
`ln -s /absolute/path/to/this/checkout/beehive/bin/beehive.cjs ~/.local/bin/beehive`
|
||||
(adjust the target to a user-owned bin directory that is on *your* PATH;
|
||||
undo with `rm`). Then run `beehive <command>` from any directory. Advanced
|
||||
fallback inside the package: `/absolute/path/to/node src/cli.ts <command>`;
|
||||
no root bootstrap is needed either way.
|
||||
- `beehive setup` with no directory uses the default host state folder
|
||||
`~/.beehive/host`, resolved from your home (never the current directory) and
|
||||
displayed once before the wizard. An occupied default is never silently
|
||||
reinitialized; pass an explicit directory for additional hosts. Owner
|
||||
approval/catalog files below stay explicit and never land in host state.
|
||||
- A deliberately chosen **new** 0700 host directory, a dedicated host service OS
|
||||
user with an interactive unlocked credential store, and a separate owner context.
|
||||
`@napi-rs/keyring` 2.0.0 may show OS approval/unlock prompts; it cannot suppress
|
||||
@@ -31,16 +43,18 @@ compatibility. Do not use frozen or existing installations for this demo.
|
||||
|
||||
## Manual setup → Start → reply → Stop
|
||||
|
||||
1. **Host, owner present:** `node src/cli.ts setup /new/private-host`
|
||||
Choose `create`, supply label, owner's **public** hex key and `wss://relay-host`;
|
||||
1. **Host, owner present:** `beehive setup`
|
||||
(default host folder `~/.beehive/host`; `beehive setup <explicit-directory>`
|
||||
for an additional host). Choose `create`, supply label, owner's **public** hex key and `wss://relay-host`;
|
||||
choose a new private pairing file. Expect OS key creation/readback approval.
|
||||
If interrupted, use `export` on the same directory; never regenerate identity.
|
||||
2. Transfer the pairing file privately to the owner. **Owner context:** run
|
||||
`node src/cli.ts setup /new/owner-exchange`, choose `approve`, select the pairing,
|
||||
`beehive setup /new/owner-exchange` (an explicit directory keeps owner
|
||||
approval files outside host state), choose `approve`, select the pairing,
|
||||
compare the full fingerprint with the host, confirm `yes`, enter a future Unix
|
||||
expiration and a new private approval file. Enter the existing owner key only
|
||||
at the hidden prompt. Transfer the approval file privately back to the host.
|
||||
3. **Host:** rerun `node src/cli.ts setup /new/private-host`, choose `import`, select
|
||||
3. **Host:** rerun `beehive setup` on the same default host folder, choose `import`, select
|
||||
that approval. This registers infrastructure only. Arrange the **already-member**
|
||||
prerequisite above out of band; there is no administrative operation here.
|
||||
4. Prepare a private local `binding.json` (absolute paths, no identity keys):
|
||||
@@ -70,22 +84,23 @@ compatibility. Do not use frozen or existing installations for this demo.
|
||||
or invent a new root to bypass its assignment**; use a separately authorized
|
||||
initial demo identity/placement, not migration or Move.
|
||||
|
||||
Run `node src/cli.ts provision-agent /new/private-host /private/binding.json /private/genesis.json`.
|
||||
Run `beehive provision-agent ~/.beehive/host /private/binding.json /private/genesis.json`.
|
||||
Enter the matching agent key at the hidden prompt. Expect OS create/readback
|
||||
prompts. The owner key is never placed on the host. If interrupted, retain all
|
||||
artifacts and use `reconcile-provision` with these exact same inputs, not a new key.
|
||||
5. Run `node src/cli.ts auth-info /new/private-host` for the exact service-user
|
||||
5. Run `beehive auth-info ~/.beehive/host` for the exact service-user
|
||||
native login context. Deliberately configure/login the supported provider as
|
||||
that user only. No borrowed Desktop cache. Presence of files is not login proof.
|
||||
6. **Owner:** `node src/cli.ts catalog /new/private-catalog.json /private/approval.json`.
|
||||
6. **Owner:** `beehive catalog /new/private-catalog.json /private/approval.json`.
|
||||
7. **Host, owner present:**
|
||||
`node src/cli.ts host /new/private-host wss://relay-host --owner-present`
|
||||
The flag consents to live OS reads, not relay admin or provider login. Each key
|
||||
`beehive host wss://relay-host --owner-present`
|
||||
(default host folder; the explicit `beehive host <host-directory> <relay>
|
||||
--owner-present` form remains available). The flag consents to live OS reads, not relay admin or provider login. Each key
|
||||
read uses an owned helper with a 10s deadline; SIGINT/SIGTERM cancels and awaits
|
||||
its actual close. An OS-owned dialog may remain after helper termination;
|
||||
inspect/dismiss it deliberately. Denied/timeout is not a missing key. Expect a
|
||||
fresh host-signed HTTP membership check, then `Host online`.
|
||||
8. **Owner:** `node src/cli.ts tui /private/private-catalog.json wss://relay-host`.
|
||||
8. **Owner:** `beehive tui /private/private-catalog.json wss://relay-host`.
|
||||
Enter owner key at the hidden prompt. A separate owner-signed membership check
|
||||
precedes private transport. Use `hosts`, `select <full host hex>`, `show`, then
|
||||
`start`. Wait for `completed | accepted` and running actual state; publication
|
||||
|
||||
@@ -10,9 +10,28 @@ can close while the host/runner continues. No direct-host HTTP API or controller
|
||||
service exists. This package intentionally lives alongside, not inside, the
|
||||
native clients: it is a separately authorized product experiment.
|
||||
|
||||
## Command launcher (one-time, reversible)
|
||||
|
||||
`bin/beehive.cjs` is a plain launcher for this checkout: it checks Node
|
||||
(>= 22.18, with a short actionable error otherwise) and runs `src/cli.ts` from
|
||||
its own real location, so it works from any directory, through a user
|
||||
symlink, and with spaces in paths. Install once into a user-owned bin
|
||||
directory already on your PATH (matching no existing command):
|
||||
|
||||
```sh
|
||||
ln -s "$(pwd)/bin/beehive.cjs" ~/.local/bin/beehive # adjust if your PATH differs
|
||||
```
|
||||
|
||||
`beehive setup` (no directory) then uses the default host state folder
|
||||
`~/.beehive/host`, resolved from your home and displayed once; an occupied
|
||||
default is never reinitialized. Pass an explicit directory for additional
|
||||
hosts. Owner approval/catalog files remain explicit and outside host state.
|
||||
Undo the command with `rm ~/.local/bin/beehive` (or your equivalent link).
|
||||
|
||||
## Offline standalone host enrollment (partial, no network)
|
||||
|
||||
`node src/cli.ts setup <directory>` without an identity file opens Beehive's
|
||||
`beehive setup <directory>` (or `node src/cli.ts setup <directory>` inside the
|
||||
package) without an identity file opens Beehive's
|
||||
own enrollment interface. No Buzz Desktop is needed. Use **create** on a fresh
|
||||
host directory: enter its label, relay URL and your existing owner **public** key.
|
||||
The host generates its durable independent key and exports a private pairing file.
|
||||
|
||||
43
beehive/bin/beehive.cjs
Executable file
43
beehive/bin/beehive.cjs
Executable file
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env node
|
||||
/* Beehive standalone command launcher.
|
||||
*
|
||||
* Runs the package CLI (src/cli.ts) from this checkout under the same Node.js,
|
||||
* resolving everything from this file's real location so it works from any
|
||||
* directory, through a user-installed symlink, and from paths containing
|
||||
* spaces. One-time reversible user install (no shell profile or system
|
||||
* change), for example into a user bin directory already on PATH:
|
||||
*
|
||||
* ln -s /absolute/path/to/beehive/bin/beehive.cjs ~/.local/bin/beehive
|
||||
*
|
||||
* Plain CommonJS on purpose: the Node version gate must be checkable before
|
||||
* any TypeScript loading (the CLI needs Node's type stripping). */
|
||||
'use strict';
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { join } = require('node:path');
|
||||
|
||||
/** Mirrors package.json "engines": Node.js >= 22.18.0. */
|
||||
function meetsMinimumNode(version, minimum) {
|
||||
const [major, minor] = String(version).split('.').map(Number);
|
||||
const [minimumMajor, minimumMinor] = String(minimum).split('.').map(Number);
|
||||
return major > minimumMajor || (major === minimumMajor && minor >= minimumMinor);
|
||||
}
|
||||
|
||||
function run() {
|
||||
if (!meetsMinimumNode(process.versions.node, '22.18.0')) {
|
||||
console.error(`beehive: Node.js 22.18.0 or newer is required; this launcher ran under Node ${process.versions.node}. Install a current Node.js (https://nodejs.org), make sure it is on PATH, and run beehive again.`);
|
||||
process.exit(1);
|
||||
}
|
||||
const cli = join(__dirname, '..', 'src', 'cli.ts');
|
||||
const child = spawnSync(process.execPath, [cli, ...process.argv.slice(2)], { stdio: 'inherit' });
|
||||
if (child.error) {
|
||||
console.error(`beehive: cannot start the Beehive CLI at ${cli}: ${child.error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (child.status !== null) process.exit(child.status);
|
||||
if (child.signal) process.kill(process.pid, child.signal); // Propagate Ctrl-C/termination as the same signal.
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (require.main === module) run();
|
||||
|
||||
module.exports = { meetsMinimumNode };
|
||||
@@ -21,6 +21,7 @@ import { verifyHostCatalog, type HostCatalog } from './host-catalog.ts';
|
||||
import { Profiles, profile, profileRevision, type Profile } from './profiles.ts';
|
||||
import { createInterface } from 'node:readline/promises';
|
||||
import { stdin, stdout } from 'node:process';
|
||||
import { homedir } from 'node:os';
|
||||
import { existsSync, mkdirSync, realpathSync, rmdirSync } from 'node:fs';
|
||||
import { resolve, join, dirname } from 'node:path';
|
||||
import { relay } from './relay.ts';
|
||||
@@ -36,10 +37,15 @@ import { prepareConversation } from './conversation.ts';
|
||||
const operationLabel = (m: Message) => `${m.host} ${m.type}${m.type === 'move' ? ` → ${String(m.body.target)}` : ''} | agent ${m.agent} | operation ${m.id}`;
|
||||
const shellQuote = (s: string) => `'${s.replaceAll("'", "'\"'\"'")}'`;
|
||||
const [command, ...args] = process.argv.slice(2);
|
||||
// Default HOST state only, resolved from the current user's home (never cwd). Owner approval/catalog
|
||||
// files stay explicit and never land in this folder. An occupied default is never reinitialized.
|
||||
const defaultHostDirectory = () => join(homedir(), '.beehive', 'host');
|
||||
const help = `Beehive — private host preview (explicit direct relay membership required)
|
||||
Command: beehive <command> from any directory once installed (one-time user link, reversible: ln -s <beehive-package>/bin/beehive.cjs <user bin on PATH>/beehive, e.g. ~/.local/bin); source fallback inside the package: node src/cli.ts <command>.
|
||||
identity Disabled: use your existing owner signer
|
||||
setup <host-directory> Offline host pairing/owner approval/import
|
||||
setup <host-directory> <identity-file> LEGACY loopback diagnostic setup (owner key copied)
|
||||
setup Offline pairing/owner approval/import on the default host folder ~/.beehive/host
|
||||
setup <host-directory> Explicit host folder (additional hosts/tests); same offline wizard
|
||||
setup <host-directory> <identity-file> LEGACY loopback diagnostic setup (owner key copied)
|
||||
provision-agent <host-directory> <binding-file> <genesis-file> Hidden matching agent import; OS credentials
|
||||
reconcile-provision <host-directory> <binding-file> <genesis-file> Explicit exact first-provision recovery
|
||||
catalog <new-file> <registration-files...> Retain verified public host registrations
|
||||
@@ -55,9 +61,11 @@ const help = `Beehive — private host preview (explicit direct relay membership
|
||||
auth-info <host-directory> [binding-id] Print local harness service context (no login)
|
||||
conversation-setup <host-directory> Legacy guidance; use local-setup action normal
|
||||
relay <port> <owner-public-key> <log-file> Dedicated ciphertext relay
|
||||
host <host-directory> <relay> --owner-present Private foreground host; bounded OS key reads
|
||||
host <relay> --owner-present Private foreground host on the default host folder; bounded OS key reads
|
||||
host <host-directory> <relay> --owner-present Private foreground host on an explicit folder; bounded OS key reads
|
||||
tui <catalog-file> <relay> Private owner UI; hidden existing owner signer
|
||||
setup/add-agent accept optional <local-key-file> <public-genesis-file> for standby import.
|
||||
The default host folder resolves from your home (~/.beehive/host), never the current directory; setup displays it once and never reinitializes an existing installation. Owner approval/catalog files remain explicit and separate from host state.
|
||||
reconcile-agent derives the interrupted binding and genesis from the retained journal; an optional public genesis file must match it.
|
||||
assignment-export accepts agent public key after filename when several slots exist.
|
||||
Move is fixture-only experimental; containment acceptance remains gated. No provider login RPC. Private host/catalog requires fresh membership-enforced relay verification; no automatic private reconnect.`;
|
||||
@@ -79,6 +87,11 @@ async function main() {
|
||||
} else if (command === 'presets') {
|
||||
showPresets();
|
||||
} else if (command === 'setup') {
|
||||
const usingDefaultHostFolder = args.length === 0;
|
||||
if (usingDefaultHostFolder) {
|
||||
args[0] = defaultHostDirectory();
|
||||
console.log(`Default host folder: ${args[0]} (pass an explicit directory for another host; existing installations are never reinitialized)`);
|
||||
}
|
||||
const dir = resolve(text(args[0]));
|
||||
if (args.length === 1 && !existsSync(join(dir, 'setup.json'))) {
|
||||
await enrollmentInput(dir); return;
|
||||
@@ -320,9 +333,13 @@ async function main() {
|
||||
const stop = () => controller.abort();
|
||||
process.on('SIGINT', stop); process.on('SIGTERM', stop);
|
||||
try {
|
||||
const directory = resolve(text(args[0])), url = text(args[1]);
|
||||
// Either the classic <host-directory> <relay> form, or the default host
|
||||
// folder with the relay first (beehive host <relay> --owner-present).
|
||||
const relayFirst = args.length > 0 && /^(wss|ws):\/\//.test(args[0]);
|
||||
const directory = args.length === 0 || relayFirst ? defaultHostDirectory() : resolve(text(args[0]));
|
||||
const url = text(args[relayFirst ? 0 : 1]);
|
||||
const privateInstallation = existsSync(join(directory, 'host-identity.json'));
|
||||
if (privateInstallation && args[2] !== '--owner-present') throw Error('Private host requires --owner-present: OS credential access may prompt. Run deliberately as the host OS user; Stop cancels and awaits the bounded helper.');
|
||||
if (privateInstallation && args[relayFirst ? 1 : 2] !== '--owner-present') throw Error('Private host requires --owner-present: OS credential access may prompt. Run deliberately as the host OS user; Stop cancels and awaits the bounded helper.');
|
||||
const credentials = privateInstallation ? { ...systemCredentials, readAsync: credentialHelperReader({ operatorApproved: true }) } : systemCredentials;
|
||||
const identity = privateInstallation ? await readHostIdentityAsync(directory, credentials, controller.signal) : undefined;
|
||||
if (identity && identity.pairing.relay !== url) throw Error('Wrong registered host relay; no network request sent');
|
||||
|
||||
186
beehive/test/launcher.test.ts
Normal file
186
beehive/test/launcher.test.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { newKey, publicKey } from '../src/protocol.ts';
|
||||
|
||||
/** Launcher-focused smoke: the installed symlink must behave exactly like the
|
||||
* documented `node src/cli.ts` invocation from any directory, keep explicit
|
||||
* directories working, keep help public-only, and gate Node before any
|
||||
* TypeScript loading. Credential-touching journeys use the established
|
||||
* isolated credential fixture via NODE_OPTIONS, never the OS store. */
|
||||
const packageRoot = fileURLToPath(new URL('..', import.meta.url));
|
||||
const launcherSource = join(packageRoot, 'bin', 'beehive.cjs');
|
||||
const loader = fileURLToPath(new URL('./isolated-credentials-loader.ts', import.meta.url));
|
||||
const nodeBin = dirname(process.execPath);
|
||||
const baseEnv = (home: string): NodeJS.ProcessEnv => ({ PATH: `${nodeBin}:/usr/bin:/bin`, HOME: home });
|
||||
const isolatedEnv = (home: string, credentials: string): NodeJS.ProcessEnv => ({ ...baseEnv(home), BEEHIVE_TEST_CREDENTIAL_FILE: credentials, NODE_OPTIONS: `--import ${loader}` });
|
||||
|
||||
async function runLauncher(installed: string, args: string[], env: NodeJS.ProcessEnv, steps: [string, string][], cwd: string, timeoutMs = 30000): Promise<{ code: number | null; output: string; answered: number }> {
|
||||
const child = spawn(installed, args, { env, cwd, stdio: ['pipe', 'pipe', 'pipe'] });
|
||||
let output = '', cursor = 0, next = 0;
|
||||
const timer = setTimeout(() => child.kill('SIGKILL'), timeoutMs);
|
||||
child.stdout.on('data', data => {
|
||||
output += data.toString();
|
||||
if (next < steps.length) {
|
||||
const [prompt, answer] = steps[next]!;
|
||||
const index = output.indexOf(prompt, cursor);
|
||||
if (index !== -1) { cursor = index + prompt.length; next++; child.stdin.write(`${answer}\n`); }
|
||||
}
|
||||
});
|
||||
child.stderr.on('data', data => { output += data.toString(); });
|
||||
try {
|
||||
const code = await new Promise<number | null>((resolve, reject) => { child.on('error', reject); child.on('exit', resolve); });
|
||||
return { code, output: `${output}\n[answered ${next}/${steps.length} prompts]`, answered: next };
|
||||
} finally { clearTimeout(timer); child.stdin.end(); }
|
||||
}
|
||||
|
||||
const createJourney = (request: string, owner: string): [string, string][] => [
|
||||
['Enrollment [', 'create'], ['Host label: ', 'launcher-host'], ['Owner PUBLIC key (hex): ', publicKey(owner)],
|
||||
['Relay URL: ', 'wss://example.invalid'], ['NEW private pairing file: ', request],
|
||||
];
|
||||
|
||||
function installInto(root: string, name = 'user bin dir'): string {
|
||||
const install = join(root, name);
|
||||
mkdirSync(install);
|
||||
const installed = join(install, 'beehive');
|
||||
symlinkSync(launcherSource, installed);
|
||||
return installed;
|
||||
}
|
||||
|
||||
test('installed launcher prints public help from an unrelated directory with spaces and creates no state', { timeout: 20000 }, async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'beehive-pairing-cli-launcher-'));
|
||||
const home = join(root, 'home dir');
|
||||
try {
|
||||
mkdirSync(home);
|
||||
const installed = installInto(root);
|
||||
const cwd = join(root, 'unrelated cwd dir');
|
||||
mkdirSync(cwd);
|
||||
const help = await runLauncher(installed, [], baseEnv(home), [], cwd);
|
||||
assert.equal(help.code, 0, help.output);
|
||||
assert.match(help.output, /^Beehive — private host preview/, help.output);
|
||||
assert.ok(help.output.includes('setup'), help.output);
|
||||
assert.ok(help.output.includes('~/.beehive/host'), help.output);
|
||||
assert.ok(!help.output.includes('Enrollment ['), help.output);
|
||||
assert.ok(!existsSync(join(home, '.beehive')), 'help must not create state');
|
||||
} finally { rmSync(root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('no-arg setup enrolls on the displayed default host folder via the isolated credential fixture', { timeout: 30000 }, async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'beehive-pairing-cli-launcher-'));
|
||||
const home = join(root, 'home dir');
|
||||
try {
|
||||
mkdirSync(home);
|
||||
const installed = installInto(root);
|
||||
const cwd = join(root, 'unrelated cwd dir');
|
||||
mkdirSync(cwd);
|
||||
const owner = newKey();
|
||||
const request = join(root, 'pairing request.json');
|
||||
const credentials = join(root, 'isolated fixture secrets.json');
|
||||
const created = await runLauncher(installed, ['setup'], isolatedEnv(home, credentials), createJourney(request, owner), cwd);
|
||||
assert.equal(created.code, 0, created.output);
|
||||
assert.equal(created.answered, 5, created.output);
|
||||
const folder = join(home, '.beehive', 'host');
|
||||
assert.ok(created.output.includes(`Default host folder: ${folder}`), created.output);
|
||||
const identity = JSON.parse(readFileSync(join(folder, 'host-identity.json'), 'utf8')) as { version: number; pairing: { host: string; owner: string; label: string }; registration: unknown };
|
||||
assert.equal(identity.version, 3);
|
||||
assert.equal(identity.pairing.label, 'launcher-host');
|
||||
assert.equal(identity.pairing.owner, publicKey(owner));
|
||||
assert.ok(/^[0-9a-f]{64}$/.test(identity.pairing.host));
|
||||
assert.equal(identity.registration, null);
|
||||
const fixture = readFileSync(credentials, 'utf8');
|
||||
assert.ok(fixture.includes(identity.pairing.host), 'host key stored in the isolated fixture');
|
||||
assert.equal(Object.keys(JSON.parse(fixture)).length, 1);
|
||||
const files = [join(folder, 'host-identity.json'), request, credentials];
|
||||
for (const file of [...files, created.output]) assert.ok(!file.includes(owner), 'owner secret never persisted or printed');
|
||||
// A second default setup must refuse to replace the retained identity.
|
||||
const before = readFileSync(join(folder, 'host-identity.json'), 'utf8');
|
||||
const second = await runLauncher(installed, ['setup'], isolatedEnv(home, credentials), [
|
||||
['Enrollment [', 'create'], ['Host label: ', 'other-host'], ['Owner PUBLIC key (hex): ', publicKey(newKey())], ['Relay URL: ', 'wss://other.example.invalid'],
|
||||
], cwd);
|
||||
assert.equal(second.code, 1, second.output);
|
||||
assert.ok(second.output.includes('Host identity already exists; no replacement or automatic migration'), second.output);
|
||||
assert.equal(readFileSync(join(folder, 'host-identity.json'), 'utf8'), before, 'retained identity unchanged');
|
||||
assert.ok(!existsSync(join(root, 'second request.json')));
|
||||
} finally { rmSync(root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('explicit host directory setup is preserved through the launcher and leaves the default unused', { timeout: 30000 }, async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'beehive-pairing-cli-launcher-'));
|
||||
const home = join(root, 'home dir');
|
||||
try {
|
||||
mkdirSync(home);
|
||||
const installed = installInto(root);
|
||||
const cwd = join(root, 'unrelated cwd dir');
|
||||
mkdirSync(cwd);
|
||||
const owner = newKey();
|
||||
const explicit = join(root, 'explicit host dir');
|
||||
const request = join(root, 'pairing request.json');
|
||||
const credentials = join(root, 'isolated fixture secrets.json');
|
||||
const created = await runLauncher(installed, ['setup', explicit], isolatedEnv(home, credentials), createJourney(request, owner), cwd);
|
||||
assert.equal(created.code, 0, created.output);
|
||||
assert.ok(!created.output.includes('Default host folder:'), created.output);
|
||||
assert.ok(existsSync(join(explicit, 'host-identity.json')), 'identity lands in the explicit directory');
|
||||
assert.ok(!existsSync(join(home, '.beehive')), 'default folder untouched');
|
||||
} finally { rmSync(root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('host startup keeps the owner-present guard on both the default folder and explicit directories', { timeout: 20000 }, async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'beehive-pairing-cli-launcher-'));
|
||||
const home = join(root, 'home dir');
|
||||
try {
|
||||
mkdirSync(home);
|
||||
const installed = installInto(root);
|
||||
const cwd = join(root, 'unrelated cwd dir');
|
||||
mkdirSync(cwd);
|
||||
const defaultFolder = join(home, '.beehive', 'host');
|
||||
mkdirSync(defaultFolder, { recursive: true });
|
||||
writeFileSync(join(defaultFolder, 'host-identity.json'), '{}', { mode: 0o600 });
|
||||
const guard = await runLauncher(installed, ['host', 'wss://example.invalid'], isolatedEnv(home, join(root, 'isolated fixture secrets.json')), [], cwd);
|
||||
assert.equal(guard.code, 1, guard.output);
|
||||
assert.ok(guard.output.includes('Private host requires --owner-present'), guard.output);
|
||||
const explicit = join(root, 'explicit host dir');
|
||||
mkdirSync(explicit);
|
||||
writeFileSync(join(explicit, 'host-identity.json'), '{}', { mode: 0o600 });
|
||||
const explicitGuard = await runLauncher(installed, ['host', explicit, 'wss://example.invalid'], isolatedEnv(home, join(root, 'isolated fixture secrets.json')), [], cwd);
|
||||
assert.equal(explicitGuard.code, 1, explicitGuard.output);
|
||||
assert.ok(explicitGuard.output.includes('Private host requires --owner-present'), explicitGuard.output);
|
||||
// Relay-first form parses the flag and resolves the default folder offline.
|
||||
const emptyHome = join(root, 'empty home dir');
|
||||
mkdirSync(emptyHome);
|
||||
const missing = await runLauncher(installed, ['host', 'ws://127.0.0.1:1', '--owner-present'], isolatedEnv(emptyHome, join(root, 'isolated fixture secrets.json')), [], cwd);
|
||||
assert.equal(missing.code, 1, missing.output);
|
||||
assert.ok(missing.output.includes(join(emptyHome, '.beehive', 'host')), missing.output);
|
||||
} finally { rmSync(root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('launcher Node gate: boundary comparison and a real old-node refusal', { timeout: 20000 }, async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'beehive-pairing-cli-launcher-'));
|
||||
const home = join(root, 'home dir');
|
||||
try {
|
||||
mkdirSync(home);
|
||||
const probe = spawnSync(process.execPath, ['-e', "const gate = require(process.argv[1]); for (const v of process.argv.slice(2)) process.stdout.write(gate.meetsMinimumNode(v, '22.18.0') + ' ');", launcherSource, '22.18.0', '22.17.9', '18.0.0', '24.15.0', '26.8.1'], { encoding: 'utf8' });
|
||||
assert.equal(probe.status, 0, probe.stderr);
|
||||
assert.equal(probe.stdout.trim(), 'true false false true true');
|
||||
// End-to-end: a PATH "node" reporting 18.9.0 must make the launcher fail
|
||||
// fast with the actionable message before any TypeScript is loaded.
|
||||
const stubBin = join(root, 'stub node bin');
|
||||
mkdirSync(stubBin);
|
||||
const patch = join(root, 'old node patch.mjs');
|
||||
writeFileSync(patch, "Object.defineProperty(process.versions, 'node', { value: '18.9.0', configurable: true });\n");
|
||||
writeFileSync(join(stubBin, 'node'), `#!/bin/sh\nexec '${process.execPath}' --import '${patch}' "$@"\n`);
|
||||
chmodSync(join(stubBin, 'node'), 0o755);
|
||||
const installed = installInto(root);
|
||||
const cwd = join(root, 'unrelated cwd dir');
|
||||
mkdirSync(cwd);
|
||||
const refused = await runLauncher(installed, [], { PATH: `${stubBin}:${nodeBin}:/usr/bin:/bin`, HOME: home }, [], cwd);
|
||||
assert.equal(refused.code, 1, refused.output);
|
||||
assert.ok(refused.output.includes('beehive: Node.js 22.18.0 or newer is required'), refused.output);
|
||||
assert.ok(refused.output.includes('18.9.0'), refused.output);
|
||||
assert.ok(refused.output.includes('nodejs.org'), refused.output);
|
||||
assert.ok(!refused.output.includes('Beehive — private host preview'), refused.output);
|
||||
} finally { rmSync(root, { recursive: true, force: true }); }
|
||||
});
|
||||
Reference in New Issue
Block a user