mirror of
https://github.com/block/buzz.git
synced 2026-11-12 04:12:21 +00:00
fix(beehive): derive first-use owner directly from Buzz key
Bypass manual routing for Buzz sign-in, derive and validate identity in the controller, and initialize only public owner routing using the current Host relay after the stale/cancel fence. Retained owners still must match; custody remains session-only. Regression drives the real Node/Bun UI once with no controller owner configured and an explicit synthetic credential fixture. Focused 12-test checks, typecheck, and first-use PTY walkthrough pass. Earlier broad-suite 400-second failure remains unresolved; no production Keychain access or native rebuild. Signed-off-by: Logan Johnson <loganj@squareup.com>
This commit is contained in:
@@ -36,16 +36,20 @@ not a claim that the OS supports reading one JSON member independently.
|
||||
## Secret and authority flow
|
||||
|
||||
1. Renderer sends the explicit `signin-buzz` action with public routing only.
|
||||
2. Controller retains existing owner/relay when configured; otherwise the human
|
||||
supplies them. Nothing is inferred from the discovered key.
|
||||
2. The Buzz action bypasses the manual routing wizard entirely. Controller retains
|
||||
an existing owner/relay; on first use it takes the current Host relay, reads and
|
||||
validates Buzz’s key with an explicit null expected-owner mode, then derives
|
||||
the public owner from that key. Renderer supplies neither identity nor secret.
|
||||
Only the public owner/relay binding is initialized via createControllerConfig,
|
||||
after the cancellation/generation fence; no host identity is changed.
|
||||
3. Existing bounded Node credential child calls pinned `@napi-rs/keyring` via
|
||||
`loadNativeEntry()`, constructs the exact item, and calls **getPassword only**.
|
||||
Parse is bounded to a 1 MiB blob. Missing item/member, native access failure,
|
||||
malformed JSON/nsec/invalid scalar, and owner mismatch are distinct fixed
|
||||
outcomes; native denial/cancellation cannot reliably be distinguished and
|
||||
are reported together, not described as successful or as missing.
|
||||
4. Only validated matching hex secret returns over private IPC. The controller
|
||||
checks generation/cancellation and independently validates the owner match
|
||||
4. Only validated hex secret returns over private IPC. The controller
|
||||
checks generation/cancellation and independently derives the public identity and validates any retained owner match
|
||||
before routing persistence/transport creation. Error text never includes
|
||||
native/parser exceptions or key material.
|
||||
5. Existing management-client and authenticated directory code consume the
|
||||
@@ -107,5 +111,39 @@ state, not an off-screen Sign out label.
|
||||
No native source changed or native package was rebuilt/tested. No production
|
||||
Keychain access, provider/relay calls, install, push, or PR. OS prompt interaction,
|
||||
actual production-item compatibility, installed feature delivery, and independent
|
||||
security review remain unvalidated. Existing guarded publication/export route
|
||||
must wait for that review.
|
||||
security review remain unvalidated. These historical limitations are not a gate on the explicitly requested prototype publication.
|
||||
|
||||
|
||||
## First-use regression and laptop smoke (current fix)
|
||||
|
||||
The prior one-gesture fixture had controller routing preconfigured and therefore
|
||||
missed the actual first-use wizard. `test/buzz-first-use-smoke.py` now drives the
|
||||
real Node controller + Bun renderer through Agents → Actions → Buzz key → Enter
|
||||
once. Preconditions are explicit: a fresh temporary HOME, **no controller.json or
|
||||
configured controller owner**, a normal synthetic Host with `wss://fixture.invalid`,
|
||||
and an explicit synthetic credential file. The fixture selects the synthetic Buzz
|
||||
identity independently of the requested owner (`BEEHIVE_TEST_BUZZ_OWNER`), matching
|
||||
the canonical reader's first-use contract. There are no production Keychain reads.
|
||||
Directory, services, native OAuth, providers and transport remain fenced by the
|
||||
existing loader; signed-in UI is verified, not real relay connectivity.
|
||||
|
||||
Run from `beehive/` with real runtime binaries (not Hermit shim symlinks):
|
||||
|
||||
```sh
|
||||
BEEHIVE_SMOKE_NODE=/absolute/path/to/node-24.15.0/bin/node \
|
||||
BEEHIVE_BUN=/absolute/path/to/packaged/bun \
|
||||
python3 -u test/buzz-first-use-smoke.py
|
||||
```
|
||||
|
||||
The test checks the derived public config, signed-in rendered state, absence of
|
||||
public-key/Step 1/nsec/Type yes prompts throughout terminal replay, unchanged
|
||||
credential file, and no private-key bytes in Beehive storage or terminal output.
|
||||
It prints the surviving temporary HOME with `ui.ansi` and `ui-frames.txt`.
|
||||
Actual walkthrough passed with Node 24.15.0 and packaged Bun 1.4.2. Focused backend
|
||||
checks remain 12/12 and TypeScript passes; cancellation/close now also exercise
|
||||
unconfigured first use, and the real credential-child check covers null owner.
|
||||
The earlier broad-suite 400-second failure above remains unresolved; no full-suite
|
||||
retry, native rebuild, production Keychain access, or installation was performed.
|
||||
For laptop acceptance: retain the normal Host, ensure no controller owner has yet
|
||||
been configured, then select Buzz-key sign-in once. Existing configured owners
|
||||
still must match. Do not delete or retarget a real retained owner just to smoke.
|
||||
|
||||
@@ -5,8 +5,9 @@ import { publicKey } from './protocol.ts';
|
||||
/** Release Desktop source contract: secret_store.rs BLOB_KEY; app_state.rs
|
||||
* IDENTITY_KEY_NAME; app_state_keyring.rs keyring_service (6d1f488d).
|
||||
* The containing item also holds agent keys. Never return it, enumerate its
|
||||
* members, migrate legacy entries, or call a write API. Explicit sign-in only. */
|
||||
export function readBuzzOwnerKey(owner: string, read: () => string | null = () => new (loadNativeEntry())('buzz-desktop', 'secrets').getPassword()): { ok: true; secret: string } | { ok: false; reason: 'missing' | 'access' | 'malformed' | 'mismatch' } {
|
||||
* members, migrate legacy entries, or call a write API. Explicit sign-in only.
|
||||
* null explicitly permits first-use identity derivation; undefined is not first use. */
|
||||
export function readBuzzOwnerKey(owner: string | null, read: () => string | null = () => new (loadNativeEntry())('buzz-desktop', 'secrets').getPassword()): { ok: true; secret: string } | { ok: false; reason: 'missing' | 'access' | 'malformed' | 'mismatch' } {
|
||||
let raw: string | null;
|
||||
try { raw = read(); } catch { return { ok: false, reason: 'access' }; }
|
||||
if (raw === null) return { ok: false, reason: 'missing' };
|
||||
@@ -19,7 +20,7 @@ export function readBuzzOwnerKey(owner: string, read: () => string | null = () =
|
||||
if (typeof blob.identity !== 'string') throw Error();
|
||||
secret = agentNsec(blob.identity);
|
||||
} catch { return { ok: false, reason: 'malformed' }; }
|
||||
if (publicKey(secret) !== owner) return { ok: false, reason: 'mismatch' };
|
||||
if (owner !== null && publicKey(secret) !== owner) return { ok: false, reason: 'mismatch' };
|
||||
return { ok: true, secret };
|
||||
}
|
||||
|
||||
|
||||
@@ -439,16 +439,22 @@ A Stop result is not a recent host report that confirms the agent is stopped.` }
|
||||
check(); this.status = 'Local agent added and stopped. Nothing was started or published to the relay.';
|
||||
} else if (request.action === 'signin' || request.action === 'signin-buzz') {
|
||||
const retained = readControllerConfig(this.ownerDirectory);
|
||||
const owner = retained?.owner ?? ownerPublicInput(v.owner ?? '');
|
||||
const relay = retained?.relay ?? v.relay ?? '';
|
||||
let owner = retained?.owner ?? (request.action === 'signin-buzz' ? null : ownerPublicInput(v.owner ?? ''));
|
||||
const relay = retained?.relay ?? (request.action === 'signin-buzz'
|
||||
? readHostIdentityPublic(this.hostDirectory).pairing.relay : v.relay ?? '');
|
||||
if (!/^(wss|ws):\/\//.test(relay)) throw plain('Enter a relay URL that starts with ws:// or wss://.');
|
||||
const result = await this.credential({ action: request.action, owner, ...(request.action === 'signin' && v.secret ? { secret: v.secret.toLowerCase() } : {}) }, abort.signal);
|
||||
delete v.secret; check();
|
||||
if (request.action === 'signin-buzz') {
|
||||
let matches = false;
|
||||
try { matches = typeof result.secret === 'string' && publicKey(result.secret) === owner; } catch { /* Untrusted helper result: never expose its bytes. */ }
|
||||
try {
|
||||
const derived = typeof result.secret === 'string' ? publicKey(result.secret) : undefined;
|
||||
matches = derived !== undefined && (owner === null || derived === owner);
|
||||
if (matches && derived !== undefined) owner = derived;
|
||||
} catch { /* Untrusted helper result: never expose its bytes. */ }
|
||||
if (!matches) throw plain('Buzz key is unavailable, malformed, or does not match the configured Beehive owner. No owner or relay was changed.');
|
||||
}
|
||||
if (owner === null) throw plain('Buzz Keychain owner key is malformed. No key or configured owner was changed.');
|
||||
if (result.secret === null) throw plain('No saved owner key. Choose Import matching owner key and sign in. Enter the matching private key with 64 hex characters.');
|
||||
if (!retained) createControllerConfig(this.ownerDirectory, owner, relay);
|
||||
this.client?.close(); this.inventory.clear(); this.offers.clear(); this.directory = [];
|
||||
|
||||
@@ -46,14 +46,8 @@ async function routing(action: string, secret = false) {
|
||||
if (key === undefined) return;
|
||||
values.secret = key;
|
||||
}
|
||||
// Buzz-key sign-in carries no extra Beehive prompt: selecting the action is
|
||||
// itself the explicit gesture (an OS keychain permission dialog is separate
|
||||
// and still applies), so the request dispatches immediately. The saved-key
|
||||
// and import paths keep their confirmation.
|
||||
if (action !== 'signin-buzz') {
|
||||
const confirmation = `${action === 'configure' ? 'Configure this computer' : 'Sign in'}\nOwner: ${shortIdentity(owner)}\nRelay: ${relay}\n${action === 'configure' ? 'Create host identity in the OS credential store?' : 'Access owner key in the OS credential store?'}`;
|
||||
if (!await screen.confirm(confirmation)) { delete values.secret; return; }
|
||||
}
|
||||
const confirmation = `${action === 'configure' ? 'Configure this computer' : 'Sign in'}\nOwner: ${shortIdentity(owner)}\nRelay: ${relay}\n${action === 'configure' ? 'Create host identity in the OS credential store?' : 'Access owner key in the OS credential store?'}`;
|
||||
if (!await screen.confirm(confirmation)) { delete values.secret; return; }
|
||||
await request(action, values); delete values.secret;
|
||||
}
|
||||
async function registerForm(continuation?: string, agent?: string) {
|
||||
@@ -157,7 +151,7 @@ The source stops before destination launch. Workspace, session and credentials s
|
||||
{ label: 'Sign out', run: () => request('signout') },
|
||||
{ label: 'Publish profile or instructions', run: () => screen.notice('Publication forms are unavailable. Use the CLI:\nbeehive drafts ~/.beehive/owner\nbeehive tui discover <relay> ~/.beehive/owner\nThis build cannot generate owner keys.') },
|
||||
] : [
|
||||
{ label: 'Sign in with Buzz key', run: () => routing('signin-buzz') },
|
||||
{ label: 'Sign in with Buzz key', run: () => request('signin-buzz') },
|
||||
{ label: 'Sign in with saved owner key', run: () => routing('signin') },
|
||||
{ label: 'Import matching owner key and sign in', run: () => routing('signin', true) },
|
||||
];
|
||||
|
||||
112
beehive/test/buzz-first-use-smoke.py
Normal file
112
beehive/test/buzz-first-use-smoke.py
Normal file
@@ -0,0 +1,112 @@
|
||||
"""Real Node controller + Bun renderer, synthetic credentials only; run from beehive.
|
||||
BEEHIVE_SMOKE_NODE and BEEHIVE_BUN must point to Node 24.15.0 and packaged Bun.
|
||||
No configured controller owner: only a normal Host and a synthetic Buzz key exist.
|
||||
"""
|
||||
import os, pty, select, time, struct, fcntl, termios, json, tempfile, subprocess
|
||||
from pathlib import Path
|
||||
root = os.getcwd()
|
||||
home = tempfile.mkdtemp(prefix='beehive-pairing-cli-buzz-first-use-')
|
||||
node = os.environ['BEEHIVE_SMOKE_NODE']
|
||||
env = {'PATH': '/usr/bin:/bin', 'HOME': home, 'TERM': 'xterm-256color',
|
||||
'BEEHIVE_TEST_CREDENTIAL_FILE': home+'/credentials.json',
|
||||
'BEEHIVE_BUN': os.environ['BEEHIVE_BUN'],
|
||||
'NODE_OPTIONS': '--import='+root+'/test/manager-installed-loader.ts'}
|
||||
seed = """import {bootstrapHostIdentity} from './src/host-identity.ts';
|
||||
import {createCredential} from './src/credential-store.ts';
|
||||
import {publicKey} from './src/protocol.ts';
|
||||
import {isolatedFileCredentials} from './test/isolated-file-credentials.ts';
|
||||
const key='1'.repeat(64), backend=isolatedFileCredentials(process.env.BEEHIVE_TEST_CREDENTIAL_FILE);
|
||||
createCredential('owner',key,backend);
|
||||
bootstrapHostIdentity(process.env.HOME+'/.beehive/host','Fixture Host',publicKey(key),'wss://fixture.invalid',backend);
|
||||
console.log(publicKey(key));"""
|
||||
owner = subprocess.check_output([node, '--input-type=module', '-e', seed], env=env, text=True).strip()
|
||||
env['BEEHIVE_TEST_BUZZ_OWNER'] = owner
|
||||
config = Path(home+'/.beehive/owner/controller.json')
|
||||
assert not config.exists(), 'Regression must start with NO configured owner'
|
||||
before = Path(env['BEEHIVE_TEST_CREDENTIAL_FILE']).read_bytes()
|
||||
pid, fd = pty.fork()
|
||||
if pid == 0:
|
||||
os.execve(node, [node, 'src/manager-entry.ts'], env)
|
||||
fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack('HHHH', 36, 140, 0, 0))
|
||||
transcript = b''
|
||||
frames = []
|
||||
import re
|
||||
ROWS, COLS = 36, 140
|
||||
def screen(buf):
|
||||
cells={}; row=col=1; i=0; n=len(buf)
|
||||
while i<n:
|
||||
b=buf[i]
|
||||
if b==27 and i+1<n and buf[i+1]==91:
|
||||
j=i+2
|
||||
while j<n and not 64<=buf[j]<127: j+=1
|
||||
if j>=n: break
|
||||
params=buf[i+2:j].decode('latin1'); final=buf[j:j+1].decode('latin1')
|
||||
if final=='H':
|
||||
p=params.split(';')
|
||||
try: row=int(p[0]) if p[0] else 1; col=int(p[1]) if len(p)>1 and p[1] else 1
|
||||
except ValueError: pass
|
||||
elif final=='J' and params in ('','0','2'): cells={}
|
||||
elif final=='K':
|
||||
for c in range(col,COLS+1): cells.pop((row,c),None)
|
||||
i=j+1; continue
|
||||
if b==27 and i+1<n and buf[i+1] in (80,93,95,94):
|
||||
j=i+2
|
||||
while j<n:
|
||||
if buf[j]==7: j+=1; break
|
||||
if buf[j]==27 and j+1<n and buf[j+1]==92: j+=2; break
|
||||
j+=1
|
||||
i=j; continue
|
||||
if b==27: i+=1; continue
|
||||
if b==13: col=1; i+=1; continue
|
||||
if b==10: row=min(ROWS,row+1); i+=1; continue
|
||||
if b<32 or b==127: i+=1; continue
|
||||
L=1 if b<128 else 2 if b<224 else 3 if b<240 else 4
|
||||
try: ch=buf[i:i+L].decode('utf-8')
|
||||
except UnicodeDecodeError: i+=1; continue
|
||||
if row<=ROWS and col<=COLS: cells[(row,col)]=ch
|
||||
col+=1; i+=L
|
||||
rows={}
|
||||
for (r,c),ch in cells.items(): rows.setdefault(r,{})[c]=ch
|
||||
return [''.join(rows.get(r,{}).get(c,' ') for c in range(1,COLS+1)).rstrip() for r in range(1,ROWS+1)]
|
||||
|
||||
def wait(seconds=1):
|
||||
global transcript
|
||||
end = time.time()+seconds
|
||||
while time.time() < end:
|
||||
if select.select([fd], [], [], .05)[0]:
|
||||
try:
|
||||
transcript += os.read(fd, 65536)
|
||||
frames.append('\n'.join(screen(transcript)))
|
||||
except OSError:
|
||||
break
|
||||
|
||||
def send(data):
|
||||
os.write(fd, data)
|
||||
wait()
|
||||
|
||||
try:
|
||||
wait(2)
|
||||
send(b'\x1b[C') # Agents
|
||||
send(b'a') # Actions
|
||||
assert 'Sign in with Buzz key' in frames[-1]
|
||||
send(b'\x1b[B') # Buzz action, not saved-key action
|
||||
send(b'\r') # ONE selection; no more input
|
||||
wait(2)
|
||||
assert config.exists(), 'One selection did not initialize owner routing'
|
||||
assert json.loads(config.read_text()) == {'version': 1, 'owner': owner, 'relay': 'wss://fixture.invalid'}
|
||||
assert any('signed in' in frame.lower() for frame in frames), 'No signed-in UI state'
|
||||
for frame in frames:
|
||||
for forbidden in ['1 of 3', 'PUBLIC KEY', 'Owner public key', 'Type yes', 'Matching nsec', 'Matching private key']:
|
||||
assert forbidden not in frame, 'Unexpected modal: '+forbidden
|
||||
assert Path(env['BEEHIVE_TEST_CREDENTIAL_FILE']).read_bytes() == before, 'Credential copy/write'
|
||||
for file in Path(home+'/.beehive').rglob('*'):
|
||||
if file.is_file():
|
||||
assert b'1'*64 not in file.read_bytes(), 'Private key persisted'
|
||||
assert b'nsec1' not in file.read_bytes(), 'nsec persisted'
|
||||
assert b'1'*64 not in transcript and b'nsec1' not in transcript, 'Private key rendered'
|
||||
print('PASS first use: Agents -> Buzz key once -> derived owner signed in; no prompts or private copy. HOME='+home)
|
||||
finally:
|
||||
Path(home+'/ui.ansi').write_bytes(transcript)
|
||||
Path(home+'/ui-frames.txt').write_text('\n--- FRAME ---\n'.join(frames))
|
||||
os.killpg(pid, 9)
|
||||
os.waitpid(pid, 0)
|
||||
@@ -8,6 +8,8 @@ import { readBuzzOwnerKey, buzzOwnerKeyErrors } from '../src/buzz-owner-key.ts';
|
||||
import { createControllerConfig } from '../src/controller-config.ts';
|
||||
import { managerCredential } from '../src/manager-credential.ts';
|
||||
import { ManagerController } from '../src/manager-controller.ts';
|
||||
import { bootstrapHostIdentity } from '../src/host-identity.ts';
|
||||
import { isolatedFileCredentials } from './isolated-file-credentials.ts';
|
||||
import { publicKey } from '../src/protocol.ts';
|
||||
|
||||
const secret = '1'.repeat(64), owner = publicKey(secret);
|
||||
@@ -27,6 +29,7 @@ test('explicit Buzz signin validates actual blob identity and excludes all other
|
||||
['wrong owner', () => JSON.stringify({identity:nip19.nsecEncode(Buffer.from('2'.repeat(64),'hex'))}), 'mismatch'],
|
||||
] as const) {
|
||||
const home = mkdtempSync(join(tmpdir(), 'beehive-pairing-cli-buzz-owner-'));
|
||||
createControllerConfig(join(home,'.beehive','owner'),owner,'wss://fixture.invalid');
|
||||
let reads = 0, connections = 0, directoryReads = 0;
|
||||
const outputs: unknown[] = [];
|
||||
const c = new ManagerController(home, s => outputs.push(s), async (input) => {
|
||||
@@ -46,7 +49,7 @@ test('explicit Buzz signin validates actual blob identity and excludes all other
|
||||
assert.equal(result.state, reason ? 'failed' : 'completed',label);
|
||||
assert.equal(connections,reason ? 0 : 1,label);
|
||||
const config = join(home,'.beehive','owner','controller.json');
|
||||
if (reason) { assert.equal(c.snapshot().owner,undefined); assert.equal(existsSync(config),false); }
|
||||
if (reason) { assert.equal(c.snapshot().owner,undefined); assert.deepEqual(JSON.parse(readFileSync(config,'utf8')), {version:1,owner,relay:'wss://fixture.invalid'}); }
|
||||
else {
|
||||
assert.equal(c.snapshot().owner,owner);
|
||||
assert.deepEqual(JSON.parse(readFileSync(config,'utf8')), {version:1,owner,relay:'wss://fixture.invalid'});
|
||||
@@ -62,18 +65,20 @@ test('explicit Buzz signin validates actual blob identity and excludes all other
|
||||
});
|
||||
|
||||
test('Buzz late completion and untrusted helper mismatch cannot configure or connect', async () => {
|
||||
for (const mode of ['cancel','close','mismatch','malformed'] as const) {
|
||||
for (const mode of ['cancel','close','mismatch','malformed','first-use-cancel','first-use-close','first-use-malformed'] as const) {
|
||||
const home = mkdtempSync(join(tmpdir(),'beehive-pairing-cli-buzz-stale-'));
|
||||
bootstrapHostIdentity(join(home,'.beehive','host'),'Fixture',owner,'wss://fixture.invalid',isolatedFileCredentials(join(home,'credentials.json')));
|
||||
if (!mode.startsWith('first-use')) createControllerConfig(join(home,'.beehive','owner'),owner,'wss://fixture.invalid');
|
||||
let finish!: (value: unknown) => void;
|
||||
const c = new ManagerController(home,()=>{},()=>new Promise(resolve=>{finish=resolve;}), (()=>{throw Error('must never connect');}) as any, async()=>undefined);
|
||||
try {
|
||||
const pending = c.request({id:1,action:'signin-buzz',values:{owner,relay:'wss://fixture.invalid'}});
|
||||
if (mode === 'cancel') c.cancel();
|
||||
if (mode === 'close') c.close();
|
||||
finish({ok:true,secret:mode === 'mismatch' ? '2'.repeat(64) : mode === 'malformed' ? nsec : secret});
|
||||
if (mode.endsWith('cancel')) c.cancel();
|
||||
if (mode.endsWith('close')) c.close();
|
||||
finish({ok:true,secret:mode === 'mismatch' ? '2'.repeat(64) : mode.endsWith('malformed') ? nsec : secret});
|
||||
assert.notEqual((await pending).state,'completed');
|
||||
assert.equal(c.snapshot().owner,undefined);
|
||||
assert.equal(existsSync(join(home,'.beehive','owner','controller.json')),false);
|
||||
assert.equal(existsSync(join(home,'.beehive','owner','controller.json')),!mode.startsWith('first-use'));
|
||||
assert.ok(!JSON.stringify(c.snapshot()).includes(nsec));
|
||||
} finally { c.close(); rmSync(home,{recursive:true,force:true}); }
|
||||
}
|
||||
@@ -82,6 +87,7 @@ test('Buzz late completion and untrusted helper mismatch cannot configure or con
|
||||
|
||||
test('actual signin child reads only the canonical item; safe missing/access errors and no Beehive copy', async () => {
|
||||
const helper = new URL('./buzz-owner-child-fixture.ts', import.meta.url);
|
||||
assert.deepEqual(await managerCredential({action:'signin-buzz',owner:null,fixture:{blob}},new AbortController().signal,helper),{ok:true,secret});
|
||||
assert.deepEqual(await managerCredential({action:'signin-buzz',owner,fixture:{blob}},new AbortController().signal,helper),{ok:true,secret});
|
||||
for (const [fixture, pattern] of [[{blob:null},/No Buzz owner key/],[{denied:nsec},/locked, denied, cancelled/],[{blob:'invalid'},/malformed/],[{blob},/does not match/]] as const) {
|
||||
await assert.rejects(managerCredential({action:'signin-buzz',owner:pattern.source.includes('does not match') ? publicKey('2'.repeat(64)) : owner,fixture},new AbortController().signal,helper), error => {
|
||||
|
||||
@@ -25,7 +25,7 @@ export async function fixtureCredential(input: any, signal: AbortSignal) {
|
||||
// Esc cancellation deterministically; inert unless the driver sets it.
|
||||
const paced = Number(process.env.BEEHIVE_TEST_SIGNIN_DELAY_MS ?? '0');
|
||||
if (paced > 0) await new Promise((resolve, reject) => { const timer = setTimeout(resolve, paced); signal.addEventListener('abort', () => { clearTimeout(timer); reject(signal.reason); }, { once: true }); });
|
||||
const key = backend.read(credentialReference('owner', input.owner));
|
||||
const key = backend.read(credentialReference('owner', process.env.BEEHIVE_TEST_BUZZ_OWNER ?? input.owner));
|
||||
const result = readBuzzOwnerKey(input.owner, () => key === null ? null : JSON.stringify({identity:nip19.nsecEncode(Buffer.from(key,'hex'))}));
|
||||
if (!result.ok) throw Error(buzzOwnerKeyErrors[result.reason]);
|
||||
return result;
|
||||
|
||||
Reference in New Issue
Block a user