@phantom/browser-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
@@ -119,6 +119,11 @@ type BrowserSDKConfig = Prettify<Omit<EmbeddedProviderConfig, "authOptions" | "a
119
119
  authUrl?: string;
120
120
  redirectUrl?: string;
121
121
  };
122
+ /** When also provided, the Auth2 PKCE flow is used instead of the legacy Phantom Connect flow. */
123
+ unstable__auth2Options?: {
124
+ authApiBaseUrl: string;
125
+ clientId: string;
126
+ };
122
127
  }>;
123
128
  type Prettify<T> = {
124
129
  [K in keyof T]: T[K];
package/dist/index.js CHANGED
@@ -1805,16 +1805,11 @@ var InjectedProvider = class {
1805
1805
  walletName: walletInfo.name
1806
1806
  });
1807
1807
  } catch (err) {
1808
- debug.warn(DebugCategory.INJECTED_PROVIDER, "Failed to connect Solana, stopping", {
1808
+ debug.warn(DebugCategory.INJECTED_PROVIDER, "Failed to connect Solana, continuing with other chains", {
1809
1809
  error: err,
1810
1810
  walletId: this.selectedWalletId,
1811
1811
  walletName: walletInfo.name
1812
1812
  });
1813
- this.emit("connect_error", {
1814
- error: err instanceof Error ? err.message : "Failed to connect",
1815
- source: options?.skipEventListeners ? "auto-connect" : "manual-connect"
1816
- });
1817
- throw err;
1818
1813
  }
1819
1814
  }
1820
1815
  if (this.addressTypes.includes(import_client4.AddressType.ethereum) && walletInfo.providers?.ethereum) {
@@ -1844,16 +1839,11 @@ var InjectedProvider = class {
1844
1839
  });
1845
1840
  }
1846
1841
  } catch (err) {
1847
- debug.warn(DebugCategory.INJECTED_PROVIDER, "Failed to connect Ethereum, stopping", {
1842
+ debug.warn(DebugCategory.INJECTED_PROVIDER, "Failed to connect Ethereum, continuing with other chains", {
1848
1843
  error: err,
1849
1844
  walletId: this.selectedWalletId,
1850
1845
  walletName: walletInfo.name
1851
1846
  });
1852
- this.emit("connect_error", {
1853
- error: err instanceof Error ? err.message : "Failed to connect",
1854
- source: options?.skipEventListeners ? "auto-connect" : "manual-connect"
1855
- });
1856
- throw err;
1857
1847
  }
1858
1848
  }
1859
1849
  return connectedAddresses;
@@ -2774,7 +2764,7 @@ var BrowserAuthProvider = class {
2774
2764
  // OAuth session management - defaults to allow refresh unless explicitly clearing after logout
2775
2765
  clear_previous_session: (phantomOptions.clearPreviousSession ?? false).toString(),
2776
2766
  allow_refresh: (phantomOptions.allowRefresh ?? true).toString(),
2777
- sdk_version: "1.0.3",
2767
+ sdk_version: "1.0.4",
2778
2768
  sdk_type: "browser",
2779
2769
  platform: detectBrowser().name,
2780
2770
  algorithm: phantomOptions.algorithm || import_constants3.DEFAULT_AUTHENTICATOR_ALGORITHM
@@ -2805,7 +2795,7 @@ var BrowserAuthProvider = class {
2805
2795
  resolve();
2806
2796
  });
2807
2797
  }
2808
- resumeAuthFromRedirect(provider) {
2798
+ async resumeAuthFromRedirect(provider) {
2809
2799
  try {
2810
2800
  const walletId = this.urlParamsAccessor.getParam("wallet_id");
2811
2801
  const sessionId = this.urlParamsAccessor.getParam("session_id");
@@ -2880,14 +2870,14 @@ var BrowserAuthProvider = class {
2880
2870
  }
2881
2871
  );
2882
2872
  }
2883
- return {
2873
+ return Promise.resolve({
2884
2874
  walletId,
2885
2875
  organizationId,
2886
2876
  accountDerivationIndex: accountDerivationIndex ? parseInt(accountDerivationIndex) : 0,
2887
2877
  expiresInMs: expiresInMs ? parseInt(expiresInMs) : 0,
2888
2878
  authUserId: authUserId || void 0,
2889
2879
  provider
2890
- };
2880
+ });
2891
2881
  } catch (error) {
2892
2882
  sessionStorage.removeItem("phantom-auth-context");
2893
2883
  throw error;
@@ -2895,6 +2885,280 @@ var BrowserAuthProvider = class {
2895
2885
  }
2896
2886
  };
2897
2887
 
