@authowl/react 0.16.1 → 0.17.0

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
  };
@@ -1417,16 +1418,46 @@ function PhoneOTP({ redirectTo, onSignedIn, onBack }) {
1417
1418
  const t = useT();
1418
1419
  const { sessionStore } = useAuthClient();
1419
1420
  const { config, isLoading } = usePublicConfig();
1420
- const { startPhoneOtp, verifyPhoneOtp } = useSignIn();
1421
+ const { preparePhoneOtp, startPhoneOtp, verifyPhoneOtp } = useSignIn();
1421
1422
  const { pending, error, setError, run } = useSubmitAction();
1422
1423
  const [stage, setStage] = React11.useState("phone");
1423
1424
  const [phoneNumber, setPhoneNumber] = React11.useState("");
1424
1425
  const [code, setCode] = React11.useState("");
1425
1426
  const [turnstileToken, setTurnstileToken] = React11.useState(null);
1427
+ const [guardState, setGuardState] = React11.useState({ status: "loading" });
1426
1428
  const [accepted, setAccepted] = React11.useState(false);
1427
1429
  const attempt = React11.useRef(null);
1430
+ const guardRequest = React11.useRef(0);
1428
1431
  const legal = config?.legal;
1429
1432
  const consentBlocked = Boolean(legal?.required && !accepted);
1433
+ const guard = guardState.status === "ready" ? guardState.data : null;
1434
+ const turnstileBlocked = guardState.status === "ready" && guardState.data.kind === "authowl_turnstile" && !turnstileToken;
1435
+ const humanCheckError = t("phoneOtp.error.humanCheck");
1436
+ const loadGuard = React11.useCallback(async () => {
1437
+ const request = ++guardRequest.current;
1438
+ setGuardState({ status: "loading" });
1439
+ setError(null);
1440
+ try {
1441
+ const result = await preparePhoneOtp();
1442
+ if (request !== guardRequest.current) return;
1443
+ if (result.error || !result.data) {
1444
+ setGuardState({ status: "error" });
1445
+ setError(humanCheckError);
1446
+ return;
1447
+ }
1448
+ setGuardState({ status: "ready", data: result.data });
1449
+ } catch {
1450
+ if (request !== guardRequest.current) return;
1451
+ setGuardState({ status: "error" });
1452
+ setError(humanCheckError);
1453
+ }
1454
+ }, [humanCheckError, preparePhoneOtp, setError]);
1455
+ React11.useEffect(() => {
1456
+ void loadGuard();
1457
+ return () => {
1458
+ guardRequest.current += 1;
1459
+ };
1460
+ }, [loadGuard]);
1430
1461
  if (isLoading) {
1431
1462
  return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "ba-fields", "data-testid": "phoneotp-loading", "aria-busy": "true", children: [
1432
1463
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "ba-skeleton" }),
@@ -1519,7 +1550,7 @@ function PhoneOTP({ redirectTo, onSignedIn, onBack }) {
1519
1550
  setError(t("signUp.error.consentRequired"));
1520
1551
  return;
1521
1552
  }
1522
- if (!turnstileToken) {
1553
+ if (!guard || guard.kind === "authowl_turnstile" && !turnstileToken) {
1523
1554
  setError(t("phoneOtp.error.humanCheck"));
1524
1555
  return;
1525
1556
  }
@@ -1528,7 +1559,23 @@ function PhoneOTP({ redirectTo, onSignedIn, onBack }) {
1528
1559
  }
1529
1560
  const idempotencyKey = attempt.current.idempotencyKey;
1530
1561
  void run(
1531
- () => startPhoneOtp({ phoneNumber, turnstileToken, idempotencyKey }),
1562
+ async () => {
1563
+ const selected = guard?.kind === "akedly_shield_v1_2" ? await preparePhoneOtp() : null;
1564
+ if (selected?.error || selected && !selected.data) {
1565
+ setGuardState({ status: "error" });
1566
+ throw new Error("Phone OTP guard could not be prepared.");
1567
+ }
1568
+ const current = selected?.data ?? guard;
1569
+ if (selected?.data) setGuardState({ status: "ready", data: selected.data });
1570
+ if (current?.kind === "akedly_shield_v1_2") {
1571
+ const akedlyShield = await (0, import_core4.solvePhoneOtpChallenge)(current);
1572
+ return startPhoneOtp({ phoneNumber, akedlyShield, idempotencyKey });
1573
+ }
1574
+ if (current?.kind === "authowl_turnstile" && turnstileToken) {
1575
+ return startPhoneOtp({ phoneNumber, turnstileToken, idempotencyKey });
1576
+ }
1577
+ throw new Error("Phone OTP guard is unavailable.");
1578
+ },
1532
1579
  {
1533
1580
  failure: t("phoneOtp.error.sendFailed"),
1534
1581
  onSuccess: () => setStage("code")
@@ -1565,7 +1612,7 @@ function PhoneOTP({ redirectTo, onSignedIn, onBack }) {
1565
1612
  testId: "phoneotp-consent"
1566
1613
  }
1567
1614
  ),
1568
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1615
+ guard?.kind === "authowl_turnstile" && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1569
1616
  Turnstile,
1570
1617
  {
1571
1618
  siteKey: config?.turnstileSiteKey ?? null,
@@ -1575,12 +1622,23 @@ function PhoneOTP({ redirectTo, onSignedIn, onBack }) {
1575
1622
  }
1576
1623
  ),
1577
1624
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(FormError, { children: error }),
1625
+ guardState.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1626
+ "button",
1627
+ {
1628
+ type: "button",
1629
+ className: "ba-link-button",
1630
+ onClick: () => {
1631
+ void loadGuard();
1632
+ },
1633
+ children: t("phoneOtp.retryHumanCheck")
1634
+ }
1635
+ ),
1578
1636
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1579
1637
  "button",
1580
1638
  {
1581
1639
  className: "ba-button",
1582
1640
  type: "submit",
1583
- disabled: pending || !turnstileToken || consentBlocked,
1641
+ disabled: pending || guardState.status !== "ready" || turnstileBlocked || consentBlocked,
1584
1642
  "aria-busy": pending || void 0,
1585
1643
  children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Busy, { busy: pending, label: t("common.sending"), children: t("phoneOtp.sendSubmit") })
1586
1644
  }
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
  };
