@stage-labs/metro 0.1.0-beta.7 → 0.1.0-beta.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.js +14 -0
- package/dist/bedrock-eventstream.js +120 -0
- package/dist/bedrock-proxy.js +260 -0
- package/dist/bedrock-upstream.js +72 -0
- package/dist/bedrock.js +37 -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 +51 -0
- package/dist/plugin.js +80 -0
- package/dist/provider-flags.js +9 -0
- package/dist/runtime-install.js +84 -0
- package/dist/runtime.js +70 -0
- package/dist/serve.js +147 -0
- package/dist/service.js +224 -0
- package/dist/tail.js +119 -0
- package/dist/update.js +45 -0
- package/dist/version.js +97 -0
- package/package.json +14 -25
- package/runtime/node_modules/@metro-labs/discord-bot/package.json +28 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +118 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +388 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/api-base.ts +1 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/attachments.ts +29 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +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/mcp/package.json +64 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/addressed.ts +25 -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 +377 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/media-note.ts +85 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/pending.ts +75 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/relay.ts +129 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/webhook-note.ts +51 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +311 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +302 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-error.ts +8 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +95 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-grant.ts +99 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-owner.ts +32 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +128 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +280 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +204 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/bundle-api.ts +111 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/claude-api.ts +97 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/claude-files.ts +329 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connect-hint.ts +39 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +307 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-json.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth-routes.ts +157 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +127 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-tools.ts +98 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-url.ts +70 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +330 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/control-api.ts +64 -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 +120 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +390 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity-key.ts +33 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity-registry.ts +13 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity-routes.ts +54 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +7 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/is-record.ts +2 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-cli-api.ts +63 -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-owner.ts +14 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/log.ts +42 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/machine-api.ts +61 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/mode-api.ts +32 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/model-api.ts +169 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-client.ts +159 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-discovery.ts +117 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-pending.ts +65 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +109 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/protocol.ts +226 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/relay.ts +295 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/return-to.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-deps.ts +82 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/secure-fs.ts +57 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/server-types.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/servers-api.ts +80 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-api.ts +29 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +66 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/signed-identity.ts +36 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor-io.ts +97 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor.ts +320 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/terminal-api.ts +71 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/terminal-tickets.ts +40 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/terminal-ws.ts +114 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-call.ts +22 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-error.ts +53 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +352 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/update-api.ts +116 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +294 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-store.ts +181 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/vault-api.ts +78 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/vault-types.ts +12 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/version.ts +2 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +19 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/address.ts +6 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +43 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-map.ts +84 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-scope.ts +64 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/client.ts +32 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-config.ts +144 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-relay.ts +39 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +69 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/file-admin.ts +329 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/file-source.ts +155 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/ids.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/key-map.ts +45 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/local-connectors.ts +277 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +253 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +33 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/servers.ts +83 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/stations.ts +12 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/vault.ts +109 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/bedrock.ts +252 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/codex-auth.ts +231 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/codex-device.ts +91 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/codex-stream.ts +288 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/codex-translate.ts +203 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/codex.ts +211 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/eventstream.ts +123 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/forward.ts +169 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/gateway.ts +193 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/model-config.ts +180 -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 +216 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/channel-owner.ts +29 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/ctx.ts +62 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/event-store.ts +91 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/frame-scope.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/group-tools.ts +129 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/index.ts +191 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/member-tools.ts +72 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/permission-relay.ts +66 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/raw-get-stream.ts +207 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/request-identity.ts +47 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-registry.ts +172 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-route.ts +43 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/session.ts +215 -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 +215 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +400 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/upload-tool.ts +66 -0
- package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +237 -0
- package/runtime/node_modules/@metro-labs/mcp/src/server.ts +2 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +60 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-inline.ts +100 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-interactive.ts +161 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-resolve.ts +200 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-xmtp.ts +111 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +184 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attachments.ts +196 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +82 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/messaging-normalize.ts +88 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/registry.ts +35 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/station-runtime.ts +64 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/types.ts +107 -0
- package/runtime/node_modules/@metro-labs/telegram/package.json +26 -0
- package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +80 -0
- package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +254 -0
- package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +87 -0
- package/runtime/node_modules/@metro-labs/telegram/src/client.ts +77 -0
- package/runtime/node_modules/@metro-labs/telegram/src/format.ts +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/stations.json +36 -0
- package/runtime/trains/.keep +0 -0
- package/LICENSE +0 -21
- package/README.md +0 -193
- 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 -159
- package/dist/cli/skill.js +0 -62
- package/dist/cli/util.js +0 -72
- package/dist/codex-rc.js +0 -236
- package/dist/dispatcher.js +0 -95
- package/dist/history.js +0 -84
- package/dist/ipc.js +0 -72
- package/dist/log.js +0 -15
- package/dist/paths.js +0 -68
- package/dist/stations/discord.js +0 -200
- package/dist/stations/index.js +0 -73
- package/dist/stations/telegram-md.js +0 -34
- package/dist/stations/telegram-upload.js +0 -113
- package/dist/stations/telegram.js +0 -213
- package/docs/agents.md +0 -185
- package/docs/uri-scheme.md +0 -71
- package/skills/metro/SKILL.md +0 -241
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export interface TrainErrorInfo {
|
|
2
|
+
code: string;
|
|
3
|
+
message: string;
|
|
4
|
+
retryable?: boolean;
|
|
5
|
+
retryAfterMs?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class TrainError extends Error {
|
|
9
|
+
readonly code: string;
|
|
10
|
+
readonly retryable?: boolean;
|
|
11
|
+
readonly retryAfterMs?: number;
|
|
12
|
+
constructor(
|
|
13
|
+
code: string,
|
|
14
|
+
message: string,
|
|
15
|
+
opts?: { retryable?: boolean; retryAfterMs?: number },
|
|
16
|
+
) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = 'TrainError';
|
|
19
|
+
this.code = code;
|
|
20
|
+
this.retryable = opts?.retryable;
|
|
21
|
+
this.retryAfterMs = opts?.retryAfterMs;
|
|
22
|
+
}
|
|
23
|
+
toErrorInfo(): TrainErrorInfo {
|
|
24
|
+
return {
|
|
25
|
+
code: this.code,
|
|
26
|
+
message: this.message,
|
|
27
|
+
...(this.retryable !== undefined ? { retryable: this.retryable } : {}),
|
|
28
|
+
...(this.retryAfterMs !== undefined
|
|
29
|
+
? { retryAfterMs: this.retryAfterMs }
|
|
30
|
+
: {}),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function serializeTrainError(err: unknown): {
|
|
36
|
+
error: string;
|
|
37
|
+
errorInfo?: TrainErrorInfo;
|
|
38
|
+
} {
|
|
39
|
+
if (err instanceof TrainError)
|
|
40
|
+
return { error: err.message, errorInfo: err.toErrorInfo() };
|
|
41
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function coerceErrorInfo(v: unknown): TrainErrorInfo | undefined {
|
|
45
|
+
if (!v || typeof v !== 'object') return undefined;
|
|
46
|
+
const o = v as Record<string, unknown>;
|
|
47
|
+
if (typeof o.code !== 'string' || typeof o.message !== 'string')
|
|
48
|
+
return undefined;
|
|
49
|
+
const info: TrainErrorInfo = { code: o.code, message: o.message };
|
|
50
|
+
if (typeof o.retryable === 'boolean') info.retryable = o.retryable;
|
|
51
|
+
if (typeof o.retryAfterMs === 'number') info.retryAfterMs = o.retryAfterMs;
|
|
52
|
+
return info;
|
|
53
|
+
}
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { execFile, spawn, type ChildProcess } from 'node:child_process';
|
|
2
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
3
|
+
import { Resolver } from 'node:dns/promises';
|
|
4
|
+
import { request as httpsRequest } from 'node:https';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { errMsg, log } from './log.js';
|
|
8
|
+
import { readJson } from './secure-fs.js';
|
|
9
|
+
import { localOwner } from '../db/file-admin.js';
|
|
10
|
+
|
|
11
|
+
const RESTART_DELAY_MS = 2_000;
|
|
12
|
+
const RESTART_DELAY_MAX_MS = 30_000;
|
|
13
|
+
const TAKEN_RE = /listener already exists/i;
|
|
14
|
+
const FUNNEL_ON_RE = /\(Funnel on\)/i;
|
|
15
|
+
|
|
16
|
+
export const tunnelWanted = (): boolean => process.env.METRO_TUNNEL?.trim() === 'tailscale';
|
|
17
|
+
|
|
18
|
+
const FUNNEL_URL_RE = /https:\/\/[a-z0-9-]+(?:\.[a-z0-9-]+)+\.ts\.net(?::\d{2,5})?(?=[\s/]|$)/i;
|
|
19
|
+
const SILENT_MS = 20_000;
|
|
20
|
+
|
|
21
|
+
export const funnelUrlIn = (text: string): string | null =>
|
|
22
|
+
FUNNEL_URL_RE.exec(text)?.[0]?.toLowerCase() ?? null;
|
|
23
|
+
|
|
24
|
+
export interface Adopted {
|
|
25
|
+
url: string | null;
|
|
26
|
+
hint: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface TunnelDriver {
|
|
30
|
+
name: string;
|
|
31
|
+
command: string;
|
|
32
|
+
args: string[];
|
|
33
|
+
urlIn: (text: string) => string | null;
|
|
34
|
+
waitsForDns: boolean;
|
|
35
|
+
adopt?: () => Promise<Adopted>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const runText = (command: string, args: string[]): Promise<string> =>
|
|
39
|
+
new Promise((resolve) => {
|
|
40
|
+
execFile(command, args, { encoding: 'utf8', timeout: 10_000 }, (_err, stdout, stderr) => {
|
|
41
|
+
resolve(`${stdout}\n${stderr}`);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export type Probe = (url: string, owner: string | null) => Promise<boolean>;
|
|
46
|
+
|
|
47
|
+
const PROBE_MS = 5_000;
|
|
48
|
+
|
|
49
|
+
async function publicAddresses(host: string): Promise<string[]> {
|
|
50
|
+
const found = await Promise.all(
|
|
51
|
+
PUBLIC_RESOLVERS.map(async (server) => {
|
|
52
|
+
const resolver = new Resolver();
|
|
53
|
+
resolver.setServers([server]);
|
|
54
|
+
const [v4, v6] = await Promise.all([resolver.resolve4(host).catch(() => []), resolver.resolve6(host).catch(() => [])]);
|
|
55
|
+
return [...v4, ...v6];
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
return [...new Set(found.flat())];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function modeThrough(ip: string, host: string): Promise<{ mode?: unknown; owner?: unknown } | null> {
|
|
62
|
+
return new Promise((resolve) => {
|
|
63
|
+
const req = httpsRequest(
|
|
64
|
+
{ host: ip, servername: host, port: 443, path: '/api/mode', method: 'GET', headers: { host }, timeout: PROBE_MS },
|
|
65
|
+
(res) => {
|
|
66
|
+
const chunks: Buffer[] = [];
|
|
67
|
+
res.on('data', (c: Buffer) => chunks.push(c));
|
|
68
|
+
res.on('end', () => {
|
|
69
|
+
try {
|
|
70
|
+
resolve(res.statusCode === 200 ? (JSON.parse(Buffer.concat(chunks).toString('utf8')) as { mode?: unknown }) : null);
|
|
71
|
+
} catch {
|
|
72
|
+
resolve(null);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
req.on('timeout', () => {
|
|
78
|
+
req.destroy();
|
|
79
|
+
});
|
|
80
|
+
req.on('error', () => {
|
|
81
|
+
resolve(null);
|
|
82
|
+
});
|
|
83
|
+
req.end();
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function daemonAnswersAt(url: string, owner: string | null): Promise<boolean> {
|
|
88
|
+
const host = new URL(url).host;
|
|
89
|
+
const addresses = await publicAddresses(host);
|
|
90
|
+
if (addresses.length === 0) return false;
|
|
91
|
+
const body = await modeThrough(addresses[0] ?? '', host);
|
|
92
|
+
return body !== null && body.mode === 'local' && (owner === null || body.owner === owner);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function nodeNameIn(statusJson: string): string | null {
|
|
96
|
+
try {
|
|
97
|
+
const parsed: unknown = JSON.parse(statusJson);
|
|
98
|
+
const self = typeof parsed === 'object' && parsed !== null ? (parsed as { Self?: { DNSName?: unknown } }).Self : undefined;
|
|
99
|
+
const name = typeof self?.DNSName === 'string' ? self.DNSName.replace(/\.$/, '').toLowerCase() : '';
|
|
100
|
+
return name === '' ? null : name;
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function adoptFunnel(bin: string, port: number, probe: Probe, owner: () => string | null): Promise<Adopted> {
|
|
107
|
+
const name = nodeNameIn(await runText(bin, ['status', '--json']));
|
|
108
|
+
if (name !== null) {
|
|
109
|
+
const url = `https://${name}`;
|
|
110
|
+
if (await probe(url, owner())) return { url, hint: `a Funnel already publishes this daemon at ${url}; using it` };
|
|
111
|
+
}
|
|
112
|
+
return funnelAlreadyServing(await runText(bin, ['funnel', 'status']), port);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function funnelAlreadyServing(status: string, port: number): Adopted {
|
|
116
|
+
const lines = status.split('\n');
|
|
117
|
+
const target = `http://127.0.0.1:${String(port)}`;
|
|
118
|
+
for (const [i, line] of lines.entries()) {
|
|
119
|
+
if (line.trimStart().startsWith('#')) continue;
|
|
120
|
+
const url = funnelUrlIn(line);
|
|
121
|
+
if (url === null) continue;
|
|
122
|
+
const body = lines.slice(i + 1, i + 6).join('\n');
|
|
123
|
+
if (!body.includes(target)) continue;
|
|
124
|
+
if (FUNNEL_ON_RE.test(line)) return { url, hint: `a Funnel already publishes this daemon at ${url}; using it` };
|
|
125
|
+
return {
|
|
126
|
+
url: null,
|
|
127
|
+
hint: `${url} is a tailnet-only serve config on this node, not a Funnel; run: tailscale serve reset`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return { url: null, hint: 'port 443 is held by another serve config on this node; run: tailscale serve reset' };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function tailscaleBin(): string {
|
|
134
|
+
const configured = process.env.METRO_TAILSCALE_BIN?.trim() ?? '';
|
|
135
|
+
return configured === '' ? 'tailscale' : configured;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const funnelDriver = (
|
|
139
|
+
port: number,
|
|
140
|
+
bin = tailscaleBin(),
|
|
141
|
+
probe: Probe = daemonAnswersAt,
|
|
142
|
+
owner: () => string | null = localOwner,
|
|
143
|
+
): TunnelDriver => ({
|
|
144
|
+
name: 'tailscale funnel',
|
|
145
|
+
command: bin,
|
|
146
|
+
args: ['funnel', String(port)],
|
|
147
|
+
urlIn: funnelUrlIn,
|
|
148
|
+
waitsForDns: true,
|
|
149
|
+
adopt: () => adoptFunnel(bin, port, probe, owner),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
let liveUrl: string | null = null;
|
|
153
|
+
|
|
154
|
+
export const currentTunnelUrl = (): string | null => liveUrl;
|
|
155
|
+
|
|
156
|
+
export type Resolves = (host: string) => Promise<boolean>;
|
|
157
|
+
|
|
158
|
+
const PUBLIC_RESOLVERS = ['1.1.1.1', '8.8.8.8'];
|
|
159
|
+
const RESOLVE_EVERY_MS = 3_000;
|
|
160
|
+
const RESOLVE_GIVE_UP_MS = 180_000;
|
|
161
|
+
|
|
162
|
+
async function resolvesAt(server: string, host: string): Promise<boolean> {
|
|
163
|
+
const resolver = new Resolver();
|
|
164
|
+
resolver.setServers([server]);
|
|
165
|
+
const answers = await Promise.all([
|
|
166
|
+
resolver.resolve4(host).catch(() => []),
|
|
167
|
+
resolver.resolve6(host).catch(() => []),
|
|
168
|
+
]);
|
|
169
|
+
return answers.some((a) => a.length > 0);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function resolvesPublicly(host: string): Promise<boolean> {
|
|
173
|
+
const found = await Promise.all(PUBLIC_RESOLVERS.map((server) => resolvesAt(server, host)));
|
|
174
|
+
return found.some(Boolean);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function untilResolvable(host: string, resolves: Resolves): Promise<boolean> {
|
|
178
|
+
const deadline = Date.now() + RESOLVE_GIVE_UP_MS;
|
|
179
|
+
while (Date.now() < deadline) {
|
|
180
|
+
if (await resolves(host)) return true;
|
|
181
|
+
await new Promise((r) => setTimeout(r, RESOLVE_EVERY_MS));
|
|
182
|
+
}
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface Endpoint {
|
|
187
|
+
id: string;
|
|
188
|
+
webhookId?: string;
|
|
189
|
+
label: string;
|
|
190
|
+
secret?: string;
|
|
191
|
+
createdAt: string;
|
|
192
|
+
}
|
|
193
|
+
interface AccountRecord {
|
|
194
|
+
id?: unknown;
|
|
195
|
+
webhookId?: unknown;
|
|
196
|
+
label?: unknown;
|
|
197
|
+
secret?: unknown;
|
|
198
|
+
createdAt?: unknown;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export const webhookPort = (): number =>
|
|
202
|
+
Number(process.env.METRO_WEBHOOK_PORT) || 8420;
|
|
203
|
+
|
|
204
|
+
const accountsFile = (): string =>
|
|
205
|
+
process.env.WEBHOOK_ACCOUNTS_FILE ??
|
|
206
|
+
join(homedir(), '.metro', 'webhook-accounts.json');
|
|
207
|
+
|
|
208
|
+
const str = (v: unknown): string | undefined =>
|
|
209
|
+
typeof v === 'string' && v !== '' ? v : undefined;
|
|
210
|
+
|
|
211
|
+
function toEndpoint(raw: AccountRecord): Endpoint | null {
|
|
212
|
+
const id = str(raw.id);
|
|
213
|
+
if (id === undefined) return null;
|
|
214
|
+
return {
|
|
215
|
+
id,
|
|
216
|
+
webhookId: str(raw.webhookId),
|
|
217
|
+
label: str(raw.label) ?? id,
|
|
218
|
+
secret: str(raw.secret),
|
|
219
|
+
createdAt: str(raw.createdAt) ?? '',
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function listEndpoints(): Endpoint[] {
|
|
224
|
+
const raw = readJson<AccountRecord[]>(accountsFile(), [], {
|
|
225
|
+
warn: 'webhook-accounts.json: malformed, ignoring',
|
|
226
|
+
});
|
|
227
|
+
if (!Array.isArray(raw)) return [];
|
|
228
|
+
return raw.map(toEndpoint).filter((e): e is Endpoint => e !== null);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export const findEndpointByWebhookId = (
|
|
232
|
+
webhookId: string,
|
|
233
|
+
): Endpoint | undefined =>
|
|
234
|
+
listEndpoints().find((e) => e.webhookId === webhookId);
|
|
235
|
+
|
|
236
|
+
export function tokenMatches(secret: string, given: string): boolean {
|
|
237
|
+
const want = Buffer.from(secret);
|
|
238
|
+
const got = Buffer.from(given);
|
|
239
|
+
return want.length === got.length && timingSafeEqual(want, got);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export class Tunnel {
|
|
243
|
+
private child: ChildProcess | null = null;
|
|
244
|
+
private closed = false;
|
|
245
|
+
private output: string[] = [];
|
|
246
|
+
private restartDelay = RESTART_DELAY_MS;
|
|
247
|
+
|
|
248
|
+
constructor(
|
|
249
|
+
private driver: TunnelDriver,
|
|
250
|
+
private onUrl: (url: string) => void = () => undefined,
|
|
251
|
+
private resolves: Resolves = resolvesPublicly,
|
|
252
|
+
) {}
|
|
253
|
+
|
|
254
|
+
private noticeUrl(text: string): void {
|
|
255
|
+
const url = this.driver.urlIn(text);
|
|
256
|
+
if (url === null || url === liveUrl) return;
|
|
257
|
+
liveUrl = url;
|
|
258
|
+
this.restartDelay = RESTART_DELAY_MS;
|
|
259
|
+
if (!this.driver.waitsForDns) {
|
|
260
|
+
log.info({ url }, `${this.driver.name} up`);
|
|
261
|
+
this.onUrl(url);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
log.info({ url }, `${this.driver.name} up; waiting for its name to resolve`);
|
|
265
|
+
this.announceWhenResolvable(url).catch((err: unknown) => {
|
|
266
|
+
log.warn({ err: errMsg(err) }, `${this.driver.name}: announce failed`);
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private async announceWhenResolvable(url: string): Promise<void> {
|
|
271
|
+
const resolved = await untilResolvable(new URL(url).host, this.resolves);
|
|
272
|
+
if (liveUrl !== url) return;
|
|
273
|
+
if (!resolved) log.warn({ url }, `${this.driver.name} name still not resolving; announcing anyway`);
|
|
274
|
+
this.onUrl(url);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private read(d: Buffer | string): void {
|
|
278
|
+
const text = (typeof d === 'string' ? d : d.toString('utf8')).trim();
|
|
279
|
+
if (text === '') return;
|
|
280
|
+
this.output.push(text);
|
|
281
|
+
log.debug({ output: text }, this.driver.name);
|
|
282
|
+
this.noticeUrl(text);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private silentCheck(): void {
|
|
286
|
+
if (this.closed || liveUrl !== null || this.child === null) return;
|
|
287
|
+
log.warn(
|
|
288
|
+
{ output: this.output.join('\n') },
|
|
289
|
+
`${this.driver.name}: no public address after ${String(SILENT_MS / 1000)}s; its own output follows`,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
start(): void {
|
|
294
|
+
if (this.closed) return;
|
|
295
|
+
log.info({ command: this.driver.command, args: this.driver.args }, `${this.driver.name} starting`);
|
|
296
|
+
this.output = [];
|
|
297
|
+
const child = spawn(this.driver.command, this.driver.args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
298
|
+
this.child = child;
|
|
299
|
+
child.stdout?.on('data', (d: Buffer | string) => {
|
|
300
|
+
this.read(d);
|
|
301
|
+
});
|
|
302
|
+
child.stderr?.on('data', (d: Buffer | string) => {
|
|
303
|
+
this.read(d);
|
|
304
|
+
});
|
|
305
|
+
setTimeout(() => {
|
|
306
|
+
this.silentCheck();
|
|
307
|
+
}, SILENT_MS).unref();
|
|
308
|
+
child.on('exit', (code) => {
|
|
309
|
+
this.child = null;
|
|
310
|
+
liveUrl = null;
|
|
311
|
+
if (this.closed) return;
|
|
312
|
+
this.afterExit(code).catch((err: unknown) => {
|
|
313
|
+
log.warn({ err: errMsg(err) }, `${this.driver.name}: exit handling failed`);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
child.on('error', (err) => {
|
|
317
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
318
|
+
this.closed = true;
|
|
319
|
+
log.error(
|
|
320
|
+
{ command: this.driver.command },
|
|
321
|
+
`${this.driver.name}: the binary is not installed, so there is no public address; install it and restart`,
|
|
322
|
+
);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
log.warn({ err: errMsg(err) }, `${this.driver.name} spawn error`);
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private async afterExit(code: number | null): Promise<void> {
|
|
330
|
+
const output = this.output.join('\n');
|
|
331
|
+
if (this.driver.adopt !== undefined && TAKEN_RE.test(output)) {
|
|
332
|
+
const found = await this.driver.adopt();
|
|
333
|
+
if (this.closed) return;
|
|
334
|
+
if (found.url !== null) {
|
|
335
|
+
log.info({ url: found.url }, `${this.driver.name}: ${found.hint}`);
|
|
336
|
+
this.noticeUrl(found.url);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
log.error({ output }, `${this.driver.name}: ${found.hint}`);
|
|
340
|
+
} else log.warn({ code, output: this.output.slice(-5).join('\n') }, `${this.driver.name} exited; restarting`);
|
|
341
|
+
setTimeout(() => {
|
|
342
|
+
this.start();
|
|
343
|
+
}, this.restartDelay);
|
|
344
|
+
this.restartDelay = Math.min(this.restartDelay * 2, RESTART_DELAY_MAX_MS);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
stop(): void {
|
|
348
|
+
this.closed = true;
|
|
349
|
+
this.child?.kill('SIGINT');
|
|
350
|
+
this.child = null;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
3
|
+
import { errMsg, log } from './log.js';
|
|
4
|
+
import { ApiError } from './api-error.js';
|
|
5
|
+
import { apiFailure, apiSession, cors, sendJson } from './api-http.js';
|
|
6
|
+
import { isRecord } from './is-record.js';
|
|
7
|
+
import { METRO_VERSION } from './version.js';
|
|
8
|
+
|
|
9
|
+
const PATH = '/api/update';
|
|
10
|
+
const CHECK_TTL_MS = 10 * 60_000;
|
|
11
|
+
const CHECK_TIMEOUT_MS = 30_000;
|
|
12
|
+
const UPDATE_TIMEOUT_MS = 5 * 60_000;
|
|
13
|
+
const RESTART_DELAY_MS = 500;
|
|
14
|
+
const OUTPUT_TAIL = 800;
|
|
15
|
+
|
|
16
|
+
export interface UpdateCheck {
|
|
17
|
+
current: string;
|
|
18
|
+
latest: string;
|
|
19
|
+
newer: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface UpdateResult {
|
|
23
|
+
updated: boolean;
|
|
24
|
+
version: string;
|
|
25
|
+
restarting: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface UpdateApiDeps {
|
|
29
|
+
authorize: (subject: string) => void;
|
|
30
|
+
restart: () => void;
|
|
31
|
+
cliBin?: () => string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const cliBinFromEnv = (): string => process.env.METRO_CLI_BIN?.trim() ?? '';
|
|
35
|
+
|
|
36
|
+
function runCli(bin: string, args: string[], timeout: number): Promise<{ stdout: string; stderr: string }> {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
execFile(process.execPath, [bin, ...args], { timeout, maxBuffer: 4 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
39
|
+
if (err === null) resolve({ stdout, stderr });
|
|
40
|
+
else reject(new ApiError(`metro update failed: ${(stderr || err.message).trim().slice(-OUTPUT_TAIL)}`, 502));
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let cached: { at: number; check: UpdateCheck } | null = null;
|
|
46
|
+
|
|
47
|
+
function parseCheck(raw: string): UpdateCheck {
|
|
48
|
+
let body: unknown;
|
|
49
|
+
try {
|
|
50
|
+
body = JSON.parse(raw.trim().split('\n').at(-1) ?? '');
|
|
51
|
+
} catch {
|
|
52
|
+
throw new ApiError('metro update --check returned no report', 502);
|
|
53
|
+
}
|
|
54
|
+
if (!isRecord(body) || typeof body.current !== 'string' || typeof body.latest !== 'string')
|
|
55
|
+
throw new ApiError('metro update --check returned an unexpected report', 502);
|
|
56
|
+
return { current: body.current, latest: body.latest, newer: body.newer === true };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function check(bin: string, now = Date.now()): Promise<UpdateCheck> {
|
|
60
|
+
if (cached !== null && now - cached.at < CHECK_TTL_MS) return cached.check;
|
|
61
|
+
const out = await runCli(bin, ['update', '--check'], CHECK_TIMEOUT_MS);
|
|
62
|
+
cached = { at: now, check: parseCheck(out.stdout) };
|
|
63
|
+
return cached.check;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function apply(bin: string, deps: UpdateApiDeps): Promise<UpdateResult> {
|
|
67
|
+
const before = await check(bin);
|
|
68
|
+
if (!before.newer) return { updated: false, version: before.current, restarting: false };
|
|
69
|
+
const out = await runCli(bin, ['update'], UPDATE_TIMEOUT_MS);
|
|
70
|
+
cached = null;
|
|
71
|
+
log.info({ from: before.current, to: before.latest, output: out.stderr.trim().slice(-OUTPUT_TAIL) }, 'update-api: metro updated, restarting');
|
|
72
|
+
setTimeout(() => {
|
|
73
|
+
deps.restart();
|
|
74
|
+
}, RESTART_DELAY_MS).unref();
|
|
75
|
+
return { updated: true, version: before.latest, restarting: true };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function binOrThrow(deps: UpdateApiDeps): string {
|
|
79
|
+
const bin = (deps.cliBin ?? cliBinFromEnv)();
|
|
80
|
+
if (bin === '')
|
|
81
|
+
throw new ApiError('this daemon was not started by metro serve, so it cannot update itself: run metro update on the machine', 400);
|
|
82
|
+
return bin;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function handleUpdateRequest(req: IncomingMessage, res: ServerResponse, deps: UpdateApiDeps): boolean {
|
|
86
|
+
const path = (req.url ?? '').split('?')[0] ?? '';
|
|
87
|
+
if (path !== PATH) return false;
|
|
88
|
+
if (req.method === 'OPTIONS') {
|
|
89
|
+
res.writeHead(204, cors(req)).end();
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
if (req.method !== 'GET' && req.method !== 'POST') {
|
|
93
|
+
sendJson(req, res, 405, { error: 'method not allowed' });
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
Promise.resolve()
|
|
97
|
+
.then(async (): Promise<unknown> => {
|
|
98
|
+
const session = await apiSession(req);
|
|
99
|
+
if (!session) throw new ApiError('unauthorized', 401);
|
|
100
|
+
deps.authorize(session.subject);
|
|
101
|
+
const bin = binOrThrow(deps);
|
|
102
|
+
if (req.method === 'GET') return { ...(await check(bin)), running: METRO_VERSION };
|
|
103
|
+
return apply(bin, deps);
|
|
104
|
+
})
|
|
105
|
+
.then((body) => {
|
|
106
|
+
sendJson(req, res, 200, body);
|
|
107
|
+
})
|
|
108
|
+
.catch((err: unknown) => {
|
|
109
|
+
if (err instanceof ApiError) apiFailure(req, res, err, 'update-api');
|
|
110
|
+
else {
|
|
111
|
+
log.warn({ err: errMsg(err) }, 'update-api: request failed');
|
|
112
|
+
if (!res.headersSent) sendJson(req, res, 500, { error: 'update api failed' });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
return true;
|
|
116
|
+
}
|