@xopcai/xopc 0.0.27 → 0.0.29

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 (239) hide show
  1. package/dist/extensions/telegram/xopc.extension.json +1 -1
  2. package/dist/extensions/weixin/src/adapters/onboard-cli.d.ts +7 -0
  3. package/dist/extensions/weixin/src/adapters/onboard-cli.js +61 -0
  4. package/dist/extensions/weixin/src/adapters/onboard-cli.js.map +1 -0
  5. package/dist/extensions/weixin/src/cli/qr-login.d.ts +5 -0
  6. package/dist/extensions/weixin/src/cli/qr-login.js +1 -1
  7. package/dist/extensions/weixin/src/cli/qr-login.js.map +1 -1
  8. package/dist/extensions/weixin/src/index.js +1 -1
  9. package/dist/extensions/weixin/src/plugin.d.ts +1 -0
  10. package/dist/extensions/weixin/src/plugin.js +2 -0
  11. package/dist/extensions/weixin/src/plugin.js.map +1 -1
  12. package/dist/gateway/static/root/assets/agents-CkgFSiCY.js +216 -0
  13. package/dist/gateway/static/root/assets/agents-CkgFSiCY.js.map +1 -0
  14. package/dist/gateway/static/root/assets/{apps-page-CBBh_Ww8.js → apps-page-Bmq19MS-.js} +2 -2
  15. package/dist/gateway/static/root/assets/{apps-page-CBBh_Ww8.js.map → apps-page-Bmq19MS-.js.map} +1 -1
  16. package/dist/gateway/static/root/assets/channels-settings-CE7jrdkO.js +9 -0
  17. package/dist/gateway/static/root/assets/channels-settings-CE7jrdkO.js.map +1 -0
  18. package/dist/gateway/static/root/assets/cron-page-BpPPcykJ.js +2 -0
  19. package/dist/gateway/static/root/assets/cron-page-BpPPcykJ.js.map +1 -0
  20. package/dist/gateway/static/root/assets/{cron-utils-08gdQfl9.js → cron-utils-N1PqD2DB.js} +2 -2
  21. package/dist/gateway/static/root/assets/{cron-utils-08gdQfl9.js.map → cron-utils-N1PqD2DB.js.map} +1 -1
  22. package/dist/gateway/static/root/assets/{dist-C1MrygQH.js → dist--p2HQ2QF.js} +2 -2
  23. package/dist/gateway/static/root/assets/{dist-C1MrygQH.js.map → dist--p2HQ2QF.js.map} +1 -1
  24. package/dist/gateway/static/root/assets/{extension-debug-page-DN3HKUGS.js → extension-debug-page-DwHCB_6T.js} +2 -2
  25. package/dist/gateway/static/root/assets/{extension-debug-page-DN3HKUGS.js.map → extension-debug-page-DwHCB_6T.js.map} +1 -1
  26. package/dist/gateway/static/root/assets/{extension-page-CoFDHZtZ.js → extension-page-BsYwQIex.js} +2 -2
  27. package/dist/gateway/static/root/assets/{extension-page-CoFDHZtZ.js.map → extension-page-BsYwQIex.js.map} +1 -1
  28. package/dist/gateway/static/root/assets/{extension-settings-page-BcPCu_Go.js → extension-settings-page-nsisEgjB.js} +2 -2
  29. package/dist/gateway/static/root/assets/{extension-settings-page-BcPCu_Go.js.map → extension-settings-page-nsisEgjB.js.map} +1 -1
  30. package/dist/gateway/static/root/assets/index-CR8zUHGR.js +4734 -0
  31. package/dist/gateway/static/root/assets/{index-PfkB8N37.js.map → index-CR8zUHGR.js.map} +1 -1
  32. package/dist/gateway/static/root/assets/index-Dnfha4O2.css +1 -0
  33. package/dist/gateway/static/root/assets/logs-page-CQwdV_Xw.js +2 -0
  34. package/dist/gateway/static/root/assets/logs-page-CQwdV_Xw.js.map +1 -0
  35. package/dist/gateway/static/root/assets/sessions-page-Be5kIGl_.js +2 -0
  36. package/dist/gateway/static/root/assets/sessions-page-Be5kIGl_.js.map +1 -0
  37. package/dist/gateway/static/root/assets/settings-page-PodSlNwr.js +2 -0
  38. package/dist/gateway/static/root/assets/settings-page-PodSlNwr.js.map +1 -0
  39. package/dist/gateway/static/root/assets/skills-page-Clg8deH0.js +3 -0
  40. package/dist/gateway/static/root/assets/{skills-page-BmBDCEbY.js.map → skills-page-Clg8deH0.js.map} +1 -1
  41. package/dist/gateway/static/root/index.html +2 -2
  42. package/dist/package.js +1 -1
  43. package/dist/src/agent/lifecycle/hook-handler.d.ts +2 -0
  44. package/dist/src/agent/lifecycle/hook-handler.js +24 -0
  45. package/dist/src/agent/lifecycle/hook-handler.js.map +1 -1
  46. package/dist/src/agent/messaging/command-handler.js +10 -2
  47. package/dist/src/agent/messaging/command-handler.js.map +1 -1
  48. package/dist/src/agent/service/process-direct-streaming.js +77 -20
  49. package/dist/src/agent/service/process-direct-streaming.js.map +1 -1
  50. package/dist/src/agent/service.d.ts +15 -0
  51. package/dist/src/agent/service.js +21 -1
  52. package/dist/src/agent/service.js.map +1 -1
  53. package/dist/src/channels/weixin/index.js +1 -1
  54. package/dist/src/cli/agent-chat-log-level-preset.d.ts +8 -0
  55. package/dist/src/cli/agent-chat-log-level-preset.js +25 -0
  56. package/dist/src/cli/agent-chat-log-level-preset.js.map +1 -0
  57. package/dist/src/cli/commands/agent/interactive.js +4 -2
  58. package/dist/src/cli/commands/agent/interactive.js.map +1 -1
  59. package/dist/src/cli/commands/agent/stream-renderer.d.ts +14 -0
  60. package/dist/src/cli/commands/agent/stream-renderer.js +99 -0
  61. package/dist/src/cli/commands/agent/stream-renderer.js.map +1 -0
  62. package/dist/src/cli/commands/agent.js +2 -2
  63. package/dist/src/cli/commands/agent.js.map +1 -1
  64. package/dist/src/cli/commands/onboard.js +77 -93
  65. package/dist/src/cli/commands/onboard.js.map +1 -1
  66. package/dist/src/cli/commands/tui.d.ts +1 -0
  67. package/dist/src/cli/commands/tui.js +40 -0
  68. package/dist/src/cli/commands/tui.js.map +1 -0
  69. package/dist/src/cli/index.d.ts +2 -0
  70. package/dist/src/cli/index.js +7 -3
  71. package/dist/src/cli/index.js.map +1 -1
  72. package/dist/src/config/schema.d.ts +6 -0
  73. package/dist/src/config/schema.js +11 -3
  74. package/dist/src/config/schema.js.map +1 -1
  75. package/dist/src/extensions/hooks.js +5 -1
  76. package/dist/src/extensions/hooks.js.map +1 -1
  77. package/dist/src/extensions/loader.d.ts +1 -0
  78. package/dist/src/extensions/loader.js +3 -1
  79. package/dist/src/extensions/loader.js.map +1 -1
  80. package/dist/src/extensions/sdk/index.d.ts +1 -1
  81. package/dist/src/extensions/sdk/index.js.map +1 -1
  82. package/dist/src/extensions/types/core.d.ts +8 -0
  83. package/dist/src/extensions/types/hooks.d.ts +16 -1
  84. package/dist/src/extensions/types/hooks.js +1 -0
  85. package/dist/src/extensions/types/hooks.js.map +1 -1
  86. package/dist/src/gateway/agents-admin.d.ts +19 -1
  87. package/dist/src/gateway/agents-admin.js +164 -3
  88. package/dist/src/gateway/agents-admin.js.map +1 -1
  89. package/dist/src/gateway/auth.d.ts +17 -3
  90. package/dist/src/gateway/auth.js +35 -16
  91. package/dist/src/gateway/auth.js.map +1 -1
  92. package/dist/src/gateway/hono/app.js +31 -1
  93. package/dist/src/gateway/hono/app.js.map +1 -1
  94. package/dist/src/gateway/hono/lib/config-payload.d.ts +1 -1
  95. package/dist/src/gateway/hono/middleware/auth.js +4 -3
  96. package/dist/src/gateway/hono/middleware/auth.js.map +1 -1
  97. package/dist/src/gateway/hono/middleware/scopes.d.ts +15 -0
  98. package/dist/src/gateway/hono/middleware/scopes.js +41 -0
  99. package/dist/src/gateway/hono/middleware/scopes.js.map +1 -0
  100. package/dist/src/gateway/hono/routes/agents.js +59 -5
  101. package/dist/src/gateway/hono/routes/agents.js.map +1 -1
  102. package/dist/src/gateway/hono/routes/config.js +2 -2
  103. package/dist/src/gateway/hono/routes/config.js.map +1 -1
  104. package/dist/src/gateway/hono/routes/public-gateway.js +1 -0
  105. package/dist/src/gateway/hono/routes/public-gateway.js.map +1 -1
  106. package/dist/src/gateway/hono/routes/sessions.js +17 -0
  107. package/dist/src/gateway/hono/routes/sessions.js.map +1 -1
  108. package/dist/src/gateway/security/audit.d.ts +18 -0
  109. package/dist/src/gateway/security/audit.js +68 -0
  110. package/dist/src/gateway/security/audit.js.map +1 -0
  111. package/dist/src/gateway/security/csp.d.ts +19 -0
  112. package/dist/src/gateway/security/csp.js +52 -0
  113. package/dist/src/gateway/security/csp.js.map +1 -0
  114. package/dist/src/gateway/security/dangerous-tools.d.ts +20 -0
  115. package/dist/src/gateway/security/dangerous-tools.js +46 -0
  116. package/dist/src/gateway/security/dangerous-tools.js.map +1 -0
  117. package/dist/src/gateway/security/flood-guard.d.ts +28 -0
  118. package/dist/src/gateway/security/flood-guard.js +42 -0
  119. package/dist/src/gateway/security/flood-guard.js.map +1 -0
  120. package/dist/src/gateway/security/index.d.ts +9 -0
  121. package/dist/src/gateway/security/index.js +10 -0
  122. package/dist/src/gateway/security/known-weak-secrets.d.ts +10 -0
  123. package/dist/src/gateway/security/known-weak-secrets.js +36 -0
  124. package/dist/src/gateway/security/known-weak-secrets.js.map +1 -0
  125. package/dist/src/gateway/security/operator-scopes.d.ts +37 -0
  126. package/dist/src/gateway/security/operator-scopes.js +137 -0
  127. package/dist/src/gateway/security/operator-scopes.js.map +1 -0
  128. package/dist/src/gateway/security/origin-check.d.ts +21 -0
  129. package/dist/src/gateway/security/origin-check.js +56 -0
  130. package/dist/src/gateway/security/origin-check.js.map +1 -0
  131. package/dist/src/gateway/security/preauth-connection-budget.d.ts +17 -0
  132. package/dist/src/gateway/security/preauth-connection-budget.js +49 -0
  133. package/dist/src/gateway/security/preauth-connection-budget.js.map +1 -0
  134. package/dist/src/gateway/security/secret-equal.d.ts +8 -0
  135. package/dist/src/gateway/security/secret-equal.js +30 -0
  136. package/dist/src/gateway/security/secret-equal.js.map +1 -0
  137. package/dist/src/gateway/service.d.ts +3 -1
  138. package/dist/src/gateway/service.js +40 -4
  139. package/dist/src/gateway/service.js.map +1 -1
  140. package/dist/src/session/client-history.d.ts +21 -0
  141. package/dist/src/session/client-history.js +89 -0
  142. package/dist/src/session/client-history.js.map +1 -0
  143. package/dist/src/session/index.d.ts +1 -0
  144. package/dist/src/session/index.js +2 -1
  145. package/dist/src/session/manager.d.ts +2 -0
  146. package/dist/src/session/manager.js +5 -0
  147. package/dist/src/session/manager.js.map +1 -1
  148. package/dist/src/session/thinking-resolve.js +1 -1
  149. package/dist/src/session/thinking-resolve.js.map +1 -1
  150. package/dist/src/tui/backends/embedded-backend.d.ts +42 -0
  151. package/dist/src/tui/backends/embedded-backend.js +173 -0
  152. package/dist/src/tui/backends/embedded-backend.js.map +1 -0
  153. package/dist/src/tui/backends/gateway-sse-backend.d.ts +53 -0
  154. package/dist/src/tui/backends/gateway-sse-backend.js +256 -0
  155. package/dist/src/tui/backends/gateway-sse-backend.js.map +1 -0
  156. package/dist/src/tui/chat-history.d.ts +4 -0
  157. package/dist/src/tui/chat-history.js +29 -0
  158. package/dist/src/tui/chat-history.js.map +1 -0
  159. package/dist/src/tui/components/assistant-message.d.ts +6 -0
  160. package/dist/src/tui/components/assistant-message.js +19 -0
  161. package/dist/src/tui/components/assistant-message.js.map +1 -0
  162. package/dist/src/tui/components/chat-log.d.ts +21 -0
  163. package/dist/src/tui/components/chat-log.js +113 -0
  164. package/dist/src/tui/components/chat-log.js.map +1 -0
  165. package/dist/src/tui/components/custom-editor.d.ts +14 -0
  166. package/dist/src/tui/components/custom-editor.js +50 -0
  167. package/dist/src/tui/components/custom-editor.js.map +1 -0
  168. package/dist/src/tui/components/fuzzy-filter.d.ts +17 -0
  169. package/dist/src/tui/components/fuzzy-filter.js +85 -0
  170. package/dist/src/tui/components/fuzzy-filter.js.map +1 -0
  171. package/dist/src/tui/components/searchable-select-list.d.ts +39 -0
  172. package/dist/src/tui/components/searchable-select-list.js +257 -0
  173. package/dist/src/tui/components/searchable-select-list.js.map +1 -0
  174. package/dist/src/tui/components/tool-execution.d.ts +16 -0
  175. package/dist/src/tui/components/tool-execution.js +76 -0
  176. package/dist/src/tui/components/tool-execution.js.map +1 -0
  177. package/dist/src/tui/components/user-message.d.ts +6 -0
  178. package/dist/src/tui/components/user-message.js +22 -0
  179. package/dist/src/tui/components/user-message.js.map +1 -0
  180. package/dist/src/tui/sse-consumer.d.ts +15 -0
  181. package/dist/src/tui/sse-consumer.js +75 -0
  182. package/dist/src/tui/sse-consumer.js.map +1 -0
  183. package/dist/src/tui/stream-assembler.d.ts +22 -0
  184. package/dist/src/tui/stream-assembler.js +63 -0
  185. package/dist/src/tui/stream-assembler.js.map +1 -0
  186. package/dist/src/tui/theme.d.ts +73 -0
  187. package/dist/src/tui/theme.js +157 -0
  188. package/dist/src/tui/theme.js.map +1 -0
  189. package/dist/src/tui/tui-agent-events.d.ts +7 -0
  190. package/dist/src/tui/tui-agent-events.js +103 -0
  191. package/dist/src/tui/tui-agent-events.js.map +1 -0
  192. package/dist/src/tui/tui-backend.d.ts +80 -0
  193. package/dist/src/tui/tui-backend.js +1 -0
  194. package/dist/src/tui/tui-commands.d.ts +23 -0
  195. package/dist/src/tui/tui-commands.js +165 -0
  196. package/dist/src/tui/tui-commands.js.map +1 -0
  197. package/dist/src/tui/tui-lifecycle.d.ts +26 -0
  198. package/dist/src/tui/tui-lifecycle.js +57 -0
  199. package/dist/src/tui/tui-lifecycle.js.map +1 -0
  200. package/dist/src/tui/tui-local-shell.d.ts +28 -0
  201. package/dist/src/tui/tui-local-shell.js +147 -0
  202. package/dist/src/tui/tui-local-shell.js.map +1 -0
  203. package/dist/src/tui/tui-overlays.d.ts +8 -0
  204. package/dist/src/tui/tui-overlays.js +22 -0
  205. package/dist/src/tui/tui-overlays.js.map +1 -0
  206. package/dist/src/tui/tui-picker-overlay.d.ts +26 -0
  207. package/dist/src/tui/tui-picker-overlay.js +69 -0
  208. package/dist/src/tui/tui-picker-overlay.js.map +1 -0
  209. package/dist/src/tui/tui-stdio-filter.d.ts +17 -0
  210. package/dist/src/tui/tui-stdio-filter.js +96 -0
  211. package/dist/src/tui/tui-stdio-filter.js.map +1 -0
  212. package/dist/src/tui/tui-submit.d.ts +25 -0
  213. package/dist/src/tui/tui-submit.js +102 -0
  214. package/dist/src/tui/tui-submit.js.map +1 -0
  215. package/dist/src/tui/tui-suspend.d.ts +10 -0
  216. package/dist/src/tui/tui-suspend.js +18 -0
  217. package/dist/src/tui/tui-suspend.js.map +1 -0
  218. package/dist/src/tui/tui-types.d.ts +86 -0
  219. package/dist/src/tui/tui-types.js +21 -0
  220. package/dist/src/tui/tui-types.js.map +1 -0
  221. package/dist/src/tui/tui.d.ts +5 -0
  222. package/dist/src/tui/tui.js +389 -0
  223. package/dist/src/tui/tui.js.map +1 -0
  224. package/package.json +5 -3
  225. package/dist/gateway/static/root/assets/agents-w8_jzuiX.js +0 -216
  226. package/dist/gateway/static/root/assets/agents-w8_jzuiX.js.map +0 -1
  227. package/dist/gateway/static/root/assets/channels-settings-DUKRPC7C.js +0 -9
  228. package/dist/gateway/static/root/assets/channels-settings-DUKRPC7C.js.map +0 -1
  229. package/dist/gateway/static/root/assets/cron-page-S18t1yG-.js +0 -2
  230. package/dist/gateway/static/root/assets/cron-page-S18t1yG-.js.map +0 -1
  231. package/dist/gateway/static/root/assets/index-OT4cGzon.css +0 -1
  232. package/dist/gateway/static/root/assets/index-PfkB8N37.js +0 -4734
  233. package/dist/gateway/static/root/assets/logs-page-DoWe1GWy.js +0 -2
  234. package/dist/gateway/static/root/assets/logs-page-DoWe1GWy.js.map +0 -1
  235. package/dist/gateway/static/root/assets/sessions-page-2uOYwEwd.js +0 -2
  236. package/dist/gateway/static/root/assets/sessions-page-2uOYwEwd.js.map +0 -1
  237. package/dist/gateway/static/root/assets/settings-page-fQWswCuq.js +0 -2
  238. package/dist/gateway/static/root/assets/settings-page-fQWswCuq.js.map +0 -1
  239. package/dist/gateway/static/root/assets/skills-page-BmBDCEbY.js +0 -3
