@peers-app/peers-ui 0.27.3 → 0.27.5

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 (311) hide show
  1. package/dist/screens/network-viewer/group-details-modal.d.ts +4 -0
  2. package/dist/screens/network-viewer/group-details-modal.js +6 -46
  3. package/dist/screens/network-viewer/network-viewer-ipc.d.ts +3 -2
  4. package/dist/screens/network-viewer/network-viewer-ipc.js +6 -0
  5. package/dist/screens/network-viewer/sync-recovery.js +103 -1
  6. package/dist/screens/variables/variable-details.js +2 -1
  7. package/dist/tabs-layout/network-status-indicator.js +26 -12
  8. package/dist/tabs-layout/use-network-status.d.ts +6 -1
  9. package/dist/tabs-layout/use-network-status.js +27 -4
  10. package/package.json +9 -3
  11. package/.claude/settings.local.json +0 -34
  12. package/.github/README.md +0 -52
  13. package/.github/workflows/delete-all-artifacts.yml +0 -49
  14. package/.github/workflows/publish.yml +0 -51
  15. package/babel.config.js +0 -7
  16. package/biome.json +0 -198
  17. package/dist/app-updates/app-updates-consumer.test.d.ts +0 -1
  18. package/dist/app-updates/app-updates-consumer.test.js +0 -56
  19. package/dist/app-updates/app-updates-settings.test.d.ts +0 -1
  20. package/dist/app-updates/app-updates-settings.test.js +0 -93
  21. package/dist/app-updates/remote-app-update-panel.test.d.ts +0 -1
  22. package/dist/app-updates/remote-app-update-panel.test.js +0 -165
  23. package/dist/components/access-level-field.test.d.ts +0 -1
  24. package/dist/components/access-level-field.test.js +0 -28
  25. package/dist/components/message-logs/tool-access-approval-card.test.d.ts +0 -1
  26. package/dist/components/message-logs/tool-access-approval-card.test.js +0 -102
  27. package/dist/components/responsive-section-nav.test.d.ts +0 -1
  28. package/dist/components/responsive-section-nav.test.js +0 -45
  29. package/dist/globals-hash.test.d.ts +0 -1
  30. package/dist/globals-hash.test.js +0 -16
  31. package/dist/hooks/use-observable-state.test.d.ts +0 -1
  32. package/dist/hooks/use-observable-state.test.js +0 -64
  33. package/dist/hooks/use-observable.test.d.ts +0 -1
  34. package/dist/hooks/use-observable.test.js +0 -191
  35. package/dist/hooks/use-rows.test.d.ts +0 -1
  36. package/dist/hooks/use-rows.test.js +0 -175
  37. package/dist/hooks/use-tool.test.d.ts +0 -1
  38. package/dist/hooks/use-tool.test.js +0 -190
  39. package/dist/host-shell/host-shell-consumer.test.d.ts +0 -1
  40. package/dist/host-shell/host-shell-consumer.test.js +0 -28
  41. package/dist/root-layout/root-layout-host.test.d.ts +0 -1
  42. package/dist/root-layout/root-layout-host.test.js +0 -86
  43. package/dist/screens/account/account-screen.test.d.ts +0 -1
  44. package/dist/screens/account/account-screen.test.js +0 -51
  45. package/dist/screens/assistants/assistant-config.test.d.ts +0 -1
  46. package/dist/screens/assistants/assistant-config.test.js +0 -57
  47. package/dist/screens/contacts/contact-list.test.d.ts +0 -1
  48. package/dist/screens/contacts/contact-list.test.js +0 -48
  49. package/dist/screens/device-pairing/device-pairing.test.d.ts +0 -1
  50. package/dist/screens/device-pairing/device-pairing.test.js +0 -119
  51. package/dist/screens/devices/devices-list.test.d.ts +0 -1
  52. package/dist/screens/devices/devices-list.test.js +0 -53
  53. package/dist/screens/groups/group-list.test.d.ts +0 -1
  54. package/dist/screens/groups/group-list.test.js +0 -38
  55. package/dist/screens/identity/identity-routes.test.d.ts +0 -1
  56. package/dist/screens/identity/identity-routes.test.js +0 -51
  57. package/dist/screens/identity/identity-screen.test.d.ts +0 -1
  58. package/dist/screens/identity/identity-screen.test.js +0 -103
  59. package/dist/screens/invites/accept-invite.test.d.ts +0 -1
  60. package/dist/screens/invites/accept-invite.test.js +0 -103
  61. package/dist/screens/invites/activity-view.test.d.ts +0 -1
  62. package/dist/screens/invites/activity-view.test.js +0 -108
  63. package/dist/screens/invites/invite-classify.test.d.ts +0 -1
  64. package/dist/screens/invites/invite-classify.test.js +0 -115
  65. package/dist/screens/network-viewer/network-viewer-ipc.test.d.ts +0 -1
  66. package/dist/screens/network-viewer/network-viewer-ipc.test.js +0 -61
  67. package/dist/screens/network-viewer/network-viewer.section.test.d.ts +0 -1
  68. package/dist/screens/network-viewer/network-viewer.section.test.js +0 -63
  69. package/dist/screens/network-viewer/sync-recovery.test.d.ts +0 -1
  70. package/dist/screens/network-viewer/sync-recovery.test.js +0 -104
  71. package/dist/screens/packages/package-helpers.test.d.ts +0 -1
  72. package/dist/screens/packages/package-helpers.test.js +0 -53
  73. package/dist/screens/packages/package-runtime-status.test.d.ts +0 -1
  74. package/dist/screens/packages/package-runtime-status.test.js +0 -50
  75. package/dist/screens/packages/packages-ipc.test.d.ts +0 -1
  76. package/dist/screens/packages/packages-ipc.test.js +0 -43
  77. package/dist/screens/settings/settings-page.test.d.ts +0 -1
  78. package/dist/screens/settings/settings-page.test.js +0 -19
  79. package/dist/screens/tools/tool-access-ipc.test.d.ts +0 -1
  80. package/dist/screens/tools/tool-access-ipc.test.js +0 -40
  81. package/dist/screens/tools/tool-catalog-ipc.test.d.ts +0 -1
  82. package/dist/screens/tools/tool-catalog-ipc.test.js +0 -52
  83. package/dist/tabs-layout/network-status-indicator.test.d.ts +0 -1
  84. package/dist/tabs-layout/network-status-indicator.test.js +0 -665
  85. package/dist/tabs-layout/tabs-state.test.d.ts +0 -1
  86. package/dist/tabs-layout/tabs-state.test.js +0 -110
  87. package/dist/tabs-layout/use-network-status.test.d.ts +0 -1
  88. package/dist/tabs-layout/use-network-status.test.js +0 -45
  89. package/dist/ui-router/ui-loader.test.d.ts +0 -1
  90. package/dist/ui-router/ui-loader.test.js +0 -91
  91. package/jest.config.js +0 -25
  92. package/release-guard.js +0 -10
  93. package/src/app-updates/app-update-controls.tsx +0 -73
  94. package/src/app-updates/app-updates-consumer.test.ts +0 -83
  95. package/src/app-updates/app-updates-consumer.ts +0 -121
  96. package/src/app-updates/app-updates-settings.test.tsx +0 -109
  97. package/src/app-updates/app-updates-settings.tsx +0 -136
  98. package/src/app-updates/remote-app-update-panel.test.tsx +0 -205
  99. package/src/app-updates/remote-app-update-panel.tsx +0 -210
  100. package/src/app-updates/use-app-update-status.ts +0 -16
  101. package/src/command-palette/command-palette-ui.tsx +0 -564
  102. package/src/command-palette/command-palette.ts +0 -242
  103. package/src/components/access-level-field.test.tsx +0 -32
  104. package/src/components/access-level-field.tsx +0 -68
  105. package/src/components/checkbox.tsx +0 -25
  106. package/src/components/group-switcher.tsx +0 -526
  107. package/src/components/input-date.tsx +0 -29
  108. package/src/components/input-number.tsx +0 -83
  109. package/src/components/input.tsx +0 -25
  110. package/src/components/inverse-lazy-list.tsx +0 -107
  111. package/src/components/io-schema-values.tsx +0 -103
  112. package/src/components/io-schema.tsx +0 -259
  113. package/src/components/lazy-list.tsx +0 -115
  114. package/src/components/list-screen.tsx +0 -126
  115. package/src/components/loading-indicator.tsx +0 -7
  116. package/src/components/markdown-editor/autolink-plugin.tsx +0 -29
  117. package/src/components/markdown-editor/editor-inline.tsx +0 -9
  118. package/src/components/markdown-editor/editor.tsx +0 -158
  119. package/src/components/markdown-editor/markdown-plugin.tsx +0 -202
  120. package/src/components/markdown-editor/mention-node.ts +0 -204
  121. package/src/components/markdown-editor/mentions-plugin.tsx +0 -354
  122. package/src/components/markdown-editor/move-line-plugin.tsx +0 -186
  123. package/src/components/markdown-editor/select-line-boundary-plugin.tsx +0 -95
  124. package/src/components/markdown-editor/theme.ts +0 -46
  125. package/src/components/markdown-editor/toolbar.tsx +0 -269
  126. package/src/components/markdown-with-mentions.tsx +0 -198
  127. package/src/components/message-logs/message-logs.tsx +0 -523
  128. package/src/components/message-logs/tool-access-approval-card.test.tsx +0 -127
  129. package/src/components/message-logs/tool-access-approval-card.tsx +0 -178
  130. package/src/components/messages/avatar.tsx +0 -171
  131. package/src/components/messages/channel-message-list.tsx +0 -189
  132. package/src/components/messages/channel-view.tsx +0 -92
  133. package/src/components/messages/message-compose.tsx +0 -192
  134. package/src/components/messages/message-display.tsx +0 -231
  135. package/src/components/messages/thread-message-list.tsx +0 -135
  136. package/src/components/qr-scanner.tsx +0 -180
  137. package/src/components/responsive-section-nav.test.tsx +0 -88
  138. package/src/components/responsive-section-nav.tsx +0 -92
  139. package/src/components/router.tsx +0 -175
  140. package/src/components/save-button.tsx +0 -134
  141. package/src/components/sortable-list.tsx +0 -130
  142. package/src/components/tabs.tsx +0 -76
  143. package/src/components/tooltip.tsx +0 -43
  144. package/src/components/trust-level-badge.tsx +0 -44
  145. package/src/components/trust-level-dropdown.tsx +0 -136
  146. package/src/components/typeahead.tsx +0 -357
  147. package/src/globals-hash.test.ts +0 -19
  148. package/src/globals.tsx +0 -226
  149. package/src/hooks/index.ts +0 -8
  150. package/src/hooks/use-host-contract.ts +0 -75
  151. package/src/hooks/use-observable-state.test.tsx +0 -101
  152. package/src/hooks/use-observable-state.ts +0 -29
  153. package/src/hooks/use-observable.test.tsx +0 -258
  154. package/src/hooks/use-observable.ts +0 -67
  155. package/src/hooks/use-on-screen.ts +0 -24
  156. package/src/hooks/use-promise.ts +0 -34
  157. package/src/hooks/use-rows.test.tsx +0 -196
  158. package/src/hooks/use-rows.ts +0 -120
  159. package/src/hooks/use-subscription.ts +0 -24
  160. package/src/hooks/use-tool.test.tsx +0 -225
  161. package/src/hooks/use-tool.ts +0 -116
  162. package/src/host-shell/host-shell-consumer.test.ts +0 -32
  163. package/src/host-shell/host-shell-consumer.ts +0 -53
  164. package/src/index.tsx +0 -42
  165. package/src/mention-configs.ts +0 -185
  166. package/src/operator-console/operator-console-state.ts +0 -119
  167. package/src/operator-console/operator-console.tsx +0 -233
  168. package/src/peers-services-registration.ts +0 -6
  169. package/src/root-layout/global-overlays.ts +0 -50
  170. package/src/root-layout/layout-picker.tsx +0 -47
  171. package/src/root-layout/layouts/full-screen-layout.tsx +0 -144
  172. package/src/root-layout/layouts/tabs-with-console-layout.tsx +0 -260
  173. package/src/root-layout/root-layout-host.test.tsx +0 -108
  174. package/src/root-layout/root-layout-host.tsx +0 -147
  175. package/src/root-layout/root-layout-registry.ts +0 -51
  176. package/src/root-layout/root-layout-state.ts +0 -55
  177. package/src/root-layout/root-layout.ts +0 -32
  178. package/src/screens/account/account-screen.test.tsx +0 -65
  179. package/src/screens/account/account-screen.tsx +0 -942
  180. package/src/screens/assistants/assistant-config.test.tsx +0 -54
  181. package/src/screens/assistants/assistant-config.tsx +0 -121
  182. package/src/screens/assistants/assistant-details.tsx +0 -89
  183. package/src/screens/assistants/assistant-info.tsx +0 -66
  184. package/src/screens/assistants/assistant-list.tsx +0 -145
  185. package/src/screens/assistants/assistant-tools.tsx +0 -82
  186. package/src/screens/console-logs/console-logs-list.tsx +0 -374
  187. package/src/screens/console-logs/log-display.tsx +0 -137
  188. package/src/screens/console-logs/log-filters.tsx +0 -64
  189. package/src/screens/console-logs/mobile-log-card.tsx +0 -161
  190. package/src/screens/console-logs/resizable-table-header.tsx +0 -110
  191. package/src/screens/contacts/contact-details.tsx +0 -123
  192. package/src/screens/contacts/contact-list.test.tsx +0 -59
  193. package/src/screens/contacts/contact-list.tsx +0 -286
  194. package/src/screens/contacts/index.ts +0 -2
  195. package/src/screens/data-explorer/data-explorer.tsx +0 -633
  196. package/src/screens/data-explorer/index.ts +0 -6
  197. package/src/screens/data-explorer/query-executor.tsx +0 -520
  198. package/src/screens/device-pairing/device-pairing.test.tsx +0 -143
  199. package/src/screens/device-pairing/device-pairing.tsx +0 -461
  200. package/src/screens/device-pairing/identity-ipc.ts +0 -25
  201. package/src/screens/device-pairing/index.ts +0 -2
  202. package/src/screens/device-pairing/use-device-pairing-state.ts +0 -15
  203. package/src/screens/devices/devices-list.test.tsx +0 -68
  204. package/src/screens/devices/devices-list.tsx +0 -365
  205. package/src/screens/devices/index.ts +0 -1
  206. package/src/screens/groups/group-details.tsx +0 -160
  207. package/src/screens/groups/group-invite-panel.tsx +0 -311
  208. package/src/screens/groups/group-list.test.tsx +0 -44
  209. package/src/screens/groups/group-list.tsx +0 -317
  210. package/src/screens/groups/group-members.tsx +0 -343
  211. package/src/screens/groups/index.ts +0 -3
  212. package/src/screens/identity/identity-routes.test.ts +0 -70
  213. package/src/screens/identity/identity-routes.ts +0 -301
  214. package/src/screens/identity/identity-screen.test.tsx +0 -138
  215. package/src/screens/identity/identity-screen.tsx +0 -282
  216. package/src/screens/identity/index.ts +0 -2
  217. package/src/screens/identity/invite-contact-panel.tsx +0 -45
  218. package/src/screens/identity/profile-invite.tsx +0 -93
  219. package/src/screens/invites/accept-invite.test.tsx +0 -115
  220. package/src/screens/invites/accept-invite.tsx +0 -318
  221. package/src/screens/invites/activity-view.test.tsx +0 -127
  222. package/src/screens/invites/activity-view.tsx +0 -162
  223. package/src/screens/invites/index.ts +0 -6
  224. package/src/screens/invites/invite-capture.ts +0 -104
  225. package/src/screens/invites/invite-classify.test.ts +0 -129
  226. package/src/screens/invites/invite-share.tsx +0 -104
  227. package/src/screens/invites/invites-ipc.ts +0 -25
  228. package/src/screens/invites/pending-invites-list.tsx +0 -256
  229. package/src/screens/network-viewer/connection-troubleshooter.tsx +0 -395
  230. package/src/screens/network-viewer/cpu-usage-graph.tsx +0 -294
  231. package/src/screens/network-viewer/device-details-modal.tsx +0 -339
  232. package/src/screens/network-viewer/group-details-modal.tsx +0 -421
  233. package/src/screens/network-viewer/index.ts +0 -8
  234. package/src/screens/network-viewer/network-viewer-ipc.test.ts +0 -72
  235. package/src/screens/network-viewer/network-viewer-ipc.ts +0 -39
  236. package/src/screens/network-viewer/network-viewer-section.ts +0 -12
  237. package/src/screens/network-viewer/network-viewer.section.test.tsx +0 -68
  238. package/src/screens/network-viewer/network-viewer.tsx +0 -708
  239. package/src/screens/network-viewer/remote-logs-panel.tsx +0 -190
  240. package/src/screens/network-viewer/sync-recovery.test.tsx +0 -114
  241. package/src/screens/network-viewer/sync-recovery.tsx +0 -461
  242. package/src/screens/network-viewer/usage-graph.tsx +0 -455
  243. package/src/screens/packages/local-folder-grants-panel.tsx +0 -290
  244. package/src/screens/packages/local-folders-ipc.ts +0 -28
  245. package/src/screens/packages/package-details.tsx +0 -166
  246. package/src/screens/packages/package-helpers.test.ts +0 -57
  247. package/src/screens/packages/package-helpers.ts +0 -137
  248. package/src/screens/packages/package-info.tsx +0 -354
  249. package/src/screens/packages/package-list.tsx +0 -519
  250. package/src/screens/packages/package-network-panel.tsx +0 -82
  251. package/src/screens/packages/package-new-local.tsx +0 -100
  252. package/src/screens/packages/package-runtime-status.test.tsx +0 -58
  253. package/src/screens/packages/package-runtime-status.tsx +0 -68
  254. package/src/screens/packages/package-versions.tsx +0 -298
  255. package/src/screens/packages/packages-ipc.test.ts +0 -47
  256. package/src/screens/packages/packages-ipc.ts +0 -125
  257. package/src/screens/peer-types/peer-type-details.tsx +0 -172
  258. package/src/screens/peer-types/peer-type-list.tsx +0 -64
  259. package/src/screens/search/global-search.tsx +0 -406
  260. package/src/screens/settings/color-mode-dropdown.tsx +0 -82
  261. package/src/screens/settings/settings-page.test.tsx +0 -24
  262. package/src/screens/settings/settings-page.tsx +0 -426
  263. package/src/screens/setup-user.tsx +0 -355
  264. package/src/screens/tools/tool-access-ipc.test.ts +0 -55
  265. package/src/screens/tools/tool-access-ipc.ts +0 -27
  266. package/src/screens/tools/tool-catalog-ipc.test.ts +0 -52
  267. package/src/screens/tools/tool-catalog-ipc.ts +0 -51
  268. package/src/screens/tools/tool-details.tsx +0 -52
  269. package/src/screens/tools/tool-info.tsx +0 -45
  270. package/src/screens/tools/tool-list.tsx +0 -106
  271. package/src/screens/tools/tool-schema.tsx +0 -44
  272. package/src/screens/tools/tool-test-details.tsx +0 -89
  273. package/src/screens/tools/tool-test-list.tsx +0 -73
  274. package/src/screens/variables/variable-details.tsx +0 -289
  275. package/src/screens/variables/variable-list.tsx +0 -110
  276. package/src/screens/welcome-modal.tsx +0 -294
  277. package/src/screens/workflows/workflow-details.tsx +0 -115
  278. package/src/screens/workflows/workflow-info.tsx +0 -22
  279. package/src/screens/workflows/workflow-instructions.tsx +0 -134
  280. package/src/screens/workflows/workflow-list.tsx +0 -117
  281. package/src/setupTests.ts +0 -38
  282. package/src/system-apps/assistants.app.ts +0 -7
  283. package/src/system-apps/console-logs.app.ts +0 -7
  284. package/src/system-apps/data-explorer.app.ts +0 -8
  285. package/src/system-apps/identity.app.ts +0 -9
  286. package/src/system-apps/index.ts +0 -77
  287. package/src/system-apps/mobile-settings.app.ts +0 -7
  288. package/src/system-apps/network-viewer.app.ts +0 -8
  289. package/src/system-apps/packages.app.ts +0 -7
  290. package/src/system-apps/search.app.ts +0 -8
  291. package/src/system-apps/settings.app.ts +0 -7
  292. package/src/system-apps/threads.app.ts +0 -7
  293. package/src/system-apps/tools.app.ts +0 -7
  294. package/src/system-apps/types.app.ts +0 -7
  295. package/src/system-apps/variables.app.ts +0 -7
  296. package/src/system-apps/workflows.app.ts +0 -7
  297. package/src/tabs-layout/network-status-indicator.test.tsx +0 -799
  298. package/src/tabs-layout/network-status-indicator.tsx +0 -756
  299. package/src/tabs-layout/tabs-layout.tsx +0 -857
  300. package/src/tabs-layout/tabs-state.test.ts +0 -152
  301. package/src/tabs-layout/tabs-state.ts +0 -587
  302. package/src/tabs-layout/use-network-status.test.tsx +0 -58
  303. package/src/tabs-layout/use-network-status.ts +0 -68
  304. package/src/ui-defaults/index.ts +0 -2
  305. package/src/ui-defaults/list-screen.tsx +0 -94
  306. package/src/ui-defaults/markdown-field.tsx +0 -87
  307. package/src/ui-router/routes-loader.ts +0 -193
  308. package/src/ui-router/ui-loader.test.tsx +0 -108
  309. package/src/ui-router/ui-loader.tsx +0 -606
  310. package/src/utils.ts +0 -95
  311. package/tsconfig.json +0 -18
