@authyon/auth 0.2.0-beta.0 → 0.2.0-beta.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,26 +1,129 @@
1
- // src/errors.ts
1
+ import {
2
+ AuthyonAbility,
3
+ AuthyonAbilityBuilder,
4
+ AuthyonSessionController,
5
+ createAuthyonAbility,
6
+ createAuthyonRules,
7
+ hasPermission,
8
+ hasPermissionGroup
9
+ } from "./chunk-3XJY6BCI.js";
10
+
11
+ // ../../internal/core/errors/authyonError.ts
12
+ var ErrorCodes = {
13
+ Unknown: "unknown",
14
+ NetworkError: "request.network_error",
15
+ Timeout: "request.timeout",
16
+ SessionMalformed: "session.malformed",
17
+ NotAuthenticated: "auth.not_authenticated",
18
+ InvalidToken: "auth.invalid_token",
19
+ MissingToken: "auth.missing_token",
20
+ EmailTaken: "user.email_taken",
21
+ PasswordWeak: "user.password_weak",
22
+ PasswordPwned: "user.password_pwned",
23
+ RateLimited: "rate_limited"
24
+ };
2
25
  var AuthyonError = class extends Error {
3
- constructor(status, body) {
26
+ constructor(status, body, options = {}) {
4
27
  super(body.detail ?? body.title ?? `Authyon request failed with status ${status}`);
5
28
  this.name = "AuthyonError";
6
29
  this.status = status;
7
30
  this.code = body.code ?? "unknown";
8
31
  this.title = body.title ?? "Error";
9
32
  this.detail = body.detail;
33
+ this.requestId = options.requestId;
34
+ this.retryAfter = options.retryAfter;
35
+ this.cause = options.cause;
10
36
  }
11
37
  is(code) {
12
38
  return this.code === code;
13
39
  }
40
+ isAny(...codes) {
41
+ return codes.includes(this.code);
42
+ }
43
+ hasPrefix(prefix) {
44
+ return this.code.startsWith(prefix);
45
+ }
46
+ isStatus(...statuses) {
47
+ return statuses.includes(this.status);
48
+ }
49
+ /** Stable interpretation for UI decisions, retries, telemetry and support flows. */
50
+ interpret() {
51
+ const category = classifyError(this.status, this.code);
52
+ return {
53
+ category,
54
+ action: actionFor(category),
55
+ retryable: isRetryable(category),
56
+ ...this.retryAfter !== void 0 ? { retryAfter: this.retryAfter } : {},
57
+ ...this.requestId !== void 0 ? { requestId: this.requestId } : {}
58
+ };
59
+ }
60
+ get category() {
61
+ return this.interpret().category;
62
+ }
63
+ get retryable() {
64
+ return this.interpret().retryable;
65
+ }
66
+ toJSON() {
67
+ return {
68
+ name: this.name,
69
+ message: this.message,
70
+ status: this.status,
71
+ code: this.code,
72
+ title: this.title,
73
+ detail: this.detail,
74
+ requestId: this.requestId,
75
+ retryAfter: this.retryAfter,
76
+ ...this.interpret()
77
+ };
78
+ }
14
79
  };
15
- var ErrorCodes = {
16
- EmailTaken: "user.email_taken",
17
- PasswordWeak: "user.password_weak",
18
- PasswordPwned: "user.password_pwned"
19
- };
80
+ function classifyError(status, code) {
81
+ if (code === ErrorCodes.NetworkError) return "network";
82
+ if (code === ErrorCodes.Timeout || status === 408) return "timeout";
83
+ if (code === ErrorCodes.RateLimited || status === 429) return "rate_limit";
84
+ if (code === ErrorCodes.EmailTaken || status === 409) return "conflict";
85
+ if (code === ErrorCodes.PasswordWeak || code === ErrorCodes.PasswordPwned) return "validation";
86
+ if (code === ErrorCodes.SessionMalformed) return "server";
87
+ if (code.startsWith("auth.") || status === 401) return "authentication";
88
+ if (status === 403) return "authorization";
89
+ if (status === 404) return "not_found";
90
+ if (status === 400 || status === 422) return "validation";
91
+ if (status >= 500) return "server";
92
+ return "unknown";
93
+ }
94
+ function actionFor(category) {
95
+ switch (category) {
96
+ case "network":
97
+ case "timeout":
98
+ case "rate_limit":
99
+ case "server":
100
+ return "retry";
101
+ case "authentication":
102
+ return "reauthenticate";
103
+ case "authorization":
104
+ return "request_access";
105
+ case "validation":
106
+ return "fix_input";
107
+ case "not_found":
108
+ return "not_found";
109
+ case "conflict":
110
+ return "resolve_conflict";
111
+ case "unknown":
112
+ return "contact_support";
113
+ }
114
+ }
115
+ function isRetryable(category) {
116
+ return category === "network" || category === "timeout" || category === "rate_limit" || category === "server";
117
+ }
20
118
 
21
- // src/storage.ts
119
+ // src/session/storage.ts
22
120
  var STORAGE_KEY = "authyon.session";
23
- function memoryStorage() {
121
+ function isSession(value) {
122
+ if (!value || typeof value !== "object") return false;
123
+ const candidate = value;
124
+ return typeof candidate.accessToken === "string" && candidate.accessToken.length > 0 && typeof candidate.refreshToken === "string" && candidate.refreshToken.length > 0 && typeof candidate.expiresIn === "number" && Number.isFinite(candidate.expiresIn) && candidate.expiresIn > 0 && typeof candidate.expiresAt === "number" && Number.isFinite(candidate.expiresAt);
125
+ }
126
+ function createMemoryStorage() {
24
127
  let session = null;
25
128
  return {
26
129
  get: () => session,
@@ -32,12 +135,16 @@ function memoryStorage() {
32
135
  }
33
136
  };
34
137
  }
35
- function localStorageAdapter(key = STORAGE_KEY) {
138
+ function createLocalStorage(key = STORAGE_KEY) {
36
139
  return {
37
140
  get() {
38
141
  try {
39
142
  const raw = window.localStorage.getItem(key);
40
- return raw ? JSON.parse(raw) : null;
143
+ if (!raw) return null;
144
+ const parsed = JSON.parse(raw);
145
+ if (isSession(parsed)) return parsed;
146
+ window.localStorage.removeItem(key);
147
+ return null;
41
148
  } catch {
42
149
  return null;
43
150
  }
@@ -56,22 +163,244 @@ function localStorageAdapter(key = STORAGE_KEY) {
56
163
  }
57
164
  };
58
165
  }
59
- function defaultStorage() {
60
- if (typeof window !== "undefined" && typeof window.localStorage !== "undefined") {
61
- return localStorageAdapter();
62
- }
63
- return memoryStorage();
166
+ function createDefaultStorage() {
167
+ return createMemoryStorage();
64
168
  }
65
169
 
66
- // src/client.ts
170
+ // ../../internal/core/config/defaults.ts
67
171
  var DEFAULT_BASE_URL = "https://api.authyon.com";
68
- var EXPIRY_SKEW_MS = 3e4;
172
+ var DEFAULT_EXPIRY_SKEW_MS = 3e4;
173
+
174
+ // ../../internal/core/http/query.ts
175
+ function appendQuery(path, params) {
176
+ if (!params) return path;
177
+ const query = new URLSearchParams();
178
+ for (const [key, value2] of Object.entries(params)) {
179
+ if (value2 !== void 0) query.set(key, String(value2));
180
+ }
181
+ const value = query.toString();
182
+ return value ? `${path}${path.includes("?") ? "&" : "?"}${value}` : path;
183
+ }
184
+
185
+ // ../../internal/core/http/httpAdapter.ts
186
+ var FetchHttpAdapter = class {
187
+ constructor(fetchImpl = fetch.bind(globalThis)) {
188
+ this.fetchImpl = fetchImpl;
189
+ }
190
+ request(request) {
191
+ return this.fetchImpl(request.url, {
192
+ method: request.method,
193
+ headers: request.headers,
194
+ body: request.body,
195
+ signal: request.signal,
196
+ redirect: "error"
197
+ });
198
+ }
199
+ };
200
+ var LoggingHttpAdapter = class {
201
+ constructor(adapter, options) {
202
+ this.adapter = adapter;
203
+ this.options = options;
204
+ }
205
+ async request(request) {
206
+ const startedAt = Date.now();
207
+ const eventBase = {
208
+ method: request.method,
209
+ url: sanitizeUrl(request.url)
210
+ };
211
+ this.log({ type: "request", ...eventBase, timestamp: startedAt });
212
+ try {
213
+ const response = await this.adapter.request(request);
214
+ this.log({
215
+ type: "response",
216
+ ...eventBase,
217
+ status: response.status,
218
+ durationMs: Date.now() - startedAt,
219
+ requestId: response.headers.get("x-request-id") ?? response.headers.get("trace-id") ?? void 0,
220
+ timestamp: Date.now()
221
+ });
222
+ return response;
223
+ } catch (error) {
224
+ this.log({
225
+ type: "error",
226
+ ...eventBase,
227
+ errorName: error instanceof Error ? error.name : "UnknownError",
228
+ durationMs: Date.now() - startedAt,
229
+ timestamp: Date.now()
230
+ });
231
+ throw error;
232
+ }
233
+ }
234
+ log(event) {
235
+ if (!this.options.enabled) return;
236
+ const logger = this.options.logger ?? defaultHttpLogger;
237
+ try {
238
+ logger(event);
239
+ } catch {
240
+ }
241
+ }
242
+ };
243
+ function defaultHttpLogger(event) {
244
+ console.debug("[Authyon HTTP]", event);
245
+ }
246
+ function sanitizeUrl(value) {
247
+ const url = new URL(value);
248
+ url.username = "";
249
+ url.password = "";
250
+ url.hash = "";
251
+ url.pathname = url.pathname.replace(/(\/platform\/workspace-invites\/)[^/]+/u, "$1REDACTED");
252
+ const queryKeys = /* @__PURE__ */ new Set();
253
+ url.searchParams.forEach((_value, key) => queryKeys.add(key));
254
+ url.search = queryKeys.size > 0 ? [...queryKeys].map((key) => `${encodeURIComponent(key)}=REDACTED`).join("&") : "";
255
+ return url.toString();
256
+ }
257
+
258
+ // ../../internal/core/http/transport.ts
259
+ var DEFAULT_TIMEOUT_MS = 15e3;
260
+ var SharedTransportError = class extends Error {
261
+ constructor(code, cause) {
262
+ super(code === "request.timeout" ? "Request timed out" : "Network request failed");
263
+ this.name = "SharedTransportError";
264
+ this.code = code;
265
+ this.cause = cause;
266
+ }
267
+ };
268
+ function createSharedTransport(options) {
269
+ const baseUrl = normalizeBaseUrl(options.baseUrl, options.allowInsecureHttp ?? false);
270
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
271
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
272
+ throw new Error("Authyon: `timeoutMs` must be a non-negative finite number");
273
+ }
274
+ if (options.httpAdapter && options.fetch) {
275
+ throw new Error("Authyon: use either `httpAdapter` or `fetch`, not both");
276
+ }
277
+ const baseAdapter = options.httpAdapter ?? new FetchHttpAdapter(options.fetch);
278
+ const httpAdapter = options.httpLogger ? new LoggingHttpAdapter(baseAdapter, options.httpLogger) : baseAdapter;
279
+ return {
280
+ baseUrl,
281
+ async request(path, init) {
282
+ const controller = new AbortController();
283
+ const timeout = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : void 0;
284
+ try {
285
+ return await httpAdapter.request({
286
+ url: `${baseUrl}${path}`,
287
+ method: init.method ?? "GET",
288
+ headers: normalizeHeaders(init.headers),
289
+ body: init.body,
290
+ signal: controller.signal
291
+ });
292
+ } catch (cause) {
293
+ throw new SharedTransportError(
294
+ controller.signal.aborted ? ErrorCodes.Timeout : ErrorCodes.NetworkError,
295
+ cause
296
+ );
297
+ } finally {
298
+ if (timeout !== void 0) clearTimeout(timeout);
299
+ }
300
+ }
301
+ };
302
+ }
303
+ function normalizeHeaders(headers) {
304
+ const normalized = {};
305
+ new Headers(headers).forEach((value, key) => {
306
+ normalized[key] = value;
307
+ });
308
+ return normalized;
309
+ }
310
+ function responseMetadata(response) {
311
+ const retryAfterHeader = response.headers.get("retry-after");
312
+ const retryAfter = retryAfterHeader ? Number(retryAfterHeader) : void 0;
313
+ return {
314
+ requestId: response.headers.get("x-request-id") ?? response.headers.get("trace-id") ?? void 0,
315
+ retryAfter: Number.isFinite(retryAfter) ? retryAfter : void 0
316
+ };
317
+ }
318
+ function normalizeBaseUrl(value, allowInsecureHttp) {
319
+ let url;
320
+ try {
321
+ url = new URL(value);
322
+ } catch {
323
+ throw new Error("Authyon: `baseUrl` must be an absolute URL");
324
+ }
325
+ const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
326
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && (loopback || allowInsecureHttp))) {
327
+ throw new Error(
328
+ "Authyon: `baseUrl` must use HTTPS (HTTP is allowed only for loopback or with `allowInsecureHttp`)"
329
+ );
330
+ }
331
+ if (url.username || url.password || url.search || url.hash) {
332
+ throw new Error(
333
+ "Authyon: `baseUrl` cannot contain credentials, query parameters, or fragments"
334
+ );
335
+ }
336
+ return url.toString().replace(/\/+$/, "");
337
+ }
338
+
339
+ // ../../internal/core/http/jsonHttpClient.ts
340
+ var JsonHttpClient = class {
341
+ constructor(transport) {
342
+ this.transport = transport;
343
+ }
344
+ async send(path, options = {}) {
345
+ const headers = { ...options.headers };
346
+ if (options.body !== void 0) headers["Content-Type"] = "application/json";
347
+ try {
348
+ return await this.transport.request(appendQuery(path, options.query), {
349
+ method: options.method ?? "GET",
350
+ headers,
351
+ body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
352
+ });
353
+ } catch (cause) {
354
+ if (!(cause instanceof SharedTransportError)) throw cause;
355
+ throw new AuthyonError(0, { code: cause.code, title: cause.message }, { cause: cause.cause });
356
+ }
357
+ }
358
+ async parse(response) {
359
+ if (!response.ok) {
360
+ let body = {};
361
+ try {
362
+ body = await response.json();
363
+ } catch {
364
+ }
365
+ throw new AuthyonError(response.status, body, responseMetadata(response));
366
+ }
367
+ if (response.status === 204) return void 0;
368
+ return await response.json();
369
+ }
370
+ async request(path, options = {}) {
371
+ return this.parse(await this.send(path, options));
372
+ }
373
+ };
374
+
375
+ // ../../internal/core/http/clientIp.ts
376
+ function clientIpHeaders(context) {
377
+ if (!context?.clientIp) return {};
378
+ return { "X-Forwarded-For": normalizeClientIp(context.clientIp) };
379
+ }
380
+ function normalizeClientIp(value) {
381
+ let ip = value.trim();
382
+ if (ip.startsWith("[") && ip.endsWith("]")) ip = ip.slice(1, -1);
383
+ if (!ip || ip.length > 45 || ip.includes(",") || /[\r\n\s]/.test(ip) || !/^[0-9a-f:.]+$/i.test(ip) || !ip.includes(".") && !ip.includes(":")) {
384
+ throw invalidClientIp();
385
+ }
386
+ try {
387
+ new URL(ip.includes(":") ? `http://[${ip}]` : `http://${ip}`);
388
+ } catch {
389
+ throw invalidClientIp();
390
+ }
391
+ return ip;
392
+ }
393
+ function invalidClientIp() {
394
+ return new Error("Authyon: `clientIp` must be a single valid IPv4 or IPv6 address");
395
+ }
396
+
397
+ // src/client/authyonClient.ts
69
398
  var FALLBACK_EXPIRES_IN = 1800;
70
399
  function readTokens(raw) {
71
400
  const tokens = raw.tokens ?? raw;
72
401
  if (!tokens.accessToken || !tokens.refreshToken) {
73
402
  throw new AuthyonError(502, {
74
- code: "session.malformed",
403
+ code: ErrorCodes.SessionMalformed,
75
404
  title: "Malformed session response",
76
405
  detail: "The session response carried no access/refresh token pair."
77
406
  });
@@ -127,7 +456,7 @@ var AuthyonClient = class {
127
456
  /** GET /auth/sessions — active refresh-token sessions with device/IP data. */
128
457
  sessions: () => this.request("/auth/sessions", { bearer: true }),
129
458
  /** GET /auth/me/activities — paginated recent account activity for the current user. */
130
- activities: (params = {}) => this.request(`/auth/me/activities?${toQuery(params)}`, { bearer: true }),
459
+ activities: (params = {}) => this.request(appendQuery("/auth/me/activities", params), { bearer: true }),
131
460
  /**
132
461
  * Revokes a single session by id (e.g. one entry from `sessions()`),
133
462
  * signing that device out without affecting the current one.
@@ -152,8 +481,8 @@ var AuthyonClient = class {
152
481
  };
153
482
  // ── Organization ─────────────────────────────────────────────────────────
154
483
  this.organization = {
155
- /** GET /auth/tenants — all organization memberships. */
156
- list: () => this.request("/auth/tenants", { bearer: true }),
484
+ /** GET /auth/tenants — filterable list of organization memberships. */
485
+ list: (params = {}) => this.request(appendQuery("/auth/tenants", params), { bearer: true }),
157
486
  /**
158
487
  * POST /auth/tenants — creates an organization owned by the signed-in
159
488
  * user (only available when self-service organization creation is
@@ -183,11 +512,11 @@ var AuthyonClient = class {
183
512
  /**
184
513
  * GET /auth/tenants/{organizationId}/members — paginated list of an
185
514
  * organization's members. Consistent with the confirmed-live
186
- * `Page<T>` envelope every other `skip`/`take` endpoint returns
515
+ * `Paged<T>` envelope every other `skip`/`take` endpoint returns
187
516
  * (`user.activities()`, `@authyon/server`'s `environment.users.list()`).
188
517
  */
189
518
  list: (organizationId, params = {}) => this.request(
190
- `/auth/tenants/${encodeURIComponent(organizationId)}/members?${toQuery(params)}`,
519
+ appendQuery(`/auth/tenants/${encodeURIComponent(organizationId)}/members`, params),
191
520
  { bearer: true }
192
521
  ),
193
522
  /** POST /auth/tenants/{organizationId}/members — invite a member by e-mail. */
@@ -280,10 +609,19 @@ var AuthyonClient = class {
280
609
  if (!options.envKey)
281
610
  throw new Error("Authyon: `envKey` is required (pk_live_... / pk_test_...)");
282
611
  this.envKey = options.envKey;
283
- this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
284
- this.storage = options.storage ?? defaultStorage();
612
+ this.clientIp = options.clientIp;
613
+ this.transport = createSharedTransport({
614
+ baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
615
+ allowInsecureHttp: options.allowInsecureHttp,
616
+ timeoutMs: options.timeoutMs,
617
+ httpAdapter: options.httpAdapter,
618
+ httpLogger: options.httpLogger,
619
+ fetch: options.fetch
620
+ });
621
+ this.baseUrl = this.transport.baseUrl;
622
+ this.http = new JsonHttpClient(this.transport);
623
+ this.storage = options.storage ?? createDefaultStorage();
285
624
  this.autoRefresh = options.autoRefresh ?? true;
286
- this.fetchImpl = options.fetch ?? fetch.bind(globalThis);
287
625
  }
288
626
  // ── Session state ────────────────────────────────────────────────────────
289
627
  /** Current persisted session, or null when signed out. */
@@ -291,7 +629,13 @@ var AuthyonClient = class {
291
629
  return this.storage.get();
292
630
  }
293
631
  isAuthenticated() {
294
- return this.getSession() !== null;
632
+ return this.getAuthState() === "authenticated";
633
+ }
634
+ /** Synchronous snapshot of the locally available authentication state. */
635
+ getAuthState() {
636
+ const session = this.getSession();
637
+ if (!session) return "signed_out";
638
+ return Date.now() < session.expiresAt ? "authenticated" : "expired";
295
639
  }
296
640
  /**
297
641
  * Returns a valid access token, refreshing it transparently when it is
@@ -300,15 +644,41 @@ var AuthyonClient = class {
300
644
  async getAccessToken() {
301
645
  const session = this.getSession();
302
646
  if (!session) return null;
303
- if (this.autoRefresh && Date.now() >= session.expiresAt - EXPIRY_SKEW_MS) {
647
+ if (this.autoRefresh && Date.now() >= session.expiresAt - DEFAULT_EXPIRY_SKEW_MS) {
304
648
  try {
305
649
  return (await this.refresh()).accessToken;
306
- } catch {
307
- return null;
650
+ } catch (error) {
651
+ if (error instanceof AuthyonError && (error.status === 401 || error.status === 403)) {
652
+ return null;
653
+ }
654
+ throw error;
308
655
  }
309
656
  }
310
657
  return session.accessToken;
311
658
  }
659
+ /**
660
+ * Refreshes when needed, validates the server-side session through `GET /auth/me`,
661
+ * and stores the fresh user profile. Returns null when the session is no longer valid.
662
+ */
663
+ async validateSession() {
664
+ const accessToken = await this.getAccessToken();
665
+ if (!accessToken) return null;
666
+ try {
667
+ const user = await this.user.me();
668
+ const current = this.getSession();
669
+ if (!current) return null;
670
+ const session = { ...current, user };
671
+ this.storage.set(session);
672
+ this.emit({ type: "session_validated", session });
673
+ return session;
674
+ } catch (error) {
675
+ if (error instanceof AuthyonError && (error.status === 401 || error.status === 403)) {
676
+ this.clearSession();
677
+ return null;
678
+ }
679
+ throw error;
680
+ }
681
+ }
312
682
  /** Subscribe to sign-in / refresh / sign-out events. Returns an unsubscribe fn. */
313
683
  onAuthStateChange(listener) {
314
684
  this.listeners.add(listener);
@@ -352,40 +722,29 @@ var AuthyonClient = class {
352
722
  async request(path, options = {}, isRetry = false) {
353
723
  const headers = {
354
724
  "X-Authyon-Environment": this.envKey,
725
+ ...clientIpHeaders({ clientIp: this.clientIp }),
355
726
  ...options.headers
356
727
  };
357
- if (options.body !== void 0) headers["Content-Type"] = "application/json";
358
728
  if (options.bearer) {
359
729
  const token = await this.getAccessToken();
360
730
  if (!token)
361
- throw new AuthyonError(401, { code: "auth.not_authenticated", title: "Not authenticated" });
731
+ throw new AuthyonError(401, {
732
+ code: ErrorCodes.NotAuthenticated,
733
+ title: "Not authenticated"
734
+ });
362
735
  headers.Authorization = `Bearer ${token}`;
363
736
  }
364
- const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
365
- method: options.method ?? "GET",
366
- headers,
367
- body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
368
- });
737
+ const response = await this.http.send(path, { ...options, headers });
369
738
  if (response.status === 401 && options.bearer && this.autoRefresh && !isRetry && this.getSession()) {
370
739
  try {
371
740
  await this.refresh();
372
741
  } catch {
373
742
  this.clearSession();
374
- throw await this.toError(response);
743
+ return this.http.parse(response);
375
744
  }
376
745
  return this.request(path, options, true);
377
746
  }
378
- if (!response.ok) throw await this.toError(response);
379
- if (response.status === 204) return void 0;
380
- return await response.json();
381
- }
382
- async toError(response) {
383
- let body = {};
384
- try {
385
- body = await response.json();
386
- } catch {
387
- }
388
- return new AuthyonError(response.status, body);
747
+ return this.http.parse(response);
389
748
  }
390
749
  // ── Auth flows ───────────────────────────────────────────────────────────
391
750
  /** POST /auth/register — creates a new user (rate-limited to 20/hour per IP). */
@@ -421,7 +780,10 @@ var AuthyonClient = class {
421
780
  if (this.refreshInFlight) return this.refreshInFlight;
422
781
  const current = this.getSession();
423
782
  if (!current)
424
- throw new AuthyonError(401, { code: "auth.not_authenticated", title: "Not authenticated" });
783
+ throw new AuthyonError(401, {
784
+ code: ErrorCodes.NotAuthenticated,
785
+ title: "Not authenticated"
786
+ });
425
787
  this.refreshInFlight = this.request("/auth/refresh", {
426
788
  method: "POST",
427
789
  body: { refreshToken: current.refreshToken }
@@ -471,6 +833,8 @@ var AuthyonClient = class {
471
833
  * A browser app has no client secret to present, so this will fail from
472
834
  * `@authyon/auth` in practice; call it from your backend via
473
835
  * `@authyon/server` instead.
836
+ *
837
+ * @deprecated Use `@authyon/server.introspect()` from a trusted backend.
474
838
  */
475
839
  async introspect(token) {
476
840
  const accessToken = token ?? await this.getAccessToken();
@@ -480,6 +844,8 @@ var AuthyonClient = class {
480
844
  * POST /auth/validate — recommended: cross-checks DB state, catches
481
845
  * revocation immediately. Same caller-authentication requirement (and
482
846
  * the same practical limitation from the browser) as `introspect()`.
847
+ *
848
+ * @deprecated Use `@authyon/server.validate()` from a trusted backend.
483
849
  */
484
850
  async validate(token) {
485
851
  const accessToken = token ?? await this.getAccessToken();
@@ -502,19 +868,61 @@ function normalizeUser(raw) {
502
868
  function createClient(options) {
503
869
  return new AuthyonClient(options);
504
870
  }
505
- function toQuery(params) {
506
- const query = new URLSearchParams();
507
- for (const [key, value] of Object.entries(params)) {
508
- if (value !== void 0) query.set(key, String(value));
871
+
872
+ // src/client/authyonClientBuilder.ts
873
+ var AuthyonClientBuilder = class {
874
+ constructor(envKey) {
875
+ this.options = { envKey };
509
876
  }
510
- return query.toString();
511
- }
877
+ withBaseUrl(baseUrl, allowInsecureHttp = false) {
878
+ this.options.baseUrl = baseUrl;
879
+ this.options.allowInsecureHttp = allowInsecureHttp;
880
+ return this;
881
+ }
882
+ withStorage(storage) {
883
+ this.options.storage = storage;
884
+ return this;
885
+ }
886
+ withAutomaticRefresh(enabled = true) {
887
+ this.options.autoRefresh = enabled;
888
+ return this;
889
+ }
890
+ withClientIp(clientIp) {
891
+ this.options.clientIp = clientIp;
892
+ return this;
893
+ }
894
+ withTimeout(timeoutMs) {
895
+ this.options.timeoutMs = timeoutMs;
896
+ return this;
897
+ }
898
+ withHttpAdapter(httpAdapter) {
899
+ this.options.httpAdapter = httpAdapter;
900
+ return this;
901
+ }
902
+ withHttpLogger(httpLogger) {
903
+ this.options.httpLogger = httpLogger;
904
+ return this;
905
+ }
906
+ build() {
907
+ return new AuthyonClient({ ...this.options });
908
+ }
909
+ };
512
910
  export {
911
+ AuthyonAbility,
912
+ AuthyonAbilityBuilder,
513
913
  AuthyonClient,
914
+ AuthyonClientBuilder,
514
915
  AuthyonError,
916
+ AuthyonSessionController,
515
917
  ErrorCodes,
918
+ FetchHttpAdapter,
919
+ LoggingHttpAdapter,
920
+ createAuthyonAbility,
921
+ createAuthyonRules,
516
922
  createClient,
517
- defaultStorage,
518
- localStorageAdapter,
519
- memoryStorage
923
+ createDefaultStorage,
924
+ createLocalStorage,
925
+ createMemoryStorage,
926
+ hasPermission,
927
+ hasPermissionGroup
520
928
  };