edge-core-js
Version:
Edge account & wallet management library
264 lines (222 loc) • 6.81 kB
JavaScript
function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }import './polyfills'
import hashjs from 'hash.js'
import HmacDRBG from 'hmac-drbg'
import { base64 } from 'rfc4648'
import { makeFetchResponse } from 'serverlet'
import { Bridge, bridgifyObject } from 'yaob'
import {
addEdgeCorePlugins,
lockEdgeCorePlugins,
makeContext,
makeFakeWorld
} from '../../core/core'
import { hideProperties } from '../hidden-properties'
import { makeNativeBridge } from './native-bridge'
import { YAOB_THROTTLE_MS } from './react-native-types'
// Tracks the status of different URI endpoints for the CORS bouncer:
const endpointCorsState = new Map
()
// Set up the bridges:
const [nativeBridge, reactBridge] =
window.edgeCore != null
? [
// Android:
makeNativeBridge((id, name, args) => {
window.edgeCore.call(id, name, JSON.stringify(args))
}),
new Bridge({
hideProperties,
sendMessage(message) {
window.edgeCore.postMessage(JSON.stringify(message))
},
throttleMs: YAOB_THROTTLE_MS
})
]
: [
// iOS:
makeNativeBridge((id, name, args) => {
window.webkit.messageHandlers.edgeCore.postMessage([id, name, args])
}),
new Bridge({
hideProperties,
sendMessage(message) {
window.webkit.messageHandlers.edgeCore.postMessage([
0,
'postMessage',
[JSON.stringify(message)]
])
},
throttleMs: YAOB_THROTTLE_MS
})
]
// Set up global objects:
window.addEdgeCorePlugins = addEdgeCorePlugins
window.nativeBridge = nativeBridge
window.reactBridge = reactBridge
function loadPlugins(pluginUris) {
const { head } = window.document
if (head == null || pluginUris.length === 0) {
lockEdgeCorePlugins()
return
}
let loaded = 0
const handleLoad = () => {
if (++loaded >= pluginUris.length) lockEdgeCorePlugins()
}
for (const uri of pluginUris) {
const script = document.createElement('script')
script.addEventListener('error', handleLoad)
script.addEventListener('load', handleLoad)
script.charset = 'utf-8'
script.defer = true
script.src = uri
head.appendChild(script)
}
}
async function makeIo() {
const csprng = new HmacDRBG({
hash: hashjs.sha256,
entropy: base64.parse(await nativeBridge.call('randomBytes', 32))
})
const nativeFetch = async (uri, opts = {}) => {
const { method = 'GET', headers = {}, body } = opts
const response = await nativeBridge.call(
'fetch',
uri,
method,
headers,
body instanceof ArrayBuffer
? base64.stringify(new Uint8Array(body))
: body,
body instanceof ArrayBuffer
)
return makeFetchResponse({
status: response.status,
headers: response.headers,
body: response.bodyIsBase64 ? base64.parse(response.body) : response.body
})
}
const io = {
disklet: {
delete(path) {
return nativeBridge.call('diskletDelete', normalizePath(path))
},
async getData(path) {
const data = await nativeBridge.call(
'diskletGetData',
normalizePath(path)
)
return base64.parse(data)
},
getText(path) {
return nativeBridge.call('diskletGetText', normalizePath(path))
},
list(path = '') {
return nativeBridge.call('diskletList', normalizePath(path))
},
setData(path, data) {
return nativeBridge.call(
'diskletSetData',
normalizePath(path),
base64.stringify(data)
)
},
setText(path, text) {
return nativeBridge.call('diskletSetText', normalizePath(path), text)
}
},
random: bytes => csprng.generate(bytes),
async scrypt(data, salt, n, r, p, dklen) {
const hash = await nativeBridge.call(
'scrypt',
base64.stringify(data),
base64.stringify(salt),
n,
r,
p,
dklen
)
return base64.parse(hash)
},
// Networking:
async fetch(
uri,
opts
) {
const { corsBypass = 'auto' } = _nullishCoalesce(opts, () => ( {}))
if (corsBypass === 'always') {
return await nativeFetch(uri, opts)
}
if (corsBypass === 'never') {
return await window.fetch(uri, opts)
}
const { protocol, host, pathname } = new URL(uri)
const endpoint = `${protocol}//${host}${pathname}`
const state = _nullishCoalesce(endpointCorsState.get(endpoint), () => ( {
windowSuccess: false,
nativeSuccess: false
}))
if (!endpointCorsState.has(endpoint)) {
endpointCorsState.set(endpoint, state)
}
// If the native fetch worked,
// then we can guess that the server has a CORS problem,
// so don't even bother with `window.fetch`:
if (!state.nativeSuccess) {
try {
const response = await window.fetch(uri, opts)
state.windowSuccess = true
return response
} catch (error) {
// If `window.fetch` has ever worked,
// then we know the server has the right CORS headers,
// so don't even bother with the native fallback:
if (state.windowSuccess) throw error
}
}
const response = await nativeFetch(uri, opts)
state.nativeSuccess = true
return response
},
async fetchCors(
uri,
opts = {}
) {
return await io.fetch(uri, opts)
}
}
return io
}
/**
* Interprets a path as a series of folder lookups,
* handling special components like `.` and `..`.
*/
export function normalizePath(path) {
if (/^\//.test(path)) throw new Error('Absolute paths are not supported')
const parts = path.split('/')
// Shift down good elements, dropping bad ones:
let i = 0 // Read index
let j = 0 // Write index
while (i < parts.length) {
const part = parts[i++]
if (part === '..') j--
else if (part !== '.' && part !== '') parts[j++] = part
if (j < 0) throw new Error('Path would escape folder')
}
// Array items from 0 to j are the path:
return parts.slice(0, j).join('/')
}
// Send the root object:
const workerApi = bridgifyObject({
async makeEdgeContext(nativeIo, logBackend, pluginUris, opts) {
loadPlugins(pluginUris)
const io = await makeIo()
return await makeContext({ io, nativeIo }, logBackend, opts)
},
async makeFakeEdgeWorld(nativeIo, logBackend, pluginUris, users = []) {
loadPlugins(pluginUris)
const io = await makeIo()
return makeFakeWorld({ io, nativeIo }, logBackend, users)
}
})
reactBridge.sendRoot(workerApi)