@phantom/react-native-sdk 1.0.3 → 1.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/dist/index.d.ts CHANGED
@@ -25,6 +25,11 @@ interface PhantomSDKConfig extends Omit<EmbeddedProviderConfig, "apiBaseUrl" | "
25
25
  authUrl?: string;
26
26
  redirectUrl?: string;
27
27
  };
28
+ /** When also provided, the Auth2 PKCE flow is used instead of the legacy Phantom Connect flow. */
29
+ unstable__auth2Options?: {
30
+ authApiBaseUrl: string;
31
+ clientId: string;
32
+ };
28
33
  }
29
34
  interface ConnectOptions {
30
35
  /** OAuth provider to use (required) */
package/dist/index.js CHANGED
@@ -557,7 +557,7 @@ var ExpoAuthProvider = class {
557
557
  // OAuth session management - defaults to allow refresh unless explicitly clearing after logout
558
558
  clear_previous_session: (phantomOptions.clearPreviousSession ?? false).toString(),
559
559
  allow_refresh: (phantomOptions.allowRefresh ?? true).toString(),
560
- sdk_version: "1.0.3",
560
+ sdk_version: "1.0.5",
561
561
  sdk_type: "react-native",
562
562
  platform: import_react_native5.Platform.OS
563
563
  });
@@ -632,6 +632,243 @@ var ExpoAuthProvider = class {
632
632
  }
633
633
  };
634
634
 
