@@ -106,8 +106,11 @@ import type {
106
106
JWK ,
107
107
JwtPayload ,
108
108
JwtHeader ,
109
+ SolanaWeb3Credentials ,
110
+ SolanaWallet ,
111
+ Web3Credentials ,
109
112
} from './lib/types'
110
- import { stringToUint8Array } from './lib/base64url'
113
+ import { stringToUint8Array , bytesToBase64URL } from './lib/base64url'
111
114
112
115
polyfillGlobalThis ( ) // Make "globalThis" available
113
116
@@ -601,6 +604,214 @@ export default class GoTrueClient {
601
604
} )
602
605
}
603
606
607
+ /**
608
+ * Signs in a user by verifying a message signed by the user's private key.
609
+ * Only Solana supported at this time, using the Sign in with Solana standard.
610
+ */
611
+ async signInWithWeb3 ( credentials : Web3Credentials ) : Promise <
612
+ | {
613
+ data : { session : Session ; user : User }
614
+ error : null
615
+ }
616
+ | { data : { session : null ; user : null } ; error : AuthError }
617
+ > {
618
+ const { chain } = credentials
619
+
620
+ if ( chain === 'solana' ) {
621
+ return await this . signInWithSolana ( credentials )
622
+ }
623
+
624
+ throw new Error ( `@supabase/auth-js: Unsupported chain "${ chain } "` )
625
+ }
626
+
627
+ private async signInWithSolana ( credentials : SolanaWeb3Credentials ) {
628
+ let message : string
629
+ let signature : Uint8Array
630
+
631
+ if ( 'message' in credentials ) {
632
+ message = credentials . message
633
+ signature = credentials . signature
634
+ } else {
635
+ const { chain, wallet, statement, options } = credentials
636
+
637
+ let resolvedWallet : SolanaWallet
638
+
639
+ if ( ! isBrowser ( ) ) {
640
+ if ( typeof wallet !== 'object' || ! options ?. url ) {
641
+ throw new Error (
642
+ '@supabase/auth-js: Both wallet and url must be specified in non-browser environments.'
643
+ )
644
+ }
645
+
646
+ resolvedWallet = wallet
647
+ } else if ( typeof wallet === 'object' ) {
648
+ resolvedWallet = wallet
649
+ } else {
650
+ const windowAny = window as any
651
+
652
+ if (
653
+ 'solana' in windowAny &&
654
+ typeof windowAny . solana === 'object' &&
655
+ ( ( 'signIn' in windowAny . solana && typeof windowAny . solana . signIn === 'function' ) ||
656
+ ( 'signMessage' in windowAny . solana &&
657
+ typeof windowAny . solana . signMessage === 'function' ) )
658
+ ) {
659
+ resolvedWallet = windowAny . solana
660
+ } else {
661
+ throw new Error (
662
+ `@supabase/auth-js: No compatible Solana wallet interface on the window object (window.solana) detected. Make sure the user already has a wallet installed and connected for this app. Prefer passing the wallet interface object directly to signInWithWeb3({ chain: 'solana', wallet: resolvedUserWallet }) instead.`
663
+ )
664
+ }
665
+ }
666
+
667
+ const url = new URL ( options ?. url ?? window . location . href )
668
+
669
+ if ( 'signIn' in resolvedWallet && resolvedWallet . signIn ) {
670
+ const output = await resolvedWallet . signIn ( {
671
+ issuedAt : new Date ( ) . toISOString ( ) ,
672
+
673
+ ...options ?. signInWithSolana ,
674
+
675
+ // non-overridable properties
676
+ version : '1' ,
677
+ domain : url . host ,
678
+ uri : url . href ,
679
+
680
+ ...( statement ? { statement } : null ) ,
681
+ } )
682
+
683
+ let outputToProcess : any
684
+
685
+ if ( Array . isArray ( output ) && output [ 0 ] && typeof output [ 0 ] === 'object' ) {
686
+ outputToProcess = output [ 0 ]
687
+ } else if (
688
+ output &&
689
+ typeof output === 'object' &&
690
+ 'signedMessage' in output &&
691
+ 'signature' in output
692
+ ) {
693
+ outputToProcess = output
694
+ } else {
695
+ throw new Error ( '@supabase/auth-js: Wallet method signIn() returned unrecognized value' )
696
+ }
697
+
698
+ if (
699
+ 'signedMessage' in outputToProcess &&
700
+ 'signature' in outputToProcess &&
701
+ ( typeof outputToProcess . signedMessage === 'string' ||
702
+ outputToProcess . signedMessage instanceof Uint8Array ) &&
703
+ outputToProcess . signature instanceof Uint8Array
704
+ ) {
705
+ message =
706
+ typeof outputToProcess . signedMessage === 'string'
707
+ ? outputToProcess . signedMessage
708
+ : new TextDecoder ( ) . decode ( outputToProcess . signedMessage )
709
+ signature = outputToProcess . signature
710
+ } else {
711
+ throw new Error (
712
+ '@supabase/auth-js: Wallet method signIn() API returned object without signedMessage and signature fields'
713
+ )
714
+ }
715
+ } else {
716
+ if (
717
+ ! ( 'signMessage' in resolvedWallet ) ||
718
+ typeof resolvedWallet . signMessage !== 'function' ||
719
+ ! ( 'publicKey' in resolvedWallet ) ||
720
+ typeof resolvedWallet !== 'object' ||
721
+ ! resolvedWallet . publicKey ||
722
+ ! ( 'toBase58' in resolvedWallet . publicKey ) ||
723
+ typeof resolvedWallet . publicKey . toBase58 !== 'function'
724
+ ) {
725
+ throw new Error (
726
+ '@supabase/auth-js: Wallet does not have a compatible signMessage() and publicKey.toBase58() API'
727
+ )
728
+ }
729
+
730
+ message = [
731
+ `${ url . host } wants you to sign in with your Solana account:` ,
732
+ resolvedWallet . publicKey . toBase58 ( ) ,
733
+ ...( statement ? [ '' , statement , '' ] : [ '' ] ) ,
734
+ 'Version: 1' ,
735
+ `URI: ${ url . href } ` ,
736
+ `Issued At: ${ options ?. signInWithSolana ?. issuedAt ?? new Date ( ) . toISOString ( ) } ` ,
737
+ ...( options ?. signInWithSolana ?. notBefore
738
+ ? [ `Not Before: ${ options . signInWithSolana . notBefore } ` ]
739
+ : [ ] ) ,
740
+ ...( options ?. signInWithSolana ?. expirationTime
741
+ ? [ `Expiration Time: ${ options . signInWithSolana . expirationTime } ` ]
742
+ : [ ] ) ,
743
+ ...( options ?. signInWithSolana ?. chainId
744
+ ? [ `Chain ID: ${ options . signInWithSolana . chainId } ` ]
745
+ : [ ] ) ,
746
+ ...( options ?. signInWithSolana ?. nonce ? [ `Nonce: ${ options . signInWithSolana . nonce } ` ] : [ ] ) ,
747
+ ...( options ?. signInWithSolana ?. requestId
748
+ ? [ `Request ID: ${ options . signInWithSolana . requestId } ` ]
749
+ : [ ] ) ,
750
+ ...( options ?. signInWithSolana ?. resources ?. length
751
+ ? [
752
+ 'Resources' ,
753
+ ...options . signInWithSolana . resources . map ( ( resource ) => `- ${ resource } ` ) ,
754
+ ]
755
+ : [ ] ) ,
756
+ ] . join ( '\n' )
757
+
758
+ const maybeSignature = await resolvedWallet . signMessage (
759
+ new TextEncoder ( ) . encode ( message ) ,
760
+ 'utf8'
761
+ )
762
+
763
+ if ( ! maybeSignature || ! ( maybeSignature instanceof Uint8Array ) ) {
764
+ throw new Error (
765
+ '@supabase/auth-js: Wallet signMessage() API returned an recognized value'
766
+ )
767
+ }
768
+
769
+ signature = maybeSignature
770
+ }
771
+ }
772
+
773
+ try {
774
+ const { data, error } = await _request (
775
+ this . fetch ,
776
+ 'POST' ,
777
+ `${ this . url } /token?grant_type=web3` ,
778
+ {
779
+ headers : this . headers ,
780
+ body : {
781
+ chain : 'solana' ,
782
+ message,
783
+ signature : bytesToBase64URL ( signature ) ,
784
+
785
+ ...( credentials . options ?. captchaToken
786
+ ? { gotrue_meta_security : { captcha_token : credentials . options ?. captchaToken } }
787
+ : null ) ,
788
+ } ,
789
+ xform : _sessionResponse ,
790
+ }
791
+ )
792
+ if ( error ) {
793
+ throw error
794
+ }
795
+ if ( ! data || ! data . session || ! data . user ) {
796
+ return {
797
+ data : { user : null , session : null } ,
798
+ error : new AuthInvalidTokenResponseError ( ) ,
799
+ }
800
+ }
801
+ if ( data . session ) {
802
+ await this . _saveSession ( data . session )
803
+ await this . _notifyAllSubscribers ( 'SIGNED_IN' , data . session )
804
+ }
805
+ return { data : { ...data } , error }
806
+ } catch ( error ) {
807
+ if ( isAuthError ( error ) ) {
808
+ return { data : { user : null , session : null } , error }
809
+ }
810
+
811
+ throw error
812
+ }
813
+ }
814
+
604
815
private async _exchangeCodeForSession ( authCode : string ) : Promise <
605
816
| {
606
817
data : { session : Session ; user : User ; redirectType : string | null }
0 commit comments