UNPKG

@orb-labs/orby-core-mini-react-native

Version:

React Native library for injecting Orby into dapps via WebView

798 lines (730 loc) 23.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.orbyInjectionScriptForDapps = void 0; exports.orbyInjectionScriptForDapps = ` (function () { const originalFetch = window.fetch; const OriginalXMLHttpRequest = window.XMLHttpRequest; const rpcToChainIdMap = new Map(); const rpcRequestMap = new Map(); const callbackMap = new Map(); let requestTimeoutLength = 10_000; let appRule; window.fetch = new Proxy(originalFetch, { apply: async function (target, thisArg, args) { try { const signal = args && args.length > 1 && args[1] ? args[1].signal : undefined; if (signal && signal.aborted) { return Promise.reject( new DOMException("Request was aborted", "AbortError") ); } const result = await handleInterceptedRequest(args, "FETCH", { target, thisArg, signal, }); if (result.intercepted) { return convertToResponseObject(result.plaintextResponse); } if (args && args.length > 1) args[1] = { ...args[1], signal }; return Reflect.apply(target, thisArg, args); } catch (error) { console.error(error); return Reflect.apply(target, thisArg, args); } }, }); window.XMLHttpRequest = function() { const xhr = new OriginalXMLHttpRequest(); let orbyResponse = null; const state = { method: null, requestUrl: null, headers: null, allOpenArgs: null, }; // Store the original methods before they get wrapped const originalOpen = xhr.open; const originalSend = xhr.send; const originalSetRequestHeader = xhr.setRequestHeader; // Create new open method xhr.open = function (...args) { state.allOpenArgs = args; state.method = args[0]; state.requestUrl = args[1]; state.headers = new Map(); return originalOpen.apply(xhr, args); }; xhr.setRequestHeader = function (header, value) { state.headers.set(header, value); return originalSetRequestHeader.apply(xhr, [header, value]); }; xhr.send = function (body) { try { const originalReadyStateChange = xhr.onreadystatechange; xhr.onreadystatechange = function () { if (xhr.readyState == 4 && orbyResponse.intercepted) { returnOrbyResponse(xhr, orbyResponse.plaintextResponse); } if (originalReadyStateChange) { originalReadyStateChange.call(xhr); } }; const xhrMetadata = { timeout: xhr.timeout, withCredentials: xhr.withCredentials, responseType: xhr.responseType, allOpenArgs: state.allOpenArgs, }; const args = [ state.requestUrl, { method: state.method, headers: state.headers, body }, xhrMetadata, ]; handleInterceptedRequest(args, "XHR", xhrMetadata) .then((result) => { orbyResponse = result; return originalSend.call(xhr, body); }) .catch(() => { return originalSend.call(xhr, body); }); } catch { return originalSend.call(xhr, body); } }; return xhr; } // handles the intercepted request async function handleInterceptedRequest( args, requestType, fetchOrXhrMetadata ) { await fetchAppRule(); const { requiresInterception, requestCategorizations } = passRequestThroughAppRule(args); let plaintextResponse; if (requiresInterception) { plaintextResponse = await processRequestByCategorizations( args, requestType, requestCategorizations, fetchOrXhrMetadata ); } return { intercepted: requiresInterception, plaintextResponse }; } // fetches app rule from Orby if its not already available. async function fetchAppRule() { if (!appRule) { const res = await sendOrbyRequest({ action: "getAppRule", id: crypto.randomUUID(), appUrl: window.location.href, }); appRule = res.appRule; requestTimeoutLength = res.requestTimeoutLength; } } // processes requests by the categorizations we got from orby. async function processRequestByCategorizations( args, requestType, requestCategorizations, fetchOrXhrMetadata ) { if (requestCategorizations.length <= 1) { return await processRequest( args, requestType, requestCategorizations[0], fetchOrXhrMetadata ); } else if (shouldProcessTogether(requestCategorizations)) { let newRequestCategorization = { appRuleApplies: requestCategorizations.every((c) => c.appRuleApplies), isJsonRpcCall: requestCategorizations.every((c) => c.isJsonRpcCall), requiresRequestAlteration: requestCategorizations.every( (c) => c.requiresRequestAlteration ), requiresRequestRedirection: requestCategorizations.every( (c) => c.requiresRequestRedirection ), requiresResponseAlteration: requestCategorizations.every( (c) => c.requiresResponseAlteration ), }; return await processRequest( args, requestType, newRequestCategorization, fetchOrXhrMetadata ); } else { return await processRequestsIndividually( args, requestType, requestCategorizations, fetchOrXhrMetadata ); } } // returns whether requests should be processed together or not. function shouldProcessTogether(requestCategorizations) { return ( (requestCategorizations.every((c) => c.appRuleApplies) && requestCategorizations.every((c) => c.isJsonRpcCall)) || (requestCategorizations.every((c) => !c.appRuleApplies) && requestCategorizations.every((c) => !c.isJsonRpcCall)) ); } // processes each request in a set individually. async function processRequestsIndividually( args, requestType, requestCategorizations, fetchOrXhrMetadata ) { const requestBody = args[1] || {}; const payload = JSON.parse(stringifyBody(requestBody.body)); const promises = requestCategorizations.map(async (c, index) => { const newArgs = JSON.parse(JSON.stringify(args)); newArgs[1] = { ...newArgs[1], body: JSON.stringify([payload[index]]), }; return await processRequest(newArgs, requestType, c, fetchOrXhrMetadata); }); const responses = await Promise.all(promises); const resultsPromises = responses.map(async (curr) => { return curr.body[0]; }); const combinedResult = await Promise.all(resultsPromises); return { ...responses[0], body: combinedResult }; } // preocesses requests async function processRequest( args, requestType, requestCategorization, fetchOrXhrMetadata ) { let response; if (requestCategorization.requiresRequestAlteration) { const newArgs = await alterRequest(args, fetchOrXhrMetadata); response = await sendOrdinaryRequest( newArgs, requestType, fetchOrXhrMetadata ); } else if (requestCategorization.requiresRequestRedirection) { response = await redirectRequest( args, requestType, requestCategorization, fetchOrXhrMetadata ); } else { response = await sendOrdinaryRequest( args, requestType, fetchOrXhrMetadata ); } let finalResponse = await alterResponse( args, response, requestCategorization, fetchOrXhrMetadata ); if (requestType == "XHR") formatXHRResponse(finalResponse); return finalResponse; } // alter request async function alterRequest(args, fetchOrXhrMetadata) { const result = await sendOrbyRequest( { action: "alterRequest", id: crypto.randomUUID(), appUrl: window.location.href, request: args, }, fetchOrXhrMetadata.signal ); if (result.success && result.response) { const clonedArgs = structuredClone(args); if (result.response.body.requestUrl) clonedArgs[0] = result.response.body.requestUrl; if (result.response.body.requestBody) clonedArgs[1].body = result.response.body.requestBody.body; return clonedArgs; } return args; } // redirect request async function redirectRequest( args, requestType, requestCategorization, fetchOrXhrMetadata ) { let message = { isJsonRpcCall: requestCategorization.isJsonRpcCall }; if (requestCategorization.isJsonRpcCall) message.rpcChainId = await getUniqueChainIdentifier(args[0]); const result = await sendOrbyRequest( { action: "redirectRequest", id: crypto.randomUUID(), appUrl: window.location.href, request: args, ...message, }, fetchOrXhrMetadata.signal ); if (!result.success) { return await sendOrdinaryRequest(args, requestType, fetchOrXhrMetadata); } else if (result.success && requestType === "FETCH") { return result.response; } else { const normalResponse = await sendOrdinaryRequest( args, requestType, fetchOrXhrMetadata ); return { ...normalResponse, body: result.response.body }; } } // alter response async function alterResponse( args, currentResponse, requestCategorization, fetchOrXhrMetadata ) { if (requestCategorization.requiresResponseAlteration) { if (fetchOrXhrMetadata.signal && args.length > 1) { delete args[1].signal; } const alteredResponse = await sendOrbyRequest( { action: "alterResponse", id: crypto.randomUUID(), appUrl: window.location.href, request: [args[0], currentResponse.body, args], }, fetchOrXhrMetadata.signal ); if (alteredResponse.success) { return { ...currentResponse, body: alteredResponse.response.body }; } } return currentResponse; } // sends ordinary fetch and chr requests async function sendOrdinaryRequest(args, requestType, fetchOrXhrMetadata) { if (requestType == "FETCH") { return await handleFetchRequest(args, fetchOrXhrMetadata); } else if (requestType == "XHR") { return await handleXHRequest(args, fetchOrXhrMetadata); } } // handle original xhr request async function handleXHRequest(args, fetchOrXhrMetadata) { return new Promise((resolve, reject) => { const localxhr = new window.originalXMLHttpRequest(); localxhr.onreadystatechange = function () { if (localxhr.readyState === 4) { resolve(captureXHRResponse(localxhr)); } }; localxhr.onerror = () => { reject(new Error("Network error occurred")); }; localxhr.timeout = fetchOrXhrMetadata.timeout; localxhr.withCredentials = fetchOrXhrMetadata.withCredentials; localxhr.responseType = fetchOrXhrMetadata.responseType; localxhr.open(...fetchOrXhrMetadata.allOpenArgs); args[1].headers.forEach((value, key) => { localxhr.setRequestHeader(key, value); }); // Send the request if (args[1].body) { localxhr.send(args[1].body); } else { localxhr.send(); } }); } // handles original fetch request async function handleFetchRequest(args, fetchOrXhrMetadata) { const { target, thisArg, signal } = fetchOrXhrMetadata; if (args && args.length > 1) args[1] = { ...args[1], signal }; const response = await Reflect.apply(target, thisArg, args); const data = await response.json(); return { headers: [...response.headers.entries()], status: response.status, statusText: response.statusText, body: data, }; } // Function that receives a response from Orby. window.receiveOrbyResponse = function (message) { if (typeof payload === 'string') { try { payload = JSON.parse(payload); } catch (e) { return; } } const { id, response } = JSON.parse(message); if (type && id && callbackMap.has(id)) { const { resolve, timeOut } = callbackMap.get(id); resolve(response); clearTimeout(timeOut); callbackMap.delete(id); } }; // Function that sends a request to the Orby. function sendOrbyRequest(message, signal) { return new Promise((resolve, reject) => { const timeOut = setTimeout( () => timeOutRequest(message), requestTimeoutLength ); callbackMap.set(message.id, { resolve, reject, timeOut }); if (signal) { signal.addEventListener("abort", () => abortRequest(message), { once: true, }); if (message.request && message.request.length > 1) { delete message.request[1].signal; } } window.ReactNativeWebView.postMessage( JSON.stringify({ type: "ODI_REQUEST", ...message }) ); }); } // Pass a request through the app rules function passRequestThroughAppRule(request) { const requestUrl = request[0]; const requestBody = request[1] || {}; // return if there are not requestBody or if the app domain is prohibited. if (appRule.isProhibitedApp) { return { errored: true, requiresInterception: false, requestCategorizations: [ { appRuleApplies: false, isJsonRpcCall: false, requiresRequestAlteration: false, requiresResponseAlteration: false, requiresRequestRedirection: false, }, ], }; } // check if the request requires app specific request alteration. const requiresRequestAlteration = appRule.appSpecificRequestAlterationRules.some( (appSpecificRequestAlterationRule) => { const regex = new RegExp(appSpecificRequestAlterationRule); return regex.test(requestUrl); } ); // check if the request requires app specific response alteration. const requiresResponseAlteration = appRule.appSpecificResponseAlterationRules.some( (appSpecificResponseAlterationRule) => { const regex = new RegExp(appSpecificResponseAlterationRule); return regex.test(requestUrl); } ); // check if the request requires app specific request redirection. const requiresRequestRedirection = appRule.appSpecificRequestRedirectRules.some( (appSpecificRequestRedirectRule) => { const regex = new RegExp(appSpecificRequestRedirectRule); return regex.test(requestUrl); } ); if ( requiresRequestAlteration || requiresRequestRedirection || requiresResponseAlteration ) { return { errored: false, requiresInterception: true, requestCategorizations: [ { appRuleApplies: true, isJsonRpcCall: false, requiresRequestAlteration, requiresResponseAlteration, requiresRequestRedirection, }, ], }; } else if (requestBody && requestBody.body) { try { const payload = JSON.parse(stringifyBody(requestBody.body)); if (Array.isArray(payload)) { const requestCategorizations = payload.map((operation) => appRuleAppliesAndIsJsonRpcCall(operation) ); return { errored: false, requiresInterception: requestCategorizations.some( (c) => c.appRuleApplies ), requestCategorizations, }; } else { const requestCategorization = appRuleAppliesAndIsJsonRpcCall(payload); return { errored: false, requiresInterception: requestCategorization.appRuleApplies, requestCategorizations: [requestCategorization], }; } } catch (error) { return { errored: true, requiresInterception: false, requestCategorizations: [ { appRuleApplies: false, isJsonRpcCall: false, requiresRequestAlteration: false, requiresResponseAlteration: false, requiresRequestRedirection: false, }, ], }; } } else { return { errored: false, requiresInterception: false, requestCategorizations: [ { appRuleApplies: false, isJsonRpcCall: false, requiresRequestAlteration: false, requiresResponseAlteration: false, requiresRequestRedirection: false, }, ], }; } } function appRuleAppliesAndIsJsonRpcCall(payload) { const isJsonRpcCall = payload.hasOwnProperty("jsonrpc") && payload.jsonrpc !== undefined; const requiresRequestRedirection = isJsonRpcCall && appRule.rpcRedirectRules.includes(payload.method); return { appRuleApplies: requiresRequestRedirection, isJsonRpcCall, requiresRequestAlteration: false, requiresResponseAlteration: false, requiresRequestRedirection, }; } // Function that fetches the rpc chain id async function getUniqueChainIdentifier(appRpcUrl) { try { if (rpcToChainIdMap.has(appRpcUrl)) { return rpcToChainIdMap.get(appRpcUrl); } else if (rpcRequestMap.has(appRpcUrl)) { return rpcRequestMap.get(appRpcUrl); } const fetchPromise = (async () => { const responses = appRule.rpcFunctionsForUniqueChainIdentifiers.map( async (rpcFunc) => { const response = await originalFetch(appRpcUrl, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ id: Math.floor(Math.random() * 1000) + 1, jsonrpc: "2.0", method: rpcFunc, params: [], }), }); return response.json(); } ); const results = await Promise.allSettled(responses); const successfulResults = results.filter( (result) => result.status === "fulfilled" ); if (successfulResults.length === 0) { throw new Error("Unable to fetch unique chain identifier"); } const chainId = parseInt(successfulResults[0]?.value?.result, 16); const identifier = Number.isNaN(chainId) ? successfulResults[1]?.value?.result : chainId.toString(); rpcToChainIdMap.set(appRpcUrl, identifier); rpcRequestMap.delete(appRpcUrl); return identifier; })(); rpcRequestMap.set(appRpcUrl, fetchPromise); return fetchPromise; } catch (error) { rpcRequestMap.delete(appRpcUrl); throw error; } } // Function that times out a request by deleting its callback functions. function abortRequest(message) { if (callbackMap.has(message.id)) { const { reject } = callbackMap.get(message.id); reject(new DOMException("Request was aborted", "AbortError")); callbackMap.delete(message.id); } } // Function that times out a request by deleting its callback functions. function timeOutRequest(message) { if (callbackMap.has(message.id)) { const { reject } = callbackMap.get(message.id); reject(new Error("Request timed out")); callbackMap.delete(message.id); } } // Function that converts a plaintext response to a response object. function convertToResponseObject(plaintextResponse) { return new Response(JSON.stringify(plaintextResponse.body), { headers: new Headers(plaintextResponse.headers), status: plaintextResponse.status, statusText: plaintextResponse.statusText, }); } // Function that turns a request body into a string. function stringifyBody(body) { if (typeof body === "string") { return body; } else if (body instanceof Uint8Array) { const decoder = new TextDecoder("utf-8"); return decoder.decode(body); } } // Function that captures the response from a XHR request. function captureXHRResponse(xhr) { return { body: xhr.response, response: xhr.response, responseText: xhr.responseText, responseType: xhr.responseType, responseURL: xhr.responseURL, responseXML: xhr.responseXML, readyState: xhr.readyState, status: xhr.status, statusText: xhr.statusText, getAllResponseHeaders: xhr.getAllResponseHeaders(), timeout: xhr.timeout, withCredentials: xhr.withCredentials, upload: xhr.upload, }; } // Function that returns orby response function returnOrbyResponse(xhr, plaintextResponse) { Object.defineProperties(xhr, { response: { value: plaintextResponse.body, writable: true, enumerable: true, configurable: true, }, responseText: { value: plaintextResponse.responseText, writable: true, enumerable: true, configurable: true, }, status: { value: plaintextResponse.status, writable: true, enumerable: true, configurable: true, }, statusText: { value: plaintextResponse.statusText, writable: true, enumerable: true, configurable: true, }, }); } // formats XHR response properly. function formatXHRResponse(plaintextResponse) { try { if (plaintextResponse.response === plaintextResponse.responseText) { plaintextResponse.responseText = plaintextResponse.body; } switch (plaintextResponse.responseType) { case "": case "text": if (typeof plaintextResponse.body !== "string") { plaintextResponse.body = JSON.stringify(plaintextResponse.body); } return; case "json": if (typeof body === "string") { plaintextResponse.body = JSON.parse(plaintextResponse.body); } return; case "arraybuffer": if (plaintextResponse.body instanceof ArrayBuffer) return; if (typeof plaintextResponse.body === "string") { const encoder = new TextEncoder(); plaintextResponse.body = encoder.encode( plaintextResponse.body ).buffer; } else if (typeof plaintextResponse.body === "object") { const str = JSON.stringify(plaintextResponse.body); const encoder = new TextEncoder(); plaintextResponse.body = encoder.encode(str).buffer; } return; case "blob": if (plaintextResponse.body instanceof Blob) return; if (typeof plaintextResponse.body === "string") { plaintextResponse.body = new Blob([plaintextResponse.body], { type: "text/plain", }); } else if (typeof plaintextResponse.body === "object") { plaintextResponse.body = new Blob( [JSON.stringify(plaintextResponse.body)], { type: "application/json" } ); } return; case "document": if (plaintextResponse.body instanceof Document) return; if (typeof plaintextResponse.body === "string") { const parser = new DOMParser(); plaintextResponse.body = parser.parseFromString( plaintextResponse.body, "text/html" ); } return; default: return; } } catch (error) { return; } } })(); `;