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

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,32 +1,299 @@
1
+ import {
2
+ AuthorizationError,
3
+ AuthyonAbility,
4
+ AuthyonAbilityBuilder,
5
+ JwksTokenVerifier,
6
+ TokenVerificationError,
7
+ __privateAdd,
8
+ __privateGet,
9
+ __privateSet,
10
+ authorizeRequest,
11
+ authorizeToken,
12
+ clientIpHeaders,
13
+ createAuthorizationErrorResponse,
14
+ createAuthyonAbility,
15
+ createAuthyonRules,
16
+ createExpressAuthorizationMiddleware,
17
+ hasPermission,
18
+ normalizeClientIp
19
+ } from "./chunk-24IFDJ73.js";
20
+
21
+ // src/client/credentialManagement.ts
22
+ function segment(value) {
23
+ if (typeof value !== "string" || !value.trim() || value === "." || value === ".." || [...value].some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127)) {
24
+ throw new TypeError("Authyon: invalid route identifier.");
25
+ }
26
+ return encodeURIComponent(value);
27
+ }
28
+ function permissionsBody(input) {
29
+ if (input.permissions !== void 0 && input.scopes !== void 0) {
30
+ throw new TypeError("Authyon: supply permissions or scopes, not both.");
31
+ }
32
+ const values = input.permissions ?? input.scopes;
33
+ if (!Array.isArray(values) || values.length === 0) {
34
+ throw new TypeError("Authyon: explicit credential permissions are required.");
35
+ }
36
+ const permissions = values.map((value) => {
37
+ if (typeof value !== "string") throw new TypeError("Authyon: invalid permission.");
38
+ const normalized = value.trim().toLowerCase();
39
+ if (!/^(?:\*|[a-z][a-z0-9_-]*):(?:\*|[a-z][a-z0-9_-]*):(?:\*|[a-z][a-z0-9_-]*)$/u.test(normalized)) {
40
+ throw new TypeError("Authyon: permissions must have namespace:resource:action format.");
41
+ }
42
+ return normalized;
43
+ });
44
+ return { permissions: [...new Set(permissions)] };
45
+ }
46
+ function lifetimeBody(input) {
47
+ if (input.lifetimeDays === void 0 || input.lifetimeDays === null) return {};
48
+ if (!Number.isInteger(input.lifetimeDays) || input.lifetimeDays < 1 || input.lifetimeDays > 3650) {
49
+ throw new RangeError("Authyon: lifetimeDays must be between 1 and 3650.");
50
+ }
51
+ return { lifetimeDays: input.lifetimeDays };
52
+ }
53
+ function credentialQuery(options = {}) {
54
+ const { skip, take, search } = options;
55
+ if (skip !== void 0 && (!Number.isInteger(skip) || skip < 0 || skip > 2147483647))
56
+ throw new RangeError("Authyon: invalid skip.");
57
+ if (take !== void 0 && (!Number.isInteger(take) || take < 1 || take > 100))
58
+ throw new RangeError("Authyon: take must be between 1 and 100.");
59
+ if (search !== void 0 && (typeof search !== "string" || search.trim().length > 200))
60
+ throw new RangeError("Authyon: search must contain at most 200 characters.");
61
+ return { search: search?.trim() || void 0, skip, take };
62
+ }
63
+ function credentialSummary(value) {
64
+ return {
65
+ id: value.id,
66
+ clientId: value.clientId,
67
+ description: value.description,
68
+ createdAt: value.createdAt,
69
+ lastUsedAt: value.lastUsedAt,
70
+ revokedAt: value.revokedAt,
71
+ expiresAt: value.expiresAt ?? null,
72
+ isActive: value.isActive,
73
+ permissions: [...value.permissions]
74
+ };
75
+ }
76
+ function credentialDetail(value) {
77
+ return {
78
+ ...credentialSummary(value),
79
+ createdBy: value.createdBy == null ? null : {
80
+ id: value.createdBy.id,
81
+ type: value.createdBy.type,
82
+ displayName: value.createdBy.displayName
83
+ },
84
+ updatedAt: value.updatedAt,
85
+ secretRotatedAt: value.secretRotatedAt,
86
+ expiresAt: value.expiresAt ?? null,
87
+ ageSeconds: value.ageSeconds,
88
+ lifetimeSeconds: value.lifetimeSeconds,
89
+ accessTokenLifetimeSeconds: value.accessTokenLifetimeSeconds
90
+ };
91
+ }
92
+ async function credentialPage(request, path, options = {}) {
93
+ const query = credentialQuery(options);
94
+ const page = await request(path, { query });
95
+ if (!page || !Array.isArray(page.data))
96
+ throw new TypeError("Authyon: expected a paginated credential response.");
97
+ return {
98
+ data: page.data.map(credentialSummary),
99
+ ...page.perPage === void 0 ? {} : { perPage: page.perPage },
100
+ pageSize: page.pageSize,
101
+ total: page.total,
102
+ pages: page.pages,
103
+ hasNext: page.hasNext,
104
+ hasPrev: page.hasPrev
105
+ };
106
+ }
107
+ async function allCredentials(request, path) {
108
+ const result = [];
109
+ const seen = /* @__PURE__ */ new Set();
110
+ let skip = 0;
111
+ for (let iteration = 0; iteration < 1e3; iteration++) {
112
+ const page = await request(
113
+ path,
114
+ iteration === 0 ? {} : { query: { skip, take: 100 } }
115
+ );
116
+ if (Array.isArray(page)) {
117
+ if (iteration !== 0)
118
+ throw new Error("Authyon: credential pagination changed during listing.");
119
+ return page.map(credentialSummary);
120
+ }
121
+ if (!page || !Array.isArray(page.data))
122
+ throw new TypeError("Authyon: invalid credential page.");
123
+ for (const row of page.data) {
124
+ if (seen.has(row.id))
125
+ throw new Error("Authyon: credential pages overlapped; retry the listing.");
126
+ seen.add(row.id);
127
+ result.push(credentialSummary(row));
128
+ }
129
+ if (!page.hasNext) return result;
130
+ if (page.data.length === 0) throw new Error("Authyon: credential pagination made no progress.");
131
+ skip += page.data.length;
132
+ }
133
+ throw new Error("Authyon: use listPage for more than 1000 credential pages.");
134
+ }
135
+
136
+ // src/client/scopedManagementClient.ts
137
+ function scopedRequest(request, source) {
138
+ return async (path, options = {}) => {
139
+ const token = typeof source === "function" ? await source() : source;
140
+ if (typeof token !== "string" || !token.trim() || /\s/u.test(token))
141
+ throw new TypeError("Authyon: a valid access token is required.");
142
+ return request(path, {
143
+ ...options,
144
+ headers: { ...options.headers, Authorization: `Bearer ${token}` }
145
+ });
146
+ };
147
+ }
148
+ function environmentPath(scope) {
149
+ return `/platform/workspaces/${segment(scope.workspaceId)}/environments/${segment(scope.environmentId)}`;
150
+ }
151
+ function tenantPath(scope) {
152
+ return `${environmentPath(scope)}/tenants/${segment(scope.tenantId)}`;
153
+ }
154
+ function credentialPath(scope) {
155
+ return `${scope.tenantId === void 0 ? environmentPath(scope) : tenantPath(scope)}/credentials`;
156
+ }
157
+ function workspacePath(id) {
158
+ return `/platform/workspaces/${segment(id)}`;
159
+ }
160
+ function workspaceRole(role) {
161
+ if (role !== "admin" && role !== "auditor")
162
+ throw new TypeError("Authyon: workspace role must be admin or auditor.");
163
+ return role;
164
+ }
165
+ var _request;
166
+ var PlatformScopedClient = class {
167
+ /** @hidden */
168
+ constructor(request, token) {
169
+ __privateAdd(this, _request);
170
+ this.credentials = {
171
+ list: (scope, options = {}) => credentialPage(__privateGet(this, _request), credentialPath(scope), options),
172
+ get: async (scope, credentialId) => credentialDetail(await __privateGet(this, _request).call(this, `${credentialPath(scope)}/${segment(credentialId)}`)),
173
+ create: (scope, input) => __privateGet(this, _request).call(this, credentialPath(scope), {
174
+ method: "POST",
175
+ body: { description: input.description, ...lifetimeBody(input), ...permissionsBody(input) }
176
+ }),
177
+ updatePermissions: (scope, credentialId, input) => __privateGet(this, _request).call(this, `${credentialPath(scope)}/${segment(credentialId)}/permissions`, {
178
+ method: "PUT",
179
+ body: permissionsBody(input)
180
+ }),
181
+ updateScopes: (scope, credentialId, scopes) => this.credentials.updatePermissions(scope, credentialId, { scopes }),
182
+ /** Returns a new secret once. API may require an explicit step-up first. */
183
+ rotate: (scope, credentialId) => __privateGet(this, _request).call(this, `${credentialPath(scope)}/${segment(credentialId)}/rotate`, { method: "POST" }),
184
+ revoke: (scope, credentialId) => __privateGet(this, _request).call(this, `${credentialPath(scope)}/${segment(credentialId)}`, { method: "DELETE" })
185
+ };
186
+ this.workspaces = {
187
+ team: (workspaceId) => __privateGet(this, _request).call(this, `${workspacePath(workspaceId)}/team`),
188
+ members: {
189
+ invite: (workspaceId, input) => __privateGet(this, _request).call(this, `${workspacePath(workspaceId)}/invites`, {
190
+ method: "POST",
191
+ body: { email: input.email, role: workspaceRole(input.role) }
192
+ }),
193
+ /** memberId comes from team.members; it is not the platform userId. */
194
+ remove: (workspaceId, memberId) => __privateGet(this, _request).call(this, `${workspacePath(workspaceId)}/members/${segment(memberId)}`, {
195
+ method: "DELETE"
196
+ }),
197
+ changeRole: (workspaceId, memberId, role) => __privateGet(this, _request).call(this, `${workspacePath(workspaceId)}/members/${segment(memberId)}`, {
198
+ method: "PATCH",
199
+ body: { role: workspaceRole(role) }
200
+ })
201
+ },
202
+ invites: {
203
+ create: (workspaceId, input) => this.workspaces.members.invite(workspaceId, input),
204
+ revoke: (workspaceId, inviteId) => __privateGet(this, _request).call(this, `${workspacePath(workspaceId)}/invites/${segment(inviteId)}`, {
205
+ method: "DELETE"
206
+ })
207
+ }
208
+ };
209
+ this.tenants = {
210
+ members: {
211
+ list: (scope, options = {}) => __privateGet(this, _request).call(this, `${tenantPath(scope)}/members`, { query: options }),
212
+ add: (scope, userId, roles) => __privateGet(this, _request).call(this, `${tenantPath(scope)}/members`, { method: "POST", body: { userId, roles } }),
213
+ remove: (scope, userId) => __privateGet(this, _request).call(this, `${tenantPath(scope)}/members/${segment(userId)}`, { method: "DELETE" })
214
+ }
215
+ };
216
+ this.auth = {
217
+ /** Explicitly confirms the current platform session; passwords are not retained. */
218
+ stepUp: (currentPassword) => __privateGet(this, _request).call(this, "/platform/auth/step-up", { method: "POST", body: { currentPassword } })
219
+ };
220
+ __privateSet(this, _request, scopedRequest(request, token));
221
+ }
222
+ };
223
+ _request = new WeakMap();
224
+ var _request2;
225
+ var UserScopedClient = class {
226
+ /** @hidden */
227
+ constructor(request, token) {
228
+ __privateAdd(this, _request2);
229
+ this.tenants = {
230
+ members: {
231
+ list: (tenantId, options = {}) => __privateGet(this, _request2).call(this, `/auth/tenants/${segment(tenantId)}/members`, { query: options }),
232
+ /** Adds an existing environment user by email; does not send an invitation email. */
233
+ invite: (tenantId, input) => __privateGet(this, _request2).call(this, `/auth/tenants/${segment(tenantId)}/members`, {
234
+ method: "POST",
235
+ body: { email: input.email, roles: input.roles }
236
+ }),
237
+ remove: (tenantId, userId) => __privateGet(this, _request2).call(this, `/auth/tenants/${segment(tenantId)}/members/${segment(userId)}`, {
238
+ method: "DELETE"
239
+ })
240
+ }
241
+ };
242
+ __privateSet(this, _request2, scopedRequest(request, token));
243
+ }
244
+ };
245
+ _request2 = new WeakMap();
246
+ var _request3;
247
+ var WorkspaceInvitesClient = class {
248
+ /** @hidden */
249
+ constructor(request) {
250
+ __privateAdd(this, _request3);
251
+ __privateSet(this, _request3, request);
252
+ }
253
+ preview(token) {
254
+ return __privateGet(this, _request3).call(this, `/platform/workspace-invites/${segment(token)}`);
255
+ }
256
+ accept(token, input = {}) {
257
+ return __privateGet(this, _request3).call(this, `/platform/workspace-invites/${segment(token)}/accept`, {
258
+ method: "POST",
259
+ body: { name: input.name, password: input.password }
260
+ });
261
+ }
262
+ };
263
+ _request3 = new WeakMap();
264
+
1
265
  // ../../internal/core/config/defaults.ts
