@bounded-sh/client 0.0.42 → 0.0.43

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/REACT_NATIVE.md CHANGED
@@ -63,20 +63,18 @@ On native, `loginWithRedirect()` opens the system/in-app browser and resolves
63
63
  with the user when the callback returns to the app. `completeLoginFromRedirect()`
64
64
  is for web callback pages.
65
65
 
66
- ## 3. Guest login
67
-
68
- Anonymous auth is fully native and does not require Expo browser packages:
69
-
70
- ```ts
71
- import { init, signInAnonymously, getCurrentUser } from "@bounded-sh/client";
72
-
73
- await init({ appId: "YOUR_APP_ID" });
74
- await signInAnonymously();
75
- const user = await getCurrentUser();
76
- ```
77
-
78
- The device-local ed25519 keypair is stored in the RN storage adapter, so the
79
- guest identity survives app restarts until `logout()` clears it.
66
+ ## 3. Guest login is browser-only in the current release
67
+
68
+ Do not call `signInAnonymously()` from React Native. The current guest identity
69
+ uses a non-extractable WebCrypto Ed25519 key stored in IndexedDB; it does not use
70
+ the React Native session-storage adapter above. Native runtimes therefore fail
71
+ closed before making an authentication request.
72
+
73
+ Use hosted email/social login from section 2, or the explicit Privy Expo bridge
74
+ below. Do not add an IndexedDB polyfill to persist a guest signing key: that does
75
+ not provide the platform-backed, non-exportable key contract a native guest flow
76
+ needs. Native anonymous auth will be documented here only after the SDK ships a
77
+ Keychain/Keystore-backed signing implementation and migration/loss semantics.
80
78
 
81
79
  ## 4. Privy on React Native (`privy-expo`)
82
80
 
