diff --git a/src/Oddworld.ts b/src/Oddworld.ts index af31722..b61e8a4 100644 --- a/src/Oddworld.ts +++ b/src/Oddworld.ts @@ -36,10 +36,22 @@ export function create< ...Chains.Chain[], ] = typeof create.defaultParameters.chains, >(parameters?: create.Parameters): Oddworld -export function create( - parameters: create.Parameters = create.defaultParameters, -): Oddworld { - const { chains, headless = true, transports, webauthn } = parameters +export function create(parameters?: create.Parameters | undefined): Oddworld { + const { + chains = create.defaultParameters.chains, + headless = create.defaultParameters.headless, + transports = create.defaultParameters.transports, + } = parameters ?? {} + + const keystoreHost = (() => { + if (parameters?.keystoreHost) return parameters.keystoreHost + if ( + typeof window !== 'undefined' && + window.location.hostname === 'localhost' + ) + return undefined + return create.defaultParameters.keystoreHost + })() const store = createStore( subscribeWithSelector( @@ -57,7 +69,7 @@ export function create( const { chain } = get() return createClient({ chain, - transport: transports[chain.id]!, + transport: (transports as Record)[chain.id]!, }) }, get delegation() { @@ -82,7 +94,7 @@ export function create( const emitter = Provider.createEmitter() - function setupSubscriptions() { + function setup() { const unsubscribe_accounts = store.subscribe( (state) => state.accounts, (accounts) => { @@ -100,12 +112,14 @@ export function create( }, ) + if (keystoreHost) setupWebAuthnOrigin({ rpId: keystoreHost }) + return () => { unsubscribe_accounts() unsubscribe_chain() } } - const unsubscribe = setupSubscriptions() + const destroy = setup() const provider = Provider.from({ ...emitter, @@ -124,7 +138,9 @@ export function create( case 'eth_requestAccounts': { if (!headless) throw new Provider.UnsupportedMethodError() - const { account } = await AccountDelegation.load(state.client) + const { account } = await AccountDelegation.load(state.client, { + rpId: keystoreHost, + }) store.setState((x) => ({ ...x, accounts: [account] })) @@ -176,7 +192,7 @@ export function create( const { account } = await AccountDelegation.create(state.client, { delegation: state.delegation, label, - rpId: webauthn?.rpId, + rpId: keystoreHost, }) store.setState((x) => ({ ...x, accounts: [account] })) @@ -304,7 +320,7 @@ export function create( }, destroy() { emitter.removeAllListeners() - unsubscribe() + destroy() }, provider, _internal: { @@ -321,31 +337,29 @@ export namespace create { ], > = { /** List of supported chains. */ - chains: chains | readonly [Chains.Chain, ...Chains.Chain[]] + chains?: chains | readonly [Chains.Chain, ...Chains.Chain[]] /** Transport to use for each chain. */ - transports: Record + transports?: Record } & ( | { /** * Whether to run EIP-1193 Provider in headless mode. * @default true */ - headless: true + headless?: true | undefined /** WebAuthn configuration. */ - webauthn?: - | { - rpId?: string | undefined - } - | undefined + keystoreHost?: string | undefined } | { headless?: false | undefined - webauthn?: undefined + keystoreHost?: undefined } ) export const defaultParameters = { chains: [Chains.odysseyTestnet], + headless: true, + keystoreHost: 'oddworld-tau.vercel.app', transports: { [Chains.odysseyTestnet.id]: http(), }, @@ -400,6 +414,30 @@ function requireParameter( }) } +function setupWebAuthnOrigin(parameters: { + rpId: string +}) { + if (typeof window === 'undefined') return + + const { rpId } = parameters + const origin = `${window.location.protocol}//${window.location.hostname}` + const url = `https://${rpId}/.well-known/webauthn` + fetch(url) + .then((x) => x.json()) + .then((x) => { + if (x.origins.includes(origin)) return + fetch(`https://${rpId}/.well-known/webauthn`, { + method: 'PATCH', + body: JSON.stringify({ + origin: `${window.location.protocol}//${window.location.hostname}`, + }), + headers: { + 'Content-Type': 'application/json', + }, + }) + }) +} + const storage = { getItem(name) { const value = localStorage.getItem(name) diff --git a/src/internal/accountDelegation.ts b/src/internal/accountDelegation.ts index 3f522fe..adb9281 100644 --- a/src/internal/accountDelegation.ts +++ b/src/internal/accountDelegation.ts @@ -232,6 +232,7 @@ export async function createWebAuthnKey( parameters: createWebAuthnKey.Parameters, ): Promise { const { expiry = 0n, rpId, label, userId } = parameters + const key = await WebAuthnP256.createCredential({ authenticatorSelection: { requireResidentKey: false, @@ -250,6 +251,7 @@ export async function createWebAuthnKey( id: userId, }, }) + return { ...key, expiry, @@ -356,11 +358,15 @@ export declare namespace execute { /** Loads an existing Account. */ export async function load( client: Client, + parameters: load.Parameters = {}, ) { + const { rpId } = parameters + // We will sign a random challenge. We need to do this to extract the // user id (ie. the address) to query for the Account's keys. const { raw } = await WebAuthnP256.sign({ challenge: '0x', + rpId, }) const response = raw.response as AuthenticatorAssertionResponse @@ -408,6 +414,13 @@ export async function load( } } +export declare namespace load { + type Parameters = { + /** Relying Party ID. */ + rpId?: string | undefined + } +} + /** Signs a payload with a key on the Account. */ export async function sign(parameters: sign.Parameters) { const { account, payload, keyIndex } = parameters