2
266
  var DEFAULT_BASE_URL = "https://api.authyon.com";
3
267
  var DEFAULT_EXPIRY_SKEW_MS = 3e4;
4
268
 
5
269
  // ../../internal/core/auth/expiringTokenProvider.ts
270
+ var _token, _inFlight;
6
271
  var ExpiringTokenProvider = class {
7
272
  constructor(acquire, expirySkewMs = DEFAULT_EXPIRY_SKEW_MS) {
8
273
  this.acquire = acquire;
9
274
  this.expirySkewMs = expirySkewMs;
10
- this.token = null;
11
- this.inFlight = null;
275
+ __privateAdd(this, _token, null);
276
+ __privateAdd(this, _inFlight, null);
12
277
  }
13
278
  async getAccessToken() {
14
- if (this.token && Date.now() < this.token.expiresAt - this.expirySkewMs) {
15
- return this.token.accessToken;
279
+ if (__privateGet(this, _token) && Date.now() < __privateGet(this, _token).expiresAt - this.expirySkewMs) {
280
+ return __privateGet(this, _token).accessToken;
16
281
  }
17
- if (this.inFlight) return this.inFlight;
18
- this.inFlight = this.acquire().then((token) => {
19
- this.token = token;
282
+ if (__privateGet(this, _inFlight)) return __privateGet(this, _inFlight);
283
+ __privateSet(this, _inFlight, this.acquire().then((token) => {
284
+ __privateSet(this, _token, token);
20
285
  return token.accessToken;
21
286
  }).finally(() => {
22
- this.inFlight = null;
23
- });
24
- return this.inFlight;
287
+ __privateSet(this, _inFlight, null);
288
+ }));
289
+ return __privateGet(this, _inFlight);
25
290
  }
26
291
  clear() {
27
- this.token = null;
292
+ __privateSet(this, _token, null);
28
293
  }
29
294
  };
295
+ _token = new WeakMap();
296
+ _inFlight = new WeakMap();
30
297
 
31
298
  // ../../internal/core/errors/authyonError.ts
32
299
  var ErrorCodes = {
@@ -157,7 +424,8 @@ var FetchHttpAdapter = class {
157
424
  method: request.method,
158
425
  headers: request.headers,
159
426
  body: request.body,
160
- signal: request.signal
427
+ signal: request.signal,
428
+ redirect: "error"
161
429
  });
162
430
  }
