@stage-labs/metro 0.1.0-beta.4 → 0.1.0-beta.41

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 (249) hide show
  1. package/dist/api.js +131 -0
  2. package/dist/bedrock-eventstream.js +120 -0
  3. package/dist/bedrock-proxy.js +260 -0
  4. package/dist/bedrock-upstream.js +72 -0
  5. package/dist/bedrock.js +106 -0
  6. package/dist/claude.js +15 -0
  7. package/dist/cli.js +181 -207
  8. package/dist/control.js +173 -0
  9. package/dist/plugin.js +82 -0
  10. package/dist/prompt.js +61 -0
  11. package/dist/runtime.js +117 -0
  12. package/dist/serve.js +77 -0
  13. package/dist/store.js +93 -0
  14. package/dist/tail.js +120 -0
  15. package/dist/update.js +33 -0
  16. package/dist/version.js +97 -0
  17. package/package.json +34 -21
  18. package/runtime/node_modules/@metro-labs/discord-bot/package.json +28 -0
  19. package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +119 -0
  20. package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +388 -0
  21. package/runtime/node_modules/@metro-labs/discord-bot/src/api-base.ts +1 -0
  22. package/runtime/node_modules/@metro-labs/discord-bot/src/attachments.ts +29 -0
  23. package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +210 -0
  24. package/runtime/node_modules/@metro-labs/discord-bot/src/group-actions.ts +50 -0
  25. package/runtime/node_modules/@metro-labs/discord-bot/src/group.ts +115 -0
  26. package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +127 -0
  27. package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +215 -0
  28. package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +43 -0
  29. package/runtime/node_modules/@metro-labs/discord-bot/src/station.ts +19 -0
  30. package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +82 -0
  31. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-speak.ts +195 -0
  32. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-transcribe.ts +292 -0
  33. package/runtime/node_modules/@metro-labs/discord-bot/src/voice.ts +197 -0
  34. package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +5 -0
  35. package/runtime/node_modules/@metro-labs/mcp/package.json +67 -0
  36. package/runtime/node_modules/@metro-labs/mcp/src/channels/dedupe.ts +24 -0
  37. package/runtime/node_modules/@metro-labs/mcp/src/channels/inbound.ts +367 -0
  38. package/runtime/node_modules/@metro-labs/mcp/src/channels/media-note.ts +85 -0
  39. package/runtime/node_modules/@metro-labs/mcp/src/channels/pending.ts +75 -0
  40. package/runtime/node_modules/@metro-labs/mcp/src/channels/relay.ts +129 -0
  41. package/runtime/node_modules/@metro-labs/mcp/src/channels/webhook-note.ts +51 -0
  42. package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +311 -0
  43. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +334 -0
  44. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-connector-api.ts +150 -0
  45. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-import.ts +76 -0
  46. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-pair.ts +20 -0
  47. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-error.ts +8 -0
  48. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +153 -0
  49. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-grant.ts +99 -0
  50. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-owner.ts +32 -0
  51. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +133 -0
  52. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +278 -0
  53. package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +383 -0
  54. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +143 -0
  55. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connect-hint.ts +33 -0
  56. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +315 -0
  57. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-json.ts +21 -0
  58. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth-routes.ts +157 -0
  59. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +127 -0
  60. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-tools.ts +100 -0
  61. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-url.ts +57 -0
  62. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +333 -0
  63. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cors.ts +29 -0
  64. package/runtime/node_modules/@metro-labs/mcp/src/daemon/crash-guard.ts +49 -0
  65. package/runtime/node_modules/@metro-labs/mcp/src/daemon/events.ts +117 -0
  66. package/runtime/node_modules/@metro-labs/mcp/src/daemon/hosted-connectors.ts +118 -0
  67. package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +369 -0
  68. package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +86 -0
  69. package/runtime/node_modules/@metro-labs/mcp/src/daemon/import-api.ts +67 -0
  70. package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-connectors-api.ts +56 -0
  71. package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-mode.ts +187 -0
  72. package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-secret.ts +21 -0
  73. package/runtime/node_modules/@metro-labs/mcp/src/daemon/log.ts +42 -0
  74. package/runtime/node_modules/@metro-labs/mcp/src/daemon/mode-api.ts +31 -0
  75. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-client.ts +162 -0
  76. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-discovery.ts +120 -0
  77. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-pending.ts +65 -0
  78. package/runtime/node_modules/@metro-labs/mcp/src/daemon/pair-codes.ts +58 -0
  79. package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +145 -0
  80. package/runtime/node_modules/@metro-labs/mcp/src/daemon/project-api.ts +214 -0
  81. package/runtime/node_modules/@metro-labs/mcp/src/daemon/protocol.ts +224 -0
  82. package/runtime/node_modules/@metro-labs/mcp/src/daemon/relay.ts +303 -0
  83. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +139 -0
  84. package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-source.ts +105 -0
  85. package/runtime/node_modules/@metro-labs/mcp/src/daemon/secure-fs.ts +57 -0
  86. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-api.ts +27 -0
  87. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +63 -0
  88. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-config.ts +27 -0
  89. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +218 -0
  90. package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-auth.ts +82 -0
  91. package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-nonces.ts +43 -0
  92. package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-routes.ts +89 -0
  93. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor-io.ts +97 -0
  94. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor.ts +320 -0
  95. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-call.ts +22 -0
  96. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-error.ts +53 -0
  97. package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +257 -0
  98. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +295 -0
  99. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-store.ts +181 -0
  100. package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +91 -0
  101. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +214 -0
  102. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-connectors.ts +143 -0
  103. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-map.ts +84 -0
  104. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-scope.ts +64 -0
  105. package/runtime/node_modules/@metro-labs/mcp/src/db/client.ts +37 -0
  106. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-config.ts +147 -0
  107. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-relay.ts +126 -0
  108. package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +326 -0
  109. package/runtime/node_modules/@metro-labs/mcp/src/db/file-admin.ts +319 -0
  110. package/runtime/node_modules/@metro-labs/mcp/src/db/file-source.ts +152 -0
  111. package/runtime/node_modules/@metro-labs/mcp/src/db/ids.ts +21 -0
  112. package/runtime/node_modules/@metro-labs/mcp/src/db/key-map.ts +45 -0
  113. package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +373 -0
  114. package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +309 -0
  115. package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +24 -0
  116. package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +105 -0
  117. package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +118 -0
  118. package/runtime/node_modules/@metro-labs/mcp/src/db/token-carry.ts +45 -0
  119. package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +72 -0
  120. package/runtime/node_modules/@metro-labs/mcp/src/db/whatsapp-login.ts +18 -0
  121. package/runtime/node_modules/@metro-labs/mcp/src/mcp/accounts.ts +141 -0
  122. package/runtime/node_modules/@metro-labs/mcp/src/mcp/call-tools.ts +203 -0
  123. package/runtime/node_modules/@metro-labs/mcp/src/mcp/channel-owner.ts +29 -0
  124. package/runtime/node_modules/@metro-labs/mcp/src/mcp/ctx.ts +62 -0
  125. package/runtime/node_modules/@metro-labs/mcp/src/mcp/event-store.ts +91 -0
  126. package/runtime/node_modules/@metro-labs/mcp/src/mcp/frame-scope.ts +21 -0
  127. package/runtime/node_modules/@metro-labs/mcp/src/mcp/group-tools.ts +129 -0
  128. package/runtime/node_modules/@metro-labs/mcp/src/mcp/index.ts +192 -0
  129. package/runtime/node_modules/@metro-labs/mcp/src/mcp/member-tools.ts +72 -0
  130. package/runtime/node_modules/@metro-labs/mcp/src/mcp/permission-relay.ts +66 -0
  131. package/runtime/node_modules/@metro-labs/mcp/src/mcp/raw-get-stream.ts +207 -0
  132. package/runtime/node_modules/@metro-labs/mcp/src/mcp/request-identity.ts +73 -0
  133. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-registry.ts +172 -0
  134. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-route.ts +43 -0
  135. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session.ts +211 -0
  136. package/runtime/node_modules/@metro-labs/mcp/src/mcp/str.ts +5 -0
  137. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-dispatch.ts +212 -0
  138. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +397 -0
  139. package/runtime/node_modules/@metro-labs/mcp/src/mcp/upload-tool.ts +66 -0
  140. package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +240 -0
  141. package/runtime/node_modules/@metro-labs/mcp/src/server.ts +2 -0
  142. package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +80 -0
  143. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-inline.ts +100 -0
  144. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-interactive.ts +161 -0
  145. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-resolve.ts +200 -0
  146. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-xmtp.ts +111 -0
  147. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +182 -0
  148. package/runtime/node_modules/@metro-labs/mcp/src/stations/attachments.ts +196 -0
  149. package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +82 -0
  150. package/runtime/node_modules/@metro-labs/mcp/src/stations/messaging-normalize.ts +88 -0
  151. package/runtime/node_modules/@metro-labs/mcp/src/stations/registry.ts +35 -0
  152. package/runtime/node_modules/@metro-labs/mcp/src/stations/station-runtime.ts +64 -0
  153. package/runtime/node_modules/@metro-labs/mcp/src/stations/types.ts +107 -0
  154. package/runtime/node_modules/@metro-labs/telegram/package.json +28 -0
  155. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +81 -0
  156. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +254 -0
  157. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +87 -0
  158. package/runtime/node_modules/@metro-labs/telegram/src/client.ts +77 -0
  159. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +141 -0
  160. package/runtime/node_modules/@metro-labs/telegram/src/group.ts +261 -0
  161. package/runtime/node_modules/@metro-labs/telegram/src/history.ts +52 -0
  162. package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +78 -0
  163. package/runtime/node_modules/@metro-labs/telegram/src/index.ts +46 -0
  164. package/runtime/node_modules/@metro-labs/telegram/src/login.ts +166 -0
  165. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +80 -0
  166. package/runtime/node_modules/@metro-labs/telegram/src/members.ts +75 -0
  167. package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +22 -0
  168. package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +167 -0
  169. package/runtime/node_modules/@metro-labs/telegram/src/station.ts +24 -0
  170. package/runtime/node_modules/@metro-labs/telegram/src/types.ts +7 -0
  171. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +1 -0
  172. package/runtime/node_modules/@metro-labs/telegram-bot/package.json +25 -0
  173. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +120 -0
  174. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +295 -0
  175. package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
  176. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +172 -0
  177. package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +103 -0
  178. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +166 -0
  179. package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
  180. package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +17 -0
  181. package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +48 -0
  182. package/runtime/node_modules/@metro-labs/telegram-bot/src/verify.ts +38 -0
  183. package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +3 -0
  184. package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
  185. package/runtime/node_modules/@metro-labs/webhook/src/station.ts +40 -0
  186. package/runtime/node_modules/@metro-labs/whatsapp/package.json +29 -0
  187. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +67 -0
  188. package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
  189. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +178 -0
  190. package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +72 -0
  191. package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +129 -0
  192. package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +318 -0
  193. package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
  194. package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +132 -0
  195. package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +57 -0
  196. package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +40 -0
  197. package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +59 -0
  198. package/runtime/node_modules/@metro-labs/whatsapp/src/logger.ts +60 -0
  199. package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +176 -0
  200. package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +84 -0
  201. package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +22 -0
  202. package/runtime/node_modules/@metro-labs/whatsapp/src/outbox.ts +38 -0
  203. package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +141 -0
  204. package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +17 -0
  205. package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +98 -0
  206. package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +6 -0
  207. package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +5 -0
  208. package/runtime/node_modules/@metro-labs/xmtp/package.json +33 -0
  209. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +113 -0
  210. package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +110 -0
  211. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +299 -0
  212. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +157 -0
  213. package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +87 -0
  214. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +375 -0
  215. package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +107 -0
  216. package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +171 -0
  217. package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +239 -0
  218. package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +55 -0
  219. package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +333 -0
  220. package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +78 -0
  221. package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +34 -0
  222. package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +56 -0
  223. package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +33 -0
  224. package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +125 -0
  225. package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +157 -0
  226. package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
  227. package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +349 -0
  228. package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +13 -0
  229. package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +126 -0
  230. package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +212 -0
  231. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +90 -0
  232. package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +84 -0
  233. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +39 -0
  234. package/runtime/runtime.json +3 -0
  235. package/runtime/trains/.keep +0 -0
  236. package/LICENSE +0 -21
  237. package/README.md +0 -117
  238. package/dist/agents/claude.js +0 -207
  239. package/dist/agents/codex.js +0 -207
  240. package/dist/agents/types.js +0 -2
  241. package/dist/channels/discord.js +0 -104
  242. package/dist/channels/telegram.js +0 -189
  243. package/dist/helpers/scope-cache.js +0 -65
  244. package/dist/helpers/streaming.js +0 -209
  245. package/dist/helpers/telegram-format.js +0 -39
  246. package/dist/helpers/turn.js +0 -40
  247. package/dist/log.js +0 -10
  248. package/dist/orchestrator.js +0 -208
  249. package/dist/paths.js +0 -68
