UNPKG

accounts

Version:

Tempo Accounts SDK

1,224 lines (1,118 loc) 32.4 kB
import { Base64 } from 'ox' import { KeyAuthorization } from 'ox/tempo' import { Account as TempoAccount } from 'viem/tempo' import { describe, expect, test } from 'vp/test' import * as z from 'zod/mini' import { accounts, chain, privateKeys, webAuthnAccounts } from '../../test/config.js' import * as CliAuth from './CliAuth.js' import * as Handler from './Handler.js' const root = accounts[0]! const webAuthnRoot = webAuthnAccounts[0]! const accessKey = TempoAccount.fromP256(privateKeys[1]!) const secpAccessKey = accounts[1]! const expiry = Math.floor(Date.now() / 1000) + 3_600 const token = '0x20c0000000000000000000000000000000000001' as const const limit = 1_000n const limits = [ { limit, token, }, ] as const async function authorize( code: string, options: { accessKey?: | { address: `0x${string}` keyType: 'secp256k1' | 'p256' | 'webAuthn' } | undefined accessKeyAddress?: `0x${string}` | undefined expiry?: number | undefined limits?: readonly { token: `0x${string}`; limit: bigint }[] | undefined } = {}, ) { const key = options.accessKey ?? accessKey const signed = await root.signKeyAuthorization( { accessKeyAddress: options.accessKeyAddress ?? key.address, keyType: key.keyType, }, { chainId: BigInt(chain.id), expiry: options.expiry ?? expiry, limits: options.limits ?? limits, }, ) const keyAuthorization = KeyAuthorization.toRpc(signed) return { accountAddress: root.address, code, keyAuthorization: z.decode(CliAuth.keyAuthorization, { ...keyAuthorization, address: keyAuthorization.keyId, }), } satisfies z.output<typeof CliAuth.authorizeRequest> } async function authorizeWebAuthn( code: string, options: { accessKey?: | { address: `0x${string}` keyType: 'secp256k1' | 'p256' | 'webAuthn' } | undefined expiry?: number | undefined limits?: readonly { token: `0x${string}`; limit: bigint }[] | undefined } = {}, ) { const key = options.accessKey ?? secpAccessKey const signed = await webAuthnRoot.signKeyAuthorization( { accessKeyAddress: key.address, keyType: key.keyType, }, { chainId: BigInt(chain.id), expiry: options.expiry ?? expiry, limits: options.limits ?? limits, }, ) const keyAuthorization = KeyAuthorization.toRpc(signed) return { accountAddress: webAuthnRoot.address, code, keyAuthorization: z.decode(CliAuth.keyAuthorization, { ...keyAuthorization, address: keyAuthorization.keyId, }), } satisfies z.output<typeof CliAuth.authorizeRequest> } async function createRequest( codeVerifier = 'device-code-verifier', options: { accessKey?: | { keyType: 'secp256k1' | 'p256' | 'webAuthn' publicKey: `0x${string}` } | undefined expiry?: number | undefined keyType?: 'secp256k1' | 'p256' | 'webAuthn' | undefined limits?: readonly { token: `0x${string}`; limit: bigint }[] | undefined } = {}, ) { const key = options.accessKey ?? accessKey return { codeVerifier, request: { codeChallenge: await createCodeChallenge(codeVerifier), ...('expiry' in options ? typeof options.expiry !== 'undefined' ? { expiry: options.expiry } : {} : { expiry }), ...('keyType' in options ? options.keyType ? { keyType: options.keyType } : {} : { keyType: key.keyType }), ...('limits' in options ? (options.limits ? { limits: options.limits } : {}) : { limits }), pubKey: key.publicKey, } satisfies z.output<typeof CliAuth.createRequest>, } } async function post<request extends z.ZodMiniType, response extends z.ZodMiniType>( handler: Handler.Handler, options: { body: z.output<request> request: request response?: response | undefined url: string }, ) { const result = await handler.fetch( new Request(options.url, { body: JSON.stringify(z.encode(options.request, options.body)), headers: { 'content-type': 'application/json' }, method: 'POST', }), ) const json = (await result.json().catch(() => ({}))) as z.input<response> return { body: options.response ? z.decode(options.response, json) : json, status: result.status, } } async function get<response extends z.ZodMiniType>( handler: Handler.Handler, options: { response?: response | undefined url: string }, ) { const result = await handler.fetch(new Request(options.url)) const json = (await result.json().catch(() => ({}))) as z.input<response> return { body: options.response ? z.decode(options.response, json) : json, status: result.status, } } describe('from', () => { test('default: shares defaults across the device-code flow', async () => { const store = CliAuth.Store.memory() const now = () => 1_000 const { codeVerifier, request } = await createRequest() const cli = CliAuth.from({ chains: [chain], now, random: () => new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]), store, ttlMs: 30_000, }) const created = await cli.createDeviceCode({ request }) const entry = await store.get(created.code) const authorized = await cli.authorize({ request: await authorize(created.code), }) const polled = await cli.poll({ code: created.code, request: { codeVerifier, }, }) expect(created).toMatchInlineSnapshot(` { "code": "ABCDEFGH", } `) expect(entry).toMatchInlineSnapshot(` { "chainId": 1337n, "code": "ABCDEFGH", "codeChallenge": "NUwjc1h8PuXcsvSOG44Rp4bMayBXnOkriHEJ19CaSQM", "createdAt": 1000, "expiresAt": 31000, "expiry": ${expiry}, "keyType": "p256", "limits": [ { "limit": 1000n, "token": "0x20c0000000000000000000000000000000000001", }, ], "pubKey": "${accessKey.publicKey}", "status": "pending", } `) expect(authorized).toMatchInlineSnapshot(` { "status": "authorized", } `) expect(polled.status).toMatchInlineSnapshot(`"authorized"`) }) }) describe('createDeviceCode', () => { test('default: creates a pending device code', async () => { const store = CliAuth.Store.memory() const now = () => 1_000 const { request } = await createRequest() const result = await CliAuth.createDeviceCode({ chainId: chain.id, now, random: () => new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]), request, store, ttlMs: 30_000, }) const entry = await store.get(result.code) expect(result).toMatchInlineSnapshot(` { "code": "ABCDEFGH", } `) expect(entry).toMatchInlineSnapshot(` { "chainId": 1337n, "code": "ABCDEFGH", "codeChallenge": "NUwjc1h8PuXcsvSOG44Rp4bMayBXnOkriHEJ19CaSQM", "createdAt": 1000, "expiresAt": 31000, "expiry": ${expiry}, "keyType": "p256", "limits": [ { "limit": 1000n, "token": "0x20c0000000000000000000000000000000000001", }, ], "pubKey": "${accessKey.publicKey}", "status": "pending", } `) }) test('behavior: policy rejection returns an error from the handler', async () => { const { request } = await createRequest() const handler = Handler.codeAuth({ policy: { validate() { throw new Error('Expiry exceeds policy.') }, }, }) const result = await post(handler, { body: request, request: CliAuth.createRequest, url: 'http://localhost/auth/pkce/code', }) expect(result).toMatchInlineSnapshot(` { "body": { "error": "Expiry exceeds policy.", }, "status": 400, } `) }) test('behavior: handler rate-limits all CLI auth endpoints together', async () => { const handler = Handler.codeAuth({ rateLimit: CliAuth.RateLimit.memory({ max: 1, windowMs: 60_000 }), }) const first = await get(handler, { url: 'http://localhost/auth/pkce/pending/ABCDEFGH', }) const second = await get(handler, { url: 'http://localhost/auth/pkce/pending/ABCDEFGH', }) expect(first.status).toMatchInlineSnapshot(`404`) expect(second).toMatchInlineSnapshot(` { "body": { "error": "Rate limit exceeded.", }, "status": 429, } `) }) test('behavior: Cloudflare rate-limit adapter shares one key across endpoints', async () => { const calls: { key: string }[] = [] const rateLimit = CliAuth.RateLimit.cloudflare({ limit(options) { calls.push(options) return { success: true } }, }) await rateLimit.limit({ key: '127.0.0.1', request: new Request('http://localhost/auth/pkce/code'), }) expect(calls).toMatchInlineSnapshot(` [ { "key": "cli-auth:127.0.0.1", }, ] `) }) test('behavior: handler ignores untrusted forwarding headers for rate-limit keys', async () => { const calls: { key: string }[] = [] const handler = Handler.codeAuth({ rateLimit: { limit(options) { calls.push({ key: options.key }) return { success: true } }, }, }) await get(handler, { url: 'http://localhost/auth/pkce/pending/ABCDEFGH', }) await handler.fetch( new Request('http://localhost/auth/pkce/pending/ABCDEFGH', { headers: { 'x-forwarded-for': '192.0.2.1', 'x-real-ip': '192.0.2.2', }, }), ) await handler.fetch( new Request('http://localhost/auth/pkce/pending/ABCDEFGH', { headers: { 'cf-connecting-ip': '192.0.2.3', }, }), ) expect(calls).toMatchInlineSnapshot(` [ { "key": "unknown", }, { "key": "unknown", }, { "key": "192.0.2.3", }, ] `) }) test('behavior: handler accepts a custom rate-limit key for trusted proxies', async () => { const calls: { key: string }[] = [] const handler = Handler.codeAuth({ rateLimit: { limit(options) { calls.push({ key: options.key }) return { success: true } }, }, rateLimitKey(request) { return request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? 'unknown' }, }) await handler.fetch( new Request('http://localhost/auth/pkce/pending/ABCDEFGH', { headers: { 'x-forwarded-for': '192.0.2.1, 192.0.2.2', }, }), ) expect(calls).toMatchInlineSnapshot(` [ { "key": "192.0.2.1", }, ] `) }) test('behavior: invalid input returns 400', async () => { const handler = Handler.codeAuth() const response = await handler.fetch( new Request('http://localhost/auth/pkce/code', { body: JSON.stringify({ expiry }), headers: { 'content-type': 'application/json' }, method: 'POST', }), ) const body = await response.json() expect(body.error).toMatchInlineSnapshot(` "[\n {\n "expected": "string",\n "code": "invalid_type",\n "path": [\n "codeChallenge"\n ],\n "message": "Invalid input"\n },\n {\n "expected": "string",\n "code": "invalid_type",\n "path": [\n "pubKey"\n ],\n "message": "Expected hex value"\n }\n]" `) expect(response.status).toMatchInlineSnapshot(`400`) }) test('behavior: handler rejects oversized JSON request bodies', async () => { const { request } = await createRequest() const handler = Handler.codeAuth({ maxBodyBytes: 16 }) const response = await handler.fetch( new Request('http://localhost/auth/pkce/code', { body: JSON.stringify(z.encode(CliAuth.createRequest, request)), headers: { 'content-type': 'application/json' }, method: 'POST', }), ) const body = await response.json() expect({ body, status: response.status }).toMatchInlineSnapshot(` { "body": { "error": "Request body is too large.", }, "status": 400, } `) }) test('behavior: handler rejects oversized streamed JSON request bodies', async () => { const handler = Handler.codeAuth({ maxBodyBytes: 16 }) const response = await handler.fetch( new Request('http://localhost/auth/pkce/code', { body: new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode('{"codeChallenge":"')) controller.enqueue(new TextEncoder().encode('x"}')) controller.close() }, }), duplex: 'half', headers: { 'content-type': 'application/json' }, method: 'POST', } as never), ) const body = await response.json() expect({ body, status: response.status }).toMatchInlineSnapshot(` { "body": { "error": "Request body is too large.", }, "status": 400, } `) }) test('behavior: rejects invalid public keys at creation time', async () => { const { request } = await createRequest() await expect( CliAuth.createDeviceCode({ chainId: chain.id, request: { ...request, pubKey: '0x1234' }, store: CliAuth.Store.memory(), }), ).rejects .toThrowErrorMatchingInlineSnapshot(`[PublicKey.InvalidSerializedSizeError: Value \`0x1234\` is an invalid public key size. Expected: 33 bytes (compressed + prefix), 64 bytes (uncompressed) or 65 bytes (uncompressed + prefix). Received 2 bytes.]`) }) test('behavior: rejects too many limits', async () => { const { request } = await createRequest() const item = { limit: '0x1' as const, token: '0x20c0000000000000000000000000000000000001' as const, } const result = await z.safeDecodeAsync(CliAuth.createRequest, { ...z.encode(CliAuth.createRequest, request), limits: Array.from({ length: 11 }, () => item), }) expect(result).toMatchInlineSnapshot(` { "error": [$ZodError: [ { "origin": "array", "code": "too_big", "maximum": 10, "inclusive": true, "path": [ "limits" ], "message": "Invalid input" } ]], "success": false, } `) }) test('behavior: rejects malformed limit tokens', async () => { const { request } = await createRequest() const result = await z.safeDecodeAsync(CliAuth.createRequest, { ...z.encode(CliAuth.createRequest, request), limits: [{ limit: '0x1', token: '0x1234' }], }) expect(result).toMatchInlineSnapshot(` { "error": [$ZodError: [ { "code": "invalid_format", "format": "template_literal", "pattern": "^0x[0-9a-fA-F]{40}$", "path": [ "limits", 0, "token" ], "message": "Expected address" } ]], "success": false, } `) }) test('behavior: handler rejects requests for unconfigured chains', async () => { const handler = Handler.codeAuth({ chains: [chain], }) const { request } = await createRequest() const result = await post(handler, { body: { ...request, chainId: BigInt(chain.id + 1), }, request: CliAuth.createRequest, url: 'http://localhost/auth/pkce/code', }) expect(result).toMatchInlineSnapshot(` { "body": { "error": "Chain 1338 not configured", }, "status": 400, } `) }) test('behavior: supports pubkey-only requests with server defaults', async () => { const store = CliAuth.Store.memory() const now = () => 1_000 const defaultExpiry = 4_600 let validatedChainId: bigint | undefined const { request } = await createRequest('device-code-verifier', { accessKey: secpAccessKey, expiry: undefined, keyType: undefined, limits: undefined, }) const result = await CliAuth.createDeviceCode({ chainId: chain.id, now, policy: { validate({ chainId, expiry, limits }) { validatedChainId = chainId return { expiry: expiry ?? defaultExpiry, ...(limits ? { limits } : {}), } }, }, random: () => new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]), request, store, ttlMs: 30_000, }) const entry = await store.get(result.code) expect({ entry, validatedChainId }).toMatchInlineSnapshot(` { "entry": { "chainId": 1337n, "code": "ABCDEFGH", "codeChallenge": "NUwjc1h8PuXcsvSOG44Rp4bMayBXnOkriHEJ19CaSQM", "createdAt": 1000, "expiresAt": 31000, "expiry": 4600, "keyType": "secp256k1", "pubKey": "${secpAccessKey.publicKey}", "status": "pending", }, "validatedChainId": 1337n, } `) }) }) describe('pending', () => { test('default: returns request details for a pending entry', async () => { const store = CliAuth.Store.memory() const { request } = await createRequest() const { code } = await CliAuth.createDeviceCode({ chainId: chain.id, random: () => new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]), request, store, }) const result = await CliAuth.pending({ code, store, }) expect(result).toMatchInlineSnapshot(` { "accessKeyAddress": "${accessKey.address.toLowerCase()}", "chainId": 1337n, "code": "ABCDEFGH", "expiry": ${expiry}, "keyType": "p256", "limits": [ { "limit": 1000n, "token": "0x20c0000000000000000000000000000000000001", }, ], "pubKey": "${accessKey.publicKey}", "status": "pending", } `) }) test('behavior: handler returns 404 for an unknown code', async () => { const handler = Handler.codeAuth() const result = await get(handler, { url: 'http://localhost/auth/pkce/pending/ABCDEFGH', }) expect(result).toMatchInlineSnapshot(` { "body": { "error": "Unknown device code.", }, "status": 404, } `) }) test('behavior: handler returns 400 for a completed code', async () => { const store = CliAuth.Store.memory() const handler = Handler.codeAuth({ chains: [chain], store, }) const { codeVerifier, request } = await createRequest() const { code } = await CliAuth.createDeviceCode({ chainId: chain.id, random: () => new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]), request, store, }) await CliAuth.authorize({ chainId: chain.id, request: await authorize(code), store, }) await CliAuth.poll({ code, request: { codeVerifier: codeVerifier, }, store, }) const result = await get(handler, { url: `http://localhost/auth/pkce/pending/${code}`, }) expect(result).toMatchInlineSnapshot(` { "body": { "error": "Device code already completed.", }, "status": 400, } `) }) test('behavior: handler accepts a hyphenated code', async () => { const store = CliAuth.Store.memory() const handler = Handler.codeAuth({ chains: [chain], store, }) const { request } = await createRequest() const { code } = await CliAuth.createDeviceCode({ chainId: chain.id, random: () => new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]), request, store, }) const result = await get(handler, { response: CliAuth.pendingResponse, url: `http://localhost/auth/pkce/pending/${code.slice(0, 4)}-${code.slice(4)}`, }) expect(result).toMatchInlineSnapshot(` { "body": { "accessKeyAddress": "${accessKey.address.toLowerCase()}", "chainId": 1337n, "code": "ABCDEFGH", "expiry": ${expiry}, "keyType": "p256", "limits": [ { "limit": 1000n, "token": "0x20c0000000000000000000000000000000000001", }, ], "pubKey": "${accessKey.publicKey}", "status": "pending", }, "status": 200, } `) }) }) describe('poll', () => { test('default: returns pending while awaiting authorization', async () => { const store = CliAuth.Store.memory() const { codeVerifier, request } = await createRequest() const { code } = await CliAuth.createDeviceCode({ chainId: chain.id, request, store, }) const result = await CliAuth.poll({ code, request: { codeVerifier: codeVerifier, }, store, }) expect(result).toMatchInlineSnapshot(` { "status": "pending", } `) }) test('behavior: rejects a PKCE mismatch', async () => { const handler = Handler.codeAuth({ chains: [chain], store: CliAuth.Store.memory(), }) const { request } = await createRequest() const created = await post(handler, { body: request, request: CliAuth.createRequest, response: CliAuth.createResponse, url: 'http://localhost/auth/pkce/code', }) const result = await post(handler, { body: { codeVerifier: 'wrong', }, request: CliAuth.pollRequest, url: `http://localhost/auth/pkce/poll/${(created.body as z.output<typeof CliAuth.createResponse>).code}`, }) expect(result).toMatchInlineSnapshot(` { "body": { "error": "Invalid code verifier.", }, "status": 400, } `) }) test('behavior: consumes an authorization exactly once', async () => { const store = CliAuth.Store.memory() const { codeVerifier, request } = await createRequest() const { code } = await CliAuth.createDeviceCode({ chainId: chain.id, request, store, }) await CliAuth.authorize({ chainId: chain.id, request: await authorize(code), store, }) const first = await CliAuth.poll({ code, request: { codeVerifier: codeVerifier, }, store, }) const second = await CliAuth.poll({ code, request: { codeVerifier: codeVerifier, }, store, }) const first_ = first.status === 'authorized' ? { ...first, keyAuthorization: { ...first.keyAuthorization, signature: { type: first.keyAuthorization.signature.type, }, }, } : first expect({ first: first_, second }).toMatchInlineSnapshot(` { "first": { "accountAddress": "${root.address}", "keyAuthorization": { "address": "${accessKey.address}", "chainId": 1337n, "expiry": ${expiry}, "keyId": "${accessKey.address}", "keyType": "p256", "limits": [ { "limit": 1000n, "token": "0x20c0000000000000000000000000000000000001", }, ], "signature": { "type": "secp256k1", }, }, "status": "authorized", }, "second": { "status": "expired", }, } `) }) test('behavior: accepts a hyphenated code when polling', async () => { const store = CliAuth.Store.memory() const { codeVerifier, request } = await createRequest() const { code } = await CliAuth.createDeviceCode({ chainId: chain.id, request, store, }) const result = await CliAuth.poll({ code: `${code.slice(0, 4)}-${code.slice(4)}`, request: { codeVerifier: codeVerifier, }, store, }) expect(result).toMatchInlineSnapshot(` { "status": "pending", } `) }) test('behavior: expires after TTL elapses', async () => { let time = 10_000 const now = () => time const store = CliAuth.Store.memory() const { codeVerifier, request } = await createRequest() const { code } = await CliAuth.createDeviceCode({ chainId: chain.id, now, request, store, ttlMs: 10, }) time += 11 const result = await CliAuth.poll({ code, now, request: { codeVerifier: codeVerifier, }, store, }) expect(result).toMatchInlineSnapshot(` { "status": "expired", } `) }) }) describe('authorize', () => { test('default: authorizes and returns the signed key authorization', async () => { const store = CliAuth.Store.memory() const { codeVerifier, request } = await createRequest() const { code } = await CliAuth.createDeviceCode({ chainId: chain.id, request, store, }) const authorized = await CliAuth.authorize({ chainId: chain.id, request: await authorize(code), store, }) const polled = await CliAuth.poll({ code, request: { codeVerifier: codeVerifier, }, store, }) expect(authorized).toMatchInlineSnapshot(` { "status": "authorized", } `) expect(polled.status).toMatchInlineSnapshot(`"authorized"`) }) test('behavior: accepts user-approved expiry and limit changes', async () => { const store = CliAuth.Store.memory() const { codeVerifier, request } = await createRequest() const { code } = await CliAuth.createDeviceCode({ chainId: chain.id, request, store, }) const approvedLimits = [ { limit: 2_000n, token, }, ] as const const authorized = await CliAuth.authorize({ chainId: chain.id, request: await authorize(code, { expiry: expiry + 60 * 60 * 24 * 6, limits: approvedLimits }), store, }) const polled = await CliAuth.poll({ code, request: { codeVerifier: codeVerifier, }, store, }) if (polled.status !== 'authorized') throw new Error('Expected device code to be authorized.') expect(authorized).toMatchInlineSnapshot(` { "status": "authorized", } `) expect({ expiry: polled.keyAuthorization.expiry, limits: polled.keyAuthorization.limits }) .toMatchInlineSnapshot(` { "expiry": ${expiry + 60 * 60 * 24 * 6}, "limits": [ { "limit": 2000n, "token": "0x20c0000000000000000000000000000000000001", }, ], } `) }) test('behavior: rejects unsigned expiry and limit changes', async () => { const store = CliAuth.Store.memory() const { request } = await createRequest() const { code } = await CliAuth.createDeviceCode({ chainId: chain.id, request, store, }) const authorized = await authorize(code) await expect( CliAuth.authorize({ chainId: chain.id, request: { ...authorized, keyAuthorization: { ...authorized.keyAuthorization, expiry: expiry + 1, }, }, store, }), ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Key authorization signature is invalid.]`) await expect( CliAuth.authorize({ chainId: chain.id, request: { ...authorized, keyAuthorization: { ...authorized.keyAuthorization, limits: [{ limit: limit + 1n, token }], }, }, store, }), ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Key authorization signature is invalid.]`) }) test('behavior: rejects a mismatched key authorization', async () => { const store = CliAuth.Store.memory() const { request } = await createRequest() const { code } = await CliAuth.createDeviceCode({ chainId: chain.id, request, store, }) await expect( CliAuth.authorize({ chainId: chain.id, request: await authorize(code, { accessKeyAddress: secpAccessKey.address }), store, }), ).rejects.toThrowErrorMatchingInlineSnapshot( `[Error: Key authorization key does not match the device-code request.]`, ) }) test('behavior: accepts a hyphenated code when authorizing', async () => { const store = CliAuth.Store.memory() const { codeVerifier, request } = await createRequest() const { code } = await CliAuth.createDeviceCode({ chainId: chain.id, request, store, }) const displayCode = `${code.slice(0, 4)}-${code.slice(4)}` const authorized = await CliAuth.authorize({ chainId: chain.id, request: await authorize(displayCode), store, }) const polled = await CliAuth.poll({ code, request: { codeVerifier: codeVerifier, }, store, }) expect(authorized).toMatchInlineSnapshot(` { "status": "authorized", } `) expect(polled.status).toMatchInlineSnapshot(`"authorized"`) }) test('behavior: accepts a WebAuthn signature envelope from RPC', async () => { const store = CliAuth.Store.memory() const { codeVerifier, request } = await createRequest('device-code-verifier', { accessKey: secpAccessKey, keyType: secpAccessKey.keyType, }) const { code } = await CliAuth.createDeviceCode({ chainId: chain.id, request, store, }) const authorized = await CliAuth.authorize({ chainId: chain.id, request: await authorizeWebAuthn(code), store, }) const polled = await CliAuth.poll({ code, request: { codeVerifier: codeVerifier, }, store, }) const keyAuthorization = polled.status === 'authorized' ? { ...polled.keyAuthorization, signature: { type: polled.keyAuthorization.signature.type, }, } : undefined expect({ authorized, polled: polled.status === 'authorized' ? { ...polled, keyAuthorization: keyAuthorization, } : polled, }).toMatchInlineSnapshot(` { "authorized": { "status": "authorized", }, "polled": { "accountAddress": "${webAuthnRoot.address}", "keyAuthorization": { "address": "${secpAccessKey.address}", "chainId": 1337n, "expiry": ${expiry}, "keyId": "${secpAccessKey.address}", "keyType": "secp256k1", "limits": [ { "limit": 1000n, "token": "0x20c0000000000000000000000000000000000001", }, ], "signature": { "type": "webAuthn", }, }, "status": "authorized", }, } `) }) }) describe('Store.kv', () => { test('default: persists encoded entries through KV', async () => { const store = CliAuth.Store.kv({ async delete() {}, async get<_value = unknown>(_key: string): Promise<_value> { throw new Error('Not implemented.') }, async set() {}, }) expect(typeof store.create).toMatchInlineSnapshot(`"function"`) expect(typeof store.get).toMatchInlineSnapshot(`"function"`) expect(typeof store.authorize).toMatchInlineSnapshot(`"function"`) expect(typeof store.consume).toMatchInlineSnapshot(`"function"`) expect(typeof store.delete).toMatchInlineSnapshot(`"function"`) }) }) async function createCodeChallenge(codeVerifier: string) { const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(codeVerifier)) return Base64.fromBytes(new Uint8Array(hash), { pad: false, url: true }) }