2888
+ // src/providers/embedded/adapters/Auth2AuthProvider.ts
2889
+ var import_auth2 = require("@phantom/auth2");
2890
+ var Auth2AuthProvider = class {
2891
+ constructor(stamper, storage, urlParamsAccessor, auth2ProviderOptions, kmsClientOptions) {
2892
+ this.stamper = stamper;
2893
+ this.storage = storage;
2894
+ this.urlParamsAccessor = urlParamsAccessor;
2895
+ this.auth2ProviderOptions = auth2ProviderOptions;
2896
+ this.kms = new import_auth2.Auth2KmsRpcClient(stamper, kmsClientOptions);
2897
+ }
2898
+ /** Redirect the browser. Extracted as a static method so tests can spy on it. */
2899
+ static navigate(url) {
2900
+ window.location.href = url;
2901
+ }
2902
+ /**
2903
+ * Builds the Auth2 /login/start URL and redirects the browser.
2904
+ *
2905
+ * Called by EmbeddedProvider.handleRedirectAuth() after the stamper has
2906
+ * already been initialized and a pending Session has been saved to storage.
2907
+ * We store the PKCE code_verifier and salt into that session so they survive
2908
+ * the page redirect without ever touching sessionStorage.
2909
+ */
2910
+ async authenticate(options) {
2911
+ if (!this.stamper.getKeyInfo()) {
2912
+ await this.stamper.init();
2913
+ }
2914
+ const keyPair = this.stamper.getCryptoKeyPair();
2915
+ if (!keyPair) {
2916
+ throw new Error("Stamper key pair not found.");
2917
+ }
2918
+ const codeVerifier = (0, import_auth2.createCodeVerifier)();
2919
+ const salt = (0, import_auth2.createSalt)();
2920
+ const session = await this.storage.getSession();
2921
+ if (!session) {
2922
+ throw new Error("Session not found.");
2923
+ }
2924
+ await this.storage.saveSession({ ...session, pkceCodeVerifier: codeVerifier, salt });
2925
+ const url = await (0, import_auth2.createConnectStartUrl)({
2926
+ keyPair,
2927
+ connectLoginUrl: this.auth2ProviderOptions.connectLoginUrl,
2928
+ clientId: this.auth2ProviderOptions.clientId,
2929
+ redirectUri: this.auth2ProviderOptions.redirectUri,
2930
+ sessionId: options.sessionId,
2931
+ provider: options.provider,
2932
+ codeVerifier,
2933
+ salt
2934
+ });
2935
+ Auth2AuthProvider.navigate(url);
2936
+ }
2937
+ /**
2938
+ * Processes the Auth2 callback after the browser returns from /login/start.
2939
+ *
2940
+ * Exchanges the authorization code for tokens, discovers the organization
2941
+ * and wallet via KMS RPC, then returns a completed AuthResult.
2942
+ */
2943
+ async resumeAuthFromRedirect(provider) {
2944
+ const code = this.urlParamsAccessor.getParam("code");
2945
+ if (!code) {
2946
+ return null;
2947
+ }
2948
+ if (!this.stamper.getKeyInfo()) {
2949
+ await this.stamper.init();
2950
+ }
2951
+ const session = await this.storage.getSession();
2952
+ if (!session) {
2953
+ throw new Error("Session not found.");
2954
+ }
2955
+ const codeVerifier = session?.pkceCodeVerifier;
2956
+ if (!codeVerifier) {
2957
+ return null;
2958
+ }
2959
+ const state = this.urlParamsAccessor.getParam("state");
2960
+ if (!state || state !== session.sessionId) {
2961
+ throw new Error("Missing or invalid Auth2 state parameter \u2014 possible CSRF attack.");
2962
+ }
2963
+ const error = this.urlParamsAccessor.getParam("error");
2964
+ if (error) {
2965
+ const description = this.urlParamsAccessor.getParam("error_description");
2966
+ throw new Error(`Auth2 callback error: ${description ?? error}`);
2967
+ }
2968
+ const { idToken, bearerToken, authUserId, expiresInMs } = await (0, import_auth2.exchangeAuthCode)({
2969
+ authApiBaseUrl: this.auth2ProviderOptions.authApiBaseUrl,
2970
+ clientId: this.auth2ProviderOptions.clientId,
2971
+ redirectUri: this.auth2ProviderOptions.redirectUri,
2972
+ code,
2973
+ codeVerifier
2974
+ });
2975
+ this.stamper.idToken = idToken;
2976
+ this.stamper.salt = session?.salt;
2977
+ await this.storage.saveSession({
2978
+ ...session,
2979
+ status: "completed",
2980
+ bearerToken,
2981
+ authUserId,
2982
+ pkceCodeVerifier: void 0,
2983
+ // no longer needed after code exchange
2984
+ salt: void 0
2985
+ // no longer needed after nonce binding is complete
2986
+ });
2987
+ const { organizationId, walletId } = await this.kms.discoverOrganizationAndWalletId(bearerToken, authUserId);
2988
+ return {
2989
+ walletId,
2990
+ organizationId,
2991
+ provider,
2992
+ accountDerivationIndex: 0,
2993
+ // discoverWalletId uses derivation index of 0.
2994
+ expiresInMs,
2995
+ authUserId,
2996
+ bearerToken
2997
+ };
2998
+ }
2999
+ };
3000
+
3001
+ // src/providers/embedded/adapters/Auth2Stamper.ts
3002
+ var import_bs582 = __toESM(require("bs58"));
3003
+ var import_base64url = require("@phantom/base64url");
3004
+ var import_sdk_types = require("@phantom/sdk-types");
3005
+ var STORE_NAME = "crypto-keys";
3006
+ var ACTIVE_KEY = "auth2-p256-signing-key";
3007
+ var Auth2Stamper = class {
3008
+ /**
3009
+ * @param dbName - IndexedDB database name (use a unique name per app to
3010
+ * avoid key collisions with other stampers, e.g. `phantom-auth2-<appId>`).
3011
+ */
3012
+ constructor(dbName) {
3013
+ this.dbName = dbName;
3014
+ this.db = null;
3015
+ this.keyPair = null;
3016
+ this._keyInfo = null;
3017
+ this.algorithm = import_sdk_types.Algorithm.secp256r1;
3018
+ this.type = "PKI";
3019
+ }
3020
+ async init() {
3021
+ await this.openDB();
3022
+ const stored = await this.loadKeyPair();
3023
+ if (stored) {
3024
+ this.keyPair = stored.keyPair;
3025
+ this._keyInfo = stored.keyInfo;
3026
+ return this._keyInfo;
3027
+ }
3028
+ return this.generateAndStore();
3029
+ }
3030
+ getKeyInfo() {
3031
+ return this._keyInfo;
3032
+ }
3033
+ getCryptoKeyPair() {
3034
+ return this.keyPair;
3035
+ }
3036
+ async stamp(params) {
3037
+ if (!this.keyPair || !this._keyInfo) {
3038
+ throw new Error("Auth2Stamper not initialized. Call init() first.");
3039
+ }
3040
+ const signatureRaw = await crypto.subtle.sign(
3041
+ { name: "ECDSA", hash: "SHA-256" },
3042
+ this.keyPair.privateKey,
3043
+ new Uint8Array(params.data)
3044
+ );
3045
+ const rawPublicKey = import_bs582.default.decode(this._keyInfo.publicKey);
3046
+ if (this.idToken === void 0 || this.salt === void 0) {
3047
+ throw new Error("Auth2Stamper not initialized with idToken or salt.");
3048
+ }
3049
+ const stampData = {
3050
+ kind: "OIDC",
3051
+ idToken: this.idToken,
3052
+ publicKey: (0, import_base64url.base64urlEncode)(rawPublicKey),
3053
+ algorithm: "Secp256r1",
3054
+ salt: this.salt,
3055
+ signature: (0, import_base64url.base64urlEncode)(new Uint8Array(signatureRaw))
3056
+ };
3057
+ return (0, import_base64url.base64urlEncode)(new TextEncoder().encode(JSON.stringify(stampData)));
3058
+ }
3059
+ async resetKeyPair() {
3060
+ await this.clearStoredKey();
3061
+ this.keyPair = null;
3062
+ this._keyInfo = null;
3063
+ return this.generateAndStore();
3064
+ }
3065
+ async clear() {
3066
+ await this.clearStoredKey();
3067
+ this.keyPair = null;
3068
+ this._keyInfo = null;
3069
+ }
3070
+ // Auth2 doesn't use key rotation; provide minimal no-op implementations.
3071
+ async rotateKeyPair() {
3072
+ return this.init();
3073
+ }
3074
+ // eslint-disable-next-line @typescript-eslint/require-await
3075
+ async commitRotation(authenticatorId) {
3076
+ if (this._keyInfo) {
3077
+ this._keyInfo.authenticatorId = authenticatorId;
3078
+ }
3079
+ }
3080
+ async rollbackRotation() {
3081
+ }
3082
+ async generateAndStore() {
3083
+ const keyPair = await crypto.subtle.generateKey(
3084
+ { name: "ECDSA", namedCurve: "P-256" },
3085
+ false,
3086
+ // non-extractable — private key never leaves Web Crypto
3087
+ ["sign", "verify"]
3088
+ );
3089
+ const rawPublicKey = new Uint8Array(await crypto.subtle.exportKey("raw", keyPair.publicKey));
3090
+ const publicKeyBase58 = import_bs582.default.encode(rawPublicKey);
3091
+ const keyIdBuffer = await crypto.subtle.digest("SHA-256", rawPublicKey.buffer);
3092
+ const keyId = (0, import_base64url.base64urlEncode)(new Uint8Array(keyIdBuffer)).substring(0, 16);
3093
+ this.keyPair = keyPair;
3094
+ this._keyInfo = {
3095
+ keyId,
3096
+ publicKey: publicKeyBase58,
3097
+ createdAt: Date.now()
3098
+ };
3099
+ await this.storeKeyPair(keyPair, this._keyInfo);
3100
+ return this._keyInfo;
3101
+ }
3102
+ async openDB() {
3103
+ return new Promise((resolve, reject) => {
3104
+ const request = indexedDB.open(this.dbName, 1);
3105
+ request.onsuccess = () => {
3106
+ this.db = request.result;
3107
+ resolve();
3108
+ };
3109
+ request.onerror = () => reject(request.error);
3110
+ request.onupgradeneeded = (event) => {
3111
+ const db = event.target.result;
3112
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
3113
+ db.createObjectStore(STORE_NAME);
3114
+ }
3115
+ };
3116
+ });
3117
+ }
3118
+ async loadKeyPair() {
3119
+ return new Promise((resolve, reject) => {
3120
+ if (!this.db) {
3121
+ throw new Error("Database not initialized");
3122
+ }
3123
+ const request = this.db.transaction([STORE_NAME], "readonly").objectStore(STORE_NAME).get(ACTIVE_KEY);
3124
+ request.onsuccess = () => {
3125
+ resolve(request.result ?? null);
3126
+ };
3127
+ request.onerror = () => {
3128
+ reject(request.error);
3129
+ };
3130
+ });
3131
+ }
3132
+ async storeKeyPair(keyPair, keyInfo) {
3133
+ return new Promise((resolve, reject) => {
3134
+ if (!this.db) {
3135
+ throw new Error("Database not initialized");
3136
+ }
3137
+ const request = this.db.transaction([STORE_NAME], "readwrite").objectStore(STORE_NAME).put({ keyPair, keyInfo }, ACTIVE_KEY);
3138
+ request.onsuccess = () => {
3139
+ resolve();
3140
+ };
3141
+ request.onerror = () => {
3142
+ reject(request.error);
3143
+ };
3144
+ });
3145
+ }
3146
+ async clearStoredKey() {
3147
+ return new Promise((resolve, reject) => {
3148
+ if (!this.db) {
3149
+ throw new Error("Database not initialized");
3150
+ }
3151
+ const request = this.db.transaction([STORE_NAME], "readwrite").objectStore(STORE_NAME).delete(ACTIVE_KEY);
3152
+ request.onsuccess = () => {
3153
+ resolve();
3154
+ };
3155
+ request.onerror = () => {
3156
+ reject(request.error);
3157
+ };
3158
+ });
3159
+ }
3160
+ };
3161
+
2898
3162
  // src/providers/embedded/adapters/phantom-app.ts
