nterminal 1.2.0
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/.env.example +12 -0
- package/LICENSE +674 -0
- package/README.md +181 -0
- package/assets/brand/app-icon-1024.png +0 -0
- package/assets/brand/app-icon-384.png +0 -0
- package/assets/brand/apple-touch-icon-360.png +0 -0
- package/assets/brand/favicon-32.png +0 -0
- package/assets/brand/favicon-64.png +0 -0
- package/assets/brand/favicon-96.png +0 -0
- package/assets/brand/favicon.svg +4 -0
- package/assets/brand/nterminal-mark-64.png +0 -0
- package/assets/brand/nterminal-mark.svg +4 -0
- package/assets/brand/nterminal-wordmark-486x68.png +0 -0
- package/assets/brand/nterminal-wordmark.svg +3 -0
- package/assets/screenshot/scr.png +0 -0
- package/bin/nterminal.js +114 -0
- package/dist/client/apple-touch-icon.png +0 -0
- package/dist/client/assets/MarkdownPreview-BeDi-V7k.js +29 -0
- package/dist/client/assets/MesloLGS-NF-Bold-Italic-DwFsXcwX.ttf +0 -0
- package/dist/client/assets/MesloLGS-NF-Bold-kN-HYz-g.ttf +0 -0
- package/dist/client/assets/MesloLGS-NF-Italic-CMg1T6-G.ttf +0 -0
- package/dist/client/assets/MesloLGS-NF-Regular-Cxr8pvCI.ttf +0 -0
- package/dist/client/assets/index-BQkKYjXb.js +33 -0
- package/dist/client/assets/index-WqeS39wU.css +1 -0
- package/dist/client/assets/notifications/character-2258.mp4 +0 -0
- package/dist/client/assets/notifications/character-2260.mp4 +0 -0
- package/dist/client/assets/notifications/character-2272.mp4 +0 -0
- package/dist/client/brand/nterminal-mark-64.png +0 -0
- package/dist/client/brand/nterminal-mark.svg +4 -0
- package/dist/client/brand/nterminal-wordmark-486x68.png +0 -0
- package/dist/client/brand/nterminal-wordmark.svg +3 -0
- package/dist/client/icons/app-icon-1024.png +0 -0
- package/dist/client/icons/app-icon-384.png +0 -0
- package/dist/client/icons/favicon-32.png +0 -0
- package/dist/client/icons/favicon-64.png +0 -0
- package/dist/client/icons/favicon-96.png +0 -0
- package/dist/client/icons/favicon.svg +4 -0
- package/dist/client/index.html +21 -0
- package/dist/client/manifest.webmanifest +24 -0
- package/dist/scripts/generate-secrets.js +3 -0
- package/dist/scripts/generate-secrets.js.map +1 -0
- package/dist/scripts/onboarding.js +814 -0
- package/dist/scripts/onboarding.js.map +1 -0
- package/dist/scripts/proxySetup.js +1007 -0
- package/dist/scripts/proxySetup.js.map +1 -0
- package/dist/server/agent/agentAuth.d.ts +6 -0
- package/dist/server/agent/agentAuth.js +35 -0
- package/dist/server/agent/agentAuth.js.map +1 -0
- package/dist/server/agent/agentProxy.d.ts +5 -0
- package/dist/server/agent/agentProxy.js +63 -0
- package/dist/server/agent/agentProxy.js.map +1 -0
- package/dist/server/agent/agentRoutes.d.ts +9 -0
- package/dist/server/agent/agentRoutes.js +327 -0
- package/dist/server/agent/agentRoutes.js.map +1 -0
- package/dist/server/agent/agentWebSocketProxy.d.ts +3 -0
- package/dist/server/agent/agentWebSocketProxy.js +65 -0
- package/dist/server/agent/agentWebSocketProxy.js.map +1 -0
- package/dist/server/auth/authService.d.ts +100 -0
- package/dist/server/auth/authService.js +415 -0
- package/dist/server/auth/authService.js.map +1 -0
- package/dist/server/auth/cookies.d.ts +11 -0
- package/dist/server/auth/cookies.js +39 -0
- package/dist/server/auth/cookies.js.map +1 -0
- package/dist/server/auth/ipMatch.d.ts +14 -0
- package/dist/server/auth/ipMatch.js +103 -0
- package/dist/server/auth/ipMatch.js.map +1 -0
- package/dist/server/auth/rateLimit.d.ts +17 -0
- package/dist/server/auth/rateLimit.js +25 -0
- package/dist/server/auth/rateLimit.js.map +1 -0
- package/dist/server/auth/totpService.d.ts +10 -0
- package/dist/server/auth/totpService.js +37 -0
- package/dist/server/auth/totpService.js.map +1 -0
- package/dist/server/config.d.ts +27 -0
- package/dist/server/config.js +138 -0
- package/dist/server/config.js.map +1 -0
- package/dist/server/files/fileExplorerService.d.ts +38 -0
- package/dist/server/files/fileExplorerService.js +551 -0
- package/dist/server/files/fileExplorerService.js.map +1 -0
- package/dist/server/files/rootToken.d.ts +51 -0
- package/dist/server/files/rootToken.js +139 -0
- package/dist/server/files/rootToken.js.map +1 -0
- package/dist/server/http.d.ts +13 -0
- package/dist/server/http.js +69 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +45 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/routes/agentManagementRoutes.d.ts +9 -0
- package/dist/server/routes/agentManagementRoutes.js +304 -0
- package/dist/server/routes/agentManagementRoutes.js.map +1 -0
- package/dist/server/routes/authRoutes.d.ts +10 -0
- package/dist/server/routes/authRoutes.js +95 -0
- package/dist/server/routes/authRoutes.js.map +1 -0
- package/dist/server/routes/fileRoutes.d.ts +11 -0
- package/dist/server/routes/fileRoutes.js +185 -0
- package/dist/server/routes/fileRoutes.js.map +1 -0
- package/dist/server/routes/notificationAssetRoutes.d.ts +9 -0
- package/dist/server/routes/notificationAssetRoutes.js +280 -0
- package/dist/server/routes/notificationAssetRoutes.js.map +1 -0
- package/dist/server/routes/securityRoutes.d.ts +7 -0
- package/dist/server/routes/securityRoutes.js +53 -0
- package/dist/server/routes/securityRoutes.js.map +1 -0
- package/dist/server/routes/socketBackpressure.d.ts +26 -0
- package/dist/server/routes/socketBackpressure.js +63 -0
- package/dist/server/routes/socketBackpressure.js.map +1 -0
- package/dist/server/routes/terminalLayoutRoutes.d.ts +9 -0
- package/dist/server/routes/terminalLayoutRoutes.js +108 -0
- package/dist/server/routes/terminalLayoutRoutes.js.map +1 -0
- package/dist/server/routes/terminalRoutes.d.ts +14 -0
- package/dist/server/routes/terminalRoutes.js +177 -0
- package/dist/server/routes/terminalRoutes.js.map +1 -0
- package/dist/server/routes/terminalWebSocket.d.ts +9 -0
- package/dist/server/routes/terminalWebSocket.js +129 -0
- package/dist/server/routes/terminalWebSocket.js.map +1 -0
- package/dist/server/routes/totpRoutes.d.ts +7 -0
- package/dist/server/routes/totpRoutes.js +46 -0
- package/dist/server/routes/totpRoutes.js.map +1 -0
- package/dist/server/routes/updateRoutes.d.ts +7 -0
- package/dist/server/routes/updateRoutes.js +24 -0
- package/dist/server/routes/updateRoutes.js.map +1 -0
- package/dist/server/routes/uploadRoutes.d.ts +9 -0
- package/dist/server/routes/uploadRoutes.js +95 -0
- package/dist/server/routes/uploadRoutes.js.map +1 -0
- package/dist/server/storage/fileStore.d.ts +90 -0
- package/dist/server/storage/fileStore.js +275 -0
- package/dist/server/storage/fileStore.js.map +1 -0
- package/dist/server/system/stats.d.ts +2 -0
- package/dist/server/system/stats.js +37 -0
- package/dist/server/system/stats.js.map +1 -0
- package/dist/server/terminal/NodePtyAdapter.d.ts +4 -0
- package/dist/server/terminal/NodePtyAdapter.js +14 -0
- package/dist/server/terminal/NodePtyAdapter.js.map +1 -0
- package/dist/server/terminal/PtyAdapter.d.ts +57 -0
- package/dist/server/terminal/PtyAdapter.js +2 -0
- package/dist/server/terminal/PtyAdapter.js.map +1 -0
- package/dist/server/terminal/TerminalManager.d.ts +74 -0
- package/dist/server/terminal/TerminalManager.js +561 -0
- package/dist/server/terminal/TerminalManager.js.map +1 -0
- package/dist/server/terminal/TmuxPtyAdapter.d.ts +25 -0
- package/dist/server/terminal/TmuxPtyAdapter.js +543 -0
- package/dist/server/terminal/TmuxPtyAdapter.js.map +1 -0
- package/dist/server/terminal/codexTranscriptSource.d.ts +9 -0
- package/dist/server/terminal/codexTranscriptSource.js +144 -0
- package/dist/server/terminal/codexTranscriptSource.js.map +1 -0
- package/dist/server/terminal/cwdResolver.d.ts +8 -0
- package/dist/server/terminal/cwdResolver.js +37 -0
- package/dist/server/terminal/cwdResolver.js.map +1 -0
- package/dist/server/terminal/outputBuffer.d.ts +7 -0
- package/dist/server/terminal/outputBuffer.js +17 -0
- package/dist/server/terminal/outputBuffer.js.map +1 -0
- package/dist/server/terminal/transcriptHistory.d.ts +7 -0
- package/dist/server/terminal/transcriptHistory.js +315 -0
- package/dist/server/terminal/transcriptHistory.js.map +1 -0
- package/dist/server/update/gitUpdate.d.ts +27 -0
- package/dist/server/update/gitUpdate.js +241 -0
- package/dist/server/update/gitUpdate.js.map +1 -0
- package/dist/server/uploads/uploadPaths.d.ts +18 -0
- package/dist/server/uploads/uploadPaths.js +116 -0
- package/dist/server/uploads/uploadPaths.js.map +1 -0
- package/dist/server/uploads/uploadService.d.ts +21 -0
- package/dist/server/uploads/uploadService.js +230 -0
- package/dist/server/uploads/uploadService.js.map +1 -0
- package/dist/shared/layoutState.d.ts +6 -0
- package/dist/shared/layoutState.js +115 -0
- package/dist/shared/layoutState.js.map +1 -0
- package/dist/shared/notificationAssets.d.ts +9 -0
- package/dist/shared/notificationAssets.js +27 -0
- package/dist/shared/notificationAssets.js.map +1 -0
- package/dist/shared/protocol.d.ts +308 -0
- package/dist/shared/protocol.js +29 -0
- package/dist/shared/protocol.js.map +1 -0
- package/dist/shared/types.d.ts +56 -0
- package/dist/shared/types.js +2 -0
- package/dist/shared/types.js.map +1 -0
- package/docs/assets/nterminal-workspace.png +0 -0
- package/docs/configuration.md +97 -0
- package/docs/features.md +126 -0
- package/docs/onboarding.md +122 -0
- package/docs/operations.md +112 -0
- package/docs/terminal-history.md +54 -0
- package/package.json +85 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/assets/notifications/character-2258.mp4 +0 -0
- package/public/assets/notifications/character-2260.mp4 +0 -0
- package/public/assets/notifications/character-2272.mp4 +0 -0
- package/public/brand/nterminal-mark-64.png +0 -0
- package/public/brand/nterminal-mark.svg +4 -0
- package/public/brand/nterminal-wordmark-486x68.png +0 -0
- package/public/brand/nterminal-wordmark.svg +3 -0
- package/public/icons/app-icon-1024.png +0 -0
- package/public/icons/app-icon-384.png +0 -0
- package/public/icons/favicon-32.png +0 -0
- package/public/icons/favicon-64.png +0 -0
- package/public/icons/favicon-96.png +0 -0
- package/public/icons/favicon.svg +4 -0
- package/public/manifest.webmanifest +24 -0
- package/scripts/nterminalctl +588 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { createHmac, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
const ROOT_TOKEN_PREFIX = 'frt1';
|
|
3
|
+
const DELETE_PREVIEW_TOKEN_PREFIX = 'fdt1';
|
|
4
|
+
export class FileRootTokenError extends Error {
|
|
5
|
+
code = 'invalid_root_token';
|
|
6
|
+
constructor() {
|
|
7
|
+
super('Invalid file root token');
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export class FileDeletePreviewTokenError extends Error {
|
|
11
|
+
code = 'invalid_delete_preview_token';
|
|
12
|
+
constructor() {
|
|
13
|
+
super('Invalid file delete preview token');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function signFileRootToken(config, options) {
|
|
17
|
+
const issuedAt = (options.now ?? (() => new Date()))().toISOString();
|
|
18
|
+
const payload = {
|
|
19
|
+
version: 1,
|
|
20
|
+
terminalId: options.terminalId,
|
|
21
|
+
rootPath: options.rootPath,
|
|
22
|
+
issuedAt,
|
|
23
|
+
nonce: (options.nonce ?? randomUUID)()
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
rootToken: signToken(ROOT_TOKEN_PREFIX, config.sessionSecret, payload),
|
|
27
|
+
issuedAt
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function verifyFileRootToken(config, token) {
|
|
31
|
+
const payload = verifyToken(config.sessionSecret, token, ROOT_TOKEN_PREFIX, () => new FileRootTokenError());
|
|
32
|
+
if (!isFileRootTokenPayload(payload)) {
|
|
33
|
+
throw new FileRootTokenError();
|
|
34
|
+
}
|
|
35
|
+
return payload;
|
|
36
|
+
}
|
|
37
|
+
export function signFileDeletePreviewToken(config, options) {
|
|
38
|
+
const payload = {
|
|
39
|
+
version: 1,
|
|
40
|
+
rootTokenHash: tokenHash(config.sessionSecret, options.rootToken),
|
|
41
|
+
path: options.path,
|
|
42
|
+
kind: options.kind,
|
|
43
|
+
descendantCount: options.descendantCount,
|
|
44
|
+
entryVersion: options.entryVersion,
|
|
45
|
+
issuedAt: (options.now ?? (() => new Date()))().toISOString(),
|
|
46
|
+
nonce: (options.nonce ?? randomUUID)()
|
|
47
|
+
};
|
|
48
|
+
return signToken(DELETE_PREVIEW_TOKEN_PREFIX, config.sessionSecret, payload);
|
|
49
|
+
}
|
|
50
|
+
export function verifyFileDeletePreviewToken(config, token, rootToken, expected) {
|
|
51
|
+
const payload = verifyToken(config.sessionSecret, token, DELETE_PREVIEW_TOKEN_PREFIX, () => new FileDeletePreviewTokenError());
|
|
52
|
+
if (!isFileDeletePreviewTokenPayload(payload)) {
|
|
53
|
+
throw new FileDeletePreviewTokenError();
|
|
54
|
+
}
|
|
55
|
+
if (payload.rootTokenHash !== tokenHash(config.sessionSecret, rootToken) ||
|
|
56
|
+
payload.path !== expected.path ||
|
|
57
|
+
payload.kind !== expected.kind ||
|
|
58
|
+
payload.descendantCount !== expected.descendantCount ||
|
|
59
|
+
!sameFileVersion(payload.entryVersion, expected.entryVersion)) {
|
|
60
|
+
throw new FileDeletePreviewTokenError();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function signToken(prefix, secret, payload) {
|
|
64
|
+
const payloadSegment = Buffer.from(JSON.stringify(payload), 'utf8').toString('base64url');
|
|
65
|
+
const signature = signatureSegment(secret, `${prefix}.${payloadSegment}`);
|
|
66
|
+
return `${prefix}.${payloadSegment}.${signature}`;
|
|
67
|
+
}
|
|
68
|
+
function verifyToken(secret, token, expectedPrefix, error) {
|
|
69
|
+
const parts = token.split('.');
|
|
70
|
+
if (parts.length !== 3 || parts[0] !== expectedPrefix || !parts[1] || !parts[2]) {
|
|
71
|
+
throw error();
|
|
72
|
+
}
|
|
73
|
+
const signedValue = `${parts[0]}.${parts[1]}`;
|
|
74
|
+
const expectedSignature = signatureSegment(secret, signedValue);
|
|
75
|
+
if (!timingSafeEqualString(parts[2], expectedSignature)) {
|
|
76
|
+
throw error();
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
return JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
throw error();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function signatureSegment(secret, value) {
|
|
86
|
+
return createHmac('sha256', secret).update(value).digest('base64url');
|
|
87
|
+
}
|
|
88
|
+
function tokenHash(secret, token) {
|
|
89
|
+
return createHmac('sha256', secret).update(token).digest('base64url');
|
|
90
|
+
}
|
|
91
|
+
function timingSafeEqualString(actual, expected) {
|
|
92
|
+
const actualBuffer = Buffer.from(actual, 'base64url');
|
|
93
|
+
const expectedBuffer = Buffer.from(expected, 'base64url');
|
|
94
|
+
return actualBuffer.length === expectedBuffer.length && timingSafeEqual(actualBuffer, expectedBuffer);
|
|
95
|
+
}
|
|
96
|
+
function isFileRootTokenPayload(value) {
|
|
97
|
+
if (!value || typeof value !== 'object') {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
const payload = value;
|
|
101
|
+
return (payload.version === 1 &&
|
|
102
|
+
typeof payload.terminalId === 'string' &&
|
|
103
|
+
payload.terminalId.length > 0 &&
|
|
104
|
+
typeof payload.rootPath === 'string' &&
|
|
105
|
+
payload.rootPath.length > 0 &&
|
|
106
|
+
typeof payload.issuedAt === 'string' &&
|
|
107
|
+
typeof payload.nonce === 'string' &&
|
|
108
|
+
payload.nonce.length > 0);
|
|
109
|
+
}
|
|
110
|
+
function isFileDeletePreviewTokenPayload(value) {
|
|
111
|
+
if (!value || typeof value !== 'object') {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
const payload = value;
|
|
115
|
+
return (payload.version === 1 &&
|
|
116
|
+
typeof payload.rootTokenHash === 'string' &&
|
|
117
|
+
typeof payload.path === 'string' &&
|
|
118
|
+
typeof payload.kind === 'string' &&
|
|
119
|
+
typeof payload.descendantCount === 'number' &&
|
|
120
|
+
Number.isSafeInteger(payload.descendantCount) &&
|
|
121
|
+
payload.descendantCount >= 0 &&
|
|
122
|
+
isFileVersionPayload(payload.entryVersion) &&
|
|
123
|
+
typeof payload.issuedAt === 'string' &&
|
|
124
|
+
typeof payload.nonce === 'string' &&
|
|
125
|
+
payload.nonce.length > 0);
|
|
126
|
+
}
|
|
127
|
+
function isFileVersionPayload(value) {
|
|
128
|
+
if (!value || typeof value !== 'object') {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
const version = value;
|
|
132
|
+
return (typeof version.size === 'number' &&
|
|
133
|
+
typeof version.mtimeMs === 'number' &&
|
|
134
|
+
(version.ino === undefined || typeof version.ino === 'number'));
|
|
135
|
+
}
|
|
136
|
+
function sameFileVersion(left, right) {
|
|
137
|
+
return left.size === right.size && left.mtimeMs === right.mtimeMs && (right.ino === undefined || left.ino === right.ino);
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=rootToken.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rootToken.js","sourceRoot":"","sources":["../../../src/server/files/rootToken.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type FastifyInstance } from 'fastify';
|
|
2
|
+
import type { AppConfig } from './config.js';
|
|
3
|
+
import { type AuthRouteServices } from './routes/authRoutes.js';
|
|
4
|
+
import { type FileRouteServices } from './routes/fileRoutes.js';
|
|
5
|
+
import { type TerminalLayoutRouteServices } from './routes/terminalLayoutRoutes.js';
|
|
6
|
+
import { type TerminalRouteServices } from './routes/terminalRoutes.js';
|
|
7
|
+
import { type UploadRouteServices } from './routes/uploadRoutes.js';
|
|
8
|
+
import { type AgentRouteServices } from './agent/agentRoutes.js';
|
|
9
|
+
import { type AgentManagementRouteServices } from './routes/agentManagementRoutes.js';
|
|
10
|
+
import { type NotificationAssetRouteServices } from './routes/notificationAssetRoutes.js';
|
|
11
|
+
export interface BuildAppServices extends AuthRouteServices, TerminalRouteServices, TerminalLayoutRouteServices, UploadRouteServices, FileRouteServices, AgentRouteServices, AgentManagementRouteServices, NotificationAssetRouteServices {
|
|
12
|
+
}
|
|
13
|
+
export declare function buildApp(config: AppConfig, services: BuildAppServices): Promise<FastifyInstance>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fastifyCookie from '@fastify/cookie';
|
|
2
|
+
import fastifyMultipart from '@fastify/multipart';
|
|
3
|
+
import fastifyStatic from '@fastify/static';
|
|
4
|
+
import fastifyWebsocket from '@fastify/websocket';
|
|
5
|
+
import Fastify from 'fastify';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { registerAuthRoutes } from './routes/authRoutes.js';
|
|
8
|
+
import { registerFileRoutes } from './routes/fileRoutes.js';
|
|
9
|
+
import { registerTerminalLayoutRoutes } from './routes/terminalLayoutRoutes.js';
|
|
10
|
+
import { registerTerminalRoutes } from './routes/terminalRoutes.js';
|
|
11
|
+
import { registerTerminalWebSocket } from './routes/terminalWebSocket.js';
|
|
12
|
+
import { registerUploadRoutes } from './routes/uploadRoutes.js';
|
|
13
|
+
import { registerTotpRoutes } from './routes/totpRoutes.js';
|
|
14
|
+
import { registerSecurityRoutes } from './routes/securityRoutes.js';
|
|
15
|
+
import { registerUpdateRoutes } from './routes/updateRoutes.js';
|
|
16
|
+
import { registerAgentRoutes } from './agent/agentRoutes.js';
|
|
17
|
+
import { registerAgentManagementRoutes } from './routes/agentManagementRoutes.js';
|
|
18
|
+
import { registerNotificationAssetRoutes } from './routes/notificationAssetRoutes.js';
|
|
19
|
+
export async function buildApp(config, services) {
|
|
20
|
+
const app = Fastify({ logger: true, trustProxy: config.trustProxy });
|
|
21
|
+
await app.register(fastifyCookie, {
|
|
22
|
+
secret: config.sessionSecret
|
|
23
|
+
});
|
|
24
|
+
await app.register(fastifyWebsocket);
|
|
25
|
+
await app.register(fastifyMultipart, {
|
|
26
|
+
throwFileSizeLimit: false,
|
|
27
|
+
limits: {
|
|
28
|
+
files: config.uploadMaxFiles,
|
|
29
|
+
fileSize: config.uploadMaxFileBytes + 1,
|
|
30
|
+
parts: config.uploadMaxFiles + 1,
|
|
31
|
+
fields: 1
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
await registerAuthRoutes(app, config, services);
|
|
35
|
+
await registerTotpRoutes(app, config, services);
|
|
36
|
+
await registerSecurityRoutes(app, config, services);
|
|
37
|
+
await registerUpdateRoutes(app, config, services);
|
|
38
|
+
await registerTerminalRoutes(app, config, services);
|
|
39
|
+
await registerTerminalLayoutRoutes(app, config, services);
|
|
40
|
+
await registerUploadRoutes(app, config, services);
|
|
41
|
+
await registerFileRoutes(app, config, services);
|
|
42
|
+
await registerNotificationAssetRoutes(app, config, services);
|
|
43
|
+
await registerTerminalWebSocket(app, config, services);
|
|
44
|
+
if (config.agentToken) {
|
|
45
|
+
await registerAgentRoutes(app, config, services);
|
|
46
|
+
}
|
|
47
|
+
await registerAgentManagementRoutes(app, config, services);
|
|
48
|
+
app.addHook('onSend', async (_request, reply, payload) => {
|
|
49
|
+
const contentType = reply.getHeader('content-type');
|
|
50
|
+
if (typeof contentType === 'string' && contentType.includes('text/html')) {
|
|
51
|
+
reply.header('Cache-Control', 'no-store');
|
|
52
|
+
}
|
|
53
|
+
return payload;
|
|
54
|
+
});
|
|
55
|
+
await app.register(fastifyStatic, {
|
|
56
|
+
root: config.staticRoot,
|
|
57
|
+
prefix: '/'
|
|
58
|
+
});
|
|
59
|
+
app.setNotFoundHandler(async (request, reply) => {
|
|
60
|
+
const requestPath = request.url.split('?', 1)[0] ?? request.url;
|
|
61
|
+
if (requestPath === '/api' || requestPath.startsWith('/api/')) {
|
|
62
|
+
return reply.code(404).send({ error: 'not_found' });
|
|
63
|
+
}
|
|
64
|
+
reply.header('Cache-Control', 'no-store');
|
|
65
|
+
return reply.sendFile('index.html', path.resolve(config.staticRoot));
|
|
66
|
+
});
|
|
67
|
+
return app;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/server/http.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { loadConfig, loadDotEnvFile } from './config.js';
|
|
2
|
+
import { buildApp } from './http.js';
|
|
3
|
+
import { AuthService } from './auth/authService.js';
|
|
4
|
+
import { TotpService } from './auth/totpService.js';
|
|
5
|
+
import { FileStore } from './storage/fileStore.js';
|
|
6
|
+
import { NodePtyAdapter } from './terminal/NodePtyAdapter.js';
|
|
7
|
+
import { TmuxPtyAdapter } from './terminal/TmuxPtyAdapter.js';
|
|
8
|
+
import { TerminalManager } from './terminal/TerminalManager.js';
|
|
9
|
+
const config = loadConfig(loadDotEnvFile());
|
|
10
|
+
const store = new FileStore(config.statePath);
|
|
11
|
+
await store.init();
|
|
12
|
+
const totpService = new TotpService('NTerminal');
|
|
13
|
+
const authService = new AuthService(config, store, { totpService });
|
|
14
|
+
// Prefer the tmux-backed adapter when available so PTYs survive
|
|
15
|
+
// `nterminalctl restart` and NTerminal updates without losing shell state.
|
|
16
|
+
// Falls back to the raw node-pty adapter when tmux isn't installed; the
|
|
17
|
+
// rest of the system works the same, just without across-restart
|
|
18
|
+
// persistence. Set NTERMINAL_PTY_PERSIST=false to force the fallback.
|
|
19
|
+
const tmuxAdapter = process.env.NTERMINAL_PTY_PERSIST === 'false' ? null : TmuxPtyAdapter.detect();
|
|
20
|
+
const ptyAdapter = tmuxAdapter ?? new NodePtyAdapter();
|
|
21
|
+
const terminalManager = new TerminalManager(config, ptyAdapter);
|
|
22
|
+
await terminalManager.restoreSessions();
|
|
23
|
+
const app = await buildApp(config, {
|
|
24
|
+
authService,
|
|
25
|
+
fileStore: store,
|
|
26
|
+
terminalManager
|
|
27
|
+
});
|
|
28
|
+
app.log.info({ adapter: tmuxAdapter ? 'tmux' : 'node-pty' }, tmuxAdapter
|
|
29
|
+
? 'PTY persistence enabled — shells survive NTerminal restart'
|
|
30
|
+
: 'PTY persistence disabled (tmux not detected or disabled); shells die on NTerminal restart');
|
|
31
|
+
store.setErrorHandler((err) => app.log.error({ err }, 'fileStore coalesced flush failed'));
|
|
32
|
+
const shutdown = async (signal) => {
|
|
33
|
+
app.log.info({ signal }, 'shutting down');
|
|
34
|
+
terminalManager.closeAll();
|
|
35
|
+
await app.close();
|
|
36
|
+
await store.flush();
|
|
37
|
+
process.exit(0);
|
|
38
|
+
};
|
|
39
|
+
process.once('SIGINT', () => void shutdown('SIGINT'));
|
|
40
|
+
process.once('SIGTERM', () => void shutdown('SIGTERM'));
|
|
41
|
+
// Last-ditch safety net for any path that exits without going through shutdown
|
|
42
|
+
// (uncaught lifecycle, beforeExit). Sync flush so it works during process tear-down.
|
|
43
|
+
process.on('beforeExit', () => store.flushSync());
|
|
44
|
+
await app.listen({ host: config.host, port: config.port });
|
|
45
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAEhE,MAAM,MAAM,GAAG,UAAU,CAAC,cAAc,EAAE,CAAC,CAAC;AAC5C,MAAM,KAAK,GAAG,IAAI,SAAS,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;AAC9C,MAAM,KAAK,CAAC,IAAI,EAAE,CAAC;AACnB,MAAM,WAAW,GAAG,IAAI,WAAW,CAAC,WAAW,CAAC,CAAC;AACjD,MAAM,WAAW,GAAG,IAAI,WAAW,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,WAAW,EAAE,CAAC,CAAC;AACpE,gEAAgE;AAChE,2EAA2E;AAC3E,wEAAwE;AACxE,iEAAiE;AACjE,sEAAsE;AACtE,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,qBAAqB,KAAK,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC,MAAM,EAAE,CAAC;AACnG,MAAM,UAAU,GAAG,WAAW,IAAI,IAAI,cAAc,EAAE,CAAC;AACvD,MAAM,eAAe,GAAG,IAAI,eAAe,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;AAChE,MAAM,eAAe,CAAC,eAAe,EAAE,CAAC;AAExC,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,MAAM,EAAE;IACjC,WAAW;IACX,SAAS,EAAE,KAAK;IAChB,eAAe;CAChB,CAAC,CAAC;AAEH,GAAG,CAAC,GAAG,CAAC,IAAI,CACV,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,UAAU,EAAE,EAC9C,WAAW;IACT,CAAC,CAAC,4DAA4D;IAC9D,CAAC,CAAC,2FAA2F,CAChG,CAAC;AAEF,KAAK,CAAC,eAAe,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,EAAE,kCAAkC,CAAC,CAAC,CAAC;AAE3F,MAAM,QAAQ,GAAG,KAAK,EAAE,MAAc,EAAiB,EAAE;IACvD,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,EAAE,eAAe,CAAC,CAAC;IAC1C,eAAe,CAAC,QAAQ,EAAE,CAAC;IAC3B,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC;IAClB,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC;IACpB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC;AAEF,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,KAAK,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;AACtD,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,KAAK,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC;AACxD,+EAA+E;AAC/E,qFAAqF;AACrF,OAAO,CAAC,EAAE,CAAC,YAAY,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC;AAElD,MAAM,GAAG,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
import type { AppConfig } from '../config.js';
|
|
3
|
+
import type { AuthService } from '../auth/authService.js';
|
|
4
|
+
import type { FileStore } from '../storage/fileStore.js';
|
|
5
|
+
export interface AgentManagementRouteServices {
|
|
6
|
+
authService: AuthService;
|
|
7
|
+
fileStore: FileStore;
|
|
8
|
+
}
|
|
9
|
+
export declare function registerAgentManagementRoutes(app: FastifyInstance, config: AppConfig, services: AgentManagementRouteServices): Promise<void>;
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { isAllowedOrigin } from '../config.js';
|
|
3
|
+
import { requireAllowedOrigin } from './authRoutes.js';
|
|
4
|
+
import { authenticateTerminalRequest } from './terminalRoutes.js';
|
|
5
|
+
import { proxyAgentRequest } from '../agent/agentProxy.js';
|
|
6
|
+
import { proxyAgentWebSocket } from '../agent/agentWebSocketProxy.js';
|
|
7
|
+
import { collectSystemStats } from '../system/stats.js';
|
|
8
|
+
export async function registerAgentManagementRoutes(app, config, services) {
|
|
9
|
+
// List agents
|
|
10
|
+
app.get('/api/agents', async (request, reply) => {
|
|
11
|
+
const auth = await authenticateTerminalRequest(services.authService, request, reply);
|
|
12
|
+
if (!auth)
|
|
13
|
+
return reply;
|
|
14
|
+
const state = await services.fileStore.read();
|
|
15
|
+
return { agents: state.agents.map(({ id, name, url }) => ({ id, name, url })) };
|
|
16
|
+
});
|
|
17
|
+
// Read or update the main server's display name. The ServerList sidebar
|
|
18
|
+
// shows this as the "Local" entry's label and lets the operator rename it
|
|
19
|
+
// inline — without an endpoint the name was hardcoded to "Local". Stored
|
|
20
|
+
// as null when the operator hasn't picked a name yet so the client knows
|
|
21
|
+
// to fall back to its default label rather than rendering empty text.
|
|
22
|
+
app.get('/api/main', async (request, reply) => {
|
|
23
|
+
const auth = await authenticateTerminalRequest(services.authService, request, reply);
|
|
24
|
+
if (!auth)
|
|
25
|
+
return reply;
|
|
26
|
+
const state = await services.fileStore.read();
|
|
27
|
+
return { name: state.mainName };
|
|
28
|
+
});
|
|
29
|
+
app.patch('/api/main', { preHandler: (request, reply) => requireAllowedOrigin(config, request, reply) }, async (request, reply) => {
|
|
30
|
+
const auth = await authenticateTerminalRequest(services.authService, request, reply);
|
|
31
|
+
if (!auth)
|
|
32
|
+
return reply;
|
|
33
|
+
const raw = request.body?.name;
|
|
34
|
+
// Accept null/empty as "clear back to default" so the operator can
|
|
35
|
+
// undo their rename without manual .env editing.
|
|
36
|
+
const trimmed = typeof raw === 'string' ? raw.trim() : '';
|
|
37
|
+
const next = trimmed.length > 0 ? trimmed.slice(0, 80) : null;
|
|
38
|
+
await services.fileStore.update((state) => ({ ...state, mainName: next }));
|
|
39
|
+
return { name: next };
|
|
40
|
+
});
|
|
41
|
+
// Add agent
|
|
42
|
+
app.post('/api/agents', { preHandler: (request, reply) => requireAllowedOrigin(config, request, reply) }, async (request, reply) => {
|
|
43
|
+
const auth = await authenticateTerminalRequest(services.authService, request, reply);
|
|
44
|
+
if (!auth)
|
|
45
|
+
return reply;
|
|
46
|
+
const { name, url, token } = request.body ?? {};
|
|
47
|
+
if (typeof name !== 'string' || !name.trim())
|
|
48
|
+
return reply.code(400).send({ error: 'name_required' });
|
|
49
|
+
if (typeof url !== 'string' || !url.trim())
|
|
50
|
+
return reply.code(400).send({ error: 'url_required' });
|
|
51
|
+
if (typeof token !== 'string' || token.length < 32)
|
|
52
|
+
return reply.code(400).send({ error: 'token_too_short' });
|
|
53
|
+
let parsedUrl;
|
|
54
|
+
try {
|
|
55
|
+
parsedUrl = new URL(url.trim());
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return reply.code(400).send({ error: 'invalid_url' });
|
|
59
|
+
}
|
|
60
|
+
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
|
|
61
|
+
return reply.code(400).send({ error: 'invalid_url_scheme' });
|
|
62
|
+
}
|
|
63
|
+
const agent = {
|
|
64
|
+
id: randomUUID(),
|
|
65
|
+
name: name.trim(),
|
|
66
|
+
url: url.trim().replace(/\/$/, ''),
|
|
67
|
+
token,
|
|
68
|
+
createdAt: new Date().toISOString()
|
|
69
|
+
};
|
|
70
|
+
await services.fileStore.update((state) => ({ ...state, agents: [...state.agents, agent] }), { flush: 'immediate' });
|
|
71
|
+
return reply.code(201).send({ agent: { id: agent.id, name: agent.name, url: agent.url } });
|
|
72
|
+
});
|
|
73
|
+
// Rename agent
|
|
74
|
+
app.patch('/api/agents/:agentId', { preHandler: (request, reply) => requireAllowedOrigin(config, request, reply) }, async (request, reply) => {
|
|
75
|
+
const auth = await authenticateTerminalRequest(services.authService, request, reply);
|
|
76
|
+
if (!auth)
|
|
77
|
+
return reply;
|
|
78
|
+
const { name } = request.body ?? {};
|
|
79
|
+
if (typeof name !== 'string' || !name.trim())
|
|
80
|
+
return reply.code(400).send({ error: 'name_required' });
|
|
81
|
+
let found = false;
|
|
82
|
+
await services.fileStore.update((state) => {
|
|
83
|
+
const agents = state.agents.map((a) => {
|
|
84
|
+
if (a.id === request.params.agentId) {
|
|
85
|
+
found = true;
|
|
86
|
+
return { ...a, name: name.trim() };
|
|
87
|
+
}
|
|
88
|
+
return a;
|
|
89
|
+
});
|
|
90
|
+
return { ...state, agents };
|
|
91
|
+
});
|
|
92
|
+
if (!found)
|
|
93
|
+
return reply.code(404).send({ error: 'agent_not_found' });
|
|
94
|
+
return { ok: true };
|
|
95
|
+
});
|
|
96
|
+
// Remove agent
|
|
97
|
+
app.delete('/api/agents/:agentId', { preHandler: (request, reply) => requireAllowedOrigin(config, request, reply) }, async (request, reply) => {
|
|
98
|
+
const auth = await authenticateTerminalRequest(services.authService, request, reply);
|
|
99
|
+
if (!auth)
|
|
100
|
+
return reply;
|
|
101
|
+
let found = false;
|
|
102
|
+
await services.fileStore.update((state) => {
|
|
103
|
+
const agents = state.agents.filter((a) => {
|
|
104
|
+
if (a.id === request.params.agentId) {
|
|
105
|
+
found = true;
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
});
|
|
110
|
+
// Drop the agent's stored layout too, so removed agents leave no orphaned state.
|
|
111
|
+
const { [request.params.agentId]: _removed, ...agentLayouts } = state.agentLayouts;
|
|
112
|
+
return { ...state, agents, agentLayouts };
|
|
113
|
+
}, { flush: 'immediate' });
|
|
114
|
+
if (!found)
|
|
115
|
+
return reply.code(404).send({ error: 'agent_not_found' });
|
|
116
|
+
return reply.code(204).send();
|
|
117
|
+
});
|
|
118
|
+
// System stats for the main host. Sidebar polls this on a long interval
|
|
119
|
+
// (~30s) so a delta-based CPU sample is meaningful without sampling
|
|
120
|
+
// overhead — see src/server/system/stats.ts.
|
|
121
|
+
app.get('/api/stats', async (request, reply) => {
|
|
122
|
+
const auth = await authenticateTerminalRequest(services.authService, request, reply);
|
|
123
|
+
if (!auth)
|
|
124
|
+
return reply;
|
|
125
|
+
return collectSystemStats();
|
|
126
|
+
});
|
|
127
|
+
// Proxy: stats for a registered agent
|
|
128
|
+
app.get('/api/agents/:agentId/stats', async (request, reply) => {
|
|
129
|
+
const auth = await authenticateTerminalRequest(services.authService, request, reply);
|
|
130
|
+
if (!auth)
|
|
131
|
+
return reply;
|
|
132
|
+
const agent = await resolveAgent(services.fileStore, request.params.agentId);
|
|
133
|
+
if (!agent)
|
|
134
|
+
return reply.code(404).send({ error: 'agent_not_found' });
|
|
135
|
+
return proxyAgentRequest(agent, '/stats', request, reply);
|
|
136
|
+
});
|
|
137
|
+
// Ping agent
|
|
138
|
+
app.get('/api/agents/:agentId/ping', async (request, reply) => {
|
|
139
|
+
const auth = await authenticateTerminalRequest(services.authService, request, reply);
|
|
140
|
+
if (!auth)
|
|
141
|
+
return reply;
|
|
142
|
+
const state = await services.fileStore.read();
|
|
143
|
+
const agent = state.agents.find((a) => a.id === request.params.agentId);
|
|
144
|
+
if (!agent)
|
|
145
|
+
return reply.code(404).send({ error: 'agent_not_found' });
|
|
146
|
+
const start = Date.now();
|
|
147
|
+
try {
|
|
148
|
+
const res = await fetch(`${agent.url}/api/agent/terminals`, {
|
|
149
|
+
headers: { Authorization: `Bearer ${agent.token}` },
|
|
150
|
+
redirect: 'manual',
|
|
151
|
+
signal: AbortSignal.timeout(5000)
|
|
152
|
+
});
|
|
153
|
+
const latencyMs = Date.now() - start;
|
|
154
|
+
return { ok: res.ok, latencyMs };
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return { ok: false, latencyMs: Date.now() - start };
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
// Proxy: version / update
|
|
161
|
+
app.get('/api/agents/:agentId/version', async (request, reply) => {
|
|
162
|
+
const auth = await authenticateTerminalRequest(services.authService, request, reply);
|
|
163
|
+
if (!auth)
|
|
164
|
+
return reply;
|
|
165
|
+
const agent = await resolveAgent(services.fileStore, request.params.agentId);
|
|
166
|
+
if (!agent)
|
|
167
|
+
return reply.code(404).send({ error: 'agent_not_found' });
|
|
168
|
+
return proxyAgentRequest(agent, '/version', request, reply);
|
|
169
|
+
});
|
|
170
|
+
app.post('/api/agents/:agentId/update/check', { preHandler: (request, reply) => requireAllowedOrigin(config, request, reply) }, async (request, reply) => {
|
|
171
|
+
const auth = await authenticateTerminalRequest(services.authService, request, reply);
|
|
172
|
+
if (!auth)
|
|
173
|
+
return reply;
|
|
174
|
+
const agent = await resolveAgent(services.fileStore, request.params.agentId);
|
|
175
|
+
if (!agent)
|
|
176
|
+
return reply.code(404).send({ error: 'agent_not_found' });
|
|
177
|
+
return proxyAgentRequest(agent, '/update/check', request, reply);
|
|
178
|
+
});
|
|
179
|
+
app.post('/api/agents/:agentId/update', { preHandler: (request, reply) => requireAllowedOrigin(config, request, reply) }, async (request, reply) => {
|
|
180
|
+
const auth = await authenticateTerminalRequest(services.authService, request, reply);
|
|
181
|
+
if (!auth)
|
|
182
|
+
return reply;
|
|
183
|
+
const agent = await resolveAgent(services.fileStore, request.params.agentId);
|
|
184
|
+
if (!agent)
|
|
185
|
+
return reply.code(404).send({ error: 'agent_not_found' });
|
|
186
|
+
return proxyAgentRequest(agent, '/update', request, reply);
|
|
187
|
+
});
|
|
188
|
+
// Proxy: terminal list
|
|
189
|
+
app.get('/api/agents/:agentId/terminals', async (request, reply) => {
|
|
190
|
+
const auth = await authenticateTerminalRequest(services.authService, request, reply);
|
|
191
|
+
if (!auth)
|
|
192
|
+
return reply;
|
|
193
|
+
const agent = await resolveAgent(services.fileStore, request.params.agentId);
|
|
194
|
+
if (!agent)
|
|
195
|
+
return reply.code(404).send({ error: 'agent_not_found' });
|
|
196
|
+
return proxyAgentRequest(agent, '/terminals', request, reply);
|
|
197
|
+
});
|
|
198
|
+
app.get('/api/agents/:agentId/terminals/:terminalId/history', async (request, reply) => {
|
|
199
|
+
const auth = await authenticateTerminalRequest(services.authService, request, reply);
|
|
200
|
+
if (!auth)
|
|
201
|
+
return reply;
|
|
202
|
+
const agent = await resolveAgent(services.fileStore, request.params.agentId);
|
|
203
|
+
if (!agent)
|
|
204
|
+
return reply.code(404).send({ error: 'agent_not_found' });
|
|
205
|
+
const query = request.url.includes('?') ? request.url.slice(request.url.indexOf('?')) : '';
|
|
206
|
+
return proxyAgentRequest(agent, `/terminals/${request.params.terminalId}/history${query}`, request, reply);
|
|
207
|
+
});
|
|
208
|
+
app.get('/api/agents/:agentId/terminals/:terminalId/transcript-history', async (request, reply) => {
|
|
209
|
+
const auth = await authenticateTerminalRequest(services.authService, request, reply);
|
|
210
|
+
if (!auth)
|
|
211
|
+
return reply;
|
|
212
|
+
const agent = await resolveAgent(services.fileStore, request.params.agentId);
|
|
213
|
+
if (!agent)
|
|
214
|
+
return reply.code(404).send({ error: 'agent_not_found' });
|
|
215
|
+
const query = request.url.includes('?') ? request.url.slice(request.url.indexOf('?')) : '';
|
|
216
|
+
return proxyAgentRequest(agent, `/terminals/${request.params.terminalId}/transcript-history${query}`, request, reply, {
|
|
217
|
+
unavailableFallback: {
|
|
218
|
+
terminalId: request.params.terminalId,
|
|
219
|
+
source: null,
|
|
220
|
+
cursor: null,
|
|
221
|
+
hasMore: false,
|
|
222
|
+
output: []
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
app.get('/api/agents/:agentId/terminals/:terminalId/transcript-source', async (request, reply) => {
|
|
227
|
+
const auth = await authenticateTerminalRequest(services.authService, request, reply);
|
|
228
|
+
if (!auth)
|
|
229
|
+
return reply;
|
|
230
|
+
const agent = await resolveAgent(services.fileStore, request.params.agentId);
|
|
231
|
+
if (!agent)
|
|
232
|
+
return reply.code(404).send({ error: 'agent_not_found' });
|
|
233
|
+
return proxyAgentRequest(agent, `/terminals/${request.params.terminalId}/transcript-source`, request, reply, {
|
|
234
|
+
unavailableFallback: { terminalId: request.params.terminalId, source: null }
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
// Proxy: create terminal
|
|
238
|
+
app.post('/api/agents/:agentId/terminals', { preHandler: (request, reply) => requireAllowedOrigin(config, request, reply) }, async (request, reply) => {
|
|
239
|
+
const auth = await authenticateTerminalRequest(services.authService, request, reply);
|
|
240
|
+
if (!auth)
|
|
241
|
+
return reply;
|
|
242
|
+
const agent = await resolveAgent(services.fileStore, request.params.agentId);
|
|
243
|
+
if (!agent)
|
|
244
|
+
return reply.code(404).send({ error: 'agent_not_found' });
|
|
245
|
+
return proxyAgentRequest(agent, '/terminals', request, reply);
|
|
246
|
+
});
|
|
247
|
+
// Proxy: close terminal
|
|
248
|
+
app.delete('/api/agents/:agentId/terminals/:terminalId', { preHandler: (request, reply) => requireAllowedOrigin(config, request, reply) }, async (request, reply) => {
|
|
249
|
+
const auth = await authenticateTerminalRequest(services.authService, request, reply);
|
|
250
|
+
if (!auth)
|
|
251
|
+
return reply;
|
|
252
|
+
const agent = await resolveAgent(services.fileStore, request.params.agentId);
|
|
253
|
+
if (!agent)
|
|
254
|
+
return reply.code(404).send({ error: 'agent_not_found' });
|
|
255
|
+
return proxyAgentRequest(agent, `/terminals/${request.params.terminalId}`, request, reply);
|
|
256
|
+
});
|
|
257
|
+
// Proxy: rename terminal
|
|
258
|
+
app.patch('/api/agents/:agentId/terminals/:terminalId', { preHandler: (request, reply) => requireAllowedOrigin(config, request, reply) }, async (request, reply) => {
|
|
259
|
+
const auth = await authenticateTerminalRequest(services.authService, request, reply);
|
|
260
|
+
if (!auth)
|
|
261
|
+
return reply;
|
|
262
|
+
const agent = await resolveAgent(services.fileStore, request.params.agentId);
|
|
263
|
+
if (!agent)
|
|
264
|
+
return reply.code(404).send({ error: 'agent_not_found' });
|
|
265
|
+
return proxyAgentRequest(agent, `/terminals/${request.params.terminalId}`, request, reply);
|
|
266
|
+
});
|
|
267
|
+
// Proxy: terminal WebSocket
|
|
268
|
+
app.get('/api/agents/:agentId/terminals/:terminalId/ws', { websocket: true }, async (socket, request) => {
|
|
269
|
+
if (!request.headers.origin || !isAllowedOrigin(config, request.headers.origin)) {
|
|
270
|
+
socket.close(1008, 'origin not allowed');
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const auth = await authenticateTerminalRequest(services.authService, request);
|
|
274
|
+
if (!auth) {
|
|
275
|
+
socket.close(1008, 'unauthorized');
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const state = await services.fileStore.read();
|
|
279
|
+
const agent = state.agents.find((a) => a.id === request.params.agentId);
|
|
280
|
+
if (!agent) {
|
|
281
|
+
socket.close(1008, 'agent not found');
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
proxyAgentWebSocket(socket, agent, request.params.terminalId);
|
|
285
|
+
});
|
|
286
|
+
// Proxy: file routes (all file operations)
|
|
287
|
+
const filePathParts = ['root', 'list', 'read', 'write', 'create', 'move', 'delete-preview', 'delete', 'open', 'preview'];
|
|
288
|
+
for (const part of filePathParts) {
|
|
289
|
+
app.post(`/api/agents/:agentId/files/${part}`, { preHandler: (request, reply) => requireAllowedOrigin(config, request, reply) }, async (request, reply) => {
|
|
290
|
+
const auth = await authenticateTerminalRequest(services.authService, request, reply);
|
|
291
|
+
if (!auth)
|
|
292
|
+
return reply;
|
|
293
|
+
const agent = await resolveAgent(services.fileStore, request.params.agentId);
|
|
294
|
+
if (!agent)
|
|
295
|
+
return reply.code(404).send({ error: 'agent_not_found' });
|
|
296
|
+
return proxyAgentRequest(agent, `/files/${part}`, request, reply);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
async function resolveAgent(fileStore, agentId) {
|
|
301
|
+
const state = await fileStore.read();
|
|
302
|
+
return state.agents.find((a) => a.id === agentId) ?? null;
|
|
303
|
+
}
|
|
304
|
+
//# sourceMappingURL=agentManagementRoutes.js.map
|