Skip to main content

yeet:auth

yeet:auth signs the daemon in from a script. It runs the same browser flow as yeet login: the platform issues a short code and a URL to claim it at, the script shows them, and once someone claims the code in a browser the daemon stores the tokens. The tokens land in the daemon's identity, so a successful login() signs in the whole daemon — every script and every later yeet command — not just the caller.

import { login } from 'yeet:auth';

const whoami = await login({
onCode: ({ code, url }) => console.log(`Log in at ${url} (code ${code})`),
});

console.log(`Signed in as ${whoami.owner_id} on host ${whoami.host_id}`);

The handshake runs daemon-side over the daemon's own connection to the platform. The script never sees the access or refresh tokens — it learns only who owns the host and which host it is.

Because the identity belongs to the daemon, it can change under a running script: yeet logout, a login from another script, the platform deregistering the host. watchWhoAmI() follows it, calling back with the current identity and every change after.

import { watchWhoAmI } from 'yeet:auth';

const unwatch = await watchWhoAmI((whoami) => {
if (whoami === null) console.log('Signed out.');
else console.log(`Signed in as ${whoami.owner_id} on host ${whoami.host_id}`);
});

Importing​

import { login, watchWhoAmI, LoginError, WhoAmIWatchError } from 'yeet:auth';
ExportKindDescription
loginfunctionMake sure the daemon is signed in, running the browser flow if it is not
watchWhoAmIfunctionFollow the daemon's identity: the current one, then every login and logout
LoginErrorclassWhat login throws — an Error carrying the daemon's error code
WhoAmIWatchErrorclassWhat watchWhoAmI throws when the watch cannot be opened

Concepts​

The browser flow​

  1. The daemon opens a login session with the platform, sending a fresh code and the host's details (hostname, kernel release).
  2. The platform acknowledges the code and reports the URL where it can be claimed. login() hands both to onCode.
  3. Someone opens the URL — or scans it as a QR code, which is what yeet login prints — and claims the code while signed in to the platform.
  4. The platform answers with tokens. The daemon stores them as its identity, and login() resolves with { owner_id, host_id }.

Step 2 is the first moment the script has anything to show; step 4 takes as long as the person takes. login() is one await across all four, and onCode is how the script gets a word in while it waits.

What one call does, by situation:

Daemon stateonCodelogin()
Already signed innot calledresolves at once with the identity the daemon holds
Signed out, platform reachablecalled once with { code, url }resolves when the browser claims the code; rejects with LOGIN_FAILED if the wait fails
Signed out, platform unreachablenot calledrejects with LOGIN_FAILED
Daemon stops waiting (e.g. shutting down)already calledrejects with LOGIN_ENDED
Script exits while the code is pendingalready callednever settles; the code is no longer claimable
pruneKey is not a string or nullnot calledrejects with INVALID_ARGS

Already signed in​

A daemon that already holds an identity never reaches the platform: login() resolves at once with the identity it has, and onCode is never called. That makes login() an ensure signed in — call it unconditionally at the top of a script that needs the platform, and it costs nothing when there is nothing to do.

To find out whether the daemon is signed in without starting a login, use yeet.whoami(): it resolves with the same identity, or null when signed out.

To sign in as someone else, sign out first with yeet logout. yeet:auth has no logout of its own.

Watching the identity​

login() and yeet.whoami() answer for one moment. The daemon's identity is shared state, though, and anything that can change it does so without telling the scripts that are running: yeet logout, yeet login from another terminal, a login() in another script, the platform deregistering the host. watchWhoAmI() is the standing version of the question. Its callback gets the identity the daemon holds right now, null when signed out, and is called again on every change — a login as { owner_id, host_id }, a logout as null — until the watch is closed.

Only changes to who are reported. The daemon refreshes its tokens on a schedule, and a refresh is not a change: the callback does not fire for it. Nor does it fire when a sign-in or sign-out fails and leaves the identity as it was.

A watch sees the daemon, not the script. Two scripts watching the same daemon get the same sequence, and a watch opened before a login() in the same script reports that login like any other.

Lifetime​

The wait for the browser is a resource of the isolate, keyed by the code. It keeps the script alive until the code is claimed or the login fails, so a script whose last statement is await login() does not exit with the code still on screen. The reverse holds too: a script that ends while the code is pending — Ctrl+C, yeet.exit(), a crash — takes its login with it, and that code can no longer be claimed. Once login() has settled, nothing is held; a script with nothing else to do exits normally.

A watch is a resource of the isolate in the same way, and it is open-ended: it keeps the script alive, like a setInterval, until the unwatch function watchWhoAmI() resolved with is called. A script that wants to exit on its own after watching must close its watch. A script that ends with a watch open takes it with it; nothing is left behind in the daemon.

