@finueva/auth 0.6.0 → 0.7.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/device.js CHANGED
@@ -439,6 +439,9 @@ function createFetch(globalOptions = {}) {
439
439
  //#region src/errors.ts
440
440
  const messages = {
441
441
  invalid_credentials: "The credentials are invalid.",
442
+ invalid_auth_input: "The authentication input is invalid.",
443
+ account_conflict: "The account is not available.",
444
+ password_reset_invalid: "The password reset request is invalid.",
442
445
  authentication_required: "Authentication is required.",
443
446
  request_origin_denied: "The request origin is not allowed.",
444
447
  invalid_invitation: "The invitation is invalid.",
@@ -448,6 +451,7 @@ const messages = {
448
451
  invalid_workspace_selection: "The workspace selection is invalid.",
449
452
  organization_context_too_large: "The organization context is too large.",
450
453
  credential_rate_limited: "Credential requests are temporarily limited.",
454
+ auth_request_rate_limited: "Authentication requests are temporarily limited.",
451
455
  state_conflict: "The authentication state conflicts with the request.",
452
456
  access_denied: "Access is denied.",
453
457
  identity_authority_unavailable: "Authentication is temporarily unavailable."
@@ -466,8 +470,56 @@ var AuthError = class extends Error {
466
470
  };
467
471
  //#endregion
468
472
  //#region src/shared.ts
473
+ const DRIVE_CREDENTIAL_LIFETIME_SECONDS = 840;
469
474
  const id = /^[A-Za-z0-9_-]{1,128}$/;
470
475
  const email = /^[^\s@]+@[^\s@]+$/;
476
+ const MAX_SOCIAL_ID_TOKEN_BYTES = 16 * 1024;
477
+ function normalizeEmail(value) {
478
+ if (typeof value !== "string") throw new TypeError("email is invalid");
479
+ const normalized = value.trim().toLowerCase();
480
+ if (normalized.length === 0 || normalized.length > 320 || !email.test(normalized)) throw new TypeError("email is invalid");
481
+ return normalized;
482
+ }
483
+ function normalizePassword(value, name = "password", minLength = 8) {
484
+ if (typeof value !== "string" || value.length < minLength || value.length > 128) throw new TypeError(`${name} is invalid`);
485
+ return value;
486
+ }
487
+ function normalizeSocialIdTokenInput(value) {
488
+ if (!isObject(value)) throw new TypeError("social ID token input is invalid");
489
+ const keys = Object.keys(value);
490
+ if (keys.length !== 3 && keys.length !== 4 || keys.some((key) => key !== "provider" && key !== "idToken" && key !== "nonce" && key !== "signUp") || value.provider !== "google" && value.provider !== "apple" || typeof value.signUp !== "boolean" || typeof value.idToken !== "string" || value.idToken.length < 1 || new TextEncoder().encode(value.idToken).byteLength > MAX_SOCIAL_ID_TOKEN_BYTES || !/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(value.idToken) || value.nonce !== void 0 && (typeof value.nonce !== "string" || value.nonce.length < 1 || value.nonce.length > 256 || !/^[\x20-\x7e]+$/.test(value.nonce))) throw new TypeError("social ID token input is invalid");
491
+ return {
492
+ provider: value.provider,
493
+ idToken: value.idToken,
494
+ ...value.nonce === void 0 ? {} : { nonce: value.nonce },
495
+ signUp: value.signUp
496
+ };
497
+ }
498
+ function socialIdTokenRequest(value) {
499
+ return {
500
+ provider: value.provider,
501
+ idToken: {
502
+ token: value.idToken,
503
+ ...value.nonce === void 0 ? {} : { nonce: value.nonce }
504
+ },
505
+ requestSignUp: value.signUp
506
+ };
507
+ }
508
+ function normalizeName(value) {
509
+ if (typeof value !== "string") throw new TypeError("name is invalid");
510
+ const normalized = value.trim();
511
+ if (normalized.length === 0 || normalized.length > 256) throw new TypeError("name is invalid");
512
+ return normalized;
513
+ }
514
+ function normalizeProfileInput(value) {
515
+ if (!isObject(value)) throw new TypeError("profile input is invalid");
516
+ const keys = Object.keys(value);
517
+ if (keys.length === 0 || keys.length > 2 || keys.some((key) => key !== "name" && key !== "image") || value.name === void 0 && value.image === void 0) throw new TypeError("profile input is invalid");
518
+ const result = {};
519
+ if (value.name !== void 0) result.name = normalizeName(value.name);
520
+ if (value.image !== void 0) result.image = value.image === null ? null : normalizeProfileImage(value.image);
521
+ return result;
522
+ }
471
523
  function normalizeOrigin(value, name) {
472
524
  if (value.length === 0 || value.length > 2048 || /[^\x20-\x7e]/.test(value)) throw new TypeError(`${name} must be an ASCII HTTP origin`);
473
525
  let url;
@@ -503,10 +555,48 @@ function normalizeSession(value) {
503
555
  }
504
556
  };
505
557
  }
558
+ function normalizeDriveCredential(value, now) {
559
+ if (!isObject(value) || value.tokenType !== "Bearer" || value.expiresIn !== DRIVE_CREDENTIAL_LIFETIME_SECONDS || typeof value.token !== "string" || value.token.length === 0 || new TextEncoder().encode(`Bearer ${value.token}`).byteLength > 16 * 1024) throw authorityUnavailable();
560
+ return {
561
+ tokenType: "Bearer",
562
+ token: value.token,
563
+ expiresAt: new Date(now + value.expiresIn * 1e3)
564
+ };
565
+ }
566
+ function normalizeWorkspaceSelection(value) {
567
+ if (!isObject(value)) throw new AuthError("invalid_workspace_selection", 422);
568
+ const keys = Object.keys(value);
569
+ if (value.type === "personal" && keys.length === 1 && keys[0] === "type") return { type: "personal" };
570
+ if (value.type === "organization" && keys.length === 2 && keys.includes("type") && keys.includes("organizationId") && typeof value.organizationId === "string" && id.test(value.organizationId)) return {
571
+ type: "organization",
572
+ organizationId: value.organizationId
573
+ };
574
+ throw new AuthError("invalid_workspace_selection", 422);
575
+ }
506
576
  function fromAuthorityError(value, fallbackStatus, retryAfter) {
507
577
  const mapped = mapError(isObject(value) && typeof value.code === "string" ? value.code : void 0, fallbackStatus);
508
578
  return new AuthError(mapped.code, mapped.status, parseRetryAfter(retryAfter));
509
579
  }
580
+ function fromAccountAuthorityError(value, status, retryAfter) {
581
+ const code = isObject(value) && typeof value.code === "string" ? value.code : void 0;
582
+ if (status === 403 && (code === "INVALID_ORIGIN" || code === "MISSING_OR_NULL_ORIGIN")) return new AuthError("request_origin_denied", 403);
583
+ if (code === "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL") return new AuthError("account_conflict", 409);
584
+ if (code === "INVALID_TOKEN") return new AuthError("password_reset_invalid", 422);
585
+ if (code === "INVALID_PASSWORD" || code === "INVALID_EMAIL_OR_PASSWORD") return new AuthError("invalid_credentials", 401);
586
+ if (code === "INVALID_EMAIL" || code === "PASSWORD_TOO_SHORT" || code === "PASSWORD_TOO_LONG" || code === "BODY_MUST_BE_AN_OBJECT" || code === "EMAIL_CAN_NOT_BE_UPDATED") return new AuthError("invalid_auth_input", 422);
587
+ if (status === 401) return new AuthError("authentication_required", 401);
588
+ if (status === 403) return new AuthError("access_denied", 403);
589
+ if (status === 409) return new AuthError("state_conflict", 409);
590
+ if (status === 429) return new AuthError("auth_request_rate_limited", 429, parseRetryAfter(retryAfter));
591
+ if (status === 400 || status === 422) return new AuthError("invalid_auth_input", 422);
592
+ return authorityUnavailable();
593
+ }
594
+ function fromSocialIdTokenAuthorityError(value, status, retryAfter) {
595
+ const code = isObject(value) && typeof value.code === "string" ? value.code : void 0;
596
+ if (code === "INVALID_TOKEN" || code === "FAILED_TO_GET_USER_INFO" || code === "USER_EMAIL_NOT_FOUND") return new AuthError("invalid_credentials", 401);
597
+ if (code === "OAUTH_LINK_ERROR") return new AuthError("account_conflict", 409);
598
+ return fromAccountAuthorityError(value, status, retryAfter);
599
+ }
510
600
  function authorityUnavailable() {
511
601
  return new AuthError("identity_authority_unavailable", 503);
512
602
  }
@@ -569,6 +659,18 @@ function toDate(value) {
569
659
  const result = value instanceof Date ? value : typeof value === "string" ? new Date(value) : void 0;
570
660
  return result && Number.isFinite(result.getTime()) ? result : void 0;
571
661
  }
662
+ function normalizeProfileImage(value) {
663
+ if (typeof value !== "string" || value.length === 0 || value.length > 2048 || /[^\x20-\x7e]/.test(value)) throw new TypeError("profile image is invalid");
664
+ let url;
665
+ try {
666
+ url = new URL(value);
667
+ } catch {
668
+ throw new TypeError("profile image is invalid");
669
+ }
670
+ const local = url.hostname === "localhost" || url.hostname.endsWith(".localhost") || url.hostname === "127.0.0.1";
671
+ if (url.protocol !== "https:" && !(local && url.protocol === "http:") || url.username || url.password || url.toString() !== value) throw new TypeError("profile image is invalid");
672
+ return value;
673
+ }
572
674
  function isId(value) {
573
675
  return typeof value === "string" && id.test(value);
574
676
  }
@@ -585,7 +687,7 @@ async function requestJsonResponse(url, options) {
585
687
  method: options.method,
586
688
  headers: options.headers,
587
689
  body: options.body,
588
- credentials: "include",
690
+ credentials: options.credentials ?? "include",
589
691
  retry: 0,
590
692
  timeout: options.timeoutMs,
591
693
  redirect: "manual",
@@ -647,8 +749,9 @@ async function readJson(response) {
647
749
  //#region src/device.ts
648
750
  const DEVICE_SCOPE = "openid";
649
751
  const GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
650
- const MAX_TOKEN_BYTES = 16 * 1024;
752
+ const MAX_TOKEN_BYTES = 16 * 1024 - 7;
651
753
  const MAX_POLLING_MS = 900 * 1e3;
754
+ const REFRESH_WINDOW_MS = 120 * 1e3;
652
755
  const errorMessages = {
653
756
  device_access_denied: "Device authorization was denied.",
654
757
  device_authorization_cancelled: "Device authorization was cancelled.",
@@ -678,7 +781,30 @@ function createDeviceAuth(options) {
678
781
  let token = null;
679
782
  let hydrated = false;
680
783
  let hydration = null;
681
- async function getToken() {
784
+ let credential;
785
+ let credentialRequest;
786
+ let selectedWorkspaceKey;
787
+ let credentialGeneration = 0;
788
+ let authorityGeneration = 0;
789
+ let authorityTail = Promise.resolve();
790
+ let persistenceTail = Promise.resolve();
791
+ let persistencePending = 0;
792
+ function clearCredential() {
793
+ credential = void 0;
794
+ selectedWorkspaceKey = void 0;
795
+ credentialGeneration += 1;
796
+ }
797
+ function serializeAuthority(operation, invalidateCredential = false) {
798
+ if (invalidateCredential) clearCredential();
799
+ const guardedOperation = () => {
800
+ if (persistencePending > 0) throw new AuthError("state_conflict", 409);
801
+ return operation();
802
+ };
803
+ const request = authorityTail.then(guardedOperation, guardedOperation);
804
+ authorityTail = request.then(() => void 0, () => void 0);
805
+ return request;
806
+ }
807
+ async function readToken() {
682
808
  if (!hydrated) {
683
809
  hydration ??= hydrate().finally(() => {
684
810
  hydration = null;
@@ -687,17 +813,28 @@ function createDeviceAuth(options) {
687
813
  }
688
814
  if (token && token.expiresAt.getTime() <= now()) {
689
815
  token = null;
816
+ authorityGeneration += 1;
817
+ clearCredential();
690
818
  await removePersisted();
691
819
  }
692
820
  return token ? copyToken(token) : null;
693
821
  }
694
822
  async function hydrate() {
695
823
  try {
696
- const loaded = options.persistence ? normalizeToken(await persistenceCall((signal) => options.persistence.read(signal)), now()) : null;
697
- if (options.persistence && !loaded) await persistenceCall((signal) => options.persistence.delete(signal));
824
+ const persisted = options.persistence ? await persistenceCall((signal) => options.persistence.read(signal)) : null;
825
+ let loaded;
826
+ try {
827
+ loaded = normalizeToken(persisted, now());
828
+ } catch {
829
+ token = null;
830
+ if (options.persistence && persisted !== null) await persistenceCall((signal) => options.persistence.delete(signal)).catch(() => void 0);
831
+ throw unavailable();
832
+ }
833
+ if (options.persistence && persisted !== null && !loaded) await persistenceCall((signal) => options.persistence.delete(signal));
698
834
  token = loaded;
699
- } catch {
835
+ } catch (error) {
700
836
  token = null;
837
+ if (error instanceof DeviceAuthorizationError) throw error;
701
838
  throw unavailable();
702
839
  } finally {
703
840
  hydrated = true;
@@ -710,6 +847,7 @@ function createDeviceAuth(options) {
710
847
  fetch: requestFetch,
711
848
  timeoutMs,
712
849
  method: "POST",
850
+ credentials: "omit",
713
851
  body: {
714
852
  client_id: options.clientId,
715
853
  scope: DEVICE_SCOPE
@@ -723,14 +861,14 @@ function createDeviceAuth(options) {
723
861
  throw unavailable();
724
862
  }
725
863
  }
726
- async function pollForToken(authorization, pollOptions = {}) {
727
- if (pollOptions.signal?.aborted) throw cancelled();
728
- const normalized = normalizeAuthorizationInput(authorization, now());
864
+ async function pollForToken(authorization, signal) {
865
+ if (signal?.aborted) throw cancelled();
866
+ const previous = await readToken();
729
867
  const startedAt = now();
730
- const deadline = Math.min(normalized.expiresAt.getTime(), startedAt + maxPollingMs);
731
- let intervalMs = normalized.intervalSeconds * 1e3;
868
+ const deadline = Math.min(authorization.expiresAt.getTime(), startedAt + maxPollingMs);
869
+ let intervalMs = authorization.intervalSeconds * 1e3;
732
870
  while (true) {
733
- await wait(intervalMs, pollOptions.signal, deadline, now);
871
+ await wait(intervalMs, signal, deadline, now);
734
872
  const requestBudget = deadline - now();
735
873
  if (requestBudget <= 0) throw expired();
736
874
  let response;
@@ -739,18 +877,19 @@ function createDeviceAuth(options) {
739
877
  fetch: requestFetch,
740
878
  timeoutMs: Math.min(timeoutMs, requestBudget),
741
879
  method: "POST",
880
+ credentials: "omit",
742
881
  body: {
743
882
  grant_type: GRANT_TYPE,
744
- device_code: normalized.deviceCode,
883
+ device_code: authorization.deviceCode,
745
884
  client_id: options.clientId
746
885
  },
747
- signal: pollOptions.signal,
886
+ signal,
748
887
  mapError(value) {
749
888
  return new DeviceProtocolError(isObject(value) && typeof value.error === "string" ? value.error : void 0);
750
889
  }
751
890
  });
752
891
  } catch (error) {
753
- if (pollOptions.signal?.aborted) throw cancelled();
892
+ if (signal?.aborted) throw cancelled();
754
893
  if (now() >= deadline) throw expired();
755
894
  if (error instanceof DeviceProtocolError) {
756
895
  if (error.protocolCode === "authorization_pending") continue;
@@ -765,43 +904,54 @@ function createDeviceAuth(options) {
765
904
  throw unavailable();
766
905
  }
767
906
  if (now() >= deadline) throw expired();
768
- const next = normalizeTokenResponse(response.value, now());
769
- if (options.persistence) try {
770
- await persistenceCall((signal) => options.persistence.replace(copyToken(next), signal));
907
+ const recoverableAccessToken = extractRedeemedToken(response.value);
908
+ let next;
909
+ try {
910
+ next = normalizeTokenResponse(response.value, now());
771
911
  } catch {
772
- token = null;
773
- hydrated = true;
774
- await persistenceCall((signal) => options.persistence.delete(signal)).catch(() => void 0);
912
+ if (recoverableAccessToken) await revokeToken(recoverableAccessToken);
913
+ await clearToken().catch(() => void 0);
914
+ throw unavailable();
915
+ }
916
+ try {
917
+ await publishReplacement(next, previous);
918
+ } catch {
919
+ await revokeToken(next.accessToken);
920
+ await retainPrevious(previous);
775
921
  throw unavailable();
776
922
  }
777
- token = next;
778
- hydrated = true;
779
923
  return copyToken(next);
780
924
  }
781
925
  }
782
- async function authenticated(path, body) {
783
- const current = await getToken();
784
- if (!current) return null;
785
- try {
786
- return await requestJsonResponse(joinURL(authOrigin, "api/v1/auth", path), {
787
- fetch: requestFetch,
788
- timeoutMs,
789
- method: body ? "POST" : "GET",
790
- headers: { authorization: `Bearer ${current.accessToken}` },
791
- body
792
- });
793
- } catch {
794
- throw unavailable();
795
- }
926
+ async function fetchBearerSession(accessToken) {
927
+ return normalizeSession((await requestJsonResponse(joinURL(authOrigin, "api/v1/auth/get-session"), {
928
+ fetch: requestFetch,
929
+ timeoutMs,
930
+ method: "GET",
931
+ credentials: "omit",
932
+ headers: { authorization: `Bearer ${accessToken}` }
933
+ })).value);
934
+ }
935
+ async function authenticated(path, current, body) {
936
+ return requestJsonResponse(joinURL(authOrigin, "api/v1/auth", path), {
937
+ fetch: requestFetch,
938
+ timeoutMs,
939
+ method: body ? "POST" : "GET",
940
+ credentials: "omit",
941
+ headers: { authorization: `Bearer ${current.accessToken}` },
942
+ body,
943
+ mapError: fromAccountAuthorityError
944
+ });
796
945
  }
797
946
  async function revoke(path) {
798
- const current = await getToken();
947
+ const current = await readToken();
799
948
  if (!current) return;
800
949
  try {
801
950
  await requestJsonResponse(joinURL(authOrigin, "api/v1/auth", path), {
802
951
  fetch: requestFetch,
803
952
  timeoutMs,
804
953
  method: "POST",
954
+ credentials: "omit",
805
955
  headers: { authorization: `Bearer ${current.accessToken}` },
806
956
  body: path === "revoke-session" ? { token: current.accessToken } : void 0
807
957
  });
@@ -809,8 +959,139 @@ function createDeviceAuth(options) {
809
959
  throw unavailable();
810
960
  }
811
961
  token = null;
962
+ hydrated = true;
963
+ authorityGeneration += 1;
964
+ clearCredential();
965
+ await removePersisted();
966
+ }
967
+ async function replaceToken(next) {
968
+ try {
969
+ if (options.persistence) await persistenceCall((signal) => options.persistence.replace(copyToken(next), signal));
970
+ } catch {
971
+ throw unavailable();
972
+ }
973
+ token = copyToken(next);
974
+ hydrated = true;
975
+ authorityGeneration += 1;
976
+ clearCredential();
977
+ }
978
+ async function publishReplacement(next, previous) {
979
+ await replaceToken(next);
980
+ if (previous && previous.accessToken !== next.accessToken) await revokeToken(previous.accessToken);
981
+ }
982
+ async function replaceIssuedToken(issued, previous) {
983
+ let session;
984
+ try {
985
+ const authoritative = await fetchBearerSession(issued.accessToken);
986
+ if (!authoritative || authoritative.user.id !== issued.userId || authoritative.expiresAt.getTime() <= now()) throw authorityUnavailable();
987
+ session = authoritative;
988
+ await publishReplacement({
989
+ accessToken: issued.accessToken,
990
+ tokenType: "Bearer",
991
+ expiresAt: authoritative.expiresAt,
992
+ scope: DEVICE_SCOPE
993
+ }, previous);
994
+ } catch (error) {
995
+ await revokeToken(issued.accessToken);
996
+ await retainPrevious(previous);
997
+ throw error instanceof AuthError ? error : authorityUnavailable();
998
+ }
999
+ return session;
1000
+ }
1001
+ async function replaceIssuedResponse(value, previous) {
1002
+ const accessToken = extractIssuedToken(value);
1003
+ let issued;
1004
+ try {
1005
+ issued = normalizeIssuedSession(value);
1006
+ } catch (error) {
1007
+ if (accessToken) await revokeToken(accessToken);
1008
+ await retainPrevious(previous);
1009
+ throw error instanceof AuthError ? error : authorityUnavailable();
1010
+ }
1011
+ return replaceIssuedToken(issued, previous);
1012
+ }
1013
+ async function revokeToken(accessToken) {
1014
+ await requestJsonResponse(joinURL(authOrigin, "api/v1/auth/revoke-session"), {
1015
+ fetch: requestFetch,
1016
+ timeoutMs,
1017
+ method: "POST",
1018
+ credentials: "omit",
1019
+ headers: { authorization: `Bearer ${accessToken}` },
1020
+ body: { token: accessToken }
1021
+ }).catch(() => void 0);
1022
+ }
1023
+ async function clearToken() {
1024
+ token = null;
1025
+ hydrated = true;
1026
+ authorityGeneration += 1;
1027
+ clearCredential();
812
1028
  await removePersisted();
813
1029
  }
1030
+ async function retainPrevious(previous) {
1031
+ if (previous) {
1032
+ try {
1033
+ const session = await fetchBearerSession(previous.accessToken);
1034
+ if (session && session.expiresAt.getTime() > now()) {
1035
+ const restored = {
1036
+ ...previous,
1037
+ expiresAt: session.expiresAt
1038
+ };
1039
+ if (options.persistence) await replaceToken(restored);
1040
+ else {
1041
+ token = copyToken(previous);
1042
+ hydrated = true;
1043
+ }
1044
+ return;
1045
+ }
1046
+ } catch {}
1047
+ await revokeToken(previous.accessToken);
1048
+ }
1049
+ token = null;
1050
+ hydrated = true;
1051
+ authorityGeneration += 1;
1052
+ clearCredential();
1053
+ await removePersisted().catch(() => void 0);
1054
+ }
1055
+ async function accountRequest(path, body, mapError = fromAccountAuthorityError) {
1056
+ clearCredential();
1057
+ return requestJsonResponse(joinURL(authOrigin, "api/v1/auth", path), {
1058
+ fetch: requestFetch,
1059
+ timeoutMs,
1060
+ method: "POST",
1061
+ credentials: "omit",
1062
+ headers: { "content-type": "application/json; charset=utf-8" },
1063
+ body,
1064
+ mapError
1065
+ });
1066
+ }
1067
+ async function getSession() {
1068
+ const current = await readToken();
1069
+ if (!current) return null;
1070
+ let session;
1071
+ try {
1072
+ session = await fetchBearerSession(current.accessToken);
1073
+ } catch (error) {
1074
+ throw error instanceof AuthError ? error : unavailable();
1075
+ }
1076
+ if (!session || session.expiresAt.getTime() <= now()) {
1077
+ await clearToken();
1078
+ return null;
1079
+ }
1080
+ if (session.expiresAt.getTime() !== current.expiresAt.getTime()) try {
1081
+ await replaceToken({
1082
+ ...current,
1083
+ expiresAt: session.expiresAt
1084
+ });
1085
+ } catch {
1086
+ token = null;
1087
+ hydrated = true;
1088
+ authorityGeneration += 1;
1089
+ clearCredential();
1090
+ await removePersisted().catch(() => void 0);
1091
+ throw unavailable();
1092
+ }
1093
+ return session;
1094
+ }
814
1095
  async function removePersisted() {
815
1096
  if (!options.persistence) return;
816
1097
  try {
@@ -821,30 +1102,236 @@ function createDeviceAuth(options) {
821
1102
  }
822
1103
  return {
823
1104
  requestAuthorization,
824
- pollForToken,
825
- getToken,
826
- async getSession() {
827
- const response = await authenticated("get-session");
828
- return response ? normalizeSession(response.value) : null;
1105
+ pollForToken(authorization, pollOptions) {
1106
+ let signal;
1107
+ let snapshot;
1108
+ try {
1109
+ signal = normalizePollSignal(pollOptions);
1110
+ if (signal?.aborted) throw cancelled();
1111
+ snapshot = normalizeAuthorizationInput(authorization, now());
1112
+ } catch (error) {
1113
+ return Promise.reject(error);
1114
+ }
1115
+ return serializeAuthority(() => pollForToken(snapshot, signal), true);
1116
+ },
1117
+ getToken: () => serializeAuthority(async () => copyTokenOrNull(await readToken())),
1118
+ getSession: () => serializeAuthority(getSession, true),
1119
+ signUpWithEmail(email, password, name) {
1120
+ let body;
1121
+ try {
1122
+ body = {
1123
+ email: normalizeEmail(email),
1124
+ password: normalizePassword(password, "password", 10),
1125
+ name: normalizeName(name)
1126
+ };
1127
+ } catch (error) {
1128
+ return Promise.reject(error);
1129
+ }
1130
+ return serializeAuthority(async () => {
1131
+ const previous = await readToken();
1132
+ return replaceIssuedResponse((await accountRequest("sign-up/email", body)).value, previous);
1133
+ }, true);
1134
+ },
1135
+ signInWithEmail(email, password) {
1136
+ let body;
1137
+ try {
1138
+ body = {
1139
+ email: normalizeEmail(email),
1140
+ password: normalizePassword(password)
1141
+ };
1142
+ } catch (error) {
1143
+ return Promise.reject(error);
1144
+ }
1145
+ return serializeAuthority(async () => {
1146
+ const previous = await readToken();
1147
+ return replaceIssuedResponse((await accountRequest("sign-in/email", body)).value, previous);
1148
+ }, true);
1149
+ },
1150
+ signInWithSocialIdToken(input) {
1151
+ let body;
1152
+ try {
1153
+ body = socialIdTokenRequest(normalizeSocialIdTokenInput(input));
1154
+ } catch (error) {
1155
+ return Promise.reject(error);
1156
+ }
1157
+ return serializeAuthority(async () => {
1158
+ const previous = await readToken();
1159
+ return replaceIssuedResponse((await accountRequest("sign-in/social", body, fromSocialIdTokenAuthorityError)).value, previous);
1160
+ }, true);
1161
+ },
1162
+ updateProfile(input) {
1163
+ let body;
1164
+ try {
1165
+ body = normalizeProfileInput(input);
1166
+ } catch (error) {
1167
+ return Promise.reject(error);
1168
+ }
1169
+ return serializeAuthority(async () => {
1170
+ const current = await readToken();
1171
+ if (!current) throw new AuthError("authentication_required", 401);
1172
+ const response = await authenticated("update-user", current, body);
1173
+ if (!isObject(response.value) || response.value.status !== true) throw authorityUnavailable();
1174
+ const session = await getSession();
1175
+ if (!session) throw new AuthError("authentication_required", 401);
1176
+ return session;
1177
+ }, true);
1178
+ },
1179
+ changePassword(currentPassword, newPassword, revokeOtherSessions = true) {
1180
+ let body;
1181
+ try {
1182
+ if (typeof revokeOtherSessions !== "boolean") throw new TypeError("revokeOtherSessions is invalid");
1183
+ body = {
1184
+ currentPassword: normalizePassword(currentPassword, "currentPassword"),
1185
+ newPassword: normalizePassword(newPassword, "newPassword", 10),
1186
+ revokeOtherSessions
1187
+ };
1188
+ } catch (error) {
1189
+ return Promise.reject(error);
1190
+ }
1191
+ return serializeAuthority(async () => {
1192
+ const current = await readToken();
1193
+ if (!current) throw new AuthError("authentication_required", 401);
1194
+ let response;
1195
+ try {
1196
+ response = await authenticated("change-password", current, body);
1197
+ } catch (error) {
1198
+ await retainPrevious(current);
1199
+ throw error instanceof AuthError ? error : authorityUnavailable();
1200
+ }
1201
+ let replacement;
1202
+ try {
1203
+ replacement = normalizePasswordChange(response.value);
1204
+ } catch (error) {
1205
+ const accessToken = extractIssuedToken(response.value);
1206
+ if (accessToken) await revokeToken(accessToken);
1207
+ await retainPrevious(current);
1208
+ throw error;
1209
+ }
1210
+ if (replacement.accessToken !== null) return replaceIssuedToken({
1211
+ accessToken: replacement.accessToken,
1212
+ userId: replacement.userId
1213
+ }, current);
1214
+ let session;
1215
+ try {
1216
+ session = await fetchBearerSession(current.accessToken);
1217
+ } catch (error) {
1218
+ await retainPrevious(current);
1219
+ throw error instanceof AuthError ? error : authorityUnavailable();
1220
+ }
1221
+ if (!session || session.user.id !== replacement.userId || session.expiresAt.getTime() <= now()) {
1222
+ await clearToken();
1223
+ throw new AuthError("authentication_required", 401);
1224
+ }
1225
+ await replaceToken({
1226
+ ...current,
1227
+ expiresAt: session.expiresAt
1228
+ });
1229
+ return session;
1230
+ }, true);
1231
+ },
1232
+ getDriveCredential(input) {
1233
+ let workspace;
1234
+ let forceRefresh;
1235
+ try {
1236
+ workspace = normalizeWorkspaceSelection(input?.workspace);
1237
+ const submittedForceRefresh = input?.forceRefresh;
1238
+ if (submittedForceRefresh !== void 0 && typeof submittedForceRefresh !== "boolean") throw new TypeError("forceRefresh is invalid");
1239
+ forceRefresh = submittedForceRefresh ?? false;
1240
+ } catch (error) {
1241
+ return Promise.reject(error);
1242
+ }
1243
+ const workspaceKey = workspace.type === "personal" ? "personal" : `organization:${workspace.organizationId}`;
1244
+ if (credentialRequest) {
1245
+ if (!forceRefresh && credentialRequest.key === workspaceKey && credentialRequest.generation === credentialGeneration && selectedWorkspaceKey === workspaceKey) return credentialRequest.value;
1246
+ return Promise.reject(new AuthError("state_conflict", 409));
1247
+ }
1248
+ if (selectedWorkspaceKey !== workspaceKey) {
1249
+ credential = void 0;
1250
+ selectedWorkspaceKey = workspaceKey;
1251
+ credentialGeneration += 1;
1252
+ }
1253
+ if (forceRefresh) {
1254
+ credential = void 0;
1255
+ credentialGeneration += 1;
1256
+ }
1257
+ const requestGeneration = credentialGeneration;
1258
+ const request = (async () => {
1259
+ const authority = await serializeAuthority(async () => {
1260
+ const current = await readToken();
1261
+ return {
1262
+ authorityGeneration,
1263
+ token: current
1264
+ };
1265
+ });
1266
+ const current = authority.token;
1267
+ if (!current) throw new AuthError("authentication_required", 401);
1268
+ if (authority.authorityGeneration !== authorityGeneration || requestGeneration !== credentialGeneration || selectedWorkspaceKey !== workspaceKey) throw new AuthError("state_conflict", 409);
1269
+ if (!forceRefresh && credential?.key === workspaceKey && credential.value.expiresAt.getTime() - now() > REFRESH_WINDOW_MS) return credential.value;
1270
+ const next = normalizeDriveCredential((await requestJsonResponse(joinURL(authOrigin, "api/v1/auth/drive-credential"), {
1271
+ fetch: requestFetch,
1272
+ timeoutMs,
1273
+ method: "POST",
1274
+ credentials: "omit",
1275
+ headers: {
1276
+ authorization: `Bearer ${current.accessToken}`,
1277
+ "content-type": "application/json; charset=utf-8"
1278
+ },
1279
+ body: { workspace }
1280
+ })).value, now());
1281
+ if (authority.authorityGeneration !== authorityGeneration || requestGeneration !== credentialGeneration || selectedWorkspaceKey !== workspaceKey) throw new AuthError("state_conflict", 409);
1282
+ credential = {
1283
+ key: workspaceKey,
1284
+ value: next
1285
+ };
1286
+ return next;
1287
+ })().finally(() => {
1288
+ if (credentialRequest?.value === request) credentialRequest = void 0;
1289
+ });
1290
+ credentialRequest = {
1291
+ generation: requestGeneration,
1292
+ key: workspaceKey,
1293
+ value: request
1294
+ };
1295
+ return request;
829
1296
  },
830
- revokeCurrentDevice: () => revoke("revoke-session"),
831
- revokeAllSessions: () => revoke("revoke-sessions")
1297
+ revokeCurrentDevice: () => serializeAuthority(() => revoke("revoke-session"), true),
1298
+ revokeAllSessions: () => serializeAuthority(() => revoke("revoke-sessions"), true)
832
1299
  };
833
1300
  async function persistenceCall(operation) {
834
- const controller = new AbortController();
835
- let timer;
1301
+ const operationTimeoutMs = Math.min(timeoutMs, 1e3);
1302
+ persistencePending += 1;
1303
+ const settledRequest = persistenceTail.then(async () => {
1304
+ const controller = new AbortController();
1305
+ const timer = setTimeout(() => controller.abort(), operationTimeoutMs);
1306
+ try {
1307
+ return await operation(controller.signal);
1308
+ } finally {
1309
+ clearTimeout(timer);
1310
+ }
1311
+ }).finally(() => {
1312
+ persistencePending -= 1;
1313
+ });
1314
+ persistenceTail = settledRequest.then(() => void 0, () => void 0);
1315
+ let publicTimer;
836
1316
  try {
837
- return await Promise.race([Promise.resolve().then(() => operation(controller.signal)), new Promise((_, reject) => {
838
- timer = setTimeout(() => {
839
- controller.abort();
840
- reject(unavailable());
841
- }, Math.min(timeoutMs, 1e3));
1317
+ return await Promise.race([settledRequest.catch(() => {
1318
+ throw unavailable();
1319
+ }), new Promise((_, reject) => {
1320
+ publicTimer = setTimeout(() => reject(unavailable()), operationTimeoutMs);
842
1321
  })]);
843
1322
  } finally {
844
- if (timer) clearTimeout(timer);
1323
+ if (publicTimer) clearTimeout(publicTimer);
845
1324
  }
846
1325
  }
847
1326
  }
1327
+ function copyTokenOrNull(value) {
1328
+ return value ? copyToken(value) : null;
1329
+ }
1330
+ function normalizePollSignal(value) {
1331
+ if (value === void 0) return void 0;
1332
+ if (!isObject(value) || Object.keys(value).some((key) => key !== "signal") || value.signal !== void 0 && !(value.signal instanceof AbortSignal)) throw new TypeError("poll options are invalid");
1333
+ return value.signal;
1334
+ }
848
1335
  var DeviceProtocolError = class extends Error {
849
1336
  protocolCode;
850
1337
  constructor(protocolCode) {
@@ -864,8 +1351,15 @@ function normalizeAuthorization(value, currentTime) {
864
1351
  };
865
1352
  }
866
1353
  function normalizeAuthorizationInput(value, currentTime) {
867
- if (!value || !boundedSecret(value.deviceCode) || typeof value.userCode !== "string" || !exactHttpsOrLocalUri(value.verificationUri) || !exactHttpsOrLocalUri(value.verificationUriComplete) || !(value.expiresAt instanceof Date) || !Number.isFinite(value.expiresAt.getTime()) || value.expiresAt.getTime() <= currentTime || !Number.isSafeInteger(value.intervalSeconds) || value.intervalSeconds < 1 || value.intervalSeconds > 60) throw invalid();
868
- return value;
1354
+ if (!value || !boundedSecret(value.deviceCode) || typeof value.userCode !== "string" || !/^[A-Z0-9-]{1,32}$/.test(value.userCode) || !exactHttpsOrLocalUri(value.verificationUri) || !exactHttpsOrLocalUri(value.verificationUriComplete) || !(value.expiresAt instanceof Date) || !Number.isFinite(value.expiresAt.getTime()) || value.expiresAt.getTime() <= currentTime || !Number.isSafeInteger(value.intervalSeconds) || value.intervalSeconds < 1 || value.intervalSeconds > 60) throw invalid();
1355
+ return {
1356
+ deviceCode: value.deviceCode,
1357
+ userCode: value.userCode,
1358
+ verificationUri: value.verificationUri,
1359
+ verificationUriComplete: value.verificationUriComplete,
1360
+ expiresAt: new Date(value.expiresAt),
1361
+ intervalSeconds: value.intervalSeconds
1362
+ };
869
1363
  }
870
1364
  function normalizeTokenResponse(value, currentTime) {
871
1365
  if (!isObject(value) || !boundedSecret(value.access_token) || value.token_type !== "Bearer" || value.scope !== DEVICE_SCOPE || !Number.isSafeInteger(value.expires_in) || value.expires_in < 1 || value.expires_in > 744 * 60 * 60) throw unavailable();
@@ -876,6 +1370,26 @@ function normalizeTokenResponse(value, currentTime) {
876
1370
  scope: DEVICE_SCOPE
877
1371
  };
878
1372
  }
1373
+ function normalizeIssuedSession(value) {
1374
+ if (!isObject(value) || !boundedSecret(value.token) || !isObject(value.user) || typeof value.user.id !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(value.user.id)) throw authorityUnavailable();
1375
+ return {
1376
+ accessToken: value.token,
1377
+ userId: value.user.id
1378
+ };
1379
+ }
1380
+ function extractIssuedToken(value) {
1381
+ return isObject(value) && boundedSecret(value.token) ? value.token : void 0;
1382
+ }
1383
+ function extractRedeemedToken(value) {
1384
+ return isObject(value) && boundedSecret(value.access_token) ? value.access_token : void 0;
1385
+ }
1386
+ function normalizePasswordChange(value) {
1387
+ if (!isObject(value) || value.token !== null && !boundedSecret(value.token) || !isObject(value.user) || typeof value.user.id !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(value.user.id)) throw authorityUnavailable();
1388
+ return {
1389
+ accessToken: value.token,
1390
+ userId: value.user.id
1391
+ };
1392
+ }
879
1393
  function normalizeToken(value, currentTime) {
880
1394
  if (value === null) return null;
881
1395
  if (!value || !boundedSecret(value.accessToken) || value.tokenType !== "Bearer" || value.scope !== DEVICE_SCOPE || !(value.expiresAt instanceof Date) || !Number.isFinite(value.expiresAt.getTime())) throw unavailable();
@@ -934,4 +1448,4 @@ function unavailable() {
934
1448
  return new DeviceAuthorizationError("identity_authority_unavailable");
935
1449
  }
936
1450
  //#endregion
937
- export { DeviceAuthorizationError, createDeviceAuth };
1451
+ export { AuthError, DeviceAuthorizationError, createDeviceAuth };