@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.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,47 +19,790 @@ 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 = {};
22
29
  __export(index_exports, {
30
+ AuthorizationError: () => AuthorizationError,
31
+ AuthyonAbility: () => AuthyonAbility,
32
+ AuthyonAbilityBuilder: () => AuthyonAbilityBuilder,
23
33
  AuthyonError: () => AuthyonError,
24
34
  AuthyonServerClient: () => AuthyonServerClient,
35
+ AuthyonServerClientBuilder: () => AuthyonServerClientBuilder,
36
+ ErrorCodes: () => ErrorCodes,
37
+ FetchHttpAdapter: () => FetchHttpAdapter,
38
+ JwksTokenVerifier: () => JwksTokenVerifier,
39
+ LoggingHttpAdapter: () => LoggingHttpAdapter,
40
+ PlatformScopedClient: () => PlatformScopedClient,
25
41
  TenantScopedClient: () => TenantScopedClient,
26
- createClient: () => createClient
42
+ TokenVerificationError: () => TokenVerificationError,
43
+ UserScopedClient: () => UserScopedClient,
44
+ WorkspaceInvitesClient: () => WorkspaceInvitesClient,
45
+ authorizeRequest: () => authorizeRequest,
46
+ authorizeToken: () => authorizeToken,
47
+ createAuthorizationErrorResponse: () => createAuthorizationErrorResponse,
48
+ createAuthyonAbility: () => createAuthyonAbility,
49
+ createAuthyonRules: () => createAuthyonRules,
50
+ createClient: () => createClient,
51
+ createExpressAuthorizationMiddleware: () => createExpressAuthorizationMiddleware,
52
+ hasPermission: () => hasPermission,
53
+ hasPermissionGroup: () => hasPermissionGroup,
54
+ normalizeClientIp: () => normalizeClientIp
27
55
  });
28
56
  module.exports = __toCommonJS(index_exports);
29
57
 
