@hyper.authio/client 0.0.0-stage → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1916 @@
1
+ import { compactDecrypt, errors, decodeProtectedHeader, createRemoteJWKSet, customFetch, jwtVerify, importPKCS8, SignJWT, importJWK } from 'jose';
2
+
3
+ // src/constants.ts
4
+ var AuthioClaimTypes = {
5
+ Subject: "sub",
6
+ JwtId: "jti",
7
+ Issuer: "iss",
8
+ Audience: "aud",
9
+ Expiration: "exp",
10
+ IssuedAt: "iat",
11
+ NotBefore: "nbf",
12
+ AuthTime: "auth_time",
13
+ TokenType: "typ",
14
+ AuthorizedParty: "azp",
15
+ Scope: "scope",
16
+ SessionId: "sid",
17
+ AuthenticationContextClass: "acr",
18
+ AuthenticationMethods: "amr",
19
+ IdentityProvider: "idp",
20
+ Nonce: "nonce",
21
+ AccessTokenHash: "at_hash",
22
+ Events: "events",
23
+ OrganizationId: "org_id",
24
+ OrganizationAlias: "org_alias",
25
+ AccountId: "account_id",
26
+ Actor: "act",
27
+ DelegationId: "delegation_id",
28
+ ClientId: "client_id",
29
+ Name: "name",
30
+ GivenName: "given_name",
31
+ FamilyName: "family_name",
32
+ UniqueName: "unique_name",
33
+ PreferredUsername: "preferred_username",
34
+ UpdatedAt: "updated_at",
35
+ Email: "email",
36
+ EmailVerified: "email_verified",
37
+ PhoneNumber: "phone_number",
38
+ PhoneNumberVerified: "phone_number_verified",
39
+ ConsumerName: "consumer_name",
40
+ CredentialId: "credential_id",
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",
48
+ UserResourceAccess: "user_resource_access",
49
+ RealmResourceAccess: "realm_resource_access",
50
+ ClientResourceAccess: "client_resource_access",
51
+ PlanResourceAccess: "plan_resource_access",
52
+ ConsumerResourceAccess: "consumer_resource_access",
53
+ SecretResourceAccess: "secret_resource_access",
54
+ SystemResourceAccess: "system_resource_access"
55
+ };
56
+ var AuthioResourceAccessClaims = Object.freeze({
57
+ [AuthioClaimTypes.UserResourceAccess]: "user",
58
+ [AuthioClaimTypes.RealmResourceAccess]: "realm",
59
+ [AuthioClaimTypes.ClientResourceAccess]: "client",
60
+ [AuthioClaimTypes.PlanResourceAccess]: "plan",
61
+ [AuthioClaimTypes.ConsumerResourceAccess]: "consumer",
62
+ [AuthioClaimTypes.SecretResourceAccess]: "secret",
63
+ [AuthioClaimTypes.SystemResourceAccess]: "system"
64
+ });
65
+ var AuthioScopes = {
66
+ OpenId: "openid",
67
+ Profile: "profile",
68
+ Email: "email",
69
+ Phone: "phone",
70
+ Roles: "roles",
71
+ OfflineAccess: "offline_access"
72
+ };
73
+ var AuthioGrantTypes = {
74
+ AuthorizationCode: "authorization_code",
75
+ RefreshToken: "refresh_token",
76
+ ClientCredentials: "client_credentials",
77
+ Password: "password",
78
+ DeviceCode: "urn:ietf:params:oauth:grant-type:device_code",
79
+ TokenExchange: "urn:ietf:params:oauth:grant-type:token-exchange",
80
+ PersonalAccessToken: "personal_access_token"
81
+ };
82
+ var AuthioTokenTypes = {
83
+ AccessTokenUrn: "urn:ietf:params:oauth:token-type:access_token",
84
+ ConnectionAccessTokenUrn: "urn:authio:params:oauth:token-type:connection-access-token",
85
+ AccessToken: "access_token",
86
+ RefreshToken: "refresh_token"
87
+ };
88
+ var AuthioOAuthErrors = {
89
+ InvalidRequest: "invalid_request",
90
+ InvalidClient: "invalid_client",
91
+ InvalidGrant: "invalid_grant",
92
+ UnauthorizedClient: "unauthorized_client",
93
+ UnsupportedGrantType: "unsupported_grant_type",
94
+ InvalidScope: "invalid_scope",
95
+ InvalidTarget: "invalid_target",
96
+ AccessDenied: "access_denied",
97
+ InvalidToken: "invalid_token",
98
+ AuthorizationPending: "authorization_pending",
99
+ SlowDown: "slow_down",
100
+ ExpiredToken: "expired_token",
101
+ ConnectionRequired: "connection_required",
102
+ ConnectionSelectionRequired: "connection_selection_required",
103
+ ConnectionPolicyViolation: "connection_policy_violation",
104
+ ServerError: "server_error",
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"
112
+ };
113
+ var AuthioWebhookEventTypes = {
114
+ UserCreated: "user_created",
115
+ UserUpdated: "user_updated",
116
+ UserDeleted: "user_deleted",
117
+ LoginSucceeded: "login_succeeded",
118
+ LoginFailed: "login_failed",
119
+ UserLockedOut: "user_locked_out",
120
+ TokenRevoked: "token_revoked",
121
+ KeysRotated: "keys_rotated",
122
+ TokenReuseDetected: "token_reuse_detected",
123
+ AdminResourceCreated: "admin_resource_created",
124
+ AdminResourceUpdated: "admin_resource_updated",
125
+ AdminResourceDeleted: "admin_resource_deleted",
126
+ UserSuspended: "user_suspended",
127
+ UserSuspensionEnded: "user_suspension_ended",
128
+ AttackDetected: "attack_detected",
129
+ CredentialCompromised: "credential_compromised",
130
+ Ping: "ping"
131
+ };
132
+ var AuthioWebhookHeaders = {
133
+ Signature: "X-Authio-Signature",
134
+ Event: "X-Authio-Event",
135
+ EventId: "X-Authio-Event-Id"
136
+ };
137
+ var AuthioCallerKinds = ["user", "client", "consumer", "account_credential"];
138
+ var AuthioPrompts = ["none", "login", "select_account", "consent"];
139
+ var AuthioAsymmetricAlgorithms = [
140
+ "RS256",
141
+ "RS384",
142
+ "RS512",
143
+ "PS256",
144
+ "PS384",
145
+ "PS512",
146
+ "ES256",
147
+ "ES384",
148
+ "ES512"
149
+ ];
150
+ var AuthioDefaults = {
151
+ DiscoveryPath: "/.well-known/openid-configuration",
152
+ ProtectedResourceMetadataPath: "/.well-known/oauth-protected-resource",
153
+ RedactedValue: "[redacted]",
154
+ PlatformRealm: "authio",
155
+ PlatformClientId: "authio",
156
+ ApiKeyPrefix: "ak_",
157
+ ClientSecretPrefix: "cs_",
158
+ PersonalAccessTokenPrefix: "pat_",
159
+ /** The `client_id` an account credential's client secret is exchanged with: `cred_` and the credential id (format N). */
160
+ CredentialClientIdPrefix: "cred_",
161
+ ApiKeyHeader: "x-api-key",
162
+ RequestIdHeader: "X-Request-Id",
163
+ /** The header `typ` of a back-channel logout token. */
164
+ LogoutTokenType: "logout+jwt",
165
+ /** The event a back-channel logout token carries. */
166
+ BackchannelLogoutEvent: "http://schemas.openid.net/event/backchannel-logout",
167
+ /** Seconds. */
168
+ ClockSkew: 30,
169
+ /** Seconds. */
170
+ ClientAssertionLifetime: 120,
171
+ /** Seconds. */
172
+ MaxClientAssertionLifetime: 3600,
173
+ /** Seconds. */
174
+ DeviceInterval: 5,
175
+ /** Seconds. */
176
+ DeviceSlowDownIncrement: 5,
177
+ /** Seconds a token without `expires_in` is assumed to live. */
178
+ AssumedTokenLifetime: 300,
179
+ /** Seconds. */
180
+ DiscoveryCacheDuration: 3600,
181
+ /** Seconds. */
182
+ HttpTimeout: 30,
183
+ /** Seconds. */
184
+ TokenRefreshSkew: 60,
185
+ /** Seconds. */
186
+ WebhookTolerance: 300,
187
+ /** Smallest RSA key Authio verifies a client assertion from, in bits. */
188
+ MinimumRsaKeySize: 2048,
189
+ CredentialHeaders: ["authorization", "proxy-authorization", "x-api-key", "cookie", "set-cookie"]
190
+ };
191
+
192
+ // src/errors.ts
193
+ var AuthioError = class extends Error {
194
+ constructor(message, options) {
195
+ super(message, options);
196
+ this.name = new.target.name;
197
+ }
198
+ };
199
+ var AuthioProtocolError = class extends AuthioError {
200
+ /** The OAuth error code. */
201
+ error;
202
+ /** The OAuth error description. */
203
+ errorDescription;
204
+ /** Authio's own error code (`error_code`), more specific than `error`. */
205
+ errorCode;
206
+ /** The HTTP status, when the error came from a response. */
207
+ status;
208
+ constructor(error, errorDescription, errorCode, status, options) {
209
+ super(errorDescription ? `${error}: ${errorDescription}` : error, options);
210
+ this.error = error;
211
+ this.errorDescription = errorDescription;
212
+ this.errorCode = errorCode;
213
+ this.status = status;
214
+ }
215
+ };
216
+ var AuthioDiscoveryError = class extends AuthioError {
217
+ };
218
+ var AuthioConfigurationError = class extends AuthioError {
219
+ };
220
+ var AuthioTokenValidationError = class extends AuthioError {
221
+ /** Why, as a stable code. */
222
+ reason;
223
+ constructor(reason, message, options) {
224
+ super(message, options);
225
+ this.reason = reason;
226
+ }
227
+ };
228
+ var AuthioCallbackError = class extends AuthioError {
229
+ /** Why, as a stable code. */
230
+ reason;
231
+ constructor(reason, message) {
232
+ super(message);
233
+ this.reason = reason;
234
+ }
235
+ };
236
+ var AuthioAdminError = class extends AuthioError {
237
+ /** The HTTP status. */
238
+ status;
239
+ /** Authio's error code, such as `ValidationFailed` or `RealmNotFound`. */
240
+ errorCode;
241
+ /** The messages, with the field each one is about. */
242
+ notifications;
243
+ /** The request id, from the envelope or the `X-Request-Id` header. */
244
+ requestId;
245
+ /** Seconds to wait before retrying, from `Retry-After` (`429` and `503`). */
246
+ retryAfterSeconds;
247
+ constructor(details) {
248
+ const notifications = details.notifications ?? [];
249
+ const summary = notifications.map((notification) => notification.message).filter(Boolean).join(" ");
250
+ super(
251
+ `The Authio Admin API answered HTTP ${details.status}${details.errorCode ? ` (${details.errorCode})` : ""}${summary ? `: ${summary}` : "."}`,
252
+ { cause: details.cause }
253
+ );
254
+ this.status = details.status;
255
+ this.errorCode = details.errorCode;
256
+ this.notifications = Object.freeze([...notifications]);
257
+ this.requestId = details.requestId;
258
+ this.retryAfterSeconds = details.retryAfterSeconds;
259
+ }
260
+ };
261
+
262
+ // src/options.ts
263
+ var systemClock = {
264
+ now: () => Date.now(),
265
+ sleep: (ms, signal) => new Promise((resolve, reject) => {
266
+ if (signal?.aborted) {
267
+ reject(signal.reason);
268
+ return;
269
+ }
270
+ const timer = setTimeout(() => {
271
+ signal?.removeEventListener("abort", onAbort);
272
+ resolve();
273
+ }, ms);
274
+ const onAbort = () => {
275
+ clearTimeout(timer);
276
+ reject(signal?.reason);
277
+ };
278
+ signal?.addEventListener("abort", onAbort, { once: true });
279
+ })
280
+ };
281
+ function resolveConnection(options) {
282
+ const requireHttps = options.requireHttps ?? true;
283
+ const issuer = resolveIssuer(options);
284
+ for (const [name, value] of [
285
+ ["authority", options.authority],
286
+ ["issuer", issuer]
287
+ ]) {
288
+ if (value && !isAllowedUrl(value, requireHttps))
289
+ throw new AuthioConfigurationError(
290
+ `The Authio ${name} '${value}' must be an absolute HTTPS URL (http is allowed only for loopback, or with requireHttps: false).`
291
+ );
292
+ }
293
+ const fetchImplementation = options.fetch ?? globalThis.fetch;
294
+ if (typeof fetchImplementation !== "function")
295
+ throw new AuthioConfigurationError("No fetch implementation is available: pass options.fetch.");
296
+ return {
297
+ issuer,
298
+ discoveryUrl: issuer + AuthioDefaults.DiscoveryPath,
299
+ requireHttps,
300
+ discoveryCacheDurationMs: seconds(options.discoveryCacheDuration, AuthioDefaults.DiscoveryCacheDuration, "discoveryCacheDuration"),
301
+ httpTimeoutMs: seconds(options.httpTimeout, AuthioDefaults.HttpTimeout, "httpTimeout"),
302
+ fetch: fetchImplementation,
303
+ clock: options.clock ?? systemClock
304
+ };
305
+ }
306
+ function resolveIssuer(options) {
307
+ const issuer = options.issuer?.trim();
308
+ if (issuer) return trimTrailingSlashes(issuer);
309
+ const authority = options.authority?.trim();
310
+ const realm = options.realm?.trim();
311
+ if (!authority || !realm)
312
+ throw new AuthioConfigurationError(
313
+ "The Authio issuer is not set, and authority and realm are not both set to compose it."
314
+ );
315
+ return `${trimTrailingSlashes(authority)}/realms/${escapeDataString(realm)}`;
316
+ }
317
+ function isAllowedUrl(value, requireHttps) {
318
+ let url;
319
+ try {
320
+ url = new URL(value);
321
+ } catch {
322
+ return false;
323
+ }
324
+ if (url.protocol === "https:") return true;
325
+ return url.protocol === "http:" && (!requireHttps || isLoopback(url.hostname));
326
+ }
327
+ function escapeDataString(value) {
328
+ return encodeURIComponent(value).replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
329
+ }
330
+ function seconds(value, fallback, name) {
331
+ const resolved = value ?? fallback;
332
+ if (!Number.isFinite(resolved) || resolved < 0)
333
+ throw new AuthioConfigurationError(`The Authio option ${name} must be a non-negative number of seconds.`);
334
+ return resolved * 1e3;
335
+ }
336
+ function trimTrailingSlashes(value) {
337
+ return value.replace(/\/+$/, "");
338
+ }
339
+ function isLoopback(hostname) {
340
+ const host = hostname.replace(/^\[|\]$/g, "").toLowerCase();
341
+ return host === "localhost" || host === "::1" || /^127(\.\d{1,3}){3}$/.test(host);
342
+ }
343
+
344
+ // src/http.ts
345
+ function send(connection, url, init, signal) {
346
+ const timeout = AbortSignal.timeout(connection.httpTimeoutMs);
347
+ return connection.fetch(url, { ...init, signal: signal ? AbortSignal.any([signal, timeout]) : timeout });
348
+ }
349
+ function formBody(parameters) {
350
+ const form = new URLSearchParams();
351
+ for (const [name, value] of Object.entries(parameters)) {
352
+ if (value !== void 0 && value !== null && value !== "") form.append(name, value);
353
+ }
354
+ return form;
355
+ }
356
+ function toScopeString(scopes) {
357
+ if (scopes === void 0 || scopes === null) return void 0;
358
+ const values = typeof scopes === "string" ? [scopes] : [...scopes];
359
+ const distinct = [...new Set(values.flatMap((scope) => (scope ?? "").split(" ").filter(Boolean)))];
360
+ return distinct.length === 0 ? void 0 : distinct.join(" ");
361
+ }
362
+ async function readProtocolJson(response) {
363
+ const body = await response.text();
364
+ if (!response.ok) throw toProtocolError(body, response.status);
365
+ return parseProtocolJson(body, response.status);
366
+ }
367
+ async function ensureProtocolSuccess(response) {
368
+ if (response.ok) return;
369
+ throw toProtocolError(await response.text(), response.status);
370
+ }
371
+ function parseProtocolJson(body, status) {
372
+ try {
373
+ const value = JSON.parse(body);
374
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new SyntaxError("The body is not a JSON object.");
375
+ return value;
376
+ } catch (error) {
377
+ throw new AuthioProtocolError(
378
+ "invalid_response",
379
+ `Authio answered with a body that could not be read: ${error.message}`,
380
+ void 0,
381
+ status,
382
+ { cause: error }
383
+ );
384
+ }
385
+ }
386
+ function toErrorResponse(body) {
387
+ if (!body?.trim()) return void 0;
388
+ try {
389
+ const value = JSON.parse(body);
390
+ return value && typeof value === "object" && typeof value.error === "string" && value.error ? value : void 0;
391
+ } catch {
392
+ return void 0;
393
+ }
394
+ }
395
+ function toProtocolError(body, status) {
396
+ const error = toErrorResponse(body);
397
+ return error ? new AuthioProtocolError(error.error, error.error_description, error.error_code, status) : new AuthioProtocolError(`http_${status}`, `Authio answered HTTP ${status} without an OAuth error.`, void 0, status);
398
+ }
399
+ function redact(value, secrets) {
400
+ Object.defineProperty(value, /* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom"), {
401
+ enumerable: false,
402
+ value() {
403
+ const copy = {};
404
+ for (const [name, member] of Object.entries(this))
405
+ copy[name] = secrets.includes(name) && member !== void 0 && member !== null ? AuthioDefaults.RedactedValue : member;
406
+ return copy;
407
+ }
408
+ });
409
+ return value;
410
+ }
411
+
412
+ // src/discovery.ts
413
+ function createAuthioDiscoveryClient(options) {
414
+ const connection = isConnection(options) ? options : resolveConnection(options);
415
+ let cached;
416
+ let pending;
417
+ const fresh = () => cached && connection.clock.now() - cached.fetchedAt < connection.discoveryCacheDurationMs ? cached.document : void 0;
418
+ return {
419
+ connection,
420
+ async get(signal) {
421
+ const current = fresh();
422
+ if (current) return current;
423
+ pending ??= fetchDocument(connection, signal).then((document) => {
424
+ cached = { document, fetchedAt: connection.clock.now() };
425
+ return document;
426
+ }).finally(() => {
427
+ pending = void 0;
428
+ });
429
+ return pending;
430
+ },
431
+ invalidate() {
432
+ cached = void 0;
433
+ }
434
+ };
435
+ }
436
+ function requireEndpoint(document, member) {
437
+ const value = document[member];
438
+ if (typeof value !== "string" || !value)
439
+ throw new AuthioDiscoveryError(`The Authio discovery document has no ${member}.`);
440
+ return value;
441
+ }
442
+ async function fetchDocument(connection, signal) {
443
+ const url = connection.discoveryUrl;
444
+ let body;
445
+ try {
446
+ const response = await send(connection, url, { method: "GET", headers: { accept: "application/json" } }, signal);
447
+ body = await response.text();
448
+ if (!response.ok) throw new AuthioDiscoveryError(`Authio answered HTTP ${response.status} for the discovery document at ${url}.`);
449
+ } catch (error) {
450
+ if (error instanceof AuthioDiscoveryError) throw error;
451
+ if (signal?.aborted) throw error;
452
+ const timedOut = error?.name === "TimeoutError";
453
+ throw new AuthioDiscoveryError(
454
+ timedOut ? `Reading the Authio discovery document at ${url} timed out.` : `The Authio discovery document at ${url} could not be read.`,
455
+ { cause: error }
456
+ );
457
+ }
458
+ let document;
459
+ try {
460
+ document = JSON.parse(body);
461
+ } catch (error) {
462
+ throw new AuthioDiscoveryError(`The Authio discovery document at ${url} is not valid JSON.`, { cause: error });
463
+ }
464
+ if (!document || typeof document !== "object" || typeof document.issuer !== "string" || !document.issuer.trim())
465
+ throw new AuthioDiscoveryError(`The Authio discovery document at ${url} has no issuer.`);
466
+ if (document.issuer.replace(/\/+$/, "") !== connection.issuer)
467
+ throw new AuthioDiscoveryError(
468
+ `The Authio discovery document names the issuer '${document.issuer}', not the configured '${connection.issuer}'.`
469
+ );
470
+ return document;
471
+ }
472
+ function isConnection(value) {
473
+ return typeof value.discoveryUrl === "string" && typeof value.httpTimeoutMs === "number";
474
+ }
475
+ var curveAlgorithms = { "P-256": "ES256", "P-384": "ES384", "P-521": "ES512" };
476
+ async function importSigningKey(input, algorithm, keyId, source) {
477
+ if (typeof input === "string") {
478
+ const text = input.trim();
479
+ if (text.startsWith("{")) return importSigningKey(parseJwk(text, source), algorithm, keyId, source);
480
+ return importPem(text, algorithm, keyId, source);
481
+ }
482
+ if (isCryptoKey(input)) {
483
+ const shape = describeCryptoKey(input, source);
484
+ return { key: input, algorithm: settleAlgorithm(shape, algorithm, source), keyId };
485
+ }
486
+ return importFromJwk(input, algorithm, keyId ?? (typeof input.kid === "string" ? input.kid : void 0), source);
487
+ }
488
+ var AuthioClientAssertionAlgorithms = AuthioAsymmetricAlgorithms;
489
+ async function importPem(pem, algorithm, keyId, source) {
490
+ const nodeCrypto = await loadNodeCrypto();
491
+ if (nodeCrypto) {
492
+ let keyObject;
493
+ try {
494
+ keyObject = nodeCrypto.createPrivateKey({ key: pem, format: "pem" });
495
+ } catch (error) {
496
+ throw new AuthioConfigurationError(`${source} could not be read as an RSA or EC private key.`, { cause: error });
497
+ }
498
+ const jwk = keyObject.export({ format: "jwk" });
499
+ return importFromJwk(jwk, algorithm, keyId, source);
500
+ }
501
+ if (!pem.includes("-----BEGIN PRIVATE KEY-----"))
502
+ throw new AuthioConfigurationError(
503
+ `${source} must be a PKCS#8 PEM ("BEGIN PRIVATE KEY") or a JWK outside Node; PKCS#1 and SEC1 keys need Node.`
504
+ );
505
+ const chosen = algorithm?.trim() || "RS256";
506
+ try {
507
+ return { key: await importPKCS8(pem, chosen), algorithm: chosen, keyId };
508
+ } catch (error) {
509
+ throw new AuthioConfigurationError(`${source} could not be imported for ${chosen}.`, { cause: error });
510
+ }
511
+ }
512
+ async function importFromJwk(jwk, algorithm, keyId, source) {
513
+ if (!jwk.d) throw new AuthioConfigurationError(`${source} is a public key; Authio needs the private key to sign.`);
514
+ const shape = describeJwk(jwk, source);
515
+ const chosen = settleAlgorithm(shape, algorithm ?? (typeof jwk.alg === "string" ? jwk.alg : void 0), source);
516
+ try {
517
+ const key = await importJWK({ ...jwk, alg: chosen }, chosen);
518
+ return { key, algorithm: chosen, keyId };
519
+ } catch (error) {
520
+ throw new AuthioConfigurationError(`${source} could not be imported for ${chosen}.`, { cause: error });
521
+ }
522
+ }
523
+ function describeJwk(jwk, source) {
524
+ if (jwk.kty === "RSA") {
525
+ const bits = base64UrlLength(jwk.n ?? "") * 8;
526
+ if (bits < AuthioDefaults.MinimumRsaKeySize)
527
+ throw new AuthioConfigurationError(`${source} is a ${bits}-bit RSA key; use at least ${AuthioDefaults.MinimumRsaKeySize} bits.`);
528
+ return { type: "RSA" };
529
+ }
530
+ if (jwk.kty === "EC") {
531
+ if (!jwk.crv || !curveAlgorithms[jwk.crv])
532
+ throw new AuthioConfigurationError(`${source} is an EC key on a curve Authio does not accept; use P-256, P-384 or P-521.`);
533
+ return { type: "EC", curve: jwk.crv };
534
+ }
535
+ throw new AuthioConfigurationError(`${source} could not be read as an RSA or EC private key.`);
536
+ }
537
+ function describeCryptoKey(key, source) {
538
+ const algorithm = key.algorithm;
539
+ if (algorithm.name === "ECDSA" && algorithm.namedCurve && curveAlgorithms[algorithm.namedCurve])
540
+ return { type: "EC", curve: algorithm.namedCurve };
541
+ if (algorithm.name === "RSASSA-PKCS1-v1_5" || algorithm.name === "RSA-PSS") {
542
+ if ((algorithm.modulusLength ?? 0) < AuthioDefaults.MinimumRsaKeySize)
543
+ throw new AuthioConfigurationError(
544
+ `${source} is a ${algorithm.modulusLength}-bit RSA key; use at least ${AuthioDefaults.MinimumRsaKeySize} bits.`
545
+ );
546
+ const size = algorithm.hash?.name.replace("SHA-", "") ?? "256";
547
+ return { type: "RSA", fixedAlgorithm: `${algorithm.name === "RSA-PSS" ? "PS" : "RS"}${size}` };
548
+ }
549
+ throw new AuthioConfigurationError(`${source} is a CryptoKey Authio cannot sign assertions with; use an RSA or EC signing key.`);
550
+ }
551
+ function settleAlgorithm(shape, configured, source) {
552
+ const suited = shape.fixedAlgorithm !== void 0 ? [shape.fixedAlgorithm] : shape.type === "EC" ? [curveAlgorithms[shape.curve]] : AuthioAsymmetricAlgorithms.filter((name) => name.startsWith("RS") || name.startsWith("PS"));
553
+ const chosen = configured?.trim() || suited[0];
554
+ if (!suited.includes(chosen))
555
+ throw new AuthioConfigurationError(
556
+ `The signing algorithm '${chosen}' does not suit the key in ${source}; use one of: ${suited.join(", ")}.`
557
+ );
558
+ return chosen;
559
+ }
560
+ function parseJwk(text, source) {
561
+ try {
562
+ const parsed = JSON.parse(text);
563
+ if (Array.isArray(parsed.keys)) {
564
+ if (parsed.keys.length !== 1) throw new Error("A JWK Set must hold exactly one key.");
565
+ return parsed.keys[0];
566
+ }
567
+ return parsed;
568
+ } catch (error) {
569
+ throw new AuthioConfigurationError(`${source} is not a valid JWK.`, { cause: error });
570
+ }
571
+ }
572
+ function base64UrlLength(value) {
573
+ const clean = value.replace(/=+$/, "");
574
+ return Math.floor(clean.length * 3 / 4);
575
+ }
576
+ function isCryptoKey(value) {
577
+ return typeof value === "object" && value !== null && "algorithm" in value && "type" in value && "usages" in value;
578
+ }
579
+ var nodeCryptoModule;
580
+ function loadNodeCrypto() {
581
+ nodeCryptoModule ??= globalThis.process?.versions?.node ? import('crypto').catch(() => void 0) : Promise.resolve(void 0);
582
+ return nodeCryptoModule;
583
+ }
584
+
585
+ // src/client-authentication.ts
586
+ function toBasicAuthorization(clientId, clientSecret) {
587
+ const bytes = new TextEncoder().encode(`${escapeDataString(clientId)}:${escapeDataString(clientSecret)}`);
588
+ let binary = "";
589
+ for (const byte of bytes) binary += String.fromCharCode(byte);
590
+ return `Basic ${btoa(binary)}`;
591
+ }
592
+ function createClientAuthenticator(settings, connection) {
593
+ const clientId = settings.clientId?.trim();
594
+ if (!clientId) throw new AuthioConfigurationError("The Authio clientId is required.");
595
+ const hasKey = settings.clientPrivateKey !== void 0 || !!settings.clientPrivateKeyPath;
596
+ const method = settings.clientAuthentication ?? (!settings.clientSecret && hasKey ? "private_key_jwt" : "client_secret_basic");
597
+ if ((method === "client_secret_basic" || method === "client_secret_post") && !settings.clientSecret)
598
+ throw new AuthioConfigurationError(`The Authio clientSecret is required with ${method}.`);
599
+ if (method === "private_key_jwt" && !hasKey)
600
+ throw new AuthioConfigurationError("private_key_jwt needs clientPrivateKey or clientPrivateKeyPath.");
601
+ const lifetimeMs = seconds(settings.clientAssertionLifetime, AuthioDefaults.ClientAssertionLifetime, "clientAssertionLifetime");
602
+ if (lifetimeMs <= 0 || lifetimeMs > AuthioDefaults.MaxClientAssertionLifetime * 1e3)
603
+ throw new AuthioConfigurationError(
604
+ `The Authio clientAssertionLifetime must be more than 0 and at most ${AuthioDefaults.MaxClientAssertionLifetime} seconds.`
605
+ );
606
+ const loadKey = method === "private_key_jwt" ? createKeyLoader(settings) : void 0;
607
+ const secret = async () => {
608
+ const value = typeof settings.clientSecret === "function" ? await settings.clientSecret() : settings.clientSecret;
609
+ if (typeof value !== "string" || !value) throw new AuthioConfigurationError("The Authio clientSecret function returned no secret.");
610
+ return value;
611
+ };
612
+ return {
613
+ clientId,
614
+ method,
615
+ async apply(form, headers) {
616
+ switch (method) {
617
+ case "client_secret_basic":
618
+ headers.set("authorization", toBasicAuthorization(clientId, await secret()));
619
+ return;
620
+ case "client_secret_post":
621
+ form.set("client_id", clientId);
622
+ form.set("client_secret", await secret());
623
+ return;
624
+ case "private_key_jwt":
625
+ form.set("client_id", clientId);
626
+ form.set("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
627
+ form.set("client_assertion", await buildAssertion(clientId, connection, lifetimeMs, await loadKey()));
628
+ return;
629
+ default:
630
+ form.set("client_id", clientId);
631
+ }
632
+ }
633
+ };
634
+ }
635
+ async function buildAssertion(clientId, connection, lifetimeMs, signing) {
636
+ const now = Math.floor(connection.clock.now() / 1e3);
637
+ const jti = [...crypto.getRandomValues(new Uint8Array(16))].map((byte) => byte.toString(16).padStart(2, "0")).join("");
638
+ return new SignJWT({ jti }).setProtectedHeader({ alg: signing.algorithm, typ: "JWT", ...signing.keyId ? { kid: signing.keyId } : {} }).setIssuer(clientId).setSubject(clientId).setAudience(connection.issuer).setIssuedAt(now).setNotBefore(now).setExpirationTime(now + Math.floor(lifetimeMs / 1e3)).sign(signing.key);
639
+ }
640
+ function createKeyLoader(settings) {
641
+ if (settings.clientPrivateKey !== void 0) {
642
+ let loaded;
643
+ return () => {
644
+ loaded ??= importSigningKey(settings.clientPrivateKey, settings.clientSigningAlgorithm, settings.clientKeyId, "clientPrivateKey");
645
+ loaded.catch(() => {
646
+ loaded = void 0;
647
+ });
648
+ return loaded;
649
+ };
650
+ }
651
+ const path = settings.clientPrivateKeyPath;
652
+ let current;
653
+ return async () => {
654
+ const nodeCrypto = await loadNodeCrypto();
655
+ if (!nodeCrypto) throw new AuthioConfigurationError("clientPrivateKeyPath needs Node; pass clientPrivateKey instead.");
656
+ const { readFile, stat } = await import('fs/promises');
657
+ let written;
658
+ try {
659
+ written = (await stat(path)).mtimeMs;
660
+ } catch (error) {
661
+ throw new AuthioConfigurationError(`clientPrivateKeyPath '${path}' could not be read.`, { cause: error });
662
+ }
663
+ if (!current || current.written !== written) {
664
+ const key = readFile(path, "utf8").then(
665
+ (pem) => importSigningKey(pem, settings.clientSigningAlgorithm, settings.clientKeyId, `clientPrivateKeyPath '${path}'`)
666
+ );
667
+ current = { written, key };
668
+ key.catch(() => {
669
+ if (current?.key === key) current = void 0;
670
+ });
671
+ }
672
+ return current.key;
673
+ };
674
+ }
675
+ var AuthioKeyManagementAlgorithms = ["RSA-OAEP", "RSA-OAEP-256"];
676
+ var AuthioLegacyKeyManagementAlgorithms = ["RSA1_5"];
677
+ var AuthioContentEncryptionAlgorithms = ["A128CBC-HS256", "A192CBC-HS384", "A256CBC-HS512"];
678
+ var rsaAlgorithmIdentifier = [48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0];
679
+ function isAuthioTokenEncrypted(token) {
680
+ return typeof token === "string" && token.split(".").length === 5;
681
+ }
682
+ function createAuthioTokenDecrypter(key, options = {}) {
683
+ const settings = typeof options === "string" ? { setting: options } : options;
684
+ const setting = settings.setting ?? "clientEncryptionKey";
685
+ const imported = /* @__PURE__ */ new Map();
686
+ const importFor = (algorithm) => {
687
+ let pending = imported.get(algorithm);
688
+ if (!pending) {
689
+ pending = Promise.resolve(typeof key === "function" ? key() : key).then((value) => importDecryptionKey(value, algorithm, setting));
690
+ imported.set(algorithm, pending);
691
+ pending.catch(() => imported.delete(algorithm));
692
+ }
693
+ return pending;
694
+ };
695
+ return async (token) => {
696
+ if (!isAuthioTokenEncrypted(token)) return token;
697
+ const algorithm = algorithmOf(token);
698
+ if (AuthioLegacyKeyManagementAlgorithms.includes(algorithm))
699
+ throw new AuthioTokenValidationError(
700
+ "legacy_algorithm",
701
+ 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."
702
+ );
703
+ if (!AuthioKeyManagementAlgorithms.includes(algorithm))
704
+ throw new AuthioTokenValidationError(
705
+ "decryption_failed",
706
+ `The token is encrypted with '${algorithm}'; Authio encrypts with ${AuthioKeyManagementAlgorithms.join(" or ")}.`
707
+ );
708
+ if (key === void 0 || key === null || key === "")
709
+ throw new AuthioTokenValidationError(
710
+ "decryption_failed",
711
+ `Authio encrypted this token to the client's own key; set ${setting} to the private key Authio showed when it generated the pair.`
712
+ );
713
+ try {
714
+ const { plaintext } = await compactDecrypt(token, (header) => importFor(header.alg), {
715
+ keyManagementAlgorithms: [...AuthioKeyManagementAlgorithms],
716
+ contentEncryptionAlgorithms: [...AuthioContentEncryptionAlgorithms]
717
+ });
718
+ return new TextDecoder().decode(plaintext);
719
+ } catch (error) {
720
+ if (error instanceof AuthioConfigurationError) throw error;
721
+ if (error instanceof errors.JOSEError)
722
+ throw new AuthioTokenValidationError("decryption_failed", `The token could not be decrypted with ${setting}: ${error.message}`, { cause: error });
723
+ throw error;
724
+ }
725
+ };
726
+ }
727
+ function encryptionKeyFromFile(path, setting = "clientEncryptionKeyPath") {
728
+ let current;
729
+ return async () => {
730
+ const nodeCrypto = await loadNodeCrypto();
731
+ if (!nodeCrypto) throw new AuthioConfigurationError(`${setting} needs Node; pass clientEncryptionKey instead.`);
732
+ const { readFile, stat } = await import('fs/promises');
733
+ let written;
734
+ try {
735
+ written = (await stat(path)).mtimeMs;
736
+ } catch (error) {
737
+ throw new AuthioConfigurationError(`${setting} '${path}' could not be read.`, { cause: error });
738
+ }
739
+ if (!current || current.written !== written) current = { written, key: readFile(path, "utf8") };
740
+ return current.key;
741
+ };
742
+ }
743
+ function algorithmOf(token) {
744
+ try {
745
+ return String(decodeProtectedHeader(token).alg ?? "");
746
+ } catch (error) {
747
+ throw new AuthioTokenValidationError("decryption_failed", "The encrypted token's header could not be read.", { cause: error });
748
+ }
749
+ }
750
+ async function importDecryptionKey(key, algorithm, setting) {
751
+ try {
752
+ if (typeof key !== "string") return await importPrivateJwk(key, algorithm, setting);
753
+ const text = key.trim();
754
+ return text.startsWith("{") ? await importPrivateJwk(JSON.parse(text), algorithm, setting) : await importPKCS8(toPkcs8(text), algorithm);
755
+ } catch (error) {
756
+ if (error instanceof AuthioConfigurationError) throw error;
757
+ throw new AuthioConfigurationError(`${setting} is neither a private JWK nor a PEM private key.`, { cause: error });
758
+ }
759
+ }
760
+ function importPrivateJwk(jwk, algorithm, setting) {
761
+ if (!jwk.d) throw new AuthioConfigurationError(`${setting} is a JWK without its private parameters; only the private key decrypts.`);
762
+ return importJWK({ ...jwk, alg: algorithm, use: "enc", key_ops: void 0 }, algorithm);
763
+ }
764
+ function toPkcs8(pem) {
765
+ const normalized = pem.replace(/\\n/g, "\n").trim();
766
+ if (!normalized.includes("BEGIN RSA PRIVATE KEY")) return normalized;
767
+ const pkcs1 = fromBase64(normalized.replace(/-----[^-]+-----|\s/g, ""));
768
+ const body = [2, 1, 0, ...rsaAlgorithmIdentifier, 4, ...derLength(pkcs1.length), ...pkcs1];
769
+ const der = Uint8Array.from([48, ...derLength(body.length), ...body]);
770
+ const lines = toBase64(der).match(/.{1,64}/g) ?? [];
771
+ return `-----BEGIN PRIVATE KEY-----
772
+ ${lines.join("\n")}
773
+ -----END PRIVATE KEY-----`;
774
+ }
775
+ function derLength(length) {
776
+ if (length < 128) return [length];
777
+ const bytes = [];
778
+ for (let rest = length; rest > 0; rest >>= 8) bytes.unshift(rest & 255);
779
+ return [128 | bytes.length, ...bytes];
780
+ }
781
+ function fromBase64(text) {
782
+ return Uint8Array.from(atob(text), (character) => character.charCodeAt(0));
783
+ }
784
+ function toBase64(bytes) {
785
+ let binary = "";
786
+ for (const byte of bytes) binary += String.fromCharCode(byte);
787
+ return btoa(binary);
788
+ }
789
+ function createAuthioSigningKeys(connection, discovery) {
790
+ let keys;
791
+ return async (signal) => {
792
+ const document = await discovery.get(signal);
793
+ const uri = document.jwks_uri;
794
+ if (!uri) throw new AuthioError("The Authio discovery document has no jwks_uri.");
795
+ if (!keys || keys.uri !== uri)
796
+ keys = { uri, set: createRemoteJWKSet(new URL(uri), { [customFetch]: connection.fetch, timeoutDuration: connection.httpTimeoutMs }) };
797
+ return keys.set;
798
+ };
799
+ }
800
+ async function verifyAuthioJwt(token, keys, expected) {
801
+ if (!looksLikeJws(token))
802
+ throw new AuthioTokenValidationError("reference_token", "The token is not a JWT; only introspection can resolve it ('introspectionOnly').");
803
+ try {
804
+ const { payload, protectedHeader } = await jwtVerify(token, keys, {
805
+ issuer: expected.issuer,
806
+ audience: [...expected.audiences],
807
+ algorithms: [...expected.algorithms && expected.algorithms.length > 0 ? expected.algorithms : AuthioAsymmetricAlgorithms],
808
+ clockTolerance: expected.clockSkew ?? AuthioDefaults.ClockSkew,
809
+ requiredClaims: [.../* @__PURE__ */ new Set(["exp", ...expected.requiredClaims ?? []])],
810
+ currentDate: new Date(expected.now)
811
+ });
812
+ return { payload, header: protectedHeader };
813
+ } catch (error) {
814
+ throw toTokenValidationError(error, expected.audiences, token);
815
+ }
816
+ }
817
+ function toTokenValidationError(error, audiences, token) {
818
+ if (error instanceof AuthioTokenValidationError) return error;
819
+ const refuse = (reason, message) => new AuthioTokenValidationError(reason, message, { cause: error });
820
+ if (error instanceof errors.JWTExpired) {
821
+ const exp = error.payload.exp;
822
+ return refuse("expired", typeof exp === "number" ? `The token expired at '${new Date(exp * 1e3).toISOString()}'` : "The token has expired");
823
+ }
824
+ if (error instanceof errors.JWTClaimValidationFailed) {
825
+ switch (error.claim) {
826
+ case "aud":
827
+ return refuse("invalid_audience", describeAudience([error.payload.aud ?? []].flat(), audiences));
828
+ case "iss":
829
+ return refuse("invalid_issuer", `The issuer '${error.payload.iss ?? ""}' is invalid`);
830
+ case "nbf":
831
+ return refuse(
832
+ "not_yet_valid",
833
+ typeof error.payload.nbf === "number" ? `The token is not valid before '${new Date(error.payload.nbf * 1e3).toISOString()}'` : "The token is not valid yet"
834
+ );
835
+ case "exp":
836
+ return refuse("expired", "The token has no expiration");
837
+ default:
838
+ return refuse("malformed", `The token's ${error.claim} claim is invalid`);
839
+ }
840
+ }
841
+ if (error instanceof errors.JWSSignatureVerificationFailed) return refuse("invalid_signature", "The signature is invalid");
842
+ if (error instanceof errors.JWKSNoMatchingKey || error instanceof errors.JWKSMultipleMatchingKeys)
843
+ return refuse("invalid_signature", "The signature key was not found");
844
+ if (error instanceof errors.JOSEAlgNotAllowed) return refuse("invalid_algorithm", `The algorithm '${algorithmOf2(token)}' is not available`);
845
+ if (error instanceof errors.JWKSTimeout || error instanceof errors.JWKSInvalid)
846
+ return refuse("signing_keys_unavailable", "Authio could not be reached to read its signing keys.");
847
+ if (error instanceof errors.JWTInvalid || error instanceof errors.JWSInvalid) return refuse("malformed", "The token is malformed");
848
+ if (error instanceof errors.JOSEError) return refuse("malformed", error.message);
849
+ return refuse("signing_keys_unavailable", "Authio could not be reached to read its signing keys.");
850
+ }
851
+ async function computeAuthioTokenHash(value, algorithm) {
852
+ const size = /(\d{3})$/.exec(algorithm)?.[1];
853
+ const hash = size === "384" ? "SHA-384" : size === "512" ? "SHA-512" : "SHA-256";
854
+ const digest = new Uint8Array(await crypto.subtle.digest(hash, new TextEncoder().encode(value)));
855
+ return toBase64Url(digest.subarray(0, digest.length / 2));
856
+ }
857
+ async function validateAuthioIdToken(token, context, expectations = {}) {
858
+ const signed = isAuthioTokenEncrypted(token) ? await context.decrypt(token) : token;
859
+ const { payload, header } = await verifyAuthioJwt(signed, context.keys, {
860
+ issuer: context.issuer,
861
+ audiences: [context.clientId],
862
+ algorithms: context.algorithms,
863
+ clockSkew: context.clockSkew,
864
+ now: context.now,
865
+ requiredClaims: ["sub", "iat"]
866
+ });
867
+ const type = typeof payload.typ === "string" ? payload.typ : void 0;
868
+ if (type !== void 0 && type.toLowerCase() !== "id")
869
+ throw new AuthioTokenValidationError("not_id_token", `Only ID tokens are accepted; this token's typ is '${type}'.`);
870
+ const audiences = [payload.aud ?? []].flat();
871
+ if (audiences.length > 1 && payload.azp !== context.clientId)
872
+ throw new AuthioTokenValidationError("invalid_audience", `The ID token has several audiences and its azp is not '${context.clientId}'.`);
873
+ if (expectations.nonce !== void 0 && payload[AuthioClaimTypes.Nonce] !== expectations.nonce)
874
+ throw new AuthioTokenValidationError(
875
+ "invalid_nonce",
876
+ 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."
877
+ );
878
+ const atHash = payload[AuthioClaimTypes.AccessTokenHash];
879
+ if (expectations.accessToken !== void 0 && typeof atHash === "string") {
880
+ const expected = await computeAuthioTokenHash(expectations.accessToken, String(header.alg ?? ""));
881
+ if (expected !== atHash) throw new AuthioTokenValidationError("invalid_at_hash", "The ID token's at_hash does not match the access token.");
882
+ }
883
+ if (expectations.maxAge !== void 0) {
884
+ const authTime = payload[AuthioClaimTypes.AuthTime];
885
+ const skew = context.clockSkew ?? AuthioDefaults.ClockSkew;
886
+ if (typeof authTime !== "number")
887
+ throw new AuthioTokenValidationError("authentication_too_old", "max_age was requested, and the ID token carries no auth_time.");
888
+ if ((authTime + expectations.maxAge + skew) * 1e3 < context.now)
889
+ throw new AuthioTokenValidationError(
890
+ "authentication_too_old",
891
+ `The person signed in at '${new Date(authTime * 1e3).toISOString()}', more than ${expectations.maxAge} seconds ago.`
892
+ );
893
+ }
894
+ return payload;
895
+ }
896
+ async function validateAuthioLogoutToken(token, context) {
897
+ const signed = isAuthioTokenEncrypted(token) ? await context.decrypt(token) : token;
898
+ const invalid = (message) => new AuthioTokenValidationError("invalid_logout_token", message);
899
+ let headerType;
900
+ try {
901
+ const header = decodeProtectedHeader(signed);
902
+ headerType = typeof header.typ === "string" ? header.typ.toLowerCase() : void 0;
903
+ } catch (error) {
904
+ throw new AuthioTokenValidationError("malformed", "The logout token is malformed", { cause: error });
905
+ }
906
+ const { payload } = await verifyAuthioJwt(signed, context.keys, {
907
+ issuer: context.issuer,
908
+ audiences: [context.clientId],
909
+ algorithms: context.algorithms,
910
+ clockSkew: context.clockSkew,
911
+ now: context.now,
912
+ requiredClaims: ["iat"]
913
+ });
914
+ if (headerType !== AuthioDefaults.LogoutTokenType && headerType !== `application/${AuthioDefaults.LogoutTokenType}`)
915
+ throw invalid(`The token's header typ is '${headerType ?? ""}', not '${AuthioDefaults.LogoutTokenType}'.`);
916
+ const events = payload[AuthioClaimTypes.Events];
917
+ if (!events || typeof events !== "object" || Array.isArray(events) || typeof events[AuthioDefaults.BackchannelLogoutEvent] !== "object")
918
+ throw invalid("The token carries no back-channel logout event.");
919
+ if (payload[AuthioClaimTypes.Nonce] !== void 0) throw invalid("A logout token must not carry a nonce.");
920
+ const subject = typeof payload.sub === "string" && payload.sub ? payload.sub : void 0;
921
+ const sessionId = typeof payload.sid === "string" && payload.sid ? payload.sid : void 0;
922
+ if (!subject && !sessionId) throw invalid("The logout token names neither a subject nor a session.");
923
+ return Object.freeze({ subject, sessionId, tokenId: typeof payload.jti === "string" ? payload.jti : void 0, claims: Object.freeze({ ...payload }) });
924
+ }
925
+ function describeAudience(tokenAudiences, audiences) {
926
+ const known = tokenAudiences.length === 0 ? "" : ` ${tokenAudiences.map((audience) => `'${audience}'`).join(" and ")}`;
927
+ const accepted = audiences.map((audience) => `'${audience}'`).join(" or ");
928
+ 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).`;
929
+ }
930
+ function looksLikeJws(token) {
931
+ const parts = token.split(".");
932
+ return parts.length === 3 && parts[0].length > 0;
933
+ }
934
+ function algorithmOf2(token) {
935
+ try {
936
+ return String(decodeProtectedHeader(token).alg ?? "");
937
+ } catch {
938
+ return "";
939
+ }
940
+ }
941
+ function toBase64Url(bytes) {
942
+ let binary = "";
943
+ for (const byte of bytes) binary += String.fromCharCode(byte);
944
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
945
+ }
946
+
947
+ // src/authorization.ts
948
+ async function createAuthioPkce(method = "S256") {
949
+ if (method !== "S256" && method !== "plain") throw new TypeError(`The PKCE method '${String(method)}' is not S256 or plain.`);
950
+ const codeVerifier = randomValue(32);
951
+ const codeChallenge = method === "plain" ? codeVerifier : toBase64Url2(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(codeVerifier))));
952
+ return { codeVerifier, codeChallenge, codeChallengeMethod: method };
953
+ }
954
+ function randomValue(bytes = 32) {
955
+ return toBase64Url2(crypto.getRandomValues(new Uint8Array(bytes)));
956
+ }
957
+ async function buildAuthorizationUrl(endpoint, clientId, request) {
958
+ const pkce = await createAuthioPkce(request.codeChallengeMethod ?? "S256");
959
+ const state = request.state?.trim() || randomValue();
960
+ const nonce = request.nonce?.trim() || randomValue();
961
+ if (request.maxAge !== void 0 && (!Number.isInteger(request.maxAge) || request.maxAge < 0))
962
+ throw new RangeError("maxAge must be a non-negative whole number of seconds.");
963
+ const display = request.popup ? "popup" : request.display;
964
+ const responseMode = request.popup ? "web_message" : request.responseMode;
965
+ const url = new URL(endpoint);
966
+ const parameters = {
967
+ ...request.extraParameters,
968
+ response_type: "code",
969
+ client_id: clientId,
970
+ redirect_uri: request.redirectUri,
971
+ scope: toScopeString(request.scopes) ?? "openid",
972
+ state,
973
+ nonce,
974
+ code_challenge: pkce.codeChallenge,
975
+ code_challenge_method: pkce.codeChallengeMethod,
976
+ resource: request.resource,
977
+ organization: request.organization,
978
+ prompt: joined(request.prompt),
979
+ max_age: request.maxAge?.toString(),
980
+ acr_values: joined(request.acrValues),
981
+ login_hint: request.loginHint,
982
+ id_token_hint: request.idTokenHint,
983
+ ui_locales: joined(request.uiLocales),
984
+ display,
985
+ response_mode: responseMode,
986
+ idp_hint: request.idpHint
987
+ };
988
+ for (const [name, value] of Object.entries(parameters)) if (value !== void 0 && value !== "") url.searchParams.set(name, value);
989
+ return {
990
+ url: url.toString(),
991
+ transaction: Object.freeze({
992
+ state,
993
+ nonce,
994
+ codeVerifier: pkce.codeVerifier,
995
+ codeChallengeMethod: pkce.codeChallengeMethod,
996
+ redirectUri: request.redirectUri,
997
+ ...request.resource ? { resource: request.resource } : {},
998
+ ...request.maxAge !== void 0 ? { maxAge: request.maxAge } : {}
999
+ })
1000
+ };
1001
+ }
1002
+ function readCallbackParameters(callback) {
1003
+ if (callback instanceof URLSearchParams) return Object.fromEntries(callback);
1004
+ if (callback instanceof URL) return Object.fromEntries(callback.searchParams);
1005
+ if (typeof callback === "string") {
1006
+ const text = callback.trim();
1007
+ const query = /^[a-z][a-z0-9+.-]*:/i.test(text) ? new URL(text).search : text.slice(text.indexOf("?") + 1);
1008
+ return Object.fromEntries(new URLSearchParams(query));
1009
+ }
1010
+ const parameters = {};
1011
+ for (const [name, value] of Object.entries(callback ?? {})) {
1012
+ const first = Array.isArray(value) ? value[0] : value;
1013
+ if (typeof first === "string") parameters[name] = first;
1014
+ }
1015
+ return parameters;
1016
+ }
1017
+ function parseAuthioBearerChallenge(header) {
1018
+ if (!header || !/^\s*bearer(\s|$)/i.test(header)) return void 0;
1019
+ const values = {};
1020
+ for (const match of header.matchAll(/([a-zA-Z_]+)\s*=\s*"((?:[^"\\]|\\.)*)"|([a-zA-Z_]+)\s*=\s*([^,\s]+)/g)) {
1021
+ const name = (match[1] ?? match[3] ?? "").toLowerCase();
1022
+ values[name] ??= (match[2] ?? match[4] ?? "").replace(/\\(.)/g, "$1");
1023
+ }
1024
+ const maxAge = values.max_age !== void 0 && /^\d+$/.test(values.max_age) ? Number(values.max_age) : void 0;
1025
+ return {
1026
+ error: values.error,
1027
+ errorDescription: values.error_description,
1028
+ scope: values.scope,
1029
+ acrValues: values.acr_values,
1030
+ maxAge,
1031
+ resourceMetadata: values.resource_metadata
1032
+ };
1033
+ }
1034
+ function joined(value) {
1035
+ if (value === void 0) return void 0;
1036
+ const text = (typeof value === "string" ? [value] : [...value]).map((item) => item.trim()).filter(Boolean).join(" ");
1037
+ return text || void 0;
1038
+ }
1039
+ function toBase64Url2(bytes) {
1040
+ let binary = "";
1041
+ for (const byte of bytes) binary += String.fromCharCode(byte);
1042
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1043
+ }
1044
+
1045
+ // src/client.ts
1046
+ function createAuthioClient(options) {
1047
+ const connection = resolveConnection(options);
1048
+ const discovery = createAuthioDiscoveryClient(connection);
1049
+ const authenticator = createClientAuthenticator(options, connection);
1050
+ const refreshSkewMs = seconds(options.tokenRefreshSkew, AuthioDefaults.TokenRefreshSkew, "tokenRefreshSkew");
1051
+ const clockSkew = seconds(options.clockSkew, AuthioDefaults.ClockSkew, "clockSkew") / 1e3;
1052
+ const decrypt = createAuthioTokenDecrypter(resolveEncryptionKey(options), {
1053
+ setting: options.clientEncryptionKey === void 0 && options.clientEncryptionKeyPath ? "clientEncryptionKeyPath" : "clientEncryptionKey",
1054
+ allowLegacyEncryptionAlgorithms: options.allowLegacyEncryptionAlgorithms
1055
+ });
1056
+ const signingKeys = createAuthioSigningKeys(connection, discovery);
1057
+ const tokens = createTokenClient(connection, discovery, authenticator);
1058
+ const clientCredentials = createClientCredentialsCache(connection, authenticator.clientId, tokens, refreshSkewMs);
1059
+ const validationContext = async (signal) => ({
1060
+ keys: await signingKeys(signal),
1061
+ decrypt,
1062
+ issuer: connection.issuer,
1063
+ clientId: authenticator.clientId,
1064
+ algorithms: options.validAlgorithms,
1065
+ clockSkew,
1066
+ now: connection.clock.now()
1067
+ });
1068
+ const validateIdToken = async (idToken, expectations = {}) => {
1069
+ requireValue(idToken, "idToken");
1070
+ return validateAuthioIdToken(idToken, await validationContext(expectations.signal), expectations);
1071
+ };
1072
+ const createAuthorizationUrl = async (request = {}) => {
1073
+ const redirectUri = request.redirectUri ?? options.redirectUri;
1074
+ requireValue(redirectUri, "redirectUri");
1075
+ const document = await discovery.get(request.signal);
1076
+ return buildAuthorizationUrl(requireEndpoint(document, "authorization_endpoint"), authenticator.clientId, { ...request, redirectUri });
1077
+ };
1078
+ return {
1079
+ issuer: connection.issuer,
1080
+ clientId: authenticator.clientId,
1081
+ connection,
1082
+ discovery,
1083
+ tokens,
1084
+ clientCredentials,
1085
+ async introspect(token, callOptions = {}) {
1086
+ requireValue(token, "token");
1087
+ const response = await postForm(
1088
+ connection,
1089
+ discovery,
1090
+ authenticator,
1091
+ "introspection_endpoint",
1092
+ { token, token_type_hint: callOptions.tokenTypeHint },
1093
+ callOptions.signal
1094
+ );
1095
+ return readProtocolJson(response);
1096
+ },
1097
+ async revoke(token, callOptions = {}) {
1098
+ requireValue(token, "token");
1099
+ const response = await postForm(
1100
+ connection,
1101
+ discovery,
1102
+ authenticator,
1103
+ "revocation_endpoint",
1104
+ { token, token_type_hint: callOptions.tokenTypeHint },
1105
+ callOptions.signal
1106
+ );
1107
+ await ensureProtocolSuccess(response);
1108
+ },
1109
+ async userInfo(accessToken, callOptions = {}) {
1110
+ requireValue(accessToken, "accessToken");
1111
+ const document = await discovery.get(callOptions.signal);
1112
+ const response = await send(
1113
+ connection,
1114
+ requireEndpoint(document, "userinfo_endpoint"),
1115
+ { method: "GET", headers: { authorization: `Bearer ${accessToken}`, accept: "application/json, application/jwt" } },
1116
+ callOptions.signal
1117
+ );
1118
+ const mediaType = response.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase();
1119
+ if (response.ok && mediaType === "application/jwt")
1120
+ return readUserInfoJwt((await response.text()).trim(), document, connection, authenticator.clientId, decrypt, signingKeys, {
1121
+ algorithms: options.validAlgorithms,
1122
+ clockSkew,
1123
+ status: response.status,
1124
+ signal: callOptions.signal
1125
+ });
1126
+ if (!response.ok) {
1127
+ const body = await response.text();
1128
+ const challenge = toErrorResponse(body) ? void 0 : readBearerChallenge(response);
1129
+ throw challenge ? new AuthioProtocolError(challenge.error, challenge.description, void 0, response.status) : toProtocolError(body, response.status);
1130
+ }
1131
+ return readProtocolJson(response);
1132
+ },
1133
+ createAuthorizationUrl,
1134
+ async handleAuthorizationCallback(callback, transaction, callOptions = {}) {
1135
+ if (!transaction?.state || !transaction.redirectUri) throw new TypeError("transaction is required: keep what createAuthorizationUrl returned.");
1136
+ const parameters = readCallbackParameters(callback);
1137
+ if (parameters.state !== transaction.state)
1138
+ throw new AuthioCallbackError("state_mismatch", "The callback's state is not the one this sign-in was started with.");
1139
+ const document = await discovery.get(callOptions.signal);
1140
+ if (parameters.iss !== void 0) {
1141
+ if (parameters.iss.replace(/\/+$/, "") !== connection.issuer)
1142
+ throw new AuthioCallbackError("issuer_mismatch", `The callback comes from the issuer '${parameters.iss}', not '${connection.issuer}' (RFC 9207).`);
1143
+ } else if (document.authorization_response_iss_parameter_supported === true) {
1144
+ throw new AuthioCallbackError("missing_issuer", "The callback carries no iss parameter, although Authio sends one (RFC 9207).");
1145
+ }
1146
+ if (parameters.error) throw new AuthioProtocolError(parameters.error, parameters.error_description, parameters.error_code);
1147
+ if (!parameters.code) throw new AuthioCallbackError("missing_code", "The callback carries neither a code nor an error.");
1148
+ const answer = await tokens.authorizationCode({
1149
+ code: parameters.code,
1150
+ redirectUri: transaction.redirectUri,
1151
+ codeVerifier: transaction.codeVerifier,
1152
+ resource: transaction.resource,
1153
+ signal: callOptions.signal
1154
+ });
1155
+ const idToken = answer.id_token ? await validateIdToken(answer.id_token, {
1156
+ nonce: transaction.nonce,
1157
+ accessToken: answer.access_token,
1158
+ maxAge: transaction.maxAge,
1159
+ signal: callOptions.signal
1160
+ }) : void 0;
1161
+ return { tokens: answer, idToken };
1162
+ },
1163
+ validateIdToken,
1164
+ async validateLogoutToken(logoutToken, callOptions = {}) {
1165
+ requireValue(logoutToken, "logoutToken");
1166
+ return validateAuthioLogoutToken(logoutToken, await validationContext(callOptions.signal));
1167
+ },
1168
+ async createEndSessionUrl(request = {}) {
1169
+ const document = await discovery.get(request.signal);
1170
+ const url = new URL(requireEndpoint(document, "end_session_endpoint"));
1171
+ const locales = request.uiLocales === void 0 ? void 0 : (typeof request.uiLocales === "string" ? [request.uiLocales] : [...request.uiLocales]).join(" ");
1172
+ const parameters = {
1173
+ id_token_hint: request.idTokenHint,
1174
+ post_logout_redirect_uri: request.postLogoutRedirectUri ?? options.postLogoutRedirectUri,
1175
+ state: request.state,
1176
+ ui_locales: locales,
1177
+ client_id: request.includeClientId === false ? void 0 : authenticator.clientId
1178
+ };
1179
+ for (const [name, value] of Object.entries(parameters)) if (value) url.searchParams.set(name, value);
1180
+ return url.toString();
1181
+ },
1182
+ async createStepUpAuthorizationUrl(challenge, request = {}) {
1183
+ const parsed = typeof challenge === "string" ? parseAuthioBearerChallenge(challenge) : challenge;
1184
+ if (!parsed || parsed.error !== AuthioOAuthErrors.InsufficientUserAuthentication)
1185
+ throw new TypeError("The challenge is not an insufficient_user_authentication bearer challenge.");
1186
+ return createAuthorizationUrl({
1187
+ ...request,
1188
+ acrValues: request.acrValues ?? parsed.acrValues,
1189
+ maxAge: request.maxAge ?? parsed.maxAge,
1190
+ prompt: request.prompt ?? (parsed.maxAge === 0 ? "login" : void 0)
1191
+ });
1192
+ },
1193
+ clientCredentialsFetch(fetchOptions = {}) {
1194
+ const inner = fetchOptions.fetch ?? connection.fetch;
1195
+ const key = { scopes: fetchOptions.scopes, resource: fetchOptions.resource };
1196
+ return async (input, init) => {
1197
+ const request = new Request(input, init);
1198
+ const body = request.body ? await request.arrayBuffer() : void 0;
1199
+ const attempt = async (token2) => {
1200
+ const headers = new Headers(request.headers);
1201
+ headers.set("authorization", `${token2.tokenType} ${token2.accessToken}`);
1202
+ return inner(request.url, { method: request.method, headers, body, signal: request.signal, redirect: request.redirect });
1203
+ };
1204
+ const token = await clientCredentials.getToken({ ...key, signal: request.signal });
1205
+ const response = await attempt(token);
1206
+ if (response.status !== 401) return response;
1207
+ clientCredentials.invalidate({ ...key, accessToken: token.accessToken });
1208
+ return attempt(await clientCredentials.getToken({ ...key, signal: request.signal }));
1209
+ };
1210
+ }
1211
+ };
1212
+ }
1213
+ function createTokenClient(connection, discovery, authenticator) {
1214
+ const requestToken = async (parameters, signal, authenticate = true) => {
1215
+ const response = await postForm(connection, discovery, authenticate ? authenticator : void 0, "token_endpoint", parameters, signal);
1216
+ const token = await readProtocolJson(response);
1217
+ return redact(token, ["access_token", "refresh_token", "id_token"]);
1218
+ };
1219
+ const pollDevice = async (authorization, options = {}) => {
1220
+ requireValue(authorization?.device_code, "device_code");
1221
+ let wait = (authorization.interval && authorization.interval > 0 ? authorization.interval : AuthioDefaults.DeviceInterval) * 1e3;
1222
+ const deadline = authorization.expires_in && authorization.expires_in > 0 ? connection.clock.now() + authorization.expires_in * 1e3 : void 0;
1223
+ for (; ; ) {
1224
+ await connection.clock.sleep(wait, options.signal);
1225
+ if (deadline !== void 0 && connection.clock.now() >= deadline)
1226
+ throw new AuthioProtocolError(AuthioOAuthErrors.ExpiredToken, "The device code expired before the user approved it.");
1227
+ try {
1228
+ return await requestToken({ grant_type: AuthioGrantTypes.DeviceCode, device_code: authorization.device_code }, options.signal);
1229
+ } catch (error) {
1230
+ if (!(error instanceof AuthioProtocolError)) throw error;
1231
+ const outcome = toDevicePollOutcome(error.error);
1232
+ if (outcome.outcome === "failed") throw error;
1233
+ wait += outcome.intervalIncrease * 1e3;
1234
+ }
1235
+ }
1236
+ };
1237
+ return {
1238
+ clientCredentials: (options = {}) => requestToken(
1239
+ { grant_type: AuthioGrantTypes.ClientCredentials, scope: toScopeString(options.scopes), resource: options.resource },
1240
+ options.signal
1241
+ ),
1242
+ authorizationCode: (options) => {
1243
+ requireValue(options?.code, "code");
1244
+ requireValue(options.redirectUri, "redirectUri");
1245
+ return requestToken(
1246
+ {
1247
+ grant_type: AuthioGrantTypes.AuthorizationCode,
1248
+ code: options.code,
1249
+ redirect_uri: options.redirectUri,
1250
+ code_verifier: options.codeVerifier,
1251
+ resource: options.resource
1252
+ },
1253
+ options.signal
1254
+ );
1255
+ },
1256
+ refresh: (options) => {
1257
+ requireValue(options?.refreshToken, "refreshToken");
1258
+ return requestToken(
1259
+ { grant_type: AuthioGrantTypes.RefreshToken, refresh_token: options.refreshToken, scope: toScopeString(options.scopes) },
1260
+ options.signal
1261
+ );
1262
+ },
1263
+ exchange: (options) => {
1264
+ requireValue(options?.subjectToken, "subjectToken");
1265
+ return requestToken(
1266
+ {
1267
+ grant_type: AuthioGrantTypes.TokenExchange,
1268
+ subject_token: options.subjectToken,
1269
+ subject_token_type: AuthioTokenTypes.AccessTokenUrn,
1270
+ requested_token_type: AuthioTokenTypes.AccessTokenUrn,
1271
+ scope: toScopeString(options.scopes),
1272
+ resource: options.resource
1273
+ },
1274
+ options.signal
1275
+ );
1276
+ },
1277
+ async exchangeForConnection(options) {
1278
+ requireValue(options?.subjectToken, "subjectToken");
1279
+ requireValue(options.provider, "provider");
1280
+ const response = await postForm(
1281
+ connection,
1282
+ discovery,
1283
+ authenticator,
1284
+ "token_endpoint",
1285
+ {
1286
+ grant_type: AuthioGrantTypes.TokenExchange,
1287
+ subject_token: options.subjectToken,
1288
+ subject_token_type: AuthioTokenTypes.AccessTokenUrn,
1289
+ requested_token_type: AuthioTokenTypes.ConnectionAccessTokenUrn,
1290
+ connection_provider: options.provider,
1291
+ connection_id: options.connectionId
1292
+ },
1293
+ options.signal
1294
+ );
1295
+ return toConnectionExchangeResult(await response.text(), response.status, options.provider);
1296
+ },
1297
+ async deviceAuthorization(options = {}) {
1298
+ const response = await postForm(
1299
+ connection,
1300
+ discovery,
1301
+ authenticator,
1302
+ "device_authorization_endpoint",
1303
+ { scope: toScopeString(options.scopes) },
1304
+ options.signal
1305
+ );
1306
+ return redact(await readProtocolJson(response), ["device_code"]);
1307
+ },
1308
+ pollDevice,
1309
+ personalAccessToken: (options) => {
1310
+ requireValue(options?.token, "token");
1311
+ return requestToken(
1312
+ {
1313
+ grant_type: AuthioGrantTypes.PersonalAccessToken,
1314
+ client_secret: options.token,
1315
+ client_id: options.clientId,
1316
+ scope: toScopeString(options.scopes),
1317
+ resource: options.resource
1318
+ },
1319
+ options.signal,
1320
+ false
1321
+ );
1322
+ },
1323
+ password: (options) => {
1324
+ requireValue(options?.username, "username");
1325
+ if (typeof options.password !== "string") throw new TypeError("password is required.");
1326
+ return requestToken(
1327
+ { grant_type: AuthioGrantTypes.Password, username: options.username, password: options.password, scope: toScopeString(options.scopes) },
1328
+ options.signal
1329
+ );
1330
+ }
1331
+ };
1332
+ }
1333
+ function createClientCredentialsCache(connection, clientId, tokens, refreshSkewMs) {
1334
+ const entries = /* @__PURE__ */ new Map();
1335
+ const pending = /* @__PURE__ */ new Map();
1336
+ const keyOf = (scope, resource) => `${connection.issuer}|${clientId}|${(scope ?? "").split(" ").filter(Boolean).sort().join(" ")}|${resource ?? ""}`;
1337
+ return {
1338
+ async getToken(options = {}) {
1339
+ const scope = toScopeString(options.scopes);
1340
+ const key = keyOf(scope, options.resource);
1341
+ const cached = entries.get(key);
1342
+ if (cached && connection.clock.now() < cached.refreshAt) return cached;
1343
+ let request = pending.get(key);
1344
+ if (!request) {
1345
+ request = tokens.clientCredentials({ scopes: scope, resource: options.resource, signal: options.signal }).then((response) => {
1346
+ const token = toClientCredentialsToken(response, connection.clock.now(), refreshSkewMs);
1347
+ entries.set(key, token);
1348
+ return token;
1349
+ }).finally(() => pending.delete(key));
1350
+ pending.set(key, request);
1351
+ }
1352
+ return request;
1353
+ },
1354
+ invalidate(options = {}) {
1355
+ const key = keyOf(toScopeString(options.scopes), options.resource);
1356
+ if (options.accessToken === void 0 || entries.get(key)?.accessToken === options.accessToken) entries.delete(key);
1357
+ }
1358
+ };
1359
+ }
1360
+ function toClientCredentialsToken(response, issuedAt, refreshSkewMs) {
1361
+ const lifetime = (response.expires_in && response.expires_in > 0 ? response.expires_in : AuthioDefaults.AssumedTokenLifetime) * 1e3;
1362
+ const expiresAt = issuedAt + lifetime;
1363
+ const skew = Math.min(refreshSkewMs, lifetime / 2);
1364
+ return redact(
1365
+ {
1366
+ accessToken: response.access_token,
1367
+ tokenType: response.token_type?.trim() ? response.token_type : "Bearer",
1368
+ expiresAt,
1369
+ refreshAt: expiresAt - skew,
1370
+ scope: response.scope
1371
+ },
1372
+ ["accessToken"]
1373
+ );
1374
+ }
1375
+ function toConnectionExchangeResult(body, status, provider) {
1376
+ if (status >= 200 && status < 300) {
1377
+ const token = parseProtocolJson(body, status);
1378
+ if (typeof token.access_token === "string" && token.access_token)
1379
+ return redact(
1380
+ withoutUndefined({
1381
+ status: "granted",
1382
+ accessToken: token.access_token,
1383
+ issuedTokenType: textOf(token.issued_token_type),
1384
+ tokenType: textOf(token.token_type),
1385
+ expiresIn: typeof token.expires_in === "number" ? token.expires_in : void 0,
1386
+ scope: textOf(token.scope),
1387
+ connectionId: textOf(token.connection_id),
1388
+ provider: textOf(token.provider) ?? provider,
1389
+ providerType: textOf(token.provider_type),
1390
+ externalAccount: token.external_account && typeof token.external_account === "object" ? token.external_account : void 0
1391
+ }),
1392
+ ["accessToken"]
1393
+ );
1394
+ }
1395
+ const error = toErrorResponse(body);
1396
+ const errorDescription = error?.error_description?.trim() ? error.error_description : `Authio answered HTTP ${status} to the connected-account token exchange.`;
1397
+ const errorCode = textOf(error?.error_code);
1398
+ switch (error?.error) {
1399
+ case AuthioOAuthErrors.ConnectionRequired:
1400
+ return withoutUndefined({
1401
+ status: "required",
1402
+ connectUri: textOf(error.connect_uri),
1403
+ provider: textOf(error.connection_provider) ?? provider,
1404
+ errorCode,
1405
+ errorDescription
1406
+ });
1407
+ case AuthioOAuthErrors.ConnectionSelectionRequired:
1408
+ return withoutUndefined({
1409
+ status: "selection_required",
1410
+ provider: textOf(error.connection_provider) ?? provider,
1411
+ connections: (Array.isArray(error.connections) ? error.connections : []).map(toConnectionChoice),
1412
+ errorCode,
1413
+ errorDescription
1414
+ });
1415
+ default:
1416
+ return withoutUndefined({
1417
+ status: "refused",
1418
+ error: error?.error ?? `http_${status}`,
1419
+ errorCode,
1420
+ errorDescription,
1421
+ connectUri: textOf(error?.connect_uri)
1422
+ });
1423
+ }
1424
+ }
1425
+ function toDevicePollOutcome(error) {
1426
+ if (error === AuthioOAuthErrors.AuthorizationPending) return { outcome: "keep_polling", intervalIncrease: 0 };
1427
+ if (error === AuthioOAuthErrors.SlowDown) return { outcome: "keep_polling", intervalIncrease: AuthioDefaults.DeviceSlowDownIncrement };
1428
+ return { outcome: "failed", error };
1429
+ }
1430
+ function toConnectionChoice(value) {
1431
+ const record = value && typeof value === "object" ? value : {};
1432
+ return withoutUndefined({ id: textOf(record.id) ?? "", label: textOf(record.label), externalLogin: textOf(record.external_login) });
1433
+ }
1434
+ function textOf(value) {
1435
+ return typeof value === "string" && value ? value : void 0;
1436
+ }
1437
+ function withoutUndefined(value) {
1438
+ for (const key of Object.keys(value)) if (value[key] === void 0) delete value[key];
1439
+ return value;
1440
+ }
1441
+ function resolveEncryptionKey(options) {
1442
+ if (options.clientEncryptionKey !== void 0) return options.clientEncryptionKey;
1443
+ if (options.clientEncryptionKeyPath) return encryptionKeyFromFile(options.clientEncryptionKeyPath);
1444
+ return void 0;
1445
+ }
1446
+ async function postForm(connection, discovery, authenticator, endpoint, parameters, signal) {
1447
+ const document = await discovery.get(signal);
1448
+ const url = requireEndpoint(document, endpoint);
1449
+ const form = formBody(parameters);
1450
+ const headers = new Headers({ accept: "application/json", "content-type": "application/x-www-form-urlencoded" });
1451
+ await authenticator?.apply(form, headers);
1452
+ return send(connection, url, { method: "POST", headers, body: form.toString() }, signal);
1453
+ }
1454
+ async function readUserInfoJwt(jwt, document, connection, clientId, decrypt, signingKeys, settings) {
1455
+ try {
1456
+ const signed = isAuthioTokenEncrypted(jwt) ? await decrypt(jwt) : jwt;
1457
+ const { payload } = await verifyAuthioJwt(signed, await signingKeys(settings.signal), {
1458
+ issuer: document.issuer.replace(/\/+$/, ""),
1459
+ audiences: [clientId],
1460
+ algorithms: settings.algorithms,
1461
+ clockSkew: settings.clockSkew,
1462
+ now: connection.clock.now(),
1463
+ requiredClaims: ["sub"]
1464
+ });
1465
+ return payload;
1466
+ } catch (error) {
1467
+ throw new AuthioProtocolError(
1468
+ "invalid_userinfo",
1469
+ `The userinfo JWT could not be verified: ${error?.message ?? "unknown reason."}`,
1470
+ void 0,
1471
+ settings.status,
1472
+ { cause: error }
1473
+ );
1474
+ }
1475
+ }
1476
+ function readBearerChallenge(response) {
1477
+ const header = response.headers.get("www-authenticate");
1478
+ if (!header || !/^bearer\s/i.test(header.trim())) return void 0;
1479
+ let error = "";
1480
+ let description;
1481
+ for (const match of header.matchAll(/([a-zA-Z_]+)\s*=\s*"((?:[^"\\]|\\.)*)"|([a-zA-Z_]+)\s*=\s*([^,\s]+)/g)) {
1482
+ const name = match[1] ?? match[3];
1483
+ const value = (match[2] ?? match[4] ?? "").replace(/\\(.)/g, "$1");
1484
+ if (name === "error") error = value;
1485
+ else if (name === "error_description") description = value;
1486
+ }
1487
+ return error ? { error, description } : void 0;
1488
+ }
1489
+ function requireValue(value, name) {
1490
+ if (typeof value !== "string" || !value.trim()) throw new TypeError(`${name} is required.`);
1491
+ }
1492
+
1493
+ // src/admin.ts
1494
+ function parseAuthioAdminEnvelope(status, body, headers) {
1495
+ const parsed = parseJson(body);
1496
+ if (status >= 200 && status < 300) {
1497
+ if (!parsed) return { ok: true };
1498
+ return withoutUndefined2({ ok: true, data: parsed.data });
1499
+ }
1500
+ const notifications = Array.isArray(parsed?.notifications) ? parsed.notifications.map(toNotification).filter((item) => item.message) : [];
1501
+ return {
1502
+ ok: false,
1503
+ error: new AuthioAdminError({
1504
+ status,
1505
+ errorCode: textOf2(parsed?.errorCode),
1506
+ notifications,
1507
+ requestId: textOf2(parsed?.requestId) ?? headerOf(headers, AuthioDefaults.RequestIdHeader),
1508
+ retryAfterSeconds: retryAfterOf(headerOf(headers, "Retry-After"))
1509
+ })
1510
+ };
1511
+ }
1512
+ async function readAuthioAdminResponse(response) {
1513
+ const envelope = parseAuthioAdminEnvelope(response.status, response.status === 204 ? null : await response.text(), response.headers);
1514
+ if (!envelope.ok) throw envelope.error;
1515
+ return envelope;
1516
+ }
1517
+ function createAuthioAccountCredential(options) {
1518
+ const value = options.credential?.trim();
1519
+ if (!value) throw new AuthioConfigurationError("The account credential is required.");
1520
+ const kind = value.startsWith(AuthioDefaults.ApiKeyPrefix) ? "api_key" : value.startsWith(AuthioDefaults.ClientSecretPrefix) ? "client_secret" : value.startsWith(AuthioDefaults.PersonalAccessTokenPrefix) ? "personal_access_token" : void 0;
1521
+ if (!kind)
1522
+ throw new AuthioConfigurationError(
1523
+ `The account credential must start with ${AuthioDefaults.ApiKeyPrefix}, ${AuthioDefaults.ClientSecretPrefix} or ${AuthioDefaults.PersonalAccessTokenPrefix}.`
1524
+ );
1525
+ const exchanged = kind === "client_secret" || kind === "personal_access_token" && options.exchangePersonalAccessToken === true;
1526
+ const clock = options.clock ?? systemClock;
1527
+ const inner = options.fetch ?? globalThis.fetch;
1528
+ const scope = toScopeString(options.scopes);
1529
+ const refreshSkewMs = seconds(options.tokenRefreshSkew, AuthioDefaults.TokenRefreshSkew, "tokenRefreshSkew");
1530
+ let client;
1531
+ let cached;
1532
+ let pending;
1533
+ if (exchanged) {
1534
+ if (kind === "client_secret" && !options.credentialId?.trim())
1535
+ throw new AuthioConfigurationError("A cs_ client secret is exchanged with its credential id: set credentialId.");
1536
+ client = createAuthioClient({
1537
+ ...options,
1538
+ realm: AuthioDefaults.PlatformRealm,
1539
+ clientId: kind === "client_secret" ? toCredentialClientId(options.credentialId) : AuthioDefaults.PlatformClientId,
1540
+ clientSecret: kind === "client_secret" ? value : void 0,
1541
+ clientAuthentication: kind === "client_secret" ? "client_secret_post" : "none"
1542
+ });
1543
+ }
1544
+ const exchange = async (signal) => {
1545
+ if (cached && clock.now() < cached.refreshAt) return cached;
1546
+ pending ??= (async () => {
1547
+ const issuedAt = clock.now();
1548
+ const answer = kind === "client_secret" ? await client.tokens.clientCredentials({ scopes: scope, signal }) : await client.tokens.personalAccessToken({ token: value, scopes: scope, signal });
1549
+ cached = toClientCredentialsToken(answer, issuedAt, refreshSkewMs);
1550
+ return cached;
1551
+ })().finally(() => {
1552
+ pending = void 0;
1553
+ });
1554
+ return pending;
1555
+ };
1556
+ const headers = async (signal) => {
1557
+ if (exchanged) {
1558
+ const token = await exchange(signal);
1559
+ return { authorization: `${token.tokenType} ${token.accessToken}` };
1560
+ }
1561
+ return options.presentAs === "x-api-key" ? { [AuthioDefaults.ApiKeyHeader]: value } : { authorization: `Bearer ${value}` };
1562
+ };
1563
+ const authenticatedFetch = async (input, init) => {
1564
+ const request = new Request(input, init);
1565
+ const body = request.body ? await request.arrayBuffer() : void 0;
1566
+ const idempotent = ["GET", "HEAD", "PUT", "DELETE"].includes(request.method.toUpperCase());
1567
+ const attempt = async () => {
1568
+ const merged = new Headers(request.headers);
1569
+ for (const [name, header] of Object.entries(await headers(request.signal))) merged.set(name, header);
1570
+ return inner(request.url, { method: request.method, headers: merged, body, signal: request.signal, redirect: request.redirect });
1571
+ };
1572
+ let response = await attempt();
1573
+ if (response.status === 401 && exchanged) {
1574
+ cached = void 0;
1575
+ response = await attempt();
1576
+ }
1577
+ if ((response.status === 429 || response.status === 503) && idempotent) {
1578
+ const wait = retryAfterOf(response.headers.get("retry-after") ?? void 0);
1579
+ if (wait !== void 0) {
1580
+ await clock.sleep(wait * 1e3, request.signal);
1581
+ response = await attempt();
1582
+ }
1583
+ }
1584
+ return response;
1585
+ };
1586
+ return {
1587
+ kind,
1588
+ exchanged,
1589
+ headers,
1590
+ invalidate() {
1591
+ cached = void 0;
1592
+ },
1593
+ fetch: authenticatedFetch
1594
+ };
1595
+ }
1596
+ function toCredentialClientId(credentialId) {
1597
+ const id = credentialId.trim();
1598
+ return id.startsWith(AuthioDefaults.CredentialClientIdPrefix) ? id : `${AuthioDefaults.CredentialClientIdPrefix}${id.replace(/[{}-]/g, "").toLowerCase()}`;
1599
+ }
1600
+ function parseJson(body) {
1601
+ if (!body?.trim()) return void 0;
1602
+ try {
1603
+ return asRecord(JSON.parse(body));
1604
+ } catch {
1605
+ return void 0;
1606
+ }
1607
+ }
1608
+ function asRecord(value) {
1609
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
1610
+ }
1611
+ function toNotification(value) {
1612
+ const record = asRecord(value) ?? {};
1613
+ return withoutUndefined2({ message: textOf2(record.message) ?? "", field: textOf2(record.field), code: textOf2(record.code) });
1614
+ }
1615
+ function headerOf(headers, name) {
1616
+ if (!headers) return void 0;
1617
+ if (typeof headers.get === "function") return headers.get(name) ?? void 0;
1618
+ const wanted = name.toLowerCase();
1619
+ for (const [key, value] of Object.entries(headers)) if (key.toLowerCase() === wanted) return value;
1620
+ return void 0;
1621
+ }
1622
+ function retryAfterOf(value) {
1623
+ if (!value?.trim()) return void 0;
1624
+ if (/^\d+$/.test(value.trim())) return Number(value.trim());
1625
+ const date = Date.parse(value);
1626
+ return Number.isNaN(date) ? void 0 : Math.max(0, Math.ceil((date - Date.now()) / 1e3));
1627
+ }
1628
+ function textOf2(value) {
1629
+ return typeof value === "string" && value ? value : void 0;
1630
+ }
1631
+ function withoutUndefined2(value) {
1632
+ for (const key of Object.keys(value)) if (value[key] === void 0) delete value[key];
1633
+ return value;
1634
+ }
1635
+
1636
+ // src/resource-access.ts
1637
+ var claimSources = new Map(Object.entries(AuthioResourceAccessClaims).map(([name, source]) => [name.toLowerCase(), source]));
1638
+ var AuthioResourceAccess = class _AuthioResourceAccess {
1639
+ /** No role at all. */
1640
+ static empty = new _AuthioResourceAccess([]);
1641
+ /** Every role read, in claim order, one entry per source it came from. */
1642
+ entries;
1643
+ /** Every distinct role name. */
1644
+ roles;
1645
+ /** Resource → every method any role allows on it. */
1646
+ permissions;
1647
+ lookup;
1648
+ constructor(entries) {
1649
+ this.entries = Object.freeze([...entries]);
1650
+ this.roles = Object.freeze(distinctIgnoringCase(this.entries.map((entry) => entry.name)));
1651
+ this.lookup = /* @__PURE__ */ new Map();
1652
+ const permissions = {};
1653
+ const spelling = /* @__PURE__ */ new Map();
1654
+ for (const entry of this.entries) {
1655
+ for (const [resource, methods] of Object.entries(entry.permissions)) {
1656
+ const resourceKey = resource.toLowerCase();
1657
+ let allowed = this.lookup.get(resourceKey);
1658
+ if (!allowed) {
1659
+ allowed = /* @__PURE__ */ new Map();
1660
+ this.lookup.set(resourceKey, allowed);
1661
+ spelling.set(resourceKey, resource);
1662
+ permissions[resource] = [];
1663
+ }
1664
+ for (const method of methods) {
1665
+ const methodKey = method.toLowerCase();
1666
+ if (allowed.has(methodKey)) continue;
1667
+ allowed.set(methodKey, method);
1668
+ permissions[spelling.get(resourceKey)].push(method);
1669
+ }
1670
+ }
1671
+ }
1672
+ this.permissions = Object.freeze(Object.fromEntries(Object.entries(permissions).map(([name, methods]) => [name, Object.freeze(methods)])));
1673
+ }
1674
+ /** Whether no role was read. */
1675
+ get isEmpty() {
1676
+ return this.entries.length === 0;
1677
+ }
1678
+ /**
1679
+ * Whether the token holds the role, from any source or only from the given ones.
1680
+ */
1681
+ hasRole(role, sources) {
1682
+ if (!role) return false;
1683
+ const wanted = role.toLowerCase();
1684
+ return this.entries.some((entry) => inSources(entry, sources) && entry.name.toLowerCase() === wanted);
1685
+ }
1686
+ /**
1687
+ * Authio's grant rule: whether any role (from any source, or only from the given ones) allows `method` on
1688
+ * `resource`.
1689
+ */
1690
+ grants(resource, method, sources) {
1691
+ if (!resource || !method) return false;
1692
+ if (!sources || sources.length === 0) return this.lookup.get(resource.toLowerCase())?.has(method.toLowerCase()) ?? false;
1693
+ return this.entries.some((entry) => inSources(entry, sources) && entryGrants(entry, resource, method));
1694
+ }
1695
+ /**
1696
+ * The roles that came from one source.
1697
+ */
1698
+ fromSource(source) {
1699
+ return this.entries.filter((entry) => entry.source === source);
1700
+ }
1701
+ };
1702
+ function resourceAccessSourceOf(claimType) {
1703
+ return claimType ? claimSources.get(claimType.toLowerCase()) : void 0;
1704
+ }
1705
+ function parseResourceAccess(claims) {
1706
+ const pairs = isIterable(claims) ? claims : Object.entries(claims);
1707
+ const roles = [];
1708
+ for (const [type, value] of pairs) {
1709
+ const source = resourceAccessSourceOf(type);
1710
+ if (source) roles.push(...parseResourceAccessClaim(source, value));
1711
+ }
1712
+ return roles.length === 0 ? AuthioResourceAccess.empty : new AuthioResourceAccess(roles);
1713
+ }
1714
+ function parseResourceAccessClaim(source, value) {
1715
+ const root = typeof value === "string" ? parseJson2(value) : value;
1716
+ if (!isPlainObject(root)) return [];
1717
+ const roles = [];
1718
+ for (const [name, rolePermissions] of Object.entries(root)) {
1719
+ const permissions = readPermissions(rolePermissions);
1720
+ if (!permissions) return [];
1721
+ roles.push(Object.freeze({ source, name, permissions }));
1722
+ }
1723
+ return roles;
1724
+ }
1725
+ function readPermissions(value) {
1726
+ if (value === null) return Object.freeze({});
1727
+ if (!isPlainObject(value)) return void 0;
1728
+ const read = {};
1729
+ const seen = /* @__PURE__ */ new Set();
1730
+ for (const [resource, methods] of Object.entries(value)) {
1731
+ const key = resource.toLowerCase();
1732
+ if (methods === null) {
1733
+ if (!seen.has(key)) {
1734
+ seen.add(key);
1735
+ read[resource] = Object.freeze([]);
1736
+ }
1737
+ continue;
1738
+ }
1739
+ if (!Array.isArray(methods)) return void 0;
1740
+ const allowed = [];
1741
+ for (const method of methods) {
1742
+ if (typeof method === "string") allowed.push(method);
1743
+ else if (method !== null) return void 0;
1744
+ }
1745
+ if (!seen.has(key)) {
1746
+ seen.add(key);
1747
+ read[resource] = Object.freeze(allowed);
1748
+ }
1749
+ }
1750
+ return Object.freeze(read);
1751
+ }
1752
+ function entryGrants(entry, resource, method) {
1753
+ const resourceKey = resource.toLowerCase();
1754
+ const methodKey = method.toLowerCase();
1755
+ for (const [name, methods] of Object.entries(entry.permissions)) {
1756
+ if (name.toLowerCase() === resourceKey) return methods.some((allowed) => allowed.toLowerCase() === methodKey);
1757
+ }
1758
+ return false;
1759
+ }
1760
+ function inSources(entry, sources) {
1761
+ return !sources || sources.length === 0 || sources.includes(entry.source);
1762
+ }
1763
+ function distinctIgnoringCase(values) {
1764
+ const seen = /* @__PURE__ */ new Set();
1765
+ return values.filter((value) => {
1766
+ const key = value.toLowerCase();
1767
+ if (seen.has(key)) return false;
1768
+ seen.add(key);
1769
+ return true;
1770
+ });
1771
+ }
1772
+ function parseJson2(text) {
1773
+ if (!text.trim()) return void 0;
1774
+ try {
1775
+ return JSON.parse(text);
1776
+ } catch {
1777
+ return void 0;
1778
+ }
1779
+ }
1780
+ function isPlainObject(value) {
1781
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1782
+ }
1783
+ function isIterable(value) {
1784
+ return typeof value === "object" && value !== null && typeof value[Symbol.iterator] === "function";
1785
+ }
1786
+
1787
+ // src/webhooks.ts
1788
+ var encoder = new TextEncoder();
1789
+ async function verifyWebhookSignature(signatureHeader, rawBody, secret, options = {}) {
1790
+ if (!secret) throw new TypeError("secret is required.");
1791
+ if (!signatureHeader?.trim())
1792
+ return { valid: false, failure: "missing_signature", reason: "The X-Authio-Signature header is missing." };
1793
+ const parsed = parseSignatureHeader(signatureHeader);
1794
+ if (!parsed)
1795
+ return {
1796
+ valid: false,
1797
+ failure: "malformed_signature",
1798
+ reason: "The X-Authio-Signature header is not of the form t={unixSeconds},v1={hex}."
1799
+ };
1800
+ const signedAt = new Date(parsed.timestamp * 1e3);
1801
+ const expected = await computeSignature(parsed.timestamp, rawBody, secret);
1802
+ let matched = false;
1803
+ for (const signature of parsed.signatures) matched = constantTimeEquals(expected, signature) || matched;
1804
+ if (!matched)
1805
+ return {
1806
+ valid: false,
1807
+ failure: "signature_mismatch",
1808
+ reason: "No v1 signature matches the body: the secret is wrong or the body was altered.",
1809
+ timestamp: signedAt
1810
+ };
1811
+ const tolerance = (options.tolerance ?? AuthioDefaults.WebhookTolerance) * 1e3;
1812
+ const now = options.now ?? Date.now();
1813
+ if (Math.abs(now - signedAt.getTime()) > tolerance)
1814
+ return {
1815
+ valid: false,
1816
+ failure: "timestamp_outside_tolerance",
1817
+ reason: `The signature was made at ${signedAt.toISOString()}, more than ${tolerance / 1e3} seconds from now.`,
1818
+ timestamp: signedAt
1819
+ };
1820
+ return { valid: true, timestamp: signedAt };
1821
+ }
1822
+ async function computeWebhookSignatureHeader(rawBody, secret, timestamp) {
1823
+ if (!secret) throw new TypeError("secret is required.");
1824
+ const seconds2 = Math.floor((timestamp instanceof Date ? timestamp.getTime() : timestamp) / 1e3);
1825
+ const signature = await computeSignature(seconds2, rawBody, secret);
1826
+ return `t=${seconds2},v1=${[...signature].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
1827
+ }
1828
+ function parseWebhookEvent(rawBody) {
1829
+ const text = typeof rawBody === "string" ? rawBody : new TextDecoder().decode(rawBody);
1830
+ const value = JSON.parse(text);
1831
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new SyntaxError("The webhook body is empty.");
1832
+ if (typeof value.type !== "string" || !value.type) throw new SyntaxError("The webhook body has no type.");
1833
+ return value;
1834
+ }
1835
+ function tryParseWebhookEvent(rawBody) {
1836
+ if (rawBody === null || rawBody === void 0) return void 0;
1837
+ try {
1838
+ return parseWebhookEvent(rawBody);
1839
+ } catch {
1840
+ return void 0;
1841
+ }
1842
+ }
1843
+ function isWebhookEventOfType(event, ...types) {
1844
+ return types.includes(event.type);
1845
+ }
1846
+ var asUserEvent = (event) => dataWhen(event, AuthioWebhookEventTypes.UserCreated, AuthioWebhookEventTypes.UserUpdated, AuthioWebhookEventTypes.UserDeleted);
1847
+ var asLoginEvent = (event) => dataWhen(event, AuthioWebhookEventTypes.LoginSucceeded, AuthioWebhookEventTypes.LoginFailed);
1848
+ var asUserLockedOutEvent = (event) => dataWhen(event, AuthioWebhookEventTypes.UserLockedOut);
1849
+ var asTokenRevokedEvent = (event) => dataWhen(event, AuthioWebhookEventTypes.TokenRevoked);
1850
+ var asKeysRotatedEvent = (event) => dataWhen(event, AuthioWebhookEventTypes.KeysRotated);
1851
+ var asTokenReuseDetectedEvent = (event) => dataWhen(event, AuthioWebhookEventTypes.TokenReuseDetected);
1852
+ var asAdminResourceEvent = (event) => dataWhen(
1853
+ event,
1854
+ AuthioWebhookEventTypes.AdminResourceCreated,
1855
+ AuthioWebhookEventTypes.AdminResourceUpdated,
1856
+ AuthioWebhookEventTypes.AdminResourceDeleted
1857
+ );
1858
+ var asUserSuspensionEvent = (event) => dataWhen(event, AuthioWebhookEventTypes.UserSuspended, AuthioWebhookEventTypes.UserSuspensionEnded);
1859
+ var asAttackDetectedEvent = (event) => dataWhen(event, AuthioWebhookEventTypes.AttackDetected);
1860
+ var asCredentialCompromisedEvent = (event) => dataWhen(event, AuthioWebhookEventTypes.CredentialCompromised);
1861
+ var asPingEvent = (event) => dataWhen(event, AuthioWebhookEventTypes.Ping);
1862
+ var knownEventTypes = new Set(Object.values(AuthioWebhookEventTypes));
1863
+ function isKnownWebhookEventType(type) {
1864
+ return knownEventTypes.has(type);
1865
+ }
1866
+ function readWebhookEventData(event) {
1867
+ return isKnownWebhookEventType(event.type) ? dataWhen(event, event.type) : void 0;
1868
+ }
1869
+ function toCamelCasedMembers(value) {
1870
+ const result = {};
1871
+ for (const [name, member] of Object.entries(value)) result[name.replace(/_([a-z0-9])/g, (_, letter) => letter.toUpperCase())] = member;
1872
+ return result;
1873
+ }
1874
+ function dataWhen(event, ...types) {
1875
+ return types.includes(event.type) && event.data !== null && typeof event.data === "object" && !Array.isArray(event.data) ? toCamelCasedMembers(event.data) : void 0;
1876
+ }
1877
+ function parseSignatureHeader(header) {
1878
+ let timestamp = 0;
1879
+ let timestamps = 0;
1880
+ const signatures = [];
1881
+ for (const raw of header.split(",")) {
1882
+ const part = raw.trim();
1883
+ if (!part) continue;
1884
+ const separator = part.indexOf("=");
1885
+ if (separator <= 0) return void 0;
1886
+ const key = part.slice(0, separator);
1887
+ const value = part.slice(separator + 1);
1888
+ if (key === "t") {
1889
+ timestamps++;
1890
+ if (!/^\d+$/.test(value)) return void 0;
1891
+ timestamp = Number(value);
1892
+ } else if (key === "v1" && value.length === 64 && /^[0-9a-fA-F]+$/.test(value)) {
1893
+ signatures.push(Uint8Array.from(value.match(/../g).map((pair) => parseInt(pair, 16))));
1894
+ }
1895
+ }
1896
+ return timestamps === 1 && signatures.length > 0 && Number.isSafeInteger(timestamp) ? { timestamp, signatures } : void 0;
1897
+ }
1898
+ async function computeSignature(timestamp, rawBody, secret) {
1899
+ const prefix = encoder.encode(`${timestamp}.`);
1900
+ const body = typeof rawBody === "string" ? encoder.encode(rawBody) : rawBody;
1901
+ const message = new Uint8Array(prefix.length + body.length);
1902
+ message.set(prefix, 0);
1903
+ message.set(body, prefix.length);
1904
+ const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
1905
+ return new Uint8Array(await crypto.subtle.sign("HMAC", key, message));
1906
+ }
1907
+ function constantTimeEquals(left, right) {
1908
+ if (left.length !== right.length) return false;
1909
+ let difference = 0;
1910
+ for (let index = 0; index < left.length; index++) difference |= left[index] ^ right[index];
1911
+ return difference === 0;
1912
+ }
1913
+
1914
+ export { AuthioAdminError, AuthioAsymmetricAlgorithms, AuthioCallbackError, AuthioCallerKinds, AuthioClaimTypes, AuthioClientAssertionAlgorithms, AuthioConfigurationError, AuthioContentEncryptionAlgorithms, AuthioDefaults, AuthioDiscoveryError, AuthioError, AuthioGrantTypes, AuthioKeyManagementAlgorithms, AuthioLegacyKeyManagementAlgorithms, AuthioOAuthErrors, AuthioPrompts, AuthioProtocolError, AuthioResourceAccess, AuthioResourceAccessClaims, AuthioScopes, 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, readWebhookEventData, resolveConnection, resolveIssuer, resourceAccessSourceOf, seconds, systemClock, toBasicAuthorization, toCamelCasedMembers, toClientCredentialsToken, toConnectionExchangeResult, toCredentialClientId, toDevicePollOutcome, toScopeString, toTokenValidationError, tryParseWebhookEvent, verifyAuthioJwt, verifyWebhookSignature };
1915
+ //# sourceMappingURL=index.js.map
1916
+ //# sourceMappingURL=index.js.map