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

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-SUL2HLRR.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`)"
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 } }
289
128
  );
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"
294
- );
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();
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;
329
145
  }
330
- async request(path, options = {}) {
331
- return this.parse(await this.send(path, options));
332
- }
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,82 @@ 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
+ * `filter` is the structured half, and the one to reach for when more
497
+ * than one condition has to hold: each entry is a condition on a JSON
498
+ * document, they are ANDed with each other and with `search`, and they
499
+ * run inside the statement — so the envelope keeps describing the
500
+ * filtered set and you can page through it. Build them with
501
+ * `tenantFilter`:
502
+ *
503
+ * ```ts
504
+ * const page = await authyon.environment.tenants.list({
505
+ * filter: [
506
+ * tenantFilter.in("public.kind", ["reseller", "partner"]),
507
+ * tenantFilter.in("custom.cnpj", [a, b]),
508
+ * ],
509
+ * take: 100,
510
+ * });
511
+ * ```
512
+ *
513
+ * Against an Authyon API older than 2026-10-21 this route ignored
514
+ * skip/take/search and answered with a bare array; one older than
515
+ * 2026-10-22 ignores `filter`, which reads as a page that is too wide
516
+ * rather than an error.
517
+ */
626
518
  list: (params = {}) => this.request("/env/tenants", { envBearer: true, query: params }),
627
519
  /** POST /env/tenants — create a tenant in the environment. */
628
520
  create: (input) => this.request("/env/tenants", { method: "POST", envBearer: true, body: input }),
@@ -634,7 +526,18 @@ var AuthyonServerClient = class {
634
526
  envBearer: true,
635
527
  body: input
636
528
  }),
637
- /** DELETE /env/tenants/{tenantId} — delete a tenant. */
529
+ /** PUT /env/tenants/{tenantId}/metadata - replace public and private metadata. */
530
+ updateMetadata: (tenantId, input) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/metadata`, {
531
+ method: "PUT",
532
+ envBearer: true,
533
+ body: input
534
+ }),
535
+ /** POST /env/tenants/{tenantId}/disable — disable without removing tenant data. */
536
+ disable: (tenantId) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/disable`, {
537
+ method: "POST",
538
+ envBearer: true
539
+ }),
540
+ /** DELETE /env/tenants/{tenantId} — logically delete (disable) a tenant. */
638
541
  delete: (tenantId) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}`, {
639
542
  method: "DELETE",
640
543
  envBearer: true
@@ -642,10 +545,16 @@ var AuthyonServerClient = class {
642
545
  members: {
643
546
  /**
644
547
  * 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.
548
+ * user records, with the same shape `environment.users.list()` items
549
+ * have, plus `tenantRoles` and `tenantPermissions` scoped to this
550
+ * tenant.
551
+ *
552
+ * Paged like every other list: the `Paged<T>` envelope carries
553
+ * `total` and `hasNext`, `take` defaults to 50 and is capped at 100.
554
+ * Up to the Authyon API of 2026-10-21 this one route answered with a
555
+ * bare array, so a caller had to page until a short page came back;
556
+ * against such an API `data` is undefined and the array is the body
557
+ * itself.
649
558
  */
650
559
  list: (tenantId, params = {}) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/members`, {
651
560
  envBearer: true,
@@ -683,6 +592,61 @@ var AuthyonServerClient = class {
683
592
  { method: "DELETE", envBearer: true }
684
593
  )
685
594
  },
595
+ /**
596
+ * Invites by e-mail or by link — for addresses that may not have an
597
+ * account yet; accepting creates it. Managing them needs the
598
+ * credential's `authyon:users:write` scope (listing, `authyon:tenants:read`).
599
+ */
600
+ invites: {
601
+ /** GET /env/tenants/{tenantId}/invites — every invite with its status, newest first. */
602
+ list: (tenantId) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/invites`, { envBearer: true }),
603
+ /**
604
+ * POST /env/tenants/{tenantId}/invites — invite an address. Always
605
+ * returns `acceptUrl`; with `sendEmail: false` Authyon does not mail
606
+ * it and you deliver the link yourself. Valid 7 days, single use, and
607
+ * not readable again later.
608
+ */
609
+ create: (tenantId, input) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/invites`, {
610
+ method: "POST",
611
+ envBearer: true,
612
+ body: {
613
+ email: input.email,
614
+ roles: input.roles,
615
+ sendEmail: input.sendEmail ?? true,
616
+ redirectUri: input.redirectUri
617
+ }
618
+ }),
619
+ /** POST .../invites/{inviteId}/resend — new link (the previous one stops working), mailed unless `sendEmail` is `false`. */
620
+ resend: (tenantId, inviteId, input = {}) => this.request(
621
+ `/env/tenants/${encodeURIComponent(tenantId)}/invites/${encodeURIComponent(inviteId)}/resend`,
622
+ { method: "POST", envBearer: true, body: { sendEmail: input.sendEmail ?? true } }
623
+ ),
624
+ /** DELETE /env/tenants/{tenantId}/invites/{inviteId} — revoke a pending invite. */
625
+ revoke: (tenantId, inviteId) => this.request(
626
+ `/env/tenants/${encodeURIComponent(tenantId)}/invites/${encodeURIComponent(inviteId)}`,
627
+ { method: "DELETE", envBearer: true }
628
+ ),
629
+ /**
630
+ * POST /auth/tenant-invites/preview — for a backend hosting the accept
631
+ * page. Public: needs only `envKey`, the token is the credential.
632
+ */
633
+ preview: (token) => this.request("/auth/tenant-invites/preview", { method: "POST", body: { token } }),
634
+ /**
635
+ * Joins the tenant; for an address with no account, also creates it
636
+ * (password required) with the e-mail already confirmed. Does not
637
+ * sign the user in.
638
+ *
639
+ * Without metadata: POST /auth/tenant-invites/accept — public, needs
640
+ * only `envKey`. With `publicMetadata`/`privateMetadata`: POST
641
+ * /env/tenant-invites/accept with the environment credential
642
+ * (`authyon:users:write`), since only your backend may set them.
643
+ */
644
+ accept: (input) => input.publicMetadata !== void 0 || input.privateMetadata !== void 0 ? this.request("/env/tenant-invites/accept", {
645
+ method: "POST",
646
+ envBearer: true,
647
+ body: input
648
+ }) : this.request("/auth/tenant-invites/accept", { method: "POST", body: input })
649
+ },
686
650
  roles: {
687
651
  /** GET /env/tenants/{tenantId}/roles — list a tenant's roles. */
688
652
  list: (tenantId) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/roles`, { envBearer: true }),
@@ -752,6 +716,60 @@ var AuthyonServerClient = class {
752
716
  loginActivity: (params = {}) => this.request("/env/audit/login-activity", { envBearer: true, query: params })
753
717
  }
754
718
  };
