@authyon/server 0.2.0-beta.1 → 0.2.0-beta.12
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 +201 -1
- package/dist/authorization-DWlLfYQJ.d.cts +129 -0
- package/dist/authorization-wrZ5ull7.d.ts +129 -0
- package/dist/authyonServerClient-t82mwTws.d.cts +900 -0
- package/dist/authyonServerClient-t82mwTws.d.ts +900 -0
- package/dist/bff.cjs +1126 -0
- package/dist/bff.d.cts +142 -0
- package/dist/bff.d.ts +142 -0
- package/dist/bff.js +791 -0
- package/dist/chunk-56H66KLG.js +323 -0
- package/dist/chunk-77J4NURK.js +13 -0
- package/dist/chunk-NKWQMYB3.js +485 -0
- package/dist/index.cjs +412 -30
- package/dist/index.d.cts +16 -746
- package/dist/index.d.ts +16 -746
- package/dist/index.js +392 -770
- package/dist/next.cjs +328 -0
- package/dist/next.d.cts +27 -0
- package/dist/next.d.ts +27 -0
- package/dist/next.js +48 -0
- package/package.json +28 -5
package/dist/index.js
CHANGED
|
@@ -1,501 +1,314 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
AuthyonError,
|
|
3
|
+
DEFAULT_BASE_URL,
|
|
4
|
+
DEFAULT_EXPIRY_SKEW_MS,
|
|
5
|
+
ErrorCodes,
|
|
6
|
+
FetchHttpAdapter,
|
|
7
|
+
JsonHttpClient,
|
|
8
|
+
LoggingHttpAdapter,
|
|
9
|
+
createSharedTransport
|
|
10
|
+
} from "./chunk-56H66KLG.js";
|
|
11
|
+
import {
|
|
12
|
+
AuthorizationError,
|
|
13
|
+
AuthyonAbility,
|
|
14
|
+
AuthyonAbilityBuilder,
|
|
15
|
+
JwksTokenVerifier,
|
|
16
|
+
TokenVerificationError,
|
|
17
|
+
authorizeRequest,
|
|
18
|
+
authorizeToken,
|
|
19
|
+
clientIpHeaders,
|
|
20
|
+
createAuthorizationErrorResponse,
|
|
21
|
+
createAuthyonAbility,
|
|
22
|
+
createAuthyonRules,
|
|
23
|
+
createExpressAuthorizationMiddleware,
|
|
24
|
+
hasPermission,
|
|
25
|
+
hasPermissionGroup,
|
|
26
|
+
normalizeClientIp
|
|
27
|
+
} from "./chunk-NKWQMYB3.js";
|
|
28
|
+
import {
|
|
29
|
+
__privateAdd,
|
|
30
|
+
__privateGet,
|
|
31
|
+
__privateSet
|
|
32
|
+
} from "./chunk-77J4NURK.js";
|
|
4
33
|
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
34
|
+
// src/client/credentialManagement.ts
|
|
35
|
+
function segment(value) {
|
|
36
|
+
if (typeof value !== "string" || !value.trim() || value === "." || value === ".." || [...value].some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127)) {
|
|
37
|
+
throw new TypeError("Authyon: invalid route identifier.");
|
|
38
|
+
}
|
|
39
|
+
return encodeURIComponent(value);
|
|
40
|
+
}
|
|
41
|
+
function permissionsBody(input) {
|
|
42
|
+
if (input.permissions !== void 0 && input.scopes !== void 0) {
|
|
43
|
+
throw new TypeError("Authyon: supply permissions or scopes, not both.");
|
|
44
|
+
}
|
|
45
|
+
const values = input.permissions ?? input.scopes;
|
|
46
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
47
|
+
throw new TypeError("Authyon: explicit credential permissions are required.");
|
|
48
|
+
}
|
|
49
|
+
const permissions = values.map((value) => {
|
|
50
|
+
if (typeof value !== "string") throw new TypeError("Authyon: invalid permission.");
|
|
51
|
+
const normalized = value.trim().toLowerCase();
|
|
52
|
+
if (!/^(?:\*|[a-z][a-z0-9_-]*):(?:\*|[a-z][a-z0-9_-]*):(?:\*|[a-z][a-z0-9_-]*)$/u.test(normalized)) {
|
|
53
|
+
throw new TypeError("Authyon: permissions must have namespace:resource:action format.");
|
|
16
54
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
this.code = body.code ?? "unknown";
|
|
51
|
-
this.title = body.title ?? "Error";
|
|
52
|
-
this.detail = body.detail;
|
|
53
|
-
this.requestId = options.requestId;
|
|
54
|
-
this.retryAfter = options.retryAfter;
|
|
55
|
-
this.cause = options.cause;
|
|
56
|
-
}
|
|
57
|
-
is(code) {
|
|
58
|
-
return this.code === code;
|
|
59
|
-
}
|
|
60
|
-
isAny(...codes) {
|
|
61
|
-
return codes.includes(this.code);
|
|
62
|
-
}
|
|
63
|
-
hasPrefix(prefix) {
|
|
64
|
-
return this.code.startsWith(prefix);
|
|
65
|
-
}
|
|
66
|
-
isStatus(...statuses) {
|
|
67
|
-
return statuses.includes(this.status);
|
|
68
|
-
}
|
|
69
|
-
/** Stable interpretation for UI decisions, retries, telemetry and support flows. */
|
|
70
|
-
interpret() {
|
|
71
|
-
const category = classifyError(this.status, this.code);
|
|
72
|
-
return {
|
|
73
|
-
category,
|
|
74
|
-
action: actionFor(category),
|
|
75
|
-
retryable: isRetryable(category),
|
|
76
|
-
...this.retryAfter !== void 0 ? { retryAfter: this.retryAfter } : {},
|
|
77
|
-
...this.requestId !== void 0 ? { requestId: this.requestId } : {}
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
get category() {
|
|
81
|
-
return this.interpret().category;
|
|
82
|
-
}
|
|
83
|
-
get retryable() {
|
|
84
|
-
return this.interpret().retryable;
|
|
85
|
-
}
|
|
86
|
-
toJSON() {
|
|
87
|
-
return {
|
|
88
|
-
name: this.name,
|
|
89
|
-
message: this.message,
|
|
90
|
-
status: this.status,
|
|
91
|
-
code: this.code,
|
|
92
|
-
title: this.title,
|
|
93
|
-
detail: this.detail,
|
|
94
|
-
requestId: this.requestId,
|
|
95
|
-
retryAfter: this.retryAfter,
|
|
96
|
-
...this.interpret()
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
function classifyError(status, code) {
|
|
101
|
-
if (code === ErrorCodes.NetworkError) return "network";
|
|
102
|
-
if (code === ErrorCodes.Timeout || status === 408) return "timeout";
|
|
103
|
-
if (code === ErrorCodes.RateLimited || status === 429) return "rate_limit";
|
|
104
|
-
if (code === ErrorCodes.EmailTaken || status === 409) return "conflict";
|
|
105
|
-
if (code === ErrorCodes.PasswordWeak || code === ErrorCodes.PasswordPwned) return "validation";
|
|
106
|
-
if (code === ErrorCodes.SessionMalformed) return "server";
|
|
107
|
-
if (code.startsWith("auth.") || status === 401) return "authentication";
|
|
108
|
-
if (status === 403) return "authorization";
|
|
109
|
-
if (status === 404) return "not_found";
|
|
110
|
-
if (status === 400 || status === 422) return "validation";
|
|
111
|
-
if (status >= 500) return "server";
|
|
112
|
-
return "unknown";
|
|
113
|
-
}
|
|
114
|
-
function actionFor(category) {
|
|
115
|
-
switch (category) {
|
|
116
|
-
case "network":
|
|
117
|
-
case "timeout":
|
|
118
|
-
case "rate_limit":
|
|
119
|
-
case "server":
|
|
120
|
-
return "retry";
|
|
121
|
-
case "authentication":
|
|
122
|
-
return "reauthenticate";
|
|
123
|
-
case "authorization":
|
|
124
|
-
return "request_access";
|
|
125
|
-
case "validation":
|
|
126
|
-
return "fix_input";
|
|
127
|
-
case "not_found":
|
|
128
|
-
return "not_found";
|
|
129
|
-
case "conflict":
|
|
130
|
-
return "resolve_conflict";
|
|
131
|
-
case "unknown":
|
|
132
|
-
return "contact_support";
|
|
133
|
-
}
|
|
55
|
+
return normalized;
|
|
56
|
+
});
|
|
57
|
+
return { permissions: [...new Set(permissions)] };
|
|
58
|
+
}
|
|
59
|
+
function lifetimeBody(input) {
|
|
60
|
+
if (input.lifetimeDays === void 0 || input.lifetimeDays === null) return {};
|
|
61
|
+
if (!Number.isInteger(input.lifetimeDays) || input.lifetimeDays < 1 || input.lifetimeDays > 3650) {
|
|
62
|
+
throw new RangeError("Authyon: lifetimeDays must be between 1 and 3650.");
|
|
63
|
+
}
|
|
64
|
+
return { lifetimeDays: input.lifetimeDays };
|
|
65
|
+
}
|
|
66
|
+
function credentialQuery(options = {}) {
|
|
67
|
+
const { skip, take, search } = options;
|
|
68
|
+
if (skip !== void 0 && (!Number.isInteger(skip) || skip < 0 || skip > 2147483647))
|
|
69
|
+
throw new RangeError("Authyon: invalid skip.");
|
|
70
|
+
if (take !== void 0 && (!Number.isInteger(take) || take < 1 || take > 100))
|
|
71
|
+
throw new RangeError("Authyon: take must be between 1 and 100.");
|
|
72
|
+
if (search !== void 0 && (typeof search !== "string" || search.trim().length > 200))
|
|
73
|
+
throw new RangeError("Authyon: search must contain at most 200 characters.");
|
|
74
|
+
return { search: search?.trim() || void 0, skip, take };
|
|
75
|
+
}
|
|
76
|
+
function credentialSummary(value) {
|
|
77
|
+
return {
|
|
78
|
+
id: value.id,
|
|
79
|
+
clientId: value.clientId,
|
|
80
|
+
description: value.description,
|
|
81
|
+
createdAt: value.createdAt,
|
|
82
|
+
lastUsedAt: value.lastUsedAt,
|
|
83
|
+
revokedAt: value.revokedAt,
|
|
84
|
+
expiresAt: value.expiresAt ?? null,
|
|
85
|
+
isActive: value.isActive,
|
|
86
|
+
permissions: [...value.permissions]
|
|
87
|
+
};
|
|
134
88
|
}
|
|
135
|
-
function
|
|
136
|
-
return
|
|
89
|
+
function credentialDetail(value) {
|
|
90
|
+
return {
|
|
91
|
+
...credentialSummary(value),
|
|
92
|
+
createdBy: value.createdBy == null ? null : {
|
|
93
|
+
id: value.createdBy.id,
|
|
94
|
+
type: value.createdBy.type,
|
|
95
|
+
displayName: value.createdBy.displayName
|
|
96
|
+
},
|
|
97
|
+
updatedAt: value.updatedAt,
|
|
98
|
+
secretRotatedAt: value.secretRotatedAt,
|
|
99
|
+
expiresAt: value.expiresAt ?? null,
|
|
100
|
+
ageSeconds: value.ageSeconds,
|
|
101
|
+
lifetimeSeconds: value.lifetimeSeconds,
|
|
102
|
+
accessTokenLifetimeSeconds: value.accessTokenLifetimeSeconds
|
|
103
|
+
};
|
|
137
104
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (!
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
105
|
+
async function credentialPage(request, path, options = {}) {
|
|
106
|
+
const query = credentialQuery(options);
|
|
107
|
+
const page = await request(path, { query });
|
|
108
|
+
if (!page || !Array.isArray(page.data))
|
|
109
|
+
throw new TypeError("Authyon: expected a paginated credential response.");
|
|
110
|
+
return {
|
|
111
|
+
data: page.data.map(credentialSummary),
|
|
112
|
+
...page.perPage === void 0 ? {} : { perPage: page.perPage },
|
|
113
|
+
pageSize: page.pageSize,
|
|
114
|
+
total: page.total,
|
|
115
|
+
pages: page.pages,
|
|
116
|
+
hasNext: page.hasNext,
|
|
117
|
+
hasPrev: page.hasPrev
|
|
118
|
+
};
|
|
148
119
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
};
|
|
164
|
-
var LoggingHttpAdapter = class {
|
|
165
|
-
constructor(adapter, options) {
|
|
166
|
-
this.adapter = adapter;
|
|
167
|
-
this.options = options;
|
|
168
|
-
}
|
|
169
|
-
async request(request) {
|
|
170
|
-
const startedAt = Date.now();
|
|
171
|
-
const eventBase = {
|
|
172
|
-
method: request.method,
|
|
173
|
-
url: sanitizeUrl(request.url)
|
|
174
|
-
};
|
|
175
|
-
this.log({ type: "request", ...eventBase, timestamp: startedAt });
|
|
176
|
-
try {
|
|
177
|
-
const response = await this.adapter.request(request);
|
|
178
|
-
this.log({
|
|
179
|
-
type: "response",
|
|
180
|
-
...eventBase,
|
|
181
|
-
status: response.status,
|
|
182
|
-
durationMs: Date.now() - startedAt,
|
|
183
|
-
requestId: response.headers.get("x-request-id") ?? response.headers.get("trace-id") ?? void 0,
|
|
184
|
-
timestamp: Date.now()
|
|
185
|
-
});
|
|
186
|
-
return response;
|
|
187
|
-
} catch (error) {
|
|
188
|
-
this.log({
|
|
189
|
-
type: "error",
|
|
190
|
-
...eventBase,
|
|
191
|
-
errorName: error instanceof Error ? error.name : "UnknownError",
|
|
192
|
-
durationMs: Date.now() - startedAt,
|
|
193
|
-
timestamp: Date.now()
|
|
194
|
-
});
|
|
195
|
-
throw error;
|
|
120
|
+
async function allCredentials(request, path) {
|
|
121
|
+
const result = [];
|
|
122
|
+
const seen = /* @__PURE__ */ new Set();
|
|
123
|
+
let skip = 0;
|
|
124
|
+
for (let iteration = 0; iteration < 1e3; iteration++) {
|
|
125
|
+
const page = await request(
|
|
126
|
+
path,
|
|
127
|
+
iteration === 0 ? {} : { query: { skip, take: 100 } }
|
|
128
|
+
);
|
|
129
|
+
if (Array.isArray(page)) {
|
|
130
|
+
if (iteration !== 0)
|
|
131
|
+
throw new Error("Authyon: credential pagination changed during listing.");
|
|
132
|
+
return page.map(credentialSummary);
|
|
196
133
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
134
|
+
if (!page || !Array.isArray(page.data))
|
|
135
|
+
throw new TypeError("Authyon: invalid credential page.");
|
|
136
|
+
for (const row of page.data) {
|
|
137
|
+
if (seen.has(row.id))
|
|
138
|
+
throw new Error("Authyon: credential pages overlapped; retry the listing.");
|
|
139
|
+
seen.add(row.id);
|
|
140
|
+
result.push(credentialSummary(row));
|
|
204
141
|
}
|
|
142
|
+
if (!page.hasNext) return result;
|
|
143
|
+
if (page.data.length === 0) throw new Error("Authyon: credential pagination made no progress.");
|
|
144
|
+
skip += page.data.length;
|
|
205
145
|
}
|
|
206
|
-
|
|
207
|
-
function defaultHttpLogger(event) {
|
|
208
|
-
console.debug("[Authyon HTTP]", event);
|
|
209
|
-
}
|
|
210
|
-
function sanitizeUrl(value) {
|
|
211
|
-
const url = new URL(value);
|
|
212
|
-
const queryKeys = /* @__PURE__ */ new Set();
|
|
213
|
-
url.searchParams.forEach((_value, key) => queryKeys.add(key));
|
|
214
|
-
url.search = queryKeys.size > 0 ? [...queryKeys].map((key) => `${encodeURIComponent(key)}=REDACTED`).join("&") : "";
|
|
215
|
-
return url.toString();
|
|
146
|
+
throw new Error("Authyon: use listPage for more than 1000 credential pages.");
|
|
216
147
|
}
|
|
217
148
|
|
|
218
|
-
//
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
};
|
|
228
|
-
function createSharedTransport(options) {
|
|
229
|
-
const baseUrl = normalizeBaseUrl(options.baseUrl, options.allowInsecureHttp ?? false);
|
|
230
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
231
|
-
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
|
|
232
|
-
throw new Error("Authyon: `timeoutMs` must be a non-negative finite number");
|
|
233
|
-
}
|
|
234
|
-
if (options.httpAdapter && options.fetch) {
|
|
235
|
-
throw new Error("Authyon: use either `httpAdapter` or `fetch`, not both");
|
|
236
|
-
}
|
|
237
|
-
const baseAdapter = options.httpAdapter ?? new FetchHttpAdapter(options.fetch);
|
|
238
|
-
const httpAdapter = options.httpLogger ? new LoggingHttpAdapter(baseAdapter, options.httpLogger) : baseAdapter;
|
|
239
|
-
return {
|
|
240
|
-
baseUrl,
|
|
241
|
-
async request(path, init) {
|
|
242
|
-
const controller = new AbortController();
|
|
243
|
-
const timeout = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : void 0;
|
|
244
|
-
try {
|
|
245
|
-
return await httpAdapter.request({
|
|
246
|
-
url: `${baseUrl}${path}`,
|
|
247
|
-
method: init.method ?? "GET",
|
|
248
|
-
headers: normalizeHeaders(init.headers),
|
|
249
|
-
body: init.body,
|
|
250
|
-
signal: controller.signal
|
|
251
|
-
});
|
|
252
|
-
} catch (cause) {
|
|
253
|
-
throw new SharedTransportError(
|
|
254
|
-
controller.signal.aborted ? ErrorCodes.Timeout : ErrorCodes.NetworkError,
|
|
255
|
-
cause
|
|
256
|
-
);
|
|
257
|
-
} finally {
|
|
258
|
-
if (timeout !== void 0) clearTimeout(timeout);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
149
|
+
// src/client/scopedManagementClient.ts
|
|
150
|
+
function scopedRequest(request, source) {
|
|
151
|
+
return async (path, options = {}) => {
|
|
152
|
+
const token = typeof source === "function" ? await source() : source;
|
|
153
|
+
if (typeof token !== "string" || !token.trim() || /\s/u.test(token))
|
|
154
|
+
throw new TypeError("Authyon: a valid access token is required.");
|
|
155
|
+
return request(path, {
|
|
156
|
+
...options,
|
|
157
|
+
headers: { ...options.headers, Authorization: `Bearer ${token}` }
|
|
158
|
+
});
|
|
261
159
|
};
|
|
262
160
|
}
|
|
263
|
-
function
|
|
264
|
-
|
|
265
|
-
new Headers(headers).forEach((value, key) => {
|
|
266
|
-
normalized[key] = value;
|
|
267
|
-
});
|
|
268
|
-
return normalized;
|
|
161
|
+
function environmentPath(scope) {
|
|
162
|
+
return `/platform/workspaces/${segment(scope.workspaceId)}/environments/${segment(scope.environmentId)}`;
|
|
269
163
|
}
|
|
270
|
-
function
|
|
271
|
-
|
|
272
|
-
const retryAfter = retryAfterHeader ? Number(retryAfterHeader) : void 0;
|
|
273
|
-
return {
|
|
274
|
-
requestId: response.headers.get("x-request-id") ?? response.headers.get("trace-id") ?? void 0,
|
|
275
|
-
retryAfter: Number.isFinite(retryAfter) ? retryAfter : void 0
|
|
276
|
-
};
|
|
164
|
+
function tenantPath(scope) {
|
|
165
|
+
return `${environmentPath(scope)}/tenants/${segment(scope.tenantId)}`;
|
|
277
166
|
}
|
|
278
|
-
function
|
|
279
|
-
|
|
280
|
-
try {
|
|
281
|
-
url = new URL(value);
|
|
282
|
-
} catch {
|
|
283
|
-
throw new Error("Authyon: `baseUrl` must be an absolute URL");
|
|
284
|
-
}
|
|
285
|
-
const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
|
|
286
|
-
if (url.protocol !== "https:" && !(url.protocol === "http:" && (loopback || allowInsecureHttp))) {
|
|
287
|
-
throw new Error(
|
|
288
|
-
"Authyon: `baseUrl` must use HTTPS (HTTP is allowed only for loopback or with `allowInsecureHttp`)"
|
|
289
|
-
);
|
|
290
|
-
}
|
|
291
|
-
if (url.username || url.password || url.search || url.hash) {
|
|
292
|
-
throw new Error(
|
|
293
|
-
"Authyon: `baseUrl` cannot contain credentials, query parameters, or fragments"
|
|
294
|
-
);
|
|
295
|
-
}
|
|
296
|
-
return url.toString().replace(/\/+$/, "");
|
|
167
|
+
function credentialPath(scope) {
|
|
168
|
+
return `${scope.tenantId === void 0 ? environmentPath(scope) : tenantPath(scope)}/credentials`;
|
|
297
169
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
})
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
170
|
+
function workspacePath(id) {
|
|
171
|
+
return `/platform/workspaces/${segment(id)}`;
|
|
172
|
+
}
|
|
173
|
+
function workspaceRole(role) {
|
|
174
|
+
if (role !== "admin" && role !== "auditor")
|
|
175
|
+
throw new TypeError("Authyon: workspace role must be admin or auditor.");
|
|
176
|
+
return role;
|
|
177
|
+
}
|
|
178
|
+
var _request;
|
|
179
|
+
var PlatformScopedClient = class {
|
|
180
|
+
/** @hidden */
|
|
181
|
+
constructor(request, token) {
|
|
182
|
+
__privateAdd(this, _request);
|
|
183
|
+
this.credentials = {
|
|
184
|
+
list: (scope, options = {}) => credentialPage(__privateGet(this, _request), credentialPath(scope), options),
|
|
185
|
+
get: async (scope, credentialId) => credentialDetail(await __privateGet(this, _request).call(this, `${credentialPath(scope)}/${segment(credentialId)}`)),
|
|
186
|
+
create: (scope, input) => __privateGet(this, _request).call(this, credentialPath(scope), {
|
|
187
|
+
method: "POST",
|
|
188
|
+
body: { description: input.description, ...lifetimeBody(input), ...permissionsBody(input) }
|
|
189
|
+
}),
|
|
190
|
+
updatePermissions: (scope, credentialId, input) => __privateGet(this, _request).call(this, `${credentialPath(scope)}/${segment(credentialId)}/permissions`, {
|
|
191
|
+
method: "PUT",
|
|
192
|
+
body: permissionsBody(input)
|
|
193
|
+
}),
|
|
194
|
+
updateScopes: (scope, credentialId, scopes) => this.credentials.updatePermissions(scope, credentialId, { scopes }),
|
|
195
|
+
/** Returns a new secret once. API may require an explicit step-up first. */
|
|
196
|
+
rotate: (scope, credentialId) => __privateGet(this, _request).call(this, `${credentialPath(scope)}/${segment(credentialId)}/rotate`, { method: "POST" }),
|
|
197
|
+
revoke: (scope, credentialId) => __privateGet(this, _request).call(this, `${credentialPath(scope)}/${segment(credentialId)}`, { method: "DELETE" })
|
|
198
|
+
};
|
|
199
|
+
this.workspaces = {
|
|
200
|
+
team: (workspaceId) => __privateGet(this, _request).call(this, `${workspacePath(workspaceId)}/team`),
|
|
201
|
+
members: {
|
|
202
|
+
invite: (workspaceId, input) => __privateGet(this, _request).call(this, `${workspacePath(workspaceId)}/invites`, {
|
|
203
|
+
method: "POST",
|
|
204
|
+
body: { email: input.email, role: workspaceRole(input.role) }
|
|
205
|
+
}),
|
|
206
|
+
/** memberId comes from team.members; it is not the platform userId. */
|
|
207
|
+
remove: (workspaceId, memberId) => __privateGet(this, _request).call(this, `${workspacePath(workspaceId)}/members/${segment(memberId)}`, {
|
|
208
|
+
method: "DELETE"
|
|
209
|
+
}),
|
|
210
|
+
changeRole: (workspaceId, memberId, role) => __privateGet(this, _request).call(this, `${workspacePath(workspaceId)}/members/${segment(memberId)}`, {
|
|
211
|
+
method: "PATCH",
|
|
212
|
+
body: { role: workspaceRole(role) }
|
|
213
|
+
})
|
|
214
|
+
},
|
|
215
|
+
invites: {
|
|
216
|
+
create: (workspaceId, input) => this.workspaces.members.invite(workspaceId, input),
|
|
217
|
+
revoke: (workspaceId, inviteId) => __privateGet(this, _request).call(this, `${workspacePath(workspaceId)}/invites/${segment(inviteId)}`, {
|
|
218
|
+
method: "DELETE"
|
|
219
|
+
})
|
|
324
220
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
221
|
+
};
|
|
222
|
+
this.tenants = {
|
|
223
|
+
members: {
|
|
224
|
+
list: (scope, options = {}) => __privateGet(this, _request).call(this, `${tenantPath(scope)}/members`, { query: options }),
|
|
225
|
+
add: (scope, userId, roles) => __privateGet(this, _request).call(this, `${tenantPath(scope)}/members`, { method: "POST", body: { userId, roles } }),
|
|
226
|
+
remove: (scope, userId) => __privateGet(this, _request).call(this, `${tenantPath(scope)}/members/${segment(userId)}`, { method: "DELETE" })
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
this.auth = {
|
|
230
|
+
/** Explicitly confirms the current platform session; passwords are not retained. */
|
|
231
|
+
stepUp: (currentPassword) => __privateGet(this, _request).call(this, "/platform/auth/step-up", { method: "POST", body: { currentPassword } })
|
|
232
|
+
};
|
|
233
|
+
__privateSet(this, _request, scopedRequest(request, token));
|
|
332
234
|
}
|
|
333
235
|
};
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}
|
|
353
|
-
|
|
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 });
|
|
236
|
+
_request = new WeakMap();
|
|
237
|
+
var _request2;
|
|
238
|
+
var UserScopedClient = class {
|
|
239
|
+
/** @hidden */
|
|
240
|
+
constructor(request, token) {
|
|
241
|
+
__privateAdd(this, _request2);
|
|
242
|
+
this.tenants = {
|
|
243
|
+
members: {
|
|
244
|
+
list: (tenantId, options = {}) => __privateGet(this, _request2).call(this, `/auth/tenants/${segment(tenantId)}/members`, { query: options }),
|
|
245
|
+
/** Adds an existing environment user by email; does not send an invitation email. */
|
|
246
|
+
invite: (tenantId, input) => __privateGet(this, _request2).call(this, `/auth/tenants/${segment(tenantId)}/members`, {
|
|
247
|
+
method: "POST",
|
|
248
|
+
body: { email: input.email, roles: input.roles }
|
|
249
|
+
}),
|
|
250
|
+
remove: (tenantId, userId) => __privateGet(this, _request2).call(this, `/auth/tenants/${segment(tenantId)}/members/${segment(userId)}`, {
|
|
251
|
+
method: "DELETE"
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
__privateSet(this, _request2, scopedRequest(request, token));
|
|
375
256
|
}
|
|
376
257
|
};
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
this
|
|
383
|
-
this
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
258
|
+
_request2 = new WeakMap();
|
|
259
|
+
var _request3;
|
|
260
|
+
var WorkspaceInvitesClient = class {
|
|
261
|
+
/** @hidden */
|
|
262
|
+
constructor(request) {
|
|
263
|
+
__privateAdd(this, _request3);
|
|
264
|
+
__privateSet(this, _request3, request);
|
|
265
|
+
}
|
|
266
|
+
preview(token) {
|
|
267
|
+
return __privateGet(this, _request3).call(this, `/platform/workspace-invites/${segment(token)}`);
|
|
268
|
+
}
|
|
269
|
+
accept(token, input = {}) {
|
|
270
|
+
return __privateGet(this, _request3).call(this, `/platform/workspace-invites/${segment(token)}/accept`, {
|
|
271
|
+
method: "POST",
|
|
272
|
+
body: { name: input.name, password: input.password }
|
|
387
273
|
});
|
|
388
274
|
}
|
|
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
275
|
};
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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");
|
|
276
|
+
_request3 = new WeakMap();
|
|
277
|
+
|
|
278
|
+
// ../../internal/core/auth/expiringTokenProvider.ts
|
|
279
|
+
var _token, _inFlight;
|
|
280
|
+
var ExpiringTokenProvider = class {
|
|
281
|
+
constructor(acquire, expirySkewMs = DEFAULT_EXPIRY_SKEW_MS) {
|
|
282
|
+
this.acquire = acquire;
|
|
283
|
+
this.expirySkewMs = expirySkewMs;
|
|
284
|
+
__privateAdd(this, _token, null);
|
|
285
|
+
__privateAdd(this, _inFlight, null);
|
|
441
286
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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`);
|
|
287
|
+
async getAccessToken() {
|
|
288
|
+
if (__privateGet(this, _token) && Date.now() < __privateGet(this, _token).expiresAt - this.expirySkewMs) {
|
|
289
|
+
return __privateGet(this, _token).accessToken;
|
|
451
290
|
}
|
|
291
|
+
if (__privateGet(this, _inFlight)) return __privateGet(this, _inFlight);
|
|
292
|
+
__privateSet(this, _inFlight, this.acquire().then((token) => {
|
|
293
|
+
__privateSet(this, _token, token);
|
|
294
|
+
return token.accessToken;
|
|
295
|
+
}).finally(() => {
|
|
296
|
+
__privateSet(this, _inFlight, null);
|
|
297
|
+
}));
|
|
298
|
+
return __privateGet(this, _inFlight);
|
|
452
299
|
}
|
|
453
|
-
|
|
454
|
-
|
|
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";
|
|
300
|
+
clear() {
|
|
301
|
+
__privateSet(this, _token, null);
|
|
477
302
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
}
|
|
303
|
+
};
|
|
304
|
+
_token = new WeakMap();
|
|
305
|
+
_inFlight = new WeakMap();
|
|
495
306
|
|
|
496
307
|
// src/client/authyonServerClient.ts
|
|
308
|
+
var _clientSecret;
|
|
497
309
|
var AuthyonServerClient = class {
|
|
498
310
|
constructor(options = {}) {
|
|
311
|
+
__privateAdd(this, _clientSecret);
|
|
499
312
|
// ── Discovery ──────────────────────────────────────────────────────────────
|
|
500
313
|
this.discovery = {
|
|
501
314
|
/** GET /.well-known/jwks.json — the environment's public signing keys. */
|
|
@@ -529,8 +342,19 @@ var AuthyonServerClient = class {
|
|
|
529
342
|
client_id: credentials.clientId,
|
|
530
343
|
client_secret: credentials.clientSecret
|
|
531
344
|
}
|
|
345
|
+
}),
|
|
346
|
+
/** POST /tenant/auth/validate — validates an already-minted tenant-client bearer token. */
|
|
347
|
+
validate: (accessToken, context) => this.request("/tenant/auth/validate", {
|
|
348
|
+
method: "POST",
|
|
349
|
+
headers: {
|
|
350
|
+
...clientIpHeaders(context),
|
|
351
|
+
Authorization: `Bearer ${accessToken}`
|
|
352
|
+
}
|
|
532
353
|
})
|
|
533
354
|
};
|
|
355
|
+
this.workspaceInvites = new WorkspaceInvitesClient(
|
|
356
|
+
(path, options) => this.http.request(path, options)
|
|
357
|
+
);
|
|
534
358
|
// ── Environment management (users, tenants, roles, permissions, audit) ───
|
|
535
359
|
this.environment = {
|
|
536
360
|
users: {
|
|
@@ -622,8 +446,53 @@ var AuthyonServerClient = class {
|
|
|
622
446
|
)
|
|
623
447
|
},
|
|
624
448
|
tenants: {
|
|
625
|
-
|
|
626
|
-
|
|
449
|
+
credentials: {
|
|
450
|
+
/** GET /env/tenants/{tenantId}/credentials. Returns metadata only, including revoked credentials. */
|
|
451
|
+
list: (tenantId) => allCredentials(
|
|
452
|
+
(path, options) => this.request(path, { ...options, envBearer: true }),
|
|
453
|
+
`/env/tenants/${segment(tenantId)}/credentials`
|
|
454
|
+
),
|
|
455
|
+
/** Paginated searchable listing. Prefer this over loading every credential. */
|
|
456
|
+
listPage: (tenantId, options = {}) => credentialPage(
|
|
457
|
+
(path, requestOptions) => this.request(path, { ...requestOptions, envBearer: true }),
|
|
458
|
+
`/env/tenants/${segment(tenantId)}/credentials`,
|
|
459
|
+
options
|
|
460
|
+
),
|
|
461
|
+
get: async (tenantId, credentialId) => credentialDetail(
|
|
462
|
+
await this.request(
|
|
463
|
+
`/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}`,
|
|
464
|
+
{ envBearer: true }
|
|
465
|
+
)
|
|
466
|
+
),
|
|
467
|
+
/** Explicit scopes/permissions; only issuance returns the client secret. */
|
|
468
|
+
create: (tenantId, input) => this.request(`/env/tenants/${segment(tenantId)}/credentials`, {
|
|
469
|
+
method: "POST",
|
|
470
|
+
envBearer: true,
|
|
471
|
+
body: {
|
|
472
|
+
description: input.description,
|
|
473
|
+
...lifetimeBody(input),
|
|
474
|
+
...permissionsBody(input)
|
|
475
|
+
}
|
|
476
|
+
}),
|
|
477
|
+
/** Returns a new secret once while keeping the same client id. */
|
|
478
|
+
rotate: (tenantId, credentialId) => this.request(
|
|
479
|
+
`/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}/rotate`,
|
|
480
|
+
{ method: "POST", envBearer: true }
|
|
481
|
+
),
|
|
482
|
+
updatePermissions: (tenantId, credentialId, input) => this.request(
|
|
483
|
+
`/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}/permissions`,
|
|
484
|
+
{ method: "PUT", envBearer: true, body: permissionsBody(input) }
|
|
485
|
+
),
|
|
486
|
+
updateScopes: (tenantId, credentialId, scopes) => this.environment.tenants.credentials.updatePermissions(tenantId, credentialId, {
|
|
487
|
+
scopes
|
|
488
|
+
}),
|
|
489
|
+
revoke: (tenantId, credentialId) => this.request(`/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}`, {
|
|
490
|
+
method: "DELETE",
|
|
491
|
+
envBearer: true
|
|
492
|
+
})
|
|
493
|
+
},
|
|
494
|
+
/** GET /env/tenants — paginated, searchable list of tenants in the environment. */
|
|
495
|
+
list: (params = {}) => this.request("/env/tenants", { envBearer: true, query: params }),
|
|
627
496
|
/** POST /env/tenants — create a tenant in the environment. */
|
|
628
497
|
create: (input) => this.request("/env/tenants", { method: "POST", envBearer: true, body: input }),
|
|
629
498
|
/** GET /env/tenants/{tenantId} — fetch a tenant by id. */
|
|
@@ -634,7 +503,18 @@ var AuthyonServerClient = class {
|
|
|
634
503
|
envBearer: true,
|
|
635
504
|
body: input
|
|
636
505
|
}),
|
|
637
|
-
/**
|
|
506
|
+
/** PUT /env/tenants/{tenantId}/metadata - replace public and private metadata. */
|
|
507
|
+
updateMetadata: (tenantId, input) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/metadata`, {
|
|
508
|
+
method: "PUT",
|
|
509
|
+
envBearer: true,
|
|
510
|
+
body: input
|
|
511
|
+
}),
|
|
512
|
+
/** POST /env/tenants/{tenantId}/disable — disable without removing tenant data. */
|
|
513
|
+
disable: (tenantId) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/disable`, {
|
|
514
|
+
method: "POST",
|
|
515
|
+
envBearer: true
|
|
516
|
+
}),
|
|
517
|
+
/** DELETE /env/tenants/{tenantId} — logically delete (disable) a tenant. */
|
|
638
518
|
delete: (tenantId) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}`, {
|
|
639
519
|
method: "DELETE",
|
|
640
520
|
envBearer: true
|
|
@@ -768,7 +648,8 @@ var AuthyonServerClient = class {
|
|
|
768
648
|
};
|
|
769
649
|
this.envKey = options.envKey;
|
|
770
650
|
this.clientId = options.clientId;
|
|
771
|
-
this
|
|
651
|
+
__privateSet(this, _clientSecret, options.clientSecret);
|
|
652
|
+
this.clientIp = options.clientIp;
|
|
772
653
|
this.transport = createSharedTransport({
|
|
773
654
|
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
|
|
774
655
|
allowInsecureHttp: options.allowInsecureHttp,
|
|
@@ -779,14 +660,14 @@ var AuthyonServerClient = class {
|
|
|
779
660
|
});
|
|
780
661
|
this.http = new JsonHttpClient(this.transport);
|
|
781
662
|
this.environmentTokenProvider = new ExpiringTokenProvider(async () => {
|
|
782
|
-
if (!this.clientId || !this
|
|
663
|
+
if (!this.clientId || !__privateGet(this, _clientSecret)) {
|
|
783
664
|
throw new Error(
|
|
784
665
|
"Authyon: `clientId`/`clientSecret` are required for environment management calls (mint a pair in the console, under the environment's OAuth clients)."
|
|
785
666
|
);
|
|
786
667
|
}
|
|
787
668
|
const token = await this.environmentAuth.token({
|
|
788
669
|
clientId: this.clientId,
|
|
789
|
-
clientSecret: this
|
|
670
|
+
clientSecret: __privateGet(this, _clientSecret)
|
|
790
671
|
});
|
|
791
672
|
return {
|
|
792
673
|
accessToken: token.access_token,
|
|
@@ -795,7 +676,10 @@ var AuthyonServerClient = class {
|
|
|
795
676
|
});
|
|
796
677
|
}
|
|
797
678
|
async request(path, options = {}) {
|
|
798
|
-
const headers = {
|
|
679
|
+
const headers = {
|
|
680
|
+
...clientIpHeaders({ clientIp: this.clientIp }),
|
|
681
|
+
...options.headers
|
|
682
|
+
};
|
|
799
683
|
if (options.envBearer) {
|
|
800
684
|
headers.Authorization = `Bearer ${await this.getEnvironmentAccessToken()}`;
|
|
801
685
|
}
|
|
@@ -855,6 +739,14 @@ var AuthyonServerClient = class {
|
|
|
855
739
|
tenant(credentials) {
|
|
856
740
|
return new TenantScopedClient(this, credentials);
|
|
857
741
|
}
|
|
742
|
+
/** Uses only the supplied platform-user access token. */
|
|
743
|
+
platform(token) {
|
|
744
|
+
return new PlatformScopedClient((path, options) => this.http.request(path, options), token);
|
|
745
|
+
}
|
|
746
|
+
/** Uses an end-user token; the configured environment key selects the environment. */
|
|
747
|
+
user(token) {
|
|
748
|
+
return new UserScopedClient((path, options) => this.request(path, options), token);
|
|
749
|
+
}
|
|
858
750
|
/** @internal used by {@link TenantScopedClient} to share the base URL/fetch/error handling. */
|
|
859
751
|
_requestAsTenant(accessToken, path, options = {}) {
|
|
860
752
|
return this.request(path, {
|
|
@@ -864,6 +756,7 @@ var AuthyonServerClient = class {
|
|
|
864
756
|
});
|
|
865
757
|
}
|
|
866
758
|
};
|
|
759
|
+
_clientSecret = new WeakMap();
|
|
867
760
|
var TenantScopedClient = class {
|
|
868
761
|
constructor(server, credentials) {
|
|
869
762
|
this.members = {
|
|
@@ -936,332 +829,56 @@ var TenantScopedClient = class {
|
|
|
936
829
|
async request(path, options = {}) {
|
|
937
830
|
return this.server._requestAsTenant(await this.getAccessToken(), path, options);
|
|
938
831
|
}
|
|
832
|
+
/** POST /tenant/auth/validate — validates this tenant client's current bearer token. */
|
|
833
|
+
async validate(context) {
|
|
834
|
+
return this.server.tenantAuth.validate(await this.getAccessToken(), context);
|
|
835
|
+
}
|
|
939
836
|
};
|
|
940
837
|
function createClient(options = {}) {
|
|
941
838
|
return new AuthyonServerClient(options);
|
|
942
839
|
}
|
|
943
840
|
|
|
944
841
|
// src/client/authyonServerClientBuilder.ts
|
|
842
|
+
var _options;
|
|
945
843
|
var AuthyonServerClientBuilder = class {
|
|
946
844
|
constructor() {
|
|
947
|
-
this
|
|
845
|
+
__privateAdd(this, _options, {});
|
|
948
846
|
}
|
|
949
847
|
withEnvironmentKey(envKey) {
|
|
950
|
-
this.
|
|
848
|
+
__privateGet(this, _options).envKey = envKey;
|
|
951
849
|
return this;
|
|
952
850
|
}
|
|
953
851
|
withEnvironmentCredentials(clientId, clientSecret) {
|
|
954
|
-
this.
|
|
955
|
-
this.
|
|
852
|
+
__privateGet(this, _options).clientId = clientId;
|
|
853
|
+
__privateGet(this, _options).clientSecret = clientSecret;
|
|
854
|
+
return this;
|
|
855
|
+
}
|
|
856
|
+
withClientIp(clientIp) {
|
|
857
|
+
__privateGet(this, _options).clientIp = clientIp;
|
|
956
858
|
return this;
|
|
957
859
|
}
|
|
958
860
|
withBaseUrl(baseUrl, allowInsecureHttp = false) {
|
|
959
|
-
this.
|
|
960
|
-
this.
|
|
861
|
+
__privateGet(this, _options).baseUrl = baseUrl;
|
|
862
|
+
__privateGet(this, _options).allowInsecureHttp = allowInsecureHttp;
|
|
961
863
|
return this;
|
|
962
864
|
}
|
|
963
865
|
withTimeout(timeoutMs) {
|
|
964
|
-
this.
|
|
866
|
+
__privateGet(this, _options).timeoutMs = timeoutMs;
|
|
965
867
|
return this;
|
|
966
868
|
}
|
|
967
869
|
withHttpAdapter(httpAdapter) {
|
|
968
|
-
this.
|
|
870
|
+
__privateGet(this, _options).httpAdapter = httpAdapter;
|
|
969
871
|
return this;
|
|
970
872
|
}
|
|
971
873
|
withHttpLogger(httpLogger) {
|
|
972
|
-
this.
|
|
874
|
+
__privateGet(this, _options).httpLogger = httpLogger;
|
|
973
875
|
return this;
|
|
974
876
|
}
|
|
975
877
|
build() {
|
|
976
|
-
return new AuthyonServerClient({ ...this
|
|
977
|
-
}
|
|
978
|
-
};
|
|
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);
|
|
878
|
+
return new AuthyonServerClient({ ...__privateGet(this, _options) });
|
|
1037
879
|
}
|
|
1038
880
|
};
|
|
1039
|
-
|
|
1040
|
-
return new AuthyonAbility(createAuthyonRules(source, options), options.detectSubjectType);
|
|
1041
|
-
}
|
|
1042
|
-
function createAuthyonRules(source = {}, options = {}) {
|
|
1043
|
-
const permissions = /* @__PURE__ */ new Set([
|
|
1044
|
-
...source.permissions ?? [],
|
|
1045
|
-
...source.scope?.split(/\s+/).filter(Boolean) ?? []
|
|
1046
|
-
]);
|
|
1047
|
-
const rules = [...permissions].map(permissionToRule).filter(isAbilityRule);
|
|
1048
|
-
for (const role of /* @__PURE__ */ new Set([...source.roles ?? [], ...options.roles ?? []])) {
|
|
1049
|
-
rules.push(...(options.roleRules?.[role] ?? []).map(cloneRule));
|
|
1050
|
-
}
|
|
1051
|
-
rules.push(...(options.rules ?? []).map(cloneRule));
|
|
1052
|
-
return rules;
|
|
1053
|
-
}
|
|
1054
|
-
function permissionToRule(permission) {
|
|
1055
|
-
const normalized = permission.trim();
|
|
1056
|
-
if (!normalized) return null;
|
|
1057
|
-
if (normalized === "*" || normalized === "*:*" || normalized === "all:manage") {
|
|
1058
|
-
return { action: "manage", subject: "all" };
|
|
1059
|
-
}
|
|
1060
|
-
const separator = normalized.lastIndexOf(":");
|
|
1061
|
-
if (separator <= 0 || separator === normalized.length - 1) return null;
|
|
1062
|
-
const subject = normalized.slice(0, separator);
|
|
1063
|
-
const action = normalized.slice(separator + 1);
|
|
1064
|
-
return {
|
|
1065
|
-
action: action === "*" ? "manage" : action,
|
|
1066
|
-
subject: subject === "*" ? "all" : subject
|
|
1067
|
-
};
|
|
1068
|
-
}
|
|
1069
|
-
function matchesToken(value, expected, wildcard) {
|
|
1070
|
-
return (Array.isArray(value) ? value : [value]).some(
|
|
1071
|
-
(candidate) => candidate === expected || candidate === wildcard || candidate === "*"
|
|
1072
|
-
);
|
|
1073
|
-
}
|
|
1074
|
-
function matchesField(pattern, field) {
|
|
1075
|
-
if (pattern === "*" || pattern === field) return true;
|
|
1076
|
-
return pattern.endsWith(".*") && field.startsWith(pattern.slice(0, -1));
|
|
1077
|
-
}
|
|
1078
|
-
function matchesConditions(subject, conditions) {
|
|
1079
|
-
return Object.entries(conditions).every(([path, expected]) => {
|
|
1080
|
-
if (path === "$and" && Array.isArray(expected)) {
|
|
1081
|
-
return expected.every(
|
|
1082
|
-
(condition) => matchesConditions(subject, condition)
|
|
1083
|
-
);
|
|
1084
|
-
}
|
|
1085
|
-
if (path === "$or" && Array.isArray(expected)) {
|
|
1086
|
-
return expected.some(
|
|
1087
|
-
(condition) => matchesConditions(subject, condition)
|
|
1088
|
-
);
|
|
1089
|
-
}
|
|
1090
|
-
return matchesValue(readPath(subject, path), expected);
|
|
1091
|
-
});
|
|
1092
|
-
}
|
|
1093
|
-
function matchesValue(actual, expected) {
|
|
1094
|
-
if (!isRecord2(expected) || !Object.keys(expected).some((key) => key.startsWith("$"))) {
|
|
1095
|
-
return isRecord2(expected) && isRecord2(actual) ? matchesConditions(actual, expected) : Object.is(actual, expected);
|
|
1096
|
-
}
|
|
1097
|
-
return Object.entries(expected).every(([operator, operand]) => {
|
|
1098
|
-
switch (operator) {
|
|
1099
|
-
case "$eq":
|
|
1100
|
-
return Object.is(actual, operand);
|
|
1101
|
-
case "$ne":
|
|
1102
|
-
return !Object.is(actual, operand);
|
|
1103
|
-
case "$in":
|
|
1104
|
-
return Array.isArray(operand) && operand.some((value) => Object.is(actual, value));
|
|
1105
|
-
case "$nin":
|
|
1106
|
-
return Array.isArray(operand) && !operand.some((value) => Object.is(actual, value));
|
|
1107
|
-
case "$gt":
|
|
1108
|
-
return typeof actual === "number" && typeof operand === "number" && actual > operand;
|
|
1109
|
-
case "$gte":
|
|
1110
|
-
return typeof actual === "number" && typeof operand === "number" && actual >= operand;
|
|
1111
|
-
case "$lt":
|
|
1112
|
-
return typeof actual === "number" && typeof operand === "number" && actual < operand;
|
|
1113
|
-
case "$lte":
|
|
1114
|
-
return typeof actual === "number" && typeof operand === "number" && actual <= operand;
|
|
1115
|
-
case "$exists":
|
|
1116
|
-
return operand ? actual !== void 0 : actual === void 0;
|
|
1117
|
-
default:
|
|
1118
|
-
return false;
|
|
1119
|
-
}
|
|
1120
|
-
});
|
|
1121
|
-
}
|
|
1122
|
-
function readPath(value, path) {
|
|
1123
|
-
return path.split(".").reduce((current, part) => isRecord2(current) ? current[part] : void 0, value);
|
|
1124
|
-
}
|
|
1125
|
-
function defaultSubjectType(subject) {
|
|
1126
|
-
const explicit = subject.__type ?? subject.type ?? subject.kind;
|
|
1127
|
-
if (typeof explicit === "string") return explicit;
|
|
1128
|
-
const constructorName = subject.constructor?.name;
|
|
1129
|
-
return typeof constructorName === "string" ? constructorName : "Object";
|
|
1130
|
-
}
|
|
1131
|
-
function cloneRule(rule) {
|
|
1132
|
-
return {
|
|
1133
|
-
...rule,
|
|
1134
|
-
action: Array.isArray(rule.action) ? [...rule.action] : rule.action,
|
|
1135
|
-
subject: Array.isArray(rule.subject) ? [...rule.subject] : rule.subject,
|
|
1136
|
-
fields: rule.fields ? [...rule.fields] : void 0
|
|
1137
|
-
};
|
|
1138
|
-
}
|
|
1139
|
-
function isRecord2(value) {
|
|
1140
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1141
|
-
}
|
|
1142
|
-
function isAbilityRule(value) {
|
|
1143
|
-
return value !== null;
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
// src/integrations/authorization.ts
|
|
1147
|
-
var AuthorizationError = class extends Error {
|
|
1148
|
-
constructor(status, code, message) {
|
|
1149
|
-
super(message);
|
|
1150
|
-
this.status = status;
|
|
1151
|
-
this.code = code;
|
|
1152
|
-
this.name = "AuthorizationError";
|
|
1153
|
-
}
|
|
1154
|
-
};
|
|
1155
|
-
async function authorizeToken(client, token, options = {}) {
|
|
1156
|
-
if (!token) throw unauthenticated();
|
|
1157
|
-
let userId;
|
|
1158
|
-
let permissions = [];
|
|
1159
|
-
let roles = [];
|
|
1160
|
-
let scope;
|
|
1161
|
-
if (options.verification === "jwks") {
|
|
1162
|
-
if (!options.jwksVerifier) {
|
|
1163
|
-
throw new Error("Authyon: `jwksVerifier` is required for JWKS verification");
|
|
1164
|
-
}
|
|
1165
|
-
let verified;
|
|
1166
|
-
try {
|
|
1167
|
-
verified = await options.jwksVerifier.verifyAccessToken(token);
|
|
1168
|
-
} catch (error) {
|
|
1169
|
-
if (!(error instanceof TokenVerificationError)) throw error;
|
|
1170
|
-
if (error.code === "token.key_unavailable") {
|
|
1171
|
-
throw new AuthorizationError(
|
|
1172
|
-
503,
|
|
1173
|
-
"authorization.unavailable",
|
|
1174
|
-
"Token verification is temporarily unavailable"
|
|
1175
|
-
);
|
|
1176
|
-
}
|
|
1177
|
-
throw unauthenticated();
|
|
1178
|
-
}
|
|
1179
|
-
const { claims } = verified;
|
|
1180
|
-
userId = claims.sub;
|
|
1181
|
-
permissions = claims.permissions ?? [];
|
|
1182
|
-
roles = claims.roles ?? [];
|
|
1183
|
-
scope = claims.scope;
|
|
1184
|
-
} else if (options.verification === "introspect") {
|
|
1185
|
-
const identity = await client.introspect(token, { clientIp: options.clientIp });
|
|
1186
|
-
if (!identity.active) throw unauthenticated();
|
|
1187
|
-
userId = identity.sub;
|
|
1188
|
-
permissions = identity.permissions ?? [];
|
|
1189
|
-
roles = identity.roles ?? [];
|
|
1190
|
-
scope = identity.scope;
|
|
1191
|
-
} else {
|
|
1192
|
-
const identity = await client.validate(token, { clientIp: options.clientIp });
|
|
1193
|
-
if (!identity.valid || !identity.user) throw unauthenticated();
|
|
1194
|
-
userId = identity.user.id;
|
|
1195
|
-
permissions = identity.user.permissions ?? [];
|
|
1196
|
-
}
|
|
1197
|
-
const ability = createAuthyonAbility(
|
|
1198
|
-
{ permissions, roles, scope },
|
|
1199
|
-
{
|
|
1200
|
-
rules: options.rules,
|
|
1201
|
-
roleRules: options.roleRules,
|
|
1202
|
-
roles: options.roles,
|
|
1203
|
-
detectSubjectType: options.detectSubjectType
|
|
1204
|
-
}
|
|
1205
|
-
);
|
|
1206
|
-
if (options.requirement && !canAccess(ability, options.requirement)) {
|
|
1207
|
-
throw new AuthorizationError(403, "authorization.forbidden", "Permission denied");
|
|
1208
|
-
}
|
|
1209
|
-
return { token, userId, ability, permissions, roles };
|
|
1210
|
-
}
|
|
1211
|
-
function authorizeRequest(client, request, options = {}) {
|
|
1212
|
-
const clientIp = options.resolveClientIp?.(request) ?? options.clientIp;
|
|
1213
|
-
return authorizeToken(client, bearerToken(request.headers.get("authorization")), {
|
|
1214
|
-
...options,
|
|
1215
|
-
clientIp
|
|
1216
|
-
});
|
|
1217
|
-
}
|
|
1218
|
-
function createExpressAuthorizationMiddleware(client, options = {}) {
|
|
1219
|
-
return async (request, response, next) => {
|
|
1220
|
-
try {
|
|
1221
|
-
const header = request.headers.authorization;
|
|
1222
|
-
const clientIp = resolveExpressClientIp(request, options);
|
|
1223
|
-
const context = await authorizeToken(
|
|
1224
|
-
client,
|
|
1225
|
-
bearerToken(Array.isArray(header) ? header[0] : header),
|
|
1226
|
-
{ ...options, clientIp }
|
|
1227
|
-
);
|
|
1228
|
-
request[options.requestProperty ?? "authyon"] = context;
|
|
1229
|
-
next();
|
|
1230
|
-
} catch (error) {
|
|
1231
|
-
if (options.passErrorsToNext || !(error instanceof AuthorizationError)) {
|
|
1232
|
-
next(error);
|
|
1233
|
-
return;
|
|
1234
|
-
}
|
|
1235
|
-
response.status(error.status).json({ code: error.code, message: error.message });
|
|
1236
|
-
}
|
|
1237
|
-
};
|
|
1238
|
-
}
|
|
1239
|
-
function resolveExpressClientIp(request, options) {
|
|
1240
|
-
const resolved = options.resolveClientIp?.(request) ?? (options.trustProxy ? request.ip : request.socket?.remoteAddress);
|
|
1241
|
-
return resolved ? normalizeClientIp(stripIpv4Prefix(resolved)) : void 0;
|
|
1242
|
-
}
|
|
1243
|
-
function stripIpv4Prefix(value) {
|
|
1244
|
-
return value.startsWith("::ffff:") ? value.slice(7) : value;
|
|
1245
|
-
}
|
|
1246
|
-
function createAuthorizationErrorResponse(error) {
|
|
1247
|
-
if (!(error instanceof AuthorizationError)) throw error;
|
|
1248
|
-
return Response.json({ code: error.code, message: error.message }, { status: error.status });
|
|
1249
|
-
}
|
|
1250
|
-
function canAccess(ability, requirement) {
|
|
1251
|
-
return ability.can(requirement.action, requirement.subject, requirement.field);
|
|
1252
|
-
}
|
|
1253
|
-
function bearerToken(header) {
|
|
1254
|
-
const match = header?.match(/^Bearer\s+(.+)$/i);
|
|
1255
|
-
if (!match?.[1]) throw unauthenticated();
|
|
1256
|
-
return match[1].trim();
|
|
1257
|
-
}
|
|
1258
|
-
function unauthenticated() {
|
|
1259
|
-
return new AuthorizationError(
|
|
1260
|
-
401,
|
|
1261
|
-
"authorization.unauthenticated",
|
|
1262
|
-
"A valid bearer token is required"
|
|
1263
|
-
);
|
|
1264
|
-
}
|
|
881
|
+
_options = new WeakMap();
|
|
1265
882
|
export {
|
|
1266
883
|
AuthorizationError,
|
|
1267
884
|
AuthyonAbility,
|
|
@@ -1273,8 +890,11 @@ export {
|
|
|
1273
890
|
FetchHttpAdapter,
|
|
1274
891
|
JwksTokenVerifier,
|
|
1275
892
|
LoggingHttpAdapter,
|
|
893
|
+
PlatformScopedClient,
|
|
1276
894
|
TenantScopedClient,
|
|
1277
895
|
TokenVerificationError,
|
|
896
|
+
UserScopedClient,
|
|
897
|
+
WorkspaceInvitesClient,
|
|
1278
898
|
authorizeRequest,
|
|
1279
899
|
authorizeToken,
|
|
1280
900
|
createAuthorizationErrorResponse,
|
|
@@ -1282,5 +902,7 @@ export {
|
|
|
1282
902
|
createAuthyonRules,
|
|
1283
903
|
createClient,
|
|
1284
904
|
createExpressAuthorizationMiddleware,
|
|
905
|
+
hasPermission,
|
|
906
|
+
hasPermissionGroup,
|
|
1285
907
|
normalizeClientIp
|
|
1286
908
|
};
|