@frak-labs/core-sdk 1.0.0-beta.a7645eaa → 1.0.0-beta.b917e199
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/cdn/bundle.js +3 -3
- package/dist/{actions-C-vPLo1H.js → actions-BlCQVBQJ.js} +1 -1
- package/dist/{actions-3g2MTs6f.cjs → actions-Bwj4zSdB.cjs} +1 -1
- package/dist/actions.cjs +1 -1
- package/dist/actions.js +1 -1
- package/dist/bundle.cjs +1 -1
- package/dist/bundle.d.cts +1 -1
- package/dist/bundle.d.ts +1 -1
- package/dist/bundle.js +1 -1
- package/dist/{index-CNukNJzV.d.ts → index-9TdOc_ub.d.ts} +4 -2
- package/dist/{index-tYNUZNRd.d.cts → index-DPIqLMCR.d.cts} +4 -2
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/sdkConfigStore-BXzz5PlK.js +1 -0
- package/dist/sdkConfigStore-DDL_fjYX.cjs +1 -0
- package/dist/src-C4ZNZbhF.cjs +13 -0
- package/dist/src-D_QRg85U.js +13 -0
- package/package.json +2 -2
- package/src/utils/FrakContext.test.ts +31 -35
- package/src/utils/FrakContext.ts +16 -33
- package/src/utils/frakContextV2Codec.test.ts +241 -0
- package/src/utils/frakContextV2Codec.ts +197 -0
- package/dist/sdkConfigStore-thuozJuB.cjs +0 -1
- package/dist/sdkConfigStore-ylgC-Tp0.js +0 -1
- package/dist/src-BhrneHv1.js +0 -13
- package/dist/src-DkkrSfA4.cjs +0 -13
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const e=require(`./sdkConfigStore-DDL_fjYX.cjs`);let t=require(`@frak-labs/frame-connector`),n=require(`@openpanel/web`);function r(n){return(0,t.jsonDecode)(e.h(n))}const i=`nexus-wallet-backup`,a=`frakwallet://`;function o(){let e=navigator.userAgent;return/Android/i.test(e)&&/Chrome\/\d+/i.test(e)}function s(e){return`intent://${e.slice(13)}#Intent;scheme=frakwallet;end`}function c(e,t){let n=t?.timeout??2500,r=!1,i=()=>{document.hidden&&(r=!0)};document.addEventListener(`visibilitychange`,i);let a=o()&&l(e)?s(e):e;window.location.href=a,setTimeout(()=>{document.removeEventListener(`visibilitychange`,i),r||t?.onFallback?.()},n)}function l(e){return e.startsWith(a)}const u={eur:`fr-FR`,usd:`en-US`,gbp:`en-GB`};function d(e){return e&&e in u?e:`eur`}function f(e){return e?u[e]??u.eur:u.eur}function p(e,t){let n=f(t),r=d(t);return e.toLocaleString(n,{style:`currency`,currency:r,minimumFractionDigits:0,maximumFractionDigits:2})}function m(e){return e?`${e}Amount`:`eurAmount`}const h={id:`frak-wallet`,name:`frak-wallet`,title:`Frak Wallet`,allow:`publickey-credentials-get *; clipboard-write; web-share *`,style:{width:`0`,height:`0`,border:`0`,position:`absolute`,zIndex:2000001,top:`-1000px`,left:`-1000px`,colorScheme:`auto`}};function g({walletBaseUrl:t,config:n}){let r=document.querySelector(`#frak-wallet`);r&&r.remove();let i=document.createElement(`iframe`);i.id=h.id,i.name=h.name,i.allow=h.allow,i.style.zIndex=h.style.zIndex.toString(),_({iframe:i,isVisible:!1});let a=n?.walletUrl??t??`https://wallet.frak.id`,o=e.v();return i.src=`${a}/listener?clientId=${encodeURIComponent(o)}`,new Promise(e=>{i.addEventListener(`load`,()=>e(i)),document.body.appendChild(i)})}function _({iframe:e,isVisible:t}){if(!t){e.style.width=`0`,e.style.height=`0`,e.style.border=`0`,e.style.position=`fixed`,e.style.top=`-1000px`,e.style.left=`-1000px`;return}e.style.position=`fixed`,e.style.top=`0`,e.style.left=`0`,e.style.width=`100%`,e.style.height=`100%`,e.style.pointerEvents=`auto`}function v(e=`/listener`){if(!window.opener)return null;let t=t=>{try{return t.location.origin===window.location.origin&&t.location.pathname===e}catch{return!1}};if(t(window.opener))return window.opener;try{let e=window.opener.frames;for(let n=0;n<e.length;n++)if(t(e[n]))return e[n];return null}catch(t){return console.error(`[findIframeInOpener] Error finding iframe with pathname ${e}:`,t),null}}function y(){if(typeof navigator>`u`)return!1;let e=navigator.userAgent;return!!(/iPhone|iPad|iPod/i.test(e)||/Macintosh/i.test(e)&&navigator.maxTouchPoints>1)}const b=y();function x(){if(typeof navigator>`u`)return!1;let e=navigator.userAgent.toLowerCase();return e.includes(`instagram`)||e.includes(`fban`)||e.includes(`fbav`)||e.includes(`facebook`)}const S=x();function C(e){b&&e.startsWith(`https://`)?window.location.href=`x-safari-https://${e.slice(8)}`:b&&e.startsWith(`http://`)?window.location.href=`x-safari-http://${e.slice(7)}`:window.location.href=`https://backend.frak.id/common/social?u=${encodeURIComponent(e)}`}function w({perCall:e,defaults:t,productUtmContent:n}){if(e===null)return;let r=e!==void 0,i=t!==void 0&&Object.keys(t).length>0;if(!r&&!i&&!(n!==void 0&&n!==``))return;let a={...t,...e??{}},o=n??e?.utmContent;return o!==void 0&&o!==``?a.utmContent=o:delete a.utmContent,a}function T(e,t){if(typeof window>`u`)return;let n=new URL(window.location.href),r=n.searchParams.get(`sso`);r&&(t.then(()=>{e.sendLifecycle({clientLifecycle:`sso-redirect-complete`,data:{compressed:r}}),console.log(`[SSO URL Listener] Forwarded compressed SSO data to iframe`)}).catch(e=>{console.error(`[SSO URL Listener] Failed to forward SSO data:`,e)}),n.searchParams.delete(`sso`),window.history.replaceState({},``,n.toString()),console.log(`[SSO URL Listener] SSO parameter detected and URL cleaned`))}var E=class e{config;iframe;isSetupDone=!1;lastResponse=null;lastRequest=null;constructor(e,t){this.config=e,this.iframe=t,this.lastRequest=null,this.lastResponse=null}setLastResponse(e,t){this.lastResponse={message:e,response:t,timestamp:Date.now()}}setLastRequest(e){this.lastRequest={event:e,timestamp:Date.now()}}updateSetupStatus(e){this.isSetupDone=e}base64Encode(e){try{return btoa(JSON.stringify(e))}catch(e){return console.warn(`Failed to encode debug data`,e),btoa(`Failed to encode data`)}}getIframeStatus(){return this.iframe?{loading:this.iframe.hasAttribute(`loading`),url:this.iframe.src,readyState:this.iframe.contentDocument?.readyState?+(this.iframe.contentDocument.readyState===`complete`):-1,contentWindow:!!this.iframe.contentWindow,isConnected:this.iframe.isConnected}:null}getNavigatorInfo(){return navigator?{userAgent:navigator.userAgent,language:navigator.language,onLine:navigator.onLine,screenWidth:window.screen.width,screenHeight:window.screen.height,pixelRatio:window.devicePixelRatio}:null}gatherDebugInfo(e){let n=this.getIframeStatus(),r=this.getNavigatorInfo(),i=`Unknown`;return e instanceof t.FrakRpcError?i=`FrakRpcError: ${e.code} '${e.message}'`:e instanceof Error?i=e.message:typeof e==`string`&&(i=e),{timestamp:new Date().toISOString(),encodedUrl:btoa(window.location.href),encodedConfig:this.config?this.base64Encode(this.config):`no-config`,navigatorInfo:r?this.base64Encode(r):`no-navigator`,iframeStatus:n?this.base64Encode(n):`not-iframe`,lastRequest:this.lastRequest?this.base64Encode(this.lastRequest):`No Frak request logged`,lastResponse:this.lastResponse?this.base64Encode(this.lastResponse):`No Frak response logged`,clientStatus:this.isSetupDone?`setup`:`not-setup`,error:i}}static empty(){return new e}formatDebugInfo(e){let t=this.gatherDebugInfo(e);return`
|
|
2
|
+
Debug Information:
|
|
3
|
+
-----------------
|
|
4
|
+
Timestamp: ${t.timestamp}
|
|
5
|
+
URL: ${t.encodedUrl}
|
|
6
|
+
Config: ${t.encodedConfig}
|
|
7
|
+
Navigator Info: ${t.navigatorInfo}
|
|
8
|
+
IFrame Status: ${t.iframeStatus}
|
|
9
|
+
Last Request: ${t.lastRequest}
|
|
10
|
+
Last Response: ${t.lastResponse}
|
|
11
|
+
Client Status: ${t.clientStatus}
|
|
12
|
+
Error: ${t.error}
|
|
13
|
+
`.trim()}};const D=(()=>{if(typeof navigator>`u`)return!1;let e=navigator.userAgent;if(!(/iPhone|iPad|iPod/i.test(e)||/Macintosh/i.test(e)&&navigator.maxTouchPoints>1))return!1;let t=e.toLowerCase();return t.includes(`instagram`)||t.includes(`fban`)||t.includes(`fbav`)||t.includes(`facebook`)})();function O(e){e?localStorage.setItem(i,e):localStorage.removeItem(i)}function k(e,t){try{let n=new URL(e);if(!n.searchParams.has(`u`))return e;let r=M(window.location.href,t);return n.searchParams.delete(`u`),n.searchParams.append(`u`,r),n.toString()}catch{return e}}function A(e){let t=new URL(window.location.href);e&&t.searchParams.set(`fmt`,e);let n=t.protocol===`http:`?`x-safari-http`:`x-safari-https`;window.location.href=`${n}://${t.host}${t.pathname}${t.search}${t.hash}`}function j(e){return e.includes(`/common/social`)}function M(e,t){if(!t)return e;try{let n=new URL(e);return n.searchParams.set(`fmt`,t),n.toString()}catch{return`${e}${e.includes(`?`)?`&`:`?`}fmt=${encodeURIComponent(t)}`}}function N(e,t,n,r,i){if(i){let e=k(t,r);window.open(e,`_blank`);return}if(l(t)){let i=k(t,r);c(i,{onFallback:()=>{e.contentWindow?.postMessage({clientLifecycle:`deep-link-failed`,data:{originalUrl:i}},n)}})}else if(D&&j(t))A(r);else{let e=k(t,r);window.location.href=e}}function P({iframe:e,targetOrigin:n}){let r=new t.Deferred;return{handleEvent:t=>{if(!(`iframeLifecycle`in t))return;let{iframeLifecycle:a,data:o}=t;switch(a){case`connected`:r.resolve(!0);break;case`do-backup`:O(o.backup);break;case`remove-backup`:localStorage.removeItem(i);break;case`show`:case`hide`:_({iframe:e,isVisible:a===`show`});break;case`redirect`:N(e,o.baseRedirectUrl,n,o.mergeToken,o.openInNewTab);break}},isConnected:r.promise}}function F({config:r,iframe:i}){let a=r?.walletUrl??`https://wallet.frak.id`,o=typeof navigator<`u`?navigator.language?.split(`-`)[0]:void 0,s=r.metadata.lang??(o===`en`||o===`fr`?o:void 0),c=r.domain??(typeof window<`u`?window.location.hostname:``);e.t.setCacheScope(c,s),e.t.reset();let l=e.t.isCacheFresh?void 0:e.t.resolve(r.domain,r.walletUrl,s),u=P({iframe:i,targetOrigin:a}),d=new t.Deferred,f=Date.now(),p=new E(r,i);if(!i.contentWindow)throw new t.FrakRpcError(t.RpcErrorCodes.configError,`The iframe does not have a content window`);let m=(0,t.createRpcClient)({emittingTransport:i.contentWindow,listeningTransport:window,targetOrigin:a,middleware:[{async onRequest(e,n){if(!await u.isConnected)throw new t.FrakRpcError(t.RpcErrorCodes.clientNotConnected,`The iframe provider isn't connected yet`);return await d.promise,n}},{onRequest(e,t){return p.setLastRequest(e),t},onResponse(e,t){return p.setLastResponse(e,t),t}}],lifecycleHandlers:{iframeLifecycle:(e,t)=>{u.handleEvent(e)}}}),h=I(m,u),g=async()=>{h(),m.cleanup(),i.remove(),e.a(),e.t.clearCache(),e.t.reset()},_;{console.log(`[Frak SDK] Initializing OpenPanel`),_=new n.OpenPanel({apiUrl:`https://op-api.gcp.frak.id`,clientId:`6eacc8d7-49ac-4936-95e9-81ef29449570`,trackScreenViews:!0,trackOutgoingLinks:!0,trackAttributes:!1,filter:({type:t,payload:n})=>(t!==`track`||!n?.properties||`sdkVersion`in n.properties||(n.properties={...n.properties,sdkVersion:`1.0.0`,userAnonymousClientId:e.v()}),!0)}),_.setGlobalProperties({sdkVersion:`1.0.0`,userAnonymousClientId:e.v()}),_.init(),_.track(`sdk_initialized`,{sdkVersion:`1.0.0`});let t=!1,r=setTimeout(()=>{t||(t=!0,_?.track(`sdk_iframe_handshake_failed`,{reason:`timeout`}))},3e4);u.isConnected.then(()=>{t||(t=!0,clearTimeout(r),_?.track(`sdk_iframe_connected`,{handshake_duration_ms:Date.now()-f}))}).catch(()=>{t||(t=!0,clearTimeout(r),_?.track(`sdk_iframe_handshake_failed`,{reason:`unknown`}))})}let v=L({config:r,rpcClient:m,lifecycleManager:u,configPromise:l,contextSent:d,openPanel:_}).then(()=>p.updateSetupStatus(!0)).catch(e=>{throw d.reject(e),e});return{config:r,debugInfo:p,waitForConnection:u.isConnected,waitForSetup:v,request:m.request,listenerRequest:m.listen,destroy:g,openPanel:_}}function I(e,t){let n,r,i=()=>e.sendLifecycle({clientLifecycle:`heartbeat`});async function a(){i(),n=setInterval(i,1e3),r=setTimeout(()=>{o(),console.log(`Heartbeat timeout: connection failed`)},3e4),await t.isConnected,o()}function o(){n&&clearInterval(n),r&&clearTimeout(r)}return a(),o}async function L({config:t,rpcClient:n,lifecycleManager:r,configPromise:a,contextSent:o,openPanel:s}){await r.isConnected,T(n,r.isConnected);let c=new URL(window.location.href),l=c.searchParams.get(`fmt`)??void 0;l&&(c.searchParams.delete(`fmt`),window.history.replaceState({},``,c.toString()));let u=n=>{let r=n?.merchantId??t.metadata.merchantId??``,i=n?.domain??``,a=n?.allowedDomains??[],o=n?.sdkConfig,s=o?.attribution||t.attribution?{...t.attribution,...o?.attribution}:void 0;e.t.setConfig(o?{isResolved:!0,merchantId:r,domain:i,allowedDomains:a,hasRawSdkConfig:!0,name:o.name??t.metadata.name,logoUrl:o.logoUrl??t.metadata.logoUrl,homepageLink:o.homepageLink??t.metadata.homepageLink,lang:o.lang??t.metadata.lang,currency:o.currency??t.metadata.currency,hidden:o.hidden,css:o.css,translations:o.translations,placements:o.placements,components:o.components,attribution:s}:{isResolved:!0,merchantId:r,domain:i,allowedDomains:a,name:t.metadata.name,logoUrl:t.metadata.logoUrl,homepageLink:t.metadata.homepageLink,lang:t.metadata.lang,currency:t.metadata.currency,attribution:s})},d=!1,f=t=>{let r=d?void 0:l;d=!0;let i=t.hasRawSdkConfig?{name:t.name,logoUrl:t.logoUrl,homepageLink:t.homepageLink,lang:t.lang,currency:t.currency,hidden:t.hidden,css:t.css,translations:t.translations,placements:t.placements,attribution:t.attribution}:t.attribution?{attribution:t.attribution}:void 0,a=e.v();if(s){let e=s.global??{};s.setGlobalProperties({...e,merchantId:t.merchantId,domain:t.domain??``})}n.sendLifecycle({clientLifecycle:`resolved-config`,data:{merchantId:t.merchantId,domain:t.domain??``,allowedDomains:t.allowedDomains??[],sourceUrl:window.location.href,...a&&{sdkAnonymousId:a},...r&&{pendingMergeToken:r},...i&&{sdkConfig:i}}})};e.t.isResolved&&(f(e.t.getConfig()),o.resolve()),a&&(u(await a),f(e.t.getConfig()),o.resolve());async function p(){let e=t.customizations?.css;e&&n.sendLifecycle({clientLifecycle:`modal-css`,data:{cssLink:e}})}async function m(){let e=t.customizations?.i18n;e&&n.sendLifecycle({clientLifecycle:`modal-i18n`,data:{i18n:e}})}async function h(){if(typeof window>`u`)return;let e=window.localStorage.getItem(i);e&&n.sendLifecycle({clientLifecycle:`restore-backup`,data:{backup:e}})}(await Promise.allSettled([p(),m(),h()])).some(e=>e.status===`rejected`)&&s?.track(`sdk_iframe_handshake_failed`,{reason:`asset_push`})}async function R({config:e}){let t=z(e),n=await g({config:t});if(!n){console.error(`Failed to create iframe`);return}let r=F({config:t,iframe:n});if(await r.waitForSetup,!await r.waitForConnection){console.error(`Failed to connect to client`);return}return r}function z(e){let t=d(e.metadata?.currency);return{...e,metadata:{...e.metadata,currency:t}}}Object.defineProperty(exports,`_`,{enumerable:!0,get:function(){return l}}),Object.defineProperty(exports,`a`,{enumerable:!0,get:function(){return b}}),Object.defineProperty(exports,`b`,{enumerable:!0,get:function(){return a}}),Object.defineProperty(exports,`c`,{enumerable:!0,get:function(){return h}}),Object.defineProperty(exports,`d`,{enumerable:!0,get:function(){return m}}),Object.defineProperty(exports,`f`,{enumerable:!0,get:function(){return p}}),Object.defineProperty(exports,`g`,{enumerable:!0,get:function(){return o}}),Object.defineProperty(exports,`h`,{enumerable:!0,get:function(){return u}}),Object.defineProperty(exports,`i`,{enumerable:!0,get:function(){return w}}),Object.defineProperty(exports,`l`,{enumerable:!0,get:function(){return g}}),Object.defineProperty(exports,`m`,{enumerable:!0,get:function(){return d}}),Object.defineProperty(exports,`n`,{enumerable:!0,get:function(){return F}}),Object.defineProperty(exports,`o`,{enumerable:!0,get:function(){return S}}),Object.defineProperty(exports,`p`,{enumerable:!0,get:function(){return f}}),Object.defineProperty(exports,`r`,{enumerable:!0,get:function(){return E}}),Object.defineProperty(exports,`s`,{enumerable:!0,get:function(){return C}}),Object.defineProperty(exports,`t`,{enumerable:!0,get:function(){return R}}),Object.defineProperty(exports,`u`,{enumerable:!0,get:function(){return v}}),Object.defineProperty(exports,`v`,{enumerable:!0,get:function(){return s}}),Object.defineProperty(exports,`x`,{enumerable:!0,get:function(){return r}}),Object.defineProperty(exports,`y`,{enumerable:!0,get:function(){return c}});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import{a as e,h as t,t as n,v as r}from"./sdkConfigStore-BXzz5PlK.js";import{Deferred as i,FrakRpcError as a,RpcErrorCodes as o,createRpcClient as s,jsonDecode as c}from"@frak-labs/frame-connector";import{OpenPanel as l}from"@openpanel/web";function u(e){return c(t(e))}const d=`nexus-wallet-backup`,f=`frakwallet://`;function p(){let e=navigator.userAgent;return/Android/i.test(e)&&/Chrome\/\d+/i.test(e)}function m(e){return`intent://${e.slice(13)}#Intent;scheme=frakwallet;end`}function h(e,t){let n=t?.timeout??2500,r=!1,i=()=>{document.hidden&&(r=!0)};document.addEventListener(`visibilitychange`,i);let a=p()&&g(e)?m(e):e;window.location.href=a,setTimeout(()=>{document.removeEventListener(`visibilitychange`,i),r||t?.onFallback?.()},n)}function g(e){return e.startsWith(f)}const _={eur:`fr-FR`,usd:`en-US`,gbp:`en-GB`};function v(e){return e&&e in _?e:`eur`}function y(e){return e?_[e]??_.eur:_.eur}function b(e,t){let n=y(t),r=v(t);return e.toLocaleString(n,{style:`currency`,currency:r,minimumFractionDigits:0,maximumFractionDigits:2})}function x(e){return e?`${e}Amount`:`eurAmount`}const S={id:`frak-wallet`,name:`frak-wallet`,title:`Frak Wallet`,allow:`publickey-credentials-get *; clipboard-write; web-share *`,style:{width:`0`,height:`0`,border:`0`,position:`absolute`,zIndex:2000001,top:`-1000px`,left:`-1000px`,colorScheme:`auto`}};function C({walletBaseUrl:e,config:t}){let n=document.querySelector(`#frak-wallet`);n&&n.remove();let i=document.createElement(`iframe`);i.id=S.id,i.name=S.name,i.allow=S.allow,i.style.zIndex=S.style.zIndex.toString(),w({iframe:i,isVisible:!1});let a=t?.walletUrl??e??`https://wallet.frak.id`,o=r();return i.src=`${a}/listener?clientId=${encodeURIComponent(o)}`,new Promise(e=>{i.addEventListener(`load`,()=>e(i)),document.body.appendChild(i)})}function w({iframe:e,isVisible:t}){if(!t){e.style.width=`0`,e.style.height=`0`,e.style.border=`0`,e.style.position=`fixed`,e.style.top=`-1000px`,e.style.left=`-1000px`;return}e.style.position=`fixed`,e.style.top=`0`,e.style.left=`0`,e.style.width=`100%`,e.style.height=`100%`,e.style.pointerEvents=`auto`}function T(e=`/listener`){if(!window.opener)return null;let t=t=>{try{return t.location.origin===window.location.origin&&t.location.pathname===e}catch{return!1}};if(t(window.opener))return window.opener;try{let e=window.opener.frames;for(let n=0;n<e.length;n++)if(t(e[n]))return e[n];return null}catch(t){return console.error(`[findIframeInOpener] Error finding iframe with pathname ${e}:`,t),null}}function E(){if(typeof navigator>`u`)return!1;let e=navigator.userAgent;return!!(/iPhone|iPad|iPod/i.test(e)||/Macintosh/i.test(e)&&navigator.maxTouchPoints>1)}const D=E();function O(){if(typeof navigator>`u`)return!1;let e=navigator.userAgent.toLowerCase();return e.includes(`instagram`)||e.includes(`fban`)||e.includes(`fbav`)||e.includes(`facebook`)}const k=O();function A(e){D&&e.startsWith(`https://`)?window.location.href=`x-safari-https://${e.slice(8)}`:D&&e.startsWith(`http://`)?window.location.href=`x-safari-http://${e.slice(7)}`:window.location.href=`https://backend.frak.id/common/social?u=${encodeURIComponent(e)}`}function j({perCall:e,defaults:t,productUtmContent:n}){if(e===null)return;let r=e!==void 0,i=t!==void 0&&Object.keys(t).length>0;if(!r&&!i&&!(n!==void 0&&n!==``))return;let a={...t,...e??{}},o=n??e?.utmContent;return o!==void 0&&o!==``?a.utmContent=o:delete a.utmContent,a}function M(e,t){if(typeof window>`u`)return;let n=new URL(window.location.href),r=n.searchParams.get(`sso`);r&&(t.then(()=>{e.sendLifecycle({clientLifecycle:`sso-redirect-complete`,data:{compressed:r}}),console.log(`[SSO URL Listener] Forwarded compressed SSO data to iframe`)}).catch(e=>{console.error(`[SSO URL Listener] Failed to forward SSO data:`,e)}),n.searchParams.delete(`sso`),window.history.replaceState({},``,n.toString()),console.log(`[SSO URL Listener] SSO parameter detected and URL cleaned`))}var N=class e{config;iframe;isSetupDone=!1;lastResponse=null;lastRequest=null;constructor(e,t){this.config=e,this.iframe=t,this.lastRequest=null,this.lastResponse=null}setLastResponse(e,t){this.lastResponse={message:e,response:t,timestamp:Date.now()}}setLastRequest(e){this.lastRequest={event:e,timestamp:Date.now()}}updateSetupStatus(e){this.isSetupDone=e}base64Encode(e){try{return btoa(JSON.stringify(e))}catch(e){return console.warn(`Failed to encode debug data`,e),btoa(`Failed to encode data`)}}getIframeStatus(){return this.iframe?{loading:this.iframe.hasAttribute(`loading`),url:this.iframe.src,readyState:this.iframe.contentDocument?.readyState?+(this.iframe.contentDocument.readyState===`complete`):-1,contentWindow:!!this.iframe.contentWindow,isConnected:this.iframe.isConnected}:null}getNavigatorInfo(){return navigator?{userAgent:navigator.userAgent,language:navigator.language,onLine:navigator.onLine,screenWidth:window.screen.width,screenHeight:window.screen.height,pixelRatio:window.devicePixelRatio}:null}gatherDebugInfo(e){let t=this.getIframeStatus(),n=this.getNavigatorInfo(),r=`Unknown`;return e instanceof a?r=`FrakRpcError: ${e.code} '${e.message}'`:e instanceof Error?r=e.message:typeof e==`string`&&(r=e),{timestamp:new Date().toISOString(),encodedUrl:btoa(window.location.href),encodedConfig:this.config?this.base64Encode(this.config):`no-config`,navigatorInfo:n?this.base64Encode(n):`no-navigator`,iframeStatus:t?this.base64Encode(t):`not-iframe`,lastRequest:this.lastRequest?this.base64Encode(this.lastRequest):`No Frak request logged`,lastResponse:this.lastResponse?this.base64Encode(this.lastResponse):`No Frak response logged`,clientStatus:this.isSetupDone?`setup`:`not-setup`,error:r}}static empty(){return new e}formatDebugInfo(e){let t=this.gatherDebugInfo(e);return`
|
|
2
|
+
Debug Information:
|
|
3
|
+
-----------------
|
|
4
|
+
Timestamp: ${t.timestamp}
|
|
5
|
+
URL: ${t.encodedUrl}
|
|
6
|
+
Config: ${t.encodedConfig}
|
|
7
|
+
Navigator Info: ${t.navigatorInfo}
|
|
8
|
+
IFrame Status: ${t.iframeStatus}
|
|
9
|
+
Last Request: ${t.lastRequest}
|
|
10
|
+
Last Response: ${t.lastResponse}
|
|
11
|
+
Client Status: ${t.clientStatus}
|
|
12
|
+
Error: ${t.error}
|
|
13
|
+
`.trim()}};const P=(()=>{if(typeof navigator>`u`)return!1;let e=navigator.userAgent;if(!(/iPhone|iPad|iPod/i.test(e)||/Macintosh/i.test(e)&&navigator.maxTouchPoints>1))return!1;let t=e.toLowerCase();return t.includes(`instagram`)||t.includes(`fban`)||t.includes(`fbav`)||t.includes(`facebook`)})();function F(e){e?localStorage.setItem(d,e):localStorage.removeItem(d)}function I(e,t){try{let n=new URL(e);if(!n.searchParams.has(`u`))return e;let r=z(window.location.href,t);return n.searchParams.delete(`u`),n.searchParams.append(`u`,r),n.toString()}catch{return e}}function L(e){let t=new URL(window.location.href);e&&t.searchParams.set(`fmt`,e);let n=t.protocol===`http:`?`x-safari-http`:`x-safari-https`;window.location.href=`${n}://${t.host}${t.pathname}${t.search}${t.hash}`}function R(e){return e.includes(`/common/social`)}function z(e,t){if(!t)return e;try{let n=new URL(e);return n.searchParams.set(`fmt`,t),n.toString()}catch{return`${e}${e.includes(`?`)?`&`:`?`}fmt=${encodeURIComponent(t)}`}}function B(e,t,n,r,i){if(i){let e=I(t,r);window.open(e,`_blank`);return}if(g(t)){let i=I(t,r);h(i,{onFallback:()=>{e.contentWindow?.postMessage({clientLifecycle:`deep-link-failed`,data:{originalUrl:i}},n)}})}else if(P&&R(t))L(r);else{let e=I(t,r);window.location.href=e}}function V({iframe:e,targetOrigin:t}){let n=new i;return{handleEvent:r=>{if(!(`iframeLifecycle`in r))return;let{iframeLifecycle:i,data:a}=r;switch(i){case`connected`:n.resolve(!0);break;case`do-backup`:F(a.backup);break;case`remove-backup`:localStorage.removeItem(d);break;case`show`:case`hide`:w({iframe:e,isVisible:i===`show`});break;case`redirect`:B(e,a.baseRedirectUrl,t,a.mergeToken,a.openInNewTab);break}},isConnected:n.promise}}function H({config:t,iframe:c}){let u=t?.walletUrl??`https://wallet.frak.id`,d=typeof navigator<`u`?navigator.language?.split(`-`)[0]:void 0,f=t.metadata.lang??(d===`en`||d===`fr`?d:void 0),p=t.domain??(typeof window<`u`?window.location.hostname:``);n.setCacheScope(p,f),n.reset();let m=n.isCacheFresh?void 0:n.resolve(t.domain,t.walletUrl,f),h=V({iframe:c,targetOrigin:u}),g=new i,_=Date.now(),v=new N(t,c);if(!c.contentWindow)throw new a(o.configError,`The iframe does not have a content window`);let y=s({emittingTransport:c.contentWindow,listeningTransport:window,targetOrigin:u,middleware:[{async onRequest(e,t){if(!await h.isConnected)throw new a(o.clientNotConnected,`The iframe provider isn't connected yet`);return await g.promise,t}},{onRequest(e,t){return v.setLastRequest(e),t},onResponse(e,t){return v.setLastResponse(e,t),t}}],lifecycleHandlers:{iframeLifecycle:(e,t)=>{h.handleEvent(e)}}}),b=U(y,h),x=async()=>{b(),y.cleanup(),c.remove(),e(),n.clearCache(),n.reset()},S;{console.log(`[Frak SDK] Initializing OpenPanel`),S=new l({apiUrl:`https://op-api.gcp.frak.id`,clientId:`6eacc8d7-49ac-4936-95e9-81ef29449570`,trackScreenViews:!0,trackOutgoingLinks:!0,trackAttributes:!1,filter:({type:e,payload:t})=>(e!==`track`||!t?.properties||`sdkVersion`in t.properties||(t.properties={...t.properties,sdkVersion:`1.0.0`,userAnonymousClientId:r()}),!0)}),S.setGlobalProperties({sdkVersion:`1.0.0`,userAnonymousClientId:r()}),S.init(),S.track(`sdk_initialized`,{sdkVersion:`1.0.0`});let e=!1,t=setTimeout(()=>{e||(e=!0,S?.track(`sdk_iframe_handshake_failed`,{reason:`timeout`}))},3e4);h.isConnected.then(()=>{e||(e=!0,clearTimeout(t),S?.track(`sdk_iframe_connected`,{handshake_duration_ms:Date.now()-_}))}).catch(()=>{e||(e=!0,clearTimeout(t),S?.track(`sdk_iframe_handshake_failed`,{reason:`unknown`}))})}let C=W({config:t,rpcClient:y,lifecycleManager:h,configPromise:m,contextSent:g,openPanel:S}).then(()=>v.updateSetupStatus(!0)).catch(e=>{throw g.reject(e),e});return{config:t,debugInfo:v,waitForConnection:h.isConnected,waitForSetup:C,request:y.request,listenerRequest:y.listen,destroy:x,openPanel:S}}function U(e,t){let n,r,i=()=>e.sendLifecycle({clientLifecycle:`heartbeat`});async function a(){i(),n=setInterval(i,1e3),r=setTimeout(()=>{o(),console.log(`Heartbeat timeout: connection failed`)},3e4),await t.isConnected,o()}function o(){n&&clearInterval(n),r&&clearTimeout(r)}return a(),o}async function W({config:e,rpcClient:t,lifecycleManager:i,configPromise:a,contextSent:o,openPanel:s}){await i.isConnected,M(t,i.isConnected);let c=new URL(window.location.href),l=c.searchParams.get(`fmt`)??void 0;l&&(c.searchParams.delete(`fmt`),window.history.replaceState({},``,c.toString()));let u=t=>{let r=t?.merchantId??e.metadata.merchantId??``,i=t?.domain??``,a=t?.allowedDomains??[],o=t?.sdkConfig,s=o?.attribution||e.attribution?{...e.attribution,...o?.attribution}:void 0;n.setConfig(o?{isResolved:!0,merchantId:r,domain:i,allowedDomains:a,hasRawSdkConfig:!0,name:o.name??e.metadata.name,logoUrl:o.logoUrl??e.metadata.logoUrl,homepageLink:o.homepageLink??e.metadata.homepageLink,lang:o.lang??e.metadata.lang,currency:o.currency??e.metadata.currency,hidden:o.hidden,css:o.css,translations:o.translations,placements:o.placements,components:o.components,attribution:s}:{isResolved:!0,merchantId:r,domain:i,allowedDomains:a,name:e.metadata.name,logoUrl:e.metadata.logoUrl,homepageLink:e.metadata.homepageLink,lang:e.metadata.lang,currency:e.metadata.currency,attribution:s})},f=!1,p=e=>{let n=f?void 0:l;f=!0;let i=e.hasRawSdkConfig?{name:e.name,logoUrl:e.logoUrl,homepageLink:e.homepageLink,lang:e.lang,currency:e.currency,hidden:e.hidden,css:e.css,translations:e.translations,placements:e.placements,attribution:e.attribution}:e.attribution?{attribution:e.attribution}:void 0,a=r();if(s){let t=s.global??{};s.setGlobalProperties({...t,merchantId:e.merchantId,domain:e.domain??``})}t.sendLifecycle({clientLifecycle:`resolved-config`,data:{merchantId:e.merchantId,domain:e.domain??``,allowedDomains:e.allowedDomains??[],sourceUrl:window.location.href,...a&&{sdkAnonymousId:a},...n&&{pendingMergeToken:n},...i&&{sdkConfig:i}}})};n.isResolved&&(p(n.getConfig()),o.resolve()),a&&(u(await a),p(n.getConfig()),o.resolve());async function m(){let n=e.customizations?.css;n&&t.sendLifecycle({clientLifecycle:`modal-css`,data:{cssLink:n}})}async function h(){let n=e.customizations?.i18n;n&&t.sendLifecycle({clientLifecycle:`modal-i18n`,data:{i18n:n}})}async function g(){if(typeof window>`u`)return;let e=window.localStorage.getItem(d);e&&t.sendLifecycle({clientLifecycle:`restore-backup`,data:{backup:e}})}(await Promise.allSettled([m(),h(),g()])).some(e=>e.status===`rejected`)&&s?.track(`sdk_iframe_handshake_failed`,{reason:`asset_push`})}async function G({config:e}){let t=K(e),n=await C({config:t});if(!n){console.error(`Failed to create iframe`);return}let r=H({config:t,iframe:n});if(await r.waitForSetup,!await r.waitForConnection){console.error(`Failed to connect to client`);return}return r}function K(e){let t=v(e.metadata?.currency);return{...e,metadata:{...e.metadata,currency:t}}}export{g as _,D as a,f as b,S as c,x as d,b as f,p as g,_ as h,j as i,C as l,v as m,H as n,k as o,y as p,N as r,A as s,G as t,T as u,m as v,u as x,h as y};
|
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"url": "https://twitter.com/QNivelais"
|
|
12
12
|
}
|
|
13
13
|
],
|
|
14
|
-
"version": "1.0.0-beta.
|
|
14
|
+
"version": "1.0.0-beta.b917e199",
|
|
15
15
|
"description": "Core SDK of the Frak wallet, low level library to interact directly with the frak ecosystem.",
|
|
16
16
|
"repository": {
|
|
17
17
|
"url": "https://github.com/frak-id/wallet",
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"viem": "^2.x"
|
|
92
92
|
},
|
|
93
93
|
"dependencies": {
|
|
94
|
-
"@frak-labs/frame-connector": "0.2.0-beta.
|
|
94
|
+
"@frak-labs/frame-connector": "0.2.0-beta.b917e199",
|
|
95
95
|
"@openpanel/web": "^1.2.0"
|
|
96
96
|
},
|
|
97
97
|
"devDependencies": {
|
|
@@ -25,10 +25,12 @@ describe("FrakContextManager", () => {
|
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
describe("V2 context", () => {
|
|
28
|
+
const MERCHANT_ID = "550e8400-e29b-41d4-a716-446655440000";
|
|
29
|
+
const CLIENT_ID = "550e8400-e29b-41d4-a716-446655440001";
|
|
28
30
|
const v2Context: FrakContextV2 = {
|
|
29
31
|
v: 2,
|
|
30
|
-
c:
|
|
31
|
-
m:
|
|
32
|
+
c: CLIENT_ID,
|
|
33
|
+
m: MERCHANT_ID,
|
|
32
34
|
t: 1709654400,
|
|
33
35
|
};
|
|
34
36
|
|
|
@@ -53,7 +55,7 @@ describe("FrakContextManager", () => {
|
|
|
53
55
|
it("should compress v2 context with wallet only (no clientId)", () => {
|
|
54
56
|
const v2WithWalletOnly: FrakContextV2 = {
|
|
55
57
|
v: 2,
|
|
56
|
-
m:
|
|
58
|
+
m: MERCHANT_ID,
|
|
57
59
|
t: 1709654400,
|
|
58
60
|
w: "0x1234567890123456789012345678901234567890" as Address,
|
|
59
61
|
};
|
|
@@ -66,8 +68,8 @@ describe("FrakContextManager", () => {
|
|
|
66
68
|
it("should compress v2 context with both clientId and wallet", () => {
|
|
67
69
|
const v2Hybrid: FrakContextV2 = {
|
|
68
70
|
v: 2,
|
|
69
|
-
c:
|
|
70
|
-
m:
|
|
71
|
+
c: CLIENT_ID,
|
|
72
|
+
m: MERCHANT_ID,
|
|
71
73
|
t: 1709654400,
|
|
72
74
|
w: "0x1234567890123456789012345678901234567890" as Address,
|
|
73
75
|
};
|
|
@@ -78,7 +80,7 @@ describe("FrakContextManager", () => {
|
|
|
78
80
|
});
|
|
79
81
|
|
|
80
82
|
it("should return undefined when v2 context is missing merchantId", () => {
|
|
81
|
-
const partial = { v: 2 as const, c:
|
|
83
|
+
const partial = { v: 2 as const, c: CLIENT_ID, t: 123 };
|
|
82
84
|
const result = FrakContextManager.compress(
|
|
83
85
|
partial as FrakContextV2
|
|
84
86
|
);
|
|
@@ -86,7 +88,7 @@ describe("FrakContextManager", () => {
|
|
|
86
88
|
});
|
|
87
89
|
|
|
88
90
|
it("should return undefined when v2 context is missing timestamp", () => {
|
|
89
|
-
const partial = { v: 2 as const, c:
|
|
91
|
+
const partial = { v: 2 as const, c: CLIENT_ID, m: MERCHANT_ID };
|
|
90
92
|
const result = FrakContextManager.compress(
|
|
91
93
|
partial as FrakContextV2
|
|
92
94
|
);
|
|
@@ -96,7 +98,7 @@ describe("FrakContextManager", () => {
|
|
|
96
98
|
it("should reject v2 context with a malformed wallet address", () => {
|
|
97
99
|
const partial = {
|
|
98
100
|
v: 2 as const,
|
|
99
|
-
m:
|
|
101
|
+
m: MERCHANT_ID,
|
|
100
102
|
t: 1709654400,
|
|
101
103
|
w: "0xnot-a-valid-address" as Address,
|
|
102
104
|
};
|
|
@@ -110,8 +112,8 @@ describe("FrakContextManager", () => {
|
|
|
110
112
|
it("should drop a malformed wallet but keep a valid clientId", () => {
|
|
111
113
|
const hybrid = {
|
|
112
114
|
v: 2 as const,
|
|
113
|
-
c:
|
|
114
|
-
m:
|
|
115
|
+
c: CLIENT_ID,
|
|
116
|
+
m: MERCHANT_ID,
|
|
115
117
|
t: 1709654400,
|
|
116
118
|
w: "0xnot-a-valid-address" as Address,
|
|
117
119
|
};
|
|
@@ -121,8 +123,8 @@ describe("FrakContextManager", () => {
|
|
|
121
123
|
const decompressed = FrakContextManager.decompress(compressed);
|
|
122
124
|
expect(decompressed).toEqual({
|
|
123
125
|
v: 2,
|
|
124
|
-
c:
|
|
125
|
-
m:
|
|
126
|
+
c: CLIENT_ID,
|
|
127
|
+
m: MERCHANT_ID,
|
|
126
128
|
t: 1709654400,
|
|
127
129
|
});
|
|
128
130
|
});
|
|
@@ -136,26 +138,20 @@ describe("FrakContextManager", () => {
|
|
|
136
138
|
expect(decompressed).toEqual(v2Context);
|
|
137
139
|
});
|
|
138
140
|
|
|
139
|
-
it("should
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
const {
|
|
143
|
-
"./
|
|
141
|
+
it("should reject payloads whose header reserved bits are set", async () => {
|
|
142
|
+
// Craft a valid V2 binary payload then flip a reserved bit
|
|
143
|
+
// in the header — decompress must refuse to parse it (forward-compat guard).
|
|
144
|
+
const { encodeFrakContextV2 } = await import(
|
|
145
|
+
"./frakContextV2Codec"
|
|
144
146
|
);
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
expect(result).toEqual({
|
|
154
|
-
v: 2,
|
|
155
|
-
c: "valid-client-id",
|
|
156
|
-
m: "merchant-uuid",
|
|
157
|
-
t: 1709654400,
|
|
158
|
-
});
|
|
147
|
+
const { base64urlEncode } = await import("./compression/b64");
|
|
148
|
+
const encoded = encodeFrakContextV2(v2Context);
|
|
149
|
+
expect(encoded).toBeDefined();
|
|
150
|
+
const tampered = new Uint8Array(encoded as Uint8Array);
|
|
151
|
+
tampered[0] |= 0x40; // set a reserved bit
|
|
152
|
+
const payload = base64urlEncode(tampered);
|
|
153
|
+
const result = FrakContextManager.decompress(payload);
|
|
154
|
+
expect(result).toBeUndefined();
|
|
159
155
|
});
|
|
160
156
|
});
|
|
161
157
|
|
|
@@ -169,8 +165,8 @@ describe("FrakContextManager", () => {
|
|
|
169
165
|
expect(result).toBeDefined();
|
|
170
166
|
expect(result).toHaveProperty("v", 2);
|
|
171
167
|
const v2 = result as FrakContextV2;
|
|
172
|
-
expect(v2.c).toBe(
|
|
173
|
-
expect(v2.m).toBe(
|
|
168
|
+
expect(v2.c).toBe(CLIENT_ID);
|
|
169
|
+
expect(v2.m).toBe(MERCHANT_ID);
|
|
174
170
|
expect(v2.t).toBe(1709654400);
|
|
175
171
|
});
|
|
176
172
|
});
|
|
@@ -650,8 +646,8 @@ describe("FrakContextManager", () => {
|
|
|
650
646
|
const url = "https://example.com/test";
|
|
651
647
|
const context: FrakContextV2 = {
|
|
652
648
|
v: 2,
|
|
653
|
-
c: "
|
|
654
|
-
m: "
|
|
649
|
+
c: "550e8400-e29b-41d4-a716-446655440001",
|
|
650
|
+
m: "550e8400-e29b-41d4-a716-446655440000",
|
|
655
651
|
t: 1709654400,
|
|
656
652
|
};
|
|
657
653
|
|
package/src/utils/FrakContext.ts
CHANGED
|
@@ -7,8 +7,7 @@ import type {
|
|
|
7
7
|
} from "../types";
|
|
8
8
|
import { isV2Context } from "../types";
|
|
9
9
|
import { base64urlDecode, base64urlEncode } from "./compression/b64";
|
|
10
|
-
import {
|
|
11
|
-
import { decompressJsonFromB64 } from "./compression/decompress";
|
|
10
|
+
import { decodeFrakContextV2, encodeFrakContextV2 } from "./frakContextV2Codec";
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
13
|
* URL parameter key for the Frak referral context
|
|
@@ -18,7 +17,8 @@ const contextKey = "fCtx";
|
|
|
18
17
|
/**
|
|
19
18
|
* Compress a Frak context into a URL-safe string.
|
|
20
19
|
*
|
|
21
|
-
* - V2 contexts are
|
|
20
|
+
* - V2 contexts are encoded using a compact binary layout (see
|
|
21
|
+
* {@link encodeFrakContextV2}) then base64url-encoded.
|
|
22
22
|
* - V1 contexts encode the wallet address as raw bytes (base64url).
|
|
23
23
|
*
|
|
24
24
|
* @param context - The context to compress (V1 or V2)
|
|
@@ -28,18 +28,9 @@ function compress(context?: FrakContextV1 | FrakContextV2): string | undefined {
|
|
|
28
28
|
if (!context) return;
|
|
29
29
|
try {
|
|
30
30
|
if (isV2Context(context)) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const hasValidWallet = context.w && isAddress(context.w);
|
|
35
|
-
if (!context.c && !hasValidWallet) return undefined;
|
|
36
|
-
return compressJsonToB64({
|
|
37
|
-
v: 2,
|
|
38
|
-
m: context.m,
|
|
39
|
-
t: context.t,
|
|
40
|
-
...(context.c ? { c: context.c } : {}),
|
|
41
|
-
...(hasValidWallet ? { w: context.w } : {}),
|
|
42
|
-
});
|
|
31
|
+
const encoded = encodeFrakContextV2(context);
|
|
32
|
+
if (!encoded) return undefined;
|
|
33
|
+
return base64urlEncode(encoded);
|
|
43
34
|
}
|
|
44
35
|
|
|
45
36
|
// V1 legacy: compress wallet address as raw bytes
|
|
@@ -54,7 +45,8 @@ function compress(context?: FrakContextV1 | FrakContextV2): string | undefined {
|
|
|
54
45
|
/**
|
|
55
46
|
* Decompress a base64url string back into a Frak context.
|
|
56
47
|
*
|
|
57
|
-
*
|
|
48
|
+
* V1 (exactly 20 bytes) and V2 (37, 41, or 57 bytes) are distinguished by
|
|
49
|
+
* their decoded byte length, so there is no ambiguity.
|
|
58
50
|
*
|
|
59
51
|
* @param context - The compressed context string
|
|
60
52
|
* @returns The decompressed FrakContext, or undefined on failure
|
|
@@ -62,25 +54,16 @@ function compress(context?: FrakContextV1 | FrakContextV2): string | undefined {
|
|
|
62
54
|
function decompress(context?: string): FrakContext | undefined {
|
|
63
55
|
if (!context || context.length === 0) return;
|
|
64
56
|
try {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
v: 2,
|
|
75
|
-
m: json.m,
|
|
76
|
-
t: json.t,
|
|
77
|
-
...(json.c ? { c: json.c } : {}),
|
|
78
|
-
...(hasValidWallet ? { w: json.w } : {}),
|
|
79
|
-
};
|
|
57
|
+
const bytes = base64urlDecode(context);
|
|
58
|
+
|
|
59
|
+
// V1 is a raw 20-byte wallet address; V2 binary is always longer
|
|
60
|
+
// and starts with a header byte whose low nibble is the version.
|
|
61
|
+
if (bytes.length !== 20) {
|
|
62
|
+
const v2 = decodeFrakContextV2(bytes);
|
|
63
|
+
if (v2) return v2;
|
|
64
|
+
return undefined;
|
|
80
65
|
}
|
|
81
66
|
|
|
82
|
-
// Fall back to V1: raw 20-byte address
|
|
83
|
-
const bytes = base64urlDecode(context);
|
|
84
67
|
const hex = bytesToHex(bytes, { size: 20 }) as Address;
|
|
85
68
|
if (isAddress(hex)) {
|
|
86
69
|
return { r: hex };
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import type { Address } from "viem";
|
|
2
|
+
import { describe, expect, it } from "../../tests/vitest-fixtures";
|
|
3
|
+
import type { FrakContextV2 } from "../types";
|
|
4
|
+
import { base64urlEncode } from "./compression/b64";
|
|
5
|
+
import {
|
|
6
|
+
decodeFrakContextV2,
|
|
7
|
+
encodeFrakContextV2,
|
|
8
|
+
isV2BinaryLength,
|
|
9
|
+
} from "./frakContextV2Codec";
|
|
10
|
+
|
|
11
|
+
const MERCHANT = "550e8400-e29b-41d4-a716-446655440000";
|
|
12
|
+
const CLIENT = "550e8400-e29b-41d4-a716-446655440001";
|
|
13
|
+
const WALLET = "0x1234567890123456789012345678901234567890" as Address;
|
|
14
|
+
|
|
15
|
+
describe("frakContextV2Codec", () => {
|
|
16
|
+
describe("encodeFrakContextV2 / decodeFrakContextV2 round-trip", () => {
|
|
17
|
+
it("round-trips a context with clientId only (37 bytes)", () => {
|
|
18
|
+
const ctx: FrakContextV2 = {
|
|
19
|
+
v: 2,
|
|
20
|
+
m: MERCHANT,
|
|
21
|
+
t: 1709654400,
|
|
22
|
+
c: CLIENT,
|
|
23
|
+
};
|
|
24
|
+
const encoded = encodeFrakContextV2(ctx);
|
|
25
|
+
expect(encoded).toBeInstanceOf(Uint8Array);
|
|
26
|
+
expect(encoded?.length).toBe(37);
|
|
27
|
+
expect(decodeFrakContextV2(encoded as Uint8Array)).toEqual(ctx);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("round-trips a context with wallet only (41 bytes)", () => {
|
|
31
|
+
const ctx: FrakContextV2 = {
|
|
32
|
+
v: 2,
|
|
33
|
+
m: MERCHANT,
|
|
34
|
+
t: 1709654400,
|
|
35
|
+
w: WALLET,
|
|
36
|
+
};
|
|
37
|
+
const encoded = encodeFrakContextV2(ctx);
|
|
38
|
+
expect(encoded?.length).toBe(41);
|
|
39
|
+
expect(decodeFrakContextV2(encoded as Uint8Array)).toEqual(ctx);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("round-trips a context with clientId + wallet (57 bytes)", () => {
|
|
43
|
+
const ctx: FrakContextV2 = {
|
|
44
|
+
v: 2,
|
|
45
|
+
m: MERCHANT,
|
|
46
|
+
t: 1709654400,
|
|
47
|
+
c: CLIENT,
|
|
48
|
+
w: WALLET,
|
|
49
|
+
};
|
|
50
|
+
const encoded = encodeFrakContextV2(ctx);
|
|
51
|
+
expect(encoded?.length).toBe(57);
|
|
52
|
+
expect(decodeFrakContextV2(encoded as Uint8Array)).toEqual(ctx);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("produces a base64url string shorter than the legacy JSON format", () => {
|
|
56
|
+
// Legacy reference: a typical anonymous context is ~115 JSON bytes
|
|
57
|
+
// \u2192 ~154 base64url chars. Wallet variant is ~165 \u2192 ~220 chars.
|
|
58
|
+
const ctxBoth: FrakContextV2 = {
|
|
59
|
+
v: 2,
|
|
60
|
+
m: MERCHANT,
|
|
61
|
+
t: 1709654400,
|
|
62
|
+
c: CLIENT,
|
|
63
|
+
w: WALLET,
|
|
64
|
+
};
|
|
65
|
+
const encoded = base64urlEncode(
|
|
66
|
+
encodeFrakContextV2(ctxBoth) as Uint8Array
|
|
67
|
+
);
|
|
68
|
+
// 57 bytes encodes to 76 chars (no padding).
|
|
69
|
+
expect(encoded.length).toBe(76);
|
|
70
|
+
// Sanity: far below the legacy ~220-char payload.
|
|
71
|
+
expect(encoded.length).toBeLessThan(100);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("preserves UUID case insensitivity on decode", () => {
|
|
75
|
+
const ctx: FrakContextV2 = {
|
|
76
|
+
v: 2,
|
|
77
|
+
m: MERCHANT.toUpperCase(),
|
|
78
|
+
t: 1,
|
|
79
|
+
c: CLIENT,
|
|
80
|
+
};
|
|
81
|
+
const encoded = encodeFrakContextV2(ctx);
|
|
82
|
+
const decoded = decodeFrakContextV2(encoded as Uint8Array);
|
|
83
|
+
// Decoded UUIDs are lower-case canonical.
|
|
84
|
+
expect(decoded?.m).toBe(MERCHANT);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("preserves timestamp at the uint32 boundary", () => {
|
|
88
|
+
const ctx: FrakContextV2 = {
|
|
89
|
+
v: 2,
|
|
90
|
+
m: MERCHANT,
|
|
91
|
+
t: 0xff_ff_ff_ff,
|
|
92
|
+
c: CLIENT,
|
|
93
|
+
};
|
|
94
|
+
const decoded = decodeFrakContextV2(
|
|
95
|
+
encodeFrakContextV2(ctx) as Uint8Array
|
|
96
|
+
);
|
|
97
|
+
expect(decoded?.t).toBe(0xff_ff_ff_ff);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("encodeFrakContextV2 validation", () => {
|
|
102
|
+
it("rejects non-UUID merchant id", () => {
|
|
103
|
+
expect(
|
|
104
|
+
encodeFrakContextV2({
|
|
105
|
+
v: 2,
|
|
106
|
+
m: "not-a-uuid",
|
|
107
|
+
t: 1,
|
|
108
|
+
c: CLIENT,
|
|
109
|
+
})
|
|
110
|
+
).toBeNull();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("rejects non-UUID client id", () => {
|
|
114
|
+
expect(
|
|
115
|
+
encodeFrakContextV2({
|
|
116
|
+
v: 2,
|
|
117
|
+
m: MERCHANT,
|
|
118
|
+
t: 1,
|
|
119
|
+
c: "not-a-uuid",
|
|
120
|
+
})
|
|
121
|
+
).toBeNull();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("rejects malformed wallet address", () => {
|
|
125
|
+
expect(
|
|
126
|
+
encodeFrakContextV2({
|
|
127
|
+
v: 2,
|
|
128
|
+
m: MERCHANT,
|
|
129
|
+
t: 1,
|
|
130
|
+
w: "0xnot-a-wallet" as Address,
|
|
131
|
+
})
|
|
132
|
+
).toBeNull();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("rejects contexts missing both c and w", () => {
|
|
136
|
+
expect(
|
|
137
|
+
encodeFrakContextV2({
|
|
138
|
+
v: 2,
|
|
139
|
+
m: MERCHANT,
|
|
140
|
+
t: 1,
|
|
141
|
+
} as FrakContextV2)
|
|
142
|
+
).toBeNull();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("rejects timestamps outside uint32 range", () => {
|
|
146
|
+
expect(
|
|
147
|
+
encodeFrakContextV2({
|
|
148
|
+
v: 2,
|
|
149
|
+
m: MERCHANT,
|
|
150
|
+
t: -1,
|
|
151
|
+
c: CLIENT,
|
|
152
|
+
})
|
|
153
|
+
).toBeNull();
|
|
154
|
+
expect(
|
|
155
|
+
encodeFrakContextV2({
|
|
156
|
+
v: 2,
|
|
157
|
+
m: MERCHANT,
|
|
158
|
+
t: 0x1_00_00_00_00,
|
|
159
|
+
c: CLIENT,
|
|
160
|
+
})
|
|
161
|
+
).toBeNull();
|
|
162
|
+
expect(
|
|
163
|
+
encodeFrakContextV2({
|
|
164
|
+
v: 2,
|
|
165
|
+
m: MERCHANT,
|
|
166
|
+
t: 1.5,
|
|
167
|
+
c: CLIENT,
|
|
168
|
+
})
|
|
169
|
+
).toBeNull();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("decodeFrakContextV2 validation", () => {
|
|
174
|
+
it("returns null on wrong version nibble", () => {
|
|
175
|
+
const encoded = encodeFrakContextV2({
|
|
176
|
+
v: 2,
|
|
177
|
+
m: MERCHANT,
|
|
178
|
+
t: 1,
|
|
179
|
+
c: CLIENT,
|
|
180
|
+
}) as Uint8Array;
|
|
181
|
+
const tampered = new Uint8Array(encoded);
|
|
182
|
+
tampered[0] = (tampered[0] & 0xf0) | 0x03; // flip version to 3
|
|
183
|
+
expect(decodeFrakContextV2(tampered)).toBeNull();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("returns null when reserved header bits are set", () => {
|
|
187
|
+
const encoded = encodeFrakContextV2({
|
|
188
|
+
v: 2,
|
|
189
|
+
m: MERCHANT,
|
|
190
|
+
t: 1,
|
|
191
|
+
c: CLIENT,
|
|
192
|
+
}) as Uint8Array;
|
|
193
|
+
const tampered = new Uint8Array(encoded);
|
|
194
|
+
tampered[0] |= 0x80;
|
|
195
|
+
expect(decodeFrakContextV2(tampered)).toBeNull();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("returns null when neither flag is set", () => {
|
|
199
|
+
const encoded = encodeFrakContextV2({
|
|
200
|
+
v: 2,
|
|
201
|
+
m: MERCHANT,
|
|
202
|
+
t: 1,
|
|
203
|
+
c: CLIENT,
|
|
204
|
+
}) as Uint8Array;
|
|
205
|
+
const tampered = new Uint8Array(encoded);
|
|
206
|
+
tampered[0] &= 0x0f; // clear flags, keep version
|
|
207
|
+
expect(decodeFrakContextV2(tampered)).toBeNull();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("returns null when byte length disagrees with flags", () => {
|
|
211
|
+
const encoded = encodeFrakContextV2({
|
|
212
|
+
v: 2,
|
|
213
|
+
m: MERCHANT,
|
|
214
|
+
t: 1,
|
|
215
|
+
c: CLIENT,
|
|
216
|
+
}) as Uint8Array;
|
|
217
|
+
// Drop the trailing byte to break the expected length.
|
|
218
|
+
const truncated = encoded.subarray(0, encoded.length - 1);
|
|
219
|
+
expect(decodeFrakContextV2(truncated)).toBeNull();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("returns null on an empty buffer", () => {
|
|
223
|
+
expect(decodeFrakContextV2(new Uint8Array(0))).toBeNull();
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe("isV2BinaryLength", () => {
|
|
228
|
+
it("matches exactly the three valid V2 sizes", () => {
|
|
229
|
+
expect(isV2BinaryLength(37)).toBe(true);
|
|
230
|
+
expect(isV2BinaryLength(41)).toBe(true);
|
|
231
|
+
expect(isV2BinaryLength(57)).toBe(true);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("rejects V1 size and everything else", () => {
|
|
235
|
+
expect(isV2BinaryLength(20)).toBe(false);
|
|
236
|
+
expect(isV2BinaryLength(0)).toBe(false);
|
|
237
|
+
expect(isV2BinaryLength(36)).toBe(false);
|
|
238
|
+
expect(isV2BinaryLength(58)).toBe(false);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
});
|