@phantom/react-native-sdk 1.0.3 → 1.0.4

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.4",
561
561
  sdk_type: "react-native",
562
562
  platform: import_react_native5.Platform.OS
563
563
  });
@@ -632,6 +632,235 @@ 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 salt = (0, import_auth2.createSalt)();
661
+ const url = await (0, import_auth2.createConnectStartUrl)({
662
+ keyPair,
663
+ connectLoginUrl: this.auth2ProviderOptions.connectLoginUrl,
664
+ clientId: this.auth2ProviderOptions.clientId,
665
+ redirectUri: this.auth2ProviderOptions.redirectUri,
666
+ sessionId: options.sessionId,
667
+ provider: options.provider,
668
+ codeVerifier,
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
+ this.stamper.idToken = idToken;
703
+ this.stamper.salt = salt;
704
+ const { organizationId, walletId } = await this.kms.discoverOrganizationAndWalletId(bearerToken, authUserId);
705
+ return {
706
+ walletId,
707
+ organizationId,
708
+ provider: options.provider,
709
+ accountDerivationIndex: 0,
710
+ // discoverWalletId uses derivation index of 0.
711
+ expiresInMs,
712
+ authUserId,
713
+ bearerToken
714
+ };
715
+ }
716
+ };
717
+
718
+ // src/providers/embedded/ExpoAuth2Stamper.ts
719
+ var SecureStore2 = __toESM(require("expo-secure-store"));
720
+ var import_bs58 = __toESM(require("bs58"));
721
+ var import_buffer = require("buffer");
722
+ var import_base64url = require("@phantom/base64url");
723
+ var import_sdk_types = require("@phantom/sdk-types");
724
+ var ExpoAuth2Stamper = class {
725
+ /**
726
+ * @param storageKey - expo-secure-store key used to persist the P-256 private key.
727
+ * Use a unique key per app, e.g. `phantom-auth2-<appId>`.
728
+ */
729
+ constructor(storageKey) {
730
+ this.storageKey = storageKey;
731
+ this.privateKey = null;
732
+ this.publicKey = null;
733
+ this._keyInfo = null;
734
+ this.algorithm = import_sdk_types.Algorithm.secp256r1;
735
+ this.type = "OIDC";
736
+ }
737
+ async init() {
738
+ const stored = await this.loadRecord();
739
+ if (stored) {
740
+ this.privateKey = await this.importPrivateKey(stored.privateKeyPkcs8);
741
+ this.publicKey = await this.importPublicKeyFromBase58(stored.keyInfo.publicKey);
742
+ this._keyInfo = stored.keyInfo;
743
+ return this._keyInfo;
744
+ }
745
+ return this.generateAndStore();
746
+ }
747
+ getKeyInfo() {
748
+ return this._keyInfo;
749
+ }
750
+ getCryptoKeyPair() {
751
+ if (!this.privateKey || !this.publicKey)
752
+ return null;
753
+ return { privateKey: this.privateKey, publicKey: this.publicKey };
754
+ }
755
+ async stamp(params) {
756
+ if (!this.privateKey || !this._keyInfo) {
757
+ throw new Error("ExpoAuth2Stamper not initialized. Call init() first.");
758
+ }
759
+ const signatureRaw = await crypto.subtle.sign(
760
+ { name: "ECDSA", hash: "SHA-256" },
761
+ this.privateKey,
762
+ new Uint8Array(params.data)
763
+ );
764
+ const rawPublicKey = import_bs58.default.decode(this._keyInfo.publicKey);
765
+ if (this.idToken === void 0 || this.salt === void 0) {
766
+ throw new Error("ExpoAuth2Stamper not initialized with idToken or salt.");
767
+ }
768
+ const stampData = {
769
+ kind: "OIDC",
770
+ idToken: this.idToken,
771
+ publicKey: (0, import_base64url.base64urlEncode)(rawPublicKey),
772
+ algorithm: "Secp256r1",
773
+ salt: this.salt,
774
+ signature: (0, import_base64url.base64urlEncode)(new Uint8Array(signatureRaw))
775
+ };
776
+ return (0, import_base64url.base64urlEncode)(new TextEncoder().encode(JSON.stringify(stampData)));
777
+ }
778
+ async resetKeyPair() {
779
+ await this.clearStoredRecord();
780
+ this.privateKey = null;
781
+ this.publicKey = null;
782
+ this._keyInfo = null;
783
+ return this.generateAndStore();
784
+ }
785
+ async clear() {
786
+ await this.clearStoredRecord();
787
+ this.privateKey = null;
788
+ this.publicKey = null;
789
+ this._keyInfo = null;
790
+ }
791
+ // Auth2 doesn't use key rotation; minimal no-op implementations.
792
+ async rotateKeyPair() {
793
+ return this.init();
794
+ }
795
+ // eslint-disable-next-line @typescript-eslint/require-await
796
+ async commitRotation(authenticatorId) {
797
+ if (this._keyInfo) {
798
+ this._keyInfo.authenticatorId = authenticatorId;
799
+ }
800
+ }
801
+ async rollbackRotation() {
802
+ }
803
+ async generateAndStore() {
804
+ const keyPair = await crypto.subtle.generateKey(
805
+ { name: "ECDSA", namedCurve: "P-256" },
806
+ true,
807
+ // extractable — needed to export PKCS#8 for SecureStore
808
+ ["sign", "verify"]
809
+ );
810
+ const rawPublicKey = new Uint8Array(await crypto.subtle.exportKey("raw", keyPair.publicKey));
811
+ const publicKeyBase58 = import_bs58.default.encode(rawPublicKey);
812
+ const keyIdBuffer = await crypto.subtle.digest("SHA-256", rawPublicKey.buffer);
813
+ const keyId = (0, import_base64url.base64urlEncode)(new Uint8Array(keyIdBuffer)).substring(0, 16);
814
+ this._keyInfo = { keyId, publicKey: publicKeyBase58, createdAt: Date.now() };
815
+ const pkcs8Buffer = await crypto.subtle.exportKey("pkcs8", keyPair.privateKey);
816
+ const privateKeyPkcs8 = import_buffer.Buffer.from(pkcs8Buffer).toString("base64");
817
+ await SecureStore2.setItemAsync(
818
+ this.storageKey,
819
+ JSON.stringify({ privateKeyPkcs8, keyInfo: this._keyInfo }),
820
+ { requireAuthentication: false }
821
+ );
822
+ this.privateKey = await this.importPrivateKey(privateKeyPkcs8);
823
+ this.publicKey = keyPair.publicKey;
824
+ return this._keyInfo;
825
+ }
826
+ async importPublicKeyFromBase58(base58PublicKey) {
827
+ const rawBytes = import_bs58.default.decode(base58PublicKey);
828
+ return crypto.subtle.importKey(
829
+ "raw",
830
+ rawBytes.buffer.slice(rawBytes.byteOffset, rawBytes.byteOffset + rawBytes.byteLength),
831
+ { name: "ECDSA", namedCurve: "P-256" },
832
+ true,
833
+ // extractable so createAuth2RequestJar can export it as JWK
834
+ ["verify"]
835
+ );
836
+ }
837
+ async importPrivateKey(pkcs8Base64) {
838
+ const pkcs8Bytes = import_buffer.Buffer.from(pkcs8Base64, "base64");
839
+ return crypto.subtle.importKey(
840
+ "pkcs8",
841
+ pkcs8Bytes,
842
+ { name: "ECDSA", namedCurve: "P-256" },
843
+ false,
844
+ // non-extractable once loaded into memory
845
+ ["sign"]
846
+ );
847
+ }
848
+ async loadRecord() {
849
+ try {
850
+ const raw = await SecureStore2.getItemAsync(this.storageKey);
851
+ return raw ? JSON.parse(raw) : null;
852
+ } catch {
853
+ return null;
854
+ }
855
+ }
856
+ async clearStoredRecord() {
857
+ try {
858
+ await SecureStore2.deleteItemAsync(this.storageKey);
859
+ } catch {
860
+ }
861
+ }
862
+ };
863
+
635
864
  // src/providers/embedded/url-params.ts
