@phantom/browser-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
@@ -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.5",
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,294 @@ 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 into that session so it survives the page
2908
+ * 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 session = await this.storage.getSession();
2920
+ if (!session) {
2921
+ throw new Error("Session not found.");
2922
+ }
2923
+ await this.storage.saveSession({ ...session, pkceCodeVerifier: codeVerifier });
2924
+ const url = await (0, import_auth2.createConnectStartUrl)({
2925
+ keyPair,
2926
+ connectLoginUrl: this.auth2ProviderOptions.connectLoginUrl,
2927
+ clientId: this.auth2ProviderOptions.clientId,
2928
+ redirectUri: this.auth2ProviderOptions.redirectUri,
2929
+ sessionId: options.sessionId,
2930
+ provider: options.provider,
2931
+ codeVerifier,
2932
+ // The P-256 ephemeral key is unique per wallet, so no additional salt is needed.
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
+ await this.stamper.setIdToken(idToken);
2976
+ await this.storage.saveSession({
2977
+ ...session,
2978
+ status: "completed",
2979
+ bearerToken,
2980
+ authUserId,
2981
+ pkceCodeVerifier: void 0
2982
+ // no longer needed after code exchange
2983
+ });
2984
+ const { organizationId, walletId } = await this.kms.discoverOrganizationAndWalletId(bearerToken, authUserId);
2985
+ return {
2986
+ walletId,
2987
+ organizationId,
2988
+ provider,
2989
+ accountDerivationIndex: 0,
2990
+ // discoverWalletId uses derivation index of 0.
2991
+ expiresInMs,
2992
+ authUserId,
2993
+ bearerToken
2994
+ };
2995
+ }
2996
+ };
2997
+
2998
+ // src/providers/embedded/adapters/Auth2Stamper.ts
2999
+ var import_bs582 = __toESM(require("bs58"));
3000
+ var import_base64url = require("@phantom/base64url");
3001
+ var import_sdk_types = require("@phantom/sdk-types");
3002
+ var STORE_NAME = "crypto-keys";
3003
+ var ACTIVE_KEY = "auth2-p256-signing-key";
3004
+ var Auth2Stamper = class {
3005
+ /**
3006
+ * @param dbName - IndexedDB database name (use a unique name per app to
3007
+ * avoid key collisions with other stampers, e.g. `phantom-auth2-<appId>`).
3008
+ */
3009
+ constructor(dbName) {
3010
+ this.dbName = dbName;
3011
+ this.db = null;
3012
+ this._keyPair = null;
3013
+ this._keyInfo = null;
3014
+ this._idToken = null;
3015
+ this.algorithm = import_sdk_types.Algorithm.secp256r1;
3016
+ this.type = "OIDC";
3017
+ }
3018
+ async init() {
3019
+ await this.openDB();
3020
+ const stored = await this.loadRecord();
3021
+ if (stored) {
3022
+ this._keyPair = stored.keyPair;
3023
+ this._keyInfo = stored.keyInfo;
3024
+ if (stored.idToken) {
3025
+ this._idToken = stored.idToken;
3026
+ }
3027
+ return this._keyInfo;
3028
+ }
3029
+ return this.generateAndStore();
3030
+ }
3031
+ getKeyInfo() {
3032
+ return this._keyInfo;
3033
+ }
3034
+ getCryptoKeyPair() {
3035
+ return this._keyPair;
3036
+ }
3037
+ /**
3038
+ * Arms the stamper with the OIDC id token for subsequent KMS stamp() calls.
3039
+ *
3040
+ * Persists the token to IndexedDB alongside the key pair so that
3041
+ * auto-connect can restore it on the next page load without a new login.
3042
+ */
3043
+ async setIdToken(idToken) {
3044
+ if (!this.db) {
3045
+ await this.openDB();
3046
+ }
3047
+ this._idToken = idToken;
3048
+ const existing = await this.loadRecord();
3049
+ if (existing) {
3050
+ await this.storeRecord({ ...existing, idToken });
3051
+ }
3052
+ }
3053
+ async stamp(params) {
3054
+ if (!this._keyPair || !this._keyInfo || this._idToken === null) {
3055
+ throw new Error("Auth2Stamper not initialized. Call init() first.");
3056
+ }
3057
+ const signatureRaw = await crypto.subtle.sign(
3058
+ { name: "ECDSA", hash: "SHA-256" },
3059
+ this._keyPair.privateKey,
3060
+ new Uint8Array(params.data)
3061
+ );
3062
+ const rawPublicKey = import_bs582.default.decode(this._keyInfo.publicKey);
3063
+ const stampData = {
3064
+ kind: this.type,
3065
+ idToken: this._idToken,
3066
+ publicKey: (0, import_base64url.base64urlEncode)(rawPublicKey),
3067
+ algorithm: this.algorithm,
3068
+ // The P-256 ephemeral key is unique per wallet, so no additional salt is needed.
3069
+ salt: "",
3070
+ signature: (0, import_base64url.base64urlEncode)(new Uint8Array(signatureRaw))
3071
+ };
3072
+ return (0, import_base64url.base64urlEncode)(new TextEncoder().encode(JSON.stringify(stampData)));
3073
+ }
3074
+ async resetKeyPair() {
3075
+ await this.clear();
3076
+ return this.generateAndStore();
3077
+ }
3078
+ async clear() {
3079
+ await this.clearStoredRecord();
3080
+ this._keyPair = null;
3081
+ this._keyInfo = null;
3082
+ this._idToken = null;
3083
+ }
3084
+ // Auth2 doesn't use key rotation; provide minimal no-op implementations.
3085
+ async rotateKeyPair() {
3086
+ return this.init();
3087
+ }
3088
+ // eslint-disable-next-line @typescript-eslint/require-await
3089
+ async commitRotation(authenticatorId) {
3090
+ if (this._keyInfo) {
3091
+ this._keyInfo.authenticatorId = authenticatorId;
3092
+ }
3093
+ }
3094
+ async rollbackRotation() {
3095
+ }
3096
+ async generateAndStore() {
3097
+ const keyPair = await crypto.subtle.generateKey(
3098
+ { name: "ECDSA", namedCurve: "P-256" },
3099
+ false,
3100
+ // non-extractable — private key never leaves Web Crypto
3101
+ ["sign", "verify"]
3102
+ );
3103
+ const rawPublicKey = new Uint8Array(await crypto.subtle.exportKey("raw", keyPair.publicKey));
3104
+ const publicKeyBase58 = import_bs582.default.encode(rawPublicKey);
3105
+ const keyIdBuffer = await crypto.subtle.digest("SHA-256", rawPublicKey.buffer);
3106
+ const keyId = (0, import_base64url.base64urlEncode)(new Uint8Array(keyIdBuffer)).substring(0, 16);
3107
+ this._keyPair = keyPair;
3108
+ this._keyInfo = {
3109
+ keyId,
3110
+ publicKey: publicKeyBase58,
3111
+ createdAt: Date.now()
3112
+ };
3113
+ await this.storeRecord({ keyPair, keyInfo: this._keyInfo });
3114
+ return this._keyInfo;
3115
+ }
3116
+ async openDB() {
3117
+ return new Promise((resolve, reject) => {
3118
+ const request = indexedDB.open(this.dbName, 1);
3119
+ request.onsuccess = () => {
3120
+ this.db = request.result;
3121
+ resolve();
3122
+ };
3123
+ request.onerror = () => reject(request.error);
3124
+ request.onupgradeneeded = (event) => {
3125
+ const db = event.target.result;
3126
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
3127
+ db.createObjectStore(STORE_NAME);
3128
+ }
3129
+ };
3130
+ });
3131
+ }
3132
+ async loadRecord() {
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], "readonly").objectStore(STORE_NAME).get(ACTIVE_KEY);
3138
+ request.onsuccess = () => {
3139
+ resolve(request.result ?? null);
3140
+ };
3141
+ request.onerror = () => {
3142
+ reject(request.error);
3143
+ };
3144
+ });
3145
+ }
3146
+ async storeRecord(record) {
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).put(record, ACTIVE_KEY);
3152
+ request.onsuccess = () => {
3153
+ resolve();
3154
+ };
3155
+ request.onerror = () => {
3156
+ reject(request.error);
3157
+ };
3158
+ });
3159
+ }
3160
+ async clearStoredRecord() {
3161
+ return new Promise((resolve, reject) => {
3162
+ if (!this.db) {
3163
+ throw new Error("Database not initialized");
3164
+ }
3165
+ const request = this.db.transaction([STORE_NAME], "readwrite").objectStore(STORE_NAME).delete(ACTIVE_KEY);
3166
+ request.onsuccess = () => {
3167
+ resolve();
3168
+ };
3169
+ request.onerror = () => {
3170
+ reject(request.error);
3171
+ };
3172
+ });
3173
+ }
3174
+ };
3175
+
2898
3176
  // src/providers/embedded/adapters/phantom-app.ts
