auramaxx 1.0.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +26 -0
- package/README.md +112 -0
- package/bin/aurawallet.js +121 -0
- package/docs/ADAPTERS.md +467 -0
- package/docs/API.md +2679 -0
- package/docs/APPS.md +198 -0
- package/docs/ARCHITECTURE.md +350 -0
- package/docs/AUTH.md +698 -0
- package/docs/BEST-PRACTICES.md +121 -0
- package/docs/CLI.md +61 -0
- package/docs/DEVELOPING-APPS.md +452 -0
- package/docs/EXTENSION.md +97 -0
- package/docs/JOBS.md +33 -0
- package/docs/MCP.md +76 -0
- package/docs/PROTOCOL.md +142 -0
- package/docs/SETUP.md +219 -0
- package/docs/WORKSPACE.md +672 -0
- package/docs/agent-auth.md +63 -0
- package/docs/aura-file.md +48 -0
- package/docs/credentials.md +53 -0
- package/docs/external/getting-started.md +65 -0
- package/docs/external/overview.md +45 -0
- package/docs/external/use-cases.md +48 -0
- package/docs/external/why-aura.md +35 -0
- package/docs/jobs/connect-agent.md +77 -0
- package/docs/jobs/migrate-from-dotenv.md +79 -0
- package/docs/jobs/recover-from-lockout.md +72 -0
- package/docs/jobs/secure-ci.md +63 -0
- package/docs/oauth2.md +42 -0
- package/docs/passkeys.md +60 -0
- package/docs/security.md +540 -0
- package/docs/specs/aura-open-protocol.md +61 -0
- package/docs/specs/aura-provider-plugin.md +24 -0
- package/docs/specs/aura-registry-model.md +31 -0
- package/docs/specs/fixtures/invalid-bad-key.aura +1 -0
- package/docs/specs/fixtures/invalid-bad-unicode-escape.aura +1 -0
- package/docs/specs/fixtures/invalid-duplicate-key.aura +2 -0
- package/docs/specs/fixtures/valid-basic.aura +4 -0
- package/docs/specs/fixtures/valid-provider-ref.aura +1 -0
- package/docs/specs/fixtures/valid-quoted-escapes.aura +2 -0
- package/docs/templates/RELEASE_NOTES_TEMPLATE.md +22 -0
- package/docs/totp.md +40 -0
- package/docs/wallet/AI.md +508 -0
- package/docs/wallet/DEVELOPING-STRATEGIES.md +713 -0
- package/docs/wallet/README.md +47 -0
- package/docs/wallet/STRATEGY.md +89 -0
- package/next.config.ts +21 -0
- package/package.json +151 -0
- package/postcss.config.mjs +8 -0
- package/prisma/migrations/20260214170000_baseline/migration.sql +511 -0
- package/prisma/migrations/20260216214537_add_passkey_model/migration.sql +18 -0
- package/prisma/migrations/20260217150500_add_credential_access_audit/migration.sql +31 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +447 -0
- package/public/logo-chevron.svg +31 -0
- package/public/logo-concentric.svg +31 -0
- package/public/logo-crosshatch.svg +39 -0
- package/public/logo-dashed.svg +39 -0
- package/public/logo-horizontal.svg +31 -0
- package/public/logo-m56.svg +64 -0
- package/public/logo.webp +0 -0
- package/scripts/add-app.js +245 -0
- package/scripts/init.sh +57 -0
- package/scripts/migrate-apikeys-to-credentials.ts +35 -0
- package/scripts/sandbox-agent-flow.sh +235 -0
- package/scripts/sandbox.sh +175 -0
- package/scripts/validate-job-docs.mjs +125 -0
- package/server/abi/SwapHelper.json +438 -0
- package/server/cli/approval.ts +447 -0
- package/server/cli/commands/app.ts +204 -0
- package/server/cli/commands/cron.ts +24 -0
- package/server/cli/commands/doctor.ts +1007 -0
- package/server/cli/commands/env.ts +456 -0
- package/server/cli/commands/init.ts +752 -0
- package/server/cli/commands/mcp.ts +125 -0
- package/server/cli/commands/restore.ts +314 -0
- package/server/cli/commands/shell-hook.ts +468 -0
- package/server/cli/commands/start.ts +62 -0
- package/server/cli/commands/status.ts +59 -0
- package/server/cli/commands/stop.ts +14 -0
- package/server/cli/commands/token.ts +180 -0
- package/server/cli/commands/unlock.ts +49 -0
- package/server/cli/commands/vault.ts +417 -0
- package/server/cli/index.ts +328 -0
- package/server/cli/lib/aura-parser.ts +64 -0
- package/server/cli/lib/credential-create.ts +74 -0
- package/server/cli/lib/credential-resolve.ts +254 -0
- package/server/cli/lib/dotenv-migrate.ts +116 -0
- package/server/cli/lib/dotenv-parser.ts +146 -0
- package/server/cli/lib/http.ts +91 -0
- package/server/cli/lib/init-steps.ts +76 -0
- package/server/cli/lib/local-agent-trust.ts +45 -0
- package/server/cli/lib/process.ts +136 -0
- package/server/cli/lib/prompt.ts +85 -0
- package/server/cli/lib/theme.ts +240 -0
- package/server/cli/socket.ts +570 -0
- package/server/cli/transport-client.ts +50 -0
- package/server/cron/index.ts +137 -0
- package/server/cron/job.ts +31 -0
- package/server/cron/jobs/balance-sync.ts +436 -0
- package/server/cron/jobs/incoming-scan.ts +506 -0
- package/server/cron/jobs/native-price.ts +70 -0
- package/server/cron/jobs/orphan-cleanup.ts +40 -0
- package/server/cron/jobs/strategy-runner.ts +175 -0
- package/server/cron/scheduler.ts +125 -0
- package/server/index.ts +406 -0
- package/server/lib/adapters/factory.ts +110 -0
- package/server/lib/adapters/index.ts +19 -0
- package/server/lib/adapters/router.ts +297 -0
- package/server/lib/adapters/telegram.ts +645 -0
- package/server/lib/adapters/types.ts +89 -0
- package/server/lib/adapters/webhook.ts +95 -0
- package/server/lib/address.ts +49 -0
- package/server/lib/agent-auth/contracts.ts +1194 -0
- package/server/lib/agent-profiles.ts +328 -0
- package/server/lib/ai.ts +285 -0
- package/server/lib/api-registry/contracts.ts +86 -0
- package/server/lib/api-registry/validation.ts +172 -0
- package/server/lib/apikey-migration.ts +189 -0
- package/server/lib/app-installer.ts +505 -0
- package/server/lib/app-tokens.ts +247 -0
- package/server/lib/auth.ts +314 -0
- package/server/lib/batch.ts +242 -0
- package/server/lib/cold.ts +874 -0
- package/server/lib/config.ts +381 -0
- package/server/lib/credential-access-audit.ts +85 -0
- package/server/lib/credential-access-policy.ts +110 -0
- package/server/lib/credential-health.ts +343 -0
- package/server/lib/credential-import.ts +487 -0
- package/server/lib/credential-scope.ts +87 -0
- package/server/lib/credential-shares.ts +190 -0
- package/server/lib/credential-transport.ts +342 -0
- package/server/lib/credential-vault.ts +77 -0
- package/server/lib/credentials.ts +333 -0
- package/server/lib/crypto.ts +8 -0
- package/server/lib/db.ts +15 -0
- package/server/lib/defaults.ts +366 -0
- package/server/lib/dex/index.ts +80 -0
- package/server/lib/dex/relay.ts +235 -0
- package/server/lib/dex/types.ts +59 -0
- package/server/lib/dex/uniswap.ts +370 -0
- package/server/lib/e2e-agent/artifacts.ts +36 -0
- package/server/lib/e2e-agent/contracts.ts +112 -0
- package/server/lib/e2e-agent/validation.ts +135 -0
- package/server/lib/encrypt.ts +128 -0
- package/server/lib/error.ts +20 -0
- package/server/lib/events.ts +205 -0
- package/server/lib/hot.ts +357 -0
- package/server/lib/key-fingerprint.ts +28 -0
- package/server/lib/logger.ts +331 -0
- package/server/lib/network.ts +137 -0
- package/server/lib/notifications.ts +219 -0
- package/server/lib/oauth2-refresh.ts +241 -0
- package/server/lib/oursecret.ts +54 -0
- package/server/lib/passkey-credential.ts +360 -0
- package/server/lib/passkey.ts +68 -0
- package/server/lib/permissions.ts +248 -0
- package/server/lib/pino.ts +24 -0
- package/server/lib/policy-preview.ts +138 -0
- package/server/lib/price.ts +338 -0
- package/server/lib/prices.ts +34 -0
- package/server/lib/project-scope.ts +239 -0
- package/server/lib/resolve-action.ts +427 -0
- package/server/lib/resolve.ts +36 -0
- package/server/lib/sessions.ts +632 -0
- package/server/lib/solana/connection.ts +26 -0
- package/server/lib/solana/jupiter.ts +128 -0
- package/server/lib/solana/transfer.ts +108 -0
- package/server/lib/solana/wallet.ts +136 -0
- package/server/lib/strategy/emits.ts +21 -0
- package/server/lib/strategy/engine.ts +1305 -0
- package/server/lib/strategy/executor.ts +115 -0
- package/server/lib/strategy/hook-context.ts +158 -0
- package/server/lib/strategy/hooks.ts +990 -0
- package/server/lib/strategy/index.ts +28 -0
- package/server/lib/strategy/installer.ts +305 -0
- package/server/lib/strategy/loader.ts +256 -0
- package/server/lib/strategy/message.ts +235 -0
- package/server/lib/strategy/repository.ts +218 -0
- package/server/lib/strategy/session-logger.ts +693 -0
- package/server/lib/strategy/sources.ts +288 -0
- package/server/lib/strategy/state.ts +189 -0
- package/server/lib/strategy/templates.ts +403 -0
- package/server/lib/strategy/tick.ts +404 -0
- package/server/lib/strategy/types.ts +230 -0
- package/server/lib/swap.ts +3 -0
- package/server/lib/temp.ts +86 -0
- package/server/lib/token-metadata.ts +86 -0
- package/server/lib/token-safety.ts +200 -0
- package/server/lib/token-search.ts +444 -0
- package/server/lib/totp.ts +194 -0
- package/server/lib/transactions.ts +123 -0
- package/server/lib/transport.ts +75 -0
- package/server/lib/txhistory/decoder.ts +262 -0
- package/server/lib/txhistory/enricher.ts +652 -0
- package/server/lib/txhistory/index.ts +391 -0
- package/server/lib/txhistory/signatures.ts +59 -0
- package/server/lib/verified-summary.ts +421 -0
- package/server/mcp/profile-policy.ts +30 -0
- package/server/mcp/server.ts +619 -0
- package/server/mcp/tools.ts +523 -0
- package/server/middleware/auth.ts +119 -0
- package/server/middleware/requestLogger.ts +84 -0
- package/server/routes/actions.ts +459 -0
- package/server/routes/adapters.ts +703 -0
- package/server/routes/addressbook.ts +113 -0
- package/server/routes/ai.ts +34 -0
- package/server/routes/apikeys.ts +295 -0
- package/server/routes/apps.ts +601 -0
- package/server/routes/auth.ts +457 -0
- package/server/routes/backup.ts +340 -0
- package/server/routes/batch.ts +270 -0
- package/server/routes/bookmarks.ts +162 -0
- package/server/routes/credential-shares.ts +198 -0
- package/server/routes/credential-vaults.ts +154 -0
- package/server/routes/credentials.ts +1290 -0
- package/server/routes/dashboard.ts +71 -0
- package/server/routes/defaults.ts +124 -0
- package/server/routes/fund.ts +229 -0
- package/server/routes/import.ts +352 -0
- package/server/routes/launch.ts +665 -0
- package/server/routes/lock.ts +54 -0
- package/server/routes/logs.ts +68 -0
- package/server/routes/nuke.ts +111 -0
- package/server/routes/passkey-credentials.ts +99 -0
- package/server/routes/passkey.ts +346 -0
- package/server/routes/portfolio.ts +217 -0
- package/server/routes/price.ts +63 -0
- package/server/routes/resolve.ts +31 -0
- package/server/routes/security.ts +45 -0
- package/server/routes/send-evm.ts +241 -0
- package/server/routes/send-solana.ts +281 -0
- package/server/routes/send.ts +178 -0
- package/server/routes/setup.ts +210 -0
- package/server/routes/strategy.ts +894 -0
- package/server/routes/swap-evm.ts +353 -0
- package/server/routes/swap-solana.ts +177 -0
- package/server/routes/swap.ts +356 -0
- package/server/routes/token.ts +247 -0
- package/server/routes/unlock.ts +403 -0
- package/server/routes/wallet-assets.ts +361 -0
- package/server/routes/wallet-transactions.ts +515 -0
- package/server/routes/wallet.ts +710 -0
- package/server/types.ts +146 -0
- package/skills/aurawallet/SKILL.md +739 -0
- package/skills/aurawallet-setup/SKILL.md +74 -0
- package/skills/security-review/SKILL.md +148 -0
- package/src/app/api/agent-requests/route.ts +30 -0
- package/src/app/api/apps/install/route.ts +126 -0
- package/src/app/api/apps/manifests/route.ts +16 -0
- package/src/app/api/apps/static/[...path]/route.ts +57 -0
- package/src/app/api/events/route.ts +92 -0
- package/src/app/api/page.tsx +212 -0
- package/src/app/api/workspace/[id]/apps/[wid]/route.ts +119 -0
- package/src/app/api/workspace/[id]/apps/route.ts +81 -0
- package/src/app/api/workspace/[id]/export/route.ts +67 -0
- package/src/app/api/workspace/[id]/route.ts +168 -0
- package/src/app/api/workspace/auth.ts +34 -0
- package/src/app/api/workspace/config/route.ts +106 -0
- package/src/app/api/workspace/import/route.ts +127 -0
- package/src/app/api/workspace/route.ts +116 -0
- package/src/app/app/page.tsx +2122 -0
- package/src/app/apple-icon.png +0 -0
- package/src/app/docs/page.tsx +178 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +572 -0
- package/src/app/health/page.tsx +5 -0
- package/src/app/hello/page.tsx +15 -0
- package/src/app/icon.png +0 -0
- package/src/app/layout.tsx +34 -0
- package/src/app/page.tsx +986 -0
- package/src/app/providers.tsx +90 -0
- package/src/app/share/[token]/page.tsx +295 -0
- package/src/components/ChainSelector.tsx +144 -0
- package/src/components/HumanActionBar.tsx +695 -0
- package/src/components/NotificationDrawer.tsx +129 -0
- package/src/components/apps/AgentKeysApp.tsx +490 -0
- package/src/components/apps/App.tsx +153 -0
- package/src/components/apps/AppGrid.tsx +15 -0
- package/src/components/apps/DetailedAddressDrawer.tsx +325 -0
- package/src/components/apps/DraggableApp.tsx +562 -0
- package/src/components/apps/IFrameApp.tsx +73 -0
- package/src/components/apps/LogsApp.tsx +360 -0
- package/src/components/apps/SendApp.tsx +394 -0
- package/src/components/apps/SetupWizardApp.tsx +1004 -0
- package/src/components/apps/SystemDefaultsApp.tsx +845 -0
- package/src/components/apps/ThirdPartyApp.tsx +428 -0
- package/src/components/apps/TokenApp.tsx +319 -0
- package/src/components/apps/TransactionsApp.tsx +438 -0
- package/src/components/apps/WalletDetailApp.tsx +1505 -0
- package/src/components/apps/index.ts +13 -0
- package/src/components/design-system/Button.tsx +53 -0
- package/src/components/design-system/ChainIndicator.tsx +65 -0
- package/src/components/design-system/ChainSelector.tsx +137 -0
- package/src/components/design-system/ConfirmationModal.tsx +106 -0
- package/src/components/design-system/ConfirmationPopover.tsx +81 -0
- package/src/components/design-system/Drawer.tsx +123 -0
- package/src/components/design-system/FilterDropdown.tsx +72 -0
- package/src/components/design-system/Modal.tsx +206 -0
- package/src/components/design-system/Popover.tsx +142 -0
- package/src/components/design-system/TextInput.tsx +85 -0
- package/src/components/design-system/Toggle.tsx +58 -0
- package/src/components/design-system/index.ts +11 -0
- package/src/components/docs/DocsThemeToggle.tsx +49 -0
- package/src/components/health/CredentialHealthDashboard.tsx +214 -0
- package/src/components/icons/ChainIcons.tsx +72 -0
- package/src/components/layout/AppStoreDrawer.tsx +369 -0
- package/src/components/layout/ContentArea.tsx +21 -0
- package/src/components/layout/TabBar.tsx +278 -0
- package/src/components/layout/WalletSidebar.tsx +1033 -0
- package/src/components/layout/index.ts +4 -0
- package/src/components/marketing/AuraWalletSpecOverlay.tsx +635 -0
- package/src/components/marketing/DeviceMorphExperience.tsx +216 -0
- package/src/components/vault/ApiKeysConsole.tsx +1080 -0
- package/src/components/vault/AuditConsole.tsx +584 -0
- package/src/components/vault/CredentialDetail.tsx +455 -0
- package/src/components/vault/CredentialEmpty.tsx +55 -0
- package/src/components/vault/CredentialField.tsx +361 -0
- package/src/components/vault/CredentialForm.tsx +1212 -0
- package/src/components/vault/CredentialList.tsx +165 -0
- package/src/components/vault/CredentialRow.tsx +97 -0
- package/src/components/vault/CredentialShareModal.tsx +178 -0
- package/src/components/vault/CredentialVault.tsx +754 -0
- package/src/components/vault/CredentialWalletWidget.tsx +103 -0
- package/src/components/vault/ImportCredentialsModal.tsx +515 -0
- package/src/components/vault/LargeTypeModal.tsx +64 -0
- package/src/components/vault/PasswordGenerator.tsx +224 -0
- package/src/components/vault/TOTPDisplay.tsx +123 -0
- package/src/components/vault/VaultSidebar.tsx +413 -0
- package/src/components/vault/types.ts +54 -0
- package/src/context/AuthContext.tsx +337 -0
- package/src/context/PriceContext.tsx +113 -0
- package/src/context/ThemeContext.tsx +164 -0
- package/src/context/WebSocketContext.tsx +269 -0
- package/src/context/WorkspaceContext.tsx +668 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useAgentActions.ts +368 -0
- package/src/hooks/useBalance.ts +103 -0
- package/src/hooks/useBalances.ts +129 -0
- package/src/instrumentation.ts +12 -0
- package/src/lib/api.ts +449 -0
- package/src/lib/app-loader.ts +148 -0
- package/src/lib/app-registry.ts +178 -0
- package/src/lib/app-sdk.ts +157 -0
- package/src/lib/audit-console-adapter.ts +151 -0
- package/src/lib/auth-client.ts +75 -0
- package/src/lib/config.ts +74 -0
- package/src/lib/crypto.ts +112 -0
- package/src/lib/db.ts +21 -0
- package/src/lib/docs.ts +390 -0
- package/src/lib/events.ts +361 -0
- package/src/lib/pino.ts +24 -0
- package/src/lib/theme-handlers.ts +168 -0
- package/src/lib/theme.ts +351 -0
- package/src/lib/tokenData.ts +378 -0
- package/src/lib/vault-crypto.ts +129 -0
- package/src/lib/websocket-server.ts +302 -0
- package/src/lib/websocket-setup.ts +79 -0
- package/src/lib/wordlist.ts +2050 -0
- package/src/lib/workspace-handlers.ts +285 -0
- package/start.sh +80 -0
- package/tailwind.config.ts +99 -0
- package/tsconfig.json +42 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
2
|
+
import type { IncomingMessage } from 'http';
|
|
3
|
+
import type { Duplex } from 'stream';
|
|
4
|
+
import type { WalletEvent, WorkspaceEvent, ThemeEvent } from './events';
|
|
5
|
+
import { WORKSPACE_EVENTS, THEME_EVENTS, APP_EVENTS } from './events';
|
|
6
|
+
import { handleWorkspaceMessage } from './workspace-handlers';
|
|
7
|
+
import { handleThemeMessage } from './theme-handlers';
|
|
8
|
+
import { validateToken, hasAnyPermission, TokenValidationResult } from './auth-client';
|
|
9
|
+
import { log as rootLog } from './pino';
|
|
10
|
+
|
|
11
|
+
const log = rootLog.child({ component: 'ws' });
|
|
12
|
+
|
|
13
|
+
// Singleton WebSocket server for Next.js
|
|
14
|
+
let wss: WebSocketServer | null = null;
|
|
15
|
+
const clients = new Set<WebSocket>();
|
|
16
|
+
|
|
17
|
+
// Heartbeat interval (30 seconds)
|
|
18
|
+
const HEARTBEAT_INTERVAL = 30000;
|
|
19
|
+
let heartbeatTimer: NodeJS.Timeout | null = null;
|
|
20
|
+
|
|
21
|
+
interface ExtendedWebSocket extends WebSocket {
|
|
22
|
+
isAlive?: boolean;
|
|
23
|
+
auth?: TokenValidationResult; // Auth info for this connection
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Workspace mutation events that require workspace:modify permission
|
|
27
|
+
const WORKSPACE_MUTATION_EVENTS = [
|
|
28
|
+
WORKSPACE_EVENTS.WORKSPACE_CREATED,
|
|
29
|
+
WORKSPACE_EVENTS.WORKSPACE_DELETED,
|
|
30
|
+
WORKSPACE_EVENTS.WORKSPACE_UPDATED,
|
|
31
|
+
WORKSPACE_EVENTS.APP_ADDED,
|
|
32
|
+
WORKSPACE_EVENTS.APP_REMOVED,
|
|
33
|
+
WORKSPACE_EVENTS.APP_UPDATED,
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if client has permission to perform workspace mutations
|
|
38
|
+
* Requires valid token with workspace:modify permission (or admin)
|
|
39
|
+
*/
|
|
40
|
+
function canModifyWorkspace(ws: ExtendedWebSocket): boolean {
|
|
41
|
+
// No auth = deny (auth required for mutations)
|
|
42
|
+
if (!ws.auth) return false;
|
|
43
|
+
|
|
44
|
+
// Invalid token = deny
|
|
45
|
+
if (!ws.auth.valid) return false;
|
|
46
|
+
|
|
47
|
+
// Admin always allowed
|
|
48
|
+
if (ws.auth.isAdmin) return true;
|
|
49
|
+
|
|
50
|
+
// Check for workspace:modify permission
|
|
51
|
+
return hasAnyPermission(ws.auth, ['workspace:modify']);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get or create the WebSocket server singleton
|
|
56
|
+
*/
|
|
57
|
+
export function getWebSocketServer(): WebSocketServer {
|
|
58
|
+
if (!wss) {
|
|
59
|
+
wss = new WebSocketServer({ noServer: true });
|
|
60
|
+
|
|
61
|
+
wss.on('connection', (ws: ExtendedWebSocket) => {
|
|
62
|
+
ws.isAlive = true;
|
|
63
|
+
clients.add(ws);
|
|
64
|
+
|
|
65
|
+
log.debug({ clients: clients.size }, 'client connected');
|
|
66
|
+
|
|
67
|
+
ws.on('pong', () => {
|
|
68
|
+
ws.isAlive = true;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
ws.on('message', async (data) => {
|
|
72
|
+
try {
|
|
73
|
+
const msg = JSON.parse(data.toString());
|
|
74
|
+
|
|
75
|
+
// Handle auth message — client sends token after connecting
|
|
76
|
+
if (msg.type === 'auth' && typeof msg.token === 'string') {
|
|
77
|
+
try {
|
|
78
|
+
const result = await validateToken(msg.token);
|
|
79
|
+
ws.auth = result;
|
|
80
|
+
if (result.valid) {
|
|
81
|
+
log.info({ agentId: result.payload?.agentId || 'admin' }, 'client authenticated');
|
|
82
|
+
} else {
|
|
83
|
+
log.warn({ error: result.error }, 'invalid token provided');
|
|
84
|
+
}
|
|
85
|
+
ws.send(JSON.stringify({ type: 'authenticated', valid: result.valid }));
|
|
86
|
+
} catch (err) {
|
|
87
|
+
log.error({ err }, 'token validation failed');
|
|
88
|
+
ws.send(JSON.stringify({ type: 'authenticated', valid: false }));
|
|
89
|
+
}
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const workspaceEventTypes = Object.values(WORKSPACE_EVENTS);
|
|
94
|
+
const themeEventTypes = Object.values(THEME_EVENTS);
|
|
95
|
+
|
|
96
|
+
// Check if this is a workspace event
|
|
97
|
+
if (workspaceEventTypes.includes(msg.type)) {
|
|
98
|
+
// Check permissions for mutation events
|
|
99
|
+
if (WORKSPACE_MUTATION_EVENTS.includes(msg.type)) {
|
|
100
|
+
if (!canModifyWorkspace(ws)) {
|
|
101
|
+
ws.send(JSON.stringify({
|
|
102
|
+
type: 'error',
|
|
103
|
+
error: 'Permission denied: workspace:modify required',
|
|
104
|
+
originalType: msg.type
|
|
105
|
+
}));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const response = await handleWorkspaceMessage(msg as WorkspaceEvent);
|
|
111
|
+
|
|
112
|
+
// If there's a response (e.g., state:response), send it back to the requesting client
|
|
113
|
+
if (response) {
|
|
114
|
+
ws.send(JSON.stringify(response));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// For mutations, broadcast to all other clients
|
|
118
|
+
if (WORKSPACE_MUTATION_EVENTS.includes(msg.type)) {
|
|
119
|
+
broadcastExcept(msg, ws);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Check if this is a theme event
|
|
123
|
+
else if (themeEventTypes.includes(msg.type)) {
|
|
124
|
+
// Theme changes also require workspace:modify
|
|
125
|
+
const themeMutations = [
|
|
126
|
+
THEME_EVENTS.THEME_MODE_CHANGED,
|
|
127
|
+
THEME_EVENTS.THEME_ACCENT_CHANGED,
|
|
128
|
+
THEME_EVENTS.THEME_UPDATED,
|
|
129
|
+
THEME_EVENTS.WORKSPACE_THEME_UPDATED
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
if (themeMutations.includes(msg.type) && !canModifyWorkspace(ws)) {
|
|
133
|
+
ws.send(JSON.stringify({
|
|
134
|
+
type: 'error',
|
|
135
|
+
error: 'Permission denied: workspace:modify required',
|
|
136
|
+
originalType: msg.type
|
|
137
|
+
}));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const response = await handleThemeMessage(msg as ThemeEvent);
|
|
142
|
+
|
|
143
|
+
// If there's a response (e.g., theme:response), send it back to the requesting client
|
|
144
|
+
if (response) {
|
|
145
|
+
ws.send(JSON.stringify(response));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// For mutations, broadcast to all other clients
|
|
149
|
+
if (themeMutations.includes(msg.type)) {
|
|
150
|
+
broadcastExcept(msg, ws);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Check if this is a app event
|
|
154
|
+
else if (Object.values(APP_EVENTS).includes(msg.type)) {
|
|
155
|
+
// app:message — broadcast to all clients (agent picks it up)
|
|
156
|
+
if (msg.type === APP_EVENTS.APP_MESSAGE) {
|
|
157
|
+
broadcastExcept(msg, ws);
|
|
158
|
+
}
|
|
159
|
+
// app:response — broadcast to all clients (host bridge picks it up)
|
|
160
|
+
else if (msg.type === APP_EVENTS.APP_RESPONSE) {
|
|
161
|
+
broadcastExcept(msg, ws);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
} catch (err) {
|
|
165
|
+
log.error({ err }, 'failed to handle message');
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
ws.on('close', () => {
|
|
170
|
+
clients.delete(ws);
|
|
171
|
+
log.debug({ clients: clients.size }, 'client disconnected');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
ws.on('error', (err) => {
|
|
175
|
+
log.error({ err }, 'client error');
|
|
176
|
+
clients.delete(ws);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Send initial connection confirmation
|
|
180
|
+
ws.send(JSON.stringify({
|
|
181
|
+
type: 'connected',
|
|
182
|
+
timestamp: Date.now(),
|
|
183
|
+
}));
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Start heartbeat if not already running
|
|
187
|
+
if (!heartbeatTimer) {
|
|
188
|
+
heartbeatTimer = setInterval(() => {
|
|
189
|
+
clients.forEach((ws) => {
|
|
190
|
+
const extWs = ws as ExtendedWebSocket;
|
|
191
|
+
if (extWs.isAlive === false) {
|
|
192
|
+
clients.delete(ws);
|
|
193
|
+
return ws.terminate();
|
|
194
|
+
}
|
|
195
|
+
extWs.isAlive = false;
|
|
196
|
+
ws.ping();
|
|
197
|
+
});
|
|
198
|
+
}, HEARTBEAT_INTERVAL);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return wss;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Handle WebSocket upgrade request
|
|
207
|
+
*/
|
|
208
|
+
export function handleUpgrade(
|
|
209
|
+
request: IncomingMessage,
|
|
210
|
+
socket: Duplex,
|
|
211
|
+
head: Buffer
|
|
212
|
+
): void {
|
|
213
|
+
const server = getWebSocketServer();
|
|
214
|
+
server.handleUpgrade(request, socket, head, (ws) => {
|
|
215
|
+
server.emit('connection', ws, request);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Broadcast an event to all connected clients
|
|
221
|
+
*/
|
|
222
|
+
export function broadcast(event: WalletEvent | WorkspaceEvent): void {
|
|
223
|
+
const message = JSON.stringify(event);
|
|
224
|
+
let sent = 0;
|
|
225
|
+
|
|
226
|
+
clients.forEach((client) => {
|
|
227
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
228
|
+
client.send(message);
|
|
229
|
+
sent++;
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
log.debug({ type: event.type, sent, total: clients.size }, 'broadcast');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Broadcast an event to all connected clients except one
|
|
238
|
+
*/
|
|
239
|
+
export function broadcastExcept(event: WalletEvent | WorkspaceEvent, excludeClient: WebSocket): void {
|
|
240
|
+
const message = JSON.stringify(event);
|
|
241
|
+
let sent = 0;
|
|
242
|
+
|
|
243
|
+
clients.forEach((client) => {
|
|
244
|
+
if (client.readyState === WebSocket.OPEN && client !== excludeClient) {
|
|
245
|
+
client.send(message);
|
|
246
|
+
sent++;
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
log.debug({ type: event.type, sent, total: clients.size - 1 }, 'broadcast (excluding sender)');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Get number of connected clients
|
|
255
|
+
*/
|
|
256
|
+
export function getClientCount(): number {
|
|
257
|
+
return clients.size;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Get connected clients with their auth info
|
|
262
|
+
*/
|
|
263
|
+
export function getAuthenticatedClients(): Array<{
|
|
264
|
+
agentId: string;
|
|
265
|
+
isAdmin: boolean;
|
|
266
|
+
permissions: string[];
|
|
267
|
+
}> {
|
|
268
|
+
const result: Array<{ agentId: string; isAdmin: boolean; permissions: string[] }> = [];
|
|
269
|
+
|
|
270
|
+
clients.forEach((client) => {
|
|
271
|
+
const extWs = client as ExtendedWebSocket;
|
|
272
|
+
if (extWs.auth?.valid) {
|
|
273
|
+
result.push({
|
|
274
|
+
agentId: extWs.auth.payload?.agentId || 'admin',
|
|
275
|
+
isAdmin: extWs.auth.isAdmin || false,
|
|
276
|
+
permissions: extWs.auth.payload?.permissions || ['admin:*']
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Cleanup WebSocket server (for testing/shutdown)
|
|
286
|
+
*/
|
|
287
|
+
export function closeWebSocketServer(): void {
|
|
288
|
+
if (heartbeatTimer) {
|
|
289
|
+
clearInterval(heartbeatTimer);
|
|
290
|
+
heartbeatTimer = null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
clients.forEach((client) => {
|
|
294
|
+
client.close();
|
|
295
|
+
});
|
|
296
|
+
clients.clear();
|
|
297
|
+
|
|
298
|
+
if (wss) {
|
|
299
|
+
wss.close();
|
|
300
|
+
wss = null;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket server setup for Next.js
|
|
3
|
+
* Runs on a separate port since Next.js API routes don't support WebSocket upgrades
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createServer } from 'http';
|
|
7
|
+
import { getWebSocketServer, broadcast, getClientCount } from './websocket-server';
|
|
8
|
+
import { ensureDefaultWorkspace } from './workspace-handlers';
|
|
9
|
+
import type { WalletEvent } from './events';
|
|
10
|
+
import { log as rootLog } from './pino';
|
|
11
|
+
|
|
12
|
+
const log = rootLog.child({ component: 'ws' });
|
|
13
|
+
|
|
14
|
+
const WS_PORT = parseInt(process.env.WS_PORT || '4748', 10);
|
|
15
|
+
|
|
16
|
+
let isSetup = false;
|
|
17
|
+
let httpServer: ReturnType<typeof createServer> | null = null;
|
|
18
|
+
|
|
19
|
+
export function setupWebSocketServer() {
|
|
20
|
+
if (isSetup) return;
|
|
21
|
+
isSetup = true;
|
|
22
|
+
|
|
23
|
+
// Create a simple HTTP server for WebSocket upgrades and broadcast endpoint
|
|
24
|
+
httpServer = createServer((req, res) => {
|
|
25
|
+
// Health check endpoint
|
|
26
|
+
if (req.url === '/health') {
|
|
27
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
28
|
+
res.end(JSON.stringify({ status: 'ok', websocket: true, clients: getClientCount() }));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Broadcast endpoint - receives events from Express and broadcasts to WebSocket clients
|
|
33
|
+
if (req.url === '/broadcast' && req.method === 'POST') {
|
|
34
|
+
let body = '';
|
|
35
|
+
req.on('data', (chunk) => {
|
|
36
|
+
body += chunk.toString();
|
|
37
|
+
});
|
|
38
|
+
req.on('end', () => {
|
|
39
|
+
try {
|
|
40
|
+
const event: WalletEvent = JSON.parse(body);
|
|
41
|
+
broadcast(event);
|
|
42
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
43
|
+
res.end(JSON.stringify({ success: true, type: event.type }));
|
|
44
|
+
} catch (err) {
|
|
45
|
+
log.error({ err }, 'failed to parse broadcast event');
|
|
46
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
47
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
res.writeHead(404);
|
|
54
|
+
res.end();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const wss = getWebSocketServer();
|
|
58
|
+
|
|
59
|
+
// Handle WebSocket upgrade
|
|
60
|
+
httpServer.on('upgrade', (request, socket, head) => {
|
|
61
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
62
|
+
wss.emit('connection', ws, request);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
httpServer.listen(WS_PORT, '127.0.0.1', async () => {
|
|
67
|
+
log.info({ port: WS_PORT, url: `ws://127.0.0.1:${WS_PORT}` }, 'WebSocket server started');
|
|
68
|
+
// Ensure default workspace exists
|
|
69
|
+
try {
|
|
70
|
+
await ensureDefaultWorkspace();
|
|
71
|
+
} catch (err) {
|
|
72
|
+
log.error({ err }, 'failed to ensure default workspace');
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getWebSocketPort(): number {
|
|
78
|
+
return WS_PORT;
|
|
79
|
+
}
|