@stage-labs/metro 0.1.0-beta.21 → 0.1.0-beta.210

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 (436) hide show
  1. package/dist/api.js +9 -101
  2. package/dist/claude-settings.js +40 -0
  3. package/dist/claude.js +152 -0
  4. package/dist/cli.js +52 -109
  5. package/dist/control.js +47 -75
  6. package/dist/hold.js +210 -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 +96 -0
  14. package/dist/runtime.js +29 -52
  15. package/dist/serve.js +157 -0
  16. package/dist/service.js +304 -0
  17. package/dist/tail.js +119 -0
  18. package/dist/update.js +13 -1
  19. package/package.json +2 -23
  20. package/runtime/marketplace/.claude-plugin/marketplace.json +12 -0
  21. package/runtime/marketplace/plugin/.claude-plugin/plugin.json +17 -0
  22. package/runtime/marketplace/plugin/.mcp.json +1 -0
  23. package/runtime/marketplace/plugin/bin/guard.mjs +71 -0
  24. package/runtime/marketplace/plugin/bin/metro-plugin.mjs +30 -0
  25. package/runtime/marketplace/plugin/bin/policy.mjs +97 -0
  26. package/runtime/marketplace/plugin/bin/session-start.mjs +31 -0
  27. package/runtime/marketplace/plugin/hooks/hooks.json +25 -0
  28. package/runtime/marketplace/plugin/orchestrator.md +26 -0
  29. package/runtime/node_modules/@metro-labs/core/package.json +47 -0
  30. package/runtime/node_modules/@metro-labs/core/src/endpoints.ts +7 -0
  31. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → core/src}/events.ts +11 -7
  32. package/runtime/node_modules/@metro-labs/{mcp/src/db → core/src}/ids.ts +5 -0
  33. package/runtime/node_modules/@metro-labs/core/src/is-record.ts +2 -0
  34. package/runtime/node_modules/@metro-labs/{mcp/src/stations → core/src}/lines.ts +24 -28
  35. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → core/src}/protocol.ts +7 -4
  36. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → core/src}/secure-fs.ts +17 -0
  37. package/runtime/node_modules/@metro-labs/core/src/station-names.ts +13 -0
  38. package/runtime/node_modules/@metro-labs/core/src/stations/account-files.ts +41 -0
  39. package/runtime/node_modules/@metro-labs/core/src/stations/account-store.ts +83 -0
  40. package/runtime/node_modules/@metro-labs/{mcp → core}/src/stations/attachments.ts +0 -1
  41. package/runtime/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +16 -0
  42. package/runtime/node_modules/@metro-labs/core/src/stations/profile.ts +58 -0
  43. package/runtime/node_modules/@metro-labs/core/src/stations/sender-profile.ts +64 -0
  44. package/runtime/node_modules/@metro-labs/{mcp → core}/src/stations/station-runtime.ts +6 -11
  45. package/runtime/node_modules/@metro-labs/core/src/stations/train-boot.ts +42 -0
  46. package/runtime/node_modules/@metro-labs/core/src/stations/train-events.ts +84 -0
  47. package/runtime/node_modules/@metro-labs/{mcp → core}/src/stations/types.ts +18 -1
  48. package/runtime/node_modules/@metro-labs/core/src/tickets.ts +37 -0
  49. package/runtime/node_modules/@metro-labs/core/src/version.ts +2 -0
  50. package/runtime/node_modules/@metro-labs/daemon/package.json +48 -0
  51. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/agent-fs.ts +134 -0
  52. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/files-api.ts +12 -0
  53. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/files.ts +89 -0
  54. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/home-fs.ts +113 -0
  55. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/job-env.ts +51 -0
  56. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/provision.ts +80 -0
  57. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedule-detail.ts +99 -0
  58. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules-api.ts +27 -0
  59. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules.ts +144 -0
  60. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/unit-files.ts +37 -0
  61. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/user.ts +89 -0
  62. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/view.ts +87 -0
  63. package/runtime/node_modules/@metro-labs/daemon/src/agents/account-attach.ts +19 -0
  64. package/runtime/node_modules/@metro-labs/daemon/src/agents/account-lookups.ts +55 -0
  65. package/runtime/node_modules/@metro-labs/daemon/src/agents/account-routes.ts +76 -0
  66. package/runtime/node_modules/@metro-labs/daemon/src/agents/accounts-api.ts +357 -0
  67. package/runtime/node_modules/@metro-labs/daemon/src/agents/admin.ts +30 -0
  68. package/runtime/node_modules/@metro-labs/daemon/src/agents/allowlist.ts +59 -0
  69. package/runtime/node_modules/@metro-labs/daemon/src/agents/api.ts +131 -0
  70. package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +132 -0
  71. package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +305 -0
  72. package/runtime/node_modules/@metro-labs/daemon/src/agents/files.ts +142 -0
  73. package/runtime/node_modules/@metro-labs/{mcp/src/db/key-map.ts → daemon/src/agents/keys.ts} +1 -11
  74. package/runtime/node_modules/@metro-labs/{mcp/src/db/agent-map.ts → daemon/src/agents/map.ts} +58 -1
  75. package/runtime/node_modules/@metro-labs/daemon/src/agents/policy-route.ts +27 -0
  76. package/runtime/node_modules/@metro-labs/{mcp/src/db/agent-scope.ts → daemon/src/agents/scope.ts} +2 -2
  77. package/runtime/node_modules/@metro-labs/daemon/src/agents/sender-cards.ts +94 -0
  78. package/runtime/node_modules/@metro-labs/daemon/src/agents/senders.ts +38 -0
  79. package/runtime/node_modules/@metro-labs/daemon/src/approvals/api.ts +55 -0
  80. package/runtime/node_modules/@metro-labs/daemon/src/approvals/pending.ts +150 -0
  81. package/runtime/node_modules/@metro-labs/daemon/src/approvals/preview.ts +66 -0
  82. package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +218 -0
  83. package/runtime/node_modules/@metro-labs/daemon/src/boot/connect-hint.ts +36 -0
  84. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/boot}/crash-guard.ts +1 -1
  85. package/runtime/node_modules/@metro-labs/daemon/src/boot/local-owner.ts +21 -0
  86. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/boot}/paths.ts +6 -34
  87. package/runtime/node_modules/@metro-labs/daemon/src/channels/addressed.ts +26 -0
  88. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/channels/inbound.ts +70 -45
  89. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/channels/media-note.ts +11 -7
  90. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/channels/pending.ts +4 -1
  91. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/channels/relay.ts +2 -2
  92. package/runtime/node_modules/@metro-labs/daemon/src/claude/api.ts +355 -0
  93. package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +359 -0
  94. package/runtime/node_modules/@metro-labs/daemon/src/claude/login.ts +208 -0
  95. package/runtime/node_modules/@metro-labs/daemon/src/claude/memory.ts +32 -0
  96. package/runtime/node_modules/@metro-labs/daemon/src/claude/onboarding.ts +60 -0
  97. package/runtime/node_modules/@metro-labs/daemon/src/claude/plugin-install.ts +149 -0
  98. package/runtime/node_modules/@metro-labs/daemon/src/claude/session-files.ts +75 -0
  99. package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +284 -0
  100. package/runtime/node_modules/@metro-labs/daemon/src/claude/settings.ts +109 -0
  101. package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +273 -0
  102. package/runtime/node_modules/@metro-labs/daemon/src/claude/skills.ts +120 -0
  103. package/runtime/node_modules/@metro-labs/daemon/src/claude/version.ts +113 -0
  104. package/runtime/node_modules/@metro-labs/{mcp/src/daemon/connector-api.ts → daemon/src/connectors/api.ts} +79 -113
  105. package/runtime/node_modules/@metro-labs/{mcp/src/db/connector-config.ts → daemon/src/connectors/config.ts} +67 -41
  106. package/runtime/node_modules/@metro-labs/daemon/src/connectors/gates.ts +72 -0
  107. package/runtime/node_modules/@metro-labs/daemon/src/connectors/health.ts +18 -0
  108. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/connectors}/oauth-client.ts +14 -17
  109. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/connectors}/oauth-discovery.ts +33 -20
  110. package/runtime/node_modules/@metro-labs/{mcp/src/daemon/connector-oauth-routes.ts → daemon/src/connectors/oauth-routes.ts} +30 -32
  111. package/runtime/node_modules/@metro-labs/{mcp/src/daemon/connector-oauth.ts → daemon/src/connectors/oauth.ts} +61 -22
  112. package/runtime/node_modules/@metro-labs/daemon/src/connectors/plugin-sync.ts +131 -0
  113. package/runtime/node_modules/@metro-labs/daemon/src/connectors/reach.ts +27 -0
  114. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-policy.ts +90 -0
  115. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay.ts +347 -0
  116. package/runtime/node_modules/@metro-labs/daemon/src/connectors/store.ts +385 -0
  117. package/runtime/node_modules/@metro-labs/daemon/src/connectors/tools.ts +81 -0
  118. package/runtime/node_modules/@metro-labs/daemon/src/connectors/url.ts +37 -0
  119. package/runtime/node_modules/@metro-labs/daemon/src/connectors/verify.ts +106 -0
  120. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/files}/attach-grant.ts +4 -4
  121. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/files}/attach-owner.ts +3 -3
  122. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-reaper.ts +174 -0
  123. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/files}/attach-serve.ts +12 -13
  124. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/files}/upload-api.ts +10 -27
  125. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/files}/upload-store.ts +4 -4
  126. package/runtime/node_modules/@metro-labs/daemon/src/gateway/bedrock.ts +233 -0
  127. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-auth.ts +208 -0
  128. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-device.ts +91 -0
  129. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-stream.ts +173 -0
  130. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-translate.ts +195 -0
  131. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-version.ts +44 -0
  132. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +128 -0
  133. package/runtime/node_modules/@metro-labs/daemon/src/gateway/effort.ts +77 -0
  134. package/runtime/node_modules/@metro-labs/daemon/src/gateway/eventstream.ts +123 -0
  135. package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +237 -0
  136. package/runtime/node_modules/@metro-labs/daemon/src/gateway/frames.ts +118 -0
  137. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +274 -0
  138. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-auth.ts +160 -0
  139. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-client.ts +46 -0
  140. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-setup.ts +150 -0
  141. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-stream.ts +139 -0
  142. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-translate.ts +224 -0
  143. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini.ts +208 -0
  144. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +199 -0
  145. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-config.ts +278 -0
  146. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-signin.ts +193 -0
  147. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-store.ts +70 -0
  148. package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter.ts +90 -0
  149. package/runtime/node_modules/@metro-labs/daemon/src/gateway/pkce.ts +36 -0
  150. package/runtime/node_modules/@metro-labs/daemon/src/gateway/provider-models.ts +66 -0
  151. package/runtime/node_modules/@metro-labs/daemon/src/gateway/served.ts +18 -0
  152. package/runtime/node_modules/@metro-labs/daemon/src/gateway/subscription.ts +120 -0
  153. package/runtime/node_modules/@metro-labs/daemon/src/gateway/text.ts +14 -0
  154. package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage.ts +244 -0
  155. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/accounts.ts +28 -17
  156. package/runtime/node_modules/@metro-labs/daemon/src/mcp/attachment-schema.ts +91 -0
  157. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/call-tools.ts +49 -20
  158. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/ctx.ts +4 -4
  159. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/event-store.ts +15 -16
  160. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/group-tools.ts +2 -2
  161. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/index.ts +24 -79
  162. package/runtime/node_modules/@metro-labs/daemon/src/mcp/instructions.ts +19 -0
  163. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/member-tools.ts +2 -2
  164. package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-prompt.ts +87 -0
  165. package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-relay.ts +79 -0
  166. package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-gate.ts +82 -0
  167. package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-snapshot.ts +79 -0
  168. package/runtime/node_modules/@metro-labs/daemon/src/mcp/profile-lookup.ts +60 -0
  169. package/runtime/node_modules/@metro-labs/daemon/src/mcp/profile-tool.ts +96 -0
  170. package/runtime/node_modules/@metro-labs/daemon/src/mcp/read-tool.ts +87 -0
  171. package/runtime/node_modules/@metro-labs/daemon/src/mcp/request-identity.ts +46 -0
  172. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-slot.ts +48 -0
  173. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/session.ts +33 -42
  174. package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-catalog.ts +88 -0
  175. package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-def.ts +9 -0
  176. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/tool-dispatch.ts +53 -40
  177. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/tool-schemas.ts +68 -119
  178. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/upload-tool.ts +6 -11
  179. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/helper-script.ts +126 -0
  180. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/install.ts +37 -0
  181. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/privilege.ts +48 -0
  182. package/runtime/node_modules/@metro-labs/daemon/src/monitor/api.ts +132 -0
  183. package/runtime/node_modules/@metro-labs/daemon/src/net/tunnel.ts +316 -0
  184. package/runtime/node_modules/@metro-labs/daemon/src/policy/policy.ts +118 -0
  185. package/runtime/node_modules/@metro-labs/daemon/src/routes/bearer.ts +35 -0
  186. package/runtime/node_modules/@metro-labs/daemon/src/routes/body.ts +24 -0
  187. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/routes}/http.ts +59 -51
  188. package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +205 -0
  189. package/runtime/node_modules/@metro-labs/daemon/src/routes/session-apis.ts +66 -0
  190. package/runtime/node_modules/@metro-labs/daemon/src/routes/session.ts +29 -0
  191. package/runtime/node_modules/@metro-labs/daemon/src/routes/threema-callback.ts +103 -0
  192. package/runtime/node_modules/@metro-labs/daemon/src/server/control.ts +39 -0
  193. package/runtime/node_modules/@metro-labs/daemon/src/server/machine.ts +58 -0
  194. package/runtime/node_modules/@metro-labs/daemon/src/server/owner.ts +26 -0
  195. package/runtime/node_modules/@metro-labs/daemon/src/server/update.ts +90 -0
  196. package/runtime/node_modules/@metro-labs/daemon/src/server.ts +2 -0
  197. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-driver.ts +42 -0
  198. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/stations/attach-inline.ts +29 -1
  199. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/stations/attach-interactive.ts +9 -36
  200. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-outlook.ts +63 -0
  201. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/stations/attach-resolve.ts +38 -13
  202. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/stations}/attach-session.ts +32 -22
  203. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/stations/attach-xmtp.ts +8 -9
  204. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/stations/attach.ts +75 -9
  205. package/runtime/node_modules/@metro-labs/daemon/src/stations/materialize.ts +265 -0
  206. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/stations/registry.ts +22 -6
  207. package/runtime/node_modules/@metro-labs/daemon/src/stations/runtime-deps.ts +82 -0
  208. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/stations}/supervisor-io.ts +3 -3
  209. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/stations}/supervisor.ts +6 -9
  210. package/runtime/node_modules/@metro-labs/daemon/src/stations/threema-callbacks.ts +32 -0
  211. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/stations}/train-call.ts +1 -1
  212. package/runtime/node_modules/@metro-labs/daemon/src/stations/webhook-endpoints.ts +51 -0
  213. package/runtime/node_modules/@metro-labs/daemon/src/terminal/api.ts +84 -0
  214. package/runtime/node_modules/@metro-labs/daemon/src/terminal/socket.ts +124 -0
  215. package/runtime/node_modules/@metro-labs/daemon/src/terminal/tickets.ts +15 -0
  216. package/runtime/node_modules/@metro-labs/daemon/src/vault/api.ts +45 -0
  217. package/runtime/node_modules/@metro-labs/daemon/src/vault/config.ts +51 -0
  218. package/runtime/node_modules/@metro-labs/daemon/src/vault/firewall.ts +9 -0
  219. package/runtime/node_modules/@metro-labs/daemon/src/vault/index.ts +96 -0
  220. package/runtime/node_modules/@metro-labs/daemon/src/vault/install.ts +56 -0
  221. package/runtime/node_modules/@metro-labs/daemon/src/vault/paths.ts +26 -0
  222. package/runtime/node_modules/@metro-labs/daemon/src/vault/proxy.ts +147 -0
  223. package/runtime/node_modules/@metro-labs/daemon/src/vault/store.ts +129 -0
  224. package/runtime/node_modules/@metro-labs/daemon/src/vault/trust.ts +44 -0
  225. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/package.json +3 -5
  226. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/accounts.ts +8 -28
  227. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/actions.ts +19 -187
  228. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/attachments.ts +3 -3
  229. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/format.ts +31 -72
  230. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/group-actions.ts +2 -2
  231. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/group.ts +2 -2
  232. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/index.ts +15 -21
  233. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/members.ts +2 -2
  234. package/runtime/node_modules/@metro-labs/discord-bot/src/profile.ts +68 -0
  235. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/send-files.ts +12 -13
  236. package/runtime/node_modules/@metro-labs/discord-bot/src/station.ts +23 -0
  237. package/runtime/node_modules/@metro-labs/http/package.json +29 -0
  238. package/runtime/node_modules/@metro-labs/http/src/api-http.ts +135 -0
  239. package/runtime/node_modules/@metro-labs/{mcp/src/daemon/session-api.ts → http/src/mode-api.ts} +13 -9
  240. package/runtime/node_modules/@metro-labs/http/src/return-to.ts +21 -0
  241. package/runtime/node_modules/@metro-labs/http/src/workos-token.ts +149 -0
  242. package/runtime/node_modules/@metro-labs/{telegram-user → outlook}/package.json +2 -5
  243. package/runtime/node_modules/@metro-labs/outlook/src/accounts.ts +93 -0
  244. package/runtime/node_modules/@metro-labs/outlook/src/actions.ts +14 -0
  245. package/runtime/node_modules/@metro-labs/outlook/src/attachments.ts +97 -0
  246. package/runtime/node_modules/@metro-labs/outlook/src/auth.ts +150 -0
  247. package/runtime/node_modules/@metro-labs/outlook/src/browser.ts +53 -0
  248. package/runtime/node_modules/@metro-labs/outlook/src/config.ts +36 -0
  249. package/runtime/node_modules/@metro-labs/outlook/src/format.ts +140 -0
  250. package/runtime/node_modules/@metro-labs/outlook/src/graph.ts +60 -0
  251. package/runtime/node_modules/@metro-labs/outlook/src/html.ts +72 -0
  252. package/runtime/node_modules/@metro-labs/outlook/src/inbound.ts +104 -0
  253. package/runtime/node_modules/@metro-labs/outlook/src/index.ts +12 -0
  254. package/runtime/node_modules/@metro-labs/outlook/src/login.ts +150 -0
  255. package/runtime/node_modules/@metro-labs/outlook/src/me.ts +41 -0
  256. package/runtime/node_modules/@metro-labs/outlook/src/outbound.ts +116 -0
  257. package/runtime/node_modules/@metro-labs/outlook/src/query.ts +119 -0
  258. package/runtime/node_modules/@metro-labs/outlook/src/state.ts +43 -0
  259. package/runtime/node_modules/@metro-labs/outlook/src/station.ts +15 -0
  260. package/runtime/node_modules/@metro-labs/outlook/src/trust.ts +70 -0
  261. package/runtime/node_modules/@metro-labs/telegram/package.json +3 -2
  262. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +38 -96
  263. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +214 -253
  264. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +67 -108
  265. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/client.ts +6 -6
  266. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +101 -147
  267. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/group.ts +4 -4
  268. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/history.ts +1 -1
  269. package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +57 -0
  270. package/runtime/node_modules/@metro-labs/telegram/src/index.ts +6 -102
  271. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/login.ts +20 -20
  272. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +64 -151
  273. package/runtime/node_modules/@metro-labs/telegram/src/members.ts +61 -33
  274. package/runtime/node_modules/@metro-labs/telegram/src/profile.ts +70 -0
  275. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/reactions.ts +3 -3
  276. package/runtime/node_modules/@metro-labs/telegram/src/station.ts +11 -1
  277. package/runtime/node_modules/@metro-labs/telegram/src/types.ts +5 -47
  278. package/runtime/node_modules/@metro-labs/telegram-bot/package.json +25 -0
  279. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +86 -0
  280. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +240 -0
  281. package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
  282. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +170 -0
  283. package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +103 -0
  284. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +90 -0
  285. package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
  286. package/runtime/node_modules/@metro-labs/telegram-bot/src/profile.ts +85 -0
  287. package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +21 -0
  288. package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +56 -0
  289. package/runtime/node_modules/@metro-labs/threema/package.json +26 -0
  290. package/runtime/node_modules/@metro-labs/threema/src/accounts.ts +95 -0
  291. package/runtime/node_modules/@metro-labs/threema/src/actions.ts +124 -0
  292. package/runtime/node_modules/@metro-labs/threema/src/api.ts +152 -0
  293. package/runtime/node_modules/@metro-labs/threema/src/crypto.ts +100 -0
  294. package/runtime/node_modules/@metro-labs/threema/src/files.ts +47 -0
  295. package/runtime/node_modules/@metro-labs/threema/src/format.ts +164 -0
  296. package/runtime/node_modules/@metro-labs/threema/src/groups.ts +96 -0
  297. package/runtime/node_modules/@metro-labs/threema/src/ids.ts +21 -0
  298. package/runtime/node_modules/@metro-labs/threema/src/inbound.ts +89 -0
  299. package/runtime/node_modules/@metro-labs/threema/src/index.ts +24 -0
  300. package/runtime/node_modules/@metro-labs/threema/src/messages.ts +264 -0
  301. package/runtime/node_modules/@metro-labs/threema/src/outbound.ts +160 -0
  302. package/runtime/node_modules/@metro-labs/threema/src/station.ts +13 -0
  303. package/runtime/node_modules/@metro-labs/threema/src/verify.ts +117 -0
  304. package/runtime/node_modules/@metro-labs/webhook/package.json +1 -1
  305. package/runtime/node_modules/@metro-labs/webhook/src/station.ts +5 -8
  306. package/runtime/node_modules/@metro-labs/whatsapp/package.json +2 -5
  307. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +6 -28
  308. package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +1 -1
  309. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +54 -11
  310. package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +2 -2
  311. package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +1 -1
  312. package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +86 -4
  313. package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +7 -58
  314. package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +11 -28
  315. package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +4 -37
  316. package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +1 -1
  317. package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +1 -1
  318. package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +1 -1
  319. package/runtime/node_modules/@metro-labs/whatsapp/src/names.ts +75 -0
  320. package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +44 -0
  321. package/runtime/node_modules/@metro-labs/whatsapp/src/resolve.ts +52 -0
  322. package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +16 -1
  323. package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +5 -9
  324. package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +0 -1
  325. package/runtime/node_modules/@metro-labs/xmtp/package.json +3 -2
  326. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +10 -20
  327. package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +2 -2
  328. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +15 -204
  329. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +4 -43
  330. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +41 -219
  331. package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +3 -3
  332. package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +3 -3
  333. package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +7 -24
  334. package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +8 -18
  335. package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +1 -1
  336. package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +2 -2
  337. package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +30 -16
  338. package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +12 -20
  339. package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +1 -19
  340. package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +1 -1
  341. package/runtime/node_modules/@metro-labs/xmtp/src/names.ts +61 -0
  342. package/runtime/node_modules/@metro-labs/xmtp/src/profile.ts +153 -0
  343. package/runtime/node_modules/@metro-labs/xmtp/src/sender.ts +112 -0
  344. package/runtime/node_modules/@metro-labs/xmtp/src/smart.ts +72 -0
  345. package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +5 -1
  346. package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +4 -51
  347. package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +30 -44
  348. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +4 -6
  349. package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +19 -5
  350. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +14 -4
  351. package/runtime/runtime.json +1 -1
  352. package/runtime/server.ts +1 -0
  353. package/runtime/stations.json +37 -0
  354. package/dist/prompt.js +0 -10
  355. package/dist/store.js +0 -70
  356. package/runtime/node_modules/@metro-labs/discord/src/station.ts +0 -19
  357. package/runtime/node_modules/@metro-labs/discord/src/voice-speak.ts +0 -195
  358. package/runtime/node_modules/@metro-labs/discord/src/voice-transcribe.ts +0 -292
  359. package/runtime/node_modules/@metro-labs/discord/src/voice.ts +0 -197
  360. package/runtime/node_modules/@metro-labs/discord/src/wire.ts +0 -5
  361. package/runtime/node_modules/@metro-labs/mcp/package.json +0 -66
  362. package/runtime/node_modules/@metro-labs/mcp/src/channels/dedupe.ts +0 -24
  363. package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +0 -311
  364. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +0 -335
  365. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +0 -129
  366. package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +0 -322
  367. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +0 -111
  368. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair.ts +0 -20
  369. package/runtime/node_modules/@metro-labs/mcp/src/daemon/collection-api.ts +0 -202
  370. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-json.ts +0 -31
  371. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-tools.ts +0 -100
  372. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-url.ts +0 -57
  373. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +0 -333
  374. package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-auth.ts +0 -164
  375. package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-oauth.ts +0 -208
  376. package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +0 -79
  377. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-pending.ts +0 -65
  378. package/runtime/node_modules/@metro-labs/mcp/src/daemon/pair-codes.ts +0 -58
  379. package/runtime/node_modules/@metro-labs/mcp/src/daemon/project-api.ts +0 -214
  380. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +0 -125
  381. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-pair.ts +0 -18
  382. package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-source.ts +0 -105
  383. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +0 -41
  384. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +0 -218
  385. package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +0 -169
  386. package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +0 -91
  387. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +0 -215
  388. package/runtime/node_modules/@metro-labs/mcp/src/db/client.ts +0 -37
  389. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-collections.ts +0 -218
  390. package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +0 -329
  391. package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +0 -364
  392. package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +0 -300
  393. package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +0 -31
  394. package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +0 -105
  395. package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +0 -131
  396. package/runtime/node_modules/@metro-labs/mcp/src/db/token-carry.ts +0 -45
  397. package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +0 -57
  398. package/runtime/node_modules/@metro-labs/mcp/src/db/whatsapp-login.ts +0 -18
  399. package/runtime/node_modules/@metro-labs/mcp/src/mcp/channel-owner.ts +0 -29
  400. package/runtime/node_modules/@metro-labs/mcp/src/mcp/frame-scope.ts +0 -21
  401. package/runtime/node_modules/@metro-labs/mcp/src/mcp/permission-relay.ts +0 -66
  402. package/runtime/node_modules/@metro-labs/mcp/src/mcp/request-identity.ts +0 -73
  403. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-registry.ts +0 -172
  404. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-route.ts +0 -43
  405. package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +0 -240
  406. package/runtime/node_modules/@metro-labs/mcp/src/server.ts +0 -2
  407. package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +0 -80
  408. package/runtime/node_modules/@metro-labs/mcp/src/stations/messaging-normalize.ts +0 -88
  409. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +0 -3
  410. package/runtime/node_modules/@metro-labs/telegram-user/src/accounts.ts +0 -81
  411. package/runtime/node_modules/@metro-labs/telegram-user/src/actions.ts +0 -254
  412. package/runtime/node_modules/@metro-labs/telegram-user/src/attachments.ts +0 -87
  413. package/runtime/node_modules/@metro-labs/telegram-user/src/format.ts +0 -141
  414. package/runtime/node_modules/@metro-labs/telegram-user/src/inbound.ts +0 -78
  415. package/runtime/node_modules/@metro-labs/telegram-user/src/index.ts +0 -46
  416. package/runtime/node_modules/@metro-labs/telegram-user/src/media-actions.ts +0 -80
  417. package/runtime/node_modules/@metro-labs/telegram-user/src/members.ts +0 -75
  418. package/runtime/node_modules/@metro-labs/telegram-user/src/normalize.ts +0 -22
  419. package/runtime/node_modules/@metro-labs/telegram-user/src/station.ts +0 -24
  420. package/runtime/node_modules/@metro-labs/telegram-user/src/types.ts +0 -7
  421. package/runtime/node_modules/@metro-labs/telegram-user/src/wire.ts +0 -1
  422. package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +0 -22
  423. package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +0 -5
  424. package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +0 -87
  425. package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +0 -55
  426. package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +0 -349
  427. /package/runtime/node_modules/@metro-labs/{mcp/src/daemon → core/src}/log.ts +0 -0
  428. /package/runtime/node_modules/@metro-labs/{mcp/src/mcp → core/src}/str.ts +0 -0
  429. /package/runtime/node_modules/@metro-labs/{mcp/src/daemon → core/src}/train-error.ts +0 -0
  430. /package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/channels/webhook-note.ts +0 -0
  431. /package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/raw-get-stream.ts +0 -0
  432. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/api-base.ts +0 -0
  433. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/verify.ts +0 -0
  434. /package/runtime/node_modules/@metro-labs/{mcp/src/daemon → http/src}/api-error.ts +0 -0
  435. /package/runtime/node_modules/@metro-labs/{mcp/src/daemon → http/src}/cors.ts +0 -0
  436. /package/runtime/node_modules/@metro-labs/{telegram → telegram-bot}/src/verify.ts +0 -0