2899
3163
  var import_browser_injected_sdk4 = require("@phantom/browser-injected-sdk");
2900
3164
 
@@ -3018,16 +3282,32 @@ var EmbeddedProvider = class extends import_embedded_provider_core.EmbeddedProvi
3018
3282
  constructor(config) {
3019
3283
  debug.log(DebugCategory.EMBEDDED_PROVIDER, "Initializing Browser EmbeddedProvider", { config });
3020
3284
  const urlParamsAccessor = new BrowserURLParamsAccessor();
3021
- const stamper = new import_indexed_db_stamper.IndexedDbStamper({
3285
+ const storage = new BrowserStorage();
3286
+ const stamper = config.unstable__auth2Options ? new Auth2Stamper(`phantom-auth2-${config.appId}`) : new import_indexed_db_stamper.IndexedDbStamper({
3022
3287
  dbName: `phantom-embedded-sdk-${config.appId}`,
3023
3288
  storeName: "crypto-keys",
3024
3289
  keyName: "signing-key"
3025
3290
  });
3026
3291
  const platformName = getPlatformName();
3027
3292
  const { name: browserName, version } = detectBrowser();
3293
+ const authProvider = config.unstable__auth2Options && config.authOptions?.authUrl && config.authOptions?.redirectUrl && stamper instanceof Auth2Stamper ? new Auth2AuthProvider(
3294
+ stamper,
3295
+ storage,
3296
+ urlParamsAccessor,
3297
+ {
3298
+ redirectUri: config.authOptions.redirectUrl,
3299
+ connectLoginUrl: config.authOptions.authUrl,
3300
+ clientId: config.unstable__auth2Options.clientId,
3301
+ authApiBaseUrl: config.unstable__auth2Options.authApiBaseUrl
3302
+ },
3303
+ {
3304
+ apiBaseUrl: config.apiBaseUrl,
3305
+ appId: config.appId
3306
+ }
3307
+ ) : new BrowserAuthProvider(urlParamsAccessor);
3028
3308
  const platform = {
3029
- storage: new BrowserStorage(),
3030
- authProvider: new BrowserAuthProvider(urlParamsAccessor),
3309
+ storage,
3310
+ authProvider,
3031
3311
  phantomAppProvider: new BrowserPhantomAppProvider(),
3032
3312
  urlParamsAccessor,
3033
3313
  stamper,
@@ -3035,13 +3315,12 @@ var EmbeddedProvider = class extends import_embedded_provider_core.EmbeddedProvi
3035
3315
  // Use detected browser name and version for identification
3036
3316
  analyticsHeaders: {
3037
3317
  [import_constants4.ANALYTICS_HEADERS.SDK_TYPE]: "browser",
3038
- [import_constants4.ANALYTICS_HEADERS.PLATFORM]: browserName,
3039
- // firefox, chrome, safari, etc.
3318
+ [import_constants4.ANALYTICS_HEADERS.PLATFORM]: "ext-sdk",
3040
3319
  [import_constants4.ANALYTICS_HEADERS.PLATFORM_VERSION]: version,
3041
- // Full user agent for more detailed info
3320
+ [import_constants4.ANALYTICS_HEADERS.CLIENT]: browserName,
3042
3321
  [import_constants4.ANALYTICS_HEADERS.APP_ID]: config.appId,
3043
3322
  [import_constants4.ANALYTICS_HEADERS.WALLET_TYPE]: config.embeddedWalletType,
3044
- [import_constants4.ANALYTICS_HEADERS.SDK_VERSION]: "1.0.3"
3323
+ [import_constants4.ANALYTICS_HEADERS.SDK_VERSION]: "1.0.4"
3045
3324
  // Replaced at build time
3046
3325
  }
3047
3326
  };
@@ -3459,6 +3738,7 @@ var ProviderManager = class {
3459
3738
  authUrl,
3460
3739
  redirectUrl: this.config.authOptions?.redirectUrl || this.getValidatedCurrentUrl()
3461
3740
  },
3741
+ unstable__auth2Options: this.config.unstable__auth2Options,
3462
3742
  embeddedWalletType: embeddedWalletType || import_constants5.DEFAULT_EMBEDDED_WALLET_TYPE,
3463
3743
  addressTypes: this.config.addressTypes || [import_client.AddressType.solana]
3464
3744
  });
package/dist/index.mjs CHANGED
@@ -1755,16 +1755,11 @@ var InjectedProvider = class {
1755
1755
  walletName: walletInfo.name
1756
1756
  });
