@stage-labs/metro 0.1.0-beta.7 → 0.1.0-beta.70

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 (266) hide show
  1. package/dist/api.js +14 -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 +101 -0
  8. package/dist/control.js +74 -0
  9. package/dist/hold.js +197 -0
  10. package/dist/local.js +93 -0
  11. package/dist/node-name.js +51 -0
  12. package/dist/plugin.js +80 -0
  13. package/dist/runtime-install.js +84 -0
  14. package/dist/runtime.js +70 -0
  15. package/dist/serve.js +147 -0
  16. package/dist/service.js +224 -0
  17. package/dist/tail.js +119 -0
  18. package/dist/update.js +45 -0
  19. package/dist/version.js +97 -0
  20. package/package.json +14 -25
  21. package/runtime/node_modules/@metro-labs/discord-bot/package.json +28 -0
  22. package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +118 -0
  23. package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +388 -0
  24. package/runtime/node_modules/@metro-labs/discord-bot/src/api-base.ts +1 -0
  25. package/runtime/node_modules/@metro-labs/discord-bot/src/attachments.ts +29 -0
  26. package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +220 -0
  27. package/runtime/node_modules/@metro-labs/discord-bot/src/group-actions.ts +50 -0
  28. package/runtime/node_modules/@metro-labs/discord-bot/src/group.ts +115 -0
  29. package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +127 -0
  30. package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +215 -0
  31. package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +43 -0
  32. package/runtime/node_modules/@metro-labs/discord-bot/src/station.ts +19 -0
  33. package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +82 -0
  34. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-speak.ts +195 -0
  35. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-transcribe.ts +292 -0
  36. package/runtime/node_modules/@metro-labs/discord-bot/src/voice.ts +197 -0
  37. package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +5 -0
  38. package/runtime/node_modules/@metro-labs/mcp/package.json +64 -0
  39. package/runtime/node_modules/@metro-labs/mcp/src/channels/addressed.ts +25 -0
  40. package/runtime/node_modules/@metro-labs/mcp/src/channels/dedupe.ts +24 -0
  41. package/runtime/node_modules/@metro-labs/mcp/src/channels/inbound.ts +377 -0
  42. package/runtime/node_modules/@metro-labs/mcp/src/channels/media-note.ts +85 -0
  43. package/runtime/node_modules/@metro-labs/mcp/src/channels/pending.ts +75 -0
  44. package/runtime/node_modules/@metro-labs/mcp/src/channels/relay.ts +129 -0
  45. package/runtime/node_modules/@metro-labs/mcp/src/channels/webhook-note.ts +51 -0
  46. package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +311 -0
  47. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +302 -0
  48. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-error.ts +8 -0
  49. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +95 -0
  50. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-grant.ts +99 -0
  51. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-owner.ts +32 -0
  52. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +128 -0
  53. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +280 -0
  54. package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +204 -0
  55. package/runtime/node_modules/@metro-labs/mcp/src/daemon/bundle-api.ts +111 -0
  56. package/runtime/node_modules/@metro-labs/mcp/src/daemon/claude-api.ts +97 -0
  57. package/runtime/node_modules/@metro-labs/mcp/src/daemon/claude-files.ts +329 -0
  58. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connect-hint.ts +39 -0
  59. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +307 -0
  60. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-json.ts +21 -0
  61. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth-routes.ts +157 -0
  62. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +127 -0
  63. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-tools.ts +98 -0
  64. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-url.ts +70 -0
  65. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +330 -0
  66. package/runtime/node_modules/@metro-labs/mcp/src/daemon/control-api.ts +64 -0
  67. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cors.ts +29 -0
  68. package/runtime/node_modules/@metro-labs/mcp/src/daemon/crash-guard.ts +49 -0
  69. package/runtime/node_modules/@metro-labs/mcp/src/daemon/events.ts +120 -0
  70. package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +384 -0
  71. package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity-key.ts +33 -0
  72. package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity-registry.ts +13 -0
  73. package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity-routes.ts +54 -0
  74. package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +7 -0
  75. package/runtime/node_modules/@metro-labs/mcp/src/daemon/is-record.ts +2 -0
  76. package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-cli-api.ts +63 -0
  77. package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-mode.ts +184 -0
  78. package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-owner.ts +14 -0
  79. package/runtime/node_modules/@metro-labs/mcp/src/daemon/log.ts +42 -0
  80. package/runtime/node_modules/@metro-labs/mcp/src/daemon/machine-api.ts +61 -0
  81. package/runtime/node_modules/@metro-labs/mcp/src/daemon/mode-api.ts +32 -0
  82. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-client.ts +159 -0
  83. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-discovery.ts +117 -0
  84. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-pending.ts +65 -0
  85. package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +109 -0
  86. package/runtime/node_modules/@metro-labs/mcp/src/daemon/protocol.ts +226 -0
  87. package/runtime/node_modules/@metro-labs/mcp/src/daemon/relay.ts +295 -0
  88. package/runtime/node_modules/@metro-labs/mcp/src/daemon/return-to.ts +21 -0
  89. package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-deps.ts +82 -0
  90. package/runtime/node_modules/@metro-labs/mcp/src/daemon/secure-fs.ts +57 -0
  91. package/runtime/node_modules/@metro-labs/mcp/src/daemon/server-types.ts +21 -0
  92. package/runtime/node_modules/@metro-labs/mcp/src/daemon/servers-api.ts +80 -0
  93. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-api.ts +29 -0
  94. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +61 -0
  95. package/runtime/node_modules/@metro-labs/mcp/src/daemon/signed-identity.ts +36 -0
  96. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor-io.ts +97 -0
  97. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor.ts +320 -0
  98. package/runtime/node_modules/@metro-labs/mcp/src/daemon/terminal-api.ts +71 -0
  99. package/runtime/node_modules/@metro-labs/mcp/src/daemon/terminal-tickets.ts +40 -0
  100. package/runtime/node_modules/@metro-labs/mcp/src/daemon/terminal-ws.ts +114 -0
  101. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-call.ts +22 -0
  102. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-error.ts +53 -0
  103. package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +352 -0
  104. package/runtime/node_modules/@metro-labs/mcp/src/daemon/update-api.ts +116 -0
  105. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +294 -0
  106. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-store.ts +181 -0
  107. package/runtime/node_modules/@metro-labs/mcp/src/daemon/vault-api.ts +78 -0
  108. package/runtime/node_modules/@metro-labs/mcp/src/daemon/vault-types.ts +12 -0
  109. package/runtime/node_modules/@metro-labs/mcp/src/daemon/version.ts +2 -0
  110. package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +19 -0
  111. package/runtime/node_modules/@metro-labs/mcp/src/db/address.ts +6 -0
  112. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +43 -0
  113. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-map.ts +84 -0
  114. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-scope.ts +64 -0
  115. package/runtime/node_modules/@metro-labs/mcp/src/db/client.ts +32 -0
  116. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-config.ts +144 -0
  117. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-relay.ts +39 -0
  118. package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +69 -0
  119. package/runtime/node_modules/@metro-labs/mcp/src/db/file-admin.ts +329 -0
  120. package/runtime/node_modules/@metro-labs/mcp/src/db/file-source.ts +155 -0
  121. package/runtime/node_modules/@metro-labs/mcp/src/db/ids.ts +21 -0
  122. package/runtime/node_modules/@metro-labs/mcp/src/db/key-map.ts +45 -0
  123. package/runtime/node_modules/@metro-labs/mcp/src/db/local-connectors.ts +277 -0
  124. package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +253 -0
  125. package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +33 -0
  126. package/runtime/node_modules/@metro-labs/mcp/src/db/servers.ts +83 -0
  127. package/runtime/node_modules/@metro-labs/mcp/src/db/stations.ts +12 -0
  128. package/runtime/node_modules/@metro-labs/mcp/src/db/vault.ts +109 -0
  129. package/runtime/node_modules/@metro-labs/mcp/src/mcp/accounts.ts +141 -0
  130. package/runtime/node_modules/@metro-labs/mcp/src/mcp/call-tools.ts +216 -0
  131. package/runtime/node_modules/@metro-labs/mcp/src/mcp/channel-owner.ts +29 -0
  132. package/runtime/node_modules/@metro-labs/mcp/src/mcp/ctx.ts +62 -0
  133. package/runtime/node_modules/@metro-labs/mcp/src/mcp/event-store.ts +91 -0
  134. package/runtime/node_modules/@metro-labs/mcp/src/mcp/frame-scope.ts +21 -0
  135. package/runtime/node_modules/@metro-labs/mcp/src/mcp/group-tools.ts +129 -0
  136. package/runtime/node_modules/@metro-labs/mcp/src/mcp/index.ts +191 -0
  137. package/runtime/node_modules/@metro-labs/mcp/src/mcp/member-tools.ts +72 -0
  138. package/runtime/node_modules/@metro-labs/mcp/src/mcp/permission-relay.ts +66 -0
  139. package/runtime/node_modules/@metro-labs/mcp/src/mcp/raw-get-stream.ts +207 -0
  140. package/runtime/node_modules/@metro-labs/mcp/src/mcp/request-identity.ts +47 -0
  141. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-registry.ts +172 -0
  142. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-route.ts +43 -0
  143. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session.ts +215 -0
  144. package/runtime/node_modules/@metro-labs/mcp/src/mcp/str.ts +5 -0
  145. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-dispatch.ts +215 -0
  146. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +400 -0
  147. package/runtime/node_modules/@metro-labs/mcp/src/mcp/upload-tool.ts +66 -0
  148. package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +237 -0
  149. package/runtime/node_modules/@metro-labs/mcp/src/server.ts +2 -0
  150. package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +60 -0
  151. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-inline.ts +100 -0
  152. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-interactive.ts +161 -0
  153. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-resolve.ts +200 -0
  154. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-xmtp.ts +111 -0
  155. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +184 -0
  156. package/runtime/node_modules/@metro-labs/mcp/src/stations/attachments.ts +196 -0
  157. package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +82 -0
  158. package/runtime/node_modules/@metro-labs/mcp/src/stations/messaging-normalize.ts +88 -0
  159. package/runtime/node_modules/@metro-labs/mcp/src/stations/registry.ts +35 -0
  160. package/runtime/node_modules/@metro-labs/mcp/src/stations/station-runtime.ts +64 -0
  161. package/runtime/node_modules/@metro-labs/mcp/src/stations/types.ts +107 -0
  162. package/runtime/node_modules/@metro-labs/telegram/package.json +26 -0
  163. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +80 -0
  164. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +254 -0
  165. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +87 -0
  166. package/runtime/node_modules/@metro-labs/telegram/src/client.ts +77 -0
  167. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +156 -0
  168. package/runtime/node_modules/@metro-labs/telegram/src/group.ts +261 -0
  169. package/runtime/node_modules/@metro-labs/telegram/src/history.ts +52 -0
  170. package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +78 -0
  171. package/runtime/node_modules/@metro-labs/telegram/src/index.ts +46 -0
  172. package/runtime/node_modules/@metro-labs/telegram/src/login.ts +166 -0
  173. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +80 -0
  174. package/runtime/node_modules/@metro-labs/telegram/src/members.ts +75 -0
  175. package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +22 -0
  176. package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +167 -0
  177. package/runtime/node_modules/@metro-labs/telegram/src/station.ts +24 -0
  178. package/runtime/node_modules/@metro-labs/telegram/src/types.ts +7 -0
  179. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +1 -0
  180. package/runtime/node_modules/@metro-labs/telegram-bot/package.json +25 -0
  181. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +120 -0
  182. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +295 -0
  183. package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
  184. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +201 -0
  185. package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +109 -0
  186. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +166 -0
  187. package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
  188. package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +17 -0
  189. package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +58 -0
  190. package/runtime/node_modules/@metro-labs/telegram-bot/src/verify.ts +38 -0
  191. package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +3 -0
  192. package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
  193. package/runtime/node_modules/@metro-labs/webhook/src/station.ts +37 -0
  194. package/runtime/node_modules/@metro-labs/whatsapp/package.json +27 -0
  195. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +66 -0
  196. package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
  197. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +178 -0
  198. package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +72 -0
  199. package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +129 -0
  200. package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +327 -0
  201. package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
  202. package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +138 -0
  203. package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +57 -0
  204. package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +40 -0
  205. package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +59 -0
  206. package/runtime/node_modules/@metro-labs/whatsapp/src/logger.ts +60 -0
  207. package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +176 -0
  208. package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +84 -0
  209. package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +22 -0
  210. package/runtime/node_modules/@metro-labs/whatsapp/src/outbox.ts +38 -0
  211. package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +185 -0
  212. package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +17 -0
  213. package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +98 -0
  214. package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +6 -0
  215. package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +5 -0
  216. package/runtime/node_modules/@metro-labs/xmtp/package.json +33 -0
  217. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +112 -0
  218. package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +110 -0
  219. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +299 -0
  220. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +157 -0
  221. package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +87 -0
  222. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +375 -0
  223. package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +107 -0
  224. package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +171 -0
  225. package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +239 -0
  226. package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +55 -0
  227. package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +335 -0
  228. package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +80 -0
  229. package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +34 -0
  230. package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +56 -0
  231. package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +33 -0
  232. package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +125 -0
  233. package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +157 -0
  234. package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
  235. package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +349 -0
  236. package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +13 -0
  237. package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +126 -0
  238. package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +212 -0
  239. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +90 -0
  240. package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +84 -0
  241. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +53 -0
  242. package/runtime/runtime.json +3 -0
  243. package/runtime/stations.json +36 -0
  244. package/runtime/trains/.keep +0 -0
  245. package/LICENSE +0 -21
  246. package/README.md +0 -193
  247. package/dist/cache.js +0 -75
  248. package/dist/cli/actions.js +0 -139
  249. package/dist/cli/config.js +0 -185
  250. package/dist/cli/index.js +0 -159
  251. package/dist/cli/skill.js +0 -62
  252. package/dist/cli/util.js +0 -72
  253. package/dist/codex-rc.js +0 -236
  254. package/dist/dispatcher.js +0 -95
  255. package/dist/history.js +0 -84
  256. package/dist/ipc.js +0 -72
  257. package/dist/log.js +0 -15
  258. package/dist/paths.js +0 -68
  259. package/dist/stations/discord.js +0 -200
  260. package/dist/stations/index.js +0 -73
  261. package/dist/stations/telegram-md.js +0 -34
  262. package/dist/stations/telegram-upload.js +0 -113
  263. package/dist/stations/telegram.js +0 -213
  264. package/docs/agents.md +0 -185
  265. package/docs/uri-scheme.md +0 -71
  266. package/skills/metro/SKILL.md +0 -241
