@authyon/server 0.2.0-beta.2 → 0.2.0-beta.21
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 +300 -2
- package/dist/authorization-D8tznR96.d.ts +129 -0
- package/dist/authorization-Soblu-A5.d.cts +129 -0
- package/dist/authyonServerClient-COrfN4o_.d.cts +1296 -0
- package/dist/authyonServerClient-COrfN4o_.d.ts +1296 -0
- package/dist/bff.cjs +1181 -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-SUL2HLRR.js +361 -0
- package/dist/index.cjs +708 -67
- package/dist/index.d.cts +47 -756
- package/dist/index.d.ts +47 -756
- package/dist/index.js +626 -784
- 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.cjs
CHANGED
|
@@ -3,6 +3,9 @@ var __defProp = Object.defineProperty;
|
|
|
3
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __typeError = (msg) => {
|
|
7
|
+
throw TypeError(msg);
|
|
8
|
+
};
|
|
6
9
|
var __export = (target, all) => {
|
|
7
10
|
for (var name in all)
|
|
8
11
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -16,6 +19,10 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
16
19
|
return to;
|
|
17
20
|
};
|
|
18
21
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
22
|
+
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
23
|
+
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
|
24
|
+
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
|
25
|
+
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
19
26
|
|
|
20
27
|
// src/index.ts
|
|
21
28
|
var index_exports = {};
|
|
@@ -30,8 +37,12 @@ __export(index_exports, {
|
|
|
30
37
|
FetchHttpAdapter: () => FetchHttpAdapter,
|
|
31
38
|
JwksTokenVerifier: () => JwksTokenVerifier,
|
|
32
39
|
LoggingHttpAdapter: () => LoggingHttpAdapter,
|
|
40
|
+
TENANT_FILTER_MAX_DEPTH: () => TENANT_FILTER_MAX_DEPTH,
|
|
41
|
+
TENANT_FILTER_MAX_VALUES: () => TENANT_FILTER_MAX_VALUES,
|
|
42
|
+
TENANT_FILTER_MAX_VALUE_LENGTH: () => TENANT_FILTER_MAX_VALUE_LENGTH,
|
|
33
43
|
TenantScopedClient: () => TenantScopedClient,
|
|
34
44
|
TokenVerificationError: () => TokenVerificationError,
|
|
45
|
+
UserScopedClient: () => UserScopedClient,
|
|
35
46
|
authorizeRequest: () => authorizeRequest,
|
|
36
47
|
authorizeToken: () => authorizeToken,
|
|
37
48
|
createAuthorizationErrorResponse: () => createAuthorizationErrorResponse,
|
|
@@ -40,39 +51,251 @@ __export(index_exports, {
|
|
|
40
51
|
createClient: () => createClient,
|
|
41
52
|
createExpressAuthorizationMiddleware: () => createExpressAuthorizationMiddleware,
|
|
42
53
|
hasPermission: () => hasPermission,
|
|
43
|
-
|
|
54
|
+
hasPermissionGroup: () => hasPermissionGroup,
|
|
55
|
+
normalizeClientIp: () => normalizeClientIp,
|
|
56
|
+
tenantFilter: () => tenantFilter
|
|
44
57
|
});
|
|
45
58
|
module.exports = __toCommonJS(index_exports);
|
|
46
59
|
|
|
60
|
+
// src/client/credentialManagement.ts
|
|
61
|
+
function segment(value) {
|
|
62
|
+
if (typeof value !== "string" || !value.trim() || value === "." || value === ".." || [...value].some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127)) {
|
|
63
|
+
throw new TypeError("Authyon: invalid route identifier.");
|
|
64
|
+
}
|
|
65
|
+
return encodeURIComponent(value);
|
|
66
|
+
}
|
|
67
|
+
function permissionsBody(input) {
|
|
68
|
+
if (input.permissions !== void 0 && input.scopes !== void 0) {
|
|
69
|
+
throw new TypeError("Authyon: supply permissions or scopes, not both.");
|
|
70
|
+
}
|
|
71
|
+
const values = input.permissions ?? input.scopes;
|
|
72
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
73
|
+
throw new TypeError("Authyon: explicit credential permissions are required.");
|
|
74
|
+
}
|
|
75
|
+
const permissions = values.map((value) => {
|
|
76
|
+
if (typeof value !== "string") throw new TypeError("Authyon: invalid permission.");
|
|
77
|
+
const normalized = value.trim().toLowerCase();
|
|
78
|
+
if (!/^(?:\*|[a-z][a-z0-9_-]*):(?:\*|[a-z][a-z0-9_-]*):(?:\*|[a-z][a-z0-9_-]*)$/u.test(normalized)) {
|
|
79
|
+
throw new TypeError("Authyon: permissions must have namespace:resource:action format.");
|
|
80
|
+
}
|
|
81
|
+
return normalized;
|
|
82
|
+
});
|
|
83
|
+
return { permissions: [...new Set(permissions)] };
|
|
84
|
+
}
|
|
85
|
+
function lifetimeBody(input) {
|
|
86
|
+
if (input.lifetimeDays === void 0 || input.lifetimeDays === null) return {};
|
|
87
|
+
if (!Number.isInteger(input.lifetimeDays) || input.lifetimeDays < 1 || input.lifetimeDays > 3650) {
|
|
88
|
+
throw new RangeError("Authyon: lifetimeDays must be between 1 and 3650.");
|
|
89
|
+
}
|
|
90
|
+
return { lifetimeDays: input.lifetimeDays };
|
|
91
|
+
}
|
|
92
|
+
function credentialQuery(options = {}) {
|
|
93
|
+
const { skip, take, search } = options;
|
|
94
|
+
if (skip !== void 0 && (!Number.isInteger(skip) || skip < 0 || skip > 2147483647))
|
|
95
|
+
throw new RangeError("Authyon: invalid skip.");
|
|
96
|
+
if (take !== void 0 && (!Number.isInteger(take) || take < 1 || take > 100))
|
|
97
|
+
throw new RangeError("Authyon: take must be between 1 and 100.");
|
|
98
|
+
if (search !== void 0 && (typeof search !== "string" || search.trim().length > 200))
|
|
99
|
+
throw new RangeError("Authyon: search must contain at most 200 characters.");
|
|
100
|
+
return { search: search?.trim() || void 0, skip, take };
|
|
101
|
+
}
|
|
102
|
+
function credentialSummary(value) {
|
|
103
|
+
return {
|
|
104
|
+
id: value.id,
|
|
105
|
+
clientId: value.clientId,
|
|
106
|
+
description: value.description,
|
|
107
|
+
createdAt: value.createdAt,
|
|
108
|
+
lastUsedAt: value.lastUsedAt,
|
|
109
|
+
revokedAt: value.revokedAt,
|
|
110
|
+
expiresAt: value.expiresAt ?? null,
|
|
111
|
+
isActive: value.isActive,
|
|
112
|
+
permissions: [...value.permissions]
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function credentialDetail(value) {
|
|
116
|
+
return {
|
|
117
|
+
...credentialSummary(value),
|
|
118
|
+
createdBy: value.createdBy == null ? null : {
|
|
119
|
+
id: value.createdBy.id,
|
|
120
|
+
type: value.createdBy.type,
|
|
121
|
+
displayName: value.createdBy.displayName
|
|
122
|
+
},
|
|
123
|
+
updatedAt: value.updatedAt,
|
|
124
|
+
secretRotatedAt: value.secretRotatedAt,
|
|
125
|
+
expiresAt: value.expiresAt ?? null,
|
|
126
|
+
ageSeconds: value.ageSeconds,
|
|
127
|
+
lifetimeSeconds: value.lifetimeSeconds,
|
|
128
|
+
accessTokenLifetimeSeconds: value.accessTokenLifetimeSeconds
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
async function credentialPage(request, path2, options = {}) {
|
|
132
|
+
const query = credentialQuery(options);
|
|
133
|
+
const page = await request(path2, { query });
|
|
134
|
+
if (!page || !Array.isArray(page.data))
|
|
135
|
+
throw new TypeError("Authyon: expected a paginated credential response.");
|
|
136
|
+
return {
|
|
137
|
+
data: page.data.map(credentialSummary),
|
|
138
|
+
page: page.page,
|
|
139
|
+
perPage: page.perPage,
|
|
140
|
+
total: page.total,
|
|
141
|
+
pages: page.pages,
|
|
142
|
+
hasNext: page.hasNext,
|
|
143
|
+
hasPrev: page.hasPrev
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
async function allCredentials(request, path2) {
|
|
147
|
+
const result = [];
|
|
148
|
+
const seen = /* @__PURE__ */ new Set();
|
|
149
|
+
let skip = 0;
|
|
150
|
+
for (let iteration = 0; iteration < 1e3; iteration++) {
|
|
151
|
+
const page = await request(
|
|
152
|
+
path2,
|
|
153
|
+
iteration === 0 ? {} : { query: { skip, take: 100 } }
|
|
154
|
+
);
|
|
155
|
+
if (Array.isArray(page)) {
|
|
156
|
+
if (iteration !== 0)
|
|
157
|
+
throw new Error("Authyon: credential pagination changed during listing.");
|
|
158
|
+
return page.map(credentialSummary);
|
|
159
|
+
}
|
|
160
|
+
if (!page || !Array.isArray(page.data))
|
|
161
|
+
throw new TypeError("Authyon: invalid credential page.");
|
|
162
|
+
for (const row of page.data) {
|
|
163
|
+
if (seen.has(row.id))
|
|
164
|
+
throw new Error("Authyon: credential pages overlapped; retry the listing.");
|
|
165
|
+
seen.add(row.id);
|
|
166
|
+
result.push(credentialSummary(row));
|
|
167
|
+
}
|
|
168
|
+
if (!page.hasNext) return result;
|
|
169
|
+
if (page.data.length === 0) throw new Error("Authyon: credential pagination made no progress.");
|
|
170
|
+
skip += page.data.length;
|
|
171
|
+
}
|
|
172
|
+
throw new Error("Authyon: use listPage for more than 1000 credential pages.");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ../../internal/core/security/approvals.ts
|
|
176
|
+
function path(id, suffix = "") {
|
|
177
|
+
if (typeof id !== "string" || !id.trim())
|
|
178
|
+
throw new TypeError("Authyon: an approval id is required.");
|
|
179
|
+
return `/auth/authorizations/${encodeURIComponent(id)}${suffix}`;
|
|
180
|
+
}
|
|
181
|
+
var NO_METHODS = {
|
|
182
|
+
password: false,
|
|
183
|
+
authenticator: false,
|
|
184
|
+
emailCode: false,
|
|
185
|
+
passkey: false,
|
|
186
|
+
passkeyCount: 0,
|
|
187
|
+
recoveryCodes: 0,
|
|
188
|
+
sso: [],
|
|
189
|
+
twoFactor: false
|
|
190
|
+
};
|
|
191
|
+
function subjectSecurity(request) {
|
|
192
|
+
return {
|
|
193
|
+
methods: async () => {
|
|
194
|
+
const me = await request("/auth/me");
|
|
195
|
+
return me.security ?? NO_METHODS;
|
|
196
|
+
},
|
|
197
|
+
approvals: {
|
|
198
|
+
get: (id) => request(path(id)),
|
|
199
|
+
webauthnOptions: (id) => request(path(id, "/webauthn/options"), { method: "POST" }),
|
|
200
|
+
confirm: (id, input) => request(path(id, "/confirm"), { method: "POST", body: input }),
|
|
201
|
+
reject: (id) => request(path(id, "/reject"), { method: "POST" })
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/client/scopedManagementClient.ts
|
|
207
|
+
function scopedRequest(request, source) {
|
|
208
|
+
return async (path2, options = {}) => {
|
|
209
|
+
const token = typeof source === "function" ? await source() : source;
|
|
210
|
+
if (typeof token !== "string" || !token.trim() || /\s/u.test(token))
|
|
211
|
+
throw new TypeError("Authyon: a valid access token is required.");
|
|
212
|
+
return request(path2, {
|
|
213
|
+
...options,
|
|
214
|
+
headers: { ...options.headers, Authorization: `Bearer ${token}` }
|
|
215
|
+
});
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
var _request;
|
|
219
|
+
var UserScopedClient = class {
|
|
220
|
+
/** @hidden */
|
|
221
|
+
constructor(request, token) {
|
|
222
|
+
__privateAdd(this, _request);
|
|
223
|
+
this.tenants = {
|
|
224
|
+
members: {
|
|
225
|
+
list: (tenantId, options = {}) => __privateGet(this, _request).call(this, `/auth/tenants/${segment(tenantId)}/members`, { query: options }),
|
|
226
|
+
/** Adds an existing environment user by email; does not send an invitation email. */
|
|
227
|
+
invite: (tenantId, input) => __privateGet(this, _request).call(this, `/auth/tenants/${segment(tenantId)}/members`, {
|
|
228
|
+
method: "POST",
|
|
229
|
+
body: { email: input.email, roles: input.roles }
|
|
230
|
+
}),
|
|
231
|
+
remove: (tenantId, userId) => __privateGet(this, _request).call(this, `/auth/tenants/${segment(tenantId)}/members/${segment(userId)}`, {
|
|
232
|
+
method: "DELETE"
|
|
233
|
+
})
|
|
234
|
+
},
|
|
235
|
+
/**
|
|
236
|
+
* Invites by e-mail or by link, acting as the user. Requires the
|
|
237
|
+
* `tenants:members:invite` permission on the tenant.
|
|
238
|
+
*/
|
|
239
|
+
invites: {
|
|
240
|
+
list: (tenantId) => __privateGet(this, _request).call(this, `/auth/tenants/${segment(tenantId)}/invites`),
|
|
241
|
+
/** Always returns `acceptUrl`; `sendEmail: false` skips the e-mail. */
|
|
242
|
+
create: (tenantId, input) => __privateGet(this, _request).call(this, `/auth/tenants/${segment(tenantId)}/invites`, {
|
|
243
|
+
method: "POST",
|
|
244
|
+
body: {
|
|
245
|
+
email: input.email,
|
|
246
|
+
roles: input.roles,
|
|
247
|
+
sendEmail: input.sendEmail ?? true,
|
|
248
|
+
redirectUri: input.redirectUri
|
|
249
|
+
}
|
|
250
|
+
}),
|
|
251
|
+
/** New link (the previous one stops working), mailed unless `sendEmail` is `false`. */
|
|
252
|
+
resend: (tenantId, inviteId, input = {}) => __privateGet(this, _request).call(this, `/auth/tenants/${segment(tenantId)}/invites/${segment(inviteId)}/resend`, {
|
|
253
|
+
method: "POST",
|
|
254
|
+
body: { sendEmail: input.sendEmail ?? true }
|
|
255
|
+
}),
|
|
256
|
+
revoke: (tenantId, inviteId) => __privateGet(this, _request).call(this, `/auth/tenants/${segment(tenantId)}/invites/${segment(inviteId)}`, {
|
|
257
|
+
method: "DELETE"
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
__privateSet(this, _request, scopedRequest(request, token));
|
|
262
|
+
this.security = subjectSecurity(__privateGet(this, _request));
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
_request = new WeakMap();
|
|
266
|
+
|
|
47
267
|
// ../../internal/core/config/defaults.ts
|
|
48
268
|
var DEFAULT_BASE_URL = "https://api.authyon.com";
|
|
49
269
|
var DEFAULT_EXPIRY_SKEW_MS = 3e4;
|
|
50
270
|
|
|
51
271
|
// ../../internal/core/auth/expiringTokenProvider.ts
|
|
272
|
+
var _token, _inFlight;
|
|
52
273
|
var ExpiringTokenProvider = class {
|
|
53
274
|
constructor(acquire, expirySkewMs = DEFAULT_EXPIRY_SKEW_MS) {
|
|
54
275
|
this.acquire = acquire;
|
|
55
276
|
this.expirySkewMs = expirySkewMs;
|
|
56
|
-
this
|
|
57
|
-
this
|
|
277
|
+
__privateAdd(this, _token, null);
|
|
278
|
+
__privateAdd(this, _inFlight, null);
|
|
58
279
|
}
|
|
59
280
|
async getAccessToken() {
|
|
60
|
-
if (this
|
|
61
|
-
return this.
|
|
281
|
+
if (__privateGet(this, _token) && Date.now() < __privateGet(this, _token).expiresAt - this.expirySkewMs) {
|
|
282
|
+
return __privateGet(this, _token).accessToken;
|
|
62
283
|
}
|
|
63
|
-
if (this
|
|
64
|
-
this
|
|
65
|
-
this
|
|
284
|
+
if (__privateGet(this, _inFlight)) return __privateGet(this, _inFlight);
|
|
285
|
+
__privateSet(this, _inFlight, this.acquire().then((token) => {
|
|
286
|
+
__privateSet(this, _token, token);
|
|
66
287
|
return token.accessToken;
|
|
67
288
|
}).finally(() => {
|
|
68
|
-
this
|
|
69
|
-
});
|
|
70
|
-
return this
|
|
289
|
+
__privateSet(this, _inFlight, null);
|
|
290
|
+
}));
|
|
291
|
+
return __privateGet(this, _inFlight);
|
|
71
292
|
}
|
|
72
293
|
clear() {
|
|
73
|
-
this
|
|
294
|
+
__privateSet(this, _token, null);
|
|
74
295
|
}
|
|
75
296
|
};
|
|
297
|
+
_token = new WeakMap();
|
|
298
|
+
_inFlight = new WeakMap();
|
|
76
299
|
|
|
77
300
|
// ../../internal/core/errors/authyonError.ts
|
|
78
301
|
var ErrorCodes = {
|
|
@@ -86,16 +309,45 @@ var ErrorCodes = {
|
|
|
86
309
|
EmailTaken: "user.email_taken",
|
|
87
310
|
PasswordWeak: "user.password_weak",
|
|
88
311
|
PasswordPwned: "user.password_pwned",
|
|
89
|
-
RateLimited: "rate_limited"
|
|
312
|
+
RateLimited: "rate_limited",
|
|
313
|
+
StepUpRequired: "step_up_required",
|
|
314
|
+
InvalidSecondFactorCode: "invalid_code",
|
|
315
|
+
VerificationAttemptsExhausted: "verification_attempts_exhausted",
|
|
316
|
+
MethodNotEnrolled: "method_not_enrolled",
|
|
317
|
+
InvalidApprovalState: "invalid_authorization_state",
|
|
318
|
+
PayloadMismatch: "payload_mismatch",
|
|
319
|
+
ApprovalNotConsumable: "authorization_not_consumable",
|
|
320
|
+
IdempotencyConflict: "idempotency_conflict",
|
|
321
|
+
UserDisabled: "user_disabled",
|
|
322
|
+
UserNotFound: "user_not_found",
|
|
323
|
+
TemporarilyUnavailable: "temporarily_unavailable",
|
|
324
|
+
InsufficientScope: "insufficient_scope",
|
|
325
|
+
CredentialThrottled: "credential_throttled",
|
|
326
|
+
ApprovalNotFound: "authorization_not_found",
|
|
327
|
+
SessionRevoked: "session_revoked",
|
|
328
|
+
TwoFactorTooManyFailures: "user.2fa.too_many_failures"
|
|
90
329
|
};
|
|
330
|
+
var STANDARD_ERROR_FIELDS = /* @__PURE__ */ new Set([
|
|
331
|
+
"type",
|
|
332
|
+
"status",
|
|
333
|
+
"title",
|
|
334
|
+
"detail",
|
|
335
|
+
"code",
|
|
336
|
+
"error",
|
|
337
|
+
"error_description"
|
|
338
|
+
]);
|
|
91
339
|
var AuthyonError = class extends Error {
|
|
92
340
|
constructor(status, body, options = {}) {
|
|
93
|
-
|
|
341
|
+
const detail = body.detail ?? body.error_description;
|
|
342
|
+
super(detail ?? body.title ?? `Authyon request failed with status ${status}`);
|
|
94
343
|
this.name = "AuthyonError";
|
|
95
344
|
this.status = status;
|
|
96
|
-
this.code = body.code ?? "unknown";
|
|
345
|
+
this.code = body.code ?? body.error ?? "unknown";
|
|
97
346
|
this.title = body.title ?? "Error";
|
|
98
|
-
this.detail =
|
|
347
|
+
this.detail = detail;
|
|
348
|
+
this.extensions = Object.fromEntries(
|
|
349
|
+
Object.entries(body).filter(([key]) => !STANDARD_ERROR_FIELDS.has(key))
|
|
350
|
+
);
|
|
99
351
|
this.requestId = options.requestId;
|
|
100
352
|
this.retryAfter = options.retryAfter;
|
|
101
353
|
this.cause = options.cause;
|
|
@@ -145,6 +397,7 @@ var AuthyonError = class extends Error {
|
|
|
145
397
|
};
|
|
146
398
|
function classifyError(status, code) {
|
|
147
399
|
if (code === ErrorCodes.NetworkError) return "network";
|
|
400
|
+
if (code === ErrorCodes.StepUpRequired) return "authentication";
|
|
148
401
|
if (code === ErrorCodes.Timeout || status === 408) return "timeout";
|
|
149
402
|
if (code === ErrorCodes.RateLimited || status === 429) return "rate_limit";
|
|
150
403
|
if (code === ErrorCodes.EmailTaken || status === 409) return "conflict";
|
|
@@ -183,14 +436,21 @@ function isRetryable(category) {
|
|
|
183
436
|
}
|
|
184
437
|
|
|
185
438
|
// ../../internal/core/http/query.ts
|
|
186
|
-
function appendQuery(
|
|
187
|
-
if (!params) return
|
|
439
|
+
function appendQuery(path2, params) {
|
|
440
|
+
if (!params) return path2;
|
|
188
441
|
const query = new URLSearchParams();
|
|
189
442
|
for (const [key, value2] of Object.entries(params)) {
|
|
190
|
-
if (value2
|
|
443
|
+
if (value2 === void 0) continue;
|
|
444
|
+
if (Array.isArray(value2)) {
|
|
445
|
+
for (const item of value2) {
|
|
446
|
+
if (item !== void 0) query.append(key, String(item));
|
|
447
|
+
}
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
query.set(key, String(value2));
|
|
191
451
|
}
|
|
192
452
|
const value = query.toString();
|
|
193
|
-
return value ? `${
|
|
453
|
+
return value ? `${path2}${path2.includes("?") ? "&" : "?"}${value}` : path2;
|
|
194
454
|
}
|
|
195
455
|
|
|
196
456
|
// ../../internal/core/http/httpAdapter.ts
|
|
@@ -203,7 +463,8 @@ var FetchHttpAdapter = class {
|
|
|
203
463
|
method: request.method,
|
|
204
464
|
headers: request.headers,
|
|
205
465
|
body: request.body,
|
|
206
|
-
signal: request.signal
|
|
466
|
+
signal: request.signal,
|
|
467
|
+
redirect: "error"
|
|
207
468
|
});
|
|
208
469
|
}
|
|
209
470
|
};
|
|
@@ -255,6 +516,9 @@ function defaultHttpLogger(event) {
|
|
|
255
516
|
}
|
|
256
517
|
function sanitizeUrl(value) {
|
|
257
518
|
const url = new URL(value);
|
|
519
|
+
url.username = "";
|
|
520
|
+
url.password = "";
|
|
521
|
+
url.hash = "";
|
|
258
522
|
const queryKeys = /* @__PURE__ */ new Set();
|
|
259
523
|
url.searchParams.forEach((_value, key) => queryKeys.add(key));
|
|
260
524
|
url.search = queryKeys.size > 0 ? [...queryKeys].map((key) => `${encodeURIComponent(key)}=REDACTED`).join("&") : "";
|
|
@@ -284,12 +548,12 @@ function createSharedTransport(options) {
|
|
|
284
548
|
const httpAdapter = options.httpLogger ? new LoggingHttpAdapter(baseAdapter, options.httpLogger) : baseAdapter;
|
|
285
549
|
return {
|
|
286
550
|
baseUrl,
|
|
287
|
-
async request(
|
|
551
|
+
async request(path2, init) {
|
|
288
552
|
const controller = new AbortController();
|
|
289
553
|
const timeout = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : void 0;
|
|
290
554
|
try {
|
|
291
555
|
return await httpAdapter.request({
|
|
292
|
-
url: `${baseUrl}${
|
|
556
|
+
url: `${baseUrl}${path2}`,
|
|
293
557
|
method: init.method ?? "GET",
|
|
294
558
|
headers: normalizeHeaders(init.headers),
|
|
295
559
|
body: init.body,
|
|
@@ -347,11 +611,11 @@ var JsonHttpClient = class {
|
|
|
347
611
|
constructor(transport) {
|
|
348
612
|
this.transport = transport;
|
|
349
613
|
}
|
|
350
|
-
async send(
|
|
614
|
+
async send(path2, options = {}) {
|
|
351
615
|
const headers = { ...options.headers };
|
|
352
616
|
if (options.body !== void 0) headers["Content-Type"] = "application/json";
|
|
353
617
|
try {
|
|
354
|
-
return await this.transport.request(appendQuery(
|
|
618
|
+
return await this.transport.request(appendQuery(path2, options.query), {
|
|
355
619
|
method: options.method ?? "GET",
|
|
356
620
|
headers,
|
|
357
621
|
body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
|
|
@@ -365,7 +629,9 @@ var JsonHttpClient = class {
|
|
|
365
629
|
if (!response.ok) {
|
|
366
630
|
let body = {};
|
|
367
631
|
try {
|
|
368
|
-
|
|
632
|
+
const parsed = await response.json();
|
|
633
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
|
|
634
|
+
body = parsed;
|
|
369
635
|
} catch {
|
|
370
636
|
}
|
|
371
637
|
throw new AuthyonError(response.status, body, responseMetadata(response));
|
|
@@ -373,8 +639,8 @@ var JsonHttpClient = class {
|
|
|
373
639
|
if (response.status === 204) return void 0;
|
|
374
640
|
return await response.json();
|
|
375
641
|
}
|
|
376
|
-
async request(
|
|
377
|
-
return this.parse(await this.send(
|
|
642
|
+
async request(path2, options = {}) {
|
|
643
|
+
return this.parse(await this.send(path2, options));
|
|
378
644
|
}
|
|
379
645
|
};
|
|
380
646
|
|
|
@@ -536,8 +802,10 @@ function isRecord(value) {
|
|
|
536
802
|
}
|
|
537
803
|
|
|
538
804
|
// src/client/authyonServerClient.ts
|
|
805
|
+
var _clientSecret;
|
|
539
806
|
var AuthyonServerClient = class {
|
|
540
807
|
constructor(options = {}) {
|
|
808
|
+
__privateAdd(this, _clientSecret);
|
|
541
809
|
// ── Discovery ──────────────────────────────────────────────────────────────
|
|
542
810
|
this.discovery = {
|
|
543
811
|
/** GET /.well-known/jwks.json — the environment's public signing keys. */
|
|
@@ -571,6 +839,14 @@ var AuthyonServerClient = class {
|
|
|
571
839
|
client_id: credentials.clientId,
|
|
572
840
|
client_secret: credentials.clientSecret
|
|
573
841
|
}
|
|
842
|
+
}),
|
|
843
|
+
/** POST /tenant/auth/validate — validates an already-minted tenant-client bearer token. */
|
|
844
|
+
validate: (accessToken, context) => this.request("/tenant/auth/validate", {
|
|
845
|
+
method: "POST",
|
|
846
|
+
headers: {
|
|
847
|
+
...clientIpHeaders(context),
|
|
848
|
+
Authorization: `Bearer ${accessToken}`
|
|
849
|
+
}
|
|
574
850
|
})
|
|
575
851
|
};
|
|
576
852
|
// ── Environment management (users, tenants, roles, permissions, audit) ───
|
|
@@ -616,6 +892,39 @@ var AuthyonServerClient = class {
|
|
|
616
892
|
envBearer: true,
|
|
617
893
|
body: { newPassword, reason }
|
|
618
894
|
}),
|
|
895
|
+
/**
|
|
896
|
+
* POST /env/password-resets — issues a password-reset token for the
|
|
897
|
+
* account with this address and returns it, so your application can
|
|
898
|
+
* deliver it in its own e-mail instead of Authyon's. Needs the
|
|
899
|
+
* `authyon:users:password` scope. Unlike the public forgot-password
|
|
900
|
+
* request, an unknown address rejects with `user.not_found` (404).
|
|
901
|
+
* A previous live token for the same account stops working.
|
|
902
|
+
*/
|
|
903
|
+
issuePasswordReset: (input) => this.request("/env/password-resets", {
|
|
904
|
+
method: "POST",
|
|
905
|
+
envBearer: true,
|
|
906
|
+
body: { ...input, sendEmail: input.sendEmail ?? false }
|
|
907
|
+
}),
|
|
908
|
+
/**
|
|
909
|
+
* POST /auth/password-reset/validate — for a backend hosting the reset
|
|
910
|
+
* page: checks a token without spending it. Public, needs only the
|
|
911
|
+
* `envKey`. Rejects with `user.password_reset.invalid` (401) when the
|
|
912
|
+
* token is unknown, used or expired.
|
|
913
|
+
*/
|
|
914
|
+
validatePasswordReset: (token) => this.request("/auth/password-reset/validate", { method: "POST", body: { token } }),
|
|
915
|
+
/**
|
|
916
|
+
* POST /auth/password-reset/confirm — redeems the token and sets the new
|
|
917
|
+
* password (every session of the account is signed out). Public, needs
|
|
918
|
+
* only the `envKey`. Resolves with the `redirectUri` the reset was issued
|
|
919
|
+
* with, or `null`.
|
|
920
|
+
*/
|
|
921
|
+
confirmPasswordReset: async (token, newPassword) => {
|
|
922
|
+
const done = await this.request(
|
|
923
|
+
"/auth/password-reset/confirm",
|
|
924
|
+
{ method: "POST", body: { token, newPassword } }
|
|
925
|
+
);
|
|
926
|
+
return { redirectUri: done?.redirectUri ?? null };
|
|
927
|
+
},
|
|
619
928
|
/** POST /env/users/{userId}/sessions/revoke-all — revokes every session the user has. */
|
|
620
929
|
revokeSessions: (userId, reason) => this.request(`/env/users/${encodeURIComponent(userId)}/sessions/revoke-all`, {
|
|
621
930
|
method: "POST",
|
|
@@ -664,7 +973,82 @@ var AuthyonServerClient = class {
|
|
|
664
973
|
)
|
|
665
974
|
},
|
|
666
975
|
tenants: {
|
|
667
|
-
|
|
976
|
+
credentials: {
|
|
977
|
+
/** GET /env/tenants/{tenantId}/credentials. Returns metadata only, including revoked credentials. */
|
|
978
|
+
list: (tenantId) => allCredentials(
|
|
979
|
+
(path2, options) => this.request(path2, { ...options, envBearer: true }),
|
|
980
|
+
`/env/tenants/${segment(tenantId)}/credentials`
|
|
981
|
+
),
|
|
982
|
+
/** Paginated searchable listing. Prefer this over loading every credential. */
|
|
983
|
+
listPage: (tenantId, options = {}) => credentialPage(
|
|
984
|
+
(path2, requestOptions) => this.request(path2, { ...requestOptions, envBearer: true }),
|
|
985
|
+
`/env/tenants/${segment(tenantId)}/credentials`,
|
|
986
|
+
options
|
|
987
|
+
),
|
|
988
|
+
get: async (tenantId, credentialId) => credentialDetail(
|
|
989
|
+
await this.request(
|
|
990
|
+
`/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}`,
|
|
991
|
+
{ envBearer: true }
|
|
992
|
+
)
|
|
993
|
+
),
|
|
994
|
+
/** Explicit scopes/permissions; only issuance returns the client secret. */
|
|
995
|
+
create: (tenantId, input) => this.request(`/env/tenants/${segment(tenantId)}/credentials`, {
|
|
996
|
+
method: "POST",
|
|
997
|
+
envBearer: true,
|
|
998
|
+
body: {
|
|
999
|
+
description: input.description,
|
|
1000
|
+
...lifetimeBody(input),
|
|
1001
|
+
...permissionsBody(input)
|
|
1002
|
+
}
|
|
1003
|
+
}),
|
|
1004
|
+
/** Returns a new secret once while keeping the same client id. */
|
|
1005
|
+
rotate: (tenantId, credentialId) => this.request(
|
|
1006
|
+
`/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}/rotate`,
|
|
1007
|
+
{ method: "POST", envBearer: true }
|
|
1008
|
+
),
|
|
1009
|
+
updatePermissions: (tenantId, credentialId, input) => this.request(
|
|
1010
|
+
`/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}/permissions`,
|
|
1011
|
+
{ method: "PUT", envBearer: true, body: permissionsBody(input) }
|
|
1012
|
+
),
|
|
1013
|
+
updateScopes: (tenantId, credentialId, scopes) => this.environment.tenants.credentials.updatePermissions(tenantId, credentialId, {
|
|
1014
|
+
scopes
|
|
1015
|
+
}),
|
|
1016
|
+
revoke: (tenantId, credentialId) => this.request(`/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}`, {
|
|
1017
|
+
method: "DELETE",
|
|
1018
|
+
envBearer: true
|
|
1019
|
+
})
|
|
1020
|
+
},
|
|
1021
|
+
/**
|
|
1022
|
+
* GET /env/tenants — one page of the environment's tenants.
|
|
1023
|
+
*
|
|
1024
|
+
* `search` matches, case-insensitively and anywhere in the value, the
|
|
1025
|
+
* name, the slug, the description and the raw text of both metadata
|
|
1026
|
+
* documents — so a tenant filed with `{"cnpj":"00.000.000/0001-00"}`
|
|
1027
|
+
* is found by the number, and `cnpj` alone finds every tenant carrying
|
|
1028
|
+
* that key. `take` defaults to 25 and is capped at 100.
|
|
1029
|
+
*
|
|
1030
|
+
* `filter` is the structured half, and the one to reach for when more
|
|
1031
|
+
* than one condition has to hold: each entry is a condition on a JSON
|
|
1032
|
+
* document, they are ANDed with each other and with `search`, and they
|
|
1033
|
+
* run inside the statement — so the envelope keeps describing the
|
|
1034
|
+
* filtered set and you can page through it. Build them with
|
|
1035
|
+
* `tenantFilter`:
|
|
1036
|
+
*
|
|
1037
|
+
* ```ts
|
|
1038
|
+
* const page = await authyon.environment.tenants.list({
|
|
1039
|
+
* filter: [
|
|
1040
|
+
* tenantFilter.in("public.kind", ["reseller", "partner"]),
|
|
1041
|
+
* tenantFilter.in("custom.cnpj", [a, b]),
|
|
1042
|
+
* ],
|
|
1043
|
+
* take: 100,
|
|
1044
|
+
* });
|
|
1045
|
+
* ```
|
|
1046
|
+
*
|
|
1047
|
+
* Against an Authyon API older than 2026-10-21 this route ignored
|
|
1048
|
+
* skip/take/search and answered with a bare array; one older than
|
|
1049
|
+
* 2026-10-22 ignores `filter`, which reads as a page that is too wide
|
|
1050
|
+
* rather than an error.
|
|
1051
|
+
*/
|
|
668
1052
|
list: (params = {}) => this.request("/env/tenants", { envBearer: true, query: params }),
|
|
669
1053
|
/** POST /env/tenants — create a tenant in the environment. */
|
|
670
1054
|
create: (input) => this.request("/env/tenants", { method: "POST", envBearer: true, body: input }),
|
|
@@ -676,7 +1060,18 @@ var AuthyonServerClient = class {
|
|
|
676
1060
|
envBearer: true,
|
|
677
1061
|
body: input
|
|
678
1062
|
}),
|
|
679
|
-
/**
|
|
1063
|
+
/** PUT /env/tenants/{tenantId}/metadata - replace public and private metadata. */
|
|
1064
|
+
updateMetadata: (tenantId, input) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/metadata`, {
|
|
1065
|
+
method: "PUT",
|
|
1066
|
+
envBearer: true,
|
|
1067
|
+
body: input
|
|
1068
|
+
}),
|
|
1069
|
+
/** POST /env/tenants/{tenantId}/disable — disable without removing tenant data. */
|
|
1070
|
+
disable: (tenantId) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/disable`, {
|
|
1071
|
+
method: "POST",
|
|
1072
|
+
envBearer: true
|
|
1073
|
+
}),
|
|
1074
|
+
/** DELETE /env/tenants/{tenantId} — logically delete (disable) a tenant. */
|
|
680
1075
|
delete: (tenantId) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}`, {
|
|
681
1076
|
method: "DELETE",
|
|
682
1077
|
envBearer: true
|
|
@@ -684,10 +1079,16 @@ var AuthyonServerClient = class {
|
|
|
684
1079
|
members: {
|
|
685
1080
|
/**
|
|
686
1081
|
* GET /env/tenants/{tenantId}/members — a tenant's members as full
|
|
687
|
-
* user records
|
|
688
|
-
* `
|
|
689
|
-
*
|
|
690
|
-
*
|
|
1082
|
+
* user records, with the same shape `environment.users.list()` items
|
|
1083
|
+
* have, plus `tenantRoles` and `tenantPermissions` scoped to this
|
|
1084
|
+
* tenant.
|
|
1085
|
+
*
|
|
1086
|
+
* Paged like every other list: the `Paged<T>` envelope carries
|
|
1087
|
+
* `total` and `hasNext`, `take` defaults to 50 and is capped at 100.
|
|
1088
|
+
* Up to the Authyon API of 2026-10-21 this one route answered with a
|
|
1089
|
+
* bare array, so a caller had to page until a short page came back;
|
|
1090
|
+
* against such an API `data` is undefined and the array is the body
|
|
1091
|
+
* itself.
|
|
691
1092
|
*/
|
|
692
1093
|
list: (tenantId, params = {}) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/members`, {
|
|
693
1094
|
envBearer: true,
|
|
@@ -725,6 +1126,61 @@ var AuthyonServerClient = class {
|
|
|
725
1126
|
{ method: "DELETE", envBearer: true }
|
|
726
1127
|
)
|
|
727
1128
|
},
|
|
1129
|
+
/**
|
|
1130
|
+
* Invites by e-mail or by link — for addresses that may not have an
|
|
1131
|
+
* account yet; accepting creates it. Managing them needs the
|
|
1132
|
+
* credential's `authyon:users:write` scope (listing, `authyon:tenants:read`).
|
|
1133
|
+
*/
|
|
1134
|
+
invites: {
|
|
1135
|
+
/** GET /env/tenants/{tenantId}/invites — every invite with its status, newest first. */
|
|
1136
|
+
list: (tenantId) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/invites`, { envBearer: true }),
|
|
1137
|
+
/**
|
|
1138
|
+
* POST /env/tenants/{tenantId}/invites — invite an address. Always
|
|
1139
|
+
* returns `acceptUrl`; with `sendEmail: false` Authyon does not mail
|
|
1140
|
+
* it and you deliver the link yourself. Valid 7 days, single use, and
|
|
1141
|
+
* not readable again later.
|
|
1142
|
+
*/
|
|
1143
|
+
create: (tenantId, input) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/invites`, {
|
|
1144
|
+
method: "POST",
|
|
1145
|
+
envBearer: true,
|
|
1146
|
+
body: {
|
|
1147
|
+
email: input.email,
|
|
1148
|
+
roles: input.roles,
|
|
1149
|
+
sendEmail: input.sendEmail ?? true,
|
|
1150
|
+
redirectUri: input.redirectUri
|
|
1151
|
+
}
|
|
1152
|
+
}),
|
|
1153
|
+
/** POST .../invites/{inviteId}/resend — new link (the previous one stops working), mailed unless `sendEmail` is `false`. */
|
|
1154
|
+
resend: (tenantId, inviteId, input = {}) => this.request(
|
|
1155
|
+
`/env/tenants/${encodeURIComponent(tenantId)}/invites/${encodeURIComponent(inviteId)}/resend`,
|
|
1156
|
+
{ method: "POST", envBearer: true, body: { sendEmail: input.sendEmail ?? true } }
|
|
1157
|
+
),
|
|
1158
|
+
/** DELETE /env/tenants/{tenantId}/invites/{inviteId} — revoke a pending invite. */
|
|
1159
|
+
revoke: (tenantId, inviteId) => this.request(
|
|
1160
|
+
`/env/tenants/${encodeURIComponent(tenantId)}/invites/${encodeURIComponent(inviteId)}`,
|
|
1161
|
+
{ method: "DELETE", envBearer: true }
|
|
1162
|
+
),
|
|
1163
|
+
/**
|
|
1164
|
+
* POST /auth/tenant-invites/preview — for a backend hosting the accept
|
|
1165
|
+
* page. Public: needs only `envKey`, the token is the credential.
|
|
1166
|
+
*/
|
|
1167
|
+
preview: (token) => this.request("/auth/tenant-invites/preview", { method: "POST", body: { token } }),
|
|
1168
|
+
/**
|
|
1169
|
+
* Joins the tenant; for an address with no account, also creates it
|
|
1170
|
+
* (password required) with the e-mail already confirmed. Does not
|
|
1171
|
+
* sign the user in.
|
|
1172
|
+
*
|
|
1173
|
+
* Without metadata: POST /auth/tenant-invites/accept — public, needs
|
|
1174
|
+
* only `envKey`. With `publicMetadata`/`privateMetadata`: POST
|
|
1175
|
+
* /env/tenant-invites/accept with the environment credential
|
|
1176
|
+
* (`authyon:users:write`), since only your backend may set them.
|
|
1177
|
+
*/
|
|
1178
|
+
accept: (input) => input.publicMetadata !== void 0 || input.privateMetadata !== void 0 ? this.request("/env/tenant-invites/accept", {
|
|
1179
|
+
method: "POST",
|
|
1180
|
+
envBearer: true,
|
|
1181
|
+
body: input
|
|
1182
|
+
}) : this.request("/auth/tenant-invites/accept", { method: "POST", body: input })
|
|
1183
|
+
},
|
|
728
1184
|
roles: {
|
|
729
1185
|
/** GET /env/tenants/{tenantId}/roles — list a tenant's roles. */
|
|
730
1186
|
list: (tenantId) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/roles`, { envBearer: true }),
|
|
@@ -794,6 +1250,60 @@ var AuthyonServerClient = class {
|
|
|
794
1250
|
loginActivity: (params = {}) => this.request("/env/audit/login-activity", { envBearer: true, query: params })
|
|
795
1251
|
}
|
|
796
1252
|
};
|
|
1253
|
+
// ── Security: one-time codes and approvals ───────────────────────────────
|
|
1254
|
+
//
|
|
1255
|
+
// Everything here runs with the environment credential: `otp` needs the
|
|
1256
|
+
// `authyon:otp:verify` scope, `approvals` needs `authyon:financial:authorize`.
|
|
1257
|
+
// Keep those credentials on the server.
|
|
1258
|
+
this.security = {
|
|
1259
|
+
otp: {
|
|
1260
|
+
/**
|
|
1261
|
+
* POST /env/users/{userId}/otp/verify — checks the user's authenticator
|
|
1262
|
+
* app (TOTP) code. A wrong code returns `{ valid: false }` instead of
|
|
1263
|
+
* throwing. Each code is accepted once; 3 wrong codes in a row sign the
|
|
1264
|
+
* user out everywhere (`sessionsRevoked: true`); 5 lock the check for 15
|
|
1265
|
+
* minutes (`rate_limited`). More than 50 wrong codes in 10 minutes from the
|
|
1266
|
+
* same credential pause it (`credential_throttled`). Needs `authyon:otp:verify`.
|
|
1267
|
+
*/
|
|
1268
|
+
check: (userId, code) => this.request(`/env/users/${segment(userId)}/otp/verify`, {
|
|
1269
|
+
method: "POST",
|
|
1270
|
+
envBearer: true,
|
|
1271
|
+
body: { code }
|
|
1272
|
+
})
|
|
1273
|
+
},
|
|
1274
|
+
/**
|
|
1275
|
+
* The customer approves a payload with their authenticator code or
|
|
1276
|
+
* passkey, once.
|
|
1277
|
+
*
|
|
1278
|
+
* 1. `create()` the approval with the payload and the customer.
|
|
1279
|
+
* 2. The customer confirms it (`@authyon/auth` → `security.approvals.confirm`).
|
|
1280
|
+
* 3. `consume()` it right before executing, and execute the returned payload.
|
|
1281
|
+
*/
|
|
1282
|
+
approvals: {
|
|
1283
|
+
/** POST /env/authorizations — registers a payload awaiting the customer's approval. */
|
|
1284
|
+
create: (input) => {
|
|
1285
|
+
const { idempotencyKey, ...body } = input;
|
|
1286
|
+
return this.request("/env/authorizations", {
|
|
1287
|
+
method: "POST",
|
|
1288
|
+
envBearer: true,
|
|
1289
|
+
headers: idempotencyKey ? { "Idempotency-Key": idempotencyKey } : void 0,
|
|
1290
|
+
body
|
|
1291
|
+
});
|
|
1292
|
+
},
|
|
1293
|
+
/** GET /env/authorizations/{id} — current status and, once approved, the assurance evidence. */
|
|
1294
|
+
get: (id) => this.request(`/env/authorizations/${segment(id)}`, { envBearer: true }),
|
|
1295
|
+
/**
|
|
1296
|
+
* POST /env/authorizations/{id}/consume — redeems an approved approval
|
|
1297
|
+
* exactly once and returns the approved payload. Pass `payload` to have
|
|
1298
|
+
* the API check it matches (`payload_mismatch` otherwise).
|
|
1299
|
+
*/
|
|
1300
|
+
consume: (id, payload) => this.request(`/env/authorizations/${segment(id)}/consume`, {
|
|
1301
|
+
method: "POST",
|
|
1302
|
+
envBearer: true,
|
|
1303
|
+
body: payload === void 0 ? void 0 : { payload }
|
|
1304
|
+
})
|
|
1305
|
+
}
|
|
1306
|
+
};
|
|
797
1307
|
// ── Generic permission grants ─────────────────────────────────────────────
|
|
798
1308
|
this.permissions = {
|
|
799
1309
|
/** POST /users/{userId}/permissions — grant a permission directly to a user. */
|
|
@@ -810,7 +1320,8 @@ var AuthyonServerClient = class {
|
|
|
810
1320
|
};
|
|
811
1321
|
this.envKey = options.envKey;
|
|
812
1322
|
this.clientId = options.clientId;
|
|
813
|
-
this
|
|
1323
|
+
__privateSet(this, _clientSecret, options.clientSecret);
|
|
1324
|
+
this.clientIp = options.clientIp;
|
|
814
1325
|
this.transport = createSharedTransport({
|
|
815
1326
|
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
|
|
816
1327
|
allowInsecureHttp: options.allowInsecureHttp,
|
|
@@ -821,14 +1332,14 @@ var AuthyonServerClient = class {
|
|
|
821
1332
|
});
|
|
822
1333
|
this.http = new JsonHttpClient(this.transport);
|
|
823
1334
|
this.environmentTokenProvider = new ExpiringTokenProvider(async () => {
|
|
824
|
-
if (!this.clientId || !this
|
|
1335
|
+
if (!this.clientId || !__privateGet(this, _clientSecret)) {
|
|
825
1336
|
throw new Error(
|
|
826
1337
|
"Authyon: `clientId`/`clientSecret` are required for environment management calls (mint a pair in the console, under the environment's OAuth clients)."
|
|
827
1338
|
);
|
|
828
1339
|
}
|
|
829
1340
|
const token = await this.environmentAuth.token({
|
|
830
1341
|
clientId: this.clientId,
|
|
831
|
-
clientSecret: this
|
|
1342
|
+
clientSecret: __privateGet(this, _clientSecret)
|
|
832
1343
|
});
|
|
833
1344
|
return {
|
|
834
1345
|
accessToken: token.access_token,
|
|
@@ -836,13 +1347,16 @@ var AuthyonServerClient = class {
|
|
|
836
1347
|
};
|
|
837
1348
|
});
|
|
838
1349
|
}
|
|
839
|
-
async request(
|
|
840
|
-
const headers = {
|
|
1350
|
+
async request(path2, options = {}) {
|
|
1351
|
+
const headers = {
|
|
1352
|
+
...clientIpHeaders({ clientIp: this.clientIp }),
|
|
1353
|
+
...options.headers
|
|
1354
|
+
};
|
|
841
1355
|
if (options.envBearer) {
|
|
842
1356
|
headers.Authorization = `Bearer ${await this.getEnvironmentAccessToken()}`;
|
|
843
1357
|
}
|
|
844
1358
|
if (this.envKey) headers["X-Authyon-Environment"] = this.envKey;
|
|
845
|
-
return this.http.request(
|
|
1359
|
+
return this.http.request(path2, { ...options, headers });
|
|
846
1360
|
}
|
|
847
1361
|
/** Mints (and caches) an environment access token from `clientId`/`clientSecret`. */
|
|
848
1362
|
async getEnvironmentAccessToken() {
|
|
@@ -897,26 +1411,52 @@ var AuthyonServerClient = class {
|
|
|
897
1411
|
tenant(credentials) {
|
|
898
1412
|
return new TenantScopedClient(this, credentials);
|
|
899
1413
|
}
|
|
1414
|
+
/** Uses an end-user token; the configured environment key selects the environment. */
|
|
1415
|
+
user(token) {
|
|
1416
|
+
return new UserScopedClient((path2, options) => this.request(path2, options), token);
|
|
1417
|
+
}
|
|
900
1418
|
/** @internal used by {@link TenantScopedClient} to share the base URL/fetch/error handling. */
|
|
901
|
-
_requestAsTenant(accessToken,
|
|
902
|
-
return this.request(
|
|
1419
|
+
_requestAsTenant(accessToken, path2, options = {}) {
|
|
1420
|
+
return this.request(path2, {
|
|
903
1421
|
...options,
|
|
904
1422
|
headers: { ...options.headers, Authorization: `Bearer ${accessToken}` },
|
|
905
1423
|
envBearer: false
|
|
906
1424
|
});
|
|
907
1425
|
}
|
|
908
1426
|
};
|
|
1427
|
+
_clientSecret = new WeakMap();
|
|
909
1428
|
var TenantScopedClient = class {
|
|
910
1429
|
constructor(server, credentials) {
|
|
1430
|
+
/** Invites to the token's tenant, by e-mail or by link. */
|
|
1431
|
+
this.invites = {
|
|
1432
|
+
/** GET /tenant/invites — every invite with its status, newest first. */
|
|
1433
|
+
list: () => this.request("/tenant/invites"),
|
|
1434
|
+
/** POST /tenant/invites — always returns `acceptUrl`; `sendEmail: false` skips the e-mail. */
|
|
1435
|
+
create: (input) => this.request("/tenant/invites", {
|
|
1436
|
+
method: "POST",
|
|
1437
|
+
body: {
|
|
1438
|
+
email: input.email,
|
|
1439
|
+
roles: input.roles,
|
|
1440
|
+
sendEmail: input.sendEmail ?? true,
|
|
1441
|
+
redirectUri: input.redirectUri
|
|
1442
|
+
}
|
|
1443
|
+
}),
|
|
1444
|
+
/** POST /tenant/invites/{inviteId}/resend — new link (the previous one stops working), mailed unless `sendEmail` is `false`. */
|
|
1445
|
+
resend: (inviteId, input = {}) => this.request(`/tenant/invites/${encodeURIComponent(inviteId)}/resend`, {
|
|
1446
|
+
method: "POST",
|
|
1447
|
+
body: { sendEmail: input.sendEmail ?? true }
|
|
1448
|
+
}),
|
|
1449
|
+
/** DELETE /tenant/invites/{inviteId} — revoke a pending invite. */
|
|
1450
|
+
revoke: (inviteId) => this.request(`/tenant/invites/${encodeURIComponent(inviteId)}`, { method: "DELETE" })
|
|
1451
|
+
};
|
|
911
1452
|
this.members = {
|
|
912
1453
|
/**
|
|
913
1454
|
* GET /tenant/members — members of the token's tenant.
|
|
914
1455
|
*
|
|
915
|
-
*
|
|
916
|
-
*
|
|
917
|
-
*
|
|
918
|
-
*
|
|
919
|
-
* to `/auth/tenants/{id}/members`'s paginated, lightweight one.
|
|
1456
|
+
* Same handler as `/env/tenants/{id}/members` (the tenant comes from the
|
|
1457
|
+
* token instead of the path), so the same `Paged<T>` envelope of full
|
|
1458
|
+
* user records, `take` defaulting to 50 and capped at 100 — as opposed
|
|
1459
|
+
* to `/auth/tenants/{id}/members`'s lightweight member shape.
|
|
920
1460
|
*/
|
|
921
1461
|
list: (params = {}) => this.request("/tenant/members", { query: params }),
|
|
922
1462
|
/** POST /tenant/members — add a member to the token's tenant. */
|
|
@@ -975,8 +1515,12 @@ var TenantScopedClient = class {
|
|
|
975
1515
|
async getAccessToken() {
|
|
976
1516
|
return this.tokenProvider.getAccessToken();
|
|
977
1517
|
}
|
|
978
|
-
async request(
|
|
979
|
-
return this.server._requestAsTenant(await this.getAccessToken(),
|
|
1518
|
+
async request(path2, options = {}) {
|
|
1519
|
+
return this.server._requestAsTenant(await this.getAccessToken(), path2, options);
|
|
1520
|
+
}
|
|
1521
|
+
/** POST /tenant/auth/validate — validates this tenant client's current bearer token. */
|
|
1522
|
+
async validate(context) {
|
|
1523
|
+
return this.server.tenantAuth.validate(await this.getAccessToken(), context);
|
|
980
1524
|
}
|
|
981
1525
|
};
|
|
982
1526
|
function createClient(options = {}) {
|
|
@@ -984,38 +1528,124 @@ function createClient(options = {}) {
|
|
|
984
1528
|
}
|
|
985
1529
|
|
|
986
1530
|
// src/client/authyonServerClientBuilder.ts
|
|
1531
|
+
var _options;
|
|
987
1532
|
var AuthyonServerClientBuilder = class {
|
|
988
1533
|
constructor() {
|
|
989
|
-
this
|
|
1534
|
+
__privateAdd(this, _options, {});
|
|
990
1535
|
}
|
|
991
1536
|
withEnvironmentKey(envKey) {
|
|
992
|
-
this.
|
|
1537
|
+
__privateGet(this, _options).envKey = envKey;
|
|
993
1538
|
return this;
|
|
994
1539
|
}
|
|
995
1540
|
withEnvironmentCredentials(clientId, clientSecret) {
|
|
996
|
-
this.
|
|
997
|
-
this.
|
|
1541
|
+
__privateGet(this, _options).clientId = clientId;
|
|
1542
|
+
__privateGet(this, _options).clientSecret = clientSecret;
|
|
1543
|
+
return this;
|
|
1544
|
+
}
|
|
1545
|
+
withClientIp(clientIp) {
|
|
1546
|
+
__privateGet(this, _options).clientIp = clientIp;
|
|
998
1547
|
return this;
|
|
999
1548
|
}
|
|
1000
1549
|
withBaseUrl(baseUrl, allowInsecureHttp = false) {
|
|
1001
|
-
this.
|
|
1002
|
-
this.
|
|
1550
|
+
__privateGet(this, _options).baseUrl = baseUrl;
|
|
1551
|
+
__privateGet(this, _options).allowInsecureHttp = allowInsecureHttp;
|
|
1003
1552
|
return this;
|
|
1004
1553
|
}
|
|
1005
1554
|
withTimeout(timeoutMs) {
|
|
1006
|
-
this.
|
|
1555
|
+
__privateGet(this, _options).timeoutMs = timeoutMs;
|
|
1007
1556
|
return this;
|
|
1008
1557
|
}
|
|
1009
1558
|
withHttpAdapter(httpAdapter) {
|
|
1010
|
-
this.
|
|
1559
|
+
__privateGet(this, _options).httpAdapter = httpAdapter;
|
|
1011
1560
|
return this;
|
|
1012
1561
|
}
|
|
1013
1562
|
withHttpLogger(httpLogger) {
|
|
1014
|
-
this.
|
|
1563
|
+
__privateGet(this, _options).httpLogger = httpLogger;
|
|
1015
1564
|
return this;
|
|
1016
1565
|
}
|
|
1017
1566
|
build() {
|
|
1018
|
-
return new AuthyonServerClient({ ...this
|
|
1567
|
+
return new AuthyonServerClient({ ...__privateGet(this, _options) });
|
|
1568
|
+
}
|
|
1569
|
+
};
|
|
1570
|
+
_options = new WeakMap();
|
|
1571
|
+
|
|
1572
|
+
// src/client/tenantFilters.ts
|
|
1573
|
+
var TENANT_FILTER_MAX_VALUES = 25;
|
|
1574
|
+
var TENANT_FILTER_MAX_VALUE_LENGTH = 200;
|
|
1575
|
+
var TENANT_FILTER_MAX_DEPTH = 3;
|
|
1576
|
+
var SCOPES = ["public", "private", "custom"];
|
|
1577
|
+
var KEY_SEGMENT = /^[A-Za-z0-9_-]{1,64}$/;
|
|
1578
|
+
function assertPath(path2) {
|
|
1579
|
+
if (typeof path2 !== "string" || path2.length === 0) {
|
|
1580
|
+
throw new RangeError("Authyon: a filter path is required, e.g. public.kind.");
|
|
1581
|
+
}
|
|
1582
|
+
const [scope, ...keys] = path2.split(".");
|
|
1583
|
+
if (!SCOPES.includes(scope)) {
|
|
1584
|
+
throw new RangeError(
|
|
1585
|
+
`Authyon: unknown filter scope "${scope}" \u2014 use ${SCOPES.join(", ")} (e.g. public.kind).`
|
|
1586
|
+
);
|
|
1587
|
+
}
|
|
1588
|
+
if (keys.length === 0) {
|
|
1589
|
+
throw new RangeError(`Authyon: filter path "${path2}" needs a key after the scope.`);
|
|
1590
|
+
}
|
|
1591
|
+
if (keys.length > TENANT_FILTER_MAX_DEPTH) {
|
|
1592
|
+
throw new RangeError(
|
|
1593
|
+
`Authyon: filter path "${path2}" goes deeper than ${TENANT_FILTER_MAX_DEPTH} levels.`
|
|
1594
|
+
);
|
|
1595
|
+
}
|
|
1596
|
+
for (const key of keys) {
|
|
1597
|
+
if (!KEY_SEGMENT.test(key)) {
|
|
1598
|
+
throw new RangeError(
|
|
1599
|
+
`Authyon: invalid filter key "${key}" \u2014 letters, digits, "_" and "-", up to 64 characters.`
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
return path2;
|
|
1604
|
+
}
|
|
1605
|
+
function assertValue(value, forIn) {
|
|
1606
|
+
if (value === void 0 || value === null) {
|
|
1607
|
+
throw new RangeError("Authyon: a filter value is required.");
|
|
1608
|
+
}
|
|
1609
|
+
const text = String(value);
|
|
1610
|
+
if (text.length === 0) {
|
|
1611
|
+
throw new RangeError("Authyon: a filter value may not be empty.");
|
|
1612
|
+
}
|
|
1613
|
+
if (text.length > TENANT_FILTER_MAX_VALUE_LENGTH) {
|
|
1614
|
+
throw new RangeError(
|
|
1615
|
+
`Authyon: a filter value may hold at most ${TENANT_FILTER_MAX_VALUE_LENGTH} characters.`
|
|
1616
|
+
);
|
|
1617
|
+
}
|
|
1618
|
+
if (text.includes(",")) {
|
|
1619
|
+
throw new RangeError(
|
|
1620
|
+
forIn ? "Authyon: an `in` value may not contain a comma \u2014 pass separate values instead." : "Authyon: a filter value may not contain a comma."
|
|
1621
|
+
);
|
|
1622
|
+
}
|
|
1623
|
+
return text;
|
|
1624
|
+
}
|
|
1625
|
+
var tenantFilter = {
|
|
1626
|
+
/** `public.kind:eq:reseller` — the key equals this value. */
|
|
1627
|
+
eq(path2, value) {
|
|
1628
|
+
return `${assertPath(path2)}:eq:${assertValue(value, false)}`;
|
|
1629
|
+
},
|
|
1630
|
+
/** `public.kind:in:reseller,partner` — the key equals any of these (SQL IN). */
|
|
1631
|
+
in(path2, values) {
|
|
1632
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
1633
|
+
throw new RangeError("Authyon: `in` needs at least one value.");
|
|
1634
|
+
}
|
|
1635
|
+
if (values.length > TENANT_FILTER_MAX_VALUES) {
|
|
1636
|
+
throw new RangeError(`Authyon: \`in\` takes at most ${TENANT_FILTER_MAX_VALUES} values.`);
|
|
1637
|
+
}
|
|
1638
|
+
const unique = [...new Set(values.map((value) => assertValue(value, true)))];
|
|
1639
|
+
return `${assertPath(path2)}:in:${unique.join(",")}`;
|
|
1640
|
+
},
|
|
1641
|
+
/** `public.notes:like:nacional` — the key contains this text, case-insensitive.
|
|
1642
|
+
* The server escapes the LIKE metacharacters, so `100%` means `100%`. */
|
|
1643
|
+
like(path2, text) {
|
|
1644
|
+
return `${assertPath(path2)}:like:${assertValue(text, false)}`;
|
|
1645
|
+
},
|
|
1646
|
+
/** `custom.cnpj:exists` — the key is present, whatever its value. */
|
|
1647
|
+
exists(path2) {
|
|
1648
|
+
return `${assertPath(path2)}:exists`;
|
|
1019
1649
|
}
|
|
1020
1650
|
};
|
|
1021
1651
|
|
|
@@ -1089,6 +1719,11 @@ function hasPermission(source, requiredPermission) {
|
|
|
1089
1719
|
const permissionSource = isPermissionList(source) ? { permissions: source } : source;
|
|
1090
1720
|
return createAuthyonAbility(permissionSource).can(requirement.action, requirement.subject);
|
|
1091
1721
|
}
|
|
1722
|
+
function hasPermissionGroup(source, group) {
|
|
1723
|
+
const hasAll = (group.allOf ?? []).every((permission) => hasPermission(source, permission));
|
|
1724
|
+
const hasAny = !group.anyOf?.length || group.anyOf.some((permission) => hasPermission(source, permission));
|
|
1725
|
+
return hasAll && hasAny;
|
|
1726
|
+
}
|
|
1092
1727
|
function isPermissionList(source) {
|
|
1093
1728
|
return Array.isArray(source);
|
|
1094
1729
|
}
|
|
@@ -1127,25 +1762,25 @@ function matchesToken(value, expected, wildcard) {
|
|
|
1127
1762
|
function matchesSegments(pattern, value) {
|
|
1128
1763
|
const patternSegments = pattern.split(":");
|
|
1129
1764
|
const valueSegments = value.split(":");
|
|
1130
|
-
return patternSegments.length === valueSegments.length && patternSegments.every((
|
|
1765
|
+
return patternSegments.length === valueSegments.length && patternSegments.every((segment2, index) => segment2 === "*" || segment2 === valueSegments[index]);
|
|
1131
1766
|
}
|
|
1132
1767
|
function matchesField(pattern, field) {
|
|
1133
1768
|
if (pattern === "*" || pattern === field) return true;
|
|
1134
1769
|
return pattern.endsWith(".*") && field.startsWith(pattern.slice(0, -1));
|
|
1135
1770
|
}
|
|
1136
1771
|
function matchesConditions(subject, conditions) {
|
|
1137
|
-
return Object.entries(conditions).every(([
|
|
1138
|
-
if (
|
|
1772
|
+
return Object.entries(conditions).every(([path2, expected]) => {
|
|
1773
|
+
if (path2 === "$and" && Array.isArray(expected)) {
|
|
1139
1774
|
return expected.every(
|
|
1140
1775
|
(condition) => matchesConditions(subject, condition)
|
|
1141
1776
|
);
|
|
1142
1777
|
}
|
|
1143
|
-
if (
|
|
1778
|
+
if (path2 === "$or" && Array.isArray(expected)) {
|
|
1144
1779
|
return expected.some(
|
|
1145
1780
|
(condition) => matchesConditions(subject, condition)
|
|
1146
1781
|
);
|
|
1147
1782
|
}
|
|
1148
|
-
return matchesValue(readPath(subject,
|
|
1783
|
+
return matchesValue(readPath(subject, path2), expected);
|
|
1149
1784
|
});
|
|
1150
1785
|
}
|
|
1151
1786
|
function matchesValue(actual, expected) {
|
|
@@ -1177,8 +1812,8 @@ function matchesValue(actual, expected) {
|
|
|
1177
1812
|
}
|
|
1178
1813
|
});
|
|
1179
1814
|
}
|
|
1180
|
-
function readPath(value,
|
|
1181
|
-
return
|
|
1815
|
+
function readPath(value, path2) {
|
|
1816
|
+
return path2.split(".").reduce((current, part) => isRecord2(current) ? current[part] : void 0, value);
|
|
1182
1817
|
}
|
|
1183
1818
|
function defaultSubjectType(subject) {
|
|
1184
1819
|
const explicit = subject.__type ?? subject.type ?? subject.kind;
|
|
@@ -1332,8 +1967,12 @@ function unauthenticated() {
|
|
|
1332
1967
|
FetchHttpAdapter,
|
|
1333
1968
|
JwksTokenVerifier,
|
|
1334
1969
|
LoggingHttpAdapter,
|
|
1970
|
+
TENANT_FILTER_MAX_DEPTH,
|
|
1971
|
+
TENANT_FILTER_MAX_VALUES,
|
|
1972
|
+
TENANT_FILTER_MAX_VALUE_LENGTH,
|
|
1335
1973
|
TenantScopedClient,
|
|
1336
1974
|
TokenVerificationError,
|
|
1975
|
+
UserScopedClient,
|
|
1337
1976
|
authorizeRequest,
|
|
1338
1977
|
authorizeToken,
|
|
1339
1978
|
createAuthorizationErrorResponse,
|
|
@@ -1342,5 +1981,7 @@ function unauthenticated() {
|
|
|
1342
1981
|
createClient,
|
|
1343
1982
|
createExpressAuthorizationMiddleware,
|
|
1344
1983
|
hasPermission,
|
|
1345
|
-
|
|
1984
|
+
hasPermissionGroup,
|
|
1985
|
+
normalizeClientIp,
|
|
1986
|
+
tenantFilter
|
|
1346
1987
|
});
|