@stage-labs/metro 0.1.0-beta.8 → 0.1.0-beta.80
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/claude-settings.js +40 -0
- package/dist/claude.js +107 -0
- package/dist/cli.js +101 -0
- package/dist/control.js +74 -0
- package/dist/hold.js +211 -0
- package/dist/local.js +93 -0
- package/dist/node-name.js +71 -0
- package/dist/plugin.js +80 -0
- package/dist/provider-flags.js +9 -0
- package/dist/runtime-install.js +87 -0
- package/dist/runtime.js +74 -0
- package/dist/serve.js +146 -0
- package/dist/service.js +242 -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/core/package.json +43 -0
- package/runtime/node_modules/@metro-labs/core/src/address.ts +6 -0
- package/runtime/node_modules/@metro-labs/core/src/endpoints.ts +7 -0
- package/runtime/node_modules/@metro-labs/core/src/events.ts +120 -0
- package/runtime/node_modules/@metro-labs/core/src/identity.ts +7 -0
- package/runtime/node_modules/@metro-labs/core/src/ids.ts +21 -0
- package/runtime/node_modules/@metro-labs/core/src/is-record.ts +2 -0
- package/runtime/node_modules/@metro-labs/core/src/lines.ts +82 -0
- package/runtime/node_modules/@metro-labs/core/src/log.ts +42 -0
- package/runtime/node_modules/@metro-labs/core/src/protocol.ts +226 -0
- package/runtime/node_modules/@metro-labs/core/src/secure-fs.ts +57 -0
- package/runtime/node_modules/@metro-labs/core/src/station-names.ts +12 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/account-store.ts +60 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/attachments.ts +196 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +88 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/station-runtime.ts +64 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/types.ts +107 -0
- package/runtime/node_modules/@metro-labs/core/src/str.ts +5 -0
- package/runtime/node_modules/@metro-labs/core/src/train-error.ts +53 -0
- package/runtime/node_modules/@metro-labs/core/src/version.ts +2 -0
- package/runtime/node_modules/@metro-labs/daemon/package.json +47 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/account-attach.ts +19 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/accounts-api.ts +311 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/admin.ts +43 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/api.ts +302 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +111 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +329 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/files.ts +155 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/keys.ts +45 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/map.ts +84 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/scope.ts +64 -0
- package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +176 -0
- package/runtime/node_modules/@metro-labs/daemon/src/boot/connect-hint.ts +39 -0
- package/runtime/node_modules/@metro-labs/daemon/src/boot/crash-guard.ts +49 -0
- package/runtime/node_modules/@metro-labs/daemon/src/boot/local-owner.ts +14 -0
- package/runtime/node_modules/@metro-labs/daemon/src/boot/paths.ts +106 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/addressed.ts +25 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/dedupe.ts +24 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/inbound.ts +377 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/media-note.ts +85 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/pending.ts +75 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/relay.ts +129 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/webhook-note.ts +51 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/api.ts +148 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +329 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/login.ts +167 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/settings.ts +117 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/api.ts +307 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/cli-api.ts +63 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/config.ts +144 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/json.ts +21 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/model.ts +69 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-client.ts +159 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-discovery.ts +117 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-pending.ts +65 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-routes.ts +157 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth.ts +127 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-target.ts +39 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay.ts +295 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/store.ts +277 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/tools.ts +98 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/url.ts +70 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/verify.ts +330 -0
- package/runtime/node_modules/@metro-labs/daemon/src/files/attach-grant.ts +99 -0
- package/runtime/node_modules/@metro-labs/daemon/src/files/attach-owner.ts +32 -0
- package/runtime/node_modules/@metro-labs/daemon/src/files/attach-serve.ts +121 -0
- package/runtime/node_modules/@metro-labs/daemon/src/files/upload-api.ts +294 -0
- package/runtime/node_modules/@metro-labs/daemon/src/files/upload-store.ts +181 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/bedrock.ts +252 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-auth.ts +231 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-device.ts +91 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-stream.ts +288 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-translate.ts +203 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +220 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/eventstream.ts +123 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +182 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +197 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +195 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-config.ts +198 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter.ts +40 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/served.ts +17 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/accounts.ts +137 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/call-tools.ts +216 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/channel-owner.ts +29 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/ctx.ts +62 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/event-store.ts +91 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/frame-scope.ts +21 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/group-tools.ts +129 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/index.ts +191 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/member-tools.ts +72 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-relay.ts +66 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/raw-get-stream.ts +207 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/request-identity.ts +47 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-registry.ts +172 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-route.ts +43 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session.ts +215 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-dispatch.ts +215 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-schemas.ts +400 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/upload-tool.ts +66 -0
- package/runtime/node_modules/@metro-labs/daemon/src/monitor/api.ts +237 -0
- package/runtime/node_modules/@metro-labs/daemon/src/net/tunnel.ts +389 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/http.ts +390 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/identity.ts +54 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +187 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/session-apis.ts +60 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/session.ts +29 -0
- package/runtime/node_modules/@metro-labs/daemon/src/server/control.ts +64 -0
- package/runtime/node_modules/@metro-labs/daemon/src/server/machine.ts +61 -0
- package/runtime/node_modules/@metro-labs/daemon/src/server/update.ts +116 -0
- package/runtime/node_modules/@metro-labs/daemon/src/server.ts +2 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-inline.ts +100 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-interactive.ts +161 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-resolve.ts +202 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-session.ts +280 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-xmtp.ts +111 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach.ts +184 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/materialize.ts +248 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/registry.ts +35 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/runtime-deps.ts +82 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor-io.ts +97 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor.ts +320 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/train-call.ts +22 -0
- package/runtime/node_modules/@metro-labs/daemon/src/terminal/api.ts +71 -0
- package/runtime/node_modules/@metro-labs/daemon/src/terminal/socket.ts +123 -0
- package/runtime/node_modules/@metro-labs/daemon/src/terminal/tickets.ts +40 -0
- 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 +220 -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/http/package.json +33 -0
- package/runtime/node_modules/@metro-labs/http/src/api-error.ts +8 -0
- package/runtime/node_modules/@metro-labs/http/src/api-http.ts +95 -0
- package/runtime/node_modules/@metro-labs/http/src/cors.ts +29 -0
- package/runtime/node_modules/@metro-labs/http/src/identity-key.ts +33 -0
- package/runtime/node_modules/@metro-labs/http/src/identity-registry.ts +13 -0
- package/runtime/node_modules/@metro-labs/http/src/mode-api.ts +32 -0
- package/runtime/node_modules/@metro-labs/http/src/return-to.ts +21 -0
- package/runtime/node_modules/@metro-labs/http/src/signed-identity.ts +36 -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 +156 -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 +201 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +109 -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 +58 -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 +327 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +138 -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 +185 -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 +335 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +80 -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 +53 -0
- package/runtime/runtime.json +3 -0
- package/runtime/server.ts +1 -0
- package/runtime/stations.json +36 -0
- package/runtime/trains/.keep +0 -0
- package/LICENSE +0 -21
- package/README.md +0 -249
- package/dist/cache.js +0 -75
- package/dist/cli/actions.js +0 -139
- package/dist/cli/config.js +0 -185
- package/dist/cli/index.js +0 -177
- package/dist/cli/skill.js +0 -62
- package/dist/cli/util.js +0 -72
- package/dist/cli/webhook.js +0 -83
- package/dist/codex-rc.js +0 -245
- package/dist/dispatcher.js +0 -122
- package/dist/history.js +0 -99
- package/dist/ipc.js +0 -72
- package/dist/log.js +0 -15
- package/dist/paths.js +0 -68
- package/dist/registry.js +0 -48
- package/dist/stations/claude.js +0 -45
- package/dist/stations/codex.js +0 -68
- package/dist/stations/discord.js +0 -200
- package/dist/stations/index.js +0 -133
- package/dist/stations/telegram-md.js +0 -34
- package/dist/stations/telegram-upload.js +0 -113
- package/dist/stations/telegram.js +0 -213
- package/dist/stations/webhook.js +0 -100
- package/dist/tunnel.js +0 -49
- package/dist/webhooks.js +0 -40
- package/docs/agents.md +0 -216
- package/docs/uri-scheme.md +0 -125
- package/skills/metro/SKILL.md +0 -249
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ConnectorAuth, OAuthAuth } from './verify.js';
|
|
2
|
+
|
|
3
|
+
export type RelayTarget =
|
|
4
|
+
| { kind: 'ok'; url: string; headers: Record<string, string> }
|
|
5
|
+
| { kind: 'missing' }
|
|
6
|
+
| { kind: 'signin' };
|
|
7
|
+
|
|
8
|
+
export const bearerHeaders = (token: string): Record<string, string> => ({
|
|
9
|
+
Authorization: `Bearer ${token}`,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export function staleUsable(auth: OAuthAuth, now = Date.now()): boolean {
|
|
13
|
+
return auth.expiresAt === undefined || auth.expiresAt > now;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function headerAuthHeaders(auth: ConnectorAuth): Record<string, string> | null {
|
|
17
|
+
return auth.kind === 'header' ? { [auth.name]: auth.value } : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function unrefreshedTarget(
|
|
21
|
+
url: string,
|
|
22
|
+
auth: OAuthAuth,
|
|
23
|
+
force: boolean,
|
|
24
|
+
now = Date.now(),
|
|
25
|
+
): RelayTarget {
|
|
26
|
+
if (force) return { kind: 'signin' };
|
|
27
|
+
if (staleUsable(auth, now))
|
|
28
|
+
return { kind: 'ok', url, headers: bearerHeaders(auth.accessToken) };
|
|
29
|
+
return { kind: 'ok', url, headers: {} };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function fixedTarget(
|
|
33
|
+
url: string,
|
|
34
|
+
auth: Exclude<ConnectorAuth, OAuthAuth>,
|
|
35
|
+
force: boolean,
|
|
36
|
+
): RelayTarget {
|
|
37
|
+
if (force) return { kind: 'signin' };
|
|
38
|
+
return { kind: 'ok', url, headers: headerAuthHeaders(auth) ?? {} };
|
|
39
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import type { RelayTarget } from './relay-target.js';
|
|
3
|
+
import type { AgentIdentity } from '@metro-labs/http/api-http';
|
|
4
|
+
import { ApiError } from '@metro-labs/http/api-error';
|
|
5
|
+
import { errMsg, log } from '@metro-labs/core/log';
|
|
6
|
+
|
|
7
|
+
export interface RelayApiDeps {
|
|
8
|
+
target: (connectorId: string, force: boolean) => Promise<RelayTarget>;
|
|
9
|
+
identify: (req: IncomingMessage) => AgentIdentity | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const ID_PATH_RE = /^\/relay\/([A-Za-z0-9][A-Za-z0-9_-]{10})$/;
|
|
13
|
+
const METHODS = new Set(['POST', 'GET', 'DELETE']);
|
|
14
|
+
const RELAY_BODY_MAX = 8 * 1024 * 1024;
|
|
15
|
+
const FIRST_BYTE_MS = 120_000;
|
|
16
|
+
const PASS_REQ = [
|
|
17
|
+
'accept',
|
|
18
|
+
'content-type',
|
|
19
|
+
'mcp-session-id',
|
|
20
|
+
'mcp-protocol-version',
|
|
21
|
+
'last-event-id',
|
|
22
|
+
] as const;
|
|
23
|
+
const PASS_RES = [
|
|
24
|
+
'content-type',
|
|
25
|
+
'mcp-session-id',
|
|
26
|
+
'mcp-protocol-version',
|
|
27
|
+
'cache-control',
|
|
28
|
+
] as const;
|
|
29
|
+
|
|
30
|
+
const keepaliveMs = (): number =>
|
|
31
|
+
Number(process.env.METRO_RELAY_KEEPALIVE_MS) || 25_000;
|
|
32
|
+
|
|
33
|
+
const reconnectUrl = (id: string): string =>
|
|
34
|
+
`https://metro.box/#/connector/${id}`;
|
|
35
|
+
|
|
36
|
+
function answer(
|
|
37
|
+
res: ServerResponse,
|
|
38
|
+
status: number,
|
|
39
|
+
body: Record<string, unknown>,
|
|
40
|
+
): void {
|
|
41
|
+
if (res.headersSent) return;
|
|
42
|
+
res.writeHead(status, { 'content-type': 'application/json' });
|
|
43
|
+
res.end(JSON.stringify(body));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const signinAnswer = (res: ServerResponse, id: string): void => {
|
|
47
|
+
answer(res, 424, {
|
|
48
|
+
error: 'this connector needs signing in again',
|
|
49
|
+
reconnect: reconnectUrl(id),
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function closeIfBodyUnread(req: IncomingMessage, res: ServerResponse): void {
|
|
54
|
+
if (req.readableEnded) return;
|
|
55
|
+
const drop = (): void => {
|
|
56
|
+
req.socket?.destroy();
|
|
57
|
+
};
|
|
58
|
+
if (res.writableFinished) drop();
|
|
59
|
+
else res.once('finish', drop);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function readCapped(req: IncomingMessage): Promise<Uint8Array<ArrayBuffer>> {
|
|
63
|
+
const declared = Number(req.headers['content-length']);
|
|
64
|
+
if (Number.isFinite(declared) && declared > RELAY_BODY_MAX)
|
|
65
|
+
throw new ApiError('relay body exceeds 8 MiB', 413);
|
|
66
|
+
const chunks: Buffer[] = [];
|
|
67
|
+
let total = 0;
|
|
68
|
+
for await (const chunk of req) {
|
|
69
|
+
const buf = chunk as Buffer;
|
|
70
|
+
total += buf.length;
|
|
71
|
+
if (total > RELAY_BODY_MAX)
|
|
72
|
+
throw new ApiError('relay body exceeds 8 MiB', 413);
|
|
73
|
+
chunks.push(buf);
|
|
74
|
+
}
|
|
75
|
+
const merged = Buffer.concat(chunks);
|
|
76
|
+
const out = new Uint8Array(new ArrayBuffer(merged.byteLength));
|
|
77
|
+
out.set(merged);
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function upstreamHeaders(
|
|
82
|
+
req: IncomingMessage,
|
|
83
|
+
injected: Record<string, string>,
|
|
84
|
+
): Record<string, string> {
|
|
85
|
+
const out: Record<string, string> = {};
|
|
86
|
+
for (const name of PASS_REQ) {
|
|
87
|
+
const value = req.headers[name];
|
|
88
|
+
if (typeof value === 'string') out[name] = value;
|
|
89
|
+
}
|
|
90
|
+
return { ...out, ...injected };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function forward(
|
|
94
|
+
req: IncomingMessage,
|
|
95
|
+
target: { url: string; headers: Record<string, string> },
|
|
96
|
+
body: Uint8Array<ArrayBuffer> | null,
|
|
97
|
+
signal: AbortSignal,
|
|
98
|
+
): Promise<Response> {
|
|
99
|
+
const firstByte = setTimeout(() => {
|
|
100
|
+
log.warn({ url: target.url }, 'relay: upstream sent no response in time');
|
|
101
|
+
}, FIRST_BYTE_MS);
|
|
102
|
+
firstByte.unref?.();
|
|
103
|
+
try {
|
|
104
|
+
return await fetch(target.url, {
|
|
105
|
+
method: req.method,
|
|
106
|
+
headers: upstreamHeaders(req, target.headers),
|
|
107
|
+
...(body === null ? {} : { body }),
|
|
108
|
+
signal,
|
|
109
|
+
redirect: 'manual',
|
|
110
|
+
});
|
|
111
|
+
} finally {
|
|
112
|
+
clearTimeout(firstByte);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
interface Keepalive {
|
|
117
|
+
touch: () => void;
|
|
118
|
+
stop: () => void;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function startKeepalive(res: ServerResponse): Keepalive {
|
|
122
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
123
|
+
const arm = (): void => {
|
|
124
|
+
timer = setTimeout(() => {
|
|
125
|
+
try {
|
|
126
|
+
res.write(': keepalive\n\n');
|
|
127
|
+
} catch {
|
|
128
|
+
log.debug('relay: keepalive write failed');
|
|
129
|
+
}
|
|
130
|
+
arm();
|
|
131
|
+
}, keepaliveMs());
|
|
132
|
+
timer.unref?.();
|
|
133
|
+
};
|
|
134
|
+
arm();
|
|
135
|
+
return {
|
|
136
|
+
touch: (): void => {
|
|
137
|
+
if (timer) clearTimeout(timer);
|
|
138
|
+
arm();
|
|
139
|
+
},
|
|
140
|
+
stop: (): void => {
|
|
141
|
+
if (timer) clearTimeout(timer);
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function pumpBody(
|
|
147
|
+
res: ServerResponse,
|
|
148
|
+
body: ReadableStream<Uint8Array>,
|
|
149
|
+
touch: () => void,
|
|
150
|
+
): Promise<void> {
|
|
151
|
+
const reader = body.getReader();
|
|
152
|
+
for (;;) {
|
|
153
|
+
const { done, value } = await reader.read();
|
|
154
|
+
if (done) return;
|
|
155
|
+
touch();
|
|
156
|
+
const ok = res.write(value);
|
|
157
|
+
if (!ok)
|
|
158
|
+
await new Promise((resolve) => {
|
|
159
|
+
res.once('drain', resolve);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function pipe(res: ServerResponse, upstream: Response): Promise<void> {
|
|
165
|
+
const headers: Record<string, string> = {};
|
|
166
|
+
for (const name of PASS_RES) {
|
|
167
|
+
const value = upstream.headers.get(name);
|
|
168
|
+
if (value !== null) headers[name] = value;
|
|
169
|
+
}
|
|
170
|
+
const sse = (headers['content-type'] ?? '').includes('text/event-stream');
|
|
171
|
+
if (sse) headers['x-accel-buffering'] = 'no';
|
|
172
|
+
res.writeHead(upstream.status, headers);
|
|
173
|
+
if (upstream.body === null) {
|
|
174
|
+
res.end();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const keepalive = sse ? startKeepalive(res) : null;
|
|
178
|
+
try {
|
|
179
|
+
await pumpBody(res, upstream.body, () => keepalive?.touch());
|
|
180
|
+
} finally {
|
|
181
|
+
keepalive?.stop();
|
|
182
|
+
}
|
|
183
|
+
res.end();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const authFailed = (status: number): boolean =>
|
|
187
|
+
status === 401 || status === 403;
|
|
188
|
+
|
|
189
|
+
type Exchanged =
|
|
190
|
+
| { kind: 'response'; upstream: Response }
|
|
191
|
+
| { kind: 'missing' }
|
|
192
|
+
| { kind: 'signin' };
|
|
193
|
+
|
|
194
|
+
async function exchange(
|
|
195
|
+
req: IncomingMessage,
|
|
196
|
+
connectorId: string,
|
|
197
|
+
deps: RelayApiDeps,
|
|
198
|
+
body: Uint8Array<ArrayBuffer> | null,
|
|
199
|
+
signal: AbortSignal,
|
|
200
|
+
): Promise<Exchanged> {
|
|
201
|
+
const target = await deps.target(connectorId, false);
|
|
202
|
+
if (target.kind !== 'ok') return { kind: target.kind };
|
|
203
|
+
let upstream = await forward(req, target, body, signal);
|
|
204
|
+
if (!authFailed(upstream.status)) return { kind: 'response', upstream };
|
|
205
|
+
await upstream.body?.cancel();
|
|
206
|
+
const fresh = await deps.target(connectorId, true);
|
|
207
|
+
if (fresh.kind !== 'ok') return { kind: 'signin' };
|
|
208
|
+
upstream = await forward(req, fresh, body, signal);
|
|
209
|
+
if (!authFailed(upstream.status)) return { kind: 'response', upstream };
|
|
210
|
+
await upstream.body?.cancel();
|
|
211
|
+
return { kind: 'signin' };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function relayExchange(
|
|
215
|
+
req: IncomingMessage,
|
|
216
|
+
res: ServerResponse,
|
|
217
|
+
connectorId: string,
|
|
218
|
+
deps: RelayApiDeps,
|
|
219
|
+
): Promise<void> {
|
|
220
|
+
const control = new AbortController();
|
|
221
|
+
const bail = (): void => {
|
|
222
|
+
if (!res.writableEnded) control.abort();
|
|
223
|
+
};
|
|
224
|
+
res.once('close', bail);
|
|
225
|
+
res.socket?.once('close', bail);
|
|
226
|
+
res.once('finish', () => {
|
|
227
|
+
res.socket?.removeListener('close', bail);
|
|
228
|
+
});
|
|
229
|
+
const body = req.method === 'POST' ? await readCapped(req) : null;
|
|
230
|
+
const out = await exchange(
|
|
231
|
+
req,
|
|
232
|
+
connectorId,
|
|
233
|
+
deps,
|
|
234
|
+
body,
|
|
235
|
+
control.signal,
|
|
236
|
+
);
|
|
237
|
+
if (out.kind === 'missing') {
|
|
238
|
+
answer(res, 404, { error: 'no such connector' });
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (out.kind === 'signin') {
|
|
242
|
+
signinAnswer(res, connectorId);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (out.upstream.status >= 300 && out.upstream.status < 400) {
|
|
246
|
+
await out.upstream.body?.cancel();
|
|
247
|
+
answer(res, 502, { error: 'the connector redirected; metro does not follow' });
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
await pipe(res, out.upstream);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function dispatch(
|
|
254
|
+
req: IncomingMessage,
|
|
255
|
+
res: ServerResponse,
|
|
256
|
+
connectorId: string,
|
|
257
|
+
deps: RelayApiDeps,
|
|
258
|
+
): void {
|
|
259
|
+
const who = deps.identify(req);
|
|
260
|
+
if (who === null) {
|
|
261
|
+
answer(res, 401, { error: 'unauthorized' });
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
relayExchange(req, res, connectorId, deps)
|
|
265
|
+
.catch((err: unknown) => {
|
|
266
|
+
if (err instanceof ApiError) {
|
|
267
|
+
answer(res, err.status, { error: err.message });
|
|
268
|
+
} else {
|
|
269
|
+
log.warn({ err: errMsg(err), connector: connectorId }, 'relay: failed');
|
|
270
|
+
answer(res, 502, { error: 'metro could not reach the connector' });
|
|
271
|
+
if (!res.writableEnded) res.end();
|
|
272
|
+
}
|
|
273
|
+
closeIfBodyUnread(req, res);
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function handleRelayRequest(
|
|
278
|
+
req: IncomingMessage,
|
|
279
|
+
res: ServerResponse,
|
|
280
|
+
deps: RelayApiDeps,
|
|
281
|
+
): boolean {
|
|
282
|
+
const path = (req.url ?? '').split('?')[0] ?? '';
|
|
283
|
+
if (path !== '/relay' && !path.startsWith('/relay/')) return false;
|
|
284
|
+
const match = ID_PATH_RE.exec(path);
|
|
285
|
+
if (match?.[1] === undefined) {
|
|
286
|
+
answer(res, 404, { error: 'no such connector' });
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
if (!METHODS.has(req.method ?? '')) {
|
|
290
|
+
answer(res, 405, { error: 'method not allowed' });
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
dispatch(req, res, match[1], deps);
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { readJson, writeSecure } from '@metro-labs/core/secure-fs';
|
|
3
|
+
import { errMsg, log } from '@metro-labs/core/log';
|
|
4
|
+
import {
|
|
5
|
+
connectorUrlText,
|
|
6
|
+
ConnectorVerifyError,
|
|
7
|
+
parseConnectorUrl,
|
|
8
|
+
verifyRemoteMcp,
|
|
9
|
+
type ConnectorAuth,
|
|
10
|
+
type OAuthAuth,
|
|
11
|
+
} from './verify.js';
|
|
12
|
+
import { oauthExpired, refreshOAuth } from './oauth.js';
|
|
13
|
+
import { advertisesOAuth } from './oauth-discovery.js';
|
|
14
|
+
import {
|
|
15
|
+
ConnectorError,
|
|
16
|
+
connectorAuth,
|
|
17
|
+
connectorName,
|
|
18
|
+
readConfig,
|
|
19
|
+
stamp,
|
|
20
|
+
type ConnectorConfig,
|
|
21
|
+
} from './config.js';
|
|
22
|
+
import {
|
|
23
|
+
bearerHeaders,
|
|
24
|
+
fixedTarget,
|
|
25
|
+
unrefreshedTarget,
|
|
26
|
+
type RelayTarget,
|
|
27
|
+
} from './relay-target.js';
|
|
28
|
+
import {
|
|
29
|
+
connectorFromRow,
|
|
30
|
+
UNVERIFIED,
|
|
31
|
+
type Connector,
|
|
32
|
+
type ConnectorCheck,
|
|
33
|
+
type ConnectorInput,
|
|
34
|
+
type DeletedConnector,
|
|
35
|
+
} from './model.js';
|
|
36
|
+
import { agentsDir } from '../agents/files.js';
|
|
37
|
+
import { assertLocalOwner, LOCAL_PROJECT_ID } from '../agents/file-admin.js';
|
|
38
|
+
import { newId } from '@metro-labs/core/ids';
|
|
39
|
+
import type { LoadedConnector } from '../stations/materialize.js';
|
|
40
|
+
|
|
41
|
+
export interface LocalConnectorRow {
|
|
42
|
+
id: string;
|
|
43
|
+
name: string;
|
|
44
|
+
url: string;
|
|
45
|
+
transport: 'http';
|
|
46
|
+
config: ConnectorConfig;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const FILE = 'connectors.json';
|
|
50
|
+
const filePath = (dir: string): string => join(dir, FILE);
|
|
51
|
+
|
|
52
|
+
const isRow = (v: unknown): v is { id: string; name: string; url: string; config?: unknown } =>
|
|
53
|
+
typeof v === 'object' &&
|
|
54
|
+
v !== null &&
|
|
55
|
+
typeof (v as { id?: unknown }).id === 'string' &&
|
|
56
|
+
typeof (v as { name?: unknown }).name === 'string' &&
|
|
57
|
+
typeof (v as { url?: unknown }).url === 'string';
|
|
58
|
+
|
|
59
|
+
const asList = (value: unknown): unknown[] =>
|
|
60
|
+
Array.isArray(value) ? value.map((item: unknown) => item) : [];
|
|
61
|
+
|
|
62
|
+
export function readLocalConnectors(dir = agentsDir()): LocalConnectorRow[] {
|
|
63
|
+
const raw = readJson<{ connectors?: unknown }>(filePath(dir), {}, {
|
|
64
|
+
warn: 'connectors.json: malformed, ignoring',
|
|
65
|
+
});
|
|
66
|
+
return asList(raw.connectors).filter(isRow).map((r) => ({
|
|
67
|
+
id: r.id,
|
|
68
|
+
name: r.name,
|
|
69
|
+
url: r.url,
|
|
70
|
+
transport: 'http' as const,
|
|
71
|
+
config: readConfig(r.config),
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function writeRows(dir: string, rows: LocalConnectorRow[]): void {
|
|
76
|
+
writeSecure(filePath(dir), `${JSON.stringify({ version: 1, connectors: rows }, null, 2)}\n`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const missing = (): ConnectorError => new ConnectorError('no such connector', 404);
|
|
80
|
+
|
|
81
|
+
function ownedRows(subject: string, project: string, dir: string): LocalConnectorRow[] {
|
|
82
|
+
assertLocalOwner(subject, dir);
|
|
83
|
+
if (project !== LOCAL_PROJECT_ID) throw new ConnectorError('no such project', 404);
|
|
84
|
+
return readLocalConnectors(dir);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function rowOrThrow(subject: string, id: string, dir: string): LocalConnectorRow {
|
|
88
|
+
assertLocalOwner(subject, dir);
|
|
89
|
+
const row = readLocalConnectors(dir).find((r) => r.id === id);
|
|
90
|
+
if (row === undefined) throw missing();
|
|
91
|
+
return row;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function replace(dir: string, row: LocalConnectorRow): Connector {
|
|
95
|
+
writeRows(dir, readLocalConnectors(dir).map((r) => (r.id === row.id ? row : r)));
|
|
96
|
+
return connectorFromRow(row);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function oauthCapable(url: URL, auth: ConnectorAuth): Promise<boolean> {
|
|
100
|
+
if (auth.kind === 'oauth') return true;
|
|
101
|
+
if (auth.kind === 'header') return false;
|
|
102
|
+
return advertisesOAuth(url);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function localImportConnectors(rows: LoadedConnector[], dir = agentsDir()): number {
|
|
106
|
+
const current = readLocalConnectors(dir);
|
|
107
|
+
for (const row of rows) {
|
|
108
|
+
const other = current.find((r) => r.id !== row.id && r.name === row.name);
|
|
109
|
+
if (other !== undefined)
|
|
110
|
+
throw new ConnectorError(`a connector named '${row.name}' already exists on this daemon; rename it first`, 409);
|
|
111
|
+
}
|
|
112
|
+
const imported: LocalConnectorRow[] = rows.map((row) => ({
|
|
113
|
+
id: row.id,
|
|
114
|
+
name: row.name,
|
|
115
|
+
url: row.url,
|
|
116
|
+
transport: 'http',
|
|
117
|
+
config: readConfig(row.config),
|
|
118
|
+
}));
|
|
119
|
+
const ids = new Set(imported.map((r) => r.id));
|
|
120
|
+
writeRows(dir, [...current.filter((r) => !ids.has(r.id)), ...imported]);
|
|
121
|
+
return imported.length;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function localListConnectors(
|
|
125
|
+
subject: string,
|
|
126
|
+
project: string,
|
|
127
|
+
dir = agentsDir(),
|
|
128
|
+
): Promise<Connector[]> {
|
|
129
|
+
return Promise.resolve(ownedRows(subject, project, dir).map(connectorFromRow));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function localGetConnector(subject: string, id: string, dir = agentsDir()): Promise<Connector> {
|
|
133
|
+
return Promise.resolve(connectorFromRow(rowOrThrow(subject, id, dir)));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function assertNameFree(name: string, exceptId: string | null, dir: string): void {
|
|
137
|
+
if (readLocalConnectors(dir).some((r) => r.id !== exceptId && r.name === name))
|
|
138
|
+
throw new ConnectorError(`a connector named '${name}' already exists on this daemon`, 409);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function insert(dir: string, name: string, url: URL, config: ConnectorConfig): Connector {
|
|
142
|
+
assertNameFree(name, null, dir);
|
|
143
|
+
const row: LocalConnectorRow = { id: newId(), name, url: connectorUrlText(url), transport: 'http', config };
|
|
144
|
+
writeRows(dir, [...readLocalConnectors(dir), row]);
|
|
145
|
+
return connectorFromRow(row);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function localCreateConnector(
|
|
149
|
+
subject: string,
|
|
150
|
+
project: string,
|
|
151
|
+
input: ConnectorInput,
|
|
152
|
+
dir = agentsDir(),
|
|
153
|
+
): Promise<Connector> {
|
|
154
|
+
ownedRows(subject, project, dir);
|
|
155
|
+
const name = connectorName(input.name);
|
|
156
|
+
const url = parseConnectorUrl(input.url);
|
|
157
|
+
const auth = connectorAuth(input.header, input.value);
|
|
158
|
+
const verified = stamp(await verifyRemoteMcp(url, auth));
|
|
159
|
+
return insert(dir, name, url, {
|
|
160
|
+
auth,
|
|
161
|
+
createdAt: new Date().toISOString(),
|
|
162
|
+
verified,
|
|
163
|
+
oauth: await oauthCapable(url, auth),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function localCreatePendingConnector(
|
|
168
|
+
subject: string,
|
|
169
|
+
project: string,
|
|
170
|
+
input: { name: unknown; url: unknown },
|
|
171
|
+
dir = agentsDir(),
|
|
172
|
+
): Promise<Connector> {
|
|
173
|
+
ownedRows(subject, project, dir);
|
|
174
|
+
const name = connectorName(input.name);
|
|
175
|
+
const url = parseConnectorUrl(input.url);
|
|
176
|
+
return Promise.resolve(
|
|
177
|
+
insert(dir, name, url, { auth: { kind: 'none' }, createdAt: new Date().toISOString(), verified: UNVERIFIED, oauth: true }),
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function localReconnectConnector(
|
|
182
|
+
subject: string,
|
|
183
|
+
id: string,
|
|
184
|
+
auth: OAuthAuth,
|
|
185
|
+
dir = agentsDir(),
|
|
186
|
+
): Promise<Connector> {
|
|
187
|
+
const row = rowOrThrow(subject, id, dir);
|
|
188
|
+
const verified = stamp(await verifyRemoteMcp(parseConnectorUrl(row.url), auth));
|
|
189
|
+
return replace(dir, { ...row, config: { ...row.config, auth, verified, oauth: true } });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function localDisconnectConnector(subject: string, id: string, dir = agentsDir()): Promise<Connector> {
|
|
193
|
+
const row = rowOrThrow(subject, id, dir);
|
|
194
|
+
if (row.config.auth.kind !== 'oauth') throw new ConnectorError('that connector is not signed in', 400);
|
|
195
|
+
return Promise.resolve(replace(dir, { ...row, config: { ...row.config, auth: { kind: 'none' }, oauth: true } }));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function freshAuth(auth: ConnectorAuth, resource: string): Promise<ConnectorAuth> {
|
|
199
|
+
if (auth.kind !== 'oauth' || !oauthExpired(auth)) return auth;
|
|
200
|
+
return refreshOAuth(auth, resource);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export async function localVerifyConnector(subject: string, id: string, dir = agentsDir()): Promise<ConnectorCheck> {
|
|
204
|
+
const row = rowOrThrow(subject, id, dir);
|
|
205
|
+
try {
|
|
206
|
+
const url = parseConnectorUrl(row.url);
|
|
207
|
+
const auth = await freshAuth(row.config.auth, url.toString());
|
|
208
|
+
const verified = stamp(await verifyRemoteMcp(url, auth));
|
|
209
|
+
const oauth = row.config.oauth || (await oauthCapable(url, auth));
|
|
210
|
+
replace(dir, { ...row, config: { ...row.config, auth, verified, oauth } });
|
|
211
|
+
return { id: row.id, name: row.name, ok: true, verified };
|
|
212
|
+
} catch (err) {
|
|
213
|
+
if (!(err instanceof ConnectorVerifyError)) throw err;
|
|
214
|
+
return { id: row.id, name: row.name, ok: false, reason: err.message };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export async function localRenameConnector(subject: string, id: string, raw: string, dir = agentsDir()): Promise<Connector> {
|
|
219
|
+
const name = connectorName(raw);
|
|
220
|
+
const row = rowOrThrow(subject, id, dir);
|
|
221
|
+
assertNameFree(name, id, dir);
|
|
222
|
+
return Promise.resolve(replace(dir, { ...row, name }));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export async function localDeleteConnector(subject: string, id: string, dir = agentsDir()): Promise<DeletedConnector> {
|
|
226
|
+
const row = rowOrThrow(subject, id, dir);
|
|
227
|
+
writeRows(dir, readLocalConnectors(dir).filter((r) => r.id !== id));
|
|
228
|
+
return Promise.resolve({ id: row.id, name: row.name });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const byIds = (ids: string[], dir: string): LocalConnectorRow[] =>
|
|
232
|
+
readLocalConnectors(dir).filter((r) => ids.includes(r.id));
|
|
233
|
+
|
|
234
|
+
export async function localConnectorNamesByIds(ids: string[], dir = agentsDir()): Promise<{ id: string; name: string }[]> {
|
|
235
|
+
return Promise.resolve(byIds(ids, dir).map((r) => ({ id: r.id, name: r.name })));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const inflight = new Map<string, Promise<OAuthAuth>>();
|
|
239
|
+
|
|
240
|
+
function refreshOnce(row: LocalConnectorRow, auth: OAuthAuth, dir: string): Promise<OAuthAuth> {
|
|
241
|
+
const running = inflight.get(row.id);
|
|
242
|
+
if (running !== undefined) return running;
|
|
243
|
+
const job = refreshOAuth(auth, parseConnectorUrl(row.url).toString())
|
|
244
|
+
.then((fresh) => {
|
|
245
|
+
replace(dir, { ...row, config: { ...row.config, auth: fresh } });
|
|
246
|
+
return fresh;
|
|
247
|
+
})
|
|
248
|
+
.finally(() => {
|
|
249
|
+
inflight.delete(row.id);
|
|
250
|
+
});
|
|
251
|
+
inflight.set(row.id, job);
|
|
252
|
+
return job;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function oauthTarget(row: LocalConnectorRow, auth: OAuthAuth, force: boolean, dir: string): Promise<RelayTarget> {
|
|
256
|
+
if (!force && !oauthExpired(auth)) return { kind: 'ok', url: row.url, headers: bearerHeaders(auth.accessToken) };
|
|
257
|
+
try {
|
|
258
|
+
const fresh = await refreshOnce(row, auth, dir);
|
|
259
|
+
return { kind: 'ok', url: row.url, headers: bearerHeaders(fresh.accessToken) };
|
|
260
|
+
} catch (err) {
|
|
261
|
+
log.warn({ id: row.id, err: errMsg(err) }, 'local relay: token refresh failed');
|
|
262
|
+
return unrefreshedTarget(row.url, auth, force);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export async function localRelayTarget(
|
|
267
|
+
connectorId: string,
|
|
268
|
+
force: boolean,
|
|
269
|
+
dir = agentsDir(),
|
|
270
|
+
): Promise<RelayTarget> {
|
|
271
|
+
const row = readLocalConnectors(dir).find((r) => r.id === connectorId);
|
|
272
|
+
if (row === undefined) return { kind: 'missing' };
|
|
273
|
+
parseConnectorUrl(row.url);
|
|
274
|
+
const auth = row.config.auth;
|
|
275
|
+
if (auth.kind === 'oauth') return oauthTarget(row, auth, force, dir);
|
|
276
|
+
return fixedTarget(row.url, auth, force);
|
|
277
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { isRecord } from '@metro-labs/core/is-record';
|
|
2
|
+
|
|
3
|
+
export const TOOL_KINDS = ['read', 'write'] as const;
|
|
4
|
+
|
|
5
|
+
export type ToolKind = (typeof TOOL_KINDS)[number];
|
|
6
|
+
|
|
7
|
+
export interface ToolInfo {
|
|
8
|
+
name: string;
|
|
9
|
+
title: string;
|
|
10
|
+
description: string;
|
|
11
|
+
kind: ToolKind;
|
|
12
|
+
annotated: boolean;
|
|
13
|
+
destructive: boolean;
|
|
14
|
+
idempotent: boolean;
|
|
15
|
+
openWorld: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const MAX_TOOLS = 500;
|
|
19
|
+
const MAX_DESCRIPTION = 400;
|
|
20
|
+
|
|
21
|
+
const str = (value: unknown, cap: number): string =>
|
|
22
|
+
typeof value === 'string' ? value.slice(0, cap) : '';
|
|
23
|
+
|
|
24
|
+
const flag = (value: unknown, fallback: boolean): boolean =>
|
|
25
|
+
typeof value === 'boolean' ? value : fallback;
|
|
26
|
+
|
|
27
|
+
export function classifyTool(annotations: unknown): ToolKind {
|
|
28
|
+
return isRecord(annotations) && annotations.readOnlyHint === true
|
|
29
|
+
? 'read'
|
|
30
|
+
: 'write';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isDestructive(annotations: unknown): boolean {
|
|
34
|
+
if (!isRecord(annotations)) return true;
|
|
35
|
+
if (annotations.readOnlyHint === true) return false;
|
|
36
|
+
return annotations.destructiveHint !== false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function toToolInfo(raw: unknown): ToolInfo | null {
|
|
40
|
+
if (!isRecord(raw)) return null;
|
|
41
|
+
const name = str(raw.name, 200);
|
|
42
|
+
if (name === '') return null;
|
|
43
|
+
const annotations = isRecord(raw.annotations) ? raw.annotations : undefined;
|
|
44
|
+
return {
|
|
45
|
+
name,
|
|
46
|
+
title: str(annotations?.title ?? raw.title, 200),
|
|
47
|
+
description: str(raw.description, MAX_DESCRIPTION),
|
|
48
|
+
kind: classifyTool(raw.annotations),
|
|
49
|
+
annotated: annotations !== undefined,
|
|
50
|
+
destructive: isDestructive(raw.annotations),
|
|
51
|
+
idempotent: flag(annotations?.idempotentHint, false),
|
|
52
|
+
openWorld: flag(annotations?.openWorldHint, true),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function toToolList(raw: unknown): ToolInfo[] {
|
|
57
|
+
if (!Array.isArray(raw)) return [];
|
|
58
|
+
const out: ToolInfo[] = [];
|
|
59
|
+
for (const entry of raw) {
|
|
60
|
+
if (out.length >= MAX_TOOLS) break;
|
|
61
|
+
const tool = toToolInfo(entry);
|
|
62
|
+
if (tool !== null) out.push(tool);
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function toStoredTool(raw: unknown): ToolInfo | null {
|
|
68
|
+
if (!isRecord(raw)) return null;
|
|
69
|
+
const name = str(raw.name, 200);
|
|
70
|
+
if (name === '') return null;
|
|
71
|
+
return {
|
|
72
|
+
name,
|
|
73
|
+
title: str(raw.title, 200),
|
|
74
|
+
description: str(raw.description, MAX_DESCRIPTION),
|
|
75
|
+
kind: raw.kind === 'read' ? 'read' : 'write',
|
|
76
|
+
annotated: raw.annotated === true,
|
|
77
|
+
destructive: raw.destructive !== false,
|
|
78
|
+
idempotent: raw.idempotent === true,
|
|
79
|
+
openWorld: raw.openWorld !== false,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function readStoredTools(raw: unknown): ToolInfo[] {
|
|
84
|
+
if (!Array.isArray(raw)) return [];
|
|
85
|
+
const out: ToolInfo[] = [];
|
|
86
|
+
for (const entry of raw) {
|
|
87
|
+
if (out.length >= MAX_TOOLS) break;
|
|
88
|
+
const tool = toStoredTool(entry);
|
|
89
|
+
if (tool !== null) out.push(tool);
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function countByKind(tools: ToolInfo[]): Record<ToolKind, number> {
|
|
95
|
+
const counts: Record<ToolKind, number> = { read: 0, write: 0 };
|
|
96
|
+
for (const tool of tools) counts[tool.kind] += 1;
|
|
97
|
+
return counts;
|
|
98
|
+
}
|