@0xsequence/wallet-wdk 3.0.3 → 3.0.5
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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-lint.log +1 -1
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +24 -0
- package/dist/dbs/auth-commitments.d.ts +21 -3
- package/dist/dbs/auth-commitments.d.ts.map +1 -1
- package/dist/dbs/index.d.ts +1 -1
- package/dist/dbs/index.d.ts.map +1 -1
- package/dist/sequence/handlers/authcode-pkce.d.ts +2 -1
- package/dist/sequence/handlers/authcode-pkce.d.ts.map +1 -1
- package/dist/sequence/handlers/authcode-pkce.js +15 -9
- package/dist/sequence/handlers/authcode.d.ts +2 -1
- package/dist/sequence/handlers/authcode.d.ts.map +1 -1
- package/dist/sequence/handlers/authcode.js +19 -10
- package/dist/sequence/handlers/idtoken.d.ts +3 -3
- package/dist/sequence/handlers/idtoken.d.ts.map +1 -1
- package/dist/sequence/handlers/idtoken.js +3 -0
- package/dist/sequence/index.d.ts +1 -1
- package/dist/sequence/index.d.ts.map +1 -1
- package/dist/sequence/manager.d.ts +3 -0
- package/dist/sequence/manager.d.ts.map +1 -1
- package/dist/sequence/manager.js +7 -1
- package/dist/sequence/types/signature-request.d.ts +4 -0
- package/dist/sequence/types/signature-request.d.ts.map +1 -1
- package/dist/sequence/types/signature-request.js +2 -0
- package/dist/sequence/wallets.d.ts +84 -2
- package/dist/sequence/wallets.d.ts.map +1 -1
- package/dist/sequence/wallets.js +180 -24
- package/package.json +8 -8
- package/src/dbs/auth-commitments.ts +6 -3
- package/src/dbs/index.ts +1 -1
- package/src/sequence/handlers/authcode-pkce.ts +16 -10
- package/src/sequence/handlers/authcode.ts +20 -11
- package/src/sequence/handlers/idtoken.ts +8 -2
- package/src/sequence/index.ts +3 -0
- package/src/sequence/manager.ts +32 -14
- package/src/sequence/types/signature-request.ts +4 -0
- package/src/sequence/wallets.ts +315 -27
- package/test/authcode-pkce.test.ts +27 -18
- package/test/authcode.test.ts +24 -19
- package/test/identity-auth-dbs.test.ts +44 -7
- package/test/idtoken.test.ts +16 -0
- package/test/sessions-idtoken.test.ts +1 -0
- package/test/wallets.test.ts +62 -1
package/src/sequence/wallets.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { MnemonicHandler } from './handlers/mnemonic.js'
|
|
|
8
8
|
import { OtpHandler } from './handlers/otp.js'
|
|
9
9
|
import { Shared } from './manager.js'
|
|
10
10
|
import { Device } from './types/device.js'
|
|
11
|
-
import { Action, Module } from './types/index.js'
|
|
11
|
+
import { Action, Actions, Module } from './types/index.js'
|
|
12
12
|
import { Kinds, SignerWithKind, WitnessExtraSignerKind } from './types/signer.js'
|
|
13
13
|
import { Wallet, WalletSelectionUiHandler } from './types/wallet.js'
|
|
14
14
|
import { PasskeysHandler } from './handlers/passkeys.js'
|
|
@@ -28,13 +28,19 @@ function getSignerKindForSignup(kind: SignupArgs['kind'] | AuthCommitment['kind'
|
|
|
28
28
|
if (kind === 'google-id-token' || kind === 'google-pkce') {
|
|
29
29
|
return Kinds.LoginGoogle
|
|
30
30
|
}
|
|
31
|
+
if (kind === 'apple-id-token' || kind === 'apple') {
|
|
32
|
+
return Kinds.LoginApple
|
|
33
|
+
}
|
|
31
34
|
if (kind.startsWith('custom-')) {
|
|
32
35
|
return kind
|
|
33
36
|
}
|
|
34
37
|
return ('login-' + kind) as string
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
function getIdTokenSignupHandler(
|
|
40
|
+
function getIdTokenSignupHandler(
|
|
41
|
+
shared: Shared,
|
|
42
|
+
kind: typeof Kinds.LoginGoogle | typeof Kinds.LoginApple | `custom-${string}`,
|
|
43
|
+
): IdTokenHandler {
|
|
38
44
|
const handler = shared.handlers.get(kind)
|
|
39
45
|
if (!handler) {
|
|
40
46
|
throw new Error('handler-not-registered')
|
|
@@ -51,6 +57,12 @@ export type StartSignUpWithRedirectArgs = {
|
|
|
51
57
|
metadata: { [key: string]: string }
|
|
52
58
|
}
|
|
53
59
|
|
|
60
|
+
export type StartAddLoginSignerWithRedirectArgs = {
|
|
61
|
+
wallet: Address.Address
|
|
62
|
+
kind: 'google-pkce' | 'apple' | `custom-${string}`
|
|
63
|
+
target: string
|
|
64
|
+
}
|
|
65
|
+
|
|
54
66
|
export type SignupStatus =
|
|
55
67
|
| { type: 'login-signer-created'; address: Address.Address }
|
|
56
68
|
| { type: 'device-signer-created'; address: Address.Address }
|
|
@@ -82,7 +94,7 @@ export type EmailOtpSignupArgs = CommonSignupArgs & {
|
|
|
82
94
|
}
|
|
83
95
|
|
|
84
96
|
export type IdTokenSignupArgs = CommonSignupArgs & {
|
|
85
|
-
kind: 'google-id-token' | `custom-${string}`
|
|
97
|
+
kind: 'google-id-token' | 'apple-id-token' | `custom-${string}`
|
|
86
98
|
idToken: string
|
|
87
99
|
}
|
|
88
100
|
|
|
@@ -106,6 +118,19 @@ export type SignupArgs =
|
|
|
106
118
|
| IdTokenSignupArgs
|
|
107
119
|
| AuthCodeSignupArgs
|
|
108
120
|
|
|
121
|
+
export type AddLoginSignerArgs = {
|
|
122
|
+
wallet: Address.Address
|
|
123
|
+
} & (
|
|
124
|
+
| { kind: 'mnemonic'; mnemonic: string }
|
|
125
|
+
| { kind: 'email-otp'; email: string }
|
|
126
|
+
| { kind: 'google-id-token' | 'apple-id-token' | `custom-${string}`; idToken: string }
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
export type RemoveLoginSignerArgs = {
|
|
130
|
+
wallet: Address.Address
|
|
131
|
+
signerAddress: Address.Address
|
|
132
|
+
}
|
|
133
|
+
|
|
109
134
|
export type LoginToWalletArgs = {
|
|
110
135
|
wallet: Address.Address
|
|
111
136
|
}
|
|
@@ -222,7 +247,7 @@ export interface WalletsInterface {
|
|
|
222
247
|
* - `kind: 'mnemonic'`: Uses a mnemonic phrase as the login credential.
|
|
223
248
|
* - `kind: 'passkey'`: Prompts the user to create a WebAuthn passkey.
|
|
224
249
|
* - `kind: 'email-otp'`: Initiates an OTP flow to the user's email.
|
|
225
|
-
* - `kind: 'google-id-token'`: Completes an OIDC ID token flow when
|
|
250
|
+
* - `kind: 'google-id-token' | 'apple-id-token'`: Completes an OIDC ID token flow when the provider is configured with `authMethod: 'id-token'`.
|
|
226
251
|
* - `kind: 'google-pkce' | 'apple'`: Completes an OAuth redirect flow.
|
|
227
252
|
* Common options like `noGuard` or `noRecovery` can customize the wallet's security features.
|
|
228
253
|
* @returns A promise that resolves to the address of the newly created wallet, or `undefined` if the sign-up was aborted.
|
|
@@ -286,6 +311,66 @@ export interface WalletsInterface {
|
|
|
286
311
|
*/
|
|
287
312
|
completeLogin(requestId: string): Promise<void>
|
|
288
313
|
|
|
314
|
+
/**
|
|
315
|
+
* Adds a new login signer to an existing wallet, enabling account federation.
|
|
316
|
+
*
|
|
317
|
+
* This allows a user to link a new login method (e.g., Google, email OTP, mnemonic) to a wallet
|
|
318
|
+
* that was originally created with a different credential. After federation, the wallet can be
|
|
319
|
+
* discovered and accessed via any of its linked login methods.
|
|
320
|
+
*
|
|
321
|
+
* @param args The arguments specifying the wallet and the new login credential to add.
|
|
322
|
+
* @returns A promise that resolves to a `requestId` for the configuration update signature request.
|
|
323
|
+
* @see {completeAddLoginSigner}
|
|
324
|
+
*/
|
|
325
|
+
addLoginSigner(args: AddLoginSignerArgs): Promise<string>
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Completes the add-login-signer process after the configuration update has been signed.
|
|
329
|
+
*
|
|
330
|
+
* @param requestId The ID of the completed signature request returned by `addLoginSigner`.
|
|
331
|
+
* @returns A promise that resolves when the configuration update has been submitted.
|
|
332
|
+
*/
|
|
333
|
+
completeAddLoginSigner(requestId: string): Promise<void>
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Initiates an add-login-signer process that involves an OAuth redirect.
|
|
337
|
+
*
|
|
338
|
+
* This is the first step for adding a social login signer (e.g., Google, Apple) to an existing wallet
|
|
339
|
+
* via a redirect-based OAuth flow. It validates the wallet, generates the necessary challenges and state,
|
|
340
|
+
* stores them locally, and returns a URL. Your application should redirect the user to this URL.
|
|
341
|
+
*
|
|
342
|
+
* After the redirect callback, call `completeRedirect` with the returned state and code. This will
|
|
343
|
+
* create a pending `AddLoginSigner` signature request internally. The caller can then discover
|
|
344
|
+
* the pending request through the signatures module and call `completeAddLoginSigner` with the requestId
|
|
345
|
+
* once it has been signed.
|
|
346
|
+
*
|
|
347
|
+
* @param args Arguments specifying the wallet, provider (`kind`), and the `target` URL for the redirect callback.
|
|
348
|
+
* @returns A promise that resolves to the full OAuth URL to which the user should be redirected.
|
|
349
|
+
* @see {completeRedirect} for the second step of this flow.
|
|
350
|
+
* @see {completeAddLoginSigner} to finalize after signing.
|
|
351
|
+
*/
|
|
352
|
+
startAddLoginSignerWithRedirect(args: StartAddLoginSignerWithRedirectArgs): Promise<string>
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Removes a login signer from an existing wallet, enabling account defederation.
|
|
356
|
+
*
|
|
357
|
+
* This allows a user to unlink a login method from a wallet. A safety guard ensures
|
|
358
|
+
* at least one login signer always remains.
|
|
359
|
+
*
|
|
360
|
+
* @param args The arguments specifying the wallet and the signer address to remove.
|
|
361
|
+
* @returns A promise that resolves to a `requestId` for the configuration update signature request.
|
|
362
|
+
* @see {completeRemoveLoginSigner}
|
|
363
|
+
*/
|
|
364
|
+
removeLoginSigner(args: RemoveLoginSignerArgs): Promise<string>
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Completes the remove-login-signer process after the configuration update has been signed.
|
|
368
|
+
*
|
|
369
|
+
* @param requestId The ID of the completed signature request returned by `removeLoginSigner`.
|
|
370
|
+
* @returns A promise that resolves when the configuration update has been submitted.
|
|
371
|
+
*/
|
|
372
|
+
completeRemoveLoginSigner(requestId: string): Promise<void>
|
|
373
|
+
|
|
289
374
|
/**
|
|
290
375
|
* Logs out from a given wallet, ending the current session.
|
|
291
376
|
*
|
|
@@ -411,6 +496,20 @@ export function isIdTokenArgs(args: SignupArgs): args is IdTokenSignupArgs {
|
|
|
411
496
|
return 'idToken' in args
|
|
412
497
|
}
|
|
413
498
|
|
|
499
|
+
function addLoginSignerToSignupArgs(args: AddLoginSignerArgs): SignupArgs {
|
|
500
|
+
switch (args.kind) {
|
|
501
|
+
case 'mnemonic':
|
|
502
|
+
return { kind: 'mnemonic', mnemonic: args.mnemonic }
|
|
503
|
+
case 'email-otp':
|
|
504
|
+
return { kind: 'email-otp', email: args.email }
|
|
505
|
+
default: {
|
|
506
|
+
// google-id-token, apple-id-token, custom-*
|
|
507
|
+
const _args = args as { kind: string; idToken: string }
|
|
508
|
+
return { kind: _args.kind as IdTokenSignupArgs['kind'], idToken: _args.idToken }
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
414
513
|
function buildCappedTree(members: { address: Address.Address; imageHash?: Hex.Hex }[]): Config.Topology {
|
|
415
514
|
const loginMemberWeight = 1n
|
|
416
515
|
|
|
@@ -721,8 +820,12 @@ export class Wallets implements WalletsInterface {
|
|
|
721
820
|
}
|
|
722
821
|
}
|
|
723
822
|
|
|
724
|
-
case 'google-id-token':
|
|
725
|
-
|
|
823
|
+
case 'google-id-token':
|
|
824
|
+
case 'apple-id-token': {
|
|
825
|
+
const handler = getIdTokenSignupHandler(
|
|
826
|
+
this.shared,
|
|
827
|
+
args.kind === 'google-id-token' ? Kinds.LoginGoogle : Kinds.LoginApple,
|
|
828
|
+
)
|
|
726
829
|
const [signer, metadata] = await handler.completeAuth(args.idToken)
|
|
727
830
|
const loginEmail = metadata.email
|
|
728
831
|
this.shared.modules.logger.log('Created new id token signer:', signer.address)
|
|
@@ -730,7 +833,7 @@ export class Wallets implements WalletsInterface {
|
|
|
730
833
|
return {
|
|
731
834
|
signer,
|
|
732
835
|
extra: {
|
|
733
|
-
signerKind:
|
|
836
|
+
signerKind: getSignerKindForSignup(args.kind),
|
|
734
837
|
},
|
|
735
838
|
loginEmail,
|
|
736
839
|
}
|
|
@@ -797,7 +900,27 @@ export class Wallets implements WalletsInterface {
|
|
|
797
900
|
if (!(handler instanceof AuthCodeHandler)) {
|
|
798
901
|
throw new Error('handler-does-not-support-redirect')
|
|
799
902
|
}
|
|
800
|
-
return handler.commitAuth(args.target,
|
|
903
|
+
return handler.commitAuth(args.target, { type: 'auth' })
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
async startAddLoginSignerWithRedirect(args: StartAddLoginSignerWithRedirectArgs) {
|
|
907
|
+
const walletEntry = await this.get(args.wallet)
|
|
908
|
+
if (!walletEntry) {
|
|
909
|
+
throw new Error('wallet-not-found')
|
|
910
|
+
}
|
|
911
|
+
if (walletEntry.status !== 'ready') {
|
|
912
|
+
throw new Error('wallet-not-ready')
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const kind = getSignupHandlerKey(args.kind)
|
|
916
|
+
const handler = this.shared.handlers.get(kind)
|
|
917
|
+
if (!handler) {
|
|
918
|
+
throw new Error('handler-not-registered')
|
|
919
|
+
}
|
|
920
|
+
if (!(handler instanceof AuthCodeHandler)) {
|
|
921
|
+
throw new Error('handler-does-not-support-redirect')
|
|
922
|
+
}
|
|
923
|
+
return handler.commitAuth(args.target, { type: 'add-signer', wallet: args.wallet })
|
|
801
924
|
}
|
|
802
925
|
|
|
803
926
|
async completeRedirect(args: CompleteRedirectArgs): Promise<string> {
|
|
@@ -806,28 +929,62 @@ export class Wallets implements WalletsInterface {
|
|
|
806
929
|
throw new Error('invalid-state')
|
|
807
930
|
}
|
|
808
931
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
932
|
+
switch (commitment.type) {
|
|
933
|
+
case 'add-signer': {
|
|
934
|
+
const handlerKind = getSignupHandlerKey(commitment.kind)
|
|
935
|
+
const handler = this.shared.handlers.get(handlerKind)
|
|
936
|
+
if (!handler) {
|
|
937
|
+
throw new Error('handler-not-registered')
|
|
938
|
+
}
|
|
939
|
+
if (!(handler instanceof AuthCodeHandler)) {
|
|
940
|
+
throw new Error('handler-does-not-support-redirect')
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const walletAddress = commitment.wallet as Address.Address
|
|
944
|
+
const walletEntry = await this.get(walletAddress)
|
|
945
|
+
if (!walletEntry) {
|
|
946
|
+
throw new Error('wallet-not-found')
|
|
947
|
+
}
|
|
948
|
+
if (walletEntry.status !== 'ready') {
|
|
949
|
+
throw new Error('wallet-not-ready')
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const [signer] = await handler.completeAuth(commitment, args.code)
|
|
953
|
+
const signerKind = getSignerKindForSignup(commitment.kind)
|
|
954
|
+
|
|
955
|
+
await this.addLoginSignerFromPrepared(walletAddress, {
|
|
956
|
+
signer,
|
|
957
|
+
extra: { signerKind },
|
|
958
|
+
})
|
|
959
|
+
break
|
|
825
960
|
}
|
|
826
|
-
|
|
827
|
-
|
|
961
|
+
|
|
962
|
+
case 'auth': {
|
|
963
|
+
await this.signUp({
|
|
964
|
+
kind: commitment.kind,
|
|
965
|
+
commitment,
|
|
966
|
+
code: args.code,
|
|
967
|
+
noGuard: args.noGuard,
|
|
968
|
+
target: commitment.target,
|
|
969
|
+
isRedirect: true,
|
|
970
|
+
use4337: args.use4337,
|
|
971
|
+
})
|
|
972
|
+
break
|
|
828
973
|
}
|
|
829
974
|
|
|
830
|
-
|
|
975
|
+
case 'reauth': {
|
|
976
|
+
const handlerKind = getSignupHandlerKey(commitment.kind)
|
|
977
|
+
const handler = this.shared.handlers.get(handlerKind)
|
|
978
|
+
if (!handler) {
|
|
979
|
+
throw new Error('handler-not-registered')
|
|
980
|
+
}
|
|
981
|
+
if (!(handler instanceof AuthCodeHandler)) {
|
|
982
|
+
throw new Error('handler-does-not-support-redirect')
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
await handler.completeAuth(commitment, args.code)
|
|
986
|
+
break
|
|
987
|
+
}
|
|
831
988
|
}
|
|
832
989
|
|
|
833
990
|
if (!commitment.target) {
|
|
@@ -1263,6 +1420,87 @@ export class Wallets implements WalletsInterface {
|
|
|
1263
1420
|
})
|
|
1264
1421
|
}
|
|
1265
1422
|
|
|
1423
|
+
async addLoginSigner(args: AddLoginSignerArgs): Promise<string> {
|
|
1424
|
+
const walletEntry = await this.get(args.wallet)
|
|
1425
|
+
if (!walletEntry) {
|
|
1426
|
+
throw new Error('wallet-not-found')
|
|
1427
|
+
}
|
|
1428
|
+
if (walletEntry.status !== 'ready') {
|
|
1429
|
+
throw new Error('wallet-not-ready')
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
const signupArgs = addLoginSignerToSignupArgs(args)
|
|
1433
|
+
const loginSigner = await this.prepareSignUp(signupArgs)
|
|
1434
|
+
return this.addLoginSignerFromPrepared(args.wallet, loginSigner)
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
async completeAddLoginSigner(requestId: string): Promise<void> {
|
|
1438
|
+
const request = await this.shared.modules.signatures.get(requestId)
|
|
1439
|
+
if (request.action !== Actions.AddLoginSigner) {
|
|
1440
|
+
throw new Error('invalid-request-action')
|
|
1441
|
+
}
|
|
1442
|
+
await this.completeConfigurationUpdate(requestId)
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
async removeLoginSigner(args: RemoveLoginSignerArgs): Promise<string> {
|
|
1446
|
+
const walletEntry = await this.get(args.wallet)
|
|
1447
|
+
if (!walletEntry) {
|
|
1448
|
+
throw new Error('wallet-not-found')
|
|
1449
|
+
}
|
|
1450
|
+
if (walletEntry.status !== 'ready') {
|
|
1451
|
+
throw new Error('wallet-not-ready')
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
const { loginTopology, modules } = await this.getConfigurationParts(args.wallet)
|
|
1455
|
+
|
|
1456
|
+
const existingSigners = Config.getSigners(loginTopology)
|
|
1457
|
+
const allExistingAddresses = [...existingSigners.signers, ...existingSigners.sapientSigners.map((s) => s.address)]
|
|
1458
|
+
|
|
1459
|
+
if (!allExistingAddresses.some((addr) => Address.isEqual(addr, args.signerAddress))) {
|
|
1460
|
+
throw new Error('signer-not-found')
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
const remainingMembers = [
|
|
1464
|
+
...existingSigners.signers
|
|
1465
|
+
.filter((x) => x !== Constants.ZeroAddress && !Address.isEqual(x, args.signerAddress))
|
|
1466
|
+
.map((x) => ({ address: x })),
|
|
1467
|
+
...existingSigners.sapientSigners
|
|
1468
|
+
.filter((x) => !Address.isEqual(x.address, args.signerAddress))
|
|
1469
|
+
.map((x) => ({ address: x.address, imageHash: x.imageHash })),
|
|
1470
|
+
]
|
|
1471
|
+
|
|
1472
|
+
if (remainingMembers.length < 1) {
|
|
1473
|
+
throw new Error('cannot-remove-last-login-signer')
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
const nextLoginTopology = buildCappedTree(remainingMembers)
|
|
1477
|
+
|
|
1478
|
+
if (this.shared.modules.sessions.hasSessionModule(modules)) {
|
|
1479
|
+
await this.shared.modules.sessions.removeIdentitySignerFromModules(modules, args.signerAddress)
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
if (this.shared.modules.recovery.hasRecoveryModule(modules)) {
|
|
1483
|
+
await this.shared.modules.recovery.removeRecoverySignerFromModules(modules, args.signerAddress)
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
const requestId = await this.requestConfigurationUpdate(
|
|
1487
|
+
args.wallet,
|
|
1488
|
+
{ loginTopology: nextLoginTopology, modules },
|
|
1489
|
+
Actions.RemoveLoginSigner,
|
|
1490
|
+
'wallet-webapp',
|
|
1491
|
+
)
|
|
1492
|
+
|
|
1493
|
+
return requestId
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
async completeRemoveLoginSigner(requestId: string): Promise<void> {
|
|
1497
|
+
const request = await this.shared.modules.signatures.get(requestId)
|
|
1498
|
+
if (request.action !== Actions.RemoveLoginSigner) {
|
|
1499
|
+
throw new Error('invalid-request-action')
|
|
1500
|
+
}
|
|
1501
|
+
await this.completeConfigurationUpdate(requestId)
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1266
1504
|
async logout<T extends { skipRemoveDevice?: boolean } | undefined = undefined>(
|
|
1267
1505
|
wallet: Address.Address,
|
|
1268
1506
|
options?: T,
|
|
@@ -1493,4 +1731,54 @@ export class Wallets implements WalletsInterface {
|
|
|
1493
1731
|
|
|
1494
1732
|
return requestId
|
|
1495
1733
|
}
|
|
1734
|
+
|
|
1735
|
+
private async addLoginSignerFromPrepared(
|
|
1736
|
+
wallet: Address.Address,
|
|
1737
|
+
loginSigner: {
|
|
1738
|
+
signer: (Signers.Signer | Signers.SapientSigner) & Signers.Witnessable
|
|
1739
|
+
extra: WitnessExtraSignerKind
|
|
1740
|
+
},
|
|
1741
|
+
): Promise<string> {
|
|
1742
|
+
const newSignerAddress = await loginSigner.signer.address
|
|
1743
|
+
|
|
1744
|
+
const { loginTopology, modules } = await this.getConfigurationParts(wallet)
|
|
1745
|
+
|
|
1746
|
+
// Check for duplicate signer
|
|
1747
|
+
const existingSigners = Config.getSigners(loginTopology)
|
|
1748
|
+
const allExistingAddresses = [...existingSigners.signers, ...existingSigners.sapientSigners.map((s) => s.address)]
|
|
1749
|
+
if (allExistingAddresses.some((addr) => Address.isEqual(addr, newSignerAddress))) {
|
|
1750
|
+
throw new Error('signer-already-exists')
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
// Build new login topology with the additional signer
|
|
1754
|
+
const existingMembers = [
|
|
1755
|
+
...existingSigners.signers.filter((x) => x !== Constants.ZeroAddress).map((x) => ({ address: x })),
|
|
1756
|
+
...existingSigners.sapientSigners.map((x) => ({ address: x.address, imageHash: x.imageHash })),
|
|
1757
|
+
]
|
|
1758
|
+
const newMember = {
|
|
1759
|
+
address: newSignerAddress,
|
|
1760
|
+
imageHash: Signers.isSapientSigner(loginSigner.signer) ? await loginSigner.signer.imageHash : undefined,
|
|
1761
|
+
}
|
|
1762
|
+
const nextLoginTopology = buildCappedTree([...existingMembers, newMember])
|
|
1763
|
+
|
|
1764
|
+
// Add non-sapient login signer to sessions module identity signers
|
|
1765
|
+
if (!Signers.isSapientSigner(loginSigner.signer) && this.shared.modules.sessions.hasSessionModule(modules)) {
|
|
1766
|
+
await this.shared.modules.sessions.addIdentitySignerToModules(modules, newSignerAddress)
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// Add to recovery module if present
|
|
1770
|
+
if (this.shared.modules.recovery.hasRecoveryModule(modules)) {
|
|
1771
|
+
await this.shared.modules.recovery.addRecoverySignerToModules(modules, newSignerAddress)
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// Witness so the wallet becomes discoverable via the new credential
|
|
1775
|
+
await loginSigner.signer.witness(this.shared.sequence.stateProvider, wallet, loginSigner.extra)
|
|
1776
|
+
|
|
1777
|
+
return this.requestConfigurationUpdate(
|
|
1778
|
+
wallet,
|
|
1779
|
+
{ loginTopology: nextLoginTopology, modules },
|
|
1780
|
+
Actions.AddLoginSigner,
|
|
1781
|
+
'wallet-webapp',
|
|
1782
|
+
)
|
|
1783
|
+
}
|
|
1496
1784
|
}
|
|
@@ -90,9 +90,8 @@ describe('AuthCodePkceHandler', () => {
|
|
|
90
90
|
describe('commitAuth', () => {
|
|
91
91
|
it('Should create Google PKCE auth commitment and return OAuth URL', async () => {
|
|
92
92
|
const target = 'https://example.com/success'
|
|
93
|
-
const isSignUp = true
|
|
94
93
|
|
|
95
|
-
const result = await handler.commitAuth(target,
|
|
94
|
+
const result = await handler.commitAuth(target, { type: 'auth' })
|
|
96
95
|
|
|
97
96
|
// Verify nitroCommitVerifier was called with correct challenge
|
|
98
97
|
expect(handler['nitroCommitVerifier']).toHaveBeenCalledWith(
|
|
@@ -110,7 +109,7 @@ describe('AuthCodePkceHandler', () => {
|
|
|
110
109
|
challenge: 'mock-challenge-hash',
|
|
111
110
|
target,
|
|
112
111
|
metadata: {},
|
|
113
|
-
|
|
112
|
+
type: 'auth',
|
|
114
113
|
})
|
|
115
114
|
|
|
116
115
|
// Verify OAuth URL is constructed correctly
|
|
@@ -127,10 +126,13 @@ describe('AuthCodePkceHandler', () => {
|
|
|
127
126
|
|
|
128
127
|
it('Should use provided state instead of generating random one', async () => {
|
|
129
128
|
const target = 'https://example.com/success'
|
|
130
|
-
const isSignUp = false
|
|
131
129
|
const customState = 'custom-state-123'
|
|
132
130
|
|
|
133
|
-
const result = await handler.commitAuth(target,
|
|
131
|
+
const result = await handler.commitAuth(target, {
|
|
132
|
+
type: 'reauth',
|
|
133
|
+
state: customState,
|
|
134
|
+
signer: '0x1234567890123456789012345678901234567890',
|
|
135
|
+
})
|
|
134
136
|
|
|
135
137
|
// Verify commitment was saved with custom state
|
|
136
138
|
expect(mockCommitments.set).toHaveBeenCalledWith({
|
|
@@ -140,7 +142,8 @@ describe('AuthCodePkceHandler', () => {
|
|
|
140
142
|
challenge: 'mock-challenge-hash',
|
|
141
143
|
target,
|
|
142
144
|
metadata: {},
|
|
143
|
-
|
|
145
|
+
type: 'reauth',
|
|
146
|
+
signer: '0x1234567890123456789012345678901234567890',
|
|
144
147
|
})
|
|
145
148
|
|
|
146
149
|
// Verify URL contains custom state
|
|
@@ -149,10 +152,9 @@ describe('AuthCodePkceHandler', () => {
|
|
|
149
152
|
|
|
150
153
|
it('Should include signer in challenge when provided', async () => {
|
|
151
154
|
const target = 'https://example.com/success'
|
|
152
|
-
const isSignUp = true
|
|
153
155
|
const signer = '0x9876543210987654321098765432109876543210'
|
|
154
156
|
|
|
155
|
-
await handler.commitAuth(target,
|
|
157
|
+
await handler.commitAuth(target, { type: 'reauth', state: 'test-state', signer })
|
|
156
158
|
|
|
157
159
|
// Verify nitroCommitVerifier was called with signer in challenge
|
|
158
160
|
expect(handler['nitroCommitVerifier']).toHaveBeenCalledWith(
|
|
@@ -164,9 +166,8 @@ describe('AuthCodePkceHandler', () => {
|
|
|
164
166
|
|
|
165
167
|
it('Should generate random state when not provided', async () => {
|
|
166
168
|
const target = 'https://example.com/success'
|
|
167
|
-
const isSignUp = true
|
|
168
169
|
|
|
169
|
-
const result = await handler.commitAuth(target,
|
|
170
|
+
const result = await handler.commitAuth(target, { type: 'auth' })
|
|
170
171
|
|
|
171
172
|
// Verify that a state parameter is present and looks like a hex string
|
|
172
173
|
expect(result).toMatch(/state=0x[a-f0-9]+/)
|
|
@@ -181,18 +182,22 @@ describe('AuthCodePkceHandler', () => {
|
|
|
181
182
|
const target = 'https://example.com/success'
|
|
182
183
|
|
|
183
184
|
// Test signup
|
|
184
|
-
await handler.commitAuth(target,
|
|
185
|
+
await handler.commitAuth(target, { type: 'auth' })
|
|
185
186
|
expect(mockCommitments.set).toHaveBeenLastCalledWith(
|
|
186
187
|
expect.objectContaining({
|
|
187
|
-
|
|
188
|
+
type: 'auth',
|
|
188
189
|
}),
|
|
189
190
|
)
|
|
190
191
|
|
|
191
192
|
// Test login
|
|
192
|
-
await handler.commitAuth(target,
|
|
193
|
+
await handler.commitAuth(target, {
|
|
194
|
+
type: 'reauth',
|
|
195
|
+
state: 'test-state',
|
|
196
|
+
signer: '0x1234567890123456789012345678901234567890',
|
|
197
|
+
})
|
|
193
198
|
expect(mockCommitments.set).toHaveBeenLastCalledWith(
|
|
194
199
|
expect.objectContaining({
|
|
195
|
-
|
|
200
|
+
type: 'reauth',
|
|
196
201
|
}),
|
|
197
202
|
)
|
|
198
203
|
})
|
|
@@ -200,13 +205,17 @@ describe('AuthCodePkceHandler', () => {
|
|
|
200
205
|
it('Should handle errors from nitroCommitVerifier', async () => {
|
|
201
206
|
vi.spyOn(handler as any, 'nitroCommitVerifier').mockRejectedValue(new Error('Nitro service unavailable'))
|
|
202
207
|
|
|
203
|
-
await expect(handler.commitAuth('https://example.com/success',
|
|
208
|
+
await expect(handler.commitAuth('https://example.com/success', { type: 'auth' })).rejects.toThrow(
|
|
209
|
+
'Nitro service unavailable',
|
|
210
|
+
)
|
|
204
211
|
})
|
|
205
212
|
|
|
206
213
|
it('Should handle database errors during commitment storage', async () => {
|
|
207
214
|
vi.mocked(mockCommitments.set).mockRejectedValue(new Error('Database write failed'))
|
|
208
215
|
|
|
209
|
-
await expect(handler.commitAuth('https://example.com/success',
|
|
216
|
+
await expect(handler.commitAuth('https://example.com/success', { type: 'auth' })).rejects.toThrow(
|
|
217
|
+
'Database write failed',
|
|
218
|
+
)
|
|
210
219
|
})
|
|
211
220
|
})
|
|
212
221
|
|
|
@@ -221,7 +230,7 @@ describe('AuthCodePkceHandler', () => {
|
|
|
221
230
|
challenge: 'test-challenge-hash',
|
|
222
231
|
target: 'https://example.com/success',
|
|
223
232
|
metadata: { scope: 'openid profile email' },
|
|
224
|
-
|
|
233
|
+
type: 'auth',
|
|
225
234
|
}
|
|
226
235
|
})
|
|
227
236
|
|
|
@@ -333,7 +342,7 @@ describe('AuthCodePkceHandler', () => {
|
|
|
333
342
|
const newRedirectUri = 'https://newdomain.com/callback'
|
|
334
343
|
handler.setRedirectUri(newRedirectUri)
|
|
335
344
|
|
|
336
|
-
return handler.commitAuth('https://example.com/success',
|
|
345
|
+
return handler.commitAuth('https://example.com/success', { type: 'auth' }).then((result) => {
|
|
337
346
|
expect(result).toContain(`redirect_uri=${encodeURIComponent(newRedirectUri)}`)
|
|
338
347
|
})
|
|
339
348
|
})
|