1// Chrome Side Panel â SsamFlow page bridge. 2// The panel never receives the refresh token. It receives only the current 3// access token from the extension and uses it for short-lived panel requests. 4(function () { 5 const params = new URLSearchParams(location.search); 6 const panelMode = params.get('sf_panel') === '1' && window.parent !== window; 7 window.SF_PANEL_MODE = panelMode; 8 window.SF_PANEL_ACCESS_TOKEN = ''; 9 document.documentElement.classList.toggle('sf-panel-mode', panelMode); 10 11 function send(message) { 12 if (!panelMode || window.parent === window) return; 13 window.parent.postMessage(message, '*'); 14 } 15 16 function publishToken(token) { 17 window.SF_PANEL_ACCESS_TOKEN = typeof token === 'string' ? token : ''; 18 window.dispatchEvent(new CustomEvent('sf-panel-auth', { 19 detail: { accessToken: window.SF_PANEL_ACCESS_TOKEN }, 20 })); 21 } 22 23 async function acceptPanelAuth(token) { 24 if (!token) { 25 publishToken(''); 26 window.SF_AUTH?.setPanelAuth?.(null); 27 return; 28 } 29 const response = await fetch('/api/panel/me', { 30 headers: { Authorization: 'Bearer ' + token }, 31 cache: 'no-store', 32 }).catch(() => null); 33 if (!response?.ok) { 34 publishToken(''); 35 window.SF_AUTH?.setPanelAuth?.(null); 36 return; 37 } 38 const body = await response.json().catch(() => null); 39 if (!body?.user) return; 40 publishToken(token); 41 window.SF_AUTH?.setPanelAuth?.({ 42 access_token: token, 43 user: body.user, 44 is_panel: true, 45 }); 46 } 47 48 // A side-panel login has an access token supplied by the extension, not a 49 // browser Supabase session. Before opening a protected sibling program, 50 // exchange that already-verified token for the shared SsamFlow cookie. 51 // This makes panel and web logins follow the same program handoff route. 52 async function syncPanelSession() { 53 // Side-panel mode receives a short-lived token from the extension. 54 // Normal SsamFlow pages already have a browser Supabase session, so 55 // use that session here as well instead of letting sibling programs 56 // perform a second, unauthenticated handoff. 57 let token = window.SF_PANEL_ACCESS_TOKEN; 58 let refreshToken = ''; 59 let expiresIn = 3600; 60 if (!token && window.supabase?.auth?.getSession) { 61 try { 62 const result = await window.supabase.auth.getSession(); 63 const session = result?.data?.session; 64 token = session?.access_token || ''; 65 refreshToken = session?.refresh_token || ''; 66 expiresIn = Number(session?.expires_in || 3600); 67 } catch { 68 token = ''; 69 } 70 } 71 if (!token) return false; 72 try { 73 const response = await fetch('/api/auth/session', { 74 method: 'POST', 75 credentials: 'include', 76 headers: { 'Content-Type': 'application/json' }, 77 body: JSON.stringify({ 78 access_token: token, 79 refresh_token: refreshToken, 80 expires_in: expiresIn, 81 }), 82 }); 83 if (!response.ok) return false; 84 const verified = await fetch(`/api/auth/me?check=${encodeURIComponent(String(Date.now()))}`, { credentials: 'include', cache: 'no-store', headers: { 'Cache-Control': 'no-cache' } }); 85 const identity = verified.ok ? await verified.json().catch(() => null) : null; 86 return Boolean(identity?.authenticated); 87 } catch { 88 return false; 89 } 90 } 91 92 window.sfOpenProgram = async function (url) { 93 const value = String(url || ''); 94 try { 95 const target = new URL(value, location.origin); 96 if (!['http:', 'https:'].includes(target.protocol)) return; 97 const synced = await syncPanelSession(); 98 // Keep the handoff explicit when a page was opened before the 99 // shared session finished syncing. This prevents a sibling app 100 // from redirecting back into its own gate over and over. 101 let nextUrl = target.toString(); 102 if (!synced && target.hostname.endsWith('.ssamflow.com') && target.hostname !== 'ssamflow.com') { 103 const bridge = new URL('https://ssamflow.com/sso-bridge.html'); 104 bridge.searchParams.set('returnTo', nextUrl); 105 nextUrl = bridge.toString(); 106 } 107 if (panelMode) { 108 send({ type: 'SSAMFLOW_OPEN_PROGRAM', url: nextUrl }); 109 } else { 110 window.open(nextUrl, '_blank', 'noopener'); 111 } 112 } catch { 113 /* invalid program URL */ 114 } 115 }; 116 117 window.sfRequestPanelContext = function () { 118 send({ type: 'SSAMFLOW_REQUEST_CONTEXT' }); 119 }; 120 121 document.addEventListener('click', event => { 122 if (!panelMode) return; 123 const link = event.target.closest?.('a#sfMypageBtn'); 124 if (!link) return; 125 event.preventDefault(); 126 try { 127 send({ type: 'SSAMFLOW_OPEN_PROGRAM', url: new URL(link.href, location.origin).toString() }); 128 } catch { /* invalid link */ } 129 }); 130 131 window.addEventListener('message', event => { 132 if (!panelMode || event.source !== window.parent) return; 133 const message = event.data || {}; 134 if (message.type === 'SSAMFLOW_PANEL_AUTH') { 135 acceptPanelAuth(message.accessToken); 136 } 137 if (message.type === 'SSAMFLOW_TAB_CONTEXT') { 138 window.dispatchEvent(new CustomEvent('sf-panel-context', { 139 detail: { url: typeof message.url === 'string' ? message.url : '' }, 140 })); 141 } 142 }); 143 144 window.addEventListener('DOMContentLoaded', () => { 145 send({ type: 'SSAMFLOW_PANEL_READY' }); 146 if (window.SF_PANEL_ACCESS_TOKEN) acceptPanelAuth(window.SF_PANEL_ACCESS_TOKEN); 147 }); 148})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.