@stage-labs/metro 0.1.0-beta.3 → 0.1.0-beta.31

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 (235) 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 +164 -253
  8. package/dist/control.js +158 -0
  9. package/dist/plugin.js +82 -0
  10. package/dist/prompt.js +61 -0
  11. package/dist/runtime.js +94 -0
  12. package/dist/store.js +93 -0
  13. package/dist/tail.js +120 -0
  14. package/dist/update.js +33 -0
  15. package/dist/version.js +97 -0
  16. package/package.json +34 -21
  17. package/runtime/node_modules/@metro-labs/discord-bot/package.json +28 -0
  18. package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +119 -0
  19. package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +388 -0
  20. package/runtime/node_modules/@metro-labs/discord-bot/src/api-base.ts +1 -0
  21. package/runtime/node_modules/@metro-labs/discord-bot/src/attachments.ts +29 -0
  22. package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +210 -0
  23. package/runtime/node_modules/@metro-labs/discord-bot/src/group-actions.ts +50 -0
  24. package/runtime/node_modules/@metro-labs/discord-bot/src/group.ts +115 -0
  25. package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +127 -0
  26. package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +215 -0
  27. package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +43 -0
  28. package/runtime/node_modules/@metro-labs/discord-bot/src/station.ts +19 -0
  29. package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +82 -0
  30. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-speak.ts +195 -0
  31. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-transcribe.ts +292 -0
  32. package/runtime/node_modules/@metro-labs/discord-bot/src/voice.ts +197 -0
  33. package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +5 -0
  34. package/runtime/node_modules/@metro-labs/mcp/package.json +66 -0
  35. package/runtime/node_modules/@metro-labs/mcp/src/channels/dedupe.ts +24 -0
  36. package/runtime/node_modules/@metro-labs/mcp/src/channels/inbound.ts +367 -0
  37. package/runtime/node_modules/@metro-labs/mcp/src/channels/media-note.ts +85 -0
  38. package/runtime/node_modules/@metro-labs/mcp/src/channels/pending.ts +75 -0
  39. package/runtime/node_modules/@metro-labs/mcp/src/channels/relay.ts +129 -0
  40. package/runtime/node_modules/@metro-labs/mcp/src/channels/webhook-note.ts +51 -0
  41. package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +311 -0
  42. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +332 -0
  43. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-connector-api.ts +150 -0
  44. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-pair.ts +20 -0
  45. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-error.ts +8 -0
  46. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +152 -0
  47. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-grant.ts +99 -0
  48. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-owner.ts +32 -0
  49. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +129 -0
  50. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +278 -0
  51. package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +330 -0
  52. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +125 -0
  53. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +313 -0
  54. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-json.ts +21 -0
  55. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth-routes.ts +157 -0
  56. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +127 -0
  57. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-tools.ts +100 -0
  58. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-url.ts +57 -0
  59. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +333 -0
  60. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cors.ts +29 -0
  61. package/runtime/node_modules/@metro-labs/mcp/src/daemon/crash-guard.ts +49 -0
  62. package/runtime/node_modules/@metro-labs/mcp/src/daemon/events.ts +117 -0
  63. package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-auth.ts +164 -0
  64. package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-oauth.ts +208 -0
  65. package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +359 -0
  66. package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +79 -0
  67. package/runtime/node_modules/@metro-labs/mcp/src/daemon/log.ts +42 -0
  68. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-client.ts +162 -0
  69. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-discovery.ts +120 -0
  70. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-pending.ts +65 -0
  71. package/runtime/node_modules/@metro-labs/mcp/src/daemon/pair-codes.ts +58 -0
  72. package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +134 -0
  73. package/runtime/node_modules/@metro-labs/mcp/src/daemon/project-api.ts +214 -0
  74. package/runtime/node_modules/@metro-labs/mcp/src/daemon/protocol.ts +224 -0
  75. package/runtime/node_modules/@metro-labs/mcp/src/daemon/relay.ts +303 -0
  76. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +139 -0
  77. package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-source.ts +105 -0
  78. package/runtime/node_modules/@metro-labs/mcp/src/daemon/secure-fs.ts +57 -0
  79. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-api.ts +27 -0
  80. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +48 -0
  81. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +218 -0
  82. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor-io.ts +97 -0
  83. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor.ts +323 -0
  84. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-call.ts +22 -0
  85. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-error.ts +53 -0
  86. package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +169 -0
  87. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +295 -0
  88. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-store.ts +181 -0
  89. package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +91 -0
  90. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +215 -0
  91. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-connectors.ts +143 -0
  92. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-map.ts +71 -0
  93. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-scope.ts +64 -0
  94. package/runtime/node_modules/@metro-labs/mcp/src/db/client.ts +37 -0
  95. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-config.ts +147 -0
  96. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-relay.ts +126 -0
  97. package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +308 -0
  98. package/runtime/node_modules/@metro-labs/mcp/src/db/ids.ts +19 -0
  99. package/runtime/node_modules/@metro-labs/mcp/src/db/key-map.ts +45 -0
  100. package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +382 -0
  101. package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +300 -0
  102. package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +24 -0
  103. package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +105 -0
  104. package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +117 -0
  105. package/runtime/node_modules/@metro-labs/mcp/src/db/token-carry.ts +45 -0
  106. package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +62 -0
  107. package/runtime/node_modules/@metro-labs/mcp/src/db/whatsapp-login.ts +18 -0
  108. package/runtime/node_modules/@metro-labs/mcp/src/mcp/accounts.ts +145 -0
  109. package/runtime/node_modules/@metro-labs/mcp/src/mcp/call-tools.ts +203 -0
  110. package/runtime/node_modules/@metro-labs/mcp/src/mcp/channel-owner.ts +29 -0
  111. package/runtime/node_modules/@metro-labs/mcp/src/mcp/ctx.ts +62 -0
  112. package/runtime/node_modules/@metro-labs/mcp/src/mcp/event-store.ts +91 -0
  113. package/runtime/node_modules/@metro-labs/mcp/src/mcp/frame-scope.ts +21 -0
  114. package/runtime/node_modules/@metro-labs/mcp/src/mcp/group-tools.ts +129 -0
  115. package/runtime/node_modules/@metro-labs/mcp/src/mcp/index.ts +192 -0
  116. package/runtime/node_modules/@metro-labs/mcp/src/mcp/member-tools.ts +72 -0
  117. package/runtime/node_modules/@metro-labs/mcp/src/mcp/permission-relay.ts +66 -0
  118. package/runtime/node_modules/@metro-labs/mcp/src/mcp/raw-get-stream.ts +207 -0
  119. package/runtime/node_modules/@metro-labs/mcp/src/mcp/request-identity.ts +73 -0
  120. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-registry.ts +172 -0
  121. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-route.ts +43 -0
  122. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session.ts +211 -0
  123. package/runtime/node_modules/@metro-labs/mcp/src/mcp/str.ts +5 -0
  124. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-dispatch.ts +212 -0
  125. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +397 -0
  126. package/runtime/node_modules/@metro-labs/mcp/src/mcp/upload-tool.ts +66 -0
  127. package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +240 -0
  128. package/runtime/node_modules/@metro-labs/mcp/src/server.ts +2 -0
  129. package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +80 -0
  130. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-inline.ts +100 -0
  131. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-interactive.ts +161 -0
  132. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-resolve.ts +200 -0
  133. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-xmtp.ts +111 -0
  134. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +182 -0
  135. package/runtime/node_modules/@metro-labs/mcp/src/stations/attachments.ts +196 -0
  136. package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +82 -0
  137. package/runtime/node_modules/@metro-labs/mcp/src/stations/messaging-normalize.ts +88 -0
  138. package/runtime/node_modules/@metro-labs/mcp/src/stations/registry.ts +35 -0
  139. package/runtime/node_modules/@metro-labs/mcp/src/stations/station-runtime.ts +64 -0
  140. package/runtime/node_modules/@metro-labs/mcp/src/stations/types.ts +107 -0
  141. package/runtime/node_modules/@metro-labs/telegram/package.json +28 -0
  142. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +81 -0
  143. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +254 -0
  144. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +87 -0
  145. package/runtime/node_modules/@metro-labs/telegram/src/client.ts +77 -0
  146. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +141 -0
  147. package/runtime/node_modules/@metro-labs/telegram/src/group.ts +261 -0
  148. package/runtime/node_modules/@metro-labs/telegram/src/history.ts +52 -0
  149. package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +78 -0
  150. package/runtime/node_modules/@metro-labs/telegram/src/index.ts +46 -0
  151. package/runtime/node_modules/@metro-labs/telegram/src/login.ts +166 -0
  152. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +80 -0
  153. package/runtime/node_modules/@metro-labs/telegram/src/members.ts +75 -0
  154. package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +22 -0
  155. package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +167 -0
  156. package/runtime/node_modules/@metro-labs/telegram/src/station.ts +24 -0
  157. package/runtime/node_modules/@metro-labs/telegram/src/types.ts +7 -0
  158. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +1 -0
  159. package/runtime/node_modules/@metro-labs/telegram-bot/package.json +25 -0
  160. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +120 -0
  161. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +295 -0
  162. package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
  163. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +172 -0
  164. package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +103 -0
  165. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +166 -0
  166. package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
  167. package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +17 -0
  168. package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +48 -0
  169. package/runtime/node_modules/@metro-labs/telegram-bot/src/verify.ts +38 -0
  170. package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +3 -0
  171. package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
  172. package/runtime/node_modules/@metro-labs/webhook/src/station.ts +40 -0
  173. package/runtime/node_modules/@metro-labs/whatsapp/package.json +29 -0
  174. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +67 -0
  175. package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
  176. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +178 -0
  177. package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +72 -0
  178. package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +129 -0
  179. package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +318 -0
  180. package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
  181. package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +132 -0
  182. package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +57 -0
  183. package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +40 -0
  184. package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +59 -0
  185. package/runtime/node_modules/@metro-labs/whatsapp/src/logger.ts +60 -0
  186. package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +176 -0
  187. package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +84 -0
  188. package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +22 -0
  189. package/runtime/node_modules/@metro-labs/whatsapp/src/outbox.ts +38 -0
  190. package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +141 -0
  191. package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +17 -0
  192. package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +98 -0
  193. package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +6 -0
  194. package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +5 -0
  195. package/runtime/node_modules/@metro-labs/xmtp/package.json +33 -0
  196. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +113 -0
  197. package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +110 -0
  198. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +299 -0
  199. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +157 -0
  200. package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +87 -0
  201. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +375 -0
  202. package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +107 -0
  203. package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +171 -0
  204. package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +239 -0
  205. package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +55 -0
  206. package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +333 -0
  207. package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +78 -0
  208. package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +34 -0
  209. package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +56 -0
  210. package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +33 -0
  211. package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +125 -0
  212. package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +157 -0
  213. package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
  214. package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +349 -0
  215. package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +13 -0
  216. package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +126 -0
  217. package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +212 -0
  218. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +90 -0
  219. package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +84 -0
  220. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +39 -0
  221. package/runtime/runtime.json +3 -0
  222. package/runtime/trains/.keep +0 -0
  223. package/LICENSE +0 -21
  224. package/README.md +0 -114
  225. package/dist/agents/claude.js +0 -229
  226. package/dist/agents/codex.js +0 -282
  227. package/dist/agents/types.js +0 -3
  228. package/dist/channels/discord.js +0 -142
  229. package/dist/channels/telegram.js +0 -209
  230. package/dist/lib/dotenv.js +0 -31
  231. package/dist/lib/scope-cache.js +0 -85
  232. package/dist/lib/streaming.js +0 -207
  233. package/dist/log.js +0 -12
  234. package/dist/orchestrator.js +0 -399
  235. package/dist/paths.js +0 -33
