@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 +9 -0
- package/dist/index.cjs +63 -5
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +67 -6
- package/package.json +2 -2
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
|
-
() =>
|
|
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 ||
|
|
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 {
|
|
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
|
-
() =>
|
|
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 ||
|
|
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.
|
|
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.
|
|
50
|
+
"@authowl/core": "0.13.0"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
53
|
"react": ">=18",
|