@someonegg/agent-hub 0.0.0-stage → 0.7.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 (185) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +104 -2
  3. package/README.zh-CN.md +99 -0
  4. package/README.zh-TW.md +99 -0
  5. package/dist/client/apple-touch-icon.png +0 -0
  6. package/dist/client/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  7. package/dist/client/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  8. package/dist/client/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  9. package/dist/client/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  10. package/dist/client/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  11. package/dist/client/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  12. package/dist/client/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  13. package/dist/client/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  14. package/dist/client/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  15. package/dist/client/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  16. package/dist/client/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  17. package/dist/client/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  18. package/dist/client/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  19. package/dist/client/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  20. package/dist/client/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  21. package/dist/client/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  22. package/dist/client/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  23. package/dist/client/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  24. package/dist/client/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  25. package/dist/client/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  26. package/dist/client/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  27. package/dist/client/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  28. package/dist/client/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  29. package/dist/client/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  30. package/dist/client/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  31. package/dist/client/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  32. package/dist/client/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  33. package/dist/client/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  34. package/dist/client/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  35. package/dist/client/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  36. package/dist/client/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  37. package/dist/client/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  38. package/dist/client/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  39. package/dist/client/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  40. package/dist/client/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  41. package/dist/client/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  42. package/dist/client/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  43. package/dist/client/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  44. package/dist/client/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  45. package/dist/client/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  46. package/dist/client/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  47. package/dist/client/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  48. package/dist/client/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  49. package/dist/client/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  50. package/dist/client/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  51. package/dist/client/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  52. package/dist/client/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  53. package/dist/client/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  54. package/dist/client/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  55. package/dist/client/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  56. package/dist/client/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  57. package/dist/client/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  58. package/dist/client/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  59. package/dist/client/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  60. package/dist/client/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  61. package/dist/client/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  62. package/dist/client/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  63. package/dist/client/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  64. package/dist/client/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  65. package/dist/client/assets/index-4ejyszaV.css +1 -0
  66. package/dist/client/assets/index-CKKT4cMK.js +309 -0
  67. package/dist/client/favicon.ico +0 -0
  68. package/dist/client/favicon.svg +6 -0
  69. package/dist/client/icons/icon-192.png +0 -0
  70. package/dist/client/icons/icon-512.png +0 -0
  71. package/dist/client/icons/icon-maskable-512.png +0 -0
  72. package/dist/client/index.html +22 -0
  73. package/dist/client/manifest.webmanifest +21 -0
  74. package/dist/server/server/adapters/claude/claude-session-source.js +220 -0
  75. package/dist/server/server/adapters/claude/identity.js +28 -0
  76. package/dist/server/server/adapters/claude/interaction.js +68 -0
  77. package/dist/server/server/adapters/claude/prompt-readiness.js +12 -0
  78. package/dist/server/server/adapters/claude/ready-script.js +25 -0
  79. package/dist/server/server/adapters/claude/session-manager.js +106 -0
  80. package/dist/server/server/adapters/claude/session-normalizer.js +173 -0
  81. package/dist/server/server/adapters/claude/user-input-normalizer.js +71 -0
  82. package/dist/server/server/adapters/codex/catalog-source.js +2 -0
  83. package/dist/server/server/adapters/codex/codex-session-source.js +250 -0
  84. package/dist/server/server/adapters/codex/identity-resolver.js +104 -0
  85. package/dist/server/server/adapters/codex/interaction-parser.js +70 -0
  86. package/dist/server/server/adapters/codex/io-errors.js +2 -0
  87. package/dist/server/server/adapters/codex/jsonl-catalog-source.js +84 -0
  88. package/dist/server/server/adapters/codex/limits.js +2 -0
  89. package/dist/server/server/adapters/codex/message-normalizer.js +66 -0
  90. package/dist/server/server/adapters/codex/response-item-parser.js +64 -0
  91. package/dist/server/server/adapters/codex/rollout-descriptor.js +2 -0
  92. package/dist/server/server/adapters/codex/session-index-reader.js +101 -0
  93. package/dist/server/server/adapters/codex/session-manager.js +51 -0
  94. package/dist/server/server/adapters/codex/session-normalizer.js +175 -0
  95. package/dist/server/server/adapters/codex/subagent-identity.js +9 -0
  96. package/dist/server/server/adapters/codex/user-input-normalizer.js +121 -0
  97. package/dist/server/server/adapters/codex/value-readers.js +4 -0
  98. package/dist/server/server/adapters/shared/binding-protocol.js +54 -0
  99. package/dist/server/server/adapters/shared/io-errors.js +13 -0
  100. package/dist/server/server/adapters/shared/jsonl-decoder.js +263 -0
  101. package/dist/server/server/adapters/shared/limits.js +4 -0
  102. package/dist/server/server/adapters/shared/session-management.js +43 -0
  103. package/dist/server/server/adapters/shared/source-cache.js +23 -0
  104. package/dist/server/server/api/session-api-mapper.js +65 -0
  105. package/dist/server/server/application/interaction-session-reader.js +17 -0
  106. package/dist/server/server/application/session-read-service.js +86 -0
  107. package/dist/server/server/application/session-reader.js +2 -0
  108. package/dist/server/server/application/session-response.js +11 -0
  109. package/dist/server/server/catalog/config.js +4 -0
  110. package/dist/server/server/catalog/session-catalog-service.js +112 -0
  111. package/dist/server/server/cli/command-error.js +27 -0
  112. package/dist/server/server/cli/main.js +42 -0
  113. package/dist/server/server/cli/output.js +52 -0
  114. package/dist/server/server/cli/resource.js +104 -0
  115. package/dist/server/server/cli/session-client.js +64 -0
  116. package/dist/server/server/cli/session-follow.js +58 -0
  117. package/dist/server/server/cli/session-output.js +184 -0
  118. package/dist/server/server/cli/session-wait.js +44 -0
  119. package/dist/server/server/cli/session.js +318 -0
  120. package/dist/server/server/cli/skill.js +37 -0
  121. package/dist/server/server/config.js +196 -0
  122. package/dist/server/server/create-hub.js +91 -0
  123. package/dist/server/server/directory-lock.js +65 -0
  124. package/dist/server/server/domain/project-directory.js +5 -0
  125. package/dist/server/server/domain/session-domain.js +2 -0
  126. package/dist/server/server/domain/session-text.js +45 -0
  127. package/dist/server/server/hosting/diff-store.js +103 -0
  128. package/dist/server/server/hosting/routes.js +168 -0
  129. package/dist/server/server/hosting/store.js +162 -0
  130. package/dist/server/server/hosting/validation-error.js +4 -0
  131. package/dist/server/server/http/api-route-handler.js +2 -0
  132. package/dist/server/server/http/api-router.js +54 -0
  133. package/dist/server/server/http/create-server.js +218 -0
  134. package/dist/server/server/http/query-params.js +19 -0
  135. package/dist/server/server/http/request-body.js +48 -0
  136. package/dist/server/server/http/request-errors.js +59 -0
  137. package/dist/server/server/http/router.js +16 -0
  138. package/dist/server/server/http/security.js +19 -0
  139. package/dist/server/server/http/session-interaction-routes.js +128 -0
  140. package/dist/server/server/http/session-management-routes.js +105 -0
  141. package/dist/server/server/http/session-path.js +10 -0
  142. package/dist/server/server/http/session-read-routes.js +143 -0
  143. package/dist/server/server/http/terminal-preview-websocket.js +179 -0
  144. package/dist/server/server/http/upload-routes.js +124 -0
  145. package/dist/server/server/interaction/interaction-service.js +276 -0
  146. package/dist/server/server/interaction/message.js +19 -0
  147. package/dist/server/server/interaction/pane-binding-manager.js +62 -0
  148. package/dist/server/server/interaction/pane-observation.js +31 -0
  149. package/dist/server/server/interaction/reconcile-session-state.js +21 -0
  150. package/dist/server/server/interaction/runtime-capabilities.js +2 -0
  151. package/dist/server/server/interaction/session-runtime.js +86 -0
  152. package/dist/server/server/interaction/terminal-activity-service.js +81 -0
  153. package/dist/server/server/interaction/terminal-preview-diff.js +71 -0
  154. package/dist/server/server/interaction/terminal-preview-service.js +184 -0
  155. package/dist/server/server/interaction/tmux-launcher.js +84 -0
  156. package/dist/server/server/interaction/tmux-preview-listener.js +107 -0
  157. package/dist/server/server/interaction/tmux-service.js +334 -0
  158. package/dist/server/server/interaction/tmux-target.js +13 -0
  159. package/dist/server/server/interaction/transport.js +9 -0
  160. package/dist/server/server/main.js +54 -0
  161. package/dist/server/server/management/operation-service.js +307 -0
  162. package/dist/server/server/management/operation-state.js +3 -0
  163. package/dist/server/server/management/session-exit-service.js +50 -0
  164. package/dist/server/server/management/session-launch-executor.js +164 -0
  165. package/dist/server/server/management/session-lifecycle-executor.js +158 -0
  166. package/dist/server/server/management/session-manager.js +64 -0
  167. package/dist/server/server/repository/catalog-snapshot-store.js +318 -0
  168. package/dist/server/server/repository/diagnostic-logger.js +70 -0
  169. package/dist/server/server/repository/refresh-coordinator.js +14 -0
  170. package/dist/server/server/repository/session-queries.js +72 -0
  171. package/dist/server/server/security/opaque-id.js +17 -0
  172. package/dist/server/server/source/session-source.js +2 -0
  173. package/dist/server/server/updates/session-updates-service.js +151 -0
  174. package/dist/server/server/updates/update-revision.js +22 -0
  175. package/dist/server/server/uploads/store.js +304 -0
  176. package/dist/server/server/version.js +21 -0
  177. package/dist/server/shared/api-contract.js +2 -0
  178. package/dist/server/shared/diff.js +158 -0
  179. package/dist/server/shared/domain.js +2 -0
  180. package/dist/server/shared/interaction.js +22 -0
  181. package/dist/server/shared/task-name.js +6 -0
  182. package/dist/server/shared/uploads.js +8 -0
  183. package/package.json +98 -4
  184. package/skills/ahub-control/SKILL.md +93 -0
  185. package/skills/ahub-publish/SKILL.md +54 -0
