@hyper.authio/client 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +120 -2
- package/dist/index.cjs +1289 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +995 -0
- package/dist/index.d.ts +995 -0
- package/dist/index.js +1240 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -4
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1289 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var jose = require('jose');
|
|
4
|
+
|
|
5
|
+
// src/constants.ts
|
|
6
|
+
var AuthioClaimTypes = {
|
|
7
|
+
Subject: "sub",
|
|
8
|
+
JwtId: "jti",
|
|
9
|
+
Issuer: "iss",
|
|
10
|
+
Audience: "aud",
|
|
11
|
+
Expiration: "exp",
|
|
12
|
+
IssuedAt: "iat",
|
|
13
|
+
NotBefore: "nbf",
|
|
14
|
+
AuthTime: "auth_time",
|
|
15
|
+
TokenType: "typ",
|
|
16
|
+
AuthorizedParty: "azp",
|
|
17
|
+
Scope: "scope",
|
|
18
|
+
SessionId: "sid",
|
|
19
|
+
AuthenticationContextClass: "acr",
|
|
20
|
+
AuthenticationMethods: "amr",
|
|
21
|
+
IdentityProvider: "idp",
|
|
22
|
+
Nonce: "nonce",
|
|
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
|
+
UserResourceAccess: "user_resource_access",
|
|
43
|
+
RealmResourceAccess: "realm_resource_access",
|
|
44
|
+
ClientResourceAccess: "client_resource_access",
|
|
45
|
+
PlanResourceAccess: "plan_resource_access",
|
|
46
|
+
ConsumerResourceAccess: "consumer_resource_access",
|
|
47
|
+
SecretResourceAccess: "secret_resource_access",
|
|
48
|
+
SystemResourceAccess: "system_resource_access"
|
|
49
|
+
};
|
|
50
|
+
var AuthioResourceAccessClaims = Object.freeze({
|
|
51
|
+
[AuthioClaimTypes.UserResourceAccess]: "user",
|
|
52
|
+
[AuthioClaimTypes.RealmResourceAccess]: "realm",
|
|
53
|
+
[AuthioClaimTypes.ClientResourceAccess]: "client",
|
|
54
|
+
[AuthioClaimTypes.PlanResourceAccess]: "plan",
|
|
55
|
+
[AuthioClaimTypes.ConsumerResourceAccess]: "consumer",
|
|
56
|
+
[AuthioClaimTypes.SecretResourceAccess]: "secret",
|
|
57
|
+
[AuthioClaimTypes.SystemResourceAccess]: "system"
|
|
58
|
+
});
|
|
59
|
+
var AuthioScopes = {
|
|
60
|
+
OpenId: "openid",
|
|
61
|
+
Profile: "profile",
|
|
62
|
+
Email: "email",
|
|
63
|
+
Phone: "phone",
|
|
64
|
+
Roles: "roles",
|
|
65
|
+
OfflineAccess: "offline_access"
|
|
66
|
+
};
|
|
67
|
+
var AuthioGrantTypes = {
|
|
68
|
+
AuthorizationCode: "authorization_code",
|
|
69
|
+
RefreshToken: "refresh_token",
|
|
70
|
+
ClientCredentials: "client_credentials",
|
|
71
|
+
Password: "password",
|
|
72
|
+
DeviceCode: "urn:ietf:params:oauth:grant-type:device_code",
|
|
73
|
+
TokenExchange: "urn:ietf:params:oauth:grant-type:token-exchange",
|
|
74
|
+
PersonalAccessToken: "personal_access_token"
|
|
75
|
+
};
|
|
76
|
+
var AuthioTokenTypes = {
|
|
77
|
+
AccessTokenUrn: "urn:ietf:params:oauth:token-type:access_token",
|
|
78
|
+
ConnectionAccessTokenUrn: "urn:authio:params:oauth:token-type:connection-access-token",
|
|
79
|
+
AccessToken: "access_token",
|
|
80
|
+
RefreshToken: "refresh_token"
|
|
81
|
+
};
|
|
82
|
+
var AuthioOAuthErrors = {
|
|
83
|
+
InvalidRequest: "invalid_request",
|
|
84
|
+
InvalidClient: "invalid_client",
|
|
85
|
+
InvalidGrant: "invalid_grant",
|
|
86
|
+
UnauthorizedClient: "unauthorized_client",
|
|
87
|
+
UnsupportedGrantType: "unsupported_grant_type",
|
|
88
|
+
InvalidScope: "invalid_scope",
|
|
89
|
+
InvalidTarget: "invalid_target",
|
|
90
|
+
AccessDenied: "access_denied",
|
|
91
|
+
InvalidToken: "invalid_token",
|
|
92
|
+
AuthorizationPending: "authorization_pending",
|
|
93
|
+
SlowDown: "slow_down",
|
|
94
|
+
ExpiredToken: "expired_token",
|
|
95
|
+
ConnectionRequired: "connection_required",
|
|
96
|
+
ConnectionSelectionRequired: "connection_selection_required",
|
|
97
|
+
ConnectionPolicyViolation: "connection_policy_violation",
|
|
98
|
+
ServerError: "server_error",
|
|
99
|
+
TemporarilyUnavailable: "temporarily_unavailable"
|
|
100
|
+
};
|
|
101
|
+
var AuthioWebhookEventTypes = {
|
|
102
|
+
UserCreated: "user_created",
|
|
103
|
+
UserUpdated: "user_updated",
|
|
104
|
+
UserDeleted: "user_deleted",
|
|
105
|
+
LoginSucceeded: "login_succeeded",
|
|
106
|
+
LoginFailed: "login_failed",
|
|
107
|
+
UserLockedOut: "user_locked_out",
|
|
108
|
+
TokenRevoked: "token_revoked",
|
|
109
|
+
KeysRotated: "keys_rotated",
|
|
110
|
+
TokenReuseDetected: "token_reuse_detected",
|
|
111
|
+
AdminResourceCreated: "admin_resource_created",
|
|
112
|
+
AdminResourceUpdated: "admin_resource_updated",
|
|
113
|
+
AdminResourceDeleted: "admin_resource_deleted",
|
|
114
|
+
UserSuspended: "user_suspended",
|
|
115
|
+
UserSuspensionEnded: "user_suspension_ended",
|
|
116
|
+
Ping: "ping"
|
|
117
|
+
};
|
|
118
|
+
var AuthioWebhookHeaders = {
|
|
119
|
+
Signature: "X-Authio-Signature",
|
|
120
|
+
Event: "X-Authio-Event",
|
|
121
|
+
EventId: "X-Authio-Event-Id"
|
|
122
|
+
};
|
|
123
|
+
var AuthioAsymmetricAlgorithms = [
|
|
124
|
+
"RS256",
|
|
125
|
+
"RS384",
|
|
126
|
+
"RS512",
|
|
127
|
+
"PS256",
|
|
128
|
+
"PS384",
|
|
129
|
+
"PS512",
|
|
130
|
+
"ES256",
|
|
131
|
+
"ES384",
|
|
132
|
+
"ES512"
|
|
133
|
+
];
|
|
134
|
+
var AuthioDefaults = {
|
|
135
|
+
DiscoveryPath: "/.well-known/openid-configuration",
|
|
136
|
+
RedactedValue: "[redacted]",
|
|
137
|
+
PlatformRealm: "authio",
|
|
138
|
+
PlatformClientId: "authio",
|
|
139
|
+
ApiKeyPrefix: "ak_",
|
|
140
|
+
ClientSecretPrefix: "cs_",
|
|
141
|
+
PersonalAccessTokenPrefix: "pat_",
|
|
142
|
+
/** Seconds. */
|
|
143
|
+
ClientAssertionLifetime: 120,
|
|
144
|
+
/** Seconds. */
|
|
145
|
+
MaxClientAssertionLifetime: 3600,
|
|
146
|
+
/** Seconds. */
|
|
147
|
+
DeviceInterval: 5,
|
|
148
|
+
/** Seconds. */
|
|
149
|
+
DeviceSlowDownIncrement: 5,
|
|
150
|
+
/** Seconds a token without `expires_in` is assumed to live. */
|
|
151
|
+
AssumedTokenLifetime: 300,
|
|
152
|
+
/** Seconds. */
|
|
153
|
+
DiscoveryCacheDuration: 3600,
|
|
154
|
+
/** Seconds. */
|
|
155
|
+
HttpTimeout: 30,
|
|
156
|
+
/** Seconds. */
|
|
157
|
+
TokenRefreshSkew: 60,
|
|
158
|
+
/** Seconds. */
|
|
159
|
+
WebhookTolerance: 300,
|
|
160
|
+
/** Smallest RSA key Authio verifies a client assertion from, in bits. */
|
|
161
|
+
MinimumRsaKeySize: 2048,
|
|
162
|
+
CredentialHeaders: ["authorization", "proxy-authorization", "x-api-key", "cookie", "set-cookie"]
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// src/errors.ts
|
|
166
|
+
var AuthioError = class extends Error {
|
|
167
|
+
constructor(message, options) {
|
|
168
|
+
super(message, options);
|
|
169
|
+
this.name = new.target.name;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
var AuthioProtocolError = class extends AuthioError {
|
|
173
|
+
/** The OAuth error code. */
|
|
174
|
+
error;
|
|
175
|
+
/** The OAuth error description. */
|
|
176
|
+
errorDescription;
|
|
177
|
+
/** Authio's own error code (`error_code`), more specific than `error`. */
|
|
178
|
+
errorCode;
|
|
179
|
+
/** The HTTP status, when the error came from a response. */
|
|
180
|
+
status;
|
|
181
|
+
constructor(error, errorDescription, errorCode, status, options) {
|
|
182
|
+
super(errorDescription ? `${error}: ${errorDescription}` : error, options);
|
|
183
|
+
this.error = error;
|
|
184
|
+
this.errorDescription = errorDescription;
|
|
185
|
+
this.errorCode = errorCode;
|
|
186
|
+
this.status = status;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
var AuthioDiscoveryError = class extends AuthioError {
|
|
190
|
+
};
|
|
191
|
+
var AuthioConfigurationError = class extends AuthioError {
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// src/options.ts
|
|
195
|
+
var systemClock = {
|
|
196
|
+
now: () => Date.now(),
|
|
197
|
+
sleep: (ms, signal) => new Promise((resolve, reject) => {
|
|
198
|
+
if (signal?.aborted) {
|
|
199
|
+
reject(signal.reason);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const timer = setTimeout(() => {
|
|
203
|
+
signal?.removeEventListener("abort", onAbort);
|
|
204
|
+
resolve();
|
|
205
|
+
}, ms);
|
|
206
|
+
const onAbort = () => {
|
|
207
|
+
clearTimeout(timer);
|
|
208
|
+
reject(signal?.reason);
|
|
209
|
+
};
|
|
210
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
211
|
+
})
|
|
212
|
+
};
|
|
213
|
+
function resolveConnection(options) {
|
|
214
|
+
const requireHttps = options.requireHttps ?? true;
|
|
215
|
+
const issuer = resolveIssuer(options);
|
|
216
|
+
for (const [name, value] of [
|
|
217
|
+
["authority", options.authority],
|
|
218
|
+
["issuer", issuer]
|
|
219
|
+
]) {
|
|
220
|
+
if (value && !isAllowedUrl(value, requireHttps))
|
|
221
|
+
throw new AuthioConfigurationError(
|
|
222
|
+
`The Authio ${name} '${value}' must be an absolute HTTPS URL (http is allowed only for loopback, or with requireHttps: false).`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
226
|
+
if (typeof fetchImplementation !== "function")
|
|
227
|
+
throw new AuthioConfigurationError("No fetch implementation is available: pass options.fetch.");
|
|
228
|
+
return {
|
|
229
|
+
issuer,
|
|
230
|
+
discoveryUrl: issuer + AuthioDefaults.DiscoveryPath,
|
|
231
|
+
requireHttps,
|
|
232
|
+
discoveryCacheDurationMs: seconds(options.discoveryCacheDuration, AuthioDefaults.DiscoveryCacheDuration, "discoveryCacheDuration"),
|
|
233
|
+
httpTimeoutMs: seconds(options.httpTimeout, AuthioDefaults.HttpTimeout, "httpTimeout"),
|
|
234
|
+
fetch: fetchImplementation,
|
|
235
|
+
clock: options.clock ?? systemClock
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function resolveIssuer(options) {
|
|
239
|
+
const issuer = options.issuer?.trim();
|
|
240
|
+
if (issuer) return trimTrailingSlashes(issuer);
|
|
241
|
+
const authority = options.authority?.trim();
|
|
242
|
+
const realm = options.realm?.trim();
|
|
243
|
+
if (!authority || !realm)
|
|
244
|
+
throw new AuthioConfigurationError(
|
|
245
|
+
"The Authio issuer is not set, and authority and realm are not both set to compose it."
|
|
246
|
+
);
|
|
247
|
+
return `${trimTrailingSlashes(authority)}/realms/${escapeDataString(realm)}`;
|
|
248
|
+
}
|
|
249
|
+
function isAllowedUrl(value, requireHttps) {
|
|
250
|
+
let url;
|
|
251
|
+
try {
|
|
252
|
+
url = new URL(value);
|
|
253
|
+
} catch {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
if (url.protocol === "https:") return true;
|
|
257
|
+
return url.protocol === "http:" && (!requireHttps || isLoopback(url.hostname));
|
|
258
|
+
}
|
|
259
|
+
function escapeDataString(value) {
|
|
260
|
+
return encodeURIComponent(value).replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
261
|
+
}
|
|
262
|
+
function seconds(value, fallback, name) {
|
|
263
|
+
const resolved = value ?? fallback;
|
|
264
|
+
if (!Number.isFinite(resolved) || resolved < 0)
|
|
265
|
+
throw new AuthioConfigurationError(`The Authio option ${name} must be a non-negative number of seconds.`);
|
|
266
|
+
return resolved * 1e3;
|
|
267
|
+
}
|
|
268
|
+
function trimTrailingSlashes(value) {
|
|
269
|
+
return value.replace(/\/+$/, "");
|
|
270
|
+
}
|
|
271
|
+
function isLoopback(hostname) {
|
|
272
|
+
const host = hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
273
|
+
return host === "localhost" || host === "::1" || /^127(\.\d{1,3}){3}$/.test(host);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/http.ts
|
|
277
|
+
function send(connection, url, init, signal) {
|
|
278
|
+
const timeout = AbortSignal.timeout(connection.httpTimeoutMs);
|
|
279
|
+
return connection.fetch(url, { ...init, signal: signal ? AbortSignal.any([signal, timeout]) : timeout });
|
|
280
|
+
}
|
|
281
|
+
function formBody(parameters) {
|
|
282
|
+
const form = new URLSearchParams();
|
|
283
|
+
for (const [name, value] of Object.entries(parameters)) {
|
|
284
|
+
if (value !== void 0 && value !== null && value !== "") form.append(name, value);
|
|
285
|
+
}
|
|
286
|
+
return form;
|
|
287
|
+
}
|
|
288
|
+
function toScopeString(scopes) {
|
|
289
|
+
if (scopes === void 0 || scopes === null) return void 0;
|
|
290
|
+
const values = typeof scopes === "string" ? [scopes] : [...scopes];
|
|
291
|
+
const distinct = [...new Set(values.flatMap((scope) => (scope ?? "").split(" ").filter(Boolean)))];
|
|
292
|
+
return distinct.length === 0 ? void 0 : distinct.join(" ");
|
|
293
|
+
}
|
|
294
|
+
async function readProtocolJson(response) {
|
|
295
|
+
const body = await response.text();
|
|
296
|
+
if (!response.ok) throw toProtocolError(body, response.status);
|
|
297
|
+
return parseProtocolJson(body, response.status);
|
|
298
|
+
}
|
|
299
|
+
async function ensureProtocolSuccess(response) {
|
|
300
|
+
if (response.ok) return;
|
|
301
|
+
throw toProtocolError(await response.text(), response.status);
|
|
302
|
+
}
|
|
303
|
+
function parseProtocolJson(body, status) {
|
|
304
|
+
try {
|
|
305
|
+
const value = JSON.parse(body);
|
|
306
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new SyntaxError("The body is not a JSON object.");
|
|
307
|
+
return value;
|
|
308
|
+
} catch (error) {
|
|
309
|
+
throw new AuthioProtocolError(
|
|
310
|
+
"invalid_response",
|
|
311
|
+
`Authio answered with a body that could not be read: ${error.message}`,
|
|
312
|
+
void 0,
|
|
313
|
+
status,
|
|
314
|
+
{ cause: error }
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function toErrorResponse(body) {
|
|
319
|
+
if (!body?.trim()) return void 0;
|
|
320
|
+
try {
|
|
321
|
+
const value = JSON.parse(body);
|
|
322
|
+
return value && typeof value === "object" && typeof value.error === "string" && value.error ? value : void 0;
|
|
323
|
+
} catch {
|
|
324
|
+
return void 0;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function toProtocolError(body, status) {
|
|
328
|
+
const error = toErrorResponse(body);
|
|
329
|
+
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);
|
|
330
|
+
}
|
|
331
|
+
function redact(value, secrets) {
|
|
332
|
+
Object.defineProperty(value, /* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom"), {
|
|
333
|
+
enumerable: false,
|
|
334
|
+
value() {
|
|
335
|
+
const copy = {};
|
|
336
|
+
for (const [name, member] of Object.entries(this))
|
|
337
|
+
copy[name] = secrets.includes(name) && member !== void 0 && member !== null ? AuthioDefaults.RedactedValue : member;
|
|
338
|
+
return copy;
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
return value;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/discovery.ts
|
|
345
|
+
function createAuthioDiscoveryClient(options) {
|
|
346
|
+
const connection = isConnection(options) ? options : resolveConnection(options);
|
|
347
|
+
let cached;
|
|
348
|
+
let pending;
|
|
349
|
+
const fresh = () => cached && connection.clock.now() - cached.fetchedAt < connection.discoveryCacheDurationMs ? cached.document : void 0;
|
|
350
|
+
return {
|
|
351
|
+
connection,
|
|
352
|
+
async get(signal) {
|
|
353
|
+
const current = fresh();
|
|
354
|
+
if (current) return current;
|
|
355
|
+
pending ??= fetchDocument(connection, signal).then((document) => {
|
|
356
|
+
cached = { document, fetchedAt: connection.clock.now() };
|
|
357
|
+
return document;
|
|
358
|
+
}).finally(() => {
|
|
359
|
+
pending = void 0;
|
|
360
|
+
});
|
|
361
|
+
return pending;
|
|
362
|
+
},
|
|
363
|
+
invalidate() {
|
|
364
|
+
cached = void 0;
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
function requireEndpoint(document, member) {
|
|
369
|
+
const value = document[member];
|
|
370
|
+
if (typeof value !== "string" || !value)
|
|
371
|
+
throw new AuthioDiscoveryError(`The Authio discovery document has no ${member}.`);
|
|
372
|
+
return value;
|
|
373
|
+
}
|
|
374
|
+
async function fetchDocument(connection, signal) {
|
|
375
|
+
const url = connection.discoveryUrl;
|
|
376
|
+
let body;
|
|
377
|
+
try {
|
|
378
|
+
const response = await send(connection, url, { method: "GET", headers: { accept: "application/json" } }, signal);
|
|
379
|
+
body = await response.text();
|
|
380
|
+
if (!response.ok) throw new AuthioDiscoveryError(`Authio answered HTTP ${response.status} for the discovery document at ${url}.`);
|
|
381
|
+
} catch (error) {
|
|
382
|
+
if (error instanceof AuthioDiscoveryError) throw error;
|
|
383
|
+
if (signal?.aborted) throw error;
|
|
384
|
+
const timedOut = error?.name === "TimeoutError";
|
|
385
|
+
throw new AuthioDiscoveryError(
|
|
386
|
+
timedOut ? `Reading the Authio discovery document at ${url} timed out.` : `The Authio discovery document at ${url} could not be read.`,
|
|
387
|
+
{ cause: error }
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
let document;
|
|
391
|
+
try {
|
|
392
|
+
document = JSON.parse(body);
|
|
393
|
+
} catch (error) {
|
|
394
|
+
throw new AuthioDiscoveryError(`The Authio discovery document at ${url} is not valid JSON.`, { cause: error });
|
|
395
|
+
}
|
|
396
|
+
if (!document || typeof document !== "object" || typeof document.issuer !== "string" || !document.issuer.trim())
|
|
397
|
+
throw new AuthioDiscoveryError(`The Authio discovery document at ${url} has no issuer.`);
|
|
398
|
+
if (document.issuer.replace(/\/+$/, "") !== connection.issuer)
|
|
399
|
+
throw new AuthioDiscoveryError(
|
|
400
|
+
`The Authio discovery document names the issuer '${document.issuer}', not the configured '${connection.issuer}'.`
|
|
401
|
+
);
|
|
402
|
+
return document;
|
|
403
|
+
}
|
|
404
|
+
function isConnection(value) {
|
|
405
|
+
return typeof value.discoveryUrl === "string" && typeof value.httpTimeoutMs === "number";
|
|
406
|
+
}
|
|
407
|
+
var curveAlgorithms = { "P-256": "ES256", "P-384": "ES384", "P-521": "ES512" };
|
|
408
|
+
async function importSigningKey(input, algorithm, keyId, source) {
|
|
409
|
+
if (typeof input === "string") {
|
|
410
|
+
const text = input.trim();
|
|
411
|
+
if (text.startsWith("{")) return importSigningKey(parseJwk(text, source), algorithm, keyId, source);
|
|
412
|
+
return importPem(text, algorithm, keyId, source);
|
|
413
|
+
}
|
|
414
|
+
if (isCryptoKey(input)) {
|
|
415
|
+
const shape = describeCryptoKey(input, source);
|
|
416
|
+
return { key: input, algorithm: settleAlgorithm(shape, algorithm, source), keyId };
|
|
417
|
+
}
|
|
418
|
+
return importFromJwk(input, algorithm, keyId ?? (typeof input.kid === "string" ? input.kid : void 0), source);
|
|
419
|
+
}
|
|
420
|
+
var AuthioClientAssertionAlgorithms = AuthioAsymmetricAlgorithms;
|
|
421
|
+
async function importPem(pem, algorithm, keyId, source) {
|
|
422
|
+
const nodeCrypto = await loadNodeCrypto();
|
|
423
|
+
if (nodeCrypto) {
|
|
424
|
+
let keyObject;
|
|
425
|
+
try {
|
|
426
|
+
keyObject = nodeCrypto.createPrivateKey({ key: pem, format: "pem" });
|
|
427
|
+
} catch (error) {
|
|
428
|
+
throw new AuthioConfigurationError(`${source} could not be read as an RSA or EC private key.`, { cause: error });
|
|
429
|
+
}
|
|
430
|
+
const jwk = keyObject.export({ format: "jwk" });
|
|
431
|
+
return importFromJwk(jwk, algorithm, keyId, source);
|
|
432
|
+
}
|
|
433
|
+
if (!pem.includes("-----BEGIN PRIVATE KEY-----"))
|
|
434
|
+
throw new AuthioConfigurationError(
|
|
435
|
+
`${source} must be a PKCS#8 PEM ("BEGIN PRIVATE KEY") or a JWK outside Node; PKCS#1 and SEC1 keys need Node.`
|
|
436
|
+
);
|
|
437
|
+
const chosen = algorithm?.trim() || "RS256";
|
|
438
|
+
try {
|
|
439
|
+
return { key: await jose.importPKCS8(pem, chosen), algorithm: chosen, keyId };
|
|
440
|
+
} catch (error) {
|
|
441
|
+
throw new AuthioConfigurationError(`${source} could not be imported for ${chosen}.`, { cause: error });
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
async function importFromJwk(jwk, algorithm, keyId, source) {
|
|
445
|
+
if (!jwk.d) throw new AuthioConfigurationError(`${source} is a public key; Authio needs the private key to sign.`);
|
|
446
|
+
const shape = describeJwk(jwk, source);
|
|
447
|
+
const chosen = settleAlgorithm(shape, algorithm ?? (typeof jwk.alg === "string" ? jwk.alg : void 0), source);
|
|
448
|
+
try {
|
|
449
|
+
const key = await jose.importJWK({ ...jwk, alg: chosen }, chosen);
|
|
450
|
+
return { key, algorithm: chosen, keyId };
|
|
451
|
+
} catch (error) {
|
|
452
|
+
throw new AuthioConfigurationError(`${source} could not be imported for ${chosen}.`, { cause: error });
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
function describeJwk(jwk, source) {
|
|
456
|
+
if (jwk.kty === "RSA") {
|
|
457
|
+
const bits = base64UrlLength(jwk.n ?? "") * 8;
|
|
458
|
+
if (bits < AuthioDefaults.MinimumRsaKeySize)
|
|
459
|
+
throw new AuthioConfigurationError(`${source} is a ${bits}-bit RSA key; use at least ${AuthioDefaults.MinimumRsaKeySize} bits.`);
|
|
460
|
+
return { type: "RSA" };
|
|
461
|
+
}
|
|
462
|
+
if (jwk.kty === "EC") {
|
|
463
|
+
if (!jwk.crv || !curveAlgorithms[jwk.crv])
|
|
464
|
+
throw new AuthioConfigurationError(`${source} is an EC key on a curve Authio does not accept; use P-256, P-384 or P-521.`);
|
|
465
|
+
return { type: "EC", curve: jwk.crv };
|
|
466
|
+
}
|
|
467
|
+
throw new AuthioConfigurationError(`${source} could not be read as an RSA or EC private key.`);
|
|
468
|
+
}
|
|
469
|
+
function describeCryptoKey(key, source) {
|
|
470
|
+
const algorithm = key.algorithm;
|
|
471
|
+
if (algorithm.name === "ECDSA" && algorithm.namedCurve && curveAlgorithms[algorithm.namedCurve])
|
|
472
|
+
return { type: "EC", curve: algorithm.namedCurve };
|
|
473
|
+
if (algorithm.name === "RSASSA-PKCS1-v1_5" || algorithm.name === "RSA-PSS") {
|
|
474
|
+
if ((algorithm.modulusLength ?? 0) < AuthioDefaults.MinimumRsaKeySize)
|
|
475
|
+
throw new AuthioConfigurationError(
|
|
476
|
+
`${source} is a ${algorithm.modulusLength}-bit RSA key; use at least ${AuthioDefaults.MinimumRsaKeySize} bits.`
|
|
477
|
+
);
|
|
478
|
+
const size = algorithm.hash?.name.replace("SHA-", "") ?? "256";
|
|
479
|
+
return { type: "RSA", fixedAlgorithm: `${algorithm.name === "RSA-PSS" ? "PS" : "RS"}${size}` };
|
|
480
|
+
}
|
|
481
|
+
throw new AuthioConfigurationError(`${source} is a CryptoKey Authio cannot sign assertions with; use an RSA or EC signing key.`);
|
|
482
|
+
}
|
|
483
|
+
function settleAlgorithm(shape, configured, source) {
|
|
484
|
+
const suited = shape.fixedAlgorithm !== void 0 ? [shape.fixedAlgorithm] : shape.type === "EC" ? [curveAlgorithms[shape.curve]] : AuthioAsymmetricAlgorithms.filter((name) => name.startsWith("RS") || name.startsWith("PS"));
|
|
485
|
+
const chosen = configured?.trim() || suited[0];
|
|
486
|
+
if (!suited.includes(chosen))
|
|
487
|
+
throw new AuthioConfigurationError(
|
|
488
|
+
`The signing algorithm '${chosen}' does not suit the key in ${source}; use one of: ${suited.join(", ")}.`
|
|
489
|
+
);
|
|
490
|
+
return chosen;
|
|
491
|
+
}
|
|
492
|
+
function parseJwk(text, source) {
|
|
493
|
+
try {
|
|
494
|
+
const parsed = JSON.parse(text);
|
|
495
|
+
if (Array.isArray(parsed.keys)) {
|
|
496
|
+
if (parsed.keys.length !== 1) throw new Error("A JWK Set must hold exactly one key.");
|
|
497
|
+
return parsed.keys[0];
|
|
498
|
+
}
|
|
499
|
+
return parsed;
|
|
500
|
+
} catch (error) {
|
|
501
|
+
throw new AuthioConfigurationError(`${source} is not a valid JWK.`, { cause: error });
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
function base64UrlLength(value) {
|
|
505
|
+
const clean = value.replace(/=+$/, "");
|
|
506
|
+
return Math.floor(clean.length * 3 / 4);
|
|
507
|
+
}
|
|
508
|
+
function isCryptoKey(value) {
|
|
509
|
+
return typeof value === "object" && value !== null && "algorithm" in value && "type" in value && "usages" in value;
|
|
510
|
+
}
|
|
511
|
+
var nodeCryptoModule;
|
|
512
|
+
function loadNodeCrypto() {
|
|
513
|
+
nodeCryptoModule ??= globalThis.process?.versions?.node ? import('crypto').catch(() => void 0) : Promise.resolve(void 0);
|
|
514
|
+
return nodeCryptoModule;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// src/client-authentication.ts
|
|
518
|
+
function toBasicAuthorization(clientId, clientSecret) {
|
|
519
|
+
const bytes = new TextEncoder().encode(`${escapeDataString(clientId)}:${escapeDataString(clientSecret)}`);
|
|
520
|
+
let binary = "";
|
|
521
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
522
|
+
return `Basic ${btoa(binary)}`;
|
|
523
|
+
}
|
|
524
|
+
function createClientAuthenticator(settings, connection) {
|
|
525
|
+
const clientId = settings.clientId?.trim();
|
|
526
|
+
if (!clientId) throw new AuthioConfigurationError("The Authio clientId is required.");
|
|
527
|
+
const hasKey = settings.clientPrivateKey !== void 0 || !!settings.clientPrivateKeyPath;
|
|
528
|
+
const method = settings.clientAuthentication ?? (!settings.clientSecret && hasKey ? "private_key_jwt" : "client_secret_basic");
|
|
529
|
+
if ((method === "client_secret_basic" || method === "client_secret_post") && !settings.clientSecret)
|
|
530
|
+
throw new AuthioConfigurationError(`The Authio clientSecret is required with ${method}.`);
|
|
531
|
+
if (method === "private_key_jwt" && !hasKey)
|
|
532
|
+
throw new AuthioConfigurationError("private_key_jwt needs clientPrivateKey or clientPrivateKeyPath.");
|
|
533
|
+
const lifetimeMs = seconds(settings.clientAssertionLifetime, AuthioDefaults.ClientAssertionLifetime, "clientAssertionLifetime");
|
|
534
|
+
if (lifetimeMs <= 0 || lifetimeMs > AuthioDefaults.MaxClientAssertionLifetime * 1e3)
|
|
535
|
+
throw new AuthioConfigurationError(
|
|
536
|
+
`The Authio clientAssertionLifetime must be more than 0 and at most ${AuthioDefaults.MaxClientAssertionLifetime} seconds.`
|
|
537
|
+
);
|
|
538
|
+
const loadKey = method === "private_key_jwt" ? createKeyLoader(settings) : void 0;
|
|
539
|
+
return {
|
|
540
|
+
clientId,
|
|
541
|
+
method,
|
|
542
|
+
async apply(form, headers) {
|
|
543
|
+
switch (method) {
|
|
544
|
+
case "client_secret_basic":
|
|
545
|
+
headers.set("authorization", toBasicAuthorization(clientId, settings.clientSecret));
|
|
546
|
+
return;
|
|
547
|
+
case "client_secret_post":
|
|
548
|
+
form.set("client_id", clientId);
|
|
549
|
+
form.set("client_secret", settings.clientSecret);
|
|
550
|
+
return;
|
|
551
|
+
case "private_key_jwt":
|
|
552
|
+
form.set("client_id", clientId);
|
|
553
|
+
form.set("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
|
|
554
|
+
form.set("client_assertion", await buildAssertion(clientId, connection, lifetimeMs, await loadKey()));
|
|
555
|
+
return;
|
|
556
|
+
default:
|
|
557
|
+
form.set("client_id", clientId);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
async function buildAssertion(clientId, connection, lifetimeMs, signing) {
|
|
563
|
+
const now = Math.floor(connection.clock.now() / 1e3);
|
|
564
|
+
const jti = [...crypto.getRandomValues(new Uint8Array(16))].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
565
|
+
return new jose.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);
|
|
566
|
+
}
|
|
567
|
+
function createKeyLoader(settings) {
|
|
568
|
+
if (settings.clientPrivateKey !== void 0) {
|
|
569
|
+
let loaded;
|
|
570
|
+
return () => {
|
|
571
|
+
loaded ??= importSigningKey(settings.clientPrivateKey, settings.clientSigningAlgorithm, settings.clientKeyId, "clientPrivateKey");
|
|
572
|
+
loaded.catch(() => {
|
|
573
|
+
loaded = void 0;
|
|
574
|
+
});
|
|
575
|
+
return loaded;
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
const path = settings.clientPrivateKeyPath;
|
|
579
|
+
let current;
|
|
580
|
+
return async () => {
|
|
581
|
+
const nodeCrypto = await loadNodeCrypto();
|
|
582
|
+
if (!nodeCrypto) throw new AuthioConfigurationError("clientPrivateKeyPath needs Node; pass clientPrivateKey instead.");
|
|
583
|
+
const { readFile, stat } = await import('fs/promises');
|
|
584
|
+
let written;
|
|
585
|
+
try {
|
|
586
|
+
written = (await stat(path)).mtimeMs;
|
|
587
|
+
} catch (error) {
|
|
588
|
+
throw new AuthioConfigurationError(`clientPrivateKeyPath '${path}' could not be read.`, { cause: error });
|
|
589
|
+
}
|
|
590
|
+
if (!current || current.written !== written) {
|
|
591
|
+
const key = readFile(path, "utf8").then(
|
|
592
|
+
(pem) => importSigningKey(pem, settings.clientSigningAlgorithm, settings.clientKeyId, `clientPrivateKeyPath '${path}'`)
|
|
593
|
+
);
|
|
594
|
+
current = { written, key };
|
|
595
|
+
key.catch(() => {
|
|
596
|
+
if (current?.key === key) current = void 0;
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
return current.key;
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
var AuthioKeyManagementAlgorithms = ["RSA-OAEP", "RSA-OAEP-256"];
|
|
603
|
+
var AuthioContentEncryptionAlgorithms = ["A128CBC-HS256", "A192CBC-HS384", "A256CBC-HS512"];
|
|
604
|
+
var rsaAlgorithmIdentifier = [48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0];
|
|
605
|
+
function isAuthioTokenEncrypted(token) {
|
|
606
|
+
return typeof token === "string" && token.split(".").length === 5;
|
|
607
|
+
}
|
|
608
|
+
function createAuthioTokenDecrypter(key, setting = "clientEncryptionKey") {
|
|
609
|
+
const imported = /* @__PURE__ */ new Map();
|
|
610
|
+
const importFor = (algorithm) => {
|
|
611
|
+
let pending = imported.get(algorithm);
|
|
612
|
+
if (!pending) {
|
|
613
|
+
pending = importDecryptionKey(key, algorithm, setting);
|
|
614
|
+
imported.set(algorithm, pending);
|
|
615
|
+
pending.catch(() => imported.delete(algorithm));
|
|
616
|
+
}
|
|
617
|
+
return pending;
|
|
618
|
+
};
|
|
619
|
+
return async (token) => {
|
|
620
|
+
if (!isAuthioTokenEncrypted(token)) return token;
|
|
621
|
+
if (!key)
|
|
622
|
+
throw new AuthioError(
|
|
623
|
+
`Authio encrypted this token to the client's own key; set ${setting} to the private key Authio showed when it generated the pair.`
|
|
624
|
+
);
|
|
625
|
+
const { plaintext } = await jose.compactDecrypt(token, (header) => importFor(header.alg), {
|
|
626
|
+
keyManagementAlgorithms: [...AuthioKeyManagementAlgorithms],
|
|
627
|
+
contentEncryptionAlgorithms: [...AuthioContentEncryptionAlgorithms]
|
|
628
|
+
});
|
|
629
|
+
return new TextDecoder().decode(plaintext);
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
async function importDecryptionKey(key, algorithm, setting) {
|
|
633
|
+
try {
|
|
634
|
+
if (typeof key !== "string") return await importPrivateJwk(key, algorithm, setting);
|
|
635
|
+
const text = key.trim();
|
|
636
|
+
return text.startsWith("{") ? await importPrivateJwk(JSON.parse(text), algorithm, setting) : await jose.importPKCS8(toPkcs8(text), algorithm);
|
|
637
|
+
} catch (error) {
|
|
638
|
+
if (error instanceof AuthioConfigurationError) throw error;
|
|
639
|
+
throw new AuthioConfigurationError(`${setting} is neither a private JWK nor a PEM private key.`, { cause: error });
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
function importPrivateJwk(jwk, algorithm, setting) {
|
|
643
|
+
if (!jwk.d) throw new AuthioConfigurationError(`${setting} is a JWK without its private parameters; only the private key decrypts.`);
|
|
644
|
+
return jose.importJWK({ ...jwk, alg: algorithm, use: "enc", key_ops: void 0 }, algorithm);
|
|
645
|
+
}
|
|
646
|
+
function toPkcs8(pem) {
|
|
647
|
+
const normalized = pem.replace(/\\n/g, "\n").trim();
|
|
648
|
+
if (!normalized.includes("BEGIN RSA PRIVATE KEY")) return normalized;
|
|
649
|
+
const pkcs1 = fromBase64(normalized.replace(/-----[^-]+-----|\s/g, ""));
|
|
650
|
+
const body = [2, 1, 0, ...rsaAlgorithmIdentifier, 4, ...derLength(pkcs1.length), ...pkcs1];
|
|
651
|
+
const der = Uint8Array.from([48, ...derLength(body.length), ...body]);
|
|
652
|
+
const lines = toBase64(der).match(/.{1,64}/g) ?? [];
|
|
653
|
+
return `-----BEGIN PRIVATE KEY-----
|
|
654
|
+
${lines.join("\n")}
|
|
655
|
+
-----END PRIVATE KEY-----`;
|
|
656
|
+
}
|
|
657
|
+
function derLength(length) {
|
|
658
|
+
if (length < 128) return [length];
|
|
659
|
+
const bytes = [];
|
|
660
|
+
for (let rest = length; rest > 0; rest >>= 8) bytes.unshift(rest & 255);
|
|
661
|
+
return [128 | bytes.length, ...bytes];
|
|
662
|
+
}
|
|
663
|
+
function fromBase64(text) {
|
|
664
|
+
return Uint8Array.from(atob(text), (character) => character.charCodeAt(0));
|
|
665
|
+
}
|
|
666
|
+
function toBase64(bytes) {
|
|
667
|
+
let binary = "";
|
|
668
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
669
|
+
return btoa(binary);
|
|
670
|
+
}
|
|
671
|
+
function createAuthioClient(options) {
|
|
672
|
+
const connection = resolveConnection(options);
|
|
673
|
+
const discovery = createAuthioDiscoveryClient(connection);
|
|
674
|
+
const authenticator = createClientAuthenticator(options, connection);
|
|
675
|
+
const refreshSkewMs = seconds(options.tokenRefreshSkew, AuthioDefaults.TokenRefreshSkew, "tokenRefreshSkew");
|
|
676
|
+
const decrypt = createAuthioTokenDecrypter(options.clientEncryptionKey);
|
|
677
|
+
const tokens = createTokenClient(connection, discovery, authenticator);
|
|
678
|
+
const clientCredentials = createClientCredentialsCache(connection, authenticator.clientId, tokens, refreshSkewMs);
|
|
679
|
+
return {
|
|
680
|
+
issuer: connection.issuer,
|
|
681
|
+
clientId: authenticator.clientId,
|
|
682
|
+
connection,
|
|
683
|
+
discovery,
|
|
684
|
+
tokens,
|
|
685
|
+
clientCredentials,
|
|
686
|
+
async introspect(token, callOptions = {}) {
|
|
687
|
+
requireValue(token, "token");
|
|
688
|
+
const response = await postForm(
|
|
689
|
+
connection,
|
|
690
|
+
discovery,
|
|
691
|
+
authenticator,
|
|
692
|
+
"introspection_endpoint",
|
|
693
|
+
{ token, token_type_hint: callOptions.tokenTypeHint },
|
|
694
|
+
callOptions.signal
|
|
695
|
+
);
|
|
696
|
+
return readProtocolJson(response);
|
|
697
|
+
},
|
|
698
|
+
async revoke(token, callOptions = {}) {
|
|
699
|
+
requireValue(token, "token");
|
|
700
|
+
const response = await postForm(
|
|
701
|
+
connection,
|
|
702
|
+
discovery,
|
|
703
|
+
authenticator,
|
|
704
|
+
"revocation_endpoint",
|
|
705
|
+
{ token, token_type_hint: callOptions.tokenTypeHint },
|
|
706
|
+
callOptions.signal
|
|
707
|
+
);
|
|
708
|
+
await ensureProtocolSuccess(response);
|
|
709
|
+
},
|
|
710
|
+
async userInfo(accessToken, callOptions = {}) {
|
|
711
|
+
requireValue(accessToken, "accessToken");
|
|
712
|
+
const document = await discovery.get(callOptions.signal);
|
|
713
|
+
const response = await send(
|
|
714
|
+
connection,
|
|
715
|
+
requireEndpoint(document, "userinfo_endpoint"),
|
|
716
|
+
{ method: "GET", headers: { authorization: `Bearer ${accessToken}`, accept: "application/json, application/jwt" } },
|
|
717
|
+
callOptions.signal
|
|
718
|
+
);
|
|
719
|
+
const mediaType = response.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase();
|
|
720
|
+
if (response.ok && mediaType === "application/jwt")
|
|
721
|
+
return readUserInfoJwt((await response.text()).trim(), document, connection, authenticator.clientId, decrypt, response.status);
|
|
722
|
+
if (!response.ok) {
|
|
723
|
+
const body = await response.text();
|
|
724
|
+
const challenge = toErrorResponse(body) ? void 0 : readBearerChallenge(response);
|
|
725
|
+
throw challenge ? new AuthioProtocolError(challenge.error, challenge.description, void 0, response.status) : toProtocolError(body, response.status);
|
|
726
|
+
}
|
|
727
|
+
return readProtocolJson(response);
|
|
728
|
+
},
|
|
729
|
+
clientCredentialsFetch(fetchOptions = {}) {
|
|
730
|
+
const inner = fetchOptions.fetch ?? connection.fetch;
|
|
731
|
+
const key = { scopes: fetchOptions.scopes, resource: fetchOptions.resource };
|
|
732
|
+
return async (input, init) => {
|
|
733
|
+
const request = new Request(input, init);
|
|
734
|
+
const body = request.body ? await request.arrayBuffer() : void 0;
|
|
735
|
+
const attempt = async (token2) => {
|
|
736
|
+
const headers = new Headers(request.headers);
|
|
737
|
+
headers.set("authorization", `${token2.tokenType} ${token2.accessToken}`);
|
|
738
|
+
return inner(request.url, { method: request.method, headers, body, signal: request.signal, redirect: request.redirect });
|
|
739
|
+
};
|
|
740
|
+
const token = await clientCredentials.getToken({ ...key, signal: request.signal });
|
|
741
|
+
const response = await attempt(token);
|
|
742
|
+
if (response.status !== 401) return response;
|
|
743
|
+
clientCredentials.invalidate({ ...key, accessToken: token.accessToken });
|
|
744
|
+
return attempt(await clientCredentials.getToken({ ...key, signal: request.signal }));
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
function createTokenClient(connection, discovery, authenticator) {
|
|
750
|
+
const requestToken = async (parameters, signal) => {
|
|
751
|
+
const response = await postForm(connection, discovery, authenticator, "token_endpoint", parameters, signal);
|
|
752
|
+
const token = await readProtocolJson(response);
|
|
753
|
+
return redact(token, ["access_token", "refresh_token", "id_token"]);
|
|
754
|
+
};
|
|
755
|
+
const pollDevice = async (authorization, options = {}) => {
|
|
756
|
+
requireValue(authorization?.device_code, "device_code");
|
|
757
|
+
let wait = (authorization.interval && authorization.interval > 0 ? authorization.interval : AuthioDefaults.DeviceInterval) * 1e3;
|
|
758
|
+
const deadline = authorization.expires_in && authorization.expires_in > 0 ? connection.clock.now() + authorization.expires_in * 1e3 : void 0;
|
|
759
|
+
for (; ; ) {
|
|
760
|
+
await connection.clock.sleep(wait, options.signal);
|
|
761
|
+
if (deadline !== void 0 && connection.clock.now() >= deadline)
|
|
762
|
+
throw new AuthioProtocolError(AuthioOAuthErrors.ExpiredToken, "The device code expired before the user approved it.");
|
|
763
|
+
try {
|
|
764
|
+
return await requestToken({ grant_type: AuthioGrantTypes.DeviceCode, device_code: authorization.device_code }, options.signal);
|
|
765
|
+
} catch (error) {
|
|
766
|
+
if (!(error instanceof AuthioProtocolError)) throw error;
|
|
767
|
+
if (error.error === AuthioOAuthErrors.AuthorizationPending) continue;
|
|
768
|
+
if (error.error === AuthioOAuthErrors.SlowDown) {
|
|
769
|
+
wait += AuthioDefaults.DeviceSlowDownIncrement * 1e3;
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
throw error;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
return {
|
|
777
|
+
clientCredentials: (options = {}) => requestToken(
|
|
778
|
+
{ grant_type: AuthioGrantTypes.ClientCredentials, scope: toScopeString(options.scopes), resource: options.resource },
|
|
779
|
+
options.signal
|
|
780
|
+
),
|
|
781
|
+
authorizationCode: (options) => {
|
|
782
|
+
requireValue(options?.code, "code");
|
|
783
|
+
requireValue(options.redirectUri, "redirectUri");
|
|
784
|
+
return requestToken(
|
|
785
|
+
{
|
|
786
|
+
grant_type: AuthioGrantTypes.AuthorizationCode,
|
|
787
|
+
code: options.code,
|
|
788
|
+
redirect_uri: options.redirectUri,
|
|
789
|
+
code_verifier: options.codeVerifier,
|
|
790
|
+
resource: options.resource
|
|
791
|
+
},
|
|
792
|
+
options.signal
|
|
793
|
+
);
|
|
794
|
+
},
|
|
795
|
+
refresh: (options) => {
|
|
796
|
+
requireValue(options?.refreshToken, "refreshToken");
|
|
797
|
+
return requestToken(
|
|
798
|
+
{ grant_type: AuthioGrantTypes.RefreshToken, refresh_token: options.refreshToken, scope: toScopeString(options.scopes) },
|
|
799
|
+
options.signal
|
|
800
|
+
);
|
|
801
|
+
},
|
|
802
|
+
exchange: (options) => {
|
|
803
|
+
requireValue(options?.subjectToken, "subjectToken");
|
|
804
|
+
return requestToken(
|
|
805
|
+
{
|
|
806
|
+
grant_type: AuthioGrantTypes.TokenExchange,
|
|
807
|
+
subject_token: options.subjectToken,
|
|
808
|
+
subject_token_type: AuthioTokenTypes.AccessTokenUrn,
|
|
809
|
+
requested_token_type: AuthioTokenTypes.AccessTokenUrn,
|
|
810
|
+
scope: toScopeString(options.scopes),
|
|
811
|
+
resource: options.resource
|
|
812
|
+
},
|
|
813
|
+
options.signal
|
|
814
|
+
);
|
|
815
|
+
},
|
|
816
|
+
async exchangeForConnection(options) {
|
|
817
|
+
requireValue(options?.subjectToken, "subjectToken");
|
|
818
|
+
requireValue(options.provider, "provider");
|
|
819
|
+
const response = await postForm(
|
|
820
|
+
connection,
|
|
821
|
+
discovery,
|
|
822
|
+
authenticator,
|
|
823
|
+
"token_endpoint",
|
|
824
|
+
{
|
|
825
|
+
grant_type: AuthioGrantTypes.TokenExchange,
|
|
826
|
+
subject_token: options.subjectToken,
|
|
827
|
+
subject_token_type: AuthioTokenTypes.AccessTokenUrn,
|
|
828
|
+
requested_token_type: AuthioTokenTypes.ConnectionAccessTokenUrn,
|
|
829
|
+
connection_provider: options.provider,
|
|
830
|
+
connection_id: options.connectionId
|
|
831
|
+
},
|
|
832
|
+
options.signal
|
|
833
|
+
);
|
|
834
|
+
return toConnectionExchangeResult(await response.text(), response.status, options.provider);
|
|
835
|
+
},
|
|
836
|
+
async deviceAuthorization(options = {}) {
|
|
837
|
+
const response = await postForm(
|
|
838
|
+
connection,
|
|
839
|
+
discovery,
|
|
840
|
+
authenticator,
|
|
841
|
+
"device_authorization_endpoint",
|
|
842
|
+
{ scope: toScopeString(options.scopes) },
|
|
843
|
+
options.signal
|
|
844
|
+
);
|
|
845
|
+
return redact(await readProtocolJson(response), ["device_code"]);
|
|
846
|
+
},
|
|
847
|
+
pollDevice,
|
|
848
|
+
password: (options) => {
|
|
849
|
+
requireValue(options?.username, "username");
|
|
850
|
+
if (typeof options.password !== "string") throw new TypeError("password is required.");
|
|
851
|
+
return requestToken(
|
|
852
|
+
{ grant_type: AuthioGrantTypes.Password, username: options.username, password: options.password, scope: toScopeString(options.scopes) },
|
|
853
|
+
options.signal
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
function createClientCredentialsCache(connection, clientId, tokens, refreshSkewMs) {
|
|
859
|
+
const entries = /* @__PURE__ */ new Map();
|
|
860
|
+
const pending = /* @__PURE__ */ new Map();
|
|
861
|
+
const keyOf = (scope, resource) => `${connection.issuer}|${clientId}|${(scope ?? "").split(" ").filter(Boolean).sort().join(" ")}|${resource ?? ""}`;
|
|
862
|
+
return {
|
|
863
|
+
async getToken(options = {}) {
|
|
864
|
+
const scope = toScopeString(options.scopes);
|
|
865
|
+
const key = keyOf(scope, options.resource);
|
|
866
|
+
const cached = entries.get(key);
|
|
867
|
+
if (cached && connection.clock.now() < cached.refreshAt) return cached;
|
|
868
|
+
let request = pending.get(key);
|
|
869
|
+
if (!request) {
|
|
870
|
+
request = tokens.clientCredentials({ scopes: scope, resource: options.resource, signal: options.signal }).then((response) => {
|
|
871
|
+
const token = toClientCredentialsToken(response, connection.clock.now(), refreshSkewMs);
|
|
872
|
+
entries.set(key, token);
|
|
873
|
+
return token;
|
|
874
|
+
}).finally(() => pending.delete(key));
|
|
875
|
+
pending.set(key, request);
|
|
876
|
+
}
|
|
877
|
+
return request;
|
|
878
|
+
},
|
|
879
|
+
invalidate(options = {}) {
|
|
880
|
+
const key = keyOf(toScopeString(options.scopes), options.resource);
|
|
881
|
+
if (options.accessToken === void 0 || entries.get(key)?.accessToken === options.accessToken) entries.delete(key);
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
function toClientCredentialsToken(response, issuedAt, refreshSkewMs) {
|
|
886
|
+
const lifetime = (response.expires_in && response.expires_in > 0 ? response.expires_in : AuthioDefaults.AssumedTokenLifetime) * 1e3;
|
|
887
|
+
const expiresAt = issuedAt + lifetime;
|
|
888
|
+
const skew = Math.min(refreshSkewMs, lifetime / 2);
|
|
889
|
+
return redact(
|
|
890
|
+
{
|
|
891
|
+
accessToken: response.access_token,
|
|
892
|
+
tokenType: response.token_type?.trim() ? response.token_type : "Bearer",
|
|
893
|
+
expiresAt,
|
|
894
|
+
refreshAt: expiresAt - skew,
|
|
895
|
+
scope: response.scope
|
|
896
|
+
},
|
|
897
|
+
["accessToken"]
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
function toConnectionExchangeResult(body, status, provider) {
|
|
901
|
+
if (status >= 200 && status < 300) {
|
|
902
|
+
const token = parseProtocolJson(body, status);
|
|
903
|
+
if (typeof token.access_token === "string" && token.access_token)
|
|
904
|
+
return redact(
|
|
905
|
+
{
|
|
906
|
+
status: "granted",
|
|
907
|
+
accessToken: token.access_token,
|
|
908
|
+
provider: typeof token.provider === "string" && token.provider ? token.provider : provider,
|
|
909
|
+
providerType: token.provider_type,
|
|
910
|
+
connectionId: token.connection_id,
|
|
911
|
+
tokenType: token.token_type,
|
|
912
|
+
issuedTokenType: token.issued_token_type,
|
|
913
|
+
scope: token.scope,
|
|
914
|
+
externalAccount: token.external_account,
|
|
915
|
+
expiresIn: token.expires_in
|
|
916
|
+
},
|
|
917
|
+
["accessToken"]
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
const error = toErrorResponse(body);
|
|
921
|
+
const description = error?.error_description?.trim() ? error.error_description : `Authio answered HTTP ${status} to the connected-account token exchange.`;
|
|
922
|
+
switch (error?.error) {
|
|
923
|
+
case AuthioOAuthErrors.ConnectionRequired:
|
|
924
|
+
return { status: "required", connectUri: error.connect_uri, description };
|
|
925
|
+
case AuthioOAuthErrors.ConnectionSelectionRequired:
|
|
926
|
+
return { status: "selection_required", connections: error.connections ?? [], description };
|
|
927
|
+
default:
|
|
928
|
+
return { status: "refused", error: error?.error ?? `http_${status}`, code: error?.error_code, description, connectUri: error?.connect_uri };
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
async function postForm(connection, discovery, authenticator, endpoint, parameters, signal) {
|
|
932
|
+
const document = await discovery.get(signal);
|
|
933
|
+
const url = requireEndpoint(document, endpoint);
|
|
934
|
+
const form = formBody(parameters);
|
|
935
|
+
const headers = new Headers({ accept: "application/json", "content-type": "application/x-www-form-urlencoded" });
|
|
936
|
+
await authenticator.apply(form, headers);
|
|
937
|
+
return send(connection, url, { method: "POST", headers, body: form.toString() }, signal);
|
|
938
|
+
}
|
|
939
|
+
async function readUserInfoJwt(jwt, document, connection, clientId, decrypt, status) {
|
|
940
|
+
try {
|
|
941
|
+
const signed = isAuthioTokenEncrypted(jwt) ? await decrypt(jwt) : jwt;
|
|
942
|
+
const keys = jose.createRemoteJWKSet(new URL(requireEndpoint(document, "jwks_uri")), { [jose.customFetch]: connection.fetch });
|
|
943
|
+
const { payload } = await jose.jwtVerify(signed, keys, {
|
|
944
|
+
issuer: document.issuer,
|
|
945
|
+
audience: clientId,
|
|
946
|
+
clockTolerance: 30
|
|
947
|
+
});
|
|
948
|
+
return payload;
|
|
949
|
+
} catch (error) {
|
|
950
|
+
throw new AuthioProtocolError(
|
|
951
|
+
"invalid_userinfo",
|
|
952
|
+
`The userinfo JWT could not be verified: ${error?.message ?? "unknown reason."}`,
|
|
953
|
+
void 0,
|
|
954
|
+
status,
|
|
955
|
+
{ cause: error }
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
function readBearerChallenge(response) {
|
|
960
|
+
const header = response.headers.get("www-authenticate");
|
|
961
|
+
if (!header || !/^bearer\s/i.test(header.trim())) return void 0;
|
|
962
|
+
let error = "";
|
|
963
|
+
let description;
|
|
964
|
+
for (const match of header.matchAll(/([a-zA-Z_]+)\s*=\s*"((?:[^"\\]|\\.)*)"|([a-zA-Z_]+)\s*=\s*([^,\s]+)/g)) {
|
|
965
|
+
const name = match[1] ?? match[3];
|
|
966
|
+
const value = (match[2] ?? match[4] ?? "").replace(/\\(.)/g, "$1");
|
|
967
|
+
if (name === "error") error = value;
|
|
968
|
+
else if (name === "error_description") description = value;
|
|
969
|
+
}
|
|
970
|
+
return error ? { error, description } : void 0;
|
|
971
|
+
}
|
|
972
|
+
function requireValue(value, name) {
|
|
973
|
+
if (typeof value !== "string" || !value.trim()) throw new TypeError(`${name} is required.`);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// src/resource-access.ts
|
|
977
|
+
var claimSources = new Map(Object.entries(AuthioResourceAccessClaims).map(([name, source]) => [name.toLowerCase(), source]));
|
|
978
|
+
var AuthioResourceAccess = class _AuthioResourceAccess {
|
|
979
|
+
/** No role at all. */
|
|
980
|
+
static empty = new _AuthioResourceAccess([]);
|
|
981
|
+
/** Every role read, in claim order, one entry per source it came from. */
|
|
982
|
+
entries;
|
|
983
|
+
/** Every distinct role name. */
|
|
984
|
+
roles;
|
|
985
|
+
/** Resource → every method any role allows on it. */
|
|
986
|
+
permissions;
|
|
987
|
+
lookup;
|
|
988
|
+
constructor(entries) {
|
|
989
|
+
this.entries = Object.freeze([...entries]);
|
|
990
|
+
this.roles = Object.freeze(distinctIgnoringCase(this.entries.map((entry) => entry.name)));
|
|
991
|
+
this.lookup = /* @__PURE__ */ new Map();
|
|
992
|
+
const permissions = {};
|
|
993
|
+
const spelling = /* @__PURE__ */ new Map();
|
|
994
|
+
for (const entry of this.entries) {
|
|
995
|
+
for (const [resource, methods] of Object.entries(entry.permissions)) {
|
|
996
|
+
const resourceKey = resource.toLowerCase();
|
|
997
|
+
let allowed = this.lookup.get(resourceKey);
|
|
998
|
+
if (!allowed) {
|
|
999
|
+
allowed = /* @__PURE__ */ new Map();
|
|
1000
|
+
this.lookup.set(resourceKey, allowed);
|
|
1001
|
+
spelling.set(resourceKey, resource);
|
|
1002
|
+
permissions[resource] = [];
|
|
1003
|
+
}
|
|
1004
|
+
for (const method of methods) {
|
|
1005
|
+
const methodKey = method.toLowerCase();
|
|
1006
|
+
if (allowed.has(methodKey)) continue;
|
|
1007
|
+
allowed.set(methodKey, method);
|
|
1008
|
+
permissions[spelling.get(resourceKey)].push(method);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
this.permissions = Object.freeze(Object.fromEntries(Object.entries(permissions).map(([name, methods]) => [name, Object.freeze(methods)])));
|
|
1013
|
+
}
|
|
1014
|
+
/** Whether no role was read. */
|
|
1015
|
+
get isEmpty() {
|
|
1016
|
+
return this.entries.length === 0;
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Whether the token holds the role, from any source or only from the given ones.
|
|
1020
|
+
*/
|
|
1021
|
+
hasRole(role, sources) {
|
|
1022
|
+
if (!role) return false;
|
|
1023
|
+
const wanted = role.toLowerCase();
|
|
1024
|
+
return this.entries.some((entry) => inSources(entry, sources) && entry.name.toLowerCase() === wanted);
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Authio's grant rule: whether any role (from any source, or only from the given ones) allows `method` on
|
|
1028
|
+
* `resource`.
|
|
1029
|
+
*/
|
|
1030
|
+
grants(resource, method, sources) {
|
|
1031
|
+
if (!resource || !method) return false;
|
|
1032
|
+
if (!sources || sources.length === 0) return this.lookup.get(resource.toLowerCase())?.has(method.toLowerCase()) ?? false;
|
|
1033
|
+
return this.entries.some((entry) => inSources(entry, sources) && entryGrants(entry, resource, method));
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* The roles that came from one source.
|
|
1037
|
+
*/
|
|
1038
|
+
fromSource(source) {
|
|
1039
|
+
return this.entries.filter((entry) => entry.source === source);
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
function resourceAccessSourceOf(claimType) {
|
|
1043
|
+
return claimType ? claimSources.get(claimType.toLowerCase()) : void 0;
|
|
1044
|
+
}
|
|
1045
|
+
function parseResourceAccess(claims) {
|
|
1046
|
+
const pairs = isIterable(claims) ? claims : Object.entries(claims);
|
|
1047
|
+
const roles = [];
|
|
1048
|
+
for (const [type, value] of pairs) {
|
|
1049
|
+
const source = resourceAccessSourceOf(type);
|
|
1050
|
+
if (source) roles.push(...parseResourceAccessClaim(source, value));
|
|
1051
|
+
}
|
|
1052
|
+
return roles.length === 0 ? AuthioResourceAccess.empty : new AuthioResourceAccess(roles);
|
|
1053
|
+
}
|
|
1054
|
+
function parseResourceAccessClaim(source, value) {
|
|
1055
|
+
const root = typeof value === "string" ? parseJson(value) : value;
|
|
1056
|
+
if (!isPlainObject(root)) return [];
|
|
1057
|
+
const roles = [];
|
|
1058
|
+
for (const [name, rolePermissions] of Object.entries(root)) {
|
|
1059
|
+
const permissions = readPermissions(rolePermissions);
|
|
1060
|
+
if (!permissions) return [];
|
|
1061
|
+
roles.push(Object.freeze({ source, name, permissions }));
|
|
1062
|
+
}
|
|
1063
|
+
return roles;
|
|
1064
|
+
}
|
|
1065
|
+
function readPermissions(value) {
|
|
1066
|
+
if (value === null) return Object.freeze({});
|
|
1067
|
+
if (!isPlainObject(value)) return void 0;
|
|
1068
|
+
const read = {};
|
|
1069
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1070
|
+
for (const [resource, methods] of Object.entries(value)) {
|
|
1071
|
+
const key = resource.toLowerCase();
|
|
1072
|
+
if (methods === null) {
|
|
1073
|
+
if (!seen.has(key)) {
|
|
1074
|
+
seen.add(key);
|
|
1075
|
+
read[resource] = Object.freeze([]);
|
|
1076
|
+
}
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
if (!Array.isArray(methods)) return void 0;
|
|
1080
|
+
const allowed = [];
|
|
1081
|
+
for (const method of methods) {
|
|
1082
|
+
if (typeof method === "string") allowed.push(method);
|
|
1083
|
+
else if (method !== null) return void 0;
|
|
1084
|
+
}
|
|
1085
|
+
if (!seen.has(key)) {
|
|
1086
|
+
seen.add(key);
|
|
1087
|
+
read[resource] = Object.freeze(allowed);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
return Object.freeze(read);
|
|
1091
|
+
}
|
|
1092
|
+
function entryGrants(entry, resource, method) {
|
|
1093
|
+
const resourceKey = resource.toLowerCase();
|
|
1094
|
+
const methodKey = method.toLowerCase();
|
|
1095
|
+
for (const [name, methods] of Object.entries(entry.permissions)) {
|
|
1096
|
+
if (name.toLowerCase() === resourceKey) return methods.some((allowed) => allowed.toLowerCase() === methodKey);
|
|
1097
|
+
}
|
|
1098
|
+
return false;
|
|
1099
|
+
}
|
|
1100
|
+
function inSources(entry, sources) {
|
|
1101
|
+
return !sources || sources.length === 0 || sources.includes(entry.source);
|
|
1102
|
+
}
|
|
1103
|
+
function distinctIgnoringCase(values) {
|
|
1104
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1105
|
+
return values.filter((value) => {
|
|
1106
|
+
const key = value.toLowerCase();
|
|
1107
|
+
if (seen.has(key)) return false;
|
|
1108
|
+
seen.add(key);
|
|
1109
|
+
return true;
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
function parseJson(text) {
|
|
1113
|
+
if (!text.trim()) return void 0;
|
|
1114
|
+
try {
|
|
1115
|
+
return JSON.parse(text);
|
|
1116
|
+
} catch {
|
|
1117
|
+
return void 0;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
function isPlainObject(value) {
|
|
1121
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1122
|
+
}
|
|
1123
|
+
function isIterable(value) {
|
|
1124
|
+
return typeof value === "object" && value !== null && typeof value[Symbol.iterator] === "function";
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// src/webhooks.ts
|
|
1128
|
+
var encoder = new TextEncoder();
|
|
1129
|
+
async function verifyWebhookSignature(signatureHeader, rawBody, secret, options = {}) {
|
|
1130
|
+
if (!secret) throw new TypeError("secret is required.");
|
|
1131
|
+
if (!signatureHeader?.trim())
|
|
1132
|
+
return { valid: false, failure: "missing_signature", reason: "The X-Authio-Signature header is missing." };
|
|
1133
|
+
const parsed = parseSignatureHeader(signatureHeader);
|
|
1134
|
+
if (!parsed)
|
|
1135
|
+
return {
|
|
1136
|
+
valid: false,
|
|
1137
|
+
failure: "malformed_signature",
|
|
1138
|
+
reason: "The X-Authio-Signature header is not of the form t={unixSeconds},v1={hex}."
|
|
1139
|
+
};
|
|
1140
|
+
const signedAt = new Date(parsed.timestamp * 1e3);
|
|
1141
|
+
const expected = await computeSignature(parsed.timestamp, rawBody, secret);
|
|
1142
|
+
let matched = false;
|
|
1143
|
+
for (const signature of parsed.signatures) matched = constantTimeEquals(expected, signature) || matched;
|
|
1144
|
+
if (!matched)
|
|
1145
|
+
return {
|
|
1146
|
+
valid: false,
|
|
1147
|
+
failure: "signature_mismatch",
|
|
1148
|
+
reason: "No v1 signature matches the body: the secret is wrong or the body was altered.",
|
|
1149
|
+
timestamp: signedAt
|
|
1150
|
+
};
|
|
1151
|
+
const tolerance = (options.tolerance ?? AuthioDefaults.WebhookTolerance) * 1e3;
|
|
1152
|
+
const now = options.now ?? Date.now();
|
|
1153
|
+
if (Math.abs(now - signedAt.getTime()) > tolerance)
|
|
1154
|
+
return {
|
|
1155
|
+
valid: false,
|
|
1156
|
+
failure: "timestamp_outside_tolerance",
|
|
1157
|
+
reason: `The signature was made at ${signedAt.toISOString()}, more than ${tolerance / 1e3} seconds from now.`,
|
|
1158
|
+
timestamp: signedAt
|
|
1159
|
+
};
|
|
1160
|
+
return { valid: true, timestamp: signedAt };
|
|
1161
|
+
}
|
|
1162
|
+
async function computeWebhookSignatureHeader(rawBody, secret, timestamp) {
|
|
1163
|
+
if (!secret) throw new TypeError("secret is required.");
|
|
1164
|
+
const seconds2 = Math.floor((timestamp instanceof Date ? timestamp.getTime() : timestamp) / 1e3);
|
|
1165
|
+
const signature = await computeSignature(seconds2, rawBody, secret);
|
|
1166
|
+
return `t=${seconds2},v1=${[...signature].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
1167
|
+
}
|
|
1168
|
+
function parseWebhookEvent(rawBody) {
|
|
1169
|
+
const text = typeof rawBody === "string" ? rawBody : new TextDecoder().decode(rawBody);
|
|
1170
|
+
const value = JSON.parse(text);
|
|
1171
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new SyntaxError("The webhook body is empty.");
|
|
1172
|
+
if (typeof value.type !== "string" || !value.type) throw new SyntaxError("The webhook body has no type.");
|
|
1173
|
+
return value;
|
|
1174
|
+
}
|
|
1175
|
+
function tryParseWebhookEvent(rawBody) {
|
|
1176
|
+
if (rawBody === null || rawBody === void 0) return void 0;
|
|
1177
|
+
try {
|
|
1178
|
+
return parseWebhookEvent(rawBody);
|
|
1179
|
+
} catch {
|
|
1180
|
+
return void 0;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
function isWebhookEventOfType(event, ...types) {
|
|
1184
|
+
return types.includes(event.type);
|
|
1185
|
+
}
|
|
1186
|
+
var asUserEvent = (event) => dataWhen(event, AuthioWebhookEventTypes.UserCreated, AuthioWebhookEventTypes.UserUpdated, AuthioWebhookEventTypes.UserDeleted);
|
|
1187
|
+
var asLoginEvent = (event) => dataWhen(event, AuthioWebhookEventTypes.LoginSucceeded, AuthioWebhookEventTypes.LoginFailed);
|
|
1188
|
+
var asUserLockedOutEvent = (event) => dataWhen(event, AuthioWebhookEventTypes.UserLockedOut);
|
|
1189
|
+
var asTokenRevokedEvent = (event) => dataWhen(event, AuthioWebhookEventTypes.TokenRevoked);
|
|
1190
|
+
var asKeysRotatedEvent = (event) => dataWhen(event, AuthioWebhookEventTypes.KeysRotated);
|
|
1191
|
+
var asTokenReuseDetectedEvent = (event) => dataWhen(event, AuthioWebhookEventTypes.TokenReuseDetected);
|
|
1192
|
+
var asAdminResourceEvent = (event) => dataWhen(
|
|
1193
|
+
event,
|
|
1194
|
+
AuthioWebhookEventTypes.AdminResourceCreated,
|
|
1195
|
+
AuthioWebhookEventTypes.AdminResourceUpdated,
|
|
1196
|
+
AuthioWebhookEventTypes.AdminResourceDeleted
|
|
1197
|
+
);
|
|
1198
|
+
var asUserSuspensionEvent = (event) => dataWhen(event, AuthioWebhookEventTypes.UserSuspended, AuthioWebhookEventTypes.UserSuspensionEnded);
|
|
1199
|
+
var asPingEvent = (event) => dataWhen(event, AuthioWebhookEventTypes.Ping);
|
|
1200
|
+
function dataWhen(event, ...types) {
|
|
1201
|
+
return types.includes(event.type) && event.data !== null && event.data !== void 0 ? event.data : void 0;
|
|
1202
|
+
}
|
|
1203
|
+
function parseSignatureHeader(header) {
|
|
1204
|
+
let timestamp = 0;
|
|
1205
|
+
let timestamps = 0;
|
|
1206
|
+
const signatures = [];
|
|
1207
|
+
for (const raw of header.split(",")) {
|
|
1208
|
+
const part = raw.trim();
|
|
1209
|
+
if (!part) continue;
|
|
1210
|
+
const separator = part.indexOf("=");
|
|
1211
|
+
if (separator <= 0) return void 0;
|
|
1212
|
+
const key = part.slice(0, separator);
|
|
1213
|
+
const value = part.slice(separator + 1);
|
|
1214
|
+
if (key === "t") {
|
|
1215
|
+
timestamps++;
|
|
1216
|
+
if (!/^\d+$/.test(value)) return void 0;
|
|
1217
|
+
timestamp = Number(value);
|
|
1218
|
+
} else if (key === "v1" && value.length === 64 && /^[0-9a-fA-F]+$/.test(value)) {
|
|
1219
|
+
signatures.push(Uint8Array.from(value.match(/../g).map((pair) => parseInt(pair, 16))));
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
return timestamps === 1 && signatures.length > 0 && Number.isSafeInteger(timestamp) ? { timestamp, signatures } : void 0;
|
|
1223
|
+
}
|
|
1224
|
+
async function computeSignature(timestamp, rawBody, secret) {
|
|
1225
|
+
const prefix = encoder.encode(`${timestamp}.`);
|
|
1226
|
+
const body = typeof rawBody === "string" ? encoder.encode(rawBody) : rawBody;
|
|
1227
|
+
const message = new Uint8Array(prefix.length + body.length);
|
|
1228
|
+
message.set(prefix, 0);
|
|
1229
|
+
message.set(body, prefix.length);
|
|
1230
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
1231
|
+
return new Uint8Array(await crypto.subtle.sign("HMAC", key, message));
|
|
1232
|
+
}
|
|
1233
|
+
function constantTimeEquals(left, right) {
|
|
1234
|
+
if (left.length !== right.length) return false;
|
|
1235
|
+
let difference = 0;
|
|
1236
|
+
for (let index = 0; index < left.length; index++) difference |= left[index] ^ right[index];
|
|
1237
|
+
return difference === 0;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
exports.AuthioAsymmetricAlgorithms = AuthioAsymmetricAlgorithms;
|
|
1241
|
+
exports.AuthioClaimTypes = AuthioClaimTypes;
|
|
1242
|
+
exports.AuthioClientAssertionAlgorithms = AuthioClientAssertionAlgorithms;
|
|
1243
|
+
exports.AuthioConfigurationError = AuthioConfigurationError;
|
|
1244
|
+
exports.AuthioContentEncryptionAlgorithms = AuthioContentEncryptionAlgorithms;
|
|
1245
|
+
exports.AuthioDefaults = AuthioDefaults;
|
|
1246
|
+
exports.AuthioDiscoveryError = AuthioDiscoveryError;
|
|
1247
|
+
exports.AuthioError = AuthioError;
|
|
1248
|
+
exports.AuthioGrantTypes = AuthioGrantTypes;
|
|
1249
|
+
exports.AuthioKeyManagementAlgorithms = AuthioKeyManagementAlgorithms;
|
|
1250
|
+
exports.AuthioOAuthErrors = AuthioOAuthErrors;
|
|
1251
|
+
exports.AuthioProtocolError = AuthioProtocolError;
|
|
1252
|
+
exports.AuthioResourceAccess = AuthioResourceAccess;
|
|
1253
|
+
exports.AuthioResourceAccessClaims = AuthioResourceAccessClaims;
|
|
1254
|
+
exports.AuthioScopes = AuthioScopes;
|
|
1255
|
+
exports.AuthioTokenTypes = AuthioTokenTypes;
|
|
1256
|
+
exports.AuthioWebhookEventTypes = AuthioWebhookEventTypes;
|
|
1257
|
+
exports.AuthioWebhookHeaders = AuthioWebhookHeaders;
|
|
1258
|
+
exports.asAdminResourceEvent = asAdminResourceEvent;
|
|
1259
|
+
exports.asKeysRotatedEvent = asKeysRotatedEvent;
|
|
1260
|
+
exports.asLoginEvent = asLoginEvent;
|
|
1261
|
+
exports.asPingEvent = asPingEvent;
|
|
1262
|
+
exports.asTokenReuseDetectedEvent = asTokenReuseDetectedEvent;
|
|
1263
|
+
exports.asTokenRevokedEvent = asTokenRevokedEvent;
|
|
1264
|
+
exports.asUserEvent = asUserEvent;
|
|
1265
|
+
exports.asUserLockedOutEvent = asUserLockedOutEvent;
|
|
1266
|
+
exports.asUserSuspensionEvent = asUserSuspensionEvent;
|
|
1267
|
+
exports.computeWebhookSignatureHeader = computeWebhookSignatureHeader;
|
|
1268
|
+
exports.createAuthioClient = createAuthioClient;
|
|
1269
|
+
exports.createAuthioDiscoveryClient = createAuthioDiscoveryClient;
|
|
1270
|
+
exports.createAuthioTokenDecrypter = createAuthioTokenDecrypter;
|
|
1271
|
+
exports.escapeDataString = escapeDataString;
|
|
1272
|
+
exports.isAllowedUrl = isAllowedUrl;
|
|
1273
|
+
exports.isAuthioTokenEncrypted = isAuthioTokenEncrypted;
|
|
1274
|
+
exports.isWebhookEventOfType = isWebhookEventOfType;
|
|
1275
|
+
exports.parseResourceAccess = parseResourceAccess;
|
|
1276
|
+
exports.parseResourceAccessClaim = parseResourceAccessClaim;
|
|
1277
|
+
exports.parseWebhookEvent = parseWebhookEvent;
|
|
1278
|
+
exports.resolveConnection = resolveConnection;
|
|
1279
|
+
exports.resolveIssuer = resolveIssuer;
|
|
1280
|
+
exports.resourceAccessSourceOf = resourceAccessSourceOf;
|
|
1281
|
+
exports.seconds = seconds;
|
|
1282
|
+
exports.systemClock = systemClock;
|
|
1283
|
+
exports.toBasicAuthorization = toBasicAuthorization;
|
|
1284
|
+
exports.toClientCredentialsToken = toClientCredentialsToken;
|
|
1285
|
+
exports.toScopeString = toScopeString;
|
|
1286
|
+
exports.tryParseWebhookEvent = tryParseWebhookEvent;
|
|
1287
|
+
exports.verifyWebhookSignature = verifyWebhookSignature;
|
|
1288
|
+
//# sourceMappingURL=index.cjs.map
|
|
1289
|
+
//# sourceMappingURL=index.cjs.map
|