719
+ // ── Security: one-time codes and approvals ───────────────────────────────
720
+ //
721
+ // Everything here runs with the environment credential: `otp` needs the
722
+ // `authyon:otp:verify` scope, `approvals` needs `authyon:financial:authorize`.
723
+ // Keep those credentials on the server.
724
+ this.security = {
725
+ otp: {
726
+ /**
727
+ * POST /env/users/{userId}/otp/verify — checks the user's authenticator
728
+ * app (TOTP) code. A wrong code returns `{ valid: false }` instead of
729
+ * throwing. Each code is accepted once; 3 wrong codes in a row sign the
730
+ * user out everywhere (`sessionsRevoked: true`); 5 lock the check for 15
731
+ * minutes (`rate_limited`). More than 50 wrong codes in 10 minutes from the
732
+ * same credential pause it (`credential_throttled`). Needs `authyon:otp:verify`.
733
+ */
734
+ check: (userId, code) => this.request(`/env/users/${segment(userId)}/otp/verify`, {
735
+ method: "POST",
736
+ envBearer: true,
737
+ body: { code }
738
+ })
739
+ },
740
+ /**
741
+ * The customer approves a payload with their authenticator code or
742
+ * passkey, once.
743
+ *
744
+ * 1. `create()` the approval with the payload and the customer.
745
+ * 2. The customer confirms it (`@authyon/auth` → `security.approvals.confirm`).
746
+ * 3. `consume()` it right before executing, and execute the returned payload.
747
+ */
748
+ approvals: {
749
+ /** POST /env/authorizations — registers a payload awaiting the customer's approval. */
750
+ create: (input) => {
751
+ const { idempotencyKey, ...body } = input;
752
+ return this.request("/env/authorizations", {
753
+ method: "POST",
754
+ envBearer: true,
755
+ headers: idempotencyKey ? { "Idempotency-Key": idempotencyKey } : void 0,
756
+ body
757
+ });
758
+ },
759
+ /** GET /env/authorizations/{id} — current status and, once approved, the assurance evidence. */
760
+ get: (id) => this.request(`/env/authorizations/${segment(id)}`, { envBearer: true }),
761
+ /**
762
+ * POST /env/authorizations/{id}/consume — redeems an approved approval
763
+ * exactly once and returns the approved payload. Pass `payload` to have
764
+ * the API check it matches (`payload_mismatch` otherwise).
765
+ */
766
+ consume: (id, payload) => this.request(`/env/authorizations/${segment(id)}/consume`, {
767
+ method: "POST",
768
+ envBearer: true,
769
+ body: payload === void 0 ? void 0 : { payload }
770
+ })
771
+ }
772
+ };
755
773
  // ── Generic permission grants ─────────────────────────────────────────────
