@phantom/react-native-sdk 0.1.6 → 0.1.8

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.js CHANGED
@@ -167,12 +167,14 @@ var ExpoAuthProvider = class {
167
167
  const url = new URL(result.url);
168
168
  const walletId = url.searchParams.get("wallet_id");
169
169
  const provider2 = url.searchParams.get("provider");
170
+ const accountDerivationIndex = url.searchParams.get("selected_account_index");
170
171
  if (!walletId) {
171
172
  throw new Error("Authentication failed: no walletId in redirect URL");
172
173
  }
173
174
  return {
174
175
  walletId,
175
- provider: provider2 || void 0
176
+ provider: provider2 || void 0,
177
+ accountDerivationIndex: accountDerivationIndex ? parseInt(accountDerivationIndex) : void 0
176
178
  };
177
179
  } else if (result.type === "cancel") {
178
180
  throw new Error("User cancelled authentication");
@@ -295,7 +297,8 @@ var import_sdk_types = require("@phantom/sdk-types");
295
297
  var ReactNativeStamper = class {
296
298
  // Optional for PKI, required for OIDC
297
299
  constructor(config = {}) {
298
- this.keyInfo = null;
300
+ this.activeKeyRecord = null;
301
+ this.pendingKeyRecord = null;
299
302
  this.algorithm = import_sdk_types.Algorithm.ed25519;
300
303
  this.type = "PKI";
301
304
  this.keyPrefix = config.keyPrefix || "phantom-rn-stamper";
@@ -305,32 +308,27 @@ var ReactNativeStamper = class {
305
308
  * Initialize the stamper and generate/load cryptographic keys
306
309
  */
307
310
  async init() {
308
- const storedSecretKey = await this.getStoredSecretKey();
309
- if (storedSecretKey) {
310
- const keyInfo2 = await this.getStoredKeyInfo();
311
- if (keyInfo2) {
312
- this.keyInfo = keyInfo2;
313
- return keyInfo2;
314
- }
311
+ this.activeKeyRecord = await this.loadActiveKeyRecord();
312
+ if (!this.activeKeyRecord) {
313
+ this.activeKeyRecord = await this.generateAndStoreNewKeyRecord("active");
315
314
  }
316
- const keyInfo = await this.generateAndStoreKeyPair();
317
- this.keyInfo = keyInfo;
318
- return keyInfo;
315
+ this.pendingKeyRecord = await this.loadPendingKeyRecord();
316
+ return this.activeKeyRecord.keyInfo;
319
317
  }
320
318
  /**
321
319
  * Get the current key information
322
320
  */
323
321
  getKeyInfo() {
324
- return this.keyInfo;
322
+ return this.activeKeyRecord?.keyInfo || null;
325
323
  }
326
324
  /**
327
325
  * Generate and store a new key pair, replacing any existing keys
328
326
  */
329
327
  async resetKeyPair() {
330
328
  await this.clear();
331
- const keyInfo = await this.generateAndStoreKeyPair();
332
- this.keyInfo = keyInfo;
333
- return keyInfo;
329
+ this.activeKeyRecord = await this.generateAndStoreNewKeyRecord("active");
330
+ this.pendingKeyRecord = null;
331
+ return this.activeKeyRecord.keyInfo;
334
332
  }
335
333
  /**
336
334
  * Create X-Phantom-Stamp header value using stored secret key
@@ -338,79 +336,129 @@ var ReactNativeStamper = class {
338
336
  * @returns Complete X-Phantom-Stamp header value
339
337
  */
340
338
  async stamp(params) {
341
- if (!this.keyInfo) {
339
+ if (!this.activeKeyRecord) {
342
340
  throw new Error("Stamper not initialized. Call init() first.");
343
341
  }
344
- const storedSecretKey = await this.getStoredSecretKey();
345
- if (!storedSecretKey) {
346
- throw new Error("Secret key not found in secure storage");
347
- }
348
- const apiKeyStamper = new import_api_key_stamper.ApiKeyStamper({ apiSecretKey: storedSecretKey });
342
+ const apiKeyStamper = new import_api_key_stamper.ApiKeyStamper({ apiSecretKey: this.activeKeyRecord.secretKey });
349
343
  return await apiKeyStamper.stamp(params);
350
344
  }
351
345
  /**
352
346
  * Clear all stored keys from SecureStore
353
347
  */
354
348
  async clear() {
355
- const infoKey = this.getInfoKey();
356
- const secretKey = this.getSecretKey();
349
+ const activeKey = this.getActiveKeyName();
350
+ const pendingKey = this.getPendingKeyName();
351
+ try {
352
+ await SecureStore2.deleteItemAsync(activeKey);
353
+ } catch (error) {
354
+ }
357
355
  try {
358
- await SecureStore2.deleteItemAsync(infoKey);
356
+ await SecureStore2.deleteItemAsync(pendingKey);
359
357
  } catch (error) {
360
358
  }
359
+ this.activeKeyRecord = null;
360
+ this.pendingKeyRecord = null;
361
+ }
362
+ /**
363
+ * Generate a new keypair for rotation without making it active
364
+ */
365
+ async rotateKeyPair() {
366
+ this.pendingKeyRecord = await this.generateAndStoreNewKeyRecord("pending");
367
+ return this.pendingKeyRecord.keyInfo;
368
+ }
369
+ /**
370
+ * Switch to the pending keypair, making it active and cleaning up the old one
371
+ */
372
+ async commitRotation(authenticatorId) {
373
+ if (!this.pendingKeyRecord) {
374
+ throw new Error("No pending keypair to commit");
375
+ }
376
+ if (this.activeKeyRecord) {
377
+ try {
378
+ await SecureStore2.deleteItemAsync(this.getActiveKeyName());
379
+ } catch (error) {
380
+ }
381
+ }
382
+ this.pendingKeyRecord.status = "active";
383
+ this.pendingKeyRecord.authenticatorId = authenticatorId;
384
+ this.pendingKeyRecord.keyInfo.authenticatorId = authenticatorId;
385
+ this.activeKeyRecord = this.pendingKeyRecord;
386
+ this.pendingKeyRecord = null;
387
+ await this.storeKeyRecord(this.activeKeyRecord, "active");
361
388
  try {
362
- await SecureStore2.deleteItemAsync(secretKey);
389
+ await SecureStore2.deleteItemAsync(this.getPendingKeyName());
363
390
  } catch (error) {
364
391
  }
365
- this.keyInfo = null;
366
392
  }
367
- async generateAndStoreKeyPair() {
393
+ /**
394
+ * Discard the pending keypair on rotation failure
395
+ */
396
+ async rollbackRotation() {
397
+ if (!this.pendingKeyRecord) {
398
+ return;
399
+ }
400
+ try {
401
+ await SecureStore2.deleteItemAsync(this.getPendingKeyName());
402
+ } catch (error) {
403
+ }
404
+ this.pendingKeyRecord = null;
405
+ }
406
+ async generateAndStoreNewKeyRecord(type) {
368
407
  const keypair = (0, import_crypto.generateKeyPair)();
369
408
  const keyId = this.createKeyId(keypair.publicKey);
409
+ const now = Date.now();
370
410
  const keyInfo = {
371
411
  keyId,
372
- publicKey: keypair.publicKey
412
+ publicKey: keypair.publicKey,
413
+ createdAt: now
414
+ };
415
+ const record = {
416
+ keyInfo,
417
+ secretKey: keypair.secretKey,
418
+ createdAt: now,
419
+ expiresAt: 0,
420
+ // Not used anymore, kept for backward compatibility
421
+ status: type
373
422
  };
374
- await this.storeKeyPair(keypair.secretKey, keyInfo);
375
- return keyInfo;
423
+ await this.storeKeyRecord(record, type);
424
+ return record;
376
425
  }
377
426
  createKeyId(publicKey) {
378
427
  return (0, import_base64url.base64urlEncode)(new TextEncoder().encode(publicKey)).substring(0, 16);
379
428
  }
380
- async storeKeyPair(secretKey, keyInfo) {
381
- const infoKey = this.getInfoKey();
382
- const secretKeyName = this.getSecretKey();
383
- await SecureStore2.setItemAsync(infoKey, JSON.stringify(keyInfo), {
384
- requireAuthentication: false
385
- });
386
- await SecureStore2.setItemAsync(secretKeyName, secretKey, {
429
+ async storeKeyRecord(record, type) {
430
+ const keyName = type === "active" ? this.getActiveKeyName() : this.getPendingKeyName();
431
+ await SecureStore2.setItemAsync(keyName, JSON.stringify(record), {
387
432
  requireAuthentication: false
388
433
  });
389
434
  }
390
- async getStoredKeyInfo() {
435
+ async loadActiveKeyRecord() {
391
436
  try {
392
- const infoKey = this.getInfoKey();
393
- const storedInfo = await SecureStore2.getItemAsync(infoKey);
394
- if (storedInfo) {
395
- return JSON.parse(storedInfo);
437
+ const activeKey = this.getActiveKeyName();
438
+ const storedRecord = await SecureStore2.getItemAsync(activeKey);
439
+ if (storedRecord) {
440
+ return JSON.parse(storedRecord);
396
441
  }
397
442
  } catch (error) {
398
443
  }
399
444
  return null;
400
445
  }
401
- async getStoredSecretKey() {
446
+ async loadPendingKeyRecord() {
402
447
  try {
403
- const secretKeyName = this.getSecretKey();
404
- return await SecureStore2.getItemAsync(secretKeyName);
448
+ const pendingKey = this.getPendingKeyName();
449
+ const storedRecord = await SecureStore2.getItemAsync(pendingKey);
450
+ if (storedRecord) {
451
+ return JSON.parse(storedRecord);
452
+ }
405
453
  } catch (error) {
406
- return null;
407
454
  }
455
+ return null;
408
456
  }
409
- getInfoKey() {
410
- return `${this.keyPrefix}-${this.organizationId}-info`;
457
+ getActiveKeyName() {
458
+ return `${this.keyPrefix}-${this.organizationId}-active`;
411
459
  }
412
- getSecretKey() {
413
- return `${this.keyPrefix}-${this.organizationId}-secret`;
460
+ getPendingKeyName() {
461
+ return `${this.keyPrefix}-${this.organizationId}-pending`;
414
462
  }
415
463
  };
416
464
 
package/dist/index.mjs CHANGED
@@ -123,12 +123,14 @@ var ExpoAuthProvider = class {
123
123
  const url = new URL(result.url);
124
124
  const walletId = url.searchParams.get("wallet_id");
125
125
  const provider2 = url.searchParams.get("provider");
126
+ const accountDerivationIndex = url.searchParams.get("selected_account_index");
126
127
  if (!walletId) {
127
128
  throw new Error("Authentication failed: no walletId in redirect URL");
128
129
  }
129
130
  return {
130
131
  walletId,
131
- provider: provider2 || void 0
132
+ provider: provider2 || void 0,
133
+ accountDerivationIndex: accountDerivationIndex ? parseInt(accountDerivationIndex) : void 0
132
134
  };
133
135
  } else if (result.type === "cancel") {
134
136
  throw new Error("User cancelled authentication");
@@ -251,7 +253,8 @@ import { Algorithm } from "@phantom/sdk-types";
251
253
  var ReactNativeStamper = class {
252
254
  // Optional for PKI, required for OIDC
253
255
  constructor(config = {}) {
254
- this.keyInfo = null;
256
+ this.activeKeyRecord = null;
257
+ this.pendingKeyRecord = null;
255
258
  this.algorithm = Algorithm.ed25519;
256
259
  this.type = "PKI";
257
260
  this.keyPrefix = config.keyPrefix || "phantom-rn-stamper";
@@ -261,32 +264,27 @@ var ReactNativeStamper = class {
261
264
  * Initialize the stamper and generate/load cryptographic keys
262
265
  */
263
266
  async init() {
264
- const storedSecretKey = await this.getStoredSecretKey();
265
- if (storedSecretKey) {
266
- const keyInfo2 = await this.getStoredKeyInfo();
267
- if (keyInfo2) {
268
- this.keyInfo = keyInfo2;
269
- return keyInfo2;
270
- }
267
+ this.activeKeyRecord = await this.loadActiveKeyRecord();
268
+ if (!this.activeKeyRecord) {
269
+ this.activeKeyRecord = await this.generateAndStoreNewKeyRecord("active");
271
270
  }
272
- const keyInfo = await this.generateAndStoreKeyPair();
273
- this.keyInfo = keyInfo;
274
- return keyInfo;
271
+ this.pendingKeyRecord = await this.loadPendingKeyRecord();
272
+ return this.activeKeyRecord.keyInfo;
275
273
  }
276
274
  /**
277
275
  * Get the current key information
278
276
  */
279
277
  getKeyInfo() {
280
- return this.keyInfo;
278
+ return this.activeKeyRecord?.keyInfo || null;
281
279
  }
282
280
  /**
283
281
  * Generate and store a new key pair, replacing any existing keys
284
282
  */
285
283
  async resetKeyPair() {
286
284
  await this.clear();
287
- const keyInfo = await this.generateAndStoreKeyPair();
288
- this.keyInfo = keyInfo;
289
- return keyInfo;
285
+ this.activeKeyRecord = await this.generateAndStoreNewKeyRecord("active");
286
+ this.pendingKeyRecord = null;
287
+ return this.activeKeyRecord.keyInfo;
290
288
  }
291
289
  /**
292
290
  * Create X-Phantom-Stamp header value using stored secret key
@@ -294,79 +292,129 @@ var ReactNativeStamper = class {
294
292
  * @returns Complete X-Phantom-Stamp header value
295
293
  */
296
294
  async stamp(params) {
297
- if (!this.keyInfo) {
295
+ if (!this.activeKeyRecord) {
298
296
  throw new Error("Stamper not initialized. Call init() first.");
299
297
  }
300
- const storedSecretKey = await this.getStoredSecretKey();
301
- if (!storedSecretKey) {
302
- throw new Error("Secret key not found in secure storage");
303
- }
304
- const apiKeyStamper = new ApiKeyStamper({ apiSecretKey: storedSecretKey });
298
+ const apiKeyStamper = new ApiKeyStamper({ apiSecretKey: this.activeKeyRecord.secretKey });
305
299
  return await apiKeyStamper.stamp(params);
306
300
  }
307
301
  /**
308
302
  * Clear all stored keys from SecureStore
309
303
  */
310
304
  async clear() {
311
- const infoKey = this.getInfoKey();
312
- const secretKey = this.getSecretKey();
305
+ const activeKey = this.getActiveKeyName();
306
+ const pendingKey = this.getPendingKeyName();
307
+ try {
308
+ await SecureStore2.deleteItemAsync(activeKey);
309
+ } catch (error) {
310
+ }
313
311
  try {
314
- await SecureStore2.deleteItemAsync(infoKey);
312
+ await SecureStore2.deleteItemAsync(pendingKey);
315
313
  } catch (error) {
316
314
  }
315
+ this.activeKeyRecord = null;
316
+ this.pendingKeyRecord = null;
317
+ }
318
+ /**
319
+ * Generate a new keypair for rotation without making it active
320
+ */
321
+ async rotateKeyPair() {
322
+ this.pendingKeyRecord = await this.generateAndStoreNewKeyRecord("pending");
323
+ return this.pendingKeyRecord.keyInfo;
324
+ }
325
+ /**
326
+ * Switch to the pending keypair, making it active and cleaning up the old one
327
+ */
328
+ async commitRotation(authenticatorId) {
329
+ if (!this.pendingKeyRecord) {
330
+ throw new Error("No pending keypair to commit");
331
+ }
332
+ if (this.activeKeyRecord) {
333
+ try {
334
+ await SecureStore2.deleteItemAsync(this.getActiveKeyName());
335
+ } catch (error) {
336
+ }
337
+ }
338
+ this.pendingKeyRecord.status = "active";
339
+ this.pendingKeyRecord.authenticatorId = authenticatorId;
340
+ this.pendingKeyRecord.keyInfo.authenticatorId = authenticatorId;
341
+ this.activeKeyRecord = this.pendingKeyRecord;
342
+ this.pendingKeyRecord = null;
343
+ await this.storeKeyRecord(this.activeKeyRecord, "active");
317
344
  try {
318
- await SecureStore2.deleteItemAsync(secretKey);
345
+ await SecureStore2.deleteItemAsync(this.getPendingKeyName());
319
346
  } catch (error) {
320
347
  }
321
- this.keyInfo = null;
322
348
  }
323
- async generateAndStoreKeyPair() {
349
+ /**
350
+ * Discard the pending keypair on rotation failure
351
+ */
352
+ async rollbackRotation() {
353
+ if (!this.pendingKeyRecord) {
354
+ return;
355
+ }
356
+ try {
357
+ await SecureStore2.deleteItemAsync(this.getPendingKeyName());
358
+ } catch (error) {
359
+ }
360
+ this.pendingKeyRecord = null;
361
+ }
362
+ async generateAndStoreNewKeyRecord(type) {
324
363
  const keypair = generateKeyPair();
325
364
  const keyId = this.createKeyId(keypair.publicKey);
365
+ const now = Date.now();
326
366
  const keyInfo = {
327
367
  keyId,
328
- publicKey: keypair.publicKey
368
+ publicKey: keypair.publicKey,
369
+ createdAt: now
370
+ };
371
+ const record = {
372
+ keyInfo,
373
+ secretKey: keypair.secretKey,
374
+ createdAt: now,
375
+ expiresAt: 0,
376
+ // Not used anymore, kept for backward compatibility
377
+ status: type
329
378
  };
330
- await this.storeKeyPair(keypair.secretKey, keyInfo);
331
- return keyInfo;
379
+ await this.storeKeyRecord(record, type);
380
+ return record;
332
381
  }
333
382
  createKeyId(publicKey) {
334
383
  return base64urlEncode(new TextEncoder().encode(publicKey)).substring(0, 16);
335
384
  }
336
- async storeKeyPair(secretKey, keyInfo) {
337
- const infoKey = this.getInfoKey();
338
- const secretKeyName = this.getSecretKey();
339
- await SecureStore2.setItemAsync(infoKey, JSON.stringify(keyInfo), {
340
- requireAuthentication: false
341
- });
342
- await SecureStore2.setItemAsync(secretKeyName, secretKey, {
385
+ async storeKeyRecord(record, type) {
386
+ const keyName = type === "active" ? this.getActiveKeyName() : this.getPendingKeyName();
387
+ await SecureStore2.setItemAsync(keyName, JSON.stringify(record), {
343
388
  requireAuthentication: false
344
389
  });
345
390
  }
346
- async getStoredKeyInfo() {
391
+ async loadActiveKeyRecord() {
347
392
  try {
348
- const infoKey = this.getInfoKey();
349
- const storedInfo = await SecureStore2.getItemAsync(infoKey);
350
- if (storedInfo) {
351
- return JSON.parse(storedInfo);
393
+ const activeKey = this.getActiveKeyName();
394
+ const storedRecord = await SecureStore2.getItemAsync(activeKey);
395
+ if (storedRecord) {
396
+ return JSON.parse(storedRecord);
352
397
  }
353
398
  } catch (error) {
354
399
  }
355
400
  return null;
356
401
  }
357
- async getStoredSecretKey() {
402
+ async loadPendingKeyRecord() {
358
403
  try {
359
- const secretKeyName = this.getSecretKey();
360
- return await SecureStore2.getItemAsync(secretKeyName);
404
+ const pendingKey = this.getPendingKeyName();
405
+ const storedRecord = await SecureStore2.getItemAsync(pendingKey);
406
+ if (storedRecord) {
407
+ return JSON.parse(storedRecord);
408
+ }
361
409
  } catch (error) {
362
- return null;
363
410
  }
411
+ return null;
364
412
  }
365
- getInfoKey() {
366
- return `${this.keyPrefix}-${this.organizationId}-info`;
413
+ getActiveKeyName() {
414
+ return `${this.keyPrefix}-${this.organizationId}-active`;
367
415
  }
368
- getSecretKey() {
369
- return `${this.keyPrefix}-${this.organizationId}-secret`;
416
+ getPendingKeyName() {
417
+ return `${this.keyPrefix}-${this.organizationId}-pending`;
370
418
  }
371
419
  };
372
420
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phantom/react-native-sdk",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Phantom Wallet SDK for React Native and Expo applications",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -47,11 +47,11 @@
47
47
  "dependencies": {
48
48
  "@phantom/api-key-stamper": "^0.1.5",
49
49
  "@phantom/base64url": "^0.1.0",
50
- "@phantom/client": "^0.1.8",
50
+ "@phantom/client": "^0.1.11",
51
51
  "@phantom/constants": "^0.0.3",
52
52
  "@phantom/crypto": "^0.1.2",
53
- "@phantom/embedded-provider-core": "^0.1.7",
54
- "@phantom/sdk-types": "^0.1.4",
53
+ "@phantom/embedded-provider-core": "^0.1.10",
54
+ "@phantom/sdk-types": "^0.1.5",
55
55
  "@types/bs58": "^5.0.0",
56
56
  "bs58": "^6.0.0",
57
57
  "buffer": "^6.0.3"