1757
1757
  } catch (err) {
1758
- debug.warn(DebugCategory.INJECTED_PROVIDER, "Failed to connect Solana, stopping", {
1758
+ debug.warn(DebugCategory.INJECTED_PROVIDER, "Failed to connect Solana, continuing with other chains", {
1759
1759
  error: err,
1760
1760
  walletId: this.selectedWalletId,
1761
1761
  walletName: walletInfo.name
1762
1762
  });
1763
- this.emit("connect_error", {
1764
- error: err instanceof Error ? err.message : "Failed to connect",
1765
- source: options?.skipEventListeners ? "auto-connect" : "manual-connect"
1766
- });
1767
- throw err;
1768
1763
  }
1769
1764
  }
1770
1765
  if (this.addressTypes.includes(AddressType3.ethereum) && walletInfo.providers?.ethereum) {
@@ -1794,16 +1789,11 @@ var InjectedProvider = class {
1794
1789
  });
1795
1790
  }
1796
1791
  } catch (err) {
1797
- debug.warn(DebugCategory.INJECTED_PROVIDER, "Failed to connect Ethereum, stopping", {
1792
+ debug.warn(DebugCategory.INJECTED_PROVIDER, "Failed to connect Ethereum, continuing with other chains", {
1798
1793
  error: err,
1799
1794
  walletId: this.selectedWalletId,
1800
1795
  walletName: walletInfo.name
1801
1796
  });
1802
- this.emit("connect_error", {
1803
- error: err instanceof Error ? err.message : "Failed to connect",
1804
- source: options?.skipEventListeners ? "auto-connect" : "manual-connect"
1805
- });
1806
- throw err;
1807
1797
  }
1808
1798
  }
1809
1799
  return connectedAddresses;
@@ -2724,7 +2714,7 @@ var BrowserAuthProvider = class {
2724
2714
  // OAuth session management - defaults to allow refresh unless explicitly clearing after logout
2725
2715
  clear_previous_session: (phantomOptions.clearPreviousSession ?? false).toString(),
2726
2716
  allow_refresh: (phantomOptions.allowRefresh ?? true).toString(),
2727
- sdk_version: "1.0.3",
2717
+ sdk_version: "1.0.4",
2728
2718
  sdk_type: "browser",
2729
2719
  platform: detectBrowser().name,
2730
2720
  algorithm: phantomOptions.algorithm || DEFAULT_AUTHENTICATOR_ALGORITHM
@@ -2755,7 +2745,7 @@ var BrowserAuthProvider = class {
2755
2745
  resolve();
2756
2746
  });
2757
2747
  }
