@authyon/server 0.2.0-beta.2 → 0.2.0-beta.4
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 +166 -1
- package/dist/authorization-Cwl7YFEZ.d.cts +976 -0
- package/dist/authorization-Cwl7YFEZ.d.ts +976 -0
- package/dist/chunk-24IFDJ73.js +490 -0
- package/dist/index.cjs +348 -26
- package/dist/index.d.cts +14 -751
- package/dist/index.d.ts +14 -751
- package/dist/index.js +357 -488
- package/dist/next.cjs +328 -0
- package/dist/next.d.cts +26 -0
- package/dist/next.d.ts +26 -0
- package/dist/next.js +47 -0
- package/package.json +20 -5
package/dist/index.js
CHANGED
|
@@ -1,32 +1,299 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AuthorizationError,
|
|
3
|
+
AuthyonAbility,
|
|
4
|
+
AuthyonAbilityBuilder,
|
|
5
|
+
JwksTokenVerifier,
|
|
6
|
+
TokenVerificationError,
|
|
7
|
+
__privateAdd,
|
|
8
|
+
__privateGet,
|
|
9
|
+
__privateSet,
|
|
10
|
+
authorizeRequest,
|
|
11
|
+
authorizeToken,
|
|
12
|
+
clientIpHeaders,
|
|
13
|
+
createAuthorizationErrorResponse,
|
|
14
|
+
createAuthyonAbility,
|
|
15
|
+
createAuthyonRules,
|
|
16
|
+
createExpressAuthorizationMiddleware,
|
|
17
|
+
hasPermission,
|
|
18
|
+
normalizeClientIp
|
|
19
|
+
} from "./chunk-24IFDJ73.js";
|
|
20
|
+
|
|
21
|
+
// src/client/credentialManagement.ts
|
|
22
|
+
function segment(value) {
|
|
23
|
+
if (typeof value !== "string" || !value.trim() || value === "." || value === ".." || [...value].some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127)) {
|
|
24
|
+
throw new TypeError("Authyon: invalid route identifier.");
|
|
25
|
+
}
|
|
26
|
+
return encodeURIComponent(value);
|
|
27
|
+
}
|
|
28
|
+
function permissionsBody(input) {
|
|
29
|
+
if (input.permissions !== void 0 && input.scopes !== void 0) {
|
|
30
|
+
throw new TypeError("Authyon: supply permissions or scopes, not both.");
|
|
31
|
+
}
|
|
32
|
+
const values = input.permissions ?? input.scopes;
|
|
33
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
34
|
+
throw new TypeError("Authyon: explicit credential permissions are required.");
|
|
35
|
+
}
|
|
36
|
+
const permissions = values.map((value) => {
|
|
37
|
+
if (typeof value !== "string") throw new TypeError("Authyon: invalid permission.");
|
|
38
|
+
const normalized = value.trim().toLowerCase();
|
|
39
|
+
if (!/^(?:\*|[a-z][a-z0-9_-]*):(?:\*|[a-z][a-z0-9_-]*):(?:\*|[a-z][a-z0-9_-]*)$/u.test(normalized)) {
|
|
40
|
+
throw new TypeError("Authyon: permissions must have namespace:resource:action format.");
|
|
41
|
+
}
|
|
42
|
+
return normalized;
|
|
43
|
+
});
|
|
44
|
+
return { permissions: [...new Set(permissions)] };
|
|
45
|
+
}
|
|
46
|
+
function lifetimeBody(input) {
|
|
47
|
+
if (input.lifetimeDays === void 0 || input.lifetimeDays === null) return {};
|
|
48
|
+
if (!Number.isInteger(input.lifetimeDays) || input.lifetimeDays < 1 || input.lifetimeDays > 3650) {
|
|
49
|
+
throw new RangeError("Authyon: lifetimeDays must be between 1 and 3650.");
|
|
50
|
+
}
|
|
51
|
+
return { lifetimeDays: input.lifetimeDays };
|
|
52
|
+
}
|
|
53
|
+
function credentialQuery(options = {}) {
|
|
54
|
+
const { skip, take, search } = options;
|
|
55
|
+
if (skip !== void 0 && (!Number.isInteger(skip) || skip < 0 || skip > 2147483647))
|
|
56
|
+
throw new RangeError("Authyon: invalid skip.");
|
|
57
|
+
if (take !== void 0 && (!Number.isInteger(take) || take < 1 || take > 100))
|
|
58
|
+
throw new RangeError("Authyon: take must be between 1 and 100.");
|
|
59
|
+
if (search !== void 0 && (typeof search !== "string" || search.trim().length > 200))
|
|
60
|
+
throw new RangeError("Authyon: search must contain at most 200 characters.");
|
|
61
|
+
return { search: search?.trim() || void 0, skip, take };
|
|
62
|
+
}
|
|
63
|
+
function credentialSummary(value) {
|
|
64
|
+
return {
|
|
65
|
+
id: value.id,
|
|
66
|
+
clientId: value.clientId,
|
|
67
|
+
description: value.description,
|
|
68
|
+
createdAt: value.createdAt,
|
|
69
|
+
lastUsedAt: value.lastUsedAt,
|
|
70
|
+
revokedAt: value.revokedAt,
|
|
71
|
+
expiresAt: value.expiresAt ?? null,
|
|
72
|
+
isActive: value.isActive,
|
|
73
|
+
permissions: [...value.permissions]
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function credentialDetail(value) {
|
|
77
|
+
return {
|
|
78
|
+
...credentialSummary(value),
|
|
79
|
+
createdBy: value.createdBy == null ? null : {
|
|
80
|
+
id: value.createdBy.id,
|
|
81
|
+
type: value.createdBy.type,
|
|
82
|
+
displayName: value.createdBy.displayName
|
|
83
|
+
},
|
|
84
|
+
updatedAt: value.updatedAt,
|
|
85
|
+
secretRotatedAt: value.secretRotatedAt,
|
|
86
|
+
expiresAt: value.expiresAt ?? null,
|
|
87
|
+
ageSeconds: value.ageSeconds,
|
|
88
|
+
lifetimeSeconds: value.lifetimeSeconds,
|
|
89
|
+
accessTokenLifetimeSeconds: value.accessTokenLifetimeSeconds
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async function credentialPage(request, path, options = {}) {
|
|
93
|
+
const query = credentialQuery(options);
|
|
94
|
+
const page = await request(path, { query });
|
|
95
|
+
if (!page || !Array.isArray(page.data))
|
|
96
|
+
throw new TypeError("Authyon: expected a paginated credential response.");
|
|
97
|
+
return {
|
|
98
|
+
data: page.data.map(credentialSummary),
|
|
99
|
+
...page.perPage === void 0 ? {} : { perPage: page.perPage },
|
|
100
|
+
pageSize: page.pageSize,
|
|
101
|
+
total: page.total,
|
|
102
|
+
pages: page.pages,
|
|
103
|
+
hasNext: page.hasNext,
|
|
104
|
+
hasPrev: page.hasPrev
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
async function allCredentials(request, path) {
|
|
108
|
+
const result = [];
|
|
109
|
+
const seen = /* @__PURE__ */ new Set();
|
|
110
|
+
let skip = 0;
|
|
111
|
+
for (let iteration = 0; iteration < 1e3; iteration++) {
|
|
112
|
+
const page = await request(
|
|
113
|
+
path,
|
|
114
|
+
iteration === 0 ? {} : { query: { skip, take: 100 } }
|
|
115
|
+
);
|
|
116
|
+
if (Array.isArray(page)) {
|
|
117
|
+
if (iteration !== 0)
|
|
118
|
+
throw new Error("Authyon: credential pagination changed during listing.");
|
|
119
|
+
return page.map(credentialSummary);
|
|
120
|
+
}
|
|
121
|
+
if (!page || !Array.isArray(page.data))
|
|
122
|
+
throw new TypeError("Authyon: invalid credential page.");
|
|
123
|
+
for (const row of page.data) {
|
|
124
|
+
if (seen.has(row.id))
|
|
125
|
+
throw new Error("Authyon: credential pages overlapped; retry the listing.");
|
|
126
|
+
seen.add(row.id);
|
|
127
|
+
result.push(credentialSummary(row));
|
|
128
|
+
}
|
|
129
|
+
if (!page.hasNext) return result;
|
|
130
|
+
if (page.data.length === 0) throw new Error("Authyon: credential pagination made no progress.");
|
|
131
|
+
skip += page.data.length;
|
|
132
|
+
}
|
|
133
|
+
throw new Error("Authyon: use listPage for more than 1000 credential pages.");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/client/scopedManagementClient.ts
|
|
137
|
+
function scopedRequest(request, source) {
|
|
138
|
+
return async (path, options = {}) => {
|
|
139
|
+
const token = typeof source === "function" ? await source() : source;
|
|
140
|
+
if (typeof token !== "string" || !token.trim() || /\s/u.test(token))
|
|
141
|
+
throw new TypeError("Authyon: a valid access token is required.");
|
|
142
|
+
return request(path, {
|
|
143
|
+
...options,
|
|
144
|
+
headers: { ...options.headers, Authorization: `Bearer ${token}` }
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function environmentPath(scope) {
|
|
149
|
+
return `/platform/workspaces/${segment(scope.workspaceId)}/environments/${segment(scope.environmentId)}`;
|
|
150
|
+
}
|
|
151
|
+
function tenantPath(scope) {
|
|
152
|
+
return `${environmentPath(scope)}/tenants/${segment(scope.tenantId)}`;
|
|
153
|
+
}
|
|
154
|
+
function credentialPath(scope) {
|
|
155
|
+
return `${scope.tenantId === void 0 ? environmentPath(scope) : tenantPath(scope)}/credentials`;
|
|
156
|
+
}
|
|
157
|
+
function workspacePath(id) {
|
|
158
|
+
return `/platform/workspaces/${segment(id)}`;
|
|
159
|
+
}
|
|
160
|
+
function workspaceRole(role) {
|
|
161
|
+
if (role !== "admin" && role !== "auditor")
|
|
162
|
+
throw new TypeError("Authyon: workspace role must be admin or auditor.");
|
|
163
|
+
return role;
|
|
164
|
+
}
|
|
165
|
+
var _request;
|
|
166
|
+
var PlatformScopedClient = class {
|
|
167
|
+
/** @hidden */
|
|
168
|
+
constructor(request, token) {
|
|
169
|
+
__privateAdd(this, _request);
|
|
170
|
+
this.credentials = {
|
|
171
|
+
list: (scope, options = {}) => credentialPage(__privateGet(this, _request), credentialPath(scope), options),
|
|
172
|
+
get: async (scope, credentialId) => credentialDetail(await __privateGet(this, _request).call(this, `${credentialPath(scope)}/${segment(credentialId)}`)),
|
|
173
|
+
create: (scope, input) => __privateGet(this, _request).call(this, credentialPath(scope), {
|
|
174
|
+
method: "POST",
|
|
175
|
+
body: { description: input.description, ...lifetimeBody(input), ...permissionsBody(input) }
|
|
176
|
+
}),
|
|
177
|
+
updatePermissions: (scope, credentialId, input) => __privateGet(this, _request).call(this, `${credentialPath(scope)}/${segment(credentialId)}/permissions`, {
|
|
178
|
+
method: "PUT",
|
|
179
|
+
body: permissionsBody(input)
|
|
180
|
+
}),
|
|
181
|
+
updateScopes: (scope, credentialId, scopes) => this.credentials.updatePermissions(scope, credentialId, { scopes }),
|
|
182
|
+
/** Returns a new secret once. API may require an explicit step-up first. */
|
|
183
|
+
rotate: (scope, credentialId) => __privateGet(this, _request).call(this, `${credentialPath(scope)}/${segment(credentialId)}/rotate`, { method: "POST" }),
|
|
184
|
+
revoke: (scope, credentialId) => __privateGet(this, _request).call(this, `${credentialPath(scope)}/${segment(credentialId)}`, { method: "DELETE" })
|
|
185
|
+
};
|
|
186
|
+
this.workspaces = {
|
|
187
|
+
team: (workspaceId) => __privateGet(this, _request).call(this, `${workspacePath(workspaceId)}/team`),
|
|
188
|
+
members: {
|
|
189
|
+
invite: (workspaceId, input) => __privateGet(this, _request).call(this, `${workspacePath(workspaceId)}/invites`, {
|
|
190
|
+
method: "POST",
|
|
191
|
+
body: { email: input.email, role: workspaceRole(input.role) }
|
|
192
|
+
}),
|
|
193
|
+
/** memberId comes from team.members; it is not the platform userId. */
|
|
194
|
+
remove: (workspaceId, memberId) => __privateGet(this, _request).call(this, `${workspacePath(workspaceId)}/members/${segment(memberId)}`, {
|
|
195
|
+
method: "DELETE"
|
|
196
|
+
}),
|
|
197
|
+
changeRole: (workspaceId, memberId, role) => __privateGet(this, _request).call(this, `${workspacePath(workspaceId)}/members/${segment(memberId)}`, {
|
|
198
|
+
method: "PATCH",
|
|
199
|
+
body: { role: workspaceRole(role) }
|
|
200
|
+
})
|
|
201
|
+
},
|
|
202
|
+
invites: {
|
|
203
|
+
create: (workspaceId, input) => this.workspaces.members.invite(workspaceId, input),
|
|
204
|
+
revoke: (workspaceId, inviteId) => __privateGet(this, _request).call(this, `${workspacePath(workspaceId)}/invites/${segment(inviteId)}`, {
|
|
205
|
+
method: "DELETE"
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
this.tenants = {
|
|
210
|
+
members: {
|
|
211
|
+
list: (scope, options = {}) => __privateGet(this, _request).call(this, `${tenantPath(scope)}/members`, { query: options }),
|
|
212
|
+
add: (scope, userId, roles) => __privateGet(this, _request).call(this, `${tenantPath(scope)}/members`, { method: "POST", body: { userId, roles } }),
|
|
213
|
+
remove: (scope, userId) => __privateGet(this, _request).call(this, `${tenantPath(scope)}/members/${segment(userId)}`, { method: "DELETE" })
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
this.auth = {
|
|
217
|
+
/** Explicitly confirms the current platform session; passwords are not retained. */
|
|
218
|
+
stepUp: (currentPassword) => __privateGet(this, _request).call(this, "/platform/auth/step-up", { method: "POST", body: { currentPassword } })
|
|
219
|
+
};
|
|
220
|
+
__privateSet(this, _request, scopedRequest(request, token));
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
_request = new WeakMap();
|
|
224
|
+
var _request2;
|
|
225
|
+
var UserScopedClient = class {
|
|
226
|
+
/** @hidden */
|
|
227
|
+
constructor(request, token) {
|
|
228
|
+
__privateAdd(this, _request2);
|
|
229
|
+
this.tenants = {
|
|
230
|
+
members: {
|
|
231
|
+
list: (tenantId, options = {}) => __privateGet(this, _request2).call(this, `/auth/tenants/${segment(tenantId)}/members`, { query: options }),
|
|
232
|
+
/** Adds an existing environment user by email; does not send an invitation email. */
|
|
233
|
+
invite: (tenantId, input) => __privateGet(this, _request2).call(this, `/auth/tenants/${segment(tenantId)}/members`, {
|
|
234
|
+
method: "POST",
|
|
235
|
+
body: { email: input.email, roles: input.roles }
|
|
236
|
+
}),
|
|
237
|
+
remove: (tenantId, userId) => __privateGet(this, _request2).call(this, `/auth/tenants/${segment(tenantId)}/members/${segment(userId)}`, {
|
|
238
|
+
method: "DELETE"
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
__privateSet(this, _request2, scopedRequest(request, token));
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
_request2 = new WeakMap();
|
|
246
|
+
var _request3;
|
|
247
|
+
var WorkspaceInvitesClient = class {
|
|
248
|
+
/** @hidden */
|
|
249
|
+
constructor(request) {
|
|
250
|
+
__privateAdd(this, _request3);
|
|
251
|
+
__privateSet(this, _request3, request);
|
|
252
|
+
}
|
|
253
|
+
preview(token) {
|
|
254
|
+
return __privateGet(this, _request3).call(this, `/platform/workspace-invites/${segment(token)}`);
|
|
255
|
+
}
|
|
256
|
+
accept(token, input = {}) {
|
|
257
|
+
return __privateGet(this, _request3).call(this, `/platform/workspace-invites/${segment(token)}/accept`, {
|
|
258
|
+
method: "POST",
|
|
259
|
+
body: { name: input.name, password: input.password }
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
_request3 = new WeakMap();
|
|
264
|
+
|
|
1
265
|
// ../../internal/core/config/defaults.ts
|
|
2
266
|
var DEFAULT_BASE_URL = "https://api.authyon.com";
|
|
3
267
|
var DEFAULT_EXPIRY_SKEW_MS = 3e4;
|
|
4
268
|
|
|
5
269
|
// ../../internal/core/auth/expiringTokenProvider.ts
|
|
270
|
+
var _token, _inFlight;
|
|
6
271
|
var ExpiringTokenProvider = class {
|
|
7
272
|
constructor(acquire, expirySkewMs = DEFAULT_EXPIRY_SKEW_MS) {
|
|
8
273
|
this.acquire = acquire;
|
|
9
274
|
this.expirySkewMs = expirySkewMs;
|
|
10
|
-
this
|
|
11
|
-
this
|
|
275
|
+
__privateAdd(this, _token, null);
|
|
276
|
+
__privateAdd(this, _inFlight, null);
|
|
12
277
|
}
|
|
13
278
|
async getAccessToken() {
|
|
14
|
-
if (this
|
|
15
|
-
return this.
|
|
279
|
+
if (__privateGet(this, _token) && Date.now() < __privateGet(this, _token).expiresAt - this.expirySkewMs) {
|
|
280
|
+
return __privateGet(this, _token).accessToken;
|
|
16
281
|
}
|
|
17
|
-
if (this
|
|
18
|
-
this
|
|
19
|
-
this
|
|
282
|
+
if (__privateGet(this, _inFlight)) return __privateGet(this, _inFlight);
|
|
283
|
+
__privateSet(this, _inFlight, this.acquire().then((token) => {
|
|
284
|
+
__privateSet(this, _token, token);
|
|
20
285
|
return token.accessToken;
|
|
21
286
|
}).finally(() => {
|
|
22
|
-
this
|
|
23
|
-
});
|
|
24
|
-
return this
|
|
287
|
+
__privateSet(this, _inFlight, null);
|
|
288
|
+
}));
|
|
289
|
+
return __privateGet(this, _inFlight);
|
|
25
290
|
}
|
|
26
291
|
clear() {
|
|
27
|
-
this
|
|
292
|
+
__privateSet(this, _token, null);
|
|
28
293
|
}
|
|
29
294
|
};
|
|
295
|
+
_token = new WeakMap();
|
|
296
|
+
_inFlight = new WeakMap();
|
|
30
297
|
|
|
31
298
|
// ../../internal/core/errors/authyonError.ts
|
|
32
299
|
var ErrorCodes = {
|
|
@@ -157,7 +424,8 @@ var FetchHttpAdapter = class {
|
|
|
157
424
|
method: request.method,
|
|
158
425
|
headers: request.headers,
|
|
159
426
|
body: request.body,
|
|
160
|
-
signal: request.signal
|
|
427
|
+
signal: request.signal,
|
|
428
|
+
redirect: "error"
|
|
161
429
|
});
|
|
162
430
|
}
|
|
163
431
|
};
|
|
@@ -209,6 +477,10 @@ function defaultHttpLogger(event) {
|
|
|
209
477
|
}
|
|
210
478
|
function sanitizeUrl(value) {
|
|
211
479
|
const url = new URL(value);
|
|
480
|
+
url.username = "";
|
|
481
|
+
url.password = "";
|
|
482
|
+
url.hash = "";
|
|
483
|
+
url.pathname = url.pathname.replace(/(\/platform\/workspace-invites\/)[^/]+/u, "$1REDACTED");
|
|
212
484
|
const queryKeys = /* @__PURE__ */ new Set();
|
|
213
485
|
url.searchParams.forEach((_value, key) => queryKeys.add(key));
|
|
214
486
|
url.search = queryKeys.size > 0 ? [...queryKeys].map((key) => `${encodeURIComponent(key)}=REDACTED`).join("&") : "";
|
|
@@ -332,170 +604,11 @@ var JsonHttpClient = class {
|
|
|
332
604
|
}
|
|
333
605
|
};
|
|
334
606
|
|
|
335
|
-
// ../../internal/core/http/clientIp.ts
|
|
336
|
-
function clientIpHeaders(context) {
|
|
337
|
-
if (!context?.clientIp) return {};
|
|
338
|
-
return { "X-Forwarded-For": normalizeClientIp(context.clientIp) };
|
|
339
|
-
}
|
|
340
|
-
function normalizeClientIp(value) {
|
|
341
|
-
let ip = value.trim();
|
|
342
|
-
if (ip.startsWith("[") && ip.endsWith("]")) ip = ip.slice(1, -1);
|
|
343
|
-
if (!ip || ip.length > 45 || ip.includes(",") || /[\r\n\s]/.test(ip) || !/^[0-9a-f:.]+$/i.test(ip) || !ip.includes(".") && !ip.includes(":")) {
|
|
344
|
-
throw invalidClientIp();
|
|
345
|
-
}
|
|
346
|
-
try {
|
|
347
|
-
new URL(ip.includes(":") ? `http://[${ip}]` : `http://${ip}`);
|
|
348
|
-
} catch {
|
|
349
|
-
throw invalidClientIp();
|
|
350
|
-
}
|
|
351
|
-
return ip;
|
|
352
|
-
}
|
|
353
|
-
function invalidClientIp() {
|
|
354
|
-
return new Error("Authyon: `clientIp` must be a single valid IPv4 or IPv6 address");
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// src/security/jwksTokenVerifier.ts
|
|
358
|
-
import {
|
|
359
|
-
createLocalJWKSet,
|
|
360
|
-
createRemoteJWKSet,
|
|
361
|
-
jwtVerify
|
|
362
|
-
} from "jose";
|
|
363
|
-
var SAFE_ASYMMETRIC_ALGORITHMS = [
|
|
364
|
-
"RS256",
|
|
365
|
-
"PS256",
|
|
366
|
-
"ES256",
|
|
367
|
-
"EdDSA"
|
|
368
|
-
];
|
|
369
|
-
var TokenVerificationError = class extends Error {
|
|
370
|
-
constructor(code, options = {}) {
|
|
371
|
-
super(messageForCode(code));
|
|
372
|
-
this.code = code;
|
|
373
|
-
this.name = "TokenVerificationError";
|
|
374
|
-
if (options.cause !== void 0) Object.defineProperty(this, "cause", { value: options.cause });
|
|
375
|
-
}
|
|
376
|
-
};
|
|
377
|
-
var JwksTokenVerifier = class {
|
|
378
|
-
constructor(options) {
|
|
379
|
-
this.options = options;
|
|
380
|
-
validateOptions(options);
|
|
381
|
-
this.algorithms = [...options.algorithms ?? ["RS256"]];
|
|
382
|
-
this.acceptedTokenTypes = new Set(options.acceptedTokenTypes ?? ["JWT", "at+jwt"]);
|
|
383
|
-
this.verifyKey = options.jwks ? createLocalJWKSet(options.jwks) : createRemoteJWKSet(new URL(options.jwksUri), {
|
|
384
|
-
timeoutDuration: options.jwksTimeoutMs ?? 5e3,
|
|
385
|
-
cacheMaxAge: options.jwksCacheMs ?? 6e5,
|
|
386
|
-
cooldownDuration: options.jwksCooldownMs ?? 3e4
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
|
-
async verifyAccessToken(token) {
|
|
390
|
-
if (!token || token.length > 16384) {
|
|
391
|
-
throw new TokenVerificationError("token.invalid");
|
|
392
|
-
}
|
|
393
|
-
try {
|
|
394
|
-
const result = await jwtVerify(token, this.verifyKey, {
|
|
395
|
-
algorithms: this.algorithms,
|
|
396
|
-
issuer: this.options.issuer,
|
|
397
|
-
audience: this.options.audience,
|
|
398
|
-
clockTolerance: this.options.clockToleranceSeconds ?? 5,
|
|
399
|
-
maxTokenAge: this.options.maxTokenAgeSeconds ?? 3600,
|
|
400
|
-
requiredClaims: ["iss", "aud", "sub", "exp", "iat"]
|
|
401
|
-
});
|
|
402
|
-
validateTokenType(result, this.acceptedTokenTypes);
|
|
403
|
-
validateClaims(result.payload);
|
|
404
|
-
return { claims: result.payload, protectedHeader: result.protectedHeader };
|
|
405
|
-
} catch (cause) {
|
|
406
|
-
if (cause instanceof TokenVerificationError) throw cause;
|
|
407
|
-
throw new TokenVerificationError(errorCode(cause), { cause });
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
};
|
|
411
|
-
function validateTokenType(result, acceptedTokenTypes) {
|
|
412
|
-
const type = result.protectedHeader.typ;
|
|
413
|
-
if (!type || !acceptedTokenTypes.has(type)) {
|
|
414
|
-
throw new TokenVerificationError("token.invalid_claims");
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
function validateClaims(claims) {
|
|
418
|
-
if (typeof claims.sub !== "string" || !claims.sub) invalidClaims();
|
|
419
|
-
if (claims.permissions !== void 0 && !isStringArray(claims.permissions)) invalidClaims();
|
|
420
|
-
if (claims.roles !== void 0 && !isStringArray(claims.roles)) invalidClaims();
|
|
421
|
-
if (claims.scope !== void 0 && typeof claims.scope !== "string") invalidClaims();
|
|
422
|
-
}
|
|
423
|
-
function validateOptions(options) {
|
|
424
|
-
const issuer = secureUrl(options.issuer, "issuer");
|
|
425
|
-
if (Boolean(options.jwksUri) === Boolean(options.jwks)) {
|
|
426
|
-
throw new Error("Authyon: configure exactly one of `jwksUri` or `jwks`");
|
|
427
|
-
}
|
|
428
|
-
const jwks = options.jwksUri ? secureUrl(options.jwksUri, "jwksUri") : void 0;
|
|
429
|
-
if (!options.audience || Array.isArray(options.audience) && options.audience.length === 0) {
|
|
430
|
-
throw new Error("Authyon: JWT `audience` is required");
|
|
431
|
-
}
|
|
432
|
-
if (jwks && issuer.origin !== jwks.origin) {
|
|
433
|
-
throw new Error("Authyon: `issuer` and `jwksUri` must use the same trusted origin");
|
|
434
|
-
}
|
|
435
|
-
const algorithms = options.algorithms ?? ["RS256"];
|
|
436
|
-
if (algorithms.length === 0 || algorithms.some((algorithm) => !SAFE_ASYMMETRIC_ALGORITHMS.includes(algorithm))) {
|
|
437
|
-
throw new Error("Authyon: only approved asymmetric JWT algorithms are accepted");
|
|
438
|
-
}
|
|
439
|
-
if (options.acceptedTokenTypes?.length === 0 || options.acceptedTokenTypes?.some((type) => !type.trim())) {
|
|
440
|
-
throw new Error("Authyon: `acceptedTokenTypes` must contain non-empty values");
|
|
441
|
-
}
|
|
442
|
-
for (const [name, value] of [
|
|
443
|
-
["clockToleranceSeconds", options.clockToleranceSeconds],
|
|
444
|
-
["maxTokenAgeSeconds", options.maxTokenAgeSeconds],
|
|
445
|
-
["jwksTimeoutMs", options.jwksTimeoutMs],
|
|
446
|
-
["jwksCacheMs", options.jwksCacheMs],
|
|
447
|
-
["jwksCooldownMs", options.jwksCooldownMs]
|
|
448
|
-
]) {
|
|
449
|
-
if (value !== void 0 && (!Number.isFinite(value) || value < 0)) {
|
|
450
|
-
throw new Error(`Authyon: \`${name}\` must be a non-negative finite number`);
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
function secureUrl(value, name) {
|
|
455
|
-
let url;
|
|
456
|
-
try {
|
|
457
|
-
url = new URL(value);
|
|
458
|
-
} catch {
|
|
459
|
-
throw new Error(`Authyon: JWT \`${name}\` must be an absolute URL`);
|
|
460
|
-
}
|
|
461
|
-
if (url.protocol !== "https:" || url.username || url.password || url.hash) {
|
|
462
|
-
throw new Error(`Authyon: JWT \`${name}\` must be a credential-free HTTPS URL`);
|
|
463
|
-
}
|
|
464
|
-
return url;
|
|
465
|
-
}
|
|
466
|
-
function errorCode(error) {
|
|
467
|
-
const code = isRecord(error) && typeof error.code === "string" ? error.code : "";
|
|
468
|
-
if (code === "ERR_JWT_EXPIRED") return "token.expired";
|
|
469
|
-
if (code === "ERR_JWS_SIGNATURE_VERIFICATION_FAILED") return "token.invalid_signature";
|
|
470
|
-
if (code === "ERR_JWKS_TIMEOUT") {
|
|
471
|
-
return "token.key_unavailable";
|
|
472
|
-
}
|
|
473
|
-
if (code === "ERR_JWKS_NO_MATCHING_KEY") return "token.invalid_signature";
|
|
474
|
-
if (code === "ERR_JWT_CLAIM_VALIDATION_FAILED") {
|
|
475
|
-
const claim = isRecord(error) && typeof error.claim === "string" ? error.claim : "";
|
|
476
|
-
return claim === "nbf" ? "token.not_active" : "token.invalid_claims";
|
|
477
|
-
}
|
|
478
|
-
return "token.invalid";
|
|
479
|
-
}
|
|
480
|
-
function messageForCode(code) {
|
|
481
|
-
if (code === "token.key_unavailable") return "Token verification key is unavailable";
|
|
482
|
-
if (code === "token.expired") return "Token has expired";
|
|
483
|
-
if (code === "token.not_active") return "Token is not active yet";
|
|
484
|
-
return "Token verification failed";
|
|
485
|
-
}
|
|
486
|
-
function invalidClaims() {
|
|
487
|
-
throw new TokenVerificationError("token.invalid_claims");
|
|
488
|
-
}
|
|
489
|
-
function isStringArray(value) {
|
|
490
|
-
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
491
|
-
}
|
|
492
|
-
function isRecord(value) {
|
|
493
|
-
return typeof value === "object" && value !== null;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
607
|
// src/client/authyonServerClient.ts
|
|
608
|
+
var _clientSecret;
|
|
497
609
|
var AuthyonServerClient = class {
|
|
498
610
|
constructor(options = {}) {
|
|
611
|
+
__privateAdd(this, _clientSecret);
|
|
499
612
|
// ── Discovery ──────────────────────────────────────────────────────────────
|
|
500
613
|
this.discovery = {
|
|
501
614
|
/** GET /.well-known/jwks.json — the environment's public signing keys. */
|
|
@@ -531,6 +644,9 @@ var AuthyonServerClient = class {
|
|
|
531
644
|
}
|
|
532
645
|
})
|
|
533
646
|
};
|
|
647
|
+
this.workspaceInvites = new WorkspaceInvitesClient(
|
|
648
|
+
(path, options) => this.http.request(path, options)
|
|
649
|
+
);
|
|
534
650
|
// ── Environment management (users, tenants, roles, permissions, audit) ───
|
|
535
651
|
this.environment = {
|
|
536
652
|
users: {
|
|
@@ -622,6 +738,47 @@ var AuthyonServerClient = class {
|
|
|
622
738
|
)
|
|
623
739
|
},
|
|
624
740
|
tenants: {
|
|
741
|
+
credentials: {
|
|
742
|
+
/** GET /env/tenants/{tenantId}/credentials. Returns metadata only, including revoked credentials. */
|
|
743
|
+
list: (tenantId) => allCredentials(
|
|
744
|
+
(path, options) => this.request(path, { ...options, envBearer: true }),
|
|
745
|
+
`/env/tenants/${segment(tenantId)}/credentials`
|
|
746
|
+
),
|
|
747
|
+
/** Paginated searchable listing. Prefer this over loading every credential. */
|
|
748
|
+
listPage: (tenantId, options = {}) => credentialPage(
|
|
749
|
+
(path, requestOptions) => this.request(path, { ...requestOptions, envBearer: true }),
|
|
750
|
+
`/env/tenants/${segment(tenantId)}/credentials`,
|
|
751
|
+
options
|
|
752
|
+
),
|
|
753
|
+
get: async (tenantId, credentialId) => credentialDetail(
|
|
754
|
+
await this.request(
|
|
755
|
+
`/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}`,
|
|
756
|
+
{ envBearer: true }
|
|
757
|
+
)
|
|
758
|
+
),
|
|
759
|
+
/** Explicit scopes/permissions; only issuance returns the client secret. */
|
|
760
|
+
create: (tenantId, input) => this.request(`/env/tenants/${segment(tenantId)}/credentials`, {
|
|
761
|
+
method: "POST",
|
|
762
|
+
envBearer: true,
|
|
763
|
+
body: { description: input.description, ...lifetimeBody(input), ...permissionsBody(input) }
|
|
764
|
+
}),
|
|
765
|
+
/** Returns a new secret once while keeping the same client id. */
|
|
766
|
+
rotate: (tenantId, credentialId) => this.request(
|
|
767
|
+
`/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}/rotate`,
|
|
768
|
+
{ method: "POST", envBearer: true }
|
|
769
|
+
),
|
|
770
|
+
updatePermissions: (tenantId, credentialId, input) => this.request(
|
|
771
|
+
`/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}/permissions`,
|
|
772
|
+
{ method: "PUT", envBearer: true, body: permissionsBody(input) }
|
|
773
|
+
),
|
|
774
|
+
updateScopes: (tenantId, credentialId, scopes) => this.environment.tenants.credentials.updatePermissions(tenantId, credentialId, {
|
|
775
|
+
scopes
|
|
776
|
+
}),
|
|
777
|
+
revoke: (tenantId, credentialId) => this.request(`/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}`, {
|
|
778
|
+
method: "DELETE",
|
|
779
|
+
envBearer: true
|
|
780
|
+
})
|
|
781
|
+
},
|
|
625
782
|
/** GET /env/tenants — paginated, searchable list of tenants in the environment. */
|
|
626
783
|
list: (params = {}) => this.request("/env/tenants", { envBearer: true, query: params }),
|
|
627
784
|
/** POST /env/tenants — create a tenant in the environment. */
|
|
@@ -768,7 +925,7 @@ var AuthyonServerClient = class {
|
|
|
768
925
|
};
|
|
769
926
|
this.envKey = options.envKey;
|
|
770
927
|
this.clientId = options.clientId;
|
|
771
|
-
this
|
|
928
|
+
__privateSet(this, _clientSecret, options.clientSecret);
|
|
772
929
|
this.transport = createSharedTransport({
|
|
773
930
|
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
|
|
774
931
|
allowInsecureHttp: options.allowInsecureHttp,
|
|
@@ -779,14 +936,14 @@ var AuthyonServerClient = class {
|
|
|
779
936
|
});
|
|
780
937
|
this.http = new JsonHttpClient(this.transport);
|
|
781
938
|
this.environmentTokenProvider = new ExpiringTokenProvider(async () => {
|
|
782
|
-
if (!this.clientId || !this
|
|
939
|
+
if (!this.clientId || !__privateGet(this, _clientSecret)) {
|
|
783
940
|
throw new Error(
|
|
784
941
|
"Authyon: `clientId`/`clientSecret` are required for environment management calls (mint a pair in the console, under the environment's OAuth clients)."
|
|
785
942
|
);
|
|
786
943
|
}
|
|
787
944
|
const token = await this.environmentAuth.token({
|
|
788
945
|
clientId: this.clientId,
|
|
789
|
-
clientSecret: this
|
|
946
|
+
clientSecret: __privateGet(this, _clientSecret)
|
|
790
947
|
});
|
|
791
948
|
return {
|
|
792
949
|
accessToken: token.access_token,
|
|
@@ -855,6 +1012,14 @@ var AuthyonServerClient = class {
|
|
|
855
1012
|
tenant(credentials) {
|
|
856
1013
|
return new TenantScopedClient(this, credentials);
|
|
857
1014
|
}
|
|
1015
|
+
/** Uses only the supplied platform-user access token. */
|
|
1016
|
+
platform(token) {
|
|
1017
|
+
return new PlatformScopedClient((path, options) => this.http.request(path, options), token);
|
|
1018
|
+
}
|
|
1019
|
+
/** Uses an end-user token; the configured environment key selects the environment. */
|
|
1020
|
+
user(token) {
|
|
1021
|
+
return new UserScopedClient((path, options) => this.request(path, options), token);
|
|
1022
|
+
}
|
|
858
1023
|
/** @internal used by {@link TenantScopedClient} to share the base URL/fetch/error handling. */
|
|
859
1024
|
_requestAsTenant(accessToken, path, options = {}) {
|
|
860
1025
|
return this.request(path, {
|
|
@@ -864,6 +1029,7 @@ var AuthyonServerClient = class {
|
|
|
864
1029
|
});
|
|
865
1030
|
}
|
|
866
1031
|
};
|
|
1032
|
+
_clientSecret = new WeakMap();
|
|
867
1033
|
var TenantScopedClient = class {
|
|
868
1034
|
constructor(server, credentials) {
|
|
869
1035
|
this.members = {
|
|
@@ -942,342 +1108,42 @@ function createClient(options = {}) {
|
|
|
942
1108
|
}
|
|
943
1109
|
|
|
944
1110
|
// src/client/authyonServerClientBuilder.ts
|
|
1111
|
+
var _options;
|
|
945
1112
|
var AuthyonServerClientBuilder = class {
|
|
946
1113
|
constructor() {
|
|
947
|
-
this
|
|
1114
|
+
__privateAdd(this, _options, {});
|
|
948
1115
|
}
|
|
949
1116
|
withEnvironmentKey(envKey) {
|
|
950
|
-
this.
|
|
1117
|
+
__privateGet(this, _options).envKey = envKey;
|
|
951
1118
|
return this;
|
|
952
1119
|
}
|
|
953
1120
|
withEnvironmentCredentials(clientId, clientSecret) {
|
|
954
|
-
this.
|
|
955
|
-
this.
|
|
1121
|
+
__privateGet(this, _options).clientId = clientId;
|
|
1122
|
+
__privateGet(this, _options).clientSecret = clientSecret;
|
|
956
1123
|
return this;
|
|
957
1124
|
}
|
|
958
1125
|
withBaseUrl(baseUrl, allowInsecureHttp = false) {
|
|
959
|
-
this.
|
|
960
|
-
this.
|
|
1126
|
+
__privateGet(this, _options).baseUrl = baseUrl;
|
|
1127
|
+
__privateGet(this, _options).allowInsecureHttp = allowInsecureHttp;
|
|
961
1128
|
return this;
|
|
962
1129
|
}
|
|
963
1130
|
withTimeout(timeoutMs) {
|
|
964
|
-
this.
|
|
1131
|
+
__privateGet(this, _options).timeoutMs = timeoutMs;
|
|
965
1132
|
return this;
|
|
966
1133
|
}
|
|
967
1134
|
withHttpAdapter(httpAdapter) {
|
|
968
|
-
this.
|
|
1135
|
+
__privateGet(this, _options).httpAdapter = httpAdapter;
|
|
969
1136
|
return this;
|
|
970
1137
|
}
|
|
971
1138
|
withHttpLogger(httpLogger) {
|
|
972
|
-
this.
|
|
1139
|
+
__privateGet(this, _options).httpLogger = httpLogger;
|
|
973
1140
|
return this;
|
|
974
1141
|
}
|
|
975
1142
|
build() {
|
|
976
|
-
return new AuthyonServerClient({ ...this
|
|
1143
|
+
return new AuthyonServerClient({ ...__privateGet(this, _options) });
|
|
977
1144
|
}
|
|
978
1145
|
};
|
|
979
|
-
|
|
980
|
-
// ../../internal/core/authorization/ability.ts
|
|
981
|
-
var AuthyonAbility = class {
|
|
982
|
-
constructor(rules = [], detectSubjectType = defaultSubjectType) {
|
|
983
|
-
this.detectSubjectType = detectSubjectType;
|
|
984
|
-
this.listeners = /* @__PURE__ */ new Set();
|
|
985
|
-
this.currentRules = rules.map(cloneRule);
|
|
986
|
-
}
|
|
987
|
-
get rules() {
|
|
988
|
-
return this.currentRules;
|
|
989
|
-
}
|
|
990
|
-
can(action, subject, field) {
|
|
991
|
-
const subjectType = typeof subject === "string" ? subject : this.detectSubjectType(subject);
|
|
992
|
-
for (let index = this.currentRules.length - 1; index >= 0; index -= 1) {
|
|
993
|
-
const rule = this.currentRules[index];
|
|
994
|
-
if (!matchesToken(rule.action, action, "manage")) continue;
|
|
995
|
-
if (!matchesToken(rule.subject, subjectType, "all")) continue;
|
|
996
|
-
if (field && rule.fields && !rule.fields.some((value) => matchesField(value, field)))
|
|
997
|
-
continue;
|
|
998
|
-
if (rule.conditions) {
|
|
999
|
-
if (typeof subject === "string" || !matchesConditions(subject, rule.conditions)) continue;
|
|
1000
|
-
}
|
|
1001
|
-
return !rule.inverted;
|
|
1002
|
-
}
|
|
1003
|
-
return false;
|
|
1004
|
-
}
|
|
1005
|
-
cannot(action, subject, field) {
|
|
1006
|
-
return !this.can(action, subject, field);
|
|
1007
|
-
}
|
|
1008
|
-
rulesFor(action, subject) {
|
|
1009
|
-
return this.currentRules.filter(
|
|
1010
|
-
(rule) => matchesToken(rule.action, action, "manage") && matchesToken(rule.subject, subject, "all")
|
|
1011
|
-
);
|
|
1012
|
-
}
|
|
1013
|
-
update(rules) {
|
|
1014
|
-
this.currentRules = rules.map(cloneRule);
|
|
1015
|
-
for (const listener of this.listeners) listener(this.rules);
|
|
1016
|
-
}
|
|
1017
|
-
on(event, listener) {
|
|
1018
|
-
if (event !== "updated") return () => void 0;
|
|
1019
|
-
this.listeners.add(listener);
|
|
1020
|
-
return () => this.listeners.delete(listener);
|
|
1021
|
-
}
|
|
1022
|
-
};
|
|
1023
|
-
var AuthyonAbilityBuilder = class {
|
|
1024
|
-
constructor() {
|
|
1025
|
-
this.rules = [];
|
|
1026
|
-
}
|
|
1027
|
-
can(action, subject, conditions, fields) {
|
|
1028
|
-
this.rules.push({ action, subject, conditions, fields });
|
|
1029
|
-
return this;
|
|
1030
|
-
}
|
|
1031
|
-
cannot(action, subject, conditions, fields, reason) {
|
|
1032
|
-
this.rules.push({ action, subject, conditions, fields, reason, inverted: true });
|
|
1033
|
-
return this;
|
|
1034
|
-
}
|
|
1035
|
-
build(options = {}) {
|
|
1036
|
-
return new AuthyonAbility(this.rules, options.detectSubjectType);
|
|
1037
|
-
}
|
|
1038
|
-
};
|
|
1039
|
-
function createAuthyonAbility(source = {}, options = {}) {
|
|
1040
|
-
return new AuthyonAbility(createAuthyonRules(source, options), options.detectSubjectType);
|
|
1041
|
-
}
|
|
1042
|
-
function hasPermission(source, requiredPermission) {
|
|
1043
|
-
const requirement = permissionToRule(requiredPermission);
|
|
1044
|
-
if (!requirement || typeof requirement.action !== "string" || typeof requirement.subject !== "string") {
|
|
1045
|
-
return false;
|
|
1046
|
-
}
|
|
1047
|
-
const permissionSource = isPermissionList(source) ? { permissions: source } : source;
|
|
1048
|
-
return createAuthyonAbility(permissionSource).can(requirement.action, requirement.subject);
|
|
1049
|
-
}
|
|
1050
|
-
function isPermissionList(source) {
|
|
1051
|
-
return Array.isArray(source);
|
|
1052
|
-
}
|
|
1053
|
-
function createAuthyonRules(source = {}, options = {}) {
|
|
1054
|
-
const permissions = /* @__PURE__ */ new Set([
|
|
1055
|
-
...source.permissions ?? [],
|
|
1056
|
-
...source.scope?.split(/\s+/).filter(Boolean) ?? []
|
|
1057
|
-
]);
|
|
1058
|
-
const rules = [...permissions].map(permissionToRule).filter(isAbilityRule);
|
|
1059
|
-
for (const role of /* @__PURE__ */ new Set([...source.roles ?? [], ...options.roles ?? []])) {
|
|
1060
|
-
rules.push(...(options.roleRules?.[role] ?? []).map(cloneRule));
|
|
1061
|
-
}
|
|
1062
|
-
rules.push(...(options.rules ?? []).map(cloneRule));
|
|
1063
|
-
return rules;
|
|
1064
|
-
}
|
|
1065
|
-
function permissionToRule(permission) {
|
|
1066
|
-
const normalized = permission.trim();
|
|
1067
|
-
if (!normalized) return null;
|
|
1068
|
-
if (normalized === "*" || normalized === "*:*" || normalized === "all:manage") {
|
|
1069
|
-
return { action: "manage", subject: "all" };
|
|
1070
|
-
}
|
|
1071
|
-
const separator = normalized.lastIndexOf(":");
|
|
1072
|
-
if (separator <= 0 || separator === normalized.length - 1) return null;
|
|
1073
|
-
const subject = normalized.slice(0, separator);
|
|
1074
|
-
const action = normalized.slice(separator + 1);
|
|
1075
|
-
return {
|
|
1076
|
-
action: action === "*" ? "manage" : action,
|
|
1077
|
-
subject: subject === "*" ? "all" : subject
|
|
1078
|
-
};
|
|
1079
|
-
}
|
|
1080
|
-
function matchesToken(value, expected, wildcard) {
|
|
1081
|
-
return (Array.isArray(value) ? value : [value]).some(
|
|
1082
|
-
(candidate) => candidate === expected || candidate === wildcard || candidate === "*" || matchesSegments(candidate, expected)
|
|
1083
|
-
);
|
|
1084
|
-
}
|
|
1085
|
-
function matchesSegments(pattern, value) {
|
|
1086
|
-
const patternSegments = pattern.split(":");
|
|
1087
|
-
const valueSegments = value.split(":");
|
|
1088
|
-
return patternSegments.length === valueSegments.length && patternSegments.every((segment, index) => segment === "*" || segment === valueSegments[index]);
|
|
1089
|
-
}
|
|
1090
|
-
function matchesField(pattern, field) {
|
|
1091
|
-
if (pattern === "*" || pattern === field) return true;
|
|
1092
|
-
return pattern.endsWith(".*") && field.startsWith(pattern.slice(0, -1));
|
|
1093
|
-
}
|
|
1094
|
-
function matchesConditions(subject, conditions) {
|
|
1095
|
-
return Object.entries(conditions).every(([path, expected]) => {
|
|
1096
|
-
if (path === "$and" && Array.isArray(expected)) {
|
|
1097
|
-
return expected.every(
|
|
1098
|
-
(condition) => matchesConditions(subject, condition)
|
|
1099
|
-
);
|
|
1100
|
-
}
|
|
1101
|
-
if (path === "$or" && Array.isArray(expected)) {
|
|
1102
|
-
return expected.some(
|
|
1103
|
-
(condition) => matchesConditions(subject, condition)
|
|
1104
|
-
);
|
|
1105
|
-
}
|
|
1106
|
-
return matchesValue(readPath(subject, path), expected);
|
|
1107
|
-
});
|
|
1108
|
-
}
|
|
1109
|
-
function matchesValue(actual, expected) {
|
|
1110
|
-
if (!isRecord2(expected) || !Object.keys(expected).some((key) => key.startsWith("$"))) {
|
|
1111
|
-
return isRecord2(expected) && isRecord2(actual) ? matchesConditions(actual, expected) : Object.is(actual, expected);
|
|
1112
|
-
}
|
|
1113
|
-
return Object.entries(expected).every(([operator, operand]) => {
|
|
1114
|
-
switch (operator) {
|
|
1115
|
-
case "$eq":
|
|
1116
|
-
return Object.is(actual, operand);
|
|
1117
|
-
case "$ne":
|
|
1118
|
-
return !Object.is(actual, operand);
|
|
1119
|
-
case "$in":
|
|
1120
|
-
return Array.isArray(operand) && operand.some((value) => Object.is(actual, value));
|
|
1121
|
-
case "$nin":
|
|
1122
|
-
return Array.isArray(operand) && !operand.some((value) => Object.is(actual, value));
|
|
1123
|
-
case "$gt":
|
|
1124
|
-
return typeof actual === "number" && typeof operand === "number" && actual > operand;
|
|
1125
|
-
case "$gte":
|
|
1126
|
-
return typeof actual === "number" && typeof operand === "number" && actual >= operand;
|
|
1127
|
-
case "$lt":
|
|
1128
|
-
return typeof actual === "number" && typeof operand === "number" && actual < operand;
|
|
1129
|
-
case "$lte":
|
|
1130
|
-
return typeof actual === "number" && typeof operand === "number" && actual <= operand;
|
|
1131
|
-
case "$exists":
|
|
1132
|
-
return operand ? actual !== void 0 : actual === void 0;
|
|
1133
|
-
default:
|
|
1134
|
-
return false;
|
|
1135
|
-
}
|
|
1136
|
-
});
|
|
1137
|
-
}
|
|
1138
|
-
function readPath(value, path) {
|
|
1139
|
-
return path.split(".").reduce((current, part) => isRecord2(current) ? current[part] : void 0, value);
|
|
1140
|
-
}
|
|
1141
|
-
function defaultSubjectType(subject) {
|
|
1142
|
-
const explicit = subject.__type ?? subject.type ?? subject.kind;
|
|
1143
|
-
if (typeof explicit === "string") return explicit;
|
|
1144
|
-
const constructorName = subject.constructor?.name;
|
|
1145
|
-
return typeof constructorName === "string" ? constructorName : "Object";
|
|
1146
|
-
}
|
|
1147
|
-
function cloneRule(rule) {
|
|
1148
|
-
return {
|
|
1149
|
-
...rule,
|
|
1150
|
-
action: Array.isArray(rule.action) ? [...rule.action] : rule.action,
|
|
1151
|
-
subject: Array.isArray(rule.subject) ? [...rule.subject] : rule.subject,
|
|
1152
|
-
fields: rule.fields ? [...rule.fields] : void 0
|
|
1153
|
-
};
|
|
1154
|
-
}
|
|
1155
|
-
function isRecord2(value) {
|
|
1156
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1157
|
-
}
|
|
1158
|
-
function isAbilityRule(value) {
|
|
1159
|
-
return value !== null;
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
// src/integrations/authorization.ts
|
|
1163
|
-
var AuthorizationError = class extends Error {
|
|
1164
|
-
constructor(status, code, message) {
|
|
1165
|
-
super(message);
|
|
1166
|
-
this.status = status;
|
|
1167
|
-
this.code = code;
|
|
1168
|
-
this.name = "AuthorizationError";
|
|
1169
|
-
}
|
|
1170
|
-
};
|
|
1171
|
-
async function authorizeToken(client, token, options = {}) {
|
|
1172
|
-
if (!token) throw unauthenticated();
|
|
1173
|
-
let userId;
|
|
1174
|
-
let permissions = [];
|
|
1175
|
-
let roles = [];
|
|
1176
|
-
let scope;
|
|
1177
|
-
if (options.verification === "jwks") {
|
|
1178
|
-
if (!options.jwksVerifier) {
|
|
1179
|
-
throw new Error("Authyon: `jwksVerifier` is required for JWKS verification");
|
|
1180
|
-
}
|
|
1181
|
-
let verified;
|
|
1182
|
-
try {
|
|
1183
|
-
verified = await options.jwksVerifier.verifyAccessToken(token);
|
|
1184
|
-
} catch (error) {
|
|
1185
|
-
if (!(error instanceof TokenVerificationError)) throw error;
|
|
1186
|
-
if (error.code === "token.key_unavailable") {
|
|
1187
|
-
throw new AuthorizationError(
|
|
1188
|
-
503,
|
|
1189
|
-
"authorization.unavailable",
|
|
1190
|
-
"Token verification is temporarily unavailable"
|
|
1191
|
-
);
|
|
1192
|
-
}
|
|
1193
|
-
throw unauthenticated();
|
|
1194
|
-
}
|
|
1195
|
-
const { claims } = verified;
|
|
1196
|
-
userId = claims.sub;
|
|
1197
|
-
permissions = claims.permissions ?? [];
|
|
1198
|
-
roles = claims.roles ?? [];
|
|
1199
|
-
scope = claims.scope;
|
|
1200
|
-
} else if (options.verification === "introspect") {
|
|
1201
|
-
const identity = await client.introspect(token, { clientIp: options.clientIp });
|
|
1202
|
-
if (!identity.active) throw unauthenticated();
|
|
1203
|
-
userId = identity.sub;
|
|
1204
|
-
permissions = identity.permissions ?? [];
|
|
1205
|
-
roles = identity.roles ?? [];
|
|
1206
|
-
scope = identity.scope;
|
|
1207
|
-
} else {
|
|
1208
|
-
const identity = await client.validate(token, { clientIp: options.clientIp });
|
|
1209
|
-
if (!identity.valid || !identity.user) throw unauthenticated();
|
|
1210
|
-
userId = identity.user.id;
|
|
1211
|
-
permissions = identity.user.permissions ?? [];
|
|
1212
|
-
}
|
|
1213
|
-
const ability = createAuthyonAbility(
|
|
1214
|
-
{ permissions, roles, scope },
|
|
1215
|
-
{
|
|
1216
|
-
rules: options.rules,
|
|
1217
|
-
roleRules: options.roleRules,
|
|
1218
|
-
roles: options.roles,
|
|
1219
|
-
detectSubjectType: options.detectSubjectType
|
|
1220
|
-
}
|
|
1221
|
-
);
|
|
1222
|
-
if (options.requirement && !canAccess(ability, options.requirement)) {
|
|
1223
|
-
throw new AuthorizationError(403, "authorization.forbidden", "Permission denied");
|
|
1224
|
-
}
|
|
1225
|
-
return { token, userId, ability, permissions, roles };
|
|
1226
|
-
}
|
|
1227
|
-
function authorizeRequest(client, request, options = {}) {
|
|
1228
|
-
const clientIp = options.resolveClientIp?.(request) ?? options.clientIp;
|
|
1229
|
-
return authorizeToken(client, bearerToken(request.headers.get("authorization")), {
|
|
1230
|
-
...options,
|
|
1231
|
-
clientIp
|
|
1232
|
-
});
|
|
1233
|
-
}
|
|
1234
|
-
function createExpressAuthorizationMiddleware(client, options = {}) {
|
|
1235
|
-
return async (request, response, next) => {
|
|
1236
|
-
try {
|
|
1237
|
-
const header = request.headers.authorization;
|
|
1238
|
-
const clientIp = resolveExpressClientIp(request, options);
|
|
1239
|
-
const context = await authorizeToken(
|
|
1240
|
-
client,
|
|
1241
|
-
bearerToken(Array.isArray(header) ? header[0] : header),
|
|
1242
|
-
{ ...options, clientIp }
|
|
1243
|
-
);
|
|
1244
|
-
request[options.requestProperty ?? "authyon"] = context;
|
|
1245
|
-
next();
|
|
1246
|
-
} catch (error) {
|
|
1247
|
-
if (options.passErrorsToNext || !(error instanceof AuthorizationError)) {
|
|
1248
|
-
next(error);
|
|
1249
|
-
return;
|
|
1250
|
-
}
|
|
1251
|
-
response.status(error.status).json({ code: error.code, message: error.message });
|
|
1252
|
-
}
|
|
1253
|
-
};
|
|
1254
|
-
}
|
|
1255
|
-
function resolveExpressClientIp(request, options) {
|
|
1256
|
-
const resolved = options.resolveClientIp?.(request) ?? (options.trustProxy ? request.ip : request.socket?.remoteAddress);
|
|
1257
|
-
return resolved ? normalizeClientIp(stripIpv4Prefix(resolved)) : void 0;
|
|
1258
|
-
}
|
|
1259
|
-
function stripIpv4Prefix(value) {
|
|
1260
|
-
return value.startsWith("::ffff:") ? value.slice(7) : value;
|
|
1261
|
-
}
|
|
1262
|
-
function createAuthorizationErrorResponse(error) {
|
|
1263
|
-
if (!(error instanceof AuthorizationError)) throw error;
|
|
1264
|
-
return Response.json({ code: error.code, message: error.message }, { status: error.status });
|
|
1265
|
-
}
|
|
1266
|
-
function canAccess(ability, requirement) {
|
|
1267
|
-
return ability.can(requirement.action, requirement.subject, requirement.field);
|
|
1268
|
-
}
|
|
1269
|
-
function bearerToken(header) {
|
|
1270
|
-
const match = header?.match(/^Bearer\s+(.+)$/i);
|
|
1271
|
-
if (!match?.[1]) throw unauthenticated();
|
|
1272
|
-
return match[1].trim();
|
|
1273
|
-
}
|
|
1274
|
-
function unauthenticated() {
|
|
1275
|
-
return new AuthorizationError(
|
|
1276
|
-
401,
|
|
1277
|
-
"authorization.unauthenticated",
|
|
1278
|
-
"A valid bearer token is required"
|
|
1279
|
-
);
|
|
1280
|
-
}
|
|
1146
|
+
_options = new WeakMap();
|
|
1281
1147
|
export {
|
|
1282
1148
|
AuthorizationError,
|
|
1283
1149
|
AuthyonAbility,
|
|
@@ -1289,8 +1155,11 @@ export {
|
|
|
1289
1155
|
FetchHttpAdapter,
|
|
1290
1156
|
JwksTokenVerifier,
|
|
1291
1157
|
LoggingHttpAdapter,
|
|
1158
|
+
PlatformScopedClient,
|
|
1292
1159
|
TenantScopedClient,
|
|
1293
1160
|
TokenVerificationError,
|
|
1161
|
+
UserScopedClient,
|
|
1162
|
+
WorkspaceInvitesClient,
|
|
1294
1163
|
authorizeRequest,
|
|
1295
1164
|
authorizeToken,
|
|
1296
1165
|
createAuthorizationErrorResponse,
|