@finueva/auth 0.5.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
@@ -1,45 +1,3 @@
1
- //#region ../../node_modules/.pnpm/destr@2.0.5/node_modules/destr/dist/index.mjs
2
- const suspectProtoRx = /"(?:_|\\u0{2}5[Ff]){2}(?:p|\\u0{2}70)(?:r|\\u0{2}72)(?:o|\\u0{2}6[Ff])(?:t|\\u0{2}74)(?:o|\\u0{2}6[Ff])(?:_|\\u0{2}5[Ff]){2}"\s*:/;
3
- const suspectConstructorRx = /"(?:c|\\u0063)(?:o|\\u006[Ff])(?:n|\\u006[Ee])(?:s|\\u0073)(?:t|\\u0074)(?:r|\\u0072)(?:u|\\u0075)(?:c|\\u0063)(?:t|\\u0074)(?:o|\\u006[Ff])(?:r|\\u0072)"\s*:/;
4
- const JsonSigRx = /^\s*["[{]|^\s*-?\d{1,16}(\.\d{1,17})?([Ee][+-]?\d+)?\s*$/;
5
- function jsonParseTransform(key, value) {
6
- if (key === "__proto__" || key === "constructor" && value && typeof value === "object" && "prototype" in value) {
7
- warnKeyDropped(key);
8
- return;
9
- }
10
- return value;
11
- }
12
- function warnKeyDropped(key) {
13
- console.warn(`[destr] Dropping "${key}" key to prevent prototype pollution.`);
14
- }
15
- function destr(value, options = {}) {
16
- if (typeof value !== "string") return value;
17
- if (value[0] === "\"" && value[value.length - 1] === "\"" && value.indexOf("\\") === -1) return value.slice(1, -1);
18
- const _value = value.trim();
19
- if (_value.length <= 9) switch (_value.toLowerCase()) {
20
- case "true": return true;
21
- case "false": return false;
22
- case "undefined": return;
23
- case "null": return null;
24
- case "nan": return NaN;
25
- case "infinity": return Number.POSITIVE_INFINITY;
26
- case "-infinity": return Number.NEGATIVE_INFINITY;
27
- }
28
- if (!JsonSigRx.test(value)) {
29
- if (options.strict) throw new SyntaxError("[destr] Invalid JSON");
30
- return value;
31
- }
32
- try {
33
- if (suspectProtoRx.test(value) || suspectConstructorRx.test(value)) {
34
- if (options.strict) throw new Error("[destr] Possible prototype pollution");
35
- return JSON.parse(value, jsonParseTransform);
36
- }
37
- return JSON.parse(value);
38
- } catch (error) {
39
- if (options.strict) throw error;
40
- return value;
41
- }
42
- }
43
1
  String.fromCharCode;
44
2
  const HASH_RE = /#/g;
45
3
  const AMPERSAND_RE = /&/g;
@@ -215,6 +173,49 @@ function stringifyParsedURL(parsed) {
215
173
  return (parsed.protocol || parsed[protocolRelative] ? (parsed.protocol || "") + "//" : "") + auth + host + pathname + search + hash;
216
174
  }
217
175
  //#endregion
176
+ //#region ../../node_modules/.pnpm/destr@2.0.5/node_modules/destr/dist/index.mjs
177
+ const suspectProtoRx = /"(?:_|\\u0{2}5[Ff]){2}(?:p|\\u0{2}70)(?:r|\\u0{2}72)(?:o|\\u0{2}6[Ff])(?:t|\\u0{2}74)(?:o|\\u0{2}6[Ff])(?:_|\\u0{2}5[Ff]){2}"\s*:/;
178
+ const suspectConstructorRx = /"(?:c|\\u0063)(?:o|\\u006[Ff])(?:n|\\u006[Ee])(?:s|\\u0073)(?:t|\\u0074)(?:r|\\u0072)(?:u|\\u0075)(?:c|\\u0063)(?:t|\\u0074)(?:o|\\u006[Ff])(?:r|\\u0072)"\s*:/;
179
+ const JsonSigRx = /^\s*["[{]|^\s*-?\d{1,16}(\.\d{1,17})?([Ee][+-]?\d+)?\s*$/;
180
+ function jsonParseTransform(key, value) {
181
+ if (key === "__proto__" || key === "constructor" && value && typeof value === "object" && "prototype" in value) {
182
+ warnKeyDropped(key);
183
+ return;
184
+ }
185
+ return value;
186
+ }
187
+ function warnKeyDropped(key) {
188
+ console.warn(`[destr] Dropping "${key}" key to prevent prototype pollution.`);
189
+ }
190
+ function destr(value, options = {}) {
191
+ if (typeof value !== "string") return value;
192
+ if (value[0] === "\"" && value[value.length - 1] === "\"" && value.indexOf("\\") === -1) return value.slice(1, -1);
193
+ const _value = value.trim();
194
+ if (_value.length <= 9) switch (_value.toLowerCase()) {
195
+ case "true": return true;
196
+ case "false": return false;
197
+ case "undefined": return;
198
+ case "null": return null;
199
+ case "nan": return NaN;
200
+ case "infinity": return Number.POSITIVE_INFINITY;
201
+ case "-infinity": return Number.NEGATIVE_INFINITY;
202
+ }
203
+ if (!JsonSigRx.test(value)) {
204
+ if (options.strict) throw new SyntaxError("[destr] Invalid JSON");
205
+ return value;
206
+ }
207
+ try {
208
+ if (suspectProtoRx.test(value) || suspectConstructorRx.test(value)) {
209
+ if (options.strict) throw new Error("[destr] Possible prototype pollution");
210
+ return JSON.parse(value, jsonParseTransform);
211
+ }
212
+ return JSON.parse(value);
213
+ } catch (error) {
214
+ if (options.strict) throw error;
215
+ return value;
216
+ }
217
+ }
218
+ //#endregion
218
219
  //#region ../../node_modules/.pnpm/ofetch@1.5.1/node_modules/ofetch/dist/shared/ofetch.CWycOUEr.mjs
219
220
  var FetchError = class extends Error {
220
221
  constructor(message, opts) {
@@ -438,6 +439,9 @@ function createFetch(globalOptions = {}) {
438
439
  //#region src/errors.ts
439
440
  const messages = {
440
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.",
441
445
  authentication_required: "Authentication is required.",
442
446
  request_origin_denied: "The request origin is not allowed.",
443
447
  invalid_invitation: "The invitation is invalid.",
@@ -447,6 +451,7 @@ const messages = {
447
451
  invalid_workspace_selection: "The workspace selection is invalid.",
448
452
  organization_context_too_large: "The organization context is too large.",
449
453
  credential_rate_limited: "Credential requests are temporarily limited.",
454
+ auth_request_rate_limited: "Authentication requests are temporarily limited.",
450
455
  state_conflict: "The authentication state conflicts with the request.",
451
456
  access_denied: "Access is denied.",
452
457
  identity_authority_unavailable: "Authentication is temporarily unavailable."
@@ -465,8 +470,56 @@ var AuthError = class extends Error {
465
470
  };
466
471
  //#endregion
467
472
  //#region src/shared.ts
473
+ const DRIVE_CREDENTIAL_LIFETIME_SECONDS = 840;
468
474
  const id = /^[A-Za-z0-9_-]{1,128}$/;
469
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
+ }
470
523
  function normalizeOrigin(value, name) {
471
524
  if (value.length === 0 || value.length > 2048 || /[^\x20-\x7e]/.test(value)) throw new TypeError(`${name} must be an ASCII HTTP origin`);
472
525
  let url;
@@ -502,10 +555,48 @@ function normalizeSession(value) {
502
555
  }
503
556
  };
504
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
+ }
505
576
  function fromAuthorityError(value, fallbackStatus, retryAfter) {
506
577
  const mapped = mapError(isObject(value) && typeof value.code === "string" ? value.code : void 0, fallbackStatus);
507
578
  return new AuthError(mapped.code, mapped.status, parseRetryAfter(retryAfter));
508
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
+ }
509
600
  function authorityUnavailable() {
510
601
  return new AuthError("identity_authority_unavailable", 503);
511
602
  }
@@ -568,6 +659,18 @@ function toDate(value) {
568
659
  const result = value instanceof Date ? value : typeof value === "string" ? new Date(value) : void 0;
569
660
  return result && Number.isFinite(result.getTime()) ? result : void 0;
570
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
+ }
571
674
  function isId(value) {
572
675
  return typeof value === "string" && id.test(value);
573
676
  }
@@ -584,7 +687,7 @@ async function requestJsonResponse(url, options) {
584
687
  method: options.method,
585
688
  headers: options.headers,
586
689
  body: options.body,
587
- credentials: "include",
690
+ credentials: options.credentials ?? "include",
588
691
  retry: 0,
589
692
  timeout: options.timeoutMs,
590
693
  redirect: "manual",
@@ -646,8 +749,9 @@ async function readJson(response) {
646
749
  //#region src/device.ts
647
750
  const DEVICE_SCOPE = "openid";
648
751
  const GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
649
- const MAX_TOKEN_BYTES = 16 * 1024;
752
+ const MAX_TOKEN_BYTES = 16 * 1024 - 7;
650
753
  const MAX_POLLING_MS = 900 * 1e3;
754
+ const REFRESH_WINDOW_MS = 120 * 1e3;
651
755
  const errorMessages = {
652
756
  device_access_denied: "Device authorization was denied.",
653
757
  device_authorization_cancelled: "Device authorization was cancelled.",
@@ -677,7 +781,30 @@ function createDeviceAuth(options) {
677
781
  let token = null;
678
782
  let hydrated = false;
679
783
  let hydration = null;
680
- 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() {
681
808
  if (!hydrated) {
682
809
  hydration ??= hydrate().finally(() => {
683
810
  hydration = null;
@@ -686,17 +813,28 @@ function createDeviceAuth(options) {
686
813
  }
687
814
  if (token && token.expiresAt.getTime() <= now()) {
688
815
  token = null;
816
+ authorityGeneration += 1;
817
+ clearCredential();
689
818
  await removePersisted();
690
819
  }
691
820
  return token ? copyToken(token) : null;
692
821
  }
693
822
  async function hydrate() {
694
823
  try {
695
- const loaded = options.persistence ? normalizeToken(await persistenceCall((signal) => options.persistence.read(signal)), now()) : null;
696
- 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));
697
834
  token = loaded;
698
- } catch {
835
+ } catch (error) {
699
836
  token = null;
837
+ if (error instanceof DeviceAuthorizationError) throw error;
700
838
  throw unavailable();
701
839
  } finally {
702
840
  hydrated = true;
@@ -705,10 +843,11 @@ function createDeviceAuth(options) {
705
843
  async function requestAuthorization(requestOptions = {}) {
706
844
  if (requestOptions.signal?.aborted) throw cancelled();
707
845
  try {
708
- return normalizeAuthorization((await requestJsonResponse(`${authOrigin}/api/v1/auth/device/code`, {
846
+ return normalizeAuthorization((await requestJsonResponse(joinURL(authOrigin, "api/v1/auth/device/code"), {
709
847
  fetch: requestFetch,
710
848
  timeoutMs,
711
849
  method: "POST",
850
+ credentials: "omit",
712
851
  body: {
713
852
  client_id: options.clientId,
714
853
  scope: DEVICE_SCOPE
@@ -722,34 +861,35 @@ function createDeviceAuth(options) {
722
861
  throw unavailable();
723
862
  }
724
863
  }
725
- async function pollForToken(authorization, pollOptions = {}) {
726
- if (pollOptions.signal?.aborted) throw cancelled();
727
- const normalized = normalizeAuthorizationInput(authorization, now());
864
+ async function pollForToken(authorization, signal) {
865
+ if (signal?.aborted) throw cancelled();
866
+ const previous = await readToken();
728
867
  const startedAt = now();
729
- const deadline = Math.min(normalized.expiresAt.getTime(), startedAt + maxPollingMs);
730
- let intervalMs = normalized.intervalSeconds * 1e3;
868
+ const deadline = Math.min(authorization.expiresAt.getTime(), startedAt + maxPollingMs);
869
+ let intervalMs = authorization.intervalSeconds * 1e3;
731
870
  while (true) {
732
- await wait(intervalMs, pollOptions.signal, deadline, now);
871
+ await wait(intervalMs, signal, deadline, now);
733
872
  const requestBudget = deadline - now();
734
873
  if (requestBudget <= 0) throw expired();
735
874
  let response;
736
875
  try {
737
- response = await requestJsonResponse(`${authOrigin}/api/v1/auth/device/token`, {
876
+ response = await requestJsonResponse(joinURL(authOrigin, "api/v1/auth/device/token"), {
738
877
  fetch: requestFetch,
739
878
  timeoutMs: Math.min(timeoutMs, requestBudget),
740
879
  method: "POST",
880
+ credentials: "omit",
741
881
  body: {
742
882
  grant_type: GRANT_TYPE,
743
- device_code: normalized.deviceCode,
883
+ device_code: authorization.deviceCode,
744
884
  client_id: options.clientId
745
885
  },
746
- signal: pollOptions.signal,
886
+ signal,
747
887
  mapError(value) {
748
888
  return new DeviceProtocolError(isObject(value) && typeof value.error === "string" ? value.error : void 0);
749
889
  }
750
890
  });
751
891
  } catch (error) {
752
- if (pollOptions.signal?.aborted) throw cancelled();
892
+ if (signal?.aborted) throw cancelled();
753
893
  if (now() >= deadline) throw expired();
754
894
  if (error instanceof DeviceProtocolError) {
755
895
  if (error.protocolCode === "authorization_pending") continue;
@@ -764,43 +904,54 @@ function createDeviceAuth(options) {
764
904
  throw unavailable();
765
905
  }
766
906
  if (now() >= deadline) throw expired();
767
- const next = normalizeTokenResponse(response.value, now());
768
- if (options.persistence) try {
769
- 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());
770
911
  } catch {
771
- token = null;
772
- hydrated = true;
773
- 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);
774
921
  throw unavailable();
775
922
  }
776
- token = next;
777
- hydrated = true;
778
923
  return copyToken(next);
779
924
  }
780
925
  }
781
- async function authenticated(path, body) {
782
- const current = await getToken();
783
- if (!current) return null;
784
- try {
785
- return await requestJsonResponse(`${authOrigin}/api/v1/auth/${path}`, {
786
- fetch: requestFetch,
787
- timeoutMs,
788
- method: body ? "POST" : "GET",
789
- headers: { authorization: `Bearer ${current.accessToken}` },
790
- body
791
- });
792
- } catch {
793
- throw unavailable();
794
- }
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
+ });
795
945
  }
796
946
  async function revoke(path) {
797
- const current = await getToken();
947
+ const current = await readToken();
798
948
  if (!current) return;
799
949
  try {
800
- await requestJsonResponse(`${authOrigin}/api/v1/auth/${path}`, {
950
+ await requestJsonResponse(joinURL(authOrigin, "api/v1/auth", path), {
801
951
  fetch: requestFetch,
802
952
  timeoutMs,
803
953
  method: "POST",
954
+ credentials: "omit",
804
955
  headers: { authorization: `Bearer ${current.accessToken}` },
805
956
  body: path === "revoke-session" ? { token: current.accessToken } : void 0
806
957
  });
@@ -808,8 +959,139 @@ function createDeviceAuth(options) {
808
959
  throw unavailable();
809
960
  }
810
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();
811
1028
  await removePersisted();
812
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
+ }
813
1095
  async function removePersisted() {
814
1096
  if (!options.persistence) return;
815
1097
  try {
@@ -820,30 +1102,236 @@ function createDeviceAuth(options) {
820
1102
  }
821
1103
  return {
822
1104
  requestAuthorization,
823
- pollForToken,
824
- getToken,
825
- async getSession() {
826
- const response = await authenticated("get-session");
827
- 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);
828
1178
  },
829
- revokeCurrentDevice: () => revoke("revoke-session"),
830
- revokeAllSessions: () => revoke("revoke-sessions")
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;
1296
+ },
1297
+ revokeCurrentDevice: () => serializeAuthority(() => revoke("revoke-session"), true),
1298
+ revokeAllSessions: () => serializeAuthority(() => revoke("revoke-sessions"), true)
831
1299
  };
832
1300
  async function persistenceCall(operation) {
833
- const controller = new AbortController();
834
- 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;
835
1316
  try {
836
- return await Promise.race([Promise.resolve().then(() => operation(controller.signal)), new Promise((_, reject) => {
837
- timer = setTimeout(() => {
838
- controller.abort();
839
- reject(unavailable());
840
- }, Math.min(timeoutMs, 1e3));
1317
+ return await Promise.race([settledRequest.catch(() => {
1318
+ throw unavailable();
1319
+ }), new Promise((_, reject) => {
1320
+ publicTimer = setTimeout(() => reject(unavailable()), operationTimeoutMs);
841
1321
  })]);
842
1322
  } finally {
843
- if (timer) clearTimeout(timer);
1323
+ if (publicTimer) clearTimeout(publicTimer);
844
1324
  }
845
1325
  }
846
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
+ }
847
1335
  var DeviceProtocolError = class extends Error {
848
1336
  protocolCode;
849
1337
  constructor(protocolCode) {
@@ -863,8 +1351,15 @@ function normalizeAuthorization(value, currentTime) {
863
1351
  };
864
1352
  }
865
1353
  function normalizeAuthorizationInput(value, currentTime) {
866
- 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();
867
- 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
+ };
868
1363
  }
869
1364
  function normalizeTokenResponse(value, currentTime) {
870
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();
@@ -875,6 +1370,26 @@ function normalizeTokenResponse(value, currentTime) {
875
1370
  scope: DEVICE_SCOPE
876
1371
  };
877
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
+ }
878
1393
  function normalizeToken(value, currentTime) {
879
1394
  if (value === null) return null;
880
1395
  if (!value || !boundedSecret(value.accessToken) || value.tokenType !== "Bearer" || value.scope !== DEVICE_SCOPE || !(value.expiresAt instanceof Date) || !Number.isFinite(value.expiresAt.getTime())) throw unavailable();
@@ -933,4 +1448,4 @@ function unavailable() {
933
1448
  return new DeviceAuthorizationError("identity_authority_unavailable");
934
1449
  }
935
1450
  //#endregion
936
- export { DeviceAuthorizationError, createDeviceAuth };
1451
+ export { AuthError, DeviceAuthorizationError, createDeviceAuth };