@phantom/embedded-provider-core 0.1.8 → 0.1.10

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.mjs CHANGED
@@ -9,6 +9,10 @@ import {
9
9
  parseTransactionResponse
10
10
  } from "@phantom/parsers";
11
11
 
12
+ // src/constants.ts
13
+ var AUTHENTICATOR_EXPIRATION_TIME_MS = 7 * 24 * 60 * 60 * 1e3;
14
+ var AUTHENTICATOR_RENEWAL_WINDOW_MS = 2 * 24 * 60 * 60 * 1e3;
15
+
12
16
  // src/auth/jwt-auth.ts
13
17
  var JWTAuth = class {
14
18
  async authenticate(options) {
@@ -175,14 +179,17 @@ var EmbeddedProvider = class {
175
179
  }
176
180
  }
177
181
  async getAndFilterWalletAddresses(walletId) {
182
+ const session = await this.storage.getSession();
183
+ const derivationIndex = session?.accountDerivationIndex ?? 0;
178
184
  const addresses = await retryWithBackoff(
179
- () => this.client.getWalletAddresses(walletId),
185
+ () => this.client.getWalletAddresses(walletId, void 0, derivationIndex),
180
186
  "getWalletAddresses",
181
187
  this.logger
182
188
  ).catch(async (error) => {
183
189
  this.logger.error("EMBEDDED_PROVIDER", "getWalletAddresses failed after retries, disconnecting", {
184
190
  walletId,
185
- error: error.message
191
+ error: error.message,
192
+ derivationIndex
186
193
  });
187
194
  await this.storage.clearSession();
188
195
  this.client = null;
@@ -223,6 +230,14 @@ var EmbeddedProvider = class {
223
230
  return null;
224
231
  }
225
232
  }
233
+ if (session.status === "completed" && !this.isSessionValid(session)) {
234
+ this.logger.warn("EMBEDDED_PROVIDER", "Session invalid due to authenticator expiration", {
235
+ sessionId: session.sessionId,
236
+ authenticatorExpiresAt: session.authenticatorExpiresAt
237
+ });
238
+ await this.storage.clearSession();
239
+ return null;
240
+ }
226
241
  return session;
227
242
  }
228
243
  /*
@@ -257,6 +272,7 @@ var EmbeddedProvider = class {
257
272
  walletId: this.walletId,
258
273
  addressCount: this.addresses.length
259
274
  });
275
+ await this.ensureValidAuthenticator();
260
276
  const result = {
261
277
  walletId: this.walletId,
262
278
  addresses: this.addresses,
@@ -286,8 +302,9 @@ var EmbeddedProvider = class {
286
302
  }
287
303
  }
288
304
  /*
289
- * We use this method to validate if a session is still valid and can be used for auto-connect.
290
- * This checks session status, expiration, and required fields.
305
+ * We use this method to validate if a session is still valid.
306
+ * This checks session status, required fields, and authenticator expiration.
307
+ * Sessions never expire by age - only authenticators expire.
291
308
  */
292
309
  isSessionValid(session) {
293
310
  if (!session) {
@@ -305,20 +322,23 @@ var EmbeddedProvider = class {
305
322
  this.logger.log("EMBEDDED_PROVIDER", "Session not completed", { status: session.status });
306
323
  return false;
307
324
  }
308
- const sessionAge = Date.now() - session.lastUsed;
309
- const maxSessionAge = 7 * 24 * 60 * 60 * 1e3;
310
- if (sessionAge > maxSessionAge) {
311
- this.logger.log("EMBEDDED_PROVIDER", "Session expired", {
312
- sessionAge,
313
- maxSessionAge,
314
- lastUsed: new Date(session.lastUsed).toISOString()
325
+ if (!session.authenticatorExpiresAt) {
326
+ this.logger.log("EMBEDDED_PROVIDER", "Session invalid - missing authenticator timing", {
327
+ sessionId: session.sessionId
328
+ });
329
+ return false;
330
+ }
331
+ if (Date.now() >= session.authenticatorExpiresAt) {
332
+ this.logger.log("EMBEDDED_PROVIDER", "Authenticator expired, session invalid", {
333
+ authenticatorExpiresAt: new Date(session.authenticatorExpiresAt).toISOString(),
334
+ now: (/* @__PURE__ */ new Date()).toISOString()
315
335
  });
316
336
  return false;
317
337
  }
318
338
  this.logger.log("EMBEDDED_PROVIDER", "Session is valid", {
319
339
  sessionId: session.sessionId,
320
340
  walletId: session.walletId,
321
- lastUsed: new Date(session.lastUsed).toISOString()
341
+ authenticatorExpires: new Date(session.authenticatorExpiresAt).toISOString()
322
342
  });
323
343
  return true;
324
344
  }
@@ -387,9 +407,11 @@ var EmbeddedProvider = class {
387
407
  platform: platformName
388
408
  });
389
409
  const base64urlPublicKey = base64urlEncode(bs58.decode(stamperInfo.publicKey));
410
+ const expiresAtMs = Date.now() + AUTHENTICATOR_EXPIRATION_TIME_MS;
411
+ const username = `user-${shortPubKey}`;
390
412
  const { organizationId } = await tempClient.createOrganization(organizationName, [
391
413
  {
392
- username: `user-${shortPubKey}`,
414
+ username,
393
415
  role: "ADMIN",
394
416
  authenticators: [
395
417
  {
@@ -397,12 +419,14 @@ var EmbeddedProvider = class {
397
419
  authenticatorKind: "keypair",
398
420
  publicKey: base64urlPublicKey,
399
421
  algorithm: "Ed25519"
422
+ // Commented for now until KMS supports fully expirable organizations
423
+ // expiresAtMs: expiresAtMs,
400
424
  }
401
425
  ]
402
426
  }
403
427
  ]);
404
428
  this.logger.info("EMBEDDED_PROVIDER", "Organization created", { organizationId });
405
- return { organizationId, stamperInfo };
429
+ return { organizationId, stamperInfo, expiresAtMs, username };
406
430
  }
407
431
  async connect(authOptions) {
408
432
  try {
@@ -431,8 +455,8 @@ var EmbeddedProvider = class {
431
455
  }
432
456
  this.validateAuthOptions(authOptions);
433
457
  this.logger.info("EMBEDDED_PROVIDER", "No existing connection, creating new auth flow");
434
- const { organizationId, stamperInfo } = await this.createOrganizationAndStamper();
435
- const session = await this.handleAuthFlow(organizationId, stamperInfo, authOptions);
458
+ const { organizationId, stamperInfo, expiresAtMs, username } = await this.createOrganizationAndStamper();
459
+ const session = await this.handleAuthFlow(organizationId, stamperInfo, authOptions, expiresAtMs, username);
436
460
  if (!session) {
437
461
  return {
438
462
  addresses: [],
@@ -444,6 +468,7 @@ var EmbeddedProvider = class {
444
468
  await this.storage.saveSession(session);
445
469
  }
446
470
  await this.initializeClientFromSession(session);
471
+ await this.ensureValidAuthenticator();
447
472
  const result = {
448
473
  walletId: this.walletId,
449
474
  addresses: this.addresses,
@@ -509,15 +534,19 @@ var EmbeddedProvider = class {
509
534
  if (!this.client || !this.walletId) {
510
535
  throw new Error("Not connected");
511
536
  }
537
+ await this.ensureValidAuthenticator();
512
538
  this.logger.info("EMBEDDED_PROVIDER", "Signing message", {
513
539
  walletId: this.walletId,
514
540
  message: params.message
515
541
  });
516
542
  const parsedMessage = parseMessage(params.message);
543
+ const session = await this.storage.getSession();
544
+ const derivationIndex = session?.accountDerivationIndex ?? 0;
517
545
  const rawResponse = await this.client.signMessage({
518
546
  walletId: this.walletId,
519
547
  message: parsedMessage.base64url,
520
- networkId: params.networkId
548
+ networkId: params.networkId,
549
+ derivationIndex
521
550
  });
522
551
  this.logger.info("EMBEDDED_PROVIDER", "Message signed successfully", {
523
552
  walletId: this.walletId,
@@ -529,19 +558,24 @@ var EmbeddedProvider = class {
529
558
  if (!this.client || !this.walletId) {
530
559
  throw new Error("Not connected");
531
560
  }
561
+ await this.ensureValidAuthenticator();
532
562
  this.logger.info("EMBEDDED_PROVIDER", "Signing and sending transaction", {
533
563
  walletId: this.walletId,
534
564
  networkId: params.networkId
535
565
  });
536
566
  const parsedTransaction = await parseTransaction(params.transaction, params.networkId);
567
+ const session = await this.storage.getSession();
568
+ const derivationIndex = session?.accountDerivationIndex ?? 0;
537
569
  this.logger.log("EMBEDDED_PROVIDER", "Parsed transaction for signing", {
538
570
  walletId: this.walletId,
539
- transaction: parsedTransaction
571
+ transaction: parsedTransaction,
572
+ derivationIndex
540
573
  });
541
574
  const rawResponse = await this.client.signAndSendTransaction({
542
575
  walletId: this.walletId,
543
576
  transaction: parsedTransaction.base64url,
544
- networkId: params.networkId
577
+ networkId: params.networkId,
578
+ derivationIndex
545
579
  });
546
580
  this.logger.info("EMBEDDED_PROVIDER", "Transaction signed and sent successfully", {
547
581
  walletId: this.walletId,
@@ -562,20 +596,20 @@ var EmbeddedProvider = class {
562
596
  * It handles app-wallet creation directly or routes to JWT/redirect authentication for user-wallets.
563
597
  * Returns null for redirect flows since they don't complete synchronously.
564
598
  */
565
- async handleAuthFlow(organizationId, stamperInfo, authOptions) {
599
+ async handleAuthFlow(organizationId, stamperInfo, authOptions, expiresAtMs, username) {
566
600
  if (this.config.embeddedWalletType === "user-wallet") {
567
601
  this.logger.info("EMBEDDED_PROVIDER", "Creating user-wallet, routing authentication", {
568
602
  authProvider: authOptions?.provider || "phantom-connect"
569
603
  });
570
604
  if (authOptions?.provider === "jwt") {
571
- return await this.handleJWTAuth(organizationId, stamperInfo, authOptions);
605
+ return await this.handleJWTAuth(organizationId, stamperInfo, authOptions, expiresAtMs, username);
572
606
  } else {
573
607
  this.logger.info("EMBEDDED_PROVIDER", "Starting redirect-based authentication flow", {
574
608
  organizationId,
575
609
  parentOrganizationId: this.config.organizationId,
576
610
  provider: authOptions?.provider
577
611
  });
578
- return await this.handleRedirectAuth(organizationId, stamperInfo, authOptions);
612
+ return await this.handleRedirectAuth(organizationId, stamperInfo, authOptions, username);
579
613
  }
580
614
  } else {
581
615
  this.logger.info("EMBEDDED_PROVIDER", "Creating app-wallet", {
@@ -598,9 +632,15 @@ var EmbeddedProvider = class {
598
632
  stamperInfo,
599
633
  authProvider: "app-wallet",
600
634
  userInfo: { embeddedWalletType: this.config.embeddedWalletType },
635
+ accountDerivationIndex: 0,
636
+ // App wallets default to index 0
601
637
  status: "completed",
602
638
  createdAt: now,
603
- lastUsed: now
639
+ lastUsed: now,
640
+ authenticatorCreatedAt: now,
641
+ authenticatorExpiresAt: expiresAtMs,
642
+ lastRenewalAttempt: void 0,
643
+ username
604
644
  };
605
645
  await this.storage.saveSession(session);
606
646
  this.logger.info("EMBEDDED_PROVIDER", "App-wallet created successfully", { walletId, organizationId });
@@ -611,7 +651,7 @@ var EmbeddedProvider = class {
611
651
  * We use this method to handle JWT-based authentication for user-wallets.
612
652
  * It authenticates using the provided JWT token and creates a completed session.
613
653
  */
614
- async handleJWTAuth(organizationId, stamperInfo, authOptions) {
654
+ async handleJWTAuth(organizationId, stamperInfo, authOptions, expiresAtMs, username) {
615
655
  this.logger.info("EMBEDDED_PROVIDER", "Using JWT authentication flow");
616
656
  if (!authOptions.jwtToken) {
617
657
  this.logger.error("EMBEDDED_PROVIDER", "JWT token missing for JWT authentication");
@@ -634,9 +674,14 @@ var EmbeddedProvider = class {
634
674
  stamperInfo,
635
675
  authProvider: authResult.provider,
636
676
  userInfo: authResult.userInfo,
677
+ accountDerivationIndex: authResult.accountDerivationIndex,
637
678
  status: "completed",
638
679
  createdAt: now,
639
- lastUsed: now
680
+ lastUsed: now,
681
+ authenticatorCreatedAt: now,
682
+ authenticatorExpiresAt: expiresAtMs,
683
+ lastRenewalAttempt: void 0,
684
+ username
640
685
  };
641
686
  this.logger.log("EMBEDDED_PROVIDER", "Saving JWT session");
642
687
  await this.storage.saveSession(session);
@@ -647,7 +692,7 @@ var EmbeddedProvider = class {
647
692
  * It saves a temporary session before redirecting to prevent losing state during the redirect flow.
648
693
  * Session timestamp is updated before redirect to prevent race conditions.
649
694
  */
650
- async handleRedirectAuth(organizationId, stamperInfo, authOptions) {
695
+ async handleRedirectAuth(organizationId, stamperInfo, authOptions, username) {
651
696
  this.logger.info("EMBEDDED_PROVIDER", "Using Phantom Connect authentication flow (redirect-based)", {
652
697
  provider: authOptions?.provider,
653
698
  hasRedirectUrl: !!this.config.authOptions?.redirectUrl,
@@ -663,9 +708,15 @@ var EmbeddedProvider = class {
663
708
  stamperInfo,
664
709
  authProvider: "phantom-connect",
665
710
  userInfo: { provider: authOptions?.provider },
711
+ accountDerivationIndex: void 0,
712
+ // Will be set when redirect completes
666
713
  status: "pending",
667
714
  createdAt: now,
668
- lastUsed: now
715
+ lastUsed: now,
716
+ authenticatorCreatedAt: now,
717
+ authenticatorExpiresAt: now + AUTHENTICATOR_EXPIRATION_TIME_MS,
718
+ lastRenewalAttempt: void 0,
719
+ username: username || `user-${stamperInfo.keyId.substring(0, 8)}`
669
720
  };
670
721
  this.logger.log("EMBEDDED_PROVIDER", "Saving temporary session before redirect", {
671
722
  sessionId: tempSession.sessionId,
@@ -697,6 +748,7 @@ var EmbeddedProvider = class {
697
748
  });
698
749
  tempSession.walletId = authResult.walletId;
699
750
  tempSession.authProvider = authResult.provider || tempSession.authProvider;
751
+ tempSession.accountDerivationIndex = authResult.accountDerivationIndex;
700
752
  tempSession.status = "completed";
701
753
  tempSession.lastUsed = Date.now();
702
754
  await this.storage.saveSession(tempSession);
@@ -712,16 +764,119 @@ var EmbeddedProvider = class {
712
764
  }
713
765
  session.walletId = authResult.walletId;
714
766
  session.authProvider = authResult.provider || session.authProvider;
767
+ session.accountDerivationIndex = authResult.accountDerivationIndex;
715
768
  session.status = "completed";
716
769
  session.lastUsed = Date.now();
717
770
  await this.storage.saveSession(session);
718
771
  await this.initializeClientFromSession(session);
772
+ await this.ensureValidAuthenticator();
719
773
  return {
720
774
  walletId: this.walletId,
721
775
  addresses: this.addresses,
722
776
  status: "completed"
723
777
  };
724
778
  }
779
+ /*
780
+ * Ensures the authenticator is valid and performs renewal if needed.
781
+ * The renewal of the authenticator can only happen meanwhile the previous authenticator is still valid.
782
+ */
783
+ async ensureValidAuthenticator() {
784
+ const session = await this.storage.getSession();
785
+ if (!session) {
786
+ throw new Error("No active session found");
787
+ }
788
+ const now = Date.now();
789
+ if (!session.authenticatorExpiresAt) {
790
+ this.logger.warn("EMBEDDED_PROVIDER", "Session missing authenticator timing - treating as invalid session");
791
+ await this.disconnect();
792
+ throw new Error("Invalid session - missing authenticator timing");
793
+ }
794
+ const timeUntilExpiry = session.authenticatorExpiresAt - now;
795
+ this.logger.log("EMBEDDED_PROVIDER", "Checking authenticator expiration", {
796
+ expiresAt: new Date(session.authenticatorExpiresAt).toISOString(),
797
+ timeUntilExpiry
798
+ });
799
+ if (timeUntilExpiry <= 0) {
800
+ this.logger.error("EMBEDDED_PROVIDER", "Authenticator has expired, disconnecting");
801
+ await this.disconnect();
802
+ throw new Error("Authenticator expired");
803
+ }
804
+ const renewalWindow = AUTHENTICATOR_RENEWAL_WINDOW_MS;
805
+ if (timeUntilExpiry <= renewalWindow) {
806
+ this.logger.info("EMBEDDED_PROVIDER", "Authenticator needs renewal", {
807
+ expiresAt: new Date(session.authenticatorExpiresAt).toISOString(),
808
+ timeUntilExpiry,
809
+ renewalWindow
810
+ });
811
+ try {
812
+ await this.renewAuthenticator(session);
813
+ this.logger.info("EMBEDDED_PROVIDER", "Authenticator renewed successfully");
814
+ } catch (error) {
815
+ this.logger.error("EMBEDDED_PROVIDER", "Failed to renew authenticator", {
816
+ error: error instanceof Error ? error.message : String(error)
817
+ });
818
+ }
819
+ }
820
+ }
821
+ /*
822
+ * We use this method to perform silent authenticator renewal.
823
+ * It generates a new keypair, creates a new authenticator, and switches to it.
824
+ */
825
+ async renewAuthenticator(session) {
826
+ if (!this.client) {
827
+ throw new Error("Client not initialized");
828
+ }
829
+ this.logger.info("EMBEDDED_PROVIDER", "Starting authenticator renewal");
830
+ try {
831
+ const newKeyInfo = await this.stamper.rotateKeyPair();
832
+ this.logger.log("EMBEDDED_PROVIDER", "Generated new keypair for renewal", {
833
+ newKeyId: newKeyInfo.keyId,
834
+ newPublicKey: newKeyInfo.publicKey
835
+ });
836
+ const base64urlPublicKey = base64urlEncode(bs58.decode(newKeyInfo.publicKey));
837
+ const expiresAtMs = Date.now() + AUTHENTICATOR_EXPIRATION_TIME_MS;
838
+ let authenticatorResult;
839
+ try {
840
+ authenticatorResult = await this.client.createAuthenticator({
841
+ organizationId: session.organizationId,
842
+ username: session.username,
843
+ authenticatorName: `auth-${newKeyInfo.keyId.substring(0, 8)}`,
844
+ authenticator: {
845
+ authenticatorName: `auth-${newKeyInfo.keyId.substring(0, 8)}`,
846
+ authenticatorKind: "keypair",
847
+ publicKey: base64urlPublicKey,
848
+ algorithm: "Ed25519"
849
+ // Commented for now until KMS supports fully expiring organizations
850
+ // expiresAtMs: expiresAtMs,
851
+ },
852
+ replaceExpirable: true
853
+ });
854
+ } catch (error) {
855
+ this.logger.error("EMBEDDED_PROVIDER", "Failed to create new authenticator", {
856
+ error: error instanceof Error ? error.message : String(error)
857
+ });
858
+ await this.stamper.rollbackRotation();
859
+ throw new Error(`Failed to create new authenticator: ${error instanceof Error ? error.message : String(error)}`);
860
+ }
861
+ this.logger.info("EMBEDDED_PROVIDER", "Created new authenticator", {
862
+ authenticatorId: authenticatorResult.id
863
+ });
864
+ await this.stamper.commitRotation(authenticatorResult.id || "unknown");
865
+ const now = Date.now();
866
+ session.stamperInfo = newKeyInfo;
867
+ session.authenticatorCreatedAt = now;
868
+ session.authenticatorExpiresAt = expiresAtMs;
869
+ session.lastRenewalAttempt = now;
870
+ await this.storage.saveSession(session);
871
+ this.logger.info("EMBEDDED_PROVIDER", "Authenticator renewal completed successfully", {
872
+ newKeyId: newKeyInfo.keyId,
873
+ expiresAt: new Date(expiresAtMs).toISOString()
874
+ });
875
+ } catch (error) {
876
+ await this.stamper.rollbackRotation();
877
+ throw error;
878
+ }
879
+ }
725
880
  /*
726
881
  * We use this method to initialize the PhantomClient and fetch wallet addresses from a completed session.
727
882
  * This is the final step that sets up the provider's client state and retrieves available addresses.
@@ -746,6 +901,8 @@ var EmbeddedProvider = class {
746
901
  }
747
902
  };
748
903
  export {
904
+ AUTHENTICATOR_EXPIRATION_TIME_MS,
905
+ AUTHENTICATOR_RENEWAL_WINDOW_MS,
749
906
  EmbeddedProvider,
750
907
  JWTAuth,
751
908
  generateSessionId,