accounts 0.16.2 → 0.17.0

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 (65) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +3 -4
  3. package/dist/cli/Provider.d.ts +1 -1
  4. package/dist/cli/Provider.d.ts.map +1 -1
  5. package/dist/cli/Provider.js +3 -3
  6. package/dist/cli/Provider.js.map +1 -1
  7. package/dist/cli/adapter.d.ts +13 -8
  8. package/dist/cli/adapter.d.ts.map +1 -1
  9. package/dist/cli/adapter.js +92 -303
  10. package/dist/cli/adapter.js.map +1 -1
  11. package/dist/core/adapters/deviceCode/deviceCode.d.ts +59 -0
  12. package/dist/core/adapters/deviceCode/deviceCode.d.ts.map +1 -0
  13. package/dist/core/adapters/deviceCode/deviceCode.js +93 -0
  14. package/dist/core/adapters/deviceCode/deviceCode.js.map +1 -0
  15. package/dist/core/adapters/deviceCode/index.d.ts +3 -0
  16. package/dist/core/adapters/deviceCode/index.d.ts.map +1 -0
  17. package/dist/core/adapters/deviceCode/index.js +3 -0
  18. package/dist/core/adapters/deviceCode/index.js.map +1 -0
  19. package/dist/core/adapters/deviceCode/tempoWallet.d.ts +9 -0
  20. package/dist/core/adapters/deviceCode/tempoWallet.d.ts.map +1 -0
  21. package/dist/core/adapters/deviceCode/tempoWallet.js +11 -0
  22. package/dist/core/adapters/deviceCode/tempoWallet.js.map +1 -0
  23. package/dist/core/adapters/internal/fromRequest.d.ts +10 -0
  24. package/dist/core/adapters/internal/fromRequest.d.ts.map +1 -1
  25. package/dist/core/adapters/internal/fromRequest.js +4 -2
  26. package/dist/core/adapters/internal/fromRequest.js.map +1 -1
  27. package/dist/server/CliAuth.d.ts +69 -1
  28. package/dist/server/CliAuth.d.ts.map +1 -1
  29. package/dist/server/CliAuth.js +6 -1
  30. package/dist/server/CliAuth.js.map +1 -1
  31. package/dist/server/Handler.d.ts +2 -1
  32. package/dist/server/Handler.d.ts.map +1 -1
  33. package/dist/server/Handler.js +2 -1
  34. package/dist/server/Handler.js.map +1 -1
  35. package/dist/server/index.d.ts +1 -0
  36. package/dist/server/index.d.ts.map +1 -1
  37. package/dist/server/index.js +1 -0
  38. package/dist/server/index.js.map +1 -1
  39. package/dist/server/internal/handlers/codeAuth.d.ts +1 -0
  40. package/dist/server/internal/handlers/codeAuth.d.ts.map +1 -1
  41. package/dist/server/internal/handlers/codeAuth.js +1 -0
  42. package/dist/server/internal/handlers/codeAuth.js.map +1 -1
  43. package/dist/server/internal/handlers/deviceCode.d.ts +60 -0
  44. package/dist/server/internal/handlers/deviceCode.d.ts.map +1 -0
  45. package/dist/server/internal/handlers/deviceCode.js +218 -0
  46. package/dist/server/internal/handlers/deviceCode.js.map +1 -0
  47. package/package.json +7 -1
  48. package/src/cli/Provider.localnet.test.ts +414 -470
  49. package/src/cli/Provider.test-d.ts +4 -3
  50. package/src/cli/Provider.ts +6 -6
  51. package/src/cli/adapter.ts +111 -380
  52. package/src/core/adapters/deviceCode/deviceCode.localnet.test.ts +226 -0
  53. package/src/core/adapters/deviceCode/deviceCode.test-d.ts +19 -0
  54. package/src/core/adapters/deviceCode/deviceCode.ts +157 -0
  55. package/src/core/adapters/deviceCode/index.ts +2 -0
  56. package/src/core/adapters/deviceCode/tempoWallet.ts +17 -0
  57. package/src/core/adapters/internal/fromRequest.ts +115 -88
  58. package/src/server/CliAuth.test.ts +29 -18
  59. package/src/server/CliAuth.ts +10 -2
  60. package/src/server/Handler.test-d.ts +19 -33
  61. package/src/server/Handler.ts +2 -1
  62. package/src/server/index.ts +1 -0
  63. package/src/server/internal/handlers/codeAuth.ts +1 -0
  64. package/src/server/internal/handlers/deviceCode.test.ts +140 -0
  65. package/src/server/internal/handlers/deviceCode.ts +323 -0