Prune keys​

pruneKey is the same value as yeet login --prune-key: a per-host secret chosen at registration time. Whoever holds it can later prune (deregister) the host through the API, which is how a host whose lifecycle something else owns — an autoscaling group, a Terraform resource — gets cleaned up when the machine goes away. See Terraform for the lifecycle patterns. Pass one when a script registers a host that will be torn down by automation; leave it out for an interactive login on a machine you manage by hand.


F login​

login(options?: {
pruneKey?: string | null;
onCode?: (issued: Issued) => void;
}): Promise<WhoAmI>

Makes sure the daemon is signed in, and resolves with who it is signed in as.

  • pruneKey — the host's prune key. Omitted or null sends none. Anything but a string is refused with INVALID_ARGS.
  • onCode — called once, as soon as the platform has issued the code, with { code, url }. Show them; this is the only signal the script gets before the browser answers. Not called when the daemon was already signed in.

If the daemon is signed in, this resolves immediately. Otherwise it resolves when the browser claims the code and the tokens are stored, or rejects with a LoginError when the platform could not be reached, the options were malformed, the login failed while waiting, or the daemon gave up the wait.

import { login } from 'yeet:auth';

const whoami = await login({
pruneKey: yeet.args.prune_key, // yeet run register.js --prune-key "$(uuidgen)"
onCode: ({ code, url }) => {
console.log(`Open ${url}`);
console.log(`and enter the code ${code} if asked.`);
},
});

An exception thrown from onCode rejects the call with that exception, not a LoginError, and does not cancel the flow the daemon has already started — the code stays claimable until the script exits.


F watchWhoAmI​

watchWhoAmI(onChange: (whoami: WhoAmI | null) => void): Promise<() => Promise<void>>

Follows the daemon's identity. Once the watch is open, onChange is called with the identity the daemon holds — WhoAmI, or null while signed out — and again each time it changes, by whatever means. See Watching the identity for what counts as a change.

Resolves with an unwatch function once the watch is open and the first call is on its way. Awaiting unwatch() ends the watch: no further calls, and the daemon frees its side. Calling it a second time, or after the daemon has already ended the watch, does nothing. Until it is called, the watch keeps the script alive.

  • onChange — called with WhoAmI | null. Anything but a function is refused with a TypeError before the daemon is asked.

Rejects with a WhoAmIWatchError if the watch could not be opened. The daemon ends a watch on its own only when it is shutting down; a watch left open through a logout stays open and reports the next login.

import { watchWhoAmI } from 'yeet:auth';

const unwatch = await watchWhoAmI((whoami) => {
console.log(whoami === null ? 'signed out' : `signed in as ${whoami.owner_id}`);
});

// ... later, when the script is done watching:
await unwatch();

The first call always comes, even when nothing has changed, so a script can treat onChange as the one place it learns the identity and skip the separate yeet.whoami() check.


Data types​

I Issued​

interface Issued {
code: string; // ten URL-safe alphanumerics: digits and ASCII letters
url: string; // where the browser claims it, as the platform reported it
}

The code is drawn from digits and unambiguous ASCII letters only, so it survives a QR scan and a manual retype. The URL already embeds the code; show the URL and the code is a fallback for someone typing by hand.

I WhoAmI​

interface WhoAmI {
owner_id: string; // the account that owns this host
host_id: string; // this host's id on the platform
}

The same shape yeet.whoami() resolves with, and what yeet whoami reports on the command line. The access and refresh tokens stay in the daemon; nothing secret crosses into the isolate.


Errors​

login() rejects with a LoginError and watchWhoAmI() with a WhoAmIWatchError. Both are Errors whose name is the class name, with a code string (null when the daemon sent none), a message, and the raw payload the daemon sent. Match on code:

import { login, LoginError } from 'yeet:auth';

try {
await login({ onCode: show });
} catch (e) {
if (!(e instanceof LoginError)) throw e;
if (e.code === 'LOGIN_ENDED') console.error('The daemon gave up waiting for the browser.');
else console.error(`Login failed: ${e.message}`);
}
codeRaised when
LOGIN_FAILEDThe platform could not be reached, refused the request, closed the connection while waiting, acknowledged a different code than was sent, or the daemon could not store the tokens. The message says which.
LOGIN_ENDEDThe stream ended with no outcome: the daemon stopped waiting — typically because it is shutting down — before the browser claimed the code.
INVALID_ARGSpruneKey was given and is neither a string nor null.
ISOLATE_GONE / WHOAMI_GONEThe daemon is tearing down the isolate or its identity store. Nothing for a script to handle.

Which phase failed is visible without a code: a rejection before onCode fired means the platform could not be reached or refused the session; one after means the wait itself failed.

