@authyon/server 0.2.0-beta.0 → 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.js CHANGED
@@ -1,33 +1,314 @@
1
- // src/errors.ts
2
- var AuthyonError = class extends Error {
3
- constructor(status, body) {
4
- super(body.detail ?? body.title ?? `Authyon request failed with status ${status}`);
5
- this.name = "AuthyonError";
6
- this.status = status;
7
- this.code = body.code ?? "unknown";
8
- this.title = body.title ?? "Error";
9
- this.detail = body.detail;
10
- }
11
- is(code) {
12
- return this.code === code;
1
+ import {
2
+ AuthyonError,
3
+ DEFAULT_BASE_URL,
4
+ DEFAULT_EXPIRY_SKEW_MS,
5
+ ErrorCodes,
6
+ FetchHttpAdapter,
7
+ JsonHttpClient,
8
+ LoggingHttpAdapter,
9
+ createSharedTransport
10
+ } from "./chunk-56H66KLG.js";
11
+ import {
12
+ AuthorizationError,
13
+ AuthyonAbility,
14
+ AuthyonAbilityBuilder,
15
+ JwksTokenVerifier,
16
+ TokenVerificationError,
17
+ authorizeRequest,
18
+ authorizeToken,
19
+ clientIpHeaders,
20
+ createAuthorizationErrorResponse,
21
+ createAuthyonAbility,
22
+ createAuthyonRules,
23
+ createExpressAuthorizationMiddleware,
24
+ hasPermission,
25
+ hasPermissionGroup,
26
+ normalizeClientIp
27
+ } from "./chunk-NKWQMYB3.js";
28
+ import {
29
+ __privateAdd,
30
+ __privateGet,
31
+ __privateSet
32
+ } from "./chunk-77J4NURK.js";
33
+
34
+ // src/client/credentialManagement.ts
35
+ function segment(value) {
36
+ if (typeof value !== "string" || !value.trim() || value === "." || value === ".." || [...value].some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127)) {
37
+ throw new TypeError("Authyon: invalid route identifier.");
13
38
  }
14
- };
39
+ return encodeURIComponent(value);
40
+ }
41
+ function permissionsBody(input) {
42
+ if (input.permissions !== void 0 && input.scopes !== void 0) {
43
+ throw new TypeError("Authyon: supply permissions or scopes, not both.");
44
+ }
45
+ const values = input.permissions ?? input.scopes;
46
+ if (!Array.isArray(values) || values.length === 0) {
47
+ throw new TypeError("Authyon: explicit credential permissions are required.");
48
+ }
49
+ const permissions = values.map((value) => {
50
+ if (typeof value !== "string") throw new TypeError("Authyon: invalid permission.");
51
+ const normalized = value.trim().toLowerCase();
52
+ if (!/^(?:\*|[a-z][a-z0-9_-]*):(?:\*|[a-z][a-z0-9_-]*):(?:\*|[a-z][a-z0-9_-]*)$/u.test(normalized)) {
53
+ throw new TypeError("Authyon: permissions must have namespace:resource:action format.");
54
+ }
55
+ return normalized;
56
+ });
57
+ return { permissions: [...new Set(permissions)] };
58
+ }
59
+ function lifetimeBody(input) {
60
+ if (input.lifetimeDays === void 0 || input.lifetimeDays === null) return {};
61
+ if (!Number.isInteger(input.lifetimeDays) || input.lifetimeDays < 1 || input.lifetimeDays > 3650) {
62
+ throw new RangeError("Authyon: lifetimeDays must be between 1 and 3650.");
63
+ }
64
+ return { lifetimeDays: input.lifetimeDays };
65
+ }
66
+ function credentialQuery(options = {}) {
67
+ const { skip, take, search } = options;
68
+ if (skip !== void 0 && (!Number.isInteger(skip) || skip < 0 || skip > 2147483647))
69
+ throw new RangeError("Authyon: invalid skip.");
70
+ if (take !== void 0 && (!Number.isInteger(take) || take < 1 || take > 100))
71
+ throw new RangeError("Authyon: take must be between 1 and 100.");
72
+ if (search !== void 0 && (typeof search !== "string" || search.trim().length > 200))
73
+ throw new RangeError("Authyon: search must contain at most 200 characters.");
74
+ return { search: search?.trim() || void 0, skip, take };
75
+ }
76
+ function credentialSummary(value) {
77
+ return {
78
+ id: value.id,
79
+ clientId: value.clientId,
80
+ description: value.description,
81
+ createdAt: value.createdAt,
82
+ lastUsedAt: value.lastUsedAt,
83
+ revokedAt: value.revokedAt,
84
+ expiresAt: value.expiresAt ?? null,
85
+ isActive: value.isActive,
86
+ permissions: [...value.permissions]
87
+ };
88
+ }
89
+ function credentialDetail(value) {
90
+ return {
91
+ ...credentialSummary(value),
92
+ createdBy: value.createdBy == null ? null : {
93
+ id: value.createdBy.id,
94
+ type: value.createdBy.type,
95
+ displayName: value.createdBy.displayName
96
+ },
97
+ updatedAt: value.updatedAt,
98
+ secretRotatedAt: value.secretRotatedAt,
99
+ expiresAt: value.expiresAt ?? null,
100
+ ageSeconds: value.ageSeconds,
101
+ lifetimeSeconds: value.lifetimeSeconds,
102
+ accessTokenLifetimeSeconds: value.accessTokenLifetimeSeconds
103
+ };
104
+ }
105
+ async function credentialPage(request, path, options = {}) {
106
+ const query = credentialQuery(options);
107
+ const page = await request(path, { query });
108
+ if (!page || !Array.isArray(page.data))
109
+ throw new TypeError("Authyon: expected a paginated credential response.");
110
+ return {
111
+ data: page.data.map(credentialSummary),
112
+ ...page.perPage === void 0 ? {} : { perPage: page.perPage },
113
+ pageSize: page.pageSize,
114
+ total: page.total,
115
+ pages: page.pages,
116
+ hasNext: page.hasNext,
117
+ hasPrev: page.hasPrev
118
+ };
119
+ }
120
+ async function allCredentials(request, path) {
121
+ const result = [];
122
+ const seen = /* @__PURE__ */ new Set();
123
+ let skip = 0;
124
+ for (let iteration = 0; iteration < 1e3; iteration++) {
125
+ const page = await request(
126
+ path,
127
+ iteration === 0 ? {} : { query: { skip, take: 100 } }
128
+ );
129
+ if (Array.isArray(page)) {
130
+ if (iteration !== 0)
131
+ throw new Error("Authyon: credential pagination changed during listing.");
132
+ return page.map(credentialSummary);
133
+ }
134
+ if (!page || !Array.isArray(page.data))
135
+ throw new TypeError("Authyon: invalid credential page.");
136
+ for (const row of page.data) {
137
+ if (seen.has(row.id))
138
+ throw new Error("Authyon: credential pages overlapped; retry the listing.");
139
+ seen.add(row.id);
140
+ result.push(credentialSummary(row));
141
+ }
142
+ if (!page.hasNext) return result;
143
+ if (page.data.length === 0) throw new Error("Authyon: credential pagination made no progress.");
144
+ skip += page.data.length;
145
+ }
146
+ throw new Error("Authyon: use listPage for more than 1000 credential pages.");
147
+ }
15
148
 
