@circuitwall/jarela 1.3.0 → 1.4.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.
Files changed (102) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  5. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  6. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  12. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  14. package/.next/standalone/.next/server/app/_not-found.html +2 -2
  15. package/.next/standalone/.next/server/app/_not-found.rsc +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  20. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  21. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  22. package/.next/standalone/.next/server/app/api/v1/builtin-tools/route.js +10 -1
  23. package/.next/standalone/.next/server/app/api/v1/builtin-tools/route.js.map +1 -1
  24. package/.next/standalone/.next/server/app/api/v1/dashboard/currency/route.js +10 -5
  25. package/.next/standalone/.next/server/app/api/v1/dashboard/currency/route.js.map +1 -1
  26. package/.next/standalone/.next/server/app/api/v1/page-capture/route.js +37 -3
  27. package/.next/standalone/.next/server/app/api/v1/page-capture/route.js.map +1 -1
  28. package/.next/standalone/.next/server/app/api/v1/providers/[provider]/probe/route.js +9 -1
  29. package/.next/standalone/.next/server/app/api/v1/providers/[provider]/probe/route.js.map +1 -1
  30. package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/run/route.js +33 -8
  31. package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/run/route.js.map +1 -1
  32. package/.next/standalone/.next/server/app/page.js +73 -204
  33. package/.next/standalone/.next/server/app/page.js.map +1 -1
  34. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  36. package/.next/standalone/.next/server/app/setup/page.js +1 -1
  37. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  39. package/.next/standalone/.next/server/chunks/1718.js +159 -0
  40. package/.next/standalone/.next/server/chunks/1718.js.map +1 -0
  41. package/.next/standalone/.next/server/chunks/2082.js +6 -3
  42. package/.next/standalone/.next/server/chunks/2082.js.map +1 -1
  43. package/.next/standalone/.next/server/chunks/210.js +28 -0
  44. package/.next/standalone/.next/server/chunks/210.js.map +1 -1
  45. package/.next/standalone/.next/server/chunks/423.js +6 -3
  46. package/.next/standalone/.next/server/chunks/423.js.map +1 -1
  47. package/.next/standalone/.next/server/chunks/4631.js +37 -5
  48. package/.next/standalone/.next/server/chunks/4631.js.map +1 -1
  49. package/.next/standalone/.next/server/chunks/8167.js +255 -204
  50. package/.next/standalone/.next/server/chunks/8167.js.map +1 -1
  51. package/.next/standalone/.next/server/chunks/8866.js +38 -5
  52. package/.next/standalone/.next/server/chunks/8866.js.map +1 -1
  53. package/.next/standalone/.next/server/chunks/9032.js +8 -0
  54. package/.next/standalone/.next/server/chunks/9032.js.map +1 -1
  55. package/.next/standalone/.next/server/chunks/{7883.js → 9557.js} +15 -3
  56. package/.next/standalone/.next/server/chunks/9557.js.map +1 -0
  57. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  58. package/.next/standalone/.next/server/middleware.js +6 -3
  59. package/.next/standalone/.next/server/pages/404.html +2 -2
  60. package/.next/standalone/.next/server/pages/500.html +1 -1
  61. package/.next/standalone/.next/server/proxy.js.map +1 -1
  62. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  63. package/.next/standalone/.next/static/chunks/{2351-68d8987bbe17ba2d.js → 2351-1ab119fb3b48f4c9.js} +258 -205
  64. package/.next/standalone/.next/static/chunks/2351-1ab119fb3b48f4c9.js.map +1 -0
  65. package/.next/standalone/.next/static/chunks/{9209-0d46118e502f8bf5.js → 4097-64691f9110cf167c.js} +14 -2
  66. package/.next/standalone/.next/static/chunks/4097-64691f9110cf167c.js.map +1 -0
  67. package/.next/standalone/.next/static/chunks/app/{page-2ab710949b62a638.js → page-145150e0468544e7.js} +74 -205
  68. package/.next/standalone/.next/static/chunks/app/page-145150e0468544e7.js.map +1 -0
  69. package/.next/standalone/.next/static/chunks/app/setup/{page-9a465b5fa755b3c3.js → page-a1463a9ace439ff7.js} +2 -2
  70. package/.next/standalone/.next/static/chunks/app/setup/{page-9a465b5fa755b3c3.js.map → page-a1463a9ace439ff7.js.map} +1 -1
  71. package/.next/standalone/.next/static/chunks/{webpack-ff5627013a5e3842.js → webpack-f4ac5c5f92cfd1c1.js} +13 -1
  72. package/.next/standalone/.next/static/chunks/webpack-f4ac5c5f92cfd1c1.js.map +1 -0
  73. package/.next/standalone/package.json +2 -1
  74. package/CHANGELOG.md +84 -0
  75. package/README.md +51 -26
  76. package/api/client.ts +10 -9
  77. package/app/api/v1/dashboard/currency/route.ts +7 -2
  78. package/app/api/v1/providers/[provider]/probe/route.ts +12 -1
  79. package/app/api/v1/threads/[thread_id]/run/route.ts +22 -8
  80. package/components/chat/InputBar.tsx +10 -1
  81. package/components/layout/AppShell.tsx +53 -17
  82. package/components/setup/PinKeypad.tsx +238 -0
  83. package/components/setup/ScreenLock.tsx +8 -173
  84. package/components/setup/UnlockScreen.tsx +25 -192
  85. package/lib/api/page-capture.test.ts +58 -0
  86. package/lib/api/page-capture.ts +31 -1
  87. package/lib/documents/remote/github.ts +16 -2
  88. package/lib/documents/remote/mail.ts +11 -2
  89. package/lib/lifecycle/shutdown.ts +9 -0
  90. package/lib/providers/github-copilot-auth.ts +2 -0
  91. package/lib/providers/github-copilot.ts +1 -0
  92. package/lib/tools/async-results.ts +11 -0
  93. package/package.json +2 -1
  94. package/scripts/install-to-system.ps1 +2 -2
  95. package/scripts/installed-launcher.ps1 +81 -17
  96. package/.next/standalone/.next/server/chunks/7883.js.map +0 -1
  97. package/.next/standalone/.next/static/chunks/2351-68d8987bbe17ba2d.js.map +0 -1
  98. package/.next/standalone/.next/static/chunks/9209-0d46118e502f8bf5.js.map +0 -1
  99. package/.next/standalone/.next/static/chunks/app/page-2ab710949b62a638.js.map +0 -1
  100. package/.next/standalone/.next/static/chunks/webpack-ff5627013a5e3842.js.map +0 -1
  101. /package/.next/standalone/.next/static/{ZKy7LJ3KXj2TIyKOg_fBH → WQdcnm9NyqpeNc0Z8_woo}/_buildManifest.js +0 -0
  102. /package/.next/standalone/.next/static/{ZKy7LJ3KXj2TIyKOg_fBH → WQdcnm9NyqpeNc0Z8_woo}/_ssgManifest.js +0 -0