@@ -1,4 +1,5 @@
1
1
  import { describe, expectTypeOf, test } from 'vp/test'
2
+ import type { DeviceCode } from 'wata'
2
3
 
3
4
  import type * as CoreProvider from '../core/Provider.js'
4
5
  import type { cli } from './adapter.js'
@@ -8,9 +9,9 @@ describe('create', () => {
8
9
  test('accepts CLI bootstrap options', () => {
9
10
  expectTypeOf<Parameters<typeof Provider.create>[0]>().toMatchTypeOf<{
10
11
  host?: string | undefined
11
- open?: ((url: string) => Promise<void> | void) | undefined
12
- pollIntervalMs?: number | undefined
13
- timeoutMs?: number | undefined
12
+ open?: ((url: string, prompt: DeviceCode.Prompt) => Promise<void> | void) | undefined
13
+ pollingInterval?: number | undefined
14
+ timeout?: number | undefined
14
15
  }>()
15
16
  })
16
17
 
@@ -8,11 +8,11 @@ import * as Storage from './storage.js'
8
8
  */
9
9
  export function create(options: create.Options): create.ReturnType {
10
10
  const {
11
- host = 'https://wallet.tempo.xyz/api/auth/cli',
11
+ host = 'https://wallet.tempo.xyz/auth/device',
12
12
  open,
13
- pollIntervalMs,
13
+ pollingInterval,
14
14
  storage = Storage.filesystem(),
15
- timeoutMs,
15
+ timeout,
16
16
  ...rest
17
17
  } = options
18
18
 
@@ -28,8 +28,8 @@ export function create(options: create.Options): create.ReturnType {
28
28
  adapter: cli({
29
29
  host,
30
30
  ...(open ? { open } : {}),
31
- ...(typeof pollIntervalMs !== 'undefined' ? { pollIntervalMs } : {}),
32
- ...(typeof timeoutMs !== 'undefined' ? { timeoutMs } : {}),
31
+ ...(pollingInterval !== undefined ? { pollingInterval } : {}),
32
+ ...(timeout !== undefined ? { timeout } : {}),
33
33
  }),
34
34
  ...(mpp ? { mpp } : {}),
35
35
  storage,
@@ -41,7 +41,7 @@ export declare namespace create {
41
41
  CoreProvider.create.Options & cli.Options,
42
42
  'adapter' | 'authorizeAccessKey' | 'host'
43
43
  > & {
44
- /** Host URL for the device-code flow. @default "https://wallet.tempo.xyz/api/auth/cli" */
44
+ /** Base URL of the wallet's device-code endpoints. @default "https://wallet.tempo.xyz/auth/device" */
45
45
  host?: string | undefined
46
46
  }
47
47
  export type ReturnType = CoreProvider.create.ReturnType
@@ -1,377 +1,149 @@
1
1
  import { spawn } from 'node:child_process'
2
- import { setTimeout as sleep } from 'node:timers/promises'
3
- import { Base64, Hash, Hex, Provider as core_Provider, RpcResponse } from 'ox'
2
+ import { Provider as core_Provider } from 'ox'
4
3
  import { KeyAuthorization } from 'ox/tempo'
4
+ import { hashMessage, hashTypedData } from 'viem'
5
+ import type { DeviceCode } from 'wata'
5
6
  import * as z from 'zod/mini'
6
7
 
7
- import * as Adapter from '../core/Adapter.js'
8
+ import type * as Adapter from '../core/Adapter.js'
9
+ import { deviceCode } from '../core/adapters/deviceCode/deviceCode.js'
8
10
  import * as Keystore from '../core/Keystore.js'
9
- import * as CliAuth from '../server/CliAuth.js'
11
+ import * as Rpc from '../core/zod/rpc.js'
10
12
 
11
13
  /**
12
- * Creates a CLI bootstrap adapter backed by the device code protocol.
14
+ * Creates a CLI bootstrap adapter backed by the Wata device-code transport
15
+ * (OAuth 2.0 Device Authorization Grant, RFC 8628 with PKCE).
16
+ *
17
+ * Authorization opens the wallet's verification page in a browser; the wallet
18
+ * responds with a signed key authorization once the user approves.
13
19
  */
14
20
  export function cli(options: cli.Options): Adapter.Adapter {
15
- const { name = 'Tempo CLI', rdns = 'xyz.tempo.cli' } = options
16
-
17
- return Adapter.define({ name, rdns }, ({ getClient, store }) => {
18
- // The CLI persists to string-based filesystem storage, so its p256
19
- // default opts into an extractable WebCrypto key (a non-extractable one
20
- // could not survive a restart). secp256k1 stays available for explicit
21
- // requests. Shared between the device code ceremony and the instance
22
- // declaration so records hydrate through the same keystores.
23
- const keystores = {
24
- p256: Keystore.webCryptoP256({ extractable: true }),
25
- secp256k1: Keystore.secp256k1(),
26
- }
27
-
28
- async function saveGeneratedAccessKey(
29
- address: Adapter.authorizeAccessKey.ReturnType['rootAddress'],
30
- accessKey: Awaited<ReturnType<Keystore.Keystore['createKey']>>,
31
- keyAuthorization: z.output<typeof CliAuth.keyAuthorization>,
32
- ) {
33
- const signed = KeyAuthorization.fromRpc(z.encode(CliAuth.keyAuthorization, keyAuthorization))
34
- store.accessKeys.add({
35
- account: address,
36
- authorization: signed,
37
- handle: accessKey.handle,
38
- publicKey: accessKey.publicKey,
39
- })
40
- }
41
-
42
- async function authorize(request: {
43
- account?: Adapter.authorizeAccessKey.ReturnType['rootAddress'] | undefined
44
- authorizeAccessKey: Adapter.authorizeAccessKey.Parameters | undefined
45
- method: 'wallet_authorizeAccessKey' | 'wallet_connect'
46
- showDeposit?: Adapter.createAccount.Parameters['showDeposit'] | undefined
47
- }) {
48
- const {
49
- host,
50
- open = defaultOpen,
51
- pollIntervalMs = 2_000,
52
- timeoutMs = 5 * 60 * 1_000,
53
- } = options
54
- const { account, authorizeAccessKey, method } = request
55
-
56
- // p256 by default; secp256k1 only when explicitly requested.
57
- const generatedKeyType = authorizeAccessKey?.keyType === 'secp256k1' ? 'secp256k1' : 'p256'
58
- const generatedAccessKey =
59
- authorizeAccessKey && !authorizeAccessKey.publicKey && !authorizeAccessKey.address
60
- ? await keystores[generatedKeyType].createKey()
61
- : undefined
62
-
63
- const publicKey = authorizeAccessKey?.publicKey ?? generatedAccessKey?.publicKey
64
- const keyType =
65
- authorizeAccessKey?.keyType ?? (generatedAccessKey ? generatedKeyType : undefined)
66
-
67
- if (!publicKey)
68
- throw new RpcResponse.InvalidParamsError({
69
- message:
70
- method === 'wallet_connect'
71
- ? '`wallet_connect` on the CLI adapter requires `capabilities.authorizeAccessKey`.'
72
- : '`wallet_authorizeAccessKey` on the CLI adapter requires key parameters.',
73
- })
74
-
75
- const codeVerifier = createCodeVerifier()
76
- const codeChallenge = createCodeChallenge(codeVerifier)
77
- const body: z.output<typeof CliAuth.createRequest> = {
78
- ...(account ? { account } : {}),
79
- chainId: BigInt(store.getState().chainId),
80
- codeChallenge,
81
- ...(typeof authorizeAccessKey?.expiry !== 'undefined'
82
- ? { expiry: authorizeAccessKey.expiry }
83
- : {}),
84
- ...(keyType ? { keyType } : {}),
85
- ...(authorizeAccessKey?.limits ? { limits: authorizeAccessKey.limits } : {}),
86
- pubKey: publicKey,
87
- ...(request.showDeposit !== undefined ? { showDeposit: request.showDeposit } : {}),
88
- }
89
- const created = await post({
90
- body,
91
- request: CliAuth.createRequest,
92
- response: CliAuth.createResponse,
93
- url: getApiUrl(host, 'code'),
94
- })
95
- const url = getBrowserUrl(host, created.code)
96
-
97
- try {
98
- await open(url)
99
- } catch (error) {
100
- throw new OpenError(url, created.code, error)
101
- }
102
-
103
- const startedAt = Date.now()
104
-
105
- while (Date.now() - startedAt < timeoutMs) {
106
- const result = await post({
107
- body: {
108
- codeVerifier,
109
- } satisfies z.output<typeof CliAuth.pollRequest>,
110
- request: CliAuth.pollRequest,
111
- response: CliAuth.pollResponse,
112
- url: getApiUrl(host, `poll/${created.code}`),
113
- })
114
-
115
- if (result.status === 'pending') {
116
- await sleep(pollIntervalMs)
117
- continue
118
- }
119
- if (result.status === 'expired')
120
- throw new Error('Device code expired before authorization completed.')
121
- if (result.action === 'updateAccessKey')
122
- throw new Error('Device code action does not match the authorization request.')
123
-
124
- if (generatedAccessKey)
125
- await saveGeneratedAccessKey(
126
- result.accountAddress,
127
- generatedAccessKey,
128
- result.keyAuthorization,
129
- )
130
-
131
- return result
132
- }
133
-
134
- throw new TimeoutError(url, created.code)
135
- }
136
-
137
- async function update(parameters: Adapter.updateAccessKey.Parameters) {
138
- const {
139
- host,
140
- open = defaultOpen,
141
- pollIntervalMs = 2_000,
142
- timeoutMs = 5 * 60 * 1_000,
143
- } = options
144
- const chainId = Number(parameters.chainId ?? store.getState().chainId)
145
- const current = store.accessKeys.list({
146
- accessKey: parameters.accessKeyAddress,
147
- account: parameters.address,
148
- chainId,
149
- })[0]
150
- const status = current?.keyAuthorization
151
- ? await store.accessKeys.getStatus({
152
- accessKey: parameters.accessKeyAddress,
153
- account: parameters.address,
154
- chainId,
155
- client: getClient({ chainId }),
156
- })
157
- : undefined
158
- const pendingKeyAuthorization =
159
- status === 'pending' && current?.keyAuthorization
160
- ? KeyAuthorization.toRpc(current.keyAuthorization)
161
- : undefined
162
- const codeVerifier = createCodeVerifier()
163
- const body = {
164
- action: 'updateAccessKey',
165
- accessKeyAddress: parameters.accessKeyAddress,
166
- account: parameters.address,
167
- chainId: BigInt(chainId),
168
- codeChallenge: createCodeChallenge(codeVerifier),
169
- ...(pendingKeyAuthorization
170
- ? {
171
- keyAuthorization: z.decode(CliAuth.keyAuthorization, {
172
- ...pendingKeyAuthorization,
173
- address: pendingKeyAuthorization.keyId,
174
- }),
175
- }
176
- : {}),
177
- limits: parameters.limits,
178
- } satisfies z.output<typeof CliAuth.createRequest>
179
- const created = await post({
180
- body,
181
- request: CliAuth.createRequest,
182
- response: CliAuth.createResponse,
183
- url: getApiUrl(host, 'code'),
184
- })
185
- const url = getBrowserUrl(host, created.code)
186
-
187
- try {
188
- await open(url)
189
- } catch (error) {
190
- throw new OpenError(url, created.code, error)
191
- }
192
-
193
- const startedAt = Date.now()
194
- while (Date.now() - startedAt < timeoutMs) {
195
- const result = await post({
196
- body: { codeVerifier },
197
- request: CliAuth.pollRequest,
198
- response: CliAuth.pollResponse,
199
- url: getApiUrl(host, `poll/${created.code}`),
21
+ const {
22
+ host,
23
+ name = 'Tempo CLI',
24
+ open = defaultOpen,
25
+ pollingInterval,
26
+ rdns = 'xyz.tempo.cli',
27
+ timeout,
28
+ } = options
29
+
30
+ return deviceCode({
31
+ actions: ({ getClient, request, store }) => {
32
+ async function getAccessKey(address: `0x${string}`) {
33
+ const account = await store.accessKeys.select({
34
+ account: address,
35
+ chainId: store.getState().chainId,
200
36
  })
201
- if (result.status === 'pending') {
202
- await sleep(pollIntervalMs)
203
- continue
204
- }
205
- if (result.status === 'expired')
206
- throw new Error('Device code expired before access key update completed.')
207
- if (result.action !== 'updateAccessKey')
208
- throw new Error('Device code action does not match the access key update request.')
209
- if (result.keyAuthorization) {
210
- store.accessKeys.updateAuthorization({
211
- accessKey: parameters.accessKeyAddress,
212
- account: parameters.address,
213
- authorization: KeyAuthorization.fromRpc(
214
- z.encode(CliAuth.keyAuthorization, result.keyAuthorization),
215
- ),
216
- chainId,
37
+ if (!account)
38
+ throw new core_Provider.UnauthorizedError({
39
+ message: `Account "${address}" cannot sign with an access key.`,
217
40
  })
218
- }
219
- return
41
+ return account
220
42
  }
221
43
 
222
- throw new TimeoutError(url, created.code)
223
- }
224
-
225
- return {
226
- actions: {
227
- async authorizeAccessKey(parameters) {
228
- const { accounts, activeAccount } = store.getState()
229
- const account = accounts[activeAccount]?.address
230
- const result = await authorize({
231
- ...(account ? { account } : {}),
232
- authorizeAccessKey: parameters,
233
- method: 'wallet_authorizeAccessKey',
234
- ...(parameters.showDeposit !== undefined
235
- ? { showDeposit: parameters.showDeposit }
236
- : {}),
237
- })
238
-
239
- if (!account)
240
- store.setState({
241
- accounts: [{ address: result.accountAddress }],
242
- activeAccount: 0,
243
- })
244
-
245
- return {
246
- keyAuthorization: z.encode(CliAuth.keyAuthorization, result.keyAuthorization),
247
- rootAddress: result.accountAddress,
248
- }
44
+ return {
45
+ async signPersonalMessage(parameters) {
46
+ const account = await getAccessKey(parameters.address)
47
+ return await account.sign({ hash: hashMessage({ raw: parameters.data }) })
249
48
  },
250
- async createAccount(parameters) {
251
- if (parameters?.digest)
252
- throw unsupported('`wallet_connect` digest signing not supported by CLI adapter.')
253
-
254
- const result = await authorize({
255
- authorizeAccessKey: parameters?.authorizeAccessKey,
256
- method: 'wallet_connect',
257
- ...(parameters?.showDeposit !== undefined
258
- ? { showDeposit: parameters.showDeposit }
259
- : {}),
49
+ async signTypedData(parameters) {
50
+ const account = await getAccessKey(parameters.address)
51
+ return await account.sign({
52
+ hash: hashTypedData(JSON.parse(parameters.data) as never),
260
53
  })
261
-
262
- return {
263
- accounts: [
264
- {
265
- address: result.accountAddress,
266
- capabilities: {},
267
- },
268
- ],
269
- keyAuthorization: z.encode(CliAuth.keyAuthorization, result.keyAuthorization),
270
- }
271
54
  },
272
- async loadAccounts(parameters) {
273
- if (parameters?.digest)
274
- throw unsupported('`wallet_connect` digest signing not supported by CLI adapter.')
275
-
276
- const result = await authorize({
277
- authorizeAccessKey: parameters?.authorizeAccessKey,
278
- method: 'wallet_connect',
279
- ...(parameters?.showDeposit !== undefined
280
- ? { showDeposit: parameters.showDeposit }
281
- : {}),
55
+ // Limit updates run their own ceremony: the wallet returns a
56
+ // replacement signed authorization for a pending (unpublished) key,
57
+ // or applies the update on-chain for a published one.
58
+ async updateAccessKey(parameters) {
59
+ const chainId = Number(parameters.chainId ?? store.getState().chainId)
60
+ const current = store.accessKeys.list({
61
+ accessKey: parameters.accessKeyAddress,
62
+ account: parameters.address,
63
+ chainId,
64
+ })[0]
65
+ const status = current?.keyAuthorization
66
+ ? await store.accessKeys.getStatus({
67
+ accessKey: parameters.accessKeyAddress,
68
+ account: parameters.address,
69
+ chainId,
70
+ client: getClient({ chainId }),
71
+ })
72
+ : undefined
73
+ const pendingKeyAuthorization =
74
+ status === 'pending' && current?.keyAuthorization
75
+ ? KeyAuthorization.toRpc(current.keyAuthorization)
76
+ : undefined
77
+
78
+ const encoded = z.encode(Rpc.wallet_updateAccessKey.parameters, {
79
+ ...parameters,
80
+ chainId: BigInt(chainId),
282
81
  })
283
-
284
- return {
285
- accounts: [
82
+ const result = (await request({
83
+ context: { account: parameters.address, chainId },
84
+ method: 'wallet_updateAccessKey',
85
+ params: [
286
86
  {
287
- address: result.accountAddress,
288
- capabilities: {},
87
+ ...encoded,
88
+ ...(pendingKeyAuthorization ? { keyAuthorization: pendingKeyAuthorization } : {}),
289
89
  },
290
90
  ],
291
- keyAuthorization: z.encode(CliAuth.keyAuthorization, result.keyAuthorization),
292
- }
293
- },
294
- async revokeAccessKey() {
295
- throw unsupported('`wallet_revokeAccessKey` not supported by CLI adapter.')
296
- },
297
- async updateAccessKey(parameters) {
298
- await update(parameters)
91
+ })) as { keyAuthorization?: KeyAuthorization.Rpc | undefined } | null | undefined
92
+
93
+ if (result?.keyAuthorization)
94
+ store.accessKeys.updateAuthorization({
95
+ accessKey: parameters.accessKeyAddress,
96
+ account: parameters.address,
97
+ authorization: KeyAuthorization.fromRpc(result.keyAuthorization),
98
+ chainId,
99
+ })
299
100
  },
300
- },
301
- async getAccount(options = {}) {
302
- const { accounts, activeAccount, chainId } = store.getState()
303
- const address = options.address ?? accounts[activeAccount]?.address
304
- if (!address) throw new core_Provider.DisconnectedError({ message: 'No active account.' })
305
- const account = await store.accessKeys.select({ account: address, chainId })
306
- if (!account)
307
- throw new core_Provider.UnauthorizedError({
308
- message: `Account "${address}" cannot sign with an access key.`,
309
- })
310
- return { account }
311
- },
312
- accessKey: { keystores },
313
- }
101
+ }
102
+ },
103
+ // The CLI persists to string-based filesystem storage, so its p256
104
+ // default opts into an extractable WebCrypto key (a non-extractable one
105
+ // could not survive a restart). secp256k1 stays available for explicit
106
+ // requests.
107
+ keystores: {
108
+ p256: Keystore.webCryptoP256({ extractable: true }),
109
+ secp256k1: Keystore.secp256k1(),
110
+ },
111
+ meta: { name },
112
+ // Only the ceremonies (bootstrap and limit updates) open a browser;
113
+ // everything else signs locally with the stored access key or fails fast.
114
+ methods: ['wallet_connect', 'wallet_authorizeAccessKey', 'wallet_updateAccessKey'],
115
+ name,
116
+ async onPrompt(prompt) {
117
+ if (!prompt.verificationUriFull) {
118
+ const code = prompt.userCode.replace(/^(.{4})(.{4})$/, '$1-$2')
119
+ process.stdout.write(`Enter code ${code} at ${prompt.verificationUri}\n`)
120
+ }
121
+ await open(prompt.verificationUriFull ?? prompt.verificationUri, prompt)
122
+ },
123
+ rdns,
124
+ ...(pollingInterval !== undefined ? { pollingInterval } : {}),
125
+ ...(timeout !== undefined ? { timeout } : {}),
126
+ url: host,
314
127
  })
315
128
  }
316
129
 
317
130
  export declare namespace cli {
318
131
  export type Options = {
319
- /** Host URL for the device code flow. API calls are made under the same base path. */
132
+ /** Base URL of the wallet's device-code endpoints (derives `/register` and `/token`). */
320
133
  host: string
321
134
  /** Provider display name. @default "Tempo CLI" */
322
135
  name?: string | undefined
323
136
  /** Browser opener override. */
324
- open?: ((url: string) => Promise<void> | void) | undefined
325
- /** Poll interval in milliseconds. @default 2000 */
326
- pollIntervalMs?: number | undefined
137
+ open?: ((url: string, prompt: DeviceCode.Prompt) => Promise<void> | void) | undefined
138
+ /** Poll cadence override in milliseconds. Defaults to the host-advertised interval. */
139
+ pollingInterval?: number | undefined
327
140
  /** Reverse-DNS provider identifier. @default "xyz.tempo.cli" */
328
141
  rdns?: string | undefined
329
- /** Poll timeout in milliseconds. @default 300000 */
330
- timeoutMs?: number | undefined
331
- }
332
- }
333
-
334
- class OpenError extends Error {
335
- code: string
336
- override cause?: unknown | undefined
337
- url: string
338
-
339
- constructor(url: string, code: string, cause?: unknown) {
340
- super(`Failed to open browser for device code ${formatCode(code)}. Open ${url} manually.`)
341
- this.name = 'OpenError'
342
- this.code = code
343
- this.cause = cause
344
- this.url = url
345
- }
346
- }
347
-
348
- class TimeoutError extends Error {
349
- code: string
350
- url: string
351
-
352
- constructor(url: string, code: string) {
353
- super(`Timed out waiting for device code ${formatCode(code)}. Continue at ${url}.`)
354
- this.name = 'TimeoutError'
355
- this.code = code
356
- this.url = url
142
+ /** Overall wait for user approval in milliseconds. @default 300000 */
143
+ timeout?: number | undefined
357
144
  }
358
145
  }
359
146
 
360
- function createCodeChallenge(codeVerifier: string) {
361
- return Base64.fromBytes(Hash.sha256(Hex.fromString(codeVerifier), { as: 'Bytes' }), {
362
- pad: false,
363
- url: true,
364
- })
365
- }
366
-
367
- function createCodeVerifier() {
368
- return Base64.fromBytes(Hex.toBytes(Hex.random(32)), { pad: false, url: true })
369
- }
370
-
371
- function formatCode(code: string) {
372
- return code.length === 8 ? `${code.slice(0, 4)}-${code.slice(4)}` : code
373
- }
374
-
375
147
  function defaultOpen(url: string) {
376
148
  const command =
377
149
  process.platform === 'darwin'
@@ -386,44 +158,3 @@ function defaultOpen(url: string) {
386
158
  })
387
159
  child.unref()
388
160
  }
389
-
390
- function getApiUrl(serviceUrl: string, path: string) {
391
- const url = new URL(serviceUrl)
392
- url.pathname = `${url.pathname.replace(/\/$/, '')}/${path.replace(/^\//, '')}`
393
- url.search = ''
394
- return url.toString()
395
- }
396
-
397
- function getBrowserUrl(serviceUrl: string, code: string) {
398
- const url = new URL(serviceUrl)
399
- url.searchParams.set('code', code)
400
- return url.toString()
401
- }
402
-
403
- async function post<
404
- const request extends z.ZodMiniType,
405
- const response extends z.ZodMiniType,
406
- >(options: {
407
- body: z.output<request>
408
- request: request
409
- response: response
410
- url: string
411
- }): Promise<z.output<response>> {
412
- const result = await fetch(options.url, {
413
- body: JSON.stringify(z.encode(options.request, options.body)),
414
- headers: { 'content-type': 'application/json' },
415
- method: 'POST',
416
- })
417
- const json = (await result.json().catch(() => ({}))) as z.input<response>
418
-
419
- if (!result.ok) {
420
- const error = (json as { error?: unknown }).error
421
- throw new Error(typeof error === 'string' ? error : `Request failed: ${result.status}`)
422
- }
423
-
424
- return z.decode(options.response, json)
425
- }
426
-
427
- function unsupported(message: string) {
428
- return new core_Provider.UnsupportedMethodError({ message })
429
- }