2758
- resumeAuthFromRedirect(provider) {
2748
+ async resumeAuthFromRedirect(provider) {
2759
2749
  try {
2760
2750
  const walletId = this.urlParamsAccessor.getParam("wallet_id");
2761
2751
  const sessionId = this.urlParamsAccessor.getParam("session_id");
@@ -2830,14 +2820,14 @@ var BrowserAuthProvider = class {
2830
2820
  }
2831
2821
  );
2832
2822
  }
2833
- return {
2823
+ return Promise.resolve({
2834
2824
  walletId,
2835
2825
  organizationId,
2836
2826
  accountDerivationIndex: accountDerivationIndex ? parseInt(accountDerivationIndex) : 0,
2837
2827
  expiresInMs: expiresInMs ? parseInt(expiresInMs) : 0,
2838
2828
  authUserId: authUserId || void 0,
2839
2829
  provider
2840
- };
2830
+ });
2841
2831
  } catch (error) {
2842
2832
  sessionStorage.removeItem("phantom-auth-context");
2843
2833
  throw error;
@@ -2845,6 +2835,286 @@ var BrowserAuthProvider = class {
2845
2835
  }
2846
2836
  };
2847
2837
 
2838
+ // src/providers/embedded/adapters/Auth2AuthProvider.ts
2839
+ import {
2840
+ createCodeVerifier,
2841
+ createSalt,
2842
+ createConnectStartUrl,
2843
+ exchangeAuthCode,
2844
+ Auth2KmsRpcClient
2845
+ } from "@phantom/auth2";
2846
+ var Auth2AuthProvider = class {
2847
+ constructor(stamper, storage, urlParamsAccessor, auth2ProviderOptions, kmsClientOptions) {
2848
+ this.stamper = stamper;
2849
+ this.storage = storage;
2850
+ this.urlParamsAccessor = urlParamsAccessor;
2851
+ this.auth2ProviderOptions = auth2ProviderOptions;
2852
+ this.kms = new Auth2KmsRpcClient(stamper, kmsClientOptions);
2853
+ }
2854
+ /** Redirect the browser. Extracted as a static method so tests can spy on it. */
2855
+ static navigate(url) {
2856
+ window.location.href = url;
2857
+ }
2858
+ /**
2859
+ * Builds the Auth2 /login/start URL and redirects the browser.
2860
+ *
2861
+ * Called by EmbeddedProvider.handleRedirectAuth() after the stamper has
2862
+ * already been initialized and a pending Session has been saved to storage.
2863
+ * We store the PKCE code_verifier and salt into that session so they survive
2864
+ * the page redirect without ever touching sessionStorage.
2865
+ */
2866
+ async authenticate(options) {
2867
+ if (!this.stamper.getKeyInfo()) {
2868
+ await this.stamper.init();
2869
+ }
2870
+ const keyPair = this.stamper.getCryptoKeyPair();
2871
+ if (!keyPair) {
2872
+ throw new Error("Stamper key pair not found.");
2873
+ }
2874
+ const codeVerifier = createCodeVerifier();
2875
+ const salt = createSalt();
2876
+ const session = await this.storage.getSession();
2877
+ if (!session) {
2878
+ throw new Error("Session not found.");
2879
+ }
2880
+ await this.storage.saveSession({ ...session, pkceCodeVerifier: codeVerifier, salt });
2881
+ const url = await createConnectStartUrl({
2882
+ keyPair,
2883
+ connectLoginUrl: this.auth2ProviderOptions.connectLoginUrl,
2884
+ clientId: this.auth2ProviderOptions.clientId,
2885
+ redirectUri: this.auth2ProviderOptions.redirectUri,
2886
+ sessionId: options.sessionId,
2887
+ provider: options.provider,
2888
+ codeVerifier,
2889
+ salt
2890
+ });
2891
+ Auth2AuthProvider.navigate(url);
2892
+ }
2893
+ /**
2894
+ * Processes the Auth2 callback after the browser returns from /login/start.
2895
+ *
2896
+ * Exchanges the authorization code for tokens, discovers the organization
2897
+ * and wallet via KMS RPC, then returns a completed AuthResult.
2898
+ */
2899
+ async resumeAuthFromRedirect(provider) {
2900
+ const code = this.urlParamsAccessor.getParam("code");
2901
+ if (!code) {
2902
+ return null;
2903
+ }
2904
+ if (!this.stamper.getKeyInfo()) {
2905
+ await this.stamper.init();
2906
+ }
2907
+ const session = await this.storage.getSession();
2908
+ if (!session) {
2909
+ throw new Error("Session not found.");
2910
+ }
2911
+ const codeVerifier = session?.pkceCodeVerifier;
2912
+ if (!codeVerifier) {
2913
+ return null;
2914
+ }
2915
+ const state = this.urlParamsAccessor.getParam("state");
2916
+ if (!state || state !== session.sessionId) {
2917
+ throw new Error("Missing or invalid Auth2 state parameter \u2014 possible CSRF attack.");
2918
+ }
2919
+ const error = this.urlParamsAccessor.getParam("error");
2920
+ if (error) {
2921
+ const description = this.urlParamsAccessor.getParam("error_description");
2922
+ throw new Error(`Auth2 callback error: ${description ?? error}`);
2923
+ }
2924
+ const { idToken, bearerToken, authUserId, expiresInMs } = await exchangeAuthCode({
2925
+ authApiBaseUrl: this.auth2ProviderOptions.authApiBaseUrl,
2926
+ clientId: this.auth2ProviderOptions.clientId,
2927
+ redirectUri: this.auth2ProviderOptions.redirectUri,
2928
+ code,
2929
+ codeVerifier
2930
+ });
2931
+ this.stamper.idToken = idToken;
2932
+ this.stamper.salt = session?.salt;
2933
+ await this.storage.saveSession({
2934
+ ...session,
2935
+ status: "completed",
2936
+ bearerToken,
2937
+ authUserId,
2938
+ pkceCodeVerifier: void 0,
2939
+ // no longer needed after code exchange
2940
+ salt: void 0
2941
+ // no longer needed after nonce binding is complete
2942
+ });
2943
+ const { organizationId, walletId } = await this.kms.discoverOrganizationAndWalletId(bearerToken, authUserId);
2944
+ return {
2945
+ walletId,
2946
+ organizationId,
2947
+ provider,
2948
+ accountDerivationIndex: 0,
2949
+ // discoverWalletId uses derivation index of 0.
2950
+ expiresInMs,
2951
+ authUserId,
2952
+ bearerToken
2953
+ };
2954
+ }
2955
+ };
2956
+
2957
+ // src/providers/embedded/adapters/Auth2Stamper.ts
2958
+ import bs582 from "bs58";
2959
+ import { base64urlEncode } from "@phantom/base64url";
2960
+ import { Algorithm } from "@phantom/sdk-types";
2961
+ var STORE_NAME = "crypto-keys";
2962
+ var ACTIVE_KEY = "auth2-p256-signing-key";
2963
+ var Auth2Stamper = class {
2964
+ /**
2965
+ * @param dbName - IndexedDB database name (use a unique name per app to
2966
+ * avoid key collisions with other stampers, e.g. `phantom-auth2-<appId>`).
2967
+ */
2968
+ constructor(dbName) {
2969
+ this.dbName = dbName;
2970
+ this.db = null;
2971
+ this.keyPair = null;
2972
+ this._keyInfo = null;
2973
+ this.algorithm = Algorithm.secp256r1;
2974
+ this.type = "PKI";
2975
+ }
2976
+ async init() {
2977
+ await this.openDB();
2978
+ const stored = await this.loadKeyPair();
2979
+ if (stored) {
2980
+ this.keyPair = stored.keyPair;
2981
+ this._keyInfo = stored.keyInfo;
2982
+ return this._keyInfo;
2983
+ }
2984
+ return this.generateAndStore();
2985
+ }
2986
+ getKeyInfo() {
2987
+ return this._keyInfo;
2988
+ }
2989
+ getCryptoKeyPair() {
2990
+ return this.keyPair;
2991
+ }
2992
+ async stamp(params) {
2993
+ if (!this.keyPair || !this._keyInfo) {
2994
+ throw new Error("Auth2Stamper not initialized. Call init() first.");
2995
+ }
2996
+ const signatureRaw = await crypto.subtle.sign(
2997
+ { name: "ECDSA", hash: "SHA-256" },
2998
+ this.keyPair.privateKey,
2999
+ new Uint8Array(params.data)
3000
+ );
3001
+ const rawPublicKey = bs582.decode(this._keyInfo.publicKey);
3002
+ if (this.idToken === void 0 || this.salt === void 0) {
3003
+ throw new Error("Auth2Stamper not initialized with idToken or salt.");
3004
+ }
3005
+ const stampData = {
3006
+ kind: "OIDC",
3007
+ idToken: this.idToken,
3008
+ publicKey: base64urlEncode(rawPublicKey),
3009
+ algorithm: "Secp256r1",
3010
+ salt: this.salt,
3011
+ signature: base64urlEncode(new Uint8Array(signatureRaw))
3012
+ };
3013
+ return base64urlEncode(new TextEncoder().encode(JSON.stringify(stampData)));
3014
+ }
3015
+ async resetKeyPair() {
3016
+ await this.clearStoredKey();
3017
+ this.keyPair = null;
3018
+ this._keyInfo = null;
3019
+ return this.generateAndStore();
3020
+ }
3021
+ async clear() {
3022
+ await this.clearStoredKey();
3023
+ this.keyPair = null;
3024
+ this._keyInfo = null;
3025
+ }
3026
+ // Auth2 doesn't use key rotation; provide minimal no-op implementations.
3027
+ async rotateKeyPair() {
3028
+ return this.init();
3029
+ }
3030
+ // eslint-disable-next-line @typescript-eslint/require-await
3031
+ async commitRotation(authenticatorId) {
3032
+ if (this._keyInfo) {
3033
+ this._keyInfo.authenticatorId = authenticatorId;
3034
+ }
3035
+ }
3036
+ async rollbackRotation() {
3037
+ }
3038
+ async generateAndStore() {
3039
+ const keyPair = await crypto.subtle.generateKey(
3040
+ { name: "ECDSA", namedCurve: "P-256" },
3041
+ false,
3042
+ // non-extractable — private key never leaves Web Crypto
3043
+ ["sign", "verify"]
3044
+ );
3045
+ const rawPublicKey = new Uint8Array(await crypto.subtle.exportKey("raw", keyPair.publicKey));
3046
+ const publicKeyBase58 = bs582.encode(rawPublicKey);
3047
+ const keyIdBuffer = await crypto.subtle.digest("SHA-256", rawPublicKey.buffer);
3048
+ const keyId = base64urlEncode(new Uint8Array(keyIdBuffer)).substring(0, 16);
3049
+ this.keyPair = keyPair;
3050
+ this._keyInfo = {
3051
+ keyId,
3052
+ publicKey: publicKeyBase58,
3053
+ createdAt: Date.now()
3054
+ };
3055
+ await this.storeKeyPair(keyPair, this._keyInfo);
3056
+ return this._keyInfo;
3057
+ }
3058
+ async openDB() {
3059
+ return new Promise((resolve, reject) => {
3060
+ const request = indexedDB.open(this.dbName, 1);
3061
+ request.onsuccess = () => {
3062
+ this.db = request.result;
3063
+ resolve();
3064
+ };
3065
+ request.onerror = () => reject(request.error);
3066
+ request.onupgradeneeded = (event) => {
3067
+ const db = event.target.result;
3068
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
3069
+ db.createObjectStore(STORE_NAME);
3070
+ }
3071
+ };
3072
+ });
3073
+ }
3074
+ async loadKeyPair() {
3075
+ return new Promise((resolve, reject) => {
3076
+ if (!this.db) {
3077
+ throw new Error("Database not initialized");
3078
+ }
3079
+ const request = this.db.transaction([STORE_NAME], "readonly").objectStore(STORE_NAME).get(ACTIVE_KEY);
3080
+ request.onsuccess = () => {
3081
+ resolve(request.result ?? null);
3082
+ };
3083
+ request.onerror = () => {
3084
+ reject(request.error);
3085
+ };
3086
+ });
3087
+ }
3088
+ async storeKeyPair(keyPair, keyInfo) {
3089
+ return new Promise((resolve, reject) => {
3090
+ if (!this.db) {
3091
+ throw new Error("Database not initialized");
3092
+ }
3093
+ const request = this.db.transaction([STORE_NAME], "readwrite").objectStore(STORE_NAME).put({ keyPair, keyInfo }, ACTIVE_KEY);
3094
+ request.onsuccess = () => {
3095
+ resolve();
3096
+ };
3097
+ request.onerror = () => {
3098
+ reject(request.error);
3099
+ };
3100
+ });
3101
+ }
3102
+ async clearStoredKey() {
3103
+ return new Promise((resolve, reject) => {
3104
+ if (!this.db) {
3105
+ throw new Error("Database not initialized");
3106
+ }
3107
+ const request = this.db.transaction([STORE_NAME], "readwrite").objectStore(STORE_NAME).delete(ACTIVE_KEY);
3108
+ request.onsuccess = () => {
3109
+ resolve();
3110
+ };
3111
+ request.onerror = () => {
3112
+ reject(request.error);
3113
+ };
3114
+ });
3115
+ }
3116
+ };
3117
+
2848
3118
  // src/providers/embedded/adapters/phantom-app.ts
