@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/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
- normalizeClientIp: () => normalizeClientIp
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.token = null;
57
- this.inFlight = null;
277
+ __privateAdd(this, _token, null);
278
+ __privateAdd(this, _inFlight, null);
58
279
  }
59
280
  async getAccessToken() {
60
- if (this.token && Date.now() < this.token.expiresAt - this.expirySkewMs) {
61
- return this.token.accessToken;
281
+ if (__privateGet(this, _token) && Date.now() < __privateGet(this, _token).expiresAt - this.expirySkewMs) {
282
+ return __privateGet(this, _token).accessToken;
62
283
  }
63
- if (this.inFlight) return this.inFlight;
64
- this.inFlight = this.acquire().then((token) => {
65
- this.token = token;
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.inFlight = null;
69
- });
70
- return this.inFlight;
289
+ __privateSet(this, _inFlight, null);
290
+ }));
291
+ return __privateGet(this, _inFlight);
71
292
  }
72
293
  clear() {
73
- this.token = null;
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
- super(body.detail ?? body.title ?? `Authyon request failed with status ${status}`);
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 = body.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(path, params) {
187
- if (!params) return path;
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 !== void 0) query.set(key, String(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 ? `${path}${path.includes("?") ? "&" : "?"}${value}` : path;
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(path, init) {
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}${path}`,
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(path, options = {}) {
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(path, options.query), {
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
- body = await response.json();
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(path, options = {}) {
377
- return this.parse(await this.send(path, options));
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
- /** GET /env/tenants — paginated, searchable list of tenants in the environment. */
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
- /** DELETE /env/tenants/{tenantId} — delete a tenant. */
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. Confirmed live: a bare array (unlike
688
- * `environment.users.list()`, this one does NOT paginate despite
689
- * accepting `skip`/`take`), with the same shape
690
- * `environment.users.list()` items have.
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.clientSecret = options.clientSecret;
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.clientSecret) {
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.clientSecret
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(path, options = {}) {
840
- const headers = { ...options.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(path, { ...options, headers });
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, path, options = {}) {
902
- return this.request(path, {
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
- * ⚠️ Inferred, not confirmed live (no tenant client credentials
916
- * available to exchange for a scoped token) — assumed to share
917
- * `/env/tenants/{id}/members`'s confirmed shape (bare array of full
918
- * user records), since both are the "admin" member listing as opposed
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(path, options = {}) {
979
- return this.server._requestAsTenant(await this.getAccessToken(), path, options);
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.options = {};
1534
+ __privateAdd(this, _options, {});
990
1535
  }
991
1536
  withEnvironmentKey(envKey) {
992
- this.options.envKey = envKey;
1537
+ __privateGet(this, _options).envKey = envKey;
993
1538
  return this;
994
1539
  }
995
1540
  withEnvironmentCredentials(clientId, clientSecret) {
996
- this.options.clientId = clientId;
997
- this.options.clientSecret = clientSecret;
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.options.baseUrl = baseUrl;
1002
- this.options.allowInsecureHttp = allowInsecureHttp;
1550
+ __privateGet(this, _options).baseUrl = baseUrl;
1551
+ __privateGet(this, _options).allowInsecureHttp = allowInsecureHttp;
1003
1552
  return this;
1004
1553
  }
1005
1554
  withTimeout(timeoutMs) {
1006
- this.options.timeoutMs = timeoutMs;
1555
+ __privateGet(this, _options).timeoutMs = timeoutMs;
1007
1556
  return this;
1008
1557
  }
1009
1558
  withHttpAdapter(httpAdapter) {
1010
- this.options.httpAdapter = httpAdapter;
1559
+ __privateGet(this, _options).httpAdapter = httpAdapter;
1011
1560
  return this;
1012
1561
  }
1013
1562
  withHttpLogger(httpLogger) {
1014
- this.options.httpLogger = httpLogger;
1563
+ __privateGet(this, _options).httpLogger = httpLogger;
1015
1564
  return this;
1016
1565
  }
1017
1566
  build() {
1018
- return new AuthyonServerClient({ ...this.options });
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((segment, index) => segment === "*" || segment === valueSegments[index]);
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(([path, expected]) => {
1138
- if (path === "$and" && Array.isArray(expected)) {
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 (path === "$or" && Array.isArray(expected)) {
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, path), expected);
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, path) {
1181
- return path.split(".").reduce((current, part) => isRecord2(current) ? current[part] : void 0, value);
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
- normalizeClientIp
1984
+ hasPermissionGroup,
1985
+ normalizeClientIp,
1986
+ tenantFilter
1346
1987
  });