@@ -0,0 +1,10 @@
1
+ export const SESSION_ROOT = "/api/v1/sessions";
2
+ export { isOpaqueId, SESSION_ID_PATTERN } from "../security/opaque-id.js";
3
+ export function sessionPath(pathname) {
4
+ const [id = "", ...segments] = pathname.slice(SESSION_ROOT.length + 1).split("/");
5
+ return { id, segments };
6
+ }
7
+ export function readPathname(request, url) {
8
+ return request.url?.startsWith("/") ? request.url.split("?")[0] : url.pathname;
9
+ }
10
+ //# sourceMappingURL=session-path.js.map
@@ -0,0 +1,143 @@
1
+ import { CatalogCapacityError, } from "../catalog/session-catalog-service.js";
2
+ import { RepositoryQueryError } from "../repository/session-queries.js";
3
+ import { SessionUpdatesError, } from "../updates/session-updates-service.js";
4
+ import { invalid, only, optional } from "./query-params.js";
5
+ import { sendJson } from "./router.js";
6
+ import { isOpaqueId, readPathname, SESSION_ROOT, sessionPath } from "./session-path.js";
7
+ /** Shape produced by the update-revision HMAC factory (base64url SHA-256). */
8
+ const REVISION_PATTERN = /^[A-Za-z0-9_-]{43}$/;
9
+ export function createSessionReadRoutes(dependencies) {
10
+ return {
11
+ matches: () => true,
12
+ async handle(request, response, url) {
13
+ const pathname = readPathname(request, url);
14
+ const headOnly = request.method === "HEAD";
15
+ const readMethod = request.method === "GET" || headOnly;
16
+ if (!readMethod)
17
+ return false;
18
+ if (pathname === SESSION_ROOT) {
19
+ const query = parseListQuery(url.searchParams);
20
+ return sendWaitResponse(request, response, headOnly, (signal) => dependencies.catalog.wait(headOnly ? undefined : query.after, signal));
21
+ }
22
+ if (!pathname.startsWith(`${SESSION_ROOT}/`)) {
23
+ return notFound(response, headOnly);
24
+ }
25
+ const { id, segments } = sessionPath(pathname);
26
+ if (!isOpaqueId(id))
27
+ return notFound(response, headOnly);
28
+ if (segments.length === 1 && segments[0] === "messages") {
29
+ only(url.searchParams, ["limit", "after", "wait", "stateRevision"]);
30
+ const limit = optional(url.searchParams, "limit");
31
+ const after = optional(url.searchParams, "after");
32
+ const wait = optional(url.searchParams, "wait");
33
+ const stateRevision = optional(url.searchParams, "stateRevision");
34
+ if (wait !== undefined && wait !== "true" && wait !== "false")
35
+ invalid("wait must be true or false");
36
+ if (stateRevision !== undefined && !REVISION_PATTERN.test(stateRevision))
37
+ invalid("stateRevision is malformed");
38
+ const query = {
39
+ ...(limit === undefined ? {} : { limit: integer(limit, "limit") }),
40
+ ...(after === undefined ? {} : { after: integer(after, "after") }),
41
+ wait: !headOnly && wait === "true",
42
+ ...(stateRevision === undefined
43
+ ? {}
44
+ : { stateRevision: stateRevision }),
45
+ };
46
+ if (!query.wait) {
47
+ // Plain reads go straight to the reader; waiter capacity bounds only long polls.
48
+ const result = await dependencies.sessions.messages(id, query);
49
+ if (result === null)
50
+ return notFound(response, headOnly, "session_not_found");
51
+ response.setHeader("Cache-Control", "no-store");
52
+ sendJson(response, 200, result, headOnly);
53
+ return true;
54
+ }
55
+ return sendWaitResponse(request, response, headOnly, (signal) => dependencies.updates.wait(id, query, signal));
56
+ }
57
+ if (segments.length === 0) {
58
+ if ([...url.searchParams].length > 0) {
59
+ invalid("session metadata does not accept query parameters");
60
+ }
61
+ const result = await dependencies.sessions.getSession(id);
62
+ if (result === null) {
63
+ return notFound(response, headOnly, "session_not_found");
64
+ }
65
+ sendJson(response, 200, result, headOnly);
66
+ return true;
67
+ }
68
+ return notFound(response, headOnly);
69
+ },
70
+ mapError(error, request, response) {
71
+ if (error instanceof SessionUpdatesError) {
72
+ const status = error.code === "session_not_found" ? 404 : 429;
73
+ if (status === 429)
74
+ response.setHeader("Retry-After", "2");
75
+ sendJson(response, status, { error: { code: error.code, message: error.message } }, request.method === "HEAD");
76
+ return true;
77
+ }
78
+ if (error instanceof CatalogCapacityError) {
79
+ response.setHeader("Retry-After", "2");
80
+ sendJson(response, 429, { error: { code: "catalog_capacity_exceeded", message: error.message } }, request.method === "HEAD");
81
+ return true;
82
+ }
83
+ if (error instanceof DOMException && error.name === "AbortError") {
84
+ if (!response.destroyed) {
85
+ response.statusCode = 204;
86
+ response.end();
87
+ }
88
+ return true;
89
+ }
90
+ if (!(error instanceof RepositoryQueryError))
91
+ return false;
92
+ sendJson(response, 400, { error: { code: error.code, message: error.message } }, request.method === "HEAD");
93
+ return true;
94
+ },
95
+ };
96
+ }
97
+ async function sendWaitResponse(request, response, headOnly, read) {
98
+ const abort = new AbortController();
99
+ const disconnect = () => {
100
+ if (!response.writableEnded)
101
+ abort.abort();
102
+ };
103
+ request.once("aborted", disconnect);
104
+ response.once("close", disconnect);
105
+ response.setHeader("Cache-Control", "no-store");
106
+ try {
107
+ const result = await read(abort.signal);
108
+ if (abort.signal.aborted || response.destroyed)
109
+ return true;
110
+ if (result === null) {
111
+ response.statusCode = 204;
112
+ response.end();
113
+ }
114
+ else
115
+ sendJson(response, 200, result, headOnly);
116
+ return true;
117
+ }
118
+ finally {
119
+ request.removeListener("aborted", disconnect);
120
+ response.removeListener("close", disconnect);
121
+ }
122
+ }
123
+ function parseListQuery(params) {
124
+ only(params, ["after"]);
125
+ const after = optional(params, "after");
126
+ if (after !== undefined && !REVISION_PATTERN.test(after))
127
+ invalid("after is malformed");
128
+ return after === undefined ? {} : { after };
129
+ }
130
+ function integer(value, name) {
131
+ if (!/^\d+$/.test(value))
132
+ invalid(`${name} must be an integer`);
133
+ const result = Number(value);
134
+ if (!Number.isSafeInteger(result)) {
135
+ invalid(`${name} is outside the supported range`);
136
+ }
137
+ return result;
138
+ }
139
+ function notFound(response, headOnly, code = "not_found") {
140
+ sendJson(response, 404, { error: { code, message: "API resource not found" } }, headOnly);
141
+ return true;
142
+ }
143
+ //# sourceMappingURL=session-read-routes.js.map
@@ -0,0 +1,179 @@
1
+ import WebSocket, { WebSocketServer } from "ws";
2
+ import { SessionInteractionError, } from "../interaction/interaction-service.js";
3
+ import { InteractionTransportError } from "../interaction/transport.js";
4
+ import { SESSION_ID_PATTERN, SESSION_ROOT } from "./session-path.js";
5
+ const PATH = new RegExp(`^${SESSION_ROOT}/(${SESSION_ID_PATTERN})/terminal-preview$`);
6
+ const MAX_BUFFERED_BYTES = 2 * 1024 * 1024;
7
+ export function createTerminalPreviewWebSocket(interaction) {
8
+ const server = new WebSocketServer({
9
+ noServer: true,
10
+ perMessageDeflate: false,
11
+ maxPayload: 1024,
12
+ });
13
+ const connections = new Map();
14
+ const counts = new Map();
15
+ let closing = false;
16
+ let shutdownTimer;
17
+ const heartbeat = setInterval(() => {
18
+ for (const [socket, connection] of connections) {
19
+ if (!connection.alive)
20
+ socket.terminate();
21
+ else if (socket.readyState === WebSocket.OPEN) {
22
+ connection.alive = false;
23
+ socket.ping();
24
+ }
25
+ }
26
+ }, 30_000);
27
+ heartbeat.unref();
28
+ return {
29
+ handle(request, socket, head) {
30
+ socket.on("error", () => socket.destroy());
31
+ if (closing)
32
+ return reject(socket, 503, "Service Unavailable");
33
+ let url;
34
+ try {
35
+ url = new URL(request.url ?? "/", "http://localhost");
36
+ }
37
+ catch {
38
+ return reject(socket, 400, "Bad Request");
39
+ }
40
+ const match = PATH.exec(url.pathname);
41
+ if (!match)
42
+ return reject(socket, 404, "Not Found");
43
+ if (url.search)
44
+ return reject(socket, 400, "Bad Request");
45
+ const secure = "encrypted" in request.socket && request.socket.encrypted;
46
+ if (request.headers.origin &&
47
+ request.headers.origin !== `${secure ? "https" : "http"}://${request.headers.host}`)
48
+ return reject(socket, 403, "Forbidden");
49
+ server.handleUpgrade(request, socket, head, (ws) => {
50
+ const sessionId = match[1];
51
+ const abort = new AbortController();
52
+ const count = counts.get(sessionId) ?? 0;
53
+ const capacityExceeded = connections.size >= 100 || count >= 10;
54
+ let release;
55
+ const stop = () => {
56
+ abort.abort();
57
+ release?.();
58
+ release = undefined;
59
+ };
60
+ const connection = { alive: true, stop };
61
+ connections.set(ws, connection);
62
+ counts.set(sessionId, count + 1);
63
+ const send = (value) => {
64
+ if (abort.signal.aborted || ws.readyState !== WebSocket.OPEN)
65
+ return;
66
+ if (ws.bufferedAmount > MAX_BUFFERED_BYTES) {
67
+ stop();
68
+ ws.close(1013, "Preview consumer is too slow");
69
+ return;
70
+ }
71
+ ws.send(JSON.stringify(value), (error) => {
72
+ if (error) {
73
+ stop();
74
+ ws.terminate();
75
+ }
76
+ });
77
+ };
78
+ const finish = (message) => {
79
+ if (abort.signal.aborted)
80
+ return;
81
+ send(message);
82
+ stop();
83
+ ws.close(message.retryable ? 1013 : 1008, message.code);
84
+ };
85
+ const fail = (error) => finish(previewError(error));
86
+ ws.on("error", () => {
87
+ stop();
88
+ ws.terminate();
89
+ });
90
+ ws.on("pong", () => {
91
+ connection.alive = true;
92
+ });
93
+ ws.on("message", () => {
94
+ stop();
95
+ ws.close(1008, "Preview accepts no application messages");
96
+ });
97
+ ws.once("close", () => {
98
+ stop();
99
+ connections.delete(ws);
100
+ const remaining = (counts.get(sessionId) ?? 1) - 1;
101
+ if (remaining)
102
+ counts.set(sessionId, remaining);
103
+ else
104
+ counts.delete(sessionId);
105
+ if (closing && !connections.size)
106
+ clearTimeout(shutdownTimer);
107
+ });
108
+ if (capacityExceeded) {
109
+ finish({
110
+ type: "error",
111
+ code: "preview_capacity_exceeded",
112
+ message: "Too many terminal preview connections",
113
+ retryable: true,
114
+ });
115
+ }
116
+ else if (!interaction) {
117
+ fail(new SessionInteractionError("interaction_not_supported", "Interaction is not enabled"));
118
+ }
119
+ else {
120
+ void interaction
121
+ .subscribePreview(sessionId, send, fail, abort.signal)
122
+ .then((close) => {
123
+ if (abort.signal.aborted)
124
+ close();
125
+ else
126
+ release = close;
127
+ })
128
+ .catch(fail);
129
+ }
130
+ });
131
+ },
132
+ close() {
133
+ if (closing)
134
+ return;
135
+ closing = true;
136
+ clearInterval(heartbeat);
137
+ for (const [socket, connection] of connections) {
138
+ connection.stop();
139
+ socket.close(1001, "Server shutting down");
140
+ }
141
+ if (connections.size) {
142
+ shutdownTimer = setTimeout(() => {
143
+ for (const socket of connections.keys())
144
+ socket.terminate();
145
+ }, 1000);
146
+ shutdownTimer.unref();
147
+ }
148
+ server.close();
149
+ },
150
+ };
151
+ }
152
+ function reject(socket, status, reason) {
153
+ socket.end(`HTTP/1.1 ${status} ${reason}\r\nConnection: close\r\nContent-Length: 0\r\n\r\n`);
154
+ }
155
+ function previewError(error) {
156
+ if (error instanceof SessionInteractionError) {
157
+ return {
158
+ type: "error",
159
+ code: error.code,
160
+ message: error.message,
161
+ retryable: ["interaction_failed", "operation_result_unknown", "session_busy"].includes(error.code),
162
+ };
163
+ }
164
+ if (error instanceof InteractionTransportError) {
165
+ return {
166
+ type: "error",
167
+ code: error.code === "disconnected" ? "interaction_not_connected" : "interaction_failed",
168
+ message: error.message,
169
+ retryable: error.code !== "disconnected",
170
+ };
171
+ }
172
+ return {
173
+ type: "error",
174
+ code: "interaction_failed",
175
+ message: "Terminal Preview is temporarily unavailable",
176
+ retryable: true,
177
+ };
178
+ }
179
+ //# sourceMappingURL=terminal-preview-websocket.js.map
@@ -0,0 +1,124 @@
1
+ import { mkdir, readFile, realpath } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { isPreviewableUploadType, MAX_UPLOAD_BYTES } from "../../shared/uploads.js";
4
+ import { resolveHomeDirectory } from "../config.js";
5
+ import { validAttachmentId } from "../uploads/store.js";
6
+ import { RequestBodyError } from "./request-body.js";
7
+ import { sendJson } from "./router.js";
8
+ import { isOpaqueId } from "./session-path.js";
9
+ const SESSION_UPLOAD_PATH = /^\/api\/v1\/sessions\/([^/]+)\/uploads(?:\/([^/]+)(\/(?:content|thumbnail))?)?$/;
10
+ async function* uploadChunks(request) {
11
+ let size = 0;
12
+ for await (const chunk of request.iterator({ destroyOnReturn: false })) {
13
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
14
+ size += buffer.byteLength;
15
+ if (size > MAX_UPLOAD_BYTES) {
16
+ request.resume();
17
+ throw new RequestBodyError(413, "body_too_large", "Files must be at most 20 MiB");
18
+ }
19
+ yield buffer;
20
+ }
21
+ if (!request.complete || request.aborted)
22
+ throw new Error("Upload interrupted");
23
+ }
24
+ export async function uploadDirectory(configured = join(resolveHomeDirectory(), "uploads")) {
25
+ await mkdir(configured, { recursive: true });
26
+ return realpath(configured);
27
+ }
28
+ export function createUploadRoutes(store, sessions) {
29
+ return {
30
+ matches: (url) => url.pathname === "/api/v1/uploads" || SESSION_UPLOAD_PATH.test(url.pathname),
31
+ async handle(request, response, url) {
32
+ const match = SESSION_UPLOAD_PATH.exec(url.pathname);
33
+ const [, encodedSessionId, attachmentId, contentPath] = match ?? [];
34
+ let id;
35
+ try {
36
+ id = match ? decodeURIComponent(encodedSessionId) : url.searchParams.get("sessionId");
37
+ }
38
+ catch {
39
+ throw new RequestBodyError(400, "invalid_session", "Invalid sessionId");
40
+ }
41
+ if (!id)
42
+ throw new RequestBodyError(400, "invalid_session", "sessionId is required");
43
+ if (!isOpaqueId(id))
44
+ throw new RequestBodyError(400, "invalid_session", "Invalid sessionId");
45
+ if (attachmentId && !validAttachmentId(attachmentId))
46
+ throw new RequestBodyError(400, "invalid_session", "Invalid attachment ID");
47
+ const method = request.method;
48
+ if (method === "POST" || method === "DELETE") {
49
+ const protocol = "encrypted" in request.socket && request.socket.encrypted ? "https" : "http";
50
+ if (request.headers.origin &&
51
+ request.headers.origin !== `${protocol}://${request.headers.host}`)
52
+ throw new RequestBodyError(403, "forbidden", "Cross-origin uploads are not allowed");
53
+ }
54
+ let allowedMethod = "GET";
55
+ if (!match)
56
+ allowedMethod = "POST";
57
+ else if (attachmentId && !contentPath)
58
+ allowedMethod = "DELETE";
59
+ if (method !== allowedMethod) {
60
+ response.setHeader("Allow", allowedMethod);
61
+ sendJson(response, 405, {
62
+ error: { code: "method_not_allowed", message: "Method not allowed" },
63
+ });
64
+ return true;
65
+ }
66
+ if (!match) {
67
+ if (Number(request.headers["content-length"]) > MAX_UPLOAD_BYTES) {
68
+ request.resume();
69
+ throw new RequestBodyError(413, "body_too_large", "Files must be at most 20 MiB");
70
+ }
71
+ if (!(await sessions.getSession(id)))
72
+ throw new RequestBodyError(404, "not_found", "Session not found");
73
+ const attachment = await store.upload(id, url.searchParams.get("filename") ?? "file", request.headers["content-type"] ?? "application/octet-stream", uploadChunks(request));
74
+ sendJson(response, 201, attachment);
75
+ }
76
+ else if (!attachmentId) {
77
+ if (!(await sessions.getSession(id)))
78
+ throw new RequestBodyError(404, "not_found", "Session not found");
79
+ sendJson(response, 200, { items: await store.list(id) });
80
+ }
81
+ else if (!contentPath) {
82
+ await store.delete(id, attachmentId);
83
+ sendJson(response, 200, { deleted: true });
84
+ }
85
+ else if (contentPath === "/thumbnail") {
86
+ const data = await store.thumbnail(id, attachmentId);
87
+ if (!data)
88
+ throw new RequestBodyError(404, "not_found", "Thumbnail unavailable");
89
+ response.setHeader("Content-Type", "image/webp");
90
+ response.setHeader("X-Content-Type-Options", "nosniff");
91
+ response.setHeader("Cache-Control", "private, max-age=86400");
92
+ response.end(data);
93
+ }
94
+ else {
95
+ const content = await (async () => {
96
+ const attachment = await store.get(id, attachmentId);
97
+ if (!attachment)
98
+ return null;
99
+ try {
100
+ return { attachment, data: await readFile(attachment.path) };
101
+ }
102
+ catch (error) {
103
+ if (error.code === "ENOENT")
104
+ return null;
105
+ throw error;
106
+ }
107
+ })();
108
+ if (!content)
109
+ throw new RequestBodyError(404, "not_found", "Attachment not found");
110
+ const { attachment, data } = content;
111
+ const preview = isPreviewableUploadType(attachment.type) && !url.searchParams.has("download");
112
+ response.setHeader("Content-Type", preview ? attachment.type : "application/octet-stream");
113
+ response.setHeader("X-Content-Type-Options", "nosniff");
114
+ response.setHeader("Content-Security-Policy", "default-src 'none'; sandbox");
115
+ response.setHeader("Cache-Control", "no-store");
116
+ response.setHeader("Content-Disposition", `${preview ? "inline" : "attachment"}; filename*=UTF-8''${encodeURIComponent(attachment.filename).replace(/['()*]/g, (c) => `%${c.charCodeAt(0).toString(16)}`)}`);
117
+ response.end(data);
118
+ }
119
+ return true;
120
+ },
121
+ mapError: () => false,
122
+ };
123
+ }
124
+ //# sourceMappingURL=upload-routes.js.map