mirror of
https://github.com/HeyPuter/puter.git
synced 2026-10-03 10:27:48 +00:00
feat: better verification step handling (#3717)
This commit is contained in:
@@ -433,6 +433,42 @@ async function resolvePermission(permission) {
|
||||
}
|
||||
}
|
||||
|
||||
// The 403 account-verification gates the hosting GUI can walk a user through.
|
||||
const VERIFICATION_GATE_CODES = new Set([
|
||||
'email_confirmation_required',
|
||||
'phone_verification_required',
|
||||
'card_verification_required',
|
||||
]);
|
||||
|
||||
// Single-flighted verification prompt: concurrent gated requests share one
|
||||
// GUI dialog rather than stacking windows.
|
||||
let pendingVerificationGate = null;
|
||||
|
||||
/**
|
||||
* Drive the hosting GUI's verification flow for a 403 `*_required` gate code.
|
||||
* Only apps hosted by the Puter GUI can prompt; every other environment
|
||||
* resolves unverified and the rejection reaches the caller unchanged.
|
||||
*
|
||||
* @param {string} code - The gate's error code.
|
||||
* @returns {Promise<{ verified: boolean }>}
|
||||
*/
|
||||
async function resolveVerificationGate(code) {
|
||||
if (globalThis.puter?.env !== 'app') return { verified: false };
|
||||
if (!pendingVerificationGate) {
|
||||
pendingVerificationGate = (async () => {
|
||||
try {
|
||||
const verified = await puter.ui.requestVerificationGate(code);
|
||||
return { verified: verified === true };
|
||||
} catch (e) {
|
||||
return { verified: false };
|
||||
} finally {
|
||||
pendingVerificationGate = null;
|
||||
}
|
||||
})();
|
||||
}
|
||||
return pendingVerificationGate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send one attempt. Resolves with a terminal outcome: { streamed: true, xhr,
|
||||
* lineStream } — NDJSON, resolved at HEADERS_RECEIVED { xhr, status } —
|
||||
@@ -529,8 +565,9 @@ function sendOnce(spec) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Classify a completed attempt into a retry decision. Reauth and permission are
|
||||
* one-shot (tracked in `ctx.done`) and apply to any request; transient backoff
|
||||
* Classify a completed attempt into a retry decision. Reauth, permission, and
|
||||
* the phone-verification gate are one-shot (tracked in `ctx.done`) and apply
|
||||
* to any request; transient backoff
|
||||
* applies only to `ctx.retrySafe` requests and honors the autoRetry kill
|
||||
* switch. Memoizes the parsed body on `outcome.parsed` and stashes any reauth
|
||||
* error on `outcome.reauthError` for the shaper.
|
||||
@@ -582,6 +619,24 @@ async function classifyRetry(outcome, ctx) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// account verification gate (403) — one-shot per gate, any method, no
|
||||
// backoff. The gate rejects in middleware before the handler runs, so
|
||||
// replay is safe once the user clears it; a user behind several gates
|
||||
// clears them one replay at a time (email → phone → card).
|
||||
const gateCode = [parsed?.code, parsed?.error?.code].find((c) =>
|
||||
VERIFICATION_GATE_CODES.has(c),
|
||||
);
|
||||
if (status === 403 && gateCode) {
|
||||
if (!ctx.done.has(gateCode)) {
|
||||
const res = await resolveVerificationGate(gateCode);
|
||||
if (res.verified) {
|
||||
ctx.done.add(gateCode);
|
||||
return { delayMs: 0 };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// gate rejection — any method, honors kill switch, fixed schedule.
|
||||
if (status === GATE_REJECT_STATUS) return gateRetry(ctx);
|
||||
|
||||
|
||||
@@ -1056,6 +1056,22 @@ export class UIModule extends EventListener {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Asks the desktop to walk the user through clearing an account
|
||||
* verification gate (a 403 `*_required` code — email confirmation, phone
|
||||
* verification, or card verification). Resolves `true` once the gate is
|
||||
* cleared, `false` otherwise.
|
||||
*
|
||||
* @internal
|
||||
* @param {string} code - The gate's error code.
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
requestVerificationGate (code) {
|
||||
return new Promise((resolve) => {
|
||||
this.#postMessageWithCallback('requestVerificationGate', resolve, { code });
|
||||
}).then((res) => res?.response === true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows an alert dialog, blocking the parent window until the user picks a
|
||||
* button. Resolves to that button's `value`, or its `label` when no value
|
||||
|
||||
Reference in New Issue
Block a user