635
+ // src/providers/embedded/ExpoAuth2AuthProvider.ts
636
+ var WebBrowser2 = __toESM(require("expo-web-browser"));
637
+ var import_auth2 = require("@phantom/auth2");
638
+ var ExpoAuth2AuthProvider = class {
639
+ constructor(stamper, auth2ProviderOptions, kmsClientOptions) {
640
+ this.stamper = stamper;
641
+ this.auth2ProviderOptions = auth2ProviderOptions;
642
+ this.kms = new import_auth2.Auth2KmsRpcClient(stamper, kmsClientOptions);
643
+ }
644
+ /**
645
+ * Runs the full PKCE Auth2 flow inline using expo-web-browser.
646
+ *
647
+ * Unlike the browser flow (which requires a page redirect and resumeAuthFromRedirect),
648
+ * expo-web-browser intercepts the OAuth callback URL and returns it synchronously,
649
+ * so the token exchange and KMS calls all happen here before returning AuthResult.
650
+ */
651
+ async authenticate(options) {
652
+ if (!this.stamper.getKeyInfo()) {
653
+ await this.stamper.init();
654
+ }
655
+ const keyPair = this.stamper.getCryptoKeyPair();
656
+ if (!keyPair) {
657
+ throw new Error("Stamper key pair not found.");
658
+ }
659
+ const codeVerifier = (0, import_auth2.createCodeVerifier)();
660
+ const url = await (0, import_auth2.createConnectStartUrl)({
661
+ keyPair,
662
+ connectLoginUrl: this.auth2ProviderOptions.connectLoginUrl,
663
+ clientId: this.auth2ProviderOptions.clientId,
664
+ redirectUri: this.auth2ProviderOptions.redirectUri,
665
+ sessionId: options.sessionId,
666
+ provider: options.provider,
667
+ codeVerifier,
668
+ // The P-256 ephemeral key is unique per wallet, so no additional salt is needed.
669
+ salt: ""
670
+ });
671
+ await WebBrowser2.warmUpAsync();
672
+ let result;
673
+ try {
674
+ result = await WebBrowser2.openAuthSessionAsync(url, this.auth2ProviderOptions.redirectUri);
675
+ } finally {
676
+ await WebBrowser2.coolDownAsync();
677
+ }
678
+ if (!result.url) {
679
+ throw new Error("Authentication failed");
680
+ }
681
+ const callbackUrl = new URL(result.url);
682
+ const state = callbackUrl.searchParams.get("state");
683
+ if (state && state !== options.sessionId) {
684
+ throw new Error("Auth2 state mismatch \u2014 possible CSRF attack.");
685
+ }
686
+ const error = callbackUrl.searchParams.get("error");
687
+ if (error) {
688
+ const description = callbackUrl.searchParams.get("error_description");
689
+ throw new Error(`Auth2 callback error: ${description ?? error}`);
690
+ }
691
+ const code = callbackUrl.searchParams.get("code");
692
+ if (!code) {
693
+ throw new Error("Auth2 callback missing authorization code");
694
+ }
695
+ const { idToken, bearerToken, authUserId, expiresInMs } = await (0, import_auth2.exchangeAuthCode)({
696
+ authApiBaseUrl: this.auth2ProviderOptions.authApiBaseUrl,
697
+ clientId: this.auth2ProviderOptions.clientId,
698
+ redirectUri: this.auth2ProviderOptions.redirectUri,
699
+ code,
700
+ codeVerifier
701
+ });
702
+ await this.stamper.setIdToken(idToken);
703
+ const { organizationId, walletId } = await this.kms.discoverOrganizationAndWalletId(bearerToken, authUserId);
704
+ return {
705
+ walletId,
706
+ organizationId,
707
+ provider: options.provider,
708
+ accountDerivationIndex: 0,
709
+ // discoverWalletId uses derivation index of 0.
710
+ expiresInMs,
711
+ authUserId,
712
+ bearerToken
713
+ };
714
+ }
715
+ };
716
+
717
+ // src/providers/embedded/ExpoAuth2Stamper.ts
718
+ var SecureStore2 = __toESM(require("expo-secure-store"));
719
+ var import_bs58 = __toESM(require("bs58"));
720
+ var import_buffer = require("buffer");
721
+ var import_base64url = require("@phantom/base64url");
722
+ var import_sdk_types = require("@phantom/sdk-types");
723
+ var ExpoAuth2Stamper = class {
724
+ /**
725
+ * @param storageKey - expo-secure-store key used to persist the P-256 private key.
726
+ * Use a unique key per app, e.g. `phantom-auth2-<appId>`.
727
+ */
728
+ constructor(storageKey) {
729
+ this.storageKey = storageKey;
730
+ this._keyPair = null;
731
+ this._keyInfo = null;
732
+ this._idToken = null;
733
+ this.algorithm = import_sdk_types.Algorithm.secp256r1;
734
+ this.type = "OIDC";
735
+ }
736
+ async init() {
737
+ const stored = await this.loadRecord();
738
+ if (stored) {
739
+ this._keyPair = {
740
+ privateKey: await this.importPrivateKey(stored.privateKeyPkcs8),
741
+ publicKey: await this.importPublicKeyFromBase58(stored.keyInfo.publicKey)
742
+ };
743
+ this._keyInfo = stored.keyInfo;
744
+ if (stored.idToken) {
745
+ this._idToken = stored.idToken;
746
+ }
747
+ return this._keyInfo;
748
+ }
749
+ return this.generateAndStore();
750
+ }
751
+ getKeyInfo() {
752
+ return this._keyInfo;
753
+ }
754
+ getCryptoKeyPair() {
755
+ return this._keyPair;
756
+ }
757
+ /**
758
+ * Arms the stamper with the OIDC id token for subsequent KMS stamp() calls.
759
+ *
760
+ * Persists the token to SecureStore alongside the key pair so that
761
+ * auto-connect can restore it on the next app launch without a new login.
762
+ */
763
+ async setIdToken(idToken) {
764
+ this._idToken = idToken;
765
+ const existing = await this.loadRecord();
766
+ if (existing) {
767
+ await this.storeRecord({ ...existing, idToken });
768
+ }
769
+ }
770
+ async stamp(params) {
771
+ if (!this._keyPair || !this._keyInfo || this._idToken === null) {
772
+ throw new Error("ExpoAuth2Stamper not initialized. Call init() first.");
773
+ }
774
+ const signatureRaw = await crypto.subtle.sign(
775
+ { name: "ECDSA", hash: "SHA-256" },
776
+ this._keyPair.privateKey,
777
+ new Uint8Array(params.data)
778
+ );
779
+ const rawPublicKey = import_bs58.default.decode(this._keyInfo.publicKey);
780
+ const stampData = {
781
+ kind: this.type,
782
+ idToken: this._idToken,
783
+ publicKey: (0, import_base64url.base64urlEncode)(rawPublicKey),
784
+ algorithm: this.algorithm,
785
+ // The P-256 ephemeral key is unique per wallet, so no additional salt is needed.
786
+ salt: "",
787
+ signature: (0, import_base64url.base64urlEncode)(new Uint8Array(signatureRaw))
788
+ };
789
+ return (0, import_base64url.base64urlEncode)(new TextEncoder().encode(JSON.stringify(stampData)));
790
+ }
791
+ async resetKeyPair() {
792
+ await this.clear();
793
+ return this.generateAndStore();
794
+ }
795
+ async clear() {
796
+ await this.clearStoredRecord();
797
+ this._keyPair = null;
798
+ this._keyInfo = null;
799
+ this._idToken = null;
800
+ }
801
+ // Auth2 doesn't use key rotation; minimal no-op implementations.
802
+ async rotateKeyPair() {
803
+ return this.init();
804
+ }
805
+ // eslint-disable-next-line @typescript-eslint/require-await
806
+ async commitRotation(authenticatorId) {
807
+ if (this._keyInfo) {
808
+ this._keyInfo.authenticatorId = authenticatorId;
809
+ }
810
+ }
811
+ async rollbackRotation() {
812
+ }
813
+ async generateAndStore() {
814
+ const keyPair = await crypto.subtle.generateKey(
815
+ { name: "ECDSA", namedCurve: "P-256" },
816
+ true,
817
+ // extractable — needed to export PKCS#8 for SecureStore
818
+ ["sign", "verify"]
819
+ );
820
+ const rawPublicKey = new Uint8Array(await crypto.subtle.exportKey("raw", keyPair.publicKey));
821
+ const publicKeyBase58 = import_bs58.default.encode(rawPublicKey);
822
+ const keyIdBuffer = await crypto.subtle.digest("SHA-256", rawPublicKey.buffer);
823
+ const keyId = (0, import_base64url.base64urlEncode)(new Uint8Array(keyIdBuffer)).substring(0, 16);
824
+ this._keyPair = keyPair;
825
+ this._keyInfo = { keyId, publicKey: publicKeyBase58, createdAt: Date.now() };
826
+ const pkcs8Buffer = await crypto.subtle.exportKey("pkcs8", keyPair.privateKey);
827
+ const privateKeyPkcs8 = import_buffer.Buffer.from(pkcs8Buffer).toString("base64");
828
+ await this.storeRecord({ privateKeyPkcs8, keyInfo: this._keyInfo });
829
+ return this._keyInfo;
830
+ }
831
+ async importPublicKeyFromBase58(base58PublicKey) {
832
+ const rawBytes = import_bs58.default.decode(base58PublicKey);
833
+ return crypto.subtle.importKey(
834
+ "raw",
835
+ rawBytes.buffer.slice(rawBytes.byteOffset, rawBytes.byteOffset + rawBytes.byteLength),
836
+ { name: "ECDSA", namedCurve: "P-256" },
837
+ true,
838
+ // extractable so createAuth2RequestJar can export it as JWK
839
+ ["verify"]
840
+ );
841
+ }
842
+ async importPrivateKey(pkcs8Base64) {
843
+ const pkcs8Bytes = import_buffer.Buffer.from(pkcs8Base64, "base64");
844
+ return crypto.subtle.importKey(
845
+ "pkcs8",
846
+ pkcs8Bytes,
847
+ { name: "ECDSA", namedCurve: "P-256" },
848
+ false,
849
+ // non-extractable once loaded into memory
850
+ ["sign"]
851
+ );
852
+ }
853
+ async loadRecord() {
854
+ try {
855
+ const raw = await SecureStore2.getItemAsync(this.storageKey);
856
+ return raw ? JSON.parse(raw) : null;
857
+ } catch {
858
+ return null;
859
+ }
860
+ }
861
+ async storeRecord(record) {
862
+ await SecureStore2.setItemAsync(this.storageKey, JSON.stringify(record), { requireAuthentication: false });
863
+ }
864
+ async clearStoredRecord() {
865
+ try {
866
+ await SecureStore2.deleteItemAsync(this.storageKey);
867
+ } catch {
868
+ }
869
+ }
870
+ };
871
+
635
872
  // src/providers/embedded/url-params.ts
