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

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