accounts 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +21 -0
  3. package/README.md +97 -0
  4. package/dist/core/AccessKey.d.ts +55 -0
  5. package/dist/core/AccessKey.d.ts.map +1 -0
  6. package/dist/core/AccessKey.js +69 -0
  7. package/dist/core/AccessKey.js.map +1 -0
  8. package/dist/core/Account.d.ts +91 -0
  9. package/dist/core/Account.d.ts.map +1 -0
  10. package/dist/core/Account.js +64 -0
  11. package/dist/core/Account.js.map +1 -0
  12. package/dist/core/Adapter.d.ts +187 -0
  13. package/dist/core/Adapter.d.ts.map +1 -0
  14. package/dist/core/Adapter.js +7 -0
  15. package/dist/core/Adapter.js.map +1 -0
  16. package/dist/core/Ceremony.d.ts +109 -0
  17. package/dist/core/Ceremony.d.ts.map +1 -0
  18. package/dist/core/Ceremony.js +104 -0
  19. package/dist/core/Ceremony.js.map +1 -0
  20. package/dist/core/Client.d.ts +16 -0
  21. package/dist/core/Client.d.ts.map +1 -0
  22. package/dist/core/Client.js +18 -0
  23. package/dist/core/Client.js.map +1 -0
  24. package/dist/core/Dialog.d.ts +52 -0
  25. package/dist/core/Dialog.d.ts.map +1 -0
  26. package/dist/core/Dialog.js +342 -0
  27. package/dist/core/Dialog.js.map +1 -0
  28. package/dist/core/Expiry.d.ts +15 -0
  29. package/dist/core/Expiry.d.ts.map +1 -0
  30. package/dist/core/Expiry.js +29 -0
  31. package/dist/core/Expiry.js.map +1 -0
  32. package/dist/core/Messenger.d.ts +86 -0
  33. package/dist/core/Messenger.d.ts.map +1 -0
  34. package/dist/core/Messenger.js +127 -0
  35. package/dist/core/Messenger.js.map +1 -0
  36. package/dist/core/Provider.d.ts +69 -0
  37. package/dist/core/Provider.d.ts.map +1 -0
  38. package/dist/core/Provider.js +401 -0
  39. package/dist/core/Provider.js.map +1 -0
  40. package/dist/core/Remote.d.ts +114 -0
  41. package/dist/core/Remote.d.ts.map +1 -0
  42. package/dist/core/Remote.js +116 -0
  43. package/dist/core/Remote.js.map +1 -0
  44. package/dist/core/Schema.d.ts +805 -0
  45. package/dist/core/Schema.d.ts.map +1 -0
  46. package/dist/core/Schema.js +43 -0
  47. package/dist/core/Schema.js.map +1 -0
  48. package/dist/core/Storage.d.ts +42 -0
  49. package/dist/core/Storage.d.ts.map +1 -0
  50. package/dist/core/Storage.js +173 -0
  51. package/dist/core/Storage.js.map +1 -0
  52. package/dist/core/Store.d.ts +58 -0
  53. package/dist/core/Store.d.ts.map +1 -0
  54. package/dist/core/Store.js +58 -0
  55. package/dist/core/Store.js.map +1 -0
  56. package/dist/core/adapters/dangerous_secp256k1.d.ts +30 -0
  57. package/dist/core/adapters/dangerous_secp256k1.d.ts.map +1 -0
  58. package/dist/core/adapters/dangerous_secp256k1.js +39 -0
  59. package/dist/core/adapters/dangerous_secp256k1.js.map +1 -0
  60. package/dist/core/adapters/dialog.d.ts +31 -0
  61. package/dist/core/adapters/dialog.d.ts.map +1 -0
  62. package/dist/core/adapters/dialog.js +306 -0
  63. package/dist/core/adapters/dialog.js.map +1 -0
  64. package/dist/core/adapters/local.d.ts +33 -0
  65. package/dist/core/adapters/local.d.ts.map +1 -0
  66. package/dist/core/adapters/local.js +227 -0
  67. package/dist/core/adapters/local.js.map +1 -0
  68. package/dist/core/adapters/webAuthn.d.ts +36 -0
  69. package/dist/core/adapters/webAuthn.d.ts.map +1 -0
  70. package/dist/core/adapters/webAuthn.js +93 -0
  71. package/dist/core/adapters/webAuthn.js.map +1 -0
  72. package/dist/core/internal/withDedupe.d.ts +12 -0
  73. package/dist/core/internal/withDedupe.d.ts.map +1 -0
  74. package/dist/core/internal/withDedupe.js +12 -0
  75. package/dist/core/internal/withDedupe.js.map +1 -0
  76. package/dist/core/zod/request.d.ts +31 -0
  77. package/dist/core/zod/request.d.ts.map +1 -0
  78. package/dist/core/zod/request.js +41 -0
  79. package/dist/core/zod/request.js.map +1 -0
  80. package/dist/core/zod/rpc.d.ts +603 -0
  81. package/dist/core/zod/rpc.d.ts.map +1 -0
  82. package/dist/core/zod/rpc.js +293 -0
  83. package/dist/core/zod/rpc.js.map +1 -0
  84. package/dist/core/zod/utils.d.ts +18 -0
  85. package/dist/core/zod/utils.d.ts.map +1 -0
  86. package/dist/core/zod/utils.js +21 -0
  87. package/dist/core/zod/utils.js.map +1 -0
  88. package/dist/index.d.ts +15 -0
  89. package/dist/index.d.ts.map +1 -0
  90. package/dist/index.js +15 -0
  91. package/dist/index.js.map +1 -0
  92. package/dist/internal/types.d.ts +284 -0
  93. package/dist/internal/types.d.ts.map +1 -0
  94. package/dist/internal/types.js +2 -0
  95. package/dist/internal/types.js.map +1 -0
  96. package/dist/server/Handler.d.ts +257 -0
  97. package/dist/server/Handler.d.ts.map +1 -0
  98. package/dist/server/Handler.js +433 -0
  99. package/dist/server/Handler.js.map +1 -0
  100. package/dist/server/Kv.d.ts +16 -0
  101. package/dist/server/Kv.d.ts.map +1 -0
  102. package/dist/server/Kv.js +30 -0
  103. package/dist/server/Kv.js.map +1 -0
  104. package/dist/server/index.d.ts +3 -0
  105. package/dist/server/index.d.ts.map +1 -0
  106. package/dist/server/index.js +3 -0
  107. package/dist/server/index.js.map +1 -0
  108. package/dist/server/internal/requestListener.d.ts +124 -0
  109. package/dist/server/internal/requestListener.d.ts.map +1 -0
  110. package/dist/server/internal/requestListener.js +173 -0
  111. package/dist/server/internal/requestListener.js.map +1 -0
  112. package/dist/wagmi/Connector.d.ts +93 -0
  113. package/dist/wagmi/Connector.d.ts.map +1 -0
  114. package/dist/wagmi/Connector.js +238 -0
  115. package/dist/wagmi/Connector.js.map +1 -0
  116. package/dist/wagmi/index.d.ts +3 -0
  117. package/dist/wagmi/index.d.ts.map +1 -0
  118. package/dist/wagmi/index.js +3 -0
  119. package/dist/wagmi/index.js.map +1 -0
  120. package/package.json +56 -2
  121. package/src/core/AccessKey.test.ts +257 -0
  122. package/src/core/AccessKey.ts +123 -0
  123. package/src/core/Account.test.ts +309 -0
  124. package/src/core/Account.ts +152 -0
  125. package/src/core/Adapter.ts +238 -0
  126. package/src/core/Ceremony.browser.test.ts +239 -0
  127. package/src/core/Ceremony.test.ts +151 -0
  128. package/src/core/Ceremony.ts +203 -0
  129. package/src/core/Client.ts +36 -0
  130. package/src/core/Dialog.browser.test.ts +309 -0
  131. package/src/core/Dialog.test-d.ts +19 -0
  132. package/src/core/Dialog.ts +442 -0
  133. package/src/core/Expiry.ts +34 -0
  134. package/src/core/Messenger.ts +206 -0
  135. package/src/core/Provider.browser.test.ts +774 -0
  136. package/src/core/Provider.connect.browser.test.ts +415 -0
  137. package/src/core/Provider.test-d.ts +53 -0
  138. package/src/core/Provider.test.ts +1566 -0
  139. package/src/core/Provider.ts +559 -0
  140. package/src/core/Remote.ts +262 -0
  141. package/src/core/Schema.test-d.ts +211 -0
  142. package/src/core/Schema.ts +143 -0
  143. package/src/core/Storage.ts +213 -0
  144. package/src/core/Store.test.ts +287 -0
  145. package/src/core/Store.ts +129 -0
  146. package/src/core/adapters/dangerous_secp256k1.ts +53 -0
  147. package/src/core/adapters/dialog.ts +379 -0
  148. package/src/core/adapters/local.test.ts +97 -0
  149. package/src/core/adapters/local.ts +277 -0
  150. package/src/core/adapters/webAuthn.ts +129 -0
  151. package/src/core/internal/withDedupe.test.ts +116 -0
  152. package/src/core/internal/withDedupe.ts +20 -0
  153. package/src/core/mppx.test.ts +83 -0
  154. package/src/core/zod/request.test.ts +121 -0
  155. package/src/core/zod/request.ts +70 -0
  156. package/src/core/zod/rpc.ts +374 -0
  157. package/src/core/zod/utils.test.ts +69 -0
  158. package/src/core/zod/utils.ts +40 -0
  159. package/src/index.ts +14 -0
  160. package/src/internal/types.ts +378 -0
  161. package/src/server/Handler.test.ts +1014 -0
  162. package/src/server/Handler.ts +605 -0
  163. package/src/server/Kv.ts +46 -0
  164. package/src/server/index.ts +2 -0
  165. package/src/server/internal/requestListener.ts +273 -0
  166. package/src/tsconfig.json +9 -0
  167. package/src/wagmi/Connector.ts +287 -0
  168. package/src/wagmi/index.ts +2 -0