2849
3119
  import { isPhantomExtensionInstalled as isPhantomExtensionInstalled3 } from "@phantom/browser-injected-sdk";
2850
3120
 
@@ -2968,16 +3238,32 @@ var EmbeddedProvider = class extends CoreEmbeddedProvider {
2968
3238
  constructor(config) {
2969
3239
  debug.log(DebugCategory.EMBEDDED_PROVIDER, "Initializing Browser EmbeddedProvider", { config });
2970
3240
  const urlParamsAccessor = new BrowserURLParamsAccessor();
2971
- const stamper = new IndexedDbStamper({
3241
+ const storage = new BrowserStorage();
3242
+ const stamper = config.unstable__auth2Options ? new Auth2Stamper(`phantom-auth2-${config.appId}`) : new IndexedDbStamper({
2972
3243
  dbName: `phantom-embedded-sdk-${config.appId}`,
2973
3244
  storeName: "crypto-keys",
2974
3245
  keyName: "signing-key"
2975
3246
  });
2976
3247
  const platformName = getPlatformName();
2977
3248
  const { name: browserName, version } = detectBrowser();
3249
+ const authProvider = config.unstable__auth2Options && config.authOptions?.authUrl && config.authOptions?.redirectUrl && stamper instanceof Auth2Stamper ? new Auth2AuthProvider(
3250
+ stamper,
3251
+ storage,
3252
+ urlParamsAccessor,
3253
+ {
3254
+ redirectUri: config.authOptions.redirectUrl,
3255
+ connectLoginUrl: config.authOptions.authUrl,
3256
+ clientId: config.unstable__auth2Options.clientId,
3257
+ authApiBaseUrl: config.unstable__auth2Options.authApiBaseUrl
3258
+ },
3259
+ {
3260
+ apiBaseUrl: config.apiBaseUrl,
3261
+ appId: config.appId
3262
+ }
3263
+ ) : new BrowserAuthProvider(urlParamsAccessor);
2978
3264
  const platform = {
2979
- storage: new BrowserStorage(),
2980
- authProvider: new BrowserAuthProvider(urlParamsAccessor),
3265
+ storage,
3266
+ authProvider,
2981
3267
  phantomAppProvider: new BrowserPhantomAppProvider(),
2982
3268
  urlParamsAccessor,
2983
3269
  stamper,
@@ -2985,13 +3271,12 @@ var EmbeddedProvider = class extends CoreEmbeddedProvider {
2985
3271
  // Use detected browser name and version for identification
2986
3272
  analyticsHeaders: {
2987
3273
  [ANALYTICS_HEADERS.SDK_TYPE]: "browser",
2988
- [ANALYTICS_HEADERS.PLATFORM]: browserName,
2989
- // firefox, chrome, safari, etc.
3274
+ [ANALYTICS_HEADERS.PLATFORM]: "ext-sdk",
2990
3275
  [ANALYTICS_HEADERS.PLATFORM_VERSION]: version,
2991
- // Full user agent for more detailed info
3276
+ [ANALYTICS_HEADERS.CLIENT]: browserName,
2992
3277
  [ANALYTICS_HEADERS.APP_ID]: config.appId,
2993
3278
  [ANALYTICS_HEADERS.WALLET_TYPE]: config.embeddedWalletType,
2994
- [ANALYTICS_HEADERS.SDK_VERSION]: "1.0.3"
3279
+ [ANALYTICS_HEADERS.SDK_VERSION]: "1.0.4"
2995
3280
  // Replaced at build time
2996
3281
  }
2997
3282
  };
@@ -3411,6 +3696,7 @@ var ProviderManager = class {
3411
3696
  authUrl,
3412
3697
  redirectUrl: this.config.authOptions?.redirectUrl || this.getValidatedCurrentUrl()
3413
3698
  },
3699
+ unstable__auth2Options: this.config.unstable__auth2Options,
3414
3700
  embeddedWalletType: embeddedWalletType || DEFAULT_EMBEDDED_WALLET_TYPE,
3415
3701
  addressTypes: this.config.addressTypes || [AddressType.solana]
3416
3702
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phantom/browser-sdk",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Browser SDK for Phantom Wallet",
5
5
  "repository": {
6
6
  "type": "git",
@@ -33,15 +33,16 @@
33
33
  "prettier": "prettier --write \"src/**/*.{ts,tsx}\""
34
34
  },
35
35
  "dependencies": {
36
- "@phantom/base64url": "^1.0.3",
37
- "@phantom/browser-injected-sdk": "^1.0.3",
38
- "@phantom/chain-interfaces": "^1.0.3",
39
- "@phantom/client": "^1.0.3",
40
- "@phantom/constants": "^1.0.3",
41
- "@phantom/embedded-provider-core": "^1.0.3",
42
- "@phantom/indexed-db-stamper": "^1.0.3",
43
- "@phantom/parsers": "^1.0.3",
44
- "@phantom/sdk-types": "^1.0.3",
36
+ "@phantom/auth2": "^1.0.0",
37
+ "@phantom/base64url": "^1.0.4",
38
+ "@phantom/browser-injected-sdk": "^1.0.4",
39
+ "@phantom/chain-interfaces": "^1.0.4",
40
+ "@phantom/client": "^1.0.4",
41
+ "@phantom/constants": "^1.0.4",
42
+ "@phantom/embedded-provider-core": "^1.0.4",
43
+ "@phantom/indexed-db-stamper": "^1.0.4",
44
+ "@phantom/parsers": "^1.0.4",
45
+ "@phantom/sdk-types": "^1.0.4",
45
46
  "axios": "^1.10.0",
46
47
  "bs58": "^6.0.0",
47
48
  "buffer": "^6.0.3",