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

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 (225) hide show
  1. package/dist/api.js +103 -0
  2. package/dist/cli.js +134 -565
  3. package/dist/control.js +102 -0
  4. package/dist/prompt.js +10 -0
  5. package/dist/runtime.js +94 -0
  6. package/dist/store.js +70 -0
  7. package/dist/update.js +33 -0
  8. package/dist/version.js +97 -0
  9. package/package.json +34 -22
  10. package/runtime/node_modules/@metro-labs/discord/package.json +28 -0
  11. package/runtime/node_modules/@metro-labs/discord/src/accounts.ts +119 -0
  12. package/runtime/node_modules/@metro-labs/discord/src/actions.ts +388 -0
  13. package/runtime/node_modules/@metro-labs/discord/src/api-base.ts +1 -0
  14. package/runtime/node_modules/@metro-labs/discord/src/attachments.ts +29 -0
  15. package/runtime/node_modules/@metro-labs/discord/src/format.ts +210 -0
  16. package/runtime/node_modules/@metro-labs/discord/src/group-actions.ts +50 -0
  17. package/runtime/node_modules/@metro-labs/discord/src/group.ts +115 -0
  18. package/runtime/node_modules/@metro-labs/discord/src/index.ts +127 -0
  19. package/runtime/node_modules/@metro-labs/discord/src/members.ts +215 -0
  20. package/runtime/node_modules/@metro-labs/discord/src/send-files.ts +43 -0
  21. package/runtime/node_modules/@metro-labs/discord/src/station.ts +19 -0
  22. package/runtime/node_modules/@metro-labs/discord/src/verify.ts +82 -0
  23. package/runtime/node_modules/@metro-labs/discord/src/voice-speak.ts +195 -0
  24. package/runtime/node_modules/@metro-labs/discord/src/voice-transcribe.ts +292 -0
  25. package/runtime/node_modules/@metro-labs/discord/src/voice.ts +197 -0
  26. package/runtime/node_modules/@metro-labs/discord/src/wire.ts +5 -0
  27. package/runtime/node_modules/@metro-labs/mcp/package.json +66 -0
  28. package/runtime/node_modules/@metro-labs/mcp/src/channels/dedupe.ts +24 -0
  29. package/runtime/node_modules/@metro-labs/mcp/src/channels/inbound.ts +367 -0
  30. package/runtime/node_modules/@metro-labs/mcp/src/channels/media-note.ts +85 -0
  31. package/runtime/node_modules/@metro-labs/mcp/src/channels/pending.ts +75 -0
  32. package/runtime/node_modules/@metro-labs/mcp/src/channels/relay.ts +129 -0
  33. package/runtime/node_modules/@metro-labs/mcp/src/channels/webhook-note.ts +51 -0
  34. package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +311 -0
  35. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +335 -0
  36. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-error.ts +8 -0
  37. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +129 -0
  38. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-grant.ts +99 -0
  39. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-owner.ts +32 -0
  40. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +124 -0
  41. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +278 -0
  42. package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +322 -0
  43. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +111 -0
  44. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair.ts +20 -0
  45. package/runtime/node_modules/@metro-labs/mcp/src/daemon/collection-api.ts +202 -0
  46. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +338 -0
  47. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-json.ts +31 -0
  48. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth-routes.ts +157 -0
  49. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +127 -0
  50. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-tools.ts +100 -0
  51. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-url.ts +57 -0
  52. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +333 -0
  53. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cors.ts +29 -0
  54. package/runtime/node_modules/@metro-labs/mcp/src/daemon/crash-guard.ts +49 -0
  55. package/runtime/node_modules/@metro-labs/mcp/src/daemon/events.ts +117 -0
  56. package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-auth.ts +164 -0
  57. package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-oauth.ts +208 -0
  58. package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +357 -0
  59. package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +79 -0
  60. package/runtime/node_modules/@metro-labs/mcp/src/daemon/log.ts +42 -0
  61. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-client.ts +162 -0
  62. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-discovery.ts +120 -0
  63. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-pending.ts +65 -0
  64. package/runtime/node_modules/@metro-labs/mcp/src/daemon/pair-codes.ts +58 -0
  65. package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +134 -0
  66. package/runtime/node_modules/@metro-labs/mcp/src/daemon/project-api.ts +214 -0
  67. package/runtime/node_modules/@metro-labs/mcp/src/daemon/protocol.ts +224 -0
  68. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +125 -0
  69. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-pair.ts +18 -0
  70. package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-source.ts +105 -0
  71. package/runtime/node_modules/@metro-labs/mcp/src/daemon/secure-fs.ts +57 -0
  72. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-api.ts +27 -0
  73. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +41 -0
  74. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +218 -0
  75. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor-io.ts +97 -0
  76. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor.ts +323 -0
  77. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-call.ts +22 -0
  78. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-error.ts +53 -0
  79. package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +169 -0
  80. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +295 -0
  81. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-store.ts +181 -0
  82. package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +91 -0
  83. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +215 -0
  84. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-map.ts +71 -0
  85. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-scope.ts +64 -0
  86. package/runtime/node_modules/@metro-labs/mcp/src/db/client.ts +37 -0
  87. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-collections.ts +218 -0
  88. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-config.ts +147 -0
  89. package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +329 -0
  90. package/runtime/node_modules/@metro-labs/mcp/src/db/ids.ts +19 -0
  91. package/runtime/node_modules/@metro-labs/mcp/src/db/key-map.ts +45 -0
  92. package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +364 -0
  93. package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +300 -0
  94. package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +31 -0
  95. package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +105 -0
  96. package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +131 -0
  97. package/runtime/node_modules/@metro-labs/mcp/src/db/token-carry.ts +45 -0
  98. package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +57 -0
  99. package/runtime/node_modules/@metro-labs/mcp/src/db/whatsapp-login.ts +18 -0
  100. package/runtime/node_modules/@metro-labs/mcp/src/mcp/accounts.ts +145 -0
  101. package/runtime/node_modules/@metro-labs/mcp/src/mcp/call-tools.ts +203 -0
  102. package/runtime/node_modules/@metro-labs/mcp/src/mcp/channel-owner.ts +29 -0
  103. package/runtime/node_modules/@metro-labs/mcp/src/mcp/ctx.ts +62 -0
  104. package/runtime/node_modules/@metro-labs/mcp/src/mcp/event-store.ts +91 -0
  105. package/runtime/node_modules/@metro-labs/mcp/src/mcp/frame-scope.ts +21 -0
  106. package/runtime/node_modules/@metro-labs/mcp/src/mcp/group-tools.ts +129 -0
  107. package/runtime/node_modules/@metro-labs/mcp/src/mcp/index.ts +192 -0
  108. package/runtime/node_modules/@metro-labs/mcp/src/mcp/member-tools.ts +72 -0
  109. package/runtime/node_modules/@metro-labs/mcp/src/mcp/permission-relay.ts +66 -0
  110. package/runtime/node_modules/@metro-labs/mcp/src/mcp/raw-get-stream.ts +207 -0
  111. package/runtime/node_modules/@metro-labs/mcp/src/mcp/request-identity.ts +73 -0
  112. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-registry.ts +172 -0
  113. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-route.ts +43 -0
  114. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session.ts +211 -0
  115. package/runtime/node_modules/@metro-labs/mcp/src/mcp/str.ts +5 -0
  116. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-dispatch.ts +212 -0
  117. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +397 -0
  118. package/runtime/node_modules/@metro-labs/mcp/src/mcp/upload-tool.ts +66 -0
  119. package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +240 -0
  120. package/runtime/node_modules/@metro-labs/mcp/src/server.ts +2 -0
  121. package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +80 -0
  122. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-inline.ts +100 -0
  123. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-interactive.ts +161 -0
  124. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-resolve.ts +200 -0
  125. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-xmtp.ts +111 -0
  126. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +182 -0
  127. package/runtime/node_modules/@metro-labs/mcp/src/stations/attachments.ts +196 -0
  128. package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +82 -0
  129. package/runtime/node_modules/@metro-labs/mcp/src/stations/messaging-normalize.ts +88 -0
  130. package/runtime/node_modules/@metro-labs/mcp/src/stations/registry.ts +35 -0
  131. package/runtime/node_modules/@metro-labs/mcp/src/stations/station-runtime.ts +64 -0
  132. package/runtime/node_modules/@metro-labs/mcp/src/stations/types.ts +107 -0
  133. package/runtime/node_modules/@metro-labs/telegram/package.json +25 -0
  134. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +120 -0
  135. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +295 -0
  136. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +128 -0
  137. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +172 -0
  138. package/runtime/node_modules/@metro-labs/telegram/src/index.ts +103 -0
  139. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +166 -0
  140. package/runtime/node_modules/@metro-labs/telegram/src/members.ts +47 -0
  141. package/runtime/node_modules/@metro-labs/telegram/src/station.ts +17 -0
  142. package/runtime/node_modules/@metro-labs/telegram/src/types.ts +48 -0
  143. package/runtime/node_modules/@metro-labs/telegram/src/verify.ts +38 -0
  144. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +3 -0
  145. package/runtime/node_modules/@metro-labs/telegram-user/package.json +28 -0
  146. package/runtime/node_modules/@metro-labs/telegram-user/src/accounts.ts +81 -0
  147. package/runtime/node_modules/@metro-labs/telegram-user/src/actions.ts +254 -0
  148. package/runtime/node_modules/@metro-labs/telegram-user/src/attachments.ts +87 -0
  149. package/runtime/node_modules/@metro-labs/telegram-user/src/client.ts +77 -0
  150. package/runtime/node_modules/@metro-labs/telegram-user/src/format.ts +141 -0
  151. package/runtime/node_modules/@metro-labs/telegram-user/src/group.ts +261 -0
  152. package/runtime/node_modules/@metro-labs/telegram-user/src/history.ts +52 -0
  153. package/runtime/node_modules/@metro-labs/telegram-user/src/inbound.ts +78 -0
  154. package/runtime/node_modules/@metro-labs/telegram-user/src/index.ts +46 -0
  155. package/runtime/node_modules/@metro-labs/telegram-user/src/login.ts +166 -0
  156. package/runtime/node_modules/@metro-labs/telegram-user/src/media-actions.ts +80 -0
  157. package/runtime/node_modules/@metro-labs/telegram-user/src/members.ts +75 -0
  158. package/runtime/node_modules/@metro-labs/telegram-user/src/normalize.ts +22 -0
  159. package/runtime/node_modules/@metro-labs/telegram-user/src/reactions.ts +167 -0
  160. package/runtime/node_modules/@metro-labs/telegram-user/src/station.ts +24 -0
  161. package/runtime/node_modules/@metro-labs/telegram-user/src/types.ts +7 -0
  162. package/runtime/node_modules/@metro-labs/telegram-user/src/wire.ts +1 -0
  163. package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
  164. package/runtime/node_modules/@metro-labs/webhook/src/station.ts +40 -0
  165. package/runtime/node_modules/@metro-labs/whatsapp/package.json +29 -0
  166. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +67 -0
  167. package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
  168. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +178 -0
  169. package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +72 -0
  170. package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +129 -0
  171. package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +318 -0
  172. package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
  173. package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +132 -0
  174. package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +57 -0
  175. package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +40 -0
  176. package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +59 -0
  177. package/runtime/node_modules/@metro-labs/whatsapp/src/logger.ts +60 -0
  178. package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +176 -0
  179. package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +84 -0
  180. package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +22 -0
  181. package/runtime/node_modules/@metro-labs/whatsapp/src/outbox.ts +38 -0
  182. package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +141 -0
  183. package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +17 -0
  184. package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +98 -0
  185. package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +6 -0
  186. package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +5 -0
  187. package/runtime/node_modules/@metro-labs/xmtp/package.json +33 -0
  188. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +113 -0
  189. package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +110 -0
  190. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +299 -0
  191. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +157 -0
  192. package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +87 -0
  193. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +375 -0
  194. package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +107 -0
  195. package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +171 -0
  196. package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +239 -0
  197. package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +55 -0
  198. package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +333 -0
  199. package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +78 -0
  200. package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +34 -0
  201. package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +56 -0
  202. package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +33 -0
  203. package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +125 -0
  204. package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +157 -0
  205. package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
  206. package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +349 -0
  207. package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +13 -0
  208. package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +126 -0
  209. package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +212 -0
  210. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +90 -0
  211. package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +84 -0
  212. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +39 -0
  213. package/runtime/runtime.json +3 -0
  214. package/runtime/trains/.keep +0 -0
  215. package/LICENSE +0 -21
  216. package/README.md +0 -78
  217. package/dist/channels/discord.js +0 -148
  218. package/dist/channels/telegram.js +0 -137
  219. package/dist/lib/address.js +0 -21
  220. package/dist/lib/codex-rc.js +0 -274
  221. package/dist/lib/dotenv.js +0 -31
  222. package/dist/log.js +0 -12
  223. package/dist/paths.js +0 -45
  224. package/dist/tail.js +0 -161
  225. package/skills/metro/SKILL.md +0 -122