16
- // src/client.ts
17
- var DEFAULT_BASE_URL = "https://api.authyon.com";
18
- var EXPIRY_SKEW_MS = 3e4;
19
- function toQuery(params = {}) {
20
- const query = new URLSearchParams();
21
- for (const [key, value] of Object.entries(params)) {
22
- if (value !== void 0) query.set(key, String(value));
23
- }
24
- const qs = query.toString();
25
- return qs ? `?${qs}` : "";
149
+ // src/client/scopedManagementClient.ts
150
+ function scopedRequest(request, source) {
151
+ return async (path, options = {}) => {
152
+ const token = typeof source === "function" ? await source() : source;
153
+ if (typeof token !== "string" || !token.trim() || /\s/u.test(token))
154
+ throw new TypeError("Authyon: a valid access token is required.");
155
+ return request(path, {
156
+ ...options,
157
+ headers: { ...options.headers, Authorization: `Bearer ${token}` }
158
+ });
159
+ };
160
+ }
161
+ function environmentPath(scope) {
162
+ return `/platform/workspaces/${segment(scope.workspaceId)}/environments/${segment(scope.environmentId)}`;
163
+ }
164
+ function tenantPath(scope) {
165
+ return `${environmentPath(scope)}/tenants/${segment(scope.tenantId)}`;
166
+ }
167
+ function credentialPath(scope) {
168
+ return `${scope.tenantId === void 0 ? environmentPath(scope) : tenantPath(scope)}/credentials`;
169
+ }
170
+ function workspacePath(id) {
171
+ return `/platform/workspaces/${segment(id)}`;
26
172
  }
173
+ function workspaceRole(role) {
174
+ if (role !== "admin" && role !== "auditor")
175
+ throw new TypeError("Authyon: workspace role must be admin or auditor.");
176
+ return role;
177
+ }
178
+ var _request;
179
+ var PlatformScopedClient = class {
180
+ /** @hidden */
181
+ constructor(request, token) {
182
+ __privateAdd(this, _request);
183
+ this.credentials = {
184
+ list: (scope, options = {}) => credentialPage(__privateGet(this, _request), credentialPath(scope), options),
185
+ get: async (scope, credentialId) => credentialDetail(await __privateGet(this, _request).call(this, `${credentialPath(scope)}/${segment(credentialId)}`)),
186
+ create: (scope, input) => __privateGet(this, _request).call(this, credentialPath(scope), {
187
+ method: "POST",
188
+ body: { description: input.description, ...lifetimeBody(input), ...permissionsBody(input) }
189
+ }),
190
+ updatePermissions: (scope, credentialId, input) => __privateGet(this, _request).call(this, `${credentialPath(scope)}/${segment(credentialId)}/permissions`, {
191
+ method: "PUT",
192
+ body: permissionsBody(input)
193
+ }),
194
+ updateScopes: (scope, credentialId, scopes) => this.credentials.updatePermissions(scope, credentialId, { scopes }),
195
+ /** Returns a new secret once. API may require an explicit step-up first. */
196
+ rotate: (scope, credentialId) => __privateGet(this, _request).call(this, `${credentialPath(scope)}/${segment(credentialId)}/rotate`, { method: "POST" }),
197
+ revoke: (scope, credentialId) => __privateGet(this, _request).call(this, `${credentialPath(scope)}/${segment(credentialId)}`, { method: "DELETE" })
198
+ };
199
+ this.workspaces = {
200
+ team: (workspaceId) => __privateGet(this, _request).call(this, `${workspacePath(workspaceId)}/team`),
201
+ members: {
202
+ invite: (workspaceId, input) => __privateGet(this, _request).call(this, `${workspacePath(workspaceId)}/invites`, {
203
+ method: "POST",
204
+ body: { email: input.email, role: workspaceRole(input.role) }
205
+ }),
206
+ /** memberId comes from team.members; it is not the platform userId. */
207
+ remove: (workspaceId, memberId) => __privateGet(this, _request).call(this, `${workspacePath(workspaceId)}/members/${segment(memberId)}`, {
208
+ method: "DELETE"
209
+ }),
210
+ changeRole: (workspaceId, memberId, role) => __privateGet(this, _request).call(this, `${workspacePath(workspaceId)}/members/${segment(memberId)}`, {
211
+ method: "PATCH",
212
+ body: { role: workspaceRole(role) }
213
+ })
214
+ },
215
+ invites: {
216
+ create: (workspaceId, input) => this.workspaces.members.invite(workspaceId, input),
217
+ revoke: (workspaceId, inviteId) => __privateGet(this, _request).call(this, `${workspacePath(workspaceId)}/invites/${segment(inviteId)}`, {
218
+ method: "DELETE"
219
+ })
220
+ }
221
+ };
222
+ this.tenants = {
223
+ members: {
224
+ list: (scope, options = {}) => __privateGet(this, _request).call(this, `${tenantPath(scope)}/members`, { query: options }),
225
+ add: (scope, userId, roles) => __privateGet(this, _request).call(this, `${tenantPath(scope)}/members`, { method: "POST", body: { userId, roles } }),
226
+ remove: (scope, userId) => __privateGet(this, _request).call(this, `${tenantPath(scope)}/members/${segment(userId)}`, { method: "DELETE" })
227
+ }
228
+ };
229
+ this.auth = {
230
+ /** Explicitly confirms the current platform session; passwords are not retained. */
231
+ stepUp: (currentPassword) => __privateGet(this, _request).call(this, "/platform/auth/step-up", { method: "POST", body: { currentPassword } })
232
+ };
233
+ __privateSet(this, _request, scopedRequest(request, token));
234
+ }
235
+ };
236
+ _request = new WeakMap();
237
+ var _request2;
238
+ var UserScopedClient = class {
239
+ /** @hidden */
240
+ constructor(request, token) {
241
+ __privateAdd(this, _request2);
242
+ this.tenants = {
243
+ members: {
244
+ list: (tenantId, options = {}) => __privateGet(this, _request2).call(this, `/auth/tenants/${segment(tenantId)}/members`, { query: options }),
245
+ /** Adds an existing environment user by email; does not send an invitation email. */
246
+ invite: (tenantId, input) => __privateGet(this, _request2).call(this, `/auth/tenants/${segment(tenantId)}/members`, {
247
+ method: "POST",
248
+ body: { email: input.email, roles: input.roles }
249
+ }),
250
+ remove: (tenantId, userId) => __privateGet(this, _request2).call(this, `/auth/tenants/${segment(tenantId)}/members/${segment(userId)}`, {
251
+ method: "DELETE"
252
+ })
253
+ }
254
+ };
255
+ __privateSet(this, _request2, scopedRequest(request, token));
256
+ }
257
+ };
258
+ _request2 = new WeakMap();
259
+ var _request3;
260
+ var WorkspaceInvitesClient = class {
261
+ /** @hidden */
262
+ constructor(request) {
263
+ __privateAdd(this, _request3);
264
+ __privateSet(this, _request3, request);
265
+ }
266
+ preview(token) {
267
+ return __privateGet(this, _request3).call(this, `/platform/workspace-invites/${segment(token)}`);
268
+ }
269
+ accept(token, input = {}) {
270
+ return __privateGet(this, _request3).call(this, `/platform/workspace-invites/${segment(token)}/accept`, {
271
+ method: "POST",
272
+ body: { name: input.name, password: input.password }
273
+ });
274
+ }
275
+ };
276
+ _request3 = new WeakMap();
277
+
278
+ // ../../internal/core/auth/expiringTokenProvider.ts
279
+ var _token, _inFlight;
280
+ var ExpiringTokenProvider = class {
281
+ constructor(acquire, expirySkewMs = DEFAULT_EXPIRY_SKEW_MS) {
282
+ this.acquire = acquire;
283
+ this.expirySkewMs = expirySkewMs;
284
+ __privateAdd(this, _token, null);
285
+ __privateAdd(this, _inFlight, null);
286
+ }
287
+ async getAccessToken() {
288
+ if (__privateGet(this, _token) && Date.now() < __privateGet(this, _token).expiresAt - this.expirySkewMs) {
289
+ return __privateGet(this, _token).accessToken;
290
+ }
291
+ if (__privateGet(this, _inFlight)) return __privateGet(this, _inFlight);
292
+ __privateSet(this, _inFlight, this.acquire().then((token) => {
293
+ __privateSet(this, _token, token);
294
+ return token.accessToken;
295
+ }).finally(() => {
296
+ __privateSet(this, _inFlight, null);
297
+ }));
298
+ return __privateGet(this, _inFlight);
299
+ }
300
+ clear() {
301
+ __privateSet(this, _token, null);
302
+ }
303
+ };
304
+ _token = new WeakMap();
305
+ _inFlight = new WeakMap();
306
+
307
+ // src/client/authyonServerClient.ts
308
+ var _clientSecret;
27
309
  var AuthyonServerClient = class {
28
310
  constructor(options = {}) {
29
- this.environmentToken = null;
30
- this.environmentTokenInFlight = null;
311
+ __privateAdd(this, _clientSecret);
31
312
  // ── Discovery ──────────────────────────────────────────────────────────────
32
313
  this.discovery = {
33
314
  /** GET /.well-known/jwks.json — the environment's public signing keys. */
@@ -61,8 +342,19 @@ var AuthyonServerClient = class {
61
342
  client_id: credentials.clientId,
62
343
  client_secret: credentials.clientSecret
63
344
  }
345
+ }),
346
+ /** POST /tenant/auth/validate — validates an already-minted tenant-client bearer token. */
347
+ validate: (accessToken, context) => this.request("/tenant/auth/validate", {
348
+ method: "POST",
349
+ headers: {
350
+ ...clientIpHeaders(context),
351
+ Authorization: `Bearer ${accessToken}`
352
+ }
64
353
  })
65
354
  };
355
+ this.workspaceInvites = new WorkspaceInvitesClient(
356
+ (path, options) => this.http.request(path, options)
357
+ );
66
358
  // ── Environment management (users, tenants, roles, permissions, audit) ───
67
359
  this.environment = {
68
360
  users: {
@@ -154,8 +446,53 @@ var AuthyonServerClient = class {
154
446
  )
155
447
  },
156
448
  tenants: {
157
- /** GET /env/tenants — list tenants in the environment. */
158
- list: () => this.request("/env/tenants", { envBearer: true }),
449
+ credentials: {
450
+ /** GET /env/tenants/{tenantId}/credentials. Returns metadata only, including revoked credentials. */
451
+ list: (tenantId) => allCredentials(
452
+ (path, options) => this.request(path, { ...options, envBearer: true }),
453
+ `/env/tenants/${segment(tenantId)}/credentials`
454
+ ),
455
+ /** Paginated searchable listing. Prefer this over loading every credential. */
456
+ listPage: (tenantId, options = {}) => credentialPage(
457
+ (path, requestOptions) => this.request(path, { ...requestOptions, envBearer: true }),
458
+ `/env/tenants/${segment(tenantId)}/credentials`,
459
+ options
460
+ ),
461
+ get: async (tenantId, credentialId) => credentialDetail(
462
+ await this.request(
463
+ `/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}`,
464
+ { envBearer: true }
465
+ )
466
+ ),
467
+ /** Explicit scopes/permissions; only issuance returns the client secret. */
468
+ create: (tenantId, input) => this.request(`/env/tenants/${segment(tenantId)}/credentials`, {
469
+ method: "POST",
470
+ envBearer: true,
471
+ body: {
472
+ description: input.description,
473
+ ...lifetimeBody(input),
474
+ ...permissionsBody(input)
475
+ }
476
+ }),
477
+ /** Returns a new secret once while keeping the same client id. */
478
+ rotate: (tenantId, credentialId) => this.request(
479
+ `/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}/rotate`,
480
+ { method: "POST", envBearer: true }
481
+ ),
482
+ updatePermissions: (tenantId, credentialId, input) => this.request(
483
+ `/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}/permissions`,
484
+ { method: "PUT", envBearer: true, body: permissionsBody(input) }
485
+ ),
486
+ updateScopes: (tenantId, credentialId, scopes) => this.environment.tenants.credentials.updatePermissions(tenantId, credentialId, {
487
+ scopes
488
+ }),
489
+ revoke: (tenantId, credentialId) => this.request(`/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}`, {
490
+ method: "DELETE",
491
+ envBearer: true
492
+ })
493
+ },
494
+ /** GET /env/tenants — paginated, searchable list of tenants in the environment. */
495
+ list: (params = {}) => this.request("/env/tenants", { envBearer: true, query: params }),
159
496
  /** POST /env/tenants — create a tenant in the environment. */
160
497
  create: (input) => this.request("/env/tenants", { method: "POST", envBearer: true, body: input }),
161
498
  /** GET /env/tenants/{tenantId} — fetch a tenant by id. */
@@ -166,7 +503,18 @@ var AuthyonServerClient = class {
166
503
  envBearer: true,
167
504
  body: input
168
505
  }),
169
- /** DELETE /env/tenants/{tenantId} — delete a tenant. */
506
+ /** PUT /env/tenants/{tenantId}/metadata - replace public and private metadata. */
507
+ updateMetadata: (tenantId, input) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/metadata`, {
508
+ method: "PUT",
509
+ envBearer: true,
510
+ body: input
511
+ }),
512
+ /** POST /env/tenants/{tenantId}/disable — disable without removing tenant data. */
513
+ disable: (tenantId) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/disable`, {
514
+ method: "POST",
515
+ envBearer: true
516
+ }),
517
+ /** DELETE /env/tenants/{tenantId} — logically delete (disable) a tenant. */
170
518
  delete: (tenantId) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}`, {
171
519
  method: "DELETE",
172
520
  envBearer: true
@@ -300,54 +648,60 @@ var AuthyonServerClient = class {
300
648
  };
301
649
  this.envKey = options.envKey;
302
650
  this.clientId = options.clientId;
303
- this.clientSecret = options.clientSecret;
304
- this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
305
- this.fetchImpl = options.fetch ?? fetch.bind(globalThis);
651
+ __privateSet(this, _clientSecret, options.clientSecret);
652
+ this.clientIp = options.clientIp;
653
+ this.transport = createSharedTransport({
654
+ baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
655
+ allowInsecureHttp: options.allowInsecureHttp,
656
+ timeoutMs: options.timeoutMs,
657
+ httpAdapter: options.httpAdapter,
658
+ httpLogger: options.httpLogger,
659
+ fetch: options.fetch
660
+ });
661
+ this.http = new JsonHttpClient(this.transport);
662
+ this.environmentTokenProvider = new ExpiringTokenProvider(async () => {
663
+ if (!this.clientId || !__privateGet(this, _clientSecret)) {
664
+ throw new Error(
665
+ "Authyon: `clientId`/`clientSecret` are required for environment management calls (mint a pair in the console, under the environment's OAuth clients)."
666
+ );
667
+ }
668
+ const token = await this.environmentAuth.token({
669
+ clientId: this.clientId,
670
+ clientSecret: __privateGet(this, _clientSecret)
671
+ });
672
+ return {
673
+ accessToken: token.access_token,
674
+ expiresAt: Date.now() + token.expires_in * 1e3
675
+ };
676
+ });
306
677
  }
307
678
  async request(path, options = {}) {
308
- const headers = { ...options.headers };
309
- if (options.body !== void 0) headers["Content-Type"] = "application/json";
679
+ const headers = {
680
+ ...clientIpHeaders({ clientIp: this.clientIp }),
681
+ ...options.headers
682
+ };
310
683
  if (options.envBearer) {
311
684
  headers.Authorization = `Bearer ${await this.getEnvironmentAccessToken()}`;
312
685
  }
313
686
  if (this.envKey) headers["X-Authyon-Environment"] = this.envKey;
314
- const response = await this.fetchImpl(`${this.baseUrl}${path}${toQuery(options.query)}`, {
315
- method: options.method ?? "GET",
316
- headers,
317
- body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
318
- });
319
- if (!response.ok) {
320
- let body = {};
321
- try {
322
- body = await response.json();
323
- } catch {
324
- }
325
- throw new AuthyonError(response.status, body);
326
- }
327
- if (response.status === 204) return void 0;
328
- return await response.json();
687
+ return this.http.request(path, { ...options, headers });
329
688
  }
330
689
  /** Mints (and caches) an environment access token from `clientId`/`clientSecret`. */
331
690
  async getEnvironmentAccessToken() {
332
- if (this.environmentToken && Date.now() < this.environmentToken.expiresAt - EXPIRY_SKEW_MS) {
333
- return this.environmentToken.accessToken;
334
- }
335
- if (this.environmentTokenInFlight) return this.environmentTokenInFlight;
336
- if (!this.clientId || !this.clientSecret) {
337
- throw new Error(
338
- "Authyon: `clientId`/`clientSecret` are required for environment management calls (mint a pair in the console, under the environment's OAuth clients)."
339
- );
691
+ return this.environmentTokenProvider.getAccessToken();
692
+ }
693
+ /** Discovers Authyon's trusted issuer/JWKS and creates a cached local access-token verifier. */
694
+ async createJwksTokenVerifier(options) {
695
+ const { publishableKey, issuer, ...verifierOptions } = options;
696
+ const configuration = await this.discovery.openidConfiguration(publishableKey);
697
+ if (issuer && configuration.issuer !== issuer) {
698
+ throw new Error("Authyon: discovered JWT issuer does not match the configured issuer");
340
699
  }
341
- this.environmentTokenInFlight = this.environmentAuth.token({ clientId: this.clientId, clientSecret: this.clientSecret }).then((token) => {
342
- this.environmentToken = {
343
- accessToken: token.access_token,
344
- expiresAt: Date.now() + token.expires_in * 1e3
345
- };
346
- return token.access_token;
347
- }).finally(() => {
348
- this.environmentTokenInFlight = null;
700
+ return new JwksTokenVerifier({
701
+ ...verifierOptions,
702
+ issuer: issuer ?? configuration.issuer,
703
+ jwksUri: configuration.jwks_uri
349
704
  });
350
- return this.environmentTokenInFlight;
351
705
  }
352
706
  // ── Token verification ────────────────────────────────────────────────────
353
707
  //
@@ -358,12 +712,22 @@ var AuthyonServerClient = class {
358
712
  // endpoint with only the `X-Authyon-Environment` header (no bearer)
359
713
  // returns 401 with `WWW-Authenticate: Bearer`.
360
714
  /** POST /auth/introspect — lightweight token introspection (RFC 7662). */
361
- introspect(token) {
362
- return this.request("/auth/introspect", { method: "POST", envBearer: true, body: { token } });
715
+ introspect(token, context) {
716
+ return this.request("/auth/introspect", {
717
+ method: "POST",
718
+ envBearer: true,
719
+ body: { token },
720
+ headers: clientIpHeaders(context)
721
+ });
363
722
  }
364
723
  /** POST /auth/validate — recommended: cross-checks DB state, catches revocation immediately. */
365
- async validate(token) {
366
- const raw = await this.request("/auth/validate", { method: "POST", envBearer: true, body: { token } });
724
+ async validate(token, context) {
725
+ const raw = await this.request("/auth/validate", {
726
+ method: "POST",
727
+ envBearer: true,
728
+ body: { token },
729
+ headers: clientIpHeaders(context)
730
+ });
367
731
  return { valid: raw.valid, reason: raw.reason ?? null, user: raw.profile };
368
732
  }
369
733
  /**
@@ -375,6 +739,14 @@ var AuthyonServerClient = class {
375
739
  tenant(credentials) {
376
740
  return new TenantScopedClient(this, credentials);
377
741
  }
742
+ /** Uses only the supplied platform-user access token. */
743
+ platform(token) {
744
+ return new PlatformScopedClient((path, options) => this.http.request(path, options), token);
745
+ }
746
+ /** Uses an end-user token; the configured environment key selects the environment. */
747
+ user(token) {
748
+ return new UserScopedClient((path, options) => this.request(path, options), token);
749
+ }
378
750
  /** @internal used by {@link TenantScopedClient} to share the base URL/fetch/error handling. */
379
751
  _requestAsTenant(accessToken, path, options = {}) {
380
752
  return this.request(path, {
@@ -384,10 +756,9 @@ var AuthyonServerClient = class {
384
756
  });
385
757
  }
386
758
  };
759
+ _clientSecret = new WeakMap();
387
760
  var TenantScopedClient = class {
388
761
  constructor(server, credentials) {
389
- this.token = null;
390
- this.tokenInFlight = null;
391
762
  this.members = {
392
763
  /**
393
764
  * GET /tenant/members — members of the token's tenant.
@@ -444,34 +815,94 @@ var TenantScopedClient = class {
444
815
  delete: (roleId) => this.request(`/tenant/roles/${encodeURIComponent(roleId)}`, { method: "DELETE" })
445
816
  };
446
817
  this.server = server;
447
- this.credentials = credentials;
448
- }
449
- async getAccessToken() {
450
- if (this.token && Date.now() < this.token.expiresAt - EXPIRY_SKEW_MS) {
451
- return this.token.accessToken;
452
- }
453
- if (this.tokenInFlight) return this.tokenInFlight;
454
- this.tokenInFlight = this.server.tenantAuth.token(this.credentials).then((token) => {
455
- this.token = {
818
+ this.tokenProvider = new ExpiringTokenProvider(async () => {
819
+ const token = await this.server.tenantAuth.token(credentials);
820
+ return {
456
821
  accessToken: token.access_token,
457
822
  expiresAt: Date.now() + token.expires_in * 1e3
458
823
  };
459
- return token.access_token;
460
- }).finally(() => {
461
- this.tokenInFlight = null;
462
824
  });
463
- return this.tokenInFlight;
825
+ }
826
+ async getAccessToken() {
827
+ return this.tokenProvider.getAccessToken();
464
828
  }
465
829
  async request(path, options = {}) {
466
830
  return this.server._requestAsTenant(await this.getAccessToken(), path, options);
467
831
  }
832
+ /** POST /tenant/auth/validate — validates this tenant client's current bearer token. */
833
+ async validate(context) {
834
+ return this.server.tenantAuth.validate(await this.getAccessToken(), context);
835
+ }
468
836
  };
469
837
  function createClient(options = {}) {
470
838
  return new AuthyonServerClient(options);
471
839
  }
840
+
841
+ // src/client/authyonServerClientBuilder.ts
842
+ var _options;
843
+ var AuthyonServerClientBuilder = class {
844
+ constructor() {
845
+ __privateAdd(this, _options, {});
846
+ }
847
+ withEnvironmentKey(envKey) {
848
+ __privateGet(this, _options).envKey = envKey;
849
+ return this;
850
+ }
851
+ withEnvironmentCredentials(clientId, clientSecret) {
852
+ __privateGet(this, _options).clientId = clientId;
853
+ __privateGet(this, _options).clientSecret = clientSecret;
854
+ return this;
855
+ }
856
+ withClientIp(clientIp) {
857
+ __privateGet(this, _options).clientIp = clientIp;
858
+ return this;
859
+ }
860
+ withBaseUrl(baseUrl, allowInsecureHttp = false) {
861
+ __privateGet(this, _options).baseUrl = baseUrl;
862
+ __privateGet(this, _options).allowInsecureHttp = allowInsecureHttp;
863
+ return this;
864
+ }
865
+ withTimeout(timeoutMs) {
866
+ __privateGet(this, _options).timeoutMs = timeoutMs;
867
+ return this;
868
+ }
869
+ withHttpAdapter(httpAdapter) {
870
+ __privateGet(this, _options).httpAdapter = httpAdapter;
871
+ return this;
872
+ }
873
+ withHttpLogger(httpLogger) {
874
+ __privateGet(this, _options).httpLogger = httpLogger;
875
+ return this;
876
+ }
877
+ build() {
878
+ return new AuthyonServerClient({ ...__privateGet(this, _options) });
879
+ }
880
+ };
881
+ _options = new WeakMap();
472
882
  export {
883
+ AuthorizationError,
884
+ AuthyonAbility,
885
+ AuthyonAbilityBuilder,
473
886
  AuthyonError,
474
887
  AuthyonServerClient,
888
+ AuthyonServerClientBuilder,
889
+ ErrorCodes,
890
+ FetchHttpAdapter,
891
+ JwksTokenVerifier,
892
+ LoggingHttpAdapter,
893
+ PlatformScopedClient,
475
894
  TenantScopedClient,
476
- createClient
895
+ TokenVerificationError,
896
+ UserScopedClient,
897
+ WorkspaceInvitesClient,
898
+ authorizeRequest,
899
+ authorizeToken,
900
+ createAuthorizationErrorResponse,
901
+ createAuthyonAbility,
902
+ createAuthyonRules,
903
+ createClient,
904
+ createExpressAuthorizationMiddleware,
905
+ hasPermission,
906
+ hasPermissionGroup,
907
+ normalizeClientIp
477
908
  };