756
774
  this.permissions = {
757
775
  /** POST /users/{userId}/permissions — grant a permission directly to a user. */
@@ -768,7 +786,8 @@ var AuthyonServerClient = class {
768
786
  };
769
787
  this.envKey = options.envKey;
770
788
  this.clientId = options.clientId;
771
- this.clientSecret = options.clientSecret;
789
+ __privateSet(this, _clientSecret, options.clientSecret);
790
+ this.clientIp = options.clientIp;
772
791
  this.transport = createSharedTransport({
773
792
  baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
774
793
  allowInsecureHttp: options.allowInsecureHttp,
@@ -779,14 +798,14 @@ var AuthyonServerClient = class {
779
798
  });
780
799
  this.http = new JsonHttpClient(this.transport);
781
800
  this.environmentTokenProvider = new ExpiringTokenProvider(async () => {
782
- if (!this.clientId || !this.clientSecret) {
801
+ if (!this.clientId || !__privateGet(this, _clientSecret)) {
783
802
  throw new Error(
784
803
  "Authyon: `clientId`/`clientSecret` are required for environment management calls (mint a pair in the console, under the environment's OAuth clients)."
785
804
  );
786
805
  }
787
806
  const token = await this.environmentAuth.token({
788
807
  clientId: this.clientId,
789
- clientSecret: this.clientSecret
808
+ clientSecret: __privateGet(this, _clientSecret)
790
809
  });
791
810
  return {
792
811
  accessToken: token.access_token,
@@ -794,13 +813,16 @@ var AuthyonServerClient = class {
794
813
  };
795
814
  });
796
815
  }
797
- async request(path, options = {}) {
798
- const headers = { ...options.headers };
816
+ async request(path2, options = {}) {
817
+ const headers = {
818
+ ...clientIpHeaders({ clientIp: this.clientIp }),
819
+ ...options.headers
820
+ };
799
821
  if (options.envBearer) {
800
822
  headers.Authorization = `Bearer ${await this.getEnvironmentAccessToken()}`;
801
823
  }
802
824
  if (this.envKey) headers["X-Authyon-Environment"] = this.envKey;
803
- return this.http.request(path, { ...options, headers });
825
+ return this.http.request(path2, { ...options, headers });
804
826
  }
805
827
  /** Mints (and caches) an environment access token from `clientId`/`clientSecret`. */
806
828
  async getEnvironmentAccessToken() {
@@ -855,26 +877,52 @@ var AuthyonServerClient = class {
855
877
  tenant(credentials) {
856
878
  return new TenantScopedClient(this, credentials);
857
879
  }
880
+ /** Uses an end-user token; the configured environment key selects the environment. */
881
+ user(token) {
882
+ return new UserScopedClient((path2, options) => this.request(path2, options), token);
883
+ }
858
884
  /** @internal used by {@link TenantScopedClient} to share the base URL/fetch/error handling. */
859
- _requestAsTenant(accessToken, path, options = {}) {
860
- return this.request(path, {
885
+ _requestAsTenant(accessToken, path2, options = {}) {
886
+ return this.request(path2, {
861
887
  ...options,
862
888
  headers: { ...options.headers, Authorization: `Bearer ${accessToken}` },
863
889
  envBearer: false
864
890
  });
865
891
  }
866
892
  };
893
+ _clientSecret = new WeakMap();
867
894
  var TenantScopedClient = class {
868
895
  constructor(server, credentials) {
896
+ /** Invites to the token's tenant, by e-mail or by link. */
897
+ this.invites = {
898
+ /** GET /tenant/invites — every invite with its status, newest first. */
899
+ list: () => this.request("/tenant/invites"),
900
+ /** POST /tenant/invites — always returns `acceptUrl`; `sendEmail: false` skips the e-mail. */
901
+ create: (input) => this.request("/tenant/invites", {
902
+ method: "POST",
903
+ body: {
904
+ email: input.email,
905
+ roles: input.roles,
906
+ sendEmail: input.sendEmail ?? true,
907
+ redirectUri: input.redirectUri
908
+ }
909
+ }),
910
+ /** POST /tenant/invites/{inviteId}/resend — new link (the previous one stops working), mailed unless `sendEmail` is `false`. */
911
+ resend: (inviteId, input = {}) => this.request(`/tenant/invites/${encodeURIComponent(inviteId)}/resend`, {
912
+ method: "POST",
913
+ body: { sendEmail: input.sendEmail ?? true }
914
+ }),
915
+ /** DELETE /tenant/invites/{inviteId} — revoke a pending invite. */
916
+ revoke: (inviteId) => this.request(`/tenant/invites/${encodeURIComponent(inviteId)}`, { method: "DELETE" })
917
+ };
869
918
  this.members = {
870
919
  /**
871
920
  * GET /tenant/members — members of the token's tenant.
872
921
  *
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.
922
+ * Same handler as `/env/tenants/{id}/members` (the tenant comes from the
923
+ * token instead of the path), so the same `Paged<T>` envelope of full
924
+ * user records, `take` defaulting to 50 and capped at 100 — as opposed
925
+ * to `/auth/tenants/{id}/members`'s lightweight member shape.
878
926
  */
879
927
  list: (params = {}) => this.request("/tenant/members", { query: params }),
880
928
  /** POST /tenant/members — add a member to the token's tenant. */
@@ -933,8 +981,12 @@ var TenantScopedClient = class {
933
981
  async getAccessToken() {
934
982
  return this.tokenProvider.getAccessToken();
935
983
  }
936
- async request(path, options = {}) {
937
- return this.server._requestAsTenant(await this.getAccessToken(), path, options);
984
+ async request(path2, options = {}) {
985
+ return this.server._requestAsTenant(await this.getAccessToken(), path2, options);
986
+ }
987
+ /** POST /tenant/auth/validate — validates this tenant client's current bearer token. */
988
+ async validate(context) {
989
+ return this.server.tenantAuth.validate(await this.getAccessToken(), context);
938
990
  }
939
991
  };
940
992
  function createClient(options = {}) {
@@ -942,342 +994,126 @@ function createClient(options = {}) {
942
994
  }
943
995
 
944
996
  // src/client/authyonServerClientBuilder.ts
997
+ var _options;
945
998
  var AuthyonServerClientBuilder = class {
946
999
  constructor() {
947
- this.options = {};
1000
+ __privateAdd(this, _options, {});
948
1001
  }
949
1002
  withEnvironmentKey(envKey) {
950
- this.options.envKey = envKey;
1003
+ __privateGet(this, _options).envKey = envKey;
951
1004
  return this;
952
1005
  }
953
1006
  withEnvironmentCredentials(clientId, clientSecret) {
954
- this.options.clientId = clientId;
955
- this.options.clientSecret = clientSecret;
1007
+ __privateGet(this, _options).clientId = clientId;
1008
+ __privateGet(this, _options).clientSecret = clientSecret;
1009
+ return this;
1010
+ }
1011
+ withClientIp(clientIp) {
1012
+ __privateGet(this, _options).clientIp = clientIp;
956
1013
  return this;
957
1014
  }
958
1015
  withBaseUrl(baseUrl, allowInsecureHttp = false) {
959
- this.options.baseUrl = baseUrl;
960
- this.options.allowInsecureHttp = allowInsecureHttp;
1016
+ __privateGet(this, _options).baseUrl = baseUrl;
1017
+ __privateGet(this, _options).allowInsecureHttp = allowInsecureHttp;
961
1018
  return this;
962
1019
  }
963
1020
  withTimeout(timeoutMs) {
964
- this.options.timeoutMs = timeoutMs;
1021
+ __privateGet(this, _options).timeoutMs = timeoutMs;
965
1022
  return this;
966
1023
  }
967
1024
  withHttpAdapter(httpAdapter) {
968
- this.options.httpAdapter = httpAdapter;
1025
+ __privateGet(this, _options).httpAdapter = httpAdapter;
969
1026
  return this;
970
1027
  }
971
1028
  withHttpLogger(httpLogger) {
972
- this.options.httpLogger = httpLogger;
1029
+ __privateGet(this, _options).httpLogger = httpLogger;
973
1030
  return this;
974
1031
  }
975
1032
  build() {
976
- return new AuthyonServerClient({ ...this.options });
1033
+ return new AuthyonServerClient({ ...__privateGet(this, _options) });
977
1034
  }
978
1035
  };
1036
+ _options = new WeakMap();
979
1037
 
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")
1038
+ // src/client/tenantFilters.ts
1039
+ var TENANT_FILTER_MAX_VALUES = 25;
1040
+ var TENANT_FILTER_MAX_VALUE_LENGTH = 200;
1041
+ var TENANT_FILTER_MAX_DEPTH = 3;
1042
+ var SCOPES = ["public", "private", "custom"];
1043
+ var KEY_SEGMENT = /^[A-Za-z0-9_-]{1,64}$/;
1044
+ function assertPath(path2) {
1045
+ if (typeof path2 !== "string" || path2.length === 0) {
1046
+ throw new RangeError("Authyon: a filter path is required, e.g. public.kind.");
1047
+ }
1048
+ const [scope, ...keys] = path2.split(".");
1049
+ if (!SCOPES.includes(scope)) {
1050
+ throw new RangeError(
1051
+ `Authyon: unknown filter scope "${scope}" \u2014 use ${SCOPES.join(", ")} (e.g. public.kind).`
1011
1052
  );
1012
1053
  }
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 = [];
1054
+ if (keys.length === 0) {
1055
+ throw new RangeError(`Authyon: filter path "${path2}" needs a key after the scope.`);
1026
1056
  }
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;
1057
+ if (keys.length > TENANT_FILTER_MAX_DEPTH) {
1058
+ throw new RangeError(
1059
+ `Authyon: filter path "${path2}" goes deeper than ${TENANT_FILTER_MAX_DEPTH} levels.`
1060
+ );
1034
1061
  }
1035
- build(options = {}) {
1036
- return new AuthyonAbility(this.rules, options.detectSubjectType);
1062
+ for (const key of keys) {
1063
+ if (!KEY_SEGMENT.test(key)) {
1064
+ throw new RangeError(
1065
+ `Authyon: invalid filter key "${key}" \u2014 letters, digits, "_" and "-", up to 64 characters.`
1066
+ );
1067
+ }
1037
1068
  }
1038
- };
1039
- function createAuthyonAbility(source = {}, options = {}) {
1040
- return new AuthyonAbility(createAuthyonRules(source, options), options.detectSubjectType);
1069
+ return path2;
1041
1070
  }
1042
- function hasPermission(source, requiredPermission) {
1043
- const requirement = permissionToRule(requiredPermission);
1044
- if (!requirement || typeof requirement.action !== "string" || typeof requirement.subject !== "string") {
1045
- return false;
1071
+ function assertValue(value, forIn) {
1072
+ if (value === void 0 || value === null) {
1073
+ throw new RangeError("Authyon: a filter value is required.");
1046
1074
  }
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));
1075
+ const text = String(value);
1076
+ if (text.length === 0) {
1077
+ throw new RangeError("Authyon: a filter value may not be empty.");
1061
1078
  }
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" };
1079
+ if (text.length > TENANT_FILTER_MAX_VALUE_LENGTH) {
1080
+ throw new RangeError(
1081
+ `Authyon: a filter value may hold at most ${TENANT_FILTER_MAX_VALUE_LENGTH} characters.`
1082
+ );
1070
1083
  }
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);
1084
+ if (text.includes(",")) {
1085
+ throw new RangeError(
1086
+ forIn ? "Authyon: an `in` value may not contain a comma \u2014 pass separate values instead." : "Authyon: a filter value may not contain a comma."
1087
+ );
1112
1088
  }
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;
1089
+ return text;
1160
1090
  }
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");
1091
+ var tenantFilter = {
1092
+ /** `public.kind:eq:reseller` — the key equals this value. */
1093
+ eq(path2, value) {
1094
+ return `${assertPath(path2)}:eq:${assertValue(value, false)}`;
1095
+ },
1096
+ /** `public.kind:in:reseller,partner` — the key equals any of these (SQL IN). */
1097
+ in(path2, values) {
1098
+ if (!Array.isArray(values) || values.length === 0) {
1099
+ throw new RangeError("Authyon: `in` needs at least one value.");
1180
1100
  }
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();
1101
+ if (values.length > TENANT_FILTER_MAX_VALUES) {
1102
+ throw new RangeError(`Authyon: \`in\` takes at most ${TENANT_FILTER_MAX_VALUES} values.`);
1194
1103
  }
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 ?? [];
1104
+ const unique = [...new Set(values.map((value) => assertValue(value, true)))];
1105
+ return `${assertPath(path2)}:in:${unique.join(",")}`;
1106
+ },
1107
+ /** `public.notes:like:nacional` — the key contains this text, case-insensitive.
1108
+ * The server escapes the LIKE metacharacters, so `100%` means `100%`. */
1109
+ like(path2, text) {
1110
+ return `${assertPath(path2)}:like:${assertValue(text, false)}`;
1111
+ },
1112
+ /** `custom.cnpj:exists` — the key is present, whatever its value. */
1113
+ exists(path2) {
1114
+ return `${assertPath(path2)}:exists`;
1212
1115
  }
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
- }
1116
+ };
1281
1117
  export {
1282
1118
  AuthorizationError,
1283
1119
  AuthyonAbility,
@@ -1289,8 +1125,12 @@ export {
1289
1125
  FetchHttpAdapter,
1290
1126
  JwksTokenVerifier,
1291
1127
  LoggingHttpAdapter,
1128
+ TENANT_FILTER_MAX_DEPTH,
1129
+ TENANT_FILTER_MAX_VALUES,
1130
+ TENANT_FILTER_MAX_VALUE_LENGTH,
1292
1131
  TenantScopedClient,
1293
1132
  TokenVerificationError,
1133
+ UserScopedClient,
1294
1134
  authorizeRequest,
1295
1135
  authorizeToken,
1296
1136
  createAuthorizationErrorResponse,
@@ -1299,5 +1139,7 @@ export {
1299
1139
  createClient,
1300
1140
  createExpressAuthorizationMiddleware,
1301
1141
  hasPermission,
1302
- normalizeClientIp
1142
+ hasPermissionGroup,
1143
+ normalizeClientIp,
1144
+ tenantFilter
1303
1145
  };