@authyon/server 0.2.0-beta.1 → 0.2.0-beta.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,501 +1,314 @@
1
- // ../../internal/core/config/defaults.ts
2
- var DEFAULT_BASE_URL = "https://api.authyon.com";
3
- var DEFAULT_EXPIRY_SKEW_MS = 3e4;
1
+ import {
2
+ AuthyonError,
3
+ DEFAULT_BASE_URL,
4
+ DEFAULT_EXPIRY_SKEW_MS,
5
+ ErrorCodes,
6
+ FetchHttpAdapter,
7
+ JsonHttpClient,
8
+ LoggingHttpAdapter,
9
+ createSharedTransport
10
+ } from "./chunk-56H66KLG.js";
11
+ import {
12
+ AuthorizationError,
13
+ AuthyonAbility,
14
+ AuthyonAbilityBuilder,
15
+ JwksTokenVerifier,
16
+ TokenVerificationError,
17
+ authorizeRequest,
18
+ authorizeToken,
19
+ clientIpHeaders,
20
+ createAuthorizationErrorResponse,
21
+ createAuthyonAbility,
22
+ createAuthyonRules,
23
+ createExpressAuthorizationMiddleware,
24
+ hasPermission,
25
+ hasPermissionGroup,
26
+ normalizeClientIp
27
+ } from "./chunk-NKWQMYB3.js";
28
+ import {
29
+ __privateAdd,
30
+ __privateGet,
31
+ __privateSet
32
+ } from "./chunk-77J4NURK.js";
4
33
 
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;
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.");
38
+ }
39
+ return encodeURIComponent(value);
40
+ }
41
+ function permissionsBody(input) {
42
+ if (input.permissions !== void 0 && input.scopes !== void 0) {
43
+ throw new TypeError("Authyon: supply permissions or scopes, not both.");
44
+ }
45
+ const values = input.permissions ?? input.scopes;
46
+ if (!Array.isArray(values) || values.length === 0) {
47
+ throw new TypeError("Authyon: explicit credential permissions are required.");
48
+ }
49
+ const permissions = values.map((value) => {
50
+ if (typeof value !== "string") throw new TypeError("Authyon: invalid permission.");
51
+ const normalized = value.trim().toLowerCase();
52
+ if (!/^(?:\*|[a-z][a-z0-9_-]*):(?:\*|[a-z][a-z0-9_-]*):(?:\*|[a-z][a-z0-9_-]*)$/u.test(normalized)) {
53
+ throw new TypeError("Authyon: permissions must have namespace:resource:action format.");
16
54
  }
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
- }
55
+ return normalized;
56
+ });
57
+ return { permissions: [...new Set(permissions)] };
58
+ }
59
+ function lifetimeBody(input) {
60
+ if (input.lifetimeDays === void 0 || input.lifetimeDays === null) return {};
61
+ if (!Number.isInteger(input.lifetimeDays) || input.lifetimeDays < 1 || input.lifetimeDays > 3650) {
62
+ throw new RangeError("Authyon: lifetimeDays must be between 1 and 3650.");
63
+ }
64
+ return { lifetimeDays: input.lifetimeDays };
65
+ }
66
+ function credentialQuery(options = {}) {
67
+ const { skip, take, search } = options;
68
+ if (skip !== void 0 && (!Number.isInteger(skip) || skip < 0 || skip > 2147483647))
69
+ throw new RangeError("Authyon: invalid skip.");
70
+ if (take !== void 0 && (!Number.isInteger(take) || take < 1 || take > 100))
71
+ throw new RangeError("Authyon: take must be between 1 and 100.");
72
+ if (search !== void 0 && (typeof search !== "string" || search.trim().length > 200))
73
+ throw new RangeError("Authyon: search must contain at most 200 characters.");
74
+ return { search: search?.trim() || void 0, skip, take };
75
+ }
76
+ function credentialSummary(value) {
77
+ return {
78
+ id: value.id,
79
+ clientId: value.clientId,
80
+ description: value.description,
81
+ createdAt: value.createdAt,
82
+ lastUsedAt: value.lastUsedAt,
83
+ revokedAt: value.revokedAt,
84
+ expiresAt: value.expiresAt ?? null,
85
+ isActive: value.isActive,
86
+ permissions: [...value.permissions]
87
+ };
134
88
  }
135
- function isRetryable(category) {
136
- return category === "network" || category === "timeout" || category === "rate_limit" || category === "server";
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
+ };
137
104
  }
