@authyon/server 0.2.0-beta.1 → 0.2.0-beta.11

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,11 @@ __export(index_exports, {
30
37
  FetchHttpAdapter: () => FetchHttpAdapter,
31
38
  JwksTokenVerifier: () => JwksTokenVerifier,
32
39
  LoggingHttpAdapter: () => LoggingHttpAdapter,
40
+ PlatformScopedClient: () => PlatformScopedClient,
33
41
  TenantScopedClient: () => TenantScopedClient,
34
42
  TokenVerificationError: () => TokenVerificationError,
43
+ UserScopedClient: () => UserScopedClient,
44
+ WorkspaceInvitesClient: () => WorkspaceInvitesClient,
35
45
  authorizeRequest: () => authorizeRequest,
36
46
  authorizeToken: () => authorizeToken,
37
47
  createAuthorizationErrorResponse: () => createAuthorizationErrorResponse,
@@ -39,39 +49,288 @@ __export(index_exports, {
39
49
  createAuthyonRules: () => createAuthyonRules,
40
50
  createClient: () => createClient,
41
51
  createExpressAuthorizationMiddleware: () => createExpressAuthorizationMiddleware,
52
+ hasPermission: () => hasPermission,
53
+ hasPermissionGroup: () => hasPermissionGroup,
42
54
  normalizeClientIp: () => normalizeClientIp
43
55
  });
44
56
  module.exports = __toCommonJS(index_exports);
45
57
 
58
+ // src/client/credentialManagement.ts
59
+ function segment(value) {
60
+ if (typeof value !== "string" || !value.trim() || value === "." || value === ".." || [...value].some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127)) {
61
+ throw new TypeError("Authyon: invalid route identifier.");
62
+ }
63
+ return encodeURIComponent(value);
64
+ }
65
+ function permissionsBody(input) {
66
+ if (input.permissions !== void 0 && input.scopes !== void 0) {
67
+ throw new TypeError("Authyon: supply permissions or scopes, not both.");
68
+ }
69
+ const values = input.permissions ?? input.scopes;
70
+ if (!Array.isArray(values) || values.length === 0) {
71
+ throw new TypeError("Authyon: explicit credential permissions are required.");
72
+ }
73
+ const permissions = values.map((value) => {
74
+ if (typeof value !== "string") throw new TypeError("Authyon: invalid permission.");
75
+ const normalized = value.trim().toLowerCase();
76
+ if (!/^(?:\*|[a-z][a-z0-9_-]*):(?:\*|[a-z][a-z0-9_-]*):(?:\*|[a-z][a-z0-9_-]*)$/u.test(normalized)) {
77
+ throw new TypeError("Authyon: permissions must have namespace:resource:action format.");
78
+ }
79
+ return normalized;
80
+ });
81
+ return { permissions: [...new Set(permissions)] };
82
+ }
83
+ function lifetimeBody(input) {
84
+ if (input.lifetimeDays === void 0 || input.lifetimeDays === null) return {};
85
+ if (!Number.isInteger(input.lifetimeDays) || input.lifetimeDays < 1 || input.lifetimeDays > 3650) {
86
+ throw new RangeError("Authyon: lifetimeDays must be between 1 and 3650.");
87
+ }
88
+ return { lifetimeDays: input.lifetimeDays };
89
+ }
90
+ function credentialQuery(options = {}) {
91
+ const { skip, take, search } = options;
92
+ if (skip !== void 0 && (!Number.isInteger(skip) || skip < 0 || skip > 2147483647))
93
+ throw new RangeError("Authyon: invalid skip.");
94
+ if (take !== void 0 && (!Number.isInteger(take) || take < 1 || take > 100))
95
+ throw new RangeError("Authyon: take must be between 1 and 100.");
96
+ if (search !== void 0 && (typeof search !== "string" || search.trim().length > 200))
97
+ throw new RangeError("Authyon: search must contain at most 200 characters.");
98
+ return { search: search?.trim() || void 0, skip, take };
99
+ }
100
+ function credentialSummary(value) {
101
+ return {
102
+ id: value.id,
103
+ clientId: value.clientId,
104
+ description: value.description,
105
+ createdAt: value.createdAt,
106
+ lastUsedAt: value.lastUsedAt,
107
+ revokedAt: value.revokedAt,
108
+ expiresAt: value.expiresAt ?? null,
109
+ isActive: value.isActive,
110
+ permissions: [...value.permissions]
111
+ };
112
+ }
113
+ function credentialDetail(value) {
114
+ return {
115
+ ...credentialSummary(value),
116
+ createdBy: value.createdBy == null ? null : {
117
+ id: value.createdBy.id,
118
+ type: value.createdBy.type,
119
+ displayName: value.createdBy.displayName
120
+ },
121
+ updatedAt: value.updatedAt,
122
+ secretRotatedAt: value.secretRotatedAt,
123
+ expiresAt: value.expiresAt ?? null,
124
+ ageSeconds: value.ageSeconds,
125
+ lifetimeSeconds: value.lifetimeSeconds,
126
+ accessTokenLifetimeSeconds: value.accessTokenLifetimeSeconds
127
+ };
128
+ }
129
+ async function credentialPage(request, path, options = {}) {
130
+ const query = credentialQuery(options);
131
+ const page = await request(path, { query });
132
+ if (!page || !Array.isArray(page.data))
133
+ throw new TypeError("Authyon: expected a paginated credential response.");
134
+ return {
135
+ data: page.data.map(credentialSummary),
136
+ ...page.perPage === void 0 ? {} : { perPage: page.perPage },
137
+ pageSize: page.pageSize,
138
+ total: page.total,
139
+ pages: page.pages,
140
+ hasNext: page.hasNext,
141
+ hasPrev: page.hasPrev
142
+ };
143
+ }
144
+ async function allCredentials(request, path) {
145
+ const result = [];
146
+ const seen = /* @__PURE__ */ new Set();
147
+ let skip = 0;
148
+ for (let iteration = 0; iteration < 1e3; iteration++) {
149
+ const page = await request(
150
+ path,
151
+ iteration === 0 ? {} : { query: { skip, take: 100 } }
152
+ );
153
+ if (Array.isArray(page)) {
154
+ if (iteration !== 0)
155
+ throw new Error("Authyon: credential pagination changed during listing.");
156
+ return page.map(credentialSummary);
157
+ }
158
+ if (!page || !Array.isArray(page.data))
159
+ throw new TypeError("Authyon: invalid credential page.");
160
+ for (const row of page.data) {
161
+ if (seen.has(row.id))
162
+ throw new Error("Authyon: credential pages overlapped; retry the listing.");
163
+ seen.add(row.id);
164
+ result.push(credentialSummary(row));
165
+ }
166
+ if (!page.hasNext) return result;
167
+ if (page.data.length === 0) throw new Error("Authyon: credential pagination made no progress.");
168
+ skip += page.data.length;
169
+ }
170
+ throw new Error("Authyon: use listPage for more than 1000 credential pages.");
171
+ }
172
+
173
+ // src/client/scopedManagementClient.ts
174
+ function scopedRequest(request, source) {
175
+ return async (path, options = {}) => {
176
+ const token = typeof source === "function" ? await source() : source;
177
+ if (typeof token !== "string" || !token.trim() || /\s/u.test(token))
178
+ throw new TypeError("Authyon: a valid access token is required.");
179
+ return request(path, {
180
+ ...options,
181
+ headers: { ...options.headers, Authorization: `Bearer ${token}` }
182
+ });
183
+ };
184
+ }
185
+ function environmentPath(scope) {
186
+ return `/platform/workspaces/${segment(scope.workspaceId)}/environments/${segment(scope.environmentId)}`;
187
+ }
188
+ function tenantPath(scope) {
189
+ return `${environmentPath(scope)}/tenants/${segment(scope.tenantId)}`;
190
+ }
191
+ function credentialPath(scope) {
192
+ return `${scope.tenantId === void 0 ? environmentPath(scope) : tenantPath(scope)}/credentials`;
193
+ }
194
+ function workspacePath(id) {
195
+ return `/platform/workspaces/${segment(id)}`;
196
+ }
197
+ function workspaceRole(role) {
198
+ if (role !== "admin" && role !== "auditor")
199
+ throw new TypeError("Authyon: workspace role must be admin or auditor.");
200
+ return role;
201
+ }
202
+ var _request;
203
+ var PlatformScopedClient = class {
204
+ /** @hidden */
205
+ constructor(request, token) {
206
+ __privateAdd(this, _request);
207
+ this.credentials = {
208
+ list: (scope, options = {}) => credentialPage(__privateGet(this, _request), credentialPath(scope), options),
209
+ get: async (scope, credentialId) => credentialDetail(await __privateGet(this, _request).call(this, `${credentialPath(scope)}/${segment(credentialId)}`)),
210
+ create: (scope, input) => __privateGet(this, _request).call(this, credentialPath(scope), {
211
+ method: "POST",
212
+ body: { description: input.description, ...lifetimeBody(input), ...permissionsBody(input) }
213
+ }),
214
+ updatePermissions: (scope, credentialId, input) => __privateGet(this, _request).call(this, `${credentialPath(scope)}/${segment(credentialId)}/permissions`, {
215
+ method: "PUT",
216
+ body: permissionsBody(input)
217
+ }),
218
+ updateScopes: (scope, credentialId, scopes) => this.credentials.updatePermissions(scope, credentialId, { scopes }),
219
+ /** Returns a new secret once. API may require an explicit step-up first. */
220
+ rotate: (scope, credentialId) => __privateGet(this, _request).call(this, `${credentialPath(scope)}/${segment(credentialId)}/rotate`, { method: "POST" }),
221
+ revoke: (scope, credentialId) => __privateGet(this, _request).call(this, `${credentialPath(scope)}/${segment(credentialId)}`, { method: "DELETE" })
222
+ };
223
+ this.workspaces = {
224
+ team: (workspaceId) => __privateGet(this, _request).call(this, `${workspacePath(workspaceId)}/team`),
225
+ members: {
226
+ invite: (workspaceId, input) => __privateGet(this, _request).call(this, `${workspacePath(workspaceId)}/invites`, {
227
+ method: "POST",
228
+ body: { email: input.email, role: workspaceRole(input.role) }
229
+ }),
230
+ /** memberId comes from team.members; it is not the platform userId. */
231
+ remove: (workspaceId, memberId) => __privateGet(this, _request).call(this, `${workspacePath(workspaceId)}/members/${segment(memberId)}`, {
232
+ method: "DELETE"
233
+ }),
234
+ changeRole: (workspaceId, memberId, role) => __privateGet(this, _request).call(this, `${workspacePath(workspaceId)}/members/${segment(memberId)}`, {
235
+ method: "PATCH",
236
+ body: { role: workspaceRole(role) }
237
+ })
238
+ },
239
+ invites: {
240
+ create: (workspaceId, input) => this.workspaces.members.invite(workspaceId, input),
241
+ revoke: (workspaceId, inviteId) => __privateGet(this, _request).call(this, `${workspacePath(workspaceId)}/invites/${segment(inviteId)}`, {
242
+ method: "DELETE"
243
+ })
244
+ }
245
+ };
246
+ this.tenants = {
247
+ members: {
248
+ list: (scope, options = {}) => __privateGet(this, _request).call(this, `${tenantPath(scope)}/members`, { query: options }),
249
+ add: (scope, userId, roles) => __privateGet(this, _request).call(this, `${tenantPath(scope)}/members`, { method: "POST", body: { userId, roles } }),
250
+ remove: (scope, userId) => __privateGet(this, _request).call(this, `${tenantPath(scope)}/members/${segment(userId)}`, { method: "DELETE" })
251
+ }
252
+ };
253
+ this.auth = {
254
+ /** Explicitly confirms the current platform session; passwords are not retained. */
255
+ stepUp: (currentPassword) => __privateGet(this, _request).call(this, "/platform/auth/step-up", { method: "POST", body: { currentPassword } })
256
+ };
257
+ __privateSet(this, _request, scopedRequest(request, token));
258
+ }
259
+ };
260
+ _request = new WeakMap();
261
+ var _request2;
262
+ var UserScopedClient = class {
263
+ /** @hidden */
264
+ constructor(request, token) {
265
+ __privateAdd(this, _request2);
266
+ this.tenants = {
267
+ members: {
268
+ list: (tenantId, options = {}) => __privateGet(this, _request2).call(this, `/auth/tenants/${segment(tenantId)}/members`, { query: options }),
269
+ /** Adds an existing environment user by email; does not send an invitation email. */
270
+ invite: (tenantId, input) => __privateGet(this, _request2).call(this, `/auth/tenants/${segment(tenantId)}/members`, {
271
+ method: "POST",
272
+ body: { email: input.email, roles: input.roles }
273
+ }),
274
+ remove: (tenantId, userId) => __privateGet(this, _request2).call(this, `/auth/tenants/${segment(tenantId)}/members/${segment(userId)}`, {
275
+ method: "DELETE"
276
+ })
277
+ }
278
+ };
279
+ __privateSet(this, _request2, scopedRequest(request, token));
280
+ }
281
+ };
282
+ _request2 = new WeakMap();
283
+ var _request3;
284
+ var WorkspaceInvitesClient = class {
285
+ /** @hidden */
286
+ constructor(request) {
287
+ __privateAdd(this, _request3);
288
+ __privateSet(this, _request3, request);
289
+ }
290
+ preview(token) {
291
+ return __privateGet(this, _request3).call(this, `/platform/workspace-invites/${segment(token)}`);
292
+ }
293
+ accept(token, input = {}) {
294
+ return __privateGet(this, _request3).call(this, `/platform/workspace-invites/${segment(token)}/accept`, {
295
+ method: "POST",
296
+ body: { name: input.name, password: input.password }
297
+ });
298
+ }
299
+ };
300
+ _request3 = new WeakMap();
301
+
46
302
  // ../../internal/core/config/defaults.ts
47
303
  var DEFAULT_BASE_URL = "https://api.authyon.com";
48
304
  var DEFAULT_EXPIRY_SKEW_MS = 3e4;
49
305
 
50
306
  // ../../internal/core/auth/expiringTokenProvider.ts
307
+ var _token, _inFlight;
51
308
  var ExpiringTokenProvider = class {
52
309
  constructor(acquire, expirySkewMs = DEFAULT_EXPIRY_SKEW_MS) {
53
310
  this.acquire = acquire;
54
311
  this.expirySkewMs = expirySkewMs;
55
- this.token = null;
56
- this.inFlight = null;
312
+ __privateAdd(this, _token, null);
313
+ __privateAdd(this, _inFlight, null);
57
314
  }
58
315
  async getAccessToken() {
59
- if (this.token && Date.now() < this.token.expiresAt - this.expirySkewMs) {
60
- return this.token.accessToken;
316
+ if (__privateGet(this, _token) && Date.now() < __privateGet(this, _token).expiresAt - this.expirySkewMs) {
317
+ return __privateGet(this, _token).accessToken;
61
318
  }
62
- if (this.inFlight) return this.inFlight;
63
- this.inFlight = this.acquire().then((token) => {
64
- this.token = token;
319
+ if (__privateGet(this, _inFlight)) return __privateGet(this, _inFlight);
320
+ __privateSet(this, _inFlight, this.acquire().then((token) => {
321
+ __privateSet(this, _token, token);
65
322
  return token.accessToken;
66
323
  }).finally(() => {
67
- this.inFlight = null;
68
- });
69
- return this.inFlight;
324
+ __privateSet(this, _inFlight, null);
325
+ }));
326
+ return __privateGet(this, _inFlight);
70
327
  }
71
328
  clear() {
72
- this.token = null;
329
+ __privateSet(this, _token, null);
73
330
  }
74
331
  };
332
+ _token = new WeakMap();
333
+ _inFlight = new WeakMap();
75
334
 
76
335
  // ../../internal/core/errors/authyonError.ts
77
336
  var ErrorCodes = {
@@ -202,7 +461,8 @@ var FetchHttpAdapter = class {
202
461
  method: request.method,
203
462
  headers: request.headers,
204
463
  body: request.body,
205
- signal: request.signal
464
+ signal: request.signal,
465
+ redirect: "error"
206
466
  });
207
467
  }
208
468
  };
