@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.
@@ -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.a7645eaa",
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.a7645eaa",
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: "test-client-id-uuid",
31
- m: "merchant-uuid-1234",
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: "merchant-uuid-1234",
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: "test-client-id-uuid",
70
- m: "merchant-uuid-1234",
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: "c", t: 123 };
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: "c", m: "m" };
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: "merchant-uuid",
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: "valid-client-id",
114
- m: "merchant-uuid",
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: "valid-client-id",
125
- m: "merchant-uuid",
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 drop malformed `w` field on decompress (defense against crafted URLs)", async () => {
140
- // Manually compress a payload bypassing client-side validation
141
- // by reaching into the underlying compressJsonToB64.
142
- const { compressJsonToB64 } = await import(
143
- "./compression/compress"
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 malformed = compressJsonToB64({
146
- v: 2,
147
- c: "valid-client-id",
148
- m: "merchant-uuid",
149
- t: 1709654400,
150
- w: "0xnot-a-real-address",
151
- });
152
- const result = FrakContextManager.decompress(malformed);
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("test-client-id-uuid");
173
- expect(v2.m).toBe("merchant-uuid-1234");
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: "client-id",
654
- m: "merchant-id",
649
+ c: "550e8400-e29b-41d4-a716-446655440001",
650
+ m: "550e8400-e29b-41d4-a716-446655440000",
655
651
  t: 1709654400,
656
652
  };
657
653
 
@@ -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 { compressJsonToB64 } from "./compression/compress";
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 serialized as compressed JSON (base64url).
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
- // Runtime validation: m + t are always required, and at least one of
32
- // c (anonymous fingerprint) or w (valid wallet) must be present.
33
- if (!context.m || !context.t) return undefined;
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
- * Attempts V2 JSON decompression first, then falls back to V1 raw bytes.
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
- // Try V2 JSON first — V2 payloads are longer than V1's 20-byte address
66
- const json = decompressJsonFromB64<FrakContextV2>(context);
67
- if (json && typeof json === "object" && json.v === 2) {
68
- if (!json.m || !json.t) return undefined;
69
- // Validate `w` with isAddress() — protects self-referral checks and
70
- // URL replacement from crafted payloads carrying a malformed wallet.
71
- const hasValidWallet = json.w && isAddress(json.w);
72
- if (!json.c && !hasValidWallet) return undefined;
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
+ });