163
431
  };
@@ -209,6 +477,10 @@ function defaultHttpLogger(event) {
209
477
  }
210
478
  function sanitizeUrl(value) {
211
479
  const url = new URL(value);
480
+ url.username = "";
481
+ url.password = "";
482
+ url.hash = "";
483
+ url.pathname = url.pathname.replace(/(\/platform\/workspace-invites\/)[^/]+/u, "$1REDACTED");
212
484
  const queryKeys = /* @__PURE__ */ new Set();
213
485
  url.searchParams.forEach((_value, key) => queryKeys.add(key));
214
486
  url.search = queryKeys.size > 0 ? [...queryKeys].map((key) => `${encodeURIComponent(key)}=REDACTED`).join("&") : "";
@@ -332,170 +604,11 @@ var JsonHttpClient = class {
332
604
  }
333
605
  };
334
606
 
335
- // ../../internal/core/http/clientIp.ts
336
- function clientIpHeaders(context) {
337
- if (!context?.clientIp) return {};
338
- return { "X-Forwarded-For": normalizeClientIp(context.clientIp) };
339
- }
340
- function normalizeClientIp(value) {
341
- let ip = value.trim();
342
- if (ip.startsWith("[") && ip.endsWith("]")) ip = ip.slice(1, -1);
343
- if (!ip || ip.length > 45 || ip.includes(",") || /[\r\n\s]/.test(ip) || !/^[0-9a-f:.]+$/i.test(ip) || !ip.includes(".") && !ip.includes(":")) {
344
- throw invalidClientIp();
345
- }
346
- try {
347
- new URL(ip.includes(":") ? `http://[${ip}]` : `http://${ip}`);
348
- } catch {
349
- throw invalidClientIp();
350
- }
351
- return ip;
352
- }
353
- function invalidClientIp() {
354
- return new Error("Authyon: `clientIp` must be a single valid IPv4 or IPv6 address");
355
- }
356
-
357
- // src/security/jwksTokenVerifier.ts
358
- import {
359
- createLocalJWKSet,
360
- createRemoteJWKSet,
361
- jwtVerify
362
- } from "jose";
363
- var SAFE_ASYMMETRIC_ALGORITHMS = [
364
- "RS256",
365
- "PS256",
366
- "ES256",
367
- "EdDSA"
368
- ];
369
- var TokenVerificationError = class extends Error {
370
- constructor(code, options = {}) {
371
- super(messageForCode(code));
372
- this.code = code;
373
- this.name = "TokenVerificationError";
374
- if (options.cause !== void 0) Object.defineProperty(this, "cause", { value: options.cause });
375
- }
376
- };
377
- var JwksTokenVerifier = class {
378
- constructor(options) {
379
- this.options = options;
380
- validateOptions(options);
381
- this.algorithms = [...options.algorithms ?? ["RS256"]];
382
- this.acceptedTokenTypes = new Set(options.acceptedTokenTypes ?? ["JWT", "at+jwt"]);
383
- this.verifyKey = options.jwks ? createLocalJWKSet(options.jwks) : createRemoteJWKSet(new URL(options.jwksUri), {
384
- timeoutDuration: options.jwksTimeoutMs ?? 5e3,
385
- cacheMaxAge: options.jwksCacheMs ?? 6e5,
386
- cooldownDuration: options.jwksCooldownMs ?? 3e4
387
- });
388
- }
389
- async verifyAccessToken(token) {
390
- if (!token || token.length > 16384) {
391
- throw new TokenVerificationError("token.invalid");
392
- }
393
- try {
394
- const result = await jwtVerify(token, this.verifyKey, {
395
- algorithms: this.algorithms,
396
- issuer: this.options.issuer,
397
- audience: this.options.audience,
398
- clockTolerance: this.options.clockToleranceSeconds ?? 5,
399
- maxTokenAge: this.options.maxTokenAgeSeconds ?? 3600,
400
- requiredClaims: ["iss", "aud", "sub", "exp", "iat"]
401
- });
402
- validateTokenType(result, this.acceptedTokenTypes);
403
- validateClaims(result.payload);
404
- return { claims: result.payload, protectedHeader: result.protectedHeader };
405
- } catch (cause) {
406
- if (cause instanceof TokenVerificationError) throw cause;
407
- throw new TokenVerificationError(errorCode(cause), { cause });
408
- }
409
- }
410
- };
411
- function validateTokenType(result, acceptedTokenTypes) {
412
- const type = result.protectedHeader.typ;
413
- if (!type || !acceptedTokenTypes.has(type)) {
414
- throw new TokenVerificationError("token.invalid_claims");
415
- }
416
- }
417
- function validateClaims(claims) {
418
- if (typeof claims.sub !== "string" || !claims.sub) invalidClaims();
419
- if (claims.permissions !== void 0 && !isStringArray(claims.permissions)) invalidClaims();
420
- if (claims.roles !== void 0 && !isStringArray(claims.roles)) invalidClaims();
421
- if (claims.scope !== void 0 && typeof claims.scope !== "string") invalidClaims();
422
- }
423
- function validateOptions(options) {
424
- const issuer = secureUrl(options.issuer, "issuer");
425
- if (Boolean(options.jwksUri) === Boolean(options.jwks)) {
426
- throw new Error("Authyon: configure exactly one of `jwksUri` or `jwks`");
427
- }
428
- const jwks = options.jwksUri ? secureUrl(options.jwksUri, "jwksUri") : void 0;
429
- if (!options.audience || Array.isArray(options.audience) && options.audience.length === 0) {
430
- throw new Error("Authyon: JWT `audience` is required");
431
- }
432
- if (jwks && issuer.origin !== jwks.origin) {
433
- throw new Error("Authyon: `issuer` and `jwksUri` must use the same trusted origin");
434
- }
435
- const algorithms = options.algorithms ?? ["RS256"];
436
- if (algorithms.length === 0 || algorithms.some((algorithm) => !SAFE_ASYMMETRIC_ALGORITHMS.includes(algorithm))) {
437
- throw new Error("Authyon: only approved asymmetric JWT algorithms are accepted");
438
- }
439
- if (options.acceptedTokenTypes?.length === 0 || options.acceptedTokenTypes?.some((type) => !type.trim())) {
440
- throw new Error("Authyon: `acceptedTokenTypes` must contain non-empty values");
441
- }
442
- for (const [name, value] of [
443
- ["clockToleranceSeconds", options.clockToleranceSeconds],
444
- ["maxTokenAgeSeconds", options.maxTokenAgeSeconds],
445
- ["jwksTimeoutMs", options.jwksTimeoutMs],
446
- ["jwksCacheMs", options.jwksCacheMs],
447
- ["jwksCooldownMs", options.jwksCooldownMs]
448
- ]) {
449
- if (value !== void 0 && (!Number.isFinite(value) || value < 0)) {
450
- throw new Error(`Authyon: \`${name}\` must be a non-negative finite number`);
451
- }
452
- }
453
- }
454
- function secureUrl(value, name) {
455
- let url;
456
- try {
457
- url = new URL(value);
458
- } catch {
459
- throw new Error(`Authyon: JWT \`${name}\` must be an absolute URL`);
460
- }
461
- if (url.protocol !== "https:" || url.username || url.password || url.hash) {
462
- throw new Error(`Authyon: JWT \`${name}\` must be a credential-free HTTPS URL`);
463
- }
464
- return url;
465
- }
466
- function errorCode(error) {
467
- const code = isRecord(error) && typeof error.code === "string" ? error.code : "";
468
- if (code === "ERR_JWT_EXPIRED") return "token.expired";
469
- if (code === "ERR_JWS_SIGNATURE_VERIFICATION_FAILED") return "token.invalid_signature";
470
- if (code === "ERR_JWKS_TIMEOUT") {
471
- return "token.key_unavailable";
472
- }
473
- if (code === "ERR_JWKS_NO_MATCHING_KEY") return "token.invalid_signature";
474
- if (code === "ERR_JWT_CLAIM_VALIDATION_FAILED") {
475
- const claim = isRecord(error) && typeof error.claim === "string" ? error.claim : "";
476
- return claim === "nbf" ? "token.not_active" : "token.invalid_claims";
477
- }
478
- return "token.invalid";
479
- }
480
- function messageForCode(code) {
481
- if (code === "token.key_unavailable") return "Token verification key is unavailable";
482
- if (code === "token.expired") return "Token has expired";
483
- if (code === "token.not_active") return "Token is not active yet";
484
- return "Token verification failed";
485
- }
486
- function invalidClaims() {
487
- throw new TokenVerificationError("token.invalid_claims");
488
- }
489
- function isStringArray(value) {
490
- return Array.isArray(value) && value.every((item) => typeof item === "string");
491
- }
492
- function isRecord(value) {
493
- return typeof value === "object" && value !== null;
494
- }
495
-
496
607
  // src/client/authyonServerClient.ts