138
-
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));
145
- }
146
- const value = query.toString();
147
- return value ? `${path}${path.includes("?") ? "&" : "?"}${value}` : path;
105
+ async function credentialPage(request, path, options = {}) {
106
+ const query = credentialQuery(options);
107
+ const page = await request(path, { query });
108
+ if (!page || !Array.isArray(page.data))
109
+ throw new TypeError("Authyon: expected a paginated credential response.");
110
+ return {
111
+ data: page.data.map(credentialSummary),
112
+ ...page.perPage === void 0 ? {} : { perPage: page.perPage },
113
+ pageSize: page.pageSize,
114
+ total: page.total,
115
+ pages: page.pages,
116
+ hasNext: page.hasNext,
117
+ hasPrev: page.hasPrev
118
+ };
148
119
  }
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;
120
+ async function allCredentials(request, path) {
121
+ const result = [];
122
+ const seen = /* @__PURE__ */ new Set();
123
+ let skip = 0;
124
+ for (let iteration = 0; iteration < 1e3; iteration++) {
125
+ const page = await request(
126
+ path,
127
+ iteration === 0 ? {} : { query: { skip, take: 100 } }
128
+ );
129
+ if (Array.isArray(page)) {
130
+ if (iteration !== 0)
131
+ throw new Error("Authyon: credential pagination changed during listing.");
132
+ return page.map(credentialSummary);
196
133
  }
197
- }
198
- log(event) {
199
- if (!this.options.enabled) return;
200
- const logger = this.options.logger ?? defaultHttpLogger;
201
- try {
202
- logger(event);
203
- } catch {
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));
204
141
  }
142
+ if (!page.hasNext) return result;
143
+ if (page.data.length === 0) throw new Error("Authyon: credential pagination made no progress.");
144
+ skip += page.data.length;
205
145
  }
206
- };
207
- function defaultHttpLogger(event) {
208
- console.debug("[Authyon HTTP]", event);
209
- }
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();
146
+ throw new Error("Authyon: use listPage for more than 1000 credential pages.");
216
147
  }
217
148
 
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;
239
- 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
- }
149
+ // src/client/scopedManagementClient.ts
150
+ function scopedRequest(request, source) {
151
+ return async (path, options = {}) => {
152
+ const token = typeof source === "function" ? await source() : source;
153
+ if (typeof token !== "string" || !token.trim() || /\s/u.test(token))
154
+ throw new TypeError("Authyon: a valid access token is required.");
155
+ return request(path, {
156
+ ...options,
157
+ headers: { ...options.headers, Authorization: `Bearer ${token}` }
158
+ });
261
159
  };
262
160
  }