30
- // src/errors.ts
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
+
302
+ // ../../internal/core/config/defaults.ts
303
+ var DEFAULT_BASE_URL = "https://api.authyon.com";
304
+ var DEFAULT_EXPIRY_SKEW_MS = 3e4;
305
+
306
+ // ../../internal/core/auth/expiringTokenProvider.ts
307
+ var _token, _inFlight;
308
+ var ExpiringTokenProvider = class {
309
+ constructor(acquire, expirySkewMs = DEFAULT_EXPIRY_SKEW_MS) {
310
+ this.acquire = acquire;
311
+ this.expirySkewMs = expirySkewMs;
312
+ __privateAdd(this, _token, null);
313
+ __privateAdd(this, _inFlight, null);
314
+ }
315
+ async getAccessToken() {
316
+ if (__privateGet(this, _token) && Date.now() < __privateGet(this, _token).expiresAt - this.expirySkewMs) {
317
+ return __privateGet(this, _token).accessToken;
318
+ }
319
+ if (__privateGet(this, _inFlight)) return __privateGet(this, _inFlight);
320
+ __privateSet(this, _inFlight, this.acquire().then((token) => {
321
+ __privateSet(this, _token, token);
322
+ return token.accessToken;
323
+ }).finally(() => {
324
+ __privateSet(this, _inFlight, null);
325
+ }));
326
+ return __privateGet(this, _inFlight);
327
+ }
328
+ clear() {
329
+ __privateSet(this, _token, null);
330
+ }
331
+ };
332
+ _token = new WeakMap();
333
+ _inFlight = new WeakMap();
334
+
335
+ // ../../internal/core/errors/authyonError.ts
336
+ var ErrorCodes = {
337
+ Unknown: "unknown",
338
+ NetworkError: "request.network_error",
339
+ Timeout: "request.timeout",
340
+ SessionMalformed: "session.malformed",
341
+ NotAuthenticated: "auth.not_authenticated",
342
+ InvalidToken: "auth.invalid_token",
343
+ MissingToken: "auth.missing_token",
344
+ EmailTaken: "user.email_taken",
345
+ PasswordWeak: "user.password_weak",
346
+ PasswordPwned: "user.password_pwned",
347
+ RateLimited: "rate_limited"
348
+ };
31
349
  var AuthyonError = class extends Error {
32
- constructor(status, body) {
350
+ constructor(status, body, options = {}) {
33
351
  super(body.detail ?? body.title ?? `Authyon request failed with status ${status}`);
34
352
  this.name = "AuthyonError";
35
353
  this.status = status;
36
354
  this.code = body.code ?? "unknown";
37
355
  this.title = body.title ?? "Error";
38
356
  this.detail = body.detail;
357
+ this.requestId = options.requestId;
358
+ this.retryAfter = options.retryAfter;
359
+ this.cause = options.cause;
39
360
  }
40
361
  is(code) {
41
362
  return this.code === code;
42
363
  }
364
+ isAny(...codes) {
365
+ return codes.includes(this.code);
366
+ }
367
+ hasPrefix(prefix) {
368
+ return this.code.startsWith(prefix);
369
+ }
370
+ isStatus(...statuses) {
371
+ return statuses.includes(this.status);
372
+ }
373
+ /** Stable interpretation for UI decisions, retries, telemetry and support flows. */
374
+ interpret() {
375
+ const category = classifyError(this.status, this.code);
376
+ return {
377
+ category,
378
+ action: actionFor(category),
379
+ retryable: isRetryable(category),
380
+ ...this.retryAfter !== void 0 ? { retryAfter: this.retryAfter } : {},
381
+ ...this.requestId !== void 0 ? { requestId: this.requestId } : {}
382
+ };
383
+ }
384
+ get category() {
385
+ return this.interpret().category;
386
+ }
387
+ get retryable() {
388
+ return this.interpret().retryable;
389
+ }
390
+ toJSON() {
391
+ return {
392
+ name: this.name,
393
+ message: this.message,
394
+ status: this.status,
395
+ code: this.code,
396
+ title: this.title,
397
+ detail: this.detail,
398
+ requestId: this.requestId,
399
+ retryAfter: this.retryAfter,
400
+ ...this.interpret()
401
+ };
402
+ }
43
403
  };
404
+ function classifyError(status, code) {
405
+ if (code === ErrorCodes.NetworkError) return "network";
406
+ if (code === ErrorCodes.Timeout || status === 408) return "timeout";
407
+ if (code === ErrorCodes.RateLimited || status === 429) return "rate_limit";
408
+ if (code === ErrorCodes.EmailTaken || status === 409) return "conflict";
409
+ if (code === ErrorCodes.PasswordWeak || code === ErrorCodes.PasswordPwned) return "validation";
410
+ if (code === ErrorCodes.SessionMalformed) return "server";
411
+ if (code.startsWith("auth.") || status === 401) return "authentication";
412
+ if (status === 403) return "authorization";
413
+ if (status === 404) return "not_found";
414
+ if (status === 400 || status === 422) return "validation";
415
+ if (status >= 500) return "server";
416
+ return "unknown";
417
+ }
418
+ function actionFor(category) {
419
+ switch (category) {
420
+ case "network":
421
+ case "timeout":
422
+ case "rate_limit":
423
+ case "server":
424
+ return "retry";
425
+ case "authentication":
426
+ return "reauthenticate";
427
+ case "authorization":
428
+ return "request_access";
429
+ case "validation":
430
+ return "fix_input";
431
+ case "not_found":
432
+ return "not_found";
433
+ case "conflict":
434
+ return "resolve_conflict";
435
+ case "unknown":
436
+ return "contact_support";
437
+ }
438
+ }
439
+ function isRetryable(category) {
440
+ return category === "network" || category === "timeout" || category === "rate_limit" || category === "server";
441
+ }
44
442
 
45
- // src/client.ts
46
- var DEFAULT_BASE_URL = "https://api.authyon.com";
47
- var EXPIRY_SKEW_MS = 3e4;
48
- function toQuery(params = {}) {
443
+ // ../../internal/core/http/query.ts
444
+ function appendQuery(path, params) {
445
+ if (!params) return path;
49
446
  const query = new URLSearchParams();
50
- for (const [key, value] of Object.entries(params)) {
51
- if (value !== void 0) query.set(key, String(value));
447
+ for (const [key, value2] of Object.entries(params)) {
448
+ if (value2 !== void 0) query.set(key, String(value2));
449
+ }
450
+ const value = query.toString();
451
+ return value ? `${path}${path.includes("?") ? "&" : "?"}${value}` : path;
452
+ }
453
+
454
+ // ../../internal/core/http/httpAdapter.ts
455
+ var FetchHttpAdapter = class {
456
+ constructor(fetchImpl = fetch.bind(globalThis)) {
457
+ this.fetchImpl = fetchImpl;
458
+ }
459
+ request(request) {
460
+ return this.fetchImpl(request.url, {
461
+ method: request.method,
462
+ headers: request.headers,
463
+ body: request.body,
464
+ signal: request.signal,
465
+ redirect: "error"
466
+ });
467
+ }
468
+ };
469
+ var LoggingHttpAdapter = class {
470
+ constructor(adapter, options) {
471
+ this.adapter = adapter;
472
+ this.options = options;
473
+ }
474
+ async request(request) {
475
+ const startedAt = Date.now();
476
+ const eventBase = {
477
+ method: request.method,
478
+ url: sanitizeUrl(request.url)
479
+ };
480
+ this.log({ type: "request", ...eventBase, timestamp: startedAt });
481
+ try {
482
+ const response = await this.adapter.request(request);
483
+ this.log({
484
+ type: "response",
485
+ ...eventBase,
486
+ status: response.status,
487
+ durationMs: Date.now() - startedAt,
488
+ requestId: response.headers.get("x-request-id") ?? response.headers.get("trace-id") ?? void 0,
489
+ timestamp: Date.now()
490
+ });
491
+ return response;
492
+ } catch (error) {
493
+ this.log({
494
+ type: "error",
495
+ ...eventBase,
496
+ errorName: error instanceof Error ? error.name : "UnknownError",
497
+ durationMs: Date.now() - startedAt,
498
+ timestamp: Date.now()
499
+ });
500
+ throw error;
501
+ }
502
+ }
503
+ log(event) {
504
+ if (!this.options.enabled) return;
505
+ const logger = this.options.logger ?? defaultHttpLogger;
506
+ try {
507
+ logger(event);
508
+ } catch {
509
+ }
510
+ }
511
+ };
512
+ function defaultHttpLogger(event) {
513
+ console.debug("[Authyon HTTP]", event);
514
+ }
515
+ function sanitizeUrl(value) {
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");
521
+ const queryKeys = /* @__PURE__ */ new Set();
522
+ url.searchParams.forEach((_value, key) => queryKeys.add(key));
523
+ url.search = queryKeys.size > 0 ? [...queryKeys].map((key) => `${encodeURIComponent(key)}=REDACTED`).join("&") : "";
524
+ return url.toString();
525
+ }
526
+
527
+ // ../../internal/core/http/transport.ts
528
+ var DEFAULT_TIMEOUT_MS = 15e3;
529
+ var SharedTransportError = class extends Error {
530
+ constructor(code, cause) {
531
+ super(code === "request.timeout" ? "Request timed out" : "Network request failed");
532
+ this.name = "SharedTransportError";
533
+ this.code = code;
534
+ this.cause = cause;
535
+ }
536
+ };
537
+ function createSharedTransport(options) {
538
+ const baseUrl = normalizeBaseUrl(options.baseUrl, options.allowInsecureHttp ?? false);
539
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
540
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
541
+ throw new Error("Authyon: `timeoutMs` must be a non-negative finite number");
542
+ }
543
+ if (options.httpAdapter && options.fetch) {
544
+ throw new Error("Authyon: use either `httpAdapter` or `fetch`, not both");
545
+ }
546
+ const baseAdapter = options.httpAdapter ?? new FetchHttpAdapter(options.fetch);
547
+ const httpAdapter = options.httpLogger ? new LoggingHttpAdapter(baseAdapter, options.httpLogger) : baseAdapter;
548
+ return {
549
+ baseUrl,
550
+ async request(path, init) {
551
+ const controller = new AbortController();
552
+ const timeout = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : void 0;
553
+ try {
554
+ return await httpAdapter.request({
555
+ url: `${baseUrl}${path}`,
556
+ method: init.method ?? "GET",
557
+ headers: normalizeHeaders(init.headers),
558
+ body: init.body,
559
+ signal: controller.signal
560
+ });
561
+ } catch (cause) {
562
+ throw new SharedTransportError(
563
+ controller.signal.aborted ? ErrorCodes.Timeout : ErrorCodes.NetworkError,
564
+ cause
565
+ );
566
+ } finally {
567
+ if (timeout !== void 0) clearTimeout(timeout);
568
+ }
569
+ }
570
+ };
571
+ }
572
+ function normalizeHeaders(headers) {
573
+ const normalized = {};
574
+ new Headers(headers).forEach((value, key) => {
575
+ normalized[key] = value;
576
+ });
577
+ return normalized;
578
+ }
579
+ function responseMetadata(response) {
580
+ const retryAfterHeader = response.headers.get("retry-after");
581
+ const retryAfter = retryAfterHeader ? Number(retryAfterHeader) : void 0;
582
+ return {
583
+ requestId: response.headers.get("x-request-id") ?? response.headers.get("trace-id") ?? void 0,
584
+ retryAfter: Number.isFinite(retryAfter) ? retryAfter : void 0
585
+ };
586
+ }
587
+ function normalizeBaseUrl(value, allowInsecureHttp) {
588
+ let url;
589
+ try {
590
+ url = new URL(value);
591
+ } catch {
592
+ throw new Error("Authyon: `baseUrl` must be an absolute URL");
593
+ }
594
+ const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
595
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && (loopback || allowInsecureHttp))) {
596
+ throw new Error(
597
+ "Authyon: `baseUrl` must use HTTPS (HTTP is allowed only for loopback or with `allowInsecureHttp`)"
598
+ );
599
+ }
600
+ if (url.username || url.password || url.search || url.hash) {
601
+ throw new Error(
602
+ "Authyon: `baseUrl` cannot contain credentials, query parameters, or fragments"
603
+ );
604
+ }
605
+ return url.toString().replace(/\/+$/, "");
606
+ }
607
+
608
+ // ../../internal/core/http/jsonHttpClient.ts
609
+ var JsonHttpClient = class {
610
+ constructor(transport) {
611
+ this.transport = transport;
612
+ }
613
+ async send(path, options = {}) {
614
+ const headers = { ...options.headers };
615
+ if (options.body !== void 0) headers["Content-Type"] = "application/json";
616
+ try {
617
+ return await this.transport.request(appendQuery(path, options.query), {
618
+ method: options.method ?? "GET",
619
+ headers,
620
+ body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
621
+ });
622
+ } catch (cause) {
623
+ if (!(cause instanceof SharedTransportError)) throw cause;
624
+ throw new AuthyonError(0, { code: cause.code, title: cause.message }, { cause: cause.cause });
625
+ }
52
626
  }
53
- const qs = query.toString();
54
- return qs ? `?${qs}` : "";
627
+ async parse(response) {
628
+ if (!response.ok) {
629
+ let body = {};
630
+ try {
631
+ body = await response.json();
632
+ } catch {
633
+ }
634
+ throw new AuthyonError(response.status, body, responseMetadata(response));
635
+ }
636
+ if (response.status === 204) return void 0;
637
+ return await response.json();
638
+ }
639
+ async request(path, options = {}) {
640
+ return this.parse(await this.send(path, options));
641
+ }
642
+ };
643
+
644
+ // ../../internal/core/http/clientIp.ts
645
+ function clientIpHeaders(context) {
646
+ if (!context?.clientIp) return {};
647
+ return { "X-Forwarded-For": normalizeClientIp(context.clientIp) };
55
648
  }
