Skip to content

Keep browser logins for 30 days across Core restarts - #1727

Merged
frahlg merged 7 commits into
masterfrom
codex/persistent-lan-login-20261018
Oct 18, 2026
Merged

frahlg merged 7 commits into
masterfrom
codex/persistent-lan-login-20261018

Conversation

@frahlg

@frahlg frahlg commented Oct 18, 2026 •

Copy link
Copy Markdown
Member

Each Core update or restart currently drops the browser’s house-password login because the server keeps its sessions only in RAM. Keep the login for 30 days and restore valid sessions from state.db when Core starts.

Store a SHA-256 digest of the opaque token, its expiry and a binding to the password version. Commit the session before sending its cookie. Logout revokes the durable row; a password change clears sessions, and the password binding also rejects old rows after an interrupted cleanup. Expired rows cannot restore and new logins prune expired entries. The cookie keeps HttpOnly, SameSite=Strict and its existing LAN HTTP behavior. A review of the stacked car-page base also found that enabling the house password gave a correctly authenticated public API-token caller viewer scopes. Keep that explicit token caller an owner; invalid tokens still get viewer scopes. Its regression fails before and passes after the fix.

Depends on #1694. This branch preserves its exact db790a0a car-page work because it owns the API constructor file. Current master, including #1723, is merged into this branch. Merge #1694 first; its commits appear in this diff until then. This PR adds the session change in eight files and does not install anything on the box.

Validation:

  • The restart and hard-coded 30-day regressions fail on the unchanged API and pass after the fix.
  • SQLite close/reopen, token-digest-only storage, logout, expiry, password changes and a password change interrupted before cleanup pass.
  • Targeted API/state race tests and full make verify pass locally. Full verification also passes after integrating current master.

Fredrik explicitly requested durable browser login with a default of about a month. No beta is published by this PR; the next box beta is being coordinated with the MCP work.

A stalled unauthenticated login body no longer holds the shared Settings write lock. A regression runs an owner settings write alongside that body; it fails before the fix and passes after. Login admission and password/session persistence remain serialized. Full make verify and targeted auth/state race checks pass.

frahlg and others added 5 commits October 17, 2026 11:26
The car ops were cmd frames only, so the box page, which speaks HTTP,
could list cars but not keep them or answer which car is on a charger.
Each op now has a route that hands the same arguments to the same
runtime and answers with the same code and reason:

- POST /api/cars is car.add, PATCH /api/cars/{id} car.update and
  DELETE /api/cars/{id} car.remove. Configure, no step-up, and only a
  caller with ftw.cars.write.
- POST /api/loadpoints/{id}/car is session.car.set, the tap, with
  {session, car_id}. Actuate, Via the op, so a phone is told to send it.

appproto.CarRefusal is the refusal table both use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The overview gets a card per car, plugged in or not: its level with
where it came from and how old it is, an estimate marked with ≈, its
charge limit, its goal and the charger it is on, with how the box knows.
The level ages between reads, and a list that stopped updating says
since when. While a charger asks which car it is, its notice offers one
button per car and None of these, and the cars it may be say "On
garage?" rather than "Not plugged in". A car the box named itself can
be corrected behind "Not this car?". The buttons are rebuilt only when
what they show changes, so a poll never takes one from under a tap.

Settings gets a Cars tab: each car with its VIN, level sources and
charger tags; rename, battery size, add and remove through the car
routes. Adding a VIN a car already has is stopped on the page: the box
would take that as the app adopting the car it made, and quietly
rename it.

The EV modal stays as it was; a car on a charger opens it with "Check
charging".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…home

From the review of #1694:

- GET /api/cars gives VINs, charge tags and sources only to a request
  that has proved itself as a protected one must. The LAN listener names
  every caller an owner while the house password is off, so a box
  published under a public name without FTW_API_TOKEN handed them to
  anyone. Others get the viewer shape and redacted: true.
- POST/PATCH/DELETE /api/cars are Local: a phone keeps sending the ops as
  cmd frames, with their id and expiry.
- The routes read their bodies with the ops' own argument checks
  (appproto.CarAddArgs, CarUpdateArgs): a negative size is refused, and a
  null identifiers clears the list as car.update does.
- The card dates the chargers' read apart from the cars': an old read, or
  a charger gone quiet, says "Charger status unknown" rather than the last
  place. The charge limit shows only beside the car's own reading.
- A tap gives up after 30 s, and a 401 says the house password is needed.
- The Cars tab says hidden, not "Not set", when the box left a field out.
- docs/cars.md names the routes, the page and who sees the VIN.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-18T14:41:43.701159Z f1bf6e1 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f1bf6e1a28

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread go/internal/api/api.go
Comment on lines +3862 to +3864
caller, ok := apiauth.FromRequest(r)
full := ok && caller.Scopes.Has(protocol.ScopeCarsRead) &&
(caller.Kind != apiauth.KindLAN || remoteProved(r, s.deps.MutationPolicy))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor the API token when LAN auth is enabled

When api.lan_auth is enabled and a client connects directly from a non-local address through a public hostname, even a valid FTW_API_TOKEN receives the redacted list. resolveLANSecret skips non-local clients, so decideLANCaller assigns the viewer scopes; this scope check then fails before remoteProved can recognize the valid token. This contradicts the documented public-token flow and prevents authenticated remote owners from seeing VINs, sources, and charge tags.

Useful? React with 👍 / 👎.

@frahlg
frahlg merged commit fdf8f93 into master Oct 18, 2026
22 checks 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