263
- function normalizeHeaders(headers) {
264
- const normalized = {};
265
- new Headers(headers).forEach((value, key) => {
266
- normalized[key] = value;
267
- });
268
- return normalized;
161
+ function environmentPath(scope) {
162
+ return `/platform/workspaces/${segment(scope.workspaceId)}/environments/${segment(scope.environmentId)}`;
269
163
  }
270
- function responseMetadata(response) {
271
- const retryAfterHeader = response.headers.get("retry-after");
272
- const retryAfter = retryAfterHeader ? Number(retryAfterHeader) : void 0;
273
- return {
274
- requestId: response.headers.get("x-request-id") ?? response.headers.get("trace-id") ?? void 0,
275
- retryAfter: Number.isFinite(retryAfter) ? retryAfter : void 0
276
- };
164
+ function tenantPath(scope) {
165
+ return `${environmentPath(scope)}/tenants/${segment(scope.tenantId)}`;
277
166
  }
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"
294
- );
295
- }
296
- return url.toString().replace(/\/+$/, "");
167
+ function credentialPath(scope) {
168
+ return `${scope.tenantId === void 0 ? environmentPath(scope) : tenantPath(scope)}/credentials`;
297
169
  }
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 });
316
- }
317
- }
318
- async parse(response) {
319
- if (!response.ok) {
320
- let body = {};
321
- try {
322
- body = await response.json();
323
- } catch {
170
+ function workspacePath(id) {
171
+ return `/platform/workspaces/${segment(id)}`;
172
+ }
173
+ function workspaceRole(role) {
174
+ if (role !== "admin" && role !== "auditor")
175
+ throw new TypeError("Authyon: workspace role must be admin or auditor.");
176
+ return role;
177
+ }
178
+ var _request;
179
+ var PlatformScopedClient = class {
180
+ /** @hidden */
181
+ constructor(request, token) {
182
+ __privateAdd(this, _request);
183
+ this.credentials = {
184
+ list: (scope, options = {}) => credentialPage(__privateGet(this, _request), credentialPath(scope), options),
185
+ get: async (scope, credentialId) => credentialDetail(await __privateGet(this, _request).call(this, `${credentialPath(scope)}/${segment(credentialId)}`)),
186
+ create: (scope, input) => __privateGet(this, _request).call(this, credentialPath(scope), {
187
+ method: "POST",
188
+ body: { description: input.description, ...lifetimeBody(input), ...permissionsBody(input) }
189
+ }),
190
+ updatePermissions: (scope, credentialId, input) => __privateGet(this, _request).call(this, `${credentialPath(scope)}/${segment(credentialId)}/permissions`, {
191
+ method: "PUT",
192
+ body: permissionsBody(input)
193
+ }),
194
+ updateScopes: (scope, credentialId, scopes) => this.credentials.updatePermissions(scope, credentialId, { scopes }),
195
+ /** Returns a new secret once. API may require an explicit step-up first. */
196
+ rotate: (scope, credentialId) => __privateGet(this, _request).call(this, `${credentialPath(scope)}/${segment(credentialId)}/rotate`, { method: "POST" }),
197
+ revoke: (scope, credentialId) => __privateGet(this, _request).call(this, `${credentialPath(scope)}/${segment(credentialId)}`, { method: "DELETE" })
198
+ };
199
+ this.workspaces = {
200
+ team: (workspaceId) => __privateGet(this, _request).call(this, `${workspacePath(workspaceId)}/team`),
201
+ members: {
202
+ invite: (workspaceId, input) => __privateGet(this, _request).call(this, `${workspacePath(workspaceId)}/invites`, {
203
+ method: "POST",
204
+ body: { email: input.email, role: workspaceRole(input.role) }
205
+ }),
206
+ /** memberId comes from team.members; it is not the platform userId. */
207
+ remove: (workspaceId, memberId) => __privateGet(this, _request).call(this, `${workspacePath(workspaceId)}/members/${segment(memberId)}`, {
208
+ method: "DELETE"
209
+ }),
210
+ changeRole: (workspaceId, memberId, role) => __privateGet(this, _request).call(this, `${workspacePath(workspaceId)}/members/${segment(memberId)}`, {
211
+ method: "PATCH",
212
+ body: { role: workspaceRole(role) }
213
+ })
214
+ },
215
+ invites: {
216
+ create: (workspaceId, input) => this.workspaces.members.invite(workspaceId, input),
217
+ revoke: (workspaceId, inviteId) => __privateGet(this, _request).call(this, `${workspacePath(workspaceId)}/invites/${segment(inviteId)}`, {
218
+ method: "DELETE"
219
+ })
324
220
  }
325
- throw new AuthyonError(response.status, body, responseMetadata(response));
326
- }
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));
221
+ };
222
+ this.tenants = {
223
+ members: {
224
+ list: (scope, options = {}) => __privateGet(this, _request).call(this, `${tenantPath(scope)}/members`, { query: options }),
225
+ add: (scope, userId, roles) => __privateGet(this, _request).call(this, `${tenantPath(scope)}/members`, { method: "POST", body: { userId, roles } }),
226
+ remove: (scope, userId) => __privateGet(this, _request).call(this, `${tenantPath(scope)}/members/${segment(userId)}`, { method: "DELETE" })
227
+ }
228
+ };
229
+ this.auth = {
230
+ /** Explicitly confirms the current platform session; passwords are not retained. */
231
+ stepUp: (currentPassword) => __privateGet(this, _request).call(this, "/platform/auth/step-up", { method: "POST", body: { currentPassword } })
232
+ };
233
+ __privateSet(this, _request, scopedRequest(request, token));
332
234
  }
333
235
  };
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");
355
- }
356
-
357
- // src/security/jwksTokenVerifier.ts
358
- import {
359
- createLocalJWKSet,
360
- createRemoteJWKSet,
361
- jwtVerify
362
- } from "jose";
363
- var SAFE_ASYMMETRIC_ALGORITHMS = [
364
- "RS256",
365
- "PS256",
366
- "ES256",
367
- "EdDSA"
368
- ];
369
- var TokenVerificationError = class extends Error {
370
- constructor(code, options = {}) {
371
- super(messageForCode(code));
372
- this.code = code;
373
- this.name = "TokenVerificationError";
374
- if (options.cause !== void 0) Object.defineProperty(this, "cause", { value: options.cause });
236
+ _request = new WeakMap();
237
+ var _request2;
238
+ var UserScopedClient = class {
239
+ /** @hidden */
240
+ constructor(request, token) {
241
+ __privateAdd(this, _request2);
242
+ this.tenants = {
243
+ members: {
244
+ list: (tenantId, options = {}) => __privateGet(this, _request2).call(this, `/auth/tenants/${segment(tenantId)}/members`, { query: options }),
245
+ /** Adds an existing environment user by email; does not send an invitation email. */
246
+ invite: (tenantId, input) => __privateGet(this, _request2).call(this, `/auth/tenants/${segment(tenantId)}/members`, {
247
+ method: "POST",
248
+ body: { email: input.email, roles: input.roles }
249
+ }),
250
+ remove: (tenantId, userId) => __privateGet(this, _request2).call(this, `/auth/tenants/${segment(tenantId)}/members/${segment(userId)}`, {
251
+ method: "DELETE"
252
+ })
253
+ }
254
+ };
255
+ __privateSet(this, _request2, scopedRequest(request, token));
375
256
  }
376
257
  };
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
258
+ _request2 = new WeakMap();
259
+ var _request3;
260
+ var WorkspaceInvitesClient = class {
261
+ /** @hidden */
262
+ constructor(request) {
263
+ __privateAdd(this, _request3);
264
+ __privateSet(this, _request3, request);
265
+ }
266
+ preview(token) {
267
+ return __privateGet(this, _request3).call(this, `/platform/workspace-invites/${segment(token)}`);
268
+ }
269
+ accept(token, input = {}) {
270
+ return __privateGet(this, _request3).call(this, `/platform/workspace-invites/${segment(token)}/accept`, {
271
+ method: "POST",
272
+ body: { name: input.name, password: input.password }
387
273
  });
388
274
  }
389
- async verifyAccessToken(token) {
390
- if (!token || token.length > 16384) {
391
- throw new TokenVerificationError("token.invalid");
392
- }
393
- try {
394
- const result = await jwtVerify(token, this.verifyKey, {
395
- algorithms: this.algorithms,
396
- issuer: this.options.issuer,
397
- audience: this.options.audience,
398
- clockTolerance: this.options.clockToleranceSeconds ?? 5,
399
- maxTokenAge: this.options.maxTokenAgeSeconds ?? 3600,
400
- requiredClaims: ["iss", "aud", "sub", "exp", "iat"]
401
- });
402
- validateTokenType(result, this.acceptedTokenTypes);
403
- validateClaims(result.payload);
404
- return { claims: result.payload, protectedHeader: result.protectedHeader };
405
- } catch (cause) {
406
- if (cause instanceof TokenVerificationError) throw cause;
407
- throw new TokenVerificationError(errorCode(cause), { cause });
408
- }
409
- }
410
275
  };
411
- function validateTokenType(result, acceptedTokenTypes) {
412
- const type = result.protectedHeader.typ;
413
- if (!type || !acceptedTokenTypes.has(type)) {
414
- throw new TokenVerificationError("token.invalid_claims");
415
- }
416
- }
417
- function validateClaims(claims) {
418
- if (typeof claims.sub !== "string" || !claims.sub) invalidClaims();
419
- if (claims.permissions !== void 0 && !isStringArray(claims.permissions)) invalidClaims();
420
- if (claims.roles !== void 0 && !isStringArray(claims.roles)) invalidClaims();
421
- if (claims.scope !== void 0 && typeof claims.scope !== "string") invalidClaims();
422
- }
423
- function validateOptions(options) {
424
- const issuer = secureUrl(options.issuer, "issuer");
425
- if (Boolean(options.jwksUri) === Boolean(options.jwks)) {
426
- throw new Error("Authyon: configure exactly one of `jwksUri` or `jwks`");
427
- }
428
- const jwks = options.jwksUri ? secureUrl(options.jwksUri, "jwksUri") : void 0;
429
- if (!options.audience || Array.isArray(options.audience) && options.audience.length === 0) {
430
- throw new Error("Authyon: JWT `audience` is required");
431
- }
432
- if (jwks && issuer.origin !== jwks.origin) {
433
- throw new Error("Authyon: `issuer` and `jwksUri` must use the same trusted origin");
434
- }
435
- const algorithms = options.algorithms ?? ["RS256"];
436
- if (algorithms.length === 0 || algorithms.some((algorithm) => !SAFE_ASYMMETRIC_ALGORITHMS.includes(algorithm))) {
437
- throw new Error("Authyon: only approved asymmetric JWT algorithms are accepted");
438
- }
439
- if (options.acceptedTokenTypes?.length === 0 || options.acceptedTokenTypes?.some((type) => !type.trim())) {
440
- throw new Error("Authyon: `acceptedTokenTypes` must contain non-empty values");
276
+ _request3 = new WeakMap();
277
+
278
+ // ../../internal/core/auth/expiringTokenProvider.ts
279
+ var _token, _inFlight;
280
+ var ExpiringTokenProvider = class {
281
+ constructor(acquire, expirySkewMs = DEFAULT_EXPIRY_SKEW_MS) {
282
+ this.acquire = acquire;
283
+ this.expirySkewMs = expirySkewMs;
284
+ __privateAdd(this, _token, null);
285
+ __privateAdd(this, _inFlight, null);
441
286
  }
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`);
287
+ async getAccessToken() {
288
+ if (__privateGet(this, _token) && Date.now() < __privateGet(this, _token).expiresAt - this.expirySkewMs) {
289
+ return __privateGet(this, _token).accessToken;
451
290
  }
291
+ if (__privateGet(this, _inFlight)) return __privateGet(this, _inFlight);
292
+ __privateSet(this, _inFlight, this.acquire().then((token) => {
293
+ __privateSet(this, _token, token);
294
+ return token.accessToken;
295
+ }).finally(() => {
296
+ __privateSet(this, _inFlight, null);
297
+ }));
298
+ return __privateGet(this, _inFlight);
452
299
  }
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";
300
+ clear() {
301
+ __privateSet(this, _token, null);
477
302
  }
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
- }
303
+ };
304
+ _token = new WeakMap();
305
+ _inFlight = new WeakMap();
495
306
 
496
307
  // src/client/authyonServerClient.ts
308
+ var _clientSecret;
497
309
  var AuthyonServerClient = class {
498
310
  constructor(options = {}) {
311
+ __privateAdd(this, _clientSecret);
499
312
  // ── Discovery ──────────────────────────────────────────────────────────────
500
313
  this.discovery = {
501
314
  /** GET /.well-known/jwks.json — the environment's public signing keys. */
@@ -529,8 +342,19 @@ var AuthyonServerClient = class {
529
342
  client_id: credentials.clientId,
530
343
  client_secret: credentials.clientSecret
531
344
  }
345
+ }),
346
+ /** POST /tenant/auth/validate — validates an already-minted tenant-client bearer token. */
347
+ validate: (accessToken, context) => this.request("/tenant/auth/validate", {
348
+ method: "POST",
349
+ headers: {
350
+ ...clientIpHeaders(context),
351
+ Authorization: `Bearer ${accessToken}`
352
+ }
532
353
  })
533
354
  };
355
+ this.workspaceInvites = new WorkspaceInvitesClient(
356
+ (path, options) => this.http.request(path, options)
357
+ );
534
358
  // ── Environment management (users, tenants, roles, permissions, audit) ───
535
359
  this.environment = {
536
360
  users: {
@@ -622,8 +446,53 @@ var AuthyonServerClient = class {
622
446
  )
623
447
  },
624
448
  tenants: {
625
- /** GET /env/tenants — list tenants in the environment. */
626
- list: () => this.request("/env/tenants", { envBearer: true }),
449
+ credentials: {
450
+ /** GET /env/tenants/{tenantId}/credentials. Returns metadata only, including revoked credentials. */
451
+ list: (tenantId) => allCredentials(
452
+ (path, options) => this.request(path, { ...options, envBearer: true }),
453
+ `/env/tenants/${segment(tenantId)}/credentials`
454
+ ),
455
+ /** Paginated searchable listing. Prefer this over loading every credential. */
456
+ listPage: (tenantId, options = {}) => credentialPage(
457
+ (path, requestOptions) => this.request(path, { ...requestOptions, envBearer: true }),
458
+ `/env/tenants/${segment(tenantId)}/credentials`,
459
+ options
460
+ ),
461
+ get: async (tenantId, credentialId) => credentialDetail(
462
+ await this.request(
463
+ `/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}`,
464
+ { envBearer: true }
465
+ )
466
+ ),
467
+ /** Explicit scopes/permissions; only issuance returns the client secret. */
468
+ create: (tenantId, input) => this.request(`/env/tenants/${segment(tenantId)}/credentials`, {
469
+ method: "POST",
470
+ envBearer: true,
471
+ body: {
472
+ description: input.description,
473
+ ...lifetimeBody(input),
474
+ ...permissionsBody(input)
475
+ }
476
+ }),
477
+ /** Returns a new secret once while keeping the same client id. */
478
+ rotate: (tenantId, credentialId) => this.request(
479
+ `/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}/rotate`,
480
+ { method: "POST", envBearer: true }
481
+ ),
482
+ updatePermissions: (tenantId, credentialId, input) => this.request(
483
+ `/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}/permissions`,
484
+ { method: "PUT", envBearer: true, body: permissionsBody(input) }
485
+ ),
486
+ updateScopes: (tenantId, credentialId, scopes) => this.environment.tenants.credentials.updatePermissions(tenantId, credentialId, {
487
+ scopes
488
+ }),
489
+ revoke: (tenantId, credentialId) => this.request(`/env/tenants/${segment(tenantId)}/credentials/${segment(credentialId)}`, {
490
+ method: "DELETE",
491
+ envBearer: true
492
+ })
493
+ },
494
+ /** GET /env/tenants — paginated, searchable list of tenants in the environment. */
495
+ list: (params = {}) => this.request("/env/tenants", { envBearer: true, query: params }),
627
496
  /** POST /env/tenants — create a tenant in the environment. */
628
497
  create: (input) => this.request("/env/tenants", { method: "POST", envBearer: true, body: input }),
629
498
  /** GET /env/tenants/{tenantId} — fetch a tenant by id. */
@@ -634,7 +503,18 @@ var AuthyonServerClient = class {
634
503
  envBearer: true,
635
504
  body: input
636
505
  }),
637
- /** DELETE /env/tenants/{tenantId} — delete a tenant. */
506
+ /** PUT /env/tenants/{tenantId}/metadata - replace public and private metadata. */
507
+ updateMetadata: (tenantId, input) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/metadata`, {
508
+ method: "PUT",
509
+ envBearer: true,
510
+ body: input
511
+ }),
512
+ /** POST /env/tenants/{tenantId}/disable — disable without removing tenant data. */
513
+ disable: (tenantId) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}/disable`, {
514
+ method: "POST",
515
+ envBearer: true
516
+ }),
517
+ /** DELETE /env/tenants/{tenantId} — logically delete (disable) a tenant. */
638
518
  delete: (tenantId) => this.request(`/env/tenants/${encodeURIComponent(tenantId)}`, {
639
519
  method: "DELETE",
640
520
  envBearer: true
@@ -768,7 +648,8 @@ var AuthyonServerClient = class {
768
648
  };
769
649
  this.envKey = options.envKey;
770
650
  this.clientId = options.clientId;
771
- this.clientSecret = options.clientSecret;
651
+ __privateSet(this, _clientSecret, options.clientSecret);
652
+ this.clientIp = options.clientIp;
772
653
  this.transport = createSharedTransport({
773
654
  baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
774
655
  allowInsecureHttp: options.allowInsecureHttp,
@@ -779,14 +660,14 @@ var AuthyonServerClient = class {
779
660
  });
780
661
  this.http = new JsonHttpClient(this.transport);
781
662
  this.environmentTokenProvider = new ExpiringTokenProvider(async () => {
782
- if (!this.clientId || !this.clientSecret) {
663
+ if (!this.clientId || !__privateGet(this, _clientSecret)) {
783
664
  throw new Error(
784
665
  "Authyon: `clientId`/`clientSecret` are required for environment management calls (mint a pair in the console, under the environment's OAuth clients)."
785
666
  );
786
667
  }
787
668
  const token = await this.environmentAuth.token({
788
669
  clientId: this.clientId,
789
- clientSecret: this.clientSecret
670
+ clientSecret: __privateGet(this, _clientSecret)
790
671
  });
791
672
  return {
792
673
  accessToken: token.access_token,
@@ -795,7 +676,10 @@ var AuthyonServerClient = class {
795
676
  });
796
677
  }
797
678
  async request(path, options = {}) {
798
- const headers = { ...options.headers };
679
+ const headers = {
680
+ ...clientIpHeaders({ clientIp: this.clientIp }),
681
+ ...options.headers
682
+ };
799
683
  if (options.envBearer) {
800
684
  headers.Authorization = `Bearer ${await this.getEnvironmentAccessToken()}`;
801
685
  }
@@ -855,6 +739,14 @@ var AuthyonServerClient = class {
855
739
  tenant(credentials) {
856
740
  return new TenantScopedClient(this, credentials);
857
741
  }
742
+ /** Uses only the supplied platform-user access token. */
743
+ platform(token) {
744
+ return new PlatformScopedClient((path, options) => this.http.request(path, options), token);
745
+ }
746
+ /** Uses an end-user token; the configured environment key selects the environment. */
747
+ user(token) {
748
+ return new UserScopedClient((path, options) => this.request(path, options), token);
749
+ }
858
750
  /** @internal used by {@link TenantScopedClient} to share the base URL/fetch/error handling. */
859
751
  _requestAsTenant(accessToken, path, options = {}) {
860
752
  return this.request(path, {
@@ -864,6 +756,7 @@ var AuthyonServerClient = class {
864
756
  });
865
757
  }
866
758
  };
759
+ _clientSecret = new WeakMap();
867
760
  var TenantScopedClient = class {
868
761
  constructor(server, credentials) {
869
762
  this.members = {
@@ -936,332 +829,56 @@ var TenantScopedClient = class {
936
829
  async request(path, options = {}) {
937
830
  return this.server._requestAsTenant(await this.getAccessToken(), path, options);
938
831
  }
832
+ /** POST /tenant/auth/validate — validates this tenant client's current bearer token. */
833
+ async validate(context) {
834
+ return this.server.tenantAuth.validate(await this.getAccessToken(), context);
835
+ }
939
836
  };
940
837
  function createClient(options = {}) {
941
838
  return new AuthyonServerClient(options);
942
839
  }
943
840
 
944
841
  // src/client/authyonServerClientBuilder.ts
842
+ var _options;
945
843
  var AuthyonServerClientBuilder = class {
946
844
  constructor() {
947
- this.options = {};
845
+ __privateAdd(this, _options, {});
948
846
  }
949
847
  withEnvironmentKey(envKey) {
950
- this.options.envKey = envKey;
848
+ __privateGet(this, _options).envKey = envKey;
951
849
  return this;
952
850
  }
953
851
  withEnvironmentCredentials(clientId, clientSecret) {
954
- this.options.clientId = clientId;
955
- this.options.clientSecret = clientSecret;
852
+ __privateGet(this, _options).clientId = clientId;
853
+ __privateGet(this, _options).clientSecret = clientSecret;
854
+ return this;
855
+ }
856
+ withClientIp(clientIp) {
857
+ __privateGet(this, _options).clientIp = clientIp;
956
858
  return this;
957
859
  }
958
860
  withBaseUrl(baseUrl, allowInsecureHttp = false) {
959
- this.options.baseUrl = baseUrl;
960
- this.options.allowInsecureHttp = allowInsecureHttp;
861
+ __privateGet(this, _options).baseUrl = baseUrl;
862
+ __privateGet(this, _options).allowInsecureHttp = allowInsecureHttp;
961
863
  return this;
962
864
  }
963
865
  withTimeout(timeoutMs) {
964
- this.options.timeoutMs = timeoutMs;
866
+ __privateGet(this, _options).timeoutMs = timeoutMs;
965
867
  return this;
966
868
  }
967
869
  withHttpAdapter(httpAdapter) {
968
- this.options.httpAdapter = httpAdapter;
870
+ __privateGet(this, _options).httpAdapter = httpAdapter;
969
871
  return this;
970
872
  }
971
873
  withHttpLogger(httpLogger) {
972
- this.options.httpLogger = httpLogger;
874
+ __privateGet(this, _options).httpLogger = httpLogger;
973
875
  return this;
974
876
  }
975
877
  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);
878
+ return new AuthyonServerClient({ ...__privateGet(this, _options) });
1037
879
  }
1038
880
  };
1039
- function createAuthyonAbility(source = {}, options = {}) {
1040
- return new AuthyonAbility(createAuthyonRules(source, options), options.detectSubjectType);
1041
- }
1042
- function createAuthyonRules(source = {}, options = {}) {
1043
- const permissions = /* @__PURE__ */ new Set([
1044
- ...source.permissions ?? [],
1045
- ...source.scope?.split(/\s+/).filter(Boolean) ?? []
1046
- ]);
1047
- const rules = [...permissions].map(permissionToRule).filter(isAbilityRule);
1048
- for (const role of /* @__PURE__ */ new Set([...source.roles ?? [], ...options.roles ?? []])) {
1049
- rules.push(...(options.roleRules?.[role] ?? []).map(cloneRule));
1050
- }
1051
- rules.push(...(options.rules ?? []).map(cloneRule));
1052
- return rules;
1053
- }
1054
- function permissionToRule(permission) {
1055
- const normalized = permission.trim();
1056
- if (!normalized) return null;
1057
- if (normalized === "*" || normalized === "*:*" || normalized === "all:manage") {
1058
- return { action: "manage", subject: "all" };
1059
- }
1060
- const separator = normalized.lastIndexOf(":");
1061
- if (separator <= 0 || separator === normalized.length - 1) return null;
1062
- const subject = normalized.slice(0, separator);
1063
- const action = normalized.slice(separator + 1);
1064
- return {
1065
- action: action === "*" ? "manage" : action,
1066
- subject: subject === "*" ? "all" : subject
1067
- };
1068
- }
1069
- function matchesToken(value, expected, wildcard) {
1070
- return (Array.isArray(value) ? value : [value]).some(
1071
- (candidate) => candidate === expected || candidate === wildcard || candidate === "*"
1072
- );
1073
- }
1074
- function matchesField(pattern, field) {
1075
- if (pattern === "*" || pattern === field) return true;
1076
- return pattern.endsWith(".*") && field.startsWith(pattern.slice(0, -1));
1077
- }
1078
- function matchesConditions(subject, conditions) {
1079
- return Object.entries(conditions).every(([path, expected]) => {
1080
- if (path === "$and" && Array.isArray(expected)) {
1081
- return expected.every(
1082
- (condition) => matchesConditions(subject, condition)
1083
- );
1084
- }
1085
- if (path === "$or" && Array.isArray(expected)) {
1086
- return expected.some(
1087
- (condition) => matchesConditions(subject, condition)
1088
- );
1089
- }
1090
- return matchesValue(readPath(subject, path), expected);
1091
- });
1092
- }
1093
- function matchesValue(actual, expected) {
1094
- if (!isRecord2(expected) || !Object.keys(expected).some((key) => key.startsWith("$"))) {
1095
- return isRecord2(expected) && isRecord2(actual) ? matchesConditions(actual, expected) : Object.is(actual, expected);
1096
- }
1097
- return Object.entries(expected).every(([operator, operand]) => {
1098
- switch (operator) {
1099
- case "$eq":
1100
- return Object.is(actual, operand);
1101
- case "$ne":
1102
- return !Object.is(actual, operand);
1103
- case "$in":
1104
- return Array.isArray(operand) && operand.some((value) => Object.is(actual, value));
1105
- case "$nin":
1106
- return Array.isArray(operand) && !operand.some((value) => Object.is(actual, value));
1107
- case "$gt":
1108
- return typeof actual === "number" && typeof operand === "number" && actual > operand;
1109
- case "$gte":
1110
- return typeof actual === "number" && typeof operand === "number" && actual >= operand;
1111
- case "$lt":
1112
- return typeof actual === "number" && typeof operand === "number" && actual < operand;
1113
- case "$lte":
1114
- return typeof actual === "number" && typeof operand === "number" && actual <= operand;
1115
- case "$exists":
1116
- return operand ? actual !== void 0 : actual === void 0;
1117
- default:
1118
- return false;
1119
- }
1120
- });
1121
- }
1122
- function readPath(value, path) {
1123
- return path.split(".").reduce((current, part) => isRecord2(current) ? current[part] : void 0, value);
1124
- }
1125
- function defaultSubjectType(subject) {
1126
- const explicit = subject.__type ?? subject.type ?? subject.kind;
1127
- if (typeof explicit === "string") return explicit;
1128
- const constructorName = subject.constructor?.name;
1129
- return typeof constructorName === "string" ? constructorName : "Object";
1130
- }
1131
- function cloneRule(rule) {
1132
- return {
1133
- ...rule,
1134
- action: Array.isArray(rule.action) ? [...rule.action] : rule.action,
1135
- subject: Array.isArray(rule.subject) ? [...rule.subject] : rule.subject,
1136
- fields: rule.fields ? [...rule.fields] : void 0
1137
- };
1138
- }
1139
- function isRecord2(value) {
1140
- return typeof value === "object" && value !== null && !Array.isArray(value);
1141
- }
1142
- function isAbilityRule(value) {
1143
- return value !== null;
1144
- }
1145
-
1146
- // src/integrations/authorization.ts
1147
- var AuthorizationError = class extends Error {
1148
- constructor(status, code, message) {
1149
- super(message);
1150
- this.status = status;
1151
- this.code = code;
1152
- this.name = "AuthorizationError";
1153
- }
1154
- };
1155
- async function authorizeToken(client, token, options = {}) {
1156
- if (!token) throw unauthenticated();
1157
- let userId;
1158
- let permissions = [];
1159
- let roles = [];
1160
- let scope;
1161
- if (options.verification === "jwks") {
1162
- if (!options.jwksVerifier) {
1163
- throw new Error("Authyon: `jwksVerifier` is required for JWKS verification");
1164
- }
1165
- let verified;
1166
- try {
1167
- verified = await options.jwksVerifier.verifyAccessToken(token);
1168
- } catch (error) {
1169
- if (!(error instanceof TokenVerificationError)) throw error;
1170
- if (error.code === "token.key_unavailable") {
1171
- throw new AuthorizationError(
1172
- 503,
1173
- "authorization.unavailable",
1174
- "Token verification is temporarily unavailable"
1175
- );
1176
- }
1177
- throw unauthenticated();
1178
- }
1179
- const { claims } = verified;
1180
- userId = claims.sub;
1181
- permissions = claims.permissions ?? [];
1182
- roles = claims.roles ?? [];
1183
- scope = claims.scope;
1184
- } else if (options.verification === "introspect") {
1185
- const identity = await client.introspect(token, { clientIp: options.clientIp });
1186
- if (!identity.active) throw unauthenticated();
1187
- userId = identity.sub;
1188
- permissions = identity.permissions ?? [];
1189
- roles = identity.roles ?? [];
1190
- scope = identity.scope;
1191
- } else {
1192
- const identity = await client.validate(token, { clientIp: options.clientIp });
1193
- if (!identity.valid || !identity.user) throw unauthenticated();
1194
- userId = identity.user.id;
1195
- permissions = identity.user.permissions ?? [];
1196
- }
1197
- const ability = createAuthyonAbility(
1198
- { permissions, roles, scope },
1199
- {
1200
- rules: options.rules,
1201
- roleRules: options.roleRules,
1202
- roles: options.roles,
1203
- detectSubjectType: options.detectSubjectType
1204
- }
1205
- );
1206
- if (options.requirement && !canAccess(ability, options.requirement)) {
1207
- throw new AuthorizationError(403, "authorization.forbidden", "Permission denied");
1208
- }
1209
- return { token, userId, ability, permissions, roles };
1210
- }
1211
- function authorizeRequest(client, request, options = {}) {
1212
- const clientIp = options.resolveClientIp?.(request) ?? options.clientIp;
1213
- return authorizeToken(client, bearerToken(request.headers.get("authorization")), {
1214
- ...options,
1215
- clientIp
1216
- });
1217
- }
1218
- function createExpressAuthorizationMiddleware(client, options = {}) {
1219
- return async (request, response, next) => {
1220
- try {
1221
- const header = request.headers.authorization;
1222
- const clientIp = resolveExpressClientIp(request, options);
1223
- const context = await authorizeToken(
1224
- client,
1225
- bearerToken(Array.isArray(header) ? header[0] : header),
1226
- { ...options, clientIp }
1227
- );
1228
- request[options.requestProperty ?? "authyon"] = context;
1229
- next();
1230
- } catch (error) {
1231
- if (options.passErrorsToNext || !(error instanceof AuthorizationError)) {
1232
- next(error);
1233
- return;
1234
- }
1235
- response.status(error.status).json({ code: error.code, message: error.message });
1236
- }
1237
- };
1238
- }
1239
- function resolveExpressClientIp(request, options) {
1240
- const resolved = options.resolveClientIp?.(request) ?? (options.trustProxy ? request.ip : request.socket?.remoteAddress);
1241
- return resolved ? normalizeClientIp(stripIpv4Prefix(resolved)) : void 0;
1242
- }
1243
- function stripIpv4Prefix(value) {
1244
- return value.startsWith("::ffff:") ? value.slice(7) : value;
1245
- }
1246
- function createAuthorizationErrorResponse(error) {
1247
- if (!(error instanceof AuthorizationError)) throw error;
1248
- return Response.json({ code: error.code, message: error.message }, { status: error.status });
1249
- }
1250
- function canAccess(ability, requirement) {
1251
- return ability.can(requirement.action, requirement.subject, requirement.field);
1252
- }
1253
- function bearerToken(header) {
1254
- const match = header?.match(/^Bearer\s+(.+)$/i);
1255
- if (!match?.[1]) throw unauthenticated();
1256
- return match[1].trim();
1257
- }
1258
- function unauthenticated() {
1259
- return new AuthorizationError(
1260
- 401,
1261
- "authorization.unauthenticated",
1262
- "A valid bearer token is required"
1263
- );
1264
- }
881
+ _options = new WeakMap();
1265
882
  export {
1266
883
  AuthorizationError,
1267
884
  AuthyonAbility,
@@ -1273,8 +890,11 @@ export {
1273
890
  FetchHttpAdapter,
1274
891
  JwksTokenVerifier,
1275
892
  LoggingHttpAdapter,
893
+ PlatformScopedClient,
1276
894
  TenantScopedClient,
1277
895
  TokenVerificationError,
896
+ UserScopedClient,
897
+ WorkspaceInvitesClient,
1278
898
  authorizeRequest,
1279
899
  authorizeToken,
1280
900
  createAuthorizationErrorResponse,
@@ -1282,5 +902,7 @@ export {
1282
902
  createAuthyonRules,
1283
903
  createClient,
1284
904
  createExpressAuthorizationMiddleware,
905
+ hasPermission,
906
+ hasPermissionGroup,
1285
907
  normalizeClientIp
1286
908
  };