@stage-labs/metro 0.1.0-beta.4 → 0.1.0-beta.41
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 +131 -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 +181 -207
- package/dist/control.js +173 -0
- package/dist/plugin.js +82 -0
- package/dist/prompt.js +61 -0
- package/dist/runtime.js +117 -0
- package/dist/serve.js +77 -0
- package/dist/store.js +93 -0
- package/dist/tail.js +120 -0
- package/dist/update.js +33 -0
- package/dist/version.js +97 -0
- package/package.json +34 -21
- package/runtime/node_modules/@metro-labs/discord-bot/package.json +28 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +119 -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 +67 -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 +334 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-connector-api.ts +150 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-import.ts +76 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-pair.ts +20 -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 +153 -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 +133 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +278 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +383 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +143 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connect-hint.ts +33 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +315 -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 +100 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-url.ts +57 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +333 -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/hosted-connectors.ts +118 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +369 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +86 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/import-api.ts +67 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-connectors-api.ts +56 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-mode.ts +187 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-secret.ts +21 -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 +31 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-client.ts +162 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-discovery.ts +120 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-pending.ts +65 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/pair-codes.ts +58 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +145 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/project-api.ts +214 -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 +303 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +139 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-source.ts +105 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/secure-fs.ts +57 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-api.ts +27 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +63 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-config.ts +27 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +218 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-auth.ts +82 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-nonces.ts +43 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-routes.ts +89 -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 +257 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +295 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-store.ts +181 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +91 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +214 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-connectors.ts +143 -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 +37 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-config.ts +147 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-relay.ts +126 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +326 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/file-admin.ts +319 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/file-source.ts +152 -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/materialize.ts +373 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +309 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +24 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +105 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +118 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/token-carry.ts +45 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +72 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/whatsapp-login.ts +18 -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 +192 -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 +73 -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 +240 -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 +80 -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 +182 -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 +28 -0
- package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +81 -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 +120 -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 +40 -0
- package/runtime/node_modules/@metro-labs/whatsapp/package.json +29 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +67 -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 +113 -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/trains/.keep +0 -0
- package/LICENSE +0 -21
- package/README.md +0 -117
- package/dist/agents/claude.js +0 -207
- package/dist/agents/codex.js +0 -207
- package/dist/agents/types.js +0 -2
- package/dist/channels/discord.js +0 -104
- package/dist/channels/telegram.js +0 -189
- package/dist/helpers/scope-cache.js +0 -65
- package/dist/helpers/streaming.js +0 -209
- package/dist/helpers/telegram-format.js +0 -39
- package/dist/helpers/turn.js +0 -40
- package/dist/log.js +0 -10
- package/dist/orchestrator.js +0 -208
- package/dist/paths.js +0 -68
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { errMsg, log } from './log.js';
|
|
4
|
+
import { configDir } from './paths.js';
|
|
5
|
+
import { metroBaseUrl } from './agent-import.js';
|
|
6
|
+
|
|
7
|
+
const FETCH_TIMEOUT_MS = 15_000;
|
|
8
|
+
const CACHE_MS = 60_000;
|
|
9
|
+
|
|
10
|
+
export interface HostedConnector {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
url: string;
|
|
14
|
+
transport: string;
|
|
15
|
+
signIn: 'connected' | 'disconnected' | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface HostedCredential {
|
|
19
|
+
token: string;
|
|
20
|
+
url: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readJsonFile(path: string): Record<string, unknown> | null {
|
|
24
|
+
try {
|
|
25
|
+
const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'));
|
|
26
|
+
return typeof parsed === 'object' && parsed !== null ? (parsed as Record<string, unknown>) : null;
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function agentOfToken(token: string): string | null {
|
|
33
|
+
const body = token.split('.')[1];
|
|
34
|
+
if (body === undefined) return null;
|
|
35
|
+
try {
|
|
36
|
+
const claims = JSON.parse(Buffer.from(body, 'base64url').toString('utf8')) as { agent?: unknown };
|
|
37
|
+
return typeof claims.agent === 'string' ? claims.agent : null;
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function credential(file: Record<string, unknown> | null): HostedCredential | null {
|
|
44
|
+
if (file === null || typeof file.token !== 'string' || file.token === '') return null;
|
|
45
|
+
return { token: file.token, url: typeof file.url === 'string' && file.url !== '' ? file.url : metroBaseUrl() };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function hostedCredentialsFor(agentId: string, dir = configDir()): HostedCredential[] {
|
|
49
|
+
const out: HostedCredential[] = [];
|
|
50
|
+
const login = credential(readJsonFile(join(dir, 'credentials.json')));
|
|
51
|
+
if (login !== null && agentOfToken(login.token) === agentId) out.push(login);
|
|
52
|
+
const runtime = join(dir, `runtime-${agentId}.json`);
|
|
53
|
+
if (existsSync(runtime)) {
|
|
54
|
+
const cred = credential(readJsonFile(runtime));
|
|
55
|
+
if (cred !== null) out.push(cred);
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const isConnector = (v: unknown): v is HostedConnector =>
|
|
61
|
+
typeof v === 'object' &&
|
|
62
|
+
v !== null &&
|
|
63
|
+
typeof (v as { id?: unknown }).id === 'string' &&
|
|
64
|
+
typeof (v as { name?: unknown }).name === 'string';
|
|
65
|
+
|
|
66
|
+
class Refused extends Error {}
|
|
67
|
+
|
|
68
|
+
async function fetchHosted(cred: HostedCredential): Promise<HostedConnector[]> {
|
|
69
|
+
const res = await fetch(`${cred.url}/api/cli/connectors`, {
|
|
70
|
+
headers: { authorization: `Bearer ${cred.token}` },
|
|
71
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
72
|
+
redirect: 'manual',
|
|
73
|
+
});
|
|
74
|
+
if (res.status === 401 || res.status === 403 || res.status === 409) {
|
|
75
|
+
await res.arrayBuffer().catch(() => undefined);
|
|
76
|
+
throw new Refused(`metro no longer accepts this credential (${String(res.status)})`);
|
|
77
|
+
}
|
|
78
|
+
if (!res.ok) throw new Error(`metro answered ${String(res.status)} for the connectors`);
|
|
79
|
+
const body = (await res.json()) as { connectors?: unknown };
|
|
80
|
+
return Array.isArray(body.connectors) ? body.connectors.filter(isConnector) : [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function firstAccepted(agentId: string, creds: HostedCredential[]): Promise<HostedConnector[] | null> {
|
|
84
|
+
for (const cred of creds) {
|
|
85
|
+
try {
|
|
86
|
+
return await fetchHosted(cred);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
if (!(err instanceof Refused)) throw err;
|
|
89
|
+
log.warn({ agent: agentId, reason: errMsg(err) }, 'hosted connectors: credential refused, trying the next');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const cache = new Map<string, { at: number; list: HostedConnector[] }>();
|
|
96
|
+
|
|
97
|
+
export async function hostedConnectorsFor(agentId: string, dir = configDir()): Promise<HostedConnector[]> {
|
|
98
|
+
const hit = cache.get(agentId);
|
|
99
|
+
if (hit !== undefined && Date.now() - hit.at < CACHE_MS) return hit.list;
|
|
100
|
+
const creds = hostedCredentialsFor(agentId, dir);
|
|
101
|
+
if (creds.length === 0) return [];
|
|
102
|
+
try {
|
|
103
|
+
const list = await firstAccepted(agentId, creds);
|
|
104
|
+
if (list === null) {
|
|
105
|
+
log.warn({ agent: agentId }, 'hosted connectors: every stored credential is stale; metro login again');
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
cache.set(agentId, { at: Date.now(), list });
|
|
109
|
+
return list;
|
|
110
|
+
} catch (err) {
|
|
111
|
+
log.warn({ agent: agentId, err: errMsg(err) }, 'hosted connectors: unavailable, listing none');
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function forgetHostedConnectors(): void {
|
|
117
|
+
cache.clear();
|
|
118
|
+
}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { handleSessionApis, type SessionApis } from './session-apis.js';
|
|
2
|
+
import { handleRelayRequest } from './relay.js';
|
|
3
|
+
import {
|
|
4
|
+
createServer,
|
|
5
|
+
type IncomingMessage,
|
|
6
|
+
type Server,
|
|
7
|
+
type ServerResponse,
|
|
8
|
+
} from 'node:http';
|
|
9
|
+
import { Line } from '../stations/lines.js';
|
|
10
|
+
import { errMsg, log } from './log.js';
|
|
11
|
+
import {
|
|
12
|
+
classifyEvent,
|
|
13
|
+
formatDisplay,
|
|
14
|
+
mintId,
|
|
15
|
+
publishEvent,
|
|
16
|
+
userSelf,
|
|
17
|
+
type MetroEvent,
|
|
18
|
+
} from './events.js';
|
|
19
|
+
import type { TrainEvent } from './protocol.js';
|
|
20
|
+
import { agentForLine, agentIdForLine } from '../db/agent-map.js';
|
|
21
|
+
import {
|
|
22
|
+
findEndpointByWebhookId,
|
|
23
|
+
listEndpoints,
|
|
24
|
+
tokenMatches,
|
|
25
|
+
webhookPort,
|
|
26
|
+
type Endpoint,
|
|
27
|
+
} from './tunnel.js';
|
|
28
|
+
import { attachmentEventUrl, handleAttachRequest } from './attach-serve.js';
|
|
29
|
+
import { webhookEntry } from '@metro-labs/webhook';
|
|
30
|
+
import { handleSiweAuthRequest } from './siwe-routes.js';
|
|
31
|
+
import { handleModeRequest } from './mode-api.js';
|
|
32
|
+
import { handleUploadRequest } from './upload-api.js';
|
|
33
|
+
import {
|
|
34
|
+
handleMonitorRequest,
|
|
35
|
+
type MonitorCall,
|
|
36
|
+
} from '../monitor/api.js';
|
|
37
|
+
import { applyMcpCors, handleMcpPreflight } from './cors.js';
|
|
38
|
+
|
|
39
|
+
const LRU_CAP = 2_000;
|
|
40
|
+
|
|
41
|
+
const METRO_VERSION = process.env.npm_package_version ?? '0.1.0-beta.15';
|
|
42
|
+
|
|
43
|
+
function dedupKey(
|
|
44
|
+
e: Pick<MetroEvent, 'station' | 'line' | 'messageId'>,
|
|
45
|
+
): string | null {
|
|
46
|
+
if (!e.messageId) return null;
|
|
47
|
+
return `${e.station} ${e.line} ${e.messageId}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface DedupSeq {
|
|
51
|
+
admit(entry: MetroEvent): number | null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function makeDedupSeq(): DedupSeq {
|
|
55
|
+
const seen = new Map<string, true>();
|
|
56
|
+
const seqByLine = new Map<string, number>();
|
|
57
|
+
|
|
58
|
+
log.info('dedup+seq: live-from-boot (no persisted seed)');
|
|
59
|
+
|
|
60
|
+
const isInbound = (e: MetroEvent): boolean => !Line.isLocal(e.from);
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
admit(entry: MetroEvent): number | null {
|
|
64
|
+
const key = dedupKey(entry);
|
|
65
|
+
if (key && isInbound(entry)) {
|
|
66
|
+
if (seen.has(key)) {
|
|
67
|
+
log.debug(
|
|
68
|
+
{
|
|
69
|
+
station: entry.station,
|
|
70
|
+
line: entry.line,
|
|
71
|
+
messageId: entry.messageId,
|
|
72
|
+
},
|
|
73
|
+
'dedup: dropped duplicate inbound message',
|
|
74
|
+
);
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
seen.set(key, true);
|
|
78
|
+
while (seen.size > LRU_CAP) {
|
|
79
|
+
const oldest = seen.keys().next();
|
|
80
|
+
if (oldest.done) break;
|
|
81
|
+
seen.delete(oldest.value);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const next = (seqByLine.get(entry.line) ?? 0) + 1;
|
|
85
|
+
seqByLine.set(entry.line, next);
|
|
86
|
+
while (seqByLine.size > LRU_CAP) {
|
|
87
|
+
const oldest = seqByLine.keys().next();
|
|
88
|
+
if (oldest.done) break;
|
|
89
|
+
seqByLine.delete(oldest.value);
|
|
90
|
+
}
|
|
91
|
+
return next;
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function withAttachmentUrl(entry: MetroEvent): MetroEvent {
|
|
97
|
+
const payload = entry.payload;
|
|
98
|
+
if (!payload || typeof payload !== 'object') return entry;
|
|
99
|
+
const agentId = agentIdForLine(entry.line);
|
|
100
|
+
if (agentId === undefined) return entry;
|
|
101
|
+
const url = attachmentEventUrl(payload as Record<string, unknown>, agentId);
|
|
102
|
+
if (!url) return entry;
|
|
103
|
+
return { ...entry, payload: { ...payload, url } };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
type Emit = (entry: MetroEvent) => void;
|
|
107
|
+
|
|
108
|
+
export function makeEmit(dedupSeq?: DedupSeq): Emit {
|
|
109
|
+
const tracker = dedupSeq ?? makeDedupSeq();
|
|
110
|
+
return function emit(entry: MetroEvent): void {
|
|
111
|
+
const seq = tracker.admit(entry);
|
|
112
|
+
if (seq === null) return;
|
|
113
|
+
const enriched: MetroEvent = withAttachmentUrl({
|
|
114
|
+
...entry,
|
|
115
|
+
seq,
|
|
116
|
+
agent: entry.agent ?? agentForLine(entry.line),
|
|
117
|
+
display: entry.display ?? formatDisplay(entry),
|
|
118
|
+
event: entry.event ?? classifyEvent(entry),
|
|
119
|
+
});
|
|
120
|
+
process.stdout.write(JSON.stringify(enriched) + '\n');
|
|
121
|
+
publishEvent(enriched);
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function eventText(env: TrainEvent): string | undefined {
|
|
126
|
+
if (env.text !== undefined) return env.text;
|
|
127
|
+
return env.emoji ? `[react ${env.emoji}]` : undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function trainEventToMetroEvent(
|
|
131
|
+
env: TrainEvent,
|
|
132
|
+
trainName: string,
|
|
133
|
+
): MetroEvent | null {
|
|
134
|
+
const line = env.line;
|
|
135
|
+
if (typeof line !== 'string') {
|
|
136
|
+
log.warn({ train: trainName }, 'train: dropped event without `line`');
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const station = env.station ?? Line.station(line) ?? trainName;
|
|
140
|
+
const isPrivate = env.is_private === true;
|
|
141
|
+
const text = eventText(env);
|
|
142
|
+
return {
|
|
143
|
+
event: env.event,
|
|
144
|
+
agent: agentForLine(line),
|
|
145
|
+
id: env.id ?? mintId(),
|
|
146
|
+
ts: env.ts ?? new Date().toISOString(),
|
|
147
|
+
station,
|
|
148
|
+
line: line as MetroEvent['line'],
|
|
149
|
+
lineName: env.line_name,
|
|
150
|
+
from: (env.from ?? `metro://${station}`) as MetroEvent['from'],
|
|
151
|
+
fromName: env.from_name,
|
|
152
|
+
fromDisplayName: env.from_display_name,
|
|
153
|
+
to: (env.to ?? (isPrivate ? userSelf() : line)) as MetroEvent['to'],
|
|
154
|
+
text,
|
|
155
|
+
messageId: env.message_id,
|
|
156
|
+
replyTo: env.reply_to,
|
|
157
|
+
payload: env.payload,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
type McpHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
162
|
+
|
|
163
|
+
export async function startWebhookServer(
|
|
164
|
+
emit: Emit,
|
|
165
|
+
apis: SessionApis = {},
|
|
166
|
+
mcp?: McpHandler,
|
|
167
|
+
monitorCall?: MonitorCall,
|
|
168
|
+
): Promise<Server> {
|
|
169
|
+
const port = webhookPort();
|
|
170
|
+
const server = createServer((req, res) => {
|
|
171
|
+
handleRequest(req, res, emit, apis, mcp, monitorCall).catch(
|
|
172
|
+
(err: unknown) => {
|
|
173
|
+
log.warn({ err: errMsg(err) }, 'webhook handler error');
|
|
174
|
+
if (!res.headersSent) res.writeHead(500).end();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
await new Promise<void>((resolve, reject) => {
|
|
178
|
+
server.once('error', reject);
|
|
179
|
+
const host = process.env.METRO_HTTP_HOST ?? '127.0.0.1';
|
|
180
|
+
server.listen(port, host, () => {
|
|
181
|
+
log.info(
|
|
182
|
+
{
|
|
183
|
+
host,
|
|
184
|
+
port,
|
|
185
|
+
endpoints: listEndpoints().length,
|
|
186
|
+
mcp: mcp ? '/' : 'off',
|
|
187
|
+
},
|
|
188
|
+
'webhook + mcp ready',
|
|
189
|
+
);
|
|
190
|
+
resolve();
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
return server;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function isMcpPath(req: IncomingMessage): boolean {
|
|
197
|
+
const path = (req.url ?? '').split('?')[0];
|
|
198
|
+
return path === '/' || path === '/mcp';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export const WEBHOOK_BODY_MAX = 25 * 1024 * 1024;
|
|
202
|
+
|
|
203
|
+
export class BodyTooLargeError extends Error {
|
|
204
|
+
constructor(readonly limit: number) {
|
|
205
|
+
super(`request body exceeds ${limit} bytes`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export async function readBody(
|
|
210
|
+
req: IncomingMessage,
|
|
211
|
+
maxBytes: number,
|
|
212
|
+
): Promise<Buffer> {
|
|
213
|
+
const chunks: Buffer[] = [];
|
|
214
|
+
let total = 0;
|
|
215
|
+
for await (const c of req) {
|
|
216
|
+
const buf = c as Buffer;
|
|
217
|
+
total += buf.length;
|
|
218
|
+
if (total > maxBytes) throw new BodyTooLargeError(maxBytes);
|
|
219
|
+
chunks.push(buf);
|
|
220
|
+
}
|
|
221
|
+
return Buffer.concat(chunks);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function flatHeaders(req: IncomingMessage): Record<string, string> {
|
|
225
|
+
return Object.fromEntries(
|
|
226
|
+
Object.entries(req.headers).map(([k, v]) => [
|
|
227
|
+
k,
|
|
228
|
+
Array.isArray(v) ? v.join(',') : (v ?? ''),
|
|
229
|
+
]),
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function parseBody(raw: Buffer): unknown {
|
|
234
|
+
const text = raw.toString('utf8');
|
|
235
|
+
try {
|
|
236
|
+
return JSON.parse(text);
|
|
237
|
+
} catch {
|
|
238
|
+
return text;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function handleWebhookPost(
|
|
243
|
+
req: IncomingMessage,
|
|
244
|
+
res: ServerResponse,
|
|
245
|
+
emit: Emit,
|
|
246
|
+
endpointId: string,
|
|
247
|
+
endpoint: Endpoint,
|
|
248
|
+
): Promise<void> {
|
|
249
|
+
let raw: Buffer;
|
|
250
|
+
try {
|
|
251
|
+
raw = await readBody(req, WEBHOOK_BODY_MAX);
|
|
252
|
+
} catch (err) {
|
|
253
|
+
if (!(err instanceof BodyTooLargeError)) throw err;
|
|
254
|
+
log.warn({ endpoint: endpointId, limit: err.limit }, 'webhook body too large — rejecting');
|
|
255
|
+
res.writeHead(413).end('payload too large');
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const body = parseBody(raw);
|
|
259
|
+
emit(
|
|
260
|
+
webhookEntry(
|
|
261
|
+
endpoint,
|
|
262
|
+
flatHeaders(req),
|
|
263
|
+
body,
|
|
264
|
+
req.method ?? 'POST',
|
|
265
|
+
hookPath(endpointId),
|
|
266
|
+
),
|
|
267
|
+
);
|
|
268
|
+
res.writeHead(200).end('ok');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function handleHealth(req: IncomingMessage, res: ServerResponse): boolean {
|
|
272
|
+
const reqPath = (req.url ?? '').split('?')[0];
|
|
273
|
+
if (reqPath !== '/health' && reqPath !== '/healthz') return false;
|
|
274
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(
|
|
275
|
+
JSON.stringify({
|
|
276
|
+
status: 'ok',
|
|
277
|
+
version: METRO_VERSION,
|
|
278
|
+
uptime: Math.round(process.uptime()),
|
|
279
|
+
}),
|
|
280
|
+
);
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const HOOK_PATH =
|
|
285
|
+
/^\/api\/webhooks\/([0-9]{17,20})\/([A-Za-z0-9_-]{32,128})$/;
|
|
286
|
+
|
|
287
|
+
const HOOK_PREFIX = '/api/webhooks/';
|
|
288
|
+
|
|
289
|
+
const hookPath = (webhookId: string): string => `${HOOK_PREFIX}${webhookId}`;
|
|
290
|
+
|
|
291
|
+
function hookTarget(
|
|
292
|
+
path: string,
|
|
293
|
+
): { id: string; endpoint: Endpoint } | null {
|
|
294
|
+
const m = HOOK_PATH.exec(path);
|
|
295
|
+
const webhookId = m?.[1];
|
|
296
|
+
const token = m?.[2];
|
|
297
|
+
if (webhookId === undefined || token === undefined) return null;
|
|
298
|
+
const endpoint = findEndpointByWebhookId(webhookId);
|
|
299
|
+
if (!endpoint?.secret || !tokenMatches(endpoint.secret, token)) return null;
|
|
300
|
+
return { id: webhookId, endpoint };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function handleWebhookRoute(
|
|
304
|
+
req: IncomingMessage,
|
|
305
|
+
res: ServerResponse,
|
|
306
|
+
emit: Emit,
|
|
307
|
+
): Promise<boolean> {
|
|
308
|
+
const path = (req.url ?? '').split('?')[0] ?? '';
|
|
309
|
+
if (!path.startsWith(HOOK_PREFIX)) return false;
|
|
310
|
+
const target = hookTarget(path);
|
|
311
|
+
if (target === null) {
|
|
312
|
+
res.writeHead(404).end();
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
if (req.method === 'GET') {
|
|
316
|
+
res.writeHead(200).end(`metro webhook ${target.id} ready\n`);
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
if (req.method !== 'POST') {
|
|
320
|
+
res.writeHead(405).end();
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
await handleWebhookPost(req, res, emit, target.id, target.endpoint);
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function handleSignInRoutes(
|
|
328
|
+
req: IncomingMessage,
|
|
329
|
+
res: ServerResponse,
|
|
330
|
+
apis: SessionApis,
|
|
331
|
+
): boolean {
|
|
332
|
+
if (handleSiweAuthRequest(req, res, apis.siwe)) return true;
|
|
333
|
+
return apis.mode !== undefined && handleModeRequest(req, res, apis.mode);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function handlePreMcpRoutes(
|
|
337
|
+
req: IncomingMessage,
|
|
338
|
+
res: ServerResponse,
|
|
339
|
+
emit: Emit,
|
|
340
|
+
apis: SessionApis,
|
|
341
|
+
monitorCall?: MonitorCall,
|
|
342
|
+
): Promise<boolean> {
|
|
343
|
+
if (handleHealth(req, res)) return true;
|
|
344
|
+
if (handleSignInRoutes(req, res, apis)) return true;
|
|
345
|
+
if (handleSessionApis(req, res, apis)) return true;
|
|
346
|
+
if (handleUploadRequest(req, res)) return true;
|
|
347
|
+
if (handleAttachRequest(req, res)) return true;
|
|
348
|
+
if (apis.relayApi && handleRelayRequest(req, res, apis.relayApi)) return true;
|
|
349
|
+
if (await handleWebhookRoute(req, res, emit)) return true;
|
|
350
|
+
return Boolean(monitorCall && handleMonitorRequest(req, res, monitorCall));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function handleRequest(
|
|
354
|
+
req: IncomingMessage,
|
|
355
|
+
res: ServerResponse,
|
|
356
|
+
emit: Emit,
|
|
357
|
+
apis: SessionApis = {},
|
|
358
|
+
mcp?: McpHandler,
|
|
359
|
+
monitorCall?: MonitorCall,
|
|
360
|
+
): Promise<void> {
|
|
361
|
+
if (await handlePreMcpRoutes(req, res, emit, apis, monitorCall)) return;
|
|
362
|
+
if (mcp && isMcpPath(req)) {
|
|
363
|
+
applyMcpCors(req, res);
|
|
364
|
+
if (handleMcpPreflight(req, res)) return;
|
|
365
|
+
await mcp(req, res);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
res.writeHead(404).end();
|
|
369
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { errMsg, log } from './log.js';
|
|
3
|
+
import { Line } from '../stations/lines.js';
|
|
4
|
+
|
|
5
|
+
const TTL_MS = 5_000;
|
|
6
|
+
type Cache = { id: string; at: number } | null;
|
|
7
|
+
|
|
8
|
+
function memo(loader: () => string): () => string {
|
|
9
|
+
let cache: Cache = null;
|
|
10
|
+
return () => {
|
|
11
|
+
if (cache && Date.now() - cache.at < TTL_MS) return cache.id;
|
|
12
|
+
const id = loader();
|
|
13
|
+
cache = { id, at: Date.now() };
|
|
14
|
+
return id;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const claudeAccountId = memo(() => {
|
|
19
|
+
let raw: string;
|
|
20
|
+
try {
|
|
21
|
+
raw = execFileSync('claude', ['auth', 'status', '--json'], {
|
|
22
|
+
encoding: 'utf8',
|
|
23
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
24
|
+
});
|
|
25
|
+
} catch (e) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`metro: failed to run 'claude auth status --json' — is Claude Code installed? (${errMsg(e)})`,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
let p: { loggedIn?: boolean; orgId?: string };
|
|
31
|
+
try {
|
|
32
|
+
p = JSON.parse(raw) as { loggedIn?: boolean; orgId?: string };
|
|
33
|
+
} catch {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`metro: 'claude auth status --json' returned non-JSON: ${raw.slice(0, 200)}`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
if (!p.loggedIn || !p.orgId)
|
|
39
|
+
throw new Error(
|
|
40
|
+
"metro: Claude Code is not logged in — run 'claude auth login'",
|
|
41
|
+
);
|
|
42
|
+
return p.orgId;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export const claudeUserId = (): string =>
|
|
46
|
+
process.env.METRO_USER_ID ?? claudeAccountId();
|
|
47
|
+
|
|
48
|
+
export const claudeSessionId = (): string | null =>
|
|
49
|
+
process.env.METRO_USER_SESSION_ID ??
|
|
50
|
+
process.env.CLAUDE_CODE_SESSION_ID ??
|
|
51
|
+
null;
|
|
52
|
+
|
|
53
|
+
export function userSelf(): Line {
|
|
54
|
+
const explicit = process.env.METRO_FROM;
|
|
55
|
+
if (explicit) return explicit as Line;
|
|
56
|
+
if (!process.env.CLAUDECODE) return 'metro://user' as Line;
|
|
57
|
+
try {
|
|
58
|
+
return Line.user('claude', claudeUserId());
|
|
59
|
+
} catch (e) {
|
|
60
|
+
log.warn(
|
|
61
|
+
{ reason: errMsg(e) },
|
|
62
|
+
'could not resolve a Claude account id; relaying as metro://user',
|
|
63
|
+
);
|
|
64
|
+
return 'metro://user' as Line;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function daemonSelf(): Line {
|
|
69
|
+
const explicit = process.env.METRO_FROM ?? process.env.METRO_SELF_URI;
|
|
70
|
+
return (explicit ?? 'metro://user') as Line;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function selfLine(): Line | null {
|
|
74
|
+
if (!process.env.CLAUDECODE) return null;
|
|
75
|
+
const s = claudeSessionId();
|
|
76
|
+
if (!s) return null;
|
|
77
|
+
try {
|
|
78
|
+
return Line.claude(claudeUserId(), s);
|
|
79
|
+
} catch (e) {
|
|
80
|
+
log.warn(
|
|
81
|
+
{ reason: errMsg(e) },
|
|
82
|
+
'could not resolve a Claude account id; relaying with no self line',
|
|
83
|
+
);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import { errMsg, log } from './log.js';
|
|
3
|
+
import {
|
|
4
|
+
apiFailure,
|
|
5
|
+
apiSession,
|
|
6
|
+
bodyField,
|
|
7
|
+
cors,
|
|
8
|
+
readJsonBody,
|
|
9
|
+
sendJson,
|
|
10
|
+
} from './api-http.js';
|
|
11
|
+
import { parsePairingCode } from './agent-import.js';
|
|
12
|
+
import type { CreatedAgent } from '../db/agent-admin.js';
|
|
13
|
+
|
|
14
|
+
const PATH = '/api/agents/import';
|
|
15
|
+
|
|
16
|
+
export interface ImportedAgent extends CreatedAgent {
|
|
17
|
+
stations: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ImportApiDeps {
|
|
21
|
+
importAgent: (subject: string, code: string) => Promise<ImportedAgent>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function handle(
|
|
25
|
+
req: IncomingMessage,
|
|
26
|
+
res: ServerResponse,
|
|
27
|
+
deps: ImportApiDeps,
|
|
28
|
+
subject: string,
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
try {
|
|
31
|
+
const code = parsePairingCode(bodyField(await readJsonBody(req), 'code'));
|
|
32
|
+
const made = await deps.importAgent(subject, code);
|
|
33
|
+
log.info(
|
|
34
|
+
{ agent: made.name, id: made.id, stations: made.stations },
|
|
35
|
+
'import-api: agent imported from metro',
|
|
36
|
+
);
|
|
37
|
+
sendJson(req, res, 201, made);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
apiFailure(req, res, err, 'import-api');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function handleImportRequest(
|
|
44
|
+
req: IncomingMessage,
|
|
45
|
+
res: ServerResponse,
|
|
46
|
+
deps: ImportApiDeps,
|
|
47
|
+
): boolean {
|
|
48
|
+
if ((req.url ?? '').split('?')[0] !== PATH) return false;
|
|
49
|
+
if (req.method === 'OPTIONS') {
|
|
50
|
+
res.writeHead(204, cors(req)).end();
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
if (req.method !== 'POST') {
|
|
54
|
+
sendJson(req, res, 405, { error: 'method not allowed' });
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
const session = apiSession(req);
|
|
58
|
+
if (!session) {
|
|
59
|
+
sendJson(req, res, 401, { error: 'unauthorized' });
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
handle(req, res, deps, session.subject).catch((err: unknown) => {
|
|
63
|
+
log.warn({ err: errMsg(err) }, 'import-api: unhandled error');
|
|
64
|
+
if (!res.headersSent) sendJson(req, res, 500, { error: 'import failed' });
|
|
65
|
+
});
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import { errMsg, log } from './log.js';
|
|
3
|
+
import { apiSession, cors, sendJson } from './api-http.js';
|
|
4
|
+
import type { HostedConnector } from './hosted-connectors.js';
|
|
5
|
+
|
|
6
|
+
const PATH = '/api/connectors';
|
|
7
|
+
|
|
8
|
+
export interface LocalConnectorsDeps {
|
|
9
|
+
listConnectors: (subject: string) => Promise<HostedConnector[]>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const payload = (c: HostedConnector): Record<string, unknown> => ({
|
|
13
|
+
id: c.id,
|
|
14
|
+
name: c.name,
|
|
15
|
+
url: c.url,
|
|
16
|
+
transport: c.transport,
|
|
17
|
+
auth: c.signIn === null ? 'none' : 'oauth',
|
|
18
|
+
header: null,
|
|
19
|
+
signIn: c.signIn,
|
|
20
|
+
verified: null,
|
|
21
|
+
managed: 'metro.box',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export function handleLocalConnectorsRequest(
|
|
25
|
+
req: IncomingMessage,
|
|
26
|
+
res: ServerResponse,
|
|
27
|
+
deps: LocalConnectorsDeps,
|
|
28
|
+
): boolean {
|
|
29
|
+
const path = (req.url ?? '').split('?')[0] ?? '';
|
|
30
|
+
if (path !== PATH && !path.startsWith(`${PATH}/`)) return false;
|
|
31
|
+
if (req.method === 'OPTIONS') {
|
|
32
|
+
res.writeHead(204, cors(req)).end();
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
if (path !== PATH || req.method !== 'GET') {
|
|
36
|
+
sendJson(req, res, path === PATH ? 405 : 404, {
|
|
37
|
+
error: 'connectors are managed on metro.box, not on a local daemon',
|
|
38
|
+
});
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
const session = apiSession(req);
|
|
42
|
+
if (!session) {
|
|
43
|
+
sendJson(req, res, 401, { error: 'unauthorized' });
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
deps
|
|
47
|
+
.listConnectors(session.subject)
|
|
48
|
+
.then((list) => {
|
|
49
|
+
sendJson(req, res, 200, { connectors: list.map(payload) });
|
|
50
|
+
})
|
|
51
|
+
.catch((err: unknown) => {
|
|
52
|
+
log.warn({ err: errMsg(err) }, 'local connectors: request failed');
|
|
53
|
+
if (!res.headersSent) sendJson(req, res, 500, { error: 'connectors failed' });
|
|
54
|
+
});
|
|
55
|
+
return true;
|
|
56
|
+
}
|