@@ -0,0 +1,184 @@
1
+ import { ApiError } from './api-error.js';
2
+ import { errMsg, log } from './log.js';
3
+ import { AttachSessions } from './attach-session.js';
4
+ import type { AgentApiDeps } from './agent-api.js';
5
+ import { ATTACHABLE, type AccountApiDeps } from './account-api.js';
6
+ import type { SessionApis } from './session-apis.js';
7
+ import type { ModeInfo } from './mode-api.js';
8
+ import { loadedAgentOf, type AgentBundle, type BundleApiDeps } from './bundle-api.js';
9
+ import { METRO_VERSION } from './version.js';
10
+ import type { ConnectorApiDeps } from './connector-api.js';
11
+ import { allowLocalConnectors } from './connector-url.js';
12
+ import { keyIdentity, type LocalCliDeps } from './local-cli-api.js';
13
+ import type { RelayApiDeps } from './relay.js';
14
+ import {
15
+ readLocalConnectors,
16
+ localImportConnectors,
17
+ localConnectorNamesByIds,
18
+ localCreateConnector,
19
+ localCreatePendingConnector,
20
+ localDeleteConnector,
21
+ localDisconnectConnector,
22
+ localGetConnector,
23
+ localListConnectors,
24
+ localReconnectConnector,
25
+ localRelayTarget,
26
+ localRenameConnector,
27
+ localVerifyConnector,
28
+ } from '../db/local-connectors.js';
29
+ import {
30
+ assertLocalOwner,
31
+ LOCAL_PROJECT_ID,
32
+ localAttachAccount,
33
+ localCreateAgent,
34
+ localDeleteAgent,
35
+ localDetachAccount,
36
+ localImportAgent,
37
+ localListAgents,
38
+ localOwnedAgentOrThrow,
39
+ localOwner,
40
+ localResetAgentKey,
41
+ readLocalAgentFile,
42
+ } from '../db/file-admin.js';
43
+ import { listAgentFiles, readAgentFile } from '../db/file-source.js';
44
+ import type { StationName } from '../db/stations.js';
45
+
46
+ export interface LocalModeDeps {
47
+ syncStations: (station: StationName) => Promise<void>;
48
+ restart: () => void;
49
+ stop: () => void;
50
+ closeAgentSession: (id: string) => Promise<boolean>;
51
+ gatherAccounts: AgentApiDeps['gatherAccounts'];
52
+ capabilities: AgentApiDeps['capabilities'];
53
+ liveness: AgentApiDeps['liveness'];
54
+ prepareAccount: AccountApiDeps['prepareAccount'];
55
+ }
56
+
57
+ function attachSessions(deps: LocalModeDeps): AttachSessions {
58
+ return new AttachSessions({
59
+ authorize: async (owner) => {
60
+ await localOwnedAgentOrThrow(owner.subject, owner.agentId);
61
+ },
62
+ complete: async (owner, station, config) => {
63
+ const ref = await localAttachAccount(owner.subject, owner.agentId, station, config);
64
+ const activated = await deps.syncStations(station).then(
65
+ () => true,
66
+ (err: unknown) => {
67
+ log.warn(
68
+ { station, err: errMsg(err) },
69
+ 'attach-session: station reload failed, the change lands at the next boot',
70
+ );
71
+ return false;
72
+ },
73
+ );
74
+ return { accountId: ref.accountId, activated };
75
+ },
76
+ });
77
+ }
78
+
79
+ function agentApi(deps: LocalModeDeps): AgentApiDeps {
80
+ return {
81
+ attachSessions: attachSessions(deps),
82
+ listAgents: localListAgents,
83
+ createAgent: localCreateAgent,
84
+ deleteAgent: localDeleteAgent,
85
+ resetKey: async (subject, id) => {
86
+ const reset = await localResetAgentKey(subject, id);
87
+ const closed = await deps.closeAgentSession(id);
88
+ log.info({ agent: reset.name, id, sessionClosed: closed }, 'local: key rotated');
89
+ return reset;
90
+ },
91
+ gatherAccounts: deps.gatherAccounts,
92
+ capabilities: deps.capabilities,
93
+ attachable: ATTACHABLE.filter((s) => s !== 'webhook'),
94
+ liveness: deps.liveness,
95
+ connectorIds: connectorIdsOfLocalAgents,
96
+ prepareAccount: deps.prepareAccount,
97
+ attachAccount: localAttachAccount,
98
+ detachAccount: localDetachAccount,
99
+ syncStations: deps.syncStations,
100
+ };
101
+ }
102
+
103
+ const allConnectorIds = (): string[] => readLocalConnectors().map((c) => c.id);
104
+
105
+ function connectorIdsOfLocalAgents(ids: string[]): Promise<Map<string, string[]>> {
106
+ const all = allConnectorIds();
107
+ return Promise.resolve(new Map(ids.map((id) => [id, all] as const)));
108
+ }
109
+
110
+ const connectorApi: ConnectorApiDeps = {
111
+ listConnectors: localListConnectors,
112
+ createConnector: localCreateConnector,
113
+ verifyConnector: localVerifyConnector,
114
+ disconnectConnector: localDisconnectConnector,
115
+ renameConnector: localRenameConnector,
116
+ deleteConnector: localDeleteConnector,
117
+ createPendingConnector: localCreatePendingConnector,
118
+ reconnectConnector: localReconnectConnector,
119
+ getConnector: localGetConnector,
120
+ };
121
+
122
+ const relayApi: RelayApiDeps = {
123
+ target: (connectorId, force) => localRelayTarget(connectorId, force),
124
+ identify: keyIdentity,
125
+ };
126
+
127
+ function agentNameOf(agentId: string): string | null {
128
+ return listAgentFiles()
129
+ .map((path) => readAgentFile(path))
130
+ .find((file) => file.id === agentId)?.name ?? null;
131
+ }
132
+
133
+ const localCli: LocalCliDeps = {
134
+ agentName: agentNameOf,
135
+ connectorEntries: () => localConnectorNamesByIds(allConnectorIds()),
136
+ };
137
+
138
+ function bundleApi(deps: LocalModeDeps): BundleApiDeps {
139
+ return {
140
+ bundle: async (subject, agentId) => {
141
+ const { agent } = await localOwnedAgentOrThrow(subject, agentId);
142
+ const file = readLocalAgentFile(agentId);
143
+ const bundle: AgentBundle = {
144
+ version: 1,
145
+ agent: { id: file.id, name: file.name, key: file.key ?? '', stations: file.stations },
146
+ connectors: readLocalConnectors().map((c) => ({ id: c.id, name: c.name, url: c.url, transport: c.transport, config: { ...c.config } })),
147
+ };
148
+ if (bundle.agent.key === '') throw new ApiError(`agent '${agent.name}' has no key to bundle`, 400);
149
+ return bundle;
150
+ },
151
+ restore: async (subject, bundle) => {
152
+ assertLocalOwner(subject);
153
+ const made = await localImportAgent(subject, loadedAgentOf(bundle));
154
+ const connectors = localImportConnectors(bundle.connectors);
155
+ for (const station of new Set(bundle.agent.stations.map((a) => a.station)))
156
+ await deps.syncStations(station).catch((err: unknown) => {
157
+ log.warn({ station, err: errMsg(err) }, 'restore: station reload failed, the change lands at the next boot');
158
+ });
159
+ return { id: made.id, name: made.name, stations: made.stations, connectors };
160
+ },
161
+ };
162
+ }
163
+
164
+ function localModeInfo(): ModeInfo {
165
+ return { mode: 'local', owner: localOwner(), project: LOCAL_PROJECT_ID, version: METRO_VERSION };
166
+ }
167
+
168
+ export function localSessionApis(deps: LocalModeDeps): SessionApis {
169
+ allowLocalConnectors(true);
170
+ return {
171
+ agentApi: agentApi(deps),
172
+ bundleApi: bundleApi(deps),
173
+ connectorApi,
174
+ relayApi,
175
+ localCli,
176
+ claudeApi: { authorize: (subject) => { assertLocalOwner(subject); } },
177
+ updateApi: { authorize: (subject) => { assertLocalOwner(subject); }, restart: deps.restart },
178
+ controlApi: { authorize: (subject) => { assertLocalOwner(subject); }, restart: deps.restart, stop: deps.stop },
179
+ machineApi: { authorize: (subject) => { assertLocalOwner(subject); } },
180
+ terminalApi: { authorize: (subject) => { assertLocalOwner(subject); } },
181
+ identity: { owner: localOwner },
182
+ mode: localModeInfo,
183
+ };
184
+ }
@@ -0,0 +1,14 @@
1
+ import { localOwner, setLocalOwner } from '../db/file-admin.js';
2
+ import { log } from './log.js';
3
+
4
+ export function applyLocalOwner(): string | null {
5
+ const wanted = process.env.METRO_OWNER?.trim() ?? '';
6
+ if (wanted !== '') {
7
+ const address = setLocalOwner(wanted);
8
+ log.info({ owner: address }, 'local daemon: owner set from METRO_OWNER');
9
+ return address;
10
+ }
11
+ const stored = localOwner();
12
+ if (stored === null) log.warn('local daemon: no owner set, so no wallet can sign in; restart with metro serve --owner <address>');
13
+ return stored;
14
+ }
@@ -0,0 +1,42 @@
1
+ import { writeSync } from 'node:fs';
2
+ import pino from 'pino';
3
+ import pinoPretty from 'pino-pretty';
4
+
5
+ const stream = process.stderr.isTTY
6
+ ? pinoPretty({
7
+ colorize: true,
8
+ translateTime: 'HH:MM:ss',
9
+ ignore: 'pid,hostname,name',
10
+ destination: 2,
11
+ })
12
+ : pino.destination(2);
13
+
14
+ export const log = pino(
15
+ { base: { name: 'metro' }, level: process.env.METRO_LOG_LEVEL ?? 'info' },
16
+ stream,
17
+ );
18
+
19
+ export const logFatalSync = (
20
+ fields: Record<string, unknown>,
21
+ msg: string,
22
+ ): void => {
23
+ const line = JSON.stringify({
24
+ level: 60,
25
+ time: Date.now(),
26
+ name: 'metro',
27
+ ...fields,
28
+ msg,
29
+ });
30
+ try {
31
+ writeSync(2, `${line}\n`);
32
+ } catch {
33
+ return;
34
+ }
35
+ };
36
+
37
+ export const errMsg = (err: unknown): string => {
38
+ if (err instanceof Error) return err.message;
39
+ if (err && typeof err === 'object' && 'message' in err)
40
+ return String(err.message);
41
+ return String(err);
42
+ };
@@ -0,0 +1,61 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { hostname } from 'node:os';
3
+ import { ApiError } from './api-error.js';
4
+ import { apiFailure, apiSession, cors, sendJson } from './api-http.js';
5
+ import { publicBaseUrl } from './attach-serve.js';
6
+ import { claudeDir } from './claude-files.js';
7
+ import { webhookPort } from './tunnel.js';
8
+ import { METRO_VERSION } from './version.js';
9
+ import { agentsDir } from '../db/file-source.js';
10
+ import { localOwner } from '../db/file-admin.js';
11
+
12
+ const PATH = '/api/server';
13
+
14
+ export interface MachineApiDeps {
15
+ authorize: (subject: string) => void;
16
+ startedAt?: string;
17
+ }
18
+
19
+ const bootedAt = new Date(Date.now() - process.uptime() * 1000).toISOString();
20
+
21
+ export function machineInfo(startedAt = bootedAt): Record<string, unknown> {
22
+ const store = process.env.METRO_RUNTIME_STORE?.trim() ?? '';
23
+ return {
24
+ version: METRO_VERSION,
25
+ owner: localOwner(),
26
+ hostname: hostname(),
27
+ platform: process.platform,
28
+ arch: process.arch,
29
+ port: webhookPort(),
30
+ publicUrl: publicBaseUrl(),
31
+ uptimeSeconds: Math.round(process.uptime()),
32
+ startedAt,
33
+ bun: process.versions.bun ?? null,
34
+ agentsDir: agentsDir(),
35
+ claudeDir: claudeDir(),
36
+ runtimeStore: store === '' ? null : store,
37
+ };
38
+ }
39
+
40
+ export function handleMachineRequest(req: IncomingMessage, res: ServerResponse, deps: MachineApiDeps): boolean {
41
+ const path = (req.url ?? '').split('?')[0] ?? '';
42
+ if (path !== PATH) return false;
43
+ if (req.method === 'OPTIONS') {
44
+ res.writeHead(204, cors(req)).end();
45
+ return true;
46
+ }
47
+ if (req.method !== 'GET') {
48
+ sendJson(req, res, 405, { error: 'method not allowed' });
49
+ return true;
50
+ }
51
+ apiSession(req)
52
+ .then((session) => {
53
+ if (!session) throw new ApiError('unauthorized', 401);
54
+ deps.authorize(session.subject);
55
+ sendJson(req, res, 200, machineInfo(deps.startedAt));
56
+ })
57
+ .catch((err: unknown) => {
58
+ apiFailure(req, res, err, 'machine-api');
59
+ });
60
+ return true;
61
+ }
@@ -0,0 +1,32 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { cors, sendJson } from './api-http.js';
3
+
4
+ const PATH = '/api/mode';
5
+
6
+ export type DaemonMode = 'hosted' | 'linked' | 'local';
7
+
8
+ export interface ModeInfo {
9
+ mode: DaemonMode;
10
+ owner: string | null;
11
+ project: string | null;
12
+ version: string;
13
+ }
14
+
15
+ export function handleModeRequest(
16
+ req: IncomingMessage,
17
+ res: ServerResponse,
18
+ info: () => ModeInfo,
19
+ ): boolean {
20
+ const path = (req.url ?? '').split('?')[0] ?? '';
21
+ if (path !== PATH) return false;
22
+ if (req.method === 'OPTIONS') {
23
+ res.writeHead(204, cors(req)).end();
24
+ return true;
25
+ }
26
+ if (req.method !== 'GET') {
27
+ sendJson(req, res, 405, { error: 'method not allowed' });
28
+ return true;
29
+ }
30
+ sendJson(req, res, 200, info());
31
+ return true;
32
+ }
@@ -0,0 +1,159 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import {
3
+ ConnectorVerifyError,
4
+ type OAuthTokens,
5
+ } from './connector-verify.js';
6
+ import type { OAuthServer } from './oauth-discovery.js';
7
+ import { isRecord } from './is-record.js';
8
+
9
+ const TOKEN_TIMEOUT_MS = 15_000;
10
+ const CLIENT_NAME = 'Metro';
11
+
12
+ export interface OAuthClient {
13
+ clientId: string;
14
+ clientSecret?: string;
15
+ }
16
+
17
+ function refused(message: string): ConnectorVerifyError {
18
+ return new ConnectorVerifyError(message, 400);
19
+ }
20
+
21
+ export function newVerifier(): string {
22
+ return randomBytes(32).toString('base64url');
23
+ }
24
+
25
+ export function challengeOf(verifier: string): string {
26
+ return createHash('sha256').update(verifier).digest('base64url');
27
+ }
28
+
29
+ export async function registerClient(
30
+ server: OAuthServer,
31
+ redirectUri: string,
32
+ ): Promise<OAuthClient> {
33
+ if (server.registrationEndpoint === null)
34
+ throw refused(
35
+ 'That server requires OAuth but does not accept client registration, so Metro cannot sign in to it.',
36
+ );
37
+ let res: Response;
38
+ try {
39
+ res = await fetch(server.registrationEndpoint, {
40
+ method: 'POST',
41
+ redirect: 'manual',
42
+ signal: AbortSignal.timeout(TOKEN_TIMEOUT_MS),
43
+ headers: { 'content-type': 'application/json', accept: 'application/json' },
44
+ body: JSON.stringify({
45
+ client_name: CLIENT_NAME,
46
+ redirect_uris: [redirectUri],
47
+ grant_types: ['authorization_code', 'refresh_token'],
48
+ response_types: ['code'],
49
+ token_endpoint_auth_method: 'none',
50
+ }),
51
+ });
52
+ } catch {
53
+ throw refused('Metro could not reach that server to register.');
54
+ }
55
+ const body: unknown = await res.json().catch(() => null);
56
+ if (!res.ok || !isRecord(body) || typeof body.client_id !== 'string')
57
+ throw refused('That server refused to register Metro as a client.');
58
+ const secret = body.client_secret;
59
+ return {
60
+ clientId: body.client_id,
61
+ ...(typeof secret === 'string' ? { clientSecret: secret } : {}),
62
+ };
63
+ }
64
+
65
+ export interface AuthorizeInput {
66
+ server: OAuthServer;
67
+ client: OAuthClient;
68
+ redirectUri: string;
69
+ state: string;
70
+ verifier: string;
71
+ resource: string;
72
+ }
73
+
74
+ export function authorizeUrl(input: AuthorizeInput): string {
75
+ const url = new URL(input.server.authorizationEndpoint);
76
+ url.searchParams.set('response_type', 'code');
77
+ url.searchParams.set('client_id', input.client.clientId);
78
+ url.searchParams.set('redirect_uri', input.redirectUri);
79
+ url.searchParams.set('state', input.state);
80
+ url.searchParams.set('code_challenge', challengeOf(input.verifier));
81
+ url.searchParams.set('code_challenge_method', 'S256');
82
+ url.searchParams.set('resource', input.resource);
83
+ return url.toString();
84
+ }
85
+
86
+ function tokensFrom(body: Record<string, unknown>): OAuthTokens {
87
+ const access = body.access_token;
88
+ if (typeof access !== 'string' || access === '')
89
+ throw refused('That server did not return an access token.');
90
+ const refresh = body.refresh_token;
91
+ const expires = body.expires_in;
92
+ return {
93
+ accessToken: access,
94
+ ...(typeof refresh === 'string' ? { refreshToken: refresh } : {}),
95
+ ...(typeof expires === 'number'
96
+ ? { expiresAt: Date.now() + expires * 1000 }
97
+ : {}),
98
+ };
99
+ }
100
+
101
+ async function postToken(
102
+ server: OAuthServer,
103
+ client: OAuthClient,
104
+ form: Record<string, string>,
105
+ ): Promise<OAuthTokens> {
106
+ const params = new URLSearchParams({
107
+ ...form,
108
+ client_id: client.clientId,
109
+ ...(client.clientSecret === undefined
110
+ ? {}
111
+ : { client_secret: client.clientSecret }),
112
+ });
113
+ let res: Response;
114
+ try {
115
+ res = await fetch(server.tokenEndpoint, {
116
+ method: 'POST',
117
+ redirect: 'manual',
118
+ signal: AbortSignal.timeout(TOKEN_TIMEOUT_MS),
119
+ headers: {
120
+ 'content-type': 'application/x-www-form-urlencoded',
121
+ accept: 'application/json',
122
+ },
123
+ body: params.toString(),
124
+ });
125
+ } catch {
126
+ throw refused('Metro could not reach that server to finish signing in.');
127
+ }
128
+ const body: unknown = await res.json().catch(() => null);
129
+ if (!res.ok || !isRecord(body))
130
+ throw refused('That server refused to complete the sign-in.');
131
+ return tokensFrom(body);
132
+ }
133
+
134
+ export function exchangeCode(
135
+ server: OAuthServer,
136
+ client: OAuthClient,
137
+ input: { code: string; redirectUri: string; verifier: string; resource: string },
138
+ ): Promise<OAuthTokens> {
139
+ return postToken(server, client, {
140
+ grant_type: 'authorization_code',
141
+ code: input.code,
142
+ redirect_uri: input.redirectUri,
143
+ code_verifier: input.verifier,
144
+ resource: input.resource,
145
+ });
146
+ }
147
+
148
+ export function refreshTokens(
149
+ server: OAuthServer,
150
+ client: OAuthClient,
151
+ refreshToken: string,
152
+ resource: string,
153
+ ): Promise<OAuthTokens> {
154
+ return postToken(server, client, {
155
+ grant_type: 'refresh_token',
156
+ refresh_token: refreshToken,
157
+ resource,
158
+ });
159
+ }
@@ -0,0 +1,117 @@
1
+ import { ConnectorVerifyError, parseConnectorUrl } from './connector-verify.js';
2
+ import { isRecord } from './is-record.js';
3
+
4
+ const DISCOVERY_TIMEOUT_MS = 10_000;
5
+
6
+ export interface OAuthServer {
7
+ issuer: string;
8
+ authorizationEndpoint: string;
9
+ tokenEndpoint: string;
10
+ registrationEndpoint: string | null;
11
+ supportsS256: boolean;
12
+ }
13
+
14
+ function refused(message: string): ConnectorVerifyError {
15
+ return new ConnectorVerifyError(message, 400);
16
+ }
17
+
18
+ const str = (value: unknown): string =>
19
+ typeof value === 'string' ? value : '';
20
+
21
+ async function getJson(url: URL): Promise<Record<string, unknown> | null> {
22
+ let res: Response;
23
+ try {
24
+ res = await fetch(url, {
25
+ redirect: 'manual',
26
+ signal: AbortSignal.timeout(DISCOVERY_TIMEOUT_MS),
27
+ headers: { accept: 'application/json' },
28
+ });
29
+ } catch {
30
+ return null;
31
+ }
32
+ if (!res.ok) {
33
+ await res.body?.cancel().catch(() => undefined);
34
+ return null;
35
+ }
36
+ const body: unknown = await res.json().catch(() => null);
37
+ return isRecord(body) ? body : null;
38
+ }
39
+
40
+ function wellKnown(base: URL, suffix: string): URL {
41
+ const url = new URL(base.toString());
42
+ url.pathname = `/.well-known/${suffix}`;
43
+ url.search = '';
44
+ url.hash = '';
45
+ return url;
46
+ }
47
+
48
+ export function resourceMetadataUrls(resource: URL): URL[] {
49
+ const path = resource.pathname.replace(/\/$/, '');
50
+ const urls = [wellKnown(resource, 'oauth-protected-resource')];
51
+ if (path !== '')
52
+ urls.unshift(
53
+ new URL(`/.well-known/oauth-protected-resource${path}`, resource.origin),
54
+ );
55
+ return urls;
56
+ }
57
+
58
+ export async function advertisesOAuth(resource: URL): Promise<boolean> {
59
+ for (const candidate of resourceMetadataUrls(resource)) {
60
+ const body = await getJson(candidate);
61
+ const servers: unknown = body?.authorization_servers;
62
+ const first: unknown = Array.isArray(servers) ? servers[0] : undefined;
63
+ if (typeof first === 'string' && first !== '') return true;
64
+ }
65
+ return false;
66
+ }
67
+
68
+ async function authServerFor(resource: URL): Promise<URL> {
69
+ for (const candidate of resourceMetadataUrls(resource)) {
70
+ const body = await getJson(candidate);
71
+ const servers: unknown = body?.authorization_servers;
72
+ const first: unknown = Array.isArray(servers) ? servers[0] : undefined;
73
+ if (typeof first === 'string' && first !== '')
74
+ return parseConnectorUrl(first);
75
+ }
76
+ return parseConnectorUrl(resource.origin);
77
+ }
78
+
79
+ function toServer(body: Record<string, unknown>, issuer: URL): OAuthServer {
80
+ const authorizationEndpoint = str(body.authorization_endpoint);
81
+ const tokenEndpoint = str(body.token_endpoint);
82
+ if (authorizationEndpoint === '' || tokenEndpoint === '')
83
+ throw refused(
84
+ `${issuer.hostname} advertises OAuth but not where to sign in.`,
85
+ );
86
+ const methods = body.code_challenge_methods_supported;
87
+ const registration = str(body.registration_endpoint);
88
+ return {
89
+ issuer: str(body.issuer) === '' ? issuer.origin : str(body.issuer),
90
+ authorizationEndpoint,
91
+ tokenEndpoint,
92
+ registrationEndpoint: registration === '' ? null : registration,
93
+ supportsS256: Array.isArray(methods) ? methods.includes('S256') : false,
94
+ };
95
+ }
96
+
97
+ export function authServerMetadataUrls(issuer: URL): URL[] {
98
+ const path = issuer.pathname.replace(/\/$/, '');
99
+ const suffixes = ['oauth-authorization-server', 'openid-configuration'];
100
+ const urls = suffixes.map((suffix) => wellKnown(issuer, suffix));
101
+ if (path === '') return urls;
102
+ const aware = suffixes.map(
103
+ (suffix) => new URL(`/.well-known/${suffix}${path}`, issuer.origin),
104
+ );
105
+ return [...aware, new URL(`${path}/.well-known/openid-configuration`, issuer.origin), ...urls];
106
+ }
107
+
108
+ export async function discoverOAuth(resource: URL): Promise<OAuthServer> {
109
+ const issuer = await authServerFor(resource);
110
+ for (const candidate of authServerMetadataUrls(issuer)) {
111
+ const body = await getJson(candidate);
112
+ if (body !== null) return toServer(body, issuer);
113
+ }
114
+ throw refused(
115
+ `${resource.hostname} needs authorization, but publishes no OAuth metadata Metro can use.`,
116
+ );
117
+ }
@@ -0,0 +1,65 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import type { OAuthClient } from './oauth-client.js';
3
+ import type { OAuthServer } from './oauth-discovery.js';
4
+
5
+ const TTL_MS = 10 * 60_000;
6
+ const SWEEP_MS = 60_000;
7
+ const MAX_PENDING = 100;
8
+
9
+ export interface PendingAuth {
10
+ subject: string;
11
+ name: string;
12
+ url: string;
13
+ resource: string;
14
+ returnTo: string;
15
+ verifier: string;
16
+ server: OAuthServer;
17
+ client: OAuthClient;
18
+ connectorId: string;
19
+ }
20
+
21
+ interface Entry extends PendingAuth {
22
+ expiresAt: number;
23
+ }
24
+
25
+ const pending = new Map<string, Entry>();
26
+ let sweeper: ReturnType<typeof setInterval> | null = null;
27
+
28
+ function sweep(now = Date.now()): void {
29
+ for (const [state, entry] of pending)
30
+ if (entry.expiresAt <= now) pending.delete(state);
31
+ }
32
+
33
+ function ensureSweeper(): void {
34
+ if (sweeper !== null) return;
35
+ sweeper = setInterval(sweep, SWEEP_MS);
36
+ sweeper.unref?.();
37
+ }
38
+
39
+ export function startPending(entry: PendingAuth, now = Date.now()): string {
40
+ sweep(now);
41
+ while (pending.size >= MAX_PENDING) {
42
+ const oldest = pending.keys().next();
43
+ if (oldest.done) break;
44
+ pending.delete(oldest.value);
45
+ }
46
+ const state = randomBytes(32).toString('base64url');
47
+ pending.set(state, { ...entry, expiresAt: now + TTL_MS });
48
+ ensureSweeper();
49
+ return state;
50
+ }
51
+
52
+ export function takePending(
53
+ state: string,
54
+ now = Date.now(),
55
+ ): PendingAuth | undefined {
56
+ const entry = pending.get(state);
57
+ if (entry === undefined) return undefined;
58
+ pending.delete(state);
59
+ return entry.expiresAt > now ? entry : undefined;
60
+ }
61
+
62
+ export function pendingCount(): number {
63
+ sweep();
64
+ return pending.size;
65
+ }