@@ -254,6 +514,10 @@ function defaultHttpLogger(event) {
254
514
  }
255
515
  function sanitizeUrl(value) {
256
516
  const url = new URL(value);
517
+ url.username = "";
518
+ url.password = "";
519
+ url.hash = "";
520
+ url.pathname = url.pathname.replace(/(\/platform\/workspace-invites\/)[^/]+/u, "$1REDACTED");
257
521
  const queryKeys = /* @__PURE__ */ new Set();
258
522
  url.searchParams.forEach((_value, key) => queryKeys.add(key));
259
523
  url.search = queryKeys.size > 0 ? [...queryKeys].map((key) => `${encodeURIComponent(key)}=REDACTED`).join("&") : "";
@@ -535,8 +799,10 @@ function isRecord(value) {
535
799
  }
536
800
 
537
801
  // src/client/authyonServerClient.ts
802
+ var _clientSecret;
538
803
  var AuthyonServerClient = class {
539
804
  constructor(options = {}) {
805
+ __privateAdd(this, _clientSecret);
540
806
  // ── Discovery ──────────────────────────────────────────────────────────────
541
807
  this.discovery = {
542
808
  /** GET /.well-known/jwks.json — the environment's public signing keys. */
@@ -570,8 +836,19 @@ var AuthyonServerClient = class {
570
836
  client_id: credentials.clientId,
571
837
  client_secret: credentials.clientSecret
572
838
  }
839
+ }),
840
+ /** POST /tenant/auth/validate — validates an already-minted tenant-client bearer token. */
841
+ validate: (accessToken, context) => this.request("/tenant/auth/validate", {
842
+ method: "POST",
843
+ headers: {
844
+ ...clientIpHeaders(context),
845
+ Authorization: `Bearer ${accessToken}`
846
+ }
573
847
  })
574
848
  };
849
+ this.workspaceInvites = new WorkspaceInvitesClient(
850
+ (path, options) => this.http.request(path, options)
851
+ );
575
852
  // ── Environment management (users, tenants, roles, permissions, audit) ───
576
853
  this.environment = {
577
854
  users: {
@@ -663,8 +940,53 @@ var AuthyonServerClient = class {
663
940
  )
664
941
  },
665
942
  tenants: {
666
- /** GET /env/tenants — list tenants in the environment. */
667
- list: () => this.request("/env/tenants", { envBearer: true }),
943
+ credentials: {
944
+ /** GET /env/tenants/{tenantId}/credentials. Returns metadata only, including revoked credentials. */
945
+ list: (tenantId) => allCredentials(
946
+ (path, options) => this.request(path, { ...options, envBearer: true }),
947
+ `/env/tenants/${segment(tenantId)}/credentials`
948
+ ),
949
+ /** Paginated searchable listing. Prefer this over loading every credential. */
950
+ listPage: (tenantId, options = {}) => credentialPage(
951
+ (path, requestOptions) => this.request(path, { ...requestOptions, envBearer: true }),
952
+ `/env/tenants/${segment(tenantId)}/credentials`,
953
+ options
954
+ ),
955
+ get: async (tenantId, credentialId) => credentialDetail(
956
+ await this.request(
957
+ `/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}`,
958
+ { envBearer: true }
959
+ )
960
+ ),
961
+ /** Explicit scopes/permissions; only issuance returns the client secret. */
962
+ create: (tenantId, input) => this.request(`/env/tenants/${segment(tenantId)}/credentials`, {
963
+ method: "POST",
964
+ envBearer: true,
965
+ body: {
966
+ description: input.description,
967
+ ...lifetimeBody(input),
968
+ ...permissionsBody(input)
969
+ }
970
+ }),
971
+ /** Returns a new secret once while keeping the same client id. */
972
+ rotate: (tenantId, credentialId) => this.request(
973
+ `/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}/rotate`,
974
+ { method: "POST", envBearer: true }
975
+ ),
976
+ updatePermissions: (tenantId, credentialId, input) => this.request(
977
+ `/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}/permissions`,
978
+ { method: "PUT", envBearer: true, body: permissionsBody(input) }
979
+ ),
980
+ updateScopes: (tenantId, credentialId, scopes) => this.environment.tenants.credentials.updatePermissions(tenantId, credentialId, {
981
+ scopes
982
+ }),
983
+ revoke: (tenantId, credentialId) => this.request(`/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}`, {
984
+ method: "DELETE",
985
+ envBearer: true
986
+ })
987
+ },
988
+ /** GET /env/tenants — paginated, searchable list of tenants in the environment. */
989
+ list: (params = {}) => this.request("/env/tenants", { envBearer: true, query: params }),
668
990
  /** POST /env/tenants — create a tenant in the environment. */
669
991
  create: (input) => this.request("/env/tenants", { method: "POST", envBearer: true, body: input }),
670
992
  /** GET /env/tenants/{tenantId} — fetch a tenant by id. */
@@ -675,7 +997,18 @@ var AuthyonServerClient = class {
675
997
  envBearer: true,
676
998
  body: input
677
999
  }),
678
- /** DELETE /env/tenants/{tenantId} — delete a tenant. */
1000
+ /** PUT /env/tenants/{tenantId}/metadata - replace public and private metadata. */
1001
+ updateMetadata: (tenantId, input) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/metadata`, {
1002
+ method: "PUT",
1003
+ envBearer: true,
1004
+ body: input
1005
+ }),
1006
+ /** POST /env/tenants/{tenantId}/disable — disable without removing tenant data. */
1007
+ disable: (tenantId) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/disable`, {
1008
+ method: "POST",
1009
+ envBearer: true
1010
+ }),
1011
+ /** DELETE /env/tenants/{tenantId} — logically delete (disable) a tenant. */
679
1012
  delete: (tenantId) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}`, {
680
1013
  method: "DELETE",
681
1014
  envBearer: true
@@ -809,7 +1142,8 @@ var AuthyonServerClient = class {
809
1142
  };
810
1143
  this.envKey = options.envKey;
811
1144
  this.clientId = options.clientId;
812
- this.clientSecret = options.clientSecret;
1145
+ __privateSet(this, _clientSecret, options.clientSecret);
1146
+ this.clientIp = options.clientIp;
813
1147
  this.transport = createSharedTransport({
814
1148
  baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
815
1149
  allowInsecureHttp: options.allowInsecureHttp,
@@ -820,14 +1154,14 @@ var AuthyonServerClient = class {
820
1154
  });
821
1155
  this.http = new JsonHttpClient(this.transport);
822
1156
  this.environmentTokenProvider = new ExpiringTokenProvider(async () => {
823
- if (!this.clientId || !this.clientSecret) {
1157
+ if (!this.clientId || !__privateGet(this, _clientSecret)) {
824
1158
  throw new Error(
825
1159
  "Authyon: `clientId`/`clientSecret` are required for environment management calls (mint a pair in the console, under the environment's OAuth clients)."
826
1160
  );
827
1161
  }
828
1162
  const token = await this.environmentAuth.token({
829
1163
  clientId: this.clientId,
830
- clientSecret: this.clientSecret
1164
+ clientSecret: __privateGet(this, _clientSecret)
831
1165
  });
832
1166
  return {
833
1167
  accessToken: token.access_token,
@@ -836,7 +1170,10 @@ var AuthyonServerClient = class {
836
1170
  });
837
1171
  }
838
1172
  async request(path, options = {}) {
839
- const headers = { ...options.headers };
1173
+ const headers = {
1174
+ ...clientIpHeaders({ clientIp: this.clientIp }),
1175
+ ...options.headers
1176
+ };
840
1177
  if (options.envBearer) {
841
1178
  headers.Authorization = `Bearer ${await this.getEnvironmentAccessToken()}`;
842
1179
  }
@@ -896,6 +1233,14 @@ var AuthyonServerClient = class {
896
1233
  tenant(credentials) {
897
1234
  return new TenantScopedClient(this, credentials);
898
1235
  }
1236
+ /** Uses only the supplied platform-user access token. */
1237
+ platform(token) {
1238
+ return new PlatformScopedClient((path, options) => this.http.request(path, options), token);
1239
+ }
1240
+ /** Uses an end-user token; the configured environment key selects the environment. */
1241
+ user(token) {
1242
+ return new UserScopedClient((path, options) => this.request(path, options), token);
1243
+ }
899
1244
  /** @internal used by {@link TenantScopedClient} to share the base URL/fetch/error handling. */
900
1245
  _requestAsTenant(accessToken, path, options = {}) {
901
1246
  return this.request(path, {
@@ -905,6 +1250,7 @@ var AuthyonServerClient = class {
905
1250
  });
906
1251
  }
907
1252
  };
1253
+ _clientSecret = new WeakMap();
908
1254
  var TenantScopedClient = class {
909
1255
  constructor(server, credentials) {
910
1256
  this.members = {
@@ -977,46 +1323,56 @@ var TenantScopedClient = class {
977
1323
  async request(path, options = {}) {
978
1324
  return this.server._requestAsTenant(await this.getAccessToken(), path, options);
979
1325
  }
1326
+ /** POST /tenant/auth/validate — validates this tenant client's current bearer token. */
1327
+ async validate(context) {
1328
+ return this.server.tenantAuth.validate(await this.getAccessToken(), context);
1329
+ }
980
1330
  };
981
1331
  function createClient(options = {}) {
982
1332
  return new AuthyonServerClient(options);
983
1333
  }
984
1334
 
985
1335
  // src/client/authyonServerClientBuilder.ts
1336
+ var _options;
986
1337
  var AuthyonServerClientBuilder = class {
987
1338
  constructor() {
988
- this.options = {};
1339
+ __privateAdd(this, _options, {});
989
1340
  }
990
1341
  withEnvironmentKey(envKey) {
991
- this.options.envKey = envKey;
1342
+ __privateGet(this, _options).envKey = envKey;
992
1343
  return this;
993
1344
  }
994
1345
  withEnvironmentCredentials(clientId, clientSecret) {
995
- this.options.clientId = clientId;
996
- this.options.clientSecret = clientSecret;
1346
+ __privateGet(this, _options).clientId = clientId;
1347
+ __privateGet(this, _options).clientSecret = clientSecret;
1348
+ return this;
1349
+ }
1350
+ withClientIp(clientIp) {
1351
+ __privateGet(this, _options).clientIp = clientIp;
997
1352
  return this;
998
1353
  }
999
1354
  withBaseUrl(baseUrl, allowInsecureHttp = false) {
1000
- this.options.baseUrl = baseUrl;
1001
- this.options.allowInsecureHttp = allowInsecureHttp;
1355
+ __privateGet(this, _options).baseUrl = baseUrl;
1356
+ __privateGet(this, _options).allowInsecureHttp = allowInsecureHttp;
1002
1357
  return this;
1003
1358
  }
1004
1359
  withTimeout(timeoutMs) {
1005
- this.options.timeoutMs = timeoutMs;
1360
+ __privateGet(this, _options).timeoutMs = timeoutMs;
1006
1361
  return this;
1007
1362
  }
1008
1363
  withHttpAdapter(httpAdapter) {
1009
- this.options.httpAdapter = httpAdapter;
1364
+ __privateGet(this, _options).httpAdapter = httpAdapter;
1010
1365
  return this;
1011
1366
  }
1012
1367
  withHttpLogger(httpLogger) {
1013
- this.options.httpLogger = httpLogger;
1368
+ __privateGet(this, _options).httpLogger = httpLogger;
1014
1369
  return this;
1015
1370
  }
1016
1371
  build() {
1017
- return new AuthyonServerClient({ ...this.options });
1372
+ return new AuthyonServerClient({ ...__privateGet(this, _options) });
1018
1373
  }
1019
1374
  };
1375
+ _options = new WeakMap();
1020
1376
 
1021
1377
  // ../../internal/core/authorization/ability.ts
1022
1378
  var AuthyonAbility = class {
@@ -1080,6 +1436,22 @@ var AuthyonAbilityBuilder = class {
1080
1436
  function createAuthyonAbility(source = {}, options = {}) {
1081
1437
  return new AuthyonAbility(createAuthyonRules(source, options), options.detectSubjectType);
1082
1438
  }
1439
+ function hasPermission(source, requiredPermission) {
1440
+ const requirement = permissionToRule(requiredPermission);
1441
+ if (!requirement || typeof requirement.action !== "string" || typeof requirement.subject !== "string") {
1442
+ return false;
1443
+ }
1444
+ const permissionSource = isPermissionList(source) ? { permissions: source } : source;
1445
+ return createAuthyonAbility(permissionSource).can(requirement.action, requirement.subject);
1446
+ }
1447
+ function hasPermissionGroup(source, group) {
1448
+ const hasAll = (group.allOf ?? []).every((permission) => hasPermission(source, permission));
1449
+ const hasAny = !group.anyOf?.length || group.anyOf.some((permission) => hasPermission(source, permission));
1450
+ return hasAll && hasAny;
1451
+ }
1452
+ function isPermissionList(source) {
1453
+ return Array.isArray(source);
1454
+ }
1083
1455
  function createAuthyonRules(source = {}, options = {}) {
1084
1456
  const permissions = /* @__PURE__ */ new Set([
1085
1457
  ...source.permissions ?? [],
@@ -1109,9 +1481,14 @@ function permissionToRule(permission) {
1109
1481
  }
1110
1482
  function matchesToken(value, expected, wildcard) {
1111
1483
  return (Array.isArray(value) ? value : [value]).some(
1112
- (candidate) => candidate === expected || candidate === wildcard || candidate === "*"
1484
+ (candidate) => candidate === expected || candidate === wildcard || candidate === "*" || matchesSegments(candidate, expected)
1113
1485
  );
1114
1486
  }
1487
+ function matchesSegments(pattern, value) {
1488
+ const patternSegments = pattern.split(":");
1489
+ const valueSegments = value.split(":");
1490
+ return patternSegments.length === valueSegments.length && patternSegments.every((segment2, index) => segment2 === "*" || segment2 === valueSegments[index]);
1491
+ }
1115
1492
  function matchesField(pattern, field) {
1116
1493
  if (pattern === "*" || pattern === field) return true;
1117
1494
  return pattern.endsWith(".*") && field.startsWith(pattern.slice(0, -1));
@@ -1315,8 +1692,11 @@ function unauthenticated() {
1315
1692
  FetchHttpAdapter,
1316
1693
  JwksTokenVerifier,
1317
1694
  LoggingHttpAdapter,
1695
+ PlatformScopedClient,
1318
1696
  TenantScopedClient,
1319
1697
  TokenVerificationError,
1698
+ UserScopedClient,
1699
+ WorkspaceInvitesClient,
1320
1700
  authorizeRequest,
1321
1701
  authorizeToken,
1322
1702
  createAuthorizationErrorResponse,
@@ -1324,5 +1704,7 @@ function unauthenticated() {
1324
1704
  createAuthyonRules,
1325
1705
  createClient,
1326
1706
  createExpressAuthorizationMiddleware,
1707
+ hasPermission,
1708
+ hasPermissionGroup,
1327
1709
  normalizeClientIp
1328
1710
  });