@@ -0,0 +1,21 @@
1
+ export const GATEWAY_API = (
2
+ process.env.THREEMA_GATEWAY_URL ?? 'https://msgapi.threema.ch'
3
+ ).replace(/\/+$/, '');
4
+
5
+ const THREEMA_ID_RE = /^(?:[A-Z0-9]{8}|\*[A-Z0-9]{7})$/;
6
+ const KEY_HEX_RE = /^[0-9a-f]{64}$/;
7
+
8
+ export const MESSAGE_ID_RE = /^[0-9a-f]{16}$/;
9
+
10
+ export const isThreemaId = (raw: string): boolean => THREEMA_ID_RE.test(raw);
11
+
12
+ export const isGatewayId = (raw: string): boolean =>
13
+ raw.startsWith('*') && isThreemaId(raw);
14
+
15
+ export const normalizeThreemaId = (raw: string): string =>
16
+ raw.trim().toUpperCase();
17
+
18
+ export function parsePrivateKey(raw: string): string | null {
19
+ const text = raw.trim().replace(/^private:/i, '').trim().toLowerCase();
20
+ return KEY_HEX_RE.test(text) ? text : null;
21
+ }
@@ -0,0 +1,89 @@
1
+ import type { Account } from './accounts.js';
2
+ import { emitInbound } from '@metro-labs/core/stations/train-events';
3
+ import {
4
+ DIRECT,
5
+ reactionEnvelope,
6
+ receiptEnvelope,
7
+ textEnvelope,
8
+ type InboundMeta,
9
+ type Room,
10
+ } from './format.js';
11
+ import { deliverFile } from './files.js';
12
+ import type { Decoded, GroupRef } from './messages.js';
13
+ import { requestSync, sentByUs } from './outbound.js';
14
+
15
+ const synced = new Set<string>();
16
+
17
+ function roomOf(acct: Account, group: GroupRef | null): Room {
18
+ if (group === null) return DIRECT;
19
+ return { group, name: acct.groups.get(group)?.name ?? null };
20
+ }
21
+
22
+ function askForRoster(acct: Account, group: GroupRef): void {
23
+ const key = `${acct.cfg.id}:${group.creator}-${group.groupId}`;
24
+ if (acct.groups.get(group) !== undefined || synced.has(key)) return;
25
+ synced.add(key);
26
+ requestSync(acct, group).catch((err: unknown) => {
27
+ process.stderr.write(`threema[${acct.cfg.id}]: could not ask ${group.creator} for the group roster: ${err instanceof Error ? err.message : String(err)}\n`);
28
+ });
29
+ }
30
+
31
+ function reactions(acct: Account, m: InboundMeta, d: Extract<Decoded, { kind: 'receipt' }>): string {
32
+ let count = 0;
33
+ for (const target of d.messageIds) {
34
+ const env = receiptEnvelope(acct.cfg.id, m, d.status, target, roomOf(acct, d.group));
35
+ if (env === null) continue;
36
+ emitInbound(acct.cfg.id, env);
37
+ count += 1;
38
+ }
39
+ return count > 0 ? 'reaction' : `receipt:${String(d.status)}`;
40
+ }
41
+
42
+ function control(acct: Account, m: InboundMeta, d: Decoded): string | null {
43
+ if (d.kind === 'group-setup') {
44
+ acct.groups.setup(m.from, d.groupId, d.members);
45
+ return 'group-setup';
46
+ }
47
+ if (d.kind === 'group-rename') {
48
+ acct.groups.rename(m.from, d.groupId, d.name);
49
+ return 'group-rename';
50
+ }
51
+ if (d.kind === 'group-leave') {
52
+ acct.groups.leave(d.group, m.from);
53
+ return 'group-leave';
54
+ }
55
+ return null;
56
+ }
57
+
58
+ function chat(acct: Account, m: InboundMeta, d: Decoded): string | null {
59
+ if (d.kind === 'text') {
60
+ emitInbound(acct.cfg.id, textEnvelope(acct.cfg.id, m, d.text, sentByUs));
61
+ return 'text';
62
+ }
63
+ if (d.kind === 'group-text') {
64
+ askForRoster(acct, d.group);
65
+ emitInbound(acct.cfg.id, textEnvelope(acct.cfg.id, m, d.text, sentByUs, roomOf(acct, d.group)));
66
+ return 'group-text';
67
+ }
68
+ if (d.kind === 'file') {
69
+ if (d.group !== null) askForRoster(acct, d.group);
70
+ deliverFile(acct, m, d.file, roomOf(acct, d.group), sentByUs);
71
+ return 'file';
72
+ }
73
+ if (d.kind === 'reaction') {
74
+ if (d.group !== null) askForRoster(acct, d.group);
75
+ emitInbound(acct.cfg.id, reactionEnvelope(acct.cfg.id, m, d.emoji, d.messageId, d.removed, roomOf(acct, d.group)));
76
+ return 'reaction';
77
+ }
78
+ return null;
79
+ }
80
+
81
+ export function deliver(acct: Account, m: InboundMeta, d: Decoded): string {
82
+ const handled = control(acct, m, d) ?? chat(acct, m, d);
83
+ if (handled !== null) return handled;
84
+ if (d.kind === 'receipt') return reactions(acct, m, d);
85
+ if (d.kind === 'typing') return 'typing';
86
+ const type = d.kind === 'other' ? `0x${d.type.toString(16)}` : d.kind;
87
+ process.stderr.write(`threema[${acct.cfg.id}]: ignored a message of type ${type} from ${m.from}\n`);
88
+ return `ignored:${type}`;
89
+ }
@@ -0,0 +1,24 @@
1
+ import { errMsg } from '@metro-labs/core/log';
2
+ import { readCalls } from '@metro-labs/core/trains/protocol';
3
+ import { announceAccounts } from '@metro-labs/core/stations/train-boot';
4
+ import { accounts, bootAccount, loadAccounts } from './accounts.js';
5
+ import { handleCall } from './actions.js';
6
+ import { fetchCredits } from './api.js';
7
+
8
+ readCalls('threema', handleCall);
9
+
10
+ for (const cfg of loadAccounts()) accounts.set(cfg.id, bootAccount(cfg));
11
+ announceAccounts('threema', accounts.keys());
12
+
13
+ for (const acct of accounts.values())
14
+ fetchCredits(acct.cfg)
15
+ .then((credits) => {
16
+ process.stderr.write(
17
+ `threema[${acct.cfg.id}] ${acct.cfg.gatewayId}: ${credits} credits left\n`,
18
+ );
19
+ })
20
+ .catch((err: unknown) => {
21
+ process.stderr.write(
22
+ `threema[${acct.cfg.id}] credits check failed: ${errMsg(err)}\n`,
23
+ );
24
+ });
@@ -0,0 +1,264 @@
1
+ import { bytesToHex, hexToBytes } from './crypto.js';
2
+
3
+ export const MSG_TEXT = 0x01;
4
+ export const MSG_FILE = 0x17;
5
+ export const MSG_GROUP_TEXT = 0x41;
6
+ export const MSG_GROUP_FILE = 0x46;
7
+ export const MSG_GROUP_SETUP = 0x4a;
8
+ export const MSG_GROUP_RENAME = 0x4b;
9
+ export const MSG_GROUP_LEAVE = 0x4c;
10
+ export const MSG_GROUP_REQUEST_SYNC = 0x51;
11
+ export const MSG_DELIVERY_RECEIPT = 0x80;
12
+ export const MSG_GROUP_DELIVERY_RECEIPT = 0x81;
13
+ export const MSG_REACTION = 0x82;
14
+ export const MSG_GROUP_REACTION = 0x83;
15
+ export const MSG_TYPING = 0x90;
16
+
17
+ export const RECEIPT_ACK = 0x03;
18
+ export const RECEIPT_DECLINE = 0x04;
19
+
20
+ export const ID_BYTES = 8;
21
+ const MESSAGE_ID_BYTES = 8;
22
+ const PROTO_MESSAGE_ID = 0x09;
23
+ const PROTO_APPLY = 0x12;
24
+ const PROTO_WITHDRAW = 0x1a;
25
+
26
+ export interface GroupRef {
27
+ creator: string;
28
+ groupId: string;
29
+ }
30
+
31
+ export interface FileData {
32
+ blobId: string;
33
+ key: string;
34
+ mime: string;
35
+ name: string;
36
+ size: number | null;
37
+ caption: string | null;
38
+ media: boolean;
39
+ }
40
+
41
+ export type Decoded =
42
+ | { kind: 'text'; text: string }
43
+ | { kind: 'file'; group: GroupRef | null; file: FileData }
44
+ | { kind: 'group-text'; group: GroupRef; text: string }
45
+ | { kind: 'group-setup'; groupId: string; members: string[] }
46
+ | { kind: 'group-rename'; groupId: string; name: string }
47
+ | { kind: 'group-leave'; group: GroupRef }
48
+ | { kind: 'receipt'; group: GroupRef | null; status: number; messageIds: string[] }
49
+ | { kind: 'reaction'; group: GroupRef | null; messageId: string; emoji: string; removed: boolean }
50
+ | { kind: 'typing' }
51
+ | { kind: 'other'; type: number };
52
+
53
+ const ascii = (bytes: Uint8Array): string => Buffer.from(bytes).toString('ascii');
54
+ const utf8 = (bytes: Uint8Array): string => Buffer.from(bytes).toString('utf8');
55
+
56
+ const groupHeader = (group: GroupRef): Uint8Array =>
57
+ new Uint8Array([...Buffer.from(group.creator, 'ascii'), ...hexToBytes(group.groupId, 'group id')]);
58
+
59
+ const readGroup = (body: Uint8Array): GroupRef | null =>
60
+ body.length < ID_BYTES * 2 ? null : { creator: ascii(body.subarray(0, ID_BYTES)), groupId: bytesToHex(body.subarray(ID_BYTES, ID_BYTES * 2)) };
61
+
62
+ function ids(body: Uint8Array, from: number): string[] {
63
+ const out: string[] = [];
64
+ for (let at = from; at + MESSAGE_ID_BYTES <= body.length; at += MESSAGE_ID_BYTES) out.push(bytesToHex(body.subarray(at, at + MESSAGE_ID_BYTES)));
65
+ return out;
66
+ }
67
+
68
+ const members = (body: Uint8Array, from: number): string[] => {
69
+ const out: string[] = [];
70
+ for (let at = from; at + ID_BYTES <= body.length; at += ID_BYTES) out.push(ascii(body.subarray(at, at + ID_BYTES)));
71
+ return out;
72
+ };
73
+
74
+ function concat(...parts: Uint8Array[]): Uint8Array {
75
+ const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
76
+ let at = 0;
77
+ for (const p of parts) {
78
+ out.set(p, at);
79
+ at += p.length;
80
+ }
81
+ return out;
82
+ }
83
+
84
+ const typed = (type: number, ...parts: Uint8Array[]): Uint8Array => concat(new Uint8Array([type]), ...parts);
85
+
86
+ export const encodeText = (text: string): Uint8Array => typed(MSG_TEXT, Buffer.from(text, 'utf8'));
87
+
88
+ export const encodeGroupText = (group: GroupRef, text: string): Uint8Array => typed(MSG_GROUP_TEXT, groupHeader(group), Buffer.from(text, 'utf8'));
89
+
90
+ const fileJson = (f: FileData): Uint8Array =>
91
+ Buffer.from(
92
+ JSON.stringify({
93
+ b: f.blobId,
94
+ k: f.key,
95
+ m: f.mime,
96
+ n: f.name,
97
+ ...(f.size === null ? {} : { s: f.size }),
98
+ i: f.media ? 1 : 0,
99
+ j: f.media ? 1 : 0,
100
+ ...(f.caption === null ? {} : { d: f.caption }),
101
+ }),
102
+ 'utf8',
103
+ );
104
+
105
+ export const encodeFile = (f: FileData): Uint8Array => typed(MSG_FILE, fileJson(f));
106
+
107
+ export const encodeGroupFile = (group: GroupRef, f: FileData): Uint8Array => typed(MSG_GROUP_FILE, groupHeader(group), fileJson(f));
108
+
109
+ const HEX_RE = /^[0-9a-f]+$/i;
110
+
111
+ const str = (v: unknown, fallback: string): string => (typeof v === 'string' && v !== '' ? v : fallback);
112
+ const num = (v: unknown): number | null => (typeof v === 'number' ? v : null);
113
+ const hexOf = (v: unknown, length: number): string | null => {
114
+ const text = typeof v === 'string' ? v.toLowerCase() : '';
115
+ return text.length === length && HEX_RE.test(text) ? text : null;
116
+ };
117
+
118
+ function parseJson(raw: Uint8Array): Record<string, unknown> | null {
119
+ try {
120
+ const parsed: unknown = JSON.parse(utf8(raw));
121
+ return typeof parsed === 'object' && parsed !== null ? (parsed as Record<string, unknown>) : null;
122
+ } catch {
123
+ return null;
124
+ }
125
+ }
126
+
127
+ export function decodeFileJson(raw: Uint8Array): FileData | null {
128
+ const o = parseJson(raw);
129
+ if (o === null) return null;
130
+ const blobId = hexOf(o.b, 32);
131
+ const key = hexOf(o.k, 64);
132
+ if (blobId === null || key === null) return null;
133
+ const rendering = num(o.j) ?? num(o.i) ?? 0;
134
+ return {
135
+ blobId,
136
+ key,
137
+ mime: str(o.m, 'application/octet-stream'),
138
+ name: str(o.n, 'file'),
139
+ size: num(o.s),
140
+ caption: typeof o.d === 'string' && o.d !== '' ? o.d : null,
141
+ media: rendering === 1,
142
+ };
143
+ }
144
+
145
+ export const encodeGroupSyncRequest = (groupId: string): Uint8Array => typed(MSG_GROUP_REQUEST_SYNC, hexToBytes(groupId, 'group id'));
146
+
147
+ function varint(n: number): Uint8Array {
148
+ const out: number[] = [];
149
+ let rest = n;
150
+ while (rest >= 0x80) {
151
+ out.push((rest & 0x7f) | 0x80);
152
+ rest = Math.floor(rest / 0x80);
153
+ }
154
+ out.push(rest);
155
+ return new Uint8Array(out);
156
+ }
157
+
158
+ export function encodeReactionProto(messageId: string, emoji: string, removed: boolean): Uint8Array {
159
+ const emojiBytes = Buffer.from(emoji, 'utf8');
160
+ return concat(
161
+ new Uint8Array([PROTO_MESSAGE_ID]),
162
+ hexToBytes(messageId, 'message id'),
163
+ new Uint8Array([removed ? PROTO_WITHDRAW : PROTO_APPLY]),
164
+ varint(emojiBytes.length),
165
+ emojiBytes,
166
+ );
167
+ }
168
+
169
+ export const encodeReaction = (messageId: string, emoji: string, removed: boolean): Uint8Array =>
170
+ typed(MSG_REACTION, encodeReactionProto(messageId, emoji, removed));
171
+
172
+ export const encodeGroupReaction = (group: GroupRef, messageId: string, emoji: string, removed: boolean): Uint8Array =>
173
+ typed(MSG_GROUP_REACTION, groupHeader(group), encodeReactionProto(messageId, emoji, removed));
174
+
175
+ function readVarint(body: Uint8Array, at: number): { value: number; next: number } | null {
176
+ let value = 0;
177
+ let shift = 1;
178
+ for (let i = at; i < body.length; i++) {
179
+ const byte = body[i] ?? 0;
180
+ value += (byte & 0x7f) * shift;
181
+ if (byte < 0x80) return { value, next: i + 1 };
182
+ shift *= 0x80;
183
+ }
184
+ return null;
185
+ }
186
+
187
+ interface ProtoField {
188
+ tag: number;
189
+ bytes: Uint8Array;
190
+ next: number;
191
+ }
192
+
193
+ function readField(body: Uint8Array, at: number): ProtoField | null {
194
+ const tag = body[at] ?? 0;
195
+ if (tag === PROTO_MESSAGE_ID)
196
+ return at + 1 + MESSAGE_ID_BYTES > body.length ? null : { tag, bytes: body.subarray(at + 1, at + 1 + MESSAGE_ID_BYTES), next: at + 1 + MESSAGE_ID_BYTES };
197
+ if (tag !== PROTO_APPLY && tag !== PROTO_WITHDRAW) return null;
198
+ const len = readVarint(body, at + 1);
199
+ if (len === null || len.next + len.value > body.length) return null;
200
+ return { tag, bytes: body.subarray(len.next, len.next + len.value), next: len.next + len.value };
201
+ }
202
+
203
+ export function decodeReactionProto(body: Uint8Array): { messageId: string; emoji: string; removed: boolean } | null {
204
+ let messageId: string | null = null;
205
+ let emoji = '';
206
+ let removed = false;
207
+ for (let at = 0; at < body.length; ) {
208
+ const field = readField(body, at);
209
+ if (field === null) return null;
210
+ if (field.tag === PROTO_MESSAGE_ID) messageId = bytesToHex(field.bytes);
211
+ else {
212
+ emoji = utf8(field.bytes);
213
+ removed = field.tag === PROTO_WITHDRAW;
214
+ }
215
+ at = field.next;
216
+ }
217
+ return messageId === null || emoji === '' ? null : { messageId, emoji, removed };
218
+ }
219
+
220
+ function decodeGroup(type: number, body: Uint8Array): Decoded {
221
+ const group = readGroup(body);
222
+ if (group === null) return { kind: 'other', type };
223
+ const rest = body.subarray(ID_BYTES * 2);
224
+ if (type === MSG_GROUP_TEXT) return { kind: 'group-text', group, text: utf8(rest) };
225
+ if (type === MSG_GROUP_FILE) {
226
+ const file = decodeFileJson(rest);
227
+ return file === null ? { kind: 'other', type } : { kind: 'file', group, file };
228
+ }
229
+ if (type === MSG_GROUP_LEAVE) return { kind: 'group-leave', group };
230
+ if (type === MSG_GROUP_DELIVERY_RECEIPT) return { kind: 'receipt', group, status: rest[0] ?? 0, messageIds: ids(rest, 1) };
231
+ const reaction = decodeReactionProto(rest);
232
+ return reaction === null ? { kind: 'other', type } : { kind: 'reaction', group, ...reaction };
233
+ }
234
+
235
+ const GROUP_TYPES = new Set([MSG_GROUP_TEXT, MSG_GROUP_FILE, MSG_GROUP_LEAVE, MSG_GROUP_DELIVERY_RECEIPT, MSG_GROUP_REACTION]);
236
+ const DIRECT_TYPES = new Set([MSG_TEXT, MSG_FILE, MSG_TYPING, MSG_DELIVERY_RECEIPT, MSG_REACTION]);
237
+
238
+ function decodeControl(type: number, body: Uint8Array): Decoded {
239
+ if (body.length < ID_BYTES) return { kind: 'other', type };
240
+ const groupId = bytesToHex(body.subarray(0, ID_BYTES));
241
+ if (type === MSG_GROUP_SETUP) return { kind: 'group-setup', groupId, members: members(body, ID_BYTES) };
242
+ return { kind: 'group-rename', groupId, name: utf8(body.subarray(ID_BYTES)) };
243
+ }
244
+
245
+ function decodeDirect(type: number, body: Uint8Array): Decoded {
246
+ if (type === MSG_TEXT) return { kind: 'text', text: utf8(body) };
247
+ if (type === MSG_FILE) {
248
+ const file = decodeFileJson(body);
249
+ return file === null ? { kind: 'other', type } : { kind: 'file', group: null, file };
250
+ }
251
+ if (type === MSG_TYPING) return { kind: 'typing' };
252
+ if (type === MSG_DELIVERY_RECEIPT) return { kind: 'receipt', group: null, status: body[0] ?? 0, messageIds: ids(body, 1) };
253
+ const reaction = decodeReactionProto(body);
254
+ return reaction === null ? { kind: 'other', type } : { kind: 'reaction', group: null, ...reaction };
255
+ }
256
+
257
+ export function decode(plain: Uint8Array): Decoded {
258
+ const type = plain[0] ?? -1;
259
+ const body = plain.subarray(1);
260
+ if (DIRECT_TYPES.has(type)) return decodeDirect(type, body);
261
+ if (type === MSG_GROUP_SETUP || type === MSG_GROUP_RENAME) return decodeControl(type, body);
262
+ if (GROUP_TYPES.has(type)) return decodeGroup(type, body);
263
+ return { kind: 'other', type };
264
+ }
@@ -0,0 +1,160 @@
1
+ import { TrainError } from '@metro-labs/core/train-error';
2
+ import { respond } from '@metro-labs/core/stations/station-runtime';
3
+ import { publicKeyFor, targetOf, type Account, type Target } from './accounts.js';
4
+ import { sendE2E } from './api.js';
5
+ import { bytesToHex, quoted, seal } from './crypto.js';
6
+ import { encodeFileFor, fileLabel, packFile, type OutgoingFile } from './files.js';
7
+ import { emitOutbound, emitOutboundReaction } from './format.js';
8
+ import { MESSAGE_ID_RE } from './ids.js';
9
+ import {
10
+ encodeGroupReaction,
11
+ encodeGroupSyncRequest,
12
+ encodeGroupText,
13
+ encodeReaction,
14
+ encodeText,
15
+ type GroupRef,
16
+ } from './messages.js';
17
+
18
+ export const MAX_TEXT_BYTES = 3500;
19
+ const SENT_MAX = 2000;
20
+
21
+ type Args = Record<string, unknown>;
22
+
23
+ const sentIds = new Set<string>();
24
+
25
+ export function noteSent(messageId: string): void {
26
+ sentIds.add(messageId);
27
+ while (sentIds.size > SENT_MAX) {
28
+ const oldest = sentIds.values().next();
29
+ if (oldest.done) break;
30
+ sentIds.delete(oldest.value);
31
+ }
32
+ }
33
+
34
+ export const sentByUs = (messageId: string): boolean => sentIds.has(messageId);
35
+
36
+ async function deliver(acct: Account, to: string, plain: Uint8Array, group: boolean): Promise<string> {
37
+ const { nonce, box } = seal(plain, await publicKeyFor(acct, to), acct.keys);
38
+ const messageId = await sendE2E(acct.cfg, to, bytesToHex(nonce), bytesToHex(box), group);
39
+ noteSent(messageId);
40
+ return messageId;
41
+ }
42
+
43
+ function recipientsOf(acct: Account, group: GroupRef): string[] {
44
+ const members = acct.groups.recipients(group);
45
+ if (members === null)
46
+ throw new TrainError(
47
+ 'threema_unknown_group',
48
+ 'metro has not received this group\'s member list yet; it asked the group creator for it, try again in a moment',
49
+ { retryable: true },
50
+ );
51
+ return members;
52
+ }
53
+
54
+ export async function requestSync(acct: Account, group: GroupRef): Promise<void> {
55
+ await deliver(acct, group.creator, encodeGroupSyncRequest(group.groupId), true);
56
+ }
57
+
58
+ async function fanOut(acct: Account, target: Target, direct: Uint8Array, grouped: (g: GroupRef) => Uint8Array): Promise<string[]> {
59
+ if (target.kind === 'user') return [await deliver(acct, target.id, direct, false)];
60
+ const ids: string[] = [];
61
+ for (const member of recipientsOf(acct, target.group)) ids.push(await deliver(acct, member, grouped(target.group), true));
62
+ if (ids.length === 0) throw new TrainError('threema_empty_group', 'nobody else is in this group', { retryable: false });
63
+ return ids;
64
+ }
65
+
66
+ interface SendArgs {
67
+ line: string;
68
+ text?: unknown;
69
+ replyTo?: unknown;
70
+ account?: string;
71
+ attachments?: unknown;
72
+ }
73
+
74
+ const filesOf = (raw: unknown): OutgoingFile[] =>
75
+ (Array.isArray(raw) ? raw : [])
76
+ .map((a) => (a ?? {}) as Record<string, unknown>)
77
+ .filter((a) => typeof a.path === 'string' && a.path !== '')
78
+ .map((a) => ({ path: String(a.path), mime: typeof a.mime === 'string' && a.mime !== '' ? a.mime : 'application/octet-stream', name: typeof a.name === 'string' && a.name !== '' ? a.name : 'file' }));
79
+
80
+ function textOf(a: SendArgs, files: OutgoingFile[]): string {
81
+ const text = typeof a.text === 'string' ? a.text : '';
82
+ if (text === '' && files.length === 0)
83
+ throw new TrainError('threema_text_required', 'give some text or a file to send', { retryable: false });
84
+ return text;
85
+ }
86
+
87
+ async function sendFiles(acct: Account, target: Target, files: OutgoingFile[], caption: string | null): Promise<{ ids: string[]; labels: string[] }> {
88
+ const ids: string[] = [];
89
+ const labels: string[] = [];
90
+ for (const [i, file] of files.entries()) {
91
+ const packed = await packFile(acct, file, i === 0 ? caption : null);
92
+ const sent = await fanOut(acct, target, encodeFileFor(null, packed), (g) => encodeFileFor(g, packed));
93
+ ids.push(...sent);
94
+ labels.push(fileLabel(file));
95
+ }
96
+ return { ids, labels };
97
+ }
98
+
99
+ function messageIdOf(raw: unknown, field: string): string {
100
+ const id = typeof raw === 'string' ? raw.toLowerCase() : '';
101
+ if (!MESSAGE_ID_RE.test(id))
102
+ throw new TrainError('threema_bad_message_id', `${field} must be a Threema message id, 16 hex characters`, { retryable: false });
103
+ return id;
104
+ }
105
+
106
+ const replyTargetOf = (raw: unknown): string | undefined => (raw === undefined || raw === null || raw === '' ? undefined : messageIdOf(raw, 'replyTo'));
107
+
108
+ function bodyOf(text: string, replyTo: string | undefined): string {
109
+ const body = replyTo === undefined ? text : quoted(replyTo, text);
110
+ if (Buffer.byteLength(body, 'utf8') > MAX_TEXT_BYTES)
111
+ throw new TrainError('threema_message_too_long', `Threema carries at most ${MAX_TEXT_BYTES} bytes of text per message; split it up`, { retryable: false });
112
+ return body;
113
+ }
114
+
115
+ interface Sent {
116
+ ids: string[];
117
+ labels: string[];
118
+ }
119
+
120
+ const resultOf = (acct: Account, sent: Sent): Record<string, unknown> => ({
121
+ messageId: sent.ids[0] ?? '',
122
+ account: acct.cfg.id,
123
+ ...(sent.ids.length > 1 ? { messageIds: sent.ids } : {}),
124
+ ...(sent.labels.length > 0 ? { attachments: sent.labels } : {}),
125
+ });
126
+
127
+ export async function send(id: string, args: Args): Promise<void> {
128
+ const a = args as unknown as SendArgs;
129
+ const files = filesOf(a.attachments);
130
+ const text = textOf(a, files);
131
+ const replyTo = replyTargetOf(a.replyTo);
132
+ const { acct, target } = targetOf(a.line, a.account);
133
+ const body = bodyOf(text, replyTo);
134
+ const sent: Sent =
135
+ files.length === 0
136
+ ? { ids: await fanOut(acct, target, encodeText(body), (g) => encodeGroupText(g, body)), labels: [] }
137
+ : await sendFiles(acct, target, files, body === '' ? null : body);
138
+ emitOutbound(acct.cfg.id, a.line, sent.ids[0] ?? '', text === '' ? `[${sent.labels.join(', ')}]` : text, replyTo);
139
+ respond(id, { result: resultOf(acct, sent) });
140
+ }
141
+
142
+ interface ReactArgs {
143
+ line: string;
144
+ messageId?: unknown;
145
+ emoji?: unknown;
146
+ account?: string;
147
+ action?: unknown;
148
+ }
149
+
150
+ export async function react(id: string, args: Args): Promise<void> {
151
+ const a = args as unknown as ReactArgs;
152
+ const emoji = typeof a.emoji === 'string' ? a.emoji.trim() : '';
153
+ if (emoji === '') throw new TrainError('threema_emoji_required', 'react needs an emoji', { retryable: false });
154
+ const messageId = messageIdOf(a.messageId, 'messageId');
155
+ const removed = a.action === 'removed';
156
+ const { acct, target } = targetOf(a.line, a.account);
157
+ const ids = await fanOut(acct, target, encodeReaction(messageId, emoji, removed), (g) => encodeGroupReaction(g, messageId, emoji, removed));
158
+ emitOutboundReaction(acct.cfg.id, a.line, ids[0] ?? '', emoji, messageId, removed);
159
+ respond(id, { result: { messageId: ids[0] ?? '', account: acct.cfg.id, emoji, removed } });
160
+ }
@@ -0,0 +1,13 @@
1
+ import type { Station, Verb } from '@metro-labs/core/stations/types';
2
+ import { groupFiles } from './groups.js';
3
+
4
+ export const threemaStation: Station = {
5
+ name: 'threema',
6
+ hasAccounts: true,
7
+ hasTrain: true,
8
+ messageVerbs: new Set<Verb>(['send', 'reply', 'react', 'unreact']),
9
+ attachmentMode: 'canonical',
10
+ forget: groupFiles.forget,
11
+ forgetExcept: groupFiles.forgetExcept,
12
+ tools: [],
13
+ };
@@ -0,0 +1,117 @@
1
+ import { createPrivateKey, createPublicKey } from 'node:crypto';
2
+ import {
3
+ GATEWAY_API,
4
+ isGatewayId,
5
+ normalizeThreemaId,
6
+ parsePrivateKey,
7
+ } from './ids.js';
8
+
9
+ export { parsePrivateKey } from './ids.js';
10
+
11
+ export class ThreemaGatewayError extends Error {}
12
+
13
+ export interface ThreemaCredentials {
14
+ gatewayId: string;
15
+ secret: string;
16
+ privateKey: string;
17
+ }
18
+
19
+ export interface ThreemaGatewayIdentity {
20
+ gatewayId: string;
21
+ publicKey: string;
22
+ credits: number;
23
+ }
24
+
25
+ const PKCS8_X25519_PREFIX = Buffer.from(
26
+ '302e020100300506032b656e04220420',
27
+ 'hex',
28
+ );
29
+ const TIMEOUT_MS = 10_000;
30
+ const PUBLIC_KEY_RE = /^[0-9a-f]{64}$/;
31
+
32
+ export function publicKeyOf(privateKeyHex: string): string {
33
+ const der = Buffer.concat([
34
+ PKCS8_X25519_PREFIX,
35
+ Buffer.from(privateKeyHex, 'hex'),
36
+ ]);
37
+ const key = createPrivateKey({ key: der, format: 'der', type: 'pkcs8' });
38
+ const jwk = createPublicKey(key).export({ format: 'jwk' });
39
+ return Buffer.from(String(jwk.x), 'base64url').toString('hex');
40
+ }
41
+
42
+ async function gatewayGet(
43
+ path: string,
44
+ gatewayId: string,
45
+ secret: string,
46
+ ): Promise<Response> {
47
+ const query = new URLSearchParams({ from: gatewayId, secret });
48
+ try {
49
+ return await fetch(`${GATEWAY_API}${path}?${query.toString()}`, {
50
+ signal: AbortSignal.timeout(TIMEOUT_MS),
51
+ });
52
+ } catch {
53
+ throw new ThreemaGatewayError(
54
+ 'could not reach the Threema Gateway to check those credentials',
55
+ );
56
+ }
57
+ }
58
+
59
+ function refused(res: Response, what: string): ThreemaGatewayError {
60
+ if (res.status === 401)
61
+ return new ThreemaGatewayError(
62
+ 'Threema rejected that Gateway ID or API secret',
63
+ );
64
+ if (res.status === 404)
65
+ return new ThreemaGatewayError(`Threema does not know ${what}`);
66
+ return new ThreemaGatewayError(
67
+ `the Threema Gateway answered ${res.status} while checking ${what}`,
68
+ );
69
+ }
70
+
71
+ async function fetchCredits(gatewayId: string, secret: string): Promise<number> {
72
+ const res = await gatewayGet('/credits', gatewayId, secret);
73
+ if (!res.ok) throw refused(res, 'the credit balance');
74
+ const credits = Number((await res.text()).trim());
75
+ if (!Number.isInteger(credits))
76
+ throw new ThreemaGatewayError(
77
+ 'the Threema Gateway gave an unexpected answer for the credit balance',
78
+ );
79
+ return credits;
80
+ }
81
+
82
+ async function fetchPublicKey(gatewayId: string, secret: string): Promise<string> {
83
+ const res = await gatewayGet(
84
+ `/pubkeys/${encodeURIComponent(gatewayId)}`,
85
+ gatewayId,
86
+ secret,
87
+ );
88
+ if (!res.ok) throw refused(res, `the public key of ${gatewayId}`);
89
+ const hex = (await res.text()).trim().toLowerCase();
90
+ if (!PUBLIC_KEY_RE.test(hex))
91
+ throw new ThreemaGatewayError(
92
+ 'the Threema Gateway gave an unexpected answer for the public key',
93
+ );
94
+ return hex;
95
+ }
96
+
97
+ export async function verifyThreemaGateway(
98
+ input: ThreemaCredentials,
99
+ ): Promise<ThreemaGatewayIdentity> {
100
+ const gatewayId = normalizeThreemaId(input.gatewayId);
101
+ if (!isGatewayId(gatewayId))
102
+ throw new ThreemaGatewayError(
103
+ 'a Threema Gateway ID is a * followed by 7 letters or digits, as in *ABCDEFG',
104
+ );
105
+ const privateKey = parsePrivateKey(input.privateKey);
106
+ if (privateKey === null)
107
+ throw new ThreemaGatewayError(
108
+ 'the private key is the 64 hex characters from the key file Threema gave you, with or without its private: prefix',
109
+ );
110
+ const credits = await fetchCredits(gatewayId, input.secret);
111
+ const registered = await fetchPublicKey(gatewayId, input.secret);
112
+ if (registered !== publicKeyOf(privateKey))
113
+ throw new ThreemaGatewayError(
114
+ `that private key does not belong to ${gatewayId}: the Gateway holds a different public key for it`,
115
+ );
116
+ return { gatewayId, publicKey: registered, credits };
117
+ }