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.
Importing
import { login, LoginError } from 'yeet:auth';
| Export | Kind | Description |
|---|---|---|
login | function | Make sure the daemon is signed in, running the browser flow if it is not |
LoginError | class | What login throws — an Error carrying the daemon's error code |
Concepts
The browser flow
- The daemon opens a login session with the platform, sending a fresh code and the host's details (hostname, kernel release).
- The platform acknowledges the code and reports the URL where it can be claimed.
login()hands both toonCode. - Someone opens the URL — or scans it as a QR code, which is what
yeet loginprints — and claims the code while signed in to the platform. - 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 state | onCode | login() |
|---|---|---|
| Already signed in | not called | resolves at once with the identity the daemon holds |
| Signed out, platform reachable | called once with { code, url } | resolves when the browser claims the code; rejects with LOGIN_FAILED if the wait fails |
| Signed out, platform unreachable | not called | rejects with LOGIN_FAILED |
| Daemon stops waiting (e.g. shutting down) | already called | rejects with LOGIN_ENDED |
| Script exits while the code is pending | already called | never settles; the code is no longer claimable |
pruneKey is not a string or null | not called | rejects 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.
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.
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 ornullsends none. Anything but a string is refused withINVALID_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.
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 — an Error whose name is "LoginError", 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}`);
}
code | Raised when |
|---|---|
LOGIN_FAILED | The 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_ENDED | The stream ended with no outcome: the daemon stopped waiting — typically because it is shutting down — before the browser claimed the code. |
INVALID_ARGS | pruneKey was given and is neither a string nor null. |
ISOLATE_GONE / WHOAMI_GONE | The 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.
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.`);
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;
}
}
}