@authyon/auth 0.1.5 → 0.2.0-beta.1

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.cjs CHANGED
@@ -20,39 +20,140 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ AuthyonAbility: () => AuthyonAbility,
24
+ AuthyonAbilityBuilder: () => AuthyonAbilityBuilder,
23
25
  AuthyonClient: () => AuthyonClient,
26
+ AuthyonClientBuilder: () => AuthyonClientBuilder,
24
27
  AuthyonError: () => AuthyonError,
28
+ AuthyonSessionController: () => AuthyonSessionController,
25
29
  ErrorCodes: () => ErrorCodes,
30
+ FetchHttpAdapter: () => FetchHttpAdapter,
31
+ LoggingHttpAdapter: () => LoggingHttpAdapter,
32
+ createAuthyonAbility: () => createAuthyonAbility,
33
+ createAuthyonRules: () => createAuthyonRules,
26
34
  createClient: () => createClient,
27
- defaultStorage: () => defaultStorage,
28
- localStorageAdapter: () => localStorageAdapter,
29
- memoryStorage: () => memoryStorage
35
+ createDefaultStorage: () => createDefaultStorage,
36
+ createLocalStorage: () => createLocalStorage,
37
+ createMemoryStorage: () => createMemoryStorage
30
38
  });
31
39
  module.exports = __toCommonJS(index_exports);
32
40
 
