@authowl/react 0.16.1 → 0.17.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/README.md CHANGED
@@ -67,6 +67,15 @@ When phone OTP is enabled, `<SignIn />` adds the localized phone flow automatica
67
67
  `<PhoneOTP />` is also available as a standalone surface. Production hosts must allow
68
68
  Cloudflare Turnstile's challenge script in their Content Security Policy. Local and CI
69
69
  servers use AuthOwl's documented dummy-token path when no public site key is configured.
70
+ When the server selects an Akedly Shield V1.2 route, these components fetch a fresh
71
+ challenge and complete its proof-of-work and optional Turnstile ceremony automatically.
72
+ Provider API keys and pipeline credentials remain server-side. Custom phone UIs can call
73
+ `phoneOtp.prepare()` and `solvePhoneOtpChallenge()` to implement the same guarded flow.
74
+ See the [complete headless and retry guide](https://github.com/mstfash/authowl-sdk/blob/main/docs/phone-otp.md).
75
+
76
+ `@akedly/shield` is installed with `@authowl/core` so every consumer bundler can
77
+ resolve the integration safely. It remains behind a dynamic import, so its
78
+ browser proof code is loaded only when the server selects a Shield route.
70
79
 
71
80
  When broad auth protection is enabled, the sign-in, sign-up, magic-link, email
72
81
  OTP, password-reset, and verification components automatically run an
package/dist/index.cjs CHANGED
@@ -519,6 +519,7 @@ function useSignIn() {
519
519
  signInPasskey: client.signIn.passkey,
520
520
  sendEmailOtp: client.emailOtp.sendVerificationOtp,
521
521
  signInEmailOtp: client.signIn.emailOtp,
522
+ preparePhoneOtp: client.phoneOtp.prepare,
522
523
  startPhoneOtp: client.phoneOtp.start,
523
524
  verifyPhoneOtp: client.phoneOtp.verify
524
525
  };
@@ -879,6 +880,14 @@ function SocialButtons({ providers, callbackURL }) {
879
880
  const { signInSocial } = useSignIn();
880
881
  const [pending, setPending] = React6.useState(null);
881
882
  const [error, setError] = React6.useState(null);
883
+ React6.useEffect(() => {
884
+ if (typeof window === "undefined") return;
885
+ const url = new URL(window.location.href);
886
+ if (!url.searchParams.has("authowl_error")) return;
887
+ url.searchParams.delete("authowl_error");
888
+ window.history.replaceState(window.history.state, "", url.toString());
889
+ setError(t("social.error.startFailed"));
890
+ }, [t]);
882
891
  if (providers.length === 0) return null;
883
892
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "ba-social", children: [
884
893
  providers.map((provider) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
@@ -893,7 +902,11 @@ function SocialButtons({ providers, callbackURL }) {
893
902
  setPending(provider);
894
903
  try {
895
904
  const destination = callbackURL ?? window.location.href;
896
- const res = await signInSocial({ provider, callbackURL: destination });
905
+ const res = await signInSocial({
906
+ provider,
907
+ callbackURL: destination,
908
+ errorCallbackURL: window.location.href
909
+ });
897
910
  if (res?.error) {
898
911
  setError(toServerError(res.error, t("social.error.startFailed")));
899
912
  setPending(null);
@@ -1417,16 +1430,46 @@ function PhoneOTP({ redirectTo, onSignedIn, onBack }) {
1417
1430
  const t = useT();
1418
1431
  const { sessionStore } = useAuthClient();
1419
1432
  const { config, isLoading } = usePublicConfig();
1420
- const { startPhoneOtp, verifyPhoneOtp } = useSignIn();
1433
+ const { preparePhoneOtp, startPhoneOtp, verifyPhoneOtp } = useSignIn();
1421
1434
  const { pending, error, setError, run } = useSubmitAction();
1422
1435
  const [stage, setStage] = React11.useState("phone");
1423
1436
  const [phoneNumber, setPhoneNumber] = React11.useState("");
1424
1437
  const [code, setCode] = React11.useState("");
1425
1438
  const [turnstileToken, setTurnstileToken] = React11.useState(null);
1439
+ const [guardState, setGuardState] = React11.useState({ status: "loading" });
1426
1440
  const [accepted, setAccepted] = React11.useState(false);
1427
1441
  const attempt = React11.useRef(null);
1442
+ const guardRequest = React11.useRef(0);
1428
1443
  const legal = config?.legal;
1429
1444
  const consentBlocked = Boolean(legal?.required && !accepted);
1445
+ const guard = guardState.status === "ready" ? guardState.data : null;
1446
+ const turnstileBlocked = guardState.status === "ready" && guardState.data.kind === "authowl_turnstile" && !turnstileToken;
1447
+ const humanCheckError = t("phoneOtp.error.humanCheck");
1448
+ const loadGuard = React11.useCallback(async () => {
1449
+ const request = ++guardRequest.current;
1450
+ setGuardState({ status: "loading" });
1451
+ setError(null);
1452
+ try {
1453
+ const result = await preparePhoneOtp();
1454
+ if (request !== guardRequest.current) return;
1455
+ if (result.error || !result.data) {
1456
+ setGuardState({ status: "error" });
1457
+ setError(humanCheckError);
1458
+ return;
1459
+ }
1460
+ setGuardState({ status: "ready", data: result.data });
1461
+ } catch {
1462
+ if (request !== guardRequest.current) return;
1463
+ setGuardState({ status: "error" });
1464
+ setError(humanCheckError);
1465
+ }
1466
+ }, [humanCheckError, preparePhoneOtp, setError]);
1467
+ React11.useEffect(() => {
1468
+ void loadGuard();
1469
+ return () => {
1470
+ guardRequest.current += 1;
1471
+ };
1472
+ }, [loadGuard]);
1430
1473
  if (isLoading) {
1431
1474
  return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "ba-fields", "data-testid": "phoneotp-loading", "aria-busy": "true", children: [
1432
1475
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "ba-skeleton" }),
@@ -1519,7 +1562,7 @@ function PhoneOTP({ redirectTo, onSignedIn, onBack }) {
1519
1562
  setError(t("signUp.error.consentRequired"));
1520
1563
  return;
1521
1564
  }
1522
- if (!turnstileToken) {
1565
+ if (!guard || guard.kind === "authowl_turnstile" && !turnstileToken) {
1523
1566
  setError(t("phoneOtp.error.humanCheck"));
1524
1567
  return;
1525
1568
  }
@@ -1528,7 +1571,23 @@ function PhoneOTP({ redirectTo, onSignedIn, onBack }) {
1528
1571
  }
1529
1572
  const idempotencyKey = attempt.current.idempotencyKey;
1530
1573
  void run(
1531
- () => startPhoneOtp({ phoneNumber, turnstileToken, idempotencyKey }),
1574
+ async () => {
1575
+ const selected = guard?.kind === "akedly_shield_v1_2" ? await preparePhoneOtp() : null;
1576
+ if (selected?.error || selected && !selected.data) {
1577
+ setGuardState({ status: "error" });
1578
+ throw new Error("Phone OTP guard could not be prepared.");
1579
+ }
1580
+ const current = selected?.data ?? guard;
1581
+ if (selected?.data) setGuardState({ status: "ready", data: selected.data });
1582
+ if (current?.kind === "akedly_shield_v1_2") {
1583
+ const akedlyShield = await (0, import_core4.solvePhoneOtpChallenge)(current);
1584
+ return startPhoneOtp({ phoneNumber, akedlyShield, idempotencyKey });
1585
+ }
1586
+ if (current?.kind === "authowl_turnstile" && turnstileToken) {
1587
+ return startPhoneOtp({ phoneNumber, turnstileToken, idempotencyKey });
1588
+ }
1589
+ throw new Error("Phone OTP guard is unavailable.");
1590
+ },
1532
1591
  {
1533
1592
  failure: t("phoneOtp.error.sendFailed"),
1534
1593
  onSuccess: () => setStage("code")
@@ -1565,7 +1624,7 @@ function PhoneOTP({ redirectTo, onSignedIn, onBack }) {
1565
1624
  testId: "phoneotp-consent"
1566
1625
  }
1567
1626
  ),
1568
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1627
+ guard?.kind === "authowl_turnstile" && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1569
1628
  Turnstile,
1570
1629
  {
1571
1630
  siteKey: config?.turnstileSiteKey ?? null,
@@ -1575,12 +1634,23 @@ function PhoneOTP({ redirectTo, onSignedIn, onBack }) {
1575
1634
  }
1576
1635
  ),
1577
1636
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(FormError, { children: error }),
1637
+ guardState.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1638
+ "button",
1639
+ {
1640
+ type: "button",
1641
+ className: "ba-link-button",
1642
+ onClick: () => {
1643
+ void loadGuard();
1644
+ },
1645
+ children: t("phoneOtp.retryHumanCheck")
1646
+ }
1647
+ ),
1578
1648
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1579
1649
  "button",
1580
1650
  {
1581
1651
  className: "ba-button",
1582
1652
  type: "submit",
1583
- disabled: pending || !turnstileToken || consentBlocked,
1653
+ disabled: pending || guardState.status !== "ready" || turnstileBlocked || consentBlocked,
1584
1654
  "aria-busy": pending || void 0,
1585
1655
  children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Busy, { busy: pending, label: t("common.sending"), children: t("phoneOtp.sendSubmit") })
1586
1656
  }
package/dist/index.d.cts CHANGED
@@ -167,6 +167,8 @@ type UseSignInResult = {
167
167
  /** Passwordless: complete sign-in with the emailed code. */
168
168
  signInEmailOtp: AuthOwlClient['signIn']['emailOtp'];
169
169
  /** Managed SMS: request a phone verification code. */
170
+ preparePhoneOtp: AuthOwlClient['phoneOtp']['prepare'];
171
+ /** Managed SMS: request a phone verification code. */
170
172
  startPhoneOtp: AuthOwlClient['phoneOtp']['start'];
171
173
  /** Managed SMS: verify the phone code and establish a session. */
172
174
  verifyPhoneOtp: AuthOwlClient['phoneOtp']['verify'];
package/dist/index.d.ts CHANGED
@@ -167,6 +167,8 @@ type UseSignInResult = {
167
167
  /** Passwordless: complete sign-in with the emailed code. */
168
168
  signInEmailOtp: AuthOwlClient['signIn']['emailOtp'];
169
169
  /** Managed SMS: request a phone verification code. */
170
+ preparePhoneOtp: AuthOwlClient['phoneOtp']['prepare'];
171
+ /** Managed SMS: request a phone verification code. */
170
172
  startPhoneOtp: AuthOwlClient['phoneOtp']['start'];
171
173
  /** Managed SMS: verify the phone code and establish a session. */
172
174
  verifyPhoneOtp: AuthOwlClient['phoneOtp']['verify'];
package/dist/index.js CHANGED
@@ -436,6 +436,7 @@ function useSignIn() {
436
436
  signInPasskey: client.signIn.passkey,
437
437
  sendEmailOtp: client.emailOtp.sendVerificationOtp,
438
438
  signInEmailOtp: client.signIn.emailOtp,
439
+ preparePhoneOtp: client.phoneOtp.prepare,
439
440
  startPhoneOtp: client.phoneOtp.start,
440
441
  verifyPhoneOtp: client.phoneOtp.verify
441
442
  };
@@ -798,6 +799,14 @@ function SocialButtons({ providers, callbackURL }) {
798
799
  const { signInSocial } = useSignIn();
799
800
  const [pending, setPending] = React6.useState(null);
800
801
  const [error, setError] = React6.useState(null);
802
+ React6.useEffect(() => {
803
+ if (typeof window === "undefined") return;
804
+ const url = new URL(window.location.href);
805
+ if (!url.searchParams.has("authowl_error")) return;
806
+ url.searchParams.delete("authowl_error");
807
+ window.history.replaceState(window.history.state, "", url.toString());
808
+ setError(t("social.error.startFailed"));
809
+ }, [t]);
801
810
  if (providers.length === 0) return null;
802
811
  return /* @__PURE__ */ jsxs3("div", { className: "ba-social", children: [
803
812
  providers.map((provider) => /* @__PURE__ */ jsx6(
@@ -812,7 +821,11 @@ function SocialButtons({ providers, callbackURL }) {
812
821
  setPending(provider);
813
822
  try {
814
823
  const destination = callbackURL ?? window.location.href;
815
- const res = await signInSocial({ provider, callbackURL: destination });
824
+ const res = await signInSocial({
825
+ provider,
826
+ callbackURL: destination,
827
+ errorCallbackURL: window.location.href
828
+ });
816
829
  if (res?.error) {
817
830
  setError(toServerError(res.error, t("social.error.startFailed")));
818
831
  setPending(null);
@@ -1287,7 +1300,10 @@ function AuthOwlBadge({ href = "https://authowl.dev", force } = {}) {
1287
1300
 
1288
1301
  // src/components/PhoneOTP.tsx
1289
1302
  import * as React11 from "react";
1290
- import { createIdempotencyKey } from "@authowl/core";
1303
+ import {
1304
+ createIdempotencyKey,
1305
+ solvePhoneOtpChallenge
1306
+ } from "@authowl/core";
1291
1307
 
1292
1308
  // src/components/ConsentDocLinks.tsx
1293
1309
  import { Fragment as Fragment4, jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
@@ -1336,16 +1352,46 @@ function PhoneOTP({ redirectTo, onSignedIn, onBack }) {
1336
1352
  const t = useT();
1337
1353
  const { sessionStore } = useAuthClient();
1338
1354
  const { config, isLoading } = usePublicConfig();
1339
- const { startPhoneOtp, verifyPhoneOtp } = useSignIn();
1355
+ const { preparePhoneOtp, startPhoneOtp, verifyPhoneOtp } = useSignIn();
1340
1356
  const { pending, error, setError, run } = useSubmitAction();
1341
1357
  const [stage, setStage] = React11.useState("phone");
1342
1358
  const [phoneNumber, setPhoneNumber] = React11.useState("");
1343
1359
  const [code, setCode] = React11.useState("");
1344
1360
  const [turnstileToken, setTurnstileToken] = React11.useState(null);
1361
+ const [guardState, setGuardState] = React11.useState({ status: "loading" });
1345
1362
  const [accepted, setAccepted] = React11.useState(false);
1346
1363
  const attempt = React11.useRef(null);
1364
+ const guardRequest = React11.useRef(0);
1347
1365
  const legal = config?.legal;
1348
1366
  const consentBlocked = Boolean(legal?.required && !accepted);
1367
+ const guard = guardState.status === "ready" ? guardState.data : null;
1368
+ const turnstileBlocked = guardState.status === "ready" && guardState.data.kind === "authowl_turnstile" && !turnstileToken;
1369
+ const humanCheckError = t("phoneOtp.error.humanCheck");
1370
+ const loadGuard = React11.useCallback(async () => {
1371
+ const request = ++guardRequest.current;
1372
+ setGuardState({ status: "loading" });
1373
+ setError(null);
1374
+ try {
1375
+ const result = await preparePhoneOtp();
1376
+ if (request !== guardRequest.current) return;
1377
+ if (result.error || !result.data) {
1378
+ setGuardState({ status: "error" });
1379
+ setError(humanCheckError);
1380
+ return;
1381
+ }
1382
+ setGuardState({ status: "ready", data: result.data });
1383
+ } catch {
1384
+ if (request !== guardRequest.current) return;
1385
+ setGuardState({ status: "error" });
1386
+ setError(humanCheckError);
1387
+ }
1388
+ }, [humanCheckError, preparePhoneOtp, setError]);
1389
+ React11.useEffect(() => {
1390
+ void loadGuard();
1391
+ return () => {
1392
+ guardRequest.current += 1;
1393
+ };
1394
+ }, [loadGuard]);
1349
1395
  if (isLoading) {
1350
1396
  return /* @__PURE__ */ jsxs12("div", { className: "ba-fields", "data-testid": "phoneotp-loading", "aria-busy": "true", children: [
1351
1397
  /* @__PURE__ */ jsx16("div", { className: "ba-skeleton" }),
@@ -1438,7 +1484,7 @@ function PhoneOTP({ redirectTo, onSignedIn, onBack }) {
1438
1484
  setError(t("signUp.error.consentRequired"));
1439
1485
  return;
1440
1486
  }
1441
- if (!turnstileToken) {
1487
+ if (!guard || guard.kind === "authowl_turnstile" && !turnstileToken) {
1442
1488
  setError(t("phoneOtp.error.humanCheck"));
1443
1489
  return;
1444
1490
  }
@@ -1447,7 +1493,23 @@ function PhoneOTP({ redirectTo, onSignedIn, onBack }) {
1447
1493
  }
1448
1494
  const idempotencyKey = attempt.current.idempotencyKey;
1449
1495
  void run(
1450
- () => startPhoneOtp({ phoneNumber, turnstileToken, idempotencyKey }),
1496
+ async () => {
1497
+ const selected = guard?.kind === "akedly_shield_v1_2" ? await preparePhoneOtp() : null;
1498
+ if (selected?.error || selected && !selected.data) {
1499
+ setGuardState({ status: "error" });
1500
+ throw new Error("Phone OTP guard could not be prepared.");
1501
+ }
1502
+ const current = selected?.data ?? guard;
1503
+ if (selected?.data) setGuardState({ status: "ready", data: selected.data });
1504
+ if (current?.kind === "akedly_shield_v1_2") {
1505
+ const akedlyShield = await solvePhoneOtpChallenge(current);
1506
+ return startPhoneOtp({ phoneNumber, akedlyShield, idempotencyKey });
1507
+ }
1508
+ if (current?.kind === "authowl_turnstile" && turnstileToken) {
1509
+ return startPhoneOtp({ phoneNumber, turnstileToken, idempotencyKey });
1510
+ }
1511
+ throw new Error("Phone OTP guard is unavailable.");
1512
+ },
1451
1513
  {
1452
1514
  failure: t("phoneOtp.error.sendFailed"),
1453
1515
  onSuccess: () => setStage("code")
@@ -1484,7 +1546,7 @@ function PhoneOTP({ redirectTo, onSignedIn, onBack }) {
1484
1546
  testId: "phoneotp-consent"
1485
1547
  }
1486
1548
  ),
1487
- /* @__PURE__ */ jsx16(
1549
+ guard?.kind === "authowl_turnstile" && /* @__PURE__ */ jsx16(
1488
1550
  Turnstile,
1489
1551
  {
1490
1552
  siteKey: config?.turnstileSiteKey ?? null,
@@ -1494,12 +1556,23 @@ function PhoneOTP({ redirectTo, onSignedIn, onBack }) {
1494
1556
  }
1495
1557
  ),
1496
1558
  /* @__PURE__ */ jsx16(FormError, { children: error }),
1559
+ guardState.status === "error" && /* @__PURE__ */ jsx16(
1560
+ "button",
1561
+ {
1562
+ type: "button",
1563
+ className: "ba-link-button",
1564
+ onClick: () => {
1565
+ void loadGuard();
1566
+ },
1567
+ children: t("phoneOtp.retryHumanCheck")
1568
+ }
1569
+ ),
1497
1570
  /* @__PURE__ */ jsx16(
1498
1571
  "button",
1499
1572
  {
1500
1573
  className: "ba-button",
1501
1574
  type: "submit",
1502
- disabled: pending || !turnstileToken || consentBlocked,
1575
+ disabled: pending || guardState.status !== "ready" || turnstileBlocked || consentBlocked,
1503
1576
  "aria-busy": pending || void 0,
1504
1577
  children: /* @__PURE__ */ jsx16(Busy, { busy: pending, label: t("common.sending"), children: t("phoneOtp.sendSubmit") })
1505
1578
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@authowl/react",
3
- "version": "0.16.1",
3
+ "version": "0.17.1",
4
4
  "description": "React provider, hooks, and drop-in components for AuthOwl, the multi-tenant auth SaaS.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -47,7 +47,7 @@
47
47
  "LICENSE"
48
48
  ],
49
49
  "dependencies": {
50
- "@authowl/core": "0.12.0"
50
+ "@authowl/core": "0.14.0"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "react": ">=18",