@bounded-sh/client 0.0.24 → 0.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/oidc-auth.d.ts +38 -10
- package/dist/index.js +168 -70
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +168 -70
- package/dist/index.mjs.map +1 -1
- package/dist/{index.native-DcetyeOb.js → index.native-BfAzgVuY.js} +171 -73
- package/dist/index.native-BfAzgVuY.js.map +1 -0
- package/dist/{index.native-D1YQ-LU7.mjs → index.native-DEWlFCQV.mjs} +171 -73
- package/dist/index.native-DEWlFCQV.mjs.map +1 -0
- package/dist/index.native.js +1 -1
- package/dist/index.native.mjs +1 -1
- package/dist/{phantom-wallet-provider-DgBk6o3j.js → phantom-wallet-provider-3xKduOOY.js} +2 -2
- package/dist/{phantom-wallet-provider-DgBk6o3j.js.map → phantom-wallet-provider-3xKduOOY.js.map} +1 -1
- package/dist/{phantom-wallet-provider-3_IulJlh.mjs → phantom-wallet-provider-B3ztJGu2.mjs} +2 -2
- package/dist/{phantom-wallet-provider-3_IulJlh.mjs.map → phantom-wallet-provider-B3ztJGu2.mjs.map} +1 -1
- package/dist/platform.d.ts +8 -0
- package/dist/{privy-wallet-provider-DNvtYCsQ.js → privy-wallet-provider-3LnqlNI-.js} +3 -3
- package/dist/{privy-wallet-provider-DNvtYCsQ.js.map → privy-wallet-provider-3LnqlNI-.js.map} +1 -1
- package/dist/{privy-wallet-provider-DQtXFGFF.mjs → privy-wallet-provider-D2i_xwYW.mjs} +3 -3
- package/dist/{privy-wallet-provider-DQtXFGFF.mjs.map → privy-wallet-provider-D2i_xwYW.mjs.map} +1 -1
- package/dist/{solana-mobile-wallet-provider-CcnkDdgZ.js → solana-mobile-wallet-provider-D2nTRj-4.js} +2 -2
- package/dist/{solana-mobile-wallet-provider-CcnkDdgZ.js.map → solana-mobile-wallet-provider-D2nTRj-4.js.map} +1 -1
- package/dist/{solana-mobile-wallet-provider-DE_zCpKE.mjs → solana-mobile-wallet-provider-dOWiujmo.mjs} +2 -2
- package/dist/{solana-mobile-wallet-provider-DE_zCpKE.mjs.map → solana-mobile-wallet-provider-dOWiujmo.mjs.map} +1 -1
- package/package.json +10 -2
- package/dist/index.native-D1YQ-LU7.mjs.map +0 -1
- package/dist/index.native-DcetyeOb.js.map +0 -1
package/dist/auth/oidc-auth.d.ts
CHANGED
|
@@ -16,22 +16,50 @@ export interface OidcLoginOptions {
|
|
|
16
16
|
* when the issuer explicitly enables text OTP.
|
|
17
17
|
*/
|
|
18
18
|
provider?: HostedAuthMethod;
|
|
19
|
+
/**
|
|
20
|
+
* Standard OIDC `prompt`. Pass `'login'` to FORCE a fresh sign-in even if the
|
|
21
|
+
* issuer already has a session — so the user can choose a DIFFERENT account
|
|
22
|
+
* (e.g. retry with another email after an access denial) instead of being
|
|
23
|
+
* silently re-signed-in as the same one. `'select_account'` behaves the same.
|
|
24
|
+
*/
|
|
25
|
+
prompt?: 'login' | 'select_account' | 'consent' | 'none' | (string & {});
|
|
19
26
|
}
|
|
20
27
|
export interface PopupLoginOptions extends OidcLoginOptions {
|
|
21
28
|
width?: number;
|
|
22
29
|
height?: number;
|
|
23
30
|
}
|
|
24
|
-
/** Start login
|
|
25
|
-
*
|
|
26
|
-
|
|
31
|
+
/** Start hosted login (Google / Apple / email / hosted phone). Works on WEB and REACT NATIVE.
|
|
32
|
+
*
|
|
33
|
+
* WEB: navigates the whole page to the hosted Bounded login and resolves (void) as the page
|
|
34
|
+
* unloads; on return to `redirectUri?code=...` you call completeLoginFromRedirect().
|
|
35
|
+
*
|
|
36
|
+
* REACT NATIVE: opens the hosted page via expo-web-browser, awaits the https universal-link
|
|
37
|
+
* callback, runs the PKCE exchange INLINE, and resolves with the signed-in User. No
|
|
38
|
+
* completeLoginFromRedirect() call is needed on native. Requires `redirectUri` to be an
|
|
39
|
+
* https UNIVERSAL LINK whose origin the app owner registered in the app's allowedOrigins
|
|
40
|
+
* (custom `myapp://` schemes are rejected by the issuer). Needs the optional peer deps
|
|
41
|
+
* expo-web-browser + (expo-crypto or react-native-get-random-values). */
|
|
42
|
+
export declare function loginWithRedirect(opts: OidcLoginOptions): Promise<User | void>;
|
|
27
43
|
/** UPGRADE the current anonymous (guest) session by linking a SOCIAL/redirect login
|
|
28
|
-
* (e.g. Google) — Firebase `linkWithPopup`/redirect parity.
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
*
|
|
44
|
+
* (e.g. Google) — Firebase `linkWithPopup`/redirect parity. Carries the guest's signed
|
|
45
|
+
* id-token THROUGH the OIDC authorize request (as a single-use hint bound to the PKCE
|
|
46
|
+
* challenge), then runs the normal redirect login. The issuer completes the link during
|
|
47
|
+
* the standard code→token exchange: if the chosen account is brand-new the guest's
|
|
48
|
+
* `@user.id` is preserved (the returned id_token is minted under it), else the account
|
|
49
|
+
* stays separate (guest id discarded). Requires an active guest session.
|
|
50
|
+
*
|
|
51
|
+
* Works on WEB (navigates, finish with completeLoginFromRedirect) and REACT NATIVE
|
|
52
|
+
* (opens expo-web-browser and resolves with the upgraded User inline). */
|
|
53
|
+
export declare function linkWithRedirect(opts: OidcLoginOptions): Promise<User | void>;
|
|
54
|
+
/** Finish login on the redirect_uri page (WEB ONLY): exchange ?code= for a token (PKCE-verified),
|
|
55
|
+
* store the session, and return the User. Idempotent: returns null if there's no ?code= in the URL.
|
|
56
|
+
* When the redirect was a guest UPGRADE (linkWithRedirect) and the issuer adopted the guest
|
|
57
|
+
* id, the exchanged id_token already carries the preserved `@user.id`, so the returned User
|
|
58
|
+
* reflects it automatically (no extra round-trip).
|
|
59
|
+
*
|
|
60
|
+
* On REACT NATIVE this returns null and is a no-op: native login completes INLINE inside
|
|
61
|
+
* loginWithRedirect()/linkWithRedirect() (which resolve with the User), so there is no
|
|
62
|
+
* separate redirect page to finish from. Calling it on RN is harmless. */
|
|
35
63
|
export declare function completeLoginFromRedirect(): Promise<User | null>;
|
|
36
64
|
/** Popup variant: open the hosted login in a popup; resolves with the User once the popup's
|
|
37
65
|
* redirect_uri page posts the code back. The redirect_uri page must call completeLoginInPopup(). */
|
package/dist/index.js
CHANGED
|
@@ -91,6 +91,17 @@ function createWebPlatform() {
|
|
|
91
91
|
const hash = createHash('sha256').update(data).digest();
|
|
92
92
|
return hash.buffer.slice(hash.byteOffset, hash.byteOffset + hash.byteLength);
|
|
93
93
|
},
|
|
94
|
+
getRandomBytes(length) {
|
|
95
|
+
const out = new Uint8Array(length);
|
|
96
|
+
const g = typeof globalThis !== 'undefined' ? globalThis : undefined;
|
|
97
|
+
if (g && g.crypto && typeof g.crypto.getRandomValues === 'function') {
|
|
98
|
+
g.crypto.getRandomValues(out);
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
throw new Error('No secure random source available for PKCE. On React Native, either import ' +
|
|
102
|
+
"'react-native-get-random-values' once at your app entry, or pass getRandomBytes " +
|
|
103
|
+
'via setPlatform({ getRandomBytes }) (e.g. expo-crypto getRandomBytes).');
|
|
104
|
+
},
|
|
94
105
|
getUserAgent() {
|
|
95
106
|
if (hasWindow && typeof navigator !== 'undefined')
|
|
96
107
|
return navigator.userAgent;
|
|
@@ -4258,24 +4269,27 @@ async function issuerBase() {
|
|
|
4258
4269
|
const cfg = await core.getConfig();
|
|
4259
4270
|
return (cfg.humanAuthApiUrl || 'https://auth.bounded.sh').replace(/\/$/, '');
|
|
4260
4271
|
}
|
|
4272
|
+
// Base64url-encode raw bytes via the platform abstraction (web: window.btoa;
|
|
4273
|
+
// RN/Node: the polyfilled/Buffer btoa supplied by getPlatform()). On web this is
|
|
4274
|
+
// byte-identical to the previous direct `btoa(...)` call.
|
|
4261
4275
|
function b64url(buf) {
|
|
4262
4276
|
let s = '';
|
|
4263
|
-
const bytes = new Uint8Array(buf);
|
|
4277
|
+
const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
|
|
4264
4278
|
for (let i = 0; i < bytes.length; i++)
|
|
4265
4279
|
s += String.fromCharCode(bytes[i]);
|
|
4266
|
-
return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
4280
|
+
return getPlatform().btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
4267
4281
|
}
|
|
4282
|
+
// PKCE verifier + S256 challenge, fully platform-neutral: random bytes and the
|
|
4283
|
+
// SHA-256 digest both come from getPlatform() (web → WebCrypto, RN → polyfill/
|
|
4284
|
+
// expo-crypto). Web output is identical to the previous raw-crypto path.
|
|
4268
4285
|
async function makePkce() {
|
|
4269
|
-
const rnd =
|
|
4270
|
-
|
|
4271
|
-
const
|
|
4272
|
-
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
|
|
4286
|
+
const rnd = getPlatform().getRandomBytes(32);
|
|
4287
|
+
const verifier = b64url(rnd);
|
|
4288
|
+
const digest = await getPlatform().sha256(getPlatform().textEncode(verifier));
|
|
4273
4289
|
return { verifier, challenge: b64url(digest) };
|
|
4274
4290
|
}
|
|
4275
4291
|
function randomState() {
|
|
4276
|
-
|
|
4277
|
-
crypto.getRandomValues(rnd);
|
|
4278
|
-
return b64url(rnd.buffer);
|
|
4292
|
+
return b64url(getPlatform().getRandomBytes(16));
|
|
4279
4293
|
}
|
|
4280
4294
|
function normalizeMethod(value) {
|
|
4281
4295
|
const normalized = String(value || '').trim().toLowerCase();
|
|
@@ -4291,84 +4305,171 @@ function appendHostedLoginHints(params, opts) {
|
|
|
4291
4305
|
const provider = normalizeMethod(opts.provider);
|
|
4292
4306
|
if (provider)
|
|
4293
4307
|
params.set('bounded_provider', provider);
|
|
4294
|
-
|
|
4295
|
-
|
|
4308
|
+
// Standard OIDC prompt (e.g. 'login' to force a fresh sign-in / account choice).
|
|
4309
|
+
const prompt = normalizeMethod(opts.prompt);
|
|
4310
|
+
if (prompt)
|
|
4311
|
+
params.set('prompt', prompt);
|
|
4312
|
+
}
|
|
4313
|
+
// Build the authorize URL + the PKCE state that pairs with it. PURE: no storage,
|
|
4314
|
+
// no navigation — so it's shared verbatim by the web redirect/popup paths and the
|
|
4315
|
+
// native (expo-web-browser) path. The caller decides where the PkceState lives
|
|
4316
|
+
// (web → sessionStorage across the full-page redirect; native → in-memory across
|
|
4317
|
+
// the awaited browser session).
|
|
4318
|
+
async function buildAuthorize(opts, guestHint) {
|
|
4296
4319
|
const cfg = await core.getConfig();
|
|
4297
4320
|
if (!cfg.appId)
|
|
4298
4321
|
throw new Error('appId is not configured (init the SDK with your appId)');
|
|
4299
4322
|
const redirectUri = opts.redirectUri;
|
|
4300
4323
|
const { verifier, challenge } = await makePkce();
|
|
4301
4324
|
const state = randomState();
|
|
4302
|
-
// Stash the verifier + state so the redirect_uri page can finish the exchange. sessionStorage
|
|
4303
|
-
// is per-tab and cleared on close — appropriate for a short-lived auth handshake.
|
|
4304
|
-
sessionStorage.setItem(PKCE_STORE_KEY, JSON.stringify({ verifier, state, redirectUri }));
|
|
4305
4325
|
const base = await issuerBase();
|
|
4306
4326
|
const params = new URLSearchParams({
|
|
4307
4327
|
client_id: cfg.appId, redirect_uri: redirectUri, response_type: 'code', scope: 'openid profile email',
|
|
4308
4328
|
code_challenge: challenge, code_challenge_method: 'S256', state,
|
|
4309
4329
|
});
|
|
4310
4330
|
appendHostedLoginHints(params, opts);
|
|
4311
|
-
|
|
4331
|
+
// Anonymous→real UPGRADE (linkWithRedirect): carry the guest's signed id-token THROUGH
|
|
4332
|
+
// the authorize request as a hint. The issuer signature-verifies it and binds it to this
|
|
4333
|
+
// request's PKCE code_challenge, so the link is completed single-use during the normal
|
|
4334
|
+
// code→token exchange — no retired /link/social, and the appId stays bound to client_id.
|
|
4335
|
+
if (guestHint)
|
|
4336
|
+
params.set('bounded_guest_hint', guestHint);
|
|
4337
|
+
return { url: `${base}/api/auth/oauth2/authorize?${params.toString()}`, pkce: { verifier, state, redirectUri } };
|
|
4338
|
+
}
|
|
4339
|
+
// Are we on React Native (no DOM)? OIDC dispatch uses this to choose the native
|
|
4340
|
+
// expo-web-browser driver vs. the web full-page-redirect/popup driver.
|
|
4341
|
+
function isNative() {
|
|
4342
|
+
return !getPlatform().hasDOM;
|
|
4343
|
+
}
|
|
4344
|
+
// ---------------------------------------------------------------------------
|
|
4345
|
+
// Native (React Native / Expo) redirect driver
|
|
4346
|
+
// ---------------------------------------------------------------------------
|
|
4347
|
+
// Lazily load expo-web-browser. It's an OPTIONAL peer dep — imported dynamically
|
|
4348
|
+
// so the web/Node bundle never requires it. Throws an actionable error if a native
|
|
4349
|
+
// app calls hosted login without installing it.
|
|
4350
|
+
async function loadExpoWebBrowser() {
|
|
4351
|
+
try {
|
|
4352
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
4353
|
+
return await import('expo-web-browser');
|
|
4354
|
+
}
|
|
4355
|
+
catch (_a) {
|
|
4356
|
+
throw new Error('React Native hosted login requires the optional peer dependency "expo-web-browser". ' +
|
|
4357
|
+
'Install it with `npx expo install expo-web-browser` (and `expo-crypto`), then retry.');
|
|
4358
|
+
}
|
|
4359
|
+
}
|
|
4360
|
+
// Parse code/state out of the universal-link callback URL. Hand-rolled (no `new URL()`)
|
|
4361
|
+
// because React Native's URL implementation is non-spec-compliant for query parsing.
|
|
4362
|
+
function parseCallbackParams(callbackUrl) {
|
|
4363
|
+
var _a, _b;
|
|
4364
|
+
const qIndex = callbackUrl.indexOf('?');
|
|
4365
|
+
if (qIndex < 0)
|
|
4366
|
+
return { code: null, state: null };
|
|
4367
|
+
const query = callbackUrl.slice(qIndex + 1).split('#')[0];
|
|
4368
|
+
const out = {};
|
|
4369
|
+
for (const part of query.split('&')) {
|
|
4370
|
+
if (!part)
|
|
4371
|
+
continue;
|
|
4372
|
+
const eq = part.indexOf('=');
|
|
4373
|
+
const k = eq < 0 ? part : part.slice(0, eq);
|
|
4374
|
+
const v = eq < 0 ? '' : part.slice(eq + 1);
|
|
4375
|
+
try {
|
|
4376
|
+
out[decodeURIComponent(k)] = decodeURIComponent(v.replace(/\+/g, ' '));
|
|
4377
|
+
}
|
|
4378
|
+
catch (_c) {
|
|
4379
|
+
out[k] = v;
|
|
4380
|
+
}
|
|
4381
|
+
}
|
|
4382
|
+
return { code: (_a = out['code']) !== null && _a !== void 0 ? _a : null, state: (_b = out['state']) !== null && _b !== void 0 ? _b : null };
|
|
4383
|
+
}
|
|
4384
|
+
// Full native login: open the hosted page in the system auth session, await the
|
|
4385
|
+
// universal-link callback, verify state, and run the SAME PKCE code→token exchange
|
|
4386
|
+
// as web. Returns the signed-in User (native completes inline — there's no separate
|
|
4387
|
+
// completeLoginFromRedirect() round-trip the way the web full-page redirect needs).
|
|
4388
|
+
async function runNativeLogin(opts, guestHint) {
|
|
4389
|
+
var _a, _b;
|
|
4390
|
+
const { url, pkce } = await buildAuthorize(opts, guestHint);
|
|
4391
|
+
const WebBrowser = await loadExpoWebBrowser();
|
|
4392
|
+
const openAuthSession = WebBrowser.openAuthSessionAsync || ((_a = WebBrowser.default) === null || _a === void 0 ? void 0 : _a.openAuthSessionAsync);
|
|
4393
|
+
if (typeof openAuthSession !== 'function') {
|
|
4394
|
+
throw new Error('expo-web-browser.openAuthSessionAsync is unavailable — update expo-web-browser.');
|
|
4395
|
+
}
|
|
4396
|
+
setAuthLoading(true);
|
|
4397
|
+
try {
|
|
4398
|
+
// openAuthSessionAsync keeps JS alive while the system browser is shown, then
|
|
4399
|
+
// resolves with the callback URL once the universal link fires — so `pkce`
|
|
4400
|
+
// stays in scope and never needs to be persisted.
|
|
4401
|
+
const result = await openAuthSession(url, opts.redirectUri);
|
|
4402
|
+
if (!result || result.type === 'cancel' || result.type === 'dismiss') {
|
|
4403
|
+
throw new Error('Login cancelled');
|
|
4404
|
+
}
|
|
4405
|
+
if (result.type !== 'success' || typeof result.url !== 'string') {
|
|
4406
|
+
throw new Error(`Hosted login did not complete (result: ${(_b = result === null || result === void 0 ? void 0 : result.type) !== null && _b !== void 0 ? _b : 'unknown'})`);
|
|
4407
|
+
}
|
|
4408
|
+
const { code, state: returnedState } = parseCallbackParams(result.url);
|
|
4409
|
+
if (!code)
|
|
4410
|
+
throw new Error('No authorization code in the callback URL');
|
|
4411
|
+
if (!returnedState || returnedState !== pkce.state) {
|
|
4412
|
+
throw new Error('OAuth state mismatch — possible CSRF; aborting');
|
|
4413
|
+
}
|
|
4414
|
+
return await exchangeCode(code, pkce.verifier, pkce.redirectUri);
|
|
4415
|
+
}
|
|
4416
|
+
finally {
|
|
4417
|
+
setAuthLoading(false);
|
|
4418
|
+
}
|
|
4312
4419
|
}
|
|
4313
|
-
/** Start login
|
|
4314
|
-
*
|
|
4420
|
+
/** Start hosted login (Google / Apple / email / hosted phone). Works on WEB and REACT NATIVE.
|
|
4421
|
+
*
|
|
4422
|
+
* WEB: navigates the whole page to the hosted Bounded login and resolves (void) as the page
|
|
4423
|
+
* unloads; on return to `redirectUri?code=...` you call completeLoginFromRedirect().
|
|
4424
|
+
*
|
|
4425
|
+
* REACT NATIVE: opens the hosted page via expo-web-browser, awaits the https universal-link
|
|
4426
|
+
* callback, runs the PKCE exchange INLINE, and resolves with the signed-in User. No
|
|
4427
|
+
* completeLoginFromRedirect() call is needed on native. Requires `redirectUri` to be an
|
|
4428
|
+
* https UNIVERSAL LINK whose origin the app owner registered in the app's allowedOrigins
|
|
4429
|
+
* (custom `myapp://` schemes are rejected by the issuer). Needs the optional peer deps
|
|
4430
|
+
* expo-web-browser + (expo-crypto or react-native-get-random-values). */
|
|
4315
4431
|
async function loginWithRedirect(opts) {
|
|
4432
|
+
if (isNative())
|
|
4433
|
+
return runNativeLogin(opts);
|
|
4316
4434
|
if (!hasDOM())
|
|
4317
|
-
throw new Error('loginWithRedirect
|
|
4318
|
-
const url = await
|
|
4435
|
+
throw new Error('loginWithRedirect requires a browser or a configured React Native platform');
|
|
4436
|
+
const { url, pkce } = await buildAuthorize(opts);
|
|
4437
|
+
// Stash the verifier + state so the redirect_uri page can finish the exchange. sessionStorage
|
|
4438
|
+
// is per-tab and cleared on close — appropriate for a short-lived auth handshake.
|
|
4439
|
+
sessionStorage.setItem(PKCE_STORE_KEY, JSON.stringify(pkce));
|
|
4319
4440
|
window.location.href = url;
|
|
4320
4441
|
}
|
|
4321
|
-
const LINK_GUEST_KEY = 'bounded.link.guest';
|
|
4322
4442
|
/** UPGRADE the current anonymous (guest) session by linking a SOCIAL/redirect login
|
|
4323
|
-
* (e.g. Google) — Firebase `linkWithPopup`/redirect parity.
|
|
4324
|
-
*
|
|
4325
|
-
*
|
|
4326
|
-
*
|
|
4443
|
+
* (e.g. Google) — Firebase `linkWithPopup`/redirect parity. Carries the guest's signed
|
|
4444
|
+
* id-token THROUGH the OIDC authorize request (as a single-use hint bound to the PKCE
|
|
4445
|
+
* challenge), then runs the normal redirect login. The issuer completes the link during
|
|
4446
|
+
* the standard code→token exchange: if the chosen account is brand-new the guest's
|
|
4447
|
+
* `@user.id` is preserved (the returned id_token is minted under it), else the account
|
|
4448
|
+
* stays separate (guest id discarded). Requires an active guest session.
|
|
4449
|
+
*
|
|
4450
|
+
* Works on WEB (navigates, finish with completeLoginFromRedirect) and REACT NATIVE
|
|
4451
|
+
* (opens expo-web-browser and resolves with the upgraded User inline). */
|
|
4327
4452
|
async function linkWithRedirect(opts) {
|
|
4328
|
-
if (!hasDOM())
|
|
4329
|
-
throw new Error('linkWithRedirect is web-only');
|
|
4330
4453
|
const guestToken = sessionManager().getIdToken();
|
|
4331
4454
|
if (!guestToken)
|
|
4332
4455
|
throw new Error('linkWithRedirect requires an active anonymous session — call signInAnonymously() first.');
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
window.location.href = url;
|
|
4336
|
-
}
|
|
4337
|
-
// If a guest-link is pending (linkWithRedirect stashed a guest token before this
|
|
4338
|
-
// redirect), exchange it + the fresh social token for an UPGRADED token via
|
|
4339
|
-
// /link/social and replace the session. Returns the upgraded user, or null if no
|
|
4340
|
-
// link was pending / the link failed (in which case the plain social login stands).
|
|
4341
|
-
async function finishGuestLinkIfPending(socialIdToken) {
|
|
4456
|
+
if (isNative())
|
|
4457
|
+
return runNativeLogin(opts, guestToken);
|
|
4342
4458
|
if (!hasDOM())
|
|
4343
|
-
|
|
4344
|
-
const
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
sessionStorage.removeItem(LINK_GUEST_KEY);
|
|
4348
|
-
try {
|
|
4349
|
-
const cfg = await core.getConfig();
|
|
4350
|
-
const base = await issuerBase();
|
|
4351
|
-
const res = await fetch(`${base}/link/social`, {
|
|
4352
|
-
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
4353
|
-
body: JSON.stringify({ appId: cfg.appId, guestToken, socialToken: socialIdToken }),
|
|
4354
|
-
});
|
|
4355
|
-
const data = await res.json().catch(() => null);
|
|
4356
|
-
const idToken = tokenField(data, 'id_token', 'idToken');
|
|
4357
|
-
if (!res.ok || !idToken)
|
|
4358
|
-
return null; // link failed -> keep the plain social session
|
|
4359
|
-
const upgraded = userFromIdToken(idToken);
|
|
4360
|
-
const accessToken = tokenField(data, 'access_token', 'accessToken') || idToken;
|
|
4361
|
-
const refreshToken = boundedRefreshTokenField(data);
|
|
4362
|
-
await sessionManager().storeSession(String(upgraded.address || ''), accessToken, idToken, refreshToken, base);
|
|
4363
|
-
setCurrentUser(upgraded);
|
|
4364
|
-
return upgraded;
|
|
4365
|
-
}
|
|
4366
|
-
catch (_a) {
|
|
4367
|
-
return null;
|
|
4368
|
-
}
|
|
4459
|
+
throw new Error('linkWithRedirect requires a browser or a configured React Native platform');
|
|
4460
|
+
const { url, pkce } = await buildAuthorize(opts, guestToken);
|
|
4461
|
+
sessionStorage.setItem(PKCE_STORE_KEY, JSON.stringify(pkce));
|
|
4462
|
+
window.location.href = url;
|
|
4369
4463
|
}
|
|
4370
|
-
/** Finish login on the redirect_uri page: exchange ?code= for a token (PKCE-verified),
|
|
4371
|
-
* the session, and return the User. Idempotent: returns null if there's no ?code= in the URL.
|
|
4464
|
+
/** Finish login on the redirect_uri page (WEB ONLY): exchange ?code= for a token (PKCE-verified),
|
|
4465
|
+
* store the session, and return the User. Idempotent: returns null if there's no ?code= in the URL.
|
|
4466
|
+
* When the redirect was a guest UPGRADE (linkWithRedirect) and the issuer adopted the guest
|
|
4467
|
+
* id, the exchanged id_token already carries the preserved `@user.id`, so the returned User
|
|
4468
|
+
* reflects it automatically (no extra round-trip).
|
|
4469
|
+
*
|
|
4470
|
+
* On REACT NATIVE this returns null and is a no-op: native login completes INLINE inside
|
|
4471
|
+
* loginWithRedirect()/linkWithRedirect() (which resolve with the User), so there is no
|
|
4472
|
+
* separate redirect page to finish from. Calling it on RN is harmless. */
|
|
4372
4473
|
async function completeLoginFromRedirect() {
|
|
4373
4474
|
if (!hasDOM())
|
|
4374
4475
|
return null;
|
|
@@ -4387,15 +4488,11 @@ async function completeLoginFromRedirect() {
|
|
|
4387
4488
|
throw new Error('OAuth state mismatch — possible CSRF; aborting');
|
|
4388
4489
|
const user = await exchangeCode(code, pkce.verifier, pkce.redirectUri);
|
|
4389
4490
|
sessionStorage.removeItem(PKCE_STORE_KEY);
|
|
4390
|
-
// If this redirect was a guest UPGRADE (linkWithRedirect), finish the link now:
|
|
4391
|
-
// swap the fresh social session for one minted under the guest's preserved id.
|
|
4392
|
-
const socialIdToken = sessionManager().getIdToken();
|
|
4393
|
-
const upgraded = socialIdToken ? await finishGuestLinkIfPending(socialIdToken) : null;
|
|
4394
4491
|
// Strip code/state from the URL so a reload doesn't re-exchange (codes are single-use anyway).
|
|
4395
4492
|
url.searchParams.delete('code');
|
|
4396
4493
|
url.searchParams.delete('state');
|
|
4397
4494
|
window.history.replaceState({}, document.title, url.pathname + url.search + url.hash);
|
|
4398
|
-
return
|
|
4495
|
+
return user;
|
|
4399
4496
|
}
|
|
4400
4497
|
finally {
|
|
4401
4498
|
setAuthLoading(false);
|
|
@@ -4405,8 +4502,9 @@ async function completeLoginFromRedirect() {
|
|
|
4405
4502
|
* redirect_uri page posts the code back. The redirect_uri page must call completeLoginInPopup(). */
|
|
4406
4503
|
async function loginWithPopup(opts) {
|
|
4407
4504
|
if (!hasDOM())
|
|
4408
|
-
throw new Error('loginWithPopup is web-only');
|
|
4409
|
-
const url = await
|
|
4505
|
+
throw new Error('loginWithPopup is web-only — on React Native use loginWithRedirect()');
|
|
4506
|
+
const { url, pkce } = await buildAuthorize(opts);
|
|
4507
|
+
sessionStorage.setItem(PKCE_STORE_KEY, JSON.stringify(pkce));
|
|
4410
4508
|
const w = opts.width || 480, h = opts.height || 720;
|
|
4411
4509
|
const left = window.screenX + (window.outerWidth - w) / 2;
|
|
4412
4510
|
const top = window.screenY + (window.outerHeight - h) / 2;
|