accounts
Version:
Tempo Accounts SDK
240 lines (197 loc) • 9.41 kB
text/typescript
import { describe, expect, test } from 'vp/test'
import { Registration, Authentication } from 'webauthx/client'
import { hooksUrl, url as webauthnUrl } from '../../test/webauthn.constants.js'
import { webAuthn } from './adapters/webAuthn.js'
import * as Provider from './Provider.js'
import * as Storage from './Storage.js'
import * as WebAuthnCeremony from './WebAuthnCeremony.js'
describe('local', () => {
test('default: creates a passkey and verifies registration', async () => {
const ceremony = WebAuthnCeremony.local()
const { options } = await ceremony.getRegistrationOptions({ name: 'Test' })
const credential = await Registration.create({ options })
const result = await ceremony.verifyRegistration(credential)
expect(result.publicKey).toMatch(/^0x[0-9a-f]+$/)
expect(result.credentialId).toBeTypeOf('string')
})
test('behavior: authenticates with an existing passkey', async () => {
const ceremony = WebAuthnCeremony.local()
const { options: regOptions } = await ceremony.getRegistrationOptions({ name: 'Test' })
const credential = await Registration.create({ options: regOptions })
const { publicKey } = await ceremony.verifyRegistration(credential)
const { options: authOptions } = await ceremony.getAuthenticationOptions({
credentialId: credential.id,
})
const response = await Authentication.sign({ options: authOptions })
const result = await ceremony.verifyAuthentication(response)
expect(result.publicKey).toMatchInlineSnapshot(`"${publicKey}"`)
expect(result.credentialId).toBeTypeOf('string')
})
test('behavior: register → authenticate → publicKeys match', async () => {
const ceremony = WebAuthnCeremony.local()
const { options: regOptions } = await ceremony.getRegistrationOptions({ name: 'Round-trip' })
const credential = await Registration.create({ options: regOptions })
const reg = await ceremony.verifyRegistration(credential)
const { options: authOptions } = await ceremony.getAuthenticationOptions({
credentialId: reg.credentialId,
})
const response = await Authentication.sign({ options: authOptions })
const auth = await ceremony.verifyAuthentication(response)
expect(auth.publicKey).toBe(reg.publicKey)
expect(auth.credentialId).toBe(reg.credentialId)
})
})
describe('server', () => {
const ceremony = WebAuthnCeremony.server({ url: webauthnUrl })
test('default: register → verify returns valid publicKey hex', async () => {
const { options } = await ceremony.getRegistrationOptions({ name: 'Server Test' })
const credential = await Registration.create({ options })
const result = await ceremony.verifyRegistration(credential)
expect(result.credentialId).toBeTypeOf('string')
expect(result.publicKey).toMatch(/^0x[0-9a-f]+$/)
})
test('behavior: authentication publicKeys match registration', async () => {
const { options: regOptions } = await ceremony.getRegistrationOptions({ name: 'Server Auth' })
const credential = await Registration.create({ options: regOptions })
const reg = await ceremony.verifyRegistration(credential)
const { options: authOptions } = await ceremony.getAuthenticationOptions({
credentialId: reg.credentialId,
})
const response = await Authentication.sign({ options: authOptions })
const auth = await ceremony.verifyAuthentication(response)
expect(auth.publicKey).toBe(reg.publicKey)
expect(auth.credentialId).toBe(reg.credentialId)
})
test('behavior: challenge consumed after use (replay → 400)', async () => {
const { options } = await ceremony.getRegistrationOptions({ name: 'Replay Test' })
const credential = await Registration.create({ options })
// First verify succeeds
const result = await ceremony.verifyRegistration(credential)
expect(result.credentialId).toBeTypeOf('string')
// Replay same credential → 400 (challenge consumed)
const response = await fetch(`${webauthnUrl}/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credential),
})
expect(response.status).toBe(400)
const body = await response.json()
expect(body.error).toMatchInlineSnapshot(`"Missing or expired challenge"`)
})
test('behavior: multiple credentials return correct publicKeys', async () => {
const { options: regA } = await ceremony.getRegistrationOptions({ name: 'User A' })
const credA = await Registration.create({ options: regA })
const a = await ceremony.verifyRegistration(credA)
const { options: regB } = await ceremony.getRegistrationOptions({ name: 'User B' })
const credB = await Registration.create({ options: regB })
const b = await ceremony.verifyRegistration(credB)
expect(a.publicKey).not.toBe(b.publicKey)
// Login with credential A
const { options: authA } = await ceremony.getAuthenticationOptions({
credentialId: a.credentialId,
})
const resA = await Authentication.sign({ options: authA })
const authResultA = await ceremony.verifyAuthentication(resA)
expect(authResultA.publicKey).toBe(a.publicKey)
// Login with credential B
const { options: authB } = await ceremony.getAuthenticationOptions({
credentialId: b.credentialId,
})
const resB = await Authentication.sign({ options: authB })
const authResultB = await ceremony.verifyAuthentication(resB)
expect(authResultB.publicKey).toBe(b.publicKey)
})
})
describe('server (provider round-trip)', () => {
const ceremony = WebAuthnCeremony.server({ url: webauthnUrl })
function createProvider() {
return Provider.create({
storage: Storage.idb({ key: crypto.randomUUID() }),
adapter: webAuthn({ ceremony }),
})
}
test('behavior: wallet_connect register → eth_accounts returns address', async () => {
const provider = createProvider()
const result = await provider.request({
method: 'wallet_connect',
params: [{ capabilities: { method: 'register' } }],
})
expect(result.accounts).toHaveLength(1)
expect(result.accounts[0]!.address).toMatch(/^0x[0-9a-fA-F]{40}$/)
const accounts = await provider.request({ method: 'eth_accounts' })
expect(accounts).toHaveLength(1)
expect(accounts[0]).toBe(result.accounts[0]!.address)
})
test('behavior: register two accounts → eth_accounts returns both', async () => {
const provider = createProvider()
// Register first
const first = await provider.request({
method: 'wallet_connect',
params: [{ capabilities: { method: 'register' } }],
})
// Register second
const second = await provider.request({
method: 'wallet_connect',
params: [{ capabilities: { method: 'register' } }],
})
expect(second.accounts).toHaveLength(2)
expect(second.accounts[0]!.address).not.toBe(first.accounts[0]!.address)
const accounts = await provider.request({ method: 'eth_accounts' })
expect(accounts).toHaveLength(2)
})
})
describe('server (hooks)', () => {
test('behavior: onRegister merges extra JSON and headers', async () => {
const regOptionsRes = await fetch(`${hooksUrl}/register/options`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Hook Test' }),
})
const { options } = await regOptionsRes.json()
const credential = await Registration.create({ options })
const res = await fetch(`${hooksUrl}/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credential),
})
const body = await res.json()
expect(body.credentialId).toBeTypeOf('string')
expect(body.publicKey).toMatch(/^0x[0-9a-f]+$/)
expect(body.sessionToken).toBe(`reg_${body.credentialId}`)
expect(res.headers.get('x-custom')).toBe('register-hook')
})
test('behavior: onAuthenticate merges extra JSON and headers', async () => {
// Register first
const regOptionsRes = await fetch(`${hooksUrl}/register/options`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Hook Auth Test' }),
})
const { options: regOptions } = await regOptionsRes.json()
const credential = await Registration.create({ options: regOptions })
const regRes = await fetch(`${hooksUrl}/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credential),
})
const { credentialId } = await regRes.json()
// Authenticate
const authOptionsRes = await fetch(`${hooksUrl}/login/options`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ credentialId }),
})
const { options: authOptions } = await authOptionsRes.json()
const response = await Authentication.sign({ options: authOptions })
const res = await fetch(`${hooksUrl}/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(response),
})
const body = await res.json()
expect(body.credentialId).toBe(credentialId)
expect(body.publicKey).toMatch(/^0x[0-9a-f]+$/)
expect(body.sessionToken).toBe(`auth_${credentialId}`)
expect(res.headers.get('x-custom')).toBe('authenticate-hook')
})
})