@@ -141,14 +141,14 @@ async function request(path, init) {
141
141
  signal: timeoutCtrl.signal
142
142
  });
143
143
  if (!res.ok) {
144
- // 423 screen-locked: surface a window event so the AppShell can
145
- // mount the ScreenLock overlay. We still throw so the caller
146
- // sees the failure, but no point retrying the lock isn't
147
- // going to clear on its own.
144
+ // 423 lock states: distinct events so AppShell can mount the
145
+ // right overlay (decrypt vs presence-check). Both throw so the
146
+ // caller still sees the failure no point retrying, the lock
147
+ // isn't going to clear on its own.
148
148
  if (res.status === 423) {
149
149
  const cloned = res.clone();
150
150
  const body = await cloned.json().catch(()=>null);
151
- if (body?.error === "screen-locked" && "undefined" !== "undefined") {}
151
+ if (false) {}
152
152
  throw new Error(`423 ${body?.error ?? "locked"}`);
153
153
  }
154
154
  const text = await res.text().catch(()=>res.statusText);
@@ -926,210 +926,26 @@ const api = {
926
926
  /* harmony export */ });
927
927
  /* harmony import */ var react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(48249);
928
928
  /* harmony import */ var react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__);
929
- /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(67484);
930
- /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_1__);
931
- /* harmony import */ var _components_ui_Logo__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(41357);
929
+ /* harmony import */ var next_navigation__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(19099);
930
+ /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(67484);
931
+ /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_2__);
932
+ /* harmony import */ var _PinKeypad__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(71654);
932
933
  /* __next_internal_client_entry_do_not_use__ UnlockScreen auto */
933
934
 
934
935
 
