@authyon/server 0.2.0-beta.2 → 0.2.0-beta.20
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 +267 -2
- package/dist/authorization-DcA5oKZT.d.cts +129 -0
- package/dist/authorization-DfFCoDQ6.d.ts +129 -0
- package/dist/authyonServerClient-UA7HdjtD.d.cts +1232 -0
- package/dist/authyonServerClient-UA7HdjtD.d.ts +1232 -0
- package/dist/bff.cjs +1174 -0
- package/dist/bff.d.cts +149 -0
- package/dist/bff.d.ts +149 -0
- package/dist/bff.js +808 -0
- package/dist/chunk-77J4NURK.js +13 -0
- package/dist/chunk-NKWQMYB3.js +485 -0
- package/dist/chunk-TDJNBD3Z.js +354 -0
- package/dist/index.cjs +591 -64
- package/dist/index.d.cts +47 -756
- package/dist/index.d.ts +47 -756
- package/dist/index.js +537 -798
- 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,277 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
Unknown: "unknown",
|
|
34
|
-
NetworkError: "request.network_error",
|
|
35
|
-
Timeout: "request.timeout",
|
|
36
|
-
SessionMalformed: "session.malformed",
|
|
37
|
-
NotAuthenticated: "auth.not_authenticated",
|
|
38
|
-
InvalidToken: "auth.invalid_token",
|
|
39
|
-
MissingToken: "auth.missing_token",
|
|
40
|
-
EmailTaken: "user.email_taken",
|
|
41
|
-
PasswordWeak: "user.password_weak",
|
|
42
|
-
PasswordPwned: "user.password_pwned",
|
|
43
|
-
RateLimited: "rate_limited"
|
|
44
|
-
};
|
|
45
|
-
var AuthyonError = class extends Error {
|
|
46
|
-
constructor(status, body, options = {}) {
|
|
47
|
-
super(body.detail ?? body.title ?? `Authyon request failed with status ${status}`);
|
|
48
|
-
this.name = "AuthyonError";
|
|
49
|
-
this.status = status;
|
|
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
|
-
}
|
|
134
|
-
}
|
|
135
|
-
function isRetryable(category) {
|
|
136
|
-
return category === "network" || category === "timeout" || category === "rate_limit" || category === "server";
|
|
137
|
-
}
|
|
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-TDJNBD3Z.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";
|
|
138
33
|
|
|
139
|
-
//
|
|
140
|
-
function
|
|
141
|
-
if (!
|
|
142
|
-
|
|
143
|
-
for (const [key, value2] of Object.entries(params)) {
|
|
144
|
-
if (value2 !== void 0) query.set(key, String(value2));
|
|
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.");
|
|
145
38
|
}
|
|
146
|
-
|
|
147
|
-
return value ? `${path}${path.includes("?") ? "&" : "?"}${value}` : path;
|
|
39
|
+
return encodeURIComponent(value);
|
|
148
40
|
}
|
|
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;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
log(event) {
|
|
199
|
-
if (!this.options.enabled) return;
|
|
200
|
-
const logger = this.options.logger ?? defaultHttpLogger;
|
|
201
|
-
try {
|
|
202
|
-
logger(event);
|
|
203
|
-
} catch {
|
|
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.");
|
|
204
54
|
}
|
|
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.");
|
|
205
63
|
}
|
|
206
|
-
};
|
|
207
|
-
function defaultHttpLogger(event) {
|
|
208
|
-
console.debug("[Authyon HTTP]", event);
|
|
64
|
+
return { lifetimeDays: input.lifetimeDays };
|
|
209
65
|
}
|
|
210
|
-
function
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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 };
|
|
216
75
|
}
|
|
217
|
-
|
|
218
|
-
// ../../internal/core/http/transport.ts
|
|
219
|
-
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
220
|
-
var SharedTransportError = class extends Error {
|
|
221
|
-
constructor(code, cause) {
|
|
222
|
-
super(code === "request.timeout" ? "Request timed out" : "Network request failed");
|
|
223
|
-
this.name = "SharedTransportError";
|
|
224
|
-
this.code = code;
|
|
225
|
-
this.cause = cause;
|
|
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;
|
|
76
|
+
function credentialSummary(value) {
|
|
239
77
|
return {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
}
|
|
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]
|
|
261
87
|
};
|
|
262
88
|
}
|
|
263
|
-
function
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
+
};
|
|
269
104
|
}
|
|
270
|
-
function
|
|
271
|
-
const
|
|
272
|
-
const
|
|
105
|
+
async function credentialPage(request, path2, options = {}) {
|
|
106
|
+
const query = credentialQuery(options);
|
|
107
|
+
const page = await request(path2, { query });
|
|
108
|
+
if (!page || !Array.isArray(page.data))
|
|
109
|
+
throw new TypeError("Authyon: expected a paginated credential response.");
|
|
273
110
|
return {
|
|
274
|
-
|
|
275
|
-
|
|
111
|
+
data: page.data.map(credentialSummary),
|
|
112
|
+
page: page.page,
|
|
113
|
+
perPage: page.perPage,
|
|
114
|
+
total: page.total,
|
|
115
|
+
pages: page.pages,
|
|
116
|
+
hasNext: page.hasNext,
|
|
117
|
+
hasPrev: page.hasPrev
|
|
276
118
|
};
|
|
277
119
|
}
|
|
278
|
-
function
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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"
|
|
120
|
+
async function allCredentials(request, path2) {
|
|
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
|
+
path2,
|
|
127
|
+
iteration === 0 ? {} : { query: { skip, take: 100 } }
|
|
294
128
|
);
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
// ../../internal/core/http/jsonHttpClient.ts
|
|
300
|
-
var JsonHttpClient = class {
|
|
301
|
-
constructor(transport) {
|
|
302
|
-
this.transport = transport;
|
|
303
|
-
}
|
|
304
|
-
async send(path, options = {}) {
|
|
305
|
-
const headers = { ...options.headers };
|
|
306
|
-
if (options.body !== void 0) headers["Content-Type"] = "application/json";
|
|
307
|
-
try {
|
|
308
|
-
return await this.transport.request(appendQuery(path, options.query), {
|
|
309
|
-
method: options.method ?? "GET",
|
|
310
|
-
headers,
|
|
311
|
-
body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
|
|
312
|
-
});
|
|
313
|
-
} catch (cause) {
|
|
314
|
-
if (!(cause instanceof SharedTransportError)) throw cause;
|
|
315
|
-
throw new AuthyonError(0, { code: cause.code, title: cause.message }, { cause: cause.cause });
|
|
129
|
+
if (Array.isArray(page)) {
|
|
130
|
+
if (iteration !== 0)
|
|
131
|
+
throw new Error("Authyon: credential pagination changed during listing.");
|
|
132
|
+
return page.map(credentialSummary);
|
|
316
133
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}
|
|
325
|
-
throw new AuthyonError(response.status, body, responseMetadata(response));
|
|
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));
|
|
326
141
|
}
|
|
327
|
-
if (
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
async request(path, options = {}) {
|
|
331
|
-
return this.parse(await this.send(path, options));
|
|
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;
|
|
332
145
|
}
|
|
333
|
-
|
|
334
|
-
|
|
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");
|
|
146
|
+
throw new Error("Authyon: use listPage for more than 1000 credential pages.");
|
|
355
147
|
}
|
|
356
148
|
|
|
357
|
-
//
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
}
|
|
363
|
-
var
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
this.code = code;
|
|
373
|
-
this.name = "TokenVerificationError";
|
|
374
|
-
if (options.cause !== void 0) Object.defineProperty(this, "cause", { value: options.cause });
|
|
375
|
-
}
|
|
149
|
+
// ../../internal/core/security/approvals.ts
|
|
150
|
+
function path(id, suffix = "") {
|
|
151
|
+
if (typeof id !== "string" || !id.trim())
|
|
152
|
+
throw new TypeError("Authyon: an approval id is required.");
|
|
153
|
+
return `/auth/authorizations/${encodeURIComponent(id)}${suffix}`;
|
|
154
|
+
}
|
|
155
|
+
var NO_METHODS = {
|
|
156
|
+
password: false,
|
|
157
|
+
authenticator: false,
|
|
158
|
+
emailCode: false,
|
|
159
|
+
passkey: false,
|
|
160
|
+
passkeyCount: 0,
|
|
161
|
+
recoveryCodes: 0,
|
|
162
|
+
sso: [],
|
|
163
|
+
twoFactor: false
|
|
376
164
|
};
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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 });
|
|
165
|
+
function subjectSecurity(request) {
|
|
166
|
+
return {
|
|
167
|
+
methods: async () => {
|
|
168
|
+
const me = await request("/auth/me");
|
|
169
|
+
return me.security ?? NO_METHODS;
|
|
170
|
+
},
|
|
171
|
+
approvals: {
|
|
172
|
+
get: (id) => request(path(id)),
|
|
173
|
+
webauthnOptions: (id) => request(path(id, "/webauthn/options"), { method: "POST" }),
|
|
174
|
+
confirm: (id, input) => request(path(id, "/confirm"), { method: "POST", body: input }),
|
|
175
|
+
reject: (id) => request(path(id, "/reject"), { method: "POST" })
|
|
408
176
|
}
|
|
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
|
-
}
|
|
177
|
+
};
|
|
416
178
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
179
|
+
|
|
180
|
+
// src/client/scopedManagementClient.ts
|
|
181
|
+
function scopedRequest(request, source) {
|
|
182
|
+
return async (path2, options = {}) => {
|
|
183
|
+
const token = typeof source === "function" ? await source() : source;
|
|
184
|
+
if (typeof token !== "string" || !token.trim() || /\s/u.test(token))
|
|
185
|
+
throw new TypeError("Authyon: a valid access token is required.");
|
|
186
|
+
return request(path2, {
|
|
187
|
+
...options,
|
|
188
|
+
headers: { ...options.headers, Authorization: `Bearer ${token}` }
|
|
189
|
+
});
|
|
190
|
+
};
|
|
422
191
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
192
|
+
var _request;
|
|
193
|
+
var UserScopedClient = class {
|
|
194
|
+
/** @hidden */
|
|
195
|
+
constructor(request, token) {
|
|
196
|
+
__privateAdd(this, _request);
|
|
197
|
+
this.tenants = {
|
|
198
|
+
members: {
|
|
199
|
+
list: (tenantId, options = {}) => __privateGet(this, _request).call(this, `/auth/tenants/${segment(tenantId)}/members`, { query: options }),
|
|
200
|
+
/** Adds an existing environment user by email; does not send an invitation email. */
|
|
201
|
+
invite: (tenantId, input) => __privateGet(this, _request).call(this, `/auth/tenants/${segment(tenantId)}/members`, {
|
|
202
|
+
method: "POST",
|
|
203
|
+
body: { email: input.email, roles: input.roles }
|
|
204
|
+
}),
|
|
205
|
+
remove: (tenantId, userId) => __privateGet(this, _request).call(this, `/auth/tenants/${segment(tenantId)}/members/${segment(userId)}`, {
|
|
206
|
+
method: "DELETE"
|
|
207
|
+
})
|
|
208
|
+
},
|
|
209
|
+
/**
|
|
210
|
+
* Invites by e-mail or by link, acting as the user. Requires the
|
|
211
|
+
* `tenants:members:invite` permission on the tenant.
|
|
212
|
+
*/
|
|
213
|
+
invites: {
|
|
214
|
+
list: (tenantId) => __privateGet(this, _request).call(this, `/auth/tenants/${segment(tenantId)}/invites`),
|
|
215
|
+
/** Always returns `acceptUrl`; `sendEmail: false` skips the e-mail. */
|
|
216
|
+
create: (tenantId, input) => __privateGet(this, _request).call(this, `/auth/tenants/${segment(tenantId)}/invites`, {
|
|
217
|
+
method: "POST",
|
|
218
|
+
body: {
|
|
219
|
+
email: input.email,
|
|
220
|
+
roles: input.roles,
|
|
221
|
+
sendEmail: input.sendEmail ?? true,
|
|
222
|
+
redirectUri: input.redirectUri
|
|
223
|
+
}
|
|
224
|
+
}),
|
|
225
|
+
/** New link (the previous one stops working), mailed unless `sendEmail` is `false`. */
|
|
226
|
+
resend: (tenantId, inviteId, input = {}) => __privateGet(this, _request).call(this, `/auth/tenants/${segment(tenantId)}/invites/${segment(inviteId)}/resend`, {
|
|
227
|
+
method: "POST",
|
|
228
|
+
body: { sendEmail: input.sendEmail ?? true }
|
|
229
|
+
}),
|
|
230
|
+
revoke: (tenantId, inviteId) => __privateGet(this, _request).call(this, `/auth/tenants/${segment(tenantId)}/invites/${segment(inviteId)}`, {
|
|
231
|
+
method: "DELETE"
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
__privateSet(this, _request, scopedRequest(request, token));
|
|
236
|
+
this.security = subjectSecurity(__privateGet(this, _request));
|
|
438
237
|
}
|
|
439
|
-
|
|
440
|
-
|
|
238
|
+
};
|
|
239
|
+
_request = new WeakMap();
|
|
240
|
+
|
|
241
|
+
// ../../internal/core/auth/expiringTokenProvider.ts
|
|
242
|
+
var _token, _inFlight;
|
|
243
|
+
var ExpiringTokenProvider = class {
|
|
244
|
+
constructor(acquire, expirySkewMs = DEFAULT_EXPIRY_SKEW_MS) {
|
|
245
|
+
this.acquire = acquire;
|
|
246
|
+
this.expirySkewMs = expirySkewMs;
|
|
247
|
+
__privateAdd(this, _token, null);
|
|
248
|
+
__privateAdd(this, _inFlight, null);
|
|
441
249
|
}
|
|
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`);
|
|
250
|
+
async getAccessToken() {
|
|
251
|
+
if (__privateGet(this, _token) && Date.now() < __privateGet(this, _token).expiresAt - this.expirySkewMs) {
|
|
252
|
+
return __privateGet(this, _token).accessToken;
|
|
451
253
|
}
|
|
254
|
+
if (__privateGet(this, _inFlight)) return __privateGet(this, _inFlight);
|
|
255
|
+
__privateSet(this, _inFlight, this.acquire().then((token) => {
|
|
256
|
+
__privateSet(this, _token, token);
|
|
257
|
+
return token.accessToken;
|
|
258
|
+
}).finally(() => {
|
|
259
|
+
__privateSet(this, _inFlight, null);
|
|
260
|
+
}));
|
|
261
|
+
return __privateGet(this, _inFlight);
|
|
452
262
|
}
|
|
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";
|
|
263
|
+
clear() {
|
|
264
|
+
__privateSet(this, _token, null);
|
|
477
265
|
}
|
|
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
|
-
}
|
|
266
|
+
};
|
|
267
|
+
_token = new WeakMap();
|
|
268
|
+
_inFlight = new WeakMap();
|
|
495
269
|
|
|
496
270
|
// src/client/authyonServerClient.ts
|
|
271
|
+
var _clientSecret;
|
|
497
272
|
var AuthyonServerClient = class {
|
|
498
273
|
constructor(options = {}) {
|
|
274
|
+
__privateAdd(this, _clientSecret);
|
|
499
275
|
// ── Discovery ──────────────────────────────────────────────────────────────
|
|
500
276
|
this.discovery = {
|
|
501
277
|
/** GET /.well-known/jwks.json — the environment's public signing keys. */
|
|
@@ -529,6 +305,14 @@ var AuthyonServerClient = class {
|
|
|
529
305
|
client_id: credentials.clientId,
|
|
530
306
|
client_secret: credentials.clientSecret
|
|
531
307
|
}
|
|
308
|
+
}),
|
|
309
|
+
/** POST /tenant/auth/validate — validates an already-minted tenant-client bearer token. */
|
|
310
|
+
validate: (accessToken, context) => this.request("/tenant/auth/validate", {
|
|
311
|
+
method: "POST",
|
|
312
|
+
headers: {
|
|
313
|
+
...clientIpHeaders(context),
|
|
314
|
+
Authorization: `Bearer ${accessToken}`
|
|
315
|
+
}
|
|
532
316
|
})
|
|
533
317
|
};
|
|
534
318
|
// ── Environment management (users, tenants, roles, permissions, audit) ───
|
|
@@ -574,6 +358,39 @@ var AuthyonServerClient = class {
|
|
|
574
358
|
envBearer: true,
|
|
575
359
|
body: { newPassword, reason }
|
|
576
360
|
}),
|
|
361
|
+
/**
|
|
362
|
+
* POST /env/password-resets — issues a password-reset token for the
|
|
363
|
+
* account with this address and returns it, so your application can
|
|
364
|
+
* deliver it in its own e-mail instead of Authyon's. Needs the
|
|
365
|
+
* `authyon:users:password` scope. Unlike the public forgot-password
|
|
366
|
+
* request, an unknown address rejects with `user.not_found` (404).
|
|
367
|
+
* A previous live token for the same account stops working.
|
|
368
|
+
*/
|
|
369
|
+
issuePasswordReset: (input) => this.request("/env/password-resets", {
|
|
370
|
+
method: "POST",
|
|
371
|
+
envBearer: true,
|
|
372
|
+
body: { ...input, sendEmail: input.sendEmail ?? false }
|
|
373
|
+
}),
|
|
374
|
+
/**
|
|
375
|
+
* POST /auth/password-reset/validate — for a backend hosting the reset
|
|
376
|
+
* page: checks a token without spending it. Public, needs only the
|
|
377
|
+
* `envKey`. Rejects with `user.password_reset.invalid` (401) when the
|
|
378
|
+
* token is unknown, used or expired.
|
|
379
|
+
*/
|
|
380
|
+
validatePasswordReset: (token) => this.request("/auth/password-reset/validate", { method: "POST", body: { token } }),
|
|
381
|
+
/**
|
|
382
|
+
* POST /auth/password-reset/confirm — redeems the token and sets the new
|
|
383
|
+
* password (every session of the account is signed out). Public, needs
|
|
384
|
+
* only the `envKey`. Resolves with the `redirectUri` the reset was issued
|
|
385
|
+
* with, or `null`.
|
|
386
|
+
*/
|
|
387
|
+
confirmPasswordReset: async (token, newPassword) => {
|
|
388
|
+
const done = await this.request(
|
|
389
|
+
"/auth/password-reset/confirm",
|
|
390
|
+
{ method: "POST", body: { token, newPassword } }
|
|
391
|
+
);
|
|
392
|
+
return { redirectUri: done?.redirectUri ?? null };
|
|
393
|
+
},
|
|
577
394
|
/** POST /env/users/{userId}/sessions/revoke-all — revokes every session the user has. */
|
|
578
395
|
revokeSessions: (userId, reason) => this.request(`/env/users/${encodeURIComponent(userId)}/sessions/revoke-all`, {
|
|
579
396
|
method: "POST",
|
|
@@ -622,7 +439,63 @@ var AuthyonServerClient = class {
|
|
|
622
439
|
)
|
|
623
440
|
},
|
|
624
441
|
tenants: {
|
|
625
|
-
|
|
442
|
+
credentials: {
|
|
443
|
+
/** GET /env/tenants/{tenantId}/credentials. Returns metadata only, including revoked credentials. */
|
|
444
|
+
list: (tenantId) => allCredentials(
|
|
445
|
+
(path2, options) => this.request(path2, { ...options, envBearer: true }),
|
|
446
|
+
`/env/tenants/${segment(tenantId)}/credentials`
|
|
447
|
+
),
|
|
448
|
+
/** Paginated searchable listing. Prefer this over loading every credential. */
|
|
449
|
+
listPage: (tenantId, options = {}) => credentialPage(
|
|
450
|
+
(path2, requestOptions) => this.request(path2, { ...requestOptions, envBearer: true }),
|
|
451
|
+
`/env/tenants/${segment(tenantId)}/credentials`,
|
|
452
|
+
options
|
|
453
|
+
),
|
|
454
|
+
get: async (tenantId, credentialId) => credentialDetail(
|
|
455
|
+
await this.request(
|
|
456
|
+
`/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}`,
|
|
457
|
+
{ envBearer: true }
|
|
458
|
+
)
|
|
459
|
+
),
|
|
460
|
+
/** Explicit scopes/permissions; only issuance returns the client secret. */
|
|
461
|
+
create: (tenantId, input) => this.request(`/env/tenants/${segment(tenantId)}/credentials`, {
|
|
462
|
+
method: "POST",
|
|
463
|
+
envBearer: true,
|
|
464
|
+
body: {
|
|
465
|
+
description: input.description,
|
|
466
|
+
...lifetimeBody(input),
|
|
467
|
+
...permissionsBody(input)
|
|
468
|
+
}
|
|
469
|
+
}),
|
|
470
|
+
/** Returns a new secret once while keeping the same client id. */
|
|
471
|
+
rotate: (tenantId, credentialId) => this.request(
|
|
472
|
+
`/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}/rotate`,
|
|
473
|
+
{ method: "POST", envBearer: true }
|
|
474
|
+
),
|
|
475
|
+
updatePermissions: (tenantId, credentialId, input) => this.request(
|
|
476
|
+
`/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}/permissions`,
|
|
477
|
+
{ method: "PUT", envBearer: true, body: permissionsBody(input) }
|
|
478
|
+
),
|
|
479
|
+
updateScopes: (tenantId, credentialId, scopes) => this.environment.tenants.credentials.updatePermissions(tenantId, credentialId, {
|
|
480
|
+
scopes
|
|
481
|
+
}),
|
|
482
|
+
revoke: (tenantId, credentialId) => this.request(`/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}`, {
|
|
483
|
+
method: "DELETE",
|
|
484
|
+
envBearer: true
|
|
485
|
+
})
|
|
486
|
+
},
|
|
487
|
+
/**
|
|
488
|
+
* GET /env/tenants — one page of the environment's tenants.
|
|
489
|
+
*
|
|
490
|
+
* `search` matches, case-insensitively and anywhere in the value, the
|
|
491
|
+
* name, the slug, the description and the raw text of both metadata
|
|
492
|
+
* documents — so a tenant filed with `{"cnpj":"00.000.000/0001-00"}`
|
|
493
|
+
* is found by the number, and `cnpj` alone finds every tenant carrying
|
|
494
|
+
* that key. `take` defaults to 25 and is capped at 100.
|
|
495
|
+
*
|
|
496
|
+
* Against an Authyon API older than 2026-10-21 this route ignored
|
|
497
|
+
* skip/take/search and answered with a bare array.
|
|
498
|
+
*/
|
|
626
499
|
list: (params = {}) => this.request("/env/tenants", { envBearer: true, query: params }),
|
|
627
500
|
/** POST /env/tenants — create a tenant in the environment. */
|
|
628
501
|
create: (input) => this.request("/env/tenants", { method: "POST", envBearer: true, body: input }),
|
|
@@ -634,7 +507,18 @@ var AuthyonServerClient = class {
|
|
|
634
507
|
envBearer: true,
|
|
635
508
|
body: input
|
|
636
509
|
}),
|
|
637
|
-
/**
|
|
510
|
+
/** PUT /env/tenants/{tenantId}/metadata - replace public and private metadata. */
|
|
511
|
+
updateMetadata: (tenantId, input) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/metadata`, {
|
|
512
|
+
method: "PUT",
|
|
513
|
+
envBearer: true,
|
|
514
|
+
body: input
|
|
515
|
+
}),
|
|
516
|
+
/** POST /env/tenants/{tenantId}/disable — disable without removing tenant data. */
|
|
517
|
+
disable: (tenantId) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/disable`, {
|
|
518
|
+
method: "POST",
|
|
519
|
+
envBearer: true
|
|
520
|
+
}),
|
|
521
|
+
/** DELETE /env/tenants/{tenantId} — logically delete (disable) a tenant. */
|
|
638
522
|
delete: (tenantId) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}`, {
|
|
639
523
|
method: "DELETE",
|
|
640
524
|
envBearer: true
|
|
@@ -642,10 +526,16 @@ var AuthyonServerClient = class {
|
|
|
642
526
|
members: {
|
|
643
527
|
/**
|
|
644
528
|
* GET /env/tenants/{tenantId}/members — a tenant's members as full
|
|
645
|
-
* user records
|
|
646
|
-
* `
|
|
647
|
-
*
|
|
648
|
-
*
|
|
529
|
+
* user records, with the same shape `environment.users.list()` items
|
|
530
|
+
* have, plus `tenantRoles` and `tenantPermissions` scoped to this
|
|
531
|
+
* tenant.
|
|
532
|
+
*
|
|
533
|
+
* Paged like every other list: the `Paged<T>` envelope carries
|
|
534
|
+
* `total` and `hasNext`, `take` defaults to 50 and is capped at 100.
|
|
535
|
+
* Up to the Authyon API of 2026-10-21 this one route answered with a
|
|
536
|
+
* bare array, so a caller had to page until a short page came back;
|
|
537
|
+
* against such an API `data` is undefined and the array is the body
|
|
538
|
+
* itself.
|
|
649
539
|
*/
|
|
650
540
|
list: (tenantId, params = {}) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/members`, {
|
|
651
541
|
envBearer: true,
|
|
@@ -683,6 +573,61 @@ var AuthyonServerClient = class {
|
|
|
683
573
|
{ method: "DELETE", envBearer: true }
|
|
684
574
|
)
|
|
685
575
|
},
|
|
576
|
+
/**
|
|
577
|
+
* Invites by e-mail or by link — for addresses that may not have an
|
|
578
|
+
* account yet; accepting creates it. Managing them needs the
|
|
579
|
+
* credential's `authyon:users:write` scope (listing, `authyon:tenants:read`).
|
|
580
|
+
*/
|
|
581
|
+
invites: {
|
|
582
|
+
/** GET /env/tenants/{tenantId}/invites — every invite with its status, newest first. */
|
|
583
|
+
list: (tenantId) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/invites`, { envBearer: true }),
|
|
584
|
+
/**
|
|
585
|
+
* POST /env/tenants/{tenantId}/invites — invite an address. Always
|
|
586
|
+
* returns `acceptUrl`; with `sendEmail: false` Authyon does not mail
|
|
587
|
+
* it and you deliver the link yourself. Valid 7 days, single use, and
|
|
588
|
+
* not readable again later.
|
|
589
|
+
*/
|
|
590
|
+
create: (tenantId, input) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/invites`, {
|
|
591
|
+
method: "POST",
|
|
592
|
+
envBearer: true,
|
|
593
|
+
body: {
|
|
594
|
+
email: input.email,
|
|
595
|
+
roles: input.roles,
|
|
596
|
+
sendEmail: input.sendEmail ?? true,
|
|
597
|
+
redirectUri: input.redirectUri
|
|
598
|
+
}
|
|
599
|
+
}),
|
|
600
|
+
/** POST .../invites/{inviteId}/resend — new link (the previous one stops working), mailed unless `sendEmail` is `false`. */
|
|
601
|
+
resend: (tenantId, inviteId, input = {}) => this.request(
|
|
602
|
+
`/env/tenants/${encodeURIComponent(tenantId)}/invites/${encodeURIComponent(inviteId)}/resend`,
|
|
603
|
+
{ method: "POST", envBearer: true, body: { sendEmail: input.sendEmail ?? true } }
|
|
604
|
+
),
|
|
605
|
+
/** DELETE /env/tenants/{tenantId}/invites/{inviteId} — revoke a pending invite. */
|
|
606
|
+
revoke: (tenantId, inviteId) => this.request(
|
|
607
|
+
`/env/tenants/${encodeURIComponent(tenantId)}/invites/${encodeURIComponent(inviteId)}`,
|
|
608
|
+
{ method: "DELETE", envBearer: true }
|
|
609
|
+
),
|
|
610
|
+
/**
|
|
611
|
+
* POST /auth/tenant-invites/preview — for a backend hosting the accept
|
|
612
|
+
* page. Public: needs only `envKey`, the token is the credential.
|
|
613
|
+
*/
|
|
614
|
+
preview: (token) => this.request("/auth/tenant-invites/preview", { method: "POST", body: { token } }),
|
|
615
|
+
/**
|
|
616
|
+
* Joins the tenant; for an address with no account, also creates it
|
|
617
|
+
* (password required) with the e-mail already confirmed. Does not
|
|
618
|
+
* sign the user in.
|
|
619
|
+
*
|
|
620
|
+
* Without metadata: POST /auth/tenant-invites/accept — public, needs
|
|
621
|
+
* only `envKey`. With `publicMetadata`/`privateMetadata`: POST
|
|
622
|
+
* /env/tenant-invites/accept with the environment credential
|
|
623
|
+
* (`authyon:users:write`), since only your backend may set them.
|
|
624
|
+
*/
|
|
625
|
+
accept: (input) => input.publicMetadata !== void 0 || input.privateMetadata !== void 0 ? this.request("/env/tenant-invites/accept", {
|
|
626
|
+
method: "POST",
|
|
627
|
+
envBearer: true,
|
|
628
|
+
body: input
|
|
629
|
+
}) : this.request("/auth/tenant-invites/accept", { method: "POST", body: input })
|
|
630
|
+
},
|
|
686
631
|
roles: {
|
|
687
632
|
/** GET /env/tenants/{tenantId}/roles — list a tenant's roles. */
|
|
688
633
|
list: (tenantId) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/roles`, { envBearer: true }),
|
|
@@ -752,6 +697,60 @@ var AuthyonServerClient = class {
|
|
|
752
697
|
loginActivity: (params = {}) => this.request("/env/audit/login-activity", { envBearer: true, query: params })
|
|
753
698
|
}
|
|
754
699
|
};
|
|
700
|
+
// ── Security: one-time codes and approvals ───────────────────────────────
|
|
701
|
+
//
|
|
702
|
+
// Everything here runs with the environment credential: `otp` needs the
|
|
703
|
+
// `authyon:otp:verify` scope, `approvals` needs `authyon:financial:authorize`.
|
|
704
|
+
// Keep those credentials on the server.
|
|
705
|
+
this.security = {
|
|
706
|
+
otp: {
|
|
707
|
+
/**
|
|
708
|
+
* POST /env/users/{userId}/otp/verify — checks the user's authenticator
|
|
709
|
+
* app (TOTP) code. A wrong code returns `{ valid: false }` instead of
|
|
710
|
+
* throwing. Each code is accepted once; 3 wrong codes in a row sign the
|
|
711
|
+
* user out everywhere (`sessionsRevoked: true`); 5 lock the check for 15
|
|
712
|
+
* minutes (`rate_limited`). More than 50 wrong codes in 10 minutes from the
|
|
713
|
+
* same credential pause it (`credential_throttled`). Needs `authyon:otp:verify`.
|
|
714
|
+
*/
|
|
715
|
+
check: (userId, code) => this.request(`/env/users/${segment(userId)}/otp/verify`, {
|
|
716
|
+
method: "POST",
|
|
717
|
+
envBearer: true,
|
|
718
|
+
body: { code }
|
|
719
|
+
})
|
|
720
|
+
},
|
|
721
|
+
/**
|
|
722
|
+
* The customer approves a payload with their authenticator code or
|
|
723
|
+
* passkey, once.
|
|
724
|
+
*
|
|
725
|
+
* 1. `create()` the approval with the payload and the customer.
|
|
726
|
+
* 2. The customer confirms it (`@authyon/auth` → `security.approvals.confirm`).
|
|
727
|
+
* 3. `consume()` it right before executing, and execute the returned payload.
|
|
728
|
+
*/
|
|
729
|
+
approvals: {
|
|
730
|
+
/** POST /env/authorizations — registers a payload awaiting the customer's approval. */
|
|
731
|
+
create: (input) => {
|
|
732
|
+
const { idempotencyKey, ...body } = input;
|
|
733
|
+
return this.request("/env/authorizations", {
|
|
734
|
+
method: "POST",
|
|
735
|
+
envBearer: true,
|
|
736
|
+
headers: idempotencyKey ? { "Idempotency-Key": idempotencyKey } : void 0,
|
|
737
|
+
body
|
|
738
|
+
});
|
|
739
|
+
},
|
|
740
|
+
/** GET /env/authorizations/{id} — current status and, once approved, the assurance evidence. */
|
|
741
|
+
get: (id) => this.request(`/env/authorizations/${segment(id)}`, { envBearer: true }),
|
|
742
|
+
/**
|
|
743
|
+
* POST /env/authorizations/{id}/consume — redeems an approved approval
|
|
744
|
+
* exactly once and returns the approved payload. Pass `payload` to have
|
|
745
|
+
* the API check it matches (`payload_mismatch` otherwise).
|
|
746
|
+
*/
|
|
747
|
+
consume: (id, payload) => this.request(`/env/authorizations/${segment(id)}/consume`, {
|
|
748
|
+
method: "POST",
|
|
749
|
+
envBearer: true,
|
|
750
|
+
body: payload === void 0 ? void 0 : { payload }
|
|
751
|
+
})
|
|
752
|
+
}
|
|
753
|
+
};
|
|
755
754
|
// ── Generic permission grants ─────────────────────────────────────────────
|
|
756
755
|
this.permissions = {
|
|
757
756
|
/** POST /users/{userId}/permissions — grant a permission directly to a user. */
|
|
@@ -768,7 +767,8 @@ var AuthyonServerClient = class {
|
|
|
768
767
|
};
|
|
769
768
|
this.envKey = options.envKey;
|
|
770
769
|
this.clientId = options.clientId;
|
|
771
|
-
this
|
|
770
|
+
__privateSet(this, _clientSecret, options.clientSecret);
|
|
771
|
+
this.clientIp = options.clientIp;
|
|
772
772
|
this.transport = createSharedTransport({
|
|
773
773
|
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
|
|
774
774
|
allowInsecureHttp: options.allowInsecureHttp,
|
|
@@ -779,14 +779,14 @@ var AuthyonServerClient = class {
|
|
|
779
779
|
});
|
|
780
780
|
this.http = new JsonHttpClient(this.transport);
|
|
781
781
|
this.environmentTokenProvider = new ExpiringTokenProvider(async () => {
|
|
782
|
-
if (!this.clientId || !this
|
|
782
|
+
if (!this.clientId || !__privateGet(this, _clientSecret)) {
|
|
783
783
|
throw new Error(
|
|
784
784
|
"Authyon: `clientId`/`clientSecret` are required for environment management calls (mint a pair in the console, under the environment's OAuth clients)."
|
|
785
785
|
);
|
|
786
786
|
}
|
|
787
787
|
const token = await this.environmentAuth.token({
|
|
788
788
|
clientId: this.clientId,
|
|
789
|
-
clientSecret: this
|
|
789
|
+
clientSecret: __privateGet(this, _clientSecret)
|
|
790
790
|
});
|
|
791
791
|
return {
|
|
792
792
|
accessToken: token.access_token,
|
|
@@ -794,13 +794,16 @@ var AuthyonServerClient = class {
|
|
|
794
794
|
};
|
|
795
795
|
});
|
|
796
796
|
}
|
|
797
|
-
async request(
|
|
798
|
-
const headers = {
|
|
797
|
+
async request(path2, options = {}) {
|
|
798
|
+
const headers = {
|
|
799
|
+
...clientIpHeaders({ clientIp: this.clientIp }),
|
|
800
|
+
...options.headers
|
|
801
|
+
};
|
|
799
802
|
if (options.envBearer) {
|
|
800
803
|
headers.Authorization = `Bearer ${await this.getEnvironmentAccessToken()}`;
|
|
801
804
|
}
|
|
802
805
|
if (this.envKey) headers["X-Authyon-Environment"] = this.envKey;
|
|
803
|
-
return this.http.request(
|
|
806
|
+
return this.http.request(path2, { ...options, headers });
|
|
804
807
|
}
|
|
805
808
|
/** Mints (and caches) an environment access token from `clientId`/`clientSecret`. */
|
|
806
809
|
async getEnvironmentAccessToken() {
|
|
@@ -855,26 +858,52 @@ var AuthyonServerClient = class {
|
|
|
855
858
|
tenant(credentials) {
|
|
856
859
|
return new TenantScopedClient(this, credentials);
|
|
857
860
|
}
|
|
861
|
+
/** Uses an end-user token; the configured environment key selects the environment. */
|
|
862
|
+
user(token) {
|
|
863
|
+
return new UserScopedClient((path2, options) => this.request(path2, options), token);
|
|
864
|
+
}
|
|
858
865
|
/** @internal used by {@link TenantScopedClient} to share the base URL/fetch/error handling. */
|
|
859
|
-
_requestAsTenant(accessToken,
|
|
860
|
-
return this.request(
|
|
866
|
+
_requestAsTenant(accessToken, path2, options = {}) {
|
|
867
|
+
return this.request(path2, {
|
|
861
868
|
...options,
|
|
862
869
|
headers: { ...options.headers, Authorization: `Bearer ${accessToken}` },
|
|
863
870
|
envBearer: false
|
|
864
871
|
});
|
|
865
872
|
}
|
|
866
873
|
};
|
|
874
|
+
_clientSecret = new WeakMap();
|
|
867
875
|
var TenantScopedClient = class {
|
|
868
876
|
constructor(server, credentials) {
|
|
877
|
+
/** Invites to the token's tenant, by e-mail or by link. */
|
|
878
|
+
this.invites = {
|
|
879
|
+
/** GET /tenant/invites — every invite with its status, newest first. */
|
|
880
|
+
list: () => this.request("/tenant/invites"),
|
|
881
|
+
/** POST /tenant/invites — always returns `acceptUrl`; `sendEmail: false` skips the e-mail. */
|
|
882
|
+
create: (input) => this.request("/tenant/invites", {
|
|
883
|
+
method: "POST",
|
|
884
|
+
body: {
|
|
885
|
+
email: input.email,
|
|
886
|
+
roles: input.roles,
|
|
887
|
+
sendEmail: input.sendEmail ?? true,
|
|
888
|
+
redirectUri: input.redirectUri
|
|
889
|
+
}
|
|
890
|
+
}),
|
|
891
|
+
/** POST /tenant/invites/{inviteId}/resend — new link (the previous one stops working), mailed unless `sendEmail` is `false`. */
|
|
892
|
+
resend: (inviteId, input = {}) => this.request(`/tenant/invites/${encodeURIComponent(inviteId)}/resend`, {
|
|
893
|
+
method: "POST",
|
|
894
|
+
body: { sendEmail: input.sendEmail ?? true }
|
|
895
|
+
}),
|
|
896
|
+
/** DELETE /tenant/invites/{inviteId} — revoke a pending invite. */
|
|
897
|
+
revoke: (inviteId) => this.request(`/tenant/invites/${encodeURIComponent(inviteId)}`, { method: "DELETE" })
|
|
898
|
+
};
|
|
869
899
|
this.members = {
|
|
870
900
|
/**
|
|
871
901
|
* GET /tenant/members — members of the token's tenant.
|
|
872
902
|
*
|
|
873
|
-
*
|
|
874
|
-
*
|
|
875
|
-
*
|
|
876
|
-
*
|
|
877
|
-
* to `/auth/tenants/{id}/members`'s paginated, lightweight one.
|
|
903
|
+
* Same handler as `/env/tenants/{id}/members` (the tenant comes from the
|
|
904
|
+
* token instead of the path), so the same `Paged<T>` envelope of full
|
|
905
|
+
* user records, `take` defaulting to 50 and capped at 100 — as opposed
|
|
906
|
+
* to `/auth/tenants/{id}/members`'s lightweight member shape.
|
|
878
907
|
*/
|
|
879
908
|
list: (params = {}) => this.request("/tenant/members", { query: params }),
|
|
880
909
|
/** POST /tenant/members — add a member to the token's tenant. */
|
|
@@ -933,8 +962,12 @@ var TenantScopedClient = class {
|
|
|
933
962
|
async getAccessToken() {
|
|
934
963
|
return this.tokenProvider.getAccessToken();
|
|
935
964
|
}
|
|
936
|
-
async request(
|
|
937
|
-
return this.server._requestAsTenant(await this.getAccessToken(),
|
|
965
|
+
async request(path2, options = {}) {
|
|
966
|
+
return this.server._requestAsTenant(await this.getAccessToken(), path2, options);
|
|
967
|
+
}
|
|
968
|
+
/** POST /tenant/auth/validate — validates this tenant client's current bearer token. */
|
|
969
|
+
async validate(context) {
|
|
970
|
+
return this.server.tenantAuth.validate(await this.getAccessToken(), context);
|
|
938
971
|
}
|
|
939
972
|
};
|
|
940
973
|
function createClient(options = {}) {
|
|
@@ -942,342 +975,46 @@ function createClient(options = {}) {
|
|
|
942
975
|
}
|
|
943
976
|
|
|
944
977
|
// src/client/authyonServerClientBuilder.ts
|
|
978
|
+
var _options;
|
|
945
979
|
var AuthyonServerClientBuilder = class {
|
|
946
980
|
constructor() {
|
|
947
|
-
this
|
|
981
|
+
__privateAdd(this, _options, {});
|
|
948
982
|
}
|
|
949
983
|
withEnvironmentKey(envKey) {
|
|
950
|
-
this.
|
|
984
|
+
__privateGet(this, _options).envKey = envKey;
|
|
951
985
|
return this;
|
|
952
986
|
}
|
|
953
987
|
withEnvironmentCredentials(clientId, clientSecret) {
|
|
954
|
-
this.
|
|
955
|
-
this.
|
|
988
|
+
__privateGet(this, _options).clientId = clientId;
|
|
989
|
+
__privateGet(this, _options).clientSecret = clientSecret;
|
|
990
|
+
return this;
|
|
991
|
+
}
|
|
992
|
+
withClientIp(clientIp) {
|
|
993
|
+
__privateGet(this, _options).clientIp = clientIp;
|
|
956
994
|
return this;
|
|
957
995
|
}
|
|
958
996
|
withBaseUrl(baseUrl, allowInsecureHttp = false) {
|
|
959
|
-
this.
|
|
960
|
-
this.
|
|
997
|
+
__privateGet(this, _options).baseUrl = baseUrl;
|
|
998
|
+
__privateGet(this, _options).allowInsecureHttp = allowInsecureHttp;
|
|
961
999
|
return this;
|
|
962
1000
|
}
|
|
963
1001
|
withTimeout(timeoutMs) {
|
|
964
|
-
this.
|
|
1002
|
+
__privateGet(this, _options).timeoutMs = timeoutMs;
|
|
965
1003
|
return this;
|
|
966
1004
|
}
|
|
967
1005
|
withHttpAdapter(httpAdapter) {
|
|
968
|
-
this.
|
|
1006
|
+
__privateGet(this, _options).httpAdapter = httpAdapter;
|
|
969
1007
|
return this;
|
|
970
1008
|
}
|
|
971
1009
|
withHttpLogger(httpLogger) {
|
|
972
|
-
this.
|
|
1010
|
+
__privateGet(this, _options).httpLogger = httpLogger;
|
|
973
1011
|
return this;
|
|
974
1012
|
}
|
|
975
1013
|
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);
|
|
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";
|
|
1014
|
+
return new AuthyonServerClient({ ...__privateGet(this, _options) });
|
|
1169
1015
|
}
|
|
1170
1016
|
};
|
|
1171
|
-
|
|
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
|
-
}
|
|
1017
|
+
_options = new WeakMap();
|
|
1281
1018
|
export {
|
|
1282
1019
|
AuthorizationError,
|
|
1283
1020
|
AuthyonAbility,
|
|
@@ -1291,6 +1028,7 @@ export {
|
|
|
1291
1028
|
LoggingHttpAdapter,
|
|
1292
1029
|
TenantScopedClient,
|
|
1293
1030
|
TokenVerificationError,
|
|
1031
|
+
UserScopedClient,
|
|
1294
1032
|
authorizeRequest,
|
|
1295
1033
|
authorizeToken,
|
|
1296
1034
|
createAuthorizationErrorResponse,
|
|
@@ -1299,5 +1037,6 @@ export {
|
|
|
1299
1037
|
createClient,
|
|
1300
1038
|
createExpressAuthorizationMiddleware,
|
|
1301
1039
|
hasPermission,
|
|
1040
|
+
hasPermissionGroup,
|
|
1302
1041
|
normalizeClientIp
|
|
1303
1042
|
};
|