@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.
- package/dist/screens/network-viewer/group-details-modal.d.ts +4 -0
- package/dist/screens/network-viewer/group-details-modal.js +6 -46
- package/dist/screens/network-viewer/network-viewer-ipc.d.ts +3 -2
- package/dist/screens/network-viewer/network-viewer-ipc.js +6 -0
- package/dist/screens/network-viewer/sync-recovery.js +103 -1
- package/dist/screens/variables/variable-details.js +2 -1
- package/dist/tabs-layout/network-status-indicator.js +26 -12
- package/dist/tabs-layout/use-network-status.d.ts +6 -1
- package/dist/tabs-layout/use-network-status.js +27 -4
- package/package.json +9 -3
- package/.claude/settings.local.json +0 -34
- package/.github/README.md +0 -52
- package/.github/workflows/delete-all-artifacts.yml +0 -49
- package/.github/workflows/publish.yml +0 -51
- package/babel.config.js +0 -7
- package/biome.json +0 -198
- package/dist/app-updates/app-updates-consumer.test.d.ts +0 -1
- package/dist/app-updates/app-updates-consumer.test.js +0 -56
- package/dist/app-updates/app-updates-settings.test.d.ts +0 -1
- package/dist/app-updates/app-updates-settings.test.js +0 -93
- package/dist/app-updates/remote-app-update-panel.test.d.ts +0 -1
- package/dist/app-updates/remote-app-update-panel.test.js +0 -165
- package/dist/components/access-level-field.test.d.ts +0 -1
- package/dist/components/access-level-field.test.js +0 -28
- package/dist/components/message-logs/tool-access-approval-card.test.d.ts +0 -1
- package/dist/components/message-logs/tool-access-approval-card.test.js +0 -102
- package/dist/components/responsive-section-nav.test.d.ts +0 -1
- package/dist/components/responsive-section-nav.test.js +0 -45
- package/dist/globals-hash.test.d.ts +0 -1
- package/dist/globals-hash.test.js +0 -16
- package/dist/hooks/use-observable-state.test.d.ts +0 -1
- package/dist/hooks/use-observable-state.test.js +0 -64
- package/dist/hooks/use-observable.test.d.ts +0 -1
- package/dist/hooks/use-observable.test.js +0 -191
- package/dist/hooks/use-rows.test.d.ts +0 -1
- package/dist/hooks/use-rows.test.js +0 -175
- package/dist/hooks/use-tool.test.d.ts +0 -1
- package/dist/hooks/use-tool.test.js +0 -190
- package/dist/host-shell/host-shell-consumer.test.d.ts +0 -1
- package/dist/host-shell/host-shell-consumer.test.js +0 -28
- package/dist/root-layout/root-layout-host.test.d.ts +0 -1
- package/dist/root-layout/root-layout-host.test.js +0 -86
- package/dist/screens/account/account-screen.test.d.ts +0 -1
- package/dist/screens/account/account-screen.test.js +0 -51
- package/dist/screens/assistants/assistant-config.test.d.ts +0 -1
- package/dist/screens/assistants/assistant-config.test.js +0 -57
- package/dist/screens/contacts/contact-list.test.d.ts +0 -1
- package/dist/screens/contacts/contact-list.test.js +0 -48
- package/dist/screens/device-pairing/device-pairing.test.d.ts +0 -1
- package/dist/screens/device-pairing/device-pairing.test.js +0 -119
- package/dist/screens/devices/devices-list.test.d.ts +0 -1
- package/dist/screens/devices/devices-list.test.js +0 -53
- package/dist/screens/groups/group-list.test.d.ts +0 -1
- package/dist/screens/groups/group-list.test.js +0 -38
- package/dist/screens/identity/identity-routes.test.d.ts +0 -1
- package/dist/screens/identity/identity-routes.test.js +0 -51
- package/dist/screens/identity/identity-screen.test.d.ts +0 -1
- package/dist/screens/identity/identity-screen.test.js +0 -103
- package/dist/screens/invites/accept-invite.test.d.ts +0 -1
- package/dist/screens/invites/accept-invite.test.js +0 -103
- package/dist/screens/invites/activity-view.test.d.ts +0 -1
- package/dist/screens/invites/activity-view.test.js +0 -108
- package/dist/screens/invites/invite-classify.test.d.ts +0 -1
- package/dist/screens/invites/invite-classify.test.js +0 -115
- package/dist/screens/network-viewer/network-viewer-ipc.test.d.ts +0 -1
- package/dist/screens/network-viewer/network-viewer-ipc.test.js +0 -61
- package/dist/screens/network-viewer/network-viewer.section.test.d.ts +0 -1
- package/dist/screens/network-viewer/network-viewer.section.test.js +0 -63
- package/dist/screens/network-viewer/sync-recovery.test.d.ts +0 -1
- package/dist/screens/network-viewer/sync-recovery.test.js +0 -104
- package/dist/screens/packages/package-helpers.test.d.ts +0 -1
- package/dist/screens/packages/package-helpers.test.js +0 -53
- package/dist/screens/packages/package-runtime-status.test.d.ts +0 -1
- package/dist/screens/packages/package-runtime-status.test.js +0 -50
- package/dist/screens/packages/packages-ipc.test.d.ts +0 -1
- package/dist/screens/packages/packages-ipc.test.js +0 -43
- package/dist/screens/settings/settings-page.test.d.ts +0 -1
- package/dist/screens/settings/settings-page.test.js +0 -19
- package/dist/screens/tools/tool-access-ipc.test.d.ts +0 -1
- package/dist/screens/tools/tool-access-ipc.test.js +0 -40
- package/dist/screens/tools/tool-catalog-ipc.test.d.ts +0 -1
- package/dist/screens/tools/tool-catalog-ipc.test.js +0 -52
- package/dist/tabs-layout/network-status-indicator.test.d.ts +0 -1
- package/dist/tabs-layout/network-status-indicator.test.js +0 -665
- package/dist/tabs-layout/tabs-state.test.d.ts +0 -1
- package/dist/tabs-layout/tabs-state.test.js +0 -110
- package/dist/tabs-layout/use-network-status.test.d.ts +0 -1
- package/dist/tabs-layout/use-network-status.test.js +0 -45
- package/dist/ui-router/ui-loader.test.d.ts +0 -1
- package/dist/ui-router/ui-loader.test.js +0 -91
- package/jest.config.js +0 -25
- package/release-guard.js +0 -10
- package/src/app-updates/app-update-controls.tsx +0 -73
- package/src/app-updates/app-updates-consumer.test.ts +0 -83
- package/src/app-updates/app-updates-consumer.ts +0 -121
- package/src/app-updates/app-updates-settings.test.tsx +0 -109
- package/src/app-updates/app-updates-settings.tsx +0 -136
- package/src/app-updates/remote-app-update-panel.test.tsx +0 -205
- package/src/app-updates/remote-app-update-panel.tsx +0 -210
- package/src/app-updates/use-app-update-status.ts +0 -16
- package/src/command-palette/command-palette-ui.tsx +0 -564
- package/src/command-palette/command-palette.ts +0 -242
- package/src/components/access-level-field.test.tsx +0 -32
- package/src/components/access-level-field.tsx +0 -68
- package/src/components/checkbox.tsx +0 -25
- package/src/components/group-switcher.tsx +0 -526
- package/src/components/input-date.tsx +0 -29
- package/src/components/input-number.tsx +0 -83
- package/src/components/input.tsx +0 -25
- package/src/components/inverse-lazy-list.tsx +0 -107
- package/src/components/io-schema-values.tsx +0 -103
- package/src/components/io-schema.tsx +0 -259
- package/src/components/lazy-list.tsx +0 -115
- package/src/components/list-screen.tsx +0 -126
- package/src/components/loading-indicator.tsx +0 -7
- package/src/components/markdown-editor/autolink-plugin.tsx +0 -29
- package/src/components/markdown-editor/editor-inline.tsx +0 -9
- package/src/components/markdown-editor/editor.tsx +0 -158
- package/src/components/markdown-editor/markdown-plugin.tsx +0 -202
- package/src/components/markdown-editor/mention-node.ts +0 -204
- package/src/components/markdown-editor/mentions-plugin.tsx +0 -354
- package/src/components/markdown-editor/move-line-plugin.tsx +0 -186
- package/src/components/markdown-editor/select-line-boundary-plugin.tsx +0 -95
- package/src/components/markdown-editor/theme.ts +0 -46
- package/src/components/markdown-editor/toolbar.tsx +0 -269
- package/src/components/markdown-with-mentions.tsx +0 -198
- package/src/components/message-logs/message-logs.tsx +0 -523
- package/src/components/message-logs/tool-access-approval-card.test.tsx +0 -127
- package/src/components/message-logs/tool-access-approval-card.tsx +0 -178
- package/src/components/messages/avatar.tsx +0 -171
- package/src/components/messages/channel-message-list.tsx +0 -189
- package/src/components/messages/channel-view.tsx +0 -92
- package/src/components/messages/message-compose.tsx +0 -192
- package/src/components/messages/message-display.tsx +0 -231
- package/src/components/messages/thread-message-list.tsx +0 -135
- package/src/components/qr-scanner.tsx +0 -180
- package/src/components/responsive-section-nav.test.tsx +0 -88
- package/src/components/responsive-section-nav.tsx +0 -92
- package/src/components/router.tsx +0 -175
- package/src/components/save-button.tsx +0 -134
- package/src/components/sortable-list.tsx +0 -130
- package/src/components/tabs.tsx +0 -76
- package/src/components/tooltip.tsx +0 -43
- package/src/components/trust-level-badge.tsx +0 -44
- package/src/components/trust-level-dropdown.tsx +0 -136
- package/src/components/typeahead.tsx +0 -357
- package/src/globals-hash.test.ts +0 -19
- package/src/globals.tsx +0 -226
- package/src/hooks/index.ts +0 -8
- package/src/hooks/use-host-contract.ts +0 -75
- package/src/hooks/use-observable-state.test.tsx +0 -101
- package/src/hooks/use-observable-state.ts +0 -29
- package/src/hooks/use-observable.test.tsx +0 -258
- package/src/hooks/use-observable.ts +0 -67
- package/src/hooks/use-on-screen.ts +0 -24
- package/src/hooks/use-promise.ts +0 -34
- package/src/hooks/use-rows.test.tsx +0 -196
- package/src/hooks/use-rows.ts +0 -120
- package/src/hooks/use-subscription.ts +0 -24
- package/src/hooks/use-tool.test.tsx +0 -225
- package/src/hooks/use-tool.ts +0 -116
- package/src/host-shell/host-shell-consumer.test.ts +0 -32
- package/src/host-shell/host-shell-consumer.ts +0 -53
- package/src/index.tsx +0 -42
- package/src/mention-configs.ts +0 -185
- package/src/operator-console/operator-console-state.ts +0 -119
- package/src/operator-console/operator-console.tsx +0 -233
- package/src/peers-services-registration.ts +0 -6
- package/src/root-layout/global-overlays.ts +0 -50
- package/src/root-layout/layout-picker.tsx +0 -47
- package/src/root-layout/layouts/full-screen-layout.tsx +0 -144
- package/src/root-layout/layouts/tabs-with-console-layout.tsx +0 -260
- package/src/root-layout/root-layout-host.test.tsx +0 -108
- package/src/root-layout/root-layout-host.tsx +0 -147
- package/src/root-layout/root-layout-registry.ts +0 -51
- package/src/root-layout/root-layout-state.ts +0 -55
- package/src/root-layout/root-layout.ts +0 -32
- package/src/screens/account/account-screen.test.tsx +0 -65
- package/src/screens/account/account-screen.tsx +0 -942
- package/src/screens/assistants/assistant-config.test.tsx +0 -54
- package/src/screens/assistants/assistant-config.tsx +0 -121
- package/src/screens/assistants/assistant-details.tsx +0 -89
- package/src/screens/assistants/assistant-info.tsx +0 -66
- package/src/screens/assistants/assistant-list.tsx +0 -145
- package/src/screens/assistants/assistant-tools.tsx +0 -82
- package/src/screens/console-logs/console-logs-list.tsx +0 -374
- package/src/screens/console-logs/log-display.tsx +0 -137
- package/src/screens/console-logs/log-filters.tsx +0 -64
- package/src/screens/console-logs/mobile-log-card.tsx +0 -161
- package/src/screens/console-logs/resizable-table-header.tsx +0 -110
- package/src/screens/contacts/contact-details.tsx +0 -123
- package/src/screens/contacts/contact-list.test.tsx +0 -59
- package/src/screens/contacts/contact-list.tsx +0 -286
- package/src/screens/contacts/index.ts +0 -2
- package/src/screens/data-explorer/data-explorer.tsx +0 -633
- package/src/screens/data-explorer/index.ts +0 -6
- package/src/screens/data-explorer/query-executor.tsx +0 -520
- package/src/screens/device-pairing/device-pairing.test.tsx +0 -143
- package/src/screens/device-pairing/device-pairing.tsx +0 -461
- package/src/screens/device-pairing/identity-ipc.ts +0 -25
- package/src/screens/device-pairing/index.ts +0 -2
- package/src/screens/device-pairing/use-device-pairing-state.ts +0 -15
- package/src/screens/devices/devices-list.test.tsx +0 -68
- package/src/screens/devices/devices-list.tsx +0 -365
- package/src/screens/devices/index.ts +0 -1
- package/src/screens/groups/group-details.tsx +0 -160
- package/src/screens/groups/group-invite-panel.tsx +0 -311
- package/src/screens/groups/group-list.test.tsx +0 -44
- package/src/screens/groups/group-list.tsx +0 -317
- package/src/screens/groups/group-members.tsx +0 -343
- package/src/screens/groups/index.ts +0 -3
- package/src/screens/identity/identity-routes.test.ts +0 -70
- package/src/screens/identity/identity-routes.ts +0 -301
- package/src/screens/identity/identity-screen.test.tsx +0 -138
- package/src/screens/identity/identity-screen.tsx +0 -282
- package/src/screens/identity/index.ts +0 -2
- package/src/screens/identity/invite-contact-panel.tsx +0 -45
- package/src/screens/identity/profile-invite.tsx +0 -93
- package/src/screens/invites/accept-invite.test.tsx +0 -115
- package/src/screens/invites/accept-invite.tsx +0 -318
- package/src/screens/invites/activity-view.test.tsx +0 -127
- package/src/screens/invites/activity-view.tsx +0 -162
- package/src/screens/invites/index.ts +0 -6
- package/src/screens/invites/invite-capture.ts +0 -104
- package/src/screens/invites/invite-classify.test.ts +0 -129
- package/src/screens/invites/invite-share.tsx +0 -104
- package/src/screens/invites/invites-ipc.ts +0 -25
- package/src/screens/invites/pending-invites-list.tsx +0 -256
- package/src/screens/network-viewer/connection-troubleshooter.tsx +0 -395
- package/src/screens/network-viewer/cpu-usage-graph.tsx +0 -294
- package/src/screens/network-viewer/device-details-modal.tsx +0 -339
- package/src/screens/network-viewer/group-details-modal.tsx +0 -421
- package/src/screens/network-viewer/index.ts +0 -8
- package/src/screens/network-viewer/network-viewer-ipc.test.ts +0 -72
- package/src/screens/network-viewer/network-viewer-ipc.ts +0 -39
- package/src/screens/network-viewer/network-viewer-section.ts +0 -12
- package/src/screens/network-viewer/network-viewer.section.test.tsx +0 -68
- package/src/screens/network-viewer/network-viewer.tsx +0 -708
- package/src/screens/network-viewer/remote-logs-panel.tsx +0 -190
- package/src/screens/network-viewer/sync-recovery.test.tsx +0 -114
- package/src/screens/network-viewer/sync-recovery.tsx +0 -461
- package/src/screens/network-viewer/usage-graph.tsx +0 -455
- package/src/screens/packages/local-folder-grants-panel.tsx +0 -290
- package/src/screens/packages/local-folders-ipc.ts +0 -28
- package/src/screens/packages/package-details.tsx +0 -166
- package/src/screens/packages/package-helpers.test.ts +0 -57
- package/src/screens/packages/package-helpers.ts +0 -137
- package/src/screens/packages/package-info.tsx +0 -354
- package/src/screens/packages/package-list.tsx +0 -519
- package/src/screens/packages/package-network-panel.tsx +0 -82
- package/src/screens/packages/package-new-local.tsx +0 -100
- package/src/screens/packages/package-runtime-status.test.tsx +0 -58
- package/src/screens/packages/package-runtime-status.tsx +0 -68
- package/src/screens/packages/package-versions.tsx +0 -298
- package/src/screens/packages/packages-ipc.test.ts +0 -47
- package/src/screens/packages/packages-ipc.ts +0 -125
- package/src/screens/peer-types/peer-type-details.tsx +0 -172
- package/src/screens/peer-types/peer-type-list.tsx +0 -64
- package/src/screens/search/global-search.tsx +0 -406
- package/src/screens/settings/color-mode-dropdown.tsx +0 -82
- package/src/screens/settings/settings-page.test.tsx +0 -24
- package/src/screens/settings/settings-page.tsx +0 -426
- package/src/screens/setup-user.tsx +0 -355
- package/src/screens/tools/tool-access-ipc.test.ts +0 -55
- package/src/screens/tools/tool-access-ipc.ts +0 -27
- package/src/screens/tools/tool-catalog-ipc.test.ts +0 -52
- package/src/screens/tools/tool-catalog-ipc.ts +0 -51
- package/src/screens/tools/tool-details.tsx +0 -52
- package/src/screens/tools/tool-info.tsx +0 -45
- package/src/screens/tools/tool-list.tsx +0 -106
- package/src/screens/tools/tool-schema.tsx +0 -44
- package/src/screens/tools/tool-test-details.tsx +0 -89
- package/src/screens/tools/tool-test-list.tsx +0 -73
- package/src/screens/variables/variable-details.tsx +0 -289
- package/src/screens/variables/variable-list.tsx +0 -110
- package/src/screens/welcome-modal.tsx +0 -294
- package/src/screens/workflows/workflow-details.tsx +0 -115
- package/src/screens/workflows/workflow-info.tsx +0 -22
- package/src/screens/workflows/workflow-instructions.tsx +0 -134
- package/src/screens/workflows/workflow-list.tsx +0 -117
- package/src/setupTests.ts +0 -38
- package/src/system-apps/assistants.app.ts +0 -7
- package/src/system-apps/console-logs.app.ts +0 -7
- package/src/system-apps/data-explorer.app.ts +0 -8
- package/src/system-apps/identity.app.ts +0 -9
- package/src/system-apps/index.ts +0 -77
- package/src/system-apps/mobile-settings.app.ts +0 -7
- package/src/system-apps/network-viewer.app.ts +0 -8
- package/src/system-apps/packages.app.ts +0 -7
- package/src/system-apps/search.app.ts +0 -8
- package/src/system-apps/settings.app.ts +0 -7
- package/src/system-apps/threads.app.ts +0 -7
- package/src/system-apps/tools.app.ts +0 -7
- package/src/system-apps/types.app.ts +0 -7
- package/src/system-apps/variables.app.ts +0 -7
- package/src/system-apps/workflows.app.ts +0 -7
- package/src/tabs-layout/network-status-indicator.test.tsx +0 -799
- package/src/tabs-layout/network-status-indicator.tsx +0 -756
- package/src/tabs-layout/tabs-layout.tsx +0 -857
- package/src/tabs-layout/tabs-state.test.ts +0 -152
- package/src/tabs-layout/tabs-state.ts +0 -587
- package/src/tabs-layout/use-network-status.test.tsx +0 -58
- package/src/tabs-layout/use-network-status.ts +0 -68
- package/src/ui-defaults/index.ts +0 -2
- package/src/ui-defaults/list-screen.tsx +0 -94
- package/src/ui-defaults/markdown-field.tsx +0 -87
- package/src/ui-router/routes-loader.ts +0 -193
- package/src/ui-router/ui-loader.test.tsx +0 -108
- package/src/ui-router/ui-loader.tsx +0 -606
- package/src/utils.ts +0 -95
- 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
|
-
}
|