@stage-labs/metro 0.1.0-beta.16 → 0.1.0-beta.161

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 (309) hide show
  1. package/dist/api.js +8 -57
  2. package/dist/claude-settings.js +40 -0
  3. package/dist/claude.js +182 -0
  4. package/dist/cli.js +56 -39
  5. package/dist/control.js +74 -0
  6. package/dist/hold.js +211 -0
  7. package/dist/local.js +77 -0
  8. package/dist/mcp-config.js +25 -0
  9. package/dist/node-name.js +71 -0
  10. package/dist/onboarding.js +43 -0
  11. package/dist/provider-flags.js +9 -0
  12. package/dist/route.js +50 -0
  13. package/dist/runtime-install.js +97 -0
  14. package/dist/runtime.js +74 -0
  15. package/dist/serve.js +155 -0
  16. package/dist/service.js +242 -0
  17. package/dist/tail.js +119 -0
  18. package/dist/update.js +13 -1
  19. package/package.json +6 -5
  20. package/runtime/marketplace/.claude-plugin/marketplace.json +12 -0
  21. package/runtime/marketplace/plugin/.claude-plugin/plugin.json +18 -0
  22. package/runtime/marketplace/plugin/.mcp.json +1 -0
  23. package/runtime/marketplace/plugin/bin/guard.mjs +55 -0
  24. package/runtime/marketplace/plugin/bin/metro-plugin.mjs +40 -0
  25. package/runtime/marketplace/plugin/bin/session-start.mjs +31 -0
  26. package/runtime/marketplace/plugin/hooks/hooks.json +25 -0
  27. package/runtime/marketplace/plugin/orchestrator.md +16 -0
  28. package/runtime/node_modules/@metro-labs/core/package.json +44 -0
  29. package/runtime/node_modules/@metro-labs/core/src/address.ts +6 -0
  30. package/runtime/node_modules/@metro-labs/core/src/endpoints.ts +7 -0
  31. package/runtime/node_modules/@metro-labs/core/src/events.ts +120 -0
  32. package/runtime/node_modules/@metro-labs/core/src/identity.ts +7 -0
  33. package/runtime/node_modules/@metro-labs/core/src/ids.ts +21 -0
  34. package/runtime/node_modules/@metro-labs/core/src/is-record.ts +2 -0
  35. package/runtime/node_modules/@metro-labs/core/src/lines.ts +87 -0
  36. package/runtime/node_modules/@metro-labs/core/src/log.ts +42 -0
  37. package/runtime/node_modules/@metro-labs/core/src/protocol.ts +226 -0
  38. package/runtime/node_modules/@metro-labs/core/src/secure-fs.ts +57 -0
  39. package/runtime/node_modules/@metro-labs/core/src/station-names.ts +13 -0
  40. package/runtime/node_modules/@metro-labs/core/src/stations/account-store.ts +60 -0
  41. package/runtime/node_modules/@metro-labs/core/src/stations/attachments.ts +196 -0
  42. package/runtime/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +88 -0
  43. package/runtime/node_modules/@metro-labs/core/src/stations/profile.ts +58 -0
  44. package/runtime/node_modules/@metro-labs/core/src/stations/station-runtime.ts +64 -0
  45. package/runtime/node_modules/@metro-labs/core/src/stations/types.ts +112 -0
  46. package/runtime/node_modules/@metro-labs/core/src/str.ts +5 -0
  47. package/runtime/node_modules/@metro-labs/core/src/train-error.ts +53 -0
  48. package/runtime/node_modules/@metro-labs/core/src/version.ts +2 -0
  49. package/runtime/node_modules/@metro-labs/daemon/package.json +47 -0
  50. package/runtime/node_modules/@metro-labs/daemon/src/agents/account-attach.ts +19 -0
  51. package/runtime/node_modules/@metro-labs/daemon/src/agents/account-routes.ts +72 -0
  52. package/runtime/node_modules/@metro-labs/daemon/src/agents/accounts-api.ts +388 -0
  53. package/runtime/node_modules/@metro-labs/daemon/src/agents/admin.ts +39 -0
  54. package/runtime/node_modules/@metro-labs/daemon/src/agents/allowlist.ts +37 -0
  55. package/runtime/node_modules/@metro-labs/daemon/src/agents/api.ts +225 -0
  56. package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +122 -0
  57. package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +364 -0
  58. package/runtime/node_modules/@metro-labs/daemon/src/agents/files.ts +185 -0
  59. package/runtime/node_modules/@metro-labs/daemon/src/agents/keys.ts +40 -0
  60. package/runtime/node_modules/@metro-labs/daemon/src/agents/map.ts +95 -0
  61. package/runtime/node_modules/@metro-labs/daemon/src/agents/scope.ts +64 -0
  62. package/runtime/node_modules/@metro-labs/daemon/src/agents/senders.ts +38 -0
  63. package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +200 -0
  64. package/runtime/node_modules/@metro-labs/daemon/src/boot/connect-hint.ts +39 -0
  65. package/runtime/node_modules/@metro-labs/daemon/src/boot/crash-guard.ts +49 -0
  66. package/runtime/node_modules/@metro-labs/daemon/src/boot/local-owner.ts +30 -0
  67. package/runtime/node_modules/@metro-labs/daemon/src/boot/paths.ts +106 -0
  68. package/runtime/node_modules/@metro-labs/daemon/src/channels/addressed.ts +25 -0
  69. package/runtime/node_modules/@metro-labs/daemon/src/channels/dedupe.ts +24 -0
  70. package/runtime/node_modules/@metro-labs/daemon/src/channels/inbound.ts +380 -0
  71. package/runtime/node_modules/@metro-labs/daemon/src/channels/media-note.ts +85 -0
  72. package/runtime/node_modules/@metro-labs/daemon/src/channels/pending.ts +75 -0
  73. package/runtime/node_modules/@metro-labs/daemon/src/channels/relay.ts +129 -0
  74. package/runtime/node_modules/@metro-labs/daemon/src/channels/webhook-note.ts +51 -0
  75. package/runtime/node_modules/@metro-labs/daemon/src/claude/api.ts +354 -0
  76. package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +358 -0
  77. package/runtime/node_modules/@metro-labs/daemon/src/claude/login.ts +169 -0
  78. package/runtime/node_modules/@metro-labs/daemon/src/claude/onboarding.ts +56 -0
  79. package/runtime/node_modules/@metro-labs/daemon/src/claude/plugin-install.ts +140 -0
  80. package/runtime/node_modules/@metro-labs/daemon/src/claude/session-files.ts +90 -0
  81. package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +253 -0
  82. package/runtime/node_modules/@metro-labs/daemon/src/claude/settings.ts +117 -0
  83. package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +263 -0
  84. package/runtime/node_modules/@metro-labs/daemon/src/claude/skills.ts +120 -0
  85. package/runtime/node_modules/@metro-labs/daemon/src/claude/version.ts +109 -0
  86. package/runtime/node_modules/@metro-labs/daemon/src/connectors/api.ts +313 -0
  87. package/runtime/node_modules/@metro-labs/daemon/src/connectors/config.ts +157 -0
  88. package/runtime/node_modules/@metro-labs/daemon/src/connectors/health.ts +18 -0
  89. package/runtime/node_modules/@metro-labs/daemon/src/connectors/model.ts +69 -0
  90. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-client.ts +166 -0
  91. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-discovery.ts +139 -0
  92. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-pending.ts +65 -0
  93. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-routes.ts +171 -0
  94. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth.ts +155 -0
  95. package/runtime/node_modules/@metro-labs/daemon/src/connectors/plugin-sync.ts +122 -0
  96. package/runtime/node_modules/@metro-labs/daemon/src/connectors/reach.ts +27 -0
  97. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-target.ts +39 -0
  98. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay.ts +304 -0
  99. package/runtime/node_modules/@metro-labs/daemon/src/connectors/store.ts +299 -0
  100. package/runtime/node_modules/@metro-labs/daemon/src/connectors/tools.ts +108 -0
  101. package/runtime/node_modules/@metro-labs/daemon/src/connectors/url.ts +59 -0
  102. package/runtime/node_modules/@metro-labs/daemon/src/connectors/verify.ts +101 -0
  103. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-grant.ts +99 -0
  104. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-owner.ts +32 -0
  105. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-serve.ts +121 -0
  106. package/runtime/node_modules/@metro-labs/daemon/src/files/upload-api.ts +294 -0
  107. package/runtime/node_modules/@metro-labs/daemon/src/files/upload-store.ts +181 -0
  108. package/runtime/node_modules/@metro-labs/daemon/src/gateway/bedrock.ts +259 -0
  109. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-auth.ts +231 -0
  110. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-device.ts +91 -0
  111. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-stream.ts +288 -0
  112. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-translate.ts +203 -0
  113. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +231 -0
  114. package/runtime/node_modules/@metro-labs/daemon/src/gateway/eventstream.ts +123 -0
  115. package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +187 -0
  116. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +247 -0
  117. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-auth.ts +182 -0
  118. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-client.ts +46 -0
  119. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-setup.ts +150 -0
  120. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-stream.ts +148 -0
  121. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-translate.ts +229 -0
  122. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini.ts +283 -0
  123. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +362 -0
  124. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-config.ts +232 -0
  125. package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter.ts +90 -0
  126. package/runtime/node_modules/@metro-labs/daemon/src/gateway/provider-models.ts +65 -0
  127. package/runtime/node_modules/@metro-labs/daemon/src/gateway/served.ts +17 -0
  128. package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage.ts +242 -0
  129. package/runtime/node_modules/@metro-labs/daemon/src/mcp/accounts.ts +151 -0
  130. package/runtime/node_modules/@metro-labs/daemon/src/mcp/call-tools.ts +216 -0
  131. package/runtime/node_modules/@metro-labs/daemon/src/mcp/channel-owner.ts +29 -0
  132. package/runtime/node_modules/@metro-labs/daemon/src/mcp/ctx.ts +62 -0
  133. package/runtime/node_modules/@metro-labs/daemon/src/mcp/event-store.ts +91 -0
  134. package/runtime/node_modules/@metro-labs/daemon/src/mcp/frame-scope.ts +21 -0
  135. package/runtime/node_modules/@metro-labs/daemon/src/mcp/group-tools.ts +129 -0
  136. package/runtime/node_modules/@metro-labs/daemon/src/mcp/index.ts +186 -0
  137. package/runtime/node_modules/@metro-labs/daemon/src/mcp/member-tools.ts +72 -0
  138. package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-relay.ts +66 -0
  139. package/runtime/node_modules/@metro-labs/daemon/src/mcp/profile-tool.ts +94 -0
  140. package/runtime/node_modules/@metro-labs/daemon/src/mcp/raw-get-stream.ts +207 -0
  141. package/runtime/node_modules/@metro-labs/daemon/src/mcp/request-identity.ts +47 -0
  142. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-registry.ts +172 -0
  143. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-route.ts +43 -0
  144. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session.ts +215 -0
  145. package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-dispatch.ts +219 -0
  146. package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-schemas.ts +400 -0
  147. package/runtime/node_modules/@metro-labs/daemon/src/mcp/upload-tool.ts +66 -0
  148. package/runtime/node_modules/@metro-labs/daemon/src/monitor/api.ts +237 -0
  149. package/runtime/node_modules/@metro-labs/daemon/src/net/tunnel.ts +389 -0
  150. package/runtime/node_modules/@metro-labs/daemon/src/routes/bearer.ts +35 -0
  151. package/runtime/node_modules/@metro-labs/daemon/src/routes/body.ts +24 -0
  152. package/runtime/node_modules/@metro-labs/daemon/src/routes/http.ts +370 -0
  153. package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +208 -0
  154. package/runtime/node_modules/@metro-labs/daemon/src/routes/session-apis.ts +58 -0
  155. package/runtime/node_modules/@metro-labs/daemon/src/routes/session.ts +29 -0
  156. package/runtime/node_modules/@metro-labs/daemon/src/routes/threema-callback.ts +103 -0
  157. package/runtime/node_modules/@metro-labs/daemon/src/server/control.ts +65 -0
  158. package/runtime/node_modules/@metro-labs/daemon/src/server/machine.ts +80 -0
  159. package/runtime/node_modules/@metro-labs/daemon/src/server/owner.ts +48 -0
  160. package/runtime/node_modules/@metro-labs/daemon/src/server/update.ts +117 -0
  161. package/runtime/node_modules/@metro-labs/daemon/src/server.ts +2 -0
  162. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-inline.ts +100 -0
  163. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-interactive.ts +161 -0
  164. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-resolve.ts +202 -0
  165. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-session.ts +280 -0
  166. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-xmtp.ts +111 -0
  167. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach.ts +248 -0
  168. package/runtime/node_modules/@metro-labs/daemon/src/stations/materialize.ts +261 -0
  169. package/runtime/node_modules/@metro-labs/daemon/src/stations/registry.ts +37 -0
  170. package/runtime/node_modules/@metro-labs/daemon/src/stations/runtime-deps.ts +82 -0
  171. package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor-io.ts +97 -0
  172. package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor.ts +320 -0
  173. package/runtime/node_modules/@metro-labs/daemon/src/stations/threema-callbacks.ts +32 -0
  174. package/runtime/node_modules/@metro-labs/daemon/src/stations/train-call.ts +22 -0
  175. package/runtime/node_modules/@metro-labs/daemon/src/terminal/api.ts +97 -0
  176. package/runtime/node_modules/@metro-labs/daemon/src/terminal/socket.ts +123 -0
  177. package/runtime/node_modules/@metro-labs/daemon/src/terminal/tickets.ts +40 -0
  178. package/runtime/node_modules/@metro-labs/discord-bot/package.json +28 -0
  179. package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +118 -0
  180. package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +390 -0
  181. package/runtime/node_modules/@metro-labs/discord-bot/src/api-base.ts +1 -0
  182. package/runtime/node_modules/@metro-labs/discord-bot/src/attachments.ts +29 -0
  183. package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +220 -0
  184. package/runtime/node_modules/@metro-labs/discord-bot/src/group-actions.ts +50 -0
  185. package/runtime/node_modules/@metro-labs/discord-bot/src/group.ts +115 -0
  186. package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +127 -0
  187. package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +215 -0
  188. package/runtime/node_modules/@metro-labs/discord-bot/src/profile.ts +48 -0
  189. package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +43 -0
  190. package/runtime/node_modules/@metro-labs/discord-bot/src/station.ts +21 -0
  191. package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +82 -0
  192. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-speak.ts +195 -0
  193. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-transcribe.ts +292 -0
  194. package/runtime/node_modules/@metro-labs/discord-bot/src/voice.ts +197 -0
  195. package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +5 -0
  196. package/runtime/node_modules/@metro-labs/http/package.json +29 -0
  197. package/runtime/node_modules/@metro-labs/http/src/api-error.ts +8 -0
  198. package/runtime/node_modules/@metro-labs/http/src/api-http.ts +106 -0
  199. package/runtime/node_modules/@metro-labs/http/src/cors.ts +29 -0
  200. package/runtime/node_modules/@metro-labs/http/src/mode-api.ts +32 -0
  201. package/runtime/node_modules/@metro-labs/http/src/return-to.ts +21 -0
  202. package/runtime/node_modules/@metro-labs/http/src/workos-token.ts +150 -0
  203. package/runtime/node_modules/@metro-labs/telegram/package.json +26 -0
  204. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +80 -0
  205. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +256 -0
  206. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +87 -0
  207. package/runtime/node_modules/@metro-labs/telegram/src/client.ts +77 -0
  208. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +156 -0
  209. package/runtime/node_modules/@metro-labs/telegram/src/group.ts +261 -0
  210. package/runtime/node_modules/@metro-labs/telegram/src/history.ts +52 -0
  211. package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +78 -0
  212. package/runtime/node_modules/@metro-labs/telegram/src/index.ts +46 -0
  213. package/runtime/node_modules/@metro-labs/telegram/src/login.ts +166 -0
  214. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +80 -0
  215. package/runtime/node_modules/@metro-labs/telegram/src/members.ts +75 -0
  216. package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +22 -0
  217. package/runtime/node_modules/@metro-labs/telegram/src/profile.ts +36 -0
  218. package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +167 -0
  219. package/runtime/node_modules/@metro-labs/telegram/src/station.ts +26 -0
  220. package/runtime/node_modules/@metro-labs/telegram/src/types.ts +7 -0
  221. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +1 -0
  222. package/runtime/node_modules/@metro-labs/telegram-bot/package.json +25 -0
  223. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +120 -0
  224. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +297 -0
  225. package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
  226. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +201 -0
  227. package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +109 -0
  228. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +166 -0
  229. package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
  230. package/runtime/node_modules/@metro-labs/telegram-bot/src/profile.ts +33 -0
  231. package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +19 -0
  232. package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +58 -0
  233. package/runtime/node_modules/@metro-labs/telegram-bot/src/verify.ts +38 -0
  234. package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +3 -0
  235. package/runtime/node_modules/@metro-labs/threema/package.json +26 -0
  236. package/runtime/node_modules/@metro-labs/threema/src/accounts.ts +107 -0
  237. package/runtime/node_modules/@metro-labs/threema/src/actions.ts +135 -0
  238. package/runtime/node_modules/@metro-labs/threema/src/api.ts +152 -0
  239. package/runtime/node_modules/@metro-labs/threema/src/crypto.ts +100 -0
  240. package/runtime/node_modules/@metro-labs/threema/src/files.ts +93 -0
  241. package/runtime/node_modules/@metro-labs/threema/src/format.ts +182 -0
  242. package/runtime/node_modules/@metro-labs/threema/src/groups.ts +95 -0
  243. package/runtime/node_modules/@metro-labs/threema/src/ids.ts +21 -0
  244. package/runtime/node_modules/@metro-labs/threema/src/inbound.ts +90 -0
  245. package/runtime/node_modules/@metro-labs/threema/src/index.ts +29 -0
  246. package/runtime/node_modules/@metro-labs/threema/src/messages.ts +264 -0
  247. package/runtime/node_modules/@metro-labs/threema/src/outbound.ts +160 -0
  248. package/runtime/node_modules/@metro-labs/threema/src/station.ts +10 -0
  249. package/runtime/node_modules/@metro-labs/threema/src/verify.ts +117 -0
  250. package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
  251. package/runtime/node_modules/@metro-labs/webhook/src/station.ts +37 -0
  252. package/runtime/node_modules/@metro-labs/whatsapp/package.json +27 -0
  253. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +66 -0
  254. package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
  255. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +203 -0
  256. package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +72 -0
  257. package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +129 -0
  258. package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +359 -0
  259. package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
  260. package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +138 -0
  261. package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +57 -0
  262. package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +40 -0
  263. package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +59 -0
  264. package/runtime/node_modules/@metro-labs/whatsapp/src/logger.ts +60 -0
  265. package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +176 -0
  266. package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +84 -0
  267. package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +22 -0
  268. package/runtime/node_modules/@metro-labs/whatsapp/src/outbox.ts +38 -0
  269. package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +185 -0
  270. package/runtime/node_modules/@metro-labs/whatsapp/src/resolve.ts +52 -0
  271. package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +20 -0
  272. package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +98 -0
  273. package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +6 -0
  274. package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +5 -0
  275. package/runtime/node_modules/@metro-labs/xmtp/package.json +35 -0
  276. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +115 -0
  277. package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +110 -0
  278. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +299 -0
  279. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +157 -0
  280. package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +87 -0
  281. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +380 -0
  282. package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +107 -0
  283. package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +171 -0
  284. package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +239 -0
  285. package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +55 -0
  286. package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +335 -0
  287. package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +80 -0
  288. package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +34 -0
  289. package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +56 -0
  290. package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +47 -0
  291. package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +125 -0
  292. package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +157 -0
  293. package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
  294. package/runtime/node_modules/@metro-labs/xmtp/src/names.ts +61 -0
  295. package/runtime/node_modules/@metro-labs/xmtp/src/profile.ts +153 -0
  296. package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +349 -0
  297. package/runtime/node_modules/@metro-labs/xmtp/src/smart.ts +72 -0
  298. package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +16 -0
  299. package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +126 -0
  300. package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +212 -0
  301. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +90 -0
  302. package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +98 -0
  303. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +53 -0
  304. package/runtime/runtime.json +3 -0
  305. package/runtime/server.ts +1 -0
  306. package/runtime/stations.json +39 -0
  307. package/runtime/trains/.keep +0 -0
  308. package/dist/login.js +0 -102
  309. package/dist/store.js +0 -63
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@metro-labs/threema",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./src/station.ts",
8
+ "./train": "./src/index.ts",
9
+ "./verify": "./src/verify.ts"
10
+ },
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "test": "tsc --noEmit && bun test test/"
14
+ },
15
+ "dependencies": {
16
+ "@metro-labs/core": "workspace:*",
17
+ "tweetnacl": "^1.0.3"
18
+ },
19
+ "devDependencies": {
20
+ "@stage-labs/config": "0.1.0-beta.2",
21
+ "eslint": "^10.3.0",
22
+ "knip": "^6.15.0",
23
+ "madge": "^8.0.0",
24
+ "typescript": "^5"
25
+ }
26
+ }
@@ -0,0 +1,107 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ import {
4
+ makeAccountStore,
5
+ resolveAccountId,
6
+ type Die,
7
+ } from '@metro-labs/core/stations/account-store';
8
+ import { Line } from '@metro-labs/core/lines';
9
+ import { fetchPublicKey } from './api.js';
10
+ import { hexToBytes, keyPairFrom, type KeyPair } from './crypto.js';
11
+ import { isGatewayId, normalizeThreemaId, parsePrivateKey } from './ids.js';
12
+ import { GroupStore, parseGroupKey } from './groups.js';
13
+ import type { GroupRef } from './messages.js';
14
+
15
+ const ACCOUNTS_FILE =
16
+ process.env.THREEMA_ACCOUNTS_FILE ??
17
+ join(homedir(), '.metro', 'threema-accounts.json');
18
+
19
+ export interface AccountConfig {
20
+ id: string;
21
+ gatewayId: string;
22
+ secret: string;
23
+ privateKey: string;
24
+ callbackId?: string;
25
+ callbackToken?: string;
26
+ owner?: string;
27
+ }
28
+
29
+ function checkAccount(a: AccountConfig, die: Die): void {
30
+ if (!a.id) die('account missing id');
31
+ if (typeof a.gatewayId !== 'string' || !isGatewayId(a.gatewayId))
32
+ die(`account '${a.id}' has no Gateway ID`);
33
+ if (typeof a.secret !== 'string' || a.secret === '')
34
+ die(`account '${a.id}' missing secret`);
35
+ if (typeof a.privateKey !== 'string' || parsePrivateKey(a.privateKey) === null)
36
+ die(`account '${a.id}' has no usable private key`);
37
+ }
38
+
39
+ export const { loadAccounts } = makeAccountStore<AccountConfig>({
40
+ prefix: 'threema',
41
+ file: ACCOUNTS_FILE,
42
+ validate(raw, die) {
43
+ const seenId = new Set<string>();
44
+ const seenGateway = new Set<string>();
45
+ for (const a of raw) {
46
+ checkAccount(a, die);
47
+ if (seenId.has(a.id)) die(`duplicate account id '${a.id}'`);
48
+ if (seenGateway.has(a.gatewayId))
49
+ die(`account '${a.id}' reuses the Gateway ID of another account`);
50
+ seenId.add(a.id);
51
+ seenGateway.add(a.gatewayId);
52
+ }
53
+ },
54
+ });
55
+
56
+ export interface Account {
57
+ cfg: AccountConfig;
58
+ keys: KeyPair;
59
+ publicKeys: Map<string, Uint8Array>;
60
+ groups: GroupStore;
61
+ }
62
+
63
+ export type Target = { kind: 'user'; id: string } | { kind: 'group'; group: GroupRef };
64
+
65
+ export const accounts = new Map<string, Account>();
66
+
67
+ export function bootAccount(cfg: AccountConfig): Account {
68
+ const key = parsePrivateKey(cfg.privateKey);
69
+ if (key === null) throw new Error(`account '${cfg.id}' has no usable private key`);
70
+ return { cfg, keys: keyPairFrom(key), publicKeys: new Map(), groups: new GroupStore(cfg.id, cfg.gatewayId) };
71
+ }
72
+
73
+ export function accountFor(id: string): Account {
74
+ const acct = accounts.get(id);
75
+ if (!acct)
76
+ throw new Error(
77
+ `unknown account '${id}' (have: ${[...accounts.keys()].join(', ')})`,
78
+ );
79
+ return acct;
80
+ }
81
+
82
+ export function targetOf(
83
+ line: string,
84
+ account?: string,
85
+ ): { acct: Account; target: Target } {
86
+ const parsed = Line.parseThreema(line);
87
+ if (parsed === null) throw new Error(`not a threema line: ${line}`);
88
+ const id = resolveAccountId(
89
+ accounts,
90
+ { account, line },
91
+ (l) => Line.parseThreema(l)?.accountId,
92
+ );
93
+ const group = parseGroupKey(parsed.resource);
94
+ const target: Target = group === null ? { kind: 'user', id: normalizeThreemaId(parsed.resource) } : { kind: 'group', group };
95
+ return { acct: accountFor(id), target };
96
+ }
97
+
98
+ export async function publicKeyFor(
99
+ acct: Account,
100
+ threemaId: string,
101
+ ): Promise<Uint8Array> {
102
+ const cached = acct.publicKeys.get(threemaId);
103
+ if (cached) return cached;
104
+ const key = hexToBytes(await fetchPublicKey(acct.cfg, threemaId), 'public key');
105
+ acct.publicKeys.set(threemaId, key);
106
+ return key;
107
+ }
@@ -0,0 +1,135 @@
1
+ import { TrainError } from '@metro-labs/core/train-error';
2
+ import {
3
+ makeStation,
4
+ respond,
5
+ type CallMsg,
6
+ } from '@metro-labs/core/stations/station-runtime';
7
+ import type { Normalized } from '@metro-labs/core/stations/messaging-normalize';
8
+ import { accountFor, accounts, publicKeyFor, targetOf, type Account } from './accounts.js';
9
+ import { hexToBytes, macMatches, open } from './crypto.js';
10
+ import { groupLineOf, type InboundMeta } from './format.js';
11
+ import { groupKey } from './groups.js';
12
+ import { deliver } from './inbound.js';
13
+ import { isThreemaId, normalizeThreemaId } from './ids.js';
14
+ import { decode } from './messages.js';
15
+ import { react, send } from './outbound.js';
16
+
17
+ export type { CallMsg };
18
+ export { MAX_TEXT_BYTES } from './outbound.js';
19
+
20
+ type Args = Record<string, unknown>;
21
+
22
+ const WEB_CHAT = 'https://web.threema.com/#!/messenger/conversation/contact/';
23
+
24
+ export const chatUrl = (gatewayId: string): string => `${WEB_CHAT}${encodeURIComponent(gatewayId)}`;
25
+
26
+ function listAccounts(id: string): void {
27
+ const list = [...accounts.values()].map((a) => ({
28
+ id: a.cfg.id,
29
+ handle: a.cfg.gatewayId,
30
+ url: chatUrl(a.cfg.gatewayId),
31
+ owner: a.cfg.owner ?? null,
32
+ gatewayId: a.cfg.gatewayId,
33
+ groups: a.groups.list().map((g) => ({ line: groupLineOf(a.cfg.id, g), name: g.name, members: g.members.length })),
34
+ ...(a.cfg.callbackId && a.cfg.callbackToken
35
+ ? { callbackId: a.cfg.callbackId, callbackToken: a.cfg.callbackToken }
36
+ : {}),
37
+ }));
38
+ respond(id, { result: { accounts: list } });
39
+ }
40
+
41
+ function listMembers(id: string, args: Args): void {
42
+ const line = typeof args.line === 'string' ? args.line : '';
43
+ const { acct, target } = targetOf(line, typeof args.account === 'string' ? args.account : undefined);
44
+ if (target.kind !== 'group') {
45
+ respond(id, { result: { members: [], capability: { supported: false, complete: false, reason: 'a Threema 1:1 chat has no roster' } } });
46
+ return;
47
+ }
48
+ const roster = acct.groups.get(target.group);
49
+ if (roster === undefined) {
50
+ respond(id, { result: { members: [], capability: { supported: true, complete: false, reason: `no member list yet for ${groupKey(target.group)}` } } });
51
+ return;
52
+ }
53
+ const members = roster.members.map((m) => ({ id: m, name: m, is_admin: m === roster.creator, is_bot: m === acct.cfg.gatewayId }));
54
+ respond(id, { result: { members, capability: { supported: true, complete: true, total: members.length } } });
55
+ }
56
+
57
+ interface CallbackArgs {
58
+ account: string;
59
+ from: string;
60
+ to: string;
61
+ messageId: string;
62
+ date: string;
63
+ nonce: string;
64
+ box: string;
65
+ mac: string;
66
+ nickname?: string;
67
+ }
68
+
69
+ const str = (v: unknown): string => (typeof v === 'string' ? v : '');
70
+
71
+ function parseCallback(args: Args): CallbackArgs {
72
+ const fields = {
73
+ account: str(args.account),
74
+ from: str(args.from),
75
+ to: str(args.to),
76
+ messageId: str(args.messageId),
77
+ date: str(args.date),
78
+ nonce: str(args.nonce),
79
+ box: str(args.box),
80
+ mac: str(args.mac),
81
+ };
82
+ for (const [key, value] of Object.entries(fields))
83
+ if (value === '')
84
+ throw new TrainError('threema_bad_callback', `callback is missing ${key}`, {
85
+ retryable: false,
86
+ });
87
+ const nickname = str(args.nickname);
88
+ return nickname === '' ? fields : { ...fields, nickname };
89
+ }
90
+
91
+ function checkedSender(acct: Account, cb: CallbackArgs): string {
92
+ if (!macMatches(acct.cfg.secret, cb, cb.mac))
93
+ throw new TrainError('threema_bad_mac', 'the callback MAC does not match this account API secret', { retryable: false });
94
+ const from = normalizeThreemaId(cb.from);
95
+ if (!isThreemaId(from))
96
+ throw new TrainError('threema_bad_callback', `'${cb.from}' is not a Threema ID`, { retryable: false });
97
+ if (normalizeThreemaId(cb.to) !== acct.cfg.gatewayId)
98
+ throw new TrainError('threema_wrong_recipient', `callback addressed to ${cb.to}, not to ${acct.cfg.gatewayId}`, { retryable: false });
99
+ return from;
100
+ }
101
+
102
+ async function callback(id: string, args: Args): Promise<void> {
103
+ const cb = parseCallback(args);
104
+ const acct = accountFor(cb.account);
105
+ const from = checkedSender(acct, cb);
106
+ const plain = open(
107
+ hexToBytes(cb.box, 'box'),
108
+ hexToBytes(cb.nonce, 'nonce'),
109
+ await publicKeyFor(acct, from),
110
+ acct.keys,
111
+ );
112
+ if (plain === null)
113
+ throw new TrainError(
114
+ 'threema_undecryptable',
115
+ `could not decrypt a message from ${from}; the private key may not match the Gateway ID`,
116
+ { retryable: false },
117
+ );
118
+ const meta: InboundMeta = { ...cb, from, messageId: cb.messageId.toLowerCase() };
119
+ respond(id, { result: { ok: true, kind: deliver(acct, meta, decode(plain)) } });
120
+ }
121
+
122
+ export function normalizeThreema(action: string, env: Args): Normalized {
123
+ if (action === 'reply')
124
+ return {
125
+ action: 'send',
126
+ args: { line: env.line, text: env.text, replyTo: env.replyTo, account: env.account },
127
+ };
128
+ if (action === 'unreact') return { action: 'react', args: { ...env, action: 'removed' } };
129
+ return { action, args: env };
130
+ }
131
+
132
+ export const handleCall = makeStation({
133
+ handlers: { accounts: listAccounts, send, react, callback, listMembers },
134
+ normalize: normalizeThreema,
135
+ });
@@ -0,0 +1,152 @@
1
+ import { errMsg } from '@metro-labs/core/log';
2
+ import { TrainError } from '@metro-labs/core/train-error';
3
+ import { GATEWAY_API, MESSAGE_ID_RE } from './ids.js';
4
+
5
+ const TIMEOUT_MS = 15_000;
6
+ const PUBLIC_KEY_RE = /^[0-9a-f]{64}$/;
7
+
8
+ export interface GatewayCreds {
9
+ gatewayId: string;
10
+ secret: string;
11
+ }
12
+
13
+ async function call(
14
+ path: string,
15
+ init: RequestInit,
16
+ label: string,
17
+ ): Promise<Response> {
18
+ try {
19
+ return await fetch(`${GATEWAY_API}${path}`, {
20
+ signal: AbortSignal.timeout(TIMEOUT_MS),
21
+ ...init,
22
+ });
23
+ } catch (err) {
24
+ throw new TrainError(
25
+ 'threema_unreachable',
26
+ `${label}: could not reach the Threema Gateway (${errMsg(err)})`,
27
+ { retryable: true },
28
+ );
29
+ }
30
+ }
31
+
32
+ const authQuery = (c: GatewayCreds): string =>
33
+ new URLSearchParams({ from: c.gatewayId, secret: c.secret }).toString();
34
+
35
+ function refusal(status: number, what: string): TrainError {
36
+ if (status === 401)
37
+ return new TrainError(
38
+ 'threema_unauthorized',
39
+ 'the Threema Gateway rejected this account credentials; check the Gateway ID and API secret',
40
+ { retryable: false },
41
+ );
42
+ if (status === 402)
43
+ return new TrainError(
44
+ 'threema_no_credits',
45
+ 'this Threema Gateway ID has no credits left; top it up at gateway.threema.ch',
46
+ { retryable: false },
47
+ );
48
+ if (status === 404)
49
+ return new TrainError('threema_unknown_id', `Threema does not know the ID ${what}`, {
50
+ retryable: false,
51
+ });
52
+ if (status === 413)
53
+ return new TrainError(
54
+ 'threema_message_too_long',
55
+ 'Threema refused the message as too long',
56
+ { retryable: false },
57
+ );
58
+ if (status === 400)
59
+ return new TrainError('threema_bad_recipient', `Threema refused the recipient ${what}`, {
60
+ retryable: false,
61
+ });
62
+ return new TrainError(
63
+ 'threema_gateway_error',
64
+ `the Threema Gateway answered ${status}`,
65
+ { retryable: status >= 500 },
66
+ );
67
+ }
68
+
69
+ export async function fetchPublicKey(
70
+ c: GatewayCreds,
71
+ threemaId: string,
72
+ ): Promise<string> {
73
+ const res = await call(
74
+ `/pubkeys/${encodeURIComponent(threemaId)}?${authQuery(c)}`,
75
+ {},
76
+ 'pubkeys',
77
+ );
78
+ if (!res.ok) throw refusal(res.status, threemaId);
79
+ const hex = (await res.text()).trim().toLowerCase();
80
+ if (!PUBLIC_KEY_RE.test(hex))
81
+ throw new TrainError(
82
+ 'threema_gateway_error',
83
+ `pubkeys: the Threema Gateway gave an unexpected answer for ${threemaId}`,
84
+ );
85
+ return hex;
86
+ }
87
+
88
+ export async function fetchCredits(c: GatewayCreds): Promise<number> {
89
+ const res = await call(`/credits?${authQuery(c)}`, {}, 'credits');
90
+ if (!res.ok) throw refusal(res.status, c.gatewayId);
91
+ const credits = Number((await res.text()).trim());
92
+ if (!Number.isInteger(credits))
93
+ throw new TrainError(
94
+ 'threema_gateway_error',
95
+ 'credits: the Threema Gateway gave an unexpected answer',
96
+ );
97
+ return credits;
98
+ }
99
+
100
+ export async function sendE2E(
101
+ c: GatewayCreds,
102
+ to: string,
103
+ nonceHex: string,
104
+ boxHex: string,
105
+ group = false,
106
+ ): Promise<string> {
107
+ const body = new URLSearchParams({
108
+ from: c.gatewayId,
109
+ to,
110
+ nonce: nonceHex,
111
+ box: boxHex,
112
+ secret: c.secret,
113
+ ...(group ? { group: '1' } : {}),
114
+ });
115
+ const res = await call(
116
+ '/send_e2e',
117
+ {
118
+ method: 'POST',
119
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
120
+ body: body.toString(),
121
+ },
122
+ 'send_e2e',
123
+ );
124
+ if (!res.ok) throw refusal(res.status, to);
125
+ const messageId = (await res.text()).trim().toLowerCase();
126
+ if (!MESSAGE_ID_RE.test(messageId))
127
+ throw new TrainError(
128
+ 'threema_gateway_error',
129
+ 'send_e2e: the Threema Gateway answered without a message id',
130
+ );
131
+ return messageId;
132
+ }
133
+
134
+ const BLOB_ID_RE = /^[0-9a-f]{32}$/;
135
+ const BLOB_TIMEOUT_MS = 120_000;
136
+
137
+ export async function uploadBlob(c: GatewayCreds, data: Uint8Array): Promise<string> {
138
+ const form = new FormData();
139
+ form.append('blob', new Blob([Buffer.from(data)]), 'blob');
140
+ const res = await call(`/upload_blob?${authQuery(c)}`, { method: 'POST', body: form, signal: AbortSignal.timeout(BLOB_TIMEOUT_MS) }, 'upload_blob');
141
+ if (!res.ok) throw refusal(res.status, 'blob');
142
+ const blobId = (await res.text()).trim().toLowerCase();
143
+ if (!BLOB_ID_RE.test(blobId)) throw new TrainError('threema_gateway_error', 'upload_blob: the Threema Gateway answered without a blob id');
144
+ return blobId;
145
+ }
146
+
147
+ export async function downloadBlob(c: GatewayCreds, blobId: string): Promise<Uint8Array> {
148
+ if (!BLOB_ID_RE.test(blobId)) throw new TrainError('threema_bad_blob', `'${blobId}' is not a blob id`, { retryable: false });
149
+ const res = await call(`/blobs/${blobId}?${authQuery(c)}`, { signal: AbortSignal.timeout(BLOB_TIMEOUT_MS) }, 'blob download');
150
+ if (!res.ok) throw refusal(res.status, `blob ${blobId}`);
151
+ return new Uint8Array(await res.arrayBuffer());
152
+ }
@@ -0,0 +1,100 @@
1
+ import { createHmac, randomInt, timingSafeEqual } from 'node:crypto';
2
+ import nacl from 'tweetnacl';
3
+
4
+ const HEX_RE = /^(?:[0-9a-fA-F]{2})*$/;
5
+
6
+ export interface KeyPair {
7
+ publicKey: Uint8Array;
8
+ secretKey: Uint8Array;
9
+ }
10
+
11
+ export function hexToBytes(hex: string, label: string): Uint8Array {
12
+ if (!HEX_RE.test(hex)) throw new Error(`${label} is not hex`);
13
+ return new Uint8Array(Buffer.from(hex, 'hex'));
14
+ }
15
+
16
+ export const bytesToHex = (bytes: Uint8Array): string =>
17
+ Buffer.from(bytes).toString('hex');
18
+
19
+ export const keyPairFrom = (privateKeyHex: string): KeyPair =>
20
+ nacl.box.keyPair.fromSecretKey(hexToBytes(privateKeyHex, 'private key'));
21
+
22
+ function pad(plain: Uint8Array): Uint8Array {
23
+ const n = randomInt(1, 256);
24
+ const out = new Uint8Array(plain.length + n);
25
+ out.set(plain);
26
+ out.fill(n, plain.length);
27
+ return out;
28
+ }
29
+
30
+ function unpad(data: Uint8Array): Uint8Array | null {
31
+ const n = data[data.length - 1];
32
+ if (n === undefined || n < 1 || n > data.length) return null;
33
+ return data.subarray(0, data.length - n);
34
+ }
35
+
36
+ export function seal(
37
+ plain: Uint8Array,
38
+ theirPublicKey: Uint8Array,
39
+ mine: KeyPair,
40
+ ): { nonce: Uint8Array; box: Uint8Array } {
41
+ const nonce = nacl.randomBytes(nacl.box.nonceLength);
42
+ return {
43
+ nonce,
44
+ box: nacl.box(pad(plain), nonce, theirPublicKey, mine.secretKey),
45
+ };
46
+ }
47
+
48
+ export function open(
49
+ box: Uint8Array,
50
+ nonce: Uint8Array,
51
+ theirPublicKey: Uint8Array,
52
+ mine: KeyPair,
53
+ ): Uint8Array | null {
54
+ const padded = nacl.box.open(box, nonce, theirPublicKey, mine.secretKey);
55
+ return padded === null ? null : unpad(padded);
56
+ }
57
+
58
+ export interface CallbackFields {
59
+ from: string;
60
+ to: string;
61
+ messageId: string;
62
+ date: string;
63
+ nonce: string;
64
+ box: string;
65
+ }
66
+
67
+ export const callbackMac = (secret: string, f: CallbackFields): string =>
68
+ createHmac('sha256', secret)
69
+ .update(f.from + f.to + f.messageId + f.date + f.nonce + f.box)
70
+ .digest('hex');
71
+
72
+ export function macMatches(
73
+ secret: string,
74
+ fields: CallbackFields,
75
+ mac: string,
76
+ ): boolean {
77
+ const want = Buffer.from(callbackMac(secret, fields), 'hex');
78
+ const got = Buffer.from(mac.toLowerCase(), 'hex');
79
+ return want.length === got.length && timingSafeEqual(want, got);
80
+ }
81
+
82
+ const QUOTE_RE = /^> quote #([0-9a-f]{16})\n\n([\s\S]*)$/;
83
+
84
+ export const quoted = (replyTo: string, text: string): string =>
85
+ `> quote #${replyTo}\n\n${text}`;
86
+
87
+ export function unquote(text: string): { replyTo?: string; text: string } {
88
+ const m = QUOTE_RE.exec(text);
89
+ const replyTo = m?.[1];
90
+ const body = m?.[2];
91
+ return replyTo === undefined || body === undefined ? { text } : { replyTo, text: body };
92
+ }
93
+
94
+ const FILE_NONCE = new Uint8Array([...new Array<number>(23).fill(0), 1]);
95
+
96
+ export const newBlobKey = (): Uint8Array => nacl.randomBytes(nacl.secretbox.keyLength);
97
+
98
+ export const sealBlob = (data: Uint8Array, key: Uint8Array): Uint8Array => nacl.secretbox(data, FILE_NONCE, key);
99
+
100
+ export const openBlob = (data: Uint8Array, key: Uint8Array): Uint8Array | null => nacl.secretbox.open(data, FILE_NONCE, key);
@@ -0,0 +1,93 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { emit, mintId } from '@metro-labs/core/stations/station-runtime';
3
+ import { assertAttachmentSize, isImageMime, kindOf, saveBufferToCache } from '@metro-labs/core/stations/attachments';
4
+ import { TrainError } from '@metro-labs/core/train-error';
5
+ import type { Account } from './accounts.js';
6
+ import { downloadBlob, uploadBlob } from './api.js';
7
+ import { bytesToHex, hexToBytes, newBlobKey, openBlob, sealBlob } from './crypto.js';
8
+ import { emitInbound, textEnvelope, type InboundMeta, type Room } from './format.js';
9
+ import { encodeFile, encodeGroupFile, type FileData, type GroupRef } from './messages.js';
10
+
11
+ export const MAX_BLOB_BYTES = 50 * 1024 * 1024;
12
+ const SELF_URI = process.env.METRO_SELF_URI ?? '';
13
+
14
+ export interface OutgoingFile {
15
+ path: string;
16
+ mime: string;
17
+ name: string;
18
+ }
19
+
20
+ export async function packFile(acct: Account, file: OutgoingFile, caption: string | null): Promise<FileData> {
21
+ const data = await readFile(file.path);
22
+ assertAttachmentSize(data.length);
23
+ if (data.length > MAX_BLOB_BYTES) throw new TrainError('threema_file_too_big', `Threema carries files up to 50 MB; ${file.name} is bigger`, { retryable: false });
24
+ const key = newBlobKey();
25
+ const blobId = await uploadBlob(acct.cfg, sealBlob(new Uint8Array(data), key));
26
+ return { blobId, key: bytesToHex(key), mime: file.mime, name: file.name, size: data.length, caption, media: isImageMime(file.mime) };
27
+ }
28
+
29
+ export const encodeFileFor = (group: GroupRef | null, file: FileData): Uint8Array => (group === null ? encodeFile(file) : encodeGroupFile(group, file));
30
+
31
+ export const fileLabel = (file: OutgoingFile): string => kindOf(file.mime, file.path);
32
+
33
+ function savedEnvelope(accountId: string, line: string, forId: string, file: FileData, path: string, bytes: number): Record<string, unknown> {
34
+ return {
35
+ kind: 'inbound',
36
+ id: mintId(),
37
+ ts: new Date().toISOString(),
38
+ station: 'threema',
39
+ line,
40
+ from: SELF_URI,
41
+ text: `📎 saved: ${path}`,
42
+ payload: {
43
+ account: accountId,
44
+ contentType: 'attachmentSaved',
45
+ attachmentFor: forId,
46
+ index: 0,
47
+ kind: kindOf(file.mime, file.name),
48
+ attachmentPath: path,
49
+ localPath: path,
50
+ mime: file.mime,
51
+ name: file.name,
52
+ size: bytes,
53
+ },
54
+ };
55
+ }
56
+
57
+ function failedEnvelope(accountId: string, line: string, forId: string, file: FileData, reason: string): Record<string, unknown> {
58
+ return {
59
+ kind: 'inbound',
60
+ id: mintId(),
61
+ ts: new Date().toISOString(),
62
+ station: 'threema',
63
+ line,
64
+ from: SELF_URI,
65
+ text: `📎 not fetched: ${reason}`,
66
+ payload: { account: accountId, contentType: 'attachmentFailed', attachmentFor: forId, index: 0, kind: kindOf(file.mime, file.name), reason },
67
+ };
68
+ }
69
+
70
+ async function fetchFile(acct: Account, m: InboundMeta, file: FileData): Promise<{ path: string; bytes: number }> {
71
+ const sealed = await downloadBlob(acct.cfg, file.blobId);
72
+ const plain = openBlob(sealed, hexToBytes(file.key, 'blob key'));
73
+ if (plain === null) throw new TrainError('threema_bad_blob', 'the file did not decrypt with the key in the message', { retryable: false });
74
+ const saved = await saveBufferToCache(plain, m.messageId, 0, { mime: file.mime, name: file.name });
75
+ return { path: saved.path, bytes: saved.bytes };
76
+ }
77
+
78
+ export function deliverFile(acct: Account, m: InboundMeta, file: FileData, room: Room, sentByUs: (id: string) => boolean): void {
79
+ const env = textEnvelope(acct.cfg.id, m, file.caption ?? '', sentByUs, room);
80
+ const line = String(env.line);
81
+ const forId = String(env.id);
82
+ const payload = { ...(env.payload as Record<string, unknown>), attachments: [{ kind: kindOf(file.mime, file.name), name: file.name, mime: file.mime, ...(file.size === null ? {} : { size: file.size }) }] };
83
+ emitInbound(acct.cfg.id, acct.cfg.owner, { ...env, payload });
84
+ fetchFile(acct, m, file)
85
+ .then(({ path, bytes }) => {
86
+ emit(savedEnvelope(acct.cfg.id, line, forId, file, path, bytes));
87
+ })
88
+ .catch((err: unknown) => {
89
+ const reason = err instanceof Error ? err.message : String(err);
90
+ process.stderr.write(`threema[${acct.cfg.id}]: could not fetch ${file.name} from ${m.from}: ${reason}\n`);
91
+ emit(failedEnvelope(acct.cfg.id, line, forId, file, reason));
92
+ });
93
+ }