@@ -0,0 +1,313 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { errMsg, log } from './log.js';
3
+ import {
4
+ apiFailure,
5
+ apiSession,
6
+ projectParam,
7
+ bodyField,
8
+ cors,
9
+ readJsonBody,
10
+ sendJson,
11
+ type ApiSession,
12
+ type Fence,
13
+ } from './api-http.js';
14
+ import { parseId } from '../db/ids.js';
15
+ import type { AgentConnectors } from '../db/agent-connectors.js';
16
+ import type { RelayServerEntry } from './connector-json.js';
17
+ import {
18
+ handleCallback,
19
+ handleConnect,
20
+ hostOf,
21
+ startOAuth,
22
+ type OAuthRouteDeps,
23
+ } from './connector-oauth-routes.js';
24
+ import { ConnectorUnauthorized } from './connector-verify.js';
25
+ import type {
26
+ Connector,
27
+ ConnectorCheck,
28
+ ConnectorInput,
29
+ DeletedConnector,
30
+ } from '../db/connectors.js';
31
+
32
+ const PREFIX = '/api/connectors';
33
+
34
+ const asText = (value: unknown): string =>
35
+ typeof value === 'string' ? value : '';
36
+
37
+ export interface ConnectorApiDeps extends OAuthRouteDeps {
38
+ listConnectors: (email: string, project: string) => Promise<Connector[]>;
39
+ connectorNamesByIds: (ids: string[]) => Promise<RelayServerEntry[]>;
40
+ agentConnectors: (email: string, agentId: string) => Promise<AgentConnectors>;
41
+ fenceRuntime: Fence;
42
+ createConnector: (
43
+ email: string,
44
+ project: string,
45
+ input: ConnectorInput,
46
+ ) => Promise<Connector>;
47
+ verifyConnector: (email: string, id: string) => Promise<ConnectorCheck>;
48
+ disconnectConnector: (email: string, id: string) => Promise<Connector>;
49
+ renameConnector: (
50
+ email: string,
51
+ id: string,
52
+ name: string,
53
+ ) => Promise<Connector>;
54
+ deleteConnector: (email: string, id: string) => Promise<DeletedConnector>;
55
+ }
56
+
57
+ type Routable =
58
+ | { kind: 'collection' }
59
+ | { kind: 'callback' }
60
+ | { kind: 'connector'; id: string }
61
+ | { kind: 'verify'; id: string }
62
+ | { kind: 'connect'; id: string }
63
+ | { kind: 'disconnect'; id: string }
64
+ | { kind: 'rename'; id: string };
65
+
66
+ type Target = Routable | { kind: 'unknown' } | null;
67
+
68
+ function subTarget(id: string, rest: string[]): Target {
69
+ if (rest.length === 0) return { kind: 'connector', id };
70
+ if (rest.length > 1) return { kind: 'unknown' };
71
+ const head = rest[0];
72
+ if (head === 'verify') return { kind: 'verify', id };
73
+ if (head === 'connect') return { kind: 'connect', id };
74
+ if (head === 'disconnect') return { kind: 'disconnect', id };
75
+ if (head === 'rename') return { kind: 'rename', id };
76
+ return { kind: 'unknown' };
77
+ }
78
+
79
+ function target(path: string): Target {
80
+ if (path === PREFIX || path === `${PREFIX}/`) return { kind: 'collection' };
81
+ if (!path.startsWith(`${PREFIX}/`)) return null;
82
+ const segments = path.slice(PREFIX.length + 1).split('/').filter(Boolean);
83
+ const head = segments[0];
84
+ if (head === undefined) return { kind: 'collection' };
85
+ if (head === 'callback' && segments.length === 1) return { kind: 'callback' };
86
+ const id = parseId(head);
87
+ return id === null ? { kind: 'unknown' } : subTarget(id, segments.slice(1));
88
+ }
89
+
90
+ function connectorPayload(
91
+ row: Connector,
92
+ detail = false,
93
+ ): Record<string, unknown> {
94
+ const { catalog, ...summary } = row.verified;
95
+ return {
96
+ id: row.id,
97
+ name: row.name,
98
+ url: row.url,
99
+ transport: row.transport,
100
+ auth: row.auth,
101
+ header: row.header,
102
+ signIn: row.signIn,
103
+ verified: detail ? { ...summary, catalog } : summary,
104
+ };
105
+ }
106
+
107
+ async function handleList(
108
+ req: IncomingMessage,
109
+ res: ServerResponse,
110
+ deps: ConnectorApiDeps,
111
+ session: ApiSession,
112
+ ): Promise<void> {
113
+ const project = projectParam(req);
114
+ if (project === null) {
115
+ sendJson(req, res, 400, { error: 'a project is required' });
116
+ return;
117
+ }
118
+ const rows = await deps.listConnectors(session.email, project);
119
+ sendJson(req, res, 200, {
120
+ connectors: rows.map((row) => connectorPayload(row)),
121
+ });
122
+ }
123
+
124
+ async function handleCreate(
125
+ req: IncomingMessage,
126
+ res: ServerResponse,
127
+ deps: ConnectorApiDeps,
128
+ session: ApiSession,
129
+ ): Promise<void> {
130
+ const body = await readJsonBody(req);
131
+ const project = projectParam(req);
132
+ if (project === null) {
133
+ sendJson(req, res, 400, { error: 'a project is required' });
134
+ return;
135
+ }
136
+ const offered = asText(bodyField(body, 'value')).trim() !== '';
137
+ try {
138
+ const created = await deps.createConnector(session.email, project, {
139
+ name: bodyField(body, 'name'),
140
+ url: bodyField(body, 'url'),
141
+ header: bodyField(body, 'header'),
142
+ value: bodyField(body, 'value'),
143
+ });
144
+ log.info(
145
+ { id: created.id, name: created.name, host: hostOf(created.url) },
146
+ 'connector-api: created connector',
147
+ );
148
+ sendJson(req, res, 201, connectorPayload(created));
149
+ } catch (err) {
150
+ if (offered || !(err instanceof ConnectorUnauthorized)) throw err;
151
+ await startOAuth(req, res, deps, session, project, body, (row) =>
152
+ connectorPayload(row),
153
+ );
154
+ }
155
+ }
156
+
157
+ async function handleVerify(
158
+ req: IncomingMessage,
159
+ res: ServerResponse,
160
+ deps: ConnectorApiDeps,
161
+ session: ApiSession,
162
+ id: string,
163
+ ): Promise<void> {
164
+ const check = await deps.verifyConnector(session.email, id);
165
+ log.info(
166
+ { id: check.id, name: check.name, ok: check.ok },
167
+ 'connector-api: re-verified connector',
168
+ );
169
+ sendJson(req, res, 200, check);
170
+ }
171
+
172
+ async function handleDisconnect(
173
+ req: IncomingMessage,
174
+ res: ServerResponse,
175
+ deps: ConnectorApiDeps,
176
+ session: ApiSession,
177
+ id: string,
178
+ ): Promise<void> {
179
+ const row = await deps.disconnectConnector(session.email, id);
180
+ log.info(
181
+ { id: row.id, name: row.name },
182
+ 'connector-api: signed the connector out',
183
+ );
184
+ sendJson(req, res, 200, connectorPayload(row));
185
+ }
186
+
187
+ async function handleDelete(
188
+ req: IncomingMessage,
189
+ res: ServerResponse,
190
+ deps: ConnectorApiDeps,
191
+ session: ApiSession,
192
+ id: string,
193
+ ): Promise<void> {
194
+ const gone = await deps.deleteConnector(session.email, id);
195
+ log.info(
196
+ { id: gone.id, name: gone.name },
197
+ 'connector-api: deleted connector',
198
+ );
199
+ sendJson(req, res, 200, { id: gone.id, name: gone.name, deleted: true });
200
+ }
201
+
202
+ async function handleConnector(
203
+ req: IncomingMessage,
204
+ res: ServerResponse,
205
+ deps: ConnectorApiDeps,
206
+ session: ApiSession,
207
+ id: string,
208
+ ): Promise<void> {
209
+ if (req.method !== 'GET') {
210
+ await handleDelete(req, res, deps, session, id);
211
+ return;
212
+ }
213
+ const row = await deps.getConnector(session.email, id);
214
+ sendJson(req, res, 200, connectorPayload(row, true));
215
+ }
216
+
217
+ async function handleRename(
218
+ req: IncomingMessage,
219
+ res: ServerResponse,
220
+ deps: ConnectorApiDeps,
221
+ session: ApiSession,
222
+ id: string,
223
+ ): Promise<void> {
224
+ const name = bodyField(await readJsonBody(req), 'name');
225
+ if (typeof name !== 'string') {
226
+ sendJson(req, res, 400, { error: 'name is required' });
227
+ return;
228
+ }
229
+ const row = await deps.renameConnector(session.email, id, name);
230
+ log.info({ id: row.id, name: row.name }, 'connector-api: renamed connector');
231
+ sendJson(req, res, 200, connectorPayload(row));
232
+ }
233
+
234
+ async function route(
235
+ req: IncomingMessage,
236
+ res: ServerResponse,
237
+ deps: ConnectorApiDeps,
238
+ session: ApiSession,
239
+ tgt: Routable,
240
+ ): Promise<void> {
241
+ try {
242
+ if (tgt.kind === 'callback') return;
243
+ if (tgt.kind === 'verify')
244
+ await handleVerify(req, res, deps, session, tgt.id);
245
+ else if (tgt.kind === 'connect')
246
+ await handleConnect(req, res, deps, session, tgt.id);
247
+ else if (tgt.kind === 'disconnect')
248
+ await handleDisconnect(req, res, deps, session, tgt.id);
249
+ else if (tgt.kind === 'rename')
250
+ await handleRename(req, res, deps, session, tgt.id);
251
+ else if (tgt.kind === 'connector')
252
+ await handleConnector(req, res, deps, session, tgt.id);
253
+ else if (req.method === 'GET') await handleList(req, res, deps, session);
254
+ else await handleCreate(req, res, deps, session);
255
+ } catch (err) {
256
+ apiFailure(req, res, err, 'connector-api');
257
+ }
258
+ }
259
+
260
+ const ALLOWED: Record<Routable['kind'], string[]> = {
261
+ collection: ['GET', 'POST'],
262
+ callback: ['GET'],
263
+ connector: ['GET', 'DELETE'],
264
+ verify: ['POST'],
265
+ connect: ['POST'],
266
+ disconnect: ['POST'],
267
+ rename: ['POST'],
268
+ };
269
+
270
+ function dispatch(
271
+ req: IncomingMessage,
272
+ res: ServerResponse,
273
+ deps: ConnectorApiDeps,
274
+ tgt: Routable,
275
+ ): Promise<void> {
276
+ if (tgt.kind === 'callback') {
277
+ handleCallback(req, res, deps);
278
+ return Promise.resolve();
279
+ }
280
+ const session = apiSession(req);
281
+ if (!session) {
282
+ sendJson(req, res, 401, { error: 'unauthorized' });
283
+ return Promise.resolve();
284
+ }
285
+ return route(req, res, deps, session, tgt);
286
+ }
287
+
288
+ export function handleConnectorApiRequest(
289
+ req: IncomingMessage,
290
+ res: ServerResponse,
291
+ deps: ConnectorApiDeps,
292
+ ): boolean {
293
+ const tgt = target((req.url ?? '').split('?')[0] ?? '');
294
+ if (tgt === null) return false;
295
+ if (req.method === 'OPTIONS') {
296
+ res.writeHead(204, cors(req)).end();
297
+ return true;
298
+ }
299
+ if (tgt.kind === 'unknown') {
300
+ sendJson(req, res, 404, { error: 'no such connector' });
301
+ return true;
302
+ }
303
+ if (!ALLOWED[tgt.kind].includes(req.method ?? '')) {
304
+ sendJson(req, res, 405, { error: 'method not allowed' });
305
+ return true;
306
+ }
307
+ dispatch(req, res, deps, tgt).catch((err: unknown) => {
308
+ log.warn({ err: errMsg(err) }, 'connector-api: unhandled error');
309
+ if (!res.headersSent)
310
+ sendJson(req, res, 500, { error: 'connector api failed' });
311
+ });
312
+ return true;
313
+ }
@@ -0,0 +1,21 @@
1
+ export interface RelayServerEntry {
2
+ id: string;
3
+ name: string;
4
+ }
5
+
6
+ export const NAME_PREFIX = 'metro.box ';
7
+
8
+ export function relayServersJson(
9
+ entries: RelayServerEntry[],
10
+ base: string,
11
+ cliToken: string,
12
+ ): string {
13
+ const mcpServers: Record<string, unknown> = {};
14
+ for (const entry of entries)
15
+ mcpServers[`${NAME_PREFIX}${entry.name}`] = {
16
+ type: 'http',
17
+ url: `${base}/relay/${entry.id}`,
18
+ headers: { Authorization: `Bearer ${cliToken}` },
19
+ };
20
+ return JSON.stringify({ mcpServers }, null, 2);
21
+ }
@@ -0,0 +1,157 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { errMsg, log } from './log.js';
3
+ import { bodyField, readJsonBody, sendJson, type ApiSession } from './api-http.js';
4
+ import {
5
+ beginOAuth,
6
+ completeOAuth,
7
+ takePending,
8
+ type PendingAuth,
9
+ } from './connector-oauth.js';
10
+ import { parseConnectorUrl } from './connector-verify.js';
11
+ import type { OAuthAuth } from './connector-verify.js';
12
+ import type { Connector } from '../db/connectors.js';
13
+
14
+ const asText = (value: unknown): string =>
15
+ typeof value === 'string' ? value : '';
16
+
17
+ export function hostOf(url: string): string {
18
+ try {
19
+ return new URL(url).hostname;
20
+ } catch {
21
+ return '';
22
+ }
23
+ }
24
+
25
+ export interface OAuthRouteDeps {
26
+ createPendingConnector: (
27
+ email: string,
28
+ project: string,
29
+ input: { name: unknown; url: unknown },
30
+ ) => Promise<Connector>;
31
+ reconnectConnector: (
32
+ email: string,
33
+ id: string,
34
+ auth: OAuthAuth,
35
+ ) => Promise<Connector>;
36
+ getConnector: (email: string, id: string) => Promise<Connector>;
37
+ }
38
+
39
+ export async function startOAuth(
40
+ req: IncomingMessage,
41
+ res: ServerResponse,
42
+ deps: OAuthRouteDeps,
43
+ session: ApiSession,
44
+ project: string,
45
+ body: unknown,
46
+ payload: (row: Connector) => Record<string, unknown>,
47
+ ): Promise<void> {
48
+ const url = parseConnectorUrl(bodyField(body, 'url'));
49
+ const row = await deps.createPendingConnector(session.email, project, {
50
+ name: bodyField(body, 'name'),
51
+ url: bodyField(body, 'url'),
52
+ });
53
+ const authorize = await beginOAuth({
54
+ email: session.email,
55
+ name: row.name,
56
+ url,
57
+ returnTo: asText(bodyField(body, 'returnTo')),
58
+ connectorId: row.id,
59
+ });
60
+ log.info(
61
+ { id: row.id, host: url.hostname },
62
+ 'connector-api: stored unconnected, sending the user to sign in',
63
+ );
64
+ sendJson(req, res, 201, { ...payload(row), authorizeUrl: authorize });
65
+ }
66
+
67
+ export async function handleConnect(
68
+ req: IncomingMessage,
69
+ res: ServerResponse,
70
+ deps: OAuthRouteDeps,
71
+ session: ApiSession,
72
+ id: string,
73
+ ): Promise<void> {
74
+ const row = await deps.getConnector(session.email, id);
75
+ const url = parseConnectorUrl(row.url);
76
+ const body = await readJsonBody(req);
77
+ const authorize = await beginOAuth({
78
+ email: session.email,
79
+ name: row.name,
80
+ url,
81
+ returnTo: asText(bodyField(body, 'returnTo')),
82
+ connectorId: row.id,
83
+ });
84
+ log.info(
85
+ { id: row.id, host: url.hostname },
86
+ 'connector-api: sending the user back to sign in',
87
+ );
88
+ sendJson(req, res, 202, { status: 'oauth', authorizeUrl: authorize });
89
+ }
90
+
91
+ function landing(entry: PendingAuth, id?: string): string {
92
+ const target = id ?? entry.connectorId;
93
+ return target === undefined ? '#/connectors' : `#/connector/${target}`;
94
+ }
95
+
96
+ function backTo(entry: PendingAuth, error?: string, id?: string): string {
97
+ const suffix =
98
+ error === undefined
99
+ ? ''
100
+ : `?connector_error=${encodeURIComponent(error)}`;
101
+ return `${entry.returnTo}${suffix}${landing(entry, id)}`;
102
+ }
103
+
104
+ function redirect(res: ServerResponse, location: string): void {
105
+ res.writeHead(302, { location, 'cache-control': 'no-store' }).end();
106
+ }
107
+
108
+ async function saveOAuth(
109
+ deps: OAuthRouteDeps,
110
+ entry: PendingAuth,
111
+ auth: OAuthAuth,
112
+ ): Promise<Connector> {
113
+ return deps.reconnectConnector(entry.email, entry.connectorId, auth);
114
+ }
115
+
116
+ async function settleCallback(
117
+ res: ServerResponse,
118
+ deps: OAuthRouteDeps,
119
+ entry: PendingAuth,
120
+ code: string,
121
+ ): Promise<void> {
122
+ try {
123
+ const auth = await completeOAuth(entry, code);
124
+ const saved = await saveOAuth(deps, entry, auth);
125
+ log.info(
126
+ { id: saved.id, name: saved.name, host: hostOf(saved.url) },
127
+ 'connector-api: oauth sign-in completed',
128
+ );
129
+ redirect(res, backTo(entry, undefined, saved.id));
130
+ } catch (err) {
131
+ log.warn({ err: errMsg(err) }, 'connector-api: oauth sign-in failed');
132
+ redirect(res, backTo(entry, errMsg(err)));
133
+ }
134
+ }
135
+
136
+ export function handleCallback(
137
+ req: IncomingMessage,
138
+ res: ServerResponse,
139
+ deps: OAuthRouteDeps,
140
+ ): void {
141
+ const query = new URLSearchParams((req.url ?? '').split('?')[1] ?? '');
142
+ const entry = takePending(query.get('state') ?? '');
143
+ if (entry === undefined) {
144
+ sendJson(req, res, 400, { error: 'that sign-in has expired — start it again' });
145
+ return;
146
+ }
147
+ const denied = query.get('error');
148
+ const code = query.get('code') ?? '';
149
+ if (denied !== null || code === '') {
150
+ redirect(res, backTo(entry, denied ?? 'no authorization code came back'));
151
+ return;
152
+ }
153
+ settleCallback(res, deps, entry, code).catch((err: unknown) => {
154
+ log.warn({ err: errMsg(err) }, 'connector-api: oauth callback failed');
155
+ if (!res.headersSent) redirect(res, backTo(entry, 'sign-in failed'));
156
+ });
157
+ }
@@ -0,0 +1,127 @@
1
+ import { publicBaseOrDefault } from './attach-serve.js';
2
+ import {
3
+ ConnectorVerifyError,
4
+ type OAuthAuth,
5
+ type OAuthTokens,
6
+ } from './connector-verify.js';
7
+ import { validateReturnTo } from './google-oauth.js';
8
+ import {
9
+ authorizeUrl,
10
+ exchangeCode,
11
+ newVerifier,
12
+ refreshTokens,
13
+ registerClient,
14
+ } from './oauth-client.js';
15
+ import { discoverOAuth } from './oauth-discovery.js';
16
+ import { startPending, takePending, type PendingAuth } from './oauth-pending.js';
17
+
18
+ export { takePending, type PendingAuth };
19
+
20
+ const REFRESH_SKEW_MS = 300_000;
21
+
22
+ export const CALLBACK_PATH = '/api/connectors/callback';
23
+
24
+ export const callbackUri = (): string =>
25
+ `${publicBaseOrDefault()}${CALLBACK_PATH}`;
26
+
27
+ function refused(message: string): ConnectorVerifyError {
28
+ return new ConnectorVerifyError(message, 400);
29
+ }
30
+
31
+ const resourceOf = (url: URL): string => url.toString();
32
+
33
+ export interface BeginInput {
34
+ connectorId: string;
35
+ email: string;
36
+ name: string;
37
+ url: URL;
38
+ returnTo: string;
39
+ }
40
+
41
+ export async function beginOAuth(input: BeginInput): Promise<string> {
42
+ if (!validateReturnTo(input.returnTo))
43
+ throw refused('that return address is not one Metro will send you back to');
44
+ const server = await discoverOAuth(input.url);
45
+ const redirectUri = callbackUri();
46
+ const client = await registerClient(server, redirectUri);
47
+ const verifier = newVerifier();
48
+ const resource = resourceOf(input.url);
49
+ const state = startPending({
50
+ email: input.email,
51
+ name: input.name,
52
+ url: input.url.toString(),
53
+ resource,
54
+ returnTo: input.returnTo,
55
+ verifier,
56
+ server,
57
+ client,
58
+ connectorId: input.connectorId,
59
+ });
60
+ return authorizeUrl({
61
+ server,
62
+ client,
63
+ redirectUri,
64
+ state,
65
+ verifier,
66
+ resource,
67
+ });
68
+ }
69
+
70
+ function authOf(pendingAuth: PendingAuth, tokens: OAuthTokens): OAuthAuth {
71
+ return {
72
+ kind: 'oauth',
73
+ issuer: pendingAuth.server.issuer,
74
+ tokenEndpoint: pendingAuth.server.tokenEndpoint,
75
+ clientId: pendingAuth.client.clientId,
76
+ ...(pendingAuth.client.clientSecret === undefined
77
+ ? {}
78
+ : { clientSecret: pendingAuth.client.clientSecret }),
79
+ ...tokens,
80
+ };
81
+ }
82
+
83
+ export async function completeOAuth(
84
+ entry: PendingAuth,
85
+ code: string,
86
+ ): Promise<OAuthAuth> {
87
+ const tokens = await exchangeCode(entry.server, entry.client, {
88
+ code,
89
+ redirectUri: callbackUri(),
90
+ verifier: entry.verifier,
91
+ resource: entry.resource,
92
+ });
93
+ return authOf(entry, tokens);
94
+ }
95
+
96
+ export function oauthExpired(auth: OAuthAuth, now = Date.now()): boolean {
97
+ return auth.expiresAt !== undefined && auth.expiresAt - REFRESH_SKEW_MS <= now;
98
+ }
99
+
100
+ export async function refreshOAuth(
101
+ auth: OAuthAuth,
102
+ resource: string,
103
+ ): Promise<OAuthAuth> {
104
+ if (auth.refreshToken === undefined)
105
+ throw refused('that connector needs signing in again');
106
+ const server = {
107
+ issuer: auth.issuer,
108
+ authorizationEndpoint: '',
109
+ tokenEndpoint: auth.tokenEndpoint,
110
+ registrationEndpoint: null,
111
+ supportsS256: true,
112
+ };
113
+ const client = {
114
+ clientId: auth.clientId,
115
+ ...(auth.clientSecret === undefined
116
+ ? {}
117
+ : { clientSecret: auth.clientSecret }),
118
+ };
119
+ const tokens = await refreshTokens(server, client, auth.refreshToken, resource);
120
+ return {
121
+ ...auth,
122
+ ...tokens,
123
+ ...(tokens.refreshToken === undefined
124
+ ? { refreshToken: auth.refreshToken }
125
+ : {}),
126
+ };
127
+ }