649
+ function normalizeClientIp(value) {
650
+ let ip = value.trim();
651
+ if (ip.startsWith("[") && ip.endsWith("]")) ip = ip.slice(1, -1);
652
+ if (!ip || ip.length > 45 || ip.includes(",") || /[\r\n\s]/.test(ip) || !/^[0-9a-f:.]+$/i.test(ip) || !ip.includes(".") && !ip.includes(":")) {
653
+ throw invalidClientIp();
654
+ }
655
+ try {
656
+ new URL(ip.includes(":") ? `http://[${ip}]` : `http://${ip}`);
657
+ } catch {
658
+ throw invalidClientIp();
659
+ }
660
+ return ip;
661
+ }
662
+ function invalidClientIp() {
663
+ return new Error("Authyon: `clientIp` must be a single valid IPv4 or IPv6 address");
664
+ }
665
+
666
+ // src/security/jwksTokenVerifier.ts
667
+ var import_jose = require("jose");
668
+ var SAFE_ASYMMETRIC_ALGORITHMS = [
669
+ "RS256",
670
+ "PS256",
671
+ "ES256",
672
+ "EdDSA"
673
+ ];
674
+ var TokenVerificationError = class extends Error {
675
+ constructor(code, options = {}) {
676
+ super(messageForCode(code));
677
+ this.code = code;
678
+ this.name = "TokenVerificationError";
679
+ if (options.cause !== void 0) Object.defineProperty(this, "cause", { value: options.cause });
680
+ }
681
+ };
682
+ var JwksTokenVerifier = class {
683
+ constructor(options) {
684
+ this.options = options;
685
+ validateOptions(options);
686
+ this.algorithms = [...options.algorithms ?? ["RS256"]];
687
+ this.acceptedTokenTypes = new Set(options.acceptedTokenTypes ?? ["JWT", "at+jwt"]);
688
+ this.verifyKey = options.jwks ? (0, import_jose.createLocalJWKSet)(options.jwks) : (0, import_jose.createRemoteJWKSet)(new URL(options.jwksUri), {
689
+ timeoutDuration: options.jwksTimeoutMs ?? 5e3,
690
+ cacheMaxAge: options.jwksCacheMs ?? 6e5,
691
+ cooldownDuration: options.jwksCooldownMs ?? 3e4
692
+ });
693
+ }
694
+ async verifyAccessToken(token) {
695
+ if (!token || token.length > 16384) {
696
+ throw new TokenVerificationError("token.invalid");
697
+ }
698
+ try {
699
+ const result = await (0, import_jose.jwtVerify)(token, this.verifyKey, {
700
+ algorithms: this.algorithms,
701
+ issuer: this.options.issuer,
702
+ audience: this.options.audience,
703
+ clockTolerance: this.options.clockToleranceSeconds ?? 5,
704
+ maxTokenAge: this.options.maxTokenAgeSeconds ?? 3600,
705
+ requiredClaims: ["iss", "aud", "sub", "exp", "iat"]
706
+ });
707
+ validateTokenType(result, this.acceptedTokenTypes);
708
+ validateClaims(result.payload);
709
+ return { claims: result.payload, protectedHeader: result.protectedHeader };
710
+ } catch (cause) {
711
+ if (cause instanceof TokenVerificationError) throw cause;
712
+ throw new TokenVerificationError(errorCode(cause), { cause });
713
+ }
714
+ }
715
+ };
716
+ function validateTokenType(result, acceptedTokenTypes) {
717
+ const type = result.protectedHeader.typ;
718
+ if (!type || !acceptedTokenTypes.has(type)) {
719
+ throw new TokenVerificationError("token.invalid_claims");
720
+ }
721
+ }
722
+ function validateClaims(claims) {
723
+ if (typeof claims.sub !== "string" || !claims.sub) invalidClaims();
724
+ if (claims.permissions !== void 0 && !isStringArray(claims.permissions)) invalidClaims();
725
+ if (claims.roles !== void 0 && !isStringArray(claims.roles)) invalidClaims();
726
+ if (claims.scope !== void 0 && typeof claims.scope !== "string") invalidClaims();
727
+ }
728
+ function validateOptions(options) {
729
+ const issuer = secureUrl(options.issuer, "issuer");
730
+ if (Boolean(options.jwksUri) === Boolean(options.jwks)) {
731
+ throw new Error("Authyon: configure exactly one of `jwksUri` or `jwks`");
732
+ }
733
+ const jwks = options.jwksUri ? secureUrl(options.jwksUri, "jwksUri") : void 0;
734
+ if (!options.audience || Array.isArray(options.audience) && options.audience.length === 0) {
735
+ throw new Error("Authyon: JWT `audience` is required");
736
+ }
737
+ if (jwks && issuer.origin !== jwks.origin) {
738
+ throw new Error("Authyon: `issuer` and `jwksUri` must use the same trusted origin");
739
+ }
740
+ const algorithms = options.algorithms ?? ["RS256"];
741
+ if (algorithms.length === 0 || algorithms.some((algorithm) => !SAFE_ASYMMETRIC_ALGORITHMS.includes(algorithm))) {
742
+ throw new Error("Authyon: only approved asymmetric JWT algorithms are accepted");
743
+ }
744
+ if (options.acceptedTokenTypes?.length === 0 || options.acceptedTokenTypes?.some((type) => !type.trim())) {
745
+ throw new Error("Authyon: `acceptedTokenTypes` must contain non-empty values");
746
+ }
747
+ for (const [name, value] of [
748
+ ["clockToleranceSeconds", options.clockToleranceSeconds],
749
+ ["maxTokenAgeSeconds", options.maxTokenAgeSeconds],
750
+ ["jwksTimeoutMs", options.jwksTimeoutMs],
751
+ ["jwksCacheMs", options.jwksCacheMs],
752
+ ["jwksCooldownMs", options.jwksCooldownMs]
753
+ ]) {
754
+ if (value !== void 0 && (!Number.isFinite(value) || value < 0)) {
755
+ throw new Error(`Authyon: \`${name}\` must be a non-negative finite number`);
756
+ }
757
+ }
758
+ }
759
+ function secureUrl(value, name) {
760
+ let url;
761
+ try {
762
+ url = new URL(value);
763
+ } catch {
764
+ throw new Error(`Authyon: JWT \`${name}\` must be an absolute URL`);
765
+ }
766
+ if (url.protocol !== "https:" || url.username || url.password || url.hash) {
767
+ throw new Error(`Authyon: JWT \`${name}\` must be a credential-free HTTPS URL`);
768
+ }
769
+ return url;
770
+ }
771
+ function errorCode(error) {
772
+ const code = isRecord(error) && typeof error.code === "string" ? error.code : "";
773
+ if (code === "ERR_JWT_EXPIRED") return "token.expired";
774
+ if (code === "ERR_JWS_SIGNATURE_VERIFICATION_FAILED") return "token.invalid_signature";
775
+ if (code === "ERR_JWKS_TIMEOUT") {
776
+ return "token.key_unavailable";
777
+ }
778
+ if (code === "ERR_JWKS_NO_MATCHING_KEY") return "token.invalid_signature";
779
+ if (code === "ERR_JWT_CLAIM_VALIDATION_FAILED") {
780
+ const claim = isRecord(error) && typeof error.claim === "string" ? error.claim : "";
781
+ return claim === "nbf" ? "token.not_active" : "token.invalid_claims";
782
+ }
783
+ return "token.invalid";
784
+ }
785
+ function messageForCode(code) {
786
+ if (code === "token.key_unavailable") return "Token verification key is unavailable";
787
+ if (code === "token.expired") return "Token has expired";
788
+ if (code === "token.not_active") return "Token is not active yet";
789
+ return "Token verification failed";
790
+ }
791
+ function invalidClaims() {
792
+ throw new TokenVerificationError("token.invalid_claims");
793
+ }
794
+ function isStringArray(value) {
795
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
796
+ }
797
+ function isRecord(value) {
798
+ return typeof value === "object" && value !== null;
799
+ }
800
+
801
+ // src/client/authyonServerClient.ts
802
+ var _clientSecret;
56
803
  var AuthyonServerClient = class {
57
804
  constructor(options = {}) {
58
- this.environmentToken = null;
59
- this.environmentTokenInFlight = null;
805
+ __privateAdd(this, _clientSecret);
60
806
  // ── Discovery ──────────────────────────────────────────────────────────────
61
807
  this.discovery = {
62
808
  /** GET /.well-known/jwks.json — the environment's public signing keys. */
@@ -90,8 +836,19 @@ var AuthyonServerClient = class {
90
836
  client_id: credentials.clientId,
91
837
  client_secret: credentials.clientSecret
92
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
+ }
93
847
  })
94
848
  };
