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

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
@@ -4,8 +4,40 @@ import {
4
4
  AuthyonSessionController,
5
5
  createAuthyonAbility,
6
6
  createAuthyonRules,
7
- hasPermission
8
- } from "./chunk-EHZEUM47.js";
7
+ hasPermission,
8
+ hasPermissionGroup
9
+ } from "./chunk-3XJY6BCI.js";
10
+
11
+ // ../../internal/core/security/approvals.ts
12
+ function path(id, suffix = "") {
13
+ if (typeof id !== "string" || !id.trim())
14
+ throw new TypeError("Authyon: an approval id is required.");
15
+ return `/auth/authorizations/${encodeURIComponent(id)}${suffix}`;
16
+ }
17
+ var NO_METHODS = {
18
+ password: false,
19
+ authenticator: false,
20
+ emailCode: false,
21
+ passkey: false,
22
+ passkeyCount: 0,
23
+ recoveryCodes: 0,
24
+ sso: [],
25
+ twoFactor: false
26
+ };
27
+ function subjectSecurity(request) {
28
+ return {
29
+ methods: async () => {
30
+ const me = await request("/auth/me");
31
+ return me.security ?? NO_METHODS;
32
+ },
33
+ approvals: {
34
+ get: (id) => request(path(id)),
35
+ webauthnOptions: (id) => request(path(id, "/webauthn/options"), { method: "POST" }),
36
+ confirm: (id, input) => request(path(id, "/confirm"), { method: "POST", body: input }),
37
+ reject: (id) => request(path(id, "/reject"), { method: "POST" })
38
+ }
39
+ };
40
+ }
9
41
 
10
42
  // ../../internal/core/errors/authyonError.ts
