feat (put-1022 put-1023 put-1024): puter.js reauth handling + GUI v2 modal + silent v1 migration#3157
Merged
Merged
Conversation
…modal + silent v1 migration
PUT-1022 (PJS-1): puter.js handles 401 reauth_required
- Single apiCall wrapper intercepts 401 { code: "reauth_required" }
- Clears local token, emits puter.auth.reauth_required { reason, auth_id }
- Queues in-flight requests, re-issues after re-auth completes
- web/app: opens puter.com login popup forwarding auth_id
- gui: no-op (handled by GUI-1 modal)
- workers: surfaces as structured exception
- All token writes go through setAuthToken() (future-proofed for PJS-2)
PUT-1023 (GUI-1): web GUI reauth modal + v2 token storage
- Detects 401 reauth_required across http + websocket connect paths
- Soft modal preserves URL/window state; auth_id forwarded into login form
- Storage key migrated: auth_token -> auth_token_v2
- Cookie cleared on logout: puter_token (legacy) cleared, backend writes puter_token_v2
- Cross-tab propagation via localStorage storage events
- WebSocket revocation surfaces as same modal (no silent failure)
PUT-1024 (PJS-2): puter.js storage versioning + silent v1->v2 migration
- New storage key puter.auth.token.v2
- On SDK init: prefer v2 key; else legacy v1 -> silent POST /auth/migrate-token (SDK-1)
- On success: store v2, clear v1. On failure: fall back to PJS-1 reauth flow
- URL-param tokens (?puter.auth.token=, ?auth_token=) also run through silent migration
- Logout clears both keys; setAuthToken writes v2 only
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
Coverage Report
File CoverageNo changed files found. |
Contributor
There was a problem hiding this comment.
Pull request overview
This PR adds coordinated handling for backend 401 { code: "reauth_required" } responses across the puter.js SDK and the web GUI, and migrates token storage from legacy v1 keys to new v2 keys with a best-effort silent migration path.
Changes:
- puter.js: capture request metadata to replay XHRs after an interactive reauth flow completes; add a reauth coordinator on the SDK instance.
- puter.js: introduce token storage versioning (
puter.auth.token→puter.auth.token.v2) and a silent v1→v2 migration via/auth/migrate-token. - GUI: replace hard logout-on-401 with centralized
reauth_requiredhandling (modal + login relaunch), migrate GUI token storage (auth_token→auth_token_v2), and handle websocket handshakereauth_required.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/puter-js/src/lib/utils.js | Captures XHR request shape and adds reauth-required interception + retry logic. |
| src/puter-js/src/index.js | Adds reauth coordinator (triggerReauth), token storage v2, and silent migration helper. |
| src/gui/src/UI/UIWindowSearch.js | Routes 401 handling through centralized window.handle401. |
| src/gui/src/UI/UIWindowRecoverPassword.js | Routes 401 handling through centralized window.handle401. |
| src/gui/src/UI/UIWindowMyWebsites.js | Routes 401 handling through centralized window.handle401. |
| src/gui/src/UI/UIWindowLogin.js | Forwards auth_id / reason in login payload for reauth-triggered logins. |
| src/gui/src/UI/UIWindowEmailConfirmationRequired.js | Routes 401 handling through centralized window.handle401. |
| src/gui/src/UI/UIWindowDesktopBGSettings.js | Routes 401 handling through centralized window.handle401. |
| src/gui/src/UI/UIWindow.js | Routes 401 handling through centralized window.handle401. |
| src/gui/src/UI/UIItem.js | Routes 401 handling through centralized window.handle401. |
| src/gui/src/UI/UIDesktop.js | Handles socket.io connect_error reauth_required signals and routes 401 via window.handle401. |
| src/gui/src/IPC.js | Routes 401 handling through centralized window.handle401. |
| src/gui/src/initgui.js | Writes v2 GUI token key and removes legacy key during bootstrap token fetch. |
| src/gui/src/helpers/open_item.js | Routes 401 handling through centralized window.handle401. |
| src/gui/src/helpers/generate_file_context_menu.js | Routes 401 handling through centralized window.handle401. |
| src/gui/src/helpers.js | Introduces centralized GUI reauth modal flow, v2 token key usage, global ajax 401 interceptor, and cross-tab propagation. |
| src/gui/src/globals.js | Prefers v2 token key on boot with fallback to legacy v1 key. |
Comments suppressed due to low confidence (1)
src/gui/src/initgui.js:405
- In the get-gui-token bootstrap, window.auth_token is assigned before computing tokenChanged, so
const tokenChanged = token !== window.auth_token;will always be false. As a result, the follow-up whoami/update_auth_data block will never run even when the token actually changed.
Suggested fix: compare against the previous token value (store it before overwriting window.auth_token), or compute tokenChanged using the prior localStorage value.
if ( r.ok ) {
const { token } = await r.json();
window.auth_token = token;
// PUT-1023 (GUI-1) — write the v2 key; drop legacy v1 key.
localStorage.setItem(window.AUTH_TOKEN_KEY_V2 || 'auth_token_v2', token);
try { localStorage.removeItem(window.AUTH_TOKEN_KEY_V1 || 'auth_token'); } catch ( e ) { /* ignore */ }
if ( typeof puter !== 'undefined' ) puter.setAuthToken(token, window.api_origin);
const tokenChanged = token !== window.auth_token;
if ( tokenChanged ) {
// This will update the list of logged in users and set the current one
try {
const whoami = await puter.os.user({ query: 'icon_size=64' });
if ( whoami ) await window.update_auth_data(token, whoami);
} catch (e) {
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
592
to
+606
| // HTTP Error - unauthorized | ||
| if ( response.target.status === 401 || resp?.code === 'token_auth_failed' ) { | ||
| // PUT-1022 (PJS-1) — v2 reauth signal. Same shape as the | ||
| // non-driver path in handle_resp above; the call is replayed | ||
| // with the fresh token after triggerReauth resolves. | ||
| if ( resp?.code === 'reauth_required' ) { | ||
| try { | ||
| await puter.triggerReauth({ | ||
| reason: resp.reason, | ||
| auth_id: resp.auth_id, | ||
| }); | ||
| if ( replayXhrAfterReauth(response, success_cb, error_cb, resolve_func, reject_func) ) { | ||
| return; | ||
| } | ||
| } catch ( e ) { |
Comment on lines
+104
to
+114
| // (PUT-1022 PJS-1). The body is captured below by intercepting send(). | ||
| xhr._puterReq = { | ||
| endpoint, | ||
| APIOrigin, | ||
| method, | ||
| contentType, | ||
| responseType, | ||
| }; | ||
| const origSend = xhr.send.bind(xhr); | ||
| xhr.send = function (body) { | ||
| xhr._puterReq.body = body; |
Comment on lines
+843
to
+849
| try { | ||
| globalThis.parent?.postMessage?.({ | ||
| msg: 'reauth_required', | ||
| appInstanceID: this.appInstanceID, | ||
| reason, | ||
| auth_id, | ||
| }, '*'); |
Comment on lines
+855
to
+863
| await new Promise((resolve, reject) => { | ||
| const onToken = (event) => { | ||
| if ( event.origin !== this.defaultGUIOrigin ) return; | ||
| if ( event.data?.msg !== 'puter.token' ) return; | ||
| globalThis.removeEventListener('message', onToken); | ||
| resolve(); | ||
| }; | ||
| globalThis.addEventListener?.('message', onToken); | ||
| // Give the user a generous window to re-auth. |
Comment on lines
+879
to
+899
| _emitReauthEvent = function ({ reason, auth_id }) { | ||
| try { | ||
| const handlers = this.eventHandlers?.['auth.reauth_required']; | ||
| if ( Array.isArray(handlers) ) { | ||
| for ( const h of handlers ) { | ||
| try { h({ reason, auth_id }); } catch ( e ) { /* swallow per-handler errors */ } | ||
| } | ||
| } | ||
| } catch ( e ) { | ||
| // Never let event delivery break the reauth flow itself. | ||
| } | ||
| }; | ||
|
|
||
| /** | ||
| * Register a listener for SDK events. Used by host apps to react | ||
| * to `auth.reauth_required` (PUT-1022 PJS-1). | ||
| */ | ||
| on = function (eventName, handler) { | ||
| if ( ! this.eventHandlers[eventName] ) this.eventHandlers[eventName] = []; | ||
| this.eventHandlers[eventName].push(handler); | ||
| return () => this.off(eventName, handler); |
…tack, richer rows Three issues surfaced during local testing of the manage-sessions UI: 1. The session row representing the caller's own cookie had a Revoke button that, if used, left the client in an ambiguous identity state (backend now rejects it too — see put-1019 backend fix). The button is now omitted entirely when session.current=true; /logout remains the right path for ending the active session. 2. The confirm-revoke prompt rendered behind the manage-sessions window because both share the dominant z-index pool. UIAlert calls now pass parent_uuid (the manage-sessions window's data-element_uuid) plus stay_on_top, so the prompt stacks above its parent. 3. Each row only showed the bare uuid. Now renders: title (app title for app sessions, label / "Browser session" / "Access token" otherwise), app icon when applicable, kind / current badges, created / last-active / expires (timeago, with absolute on hover), and last_ip. App metadata is joined server-side per the backend listSessions change. Adds en.js strings for the new labels (ui_session_*). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
driverCall_ replay (G1): on 401 reauth_required, re-enter driverCall_ instead of calling replayXhrAfterReauth. The generic helper wires the retried XHR through setupXhrEventHandlers which resolves with the parsed response and silently skips driverCall_'s streaming detection, usage-limit / email-confirmation handling, settings.transform, and resp.result unwrapping — i.e. it would change the driver call API contract on retry. One-shot via settings._reauthReplayed so a fresh-token rejection bubbles up instead of looping. responseType drift (G2): driverCall_ sets xhr.responseType from settings AFTER initXhr (which captured the stale value into xhr._puterReq). Mirror the mutation onto _puterReq so any replay path builds the retry with the live config rather than the snapshot. targetOrigin lockdown (G3): triggerReauth's parent.postMessage now targets this.defaultGUIOrigin instead of '*'. The payload carries reauth metadata + auth_id and is only meaningful to the GUI; '*' would leak the signal to whatever frame happened to be embedding us. event.source pinning (G4): the reauth wait listener also matches event.source against globalThis.parent, not just event.origin — origin alone admits any same-origin frame on the GUI domain. event name alignment (G5): the SDK now emits and listens for 'puter.auth.reauth_required' (matches the documented name in the PR / commit description). The old 'auth.reauth_required' key isn't yet consumed externally so this is a safe rename. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PUT-1022 (PJS-1): puter.js handles 401 reauth_required
PUT-1023 (GUI-1): web GUI reauth modal + v2 token storage
PUT-1024 (PJS-2): puter.js storage versioning + silent v1->v2 migration