package/dist/index.js CHANGED
@@ -4530,23 +4530,74 @@ async function completeLoginFromRedirect() {
4530
4530
  async function loginWithPopup(opts) {
4531
4531
  if (!hasDOM())
4532
4532
  throw new Error('loginWithPopup is web-only — on React Native use loginWithRedirect()');
4533
- const { url, pkce } = await buildAuthorize(opts);
4534
- sessionStorage.setItem(PKCE_STORE_KEY, JSON.stringify(pkce));
4533
+ // Open synchronously, before PKCE/config awaits consume the click's user-
4534
+ // activation window. Opening only after buildAuthorize() intermittently
4535
+ // left Chrome with a real named window stranded at about:blank (or blocked
4536
+ // it entirely), while callers waited forever for an OAuth message.
4535
4537
  const w = opts.width || 480, h = opts.height || 720;
4536
4538
  const left = window.screenX + (window.outerWidth - w) / 2;
4537
4539
  const top = window.screenY + (window.outerHeight - h) / 2;
4538
- const popup = window.open(url, 'bounded-oidc', `width=${w},height=${h},left=${left},top=${top}`);
4540
+ const popup = window.open('', 'bounded-oidc', `width=${w},height=${h},left=${left},top=${top}`);
4539
4541
  if (!popup)
4540
4542
  throw new Error('Popup blocked — use loginWithRedirect() instead');
4541
- // Use the RESOLVED redirect_uri (buildAuthorize defaulted it to the current page on web),
4542
- // not opts.redirectUri which may be undefined.
4543
- const expectedOrigin = new URL(pkce.redirectUri).origin;
4543
+ let authorization;
4544
+ try {
4545
+ authorization = await buildAuthorize(opts);
4546
+ }
4547
+ catch (error) {
4548
+ try {
4549
+ popup.close();
4550
+ }
4551
+ catch ( /* ignore */_a) { /* ignore */ }
4552
+ throw error;
4553
+ }
4554
+ const { url, pkce } = authorization;
4555
+ // Validate every remaining synchronous setup step before navigating. Both
4556
+ // URL parsing and sessionStorage can throw (malformed caller input, blocked
4557
+ // storage, quota/privacy mode); because the popup is already open to retain
4558
+ // user activation, close it on either failure instead of orphaning a blank
4559
+ // window with no listener or watchdog attached.
4560
+ let expectedOrigin;
4561
+ try {
4562
+ expectedOrigin = new URL(pkce.redirectUri).origin;
4563
+ sessionStorage.setItem(PKCE_STORE_KEY, JSON.stringify(pkce));
4564
+ }
4565
+ catch (error) {
4566
+ try {
4567
+ popup.close();
4568
+ }
4569
+ catch ( /* ignore */_b) { /* ignore */ }
4570
+ throw error;
4571
+ }
4572
+ try {
4573
+ popup.location.replace(url);
4574
+ }
4575
+ catch (_c) {
4576
+ try {
4577
+ popup.location.href = url;
4578
+ }
4579
+ catch (_d) {
4580
+ try {
4581
+ popup.close();
4582
+ }
4583
+ catch ( /* ignore */_e) { /* ignore */ }
4584
+ throw new Error('Popup failed to navigate — use loginWithRedirect() instead');
4585
+ }
4586
+ }
4587
+ // expectedOrigin came from the RESOLVED redirect_uri (buildAuthorize defaulted it to the
4588
+ // current page on web), not opts.redirectUri which may be undefined.
4544
4589
  setAuthLoading(true); // reflect "signing in…" while the popup is open
4545
4590
  try {
4546
4591
  return await new Promise((resolve, reject) => {
4547
4592
  let settled = false;
4548
4593
  let timer;
4549
- const cleanup = () => { settled = true; clearInterval(timer); window.removeEventListener('message', onMsg); };
4594
+ let navigationTimer;
4595
+ const cleanup = () => {
4596
+ settled = true;
4597
+ clearInterval(timer);
4598
+ clearTimeout(navigationTimer);
4599
+ window.removeEventListener('message', onMsg);
4600
+ };
4550
4601
  const onMsg = async (ev) => {
4551
4602
  if (ev.origin !== expectedOrigin)
4552
4603
  return; // only trust the redirect_uri origin
@@ -4574,6 +4625,27 @@ async function loginWithPopup(opts) {
4574
4625
  }
4575
4626
  };
4576
4627
  window.addEventListener('message', onMsg);
4628
+ // Some browser/extension combinations can return a real popup handle but
4629
+ // strand the window at about:blank instead of navigating to the hosted
4630
+ // authorize URL. The old code then waited forever because the popup was
4631
+ // open and no message could ever arrive. Once the popup becomes
4632
+ // cross-origin, reading location throws — that is positive evidence that
4633
+ // navigation succeeded, so only a still-readable about:blank is rejected.
4634
+ navigationTimer = setTimeout(() => {
4635
+ if (settled || popup.closed)
4636
+ return;
4637
+ try {
4638
+ if (popup.location.href === 'about:blank') {
4639
+ cleanup();
4640
+ try {
4641
+ popup.close();
4642
+ }
4643
+ catch ( /* ignore */_a) { /* ignore */ }
4644
+ reject(new Error('Popup stalled at about:blank — use loginWithRedirect() instead'));
4645
+ }
4646
+ }
4647
+ catch ( /* cross-origin hosted auth loaded normally */_b) { /* cross-origin hosted auth loaded normally */ }
4648
+ }, 4000);
4577
4649
  // The popup posts its code and then closes ITSELF, so a naive "popup.closed -> reject"
4578
4650
  // races the just-posted message and would report a spurious "Login window closed" on a
4579
4651
  // SUCCESSFUL login. When we see it closed, wait a beat for that pending message to settle
@@ -5345,6 +5417,12 @@ function useQuery(path, options) {
5345
5417
  core.subscribe(path, Object.assign(Object.assign({}, options), { onData: (d) => {
5346
5418
  if (!active)
5347
5419
  return;
5420
+ // A reconnect can legitimately surface a transport error before the
5421
+ // socket re-authenticates and sends a new authorized snapshot. That
5422
+ // fresh snapshot is the recovery boundary: keep the error visible
5423
+ // until data arrives, then clear it so React does not remain stuck on
5424
+ // a failure page after realtime has recovered.
5425
+ setError(null);
5348
5426
  setData(d);
5349
5427
  setLoading(false);
5350
5428
  }, onError: (e) => {