watchWhoAmI() has fewer ways to fail, since it never reaches the platform:

codeRaised when
INVALID_ARGSThe daemon was handed arguments it did not expect. Not reachable through watchWhoAmI() itself, which refuses a non-function with a TypeError first.
ISOLATE_GONE / WHOAMI_GONEThe daemon is tearing down the isolate or its identity store.
MOUNT_FAILEDThe isolate could not take the watch on as a resource.

Once open, a watch does not fail. If the daemon shuts down, the watch simply ends: onChange is not called again and unwatch() becomes a no-op.


Recipes​

Ensure signed in, then carry on​

login() is cheap when the daemon is already signed in, so a script that needs the platform can start with it and never check first:

import { login } from 'yeet:auth';

await login({
onCode: ({ url }) => console.log(`This host is not signed in yet. Log in at ${url}`),
});

// From here on, yeet.graph and anything else that needs the platform will work.

Check first, prompt only if needed​

login() already short-circuits when the daemon is signed in, but sometimes the script wants to say something different in each case — or refuse to start a login at all, say in a non-interactive run:

import { login } from 'yeet:auth';

const me = await yeet.whoami();

if (me) {
console.log(`Already signed in as ${me.owner_id}.`);
} else if (yeet.args.no_login) {
console.error('Not signed in, and --no-login was given. Run `yeet login` first.');
yeet.exit();
} else {
const whoami = await login({
onCode: ({ url }) => console.log(`Not signed in yet. Log in at ${url}`),
});
console.log(`Signed in as ${whoami.owner_id}.`);
}

Show the code in a TUI while you wait​

The wait can be long, so put the code somewhere that stays on screen. A signal driven from onCode is enough:

import { login, LoginError } from 'yeet:auth';
import { Box, Text, mount, signal } from 'yeet:tui';

const status = signal('Contacting the platform…');

mount(() => (
<Box border="round" padding={1}>
<Text>{() => status.get()}</Text>
</Box>
));

try {
const whoami = await login({
onCode: ({ code, url }) => status.set(`Log in at ${url}\ncode: ${code}`),
});
status.set(`Signed in as ${whoami.owner_id} on ${whoami.host_id}`);
} catch (e) {
status.set(e instanceof LoginError ? `Login failed (${e.code}): ${e.message}` : String(e));
}

Register a host that automation will tear down​

Generate the prune key outside the script, pass it in, and keep it where the teardown can find it — the same shape as the autoscaled-host pattern:

import { login } from 'yeet:auth';

const pruneKey = yeet.args.prune_key;
if (typeof pruneKey !== 'string') {
console.error('usage: yeet run register.js --prune-key <secret>');
yeet.exit();
}

const { host_id } = await login({
pruneKey,
onCode: ({ url }) => console.log(`Claim this host at ${url}`),
});

console.log(`Registered as ${host_id}; prune with the key you passed.`);

React to a logout​

A long-running script that needs the platform can learn it has lost its sign-in the moment it happens, rather than at the next failed call. Pair login() with a watch:

import { login, watchWhoAmI } from 'yeet:auth';

await login({ onCode: ({ url }) => console.log(`Log in at ${url}`) });

await watchWhoAmI((whoami) => {
if (whoami !== null) return;
console.error('This host was signed out. Stopping.');
yeet.exit();
});

// Carry on; the callback above ends the script if the sign-in goes away.

The watch's first call reports the identity login() just returned, so the null check skips it. To sign back in instead of stopping, call login() again from the callback: the watch is still open and will report the new identity when the browser claims the code.

Keep a status line in sync​

A TUI that shows who the daemon is signed in as should follow the daemon, not what the script last did. Drive a signal from the watch:

import { watchWhoAmI } from 'yeet:auth';
import { Box, Text, mount, signal } from 'yeet:tui';

const who = signal('…');

mount(() => (
<Box border="round" padding={1}>
<Text>{() => who.get()}</Text>
</Box>
));

const unwatch = await watchWhoAmI((whoami) => {
who.set(whoami === null ? 'Not signed in' : `${whoami.owner_id} on ${whoami.host_id}`);
});

Running yeet logout or yeet login in another terminal updates the line. Close the watch with unwatch() when the TUI exits, or let the script's end take it.

Retry an abandoned login​

LOGIN_ENDED means the daemon stopped waiting, not that the person declined. If the script is meant to outlive a daemon restart, run the flow again — a new code is issued each time:

import { login, LoginError } from 'yeet:auth';

async function signIn(show) {
for (;;) {
try {
return await login({ onCode: show });
} catch (e) {
if (e instanceof LoginError && e.code === 'LOGIN_ENDED') continue;
throw e;
}
}
}