@stage-labs/metro 0.1.0-beta.6 → 0.1.0-beta.60
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.
- package/dist/api.js +14 -0
- package/dist/bedrock-eventstream.js +120 -0
- package/dist/bedrock-proxy.js +260 -0
- package/dist/bedrock-upstream.js +72 -0
- package/dist/bedrock.js +106 -0
- package/dist/claude.js +15 -0
- package/dist/cli.js +94 -0
- package/dist/control.js +74 -0
- package/dist/local.js +93 -0
- package/dist/plugin.js +80 -0
- package/dist/runtime-install.js +84 -0
- package/dist/runtime.js +63 -0
- package/dist/serve.js +147 -0
- package/dist/tail.js +119 -0
- package/dist/update.js +45 -0
- package/dist/version.js +97 -0
- package/package.json +14 -25
- package/runtime/node_modules/@metro-labs/discord-bot/package.json +28 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +118 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +388 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/api-base.ts +1 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/attachments.ts +29 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +210 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/group-actions.ts +50 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/group.ts +115 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +127 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +215 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +43 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/station.ts +19 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +82 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/voice-speak.ts +195 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/voice-transcribe.ts +292 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/voice.ts +197 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +5 -0
- package/runtime/node_modules/@metro-labs/mcp/package.json +63 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/dedupe.ts +24 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/inbound.ts +367 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/media-note.ts +85 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/pending.ts +75 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/relay.ts +129 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/webhook-note.ts +51 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +311 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +302 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-connector-api.ts +138 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-error.ts +8 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +95 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-grant.ts +99 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-owner.ts +32 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +128 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +280 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +199 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/bundle-api.ts +111 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/claude-api.ts +97 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/claude-files.ts +329 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connect-hint.ts +39 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +311 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-json.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth-routes.ts +157 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +127 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-tools.ts +98 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-url.ts +70 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +330 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/cors.ts +29 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/crash-guard.ts +49 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/events.ts +117 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +368 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity-key.ts +33 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity-registry.ts +13 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity-routes.ts +54 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +7 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/is-record.ts +2 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-cli-api.ts +70 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-mode.ts +196 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-owner.ts +14 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/log.ts +42 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/mode-api.ts +32 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-client.ts +159 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-discovery.ts +117 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-pending.ts +65 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +109 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/protocol.ts +224 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/relay.ts +302 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/return-to.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-deps.ts +82 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/secure-fs.ts +57 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/server-types.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/servers-api.ts +80 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-api.ts +29 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +58 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/signed-identity.ts +36 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor-io.ts +97 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor.ts +320 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-call.ts +22 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-error.ts +53 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +352 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/update-api.ts +116 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +294 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-store.ts +181 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/vault-api.ts +78 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/vault-types.ts +12 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/version.ts +2 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +19 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/address.ts +6 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +43 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-map.ts +84 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-scope.ts +64 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/client.ts +32 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-config.ts +144 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-relay.ts +39 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +77 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/file-admin.ts +365 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/file-source.ts +161 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/ids.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/key-map.ts +45 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/local-connectors.ts +322 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +253 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +33 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/servers.ts +83 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/stations.ts +12 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/vault.ts +109 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/accounts.ts +141 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/call-tools.ts +203 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/channel-owner.ts +29 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/ctx.ts +62 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/event-store.ts +91 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/frame-scope.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/group-tools.ts +129 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/index.ts +191 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/member-tools.ts +72 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/permission-relay.ts +66 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/raw-get-stream.ts +207 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/request-identity.ts +47 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-registry.ts +172 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-route.ts +43 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/session.ts +211 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/str.ts +5 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-dispatch.ts +212 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +397 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/upload-tool.ts +66 -0
- package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +237 -0
- package/runtime/node_modules/@metro-labs/mcp/src/server.ts +2 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +60 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-inline.ts +100 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-interactive.ts +161 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-resolve.ts +200 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-xmtp.ts +111 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +184 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attachments.ts +196 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +82 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/messaging-normalize.ts +88 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/registry.ts +35 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/station-runtime.ts +64 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/types.ts +107 -0
- package/runtime/node_modules/@metro-labs/telegram/package.json +26 -0
- package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +80 -0
- package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +254 -0
- package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +87 -0
- package/runtime/node_modules/@metro-labs/telegram/src/client.ts +77 -0
- package/runtime/node_modules/@metro-labs/telegram/src/format.ts +141 -0
- package/runtime/node_modules/@metro-labs/telegram/src/group.ts +261 -0
- package/runtime/node_modules/@metro-labs/telegram/src/history.ts +52 -0
- package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +78 -0
- package/runtime/node_modules/@metro-labs/telegram/src/index.ts +46 -0
- package/runtime/node_modules/@metro-labs/telegram/src/login.ts +166 -0
- package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +80 -0
- package/runtime/node_modules/@metro-labs/telegram/src/members.ts +75 -0
- package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +22 -0
- package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +167 -0
- package/runtime/node_modules/@metro-labs/telegram/src/station.ts +24 -0
- package/runtime/node_modules/@metro-labs/telegram/src/types.ts +7 -0
- package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +1 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/package.json +25 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +119 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +295 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +172 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +103 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +166 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +17 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +48 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/verify.ts +38 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +3 -0
- package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
- package/runtime/node_modules/@metro-labs/webhook/src/station.ts +37 -0
- package/runtime/node_modules/@metro-labs/whatsapp/package.json +27 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +66 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +178 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +72 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +129 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +318 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +132 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +57 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +40 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +59 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/logger.ts +60 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +176 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +84 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +22 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/outbox.ts +38 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +141 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +17 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +98 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +6 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +5 -0
- package/runtime/node_modules/@metro-labs/xmtp/package.json +33 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +112 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +110 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +299 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +157 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +87 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +375 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +107 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +171 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +239 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +55 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +333 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +78 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +34 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +56 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +33 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +125 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +157 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +349 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +13 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +126 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +212 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +90 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +84 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +39 -0
- package/runtime/runtime.json +3 -0
- package/runtime/stations.json +35 -0
- package/runtime/trains/.keep +0 -0
- package/LICENSE +0 -21
- package/README.md +0 -192
- package/dist/cache.js +0 -74
- package/dist/cli/actions.js +0 -156
- package/dist/cli/config.js +0 -184
- package/dist/cli/index.js +0 -162
- package/dist/cli/skill.js +0 -62
- package/dist/cli/util.js +0 -72
- package/dist/codex-rc.js +0 -236
- package/dist/dispatcher.js +0 -112
- package/dist/history.js +0 -84
- package/dist/ipc.js +0 -71
- package/dist/log.js +0 -15
- package/dist/paths.js +0 -68
- package/dist/stations/discord.js +0 -198
- package/dist/stations/index.js +0 -73
- package/dist/stations/telegram-md.js +0 -34
- package/dist/stations/telegram-upload.js +0 -113
- package/dist/stations/telegram.js +0 -235
- package/docs/agents.md +0 -168
- package/docs/uri-scheme.md +0 -71
- package/skills/metro/SKILL.md +0 -220
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { errMsg } from '../daemon/log.js';
|
|
3
|
+
import { chmodIfExists } from '../daemon/secure-fs.js';
|
|
4
|
+
|
|
5
|
+
export type Die = (msg: string) => never;
|
|
6
|
+
|
|
7
|
+
export interface MakeLoaderOpts<T> {
|
|
8
|
+
prefix: string;
|
|
9
|
+
file: string;
|
|
10
|
+
validate: (raw: T[], die: Die) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface Loader<T> {
|
|
14
|
+
die: Die;
|
|
15
|
+
loadAccounts: () => T[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function makeAccountStore<T extends { id: string }>(
|
|
19
|
+
opts: MakeLoaderOpts<T>,
|
|
20
|
+
): Loader<T> {
|
|
21
|
+
const die: Die = (msg) => {
|
|
22
|
+
process.stderr.write(`${opts.prefix}: ${msg}\n`);
|
|
23
|
+
process.exit(2);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function loadAccounts(): T[] {
|
|
27
|
+
if (!existsSync(opts.file))
|
|
28
|
+
return die(
|
|
29
|
+
`no accounts file ${opts.file} — the daemon writes it from the database at boot; set DATABASE_URL and populate the accounts table`,
|
|
30
|
+
);
|
|
31
|
+
chmodIfExists(opts.file);
|
|
32
|
+
let raw: T[];
|
|
33
|
+
try {
|
|
34
|
+
raw = JSON.parse(readFileSync(opts.file, 'utf8')) as T[];
|
|
35
|
+
} catch (e) {
|
|
36
|
+
return die(`bad ${opts.file}: ${errMsg(e)}`);
|
|
37
|
+
}
|
|
38
|
+
if (!Array.isArray(raw) || raw.length === 0)
|
|
39
|
+
die(`${opts.file} must be a non-empty array`);
|
|
40
|
+
opts.validate(raw, die);
|
|
41
|
+
return raw;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { die, loadAccounts };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function resolveAccountId(
|
|
48
|
+
accounts: Map<string, unknown>,
|
|
49
|
+
args: { account?: string; line?: string },
|
|
50
|
+
parseAccountId: (line: string) => string | undefined,
|
|
51
|
+
): string {
|
|
52
|
+
let id = args.account;
|
|
53
|
+
id ??= args.line ? parseAccountId(args.line) : undefined;
|
|
54
|
+
id ??= (accounts.size === 1 ? [...accounts.keys()][0] : 'default') ?? 'default';
|
|
55
|
+
if (!accounts.has(id))
|
|
56
|
+
throw new Error(
|
|
57
|
+
`unknown account '${id}' (have: ${[...accounts.keys()].join(', ')})`,
|
|
58
|
+
);
|
|
59
|
+
return id;
|
|
60
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
export const MAX_INLINE_BYTES = 8 * 1024 * 1024;
|
|
6
|
+
|
|
7
|
+
const INLINE_TEMP_PREFIX = 'metro-inline-';
|
|
8
|
+
|
|
9
|
+
const BASE64_RE = /^[A-Za-z0-9+/_-]*={0,2}$/;
|
|
10
|
+
|
|
11
|
+
const mib = (n: number): string => `${(n / (1024 * 1024)).toFixed(2)} MiB`;
|
|
12
|
+
|
|
13
|
+
const LIMIT_HINT =
|
|
14
|
+
`inline \`data\` is capped at ${mib(MAX_INLINE_BYTES)} (${MAX_INLINE_BYTES} bytes) of decoded ` +
|
|
15
|
+
'bytes per attachment and per send, because the base64 travels inside the MCP request body. ' +
|
|
16
|
+
'Pass `url`, or a `path` already on the daemon host, for anything larger.';
|
|
17
|
+
|
|
18
|
+
function assertInlineSize(bytes: number, label: string): void {
|
|
19
|
+
if (bytes > MAX_INLINE_BYTES)
|
|
20
|
+
throw new Error(`inline attachment ${label} is ${mib(bytes)}; ${LIMIT_HINT}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function assertInlineTotal(bytes: number): void {
|
|
24
|
+
if (bytes > MAX_INLINE_BYTES)
|
|
25
|
+
throw new Error(
|
|
26
|
+
`inline attachments total ${mib(bytes)} in one send; ${LIMIT_HINT}`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const padOf = (b64: string): number =>
|
|
31
|
+
b64.endsWith('==') ? 2 : b64.endsWith('=') ? 1 : 0;
|
|
32
|
+
|
|
33
|
+
export const decodedLengthOf = (b64: string): number =>
|
|
34
|
+
Math.max(0, Math.floor((b64.length * 3) / 4) - padOf(b64));
|
|
35
|
+
|
|
36
|
+
const malformed = (why: string): Error =>
|
|
37
|
+
new Error(
|
|
38
|
+
`inline \`data\` ${why}; pass the file's bytes base64-encoded ` +
|
|
39
|
+
'(standard or url-safe alphabet, optionally behind a `data:<mime>;base64,` prefix)',
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const DATA_URL_RE = /^data:([^,]*),/i;
|
|
43
|
+
|
|
44
|
+
export interface InlineSource {
|
|
45
|
+
data: string;
|
|
46
|
+
mime?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function splitInlineData(raw: string): InlineSource {
|
|
50
|
+
const compact = raw.replace(/\s+/g, '');
|
|
51
|
+
const match = DATA_URL_RE.exec(compact);
|
|
52
|
+
if (!match) return { data: raw };
|
|
53
|
+
const params = (match[1] ?? '').split(';');
|
|
54
|
+
if (!params.slice(1).some((p) => p.toLowerCase() === 'base64'))
|
|
55
|
+
throw malformed('is a `data:` url whose payload is not base64');
|
|
56
|
+
const mime = params[0]?.toLowerCase();
|
|
57
|
+
const data = compact.slice(match[0].length);
|
|
58
|
+
return mime === undefined || mime === '' ? { data } : { data, mime };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function decodeInline(raw: string, label: string): Uint8Array {
|
|
62
|
+
const b64 = raw.replace(/\s+/g, '');
|
|
63
|
+
if (!b64) throw malformed('is empty');
|
|
64
|
+
const expected = decodedLengthOf(b64);
|
|
65
|
+
assertInlineSize(expected, label);
|
|
66
|
+
if (!BASE64_RE.test(b64)) throw malformed('is not base64');
|
|
67
|
+
if (b64.length % 4 === 1 || (padOf(b64) > 0 && b64.length % 4 !== 0))
|
|
68
|
+
throw malformed('is not a whole number of base64 quanta');
|
|
69
|
+
const buf = Buffer.from(b64, 'base64');
|
|
70
|
+
if (buf.length !== expected)
|
|
71
|
+
throw malformed(`decoded to ${buf.length} bytes instead of ${expected}`);
|
|
72
|
+
return new Uint8Array(buf);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const safeFileName = (name: string | undefined): string => {
|
|
76
|
+
const base = (name ?? '').split(/[\\/]/).pop() ?? '';
|
|
77
|
+
const clean = base
|
|
78
|
+
.replace(/[^A-Za-z0-9._-]/g, '_')
|
|
79
|
+
.replace(/^\.+/, '')
|
|
80
|
+
.slice(0, 64);
|
|
81
|
+
return clean || 'attachment';
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export interface InlineTemp {
|
|
85
|
+
dir: string;
|
|
86
|
+
path: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function writeInlineTemp(
|
|
90
|
+
data: Uint8Array,
|
|
91
|
+
name: string | undefined,
|
|
92
|
+
): Promise<InlineTemp> {
|
|
93
|
+
const dir = await mkdtemp(join(tmpdir(), INLINE_TEMP_PREFIX));
|
|
94
|
+
const path = join(dir, safeFileName(name));
|
|
95
|
+
await writeFile(path, data, { mode: 0o600 });
|
|
96
|
+
return { dir, path };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const removeInlineTemp = (dir: string): Promise<void> =>
|
|
100
|
+
rm(dir, { recursive: true, force: true });
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { StationAttachError } from './attach.js';
|
|
2
|
+
|
|
3
|
+
export const INTERACTIVE_STATIONS = ['telegram', 'whatsapp'] as const;
|
|
4
|
+
|
|
5
|
+
export type InteractiveStation = (typeof INTERACTIVE_STATIONS)[number];
|
|
6
|
+
|
|
7
|
+
export type AttachStep = 'code' | 'password' | 'scan' | 'pair';
|
|
8
|
+
|
|
9
|
+
export interface AttachPrompt {
|
|
10
|
+
step: AttachStep;
|
|
11
|
+
prompt: string;
|
|
12
|
+
qr?: string;
|
|
13
|
+
pairingCode?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface AttachOutcome {
|
|
17
|
+
config: Record<string, unknown>;
|
|
18
|
+
identity: Record<string, string>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface DriverHooks {
|
|
22
|
+
prompt: (p: AttachPrompt) => void;
|
|
23
|
+
done: (o: AttachOutcome) => void;
|
|
24
|
+
fail: (message: string) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AttachDriver {
|
|
28
|
+
submit: (input: { code?: unknown; password?: unknown }) => Promise<void>;
|
|
29
|
+
cancel: () => Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface StartedAttach {
|
|
33
|
+
driver: AttachDriver;
|
|
34
|
+
prompt: AttachPrompt;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const CODE_PROMPT =
|
|
38
|
+
'Telegram sent a login code to that number. Enter it here.';
|
|
39
|
+
const PASSWORD_PROMPT =
|
|
40
|
+
'This account has two-step verification. Enter its password.';
|
|
41
|
+
const SCAN_PROMPT =
|
|
42
|
+
'Open WhatsApp on your phone, go to Settings, Linked Devices, Link a Device, and scan this code.';
|
|
43
|
+
const PAIR_PROMPT =
|
|
44
|
+
'Open WhatsApp on your phone, go to Settings, Linked Devices, Link a Device, Link with phone number, and type this code.';
|
|
45
|
+
|
|
46
|
+
export function isInteractiveStation(raw: unknown): raw is InteractiveStation {
|
|
47
|
+
return (
|
|
48
|
+
typeof raw === 'string' &&
|
|
49
|
+
(INTERACTIVE_STATIONS as readonly string[]).includes(raw)
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function refuse(err: unknown, fallback: string): never {
|
|
54
|
+
throw new StationAttachError(
|
|
55
|
+
err instanceof Error && err.message !== '' ? err.message : fallback,
|
|
56
|
+
400,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function startTelegramUser(
|
|
61
|
+
input: Record<string, unknown>,
|
|
62
|
+
hooks: DriverHooks,
|
|
63
|
+
): Promise<StartedAttach> {
|
|
64
|
+
const { TelegramLogin, TelegramLoginError, validateCredentials } =
|
|
65
|
+
await import('@metro-labs/telegram/login');
|
|
66
|
+
const isOwn = (e: unknown): boolean => e instanceof TelegramLoginError;
|
|
67
|
+
let login;
|
|
68
|
+
try {
|
|
69
|
+
login = new TelegramLogin(
|
|
70
|
+
validateCredentials({
|
|
71
|
+
apiId: input.apiId,
|
|
72
|
+
apiHash: input.apiHash,
|
|
73
|
+
phone: input.phone,
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
await login.requestCode();
|
|
77
|
+
} catch (err) {
|
|
78
|
+
await login?.close();
|
|
79
|
+
refuse(isOwn(err) ? err : null, 'Telegram refused to start the sign-in');
|
|
80
|
+
}
|
|
81
|
+
const settled = login;
|
|
82
|
+
return {
|
|
83
|
+
prompt: { step: 'code', prompt: CODE_PROMPT },
|
|
84
|
+
driver: {
|
|
85
|
+
cancel: () => settled.close(),
|
|
86
|
+
submit: async (values) => {
|
|
87
|
+
try {
|
|
88
|
+
if (typeof values.password === 'string') {
|
|
89
|
+
hooks.done(await settled.submitPassword(values.password));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (typeof values.code !== 'string')
|
|
93
|
+
throw new StationAttachError('enter the login code', 400);
|
|
94
|
+
const result = await settled.submitCode(values.code);
|
|
95
|
+
if (result === 'password') {
|
|
96
|
+
hooks.prompt({ step: 'password', prompt: PASSWORD_PROMPT });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
hooks.done(result);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
if (err instanceof StationAttachError) throw err;
|
|
102
|
+
refuse(isOwn(err) ? err : null, 'Telegram refused the sign-in');
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function startWhatsapp(
|
|
110
|
+
input: Record<string, unknown>,
|
|
111
|
+
hooks: DriverHooks,
|
|
112
|
+
): Promise<StartedAttach> {
|
|
113
|
+
const { WhatsappLogin, WhatsappLoginError, normalizePhone } = await import(
|
|
114
|
+
'@metro-labs/whatsapp/login'
|
|
115
|
+
);
|
|
116
|
+
const isOwn = (e: unknown): boolean => e instanceof WhatsappLoginError;
|
|
117
|
+
let login;
|
|
118
|
+
try {
|
|
119
|
+
login = new WhatsappLogin(normalizePhone(input.phone), {
|
|
120
|
+
onQr: (qr) => {
|
|
121
|
+
hooks.prompt({ step: 'scan', prompt: SCAN_PROMPT, qr });
|
|
122
|
+
},
|
|
123
|
+
onPairingCode: (pairingCode) => {
|
|
124
|
+
hooks.prompt({ step: 'pair', prompt: PAIR_PROMPT, pairingCode });
|
|
125
|
+
},
|
|
126
|
+
onPaired: hooks.done,
|
|
127
|
+
onFailed: hooks.fail,
|
|
128
|
+
});
|
|
129
|
+
await login.start();
|
|
130
|
+
} catch (err) {
|
|
131
|
+
await login?.cancel();
|
|
132
|
+
refuse(isOwn(err) ? err : null, 'WhatsApp refused to start the pairing');
|
|
133
|
+
}
|
|
134
|
+
const settled = login;
|
|
135
|
+
const qrMode = settled.mode === 'qr';
|
|
136
|
+
return {
|
|
137
|
+
prompt: qrMode
|
|
138
|
+
? { step: 'scan', prompt: SCAN_PROMPT }
|
|
139
|
+
: { step: 'pair', prompt: PAIR_PROMPT },
|
|
140
|
+
driver: {
|
|
141
|
+
cancel: () => settled.cancel(),
|
|
142
|
+
submit: () =>
|
|
143
|
+
Promise.reject(
|
|
144
|
+
new StationAttachError(
|
|
145
|
+
'this pairing finishes on your phone, there is nothing to submit here',
|
|
146
|
+
409,
|
|
147
|
+
),
|
|
148
|
+
),
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function startInteractiveAttach(
|
|
154
|
+
station: InteractiveStation,
|
|
155
|
+
input: Record<string, unknown>,
|
|
156
|
+
hooks: DriverHooks,
|
|
157
|
+
): Promise<StartedAttach> {
|
|
158
|
+
return station === 'telegram'
|
|
159
|
+
? startTelegramUser(input, hooks)
|
|
160
|
+
: startWhatsapp(input, hooks);
|
|
161
|
+
}
|
|
@@ -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
|
+
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
|
+
}
|