@portal-hq/web 3.6.2-alpha → 3.7.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.
- package/hypernative.d.ts +346 -0
- package/lib/commonjs/index.js +144 -2
- package/lib/commonjs/index.test.js +119 -2
- package/lib/commonjs/integrations/security/hypernative/index.js +101 -0
- package/lib/commonjs/integrations/security/hypernative/index.test.js +151 -0
- package/lib/commonjs/integrations/security/index.js +16 -0
- package/lib/commonjs/integrations/trading/zero-x/index.js +17 -4
- package/lib/commonjs/integrations/trading/zero-x/index.test.js +61 -15
- package/lib/commonjs/mpc/index.js +156 -5
- package/lib/commonjs/mpc/index.test.js +794 -5
- package/lib/commonjs/passkeys/index.js +394 -0
- package/lib/commonjs/passkeys/types.js +2 -0
- package/lib/commonjs/provider/index.js +5 -2
- package/lib/esm/index.js +144 -2
- package/lib/esm/index.test.js +119 -2
- package/lib/esm/integrations/security/hypernative/index.js +98 -0
- package/lib/esm/integrations/security/hypernative/index.test.js +146 -0
- package/lib/esm/integrations/security/index.js +10 -0
- package/lib/esm/integrations/trading/zero-x/index.js +17 -4
- package/lib/esm/integrations/trading/zero-x/index.test.js +62 -16
- package/lib/esm/mpc/index.js +156 -5
- package/lib/esm/mpc/index.test.js +795 -6
- package/lib/esm/passkeys/index.js +390 -0
- package/lib/esm/passkeys/types.js +1 -0
- package/lib/esm/provider/index.js +5 -2
- package/lifi-types.d.ts +1236 -0
- package/package.json +6 -3
- package/src/__mocks/constants.ts +422 -5
- package/src/__mocks/portal/mpc.ts +1 -0
- package/src/index.test.ts +179 -3
- package/src/index.ts +212 -4
- package/src/integrations/security/hypernative/index.test.ts +196 -0
- package/src/integrations/security/hypernative/index.ts +106 -0
- package/src/integrations/security/index.ts +14 -0
- package/src/integrations/trading/zero-x/index.test.ts +98 -19
- package/src/integrations/trading/zero-x/index.ts +29 -9
- package/src/mpc/index.test.ts +944 -7
- package/src/mpc/index.ts +200 -10
- package/src/passkeys/index.ts +536 -0
- package/src/passkeys/types.ts +78 -0
- package/src/provider/index.ts +5 -0
- package/tsconfig.json +7 -1
- package/types.d.ts +45 -12
- package/yieldxyz-types.d.ts +778 -0
- package/zero-x.d.ts +204 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AuthenticationParams,
|
|
3
|
+
BackupShareResult,
|
|
4
|
+
BeginAuthenticationResponse,
|
|
5
|
+
BeginRegistrationResponse,
|
|
6
|
+
FetchFunction,
|
|
7
|
+
FinishAuthenticationResponse,
|
|
8
|
+
GetJwtFunction,
|
|
9
|
+
LoginParams,
|
|
10
|
+
PasskeyServiceConfig,
|
|
11
|
+
PasskeyStatusResponse,
|
|
12
|
+
PublicKeyCredentialCreationOptionsJSON,
|
|
13
|
+
PublicKeyCredentialRequestOptionsJSON,
|
|
14
|
+
RegistrationParams,
|
|
15
|
+
} from './types'
|
|
16
|
+
|
|
17
|
+
export class PasskeyService {
|
|
18
|
+
private readonly defaultDomain: string
|
|
19
|
+
private readonly getJwt: GetJwtFunction
|
|
20
|
+
private readonly fetchImpl: FetchFunction
|
|
21
|
+
|
|
22
|
+
constructor({ defaultDomain, getJwt, fetchImpl }: PasskeyServiceConfig) {
|
|
23
|
+
this.defaultDomain = defaultDomain
|
|
24
|
+
this.getJwt = getJwt
|
|
25
|
+
this.fetchImpl = fetchImpl ?? fetch.bind(globalThis)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Generate MPC backup share and register a passkey in the top-level window
|
|
30
|
+
* while storing the provided encryption key with the relying party.
|
|
31
|
+
*/
|
|
32
|
+
public async registerPasskeyAndStoreKey(
|
|
33
|
+
params: RegistrationParams,
|
|
34
|
+
): Promise<void> {
|
|
35
|
+
const { customDomain, encryptionKey } = params
|
|
36
|
+
const { origin, relyingPartyOrigins, relyingPartyId, relyingPartyName } =
|
|
37
|
+
this.resolveParams(params)
|
|
38
|
+
|
|
39
|
+
const status = await this.getStatus(origin, relyingPartyId)
|
|
40
|
+
const normalizedStatus = status?.toLowerCase()
|
|
41
|
+
const shouldRegister =
|
|
42
|
+
!normalizedStatus ||
|
|
43
|
+
normalizedStatus === 'not registered' ||
|
|
44
|
+
normalizedStatus === 'registered'
|
|
45
|
+
|
|
46
|
+
if (!shouldRegister) {
|
|
47
|
+
await this.loginAndStoreKey({
|
|
48
|
+
customDomain,
|
|
49
|
+
relyingPartyId,
|
|
50
|
+
relyingPartyOrigins,
|
|
51
|
+
encryptionKey,
|
|
52
|
+
})
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const begin = await this.postJson<BeginRegistrationResponse>(
|
|
57
|
+
`${origin}/passkeys/begin-registration`,
|
|
58
|
+
{
|
|
59
|
+
relyingParty: relyingPartyId,
|
|
60
|
+
relyingPartyName,
|
|
61
|
+
relyingPartyOrigins,
|
|
62
|
+
},
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
const credential = await this.createCredential(begin.options.publicKey)
|
|
66
|
+
const attestation = serializeAttestationCredential(credential)
|
|
67
|
+
|
|
68
|
+
await this.postJsonVoid(
|
|
69
|
+
`${origin}/passkeys/finish-registration`,
|
|
70
|
+
{
|
|
71
|
+
sessionId: begin.sessionId,
|
|
72
|
+
relyingParty: relyingPartyId,
|
|
73
|
+
relyingPartyOrigins,
|
|
74
|
+
attestation,
|
|
75
|
+
encryptionKey,
|
|
76
|
+
},
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Authenticate with passkey in the current browsing context and return the encryption key.
|
|
82
|
+
*/
|
|
83
|
+
public async authenticatePasskeyAndRetrieveKey(
|
|
84
|
+
params: AuthenticationParams,
|
|
85
|
+
): Promise<string> {
|
|
86
|
+
const { origin, relyingPartyOrigins, relyingPartyId, relyingPartyName } =
|
|
87
|
+
this.resolveParams(params)
|
|
88
|
+
|
|
89
|
+
const begin = await this.postJson<BeginAuthenticationResponse>(
|
|
90
|
+
`${origin}/passkeys/begin-authentication`,
|
|
91
|
+
{
|
|
92
|
+
relyingParty: relyingPartyId,
|
|
93
|
+
relyingPartyName,
|
|
94
|
+
relyingPartyOrigins,
|
|
95
|
+
},
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
const credential = await this.getCredential(begin.options.publicKey)
|
|
99
|
+
const assertion = serializeAssertionCredential(credential)
|
|
100
|
+
|
|
101
|
+
const finish = await this.postJson<FinishAuthenticationResponse>(
|
|
102
|
+
`${origin}/passkeys/finish-authentication`,
|
|
103
|
+
{
|
|
104
|
+
sessionId: begin.sessionId,
|
|
105
|
+
relyingParty: relyingPartyId,
|
|
106
|
+
relyingPartyOrigins,
|
|
107
|
+
assertion,
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if (!finish?.encryptionKey) {
|
|
112
|
+
throw new Error('Passkey authentication succeeded but no encryption key was returned.')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return finish.encryptionKey
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Register a passkey without tying it to an encryption key.
|
|
120
|
+
* The encryption key can be stored later using authenticatePasskeyAndWriteKey.
|
|
121
|
+
*/
|
|
122
|
+
public async registerPasskey(
|
|
123
|
+
params: Omit<RegistrationParams, 'encryptionKey' | 'cipherText'>,
|
|
124
|
+
): Promise<void> {
|
|
125
|
+
const { origin, relyingPartyOrigins, relyingPartyId, relyingPartyName } =
|
|
126
|
+
this.resolveParams(params)
|
|
127
|
+
|
|
128
|
+
const status = await this.getStatus(origin, relyingPartyId)
|
|
129
|
+
const normalizedStatus = status?.toLowerCase()
|
|
130
|
+
if (normalizedStatus && normalizedStatus !== 'not registered' && normalizedStatus !== 'registered') {
|
|
131
|
+
throw new Error(`Passkey already exists with status: ${status}. Cannot register new credential.`)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const begin = await this.postJson<BeginRegistrationResponse>(
|
|
135
|
+
`${origin}/passkeys/begin-credential-registration`,
|
|
136
|
+
{
|
|
137
|
+
relyingParty: relyingPartyId,
|
|
138
|
+
relyingPartyName,
|
|
139
|
+
relyingPartyOrigins,
|
|
140
|
+
},
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
const credential = await this.createCredential(begin.options.publicKey)
|
|
144
|
+
const attestation = serializeAttestationCredential(credential)
|
|
145
|
+
|
|
146
|
+
await this.postJsonVoid(
|
|
147
|
+
`${origin}/passkeys/finish-credential-registration`,
|
|
148
|
+
{
|
|
149
|
+
sessionId: begin.sessionId,
|
|
150
|
+
relyingParty: relyingPartyId,
|
|
151
|
+
relyingPartyOrigins,
|
|
152
|
+
attestation,
|
|
153
|
+
},
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Authenticate with passkey and store an encryption key.
|
|
159
|
+
* Used after registerPasskey to associate an encryption key with the passkey.
|
|
160
|
+
*/
|
|
161
|
+
public async authenticatePasskeyAndWriteKey(
|
|
162
|
+
params: AuthenticationParams & { encryptionKey: string },
|
|
163
|
+
): Promise<void> {
|
|
164
|
+
const { encryptionKey } = params
|
|
165
|
+
const { origin, relyingPartyOrigins, relyingPartyId, relyingPartyName } =
|
|
166
|
+
this.resolveParams(params)
|
|
167
|
+
|
|
168
|
+
const begin = await this.postJson<BeginAuthenticationResponse>(
|
|
169
|
+
`${origin}/passkeys/begin-login`,
|
|
170
|
+
{
|
|
171
|
+
relyingParty: relyingPartyId,
|
|
172
|
+
relyingPartyName,
|
|
173
|
+
relyingPartyOrigins,
|
|
174
|
+
},
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
const credential = await this.getCredential(begin.options.publicKey)
|
|
178
|
+
const assertion = serializeAssertionCredential(credential)
|
|
179
|
+
|
|
180
|
+
await this.postJsonVoid(
|
|
181
|
+
`${origin}/passkeys/finish-login/write`,
|
|
182
|
+
{
|
|
183
|
+
sessionId: begin.sessionId,
|
|
184
|
+
relyingParty: relyingPartyId,
|
|
185
|
+
relyingPartyOrigins,
|
|
186
|
+
assertion,
|
|
187
|
+
encryptionKey,
|
|
188
|
+
},
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Utility used by combined backup flow to produce reusable payloads.
|
|
194
|
+
*/
|
|
195
|
+
public generateBackupSharePayload(cipherText: string, encryptionKey: string): BackupShareResult {
|
|
196
|
+
return {
|
|
197
|
+
cipherText,
|
|
198
|
+
encryptionKey,
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private async createCredential(
|
|
203
|
+
options: PublicKeyCredentialCreationOptionsJSON,
|
|
204
|
+
): Promise<PublicKeyCredential> {
|
|
205
|
+
const publicKey = decodeCreationOptions(options)
|
|
206
|
+
const credential = (await navigator.credentials.create({
|
|
207
|
+
publicKey,
|
|
208
|
+
})) as PublicKeyCredential | null
|
|
209
|
+
|
|
210
|
+
if (!credential) {
|
|
211
|
+
throw new Error('Passkey registration was cancelled or failed to create credential.')
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return credential
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private async loginAndStoreKey(params: LoginParams): Promise<void> {
|
|
218
|
+
const { customDomain, relyingPartyId, relyingPartyOrigins, encryptionKey } =
|
|
219
|
+
params
|
|
220
|
+
|
|
221
|
+
const origin = this.normalizeDomain(customDomain)
|
|
222
|
+
|
|
223
|
+
const begin = await this.postJson<BeginAuthenticationResponse>(
|
|
224
|
+
`${origin}/passkeys/begin-login`,
|
|
225
|
+
{
|
|
226
|
+
relyingParty: relyingPartyId,
|
|
227
|
+
relyingPartyOrigins,
|
|
228
|
+
},
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
const credential = await this.getCredential(begin.options.publicKey)
|
|
232
|
+
const assertion = serializeAssertionCredential(credential)
|
|
233
|
+
|
|
234
|
+
await this.postJsonVoid(
|
|
235
|
+
`${origin}/passkeys/finish-login/write`,
|
|
236
|
+
{
|
|
237
|
+
sessionId: begin.sessionId,
|
|
238
|
+
relyingParty: relyingPartyId,
|
|
239
|
+
relyingPartyOrigins,
|
|
240
|
+
assertion,
|
|
241
|
+
encryptionKey,
|
|
242
|
+
},
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private async getStatus(
|
|
247
|
+
origin: string,
|
|
248
|
+
relyingPartyId: string,
|
|
249
|
+
): Promise<string | undefined> {
|
|
250
|
+
try {
|
|
251
|
+
const query = new URLSearchParams({ relyingParty: relyingPartyId })
|
|
252
|
+
const url = `${origin}/passkeys/status?${query.toString()}`
|
|
253
|
+
|
|
254
|
+
const response = await this.fetchImpl(url, {
|
|
255
|
+
method: 'GET',
|
|
256
|
+
headers: await this.buildHeaders(),
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
if (!response.ok) {
|
|
260
|
+
return undefined
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const raw = await response.text()
|
|
264
|
+
if (!raw) {
|
|
265
|
+
return undefined
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const data = JSON.parse(raw) as PasskeyStatusResponse
|
|
269
|
+
return data.status
|
|
270
|
+
} catch (error) {
|
|
271
|
+
console.warn('[Portal] Failed to fetch passkey status', error)
|
|
272
|
+
return undefined
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private async getCredential(
|
|
277
|
+
options: PublicKeyCredentialRequestOptionsJSON,
|
|
278
|
+
): Promise<PublicKeyCredential> {
|
|
279
|
+
const publicKey = decodeRequestOptions(options)
|
|
280
|
+
const credential = (await navigator.credentials.get({
|
|
281
|
+
publicKey,
|
|
282
|
+
})) as PublicKeyCredential | null
|
|
283
|
+
|
|
284
|
+
if (!credential) {
|
|
285
|
+
throw new Error('Passkey authentication was cancelled or failed to resolve credential.')
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return credential
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private normalizeDomain(customDomain?: string): string {
|
|
292
|
+
const domain = customDomain ?? this.defaultDomain
|
|
293
|
+
if (!domain) {
|
|
294
|
+
throw new Error('Passkey domain is not configured.')
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (domain.startsWith('http://') || domain.startsWith('https://')) {
|
|
298
|
+
return domain
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return `https://${domain}`
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private buildRelyingPartyOrigins(origin: string): string[] {
|
|
305
|
+
const set = new Set<string>([origin])
|
|
306
|
+
|
|
307
|
+
if (typeof window !== 'undefined' && window.location?.origin) {
|
|
308
|
+
set.add(window.location.origin)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return Array.from(set)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private resolveParams(options: {
|
|
315
|
+
customDomain?: string
|
|
316
|
+
relyingPartyId: string
|
|
317
|
+
relyingPartyName?: string
|
|
318
|
+
}): {
|
|
319
|
+
origin: string
|
|
320
|
+
relyingPartyOrigins: string[]
|
|
321
|
+
relyingPartyId: string
|
|
322
|
+
relyingPartyName?: string
|
|
323
|
+
} {
|
|
324
|
+
const origin = this.normalizeDomain(options.customDomain)
|
|
325
|
+
const relyingPartyOrigins = this.buildRelyingPartyOrigins(origin)
|
|
326
|
+
return {
|
|
327
|
+
origin,
|
|
328
|
+
relyingPartyOrigins,
|
|
329
|
+
relyingPartyId: options.relyingPartyId,
|
|
330
|
+
relyingPartyName: options.relyingPartyName,
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private async postJson<T>(
|
|
335
|
+
url: string,
|
|
336
|
+
body: Record<string, unknown>,
|
|
337
|
+
): Promise<T> {
|
|
338
|
+
const response = await this.fetchImpl(url, {
|
|
339
|
+
method: 'POST',
|
|
340
|
+
headers: await this.buildHeaders(),
|
|
341
|
+
body: JSON.stringify(body),
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
if (!response.ok) {
|
|
345
|
+
const text = await safeReadText(response)
|
|
346
|
+
throw new Error(
|
|
347
|
+
`Passkey request failed (${response.status} ${response.statusText}): ${text}`,
|
|
348
|
+
)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return response.json() as Promise<T>
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private async postJsonVoid(
|
|
355
|
+
url: string,
|
|
356
|
+
body: Record<string, unknown>,
|
|
357
|
+
): Promise<void> {
|
|
358
|
+
const response = await this.fetchImpl(url, {
|
|
359
|
+
method: 'POST',
|
|
360
|
+
headers: await this.buildHeaders(),
|
|
361
|
+
body: JSON.stringify(body),
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
if (!response.ok) {
|
|
365
|
+
const text = await safeReadText(response)
|
|
366
|
+
throw new Error(
|
|
367
|
+
`Passkey request failed (${response.status} ${response.statusText}): ${text}`,
|
|
368
|
+
)
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private async buildHeaders(): Promise<HeadersInit> {
|
|
373
|
+
const jwt = await this.getJwt()
|
|
374
|
+
return {
|
|
375
|
+
'Content-Type': 'application/json',
|
|
376
|
+
Authorization: `Bearer ${jwt}`,
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const decodeCreationOptions = (
|
|
382
|
+
options: PublicKeyCredentialCreationOptionsJSON,
|
|
383
|
+
): PublicKeyCredentialCreationOptions => {
|
|
384
|
+
const { excludeCredentials, ...rest } = options
|
|
385
|
+
|
|
386
|
+
const publicKey: PublicKeyCredentialCreationOptions = {
|
|
387
|
+
...rest,
|
|
388
|
+
challenge: base64UrlToArrayBuffer(options.challenge),
|
|
389
|
+
user: {
|
|
390
|
+
...options.user,
|
|
391
|
+
id: base64UrlToArrayBuffer(options.user.id),
|
|
392
|
+
},
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (excludeCredentials) {
|
|
396
|
+
publicKey.excludeCredentials = excludeCredentials.map(
|
|
397
|
+
(credential): PublicKeyCredentialDescriptor => ({
|
|
398
|
+
type: credential.type,
|
|
399
|
+
id: base64UrlToArrayBuffer(credential.id),
|
|
400
|
+
transports: credential.transports,
|
|
401
|
+
}),
|
|
402
|
+
)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return publicKey
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const decodeRequestOptions = (
|
|
409
|
+
options: PublicKeyCredentialRequestOptionsJSON,
|
|
410
|
+
): PublicKeyCredentialRequestOptions => {
|
|
411
|
+
const { allowCredentials, ...rest } = options
|
|
412
|
+
|
|
413
|
+
const publicKey: PublicKeyCredentialRequestOptions = {
|
|
414
|
+
...rest,
|
|
415
|
+
challenge: base64UrlToArrayBuffer(options.challenge),
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (allowCredentials) {
|
|
419
|
+
publicKey.allowCredentials = allowCredentials.map(
|
|
420
|
+
(credential): PublicKeyCredentialDescriptor => ({
|
|
421
|
+
type: credential.type,
|
|
422
|
+
id: base64UrlToArrayBuffer(credential.id),
|
|
423
|
+
transports: credential.transports,
|
|
424
|
+
}),
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return publicKey
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const serializeAttestationCredential = (
|
|
432
|
+
credential: PublicKeyCredential,
|
|
433
|
+
): string => {
|
|
434
|
+
const { response } = credential
|
|
435
|
+
const attestationResponse = response as AuthenticatorAttestationResponse
|
|
436
|
+
|
|
437
|
+
const payload = {
|
|
438
|
+
id: credential.id,
|
|
439
|
+
rawId: arrayBufferToBase64Url(credential.rawId),
|
|
440
|
+
type: credential.type,
|
|
441
|
+
response: {
|
|
442
|
+
clientDataJSON: arrayBufferToBase64Url(attestationResponse.clientDataJSON),
|
|
443
|
+
attestationObject: attestationResponse.attestationObject
|
|
444
|
+
? arrayBufferToBase64Url(attestationResponse.attestationObject)
|
|
445
|
+
: undefined,
|
|
446
|
+
transports: attestationResponse.getTransports?.(),
|
|
447
|
+
},
|
|
448
|
+
clientExtensionResults: credential.getClientExtensionResults(),
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return JSON.stringify(payload)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const serializeAssertionCredential = (
|
|
455
|
+
credential: PublicKeyCredential,
|
|
456
|
+
): string => {
|
|
457
|
+
const { response } = credential
|
|
458
|
+
const assertionResponse = response as AuthenticatorAssertionResponse
|
|
459
|
+
|
|
460
|
+
const payload = {
|
|
461
|
+
id: credential.id,
|
|
462
|
+
rawId: arrayBufferToBase64Url(credential.rawId),
|
|
463
|
+
type: credential.type,
|
|
464
|
+
response: {
|
|
465
|
+
clientDataJSON: arrayBufferToBase64Url(assertionResponse.clientDataJSON),
|
|
466
|
+
authenticatorData: arrayBufferToBase64Url(assertionResponse.authenticatorData),
|
|
467
|
+
signature: arrayBufferToBase64Url(assertionResponse.signature),
|
|
468
|
+
userHandle: assertionResponse.userHandle
|
|
469
|
+
? arrayBufferToBase64Url(assertionResponse.userHandle)
|
|
470
|
+
: null,
|
|
471
|
+
},
|
|
472
|
+
clientExtensionResults: credential.getClientExtensionResults(),
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return JSON.stringify(payload)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const base64UrlToArrayBuffer = (value: string): ArrayBuffer => {
|
|
479
|
+
const normalized = value.replace(/-/g, '+').replace(/_/g, '/')
|
|
480
|
+
const padded = normalized.padEnd(normalized.length + ((4 - (normalized.length % 4)) % 4), '=')
|
|
481
|
+
|
|
482
|
+
const binary =
|
|
483
|
+
typeof atob === 'function'
|
|
484
|
+
? atob(padded)
|
|
485
|
+
: decodeWithBufferFallback(padded)
|
|
486
|
+
|
|
487
|
+
const bytes = new Uint8Array(binary.length)
|
|
488
|
+
for (let i = 0; i < binary.length; i++) {
|
|
489
|
+
bytes[i] = binary.charCodeAt(i)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return bytes.buffer
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const arrayBufferToBase64Url = (buffer: ArrayBuffer): string => {
|
|
496
|
+
const bytes = new Uint8Array(buffer)
|
|
497
|
+
let binary = ''
|
|
498
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
499
|
+
binary += String.fromCharCode(bytes[i])
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const base64 =
|
|
503
|
+
typeof btoa === 'function'
|
|
504
|
+
? btoa(binary)
|
|
505
|
+
: encodeWithBufferFallback(binary)
|
|
506
|
+
|
|
507
|
+
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const safeReadText = async (response: Response): Promise<string> => {
|
|
511
|
+
try {
|
|
512
|
+
return await response.text()
|
|
513
|
+
} catch (error) {
|
|
514
|
+
return (error as Error).message
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
export default PasskeyService
|
|
519
|
+
|
|
520
|
+
const decodeWithBufferFallback = (value: string): string => {
|
|
521
|
+
const bufferCtor = (globalThis as Record<string, any>).Buffer
|
|
522
|
+
if (bufferCtor) {
|
|
523
|
+
return bufferCtor.from(value, 'base64').toString('binary')
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
throw new Error('Base64 decoding is not supported in this environment.')
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const encodeWithBufferFallback = (binary: string): string => {
|
|
530
|
+
const bufferCtor = (globalThis as Record<string, any>).Buffer
|
|
531
|
+
if (bufferCtor) {
|
|
532
|
+
return bufferCtor.from(binary, 'binary').toString('base64')
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
throw new Error('Base64 encoding is not supported in this environment.')
|
|
536
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { PasskeyOptions, BackupShareResult } from '../../types'
|
|
2
|
+
|
|
3
|
+
export type FetchFunction = typeof fetch
|
|
4
|
+
export type GetJwtFunction = () => Promise<string>
|
|
5
|
+
|
|
6
|
+
export interface PasskeyServiceConfig {
|
|
7
|
+
/**
|
|
8
|
+
* Default hostname (or origin) that hosts Portal passkey endpoints.
|
|
9
|
+
*/
|
|
10
|
+
defaultDomain: string
|
|
11
|
+
/**
|
|
12
|
+
* Callback to fetch a scoped JWT for passkey operations.
|
|
13
|
+
* This is more secure than using the API key directly as the JWT
|
|
14
|
+
* has only "passkey" permission, reducing blast radius if compromised.
|
|
15
|
+
*/
|
|
16
|
+
getJwt: GetJwtFunction
|
|
17
|
+
/**
|
|
18
|
+
* Allow injecting a custom fetch implementation (primarily for testing).
|
|
19
|
+
*/
|
|
20
|
+
fetchImpl?: FetchFunction
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface RegistrationParams extends PasskeyOptions {
|
|
24
|
+
cipherText: string
|
|
25
|
+
encryptionKey: string
|
|
26
|
+
relyingPartyId: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface AuthenticationParams extends PasskeyOptions {
|
|
30
|
+
relyingPartyId: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface LoginParams extends PasskeyOptions {
|
|
34
|
+
relyingPartyId: string
|
|
35
|
+
relyingPartyOrigins: string[]
|
|
36
|
+
encryptionKey: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface BeginRegistrationResponse {
|
|
40
|
+
options: { publicKey: PublicKeyCredentialCreationOptionsJSON }
|
|
41
|
+
sessionId: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface BeginAuthenticationResponse {
|
|
45
|
+
options: { publicKey: PublicKeyCredentialRequestOptionsJSON }
|
|
46
|
+
sessionId: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface FinishAuthenticationResponse {
|
|
50
|
+
encryptionKey: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface PasskeyStatusResponse {
|
|
54
|
+
status: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type PublicKeyCredentialCreationOptionsJSON = Omit<
|
|
58
|
+
PublicKeyCredentialCreationOptions,
|
|
59
|
+
'challenge' | 'user' | 'excludeCredentials'
|
|
60
|
+
> & {
|
|
61
|
+
challenge: string
|
|
62
|
+
user: Omit<PublicKeyCredentialUserEntity, 'id'> & { id: string }
|
|
63
|
+
excludeCredentials?: Array<
|
|
64
|
+
Omit<PublicKeyCredentialDescriptor, 'id'> & { id: string }
|
|
65
|
+
>
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type PublicKeyCredentialRequestOptionsJSON = Omit<
|
|
69
|
+
PublicKeyCredentialRequestOptions,
|
|
70
|
+
'challenge' | 'allowCredentials'
|
|
71
|
+
> & {
|
|
72
|
+
challenge: string
|
|
73
|
+
allowCredentials?: Array<
|
|
74
|
+
Omit<PublicKeyCredentialDescriptor, 'id'> & { id: string }
|
|
75
|
+
>
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type { PasskeyOptions, BackupShareResult }
|
package/src/provider/index.ts
CHANGED
|
@@ -248,6 +248,7 @@ class Provider {
|
|
|
248
248
|
chainId: requestChainId,
|
|
249
249
|
method,
|
|
250
250
|
params,
|
|
251
|
+
sponsorGas,
|
|
251
252
|
}: RequestArguments): Promise<any> {
|
|
252
253
|
const isSignerMethod = signerMethods.includes(method)
|
|
253
254
|
const chainId = this.getCAIP2ChainId(requestChainId)
|
|
@@ -278,6 +279,7 @@ class Provider {
|
|
|
278
279
|
chainId,
|
|
279
280
|
method,
|
|
280
281
|
params,
|
|
282
|
+
sponsorGas,
|
|
281
283
|
})
|
|
282
284
|
|
|
283
285
|
if (transactionHash) {
|
|
@@ -432,6 +434,7 @@ class Provider {
|
|
|
432
434
|
chainId,
|
|
433
435
|
method,
|
|
434
436
|
params,
|
|
437
|
+
sponsorGas,
|
|
435
438
|
}: RequestArguments): Promise<any> {
|
|
436
439
|
const isApproved = passiveSignerMethods.includes(method)
|
|
437
440
|
? true
|
|
@@ -467,6 +470,7 @@ class Provider {
|
|
|
467
470
|
method,
|
|
468
471
|
params: this.buildParams(method, params),
|
|
469
472
|
rpcUrl: this.portal.getRpcUrl(chainId),
|
|
473
|
+
sponsorGas,
|
|
470
474
|
})
|
|
471
475
|
return result
|
|
472
476
|
}
|
|
@@ -480,6 +484,7 @@ class Provider {
|
|
|
480
484
|
method,
|
|
481
485
|
params: this.buildParams(method, params),
|
|
482
486
|
rpcUrl: this.portal.getRpcUrl(chainId),
|
|
487
|
+
sponsorGas,
|
|
483
488
|
})
|
|
484
489
|
return result
|
|
485
490
|
}
|
package/tsconfig.json
CHANGED
|
@@ -19,6 +19,12 @@
|
|
|
19
19
|
"types": ["jest"]
|
|
20
20
|
},
|
|
21
21
|
"exclude": ["lib", "**/node_modules", "**/jest.config.ts"],
|
|
22
|
-
"files": [
|
|
22
|
+
"files": [
|
|
23
|
+
"types.d.ts",
|
|
24
|
+
"yieldxyz-types.d.ts",
|
|
25
|
+
"lifi-types.d.ts",
|
|
26
|
+
"hypernative.d.ts",
|
|
27
|
+
"zero-x.d.ts"
|
|
28
|
+
],
|
|
23
29
|
"include": ["**/*.ts", "**/*.tsx"]
|
|
24
30
|
}
|