33
- // src/errors.ts
41
+ // ../../internal/core/errors/authyonError.ts
42
+ var ErrorCodes = {
43
+ Unknown: "unknown",
44
+ NetworkError: "request.network_error",
45
+ Timeout: "request.timeout",
46
+ SessionMalformed: "session.malformed",
47
+ NotAuthenticated: "auth.not_authenticated",
48
+ InvalidToken: "auth.invalid_token",
49
+ MissingToken: "auth.missing_token",
50
+ EmailTaken: "user.email_taken",
51
+ PasswordWeak: "user.password_weak",
52
+ PasswordPwned: "user.password_pwned",
53
+ RateLimited: "rate_limited"
54
+ };
34
55
  var AuthyonError = class extends Error {
35
- constructor(status, body) {
56
+ constructor(status, body, options = {}) {
36
57
  super(body.detail ?? body.title ?? `Authyon request failed with status ${status}`);
37
58
  this.name = "AuthyonError";
38
59
  this.status = status;
39
60
  this.code = body.code ?? "unknown";
40
61
  this.title = body.title ?? "Error";
41
62
  this.detail = body.detail;
63
+ this.requestId = options.requestId;
64
+ this.retryAfter = options.retryAfter;
65
+ this.cause = options.cause;
42
66
  }
43
67
  is(code) {
44
68
  return this.code === code;
45
69
  }
70
+ isAny(...codes) {
71
+ return codes.includes(this.code);
72
+ }
73
+ hasPrefix(prefix) {
74
+ return this.code.startsWith(prefix);
75
+ }
76
+ isStatus(...statuses) {
77
+ return statuses.includes(this.status);
78
+ }
79
+ /** Stable interpretation for UI decisions, retries, telemetry and support flows. */
80
+ interpret() {
81
+ const category = classifyError(this.status, this.code);
82
+ return {
83
+ category,
84
+ action: actionFor(category),
85
+ retryable: isRetryable(category),
86
+ ...this.retryAfter !== void 0 ? { retryAfter: this.retryAfter } : {},
87
+ ...this.requestId !== void 0 ? { requestId: this.requestId } : {}
88
+ };
89
+ }
90
+ get category() {
91
+ return this.interpret().category;
92
+ }
93
+ get retryable() {
94
+ return this.interpret().retryable;
95
+ }
96
+ toJSON() {
97
+ return {
98
+ name: this.name,
99
+ message: this.message,
100
+ status: this.status,
101
+ code: this.code,
102
+ title: this.title,
103
+ detail: this.detail,
104
+ requestId: this.requestId,
105
+ retryAfter: this.retryAfter,
106
+ ...this.interpret()
107
+ };
108
+ }
46
109
  };
47
- var ErrorCodes = {
48
- EmailTaken: "user.email_taken",
49
- PasswordWeak: "user.password_weak",
50
- PasswordPwned: "user.password_pwned"
51
- };
110
+ function classifyError(status, code) {
111
+ if (code === ErrorCodes.NetworkError) return "network";
112
+ if (code === ErrorCodes.Timeout || status === 408) return "timeout";
113
+ if (code === ErrorCodes.RateLimited || status === 429) return "rate_limit";
114
+ if (code === ErrorCodes.EmailTaken || status === 409) return "conflict";
115
+ if (code === ErrorCodes.PasswordWeak || code === ErrorCodes.PasswordPwned) return "validation";
116
+ if (code === ErrorCodes.SessionMalformed) return "server";
117
+ if (code.startsWith("auth.") || status === 401) return "authentication";
118
+ if (status === 403) return "authorization";
119
+ if (status === 404) return "not_found";
120
+ if (status === 400 || status === 422) return "validation";
121
+ if (status >= 500) return "server";
122
+ return "unknown";
123
+ }
124
+ function actionFor(category) {
125
+ switch (category) {
126
+ case "network":
127
+ case "timeout":
128
+ case "rate_limit":
129
+ case "server":
130
+ return "retry";
131
+ case "authentication":
132
+ return "reauthenticate";
133
+ case "authorization":
134
+ return "request_access";
135
+ case "validation":
136
+ return "fix_input";
137
+ case "not_found":
138
+ return "not_found";
139
+ case "conflict":
140
+ return "resolve_conflict";
141
+ case "unknown":
142
+ return "contact_support";
143
+ }
144
+ }
145
+ function isRetryable(category) {
146
+ return category === "network" || category === "timeout" || category === "rate_limit" || category === "server";
147
+ }
52
148
 
53
- // src/storage.ts
149
+ // src/session/storage.ts
54
150
  var STORAGE_KEY = "authyon.session";
55
- function memoryStorage() {
151
+ function isSession(value) {
152
+ if (!value || typeof value !== "object") return false;
153
+ const candidate = value;
154
+ 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);
155
+ }
156
+ function createMemoryStorage() {
56
157
  let session = null;
57
158
  return {
58
159
  get: () => session,
@@ -64,12 +165,16 @@ function memoryStorage() {
64
165
  }
65
166
  };
66
167
  }
67
- function localStorageAdapter(key = STORAGE_KEY) {
168
+ function createLocalStorage(key = STORAGE_KEY) {
68
169
  return {
69
170
  get() {
70
171
  try {
71
172
  const raw = window.localStorage.getItem(key);
72
- return raw ? JSON.parse(raw) : null;
173
+ if (!raw) return null;
174
+ const parsed = JSON.parse(raw);
175
+ if (isSession(parsed)) return parsed;
176
+ window.localStorage.removeItem(key);
177
+ return null;
73
178
  } catch {
74
179
  return null;
75
180
  }
@@ -88,22 +193,217 @@ function localStorageAdapter(key = STORAGE_KEY) {
88
193
  }
89
194
  };
90
195
  }
91
- function defaultStorage() {
92
- if (typeof window !== "undefined" && typeof window.localStorage !== "undefined") {
93
- return localStorageAdapter();
94
- }
95
- return memoryStorage();
196
+ function createDefaultStorage() {
197
+ return createMemoryStorage();
96
198
  }
97
199
 
98
- // src/client.ts
200
+ // ../../internal/core/config/defaults.ts
99
201
  var DEFAULT_BASE_URL = "https://api.authyon.com";
100
- var EXPIRY_SKEW_MS = 3e4;
202
+ var DEFAULT_EXPIRY_SKEW_MS = 3e4;
203
+
204
+ // ../../internal/core/http/query.ts
205
+ function appendQuery(path, params) {
206
+ if (!params) return path;
207
+ const query = new URLSearchParams();
208
+ for (const [key, value2] of Object.entries(params)) {
209
+ if (value2 !== void 0) query.set(key, String(value2));
210
+ }
211
+ const value = query.toString();
212
+ return value ? `${path}${path.includes("?") ? "&" : "?"}${value}` : path;
213
+ }
214
+
215
+ // ../../internal/core/http/httpAdapter.ts
216
+ var FetchHttpAdapter = class {
217
+ constructor(fetchImpl = fetch.bind(globalThis)) {
218
+ this.fetchImpl = fetchImpl;
219
+ }
220
+ request(request) {
221
+ return this.fetchImpl(request.url, {
222
+ method: request.method,
223
+ headers: request.headers,
224
+ body: request.body,
225
+ signal: request.signal
226
+ });
227
+ }
228
+ };
229
+ var LoggingHttpAdapter = class {
230
+ constructor(adapter, options) {
231
+ this.adapter = adapter;
232
+ this.options = options;
233
+ }
234
+ async request(request) {
235
+ const startedAt = Date.now();
236
+ const eventBase = {
237
+ method: request.method,
238
+ url: sanitizeUrl(request.url)
239
+ };
240
+ this.log({ type: "request", ...eventBase, timestamp: startedAt });
241
+ try {
242
+ const response = await this.adapter.request(request);
243
+ this.log({
244
+ type: "response",
245
+ ...eventBase,
246
+ status: response.status,
247
+ durationMs: Date.now() - startedAt,
248
+ requestId: response.headers.get("x-request-id") ?? response.headers.get("trace-id") ?? void 0,
249
+ timestamp: Date.now()
250
+ });
251
+ return response;
252
+ } catch (error) {
253
+ this.log({
254
+ type: "error",
255
+ ...eventBase,
256
+ errorName: error instanceof Error ? error.name : "UnknownError",
257
+ durationMs: Date.now() - startedAt,
258
+ timestamp: Date.now()
259
+ });
260
+ throw error;
261
+ }
262
+ }
263
+ log(event) {
264
+ if (!this.options.enabled) return;
265
+ const logger = this.options.logger ?? defaultHttpLogger;
266
+ try {
267
+ logger(event);
268
+ } catch {
269
+ }
270
+ }
271
+ };
272
+ function defaultHttpLogger(event) {
273
+ console.debug("[Authyon HTTP]", event);
274
+ }
275
+ function sanitizeUrl(value) {
276
+ const url = new URL(value);
277
+ const queryKeys = /* @__PURE__ */ new Set();
278
+ url.searchParams.forEach((_value, key) => queryKeys.add(key));
279
+ url.search = queryKeys.size > 0 ? [...queryKeys].map((key) => `${encodeURIComponent(key)}=REDACTED`).join("&") : "";
280
+ return url.toString();
281
+ }
282
+
283
+ // ../../internal/core/http/transport.ts
284
+ var DEFAULT_TIMEOUT_MS = 15e3;
285
+ var SharedTransportError = class extends Error {
286
+ constructor(code, cause) {
287
+ super(code === "request.timeout" ? "Request timed out" : "Network request failed");
288
+ this.name = "SharedTransportError";
289
+ this.code = code;
290
+ this.cause = cause;
291
+ }
292
+ };
293
+ function createSharedTransport(options) {
294
+ const baseUrl = normalizeBaseUrl(options.baseUrl, options.allowInsecureHttp ?? false);
295
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
296
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
297
+ throw new Error("Authyon: `timeoutMs` must be a non-negative finite number");
298
+ }
299
+ if (options.httpAdapter && options.fetch) {
300
+ throw new Error("Authyon: use either `httpAdapter` or `fetch`, not both");
301
+ }
302
+ const baseAdapter = options.httpAdapter ?? new FetchHttpAdapter(options.fetch);
303
+ const httpAdapter = options.httpLogger ? new LoggingHttpAdapter(baseAdapter, options.httpLogger) : baseAdapter;
304
+ return {
305
+ baseUrl,
306
+ async request(path, init) {
307
+ const controller = new AbortController();
308
+ const timeout = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : void 0;
309
+ try {
310
+ return await httpAdapter.request({
311
+ url: `${baseUrl}${path}`,
312
+ method: init.method ?? "GET",
313
+ headers: normalizeHeaders(init.headers),
314
+ body: init.body,
315
+ signal: controller.signal
316
+ });
317
+ } catch (cause) {
318
+ throw new SharedTransportError(
319
+ controller.signal.aborted ? ErrorCodes.Timeout : ErrorCodes.NetworkError,
320
+ cause
321
+ );
322
+ } finally {
323
+ if (timeout !== void 0) clearTimeout(timeout);
324
+ }
325
+ }
326
+ };
327
+ }
328
+ function normalizeHeaders(headers) {
329
+ const normalized = {};
330
+ new Headers(headers).forEach((value, key) => {
331
+ normalized[key] = value;
332
+ });
333
+ return normalized;
334
+ }
335
+ function responseMetadata(response) {
336
+ const retryAfterHeader = response.headers.get("retry-after");
337
+ const retryAfter = retryAfterHeader ? Number(retryAfterHeader) : void 0;
338
+ return {
339
+ requestId: response.headers.get("x-request-id") ?? response.headers.get("trace-id") ?? void 0,
340
+ retryAfter: Number.isFinite(retryAfter) ? retryAfter : void 0
341
+ };
342
+ }
343
+ function normalizeBaseUrl(value, allowInsecureHttp) {
344
+ let url;
345
+ try {
346
+ url = new URL(value);
347
+ } catch {
348
+ throw new Error("Authyon: `baseUrl` must be an absolute URL");
349
+ }
350
+ const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
351
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && (loopback || allowInsecureHttp))) {
352
+ throw new Error(
353
+ "Authyon: `baseUrl` must use HTTPS (HTTP is allowed only for loopback or with `allowInsecureHttp`)"
354
+ );
355
+ }
356
+ if (url.username || url.password || url.search || url.hash) {
357
+ throw new Error(
358
+ "Authyon: `baseUrl` cannot contain credentials, query parameters, or fragments"
359
+ );
360
+ }
361
+ return url.toString().replace(/\/+$/, "");
362
+ }
363
+
364
+ // ../../internal/core/http/jsonHttpClient.ts
365
+ var JsonHttpClient = class {
366
+ constructor(transport) {
367
+ this.transport = transport;
368
+ }
369
+ async send(path, options = {}) {
370
+ const headers = { ...options.headers };
371
+ if (options.body !== void 0) headers["Content-Type"] = "application/json";
372
+ try {
373
+ return await this.transport.request(appendQuery(path, options.query), {
374
+ method: options.method ?? "GET",
375
+ headers,
376
+ body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
377
+ });
378
+ } catch (cause) {
379
+ if (!(cause instanceof SharedTransportError)) throw cause;
380
+ throw new AuthyonError(0, { code: cause.code, title: cause.message }, { cause: cause.cause });
381
+ }
382
+ }
383
+ async parse(response) {
384
+ if (!response.ok) {
385
+ let body = {};
386
+ try {
387
+ body = await response.json();
388
+ } catch {
389
+ }
390
+ throw new AuthyonError(response.status, body, responseMetadata(response));
391
+ }
392
+ if (response.status === 204) return void 0;
393
+ return await response.json();
394
+ }
395
+ async request(path, options = {}) {
396
+ return this.parse(await this.send(path, options));
397
+ }
398
+ };
399
+
400
+ // src/client/authyonClient.ts
101
401
  var FALLBACK_EXPIRES_IN = 1800;
102
402
  function readTokens(raw) {
103
403
  const tokens = raw.tokens ?? raw;
104
404
  if (!tokens.accessToken || !tokens.refreshToken) {
105
405
  throw new AuthyonError(502, {
106
- code: "session.malformed",
406
+ code: ErrorCodes.SessionMalformed,
107
407
  title: "Malformed session response",
108
408
  detail: "The session response carried no access/refresh token pair."
109
409
  });
@@ -159,7 +459,7 @@ var AuthyonClient = class {
159
459
  /** GET /auth/sessions — active refresh-token sessions with device/IP data. */
160
460
  sessions: () => this.request("/auth/sessions", { bearer: true }),
161
461
  /** GET /auth/me/activities — paginated recent account activity for the current user. */
162
- activities: (params = {}) => this.request(`/auth/me/activities?${toQuery(params)}`, { bearer: true }),
462
+ activities: (params = {}) => this.request(appendQuery("/auth/me/activities", params), { bearer: true }),
163
463
  /**
164
464
  * Revokes a single session by id (e.g. one entry from `sessions()`),
165
465
  * signing that device out without affecting the current one.
@@ -215,11 +515,11 @@ var AuthyonClient = class {
215
515
  /**
216
516
  * GET /auth/tenants/{organizationId}/members — paginated list of an
217
517
  * organization's members. Consistent with the confirmed-live
218
- * `Page<T>` envelope every other `skip`/`take` endpoint returns
518
+ * `Paged<T>` envelope every other `skip`/`take` endpoint returns
219
519
  * (`user.activities()`, `@authyon/server`'s `environment.users.list()`).
220
520
  */
221
521
  list: (organizationId, params = {}) => this.request(
222
- `/auth/tenants/${encodeURIComponent(organizationId)}/members?${toQuery(params)}`,
522
+ appendQuery(`/auth/tenants/${encodeURIComponent(organizationId)}/members`, params),
223
523
  { bearer: true }
224
524
  ),
225
525
  /** POST /auth/tenants/{organizationId}/members — invite a member by e-mail. */
@@ -312,10 +612,18 @@ var AuthyonClient = class {
312
612
  if (!options.envKey)
313
613
  throw new Error("Authyon: `envKey` is required (pk_live_... / pk_test_...)");
314
614
  this.envKey = options.envKey;
315
- this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
316
- this.storage = options.storage ?? defaultStorage();
615
+ this.transport = createSharedTransport({
616
+ baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
617
+ allowInsecureHttp: options.allowInsecureHttp,
618
+ timeoutMs: options.timeoutMs,
619
+ httpAdapter: options.httpAdapter,
620
+ httpLogger: options.httpLogger,
621
+ fetch: options.fetch
622
+ });
623
+ this.baseUrl = this.transport.baseUrl;
624
+ this.http = new JsonHttpClient(this.transport);
625
+ this.storage = options.storage ?? createDefaultStorage();
317
626
  this.autoRefresh = options.autoRefresh ?? true;
318
- this.fetchImpl = options.fetch ?? fetch.bind(globalThis);
319
627
  }
320
628
  // ── Session state ────────────────────────────────────────────────────────
321
629
  /** Current persisted session, or null when signed out. */
@@ -323,7 +631,13 @@ var AuthyonClient = class {
323
631
  return this.storage.get();
324
632
  }
325
633
  isAuthenticated() {
326
- return this.getSession() !== null;
634
+ return this.getAuthState() === "authenticated";
635
+ }
636
+ /** Synchronous snapshot of the locally available authentication state. */
637
+ getAuthState() {
638
+ const session = this.getSession();
639
+ if (!session) return "signed_out";
640
+ return Date.now() < session.expiresAt ? "authenticated" : "expired";
327
641
  }
328
642
  /**
329
643
  * Returns a valid access token, refreshing it transparently when it is
@@ -332,15 +646,41 @@ var AuthyonClient = class {
332
646
  async getAccessToken() {
333
647
  const session = this.getSession();
334
648
  if (!session) return null;
335
- if (this.autoRefresh && Date.now() >= session.expiresAt - EXPIRY_SKEW_MS) {
649
+ if (this.autoRefresh && Date.now() >= session.expiresAt - DEFAULT_EXPIRY_SKEW_MS) {
336
650
  try {
337
651
  return (await this.refresh()).accessToken;
338
- } catch {
339
- return null;
652
+ } catch (error) {
653
+ if (error instanceof AuthyonError && (error.status === 401 || error.status === 403)) {
654
+ return null;
655
+ }
656
+ throw error;
340
657
  }
341
658
  }
342
659
  return session.accessToken;
343
660
  }
661
+ /**
662
+ * Refreshes when needed, validates the server-side session through `GET /auth/me`,
663
+ * and stores the fresh user profile. Returns null when the session is no longer valid.
664
+ */
665
+ async validateSession() {
666
+ const accessToken = await this.getAccessToken();
667
+ if (!accessToken) return null;
668
+ try {
669
+ const user = await this.user.me();
670
+ const current = this.getSession();
671
+ if (!current) return null;
672
+ const session = { ...current, user };
673
+ this.storage.set(session);
674
+ this.emit({ type: "session_validated", session });
675
+ return session;
676
+ } catch (error) {
677
+ if (error instanceof AuthyonError && (error.status === 401 || error.status === 403)) {
678
+ this.clearSession();
679
+ return null;
680
+ }
681
+ throw error;
682
+ }
683
+ }
344
684
  /** Subscribe to sign-in / refresh / sign-out events. Returns an unsubscribe fn. */
345
685
  onAuthStateChange(listener) {
346
686
  this.listeners.add(listener);
@@ -386,38 +726,26 @@ var AuthyonClient = class {
386
726
  "X-Authyon-Environment": this.envKey,
387
727
  ...options.headers
388
728
  };
389
- if (options.body !== void 0) headers["Content-Type"] = "application/json";
390
729
  if (options.bearer) {
391
730
  const token = await this.getAccessToken();
392
731
  if (!token)
393
- throw new AuthyonError(401, { code: "auth.not_authenticated", title: "Not authenticated" });
732
+ throw new AuthyonError(401, {
733
+ code: ErrorCodes.NotAuthenticated,
734
+ title: "Not authenticated"
735
+ });
394
736
  headers.Authorization = `Bearer ${token}`;
395
737
  }
396
- const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
397
- method: options.method ?? "GET",
398
- headers,
399
- body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
400
- });
738
+ const response = await this.http.send(path, { ...options, headers });
401
739
  if (response.status === 401 && options.bearer && this.autoRefresh && !isRetry && this.getSession()) {
402
740
  try {
403
741
  await this.refresh();
404
742
  } catch {
405
743
  this.clearSession();
406
- throw await this.toError(response);
744
+ return this.http.parse(response);
407
745
  }
408
746
  return this.request(path, options, true);
409
747
  }
410
- if (!response.ok) throw await this.toError(response);
411
- if (response.status === 204) return void 0;
412
- return await response.json();
413
- }
414
- async toError(response) {
415
- let body = {};
416
- try {
417
- body = await response.json();
418
- } catch {
419
- }
420
- return new AuthyonError(response.status, body);
748
+ return this.http.parse(response);
421
749
  }
422
750
  // ── Auth flows ───────────────────────────────────────────────────────────
423
751
  /** POST /auth/register — creates a new user (rate-limited to 20/hour per IP). */
@@ -453,7 +781,10 @@ var AuthyonClient = class {
453
781
  if (this.refreshInFlight) return this.refreshInFlight;
454
782
  const current = this.getSession();
455
783
  if (!current)
456
- throw new AuthyonError(401, { code: "auth.not_authenticated", title: "Not authenticated" });
784
+ throw new AuthyonError(401, {
785
+ code: ErrorCodes.NotAuthenticated,
786
+ title: "Not authenticated"
787
+ });
457
788
  this.refreshInFlight = this.request("/auth/refresh", {
458
789
  method: "POST",
459
790
  body: { refreshToken: current.refreshToken }
@@ -503,6 +834,8 @@ var AuthyonClient = class {
503
834
  * A browser app has no client secret to present, so this will fail from
504
835
  * `@authyon/auth` in practice; call it from your backend via
505
836
  * `@authyon/server` instead.
837
+ *
838
+ * @deprecated Use `@authyon/server.introspect()` from a trusted backend.
506
839
  */
507
840
  async introspect(token) {
508
841
  const accessToken = token ?? await this.getAccessToken();
@@ -512,6 +845,8 @@ var AuthyonClient = class {
512
845
  * POST /auth/validate — recommended: cross-checks DB state, catches
513
846
  * revocation immediately. Same caller-authentication requirement (and
514
847
  * the same practical limitation from the browser) as `introspect()`.
848
+ *
849
+ * @deprecated Use `@authyon/server.validate()` from a trusted backend.
515
850
  */
516
851
  async validate(token) {
517
852
  const accessToken = token ?? await this.getAccessToken();
@@ -534,20 +869,352 @@ function normalizeUser(raw) {
534
869
  function createClient(options) {
535
870
  return new AuthyonClient(options);
536
871
  }
537
- function toQuery(params) {
538
- const query = new URLSearchParams();
539
- for (const [key, value] of Object.entries(params)) {
540
- if (value !== void 0) query.set(key, String(value));
872
+
873
+ // src/client/authyonClientBuilder.ts
874
+ var AuthyonClientBuilder = class {
875
+ constructor(envKey) {
876
+ this.options = { envKey };
877
+ }
878
+ withBaseUrl(baseUrl, allowInsecureHttp = false) {
879
+ this.options.baseUrl = baseUrl;
880
+ this.options.allowInsecureHttp = allowInsecureHttp;
881
+ return this;
882
+ }
883
+ withStorage(storage) {
884
+ this.options.storage = storage;
885
+ return this;
886
+ }
887
+ withAutomaticRefresh(enabled = true) {
888
+ this.options.autoRefresh = enabled;
889
+ return this;
890
+ }
891
+ withTimeout(timeoutMs) {
892
+ this.options.timeoutMs = timeoutMs;
893
+ return this;
894
+ }
895
+ withHttpAdapter(httpAdapter) {
896
+ this.options.httpAdapter = httpAdapter;
897
+ return this;
898
+ }
899
+ withHttpLogger(httpLogger) {
900
+ this.options.httpLogger = httpLogger;
901
+ return this;
902
+ }
903
+ build() {
904
+ return new AuthyonClient({ ...this.options });
905
+ }
906
+ };
907
+
908
+ // src/session/sessionController.ts
909
+ var SERVER_SNAPSHOT = {
910
+ status: "unauthenticated",
911
+ session: null,
912
+ user: null,
913
+ error: null
914
+ };
915
+ var AuthyonSessionController = class {
916
+ constructor(client, options = {}) {
917
+ this.client = client;
918
+ this.listeners = /* @__PURE__ */ new Set();
919
+ this.getSnapshot = () => this.snapshot;
920
+ this.getServerSnapshot = () => SERVER_SNAPSHOT;
921
+ this.subscribe = (listener) => {
922
+ this.listeners.add(listener);
923
+ return () => this.listeners.delete(listener);
924
+ };
925
+ this.refreshAheadMs = options.refreshAheadMs ?? 3e4;
926
+ if (!Number.isFinite(this.refreshAheadMs) || this.refreshAheadMs < 0) {
927
+ throw new Error("Authyon: `refreshAheadMs` must be a non-negative finite number");
928
+ }
929
+ const session = client.getSession();
930
+ this.snapshot = {
931
+ status: session ? "validating" : "unauthenticated",
932
+ session,
933
+ user: session?.user ?? null,
934
+ error: null
935
+ };
936
+ }
937
+ start() {
938
+ if (!this.unsubscribeAuth) {
939
+ this.unsubscribeAuth = this.client.onAuthStateChange((event) => {
940
+ if (event.type === "signed_out") {
941
+ this.cancelRefresh();
942
+ this.setSnapshot({
943
+ status: "unauthenticated",
944
+ session: null,
945
+ user: null,
946
+ error: null
947
+ });
948
+ return;
949
+ }
950
+ if (event.type === "session_validated") {
951
+ this.acceptSession(event.session);
952
+ return;
953
+ }
954
+ this.setSnapshot({
955
+ status: "validating",
956
+ session: event.session,
957
+ user: event.session.user ?? null,
958
+ error: null
959
+ });
960
+ void this.validate();
961
+ });
962
+ }
963
+ void this.validate();
964
+ return () => this.stop();
965
+ }
966
+ stop() {
967
+ this.unsubscribeAuth?.();
968
+ this.unsubscribeAuth = void 0;
969
+ this.cancelRefresh();
970
+ }
971
+ validate() {
972
+ if (this.validation) return this.validation;
973
+ const localSession = this.client.getSession();
974
+ if (!localSession) {
975
+ this.setSnapshot({
976
+ status: "unauthenticated",
977
+ session: null,
978
+ user: null,
979
+ error: null
980
+ });
981
+ return Promise.resolve(this.snapshot);
982
+ }
983
+ this.setSnapshot({ ...this.snapshot, status: "validating", error: null });
984
+ this.validation = this.client.validateSession().then((session) => {
985
+ if (session) this.acceptSession(session);
986
+ else {
987
+ this.setSnapshot({
988
+ status: "unauthenticated",
989
+ session: null,
990
+ user: null,
991
+ error: null
992
+ });
993
+ }
994
+ return this.snapshot;
995
+ }).catch((error) => {
996
+ this.setSnapshot({ ...this.snapshot, status: "error", error });
997
+ return this.snapshot;
998
+ }).finally(() => {
999
+ this.validation = void 0;
1000
+ });
1001
+ return this.validation;
1002
+ }
1003
+ async refreshNow() {
1004
+ if (!this.client.getSession()) return this.validate();
1005
+ try {
1006
+ await this.client.refresh();
1007
+ } catch (error) {
1008
+ if (!this.client.getSession()) return this.validate();
1009
+ this.setSnapshot({ ...this.snapshot, status: "error", error });
1010
+ return this.snapshot;
1011
+ }
1012
+ return this.validate();
1013
+ }
1014
+ acceptSession(session) {
1015
+ this.setSnapshot({
1016
+ status: "authenticated",
1017
+ session,
1018
+ user: session.user ?? null,
1019
+ error: null
1020
+ });
1021
+ this.scheduleRefresh(session);
1022
+ }
1023
+ scheduleRefresh(session) {
1024
+ this.cancelRefresh();
1025
+ const delay = Math.max(1e3, session.expiresAt - Date.now() - this.refreshAheadMs);
1026
+ this.refreshTimer = setTimeout(() => void this.refreshNow(), delay);
1027
+ }
1028
+ cancelRefresh() {
1029
+ if (this.refreshTimer !== void 0) clearTimeout(this.refreshTimer);
1030
+ this.refreshTimer = void 0;
1031
+ }
1032
+ setSnapshot(snapshot) {
1033
+ this.snapshot = snapshot;
1034
+ for (const listener of this.listeners) listener();
1035
+ }
1036
+ };
1037
+
1038
+ // ../../internal/core/authorization/ability.ts
1039
+ var AuthyonAbility = class {
1040
+ constructor(rules = [], detectSubjectType = defaultSubjectType) {
1041
+ this.detectSubjectType = detectSubjectType;
1042
+ this.listeners = /* @__PURE__ */ new Set();
1043
+ this.currentRules = rules.map(cloneRule);
1044
+ }
1045
+ get rules() {
1046
+ return this.currentRules;
1047
+ }
1048
+ can(action, subject, field) {
1049
+ const subjectType = typeof subject === "string" ? subject : this.detectSubjectType(subject);
1050
+ for (let index = this.currentRules.length - 1; index >= 0; index -= 1) {
1051
+ const rule = this.currentRules[index];
1052
+ if (!matchesToken(rule.action, action, "manage")) continue;
1053
+ if (!matchesToken(rule.subject, subjectType, "all")) continue;
1054
+ if (field && rule.fields && !rule.fields.some((value) => matchesField(value, field)))
1055
+ continue;
1056
+ if (rule.conditions) {
1057
+ if (typeof subject === "string" || !matchesConditions(subject, rule.conditions)) continue;
1058
+ }
1059
+ return !rule.inverted;
1060
+ }
1061
+ return false;
1062
+ }
1063
+ cannot(action, subject, field) {
1064
+ return !this.can(action, subject, field);
1065
+ }
1066
+ rulesFor(action, subject) {
1067
+ return this.currentRules.filter(
1068
+ (rule) => matchesToken(rule.action, action, "manage") && matchesToken(rule.subject, subject, "all")
1069
+ );
1070
+ }
1071
+ update(rules) {
1072
+ this.currentRules = rules.map(cloneRule);
1073
+ for (const listener of this.listeners) listener(this.rules);
1074
+ }
1075
+ on(event, listener) {
1076
+ if (event !== "updated") return () => void 0;
1077
+ this.listeners.add(listener);
1078
+ return () => this.listeners.delete(listener);
1079
+ }
1080
+ };
1081
+ var AuthyonAbilityBuilder = class {
1082
+ constructor() {
1083
+ this.rules = [];
1084
+ }
1085
+ can(action, subject, conditions, fields) {
1086
+ this.rules.push({ action, subject, conditions, fields });
1087
+ return this;
541
1088
  }
542
- return query.toString();
1089
+ cannot(action, subject, conditions, fields, reason) {
1090
+ this.rules.push({ action, subject, conditions, fields, reason, inverted: true });
1091
+ return this;
1092
+ }
1093
+ build(options = {}) {
1094
+ return new AuthyonAbility(this.rules, options.detectSubjectType);
1095
+ }
1096
+ };
1097
+ function createAuthyonAbility(source = {}, options = {}) {
1098
+ return new AuthyonAbility(createAuthyonRules(source, options), options.detectSubjectType);
1099
+ }
1100
+ function createAuthyonRules(source = {}, options = {}) {
1101
+ const permissions = /* @__PURE__ */ new Set([
1102
+ ...source.permissions ?? [],
1103
+ ...source.scope?.split(/\s+/).filter(Boolean) ?? []
1104
+ ]);
1105
+ const rules = [...permissions].map(permissionToRule).filter(isAbilityRule);
1106
+ for (const role of /* @__PURE__ */ new Set([...source.roles ?? [], ...options.roles ?? []])) {
1107
+ rules.push(...(options.roleRules?.[role] ?? []).map(cloneRule));
1108
+ }
1109
+ rules.push(...(options.rules ?? []).map(cloneRule));
1110
+ return rules;
1111
+ }
1112
+ function permissionToRule(permission) {
1113
+ const normalized = permission.trim();
1114
+ if (!normalized) return null;
1115
+ if (normalized === "*" || normalized === "*:*" || normalized === "all:manage") {
1116
+ return { action: "manage", subject: "all" };
1117
+ }
1118
+ const separator = normalized.lastIndexOf(":");
1119
+ if (separator <= 0 || separator === normalized.length - 1) return null;
1120
+ const subject = normalized.slice(0, separator);
1121
+ const action = normalized.slice(separator + 1);
1122
+ return {
1123
+ action: action === "*" ? "manage" : action,
1124
+ subject: subject === "*" ? "all" : subject
1125
+ };
1126
+ }
1127
+ function matchesToken(value, expected, wildcard) {
1128
+ return (Array.isArray(value) ? value : [value]).some(
1129
+ (candidate) => candidate === expected || candidate === wildcard || candidate === "*"
1130
+ );
1131
+ }
1132
+ function matchesField(pattern, field) {
1133
+ if (pattern === "*" || pattern === field) return true;
1134
+ return pattern.endsWith(".*") && field.startsWith(pattern.slice(0, -1));
1135
+ }
1136
+ function matchesConditions(subject, conditions) {
1137
+ return Object.entries(conditions).every(([path, expected]) => {
1138
+ if (path === "$and" && Array.isArray(expected)) {
1139
+ return expected.every(
1140
+ (condition) => matchesConditions(subject, condition)
1141
+ );
1142
+ }
1143
+ if (path === "$or" && Array.isArray(expected)) {
1144
+ return expected.some(
1145
+ (condition) => matchesConditions(subject, condition)
1146
+ );
1147
+ }
1148
+ return matchesValue(readPath(subject, path), expected);
1149
+ });
1150
+ }
1151
+ function matchesValue(actual, expected) {
1152
+ if (!isRecord(expected) || !Object.keys(expected).some((key) => key.startsWith("$"))) {
1153
+ return isRecord(expected) && isRecord(actual) ? matchesConditions(actual, expected) : Object.is(actual, expected);
1154
+ }
1155
+ return Object.entries(expected).every(([operator, operand]) => {
1156
+ switch (operator) {
1157
+ case "$eq":
1158
+ return Object.is(actual, operand);
1159
+ case "$ne":
1160
+ return !Object.is(actual, operand);
1161
+ case "$in":
1162
+ return Array.isArray(operand) && operand.some((value) => Object.is(actual, value));
1163
+ case "$nin":
1164
+ return Array.isArray(operand) && !operand.some((value) => Object.is(actual, value));
1165
+ case "$gt":
1166
+ return typeof actual === "number" && typeof operand === "number" && actual > operand;
1167
+ case "$gte":
1168
+ return typeof actual === "number" && typeof operand === "number" && actual >= operand;
1169
+ case "$lt":
1170
+ return typeof actual === "number" && typeof operand === "number" && actual < operand;
1171
+ case "$lte":
1172
+ return typeof actual === "number" && typeof operand === "number" && actual <= operand;
1173
+ case "$exists":
1174
+ return operand ? actual !== void 0 : actual === void 0;
1175
+ default:
1176
+ return false;
1177
+ }
1178
+ });
1179
+ }
1180
+ function readPath(value, path) {
1181
+ return path.split(".").reduce((current, part) => isRecord(current) ? current[part] : void 0, value);
1182
+ }
1183
+ function defaultSubjectType(subject) {
1184
+ const explicit = subject.__type ?? subject.type ?? subject.kind;
1185
+ if (typeof explicit === "string") return explicit;
1186
+ const constructorName = subject.constructor?.name;
1187
+ return typeof constructorName === "string" ? constructorName : "Object";
1188
+ }
1189
+ function cloneRule(rule) {
1190
+ return {
1191
+ ...rule,
1192
+ action: Array.isArray(rule.action) ? [...rule.action] : rule.action,
1193
+ subject: Array.isArray(rule.subject) ? [...rule.subject] : rule.subject,
1194
+ fields: rule.fields ? [...rule.fields] : void 0
1195
+ };
1196
+ }
1197
+ function isRecord(value) {
1198
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1199
+ }
1200
+ function isAbilityRule(value) {
1201
+ return value !== null;
543
1202
  }
544
1203
  // Annotate the CommonJS export names for ESM import in node:
545
1204
  0 && (module.exports = {
1205
+ AuthyonAbility,
1206
+ AuthyonAbilityBuilder,
546
1207
  AuthyonClient,
1208
+ AuthyonClientBuilder,
547
1209
  AuthyonError,
1210
+ AuthyonSessionController,
548
1211
  ErrorCodes,
1212
+ FetchHttpAdapter,
1213
+ LoggingHttpAdapter,
1214
+ createAuthyonAbility,
1215
+ createAuthyonRules,
549
1216
  createClient,
550
- defaultStorage,
551
- localStorageAdapter,
552
- memoryStorage
1217
+ createDefaultStorage,
1218
+ createLocalStorage,
1219
+ createMemoryStorage
553
1220
  });