@@ -1287,7 +1288,10 @@ function AuthOwlBadge({ href = "https://authowl.dev", force } = {}) {
1287
1288
 
1288
1289
  // src/components/PhoneOTP.tsx
1289
1290
  import * as React11 from "react";
1290
- import { createIdempotencyKey } from "@authowl/core";
1291
+ import {
1292
+ createIdempotencyKey,
1293
+ solvePhoneOtpChallenge
1294
+ } from "@authowl/core";
1291
1295
 
1292
1296
  // src/components/ConsentDocLinks.tsx
1293
1297
  import { Fragment as Fragment4, jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
@@ -1336,16 +1340,46 @@ function PhoneOTP({ redirectTo, onSignedIn, onBack }) {
1336
1340
  const t = useT();
1337
1341
  const { sessionStore } = useAuthClient();
1338
1342
  const { config, isLoading } = usePublicConfig();
1339
- const { startPhoneOtp, verifyPhoneOtp } = useSignIn();
1343
+ const { preparePhoneOtp, startPhoneOtp, verifyPhoneOtp } = useSignIn();
1340
1344
  const { pending, error, setError, run } = useSubmitAction();
1341
1345
  const [stage, setStage] = React11.useState("phone");
1342
1346
  const [phoneNumber, setPhoneNumber] = React11.useState("");
1343
1347
  const [code, setCode] = React11.useState("");
1344
1348
  const [turnstileToken, setTurnstileToken] = React11.useState(null);
1349
+ const [guardState, setGuardState] = React11.useState({ status: "loading" });
1345
1350
  const [accepted, setAccepted] = React11.useState(false);
1346
1351
  const attempt = React11.useRef(null);
1352
+ const guardRequest = React11.useRef(0);
1347
1353
  const legal = config?.legal;
1348
1354
  const consentBlocked = Boolean(legal?.required && !accepted);
1355
+ const guard = guardState.status === "ready" ? guardState.data : null;
1356
+ const turnstileBlocked = guardState.status === "ready" && guardState.data.kind === "authowl_turnstile" && !turnstileToken;
1357
+ const humanCheckError = t("phoneOtp.error.humanCheck");
1358
+ const loadGuard = React11.useCallback(async () => {
1359
+ const request = ++guardRequest.current;
1360
+ setGuardState({ status: "loading" });
1361
+ setError(null);
1362
+ try {
1363
+ const result = await preparePhoneOtp();
1364
+ if (request !== guardRequest.current) return;
1365
+ if (result.error || !result.data) {
1366
+ setGuardState({ status: "error" });
1367
+ setError(humanCheckError);
1368
+ return;
1369
+ }
1370
+ setGuardState({ status: "ready", data: result.data });
1371
+ } catch {
1372
+ if (request !== guardRequest.current) return;
1373
+ setGuardState({ status: "error" });
1374
+ setError(humanCheckError);
1375
+ }
1376
+ }, [humanCheckError, preparePhoneOtp, setError]);
1377
+ React11.useEffect(() => {
1378
+ void loadGuard();
1379
+ return () => {
1380
+ guardRequest.current += 1;
1381
+ };
1382
+ }, [loadGuard]);
1349
1383
  if (isLoading) {
1350
1384
  return /* @__PURE__ */ jsxs12("div", { className: "ba-fields", "data-testid": "phoneotp-loading", "aria-busy": "true", children: [
1351
1385
  /* @__PURE__ */ jsx16("div", { className: "ba-skeleton" }),
@@ -1438,7 +1472,7 @@ function PhoneOTP({ redirectTo, onSignedIn, onBack }) {
1438
1472
  setError(t("signUp.error.consentRequired"));
1439
1473
  return;
1440
1474
  }
1441
- if (!turnstileToken) {
1475
+ if (!guard || guard.kind === "authowl_turnstile" && !turnstileToken) {
1442
1476
  setError(t("phoneOtp.error.humanCheck"));
1443
1477
  return;
1444
1478
  }
@@ -1447,7 +1481,23 @@ function PhoneOTP({ redirectTo, onSignedIn, onBack }) {
1447
1481
  }
1448
1482
  const idempotencyKey = attempt.current.idempotencyKey;
1449
1483
  void run(
1450
- () => startPhoneOtp({ phoneNumber, turnstileToken, idempotencyKey }),
1484
+ async () => {
1485
+ const selected = guard?.kind === "akedly_shield_v1_2" ? await preparePhoneOtp() : null;
1486
+ if (selected?.error || selected && !selected.data) {
1487
+ setGuardState({ status: "error" });
1488
+ throw new Error("Phone OTP guard could not be prepared.");
1489
+ }
1490
+ const current = selected?.data ?? guard;
1491
+ if (selected?.data) setGuardState({ status: "ready", data: selected.data });
1492
+ if (current?.kind === "akedly_shield_v1_2") {
1493
+ const akedlyShield = await solvePhoneOtpChallenge(current);
1494
+ return startPhoneOtp({ phoneNumber, akedlyShield, idempotencyKey });
1495
+ }
1496
+ if (current?.kind === "authowl_turnstile" && turnstileToken) {
1497
+ return startPhoneOtp({ phoneNumber, turnstileToken, idempotencyKey });
1498
+ }
1499
+ throw new Error("Phone OTP guard is unavailable.");
1500
+ },
1451
1501
  {
1452
1502
  failure: t("phoneOtp.error.sendFailed"),
1453
1503
  onSuccess: () => setStage("code")
@@ -1484,7 +1534,7 @@ function PhoneOTP({ redirectTo, onSignedIn, onBack }) {
1484
1534
  testId: "phoneotp-consent"
1485
1535
  }
1486
1536
  ),
1487
- /* @__PURE__ */ jsx16(
1537
+ guard?.kind === "authowl_turnstile" && /* @__PURE__ */ jsx16(
1488
1538
  Turnstile,
1489
1539
  {
1490
1540
  siteKey: config?.turnstileSiteKey ?? null,
@@ -1494,12 +1544,23 @@ function PhoneOTP({ redirectTo, onSignedIn, onBack }) {
1494
1544
  }
1495
1545
  ),
1496
1546
  /* @__PURE__ */ jsx16(FormError, { children: error }),
1547
+ guardState.status === "error" && /* @__PURE__ */ jsx16(
1548
+ "button",
1549
+ {
1550
+ type: "button",
1551
+ className: "ba-link-button",
1552
+ onClick: () => {
1553
+ void loadGuard();
1554
+ },
1555
+ children: t("phoneOtp.retryHumanCheck")
1556
+ }
1557
+ ),
1497
1558
  /* @__PURE__ */ jsx16(
1498
1559
  "button",
1499
1560
  {
1500
1561
  className: "ba-button",
1501
1562
  type: "submit",
1502
- disabled: pending || !turnstileToken || consentBlocked,
1563
+ disabled: pending || guardState.status !== "ready" || turnstileBlocked || consentBlocked,
1503
1564
  "aria-busy": pending || void 0,
1504
1565
  children: /* @__PURE__ */ jsx16(Busy, { busy: pending, label: t("common.sending"), children: t("phoneOtp.sendSubmit") })
1505
1566
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@authowl/react",
3
- "version": "0.16.1",
3
+ "version": "0.17.0",
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.13.0"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "react": ">=18",