11
43
  var ErrorCodes = {
@@ -19,16 +51,45 @@ var ErrorCodes = {
19
51
  EmailTaken: "user.email_taken",
20
52
  PasswordWeak: "user.password_weak",
21
53
  PasswordPwned: "user.password_pwned",
22
- RateLimited: "rate_limited"
54
+ RateLimited: "rate_limited",
55
+ StepUpRequired: "step_up_required",
56
+ InvalidSecondFactorCode: "invalid_code",
57
+ VerificationAttemptsExhausted: "verification_attempts_exhausted",
58
+ MethodNotEnrolled: "method_not_enrolled",
59
+ InvalidApprovalState: "invalid_authorization_state",
60
+ PayloadMismatch: "payload_mismatch",
61
+ ApprovalNotConsumable: "authorization_not_consumable",
62
+ IdempotencyConflict: "idempotency_conflict",
63
+ UserDisabled: "user_disabled",
64
+ UserNotFound: "user_not_found",
65
+ TemporarilyUnavailable: "temporarily_unavailable",
66
+ InsufficientScope: "insufficient_scope",
67
+ CredentialThrottled: "credential_throttled",
68
+ ApprovalNotFound: "authorization_not_found",
69
+ SessionRevoked: "session_revoked",
70
+ TwoFactorTooManyFailures: "user.2fa.too_many_failures"
23
71
  };
72
+ var STANDARD_ERROR_FIELDS = /* @__PURE__ */ new Set([
73
+ "type",
74
+ "status",
75
+ "title",
76
+ "detail",
77
+ "code",
78
+ "error",
79
+ "error_description"
80
+ ]);
24
81
  var AuthyonError = class extends Error {
25
82
  constructor(status, body, options = {}) {
26
- super(body.detail ?? body.title ?? `Authyon request failed with status ${status}`);
83
+ const detail = body.detail ?? body.error_description;
84
+ super(detail ?? body.title ?? `Authyon request failed with status ${status}`);
27
85
  this.name = "AuthyonError";
28
86
  this.status = status;
29
- this.code = body.code ?? "unknown";
87
+ this.code = body.code ?? body.error ?? "unknown";
30
88
  this.title = body.title ?? "Error";
31
- this.detail = body.detail;
89
+ this.detail = detail;
90
+ this.extensions = Object.fromEntries(
91
+ Object.entries(body).filter(([key]) => !STANDARD_ERROR_FIELDS.has(key))
92
+ );
32
93
  this.requestId = options.requestId;
33
94
  this.retryAfter = options.retryAfter;
34
95
  this.cause = options.cause;
@@ -78,6 +139,7 @@ var AuthyonError = class extends Error {
78
139
  };
79
140
  function classifyError(status, code) {
80
141
  if (code === ErrorCodes.NetworkError) return "network";
142
+ if (code === ErrorCodes.StepUpRequired) return "authentication";
81
143
  if (code === ErrorCodes.Timeout || status === 408) return "timeout";
82
144
  if (code === ErrorCodes.RateLimited || status === 429) return "rate_limit";
83
145
  if (code === ErrorCodes.EmailTaken || status === 409) return "conflict";
@@ -171,14 +233,14 @@ var DEFAULT_BASE_URL = "https://api.authyon.com";
171
233
  var DEFAULT_EXPIRY_SKEW_MS = 3e4;
172
234
 
173
235
  // ../../internal/core/http/query.ts
174
- function appendQuery(path, params) {
175
- if (!params) return path;
236
+ function appendQuery(path2, params) {
237
+ if (!params) return path2;
176
238
  const query = new URLSearchParams();
177
239
  for (const [key, value2] of Object.entries(params)) {
178
240
  if (value2 !== void 0) query.set(key, String(value2));
179
241
  }
180
242
  const value = query.toString();
181
- return value ? `${path}${path.includes("?") ? "&" : "?"}${value}` : path;
243
+ return value ? `${path2}${path2.includes("?") ? "&" : "?"}${value}` : path2;
182
244
  }
183
245
 
184
246
  // ../../internal/core/http/httpAdapter.ts
@@ -191,7 +253,8 @@ var FetchHttpAdapter = class {
191
253
  method: request.method,
192
254
  headers: request.headers,
193
255
  body: request.body,
194
- signal: request.signal
256
+ signal: request.signal,
257
+ redirect: "error"
195
258
  });
196
259
  }
197
260
  };
@@ -243,6 +306,9 @@ function defaultHttpLogger(event) {
243
306
  }
244
307
  function sanitizeUrl(value) {
245
308
  const url = new URL(value);
309
+ url.username = "";
310
+ url.password = "";
311
+ url.hash = "";
246
312
  const queryKeys = /* @__PURE__ */ new Set();
247
313
  url.searchParams.forEach((_value, key) => queryKeys.add(key));
248
314
  url.search = queryKeys.size > 0 ? [...queryKeys].map((key) => `${encodeURIComponent(key)}=REDACTED`).join("&") : "";
@@ -272,12 +338,12 @@ function createSharedTransport(options) {
272
338
  const httpAdapter = options.httpLogger ? new LoggingHttpAdapter(baseAdapter, options.httpLogger) : baseAdapter;
273
339
  return {
274
340
  baseUrl,
275
- async request(path, init) {
341
+ async request(path2, init) {
276
342
  const controller = new AbortController();
277
343
  const timeout = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : void 0;
278
344
  try {
279
345
  return await httpAdapter.request({
280
- url: `${baseUrl}${path}`,
346
+ url: `${baseUrl}${path2}`,
281
347
  method: init.method ?? "GET",
282
348
  headers: normalizeHeaders(init.headers),
283
349
  body: init.body,
@@ -335,11 +401,11 @@ var JsonHttpClient = class {
335
401
  constructor(transport) {
336
402
  this.transport = transport;
337
403
  }
338
- async send(path, options = {}) {
404
+ async send(path2, options = {}) {
339
405
  const headers = { ...options.headers };
340
406
  if (options.body !== void 0) headers["Content-Type"] = "application/json";
341
407
  try {
342
- return await this.transport.request(appendQuery(path, options.query), {
408
+ return await this.transport.request(appendQuery(path2, options.query), {
343
409
  method: options.method ?? "GET",
344
410
  headers,
345
411
  body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
@@ -353,7 +419,9 @@ var JsonHttpClient = class {
353
419
  if (!response.ok) {
354
420
  let body = {};
355
421
  try {
356
- body = await response.json();
422
+ const parsed = await response.json();
423
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
424
+ body = parsed;
357
425
  } catch {
358
426
  }
359
427
  throw new AuthyonError(response.status, body, responseMetadata(response));
@@ -361,11 +429,33 @@ var JsonHttpClient = class {
361
429
  if (response.status === 204) return void 0;
362
430
  return await response.json();
363
431
  }
364
- async request(path, options = {}) {
365
- return this.parse(await this.send(path, options));
432
+ async request(path2, options = {}) {
433
+ return this.parse(await this.send(path2, options));
366
434
  }
367
435
  };
368
436
 
437
+ // ../../internal/core/http/clientIp.ts
438
+ function clientIpHeaders(context) {
439
+ if (!context?.clientIp) return {};
440
+ return { "X-Forwarded-For": normalizeClientIp(context.clientIp) };
441
+ }
442
+ function normalizeClientIp(value) {
443
+ let ip = value.trim();
444
+ if (ip.startsWith("[") && ip.endsWith("]")) ip = ip.slice(1, -1);
445
+ if (!ip || ip.length > 45 || ip.includes(",") || /[\r\n\s]/.test(ip) || !/^[0-9a-f:.]+$/i.test(ip) || !ip.includes(".") && !ip.includes(":")) {
446
+ throw invalidClientIp();
447
+ }
448
+ try {
449
+ new URL(ip.includes(":") ? `http://[${ip}]` : `http://${ip}`);
450
+ } catch {
451
+ throw invalidClientIp();
452
+ }
453
+ return ip;
454
+ }
455
+ function invalidClientIp() {
456
+ return new Error("Authyon: `clientIp` must be a single valid IPv4 or IPv6 address");
457
+ }
458
+
369
459
  // src/client/authyonClient.ts
370
460
  var FALLBACK_EXPIRES_IN = 1800;
371
461
  function readTokens(raw) {
@@ -443,18 +533,41 @@ var AuthyonClient = class {
443
533
  method: "DELETE",
444
534
  bearer: true
445
535
  }),
446
- /** POST /auth/password-reset/request — always resolves (no account enumeration). */
447
- requestPasswordReset: (email) => this.request("/auth/password-reset/request", { method: "POST", body: { email } }),
448
- /** POST /auth/password-reset/confirm — sets a new password and revokes all refresh tokens. */
449
- confirmPasswordReset: (token, newPassword) => this.request("/auth/password-reset/confirm", {
536
+ /**
537
+ * POST /auth/password-reset/request — resolves whether or not the address
538
+ * has an account (no account enumeration). `redirectUri` is where the
539
+ * person lands after changing the password; it must be one of the
540
+ * environment's registered redirect URIs, or the call rejects with
541
+ * `user.password_reset.redirect_uri.not_allowed`.
542
+ */
543
+ requestPasswordReset: (email, options = {}) => this.request("/auth/password-reset/request", {
450
544
  method: "POST",
451
- body: { token, newPassword }
452
- })
545
+ body: { email, ...options }
546
+ }),
547
+ /**
548
+ * POST /auth/password-reset/validate — checks a reset token without
549
+ * spending it, so the reset page can refuse a dead link before asking for
550
+ * a password. Rejects with `user.password_reset.invalid` (401) when the
551
+ * token is unknown, used or expired.
552
+ */
553
+ validatePasswordReset: (token) => this.request("/auth/password-reset/validate", { method: "POST", body: { token } }),
554
+ /**
555
+ * POST /auth/password-reset/confirm — sets a new password and revokes all
556
+ * refresh tokens. Resolves with the `redirectUri` the reset was requested
557
+ * with (`null` when there was none); following it is up to the caller.
558
+ */
559
+ confirmPasswordReset: async (token, newPassword) => {
560
+ const done = await this.request(
561
+ "/auth/password-reset/confirm",
562
+ { method: "POST", body: { token, newPassword } }
563
+ );
564
+ return { redirectUri: done?.redirectUri ?? null };
565
+ }
453
566
  };
454
567
  // ── Organization ─────────────────────────────────────────────────────────
455
568
  this.organization = {
456
- /** GET /auth/tenants — all organization memberships. */
457
- list: () => this.request("/auth/tenants", { bearer: true }),
569
+ /** GET /auth/tenants — filterable list of organization memberships. */
570
+ list: (params = {}) => this.request(appendQuery("/auth/tenants", params), { bearer: true }),
458
571
  /**
459
572
  * POST /auth/tenants — creates an organization owned by the signed-in
460
573
  * user (only available when self-service organization creation is
@@ -491,7 +604,11 @@ var AuthyonClient = class {
491
604
  appendQuery(`/auth/tenants/${encodeURIComponent(organizationId)}/members`, params),
492
605
  { bearer: true }
493
606
  ),
494
- /** POST /auth/tenants/{organizationId}/members — invite a member by e-mail. */
607
+ /**
608
+ * POST /auth/tenants/{organizationId}/members — add an existing
609
+ * environment user by e-mail (no e-mail is sent). To invite an address
610
+ * that may not have an account yet, use `organization.invites.create`.
611
+ */
495
612
  invite: (organizationId, params) => this.request(`/auth/tenants/${encodeURIComponent(organizationId)}/members`, {
496
613
  method: "POST",
497
614
  bearer: true,
@@ -503,6 +620,59 @@ var AuthyonClient = class {
503
620
  { method: "DELETE", bearer: true }
504
621
  )
505
622
  },
623
+ /**
624
+ * Invites by e-mail or by link. Managing them requires the
625
+ * `tenants:members:invite` permission on the organization; `preview` and
626
+ * `accept` are public — the token in the link is the credential.
627
+ */
628
+ invites: {
629
+ /** GET /auth/tenants/{organizationId}/invites — every invite with its status, newest first. */
630
+ list: (organizationId) => this.request(`/auth/tenants/${encodeURIComponent(organizationId)}/invites`, {
631
+ bearer: true
632
+ }),
633
+ /**
634
+ * POST /auth/tenants/{organizationId}/invites — invite an address.
635
+ * Always returns `acceptUrl`; with `sendEmail: false` Authyon does not
636
+ * mail it and you deliver the link yourself. The link is valid for 7
637
+ * days, once, and can't be read again later.
638
+ */
639
+ create: (organizationId, params) => this.request(`/auth/tenants/${encodeURIComponent(organizationId)}/invites`, {
640
+ method: "POST",
641
+ bearer: true,
642
+ body: {
643
+ email: params.email,
644
+ roles: params.roles,
645
+ sendEmail: params.sendEmail ?? true,
646
+ redirectUri: params.redirectUri
647
+ }
648
+ }),
649
+ /**
650
+ * POST /auth/tenants/{organizationId}/invites/{inviteId}/resend — issue
651
+ * a new link (the previous one stops working), mailing it unless
652
+ * `sendEmail` is `false`.
653
+ */
654
+ resend: (organizationId, inviteId, params = {}) => this.request(
655
+ `/auth/tenants/${encodeURIComponent(organizationId)}/invites/${encodeURIComponent(inviteId)}/resend`,
656
+ { method: "POST", bearer: true, body: { sendEmail: params.sendEmail ?? true } }
657
+ ),
658
+ /** DELETE /auth/tenants/{organizationId}/invites/{inviteId} — revoke a pending invite. */
659
+ revoke: (organizationId, inviteId) => this.request(
660
+ `/auth/tenants/${encodeURIComponent(organizationId)}/invites/${encodeURIComponent(inviteId)}`,
661
+ { method: "DELETE", bearer: true }
662
+ ),
663
+ /**
664
+ * POST /auth/tenant-invites/preview — public. Read the `token` query
665
+ * parameter of the invite link and pick the form: one click when
666
+ * `accountExists`, sign-up otherwise.
667
+ */
668
+ preview: (token) => this.request("/auth/tenant-invites/preview", { method: "POST", body: { token } }),
669
+ /**
670
+ * POST /auth/tenant-invites/accept — public. Joins the organization; for
671
+ * an address with no account, also creates it (password required) with
672
+ * the e-mail already confirmed. Does not sign the user in.
673
+ */
674
+ accept: (params) => this.request("/auth/tenant-invites/accept", { method: "POST", body: params })
675
+ },
506
676
  roles: {
507
677
  /** GET /auth/tenants/{organizationId}/roles — roles available in the organization. */
508
678
  list: (organizationId) => this.request(`/auth/tenants/${encodeURIComponent(organizationId)}/roles`, {
@@ -578,9 +748,20 @@ var AuthyonClient = class {
578
748
  })
579
749
  }
580
750
  };
751
+ // ── Security (customer side) ─────────────────────────────────────────────
752
+ /**
753
+ * `security.approvals`: approve or reject what your backend asked the
754
+ * customer to confirm (`@authyon/server` → `security.approvals.create`).
755
+ * Show the payload with `get()`, then `confirm()` with the customer's
756
+ * authenticator code or passkey, or `reject()`.
757
+ */
758
+ this.security = subjectSecurity(
759
+ (path2, options) => this.request(path2, { ...options, bearer: true })
760
+ );
581
761
  if (!options.envKey)
582
762
  throw new Error("Authyon: `envKey` is required (pk_live_... / pk_test_...)");
583
763
  this.envKey = options.envKey;
764
+ this.clientIp = options.clientIp;
584
765
  this.transport = createSharedTransport({
585
766
  baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
586
767
  allowInsecureHttp: options.allowInsecureHttp,
@@ -690,9 +871,10 @@ var AuthyonClient = class {
690
871
  }
691
872
  }
692
873
  // ── HTTP core ────────────────────────────────────────────────────────────
693
- async request(path, options = {}, isRetry = false) {
874
+ async request(path2, options = {}, isRetry = false) {
694
875
  const headers = {
695
876
  "X-Authyon-Environment": this.envKey,
877
+ ...clientIpHeaders({ clientIp: this.clientIp }),
696
878
  ...options.headers
697
879
  };
698
880
  if (options.bearer) {
@@ -704,7 +886,7 @@ var AuthyonClient = class {
704
886
  });
705
887
  headers.Authorization = `Bearer ${token}`;
706
888
  }
707
- const response = await this.http.send(path, { ...options, headers });
889
+ const response = await this.http.send(path2, { ...options, headers });
708
890
  if (response.status === 401 && options.bearer && this.autoRefresh && !isRetry && this.getSession()) {
709
891
  try {
710
892
  await this.refresh();
@@ -712,7 +894,7 @@ var AuthyonClient = class {
712
894
  this.clearSession();
713
895
  return this.http.parse(response);
714
896
  }
715
- return this.request(path, options, true);
897
+ return this.request(path2, options, true);
716
898
  }
717
899
  return this.http.parse(response);
718
900
  }
@@ -857,6 +1039,10 @@ var AuthyonClientBuilder = class {
857
1039
  this.options.autoRefresh = enabled;
858
1040
  return this;
859
1041
  }
1042
+ withClientIp(clientIp) {
1043
+ this.options.clientIp = clientIp;
1044
+ return this;
1045
+ }
860
1046
  withTimeout(timeoutMs) {
861
1047
  this.options.timeoutMs = timeoutMs;
862
1048
  return this;
@@ -889,5 +1075,6 @@ export {
889
1075
  createDefaultStorage,
890
1076
  createLocalStorage,
891
1077
  createMemoryStorage,
892
- hasPermission
1078
+ hasPermission,
1079
+ hasPermissionGroup
893
1080
  };
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { U as User, B as SessionControllerOptions, A as AuthyonClient, E as SessionSnapshot, S as Session, f as AbilitySubject, n as AuthyonAbilityOptions, l as AuthyonAbility } from '../ability-g6nBpOQM.cjs';
4
- export { q as AuthyonSessionController, G as SessionSnapshotListener, J as SessionStatus } from '../ability-g6nBpOQM.cjs';
3
+ import { ac as User, a1 as SessionControllerOptions, A as AuthyonClient, a3 as SessionSnapshot, a0 as Session, f as AbilitySubject, t as AuthyonAbilityOptions, r as AuthyonAbility } from '../ability-BTcayvkM.cjs';
4
+ export { w as AuthyonSessionController, a4 as SessionSnapshotListener, a5 as SessionStatus } from '../ability-BTcayvkM.cjs';
5
5
 
6
6
  interface AuthyonProviderProps<TUser extends User = User> extends SessionControllerOptions {
7
7
  client: AuthyonClient;
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { U as User, B as SessionControllerOptions, A as AuthyonClient, E as SessionSnapshot, S as Session, f as AbilitySubject, n as AuthyonAbilityOptions, l as AuthyonAbility } from '../ability-g6nBpOQM.js';
4
- export { q as AuthyonSessionController, G as SessionSnapshotListener, J as SessionStatus } from '../ability-g6nBpOQM.js';
3
+ import { ac as User, a1 as SessionControllerOptions, A as AuthyonClient, a3 as SessionSnapshot, a0 as Session, f as AbilitySubject, t as AuthyonAbilityOptions, r as AuthyonAbility } from '../ability-BTcayvkM.js';
4
+ export { w as AuthyonSessionController, a4 as SessionSnapshotListener, a5 as SessionStatus } from '../ability-BTcayvkM.js';
5
5
 
6
6
  interface AuthyonProviderProps<TUser extends User = User> extends SessionControllerOptions {
7
7
  client: AuthyonClient;
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  AuthyonSessionController,
4
4
  createAuthyonAbility
5
- } from "../chunk-EHZEUM47.js";
5
+ } from "../chunk-3XJY6BCI.js";
6
6
 
7
7
  // src/react/index.tsx
8
8
  import {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@authyon/auth",
3
- "version": "0.2.0-beta.2",
3
+ "version": "0.2.0-beta.20",
4
4
  "description": "Authyon SDK for browsers — auth, sessions, multi-tenant and 2FA for vanilla JS/TS.",
5
5
  "license": "MIT",
6
6
  "type": "module",