@stage-labs/metro 0.1.0-beta.2 → 0.1.0-beta.200

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 (391) hide show
  1. package/dist/api.js +11 -0
  2. package/dist/claude-settings.js +40 -0
  3. package/dist/claude.js +163 -0
  4. package/dist/cli.js +77 -567
  5. package/dist/control.js +74 -0
  6. package/dist/hold.js +211 -0
  7. package/dist/local.js +43 -0
  8. package/dist/mcp-config.js +25 -0
  9. package/dist/node-name.js +71 -0
  10. package/dist/onboarding.js +44 -0
  11. package/dist/provider-flags.js +9 -0
  12. package/dist/route.js +58 -0
  13. package/dist/runtime-install.js +97 -0
  14. package/dist/runtime.js +74 -0
  15. package/dist/serve.js +158 -0
  16. package/dist/service.js +243 -0
  17. package/dist/tail.js +102 -144
  18. package/dist/update.js +45 -0
  19. package/dist/version.js +97 -0
  20. package/package.json +14 -23
  21. package/runtime/marketplace/.claude-plugin/marketplace.json +12 -0
  22. package/runtime/marketplace/plugin/.claude-plugin/plugin.json +17 -0
  23. package/runtime/marketplace/plugin/.mcp.json +1 -0
  24. package/runtime/marketplace/plugin/bin/guard.mjs +71 -0
  25. package/runtime/marketplace/plugin/bin/metro-plugin.mjs +30 -0
  26. package/runtime/marketplace/plugin/bin/policy.mjs +97 -0
  27. package/runtime/marketplace/plugin/bin/session-start.mjs +31 -0
  28. package/runtime/marketplace/plugin/hooks/hooks.json +25 -0
  29. package/runtime/marketplace/plugin/orchestrator.md +24 -0
  30. package/runtime/node_modules/@metro-labs/core/package.json +47 -0
  31. package/runtime/node_modules/@metro-labs/core/src/endpoints.ts +7 -0
  32. package/runtime/node_modules/@metro-labs/core/src/events.ts +119 -0
  33. package/runtime/node_modules/@metro-labs/core/src/identity.ts +7 -0
  34. package/runtime/node_modules/@metro-labs/core/src/ids.ts +24 -0
  35. package/runtime/node_modules/@metro-labs/core/src/is-record.ts +2 -0
  36. package/runtime/node_modules/@metro-labs/core/src/lines.ts +91 -0
  37. package/runtime/node_modules/@metro-labs/core/src/log.ts +42 -0
  38. package/runtime/node_modules/@metro-labs/core/src/protocol.ts +227 -0
  39. package/runtime/node_modules/@metro-labs/core/src/secure-fs.ts +74 -0
  40. package/runtime/node_modules/@metro-labs/core/src/station-names.ts +13 -0
  41. package/runtime/node_modules/@metro-labs/core/src/stations/account-files.ts +41 -0
  42. package/runtime/node_modules/@metro-labs/core/src/stations/account-store.ts +60 -0
  43. package/runtime/node_modules/@metro-labs/core/src/stations/attachments.ts +196 -0
  44. package/runtime/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +88 -0
  45. package/runtime/node_modules/@metro-labs/core/src/stations/profile.ts +58 -0
  46. package/runtime/node_modules/@metro-labs/core/src/stations/sender-profile.ts +64 -0
  47. package/runtime/node_modules/@metro-labs/core/src/stations/station-runtime.ts +61 -0
  48. package/runtime/node_modules/@metro-labs/core/src/stations/train-boot.ts +42 -0
  49. package/runtime/node_modules/@metro-labs/core/src/stations/train-events.ts +89 -0
  50. package/runtime/node_modules/@metro-labs/core/src/stations/types.ts +123 -0
  51. package/runtime/node_modules/@metro-labs/core/src/str.ts +5 -0
  52. package/runtime/node_modules/@metro-labs/core/src/tickets.ts +37 -0
  53. package/runtime/node_modules/@metro-labs/core/src/train-error.ts +53 -0
  54. package/runtime/node_modules/@metro-labs/core/src/version.ts +2 -0
  55. package/runtime/node_modules/@metro-labs/daemon/package.json +48 -0
  56. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/agent-fs.ts +134 -0
  57. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/files-api.ts +12 -0
  58. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/files.ts +119 -0
  59. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/home-fs.ts +113 -0
  60. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/job-env.ts +51 -0
  61. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/provision.ts +91 -0
  62. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedule-detail.ts +100 -0
  63. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules-api.ts +27 -0
  64. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules.ts +252 -0
  65. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/unit-files.ts +61 -0
  66. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/user.ts +91 -0
  67. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/view.ts +87 -0
  68. package/runtime/node_modules/@metro-labs/daemon/src/agents/account-attach.ts +19 -0
  69. package/runtime/node_modules/@metro-labs/daemon/src/agents/account-lookups.ts +55 -0
  70. package/runtime/node_modules/@metro-labs/daemon/src/agents/account-routes.ts +76 -0
  71. package/runtime/node_modules/@metro-labs/daemon/src/agents/accounts-api.ts +357 -0
  72. package/runtime/node_modules/@metro-labs/daemon/src/agents/admin.ts +30 -0
  73. package/runtime/node_modules/@metro-labs/daemon/src/agents/allowlist.ts +59 -0
  74. package/runtime/node_modules/@metro-labs/daemon/src/agents/api.ts +131 -0
  75. package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +132 -0
  76. package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +305 -0
  77. package/runtime/node_modules/@metro-labs/daemon/src/agents/files.ts +168 -0
  78. package/runtime/node_modules/@metro-labs/daemon/src/agents/keys.ts +35 -0
  79. package/runtime/node_modules/@metro-labs/daemon/src/agents/map.ts +128 -0
  80. package/runtime/node_modules/@metro-labs/daemon/src/agents/policy-route.ts +27 -0
  81. package/runtime/node_modules/@metro-labs/daemon/src/agents/scope.ts +64 -0
  82. package/runtime/node_modules/@metro-labs/daemon/src/agents/sender-cards.ts +86 -0
  83. package/runtime/node_modules/@metro-labs/daemon/src/agents/senders.ts +38 -0
  84. package/runtime/node_modules/@metro-labs/daemon/src/approvals/api.ts +55 -0
  85. package/runtime/node_modules/@metro-labs/daemon/src/approvals/needs.ts +2 -0
  86. package/runtime/node_modules/@metro-labs/daemon/src/approvals/pending.ts +149 -0
  87. package/runtime/node_modules/@metro-labs/daemon/src/approvals/preview.ts +66 -0
  88. package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +221 -0
  89. package/runtime/node_modules/@metro-labs/daemon/src/boot/connect-hint.ts +39 -0
  90. package/runtime/node_modules/@metro-labs/daemon/src/boot/crash-guard.ts +49 -0
  91. package/runtime/node_modules/@metro-labs/daemon/src/boot/local-owner.ts +21 -0
  92. package/runtime/node_modules/@metro-labs/daemon/src/boot/paths.ts +106 -0
  93. package/runtime/node_modules/@metro-labs/daemon/src/channels/addressed.ts +26 -0
  94. package/runtime/node_modules/@metro-labs/daemon/src/channels/dedupe.ts +12 -0
  95. package/runtime/node_modules/@metro-labs/daemon/src/channels/inbound.ts +370 -0
  96. package/runtime/node_modules/@metro-labs/daemon/src/channels/media-note.ts +90 -0
  97. package/runtime/node_modules/@metro-labs/daemon/src/channels/pending.ts +78 -0
  98. package/runtime/node_modules/@metro-labs/daemon/src/channels/react-note.ts +13 -0
  99. package/runtime/node_modules/@metro-labs/daemon/src/channels/relay.ts +129 -0
  100. package/runtime/node_modules/@metro-labs/daemon/src/channels/reply-id.ts +1 -0
  101. package/runtime/node_modules/@metro-labs/daemon/src/channels/webhook-note.ts +51 -0
  102. package/runtime/node_modules/@metro-labs/daemon/src/claude/api.ts +355 -0
  103. package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +359 -0
  104. package/runtime/node_modules/@metro-labs/daemon/src/claude/login.ts +208 -0
  105. package/runtime/node_modules/@metro-labs/daemon/src/claude/memory.ts +82 -0
  106. package/runtime/node_modules/@metro-labs/daemon/src/claude/onboarding.ts +60 -0
  107. package/runtime/node_modules/@metro-labs/daemon/src/claude/plugin-install.ts +149 -0
  108. package/runtime/node_modules/@metro-labs/daemon/src/claude/session-files.ts +75 -0
  109. package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +283 -0
  110. package/runtime/node_modules/@metro-labs/daemon/src/claude/settings.ts +109 -0
  111. package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +271 -0
  112. package/runtime/node_modules/@metro-labs/daemon/src/claude/skills.ts +120 -0
  113. package/runtime/node_modules/@metro-labs/daemon/src/claude/version.ts +113 -0
  114. package/runtime/node_modules/@metro-labs/daemon/src/connectors/api.ts +306 -0
  115. package/runtime/node_modules/@metro-labs/daemon/src/connectors/config.ts +176 -0
  116. package/runtime/node_modules/@metro-labs/daemon/src/connectors/gates.ts +72 -0
  117. package/runtime/node_modules/@metro-labs/daemon/src/connectors/health.ts +18 -0
  118. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-client.ts +159 -0
  119. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-discovery.ts +135 -0
  120. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-routes.ts +158 -0
  121. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth.ts +166 -0
  122. package/runtime/node_modules/@metro-labs/daemon/src/connectors/plugin-sync.ts +131 -0
  123. package/runtime/node_modules/@metro-labs/daemon/src/connectors/reach.ts +27 -0
  124. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-policy.ts +90 -0
  125. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-target.ts +29 -0
  126. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay.ts +348 -0
  127. package/runtime/node_modules/@metro-labs/daemon/src/connectors/store.ts +358 -0
  128. package/runtime/node_modules/@metro-labs/daemon/src/connectors/tools.ts +81 -0
  129. package/runtime/node_modules/@metro-labs/daemon/src/connectors/url.ts +37 -0
  130. package/runtime/node_modules/@metro-labs/daemon/src/connectors/verify.ts +106 -0
  131. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-grant.ts +99 -0
  132. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-owner.ts +32 -0
  133. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-reaper.ts +174 -0
  134. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-serve.ts +123 -0
  135. package/runtime/node_modules/@metro-labs/daemon/src/files/upload-api.ts +278 -0
  136. package/runtime/node_modules/@metro-labs/daemon/src/files/upload-store.ts +181 -0
  137. package/runtime/node_modules/@metro-labs/daemon/src/gateway/bedrock.ts +233 -0
  138. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-auth.ts +208 -0
  139. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-device.ts +91 -0
  140. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-stream.ts +173 -0
  141. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-translate.ts +194 -0
  142. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +137 -0
  143. package/runtime/node_modules/@metro-labs/daemon/src/gateway/effort.ts +77 -0
  144. package/runtime/node_modules/@metro-labs/daemon/src/gateway/eventstream.ts +123 -0
  145. package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +237 -0
  146. package/runtime/node_modules/@metro-labs/daemon/src/gateway/frames.ts +118 -0
  147. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +274 -0
  148. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-auth.ts +160 -0
  149. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-client.ts +46 -0
  150. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-setup.ts +150 -0
  151. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-stream.ts +139 -0
  152. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-translate.ts +223 -0
  153. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini.ts +208 -0
  154. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +199 -0
  155. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-config.ts +299 -0
  156. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-signin.ts +193 -0
  157. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-store.ts +70 -0
  158. package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter.ts +90 -0
  159. package/runtime/node_modules/@metro-labs/daemon/src/gateway/pkce.ts +36 -0
  160. package/runtime/node_modules/@metro-labs/daemon/src/gateway/provider-models.ts +66 -0
  161. package/runtime/node_modules/@metro-labs/daemon/src/gateway/served.ts +18 -0
  162. package/runtime/node_modules/@metro-labs/daemon/src/gateway/subscription.ts +120 -0
  163. package/runtime/node_modules/@metro-labs/daemon/src/gateway/text.ts +15 -0
  164. package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage.ts +244 -0
  165. package/runtime/node_modules/@metro-labs/daemon/src/mcp/accounts.ts +156 -0
  166. package/runtime/node_modules/@metro-labs/daemon/src/mcp/attachment-schema.ts +91 -0
  167. package/runtime/node_modules/@metro-labs/daemon/src/mcp/call-tools.ts +226 -0
  168. package/runtime/node_modules/@metro-labs/daemon/src/mcp/ctx.ts +62 -0
  169. package/runtime/node_modules/@metro-labs/daemon/src/mcp/event-store.ts +90 -0
  170. package/runtime/node_modules/@metro-labs/daemon/src/mcp/group-tools.ts +129 -0
  171. package/runtime/node_modules/@metro-labs/daemon/src/mcp/index.ts +137 -0
  172. package/runtime/node_modules/@metro-labs/daemon/src/mcp/instructions.ts +19 -0
  173. package/runtime/node_modules/@metro-labs/daemon/src/mcp/member-tools.ts +72 -0
  174. package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-prompt.ts +87 -0
  175. package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-relay.ts +79 -0
  176. package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-gate.ts +83 -0
  177. package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-snapshot.ts +79 -0
  178. package/runtime/node_modules/@metro-labs/daemon/src/mcp/profile-lookup.ts +60 -0
  179. package/runtime/node_modules/@metro-labs/daemon/src/mcp/profile-tool.ts +96 -0
  180. package/runtime/node_modules/@metro-labs/daemon/src/mcp/raw-get-stream.ts +207 -0
  181. package/runtime/node_modules/@metro-labs/daemon/src/mcp/read-tool.ts +87 -0
  182. package/runtime/node_modules/@metro-labs/daemon/src/mcp/request-identity.ts +46 -0
  183. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-slot.ts +48 -0
  184. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session.ts +202 -0
  185. package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-catalog.ts +88 -0
  186. package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-def.ts +9 -0
  187. package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-dispatch.ts +225 -0
  188. package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-schemas.ts +333 -0
  189. package/runtime/node_modules/@metro-labs/daemon/src/mcp/upload-tool.ts +61 -0
  190. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/api.ts +43 -0
  191. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/helper-script.ts +126 -0
  192. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/move.ts +165 -0
  193. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/privilege.ts +48 -0
  194. package/runtime/node_modules/@metro-labs/daemon/src/monitor/api.ts +132 -0
  195. package/runtime/node_modules/@metro-labs/daemon/src/net/tunnel.ts +316 -0
  196. package/runtime/node_modules/@metro-labs/daemon/src/policy/policy.ts +118 -0
  197. package/runtime/node_modules/@metro-labs/daemon/src/routes/bearer.ts +35 -0
  198. package/runtime/node_modules/@metro-labs/daemon/src/routes/body.ts +24 -0
  199. package/runtime/node_modules/@metro-labs/daemon/src/routes/http.ts +365 -0
  200. package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +208 -0
  201. package/runtime/node_modules/@metro-labs/daemon/src/routes/session-apis.ts +71 -0
  202. package/runtime/node_modules/@metro-labs/daemon/src/routes/session.ts +29 -0
  203. package/runtime/node_modules/@metro-labs/daemon/src/routes/threema-callback.ts +103 -0
  204. package/runtime/node_modules/@metro-labs/daemon/src/server/control.ts +39 -0
  205. package/runtime/node_modules/@metro-labs/daemon/src/server/machine.ts +58 -0
  206. package/runtime/node_modules/@metro-labs/daemon/src/server/owner.ts +26 -0
  207. package/runtime/node_modules/@metro-labs/daemon/src/server/update.ts +90 -0
  208. package/runtime/node_modules/@metro-labs/daemon/src/server.ts +2 -0
  209. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-driver.ts +42 -0
  210. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-inline.ts +128 -0
  211. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-interactive.ts +134 -0
  212. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-outlook.ts +63 -0
  213. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-resolve.ts +225 -0
  214. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-session.ts +288 -0
  215. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-xmtp.ts +111 -0
  216. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach.ts +248 -0
  217. package/runtime/node_modules/@metro-labs/daemon/src/stations/materialize.ts +265 -0
  218. package/runtime/node_modules/@metro-labs/daemon/src/stations/registry.ts +51 -0
  219. package/runtime/node_modules/@metro-labs/daemon/src/stations/runtime-deps.ts +82 -0
  220. package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor-io.ts +97 -0
  221. package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor.ts +320 -0
  222. package/runtime/node_modules/@metro-labs/daemon/src/stations/threema-callbacks.ts +32 -0
  223. package/runtime/node_modules/@metro-labs/daemon/src/stations/train-call.ts +22 -0
  224. package/runtime/node_modules/@metro-labs/daemon/src/stations/webhook-endpoints.ts +51 -0
  225. package/runtime/node_modules/@metro-labs/daemon/src/terminal/api.ts +84 -0
  226. package/runtime/node_modules/@metro-labs/daemon/src/terminal/socket.ts +124 -0
  227. package/runtime/node_modules/@metro-labs/daemon/src/terminal/tickets.ts +15 -0
  228. package/runtime/node_modules/@metro-labs/daemon/src/vault/api.ts +45 -0
  229. package/runtime/node_modules/@metro-labs/daemon/src/vault/config.ts +51 -0
  230. package/runtime/node_modules/@metro-labs/daemon/src/vault/firewall.ts +48 -0
  231. package/runtime/node_modules/@metro-labs/daemon/src/vault/index.ts +96 -0
  232. package/runtime/node_modules/@metro-labs/daemon/src/vault/install.ts +56 -0
  233. package/runtime/node_modules/@metro-labs/daemon/src/vault/paths.ts +26 -0
  234. package/runtime/node_modules/@metro-labs/daemon/src/vault/proxy.ts +148 -0
  235. package/runtime/node_modules/@metro-labs/daemon/src/vault/store.ts +130 -0
  236. package/runtime/node_modules/@metro-labs/daemon/src/vault/trust.ts +61 -0
  237. package/runtime/node_modules/@metro-labs/discord-bot/package.json +26 -0
  238. package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +117 -0
  239. package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +240 -0
  240. package/runtime/node_modules/@metro-labs/discord-bot/src/api-base.ts +1 -0
  241. package/runtime/node_modules/@metro-labs/discord-bot/src/attachments.ts +29 -0
  242. package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +174 -0
  243. package/runtime/node_modules/@metro-labs/discord-bot/src/group-actions.ts +50 -0
  244. package/runtime/node_modules/@metro-labs/discord-bot/src/group.ts +115 -0
  245. package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +122 -0
  246. package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +215 -0
  247. package/runtime/node_modules/@metro-labs/discord-bot/src/profile.ts +68 -0
  248. package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +43 -0
  249. package/runtime/node_modules/@metro-labs/discord-bot/src/station.ts +22 -0
  250. package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +82 -0
  251. package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +1 -0
  252. package/runtime/node_modules/@metro-labs/http/package.json +29 -0
  253. package/runtime/node_modules/@metro-labs/http/src/api-error.ts +8 -0
  254. package/runtime/node_modules/@metro-labs/http/src/api-http.ts +138 -0
  255. package/runtime/node_modules/@metro-labs/http/src/cors.ts +29 -0
  256. package/runtime/node_modules/@metro-labs/http/src/mode-api.ts +32 -0
  257. package/runtime/node_modules/@metro-labs/http/src/return-to.ts +21 -0
  258. package/runtime/node_modules/@metro-labs/http/src/workos-token.ts +149 -0
  259. package/runtime/node_modules/@metro-labs/outlook/package.json +25 -0
  260. package/runtime/node_modules/@metro-labs/outlook/src/accounts.ts +107 -0
  261. package/runtime/node_modules/@metro-labs/outlook/src/actions.ts +20 -0
  262. package/runtime/node_modules/@metro-labs/outlook/src/attachments.ts +97 -0
  263. package/runtime/node_modules/@metro-labs/outlook/src/auth.ts +150 -0
  264. package/runtime/node_modules/@metro-labs/outlook/src/browser.ts +53 -0
  265. package/runtime/node_modules/@metro-labs/outlook/src/config.ts +36 -0
  266. package/runtime/node_modules/@metro-labs/outlook/src/format.ts +141 -0
  267. package/runtime/node_modules/@metro-labs/outlook/src/graph.ts +60 -0
  268. package/runtime/node_modules/@metro-labs/outlook/src/html.ts +72 -0
  269. package/runtime/node_modules/@metro-labs/outlook/src/inbound.ts +104 -0
  270. package/runtime/node_modules/@metro-labs/outlook/src/index.ts +12 -0
  271. package/runtime/node_modules/@metro-labs/outlook/src/login.ts +150 -0
  272. package/runtime/node_modules/@metro-labs/outlook/src/me.ts +41 -0
  273. package/runtime/node_modules/@metro-labs/outlook/src/outbound.ts +116 -0
  274. package/runtime/node_modules/@metro-labs/outlook/src/query.ts +119 -0
  275. package/runtime/node_modules/@metro-labs/outlook/src/state.ts +43 -0
  276. package/runtime/node_modules/@metro-labs/outlook/src/station.ts +15 -0
  277. package/runtime/node_modules/@metro-labs/outlook/src/trust.ts +70 -0
  278. package/runtime/node_modules/@metro-labs/telegram/package.json +26 -0
  279. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +80 -0
  280. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +256 -0
  281. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +87 -0
  282. package/runtime/node_modules/@metro-labs/telegram/src/client.ts +77 -0
  283. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +128 -0
  284. package/runtime/node_modules/@metro-labs/telegram/src/group.ts +261 -0
  285. package/runtime/node_modules/@metro-labs/telegram/src/history.ts +52 -0
  286. package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +57 -0
  287. package/runtime/node_modules/@metro-labs/telegram/src/index.ts +7 -0
  288. package/runtime/node_modules/@metro-labs/telegram/src/login.ts +166 -0
  289. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +80 -0
  290. package/runtime/node_modules/@metro-labs/telegram/src/members.ts +75 -0
  291. package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +22 -0
  292. package/runtime/node_modules/@metro-labs/telegram/src/profile.ts +70 -0
  293. package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +167 -0
  294. package/runtime/node_modules/@metro-labs/telegram/src/station.ts +27 -0
  295. package/runtime/node_modules/@metro-labs/telegram/src/types.ts +6 -0
  296. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +1 -0
  297. package/runtime/node_modules/@metro-labs/telegram-bot/package.json +25 -0
  298. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +119 -0
  299. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +246 -0
  300. package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
  301. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +173 -0
  302. package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +103 -0
  303. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +95 -0
  304. package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
  305. package/runtime/node_modules/@metro-labs/telegram-bot/src/profile.ts +65 -0
  306. package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +20 -0
  307. package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +56 -0
  308. package/runtime/node_modules/@metro-labs/telegram-bot/src/verify.ts +38 -0
  309. package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +1 -0
  310. package/runtime/node_modules/@metro-labs/threema/package.json +26 -0
  311. package/runtime/node_modules/@metro-labs/threema/src/accounts.ts +106 -0
  312. package/runtime/node_modules/@metro-labs/threema/src/actions.ts +134 -0
  313. package/runtime/node_modules/@metro-labs/threema/src/api.ts +152 -0
  314. package/runtime/node_modules/@metro-labs/threema/src/crypto.ts +100 -0
  315. package/runtime/node_modules/@metro-labs/threema/src/files.ts +58 -0
  316. package/runtime/node_modules/@metro-labs/threema/src/format.ts +169 -0
  317. package/runtime/node_modules/@metro-labs/threema/src/groups.ts +96 -0
  318. package/runtime/node_modules/@metro-labs/threema/src/ids.ts +21 -0
  319. package/runtime/node_modules/@metro-labs/threema/src/inbound.ts +89 -0
  320. package/runtime/node_modules/@metro-labs/threema/src/index.ts +24 -0
  321. package/runtime/node_modules/@metro-labs/threema/src/messages.ts +264 -0
  322. package/runtime/node_modules/@metro-labs/threema/src/outbound.ts +160 -0
  323. package/runtime/node_modules/@metro-labs/threema/src/station.ts +13 -0
  324. package/runtime/node_modules/@metro-labs/threema/src/verify.ts +117 -0
  325. package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
  326. package/runtime/node_modules/@metro-labs/webhook/src/station.ts +37 -0
  327. package/runtime/node_modules/@metro-labs/whatsapp/package.json +27 -0
  328. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +66 -0
  329. package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
  330. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +213 -0
  331. package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +72 -0
  332. package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +129 -0
  333. package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +388 -0
  334. package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
  335. package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +83 -0
  336. package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +40 -0
  337. package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +7 -0
  338. package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +59 -0
  339. package/runtime/node_modules/@metro-labs/whatsapp/src/logger.ts +60 -0
  340. package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +176 -0
  341. package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +84 -0
  342. package/runtime/node_modules/@metro-labs/whatsapp/src/names.ts +28 -0
  343. package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +22 -0
  344. package/runtime/node_modules/@metro-labs/whatsapp/src/outbox.ts +38 -0
  345. package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +185 -0
  346. package/runtime/node_modules/@metro-labs/whatsapp/src/resolve.ts +52 -0
  347. package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +24 -0
  348. package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +94 -0
  349. package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +5 -0
  350. package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +1 -0
  351. package/runtime/node_modules/@metro-labs/xmtp/package.json +34 -0
  352. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +114 -0
  353. package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +110 -0
  354. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +101 -0
  355. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +118 -0
  356. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +244 -0
  357. package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +107 -0
  358. package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +171 -0
  359. package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +239 -0
  360. package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +316 -0
  361. package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +69 -0
  362. package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +34 -0
  363. package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +56 -0
  364. package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +47 -0
  365. package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +122 -0
  366. package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +139 -0
  367. package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
  368. package/runtime/node_modules/@metro-labs/xmtp/src/names.ts +61 -0
  369. package/runtime/node_modules/@metro-labs/xmtp/src/profile.ts +153 -0
  370. package/runtime/node_modules/@metro-labs/xmtp/src/sender.ts +112 -0
  371. package/runtime/node_modules/@metro-labs/xmtp/src/smart.ts +72 -0
  372. package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +17 -0
  373. package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +79 -0
  374. package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +198 -0
  375. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +88 -0
  376. package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +98 -0
  377. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +51 -0
  378. package/runtime/runtime.json +3 -0
  379. package/runtime/server.ts +1 -0
  380. package/runtime/stations.json +37 -0
  381. package/runtime/trains/.keep +0 -0
  382. package/LICENSE +0 -21
  383. package/README.md +0 -78
  384. package/dist/channels/discord.js +0 -148
  385. package/dist/channels/telegram.js +0 -137
  386. package/dist/lib/address.js +0 -21
  387. package/dist/lib/codex-rc.js +0 -274
  388. package/dist/lib/dotenv.js +0 -31
  389. package/dist/log.js +0 -12
  390. package/dist/paths.js +0 -45
  391. package/skills/metro/SKILL.md +0 -122
