@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.cjs CHANGED
@@ -172,34 +172,95 @@ var SessionStore = class {
172
172
  }
173
173
  };
174
174
 
175
- // src/modules/auth/email.ts
176
- var EmailModule = class {
175
+ // src/modules/auth/email/magicLink.ts
176
+ var MagicLinkModule = class {
177
177
  constructor(_webSignerClient) {
178
178
  this._webSignerClient = _webSignerClient;
179
179
  }
180
180
  /**
181
- * Initialize user authentication process using email.
181
+ * Completes authentication with magic link that contains a bundle.
182
182
  *
183
- * @param params {EmailInitializeAuthParams} params to initialize the user authentication process.
184
- * @returns {Promise<void>} promise that resolves to the result of the authentication process.
183
+ * @param params {CompleteAuthWithBundleParams} params received as URL params necessary to complete authentication process.
184
+ * @returns {Promise<void>} promise that completes the authentication process.
185
185
  */
186
- async initialize(params) {
187
- return this._webSignerClient.emailAuth(params);
186
+ async complete(params) {
187
+ return this._webSignerClient.completeAuthWithBundle({
188
+ ...params,
189
+ sessionType: "email" /* Email */
190
+ });
191
+ }
192
+ };
193
+
194
+ // src/errors/SDKError.ts
195
+ var SDKError = class extends Error {
196
+ _props;
197
+ constructor(message, props) {
198
+ super(message);
199
+ this._props = props;
200
+ }
201
+ get message() {
202
+ return this._props.message;
203
+ }
204
+ };
205
+
206
+ // src/errors/NotFoundError.ts
207
+ var NotFoundError = class _NotFoundError extends SDKError {
208
+ constructor(props) {
209
+ super(_NotFoundError.name, props);
210
+ }
211
+ };
212
+
213
+ // src/modules/auth/email/otp.ts
214
+ var OtpModule = class {
215
+ constructor(_webSignerClient) {
216
+ this._webSignerClient = _webSignerClient;
188
217
  }
189
218
  /**
190
- * Completes authentication with bundle after user receives the bundle and subOrgId as URL params.
219
+ * Completes authentication with OTP code after user receives the bundle and subOrgId.
191
220
  *
192
- * @param params {CompleteAuthWithBundleParams} params received as URL params necessary to complete authentication process.
221
+ * @param params {OtpAuthParams} params to complete the authentication process.
193
222
  * @returns {Promise<void>} promise that completes the authentication process.
194
223
  */
195
224
  async complete(params) {
225
+ const { auth } = await this._webSignerClient.otpAuth(params);
226
+ if (!auth)
227
+ throw new NotFoundError({
228
+ message: "No OTP authentication response returned."
229
+ });
196
230
  return this._webSignerClient.completeAuthWithBundle({
197
- ...params,
231
+ bundle: auth.credentialBundle,
232
+ subOrgId: auth.subOrgId,
198
233
  sessionType: "email" /* Email */
199
234
  });
200
235
  }
201
236
  };
202
237
 
238
+ // src/modules/auth/email.ts
239
+ var EmailModule = class {
240
+ constructor(_webSignerClient) {
241
+ this._webSignerClient = _webSignerClient;
242
+ this._magicLinkModule = new MagicLinkModule(this._webSignerClient);
243
+ this._otpModule = new OtpModule(this._webSignerClient);
244
+ }
245
+ _magicLinkModule;
246
+ _otpModule;
247
+ /**
248
+ * Initialize user authentication process using email.
249
+ *
250
+ * @param params {EmailInitializeAuthParams} params to initialize the user authentication process.
251
+ * @returns {Promise<void>} promise that resolves to the result of the authentication process.
252
+ */
253
+ async initialize(params) {
254
+ return this._webSignerClient.emailAuth(params);
255
+ }
256
+ get magicLink() {
257
+ return this._magicLinkModule;
258
+ }
259
+ get otp() {
260
+ return this._otpModule;
261
+ }
262
+ };
263
+
203
264
  // src/modules/auth/passkeys.ts
204
265
  var PasskeysModule = class {
205
266
  constructor(_webSignerClient) {
@@ -209,7 +270,6 @@ var PasskeysModule = class {
209
270
  * Signs in a user using Passkeys.
210
271
  *
211
272
  * @param params {WebauthnSignInParams} params for the sign-in process.
212
- * @returns {Promise<void>} promise that resolves to the result of the sign-in process.
213
273
  */
214
274
  async signIn(params) {
215
275
  return this._webSignerClient.webauthnSignIn(params);
@@ -218,7 +278,40 @@ var PasskeysModule = class {
218
278
 
219
279
  // src/modules/auth/oauth/google.ts
220
280
  var jose = __toESM(require("jose"), 1);
221
- var import_sha = require("sha.js");
281
+
282
+ // src/lib/sha256.ts
283
+ var LibSha256 = class {
284
+ /**
285
+ * Compute SHA-256 and return hex string using Web Crypto when available.
286
+ * Falls back to Node's crypto.createHash when running in Node.
287
+ */
288
+ static async sha256Hex(input) {
289
+ let data;
290
+ if (typeof input === "string") {
291
+ data = new TextEncoder().encode(input);
292
+ } else if (input instanceof Uint8Array) {
293
+ data = input;
294
+ } else {
295
+ data = new Uint8Array(input);
296
+ }
297
+ const subtle = typeof globalThis !== "undefined" && globalThis.crypto?.subtle ? globalThis.crypto.subtle : void 0;
298
+ if (subtle) {
299
+ const hash = await subtle.digest("SHA-256", data);
300
+ return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
301
+ }
302
+ try {
303
+ const nodeCrypto = require("crypto");
304
+ const hash = nodeCrypto.createHash("sha256").update(data).digest("hex");
305
+ return hash;
306
+ } catch {
307
+ throw new Error(
308
+ "No crypto provider available. Provide a global crypto.subtle or run in Node 18+."
309
+ );
310
+ }
311
+ }
312
+ };
313
+
314
+ // src/modules/auth/oauth/google.ts
222
315
  var GoogleModule = class {
223
316
  constructor(_webSignerClient) {
224
317
  this._webSignerClient = _webSignerClient;
@@ -236,7 +329,7 @@ var GoogleModule = class {
236
329
  ).href;
237
330
  url.searchParams.set("redirect_uri", internalUrl);
238
331
  const publicKey = await this._webSignerClient.getIframePublicKey();
239
- const nonce = new import_sha.sha256().update(publicKey).digest("hex");
332
+ const nonce = await LibSha256.sha256Hex(publicKey);
240
333
  url.searchParams.set("nonce", nonce);
241
334
  const state = new jose.UnsecuredJWT({
242
335
  apiKey: this._webSignerClient.configuration.apiKey,
@@ -250,18 +343,6 @@ var GoogleModule = class {
250
343
  }
251
344
  };
252
345
 
253
- // src/errors/SDKError.ts
254
- var SDKError = class extends Error {
255
- _props;
256
- constructor(message, props) {
257
- super(message);
258
- this._props = props;
259
- }
260
- get message() {
261
- return this._props.message;
262
- }
263
- };
264
-
265
346
  // src/errors/BadRequestError.ts
266
347
  var BadRequestError = class _BadRequestError extends SDKError {
267
348
  constructor(props) {
@@ -312,7 +393,6 @@ var FacebookModule = class {
312
393
 
313
394
  // src/modules/auth/oauth/apple.ts
314
395
  var jose3 = __toESM(require("jose"), 1);
315
- var import_sha2 = require("sha.js");
316
396
  var AppleModule = class {
317
397
  constructor(_webSignerClient) {
318
398
  this._webSignerClient = _webSignerClient;
@@ -330,7 +410,7 @@ var AppleModule = class {
330
410
  ).href;
331
411
  url.searchParams.set("redirect_uri", internalUrl);
332
412
  const publicKey = await this._webSignerClient.getIframePublicKey();
333
- const nonce = new import_sha2.sha256().update(publicKey).digest("hex");
413
+ const nonce = await LibSha256.sha256Hex(publicKey);
334
414
  url.searchParams.set("nonce", nonce);
335
415
  const state = new jose3.UnsecuredJWT({
336
416
  apiKey: this._webSignerClient.configuration.apiKey,
@@ -450,13 +530,29 @@ var import_iframe_stamper = require("@turnkey/iframe-stamper");
450
530
  var import_webauthn_stamper = require("@turnkey/webauthn-stamper");
451
531
 
452
532
  // src/lib/base64.ts
453
- var import_buffer = require("buffer");
454
533
  var LibBase64 = class {
534
+ // Convert an ArrayBuffer to base64url (no padding)
455
535
  static fromBuffer(buffer) {
456
- return import_buffer.Buffer.from(buffer).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
457
- }
458
- static toBuffer(base64) {
459
- return import_buffer.Buffer.from(base64, "base64").buffer;
536
+ const bytes = new Uint8Array(buffer);
537
+ const CHUNK_SIZE = 32768;
538
+ let binary = "";
539
+ for (let i = 0; i < bytes.length; i += CHUNK_SIZE) {
540
+ const chunk = bytes.subarray(i, i + CHUNK_SIZE);
541
+ binary += String.fromCharCode(...chunk);
542
+ }
543
+ const base64 = typeof btoa === "function" ? btoa(binary) : Buffer.from(bytes).toString("base64");
544
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
545
+ }
546
+ // Convert base64url (no padding) to ArrayBuffer
547
+ static toBuffer(base64url) {
548
+ let base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
549
+ const pad = base64.length % 4;
550
+ if (pad) base64 += "=".repeat(4 - pad);
551
+ const binary = typeof atob === "function" ? atob(base64) : Buffer.from(base64, "base64").toString("binary");
552
+ const len = binary.length;
553
+ const bytes = new Uint8Array(len);
554
+ for (let i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i);
555
+ return bytes.buffer;
460
556
  }
461
557
  };
462
558
 
@@ -485,13 +581,6 @@ var UnauthorizedError = class _UnauthorizedError extends SDKError {
485
581
  }
486
582
  };
487
583
 
488
- // src/errors/NotFoundError.ts
489
- var NotFoundError = class _NotFoundError extends SDKError {
490
- constructor(props) {
491
- super(_NotFoundError.name, props);
492
- }
493
- };
494
-
495
584
  // src/errors/GenericError.ts
496
585
  var GenericError = class _GenericError extends SDKError {
497
586
  constructor(props) {
@@ -506,23 +595,8 @@ var UnauthenticatedError = class _UnauthenticatedError extends SDKError {
506
595
  }
507
596
  };
508
597
 
509
- // src/types/routes.ts
510
- var ROUTE_METHOD_MAP = {
511
- "/v1/signup": "POST",
512
- "/v1/email-auth": "POST",
513
- "/v1/lookup": "POST",
514
- "/v1/signin": "POST",
515
- "/v1/sign": "POST",
516
- "/v1/oauth/init": "POST",
517
- "/v1/refresh": "POST",
518
- "/v1/csrf-token": "GET",
519
- "/v1/logout": "POST",
520
- "/v1/me": "GET"
521
- };
522
-
523
598
  // src/signer/index.ts
524
- var import_jwt_decode = require("jwt-decode");
525
- var import_date_fns = require("date-fns");
599
+ var import_jose = require("jose");
526
600
  var SignerClient = class {
527
601
  _turnkeyClient;
528
602
  _configuration;
@@ -550,14 +624,14 @@ var SignerClient = class {
550
624
  async getUser() {
551
625
  if (this._sessionStore.user) return this._sessionStore.user;
552
626
  try {
553
- const user = await this.request("/v1/me");
627
+ const user = await this.request("/v1/me", "GET");
554
628
  this._sessionStore.user = user;
555
629
  return user;
556
630
  } catch (error) {
557
631
  if (error instanceof UnauthorizedError) {
558
632
  try {
559
633
  await this._refreshToken();
560
- const user = await this.request("/v1/me");
634
+ const user = await this.request("/v1/me", "GET");
561
635
  this._sessionStore.user = user;
562
636
  return user;
563
637
  } catch (error2) {
@@ -605,8 +679,8 @@ var SignerClient = class {
605
679
  }
606
680
  _isTokenExpired(token) {
607
681
  try {
608
- const decoded = (0, import_jwt_decode.jwtDecode)(token);
609
- if (decoded.exp) {
682
+ const decoded = (0, import_jose.decodeJwt)(token);
683
+ if (typeof decoded.exp === "number") {
610
684
  return decoded.exp * 1e3 <= Date.now();
611
685
  }
612
686
  return true;
@@ -617,7 +691,7 @@ var SignerClient = class {
617
691
  async logout() {
618
692
  if (this._refreshTimer) clearTimeout(this._refreshTimer);
619
693
  this._refreshTimer = void 0;
620
- await this.request("/v1/logout");
694
+ await this.request("/v1/logout", "POST");
621
695
  this._sessionStore.clearAll();
622
696
  }
623
697
  async signRawMessage(msg) {
@@ -637,7 +711,7 @@ var SignerClient = class {
637
711
  signWith: this._sessionStore.user.walletAddress
638
712
  }
639
713
  });
640
- const { signature } = await this.request("/v1/sign", {
714
+ const { signature } = await this.request("/v1/sign", "POST", {
641
715
  stampedRequest
642
716
  });
643
717
  return signature;
@@ -650,7 +724,7 @@ var SignerClient = class {
650
724
  }
651
725
  async lookUpUser(email) {
652
726
  try {
653
- const { subOrgId } = await this.request("/v1/lookup", { email });
727
+ const { subOrgId } = await this.request("/v1/lookup", "POST", { email });
654
728
  return subOrgId;
655
729
  } catch (error) {
656
730
  if (!(error instanceof SDKError)) throw error;
@@ -671,9 +745,13 @@ var SignerClient = class {
671
745
  const stampedRequest = await this._turnkeyClient.stampGetWhoami({
672
746
  organizationId: orgId
673
747
  });
674
- const { user, token, csrfToken } = await this.request("/v1/signin", {
675
- stampedRequest
676
- });
748
+ const { user, token, csrfToken } = await this.request(
749
+ "/v1/signin",
750
+ "POST",
751
+ {
752
+ stampedRequest
753
+ }
754
+ );
677
755
  const credentialId = (() => {
678
756
  try {
679
757
  return JSON.parse(stampedRequest?.stamp.stampHeaderValue).credentialId;
@@ -689,7 +767,7 @@ var SignerClient = class {
689
767
  this._sessionStore.csrfToken = csrfToken;
690
768
  this._scheduleRefresh(token);
691
769
  }
692
- async request(route, body) {
770
+ async request(route, method, body) {
693
771
  const url = new URL(`${route}`, this._configuration.apiUrl);
694
772
  const token = this._sessionStore.token;
695
773
  const csrfToken = this._sessionStore.csrfToken;
@@ -704,8 +782,8 @@ var SignerClient = class {
704
782
  headers["X-CSRF-Token"] = csrfToken;
705
783
  }
706
784
  const response = await fetch(url, {
707
- method: ROUTE_METHOD_MAP[route],
708
- body: JSON.stringify(body),
785
+ method,
786
+ body: method === "POST" ? JSON.stringify(body) : void 0,
709
787
  headers,
710
788
  credentials: "include"
711
789
  });
@@ -728,13 +806,24 @@ var SignerClient = class {
728
806
  }
729
807
  return { ...data };
730
808
  }
809
+ /**
810
+ * Compute milliseconds until refresh time.
811
+ * - expSeconds is the JWT exp claim (seconds).
812
+ * - earlyMinutes is how many minutes before exp to refresh (default 2).
813
+ * Returns 0 if refresh time is already past.
814
+ */
815
+ _computeRefreshDelayMs(expSeconds, earlyMinutes = 2) {
816
+ const expiryMs = expSeconds * 1e3;
817
+ const refreshMs = expiryMs - earlyMinutes * 60 * 1e3;
818
+ const now = Date.now();
819
+ const delay = refreshMs - now;
820
+ return delay <= 0 ? 0 : delay;
821
+ }
731
822
  _scheduleRefresh(token) {
732
823
  try {
733
- const decoded = (0, import_jwt_decode.jwtDecode)(token);
824
+ const decoded = (0, import_jose.decodeJwt)(token);
734
825
  if (!decoded.exp) return;
735
- const expiryDate = new Date(decoded.exp * 1e3);
736
- const refreshDate = (0, import_date_fns.subMinutes)(expiryDate, 2);
737
- const delay = (0, import_date_fns.isBefore)(refreshDate, /* @__PURE__ */ new Date()) ? 0 : (0, import_date_fns.differenceInMilliseconds)(refreshDate, /* @__PURE__ */ new Date());
826
+ const delay = this._computeRefreshDelayMs(decoded.exp, 2);
738
827
  if (this._refreshTimer) clearTimeout(this._refreshTimer);
739
828
  this._refreshTimer = setTimeout(() => {
740
829
  this._refreshTimer = void 0;
@@ -750,10 +839,10 @@ var SignerClient = class {
750
839
  const RETRY_DELAY_MS = 5e3;
751
840
  try {
752
841
  if (!this._sessionStore.csrfToken) {
753
- const { csrfToken } = await this.request("/v1/csrf-token");
842
+ const { csrfToken } = await this.request("/v1/csrf-token", "GET");
754
843
  this._sessionStore.csrfToken = csrfToken;
755
844
  }
756
- const refreshPromise = this.request("/v1/refresh");
845
+ const refreshPromise = this.request("/v1/refresh", "POST");
757
846
  const data = await Promise.race([
758
847
  refreshPromise,
759
848
  new Promise(
@@ -868,7 +957,7 @@ var WebSignerClient = class extends SignerClient {
868
957
  * @param {string} provider provider for which we are getting the URL, currently google or apple
869
958
  */
870
959
  async getOAuthInitUrl(provider) {
871
- const { url } = await this.request("/v1/oauth/init", {
960
+ const { url } = await this.request("/v1/oauth/init", "POST", {
872
961
  provider
873
962
  });
874
963
  return url;
@@ -899,21 +988,65 @@ var WebSignerClient = class extends SignerClient {
899
988
  }
900
989
  }
901
990
  /**
902
- * Handle auth user process with email.
991
+ * Handles auth user process with email according to the method of the used app.
903
992
  *
904
- * @param {EmailInitializeAuthParams} params Params needed for the initialization of the auth process
993
+ * @param {EmailInitializeAuthParams} params params needed for the initialization of the auth process
905
994
  */
906
995
  async emailAuth(params) {
907
- const existingUserSubOrgId = await this.lookUpUser(params.email);
908
- if (!existingUserSubOrgId) {
909
- await this._createAccount({ method: "email", ...params });
996
+ const method = await this._getEmailAuthMethod();
997
+ if (method === "magiclink") {
998
+ const existingUserSubOrgId = await this.lookUpUser(params.email);
999
+ if (!existingUserSubOrgId) {
1000
+ await this._createAccount({ method: "email", ...params });
1001
+ } else {
1002
+ await this._signInWithEmail(params);
1003
+ }
1004
+ return { method };
1005
+ } else if (method === "otp") {
1006
+ const { init } = await this._initOtpAuth({ email: params.email });
1007
+ if (!init?.otpId)
1008
+ throw new NotFoundError({ message: "No OTP init response returned." });
1009
+ return {
1010
+ method,
1011
+ otpId: init.otpId
1012
+ };
910
1013
  } else {
911
- await this._signInWithEmail(params);
1014
+ throw new Error("Invalid email authentication method.");
912
1015
  }
913
1016
  }
914
1017
  async getIframePublicKey() {
915
1018
  return await this._initIframeStamper();
916
1019
  }
1020
+ /**
1021
+ * Verifies the provided otp code.
1022
+ *
1023
+ * @param {OtpAuthParams} params params needed for otp code verification
1024
+ */
1025
+ async otpAuth(params) {
1026
+ return this.request("/v1/otp-auth", "POST", {
1027
+ auth: {
1028
+ email: params.email,
1029
+ otpCode: params.otpCode,
1030
+ otpId: params.otpId,
1031
+ targetPublicKey: await this.getIframePublicKey()
1032
+ }
1033
+ });
1034
+ }
1035
+ /**
1036
+ * Gets the email authentication method of the app.
1037
+ */
1038
+ async _getEmailAuthMethod() {
1039
+ const { method } = await this.request("/v1/email-auth", "GET");
1040
+ return method;
1041
+ }
1042
+ /**
1043
+ * Starts email authentication process via otp.
1044
+ */
1045
+ async _initOtpAuth(params) {
1046
+ return this.request("/v1/otp-auth", "POST", {
1047
+ init: { email: params.email }
1048
+ });
1049
+ }
917
1050
  /**
918
1051
  * Completes the authentication process with a credential bundle.
919
1052
  *
@@ -960,7 +1093,7 @@ var WebSignerClient = class extends SignerClient {
960
1093
  expirationSeconds,
961
1094
  redirectUrl
962
1095
  }) {
963
- return this.request("/v1/email-auth", {
1096
+ return this.request("/v1/email-auth", "POST", {
964
1097
  email,
965
1098
  targetPublicKey: await this.getIframePublicKey(),
966
1099
  expirationSeconds,
@@ -976,13 +1109,17 @@ var WebSignerClient = class extends SignerClient {
976
1109
  const { challenge, attestation } = await this._webauthnGenerateAttestation(
977
1110
  params.email
978
1111
  );
979
- const { user, token, csrfToken } = await this.request("/v1/signup", {
980
- passkey: {
981
- challenge: LibBase64.fromBuffer(challenge),
982
- attestation
983
- },
984
- email: params.email
985
- });
1112
+ const { user, token, csrfToken } = await this.request(
1113
+ "/v1/signup",
1114
+ "POST",
1115
+ {
1116
+ passkey: {
1117
+ challenge: LibBase64.fromBuffer(challenge),
1118
+ attestation
1119
+ },
1120
+ email: params.email
1121
+ }
1122
+ );
986
1123
  this._sessionStore.user = {
987
1124
  ...user,
988
1125
  credentialId: attestation.credentialId
@@ -999,7 +1136,7 @@ var WebSignerClient = class extends SignerClient {
999
1136
  */
1000
1137
  async _createEmailAccount(params) {
1001
1138
  const { email, expirationSeconds, redirectUrl } = params;
1002
- const response = await this.request("/v1/signup", {
1139
+ const response = await this.request("/v1/signup", "POST", {
1003
1140
  email,
1004
1141
  iframe: {
1005
1142
  targetPublicKey: await this.getIframePublicKey(),