@@ -7,7 +7,7 @@ import { seedWorkspaceBootstrapFiles } from "../agent/context/workspace-seed.js"
7
7
  import { applyAgentConfig, findAgentEntryIndex, pruneAgentConfig, removeAgentDirsFromDisk } from "../commands/agents.config.js";
8
8
  import { GATEWAY_BUILTIN_TOOL_IDS } from "./agent-builtin-tools.js";
9
9
  import { join, resolve } from "node:path";
10
- import { mkdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
10
+ import { mkdir, readFile, realpath, stat, unlink, writeFile } from "node:fs/promises";
11
11
  //#region src/gateway/agents-admin.ts
12
12
  /**
13
13
  * Gateway REST helpers for multi-agent management.
@@ -29,7 +29,17 @@ function collectAgentIdsForList(cfg) {
29
29
  ids.add(defaultId);
30
30
  return [...ids];
31
31
  }
32
- function listGatewayAgents(cfg) {
32
+ /** Extract `**Avatar:**` value from bootstrap IDENTITY.md (same line shape as the gateway console parser). */
33
+ function extractAvatarFromIdentityMarkdown(content) {
34
+ for (const line of content.split("\n")) {
35
+ const match = line.match(/^[-*]\s+\*\*Avatar:\*\*\s*(.*)/i);
36
+ if (match) {
37
+ const v = match[1]?.trim() ?? "";
38
+ return v.length > 0 ? v : void 0;
39
+ }
40
+ }
41
+ }
42
+ async function listGatewayAgents(cfg) {
33
43
  const defaultId = resolveDefaultAgentId(cfg);
34
44
  const agents = [];
35
45
  const defaultsSkills = cfg.agents?.defaults?.skills;
@@ -43,10 +53,15 @@ function listGatewayAgents(cfg) {
43
53
  } : void 0;
44
54
  const entrySkills = entry?.skills;
45
55
  const entryDisable = entry?.tools?.disable ?? [];
56
+ let avatar;
57
+ try {
58
+ avatar = extractAvatarFromIdentityMarkdown(await readFile(join(resolveAgentBootstrapDir(cfg, id), WORKSPACE_FILES.IDENTITY), "utf-8"));
59
+ } catch {}
46
60
  agents.push({
47
61
  id,
48
62
  ...entry?.name?.trim() ? { name: entry.name.trim() } : {},
49
63
  ...entry?.description?.trim() ? { description: entry.description.trim() } : {},
64
+ ...avatar ? { avatar } : {},
50
65
  workspace: profile.resolvedWorkspacePath,
51
66
  bootstrapDir: resolveAgentBootstrapDir(cfg, id),
52
67
  ...model ? { model } : {},
@@ -338,7 +353,153 @@ async function writeAgentBootstrapFile(cfg, agentId, name, content) {
338
353
  }
339
354
  };
340
355
  }
356
+ const AGENT_AVATAR_MAX_BYTES = 512 * 1024;
357
+ const AGENT_AVATAR_BASENAME = "agent-avatar";
358
+ const AGENT_AVATAR_EXTENSIONS = [
359
+ ".png",
360
+ ".jpg",
361
+ ".jpeg",
362
+ ".webp"
363
+ ];
364
+ function agentAvatarFilenames() {
365
+ return AGENT_AVATAR_EXTENSIONS.map((ext) => `${AGENT_AVATAR_BASENAME}${ext}`);
366
+ }
367
+ function mimeToExt(mime) {
368
+ const m = mime.toLowerCase().trim();
369
+ if (m === "image/png") return ".png";
370
+ if (m === "image/jpeg" || m === "image/jpg") return ".jpg";
371
+ if (m === "image/webp") return ".webp";
372
+ return null;
373
+ }
374
+ function detectImageMimeFromBytes(buf) {
375
+ if (buf.length >= 8 && buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71) return "image/png";
376
+ if (buf.length >= 3 && buf[0] === 255 && buf[1] === 216 && buf[2] === 255) return "image/jpeg";
377
+ if (buf.length >= 12 && buf[0] === 82 && buf[1] === 73 && buf[2] === 70 && buf[3] === 70 && buf[8] === 87 && buf[9] === 69 && buf[10] === 66 && buf[11] === 80) return "image/webp";
378
+ return null;
379
+ }
380
+ function assertAgentExistsForAvatar(cfg, id) {
381
+ if (collectAgentIdsForList(cfg).every((x) => x !== id)) return {
382
+ ok: false,
383
+ error: `agent "${id}" not found`,
384
+ status: 404
385
+ };
386
+ return null;
387
+ }
388
+ async function readAgentAvatarFile(cfg, agentId) {
389
+ const missingAgent = assertAgentExistsForAvatar(cfg, agentId);
390
+ if (missingAgent) return missingAgent;
391
+ const id = normalizeAgentId(agentId);
392
+ const root = await bootstrapRootReal(cfg, id);
393
+ for (const name of agentAvatarFilenames()) {
394
+ const abs = resolveWorkspaceSafePath(root, name);
395
+ if (!abs) continue;
396
+ try {
397
+ const st = await stat(abs);
398
+ if (!st.isFile() || st.size <= 0 || st.size > AGENT_AVATAR_MAX_BYTES) continue;
399
+ const buffer = await readFile(abs);
400
+ const detected = detectImageMimeFromBytes(buffer);
401
+ if (!detected) continue;
402
+ return {
403
+ ok: true,
404
+ data: {
405
+ agentId: id,
406
+ buffer,
407
+ contentType: detected,
408
+ path: abs
409
+ }
410
+ };
411
+ } catch {}
412
+ }
413
+ return {
414
+ ok: false,
415
+ error: "avatar not found",
416
+ status: 404
417
+ };
418
+ }
419
+ async function writeAgentAvatarFromBase64(cfg, agentId, base64, mimeType) {
420
+ const missingAgent = assertAgentExistsForAvatar(cfg, agentId);
421
+ if (missingAgent) return missingAgent;
422
+ const id = normalizeAgentId(agentId);
423
+ const ext = mimeToExt(mimeType);
424
+ if (!ext) return {
425
+ ok: false,
426
+ error: "unsupported mimeType (use image/png, image/jpeg, or image/webp)",
427
+ status: 400
428
+ };
429
+ let raw;
430
+ try {
431
+ raw = Buffer.from(base64, "base64");
432
+ } catch {
433
+ return {
434
+ ok: false,
435
+ error: "invalid base64",
436
+ status: 400
437
+ };
438
+ }
439
+ if (raw.length === 0 || raw.length > AGENT_AVATAR_MAX_BYTES) return {
440
+ ok: false,
441
+ error: `avatar must be non-empty and at most ${AGENT_AVATAR_MAX_BYTES} bytes`,
442
+ status: 400
443
+ };
444
+ const detected = detectImageMimeFromBytes(raw);
445
+ if (!detected || !extMatchesDetectedMime(ext, detected)) return {
446
+ ok: false,
447
+ error: "file content does not match declared image type",
448
+ status: 400
449
+ };
450
+ const root = await bootstrapRootReal(cfg, id);
451
+ const rootReal = await bootstrapRootReal(cfg, id);
452
+ const targetName = `${AGENT_AVATAR_BASENAME}${ext}`;
453
+ const abs = resolveWorkspaceSafePath(root, targetName);
454
+ if (!abs || !isPathUnderWorkspace(rootReal, abs)) return {
455
+ ok: false,
456
+ error: "invalid path",
457
+ status: 400
458
+ };
459
+ for (const name of agentAvatarFilenames()) {
460
+ if (name === targetName) continue;
461
+ const other = resolveWorkspaceSafePath(root, name);
462
+ if (other && isPathUnderWorkspace(rootReal, other)) try {
463
+ await unlink(other);
464
+ } catch {}
465
+ }
466
+ await writeFile(abs, raw);
467
+ return {
468
+ ok: true,
469
+ data: {
470
+ agentId: id,
471
+ path: abs
472
+ }
473
+ };
474
+ }
475
+ function mimeToExtToMime(ext) {
476
+ if (ext === ".png") return "image/png";
477
+ if (ext === ".webp") return "image/webp";
478
+ return "image/jpeg";
479
+ }
480
+ function extMatchesDetectedMime(ext, detected) {
481
+ return detected === mimeToExtToMime(ext);
482
+ }
483
+ /** Remove any `agent-avatar.*` in the agent bootstrap dir. Idempotent: ok even when no file existed. */
484
+ async function deleteAgentAvatarFile(cfg, agentId) {
485
+ const missingAgent = assertAgentExistsForAvatar(cfg, agentId);
486
+ if (missingAgent) return missingAgent;
487
+ const id = normalizeAgentId(agentId);
488
+ const root = await bootstrapRootReal(cfg, id);
489
+ const rootReal = await bootstrapRootReal(cfg, id);
490
+ for (const name of agentAvatarFilenames()) {
491
+ const abs = resolveWorkspaceSafePath(root, name);
492
+ if (!abs || !isPathUnderWorkspace(rootReal, abs)) continue;
493
+ try {
494
+ await unlink(abs);
495
+ } catch {}
496
+ }
497
+ return {
498
+ ok: true,
499
+ data: { agentId: id }
500
+ };
501
+ }
341
502
  //#endregion
342
- export { finalizeCreateAgentDirs, listAgentBootstrapFiles, listGatewayAgents, prepareCreateAgent, prepareDeleteAgent, prepareUpdateAgent, readAgentBootstrapFile, runAfterDeletePurge, writeAgentBootstrapFile };
503
+ export { deleteAgentAvatarFile, extractAvatarFromIdentityMarkdown, finalizeCreateAgentDirs, listAgentBootstrapFiles, listGatewayAgents, prepareCreateAgent, prepareDeleteAgent, prepareUpdateAgent, readAgentAvatarFile, readAgentBootstrapFile, runAfterDeletePurge, writeAgentAvatarFromBase64, writeAgentBootstrapFile };
343
504
 
344
505
  //# sourceMappingURL=agents-admin.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"agents-admin.js","names":["pathResolve"],"sources":["../../../src/gateway/agents-admin.ts"],"sourcesContent":["/**\n * Gateway REST helpers for multi-agent management.\n */\n\nimport { mkdir, readFile, realpath, stat, writeFile } from 'node:fs/promises';\nimport { join, resolve as pathResolve } from 'node:path';\n\nimport {\n DEFAULT_AGENT_ID,\n listAgentEntries,\n normalizeAgentId,\n resolveAgentBootstrapDir,\n resolveAgentDir,\n resolveAgentWorkspaceDir,\n resolveDefaultAgentId,\n resolveUserPath,\n validateAgentIdForNewAgent,\n} from '../agent/agent-scope.js';\nimport { BOOTSTRAP_FILES } from '../agent/context/workspace.js';\nimport { seedWorkspaceBootstrapFiles } from '../agent/context/workspace-seed.js';\nimport {\n applyAgentConfig,\n findAgentEntryIndex,\n pruneAgentConfig,\n removeAgentDirsFromDisk,\n} from '../commands/agents.config.js';\nimport type { Config } from '../config/schema.js';\nimport { WORKSPACE_FILES } from '../config/paths.js';\nimport { resolveEffectiveAgentProfile } from '../config/agent-profile.js';\nimport { GATEWAY_BUILTIN_TOOL_IDS } from './agent-builtin-tools.js';\nimport { isPathUnderWorkspace, resolveWorkspaceSafePath } from './workspace-editor-path.js';\n\nconst EDITABLE_BOOTSTRAP_NAMES = new Set<string>([\n ...BOOTSTRAP_FILES,\n WORKSPACE_FILES.BOOTSTRAP,\n WORKSPACE_FILES.CONTEXT,\n WORKSPACE_FILES.SKILLS,\n]);\n\nexport type GatewayAgentRow = {\n id: string;\n name?: string;\n description?: string;\n workspace: string;\n bootstrapDir: string;\n model?: { primary?: string; fallbacks?: string[] };\n isDefault: boolean;\n skills: {\n defaults: string[];\n entry?: string[];\n effectiveAllowlist?: string[];\n };\n tools: {\n defaultsDisable: string[];\n entryDisable: string[];\n effectiveDisable: string[];\n };\n};\n\nexport type GatewayAgentsListResponse = {\n defaultId: string;\n agents: GatewayAgentRow[];\n builtinToolIds: string[];\n};\n\nfunction collectAgentIdsForList(cfg: Config): string[] {\n const entries = listAgentEntries(cfg).filter((e) => e.enabled !== false);\n const defaultId = resolveDefaultAgentId(cfg);\n if (entries.length === 0) {\n return [defaultId];\n }\n const ids = new Set<string>();\n for (const e of entries) {\n ids.add(normalizeAgentId(e.id));\n }\n ids.add(defaultId);\n return [...ids];\n}\n\nexport function listGatewayAgents(cfg: Config): GatewayAgentsListResponse {\n const defaultId = resolveDefaultAgentId(cfg);\n const agents: GatewayAgentRow[] = [];\n const defaultsSkills = cfg.agents?.defaults?.skills;\n const defaultsDisable = cfg.agents?.defaults?.tools?.disable ?? [];\n for (const id of collectAgentIdsForList(cfg)) {\n const profile = resolveEffectiveAgentProfile(cfg, id);\n const entry = listAgentEntries(cfg).find((e) => normalizeAgentId(e.id) === id);\n const model =\n profile.primaryModelRef || profile.fallbacks.length > 0\n ? {\n ...(profile.primaryModelRef ? { primary: profile.primaryModelRef } : {}),\n ...(profile.fallbacks.length > 0 ? { fallbacks: profile.fallbacks } : {}),\n }\n : undefined;\n const entrySkills = entry?.skills;\n const entryDisable = entry?.tools?.disable ?? [];\n agents.push({\n id,\n ...(entry?.name?.trim() ? { name: entry.name.trim() } : {}),\n ...(entry?.description?.trim() ? { description: entry.description.trim() } : {}),\n workspace: profile.resolvedWorkspacePath,\n bootstrapDir: resolveAgentBootstrapDir(cfg, id),\n ...(model ? { model } : {}),\n isDefault: id === defaultId,\n skills: {\n defaults: defaultsSkills ? [...defaultsSkills] : [],\n ...(entrySkills !== undefined ? { entry: [...entrySkills] } : {}),\n ...(profile.skillsAllowlist !== undefined\n ? { effectiveAllowlist: [...profile.skillsAllowlist] }\n : {}),\n },\n tools: {\n defaultsDisable: [...defaultsDisable],\n entryDisable: [...entryDisable],\n effectiveDisable: [...profile.tools.disable].sort((a, b) => a.localeCompare(b)),\n },\n });\n }\n agents.sort((a, b) => a.id.localeCompare(b.id));\n return { defaultId, agents, builtinToolIds: [...GATEWAY_BUILTIN_TOOL_IDS] };\n}\n\nexport type CreateAgentBody = {\n /** Display name stored on the agent entry. */\n name: string;\n /** Optional id seed; normalized agent id defaults from `name` when omitted. */\n id?: string;\n workspace: string;\n model?: string;\n agentDir?: string;\n description?: string;\n};\n\nexport type AgentAdminHttpStatus = 400 | 404 | 409;\n\nexport type AgentAdminResult<T> =\n | { ok: true; data: T }\n | { ok: false; error: string; status?: AgentAdminHttpStatus };\n\nfunction requireNonMain(id: string): AgentAdminResult<never> | null {\n if (normalizeAgentId(id) === DEFAULT_AGENT_ID) {\n return { ok: false, error: `\"${DEFAULT_AGENT_ID}\" is reserved`, status: 400 };\n }\n return null;\n}\n\nexport function prepareCreateAgent(\n cfg: Config,\n body: CreateAgentBody,\n): AgentAdminResult<{ nextConfig: Config; agentId: string; workspace: string }> {\n const name = body.name?.trim() ?? '';\n if (!name) {\n return { ok: false, error: 'name is required', status: 400 };\n }\n const workspace = body.workspace?.trim() ?? '';\n if (!workspace) {\n return { ok: false, error: 'workspace is required', status: 400 };\n }\n const idRes = validateAgentIdForNewAgent(body.id, name);\n if (idRes.ok === false) {\n return { ok: false, error: idRes.error, status: 400 };\n }\n const agentId = idRes.agentId;\n if (findAgentEntryIndex(listAgentEntries(cfg), agentId) >= 0) {\n return { ok: false, error: `agent \"${agentId}\" already exists`, status: 409 };\n }\n const wsAbs = resolveUserPath(workspace);\n let next = applyAgentConfig(cfg, {\n agentId,\n name,\n workspace: wsAbs,\n ...(body.model?.trim() ? { model: body.model.trim() } : {}),\n ...(body.agentDir?.trim() ? { agentDir: body.agentDir.trim() } : {}),\n ...(body.description?.trim() ? { description: body.description.trim() } : {}),\n });\n return { ok: true, data: { nextConfig: next, agentId, workspace: wsAbs } };\n}\n\nexport async function finalizeCreateAgentDirs(cfg: Config, agentId: string): Promise<void> {\n const wsPath = resolveAgentWorkspaceDir(cfg, agentId);\n const adPath = resolveAgentDir(cfg, agentId);\n const bootstrapPath = resolveAgentBootstrapDir(cfg, agentId);\n await mkdir(wsPath, { recursive: true });\n await mkdir(adPath, { recursive: true });\n await mkdir(join(adPath, 'credentials'), { recursive: true });\n await mkdir(bootstrapPath, { recursive: true });\n const id = normalizeAgentId(agentId);\n const entry = listAgentEntries(cfg).find((e) => normalizeAgentId(e.id) === id);\n const displayName = entry?.name?.trim() || id;\n seedWorkspaceBootstrapFiles(bootstrapPath, { displayName });\n}\n\nexport type UpdateAgentBody = {\n name?: string;\n description?: string | null;\n workspace?: string;\n model?: string | null;\n agentDir?: string | null;\n setDefault?: boolean;\n /** Replace `agents.list[].skills`; `null` removes the key (inherit defaults). */\n skills?: string[] | null;\n /** Replace `agents.list[].tools.disable`; `null` clears entry-level disables. */\n toolsDisable?: string[] | null;\n};\n\nexport function prepareUpdateAgent(\n cfg: Config,\n agentIdRaw: string,\n body: UpdateAgentBody,\n): AgentAdminResult<{ nextConfig: Config }> {\n const agentId = normalizeAgentId(agentIdRaw);\n let list = [...listAgentEntries(cfg)];\n let idx = findAgentEntryIndex(list, agentId);\n if (idx < 0 && agentId === resolveDefaultAgentId(cfg)) {\n list = [...list, { id: agentId, enabled: true as const }];\n idx = list.length - 1;\n }\n if (idx < 0) {\n return { ok: false, error: `agent \"${agentId}\" not found`, status: 404 };\n }\n\n type Entry = (typeof list)[number];\n const entry: Entry = { ...list[idx] };\n\n if (body.name !== undefined) {\n const n = body.name.trim();\n if (n) {\n entry.name = n;\n }\n }\n if (body.description !== undefined) {\n if (body.description === null || String(body.description).trim() === '') {\n delete entry.description;\n } else {\n entry.description = String(body.description).trim();\n }\n }\n if (body.workspace !== undefined) {\n const w = body.workspace.trim();\n if (w) {\n entry.workspace = resolveUserPath(w);\n }\n }\n if (body.model !== undefined) {\n if (body.model === null || String(body.model).trim() === '') {\n delete entry.model;\n } else {\n entry.model = String(body.model).trim() as Entry['model'];\n }\n }\n if (body.agentDir !== undefined) {\n if (body.agentDir === null || String(body.agentDir).trim() === '') {\n delete entry.agentDir;\n } else {\n entry.agentDir = String(body.agentDir).trim();\n }\n }\n\n if (body.skills !== undefined) {\n if (body.skills === null) {\n delete entry.skills;\n } else {\n const next = body.skills.map((s) => String(s).trim()).filter(Boolean);\n if (next.length === 0) {\n entry.skills = [];\n } else {\n entry.skills = next;\n }\n }\n }\n\n if (body.toolsDisable !== undefined) {\n if (body.toolsDisable === null) {\n if (entry.tools) {\n delete entry.tools.disable;\n if (Object.keys(entry.tools).length === 0) {\n delete entry.tools;\n }\n }\n } else {\n const next = body.toolsDisable.map((s) => String(s).trim()).filter(Boolean);\n entry.tools = { ...entry.tools, disable: next };\n }\n }\n\n list[idx] = entry;\n let next: Config = {\n ...cfg,\n agents: {\n ...cfg.agents,\n list,\n },\n };\n\n if (body.setDefault === true) {\n next = {\n ...next,\n agents: {\n ...next.agents,\n default: agentId,\n },\n };\n }\n return { ok: true, data: { nextConfig: next } };\n}\n\nexport function prepareDeleteAgent(\n cfg: Config,\n agentIdRaw: string,\n): AgentAdminResult<{ nextConfig: Config; agentId: string }> {\n const agentId = normalizeAgentId(agentIdRaw);\n const reserved = requireNonMain(agentId);\n if (reserved) {\n return reserved;\n }\n if (findAgentEntryIndex(listAgentEntries(cfg), agentId) < 0) {\n return { ok: false, error: `agent \"${agentId}\" not found`, status: 404 };\n }\n const { config: pruned } = pruneAgentConfig(cfg, agentId);\n return { ok: true, data: { nextConfig: pruned, agentId } };\n}\n\nexport async function runAfterDeletePurge(cfg: Config, agentId: string): Promise<void> {\n await removeAgentDirsFromDisk(cfg, agentId);\n}\n\nexport type AgentFileEntry = {\n name: string;\n missing: boolean;\n size?: number;\n updatedAtMs?: number;\n};\n\nasync function bootstrapRootReal(cfg: Config, agentId: string): Promise<string> {\n const dir = resolveAgentBootstrapDir(cfg, agentId);\n await mkdir(dir, { recursive: true });\n try {\n return await realpath(dir);\n } catch {\n return pathResolve(dir);\n }\n}\n\nfunction assertAllowedFile(name: string): AgentAdminResult<never> | null {\n if (!name || name.includes('/') || name.includes('\\\\') || !EDITABLE_BOOTSTRAP_NAMES.has(name)) {\n return { ok: false, error: `unsupported file \"${name}\"`, status: 400 };\n }\n return null;\n}\n\nexport async function listAgentBootstrapFiles(\n cfg: Config,\n agentId: string,\n): Promise<AgentAdminResult<{ agentId: string; bootstrapDir: string; files: AgentFileEntry[] }>> {\n const id = normalizeAgentId(agentId);\n if (collectAgentIdsForList(cfg).every((x) => x !== id)) {\n return { ok: false, error: `agent \"${id}\" not found`, status: 404 };\n }\n const root = await bootstrapRootReal(cfg, id);\n const names = [...EDITABLE_BOOTSTRAP_NAMES];\n const files: AgentFileEntry[] = [];\n for (const name of names.sort((a, b) => a.localeCompare(b))) {\n const abs = resolveWorkspaceSafePath(root, name);\n if (!abs) {\n continue;\n }\n try {\n const st = await stat(abs);\n if (!st.isFile()) {\n continue;\n }\n files.push({\n name,\n missing: false,\n size: st.size,\n updatedAtMs: st.mtimeMs,\n });\n } catch {\n files.push({ name, missing: true });\n }\n }\n files.sort((a, b) => a.name.localeCompare(b.name));\n return { ok: true, data: { agentId: id, bootstrapDir: root, files } };\n}\n\nexport async function readAgentBootstrapFile(\n cfg: Config,\n agentId: string,\n name: string,\n): Promise<AgentAdminResult<{ agentId: string; content: string; path: string }>> {\n const bad = assertAllowedFile(name);\n if (bad) {\n return bad;\n }\n const id = normalizeAgentId(agentId);\n if (collectAgentIdsForList(cfg).every((x) => x !== id)) {\n return { ok: false, error: `agent \"${id}\" not found`, status: 404 };\n }\n const root = await bootstrapRootReal(cfg, id);\n const abs = resolveWorkspaceSafePath(root, name);\n if (!abs) {\n return { ok: false, error: 'invalid path', status: 400 };\n }\n try {\n const content = await readFile(abs, 'utf-8');\n return { ok: true, data: { agentId: id, content, path: abs } };\n } catch {\n return { ok: false, error: 'file not found', status: 404 };\n }\n}\n\nexport async function writeAgentBootstrapFile(\n cfg: Config,\n agentId: string,\n name: string,\n content: string,\n): Promise<AgentAdminResult<{ agentId: string; path: string }>> {\n const bad = assertAllowedFile(name);\n if (bad) {\n return bad;\n }\n const id = normalizeAgentId(agentId);\n if (collectAgentIdsForList(cfg).every((x) => x !== id)) {\n return { ok: false, error: `agent \"${id}\" not found`, status: 404 };\n }\n const root = await bootstrapRootReal(cfg, id);\n const abs = resolveWorkspaceSafePath(root, name);\n if (!abs) {\n return { ok: false, error: 'invalid path', status: 400 };\n }\n const rootReal = await bootstrapRootReal(cfg, id);\n if (!isPathUnderWorkspace(rootReal, abs)) {\n return { ok: false, error: 'path escapes bootstrap root', status: 400 };\n }\n await writeFile(abs, content, 'utf-8');\n return { ok: true, data: { agentId: id, path: abs } };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"agents-admin.js","names":["pathResolve"],"sources":["../../../src/gateway/agents-admin.ts"],"sourcesContent":["/**\n * Gateway REST helpers for multi-agent management.\n */\n\nimport { mkdir, readFile, realpath, stat, unlink, writeFile } from 'node:fs/promises';\nimport { join, resolve as pathResolve } from 'node:path';\n\nimport {\n DEFAULT_AGENT_ID,\n listAgentEntries,\n normalizeAgentId,\n resolveAgentBootstrapDir,\n resolveAgentDir,\n resolveAgentWorkspaceDir,\n resolveDefaultAgentId,\n resolveUserPath,\n validateAgentIdForNewAgent,\n} from '../agent/agent-scope.js';\nimport { BOOTSTRAP_FILES } from '../agent/context/workspace.js';\nimport { seedWorkspaceBootstrapFiles } from '../agent/context/workspace-seed.js';\nimport {\n applyAgentConfig,\n findAgentEntryIndex,\n pruneAgentConfig,\n removeAgentDirsFromDisk,\n} from '../commands/agents.config.js';\nimport type { Config } from '../config/schema.js';\nimport { WORKSPACE_FILES } from '../config/paths.js';\nimport { resolveEffectiveAgentProfile } from '../config/agent-profile.js';\nimport { GATEWAY_BUILTIN_TOOL_IDS } from './agent-builtin-tools.js';\nimport { isPathUnderWorkspace, resolveWorkspaceSafePath } from './workspace-editor-path.js';\n\nconst EDITABLE_BOOTSTRAP_NAMES = new Set<string>([\n ...BOOTSTRAP_FILES,\n WORKSPACE_FILES.BOOTSTRAP,\n WORKSPACE_FILES.CONTEXT,\n WORKSPACE_FILES.SKILLS,\n]);\n\nexport type GatewayAgentRow = {\n id: string;\n name?: string;\n description?: string;\n /** Value from `IDENTITY.md` **Avatar:** line when present (may be URL, `xopc:…`, etc.). */\n avatar?: string;\n workspace: string;\n bootstrapDir: string;\n model?: { primary?: string; fallbacks?: string[] };\n isDefault: boolean;\n skills: {\n defaults: string[];\n entry?: string[];\n effectiveAllowlist?: string[];\n };\n tools: {\n defaultsDisable: string[];\n entryDisable: string[];\n effectiveDisable: string[];\n };\n};\n\nexport type GatewayAgentsListResponse = {\n defaultId: string;\n agents: GatewayAgentRow[];\n builtinToolIds: string[];\n};\n\nfunction collectAgentIdsForList(cfg: Config): string[] {\n const entries = listAgentEntries(cfg).filter((e) => e.enabled !== false);\n const defaultId = resolveDefaultAgentId(cfg);\n if (entries.length === 0) {\n return [defaultId];\n }\n const ids = new Set<string>();\n for (const e of entries) {\n ids.add(normalizeAgentId(e.id));\n }\n ids.add(defaultId);\n return [...ids];\n}\n\n/** Extract `**Avatar:**` value from bootstrap IDENTITY.md (same line shape as the gateway console parser). */\nexport function extractAvatarFromIdentityMarkdown(content: string): string | undefined {\n for (const line of content.split('\\n')) {\n const match = line.match(/^[-*]\\s+\\*\\*Avatar:\\*\\*\\s*(.*)/i);\n if (match) {\n const v = match[1]?.trim() ?? '';\n return v.length > 0 ? v : undefined;\n }\n }\n return undefined;\n}\n\nexport async function listGatewayAgents(cfg: Config): Promise<GatewayAgentsListResponse> {\n const defaultId = resolveDefaultAgentId(cfg);\n const agents: GatewayAgentRow[] = [];\n const defaultsSkills = cfg.agents?.defaults?.skills;\n const defaultsDisable = cfg.agents?.defaults?.tools?.disable ?? [];\n for (const id of collectAgentIdsForList(cfg)) {\n const profile = resolveEffectiveAgentProfile(cfg, id);\n const entry = listAgentEntries(cfg).find((e) => normalizeAgentId(e.id) === id);\n const model =\n profile.primaryModelRef || profile.fallbacks.length > 0\n ? {\n ...(profile.primaryModelRef ? { primary: profile.primaryModelRef } : {}),\n ...(profile.fallbacks.length > 0 ? { fallbacks: profile.fallbacks } : {}),\n }\n : undefined;\n const entrySkills = entry?.skills;\n const entryDisable = entry?.tools?.disable ?? [];\n let avatar: string | undefined;\n try {\n const identityPath = join(resolveAgentBootstrapDir(cfg, id), WORKSPACE_FILES.IDENTITY);\n const content = await readFile(identityPath, 'utf-8');\n avatar = extractAvatarFromIdentityMarkdown(content);\n } catch {\n /* missing IDENTITY.md or unreadable */\n }\n agents.push({\n id,\n ...(entry?.name?.trim() ? { name: entry.name.trim() } : {}),\n ...(entry?.description?.trim() ? { description: entry.description.trim() } : {}),\n ...(avatar ? { avatar } : {}),\n workspace: profile.resolvedWorkspacePath,\n bootstrapDir: resolveAgentBootstrapDir(cfg, id),\n ...(model ? { model } : {}),\n isDefault: id === defaultId,\n skills: {\n defaults: defaultsSkills ? [...defaultsSkills] : [],\n ...(entrySkills !== undefined ? { entry: [...entrySkills] } : {}),\n ...(profile.skillsAllowlist !== undefined\n ? { effectiveAllowlist: [...profile.skillsAllowlist] }\n : {}),\n },\n tools: {\n defaultsDisable: [...defaultsDisable],\n entryDisable: [...entryDisable],\n effectiveDisable: [...profile.tools.disable].sort((a, b) => a.localeCompare(b)),\n },\n });\n }\n agents.sort((a, b) => a.id.localeCompare(b.id));\n return { defaultId, agents, builtinToolIds: [...GATEWAY_BUILTIN_TOOL_IDS] };\n}\n\nexport type CreateAgentBody = {\n /** Display name stored on the agent entry. */\n name: string;\n /** Optional id seed; normalized agent id defaults from `name` when omitted. */\n id?: string;\n workspace: string;\n model?: string;\n agentDir?: string;\n description?: string;\n};\n\nexport type AgentAdminHttpStatus = 400 | 404 | 409;\n\nexport type AgentAdminResult<T> =\n | { ok: true; data: T }\n | { ok: false; error: string; status?: AgentAdminHttpStatus };\n\nfunction requireNonMain(id: string): AgentAdminResult<never> | null {\n if (normalizeAgentId(id) === DEFAULT_AGENT_ID) {\n return { ok: false, error: `\"${DEFAULT_AGENT_ID}\" is reserved`, status: 400 };\n }\n return null;\n}\n\nexport function prepareCreateAgent(\n cfg: Config,\n body: CreateAgentBody,\n): AgentAdminResult<{ nextConfig: Config; agentId: string; workspace: string }> {\n const name = body.name?.trim() ?? '';\n if (!name) {\n return { ok: false, error: 'name is required', status: 400 };\n }\n const workspace = body.workspace?.trim() ?? '';\n if (!workspace) {\n return { ok: false, error: 'workspace is required', status: 400 };\n }\n const idRes = validateAgentIdForNewAgent(body.id, name);\n if (idRes.ok === false) {\n return { ok: false, error: idRes.error, status: 400 };\n }\n const agentId = idRes.agentId;\n if (findAgentEntryIndex(listAgentEntries(cfg), agentId) >= 0) {\n return { ok: false, error: `agent \"${agentId}\" already exists`, status: 409 };\n }\n const wsAbs = resolveUserPath(workspace);\n let next = applyAgentConfig(cfg, {\n agentId,\n name,\n workspace: wsAbs,\n ...(body.model?.trim() ? { model: body.model.trim() } : {}),\n ...(body.agentDir?.trim() ? { agentDir: body.agentDir.trim() } : {}),\n ...(body.description?.trim() ? { description: body.description.trim() } : {}),\n });\n return { ok: true, data: { nextConfig: next, agentId, workspace: wsAbs } };\n}\n\nexport async function finalizeCreateAgentDirs(cfg: Config, agentId: string): Promise<void> {\n const wsPath = resolveAgentWorkspaceDir(cfg, agentId);\n const adPath = resolveAgentDir(cfg, agentId);\n const bootstrapPath = resolveAgentBootstrapDir(cfg, agentId);\n await mkdir(wsPath, { recursive: true });\n await mkdir(adPath, { recursive: true });\n await mkdir(join(adPath, 'credentials'), { recursive: true });\n await mkdir(bootstrapPath, { recursive: true });\n const id = normalizeAgentId(agentId);\n const entry = listAgentEntries(cfg).find((e) => normalizeAgentId(e.id) === id);\n const displayName = entry?.name?.trim() || id;\n seedWorkspaceBootstrapFiles(bootstrapPath, { displayName });\n}\n\nexport type UpdateAgentBody = {\n name?: string;\n description?: string | null;\n workspace?: string;\n model?: string | null;\n agentDir?: string | null;\n setDefault?: boolean;\n /** Replace `agents.list[].skills`; `null` removes the key (inherit defaults). */\n skills?: string[] | null;\n /** Replace `agents.list[].tools.disable`; `null` clears entry-level disables. */\n toolsDisable?: string[] | null;\n};\n\nexport function prepareUpdateAgent(\n cfg: Config,\n agentIdRaw: string,\n body: UpdateAgentBody,\n): AgentAdminResult<{ nextConfig: Config }> {\n const agentId = normalizeAgentId(agentIdRaw);\n let list = [...listAgentEntries(cfg)];\n let idx = findAgentEntryIndex(list, agentId);\n if (idx < 0 && agentId === resolveDefaultAgentId(cfg)) {\n list = [...list, { id: agentId, enabled: true as const }];\n idx = list.length - 1;\n }\n if (idx < 0) {\n return { ok: false, error: `agent \"${agentId}\" not found`, status: 404 };\n }\n\n type Entry = (typeof list)[number];\n const entry: Entry = { ...list[idx] };\n\n if (body.name !== undefined) {\n const n = body.name.trim();\n if (n) {\n entry.name = n;\n }\n }\n if (body.description !== undefined) {\n if (body.description === null || String(body.description).trim() === '') {\n delete entry.description;\n } else {\n entry.description = String(body.description).trim();\n }\n }\n if (body.workspace !== undefined) {\n const w = body.workspace.trim();\n if (w) {\n entry.workspace = resolveUserPath(w);\n }\n }\n if (body.model !== undefined) {\n if (body.model === null || String(body.model).trim() === '') {\n delete entry.model;\n } else {\n entry.model = String(body.model).trim() as Entry['model'];\n }\n }\n if (body.agentDir !== undefined) {\n if (body.agentDir === null || String(body.agentDir).trim() === '') {\n delete entry.agentDir;\n } else {\n entry.agentDir = String(body.agentDir).trim();\n }\n }\n\n if (body.skills !== undefined) {\n if (body.skills === null) {\n delete entry.skills;\n } else {\n const next = body.skills.map((s) => String(s).trim()).filter(Boolean);\n if (next.length === 0) {\n entry.skills = [];\n } else {\n entry.skills = next;\n }\n }\n }\n\n if (body.toolsDisable !== undefined) {\n if (body.toolsDisable === null) {\n if (entry.tools) {\n delete entry.tools.disable;\n if (Object.keys(entry.tools).length === 0) {\n delete entry.tools;\n }\n }\n } else {\n const next = body.toolsDisable.map((s) => String(s).trim()).filter(Boolean);\n entry.tools = { ...entry.tools, disable: next };\n }\n }\n\n list[idx] = entry;\n let next: Config = {\n ...cfg,\n agents: {\n ...cfg.agents,\n list,\n },\n };\n\n if (body.setDefault === true) {\n next = {\n ...next,\n agents: {\n ...next.agents,\n default: agentId,\n },\n };\n }\n return { ok: true, data: { nextConfig: next } };\n}\n\nexport function prepareDeleteAgent(\n cfg: Config,\n agentIdRaw: string,\n): AgentAdminResult<{ nextConfig: Config; agentId: string }> {\n const agentId = normalizeAgentId(agentIdRaw);\n const reserved = requireNonMain(agentId);\n if (reserved) {\n return reserved;\n }\n if (findAgentEntryIndex(listAgentEntries(cfg), agentId) < 0) {\n return { ok: false, error: `agent \"${agentId}\" not found`, status: 404 };\n }\n const { config: pruned } = pruneAgentConfig(cfg, agentId);\n return { ok: true, data: { nextConfig: pruned, agentId } };\n}\n\nexport async function runAfterDeletePurge(cfg: Config, agentId: string): Promise<void> {\n await removeAgentDirsFromDisk(cfg, agentId);\n}\n\nexport type AgentFileEntry = {\n name: string;\n missing: boolean;\n size?: number;\n updatedAtMs?: number;\n};\n\nasync function bootstrapRootReal(cfg: Config, agentId: string): Promise<string> {\n const dir = resolveAgentBootstrapDir(cfg, agentId);\n await mkdir(dir, { recursive: true });\n try {\n return await realpath(dir);\n } catch {\n return pathResolve(dir);\n }\n}\n\nfunction assertAllowedFile(name: string): AgentAdminResult<never> | null {\n if (!name || name.includes('/') || name.includes('\\\\') || !EDITABLE_BOOTSTRAP_NAMES.has(name)) {\n return { ok: false, error: `unsupported file \"${name}\"`, status: 400 };\n }\n return null;\n}\n\nexport async function listAgentBootstrapFiles(\n cfg: Config,\n agentId: string,\n): Promise<AgentAdminResult<{ agentId: string; bootstrapDir: string; files: AgentFileEntry[] }>> {\n const id = normalizeAgentId(agentId);\n if (collectAgentIdsForList(cfg).every((x) => x !== id)) {\n return { ok: false, error: `agent \"${id}\" not found`, status: 404 };\n }\n const root = await bootstrapRootReal(cfg, id);\n const names = [...EDITABLE_BOOTSTRAP_NAMES];\n const files: AgentFileEntry[] = [];\n for (const name of names.sort((a, b) => a.localeCompare(b))) {\n const abs = resolveWorkspaceSafePath(root, name);\n if (!abs) {\n continue;\n }\n try {\n const st = await stat(abs);\n if (!st.isFile()) {\n continue;\n }\n files.push({\n name,\n missing: false,\n size: st.size,\n updatedAtMs: st.mtimeMs,\n });\n } catch {\n files.push({ name, missing: true });\n }\n }\n files.sort((a, b) => a.name.localeCompare(b.name));\n return { ok: true, data: { agentId: id, bootstrapDir: root, files } };\n}\n\nexport async function readAgentBootstrapFile(\n cfg: Config,\n agentId: string,\n name: string,\n): Promise<AgentAdminResult<{ agentId: string; content: string; path: string }>> {\n const bad = assertAllowedFile(name);\n if (bad) {\n return bad;\n }\n const id = normalizeAgentId(agentId);\n if (collectAgentIdsForList(cfg).every((x) => x !== id)) {\n return { ok: false, error: `agent \"${id}\" not found`, status: 404 };\n }\n const root = await bootstrapRootReal(cfg, id);\n const abs = resolveWorkspaceSafePath(root, name);\n if (!abs) {\n return { ok: false, error: 'invalid path', status: 400 };\n }\n try {\n const content = await readFile(abs, 'utf-8');\n return { ok: true, data: { agentId: id, content, path: abs } };\n } catch {\n return { ok: false, error: 'file not found', status: 404 };\n }\n}\n\nexport async function writeAgentBootstrapFile(\n cfg: Config,\n agentId: string,\n name: string,\n content: string,\n): Promise<AgentAdminResult<{ agentId: string; path: string }>> {\n const bad = assertAllowedFile(name);\n if (bad) {\n return bad;\n }\n const id = normalizeAgentId(agentId);\n if (collectAgentIdsForList(cfg).every((x) => x !== id)) {\n return { ok: false, error: `agent \"${id}\" not found`, status: 404 };\n }\n const root = await bootstrapRootReal(cfg, id);\n const abs = resolveWorkspaceSafePath(root, name);\n if (!abs) {\n return { ok: false, error: 'invalid path', status: 400 };\n }\n const rootReal = await bootstrapRootReal(cfg, id);\n if (!isPathUnderWorkspace(rootReal, abs)) {\n return { ok: false, error: 'path escapes bootstrap root', status: 400 };\n }\n await writeFile(abs, content, 'utf-8');\n return { ok: true, data: { agentId: id, path: abs } };\n}\n\n// ---------------------------------------------------------------------------\n// Binary agent avatar (bootstrap dir, not a markdown bootstrap file)\n// ---------------------------------------------------------------------------\n\nconst AGENT_AVATAR_MAX_BYTES = 512 * 1024;\nconst AGENT_AVATAR_BASENAME = 'agent-avatar';\n\nconst AGENT_AVATAR_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.webp'] as const;\n\nfunction agentAvatarFilenames(): string[] {\n return AGENT_AVATAR_EXTENSIONS.map((ext) => `${AGENT_AVATAR_BASENAME}${ext}`);\n}\n\nfunction mimeToExt(mime: string): '.png' | '.jpg' | '.jpeg' | '.webp' | null {\n const m = mime.toLowerCase().trim();\n if (m === 'image/png') return '.png';\n if (m === 'image/jpeg' || m === 'image/jpg') return '.jpg';\n if (m === 'image/webp') return '.webp';\n return null;\n}\n\nfunction detectImageMimeFromBytes(buf: Uint8Array): 'image/png' | 'image/jpeg' | 'image/webp' | null {\n if (buf.length >= 8 && buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) {\n return 'image/png';\n }\n if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) {\n return 'image/jpeg';\n }\n if (\n buf.length >= 12 &&\n buf[0] === 0x52 &&\n buf[1] === 0x49 &&\n buf[2] === 0x46 &&\n buf[3] === 0x46 &&\n buf[8] === 0x57 &&\n buf[9] === 0x45 &&\n buf[10] === 0x42 &&\n buf[11] === 0x50\n ) {\n return 'image/webp';\n }\n return null;\n}\n\nfunction assertAgentExistsForAvatar(cfg: Config, id: string): AgentAdminResult<never> | null {\n if (collectAgentIdsForList(cfg).every((x) => x !== id)) {\n return { ok: false, error: `agent \"${id}\" not found`, status: 404 };\n }\n return null;\n}\n\nexport async function readAgentAvatarFile(\n cfg: Config,\n agentId: string,\n): Promise<AgentAdminResult<{ agentId: string; buffer: Buffer; contentType: string; path: string }>> {\n const missingAgent = assertAgentExistsForAvatar(cfg, agentId);\n if (missingAgent) {\n return missingAgent;\n }\n const id = normalizeAgentId(agentId);\n const root = await bootstrapRootReal(cfg, id);\n for (const name of agentAvatarFilenames()) {\n const abs = resolveWorkspaceSafePath(root, name);\n if (!abs) {\n continue;\n }\n try {\n const st = await stat(abs);\n if (!st.isFile() || st.size <= 0 || st.size > AGENT_AVATAR_MAX_BYTES) {\n continue;\n }\n const buffer = await readFile(abs);\n const detected = detectImageMimeFromBytes(buffer);\n if (!detected) {\n continue;\n }\n return { ok: true, data: { agentId: id, buffer, contentType: detected, path: abs } };\n } catch {\n /* try next */\n }\n }\n return { ok: false, error: 'avatar not found', status: 404 };\n}\n\nexport async function writeAgentAvatarFromBase64(\n cfg: Config,\n agentId: string,\n base64: string,\n mimeType: string,\n): Promise<AgentAdminResult<{ agentId: string; path: string }>> {\n const missingAgent = assertAgentExistsForAvatar(cfg, agentId);\n if (missingAgent) {\n return missingAgent;\n }\n const id = normalizeAgentId(agentId);\n const ext = mimeToExt(mimeType);\n if (!ext) {\n return { ok: false, error: 'unsupported mimeType (use image/png, image/jpeg, or image/webp)', status: 400 };\n }\n let raw: Buffer;\n try {\n raw = Buffer.from(base64, 'base64');\n } catch {\n return { ok: false, error: 'invalid base64', status: 400 };\n }\n if (raw.length === 0 || raw.length > AGENT_AVATAR_MAX_BYTES) {\n return { ok: false, error: `avatar must be non-empty and at most ${AGENT_AVATAR_MAX_BYTES} bytes`, status: 400 };\n }\n const detected = detectImageMimeFromBytes(raw);\n if (!detected || !extMatchesDetectedMime(ext, detected)) {\n return { ok: false, error: 'file content does not match declared image type', status: 400 };\n }\n\n const root = await bootstrapRootReal(cfg, id);\n const rootReal = await bootstrapRootReal(cfg, id);\n const targetName = `${AGENT_AVATAR_BASENAME}${ext}`;\n const abs = resolveWorkspaceSafePath(root, targetName);\n if (!abs || !isPathUnderWorkspace(rootReal, abs)) {\n return { ok: false, error: 'invalid path', status: 400 };\n }\n for (const name of agentAvatarFilenames()) {\n if (name === targetName) {\n continue;\n }\n const other = resolveWorkspaceSafePath(root, name);\n if (other && isPathUnderWorkspace(rootReal, other)) {\n try {\n await unlink(other);\n } catch {\n /* absent */\n }\n }\n }\n await writeFile(abs, raw);\n return { ok: true, data: { agentId: id, path: abs } };\n}\n\nfunction mimeToExtToMime(ext: '.png' | '.jpg' | '.jpeg' | '.webp'): 'image/png' | 'image/jpeg' | 'image/webp' {\n if (ext === '.png') return 'image/png';\n if (ext === '.webp') return 'image/webp';\n return 'image/jpeg';\n}\n\nfunction extMatchesDetectedMime(\n ext: '.png' | '.jpg' | '.jpeg' | '.webp',\n detected: 'image/png' | 'image/jpeg' | 'image/webp',\n): boolean {\n return detected === mimeToExtToMime(ext);\n}\n\n/** Remove any `agent-avatar.*` in the agent bootstrap dir. Idempotent: ok even when no file existed. */\nexport async function deleteAgentAvatarFile(cfg: Config, agentId: string): Promise<AgentAdminResult<{ agentId: string }>> {\n const missingAgent = assertAgentExistsForAvatar(cfg, agentId);\n if (missingAgent) {\n return missingAgent;\n }\n const id = normalizeAgentId(agentId);\n const root = await bootstrapRootReal(cfg, id);\n const rootReal = await bootstrapRootReal(cfg, id);\n for (const name of agentAvatarFilenames()) {\n const abs = resolveWorkspaceSafePath(root, name);\n if (!abs || !isPathUnderWorkspace(rootReal, abs)) {\n continue;\n }\n try {\n await unlink(abs);\n } catch {\n /* absent */\n }\n }\n return { ok: true, data: { agentId: id } };\n}\n"],"mappings":"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"}
@@ -1,10 +1,16 @@
1
1
  import type { GatewayAuthConfig } from '../config/schema.js';
2
2
  /**
3
3
  * Resolved gateway authentication configuration.
4
+ *
5
+ * Supports three modes:
6
+ * - `none`: no authentication (local dev only)
7
+ * - `token`: Bearer token authentication (default)
8
+ * - `password`: password-based authentication (for simpler setups)
4
9
  */
5
10
  export interface ResolvedGatewayAuth {
6
- mode: 'none' | 'token';
11
+ mode: 'none' | 'token' | 'password';
7
12
  token?: string;
13
+ password?: string;
8
14
  }
9
15
  /**
10
16
  * Resolve gateway authentication configuration.
@@ -20,12 +26,20 @@ export declare function resolveGatewayAuth(params: {
20
26
  export declare function assertGatewayAuthConfigured(auth: ResolvedGatewayAuth): void;
21
27
  /**
22
28
  * Constant-time string comparison to prevent timing attacks.
29
+ *
30
+ * Uses `crypto.timingSafeEqual` with padding so both buffers always have
31
+ * the same byte length. The actual length is checked separately.
32
+ *
33
+ * @deprecated Use `safeEqualSecret` from `./security/secret-equal.js` directly.
23
34
  */
24
35
  export declare function safeCompare(a: string, b: string): boolean;
25
36
  /**
26
- * Validate token against configured auth.
37
+ * Validate a credential against configured auth using constant-time comparison.
38
+ *
39
+ * Works for both token and password modes — the caller extracts the credential
40
+ * from the appropriate transport (header, query param, etc.).
27
41
  */
28
- export declare function validateToken(auth: ResolvedGatewayAuth, providedToken?: string | null): boolean;
42
+ export declare function validateToken(auth: ResolvedGatewayAuth, providedCredential?: string | null): boolean;
29
43
  /**
30
44
  * Extract token from request headers.
31
45
  * Supports: Authorization: Bearer <token>, X-Api-Key: <token>
@@ -1,3 +1,4 @@
1
+ import { safeEqualSecret } from "./security/secret-equal.js";
1
2
  import crypto from "crypto";
2
3
  //#region src/gateway/auth.ts
3
4
  /**
@@ -9,16 +10,26 @@ function resolveGatewayAuth(params) {
9
10
  const config = params.authConfig ?? { mode: "token" };
10
11
  const envMode = env.XOPC_GATEWAY_AUTH_MODE;
11
12
  const envToken = env.XOPC_GATEWAY_TOKEN;
13
+ const envPassword = env.XOPC_GATEWAY_PASSWORD;
12
14
  let mode = "token";
13
- if (envMode === "none" || envMode === "token") mode = envMode;
14
- else if (config.mode === "none") mode = "none";
15
+ if (envMode === "none" || envMode === "token" || envMode === "password") mode = envMode;
16
+ else if (config.mode === "none" || config.mode === "password") mode = config.mode;
17
+ const hasToken = Boolean(envToken || config.token);
18
+ const hasPassword = Boolean(envPassword || config.password);
19
+ if (hasToken && hasPassword) throw new Error("Invalid config: both gateway.auth.token and gateway.auth.password are set. Choose one authentication mode: \"token\" (Bearer header) or \"password\".");
15
20
  let token;
16
- if (envToken) token = envToken;
21
+ if (mode === "token") if (envToken) token = envToken;
17
22
  else if (config.token) token = config.token;
18
- else if (mode === "token") token = crypto.randomBytes(24).toString("hex");
23
+ else token = crypto.randomBytes(24).toString("hex");
24
+ let password;
25
+ if (mode === "password") {
26
+ if (envPassword) password = envPassword;
27
+ else if (config.password) password = config.password;
28
+ }
19
29
  return {
20
30
  mode,
21
- token
31
+ token,
32
+ password
22
33
  };
23
34
  }
24
35
  /**
@@ -26,26 +37,34 @@ function resolveGatewayAuth(params) {
26
37
  */
27
38
  function assertGatewayAuthConfigured(auth) {
28
39
  if (auth.mode === "token" && !auth.token) throw new Error("Gateway auth mode is token, but no token was configured. Set gateway.auth.token in config or XOPC_GATEWAY_TOKEN environment variable.");
40
+ if (auth.mode === "password" && !auth.password) throw new Error("Gateway auth mode is password, but no password was configured. Set gateway.auth.password in config or XOPC_GATEWAY_PASSWORD environment variable.");
29
41
  }
30
42
  /**
31
43
  * Constant-time string comparison to prevent timing attacks.
44
+ *
45
+ * Uses `crypto.timingSafeEqual` with padding so both buffers always have
46
+ * the same byte length. The actual length is checked separately.
47
+ *
48
+ * @deprecated Use `safeEqualSecret` from `./security/secret-equal.js` directly.
32
49
  */
33
50
  function safeCompare(a, b) {
34
- const aBuf = Buffer.from(a, "utf8");
35
- const bBuf = Buffer.from(b, "utf8");
36
- if (aBuf.length === bBuf.length) return crypto.timingSafeEqual(aBuf, bBuf);
37
- let result = aBuf.length ^ bBuf.length;
38
- const maxLen = Math.max(aBuf.length, bBuf.length);
39
- for (let i = 0; i < maxLen; i++) result |= aBuf[i % aBuf.length] ^ bBuf[i % bBuf.length];
40
- return result === 0;
51
+ return safeEqualSecret(a, b);
41
52
  }
42
53
  /**
43
- * Validate token against configured auth.
54
+ * Validate a credential against configured auth using constant-time comparison.
55
+ *
56
+ * Works for both token and password modes — the caller extracts the credential
57
+ * from the appropriate transport (header, query param, etc.).
44
58
  */
45
- function validateToken(auth, providedToken) {
59
+ function validateToken(auth, providedCredential) {
46
60
  if (auth.mode === "none") return true;
47
- if (!auth.token || !providedToken) return false;
48
- return safeCompare(auth.token, providedToken);
61
+ if (!providedCredential) return false;
62
+ if (auth.mode === "password") {
63
+ if (!auth.password) return false;
64
+ return safeEqualSecret(auth.password, providedCredential);
65
+ }
66
+ if (!auth.token) return false;
67
+ return safeEqualSecret(auth.token, providedCredential);
49
68
  }
50
69
  /**
51
70
  * Extract token from request headers.
@@ -1 +1 @@
1
- {"version":3,"file":"auth.js","names":[],"sources":["../../../src/gateway/auth.ts"],"sourcesContent":["import crypto from 'crypto';\nimport type { GatewayAuthConfig } from '../config/schema.js';\n\n/**\n * Resolved gateway authentication configuration.\n */\nexport interface ResolvedGatewayAuth {\n mode: 'none' | 'token';\n token?: string;\n}\n\n/**\n * Resolve gateway authentication configuration.\n * Priority: env vars > config > defaults\n */\nexport function resolveGatewayAuth(params: {\n authConfig?: GatewayAuthConfig | null;\n env?: NodeJS.ProcessEnv;\n}): ResolvedGatewayAuth {\n const env = params.env ?? process.env;\n const config: GatewayAuthConfig = params.authConfig ?? { mode: 'token' };\n\n // Environment variables take precedence\n const envMode = env.XOPC_GATEWAY_AUTH_MODE;\n const envToken = env.XOPC_GATEWAY_TOKEN;\n\n // Resolve mode\n let mode: ResolvedGatewayAuth['mode'] = 'token';\n if (envMode === 'none' || envMode === 'token') {\n mode = envMode;\n } else if (config.mode === 'none') {\n mode = 'none';\n }\n\n // Resolve token\n let token: string | undefined;\n if (envToken) {\n token = envToken;\n } else if (config.token) {\n token = config.token;\n } else if (mode === 'token') {\n // Auto-generate token if not provided\n token = crypto.randomBytes(24).toString('hex');\n }\n\n return { mode, token };\n}\n\n/**\n * Assert that gateway auth is properly configured.\n */\nexport function assertGatewayAuthConfigured(auth: ResolvedGatewayAuth): void {\n if (auth.mode === 'token' && !auth.token) {\n throw new Error(\n 'Gateway auth mode is token, but no token was configured. ' +\n 'Set gateway.auth.token in config or XOPC_GATEWAY_TOKEN environment variable.'\n );\n }\n}\n\n/**\n * Constant-time string comparison to prevent timing attacks.\n */\nexport function safeCompare(a: string, b: string): boolean {\n const aBuf = Buffer.from(a, 'utf8');\n const bBuf = Buffer.from(b, 'utf8');\n \n if (aBuf.length === bBuf.length) {\n return crypto.timingSafeEqual(aBuf, bBuf);\n }\n \n // For different lengths\n let result = aBuf.length ^ bBuf.length;\n const maxLen = Math.max(aBuf.length, bBuf.length);\n for (let i = 0; i < maxLen; i++) {\n result |= (aBuf[i % aBuf.length] ^ bBuf[i % bBuf.length]);\n }\n return result === 0;\n}\n\n/**\n * Validate token against configured auth.\n */\nexport function validateToken(auth: ResolvedGatewayAuth, providedToken?: string | null): boolean {\n if (auth.mode === 'none') {\n return true;\n }\n if (!auth.token || !providedToken) {\n return false;\n }\n return safeCompare(auth.token, providedToken);\n}\n\n/**\n * Extract token from request headers.\n * Supports: Authorization: Bearer <token>, X-Api-Key: <token>\n */\nexport function extractToken(headers?: Record<string, string | string[] | undefined>): string | undefined {\n if (!headers) return undefined;\n\n // Authorization: Bearer <token>\n const authHeader = headers.authorization;\n if (authHeader) {\n const value = Array.isArray(authHeader) ? authHeader[0] : authHeader;\n if (value?.startsWith('Bearer ')) {\n return value.slice(7);\n }\n }\n\n // X-Api-Key: <token>\n const apiKey = headers['x-api-key'];\n if (apiKey) {\n return Array.isArray(apiKey) ? apiKey[0] : apiKey;\n }\n\n return undefined;\n}\n"],"mappings":";;;;;;AAeA,SAAgB,mBAAmB,QAGX;CACtB,MAAM,MAAM,OAAO,OAAO,QAAQ;CAClC,MAAM,SAA4B,OAAO,cAAc,EAAE,MAAM,SAAS;CAGxE,MAAM,UAAU,IAAI;CACpB,MAAM,WAAW,IAAI;CAGrB,IAAI,OAAoC;AACxC,KAAI,YAAY,UAAU,YAAY,QACpC,QAAO;UACE,OAAO,SAAS,OACzB,QAAO;CAIT,IAAI;AACJ,KAAI,SACF,SAAQ;UACC,OAAO,MAChB,SAAQ,OAAO;UACN,SAAS,QAElB,SAAQ,OAAO,YAAY,GAAG,CAAC,SAAS,MAAM;AAGhD,QAAO;EAAE;EAAM;EAAO;;;;;AAMxB,SAAgB,4BAA4B,MAAiC;AAC3E,KAAI,KAAK,SAAS,WAAW,CAAC,KAAK,MACjC,OAAM,IAAI,MACR,wIAED;;;;;AAOL,SAAgB,YAAY,GAAW,GAAoB;CACzD,MAAM,OAAO,OAAO,KAAK,GAAG,OAAO;CACnC,MAAM,OAAO,OAAO,KAAK,GAAG,OAAO;AAEnC,KAAI,KAAK,WAAW,KAAK,OACvB,QAAO,OAAO,gBAAgB,MAAM,KAAK;CAI3C,IAAI,SAAS,KAAK,SAAS,KAAK;CAChC,MAAM,SAAS,KAAK,IAAI,KAAK,QAAQ,KAAK,OAAO;AACjD,MAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,IAC1B,WAAW,KAAK,IAAI,KAAK,UAAU,KAAK,IAAI,KAAK;AAEnD,QAAO,WAAW;;;;;AAMpB,SAAgB,cAAc,MAA2B,eAAwC;AAC/F,KAAI,KAAK,SAAS,OAChB,QAAO;AAET,KAAI,CAAC,KAAK,SAAS,CAAC,cAClB,QAAO;AAET,QAAO,YAAY,KAAK,OAAO,cAAc;;;;;;AAO/C,SAAgB,aAAa,SAA6E;AACxG,KAAI,CAAC,QAAS,QAAO,KAAA;CAGrB,MAAM,aAAa,QAAQ;AAC3B,KAAI,YAAY;EACd,MAAM,QAAQ,MAAM,QAAQ,WAAW,GAAG,WAAW,KAAK;AAC1D,MAAI,OAAO,WAAW,UAAU,CAC9B,QAAO,MAAM,MAAM,EAAE;;CAKzB,MAAM,SAAS,QAAQ;AACvB,KAAI,OACF,QAAO,MAAM,QAAQ,OAAO,GAAG,OAAO,KAAK"}
1
+ {"version":3,"file":"auth.js","names":[],"sources":["../../../src/gateway/auth.ts"],"sourcesContent":["import crypto from 'crypto';\nimport type { GatewayAuthConfig } from '../config/schema.js';\nimport { safeEqualSecret } from './security/secret-equal.js';\n\n/**\n * Resolved gateway authentication configuration.\n *\n * Supports three modes:\n * - `none`: no authentication (local dev only)\n * - `token`: Bearer token authentication (default)\n * - `password`: password-based authentication (for simpler setups)\n */\nexport interface ResolvedGatewayAuth {\n mode: 'none' | 'token' | 'password';\n token?: string;\n password?: string;\n}\n\n/**\n * Resolve gateway authentication configuration.\n * Priority: env vars > config > defaults\n */\nexport function resolveGatewayAuth(params: {\n authConfig?: GatewayAuthConfig | null;\n env?: NodeJS.ProcessEnv;\n}): ResolvedGatewayAuth {\n const env = params.env ?? process.env;\n const config: GatewayAuthConfig = params.authConfig ?? { mode: 'token' };\n\n // Environment variables take precedence\n const envMode = env.XOPC_GATEWAY_AUTH_MODE;\n const envToken = env.XOPC_GATEWAY_TOKEN;\n const envPassword = env.XOPC_GATEWAY_PASSWORD;\n\n // Resolve mode\n let mode: ResolvedGatewayAuth['mode'] = 'token';\n if (envMode === 'none' || envMode === 'token' || envMode === 'password') {\n mode = envMode;\n } else if (config.mode === 'none' || config.mode === 'password') {\n mode = config.mode;\n }\n\n // Ambiguity detection: reject conflicting credential types\n const hasToken = Boolean(envToken || config.token);\n const hasPassword = Boolean(envPassword || config.password);\n if (hasToken && hasPassword) {\n throw new Error(\n 'Invalid config: both gateway.auth.token and gateway.auth.password are set. ' +\n 'Choose one authentication mode: \"token\" (Bearer header) or \"password\".',\n );\n }\n\n // Resolve token\n let token: string | undefined;\n if (mode === 'token') {\n if (envToken) {\n token = envToken;\n } else if (config.token) {\n token = config.token;\n } else {\n // Auto-generate token if not provided\n token = crypto.randomBytes(24).toString('hex');\n }\n }\n\n // Resolve password\n let password: string | undefined;\n if (mode === 'password') {\n if (envPassword) {\n password = envPassword;\n } else if (config.password) {\n password = config.password;\n }\n }\n\n return { mode, token, password };\n}\n\n/**\n * Assert that gateway auth is properly configured.\n */\nexport function assertGatewayAuthConfigured(auth: ResolvedGatewayAuth): void {\n if (auth.mode === 'token' && !auth.token) {\n throw new Error(\n 'Gateway auth mode is token, but no token was configured. ' +\n 'Set gateway.auth.token in config or XOPC_GATEWAY_TOKEN environment variable.',\n );\n }\n if (auth.mode === 'password' && !auth.password) {\n throw new Error(\n 'Gateway auth mode is password, but no password was configured. ' +\n 'Set gateway.auth.password in config or XOPC_GATEWAY_PASSWORD environment variable.',\n );\n }\n}\n\n/**\n * Constant-time string comparison to prevent timing attacks.\n *\n * Uses `crypto.timingSafeEqual` with padding so both buffers always have\n * the same byte length. The actual length is checked separately.\n *\n * @deprecated Use `safeEqualSecret` from `./security/secret-equal.js` directly.\n */\nexport function safeCompare(a: string, b: string): boolean {\n return safeEqualSecret(a, b);\n}\n\n/**\n * Validate a credential against configured auth using constant-time comparison.\n *\n * Works for both token and password modes the caller extracts the credential\n * from the appropriate transport (header, query param, etc.).\n */\nexport function validateToken(auth: ResolvedGatewayAuth, providedCredential?: string | null): boolean {\n if (auth.mode === 'none') {\n return true;\n }\n\n if (!providedCredential) {\n return false;\n }\n\n if (auth.mode === 'password') {\n if (!auth.password) return false;\n return safeEqualSecret(auth.password, providedCredential);\n }\n\n // Default: token mode\n if (!auth.token) return false;\n return safeEqualSecret(auth.token, providedCredential);\n}\n\n/**\n * Extract token from request headers.\n * Supports: Authorization: Bearer <token>, X-Api-Key: <token>\n */\nexport function extractToken(headers?: Record<string, string | string[] | undefined>): string | undefined {\n if (!headers) return undefined;\n\n // Authorization: Bearer <token>\n const authHeader = headers.authorization;\n if (authHeader) {\n const value = Array.isArray(authHeader) ? authHeader[0] : authHeader;\n if (value?.startsWith('Bearer ')) {\n return value.slice(7);\n }\n }\n\n // X-Api-Key: <token>\n const apiKey = headers['x-api-key'];\n if (apiKey) {\n return Array.isArray(apiKey) ? apiKey[0] : apiKey;\n }\n\n return undefined;\n}\n"],"mappings":";;;;;;;AAsBA,SAAgB,mBAAmB,QAGX;CACtB,MAAM,MAAM,OAAO,OAAO,QAAQ;CAClC,MAAM,SAA4B,OAAO,cAAc,EAAE,MAAM,SAAS;CAGxE,MAAM,UAAU,IAAI;CACpB,MAAM,WAAW,IAAI;CACrB,MAAM,cAAc,IAAI;CAGxB,IAAI,OAAoC;AACxC,KAAI,YAAY,UAAU,YAAY,WAAW,YAAY,WAC3D,QAAO;UACE,OAAO,SAAS,UAAU,OAAO,SAAS,WACnD,QAAO,OAAO;CAIhB,MAAM,WAAW,QAAQ,YAAY,OAAO,MAAM;CAClD,MAAM,cAAc,QAAQ,eAAe,OAAO,SAAS;AAC3D,KAAI,YAAY,YACd,OAAM,IAAI,MACR,wJAED;CAIH,IAAI;AACJ,KAAI,SAAS,QACX,KAAI,SACF,SAAQ;UACC,OAAO,MAChB,SAAQ,OAAO;KAGf,SAAQ,OAAO,YAAY,GAAG,CAAC,SAAS,MAAM;CAKlD,IAAI;AACJ,KAAI,SAAS;MACP,YACF,YAAW;WACF,OAAO,SAChB,YAAW,OAAO;;AAItB,QAAO;EAAE;EAAM;EAAO;EAAU;;;;;AAMlC,SAAgB,4BAA4B,MAAiC;AAC3E,KAAI,KAAK,SAAS,WAAW,CAAC,KAAK,MACjC,OAAM,IAAI,MACR,wIAED;AAEH,KAAI,KAAK,SAAS,cAAc,CAAC,KAAK,SACpC,OAAM,IAAI,MACR,oJAED;;;;;;;;;;AAYL,SAAgB,YAAY,GAAW,GAAoB;AACzD,QAAO,gBAAgB,GAAG,EAAE;;;;;;;;AAS9B,SAAgB,cAAc,MAA2B,oBAA6C;AACpG,KAAI,KAAK,SAAS,OAChB,QAAO;AAGT,KAAI,CAAC,mBACH,QAAO;AAGT,KAAI,KAAK,SAAS,YAAY;AAC5B,MAAI,CAAC,KAAK,SAAU,QAAO;AAC3B,SAAO,gBAAgB,KAAK,UAAU,mBAAmB;;AAI3D,KAAI,CAAC,KAAK,MAAO,QAAO;AACxB,QAAO,gBAAgB,KAAK,OAAO,mBAAmB;;;;;;AAOxD,SAAgB,aAAa,SAA6E;AACxG,KAAI,CAAC,QAAS,QAAO,KAAA;CAGrB,MAAM,aAAa,QAAQ;AAC3B,KAAI,YAAY;EACd,MAAM,QAAQ,MAAM,QAAQ,WAAW,GAAG,WAAW,KAAK;AAC1D,MAAI,OAAO,WAAW,UAAU,CAC9B,QAAO,MAAM,MAAM,EAAE;;CAKzB,MAAM,SAAS,QAAQ;AACvB,KAAI,OACF,QAAO,MAAM,QAAQ,OAAO,GAAG,OAAO,KAAK"}
@@ -1,7 +1,10 @@
1
1
  import { createLogger } from "../../utils/logger/index.js";
2
2
  import { init_logger } from "../../utils/logger.js";
3
3
  import { maxWebchatAgentRequestBodyBytes } from "../chat-limits.js";
4
+ import { buildGatewayConsoleCspHeader } from "../security/csp.js";
5
+ import { checkBrowserOrigin } from "../security/origin-check.js";
4
6
  import { auth } from "./middleware/auth.js";
7
+ import { operatorScopes } from "./middleware/scopes.js";
5
8
  import { logContextMiddleware } from "./middleware/log-context.js";
6
9
  import { logger } from "./middleware/logger.js";
7
10
  import { registerPublicExtensionAssetRoutes } from "./routes/auth-registry-extensions.js";
@@ -57,6 +60,7 @@ function createHonoApp(config) {
57
60
  app.use(logContextMiddleware());
58
61
  app.use(logger());
59
62
  app.use(cors(CORS_OPTIONS));
63
+ const gatewayConsoleCsp = buildGatewayConsoleCspHeader();
60
64
  app.use(createMiddleware(async (c, next) => {
61
65
  await next();
62
66
  if (isExtensionGatewayUiAssetPath(c.req.path)) return;
@@ -65,7 +69,32 @@ function createHonoApp(config) {
65
69
  c.header("Referrer-Policy", "strict-origin-when-cross-origin");
66
70
  c.header("X-XSS-Protection", "1; mode=block");
67
71
  c.header("Permissions-Policy", "camera=(), microphone=(self), geolocation=()");
68
- c.header("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'");
72
+ c.header("Content-Security-Policy", gatewayConsoleCsp);
73
+ }));
74
+ const allowedOrigins = Array.isArray(corsOrigin) ? corsOrigin : [corsOrigin];
75
+ app.use("/api/*", createMiddleware(async (c, next) => {
76
+ if (isExtensionGatewayUiAssetPath(c.req.path)) return next();
77
+ const origin = c.req.header("origin");
78
+ if (!origin) return next();
79
+ const result = checkBrowserOrigin({
80
+ requestHost: c.req.header("host"),
81
+ origin,
82
+ allowedOrigins,
83
+ allowHostHeaderOriginFallback: true,
84
+ isLocalClient: false
85
+ });
86
+ if (!result.ok) {
87
+ log.warn({
88
+ origin,
89
+ reason: "reason" in result ? result.reason : "unknown",
90
+ path: c.req.path
91
+ }, "Browser origin check failed");
92
+ return c.json({
93
+ error: "Forbidden",
94
+ message: "Origin not allowed"
95
+ }, 403);
96
+ }
97
+ return next();
69
98
  }));
70
99
  app.use("/api/skills/upload", bodyLimit({
71
100
  maxSize: 10 * 1024 * 1024,
@@ -96,6 +125,7 @@ function createHonoApp(config) {
96
125
  token,
97
126
  getGatewayAuth: () => service.currentConfig.gateway?.auth
98
127
  }));
128
+ authenticated.use(operatorScopes());
99
129
  const strictRateLimiter = /* @__PURE__ */ new Map();
100
130
  setInterval(() => {
101
131
  for (const [ip, limiter] of strictRateLimiter.entries()) if (limiter.consume().remaining === 9) strictRateLimiter.delete(ip);
@@ -1 +1 @@
1
- {"version":3,"file":"app.js","names":[],"sources":["../../../../src/gateway/hono/app.ts"],"sourcesContent":["import { Hono } from 'hono';\nimport { cors } from 'hono/cors';\nimport { createMiddleware } from 'hono/factory';\nimport { bodyLimit } from 'hono/body-limit';\n\nimport { createFixedWindowRateLimiter } from '../../infra/rate-limit.js';\nimport { createLogger } from '../../utils/logger.js';\nimport type { GatewayService } from '../service.js';\nimport { maxWebchatAgentRequestBodyBytes } from '../chat-limits.js';\nimport { auth } from './middleware/auth.js';\nimport { logContextMiddleware } from './middleware/log-context.js';\nimport { logger } from './middleware/logger.js';\nimport { registerPublicExtensionAssetRoutes } from './routes/auth-registry-extensions.js';\nimport { registerAuthenticatedRoutes } from './routes/index.js';\nimport { registerPublicGatewayRoutes } from './routes/public-gateway.js';\n\nconst log = createLogger('HonoApp');\n\nexport interface HonoAppConfig {\n service: GatewayService;\n token?: string;\n}\n\n/**\n * Extension sandbox HTML under `/api/extensions/:id/assets/*` ships its own CSP\n * (`frame-ancestors 'self'`). The global gateway middleware must not overwrite it\n * with `frame-ancestors 'none'` / `X-Frame-Options: DENY`, or the console cannot embed iframes.\n */\nexport function isExtensionGatewayUiAssetPath(path: string): boolean {\n return /^\\/api\\/extensions\\/[^/]+\\/assets\\//.test(path);\n}\n\nexport function createHonoApp(config: HonoAppConfig): Hono {\n const { service, token } = config;\n const app = new Hono();\n\n const gatewayPort = service.currentConfig.gateway.port ?? 18790;\n const configuredOrigins = service.currentConfig.gateway.corsOrigins;\n\n let corsOrigin: string | string[];\n if (configuredOrigins && configuredOrigins.length > 0) {\n corsOrigin = configuredOrigins;\n } else {\n corsOrigin = [\n `http://localhost:${gatewayPort}`,\n `http://127.0.0.1:${gatewayPort}`,\n 'http://localhost:3000',\n 'http://127.0.0.1:3000',\n ];\n }\n\n const CORS_OPTIONS = {\n origin: corsOrigin,\n allowMethods: ['GET', 'POST', 'PATCH', 'DELETE', 'OPTIONS'],\n allowHeaders: ['Content-Type', 'Authorization', 'Accept', 'X-Session-Id', 'Last-Event-ID'],\n credentials: true,\n maxAge: 86400,\n };\n\n app.use(logContextMiddleware());\n app.use(logger());\n app.use(cors(CORS_OPTIONS));\n\n app.use(createMiddleware(async (c, next) => {\n await next();\n if (isExtensionGatewayUiAssetPath(c.req.path)) {\n return;\n }\n c.header('X-Frame-Options', 'DENY');\n c.header('X-Content-Type-Options', 'nosniff');\n c.header('Referrer-Policy', 'strict-origin-when-cross-origin');\n c.header('X-XSS-Protection', '1; mode=block');\n // microphone=(self): allow same-origin chat voice (composer). microphone=() breaks packaged Electron loading the gateway SPA.\n c.header('Permissions-Policy', 'camera=(), microphone=(self), geolocation=()');\n c.header(\n 'Content-Security-Policy',\n \"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'\",\n );\n }));\n\n app.use('/api/skills/upload', bodyLimit({\n maxSize: 10 * 1024 * 1024,\n onError: (c) => {\n return c.json({ error: 'Skill package too large', maxSize: '10MB' }, 413);\n },\n }));\n\n const DEFAULT_API_BODY_MAX = 1 * 1024 * 1024;\n const WEBCHAT_AGENT_BODY_MAX = maxWebchatAgentRequestBodyBytes();\n\n app.use('/api/*', async (c, next) => {\n const maxSize = c.req.path === '/api/agent' ? WEBCHAT_AGENT_BODY_MAX : DEFAULT_API_BODY_MAX;\n const maxSizeMb = Math.ceil(maxSize / (1024 * 1024));\n return bodyLimit({\n maxSize,\n onError: (ctx) =>\n ctx.json({ error: 'Request body too large', maxSize: `${maxSizeMb}MB` }, 413),\n })(c, next);\n });\n\n registerPublicGatewayRoutes(app, service);\n\n // Extension UI assets are served without auth: sandboxed iframes (no allow-same-origin)\n // have an opaque origin of `null` and cannot forward the ?token= from the parent HTML URL.\n // Security is enforced by the strict CSP (frame-ancestors 'self') on every response.\n registerPublicExtensionAssetRoutes(app, service);\n\n const authenticated = new Hono();\n authenticated.use(\n auth({\n token,\n getGatewayAuth: () => service.currentConfig.gateway?.auth,\n }),\n );\n\n const strictRateLimiter = new Map<string, ReturnType<typeof createFixedWindowRateLimiter>>();\n\n const RATE_LIMIT_CLEANUP_INTERVAL = 5 * 60 * 1000;\n setInterval(() => {\n for (const [ip, limiter] of strictRateLimiter.entries()) {\n const result = limiter.consume();\n if (result.remaining === 9) {\n strictRateLimiter.delete(ip);\n }\n }\n }, RATE_LIMIT_CLEANUP_INTERVAL);\n\n const strictRateLimitMiddleware = createMiddleware(async (c, next) => {\n /*\n const clientIp = c.req.header('x-forwarded-for')?.split(',')[0]?.trim()\n ?? c.req.header('x-real-ip')\n ?? 'unknown';\n\n let limiter = strictRateLimiter.get(clientIp);\n if (!limiter) {\n limiter = createFixedWindowRateLimiter({ maxRequests: 10, windowMs: 60_000 });\n strictRateLimiter.set(clientIp, limiter);\n }\n\n const result = limiter.consume();\n if (!result.allowed) {\n c.header('Retry-After', String(Math.ceil(result.retryAfterMs / 1000)));\n return c.json({ error: 'Too many requests' }, 429);\n }\n\n c.header('X-RateLimit-Remaining', String(result.remaining));\n */\n await next();\n });\n\n const sseConfig = {\n service,\n maxSseConnections: service.currentConfig.gateway.maxSseConnections,\n };\n\n registerAuthenticatedRoutes(authenticated, {\n service,\n strictRateLimitMiddleware,\n sseConfig,\n });\n\n app.route('/', authenticated);\n\n app.notFound((c) => {\n return c.json({ error: 'Not found' }, 404);\n });\n\n app.onError((err, c) => {\n log.error({ err }, 'Hono error');\n return c.json({ error: 'Internal server error' }, 500);\n });\n\n return app;\n}\n"],"mappings":";;;;;;;;;;;;;;aAMqD;AAUrD,MAAM,MAAM,aAAa,UAAU;;;;;;AAYnC,SAAgB,8BAA8B,MAAuB;AACnE,QAAO,sCAAsC,KAAK,KAAK;;AAGzD,SAAgB,cAAc,QAA6B;CACzD,MAAM,EAAE,SAAS,UAAU;CAC3B,MAAM,MAAM,IAAI,MAAM;CAEtB,MAAM,cAAc,QAAQ,cAAc,QAAQ,QAAQ;CAC1D,MAAM,oBAAoB,QAAQ,cAAc,QAAQ;CAExD,IAAI;AACJ,KAAI,qBAAqB,kBAAkB,SAAS,EAClD,cAAa;KAEb,cAAa;EACX,oBAAoB;EACpB,oBAAoB;EACpB;EACA;EACD;CAGH,MAAM,eAAe;EACnB,QAAQ;EACR,cAAc;GAAC;GAAO;GAAQ;GAAS;GAAU;GAAU;EAC3D,cAAc;GAAC;GAAgB;GAAiB;GAAU;GAAgB;GAAgB;EAC1F,aAAa;EACb,QAAQ;EACT;AAED,KAAI,IAAI,sBAAsB,CAAC;AAC/B,KAAI,IAAI,QAAQ,CAAC;AACjB,KAAI,IAAI,KAAK,aAAa,CAAC;AAE3B,KAAI,IAAI,iBAAiB,OAAO,GAAG,SAAS;AAC1C,QAAM,MAAM;AACZ,MAAI,8BAA8B,EAAE,IAAI,KAAK,CAC3C;AAEF,IAAE,OAAO,mBAAmB,OAAO;AACnC,IAAE,OAAO,0BAA0B,UAAU;AAC7C,IAAE,OAAO,mBAAmB,kCAAkC;AAC9D,IAAE,OAAO,oBAAoB,gBAAgB;AAE7C,IAAE,OAAO,sBAAsB,+CAA+C;AAC9E,IAAE,OACA,2BACA,0KACD;GACD,CAAC;AAEH,KAAI,IAAI,sBAAsB,UAAU;EACtC,SAAS,KAAK,OAAO;EACrB,UAAU,MAAM;AACd,UAAO,EAAE,KAAK;IAAE,OAAO;IAA2B,SAAS;IAAQ,EAAE,IAAI;;EAE5E,CAAC,CAAC;CAEH,MAAM,uBAAuB,IAAI,OAAO;CACxC,MAAM,yBAAyB,iCAAiC;AAEhE,KAAI,IAAI,UAAU,OAAO,GAAG,SAAS;EACnC,MAAM,UAAU,EAAE,IAAI,SAAS,eAAe,yBAAyB;EACvE,MAAM,YAAY,KAAK,KAAK,WAAW,OAAO,MAAM;AACpD,SAAO,UAAU;GACf;GACA,UAAU,QACR,IAAI,KAAK;IAAE,OAAO;IAA0B,SAAS,GAAG,UAAU;IAAK,EAAE,IAAI;GAChF,CAAC,CAAC,GAAG,KAAK;GACX;AAEF,6BAA4B,KAAK,QAAQ;AAKzC,oCAAmC,KAAK,QAAQ;CAEhD,MAAM,gBAAgB,IAAI,MAAM;AAChC,eAAc,IACZ,KAAK;EACH;EACA,sBAAsB,QAAQ,cAAc,SAAS;EACtD,CAAC,CACH;CAED,MAAM,oCAAoB,IAAI,KAA8D;AAG5F,mBAAkB;AAChB,OAAK,MAAM,CAAC,IAAI,YAAY,kBAAkB,SAAS,CAErD,KADe,QAAQ,SACb,CAAC,cAAc,EACvB,mBAAkB,OAAO,GAAG;IALE,MAAS,IAQd;AA8B/B,6BAA4B,eAAe;EACzC;EACA,2BA9BgC,iBAAiB,OAAO,GAAG,SAAS;AAoBpE,SAAM,MAAM;IAUa;EACzB,WAAA;GAPA;GACA,mBAAmB,QAAQ,cAAc,QAAQ;GAMxC;EACV,CAAC;AAEF,KAAI,MAAM,KAAK,cAAc;AAE7B,KAAI,UAAU,MAAM;AAClB,SAAO,EAAE,KAAK,EAAE,OAAO,aAAa,EAAE,IAAI;GAC1C;AAEF,KAAI,SAAS,KAAK,MAAM;AACtB,MAAI,MAAM,EAAE,KAAK,EAAE,aAAa;AAChC,SAAO,EAAE,KAAK,EAAE,OAAO,yBAAyB,EAAE,IAAI;GACtD;AAEF,QAAO"}
1
+ {"version":3,"file":"app.js","names":[],"sources":["../../../../src/gateway/hono/app.ts"],"sourcesContent":["import { Hono } from 'hono';\nimport { cors } from 'hono/cors';\nimport { createMiddleware } from 'hono/factory';\nimport { bodyLimit } from 'hono/body-limit';\n\nimport { createFixedWindowRateLimiter } from '../../infra/rate-limit.js';\nimport { createLogger } from '../../utils/logger.js';\nimport type { GatewayService } from '../service.js';\nimport { maxWebchatAgentRequestBodyBytes } from '../chat-limits.js';\nimport { buildGatewayConsoleCspHeader } from '../security/csp.js';\nimport { checkBrowserOrigin } from '../security/origin-check.js';\nimport { auth } from './middleware/auth.js';\nimport { operatorScopes } from './middleware/scopes.js';\nimport { logContextMiddleware } from './middleware/log-context.js';\nimport { logger } from './middleware/logger.js';\nimport { registerPublicExtensionAssetRoutes } from './routes/auth-registry-extensions.js';\nimport { registerAuthenticatedRoutes } from './routes/index.js';\nimport { registerPublicGatewayRoutes } from './routes/public-gateway.js';\n\nconst log = createLogger('HonoApp');\n\nexport interface HonoAppConfig {\n service: GatewayService;\n token?: string;\n}\n\n/**\n * Extension sandbox HTML under `/api/extensions/:id/assets/*` ships its own CSP\n * (`frame-ancestors 'self'`). The global gateway middleware must not overwrite it\n * with `frame-ancestors 'none'` / `X-Frame-Options: DENY`, or the console cannot embed iframes.\n */\nexport function isExtensionGatewayUiAssetPath(path: string): boolean {\n return /^\\/api\\/extensions\\/[^/]+\\/assets\\//.test(path);\n}\n\nexport function createHonoApp(config: HonoAppConfig): Hono {\n const { service, token } = config;\n const app = new Hono();\n\n const gatewayPort = service.currentConfig.gateway.port ?? 18790;\n const configuredOrigins = service.currentConfig.gateway.corsOrigins;\n\n let corsOrigin: string | string[];\n if (configuredOrigins && configuredOrigins.length > 0) {\n corsOrigin = configuredOrigins;\n } else {\n corsOrigin = [\n `http://localhost:${gatewayPort}`,\n `http://127.0.0.1:${gatewayPort}`,\n 'http://localhost:3000',\n 'http://127.0.0.1:3000',\n ];\n }\n\n const CORS_OPTIONS = {\n origin: corsOrigin,\n allowMethods: ['GET', 'POST', 'PATCH', 'DELETE', 'OPTIONS'],\n allowHeaders: ['Content-Type', 'Authorization', 'Accept', 'X-Session-Id', 'Last-Event-ID'],\n credentials: true,\n maxAge: 86400,\n };\n\n app.use(logContextMiddleware());\n app.use(logger());\n app.use(cors(CORS_OPTIONS));\n\n // Build CSP header once at startup (no inline script hashes needed for SPA)\n const gatewayConsoleCsp = buildGatewayConsoleCspHeader();\n\n // Security headers middleware\n app.use(createMiddleware(async (c, next) => {\n await next();\n if (isExtensionGatewayUiAssetPath(c.req.path)) {\n return;\n }\n c.header('X-Frame-Options', 'DENY');\n c.header('X-Content-Type-Options', 'nosniff');\n c.header('Referrer-Policy', 'strict-origin-when-cross-origin');\n c.header('X-XSS-Protection', '1; mode=block');\n // microphone=(self): allow same-origin chat voice (composer). microphone=() breaks packaged Electron loading the gateway SPA.\n c.header('Permissions-Policy', 'camera=(), microphone=(self), geolocation=()');\n c.header('Content-Security-Policy', gatewayConsoleCsp);\n }));\n\n // Browser Origin check middleware for API routes (CSRF protection).\n // Non-browser requests (no Origin header) pass through — they are\n // authenticated by the token middleware instead.\n const allowedOrigins = Array.isArray(corsOrigin) ? corsOrigin : [corsOrigin];\n app.use('/api/*', createMiddleware(async (c, next) => {\n // Sandboxed extension iframes (no allow-same-origin) send `Origin: null`.\n // `checkBrowserOrigin` rejects that; these routes rely on CSP instead\n // (`registerPublicExtensionAssetRoutes`).\n if (isExtensionGatewayUiAssetPath(c.req.path)) {\n return next();\n }\n\n const origin = c.req.header('origin');\n if (!origin) {\n // Non-browser request (CLI, server-to-server) — skip origin check\n return next();\n }\n\n const result = checkBrowserOrigin({\n requestHost: c.req.header('host'),\n origin,\n allowedOrigins,\n allowHostHeaderOriginFallback: true,\n isLocalClient: false,\n });\n\n if (!result.ok) {\n log.warn(\n { origin, reason: 'reason' in result ? result.reason : 'unknown', path: c.req.path },\n 'Browser origin check failed',\n );\n return c.json({ error: 'Forbidden', message: 'Origin not allowed' }, 403);\n }\n\n return next();\n }));\n\n app.use('/api/skills/upload', bodyLimit({\n maxSize: 10 * 1024 * 1024,\n onError: (c) => {\n return c.json({ error: 'Skill package too large', maxSize: '10MB' }, 413);\n },\n }));\n\n const DEFAULT_API_BODY_MAX = 1 * 1024 * 1024;\n const WEBCHAT_AGENT_BODY_MAX = maxWebchatAgentRequestBodyBytes();\n\n app.use('/api/*', async (c, next) => {\n const maxSize = c.req.path === '/api/agent' ? WEBCHAT_AGENT_BODY_MAX : DEFAULT_API_BODY_MAX;\n const maxSizeMb = Math.ceil(maxSize / (1024 * 1024));\n return bodyLimit({\n maxSize,\n onError: (ctx) =>\n ctx.json({ error: 'Request body too large', maxSize: `${maxSizeMb}MB` }, 413),\n })(c, next);\n });\n\n registerPublicGatewayRoutes(app, service);\n\n // Extension UI assets are served without auth: sandboxed iframes (no allow-same-origin)\n // have an opaque origin of `null` and cannot forward the ?token= from the parent HTML URL.\n // Security is enforced by the strict CSP (frame-ancestors 'self') on every response.\n registerPublicExtensionAssetRoutes(app, service);\n\n const authenticated = new Hono();\n authenticated.use(\n auth({\n token,\n getGatewayAuth: () => service.currentConfig.gateway?.auth,\n }),\n );\n authenticated.use(operatorScopes());\n\n const strictRateLimiter = new Map<string, ReturnType<typeof createFixedWindowRateLimiter>>();\n\n const RATE_LIMIT_CLEANUP_INTERVAL = 5 * 60 * 1000;\n setInterval(() => {\n for (const [ip, limiter] of strictRateLimiter.entries()) {\n const result = limiter.consume();\n if (result.remaining === 9) {\n strictRateLimiter.delete(ip);\n }\n }\n }, RATE_LIMIT_CLEANUP_INTERVAL);\n\n const strictRateLimitMiddleware = createMiddleware(async (c, next) => {\n /*\n const clientIp = c.req.header('x-forwarded-for')?.split(',')[0]?.trim()\n ?? c.req.header('x-real-ip')\n ?? 'unknown';\n\n let limiter = strictRateLimiter.get(clientIp);\n if (!limiter) {\n limiter = createFixedWindowRateLimiter({ maxRequests: 10, windowMs: 60_000 });\n strictRateLimiter.set(clientIp, limiter);\n }\n\n const result = limiter.consume();\n if (!result.allowed) {\n c.header('Retry-After', String(Math.ceil(result.retryAfterMs / 1000)));\n return c.json({ error: 'Too many requests' }, 429);\n }\n\n c.header('X-RateLimit-Remaining', String(result.remaining));\n */\n await next();\n });\n\n const sseConfig = {\n service,\n maxSseConnections: service.currentConfig.gateway.maxSseConnections,\n };\n\n registerAuthenticatedRoutes(authenticated, {\n service,\n strictRateLimitMiddleware,\n sseConfig,\n });\n\n app.route('/', authenticated);\n\n app.notFound((c) => {\n return c.json({ error: 'Not found' }, 404);\n });\n\n app.onError((err, c) => {\n log.error({ err }, 'Hono error');\n return c.json({ error: 'Internal server error' }, 500);\n });\n\n return app;\n}\n"],"mappings":"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"}
@@ -124,7 +124,7 @@ export declare function buildSafeWebConfigPayload(service: GatewayService): Prom
124
124
  host: string;
125
125
  port: number;
126
126
  auth: {
127
- mode: "none" | "token";
127
+ mode: "none" | "token" | "password";
128
128
  token: string;
129
129
  };
130
130
  heartbeat: {
@@ -1,16 +1,17 @@
1
1
  import { createLogger } from "../../../utils/logger/index.js";
2
2
  import { init_logger } from "../../../utils/logger.js";
3
+ import { safeEqualSecret } from "../../security/secret-equal.js";
3
4
  import { getAuthFailureRateLimiter, getClientIpFromHeaders, isAuthRateLimitGloballyDisabled, resolveAuthRateLimitConfig } from "../../auth-rate-limit.js";
4
5
  import { createMiddleware } from "hono/factory";
5
6
  //#region src/gateway/hono/middleware/auth.ts
6
7
  init_logger();
7
8
  const log = createLogger("Hono:Auth");
8
9
  /**
9
- * Validate token from header or query parameter
10
+ * Validate token using constant-time comparison to prevent timing attacks.
10
11
  */
11
12
  function validateToken(providedToken, expectedToken) {
12
13
  if (!providedToken) return false;
13
- return providedToken === expectedToken;
14
+ return safeEqualSecret(providedToken, expectedToken);
14
15
  }
15
16
  /**
16
17
  * Extract token from Authorization header
@@ -106,7 +107,7 @@ function validateWebSocketAuth(url, authHeader, expectedToken) {
106
107
  error: "Missing authentication token"
107
108
  };
108
109
  }
109
- if (!validateToken(providedToken, expectedToken)) {
110
+ if (!safeEqualSecret(providedToken, expectedToken)) {
110
111
  log.warn({
111
112
  path: url.pathname,
112
113
  reason: "invalid_token"