fix(cli): point logicsrc login at the real app + add device-code login
Some checks are pending
CI / build (push) Waiting to run
test / test (push) Waiting to run

`logicsrc login` defaulted to http://localhost:4010 — a dev origin that
doesn't exist on an installed machine, so the printed authorize URL went
nowhere. It now defaults to the hosted credentials app (apps/pwa), reads
the documented $LOGICSRC_API, and only reuses a stored apiUrl once that
identity has actually completed a login (which is how machines got stuck
pointing at localhost). Note logicsrc.com is the marketing site and has
no /cli routes.

The loopback flow is also unusable over SSH: redirect_uri is
http://127.0.0.1:<port>/callback, which resolves to the *browser's*
machine, not the CLI's. Added a device-authorization flow — the CLI
prints a short user_code, the human approves it from any browser:

  POST /cli/device/code   mint device_code + user_code (10 min TTL)
  GET  /cli/device        approve page (login required; typo-tolerant)
  POST /cli/device        approve/deny (CSRF-guarded browser form)
  POST /cli/device/token  CLI polls -> lsk_ API key

device_code is stored sha256-hashed, single-use, with authorization_pending
/ slow_down / access_denied / expired_token poll semantics. The CLI picks
the flow automatically (SSH/CI/no-DISPLAY -> device), with --device/--web
to force it and a fallback to loopback against servers without /cli/device.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Anthony Ettinger 2026-07-28 18:07:49 +00:00
parent eaf0a6162b
commit cf475f0f4c
7 changed files with 311 additions and 16 deletions

View file

@ -38,8 +38,37 @@ export function identityPath(): string {
: join(logicsrcHome(), "identity.json");
}
/**
* The hosted LogicSRC credentials app (`apps/pwa`) where `logicsrc login`
* goes when nothing else is configured. This is deliberately NOT logicsrc.com:
* that origin serves the marketing site and has no /cli routes.
*/
export const DEFAULT_API_URL = "https://logicsrc-credentials-production.up.railway.app";
/** An explicitly configured API origin, if any. `LOGICSRC_API` is the documented one. */
export function envApiUrl(): string | undefined {
return (
process.env.LOGICSRC_API ||
process.env.LOGICSRC_API_URL ||
// Legacy: CommandBoard is a different service, so this is the last resort.
process.env.COMMANDBOARD_API_URL ||
undefined
);
}
export function defaultApiUrl(): string {
return process.env.COMMANDBOARD_API_URL || process.env.LOGICSRC_API_URL || "http://localhost:4010";
return envApiUrl() || DEFAULT_API_URL;
}
/**
* Resolve the API origin for a command: an explicit `--api-url` wins, then the
* environment, then whatever a *logged-in* identity was registered against.
* A stored URL from an identity that never completed login is ignored that
* is how machines got stuck pointing at a dev `localhost` server.
*/
export function resolveApiUrl(identity?: Pick<LocalIdentity, "apiUrl" | "apiToken">, override?: string): string {
const stored = identity?.apiToken ? identity.apiUrl : undefined;
return (override || envApiUrl() || stored || DEFAULT_API_URL).replace(/\/+$/, "");
}
function writeSecure(file: string, data: unknown): void {
@ -96,7 +125,7 @@ export async function updateIdentity(
export function requireAuth(file = identityPath()): LocalIdentity & { apiToken: string; email: string } {
const identity = readIdentity(file);
if (!identity?.apiToken || !identity.email) {
throw new Error('Not logged in. Run "logicsrc login --email you@example.com" first.');
throw new Error('Not logged in. Run "logicsrc login" first.');
}
return identity as LocalIdentity & { apiToken: string; email: string };
}

View file

@ -89,6 +89,9 @@ export {
identityPath,
logicsrcHome,
defaultApiUrl,
envApiUrl,
resolveApiUrl,
DEFAULT_API_URL,
type LocalIdentity
} from "./identity.js";
export {