@@ -1,942 +0,0 @@
1
- import {
2
- copyToClipboard,
3
- getMe,
4
- type IAnchorRef,
5
- type IIdentityKeyInfo,
6
- identityRecoveryNoticesVar,
7
- identityRotatedAwayVar,
8
- thisDeviceId,
9
- } from "@peers-app/peers-sdk";
10
- import { useState } from "react";
11
- import { LoadingIndicator } from "../../components/loading-indicator";
12
- import { Tooltip } from "../../components/tooltip";
13
- import { useObservable, usePromise } from "../../hooks";
14
- import {
15
- persistPeersServicesRegistration,
16
- registrationStatusPvar,
17
- } from "../../peers-services-registration";
18
- import { identityTools } from "../device-pairing/identity-ipc";
19
-
20
- const INFO_TEXT = `### What does registering do?
21
-
22
- Registering your account links your **public key** with the Peers cloud service. This enables cloud-powered features like:
23
-
24
- - **AI Assistant** - use the built-in assistant powered by hosted LLM inference
25
- - **Cloud relay** - improved connectivity between your devices
26
- - **Future services** - new cloud features as they become available
27
-
28
- ### Is it safe?
29
-
30
- Registration uses a **cryptographic challenge-response** to prove you own your secret key.
31
- Your **secret key never leaves your device**. Only your public key is shared with the server.
32
-
33
- ### Do I have to register?
34
-
35
- No. Peers works fully in local/peer-to-peer mode without registration. Cloud features simply won't be available until you register.`;
36
-
37
- /** Identity profile, public identifiers, device shortcuts, and service registration. */
38
- export function AccountScreen() {
39
- const [registrationStatus] = useObservable(registrationStatusPvar);
40
- const [deviceId] = useObservable(thisDeviceId);
41
- const me = usePromise(getMe);
42
- const [isRegistering, setIsRegistering] = useState(false);
43
- const [error, setError] = useState<string | null>(null);
44
- const [success, setSuccess] = useState<string | null>(null);
45
-
46
- const isRegistered = !!registrationStatus;
47
-
48
- const handleRegister = async () => {
49
- setIsRegistering(true);
50
- setError(null);
51
- setSuccess(null);
52
-
53
- try {
54
- const { token } = await (await identityTools()).registerWithPeersServices();
55
- await persistPeersServicesRegistration(token);
56
- setSuccess("Account registered successfully.");
57
- } catch (err) {
58
- setError(`Registration failed: ${String(err)}`);
59
- } finally {
60
- setIsRegistering(false);
61
- }
62
- };
63
-
64
- if (!me) {
65
- return <LoadingIndicator />;
66
- }
67
-
68
- return (
69
- <div className="container-xxl p-3">
70
- <div className="mb-3">
71
- <h4 className="mb-1 d-flex align-items-center gap-2">
72
- <i className="bi bi-person-circle" />
73
- Account
74
- </h4>
75
- <div className="small text-muted">Your public identity and account-level services</div>
76
- </div>
77
-
78
- {error && (
79
- <div className="alert alert-danger alert-dismissible" role="alert">
80
- <i className="bi bi-exclamation-triangle me-2" />
81
- {error}
82
- <button
83
- type="button"
84
- className="btn-close"
85
- onClick={() => setError(null)}
86
- aria-label="Close"
87
- />
88
- </div>
89
- )}
90
-
91
- {success && (
92
- <div className="alert alert-success alert-dismissible" role="alert">
93
- <i className="bi bi-check-circle me-2" />
94
- {success}
95
- <button
96
- type="button"
97
- className="btn-close"
98
- onClick={() => setSuccess(null)}
99
- aria-label="Close"
100
- />
101
- </div>
102
- )}
103
-
104
- <div className="card mb-3">
105
- <div className="card-body d-flex align-items-center gap-3 flex-wrap">
106
- <span
107
- className="rounded-circle bg-primary-subtle text-primary-emphasis d-inline-flex align-items-center justify-content-center fs-4 fw-semibold"
108
- style={{ width: 58, height: 58 }}
109
- aria-hidden="true"
110
- >
111
- {profileInitials(me.name)}
112
- </span>
113
- <div className="flex-grow-1">
114
- <h5 className="card-title mb-1">{me.name || "Unnamed user"}</h5>
115
- <div className="small text-muted">Peers identity</div>
116
- </div>
117
- <div className="d-flex gap-2 flex-wrap">
118
- <a href="#settings" className="btn btn-outline-secondary btn-sm">
119
- Edit display name
120
- </a>
121
- <a href="#identity/devices" className="btn btn-outline-primary btn-sm">
122
- Manage devices
123
- </a>
124
- <a href="#identity/devices/pair" className="btn btn-primary btn-sm">
125
- Add device
126
- </a>
127
- </div>
128
- </div>
129
- </div>
130
-
131
- <div className="card mb-3">
132
- <div className="card-body">
133
- <h5 className="card-title">Technical identity</h5>
134
- <p className="small text-muted">
135
- Share these values only when diagnosing identity or connection problems.
136
- </p>
137
- <TechnicalValue
138
- label="User ID"
139
- value={me.userId}
140
- help="Your stable identifier across Peers."
141
- />
142
- <TechnicalValue
143
- label="Signing key"
144
- value={me.publicKey}
145
- help="Other people use this public key to verify your signatures."
146
- />
147
- <TechnicalValue
148
- label="Encryption key"
149
- value={me.publicBoxKey}
150
- help="Other people use this public key to encrypt data for you."
151
- />
152
- <TechnicalValue
153
- label="Current device ID"
154
- value={deviceId || "Not available"}
155
- help="Identifies this installation on Peers networks."
156
- />
157
- </div>
158
- </div>
159
-
160
- <RecoveryNotice />
161
- <SigningKeyCard />
162
- <RecoveryEmailCard />
163
- <AnchorsCard />
164
- <PasskeyCard />
165
-
166
- <div className="card">
167
- <div className="card-body">
168
- <h5 className="card-title d-flex align-items-center gap-2">
169
- Peers Services Registration
170
- <Tooltip markdownContent={INFO_TEXT} />
171
- </h5>
172
-
173
- {isRegistered ? (
174
- <div>
175
- <div className="d-flex align-items-center gap-2 mb-2">
176
- <i className="bi bi-check-circle-fill text-success" style={{ fontSize: "1.2em" }} />
177
- <span>Registered</span>
178
- </div>
179
- <small className="text-muted d-block mb-3">
180
- Registered on{" "}
181
- {new Date(registrationStatus as string).toLocaleDateString(undefined, {
182
- year: "numeric",
183
- month: "long",
184
- day: "numeric",
185
- })}
186
- </small>
187
- <button
188
- type="button"
189
- className="btn btn-outline-secondary btn-sm"
190
- onClick={handleRegister}
191
- disabled={isRegistering}
192
- >
193
- {isRegistering ? (
194
- <>
195
- <span className="spinner-border spinner-border-sm me-2" />
196
- Refreshing...
197
- </>
198
- ) : (
199
- <>
200
- <i className="bi bi-arrow-clockwise me-2" />
201
- Refresh Token
202
- </>
203
- )}
204
- </button>
205
- </div>
206
- ) : (
207
- <div>
208
- <p className="text-muted mb-3">
209
- Register your account to enable cloud-powered features like the AI assistant.
210
- </p>
211
- <button
212
- type="button"
213
- className="btn btn-primary"
214
- onClick={handleRegister}
215
- disabled={isRegistering}
216
- >
217
- {isRegistering ? (
218
- <>
219
- <span className="spinner-border spinner-border-sm me-2" />
220
- Registering...
221
- </>
222
- ) : (
223
- <>
224
- <i className="bi bi-cloud-upload me-2" />
225
- Register Account
226
- </>
227
- )}
228
- </button>
229
- </div>
230
- )}
231
- </div>
232
- </div>
233
- </div>
234
- );
235
- }
236
-
237
- const ROTATE_INFO = `### Rotating your signing key
238
-
239
- Your signing key is how other people and your own devices verify that something came from you.
240
- Replacing it needs an **anchor** already on your profile. A recovery email lets this device
241
- publish the new key to Peers Services. An https anchor cannot be written to from here, so
242
- **Rotate signing key** shows a document for you to place there. The key on this device changes
243
- only after every anchor on your profile lists the new key.
244
-
245
- The database key is stored separately, so a rotation does not re-encrypt the database.`;
246
-
247
- /** A recovery waiting on the delay, or held because a device contested it. */
248
- function RecoveryNotice() {
249
- const [notices] = useObservable(identityRecoveryNoticesVar);
250
- const [busy, setBusy] = useState<string | null>(null);
251
- const [error, setError] = useState<string | null>(null);
252
- if (!notices || notices.length === 0) return null;
253
-
254
- const act = async (label: string, action: () => Promise<void>) => {
255
- setBusy(label);
256
- setError(null);
257
- try {
258
- await action();
259
- } catch (err) {
260
- setError(err instanceof Error ? err.message : String(err));
261
- } finally {
262
- setBusy(null);
263
- }
264
- };
265
-
266
- return (
267
- <div className="card mb-3">
268
- <div className="card-body">
269
- <h5 className="card-title">Key recovery</h5>
270
- {notices.map((notice) => (
271
- <div key={`${notice.userId}:${notice.pendingPublicKey}`} className="mb-3">
272
- <p className="mb-2">
273
- {notice.own
274
- ? "A new key was published for this account without your current key's signature."
275
- : "A contact's key is being recovered."}{" "}
276
- {notice.contested
277
- ? "A device that holds the current key objects. The new key stays untrusted until you accept it."
278
- : `Peers adopt it after ${new Date(notice.readyAt).toLocaleString()} unless someone contests it.`}
279
- </p>
280
- <code className="small text-break">{notice.pendingPublicKey}</code>
281
- <div className="mt-2">
282
- {notice.own ? (
283
- <button
284
- type="button"
285
- className="btn btn-warning btn-sm"
286
- disabled={busy !== null}
287
- onClick={() =>
288
- act(notice.pendingPublicKey, async () => {
289
- const tools = await identityTools();
290
- await tools.contestRecovery({ pendingKey: notice.pendingPublicKey });
291
- })
292
- }
293
- >
294
- {busy === notice.pendingPublicKey ? "Sending..." : "Contest"}
295
- </button>
296
- ) : (
297
- <button
298
- type="button"
299
- className="btn btn-outline-primary btn-sm"
300
- disabled={busy !== null}
301
- onClick={() =>
302
- act(notice.pendingPublicKey, async () => {
303
- const tools = await identityTools();
304
- await tools.acceptRecovery({
305
- userId: notice.userId,
306
- pendingKey: notice.pendingPublicKey,
307
- });
308
- })
309
- }
310
- >
311
- {busy === notice.pendingPublicKey ? "Accepting..." : "Accept the new key"}
312
- </button>
313
- )}
314
- </div>
315
- </div>
316
- ))}
317
- {error && <p className="alert alert-danger mb-0">{error}</p>}
318
- </div>
319
- </div>
320
- );
321
- }
322
-
323
- function bytesFromBase64Url(value: string): ArrayBuffer {
324
- const padded = value.replace(/-/g, "+").replace(/_/g, "/");
325
- const binary = atob(padded);
326
- const bytes = new Uint8Array(binary.length);
327
- for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
328
- return bytes.buffer;
329
- }
330
-
331
- function base64UrlFromBytes(buffer: ArrayBuffer): string {
332
- const bytes = new Uint8Array(buffer);
333
- let binary = "";
334
- for (const byte of bytes) binary += String.fromCharCode(byte);
335
- return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
336
- }
337
-
338
- function credentialJson(credential: PublicKeyCredential): Record<string, unknown> {
339
- const withJson = credential as PublicKeyCredential & { toJSON?: () => Record<string, unknown> };
340
- if (typeof withJson.toJSON === "function") return withJson.toJSON();
341
- const response = credential.response;
342
- const body: Record<string, string> = {
343
- clientDataJSON: base64UrlFromBytes(response.clientDataJSON),
344
- };
345
- if ("attestationObject" in response) {
346
- body.attestationObject = base64UrlFromBytes(
347
- (response as AuthenticatorAttestationResponse).attestationObject,
348
- );
349
- }
350
- if ("authenticatorData" in response && "signature" in response) {
351
- const assertion = response as AuthenticatorAssertionResponse;
352
- body.authenticatorData = base64UrlFromBytes(assertion.authenticatorData);
353
- body.signature = base64UrlFromBytes(assertion.signature);
354
- }
355
- return {
356
- id: credential.id,
357
- rawId: base64UrlFromBytes(credential.rawId),
358
- type: credential.type,
359
- response: body,
360
- clientExtensionResults: credential.getClientExtensionResults(),
361
- };
362
- }
363
-
364
- /** Add or remove an anchor by publishing the new list at the anchors already on record. */
365
- function AnchorsCard() {
366
- const info = usePromise(async () => (await identityTools()).getKeyInfo(), undefined, []);
367
- const [anchors, setAnchors] = useState<IAnchorRef[] | null>(null);
368
- const [url, setUrl] = useState("");
369
- const [busy, setBusy] = useState(false);
370
- const [error, setError] = useState<string | null>(null);
371
- const shown = anchors ?? info?.anchors?.onRecord ?? [];
372
-
373
- const publish = async (next: { type: "https" | "peers-services"; url: string }[]) => {
374
- setBusy(true);
375
- setError(null);
376
- try {
377
- const tools = await identityTools();
378
- const saved = await tools.setAnchors({ anchors: next });
379
- setAnchors(saved.anchors);
380
- setUrl("");
381
- } catch (err) {
382
- setError(err instanceof Error ? err.message : String(err));
383
- } finally {
384
- setBusy(false);
385
- }
386
- };
387
-
388
- return (
389
- <div className="card mb-3">
390
- <div className="card-body">
391
- <h5 className="card-title">Anchors</h5>
392
- <p className="text-muted">
393
- Peers ask these places before they believe a different key. Adding one publishes the new
394
- list at an anchor already here. An https anchor has to be updated first. Removing the last
395
- one means a lost key cannot be recovered.
396
- </p>
397
- {shown.length === 0 && <p>None yet.</p>}
398
- <ul className="list-unstyled">
399
- {shown.map((anchor) => (
400
- <li
401
- key={`${anchor.type}:${anchor.url}`}
402
- className="d-flex justify-content-between mb-2"
403
- >
404
- <code className="small text-break">{anchor.url}</code>
405
- <button
406
- type="button"
407
- className="btn btn-outline-danger btn-sm ms-2"
408
- disabled={busy}
409
- onClick={() => publish(shown.filter((item) => item.url !== anchor.url))}
410
- >
411
- Remove
412
- </button>
413
- </li>
414
- ))}
415
- </ul>
416
- <div className="input-group">
417
- <input
418
- className="form-control"
419
- placeholder="https://example.com/.well-known/peers-identity.json"
420
- value={url}
421
- onChange={(event) => setUrl(event.target.value)}
422
- />
423
- <button
424
- type="button"
425
- className="btn btn-outline-primary"
426
- disabled={busy || url.trim() === ""}
427
- onClick={() => publish([...shown, { type: "https", url: url.trim() }])}
428
- >
429
- {busy ? "Publishing..." : "Add"}
430
- </button>
431
- </div>
432
- {error && <p className="alert alert-danger mt-3 mb-0">{error}</p>}
433
- </div>
434
- </div>
435
- );
436
- }
437
-
438
- /** Bind a passkey that can open an anchor-write session. */
439
- function PasskeyCard() {
440
- // A passkey is bound to a website. The Electron renderer loads from a file, which has no
441
- // host; the registry refuses that origin and the browser would too.
442
- const onWebsite = typeof window !== "undefined" && /^https?:$/.test(window.location.protocol);
443
- const supported = onWebsite && typeof PublicKeyCredential !== "undefined";
444
- const [busy, setBusy] = useState(false);
445
- const [message, setMessage] = useState<string | null>(null);
446
- const [error, setError] = useState<string | null>(null);
447
-
448
- const run = async (action: "register" | "session") => {
449
- setBusy(true);
450
- setError(null);
451
- setMessage(null);
452
- try {
453
- const tools = await identityTools();
454
- const origin = window.location.origin;
455
- if (action === "register") {
456
- const started = await tools.passkeyRegisterStart({ origin });
457
- const options = started.options as PublicKeyCredentialCreationOptionsJSON;
458
- const credential = (await navigator.credentials.create({
459
- publicKey: {
460
- ...options,
461
- challenge: bytesFromBase64Url(options.challenge),
462
- user: { ...options.user, id: bytesFromBase64Url(options.user.id) },
463
- excludeCredentials: options.excludeCredentials?.map((item) => ({
464
- ...item,
465
- id: bytesFromBase64Url(item.id),
466
- })),
467
- },
468
- })) as PublicKeyCredential | null;
469
- if (!credential) throw new Error("No passkey was created");
470
- await tools.passkeyRegisterFinish({ response: credentialJson(credential) });
471
- setMessage("Passkey saved. It can open an anchor session without the signing key.");
472
- } else {
473
- const started = await tools.passkeyAuthStart({ origin });
474
- const options = started.options as PublicKeyCredentialRequestOptionsJSON;
475
- const credential = (await navigator.credentials.get({
476
- publicKey: {
477
- ...options,
478
- challenge: bytesFromBase64Url(options.challenge),
479
- allowCredentials: options.allowCredentials?.map((item) => ({
480
- ...item,
481
- id: bytesFromBase64Url(item.id),
482
- })),
483
- },
484
- })) as PublicKeyCredential | null;
485
- if (!credential) throw new Error("No passkey was used");
486
- const session = await tools.passkeyAuthFinish({ response: credentialJson(credential) });
487
- const minutes = Math.round(session.expiresIn / 60);
488
- setMessage(`Anchor session open for about ${minutes} minutes.`);
489
- }
490
- } catch (err) {
491
- setError(err instanceof Error ? err.message : String(err));
492
- } finally {
493
- setBusy(false);
494
- }
495
- };
496
-
497
- return (
498
- <div className="card mb-3">
499
- <div className="card-body">
500
- <h5 className="card-title">Passkey</h5>
501
- <p className="text-muted">
502
- A passkey opens the same short session as the recovery email. It does not replace the
503
- signing key. This page has to be served on the passkey's site.
504
- </p>
505
- {supported ? (
506
- <div className="d-flex gap-2">
507
- <button
508
- type="button"
509
- className="btn btn-outline-primary btn-sm"
510
- disabled={busy}
511
- onClick={() => run("register")}
512
- >
513
- {busy ? "Waiting..." : "Add a passkey"}
514
- </button>
515
- <button
516
- type="button"
517
- className="btn btn-outline-secondary btn-sm"
518
- disabled={busy}
519
- onClick={() => run("session")}
520
- >
521
- Use passkey
522
- </button>
523
- </div>
524
- ) : (
525
- <p className="mb-0">
526
- {onWebsite
527
- ? "This browser cannot create a passkey."
528
- : "This window is not a website, so it cannot hold a passkey. Open your account in the web app to add or use one."}
529
- </p>
530
- )}
531
- {message && <p className="mt-3 mb-0">{message}</p>}
532
- {error && <p className="alert alert-danger mt-3 mb-0">{error}</p>}
533
- </div>
534
- </div>
535
- );
536
- }
537
-
538
- type PublicKeyCredentialCreationOptionsJSON = {
539
- challenge: string;
540
- user: { id: string; name: string; displayName: string };
541
- excludeCredentials?: {
542
- id: string;
543
- type: PublicKeyCredentialType;
544
- transports?: AuthenticatorTransport[];
545
- }[];
546
- rp: PublicKeyCredentialRpEntity;
547
- pubKeyCredParams: PublicKeyCredentialParameters[];
548
- timeout?: number;
549
- attestation?: AttestationConveyancePreference;
550
- authenticatorSelection?: AuthenticatorSelectionCriteria;
551
- };
552
-
553
- type PublicKeyCredentialRequestOptionsJSON = {
554
- challenge: string;
555
- allowCredentials?: {
556
- id: string;
557
- type: PublicKeyCredentialType;
558
- transports?: AuthenticatorTransport[];
559
- }[];
560
- timeout?: number;
561
- rpId?: string;
562
- userVerification?: UserVerificationRequirement;
563
- };
564
-
565
- /** Current signing key, previous keys, on-record anchors, and manual rotation. */
566
- function SigningKeyCard() {
567
- const info = usePromise(() => identityTools().then((tools) => tools.getKeyInfo()));
568
- const [rotatedAway] = useObservable(identityRotatedAwayVar);
569
- const [manualDocument, setManualDocument] = useState<string | null>(null);
570
- const [busy, setBusy] = useState<"prepare" | "confirm" | null>(null);
571
- const [error, setError] = useState<string | null>(null);
572
-
573
- // Writes through every anchor that has a writer. When an anchor on record has none (an
574
- // https document) the host hands the document back first; confirm polls until every
575
- // anchor lists the new key. A committed rotation restarts the host.
576
- const prepare = async () => {
577
- setBusy("prepare");
578
- setError(null);
579
- try {
580
- const tools = await identityTools();
581
- const result = await tools.rotateKey({ publish: "anchors", confirm: false });
582
- if (result.committed) return;
583
- setManualDocument(JSON.stringify(result.manualDocument, null, 2));
584
- } catch (err) {
585
- setError(err instanceof Error ? err.message : String(err));
586
- } finally {
587
- setBusy(null);
588
- }
589
- };
590
-
591
- const confirm = async () => {
592
- setBusy("confirm");
593
- setError(null);
594
- try {
595
- const tools = await identityTools();
596
- await tools.rotateKey({ publish: "anchors", confirm: true });
597
- } catch (err) {
598
- setError(err instanceof Error ? err.message : String(err));
599
- setBusy(null);
600
- }
601
- };
602
-
603
- return (
604
- <div className="card mb-3">
605
- <div className="card-body">
606
- <h5 className="card-title d-flex align-items-center gap-2">
607
- Signing key
608
- <Tooltip markdownContent={ROTATE_INFO} />
609
- </h5>
610
- {rotatedAway && (
611
- <p className="alert alert-warning">
612
- This device holds an old key. Pair it again. The anchors confirmed{" "}
613
- <code>{rotatedAway.newPublicKey}</code>.
614
- </p>
615
- )}
616
- {!info ? (
617
- <LoadingIndicator />
618
- ) : (
619
- <>
620
- <KeyStatusList info={info} />
621
- {info.canRotate ? (
622
- <div className="mt-3">
623
- <button
624
- type="button"
625
- className="btn btn-outline-primary btn-sm"
626
- onClick={prepare}
627
- disabled={busy !== null}
628
- >
629
- {busy === "prepare" ? "Rotating..." : "Rotate signing key"}
630
- </button>
631
- {manualDocument && (
632
- <>
633
- <pre className="small mt-3 mb-2 text-break">{manualDocument}</pre>
634
- <p className="small text-muted">
635
- Place this document at every https anchor listed above, then confirm. This
636
- device waits until every anchor lists the new key before it changes anything.
637
- </p>
638
- <button
639
- type="button"
640
- className="btn btn-primary btn-sm"
641
- onClick={confirm}
642
- disabled={busy !== null}
643
- >
644
- {busy === "confirm" ? "Waiting for the anchors..." : "I've published this"}
645
- </button>
646
- </>
647
- )}
648
- </div>
649
- ) : (
650
- <p className="alert alert-secondary mt-3 mb-0">
651
- This host's identity is fixed at startup and cannot be rotated here.
652
- </p>
653
- )}
654
- {error && <p className="alert alert-danger mt-3 mb-0">{error}</p>}
655
- </>
656
- )}
657
- </div>
658
- </div>
659
- );
660
- }
661
-
662
- const RECOVERY_INFO = `### Recovery email
663
-
664
- This mailbox is how Peers Services agrees to publish a new key. The code proves you can read
665
- the mailbox. The signing key is still what other people verify.
666
-
667
- Without a recovery email, Peers Services refuses a new key. **Prepare manual rotation** still
668
- works if another anchor is on your profile. If you lose the signing key and have no recovery
669
- email, there is no way back into this account.`;
670
-
671
- /** Bind, confirm, and use a recovery email to publish a key rotation. */
672
- function RecoveryEmailCard() {
673
- const current = usePromise(() =>
674
- identityTools()
675
- .then((tools) => tools.getRecoveryEmail())
676
- .catch(() => ({ email: undefined })),
677
- );
678
- const [email, setEmail] = useState("");
679
- const [code, setCode] = useState("");
680
- const [bound, setBound] = useState<string | null | undefined>(undefined);
681
- const [busy, setBusy] = useState<string | null>(null);
682
- const [error, setError] = useState<string | null>(null);
683
- const [notice, setNotice] = useState<string | null>(null);
684
- const shown = bound === undefined ? (current?.email ?? "") : (bound ?? "");
685
-
686
- const run = async (label: string, action: () => Promise<string>) => {
687
- setBusy(label);
688
- setError(null);
689
- setNotice(null);
690
- try {
691
- setNotice(await action());
692
- } catch (err) {
693
- setError(err instanceof Error ? err.message : String(err));
694
- } finally {
695
- setBusy(null);
696
- }
697
- };
698
-
699
- return (
700
- <div className="card mb-3">
701
- <div className="card-body">
702
- <h5 className="card-title d-flex align-items-center gap-2">
703
- Recovery email
704
- <Tooltip markdownContent={RECOVERY_INFO} />
705
- </h5>
706
- {shown ? (
707
- <p className="mb-3">
708
- Bound to <strong>{shown}</strong>
709
- </p>
710
- ) : (
711
- <p className="text-muted mb-3">
712
- Optional. Bind one if you want Peers Services to accept a new signing key.
713
- </p>
714
- )}
715
- {!shown && (
716
- <input
717
- className="form-control form-control-sm mb-2"
718
- type="email"
719
- placeholder="you@example.com"
720
- value={email}
721
- onChange={(event) => setEmail(event.target.value)}
722
- disabled={busy !== null}
723
- />
724
- )}
725
- <input
726
- className="form-control form-control-sm mb-2"
727
- inputMode="numeric"
728
- placeholder="Code from the email"
729
- value={code}
730
- onChange={(event) => setCode(event.target.value)}
731
- disabled={busy !== null}
732
- />
733
- <div className="d-flex flex-wrap gap-2">
734
- {!shown && (
735
- <>
736
- <button
737
- type="button"
738
- className="btn btn-outline-primary btn-sm"
739
- disabled={busy !== null || email.trim() === ""}
740
- onClick={() =>
741
- run("bind", async () => {
742
- const tools = await identityTools();
743
- await tools.bindEmailStart({ email: email.trim() });
744
- return `A code was sent to ${email.trim()}.`;
745
- })
746
- }
747
- >
748
- {busy === "bind" ? "Sending..." : "Send bind code"}
749
- </button>
750
- <button
751
- type="button"
752
- className="btn btn-primary btn-sm"
753
- disabled={busy !== null || code.trim() === ""}
754
- onClick={() =>
755
- run("verify", async () => {
756
- const tools = await identityTools();
757
- const verified = await tools.bindEmailVerify({
758
- email: email.trim(),
759
- code: code.trim(),
760
- });
761
- setBound(verified.email);
762
- setCode("");
763
- return `Recovery email saved: ${verified.email}`;
764
- })
765
- }
766
- >
767
- {busy === "verify" ? "Checking..." : "Verify"}
768
- </button>
769
- </>
770
- )}
771
- {shown && (
772
- <>
773
- <button
774
- type="button"
775
- className="btn btn-outline-primary btn-sm"
776
- disabled={busy !== null}
777
- onClick={() =>
778
- run("session", async () => {
779
- const tools = await identityTools();
780
- await tools.startAnchorSession();
781
- return "A code was sent to the recovery email.";
782
- })
783
- }
784
- >
785
- {busy === "session" ? "Sending..." : "Send rotation code"}
786
- </button>
787
- <button
788
- type="button"
789
- className="btn btn-primary btn-sm"
790
- disabled={busy !== null || code.trim() === ""}
791
- onClick={() =>
792
- run("confirm", async () => {
793
- const tools = await identityTools();
794
- const session = await tools.confirmAnchorSession({ code: code.trim() });
795
- setCode("");
796
- const minutes = Math.max(1, Math.round(session.expiresIn / 60));
797
- return `Session open for about ${minutes} minutes. You can rotate now.`;
798
- })
799
- }
800
- >
801
- {busy === "confirm" ? "Checking..." : "Open session"}
802
- </button>
803
- <button
804
- type="button"
805
- className="btn btn-outline-secondary btn-sm"
806
- disabled={busy !== null}
807
- onClick={() =>
808
- run("rotate", async () => {
809
- const tools = await identityTools();
810
- const result = await tools.rotateKey({ publish: "anchors" });
811
- return `Rotated to ${result.newPublicKey}. This host will restart.`;
812
- })
813
- }
814
- >
815
- {busy === "rotate" ? "Rotating..." : "Rotate signing key"}
816
- </button>
817
- <button
818
- type="button"
819
- className="btn btn-link btn-sm"
820
- disabled={busy !== null}
821
- onClick={() =>
822
- run("unbind", async () => {
823
- const tools = await identityTools();
824
- await tools.unbindEmail();
825
- setBound(null);
826
- return "Recovery email removed.";
827
- })
828
- }
829
- >
830
- Remove
831
- </button>
832
- </>
833
- )}
834
- </div>
835
- {notice && <p className="alert alert-success mt-3 mb-0">{notice}</p>}
836
- {error && <p className="alert alert-danger mt-3 mb-0">{error}</p>}
837
- </div>
838
- </div>
839
- );
840
- }
841
-
842
- function KeyStatusList({ info }: { info: IIdentityKeyInfo }) {
843
- const registryStatus = (publicKey: string) =>
844
- info.registry?.keys?.find((k) => k.publicKey === publicKey)?.status;
845
- return (
846
- <div className="small">
847
- <div className="d-flex align-items-center gap-2 flex-wrap">
848
- <code className="text-break">{info.publicKey}</code>
849
- <span className="badge text-bg-success">current</span>
850
- <RegistryBadge status={registryStatus(info.publicKey)} />
851
- </div>
852
- {info.previousPublicKeys.length > 0 && (
853
- <ul className="list-unstyled mt-2 mb-0 text-muted">
854
- {[...info.previousPublicKeys].reverse().map((previous) => (
855
- <li key={previous.publicKey} className="d-flex align-items-center gap-2 flex-wrap">
856
- <code className="text-break">{previous.publicKey}</code>
857
- <span className="badge text-bg-secondary">
858
- {previous.reason === "rotation" || !previous.reason ? "retired" : "revoked"}
859
- </span>
860
- <RegistryBadge status={registryStatus(previous.publicKey)} />
861
- <span>{new Date(previous.replacedAt).toLocaleDateString()}</span>
862
- </li>
863
- ))}
864
- </ul>
865
- )}
866
- {info.anchors && info.anchors.onRecord.length > 0 && (
867
- <ul className="list-unstyled mt-2 mb-0 text-muted">
868
- {info.anchors.onRecord.map((anchor) => {
869
- const view = info.anchors?.views.find((item) => item.url === anchor.url);
870
- return (
871
- <li key={anchor.url}>
872
- <span className="badge text-bg-light me-2">{anchor.type}</span>
873
- <code className="text-break">{anchor.url}</code>
874
- {view?.error && <span className="ms-2">{view.error}</span>}
875
- </li>
876
- );
877
- })}
878
- </ul>
879
- )}
880
- <div className="text-muted mt-2">
881
- {info.registry
882
- ? info.registry.error
883
- ? `Key registry at ${info.registry.url} is not reachable: ${info.registry.error}`
884
- : info.registry.keys
885
- ? `Key registry: ${info.registry.url}`
886
- : `The key registry at ${info.registry.url} has no record of this account yet; it is created on the first rotation.`
887
- : "No key registry is configured (peers-services disabled)."}
888
- </div>
889
- </div>
890
- );
891
- }
892
-
893
- function RegistryBadge({ status }: { status?: string }) {
894
- if (!status) return null;
895
- const tone =
896
- status === "active"
897
- ? "text-bg-primary"
898
- : status === "revoked"
899
- ? "text-bg-danger"
900
- : "text-bg-light";
901
- return <span className={`badge ${tone}`}>registry: {status}</span>;
902
- }
903
-
904
- function TechnicalValue(props: { label: string; value: string; help: string }) {
905
- const [copied, setCopied] = useState(false);
906
- const copy = async () => {
907
- if (await copyToClipboard(props.value)) {
908
- setCopied(true);
909
- window.setTimeout(() => setCopied(false), 1500);
910
- }
911
- };
912
- return (
913
- <div className="border-top py-3">
914
- <div className="d-flex align-items-center gap-2 mb-1">
915
- <strong className="small">{props.label}</strong>
916
- <Tooltip markdownContent={props.help} />
917
- <button
918
- type="button"
919
- className="btn btn-link btn-sm ms-auto py-0"
920
- onClick={() => void copy()}
921
- aria-label={`Copy ${props.label}`}
922
- >
923
- <i className={`bi ${copied ? "bi-check-lg" : "bi-copy"} me-1`} />
924
- {copied ? "Copied" : "Copy"}
925
- </button>
926
- </div>
927
- <code className="small text-break user-select-all">{props.value}</code>
928
- </div>
929
- );
930
- }
931
-
932
- function profileInitials(name: string): string {
933
- return (
934
- name
935
- .trim()
936
- .split(/\s+/)
937
- .filter(Boolean)
938
- .slice(0, 2)
939
- .map((part) => part[0]?.toLocaleUpperCase())
940
- .join("") || "?"
941
- );
942
- }