@hyper.authio/client 0.1.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { compactDecrypt, createRemoteJWKSet, customFetch, jwtVerify, importPKCS8, SignJWT, importJWK } from 'jose';
1
+ import { compactDecrypt, errors, decodeProtectedHeader, createRemoteJWKSet, customFetch, jwtVerify, importPKCS8, SignJWT, importJWK } from 'jose';
2
2
 
3
3
  // src/constants.ts
4
4
  var AuthioClaimTypes = {
@@ -18,6 +18,8 @@ var AuthioClaimTypes = {
18
18
  AuthenticationMethods: "amr",
19
19
  IdentityProvider: "idp",
20
20
  Nonce: "nonce",
21
+ AccessTokenHash: "at_hash",
22
+ Events: "events",
21
23
  OrganizationId: "org_id",
22
24
  OrganizationAlias: "org_alias",
23
25
  AccountId: "account_id",
@@ -37,6 +39,12 @@ var AuthioClaimTypes = {
37
39
  ConsumerName: "consumer_name",
38
40
  CredentialId: "credential_id",
39
41
  CredentialType: "credential_type",
42
+ CredentialName: "credential_name",
43
+ CredentialRealm: "credential_realm",
44
+ CredentialClient: "credential_client",
45
+ ActorType: "actor_type",
46
+ OwnerUserId: "owner_user_id",
47
+ OwnerUsername: "owner_username",
40
48
  UserResourceAccess: "user_resource_access",
41
49
  RealmResourceAccess: "realm_resource_access",
42
50
  ClientResourceAccess: "client_resource_access",
@@ -94,7 +102,13 @@ var AuthioOAuthErrors = {
94
102
  ConnectionSelectionRequired: "connection_selection_required",
95
103
  ConnectionPolicyViolation: "connection_policy_violation",
96
104
  ServerError: "server_error",
97
- TemporarilyUnavailable: "temporarily_unavailable"
105
+ TemporarilyUnavailable: "temporarily_unavailable",
106
+ LoginRequired: "login_required",
107
+ InteractionRequired: "interaction_required",
108
+ ConsentRequired: "consent_required",
109
+ AccountSelectionRequired: "account_selection_required",
110
+ InsufficientScope: "insufficient_scope",
111
+ InsufficientUserAuthentication: "insufficient_user_authentication"
98
112
  };
99
113
  var AuthioWebhookEventTypes = {
100
114
  UserCreated: "user_created",
@@ -111,6 +125,8 @@ var AuthioWebhookEventTypes = {
111
125
  AdminResourceDeleted: "admin_resource_deleted",
112
126
  UserSuspended: "user_suspended",
113
127
  UserSuspensionEnded: "user_suspension_ended",
128
+ AttackDetected: "attack_detected",
129
+ CredentialCompromised: "credential_compromised",
114
130
  Ping: "ping"
115
131
  };
116
132
  var AuthioWebhookHeaders = {
@@ -118,6 +134,8 @@ var AuthioWebhookHeaders = {
118
134
  Event: "X-Authio-Event",
119
135
  EventId: "X-Authio-Event-Id"
120
136
  };
137
+ var AuthioCallerKinds = ["user", "client", "consumer", "account_credential"];
138
+ var AuthioPrompts = ["none", "login", "select_account", "consent"];
121
139
  var AuthioAsymmetricAlgorithms = [
122
140
  "RS256",
123
141
  "RS384",
@@ -129,14 +147,27 @@ var AuthioAsymmetricAlgorithms = [
129
147
  "ES384",
130
148
  "ES512"
131
149
  ];
150
+ var AuthioHmacAlgorithms = ["HS256", "HS384", "HS512"];
151
+ var AuthioHmacMinimumSecretBytes = Object.freeze({ HS256: 32, HS384: 48, HS512: 64 });
132
152
  var AuthioDefaults = {
133
153
  DiscoveryPath: "/.well-known/openid-configuration",
154
+ ProtectedResourceMetadataPath: "/.well-known/oauth-protected-resource",
134
155
  RedactedValue: "[redacted]",
135
156
  PlatformRealm: "authio",
136
157
  PlatformClientId: "authio",
137
158
  ApiKeyPrefix: "ak_",
138
159
  ClientSecretPrefix: "cs_",
139
160
  PersonalAccessTokenPrefix: "pat_",
161
+ /** The `client_id` an account credential's client secret is exchanged with: `cred_` and the credential id (format N). */
162
+ CredentialClientIdPrefix: "cred_",
163
+ ApiKeyHeader: "x-api-key",
164
+ RequestIdHeader: "X-Request-Id",
165
+ /** The header `typ` of a back-channel logout token. */
166
+ LogoutTokenType: "logout+jwt",
167
+ /** The event a back-channel logout token carries. */
168
+ BackchannelLogoutEvent: "http://schemas.openid.net/event/backchannel-logout",
169
+ /** Seconds. */
170
+ ClockSkew: 30,
140
171
  /** Seconds. */
141
172
  ClientAssertionLifetime: 120,
142
173
  /** Seconds. */
@@ -188,6 +219,47 @@ var AuthioDiscoveryError = class extends AuthioError {
188
219
  };
189
220
  var AuthioConfigurationError = class extends AuthioError {
190
221
  };
222
+ var AuthioTokenValidationError = class extends AuthioError {
223
+ /** Why, as a stable code. */
224
+ reason;
225
+ constructor(reason, message, options) {
226
+ super(message, options);
227
+ this.reason = reason;
228
+ }
229
+ };
230
+ var AuthioCallbackError = class extends AuthioError {
231
+ /** Why, as a stable code. */
232
+ reason;
233
+ constructor(reason, message) {
234
+ super(message);
235
+ this.reason = reason;
236
+ }
237
+ };
238
+ var AuthioAdminError = class extends AuthioError {
239
+ /** The HTTP status. */
240
+ status;
241
+ /** Authio's error code, such as `ValidationFailed` or `RealmNotFound`. */
242
+ errorCode;
243
+ /** The messages, with the field each one is about. */
244
+ notifications;
245
+ /** The request id, from the envelope or the `X-Request-Id` header. */
246
+ requestId;
247
+ /** Seconds to wait before retrying, from `Retry-After` (`429` and `503`). */
248
+ retryAfterSeconds;
249
+ constructor(details) {
250
+ const notifications = details.notifications ?? [];
251
+ const summary = notifications.map((notification) => notification.message).filter(Boolean).join(" ");
252
+ super(
253
+ `The Authio Admin API answered HTTP ${details.status}${details.errorCode ? ` (${details.errorCode})` : ""}${summary ? `: ${summary}` : "."}`,
254
+ { cause: details.cause }
255
+ );
256
+ this.status = details.status;
257
+ this.errorCode = details.errorCode;
258
+ this.notifications = Object.freeze([...notifications]);
259
+ this.requestId = details.requestId;
260
+ this.retryAfterSeconds = details.retryAfterSeconds;
261
+ }
262
+ };
191
263
 
192
264
  // src/options.ts
193
265
  var systemClock = {
@@ -208,6 +280,12 @@ var systemClock = {
208
280
  signal?.addEventListener("abort", onAbort, { once: true });
209
281
  })
210
282
  };
283
+ var AuthioTokenEndpointAuthMethods = [
284
+ "client_secret_basic",
285
+ "client_secret_post",
286
+ "private_key_jwt",
287
+ "none"
288
+ ];
211
289
  function resolveConnection(options) {
212
290
  const requireHttps = options.requireHttps ?? true;
213
291
  const issuer = resolveIssuer(options);
@@ -523,7 +601,9 @@ function createClientAuthenticator(settings, connection) {
523
601
  const clientId = settings.clientId?.trim();
524
602
  if (!clientId) throw new AuthioConfigurationError("The Authio clientId is required.");
525
603
  const hasKey = settings.clientPrivateKey !== void 0 || !!settings.clientPrivateKeyPath;
526
- const method = settings.clientAuthentication ?? (!settings.clientSecret && hasKey ? "private_key_jwt" : "client_secret_basic");
604
+ const method = settings.tokenEndpointAuthMethod ?? (settings.clientSecret ? "client_secret_basic" : hasKey ? "private_key_jwt" : "none");
605
+ if (!AuthioTokenEndpointAuthMethods.includes(method))
606
+ throw new AuthioConfigurationError(`The Authio tokenEndpointAuthMethod '${String(method)}' is not one of ${AuthioTokenEndpointAuthMethods.join(", ")}.`);
527
607
  if ((method === "client_secret_basic" || method === "client_secret_post") && !settings.clientSecret)
528
608
  throw new AuthioConfigurationError(`The Authio clientSecret is required with ${method}.`);
529
609
  if (method === "private_key_jwt" && !hasKey)
@@ -534,17 +614,22 @@ function createClientAuthenticator(settings, connection) {
534
614
  `The Authio clientAssertionLifetime must be more than 0 and at most ${AuthioDefaults.MaxClientAssertionLifetime} seconds.`
535
615
  );
536
616
  const loadKey = method === "private_key_jwt" ? createKeyLoader(settings) : void 0;
617
+ const secret = async () => {
618
+ const value = typeof settings.clientSecret === "function" ? await settings.clientSecret() : settings.clientSecret;
619
+ if (typeof value !== "string" || !value) throw new AuthioConfigurationError("The Authio clientSecret function returned no secret.");
620
+ return value;
621
+ };
537
622
  return {
538
623
  clientId,
539
624
  method,
540
625
  async apply(form, headers) {
541
626
  switch (method) {
542
627
  case "client_secret_basic":
543
- headers.set("authorization", toBasicAuthorization(clientId, settings.clientSecret));
628
+ headers.set("authorization", toBasicAuthorization(clientId, await secret()));
544
629
  return;
545
630
  case "client_secret_post":
546
631
  form.set("client_id", clientId);
547
- form.set("client_secret", settings.clientSecret);
632
+ form.set("client_secret", await secret());
548
633
  return;
549
634
  case "private_key_jwt":
550
635
  form.set("client_id", clientId);
@@ -598,17 +683,28 @@ function createKeyLoader(settings) {
598
683
  };
599
684
  }
600
685
  var AuthioKeyManagementAlgorithms = ["RSA-OAEP", "RSA-OAEP-256"];
686
+ var AuthioLegacyKeyManagementAlgorithms = ["RSA1_5"];
601
687
  var AuthioContentEncryptionAlgorithms = ["A128CBC-HS256", "A192CBC-HS384", "A256CBC-HS512"];
688
+ var AuthioSupportedContentEncryptionAlgorithms = [...AuthioContentEncryptionAlgorithms, "A128GCM", "A192GCM", "A256GCM"];
602
689
  var rsaAlgorithmIdentifier = [48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0];
603
690
  function isAuthioTokenEncrypted(token) {
604
691
  return typeof token === "string" && token.split(".").length === 5;
605
692
  }
606
- function createAuthioTokenDecrypter(key, setting = "clientEncryptionKey") {
693
+ function createAuthioTokenDecrypter(key, options = {}) {
694
+ const settings = typeof options === "string" ? { setting: options } : options;
695
+ const setting = settings.setting ?? "clientEncryptionKey";
696
+ const keyManagementAlgorithms = resolveAllowlist(settings.encryptionAlgorithms, AuthioKeyManagementAlgorithms, AuthioKeyManagementAlgorithms, "encryptionAlgorithms");
697
+ const contentEncryptionAlgorithms = resolveAllowlist(
698
+ settings.encryptionMethods,
699
+ AuthioContentEncryptionAlgorithms,
700
+ AuthioSupportedContentEncryptionAlgorithms,
701
+ "encryptionMethods"
702
+ );
607
703
  const imported = /* @__PURE__ */ new Map();
608
704
  const importFor = (algorithm) => {
609
705
  let pending = imported.get(algorithm);
610
706
  if (!pending) {
611
- pending = importDecryptionKey(key, algorithm, setting);
707
+ pending = Promise.resolve(typeof key === "function" ? key() : key).then((value) => importDecryptionKey(value, algorithm, setting));
612
708
  imported.set(algorithm, pending);
613
709
  pending.catch(() => imported.delete(algorithm));
614
710
  }
@@ -616,17 +712,83 @@ function createAuthioTokenDecrypter(key, setting = "clientEncryptionKey") {
616
712
  };
617
713
  return async (token) => {
618
714
  if (!isAuthioTokenEncrypted(token)) return token;
619
- if (!key)
620
- throw new AuthioError(
715
+ const algorithm = algorithmOf(token);
716
+ if (AuthioLegacyKeyManagementAlgorithms.includes(algorithm))
717
+ throw new AuthioTokenValidationError(
718
+ "legacy_algorithm",
719
+ settings.allowLegacyEncryptionAlgorithms ? "The token is encrypted with RSA1_5 (PKCS#1 v1.5), which Node and WebCrypto cannot decrypt even with allowLegacyEncryptionAlgorithms; register RSA-OAEP-256 as the client's encryption algorithm in Authio." : "The token is encrypted with the legacy RSA1_5 algorithm, which is refused; register RSA-OAEP-256 as the client's encryption algorithm in Authio."
720
+ );
721
+ if (!keyManagementAlgorithms.includes(algorithm))
722
+ throw new AuthioTokenValidationError(
723
+ "decryption_failed",
724
+ `The token is encrypted with '${algorithm}', and encryptionAlgorithms accepts ${keyManagementAlgorithms.join(" or ")}.`
725
+ );
726
+ const method = contentMethodOf(token);
727
+ if (!contentEncryptionAlgorithms.includes(method))
728
+ throw new AuthioTokenValidationError(
729
+ "decryption_failed",
730
+ `The token's content is encrypted with '${method}', and encryptionMethods accepts ${contentEncryptionAlgorithms.join(" or ")}.`
731
+ );
732
+ if (key === void 0 || key === null || key === "")
733
+ throw new AuthioTokenValidationError(
734
+ "decryption_failed",
621
735
  `Authio encrypted this token to the client's own key; set ${setting} to the private key Authio showed when it generated the pair.`
622
736
  );
623
- const { plaintext } = await compactDecrypt(token, (header) => importFor(header.alg), {
624
- keyManagementAlgorithms: [...AuthioKeyManagementAlgorithms],
625
- contentEncryptionAlgorithms: [...AuthioContentEncryptionAlgorithms]
626
- });
627
- return new TextDecoder().decode(plaintext);
737
+ try {
738
+ const { plaintext } = await compactDecrypt(token, (header) => importFor(header.alg), {
739
+ keyManagementAlgorithms: [...keyManagementAlgorithms],
740
+ contentEncryptionAlgorithms: [...contentEncryptionAlgorithms]
741
+ });
742
+ return new TextDecoder().decode(plaintext);
743
+ } catch (error) {
744
+ if (error instanceof AuthioConfigurationError) throw error;
745
+ if (error instanceof errors.JOSEError)
746
+ throw new AuthioTokenValidationError("decryption_failed", `The token could not be decrypted with ${setting}: ${error.message}`, { cause: error });
747
+ throw error;
748
+ }
628
749
  };
629
750
  }
751
+ function encryptionKeyFromFile(path, setting = "clientEncryptionKeyPath") {
752
+ let current;
753
+ return async () => {
754
+ const nodeCrypto = await loadNodeCrypto();
755
+ if (!nodeCrypto) throw new AuthioConfigurationError(`${setting} needs Node; pass clientEncryptionKey instead.`);
756
+ const { readFile, stat } = await import('fs/promises');
757
+ let written;
758
+ try {
759
+ written = (await stat(path)).mtimeMs;
760
+ } catch (error) {
761
+ throw new AuthioConfigurationError(`${setting} '${path}' could not be read.`, { cause: error });
762
+ }
763
+ if (!current || current.written !== written) current = { written, key: readFile(path, "utf8") };
764
+ return current.key;
765
+ };
766
+ }
767
+ function algorithmOf(token) {
768
+ try {
769
+ return String(decodeProtectedHeader(token).alg ?? "");
770
+ } catch (error) {
771
+ throw new AuthioTokenValidationError("decryption_failed", "The encrypted token's header could not be read.", { cause: error });
772
+ }
773
+ }
774
+ function contentMethodOf(token) {
775
+ try {
776
+ return String(decodeProtectedHeader(token).enc ?? "");
777
+ } catch (error) {
778
+ throw new AuthioTokenValidationError("decryption_failed", "The encrypted token's header could not be read.", { cause: error });
779
+ }
780
+ }
781
+ function resolveAllowlist(configured, defaults, supported, setting) {
782
+ if (configured === void 0 || configured.length === 0) return defaults;
783
+ for (const value of configured) {
784
+ if (AuthioLegacyKeyManagementAlgorithms.includes(value))
785
+ throw new AuthioConfigurationError(
786
+ `${setting} cannot hold ${value}: Node and WebCrypto cannot decrypt PKCS#1 v1.5; register RSA-OAEP-256 as the client's encryption algorithm in Authio.`
787
+ );
788
+ if (!supported.includes(value)) throw new AuthioConfigurationError(`${setting} holds '${value}'; it accepts ${supported.join(", ")}.`);
789
+ }
790
+ return [...new Set(configured)];
791
+ }
630
792
  async function importDecryptionKey(key, algorithm, setting) {
631
793
  try {
632
794
  if (typeof key !== "string") return await importPrivateJwk(key, algorithm, setting);
@@ -666,14 +828,362 @@ function toBase64(bytes) {
666
828
  for (const byte of bytes) binary += String.fromCharCode(byte);
667
829
  return btoa(binary);
668
830
  }
831
+ function createAuthioSigningKeys(connection, discovery) {
832
+ let keys;
833
+ return async (signal) => {
834
+ const document = await discovery.get(signal);
835
+ const uri = document.jwks_uri;
836
+ if (!uri) throw new AuthioError("The Authio discovery document has no jwks_uri.");
837
+ if (!keys || keys.uri !== uri)
838
+ keys = { uri, set: createRemoteJWKSet(new URL(uri), { [customFetch]: connection.fetch, timeoutDuration: connection.httpTimeoutMs }) };
839
+ return keys.set;
840
+ };
841
+ }
842
+ async function verifyAuthioJwt(token, keys, expected) {
843
+ if (!looksLikeJws(token))
844
+ throw new AuthioTokenValidationError("reference_token", "The token is not a JWT; only introspection can resolve it ('introspectionOnly').");
845
+ try {
846
+ const { payload, protectedHeader } = await jwtVerify(token, keys, {
847
+ issuer: expected.issuer,
848
+ audience: expected.audiences ? [...expected.audiences] : void 0,
849
+ algorithms: [...expected.algorithms && expected.algorithms.length > 0 ? expected.algorithms : AuthioAsymmetricAlgorithms],
850
+ clockTolerance: expected.clockSkew ?? AuthioDefaults.ClockSkew,
851
+ requiredClaims: [.../* @__PURE__ */ new Set(["exp", ...expected.requiredClaims ?? []])],
852
+ currentDate: new Date(expected.now)
853
+ });
854
+ return { payload, header: protectedHeader };
855
+ } catch (error) {
856
+ if (error instanceof AuthioConfigurationError) throw error;
857
+ throw toTokenValidationError(error, expected.audiences ?? [], token);
858
+ }
859
+ }
860
+ function withClientSecret(keys, secret) {
861
+ if (!secret) return keys;
862
+ return async (header, token) => {
863
+ const algorithm = String(header.alg);
864
+ if (!AuthioHmacAlgorithms.includes(algorithm)) return keys(header, token);
865
+ const bytes = new TextEncoder().encode(await secret());
866
+ requireHmacSecretLength(algorithm, bytes.length);
867
+ return bytes;
868
+ };
869
+ }
870
+ function requireHmacSecretLength(algorithm, length) {
871
+ const minimum = AuthioHmacMinimumSecretBytes[algorithm];
872
+ if (minimum !== void 0 && length < minimum)
873
+ throw new AuthioConfigurationError(
874
+ `The Authio clientSecret has ${length} bytes, and ${algorithm} needs at least ${minimum} (RFC 7518 \xA73.2): Authio refuses a shorter secret for it.`
875
+ );
876
+ }
877
+ function acceptedSigningAlgorithms(configured, announced) {
878
+ if (configured) return [configured];
879
+ const asymmetric = (Array.isArray(announced) ? announced : []).filter(
880
+ (algorithm) => typeof algorithm === "string" && AuthioAsymmetricAlgorithms.includes(algorithm)
881
+ );
882
+ return asymmetric.length > 0 ? asymmetric : AuthioAsymmetricAlgorithms;
883
+ }
884
+ function toTokenValidationError(error, audiences, token) {
885
+ if (error instanceof AuthioTokenValidationError) return error;
886
+ const refuse = (reason, message) => new AuthioTokenValidationError(reason, message, { cause: error });
887
+ if (error instanceof errors.JWTExpired) {
888
+ const exp = error.payload.exp;
889
+ return refuse("expired", typeof exp === "number" ? `The token expired at '${new Date(exp * 1e3).toISOString()}'` : "The token has expired");
890
+ }
891
+ if (error instanceof errors.JWTClaimValidationFailed) {
892
+ switch (error.claim) {
893
+ case "aud":
894
+ return refuse("invalid_audience", describeAudience([error.payload.aud ?? []].flat(), audiences));
895
+ case "iss":
896
+ return refuse("invalid_issuer", `The issuer '${error.payload.iss ?? ""}' is invalid`);
897
+ case "nbf":
898
+ return refuse(
899
+ "not_yet_valid",
900
+ typeof error.payload.nbf === "number" ? `The token is not valid before '${new Date(error.payload.nbf * 1e3).toISOString()}'` : "The token is not valid yet"
901
+ );
902
+ case "exp":
903
+ return refuse("expired", "The token has no expiration");
904
+ default:
905
+ return refuse("malformed", `The token's ${error.claim} claim is invalid`);
906
+ }
907
+ }
908
+ if (error instanceof errors.JWSSignatureVerificationFailed) return refuse("invalid_signature", "The signature is invalid");
909
+ if (error instanceof errors.JWKSNoMatchingKey || error instanceof errors.JWKSMultipleMatchingKeys)
910
+ return refuse("invalid_signature", "The signature key was not found");
911
+ if (error instanceof errors.JOSEAlgNotAllowed) return refuse("invalid_algorithm", `The algorithm '${algorithmOf2(token)}' is not available`);
912
+ if (error instanceof errors.JWKSTimeout || error instanceof errors.JWKSInvalid)
913
+ return refuse("signing_keys_unavailable", "Authio could not be reached to read its signing keys.");
914
+ if (error instanceof errors.JWTInvalid || error instanceof errors.JWSInvalid) return refuse("malformed", "The token is malformed");
915
+ if (error instanceof errors.JOSEError) return refuse("malformed", error.message);
916
+ return refuse("signing_keys_unavailable", "Authio could not be reached to read its signing keys.");
917
+ }
918
+ async function computeAuthioTokenHash(value, algorithm) {
919
+ const size = /(\d{3})$/.exec(algorithm)?.[1];
920
+ const hash = size === "384" ? "SHA-384" : size === "512" ? "SHA-512" : "SHA-256";
921
+ const digest = new Uint8Array(await crypto.subtle.digest(hash, new TextEncoder().encode(value)));
922
+ return toBase64Url(digest.subarray(0, digest.length / 2));
923
+ }
924
+ async function validateAuthioIdToken(token, context, expectations = {}) {
925
+ const signed = isAuthioTokenEncrypted(token) ? await context.decrypt(token) : token;
926
+ const { payload, header } = await verifyAuthioJwt(signed, context.keys, {
927
+ issuer: context.issuer,
928
+ audiences: [context.clientId],
929
+ algorithms: context.algorithms,
930
+ clockSkew: context.clockSkew,
931
+ now: context.now,
932
+ requiredClaims: ["sub", "iat"]
933
+ });
934
+ const type = typeof payload.typ === "string" ? payload.typ : void 0;
935
+ if (type !== void 0 && type.toLowerCase() !== "id")
936
+ throw new AuthioTokenValidationError("not_id_token", `Only ID tokens are accepted; this token's typ is '${type}'.`);
937
+ const audiences = [payload.aud ?? []].flat();
938
+ if (audiences.length > 1 && payload.azp !== context.clientId)
939
+ throw new AuthioTokenValidationError("invalid_audience", `The ID token has several audiences and its azp is not '${context.clientId}'.`);
940
+ if (expectations.nonce !== void 0 && payload[AuthioClaimTypes.Nonce] !== expectations.nonce)
941
+ throw new AuthioTokenValidationError(
942
+ "invalid_nonce",
943
+ payload[AuthioClaimTypes.Nonce] === void 0 ? "The ID token carries no nonce, although one was sent." : "The ID token's nonce is not the one sent."
944
+ );
945
+ const atHash = payload[AuthioClaimTypes.AccessTokenHash];
946
+ if (expectations.accessToken !== void 0 && typeof atHash === "string") {
947
+ const expected = await computeAuthioTokenHash(expectations.accessToken, String(header.alg ?? ""));
948
+ if (expected !== atHash) throw new AuthioTokenValidationError("invalid_at_hash", "The ID token's at_hash does not match the access token.");
949
+ }
950
+ if (expectations.maxAge !== void 0) {
951
+ const authTime = payload[AuthioClaimTypes.AuthTime];
952
+ const skew = context.clockSkew ?? AuthioDefaults.ClockSkew;
953
+ if (typeof authTime !== "number")
954
+ throw new AuthioTokenValidationError("authentication_too_old", "max_age was requested, and the ID token carries no auth_time.");
955
+ if ((authTime + expectations.maxAge + skew) * 1e3 < context.now)
956
+ throw new AuthioTokenValidationError(
957
+ "authentication_too_old",
958
+ `The person signed in at '${new Date(authTime * 1e3).toISOString()}', more than ${expectations.maxAge} seconds ago.`
959
+ );
960
+ }
961
+ return payload;
962
+ }
963
+ async function validateAuthioLogoutToken(token, context) {
964
+ const signed = isAuthioTokenEncrypted(token) ? await context.decrypt(token) : token;
965
+ const invalid = (message) => new AuthioTokenValidationError("invalid_logout_token", message);
966
+ let headerType;
967
+ try {
968
+ const header = decodeProtectedHeader(signed);
969
+ headerType = typeof header.typ === "string" ? header.typ.toLowerCase() : void 0;
970
+ } catch (error) {
971
+ throw new AuthioTokenValidationError("malformed", "The logout token is malformed", { cause: error });
972
+ }
973
+ const { payload } = await verifyAuthioJwt(signed, context.keys, {
974
+ issuer: context.issuer,
975
+ audiences: [context.clientId],
976
+ algorithms: context.algorithms,
977
+ clockSkew: context.clockSkew,
978
+ now: context.now,
979
+ requiredClaims: ["iat"]
980
+ });
981
+ if (headerType !== AuthioDefaults.LogoutTokenType && headerType !== `application/${AuthioDefaults.LogoutTokenType}`)
982
+ throw invalid(`The token's header typ is '${headerType ?? ""}', not '${AuthioDefaults.LogoutTokenType}'.`);
983
+ const events = payload[AuthioClaimTypes.Events];
984
+ if (!events || typeof events !== "object" || Array.isArray(events) || typeof events[AuthioDefaults.BackchannelLogoutEvent] !== "object")
985
+ throw invalid("The token carries no back-channel logout event.");
986
+ if (payload[AuthioClaimTypes.Nonce] !== void 0) throw invalid("A logout token must not carry a nonce.");
987
+ const subject = typeof payload.sub === "string" && payload.sub ? payload.sub : void 0;
988
+ const sessionId = typeof payload.sid === "string" && payload.sid ? payload.sid : void 0;
989
+ if (!subject && !sessionId) throw invalid("The logout token names neither a subject nor a session.");
990
+ return Object.freeze({ subject, sessionId, tokenId: typeof payload.jti === "string" ? payload.jti : void 0, claims: Object.freeze({ ...payload }) });
991
+ }
992
+ function describeAudience(tokenAudiences, audiences) {
993
+ const known = tokenAudiences.length === 0 ? "" : ` ${tokenAudiences.map((audience) => `'${audience}'`).join(" and ")}`;
994
+ const accepted = audiences.map((audience) => `'${audience}'`).join(" or ");
995
+ return audiences.length === 1 ? `The token is not addressed to this API: its audience${known} is not ${accepted}. Request the token for that resource (RFC 8707).` : `The token is not addressed to this API: its audience${known} is not ${accepted}. Request the token for one of those resources (RFC 8707).`;
996
+ }
997
+ function looksLikeJws(token) {
998
+ const parts = token.split(".");
999
+ return parts.length === 3 && parts[0].length > 0;
1000
+ }
1001
+ function algorithmOf2(token) {
1002
+ try {
1003
+ return String(decodeProtectedHeader(token).alg ?? "");
1004
+ } catch {
1005
+ return "";
1006
+ }
1007
+ }
1008
+ function toBase64Url(bytes) {
1009
+ let binary = "";
1010
+ for (const byte of bytes) binary += String.fromCharCode(byte);
1011
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1012
+ }
1013
+
1014
+ // src/authorization.ts
1015
+ var AuthioChecks = ["pkce", "state", "nonce", "none"];
1016
+ async function createAuthioPkce(method = "S256") {
1017
+ if (method !== "S256" && method !== "plain") throw new TypeError(`The PKCE method '${String(method)}' is not S256 or plain.`);
1018
+ const codeVerifier = randomValue(32);
1019
+ const codeChallenge = method === "plain" ? codeVerifier : toBase64Url2(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(codeVerifier))));
1020
+ return { codeVerifier, codeChallenge, codeChallengeMethod: method };
1021
+ }
1022
+ function randomValue(bytes = 32) {
1023
+ return toBase64Url2(crypto.getRandomValues(new Uint8Array(bytes)));
1024
+ }
1025
+ function resolveChecks(checks, setting = "checks") {
1026
+ const resolved = [];
1027
+ for (const check of checks ?? []) {
1028
+ if (!AuthioChecks.includes(check))
1029
+ throw new AuthioConfigurationError(`The Authio ${setting} value '${String(check)}' is not one of ${AuthioChecks.join(", ")}.`);
1030
+ if (check !== "none" && !resolved.includes(check)) resolved.push(check);
1031
+ }
1032
+ return Object.freeze(resolved);
1033
+ }
1034
+ async function buildAuthorizationUrl(endpoint, clientId, request) {
1035
+ const checks = resolveChecks(request.checks);
1036
+ const pkce = checks.includes("pkce") ? await createAuthioPkce(request.codeChallengeMethod ?? "S256") : void 0;
1037
+ const state = checks.includes("state") ? request.state?.trim() || randomValue() : void 0;
1038
+ const nonce = checks.includes("nonce") ? request.nonce?.trim() || randomValue() : void 0;
1039
+ if (request.maxAge !== void 0 && (!Number.isInteger(request.maxAge) || request.maxAge < 0))
1040
+ throw new RangeError("maxAge must be a non-negative whole number of seconds.");
1041
+ const display = request.popup ? "popup" : request.display;
1042
+ const responseMode = request.popup ? "web_message" : request.responseMode;
1043
+ if (responseMode !== void 0 && responseMode !== "query" && responseMode !== "web_message")
1044
+ throw new AuthioConfigurationError(`The Authio responseMode '${String(responseMode)}' is not query or web_message.`);
1045
+ const url = new URL(endpoint);
1046
+ const parameters = {
1047
+ ...request.extraParameters,
1048
+ response_type: "code",
1049
+ client_id: clientId,
1050
+ redirect_uri: request.redirectUri,
1051
+ scope: toScopeString(request.scopes) ?? "openid",
1052
+ state: state ?? request.state?.trim(),
1053
+ nonce: nonce ?? request.nonce?.trim(),
1054
+ code_challenge: pkce?.codeChallenge,
1055
+ code_challenge_method: pkce?.codeChallengeMethod,
1056
+ resource: request.resource,
1057
+ organization: request.organization,
1058
+ prompt: joined(request.prompt),
1059
+ max_age: request.maxAge?.toString(),
1060
+ acr_values: joined(request.acrValues),
1061
+ login_hint: request.loginHint,
1062
+ id_token_hint: request.idTokenHint,
1063
+ ui_locales: joined(request.uiLocales),
1064
+ display,
1065
+ response_mode: responseMode,
1066
+ idp_hint: request.idpHint
1067
+ };
1068
+ for (const [name, value] of Object.entries(parameters)) if (value !== void 0 && value !== "") url.searchParams.set(name, value);
1069
+ return {
1070
+ url: url.toString(),
1071
+ transaction: Object.freeze({
1072
+ checks,
1073
+ ...state ? { state } : {},
1074
+ ...nonce ? { nonce } : {},
1075
+ ...pkce ? { codeVerifier: pkce.codeVerifier, codeChallengeMethod: pkce.codeChallengeMethod } : {},
1076
+ redirectUri: request.redirectUri,
1077
+ ...request.resource ? { resource: request.resource } : {},
1078
+ ...request.maxAge !== void 0 ? { maxAge: request.maxAge } : {}
1079
+ })
1080
+ };
1081
+ }
1082
+ function readCallbackParameters(callback) {
1083
+ if (callback instanceof URLSearchParams) return Object.fromEntries(callback);
1084
+ if (callback instanceof URL) return Object.fromEntries(callback.searchParams);
1085
+ if (typeof callback === "string") {
1086
+ const text = callback.trim();
1087
+ const query = /^[a-z][a-z0-9+.-]*:/i.test(text) ? new URL(text).search : text.slice(text.indexOf("?") + 1);
1088
+ return Object.fromEntries(new URLSearchParams(query));
1089
+ }
1090
+ const parameters = {};
1091
+ for (const [name, value] of Object.entries(callback ?? {})) {
1092
+ const first = Array.isArray(value) ? value[0] : value;
1093
+ if (typeof first === "string") parameters[name] = first;
1094
+ }
1095
+ return parameters;
1096
+ }
1097
+ function readAuthioFrontChannelLogout(request, issuer) {
1098
+ const parameters = readCallbackParameters(request);
1099
+ const sessionId = parameters.sid?.trim() || void 0;
1100
+ const sentIssuer = parameters.iss?.trim() || void 0;
1101
+ if (sentIssuer !== void 0 && sentIssuer.replace(/\/+$/, "") !== issuer.replace(/\/+$/, ""))
1102
+ throw new AuthioCallbackError("issuer_mismatch", `The front-channel logout comes from the issuer '${sentIssuer}', not '${issuer}'.`);
1103
+ if (sessionId !== void 0 && sentIssuer === void 0)
1104
+ throw new AuthioCallbackError("missing_issuer", "The front-channel logout names a sid without its iss.");
1105
+ return Object.freeze({ sessionId, issuer: sentIssuer });
1106
+ }
1107
+ function parseAuthioBearerChallenge(header) {
1108
+ if (!header || !/^\s*bearer(\s|$)/i.test(header)) return void 0;
1109
+ const values = {};
1110
+ for (const match of header.matchAll(/([a-zA-Z_]+)\s*=\s*"((?:[^"\\]|\\.)*)"|([a-zA-Z_]+)\s*=\s*([^,\s]+)/g)) {
1111
+ const name = (match[1] ?? match[3] ?? "").toLowerCase();
1112
+ values[name] ??= (match[2] ?? match[4] ?? "").replace(/\\(.)/g, "$1");
1113
+ }
1114
+ const maxAge = values.max_age !== void 0 && /^\d+$/.test(values.max_age) ? Number(values.max_age) : void 0;
1115
+ return {
1116
+ error: values.error,
1117
+ errorDescription: values.error_description,
1118
+ scope: values.scope,
1119
+ acrValues: values.acr_values,
1120
+ maxAge,
1121
+ resourceMetadata: values.resource_metadata
1122
+ };
1123
+ }
1124
+ function joined(value) {
1125
+ if (value === void 0) return void 0;
1126
+ const text = (typeof value === "string" ? [value] : [...value]).map((item) => item.trim()).filter(Boolean).join(" ");
1127
+ return text || void 0;
1128
+ }
1129
+ function toBase64Url2(bytes) {
1130
+ let binary = "";
1131
+ for (const byte of bytes) binary += String.fromCharCode(byte);
1132
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1133
+ }
1134
+
1135
+ // src/client.ts
669
1136
  function createAuthioClient(options) {
670
1137
  const connection = resolveConnection(options);
671
1138
  const discovery = createAuthioDiscoveryClient(connection);
672
1139
  const authenticator = createClientAuthenticator(options, connection);
673
1140
  const refreshSkewMs = seconds(options.tokenRefreshSkew, AuthioDefaults.TokenRefreshSkew, "tokenRefreshSkew");
674
- const decrypt = createAuthioTokenDecrypter(options.clientEncryptionKey);
1141
+ const clockSkew = seconds(options.clockSkew, AuthioDefaults.ClockSkew, "clockSkew") / 1e3;
1142
+ const encryptionKey = resolveEncryptionKey(options);
1143
+ const decrypt = createAuthioTokenDecrypter(encryptionKey, {
1144
+ setting: options.clientEncryptionKey === void 0 && options.clientEncryptionKeyPath ? "clientEncryptionKeyPath" : "clientEncryptionKey",
1145
+ allowLegacyEncryptionAlgorithms: options.allowLegacyEncryptionAlgorithms,
1146
+ encryptionAlgorithms: options.encryptionAlgorithms,
1147
+ encryptionMethods: options.encryptionMethods
1148
+ });
1149
+ const checks = resolveChecks(options.checks);
1150
+ const codeChallengeMethod = resolveCodeChallengeMethod(options.codeChallengeMethod);
1151
+ const responseMode = resolveResponseMode(options.responseMode);
1152
+ const idTokenAlgorithm = resolveSigningAlgorithm(options.idTokenSignedResponseAlg, "idTokenSignedResponseAlg", options.clientSecret);
1153
+ const userInfoAlgorithm = resolveSigningAlgorithm(options.userinfoSignedResponseAlg, "userinfoSignedResponseAlg", options.clientSecret) ?? idTokenAlgorithm;
1154
+ const accessTokenAlgorithm = resolveSigningAlgorithm(options.accessTokenSignedResponseAlg, "accessTokenSignedResponseAlg", options.clientSecret);
1155
+ const clientSecret = options.clientSecret === void 0 ? void 0 : () => readSecret(options.clientSecret);
1156
+ const signingKeys = createAuthioSigningKeys(connection, discovery);
675
1157
  const tokens = createTokenClient(connection, discovery, authenticator);
676
1158
  const clientCredentials = createClientCredentialsCache(connection, authenticator.clientId, tokens, refreshSkewMs);
1159
+ const validationContext = async (signal) => {
1160
+ const document = await discovery.get(signal);
1161
+ return {
1162
+ keys: withClientSecret(await signingKeys(signal), clientSecret),
1163
+ decrypt,
1164
+ issuer: connection.issuer,
1165
+ clientId: authenticator.clientId,
1166
+ algorithms: acceptedSigningAlgorithms(idTokenAlgorithm, document.id_token_signing_alg_values_supported),
1167
+ clockSkew,
1168
+ now: connection.clock.now()
1169
+ };
1170
+ };
1171
+ const validateIdToken = async (idToken, expectations = {}) => {
1172
+ requireValue(idToken, "idToken");
1173
+ return validateAuthioIdToken(idToken, await validationContext(expectations.signal), expectations);
1174
+ };
1175
+ const createAuthorizationUrl = async (request = {}) => {
1176
+ const redirectUri = request.redirectUri ?? options.redirectUri;
1177
+ requireValue(redirectUri, "redirectUri");
1178
+ const document = await discovery.get(request.signal);
1179
+ return buildAuthorizationUrl(requireEndpoint(document, "authorization_endpoint"), authenticator.clientId, {
1180
+ ...request,
1181
+ redirectUri,
1182
+ checks: request.checks ?? checks,
1183
+ codeChallengeMethod: request.codeChallengeMethod ?? codeChallengeMethod,
1184
+ responseMode: request.responseMode ?? responseMode
1185
+ });
1186
+ };
677
1187
  return {
678
1188
  issuer: connection.issuer,
679
1189
  clientId: authenticator.clientId,
@@ -716,7 +1226,19 @@ function createAuthioClient(options) {
716
1226
  );
717
1227
  const mediaType = response.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase();
718
1228
  if (response.ok && mediaType === "application/jwt")
719
- return readUserInfoJwt((await response.text()).trim(), document, connection, authenticator.clientId, decrypt, response.status);
1229
+ return readUserInfoJwt(
1230
+ (await response.text()).trim(),
1231
+ document,
1232
+ connection,
1233
+ authenticator.clientId,
1234
+ decrypt,
1235
+ withClientSecret(await signingKeys(callOptions.signal), clientSecret),
1236
+ {
1237
+ algorithms: acceptedSigningAlgorithms(userInfoAlgorithm, document.userinfo_signing_alg_values_supported ?? document.id_token_signing_alg_values_supported),
1238
+ clockSkew,
1239
+ status: response.status
1240
+ }
1241
+ );
720
1242
  if (!response.ok) {
721
1243
  const body = await response.text();
722
1244
  const challenge = toErrorResponse(body) ? void 0 : readBearerChallenge(response);
@@ -724,6 +1246,84 @@ function createAuthioClient(options) {
724
1246
  }
725
1247
  return readProtocolJson(response);
726
1248
  },
1249
+ createAuthorizationUrl,
1250
+ async handleAuthorizationCallback(callback, transaction, callOptions = {}) {
1251
+ if (!transaction?.redirectUri || !Array.isArray(transaction.checks))
1252
+ throw new TypeError("transaction is required: keep what createAuthorizationUrl returned.");
1253
+ const parameters = readCallbackParameters(callback);
1254
+ if (transaction.checks.includes("state") && (!transaction.state || parameters.state !== transaction.state))
1255
+ throw new AuthioCallbackError("state_mismatch", "The callback's state is not the one this sign-in was started with.");
1256
+ const document = await discovery.get(callOptions.signal);
1257
+ if (parameters.iss !== void 0) {
1258
+ if (parameters.iss.replace(/\/+$/, "") !== connection.issuer)
1259
+ throw new AuthioCallbackError("issuer_mismatch", `The callback comes from the issuer '${parameters.iss}', not '${connection.issuer}' (RFC 9207).`);
1260
+ } else if (document.authorization_response_iss_parameter_supported === true) {
1261
+ throw new AuthioCallbackError("missing_issuer", "The callback carries no iss parameter, although Authio sends one (RFC 9207).");
1262
+ }
1263
+ if (parameters.error) throw new AuthioProtocolError(parameters.error, parameters.error_description, parameters.error_code);
1264
+ if (!parameters.code) throw new AuthioCallbackError("missing_code", "The callback carries neither a code nor an error.");
1265
+ const answer = await tokens.authorizationCode({
1266
+ code: parameters.code,
1267
+ redirectUri: transaction.redirectUri,
1268
+ codeVerifier: transaction.checks.includes("pkce") ? transaction.codeVerifier : void 0,
1269
+ resource: transaction.resource,
1270
+ signal: callOptions.signal
1271
+ });
1272
+ const idToken = answer.id_token ? await validateIdToken(answer.id_token, {
1273
+ nonce: transaction.checks.includes("nonce") ? transaction.nonce : void 0,
1274
+ accessToken: answer.access_token,
1275
+ maxAge: transaction.maxAge,
1276
+ signal: callOptions.signal
1277
+ }) : void 0;
1278
+ return { tokens: answer, idToken };
1279
+ },
1280
+ validateIdToken,
1281
+ async validateLogoutToken(logoutToken, callOptions = {}) {
1282
+ requireValue(logoutToken, "logoutToken");
1283
+ return validateAuthioLogoutToken(logoutToken, await validationContext(callOptions.signal));
1284
+ },
1285
+ validateFrontChannelLogout(request) {
1286
+ return readAuthioFrontChannelLogout(request, connection.issuer);
1287
+ },
1288
+ async readAccessTokenClaims(accessToken, callOptions = {}) {
1289
+ requireValue(accessToken, "accessToken");
1290
+ if (isAuthioTokenEncrypted(accessToken) && encryptionKey === void 0) return void 0;
1291
+ const signed = await decrypt(accessToken);
1292
+ if (signed.split(".").length !== 3) return void 0;
1293
+ const document = await discovery.get(callOptions.signal);
1294
+ const { payload } = await verifyAuthioJwt(signed, withClientSecret(await signingKeys(callOptions.signal), clientSecret), {
1295
+ issuer: connection.issuer,
1296
+ algorithms: acceptedSigningAlgorithms(accessTokenAlgorithm, document.id_token_signing_alg_values_supported),
1297
+ clockSkew,
1298
+ now: connection.clock.now()
1299
+ });
1300
+ return payload;
1301
+ },
1302
+ async createEndSessionUrl(request = {}) {
1303
+ const document = await discovery.get(request.signal);
1304
+ const url = new URL(requireEndpoint(document, "end_session_endpoint"));
1305
+ const locales = request.uiLocales === void 0 ? void 0 : (typeof request.uiLocales === "string" ? [request.uiLocales] : [...request.uiLocales]).join(" ");
1306
+ const parameters = {
1307
+ id_token_hint: request.idTokenHint,
1308
+ post_logout_redirect_uri: request.postLogoutRedirectUri ?? options.postLogoutRedirectUri,
1309
+ state: request.state,
1310
+ ui_locales: locales,
1311
+ client_id: request.includeClientId === false ? void 0 : authenticator.clientId
1312
+ };
1313
+ for (const [name, value] of Object.entries(parameters)) if (value) url.searchParams.set(name, value);
1314
+ return url.toString();
1315
+ },
1316
+ async createStepUpAuthorizationUrl(challenge, request = {}) {
1317
+ const parsed = typeof challenge === "string" ? parseAuthioBearerChallenge(challenge) : challenge;
1318
+ if (!parsed || parsed.error !== AuthioOAuthErrors.InsufficientUserAuthentication)
1319
+ throw new TypeError("The challenge is not an insufficient_user_authentication bearer challenge.");
1320
+ return createAuthorizationUrl({
1321
+ ...request,
1322
+ acrValues: request.acrValues ?? parsed.acrValues,
1323
+ maxAge: request.maxAge ?? parsed.maxAge,
1324
+ prompt: request.prompt ?? (parsed.maxAge === 0 ? "login" : void 0)
1325
+ });
1326
+ },
727
1327
  clientCredentialsFetch(fetchOptions = {}) {
728
1328
  const inner = fetchOptions.fetch ?? connection.fetch;
729
1329
  const key = { scopes: fetchOptions.scopes, resource: fetchOptions.resource };
@@ -745,8 +1345,8 @@ function createAuthioClient(options) {
745
1345
  };
746
1346
  }
747
1347
  function createTokenClient(connection, discovery, authenticator) {
748
- const requestToken = async (parameters, signal) => {
749
- const response = await postForm(connection, discovery, authenticator, "token_endpoint", parameters, signal);
1348
+ const requestToken = async (parameters, signal, authenticate = true) => {
1349
+ const response = await postForm(connection, discovery, authenticate ? authenticator : void 0, "token_endpoint", parameters, signal);
750
1350
  const token = await readProtocolJson(response);
751
1351
  return redact(token, ["access_token", "refresh_token", "id_token"]);
752
1352
  };
@@ -762,12 +1362,9 @@ function createTokenClient(connection, discovery, authenticator) {
762
1362
  return await requestToken({ grant_type: AuthioGrantTypes.DeviceCode, device_code: authorization.device_code }, options.signal);
763
1363
  } catch (error) {
764
1364
  if (!(error instanceof AuthioProtocolError)) throw error;
765
- if (error.error === AuthioOAuthErrors.AuthorizationPending) continue;
766
- if (error.error === AuthioOAuthErrors.SlowDown) {
767
- wait += AuthioDefaults.DeviceSlowDownIncrement * 1e3;
768
- continue;
769
- }
770
- throw error;
1365
+ const outcome = toDevicePollOutcome(error.error);
1366
+ if (outcome.outcome === "failed") throw error;
1367
+ wait += outcome.intervalIncrease * 1e3;
771
1368
  }
772
1369
  }
773
1370
  };
@@ -793,7 +1390,7 @@ function createTokenClient(connection, discovery, authenticator) {
793
1390
  refresh: (options) => {
794
1391
  requireValue(options?.refreshToken, "refreshToken");
795
1392
  return requestToken(
796
- { grant_type: AuthioGrantTypes.RefreshToken, refresh_token: options.refreshToken, scope: toScopeString(options.scopes) },
1393
+ { grant_type: AuthioGrantTypes.RefreshToken, refresh_token: options.refreshToken, scope: toScopeString(options.scopes), resource: options.resource },
797
1394
  options.signal
798
1395
  );
799
1396
  },
@@ -843,6 +1440,20 @@ function createTokenClient(connection, discovery, authenticator) {
843
1440
  return redact(await readProtocolJson(response), ["device_code"]);
844
1441
  },
845
1442
  pollDevice,
1443
+ personalAccessToken: (options) => {
1444
+ requireValue(options?.token, "token");
1445
+ return requestToken(
1446
+ {
1447
+ grant_type: AuthioGrantTypes.PersonalAccessToken,
1448
+ client_secret: options.token,
1449
+ client_id: options.clientId,
1450
+ scope: toScopeString(options.scopes),
1451
+ resource: options.resource
1452
+ },
1453
+ options.signal,
1454
+ false
1455
+ );
1456
+ },
846
1457
  password: (options) => {
847
1458
  requireValue(options?.username, "username");
848
1459
  if (typeof options.password !== "string") throw new TypeError("password is required.");
@@ -900,48 +1511,90 @@ function toConnectionExchangeResult(body, status, provider) {
900
1511
  const token = parseProtocolJson(body, status);
901
1512
  if (typeof token.access_token === "string" && token.access_token)
902
1513
  return redact(
903
- {
1514
+ withoutUndefined({
904
1515
  status: "granted",
905
1516
  accessToken: token.access_token,
906
- provider: typeof token.provider === "string" && token.provider ? token.provider : provider,
907
- providerType: token.provider_type,
908
- connectionId: token.connection_id,
909
- tokenType: token.token_type,
910
- issuedTokenType: token.issued_token_type,
911
- scope: token.scope,
912
- externalAccount: token.external_account,
913
- expiresIn: token.expires_in
914
- },
1517
+ issuedTokenType: textOf(token.issued_token_type),
1518
+ tokenType: textOf(token.token_type),
1519
+ expiresIn: typeof token.expires_in === "number" ? token.expires_in : void 0,
1520
+ scope: textOf(token.scope),
1521
+ connectionId: textOf(token.connection_id),
1522
+ provider: textOf(token.provider) ?? provider,
1523
+ providerType: textOf(token.provider_type),
1524
+ externalAccount: token.external_account && typeof token.external_account === "object" ? token.external_account : void 0
1525
+ }),
915
1526
  ["accessToken"]
916
1527
  );
917
1528
  }
918
1529
  const error = toErrorResponse(body);
919
- const description = error?.error_description?.trim() ? error.error_description : `Authio answered HTTP ${status} to the connected-account token exchange.`;
1530
+ const errorDescription = error?.error_description?.trim() ? error.error_description : `Authio answered HTTP ${status} to the connected-account token exchange.`;
1531
+ const errorCode = textOf(error?.error_code);
920
1532
  switch (error?.error) {
921
1533
  case AuthioOAuthErrors.ConnectionRequired:
922
- return { status: "required", connectUri: error.connect_uri, description };
1534
+ return withoutUndefined({
1535
+ status: "required",
1536
+ connectUri: textOf(error.connect_uri),
1537
+ provider: textOf(error.connection_provider) ?? provider,
1538
+ errorCode,
1539
+ errorDescription
1540
+ });
923
1541
  case AuthioOAuthErrors.ConnectionSelectionRequired:
924
- return { status: "selection_required", connections: error.connections ?? [], description };
1542
+ return withoutUndefined({
1543
+ status: "selection_required",
1544
+ provider: textOf(error.connection_provider) ?? provider,
1545
+ connections: (Array.isArray(error.connections) ? error.connections : []).map(toConnectionChoice),
1546
+ errorCode,
1547
+ errorDescription
1548
+ });
925
1549
  default:
926
- return { status: "refused", error: error?.error ?? `http_${status}`, code: error?.error_code, description, connectUri: error?.connect_uri };
1550
+ return withoutUndefined({
1551
+ status: "refused",
1552
+ error: error?.error ?? `http_${status}`,
1553
+ errorCode,
1554
+ errorDescription,
1555
+ connectUri: textOf(error?.connect_uri)
1556
+ });
927
1557
  }
928
1558
  }
1559
+ function toDevicePollOutcome(error) {
1560
+ if (error === AuthioOAuthErrors.AuthorizationPending) return { outcome: "keep_polling", intervalIncrease: 0 };
1561
+ if (error === AuthioOAuthErrors.SlowDown) return { outcome: "keep_polling", intervalIncrease: AuthioDefaults.DeviceSlowDownIncrement };
1562
+ return { outcome: "failed", error };
1563
+ }
1564
+ function toConnectionChoice(value) {
1565
+ const record = value && typeof value === "object" ? value : {};
1566
+ return withoutUndefined({ id: textOf(record.id) ?? "", label: textOf(record.label), externalLogin: textOf(record.external_login) });
1567
+ }
1568
+ function textOf(value) {
1569
+ return typeof value === "string" && value ? value : void 0;
1570
+ }
1571
+ function withoutUndefined(value) {
1572
+ for (const key of Object.keys(value)) if (value[key] === void 0) delete value[key];
1573
+ return value;
1574
+ }
1575
+ function resolveEncryptionKey(options) {
1576
+ if (options.clientEncryptionKey !== void 0) return options.clientEncryptionKey;
1577
+ if (options.clientEncryptionKeyPath) return encryptionKeyFromFile(options.clientEncryptionKeyPath);
1578
+ return void 0;
1579
+ }
929
1580
  async function postForm(connection, discovery, authenticator, endpoint, parameters, signal) {
930
1581
  const document = await discovery.get(signal);
931
1582
  const url = requireEndpoint(document, endpoint);
932
1583
  const form = formBody(parameters);
933
1584
  const headers = new Headers({ accept: "application/json", "content-type": "application/x-www-form-urlencoded" });
934
- await authenticator.apply(form, headers);
1585
+ await authenticator?.apply(form, headers);
935
1586
  return send(connection, url, { method: "POST", headers, body: form.toString() }, signal);
936
1587
  }
937
- async function readUserInfoJwt(jwt, document, connection, clientId, decrypt, status) {
1588
+ async function readUserInfoJwt(jwt, document, connection, clientId, decrypt, keys, settings) {
938
1589
  try {
939
1590
  const signed = isAuthioTokenEncrypted(jwt) ? await decrypt(jwt) : jwt;
940
- const keys = createRemoteJWKSet(new URL(requireEndpoint(document, "jwks_uri")), { [customFetch]: connection.fetch });
941
- const { payload } = await jwtVerify(signed, keys, {
942
- issuer: document.issuer,
943
- audience: clientId,
944
- clockTolerance: 30
1591
+ const { payload } = await verifyAuthioJwt(signed, keys, {
1592
+ issuer: document.issuer.replace(/\/+$/, ""),
1593
+ audiences: [clientId],
1594
+ algorithms: settings.algorithms,
1595
+ clockSkew: settings.clockSkew,
1596
+ now: connection.clock.now(),
1597
+ requiredClaims: ["sub"]
945
1598
  });
946
1599
  return payload;
947
1600
  } catch (error) {
@@ -949,7 +1602,7 @@ async function readUserInfoJwt(jwt, document, connection, clientId, decrypt, sta
949
1602
  "invalid_userinfo",
950
1603
  `The userinfo JWT could not be verified: ${error?.message ?? "unknown reason."}`,
951
1604
  void 0,
952
- status,
1605
+ settings.status,
953
1606
  { cause: error }
954
1607
  );
955
1608
  }
@@ -967,10 +1620,180 @@ function readBearerChallenge(response) {
967
1620
  }
968
1621
  return error ? { error, description } : void 0;
969
1622
  }
1623
+ function resolveCodeChallengeMethod(method) {
1624
+ if (method !== void 0 && method !== "S256" && method !== "plain")
1625
+ throw new AuthioConfigurationError(`The Authio codeChallengeMethod '${String(method)}' is not S256 or plain.`);
1626
+ return method;
1627
+ }
1628
+ function resolveResponseMode(mode) {
1629
+ if (mode !== void 0 && mode !== "query" && mode !== "web_message")
1630
+ throw new AuthioConfigurationError(`The Authio responseMode '${String(mode)}' is not query or web_message.`);
1631
+ return mode;
1632
+ }
1633
+ function resolveSigningAlgorithm(algorithm, setting, clientSecret) {
1634
+ if (algorithm === void 0 || algorithm === "") return void 0;
1635
+ const symmetric = AuthioHmacAlgorithms.includes(algorithm);
1636
+ if (!symmetric && !AuthioAsymmetricAlgorithms.includes(algorithm))
1637
+ throw new AuthioConfigurationError(
1638
+ `The Authio ${setting} '${algorithm}' is not one of ${[...AuthioAsymmetricAlgorithms, ...AuthioHmacAlgorithms].join(", ")}.`
1639
+ );
1640
+ if (symmetric && !clientSecret)
1641
+ throw new AuthioConfigurationError(`The Authio ${setting} ${algorithm} is verified with the client secret (OpenID Connect Core \xA710.1): set clientSecret.`);
1642
+ if (symmetric && typeof clientSecret === "string") requireHmacSecretLength(algorithm, new TextEncoder().encode(clientSecret).length);
1643
+ return algorithm;
1644
+ }
1645
+ async function readSecret(source) {
1646
+ const value = typeof source === "function" ? await source() : source;
1647
+ if (typeof value !== "string" || !value) throw new AuthioConfigurationError("The Authio clientSecret function returned no secret.");
1648
+ return value;
1649
+ }
970
1650
  function requireValue(value, name) {
971
1651
  if (typeof value !== "string" || !value.trim()) throw new TypeError(`${name} is required.`);
972
1652
  }
973
1653
 
1654
+ // src/admin.ts
1655
+ function parseAuthioAdminEnvelope(status, body, headers) {
1656
+ const parsed = parseJson(body);
1657
+ if (status >= 200 && status < 300) {
1658
+ if (!parsed) return { ok: true };
1659
+ return withoutUndefined2({ ok: true, data: parsed.data });
1660
+ }
1661
+ const notifications = Array.isArray(parsed?.notifications) ? parsed.notifications.map(toNotification).filter((item) => item.message) : [];
1662
+ return {
1663
+ ok: false,
1664
+ error: new AuthioAdminError({
1665
+ status,
1666
+ errorCode: textOf2(parsed?.errorCode),
1667
+ notifications,
1668
+ requestId: textOf2(parsed?.requestId) ?? headerOf(headers, AuthioDefaults.RequestIdHeader),
1669
+ retryAfterSeconds: retryAfterOf(headerOf(headers, "Retry-After"))
1670
+ })
1671
+ };
1672
+ }
1673
+ async function readAuthioAdminResponse(response) {
1674
+ const envelope = parseAuthioAdminEnvelope(response.status, response.status === 204 ? null : await response.text(), response.headers);
1675
+ if (!envelope.ok) throw envelope.error;
1676
+ return envelope;
1677
+ }
1678
+ function createAuthioAccountCredential(options) {
1679
+ const value = options.credential?.trim();
1680
+ if (!value) throw new AuthioConfigurationError("The account credential is required.");
1681
+ const kind = value.startsWith(AuthioDefaults.ApiKeyPrefix) ? "api_key" : value.startsWith(AuthioDefaults.ClientSecretPrefix) ? "client_secret" : value.startsWith(AuthioDefaults.PersonalAccessTokenPrefix) ? "personal_access_token" : void 0;
1682
+ if (!kind)
1683
+ throw new AuthioConfigurationError(
1684
+ `The account credential must start with ${AuthioDefaults.ApiKeyPrefix}, ${AuthioDefaults.ClientSecretPrefix} or ${AuthioDefaults.PersonalAccessTokenPrefix}.`
1685
+ );
1686
+ const exchanged = kind === "client_secret" || kind === "personal_access_token" && options.exchangePersonalAccessToken === true;
1687
+ const clock = options.clock ?? systemClock;
1688
+ const inner = options.fetch ?? globalThis.fetch;
1689
+ const scope = toScopeString(options.scopes);
1690
+ const refreshSkewMs = seconds(options.tokenRefreshSkew, AuthioDefaults.TokenRefreshSkew, "tokenRefreshSkew");
1691
+ let client;
1692
+ let cached;
1693
+ let pending;
1694
+ if (exchanged) {
1695
+ if (kind === "client_secret" && !options.credentialId?.trim())
1696
+ throw new AuthioConfigurationError("A cs_ client secret is exchanged with its credential id: set credentialId.");
1697
+ client = createAuthioClient({
1698
+ ...options,
1699
+ realm: AuthioDefaults.PlatformRealm,
1700
+ clientId: kind === "client_secret" ? toCredentialClientId(options.credentialId) : AuthioDefaults.PlatformClientId,
1701
+ clientSecret: kind === "client_secret" ? value : void 0,
1702
+ tokenEndpointAuthMethod: kind === "client_secret" ? "client_secret_post" : "none"
1703
+ });
1704
+ }
1705
+ const exchange = async (signal) => {
1706
+ if (cached && clock.now() < cached.refreshAt) return cached;
1707
+ pending ??= (async () => {
1708
+ const issuedAt = clock.now();
1709
+ const answer = kind === "client_secret" ? await client.tokens.clientCredentials({ scopes: scope, signal }) : await client.tokens.personalAccessToken({ token: value, scopes: scope, signal });
1710
+ cached = toClientCredentialsToken(answer, issuedAt, refreshSkewMs);
1711
+ return cached;
1712
+ })().finally(() => {
1713
+ pending = void 0;
1714
+ });
1715
+ return pending;
1716
+ };
1717
+ const headers = async (signal) => {
1718
+ if (exchanged) {
1719
+ const token = await exchange(signal);
1720
+ return { authorization: `${token.tokenType} ${token.accessToken}` };
1721
+ }
1722
+ return options.presentAs === "x-api-key" ? { [AuthioDefaults.ApiKeyHeader]: value } : { authorization: `Bearer ${value}` };
1723
+ };
1724
+ const authenticatedFetch = async (input, init) => {
1725
+ const request = new Request(input, init);
1726
+ const body = request.body ? await request.arrayBuffer() : void 0;
1727
+ const idempotent = ["GET", "HEAD", "PUT", "DELETE"].includes(request.method.toUpperCase());
1728
+ const attempt = async () => {
1729
+ const merged = new Headers(request.headers);
1730
+ for (const [name, header] of Object.entries(await headers(request.signal))) merged.set(name, header);
1731
+ return inner(request.url, { method: request.method, headers: merged, body, signal: request.signal, redirect: request.redirect });
1732
+ };
1733
+ let response = await attempt();
1734
+ if (response.status === 401 && exchanged) {
1735
+ cached = void 0;
1736
+ response = await attempt();
1737
+ }
1738
+ if ((response.status === 429 || response.status === 503) && idempotent) {
1739
+ const wait = retryAfterOf(response.headers.get("retry-after") ?? void 0);
1740
+ if (wait !== void 0) {
1741
+ await clock.sleep(wait * 1e3, request.signal);
1742
+ response = await attempt();
1743
+ }
1744
+ }
1745
+ return response;
1746
+ };
1747
+ return {
1748
+ kind,
1749
+ exchanged,
1750
+ headers,
1751
+ invalidate() {
1752
+ cached = void 0;
1753
+ },
1754
+ fetch: authenticatedFetch
1755
+ };
1756
+ }
1757
+ function toCredentialClientId(credentialId) {
1758
+ const id = credentialId.trim();
1759
+ return id.startsWith(AuthioDefaults.CredentialClientIdPrefix) ? id : `${AuthioDefaults.CredentialClientIdPrefix}${id.replace(/[{}-]/g, "").toLowerCase()}`;
1760
+ }
1761
+ function parseJson(body) {
1762
+ if (!body?.trim()) return void 0;
1763
+ try {
1764
+ return asRecord(JSON.parse(body));
1765
+ } catch {
1766
+ return void 0;
1767
+ }
1768
+ }
1769
+ function asRecord(value) {
1770
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
1771
+ }
1772
+ function toNotification(value) {
1773
+ const record = asRecord(value) ?? {};
1774
+ return withoutUndefined2({ message: textOf2(record.message) ?? "", field: textOf2(record.field), code: textOf2(record.code) });
1775
+ }
1776
+ function headerOf(headers, name) {
1777
+ if (!headers) return void 0;
1778
+ if (typeof headers.get === "function") return headers.get(name) ?? void 0;
1779
+ const wanted = name.toLowerCase();
1780
+ for (const [key, value] of Object.entries(headers)) if (key.toLowerCase() === wanted) return value;
1781
+ return void 0;
1782
+ }
1783
+ function retryAfterOf(value) {
1784
+ if (!value?.trim()) return void 0;
1785
+ if (/^\d+$/.test(value.trim())) return Number(value.trim());
1786
+ const date = Date.parse(value);
1787
+ return Number.isNaN(date) ? void 0 : Math.max(0, Math.ceil((date - Date.now()) / 1e3));
1788
+ }
1789
+ function textOf2(value) {
1790
+ return typeof value === "string" && value ? value : void 0;
1791
+ }
1792
+ function withoutUndefined2(value) {
1793
+ for (const key of Object.keys(value)) if (value[key] === void 0) delete value[key];
1794
+ return value;
1795
+ }
1796
+
974
1797
  // src/resource-access.ts
975
1798
  var claimSources = new Map(Object.entries(AuthioResourceAccessClaims).map(([name, source]) => [name.toLowerCase(), source]));
976
1799
  var AuthioResourceAccess = class _AuthioResourceAccess {
@@ -1050,7 +1873,7 @@ function parseResourceAccess(claims) {
1050
1873
  return roles.length === 0 ? AuthioResourceAccess.empty : new AuthioResourceAccess(roles);
1051
1874
  }
1052
1875
  function parseResourceAccessClaim(source, value) {
1053
- const root = typeof value === "string" ? parseJson(value) : value;
1876
+ const root = typeof value === "string" ? parseJson2(value) : value;
1054
1877
  if (!isPlainObject(root)) return [];
1055
1878
  const roles = [];
1056
1879
  for (const [name, rolePermissions] of Object.entries(root)) {
@@ -1107,7 +1930,7 @@ function distinctIgnoringCase(values) {
1107
1930
  return true;
1108
1931
  });
1109
1932
  }
1110
- function parseJson(text) {
1933
+ function parseJson2(text) {
1111
1934
  if (!text.trim()) return void 0;
1112
1935
  try {
1113
1936
  return JSON.parse(text);
@@ -1194,9 +2017,23 @@ var asAdminResourceEvent = (event) => dataWhen(
1194
2017
  AuthioWebhookEventTypes.AdminResourceDeleted
1195
2018
  );
1196
2019
  var asUserSuspensionEvent = (event) => dataWhen(event, AuthioWebhookEventTypes.UserSuspended, AuthioWebhookEventTypes.UserSuspensionEnded);
2020
+ var asAttackDetectedEvent = (event) => dataWhen(event, AuthioWebhookEventTypes.AttackDetected);
2021
+ var asCredentialCompromisedEvent = (event) => dataWhen(event, AuthioWebhookEventTypes.CredentialCompromised);
1197
2022
  var asPingEvent = (event) => dataWhen(event, AuthioWebhookEventTypes.Ping);
2023
+ var knownEventTypes = new Set(Object.values(AuthioWebhookEventTypes));
2024
+ function isKnownWebhookEventType(type) {
2025
+ return knownEventTypes.has(type);
2026
+ }
2027
+ function readWebhookEventData(event) {
2028
+ return isKnownWebhookEventType(event.type) ? dataWhen(event, event.type) : void 0;
2029
+ }
2030
+ function toCamelCasedMembers(value) {
2031
+ const result = {};
2032
+ for (const [name, member] of Object.entries(value)) result[name.replace(/_([a-z0-9])/g, (_, letter) => letter.toUpperCase())] = member;
2033
+ return result;
2034
+ }
1198
2035
  function dataWhen(event, ...types) {
1199
- return types.includes(event.type) && event.data !== null && event.data !== void 0 ? event.data : void 0;
2036
+ return types.includes(event.type) && event.data !== null && typeof event.data === "object" && !Array.isArray(event.data) ? toCamelCasedMembers(event.data) : void 0;
1200
2037
  }
1201
2038
  function parseSignatureHeader(header) {
1202
2039
  let timestamp = 0;
@@ -1235,6 +2072,6 @@ function constantTimeEquals(left, right) {
1235
2072
  return difference === 0;
1236
2073
  }
1237
2074
 
1238
- export { AuthioAsymmetricAlgorithms, AuthioClaimTypes, AuthioClientAssertionAlgorithms, AuthioConfigurationError, AuthioContentEncryptionAlgorithms, AuthioDefaults, AuthioDiscoveryError, AuthioError, AuthioGrantTypes, AuthioKeyManagementAlgorithms, AuthioOAuthErrors, AuthioProtocolError, AuthioResourceAccess, AuthioResourceAccessClaims, AuthioScopes, AuthioTokenTypes, AuthioWebhookEventTypes, AuthioWebhookHeaders, asAdminResourceEvent, asKeysRotatedEvent, asLoginEvent, asPingEvent, asTokenReuseDetectedEvent, asTokenRevokedEvent, asUserEvent, asUserLockedOutEvent, asUserSuspensionEvent, computeWebhookSignatureHeader, createAuthioClient, createAuthioDiscoveryClient, createAuthioTokenDecrypter, escapeDataString, isAllowedUrl, isAuthioTokenEncrypted, isWebhookEventOfType, parseResourceAccess, parseResourceAccessClaim, parseWebhookEvent, resolveConnection, resolveIssuer, resourceAccessSourceOf, seconds, systemClock, toBasicAuthorization, toClientCredentialsToken, toScopeString, tryParseWebhookEvent, verifyWebhookSignature };
2075
+ export { AuthioAdminError, AuthioAsymmetricAlgorithms, AuthioCallbackError, AuthioCallerKinds, AuthioChecks, AuthioClaimTypes, AuthioClientAssertionAlgorithms, AuthioConfigurationError, AuthioContentEncryptionAlgorithms, AuthioDefaults, AuthioDiscoveryError, AuthioError, AuthioGrantTypes, AuthioHmacAlgorithms, AuthioHmacMinimumSecretBytes, AuthioKeyManagementAlgorithms, AuthioLegacyKeyManagementAlgorithms, AuthioOAuthErrors, AuthioPrompts, AuthioProtocolError, AuthioResourceAccess, AuthioResourceAccessClaims, AuthioScopes, AuthioSupportedContentEncryptionAlgorithms, AuthioTokenEndpointAuthMethods, AuthioTokenTypes, AuthioTokenValidationError, AuthioWebhookEventTypes, AuthioWebhookHeaders, asAdminResourceEvent, asAttackDetectedEvent, asCredentialCompromisedEvent, asKeysRotatedEvent, asLoginEvent, asPingEvent, asTokenReuseDetectedEvent, asTokenRevokedEvent, asUserEvent, asUserLockedOutEvent, asUserSuspensionEvent, computeAuthioTokenHash, computeWebhookSignatureHeader, createAuthioAccountCredential, createAuthioClient, createAuthioDiscoveryClient, createAuthioPkce, createAuthioSigningKeys, createAuthioTokenDecrypter, encryptionKeyFromFile, escapeDataString, isAllowedUrl, isAuthioTokenEncrypted, isKnownWebhookEventType, isWebhookEventOfType, parseAuthioAdminEnvelope, parseAuthioBearerChallenge, parseResourceAccess, parseResourceAccessClaim, parseWebhookEvent, randomValue, readAuthioAdminResponse, readAuthioFrontChannelLogout, readWebhookEventData, requireHmacSecretLength, resolveConnection, resolveIssuer, resourceAccessSourceOf, seconds, systemClock, toBasicAuthorization, toCamelCasedMembers, toClientCredentialsToken, toConnectionExchangeResult, toCredentialClientId, toDevicePollOutcome, toScopeString, toTokenValidationError, tryParseWebhookEvent, verifyAuthioJwt, verifyWebhookSignature, withClientSecret };
1239
2076
  //# sourceMappingURL=index.js.map
1240
2077
  //# sourceMappingURL=index.js.map