accounts
Version:
Tempo Accounts SDK
990 lines (901 loc) • 30.4 kB
text/typescript
import { mkdtemp } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import type { Address, Hex } from 'ox'
import { Hex as ox_Hex } from 'ox'
import { KeyAuthorization } from 'ox/tempo'
import { type Address as ViemAddress, hashMessage, parseUnits } from 'viem'
import { Actions, Addresses } from 'viem/tempo'
import { describe, expect, test, vi } from 'vp/test'
import type * as z from 'zod/mini'
import { accounts, chain, getClient } from '../../test/config.js'
import { createDeviceCodeHost, submitVerify } from '../../test/deviceCode.js'
import { createJsonStorage, createServer } from '../../test/utils.js'
import type * as Rpc from '../core/zod/rpc.js'
import * as Handler from '../server/Handler.js'
import * as Provider from './Provider.js'
import * as Storage from './storage.js'
const root = accounts[0]!
const accessKey = accounts[1]!
const accessKey_2 = accounts[2]!
const expiry = Math.floor(Date.now() / 1000) + 3_600
const expiry_2 = expiry + 60
function connectRequest(
options: {
accessKey?: typeof accessKey | undefined
expiry?: number | undefined
method?: 'login' | 'register' | undefined
showDeposit?: z.output<typeof Rpc.wallet_connect.showDeposit> | undefined
} = {},
) {
const { accessKey: key = accessKey, expiry: expiry_ = expiry, method, showDeposit } = options
return {
method: 'wallet_connect',
params: [
{
capabilities: {
authorizeAccessKey: {
expiry: expiry_,
keyType: key.keyType,
publicKey: key.publicKey,
},
...(method ? { method } : {}),
...(showDeposit !== undefined ? { showDeposit } : {}),
},
},
],
} as const
}
async function createStoragePath() {
return join(await mkdtemp(join(tmpdir(), 'accounts-cli-')), 'store.json')
}
async function readAccessKeys(storage: ReturnType<typeof Storage.filesystem>) {
const value = await storage.getItem<{
state: {
accessKeys: {
access: Address.Address
address: Address.Address
chainId: number
expiry?: number | undefined
handle?: { kind: string; privateKey: Hex.Hex } | undefined
keyType: string
}[]
}
}>('store')
return value!.state.accessKeys
}
async function fund(address: ViemAddress) {
await Actions.token.transferSync(getClient(), {
account: root,
feeToken: Addresses.pathUsd,
to: address,
token: Addresses.pathUsd,
amount: parseUnits('10', 6),
})
}
const transferCall = Actions.token.transfer.call({
to: '0x0000000000000000000000000000000000000001',
token: Addresses.pathUsd,
amount: parseUnits('1', 6),
})
function connectCapabilities(host: ReturnType<typeof createDeviceCodeHost>, index: number) {
const params = host.requests()[index]!.params as readonly {
capabilities?:
| NonNullable<Rpc.wallet_connect.Decoded['params']>[number]['capabilities']
| undefined
}[]
return params[0]?.capabilities
}
describe('Provider.create', () => {
test('default: bootstraps wallet_connect through the device-code flow', async () => {
const host = createDeviceCodeHost()
const server = await createServer(host.listener)
const opened: string[] = []
try {
const provider = Provider.create({
chains: [chain],
open: async (url, prompt) => {
opened.push(url)
await fetch(url)
await submitVerify(prompt)
},
host: `${server.url}/auth/device`,
name: 'Accounts playground',
storage: createJsonStorage(),
})
const result = await provider.request(connectRequest())
const account = result.accounts[0]!
const keyAuthorization = account.capabilities.keyAuthorization
? {
...account.capabilities.keyAuthorization,
signature: {
type: account.capabilities.keyAuthorization.signature.type,
},
}
: undefined
expect(opened).toHaveLength(1)
expect(host.metas()).toEqual([{ name: 'Accounts playground' }])
expect(opened[0]).toMatch(
new RegExp(`^${server.url}/auth/device/verify\\?user_code=[A-Z]{4}-[A-Z]{4}$`),
)
expect({
account: {
address: account.address,
capabilities: keyAuthorization ? { keyAuthorization } : {},
},
}).toMatchInlineSnapshot(`
{
"account": {
"address": "${root.address}",
"capabilities": {
"keyAuthorization": {
"address": "${accessKey.address.toLowerCase()}",
"chainId": "${ox_Hex.fromNumber(chain.id)}",
"expiry": "${ox_Hex.fromNumber(expiry)}",
"keyId": "${accessKey.address.toLowerCase()}",
"keyType": "secp256k1",
"signature": {
"type": "secp256k1",
},
},
},
},
}
`)
} finally {
await server.closeAsync()
}
})
test('behavior: preserves wallet_connect authorization capabilities', async () => {
const host = createDeviceCodeHost()
const server = await createServer(host.listener)
let listener: Parameters<typeof createServer>[0] | undefined
const authServer = await createServer((request, response) => {
if (!listener) {
const auth = Handler.auth({ origin: authServer.url })
listener = Handler.compose([auth], { path: '/auth' }).listener
}
return listener(request, response)
})
try {
const provider = Provider.create({
chains: [chain],
open: async (_url, prompt) => {
await submitVerify(prompt)
},
host: `${server.url}/auth/device`,
storage: createJsonStorage(),
})
const result = await provider.request({
method: 'wallet_connect',
params: [
{
capabilities: {
auth: {
resources: [
'urn:nanocodex:memory:read',
'urn:nanocodex:connector:github:repo:read',
],
returnToken: true,
url: `${authServer.url}/auth`,
},
authorizeAccessKey: {
expiry,
keyType: accessKey.keyType,
publicKey: accessKey.publicKey,
scopes: [
{
address: Addresses.pathUsd,
selector: 'transfer(address,uint256)',
},
],
},
identity: { email: true },
method: 'login',
},
},
],
})
const capabilities = connectCapabilities(host, 0)
const connected = result.accounts[0]!.capabilities
const message = connected.personalSign?.message
expect({
auth: capabilities?.auth,
authToken: connected.auth?.token ? 'present' : 'missing',
identity: capabilities?.identity,
personalSign: {
keyAuthorization: connected.personalSign?.keyAuthorization ? 'present' : 'missing',
message: message ? 'present' : 'missing',
signature: connected.signature ? 'present' : 'missing',
witness:
message && connected.personalSign?.keyAuthorization
? KeyAuthorization.deserialize(connected.personalSign.keyAuthorization).witness ===
hashMessage(message)
: false,
},
scopes: capabilities?.authorizeAccessKey?.scopes,
}).toMatchInlineSnapshot(`
{
"auth": {
"challenge": "${authServer.url}/auth/challenge",
"logout": "${authServer.url}/auth/logout",
"resources": [
"urn:nanocodex:memory:read",
"urn:nanocodex:connector:github:repo:read",
],
"returnToken": true,
"verify": "${authServer.url}/auth",
},
"authToken": "present",
"identity": {
"email": true,
},
"personalSign": {
"keyAuthorization": "present",
"message": "present",
"signature": "present",
"witness": true,
},
"scopes": [
{
"address": "${Addresses.pathUsd}",
"selector": "transfer(address,uint256)",
},
],
}
`)
} finally {
await authServer.closeAsync()
await server.closeAsync()
}
})
test('behavior: forwards showDeposit through registration device-code requests', async () => {
const host = createDeviceCodeHost()
const server = await createServer(host.listener)
try {
const provider = Provider.create({
chains: [chain],
open: async (_url, prompt) => {
await submitVerify(prompt)
},
host: `${server.url}/auth/device`,
storage: createJsonStorage(),
})
await provider.request(
connectRequest({
method: 'register',
showDeposit: {
amount: '50',
displayName: 'DoorDash',
on: 'register',
token: 'USDC',
},
}),
)
expect(connectCapabilities(host, 0)?.showDeposit).toMatchInlineSnapshot(`
{
"amount": "50",
"displayName": "DoorDash",
"on": "register",
"token": "USDC",
}
`)
} finally {
await server.closeAsync()
}
})
test('behavior: forwards showDeposit through login device-code requests', async () => {
const host = createDeviceCodeHost()
const server = await createServer(host.listener)
try {
const provider = Provider.create({
chains: [chain],
open: async (_url, prompt) => {
await submitVerify(prompt)
},
host: `${server.url}/auth/device`,
storage: createJsonStorage(),
})
await provider.request(connectRequest({ method: 'login', showDeposit: true }))
expect(connectCapabilities(host, 0)?.showDeposit).toBe(true)
} finally {
await server.closeAsync()
}
})
test('behavior: forwards showDeposit through wallet_authorizeAccessKey device-code requests', async () => {
const host = createDeviceCodeHost()
const server = await createServer(host.listener)
try {
const provider = Provider.create({
chains: [chain],
open: async (_url, prompt) => {
await submitVerify(prompt)
},
host: `${server.url}/auth/device`,
storage: createJsonStorage(),
})
await provider.request(connectRequest())
await provider.request({
method: 'wallet_authorizeAccessKey',
params: [
{
expiry,
keyType: accessKey.keyType,
publicKey: accessKey.publicKey,
showDeposit: true,
},
],
})
const params = host.requests()[1]!.params as readonly { showDeposit?: unknown }[]
expect(host.requests()[1]!.method).toBe('wallet_authorizeAccessKey')
expect(params[0]?.showDeposit).toBe(true)
} finally {
await server.closeAsync()
}
})
test('behavior: browser-open failures surface the URL and code', async () => {
const host = createDeviceCodeHost()
const server = await createServer(host.listener)
try {
const provider = Provider.create({
chains: [chain],
open() {
throw new Error('browser unavailable')
},
host: `${server.url}/auth/device`,
storage: createJsonStorage(),
})
const error = await provider.request(connectRequest()).then(
() => undefined,
(error: Error) => error,
)
expect(error?.message).toMatch(
/^Failed to surface device code [A-Z]{4}-[A-Z]{4}\. Open .*\/auth\/device\/verify\?user_code=[A-Z]{4}-[A-Z]{4} manually\.$/,
)
} finally {
await server.closeAsync()
}
})
test('behavior: prints the user code when the complete verification URI is absent', async () => {
const host = createDeviceCodeHost({ omitVerificationUriFull: true })
const server = await createServer(host.listener)
const write = vi.spyOn(process.stdout, 'write').mockImplementation(() => true)
try {
const provider = Provider.create({
chains: [chain],
open: async (url, prompt) => {
expect(url).toBe(prompt.verificationUri)
await submitVerify(prompt)
},
host: `${server.url}/auth/device`,
storage: createJsonStorage(),
})
await provider.request(connectRequest())
expect(write).toHaveBeenCalledWith(
expect.stringMatching(/^Enter code [A-Z]{4}-[A-Z]{4} at http/),
)
} finally {
write.mockRestore()
await server.closeAsync()
}
})
test('behavior: times out while waiting for authorization', async () => {
const host = createDeviceCodeHost({ pollingInterval: 10 })
const server = await createServer(host.listener)
try {
const provider = Provider.create({
chains: [chain],
open() {},
host: `${server.url}/auth/device`,
storage: createJsonStorage(),
timeout: 100,
})
const error = await provider.request(connectRequest()).then(
() => undefined,
(error: Error) => error,
)
expect(error?.message).toMatch(
/^Timed out waiting for device code [A-Z]{4}-[A-Z]{4}\. Continue at /,
)
} finally {
await server.closeAsync()
}
})
test('behavior: wallet_authorizeAccessKey requires a connected account', async () => {
const host = createDeviceCodeHost()
const server = await createServer(host.listener)
try {
const provider = Provider.create({
chains: [chain],
open() {
throw new Error('Unexpected browser open.')
},
host: `${server.url}/auth/device`,
storage: createJsonStorage(),
})
await expect(
provider.request({
method: 'wallet_authorizeAccessKey',
params: [
{ expiry: expiry_2, keyType: accessKey_2.keyType, publicKey: accessKey_2.publicKey },
],
}),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Provider.DisconnectedError: No active account.]`,
)
} finally {
await server.closeAsync()
}
})
test('behavior: personal_sign requires a connected account without opening the browser', async () => {
const host = createDeviceCodeHost()
const server = await createServer(host.listener)
try {
const provider = Provider.create({
chains: [chain],
open() {
throw new Error('Unexpected browser open.')
},
host: `${server.url}/auth/device`,
storage: createJsonStorage(),
})
await expect(
provider.request({
method: 'personal_sign',
params: ['0x68656c6c6f', root.address],
}),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Provider.DisconnectedError: No accounts connected.]`,
)
expect(host.requests()).toHaveLength(0)
} finally {
await server.closeAsync()
}
})
test('behavior: authorizes an access key for the active account', async () => {
const host = createDeviceCodeHost()
const server = await createServer(host.listener)
const opened: string[] = []
try {
const provider = Provider.create({
chains: [chain],
open: async (url, prompt) => {
opened.push(url)
await submitVerify(prompt)
},
host: `${server.url}/auth/device`,
storage: createJsonStorage(),
})
await provider.request(connectRequest())
const result = await provider.request({
method: 'wallet_authorizeAccessKey',
params: [
{ expiry: expiry_2, keyType: accessKey_2.keyType, publicKey: accessKey_2.publicKey },
],
})
const keyAuthorization = {
...result.keyAuthorization,
signature: {
type: result.keyAuthorization.signature.type,
},
}
expect(opened).toHaveLength(2)
expect({
keyAuthorization,
rootAddress: result.rootAddress,
}).toMatchInlineSnapshot(`
{
"keyAuthorization": {
"address": "${accessKey_2.address.toLowerCase()}",
"chainId": "${ox_Hex.fromNumber(chain.id)}",
"expiry": "${ox_Hex.fromNumber(expiry_2)}",
"keyId": "${accessKey_2.address.toLowerCase()}",
"keyType": "secp256k1",
"signature": {
"type": "secp256k1",
},
},
"rootAddress": "${root.address}",
}
`)
} finally {
await server.closeAsync()
}
})
test('behavior: forwards revokeAccessKey after bootstrap', async () => {
const host = createDeviceCodeHost()
const server = await createServer(host.listener)
try {
const provider = Provider.create({
chains: [chain],
open: async (_url, prompt) => {
await submitVerify(prompt)
},
host: `${server.url}/auth/device`,
storage: createJsonStorage(),
})
await provider.request(connectRequest())
await provider.request({
method: 'wallet_revokeAccessKey',
params: [{ accessKeyAddress: accessKey.address, address: root.address }],
})
expect(host.requests().map(({ method }) => method)).toEqual([
'wallet_connect',
'wallet_revokeAccessKey',
])
} finally {
await server.closeAsync()
}
})
test('behavior: generates, persists, and reuses a managed key during wallet_connect', async () => {
const host = createDeviceCodeHost()
const server = await createServer(host.listener)
const storagePath = await createStoragePath()
const storage = Storage.filesystem({ path: storagePath })
try {
const provider = Provider.create({
chains: [chain],
open: async (_url, prompt) => {
await submitVerify(prompt)
},
host: `${server.url}/auth/device`,
storage,
})
const result = await provider.request({
method: 'wallet_connect',
params: [{ capabilities: { authorizeAccessKey: { expiry: expiry_2 } } }],
})
const account = result.accounts[0]!
await fund(account.address)
const receipt = await provider.request({
method: 'eth_sendTransactionSync',
params: [{ calls: [transferCall] }],
})
const storage_2 = Storage.filesystem({ path: storagePath })
const provider_2 = Provider.create({
chains: [chain],
open() {
throw new Error('Unexpected browser open.')
},
host: `${server.url}/auth/device`,
storage: storage_2,
})
const receipt_2 = await provider_2.request({
method: 'eth_sendTransactionSync',
params: [{ calls: [transferCall] }],
})
const [entry] = await readAccessKeys(storage)
expect({
initial: receipt.status,
restored: receipt_2.status,
}).toMatchInlineSnapshot(`
{
"initial": "0x1",
"restored": "0x1",
}
`)
expect(entry!.access).toBe('0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266')
expect(entry!.chainId).toBe(1337)
expect(entry!.keyType).toBe('p256')
expect(entry!.address).toMatch(/^0x[0-9a-f]{40}$/i)
// The CLI default is an extractable WebCrypto P-256 key (JWK handle),
// which survives the filesystem storage round-trip above.
expect(entry!.handle).toMatchObject({ kind: 'webcrypto-p256' })
expect((entry!.handle as { jwk?: { d?: string } }).jwk?.d).toBeTruthy()
} finally {
await server.closeAsync()
}
})
test('behavior: forwards signing even when a managed access key is available', async () => {
const host = createDeviceCodeHost()
const server = await createServer(host.listener)
try {
const provider = Provider.create({
chains: [chain],
open: async (_url, prompt) => {
await submitVerify(prompt)
},
host: `${server.url}/auth/device`,
storage: createJsonStorage(),
})
const connected = await provider.request({
method: 'wallet_connect',
params: [{ capabilities: { authorizeAccessKey: { expiry: expiry_2 } } }],
})
const address = connected.accounts[0]!.address
const personal = await provider.request({
method: 'personal_sign',
params: ['0x68656c6c6f', address],
})
const typedData = await provider.request({
method: 'eth_signTypedData_v4',
params: [
address,
JSON.stringify({
domain: {},
message: { contents: 'hello' },
primaryType: 'Message',
types: {
EIP712Domain: [],
Message: [{ name: 'contents', type: 'string' }],
},
}),
],
})
expect(personal).toMatch(/^0x/)
expect(typedData).toMatch(/^0x/)
expect(host.requests().map((request) => request.method)).toMatchInlineSnapshot(`
[
"wallet_connect",
"personal_sign",
"eth_signTypedData_v4",
]
`)
} finally {
await server.closeAsync()
}
})
test('behavior: forwards signing with a scoped access key', async () => {
const host = createDeviceCodeHost()
const server = await createServer(host.listener)
try {
const provider = Provider.create({
chains: [chain],
open: async (_url, prompt) => {
await submitVerify(prompt)
},
host: `${server.url}/auth/device`,
storage: createJsonStorage(),
})
const connected = await provider.request({
method: 'wallet_connect',
params: [
{
capabilities: {
authorizeAccessKey: {
expiry: expiry_2,
keyType: accessKey.keyType,
publicKey: accessKey.publicKey,
scopes: [
{
address: Addresses.pathUsd,
selector: 'transfer(address,uint256)',
},
],
},
},
},
],
})
const address = connected.accounts[0]!.address
const personal = await provider.request({
method: 'personal_sign',
params: ['0x68656c6c6f', address],
})
const typedData = await provider.request({
method: 'eth_signTypedData_v4',
params: [
address,
JSON.stringify({
domain: {},
message: { contents: 'hello' },
primaryType: 'Message',
types: {
EIP712Domain: [],
Message: [{ name: 'contents', type: 'string' }],
},
}),
],
})
expect({
methods: host.requests().map((request) => request.method),
personal: personal ? expect.any(String) : personal,
typedData: typedData ? expect.any(String) : typedData,
}).toMatchInlineSnapshot(`
{
"methods": [
"wallet_connect",
"personal_sign",
"eth_signTypedData_v4",
],
"personal": Any<String>,
"typedData": Any<String>,
}
`)
} finally {
await server.closeAsync()
}
})
test('behavior: generates a managed key for wallet_authorizeAccessKey without publicKey', async () => {
const host = createDeviceCodeHost()
const server = await createServer(host.listener)
const storagePath = await createStoragePath()
const storage = Storage.filesystem({ path: storagePath })
try {
const provider = Provider.create({
chains: [chain],
open: async (_url, prompt) => {
await submitVerify(prompt)
},
host: `${server.url}/auth/device`,
storage,
})
const connected = await provider.request({
method: 'wallet_connect',
params: [{ capabilities: {} }],
})
const result = await provider.request({
method: 'wallet_authorizeAccessKey',
params: [{ expiry: expiry_2 }],
})
await fund(result.rootAddress)
const receipt = await provider.request({
method: 'eth_sendTransactionSync',
params: [{ calls: [transferCall] }],
})
const [entry] = await readAccessKeys(storage)
expect(connected.accounts[0]!.address).toBe(root.address)
expect(receipt.status).toMatchInlineSnapshot(`"0x1"`)
expect({
access: entry!.access,
chainId: entry!.chainId,
}).toMatchInlineSnapshot(`
{
"access": "${root.address}",
"chainId": ${chain.id},
}
`)
} finally {
await server.closeAsync()
}
})
test('behavior: wallet_updateAccessKey replaces an unpublished key authorization', async () => {
const host = createDeviceCodeHost()
const server = await createServer(host.listener)
const storagePath = await createStoragePath()
const storage = Storage.filesystem({ path: storagePath })
try {
const provider = Provider.create({
chains: [chain],
open: async (_url, prompt) => {
await submitVerify(prompt)
},
host: `${server.url}/auth/device`,
storage,
})
const result = await provider.request({
method: 'wallet_connect',
params: [
{
capabilities: {
authorizeAccessKey: {
expiry: expiry_2,
limits: [{ limit: ox_Hex.fromNumber(1n), token: Addresses.pathUsd }],
},
},
},
],
})
const account = result.accounts[0]!
const accessKeyAddress = account.capabilities.keyAuthorization!.keyId
const updated = parseUnits('9', 6)
await provider.request({
method: 'wallet_updateAccessKey',
params: [
{
address: account.address,
accessKeyAddress,
limits: [{ limit: ox_Hex.fromNumber(updated), token: Addresses.pathUsd }],
},
],
})
const [stored] = provider.store.accessKeys.list({
accessKey: accessKeyAddress,
account: account.address,
chainId: chain.id,
})
expect(stored?.keyAuthorization?.limits).toEqual([
{ limit: updated, token: Addresses.pathUsd },
])
expect(
await provider.store.accessKeys.getStatus({
accessKey: accessKeyAddress,
account: account.address,
chainId: chain.id,
client: getClient(),
}),
).toBe('pending')
} finally {
await server.closeAsync()
}
})
test('behavior: wallet_updateAccessKey updates published spending limits through approval', async () => {
const host = createDeviceCodeHost()
const server = await createServer(host.listener)
const storagePath = await createStoragePath()
const storage = Storage.filesystem({ path: storagePath })
try {
const provider = Provider.create({
chains: [chain],
open: async (_url, prompt) => {
await submitVerify(prompt)
},
host: `${server.url}/auth/device`,
storage,
})
const result = await provider.request({
method: 'wallet_connect',
params: [{ capabilities: { authorizeAccessKey: { expiry: expiry_2 } } }],
})
const account = result.accounts[0]!
const accessKeyAddress = account.capabilities.keyAuthorization!.keyId
await fund(account.address)
await provider.request({
method: 'eth_sendTransactionSync',
params: [{ calls: [transferCall] }],
})
const updated = parseUnits('9', 6)
await provider.request({
method: 'wallet_updateAccessKey',
params: [
{
address: account.address,
accessKeyAddress,
limits: [{ limit: ox_Hex.fromNumber(updated), token: Addresses.pathUsd }],
},
],
})
const { remaining } = await Actions.accessKey.getRemainingLimit(getClient(), {
account: account.address,
accessKey: accessKeyAddress,
token: Addresses.pathUsd,
})
expect(remaining).toBe(updated)
} finally {
await server.closeAsync()
}
})
test('behavior: regenerates a managed key when the requested key type changes', async () => {
const host = createDeviceCodeHost()
const server = await createServer(host.listener)
const storagePath = await createStoragePath()
const storage = Storage.filesystem({ path: storagePath })
try {
const provider = Provider.create({
chains: [chain],
open: async (_url, prompt) => {
await submitVerify(prompt)
},
host: `${server.url}/auth/device`,
storage,
})
await provider.request({
method: 'wallet_connect',
params: [{ capabilities: {} }],
})
// secp256k1 is requested explicitly; p256 is the default — a genuine
// type change that regenerates the managed key.
const first = await provider.request({
method: 'wallet_authorizeAccessKey',
params: [{ expiry, keyType: 'secp256k1' }],
})
const second = await provider.request({
method: 'wallet_authorizeAccessKey',
params: [{ expiry: expiry_2, keyType: 'p256' }],
})
await fund(second.rootAddress)
const receipt = await provider.request({
method: 'eth_sendTransactionSync',
params: [{ calls: [transferCall] }],
})
const keys = await readAccessKeys(storage)
expect(first.keyAuthorization.keyType).toBe('secp256k1')
expect(second.keyAuthorization.keyType).toBe('p256')
expect(first.keyAuthorization.keyId).not.toBe(second.keyAuthorization.keyId)
expect(new Set(keys.map((key) => key.keyType))).toEqual(new Set(['secp256k1', 'p256']))
expect(receipt.status).toBe('0x1')
} finally {
await server.closeAsync()
}
})
})