Skip to content

Open the identity on a new device by signing in to the account - #1856

Closed
joepio wants to merge 11 commits into
developfrom
claude/project-thread-1oik3x
Closed

joepio wants to merge 11 commits into
developfrom
claude/project-thread-1oik3x

Conversation

@joepio

@joepio joepio commented Sep 26, 2026

Copy link
Copy Markdown
Member

Requested by Joep · project thread

Related Issues

Related: #277 (sign in with an existing account such as Google). Needs ontola/atomic-saas#164 for the endpoints; without them nothing changes.

Checklist

  • Add changelog entry linking to issue, describe API changes
  • Add or update tests if needed
  • Update docs if needed

What this does

Before: on a new device, a hosted account holder signs in and is then asked for their passkey or recovery code to open their identity. New accounts must register a passkey and save a recovery code during onboarding.

After: when the account service offers assisted recovery, signing in is enough.

  • A new account's backup gets an atomic-assisted wrapper only, so onboarding goes straight from the profile name to the workspace: no passkey prompt, no recovery code.
  • The sign-in and restore steps unlock the backup by themselves when the signed-in account owns it ("Unlocking you@example.com…"). A sign-in older than 30 minutes is asked to sign in again.
  • Backups made before this get the wrapper in the background the next time a passkey or recovery code opens them.
  • Both steps offer Continue with Google when the account service has it, going straight to Google and back to the app.

How

The wrapper is one more envelope-v2 wrapper next to webauthn-prf and recovery-code. The browser asks POST /api/recovery-secret/assisted-key for the key-encryption key for a salt and does the AES-GCM wrap and unwrap itself, exactly as with a passkey's PRF output, so the DEK and the agent secret never leave the browser. GET /api/auth/providers says whether the service offers it (assisted_recovery) and Google (google). New code: helpers/managed/accountProviders.ts, the assisted functions in recovery.ts, GoogleSignInButton.tsx, and an onBackupWithAccount prop on NewIdentitySection.

Tests

  • recovery-assisted.test.ts: a backup built for the account opens with the account alone and nothing secret is sent; an old sign-in gets FreshSignInRequiredError.
  • Existing managed, getting-started and component suites pass (353 tests), plus typecheck and pnpm run -r lint.
  • Ran the real stack locally (account service with a key file, portal, app, node): a new account went straight to its workspace with only an atomic-assisted wrapper, then a second browser that only signed in by email link was opened as the same agent and landed in the workspace. Screenshots in a comment.
  • Not run end to end: the background upgrade of an existing passkey or code backup.

Generated by Claude Code

When the account service offers assisted recovery, the backup gets an
atomic-assisted wrapper whose key the service hands to a recently signed-in
account (the DEK and the secret stay in the browser). So:

- A new account's backup is wrapped by the account alone: onboarding asks
  for no passkey and shows no recovery code.
- The sign-in and restore steps unlock it by themselves after a recent
  sign-in; an old sign-in is asked to sign in again.
- Backups made before this get the wrapper the next time a passkey or
  recovery code opens them.
- Both steps offer "Continue with Google", straight back to the app.

Without the endpoints (a standalone server), nothing changes.
@joepio joepio self-assigned this Sep 26, 2026

joepio commented Sep 26, 2026

Copy link
Copy Markdown
Member Author