@@ -0,0 +1,152 @@
1
+ import { Provider, type WebCryptoP256 } from 'ox'
2
+ import { type KeyAuthorization } from 'ox/tempo'
3
+ import type { Hex } from 'viem'
4
+ import type { Address, JsonRpcAccount } from 'viem/accounts'
5
+ import { Account as TempoAccount } from 'viem/tempo'
6
+
7
+ import type { OneOf } from '../internal/types.js'
8
+ import type * as core_Store from './Store.js'
9
+
10
+ /** Account stored in the provider state. */
11
+ export type Store = {
12
+ /** Account address. */
13
+ address: Address
14
+ } & OneOf<
15
+ | {}
16
+ | Pick<TempoAccount.Account, 'keyType' | 'sign'>
17
+ | { keyType: 'secp256k1'; privateKey: Hex }
18
+ | { keyType: 'p256'; privateKey: Hex }
19
+ | { keyType: 'webAuthn'; credential: { id: string; publicKey: Hex; rpId: string } }
20
+ | {
21
+ keyType: 'webCrypto'
22
+ keyPair: Awaited<ReturnType<typeof WebCryptoP256.createKeyPair>>
23
+ }
24
+ | {
25
+ keyType: 'webAuthn_headless'
26
+ privateKey: Hex
27
+ rpId: string
28
+ origin: string
29
+ }
30
+ >
31
+
32
+ /** Access key entry stored alongside accounts. */
33
+ export type AccessKey = {
34
+ /** Access key address. */
35
+ address: Address
36
+ /** Owner of the access key. */
37
+ access: Address
38
+ /** Unix timestamp when the access key expires. */
39
+ expiry?: number | undefined
40
+ /** Signed key authorization to attach to the first transaction. Consumed on use. */
41
+ keyAuthorization?: KeyAuthorization.Signed | undefined
42
+ /** The WebCrypto key pair backing the access key. Only present for locally-generated keys. */
43
+ keyPair?: Awaited<ReturnType<typeof WebCryptoP256.createKeyPair>> | undefined
44
+ /** Key type. */
45
+ keyType: 'secp256k1' | 'p256' | 'webAuthn' | 'webCrypto'
46
+ /** TIP-20 spending limits for the access key. */
47
+ limits?: { token: Address; limit: bigint }[] | undefined
48
+ }
49
+
50
+ /** Resolves a viem Account from the store by address (or active account). */
51
+ export function find(options: find.Options & { signable: true }): TempoAccount.Account
52
+ export function find(options: find.Options): TempoAccount.Account | JsonRpcAccount
53
+ export function find(options: find.Options): TempoAccount.Account | JsonRpcAccount {
54
+ const { accessKey = true, address, signable = false, store } = options
55
+ const { accessKeys, accounts, activeAccount } = store.getState()
56
+
57
+ const activeAddr = accounts[activeAccount]?.address
58
+ const root = address
59
+ ? accounts.find((a) => a.address === address)
60
+ : accounts.find((a) => a.address === activeAddr)
61
+ if (!root)
62
+ throw address
63
+ ? new Provider.UnauthorizedError({ message: `Account "${address}" not found.` })
64
+ : new Provider.DisconnectedError({ message: 'No active account.' })
65
+
66
+ // When accessKey is requested, prefer a locally-signable access key for this address.
67
+ if (accessKey) {
68
+ const key = accessKeys.find(
69
+ (a) => a.access.toLowerCase() === root.address.toLowerCase() && a.keyPair,
70
+ )
71
+ if (key) {
72
+ // Remove expired access keys.
73
+ if (key.expiry && key.expiry < Date.now() / 1000)
74
+ store.setState({ accessKeys: accessKeys.filter((a) => a !== key) })
75
+ else return hydrateAccessKey(key) as never
76
+ }
77
+ }
78
+
79
+ return hydrate(root, { signable }) as never
80
+ }
81
+
82
+ export declare namespace find {
83
+ type Options = {
84
+ /** Whether to resolve an access key for this account. @default true */
85
+ accessKey?: boolean | undefined
86
+ /** Address to resolve. Defaults to the active account. */
87
+ address?: Address | undefined
88
+ /** Whether to hydrate signing capability. @default false */
89
+ signable?: boolean | undefined
90
+ /** Reactive state store. */
91
+ store: core_Store.Store
92
+ }
93
+ }
94
+
95
+ /** Overloaded signature for `find` without `store` (pre-bound by the provider). */
96
+ export type Find = {
97
+ (options: Omit<find.Options, 'store'> & { signable: true }): TempoAccount.Account
98
+ (options?: Omit<find.Options, 'store'>): TempoAccount.Account | JsonRpcAccount
99
+ }
100
+
101
+ /** Hydrates an access key entry to a viem Account. Only works for locally-generated keys with a `keyPair`. */
102
+ export function hydrateAccessKey(accessKey: AccessKey): TempoAccount.Account {
103
+ if (!accessKey.keyPair)
104
+ throw new Provider.UnauthorizedError({
105
+ message: 'External access key cannot be hydrated for signing.',
106
+ })
107
+ return TempoAccount.fromWebCryptoP256(accessKey.keyPair, { access: accessKey.access })
108
+ }
109
+
110
+ /** Hydrates a store account to a viem Account. */
111
+ export function hydrate(account: Store, options: { signable: true }): TempoAccount.Account
112
+ export function hydrate(
113
+ account: Store,
114
+ options?: hydrate.Options,
115
+ ): TempoAccount.Account | JsonRpcAccount
116
+ export function hydrate(
117
+ account: Store,
118
+ options: hydrate.Options = {},
119
+ ): TempoAccount.Account | JsonRpcAccount {
120
+ const { signable = false } = options
121
+ if (!signable) return { address: account.address, type: 'json-rpc' }
122
+ if ('sign' in account && typeof account.sign === 'function')
123
+ return account as TempoAccount.Account
124
+ if (!account.keyType)
125
+ throw new Provider.UnauthorizedError({ message: `Account "${account.address}" cannot sign.` })
126
+ switch (account.keyType) {
127
+ case 'secp256k1':
128
+ return TempoAccount.fromSecp256k1(account.privateKey)
129
+ case 'p256':
130
+ return TempoAccount.fromP256(account.privateKey)
131
+ case 'webCrypto':
132
+ return TempoAccount.fromWebCryptoP256(account.keyPair)
133
+ case 'webAuthn':
134
+ return TempoAccount.fromWebAuthnP256(account.credential, {
135
+ rpId: account.credential.rpId,
136
+ })
137
+ case 'webAuthn_headless':
138
+ return TempoAccount.fromHeadlessWebAuthn(account.privateKey, {
139
+ rpId: account.rpId,
140
+ origin: account.origin,
141
+ })
142
+ default:
143
+ throw new Provider.UnauthorizedError({ message: 'Unknown key type.' })
144
+ }
145
+ }
146
+
147
+ export declare namespace hydrate {
148
+ type Options = {
149
+ /** Whether to hydrate signing capability. @default false */
150
+ signable?: boolean | undefined
151
+ }
152
+ }
@@ -0,0 +1,238 @@
1
+ import type { KeyAuthorization } from 'ox/tempo'
2
+ import type { Client, Hex, Transport } from 'viem'
3
+ import type { Address } from 'viem/accounts'
4
+ import type { tempo } from 'viem/chains'
5
+
6
+ import type * as Account from './Account.js'
7
+ import type * as Schema from './Schema.js'
8
+ import type * as Storage from './Storage.js'
9
+ import type * as Store from './Store.js'
10
+ import type * as Rpc from './zod/rpc.js'
11
+
12
+ /** Wire-format request (method + params) for a given RPC schema item. */
13
+ type EncodedRequest<encoded extends { method: unknown; params: unknown }> = Pick<
14
+ encoded,
15
+ 'method' | 'params'
16
+ >
17
+
18
+ /** Adapter interface for the provider. */
19
+ export type Adapter = SetupFn & Meta
20
+
21
+ /** The setup function an adapter must implement. */
22
+ export type SetupFn = (params: SetupFn.Parameters) => Instance
23
+
24
+ /** Static metadata attached to an adapter function. */
25
+ export type Meta = {
26
+ /** Data URI of the provider icon. @default Black 1×1 SVG. */
27
+ icon?: `data:image/${string}` | undefined
28
+ /** Display name of the provider (e.g. `"My Wallet"`). @default "Injected Wallet" */
29
+ name?: string | undefined
30
+ /** Reverse DNS identifier (e.g. `"com.example.mywallet"`). @default `com.{lowercase name}` */
31
+ rdns?: string | undefined
32
+ }
33
+
34
+ export type Instance = {
35
+ /** Adapter actions dispatched by the provider's `request()` method. */
36
+ actions: {
37
+ /** Grant an access key for the active account. */
38
+ authorizeAccessKey?:
39
+ | ((
40
+ params: authorizeAccessKey.Parameters,
41
+ request: EncodedRequest<Rpc.wallet_authorizeAccessKey.Encoded>,
42
+ ) => Promise<authorizeAccessKey.ReturnType>)
43
+ | undefined
44
+ /** Create a new account (e.g. WebAuthn registration). */
45
+ createAccount: (
46
+ params: createAccount.Parameters,
47
+ request: EncodedRequest<Rpc.wallet_connect.Encoded>,
48
+ ) => Promise<createAccount.ReturnType>
49
+ /** Disconnect hook for adapter-specific cleanup. */
50
+ disconnect?: (() => Promise<void>) | undefined
51
+ /** Discover existing accounts (e.g. WebAuthn assertion). */
52
+ loadAccounts: (
53
+ params: loadAccounts.Parameters | undefined,
54
+ request: EncodedRequest<Rpc.wallet_connect.Encoded>,
55
+ ) => Promise<loadAccounts.ReturnType>
56
+ /** Revoke an access key. */
57
+ revokeAccessKey?:
58
+ | ((
59
+ params: revokeAccessKey.Parameters,
60
+ request: EncodedRequest<Rpc.wallet_revokeAccessKey.Encoded>,
61
+ ) => Promise<void>)
62
+ | undefined
63
+ /** Send a transaction. */
64
+ sendTransaction: (
65
+ params: sendTransaction.Parameters,
66
+ request: EncodedRequest<Rpc.eth_sendTransaction.Encoded>,
67
+ ) => Promise<sendTransaction.ReturnType>
68
+ /** Send a transaction and wait for the receipt. */
69
+ sendTransactionSync: (
70
+ params: sendTransactionSync.Parameters,
71
+ request: EncodedRequest<Rpc.eth_sendTransactionSync.Encoded>,
72
+ ) => Promise<sendTransactionSync.ReturnType>
73
+ /** Sign a personal message (EIP-191). */
74
+ signPersonalMessage: (
75
+ params: signPersonalMessage.Parameters,
76
+ request: EncodedRequest<Rpc.personal_sign.Encoded>,
77
+ ) => Promise<Hex>
78
+ /** Sign a transaction without broadcasting it. */
79
+ signTransaction: (
80
+ params: signTransaction.Parameters,
81
+ request: EncodedRequest<Rpc.eth_signTransaction.Encoded>,
82
+ ) => Promise<signTransaction.ReturnType>
83
+ /** Sign EIP-712 typed data. */
84
+ signTypedData: (
85
+ params: signTypedData.Parameters,
86
+ request: EncodedRequest<Rpc.eth_signTypedData_v4.Encoded>,
87
+ ) => Promise<Hex>
88
+ /** Switch chain hook for adapter-specific handling. */
89
+ switchChain?: ((params: switchChain.Parameters) => Promise<void>) | undefined
90
+ }
91
+ /** Cleanup function called when the provider is destroyed. */
92
+ cleanup?: (() => void) | undefined
93
+ /** When `true`, the provider merges new accounts onto existing ones instead of replacing. */
94
+ persistAccounts?: boolean | undefined
95
+ }
96
+
97
+ export declare namespace SetupFn {
98
+ /** Parameters passed to an adapter's setup function. */
99
+ export type Parameters = {
100
+ /** Returns the rehydrated local account for the given address, or the active account if omitted. */
101
+ getAccount: Account.Find
102
+ /** Get the viem client for a given chain ID. Defaults to the active chain. */
103
+ getClient: (options?: getClient.Options | undefined) => Client<Transport, typeof tempo>
104
+ /** Storage adapter used by the provider. */
105
+ storage: Storage.Storage
106
+ /** Reactive state store. */
107
+ store: Store.Store
108
+ }
109
+
110
+ /** Value returned from an adapter's setup function. */
111
+ export type ReturnType = Instance
112
+ }
113
+
114
+ /** Creates an adapter from metadata and a setup function. */
115
+ export function define(meta: Meta, fn: SetupFn): Adapter {
116
+ const { name, ...rest } = meta
117
+ Object.defineProperty(fn, 'name', { value: name, configurable: true })
118
+ return Object.assign(fn, rest) as Adapter
119
+ }
120
+
121
+ /** Spreads decoded params. */
122
+ export type ActionRequest<item extends Schema.Item> =
123
+ Schema.Decoded<item>['params'] extends readonly [infer first] ? first : never
124
+
125
+ export declare namespace getClient {
126
+ type Options = {
127
+ /** Chain ID. Defaults to the active chain. */
128
+ chainId?: number | undefined
129
+ /** Fee payer service URL. */
130
+ feePayer?: string | undefined
131
+ }
132
+ }
133
+
134
+ export declare namespace createAccount {
135
+ type Parameters = {
136
+ /** Grant an access key during the ceremony. */
137
+ authorizeAccessKey?: authorizeAccessKey.Parameters | undefined
138
+ /** Digest to sign. */
139
+ digest?: Hex | undefined
140
+ /** Display name for the new account (e.g. credential name for WebAuthn). */
141
+ name: string
142
+ /** Opaque user identifier (e.g. for WebAuthn `user.id`). */
143
+ userId?: string | undefined
144
+ }
145
+ type ReturnType = {
146
+ accounts: readonly Store.Account[]
147
+ /** Signed key authorization, if an access key was granted. */
148
+ keyAuthorization?: authorizeAccessKey.ReturnType | undefined
149
+ /** Signature over the digest, if one was provided. */
150
+ signature?: Hex | undefined
151
+ }
152
+ }
153
+
154
+ export declare namespace loadAccounts {
155
+ type Parameters = {
156
+ /** Grant an access key during the ceremony. */
157
+ authorizeAccessKey?: authorizeAccessKey.Parameters | undefined
158
+ /** Credential ID to restrict authentication to a specific credential. */
159
+ credentialId?: string | undefined
160
+ /** Digest to sign. */
161
+ digest?: Hex | undefined
162
+ /** When `true`, prompts the user to pick from all available credentials instead of using the last-used one. */
163
+ selectAccount?: boolean | undefined
164
+ }
165
+ type ReturnType = {
166
+ /** Loaded accounts. */
167
+ accounts: readonly Store.Account[]
168
+ /** Signed key authorization, if an access key was granted. */
169
+ keyAuthorization?: authorizeAccessKey.ReturnType | undefined
170
+ /** Signature over the digest, if one was provided. */
171
+ signature?: Hex | undefined
172
+ }
173
+ }
174
+
175
+ export declare namespace signPersonalMessage {
176
+ type Parameters = {
177
+ /** Address of the account to sign with. */
178
+ address: Address
179
+ /** Hex-encoded message data. */
180
+ data: Hex
181
+ }
182
+ }
183
+
184
+ export declare namespace signTypedData {
185
+ type Parameters = {
186
+ /** Address of the account to sign with. */
187
+ address: Address
188
+ /** JSON-encoded EIP-712 typed data. */
189
+ data: string
190
+ }
191
+ }
192
+
193
+ export declare namespace authorizeAccessKey {
194
+ type Parameters = {
195
+ /** Access key address. Alternative to `publicKey` when the caller already knows the derived address. */
196
+ address?: Address | undefined
197
+ /** Unix timestamp (seconds) when the key expires. */
198
+ expiry: number
199
+ /** Key type of the external public key. Required when `publicKey` or `address` is provided. */
200
+ keyType?: 'secp256k1' | 'p256' | 'webAuthn' | undefined
201
+ /** TIP-20 spending limits for this key. */
202
+ limits?: readonly { token: Address; limit: bigint }[] | undefined
203
+ /** External public key to authorize. When provided, no key pair is generated — the caller holds the signing material. */
204
+ publicKey?: Hex | undefined
205
+ /** Pre-computed signature over the key authorization digest (skips a second signing ceremony). */
206
+ signature?: Hex | undefined
207
+ }
208
+
209
+ type ReturnType = KeyAuthorization.Rpc
210
+ }
211
+
212
+ export declare namespace revokeAccessKey {
213
+ type Parameters = {
214
+ /** Root account address. */
215
+ address: Address
216
+ /** Address of the access key to revoke. */
217
+ accessKeyAddress: Address
218
+ }
219
+ }
220
+
221
+ export declare namespace sendTransaction {
222
+ type Parameters = ActionRequest<typeof Rpc.eth_sendTransaction.schema>
223
+ type ReturnType = Rpc.eth_sendTransaction.Encoded['returns']
224
+ }
225
+
226
+ export declare namespace sendTransactionSync {
227
+ type Parameters = ActionRequest<typeof Rpc.eth_sendTransactionSync.schema>
228
+ type ReturnType = Rpc.eth_sendTransactionSync.Encoded['returns']
229
+ }
230
+
231
+ export declare namespace signTransaction {
232
+ type Parameters = ActionRequest<typeof Rpc.eth_signTransaction.schema>
233
+ type ReturnType = Hex
234
+ }
235
+
236
+ export declare namespace switchChain {
237
+ type Parameters = { chainId: number }
238
+ }
@@ -0,0 +1,239 @@
1
+ import { describe, expect, test } from 'vp/test'
2
+ import { Registration, Authentication } from 'webauthx/client'
3
+
4
+ import { hooksUrl, url as webauthnUrl } from '../../test/webauthn.constants.js'
5
+ import { webAuthn } from './adapters/webAuthn.js'
6
+ import * as Ceremony from './Ceremony.js'
7
+ import * as Provider from './Provider.js'
8
+ import * as Storage from './Storage.js'
9
+
10
+ describe('local', () => {
11
+ test('default: creates a passkey and verifies registration', async () => {
12
+ const ceremony = Ceremony.local()
13
+ const { options } = await ceremony.getRegistrationOptions({ name: 'Test' })
14
+ const credential = await Registration.create({ options })
15
+ const result = await ceremony.verifyRegistration(credential)
16
+
17
+ expect(result.publicKey).toMatch(/^0x[0-9a-f]+$/)
18
+ expect(result.credentialId).toBeTypeOf('string')
19
+ })
20
+
21
+ test('behavior: authenticates with an existing passkey', async () => {
22
+ const ceremony = Ceremony.local()
23
+ const { options: regOptions } = await ceremony.getRegistrationOptions({ name: 'Test' })
24
+ const credential = await Registration.create({ options: regOptions })
25
+ const { publicKey } = await ceremony.verifyRegistration(credential)
26
+
27
+ const { options: authOptions } = await ceremony.getAuthenticationOptions({
28
+ credentialId: credential.id,
29
+ })
30
+ const response = await Authentication.sign({ options: authOptions })
31
+ const result = await ceremony.verifyAuthentication(response)
32
+
33
+ expect(result.publicKey).toMatchInlineSnapshot(`"${publicKey}"`)
34
+ expect(result.credentialId).toBeTypeOf('string')
35
+ })
36
+
37
+ test('behavior: register → authenticate → publicKeys match', async () => {
38
+ const ceremony = Ceremony.local()
39
+ const { options: regOptions } = await ceremony.getRegistrationOptions({ name: 'Round-trip' })
40
+ const credential = await Registration.create({ options: regOptions })
41
+ const reg = await ceremony.verifyRegistration(credential)
42
+
43
+ const { options: authOptions } = await ceremony.getAuthenticationOptions({
44
+ credentialId: reg.credentialId,
45
+ })
46
+ const response = await Authentication.sign({ options: authOptions })
47
+ const auth = await ceremony.verifyAuthentication(response)
48
+
49
+ expect(auth.publicKey).toBe(reg.publicKey)
50
+ expect(auth.credentialId).toBe(reg.credentialId)
51
+ })
52
+ })
53
+
54
+ describe('server', () => {
55
+ const ceremony = Ceremony.server({ url: webauthnUrl })
56
+
57
+ test('default: register → verify returns valid publicKey hex', async () => {
58
+ const { options } = await ceremony.getRegistrationOptions({ name: 'Server Test' })
59
+ const credential = await Registration.create({ options })
60
+ const result = await ceremony.verifyRegistration(credential)
61
+
62
+ expect(result.credentialId).toBeTypeOf('string')
63
+ expect(result.publicKey).toMatch(/^0x[0-9a-f]+$/)
64
+ })
65
+
66
+ test('behavior: authentication publicKeys match registration', async () => {
67
+ const { options: regOptions } = await ceremony.getRegistrationOptions({ name: 'Server Auth' })
68
+ const credential = await Registration.create({ options: regOptions })
69
+ const reg = await ceremony.verifyRegistration(credential)
70
+
71
+ const { options: authOptions } = await ceremony.getAuthenticationOptions({
72
+ credentialId: reg.credentialId,
73
+ })
74
+ const response = await Authentication.sign({ options: authOptions })
75
+ const auth = await ceremony.verifyAuthentication(response)
76
+
77
+ expect(auth.publicKey).toBe(reg.publicKey)
78
+ expect(auth.credentialId).toBe(reg.credentialId)
79
+ })
80
+
81
+ test('behavior: challenge consumed after use (replay → 400)', async () => {
82
+ const { options } = await ceremony.getRegistrationOptions({ name: 'Replay Test' })
83
+ const credential = await Registration.create({ options })
84
+
85
+ // First verify succeeds
86
+ const result = await ceremony.verifyRegistration(credential)
87
+ expect(result.credentialId).toBeTypeOf('string')
88
+
89
+ // Replay same credential → 400 (challenge consumed)
90
+ const response = await fetch(`${webauthnUrl}/register`, {
91
+ method: 'POST',
92
+ headers: { 'Content-Type': 'application/json' },
93
+ body: JSON.stringify(credential),
94
+ })
95
+ expect(response.status).toBe(400)
96
+ const body = await response.json()
97
+ expect(body.error).toMatchInlineSnapshot(`"Missing or expired challenge"`)
98
+ })
99
+
100
+ test('behavior: multiple credentials return correct publicKeys', async () => {
101
+ const { options: regA } = await ceremony.getRegistrationOptions({ name: 'User A' })
102
+ const credA = await Registration.create({ options: regA })
103
+ const a = await ceremony.verifyRegistration(credA)
104
+
105
+ const { options: regB } = await ceremony.getRegistrationOptions({ name: 'User B' })
106
+ const credB = await Registration.create({ options: regB })
107
+ const b = await ceremony.verifyRegistration(credB)
108
+
109
+ expect(a.publicKey).not.toBe(b.publicKey)
110
+
111
+ // Login with credential A
112
+ const { options: authA } = await ceremony.getAuthenticationOptions({
113
+ credentialId: a.credentialId,
114
+ })
115
+ const resA = await Authentication.sign({ options: authA })
116
+ const authResultA = await ceremony.verifyAuthentication(resA)
117
+ expect(authResultA.publicKey).toBe(a.publicKey)
118
+
119
+ // Login with credential B
120
+ const { options: authB } = await ceremony.getAuthenticationOptions({
121
+ credentialId: b.credentialId,
122
+ })
123
+ const resB = await Authentication.sign({ options: authB })
124
+ const authResultB = await ceremony.verifyAuthentication(resB)
125
+ expect(authResultB.publicKey).toBe(b.publicKey)
126
+ })
127
+ })
128
+
129
+ describe('server (provider round-trip)', () => {
130
+ const ceremony = Ceremony.server({ url: webauthnUrl })
131
+
132
+ function createProvider() {
133
+ return Provider.create({
134
+ storage: Storage.idb({ key: crypto.randomUUID() }),
135
+ adapter: webAuthn({ ceremony }),
136
+ })
137
+ }
138
+
139
+ test('behavior: wallet_connect register → eth_accounts returns address', async () => {
140
+ const provider = createProvider()
141
+
142
+ const result = await provider.request({
143
+ method: 'wallet_connect',
144
+ params: [{ capabilities: { method: 'register' } }],
145
+ })
146
+
147
+ expect(result.accounts).toHaveLength(1)
148
+ expect(result.accounts[0]!.address).toMatch(/^0x[0-9a-fA-F]{40}$/)
149
+
150
+ const accounts = await provider.request({ method: 'eth_accounts' })
151
+ expect(accounts).toHaveLength(1)
152
+ expect(accounts[0]).toBe(result.accounts[0]!.address)
153
+ })
154
+
155
+ test('behavior: register two accounts → eth_accounts returns both', async () => {
156
+ const provider = createProvider()
157
+
158
+ // Register first
159
+ const first = await provider.request({
160
+ method: 'wallet_connect',
161
+ params: [{ capabilities: { method: 'register' } }],
162
+ })
163
+
164
+ // Register second
165
+ const second = await provider.request({
166
+ method: 'wallet_connect',
167
+ params: [{ capabilities: { method: 'register' } }],
168
+ })
169
+
170
+ expect(second.accounts).toHaveLength(2)
171
+ expect(second.accounts[0]!.address).not.toBe(first.accounts[0]!.address)
172
+
173
+ const accounts = await provider.request({ method: 'eth_accounts' })
174
+ expect(accounts).toHaveLength(2)
175
+ })
176
+ })
177
+
178
+ describe('server (hooks)', () => {
179
+ test('behavior: onRegister merges extra JSON and headers', async () => {
180
+ const regOptionsRes = await fetch(`${hooksUrl}/register/options`, {
181
+ method: 'POST',
182
+ headers: { 'Content-Type': 'application/json' },
183
+ body: JSON.stringify({ name: 'Hook Test' }),
184
+ })
185
+ const { options } = await regOptionsRes.json()
186
+ const credential = await Registration.create({ options })
187
+
188
+ const res = await fetch(`${hooksUrl}/register`, {
189
+ method: 'POST',
190
+ headers: { 'Content-Type': 'application/json' },
191
+ body: JSON.stringify(credential),
192
+ })
193
+
194
+ const body = await res.json()
195
+ expect(body.credentialId).toBeTypeOf('string')
196
+ expect(body.publicKey).toMatch(/^0x[0-9a-f]+$/)
197
+ expect(body.sessionToken).toBe(`reg_${body.credentialId}`)
198
+ expect(res.headers.get('x-custom')).toBe('register-hook')
199
+ })
200
+
201
+ test('behavior: onAuthenticate merges extra JSON and headers', async () => {
202
+ // Register first
203
+ const regOptionsRes = await fetch(`${hooksUrl}/register/options`, {
204
+ method: 'POST',
205
+ headers: { 'Content-Type': 'application/json' },
206
+ body: JSON.stringify({ name: 'Hook Auth Test' }),
207
+ })
208
+ const { options: regOptions } = await regOptionsRes.json()
209
+ const credential = await Registration.create({ options: regOptions })
210
+
211
+ const regRes = await fetch(`${hooksUrl}/register`, {
212
+ method: 'POST',
213
+ headers: { 'Content-Type': 'application/json' },
214
+ body: JSON.stringify(credential),
215
+ })
216
+ const { credentialId } = await regRes.json()
217
+
218
+ // Authenticate
219
+ const authOptionsRes = await fetch(`${hooksUrl}/login/options`, {
220
+ method: 'POST',
221
+ headers: { 'Content-Type': 'application/json' },
222
+ body: JSON.stringify({ credentialId }),
223
+ })
224
+ const { options: authOptions } = await authOptionsRes.json()
225
+ const response = await Authentication.sign({ options: authOptions })
226
+
227
+ const res = await fetch(`${hooksUrl}/login`, {
228
+ method: 'POST',
229
+ headers: { 'Content-Type': 'application/json' },
230
+ body: JSON.stringify(response),
231
+ })
232
+
233
+ const body = await res.json()
234
+ expect(body.credentialId).toBe(credentialId)
235
+ expect(body.publicKey).toMatch(/^0x[0-9a-f]+$/)
236
+ expect(body.sessionToken).toBe(`auth_${credentialId}`)
237
+ expect(res.headers.get('x-custom')).toBe('authenticate-hook')
238
+ })
239
+ })