@blueforge-studio/fauth

forge-auth CLI — sign in with the device flow and inspect your forge-auth identity

Package Info

Latest Version0.4.0
Licensenone
Keywords-
Repository-
Author-
Homepage-
Bugs-

Versions

VersionPublished
0.4.02026-10-13
0.3.02026-10-13
0.2.02026-09-25

Per-version supply chain

VersionSigSBOMAtt
0.4.0
0.3.0
0.2.0

Click a version to view its details.

Supply chain

Provenance

Published by
web:14
When
2026-10-13T11:06:02Z
sha256 (registry-computed)
45eb560c1e123858f0ff2290caa2de7e6d6b54a9659eb858bb3a1ce1474756d3

Observed by the registry at publish.

Claimed by the build

The publisher claimed nothing about its build.

Attachments

Signature
none
SBOM
none

Dependencies

Loading dependency graph...

README

fauth — forge-auth CLI

fauth is the forge-auth member of the federation CLI family, alongside fgit (forge-git), forge (forge-control) and packr-cli. It signs in with forge-auth's device flow and tells you who you are, so you no longer need to read the production database to find your own user id.

fauth login                                # device flow for app "fauth": open the URL, enter the code
fauth whoami                               # user id, email, name, org memberships
fauth whoami --json
fauth orgs list | show <idOrSlug> | members <idOrSlug>
fauth orgs create --name "Acme" --slug acme --yes
fauth logout                               # revokes the key on the server, deletes the local file

Commands

