@frak-labs/frame-connector 0.0.1

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/README.md ADDED
@@ -0,0 +1,234 @@
1
+ # @frak-labs/frame-connector
2
+
3
+ Modern, type-safe RPC communication layer for cross-window postMessage communication.
4
+
5
+ ## Overview
6
+
7
+ This package provides a robust RPC system for communication between the Frak Wallet and SDK clients. It maintains 100% backward compatibility with the existing message format while providing a modern API with promises and async iterators.
8
+
9
+ ## Features
10
+
11
+ - **Type-safe**: Full TypeScript support with schema-based typing
12
+ - **Backward compatible**: Uses the existing `{ id, topic, data }` message format
13
+ - **Modern API**: Promises for one-shot requests, async iterators for streams
14
+ - **Functional**: No classes, just pure functions
15
+ - **Secure**: Origin validation and error handling built-in
16
+ - **Framework-agnostic**: Works with any transport mechanism
17
+
18
+ ## Architecture
19
+
20
+ ### Message Format (Unchanged)
21
+
22
+ Messages maintain the exact same format for backward compatibility:
23
+
24
+ ```typescript
25
+ {
26
+ id: string // Unique request identifier
27
+ topic: string // Method name (e.g., 'frak_sendInteraction')
28
+ data: unknown // Request parameters or response data
29
+ }
30
+ ```
31
+
32
+ ### Response Types
33
+
34
+ Each RPC method in the schema is annotated with a `ResponseType`:
35
+
36
+ - `"promise"`: One-shot request that resolves once (e.g., `frak_sendInteraction`)
37
+ - `"stream"`: Streaming request that can emit multiple values (e.g., `frak_listenToWalletStatus`)
38
+
39
+ ## Usage
40
+
41
+ ### Client-Side (SDK)
42
+
43
+ ```typescript
44
+ import { createRpcClient } from '@frak-labs/frame-connector'
45
+
46
+ // Create the client
47
+ const client = createRpcClient({
48
+ transport: window,
49
+ targetOrigin: 'https://wallet.frak.id'
50
+ })
51
+
52
+ // Connect before making requests
53
+ await client.connect()
54
+
55
+ // One-shot request (promise-based)
56
+ const result = await client.request(
57
+ 'frak_sendInteraction',
58
+ productId,
59
+ interaction,
60
+ signature
61
+ )
62
+
63
+ // Streaming request (async iterator)
64
+ for await (const status of client.stream('frak_listenToWalletStatus')) {
65
+ console.log('Wallet status:', status)
66
+
67
+ if (status.key === 'connected') {
68
+ console.log('Connected to wallet:', status.wallet)
69
+ }
70
+ }
71
+
72
+ // Cleanup when done
73
+ client.cleanup()
74
+ ```
75
+
76
+ ### Wallet-Side (Listener)
77
+
78
+ ```typescript
79
+ import { createRpcListener } from '@frak-labs/frame-connector'
80
+
81
+ // Create the listener
82
+ const listener = createRpcListener({
83
+ transport: window,
84
+ allowedOrigins: ['https://example.com', 'https://app.example.com']
85
+ })
86
+
87
+ // Register a promise handler (one-shot)
88
+ listener.handle('frak_sendInteraction', async (params, context) => {
89
+ const [productId, interaction, signature] = params
90
+
91
+ // Process the interaction
92
+ const hash = await processInteraction(productId, interaction, signature)
93
+
94
+ return {
95
+ status: 'success',
96
+ hash
97
+ }
98
+ })
99
+
100
+ // Register a stream handler (multiple emissions)
101
+ listener.handleStream('frak_listenToWalletStatus', (params, emit, context) => {
102
+ // Emit initial status
103
+ emit({ key: 'connecting' })
104
+
105
+ // Set up listener for wallet changes
106
+ const unsubscribe = walletState.subscribe((state) => {
107
+ if (state.connected) {
108
+ emit({ key: 'connected', wallet: state.address })
109
+ } else {
110
+ emit({ key: 'not-connected' })
111
+ }
112
+ })
113
+
114
+ // Return cleanup function (optional)
115
+ return () => unsubscribe()
116
+ })
117
+
118
+ // Cleanup when done
119
+ listener.cleanup()
120
+ ```
121
+
122
+ ## API Reference
123
+
124
+ ### Client
125
+
126
+ #### `createRpcClient(config: RpcClientConfig): RpcClient`
127
+
128
+ Creates an RPC client for SDK-side communication.
129
+
130
+ **Config:**
131
+ - `transport: RpcTransport` - The transport mechanism (e.g., `window`)
132
+ - `targetOrigin: string` - Target origin for postMessage
133
+ - `handshake?: HandshakeConfig` - Optional handshake configuration
134
+
135
+ **Returns:**
136
+ - `connect(): Promise<void>` - Establish connection
137
+ - `request(method, ...params): Promise<T>` - Make one-shot request
138
+ - `stream(method, ...params): AsyncIterableIterator<T>` - Make streaming request
139
+ - `getState(): ConnectionState` - Get current connection state
140
+ - `cleanup(): void` - Clean up resources
141
+
142
+ ### Listener
143
+
144
+ #### `createRpcListener(config: RpcListenerConfig): RpcListener`
145
+
146
+ Creates an RPC listener for Wallet-side communication.
147
+
148
+ **Config:**
149
+ - `transport: RpcTransport` - The transport mechanism (e.g., `window`)
150
+ - `allowedOrigins: string | string[]` - Allowed origins for security
151
+
152
+ **Returns:**
153
+ - `handle(method, handler): void` - Register promise handler
154
+ - `handleStream(method, handler): void` - Register stream handler
155
+ - `unregister(method): void` - Unregister a handler
156
+ - `cleanup(): void` - Clean up resources
157
+
158
+ ## Type Safety
159
+
160
+ The package provides full type safety through the RPC schema:
161
+
162
+ ```typescript
163
+ // Method names are type-checked
164
+ client.request('frak_sendInteraction', ...) // ✓ Valid
165
+ client.request('invalid_method', ...) // ✗ Type error
166
+
167
+ // Parameters are type-checked
168
+ client.request('frak_sendInteraction', productId, interaction) // ✓ Valid
169
+ client.request('frak_sendInteraction', 'wrong-params') // ✗ Type error
170
+
171
+ // Return types are inferred
172
+ const result = await client.request('frak_sendInteraction', ...)
173
+ // result is typed as SendInteractionReturnType
174
+ ```
175
+
176
+ ## Error Handling
177
+
178
+ Errors are thrown as `FrakRpcError` with standard error codes:
179
+
180
+ ```typescript
181
+ import { RpcErrorCodes } from '@frak-labs/frame-connector'
182
+
183
+ try {
184
+ const result = await client.request('frak_sendInteraction', ...)
185
+ } catch (error) {
186
+ if (error.code === RpcErrorCodes.userRejected) {
187
+ console.log('User rejected the request')
188
+ } else if (error.code === RpcErrorCodes.invalidParams) {
189
+ console.error('Invalid parameters:', error.message)
190
+ }
191
+ }
192
+ ```
193
+
194
+ ## Development
195
+
196
+ This package is part of the Frak Wallet monorepo and uses Bun as the package manager.
197
+
198
+ ```bash
199
+ # Install dependencies (from repo root)
200
+ bun install
201
+
202
+ # Type check
203
+ bun run typecheck
204
+
205
+ # Format
206
+ bun run format
207
+ ```
208
+
209
+ ## Migration Guide
210
+
211
+ For existing code using the old callback-based API, migration to the new API is straightforward:
212
+
213
+ ### Before (callback-based):
214
+
215
+ ```typescript
216
+ // Old SDK client
217
+ await transport.listenerRequest(
218
+ { method: 'frak_listenToWalletStatus' },
219
+ (status) => {
220
+ console.log('Status:', status)
221
+ }
222
+ )
223
+ ```
224
+
225
+ ### After (async iterator):
226
+
227
+ ```typescript
228
+ // New RPC client
229
+ for await (const status of client.stream('frak_listenToWalletStatus')) {
230
+ console.log('Status:', status)
231
+ }
232
+ ```
233
+
234
+ The underlying message format remains unchanged, ensuring 100% backward compatibility.
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";let __rslib_import_meta_url__="undefined"==typeof document?new(require("url".replace("",""))).URL("file:"+__filename).href:document.currentScript&&document.currentScript.src||new URL("main.js",document.baseURI).href;var __webpack_require__={};__webpack_require__.d=(e,r)=>{for(var t in r)__webpack_require__.o(r,t)&&!__webpack_require__.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},__webpack_require__.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),__webpack_require__.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var __webpack_exports__={};__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,{createRpcListener:()=>createRpcListener,RpcErrorCodes:()=>RpcErrorCodes,ClientNotFound:()=>ClientNotFound,InternalError:()=>InternalError,decompressJson:()=>decompressJson,MethodNotFoundError:()=>MethodNotFoundError,compressJson:()=>compressJson,Deferred:()=>Deferred,createClientCompressionMiddleware:()=>createClientCompressionMiddleware,createListenerCompressionMiddleware:()=>createListenerCompressionMiddleware,createRpcClient:()=>createRpcClient,hashAndCompressData:()=>hashAndCompressData,FrakRpcError:()=>FrakRpcError,decompressDataAndCheckHash:()=>decompressDataAndCheckHash});let RpcErrorCodes={parseError:-32700,invalidRequest:-32600,methodNotFound:-32601,invalidParams:-32602,internalError:-32603,serverError:-32e3,clientNotConnected:-32001,configError:-32002,corruptedResponse:-32003,clientAborted:-32004,walletNotConnected:-32005,serverErrorForInteractionDelegation:-32006,userRejected:-32007};class FrakRpcError extends Error{code;data;constructor(e,r,t){super(r),this.code=e,this.data=t}toJSON(){return{code:this.code,message:this.message,data:this.data}}}class MethodNotFoundError extends FrakRpcError{constructor(e,r){super(RpcErrorCodes.methodNotFound,e,{method:r})}}class InternalError extends FrakRpcError{constructor(e){super(RpcErrorCodes.internalError,e)}}class ClientNotFound extends FrakRpcError{constructor(){super(RpcErrorCodes.clientNotConnected,"Client not found")}}class Deferred{_promise;_resolve;_reject;constructor(){this._promise=new Promise((e,r)=>{this._resolve=e,this._reject=r})}get promise(){return this._promise}resolve=e=>{this._resolve?.(e)};reject=e=>{this._reject?.(e)}}function createRpcClient(e){let{emittingTransport:r,listeningTransport:t,targetOrigin:o,middleware:n=[],lifecycleHandlers:s}=e,a=new Map;async function i(e){try{"clientLifecycle"in e&&s?.clientLifecycle?await s.clientLifecycle(e,{origin:o,source:null}):"iframeLifecycle"in e&&s?.iframeLifecycle&&await s.iframeLifecycle(e,{origin:o,source:null})}catch(e){console.error("[RPC Client] Lifecycle handler error:",e)}}async function c(e){let r={origin:o,source:null};for(let t of n)t.onRequest&&await t.onRequest(e,r);return e}async function d(e,r){let t={origin:o,source:null},s=r;for(let r of n)r.onResponse&&(s=await r.onResponse(e,s,t));return s}async function l(e){var r,t;let n;try{let r=new URL(e.origin).origin.toLowerCase(),t=new URL(o).origin.toLowerCase();if(r!==t)return void console.log("Not expected origin",r,t)}catch(e){console.error("[RPC Client] Invalid origin",e);return}if("object"==typeof(r=e.data)&&r&&("clientLifecycle"in r||"iframeLifecycle"in r))return void await i(e.data);if(!("object"==typeof(t=e.data)&&t&&"id"in t&&"topic"in t&&"data"in t))return;try{let r=e.data.data,t=r instanceof Uint8Array||ArrayBuffer.isView(r);n=await d(e.data,t?{result:r}:r)}catch(e){console.error("[RPC Client] Middleware error on response:",e);return}let s=a.get(e.data.id);s&&s(n)}async function p(e){let t=e;try{t=await c(e)}catch(e){throw console.error("[RPC Client] Middleware error on request:",e),e}r.postMessage(t,o)}function u(){return`${Date.now()}-${Math.random().toString(36).substring(2,9)}`}return t.addEventListener("message",l),{request:function(e){let r=u(),t=new Deferred;return a.set(r,e=>{e.error?t.reject(new FrakRpcError(e.error.code,e.error.message,e.error.data)):t.resolve(e.result),a.delete(r)}),p({id:r,topic:e.method,data:{method:e.method,params:e.params}}).catch(e=>{a.delete(r),t.reject(e)}),t.promise},listen:function(e,r){let t=u();return a.set(t,e=>{e.error?(console.error("[RPC Client] Listener error:",e.error),a.delete(t)):r(e.result)}),p({id:t,topic:e.method,data:{method:e.method,params:e.params}}).catch(e=>{console.error("[RPC Client] Failed to send listener request:",e),a.delete(t)}),()=>{a.delete(t)}},sendLifecycle:function(e){r.postMessage(e,o)},cleanup:function(){t.removeEventListener("message",l),a.clear()}}}function createRpcListener(e){let{transport:r,allowedOrigins:t,middleware:o=[],lifecycleHandlers:n}=e,s=Array.isArray(t)?t:[t],a=new Map,i=new Map;async function c(e,r){let t=r;for(let r of o)r.onRequest&&(t=await r.onRequest(e,t));return t}async function d(e,r,t){let n=r;for(let r of o)r.onResponse&&(n=await r.onResponse(e,n,t));return n}function l(e,r,t,o,n){if(!e)return void console.error("[RPC Listener] No source to send response to");let s={id:t,topic:o,data:!n.error&&(n.result instanceof Uint8Array||ArrayBuffer.isView(n.result))?n.result:n};"postMessage"in e&&"function"==typeof e.postMessage&&e.postMessage(s,{targetOrigin:r})}function p(e,r,t,o,n){l(e,r,t,o,{error:n instanceof FrakRpcError?n.toJSON():{code:RpcErrorCodes.internalError,message:n.message}})}async function u(e,r){try{"clientLifecycle"in e&&n?.clientLifecycle?await n.clientLifecycle(e,r):"iframeLifecycle"in e&&n?.iframeLifecycle&&await n.iframeLifecycle(e,r)}catch(e){console.error("[RPC Listener] Lifecycle handler error:",e)}}async function _(e){var r,t;let o;if(!function(e){try{if(s.includes("*"))return!0;let r=new URL(e).origin.toLowerCase();return s.some(e=>{let t=new URL(e).origin.toLowerCase();return r===t})}catch(e){return console.error("[RPC Listener] Invalid origin",e),!1}}(e.origin))return void console.warn("[RPC Listener] Message from disallowed origin:",e.origin);let n={origin:e.origin,source:e.source};if("object"==typeof(r=e.data)&&r&&("clientLifecycle"in r||"iframeLifecycle"in r))return void await u(e.data,n);if("object"==typeof(t=e.data)&&t&&"id"in t&&"topic"in t&&"data"in t){try{o=await c(e.data,n)}catch(r){p(e.source,e.origin,e.data.id,e.data.topic,r instanceof Error?r:Error(String(r)));return}try{await f(e,o)}catch(r){p(e.source,e.origin,e.data.id,e.data.topic,r instanceof Error?r:Error(String(r)))}}}async function f(e,r){let{id:t,topic:o,data:n}=e.data,s=a.get(o);if(s){let a=await s(n,r),i=await d(e.data,{result:a},r);l(e.source,e.origin,t,o,i);return}let c=i.get(o);if(c){let s=async n=>{let s;try{s=await d(e.data,{result:n},r)}catch(e){console.error("[RPC Listener] Middleware failed on stream chunk:",e);return}l(e.source,e.origin,t,o,s)};await c(n,s,r);return}console.error("[RPC Listener] No handler found for method:",{topic:o,handlers:i.keys(),promiseHandler:a.keys()}),p(e.source,e.origin,t,o,new FrakRpcError(RpcErrorCodes.methodNotFound,`Method not found: ${o}`))}return r.addEventListener("message",_),{handle:function(e,r){a.set(e,r)},handleStream:function(e,r){i.set(e,r)},unregister:function(e){a.delete(e),i.delete(e)},cleanup:function(){r.removeEventListener("message",_),a.clear(),i.clear()}}}let index_js_namespaceObject=require("@jsonjoy.com/json-pack/lib/cbor/index.js"),external_viem_namespaceObject=require("viem"),encoder=new index_js_namespaceObject.CborEncoder,decoder=new index_js_namespaceObject.CborDecoder;function hashAndCompressData(e){let r={...e,validationHash:hashJson(e??{})};return encoder.encode(r)}function decompressDataAndCheckHash(e){if(!e.length)throw new FrakRpcError(RpcErrorCodes.corruptedResponse,"Missing compressed data");let r=decompressJson(e);if(!r)throw new FrakRpcError(RpcErrorCodes.corruptedResponse,"Invalid compressed data");if(!r?.validationHash)throw new FrakRpcError(RpcErrorCodes.corruptedResponse,"Missing validation hash");let{validationHash:t,...o}=r,n=hashJson(o);if(n!==r.validationHash)throw console.warn("Validation error",{validationHash:t,rawResultData:o,parsedData:r,expectedValidationHashes:n,recomputedHash:hashJson(void 0)}),new FrakRpcError(RpcErrorCodes.corruptedResponse,"Invalid validation hash");return r}function compressJson(e){return encoder.encode(e)}function decompressJson(e){try{return decoder.decode(e)}catch(r){return console.error("Invalid compressed data",{e:r,data:e}),null}}function hashJson(e){return(0,external_viem_namespaceObject.sha256)(encoder.encode(e))}let createClientCompressionMiddleware=()=>({onRequest:(e,r)=>{if(e.data instanceof Uint8Array||ArrayBuffer.isView(e.data))return r;try{e.data=hashAndCompressData(e.data)}catch(e){console.error("[Compression Middleware] Failed to compress request",e)}return r},onResponse:(e,r,t)=>{if(r.error||!(r.result instanceof Uint8Array||ArrayBuffer.isView(r.result)))return r;try{let{validationHash:e,...t}=decompressDataAndCheckHash(r.result);"object"==typeof t&&null!==t&&"result"in t?r.result=t.result:r.result=t}catch(e){console.error("[Compression Middleware] Failed to decompress response",e)}return r}}),createListenerCompressionMiddleware=()=>({onRequest:(e,r)=>{if(!(e.data instanceof Uint8Array||ArrayBuffer.isView(e.data)))return r;try{let{validationHash:r,...t}=decompressDataAndCheckHash(e.data);"object"==typeof t&&"params"in t?e.data=t.params:e.data=t}catch(e){throw console.error("[Compression Middleware] Failed to decompress request",e),e}return r},onResponse:(e,r,t)=>{if(r.error||r.result instanceof Uint8Array||ArrayBuffer.isView(r.result))return r;try{let t={method:e.topic,result:r.result};r.result=hashAndCompressData(t)}catch(e){console.error("[Compression Middleware] Failed to compress response",e)}return r}});for(var __webpack_i__ in exports.ClientNotFound=__webpack_exports__.ClientNotFound,exports.Deferred=__webpack_exports__.Deferred,exports.FrakRpcError=__webpack_exports__.FrakRpcError,exports.InternalError=__webpack_exports__.InternalError,exports.MethodNotFoundError=__webpack_exports__.MethodNotFoundError,exports.RpcErrorCodes=__webpack_exports__.RpcErrorCodes,exports.compressJson=__webpack_exports__.compressJson,exports.createClientCompressionMiddleware=__webpack_exports__.createClientCompressionMiddleware,exports.createListenerCompressionMiddleware=__webpack_exports__.createListenerCompressionMiddleware,exports.createRpcClient=__webpack_exports__.createRpcClient,exports.createRpcListener=__webpack_exports__.createRpcListener,exports.decompressDataAndCheckHash=__webpack_exports__.decompressDataAndCheckHash,exports.decompressJson=__webpack_exports__.decompressJson,exports.hashAndCompressData=__webpack_exports__.hashAndCompressData,__webpack_exports__)-1===["ClientNotFound","Deferred","FrakRpcError","InternalError","MethodNotFoundError","RpcErrorCodes","compressJson","createClientCompressionMiddleware","createListenerCompressionMiddleware","createRpcClient","createRpcListener","decompressDataAndCheckHash","decompressJson","hashAndCompressData"].indexOf(__webpack_i__)&&(exports[__webpack_i__]=__webpack_exports__[__webpack_i__]);Object.defineProperty(exports,"__esModule",{value:!0});