accounts
Version:
Tempo Accounts SDK
120 lines • 5.06 kB
JavaScript
import { PublicKey, Signature } from 'ox';
import { SignatureEnvelope } from 'ox/tempo';
import { Account } from 'viem/tempo';
import { Authentication, Registration } from 'webauthx/client';
import * as Adapter from '../Adapter.js';
import * as WebAuthnCeremony from '../WebAuthnCeremony.js';
import * as Rpc from '../zod/rpc.js';
import { local } from './local.js';
/**
* Creates a WebAuthn adapter backed by real passkey ceremonies.
*
* Wraps the {@link local} adapter with WebAuthn registration and authentication flows,
* using the provided {@link WebAuthnCeremony} for challenge generation and verification.
*
* @example
* ```ts
* import { webAuthn } from 'accounts'
*
* const provider = Provider.create({
* adapter: webAuthn(),
* })
* ```
*/
export function webAuthn(options = {}) {
const { auth, authUrl, icon, name, rdns } = options;
const url = (() => {
if (auth)
return typeof auth === 'string' ? auth : auth.url;
return authUrl;
})();
return Adapter.define({ icon, name, rdns }, (parameters) => {
const { storage } = parameters;
const ceremony = options.ceremony ??
(url ? WebAuthnCeremony.server({ url }) : WebAuthnCeremony.local({ storage }));
const base = local({
async createAccount(parameters) {
const { options } = await ceremony.getRegistrationOptions(parameters);
const rpId = options.publicKey?.rp.id;
if (!rpId)
throw new Error('rpId is required');
const credential = await Registration.create({ options });
const { publicKey, email, username } = await ceremony.verifyRegistration(credential, {
name: parameters.name,
});
await storage.setItem('lastCredentialId', credential.id);
const account = Account.fromWebAuthnP256({ id: credential.id, publicKey });
return {
accounts: [
{
address: account.address,
label: parameters.name,
keyType: 'webAuthn',
credential: { id: credential.id, publicKey, rpId },
},
],
email,
username,
};
},
async loadAccounts(parameters = {}) {
const { selectAccount, digest } = parameters;
const credentialId = selectAccount
? undefined
: (parameters?.credentialId ??
(await storage.getItem('lastCredentialId')) ??
undefined);
const { options } = await ceremony.getAuthenticationOptions({
...parameters,
challenge: digest,
credentialId,
});
const rpId = options.publicKey?.rpId;
if (!rpId)
throw new Error('rpId is required');
const response = await Authentication.sign({ options });
const { publicKey, email, username } = await ceremony.verifyAuthentication(response);
await storage.setItem('lastCredentialId', response.id);
const account = Account.fromWebAuthnP256({ id: response.id, publicKey }, { rpId });
const signature = digest
? SignatureEnvelope.serialize({
metadata: response.metadata,
publicKey: PublicKey.fromHex(publicKey),
signature: Signature.from(response.signature),
type: 'webAuthn',
}, { magic: true })
: undefined;
return {
accounts: [
{
address: account.address,
keyType: 'webAuthn',
credential: { id: response.id, publicKey, rpId },
},
],
email,
signature,
username,
};
},
})(parameters);
// When a server-backed ceremony is used, also revoke the
// `Handler.webAuthn` session on disconnect — otherwise the
// `accounts_webauthn` cookie persists past `wallet_disconnect`
// and follow-up authenticated requests still succeed.
const disconnect = url
? async () => {
await fetch(`${url}/logout`, {
method: 'POST',
credentials: 'include',
}).catch(() => { });
}
: undefined;
return {
...base,
actions: { ...base.actions, ...(disconnect ? { disconnect } : {}) },
persistAccounts: true,
};
});
}
//# sourceMappingURL=webAuthn.js.map