Skip to content

feat (put-1022 put-1023 put-1024): puter.js reauth handling + GUI v2 modal + silent v1 migration#3157

Merged
Salazareo merged 3 commits into
mainfrom
DS/put-1022-put-1023-put-1024
May 27, 2026
Merged

feat (put-1022 put-1023 put-1024): puter.js reauth handling + GUI v2 modal + silent v1 migration#3157
Salazareo merged 3 commits into
mainfrom
DS/put-1022-put-1023-put-1024

Conversation

@Salazareo
Copy link
Copy Markdown
Member

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

…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>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 26, 2026

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 71.52%
🟰 ±0%
13401 / 18735
🔵 Statements 69.95%
🟰 ±0%
14154 / 20233
🔵 Functions 70.38%
🟰 ±0%
2217 / 3150
🔵 Branches 59.63%
🟰 ±0%
9002 / 15095
File CoverageNo changed files found.
Generated in workflow #240 for commit 9472c65 by the Vitest Coverage Report Action

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.tokenputer.auth.token.v2) and a silent v1→v2 migration via /auth/migrate-token.
  • GUI: replace hard logout-on-401 with centralized reauth_required handling (modal + login relaunch), migrate GUI token storage (auth_tokenauth_token_v2), and handle websocket handshake reauth_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 thread src/puter-js/src/index.js Outdated
Comment on lines +843 to +849
try {
globalThis.parent?.postMessage?.({
msg: 'reauth_required',
appInstanceID: this.appInstanceID,
reason,
auth_id,
}, '*');
Comment thread src/puter-js/src/index.js
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 thread src/puter-js/src/index.js
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);
Salazareo and others added 2 commits May 26, 2026 16:32
…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>
@Salazareo Salazareo merged commit 3ae076b into main May 27, 2026
4 checks passed
@Salazareo Salazareo deleted the DS/put-1022-put-1023-put-1024 branch May 27, 2026 18:45
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.

2 participants