@authyon/server 0.2.0-beta.0 → 0.2.0-beta.2
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/LICENSE +21 -0
- package/README.md +126 -2
- package/dist/browser.cjs +21 -0
- package/dist/browser.d.cts +2 -0
- package/dist/browser.d.ts +2 -0
- package/dist/browser.js +4 -0
- package/dist/index.cjs +909 -70
- package/dist/index.d.cts +352 -65
- package/dist/index.d.ts +352 -65
- package/dist/index.js +895 -69
- package/package.json +58 -36
package/dist/index.cjs
CHANGED
|
@@ -20,43 +20,524 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
AuthorizationError: () => AuthorizationError,
|
|
24
|
+
AuthyonAbility: () => AuthyonAbility,
|
|
25
|
+
AuthyonAbilityBuilder: () => AuthyonAbilityBuilder,
|
|
23
26
|
AuthyonError: () => AuthyonError,
|
|
24
27
|
AuthyonServerClient: () => AuthyonServerClient,
|
|
28
|
+
AuthyonServerClientBuilder: () => AuthyonServerClientBuilder,
|
|
29
|
+
ErrorCodes: () => ErrorCodes,
|
|
30
|
+
FetchHttpAdapter: () => FetchHttpAdapter,
|
|
31
|
+
JwksTokenVerifier: () => JwksTokenVerifier,
|
|
32
|
+
LoggingHttpAdapter: () => LoggingHttpAdapter,
|
|
25
33
|
TenantScopedClient: () => TenantScopedClient,
|
|
26
|
-
|
|
34
|
+
TokenVerificationError: () => TokenVerificationError,
|
|
35
|
+
authorizeRequest: () => authorizeRequest,
|
|
36
|
+
authorizeToken: () => authorizeToken,
|
|
37
|
+
createAuthorizationErrorResponse: () => createAuthorizationErrorResponse,
|
|
38
|
+
createAuthyonAbility: () => createAuthyonAbility,
|
|
39
|
+
createAuthyonRules: () => createAuthyonRules,
|
|
40
|
+
createClient: () => createClient,
|
|
41
|
+
createExpressAuthorizationMiddleware: () => createExpressAuthorizationMiddleware,
|
|
42
|
+
hasPermission: () => hasPermission,
|
|
43
|
+
normalizeClientIp: () => normalizeClientIp
|
|
27
44
|
});
|
|
28
45
|
module.exports = __toCommonJS(index_exports);
|
|
29
46
|
|
|
30
|
-
//
|
|
47
|
+
// ../../internal/core/config/defaults.ts
|
|
48
|
+
var DEFAULT_BASE_URL = "https://api.authyon.com";
|
|
49
|
+
var DEFAULT_EXPIRY_SKEW_MS = 3e4;
|
|
50
|
+
|
|
51
|
+
// ../../internal/core/auth/expiringTokenProvider.ts
|
|
52
|
+
var ExpiringTokenProvider = class {
|
|
53
|
+
constructor(acquire, expirySkewMs = DEFAULT_EXPIRY_SKEW_MS) {
|
|
54
|
+
this.acquire = acquire;
|
|
55
|
+
this.expirySkewMs = expirySkewMs;
|
|
56
|
+
this.token = null;
|
|
57
|
+
this.inFlight = null;
|
|
58
|
+
}
|
|
59
|
+
async getAccessToken() {
|
|
60
|
+
if (this.token && Date.now() < this.token.expiresAt - this.expirySkewMs) {
|
|
61
|
+
return this.token.accessToken;
|
|
62
|
+
}
|
|
63
|
+
if (this.inFlight) return this.inFlight;
|
|
64
|
+
this.inFlight = this.acquire().then((token) => {
|
|
65
|
+
this.token = token;
|
|
66
|
+
return token.accessToken;
|
|
67
|
+
}).finally(() => {
|
|
68
|
+
this.inFlight = null;
|
|
69
|
+
});
|
|
70
|
+
return this.inFlight;
|
|
71
|
+
}
|
|
72
|
+
clear() {
|
|
73
|
+
this.token = null;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// ../../internal/core/errors/authyonError.ts
|
|
78
|
+
var ErrorCodes = {
|
|
79
|
+
Unknown: "unknown",
|
|
80
|
+
NetworkError: "request.network_error",
|
|
81
|
+
Timeout: "request.timeout",
|
|
82
|
+
SessionMalformed: "session.malformed",
|
|
83
|
+
NotAuthenticated: "auth.not_authenticated",
|
|
84
|
+
InvalidToken: "auth.invalid_token",
|
|
85
|
+
MissingToken: "auth.missing_token",
|
|
86
|
+
EmailTaken: "user.email_taken",
|
|
87
|
+
PasswordWeak: "user.password_weak",
|
|
88
|
+
PasswordPwned: "user.password_pwned",
|
|
89
|
+
RateLimited: "rate_limited"
|
|
90
|
+
};
|
|
31
91
|
var AuthyonError = class extends Error {
|
|
32
|
-
constructor(status, body) {
|
|
92
|
+
constructor(status, body, options = {}) {
|
|
33
93
|
super(body.detail ?? body.title ?? `Authyon request failed with status ${status}`);
|
|
34
94
|
this.name = "AuthyonError";
|
|
35
95
|
this.status = status;
|
|
36
96
|
this.code = body.code ?? "unknown";
|
|
37
97
|
this.title = body.title ?? "Error";
|
|
38
98
|
this.detail = body.detail;
|
|
99
|
+
this.requestId = options.requestId;
|
|
100
|
+
this.retryAfter = options.retryAfter;
|
|
101
|
+
this.cause = options.cause;
|
|
39
102
|
}
|
|
40
103
|
is(code) {
|
|
41
104
|
return this.code === code;
|
|
42
105
|
}
|
|
106
|
+
isAny(...codes) {
|
|
107
|
+
return codes.includes(this.code);
|
|
108
|
+
}
|
|
109
|
+
hasPrefix(prefix) {
|
|
110
|
+
return this.code.startsWith(prefix);
|
|
111
|
+
}
|
|
112
|
+
isStatus(...statuses) {
|
|
113
|
+
return statuses.includes(this.status);
|
|
114
|
+
}
|
|
115
|
+
/** Stable interpretation for UI decisions, retries, telemetry and support flows. */
|
|
116
|
+
interpret() {
|
|
117
|
+
const category = classifyError(this.status, this.code);
|
|
118
|
+
return {
|
|
119
|
+
category,
|
|
120
|
+
action: actionFor(category),
|
|
121
|
+
retryable: isRetryable(category),
|
|
122
|
+
...this.retryAfter !== void 0 ? { retryAfter: this.retryAfter } : {},
|
|
123
|
+
...this.requestId !== void 0 ? { requestId: this.requestId } : {}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
get category() {
|
|
127
|
+
return this.interpret().category;
|
|
128
|
+
}
|
|
129
|
+
get retryable() {
|
|
130
|
+
return this.interpret().retryable;
|
|
131
|
+
}
|
|
132
|
+
toJSON() {
|
|
133
|
+
return {
|
|
134
|
+
name: this.name,
|
|
135
|
+
message: this.message,
|
|
136
|
+
status: this.status,
|
|
137
|
+
code: this.code,
|
|
138
|
+
title: this.title,
|
|
139
|
+
detail: this.detail,
|
|
140
|
+
requestId: this.requestId,
|
|
141
|
+
retryAfter: this.retryAfter,
|
|
142
|
+
...this.interpret()
|
|
143
|
+
};
|
|
144
|
+
}
|
|
43
145
|
};
|
|
146
|
+
function classifyError(status, code) {
|
|
147
|
+
if (code === ErrorCodes.NetworkError) return "network";
|
|
148
|
+
if (code === ErrorCodes.Timeout || status === 408) return "timeout";
|
|
149
|
+
if (code === ErrorCodes.RateLimited || status === 429) return "rate_limit";
|
|
150
|
+
if (code === ErrorCodes.EmailTaken || status === 409) return "conflict";
|
|
151
|
+
if (code === ErrorCodes.PasswordWeak || code === ErrorCodes.PasswordPwned) return "validation";
|
|
152
|
+
if (code === ErrorCodes.SessionMalformed) return "server";
|
|
153
|
+
if (code.startsWith("auth.") || status === 401) return "authentication";
|
|
154
|
+
if (status === 403) return "authorization";
|
|
155
|
+
if (status === 404) return "not_found";
|
|
156
|
+
if (status === 400 || status === 422) return "validation";
|
|
157
|
+
if (status >= 500) return "server";
|
|
158
|
+
return "unknown";
|
|
159
|
+
}
|
|
160
|
+
function actionFor(category) {
|
|
161
|
+
switch (category) {
|
|
162
|
+
case "network":
|
|
163
|
+
case "timeout":
|
|
164
|
+
case "rate_limit":
|
|
165
|
+
case "server":
|
|
166
|
+
return "retry";
|
|
167
|
+
case "authentication":
|
|
168
|
+
return "reauthenticate";
|
|
169
|
+
case "authorization":
|
|
170
|
+
return "request_access";
|
|
171
|
+
case "validation":
|
|
172
|
+
return "fix_input";
|
|
173
|
+
case "not_found":
|
|
174
|
+
return "not_found";
|
|
175
|
+
case "conflict":
|
|
176
|
+
return "resolve_conflict";
|
|
177
|
+
case "unknown":
|
|
178
|
+
return "contact_support";
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function isRetryable(category) {
|
|
182
|
+
return category === "network" || category === "timeout" || category === "rate_limit" || category === "server";
|
|
183
|
+
}
|
|
44
184
|
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
function toQuery(params = {}) {
|
|
185
|
+
// ../../internal/core/http/query.ts
|
|
186
|
+
function appendQuery(path, params) {
|
|
187
|
+
if (!params) return path;
|
|
49
188
|
const query = new URLSearchParams();
|
|
50
|
-
for (const [key,
|
|
51
|
-
if (
|
|
189
|
+
for (const [key, value2] of Object.entries(params)) {
|
|
190
|
+
if (value2 !== void 0) query.set(key, String(value2));
|
|
191
|
+
}
|
|
192
|
+
const value = query.toString();
|
|
193
|
+
return value ? `${path}${path.includes("?") ? "&" : "?"}${value}` : path;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ../../internal/core/http/httpAdapter.ts
|
|
197
|
+
var FetchHttpAdapter = class {
|
|
198
|
+
constructor(fetchImpl = fetch.bind(globalThis)) {
|
|
199
|
+
this.fetchImpl = fetchImpl;
|
|
200
|
+
}
|
|
201
|
+
request(request) {
|
|
202
|
+
return this.fetchImpl(request.url, {
|
|
203
|
+
method: request.method,
|
|
204
|
+
headers: request.headers,
|
|
205
|
+
body: request.body,
|
|
206
|
+
signal: request.signal
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
var LoggingHttpAdapter = class {
|
|
211
|
+
constructor(adapter, options) {
|
|
212
|
+
this.adapter = adapter;
|
|
213
|
+
this.options = options;
|
|
214
|
+
}
|
|
215
|
+
async request(request) {
|
|
216
|
+
const startedAt = Date.now();
|
|
217
|
+
const eventBase = {
|
|
218
|
+
method: request.method,
|
|
219
|
+
url: sanitizeUrl(request.url)
|
|
220
|
+
};
|
|
221
|
+
this.log({ type: "request", ...eventBase, timestamp: startedAt });
|
|
222
|
+
try {
|
|
223
|
+
const response = await this.adapter.request(request);
|
|
224
|
+
this.log({
|
|
225
|
+
type: "response",
|
|
226
|
+
...eventBase,
|
|
227
|
+
status: response.status,
|
|
228
|
+
durationMs: Date.now() - startedAt,
|
|
229
|
+
requestId: response.headers.get("x-request-id") ?? response.headers.get("trace-id") ?? void 0,
|
|
230
|
+
timestamp: Date.now()
|
|
231
|
+
});
|
|
232
|
+
return response;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
this.log({
|
|
235
|
+
type: "error",
|
|
236
|
+
...eventBase,
|
|
237
|
+
errorName: error instanceof Error ? error.name : "UnknownError",
|
|
238
|
+
durationMs: Date.now() - startedAt,
|
|
239
|
+
timestamp: Date.now()
|
|
240
|
+
});
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
log(event) {
|
|
245
|
+
if (!this.options.enabled) return;
|
|
246
|
+
const logger = this.options.logger ?? defaultHttpLogger;
|
|
247
|
+
try {
|
|
248
|
+
logger(event);
|
|
249
|
+
} catch {
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
function defaultHttpLogger(event) {
|
|
254
|
+
console.debug("[Authyon HTTP]", event);
|
|
255
|
+
}
|
|
256
|
+
function sanitizeUrl(value) {
|
|
257
|
+
const url = new URL(value);
|
|
258
|
+
const queryKeys = /* @__PURE__ */ new Set();
|
|
259
|
+
url.searchParams.forEach((_value, key) => queryKeys.add(key));
|
|
260
|
+
url.search = queryKeys.size > 0 ? [...queryKeys].map((key) => `${encodeURIComponent(key)}=REDACTED`).join("&") : "";
|
|
261
|
+
return url.toString();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ../../internal/core/http/transport.ts
|
|
265
|
+
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
266
|
+
var SharedTransportError = class extends Error {
|
|
267
|
+
constructor(code, cause) {
|
|
268
|
+
super(code === "request.timeout" ? "Request timed out" : "Network request failed");
|
|
269
|
+
this.name = "SharedTransportError";
|
|
270
|
+
this.code = code;
|
|
271
|
+
this.cause = cause;
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
function createSharedTransport(options) {
|
|
275
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl, options.allowInsecureHttp ?? false);
|
|
276
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
277
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
|
|
278
|
+
throw new Error("Authyon: `timeoutMs` must be a non-negative finite number");
|
|
279
|
+
}
|
|
280
|
+
if (options.httpAdapter && options.fetch) {
|
|
281
|
+
throw new Error("Authyon: use either `httpAdapter` or `fetch`, not both");
|
|
282
|
+
}
|
|
283
|
+
const baseAdapter = options.httpAdapter ?? new FetchHttpAdapter(options.fetch);
|
|
284
|
+
const httpAdapter = options.httpLogger ? new LoggingHttpAdapter(baseAdapter, options.httpLogger) : baseAdapter;
|
|
285
|
+
return {
|
|
286
|
+
baseUrl,
|
|
287
|
+
async request(path, init) {
|
|
288
|
+
const controller = new AbortController();
|
|
289
|
+
const timeout = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : void 0;
|
|
290
|
+
try {
|
|
291
|
+
return await httpAdapter.request({
|
|
292
|
+
url: `${baseUrl}${path}`,
|
|
293
|
+
method: init.method ?? "GET",
|
|
294
|
+
headers: normalizeHeaders(init.headers),
|
|
295
|
+
body: init.body,
|
|
296
|
+
signal: controller.signal
|
|
297
|
+
});
|
|
298
|
+
} catch (cause) {
|
|
299
|
+
throw new SharedTransportError(
|
|
300
|
+
controller.signal.aborted ? ErrorCodes.Timeout : ErrorCodes.NetworkError,
|
|
301
|
+
cause
|
|
302
|
+
);
|
|
303
|
+
} finally {
|
|
304
|
+
if (timeout !== void 0) clearTimeout(timeout);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function normalizeHeaders(headers) {
|
|
310
|
+
const normalized = {};
|
|
311
|
+
new Headers(headers).forEach((value, key) => {
|
|
312
|
+
normalized[key] = value;
|
|
313
|
+
});
|
|
314
|
+
return normalized;
|
|
315
|
+
}
|
|
316
|
+
function responseMetadata(response) {
|
|
317
|
+
const retryAfterHeader = response.headers.get("retry-after");
|
|
318
|
+
const retryAfter = retryAfterHeader ? Number(retryAfterHeader) : void 0;
|
|
319
|
+
return {
|
|
320
|
+
requestId: response.headers.get("x-request-id") ?? response.headers.get("trace-id") ?? void 0,
|
|
321
|
+
retryAfter: Number.isFinite(retryAfter) ? retryAfter : void 0
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
function normalizeBaseUrl(value, allowInsecureHttp) {
|
|
325
|
+
let url;
|
|
326
|
+
try {
|
|
327
|
+
url = new URL(value);
|
|
328
|
+
} catch {
|
|
329
|
+
throw new Error("Authyon: `baseUrl` must be an absolute URL");
|
|
330
|
+
}
|
|
331
|
+
const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
|
|
332
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && (loopback || allowInsecureHttp))) {
|
|
333
|
+
throw new Error(
|
|
334
|
+
"Authyon: `baseUrl` must use HTTPS (HTTP is allowed only for loopback or with `allowInsecureHttp`)"
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
if (url.username || url.password || url.search || url.hash) {
|
|
338
|
+
throw new Error(
|
|
339
|
+
"Authyon: `baseUrl` cannot contain credentials, query parameters, or fragments"
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
return url.toString().replace(/\/+$/, "");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ../../internal/core/http/jsonHttpClient.ts
|
|
346
|
+
var JsonHttpClient = class {
|
|
347
|
+
constructor(transport) {
|
|
348
|
+
this.transport = transport;
|
|
349
|
+
}
|
|
350
|
+
async send(path, options = {}) {
|
|
351
|
+
const headers = { ...options.headers };
|
|
352
|
+
if (options.body !== void 0) headers["Content-Type"] = "application/json";
|
|
353
|
+
try {
|
|
354
|
+
return await this.transport.request(appendQuery(path, options.query), {
|
|
355
|
+
method: options.method ?? "GET",
|
|
356
|
+
headers,
|
|
357
|
+
body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
|
|
358
|
+
});
|
|
359
|
+
} catch (cause) {
|
|
360
|
+
if (!(cause instanceof SharedTransportError)) throw cause;
|
|
361
|
+
throw new AuthyonError(0, { code: cause.code, title: cause.message }, { cause: cause.cause });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
async parse(response) {
|
|
365
|
+
if (!response.ok) {
|
|
366
|
+
let body = {};
|
|
367
|
+
try {
|
|
368
|
+
body = await response.json();
|
|
369
|
+
} catch {
|
|
370
|
+
}
|
|
371
|
+
throw new AuthyonError(response.status, body, responseMetadata(response));
|
|
372
|
+
}
|
|
373
|
+
if (response.status === 204) return void 0;
|
|
374
|
+
return await response.json();
|
|
375
|
+
}
|
|
376
|
+
async request(path, options = {}) {
|
|
377
|
+
return this.parse(await this.send(path, options));
|
|
52
378
|
}
|
|
53
|
-
|
|
54
|
-
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// ../../internal/core/http/clientIp.ts
|
|
382
|
+
function clientIpHeaders(context) {
|
|
383
|
+
if (!context?.clientIp) return {};
|
|
384
|
+
return { "X-Forwarded-For": normalizeClientIp(context.clientIp) };
|
|
385
|
+
}
|
|
386
|
+
function normalizeClientIp(value) {
|
|
387
|
+
let ip = value.trim();
|
|
388
|
+
if (ip.startsWith("[") && ip.endsWith("]")) ip = ip.slice(1, -1);
|
|
389
|
+
if (!ip || ip.length > 45 || ip.includes(",") || /[\r\n\s]/.test(ip) || !/^[0-9a-f:.]+$/i.test(ip) || !ip.includes(".") && !ip.includes(":")) {
|
|
390
|
+
throw invalidClientIp();
|
|
391
|
+
}
|
|
392
|
+
try {
|
|
393
|
+
new URL(ip.includes(":") ? `http://[${ip}]` : `http://${ip}`);
|
|
394
|
+
} catch {
|
|
395
|
+
throw invalidClientIp();
|
|
396
|
+
}
|
|
397
|
+
return ip;
|
|
398
|
+
}
|
|
399
|
+
function invalidClientIp() {
|
|
400
|
+
return new Error("Authyon: `clientIp` must be a single valid IPv4 or IPv6 address");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// src/security/jwksTokenVerifier.ts
|
|
404
|
+
var import_jose = require("jose");
|
|
405
|
+
var SAFE_ASYMMETRIC_ALGORITHMS = [
|
|
406
|
+
"RS256",
|
|
407
|
+
"PS256",
|
|
408
|
+
"ES256",
|
|
409
|
+
"EdDSA"
|
|
410
|
+
];
|
|
411
|
+
var TokenVerificationError = class extends Error {
|
|
412
|
+
constructor(code, options = {}) {
|
|
413
|
+
super(messageForCode(code));
|
|
414
|
+
this.code = code;
|
|
415
|
+
this.name = "TokenVerificationError";
|
|
416
|
+
if (options.cause !== void 0) Object.defineProperty(this, "cause", { value: options.cause });
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
var JwksTokenVerifier = class {
|
|
420
|
+
constructor(options) {
|
|
421
|
+
this.options = options;
|
|
422
|
+
validateOptions(options);
|
|
423
|
+
this.algorithms = [...options.algorithms ?? ["RS256"]];
|
|
424
|
+
this.acceptedTokenTypes = new Set(options.acceptedTokenTypes ?? ["JWT", "at+jwt"]);
|
|
425
|
+
this.verifyKey = options.jwks ? (0, import_jose.createLocalJWKSet)(options.jwks) : (0, import_jose.createRemoteJWKSet)(new URL(options.jwksUri), {
|
|
426
|
+
timeoutDuration: options.jwksTimeoutMs ?? 5e3,
|
|
427
|
+
cacheMaxAge: options.jwksCacheMs ?? 6e5,
|
|
428
|
+
cooldownDuration: options.jwksCooldownMs ?? 3e4
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
async verifyAccessToken(token) {
|
|
432
|
+
if (!token || token.length > 16384) {
|
|
433
|
+
throw new TokenVerificationError("token.invalid");
|
|
434
|
+
}
|
|
435
|
+
try {
|
|
436
|
+
const result = await (0, import_jose.jwtVerify)(token, this.verifyKey, {
|
|
437
|
+
algorithms: this.algorithms,
|
|
438
|
+
issuer: this.options.issuer,
|
|
439
|
+
audience: this.options.audience,
|
|
440
|
+
clockTolerance: this.options.clockToleranceSeconds ?? 5,
|
|
441
|
+
maxTokenAge: this.options.maxTokenAgeSeconds ?? 3600,
|
|
442
|
+
requiredClaims: ["iss", "aud", "sub", "exp", "iat"]
|
|
443
|
+
});
|
|
444
|
+
validateTokenType(result, this.acceptedTokenTypes);
|
|
445
|
+
validateClaims(result.payload);
|
|
446
|
+
return { claims: result.payload, protectedHeader: result.protectedHeader };
|
|
447
|
+
} catch (cause) {
|
|
448
|
+
if (cause instanceof TokenVerificationError) throw cause;
|
|
449
|
+
throw new TokenVerificationError(errorCode(cause), { cause });
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
function validateTokenType(result, acceptedTokenTypes) {
|
|
454
|
+
const type = result.protectedHeader.typ;
|
|
455
|
+
if (!type || !acceptedTokenTypes.has(type)) {
|
|
456
|
+
throw new TokenVerificationError("token.invalid_claims");
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
function validateClaims(claims) {
|
|
460
|
+
if (typeof claims.sub !== "string" || !claims.sub) invalidClaims();
|
|
461
|
+
if (claims.permissions !== void 0 && !isStringArray(claims.permissions)) invalidClaims();
|
|
462
|
+
if (claims.roles !== void 0 && !isStringArray(claims.roles)) invalidClaims();
|
|
463
|
+
if (claims.scope !== void 0 && typeof claims.scope !== "string") invalidClaims();
|
|
464
|
+
}
|
|
465
|
+
function validateOptions(options) {
|
|
466
|
+
const issuer = secureUrl(options.issuer, "issuer");
|
|
467
|
+
if (Boolean(options.jwksUri) === Boolean(options.jwks)) {
|
|
468
|
+
throw new Error("Authyon: configure exactly one of `jwksUri` or `jwks`");
|
|
469
|
+
}
|
|
470
|
+
const jwks = options.jwksUri ? secureUrl(options.jwksUri, "jwksUri") : void 0;
|
|
471
|
+
if (!options.audience || Array.isArray(options.audience) && options.audience.length === 0) {
|
|
472
|
+
throw new Error("Authyon: JWT `audience` is required");
|
|
473
|
+
}
|
|
474
|
+
if (jwks && issuer.origin !== jwks.origin) {
|
|
475
|
+
throw new Error("Authyon: `issuer` and `jwksUri` must use the same trusted origin");
|
|
476
|
+
}
|
|
477
|
+
const algorithms = options.algorithms ?? ["RS256"];
|
|
478
|
+
if (algorithms.length === 0 || algorithms.some((algorithm) => !SAFE_ASYMMETRIC_ALGORITHMS.includes(algorithm))) {
|
|
479
|
+
throw new Error("Authyon: only approved asymmetric JWT algorithms are accepted");
|
|
480
|
+
}
|
|
481
|
+
if (options.acceptedTokenTypes?.length === 0 || options.acceptedTokenTypes?.some((type) => !type.trim())) {
|
|
482
|
+
throw new Error("Authyon: `acceptedTokenTypes` must contain non-empty values");
|
|
483
|
+
}
|
|
484
|
+
for (const [name, value] of [
|
|
485
|
+
["clockToleranceSeconds", options.clockToleranceSeconds],
|
|
486
|
+
["maxTokenAgeSeconds", options.maxTokenAgeSeconds],
|
|
487
|
+
["jwksTimeoutMs", options.jwksTimeoutMs],
|
|
488
|
+
["jwksCacheMs", options.jwksCacheMs],
|
|
489
|
+
["jwksCooldownMs", options.jwksCooldownMs]
|
|
490
|
+
]) {
|
|
491
|
+
if (value !== void 0 && (!Number.isFinite(value) || value < 0)) {
|
|
492
|
+
throw new Error(`Authyon: \`${name}\` must be a non-negative finite number`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
function secureUrl(value, name) {
|
|
497
|
+
let url;
|
|
498
|
+
try {
|
|
499
|
+
url = new URL(value);
|
|
500
|
+
} catch {
|
|
501
|
+
throw new Error(`Authyon: JWT \`${name}\` must be an absolute URL`);
|
|
502
|
+
}
|
|
503
|
+
if (url.protocol !== "https:" || url.username || url.password || url.hash) {
|
|
504
|
+
throw new Error(`Authyon: JWT \`${name}\` must be a credential-free HTTPS URL`);
|
|
505
|
+
}
|
|
506
|
+
return url;
|
|
507
|
+
}
|
|
508
|
+
function errorCode(error) {
|
|
509
|
+
const code = isRecord(error) && typeof error.code === "string" ? error.code : "";
|
|
510
|
+
if (code === "ERR_JWT_EXPIRED") return "token.expired";
|
|
511
|
+
if (code === "ERR_JWS_SIGNATURE_VERIFICATION_FAILED") return "token.invalid_signature";
|
|
512
|
+
if (code === "ERR_JWKS_TIMEOUT") {
|
|
513
|
+
return "token.key_unavailable";
|
|
514
|
+
}
|
|
515
|
+
if (code === "ERR_JWKS_NO_MATCHING_KEY") return "token.invalid_signature";
|
|
516
|
+
if (code === "ERR_JWT_CLAIM_VALIDATION_FAILED") {
|
|
517
|
+
const claim = isRecord(error) && typeof error.claim === "string" ? error.claim : "";
|
|
518
|
+
return claim === "nbf" ? "token.not_active" : "token.invalid_claims";
|
|
519
|
+
}
|
|
520
|
+
return "token.invalid";
|
|
521
|
+
}
|
|
522
|
+
function messageForCode(code) {
|
|
523
|
+
if (code === "token.key_unavailable") return "Token verification key is unavailable";
|
|
524
|
+
if (code === "token.expired") return "Token has expired";
|
|
525
|
+
if (code === "token.not_active") return "Token is not active yet";
|
|
526
|
+
return "Token verification failed";
|
|
55
527
|
}
|
|
528
|
+
function invalidClaims() {
|
|
529
|
+
throw new TokenVerificationError("token.invalid_claims");
|
|
530
|
+
}
|
|
531
|
+
function isStringArray(value) {
|
|
532
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
533
|
+
}
|
|
534
|
+
function isRecord(value) {
|
|
535
|
+
return typeof value === "object" && value !== null;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// src/client/authyonServerClient.ts
|
|
56
539
|
var AuthyonServerClient = class {
|
|
57
540
|
constructor(options = {}) {
|
|
58
|
-
this.environmentToken = null;
|
|
59
|
-
this.environmentTokenInFlight = null;
|
|
60
541
|
// ── Discovery ──────────────────────────────────────────────────────────────
|
|
61
542
|
this.discovery = {
|
|
62
543
|
/** GET /.well-known/jwks.json — the environment's public signing keys. */
|
|
@@ -183,8 +664,8 @@ var AuthyonServerClient = class {
|
|
|
183
664
|
)
|
|
184
665
|
},
|
|
185
666
|
tenants: {
|
|
186
|
-
/** GET /env/tenants — list tenants in the environment. */
|
|
187
|
-
list: () => this.request("/env/tenants", { envBearer: true }),
|
|
667
|
+
/** GET /env/tenants — paginated, searchable list of tenants in the environment. */
|
|
668
|
+
list: (params = {}) => this.request("/env/tenants", { envBearer: true, query: params }),
|
|
188
669
|
/** POST /env/tenants — create a tenant in the environment. */
|
|
189
670
|
create: (input) => this.request("/env/tenants", { method: "POST", envBearer: true, body: input }),
|
|
190
671
|
/** GET /env/tenants/{tenantId} — fetch a tenant by id. */
|
|
@@ -330,53 +811,55 @@ var AuthyonServerClient = class {
|
|
|
330
811
|
this.envKey = options.envKey;
|
|
331
812
|
this.clientId = options.clientId;
|
|
332
813
|
this.clientSecret = options.clientSecret;
|
|
333
|
-
this.
|
|
334
|
-
|
|
814
|
+
this.transport = createSharedTransport({
|
|
815
|
+
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
|
|
816
|
+
allowInsecureHttp: options.allowInsecureHttp,
|
|
817
|
+
timeoutMs: options.timeoutMs,
|
|
818
|
+
httpAdapter: options.httpAdapter,
|
|
819
|
+
httpLogger: options.httpLogger,
|
|
820
|
+
fetch: options.fetch
|
|
821
|
+
});
|
|
822
|
+
this.http = new JsonHttpClient(this.transport);
|
|
823
|
+
this.environmentTokenProvider = new ExpiringTokenProvider(async () => {
|
|
824
|
+
if (!this.clientId || !this.clientSecret) {
|
|
825
|
+
throw new Error(
|
|
826
|
+
"Authyon: `clientId`/`clientSecret` are required for environment management calls (mint a pair in the console, under the environment's OAuth clients)."
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
const token = await this.environmentAuth.token({
|
|
830
|
+
clientId: this.clientId,
|
|
831
|
+
clientSecret: this.clientSecret
|
|
832
|
+
});
|
|
833
|
+
return {
|
|
834
|
+
accessToken: token.access_token,
|
|
835
|
+
expiresAt: Date.now() + token.expires_in * 1e3
|
|
836
|
+
};
|
|
837
|
+
});
|
|
335
838
|
}
|
|
336
839
|
async request(path, options = {}) {
|
|
337
840
|
const headers = { ...options.headers };
|
|
338
|
-
if (options.body !== void 0) headers["Content-Type"] = "application/json";
|
|
339
841
|
if (options.envBearer) {
|
|
340
842
|
headers.Authorization = `Bearer ${await this.getEnvironmentAccessToken()}`;
|
|
341
843
|
}
|
|
342
844
|
if (this.envKey) headers["X-Authyon-Environment"] = this.envKey;
|
|
343
|
-
|
|
344
|
-
method: options.method ?? "GET",
|
|
345
|
-
headers,
|
|
346
|
-
body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
|
|
347
|
-
});
|
|
348
|
-
if (!response.ok) {
|
|
349
|
-
let body = {};
|
|
350
|
-
try {
|
|
351
|
-
body = await response.json();
|
|
352
|
-
} catch {
|
|
353
|
-
}
|
|
354
|
-
throw new AuthyonError(response.status, body);
|
|
355
|
-
}
|
|
356
|
-
if (response.status === 204) return void 0;
|
|
357
|
-
return await response.json();
|
|
845
|
+
return this.http.request(path, { ...options, headers });
|
|
358
846
|
}
|
|
359
847
|
/** Mints (and caches) an environment access token from `clientId`/`clientSecret`. */
|
|
360
848
|
async getEnvironmentAccessToken() {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
);
|
|
849
|
+
return this.environmentTokenProvider.getAccessToken();
|
|
850
|
+
}
|
|
851
|
+
/** Discovers Authyon's trusted issuer/JWKS and creates a cached local access-token verifier. */
|
|
852
|
+
async createJwksTokenVerifier(options) {
|
|
853
|
+
const { publishableKey, issuer, ...verifierOptions } = options;
|
|
854
|
+
const configuration = await this.discovery.openidConfiguration(publishableKey);
|
|
855
|
+
if (issuer && configuration.issuer !== issuer) {
|
|
856
|
+
throw new Error("Authyon: discovered JWT issuer does not match the configured issuer");
|
|
369
857
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
};
|
|
375
|
-
return token.access_token;
|
|
376
|
-
}).finally(() => {
|
|
377
|
-
this.environmentTokenInFlight = null;
|
|
858
|
+
return new JwksTokenVerifier({
|
|
859
|
+
...verifierOptions,
|
|
860
|
+
issuer: issuer ?? configuration.issuer,
|
|
861
|
+
jwksUri: configuration.jwks_uri
|
|
378
862
|
});
|
|
379
|
-
return this.environmentTokenInFlight;
|
|
380
863
|
}
|
|
381
864
|
// ── Token verification ────────────────────────────────────────────────────
|
|
382
865
|
//
|
|
@@ -387,12 +870,22 @@ var AuthyonServerClient = class {
|
|
|
387
870
|
// endpoint with only the `X-Authyon-Environment` header (no bearer)
|
|
388
871
|
// returns 401 with `WWW-Authenticate: Bearer`.
|
|
389
872
|
/** POST /auth/introspect — lightweight token introspection (RFC 7662). */
|
|
390
|
-
introspect(token) {
|
|
391
|
-
return this.request("/auth/introspect", {
|
|
873
|
+
introspect(token, context) {
|
|
874
|
+
return this.request("/auth/introspect", {
|
|
875
|
+
method: "POST",
|
|
876
|
+
envBearer: true,
|
|
877
|
+
body: { token },
|
|
878
|
+
headers: clientIpHeaders(context)
|
|
879
|
+
});
|
|
392
880
|
}
|
|
393
881
|
/** POST /auth/validate — recommended: cross-checks DB state, catches revocation immediately. */
|
|
394
|
-
async validate(token) {
|
|
395
|
-
const raw = await this.request("/auth/validate", {
|
|
882
|
+
async validate(token, context) {
|
|
883
|
+
const raw = await this.request("/auth/validate", {
|
|
884
|
+
method: "POST",
|
|
885
|
+
envBearer: true,
|
|
886
|
+
body: { token },
|
|
887
|
+
headers: clientIpHeaders(context)
|
|
888
|
+
});
|
|
396
889
|
return { valid: raw.valid, reason: raw.reason ?? null, user: raw.profile };
|
|
397
890
|
}
|
|
398
891
|
/**
|
|
@@ -415,8 +908,6 @@ var AuthyonServerClient = class {
|
|
|
415
908
|
};
|
|
416
909
|
var TenantScopedClient = class {
|
|
417
910
|
constructor(server, credentials) {
|
|
418
|
-
this.token = null;
|
|
419
|
-
this.tokenInFlight = null;
|
|
420
911
|
this.members = {
|
|
421
912
|
/**
|
|
422
913
|
* GET /tenant/members — members of the token's tenant.
|
|
@@ -473,23 +964,16 @@ var TenantScopedClient = class {
|
|
|
473
964
|
delete: (roleId) => this.request(`/tenant/roles/${encodeURIComponent(roleId)}`, { method: "DELETE" })
|
|
474
965
|
};
|
|
475
966
|
this.server = server;
|
|
476
|
-
this.
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
if (this.token && Date.now() < this.token.expiresAt - EXPIRY_SKEW_MS) {
|
|
480
|
-
return this.token.accessToken;
|
|
481
|
-
}
|
|
482
|
-
if (this.tokenInFlight) return this.tokenInFlight;
|
|
483
|
-
this.tokenInFlight = this.server.tenantAuth.token(this.credentials).then((token) => {
|
|
484
|
-
this.token = {
|
|
967
|
+
this.tokenProvider = new ExpiringTokenProvider(async () => {
|
|
968
|
+
const token = await this.server.tenantAuth.token(credentials);
|
|
969
|
+
return {
|
|
485
970
|
accessToken: token.access_token,
|
|
486
971
|
expiresAt: Date.now() + token.expires_in * 1e3
|
|
487
972
|
};
|
|
488
|
-
return token.access_token;
|
|
489
|
-
}).finally(() => {
|
|
490
|
-
this.tokenInFlight = null;
|
|
491
973
|
});
|
|
492
|
-
|
|
974
|
+
}
|
|
975
|
+
async getAccessToken() {
|
|
976
|
+
return this.tokenProvider.getAccessToken();
|
|
493
977
|
}
|
|
494
978
|
async request(path, options = {}) {
|
|
495
979
|
return this.server._requestAsTenant(await this.getAccessToken(), path, options);
|
|
@@ -498,10 +982,365 @@ var TenantScopedClient = class {
|
|
|
498
982
|
function createClient(options = {}) {
|
|
499
983
|
return new AuthyonServerClient(options);
|
|
500
984
|
}
|
|
985
|
+
|
|
986
|
+
// src/client/authyonServerClientBuilder.ts
|
|
987
|
+
var AuthyonServerClientBuilder = class {
|
|
988
|
+
constructor() {
|
|
989
|
+
this.options = {};
|
|
990
|
+
}
|
|
991
|
+
withEnvironmentKey(envKey) {
|
|
992
|
+
this.options.envKey = envKey;
|
|
993
|
+
return this;
|
|
994
|
+
}
|
|
995
|
+
withEnvironmentCredentials(clientId, clientSecret) {
|
|
996
|
+
this.options.clientId = clientId;
|
|
997
|
+
this.options.clientSecret = clientSecret;
|
|
998
|
+
return this;
|
|
999
|
+
}
|
|
1000
|
+
withBaseUrl(baseUrl, allowInsecureHttp = false) {
|
|
1001
|
+
this.options.baseUrl = baseUrl;
|
|
1002
|
+
this.options.allowInsecureHttp = allowInsecureHttp;
|
|
1003
|
+
return this;
|
|
1004
|
+
}
|
|
1005
|
+
withTimeout(timeoutMs) {
|
|
1006
|
+
this.options.timeoutMs = timeoutMs;
|
|
1007
|
+
return this;
|
|
1008
|
+
}
|
|
1009
|
+
withHttpAdapter(httpAdapter) {
|
|
1010
|
+
this.options.httpAdapter = httpAdapter;
|
|
1011
|
+
return this;
|
|
1012
|
+
}
|
|
1013
|
+
withHttpLogger(httpLogger) {
|
|
1014
|
+
this.options.httpLogger = httpLogger;
|
|
1015
|
+
return this;
|
|
1016
|
+
}
|
|
1017
|
+
build() {
|
|
1018
|
+
return new AuthyonServerClient({ ...this.options });
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
1021
|
+
|
|
1022
|
+
// ../../internal/core/authorization/ability.ts
|
|
1023
|
+
var AuthyonAbility = class {
|
|
1024
|
+
constructor(rules = [], detectSubjectType = defaultSubjectType) {
|
|
1025
|
+
this.detectSubjectType = detectSubjectType;
|
|
1026
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
1027
|
+
this.currentRules = rules.map(cloneRule);
|
|
1028
|
+
}
|
|
1029
|
+
get rules() {
|
|
1030
|
+
return this.currentRules;
|
|
1031
|
+
}
|
|
1032
|
+
can(action, subject, field) {
|
|
1033
|
+
const subjectType = typeof subject === "string" ? subject : this.detectSubjectType(subject);
|
|
1034
|
+
for (let index = this.currentRules.length - 1; index >= 0; index -= 1) {
|
|
1035
|
+
const rule = this.currentRules[index];
|
|
1036
|
+
if (!matchesToken(rule.action, action, "manage")) continue;
|
|
1037
|
+
if (!matchesToken(rule.subject, subjectType, "all")) continue;
|
|
1038
|
+
if (field && rule.fields && !rule.fields.some((value) => matchesField(value, field)))
|
|
1039
|
+
continue;
|
|
1040
|
+
if (rule.conditions) {
|
|
1041
|
+
if (typeof subject === "string" || !matchesConditions(subject, rule.conditions)) continue;
|
|
1042
|
+
}
|
|
1043
|
+
return !rule.inverted;
|
|
1044
|
+
}
|
|
1045
|
+
return false;
|
|
1046
|
+
}
|
|
1047
|
+
cannot(action, subject, field) {
|
|
1048
|
+
return !this.can(action, subject, field);
|
|
1049
|
+
}
|
|
1050
|
+
rulesFor(action, subject) {
|
|
1051
|
+
return this.currentRules.filter(
|
|
1052
|
+
(rule) => matchesToken(rule.action, action, "manage") && matchesToken(rule.subject, subject, "all")
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
update(rules) {
|
|
1056
|
+
this.currentRules = rules.map(cloneRule);
|
|
1057
|
+
for (const listener of this.listeners) listener(this.rules);
|
|
1058
|
+
}
|
|
1059
|
+
on(event, listener) {
|
|
1060
|
+
if (event !== "updated") return () => void 0;
|
|
1061
|
+
this.listeners.add(listener);
|
|
1062
|
+
return () => this.listeners.delete(listener);
|
|
1063
|
+
}
|
|
1064
|
+
};
|
|
1065
|
+
var AuthyonAbilityBuilder = class {
|
|
1066
|
+
constructor() {
|
|
1067
|
+
this.rules = [];
|
|
1068
|
+
}
|
|
1069
|
+
can(action, subject, conditions, fields) {
|
|
1070
|
+
this.rules.push({ action, subject, conditions, fields });
|
|
1071
|
+
return this;
|
|
1072
|
+
}
|
|
1073
|
+
cannot(action, subject, conditions, fields, reason) {
|
|
1074
|
+
this.rules.push({ action, subject, conditions, fields, reason, inverted: true });
|
|
1075
|
+
return this;
|
|
1076
|
+
}
|
|
1077
|
+
build(options = {}) {
|
|
1078
|
+
return new AuthyonAbility(this.rules, options.detectSubjectType);
|
|
1079
|
+
}
|
|
1080
|
+
};
|
|
1081
|
+
function createAuthyonAbility(source = {}, options = {}) {
|
|
1082
|
+
return new AuthyonAbility(createAuthyonRules(source, options), options.detectSubjectType);
|
|
1083
|
+
}
|
|
1084
|
+
function hasPermission(source, requiredPermission) {
|
|
1085
|
+
const requirement = permissionToRule(requiredPermission);
|
|
1086
|
+
if (!requirement || typeof requirement.action !== "string" || typeof requirement.subject !== "string") {
|
|
1087
|
+
return false;
|
|
1088
|
+
}
|
|
1089
|
+
const permissionSource = isPermissionList(source) ? { permissions: source } : source;
|
|
1090
|
+
return createAuthyonAbility(permissionSource).can(requirement.action, requirement.subject);
|
|
1091
|
+
}
|
|
1092
|
+
function isPermissionList(source) {
|
|
1093
|
+
return Array.isArray(source);
|
|
1094
|
+
}
|
|
1095
|
+
function createAuthyonRules(source = {}, options = {}) {
|
|
1096
|
+
const permissions = /* @__PURE__ */ new Set([
|
|
1097
|
+
...source.permissions ?? [],
|
|
1098
|
+
...source.scope?.split(/\s+/).filter(Boolean) ?? []
|
|
1099
|
+
]);
|
|
1100
|
+
const rules = [...permissions].map(permissionToRule).filter(isAbilityRule);
|
|
1101
|
+
for (const role of /* @__PURE__ */ new Set([...source.roles ?? [], ...options.roles ?? []])) {
|
|
1102
|
+
rules.push(...(options.roleRules?.[role] ?? []).map(cloneRule));
|
|
1103
|
+
}
|
|
1104
|
+
rules.push(...(options.rules ?? []).map(cloneRule));
|
|
1105
|
+
return rules;
|
|
1106
|
+
}
|
|
1107
|
+
function permissionToRule(permission) {
|
|
1108
|
+
const normalized = permission.trim();
|
|
1109
|
+
if (!normalized) return null;
|
|
1110
|
+
if (normalized === "*" || normalized === "*:*" || normalized === "all:manage") {
|
|
1111
|
+
return { action: "manage", subject: "all" };
|
|
1112
|
+
}
|
|
1113
|
+
const separator = normalized.lastIndexOf(":");
|
|
1114
|
+
if (separator <= 0 || separator === normalized.length - 1) return null;
|
|
1115
|
+
const subject = normalized.slice(0, separator);
|
|
1116
|
+
const action = normalized.slice(separator + 1);
|
|
1117
|
+
return {
|
|
1118
|
+
action: action === "*" ? "manage" : action,
|
|
1119
|
+
subject: subject === "*" ? "all" : subject
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
function matchesToken(value, expected, wildcard) {
|
|
1123
|
+
return (Array.isArray(value) ? value : [value]).some(
|
|
1124
|
+
(candidate) => candidate === expected || candidate === wildcard || candidate === "*" || matchesSegments(candidate, expected)
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
function matchesSegments(pattern, value) {
|
|
1128
|
+
const patternSegments = pattern.split(":");
|
|
1129
|
+
const valueSegments = value.split(":");
|
|
1130
|
+
return patternSegments.length === valueSegments.length && patternSegments.every((segment, index) => segment === "*" || segment === valueSegments[index]);
|
|
1131
|
+
}
|
|
1132
|
+
function matchesField(pattern, field) {
|
|
1133
|
+
if (pattern === "*" || pattern === field) return true;
|
|
1134
|
+
return pattern.endsWith(".*") && field.startsWith(pattern.slice(0, -1));
|
|
1135
|
+
}
|
|
1136
|
+
function matchesConditions(subject, conditions) {
|
|
1137
|
+
return Object.entries(conditions).every(([path, expected]) => {
|
|
1138
|
+
if (path === "$and" && Array.isArray(expected)) {
|
|
1139
|
+
return expected.every(
|
|
1140
|
+
(condition) => matchesConditions(subject, condition)
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
if (path === "$or" && Array.isArray(expected)) {
|
|
1144
|
+
return expected.some(
|
|
1145
|
+
(condition) => matchesConditions(subject, condition)
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1148
|
+
return matchesValue(readPath(subject, path), expected);
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
function matchesValue(actual, expected) {
|
|
1152
|
+
if (!isRecord2(expected) || !Object.keys(expected).some((key) => key.startsWith("$"))) {
|
|
1153
|
+
return isRecord2(expected) && isRecord2(actual) ? matchesConditions(actual, expected) : Object.is(actual, expected);
|
|
1154
|
+
}
|
|
1155
|
+
return Object.entries(expected).every(([operator, operand]) => {
|
|
1156
|
+
switch (operator) {
|
|
1157
|
+
case "$eq":
|
|
1158
|
+
return Object.is(actual, operand);
|
|
1159
|
+
case "$ne":
|
|
1160
|
+
return !Object.is(actual, operand);
|
|
1161
|
+
case "$in":
|
|
1162
|
+
return Array.isArray(operand) && operand.some((value) => Object.is(actual, value));
|
|
1163
|
+
case "$nin":
|
|
1164
|
+
return Array.isArray(operand) && !operand.some((value) => Object.is(actual, value));
|
|
1165
|
+
case "$gt":
|
|
1166
|
+
return typeof actual === "number" && typeof operand === "number" && actual > operand;
|
|
1167
|
+
case "$gte":
|
|
1168
|
+
return typeof actual === "number" && typeof operand === "number" && actual >= operand;
|
|
1169
|
+
case "$lt":
|
|
1170
|
+
return typeof actual === "number" && typeof operand === "number" && actual < operand;
|
|
1171
|
+
case "$lte":
|
|
1172
|
+
return typeof actual === "number" && typeof operand === "number" && actual <= operand;
|
|
1173
|
+
case "$exists":
|
|
1174
|
+
return operand ? actual !== void 0 : actual === void 0;
|
|
1175
|
+
default:
|
|
1176
|
+
return false;
|
|
1177
|
+
}
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
function readPath(value, path) {
|
|
1181
|
+
return path.split(".").reduce((current, part) => isRecord2(current) ? current[part] : void 0, value);
|
|
1182
|
+
}
|
|
1183
|
+
function defaultSubjectType(subject) {
|
|
1184
|
+
const explicit = subject.__type ?? subject.type ?? subject.kind;
|
|
1185
|
+
if (typeof explicit === "string") return explicit;
|
|
1186
|
+
const constructorName = subject.constructor?.name;
|
|
1187
|
+
return typeof constructorName === "string" ? constructorName : "Object";
|
|
1188
|
+
}
|
|
1189
|
+
function cloneRule(rule) {
|
|
1190
|
+
return {
|
|
1191
|
+
...rule,
|
|
1192
|
+
action: Array.isArray(rule.action) ? [...rule.action] : rule.action,
|
|
1193
|
+
subject: Array.isArray(rule.subject) ? [...rule.subject] : rule.subject,
|
|
1194
|
+
fields: rule.fields ? [...rule.fields] : void 0
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
function isRecord2(value) {
|
|
1198
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1199
|
+
}
|
|
1200
|
+
function isAbilityRule(value) {
|
|
1201
|
+
return value !== null;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// src/integrations/authorization.ts
|
|
1205
|
+
var AuthorizationError = class extends Error {
|
|
1206
|
+
constructor(status, code, message) {
|
|
1207
|
+
super(message);
|
|
1208
|
+
this.status = status;
|
|
1209
|
+
this.code = code;
|
|
1210
|
+
this.name = "AuthorizationError";
|
|
1211
|
+
}
|
|
1212
|
+
};
|
|
1213
|
+
async function authorizeToken(client, token, options = {}) {
|
|
1214
|
+
if (!token) throw unauthenticated();
|
|
1215
|
+
let userId;
|
|
1216
|
+
let permissions = [];
|
|
1217
|
+
let roles = [];
|
|
1218
|
+
let scope;
|
|
1219
|
+
if (options.verification === "jwks") {
|
|
1220
|
+
if (!options.jwksVerifier) {
|
|
1221
|
+
throw new Error("Authyon: `jwksVerifier` is required for JWKS verification");
|
|
1222
|
+
}
|
|
1223
|
+
let verified;
|
|
1224
|
+
try {
|
|
1225
|
+
verified = await options.jwksVerifier.verifyAccessToken(token);
|
|
1226
|
+
} catch (error) {
|
|
1227
|
+
if (!(error instanceof TokenVerificationError)) throw error;
|
|
1228
|
+
if (error.code === "token.key_unavailable") {
|
|
1229
|
+
throw new AuthorizationError(
|
|
1230
|
+
503,
|
|
1231
|
+
"authorization.unavailable",
|
|
1232
|
+
"Token verification is temporarily unavailable"
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
1235
|
+
throw unauthenticated();
|
|
1236
|
+
}
|
|
1237
|
+
const { claims } = verified;
|
|
1238
|
+
userId = claims.sub;
|
|
1239
|
+
permissions = claims.permissions ?? [];
|
|
1240
|
+
roles = claims.roles ?? [];
|
|
1241
|
+
scope = claims.scope;
|
|
1242
|
+
} else if (options.verification === "introspect") {
|
|
1243
|
+
const identity = await client.introspect(token, { clientIp: options.clientIp });
|
|
1244
|
+
if (!identity.active) throw unauthenticated();
|
|
1245
|
+
userId = identity.sub;
|
|
1246
|
+
permissions = identity.permissions ?? [];
|
|
1247
|
+
roles = identity.roles ?? [];
|
|
1248
|
+
scope = identity.scope;
|
|
1249
|
+
} else {
|
|
1250
|
+
const identity = await client.validate(token, { clientIp: options.clientIp });
|
|
1251
|
+
if (!identity.valid || !identity.user) throw unauthenticated();
|
|
1252
|
+
userId = identity.user.id;
|
|
1253
|
+
permissions = identity.user.permissions ?? [];
|
|
1254
|
+
}
|
|
1255
|
+
const ability = createAuthyonAbility(
|
|
1256
|
+
{ permissions, roles, scope },
|
|
1257
|
+
{
|
|
1258
|
+
rules: options.rules,
|
|
1259
|
+
roleRules: options.roleRules,
|
|
1260
|
+
roles: options.roles,
|
|
1261
|
+
detectSubjectType: options.detectSubjectType
|
|
1262
|
+
}
|
|
1263
|
+
);
|
|
1264
|
+
if (options.requirement && !canAccess(ability, options.requirement)) {
|
|
1265
|
+
throw new AuthorizationError(403, "authorization.forbidden", "Permission denied");
|
|
1266
|
+
}
|
|
1267
|
+
return { token, userId, ability, permissions, roles };
|
|
1268
|
+
}
|
|
1269
|
+
function authorizeRequest(client, request, options = {}) {
|
|
1270
|
+
const clientIp = options.resolveClientIp?.(request) ?? options.clientIp;
|
|
1271
|
+
return authorizeToken(client, bearerToken(request.headers.get("authorization")), {
|
|
1272
|
+
...options,
|
|
1273
|
+
clientIp
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
function createExpressAuthorizationMiddleware(client, options = {}) {
|
|
1277
|
+
return async (request, response, next) => {
|
|
1278
|
+
try {
|
|
1279
|
+
const header = request.headers.authorization;
|
|
1280
|
+
const clientIp = resolveExpressClientIp(request, options);
|
|
1281
|
+
const context = await authorizeToken(
|
|
1282
|
+
client,
|
|
1283
|
+
bearerToken(Array.isArray(header) ? header[0] : header),
|
|
1284
|
+
{ ...options, clientIp }
|
|
1285
|
+
);
|
|
1286
|
+
request[options.requestProperty ?? "authyon"] = context;
|
|
1287
|
+
next();
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
if (options.passErrorsToNext || !(error instanceof AuthorizationError)) {
|
|
1290
|
+
next(error);
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
response.status(error.status).json({ code: error.code, message: error.message });
|
|
1294
|
+
}
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
function resolveExpressClientIp(request, options) {
|
|
1298
|
+
const resolved = options.resolveClientIp?.(request) ?? (options.trustProxy ? request.ip : request.socket?.remoteAddress);
|
|
1299
|
+
return resolved ? normalizeClientIp(stripIpv4Prefix(resolved)) : void 0;
|
|
1300
|
+
}
|
|
1301
|
+
function stripIpv4Prefix(value) {
|
|
1302
|
+
return value.startsWith("::ffff:") ? value.slice(7) : value;
|
|
1303
|
+
}
|
|
1304
|
+
function createAuthorizationErrorResponse(error) {
|
|
1305
|
+
if (!(error instanceof AuthorizationError)) throw error;
|
|
1306
|
+
return Response.json({ code: error.code, message: error.message }, { status: error.status });
|
|
1307
|
+
}
|
|
1308
|
+
function canAccess(ability, requirement) {
|
|
1309
|
+
return ability.can(requirement.action, requirement.subject, requirement.field);
|
|
1310
|
+
}
|
|
1311
|
+
function bearerToken(header) {
|
|
1312
|
+
const match = header?.match(/^Bearer\s+(.+)$/i);
|
|
1313
|
+
if (!match?.[1]) throw unauthenticated();
|
|
1314
|
+
return match[1].trim();
|
|
1315
|
+
}
|
|
1316
|
+
function unauthenticated() {
|
|
1317
|
+
return new AuthorizationError(
|
|
1318
|
+
401,
|
|
1319
|
+
"authorization.unauthenticated",
|
|
1320
|
+
"A valid bearer token is required"
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
501
1323
|
// Annotate the CommonJS export names for ESM import in node:
|
|
502
1324
|
0 && (module.exports = {
|
|
1325
|
+
AuthorizationError,
|
|
1326
|
+
AuthyonAbility,
|
|
1327
|
+
AuthyonAbilityBuilder,
|
|
503
1328
|
AuthyonError,
|
|
504
1329
|
AuthyonServerClient,
|
|
1330
|
+
AuthyonServerClientBuilder,
|
|
1331
|
+
ErrorCodes,
|
|
1332
|
+
FetchHttpAdapter,
|
|
1333
|
+
JwksTokenVerifier,
|
|
1334
|
+
LoggingHttpAdapter,
|
|
505
1335
|
TenantScopedClient,
|
|
506
|
-
|
|
1336
|
+
TokenVerificationError,
|
|
1337
|
+
authorizeRequest,
|
|
1338
|
+
authorizeToken,
|
|
1339
|
+
createAuthorizationErrorResponse,
|
|
1340
|
+
createAuthyonAbility,
|
|
1341
|
+
createAuthyonRules,
|
|
1342
|
+
createClient,
|
|
1343
|
+
createExpressAuthorizationMiddleware,
|
|
1344
|
+
hasPermission,
|
|
1345
|
+
normalizeClientIp
|
|
507
1346
|
});
|