@peers-app/peers-ui 0.27.3 → 0.27.4

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 (309) hide show
  1. package/dist/screens/network-viewer/network-viewer-ipc.d.ts +3 -1
  2. package/dist/screens/network-viewer/network-viewer-ipc.js +6 -0
  3. package/dist/screens/network-viewer/sync-recovery.js +103 -1
  4. package/dist/screens/variables/variable-details.js +2 -1
  5. package/dist/tabs-layout/network-status-indicator.js +26 -12
  6. package/dist/tabs-layout/use-network-status.d.ts +6 -1
  7. package/dist/tabs-layout/use-network-status.js +27 -4
  8. package/package.json +9 -3
  9. package/.claude/settings.local.json +0 -34
  10. package/.github/README.md +0 -52
  11. package/.github/workflows/delete-all-artifacts.yml +0 -49
  12. package/.github/workflows/publish.yml +0 -51
  13. package/babel.config.js +0 -7
  14. package/biome.json +0 -198
  15. package/dist/app-updates/app-updates-consumer.test.d.ts +0 -1
  16. package/dist/app-updates/app-updates-consumer.test.js +0 -56
  17. package/dist/app-updates/app-updates-settings.test.d.ts +0 -1
  18. package/dist/app-updates/app-updates-settings.test.js +0 -93
  19. package/dist/app-updates/remote-app-update-panel.test.d.ts +0 -1
  20. package/dist/app-updates/remote-app-update-panel.test.js +0 -165
  21. package/dist/components/access-level-field.test.d.ts +0 -1
  22. package/dist/components/access-level-field.test.js +0 -28
  23. package/dist/components/message-logs/tool-access-approval-card.test.d.ts +0 -1
  24. package/dist/components/message-logs/tool-access-approval-card.test.js +0 -102
  25. package/dist/components/responsive-section-nav.test.d.ts +0 -1
  26. package/dist/components/responsive-section-nav.test.js +0 -45
  27. package/dist/globals-hash.test.d.ts +0 -1
  28. package/dist/globals-hash.test.js +0 -16
  29. package/dist/hooks/use-observable-state.test.d.ts +0 -1
  30. package/dist/hooks/use-observable-state.test.js +0 -64
  31. package/dist/hooks/use-observable.test.d.ts +0 -1
  32. package/dist/hooks/use-observable.test.js +0 -191
  33. package/dist/hooks/use-rows.test.d.ts +0 -1
  34. package/dist/hooks/use-rows.test.js +0 -175
  35. package/dist/hooks/use-tool.test.d.ts +0 -1
  36. package/dist/hooks/use-tool.test.js +0 -190
  37. package/dist/host-shell/host-shell-consumer.test.d.ts +0 -1
  38. package/dist/host-shell/host-shell-consumer.test.js +0 -28
  39. package/dist/root-layout/root-layout-host.test.d.ts +0 -1
  40. package/dist/root-layout/root-layout-host.test.js +0 -86
  41. package/dist/screens/account/account-screen.test.d.ts +0 -1
  42. package/dist/screens/account/account-screen.test.js +0 -51
  43. package/dist/screens/assistants/assistant-config.test.d.ts +0 -1
  44. package/dist/screens/assistants/assistant-config.test.js +0 -57
  45. package/dist/screens/contacts/contact-list.test.d.ts +0 -1
  46. package/dist/screens/contacts/contact-list.test.js +0 -48
  47. package/dist/screens/device-pairing/device-pairing.test.d.ts +0 -1
  48. package/dist/screens/device-pairing/device-pairing.test.js +0 -119
  49. package/dist/screens/devices/devices-list.test.d.ts +0 -1
  50. package/dist/screens/devices/devices-list.test.js +0 -53
  51. package/dist/screens/groups/group-list.test.d.ts +0 -1
  52. package/dist/screens/groups/group-list.test.js +0 -38
  53. package/dist/screens/identity/identity-routes.test.d.ts +0 -1
  54. package/dist/screens/identity/identity-routes.test.js +0 -51
  55. package/dist/screens/identity/identity-screen.test.d.ts +0 -1
  56. package/dist/screens/identity/identity-screen.test.js +0 -103
  57. package/dist/screens/invites/accept-invite.test.d.ts +0 -1
  58. package/dist/screens/invites/accept-invite.test.js +0 -103
  59. package/dist/screens/invites/activity-view.test.d.ts +0 -1
  60. package/dist/screens/invites/activity-view.test.js +0 -108
  61. package/dist/screens/invites/invite-classify.test.d.ts +0 -1
  62. package/dist/screens/invites/invite-classify.test.js +0 -115
  63. package/dist/screens/network-viewer/network-viewer-ipc.test.d.ts +0 -1
  64. package/dist/screens/network-viewer/network-viewer-ipc.test.js +0 -61
  65. package/dist/screens/network-viewer/network-viewer.section.test.d.ts +0 -1
  66. package/dist/screens/network-viewer/network-viewer.section.test.js +0 -63
  67. package/dist/screens/network-viewer/sync-recovery.test.d.ts +0 -1
  68. package/dist/screens/network-viewer/sync-recovery.test.js +0 -104
  69. package/dist/screens/packages/package-helpers.test.d.ts +0 -1
  70. package/dist/screens/packages/package-helpers.test.js +0 -53
  71. package/dist/screens/packages/package-runtime-status.test.d.ts +0 -1
  72. package/dist/screens/packages/package-runtime-status.test.js +0 -50
  73. package/dist/screens/packages/packages-ipc.test.d.ts +0 -1
  74. package/dist/screens/packages/packages-ipc.test.js +0 -43
  75. package/dist/screens/settings/settings-page.test.d.ts +0 -1
  76. package/dist/screens/settings/settings-page.test.js +0 -19
  77. package/dist/screens/tools/tool-access-ipc.test.d.ts +0 -1
  78. package/dist/screens/tools/tool-access-ipc.test.js +0 -40
  79. package/dist/screens/tools/tool-catalog-ipc.test.d.ts +0 -1
  80. package/dist/screens/tools/tool-catalog-ipc.test.js +0 -52
  81. package/dist/tabs-layout/network-status-indicator.test.d.ts +0 -1
  82. package/dist/tabs-layout/network-status-indicator.test.js +0 -665
  83. package/dist/tabs-layout/tabs-state.test.d.ts +0 -1
  84. package/dist/tabs-layout/tabs-state.test.js +0 -110
  85. package/dist/tabs-layout/use-network-status.test.d.ts +0 -1
  86. package/dist/tabs-layout/use-network-status.test.js +0 -45
  87. package/dist/ui-router/ui-loader.test.d.ts +0 -1
  88. package/dist/ui-router/ui-loader.test.js +0 -91
  89. package/jest.config.js +0 -25
  90. package/release-guard.js +0 -10
  91. package/src/app-updates/app-update-controls.tsx +0 -73
  92. package/src/app-updates/app-updates-consumer.test.ts +0 -83
  93. package/src/app-updates/app-updates-consumer.ts +0 -121
  94. package/src/app-updates/app-updates-settings.test.tsx +0 -109
  95. package/src/app-updates/app-updates-settings.tsx +0 -136
  96. package/src/app-updates/remote-app-update-panel.test.tsx +0 -205
  97. package/src/app-updates/remote-app-update-panel.tsx +0 -210
  98. package/src/app-updates/use-app-update-status.ts +0 -16
  99. package/src/command-palette/command-palette-ui.tsx +0 -564
  100. package/src/command-palette/command-palette.ts +0 -242
  101. package/src/components/access-level-field.test.tsx +0 -32
  102. package/src/components/access-level-field.tsx +0 -68
  103. package/src/components/checkbox.tsx +0 -25
  104. package/src/components/group-switcher.tsx +0 -526
  105. package/src/components/input-date.tsx +0 -29
  106. package/src/components/input-number.tsx +0 -83
  107. package/src/components/input.tsx +0 -25
  108. package/src/components/inverse-lazy-list.tsx +0 -107
  109. package/src/components/io-schema-values.tsx +0 -103
  110. package/src/components/io-schema.tsx +0 -259
  111. package/src/components/lazy-list.tsx +0 -115
  112. package/src/components/list-screen.tsx +0 -126
  113. package/src/components/loading-indicator.tsx +0 -7
  114. package/src/components/markdown-editor/autolink-plugin.tsx +0 -29
  115. package/src/components/markdown-editor/editor-inline.tsx +0 -9
  116. package/src/components/markdown-editor/editor.tsx +0 -158
  117. package/src/components/markdown-editor/markdown-plugin.tsx +0 -202
  118. package/src/components/markdown-editor/mention-node.ts +0 -204
  119. package/src/components/markdown-editor/mentions-plugin.tsx +0 -354
  120. package/src/components/markdown-editor/move-line-plugin.tsx +0 -186
  121. package/src/components/markdown-editor/select-line-boundary-plugin.tsx +0 -95
  122. package/src/components/markdown-editor/theme.ts +0 -46
  123. package/src/components/markdown-editor/toolbar.tsx +0 -269
  124. package/src/components/markdown-with-mentions.tsx +0 -198
  125. package/src/components/message-logs/message-logs.tsx +0 -523
  126. package/src/components/message-logs/tool-access-approval-card.test.tsx +0 -127
  127. package/src/components/message-logs/tool-access-approval-card.tsx +0 -178
  128. package/src/components/messages/avatar.tsx +0 -171
  129. package/src/components/messages/channel-message-list.tsx +0 -189
  130. package/src/components/messages/channel-view.tsx +0 -92
  131. package/src/components/messages/message-compose.tsx +0 -192
  132. package/src/components/messages/message-display.tsx +0 -231
  133. package/src/components/messages/thread-message-list.tsx +0 -135
  134. package/src/components/qr-scanner.tsx +0 -180
  135. package/src/components/responsive-section-nav.test.tsx +0 -88
  136. package/src/components/responsive-section-nav.tsx +0 -92
  137. package/src/components/router.tsx +0 -175
  138. package/src/components/save-button.tsx +0 -134
  139. package/src/components/sortable-list.tsx +0 -130
  140. package/src/components/tabs.tsx +0 -76
  141. package/src/components/tooltip.tsx +0 -43
  142. package/src/components/trust-level-badge.tsx +0 -44
  143. package/src/components/trust-level-dropdown.tsx +0 -136
  144. package/src/components/typeahead.tsx +0 -357
  145. package/src/globals-hash.test.ts +0 -19
  146. package/src/globals.tsx +0 -226
  147. package/src/hooks/index.ts +0 -8
  148. package/src/hooks/use-host-contract.ts +0 -75
  149. package/src/hooks/use-observable-state.test.tsx +0 -101
  150. package/src/hooks/use-observable-state.ts +0 -29
  151. package/src/hooks/use-observable.test.tsx +0 -258
  152. package/src/hooks/use-observable.ts +0 -67
  153. package/src/hooks/use-on-screen.ts +0 -24
  154. package/src/hooks/use-promise.ts +0 -34
  155. package/src/hooks/use-rows.test.tsx +0 -196
  156. package/src/hooks/use-rows.ts +0 -120
  157. package/src/hooks/use-subscription.ts +0 -24
  158. package/src/hooks/use-tool.test.tsx +0 -225
  159. package/src/hooks/use-tool.ts +0 -116
  160. package/src/host-shell/host-shell-consumer.test.ts +0 -32
  161. package/src/host-shell/host-shell-consumer.ts +0 -53
  162. package/src/index.tsx +0 -42
  163. package/src/mention-configs.ts +0 -185
  164. package/src/operator-console/operator-console-state.ts +0 -119
  165. package/src/operator-console/operator-console.tsx +0 -233
  166. package/src/peers-services-registration.ts +0 -6
  167. package/src/root-layout/global-overlays.ts +0 -50
  168. package/src/root-layout/layout-picker.tsx +0 -47
  169. package/src/root-layout/layouts/full-screen-layout.tsx +0 -144
  170. package/src/root-layout/layouts/tabs-with-console-layout.tsx +0 -260
  171. package/src/root-layout/root-layout-host.test.tsx +0 -108
  172. package/src/root-layout/root-layout-host.tsx +0 -147
  173. package/src/root-layout/root-layout-registry.ts +0 -51
  174. package/src/root-layout/root-layout-state.ts +0 -55
  175. package/src/root-layout/root-layout.ts +0 -32
  176. package/src/screens/account/account-screen.test.tsx +0 -65
  177. package/src/screens/account/account-screen.tsx +0 -942
  178. package/src/screens/assistants/assistant-config.test.tsx +0 -54
  179. package/src/screens/assistants/assistant-config.tsx +0 -121
  180. package/src/screens/assistants/assistant-details.tsx +0 -89
  181. package/src/screens/assistants/assistant-info.tsx +0 -66
  182. package/src/screens/assistants/assistant-list.tsx +0 -145
  183. package/src/screens/assistants/assistant-tools.tsx +0 -82
  184. package/src/screens/console-logs/console-logs-list.tsx +0 -374
  185. package/src/screens/console-logs/log-display.tsx +0 -137
  186. package/src/screens/console-logs/log-filters.tsx +0 -64
  187. package/src/screens/console-logs/mobile-log-card.tsx +0 -161
  188. package/src/screens/console-logs/resizable-table-header.tsx +0 -110
  189. package/src/screens/contacts/contact-details.tsx +0 -123
  190. package/src/screens/contacts/contact-list.test.tsx +0 -59
  191. package/src/screens/contacts/contact-list.tsx +0 -286
  192. package/src/screens/contacts/index.ts +0 -2
  193. package/src/screens/data-explorer/data-explorer.tsx +0 -633
  194. package/src/screens/data-explorer/index.ts +0 -6
  195. package/src/screens/data-explorer/query-executor.tsx +0 -520
  196. package/src/screens/device-pairing/device-pairing.test.tsx +0 -143
  197. package/src/screens/device-pairing/device-pairing.tsx +0 -461
  198. package/src/screens/device-pairing/identity-ipc.ts +0 -25
  199. package/src/screens/device-pairing/index.ts +0 -2
  200. package/src/screens/device-pairing/use-device-pairing-state.ts +0 -15
  201. package/src/screens/devices/devices-list.test.tsx +0 -68
  202. package/src/screens/devices/devices-list.tsx +0 -365
  203. package/src/screens/devices/index.ts +0 -1
  204. package/src/screens/groups/group-details.tsx +0 -160
  205. package/src/screens/groups/group-invite-panel.tsx +0 -311
  206. package/src/screens/groups/group-list.test.tsx +0 -44
  207. package/src/screens/groups/group-list.tsx +0 -317
  208. package/src/screens/groups/group-members.tsx +0 -343
  209. package/src/screens/groups/index.ts +0 -3
  210. package/src/screens/identity/identity-routes.test.ts +0 -70
  211. package/src/screens/identity/identity-routes.ts +0 -301
  212. package/src/screens/identity/identity-screen.test.tsx +0 -138
  213. package/src/screens/identity/identity-screen.tsx +0 -282
  214. package/src/screens/identity/index.ts +0 -2
  215. package/src/screens/identity/invite-contact-panel.tsx +0 -45
  216. package/src/screens/identity/profile-invite.tsx +0 -93
  217. package/src/screens/invites/accept-invite.test.tsx +0 -115
  218. package/src/screens/invites/accept-invite.tsx +0 -318
  219. package/src/screens/invites/activity-view.test.tsx +0 -127
  220. package/src/screens/invites/activity-view.tsx +0 -162
  221. package/src/screens/invites/index.ts +0 -6
  222. package/src/screens/invites/invite-capture.ts +0 -104
  223. package/src/screens/invites/invite-classify.test.ts +0 -129
  224. package/src/screens/invites/invite-share.tsx +0 -104
  225. package/src/screens/invites/invites-ipc.ts +0 -25
  226. package/src/screens/invites/pending-invites-list.tsx +0 -256
  227. package/src/screens/network-viewer/connection-troubleshooter.tsx +0 -395
  228. package/src/screens/network-viewer/cpu-usage-graph.tsx +0 -294
  229. package/src/screens/network-viewer/device-details-modal.tsx +0 -339
  230. package/src/screens/network-viewer/group-details-modal.tsx +0 -421
  231. package/src/screens/network-viewer/index.ts +0 -8
  232. package/src/screens/network-viewer/network-viewer-ipc.test.ts +0 -72
  233. package/src/screens/network-viewer/network-viewer-ipc.ts +0 -39
  234. package/src/screens/network-viewer/network-viewer-section.ts +0 -12
  235. package/src/screens/network-viewer/network-viewer.section.test.tsx +0 -68
  236. package/src/screens/network-viewer/network-viewer.tsx +0 -708
  237. package/src/screens/network-viewer/remote-logs-panel.tsx +0 -190
  238. package/src/screens/network-viewer/sync-recovery.test.tsx +0 -114
  239. package/src/screens/network-viewer/sync-recovery.tsx +0 -461
  240. package/src/screens/network-viewer/usage-graph.tsx +0 -455
  241. package/src/screens/packages/local-folder-grants-panel.tsx +0 -290
  242. package/src/screens/packages/local-folders-ipc.ts +0 -28
  243. package/src/screens/packages/package-details.tsx +0 -166
  244. package/src/screens/packages/package-helpers.test.ts +0 -57
  245. package/src/screens/packages/package-helpers.ts +0 -137
  246. package/src/screens/packages/package-info.tsx +0 -354
  247. package/src/screens/packages/package-list.tsx +0 -519
  248. package/src/screens/packages/package-network-panel.tsx +0 -82
  249. package/src/screens/packages/package-new-local.tsx +0 -100
  250. package/src/screens/packages/package-runtime-status.test.tsx +0 -58
  251. package/src/screens/packages/package-runtime-status.tsx +0 -68
  252. package/src/screens/packages/package-versions.tsx +0 -298
  253. package/src/screens/packages/packages-ipc.test.ts +0 -47
  254. package/src/screens/packages/packages-ipc.ts +0 -125
  255. package/src/screens/peer-types/peer-type-details.tsx +0 -172
  256. package/src/screens/peer-types/peer-type-list.tsx +0 -64
  257. package/src/screens/search/global-search.tsx +0 -406
  258. package/src/screens/settings/color-mode-dropdown.tsx +0 -82
  259. package/src/screens/settings/settings-page.test.tsx +0 -24
  260. package/src/screens/settings/settings-page.tsx +0 -426
  261. package/src/screens/setup-user.tsx +0 -355
  262. package/src/screens/tools/tool-access-ipc.test.ts +0 -55
  263. package/src/screens/tools/tool-access-ipc.ts +0 -27
  264. package/src/screens/tools/tool-catalog-ipc.test.ts +0 -52
  265. package/src/screens/tools/tool-catalog-ipc.ts +0 -51
  266. package/src/screens/tools/tool-details.tsx +0 -52
  267. package/src/screens/tools/tool-info.tsx +0 -45
  268. package/src/screens/tools/tool-list.tsx +0 -106
  269. package/src/screens/tools/tool-schema.tsx +0 -44
  270. package/src/screens/tools/tool-test-details.tsx +0 -89
  271. package/src/screens/tools/tool-test-list.tsx +0 -73
  272. package/src/screens/variables/variable-details.tsx +0 -289
  273. package/src/screens/variables/variable-list.tsx +0 -110
  274. package/src/screens/welcome-modal.tsx +0 -294
  275. package/src/screens/workflows/workflow-details.tsx +0 -115
  276. package/src/screens/workflows/workflow-info.tsx +0 -22
  277. package/src/screens/workflows/workflow-instructions.tsx +0 -134
  278. package/src/screens/workflows/workflow-list.tsx +0 -117
  279. package/src/setupTests.ts +0 -38
  280. package/src/system-apps/assistants.app.ts +0 -7
  281. package/src/system-apps/console-logs.app.ts +0 -7
  282. package/src/system-apps/data-explorer.app.ts +0 -8
  283. package/src/system-apps/identity.app.ts +0 -9
  284. package/src/system-apps/index.ts +0 -77
  285. package/src/system-apps/mobile-settings.app.ts +0 -7
  286. package/src/system-apps/network-viewer.app.ts +0 -8
  287. package/src/system-apps/packages.app.ts +0 -7
  288. package/src/system-apps/search.app.ts +0 -8
  289. package/src/system-apps/settings.app.ts +0 -7
  290. package/src/system-apps/threads.app.ts +0 -7
  291. package/src/system-apps/tools.app.ts +0 -7
  292. package/src/system-apps/types.app.ts +0 -7
  293. package/src/system-apps/variables.app.ts +0 -7
  294. package/src/system-apps/workflows.app.ts +0 -7
  295. package/src/tabs-layout/network-status-indicator.test.tsx +0 -799
  296. package/src/tabs-layout/network-status-indicator.tsx +0 -756
  297. package/src/tabs-layout/tabs-layout.tsx +0 -857
  298. package/src/tabs-layout/tabs-state.test.ts +0 -152
  299. package/src/tabs-layout/tabs-state.ts +0 -587
  300. package/src/tabs-layout/use-network-status.test.tsx +0 -58
  301. package/src/tabs-layout/use-network-status.ts +0 -68
  302. package/src/ui-defaults/index.ts +0 -2
  303. package/src/ui-defaults/list-screen.tsx +0 -94
  304. package/src/ui-defaults/markdown-field.tsx +0 -87
  305. package/src/ui-router/routes-loader.ts +0 -193
  306. package/src/ui-router/ui-loader.test.tsx +0 -108
  307. package/src/ui-router/ui-loader.tsx +0 -606
  308. package/src/utils.ts +0 -95
  309. 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
- }