@fourt/sdk 1.2.2 → 1.3.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,3 +1,10 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
1
8
  // src/session/index.ts
2
9
  import { createStore } from "zustand";
3
10
  import { persist, createJSONStorage } from "zustand/middleware";
@@ -136,34 +143,95 @@ var SessionStore = class {
136
143
  }
137
144
  };
138
145
 
139
- // src/modules/auth/email.ts
140
- var EmailModule = class {
146
+ // src/modules/auth/email/magicLink.ts
147
+ var MagicLinkModule = class {
141
148
  constructor(_webSignerClient) {
142
149
  this._webSignerClient = _webSignerClient;
143
150
  }
144
151
  /**
145
- * Initialize user authentication process using email.
152
+ * Completes authentication with magic link that contains a bundle.
146
153
  *
147
- * @param params {EmailInitializeAuthParams} params to initialize the user authentication process.
148
- * @returns {Promise<void>} promise that resolves to the result of the authentication process.
154
+ * @param params {CompleteAuthWithBundleParams} params received as URL params necessary to complete authentication process.
155
+ * @returns {Promise<void>} promise that completes the authentication process.
149
156
  */
150
- async initialize(params) {
151
- return this._webSignerClient.emailAuth(params);
157
+ async complete(params) {
158
+ return this._webSignerClient.completeAuthWithBundle({
159
+ ...params,
160
+ sessionType: "email" /* Email */
161
+ });
162
+ }
163
+ };
164
+
165
+ // src/errors/SDKError.ts
166
+ var SDKError = class extends Error {
167
+ _props;
168
+ constructor(message, props) {
169
+ super(message);
170
+ this._props = props;
171
+ }
172
+ get message() {
173
+ return this._props.message;
174
+ }
175
+ };
176
+
177
+ // src/errors/NotFoundError.ts
178
+ var NotFoundError = class _NotFoundError extends SDKError {
179
+ constructor(props) {
180
+ super(_NotFoundError.name, props);
181
+ }
182
+ };
183
+
184
+ // src/modules/auth/email/otp.ts
185
+ var OtpModule = class {
186
+ constructor(_webSignerClient) {
187
+ this._webSignerClient = _webSignerClient;
152
188
  }
153
189
  /**
154
- * Completes authentication with bundle after user receives the bundle and subOrgId as URL params.
190
+ * Completes authentication with OTP code after user receives the bundle and subOrgId.
155
191
  *
156
- * @param params {CompleteAuthWithBundleParams} params received as URL params necessary to complete authentication process.
192
+ * @param params {OtpAuthParams} params to complete the authentication process.
157
193
  * @returns {Promise<void>} promise that completes the authentication process.
158
194
  */
159
195
  async complete(params) {
196
+ const { auth } = await this._webSignerClient.otpAuth(params);
197
+ if (!auth)
198
+ throw new NotFoundError({
199
+ message: "No OTP authentication response returned."
200
+ });
160
201
  return this._webSignerClient.completeAuthWithBundle({
161
- ...params,
202
+ bundle: auth.credentialBundle,
203
+ subOrgId: auth.subOrgId,
162
204
  sessionType: "email" /* Email */
163
205
  });
164
206
  }
165
207
  };
166
208
 
209
+ // src/modules/auth/email.ts
210
+ var EmailModule = class {
211
+ constructor(_webSignerClient) {
212
+ this._webSignerClient = _webSignerClient;
213
+ this._magicLinkModule = new MagicLinkModule(this._webSignerClient);
214
+ this._otpModule = new OtpModule(this._webSignerClient);
215
+ }
216
+ _magicLinkModule;
217
+ _otpModule;
218
+ /**
219
+ * Initialize user authentication process using email.
220
+ *
221
+ * @param params {EmailInitializeAuthParams} params to initialize the user authentication process.
222
+ * @returns {Promise<void>} promise that resolves to the result of the authentication process.
223
+ */
224
+ async initialize(params) {
225
+ return this._webSignerClient.emailAuth(params);
226
+ }
227
+ get magicLink() {
228
+ return this._magicLinkModule;
229
+ }
230
+ get otp() {
231
+ return this._otpModule;
232
+ }
233
+ };
234
+
167
235
  // src/modules/auth/passkeys.ts
168
236
  var PasskeysModule = class {
169
237
  constructor(_webSignerClient) {
@@ -173,7 +241,6 @@ var PasskeysModule = class {
173
241
  * Signs in a user using Passkeys.
174
242
  *
175
243
  * @param params {WebauthnSignInParams} params for the sign-in process.
176
- * @returns {Promise<void>} promise that resolves to the result of the sign-in process.
177
244
  */
178
245
  async signIn(params) {
179
246
  return this._webSignerClient.webauthnSignIn(params);
@@ -182,7 +249,40 @@ var PasskeysModule = class {
182
249
 
183
250
  // src/modules/auth/oauth/google.ts
184
251
  import * as jose from "jose";
185
- import { sha256 } from "sha.js";
252
+
253
+ // src/lib/sha256.ts
254
+ var LibSha256 = class {
255
+ /**
256
+ * Compute SHA-256 and return hex string using Web Crypto when available.
257
+ * Falls back to Node's crypto.createHash when running in Node.
258
+ */
259
+ static async sha256Hex(input) {
260
+ let data;
261
+ if (typeof input === "string") {
262
+ data = new TextEncoder().encode(input);
263
+ } else if (input instanceof Uint8Array) {
264
+ data = input;
265
+ } else {
266
+ data = new Uint8Array(input);
267
+ }
268
+ const subtle = typeof globalThis !== "undefined" && globalThis.crypto?.subtle ? globalThis.crypto.subtle : void 0;
269
+ if (subtle) {
270
+ const hash = await subtle.digest("SHA-256", data);
271
+ return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
272
+ }
273
+ try {
274
+ const nodeCrypto = __require("crypto");
275
+ const hash = nodeCrypto.createHash("sha256").update(data).digest("hex");
276
+ return hash;
277
+ } catch {
278
+ throw new Error(
279
+ "No crypto provider available. Provide a global crypto.subtle or run in Node 18+."
280
+ );
281
+ }
282
+ }
283
+ };
284
+
285
+ // src/modules/auth/oauth/google.ts
186
286
  var GoogleModule = class {
187
287
  constructor(_webSignerClient) {
188
288
  this._webSignerClient = _webSignerClient;
@@ -200,7 +300,7 @@ var GoogleModule = class {
200
300
  ).href;
201
301
  url.searchParams.set("redirect_uri", internalUrl);
202
302
  const publicKey = await this._webSignerClient.getIframePublicKey();
203
- const nonce = new sha256().update(publicKey).digest("hex");
303
+ const nonce = await LibSha256.sha256Hex(publicKey);
204
304
  url.searchParams.set("nonce", nonce);
205
305
  const state = new jose.UnsecuredJWT({
206
306
  apiKey: this._webSignerClient.configuration.apiKey,
@@ -214,18 +314,6 @@ var GoogleModule = class {
214
314
  }
215
315
  };
216
316
 
217
- // src/errors/SDKError.ts
218
- var SDKError = class extends Error {
219
- _props;
220
- constructor(message, props) {
221
- super(message);
222
- this._props = props;
223
- }
224
- get message() {
225
- return this._props.message;
226
- }
227
- };
228
-
229
317
  // src/errors/BadRequestError.ts
230
318
  var BadRequestError = class _BadRequestError extends SDKError {
231
319
  constructor(props) {
@@ -276,7 +364,6 @@ var FacebookModule = class {
276
364
 
277
365
  // src/modules/auth/oauth/apple.ts
278
366
  import * as jose3 from "jose";
279
- import { sha256 as sha2562 } from "sha.js";
280
367
  var AppleModule = class {
281
368
  constructor(_webSignerClient) {
282
369
  this._webSignerClient = _webSignerClient;
@@ -294,7 +381,7 @@ var AppleModule = class {
294
381
  ).href;
295
382
  url.searchParams.set("redirect_uri", internalUrl);
296
383
  const publicKey = await this._webSignerClient.getIframePublicKey();
297
- const nonce = new sha2562().update(publicKey).digest("hex");
384
+ const nonce = await LibSha256.sha256Hex(publicKey);
298
385
  url.searchParams.set("nonce", nonce);
299
386
  const state = new jose3.UnsecuredJWT({
300
387
  apiKey: this._webSignerClient.configuration.apiKey,
@@ -414,13 +501,29 @@ import { IframeStamper } from "@turnkey/iframe-stamper";
414
501
  import { WebauthnStamper } from "@turnkey/webauthn-stamper";
415
502
 
416
503
  // src/lib/base64.ts
417
- import { Buffer } from "buffer";
418
504
  var LibBase64 = class {
505
+ // Convert an ArrayBuffer to base64url (no padding)
419
506
  static fromBuffer(buffer) {
420
- return Buffer.from(buffer).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
421
- }
422
- static toBuffer(base64) {
423
- return Buffer.from(base64, "base64").buffer;
507
+ const bytes = new Uint8Array(buffer);
508
+ const CHUNK_SIZE = 32768;
509
+ let binary = "";
510
+ for (let i = 0; i < bytes.length; i += CHUNK_SIZE) {
511
+ const chunk = bytes.subarray(i, i + CHUNK_SIZE);
512
+ binary += String.fromCharCode(...chunk);
513
+ }
514
+ const base64 = typeof btoa === "function" ? btoa(binary) : Buffer.from(bytes).toString("base64");
515
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
516
+ }
517
+ // Convert base64url (no padding) to ArrayBuffer
518
+ static toBuffer(base64url) {
519
+ let base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
520
+ const pad = base64.length % 4;
521
+ if (pad) base64 += "=".repeat(4 - pad);
522
+ const binary = typeof atob === "function" ? atob(base64) : Buffer.from(base64, "base64").toString("binary");
523
+ const len = binary.length;
524
+ const bytes = new Uint8Array(len);
525
+ for (let i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i);
526
+ return bytes.buffer;
424
527
  }
425
528
  };
426
529
 
@@ -449,13 +552,6 @@ var UnauthorizedError = class _UnauthorizedError extends SDKError {
449
552
  }
450
553
  };
451
554
 
452
- // src/errors/NotFoundError.ts
453
- var NotFoundError = class _NotFoundError extends SDKError {
454
- constructor(props) {
455
- super(_NotFoundError.name, props);
456
- }
457
- };
458
-
459
555
  // src/errors/GenericError.ts
460
556
  var GenericError = class _GenericError extends SDKError {
461
557
  constructor(props) {
@@ -470,23 +566,8 @@ var UnauthenticatedError = class _UnauthenticatedError extends SDKError {
470
566
  }
471
567
  };
472
568
 
473
- // src/types/routes.ts
474
- var ROUTE_METHOD_MAP = {
475
- "/v1/signup": "POST",
476
- "/v1/email-auth": "POST",
477
- "/v1/lookup": "POST",
478
- "/v1/signin": "POST",
479
- "/v1/sign": "POST",
480
- "/v1/oauth/init": "POST",
481
- "/v1/refresh": "POST",
482
- "/v1/csrf-token": "GET",
483
- "/v1/logout": "POST",
484
- "/v1/me": "GET"
485
- };
486
-
487
569
  // src/signer/index.ts
488
- import { jwtDecode } from "jwt-decode";
489
- import { differenceInMilliseconds, isBefore, subMinutes } from "date-fns";
570
+ import { decodeJwt } from "jose";
490
571
  var SignerClient = class {
491
572
  _turnkeyClient;
492
573
  _configuration;
@@ -514,14 +595,14 @@ var SignerClient = class {
514
595
  async getUser() {
515
596
  if (this._sessionStore.user) return this._sessionStore.user;
516
597
  try {
517
- const user = await this.request("/v1/me");
598
+ const user = await this.request("/v1/me", "GET");
518
599
  this._sessionStore.user = user;
519
600
  return user;
520
601
  } catch (error) {
521
602
  if (error instanceof UnauthorizedError) {
522
603
  try {
523
604
  await this._refreshToken();
524
- const user = await this.request("/v1/me");
605
+ const user = await this.request("/v1/me", "GET");
525
606
  this._sessionStore.user = user;
526
607
  return user;
527
608
  } catch (error2) {
@@ -569,8 +650,8 @@ var SignerClient = class {
569
650
  }
570
651
  _isTokenExpired(token) {
571
652
  try {
572
- const decoded = jwtDecode(token);
573
- if (decoded.exp) {
653
+ const decoded = decodeJwt(token);
654
+ if (typeof decoded.exp === "number") {
574
655
  return decoded.exp * 1e3 <= Date.now();
575
656
  }
576
657
  return true;
@@ -581,7 +662,7 @@ var SignerClient = class {
581
662
  async logout() {
582
663
  if (this._refreshTimer) clearTimeout(this._refreshTimer);
583
664
  this._refreshTimer = void 0;
584
- await this.request("/v1/logout");
665
+ await this.request("/v1/logout", "POST");
585
666
  this._sessionStore.clearAll();
586
667
  }
587
668
  async signRawMessage(msg) {
@@ -601,7 +682,7 @@ var SignerClient = class {
601
682
  signWith: this._sessionStore.user.walletAddress
602
683
  }
603
684
  });
604
- const { signature } = await this.request("/v1/sign", {
685
+ const { signature } = await this.request("/v1/sign", "POST", {
605
686
  stampedRequest
606
687
  });
607
688
  return signature;
@@ -614,7 +695,7 @@ var SignerClient = class {
614
695
  }
615
696
  async lookUpUser(email) {
616
697
  try {
617
- const { subOrgId } = await this.request("/v1/lookup", { email });
698
+ const { subOrgId } = await this.request("/v1/lookup", "POST", { email });
618
699
  return subOrgId;
619
700
  } catch (error) {
620
701
  if (!(error instanceof SDKError)) throw error;
@@ -635,9 +716,13 @@ var SignerClient = class {
635
716
  const stampedRequest = await this._turnkeyClient.stampGetWhoami({
636
717
  organizationId: orgId
637
718
  });
638
- const { user, token, csrfToken } = await this.request("/v1/signin", {
639
- stampedRequest
640
- });
719
+ const { user, token, csrfToken } = await this.request(
720
+ "/v1/signin",
721
+ "POST",
722
+ {
723
+ stampedRequest
724
+ }
725
+ );
641
726
  const credentialId = (() => {
642
727
  try {
643
728
  return JSON.parse(stampedRequest?.stamp.stampHeaderValue).credentialId;
@@ -653,7 +738,7 @@ var SignerClient = class {
653
738
  this._sessionStore.csrfToken = csrfToken;
654
739
  this._scheduleRefresh(token);
655
740
  }
656
- async request(route, body) {
741
+ async request(route, method, body) {
657
742
  const url = new URL(`${route}`, this._configuration.apiUrl);
658
743
  const token = this._sessionStore.token;
659
744
  const csrfToken = this._sessionStore.csrfToken;
@@ -668,8 +753,8 @@ var SignerClient = class {
668
753
  headers["X-CSRF-Token"] = csrfToken;
669
754
  }
670
755
  const response = await fetch(url, {
671
- method: ROUTE_METHOD_MAP[route],
672
- body: JSON.stringify(body),
756
+ method,
757
+ body: method === "POST" ? JSON.stringify(body) : void 0,
673
758
  headers,
674
759
  credentials: "include"
675
760
  });
@@ -692,13 +777,24 @@ var SignerClient = class {
692
777
  }
693
778
  return { ...data };
694
779
  }
780
+ /**
781
+ * Compute milliseconds until refresh time.
782
+ * - expSeconds is the JWT exp claim (seconds).
783
+ * - earlyMinutes is how many minutes before exp to refresh (default 2).
784
+ * Returns 0 if refresh time is already past.
785
+ */
786
+ _computeRefreshDelayMs(expSeconds, earlyMinutes = 2) {
787
+ const expiryMs = expSeconds * 1e3;
788
+ const refreshMs = expiryMs - earlyMinutes * 60 * 1e3;
789
+ const now = Date.now();
790
+ const delay = refreshMs - now;
791
+ return delay <= 0 ? 0 : delay;
792
+ }
695
793
  _scheduleRefresh(token) {
696
794
  try {
697
- const decoded = jwtDecode(token);
795
+ const decoded = decodeJwt(token);
698
796
  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());
797
+ const delay = this._computeRefreshDelayMs(decoded.exp, 2);
702
798
  if (this._refreshTimer) clearTimeout(this._refreshTimer);
703
799
  this._refreshTimer = setTimeout(() => {
704
800
  this._refreshTimer = void 0;
@@ -714,10 +810,10 @@ var SignerClient = class {
714
810
  const RETRY_DELAY_MS = 5e3;
715
811
  try {
716
812
  if (!this._sessionStore.csrfToken) {
717
- const { csrfToken } = await this.request("/v1/csrf-token");
813
+ const { csrfToken } = await this.request("/v1/csrf-token", "GET");
718
814
  this._sessionStore.csrfToken = csrfToken;
719
815
  }
720
- const refreshPromise = this.request("/v1/refresh");
816
+ const refreshPromise = this.request("/v1/refresh", "POST");
721
817
  const data = await Promise.race([
722
818
  refreshPromise,
723
819
  new Promise(
@@ -832,7 +928,7 @@ var WebSignerClient = class extends SignerClient {
832
928
  * @param {string} provider provider for which we are getting the URL, currently google or apple
833
929
  */
834
930
  async getOAuthInitUrl(provider) {
835
- const { url } = await this.request("/v1/oauth/init", {
931
+ const { url } = await this.request("/v1/oauth/init", "POST", {
836
932
  provider
837
933
  });
838
934
  return url;
@@ -863,21 +959,65 @@ var WebSignerClient = class extends SignerClient {
863
959
  }
864
960
  }
865
961
  /**
866
- * Handle auth user process with email.
962
+ * Handles auth user process with email according to the method of the used app.
867
963
  *
868
- * @param {EmailInitializeAuthParams} params Params needed for the initialization of the auth process
964
+ * @param {EmailInitializeAuthParams} params params needed for the initialization of the auth process
869
965
  */
870
966
  async emailAuth(params) {
871
- const existingUserSubOrgId = await this.lookUpUser(params.email);
872
- if (!existingUserSubOrgId) {
873
- await this._createAccount({ method: "email", ...params });
967
+ const method = await this._getEmailAuthMethod();
968
+ if (method === "magiclink") {
969
+ const existingUserSubOrgId = await this.lookUpUser(params.email);
970
+ if (!existingUserSubOrgId) {
971
+ await this._createAccount({ method: "email", ...params });
972
+ } else {
973
+ await this._signInWithEmail(params);
974
+ }
975
+ return { method };
976
+ } else if (method === "otp") {
977
+ const { init } = await this._initOtpAuth({ email: params.email });
978
+ if (!init?.otpId)
979
+ throw new NotFoundError({ message: "No OTP init response returned." });
980
+ return {
981
+ method,
982
+ otpId: init.otpId
983
+ };
874
984
  } else {
875
- await this._signInWithEmail(params);
985
+ throw new Error("Invalid email authentication method.");
876
986
  }
877
987
  }
878
988
  async getIframePublicKey() {
879
989
  return await this._initIframeStamper();
880
990
  }
991
+ /**
992
+ * Verifies the provided otp code.
993
+ *
994
+ * @param {OtpAuthParams} params params needed for otp code verification
995
+ */
996
+ async otpAuth(params) {
997
+ return this.request("/v1/otp-auth", "POST", {
998
+ auth: {
999
+ email: params.email,
1000
+ otpCode: params.otpCode,
1001
+ otpId: params.otpId,
1002
+ targetPublicKey: await this.getIframePublicKey()
1003
+ }
1004
+ });
1005
+ }
1006
+ /**
1007
+ * Gets the email authentication method of the app.
1008
+ */
1009
+ async _getEmailAuthMethod() {
1010
+ const { method } = await this.request("/v1/email-auth", "GET");
1011
+ return method;
1012
+ }
1013
+ /**
1014
+ * Starts email authentication process via otp.
1015
+ */
1016
+ async _initOtpAuth(params) {
1017
+ return this.request("/v1/otp-auth", "POST", {
1018
+ init: { email: params.email }
1019
+ });
1020
+ }
881
1021
  /**
882
1022
  * Completes the authentication process with a credential bundle.
883
1023
  *
@@ -924,7 +1064,7 @@ var WebSignerClient = class extends SignerClient {
924
1064
  expirationSeconds,
925
1065
  redirectUrl
926
1066
  }) {
927
- return this.request("/v1/email-auth", {
1067
+ return this.request("/v1/email-auth", "POST", {
928
1068
  email,
929
1069
  targetPublicKey: await this.getIframePublicKey(),
930
1070
  expirationSeconds,
@@ -940,13 +1080,17 @@ var WebSignerClient = class extends SignerClient {
940
1080
  const { challenge, attestation } = await this._webauthnGenerateAttestation(
941
1081
  params.email
942
1082
  );
943
- const { user, token, csrfToken } = await this.request("/v1/signup", {
944
- passkey: {
945
- challenge: LibBase64.fromBuffer(challenge),
946
- attestation
947
- },
948
- email: params.email
949
- });
1083
+ const { user, token, csrfToken } = await this.request(
1084
+ "/v1/signup",
1085
+ "POST",
1086
+ {
1087
+ passkey: {
1088
+ challenge: LibBase64.fromBuffer(challenge),
1089
+ attestation
1090
+ },
1091
+ email: params.email
1092
+ }
1093
+ );
950
1094
  this._sessionStore.user = {
951
1095
  ...user,
952
1096
  credentialId: attestation.credentialId
@@ -963,7 +1107,7 @@ var WebSignerClient = class extends SignerClient {
963
1107
  */
964
1108
  async _createEmailAccount(params) {
965
1109
  const { email, expirationSeconds, redirectUrl } = params;
966
- const response = await this.request("/v1/signup", {
1110
+ const response = await this.request("/v1/signup", "POST", {
967
1111
  email,
968
1112
  iframe: {
969
1113
  targetPublicKey: await this.getIframePublicKey(),