@@ -0,0 +1,200 @@
1
+ import {
2
+ assertAttachmentSize,
3
+ assertContentLength,
4
+ guessMime,
5
+ saveBufferToCache,
6
+ } from './attachments.js';
7
+ import {
8
+ assertInlineTotal,
9
+ decodeInline,
10
+ removeInlineTemp,
11
+ splitInlineData,
12
+ writeInlineTemp,
13
+ } from './attach-inline.js';
14
+ import { realpathSync } from 'node:fs';
15
+ import { readUpload, UPLOAD_TTL_MS } from '../daemon/upload-store.js';
16
+ import type { CanonicalAttachment } from './types.js';
17
+
18
+ export interface ResolvedAttachment {
19
+ path: string;
20
+ mime: string;
21
+ name: string;
22
+ bytes: number;
23
+ temp?: string;
24
+ }
25
+
26
+ export interface ResolveOptions {
27
+ allowed?: Set<string>;
28
+ }
29
+
30
+ interface InlineBudget {
31
+ used: number;
32
+ }
33
+
34
+ const FETCH_TIMEOUT_MS = 60_000;
35
+
36
+ const basenameOf = (src: string): string =>
37
+ src.split('?')[0]?.split('#')[0]?.split('/').filter(Boolean).pop() ?? '';
38
+
39
+ const isHttpUrl = (src: string): boolean => /^https?:\/\//i.test(src);
40
+
41
+ const mintId = (): string => Math.random().toString(36).slice(2, 12);
42
+
43
+ const HIDDEN_SEGMENT = /(^|\/)\.[^/]/;
44
+
45
+ function assertReadablePath(path: string): void {
46
+ let real: string;
47
+ try {
48
+ real = realpathSync(path);
49
+ } catch {
50
+ return;
51
+ }
52
+ if (HIDDEN_SEGMENT.test(real))
53
+ throw new Error(
54
+ `attachment path '${path}' resolves into a hidden directory ` +
55
+ `('${real}') and will not be read. \`path\` is read on the daemon ` +
56
+ 'machine; put the file somewhere non-hidden or pass `upload` instead.',
57
+ );
58
+ }
59
+
60
+ async function fromPath(
61
+ a: CanonicalAttachment,
62
+ path: string,
63
+ ): Promise<ResolvedAttachment> {
64
+ assertReadablePath(path);
65
+ const file = Bun.file(path);
66
+ if (!(await file.exists()))
67
+ throw new Error(
68
+ `attachment path '${path}' does not exist on the metro host. ` +
69
+ '`path` is read on the daemon machine, not on the caller\'s machine; ' +
70
+ 'pass `url` instead if the file is not already on the daemon.',
71
+ );
72
+ assertAttachmentSize(file.size);
73
+ return {
74
+ path,
75
+ mime: a.mime ?? guessMime(path),
76
+ name: a.name ?? basenameOf(path) ?? 'attachment',
77
+ bytes: file.size,
78
+ };
79
+ }
80
+
81
+ async function fromUrl(
82
+ a: CanonicalAttachment,
83
+ url: string,
84
+ ): Promise<ResolvedAttachment> {
85
+ if (!isHttpUrl(url))
86
+ throw new Error(
87
+ `attachment url '${url}' is not an http(s) url; use \`path\` for a local file`,
88
+ );
89
+ const res = await fetch(url, {
90
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
91
+ });
92
+ if (!res.ok)
93
+ throw new Error(`attachment fetch failed ${res.status} for '${url}'`);
94
+ assertContentLength(res.headers.get('content-length'));
95
+ const data = new Uint8Array(await res.arrayBuffer());
96
+ const mime =
97
+ a.mime ?? res.headers.get('content-type')?.split(';')[0] ?? guessMime(url);
98
+ const name = a.name ?? basenameOf(url) ?? 'attachment';
99
+ const saved = await saveBufferToCache(data, `out${mintId()}`, 0, {
100
+ mime,
101
+ name,
102
+ });
103
+ return { path: saved.path, mime, name, bytes: data.length };
104
+ }
105
+
106
+ async function fromData(
107
+ a: CanonicalAttachment,
108
+ data: string,
109
+ budget: InlineBudget,
110
+ ): Promise<ResolvedAttachment> {
111
+ const name = a.name ?? 'attachment';
112
+ const source = splitInlineData(data);
113
+ const bytes = decodeInline(source.data, `'${name}'`);
114
+ budget.used += bytes.length;
115
+ assertInlineTotal(budget.used);
116
+ const { dir, path } = await writeInlineTemp(bytes, a.name);
117
+ return {
118
+ path,
119
+ mime: a.mime ?? source.mime ?? guessMime(name),
120
+ name,
121
+ bytes: bytes.length,
122
+ temp: dir,
123
+ };
124
+ }
125
+
126
+ const uploadMissing = (id: string): Error =>
127
+ new Error(
128
+ `upload '${id}' is not a live upload of yours. Either it was never created, or its ` +
129
+ 'bytes were never pushed to `POST /api/uploads`, or it belongs to another agent, or ' +
130
+ `it expired (an upload lives ${Math.round(UPLOAD_TTL_MS / 60_000)} minutes). ` +
131
+ 'Create a new one with `create_upload` and push the file again.',
132
+ );
133
+
134
+ function fromUpload(
135
+ a: CanonicalAttachment,
136
+ id: string,
137
+ allowed: Set<string> | undefined,
138
+ ): ResolvedAttachment {
139
+ const rec = allowed === undefined ? undefined : readUpload(id);
140
+ if (rec === undefined || !(allowed?.has(rec.agentId) ?? false))
141
+ throw uploadMissing(id);
142
+ const name = a.name ?? rec.name;
143
+ return { path: rec.path, mime: a.mime ?? rec.mime, name, bytes: rec.bytes };
144
+ }
145
+
146
+ export const SOURCE_KEYS = ['path', 'url', 'data', 'upload'] as const;
147
+
148
+ const sourcesOf = (a: CanonicalAttachment): string[] =>
149
+ SOURCE_KEYS.filter((k) => {
150
+ const v = a[k];
151
+ return typeof v === 'string' && v.length > 0;
152
+ });
153
+
154
+ export async function resolveAttachment(
155
+ a: CanonicalAttachment,
156
+ budget: InlineBudget = { used: 0 },
157
+ opts: ResolveOptions = {},
158
+ ): Promise<ResolvedAttachment> {
159
+ const sources = sourcesOf(a);
160
+ if (sources.length === 0)
161
+ throw new Error(
162
+ 'attachment requires exactly one of `upload`, `data`, `url` or `path`',
163
+ );
164
+ if (sources.length > 1)
165
+ throw new Error(
166
+ `attachment names ${sources.length} sources (${sources
167
+ .map((s) => `\`${s}\``)
168
+ .join(', ')}); pass exactly one of \`upload\`, \`data\`, \`url\` or \`path\``,
169
+ );
170
+ if (a.upload) return fromUpload(a, a.upload, opts.allowed);
171
+ if (a.data) return fromData(a, a.data, budget);
172
+ if (a.path) return fromPath(a, a.path);
173
+ return fromUrl(a, a.url ?? '');
174
+ }
175
+
176
+ export async function cleanupAttachments(
177
+ atts: ResolvedAttachment[],
178
+ ): Promise<void> {
179
+ for (const a of atts) if (a.temp) await removeInlineTemp(a.temp);
180
+ }
181
+
182
+ export async function resolveAttachments(
183
+ atts: CanonicalAttachment[],
184
+ opts: ResolveOptions = {},
185
+ ): Promise<ResolvedAttachment[]> {
186
+ const out: ResolvedAttachment[] = [];
187
+ const budget: InlineBudget = { used: 0 };
188
+ for (let i = 0; i < atts.length; i++) {
189
+ const a = atts[i];
190
+ if (!a) continue;
191
+ try {
192
+ out.push(await resolveAttachment(a, budget, opts));
193
+ } catch (e) {
194
+ await cleanupAttachments(out);
195
+ const why = e instanceof Error ? e.message : String(e);
196
+ throw new Error(`attachment ${i + 1} of ${atts.length}: ${why}`);
197
+ }
198
+ }
199
+ return out;
200
+ }
@@ -0,0 +1,111 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { rmSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { errMsg, log } from '../daemon/log.js';
5
+
6
+ export class XmtpAttachError extends Error {}
7
+
8
+ export interface XmtpIdentity {
9
+ inboxId: string;
10
+ address: string;
11
+ installationId: string;
12
+ dbPath: string;
13
+ }
14
+
15
+ export type VerifyXmtpKey = (
16
+ privateKey: string,
17
+ dbPath: string,
18
+ ) => Promise<XmtpIdentity>;
19
+
20
+ const VERIFY_TIMEOUT_MS = 90_000;
21
+ const UNREACHABLE =
22
+ 'Metro could not open an XMTP inbox with the key it generated, so nothing was attached';
23
+
24
+ export function newXmtpDbPath(): string {
25
+ return `~/.metro/xmtp-production-${randomBytes(8).toString('hex')}.db3`;
26
+ }
27
+
28
+ export function discardXmtpDb(dbPath: string): void {
29
+ for (const suffix of ['', '-wal', '-shm'])
30
+ rmSync(`${dbPath}${suffix}`, { force: true });
31
+ }
32
+
33
+ export function withoutKey(message: string, privateKey: string): string {
34
+ return message.split(privateKey).join('[redacted]');
35
+ }
36
+
37
+ interface VerifyReply {
38
+ ok?: unknown;
39
+ inboxId?: unknown;
40
+ address?: unknown;
41
+ installationId?: unknown;
42
+ dbPath?: unknown;
43
+ error?: unknown;
44
+ }
45
+
46
+ const str = (value: unknown): string => (typeof value === 'string' ? value : '');
47
+
48
+ function readReply(raw: string): VerifyReply {
49
+ const line = raw.trim().split('\n').at(-1) ?? '';
50
+ try {
51
+ const parsed: unknown = JSON.parse(line);
52
+ return typeof parsed === 'object' && parsed !== null ? parsed : {};
53
+ } catch {
54
+ return {};
55
+ }
56
+ }
57
+
58
+ async function runVerifier(payload: string): Promise<string> {
59
+ const script = fileURLToPath(import.meta.resolve('@metro-labs/xmtp/verify'));
60
+ const proc = Bun.spawn(['bun', 'run', script], {
61
+ stdin: 'pipe',
62
+ stdout: 'pipe',
63
+ stderr: 'pipe',
64
+ });
65
+ await proc.stdin.write(payload);
66
+ await proc.stdin.end();
67
+ const timer = setTimeout(() => {
68
+ proc.kill();
69
+ }, VERIFY_TIMEOUT_MS);
70
+ try {
71
+ const [out, err, code] = await Promise.all([
72
+ new Response(proc.stdout).text(),
73
+ new Response(proc.stderr).text(),
74
+ proc.exited,
75
+ ]);
76
+ if (code !== 0)
77
+ log.warn(
78
+ { code, stderr: err.trim().slice(-500) },
79
+ 'attach-xmtp: the inbox check exited badly',
80
+ );
81
+ return out;
82
+ } finally {
83
+ clearTimeout(timer);
84
+ }
85
+ }
86
+
87
+ export async function verifyXmtpKeyOutOfProcess(
88
+ privateKey: string,
89
+ dbPath: string,
90
+ ): Promise<XmtpIdentity> {
91
+ const raw = await runVerifier(
92
+ JSON.stringify({ privateKey, dbPath }),
93
+ ).catch((err: unknown) => {
94
+ log.warn({ err: errMsg(err) }, 'attach-xmtp: could not run the inbox check');
95
+ throw new XmtpAttachError(UNREACHABLE);
96
+ });
97
+ const reply = readReply(raw);
98
+ if (reply.ok !== true)
99
+ throw new XmtpAttachError(
100
+ withoutKey(str(reply.error) || UNREACHABLE, privateKey),
101
+ );
102
+ const identity = {
103
+ inboxId: str(reply.inboxId),
104
+ address: str(reply.address),
105
+ installationId: str(reply.installationId),
106
+ dbPath: str(reply.dbPath),
107
+ };
108
+ if (identity.inboxId === '' || identity.address === '')
109
+ throw new XmtpAttachError(UNREACHABLE);
110
+ return identity;
111
+ }
@@ -0,0 +1,182 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import {
3
+ DiscordTokenError,
4
+ verifyDiscordBotToken,
5
+ } from '@metro-labs/discord/verify';
6
+ import {
7
+ TelegramTokenError,
8
+ verifyTelegramBotToken,
9
+ } from '@metro-labs/telegram/verify';
10
+ import { ApiError } from '../daemon/api-error.js';
11
+ import { publicBaseOrDefault } from '../daemon/attach-serve.js';
12
+ import {
13
+ discardXmtpDb,
14
+ newXmtpDbPath,
15
+ verifyXmtpKeyOutOfProcess,
16
+ XmtpAttachError,
17
+ type VerifyXmtpKey,
18
+ } from './attach-xmtp.js';
19
+
20
+ export const ATTACHABLE_STATIONS = [
21
+ 'discord',
22
+ 'telegram',
23
+ 'xmtp',
24
+ 'webhook',
25
+ ] as const;
26
+
27
+ export type AttachStation = (typeof ATTACHABLE_STATIONS)[number];
28
+
29
+ export class StationAttachError extends ApiError {}
30
+
31
+ export interface AttachInput {
32
+ station: AttachStation;
33
+ token?: unknown;
34
+ }
35
+
36
+ export interface OneTimeSecret {
37
+ label: string;
38
+ value: string;
39
+ note: string;
40
+ }
41
+
42
+ export interface PreparedAccount {
43
+ config: Record<string, unknown>;
44
+ identity: Record<string, string>;
45
+ secret?: OneTimeSecret;
46
+ discard?: () => void;
47
+ }
48
+
49
+ const TOKEN_RE = /^[A-Za-z0-9._:-]{8,256}$/;
50
+
51
+ const SECP256K1_ORDER = BigInt(
52
+ '0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141',
53
+ );
54
+
55
+ export function isAttachStation(raw: unknown): raw is AttachStation {
56
+ return (
57
+ typeof raw === 'string' &&
58
+ (ATTACHABLE_STATIONS as readonly string[]).includes(raw)
59
+ );
60
+ }
61
+
62
+ function requireToken(raw: unknown, label: string): string {
63
+ const token = typeof raw === 'string' ? raw.trim() : '';
64
+ if (token === '')
65
+ throw new StationAttachError(`a ${label} bot token is required`, 400);
66
+ if (!TOKEN_RE.test(token))
67
+ throw new StationAttachError(
68
+ `that does not look like a ${label} bot token`,
69
+ 400,
70
+ );
71
+ return token;
72
+ }
73
+
74
+ function rejected(err: unknown, own: boolean, fallback: string): never {
75
+ throw new StationAttachError(
76
+ own && err instanceof Error ? err.message : fallback,
77
+ 400,
78
+ );
79
+ }
80
+
81
+ async function prepareDiscord(raw: unknown): Promise<PreparedAccount> {
82
+ const token = requireToken(raw, 'Discord');
83
+ try {
84
+ const bot = await verifyDiscordBotToken(token);
85
+ if (!bot.messageContent)
86
+ throw new StationAttachError(
87
+ 'this Discord application does not have the Message Content intent enabled. Turn it on under Bot / Privileged Gateway Intents in the developer portal, then attach it again',
88
+ 400,
89
+ );
90
+ return {
91
+ config: { token },
92
+ identity: { userId: bot.userId, username: bot.username },
93
+ };
94
+ } catch (err) {
95
+ if (err instanceof StationAttachError) throw err;
96
+ return rejected(
97
+ err,
98
+ err instanceof DiscordTokenError,
99
+ 'Discord rejected that bot token',
100
+ );
101
+ }
102
+ }
103
+
104
+ async function prepareTelegram(raw: unknown): Promise<PreparedAccount> {
105
+ const token = requireToken(raw, 'Telegram');
106
+ try {
107
+ const bot = await verifyTelegramBotToken(token);
108
+ return {
109
+ config: { token },
110
+ identity: { botId: String(bot.botId), username: bot.username },
111
+ };
112
+ } catch (err) {
113
+ return rejected(
114
+ err,
115
+ err instanceof TelegramTokenError,
116
+ 'Telegram rejected that bot token',
117
+ );
118
+ }
119
+ }
120
+
121
+ export function newXmtpPrivateKey(): string {
122
+ for (;;) {
123
+ const hex = randomBytes(32).toString('hex');
124
+ const value = BigInt(`0x${hex}`);
125
+ if (value > 0n && value < SECP256K1_ORDER) return `0x${hex}`;
126
+ }
127
+ }
128
+
129
+ async function prepareXmtp(verify: VerifyXmtpKey): Promise<PreparedAccount> {
130
+ const privateKey = newXmtpPrivateKey();
131
+ const dbPath = newXmtpDbPath();
132
+ const identity = await verify(privateKey, dbPath).catch((err: unknown) =>
133
+ rejected(
134
+ err,
135
+ err instanceof XmtpAttachError,
136
+ 'Metro could not open an XMTP inbox with the key it generated, so nothing was attached',
137
+ ),
138
+ );
139
+ return {
140
+ config: { privateKey, dbPath },
141
+ identity: { inboxId: identity.inboxId, address: identity.address },
142
+ secret: {
143
+ label: 'xmtp private key',
144
+ value: privateKey,
145
+ note: 'Metro generated this key, opened inbox ' +
146
+ `${identity.inboxId} with it, and stores it with the account. ` +
147
+ 'It is shown once, here, and is never returned by any API again.',
148
+ },
149
+ discard: () => {
150
+ discardXmtpDb(identity.dbPath);
151
+ },
152
+ };
153
+ }
154
+
155
+ export const hookUrl = (webhookId: string, secret: string): string =>
156
+ `${publicBaseOrDefault().replace(/\/+$/, '')}/api/webhooks/${webhookId}/${secret}`;
157
+
158
+ const WEBHOOK_ID_FLOOR = 10n ** 18n;
159
+
160
+ export function newWebhookId(): string {
161
+ const raw = BigInt(`0x${randomBytes(8).toString('hex')}`);
162
+ return String(WEBHOOK_ID_FLOOR + (raw % (9n * WEBHOOK_ID_FLOOR)));
163
+ }
164
+
165
+ function prepareWebhook(): PreparedAccount {
166
+ const secret = randomBytes(48).toString('base64url');
167
+ const webhookId = newWebhookId();
168
+ return {
169
+ config: { secret, webhookId, createdAt: new Date().toISOString() },
170
+ identity: { endpoint: hookUrl(webhookId, secret) },
171
+ };
172
+ }
173
+
174
+ export async function prepareAccount(
175
+ input: AttachInput,
176
+ verify: VerifyXmtpKey = verifyXmtpKeyOutOfProcess,
177
+ ): Promise<PreparedAccount> {
178
+ if (input.station === 'discord') return prepareDiscord(input.token);
179
+ if (input.station === 'telegram') return prepareTelegram(input.token);
180
+ if (input.station === 'webhook') return prepareWebhook();
181
+ return prepareXmtp(verify);
182
+ }
@@ -0,0 +1,196 @@
1
+ export const attachDir = (): string =>
2
+ process.env.METRO_XMTP_ATTACH_DIR ??
3
+ `${process.env.HOME}/.cache/metro/messenger-uploads`;
4
+
5
+ const CACHE_NAME_RE = /^msg_[A-Za-z0-9]{1,16}_\d+\.[A-Za-z0-9]{1,5}$/;
6
+
7
+ export const resolveCachedAttachment = (name: string): string | null =>
8
+ CACHE_NAME_RE.test(name) ? `${attachDir()}/${name}` : null;
9
+
10
+ const MIME_EXT: Record<string, string> = {
11
+ 'image/jpeg': 'jpg',
12
+ 'image/jpg': 'jpg',
13
+ 'image/png': 'png',
14
+ 'image/gif': 'gif',
15
+ 'image/webp': 'webp',
16
+ 'image/heic': 'heic',
17
+ 'audio/mp4': 'm4a',
18
+ 'audio/mpeg': 'mp3',
19
+ 'audio/ogg': 'ogg',
20
+ 'audio/wav': 'wav',
21
+ 'video/mp4': 'mp4',
22
+ 'video/quicktime': 'mov',
23
+ 'video/webm': 'webm',
24
+ 'application/pdf': 'pdf',
25
+ };
26
+
27
+ const extFromName = (filename: string | undefined): string | undefined => {
28
+ const ext = filename
29
+ ?.split('?')[0]
30
+ ?.split('#')[0]
31
+ ?.split('.')
32
+ .pop()
33
+ ?.toLowerCase();
34
+ return ext !== undefined && ext.length >= 1 && ext.length <= 5
35
+ ? ext
36
+ : undefined;
37
+ };
38
+
39
+ export const extFromMime = (mime: string | undefined): string | undefined => {
40
+ if (mime === undefined) return undefined;
41
+ const mapped = MIME_EXT[mime];
42
+ if (mapped) return mapped;
43
+ if (mime.startsWith('image/')) return mime.slice(6).replace('jpeg', 'jpg');
44
+ return undefined;
45
+ };
46
+
47
+ const extFor = (
48
+ filename: string | undefined,
49
+ mime: string | undefined,
50
+ ): string => extFromName(filename) ?? extFromMime(mime) ?? 'bin';
51
+
52
+ const cacheFileName = (
53
+ messageId: string,
54
+ index: number,
55
+ filename: string | undefined,
56
+ mime: string | undefined,
57
+ ): string => {
58
+ const safeId =
59
+ messageId.replace(/[^a-zA-Z0-9]/g, '').slice(0, 16) || 'unknown';
60
+ return `msg_${safeId}_${index}.${extFor(filename, mime)}`;
61
+ };
62
+
63
+ export interface SavedAttachment {
64
+ path: string;
65
+ mime?: string;
66
+ name?: string;
67
+ bytes: number;
68
+ }
69
+
70
+ export const MAX_ATTACHMENT_BYTES = Number(
71
+ process.env.METRO_ATTACH_MAX_BYTES ?? 100 * 1024 * 1024,
72
+ );
73
+
74
+ export function assertAttachmentSize(bytes: number): void {
75
+ if (bytes > MAX_ATTACHMENT_BYTES)
76
+ throw new Error(
77
+ `attachment size ${bytes} exceeds limit of ${MAX_ATTACHMENT_BYTES} bytes`,
78
+ );
79
+ }
80
+
81
+ export function assertContentLength(header: string | null | undefined): void {
82
+ if (header == null) return;
83
+ const n = Number(header);
84
+ if (Number.isFinite(n)) assertAttachmentSize(n);
85
+ }
86
+
87
+ const cachePathFor = async (
88
+ messageId: string,
89
+ index: number,
90
+ meta: { mime?: string; name?: string },
91
+ ): Promise<string> => {
92
+ const { mkdir } = await import('node:fs/promises');
93
+ const nodePath = await import('node:path');
94
+ const dir = attachDir();
95
+ await mkdir(dir, { recursive: true });
96
+ return nodePath.join(
97
+ dir,
98
+ cacheFileName(messageId, index, meta.name, meta.mime),
99
+ );
100
+ };
101
+
102
+ export const saveBufferToCache = async (
103
+ data: Uint8Array,
104
+ messageId: string,
105
+ index: number,
106
+ meta: { mime?: string; name?: string },
107
+ ): Promise<SavedAttachment> => {
108
+ assertAttachmentSize(data.length);
109
+ const { writeFile } = await import('node:fs/promises');
110
+ const path = await cachePathFor(messageId, index, meta);
111
+ await writeFile(path, data);
112
+ return { path, mime: meta.mime, name: meta.name, bytes: data.length };
113
+ };
114
+
115
+ export const saveStreamToCache = async (
116
+ chunks: AsyncIterable<Uint8Array>,
117
+ messageId: string,
118
+ index: number,
119
+ meta: { mime?: string; name?: string },
120
+ ): Promise<SavedAttachment> => {
121
+ const { open, rename, rm } = await import('node:fs/promises');
122
+ const path = await cachePathFor(messageId, index, meta);
123
+ const part = `${path}.part`;
124
+ const handle = await open(part, 'w');
125
+ let bytes = 0;
126
+ try {
127
+ for await (const chunk of chunks) {
128
+ bytes += chunk.length;
129
+ assertAttachmentSize(bytes);
130
+ await handle.write(chunk);
131
+ }
132
+ } catch (err) {
133
+ await handle.close();
134
+ await rm(part, { force: true });
135
+ throw err;
136
+ }
137
+ await handle.close();
138
+ await rename(part, path);
139
+ return { path, mime: meta.mime, name: meta.name, bytes };
140
+ };
141
+
142
+ export const guessMime = (path: string): string => {
143
+ const ext = (path.split('.').pop() ?? '').toLowerCase();
144
+ const map: Record<string, string> = {
145
+ png: 'image/png',
146
+ jpg: 'image/jpeg',
147
+ jpeg: 'image/jpeg',
148
+ gif: 'image/gif',
149
+ webp: 'image/webp',
150
+ heic: 'image/heic',
151
+ mp4: 'video/mp4',
152
+ mov: 'video/quicktime',
153
+ webm: 'video/webm',
154
+ m4v: 'video/x-m4v',
155
+ m4a: 'audio/mp4',
156
+ mp3: 'audio/mpeg',
157
+ ogg: 'audio/ogg',
158
+ wav: 'audio/wav',
159
+ pdf: 'application/pdf',
160
+ };
161
+ return map[ext] ?? 'application/octet-stream';
162
+ };
163
+
164
+ export const isImageMime = (mime: string): boolean =>
165
+ mime.toLowerCase().startsWith('image/');
166
+ export const isImageExt = (path: string): boolean =>
167
+ /\.(png|jpe?g|gif|webp|heic|bmp|svg)$/i.test(path);
168
+
169
+ export const appendFile = async (
170
+ form: FormData,
171
+ field: string,
172
+ path: string,
173
+ name: string,
174
+ ): Promise<void> => {
175
+ const data = await Bun.file(path).arrayBuffer();
176
+ form.append(field, new Blob([data]), name);
177
+ };
178
+
179
+ export const kindOf = (mime: string, path: string): string => {
180
+ if (isImageMime(mime) || isImageExt(path)) return 'image';
181
+ if (mime.startsWith('video/')) return 'video';
182
+ if (mime.startsWith('audio/')) return 'audio';
183
+ return 'file';
184
+ };
185
+
186
+ export const toCanonical = (a: {
187
+ path: string;
188
+ mime: string;
189
+ name: string;
190
+ }): Record<string, unknown> => ({
191
+ kind: kindOf(a.mime, a.path),
192
+ path: a.path,
193
+ url: a.path,
194
+ name: a.name,
195
+ mime: a.mime,
196
+ });