CommandWhat it does
fauth login [--app-id <id or slug>]Runs the device flow (POST /api/auth/device, then polls /api/auth/device/token). The app defaults to the slug fauth (FAUTH_APP_ID overrides). Prints the verification URL and code, waits for browser approval, then stores the minted key.
fauth login --admin --email <e> [--app <slug>] [--days <n>] [--json]Agentic login with no browser and no prompts; see Agentic login.
fauth admin device-approve <code> --as <email or id> [--json]Approves someone else's waiting device login (a host with no browser) as that existing user, using the admin key (same sources as --admin). The waiting login then gets its key, which carries the user's own scopes.
fauth admin service-account-delete <id> [--json]Deletes a service account by id with the admin key (same sources as --admin). The server answers 204 even for an unknown id, so a repeat is a no-op; it does not say whether the id existed.
fauth admin apps create --name <n> --slug <s> [--redirect <url>]... [--webhook-url <u>] [--show-secret] [--json]Registers an app with the admin key. Idempotent: if the slug exists it prints the existing app (id included) and exits 0, changing nothing (a differing --redirect set only prints a note). The generated webhook secret is never printed unless --show-secret.
fauth admin apps show <slug> [--json]Prints an app's non-secret fields (id, slug, name, enabled, redirect URLs, webhook URL) so consumers can set e.g. FORGE_AUTH_APP_ID without hand-copying. Exits 2 for an unknown slug.
fauth admin mint-key --email <e> --app <idOrSlug> --scopes <a,b> [--days N] [--name n] (--print-key | --out <file>) [--json]Admin-mints a user-bound key with explicit scopes (the server rejects unknown ones; admin mint may grant any catalogue scope). Unlike login --admin it does not touch your stored login. The key is shown once: --print-key writes only the key to stdout (summary on stderr), --out writes a new mode-0600 file and refuses to overwrite.
fauth login --key-stdinStores an existing user-bound fk_… key read from stdin (for example fauth login --key-stdin < file). The key is checked before it is saved, and the command refuses to read from a terminal.
fauth logoutRevokes the stored key on the server (DELETE /api/v1/me/key), then deletes the local file. If revocation fails, the file is still deleted and a warning says the key may still be valid.
fauth whoami [--json]User id, email, name, and org memberships (org id, slug, name, role) plus the default org.
fauth orgs list [--json]The organizations you belong to.
fauth orgs show <idOrSlug> [--json]One organization you belong to (for non-members it answers exactly as if the org didn't exist).
fauth orgs members <idOrSlug> [--json]Its members (user id, email, name, role).
fauth orgs create --name <n> --slug <s> --yes [--json]Creates an org with you as owner. Without --yes, prints what would be created and exits 2. A taken slug exits 1.

Global option: --base-url <url> (env FAUTH_BASE_URL), default https://forge-auth.blueforge.studio. The URL must use https; plain http is allowed only for localhost.

Exit codes

CodeMeaning
0ok
1error (network, server, unexpected response, credential file I/O, slug taken, org not found)
2usage (bad flags or arguments, unknown app, base-URL mismatch, orgs create without --yes)
3not logged in (no credential, or the server rejected it)

Agentic login

Use this when an agent or script needs to be the operator without a browser. It follows the same pattern as fgit and packr: a secret in ~/.forge-secrets, one command, no TTY. The device flow is still the path for humans.

fauth login --admin --email you@example.com            # app "fauth", key valid for 30 days
fauth login --admin --email you@example.com --days 7 --json
FAUTH_EMAIL=you@example.com fauth login --admin --app fauth

Steps:

  1. Reads the forge-auth admin API key (the server's FORGE_GIT_AUTH_ADMIN_KEY), trying these sources in order:

    1. the FAUTH_ADMIN_KEY env var
    2. ~/.forge-secrets/forge-auth-admin.key, which must be mode 0600. fauth refuses a group- or world-readable file and prints a chmod 600 hint.
    3. on macOS, the keychain item forge-auth-admin-key (security find-generic-password -s forge-auth-admin-key -w)

    The key is never read from argv.

  2. Calls POST /api/admin/users/keys with { email, app, expiresInDays }. The server looks up the existing user by email (case-insensitive; it never creates users) and mints a key tied to that user and app, carrying the user's own scope set (user.scopes), as a device-flow key would. It uses the same code path as the device flow, so every /api/v1 route accepts the key the same way.

  3. Checks the new key with GET /api/v1/me. If that fails, fauth revokes the key and saves nothing.

  4. Saves the key to ~/.forge-secrets/forge-auth.json (0600) in the same form as the device flow, plus expiresAt, then prints whoami (or --json).

The email comes from --email, otherwise FAUTH_EMAIL, otherwise the command exits 2. --days accepts 1–90 and defaults to 30.

Security. The admin key can mint a key for any user. Keep it only in ~/.forge-secrets/forge-auth-admin.key (mode 0600) or the keychain. Never commit it, paste it into a chat, or pass it as a command-line argument. fauth never prints the admin key or the minted key. Each mint writes an audit line on the server (forge_auth.admin.user_key_minted: target user, app, key id, expiry; never the key), and fauth logout revokes the minted key.

Storing the admin key once, without it touching argv or shell history:

pbpaste > ~/.forge-secrets/forge-auth-admin.key && chmod 600 ~/.forge-secrets/forge-auth-admin.key
# or: security add-generic-password -s forge-auth-admin-key -a "$USER" -w   (prompts for the value)

Credentials

  • The credential is stored in ~/.forge-secrets/forge-auth.json with mode 0600, inside a 0700 directory. It is written to a temp file and then renamed into place. FORGE_SECRETS_DIR overrides the directory.
  • The file holds { baseUrl, token, appId, userId, email, savedAt }. The token is never printed.
  • A stored key is only ever sent to the baseUrl it was issued by. If --base-url names a different server, the command exits with 2 and makes no request.
  • FAUTH_TOKEN (env) takes precedence over the file. This matches how FORGE_GIT_TOKEN works for fgit.
  • Secrets never go in argv. The key is sent only in a JSON body (/api/v1/users/resolve) or an Authorization: Bearer header, never in a URL.
  • If the file is group- or world-readable, fauth prints a warning.

Before first login: register the fauth app (once)

forge-auth's device flow issues its key for a registered app (POST /api/auth/device { appId }). appId can be the app's id or its slug, and an unknown app returns 404 APP_NOT_FOUND. fauth login defaults to the slug fauth, so an admin registers that app once (safe to repeat):

fauth admin apps create --name "fauth CLI" --slug fauth
fauth admin apps show fauth        # prints the app_… id

Until then, pass any existing app id or slug with --app-id. The key is bound to its user, and the /api/v1 routes don't care which app it came from.

Server endpoints

The /api/v1 routes authenticate with Authorization: Bearer fk_…, and the admin mint uses X-Admin-Api-Key. The key must be user-bound (an app/admin key with no user gets 401) and unexpired (expires_at is honored). api_key rows have no scopes yet, so there is no scope check: a user-bound key acts as its user for these self-service routes. When key scopes land, the routes map to the scopes in the table. Every route is rate-limited in memory per key.

EndpointScope (future)LimitAnswers
GET /api/v1/meread:user120/min{ user: {id,email,name}, memberships, activeOrganizationId, organizationsEnabled, membershipsTruncated }; flag off → memberships: []
DELETE /api/v1/me/keywrite:user10/minrevokes exactly the presented key → { revoked }
GET /api/v1/orgsread:organization120/minthe caller's memberships (same claims as get-session)
POST /api/v1/orgs {name, slug}write:organization5/min201 via auth.api.createOrganization (caller = owner); 409 SLUG_TAKEN; 403 for guests or at the org limit; 404 ORGANIZATIONS_DISABLED when the flag is off
POST /api/admin/users/keys {email, app?, name?, expiresInDays?, scopes?}X-Admin-Api-Key (not a user key)20/min201 { key, keyId, userId, email, appId, expiresAt, scopes } (the key is shown once; scopes omitted → the user's row); 404 USER_NOT_FOUND/APP_NOT_FOUND, 403 APP_DISABLED, 400 bad input, 401 bad admin key. Audited.
GET /api/admin/apps, POST /api/admin/apps {name, slug, redirectUrls?, webhookUrl?, includeSecret?}X-Admin-Api-Keylist apps; 201 { app } (webhook secret only with includeSecret: true); 400 on a duplicate slug.
POST /api/admin/device/approve {code, user}X-Admin-Api-Key20/min200 { ok, userId, email, appId }; 404 USER_NOT_FOUND/DEVICE_REQUEST_NOT_FOUND, 409 ALREADY_APPROVED, 400, 401. Audited.
GET /api/v1/orgs/:idOrSlugread:organization120/minorg + members, members only; a non-member gets the same 404 as a missing org

If a server predates these routes, whoami falls back to POST /api/v1/users/resolve (id + email only), and the orgs commands report that the server needs upgrading. An admin user lookup (fauth users find) is not built yet.

Build, install, publish

This directory is not a pnpm workspace member. commander and esbuild are devDependencies of the forge-auth root, and the bundle includes everything, so the published package has no runtime dependencies.

# from the forge-auth repo root
pnpm install
pnpm fauth:bundle                     # → packages/fauth/dist/fauth.mjs
pnpm fauth whoami                     # or run from source via tsx

# install the local build globally (before it is on packr)
cd packages/fauth && npm pack --pack-destination /tmp && npm install -g /tmp/blueforge-studio-fauth-$(node -p "require('./package.json').version").tgz

To publish to packr (operator only; not automated yet):

pnpm fauth:bundle
cd packages/fauth
npm view @blueforge-studio/fauth@$(node -p "require('./package.json').version") --registry https://api.packr.blueforge.studio/ version \
  && echo "already published — bump the version first"      # packr answers 409 to an overwrite
npm publish --registry https://api.packr.blueforge.studio/   # auth: PACKR_TOKEN via ~/.npmrc
# consumers: npm install -g @blueforge-studio/fauth --registry https://api.packr.blueforge.studio/

Never probe packr with curl -X PUT, because an empty PUT creates an empty version. The same rule is documented in fgit's scripts/publish-fgit.sh.