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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,501 +1,277 @@
1
- // ../../internal/core/config/defaults.ts
2
- var DEFAULT_BASE_URL = "https://api.authyon.com";
3
- var DEFAULT_EXPIRY_SKEW_MS = 3e4;
4
-
5
- // ../../internal/core/auth/expiringTokenProvider.ts
6
- var ExpiringTokenProvider = class {
7
- constructor(acquire, expirySkewMs = DEFAULT_EXPIRY_SKEW_MS) {
8
- this.acquire = acquire;
9
- this.expirySkewMs = expirySkewMs;
10
- this.token = null;
11
- this.inFlight = null;
12
- }
13
- async getAccessToken() {
14
- if (this.token && Date.now() < this.token.expiresAt - this.expirySkewMs) {
15
- return this.token.accessToken;
16
- }
17
- if (this.inFlight) return this.inFlight;
18
- this.inFlight = this.acquire().then((token) => {
19
- this.token = token;
20
- return token.accessToken;
21
- }).finally(() => {
22
- this.inFlight = null;
23
- });
24
- return this.inFlight;
25
- }
26
- clear() {
27
- this.token = null;
28
- }
29
- };
30
-
31
- // ../../internal/core/errors/authyonError.ts
32
- var ErrorCodes = {
33
- Unknown: "unknown",
34
- NetworkError: "request.network_error",
35
- Timeout: "request.timeout",
36
- SessionMalformed: "session.malformed",
37
- NotAuthenticated: "auth.not_authenticated",
38
- InvalidToken: "auth.invalid_token",
39
- MissingToken: "auth.missing_token",
40
- EmailTaken: "user.email_taken",
41
- PasswordWeak: "user.password_weak",
42
- PasswordPwned: "user.password_pwned",
43
- RateLimited: "rate_limited"
44
- };
45
- var AuthyonError = class extends Error {
46
- constructor(status, body, options = {}) {
47
- super(body.detail ?? body.title ?? `Authyon request failed with status ${status}`);
48
- this.name = "AuthyonError";
49
- this.status = status;
50
- this.code = body.code ?? "unknown";
51
- this.title = body.title ?? "Error";
52
- this.detail = body.detail;
53
- this.requestId = options.requestId;
54
- this.retryAfter = options.retryAfter;
55
- this.cause = options.cause;
56
- }
57
- is(code) {
58
- return this.code === code;
59
- }
60
- isAny(...codes) {
61
- return codes.includes(this.code);
62
- }
63
- hasPrefix(prefix) {
64
- return this.code.startsWith(prefix);
65
- }
66
- isStatus(...statuses) {
67
- return statuses.includes(this.status);
68
- }
69
- /** Stable interpretation for UI decisions, retries, telemetry and support flows. */
70
- interpret() {
71
- const category = classifyError(this.status, this.code);
72
- return {
73
- category,
74
- action: actionFor(category),
75
- retryable: isRetryable(category),
76
- ...this.retryAfter !== void 0 ? { retryAfter: this.retryAfter } : {},
77
- ...this.requestId !== void 0 ? { requestId: this.requestId } : {}
78
- };
79
- }
80
- get category() {
81
- return this.interpret().category;
82
- }
83
- get retryable() {
84
- return this.interpret().retryable;
85
- }
86
- toJSON() {
87
- return {
88
- name: this.name,
89
- message: this.message,
90
- status: this.status,
91
- code: this.code,
92
- title: this.title,
93
- detail: this.detail,
94
- requestId: this.requestId,
95
- retryAfter: this.retryAfter,
96
- ...this.interpret()
97
- };
98
- }
99
- };
100
- function classifyError(status, code) {
101
- if (code === ErrorCodes.NetworkError) return "network";
102
- if (code === ErrorCodes.Timeout || status === 408) return "timeout";
103
- if (code === ErrorCodes.RateLimited || status === 429) return "rate_limit";
104
- if (code === ErrorCodes.EmailTaken || status === 409) return "conflict";
105
- if (code === ErrorCodes.PasswordWeak || code === ErrorCodes.PasswordPwned) return "validation";
106
- if (code === ErrorCodes.SessionMalformed) return "server";
107
- if (code.startsWith("auth.") || status === 401) return "authentication";
108
- if (status === 403) return "authorization";
109
- if (status === 404) return "not_found";
110
- if (status === 400 || status === 422) return "validation";
111
- if (status >= 500) return "server";
112
- return "unknown";
113
- }
114
- function actionFor(category) {
115
- switch (category) {
116
- case "network":
117
- case "timeout":
118
- case "rate_limit":
119
- case "server":
120
- return "retry";
121
- case "authentication":
122
- return "reauthenticate";
123
- case "authorization":
124
- return "request_access";
125
- case "validation":
126
- return "fix_input";
127
- case "not_found":
128
- return "not_found";
129
- case "conflict":
130
- return "resolve_conflict";
131
- case "unknown":
132
- return "contact_support";
133
- }
134
- }
135
- function isRetryable(category) {
136
- return category === "network" || category === "timeout" || category === "rate_limit" || category === "server";
137
- }
1
+ import {
2
+ AuthyonError,
3
+ DEFAULT_BASE_URL,
4
+ DEFAULT_EXPIRY_SKEW_MS,
5
+ ErrorCodes,
6
+ FetchHttpAdapter,
7
+ JsonHttpClient,
8
+ LoggingHttpAdapter,
9
+ createSharedTransport
10
+ } from "./chunk-TDJNBD3Z.js";
11
+ import {
12
+ AuthorizationError,
13
+ AuthyonAbility,
14
+ AuthyonAbilityBuilder,
15
+ JwksTokenVerifier,
16
+ TokenVerificationError,
17
+ authorizeRequest,
18
+ authorizeToken,
19
+ clientIpHeaders,
20
+ createAuthorizationErrorResponse,
21
+ createAuthyonAbility,
22
+ createAuthyonRules,
23
+ createExpressAuthorizationMiddleware,
24
+ hasPermission,
25
+ hasPermissionGroup,
26
+ normalizeClientIp
27
+ } from "./chunk-NKWQMYB3.js";
28
+ import {
29
+ __privateAdd,
30
+ __privateGet,
31
+ __privateSet
32
+ } from "./chunk-77J4NURK.js";
138
33
 
