Skip to content

Batch 2026-09-30: clock skew, vault restore, connect dialog, twin agent, Google sign-in - #1946

Merged
joepio merged 28 commits into
developfrom
trekmeester/combined-20260930
Oct 1, 2026
Merged

joepio merged 28 commits into
developfrom
trekmeester/combined-20260930

Conversation

@joepio

@joepio joepio commented Sep 30, 2026

Copy link
Copy Markdown
Member

Before: #1935, #1936, #1944, #1945 and #1856 each waited for their own full Mancave run, several hours in a row.

After: one run tests them together, and squash-merging this batch lands them all.

Includes #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). #1866 had catalog conflicts and stays out.

How: each PR head merged onto develop 5ae2786; CHANGELOG conflict between #1944 and #1856 resolved by keeping both entries.


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.
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".
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.
…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.
download-urls signs at most 64 objects per request, and restoreDrive asked
for every object at once, so a drive with a longer history could not be
restored on a new device.
An app that calls store.proxy.connect used to get a bar above it that
read "This app wants to use your Pets account through <proxy origin>.
The proxy keeps the connection under your account; this app may use it
until you revoke that." It now gets a dialog titled "Connect <platform>"
that says which app wants which account, that you approve it on the next
page (naming the proxy's host, which is the address to expect there),
and that the app never sees your password.

When you already connected that platform, the dialog lists each such
connection with when it was connected and which apps use it, each with
"Use this account"; "Connect another account" still goes to the proxy.
The lookup settles either way, so the dialog shows a placeholder while
it runs instead of changing its buttons under your cursor.

A failure (no app identity, proxy unreachable) is shown in the dialog so
you can retry; the app hears `cancelled` if you close it. Closing the
dialog while "Connect" is still preparing no longer navigates away
afterwards. Dialog gains an optional `labelledBy`, so the dialog is
named by its title for screen readers and tests.
…its on

Seen in screenshots of the dialog against the Pets demo:

- On a 390 px phone "Cancel" broke into "Cance" / "l" next to "Connect
  another account". That button is now "Use another account", which sits
  beside "Use this account" and fits, and the footer wraps its buttons
  onto a second line instead of squeezing them.
- The placeholder shown while looking for existing connections was 0 px
  high, so the dialog just had a gap. It is now a line-sized pulse.
- A proxy error ran out of its box, because it names the request path.
  It now wraps, and leads with what failed ("Could not use this
  account." or "Could not start connecting.") above the proxy's words.
The dialog's content already pads its bottom, so the list's own margin
left an empty band above the buttons.
The same public key has been written padded, unpadded, standard and
URL-safe. A profile stored under one spelling was invisible to the
canonical unpadded subject, which got an empty synthesized agent instead,
so one person showed up as two resources.
The React Compiler test refused GettingStartedFlow.tsx with 'PruneHoistedContexts: Unexpected hoisted function' once the assisted-unlock effect called handleSignInWithSecret, declared further down. The function now sits above the effect, next to accountSignIn, and moves nothing else.
@joepio
joepio marked this pull request as ready for review October 1, 2026 03:09
@joepio
joepio merged commit f2a07aa into develop Oct 1, 2026
8 checks passed
@joepio
joepio deleted the trekmeester/combined-20260930 branch October 1, 2026 03:09
joepio added a commit that referenced this pull request Oct 1, 2026
atomic-saas CI builds the atomic-server branch of the same name. This branch
was stuck before the batch and the audit fixes, so saas #164 ran against an app
that reads /api/me far more than develop's does. Same tree as develop.
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