608
+ var _clientSecret;
497
609
  var AuthyonServerClient = class {
498
610
  constructor(options = {}) {
611
+ __privateAdd(this, _clientSecret);
499
612
  // ── Discovery ──────────────────────────────────────────────────────────────
500
613
  this.discovery = {
501
614
  /** GET /.well-known/jwks.json — the environment's public signing keys. */
@@ -531,6 +644,9 @@ var AuthyonServerClient = class {
531
644
  }
532
645
  })
533
646
  };
647
+ this.workspaceInvites = new WorkspaceInvitesClient(
648
+ (path, options) => this.http.request(path, options)
649
+ );
534
650
  // ── Environment management (users, tenants, roles, permissions, audit) ───
535
651
  this.environment = {
536
652
  users: {
@@ -622,6 +738,47 @@ var AuthyonServerClient = class {
622
738
  )
623
739
  },
624
740
  tenants: {
741
+ credentials: {
742
+ /** GET /env/tenants/{tenantId}/credentials. Returns metadata only, including revoked credentials. */
743
+ list: (tenantId) => allCredentials(
744
+ (path, options) => this.request(path, { ...options, envBearer: true }),
745
+ `/env/tenants/${segment(tenantId)}/credentials`
746
+ ),
747
+ /** Paginated searchable listing. Prefer this over loading every credential. */
748
+ listPage: (tenantId, options = {}) => credentialPage(
749
+ (path, requestOptions) => this.request(path, { ...requestOptions, envBearer: true }),
750
+ `/env/tenants/${segment(tenantId)}/credentials`,
751
+ options
752
+ ),
753
+ get: async (tenantId, credentialId) => credentialDetail(
754
+ await this.request(
755
+ `/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}`,
756
+ { envBearer: true }
757
+ )
758
+ ),
759
+ /** Explicit scopes/permissions; only issuance returns the client secret. */
760
+ create: (tenantId, input) => this.request(`/env/tenants/${segment(tenantId)}/credentials`, {
761
+ method: "POST",
762
+ envBearer: true,
763
+ body: { description: input.description, ...lifetimeBody(input), ...permissionsBody(input) }
764
+ }),
765
+ /** Returns a new secret once while keeping the same client id. */
766
+ rotate: (tenantId, credentialId) => this.request(
767
+ `/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}/rotate`,
768
+ { method: "POST", envBearer: true }
769
+ ),
770
+ updatePermissions: (tenantId, credentialId, input) => this.request(
771
+ `/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}/permissions`,
772
+ { method: "PUT", envBearer: true, body: permissionsBody(input) }
773
+ ),
774
+ updateScopes: (tenantId, credentialId, scopes) => this.environment.tenants.credentials.updatePermissions(tenantId, credentialId, {
775
+ scopes
776
+ }),
777
+ revoke: (tenantId, credentialId) => this.request(`/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}`, {
778
+ method: "DELETE",
779
+ envBearer: true
780
+ })
781
+ },
625
782
  /** GET /env/tenants — paginated, searchable list of tenants in the environment. */
626
783
  list: (params = {}) => this.request("/env/tenants", { envBearer: true, query: params }),
627
784
  /** POST /env/tenants — create a tenant in the environment. */
@@ -768,7 +925,7 @@ var AuthyonServerClient = class {
768
925
  };
769
926
  this.envKey = options.envKey;
770
927
  this.clientId = options.clientId;
771
- this.clientSecret = options.clientSecret;
928
+ __privateSet(this, _clientSecret, options.clientSecret);
772
929
  this.transport = createSharedTransport({
773
930
  baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
774
931
  allowInsecureHttp: options.allowInsecureHttp,
@@ -779,14 +936,14 @@ var AuthyonServerClient = class {
779
936
  });
780
937
  this.http = new JsonHttpClient(this.transport);
781
938
  this.environmentTokenProvider = new ExpiringTokenProvider(async () => {
782
- if (!this.clientId || !this.clientSecret) {
939
+ if (!this.clientId || !__privateGet(this, _clientSecret)) {
783
940
  throw new Error(
784
941
  "Authyon: `clientId`/`clientSecret` are required for environment management calls (mint a pair in the console, under the environment's OAuth clients)."
785
942
  );
786
943
  }
787
944
  const token = await this.environmentAuth.token({
788
945
  clientId: this.clientId,
789
- clientSecret: this.clientSecret
946
+ clientSecret: __privateGet(this, _clientSecret)
790
947
  });
791
948
  return {
792
949
  accessToken: token.access_token,
@@ -855,6 +1012,14 @@ var AuthyonServerClient = class {
855
1012
  tenant(credentials) {
856
1013
  return new TenantScopedClient(this, credentials);
857
1014
  }
1015
+ /** Uses only the supplied platform-user access token. */
1016
+ platform(token) {
1017
+ return new PlatformScopedClient((path, options) => this.http.request(path, options), token);
1018
+ }
1019
+ /** Uses an end-user token; the configured environment key selects the environment. */
1020
+ user(token) {
1021
+ return new UserScopedClient((path, options) => this.request(path, options), token);
1022
+ }
858
1023
  /** @internal used by {@link TenantScopedClient} to share the base URL/fetch/error handling. */
859
1024
  _requestAsTenant(accessToken, path, options = {}) {
860
1025
  return this.request(path, {
@@ -864,6 +1029,7 @@ var AuthyonServerClient = class {
864
1029
  });
865
1030
  }
866
1031
  };
1032
+ _clientSecret = new WeakMap();
867
1033
  var TenantScopedClient = class {
868
1034
  constructor(server, credentials) {
869
1035
  this.members = {
@@ -942,342 +1108,42 @@ function createClient(options = {}) {
942
1108
  }
943
1109
 
944
1110
  // src/client/authyonServerClientBuilder.ts
1111
+ var _options;
945
1112
  var AuthyonServerClientBuilder = class {
946
1113
  constructor() {
947
- this.options = {};
1114
+ __privateAdd(this, _options, {});
948
1115
  }
949
1116
  withEnvironmentKey(envKey) {
950
- this.options.envKey = envKey;
1117
+ __privateGet(this, _options).envKey = envKey;
951
1118
  return this;
952
1119
  }
953
1120
  withEnvironmentCredentials(clientId, clientSecret) {
954
- this.options.clientId = clientId;
955
- this.options.clientSecret = clientSecret;
1121
+ __privateGet(this, _options).clientId = clientId;
1122
+ __privateGet(this, _options).clientSecret = clientSecret;
956
1123
  return this;
957
1124
  }
958
1125
  withBaseUrl(baseUrl, allowInsecureHttp = false) {
959
- this.options.baseUrl = baseUrl;
960
- this.options.allowInsecureHttp = allowInsecureHttp;
1126
+ __privateGet(this, _options).baseUrl = baseUrl;
1127
+ __privateGet(this, _options).allowInsecureHttp = allowInsecureHttp;
961
1128
  return this;
962
1129
  }
963
1130
  withTimeout(timeoutMs) {
964
- this.options.timeoutMs = timeoutMs;
1131
+ __privateGet(this, _options).timeoutMs = timeoutMs;
965
1132
  return this;
966
1133
  }
967
1134
  withHttpAdapter(httpAdapter) {
968
- this.options.httpAdapter = httpAdapter;
1135
+ __privateGet(this, _options).httpAdapter = httpAdapter;
969
1136
  return this;
970
1137
  }
971
1138
  withHttpLogger(httpLogger) {
972
- this.options.httpLogger = httpLogger;
1139
+ __privateGet(this, _options).httpLogger = httpLogger;
973
1140
  return this;
974
1141
  }
975
1142
  build() {
976
- return new AuthyonServerClient({ ...this.options });
1143
+ return new AuthyonServerClient({ ...__privateGet(this, _options) });
977
1144
  }
978
1145
  };
979
-
980
- // ../../internal/core/authorization/ability.ts
981
- var AuthyonAbility = class {
982
- constructor(rules = [], detectSubjectType = defaultSubjectType) {
983
- this.detectSubjectType = detectSubjectType;
984
- this.listeners = /* @__PURE__ */ new Set();
985
- this.currentRules = rules.map(cloneRule);
986
- }
987
- get rules() {
988
- return this.currentRules;
989
- }
990
- can(action, subject, field) {
991
- const subjectType = typeof subject === "string" ? subject : this.detectSubjectType(subject);
992
- for (let index = this.currentRules.length - 1; index >= 0; index -= 1) {
993
- const rule = this.currentRules[index];
994
- if (!matchesToken(rule.action, action, "manage")) continue;
995
- if (!matchesToken(rule.subject, subjectType, "all")) continue;
996
- if (field && rule.fields && !rule.fields.some((value) => matchesField(value, field)))
997
- continue;
998
- if (rule.conditions) {
999
- if (typeof subject === "string" || !matchesConditions(subject, rule.conditions)) continue;
1000
- }
1001
- return !rule.inverted;
1002
- }
1003
- return false;
1004
- }
1005
- cannot(action, subject, field) {
1006
- return !this.can(action, subject, field);
1007
- }
1008
- rulesFor(action, subject) {
1009
- return this.currentRules.filter(
1010
- (rule) => matchesToken(rule.action, action, "manage") && matchesToken(rule.subject, subject, "all")
1011
- );
1012
- }
1013
- update(rules) {
1014
- this.currentRules = rules.map(cloneRule);
1015
- for (const listener of this.listeners) listener(this.rules);
1016
- }
1017
- on(event, listener) {
1018
- if (event !== "updated") return () => void 0;
1019
- this.listeners.add(listener);
1020
- return () => this.listeners.delete(listener);
1021
- }
1022
- };
1023
- var AuthyonAbilityBuilder = class {
1024
- constructor() {
1025
- this.rules = [];
1026
- }
1027
- can(action, subject, conditions, fields) {
1028
- this.rules.push({ action, subject, conditions, fields });
1029
- return this;
1030
- }
1031
- cannot(action, subject, conditions, fields, reason) {
1032
- this.rules.push({ action, subject, conditions, fields, reason, inverted: true });
1033
- return this;
1034
- }
1035
- build(options = {}) {
1036
- return new AuthyonAbility(this.rules, options.detectSubjectType);
1037
- }
1038
- };
1039
- function createAuthyonAbility(source = {}, options = {}) {
1040
- return new AuthyonAbility(createAuthyonRules(source, options), options.detectSubjectType);
1041
- }
1042
- function hasPermission(source, requiredPermission) {
1043
- const requirement = permissionToRule(requiredPermission);
1044
- if (!requirement || typeof requirement.action !== "string" || typeof requirement.subject !== "string") {
1045
- return false;
1046
- }
1047
- const permissionSource = isPermissionList(source) ? { permissions: source } : source;
1048
- return createAuthyonAbility(permissionSource).can(requirement.action, requirement.subject);
1049
- }
1050
- function isPermissionList(source) {
1051
- return Array.isArray(source);
1052
- }
1053
- function createAuthyonRules(source = {}, options = {}) {
1054
- const permissions = /* @__PURE__ */ new Set([
1055
- ...source.permissions ?? [],
1056
- ...source.scope?.split(/\s+/).filter(Boolean) ?? []
1057
- ]);
1058
- const rules = [...permissions].map(permissionToRule).filter(isAbilityRule);
1059
- for (const role of /* @__PURE__ */ new Set([...source.roles ?? [], ...options.roles ?? []])) {
1060
- rules.push(...(options.roleRules?.[role] ?? []).map(cloneRule));
1061
- }
1062
- rules.push(...(options.rules ?? []).map(cloneRule));
1063
- return rules;
1064
- }
1065
- function permissionToRule(permission) {
1066
- const normalized = permission.trim();
1067
- if (!normalized) return null;
1068
- if (normalized === "*" || normalized === "*:*" || normalized === "all:manage") {
1069
- return { action: "manage", subject: "all" };
1070
- }
1071
- const separator = normalized.lastIndexOf(":");
1072
- if (separator <= 0 || separator === normalized.length - 1) return null;
1073
- const subject = normalized.slice(0, separator);
1074
- const action = normalized.slice(separator + 1);
1075
- return {
1076
- action: action === "*" ? "manage" : action,
1077
- subject: subject === "*" ? "all" : subject
1078
- };
1079
- }
1080
- function matchesToken(value, expected, wildcard) {
1081
- return (Array.isArray(value) ? value : [value]).some(
1082
- (candidate) => candidate === expected || candidate === wildcard || candidate === "*" || matchesSegments(candidate, expected)
1083
- );
1084
- }
1085
- function matchesSegments(pattern, value) {
1086
- const patternSegments = pattern.split(":");
1087
- const valueSegments = value.split(":");
1088
- return patternSegments.length === valueSegments.length && patternSegments.every((segment, index) => segment === "*" || segment === valueSegments[index]);
1089
- }
1090
- function matchesField(pattern, field) {
1091
- if (pattern === "*" || pattern === field) return true;
1092
- return pattern.endsWith(".*") && field.startsWith(pattern.slice(0, -1));
1093
- }
1094
- function matchesConditions(subject, conditions) {
1095
- return Object.entries(conditions).every(([path, expected]) => {
1096
- if (path === "$and" && Array.isArray(expected)) {
1097
- return expected.every(
1098
- (condition) => matchesConditions(subject, condition)
1099
- );
1100
- }
1101
- if (path === "$or" && Array.isArray(expected)) {
1102
- return expected.some(
1103
- (condition) => matchesConditions(subject, condition)
1104
- );
1105
- }
1106
- return matchesValue(readPath(subject, path), expected);
1107
- });
1108
- }
1109
- function matchesValue(actual, expected) {
1110
- if (!isRecord2(expected) || !Object.keys(expected).some((key) => key.startsWith("$"))) {
1111
- return isRecord2(expected) && isRecord2(actual) ? matchesConditions(actual, expected) : Object.is(actual, expected);
1112
- }
1113
- return Object.entries(expected).every(([operator, operand]) => {
1114
- switch (operator) {
1115
- case "$eq":
1116
- return Object.is(actual, operand);
1117
- case "$ne":
1118
- return !Object.is(actual, operand);
1119
- case "$in":
1120
- return Array.isArray(operand) && operand.some((value) => Object.is(actual, value));
1121
- case "$nin":
1122
- return Array.isArray(operand) && !operand.some((value) => Object.is(actual, value));
1123
- case "$gt":
1124
- return typeof actual === "number" && typeof operand === "number" && actual > operand;
1125
- case "$gte":
1126
- return typeof actual === "number" && typeof operand === "number" && actual >= operand;
1127
- case "$lt":
1128
- return typeof actual === "number" && typeof operand === "number" && actual < operand;
1129
- case "$lte":
1130
- return typeof actual === "number" && typeof operand === "number" && actual <= operand;
1131
- case "$exists":
1132
- return operand ? actual !== void 0 : actual === void 0;
1133
- default:
1134
- return false;
1135
- }
1136
- });
1137
- }
1138
- function readPath(value, path) {
1139
- return path.split(".").reduce((current, part) => isRecord2(current) ? current[part] : void 0, value);
1140
- }
1141
- function defaultSubjectType(subject) {
1142
- const explicit = subject.__type ?? subject.type ?? subject.kind;
1143
- if (typeof explicit === "string") return explicit;
1144
- const constructorName = subject.constructor?.name;
1145
- return typeof constructorName === "string" ? constructorName : "Object";
1146
- }
1147
- function cloneRule(rule) {
1148
- return {
1149
- ...rule,
1150
- action: Array.isArray(rule.action) ? [...rule.action] : rule.action,
1151
- subject: Array.isArray(rule.subject) ? [...rule.subject] : rule.subject,
1152
- fields: rule.fields ? [...rule.fields] : void 0
1153
- };
1154
- }
1155
- function isRecord2(value) {
1156
- return typeof value === "object" && value !== null && !Array.isArray(value);
1157
- }
1158
- function isAbilityRule(value) {
1159
- return value !== null;
1160
- }
1161
-
1162
- // src/integrations/authorization.ts
1163
- var AuthorizationError = class extends Error {
1164
- constructor(status, code, message) {
1165
- super(message);
1166
- this.status = status;
1167
- this.code = code;
1168
- this.name = "AuthorizationError";
1169
- }
1170
- };
1171
- async function authorizeToken(client, token, options = {}) {
1172
- if (!token) throw unauthenticated();
1173
- let userId;
1174
- let permissions = [];
1175
- let roles = [];
1176
- let scope;
1177
- if (options.verification === "jwks") {
1178
- if (!options.jwksVerifier) {
1179
- throw new Error("Authyon: `jwksVerifier` is required for JWKS verification");
1180
- }
1181
- let verified;
1182
- try {
1183
- verified = await options.jwksVerifier.verifyAccessToken(token);
1184
- } catch (error) {
1185
- if (!(error instanceof TokenVerificationError)) throw error;
1186
- if (error.code === "token.key_unavailable") {
1187
- throw new AuthorizationError(
1188
- 503,
1189
- "authorization.unavailable",
1190
- "Token verification is temporarily unavailable"
1191
- );
1192
- }
1193
- throw unauthenticated();
1194
- }
1195
- const { claims } = verified;
1196
- userId = claims.sub;
1197
- permissions = claims.permissions ?? [];
1198
- roles = claims.roles ?? [];
1199
- scope = claims.scope;
1200
- } else if (options.verification === "introspect") {
1201
- const identity = await client.introspect(token, { clientIp: options.clientIp });
1202
- if (!identity.active) throw unauthenticated();
1203
- userId = identity.sub;
1204
- permissions = identity.permissions ?? [];
1205
- roles = identity.roles ?? [];
1206
- scope = identity.scope;
1207
- } else {
1208
- const identity = await client.validate(token, { clientIp: options.clientIp });
1209
- if (!identity.valid || !identity.user) throw unauthenticated();
1210
- userId = identity.user.id;
1211
- permissions = identity.user.permissions ?? [];
1212
- }
1213
- const ability = createAuthyonAbility(
1214
- { permissions, roles, scope },
1215
- {
1216
- rules: options.rules,
1217
- roleRules: options.roleRules,
1218
- roles: options.roles,
1219
- detectSubjectType: options.detectSubjectType
1220
- }
1221
- );
1222
- if (options.requirement && !canAccess(ability, options.requirement)) {
1223
- throw new AuthorizationError(403, "authorization.forbidden", "Permission denied");
1224
- }
1225
- return { token, userId, ability, permissions, roles };
1226
- }
1227
- function authorizeRequest(client, request, options = {}) {
1228
- const clientIp = options.resolveClientIp?.(request) ?? options.clientIp;
1229
- return authorizeToken(client, bearerToken(request.headers.get("authorization")), {
1230
- ...options,
1231
- clientIp
1232
- });
1233
- }
1234
- function createExpressAuthorizationMiddleware(client, options = {}) {
1235
- return async (request, response, next) => {
1236
- try {
1237
- const header = request.headers.authorization;
1238
- const clientIp = resolveExpressClientIp(request, options);
1239
- const context = await authorizeToken(
1240
- client,
1241
- bearerToken(Array.isArray(header) ? header[0] : header),
1242
- { ...options, clientIp }
1243
- );
1244
- request[options.requestProperty ?? "authyon"] = context;
1245
- next();
1246
- } catch (error) {
1247
- if (options.passErrorsToNext || !(error instanceof AuthorizationError)) {
1248
- next(error);
1249
- return;
1250
- }
1251
- response.status(error.status).json({ code: error.code, message: error.message });
1252
- }
1253
- };
1254
- }
1255
- function resolveExpressClientIp(request, options) {
1256
- const resolved = options.resolveClientIp?.(request) ?? (options.trustProxy ? request.ip : request.socket?.remoteAddress);
1257
- return resolved ? normalizeClientIp(stripIpv4Prefix(resolved)) : void 0;
1258
- }
1259
- function stripIpv4Prefix(value) {
1260
- return value.startsWith("::ffff:") ? value.slice(7) : value;
1261
- }
1262
- function createAuthorizationErrorResponse(error) {
1263
- if (!(error instanceof AuthorizationError)) throw error;
1264
- return Response.json({ code: error.code, message: error.message }, { status: error.status });
1265
- }
1266
- function canAccess(ability, requirement) {
1267
- return ability.can(requirement.action, requirement.subject, requirement.field);
1268
- }
1269
- function bearerToken(header) {
1270
- const match = header?.match(/^Bearer\s+(.+)$/i);
1271
- if (!match?.[1]) throw unauthenticated();
1272
- return match[1].trim();
1273
- }
1274
- function unauthenticated() {
1275
- return new AuthorizationError(
1276
- 401,
1277
- "authorization.unauthenticated",
1278
- "A valid bearer token is required"
1279
- );
1280
- }
1146
+ _options = new WeakMap();
1281
1147
  export {
1282
1148
  AuthorizationError,
1283
1149
  AuthyonAbility,
@@ -1289,8 +1155,11 @@ export {
1289
1155
  FetchHttpAdapter,
1290
1156
  JwksTokenVerifier,
1291
1157
  LoggingHttpAdapter,
1158
+ PlatformScopedClient,
1292
1159
  TenantScopedClient,
1293
1160
  TokenVerificationError,
1161
+ UserScopedClient,
1162
+ WorkspaceInvitesClient,
1294
1163
  authorizeRequest,
1295
1164
  authorizeToken,
1296
1165
  createAuthorizationErrorResponse,