2899
3177
  var import_browser_injected_sdk4 = require("@phantom/browser-injected-sdk");
2900
3178
 
@@ -3018,16 +3296,32 @@ var EmbeddedProvider = class extends import_embedded_provider_core.EmbeddedProvi
3018
3296
  constructor(config) {
3019
3297
  debug.log(DebugCategory.EMBEDDED_PROVIDER, "Initializing Browser EmbeddedProvider", { config });
3020
3298
  const urlParamsAccessor = new BrowserURLParamsAccessor();
3021
- const stamper = new import_indexed_db_stamper.IndexedDbStamper({
3299
+ const storage = new BrowserStorage();
3300
+ const stamper = config.unstable__auth2Options ? new Auth2Stamper(`phantom-auth2-${config.appId}`) : new import_indexed_db_stamper.IndexedDbStamper({
3022
3301
  dbName: `phantom-embedded-sdk-${config.appId}`,
3023
3302
  storeName: "crypto-keys",
3024
3303
  keyName: "signing-key"
3025
3304
  });
3026
3305
  const platformName = getPlatformName();
3027
3306
  const { name: browserName, version } = detectBrowser();
3307
+ const authProvider = config.unstable__auth2Options && config.authOptions?.authUrl && config.authOptions?.redirectUrl && stamper instanceof Auth2Stamper ? new Auth2AuthProvider(
3308
+ stamper,
3309
+ storage,
3310
+ urlParamsAccessor,
3311
+ {
3312
+ redirectUri: config.authOptions.redirectUrl,
3313
+ connectLoginUrl: config.authOptions.authUrl,
3314
+ clientId: config.unstable__auth2Options.clientId,
3315
+ authApiBaseUrl: config.unstable__auth2Options.authApiBaseUrl
3316
+ },
3317
+ {
3318
+ apiBaseUrl: config.apiBaseUrl,
3319
+ appId: config.appId
3320
+ }
3321
+ ) : new BrowserAuthProvider(urlParamsAccessor);
3028
3322
  const platform = {
3029
- storage: new BrowserStorage(),
3030
- authProvider: new BrowserAuthProvider(urlParamsAccessor),
3323
+ storage,
3324
+ authProvider,
3031
3325
  phantomAppProvider: new BrowserPhantomAppProvider(),
3032
3326
  urlParamsAccessor,
3033
3327
  stamper,
@@ -3035,13 +3329,12 @@ var EmbeddedProvider = class extends import_embedded_provider_core.EmbeddedProvi
3035
3329
  // Use detected browser name and version for identification
3036
3330
  analyticsHeaders: {
3037
3331
  [import_constants4.ANALYTICS_HEADERS.SDK_TYPE]: "browser",
3038
- [import_constants4.ANALYTICS_HEADERS.PLATFORM]: browserName,
3039
- // firefox, chrome, safari, etc.
3332
+ [import_constants4.ANALYTICS_HEADERS.PLATFORM]: "ext-sdk",
3040
3333
  [import_constants4.ANALYTICS_HEADERS.PLATFORM_VERSION]: version,
3041
- // Full user agent for more detailed info
3334
+ [import_constants4.ANALYTICS_HEADERS.CLIENT]: browserName,
3042
3335
  [import_constants4.ANALYTICS_HEADERS.APP_ID]: config.appId,
3043
3336
  [import_constants4.ANALYTICS_HEADERS.WALLET_TYPE]: config.embeddedWalletType,
3044
- [import_constants4.ANALYTICS_HEADERS.SDK_VERSION]: "1.0.3"
3337
+ [import_constants4.ANALYTICS_HEADERS.SDK_VERSION]: "1.0.5"
3045
3338
  // Replaced at build time
3046
3339
  }
3047
3340
  };
@@ -3459,6 +3752,7 @@ var ProviderManager = class {
3459
3752
  authUrl,
3460
3753
  redirectUrl: this.config.authOptions?.redirectUrl || this.getValidatedCurrentUrl()
3461
3754
  },
3755
+ unstable__auth2Options: this.config.unstable__auth2Options,
3462
3756
  embeddedWalletType: embeddedWalletType || import_constants5.DEFAULT_EMBEDDED_WALLET_TYPE,
3463
3757
  addressTypes: this.config.addressTypes || [import_client.AddressType.solana]
3464
3758
  });
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.5",
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,299 @@ var BrowserAuthProvider = class {
2845
2835
  }
2846
2836
  };
