@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/README.md +50 -4
- package/dist/device.d.ts +8 -2
- package/dist/device.js +570 -56
- package/dist/index.d.ts +2 -3
- package/dist/index.js +321 -101
- package/dist/server.d.ts +6 -3
- package/dist/server.js +126 -35
- package/dist/{types-HSdjQWMZ.d.ts → types-B1SZbs6Z.d.ts} +28 -1
- package/package.json +2 -1
- package/dist/errors-Crd93Rah.d.ts +0 -11
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
|
-
|
|
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
|
|
697
|
-
|
|
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,
|
|
727
|
-
if (
|
|
728
|
-
const
|
|
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(
|
|
731
|
-
let intervalMs =
|
|
868
|
+
const deadline = Math.min(authorization.expiresAt.getTime(), startedAt + maxPollingMs);
|
|
869
|
+
let intervalMs = authorization.intervalSeconds * 1e3;
|
|
732
870
|
while (true) {
|
|
733
|
-
await wait(intervalMs,
|
|
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:
|
|
883
|
+
device_code: authorization.deviceCode,
|
|
745
884
|
client_id: options.clientId
|
|
746
885
|
},
|
|
747
|
-
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 (
|
|
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
|
|
769
|
-
|
|
770
|
-
|
|
907
|
+
const recoverableAccessToken = extractRedeemedToken(response.value);
|
|
908
|
+
let next;
|
|
909
|
+
try {
|
|
910
|
+
next = normalizeTokenResponse(response.value, now());
|
|
771
911
|
} catch {
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
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
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
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
|
|
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
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
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
|
|
835
|
-
|
|
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([
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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 (
|
|
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
|
|
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 };
|