@@ -0,0 +1,153 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { ApiError } from './api-error.js';
3
+ import { errMsg, log } from './log.js';
4
+ import {
5
+ verifyAgentToken,
6
+ verifyRunToken,
7
+ verifySession,
8
+ type AgentClaims,
9
+ type RunClaims,
10
+ } from './session.js';
11
+ import { extractToken } from '../mcp/request-identity.js';
12
+
13
+ const BODY_MAX = 4 * 1024;
14
+ const DRAIN_MAX = 2 * BODY_MAX;
15
+
16
+ export interface ApiSession {
17
+ subject: string;
18
+ }
19
+
20
+ export function cors(req: IncomingMessage): Record<string, string> {
21
+ return {
22
+ 'access-control-allow-origin': req.headers.origin ?? '*',
23
+ 'access-control-allow-methods': 'GET, POST, PUT, DELETE, OPTIONS',
24
+ 'access-control-allow-headers': 'Authorization, Content-Type',
25
+ 'access-control-allow-private-network': 'true',
26
+ 'access-control-max-age': '86400',
27
+ vary: 'Origin',
28
+ };
29
+ }
30
+
31
+ export function sendJson(
32
+ req: IncomingMessage,
33
+ res: ServerResponse,
34
+ status: number,
35
+ body: unknown,
36
+ ): void {
37
+ res.writeHead(status, {
38
+ 'content-type': 'application/json',
39
+ 'cache-control': 'no-store',
40
+ ...cors(req),
41
+ });
42
+ res.end(JSON.stringify(body));
43
+ }
44
+
45
+ export function apiSession(req: IncomingMessage): ApiSession | null {
46
+ const secret = process.env.METRO_SESSION_SECRET?.trim() ?? '';
47
+ if (secret === '') return null;
48
+ const token = extractToken(req) ?? '';
49
+ if (token === '') return null;
50
+ try {
51
+ const { subject } = verifySession(token, secret);
52
+ return { subject: subject.toLowerCase() };
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ export function runIdentity(req: IncomingMessage): RunClaims | null {
59
+ const secret = process.env.METRO_SESSION_SECRET?.trim() ?? '';
60
+ if (secret === '') return null;
61
+ const token = extractToken(req) ?? '';
62
+ if (token === '') return null;
63
+ try {
64
+ const claims = verifyRunToken(token, secret);
65
+ return { ...claims, subject: claims.subject.toLowerCase() };
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+
71
+ export interface AgentIdentity extends AgentClaims {
72
+ runtimeId?: string;
73
+ }
74
+
75
+ export function agentIdentity(req: IncomingMessage): AgentIdentity | null {
76
+ const secret = process.env.METRO_SESSION_SECRET?.trim() ?? '';
77
+ if (secret === '') return null;
78
+ const token = extractToken(req) ?? '';
79
+ if (token === '') return null;
80
+ try {
81
+ const { subject, agentId } = verifyAgentToken(token, secret);
82
+ return { subject: subject.toLowerCase(), agentId };
83
+ } catch {
84
+ const run = runIdentity(req);
85
+ if (run === null) return null;
86
+ return { subject: run.subject, agentId: run.agentId, runtimeId: run.runtimeId };
87
+ }
88
+ }
89
+
90
+ export type Fence = (runtimeId: string, agentId: string) => Promise<void>;
91
+
92
+ const RELEASED = 'this machine no longer holds the agent';
93
+
94
+ export async function assertLease(
95
+ who: AgentIdentity,
96
+ fence: Fence,
97
+ ): Promise<void> {
98
+ if (who.runtimeId === undefined) return;
99
+ try {
100
+ await fence(who.runtimeId, who.agentId);
101
+ } catch (err) {
102
+ if (err instanceof ApiError) throw new ApiError(RELEASED, 401);
103
+ throw err;
104
+ }
105
+ }
106
+
107
+ export function projectParam(req: IncomingMessage): string | null {
108
+ const raw = new URL(req.url ?? '/', 'http://localhost').searchParams.get(
109
+ 'project',
110
+ );
111
+ return raw === null || raw === '' ? null : raw;
112
+ }
113
+
114
+ export async function readJsonBody(req: IncomingMessage): Promise<unknown> {
115
+ const chunks: Buffer[] = [];
116
+ let total = 0;
117
+ let over = false;
118
+ for await (const c of req) {
119
+ const buf = c as Buffer;
120
+ total += buf.length;
121
+ if (total > BODY_MAX) over = true;
122
+ if (!over) chunks.push(buf);
123
+ else if (total > DRAIN_MAX) break;
124
+ }
125
+ if (over) throw new ApiError('request body too large', 413);
126
+ const raw = Buffer.concat(chunks).toString('utf8').trim();
127
+ if (raw === '') return {};
128
+ try {
129
+ return JSON.parse(raw);
130
+ } catch {
131
+ throw new ApiError('body must be JSON', 400);
132
+ }
133
+ }
134
+
135
+ export function apiFailure(
136
+ req: IncomingMessage,
137
+ res: ServerResponse,
138
+ err: unknown,
139
+ label = 'agent-api',
140
+ ): void {
141
+ if (err instanceof ApiError) {
142
+ sendJson(req, res, err.status, { error: err.message });
143
+ return;
144
+ }
145
+ log.warn({ err: errMsg(err) }, `${label}: request failed`);
146
+ if (!res.headersSent) sendJson(req, res, 500, { error: `${label} failed` });
147
+ }
148
+
149
+ export function bodyField(body: unknown, key: string): unknown {
150
+ return typeof body === 'object' && body !== null && !Array.isArray(body)
151
+ ? (body as Record<string, unknown>)[key]
152
+ : undefined;
153
+ }
@@ -0,0 +1,99 @@
1
+ import { randomBytes, timingSafeEqual } from 'node:crypto';
2
+ import { ID_RE } from '../db/ids.js';
3
+ import { readFileSync } from 'node:fs';
4
+ import { resolveCachedAttachment } from '../stations/attachments.js';
5
+ import { writeSecure } from './secure-fs.js';
6
+
7
+ const GRANT_SUFFIX = '.grant';
8
+
9
+ export interface AttachmentGrant {
10
+ token: string;
11
+ agentId: string;
12
+ mintedAt: number;
13
+ }
14
+
15
+ function newAttachmentToken(prefix = 'at'): string {
16
+ return `${prefix}_${randomBytes(32).toString('base64url')}`;
17
+ }
18
+
19
+ function parseGrant(raw: string): AttachmentGrant | undefined {
20
+ let parsed: unknown;
21
+ try {
22
+ parsed = JSON.parse(raw);
23
+ } catch {
24
+ return undefined;
25
+ }
26
+ if (typeof parsed !== 'object' || parsed === null) return undefined;
27
+ const { token, agentId, mintedAt } = parsed as Record<string, unknown>;
28
+ if (typeof token !== 'string' || token === '') return undefined;
29
+ if (typeof agentId !== 'string' || !ID_RE.test(agentId)) return undefined;
30
+ return {
31
+ token,
32
+ agentId,
33
+ mintedAt: typeof mintedAt === 'number' ? mintedAt : 0,
34
+ };
35
+ }
36
+
37
+ export function readGrant(path: string): AttachmentGrant | undefined {
38
+ try {
39
+ return parseGrant(readFileSync(`${path}${GRANT_SUFFIX}`, 'utf8'));
40
+ } catch {
41
+ return undefined;
42
+ }
43
+ }
44
+
45
+ export function issueGrant(
46
+ path: string,
47
+ agentId: string,
48
+ prefix?: string,
49
+ ): string {
50
+ const existing = readGrant(path);
51
+ if (existing?.agentId === agentId) return existing.token;
52
+ const grant: AttachmentGrant = {
53
+ token: newAttachmentToken(prefix),
54
+ agentId,
55
+ mintedAt: Date.now(),
56
+ };
57
+ writeSecure(`${path}${GRANT_SUFFIX}`, `${JSON.stringify(grant)}\n`);
58
+ return grant.token;
59
+ }
60
+
61
+ function sameToken(presented: string, stored: string): boolean {
62
+ const a = Buffer.from(presented, 'utf8');
63
+ const b = Buffer.from(stored, 'utf8');
64
+ return a.length === b.length && timingSafeEqual(a, b);
65
+ }
66
+
67
+ export function grantAllowsPath(
68
+ path: string,
69
+ owner: string,
70
+ presented: string,
71
+ ): boolean {
72
+ if (presented === '') return false;
73
+ const grant = readGrant(path);
74
+ if (grant === undefined) return false;
75
+ if (grant.agentId !== owner) return false;
76
+ return sameToken(presented, grant.token);
77
+ }
78
+
79
+ export function readAttachmentGrant(name: string): AttachmentGrant | undefined {
80
+ const path = resolveCachedAttachment(name);
81
+ return path === null ? undefined : readGrant(path);
82
+ }
83
+
84
+ export function issueAttachmentGrant(
85
+ name: string,
86
+ agentId: string,
87
+ ): string | undefined {
88
+ const path = resolveCachedAttachment(name);
89
+ return path === null ? undefined : issueGrant(path, agentId);
90
+ }
91
+
92
+ export function grantAllows(
93
+ name: string,
94
+ owner: string,
95
+ presented: string,
96
+ ): boolean {
97
+ const path = resolveCachedAttachment(name);
98
+ return path === null ? false : grantAllowsPath(path, owner, presented);
99
+ }
@@ -0,0 +1,32 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { ID_RE } from '../db/ids.js';
3
+ import { resolveCachedAttachment } from '../stations/attachments.js';
4
+ import { writeSecure } from './secure-fs.js';
5
+
6
+ const OWNER_SUFFIX = '.owner';
7
+
8
+ export function recordOwner(path: string, agentId: string): void {
9
+ writeSecure(`${path}${OWNER_SUFFIX}`, `${agentId}\n`);
10
+ }
11
+
12
+ export function ownerOf(path: string): string | undefined {
13
+ let raw: string;
14
+ try {
15
+ raw = readFileSync(`${path}${OWNER_SUFFIX}`, 'utf8');
16
+ } catch {
17
+ return undefined;
18
+ }
19
+ const agentId = raw.trim();
20
+ return ID_RE.test(agentId) ? agentId : undefined;
21
+ }
22
+
23
+ export function recordAttachmentOwner(name: string, agentId: string): void {
24
+ const path = resolveCachedAttachment(name);
25
+ if (path === null) return;
26
+ recordOwner(path, agentId);
27
+ }
28
+
29
+ export function attachmentOwner(name: string): string | undefined {
30
+ const path = resolveCachedAttachment(name);
31
+ return path === null ? undefined : ownerOf(path);
32
+ }
@@ -0,0 +1,133 @@
1
+ import { createReadStream } from 'node:fs';
2
+ import { stat } from 'node:fs/promises';
3
+ import type { IncomingMessage, ServerResponse } from 'node:http';
4
+ import {
5
+ guessMime,
6
+ resolveCachedAttachment,
7
+ } from '../stations/attachments.js';
8
+ import {
9
+ allowedAgents,
10
+ authConfigFromEnv,
11
+ authenticate,
12
+ extractToken,
13
+ } from '../mcp/request-identity.js';
14
+ import { attachmentOwner, recordAttachmentOwner } from './attach-owner.js';
15
+ import { grantAllows, issueAttachmentGrant } from './attach-grant.js';
16
+ import { errMsg, log } from './log.js';
17
+ import { configuredTunnelHost, currentTunnelUrl, webhookPort } from './tunnel.js';
18
+ import { isLocalMode } from './paths.js';
19
+
20
+ function authorized(req: IncomingMessage, name: string): boolean {
21
+ const owner = attachmentOwner(name);
22
+ if (owner === undefined) return false;
23
+ if (grantAllows(name, owner, extractToken(req) ?? '')) return true;
24
+ return allowedAgents(authenticate(req, authConfigFromEnv()) ?? undefined).has(
25
+ owner,
26
+ );
27
+ }
28
+
29
+ const DEFAULT_PUBLIC_BASE = 'https://mcp.metro.box';
30
+
31
+ export function publicBaseUrl(): string | null {
32
+ const env = process.env.METRO_PUBLIC_URL?.trim();
33
+ if (env) return env.replace(/\/+$/, '');
34
+ const live = currentTunnelUrl();
35
+ if (live !== null) return live;
36
+ const host = configuredTunnelHost();
37
+ return host ? `https://${host}` : null;
38
+ }
39
+
40
+ const servesLocally = (): boolean =>
41
+ (process.env.METRO_RUN_TOKEN?.trim() ?? '') !== '' || isLocalMode();
42
+
43
+ const localBase = (): string | null =>
44
+ servesLocally() ? `http://127.0.0.1:${String(webhookPort())}` : null;
45
+
46
+ export const publicBaseOrDefault = (): string =>
47
+ publicBaseUrl() ?? localBase() ?? DEFAULT_PUBLIC_BASE;
48
+
49
+ export function attachmentUrl(
50
+ pathOrName: string,
51
+ agentId: string,
52
+ ): string | null {
53
+ const base = publicBaseUrl() ?? localBase();
54
+ if (!base) return null;
55
+ const name = pathOrName.split('/').pop();
56
+ if (!name || !resolveCachedAttachment(name)) return null;
57
+ recordAttachmentOwner(name, agentId);
58
+ const token = issueAttachmentGrant(name, agentId);
59
+ if (token === undefined) return null;
60
+ return `${base}/attach/${encodeURIComponent(name)}?token=${encodeURIComponent(token)}`;
61
+ }
62
+
63
+ export function attachmentEventUrl(
64
+ payload: Record<string, unknown>,
65
+ agentId: string,
66
+ ): string | null {
67
+ if (payload.contentType !== 'attachmentSaved') return null;
68
+ if (typeof payload.url === 'string' && payload.url.length > 0) return null;
69
+ const p = payload.attachmentPath ?? payload.localPath;
70
+ return typeof p === 'string' ? attachmentUrl(p, agentId) : null;
71
+ }
72
+
73
+ async function serveFile(
74
+ res: ServerResponse,
75
+ method: string,
76
+ name: string,
77
+ ): Promise<void> {
78
+ const path = resolveCachedAttachment(name);
79
+ if (!path) {
80
+ res.writeHead(404).end();
81
+ return;
82
+ }
83
+ let size: number;
84
+ try {
85
+ size = (await stat(path)).size;
86
+ } catch {
87
+ res.writeHead(404).end();
88
+ return;
89
+ }
90
+ res.writeHead(200, {
91
+ 'content-type': guessMime(path),
92
+ 'content-length': String(size),
93
+ 'cache-control': 'private, max-age=86400',
94
+ });
95
+ if (method === 'HEAD') {
96
+ res.end();
97
+ return;
98
+ }
99
+ const stream = createReadStream(path);
100
+ stream.on('error', (err) => {
101
+ log.warn({ err: errMsg(err) }, 'attach: read stream error');
102
+ res.end();
103
+ });
104
+ stream.pipe(res);
105
+ }
106
+
107
+ export function handleAttachRequest(
108
+ req: IncomingMessage,
109
+ res: ServerResponse,
110
+ ): boolean {
111
+ const [rawPath] = (req.url ?? '').split('?', 2);
112
+ const m = /^\/attach\/([^/]+)$/.exec(rawPath ?? '');
113
+ if (!m) return false;
114
+ const method = req.method ?? 'GET';
115
+ if (method !== 'GET' && method !== 'HEAD') {
116
+ res.writeHead(405).end();
117
+ return true;
118
+ }
119
+ const name = decodeURIComponent(m[1] ?? '');
120
+ if (resolveCachedAttachment(name) === null) {
121
+ res.writeHead(404).end();
122
+ return true;
123
+ }
124
+ if (!authorized(req, name)) {
125
+ res.writeHead(401).end();
126
+ return true;
127
+ }
128
+ serveFile(res, method, name).catch((err: unknown) => {
129
+ log.warn({ err: errMsg(err) }, 'attach: serve error');
130
+ if (!res.headersSent) res.writeHead(500).end();
131
+ });
132
+ return true;
133
+ }
@@ -0,0 +1,278 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { ApiError } from './api-error.js';
3
+ import { errMsg, log } from './log.js';
4
+ import type { StationName } from '../db/schema.js';
5
+ import {
6
+ startInteractiveAttach,
7
+ type AttachDriver,
8
+ type AttachOutcome,
9
+ type AttachPrompt,
10
+ type AttachStep,
11
+ type DriverHooks,
12
+ type InteractiveStation,
13
+ type StartedAttach,
14
+ } from '../stations/attach-interactive.js';
15
+
16
+ export type StartAttach = (
17
+ station: InteractiveStation,
18
+ input: Record<string, unknown>,
19
+ hooks: DriverHooks,
20
+ ) => Promise<StartedAttach>;
21
+
22
+ export const ATTACH_ID_RE = /^as_[A-Za-z0-9_-]{22}$/;
23
+
24
+ const PENDING_TTL_MS = 5 * 60_000;
25
+ const SETTLED_TTL_MS = 60_000;
26
+ const SWEEP_MS = 15_000;
27
+ const MAX_PER_AGENT = 2;
28
+ const MAX_TOTAL = 40;
29
+
30
+ export interface AttachOwner {
31
+ subject: string;
32
+ agentId: string;
33
+ }
34
+
35
+ export interface AttachView {
36
+ attachId: string;
37
+ station: string;
38
+ status: 'pending' | 'done' | 'failed';
39
+ step: AttachStep | null;
40
+ prompt: string;
41
+ qr: string | null;
42
+ pairingCode: string | null;
43
+ accountId: string | null;
44
+ identity: Record<string, string>;
45
+ activated: boolean;
46
+ error: string | null;
47
+ expiresAt: number;
48
+ }
49
+
50
+ export type CompleteAttach = (
51
+ owner: AttachOwner,
52
+ station: StationName,
53
+ config: Record<string, unknown>,
54
+ ) => Promise<{ accountId: string; activated: boolean }>;
55
+
56
+ export type AuthorizeAttach = (owner: AttachOwner) => Promise<void>;
57
+
58
+ export interface AttachSessionDeps {
59
+ authorize: AuthorizeAttach;
60
+ complete: CompleteAttach;
61
+ start?: StartAttach;
62
+ }
63
+
64
+ interface Session {
65
+ owner: AttachOwner;
66
+ driver: AttachDriver | null;
67
+ view: AttachView;
68
+ }
69
+
70
+ const missing = (): ApiError => new ApiError('no such attach session', 404);
71
+
72
+ function newAttachId(): string {
73
+ return `as_${randomBytes(16).toString('base64url')}`;
74
+ }
75
+
76
+ function blankView(attachId: string, station: string): AttachView {
77
+ return {
78
+ attachId,
79
+ station,
80
+ status: 'pending',
81
+ step: null,
82
+ prompt: '',
83
+ qr: null,
84
+ pairingCode: null,
85
+ accountId: null,
86
+ identity: {},
87
+ activated: false,
88
+ error: null,
89
+ expiresAt: Date.now() + PENDING_TTL_MS,
90
+ };
91
+ }
92
+
93
+ export class AttachSessions {
94
+ private sessions = new Map<string, Session>();
95
+ private sweeper: ReturnType<typeof setInterval> | null = null;
96
+
97
+ constructor(private deps: AttachSessionDeps) {}
98
+
99
+ private ensureSweeper(): void {
100
+ if (this.sweeper) return;
101
+ this.sweeper = setInterval(() => {
102
+ this.sweep().catch((err: unknown) => {
103
+ log.warn({ err: errMsg(err) }, 'attach-session: sweep failed');
104
+ });
105
+ }, SWEEP_MS);
106
+ this.sweeper.unref();
107
+ }
108
+
109
+ private own(owner: AttachOwner, attachId: string): Session {
110
+ const session = this.sessions.get(attachId);
111
+ if (
112
+ session?.owner.subject !== owner.subject ||
113
+ session.owner.agentId !== owner.agentId
114
+ )
115
+ throw missing();
116
+ return session;
117
+ }
118
+
119
+ private admit(owner: AttachOwner): void {
120
+ if (this.sessions.size >= MAX_TOTAL)
121
+ throw new ApiError(
122
+ 'too many sign-ins are in flight on this daemon, try again shortly',
123
+ 429,
124
+ );
125
+ const mine = [...this.sessions.values()].filter(
126
+ (s) => s.owner.agentId === owner.agentId && s.view.status === 'pending',
127
+ );
128
+ if (mine.length >= MAX_PER_AGENT)
129
+ throw new ApiError(
130
+ 'this agent already has a sign-in in progress, finish or cancel it first',
131
+ 409,
132
+ );
133
+ }
134
+
135
+ private settle(session: Session, patch: Partial<AttachView>): void {
136
+ Object.assign(session.view, patch, {
137
+ qr: null,
138
+ pairingCode: null,
139
+ step: null,
140
+ expiresAt: Date.now() + SETTLED_TTL_MS,
141
+ });
142
+ session.driver = null;
143
+ }
144
+
145
+ private async finish(
146
+ attachId: string,
147
+ station: StationName,
148
+ outcome: AttachOutcome,
149
+ ): Promise<void> {
150
+ const session = this.sessions.get(attachId);
151
+ if (session?.view.status !== 'pending') return;
152
+ try {
153
+ const landed = await this.deps.complete(
154
+ session.owner,
155
+ station,
156
+ outcome.config,
157
+ );
158
+ this.settle(session, {
159
+ status: 'done',
160
+ prompt: '',
161
+ identity: outcome.identity,
162
+ accountId: landed.accountId,
163
+ activated: landed.activated,
164
+ });
165
+ log.info(
166
+ { agentId: session.owner.agentId, station, account: landed.accountId },
167
+ 'attach-session: signed in and attached a station account',
168
+ );
169
+ } catch (err) {
170
+ const reason =
171
+ err instanceof ApiError ? err.message : 'could not store the account';
172
+ log.warn({ station, err: errMsg(err) }, 'attach-session: store failed');
173
+ this.settle(session, { status: 'failed', prompt: '', error: reason });
174
+ }
175
+ }
176
+
177
+ private hooksFor(attachId: string, station: StationName) {
178
+ return {
179
+ prompt: (p: AttachPrompt): void => {
180
+ const session = this.sessions.get(attachId);
181
+ if (session?.view.status !== 'pending') return;
182
+ session.view.step = p.step;
183
+ session.view.prompt = p.prompt;
184
+ session.view.qr = p.qr ?? null;
185
+ session.view.pairingCode = p.pairingCode ?? null;
186
+ },
187
+ done: (o: AttachOutcome): void => {
188
+ this.finish(attachId, station, o).catch((err: unknown) => {
189
+ log.warn(
190
+ { attachId, station, err: errMsg(err) },
191
+ 'attach-session: completion failed',
192
+ );
193
+ });
194
+ },
195
+ fail: (message: string): void => {
196
+ const session = this.sessions.get(attachId);
197
+ if (session?.view.status !== 'pending') return;
198
+ this.settle(session, { status: 'failed', prompt: '', error: message });
199
+ },
200
+ };
201
+ }
202
+
203
+ async start(
204
+ owner: AttachOwner,
205
+ station: InteractiveStation,
206
+ input: Record<string, unknown>,
207
+ ): Promise<AttachView> {
208
+ await this.deps.authorize(owner);
209
+ this.admit(owner);
210
+ this.ensureSweeper();
211
+ const attachId = newAttachId();
212
+ const session: Session = {
213
+ owner,
214
+ driver: null,
215
+ view: blankView(attachId, station),
216
+ };
217
+ this.sessions.set(attachId, session);
218
+ try {
219
+ const started = await (this.deps.start ?? startInteractiveAttach)(
220
+ station,
221
+ input,
222
+ this.hooksFor(attachId, station),
223
+ );
224
+ session.driver = started.driver;
225
+ this.hooksFor(attachId, station).prompt(started.prompt);
226
+ } catch (err) {
227
+ this.sessions.delete(attachId);
228
+ throw err;
229
+ }
230
+ log.info(
231
+ { agentId: owner.agentId, station, attachId },
232
+ 'attach-session: started',
233
+ );
234
+ return { ...session.view };
235
+ }
236
+
237
+ view(owner: AttachOwner, attachId: string): AttachView {
238
+ return { ...this.own(owner, attachId).view };
239
+ }
240
+
241
+ async submit(
242
+ owner: AttachOwner,
243
+ attachId: string,
244
+ input: { code?: unknown; password?: unknown },
245
+ ): Promise<AttachView> {
246
+ const session = this.own(owner, attachId);
247
+ const driver = session.driver;
248
+ if (session.view.status !== 'pending' || !driver)
249
+ throw new ApiError('this sign-in is already finished', 409);
250
+ await driver.submit(input);
251
+ return { ...session.view };
252
+ }
253
+
254
+ async cancel(owner: AttachOwner, attachId: string): Promise<void> {
255
+ const session = this.own(owner, attachId);
256
+ this.sessions.delete(attachId);
257
+ await session.driver?.cancel().catch(() => undefined);
258
+ log.info({ attachId }, 'attach-session: cancelled');
259
+ }
260
+
261
+ async sweep(now = Date.now()): Promise<void> {
262
+ for (const [attachId, session] of [...this.sessions]) {
263
+ if (session.view.expiresAt > now) continue;
264
+ this.sessions.delete(attachId);
265
+ const driver = session.driver;
266
+ session.driver = null;
267
+ if (!driver) continue;
268
+ log.info({ attachId }, 'attach-session: expired, credentials discarded');
269
+ await driver.cancel().catch(() => undefined);
270
+ }
271
+ }
272
+
273
+ async stop(): Promise<void> {
274
+ if (this.sweeper) clearInterval(this.sweeper);
275
+ this.sweeper = null;
276
+ await this.sweep(Number.MAX_SAFE_INTEGER);
277
+ }
278
+ }