accounts 0.16.0 → 0.16.2
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.
- package/CHANGELOG.md +21 -0
- package/dist/core/Adapter.d.ts +2 -2
- package/dist/core/Adapter.d.ts.map +1 -1
- package/dist/core/Provider.d.ts.map +1 -1
- package/dist/core/Provider.js +9 -2
- package/dist/core/Provider.js.map +1 -1
- package/dist/core/Schema.d.ts +2 -2
- package/dist/core/WebAuthnCeremony.d.ts +2 -4
- package/dist/core/WebAuthnCeremony.d.ts.map +1 -1
- package/dist/core/WebAuthnCeremony.js +8 -4
- package/dist/core/WebAuthnCeremony.js.map +1 -1
- package/dist/core/adapters/webAuthn.js +1 -1
- package/dist/core/adapters/webAuthn.js.map +1 -1
- package/dist/core/zod/rpc.d.ts +4 -4
- package/dist/core/zod/rpc.js +2 -2
- package/dist/core/zod/rpc.js.map +1 -1
- package/dist/server/internal/handlers/utils.d.ts.map +1 -1
- package/dist/server/internal/handlers/utils.js +10 -0
- package/dist/server/internal/handlers/utils.js.map +1 -1
- package/dist/server/internal/handlers/webAuthn.d.ts +7 -1
- package/dist/server/internal/handlers/webAuthn.d.ts.map +1 -1
- package/dist/server/internal/handlers/webAuthn.js +21 -12
- package/dist/server/internal/handlers/webAuthn.js.map +1 -1
- package/package.json +3 -3
- package/src/core/Adapter.ts +2 -2
- package/src/core/Provider.browser.test.ts +30 -0
- package/src/core/Provider.localnet.test.ts +14 -0
- package/src/core/Provider.test.ts +32 -0
- package/src/core/Provider.ts +10 -2
- package/src/core/Schema.test-d.ts +1 -1
- package/src/core/WebAuthnCeremony.test.ts +21 -0
- package/src/core/WebAuthnCeremony.ts +11 -8
- package/src/core/adapters/webAuthn.ts +1 -1
- package/src/core/mppx.localnet.test.ts +53 -2
- package/src/core/zod/rpc.test.ts +19 -0
- package/src/core/zod/rpc.ts +2 -2
- package/src/server/internal/handlers/utils.test.ts +53 -0
- package/src/server/internal/handlers/utils.ts +16 -0
- package/src/server/internal/handlers/webAuthn.test.ts +76 -0
- package/src/server/internal/handlers/webAuthn.ts +28 -14
|
@@ -54,12 +54,10 @@ export declare namespace verifyRegistration {
|
|
|
54
54
|
|
|
55
55
|
export declare namespace getAuthenticationOptions {
|
|
56
56
|
type Parameters = {
|
|
57
|
-
/** Credential IDs to allow (restricts which credentials can be used). */
|
|
58
|
-
allowCredentialIds?: readonly string[] | undefined
|
|
59
57
|
/** Challenge to use. */
|
|
60
58
|
challenge?: `0x${string}` | undefined
|
|
61
|
-
/** Credential ID to restrict authentication to
|
|
62
|
-
credentialId?: string | undefined
|
|
59
|
+
/** Credential ID or IDs to restrict authentication to. */
|
|
60
|
+
credentialId?: string | readonly string[] | undefined
|
|
63
61
|
/** Mediation hint for passkey autofill / conditional UI. */
|
|
64
62
|
mediation?: 'conditional' | 'optional' | 'required' | 'silent' | undefined
|
|
65
63
|
}
|
|
@@ -124,10 +122,15 @@ export function local(options: local.Options = {}): WebAuthnCeremony {
|
|
|
124
122
|
},
|
|
125
123
|
|
|
126
124
|
async getAuthenticationOptions(parameters = {}) {
|
|
127
|
-
const {
|
|
125
|
+
const { challenge, credentialId } = parameters
|
|
128
126
|
const { options } = Authentication.getOptions({
|
|
129
127
|
challenge,
|
|
130
|
-
credentialId:
|
|
128
|
+
credentialId:
|
|
129
|
+
typeof credentialId === 'string'
|
|
130
|
+
? credentialId
|
|
131
|
+
: credentialId
|
|
132
|
+
? [...credentialId]
|
|
133
|
+
: undefined,
|
|
131
134
|
rpId,
|
|
132
135
|
})
|
|
133
136
|
return { options }
|
|
@@ -189,8 +192,8 @@ export function server(options: server.Options): WebAuthnCeremony {
|
|
|
189
192
|
},
|
|
190
193
|
|
|
191
194
|
async getAuthenticationOptions(parameters = {}) {
|
|
192
|
-
const {
|
|
193
|
-
return request('/login/options', {
|
|
195
|
+
const { challenge, credentialId, mediation } = parameters
|
|
196
|
+
return request('/login/options', { challenge, credentialId, mediation })
|
|
194
197
|
},
|
|
195
198
|
|
|
196
199
|
async verifyAuthentication(response) {
|
|
@@ -77,7 +77,7 @@ export function webAuthn(options: webAuthn.Options = {}): Adapter.Adapter {
|
|
|
77
77
|
}
|
|
78
78
|
},
|
|
79
79
|
async loadAccounts(parameters = {}) {
|
|
80
|
-
const {
|
|
80
|
+
const { digest, selectAccount } = parameters
|
|
81
81
|
|
|
82
82
|
const credentialId = selectAccount
|
|
83
83
|
? undefined
|
|
@@ -25,6 +25,19 @@ const payment = ServerMppx.create({
|
|
|
25
25
|
secretKey: 'test-secret-key-test-secret-key-32',
|
|
26
26
|
})
|
|
27
27
|
|
|
28
|
+
const sponsoredPayment = ServerMppx.create({
|
|
29
|
+
methods: [
|
|
30
|
+
tempo({
|
|
31
|
+
account: accounts[1]!,
|
|
32
|
+
currency: Addresses.pathUsd,
|
|
33
|
+
feePayer: accounts[0]!,
|
|
34
|
+
getClient: () => client,
|
|
35
|
+
}),
|
|
36
|
+
],
|
|
37
|
+
realm: 'mppx-sponsored-test',
|
|
38
|
+
secretKey: 'sponsored-test-secret-key-32-bytes',
|
|
39
|
+
})
|
|
40
|
+
|
|
28
41
|
let server: Server
|
|
29
42
|
|
|
30
43
|
beforeAll(async () => {
|
|
@@ -90,6 +103,44 @@ describe('mppx integration', () => {
|
|
|
90
103
|
expect(res.status).toMatchInlineSnapshot(`200`)
|
|
91
104
|
})
|
|
92
105
|
|
|
106
|
+
test('pull mode preserves server-sponsored transactions', async () => {
|
|
107
|
+
const sponsoredServer = await createServer(async (req, res) => {
|
|
108
|
+
const result = await ServerMppx.toNodeListener(sponsoredPayment.charge({ amount: '1' }))(
|
|
109
|
+
req,
|
|
110
|
+
res,
|
|
111
|
+
)
|
|
112
|
+
if (result.status === 402) return
|
|
113
|
+
res.writeHead(200)
|
|
114
|
+
res.end()
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const provider = Provider.create({
|
|
119
|
+
adapter: headlessWebAuthn(),
|
|
120
|
+
chains: [chain],
|
|
121
|
+
mpp: false,
|
|
122
|
+
})
|
|
123
|
+
const address = await connect(provider)
|
|
124
|
+
await fund(address, '1')
|
|
125
|
+
|
|
126
|
+
const mppx = ClientMppx.create({
|
|
127
|
+
methods: [
|
|
128
|
+
clientTempo({
|
|
129
|
+
account: provider.getAccount(),
|
|
130
|
+
...provider.getMppxParameters(),
|
|
131
|
+
mode: 'pull',
|
|
132
|
+
}),
|
|
133
|
+
],
|
|
134
|
+
polyfill: false,
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const res = await mppx.fetch(`${sponsoredServer.url}/fortune`)
|
|
138
|
+
expect(res.status).toMatchInlineSnapshot(`200`)
|
|
139
|
+
} finally {
|
|
140
|
+
await sponsoredServer.closeAsync()
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
|
|
93
144
|
test('mppx global front door uses provider access keys', async () => {
|
|
94
145
|
const provider = Provider.create({
|
|
95
146
|
adapter: headlessWebAuthn(),
|
|
@@ -267,12 +318,12 @@ async function connect(provider: ReturnType<typeof Provider.create>) {
|
|
|
267
318
|
return register.accounts[0]!.address
|
|
268
319
|
}
|
|
269
320
|
|
|
270
|
-
async function fund(address: `0x${string}
|
|
321
|
+
async function fund(address: `0x${string}`, amount = '10') {
|
|
271
322
|
await Actions.token.transferSync(client, {
|
|
272
323
|
account: accounts[0]!,
|
|
273
324
|
feeToken: Addresses.pathUsd,
|
|
274
325
|
to: address,
|
|
275
326
|
token: Addresses.pathUsd,
|
|
276
|
-
amount: parseUnits(
|
|
327
|
+
amount: parseUnits(amount, 6),
|
|
277
328
|
})
|
|
278
329
|
}
|
package/src/core/zod/rpc.test.ts
CHANGED
|
@@ -132,6 +132,25 @@ describe('transactionRequest.keyAuthorization', () => {
|
|
|
132
132
|
})
|
|
133
133
|
})
|
|
134
134
|
|
|
135
|
+
describe('wallet_connect.capabilities.request: credentials', () => {
|
|
136
|
+
test('accepts multiple credential IDs for login', () => {
|
|
137
|
+
expect(
|
|
138
|
+
z.parse(Rpc.wallet_connect.capabilities.request, {
|
|
139
|
+
credentialId: ['cred-1', 'cred-2'],
|
|
140
|
+
method: 'login',
|
|
141
|
+
}),
|
|
142
|
+
).toMatchInlineSnapshot(`
|
|
143
|
+
{
|
|
144
|
+
"credentialId": [
|
|
145
|
+
"cred-1",
|
|
146
|
+
"cred-2",
|
|
147
|
+
],
|
|
148
|
+
"method": "login",
|
|
149
|
+
}
|
|
150
|
+
`)
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
135
154
|
describe('wallet_connect.capabilities.request: auth', () => {
|
|
136
155
|
test('accepts string shorthand', () => {
|
|
137
156
|
expect(
|
package/src/core/zod/rpc.ts
CHANGED
|
@@ -710,7 +710,7 @@ export namespace wallet_connect {
|
|
|
710
710
|
}),
|
|
711
711
|
z.object({
|
|
712
712
|
digest: z.optional(u.hex()),
|
|
713
|
-
credentialId: z.optional(z.string()),
|
|
713
|
+
credentialId: z.optional(z.union([z.string(), z.array(z.string())])),
|
|
714
714
|
authorizeAccessKey,
|
|
715
715
|
auth,
|
|
716
716
|
identity,
|
|
@@ -829,7 +829,7 @@ export namespace wallet_connect_strict {
|
|
|
829
829
|
}),
|
|
830
830
|
z.object({
|
|
831
831
|
digest: z.optional(u.hex()),
|
|
832
|
-
credentialId: z.optional(z.string()),
|
|
832
|
+
credentialId: z.optional(z.union([z.string(), z.array(z.string())])),
|
|
833
833
|
authorizeAccessKey,
|
|
834
834
|
auth,
|
|
835
835
|
identity,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { RpcRequest } from 'ox'
|
|
2
|
+
import { describe, expect, test } from 'vp/test'
|
|
3
|
+
|
|
4
|
+
import * as Utils from './utils.js'
|
|
5
|
+
|
|
6
|
+
const request = RpcRequest.from({ id: 1, method: 'eth_sendRawTransaction', params: ['0x00'] })
|
|
7
|
+
|
|
8
|
+
describe('rpcErrorJson', () => {
|
|
9
|
+
test('maps upstream expired transactions to a transaction-rejected error', () => {
|
|
10
|
+
const error = {
|
|
11
|
+
cause: {
|
|
12
|
+
code: -32603,
|
|
13
|
+
message: 'Revm error: transaction expired',
|
|
14
|
+
},
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
expect(Utils.rpcErrorJson(request, error)).toMatchInlineSnapshot(`
|
|
18
|
+
{
|
|
19
|
+
"error": {
|
|
20
|
+
"code": -32003,
|
|
21
|
+
"data": {
|
|
22
|
+
"code": "transaction_expired",
|
|
23
|
+
},
|
|
24
|
+
"message": "Transaction expired.",
|
|
25
|
+
},
|
|
26
|
+
"id": 1,
|
|
27
|
+
"jsonrpc": "2.0",
|
|
28
|
+
}
|
|
29
|
+
`)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('preserves unrelated upstream internal errors', () => {
|
|
33
|
+
const error = {
|
|
34
|
+
code: -32603,
|
|
35
|
+
data: { reason: 'database unavailable' },
|
|
36
|
+
message: 'Revm error: database unavailable',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
expect(Utils.rpcErrorJson(request, error)).toMatchInlineSnapshot(`
|
|
40
|
+
{
|
|
41
|
+
"error": {
|
|
42
|
+
"code": -32603,
|
|
43
|
+
"data": {
|
|
44
|
+
"reason": "database unavailable",
|
|
45
|
+
},
|
|
46
|
+
"message": "Revm error: database unavailable",
|
|
47
|
+
},
|
|
48
|
+
"id": 1,
|
|
49
|
+
"jsonrpc": "2.0",
|
|
50
|
+
}
|
|
51
|
+
`)
|
|
52
|
+
})
|
|
53
|
+
})
|
|
@@ -97,6 +97,22 @@ export function rpcErrorJson(request: RpcRequest.RpcRequest, error: unknown) {
|
|
|
97
97
|
)
|
|
98
98
|
|
|
99
99
|
const inner = resolveError(error)
|
|
100
|
+
if (
|
|
101
|
+
inner.code === RpcResponse.InternalError.code &&
|
|
102
|
+
inner.message !== undefined &&
|
|
103
|
+
/^Revm error: transaction expired\.?$/.test(inner.message)
|
|
104
|
+
)
|
|
105
|
+
return RpcResponse.from(
|
|
106
|
+
{
|
|
107
|
+
error: {
|
|
108
|
+
code: -32003,
|
|
109
|
+
data: { code: 'transaction_expired' },
|
|
110
|
+
message: 'Transaction expired.',
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{ request },
|
|
114
|
+
)
|
|
115
|
+
|
|
100
116
|
const message = inner.message ?? (error as Error).message
|
|
101
117
|
const code = inner.code ?? -32603
|
|
102
118
|
const data = inner.data
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import { vi } from 'vitest'
|
|
1
2
|
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vp/test'
|
|
3
|
+
import { Registration } from 'webauthx/server'
|
|
2
4
|
|
|
3
5
|
import { createServer, type Server } from '../../../../test/utils.js'
|
|
4
6
|
import * as WebAuthnCeremony from '../../../core/WebAuthnCeremony.js'
|
|
5
7
|
import * as Kv from '../../Kv.js'
|
|
6
8
|
import { type SessionPayload, webAuthn } from './webAuthn.js'
|
|
7
9
|
|
|
10
|
+
vi.mock('webauthx/server', { spy: true })
|
|
11
|
+
|
|
8
12
|
let server: Server
|
|
9
13
|
let ceremony: WebAuthnCeremony.WebAuthnCeremony
|
|
10
14
|
|
|
@@ -52,6 +56,25 @@ describe('POST /login/options', () => {
|
|
|
52
56
|
const { options: b } = await ceremony.getAuthenticationOptions()
|
|
53
57
|
expect(a.publicKey!.challenge).not.toBe(b.publicKey!.challenge)
|
|
54
58
|
})
|
|
59
|
+
|
|
60
|
+
test('behavior: restricts authentication to multiple credentials', async () => {
|
|
61
|
+
const { options } = await ceremony.getAuthenticationOptions({
|
|
62
|
+
credentialId: ['Y3JlZC0x', 'Y3JlZC0y'],
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
expect(options.publicKey?.allowCredentials).toMatchInlineSnapshot(`
|
|
66
|
+
[
|
|
67
|
+
{
|
|
68
|
+
"id": "Y3JlZC0x",
|
|
69
|
+
"type": "public-key",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"id": "Y3JlZC0y",
|
|
73
|
+
"type": "public-key",
|
|
74
|
+
},
|
|
75
|
+
]
|
|
76
|
+
`)
|
|
77
|
+
})
|
|
55
78
|
})
|
|
56
79
|
|
|
57
80
|
describe('POST /register', () => {
|
|
@@ -152,6 +175,59 @@ describe('hooks', () => {
|
|
|
152
175
|
await hookServer.closeAsync()
|
|
153
176
|
})
|
|
154
177
|
|
|
178
|
+
test('behavior: onRegister rejection removes the credential', async () => {
|
|
179
|
+
const kv = Kv.memory()
|
|
180
|
+
await kv.set('challenge:0x01', { created: Date.now(), name: 'Test' })
|
|
181
|
+
const publicKey =
|
|
182
|
+
'0x04ab891400140fc4f8e941ce0ff90e419de9470acaca613bbd717a4775435031a7d884318e919fd3b3e5a631d866d8a380b44063e70f0c381ee16e0652f7f97554'
|
|
183
|
+
vi.mocked(Registration.verify).mockReturnValueOnce({
|
|
184
|
+
aaguid: 'fbfc3007-154e-4ecc-8c0b-6e020557d7bd',
|
|
185
|
+
credential: { publicKey },
|
|
186
|
+
} as never)
|
|
187
|
+
const hookServer = await createServer(
|
|
188
|
+
webAuthn({
|
|
189
|
+
kv,
|
|
190
|
+
origin: 'http://localhost',
|
|
191
|
+
rpId: 'localhost',
|
|
192
|
+
onRegister() {
|
|
193
|
+
throw new Error('Authenticator not allowed')
|
|
194
|
+
},
|
|
195
|
+
}).listener,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const clientDataJSON = 'eyJjaGFsbGVuZ2UiOiJBUSJ9'
|
|
200
|
+
const response = await fetch(`${hookServer.url}/register`, {
|
|
201
|
+
method: 'POST',
|
|
202
|
+
headers: { 'Content-Type': 'application/json' },
|
|
203
|
+
body: JSON.stringify({
|
|
204
|
+
attestationObject: 'AA',
|
|
205
|
+
clientDataJSON,
|
|
206
|
+
id: 'credential',
|
|
207
|
+
publicKey,
|
|
208
|
+
raw: {
|
|
209
|
+
authenticatorAttachment: null,
|
|
210
|
+
id: 'credential',
|
|
211
|
+
rawId: 'Y3JlZGVudGlhbA',
|
|
212
|
+
response: { clientDataJSON },
|
|
213
|
+
type: 'public-key',
|
|
214
|
+
},
|
|
215
|
+
}),
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
expect(response.status).toMatchInlineSnapshot(`400`)
|
|
219
|
+
expect(await response.json()).toMatchInlineSnapshot(`
|
|
220
|
+
{
|
|
221
|
+
"error": "Authenticator not allowed",
|
|
222
|
+
}
|
|
223
|
+
`)
|
|
224
|
+
expect(await kv.get('credential:credential')).toBeUndefined()
|
|
225
|
+
expect(await kv.get('challenge:0x01')).toBeUndefined()
|
|
226
|
+
} finally {
|
|
227
|
+
await hookServer.closeAsync()
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
|
|
155
231
|
test('behavior: onAuthenticate error does not call hook', async () => {
|
|
156
232
|
let called = false
|
|
157
233
|
const hookServer = await createServer(
|
|
@@ -152,6 +152,7 @@ export function webAuthn(options: webAuthn.Options): webAuthn.ReturnType {
|
|
|
152
152
|
rpId,
|
|
153
153
|
})
|
|
154
154
|
|
|
155
|
+
const { aaguid } = result
|
|
155
156
|
const { publicKey } = result.credential
|
|
156
157
|
const credentialId = credential.id
|
|
157
158
|
// Base64url-encode the userId we registered with so it matches
|
|
@@ -160,22 +161,29 @@ export function webAuthn(options: webAuthn.Options): webAuthn.ReturnType {
|
|
|
160
161
|
const userId = stored.userId
|
|
161
162
|
? Base64.fromBytes(Bytes.fromString(stored.userId), { pad: false, url: true })
|
|
162
163
|
: undefined
|
|
163
|
-
const
|
|
164
|
+
const key = `credential:${credentialId}`
|
|
165
|
+
const created = await createCredential(kv, key, {
|
|
164
166
|
publicKey,
|
|
165
167
|
...(userId ? { userId } : {}),
|
|
166
168
|
})
|
|
167
169
|
if (!created) throw new Error('Credential already exists')
|
|
168
170
|
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
171
|
+
const hook = await (async () => {
|
|
172
|
+
try {
|
|
173
|
+
return await onRegister?.({
|
|
174
|
+
...(aaguid ? { aaguid } : {}),
|
|
175
|
+
credentialId,
|
|
176
|
+
name: stored.name,
|
|
177
|
+
publicKey,
|
|
178
|
+
request: c.req.raw,
|
|
179
|
+
...(userId ? { userId } : {}),
|
|
180
|
+
})
|
|
181
|
+
} catch (error) {
|
|
182
|
+
await Promise.all([kv.delete(key), kv.delete(`challenge:${challenge}`)])
|
|
183
|
+
throw error
|
|
184
|
+
}
|
|
185
|
+
})()
|
|
186
|
+
await kv.delete(`challenge:${challenge}`)
|
|
179
187
|
|
|
180
188
|
// Successful registration is also a successful authentication for
|
|
181
189
|
// the freshly-minted credential. Issue a session here so the
|
|
@@ -231,13 +239,13 @@ export function webAuthn(options: webAuthn.Options): webAuthn.ReturnType {
|
|
|
231
239
|
} = body as {
|
|
232
240
|
allowCredentialIds?: string[]
|
|
233
241
|
challenge?: Hex.Hex
|
|
234
|
-
credentialId?: string
|
|
242
|
+
credentialId?: string | string[]
|
|
235
243
|
mediation?: string
|
|
236
244
|
}
|
|
237
245
|
|
|
238
246
|
const { challenge, options: authOptions } = Authentication.getOptions({
|
|
239
247
|
challenge: requestChallenge,
|
|
240
|
-
credentialId:
|
|
248
|
+
credentialId: credentialId ?? allowCredentialIds,
|
|
241
249
|
rpId,
|
|
242
250
|
})
|
|
243
251
|
const options = mediation ? { ...authOptions, mediation } : authOptions
|
|
@@ -414,8 +422,14 @@ export declare namespace webAuthn {
|
|
|
414
422
|
* best-effort `get` then `set` storage.
|
|
415
423
|
*/
|
|
416
424
|
kv: Kv.Kv
|
|
417
|
-
/**
|
|
425
|
+
/**
|
|
426
|
+
* Called after a successful registration. The returned response is merged
|
|
427
|
+
* onto the default JSON response. Throwing rejects the request and removes
|
|
428
|
+
* the newly stored credential.
|
|
429
|
+
*/
|
|
418
430
|
onRegister?: (parameters: {
|
|
431
|
+
/** Authenticator model identifier, when the authenticator reports one. */
|
|
432
|
+
aaguid?: string | undefined
|
|
419
433
|
credentialId: string
|
|
420
434
|
/** The name provided during `/register/options` (e.g. user email). */
|
|
421
435
|
name: string
|