636
873
  var import_react_native6 = require("react-native");
637
874
  var ExpoURLParamsAccessor = class {
@@ -701,11 +938,11 @@ var ExpoURLParamsAccessor = class {
701
938
  };
702
939
 
703
940
  // src/providers/embedded/stamper.ts
704
- var SecureStore2 = __toESM(require("expo-secure-store"));
941
+ var SecureStore3 = __toESM(require("expo-secure-store"));
705
942
  var import_api_key_stamper = require("@phantom/api-key-stamper");
706
943
  var import_constants2 = require("@phantom/constants");
707
944
  var import_crypto = require("@phantom/crypto");
708
- var import_base64url = require("@phantom/base64url");
945
+ var import_base64url2 = require("@phantom/base64url");
709
946
  var ReactNativeStamper = class {
710
947
  // Optional for PKI, required for OIDC
711
948
  constructor(config = {}) {
@@ -761,11 +998,11 @@ var ReactNativeStamper = class {
761
998
  const activeKey = this.getActiveKeyName();
762
999
  const pendingKey = this.getPendingKeyName();
763
1000
  try {
764
- await SecureStore2.deleteItemAsync(activeKey);
1001
+ await SecureStore3.deleteItemAsync(activeKey);
765
1002
  } catch (error) {
766
1003
  }
767
1004
  try {
768
- await SecureStore2.deleteItemAsync(pendingKey);
1005
+ await SecureStore3.deleteItemAsync(pendingKey);
769
1006
  } catch (error) {
770
1007
  }
771
1008
  this.activeKeyRecord = null;
@@ -787,7 +1024,7 @@ var ReactNativeStamper = class {
787
1024
  }
788
1025
  if (this.activeKeyRecord) {
789
1026
  try {
790
- await SecureStore2.deleteItemAsync(this.getActiveKeyName());
1027
+ await SecureStore3.deleteItemAsync(this.getActiveKeyName());
791
1028
  } catch (error) {
792
1029
  }
793
1030
  }
@@ -798,7 +1035,7 @@ var ReactNativeStamper = class {
798
1035
  this.pendingKeyRecord = null;
799
1036
  await this.storeKeyRecord(this.activeKeyRecord, "active");
800
1037
  try {
801
- await SecureStore2.deleteItemAsync(this.getPendingKeyName());
1038
+ await SecureStore3.deleteItemAsync(this.getPendingKeyName());
802
1039
  } catch (error) {
803
1040
  }
804
1041
  }
@@ -810,7 +1047,7 @@ var ReactNativeStamper = class {
810
1047
  return;
811
1048
  }
812
1049
  try {
813
- await SecureStore2.deleteItemAsync(this.getPendingKeyName());
1050
+ await SecureStore3.deleteItemAsync(this.getPendingKeyName());
814
1051
  } catch (error) {
815
1052
  }
816
1053
  this.pendingKeyRecord = null;
@@ -836,18 +1073,18 @@ var ReactNativeStamper = class {
836
1073
  return record;
837
1074
  }
838
1075
  createKeyId(publicKey) {
839
- return (0, import_base64url.base64urlEncode)(new TextEncoder().encode(publicKey)).substring(0, 16);
1076
+ return (0, import_base64url2.base64urlEncode)(new TextEncoder().encode(publicKey)).substring(0, 16);
840
1077
  }
841
1078
  async storeKeyRecord(record, type) {
842
1079
  const keyName = type === "active" ? this.getActiveKeyName() : this.getPendingKeyName();
843
- await SecureStore2.setItemAsync(keyName, JSON.stringify(record), {
1080
+ await SecureStore3.setItemAsync(keyName, JSON.stringify(record), {
844
1081
  requireAuthentication: false
845
1082
  });
846
1083
  }
847
1084
  async loadActiveKeyRecord() {
848
1085
  try {
849
1086
  const activeKey = this.getActiveKeyName();
850
- const storedRecord = await SecureStore2.getItemAsync(activeKey);
1087
+ const storedRecord = await SecureStore3.getItemAsync(activeKey);
851
1088
  if (storedRecord) {
852
1089
  return JSON.parse(storedRecord);
853
1090
  }
@@ -858,7 +1095,7 @@ var ReactNativeStamper = class {
858
1095
  async loadPendingKeyRecord() {
859
1096
  try {
860
1097
  const pendingKey = this.getPendingKeyName();
861
- const storedRecord = await SecureStore2.getItemAsync(pendingKey);
1098
+ const storedRecord = await SecureStore3.getItemAsync(pendingKey);
862
1099
  if (storedRecord) {
863
1100
  return JSON.parse(storedRecord);
864
1101
  }
@@ -940,13 +1177,25 @@ function PhantomProvider({ children, config, debugConfig, theme, appIcon, appNam
940
1177
  }, [config]);
941
1178
  const sdk = (0, import_react8.useMemo)(() => {
942
1179
  const storage = new ExpoSecureStorage();
943
- const authProvider = new ExpoAuthProvider();
944
1180
  const urlParamsAccessor = new ExpoURLParamsAccessor();
945
1181
  const logger = new ExpoLogger(debugConfig?.enabled || false);
946
- const stamper = new ReactNativeStamper({
1182
+ const stamper = config.unstable__auth2Options ? new ExpoAuth2Stamper(`phantom-auth2-${memoizedConfig.appId}`) : new ReactNativeStamper({
947
1183
  keyPrefix: `phantom-rn-${memoizedConfig.appId}`,
948
1184
  appId: memoizedConfig.appId
949
1185
  });
1186
+ const authProvider = config.unstable__auth2Options && config.authOptions?.authUrl && config.authOptions?.redirectUrl && config.apiBaseUrl && stamper instanceof ExpoAuth2Stamper ? new ExpoAuth2AuthProvider(
1187
+ stamper,
1188
+ {
1189
+ redirectUri: config.authOptions.redirectUrl,
1190
+ connectLoginUrl: config.authOptions.authUrl,
1191
+ clientId: config.unstable__auth2Options.clientId,
1192
+ authApiBaseUrl: config.unstable__auth2Options.authApiBaseUrl
1193
+ },
1194
+ {
1195
+ apiBaseUrl: config.apiBaseUrl,
1196
+ appId: config.appId
1197
+ }
1198
+ ) : new ExpoAuthProvider();
950
1199
  const platformName = `${import_react_native7.Platform.OS}-${import_react_native7.Platform.Version}`;
951
1200
  const platform = {
952
1201
  storage,
@@ -957,16 +1206,17 @@ function PhantomProvider({ children, config, debugConfig, theme, appIcon, appNam
957
1206
  name: platformName,
958
1207
  analyticsHeaders: {
959
1208
  [import_constants3.ANALYTICS_HEADERS.SDK_TYPE]: "react-native",
960
- [import_constants3.ANALYTICS_HEADERS.PLATFORM]: import_react_native7.Platform.OS,
1209
+ [import_constants3.ANALYTICS_HEADERS.PLATFORM]: "ext-sdk",
961
1210
  [import_constants3.ANALYTICS_HEADERS.PLATFORM_VERSION]: `${import_react_native7.Platform.Version}`,
1211
+ [import_constants3.ANALYTICS_HEADERS.CLIENT]: import_react_native7.Platform.OS,
962
1212
  [import_constants3.ANALYTICS_HEADERS.APP_ID]: config.appId,
963
1213
  [import_constants3.ANALYTICS_HEADERS.WALLET_TYPE]: config.embeddedWalletType,
964
- [import_constants3.ANALYTICS_HEADERS.SDK_VERSION]: "1.0.3"
1214
+ [import_constants3.ANALYTICS_HEADERS.SDK_VERSION]: "1.0.5"
965
1215
  // Replaced at build time
966
1216
  }
967
1217
  };
968
1218
  return new import_embedded_provider_core.EmbeddedProvider(memoizedConfig, platform, logger);
969
- }, [memoizedConfig, debugConfig, config.appId, config.embeddedWalletType]);
1219
+ }, [memoizedConfig, debugConfig, config]);
970
1220
  (0, import_react8.useEffect)(() => {
971
1221
  const handleConnectStart = () => {
972
1222
  setIsConnecting(true);
package/dist/index.mjs CHANGED
@@ -515,7 +515,7 @@ var ExpoAuthProvider = class {
515
515
  // OAuth session management - defaults to allow refresh unless explicitly clearing after logout
516
516
  clear_previous_session: (phantomOptions.clearPreviousSession ?? false).toString(),
517
517
  allow_refresh: (phantomOptions.allowRefresh ?? true).toString(),
518
- sdk_version: "1.0.3",
518
+ sdk_version: "1.0.5",
519
519
  sdk_type: "react-native",
520
520
  platform: Platform.OS
521
521
  });
@@ -590,6 +590,248 @@ var ExpoAuthProvider = class {
590
590
  }
591
591
  };
592
592
 
593
+ // src/providers/embedded/ExpoAuth2AuthProvider.ts
594
+ import * as WebBrowser2 from "expo-web-browser";
595
+ import {
596
+ createCodeVerifier,
597
+ exchangeAuthCode,
598
+ Auth2KmsRpcClient,
599
+ createConnectStartUrl
600
+ } from "@phantom/auth2";
601
+ var ExpoAuth2AuthProvider = class {
602
+ constructor(stamper, auth2ProviderOptions, kmsClientOptions) {
603
+ this.stamper = stamper;
604
+ this.auth2ProviderOptions = auth2ProviderOptions;
605
+ this.kms = new Auth2KmsRpcClient(stamper, kmsClientOptions);
606
+ }
607
+ /**
608
+ * Runs the full PKCE Auth2 flow inline using expo-web-browser.
609
+ *
610
+ * Unlike the browser flow (which requires a page redirect and resumeAuthFromRedirect),
611
+ * expo-web-browser intercepts the OAuth callback URL and returns it synchronously,
612
+ * so the token exchange and KMS calls all happen here before returning AuthResult.
613
+ */
614
+ async authenticate(options) {
615
+ if (!this.stamper.getKeyInfo()) {
616
+ await this.stamper.init();
617
+ }
618
+ const keyPair = this.stamper.getCryptoKeyPair();
619
+ if (!keyPair) {
620
+ throw new Error("Stamper key pair not found.");
621
+ }
622
+ const codeVerifier = createCodeVerifier();
623
+ const url = await createConnectStartUrl({
624
+ keyPair,
625
+ connectLoginUrl: this.auth2ProviderOptions.connectLoginUrl,
626
+ clientId: this.auth2ProviderOptions.clientId,
627
+ redirectUri: this.auth2ProviderOptions.redirectUri,
628
+ sessionId: options.sessionId,
629
+ provider: options.provider,
630
+ codeVerifier,
631
+ // The P-256 ephemeral key is unique per wallet, so no additional salt is needed.
632
+ salt: ""
633
+ });
634
+ await WebBrowser2.warmUpAsync();
635
+ let result;
636
+ try {
637
+ result = await WebBrowser2.openAuthSessionAsync(url, this.auth2ProviderOptions.redirectUri);
638
+ } finally {
639
+ await WebBrowser2.coolDownAsync();
640
+ }
641
+ if (!result.url) {
642
+ throw new Error("Authentication failed");
643
+ }
644
+ const callbackUrl = new URL(result.url);
645
+ const state = callbackUrl.searchParams.get("state");
646
+ if (state && state !== options.sessionId) {
647
+ throw new Error("Auth2 state mismatch \u2014 possible CSRF attack.");
648
+ }
649
+ const error = callbackUrl.searchParams.get("error");
650
+ if (error) {
651
+ const description = callbackUrl.searchParams.get("error_description");
652
+ throw new Error(`Auth2 callback error: ${description ?? error}`);
653
+ }
654
+ const code = callbackUrl.searchParams.get("code");
655
+ if (!code) {
656
+ throw new Error("Auth2 callback missing authorization code");
657
+ }
658
+ const { idToken, bearerToken, authUserId, expiresInMs } = await exchangeAuthCode({
659
+ authApiBaseUrl: this.auth2ProviderOptions.authApiBaseUrl,
660
+ clientId: this.auth2ProviderOptions.clientId,
661
+ redirectUri: this.auth2ProviderOptions.redirectUri,
662
+ code,
663
+ codeVerifier
664
+ });
665
+ await this.stamper.setIdToken(idToken);
666
+ const { organizationId, walletId } = await this.kms.discoverOrganizationAndWalletId(bearerToken, authUserId);
667
+ return {
668
+ walletId,
669
+ organizationId,
670
+ provider: options.provider,
671
+ accountDerivationIndex: 0,
672
+ // discoverWalletId uses derivation index of 0.
673
+ expiresInMs,
674
+ authUserId,
675
+ bearerToken
676
+ };
677
+ }
678
+ };
679
+
680
+ // src/providers/embedded/ExpoAuth2Stamper.ts
681
+ import * as SecureStore2 from "expo-secure-store";
682
+ import bs58 from "bs58";
683
+ import { Buffer } from "buffer";
684
+ import { base64urlEncode } from "@phantom/base64url";
685
+ import { Algorithm } from "@phantom/sdk-types";
686
+ var ExpoAuth2Stamper = class {
687
+ /**
688
+ * @param storageKey - expo-secure-store key used to persist the P-256 private key.
689
+ * Use a unique key per app, e.g. `phantom-auth2-<appId>`.
690
+ */
691
+ constructor(storageKey) {
692
+ this.storageKey = storageKey;
693
+ this._keyPair = null;
694
+ this._keyInfo = null;
695
+ this._idToken = null;
696
+ this.algorithm = Algorithm.secp256r1;
697
+ this.type = "OIDC";
698
+ }
699
+ async init() {
700
+ const stored = await this.loadRecord();
701
+ if (stored) {
702
+ this._keyPair = {
703
+ privateKey: await this.importPrivateKey(stored.privateKeyPkcs8),
704
+ publicKey: await this.importPublicKeyFromBase58(stored.keyInfo.publicKey)
705
+ };
706
+ this._keyInfo = stored.keyInfo;
707
+ if (stored.idToken) {
708
+ this._idToken = stored.idToken;
709
+ }
710
+ return this._keyInfo;
711
+ }
712
+ return this.generateAndStore();
713
+ }
714
+ getKeyInfo() {
715
+ return this._keyInfo;
716
+ }
717
+ getCryptoKeyPair() {
718
+ return this._keyPair;
719
+ }
720
+ /**
721
+ * Arms the stamper with the OIDC id token for subsequent KMS stamp() calls.
722
+ *
723
+ * Persists the token to SecureStore alongside the key pair so that
724
+ * auto-connect can restore it on the next app launch without a new login.
725
+ */
726
+ async setIdToken(idToken) {
727
+ this._idToken = idToken;
728
+ const existing = await this.loadRecord();
729
+ if (existing) {
730
+ await this.storeRecord({ ...existing, idToken });
731
+ }
732
+ }
733
+ async stamp(params) {
734
+ if (!this._keyPair || !this._keyInfo || this._idToken === null) {
735
+ throw new Error("ExpoAuth2Stamper not initialized. Call init() first.");
736
+ }
737
+ const signatureRaw = await crypto.subtle.sign(
738
+ { name: "ECDSA", hash: "SHA-256" },
739
+ this._keyPair.privateKey,
740
+ new Uint8Array(params.data)
741
+ );
742
+ const rawPublicKey = bs58.decode(this._keyInfo.publicKey);
743
+ const stampData = {
744
+ kind: this.type,
745
+ idToken: this._idToken,
746
+ publicKey: base64urlEncode(rawPublicKey),
747
+ algorithm: this.algorithm,
748
+ // The P-256 ephemeral key is unique per wallet, so no additional salt is needed.
749
+ salt: "",
750
+ signature: base64urlEncode(new Uint8Array(signatureRaw))
751
+ };
752
+ return base64urlEncode(new TextEncoder().encode(JSON.stringify(stampData)));
753
+ }
754
+ async resetKeyPair() {
755
+ await this.clear();
756
+ return this.generateAndStore();
757
+ }
758
+ async clear() {
759
+ await this.clearStoredRecord();
760
+ this._keyPair = null;
761
+ this._keyInfo = null;
762
+ this._idToken = null;
763
+ }
764
+ // Auth2 doesn't use key rotation; minimal no-op implementations.
765
+ async rotateKeyPair() {
766
+ return this.init();
767
+ }
768
+ // eslint-disable-next-line @typescript-eslint/require-await
769
+ async commitRotation(authenticatorId) {
770
+ if (this._keyInfo) {
771
+ this._keyInfo.authenticatorId = authenticatorId;
772
+ }
773
+ }
774
+ async rollbackRotation() {
775
+ }
776
+ async generateAndStore() {
777
+ const keyPair = await crypto.subtle.generateKey(
778
+ { name: "ECDSA", namedCurve: "P-256" },
779
+ true,
780
+ // extractable — needed to export PKCS#8 for SecureStore
781
+ ["sign", "verify"]
782
+ );
783
+ const rawPublicKey = new Uint8Array(await crypto.subtle.exportKey("raw", keyPair.publicKey));
784
+ const publicKeyBase58 = bs58.encode(rawPublicKey);
785
+ const keyIdBuffer = await crypto.subtle.digest("SHA-256", rawPublicKey.buffer);
786
+ const keyId = base64urlEncode(new Uint8Array(keyIdBuffer)).substring(0, 16);
787
+ this._keyPair = keyPair;
788
+ this._keyInfo = { keyId, publicKey: publicKeyBase58, createdAt: Date.now() };
789
+ const pkcs8Buffer = await crypto.subtle.exportKey("pkcs8", keyPair.privateKey);
790
+ const privateKeyPkcs8 = Buffer.from(pkcs8Buffer).toString("base64");
791
+ await this.storeRecord({ privateKeyPkcs8, keyInfo: this._keyInfo });
792
+ return this._keyInfo;
793
+ }
794
+ async importPublicKeyFromBase58(base58PublicKey) {
795
+ const rawBytes = bs58.decode(base58PublicKey);
796
+ return crypto.subtle.importKey(
797
+ "raw",
798
+ rawBytes.buffer.slice(rawBytes.byteOffset, rawBytes.byteOffset + rawBytes.byteLength),
799
+ { name: "ECDSA", namedCurve: "P-256" },
800
+ true,
801
+ // extractable so createAuth2RequestJar can export it as JWK
802
+ ["verify"]
803
+ );
804
+ }
805
+ async importPrivateKey(pkcs8Base64) {
806
+ const pkcs8Bytes = Buffer.from(pkcs8Base64, "base64");
807
+ return crypto.subtle.importKey(
808
+ "pkcs8",
809
+ pkcs8Bytes,
810
+ { name: "ECDSA", namedCurve: "P-256" },
811
+ false,
812
+ // non-extractable once loaded into memory
813
+ ["sign"]
814
+ );
815
+ }
816
+ async loadRecord() {
817
+ try {
818
+ const raw = await SecureStore2.getItemAsync(this.storageKey);
819
+ return raw ? JSON.parse(raw) : null;
820
+ } catch {
821
+ return null;
822
+ }
823
+ }
824
+ async storeRecord(record) {
825
+ await SecureStore2.setItemAsync(this.storageKey, JSON.stringify(record), { requireAuthentication: false });
826
+ }
827
+ async clearStoredRecord() {
828
+ try {
829
+ await SecureStore2.deleteItemAsync(this.storageKey);
830
+ } catch {
831
+ }
832
+ }
833
+ };
834
+
593
835
  // src/providers/embedded/url-params.ts
594
836
  import { Linking } from "react-native";
595
837
  var ExpoURLParamsAccessor = class {
@@ -659,11 +901,11 @@ var ExpoURLParamsAccessor = class {
659
901
  };
660
902
 
661
903
  // src/providers/embedded/stamper.ts
662
- import * as SecureStore2 from "expo-secure-store";
904
+ import * as SecureStore3 from "expo-secure-store";
663
905
  import { ApiKeyStamper } from "@phantom/api-key-stamper";
664
906
  import { DEFAULT_AUTHENTICATOR_ALGORITHM } from "@phantom/constants";
665
907
  import { generateKeyPair } from "@phantom/crypto";
666
- import { base64urlEncode } from "@phantom/base64url";
908
+ import { base64urlEncode as base64urlEncode2 } from "@phantom/base64url";
667
909
  var ReactNativeStamper = class {
668
910
  // Optional for PKI, required for OIDC
669
911
  constructor(config = {}) {
@@ -719,11 +961,11 @@ var ReactNativeStamper = class {
719
961
  const activeKey = this.getActiveKeyName();
720
962
  const pendingKey = this.getPendingKeyName();
721
963
  try {
722
- await SecureStore2.deleteItemAsync(activeKey);
964
+ await SecureStore3.deleteItemAsync(activeKey);
723
965
  } catch (error) {
724
966
  }
725
967
  try {
726
- await SecureStore2.deleteItemAsync(pendingKey);
968
+ await SecureStore3.deleteItemAsync(pendingKey);
727
969
  } catch (error) {
728
970
  }
729
971
  this.activeKeyRecord = null;
@@ -745,7 +987,7 @@ var ReactNativeStamper = class {
745
987
  }
746
988
  if (this.activeKeyRecord) {
747
989
  try {
748
- await SecureStore2.deleteItemAsync(this.getActiveKeyName());
990
+ await SecureStore3.deleteItemAsync(this.getActiveKeyName());
749
991
  } catch (error) {
750
992
  }
751
993
  }
@@ -756,7 +998,7 @@ var ReactNativeStamper = class {
756
998
  this.pendingKeyRecord = null;
757
999
  await this.storeKeyRecord(this.activeKeyRecord, "active");
758
1000
  try {
759
- await SecureStore2.deleteItemAsync(this.getPendingKeyName());
1001
+ await SecureStore3.deleteItemAsync(this.getPendingKeyName());
760
1002
  } catch (error) {
761
1003
  }
762
1004
  }
@@ -768,7 +1010,7 @@ var ReactNativeStamper = class {
768
1010
  return;
769
1011
  }
770
1012
  try {
771
- await SecureStore2.deleteItemAsync(this.getPendingKeyName());
1013
+ await SecureStore3.deleteItemAsync(this.getPendingKeyName());
772
1014
  } catch (error) {
773
1015
  }
774
1016
  this.pendingKeyRecord = null;
@@ -794,18 +1036,18 @@ var ReactNativeStamper = class {
794
1036
  return record;
795
1037
  }
796
1038
  createKeyId(publicKey) {
797
- return base64urlEncode(new TextEncoder().encode(publicKey)).substring(0, 16);
1039
+ return base64urlEncode2(new TextEncoder().encode(publicKey)).substring(0, 16);
798
1040
  }
799
1041
  async storeKeyRecord(record, type) {
800
1042
  const keyName = type === "active" ? this.getActiveKeyName() : this.getPendingKeyName();
801
- await SecureStore2.setItemAsync(keyName, JSON.stringify(record), {
1043
+ await SecureStore3.setItemAsync(keyName, JSON.stringify(record), {
802
1044
  requireAuthentication: false
803
1045
  });
804
1046
  }
805
1047
  async loadActiveKeyRecord() {
806
1048
  try {
807
1049
  const activeKey = this.getActiveKeyName();
808
- const storedRecord = await SecureStore2.getItemAsync(activeKey);
1050
+ const storedRecord = await SecureStore3.getItemAsync(activeKey);
809
1051
  if (storedRecord) {
810
1052
  return JSON.parse(storedRecord);
811
1053
  }
@@ -816,7 +1058,7 @@ var ReactNativeStamper = class {
816
1058
  async loadPendingKeyRecord() {
817
1059
  try {
818
1060
  const pendingKey = this.getPendingKeyName();
819
- const storedRecord = await SecureStore2.getItemAsync(pendingKey);
1061
+ const storedRecord = await SecureStore3.getItemAsync(pendingKey);
820
1062
  if (storedRecord) {
821
1063
  return JSON.parse(storedRecord);
822
1064
  }
@@ -898,13 +1140,25 @@ function PhantomProvider({ children, config, debugConfig, theme, appIcon, appNam
898
1140
  }, [config]);
899
1141
  const sdk = useMemo2(() => {
900
1142
  const storage = new ExpoSecureStorage();
901
- const authProvider = new ExpoAuthProvider();
902
1143
  const urlParamsAccessor = new ExpoURLParamsAccessor();
903
1144
  const logger = new ExpoLogger(debugConfig?.enabled || false);
904
- const stamper = new ReactNativeStamper({
1145
+ const stamper = config.unstable__auth2Options ? new ExpoAuth2Stamper(`phantom-auth2-${memoizedConfig.appId}`) : new ReactNativeStamper({
905
1146
  keyPrefix: `phantom-rn-${memoizedConfig.appId}`,
906
1147
  appId: memoizedConfig.appId
907
1148
  });
1149
+ const authProvider = config.unstable__auth2Options && config.authOptions?.authUrl && config.authOptions?.redirectUrl && config.apiBaseUrl && stamper instanceof ExpoAuth2Stamper ? new ExpoAuth2AuthProvider(
1150
+ stamper,
1151
+ {
1152
+ redirectUri: config.authOptions.redirectUrl,
1153
+ connectLoginUrl: config.authOptions.authUrl,
1154
+ clientId: config.unstable__auth2Options.clientId,
1155
+ authApiBaseUrl: config.unstable__auth2Options.authApiBaseUrl
1156
+ },
1157
+ {
1158
+ apiBaseUrl: config.apiBaseUrl,
1159
+ appId: config.appId
1160
+ }
1161
+ ) : new ExpoAuthProvider();
908
1162
  const platformName = `${Platform2.OS}-${Platform2.Version}`;
909
1163
  const platform = {
910
1164
  storage,
@@ -915,16 +1169,17 @@ function PhantomProvider({ children, config, debugConfig, theme, appIcon, appNam
915
1169
  name: platformName,
916
1170
  analyticsHeaders: {
917
1171
  [ANALYTICS_HEADERS.SDK_TYPE]: "react-native",
918
- [ANALYTICS_HEADERS.PLATFORM]: Platform2.OS,
1172
+ [ANALYTICS_HEADERS.PLATFORM]: "ext-sdk",
919
1173
  [ANALYTICS_HEADERS.PLATFORM_VERSION]: `${Platform2.Version}`,
1174
+ [ANALYTICS_HEADERS.CLIENT]: Platform2.OS,
920
1175
  [ANALYTICS_HEADERS.APP_ID]: config.appId,
921
1176
  [ANALYTICS_HEADERS.WALLET_TYPE]: config.embeddedWalletType,
922
- [ANALYTICS_HEADERS.SDK_VERSION]: "1.0.3"
1177
+ [ANALYTICS_HEADERS.SDK_VERSION]: "1.0.5"
923
1178
  // Replaced at build time
924
1179
  }
925
1180
  };
926
1181
  return new EmbeddedProvider(memoizedConfig, platform, logger);
927
- }, [memoizedConfig, debugConfig, config.appId, config.embeddedWalletType]);
1182
+ }, [memoizedConfig, debugConfig, config]);
928
1183
  useEffect2(() => {
929
1184
  const handleConnectStart = () => {
930
1185
  setIsConnecting(true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phantom/react-native-sdk",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Phantom Wallet SDK for React Native and Expo applications",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -45,15 +45,16 @@
45
45
  "directory": "packages/react-native-sdk"
46
46
  },
47
47
  "dependencies": {
48
- "@phantom/api-key-stamper": "^1.0.3",
49
- "@phantom/base64url": "^1.0.3",
50
- "@phantom/chain-interfaces": "^1.0.3",
51
- "@phantom/client": "^1.0.3",
52
- "@phantom/constants": "^1.0.3",
53
- "@phantom/crypto": "^1.0.3",
54
- "@phantom/embedded-provider-core": "^1.0.3",
55
- "@phantom/sdk-types": "^1.0.3",
56
- "@phantom/wallet-sdk-ui": "^1.0.3",
48
+ "@phantom/api-key-stamper": "^1.0.5",
49
+ "@phantom/auth2": "^1.0.1",
50
+ "@phantom/base64url": "^1.0.5",
51
+ "@phantom/chain-interfaces": "^1.0.5",
52
+ "@phantom/client": "^1.0.5",
53
+ "@phantom/constants": "^1.0.5",
54
+ "@phantom/crypto": "^1.0.5",
55
+ "@phantom/embedded-provider-core": "^1.0.5",
56
+ "@phantom/sdk-types": "^1.0.5",
57
+ "@phantom/wallet-sdk-ui": "^1.0.5",
57
58
  "@types/bs58": "^5.0.0",
58
59
  "bs58": "^6.0.0",
59
60
  "buffer": "^6.0.3"