849
+ this.workspaceInvites = new WorkspaceInvitesClient(
850
+ (path, options) => this.http.request(path, options)
851
+ );
95
852
  // ── Environment management (users, tenants, roles, permissions, audit) ───
96
853
  this.environment = {
97
854
  users: {
@@ -183,8 +940,53 @@ var AuthyonServerClient = class {
183
940
  )
184
941
  },
185
942
  tenants: {
186
- /** GET /env/tenants — list tenants in the environment. */
187
- 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 }),
188
990
  /** POST /env/tenants — create a tenant in the environment. */
189
991
  create: (input) => this.request("/env/tenants", { method: "POST", envBearer: true, body: input }),
190
992
  /** GET /env/tenants/{tenantId} — fetch a tenant by id. */
@@ -195,7 +997,18 @@ var AuthyonServerClient = class {
195
997
  envBearer: true,
196
998
  body: input
197
999
  }),
198
- /** 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. */
199
1012
  delete: (tenantId) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}`, {
200
1013
  method: "DELETE",
201
1014
  envBearer: true
@@ -329,54 +1142,60 @@ var AuthyonServerClient = class {
329
1142
  };
330
1143
  this.envKey = options.envKey;
331
1144
  this.clientId = options.clientId;
332
- this.clientSecret = options.clientSecret;
333
- this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
334
- this.fetchImpl = options.fetch ?? fetch.bind(globalThis);
1145
+ __privateSet(this, _clientSecret, options.clientSecret);
1146
+ this.clientIp = options.clientIp;
1147
+ this.transport = createSharedTransport({
1148
+ baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
1149
+ allowInsecureHttp: options.allowInsecureHttp,
1150
+ timeoutMs: options.timeoutMs,
1151
+ httpAdapter: options.httpAdapter,
1152
+ httpLogger: options.httpLogger,
1153
+ fetch: options.fetch
1154
+ });
1155
+ this.http = new JsonHttpClient(this.transport);
1156
+ this.environmentTokenProvider = new ExpiringTokenProvider(async () => {
1157
+ if (!this.clientId || !__privateGet(this, _clientSecret)) {
1158
+ throw new Error(
1159
+ "Authyon: `clientId`/`clientSecret` are required for environment management calls (mint a pair in the console, under the environment's OAuth clients)."
1160
+ );
1161
+ }
1162
+ const token = await this.environmentAuth.token({
1163
+ clientId: this.clientId,
1164
+ clientSecret: __privateGet(this, _clientSecret)
1165
+ });
1166
+ return {
1167
+ accessToken: token.access_token,
1168
+ expiresAt: Date.now() + token.expires_in * 1e3
1169
+ };
1170
+ });
335
1171
  }
336
1172
  async request(path, options = {}) {
337
- const headers = { ...options.headers };
338
- if (options.body !== void 0) headers["Content-Type"] = "application/json";
1173
+ const headers = {
1174
+ ...clientIpHeaders({ clientIp: this.clientIp }),
1175
+ ...options.headers
1176
+ };
339
1177
  if (options.envBearer) {
340
1178
  headers.Authorization = `Bearer ${await this.getEnvironmentAccessToken()}`;
341
1179
  }
342
1180
  if (this.envKey) headers["X-Authyon-Environment"] = this.envKey;
343
- const response = await this.fetchImpl(`${this.baseUrl}${path}${toQuery(options.query)}`, {
344
- method: options.method ?? "GET",
345
- headers,
346
- body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
347
- });
348
- if (!response.ok) {
349
- let body = {};
350
- try {
351
- body = await response.json();
352
- } catch {
353
- }
354
- throw new AuthyonError(response.status, body);
355
- }
356
- if (response.status === 204) return void 0;
357
- return await response.json();
1181
+ return this.http.request(path, { ...options, headers });
358
1182
  }
359
1183
  /** Mints (and caches) an environment access token from `clientId`/`clientSecret`. */
360
1184
  async getEnvironmentAccessToken() {
361
- if (this.environmentToken && Date.now() < this.environmentToken.expiresAt - EXPIRY_SKEW_MS) {
362
- return this.environmentToken.accessToken;
363
- }
364
- if (this.environmentTokenInFlight) return this.environmentTokenInFlight;
365
- if (!this.clientId || !this.clientSecret) {
366
- throw new Error(
367
- "Authyon: `clientId`/`clientSecret` are required for environment management calls (mint a pair in the console, under the environment's OAuth clients)."
368
- );
1185
+ return this.environmentTokenProvider.getAccessToken();
1186
+ }
1187
+ /** Discovers Authyon's trusted issuer/JWKS and creates a cached local access-token verifier. */
1188
+ async createJwksTokenVerifier(options) {
1189
+ const { publishableKey, issuer, ...verifierOptions } = options;
1190
+ const configuration = await this.discovery.openidConfiguration(publishableKey);
1191
+ if (issuer && configuration.issuer !== issuer) {
1192
+ throw new Error("Authyon: discovered JWT issuer does not match the configured issuer");
369
1193
  }
370
- this.environmentTokenInFlight = this.environmentAuth.token({ clientId: this.clientId, clientSecret: this.clientSecret }).then((token) => {
371
- this.environmentToken = {
372
- accessToken: token.access_token,
373
- expiresAt: Date.now() + token.expires_in * 1e3
374
- };
375
- return token.access_token;
376
- }).finally(() => {
377
- this.environmentTokenInFlight = null;
1194
+ return new JwksTokenVerifier({
1195
+ ...verifierOptions,
1196
+ issuer: issuer ?? configuration.issuer,
1197
+ jwksUri: configuration.jwks_uri
378
1198
  });
379
- return this.environmentTokenInFlight;
380
1199
  }
381
1200
  // ── Token verification ────────────────────────────────────────────────────
382
1201
  //
@@ -387,12 +1206,22 @@ var AuthyonServerClient = class {
387
1206
  // endpoint with only the `X-Authyon-Environment` header (no bearer)
388
1207
  // returns 401 with `WWW-Authenticate: Bearer`.
389
1208
  /** POST /auth/introspect — lightweight token introspection (RFC 7662). */
390
- introspect(token) {
391
- return this.request("/auth/introspect", { method: "POST", envBearer: true, body: { token } });
1209
+ introspect(token, context) {
1210
+ return this.request("/auth/introspect", {
1211
+ method: "POST",
1212
+ envBearer: true,
1213
+ body: { token },
1214
+ headers: clientIpHeaders(context)
1215
+ });
392
1216
  }
393
1217
  /** POST /auth/validate — recommended: cross-checks DB state, catches revocation immediately. */
394
- async validate(token) {
395
- const raw = await this.request("/auth/validate", { method: "POST", envBearer: true, body: { token } });
1218
+ async validate(token, context) {
1219
+ const raw = await this.request("/auth/validate", {
1220
+ method: "POST",
1221
+ envBearer: true,
1222
+ body: { token },
1223
+ headers: clientIpHeaders(context)
1224
+ });
396
1225
  return { valid: raw.valid, reason: raw.reason ?? null, user: raw.profile };
397
1226
  }
398
1227
  /**
@@ -404,6 +1233,14 @@ var AuthyonServerClient = class {
404
1233
  tenant(credentials) {
405
1234
  return new TenantScopedClient(this, credentials);
406
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
+ }
407
1244
  /** @internal used by {@link TenantScopedClient} to share the base URL/fetch/error handling. */
408
1245
  _requestAsTenant(accessToken, path, options = {}) {
409
1246
  return this.request(path, {
@@ -413,10 +1250,9 @@ var AuthyonServerClient = class {
413
1250
  });
414
1251
  }
415
1252
  };
1253
+ _clientSecret = new WeakMap();
416
1254
  var TenantScopedClient = class {
417
1255
  constructor(server, credentials) {
418
- this.token = null;
419
- this.tokenInFlight = null;
420
1256
  this.members = {
421
1257
  /**
422
1258
  * GET /tenant/members — members of the token's tenant.
@@ -473,35 +1309,402 @@ var TenantScopedClient = class {
473
1309
  delete: (roleId) => this.request(`/tenant/roles/${encodeURIComponent(roleId)}`, { method: "DELETE" })
474
1310
  };
475
1311
  this.server = server;
476
- this.credentials = credentials;
477
- }
478
- async getAccessToken() {
479
- if (this.token && Date.now() < this.token.expiresAt - EXPIRY_SKEW_MS) {
480
- return this.token.accessToken;
481
- }
482
- if (this.tokenInFlight) return this.tokenInFlight;
483
- this.tokenInFlight = this.server.tenantAuth.token(this.credentials).then((token) => {
484
- this.token = {
1312
+ this.tokenProvider = new ExpiringTokenProvider(async () => {
1313
+ const token = await this.server.tenantAuth.token(credentials);
1314
+ return {
485
1315
  accessToken: token.access_token,
486
1316
  expiresAt: Date.now() + token.expires_in * 1e3
487
1317
  };
488
- return token.access_token;
489
- }).finally(() => {
490
- this.tokenInFlight = null;
491
1318
  });
492
- return this.tokenInFlight;
1319
+ }
1320
+ async getAccessToken() {
1321
+ return this.tokenProvider.getAccessToken();
493
1322
  }
494
1323
  async request(path, options = {}) {
495
1324
  return this.server._requestAsTenant(await this.getAccessToken(), path, options);
496
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
+ }
497
1330
  };
498
1331
  function createClient(options = {}) {
499
1332
  return new AuthyonServerClient(options);
500
1333
  }
1334
+
1335
+ // src/client/authyonServerClientBuilder.ts
1336
+ var _options;
1337
+ var AuthyonServerClientBuilder = class {
1338
+ constructor() {
1339
+ __privateAdd(this, _options, {});
1340
+ }
1341
+ withEnvironmentKey(envKey) {
1342
+ __privateGet(this, _options).envKey = envKey;
1343
+ return this;
1344
+ }
1345
+ withEnvironmentCredentials(clientId, 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;
1352
+ return this;
1353
+ }
1354
+ withBaseUrl(baseUrl, allowInsecureHttp = false) {
1355
+ __privateGet(this, _options).baseUrl = baseUrl;
1356
+ __privateGet(this, _options).allowInsecureHttp = allowInsecureHttp;
1357
+ return this;
1358
+ }
1359
+ withTimeout(timeoutMs) {
1360
+ __privateGet(this, _options).timeoutMs = timeoutMs;
1361
+ return this;
1362
+ }
1363
+ withHttpAdapter(httpAdapter) {
1364
+ __privateGet(this, _options).httpAdapter = httpAdapter;
1365
+ return this;
1366
+ }
1367
+ withHttpLogger(httpLogger) {
1368
+ __privateGet(this, _options).httpLogger = httpLogger;
1369
+ return this;
1370
+ }
1371
+ build() {
1372
+ return new AuthyonServerClient({ ...__privateGet(this, _options) });
1373
+ }
1374
+ };
1375
+ _options = new WeakMap();
1376
+
1377
+ // ../../internal/core/authorization/ability.ts
1378
+ var AuthyonAbility = class {
1379
+ constructor(rules = [], detectSubjectType = defaultSubjectType) {
1380
+ this.detectSubjectType = detectSubjectType;
1381
+ this.listeners = /* @__PURE__ */ new Set();
1382
+ this.currentRules = rules.map(cloneRule);
1383
+ }
1384
+ get rules() {
1385
+ return this.currentRules;
1386
+ }
1387
+ can(action, subject, field) {
1388
+ const subjectType = typeof subject === "string" ? subject : this.detectSubjectType(subject);
1389
+ for (let index = this.currentRules.length - 1; index >= 0; index -= 1) {
1390
+ const rule = this.currentRules[index];
1391
+ if (!matchesToken(rule.action, action, "manage")) continue;
1392
+ if (!matchesToken(rule.subject, subjectType, "all")) continue;
1393
+ if (field && rule.fields && !rule.fields.some((value) => matchesField(value, field)))
1394
+ continue;
1395
+ if (rule.conditions) {
1396
+ if (typeof subject === "string" || !matchesConditions(subject, rule.conditions)) continue;
1397
+ }
1398
+ return !rule.inverted;
1399
+ }
1400
+ return false;
1401
+ }
1402
+ cannot(action, subject, field) {
1403
+ return !this.can(action, subject, field);
1404
+ }
1405
+ rulesFor(action, subject) {
1406
+ return this.currentRules.filter(
1407
+ (rule) => matchesToken(rule.action, action, "manage") && matchesToken(rule.subject, subject, "all")
1408
+ );
1409
+ }
1410
+ update(rules) {
1411
+ this.currentRules = rules.map(cloneRule);
1412
+ for (const listener of this.listeners) listener(this.rules);
1413
+ }
1414
+ on(event, listener) {
1415
+ if (event !== "updated") return () => void 0;
1416
+ this.listeners.add(listener);
1417
+ return () => this.listeners.delete(listener);
1418
+ }
1419
+ };
1420
+ var AuthyonAbilityBuilder = class {
1421
+ constructor() {
1422
+ this.rules = [];
1423
+ }
1424
+ can(action, subject, conditions, fields) {
1425
+ this.rules.push({ action, subject, conditions, fields });
1426
+ return this;
1427
+ }
1428
+ cannot(action, subject, conditions, fields, reason) {
1429
+ this.rules.push({ action, subject, conditions, fields, reason, inverted: true });
1430
+ return this;
1431
+ }
1432
+ build(options = {}) {
1433
+ return new AuthyonAbility(this.rules, options.detectSubjectType);
1434
+ }
1435
+ };
1436
+ function createAuthyonAbility(source = {}, options = {}) {
1437
+ return new AuthyonAbility(createAuthyonRules(source, options), options.detectSubjectType);
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
+ }
1455
+ function createAuthyonRules(source = {}, options = {}) {
1456
+ const permissions = /* @__PURE__ */ new Set([
1457
+ ...source.permissions ?? [],
1458
+ ...source.scope?.split(/\s+/).filter(Boolean) ?? []
1459
+ ]);
1460
+ const rules = [...permissions].map(permissionToRule).filter(isAbilityRule);
1461
+ for (const role of /* @__PURE__ */ new Set([...source.roles ?? [], ...options.roles ?? []])) {
1462
+ rules.push(...(options.roleRules?.[role] ?? []).map(cloneRule));
1463
+ }
1464
+ rules.push(...(options.rules ?? []).map(cloneRule));
1465
+ return rules;
1466
+ }
1467
+ function permissionToRule(permission) {
1468
+ const normalized = permission.trim();
1469
+ if (!normalized) return null;
1470
+ if (normalized === "*" || normalized === "*:*" || normalized === "all:manage") {
1471
+ return { action: "manage", subject: "all" };
1472
+ }
1473
+ const separator = normalized.lastIndexOf(":");
1474
+ if (separator <= 0 || separator === normalized.length - 1) return null;
1475
+ const subject = normalized.slice(0, separator);
1476
+ const action = normalized.slice(separator + 1);
1477
+ return {
1478
+ action: action === "*" ? "manage" : action,
1479
+ subject: subject === "*" ? "all" : subject
1480
+ };
1481
+ }
1482
+ function matchesToken(value, expected, wildcard) {
1483
+ return (Array.isArray(value) ? value : [value]).some(
1484
+ (candidate) => candidate === expected || candidate === wildcard || candidate === "*" || matchesSegments(candidate, expected)
1485
+ );
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
+ }
1492
+ function matchesField(pattern, field) {
1493
+ if (pattern === "*" || pattern === field) return true;
1494
+ return pattern.endsWith(".*") && field.startsWith(pattern.slice(0, -1));
1495
+ }
1496
+ function matchesConditions(subject, conditions) {
1497
+ return Object.entries(conditions).every(([path, expected]) => {
1498
+ if (path === "$and" && Array.isArray(expected)) {
1499
+ return expected.every(
1500
+ (condition) => matchesConditions(subject, condition)
1501
+ );
1502
+ }
1503
+ if (path === "$or" && Array.isArray(expected)) {
1504
+ return expected.some(
1505
+ (condition) => matchesConditions(subject, condition)
1506
+ );
1507
+ }
1508
+ return matchesValue(readPath(subject, path), expected);
1509
+ });
1510
+ }
1511
+ function matchesValue(actual, expected) {
1512
+ if (!isRecord2(expected) || !Object.keys(expected).some((key) => key.startsWith("$"))) {
1513
+ return isRecord2(expected) && isRecord2(actual) ? matchesConditions(actual, expected) : Object.is(actual, expected);
1514
+ }
1515
+ return Object.entries(expected).every(([operator, operand]) => {
1516
+ switch (operator) {
1517
+ case "$eq":
1518
+ return Object.is(actual, operand);
1519
+ case "$ne":
1520
+ return !Object.is(actual, operand);
1521
+ case "$in":
1522
+ return Array.isArray(operand) && operand.some((value) => Object.is(actual, value));
1523
+ case "$nin":
1524
+ return Array.isArray(operand) && !operand.some((value) => Object.is(actual, value));
1525
+ case "$gt":
1526
+ return typeof actual === "number" && typeof operand === "number" && actual > operand;
1527
+ case "$gte":
1528
+ return typeof actual === "number" && typeof operand === "number" && actual >= operand;
1529
+ case "$lt":
1530
+ return typeof actual === "number" && typeof operand === "number" && actual < operand;
1531
+ case "$lte":
1532
+ return typeof actual === "number" && typeof operand === "number" && actual <= operand;
1533
+ case "$exists":
1534
+ return operand ? actual !== void 0 : actual === void 0;
1535
+ default:
1536
+ return false;
1537
+ }
1538
+ });
1539
+ }
1540
+ function readPath(value, path) {
1541
+ return path.split(".").reduce((current, part) => isRecord2(current) ? current[part] : void 0, value);
1542
+ }
1543
+ function defaultSubjectType(subject) {
1544
+ const explicit = subject.__type ?? subject.type ?? subject.kind;
1545
+ if (typeof explicit === "string") return explicit;
1546
+ const constructorName = subject.constructor?.name;
1547
+ return typeof constructorName === "string" ? constructorName : "Object";
1548
+ }
1549
+ function cloneRule(rule) {
1550
+ return {
1551
+ ...rule,
1552
+ action: Array.isArray(rule.action) ? [...rule.action] : rule.action,
1553
+ subject: Array.isArray(rule.subject) ? [...rule.subject] : rule.subject,
1554
+ fields: rule.fields ? [...rule.fields] : void 0
1555
+ };
1556
+ }
1557
+ function isRecord2(value) {
1558
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1559
+ }
1560
+ function isAbilityRule(value) {
1561
+ return value !== null;
1562
+ }
1563
+
1564
+ // src/integrations/authorization.ts
1565
+ var AuthorizationError = class extends Error {
1566
+ constructor(status, code, message) {
1567
+ super(message);
1568
+ this.status = status;
1569
+ this.code = code;
1570
+ this.name = "AuthorizationError";
1571
+ }
1572
+ };
1573
+ async function authorizeToken(client, token, options = {}) {
1574
+ if (!token) throw unauthenticated();
1575
+ let userId;
1576
+ let permissions = [];
1577
+ let roles = [];
1578
+ let scope;
1579
+ if (options.verification === "jwks") {
1580
+ if (!options.jwksVerifier) {
1581
+ throw new Error("Authyon: `jwksVerifier` is required for JWKS verification");
1582
+ }
1583
+ let verified;
1584
+ try {
1585
+ verified = await options.jwksVerifier.verifyAccessToken(token);
1586
+ } catch (error) {
1587
+ if (!(error instanceof TokenVerificationError)) throw error;
1588
+ if (error.code === "token.key_unavailable") {
1589
+ throw new AuthorizationError(
1590
+ 503,
1591
+ "authorization.unavailable",
1592
+ "Token verification is temporarily unavailable"
1593
+ );
1594
+ }
1595
+ throw unauthenticated();
1596
+ }
1597
+ const { claims } = verified;
1598
+ userId = claims.sub;
1599
+ permissions = claims.permissions ?? [];
1600
+ roles = claims.roles ?? [];
1601
+ scope = claims.scope;
1602
+ } else if (options.verification === "introspect") {
1603
+ const identity = await client.introspect(token, { clientIp: options.clientIp });
1604
+ if (!identity.active) throw unauthenticated();
1605
+ userId = identity.sub;
1606
+ permissions = identity.permissions ?? [];
1607
+ roles = identity.roles ?? [];
1608
+ scope = identity.scope;
1609
+ } else {
1610
+ const identity = await client.validate(token, { clientIp: options.clientIp });
1611
+ if (!identity.valid || !identity.user) throw unauthenticated();
1612
+ userId = identity.user.id;
1613
+ permissions = identity.user.permissions ?? [];
1614
+ }
1615
+ const ability = createAuthyonAbility(
1616
+ { permissions, roles, scope },
1617
+ {
1618
+ rules: options.rules,
1619
+ roleRules: options.roleRules,
1620
+ roles: options.roles,
1621
+ detectSubjectType: options.detectSubjectType
1622
+ }
1623
+ );
1624
+ if (options.requirement && !canAccess(ability, options.requirement)) {
1625
+ throw new AuthorizationError(403, "authorization.forbidden", "Permission denied");
1626
+ }
1627
+ return { token, userId, ability, permissions, roles };
1628
+ }
1629
+ function authorizeRequest(client, request, options = {}) {
1630
+ const clientIp = options.resolveClientIp?.(request) ?? options.clientIp;
1631
+ return authorizeToken(client, bearerToken(request.headers.get("authorization")), {
1632
+ ...options,
1633
+ clientIp
1634
+ });
1635
+ }
1636
+ function createExpressAuthorizationMiddleware(client, options = {}) {
1637
+ return async (request, response, next) => {
1638
+ try {
1639
+ const header = request.headers.authorization;
1640
+ const clientIp = resolveExpressClientIp(request, options);
1641
+ const context = await authorizeToken(
1642
+ client,
1643
+ bearerToken(Array.isArray(header) ? header[0] : header),
1644
+ { ...options, clientIp }
1645
+ );
1646
+ request[options.requestProperty ?? "authyon"] = context;
1647
+ next();
1648
+ } catch (error) {
1649
+ if (options.passErrorsToNext || !(error instanceof AuthorizationError)) {
1650
+ next(error);
1651
+ return;
1652
+ }
1653
+ response.status(error.status).json({ code: error.code, message: error.message });
1654
+ }
1655
+ };
1656
+ }
1657
+ function resolveExpressClientIp(request, options) {
1658
+ const resolved = options.resolveClientIp?.(request) ?? (options.trustProxy ? request.ip : request.socket?.remoteAddress);
1659
+ return resolved ? normalizeClientIp(stripIpv4Prefix(resolved)) : void 0;
1660
+ }
1661
+ function stripIpv4Prefix(value) {
1662
+ return value.startsWith("::ffff:") ? value.slice(7) : value;
1663
+ }
1664
+ function createAuthorizationErrorResponse(error) {
1665
+ if (!(error instanceof AuthorizationError)) throw error;
1666
+ return Response.json({ code: error.code, message: error.message }, { status: error.status });
1667
+ }
1668
+ function canAccess(ability, requirement) {
1669
+ return ability.can(requirement.action, requirement.subject, requirement.field);
1670
+ }
1671
+ function bearerToken(header) {
1672
+ const match = header?.match(/^Bearer\s+(.+)$/i);
1673
+ if (!match?.[1]) throw unauthenticated();
1674
+ return match[1].trim();
1675
+ }
1676
+ function unauthenticated() {
1677
+ return new AuthorizationError(
1678
+ 401,
1679
+ "authorization.unauthenticated",
1680
+ "A valid bearer token is required"
1681
+ );
1682
+ }
501
1683
  // Annotate the CommonJS export names for ESM import in node:
502
1684
  0 && (module.exports = {
1685
+ AuthorizationError,
1686
+ AuthyonAbility,
1687
+ AuthyonAbilityBuilder,
503
1688
  AuthyonError,
504
1689
  AuthyonServerClient,
1690
+ AuthyonServerClientBuilder,
1691
+ ErrorCodes,
1692
+ FetchHttpAdapter,
1693
+ JwksTokenVerifier,
1694
+ LoggingHttpAdapter,
1695
+ PlatformScopedClient,
505
1696
  TenantScopedClient,
506
- createClient
1697
+ TokenVerificationError,
1698
+ UserScopedClient,
1699
+ WorkspaceInvitesClient,
1700
+ authorizeRequest,
1701
+ authorizeToken,
1702
+ createAuthorizationErrorResponse,
1703
+ createAuthyonAbility,
1704
+ createAuthyonRules,
1705
+ createClient,
1706
+ createExpressAuthorizationMiddleware,
1707
+ hasPermission,
1708
+ hasPermissionGroup,
1709
+ normalizeClientIp
507
1710
  });