@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.cjs CHANGED
@@ -39,14 +39,13 @@ var import_zustand = require("zustand");
39
39
  var import_middleware = require("zustand/middleware");
40
40
  var SessionStore = class {
41
41
  _store;
42
- /**
43
- * Initializes a new instance of the `SessionStore` class by creating a new `zustand`store with the initial state.
44
- */
45
42
  constructor() {
46
43
  this._store = (0, import_zustand.createStore)()(
47
44
  (0, import_middleware.persist)(this._getInitialState, {
48
- name: "fourt.io-signer-session",
49
- storage: (0, import_middleware.createJSONStorage)(() => localStorage)
45
+ name: "fourt-session",
46
+ storage: (0, import_middleware.createJSONStorage)(() => localStorage),
47
+ // keep only these keys in persisted storage
48
+ partialize: (state) => ({ bundle: state.bundle, type: state.type })
50
49
  })
51
50
  );
52
51
  }
@@ -402,26 +401,29 @@ var UserModule = class {
402
401
  this._webSignerClient = _webSignerClient;
403
402
  }
404
403
  /**
405
- * Gets the user information.
406
- *
407
- * @returns {User | undefined} user information.
404
+ * Retrieves information for the authenticated user.
405
+ * Assumes a user is already logged in, otherwise it will throw an error.
408
406
  */
409
- get info() {
410
- return this._webSignerClient.user;
407
+ async getInfo() {
408
+ return this._webSignerClient.getUser();
411
409
  }
412
- /** Gets the user token.
413
- *
414
- * @returns {string | undefined} user token.
410
+ /**
411
+ * Checks if a user is currently logged in to the fourt.io SDK.
415
412
  */
416
- get token() {
413
+ async isLoggedIn() {
414
+ return this._webSignerClient.isLoggedIn();
415
+ }
416
+ /**
417
+ * Generates an access token with a lifespan of 15 minutes.
418
+ * Assumes a user is already logged in, otherwise it will throw an error.
419
+ */
420
+ async getToken() {
417
421
  return this._webSignerClient.getToken();
418
422
  }
419
423
  /**
420
424
  * Logs out the user.
421
- *
422
- * @returns {void}
423
425
  */
424
- logout() {
426
+ async logout() {
425
427
  return this._webSignerClient.logout();
426
428
  }
427
429
  };
@@ -488,14 +490,17 @@ var UnauthenticatedError = class _UnauthenticatedError extends SDKError {
488
490
  }
489
491
  };
490
492
 
491
- // src/types/Routes.ts
493
+ // src/types/routes.ts
492
494
  var ROUTE_METHOD_MAP = {
493
495
  "/v1/signup": "POST",
494
496
  "/v1/email-auth": "POST",
495
497
  "/v1/lookup": "POST",
496
498
  "/v1/signin": "POST",
497
499
  "/v1/sign": "POST",
498
- "v1/oauth/init": "POST"
500
+ "/v1/oauth/init": "POST",
501
+ "/v1/refresh": "POST",
502
+ "/v1/logout": "POST",
503
+ "/v1/me": "GET"
499
504
  };
500
505
 
501
506
  // src/signer/index.ts
@@ -505,7 +510,8 @@ var SignerClient = class {
505
510
  _turnkeyClient;
506
511
  _configuration;
507
512
  _sessionStore;
508
- _user;
513
+ _refreshPromise;
514
+ _refreshTimer;
509
515
  constructor({
510
516
  stamper,
511
517
  configuration: { apiUrl, paymasterRpcUrl, ...requiredConfiguration }
@@ -521,54 +527,97 @@ var SignerClient = class {
521
527
  };
522
528
  this._sessionStore = new SessionStore();
523
529
  }
524
- logout() {
525
- this._user = void 0;
526
- this.sessionStore.clearAll();
527
- }
528
530
  get configuration() {
529
531
  return this._configuration;
530
532
  }
531
- get user() {
532
- if (this._user) return this._user;
533
- if (!this.sessionStore.token) {
534
- this.sessionStore.clearAll();
535
- return void 0;
536
- }
537
- const decodedToken = (0, import_jwt_decode.jwtDecode)(this.sessionStore.token);
538
- if (decodedToken.exp && (0, import_date_fns.isPast)(new Date((0, import_date_fns.secondsToMilliseconds)(decodedToken.exp)))) {
539
- this.sessionStore.clearAll();
540
- return void 0;
533
+ async getUser() {
534
+ if (this._sessionStore.user) return this._sessionStore.user;
535
+ try {
536
+ const user = await this.request("/v1/me");
537
+ this._sessionStore.user = user;
538
+ return user;
539
+ } catch (error) {
540
+ if (error instanceof UnauthorizedError) {
541
+ try {
542
+ await this._refreshToken();
543
+ const user = await this.request("/v1/me");
544
+ this._sessionStore.user = user;
545
+ return user;
546
+ } catch (error2) {
547
+ throw error2;
548
+ }
549
+ }
550
+ throw error;
541
551
  }
542
- if (this.sessionStore.user) this._user = this.sessionStore.user;
543
- return this._user;
544
552
  }
545
- set user(value) {
546
- this._user = value;
553
+ async isLoggedIn() {
554
+ const token = this._sessionStore.token;
555
+ if (token && !this._isTokenExpired(token)) return true;
556
+ try {
557
+ await this._refreshToken();
558
+ return !!this._sessionStore.token;
559
+ } catch {
560
+ return false;
561
+ }
547
562
  }
548
- set stamper(stamper) {
549
- this._turnkeyClient.stamper = stamper;
563
+ async getToken() {
564
+ if (!this._sessionStore.token) {
565
+ try {
566
+ await this._refreshToken();
567
+ } catch {
568
+ throw new UnauthorizedError({
569
+ message: "No token found, user might not be logged in"
570
+ });
571
+ }
572
+ } else if (this._isTokenExpired(this._sessionStore.token)) {
573
+ try {
574
+ await this._refreshToken();
575
+ } catch {
576
+ throw new UnauthorizedError({
577
+ message: "Token expired and refresh failed"
578
+ });
579
+ }
580
+ }
581
+ const token = this._sessionStore.token;
582
+ if (!token) {
583
+ throw new UnauthorizedError({
584
+ message: "No token found, user might not be logged in"
585
+ });
586
+ }
587
+ return token;
550
588
  }
551
- get stamper() {
552
- return this._turnkeyClient.stamper;
589
+ _isTokenExpired(token) {
590
+ try {
591
+ const decoded = (0, import_jwt_decode.jwtDecode)(token);
592
+ if (decoded.exp) {
593
+ return decoded.exp * 1e3 <= Date.now();
594
+ }
595
+ return true;
596
+ } catch {
597
+ return true;
598
+ }
553
599
  }
554
- get sessionStore() {
555
- return this._sessionStore;
600
+ async logout() {
601
+ if (this._refreshTimer) clearTimeout(this._refreshTimer);
602
+ this._refreshTimer = void 0;
603
+ await this.request("/v1/logout");
604
+ this._sessionStore.clearAll();
556
605
  }
557
606
  async signRawMessage(msg) {
558
- if (!this._user) {
607
+ if (!this._sessionStore.token || !this._sessionStore.user) {
559
608
  throw new UnauthorizedError({
560
609
  message: "SignerClient must be authenticated to sign a message"
561
610
  });
562
611
  }
563
612
  const stampedRequest = await this._turnkeyClient.stampSignRawPayload({
564
- organizationId: this._user.subOrgId,
613
+ organizationId: this._sessionStore.user.subOrgId,
565
614
  type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2",
566
615
  timestampMs: Date.now().toString(),
567
616
  parameters: {
568
617
  encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
569
618
  hashFunction: "HASH_FUNCTION_NO_OP",
570
619
  payload: msg,
571
- signWith: this._user.walletAddress
620
+ signWith: this._sessionStore.user.walletAddress
572
621
  }
573
622
  });
574
623
  const { signature } = await this.request("/v1/sign", {
@@ -576,8 +625,11 @@ var SignerClient = class {
576
625
  });
577
626
  return signature;
578
627
  }
579
- getToken() {
580
- return this._sessionStore.token;
628
+ set stamper(stamper) {
629
+ this._turnkeyClient.stamper = stamper;
630
+ }
631
+ get stamper() {
632
+ return this._turnkeyClient.stamper;
581
633
  }
582
634
  async lookUpUser(email) {
583
635
  try {
@@ -597,7 +649,7 @@ var SignerClient = class {
597
649
  }
598
650
  }
599
651
  async whoAmI(subOrgId) {
600
- const orgId = subOrgId || this._user?.subOrgId;
652
+ const orgId = subOrgId || this._sessionStore.user?.subOrgId;
601
653
  if (!orgId) throw new BadRequestError({ message: "No orgId provided" });
602
654
  const stampedRequest = await this._turnkeyClient.stampGetWhoami({
603
655
  organizationId: orgId
@@ -612,19 +664,20 @@ var SignerClient = class {
612
664
  return void 0;
613
665
  }
614
666
  })();
615
- this._user = {
667
+ this._sessionStore.user = {
616
668
  ...user,
617
669
  credentialId
618
670
  };
619
- this.sessionStore.user = this.user;
620
- this.sessionStore.token = token;
671
+ this._sessionStore.token = token;
672
+ this._scheduleRefresh(token);
621
673
  }
622
674
  async request(route, body) {
623
675
  const url = new URL(`${route}`, this._configuration.apiUrl);
624
- const token = this.sessionStore.token;
676
+ const token = this._sessionStore.token;
625
677
  const headers = {
626
678
  "Content-Type": "application/json",
627
- "X-FOURT-KEY": this._configuration.apiKey
679
+ "X-FOURT-KEY": this._configuration.apiKey,
680
+ "X-CSRF-TOKEN": this._getCookie("csrfToken") ?? ""
628
681
  };
629
682
  if (token) {
630
683
  headers["Authorization"] = `Bearer ${token}`;
@@ -639,7 +692,6 @@ var SignerClient = class {
639
692
  if (error) {
640
693
  switch (error.kind) {
641
694
  case "UnauthorizedError": {
642
- this.logout();
643
695
  throw new UnauthorizedError({ message: error.message });
644
696
  }
645
697
  case "NotFoundError": {
@@ -655,6 +707,81 @@ var SignerClient = class {
655
707
  }
656
708
  return { ...data };
657
709
  }
710
+ _scheduleRefresh(token) {
711
+ try {
712
+ const decoded = (0, import_jwt_decode.jwtDecode)(token);
713
+ if (!decoded.exp) return;
714
+ const expiryDate = new Date(decoded.exp * 1e3);
715
+ const refreshDate = (0, import_date_fns.subMinutes)(expiryDate, 2);
716
+ const delay = (0, import_date_fns.isBefore)(refreshDate, /* @__PURE__ */ new Date()) ? 0 : (0, import_date_fns.differenceInMilliseconds)(refreshDate, /* @__PURE__ */ new Date());
717
+ if (this._refreshTimer) clearTimeout(this._refreshTimer);
718
+ this._refreshTimer = setTimeout(() => {
719
+ this._refreshTimer = void 0;
720
+ this._refreshToken();
721
+ }, delay);
722
+ } catch {
723
+ }
724
+ }
725
+ async _refreshToken() {
726
+ if (this._refreshPromise) return this._refreshPromise;
727
+ this._refreshPromise = (async () => {
728
+ const TIMEOUT_MS = 1e4;
729
+ const RETRY_DELAY_MS = 5e3;
730
+ try {
731
+ const refreshPromise = this.request("/v1/refresh");
732
+ const data = await Promise.race([
733
+ refreshPromise,
734
+ new Promise(
735
+ (_, reject) => setTimeout(() => reject(new Error("Refresh timeout")), TIMEOUT_MS)
736
+ )
737
+ ]);
738
+ if (!data || !data.token) {
739
+ throw new UnauthorizedError({
740
+ message: "Refresh did not return a token"
741
+ });
742
+ }
743
+ this._sessionStore.token = data.token;
744
+ this._scheduleRefresh(data.token);
745
+ } catch (error) {
746
+ if (error instanceof UnauthorizedError) {
747
+ try {
748
+ this._sessionStore.clearAll();
749
+ } catch {
750
+ }
751
+ throw error;
752
+ }
753
+ if (this._refreshTimer) clearTimeout(this._refreshTimer);
754
+ const MAX_RETRIES = 5;
755
+ let retryCount = 0;
756
+ this._refreshTimer = setTimeout(() => {
757
+ this._refreshTimer = void 0;
758
+ void this._refreshToken().catch(() => {
759
+ retryCount++;
760
+ if (retryCount <= MAX_RETRIES) {
761
+ const nextDelay = Math.min(
762
+ RETRY_DELAY_MS * 2 ** (retryCount - 1),
763
+ 6e4
764
+ );
765
+ this._refreshTimer = setTimeout(() => {
766
+ this._refreshTimer = void 0;
767
+ void this._refreshToken().catch(() => {
768
+ });
769
+ }, nextDelay);
770
+ }
771
+ });
772
+ }, RETRY_DELAY_MS);
773
+ throw error;
774
+ } finally {
775
+ this._refreshPromise = void 0;
776
+ }
777
+ })();
778
+ return this._refreshPromise;
779
+ }
780
+ _getCookie(name) {
781
+ if (typeof document === "undefined") return null;
782
+ const match = document.cookie.match(new RegExp("(^| )" + name + "=([^;]+)"));
783
+ return match ? decodeURIComponent(match[2]) : null;
784
+ }
658
785
  };
659
786
 
660
787
  // src/signer/web.ts
@@ -697,10 +824,6 @@ var WebSignerClient = class extends SignerClient {
697
824
  this.webauthnStamper = new import_webauthn_stamper.WebauthnStamper({ rpId: webauthn.rpId });
698
825
  this.oauthConfiguration = oauth;
699
826
  }
700
- async signRawMessage(msg) {
701
- await this.updateStamper();
702
- return super.signRawMessage(msg);
703
- }
704
827
  async logout() {
705
828
  super.logout();
706
829
  this.iframeStamper.clear();
@@ -714,23 +837,20 @@ var WebSignerClient = class extends SignerClient {
714
837
  this.iframeStamper = stamper;
715
838
  await this._initIframeStamper();
716
839
  }
840
+ async signRawMessage(msg) {
841
+ await this._updateStamper();
842
+ return super.signRawMessage(msg);
843
+ }
717
844
  /**
718
- * Checks for an existing session and if exists, updates the stamper accordingly.
845
+ * Get the pre-filled URL for initiating oauth with a specific provider.
719
846
  *
847
+ * @param {string} provider provider for which we are getting the URL, currently google or apple
720
848
  */
721
- async updateStamper() {
722
- if (this._sessionStore.type === void 0 && (this._sessionStore.bundle === void 0 || this._sessionStore.token === void 0))
723
- return;
724
- if (this._sessionStore.type === "passkeys" /* Passkeys */) {
725
- this.stamper = this.webauthnStamper;
726
- } else {
727
- this.stamper = this.iframeStamper;
728
- await this.completeAuthWithBundle({
729
- bundle: this._sessionStore.bundle,
730
- subOrgId: this.user?.subOrgId,
731
- sessionType: this._sessionStore.type
732
- });
733
- }
849
+ async getOAuthInitUrl(provider) {
850
+ const { url } = await this.request("/v1/oauth/init", {
851
+ provider
852
+ });
853
+ return url;
734
854
  }
735
855
  /**
736
856
  * Signs in a user with webauthn.
@@ -745,12 +865,12 @@ var WebSignerClient = class extends SignerClient {
745
865
  this.stamper = this.webauthnStamper;
746
866
  await this.whoAmI(existingUserSubOrgId);
747
867
  this._sessionStore.type = "passkeys" /* Passkeys */;
748
- if (!this.user || !this.user.credentialId) {
868
+ if (!this._sessionStore.user || !this._sessionStore.user.credentialId) {
749
869
  return;
750
870
  }
751
871
  this.webauthnStamper.allowCredentials = [
752
872
  {
753
- id: LibBase64.toBuffer(this.user.credentialId),
873
+ id: LibBase64.toBuffer(this._sessionStore.user.credentialId),
754
874
  type: "public-key",
755
875
  transports: ["internal", "usb"]
756
876
  }
@@ -773,23 +893,6 @@ var WebSignerClient = class extends SignerClient {
773
893
  async getIframePublicKey() {
774
894
  return await this._initIframeStamper();
775
895
  }
776
- /**
777
- * Signs in a user with email.
778
- *
779
- * @param {EmailInitializeAuthParams} params params for the sign in
780
- */
781
- async _signInWithEmail({
782
- email,
783
- expirationSeconds,
784
- redirectUrl
785
- }) {
786
- return this.request("/v1/email-auth", {
787
- email,
788
- targetPublicKey: await this.getIframePublicKey(),
789
- expirationSeconds,
790
- redirectUrl: redirectUrl.toString()
791
- });
792
- }
793
896
  /**
794
897
  * Completes the authentication process with a credential bundle.
795
898
  *
@@ -809,6 +912,40 @@ var WebSignerClient = class extends SignerClient {
809
912
  this._sessionStore.type = sessionType;
810
913
  this._sessionStore.bundle = bundle;
811
914
  }
915
+ /**
916
+ * Checks for an existing session and if exists, updates the stamper accordingly.
917
+ */
918
+ async _updateStamper() {
919
+ if (this._sessionStore.type === void 0 && (this._sessionStore.bundle === void 0 || this._sessionStore.token === void 0))
920
+ return;
921
+ if (this._sessionStore.type === "passkeys" /* Passkeys */) {
922
+ this.stamper = this.webauthnStamper;
923
+ } else {
924
+ this.stamper = this.iframeStamper;
925
+ await this.completeAuthWithBundle({
926
+ bundle: this._sessionStore.bundle,
927
+ subOrgId: this._sessionStore.user?.subOrgId,
928
+ sessionType: this._sessionStore.type
929
+ });
930
+ }
931
+ }
932
+ /**
933
+ * Signs in a user with email.
934
+ *
935
+ * @param {EmailInitializeAuthParams} params params for the sign in
936
+ */
937
+ async _signInWithEmail({
938
+ email,
939
+ expirationSeconds,
940
+ redirectUrl
941
+ }) {
942
+ return this.request("/v1/email-auth", {
943
+ email,
944
+ targetPublicKey: await this.getIframePublicKey(),
945
+ expirationSeconds,
946
+ redirectUrl: redirectUrl.toString()
947
+ });
948
+ }
812
949
  /**
813
950
  * Creates a passkey account using the webauthn stamper.
814
951
  *
@@ -818,28 +955,20 @@ var WebSignerClient = class extends SignerClient {
818
955
  const { challenge, attestation } = await this._webauthnGenerateAttestation(
819
956
  params.email
820
957
  );
821
- const {
822
- token,
823
- user: { id, email, subOrgId, walletAddress, salt, smartAccountAddress }
824
- } = await this.request("/v1/signup", {
958
+ const { token, user } = await this.request("/v1/signup", {
825
959
  passkey: {
826
960
  challenge: LibBase64.fromBuffer(challenge),
827
961
  attestation
828
962
  },
829
963
  email: params.email
830
964
  });
831
- this.user = {
832
- id,
833
- email,
834
- subOrgId,
835
- walletAddress,
836
- salt,
837
- smartAccountAddress,
965
+ this._sessionStore.user = {
966
+ ...user,
838
967
  credentialId: attestation.credentialId
839
968
  };
840
- this._sessionStore.user = this.user;
841
969
  this._sessionStore.type = "passkeys" /* Passkeys */;
842
970
  this._sessionStore.token = token;
971
+ this._scheduleRefresh(token);
843
972
  }
844
973
  /**
845
974
  * Creates an email account using the iframe stamper.
@@ -916,17 +1045,6 @@ var WebSignerClient = class extends SignerClient {
916
1045
  this.stamper = this.iframeStamper;
917
1046
  return this.iframeStamper.publicKey();
918
1047
  }
919
- /**
920
- * Get the pre-filled URL for initiating oauth with a specific provider.
921
- *
922
- * @param {string} provider provider for which we are getting the URL, currently google or apple
923
- */
924
- async getOAuthInitUrl(provider) {
925
- const { url } = await this.request("v1/oauth/init", {
926
- provider
927
- });
928
- return url;
929
- }
930
1048
  };
931
1049
 
932
1050
  // src/third-party/viem.ts
@@ -939,13 +1057,13 @@ var ViemModule = class {
939
1057
  this._signerClient = _signerClient;
940
1058
  }
941
1059
  async toLocalAccount() {
942
- const user = this._signerClient.user;
1060
+ const user = await this._signerClient.getUser();
943
1061
  if (!user) {
944
1062
  throw new UnauthenticatedError({ message: "Signer not authenticated" });
945
1063
  }
946
1064
  return (0, import_accounts.toAccount)({
947
1065
  address: user.walletAddress,
948
- signMessage: (msg) => this.signMessage(msg.message),
1066
+ signMessage: ({ message }) => this.signMessage(message),
949
1067
  signTypedData: (typedDataDefinition) => this.signTypedData(typedDataDefinition),
950
1068
  signTransaction: this.signTransaction
951
1069
  });
@@ -954,7 +1072,7 @@ var ViemModule = class {
954
1072
  client,
955
1073
  owner
956
1074
  }) {
957
- const user = this._signerClient.user;
1075
+ const user = await this._signerClient.getUser();
958
1076
  if (!user) {
959
1077
  throw new UnauthenticatedError({ message: "Signer not authenticated" });
960
1078
  }
@@ -990,7 +1108,7 @@ var ViemModule = class {
990
1108
  }
991
1109
  async signTransaction(transaction, options) {
992
1110
  const serializeFn = options?.serializer ?? import_viem.serializeTransaction;
993
- const serializedTx = serializeFn(transaction);
1111
+ const serializedTx = await serializeFn(transaction);
994
1112
  const signatureHex = await this._signerClient.signRawMessage(
995
1113
  (0, import_viem.keccak256)(serializedTx)
996
1114
  );