@fourt/sdk 1.1.7 → 1.2.0

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
@@ -1,16 +1,15 @@
1
1
  // src/session/index.ts
2
2
  import { createStore } from "zustand";
3
- import { createJSONStorage, persist } from "zustand/middleware";
3
+ import { persist, createJSONStorage } from "zustand/middleware";
4
4
  var SessionStore = class {
5
5
  _store;
6
- /**
7
- * Initializes a new instance of the `SessionStore` class by creating a new `zustand`store with the initial state.
8
- */
9
6
  constructor() {
10
7
  this._store = createStore()(
11
8
  persist(this._getInitialState, {
12
- name: "fourt.io-signer-session",
13
- storage: createJSONStorage(() => localStorage)
9
+ name: "fourt-session",
10
+ storage: createJSONStorage(() => localStorage),
11
+ // keep only these keys in persisted storage
12
+ partialize: (state) => ({ bundle: state.bundle, type: state.type })
14
13
  })
15
14
  );
16
15
  }
@@ -366,26 +365,29 @@ var UserModule = class {
366
365
  this._webSignerClient = _webSignerClient;
367
366
  }
368
367
  /**
369
- * Gets the user information.
370
- *
371
- * @returns {User | undefined} user information.
368
+ * Retrieves information for the authenticated user.
369
+ * Assumes a user is already logged in, otherwise it will throw an error.
372
370
  */
373
- get info() {
374
- return this._webSignerClient.user;
371
+ async getInfo() {
372
+ return this._webSignerClient.getUser();
375
373
  }
376
- /** Gets the user token.
377
- *
378
- * @returns {string | undefined} user token.
374
+ /**
375
+ * Checks if a user is currently logged in to the fourt.io SDK.
379
376
  */
380
- get token() {
377
+ async isLoggedIn() {
378
+ return this._webSignerClient.isLoggedIn();
379
+ }
380
+ /**
381
+ * Generates an access token with a lifespan of 15 minutes.
382
+ * Assumes a user is already logged in, otherwise it will throw an error.
383
+ */
384
+ async getToken() {
381
385
  return this._webSignerClient.getToken();
382
386
  }
383
387
  /**
384
388
  * Logs out the user.
385
- *
386
- * @returns {void}
387
389
  */
388
- logout() {
390
+ async logout() {
389
391
  return this._webSignerClient.logout();
390
392
  }
391
393
  };
@@ -452,24 +454,28 @@ var UnauthenticatedError = class _UnauthenticatedError extends SDKError {
452
454
  }
453
455
  };
454
456
 
455
- // src/types/Routes.ts
457
+ // src/types/routes.ts
456
458
  var ROUTE_METHOD_MAP = {
457
459
  "/v1/signup": "POST",
458
460
  "/v1/email-auth": "POST",
459
461
  "/v1/lookup": "POST",
460
462
  "/v1/signin": "POST",
461
463
  "/v1/sign": "POST",
462
- "v1/oauth/init": "POST"
464
+ "/v1/oauth/init": "POST",
465
+ "/v1/refresh": "POST",
466
+ "/v1/logout": "POST",
467
+ "/v1/me": "GET"
463
468
  };
464
469
 
465
470
  // src/signer/index.ts
466
471
  import { jwtDecode } from "jwt-decode";
467
- import { isPast, secondsToMilliseconds } from "date-fns";
472
+ import { differenceInMilliseconds, isBefore, subMinutes } from "date-fns";
468
473
  var SignerClient = class {
469
474
  _turnkeyClient;
470
475
  _configuration;
471
476
  _sessionStore;
472
- _user;
477
+ _refreshPromise;
478
+ _refreshTimer;
473
479
  constructor({
474
480
  stamper,
475
481
  configuration: { apiUrl, paymasterRpcUrl, ...requiredConfiguration }
@@ -485,54 +491,97 @@ var SignerClient = class {
485
491
  };
486
492
  this._sessionStore = new SessionStore();
487
493
  }
488
- logout() {
489
- this._user = void 0;
490
- this.sessionStore.clearAll();
491
- }
492
494
  get configuration() {
493
495
  return this._configuration;
494
496
  }
495
- get user() {
496
- if (this._user) return this._user;
497
- if (!this.sessionStore.token) {
498
- this.sessionStore.clearAll();
499
- return void 0;
500
- }
501
- const decodedToken = jwtDecode(this.sessionStore.token);
502
- if (decodedToken.exp && isPast(new Date(secondsToMilliseconds(decodedToken.exp)))) {
503
- this.sessionStore.clearAll();
504
- return void 0;
497
+ async getUser() {
498
+ if (this._sessionStore.user) return this._sessionStore.user;
499
+ try {
500
+ const user = await this.request("/v1/me");
501
+ this._sessionStore.user = user;
502
+ return user;
503
+ } catch (error) {
504
+ if (error instanceof UnauthorizedError) {
505
+ try {
506
+ await this._refreshToken();
507
+ const user = await this.request("/v1/me");
508
+ this._sessionStore.user = user;
509
+ return user;
510
+ } catch (error2) {
511
+ throw error2;
512
+ }
513
+ }
514
+ throw error;
505
515
  }
506
- if (this.sessionStore.user) this._user = this.sessionStore.user;
507
- return this._user;
508
516
  }
509
- set user(value) {
510
- this._user = value;
517
+ async isLoggedIn() {
518
+ const token = this._sessionStore.token;
519
+ if (token && !this._isTokenExpired(token)) return true;
520
+ try {
521
+ await this._refreshToken();
522
+ return !!this._sessionStore.token;
523
+ } catch {
524
+ return false;
525
+ }
511
526
  }
512
- set stamper(stamper) {
513
- this._turnkeyClient.stamper = stamper;
527
+ async getToken() {
528
+ if (!this._sessionStore.token) {
529
+ try {
530
+ await this._refreshToken();
531
+ } catch {
532
+ throw new UnauthorizedError({
533
+ message: "No token found, user might not be logged in"
534
+ });
535
+ }
536
+ } else if (this._isTokenExpired(this._sessionStore.token)) {
537
+ try {
538
+ await this._refreshToken();
539
+ } catch {
540
+ throw new UnauthorizedError({
541
+ message: "Token expired and refresh failed"
542
+ });
543
+ }
544
+ }
545
+ const token = this._sessionStore.token;
546
+ if (!token) {
547
+ throw new UnauthorizedError({
548
+ message: "No token found, user might not be logged in"
549
+ });
550
+ }
551
+ return token;
514
552
  }
515
- get stamper() {
516
- return this._turnkeyClient.stamper;
553
+ _isTokenExpired(token) {
554
+ try {
555
+ const decoded = jwtDecode(token);
556
+ if (decoded.exp) {
557
+ return decoded.exp * 1e3 <= Date.now();
558
+ }
559
+ return true;
560
+ } catch {
561
+ return true;
562
+ }
517
563
  }
518
- get sessionStore() {
519
- return this._sessionStore;
564
+ async logout() {
565
+ if (this._refreshTimer) clearTimeout(this._refreshTimer);
566
+ this._refreshTimer = void 0;
567
+ await this.request("/v1/logout");
568
+ this._sessionStore.clearAll();
520
569
  }
521
570
  async signRawMessage(msg) {
522
- if (!this._user) {
571
+ if (!this._sessionStore.token || !this._sessionStore.user) {
523
572
  throw new UnauthorizedError({
524
573
  message: "SignerClient must be authenticated to sign a message"
525
574
  });
526
575
  }
527
576
  const stampedRequest = await this._turnkeyClient.stampSignRawPayload({
528
- organizationId: this._user.subOrgId,
577
+ organizationId: this._sessionStore.user.subOrgId,
529
578
  type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2",
530
579
  timestampMs: Date.now().toString(),
531
580
  parameters: {
532
581
  encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
533
582
  hashFunction: "HASH_FUNCTION_NO_OP",
534
583
  payload: msg,
535
- signWith: this._user.walletAddress
584
+ signWith: this._sessionStore.user.walletAddress
536
585
  }
537
586
  });
538
587
  const { signature } = await this.request("/v1/sign", {
@@ -540,8 +589,11 @@ var SignerClient = class {
540
589
  });
541
590
  return signature;
542
591
  }
543
- getToken() {
544
- return this._sessionStore.token;
592
+ set stamper(stamper) {
593
+ this._turnkeyClient.stamper = stamper;
594
+ }
595
+ get stamper() {
596
+ return this._turnkeyClient.stamper;
545
597
  }
546
598
  async lookUpUser(email) {
547
599
  try {
@@ -561,7 +613,7 @@ var SignerClient = class {
561
613
  }
562
614
  }
563
615
  async whoAmI(subOrgId) {
564
- const orgId = subOrgId || this._user?.subOrgId;
616
+ const orgId = subOrgId || this._sessionStore.user?.subOrgId;
565
617
  if (!orgId) throw new BadRequestError({ message: "No orgId provided" });
566
618
  const stampedRequest = await this._turnkeyClient.stampGetWhoami({
567
619
  organizationId: orgId
@@ -576,19 +628,20 @@ var SignerClient = class {
576
628
  return void 0;
577
629
  }
578
630
  })();
579
- this._user = {
631
+ this._sessionStore.user = {
580
632
  ...user,
581
633
  credentialId
582
634
  };
583
- this.sessionStore.user = this.user;
584
- this.sessionStore.token = token;
635
+ this._sessionStore.token = token;
636
+ this._scheduleRefresh(token);
585
637
  }
586
638
  async request(route, body) {
587
639
  const url = new URL(`${route}`, this._configuration.apiUrl);
588
- const token = this.sessionStore.token;
640
+ const token = this._sessionStore.token;
589
641
  const headers = {
590
642
  "Content-Type": "application/json",
591
- "X-FOURT-KEY": this._configuration.apiKey
643
+ "X-FOURT-KEY": this._configuration.apiKey,
644
+ "X-CSRF-TOKEN": this._getCookie("csrfToken") ?? ""
592
645
  };
593
646
  if (token) {
594
647
  headers["Authorization"] = `Bearer ${token}`;
@@ -603,7 +656,6 @@ var SignerClient = class {
603
656
  if (error) {
604
657
  switch (error.kind) {
605
658
  case "UnauthorizedError": {
606
- this.logout();
607
659
  throw new UnauthorizedError({ message: error.message });
608
660
  }
609
661
  case "NotFoundError": {
@@ -619,6 +671,81 @@ var SignerClient = class {
619
671
  }
620
672
  return { ...data };
621
673
  }
674
+ _scheduleRefresh(token) {
675
+ try {
676
+ const decoded = jwtDecode(token);
677
+ if (!decoded.exp) return;
678
+ const expiryDate = new Date(decoded.exp * 1e3);
679
+ const refreshDate = subMinutes(expiryDate, 2);
680
+ const delay = isBefore(refreshDate, /* @__PURE__ */ new Date()) ? 0 : differenceInMilliseconds(refreshDate, /* @__PURE__ */ new Date());
681
+ if (this._refreshTimer) clearTimeout(this._refreshTimer);
682
+ this._refreshTimer = setTimeout(() => {
683
+ this._refreshTimer = void 0;
684
+ this._refreshToken();
685
+ }, delay);
686
+ } catch {
687
+ }
688
+ }
689
+ async _refreshToken() {
690
+ if (this._refreshPromise) return this._refreshPromise;
691
+ this._refreshPromise = (async () => {
692
+ const TIMEOUT_MS = 1e4;
693
+ const RETRY_DELAY_MS = 5e3;
694
+ try {
695
+ const refreshPromise = this.request("/v1/refresh");
696
+ const data = await Promise.race([
697
+ refreshPromise,
698
+ new Promise(
699
+ (_, reject) => setTimeout(() => reject(new Error("Refresh timeout")), TIMEOUT_MS)
700
+ )
701
+ ]);
702
+ if (!data || !data.token) {
703
+ throw new UnauthorizedError({
704
+ message: "Refresh did not return a token"
705
+ });
706
+ }
707
+ this._sessionStore.token = data.token;
708
+ this._scheduleRefresh(data.token);
709
+ } catch (error) {
710
+ if (error instanceof UnauthorizedError) {
711
+ try {
712
+ this._sessionStore.clearAll();
713
+ } catch {
714
+ }
715
+ throw error;
716
+ }
717
+ if (this._refreshTimer) clearTimeout(this._refreshTimer);
718
+ const MAX_RETRIES = 5;
719
+ let retryCount = 0;
720
+ this._refreshTimer = setTimeout(() => {
721
+ this._refreshTimer = void 0;
722
+ void this._refreshToken().catch(() => {
723
+ retryCount++;
724
+ if (retryCount <= MAX_RETRIES) {
725
+ const nextDelay = Math.min(
726
+ RETRY_DELAY_MS * 2 ** (retryCount - 1),
727
+ 6e4
728
+ );
729
+ this._refreshTimer = setTimeout(() => {
730
+ this._refreshTimer = void 0;
731
+ void this._refreshToken().catch(() => {
732
+ });
733
+ }, nextDelay);
734
+ }
735
+ });
736
+ }, RETRY_DELAY_MS);
737
+ throw error;
738
+ } finally {
739
+ this._refreshPromise = void 0;
740
+ }
741
+ })();
742
+ return this._refreshPromise;
743
+ }
744
+ _getCookie(name) {
745
+ if (typeof document === "undefined") return null;
746
+ const match = document.cookie.match(new RegExp("(^| )" + name + "=([^;]+)"));
747
+ return match ? decodeURIComponent(match[2]) : null;
748
+ }
622
749
  };
623
750
 
624
751
  // src/signer/web.ts
@@ -661,10 +788,6 @@ var WebSignerClient = class extends SignerClient {
661
788
  this.webauthnStamper = new WebauthnStamper({ rpId: webauthn.rpId });
662
789
  this.oauthConfiguration = oauth;
663
790
  }
664
- async signRawMessage(msg) {
665
- await this.updateStamper();
666
- return super.signRawMessage(msg);
667
- }
668
791
  async logout() {
669
792
  super.logout();
670
793
  this.iframeStamper.clear();
@@ -678,23 +801,20 @@ var WebSignerClient = class extends SignerClient {
678
801
  this.iframeStamper = stamper;
679
802
  await this._initIframeStamper();
680
803
  }
804
+ async signRawMessage(msg) {
805
+ await this._updateStamper();
806
+ return super.signRawMessage(msg);
807
+ }
681
808
  /**
682
- * Checks for an existing session and if exists, updates the stamper accordingly.
809
+ * Get the pre-filled URL for initiating oauth with a specific provider.
683
810
  *
811
+ * @param {string} provider provider for which we are getting the URL, currently google or apple
684
812
  */
685
- async updateStamper() {
686
- if (this._sessionStore.type === void 0 && (this._sessionStore.bundle === void 0 || this._sessionStore.token === void 0))
687
- return;
688
- if (this._sessionStore.type === "passkeys" /* Passkeys */) {
689
- this.stamper = this.webauthnStamper;
690
- } else {
691
- this.stamper = this.iframeStamper;
692
- await this.completeAuthWithBundle({
693
- bundle: this._sessionStore.bundle,
694
- subOrgId: this.user?.subOrgId,
695
- sessionType: this._sessionStore.type
696
- });
697
- }
813
+ async getOAuthInitUrl(provider) {
814
+ const { url } = await this.request("/v1/oauth/init", {
815
+ provider
816
+ });
817
+ return url;
698
818
  }
699
819
  /**
700
820
  * Signs in a user with webauthn.
@@ -709,12 +829,12 @@ var WebSignerClient = class extends SignerClient {
709
829
  this.stamper = this.webauthnStamper;
710
830
  await this.whoAmI(existingUserSubOrgId);
711
831
  this._sessionStore.type = "passkeys" /* Passkeys */;
712
- if (!this.user || !this.user.credentialId) {
832
+ if (!this._sessionStore.user || !this._sessionStore.user.credentialId) {
713
833
  return;
714
834
  }
715
835
  this.webauthnStamper.allowCredentials = [
716
836
  {
717
- id: LibBase64.toBuffer(this.user.credentialId),
837
+ id: LibBase64.toBuffer(this._sessionStore.user.credentialId),
718
838
  type: "public-key",
719
839
  transports: ["internal", "usb"]
720
840
  }
@@ -737,23 +857,6 @@ var WebSignerClient = class extends SignerClient {
737
857
  async getIframePublicKey() {
738
858
  return await this._initIframeStamper();
739
859
  }
740
- /**
741
- * Signs in a user with email.
742
- *
743
- * @param {EmailInitializeAuthParams} params params for the sign in
744
- */
745
- async _signInWithEmail({
746
- email,
747
- expirationSeconds,
748
- redirectUrl
749
- }) {
750
- return this.request("/v1/email-auth", {
751
- email,
752
- targetPublicKey: await this.getIframePublicKey(),
753
- expirationSeconds,
754
- redirectUrl: redirectUrl.toString()
755
- });
756
- }
757
860
  /**
758
861
  * Completes the authentication process with a credential bundle.
759
862
  *
@@ -773,6 +876,40 @@ var WebSignerClient = class extends SignerClient {
773
876
  this._sessionStore.type = sessionType;
774
877
  this._sessionStore.bundle = bundle;
775
878
  }
879
+ /**
880
+ * Checks for an existing session and if exists, updates the stamper accordingly.
881
+ */
882
+ async _updateStamper() {
883
+ if (this._sessionStore.type === void 0 && (this._sessionStore.bundle === void 0 || this._sessionStore.token === void 0))
884
+ return;
885
+ if (this._sessionStore.type === "passkeys" /* Passkeys */) {
886
+ this.stamper = this.webauthnStamper;
887
+ } else {
888
+ this.stamper = this.iframeStamper;
889
+ await this.completeAuthWithBundle({
890
+ bundle: this._sessionStore.bundle,
891
+ subOrgId: this._sessionStore.user?.subOrgId,
892
+ sessionType: this._sessionStore.type
893
+ });
894
+ }
895
+ }
896
+ /**
897
+ * Signs in a user with email.
898
+ *
899
+ * @param {EmailInitializeAuthParams} params params for the sign in
900
+ */
901
+ async _signInWithEmail({
902
+ email,
903
+ expirationSeconds,
904
+ redirectUrl
905
+ }) {
906
+ return this.request("/v1/email-auth", {
907
+ email,
908
+ targetPublicKey: await this.getIframePublicKey(),
909
+ expirationSeconds,
910
+ redirectUrl: redirectUrl.toString()
911
+ });
912
+ }
776
913
  /**
777
914
  * Creates a passkey account using the webauthn stamper.
778
915
  *
@@ -782,28 +919,20 @@ var WebSignerClient = class extends SignerClient {
782
919
  const { challenge, attestation } = await this._webauthnGenerateAttestation(
783
920
  params.email
784
921
  );
785
- const {
786
- token,
787
- user: { id, email, subOrgId, walletAddress, salt, smartAccountAddress }
788
- } = await this.request("/v1/signup", {
922
+ const { token, user } = await this.request("/v1/signup", {
789
923
  passkey: {
790
924
  challenge: LibBase64.fromBuffer(challenge),
791
925
  attestation
792
926
  },
793
927
  email: params.email
794
928
  });
795
- this.user = {
796
- id,
797
- email,
798
- subOrgId,
799
- walletAddress,
800
- salt,
801
- smartAccountAddress,
929
+ this._sessionStore.user = {
930
+ ...user,
802
931
  credentialId: attestation.credentialId
803
932
  };
804
- this._sessionStore.user = this.user;
805
933
  this._sessionStore.type = "passkeys" /* Passkeys */;
806
934
  this._sessionStore.token = token;
935
+ this._scheduleRefresh(token);
807
936
  }
808
937
  /**
809
938
  * Creates an email account using the iframe stamper.
@@ -880,17 +1009,6 @@ var WebSignerClient = class extends SignerClient {
880
1009
  this.stamper = this.iframeStamper;
881
1010
  return this.iframeStamper.publicKey();
882
1011
  }
883
- /**
884
- * Get the pre-filled URL for initiating oauth with a specific provider.
885
- *
886
- * @param {string} provider provider for which we are getting the URL, currently google or apple
887
- */
888
- async getOAuthInitUrl(provider) {
889
- const { url } = await this.request("v1/oauth/init", {
890
- provider
891
- });
892
- return url;
893
- }
894
1012
  };
895
1013
 
896
1014
  // src/third-party/viem.ts
@@ -913,13 +1031,13 @@ var ViemModule = class {
913
1031
  this._signerClient = _signerClient;
914
1032
  }
915
1033
  async toLocalAccount() {
916
- const user = this._signerClient.user;
1034
+ const user = await this._signerClient.getUser();
917
1035
  if (!user) {
918
1036
  throw new UnauthenticatedError({ message: "Signer not authenticated" });
919
1037
  }
920
1038
  return toAccount({
921
1039
  address: user.walletAddress,
922
- signMessage: (msg) => this.signMessage(msg.message),
1040
+ signMessage: ({ message }) => this.signMessage(message),
923
1041
  signTypedData: (typedDataDefinition) => this.signTypedData(typedDataDefinition),
924
1042
  signTransaction: this.signTransaction
925
1043
  });
@@ -928,7 +1046,7 @@ var ViemModule = class {
928
1046
  client,
929
1047
  owner
930
1048
  }) {
931
- const user = this._signerClient.user;
1049
+ const user = await this._signerClient.getUser();
932
1050
  if (!user) {
933
1051
  throw new UnauthenticatedError({ message: "Signer not authenticated" });
934
1052
  }
@@ -964,7 +1082,7 @@ var ViemModule = class {
964
1082
  }
965
1083
  async signTransaction(transaction, options) {
966
1084
  const serializeFn = options?.serializer ?? serializeTransaction;
967
- const serializedTx = serializeFn(transaction);
1085
+ const serializedTx = await serializeFn(transaction);
968
1086
  const signatureHex = await this._signerClient.signRawMessage(
969
1087
  keccak256(serializedTx)
970
1088
  );