636
865
  var import_react_native6 = require("react-native");
637
866
  var ExpoURLParamsAccessor = class {
@@ -701,11 +930,11 @@ var ExpoURLParamsAccessor = class {
701
930
  };
702
931
 
703
932
  // src/providers/embedded/stamper.ts
704
- var SecureStore2 = __toESM(require("expo-secure-store"));
933
+ var SecureStore3 = __toESM(require("expo-secure-store"));
705
934
  var import_api_key_stamper = require("@phantom/api-key-stamper");
706
935
  var import_constants2 = require("@phantom/constants");
707
936
  var import_crypto = require("@phantom/crypto");
708
- var import_base64url = require("@phantom/base64url");
937
+ var import_base64url2 = require("@phantom/base64url");
709
938
  var ReactNativeStamper = class {
710
939
  // Optional for PKI, required for OIDC
711
940
  constructor(config = {}) {
@@ -761,11 +990,11 @@ var ReactNativeStamper = class {
761
990
  const activeKey = this.getActiveKeyName();
762
991
  const pendingKey = this.getPendingKeyName();
763
992
  try {
764
- await SecureStore2.deleteItemAsync(activeKey);
993
+ await SecureStore3.deleteItemAsync(activeKey);
765
994
  } catch (error) {
766
995
  }
767
996
  try {
768
- await SecureStore2.deleteItemAsync(pendingKey);
997
+ await SecureStore3.deleteItemAsync(pendingKey);
769
998
  } catch (error) {
770
999
  }
771
1000
  this.activeKeyRecord = null;
@@ -787,7 +1016,7 @@ var ReactNativeStamper = class {
787
1016
  }
788
1017
  if (this.activeKeyRecord) {
789
1018
  try {
790
- await SecureStore2.deleteItemAsync(this.getActiveKeyName());
1019
+ await SecureStore3.deleteItemAsync(this.getActiveKeyName());
791
1020
  } catch (error) {
792
1021
  }
793
1022
  }
@@ -798,7 +1027,7 @@ var ReactNativeStamper = class {
798
1027
  this.pendingKeyRecord = null;
799
1028
  await this.storeKeyRecord(this.activeKeyRecord, "active");
800
1029
  try {
801
- await SecureStore2.deleteItemAsync(this.getPendingKeyName());
1030
+ await SecureStore3.deleteItemAsync(this.getPendingKeyName());
802
1031
  } catch (error) {
803
1032
  }
804
1033
  }
@@ -810,7 +1039,7 @@ var ReactNativeStamper = class {
810
1039
  return;
811
1040
  }
812
1041
  try {
813
- await SecureStore2.deleteItemAsync(this.getPendingKeyName());
1042
+ await SecureStore3.deleteItemAsync(this.getPendingKeyName());
814
1043
  } catch (error) {
815
1044
  }
816
1045
  this.pendingKeyRecord = null;
@@ -836,18 +1065,18 @@ var ReactNativeStamper = class {
836
1065
  return record;
837
1066
  }
838
1067
  createKeyId(publicKey) {
839
- return (0, import_base64url.base64urlEncode)(new TextEncoder().encode(publicKey)).substring(0, 16);
1068
+ return (0, import_base64url2.base64urlEncode)(new TextEncoder().encode(publicKey)).substring(0, 16);
840
1069
  }
841
1070
  async storeKeyRecord(record, type) {
842
1071
  const keyName = type === "active" ? this.getActiveKeyName() : this.getPendingKeyName();
843
- await SecureStore2.setItemAsync(keyName, JSON.stringify(record), {
1072
+ await SecureStore3.setItemAsync(keyName, JSON.stringify(record), {
844
1073
  requireAuthentication: false
845
1074
  });
846
1075
  }
847
1076
  async loadActiveKeyRecord() {
848
1077
  try {
849
1078
  const activeKey = this.getActiveKeyName();
850
- const storedRecord = await SecureStore2.getItemAsync(activeKey);
1079
+ const storedRecord = await SecureStore3.getItemAsync(activeKey);
851
1080
  if (storedRecord) {
852
1081
  return JSON.parse(storedRecord);
853
1082
  }
@@ -858,7 +1087,7 @@ var ReactNativeStamper = class {
858
1087
  async loadPendingKeyRecord() {
859
1088
  try {
860
1089
  const pendingKey = this.getPendingKeyName();
861
- const storedRecord = await SecureStore2.getItemAsync(pendingKey);
1090
+ const storedRecord = await SecureStore3.getItemAsync(pendingKey);
862
1091
  if (storedRecord) {
863
1092
  return JSON.parse(storedRecord);
864
1093
  }
@@ -940,13 +1169,25 @@ function PhantomProvider({ children, config, debugConfig, theme, appIcon, appNam
940
1169
  }, [config]);
941
1170
  const sdk = (0, import_react8.useMemo)(() => {
942
1171
  const storage = new ExpoSecureStorage();
943
- const authProvider = new ExpoAuthProvider();
944
1172
  const urlParamsAccessor = new ExpoURLParamsAccessor();
945
1173
  const logger = new ExpoLogger(debugConfig?.enabled || false);
946
- const stamper = new ReactNativeStamper({
1174
+ const stamper = config.unstable__auth2Options ? new ExpoAuth2Stamper(`phantom-auth2-${memoizedConfig.appId}`) : new ReactNativeStamper({
947
1175
  keyPrefix: `phantom-rn-${memoizedConfig.appId}`,
948
1176
  appId: memoizedConfig.appId
949
1177
  });
1178
+ const authProvider = config.unstable__auth2Options && config.authOptions?.authUrl && config.authOptions?.redirectUrl && config.apiBaseUrl && stamper instanceof ExpoAuth2Stamper ? new ExpoAuth2AuthProvider(
1179
+ stamper,
1180
+ {
1181
+ redirectUri: config.authOptions.redirectUrl,
1182
+ connectLoginUrl: config.authOptions.authUrl,
1183
+ clientId: config.unstable__auth2Options.clientId,
1184
+ authApiBaseUrl: config.unstable__auth2Options.authApiBaseUrl
1185
+ },
1186
+ {
1187
+ apiBaseUrl: config.apiBaseUrl,
1188
+ appId: config.appId
1189
+ }
1190
+ ) : new ExpoAuthProvider();
950
1191
  const platformName = `${import_react_native7.Platform.OS}-${import_react_native7.Platform.Version}`;
951
1192
  const platform = {
952
1193
  storage,
@@ -957,16 +1198,17 @@ function PhantomProvider({ children, config, debugConfig, theme, appIcon, appNam
957
1198
  name: platformName,
958
1199
  analyticsHeaders: {
959
1200
  [import_constants3.ANALYTICS_HEADERS.SDK_TYPE]: "react-native",
960
- [import_constants3.ANALYTICS_HEADERS.PLATFORM]: import_react_native7.Platform.OS,
1201
+ [import_constants3.ANALYTICS_HEADERS.PLATFORM]: "ext-sdk",
961
1202
  [import_constants3.ANALYTICS_HEADERS.PLATFORM_VERSION]: `${import_react_native7.Platform.Version}`,
1203
+ [import_constants3.ANALYTICS_HEADERS.CLIENT]: import_react_native7.Platform.OS,
962
1204
  [import_constants3.ANALYTICS_HEADERS.APP_ID]: config.appId,
963
1205
  [import_constants3.ANALYTICS_HEADERS.WALLET_TYPE]: config.embeddedWalletType,
964
- [import_constants3.ANALYTICS_HEADERS.SDK_VERSION]: "1.0.3"
1206
+ [import_constants3.ANALYTICS_HEADERS.SDK_VERSION]: "1.0.4"
965
1207
  // Replaced at build time
966
1208
  }
967
1209
  };
968
1210
  return new import_embedded_provider_core.EmbeddedProvider(memoizedConfig, platform, logger);
969
- }, [memoizedConfig, debugConfig, config.appId, config.embeddedWalletType]);
1211
+ }, [memoizedConfig, debugConfig, config]);
970
1212
  (0, import_react8.useEffect)(() => {
971
1213
  const handleConnectStart = () => {
972
1214
  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.4",
519
519
  sdk_type: "react-native",
520
520
  platform: Platform.OS
521
521
  });
@@ -590,6 +590,241 @@ 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
+ createSalt,
598
+ exchangeAuthCode,
599
+ Auth2KmsRpcClient,
600
+ createConnectStartUrl
601
+ } from "@phantom/auth2";
602
+ var ExpoAuth2AuthProvider = class {
603
+ constructor(stamper, auth2ProviderOptions, kmsClientOptions) {
604
+ this.stamper = stamper;
605
+ this.auth2ProviderOptions = auth2ProviderOptions;
606
+ this.kms = new Auth2KmsRpcClient(stamper, kmsClientOptions);
607
+ }
608
+ /**
609
+ * Runs the full PKCE Auth2 flow inline using expo-web-browser.
610
+ *
611
+ * Unlike the browser flow (which requires a page redirect and resumeAuthFromRedirect),
612
+ * expo-web-browser intercepts the OAuth callback URL and returns it synchronously,
613
+ * so the token exchange and KMS calls all happen here before returning AuthResult.
614
+ */
615
+ async authenticate(options) {
616
+ if (!this.stamper.getKeyInfo()) {
617
+ await this.stamper.init();
618
+ }
619
+ const keyPair = this.stamper.getCryptoKeyPair();
620
+ if (!keyPair) {
621
+ throw new Error("Stamper key pair not found.");
622
+ }
623
+ const codeVerifier = createCodeVerifier();
624
+ const salt = createSalt();
625
+ const url = await createConnectStartUrl({
626
+ keyPair,
627
+ connectLoginUrl: this.auth2ProviderOptions.connectLoginUrl,
628
+ clientId: this.auth2ProviderOptions.clientId,
629
+ redirectUri: this.auth2ProviderOptions.redirectUri,
630
+ sessionId: options.sessionId,
631
+ provider: options.provider,
632
+ codeVerifier,
633
+ salt
634
+ });
635
+ await WebBrowser2.warmUpAsync();
636
+ let result;
637
+ try {
638
+ result = await WebBrowser2.openAuthSessionAsync(url, this.auth2ProviderOptions.redirectUri);
639
+ } finally {
640
+ await WebBrowser2.coolDownAsync();
641
+ }
642
+ if (!result.url) {
643
+ throw new Error("Authentication failed");
644
+ }
645
+ const callbackUrl = new URL(result.url);
646
+ const state = callbackUrl.searchParams.get("state");
647
+ if (state && state !== options.sessionId) {
648
+ throw new Error("Auth2 state mismatch \u2014 possible CSRF attack.");
649
+ }
650
+ const error = callbackUrl.searchParams.get("error");
651
+ if (error) {
652
+ const description = callbackUrl.searchParams.get("error_description");
653
+ throw new Error(`Auth2 callback error: ${description ?? error}`);
654
+ }
655
+ const code = callbackUrl.searchParams.get("code");
656
+ if (!code) {
657
+ throw new Error("Auth2 callback missing authorization code");
658
+ }
659
+ const { idToken, bearerToken, authUserId, expiresInMs } = await exchangeAuthCode({
660
+ authApiBaseUrl: this.auth2ProviderOptions.authApiBaseUrl,
661
+ clientId: this.auth2ProviderOptions.clientId,
662
+ redirectUri: this.auth2ProviderOptions.redirectUri,
663
+ code,
664
+ codeVerifier
665
+ });
666
+ this.stamper.idToken = idToken;
667
+ this.stamper.salt = salt;
668
+ const { organizationId, walletId } = await this.kms.discoverOrganizationAndWalletId(bearerToken, authUserId);
669
+ return {
670
+ walletId,
671
+ organizationId,
672
+ provider: options.provider,
673
+ accountDerivationIndex: 0,
674
+ // discoverWalletId uses derivation index of 0.
675
+ expiresInMs,
676
+ authUserId,
677
+ bearerToken
678
+ };
679
+ }
680
+ };
681
+
682
+ // src/providers/embedded/ExpoAuth2Stamper.ts
683
+ import * as SecureStore2 from "expo-secure-store";
684
+ import bs58 from "bs58";
685
+ import { Buffer } from "buffer";
686
+ import { base64urlEncode } from "@phantom/base64url";
687
+ import { Algorithm } from "@phantom/sdk-types";
688
+ var ExpoAuth2Stamper = class {
689
+ /**
690
+ * @param storageKey - expo-secure-store key used to persist the P-256 private key.
691
+ * Use a unique key per app, e.g. `phantom-auth2-<appId>`.
692
+ */
693
+ constructor(storageKey) {
694
+ this.storageKey = storageKey;
695
+ this.privateKey = null;
696
+ this.publicKey = null;
697
+ this._keyInfo = null;
698
+ this.algorithm = Algorithm.secp256r1;
699
+ this.type = "OIDC";
700
+ }
701
+ async init() {
702
+ const stored = await this.loadRecord();
703
+ if (stored) {
704
+ this.privateKey = await this.importPrivateKey(stored.privateKeyPkcs8);
705
+ this.publicKey = await this.importPublicKeyFromBase58(stored.keyInfo.publicKey);
706
+ this._keyInfo = stored.keyInfo;
707
+ return this._keyInfo;
708
+ }
709
+ return this.generateAndStore();
710
+ }
711
+ getKeyInfo() {
712
+ return this._keyInfo;
713
+ }
714
+ getCryptoKeyPair() {
715
+ if (!this.privateKey || !this.publicKey)
716
+ return null;
717
+ return { privateKey: this.privateKey, publicKey: this.publicKey };
718
+ }
719
+ async stamp(params) {
720
+ if (!this.privateKey || !this._keyInfo) {
721
+ throw new Error("ExpoAuth2Stamper not initialized. Call init() first.");
722
+ }
723
+ const signatureRaw = await crypto.subtle.sign(
724
+ { name: "ECDSA", hash: "SHA-256" },
725
+ this.privateKey,
726
+ new Uint8Array(params.data)
727
+ );
728
+ const rawPublicKey = bs58.decode(this._keyInfo.publicKey);
729
+ if (this.idToken === void 0 || this.salt === void 0) {
730
+ throw new Error("ExpoAuth2Stamper not initialized with idToken or salt.");
731
+ }
732
+ const stampData = {
733
+ kind: "OIDC",
734
+ idToken: this.idToken,
735
+ publicKey: base64urlEncode(rawPublicKey),
736
+ algorithm: "Secp256r1",
737
+ salt: this.salt,
738
+ signature: base64urlEncode(new Uint8Array(signatureRaw))
739
+ };
740
+ return base64urlEncode(new TextEncoder().encode(JSON.stringify(stampData)));
741
+ }
742
+ async resetKeyPair() {
743
+ await this.clearStoredRecord();
744
+ this.privateKey = null;
745
+ this.publicKey = null;
746
+ this._keyInfo = null;
747
+ return this.generateAndStore();
748
+ }
749
+ async clear() {
750
+ await this.clearStoredRecord();
751
+ this.privateKey = null;
752
+ this.publicKey = null;
753
+ this._keyInfo = null;
754
+ }
755
+ // Auth2 doesn't use key rotation; minimal no-op implementations.
756
+ async rotateKeyPair() {
757
+ return this.init();
758
+ }
759
+ // eslint-disable-next-line @typescript-eslint/require-await
760
+ async commitRotation(authenticatorId) {
761
+ if (this._keyInfo) {
762
+ this._keyInfo.authenticatorId = authenticatorId;
763
+ }
764
+ }
765
+ async rollbackRotation() {
766
+ }
767
+ async generateAndStore() {
768
+ const keyPair = await crypto.subtle.generateKey(
769
+ { name: "ECDSA", namedCurve: "P-256" },
770
+ true,
771
+ // extractable — needed to export PKCS#8 for SecureStore
772
+ ["sign", "verify"]
773
+ );
774
+ const rawPublicKey = new Uint8Array(await crypto.subtle.exportKey("raw", keyPair.publicKey));
775
+ const publicKeyBase58 = bs58.encode(rawPublicKey);
776
+ const keyIdBuffer = await crypto.subtle.digest("SHA-256", rawPublicKey.buffer);
777
+ const keyId = base64urlEncode(new Uint8Array(keyIdBuffer)).substring(0, 16);
778
+ this._keyInfo = { keyId, publicKey: publicKeyBase58, createdAt: Date.now() };
779
+ const pkcs8Buffer = await crypto.subtle.exportKey("pkcs8", keyPair.privateKey);
780
+ const privateKeyPkcs8 = Buffer.from(pkcs8Buffer).toString("base64");
781
+ await SecureStore2.setItemAsync(
782
+ this.storageKey,
783
+ JSON.stringify({ privateKeyPkcs8, keyInfo: this._keyInfo }),
784
+ { requireAuthentication: false }
785
+ );
786
+ this.privateKey = await this.importPrivateKey(privateKeyPkcs8);
787
+ this.publicKey = keyPair.publicKey;
788
+ return this._keyInfo;
789
+ }
790
+ async importPublicKeyFromBase58(base58PublicKey) {
791
+ const rawBytes = bs58.decode(base58PublicKey);
792
+ return crypto.subtle.importKey(
793
+ "raw",
794
+ rawBytes.buffer.slice(rawBytes.byteOffset, rawBytes.byteOffset + rawBytes.byteLength),
795
+ { name: "ECDSA", namedCurve: "P-256" },
796
+ true,
797
+ // extractable so createAuth2RequestJar can export it as JWK
798
+ ["verify"]
799
+ );
800
+ }
801
+ async importPrivateKey(pkcs8Base64) {
802
+ const pkcs8Bytes = Buffer.from(pkcs8Base64, "base64");
803
+ return crypto.subtle.importKey(
804
+ "pkcs8",
805
+ pkcs8Bytes,
806
+ { name: "ECDSA", namedCurve: "P-256" },
807
+ false,
808
+ // non-extractable once loaded into memory
809
+ ["sign"]
810
+ );
811
+ }
812
+ async loadRecord() {
813
+ try {
814
+ const raw = await SecureStore2.getItemAsync(this.storageKey);
815
+ return raw ? JSON.parse(raw) : null;
816
+ } catch {
817
+ return null;
818
+ }
819
+ }
820
+ async clearStoredRecord() {
821
+ try {
822
+ await SecureStore2.deleteItemAsync(this.storageKey);
823
+ } catch {
824
+ }
825
+ }
826
+ };
827
+
593
828
  // src/providers/embedded/url-params.ts
594
829
  import { Linking } from "react-native";
595
830
  var ExpoURLParamsAccessor = class {
@@ -659,11 +894,11 @@ var ExpoURLParamsAccessor = class {
659
894
  };
660
895
 
661
896
  // src/providers/embedded/stamper.ts
662
- import * as SecureStore2 from "expo-secure-store";
897
+ import * as SecureStore3 from "expo-secure-store";
663
898
  import { ApiKeyStamper } from "@phantom/api-key-stamper";
664
899
  import { DEFAULT_AUTHENTICATOR_ALGORITHM } from "@phantom/constants";
665
900
  import { generateKeyPair } from "@phantom/crypto";
666
- import { base64urlEncode } from "@phantom/base64url";
901
+ import { base64urlEncode as base64urlEncode2 } from "@phantom/base64url";
667
902
  var ReactNativeStamper = class {
668
903
  // Optional for PKI, required for OIDC
669
904
  constructor(config = {}) {
@@ -719,11 +954,11 @@ var ReactNativeStamper = class {
719
954
  const activeKey = this.getActiveKeyName();
720
955
  const pendingKey = this.getPendingKeyName();
721
956
  try {
722
- await SecureStore2.deleteItemAsync(activeKey);
957
+ await SecureStore3.deleteItemAsync(activeKey);
723
958
  } catch (error) {
724
959
  }
725
960
  try {
726
- await SecureStore2.deleteItemAsync(pendingKey);
961
+ await SecureStore3.deleteItemAsync(pendingKey);
727
962
  } catch (error) {
728
963
  }
729
964
  this.activeKeyRecord = null;
@@ -745,7 +980,7 @@ var ReactNativeStamper = class {
745
980
  }
746
981
  if (this.activeKeyRecord) {
747
982
  try {
748
- await SecureStore2.deleteItemAsync(this.getActiveKeyName());
983
+ await SecureStore3.deleteItemAsync(this.getActiveKeyName());
749
984
  } catch (error) {
750
985
  }
751
986
  }
@@ -756,7 +991,7 @@ var ReactNativeStamper = class {
756
991
  this.pendingKeyRecord = null;
757
992
  await this.storeKeyRecord(this.activeKeyRecord, "active");
758
993
  try {
759
- await SecureStore2.deleteItemAsync(this.getPendingKeyName());
994
+ await SecureStore3.deleteItemAsync(this.getPendingKeyName());
760
995
  } catch (error) {
761
996
  }
762
997
  }
@@ -768,7 +1003,7 @@ var ReactNativeStamper = class {
768
1003
  return;
769
1004
  }
770
1005
  try {
771
- await SecureStore2.deleteItemAsync(this.getPendingKeyName());
1006
+ await SecureStore3.deleteItemAsync(this.getPendingKeyName());
772
1007
  } catch (error) {
773
1008
  }
774
1009
  this.pendingKeyRecord = null;
@@ -794,18 +1029,18 @@ var ReactNativeStamper = class {
794
1029
  return record;
795
1030
  }
796
1031
  createKeyId(publicKey) {
797
- return base64urlEncode(new TextEncoder().encode(publicKey)).substring(0, 16);
1032
+ return base64urlEncode2(new TextEncoder().encode(publicKey)).substring(0, 16);
798
1033
  }
799
1034
  async storeKeyRecord(record, type) {
800
1035
  const keyName = type === "active" ? this.getActiveKeyName() : this.getPendingKeyName();
801
- await SecureStore2.setItemAsync(keyName, JSON.stringify(record), {
1036
+ await SecureStore3.setItemAsync(keyName, JSON.stringify(record), {
802
1037
  requireAuthentication: false
803
1038
  });
804
1039
  }
805
1040
  async loadActiveKeyRecord() {
806
1041
  try {
807
1042
  const activeKey = this.getActiveKeyName();
808
- const storedRecord = await SecureStore2.getItemAsync(activeKey);
1043
+ const storedRecord = await SecureStore3.getItemAsync(activeKey);
809
1044
  if (storedRecord) {
810
1045
  return JSON.parse(storedRecord);
811
1046
  }
@@ -816,7 +1051,7 @@ var ReactNativeStamper = class {
816
1051
  async loadPendingKeyRecord() {
817
1052
  try {
818
1053
  const pendingKey = this.getPendingKeyName();
819
- const storedRecord = await SecureStore2.getItemAsync(pendingKey);
1054
+ const storedRecord = await SecureStore3.getItemAsync(pendingKey);
820
1055
  if (storedRecord) {
821
1056
  return JSON.parse(storedRecord);
822
1057
  }
@@ -898,13 +1133,25 @@ function PhantomProvider({ children, config, debugConfig, theme, appIcon, appNam
898
1133
  }, [config]);
899
1134
  const sdk = useMemo2(() => {
900
1135
  const storage = new ExpoSecureStorage();
901
- const authProvider = new ExpoAuthProvider();
902
1136
  const urlParamsAccessor = new ExpoURLParamsAccessor();
903
1137
  const logger = new ExpoLogger(debugConfig?.enabled || false);
904
- const stamper = new ReactNativeStamper({
1138
+ const stamper = config.unstable__auth2Options ? new ExpoAuth2Stamper(`phantom-auth2-${memoizedConfig.appId}`) : new ReactNativeStamper({
905
1139
  keyPrefix: `phantom-rn-${memoizedConfig.appId}`,
906
1140
  appId: memoizedConfig.appId
907
1141
  });
1142
+ const authProvider = config.unstable__auth2Options && config.authOptions?.authUrl && config.authOptions?.redirectUrl && config.apiBaseUrl && stamper instanceof ExpoAuth2Stamper ? new ExpoAuth2AuthProvider(
1143
+ stamper,
1144
+ {
1145
+ redirectUri: config.authOptions.redirectUrl,
1146
+ connectLoginUrl: config.authOptions.authUrl,
1147
+ clientId: config.unstable__auth2Options.clientId,
1148
+ authApiBaseUrl: config.unstable__auth2Options.authApiBaseUrl
1149
+ },
1150
+ {
1151
+ apiBaseUrl: config.apiBaseUrl,
1152
+ appId: config.appId
1153
+ }
1154
+ ) : new ExpoAuthProvider();
908
1155
  const platformName = `${Platform2.OS}-${Platform2.Version}`;
909
1156
  const platform = {
910
1157
  storage,
@@ -915,16 +1162,17 @@ function PhantomProvider({ children, config, debugConfig, theme, appIcon, appNam
915
1162
  name: platformName,
916
1163
  analyticsHeaders: {
917
1164
  [ANALYTICS_HEADERS.SDK_TYPE]: "react-native",
918
- [ANALYTICS_HEADERS.PLATFORM]: Platform2.OS,
1165
+ [ANALYTICS_HEADERS.PLATFORM]: "ext-sdk",
919
1166
  [ANALYTICS_HEADERS.PLATFORM_VERSION]: `${Platform2.Version}`,
1167
+ [ANALYTICS_HEADERS.CLIENT]: Platform2.OS,
920
1168
  [ANALYTICS_HEADERS.APP_ID]: config.appId,
921
1169
  [ANALYTICS_HEADERS.WALLET_TYPE]: config.embeddedWalletType,
922
- [ANALYTICS_HEADERS.SDK_VERSION]: "1.0.3"
1170
+ [ANALYTICS_HEADERS.SDK_VERSION]: "1.0.4"
923
1171
  // Replaced at build time
924
1172
  }
925
1173
  };
926
1174
  return new EmbeddedProvider(memoizedConfig, platform, logger);
927
- }, [memoizedConfig, debugConfig, config.appId, config.embeddedWalletType]);
1175
+ }, [memoizedConfig, debugConfig, config]);
928
1176
  useEffect2(() => {
929
1177
  const handleConnectStart = () => {
930
1178
  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.4",
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.4",
49
+ "@phantom/auth2": "^1.0.0",
50
+ "@phantom/base64url": "^1.0.4",
51
+ "@phantom/chain-interfaces": "^1.0.4",
52
+ "@phantom/client": "^1.0.4",
53
+ "@phantom/constants": "^1.0.4",
54
+ "@phantom/crypto": "^1.0.4",
55
+ "@phantom/embedded-provider-core": "^1.0.4",
56
+ "@phantom/sdk-types": "^1.0.4",
57
+ "@phantom/wallet-sdk-ui": "^1.0.4",
57
58
  "@types/bs58": "^5.0.0",
58
59
  "bs58": "^6.0.0",
59
60
  "buffer": "^6.0.3"