139
- // ../../internal/core/http/query.ts
140
- function appendQuery(path, params) {
141
- if (!params) return path;
142
- const query = new URLSearchParams();
143
- for (const [key, value2] of Object.entries(params)) {
144
- if (value2 !== void 0) query.set(key, String(value2));
34
+ // src/client/credentialManagement.ts
35
+ function segment(value) {
36
+ if (typeof value !== "string" || !value.trim() || value === "." || value === ".." || [...value].some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127)) {
37
+ throw new TypeError("Authyon: invalid route identifier.");
145
38
  }
146
- const value = query.toString();
147
- return value ? `${path}${path.includes("?") ? "&" : "?"}${value}` : path;
39
+ return encodeURIComponent(value);
148
40
  }
149
-
150
- // ../../internal/core/http/httpAdapter.ts
151
- var FetchHttpAdapter = class {
152
- constructor(fetchImpl = fetch.bind(globalThis)) {
153
- this.fetchImpl = fetchImpl;
154
- }
155
- request(request) {
156
- return this.fetchImpl(request.url, {
157
- method: request.method,
158
- headers: request.headers,
159
- body: request.body,
160
- signal: request.signal
161
- });
162
- }
163
- };
164
- var LoggingHttpAdapter = class {
165
- constructor(adapter, options) {
166
- this.adapter = adapter;
167
- this.options = options;
168
- }
169
- async request(request) {
170
- const startedAt = Date.now();
171
- const eventBase = {
172
- method: request.method,
173
- url: sanitizeUrl(request.url)
174
- };
175
- this.log({ type: "request", ...eventBase, timestamp: startedAt });
176
- try {
177
- const response = await this.adapter.request(request);
178
- this.log({
179
- type: "response",
180
- ...eventBase,
181
- status: response.status,
182
- durationMs: Date.now() - startedAt,
183
- requestId: response.headers.get("x-request-id") ?? response.headers.get("trace-id") ?? void 0,
184
- timestamp: Date.now()
185
- });
186
- return response;
187
- } catch (error) {
188
- this.log({
189
- type: "error",
190
- ...eventBase,
191
- errorName: error instanceof Error ? error.name : "UnknownError",
192
- durationMs: Date.now() - startedAt,
193
- timestamp: Date.now()
194
- });
195
- throw error;
196
- }
197
- }
198
- log(event) {
199
- if (!this.options.enabled) return;
200
- const logger = this.options.logger ?? defaultHttpLogger;
201
- try {
202
- logger(event);
203
- } catch {
41
+ function permissionsBody(input) {
42
+ if (input.permissions !== void 0 && input.scopes !== void 0) {
43
+ throw new TypeError("Authyon: supply permissions or scopes, not both.");
44
+ }
45
+ const values = input.permissions ?? input.scopes;
46
+ if (!Array.isArray(values) || values.length === 0) {
47
+ throw new TypeError("Authyon: explicit credential permissions are required.");
48
+ }
49
+ const permissions = values.map((value) => {
50
+ if (typeof value !== "string") throw new TypeError("Authyon: invalid permission.");
51
+ const normalized = value.trim().toLowerCase();
52
+ if (!/^(?:\*|[a-z][a-z0-9_-]*):(?:\*|[a-z][a-z0-9_-]*):(?:\*|[a-z][a-z0-9_-]*)$/u.test(normalized)) {
53
+ throw new TypeError("Authyon: permissions must have namespace:resource:action format.");
204
54
  }
55
+ return normalized;
56
+ });
57
+ return { permissions: [...new Set(permissions)] };
58
+ }
59
+ function lifetimeBody(input) {
60
+ if (input.lifetimeDays === void 0 || input.lifetimeDays === null) return {};
61
+ if (!Number.isInteger(input.lifetimeDays) || input.lifetimeDays < 1 || input.lifetimeDays > 3650) {
62
+ throw new RangeError("Authyon: lifetimeDays must be between 1 and 3650.");
205
63
  }
206
- };
207
- function defaultHttpLogger(event) {
208
- console.debug("[Authyon HTTP]", event);
64
+ return { lifetimeDays: input.lifetimeDays };
209
65
  }
210
- function sanitizeUrl(value) {
211
- const url = new URL(value);
212
- const queryKeys = /* @__PURE__ */ new Set();
213
- url.searchParams.forEach((_value, key) => queryKeys.add(key));
214
- url.search = queryKeys.size > 0 ? [...queryKeys].map((key) => `${encodeURIComponent(key)}=REDACTED`).join("&") : "";
215
- return url.toString();
66
+ function credentialQuery(options = {}) {
67
+ const { skip, take, search } = options;
68
+ if (skip !== void 0 && (!Number.isInteger(skip) || skip < 0 || skip > 2147483647))
69
+ throw new RangeError("Authyon: invalid skip.");
70
+ if (take !== void 0 && (!Number.isInteger(take) || take < 1 || take > 100))
71
+ throw new RangeError("Authyon: take must be between 1 and 100.");
72
+ if (search !== void 0 && (typeof search !== "string" || search.trim().length > 200))
73
+ throw new RangeError("Authyon: search must contain at most 200 characters.");
74
+ return { search: search?.trim() || void 0, skip, take };
216
75
  }
217
-
218
- // ../../internal/core/http/transport.ts
219
- var DEFAULT_TIMEOUT_MS = 15e3;
220
- var SharedTransportError = class extends Error {
221
- constructor(code, cause) {
222
- super(code === "request.timeout" ? "Request timed out" : "Network request failed");
223
- this.name = "SharedTransportError";
224
- this.code = code;
225
- this.cause = cause;
226
- }
227
- };
228
- function createSharedTransport(options) {
229
- const baseUrl = normalizeBaseUrl(options.baseUrl, options.allowInsecureHttp ?? false);
230
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
231
- if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
232
- throw new Error("Authyon: `timeoutMs` must be a non-negative finite number");
233
- }
234
- if (options.httpAdapter && options.fetch) {
235
- throw new Error("Authyon: use either `httpAdapter` or `fetch`, not both");
236
- }
237
- const baseAdapter = options.httpAdapter ?? new FetchHttpAdapter(options.fetch);
238
- const httpAdapter = options.httpLogger ? new LoggingHttpAdapter(baseAdapter, options.httpLogger) : baseAdapter;
76
+ function credentialSummary(value) {
239
77
  return {
240
- baseUrl,
241
- async request(path, init) {
242
- const controller = new AbortController();
243
- const timeout = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : void 0;
244
- try {
245
- return await httpAdapter.request({
246
- url: `${baseUrl}${path}`,
247
- method: init.method ?? "GET",
248
- headers: normalizeHeaders(init.headers),
249
- body: init.body,
250
- signal: controller.signal
251
- });
252
- } catch (cause) {
253
- throw new SharedTransportError(
254
- controller.signal.aborted ? ErrorCodes.Timeout : ErrorCodes.NetworkError,
255
- cause
256
- );
257
- } finally {
258
- if (timeout !== void 0) clearTimeout(timeout);
259
- }
260
- }
78
+ id: value.id,
79
+ clientId: value.clientId,
80
+ description: value.description,
81
+ createdAt: value.createdAt,
82
+ lastUsedAt: value.lastUsedAt,
83
+ revokedAt: value.revokedAt,
84
+ expiresAt: value.expiresAt ?? null,
85
+ isActive: value.isActive,
86
+ permissions: [...value.permissions]
261
87
  };
262
88
  }
263
- function normalizeHeaders(headers) {
264
- const normalized = {};
265
- new Headers(headers).forEach((value, key) => {
266
- normalized[key] = value;
267
- });
268
- return normalized;
89
+ function credentialDetail(value) {
90
+ return {
91
+ ...credentialSummary(value),
92
+ createdBy: value.createdBy == null ? null : {
93
+ id: value.createdBy.id,
94
+ type: value.createdBy.type,
95
+ displayName: value.createdBy.displayName
96
+ },
97
+ updatedAt: value.updatedAt,
98
+ secretRotatedAt: value.secretRotatedAt,
99
+ expiresAt: value.expiresAt ?? null,
100
+ ageSeconds: value.ageSeconds,
101
+ lifetimeSeconds: value.lifetimeSeconds,
102
+ accessTokenLifetimeSeconds: value.accessTokenLifetimeSeconds
103
+ };
269
104
  }
270
- function responseMetadata(response) {
271
- const retryAfterHeader = response.headers.get("retry-after");
272
- const retryAfter = retryAfterHeader ? Number(retryAfterHeader) : void 0;
105
+ async function credentialPage(request, path2, options = {}) {
106
+ const query = credentialQuery(options);
107
+ const page = await request(path2, { query });
108
+ if (!page || !Array.isArray(page.data))
109
+ throw new TypeError("Authyon: expected a paginated credential response.");
273
110
  return {
274
- requestId: response.headers.get("x-request-id") ?? response.headers.get("trace-id") ?? void 0,
275
- retryAfter: Number.isFinite(retryAfter) ? retryAfter : void 0
111
+ data: page.data.map(credentialSummary),
112
+ page: page.page,
113
+ perPage: page.perPage,
114
+ total: page.total,
115
+ pages: page.pages,
116
+ hasNext: page.hasNext,
117
+ hasPrev: page.hasPrev
276
118
  };
277
119
  }
278
- function normalizeBaseUrl(value, allowInsecureHttp) {
279
- let url;
280
- try {
281
- url = new URL(value);
282
- } catch {
283
- throw new Error("Authyon: `baseUrl` must be an absolute URL");
284
- }
285
- const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
286
- if (url.protocol !== "https:" && !(url.protocol === "http:" && (loopback || allowInsecureHttp))) {
287
- throw new Error(
288
- "Authyon: `baseUrl` must use HTTPS (HTTP is allowed only for loopback or with `allowInsecureHttp`)"
289
- );
290
- }
291
- if (url.username || url.password || url.search || url.hash) {
292
- throw new Error(
293
- "Authyon: `baseUrl` cannot contain credentials, query parameters, or fragments"
120
+ async function allCredentials(request, path2) {
121
+ const result = [];
122
+ const seen = /* @__PURE__ */ new Set();
123
+ let skip = 0;
124
+ for (let iteration = 0; iteration < 1e3; iteration++) {
125
+ const page = await request(
126
+ path2,
127
+ iteration === 0 ? {} : { query: { skip, take: 100 } }
294
128
  );
295
- }
296
- return url.toString().replace(/\/+$/, "");
297
- }
298
-
299
- // ../../internal/core/http/jsonHttpClient.ts
300
- var JsonHttpClient = class {
301
- constructor(transport) {
302
- this.transport = transport;
303
- }
304
- async send(path, options = {}) {
305
- const headers = { ...options.headers };
306
- if (options.body !== void 0) headers["Content-Type"] = "application/json";
307
- try {
308
- return await this.transport.request(appendQuery(path, options.query), {
309
- method: options.method ?? "GET",
310
- headers,
311
- body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
312
- });
313
- } catch (cause) {
314
- if (!(cause instanceof SharedTransportError)) throw cause;
315
- throw new AuthyonError(0, { code: cause.code, title: cause.message }, { cause: cause.cause });
129
+ if (Array.isArray(page)) {
130
+ if (iteration !== 0)
131
+ throw new Error("Authyon: credential pagination changed during listing.");
132
+ return page.map(credentialSummary);
316
133
  }
317
- }
318
- async parse(response) {
319
- if (!response.ok) {
320
- let body = {};
321
- try {
322
- body = await response.json();
323
- } catch {
324
- }
325
- throw new AuthyonError(response.status, body, responseMetadata(response));
134
+ if (!page || !Array.isArray(page.data))
135
+ throw new TypeError("Authyon: invalid credential page.");
136
+ for (const row of page.data) {
137
+ if (seen.has(row.id))
138
+ throw new Error("Authyon: credential pages overlapped; retry the listing.");
139
+ seen.add(row.id);
140
+ result.push(credentialSummary(row));
326
141
  }
327
- if (response.status === 204) return void 0;
328
- return await response.json();
329
- }
330
- async request(path, options = {}) {
331
- return this.parse(await this.send(path, options));
142
+ if (!page.hasNext) return result;
143
+ if (page.data.length === 0) throw new Error("Authyon: credential pagination made no progress.");
144
+ skip += page.data.length;
332
145
  }
333
- };
334
-
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");
146
+ throw new Error("Authyon: use listPage for more than 1000 credential pages.");
355
147
  }
356
148
 
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
- }
149
+ // ../../internal/core/security/approvals.ts
150
+ function path(id, suffix = "") {
151
+ if (typeof id !== "string" || !id.trim())
152
+ throw new TypeError("Authyon: an approval id is required.");
153
+ return `/auth/authorizations/${encodeURIComponent(id)}${suffix}`;
154
+ }
155
+ var NO_METHODS = {
156
+ password: false,
157
+ authenticator: false,
158
+ emailCode: false,
159
+ passkey: false,
160
+ passkeyCount: 0,
161
+ recoveryCodes: 0,
162
+ sso: [],
163
+ twoFactor: false
376
164
  };
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 });
165
+ function subjectSecurity(request) {
166
+ return {
167
+ methods: async () => {
168
+ const me = await request("/auth/me");
169
+ return me.security ?? NO_METHODS;
170
+ },
171
+ approvals: {
172
+ get: (id) => request(path(id)),
173
+ webauthnOptions: (id) => request(path(id, "/webauthn/options"), { method: "POST" }),
174
+ confirm: (id, input) => request(path(id, "/confirm"), { method: "POST", body: input }),
175
+ reject: (id) => request(path(id, "/reject"), { method: "POST" })
408
176
  }
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
- }
177
+ };
416
178
  }
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();
179
+
180
+ // src/client/scopedManagementClient.ts
181
+ function scopedRequest(request, source) {
182
+ return async (path2, options = {}) => {
183
+ const token = typeof source === "function" ? await source() : source;
184
+ if (typeof token !== "string" || !token.trim() || /\s/u.test(token))
185
+ throw new TypeError("Authyon: a valid access token is required.");
186
+ return request(path2, {
187
+ ...options,
188
+ headers: { ...options.headers, Authorization: `Bearer ${token}` }
189
+ });
190
+ };
422
191
  }
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");
192
+ var _request;
193
+ var UserScopedClient = class {
194
+ /** @hidden */
195
+ constructor(request, token) {
196
+ __privateAdd(this, _request);
197
+ this.tenants = {
198
+ members: {
199
+ list: (tenantId, options = {}) => __privateGet(this, _request).call(this, `/auth/tenants/${segment(tenantId)}/members`, { query: options }),
200
+ /** Adds an existing environment user by email; does not send an invitation email. */
201
+ invite: (tenantId, input) => __privateGet(this, _request).call(this, `/auth/tenants/${segment(tenantId)}/members`, {
202
+ method: "POST",
203
+ body: { email: input.email, roles: input.roles }
204
+ }),
205
+ remove: (tenantId, userId) => __privateGet(this, _request).call(this, `/auth/tenants/${segment(tenantId)}/members/${segment(userId)}`, {
206
+ method: "DELETE"
207
+ })
208
+ },
209
+ /**
210
+ * Invites by e-mail or by link, acting as the user. Requires the
211
+ * `tenants:members:invite` permission on the tenant.
212
+ */
213
+ invites: {
214
+ list: (tenantId) => __privateGet(this, _request).call(this, `/auth/tenants/${segment(tenantId)}/invites`),
215
+ /** Always returns `acceptUrl`; `sendEmail: false` skips the e-mail. */
216
+ create: (tenantId, input) => __privateGet(this, _request).call(this, `/auth/tenants/${segment(tenantId)}/invites`, {
217
+ method: "POST",
218
+ body: {
219
+ email: input.email,
220
+ roles: input.roles,
221
+ sendEmail: input.sendEmail ?? true,
222
+ redirectUri: input.redirectUri
223
+ }
224
+ }),
225
+ /** New link (the previous one stops working), mailed unless `sendEmail` is `false`. */
226
+ resend: (tenantId, inviteId, input = {}) => __privateGet(this, _request).call(this, `/auth/tenants/${segment(tenantId)}/invites/${segment(inviteId)}/resend`, {
227
+ method: "POST",
228
+ body: { sendEmail: input.sendEmail ?? true }
229
+ }),
230
+ revoke: (tenantId, inviteId) => __privateGet(this, _request).call(this, `/auth/tenants/${segment(tenantId)}/invites/${segment(inviteId)}`, {
231
+ method: "DELETE"
232
+ })
233
+ }
234
+ };
235
+ __privateSet(this, _request, scopedRequest(request, token));
236
+ this.security = subjectSecurity(__privateGet(this, _request));
438
237
  }
439
- if (options.acceptedTokenTypes?.length === 0 || options.acceptedTokenTypes?.some((type) => !type.trim())) {
440
- throw new Error("Authyon: `acceptedTokenTypes` must contain non-empty values");
238
+ };
239
+ _request = new WeakMap();
240
+
241
+ // ../../internal/core/auth/expiringTokenProvider.ts
242
+ var _token, _inFlight;
243
+ var ExpiringTokenProvider = class {
244
+ constructor(acquire, expirySkewMs = DEFAULT_EXPIRY_SKEW_MS) {
245
+ this.acquire = acquire;
246
+ this.expirySkewMs = expirySkewMs;
247
+ __privateAdd(this, _token, null);
248
+ __privateAdd(this, _inFlight, null);
441
249
  }
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`);
250
+ async getAccessToken() {
251
+ if (__privateGet(this, _token) && Date.now() < __privateGet(this, _token).expiresAt - this.expirySkewMs) {
252
+ return __privateGet(this, _token).accessToken;
451
253
  }
254
+ if (__privateGet(this, _inFlight)) return __privateGet(this, _inFlight);
255
+ __privateSet(this, _inFlight, this.acquire().then((token) => {
256
+ __privateSet(this, _token, token);
257
+ return token.accessToken;
258
+ }).finally(() => {
259
+ __privateSet(this, _inFlight, null);
260
+ }));
261
+ return __privateGet(this, _inFlight);
452
262
  }
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";
263
+ clear() {
264
+ __privateSet(this, _token, null);
477
265
  }
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
- }
266
+ };
267
+ _token = new WeakMap();
268
+ _inFlight = new WeakMap();
495
269
 
496
270
  // src/client/authyonServerClient.ts
271
+ var _clientSecret;
497
272
  var AuthyonServerClient = class {
498
273
  constructor(options = {}) {
274
+ __privateAdd(this, _clientSecret);
499
275
  // ── Discovery ──────────────────────────────────────────────────────────────
500
276
  this.discovery = {
501
277
  /** GET /.well-known/jwks.json — the environment's public signing keys. */
@@ -529,6 +305,14 @@ var AuthyonServerClient = class {
529
305
  client_id: credentials.clientId,
530
306
  client_secret: credentials.clientSecret
531
307
  }
308
+ }),
309
+ /** POST /tenant/auth/validate — validates an already-minted tenant-client bearer token. */
310
+ validate: (accessToken, context) => this.request("/tenant/auth/validate", {
311
+ method: "POST",
312
+ headers: {
313
+ ...clientIpHeaders(context),
314
+ Authorization: `Bearer ${accessToken}`
315
+ }
532
316
  })
533
317
  };
534
318
  // ── Environment management (users, tenants, roles, permissions, audit) ───
@@ -574,6 +358,39 @@ var AuthyonServerClient = class {
574
358
  envBearer: true,
575
359
  body: { newPassword, reason }
576
360
  }),
361
+ /**
362
+ * POST /env/password-resets — issues a password-reset token for the
363
+ * account with this address and returns it, so your application can
364
+ * deliver it in its own e-mail instead of Authyon's. Needs the
365
+ * `authyon:users:password` scope. Unlike the public forgot-password
366
+ * request, an unknown address rejects with `user.not_found` (404).
367
+ * A previous live token for the same account stops working.
368
+ */
369
+ issuePasswordReset: (input) => this.request("/env/password-resets", {
370
+ method: "POST",
371
+ envBearer: true,
372
+ body: { ...input, sendEmail: input.sendEmail ?? false }
373
+ }),
374
+ /**
375
+ * POST /auth/password-reset/validate — for a backend hosting the reset
376
+ * page: checks a token without spending it. Public, needs only the
377
+ * `envKey`. Rejects with `user.password_reset.invalid` (401) when the
378
+ * token is unknown, used or expired.
379
+ */
380
+ validatePasswordReset: (token) => this.request("/auth/password-reset/validate", { method: "POST", body: { token } }),
381
+ /**
382
+ * POST /auth/password-reset/confirm — redeems the token and sets the new
383
+ * password (every session of the account is signed out). Public, needs
384
+ * only the `envKey`. Resolves with the `redirectUri` the reset was issued
385
+ * with, or `null`.
386
+ */
387
+ confirmPasswordReset: async (token, newPassword) => {
388
+ const done = await this.request(
389
+ "/auth/password-reset/confirm",
390
+ { method: "POST", body: { token, newPassword } }
391
+ );
392
+ return { redirectUri: done?.redirectUri ?? null };
393
+ },
577
394
  /** POST /env/users/{userId}/sessions/revoke-all — revokes every session the user has. */
578
395
  revokeSessions: (userId, reason) => this.request(`/env/users/${encodeURIComponent(userId)}/sessions/revoke-all`, {
579
396
  method: "POST",
@@ -622,7 +439,63 @@ var AuthyonServerClient = class {
622
439
  )
623
440
  },
624
441
  tenants: {
625
- /** GET /env/tenants — paginated, searchable list of tenants in the environment. */
442
+ credentials: {
443
+ /** GET /env/tenants/{tenantId}/credentials. Returns metadata only, including revoked credentials. */
444
+ list: (tenantId) => allCredentials(
445
+ (path2, options) => this.request(path2, { ...options, envBearer: true }),
446
+ `/env/tenants/${segment(tenantId)}/credentials`
447
+ ),
448
+ /** Paginated searchable listing. Prefer this over loading every credential. */
449
+ listPage: (tenantId, options = {}) => credentialPage(
450
+ (path2, requestOptions) => this.request(path2, { ...requestOptions, envBearer: true }),
451
+ `/env/tenants/${segment(tenantId)}/credentials`,
452
+ options
453
+ ),
454
+ get: async (tenantId, credentialId) => credentialDetail(
455
+ await this.request(
456
+ `/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}`,
457
+ { envBearer: true }
458
+ )
459
+ ),
460
+ /** Explicit scopes/permissions; only issuance returns the client secret. */
461
+ create: (tenantId, input) => this.request(`/env/tenants/${segment(tenantId)}/credentials`, {
462
+ method: "POST",
463
+ envBearer: true,
464
+ body: {
465
+ description: input.description,
466
+ ...lifetimeBody(input),
467
+ ...permissionsBody(input)
468
+ }
469
+ }),
470
+ /** Returns a new secret once while keeping the same client id. */
471
+ rotate: (tenantId, credentialId) => this.request(
472
+ `/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}/rotate`,
473
+ { method: "POST", envBearer: true }
474
+ ),
475
+ updatePermissions: (tenantId, credentialId, input) => this.request(
476
+ `/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}/permissions`,
477
+ { method: "PUT", envBearer: true, body: permissionsBody(input) }
478
+ ),
479
+ updateScopes: (tenantId, credentialId, scopes) => this.environment.tenants.credentials.updatePermissions(tenantId, credentialId, {
480
+ scopes
481
+ }),
482
+ revoke: (tenantId, credentialId) => this.request(`/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}`, {
483
+ method: "DELETE",
484
+ envBearer: true
485
+ })
486
+ },
487
+ /**
488
+ * GET /env/tenants — one page of the environment's tenants.
489
+ *
490
+ * `search` matches, case-insensitively and anywhere in the value, the
491
+ * name, the slug, the description and the raw text of both metadata
492
+ * documents — so a tenant filed with `{"cnpj":"00.000.000/0001-00"}`
493
+ * is found by the number, and `cnpj` alone finds every tenant carrying
494
+ * that key. `take` defaults to 25 and is capped at 100.
495
+ *
496
+ * Against an Authyon API older than 2026-10-21 this route ignored
497
+ * skip/take/search and answered with a bare array.
498
+ */
626
499
  list: (params = {}) => this.request("/env/tenants", { envBearer: true, query: params }),
627
500
  /** POST /env/tenants — create a tenant in the environment. */
628
501
  create: (input) => this.request("/env/tenants", { method: "POST", envBearer: true, body: input }),
@@ -634,7 +507,18 @@ var AuthyonServerClient = class {
634
507
  envBearer: true,
635
508
  body: input
636
509
  }),
637
- /** DELETE /env/tenants/{tenantId} — delete a tenant. */
510
+ /** PUT /env/tenants/{tenantId}/metadata - replace public and private metadata. */
511
+ updateMetadata: (tenantId, input) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/metadata`, {
512
+ method: "PUT",
513
+ envBearer: true,
514
+ body: input
515
+ }),
516
+ /** POST /env/tenants/{tenantId}/disable — disable without removing tenant data. */
517
+ disable: (tenantId) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/disable`, {
518
+ method: "POST",
519
+ envBearer: true
520
+ }),
521
+ /** DELETE /env/tenants/{tenantId} — logically delete (disable) a tenant. */
638
522
  delete: (tenantId) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}`, {
639
523
  method: "DELETE",
640
524
  envBearer: true
@@ -642,10 +526,16 @@ var AuthyonServerClient = class {
642
526
  members: {
643
527
  /**
644
528
  * GET /env/tenants/{tenantId}/members — a tenant's members as full
645
- * user records. Confirmed live: a bare array (unlike
646
- * `environment.users.list()`, this one does NOT paginate despite
647
- * accepting `skip`/`take`), with the same shape
648
- * `environment.users.list()` items have.
529
+ * user records, with the same shape `environment.users.list()` items
530
+ * have, plus `tenantRoles` and `tenantPermissions` scoped to this
531
+ * tenant.
532
+ *
533
+ * Paged like every other list: the `Paged<T>` envelope carries
534
+ * `total` and `hasNext`, `take` defaults to 50 and is capped at 100.
535
+ * Up to the Authyon API of 2026-10-21 this one route answered with a
536
+ * bare array, so a caller had to page until a short page came back;
537
+ * against such an API `data` is undefined and the array is the body
538
+ * itself.
649
539
  */
650
540
  list: (tenantId, params = {}) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/members`, {
651
541
  envBearer: true,
@@ -683,6 +573,61 @@ var AuthyonServerClient = class {
683
573
  { method: "DELETE", envBearer: true }
684
574
  )
685
575
  },
576
+ /**
577
+ * Invites by e-mail or by link — for addresses that may not have an
578
+ * account yet; accepting creates it. Managing them needs the
579
+ * credential's `authyon:users:write` scope (listing, `authyon:tenants:read`).
580
+ */
581
+ invites: {
582
+ /** GET /env/tenants/{tenantId}/invites — every invite with its status, newest first. */
583
+ list: (tenantId) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/invites`, { envBearer: true }),
584
+ /**
585
+ * POST /env/tenants/{tenantId}/invites — invite an address. Always
586
+ * returns `acceptUrl`; with `sendEmail: false` Authyon does not mail
587
+ * it and you deliver the link yourself. Valid 7 days, single use, and
588
+ * not readable again later.
589
+ */
590
+ create: (tenantId, input) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/invites`, {
591
+ method: "POST",
592
+ envBearer: true,
593
+ body: {
594
+ email: input.email,
595
+ roles: input.roles,
596
+ sendEmail: input.sendEmail ?? true,
597
+ redirectUri: input.redirectUri
598
+ }
599
+ }),
600
+ /** POST .../invites/{inviteId}/resend — new link (the previous one stops working), mailed unless `sendEmail` is `false`. */
601
+ resend: (tenantId, inviteId, input = {}) => this.request(
602
+ `/env/tenants/${encodeURIComponent(tenantId)}/invites/${encodeURIComponent(inviteId)}/resend`,
603
+ { method: "POST", envBearer: true, body: { sendEmail: input.sendEmail ?? true } }
604
+ ),
605
+ /** DELETE /env/tenants/{tenantId}/invites/{inviteId} — revoke a pending invite. */
606
+ revoke: (tenantId, inviteId) => this.request(
607
+ `/env/tenants/${encodeURIComponent(tenantId)}/invites/${encodeURIComponent(inviteId)}`,
608
+ { method: "DELETE", envBearer: true }
609
+ ),
610
+ /**
611
+ * POST /auth/tenant-invites/preview — for a backend hosting the accept
612
+ * page. Public: needs only `envKey`, the token is the credential.
613
+ */
614
+ preview: (token) => this.request("/auth/tenant-invites/preview", { method: "POST", body: { token } }),
615
+ /**
616
+ * Joins the tenant; for an address with no account, also creates it
617
+ * (password required) with the e-mail already confirmed. Does not
618
+ * sign the user in.
619
+ *
620
+ * Without metadata: POST /auth/tenant-invites/accept — public, needs
621
+ * only `envKey`. With `publicMetadata`/`privateMetadata`: POST
622
+ * /env/tenant-invites/accept with the environment credential
623
+ * (`authyon:users:write`), since only your backend may set them.
624
+ */
625
+ accept: (input) => input.publicMetadata !== void 0 || input.privateMetadata !== void 0 ? this.request("/env/tenant-invites/accept", {
626
+ method: "POST",
627
+ envBearer: true,
628
+ body: input
629
+ }) : this.request("/auth/tenant-invites/accept", { method: "POST", body: input })
630
+ },
686
631
  roles: {
687
632
  /** GET /env/tenants/{tenantId}/roles — list a tenant's roles. */
688
633
  list: (tenantId) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/roles`, { envBearer: true }),
@@ -752,6 +697,60 @@ var AuthyonServerClient = class {
752
697
  loginActivity: (params = {}) => this.request("/env/audit/login-activity", { envBearer: true, query: params })
753
698
  }
754
699
  };
700
+ // ── Security: one-time codes and approvals ───────────────────────────────
701
+ //
702
+ // Everything here runs with the environment credential: `otp` needs the
703
+ // `authyon:otp:verify` scope, `approvals` needs `authyon:financial:authorize`.
704
+ // Keep those credentials on the server.
705
+ this.security = {
706
+ otp: {
707
+ /**
708
+ * POST /env/users/{userId}/otp/verify — checks the user's authenticator
709
+ * app (TOTP) code. A wrong code returns `{ valid: false }` instead of
710
+ * throwing. Each code is accepted once; 3 wrong codes in a row sign the
711
+ * user out everywhere (`sessionsRevoked: true`); 5 lock the check for 15
712
+ * minutes (`rate_limited`). More than 50 wrong codes in 10 minutes from the
713
+ * same credential pause it (`credential_throttled`). Needs `authyon:otp:verify`.
714
+ */
715
+ check: (userId, code) => this.request(`/env/users/${segment(userId)}/otp/verify`, {
716
+ method: "POST",
717
+ envBearer: true,
718
+ body: { code }
719
+ })
720
+ },
721
+ /**
722
+ * The customer approves a payload with their authenticator code or
723
+ * passkey, once.
724
+ *
725
+ * 1. `create()` the approval with the payload and the customer.
726
+ * 2. The customer confirms it (`@authyon/auth` → `security.approvals.confirm`).
727
+ * 3. `consume()` it right before executing, and execute the returned payload.
728
+ */
729
+ approvals: {
730
+ /** POST /env/authorizations — registers a payload awaiting the customer's approval. */
731
+ create: (input) => {
732
+ const { idempotencyKey, ...body } = input;
733
+ return this.request("/env/authorizations", {
734
+ method: "POST",
735
+ envBearer: true,
736
+ headers: idempotencyKey ? { "Idempotency-Key": idempotencyKey } : void 0,
737
+ body
738
+ });
739
+ },
740
+ /** GET /env/authorizations/{id} — current status and, once approved, the assurance evidence. */
741
+ get: (id) => this.request(`/env/authorizations/${segment(id)}`, { envBearer: true }),
742
+ /**
743
+ * POST /env/authorizations/{id}/consume — redeems an approved approval
744
+ * exactly once and returns the approved payload. Pass `payload` to have
745
+ * the API check it matches (`payload_mismatch` otherwise).
746
+ */
747
+ consume: (id, payload) => this.request(`/env/authorizations/${segment(id)}/consume`, {
748
+ method: "POST",
749
+ envBearer: true,
750
+ body: payload === void 0 ? void 0 : { payload }
751
+ })
752
+ }
753
+ };
755
754
  // ── Generic permission grants ─────────────────────────────────────────────
756
755
  this.permissions = {
757
756
  /** POST /users/{userId}/permissions — grant a permission directly to a user. */
@@ -768,7 +767,8 @@ var AuthyonServerClient = class {
768
767
  };
769
768
  this.envKey = options.envKey;
770
769
  this.clientId = options.clientId;
771
- this.clientSecret = options.clientSecret;
770
+ __privateSet(this, _clientSecret, options.clientSecret);
771
+ this.clientIp = options.clientIp;
772
772
  this.transport = createSharedTransport({
773
773
  baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
774
774
  allowInsecureHttp: options.allowInsecureHttp,
@@ -779,14 +779,14 @@ var AuthyonServerClient = class {
779
779
  });
780
780
  this.http = new JsonHttpClient(this.transport);
781
781
  this.environmentTokenProvider = new ExpiringTokenProvider(async () => {
782
- if (!this.clientId || !this.clientSecret) {
782
+ if (!this.clientId || !__privateGet(this, _clientSecret)) {
783
783
  throw new Error(
784
784
  "Authyon: `clientId`/`clientSecret` are required for environment management calls (mint a pair in the console, under the environment's OAuth clients)."
785
785
  );
786
786
  }
787
787
  const token = await this.environmentAuth.token({
788
788
  clientId: this.clientId,
789
- clientSecret: this.clientSecret
789
+ clientSecret: __privateGet(this, _clientSecret)
790
790
  });
791
791
  return {
792
792
  accessToken: token.access_token,
@@ -794,13 +794,16 @@ var AuthyonServerClient = class {
794
794
  };
795
795
  });
796
796
  }
797
- async request(path, options = {}) {
798
- const headers = { ...options.headers };
797
+ async request(path2, options = {}) {
798
+ const headers = {
799
+ ...clientIpHeaders({ clientIp: this.clientIp }),
800
+ ...options.headers
801
+ };
799
802
  if (options.envBearer) {
800
803
  headers.Authorization = `Bearer ${await this.getEnvironmentAccessToken()}`;
801
804
  }
802
805
  if (this.envKey) headers["X-Authyon-Environment"] = this.envKey;
803
- return this.http.request(path, { ...options, headers });
806
+ return this.http.request(path2, { ...options, headers });
804
807
  }
805
808
  /** Mints (and caches) an environment access token from `clientId`/`clientSecret`. */
806
809
  async getEnvironmentAccessToken() {
@@ -855,26 +858,52 @@ var AuthyonServerClient = class {
855
858
  tenant(credentials) {
856
859
  return new TenantScopedClient(this, credentials);
857
860
  }
861
+ /** Uses an end-user token; the configured environment key selects the environment. */
862
+ user(token) {
863
+ return new UserScopedClient((path2, options) => this.request(path2, options), token);
864
+ }
858
865
  /** @internal used by {@link TenantScopedClient} to share the base URL/fetch/error handling. */
859
- _requestAsTenant(accessToken, path, options = {}) {
860
- return this.request(path, {
866
+ _requestAsTenant(accessToken, path2, options = {}) {
867
+ return this.request(path2, {
861
868
  ...options,
862
869
  headers: { ...options.headers, Authorization: `Bearer ${accessToken}` },
863
870
  envBearer: false
864
871
  });
865
872
  }
866
873
  };
874
+ _clientSecret = new WeakMap();
867
875
  var TenantScopedClient = class {
868
876
  constructor(server, credentials) {
877
+ /** Invites to the token's tenant, by e-mail or by link. */
878
+ this.invites = {
879
+ /** GET /tenant/invites — every invite with its status, newest first. */
880
+ list: () => this.request("/tenant/invites"),
881
+ /** POST /tenant/invites — always returns `acceptUrl`; `sendEmail: false` skips the e-mail. */
882
+ create: (input) => this.request("/tenant/invites", {
883
+ method: "POST",
884
+ body: {
885
+ email: input.email,
886
+ roles: input.roles,
887
+ sendEmail: input.sendEmail ?? true,
888
+ redirectUri: input.redirectUri
889
+ }
890
+ }),
891
+ /** POST /tenant/invites/{inviteId}/resend — new link (the previous one stops working), mailed unless `sendEmail` is `false`. */
892
+ resend: (inviteId, input = {}) => this.request(`/tenant/invites/${encodeURIComponent(inviteId)}/resend`, {
893
+ method: "POST",
894
+ body: { sendEmail: input.sendEmail ?? true }
895
+ }),
896
+ /** DELETE /tenant/invites/{inviteId} — revoke a pending invite. */
897
+ revoke: (inviteId) => this.request(`/tenant/invites/${encodeURIComponent(inviteId)}`, { method: "DELETE" })
898
+ };
869
899
  this.members = {
870
900
  /**
871
901
  * GET /tenant/members — members of the token's tenant.
872
902
  *
873
- * ⚠️ Inferred, not confirmed live (no tenant client credentials
874
- * available to exchange for a scoped token) — assumed to share
875
- * `/env/tenants/{id}/members`'s confirmed shape (bare array of full
876
- * user records), since both are the "admin" member listing as opposed
877
- * to `/auth/tenants/{id}/members`'s paginated, lightweight one.
903
+ * Same handler as `/env/tenants/{id}/members` (the tenant comes from the
904
+ * token instead of the path), so the same `Paged<T>` envelope of full
905
+ * user records, `take` defaulting to 50 and capped at 100 — as opposed
906
+ * to `/auth/tenants/{id}/members`'s lightweight member shape.
878
907
  */
879
908
  list: (params = {}) => this.request("/tenant/members", { query: params }),
880
909
  /** POST /tenant/members — add a member to the token's tenant. */
@@ -933,8 +962,12 @@ var TenantScopedClient = class {
933
962
  async getAccessToken() {
934
963
  return this.tokenProvider.getAccessToken();
935
964
  }
936
- async request(path, options = {}) {
937
- return this.server._requestAsTenant(await this.getAccessToken(), path, options);
965
+ async request(path2, options = {}) {
966
+ return this.server._requestAsTenant(await this.getAccessToken(), path2, options);
967
+ }
968
+ /** POST /tenant/auth/validate — validates this tenant client's current bearer token. */
969
+ async validate(context) {
970
+ return this.server.tenantAuth.validate(await this.getAccessToken(), context);
938
971
  }
939
972
  };
940
973
  function createClient(options = {}) {
@@ -942,342 +975,46 @@ function createClient(options = {}) {
942
975
  }
943
976
 
944
977
  // src/client/authyonServerClientBuilder.ts
978
+ var _options;
945
979
  var AuthyonServerClientBuilder = class {
946
980
  constructor() {
947
- this.options = {};
981
+ __privateAdd(this, _options, {});
948
982
  }
949
983
  withEnvironmentKey(envKey) {
950
- this.options.envKey = envKey;
984
+ __privateGet(this, _options).envKey = envKey;
951
985
  return this;
952
986
  }
953
987
  withEnvironmentCredentials(clientId, clientSecret) {
954
- this.options.clientId = clientId;
955
- this.options.clientSecret = clientSecret;
988
+ __privateGet(this, _options).clientId = clientId;
989
+ __privateGet(this, _options).clientSecret = clientSecret;
990
+ return this;
991
+ }
992
+ withClientIp(clientIp) {
993
+ __privateGet(this, _options).clientIp = clientIp;
956
994
  return this;
957
995
  }
958
996
  withBaseUrl(baseUrl, allowInsecureHttp = false) {
959
- this.options.baseUrl = baseUrl;
960
- this.options.allowInsecureHttp = allowInsecureHttp;
997
+ __privateGet(this, _options).baseUrl = baseUrl;
998
+ __privateGet(this, _options).allowInsecureHttp = allowInsecureHttp;
961
999
  return this;
962
1000
  }
963
1001
  withTimeout(timeoutMs) {
964
- this.options.timeoutMs = timeoutMs;
1002
+ __privateGet(this, _options).timeoutMs = timeoutMs;
965
1003
  return this;
966
1004
  }
967
1005
  withHttpAdapter(httpAdapter) {
968
- this.options.httpAdapter = httpAdapter;
1006
+ __privateGet(this, _options).httpAdapter = httpAdapter;
969
1007
  return this;
970
1008
  }
971
1009
  withHttpLogger(httpLogger) {
972
- this.options.httpLogger = httpLogger;
1010
+ __privateGet(this, _options).httpLogger = httpLogger;
973
1011
  return this;
974
1012
  }
975
1013
  build() {
976
- return new AuthyonServerClient({ ...this.options });
977
- }
978
- };
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";
1014
+ return new AuthyonServerClient({ ...__privateGet(this, _options) });
1169
1015
  }
1170
1016
  };
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
- }
1017
+ _options = new WeakMap();
1281
1018
  export {
1282
1019
  AuthorizationError,
1283
1020
  AuthyonAbility,
@@ -1291,6 +1028,7 @@ export {
1291
1028
  LoggingHttpAdapter,
1292
1029
  TenantScopedClient,
1293
1030
  TokenVerificationError,
1031
+ UserScopedClient,
1294
1032
  authorizeRequest,
1295
1033
  authorizeToken,
1296
1034
  createAuthorizationErrorResponse,
@@ -1299,5 +1037,6 @@ export {
1299
1037
  createClient,
1300
1038
  createExpressAuthorizationMiddleware,
1301
1039
  hasPermission,
1040
+ hasPermissionGroup,
1302
1041
  normalizeClientIp
1303
1042
  };