Skip to content

feat(auth): A2 — sign in by mailed code on web, iOS, Mac and CLI#291

Merged
oratis merged 1 commit into
feat/auth-google-serverfrom
feat/auth-otp-clients
Jul 24, 2026
Merged

feat(auth): A2 — sign in by mailed code on web, iOS, Mac and CLI#291
oratis merged 1 commit into
feat/auth-google-serverfrom
feat/auth-otp-clients

Conversation

@oratis

@oratis oratis commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Stacked on #290. The four clients for the A1 endpoints.

Everywhere the shape is the same: type an address, get six digits, you're in — and if the address is new that same step creates the account. Passwords move one layer down ("Use a password instead") rather than away.

Per client

  • Web login page — code-first. The code step is revealed only after a code actually went out; the confirmation names the address; editing the address rewinds a step (an outstanding code belongs to the old one); a burned or expired code returns to "send one" instead of leaving a dead field. LOGIN_HTML now joins the inline-<script> syntax guard (it wasn't covered).
  • iOS — one form serves both Settings and onboarding, so they can't drift. SignInCodeError carries the server's typed reason: the status alone can't tell "a code just went out" from "too many today", and those need different words.
  • Mac menu-bar app — "Email me a code", then whichever credential is filled signs in (code first, else password).
  • CLIlisa login mails a code by default; --password keeps the old path. Wrong digits retry in place while the code is still live (the server burns it after five, so three tries is safe).

Two fixes found while testing the CLI end-to-end

Both pre-existing, both hit immediately once there was a second prompt:

  1. One readline interface per command, not per prompt. A fresh interface drops whatever the previous reader buffered, so every prompt after the first read nothing as soon as stdin wasn't an interactive terminal.
  2. An unreachable server is a typed result, not an exception. The blanket catch around the login helper reported every error — including bugs — as "could not reach", which is how the first version of this hid a real failure from me.

Also documents login / logout / billing in lisa --help, which never listed them.

Verification

  • Web: driven in a browser against a stub server — happy path (code → signed in), wrong code (errors in place, stays on the step), daily cap (errors, stays on email step), password toggle both ways, and editing the address rewinding the step. No console errors. Screenshots in the thread below.
  • CLI: driven end-to-end against a stub — happy path stores the session and prints the billing summary, wrong-then-right code retries in place, unreachable server reports honestly.
  • iOS: full swiftc -typecheck against the iOS 17 simulator SDK with LISA_ENABLE_SIWA (xcodegen isn't installed here, so this stands in for a project build; I confirmed the check does catch injected errors rather than passing vacuously).
  • Mac: swift build clean.
  • npm test1173 green, typecheck clean.

🤖 Generated with Claude Code

The four clients for the A1 endpoints. Everywhere the shape is the same:
type an address, get six digits, you're in — and if the address is new
that same step creates the account, because reading the mail is the proof.
Passwords move one layer down ("Use a password instead") rather than away.

- web login page: code-first, with the step revealed only after a code is
  actually sent, the confirmation naming the address, and editing the
  address rewinding a step (an outstanding code belongs to the old one).
  A burned or expired code returns to "send one" instead of leaving a
  dead field. LOGIN_HTML joins the inline-<script> syntax guard.
- iOS: one form serves both Settings and onboarding, so they can't drift.
  SignInCodeError carries the server's typed reason — status alone can't
  tell "a code just went out" from "too many today", and those need
  different words.
- Mac menu-bar app: "Email me a code", then whichever credential is
  filled signs in (code first, else password).
- CLI: `lisa login` mails a code by default; `--password` keeps the old
  path. Wrong digits retry in place while the code is still live.

Two fixes found while testing the CLI end-to-end:
- one readline interface per command instead of one per prompt. A fresh
  interface drops what the previous reader buffered, so every prompt
  after the first read nothing as soon as stdin wasn't a terminal.
- an unreachable server is now a typed `unreachable` result rather than
  an exception caught by a blanket handler that reported every error —
  including bugs — as "could not reach".

Also documents login/logout/billing in `lisa --help`, which never listed
them.

Plan: docs/PLAN_AUTH_OTP_GOOGLE_v1.0.md (A2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@oratis

oratis commented Jul 24, 2026

Copy link
Copy Markdown
Owner Author

Reviewed as security-critical client auth. No blockers — ready to merge.

Verified good

  • Token storage is secure on every platform: iOS → Keychain (TokenStore.save, kSecAttrAccessibleAfterFirstUnlock); Mac config.env and CLI saveConfigEnv both 0o600. No token ever appears in a URL or log (CLI prints email (uid) only).
  • No enumeration oracle: OTP request/verify is register-or-login unified, so it can't be used to probe which addresses have accounts. The only existence-revealing message (email_taken) is on the untouched password/register path.
  • Web is XSS-safe: say() writes textContent, the reflected email is concatenated into textContent, and there's no ${/backtick in the template — now guarded by the new LOGIN_HTML syntax test.
  • CLI fixes are real: the single shared readline interface (closed in finally) and the typed unreachable result (vs the old blanket catch that mislabeled bugs as network errors) are both correct; failure returns before cmdBilling.

Non-blocking note

  • OTP code / password / session travel in cleartext if a client is pointed at an explicit http:// base. parseCloudBase upgrades bare hosts to https and the Secure cookie is cloud-only, so this only affects deliberately-http self-hosted LAN — acceptable, matching the existing pairing model. Worth a one-line doc caveat.

Nice touch disabling the buttons during an in-flight request — it doubles as double-submit / attempt-burn protection.

@oratis
oratis merged commit b028752 into feat/auth-google-server Jul 24, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant