gitspace 0.2.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +21 -0
- package/.gitspace/bundle.json +50 -0
- package/.gitspace/select/01-status.sh +40 -0
- package/.gitspace/setup/01-install-deps.sh +12 -0
- package/.gitspace/setup/02-typecheck.sh +16 -0
- package/AGENTS.md +439 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +25 -0
- package/README.md +607 -0
- package/bin/gssh +62 -0
- package/bun.lock +647 -0
- package/docs/CONNECTION.md +623 -0
- package/docs/GATEWAY-WORKER.md +319 -0
- package/docs/GETTING-STARTED.md +448 -0
- package/docs/GITSPACE-PLATFORM.md +1819 -0
- package/docs/INFRASTRUCTURE.md +1347 -0
- package/docs/PROTOCOL.md +619 -0
- package/docs/QUICKSTART.md +174 -0
- package/docs/RELAY.md +327 -0
- package/docs/REMOTE-DESIGN.md +549 -0
- package/docs/ROADMAP.md +564 -0
- package/docs/SITE_DOCS_FIGMA_MAKE.md +1167 -0
- package/docs/STACK-DESIGN.md +588 -0
- package/docs/UNIFIED_ARCHITECTURE.md +292 -0
- package/experiments/pty-benchmark.ts +148 -0
- package/experiments/pty-latency.ts +100 -0
- package/experiments/router/client.ts +199 -0
- package/experiments/router/protocol.ts +74 -0
- package/experiments/router/router.ts +217 -0
- package/experiments/router/session.ts +180 -0
- package/experiments/router/test.ts +133 -0
- package/experiments/socket-bandwidth.ts +77 -0
- package/homebrew/gitspace.rb +45 -0
- package/landing-page/ATTRIBUTIONS.md +3 -0
- package/landing-page/README.md +11 -0
- package/landing-page/bun.lock +801 -0
- package/landing-page/guidelines/Guidelines.md +61 -0
- package/landing-page/index.html +37 -0
- package/landing-page/package.json +90 -0
- package/landing-page/postcss.config.mjs +15 -0
- package/landing-page/public/_redirects +1 -0
- package/landing-page/public/favicon.png +0 -0
- package/landing-page/src/app/App.tsx +53 -0
- package/landing-page/src/app/components/figma/ImageWithFallback.tsx +27 -0
- package/landing-page/src/app/components/ui/accordion.tsx +66 -0
- package/landing-page/src/app/components/ui/alert-dialog.tsx +157 -0
- package/landing-page/src/app/components/ui/alert.tsx +66 -0
- package/landing-page/src/app/components/ui/aspect-ratio.tsx +11 -0
- package/landing-page/src/app/components/ui/avatar.tsx +53 -0
- package/landing-page/src/app/components/ui/badge.tsx +46 -0
- package/landing-page/src/app/components/ui/breadcrumb.tsx +109 -0
- package/landing-page/src/app/components/ui/button.tsx +57 -0
- package/landing-page/src/app/components/ui/calendar.tsx +75 -0
- package/landing-page/src/app/components/ui/card.tsx +92 -0
- package/landing-page/src/app/components/ui/carousel.tsx +241 -0
- package/landing-page/src/app/components/ui/chart.tsx +353 -0
- package/landing-page/src/app/components/ui/checkbox.tsx +32 -0
- package/landing-page/src/app/components/ui/collapsible.tsx +33 -0
- package/landing-page/src/app/components/ui/command.tsx +177 -0
- package/landing-page/src/app/components/ui/context-menu.tsx +252 -0
- package/landing-page/src/app/components/ui/dialog.tsx +135 -0
- package/landing-page/src/app/components/ui/drawer.tsx +132 -0
- package/landing-page/src/app/components/ui/dropdown-menu.tsx +257 -0
- package/landing-page/src/app/components/ui/form.tsx +168 -0
- package/landing-page/src/app/components/ui/hover-card.tsx +44 -0
- package/landing-page/src/app/components/ui/input-otp.tsx +77 -0
- package/landing-page/src/app/components/ui/input.tsx +21 -0
- package/landing-page/src/app/components/ui/label.tsx +24 -0
- package/landing-page/src/app/components/ui/menubar.tsx +276 -0
- package/landing-page/src/app/components/ui/navigation-menu.tsx +168 -0
- package/landing-page/src/app/components/ui/pagination.tsx +127 -0
- package/landing-page/src/app/components/ui/popover.tsx +48 -0
- package/landing-page/src/app/components/ui/progress.tsx +31 -0
- package/landing-page/src/app/components/ui/radio-group.tsx +45 -0
- package/landing-page/src/app/components/ui/resizable.tsx +56 -0
- package/landing-page/src/app/components/ui/scroll-area.tsx +58 -0
- package/landing-page/src/app/components/ui/select.tsx +189 -0
- package/landing-page/src/app/components/ui/separator.tsx +28 -0
- package/landing-page/src/app/components/ui/sheet.tsx +139 -0
- package/landing-page/src/app/components/ui/sidebar.tsx +726 -0
- package/landing-page/src/app/components/ui/skeleton.tsx +13 -0
- package/landing-page/src/app/components/ui/slider.tsx +63 -0
- package/landing-page/src/app/components/ui/sonner.tsx +25 -0
- package/landing-page/src/app/components/ui/switch.tsx +31 -0
- package/landing-page/src/app/components/ui/table.tsx +116 -0
- package/landing-page/src/app/components/ui/tabs.tsx +66 -0
- package/landing-page/src/app/components/ui/textarea.tsx +18 -0
- package/landing-page/src/app/components/ui/toggle-group.tsx +73 -0
- package/landing-page/src/app/components/ui/toggle.tsx +47 -0
- package/landing-page/src/app/components/ui/tooltip.tsx +61 -0
- package/landing-page/src/app/components/ui/use-mobile.ts +21 -0
- package/landing-page/src/app/components/ui/utils.ts +6 -0
- package/landing-page/src/components/docs/DocsContent.tsx +718 -0
- package/landing-page/src/components/docs/DocsSidebar.tsx +84 -0
- package/landing-page/src/components/landing/CTA.tsx +59 -0
- package/landing-page/src/components/landing/Comparison.tsx +84 -0
- package/landing-page/src/components/landing/FaultyTerminal.tsx +424 -0
- package/landing-page/src/components/landing/Features.tsx +201 -0
- package/landing-page/src/components/landing/Hero.tsx +142 -0
- package/landing-page/src/components/landing/Pricing.tsx +140 -0
- package/landing-page/src/components/landing/Roadmap.tsx +86 -0
- package/landing-page/src/components/landing/Security.tsx +81 -0
- package/landing-page/src/components/landing/TerminalWindow.tsx +27 -0
- package/landing-page/src/components/landing/UseCases.tsx +55 -0
- package/landing-page/src/components/landing/Workflow.tsx +101 -0
- package/landing-page/src/components/layout/DashboardNavbar.tsx +37 -0
- package/landing-page/src/components/layout/Footer.tsx +55 -0
- package/landing-page/src/components/layout/LandingNavbar.tsx +82 -0
- package/landing-page/src/components/ui/badge.tsx +39 -0
- package/landing-page/src/components/ui/breadcrumb.tsx +115 -0
- package/landing-page/src/components/ui/button.tsx +57 -0
- package/landing-page/src/components/ui/card.tsx +79 -0
- package/landing-page/src/components/ui/mock-terminal.tsx +68 -0
- package/landing-page/src/components/ui/separator.tsx +28 -0
- package/landing-page/src/lib/utils.ts +6 -0
- package/landing-page/src/main.tsx +10 -0
- package/landing-page/src/pages/Dashboard.tsx +133 -0
- package/landing-page/src/pages/DocsPage.tsx +79 -0
- package/landing-page/src/pages/LandingPage.tsx +31 -0
- package/landing-page/src/pages/TerminalView.tsx +106 -0
- package/landing-page/src/styles/fonts.css +0 -0
- package/landing-page/src/styles/index.css +3 -0
- package/landing-page/src/styles/tailwind.css +4 -0
- package/landing-page/src/styles/theme.css +181 -0
- package/landing-page/vite.config.ts +19 -0
- package/npm/darwin-arm64/bin/gssh +0 -0
- package/npm/darwin-arm64/package.json +20 -0
- package/package.json +74 -0
- package/scripts/build.ts +284 -0
- package/scripts/release.ts +140 -0
- package/src/__tests__/test-utils.ts +298 -0
- package/src/commands/__tests__/serve-messages.test.ts +190 -0
- package/src/commands/access.ts +298 -0
- package/src/commands/add.ts +452 -0
- package/src/commands/auth.ts +364 -0
- package/src/commands/connect.ts +287 -0
- package/src/commands/directory.ts +16 -0
- package/src/commands/host.ts +396 -0
- package/src/commands/identity.ts +184 -0
- package/src/commands/list.ts +200 -0
- package/src/commands/relay.ts +315 -0
- package/src/commands/remove.ts +241 -0
- package/src/commands/serve.ts +1493 -0
- package/src/commands/share.ts +456 -0
- package/src/commands/status.ts +125 -0
- package/src/commands/switch.ts +353 -0
- package/src/commands/tmux.ts +317 -0
- package/src/core/__tests__/access.test.ts +240 -0
- package/src/core/access.ts +277 -0
- package/src/core/bundle.ts +342 -0
- package/src/core/config.ts +510 -0
- package/src/core/git.ts +317 -0
- package/src/core/github.ts +151 -0
- package/src/core/identity.ts +631 -0
- package/src/core/linear.ts +225 -0
- package/src/core/shell.ts +161 -0
- package/src/core/trusted-relays.ts +315 -0
- package/src/index.ts +821 -0
- package/src/lib/remote-session/index.ts +7 -0
- package/src/lib/remote-session/protocol.ts +267 -0
- package/src/lib/remote-session/session-handler.ts +581 -0
- package/src/lib/remote-session/workspace-scanner.ts +167 -0
- package/src/lib/tmux-lite/README.md +81 -0
- package/src/lib/tmux-lite/cli.ts +796 -0
- package/src/lib/tmux-lite/crypto/__tests__/helpers/handshake-runner.ts +349 -0
- package/src/lib/tmux-lite/crypto/__tests__/helpers/mock-relay.ts +291 -0
- package/src/lib/tmux-lite/crypto/__tests__/helpers/test-identities.ts +142 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/authorization.integration.test.ts +339 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/e2e-communication.integration.test.ts +477 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/error-handling.integration.test.ts +499 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/handshake.integration.test.ts +371 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/security.integration.test.ts +573 -0
- package/src/lib/tmux-lite/crypto/access-control.test.ts +512 -0
- package/src/lib/tmux-lite/crypto/access-control.ts +320 -0
- package/src/lib/tmux-lite/crypto/frames.test.ts +262 -0
- package/src/lib/tmux-lite/crypto/frames.ts +141 -0
- package/src/lib/tmux-lite/crypto/handshake.ts +894 -0
- package/src/lib/tmux-lite/crypto/identity.test.ts +220 -0
- package/src/lib/tmux-lite/crypto/identity.ts +286 -0
- package/src/lib/tmux-lite/crypto/index.ts +51 -0
- package/src/lib/tmux-lite/crypto/invites.test.ts +381 -0
- package/src/lib/tmux-lite/crypto/invites.ts +215 -0
- package/src/lib/tmux-lite/crypto/keyexchange.ts +435 -0
- package/src/lib/tmux-lite/crypto/keys.test.ts +58 -0
- package/src/lib/tmux-lite/crypto/keys.ts +47 -0
- package/src/lib/tmux-lite/crypto/secretbox.test.ts +169 -0
- package/src/lib/tmux-lite/crypto/secretbox.ts +124 -0
- package/src/lib/tmux-lite/handshake-handler.ts +451 -0
- package/src/lib/tmux-lite/protocol.test.ts +307 -0
- package/src/lib/tmux-lite/protocol.ts +266 -0
- package/src/lib/tmux-lite/relay-client.ts +506 -0
- package/src/lib/tmux-lite/server.ts +1250 -0
- package/src/lib/tmux-lite/shell-integration.sh +37 -0
- package/src/lib/tmux-lite/terminal-queries.test.ts +54 -0
- package/src/lib/tmux-lite/terminal-queries.ts +49 -0
- package/src/relay/__tests__/e2e-flow.test.ts +1284 -0
- package/src/relay/__tests__/helpers/auth.ts +354 -0
- package/src/relay/__tests__/helpers/ports.ts +51 -0
- package/src/relay/__tests__/protocol-validation.test.ts +265 -0
- package/src/relay/authorization.ts +303 -0
- package/src/relay/embedded-assets.generated.d.ts +15 -0
- package/src/relay/identity.ts +352 -0
- package/src/relay/index.ts +57 -0
- package/src/relay/pipes.test.ts +427 -0
- package/src/relay/pipes.ts +195 -0
- package/src/relay/protocol.ts +804 -0
- package/src/relay/registries.test.ts +437 -0
- package/src/relay/registries.ts +593 -0
- package/src/relay/server.test.ts +1323 -0
- package/src/relay/server.ts +1092 -0
- package/src/relay/signing.ts +238 -0
- package/src/relay/types.ts +69 -0
- package/src/serve/client-session-manager.ts +622 -0
- package/src/serve/daemon.ts +497 -0
- package/src/serve/pty-session.ts +236 -0
- package/src/serve/types.ts +169 -0
- package/src/shared/components/Flow.tsx +453 -0
- package/src/shared/components/Flow.tui.tsx +343 -0
- package/src/shared/components/Flow.web.tsx +442 -0
- package/src/shared/components/Inbox.tsx +446 -0
- package/src/shared/components/Inbox.tui.tsx +262 -0
- package/src/shared/components/Inbox.web.tsx +329 -0
- package/src/shared/components/MachineList.tsx +187 -0
- package/src/shared/components/MachineList.tui.tsx +161 -0
- package/src/shared/components/MachineList.web.tsx +210 -0
- package/src/shared/components/ProjectList.tsx +176 -0
- package/src/shared/components/ProjectList.tui.tsx +109 -0
- package/src/shared/components/ProjectList.web.tsx +143 -0
- package/src/shared/components/SpacesBrowser.tsx +332 -0
- package/src/shared/components/SpacesBrowser.tui.tsx +163 -0
- package/src/shared/components/SpacesBrowser.web.tsx +221 -0
- package/src/shared/components/index.ts +103 -0
- package/src/shared/hooks/index.ts +16 -0
- package/src/shared/hooks/useNavigation.ts +226 -0
- package/src/shared/index.ts +122 -0
- package/src/shared/providers/LocalMachineProvider.ts +425 -0
- package/src/shared/providers/MachineProvider.ts +165 -0
- package/src/shared/providers/RemoteMachineProvider.ts +444 -0
- package/src/shared/providers/index.ts +26 -0
- package/src/shared/types.ts +145 -0
- package/src/tui/adapters.ts +120 -0
- package/src/tui/app.tsx +1816 -0
- package/src/tui/components/Terminal.tsx +580 -0
- package/src/tui/hooks/index.ts +35 -0
- package/src/tui/hooks/useAppState.ts +314 -0
- package/src/tui/hooks/useDaemonStatus.ts +174 -0
- package/src/tui/hooks/useInboxTUI.ts +113 -0
- package/src/tui/hooks/useRemoteMachines.ts +209 -0
- package/src/tui/index.ts +24 -0
- package/src/tui/state.ts +299 -0
- package/src/tui/terminal-bracketed-paste.test.ts +45 -0
- package/src/tui/terminal-bracketed-paste.ts +47 -0
- package/src/types/bundle.ts +112 -0
- package/src/types/config.ts +89 -0
- package/src/types/errors.ts +206 -0
- package/src/types/identity.ts +284 -0
- package/src/types/workspace-fuzzy.ts +49 -0
- package/src/types/workspace.ts +151 -0
- package/src/utils/bun-socket-writer.ts +80 -0
- package/src/utils/deps.ts +127 -0
- package/src/utils/fuzzy-match.ts +125 -0
- package/src/utils/logger.ts +127 -0
- package/src/utils/markdown.ts +254 -0
- package/src/utils/onboarding.ts +229 -0
- package/src/utils/prompts.ts +114 -0
- package/src/utils/run-commands.ts +112 -0
- package/src/utils/run-scripts.ts +142 -0
- package/src/utils/sanitize.ts +98 -0
- package/src/utils/secrets.ts +122 -0
- package/src/utils/shell-escape.ts +40 -0
- package/src/utils/utf8.ts +79 -0
- package/src/utils/workspace-state.ts +47 -0
- package/src/web/README.md +73 -0
- package/src/web/bun.lock +575 -0
- package/src/web/eslint.config.js +23 -0
- package/src/web/index.html +16 -0
- package/src/web/package.json +37 -0
- package/src/web/public/vite.svg +1 -0
- package/src/web/src/App.tsx +604 -0
- package/src/web/src/assets/react.svg +1 -0
- package/src/web/src/components/Terminal.tsx +207 -0
- package/src/web/src/hooks/useRelayConnection.ts +224 -0
- package/src/web/src/hooks/useTerminal.ts +699 -0
- package/src/web/src/index.css +55 -0
- package/src/web/src/lib/crypto/__tests__/web-terminal.test.ts +1158 -0
- package/src/web/src/lib/crypto/frames.ts +205 -0
- package/src/web/src/lib/crypto/handshake.ts +396 -0
- package/src/web/src/lib/crypto/identity.ts +128 -0
- package/src/web/src/lib/crypto/keyexchange.ts +246 -0
- package/src/web/src/lib/crypto/relay-signing.ts +53 -0
- package/src/web/src/lib/invite.ts +58 -0
- package/src/web/src/lib/storage/identity-store.ts +94 -0
- package/src/web/src/main.tsx +10 -0
- package/src/web/src/types/identity.ts +45 -0
- package/src/web/tsconfig.app.json +28 -0
- package/src/web/tsconfig.json +7 -0
- package/src/web/tsconfig.node.json +26 -0
- package/src/web/vite.config.ts +31 -0
- package/todo-security.md +92 -0
- package/tsconfig.json +23 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-shm +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-wal +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-shm +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-wal +0 -0
- package/worker/bun.lock +237 -0
- package/worker/package.json +22 -0
- package/worker/schema.sql +96 -0
- package/worker/src/handlers/auth.ts +451 -0
- package/worker/src/handlers/subdomains.ts +376 -0
- package/worker/src/handlers/user.ts +98 -0
- package/worker/src/index.ts +70 -0
- package/worker/src/middleware/auth.ts +152 -0
- package/worker/src/services/cloudflare.ts +609 -0
- package/worker/src/types.ts +96 -0
- package/worker/tsconfig.json +15 -0
- package/worker/wrangler.toml +26 -0
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity file operations for managing keypairs, access lists, and machine identity
|
|
3
|
+
*
|
|
4
|
+
* This module handles persistent storage of cryptographic identities and access control:
|
|
5
|
+
* - Encrypted keypair storage (password-protected)
|
|
6
|
+
* - Access list management (authorized public keys)
|
|
7
|
+
* - Machine identity configuration
|
|
8
|
+
*
|
|
9
|
+
* Directory structure:
|
|
10
|
+
* ~/gitspace/.identity/
|
|
11
|
+
* ├── keypair.json # Encrypted identity keypair
|
|
12
|
+
* ├── access-list.json # Allowed public keys
|
|
13
|
+
* └── machine.json # Machine registration info
|
|
14
|
+
*
|
|
15
|
+
* @module identity
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
existsSync,
|
|
20
|
+
mkdirSync,
|
|
21
|
+
readFileSync,
|
|
22
|
+
writeFileSync,
|
|
23
|
+
} from 'node:fs';
|
|
24
|
+
import { join } from 'node:path';
|
|
25
|
+
import type {
|
|
26
|
+
Identity,
|
|
27
|
+
PublicIdentity,
|
|
28
|
+
AccessEntry,
|
|
29
|
+
AccessType,
|
|
30
|
+
MachineIdentity,
|
|
31
|
+
} from '../types/identity.js';
|
|
32
|
+
import {
|
|
33
|
+
generateIdentity,
|
|
34
|
+
serializeIdentity,
|
|
35
|
+
deserializeIdentity,
|
|
36
|
+
getPublicIdentity,
|
|
37
|
+
} from '../lib/tmux-lite/crypto/identity.js';
|
|
38
|
+
import { DEFAULT_ACCESS_TYPE } from '../lib/tmux-lite/crypto/access-control.js';
|
|
39
|
+
import { seal, open } from '../lib/tmux-lite/crypto/secretbox.js';
|
|
40
|
+
import { deriveKey, generateSalt } from '../lib/tmux-lite/crypto/keys.js';
|
|
41
|
+
import { getSpacesDir } from './config.js';
|
|
42
|
+
import {
|
|
43
|
+
SpacesError,
|
|
44
|
+
NoIdentityError,
|
|
45
|
+
InvalidPasswordError,
|
|
46
|
+
IdentityExistsError,
|
|
47
|
+
} from '../types/errors.js';
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// Storage Format Types
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Encrypted keypair storage format
|
|
55
|
+
*/
|
|
56
|
+
interface EncryptedKeypairStorage {
|
|
57
|
+
version: 1;
|
|
58
|
+
id: string;
|
|
59
|
+
label?: string;
|
|
60
|
+
createdAt: number;
|
|
61
|
+
signingPublicKey: string;
|
|
62
|
+
keyExchangePublicKey: string;
|
|
63
|
+
/** base64-encoded encrypted secrets (nonce prepended) */
|
|
64
|
+
encryptedSecrets: string;
|
|
65
|
+
/** base64-encoded salt for key derivation */
|
|
66
|
+
salt: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Decrypted secrets structure
|
|
71
|
+
*/
|
|
72
|
+
interface DecryptedSecrets {
|
|
73
|
+
signingSecretKey: string;
|
|
74
|
+
keyExchangePrivateKey: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ============================================================================
|
|
78
|
+
// Directory Paths
|
|
79
|
+
// ============================================================================
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get the identity directory path
|
|
83
|
+
*
|
|
84
|
+
* @returns Path to ~/gitspace/.identity/
|
|
85
|
+
*/
|
|
86
|
+
export function getIdentityDir(): string {
|
|
87
|
+
return join(getSpacesDir(), '.identity');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get the keypair file path
|
|
92
|
+
*
|
|
93
|
+
* @returns Path to keypair.json
|
|
94
|
+
*/
|
|
95
|
+
export function getKeypairPath(): string {
|
|
96
|
+
return join(getIdentityDir(), 'keypair.json');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get the access list file path
|
|
101
|
+
*
|
|
102
|
+
* @returns Path to access-list.json
|
|
103
|
+
*/
|
|
104
|
+
export function getAccessListPath(): string {
|
|
105
|
+
return join(getIdentityDir(), 'access-list.json');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get the machine identity file path
|
|
110
|
+
*
|
|
111
|
+
* @returns Path to machine.json
|
|
112
|
+
*/
|
|
113
|
+
export function getMachineIdentityPath(): string {
|
|
114
|
+
return join(getIdentityDir(), 'machine.json');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Ensure identity directory exists
|
|
119
|
+
*/
|
|
120
|
+
function ensureIdentityDir(): void {
|
|
121
|
+
const identityDir = getIdentityDir();
|
|
122
|
+
if (!existsSync(identityDir)) {
|
|
123
|
+
mkdirSync(identityDir, { recursive: true, mode: 0o700 });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ============================================================================
|
|
128
|
+
// Keypair Management
|
|
129
|
+
// ============================================================================
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Generate a new identity and save it to disk (encrypted)
|
|
133
|
+
*
|
|
134
|
+
* Creates a new Ed25519 + X25519 keypair, encrypts the secret keys with
|
|
135
|
+
* a password-derived key, and saves to keypair.json.
|
|
136
|
+
*
|
|
137
|
+
* @param password - Password to encrypt the keypair
|
|
138
|
+
* @param label - Optional human-readable label
|
|
139
|
+
* @param force - If true, overwrite existing keypair
|
|
140
|
+
* @returns Public identity information
|
|
141
|
+
* @throws {IdentityExistsError} If keypair exists and force is false
|
|
142
|
+
*/
|
|
143
|
+
export async function generateAndSaveKeypair(
|
|
144
|
+
password: string,
|
|
145
|
+
label?: string,
|
|
146
|
+
force: boolean = false
|
|
147
|
+
): Promise<PublicIdentity> {
|
|
148
|
+
// Check if keypair already exists
|
|
149
|
+
if (keypairExists() && !force) {
|
|
150
|
+
throw new IdentityExistsError();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
ensureIdentityDir();
|
|
154
|
+
|
|
155
|
+
// Generate new identity
|
|
156
|
+
const identity = generateIdentity(label);
|
|
157
|
+
|
|
158
|
+
// Serialize identity to get base64 strings
|
|
159
|
+
const serialized = serializeIdentity(identity);
|
|
160
|
+
|
|
161
|
+
// Create secrets object to encrypt
|
|
162
|
+
const secrets: DecryptedSecrets = {
|
|
163
|
+
signingSecretKey: serialized.signingSecretKey,
|
|
164
|
+
keyExchangePrivateKey: serialized.keyExchangePrivateKey,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// Generate salt and derive encryption key from password
|
|
168
|
+
const salt = generateSalt();
|
|
169
|
+
const encryptionKey = await deriveKey(password, salt);
|
|
170
|
+
|
|
171
|
+
// Encrypt secrets
|
|
172
|
+
const secretsJson = JSON.stringify(secrets);
|
|
173
|
+
const encryptedSecrets = seal(Buffer.from(secretsJson, 'utf-8'), encryptionKey);
|
|
174
|
+
|
|
175
|
+
// Create storage format
|
|
176
|
+
const storage: EncryptedKeypairStorage = {
|
|
177
|
+
version: 1,
|
|
178
|
+
id: identity.id,
|
|
179
|
+
label: identity.label,
|
|
180
|
+
createdAt: identity.createdAt,
|
|
181
|
+
signingPublicKey: serialized.signingPublicKey,
|
|
182
|
+
keyExchangePublicKey: serialized.keyExchangePublicKey,
|
|
183
|
+
encryptedSecrets: encryptedSecrets.toString('base64'),
|
|
184
|
+
salt: salt.toString('base64'),
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// Write to disk
|
|
188
|
+
try {
|
|
189
|
+
writeFileSync(getKeypairPath(), JSON.stringify(storage, null, 2), {
|
|
190
|
+
encoding: 'utf-8',
|
|
191
|
+
mode: 0o600, // Owner read/write only
|
|
192
|
+
});
|
|
193
|
+
} catch (error) {
|
|
194
|
+
throw new SpacesError(
|
|
195
|
+
`Failed to save keypair: ${
|
|
196
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
197
|
+
}`,
|
|
198
|
+
'SYSTEM_ERROR',
|
|
199
|
+
2
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return getPublicIdentity(identity);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Load and decrypt the keypair from disk
|
|
208
|
+
*
|
|
209
|
+
* Reads the encrypted keypair, derives the decryption key from the password,
|
|
210
|
+
* and returns the full identity with secret keys.
|
|
211
|
+
*
|
|
212
|
+
* @param password - Password to decrypt the keypair
|
|
213
|
+
* @returns Complete identity with secret keys
|
|
214
|
+
* @throws {NoIdentityError} If keypair doesn't exist
|
|
215
|
+
* @throws {InvalidPasswordError} If password is incorrect
|
|
216
|
+
*/
|
|
217
|
+
export async function loadKeypair(password: string): Promise<Identity> {
|
|
218
|
+
if (!keypairExists()) {
|
|
219
|
+
throw new NoIdentityError();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Read storage file
|
|
223
|
+
let storage: EncryptedKeypairStorage;
|
|
224
|
+
try {
|
|
225
|
+
const content = readFileSync(getKeypairPath(), 'utf-8');
|
|
226
|
+
storage = JSON.parse(content) as EncryptedKeypairStorage;
|
|
227
|
+
} catch (error) {
|
|
228
|
+
throw new SpacesError(
|
|
229
|
+
`Failed to read keypair: ${
|
|
230
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
231
|
+
}`,
|
|
232
|
+
'SYSTEM_ERROR',
|
|
233
|
+
2
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Derive decryption key from password
|
|
238
|
+
const salt = Buffer.from(storage.salt, 'base64');
|
|
239
|
+
const decryptionKey = await deriveKey(password, salt);
|
|
240
|
+
|
|
241
|
+
// Decrypt secrets
|
|
242
|
+
const encryptedSecrets = Buffer.from(storage.encryptedSecrets, 'base64');
|
|
243
|
+
const decryptedSecretsBuffer = open(encryptedSecrets, decryptionKey);
|
|
244
|
+
|
|
245
|
+
if (!decryptedSecretsBuffer) {
|
|
246
|
+
throw new InvalidPasswordError();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Parse secrets
|
|
250
|
+
let secrets: DecryptedSecrets;
|
|
251
|
+
try {
|
|
252
|
+
secrets = JSON.parse(decryptedSecretsBuffer.toString('utf-8')) as DecryptedSecrets;
|
|
253
|
+
} catch (error) {
|
|
254
|
+
throw new SpacesError(
|
|
255
|
+
'Failed to parse decrypted secrets',
|
|
256
|
+
'SYSTEM_ERROR',
|
|
257
|
+
2
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Reconstruct stored identity format
|
|
262
|
+
const storedIdentity = {
|
|
263
|
+
id: storage.id,
|
|
264
|
+
label: storage.label,
|
|
265
|
+
createdAt: storage.createdAt,
|
|
266
|
+
signingPublicKey: storage.signingPublicKey,
|
|
267
|
+
keyExchangePublicKey: storage.keyExchangePublicKey,
|
|
268
|
+
signingSecretKey: secrets.signingSecretKey,
|
|
269
|
+
keyExchangePrivateKey: secrets.keyExchangePrivateKey,
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// Deserialize to Identity format
|
|
273
|
+
return deserializeIdentity(storedIdentity);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Check if a keypair exists on disk
|
|
278
|
+
*
|
|
279
|
+
* @returns True if keypair.json exists
|
|
280
|
+
*/
|
|
281
|
+
export function keypairExists(): boolean {
|
|
282
|
+
return existsSync(getKeypairPath());
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Get the public identity without requiring password
|
|
287
|
+
*
|
|
288
|
+
* Reads only the public keys from the stored keypair file.
|
|
289
|
+
* This is safe to call without authentication.
|
|
290
|
+
*
|
|
291
|
+
* @returns Public identity if keypair exists, null otherwise
|
|
292
|
+
*/
|
|
293
|
+
export function getPublicKeyWithoutPassword(): PublicIdentity | null {
|
|
294
|
+
if (!keypairExists()) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
const content = readFileSync(getKeypairPath(), 'utf-8');
|
|
300
|
+
const storage = JSON.parse(content) as EncryptedKeypairStorage;
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
id: storage.id,
|
|
304
|
+
signingPublicKey: storage.signingPublicKey,
|
|
305
|
+
keyExchangePublicKey: storage.keyExchangePublicKey,
|
|
306
|
+
label: storage.label,
|
|
307
|
+
};
|
|
308
|
+
} catch (error) {
|
|
309
|
+
throw new SpacesError(
|
|
310
|
+
`Failed to read public key: ${
|
|
311
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
312
|
+
}`,
|
|
313
|
+
'SYSTEM_ERROR',
|
|
314
|
+
2
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ============================================================================
|
|
320
|
+
// Access List Management
|
|
321
|
+
// ============================================================================
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Read the access list from disk
|
|
325
|
+
*
|
|
326
|
+
* Returns an empty array if the file doesn't exist.
|
|
327
|
+
*
|
|
328
|
+
* @returns Array of access entries
|
|
329
|
+
*/
|
|
330
|
+
export function readAccessList(): AccessEntry[] {
|
|
331
|
+
const accessListPath = getAccessListPath();
|
|
332
|
+
|
|
333
|
+
if (!existsSync(accessListPath)) {
|
|
334
|
+
return [];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
const content = readFileSync(accessListPath, 'utf-8');
|
|
339
|
+
return JSON.parse(content) as AccessEntry[];
|
|
340
|
+
} catch (error) {
|
|
341
|
+
throw new SpacesError(
|
|
342
|
+
`Failed to read access list: ${
|
|
343
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
344
|
+
}`,
|
|
345
|
+
'SYSTEM_ERROR',
|
|
346
|
+
2
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Write the access list to disk
|
|
353
|
+
*
|
|
354
|
+
* Creates the identity directory if it doesn't exist.
|
|
355
|
+
*
|
|
356
|
+
* @param entries - Array of access entries to write
|
|
357
|
+
*/
|
|
358
|
+
export function writeAccessList(entries: AccessEntry[]): void {
|
|
359
|
+
ensureIdentityDir();
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
writeFileSync(
|
|
363
|
+
getAccessListPath(),
|
|
364
|
+
JSON.stringify(entries, null, 2),
|
|
365
|
+
{
|
|
366
|
+
encoding: 'utf-8',
|
|
367
|
+
mode: 0o600, // Owner read/write only
|
|
368
|
+
}
|
|
369
|
+
);
|
|
370
|
+
} catch (error) {
|
|
371
|
+
throw new SpacesError(
|
|
372
|
+
`Failed to write access list: ${
|
|
373
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
374
|
+
}`,
|
|
375
|
+
'SYSTEM_ERROR',
|
|
376
|
+
2
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Add a new identity to the access list
|
|
383
|
+
*
|
|
384
|
+
* Creates a new access entry with the given access type.
|
|
385
|
+
* If the identity already exists, it will be replaced.
|
|
386
|
+
*
|
|
387
|
+
* @param publicIdentity - Public identity to add
|
|
388
|
+
* @param label - Optional label override (uses identity.label if not provided)
|
|
389
|
+
* @param accessType - Access type to grant (defaults to 'full')
|
|
390
|
+
* @param sessionId - Optional session ID for session-invite access
|
|
391
|
+
* @returns The created access entry
|
|
392
|
+
*/
|
|
393
|
+
export function addAccess(
|
|
394
|
+
publicIdentity: PublicIdentity,
|
|
395
|
+
label?: string,
|
|
396
|
+
accessType: AccessType = DEFAULT_ACCESS_TYPE,
|
|
397
|
+
sessionId?: string
|
|
398
|
+
): AccessEntry {
|
|
399
|
+
const entries = readAccessList();
|
|
400
|
+
|
|
401
|
+
// Create new entry
|
|
402
|
+
const newEntry: AccessEntry = {
|
|
403
|
+
identityId: publicIdentity.id,
|
|
404
|
+
signingPublicKey: publicIdentity.signingPublicKey,
|
|
405
|
+
keyExchangePublicKey: publicIdentity.keyExchangePublicKey,
|
|
406
|
+
label: label || publicIdentity.label,
|
|
407
|
+
grantedAt: Date.now(),
|
|
408
|
+
accessType,
|
|
409
|
+
sessionId,
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
// Remove existing entry with same ID (if any)
|
|
413
|
+
const filteredEntries = entries.filter(
|
|
414
|
+
(e) => e.identityId !== publicIdentity.id
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
// Add new entry
|
|
418
|
+
filteredEntries.push(newEntry);
|
|
419
|
+
|
|
420
|
+
// Write back to disk
|
|
421
|
+
writeAccessList(filteredEntries);
|
|
422
|
+
|
|
423
|
+
return newEntry;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Remove an identity from the access list
|
|
428
|
+
*
|
|
429
|
+
* Searches by identity ID or label (case-insensitive).
|
|
430
|
+
*
|
|
431
|
+
* @param identityIdOrLabel - Identity ID or label to remove
|
|
432
|
+
* @returns True if an entry was removed, false if not found
|
|
433
|
+
*/
|
|
434
|
+
export function removeAccess(identityIdOrLabel: string): boolean {
|
|
435
|
+
const entries = readAccessList();
|
|
436
|
+
const searchLower = identityIdOrLabel.toLowerCase();
|
|
437
|
+
|
|
438
|
+
const filteredEntries = entries.filter((e) => {
|
|
439
|
+
const matchesId = e.identityId.toLowerCase() === searchLower;
|
|
440
|
+
const matchesLabel = e.label?.toLowerCase() === searchLower;
|
|
441
|
+
return !matchesId && !matchesLabel;
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// Check if anything was removed
|
|
445
|
+
if (filteredEntries.length === entries.length) {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
writeAccessList(filteredEntries);
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Get an access entry by identity ID or label
|
|
455
|
+
*
|
|
456
|
+
* Searches by identity ID or label (case-insensitive).
|
|
457
|
+
*
|
|
458
|
+
* @param identityIdOrLabel - Identity ID or label to search for
|
|
459
|
+
* @returns Access entry if found, undefined otherwise
|
|
460
|
+
*/
|
|
461
|
+
export function getAccessEntry(identityIdOrLabel: string): AccessEntry | undefined {
|
|
462
|
+
const entries = readAccessList();
|
|
463
|
+
const searchLower = identityIdOrLabel.toLowerCase();
|
|
464
|
+
|
|
465
|
+
return entries.find((e) => {
|
|
466
|
+
const matchesId = e.identityId.toLowerCase() === searchLower;
|
|
467
|
+
const matchesLabel = e.label?.toLowerCase() === searchLower;
|
|
468
|
+
return matchesId || matchesLabel;
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// ============================================================================
|
|
473
|
+
// Machine Identity Management
|
|
474
|
+
// ============================================================================
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Read machine identity configuration from disk
|
|
478
|
+
*
|
|
479
|
+
* @returns Machine identity if exists, null otherwise
|
|
480
|
+
*/
|
|
481
|
+
export function readMachineIdentity(): MachineIdentity | null {
|
|
482
|
+
const machineIdentityPath = getMachineIdentityPath();
|
|
483
|
+
|
|
484
|
+
if (!existsSync(machineIdentityPath)) {
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
try {
|
|
489
|
+
const content = readFileSync(machineIdentityPath, 'utf-8');
|
|
490
|
+
return JSON.parse(content) as MachineIdentity;
|
|
491
|
+
} catch (error) {
|
|
492
|
+
throw new SpacesError(
|
|
493
|
+
`Failed to read machine identity: ${
|
|
494
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
495
|
+
}`,
|
|
496
|
+
'SYSTEM_ERROR',
|
|
497
|
+
2
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Write machine identity configuration to disk
|
|
504
|
+
*
|
|
505
|
+
* Creates the identity directory if it doesn't exist.
|
|
506
|
+
*
|
|
507
|
+
* @param identity - Machine identity to write
|
|
508
|
+
*/
|
|
509
|
+
export function writeMachineIdentity(identity: MachineIdentity): void {
|
|
510
|
+
ensureIdentityDir();
|
|
511
|
+
|
|
512
|
+
try {
|
|
513
|
+
writeFileSync(
|
|
514
|
+
getMachineIdentityPath(),
|
|
515
|
+
JSON.stringify(identity, null, 2),
|
|
516
|
+
{
|
|
517
|
+
encoding: 'utf-8',
|
|
518
|
+
mode: 0o600, // Owner read/write only
|
|
519
|
+
}
|
|
520
|
+
);
|
|
521
|
+
} catch (error) {
|
|
522
|
+
throw new SpacesError(
|
|
523
|
+
`Failed to write machine identity: ${
|
|
524
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
525
|
+
}`,
|
|
526
|
+
'SYSTEM_ERROR',
|
|
527
|
+
2
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// ============================================================================
|
|
533
|
+
// Relay Configuration Management
|
|
534
|
+
// ============================================================================
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Relay configuration for coordination between serve/share/access commands
|
|
538
|
+
*
|
|
539
|
+
* Note: Authentication is now done via challenge-response (no JWT tokens)
|
|
540
|
+
*/
|
|
541
|
+
export interface RelayConfig {
|
|
542
|
+
/** Relay WebSocket URL */
|
|
543
|
+
relayUrl: string;
|
|
544
|
+
/** Machine ID registered with relay */
|
|
545
|
+
machineId: string;
|
|
546
|
+
/** When this config was saved */
|
|
547
|
+
savedAt: number;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Get the relay config file path
|
|
552
|
+
*
|
|
553
|
+
* @returns Path to relay.json
|
|
554
|
+
*/
|
|
555
|
+
export function getRelayConfigPath(): string {
|
|
556
|
+
return join(getIdentityDir(), 'relay.json');
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Read relay configuration from disk
|
|
561
|
+
*
|
|
562
|
+
* @returns Relay config if exists, null otherwise
|
|
563
|
+
*/
|
|
564
|
+
export function readRelayConfig(): RelayConfig | null {
|
|
565
|
+
const relayConfigPath = getRelayConfigPath();
|
|
566
|
+
|
|
567
|
+
if (!existsSync(relayConfigPath)) {
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
try {
|
|
572
|
+
const content = readFileSync(relayConfigPath, 'utf-8');
|
|
573
|
+
return JSON.parse(content) as RelayConfig;
|
|
574
|
+
} catch (error) {
|
|
575
|
+
throw new SpacesError(
|
|
576
|
+
`Failed to read relay config: ${
|
|
577
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
578
|
+
}`,
|
|
579
|
+
'SYSTEM_ERROR',
|
|
580
|
+
2
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Write relay configuration to disk
|
|
587
|
+
*
|
|
588
|
+
* Creates the identity directory if it doesn't exist.
|
|
589
|
+
*
|
|
590
|
+
* @param config - Relay configuration to write
|
|
591
|
+
*/
|
|
592
|
+
export function writeRelayConfig(config: RelayConfig): void {
|
|
593
|
+
ensureIdentityDir();
|
|
594
|
+
|
|
595
|
+
try {
|
|
596
|
+
writeFileSync(
|
|
597
|
+
getRelayConfigPath(),
|
|
598
|
+
JSON.stringify(config, null, 2),
|
|
599
|
+
{
|
|
600
|
+
encoding: 'utf-8',
|
|
601
|
+
mode: 0o600, // Owner read/write only
|
|
602
|
+
}
|
|
603
|
+
);
|
|
604
|
+
} catch (error) {
|
|
605
|
+
throw new SpacesError(
|
|
606
|
+
`Failed to write relay config: ${
|
|
607
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
608
|
+
}`,
|
|
609
|
+
'SYSTEM_ERROR',
|
|
610
|
+
2
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Clear relay configuration
|
|
617
|
+
*
|
|
618
|
+
* Removes the relay config file if it exists.
|
|
619
|
+
*/
|
|
620
|
+
export function clearRelayConfig(): void {
|
|
621
|
+
const relayConfigPath = getRelayConfigPath();
|
|
622
|
+
|
|
623
|
+
if (existsSync(relayConfigPath)) {
|
|
624
|
+
try {
|
|
625
|
+
const { unlinkSync } = require('node:fs');
|
|
626
|
+
unlinkSync(relayConfigPath);
|
|
627
|
+
} catch (error) {
|
|
628
|
+
// Ignore errors when clearing
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|