@@ -0,0 +1,107 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ import { makeAccountStore, resolveAccountId, type Die } from '@metro-labs/core/stations/account-store';
4
+ import { TrainError } from '@metro-labs/core/train-error';
5
+ import { refreshTokens, type FetchLike } from './auth.js';
6
+ import { conversationOfLine } from './format.js';
7
+ import { loadState, saveState, type AccountState } from './state.js';
8
+
9
+ const ACCOUNTS_FILE = process.env.OUTLOOK_ACCOUNTS_FILE ?? join(homedir(), '.metro', 'outlook-accounts.json');
10
+
11
+ const RENEW_BEFORE_MS = 5 * 60_000;
12
+
13
+ export interface AccountConfig {
14
+ id: string;
15
+ accountEmail: string;
16
+ refreshToken: string;
17
+ accessToken?: string;
18
+ expiresAt?: number;
19
+ tenantId?: string | null;
20
+ deltaLink?: string | null;
21
+ includeAutomated?: boolean;
22
+ }
23
+
24
+ function checkAccount(a: AccountConfig, die: Die): void {
25
+ if (!a.id) die('account missing id');
26
+ if (typeof a.accountEmail !== 'string' || !a.accountEmail.includes('@')) die(`account '${a.id}' has no mailbox address`);
27
+ if (typeof a.refreshToken !== 'string' || a.refreshToken === '') die(`account '${a.id}' has no refresh token`);
28
+ }
29
+
30
+ export const { loadAccounts } = makeAccountStore<AccountConfig>({
31
+ prefix: 'outlook',
32
+ file: ACCOUNTS_FILE,
33
+ validate(raw, die) {
34
+ const seen = new Set<string>();
35
+ for (const a of raw) {
36
+ checkAccount(a, die);
37
+ if (seen.has(a.id)) die(`duplicate account id '${a.id}'`);
38
+ seen.add(a.id);
39
+ }
40
+ },
41
+ });
42
+
43
+ export class Account {
44
+ readonly email: string;
45
+ readonly state: AccountState;
46
+ private refreshing: Promise<string> | null = null;
47
+
48
+ constructor(
49
+ readonly cfg: AccountConfig,
50
+ private readonly fetchImpl: FetchLike = (input, init) => fetch(input, init),
51
+ ) {
52
+ this.email = cfg.accountEmail.toLowerCase();
53
+ this.state = loadState(cfg.id, {
54
+ refreshToken: cfg.refreshToken,
55
+ accessToken: cfg.accessToken ?? '',
56
+ expiresAt: cfg.expiresAt ?? 0,
57
+ deltaLink: cfg.deltaLink ?? null,
58
+ });
59
+ }
60
+
61
+ get id(): string {
62
+ return this.cfg.id;
63
+ }
64
+
65
+ save(): void {
66
+ saveState(this.cfg.id, this.state);
67
+ }
68
+
69
+ token(force = false): Promise<string> {
70
+ const fresh = this.state.accessToken !== '' && this.state.expiresAt - RENEW_BEFORE_MS > Date.now();
71
+ if (fresh && !force) return Promise.resolve(this.state.accessToken);
72
+ this.refreshing ??= this.renew().finally(() => {
73
+ this.refreshing = null;
74
+ });
75
+ return this.refreshing;
76
+ }
77
+
78
+ private async renew(): Promise<string> {
79
+ let tokens;
80
+ try {
81
+ tokens = await refreshTokens(this.state.refreshToken, this.fetchImpl);
82
+ } catch (err) {
83
+ throw new TrainError('outlook_signed_out', err instanceof Error ? err.message : String(err), { retryable: false });
84
+ }
85
+ Object.assign(this.state, tokens);
86
+ this.save();
87
+ return tokens.accessToken;
88
+ }
89
+
90
+ fetch(input: string, init?: RequestInit): Promise<Response> {
91
+ return this.fetchImpl(input, init);
92
+ }
93
+ }
94
+
95
+ export const accounts = new Map<string, Account>();
96
+
97
+ export function accountFor(id: string): Account {
98
+ const acct = accounts.get(id);
99
+ if (!acct) throw new Error(`unknown account '${id}' (have: ${[...accounts.keys()].join(', ')})`);
100
+ return acct;
101
+ }
102
+
103
+ export function accountOf(args: { account?: unknown; line?: unknown }): Account {
104
+ const line = typeof args.line === 'string' && args.line !== '' ? args.line : undefined;
105
+ const account = typeof args.account === 'string' && args.account !== '' ? args.account : undefined;
106
+ return accountFor(resolveAccountId(accounts, { account, line }, (l) => conversationOfLine(l)?.accountId));
107
+ }
@@ -0,0 +1,20 @@
1
+ import { makeStation, respond } from '@metro-labs/core/stations/station-runtime';
2
+ import type { Normalized } from '@metro-labs/core/stations/messaging-normalize';
3
+ import { accounts } from './accounts.js';
4
+ import { MAILBOX_URL } from './config.js';
5
+ import { reply, send } from './outbound.js';
6
+ import { read } from './query.js';
7
+
8
+ type Args = Record<string, unknown>;
9
+
10
+ function listAccounts(id: string): void {
11
+ const list = [...accounts.values()].map((a) => ({ id: a.id, handle: a.email, url: MAILBOX_URL, email: a.email }));
12
+ respond(id, { result: { accounts: list } });
13
+ }
14
+
15
+ const normalizeOutlook = (action: string, args: Args): Normalized => ({ action, args });
16
+
17
+ export const handleCall = makeStation({
18
+ handlers: { accounts: listAccounts, send, reply, read },
19
+ normalize: normalizeOutlook,
20
+ });
@@ -0,0 +1,97 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFile, stat } from 'node:fs/promises';
3
+ import { assertAttachmentSize, kindOf, saveBufferToCache, type SavedAttachment } from '@metro-labs/core/stations/attachments';
4
+ import { TrainError } from '@metro-labs/core/train-error';
5
+ import type { Account } from './accounts.js';
6
+ import type { AttachmentMeta } from './format.js';
7
+ import { graph, graphJson, messagePath } from './graph.js';
8
+
9
+ export const MAX_FILE_BYTES = 3 * 1024 * 1024;
10
+
11
+ const FILE_TYPE = '#microsoft.graph.fileAttachment';
12
+
13
+ export interface GraphAttachment {
14
+ id: string;
15
+ name?: string;
16
+ contentType?: string;
17
+ size?: number;
18
+ isInline?: boolean;
19
+ '@odata.type'?: string;
20
+ }
21
+
22
+ export interface MailFile extends AttachmentMeta {
23
+ id: string;
24
+ }
25
+
26
+ export const metaOf = (f: MailFile): AttachmentMeta => ({
27
+ kind: f.kind,
28
+ name: f.name,
29
+ mime: f.mime,
30
+ ...(f.size === undefined ? {} : { size: f.size }),
31
+ });
32
+
33
+ export async function listFiles(acct: Account, messageId: string): Promise<MailFile[]> {
34
+ const body = await graphJson<{ value?: GraphAttachment[] }>(
35
+ acct,
36
+ `${messagePath(messageId)}/attachments?$select=id,name,contentType,size,isInline`,
37
+ );
38
+ return (body.value ?? [])
39
+ .filter((a) => a['@odata.type'] === undefined || a['@odata.type'] === FILE_TYPE)
40
+ .map((a) => {
41
+ const name = a.name ?? 'attachment';
42
+ const mime = a.contentType ?? 'application/octet-stream';
43
+ return { id: a.id, name, mime, kind: kindOf(mime, name), ...(typeof a.size === 'number' ? { size: a.size } : {}) };
44
+ });
45
+ }
46
+
47
+ const cacheKey = (messageId: string): string => createHash('sha256').update(messageId).digest('hex').slice(0, 16);
48
+
49
+ export async function saveFile(acct: Account, messageId: string, file: MailFile, index: number): Promise<SavedAttachment> {
50
+ if (file.size !== undefined) assertAttachmentSize(file.size);
51
+ const res = await graph(acct, `${messagePath(messageId)}/attachments/${encodeURIComponent(file.id)}/$value`);
52
+ const data = new Uint8Array(await res.arrayBuffer());
53
+ return saveBufferToCache(data, cacheKey(messageId), index, { mime: file.mime, name: file.name });
54
+ }
55
+
56
+ export interface OutgoingFile {
57
+ path: string;
58
+ mime: string;
59
+ name: string;
60
+ }
61
+
62
+ export const filesOf = (raw: unknown): OutgoingFile[] =>
63
+ (Array.isArray(raw) ? raw : [])
64
+ .map((a) => (a ?? {}) as Record<string, unknown>)
65
+ .filter((a) => typeof a.path === 'string' && a.path !== '')
66
+ .map((a) => ({
67
+ path: String(a.path),
68
+ mime: typeof a.mime === 'string' && a.mime !== '' ? a.mime : 'application/octet-stream',
69
+ name: typeof a.name === 'string' && a.name !== '' ? a.name : 'file',
70
+ }));
71
+
72
+ export async function assertSendable(files: OutgoingFile[]): Promise<void> {
73
+ for (const file of files) {
74
+ const { size } = await stat(file.path);
75
+ if (size > MAX_FILE_BYTES)
76
+ throw new TrainError(
77
+ 'outlook_file_too_big',
78
+ `Metro sends Outlook files up to 3 MB for now; ${file.name} is ${(size / 1024 / 1024).toFixed(1)} MB, so share a link to it instead`,
79
+ { retryable: false },
80
+ );
81
+ }
82
+ }
83
+
84
+ export async function attachFile(acct: Account, draftId: string, file: OutgoingFile): Promise<string> {
85
+ const data = await readFile(file.path);
86
+ await graph(acct, `${messagePath(draftId)}/attachments`, {
87
+ method: 'POST',
88
+ headers: { 'content-type': 'application/json' },
89
+ body: JSON.stringify({
90
+ '@odata.type': FILE_TYPE,
91
+ name: file.name,
92
+ contentType: file.mime,
93
+ contentBytes: data.toString('base64'),
94
+ }),
95
+ });
96
+ return kindOf(file.mime, file.path);
97
+ }
@@ -0,0 +1,150 @@
1
+ import { clientId, loginBase, NOT_SET_UP, SCOPES } from './config.js';
2
+
3
+ export class OutlookAuthError extends Error {}
4
+
5
+ export type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
6
+
7
+ export interface Tokens {
8
+ accessToken: string;
9
+ refreshToken: string;
10
+ expiresAt: number;
11
+ }
12
+
13
+ export interface DeviceCode {
14
+ deviceCode: string;
15
+ userCode: string;
16
+ verificationUri: string;
17
+ message: string;
18
+ expiresAt: number;
19
+ intervalMs: number;
20
+ }
21
+
22
+ export type Poll =
23
+ | { kind: 'pending' }
24
+ | { kind: 'slow_down' }
25
+ | { kind: 'done'; tokens: Tokens; tenantId: string | null }
26
+ | { kind: 'failed'; message: string };
27
+
28
+ type Body = Record<string, unknown>;
29
+
30
+ const DEVICE_GRANT = 'urn:ietf:params:oauth:grant-type:device_code';
31
+ const CONSENT_CODES = new Set([65001, 90094, 90095]);
32
+ const POLICY_CODES = new Set([530035, 53003]);
33
+
34
+ const text = (v: unknown): string => (typeof v === 'string' ? v : '');
35
+ const seconds = (v: unknown, fallback: number): number =>
36
+ typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : fallback;
37
+
38
+ export function requireClientId(id = clientId()): string {
39
+ if (id === '') throw new OutlookAuthError(NOT_SET_UP);
40
+ return id;
41
+ }
42
+
43
+ export async function postForm(path: string, fields: Record<string, string>, fetchImpl: FetchLike): Promise<{ status: number; body: Body }> {
44
+ let res: Response;
45
+ try {
46
+ res = await fetchImpl(`${loginBase()}/oauth2/v2.0/${path}`, {
47
+ method: 'POST',
48
+ headers: { 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' },
49
+ body: new URLSearchParams(fields).toString(),
50
+ });
51
+ } catch (err) {
52
+ throw new OutlookAuthError(`Metro could not reach Microsoft: ${err instanceof Error ? err.message : String(err)}`);
53
+ }
54
+ const raw: unknown = await res.json().catch(() => ({}));
55
+ return { status: res.status, body: typeof raw === 'object' && raw !== null ? (raw as Body) : {} };
56
+ }
57
+
58
+ function codesOf(body: Body): number[] {
59
+ const listed = Array.isArray(body.error_codes) ? body.error_codes.filter((c): c is number => typeof c === 'number') : [];
60
+ const named = [...text(body.error_description).matchAll(/AADSTS(\d+)/g)].map((m) => Number(m[1]));
61
+ return [...listed, ...named];
62
+ }
63
+
64
+ const needsConsent = (error: string, codes: number[]): boolean =>
65
+ error === 'consent_required' || codes.some((c) => CONSENT_CODES.has(c));
66
+
67
+ function knownFailure(error: string, codes: number[], id: string): string | null {
68
+ if (needsConsent(error, codes))
69
+ return `An administrator of this Microsoft 365 organization must approve Metro once before it can read this mailbox. They can do it at https://login.microsoftonline.com/organizations/adminconsent?client_id=${id} and then you can start again.`;
70
+ if (codes.some((c) => POLICY_CODES.has(c)))
71
+ return 'Your company blocks this kind of sign-in. Ask your Microsoft 365 administrator, or use the Microsoft sign-in page instead of a code.';
72
+ if (error === 'expired_token' || codes.includes(70020)) return 'The sign-in code expired before it was used. Start again.';
73
+ if (error === 'access_denied' || codes.includes(65004)) return 'The sign-in was declined, so nothing was connected.';
74
+ return null;
75
+ }
76
+
77
+ export function failureOf(body: Body, id = clientId()): string {
78
+ const error = text(body.error);
79
+ const known = knownFailure(error, codesOf(body), id);
80
+ if (known !== null) return known;
81
+ const detail = text(body.error_description).split(/\r?\n/)[0] ?? '';
82
+ return detail === '' ? `Microsoft refused the sign-in (${error === '' ? 'no reason given' : error}).` : `Microsoft refused the sign-in: ${detail}`;
83
+ }
84
+
85
+ function decodeClaims(jwt: string): Body | null {
86
+ const part = jwt.split('.')[1];
87
+ if (part === undefined) return null;
88
+ try {
89
+ const raw: unknown = JSON.parse(Buffer.from(part, 'base64url').toString('utf8'));
90
+ return typeof raw === 'object' && raw !== null ? (raw as Body) : null;
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
95
+
96
+ export function tenantOf(...jwts: string[]): string | null {
97
+ for (const jwt of jwts) {
98
+ const tid = decodeClaims(jwt)?.tid;
99
+ if (typeof tid === 'string' && tid !== '') return tid;
100
+ }
101
+ return null;
102
+ }
103
+
104
+ export function tokensOf(body: Body, now: number, previous?: string): Tokens {
105
+ const accessToken = text(body.access_token);
106
+ const refreshToken = text(body.refresh_token) || (previous ?? '');
107
+ if (accessToken === '' || refreshToken === '') throw new OutlookAuthError('Microsoft answered without the tokens Metro needs.');
108
+ return { accessToken, refreshToken, expiresAt: now + seconds(body.expires_in, 3600) * 1000 };
109
+ }
110
+
111
+ export async function requestDeviceCode(fetchImpl: FetchLike = fetch, now = Date.now()): Promise<DeviceCode> {
112
+ const { status, body } = await postForm('devicecode', { client_id: requireClientId(), scope: SCOPES }, fetchImpl);
113
+ const userCode = text(body.user_code);
114
+ const deviceCode = text(body.device_code);
115
+ if (status !== 200 || userCode === '' || deviceCode === '') throw new OutlookAuthError(failureOf(body));
116
+ return {
117
+ deviceCode,
118
+ userCode,
119
+ verificationUri: text(body.verification_uri) || 'https://microsoft.com/devicelogin',
120
+ message: text(body.message),
121
+ expiresAt: now + seconds(body.expires_in, 900) * 1000,
122
+ intervalMs: seconds(body.interval, 5) * 1000,
123
+ };
124
+ }
125
+
126
+ export async function pollDeviceCode(code: DeviceCode, fetchImpl: FetchLike = fetch, now = Date.now()): Promise<Poll> {
127
+ const { status, body } = await postForm(
128
+ 'token',
129
+ { grant_type: DEVICE_GRANT, client_id: requireClientId(), device_code: code.deviceCode },
130
+ fetchImpl,
131
+ );
132
+ if (status === 200) {
133
+ const tokens = tokensOf(body, now);
134
+ return { kind: 'done', tokens, tenantId: tenantOf(text(body.id_token), tokens.accessToken) };
135
+ }
136
+ const error = text(body.error);
137
+ if (error === 'authorization_pending') return { kind: 'pending' };
138
+ if (error === 'slow_down') return { kind: 'slow_down' };
139
+ return { kind: 'failed', message: failureOf(body) };
140
+ }
141
+
142
+ export async function refreshTokens(refreshToken: string, fetchImpl: FetchLike = fetch, now = Date.now()): Promise<Tokens> {
143
+ const { status, body } = await postForm(
144
+ 'token',
145
+ { grant_type: 'refresh_token', client_id: requireClientId(), refresh_token: refreshToken, scope: SCOPES },
146
+ fetchImpl,
147
+ );
148
+ if (status !== 200) throw new OutlookAuthError(`Microsoft refused to renew this mailbox's sign-in, so connect Outlook again. ${failureOf(body)}`);
149
+ return tokensOf(body, now, refreshToken);
150
+ }
@@ -0,0 +1,53 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { failureOf, OutlookAuthError, postForm, requireClientId, tenantOf, tokensOf, type FetchLike, type Tokens } from './auth.js';
3
+ import { loginBase, redirectUri, SCOPES } from './config.js';
4
+
5
+ export interface Pkce {
6
+ verifier: string;
7
+ challenge: string;
8
+ }
9
+
10
+ export function pkcePair(verifier = randomBytes(32).toString('base64url')): Pkce {
11
+ return { verifier, challenge: createHash('sha256').update(verifier).digest('base64url') };
12
+ }
13
+
14
+ export const newState = (): string => randomBytes(24).toString('base64url');
15
+
16
+ export function authorizeUrl(challenge: string, state: string, loginHint: string | null = null): string {
17
+ const query = new URLSearchParams({
18
+ ...(loginHint === null ? {} : { login_hint: loginHint }),
19
+ client_id: requireClientId(),
20
+ response_type: 'code',
21
+ redirect_uri: redirectUri(),
22
+ response_mode: 'query',
23
+ scope: SCOPES,
24
+ state,
25
+ code_challenge: challenge,
26
+ code_challenge_method: 'S256',
27
+ prompt: 'select_account',
28
+ });
29
+ return `${loginBase()}/oauth2/v2.0/authorize?${query.toString()}`;
30
+ }
31
+
32
+ export interface Redeemed {
33
+ tokens: Tokens;
34
+ tenantId: string | null;
35
+ }
36
+
37
+ export async function redeemCode(code: string, verifier: string, fetchImpl: FetchLike, now = Date.now()): Promise<Redeemed> {
38
+ const { status, body } = await postForm(
39
+ 'token',
40
+ {
41
+ grant_type: 'authorization_code',
42
+ client_id: requireClientId(),
43
+ code,
44
+ code_verifier: verifier,
45
+ redirect_uri: redirectUri(),
46
+ scope: SCOPES,
47
+ },
48
+ fetchImpl,
49
+ );
50
+ if (status !== 200) throw new OutlookAuthError(failureOf(body));
51
+ const tokens = tokensOf(body, now);
52
+ return { tokens, tenantId: tenantOf(typeof body.id_token === 'string' ? body.id_token : '', tokens.accessToken) };
53
+ }
@@ -0,0 +1,36 @@
1
+ export const OUTLOOK_CLIENT_ID = 'a2fbd978-97ee-4cc9-92bb-9d9567a7bf40';
2
+
3
+ export const SCOPES = [
4
+ 'https://graph.microsoft.com/Mail.ReadWrite',
5
+ 'https://graph.microsoft.com/Mail.Send',
6
+ 'https://graph.microsoft.com/User.Read',
7
+ 'offline_access',
8
+ 'openid',
9
+ ].join(' ');
10
+
11
+ export const NOT_SET_UP = 'Outlook is not set up on this Metro yet.';
12
+
13
+ export const MAILBOX_URL = 'https://outlook.office.com/mail/';
14
+
15
+ const trimmed = (raw: string | undefined): string => (raw ?? '').trim();
16
+
17
+ export function clientId(): string {
18
+ const fromEnv = trimmed(process.env.METRO_OUTLOOK_CLIENT_ID);
19
+ return fromEnv === '' ? OUTLOOK_CLIENT_ID : fromEnv;
20
+ }
21
+
22
+ const base = (raw: string | undefined, fallback: string): string => {
23
+ const value = trimmed(raw);
24
+ return (value === '' ? fallback : value).replace(/\/+$/, '');
25
+ };
26
+
27
+ export const loginBase = (): string =>
28
+ base(process.env.METRO_OUTLOOK_LOGIN_URL, 'https://login.microsoftonline.com/common');
29
+
30
+ export const redirectUri = (): string => {
31
+ const value = trimmed(process.env.METRO_OUTLOOK_REDIRECT);
32
+ return value === '' ? 'https://metro.box/' : value;
33
+ };
34
+
35
+ export const graphBase = (): string =>
36
+ base(process.env.METRO_OUTLOOK_GRAPH_URL, 'https://graph.microsoft.com/v1.0');
@@ -0,0 +1,141 @@
1
+ import { Line } from '@metro-labs/core/lines';
2
+ import { mintId } from '@metro-labs/core/stations/station-runtime';
3
+ import { bodyText, capText } from './html.js';
4
+
5
+ export interface Address {
6
+ emailAddress?: { name?: string; address?: string };
7
+ }
8
+
9
+ export interface GraphMessage {
10
+ id: string;
11
+ conversationId?: string;
12
+ subject?: string | null;
13
+ from?: Address | null;
14
+ toRecipients?: Address[];
15
+ ccRecipients?: Address[];
16
+ receivedDateTime?: string;
17
+ bodyPreview?: string;
18
+ body?: { contentType?: string; content?: string };
19
+ hasAttachments?: boolean;
20
+ isRead?: boolean;
21
+ internetMessageId?: string;
22
+ '@removed'?: unknown;
23
+ }
24
+
25
+ export const MESSAGE_FIELDS = [
26
+ 'id',
27
+ 'conversationId',
28
+ 'subject',
29
+ 'from',
30
+ 'toRecipients',
31
+ 'ccRecipients',
32
+ 'receivedDateTime',
33
+ 'bodyPreview',
34
+ 'body',
35
+ 'hasAttachments',
36
+ 'isRead',
37
+ 'internetMessageId',
38
+ ].join(',');
39
+
40
+ export const LIST_FIELDS = MESSAGE_FIELDS.replace(',body,', ',');
41
+
42
+ const encodeResource = (id: string): string => id.replace(/%/g, '%25').replace(/\//g, '%2F').replace(/\+/g, '%2B');
43
+
44
+ export const lineOf = (accountId: string, conversationId: string): string =>
45
+ `metro://outlook/${accountId}/${encodeResource(conversationId)}`;
46
+
47
+ export const userOf = (accountId: string, address: string): string =>
48
+ `metro://outlook/${accountId}/user/${address.toLowerCase()}`;
49
+
50
+ export function conversationOfLine(line: string): { accountId: string; conversationId: string } | null {
51
+ const parsed = Line.parseOutlook(line);
52
+ if (parsed === null) return null;
53
+ try {
54
+ return { accountId: parsed.accountId, conversationId: decodeURIComponent(parsed.resource) };
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ export const addressOf = (a: Address | null | undefined): string => (a?.emailAddress?.address ?? '').toLowerCase();
61
+ export const nameOf = (a: Address | null | undefined): string => a?.emailAddress?.name ?? '';
62
+
63
+ const addresses = (list: Address[] | undefined): string[] => (list ?? []).map(addressOf).filter((s) => s !== '');
64
+
65
+ export const titleOf = (m: GraphMessage): string => (m.subject ?? '').trim();
66
+
67
+ export function messageText(m: GraphMessage): string {
68
+ const title = titleOf(m);
69
+ const body = capText(bodyText(m.body));
70
+ return title === '' ? body : `Subject: ${title}\n\n${body}`;
71
+ }
72
+
73
+ export function isPrivateTo(m: GraphMessage, self: string): boolean {
74
+ const to = addresses(m.toRecipients);
75
+ return to.length === 1 && to[0] === self && addresses(m.ccRecipients).length === 0;
76
+ }
77
+
78
+ export interface AttachmentMeta {
79
+ kind: string;
80
+ name: string;
81
+ mime: string;
82
+ size?: number;
83
+ }
84
+
85
+ export function inboundEnvelope(accountId: string, self: string, m: GraphMessage, attachments: AttachmentMeta[], verified: boolean): Record<string, unknown> {
86
+ const from = addressOf(m.from);
87
+ const title = titleOf(m);
88
+ return {
89
+ kind: 'inbound',
90
+ id: mintId(),
91
+ ts: m.receivedDateTime ?? new Date().toISOString(),
92
+ station: 'outlook',
93
+ line: lineOf(accountId, m.conversationId ?? m.id),
94
+ line_name: title === '' ? '(no subject)' : title,
95
+ from: userOf(accountId, from === '' ? 'unknown' : from),
96
+ from_name: nameOf(m.from) || from,
97
+ from_display_name: nameOf(m.from) || undefined,
98
+ message_id: m.id,
99
+ text: messageText(m),
100
+ is_private: isPrivateTo(m, self),
101
+ sender_verified: verified,
102
+ payload: {
103
+ from,
104
+ to: addresses(m.toRecipients),
105
+ cc: addresses(m.ccRecipients),
106
+ subject: title,
107
+ internetMessageId: m.internetMessageId ?? null,
108
+ ...(attachments.length === 0 ? {} : { attachments }),
109
+ },
110
+ };
111
+ }
112
+
113
+ export function summaryOf(accountId: string, m: GraphMessage): Record<string, unknown> {
114
+ return {
115
+ message_id: m.id,
116
+ line: lineOf(accountId, m.conversationId ?? m.id),
117
+ from: addressOf(m.from),
118
+ from_name: nameOf(m.from),
119
+ date: m.receivedDateTime ?? null,
120
+ title: titleOf(m),
121
+ text_preview: (m.bodyPreview ?? '').trim(),
122
+ has_attachments: m.hasAttachments === true,
123
+ is_read: m.isRead === true,
124
+ };
125
+ }
126
+
127
+ export function fullOf(accountId: string, m: GraphMessage): Record<string, unknown> {
128
+ return {
129
+ message_id: m.id,
130
+ line: lineOf(accountId, m.conversationId ?? m.id),
131
+ from: addressOf(m.from),
132
+ from_name: nameOf(m.from),
133
+ date: m.receivedDateTime ?? null,
134
+ title: titleOf(m),
135
+ has_attachments: m.hasAttachments === true,
136
+ is_read: m.isRead === true,
137
+ to: addresses(m.toRecipients),
138
+ cc: addresses(m.ccRecipients),
139
+ text: capText(bodyText(m.body)),
140
+ };
141
+ }
@@ -0,0 +1,60 @@
1
+ import { TrainError } from '@metro-labs/core/train-error';
2
+ import type { Account } from './accounts.js';
3
+ import { graphBase } from './config.js';
4
+
5
+ type Json = Record<string, unknown>;
6
+
7
+ const urlOf = (path: string): string => (/^https?:\/\//.test(path) ? path : `${graphBase()}${path}`);
8
+
9
+ async function graphMessage(res: Response): Promise<string> {
10
+ const body = (await res.json().catch(() => ({}))) as { error?: { code?: unknown; message?: unknown } };
11
+ const message = typeof body.error?.message === 'string' ? body.error.message : '';
12
+ const code = typeof body.error?.code === 'string' ? body.error.code : '';
13
+ return [code, message].filter((s) => s !== '').join(': ') || `HTTP ${String(res.status)}`;
14
+ }
15
+
16
+ export class GraphGone extends Error {}
17
+
18
+ async function refusal(res: Response): Promise<Error> {
19
+ const detail = await graphMessage(res);
20
+ const status = res.status;
21
+ if (status === 410) return new GraphGone(detail);
22
+ if (status === 401) return new TrainError('outlook_signed_out', `Microsoft refused this mailbox's sign-in (${detail}); connect Outlook again from the page`, { retryable: false });
23
+ if (status === 403) return new TrainError('outlook_forbidden', `Microsoft does not let Metro do that on this mailbox (${detail})`, { retryable: false });
24
+ if (status === 404) return new TrainError('outlook_not_found', `Outlook has no such message or conversation (${detail})`, { retryable: false });
25
+ if (status === 413) return new TrainError('outlook_too_large', `Outlook refused the size of that request (${detail})`, { retryable: false });
26
+ if (status === 429) return new TrainError('outlook_throttled', `Microsoft asked Metro to slow down (${detail}); try again in a minute`, { retryable: true });
27
+ return new TrainError('outlook_graph_error', `Microsoft Graph answered ${String(status)} (${detail})`, { retryable: status >= 500 });
28
+ }
29
+
30
+ async function send(acct: Account, path: string, init: RequestInit, force: boolean): Promise<Response> {
31
+ const headers = new Headers(init.headers);
32
+ headers.set('authorization', `Bearer ${await acct.token(force)}`);
33
+ headers.set('accept', 'application/json');
34
+ return acct.fetch(urlOf(path), { ...init, headers });
35
+ }
36
+
37
+ export async function graph(acct: Account, path: string, init: RequestInit = {}): Promise<Response> {
38
+ let res = await send(acct, path, init, false);
39
+ if (res.status === 401) res = await send(acct, path, init, true);
40
+ if (!res.ok) throw await refusal(res);
41
+ return res;
42
+ }
43
+
44
+ export async function graphJson<T = Json>(acct: Account, path: string, init: RequestInit = {}): Promise<T> {
45
+ const res = await graph(acct, path, init);
46
+ return (await res.json()) as T;
47
+ }
48
+
49
+ export function jsonInit(method: string, body: unknown): RequestInit {
50
+ return { method, headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) };
51
+ }
52
+
53
+ export const odataQuote = (value: string): string => `'${value.replace(/'/g, "''")}'`;
54
+
55
+ export const queryOf = (params: Record<string, string>): string =>
56
+ Object.entries(params)
57
+ .map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
58
+ .join('&');
59
+
60
+ export const messagePath = (messageId: string): string => `/me/messages/${encodeURIComponent(messageId)}`;