2847
2837
 
2838
+ // src/providers/embedded/adapters/Auth2AuthProvider.ts
2839
+ import {
2840
+ createCodeVerifier,
2841
+ createConnectStartUrl,
2842
+ exchangeAuthCode,
2843
+ Auth2KmsRpcClient
2844
+ } from "@phantom/auth2";
2845
+ var Auth2AuthProvider = class {
2846
+ constructor(stamper, storage, urlParamsAccessor, auth2ProviderOptions, kmsClientOptions) {
2847
+ this.stamper = stamper;
2848
+ this.storage = storage;
2849
+ this.urlParamsAccessor = urlParamsAccessor;
2850
+ this.auth2ProviderOptions = auth2ProviderOptions;
2851
+ this.kms = new Auth2KmsRpcClient(stamper, kmsClientOptions);
2852
+ }
2853
+ /** Redirect the browser. Extracted as a static method so tests can spy on it. */
2854
+ static navigate(url) {
2855
+ window.location.href = url;
2856
+ }
2857
+ /**
2858
+ * Builds the Auth2 /login/start URL and redirects the browser.
2859
+ *
2860
+ * Called by EmbeddedProvider.handleRedirectAuth() after the stamper has
2861
+ * already been initialized and a pending Session has been saved to storage.
2862
+ * We store the PKCE code_verifier into that session so it survives the page
2863
+ * redirect without ever touching sessionStorage.
2864
+ */
2865
+ async authenticate(options) {
2866
+ if (!this.stamper.getKeyInfo()) {
2867
+ await this.stamper.init();
2868
+ }
2869
+ const keyPair = this.stamper.getCryptoKeyPair();
2870
+ if (!keyPair) {
2871
+ throw new Error("Stamper key pair not found.");
2872
+ }
2873
+ const codeVerifier = createCodeVerifier();
2874
+ const session = await this.storage.getSession();
2875
+ if (!session) {
2876
+ throw new Error("Session not found.");
2877
+ }
2878
+ await this.storage.saveSession({ ...session, pkceCodeVerifier: codeVerifier });
2879
+ const url = await createConnectStartUrl({
2880
+ keyPair,
2881
+ connectLoginUrl: this.auth2ProviderOptions.connectLoginUrl,
2882
+ clientId: this.auth2ProviderOptions.clientId,
2883
+ redirectUri: this.auth2ProviderOptions.redirectUri,
2884
+ sessionId: options.sessionId,
2885
+ provider: options.provider,
2886
+ codeVerifier,
2887
+ // The P-256 ephemeral key is unique per wallet, so no additional salt is needed.
2888
+ salt: ""
2889
+ });
2890
+ Auth2AuthProvider.navigate(url);
2891
+ }
2892
+ /**
2893
+ * Processes the Auth2 callback after the browser returns from /login/start.
2894
+ *
2895
+ * Exchanges the authorization code for tokens, discovers the organization
2896
+ * and wallet via KMS RPC, then returns a completed AuthResult.
2897
+ */
2898
+ async resumeAuthFromRedirect(provider) {
2899
+ const code = this.urlParamsAccessor.getParam("code");
2900
+ if (!code) {
2901
+ return null;
2902
+ }
2903
+ if (!this.stamper.getKeyInfo()) {
2904
+ await this.stamper.init();
2905
+ }
2906
+ const session = await this.storage.getSession();
2907
+ if (!session) {
2908
+ throw new Error("Session not found.");
2909
+ }
2910
+ const codeVerifier = session?.pkceCodeVerifier;
2911
+ if (!codeVerifier) {
2912
+ return null;
2913
+ }
2914
+ const state = this.urlParamsAccessor.getParam("state");
2915
+ if (!state || state !== session.sessionId) {
2916
+ throw new Error("Missing or invalid Auth2 state parameter \u2014 possible CSRF attack.");
2917
+ }
2918
+ const error = this.urlParamsAccessor.getParam("error");
2919
+ if (error) {
2920
+ const description = this.urlParamsAccessor.getParam("error_description");
2921
+ throw new Error(`Auth2 callback error: ${description ?? error}`);
2922
+ }
2923
+ const { idToken, bearerToken, authUserId, expiresInMs } = await exchangeAuthCode({
2924
+ authApiBaseUrl: this.auth2ProviderOptions.authApiBaseUrl,
2925
+ clientId: this.auth2ProviderOptions.clientId,
2926
+ redirectUri: this.auth2ProviderOptions.redirectUri,
2927
+ code,
2928
+ codeVerifier
2929
+ });
2930
+ await this.stamper.setIdToken(idToken);
2931
+ await this.storage.saveSession({
2932
+ ...session,
2933
+ status: "completed",
2934
+ bearerToken,
2935
+ authUserId,
2936
+ pkceCodeVerifier: void 0
2937
+ // no longer needed after code exchange
2938
+ });
2939
+ const { organizationId, walletId } = await this.kms.discoverOrganizationAndWalletId(bearerToken, authUserId);
2940
+ return {
2941
+ walletId,
2942
+ organizationId,
2943
+ provider,
2944
+ accountDerivationIndex: 0,
2945
+ // discoverWalletId uses derivation index of 0.
2946
+ expiresInMs,
2947
+ authUserId,
2948
+ bearerToken
2949
+ };
2950
+ }
2951
+ };
2952
+
2953
+ // src/providers/embedded/adapters/Auth2Stamper.ts
2954
+ import bs582 from "bs58";
2955
+ import { base64urlEncode } from "@phantom/base64url";
2956
+ import { Algorithm } from "@phantom/sdk-types";
2957
+ var STORE_NAME = "crypto-keys";
2958
+ var ACTIVE_KEY = "auth2-p256-signing-key";
2959
+ var Auth2Stamper = class {
2960
+ /**
2961
+ * @param dbName - IndexedDB database name (use a unique name per app to
2962
+ * avoid key collisions with other stampers, e.g. `phantom-auth2-<appId>`).
2963
+ */
2964
+ constructor(dbName) {
2965
+ this.dbName = dbName;
2966
+ this.db = null;
2967
+ this._keyPair = null;
2968
+ this._keyInfo = null;
2969
+ this._idToken = null;
2970
+ this.algorithm = Algorithm.secp256r1;
2971
+ this.type = "OIDC";
2972
+ }
2973
+ async init() {
2974
+ await this.openDB();
2975
+ const stored = await this.loadRecord();
2976
+ if (stored) {
2977
+ this._keyPair = stored.keyPair;
2978
+ this._keyInfo = stored.keyInfo;
2979
+ if (stored.idToken) {
2980
+ this._idToken = stored.idToken;
2981
+ }
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
+ /**
2993
+ * Arms the stamper with the OIDC id token for subsequent KMS stamp() calls.
2994
+ *
2995
+ * Persists the token to IndexedDB alongside the key pair so that
2996
+ * auto-connect can restore it on the next page load without a new login.
2997
+ */
2998
+ async setIdToken(idToken) {
2999
+ if (!this.db) {
3000
+ await this.openDB();
3001
+ }
3002
+ this._idToken = idToken;
3003
+ const existing = await this.loadRecord();
3004
+ if (existing) {
3005
+ await this.storeRecord({ ...existing, idToken });
3006
+ }
3007
+ }
3008
+ async stamp(params) {
3009
+ if (!this._keyPair || !this._keyInfo || this._idToken === null) {
3010
+ throw new Error("Auth2Stamper not initialized. Call init() first.");
3011
+ }
3012
+ const signatureRaw = await crypto.subtle.sign(
3013
+ { name: "ECDSA", hash: "SHA-256" },
3014
+ this._keyPair.privateKey,
3015
+ new Uint8Array(params.data)
3016
+ );
3017
+ const rawPublicKey = bs582.decode(this._keyInfo.publicKey);
3018
+ const stampData = {
3019
+ kind: this.type,
3020
+ idToken: this._idToken,
3021
+ publicKey: base64urlEncode(rawPublicKey),
3022
+ algorithm: this.algorithm,
3023
+ // The P-256 ephemeral key is unique per wallet, so no additional salt is needed.
3024
+ salt: "",
3025
+ signature: base64urlEncode(new Uint8Array(signatureRaw))
3026
+ };
3027
+ return base64urlEncode(new TextEncoder().encode(JSON.stringify(stampData)));
3028
+ }
3029
+ async resetKeyPair() {
3030
+ await this.clear();
3031
+ return this.generateAndStore();
3032
+ }
3033
+ async clear() {
3034
+ await this.clearStoredRecord();
3035
+ this._keyPair = null;
3036
+ this._keyInfo = null;
3037
+ this._idToken = null;
3038
+ }
3039
+ // Auth2 doesn't use key rotation; provide minimal no-op implementations.
3040
+ async rotateKeyPair() {
3041
+ return this.init();
3042
+ }
3043
+ // eslint-disable-next-line @typescript-eslint/require-await
3044
+ async commitRotation(authenticatorId) {
3045
+ if (this._keyInfo) {
3046
+ this._keyInfo.authenticatorId = authenticatorId;
3047
+ }
3048
+ }
3049
+ async rollbackRotation() {
3050
+ }
3051
+ async generateAndStore() {
3052
+ const keyPair = await crypto.subtle.generateKey(
3053
+ { name: "ECDSA", namedCurve: "P-256" },
3054
+ false,
3055
+ // non-extractable — private key never leaves Web Crypto
3056
+ ["sign", "verify"]
3057
+ );
3058
+ const rawPublicKey = new Uint8Array(await crypto.subtle.exportKey("raw", keyPair.publicKey));
3059
+ const publicKeyBase58 = bs582.encode(rawPublicKey);
3060
+ const keyIdBuffer = await crypto.subtle.digest("SHA-256", rawPublicKey.buffer);
3061
+ const keyId = base64urlEncode(new Uint8Array(keyIdBuffer)).substring(0, 16);
3062
+ this._keyPair = keyPair;
3063
+ this._keyInfo = {
3064
+ keyId,
3065
+ publicKey: publicKeyBase58,
3066
+ createdAt: Date.now()
3067
+ };
3068
+ await this.storeRecord({ keyPair, keyInfo: this._keyInfo });
3069
+ return this._keyInfo;
3070
+ }
3071
+ async openDB() {
3072
+ return new Promise((resolve, reject) => {
3073
+ const request = indexedDB.open(this.dbName, 1);
3074
+ request.onsuccess = () => {
3075
+ this.db = request.result;
3076
+ resolve();
3077
+ };
3078
+ request.onerror = () => reject(request.error);
3079
+ request.onupgradeneeded = (event) => {
3080
+ const db = event.target.result;
3081
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
3082
+ db.createObjectStore(STORE_NAME);
3083
+ }
3084
+ };
3085
+ });
3086
+ }
3087
+ async loadRecord() {
3088
+ return new Promise((resolve, reject) => {
3089
+ if (!this.db) {
3090
+ throw new Error("Database not initialized");
3091
+ }
3092
+ const request = this.db.transaction([STORE_NAME], "readonly").objectStore(STORE_NAME).get(ACTIVE_KEY);
3093
+ request.onsuccess = () => {
3094
+ resolve(request.result ?? null);
3095
+ };
3096
+ request.onerror = () => {
3097
+ reject(request.error);
3098
+ };
3099
+ });
3100
+ }
3101
+ async storeRecord(record) {
3102
+ return new Promise((resolve, reject) => {
3103
+ if (!this.db) {
3104
+ throw new Error("Database not initialized");
3105
+ }
3106
+ const request = this.db.transaction([STORE_NAME], "readwrite").objectStore(STORE_NAME).put(record, ACTIVE_KEY);
3107
+ request.onsuccess = () => {
3108
+ resolve();
3109
+ };
3110
+ request.onerror = () => {
3111
+ reject(request.error);
3112
+ };
3113
+ });
3114
+ }
3115
+ async clearStoredRecord() {
3116
+ return new Promise((resolve, reject) => {
3117
+ if (!this.db) {
3118
+ throw new Error("Database not initialized");
3119
+ }
3120
+ const request = this.db.transaction([STORE_NAME], "readwrite").objectStore(STORE_NAME).delete(ACTIVE_KEY);
3121
+ request.onsuccess = () => {
3122
+ resolve();
3123
+ };
3124
+ request.onerror = () => {
3125
+ reject(request.error);
3126
+ };
3127
+ });
3128
+ }
3129
+ };
3130
+
2848
3131
  // src/providers/embedded/adapters/phantom-app.ts
2849
3132
  import { isPhantomExtensionInstalled as isPhantomExtensionInstalled3 } from "@phantom/browser-injected-sdk";
2850
3133
 
@@ -2968,16 +3251,32 @@ var EmbeddedProvider = class extends CoreEmbeddedProvider {
2968
3251
  constructor(config) {
2969
3252
  debug.log(DebugCategory.EMBEDDED_PROVIDER, "Initializing Browser EmbeddedProvider", { config });
2970
3253
  const urlParamsAccessor = new BrowserURLParamsAccessor();
2971
- const stamper = new IndexedDbStamper({
3254
+ const storage = new BrowserStorage();
3255
+ const stamper = config.unstable__auth2Options ? new Auth2Stamper(`phantom-auth2-${config.appId}`) : new IndexedDbStamper({
2972
3256
  dbName: `phantom-embedded-sdk-${config.appId}`,
2973
3257
  storeName: "crypto-keys",
2974
3258
  keyName: "signing-key"
2975
3259
  });
2976
3260
  const platformName = getPlatformName();
2977
3261
  const { name: browserName, version } = detectBrowser();
3262
+ const authProvider = config.unstable__auth2Options && config.authOptions?.authUrl && config.authOptions?.redirectUrl && stamper instanceof Auth2Stamper ? new Auth2AuthProvider(
3263
+ stamper,
3264
+ storage,
3265
+ urlParamsAccessor,
3266
+ {
3267
+ redirectUri: config.authOptions.redirectUrl,
3268
+ connectLoginUrl: config.authOptions.authUrl,
3269
+ clientId: config.unstable__auth2Options.clientId,
3270
+ authApiBaseUrl: config.unstable__auth2Options.authApiBaseUrl
3271
+ },
3272
+ {
3273
+ apiBaseUrl: config.apiBaseUrl,
3274
+ appId: config.appId
3275
+ }
3276
+ ) : new BrowserAuthProvider(urlParamsAccessor);
2978
3277
  const platform = {
2979
- storage: new BrowserStorage(),
2980
- authProvider: new BrowserAuthProvider(urlParamsAccessor),
3278
+ storage,
3279
+ authProvider,
2981
3280
  phantomAppProvider: new BrowserPhantomAppProvider(),
2982
3281
  urlParamsAccessor,
2983
3282
  stamper,
@@ -2985,13 +3284,12 @@ var EmbeddedProvider = class extends CoreEmbeddedProvider {
2985
3284
  // Use detected browser name and version for identification
2986
3285
  analyticsHeaders: {
2987
3286
  [ANALYTICS_HEADERS.SDK_TYPE]: "browser",
2988
- [ANALYTICS_HEADERS.PLATFORM]: browserName,
2989
- // firefox, chrome, safari, etc.
3287
+ [ANALYTICS_HEADERS.PLATFORM]: "ext-sdk",
2990
3288
  [ANALYTICS_HEADERS.PLATFORM_VERSION]: version,
2991
- // Full user agent for more detailed info
3289
+ [ANALYTICS_HEADERS.CLIENT]: browserName,
2992
3290
  [ANALYTICS_HEADERS.APP_ID]: config.appId,
2993
3291
  [ANALYTICS_HEADERS.WALLET_TYPE]: config.embeddedWalletType,
2994
- [ANALYTICS_HEADERS.SDK_VERSION]: "1.0.3"
3292
+ [ANALYTICS_HEADERS.SDK_VERSION]: "1.0.5"
2995
3293
  // Replaced at build time
2996
3294
  }
2997
3295
  };
@@ -3411,6 +3709,7 @@ var ProviderManager = class {
3411
3709
  authUrl,
3412
3710
  redirectUrl: this.config.authOptions?.redirectUrl || this.getValidatedCurrentUrl()
3413
3711
  },
3712
+ unstable__auth2Options: this.config.unstable__auth2Options,
3414
3713
  embeddedWalletType: embeddedWalletType || DEFAULT_EMBEDDED_WALLET_TYPE,
3415
3714
  addressTypes: this.config.addressTypes || [AddressType.solana]
3416
3715
  });
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.5",
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.1",
37
+ "@phantom/base64url": "^1.0.5",
38
+ "@phantom/browser-injected-sdk": "^1.0.5",
39
+ "@phantom/chain-interfaces": "^1.0.5",
40
+ "@phantom/client": "^1.0.5",
41
+ "@phantom/constants": "^1.0.5",
42
+ "@phantom/embedded-provider-core": "^1.0.5",
43
+ "@phantom/indexed-db-stamper": "^1.0.5",
44
+ "@phantom/parsers": "^1.0.5",
45
+ "@phantom/sdk-types": "^1.0.5",
45
46
  "axios": "^1.10.0",
46
47
  "bs58": "^6.0.0",
47
48
  "buffer": "^6.0.3",