Screenshots from a local stack (saas with a key file, portal, this branch's app, a node): a new account going straight to its workspace with an atomic-assisted wrapper stored, a second browser with no local identity on the sign-in step, and that browser opened as the same agent after only a magic-link sign-in. They are kept out of the repo and posted in the project thread, under google-signin/assisted/ in the project files. Needs ontola/atomic-saas#164 deployed first; without it /api/auth/providers reports nothing and the app behaves as before.


Generated by Claude Code

The app's sign-in step offered Google and an agent secret, the portal offered
Google, passkey and an email link, and its homepage panel had no passkey.
Every account sign-in now renders AccountSignIn from @tomic/service-ui, so the
options and their order are the same everywhere. The app does the passkey and
email-link sign-in itself against the account service; builds without an
account service show none of it and send no requests for it.

Also names assisted recovery on the account page instead of falling back to
"your recovery password".

joepio commented Sep 26, 2026

Copy link
Copy Markdown
Member Author

New commit 42de79a: every account sign-in (the portal's sign-in page and homepage panel, the app's sign-in and restore steps) now renders one AccountSignIn component from @tomic/service-ui: Google (when configured), passkey, email link, in that order. The app runs the passkey and email-link sign-in itself against the account service. Builds without an account service (FOSS release, self-hosted) show none of it and make no requests for it; a unit test covers that.

Tested on a local stack: a new device opened the account's identity from the app's sign-in step with only an email link, and again with only a passkey. Screenshots (portal sign-in, homepage panel, app sign-in in light, dark and phone widths) are kept out of the repo and posted in the project thread, under google-signin/same-options/ in the project files. The portal side is ontola/atomic-saas#164 (commit b71b04d).


Generated by Claude Code

An app that cannot hold the account cookie showed a device-link code instead
of the sign-in options. It now shows the same AccountSignIn; each option opens
the portal in the system browser at /link with that option (via=google,
passkey or email) and this device's code, and the app continues once the code
is approved there.
AccountSignIn gets a "How safe is each option?" link that opens a dialog:
what a passkey, Google and an email link each trust, who learns what, and what
signing in opens. With assisted recovery on it says plainly that Atomic keeps
a key that, with its database, can open the identity backup.
Account recovery gets a 'Let Atomic help me recover' switch. Off removes
the assisted wrapper, so only a passkey or recovery code opens the identity,
and the app adds none in the background. A new assisted wrapper records the
service key id; when an unlock says the key was rotated, the app re-wraps
with the current key. The app shows an account's address and keeps the
account id for backups and bindings.
…count opens

With account ids the app shows the address from /api/me in the profile, the
share dialog and onboarding. A backup that only signing in opens can now get
a passkey from the recovery card: a recent sign-in reaches the DEK, as it
does for unlocking.
The onboarding flow now shows the account's address; without it in the session mock the invite test fell through to signed out.
The sign-in step now shows the same account options as the portal, each finished in the system browser, so the separate "Forgot it? Restore from" button is gone there. The spec checks for the options instead.

joepio commented Sep 26, 2026

Copy link
Copy Markdown
Member Author

Main (Mancave) / CI on 3e7861c is red, but this PR is not the cause. Both attempts of run 36235657226 stopped three seconds into the Dagger step with start engine: failed to start container, before any build or test ran. That is the Docker daemon on the Mancave runner. I've used the one re-run, and it failed the same way.

On this commit the downstream atomic-saas check/clippy/test job is green. Locally, the e2e specs that failed on the earlier head pass: native-account-restore (fixed in 3e7861c), sign-in-without-data, search, second-device-load and canvas-live-update. Once Docker on Mancave is back, this needs a fresh dispatch of main.yml.


Generated by Claude Code

joepio commented Sep 27, 2026

Copy link
Copy Markdown
Member Author

Run 4712 (Main (Mancave) / CI on Mancave-2) failed with Dagger exit code 135, a SIGBUS, about 10 minutes into the Dagger step. The job left no downloadable log (404), so I could not read the failing step itself.

I don't think the cause is this PR:

  • The develop run 4707 on the Mancave runner died in the same window, around 10:10 UTC.
  • That run's Dagger status on develop never left "pending".
  • Two runners dying together points at the shared Dagger engine or Docker on the Mancave host, not at a test. Mancave had just come back from Docker being down this morning.
  • The same head passed its unit tests and the e2e specs it touches locally.

I don't know of a fix to port. Once the run finishes (Downstream is still queued), I'll re-run the failed job once. GitHub refuses a re-run while the run is still in progress.


Generated by Claude Code

joepio commented Sep 27, 2026

Copy link
Copy Markdown
Member Author

The re-run of run 4712 failed within seconds with start engine: failed to run command [docker version]. Docker is not running on Mancave, so this is no test verdict, and there is nothing in this PR to fix. I've spent the one re-run. I'll run CI again once Docker is back on the host.


Generated by Claude Code

…roviders

Provider buttons now read just the provider's name, side by side in one
row, instead of "Continue with ..." stacked on top.
Side by side they need about 330px; on a phone the names would clip. Icons
keep their size inside a narrow button.
joepio added a commit that referenced this pull request Oct 1, 2026
…nt, Google sign-in (#1946)

Lands #1935 (server clock skew), #1936 (vault restore over 64 objects), #1944 (connect dialog), #1945 (twin agent resource) and #1856 (sign in to open the identity, pairs with atomic-saas #164), tested together on Mancave.

joepio commented Oct 1, 2026

Copy link
Copy Markdown
Member Author

Landed on develop through the batch #1946 (squash f2a07aa), tested together on Mancave. Closing this PR since its commits are in that batch. atomic-saas #164 is next.


Generated by Claude Code

@joepio joepio closed this Oct 1, 2026
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