@kolbo/app-sdk
Advanced tools
+1
-1
| { | ||
| "name": "@kolbo/app-sdk", | ||
| "version": "2.0.15", | ||
| "version": "2.0.16", | ||
| "description": "Kolbo App Builder SDK — auth, data, storage and AI for generated apps", | ||
@@ -5,0 +5,0 @@ "type": "module", |
+57
-12
@@ -240,10 +240,23 @@ // Kolbo-native adapter. | ||
| if (inPopup) { | ||
| // Three-channel handoff so at least one reaches the opener even | ||
| // if the browser severs window.opener across cross-origin hops | ||
| // (COOP) or suppresses storage events on tab-close. | ||
| try { window.opener.postMessage({ type: '__kolbo_oauth_done__', ok: true }, '*'); } catch (_) {} | ||
| // Three-channel handoff so at least one reaches the opener. The | ||
| // tokens themselves are sent inline because popup and opener are | ||
| // often on DIFFERENT origins (popup = snapshot host media-dev, | ||
| // opener = p-preview proxy on the backend origin). localStorage | ||
| // is per-origin, so the opener CANNOT read the session we just | ||
| // wrote in the popup — postMessage is the only cross-origin | ||
| // transport that carries the tokens through. | ||
| const payload = { | ||
| type: '__kolbo_oauth_done__', | ||
| ok: true, | ||
| session: { | ||
| access_token: session.access_token, | ||
| refresh_token: session.refresh_token, | ||
| expires_at: session.expires_at, | ||
| }, | ||
| }; | ||
| try { window.opener.postMessage(payload, '*'); } catch (_) {} | ||
| try { | ||
| if (typeof BroadcastChannel === 'function') { | ||
| const ch = new BroadcastChannel(`kolbo-oauth:${this._appId || ''}`); | ||
| ch.postMessage({ type: '__kolbo_oauth_done__', ok: true }); | ||
| ch.postMessage(payload); | ||
| // Close channel on next tick so the message actually ships. | ||
@@ -253,5 +266,5 @@ setTimeout(() => { try { ch.close(); } catch (_) {} }, 50); | ||
| } catch (_) {} | ||
| // Give the opener a tick to read all three channels (postMessage, | ||
| // storage event from _writeStored above, BroadcastChannel). | ||
| setTimeout(() => { try { window.close(); } catch (_) {} }, 120); | ||
| // Give the opener a tick to consume the message + write its own | ||
| // localStorage (postMessage delivery is microtask-scheduled). | ||
| setTimeout(() => { try { window.close(); } catch (_) {} }, 150); | ||
| } | ||
@@ -670,7 +683,22 @@ } else if (hashError && inPopup) { | ||
| // Popup auto-wrote the session to our scoped localStorage key | ||
| // before posting. Hydrate user via /me if missing BEFORE firing | ||
| // SIGNED_IN — otherwise React subscribers see user=null and | ||
| // AuthGuard redirects to /login. | ||
| // Cross-origin case: popup sent its tokens inline because our | ||
| // localStorage is isolated from the popup's. Write them into our | ||
| // OWN scoped storage key first — otherwise hydrate reads back | ||
| // the pre-existing admin/empty session and the user is stranded | ||
| // on /login even though Google sign-in succeeded. | ||
| try { | ||
| if (event.data.ok && event.data.session && event.data.session.access_token) { | ||
| _writeStored(self._storageKey, { | ||
| access_token: event.data.session.access_token, | ||
| refresh_token: event.data.session.refresh_token || null, | ||
| expires_at: event.data.session.expires_at || null, | ||
| user: null, // hydrated via /me next | ||
| }); | ||
| } | ||
| } catch (_) { /* non-fatal */ } | ||
| // Hydrate user via /me if missing BEFORE firing SIGNED_IN — | ||
| // otherwise React subscribers see user=null and AuthGuard | ||
| // redirects to /login. | ||
| try { | ||
| if (event.data.ok) { | ||
@@ -716,2 +744,19 @@ await self._hydrateAndDispatchSignIn(); | ||
| if (ev.data.ok) { | ||
| // Write tokens carried in the message payload into our own | ||
| // scoped localStorage — needed when popup + opener are on | ||
| // different origins and localStorage/storage-events can't | ||
| // bridge the gap. BroadcastChannel itself is same-origin | ||
| // so this path only fires when both share an origin, but | ||
| // keeping the write here guards against future changes | ||
| // to the popup's origin. | ||
| try { | ||
| if (ev.data.session && ev.data.session.access_token) { | ||
| _writeStored(self._storageKey, { | ||
| access_token: ev.data.session.access_token, | ||
| refresh_token: ev.data.session.refresh_token || null, | ||
| expires_at: ev.data.session.expires_at || null, | ||
| user: null, | ||
| }); | ||
| } | ||
| } catch (_) { /* non-fatal */ } | ||
| const hydrated = await self._hydrateAndDispatchSignIn(); | ||
@@ -718,0 +763,0 @@ done({ data: { user: hydrated?.user || null }, error: null }); |
67313
3.59%1187
3.85%