935
- // PIN unlock splash for ADR-0063 PIN-wrapped keyfiles. Rendered by the
936
- // root route when the server detects the master key is locked. Submits
937
- // the 6-digit PIN to /api/v1/security/unlock; on success, reloads so
938
- // the server re-renders into the normal authenticated tree.
939
- const PIN_LENGTH = 6;
940
- function UnlockScreen() {
941
- const [digits, setDigits] = (0,react__WEBPACK_IMPORTED_MODULE_1__.useState)("");
942
- const [error, setError] = (0,react__WEBPACK_IMPORTED_MODULE_1__.useState)(null);
943
- const [submitting, setSubmitting] = (0,react__WEBPACK_IMPORTED_MODULE_1__.useState)(false);
944
- const [retryAfterSec, setRetryAfterSec] = (0,react__WEBPACK_IMPORTED_MODULE_1__.useState)(0);
945
- const containerRef = (0,react__WEBPACK_IMPORTED_MODULE_1__.useRef)(null);
946
- // Hard guard against parallel submits. setState updaters can run more
947
- // than once (dev StrictMode, concurrent rendering), so if `submit`
948
- // were called from inside `setDigits` we'd POST twice and the second
949
- // request would race the first into `unlockMasterKey()` after state
950
- // already flipped to unlocked - the route would 500.
951
- const submittingRef = (0,react__WEBPACK_IMPORTED_MODULE_1__.useRef)(false);
952
- const submit = (0,react__WEBPACK_IMPORTED_MODULE_1__.useCallback)(async (pin)=>{
953
- if (submittingRef.current) return;
954
- submittingRef.current = true;
955
- setSubmitting(true);
956
- setError(null);
957
- try {
958
- const res = await fetch("/api/v1/security/unlock", {
959
- method: "POST",
960
- headers: {
961
- "content-type": "application/json"
962
- },
963
- body: JSON.stringify({
964
- pin
965
- })
966
- });
967
- if (res.ok) {
968
- window.location.reload();
969
- return;
970
- }
971
- const body = await res.json().catch(()=>({}));
972
- if (res.status === 409 && body.error === "not-locked") {
973
- // Master key was already unlocked (host typed the PIN, or
974
- // another tab beat us to it). The goal state is reached —
975
- // reload into the app shell.
976
- window.location.reload();
977
- return;
978
- }
979
- if (res.status === 429 && typeof body.retry_after_ms === "number") {
980
- setRetryAfterSec(Math.ceil(body.retry_after_ms / 1000));
981
- setError("Too many attempts. Try again later.");
982
- } else if (res.status === 401) {
983
- setError("Wrong PIN. Try again.");
984
- } else if (res.status === 400) {
985
- setError("Invalid PIN format.");
986
- } else {
987
- setError(body.error ?? `Error (${res.status})`);
988
- }
989
- setDigits("");
990
- } catch (err) {
991
- setError(err instanceof Error ? err.message : String(err));
992
- setDigits("");
993
- } finally{
994
- submittingRef.current = false;
995
- setSubmitting(false);
996
- }
997
- }, []);
998
- const append = (0,react__WEBPACK_IMPORTED_MODULE_1__.useCallback)((d)=>{
999
- if (submitting || retryAfterSec > 0) return;
1000
- setError(null);
1001
- setDigits((cur)=>cur.length >= PIN_LENGTH ? cur : cur + d);
1002
- }, [
1003
- submitting,
1004
- retryAfterSec
1005
- ]);
1006
- // Auto-submit once the buffer hits 6 digits. Effect runs once per
1007
- // state transition (not per updater invocation), so we POST exactly
1008
- // one time even under StrictMode double-render.
1009
- process.env.__NEXT_PRIVATE_MINIMIZE_MACRO_FALSE && (0,react__WEBPACK_IMPORTED_MODULE_1__.useEffect)(()=>{
1010
- if (digits.length === PIN_LENGTH && !submittingRef.current) {
1011
- void submit(digits);
1012
- }
1013
- }, [
1014
- digits,
1015
- submit
1016
- ]);
1017
- const backspace = (0,react__WEBPACK_IMPORTED_MODULE_1__.useCallback)(()=>{
1018
- if (submitting) return;
1019
- setError(null);
1020
- setDigits((cur)=>cur.slice(0, -1));
1021
- }, [
1022
- submitting
1023
- ]);
1024
- // Physical keyboard support.
1025
- process.env.__NEXT_PRIVATE_MINIMIZE_MACRO_FALSE && (0,react__WEBPACK_IMPORTED_MODULE_1__.useEffect)(()=>{
1026
- function onKey(e) {
1027
- if (/^[0-9]$/.test(e.key)) {
1028
- e.preventDefault();
1029
- append(e.key);
1030
- } else if (e.key === "Backspace") {
1031
- e.preventDefault();
1032
- backspace();
1033
- }
1034
- }
1035
- window.addEventListener("keydown", onKey);
1036
- return ()=>window.removeEventListener("keydown", onKey);
1037
- }, [
1038
- append,
1039
- backspace
1040
- ]);
1041
- // Tick down the rate-limit countdown.
1042
- process.env.__NEXT_PRIVATE_MINIMIZE_MACRO_FALSE && (0,react__WEBPACK_IMPORTED_MODULE_1__.useEffect)(()=>{
1043
- if (retryAfterSec <= 0) return;
1044
- const t = setInterval(()=>{
1045
- setRetryAfterSec((s)=>s > 0 ? s - 1 : 0);
1046
- }, 1000);
1047
- return ()=>clearInterval(t);
936
+
937
+ function UnlockScreen({ onUnlock } = {}) {
938
+ const router = (0,next_navigation__WEBPACK_IMPORTED_MODULE_1__.useRouter)();
939
+ const onSuccess = (0,react__WEBPACK_IMPORTED_MODULE_2__.useCallback)(()=>{
940
+ if (onUnlock) onUnlock();
941
+ else router.refresh();
1048
942
  }, [
1049
- retryAfterSec
943
+ onUnlock,
944
+ router
1050
945
  ]);
1051
- return /*#__PURE__*/ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsxs)("div", {
1052
- ref: containerRef,
1053
- className: "fixed inset-0 z-[1000] flex flex-col items-center justify-center gap-6 bg-surface text-fg",
1054
- style: {
1055
- paddingTop: "env(safe-area-inset-top)",
1056
- paddingBottom: "env(safe-area-inset-bottom)"
1057
- },
1058
- children: [
1059
- /*#__PURE__*/ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_components_ui_Logo__WEBPACK_IMPORTED_MODULE_2__/* .Logo */ .g, {
1060
- className: "h-16 w-auto"
1061
- }),
1062
- /*#__PURE__*/ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsxs)("div", {
1063
- className: "w-full max-w-xs p-6",
1064
- children: [
1065
- /*#__PURE__*/ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("h1", {
1066
- className: "mb-1 text-center text-lg font-semibold text-fg",
1067
- children: "Unlock Jarela"
1068
- }),
1069
- /*#__PURE__*/ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("p", {
1070
- className: "mb-6 text-center text-xs text-fg-faint",
1071
- children: "Enter your 6-digit PIN to decrypt your data."
1072
- }),
1073
- /*#__PURE__*/ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("div", {
1074
- className: "mb-6 flex justify-center gap-3",
1075
- "aria-label": "PIN entry progress",
1076
- children: Array.from({
1077
- length: PIN_LENGTH
1078
- }).map((_, i)=>/*#__PURE__*/ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("span", {
1079
- className: `h-3 w-3 rounded-full transition-colors ${i < digits.length ? error ? "bg-red-500" : "bg-fg" : "bg-surface-3"}`
1080
- }, i))
1081
- }),
1082
- /*#__PURE__*/ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsxs)("div", {
1083
- className: "grid grid-cols-3 gap-2",
1084
- children: [
1085
- [
1086
- "1",
1087
- "2",
1088
- "3",
1089
- "4",
1090
- "5",
1091
- "6",
1092
- "7",
1093
- "8",
1094
- "9"
1095
- ].map((d)=>/*#__PURE__*/ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(PinKey, {
1096
- digit: d,
1097
- onPress: ()=>append(d),
1098
- disabled: submitting || retryAfterSec > 0
1099
- }, d)),
1100
- /*#__PURE__*/ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("div", {}),
1101
- /*#__PURE__*/ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(PinKey, {
1102
- digit: "0",
1103
- onPress: ()=>append("0"),
1104
- disabled: submitting || retryAfterSec > 0
1105
- }),
1106
- /*#__PURE__*/ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(PinKey, {
1107
- digit: "←",
1108
- onPress: backspace,
1109
- disabled: submitting || digits.length === 0,
1110
- ariaLabel: "Backspace"
1111
- })
1112
- ]
1113
- }),
1114
- /*#__PURE__*/ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("p", {
1115
- className: `mt-4 min-h-[1.5rem] text-center text-xs ${error ? "text-red-400" : "text-fg-faint"}`,
1116
- role: "status",
1117
- "aria-live": "polite",
1118
- children: retryAfterSec > 0 ? `Try again in ${retryAfterSec}s` : error ?? (submitting ? "Unlocking…" : "\u00A0")
1119
- })
1120
- ]
1121
- })
1122
- ]
1123
- });
1124
- }
1125
- function PinKey({ digit, onPress, disabled, ariaLabel }) {
1126
- return /*#__PURE__*/ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("button", {
1127
- type: "button",
1128
- onClick: onPress,
1129
- disabled: disabled,
1130
- "aria-label": ariaLabel ?? digit,
1131
- className: "h-14 rounded-xl bg-surface-3 text-xl font-medium text-fg transition-colors hover:bg-surface-3/70 active:bg-surface-3/50 disabled:cursor-not-allowed disabled:opacity-50",
1132
- children: digit
946
+ return /*#__PURE__*/ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_PinKeypad__WEBPACK_IMPORTED_MODULE_3__/* .PinKeypad */ .y, {
947
+ mode: "decrypt",
948
+ onSuccess: onSuccess
1133
949
  });
1134
950
  }
1135
951
 
@@ -1757,6 +1573,241 @@ function() { throw new Error("Attempted to call UnlockScreen() from the server b
1757
1573
  "UnlockScreen",
1758
1574
  );
1759
1575
 
1576
+ /***/ }),
1577
+
1578
+ /***/ 71654:
1579
+ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
1580
+
1581
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
1582
+ /* harmony export */ y: () => (/* binding */ PinKeypad)
1583
+ /* harmony export */ });
1584
+ /* harmony import */ var react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(48249);
1585
+ /* harmony import */ var react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__);
1586
+ /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(67484);
1587
+ /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_1__);
1588
+ /* harmony import */ var _components_ui_Logo__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(41357);
1589
+ /* __next_internal_client_entry_do_not_use__ PinKeypad auto */
1590
+
1591
+
1592
+ // Shared 6-digit PIN keypad used by both the decrypt splash (master key
1593
+ // locked at boot) and the screen-lock overlay (idle timer fired). The
1594
+ // only differences between the two states are (a) the endpoint hit
1595
+ // (b) the copy on screen and (c) what happens after success. Everything
1596
+ // else — keypad layout, dot strip, rate-limit handling, keyboard
1597
+ // support, error mapping — is identical, so the two were collapsed into
1598
+ // one component to avoid drift.
1599
+ const PIN_LENGTH = 6;
1600
+ const MODES = {
1601
+ decrypt: {
1602
+ endpoint: "/api/v1/security/unlock",
1603
+ title: "Decrypt Jarela",
1604
+ subtitle: "Enter your 6-digit PIN to decrypt your data.",
1605
+ busyLabel: "Decrypting\u2026"
1606
+ },
1607
+ unlock: {
1608
+ endpoint: "/api/v1/security/verify-pin",
1609
+ title: "Welcome back",
1610
+ subtitle: "Enter your 6-digit PIN to unlock.",
1611
+ busyLabel: "Unlocking\u2026"
1612
+ }
1613
+ };
1614
+ function PinKeypad({ mode, onSuccess }) {
1615
+ const cfg = MODES[mode];
1616
+ const [digits, setDigits] = (0,react__WEBPACK_IMPORTED_MODULE_1__.useState)("");
1617
+ const [error, setError] = (0,react__WEBPACK_IMPORTED_MODULE_1__.useState)(null);
1618
+ const [submitting, setSubmitting] = (0,react__WEBPACK_IMPORTED_MODULE_1__.useState)(false);
1619
+ const [retryAfterSec, setRetryAfterSec] = (0,react__WEBPACK_IMPORTED_MODULE_1__.useState)(0);
1620
+ // Hard guard against parallel submits. setState updaters can run more
1621
+ // than once (dev StrictMode, concurrent rendering), so if `submit`
1622
+ // were called from inside `setDigits` we'd POST twice and the second
1623
+ // request would race the first into `unlockMasterKey()` after state
1624
+ // already flipped to unlocked — the route would 500.
1625
+ const submittingRef = (0,react__WEBPACK_IMPORTED_MODULE_1__.useRef)(false);
1626
+ const submit = (0,react__WEBPACK_IMPORTED_MODULE_1__.useCallback)(async (pin)=>{
1627
+ if (submittingRef.current) return;
1628
+ submittingRef.current = true;
1629
+ setSubmitting(true);
1630
+ setError(null);
1631
+ try {
1632
+ const res = await fetch(cfg.endpoint, {
1633
+ method: "POST",
1634
+ headers: {
1635
+ "content-type": "application/json"
1636
+ },
1637
+ body: JSON.stringify({
1638
+ pin
1639
+ })
1640
+ });
1641
+ if (res.ok) {
1642
+ onSuccess();
1643
+ return;
1644
+ }
1645
+ const body = await res.json().catch(()=>({}));
1646
+ // Goal-state convergence: both endpoints can return 409 when the
1647
+ // server already reached the target state (decrypt → master key
1648
+ // already unlocked; unlock → screen not locked). Treat as success
1649
+ // rather than surfacing a confusing error.
1650
+ if (res.status === 409 && (body.error === "not-locked" || body.error === "no-pin")) {
1651
+ onSuccess();
1652
+ return;
1653
+ }
1654
+ if (res.status === 429 && typeof body.retry_after_ms === "number") {
1655
+ setRetryAfterSec(Math.ceil(body.retry_after_ms / 1000));
1656
+ setError("Too many attempts. Try again later.");
1657
+ } else if (res.status === 401) {
1658
+ setError("Wrong PIN. Try again.");
1659
+ } else if (res.status === 400) {
1660
+ setError("Invalid PIN format.");
1661
+ } else {
1662
+ setError(body.error ?? `Error (${res.status})`);
1663
+ }
1664
+ setDigits("");
1665
+ } catch (err) {
1666
+ setError(err instanceof Error ? err.message : String(err));
1667
+ setDigits("");
1668
+ } finally{
1669
+ submittingRef.current = false;
1670
+ setSubmitting(false);
1671
+ }
1672
+ }, [
1673
+ cfg.endpoint,
1674
+ onSuccess
1675
+ ]);
1676
+ const append = (0,react__WEBPACK_IMPORTED_MODULE_1__.useCallback)((d)=>{
1677
+ if (submitting || retryAfterSec > 0) return;
1678
+ setError(null);
1679
+ setDigits((cur)=>cur.length >= PIN_LENGTH ? cur : cur + d);
1680
+ }, [
1681
+ submitting,
1682
+ retryAfterSec
1683
+ ]);
1684
+ // Auto-submit once the buffer hits 6 digits.
1685
+ process.env.__NEXT_PRIVATE_MINIMIZE_MACRO_FALSE && (0,react__WEBPACK_IMPORTED_MODULE_1__.useEffect)(()=>{
1686
+ if (digits.length === PIN_LENGTH && !submittingRef.current) {
1687
+ void submit(digits);
1688
+ }
1689
+ }, [
1690
+ digits,
1691
+ submit
1692
+ ]);
1693
+ const backspace = (0,react__WEBPACK_IMPORTED_MODULE_1__.useCallback)(()=>{
1694
+ if (submitting) return;
1695
+ setError(null);
1696
+ setDigits((cur)=>cur.slice(0, -1));
1697
+ }, [
1698
+ submitting
1699
+ ]);
1700
+ process.env.__NEXT_PRIVATE_MINIMIZE_MACRO_FALSE && (0,react__WEBPACK_IMPORTED_MODULE_1__.useEffect)(()=>{
1701
+ function onKey(e) {
1702
+ if (/^[0-9]$/.test(e.key)) {
1703
+ e.preventDefault();
1704
+ append(e.key);
1705
+ } else if (e.key === "Backspace") {
1706
+ e.preventDefault();
1707
+ backspace();
1708
+ }
1709
+ }
1710
+ window.addEventListener("keydown", onKey);
1711
+ return ()=>window.removeEventListener("keydown", onKey);
1712
+ }, [
1713
+ append,
1714
+ backspace
1715
+ ]);
1716
+ process.env.__NEXT_PRIVATE_MINIMIZE_MACRO_FALSE && (0,react__WEBPACK_IMPORTED_MODULE_1__.useEffect)(()=>{
1717
+ if (retryAfterSec <= 0) return;
1718
+ const t = setInterval(()=>{
1719
+ setRetryAfterSec((s)=>s > 0 ? s - 1 : 0);
1720
+ }, 1000);
1721
+ return ()=>clearInterval(t);
1722
+ }, [
1723
+ retryAfterSec
1724
+ ]);
1725
+ return /*#__PURE__*/ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsxs)("div", {
1726
+ className: "fixed inset-0 z-[1000] flex flex-col items-center justify-center gap-6 bg-surface text-fg animate-in fade-in duration-200",
1727
+ style: {
1728
+ paddingTop: "env(safe-area-inset-top)",
1729
+ paddingBottom: "env(safe-area-inset-bottom)"
1730
+ },
1731
+ "data-pin-mode": mode,
1732
+ children: [
1733
+ /*#__PURE__*/ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_components_ui_Logo__WEBPACK_IMPORTED_MODULE_2__/* .Logo */ .g, {
1734
+ className: "h-16 w-auto"
1735
+ }),
1736
+ /*#__PURE__*/ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsxs)("div", {
1737
+ className: "w-full max-w-xs p-6",
1738
+ children: [
1739
+ /*#__PURE__*/ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("h1", {
1740
+ className: "mb-1 text-center text-lg font-semibold text-fg",
1741
+ children: cfg.title
1742
+ }),
1743
+ /*#__PURE__*/ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("p", {
1744
+ className: "mb-6 text-center text-xs text-fg-faint",
1745
+ children: cfg.subtitle
1746
+ }),
1747
+ /*#__PURE__*/ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("div", {
1748
+ className: "mb-6 flex justify-center gap-3",
1749
+ "aria-label": "PIN entry progress",
1750
+ children: Array.from({
1751
+ length: PIN_LENGTH
1752
+ }).map((_, i)=>/*#__PURE__*/ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("span", {
1753
+ className: `h-3 w-3 rounded-full transition-colors ${i < digits.length ? error ? "bg-red-500" : "bg-fg" : "bg-surface-3"}`
1754
+ }, i))
1755
+ }),
1756
+ /*#__PURE__*/ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsxs)("div", {
1757
+ className: "grid grid-cols-3 gap-2",
1758
+ children: [
1759
+ [
1760
+ "1",
1761
+ "2",
1762
+ "3",
1763
+ "4",
1764
+ "5",
1765
+ "6",
1766
+ "7",
1767
+ "8",
1768
+ "9"
1769
+ ].map((d)=>/*#__PURE__*/ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(PinKey, {
1770
+ digit: d,
1771
+ onPress: ()=>append(d),
1772
+ disabled: submitting || retryAfterSec > 0
1773
+ }, d)),
1774
+ /*#__PURE__*/ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("div", {}),
1775
+ /*#__PURE__*/ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(PinKey, {
1776
+ digit: "0",
1777
+ onPress: ()=>append("0"),
1778
+ disabled: submitting || retryAfterSec > 0
1779
+ }),
1780
+ /*#__PURE__*/ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(PinKey, {
1781
+ digit: "\\u2190",
1782
+ onPress: backspace,
1783
+ disabled: submitting || digits.length === 0,
1784
+ ariaLabel: "Backspace"
1785
+ })
1786
+ ]
1787
+ }),
1788
+ /*#__PURE__*/ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("p", {
1789
+ className: `mt-4 min-h-[1.5rem] text-center text-xs ${error ? "text-red-400" : "text-fg-faint"}`,
1790
+ role: "status",
1791
+ "aria-live": "polite",
1792
+ children: retryAfterSec > 0 ? `Try again in ${retryAfterSec}s` : error ?? (submitting ? cfg.busyLabel : "\u00A0")
1793
+ })
1794
+ ]
1795
+ })
1796
+ ]
1797
+ });
1798
+ }
1799
+ function PinKey({ digit, onPress, disabled, ariaLabel }) {
1800
+ return /*#__PURE__*/ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("button", {
1801
+ type: "button",
1802
+ onClick: onPress,
1803
+ disabled: disabled,
1804
+ "aria-label": ariaLabel ?? digit,
1805
+ className: "h-14 rounded-xl bg-surface-3 text-xl font-medium text-fg transition-colors hover:bg-surface-3/70 active:bg-surface-3/50 disabled:cursor-not-allowed disabled:opacity-50",
1806
+ children: digit
1807
+ });
1808
+ }
1809
+
1810
+
1760
1811
  /***/ }),
1761
1812
 
1762
1813
  /***/ 95984: