@fourt/sdk 1.1.7 → 1.2.1

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
  }
@@ -46,6 +45,22 @@ var SessionStore = class {
46
45
  set token(token) {
47
46
  this._store.setState({ token });
48
47
  }
48
+ /**
49
+ * Gets the CSRF token from the session state.
50
+ *
51
+ * @returns {string | undefined} the CSRF token.
52
+ */
53
+ get csrfToken() {
54
+ return this._store.getState().csrfToken;
55
+ }
56
+ /**
57
+ * Sets the CSRF token in the session state.
58
+ *
59
+ * @param {string} csrfToken the CSRF token to set.
60
+ */
61
+ set csrfToken(csrfToken) {
62
+ this._store.setState({ csrfToken });
63
+ }
49
64
  /**
50
65
  * Gets the bundle from the session state.
51
66
  *
@@ -366,26 +381,29 @@ var UserModule = class {
366
381
  this._webSignerClient = _webSignerClient;
367
382
  }
368
383
  /**
369
- * Gets the user information.
370
- *
371
- * @returns {User | undefined} user information.
384
+ * Retrieves information for the authenticated user.
385
+ * Assumes a user is already logged in, otherwise it will throw an error.
372
386
  */
373
- get info() {
374
- return this._webSignerClient.user;
387
+ async getInfo() {
388
+ return this._webSignerClient.getUser();
375
389
  }
376
- /** Gets the user token.
377
- *
378
- * @returns {string | undefined} user token.
390
+ /**
391
+ * Checks if a user is currently logged in to the fourt.io SDK.
379
392
  */
380
- get token() {
393
+ async isLoggedIn() {
394
+ return this._webSignerClient.isLoggedIn();
395
+ }
396
+ /**
397
+ * Generates an access token with a lifespan of 15 minutes.
398
+ * Assumes a user is already logged in, otherwise it will throw an error.
399
+ */
400
+ async getToken() {
381
401
  return this._webSignerClient.getToken();
382
402
  }
383
403
  /**
384
404
  * Logs out the user.
385
- *
386
- * @returns {void}
387
405
  */
388
- logout() {
406
+ async logout() {
389
407
  return this._webSignerClient.logout();
390
408
  }
391
409
  };
@@ -452,24 +470,29 @@ var UnauthenticatedError = class _UnauthenticatedError extends SDKError {
452
470
  }
453
471
  };
454
472
 
455
- // src/types/Routes.ts
473
+ // src/types/routes.ts
456
474
  var ROUTE_METHOD_MAP = {
457
475
  "/v1/signup": "POST",
458
476
  "/v1/email-auth": "POST",
459
477
  "/v1/lookup": "POST",
460
478
  "/v1/signin": "POST",
461
479
  "/v1/sign": "POST",
462
- "v1/oauth/init": "POST"
480
+ "/v1/oauth/init": "POST",
481
+ "/v1/refresh": "POST",
482
+ "/v1/csrf-token": "GET",
483
+ "/v1/logout": "POST",
484
+ "/v1/me": "GET"
463
485
  };
464
486
 
465
487
  // src/signer/index.ts
466
488
  import { jwtDecode } from "jwt-decode";
467
- import { isPast, secondsToMilliseconds } from "date-fns";
489
+ import { differenceInMilliseconds, isBefore, subMinutes } from "date-fns";
468
490
  var SignerClient = class {
469
491
  _turnkeyClient;
470
492
  _configuration;
471
493
  _sessionStore;
472
- _user;
494
+ _refreshPromise;
495
+ _refreshTimer;
473
496
  constructor({
474
497
  stamper,
475
498
  configuration: { apiUrl, paymasterRpcUrl, ...requiredConfiguration }
@@ -485,54 +508,97 @@ var SignerClient = class {
485
508
  };
486
509
  this._sessionStore = new SessionStore();
487
510
  }
488
- logout() {
489
- this._user = void 0;
490
- this.sessionStore.clearAll();
491
- }
492
511
  get configuration() {
493
512
  return this._configuration;
494
513
  }
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;
514
+ async getUser() {
515
+ if (this._sessionStore.user) return this._sessionStore.user;
516
+ try {
517
+ const user = await this.request("/v1/me");
518
+ this._sessionStore.user = user;
519
+ return user;
520
+ } catch (error) {
521
+ if (error instanceof UnauthorizedError) {
522
+ try {
523
+ await this._refreshToken();
524
+ const user = await this.request("/v1/me");
525
+ this._sessionStore.user = user;
526
+ return user;
527
+ } catch (error2) {
528
+ throw error2;
529
+ }
530
+ }
531
+ throw error;
505
532
  }
506
- if (this.sessionStore.user) this._user = this.sessionStore.user;
507
- return this._user;
508
533
  }
509
- set user(value) {
510
- this._user = value;
534
+ async isLoggedIn() {
535
+ const token = this._sessionStore.token;
536
+ if (token && !this._isTokenExpired(token)) return true;
537
+ try {
538
+ await this._refreshToken();
539
+ return !!this._sessionStore.token;
540
+ } catch {
541
+ return false;
542
+ }
511
543
  }
512
- set stamper(stamper) {
513
- this._turnkeyClient.stamper = stamper;
544
+ async getToken() {
545
+ if (!this._sessionStore.token) {
546
+ try {
547
+ await this._refreshToken();
548
+ } catch {
549
+ throw new UnauthorizedError({
550
+ message: "No token found, user might not be logged in"
551
+ });
552
+ }
553
+ } else if (this._isTokenExpired(this._sessionStore.token)) {
554
+ try {
555
+ await this._refreshToken();
556
+ } catch {
557
+ throw new UnauthorizedError({
558
+ message: "Token expired and refresh failed"
559
+ });
560
+ }
561
+ }
562
+ const token = this._sessionStore.token;
563
+ if (!token) {
564
+ throw new UnauthorizedError({
565
+ message: "No token found, user might not be logged in"
566
+ });
567
+ }
568
+ return token;
514
569
  }
515
- get stamper() {
516
- return this._turnkeyClient.stamper;
570
+ _isTokenExpired(token) {
571
+ try {
572
+ const decoded = jwtDecode(token);
573
+ if (decoded.exp) {
574
+ return decoded.exp * 1e3 <= Date.now();
575
+ }
576
+ return true;
577
+ } catch {
578
+ return true;
579
+ }
517
580
  }
518
- get sessionStore() {
519
- return this._sessionStore;
581
+ async logout() {
582
+ if (this._refreshTimer) clearTimeout(this._refreshTimer);
583
+ this._refreshTimer = void 0;
584
+ await this.request("/v1/logout");
585
+ this._sessionStore.clearAll();
520
586
  }
521
587
  async signRawMessage(msg) {
522
- if (!this._user) {
588
+ if (!this._sessionStore.token || !this._sessionStore.user) {
523
589
  throw new UnauthorizedError({
524
590
  message: "SignerClient must be authenticated to sign a message"
525
591
  });
526
592
  }
527
593
  const stampedRequest = await this._turnkeyClient.stampSignRawPayload({
528
- organizationId: this._user.subOrgId,
594
+ organizationId: this._sessionStore.user.subOrgId,
529
595
  type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2",
530
596
  timestampMs: Date.now().toString(),
531
597
  parameters: {
532
598
  encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
533
599
  hashFunction: "HASH_FUNCTION_NO_OP",
534
600
  payload: msg,
535
- signWith: this._user.walletAddress
601
+ signWith: this._sessionStore.user.walletAddress
536
602
  }
537
603
  });
538
604
  const { signature } = await this.request("/v1/sign", {
@@ -540,8 +606,11 @@ var SignerClient = class {
540
606
  });
541
607
  return signature;
542
608
  }
543
- getToken() {
544
- return this._sessionStore.token;
609
+ set stamper(stamper) {
610
+ this._turnkeyClient.stamper = stamper;
611
+ }
612
+ get stamper() {
613
+ return this._turnkeyClient.stamper;
545
614
  }
546
615
  async lookUpUser(email) {
547
616
  try {
@@ -561,12 +630,12 @@ var SignerClient = class {
561
630
  }
562
631
  }
563
632
  async whoAmI(subOrgId) {
564
- const orgId = subOrgId || this._user?.subOrgId;
633
+ const orgId = subOrgId || this._sessionStore.user?.subOrgId;
565
634
  if (!orgId) throw new BadRequestError({ message: "No orgId provided" });
566
635
  const stampedRequest = await this._turnkeyClient.stampGetWhoami({
567
636
  organizationId: orgId
568
637
  });
569
- const { user, token } = await this.request("/v1/signin", {
638
+ const { user, token, csrfToken } = await this.request("/v1/signin", {
570
639
  stampedRequest
571
640
  });
572
641
  const credentialId = (() => {
@@ -576,16 +645,18 @@ var SignerClient = class {
576
645
  return void 0;
577
646
  }
578
647
  })();
579
- this._user = {
648
+ this._sessionStore.user = {
580
649
  ...user,
581
650
  credentialId
582
651
  };
583
- this.sessionStore.user = this.user;
584
- this.sessionStore.token = token;
652
+ this._sessionStore.token = token;
653
+ this._sessionStore.csrfToken = csrfToken;
654
+ this._scheduleRefresh(token);
585
655
  }
586
656
  async request(route, body) {
587
657
  const url = new URL(`${route}`, this._configuration.apiUrl);
588
- const token = this.sessionStore.token;
658
+ const token = this._sessionStore.token;
659
+ const csrfToken = this._sessionStore.csrfToken;
589
660
  const headers = {
590
661
  "Content-Type": "application/json",
591
662
  "X-FOURT-KEY": this._configuration.apiKey
@@ -593,6 +664,9 @@ var SignerClient = class {
593
664
  if (token) {
594
665
  headers["Authorization"] = `Bearer ${token}`;
595
666
  }
667
+ if (csrfToken) {
668
+ headers["X-CSRF-Token"] = csrfToken;
669
+ }
596
670
  const response = await fetch(url, {
597
671
  method: ROUTE_METHOD_MAP[route],
598
672
  body: JSON.stringify(body),
@@ -603,7 +677,6 @@ var SignerClient = class {
603
677
  if (error) {
604
678
  switch (error.kind) {
605
679
  case "UnauthorizedError": {
606
- this.logout();
607
680
  throw new UnauthorizedError({ message: error.message });
608
681
  }
609
682
  case "NotFoundError": {
@@ -619,6 +692,80 @@ var SignerClient = class {
619
692
  }
620
693
  return { ...data };
621
694
  }
695
+ _scheduleRefresh(token) {
696
+ try {
697
+ const decoded = jwtDecode(token);
698
+ if (!decoded.exp) return;
699
+ const expiryDate = new Date(decoded.exp * 1e3);
700
+ const refreshDate = subMinutes(expiryDate, 2);
701
+ const delay = isBefore(refreshDate, /* @__PURE__ */ new Date()) ? 0 : differenceInMilliseconds(refreshDate, /* @__PURE__ */ new Date());
702
+ if (this._refreshTimer) clearTimeout(this._refreshTimer);
703
+ this._refreshTimer = setTimeout(() => {
704
+ this._refreshTimer = void 0;
705
+ this._refreshToken();
706
+ }, delay);
707
+ } catch {
708
+ }
709
+ }
710
+ async _refreshToken() {
711
+ if (this._refreshPromise) return this._refreshPromise;
712
+ this._refreshPromise = (async () => {
713
+ const TIMEOUT_MS = 1e4;
714
+ const RETRY_DELAY_MS = 5e3;
715
+ try {
716
+ if (!this._sessionStore.csrfToken) {
717
+ const { csrfToken } = await this.request("/v1/csrf-token");
718
+ this._sessionStore.csrfToken = csrfToken;
719
+ }
720
+ const refreshPromise = this.request("/v1/refresh");
721
+ const data = await Promise.race([
722
+ refreshPromise,
723
+ new Promise(
724
+ (_, reject) => setTimeout(() => reject(new Error("Refresh timeout")), TIMEOUT_MS)
725
+ )
726
+ ]);
727
+ if (!data || !data.token) {
728
+ throw new UnauthorizedError({
729
+ message: "Refresh did not return a token"
730
+ });
731
+ }
732
+ this._sessionStore.token = data.token;
733
+ this._scheduleRefresh(data.token);
734
+ } catch (error) {
735
+ if (error instanceof UnauthorizedError) {
736
+ try {
737
+ this._sessionStore.clearAll();
738
+ } catch {
739
+ }
740
+ throw error;
741
+ }
742
+ if (this._refreshTimer) clearTimeout(this._refreshTimer);
743
+ const MAX_RETRIES = 5;
744
+ let retryCount = 0;
745
+ this._refreshTimer = setTimeout(() => {
746
+ this._refreshTimer = void 0;
747
+ void this._refreshToken().catch(() => {
748
+ retryCount++;
749
+ if (retryCount <= MAX_RETRIES) {
750
+ const nextDelay = Math.min(
751
+ RETRY_DELAY_MS * 2 ** (retryCount - 1),
752
+ 6e4
753
+ );
754
+ this._refreshTimer = setTimeout(() => {
755
+ this._refreshTimer = void 0;
756
+ void this._refreshToken().catch(() => {
757
+ });
758
+ }, nextDelay);
759
+ }
760
+ });
761
+ }, RETRY_DELAY_MS);
762
+ throw error;
763
+ } finally {
764
+ this._refreshPromise = void 0;
765
+ }
766
+ })();
767
+ return this._refreshPromise;
768
+ }
622
769
  };
623
770
 
624
771
  // src/signer/web.ts
@@ -661,10 +808,6 @@ var WebSignerClient = class extends SignerClient {
661
808
  this.webauthnStamper = new WebauthnStamper({ rpId: webauthn.rpId });
662
809
  this.oauthConfiguration = oauth;
663
810
  }
664
- async signRawMessage(msg) {
665
- await this.updateStamper();
666
- return super.signRawMessage(msg);
667
- }
668
811
  async logout() {
669
812
  super.logout();
670
813
  this.iframeStamper.clear();
@@ -678,23 +821,20 @@ var WebSignerClient = class extends SignerClient {
678
821
  this.iframeStamper = stamper;
679
822
  await this._initIframeStamper();
680
823
  }
824
+ async signRawMessage(msg) {
825
+ await this._updateStamper();
826
+ return super.signRawMessage(msg);
827
+ }
681
828
  /**
682
- * Checks for an existing session and if exists, updates the stamper accordingly.
829
+ * Get the pre-filled URL for initiating oauth with a specific provider.
683
830
  *
831
+ * @param {string} provider provider for which we are getting the URL, currently google or apple
684
832
  */
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
- }
833
+ async getOAuthInitUrl(provider) {
834
+ const { url } = await this.request("/v1/oauth/init", {
835
+ provider
836
+ });
837
+ return url;
698
838
  }
699
839
  /**
700
840
  * Signs in a user with webauthn.
@@ -709,12 +849,12 @@ var WebSignerClient = class extends SignerClient {
709
849
  this.stamper = this.webauthnStamper;
710
850
  await this.whoAmI(existingUserSubOrgId);
711
851
  this._sessionStore.type = "passkeys" /* Passkeys */;
712
- if (!this.user || !this.user.credentialId) {
852
+ if (!this._sessionStore.user || !this._sessionStore.user.credentialId) {
713
853
  return;
714
854
  }
715
855
  this.webauthnStamper.allowCredentials = [
716
856
  {
717
- id: LibBase64.toBuffer(this.user.credentialId),
857
+ id: LibBase64.toBuffer(this._sessionStore.user.credentialId),
718
858
  type: "public-key",
719
859
  transports: ["internal", "usb"]
720
860
  }
@@ -737,23 +877,6 @@ var WebSignerClient = class extends SignerClient {
737
877
  async getIframePublicKey() {
738
878
  return await this._initIframeStamper();
739
879
  }
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
880
  /**
758
881
  * Completes the authentication process with a credential bundle.
759
882
  *
@@ -773,6 +896,40 @@ var WebSignerClient = class extends SignerClient {
773
896
  this._sessionStore.type = sessionType;
774
897
  this._sessionStore.bundle = bundle;
775
898
  }
899
+ /**
900
+ * Checks for an existing session and if exists, updates the stamper accordingly.
901
+ */
902
+ async _updateStamper() {
903
+ if (this._sessionStore.type === void 0 && (this._sessionStore.bundle === void 0 || this._sessionStore.token === void 0))
904
+ return;
905
+ if (this._sessionStore.type === "passkeys" /* Passkeys */) {
906
+ this.stamper = this.webauthnStamper;
907
+ } else {
908
+ this.stamper = this.iframeStamper;
909
+ await this.completeAuthWithBundle({
910
+ bundle: this._sessionStore.bundle,
911
+ subOrgId: this._sessionStore.user?.subOrgId,
912
+ sessionType: this._sessionStore.type
913
+ });
914
+ }
915
+ }
916
+ /**
917
+ * Signs in a user with email.
918
+ *
919
+ * @param {EmailInitializeAuthParams} params params for the sign in
920
+ */
921
+ async _signInWithEmail({
922
+ email,
923
+ expirationSeconds,
924
+ redirectUrl
925
+ }) {
926
+ return this.request("/v1/email-auth", {
927
+ email,
928
+ targetPublicKey: await this.getIframePublicKey(),
929
+ expirationSeconds,
930
+ redirectUrl: redirectUrl.toString()
931
+ });
932
+ }
776
933
  /**
777
934
  * Creates a passkey account using the webauthn stamper.
778
935
  *
@@ -782,28 +939,21 @@ var WebSignerClient = class extends SignerClient {
782
939
  const { challenge, attestation } = await this._webauthnGenerateAttestation(
783
940
  params.email
784
941
  );
785
- const {
786
- token,
787
- user: { id, email, subOrgId, walletAddress, salt, smartAccountAddress }
788
- } = await this.request("/v1/signup", {
942
+ const { user, token, csrfToken } = await this.request("/v1/signup", {
789
943
  passkey: {
790
944
  challenge: LibBase64.fromBuffer(challenge),
791
945
  attestation
792
946
  },
793
947
  email: params.email
794
948
  });
795
- this.user = {
796
- id,
797
- email,
798
- subOrgId,
799
- walletAddress,
800
- salt,
801
- smartAccountAddress,
949
+ this._sessionStore.user = {
950
+ ...user,
802
951
  credentialId: attestation.credentialId
803
952
  };
804
- this._sessionStore.user = this.user;
805
953
  this._sessionStore.type = "passkeys" /* Passkeys */;
806
954
  this._sessionStore.token = token;
955
+ this._sessionStore.csrfToken = csrfToken;
956
+ this._scheduleRefresh(token);
807
957
  }
808
958
  /**
809
959
  * Creates an email account using the iframe stamper.
@@ -880,17 +1030,6 @@ var WebSignerClient = class extends SignerClient {
880
1030
  this.stamper = this.iframeStamper;
881
1031
  return this.iframeStamper.publicKey();
882
1032
  }
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
1033
  };
895
1034
 
896
1035
  // src/third-party/viem.ts
@@ -913,13 +1052,13 @@ var ViemModule = class {
913
1052
  this._signerClient = _signerClient;
914
1053
  }
915
1054
  async toLocalAccount() {
916
- const user = this._signerClient.user;
1055
+ const user = await this._signerClient.getUser();
917
1056
  if (!user) {
918
1057
  throw new UnauthenticatedError({ message: "Signer not authenticated" });
919
1058
  }
920
1059
  return toAccount({
921
1060
  address: user.walletAddress,
922
- signMessage: (msg) => this.signMessage(msg.message),
1061
+ signMessage: ({ message }) => this.signMessage(message),
923
1062
  signTypedData: (typedDataDefinition) => this.signTypedData(typedDataDefinition),
924
1063
  signTransaction: this.signTransaction
925
1064
  });
@@ -928,7 +1067,7 @@ var ViemModule = class {
928
1067
  client,
929
1068
  owner
930
1069
  }) {
931
- const user = this._signerClient.user;
1070
+ const user = await this._signerClient.getUser();
932
1071
  if (!user) {
933
1072
  throw new UnauthenticatedError({ message: "Signer not authenticated" });
934
1073
  }
@@ -964,7 +1103,7 @@ var ViemModule = class {
964
1103
  }
965
1104
  async signTransaction(transaction, options) {
966
1105
  const serializeFn = options?.serializer ?? serializeTransaction;
967
- const serializedTx = serializeFn(transaction);
1106
+ const serializedTx = await serializeFn(transaction);
968
1107
  const signatureHex = await this._signerClient.signRawMessage(
969
1108
  keccak256(serializedTx)
970
1109
  );