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.
Files changed (197) hide show
  1. package/.env.example +12 -0
  2. package/LICENSE +674 -0
  3. package/README.md +181 -0
  4. package/assets/brand/app-icon-1024.png +0 -0
  5. package/assets/brand/app-icon-384.png +0 -0
  6. package/assets/brand/apple-touch-icon-360.png +0 -0
  7. package/assets/brand/favicon-32.png +0 -0
  8. package/assets/brand/favicon-64.png +0 -0
  9. package/assets/brand/favicon-96.png +0 -0
  10. package/assets/brand/favicon.svg +4 -0
  11. package/assets/brand/nterminal-mark-64.png +0 -0
  12. package/assets/brand/nterminal-mark.svg +4 -0
  13. package/assets/brand/nterminal-wordmark-486x68.png +0 -0
  14. package/assets/brand/nterminal-wordmark.svg +3 -0
  15. package/assets/screenshot/scr.png +0 -0
  16. package/bin/nterminal.js +114 -0
  17. package/dist/client/apple-touch-icon.png +0 -0
  18. package/dist/client/assets/MarkdownPreview-BeDi-V7k.js +29 -0
  19. package/dist/client/assets/MesloLGS-NF-Bold-Italic-DwFsXcwX.ttf +0 -0
  20. package/dist/client/assets/MesloLGS-NF-Bold-kN-HYz-g.ttf +0 -0
  21. package/dist/client/assets/MesloLGS-NF-Italic-CMg1T6-G.ttf +0 -0
  22. package/dist/client/assets/MesloLGS-NF-Regular-Cxr8pvCI.ttf +0 -0
  23. package/dist/client/assets/index-BQkKYjXb.js +33 -0
  24. package/dist/client/assets/index-WqeS39wU.css +1 -0
  25. package/dist/client/assets/notifications/character-2258.mp4 +0 -0
  26. package/dist/client/assets/notifications/character-2260.mp4 +0 -0
  27. package/dist/client/assets/notifications/character-2272.mp4 +0 -0
  28. package/dist/client/brand/nterminal-mark-64.png +0 -0
  29. package/dist/client/brand/nterminal-mark.svg +4 -0
  30. package/dist/client/brand/nterminal-wordmark-486x68.png +0 -0
  31. package/dist/client/brand/nterminal-wordmark.svg +3 -0
  32. package/dist/client/icons/app-icon-1024.png +0 -0
  33. package/dist/client/icons/app-icon-384.png +0 -0
  34. package/dist/client/icons/favicon-32.png +0 -0
  35. package/dist/client/icons/favicon-64.png +0 -0
  36. package/dist/client/icons/favicon-96.png +0 -0
  37. package/dist/client/icons/favicon.svg +4 -0
  38. package/dist/client/index.html +21 -0
  39. package/dist/client/manifest.webmanifest +24 -0
  40. package/dist/scripts/generate-secrets.js +3 -0
  41. package/dist/scripts/generate-secrets.js.map +1 -0
  42. package/dist/scripts/onboarding.js +814 -0
  43. package/dist/scripts/onboarding.js.map +1 -0
  44. package/dist/scripts/proxySetup.js +1007 -0
  45. package/dist/scripts/proxySetup.js.map +1 -0
  46. package/dist/server/agent/agentAuth.d.ts +6 -0
  47. package/dist/server/agent/agentAuth.js +35 -0
  48. package/dist/server/agent/agentAuth.js.map +1 -0
  49. package/dist/server/agent/agentProxy.d.ts +5 -0
  50. package/dist/server/agent/agentProxy.js +63 -0
  51. package/dist/server/agent/agentProxy.js.map +1 -0
  52. package/dist/server/agent/agentRoutes.d.ts +9 -0
  53. package/dist/server/agent/agentRoutes.js +327 -0
  54. package/dist/server/agent/agentRoutes.js.map +1 -0
  55. package/dist/server/agent/agentWebSocketProxy.d.ts +3 -0
  56. package/dist/server/agent/agentWebSocketProxy.js +65 -0
  57. package/dist/server/agent/agentWebSocketProxy.js.map +1 -0
  58. package/dist/server/auth/authService.d.ts +100 -0
  59. package/dist/server/auth/authService.js +415 -0
  60. package/dist/server/auth/authService.js.map +1 -0
  61. package/dist/server/auth/cookies.d.ts +11 -0
  62. package/dist/server/auth/cookies.js +39 -0
  63. package/dist/server/auth/cookies.js.map +1 -0
  64. package/dist/server/auth/ipMatch.d.ts +14 -0
  65. package/dist/server/auth/ipMatch.js +103 -0
  66. package/dist/server/auth/ipMatch.js.map +1 -0
  67. package/dist/server/auth/rateLimit.d.ts +17 -0
  68. package/dist/server/auth/rateLimit.js +25 -0
  69. package/dist/server/auth/rateLimit.js.map +1 -0
  70. package/dist/server/auth/totpService.d.ts +10 -0
  71. package/dist/server/auth/totpService.js +37 -0
  72. package/dist/server/auth/totpService.js.map +1 -0
  73. package/dist/server/config.d.ts +27 -0
  74. package/dist/server/config.js +138 -0
  75. package/dist/server/config.js.map +1 -0
  76. package/dist/server/files/fileExplorerService.d.ts +38 -0
  77. package/dist/server/files/fileExplorerService.js +551 -0
  78. package/dist/server/files/fileExplorerService.js.map +1 -0
  79. package/dist/server/files/rootToken.d.ts +51 -0
  80. package/dist/server/files/rootToken.js +139 -0
  81. package/dist/server/files/rootToken.js.map +1 -0
  82. package/dist/server/http.d.ts +13 -0
  83. package/dist/server/http.js +69 -0
  84. package/dist/server/http.js.map +1 -0
  85. package/dist/server/index.d.ts +1 -0
  86. package/dist/server/index.js +45 -0
  87. package/dist/server/index.js.map +1 -0
  88. package/dist/server/routes/agentManagementRoutes.d.ts +9 -0
  89. package/dist/server/routes/agentManagementRoutes.js +304 -0
  90. package/dist/server/routes/agentManagementRoutes.js.map +1 -0
  91. package/dist/server/routes/authRoutes.d.ts +10 -0
  92. package/dist/server/routes/authRoutes.js +95 -0
  93. package/dist/server/routes/authRoutes.js.map +1 -0
  94. package/dist/server/routes/fileRoutes.d.ts +11 -0
  95. package/dist/server/routes/fileRoutes.js +185 -0
  96. package/dist/server/routes/fileRoutes.js.map +1 -0
  97. package/dist/server/routes/notificationAssetRoutes.d.ts +9 -0
  98. package/dist/server/routes/notificationAssetRoutes.js +280 -0
  99. package/dist/server/routes/notificationAssetRoutes.js.map +1 -0
  100. package/dist/server/routes/securityRoutes.d.ts +7 -0
  101. package/dist/server/routes/securityRoutes.js +53 -0
  102. package/dist/server/routes/securityRoutes.js.map +1 -0
  103. package/dist/server/routes/socketBackpressure.d.ts +26 -0
  104. package/dist/server/routes/socketBackpressure.js +63 -0
  105. package/dist/server/routes/socketBackpressure.js.map +1 -0
  106. package/dist/server/routes/terminalLayoutRoutes.d.ts +9 -0
  107. package/dist/server/routes/terminalLayoutRoutes.js +108 -0
  108. package/dist/server/routes/terminalLayoutRoutes.js.map +1 -0
  109. package/dist/server/routes/terminalRoutes.d.ts +14 -0
  110. package/dist/server/routes/terminalRoutes.js +177 -0
  111. package/dist/server/routes/terminalRoutes.js.map +1 -0
  112. package/dist/server/routes/terminalWebSocket.d.ts +9 -0
  113. package/dist/server/routes/terminalWebSocket.js +129 -0
  114. package/dist/server/routes/terminalWebSocket.js.map +1 -0
  115. package/dist/server/routes/totpRoutes.d.ts +7 -0
  116. package/dist/server/routes/totpRoutes.js +46 -0
  117. package/dist/server/routes/totpRoutes.js.map +1 -0
  118. package/dist/server/routes/updateRoutes.d.ts +7 -0
  119. package/dist/server/routes/updateRoutes.js +24 -0
  120. package/dist/server/routes/updateRoutes.js.map +1 -0
  121. package/dist/server/routes/uploadRoutes.d.ts +9 -0
  122. package/dist/server/routes/uploadRoutes.js +95 -0
  123. package/dist/server/routes/uploadRoutes.js.map +1 -0
  124. package/dist/server/storage/fileStore.d.ts +90 -0
  125. package/dist/server/storage/fileStore.js +275 -0
  126. package/dist/server/storage/fileStore.js.map +1 -0
  127. package/dist/server/system/stats.d.ts +2 -0
  128. package/dist/server/system/stats.js +37 -0
  129. package/dist/server/system/stats.js.map +1 -0
  130. package/dist/server/terminal/NodePtyAdapter.d.ts +4 -0
  131. package/dist/server/terminal/NodePtyAdapter.js +14 -0
  132. package/dist/server/terminal/NodePtyAdapter.js.map +1 -0
  133. package/dist/server/terminal/PtyAdapter.d.ts +57 -0
  134. package/dist/server/terminal/PtyAdapter.js +2 -0
  135. package/dist/server/terminal/PtyAdapter.js.map +1 -0
  136. package/dist/server/terminal/TerminalManager.d.ts +74 -0
  137. package/dist/server/terminal/TerminalManager.js +561 -0
  138. package/dist/server/terminal/TerminalManager.js.map +1 -0
  139. package/dist/server/terminal/TmuxPtyAdapter.d.ts +25 -0
  140. package/dist/server/terminal/TmuxPtyAdapter.js +543 -0
  141. package/dist/server/terminal/TmuxPtyAdapter.js.map +1 -0
  142. package/dist/server/terminal/codexTranscriptSource.d.ts +9 -0
  143. package/dist/server/terminal/codexTranscriptSource.js +144 -0
  144. package/dist/server/terminal/codexTranscriptSource.js.map +1 -0
  145. package/dist/server/terminal/cwdResolver.d.ts +8 -0
  146. package/dist/server/terminal/cwdResolver.js +37 -0
  147. package/dist/server/terminal/cwdResolver.js.map +1 -0
  148. package/dist/server/terminal/outputBuffer.d.ts +7 -0
  149. package/dist/server/terminal/outputBuffer.js +17 -0
  150. package/dist/server/terminal/outputBuffer.js.map +1 -0
  151. package/dist/server/terminal/transcriptHistory.d.ts +7 -0
  152. package/dist/server/terminal/transcriptHistory.js +315 -0
  153. package/dist/server/terminal/transcriptHistory.js.map +1 -0
  154. package/dist/server/update/gitUpdate.d.ts +27 -0
  155. package/dist/server/update/gitUpdate.js +241 -0
  156. package/dist/server/update/gitUpdate.js.map +1 -0
  157. package/dist/server/uploads/uploadPaths.d.ts +18 -0
  158. package/dist/server/uploads/uploadPaths.js +116 -0
  159. package/dist/server/uploads/uploadPaths.js.map +1 -0
  160. package/dist/server/uploads/uploadService.d.ts +21 -0
  161. package/dist/server/uploads/uploadService.js +230 -0
  162. package/dist/server/uploads/uploadService.js.map +1 -0
  163. package/dist/shared/layoutState.d.ts +6 -0
  164. package/dist/shared/layoutState.js +115 -0
  165. package/dist/shared/layoutState.js.map +1 -0
  166. package/dist/shared/notificationAssets.d.ts +9 -0
  167. package/dist/shared/notificationAssets.js +27 -0
  168. package/dist/shared/notificationAssets.js.map +1 -0
  169. package/dist/shared/protocol.d.ts +308 -0
  170. package/dist/shared/protocol.js +29 -0
  171. package/dist/shared/protocol.js.map +1 -0
  172. package/dist/shared/types.d.ts +56 -0
  173. package/dist/shared/types.js +2 -0
  174. package/dist/shared/types.js.map +1 -0
  175. package/docs/assets/nterminal-workspace.png +0 -0
  176. package/docs/configuration.md +97 -0
  177. package/docs/features.md +126 -0
  178. package/docs/onboarding.md +122 -0
  179. package/docs/operations.md +112 -0
  180. package/docs/terminal-history.md +54 -0
  181. package/package.json +85 -0
  182. package/public/apple-touch-icon.png +0 -0
  183. package/public/assets/notifications/character-2258.mp4 +0 -0
  184. package/public/assets/notifications/character-2260.mp4 +0 -0
  185. package/public/assets/notifications/character-2272.mp4 +0 -0
  186. package/public/brand/nterminal-mark-64.png +0 -0
  187. package/public/brand/nterminal-mark.svg +4 -0
  188. package/public/brand/nterminal-wordmark-486x68.png +0 -0
  189. package/public/brand/nterminal-wordmark.svg +3 -0
  190. package/public/icons/app-icon-1024.png +0 -0
  191. package/public/icons/app-icon-384.png +0 -0
  192. package/public/icons/favicon-32.png +0 -0
  193. package/public/icons/favicon-64.png +0 -0
  194. package/public/icons/favicon-96.png +0 -0
  195. package/public/icons/favicon.svg +4 -0
  196. package/public/manifest.webmanifest +24 -0
  197. 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":"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"}
@@ -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