@authyon/server 0.2.0-beta.2 → 0.2.0-beta.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 = {};
@@ -32,6 +39,7 @@ __export(index_exports, {
32
39
  LoggingHttpAdapter: () => LoggingHttpAdapter,
33
40
  TenantScopedClient: () => TenantScopedClient,
34
41
  TokenVerificationError: () => TokenVerificationError,
42
+ UserScopedClient: () => UserScopedClient,
35
43
  authorizeRequest: () => authorizeRequest,
36
44
  authorizeToken: () => authorizeToken,
37
45
  createAuthorizationErrorResponse: () => createAuthorizationErrorResponse,
@@ -40,39 +48,250 @@ __export(index_exports, {
40
48
  createClient: () => createClient,
41
49
  createExpressAuthorizationMiddleware: () => createExpressAuthorizationMiddleware,
42
50
  hasPermission: () => hasPermission,
51
+ hasPermissionGroup: () => hasPermissionGroup,
43
52
  normalizeClientIp: () => normalizeClientIp
44
53
  });
45
54
  module.exports = __toCommonJS(index_exports);
46
55
 
56
+ // src/client/credentialManagement.ts
57
+ function segment(value) {
58
+ if (typeof value !== "string" || !value.trim() || value === "." || value === ".." || [...value].some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127)) {
59
+ throw new TypeError("Authyon: invalid route identifier.");
60
+ }
61
+ return encodeURIComponent(value);
62
+ }
63
+ function permissionsBody(input) {
64
+ if (input.permissions !== void 0 && input.scopes !== void 0) {
65
+ throw new TypeError("Authyon: supply permissions or scopes, not both.");
66
+ }
67
+ const values = input.permissions ?? input.scopes;
68
+ if (!Array.isArray(values) || values.length === 0) {
69
+ throw new TypeError("Authyon: explicit credential permissions are required.");
70
+ }
71
+ const permissions = values.map((value) => {
72
+ if (typeof value !== "string") throw new TypeError("Authyon: invalid permission.");
73
+ const normalized = value.trim().toLowerCase();
74
+ if (!/^(?:\*|[a-z][a-z0-9_-]*):(?:\*|[a-z][a-z0-9_-]*):(?:\*|[a-z][a-z0-9_-]*)$/u.test(normalized)) {
75
+ throw new TypeError("Authyon: permissions must have namespace:resource:action format.");
76
+ }
77
+ return normalized;
78
+ });
79
+ return { permissions: [...new Set(permissions)] };
80
+ }
81
+ function lifetimeBody(input) {
82
+ if (input.lifetimeDays === void 0 || input.lifetimeDays === null) return {};
83
+ if (!Number.isInteger(input.lifetimeDays) || input.lifetimeDays < 1 || input.lifetimeDays > 3650) {
84
+ throw new RangeError("Authyon: lifetimeDays must be between 1 and 3650.");
85
+ }
86
+ return { lifetimeDays: input.lifetimeDays };
87
+ }
88
+ function credentialQuery(options = {}) {
89
+ const { skip, take, search } = options;
90
+ if (skip !== void 0 && (!Number.isInteger(skip) || skip < 0 || skip > 2147483647))
91
+ throw new RangeError("Authyon: invalid skip.");
92
+ if (take !== void 0 && (!Number.isInteger(take) || take < 1 || take > 100))
93
+ throw new RangeError("Authyon: take must be between 1 and 100.");
94
+ if (search !== void 0 && (typeof search !== "string" || search.trim().length > 200))
95
+ throw new RangeError("Authyon: search must contain at most 200 characters.");
96
+ return { search: search?.trim() || void 0, skip, take };
97
+ }
98
+ function credentialSummary(value) {
99
+ return {
100
+ id: value.id,
101
+ clientId: value.clientId,
102
+ description: value.description,
103
+ createdAt: value.createdAt,
104
+ lastUsedAt: value.lastUsedAt,
105
+ revokedAt: value.revokedAt,
106
+ expiresAt: value.expiresAt ?? null,
107
+ isActive: value.isActive,
108
+ permissions: [...value.permissions]
109
+ };
110
+ }
111
+ function credentialDetail(value) {
112
+ return {
113
+ ...credentialSummary(value),
114
+ createdBy: value.createdBy == null ? null : {
115
+ id: value.createdBy.id,
116
+ type: value.createdBy.type,
117
+ displayName: value.createdBy.displayName
118
+ },
119
+ updatedAt: value.updatedAt,
120
+ secretRotatedAt: value.secretRotatedAt,
121
+ expiresAt: value.expiresAt ?? null,
122
+ ageSeconds: value.ageSeconds,
123
+ lifetimeSeconds: value.lifetimeSeconds,
124
+ accessTokenLifetimeSeconds: value.accessTokenLifetimeSeconds
125
+ };
126
+ }
127
+ async function credentialPage(request, path2, options = {}) {
128
+ const query = credentialQuery(options);
129
+ const page = await request(path2, { query });
130
+ if (!page || !Array.isArray(page.data))
131
+ throw new TypeError("Authyon: expected a paginated credential response.");
132
+ return {
133
+ data: page.data.map(credentialSummary),
134
+ page: page.page,
135
+ perPage: page.perPage,
136
+ total: page.total,
137
+ pages: page.pages,
138
+ hasNext: page.hasNext,
139
+ hasPrev: page.hasPrev
140
+ };
141
+ }
142
+ async function allCredentials(request, path2) {
143
+ const result = [];
144
+ const seen = /* @__PURE__ */ new Set();
145
+ let skip = 0;
146
+ for (let iteration = 0; iteration < 1e3; iteration++) {
147
+ const page = await request(
148
+ path2,
149
+ iteration === 0 ? {} : { query: { skip, take: 100 } }
150
+ );
151
+ if (Array.isArray(page)) {
152
+ if (iteration !== 0)
153
+ throw new Error("Authyon: credential pagination changed during listing.");
154
+ return page.map(credentialSummary);
155
+ }
156
+ if (!page || !Array.isArray(page.data))
157
+ throw new TypeError("Authyon: invalid credential page.");
158
+ for (const row of page.data) {
159
+ if (seen.has(row.id))
160
+ throw new Error("Authyon: credential pages overlapped; retry the listing.");
161
+ seen.add(row.id);
162
+ result.push(credentialSummary(row));
163
+ }
164
+ if (!page.hasNext) return result;
165
+ if (page.data.length === 0) throw new Error("Authyon: credential pagination made no progress.");
166
+ skip += page.data.length;
167
+ }
168
+ throw new Error("Authyon: use listPage for more than 1000 credential pages.");
169
+ }
170
+
171
+ // ../../internal/core/security/approvals.ts
172
+ function path(id, suffix = "") {
173
+ if (typeof id !== "string" || !id.trim())
174
+ throw new TypeError("Authyon: an approval id is required.");
175
+ return `/auth/authorizations/${encodeURIComponent(id)}${suffix}`;
176
+ }
177
+ var NO_METHODS = {
178
+ password: false,
179
+ authenticator: false,
180
+ emailCode: false,
181
+ passkey: false,
182
+ passkeyCount: 0,
183
+ recoveryCodes: 0,
184
+ sso: [],
185
+ twoFactor: false
186
+ };
187
+ function subjectSecurity(request) {
188
+ return {
189
+ methods: async () => {
190
+ const me = await request("/auth/me");
191
+ return me.security ?? NO_METHODS;
192
+ },
193
+ approvals: {
194
+ get: (id) => request(path(id)),
195
+ webauthnOptions: (id) => request(path(id, "/webauthn/options"), { method: "POST" }),
196
+ confirm: (id, input) => request(path(id, "/confirm"), { method: "POST", body: input }),
197
+ reject: (id) => request(path(id, "/reject"), { method: "POST" })
198
+ }
199
+ };
200
+ }
201
+
202
+ // src/client/scopedManagementClient.ts
203
+ function scopedRequest(request, source) {
204
+ return async (path2, options = {}) => {
205
+ const token = typeof source === "function" ? await source() : source;
206
+ if (typeof token !== "string" || !token.trim() || /\s/u.test(token))
207
+ throw new TypeError("Authyon: a valid access token is required.");
208
+ return request(path2, {
209
+ ...options,
210
+ headers: { ...options.headers, Authorization: `Bearer ${token}` }
211
+ });
212
+ };
213
+ }
214
+ var _request;
215
+ var UserScopedClient = class {
216
+ /** @hidden */
217
+ constructor(request, token) {
218
+ __privateAdd(this, _request);
219
+ this.tenants = {
220
+ members: {
221
+ list: (tenantId, options = {}) => __privateGet(this, _request).call(this, `/auth/tenants/${segment(tenantId)}/members`, { query: options }),
222
+ /** Adds an existing environment user by email; does not send an invitation email. */
223
+ invite: (tenantId, input) => __privateGet(this, _request).call(this, `/auth/tenants/${segment(tenantId)}/members`, {
224
+ method: "POST",
225
+ body: { email: input.email, roles: input.roles }
226
+ }),
227
+ remove: (tenantId, userId) => __privateGet(this, _request).call(this, `/auth/tenants/${segment(tenantId)}/members/${segment(userId)}`, {
228
+ method: "DELETE"
229
+ })
230
+ },
231
+ /**
232
+ * Invites by e-mail or by link, acting as the user. Requires the
233
+ * `tenants:members:invite` permission on the tenant.
234
+ */
235
+ invites: {
236
+ list: (tenantId) => __privateGet(this, _request).call(this, `/auth/tenants/${segment(tenantId)}/invites`),
237
+ /** Always returns `acceptUrl`; `sendEmail: false` skips the e-mail. */
238
+ create: (tenantId, input) => __privateGet(this, _request).call(this, `/auth/tenants/${segment(tenantId)}/invites`, {
239
+ method: "POST",
240
+ body: {
241
+ email: input.email,
242
+ roles: input.roles,
243
+ sendEmail: input.sendEmail ?? true,
244
+ redirectUri: input.redirectUri
245
+ }
246
+ }),
247
+ /** New link (the previous one stops working), mailed unless `sendEmail` is `false`. */
248
+ resend: (tenantId, inviteId, input = {}) => __privateGet(this, _request).call(this, `/auth/tenants/${segment(tenantId)}/invites/${segment(inviteId)}/resend`, {
249
+ method: "POST",
250
+ body: { sendEmail: input.sendEmail ?? true }
251
+ }),
252
+ revoke: (tenantId, inviteId) => __privateGet(this, _request).call(this, `/auth/tenants/${segment(tenantId)}/invites/${segment(inviteId)}`, {
253
+ method: "DELETE"
254
+ })
255
+ }
256
+ };
257
+ __privateSet(this, _request, scopedRequest(request, token));
258
+ this.security = subjectSecurity(__privateGet(this, _request));
259
+ }
260
+ };
261
+ _request = new WeakMap();
262
+
47
263
  // ../../internal/core/config/defaults.ts
48
264
  var DEFAULT_BASE_URL = "https://api.authyon.com";
49
265
  var DEFAULT_EXPIRY_SKEW_MS = 3e4;
50
266
 
51
267
  // ../../internal/core/auth/expiringTokenProvider.ts
268
+ var _token, _inFlight;
52
269
  var ExpiringTokenProvider = class {
53
270
  constructor(acquire, expirySkewMs = DEFAULT_EXPIRY_SKEW_MS) {
54
271
  this.acquire = acquire;
55
272
  this.expirySkewMs = expirySkewMs;
56
- this.token = null;
57
- this.inFlight = null;
273
+ __privateAdd(this, _token, null);
274
+ __privateAdd(this, _inFlight, null);
58
275
  }
59
276
  async getAccessToken() {
60
- if (this.token && Date.now() < this.token.expiresAt - this.expirySkewMs) {
61
- return this.token.accessToken;
277
+ if (__privateGet(this, _token) && Date.now() < __privateGet(this, _token).expiresAt - this.expirySkewMs) {
278
+ return __privateGet(this, _token).accessToken;
62
279
  }
63
- if (this.inFlight) return this.inFlight;
64
- this.inFlight = this.acquire().then((token) => {
65
- this.token = token;
280
+ if (__privateGet(this, _inFlight)) return __privateGet(this, _inFlight);
281
+ __privateSet(this, _inFlight, this.acquire().then((token) => {
282
+ __privateSet(this, _token, token);
66
283
  return token.accessToken;
67
284
  }).finally(() => {
68
- this.inFlight = null;
69
- });
70
- return this.inFlight;
285
+ __privateSet(this, _inFlight, null);
286
+ }));
287
+ return __privateGet(this, _inFlight);
71
288
  }
72
289
  clear() {
73
- this.token = null;
290
+ __privateSet(this, _token, null);
74
291
  }
75
292
  };
293
+ _token = new WeakMap();
294
+ _inFlight = new WeakMap();
76
295
 
77
296
  // ../../internal/core/errors/authyonError.ts
78
297
  var ErrorCodes = {
@@ -86,16 +305,45 @@ var ErrorCodes = {
86
305
  EmailTaken: "user.email_taken",
87
306
  PasswordWeak: "user.password_weak",
88
307
  PasswordPwned: "user.password_pwned",
89
- RateLimited: "rate_limited"
308
+ RateLimited: "rate_limited",
309
+ StepUpRequired: "step_up_required",
310
+ InvalidSecondFactorCode: "invalid_code",
311
+ VerificationAttemptsExhausted: "verification_attempts_exhausted",
312
+ MethodNotEnrolled: "method_not_enrolled",
313
+ InvalidApprovalState: "invalid_authorization_state",
314
+ PayloadMismatch: "payload_mismatch",
315
+ ApprovalNotConsumable: "authorization_not_consumable",
316
+ IdempotencyConflict: "idempotency_conflict",
317
+ UserDisabled: "user_disabled",
318
+ UserNotFound: "user_not_found",
319
+ TemporarilyUnavailable: "temporarily_unavailable",
320
+ InsufficientScope: "insufficient_scope",
321
+ CredentialThrottled: "credential_throttled",
322
+ ApprovalNotFound: "authorization_not_found",
323
+ SessionRevoked: "session_revoked",
324
+ TwoFactorTooManyFailures: "user.2fa.too_many_failures"
90
325
  };
326
+ var STANDARD_ERROR_FIELDS = /* @__PURE__ */ new Set([
327
+ "type",
328
+ "status",
329
+ "title",
330
+ "detail",
331
+ "code",
332
+ "error",
333
+ "error_description"
334
+ ]);
91
335
  var AuthyonError = class extends Error {
92
336
  constructor(status, body, options = {}) {
93
- super(body.detail ?? body.title ?? `Authyon request failed with status ${status}`);
337
+ const detail = body.detail ?? body.error_description;
338
+ super(detail ?? body.title ?? `Authyon request failed with status ${status}`);
94
339
  this.name = "AuthyonError";
95
340
  this.status = status;
96
- this.code = body.code ?? "unknown";
341
+ this.code = body.code ?? body.error ?? "unknown";
97
342
  this.title = body.title ?? "Error";
98
- this.detail = body.detail;
343
+ this.detail = detail;
344
+ this.extensions = Object.fromEntries(
345
+ Object.entries(body).filter(([key]) => !STANDARD_ERROR_FIELDS.has(key))
346
+ );
99
347
  this.requestId = options.requestId;
100
348
  this.retryAfter = options.retryAfter;
101
349
  this.cause = options.cause;
@@ -145,6 +393,7 @@ var AuthyonError = class extends Error {
145
393
  };
146
394
  function classifyError(status, code) {
147
395
  if (code === ErrorCodes.NetworkError) return "network";
396
+ if (code === ErrorCodes.StepUpRequired) return "authentication";
148
397
  if (code === ErrorCodes.Timeout || status === 408) return "timeout";
149
398
  if (code === ErrorCodes.RateLimited || status === 429) return "rate_limit";
150
399
  if (code === ErrorCodes.EmailTaken || status === 409) return "conflict";
@@ -183,14 +432,14 @@ function isRetryable(category) {
183
432
  }
184
433
 
185
434
  // ../../internal/core/http/query.ts
186
- function appendQuery(path, params) {
187
- if (!params) return path;
435
+ function appendQuery(path2, params) {
436
+ if (!params) return path2;
188
437
  const query = new URLSearchParams();
189
438
  for (const [key, value2] of Object.entries(params)) {
190
439
  if (value2 !== void 0) query.set(key, String(value2));
191
440
  }
192
441
  const value = query.toString();
193
- return value ? `${path}${path.includes("?") ? "&" : "?"}${value}` : path;
442
+ return value ? `${path2}${path2.includes("?") ? "&" : "?"}${value}` : path2;
194
443
  }
195
444
 
196
445
  // ../../internal/core/http/httpAdapter.ts
@@ -203,7 +452,8 @@ var FetchHttpAdapter = class {
203
452
  method: request.method,
204
453
  headers: request.headers,
205
454
  body: request.body,
206
- signal: request.signal
455
+ signal: request.signal,
456
+ redirect: "error"
207
457
  });
208
458
  }
209
459
  };
@@ -255,6 +505,9 @@ function defaultHttpLogger(event) {
255
505
  }
256
506
  function sanitizeUrl(value) {
257
507
  const url = new URL(value);
508
+ url.username = "";
509
+ url.password = "";
510
+ url.hash = "";
258
511
  const queryKeys = /* @__PURE__ */ new Set();
259
512
  url.searchParams.forEach((_value, key) => queryKeys.add(key));
260
513
  url.search = queryKeys.size > 0 ? [...queryKeys].map((key) => `${encodeURIComponent(key)}=REDACTED`).join("&") : "";
@@ -284,12 +537,12 @@ function createSharedTransport(options) {
284
537
  const httpAdapter = options.httpLogger ? new LoggingHttpAdapter(baseAdapter, options.httpLogger) : baseAdapter;
285
538
  return {
286
539
  baseUrl,
287
- async request(path, init) {
540
+ async request(path2, init) {
288
541
  const controller = new AbortController();
289
542
  const timeout = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : void 0;
290
543
  try {
291
544
  return await httpAdapter.request({
292
- url: `${baseUrl}${path}`,
545
+ url: `${baseUrl}${path2}`,
293
546
  method: init.method ?? "GET",
294
547
  headers: normalizeHeaders(init.headers),
295
548
  body: init.body,
@@ -347,11 +600,11 @@ var JsonHttpClient = class {
347
600
  constructor(transport) {
348
601
  this.transport = transport;
349
602
  }
350
- async send(path, options = {}) {
603
+ async send(path2, options = {}) {
351
604
  const headers = { ...options.headers };
352
605
  if (options.body !== void 0) headers["Content-Type"] = "application/json";
353
606
  try {
354
- return await this.transport.request(appendQuery(path, options.query), {
607
+ return await this.transport.request(appendQuery(path2, options.query), {
355
608
  method: options.method ?? "GET",
356
609
  headers,
357
610
  body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
@@ -365,7 +618,9 @@ var JsonHttpClient = class {
365
618
  if (!response.ok) {
366
619
  let body = {};
367
620
  try {
368
- body = await response.json();
621
+ const parsed = await response.json();
622
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
623
+ body = parsed;
369
624
  } catch {
370
625
  }
371
626
  throw new AuthyonError(response.status, body, responseMetadata(response));
@@ -373,8 +628,8 @@ var JsonHttpClient = class {
373
628
  if (response.status === 204) return void 0;
374
629
  return await response.json();
375
630
  }
376
- async request(path, options = {}) {
377
- return this.parse(await this.send(path, options));
631
+ async request(path2, options = {}) {
632
+ return this.parse(await this.send(path2, options));
378
633
  }
379
634
  };
380
635
 
@@ -536,8 +791,10 @@ function isRecord(value) {
536
791
  }
537
792
 
538
793
  // src/client/authyonServerClient.ts
794
+ var _clientSecret;
539
795
  var AuthyonServerClient = class {
540
796
  constructor(options = {}) {
797
+ __privateAdd(this, _clientSecret);
541
798
  // ── Discovery ──────────────────────────────────────────────────────────────
542
799
  this.discovery = {
543
800
  /** GET /.well-known/jwks.json — the environment's public signing keys. */
@@ -571,6 +828,14 @@ var AuthyonServerClient = class {
571
828
  client_id: credentials.clientId,
572
829
  client_secret: credentials.clientSecret
573
830
  }
831
+ }),
832
+ /** POST /tenant/auth/validate — validates an already-minted tenant-client bearer token. */
833
+ validate: (accessToken, context) => this.request("/tenant/auth/validate", {
834
+ method: "POST",
835
+ headers: {
836
+ ...clientIpHeaders(context),
837
+ Authorization: `Bearer ${accessToken}`
838
+ }
574
839
  })
575
840
  };
576
841
  // ── Environment management (users, tenants, roles, permissions, audit) ───
@@ -616,6 +881,39 @@ var AuthyonServerClient = class {
616
881
  envBearer: true,
617
882
  body: { newPassword, reason }
618
883
  }),
884
+ /**
885
+ * POST /env/password-resets — issues a password-reset token for the
886
+ * account with this address and returns it, so your application can
887
+ * deliver it in its own e-mail instead of Authyon's. Needs the
888
+ * `authyon:users:password` scope. Unlike the public forgot-password
889
+ * request, an unknown address rejects with `user.not_found` (404).
890
+ * A previous live token for the same account stops working.
891
+ */
892
+ issuePasswordReset: (input) => this.request("/env/password-resets", {
893
+ method: "POST",
894
+ envBearer: true,
895
+ body: { ...input, sendEmail: input.sendEmail ?? false }
896
+ }),
897
+ /**
898
+ * POST /auth/password-reset/validate — for a backend hosting the reset
899
+ * page: checks a token without spending it. Public, needs only the
900
+ * `envKey`. Rejects with `user.password_reset.invalid` (401) when the
901
+ * token is unknown, used or expired.
902
+ */
903
+ validatePasswordReset: (token) => this.request("/auth/password-reset/validate", { method: "POST", body: { token } }),
904
+ /**
905
+ * POST /auth/password-reset/confirm — redeems the token and sets the new
906
+ * password (every session of the account is signed out). Public, needs
907
+ * only the `envKey`. Resolves with the `redirectUri` the reset was issued
908
+ * with, or `null`.
909
+ */
910
+ confirmPasswordReset: async (token, newPassword) => {
911
+ const done = await this.request(
912
+ "/auth/password-reset/confirm",
913
+ { method: "POST", body: { token, newPassword } }
914
+ );
915
+ return { redirectUri: done?.redirectUri ?? null };
916
+ },
619
917
  /** POST /env/users/{userId}/sessions/revoke-all — revokes every session the user has. */
620
918
  revokeSessions: (userId, reason) => this.request(`/env/users/${encodeURIComponent(userId)}/sessions/revoke-all`, {
621
919
  method: "POST",
@@ -664,7 +962,63 @@ var AuthyonServerClient = class {
664
962
  )
665
963
  },
666
964
  tenants: {
667
- /** GET /env/tenants — paginated, searchable list of tenants in the environment. */
965
+ credentials: {
966
+ /** GET /env/tenants/{tenantId}/credentials. Returns metadata only, including revoked credentials. */
967
+ list: (tenantId) => allCredentials(
968
+ (path2, options) => this.request(path2, { ...options, envBearer: true }),
969
+ `/env/tenants/${segment(tenantId)}/credentials`
970
+ ),
971
+ /** Paginated searchable listing. Prefer this over loading every credential. */
972
+ listPage: (tenantId, options = {}) => credentialPage(
973
+ (path2, requestOptions) => this.request(path2, { ...requestOptions, envBearer: true }),
974
+ `/env/tenants/${segment(tenantId)}/credentials`,
975
+ options
976
+ ),
977
+ get: async (tenantId, credentialId) => credentialDetail(
978
+ await this.request(
979
+ `/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}`,
980
+ { envBearer: true }
981
+ )
982
+ ),
983
+ /** Explicit scopes/permissions; only issuance returns the client secret. */
984
+ create: (tenantId, input) => this.request(`/env/tenants/${segment(tenantId)}/credentials`, {
985
+ method: "POST",
986
+ envBearer: true,
987
+ body: {
988
+ description: input.description,
989
+ ...lifetimeBody(input),
990
+ ...permissionsBody(input)
991
+ }
992
+ }),
993
+ /** Returns a new secret once while keeping the same client id. */
994
+ rotate: (tenantId, credentialId) => this.request(
995
+ `/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}/rotate`,
996
+ { method: "POST", envBearer: true }
997
+ ),
998
+ updatePermissions: (tenantId, credentialId, input) => this.request(
999
+ `/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}/permissions`,
1000
+ { method: "PUT", envBearer: true, body: permissionsBody(input) }
1001
+ ),
1002
+ updateScopes: (tenantId, credentialId, scopes) => this.environment.tenants.credentials.updatePermissions(tenantId, credentialId, {
1003
+ scopes
1004
+ }),
1005
+ revoke: (tenantId, credentialId) => this.request(`/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}`, {
1006
+ method: "DELETE",
1007
+ envBearer: true
1008
+ })
1009
+ },
1010
+ /**
1011
+ * GET /env/tenants — one page of the environment's tenants.
1012
+ *
1013
+ * `search` matches, case-insensitively and anywhere in the value, the
1014
+ * name, the slug, the description and the raw text of both metadata
1015
+ * documents — so a tenant filed with `{"cnpj":"00.000.000/0001-00"}`
1016
+ * is found by the number, and `cnpj` alone finds every tenant carrying
1017
+ * that key. `take` defaults to 25 and is capped at 100.
1018
+ *
1019
+ * Against an Authyon API older than 2026-10-21 this route ignored
1020
+ * skip/take/search and answered with a bare array.
1021
+ */
668
1022
  list: (params = {}) => this.request("/env/tenants", { envBearer: true, query: params }),
669
1023
  /** POST /env/tenants — create a tenant in the environment. */
670
1024
  create: (input) => this.request("/env/tenants", { method: "POST", envBearer: true, body: input }),
@@ -676,7 +1030,18 @@ var AuthyonServerClient = class {
676
1030
  envBearer: true,
677
1031
  body: input
678
1032
  }),
679
- /** DELETE /env/tenants/{tenantId} — delete a tenant. */
1033
+ /** PUT /env/tenants/{tenantId}/metadata - replace public and private metadata. */
1034
+ updateMetadata: (tenantId, input) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/metadata`, {
1035
+ method: "PUT",
1036
+ envBearer: true,
1037
+ body: input
1038
+ }),
1039
+ /** POST /env/tenants/{tenantId}/disable — disable without removing tenant data. */
1040
+ disable: (tenantId) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/disable`, {
1041
+ method: "POST",
1042
+ envBearer: true
1043
+ }),
1044
+ /** DELETE /env/tenants/{tenantId} — logically delete (disable) a tenant. */
680
1045
  delete: (tenantId) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}`, {
681
1046
  method: "DELETE",
682
1047
  envBearer: true
@@ -684,10 +1049,16 @@ var AuthyonServerClient = class {
684
1049
  members: {
685
1050
  /**
686
1051
  * 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.
1052
+ * user records, with the same shape `environment.users.list()` items
1053
+ * have, plus `tenantRoles` and `tenantPermissions` scoped to this
1054
+ * tenant.
1055
+ *
1056
+ * Paged like every other list: the `Paged<T>` envelope carries
1057
+ * `total` and `hasNext`, `take` defaults to 50 and is capped at 100.
1058
+ * Up to the Authyon API of 2026-10-21 this one route answered with a
1059
+ * bare array, so a caller had to page until a short page came back;
1060
+ * against such an API `data` is undefined and the array is the body
1061
+ * itself.
691
1062
  */
692
1063
  list: (tenantId, params = {}) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/members`, {
693
1064
  envBearer: true,
@@ -725,6 +1096,61 @@ var AuthyonServerClient = class {
725
1096
  { method: "DELETE", envBearer: true }
726
1097
  )
727
1098
  },
1099
+ /**
1100
+ * Invites by e-mail or by link — for addresses that may not have an
1101
+ * account yet; accepting creates it. Managing them needs the
1102
+ * credential's `authyon:users:write` scope (listing, `authyon:tenants:read`).
1103
+ */
1104
+ invites: {
1105
+ /** GET /env/tenants/{tenantId}/invites — every invite with its status, newest first. */
1106
+ list: (tenantId) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/invites`, { envBearer: true }),
1107
+ /**
1108
+ * POST /env/tenants/{tenantId}/invites — invite an address. Always
1109
+ * returns `acceptUrl`; with `sendEmail: false` Authyon does not mail
1110
+ * it and you deliver the link yourself. Valid 7 days, single use, and
1111
+ * not readable again later.
1112
+ */
1113
+ create: (tenantId, input) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/invites`, {
1114
+ method: "POST",
1115
+ envBearer: true,
1116
+ body: {
1117
+ email: input.email,
1118
+ roles: input.roles,
1119
+ sendEmail: input.sendEmail ?? true,
1120
+ redirectUri: input.redirectUri
1121
+ }
1122
+ }),
1123
+ /** POST .../invites/{inviteId}/resend — new link (the previous one stops working), mailed unless `sendEmail` is `false`. */
1124
+ resend: (tenantId, inviteId, input = {}) => this.request(
1125
+ `/env/tenants/${encodeURIComponent(tenantId)}/invites/${encodeURIComponent(inviteId)}/resend`,
1126
+ { method: "POST", envBearer: true, body: { sendEmail: input.sendEmail ?? true } }
1127
+ ),
1128
+ /** DELETE /env/tenants/{tenantId}/invites/{inviteId} — revoke a pending invite. */
1129
+ revoke: (tenantId, inviteId) => this.request(
1130
+ `/env/tenants/${encodeURIComponent(tenantId)}/invites/${encodeURIComponent(inviteId)}`,
1131
+ { method: "DELETE", envBearer: true }
1132
+ ),
1133
+ /**
1134
+ * POST /auth/tenant-invites/preview — for a backend hosting the accept
1135
+ * page. Public: needs only `envKey`, the token is the credential.
1136
+ */
1137
+ preview: (token) => this.request("/auth/tenant-invites/preview", { method: "POST", body: { token } }),
1138
+ /**
1139
+ * Joins the tenant; for an address with no account, also creates it
1140
+ * (password required) with the e-mail already confirmed. Does not
1141
+ * sign the user in.
1142
+ *
1143
+ * Without metadata: POST /auth/tenant-invites/accept — public, needs
1144
+ * only `envKey`. With `publicMetadata`/`privateMetadata`: POST
1145
+ * /env/tenant-invites/accept with the environment credential
1146
+ * (`authyon:users:write`), since only your backend may set them.
1147
+ */
1148
+ accept: (input) => input.publicMetadata !== void 0 || input.privateMetadata !== void 0 ? this.request("/env/tenant-invites/accept", {
1149
+ method: "POST",
1150
+ envBearer: true,
1151
+ body: input
1152
+ }) : this.request("/auth/tenant-invites/accept", { method: "POST", body: input })
1153
+ },
728
1154
  roles: {
729
1155
  /** GET /env/tenants/{tenantId}/roles — list a tenant's roles. */
730
1156
  list: (tenantId) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/roles`, { envBearer: true }),
@@ -794,6 +1220,60 @@ var AuthyonServerClient = class {
794
1220
  loginActivity: (params = {}) => this.request("/env/audit/login-activity", { envBearer: true, query: params })
795
1221
  }
796
1222
  };
1223
+ // ── Security: one-time codes and approvals ───────────────────────────────
1224
+ //
1225
+ // Everything here runs with the environment credential: `otp` needs the
1226
+ // `authyon:otp:verify` scope, `approvals` needs `authyon:financial:authorize`.
1227
+ // Keep those credentials on the server.
1228
+ this.security = {
1229
+ otp: {
1230
+ /**
1231
+ * POST /env/users/{userId}/otp/verify — checks the user's authenticator
1232
+ * app (TOTP) code. A wrong code returns `{ valid: false }` instead of
1233
+ * throwing. Each code is accepted once; 3 wrong codes in a row sign the
1234
+ * user out everywhere (`sessionsRevoked: true`); 5 lock the check for 15
1235
+ * minutes (`rate_limited`). More than 50 wrong codes in 10 minutes from the
1236
+ * same credential pause it (`credential_throttled`). Needs `authyon:otp:verify`.
1237
+ */
1238
+ check: (userId, code) => this.request(`/env/users/${segment(userId)}/otp/verify`, {
1239
+ method: "POST",
1240
+ envBearer: true,
1241
+ body: { code }
1242
+ })
1243
+ },
1244
+ /**
1245
+ * The customer approves a payload with their authenticator code or
1246
+ * passkey, once.
1247
+ *
1248
+ * 1. `create()` the approval with the payload and the customer.
1249
+ * 2. The customer confirms it (`@authyon/auth` → `security.approvals.confirm`).
1250
+ * 3. `consume()` it right before executing, and execute the returned payload.
1251
+ */
1252
+ approvals: {
1253
+ /** POST /env/authorizations — registers a payload awaiting the customer's approval. */
1254
+ create: (input) => {
1255
+ const { idempotencyKey, ...body } = input;
1256
+ return this.request("/env/authorizations", {
1257
+ method: "POST",
1258
+ envBearer: true,
1259
+ headers: idempotencyKey ? { "Idempotency-Key": idempotencyKey } : void 0,
1260
+ body
1261
+ });
1262
+ },
1263
+ /** GET /env/authorizations/{id} — current status and, once approved, the assurance evidence. */
1264
+ get: (id) => this.request(`/env/authorizations/${segment(id)}`, { envBearer: true }),
1265
+ /**
1266
+ * POST /env/authorizations/{id}/consume — redeems an approved approval
1267
+ * exactly once and returns the approved payload. Pass `payload` to have
1268
+ * the API check it matches (`payload_mismatch` otherwise).
1269
+ */
1270
+ consume: (id, payload) => this.request(`/env/authorizations/${segment(id)}/consume`, {
1271
+ method: "POST",
1272
+ envBearer: true,
1273
+ body: payload === void 0 ? void 0 : { payload }
1274
+ })
1275
+ }
1276
+ };
797
1277
  // ── Generic permission grants ─────────────────────────────────────────────
798
1278
  this.permissions = {
799
1279
  /** POST /users/{userId}/permissions — grant a permission directly to a user. */
@@ -810,7 +1290,8 @@ var AuthyonServerClient = class {
810
1290
  };
811
1291
  this.envKey = options.envKey;
812
1292
  this.clientId = options.clientId;
813
- this.clientSecret = options.clientSecret;
1293
+ __privateSet(this, _clientSecret, options.clientSecret);
1294
+ this.clientIp = options.clientIp;
814
1295
  this.transport = createSharedTransport({
815
1296
  baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
816
1297
  allowInsecureHttp: options.allowInsecureHttp,
@@ -821,14 +1302,14 @@ var AuthyonServerClient = class {
821
1302
  });
822
1303
  this.http = new JsonHttpClient(this.transport);
823
1304
  this.environmentTokenProvider = new ExpiringTokenProvider(async () => {
824
- if (!this.clientId || !this.clientSecret) {
1305
+ if (!this.clientId || !__privateGet(this, _clientSecret)) {
825
1306
  throw new Error(
826
1307
  "Authyon: `clientId`/`clientSecret` are required for environment management calls (mint a pair in the console, under the environment's OAuth clients)."
827
1308
  );
828
1309
  }
829
1310
  const token = await this.environmentAuth.token({
830
1311
  clientId: this.clientId,
831
- clientSecret: this.clientSecret
1312
+ clientSecret: __privateGet(this, _clientSecret)
832
1313
  });
833
1314
  return {
834
1315
  accessToken: token.access_token,
@@ -836,13 +1317,16 @@ var AuthyonServerClient = class {
836
1317
  };
837
1318
  });
838
1319
  }
839
- async request(path, options = {}) {
840
- const headers = { ...options.headers };
1320
+ async request(path2, options = {}) {
1321
+ const headers = {
1322
+ ...clientIpHeaders({ clientIp: this.clientIp }),
1323
+ ...options.headers
1324
+ };
841
1325
  if (options.envBearer) {
842
1326
  headers.Authorization = `Bearer ${await this.getEnvironmentAccessToken()}`;
843
1327
  }
844
1328
  if (this.envKey) headers["X-Authyon-Environment"] = this.envKey;
845
- return this.http.request(path, { ...options, headers });
1329
+ return this.http.request(path2, { ...options, headers });
846
1330
  }
847
1331
  /** Mints (and caches) an environment access token from `clientId`/`clientSecret`. */
848
1332
  async getEnvironmentAccessToken() {
@@ -897,26 +1381,52 @@ var AuthyonServerClient = class {
897
1381
  tenant(credentials) {
898
1382
  return new TenantScopedClient(this, credentials);
899
1383
  }
1384
+ /** Uses an end-user token; the configured environment key selects the environment. */
1385
+ user(token) {
1386
+ return new UserScopedClient((path2, options) => this.request(path2, options), token);
1387
+ }
900
1388
  /** @internal used by {@link TenantScopedClient} to share the base URL/fetch/error handling. */
901
- _requestAsTenant(accessToken, path, options = {}) {
902
- return this.request(path, {
1389
+ _requestAsTenant(accessToken, path2, options = {}) {
1390
+ return this.request(path2, {
903
1391
  ...options,
904
1392
  headers: { ...options.headers, Authorization: `Bearer ${accessToken}` },
905
1393
  envBearer: false
906
1394
  });
907
1395
  }
908
1396
  };
1397
+ _clientSecret = new WeakMap();
909
1398
  var TenantScopedClient = class {
910
1399
  constructor(server, credentials) {
1400
+ /** Invites to the token's tenant, by e-mail or by link. */
1401
+ this.invites = {
1402
+ /** GET /tenant/invites — every invite with its status, newest first. */
1403
+ list: () => this.request("/tenant/invites"),
1404
+ /** POST /tenant/invites — always returns `acceptUrl`; `sendEmail: false` skips the e-mail. */
1405
+ create: (input) => this.request("/tenant/invites", {
1406
+ method: "POST",
1407
+ body: {
1408
+ email: input.email,
1409
+ roles: input.roles,
1410
+ sendEmail: input.sendEmail ?? true,
1411
+ redirectUri: input.redirectUri
1412
+ }
1413
+ }),
1414
+ /** POST /tenant/invites/{inviteId}/resend — new link (the previous one stops working), mailed unless `sendEmail` is `false`. */
1415
+ resend: (inviteId, input = {}) => this.request(`/tenant/invites/${encodeURIComponent(inviteId)}/resend`, {
1416
+ method: "POST",
1417
+ body: { sendEmail: input.sendEmail ?? true }
1418
+ }),
1419
+ /** DELETE /tenant/invites/{inviteId} — revoke a pending invite. */
1420
+ revoke: (inviteId) => this.request(`/tenant/invites/${encodeURIComponent(inviteId)}`, { method: "DELETE" })
1421
+ };
911
1422
  this.members = {
912
1423
  /**
913
1424
  * GET /tenant/members — members of the token's tenant.
914
1425
  *
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.
1426
+ * Same handler as `/env/tenants/{id}/members` (the tenant comes from the
1427
+ * token instead of the path), so the same `Paged<T>` envelope of full
1428
+ * user records, `take` defaulting to 50 and capped at 100 — as opposed
1429
+ * to `/auth/tenants/{id}/members`'s lightweight member shape.
920
1430
  */
921
1431
  list: (params = {}) => this.request("/tenant/members", { query: params }),
922
1432
  /** POST /tenant/members — add a member to the token's tenant. */
@@ -975,8 +1485,12 @@ var TenantScopedClient = class {
975
1485
  async getAccessToken() {
976
1486
  return this.tokenProvider.getAccessToken();
977
1487
  }
978
- async request(path, options = {}) {
979
- return this.server._requestAsTenant(await this.getAccessToken(), path, options);
1488
+ async request(path2, options = {}) {
1489
+ return this.server._requestAsTenant(await this.getAccessToken(), path2, options);
1490
+ }
1491
+ /** POST /tenant/auth/validate — validates this tenant client's current bearer token. */
1492
+ async validate(context) {
1493
+ return this.server.tenantAuth.validate(await this.getAccessToken(), context);
980
1494
  }
981
1495
  };
982
1496
  function createClient(options = {}) {
@@ -984,40 +1498,46 @@ function createClient(options = {}) {
984
1498
  }
985
1499
 
986
1500
  // src/client/authyonServerClientBuilder.ts
1501
+ var _options;
987
1502
  var AuthyonServerClientBuilder = class {
988
1503
  constructor() {
989
- this.options = {};
1504
+ __privateAdd(this, _options, {});
990
1505
  }
991
1506
  withEnvironmentKey(envKey) {
992
- this.options.envKey = envKey;
1507
+ __privateGet(this, _options).envKey = envKey;
993
1508
  return this;
994
1509
  }
995
1510
  withEnvironmentCredentials(clientId, clientSecret) {
996
- this.options.clientId = clientId;
997
- this.options.clientSecret = clientSecret;
1511
+ __privateGet(this, _options).clientId = clientId;
1512
+ __privateGet(this, _options).clientSecret = clientSecret;
1513
+ return this;
1514
+ }
1515
+ withClientIp(clientIp) {
1516
+ __privateGet(this, _options).clientIp = clientIp;
998
1517
  return this;
999
1518
  }
1000
1519
  withBaseUrl(baseUrl, allowInsecureHttp = false) {
1001
- this.options.baseUrl = baseUrl;
1002
- this.options.allowInsecureHttp = allowInsecureHttp;
1520
+ __privateGet(this, _options).baseUrl = baseUrl;
1521
+ __privateGet(this, _options).allowInsecureHttp = allowInsecureHttp;
1003
1522
  return this;
1004
1523
  }
1005
1524
  withTimeout(timeoutMs) {
1006
- this.options.timeoutMs = timeoutMs;
1525
+ __privateGet(this, _options).timeoutMs = timeoutMs;
1007
1526
  return this;
1008
1527
  }
1009
1528
  withHttpAdapter(httpAdapter) {
1010
- this.options.httpAdapter = httpAdapter;
1529
+ __privateGet(this, _options).httpAdapter = httpAdapter;
1011
1530
  return this;
1012
1531
  }
1013
1532
  withHttpLogger(httpLogger) {
1014
- this.options.httpLogger = httpLogger;
1533
+ __privateGet(this, _options).httpLogger = httpLogger;
1015
1534
  return this;
1016
1535
  }
1017
1536
  build() {
1018
- return new AuthyonServerClient({ ...this.options });
1537
+ return new AuthyonServerClient({ ...__privateGet(this, _options) });
1019
1538
  }
1020
1539
  };
1540
+ _options = new WeakMap();
1021
1541
 
1022
1542
  // ../../internal/core/authorization/ability.ts
1023
1543
  var AuthyonAbility = class {
@@ -1089,6 +1609,11 @@ function hasPermission(source, requiredPermission) {
1089
1609
  const permissionSource = isPermissionList(source) ? { permissions: source } : source;
1090
1610
  return createAuthyonAbility(permissionSource).can(requirement.action, requirement.subject);
1091
1611
  }
1612
+ function hasPermissionGroup(source, group) {
1613
+ const hasAll = (group.allOf ?? []).every((permission) => hasPermission(source, permission));
1614
+ const hasAny = !group.anyOf?.length || group.anyOf.some((permission) => hasPermission(source, permission));
1615
+ return hasAll && hasAny;
1616
+ }
1092
1617
  function isPermissionList(source) {
1093
1618
  return Array.isArray(source);
1094
1619
  }
@@ -1127,25 +1652,25 @@ function matchesToken(value, expected, wildcard) {
1127
1652
  function matchesSegments(pattern, value) {
1128
1653
  const patternSegments = pattern.split(":");
1129
1654
  const valueSegments = value.split(":");
1130
- return patternSegments.length === valueSegments.length && patternSegments.every((segment, index) => segment === "*" || segment === valueSegments[index]);
1655
+ return patternSegments.length === valueSegments.length && patternSegments.every((segment2, index) => segment2 === "*" || segment2 === valueSegments[index]);
1131
1656
  }
1132
1657
  function matchesField(pattern, field) {
1133
1658
  if (pattern === "*" || pattern === field) return true;
1134
1659
  return pattern.endsWith(".*") && field.startsWith(pattern.slice(0, -1));
1135
1660
  }
1136
1661
  function matchesConditions(subject, conditions) {
1137
- return Object.entries(conditions).every(([path, expected]) => {
1138
- if (path === "$and" && Array.isArray(expected)) {
1662
+ return Object.entries(conditions).every(([path2, expected]) => {
1663
+ if (path2 === "$and" && Array.isArray(expected)) {
1139
1664
  return expected.every(
1140
1665
  (condition) => matchesConditions(subject, condition)
1141
1666
  );
1142
1667
  }
1143
- if (path === "$or" && Array.isArray(expected)) {
1668
+ if (path2 === "$or" && Array.isArray(expected)) {
1144
1669
  return expected.some(
1145
1670
  (condition) => matchesConditions(subject, condition)
1146
1671
  );
1147
1672
  }
1148
- return matchesValue(readPath(subject, path), expected);
1673
+ return matchesValue(readPath(subject, path2), expected);
1149
1674
  });
1150
1675
  }
1151
1676
  function matchesValue(actual, expected) {
@@ -1177,8 +1702,8 @@ function matchesValue(actual, expected) {
1177
1702
  }
1178
1703
  });
1179
1704
  }
1180
- function readPath(value, path) {
1181
- return path.split(".").reduce((current, part) => isRecord2(current) ? current[part] : void 0, value);
1705
+ function readPath(value, path2) {
1706
+ return path2.split(".").reduce((current, part) => isRecord2(current) ? current[part] : void 0, value);
1182
1707
  }
1183
1708
  function defaultSubjectType(subject) {
1184
1709
  const explicit = subject.__type ?? subject.type ?? subject.kind;
@@ -1334,6 +1859,7 @@ function unauthenticated() {
1334
1859
  LoggingHttpAdapter,
1335
1860
  TenantScopedClient,
1336
1861
  TokenVerificationError,
1862
+ UserScopedClient,
1337
1863
  authorizeRequest,
1338
1864
  authorizeToken,
1339
1865
  createAuthorizationErrorResponse,
@@ -1342,5 +1868,6 @@ function unauthenticated() {
1342
1868
  createClient,
1343
1869
  createExpressAuthorizationMiddleware,
1344
1870
  hasPermission,
1871
+ hasPermissionGroup,
1345
1872
  normalizeClientIp
1346
1873
  });