@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,294 @@
|
|
|
1
|
+
import { open, rename, rm, type FileHandle } from 'node:fs/promises';
|
|
2
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
3
|
+
import { guessMime } from '../stations/attachments.js';
|
|
4
|
+
import { safeFileName } from '../stations/attach-inline.js';
|
|
5
|
+
import {
|
|
6
|
+
allowedAgents,
|
|
7
|
+
authenticate,
|
|
8
|
+
extractToken,
|
|
9
|
+
} from '../mcp/request-identity.js';
|
|
10
|
+
import { ApiError } from './api-error.js';
|
|
11
|
+
import { apiFailure, cors, sendJson } from './api-http.js';
|
|
12
|
+
import { log } from './log.js';
|
|
13
|
+
import { writeSecure } from './secure-fs.js';
|
|
14
|
+
import {
|
|
15
|
+
createUploadSlot,
|
|
16
|
+
liveUploadBytes,
|
|
17
|
+
MAX_LIVE_UPLOAD_BYTES,
|
|
18
|
+
MAX_UPLOAD_BYTES,
|
|
19
|
+
readUpload,
|
|
20
|
+
removeUpload,
|
|
21
|
+
sweepUploads,
|
|
22
|
+
uploadPath,
|
|
23
|
+
uploadSlot,
|
|
24
|
+
uploadTicketAllows,
|
|
25
|
+
UPLOAD_TTL_MS,
|
|
26
|
+
type UploadRecord,
|
|
27
|
+
} from './upload-store.js';
|
|
28
|
+
|
|
29
|
+
const PREFIX = '/api/uploads';
|
|
30
|
+
const ITEM_METHODS = ['PUT', 'POST', 'DELETE'];
|
|
31
|
+
const DRAIN_MAX = 2 * MAX_UPLOAD_BYTES;
|
|
32
|
+
const VAGUE_MIMES = new Set([
|
|
33
|
+
'application/x-www-form-urlencoded',
|
|
34
|
+
'application/octet-stream',
|
|
35
|
+
'',
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
const mib = (n: number): string => `${Math.round(n / (1024 * 1024))} MiB`;
|
|
39
|
+
|
|
40
|
+
const uploadLimit = (): string =>
|
|
41
|
+
`${mib(MAX_UPLOAD_BYTES)} (${MAX_UPLOAD_BYTES} bytes)`;
|
|
42
|
+
|
|
43
|
+
const tooLarge = (): string =>
|
|
44
|
+
`upload exceeds the ${uploadLimit()} per-file limit; ` +
|
|
45
|
+
'send a link in `text` for anything larger';
|
|
46
|
+
|
|
47
|
+
const noRoom = (): string =>
|
|
48
|
+
`metro is already holding close to ${mib(MAX_LIVE_UPLOAD_BYTES)} of pending uploads, ` +
|
|
49
|
+
`which is the daemon-wide cap; each one expires ${Math.round(UPLOAD_TTL_MS / 60_000)} ` +
|
|
50
|
+
'minutes after it is created, or DELETE one you no longer need';
|
|
51
|
+
|
|
52
|
+
type Target =
|
|
53
|
+
| { kind: 'collection' }
|
|
54
|
+
| { kind: 'item'; id: string }
|
|
55
|
+
| { kind: 'unknown' }
|
|
56
|
+
| null;
|
|
57
|
+
|
|
58
|
+
export function target(path: string): Target {
|
|
59
|
+
if (path === PREFIX || path === `${PREFIX}/`) return { kind: 'collection' };
|
|
60
|
+
if (!path.startsWith(`${PREFIX}/`)) return null;
|
|
61
|
+
const rest = path.slice(PREFIX.length + 1).split('/').filter(Boolean);
|
|
62
|
+
const id = rest.length === 1 ? rest[0] : undefined;
|
|
63
|
+
if (id === undefined || uploadPath(id) === null) return { kind: 'unknown' };
|
|
64
|
+
return { kind: 'item', id };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const query = (req: IncomingMessage, key: string): string | undefined =>
|
|
68
|
+
new URL(req.url ?? '/', 'http://localhost').searchParams.get(key) ?? undefined;
|
|
69
|
+
|
|
70
|
+
const identityScope = (req: IncomingMessage): Set<string> =>
|
|
71
|
+
allowedAgents(authenticate(req) ?? undefined);
|
|
72
|
+
|
|
73
|
+
function ownerFromScope(req: IncomingMessage, allowed: Set<string>): string {
|
|
74
|
+
const requested = query(req, 'agent');
|
|
75
|
+
if (requested !== undefined) {
|
|
76
|
+
if (!allowed.has(requested))
|
|
77
|
+
throw new ApiError(`agent ${requested} is outside your scope`, 403);
|
|
78
|
+
return requested;
|
|
79
|
+
}
|
|
80
|
+
const [only] = [...allowed];
|
|
81
|
+
if (allowed.size === 1 && only !== undefined) return only;
|
|
82
|
+
throw new ApiError(
|
|
83
|
+
`this credential covers ${allowed.size} agents; ` +
|
|
84
|
+
'name the owning agent with ?agent=<id>',
|
|
85
|
+
400,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function mimeFor(req: IncomingMessage, name: string): string {
|
|
90
|
+
const explicit = query(req, 'mime');
|
|
91
|
+
if (explicit !== undefined && explicit !== '') return explicit;
|
|
92
|
+
const header = (req.headers['content-type'] ?? '').split(';')[0]?.trim() ?? '';
|
|
93
|
+
return VAGUE_MIMES.has(header.toLowerCase()) ? guessMime(name) : header;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function roomForUpload(): number {
|
|
97
|
+
sweepUploads();
|
|
98
|
+
const room = MAX_LIVE_UPLOAD_BYTES - liveUploadBytes();
|
|
99
|
+
if (room <= 0) throw new ApiError(noRoom(), 507);
|
|
100
|
+
return room;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const overflowAt = (total: number, room: number): ApiError | undefined => {
|
|
104
|
+
if (total > MAX_UPLOAD_BYTES) return new ApiError(tooLarge(), 413);
|
|
105
|
+
return total > room ? new ApiError(noRoom(), 507) : undefined;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
function declaredLength(req: IncomingMessage): number | undefined {
|
|
109
|
+
const n = Number(req.headers['content-length']);
|
|
110
|
+
return Number.isFinite(n) && n >= 0 ? n : undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function assertRoomForDeclared(req: IncomingMessage, room: number): void {
|
|
114
|
+
const declared = declaredLength(req);
|
|
115
|
+
if (declared === undefined) return;
|
|
116
|
+
if (declared > MAX_UPLOAD_BYTES) throw new ApiError(tooLarge(), 413);
|
|
117
|
+
if (declared > room) throw new ApiError(noRoom(), 507);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function drainInto(
|
|
121
|
+
req: IncomingMessage,
|
|
122
|
+
handle: FileHandle,
|
|
123
|
+
room: number,
|
|
124
|
+
): Promise<number> {
|
|
125
|
+
let over: ApiError | undefined;
|
|
126
|
+
let total = 0;
|
|
127
|
+
try {
|
|
128
|
+
for await (const chunk of req) {
|
|
129
|
+
const buf = chunk as Buffer;
|
|
130
|
+
total += buf.length;
|
|
131
|
+
over ??= overflowAt(total, room);
|
|
132
|
+
if (over === undefined) await handle.write(buf);
|
|
133
|
+
else if (total > DRAIN_MAX) break;
|
|
134
|
+
}
|
|
135
|
+
} finally {
|
|
136
|
+
await handle.close();
|
|
137
|
+
}
|
|
138
|
+
if (over !== undefined) throw over;
|
|
139
|
+
if (total === 0) throw new ApiError('upload body is empty', 400);
|
|
140
|
+
return total;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function streamToSlot(req: IncomingMessage, id: string): Promise<number> {
|
|
144
|
+
const part = uploadPath(id, '.part');
|
|
145
|
+
const final = uploadPath(id);
|
|
146
|
+
if (part === null || final === null) throw new ApiError('bad upload id', 400);
|
|
147
|
+
const room = roomForUpload();
|
|
148
|
+
assertRoomForDeclared(req, room);
|
|
149
|
+
const handle = await open(part, 'wx', 0o600).catch(() => {
|
|
150
|
+
throw new ApiError('this upload is already being written', 409);
|
|
151
|
+
});
|
|
152
|
+
const total = await drainInto(req, handle, room).catch(
|
|
153
|
+
async (err: unknown) => {
|
|
154
|
+
await rm(part, { force: true });
|
|
155
|
+
throw err;
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
await rename(part, final);
|
|
159
|
+
return total;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const payload = (rec: UploadRecord): Record<string, unknown> => ({
|
|
163
|
+
id: rec.id,
|
|
164
|
+
name: rec.name,
|
|
165
|
+
mime: rec.mime,
|
|
166
|
+
bytes: rec.bytes,
|
|
167
|
+
expires_at: new Date(rec.expiresAt).toISOString(),
|
|
168
|
+
attachment: { upload: rec.id },
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
async function fill(
|
|
172
|
+
req: IncomingMessage,
|
|
173
|
+
res: ServerResponse,
|
|
174
|
+
id: string,
|
|
175
|
+
status: number,
|
|
176
|
+
): Promise<void> {
|
|
177
|
+
const bytes = await streamToSlot(req, id);
|
|
178
|
+
const rec = readUpload(id);
|
|
179
|
+
if (rec === undefined) throw new ApiError('upload expired while storing', 410);
|
|
180
|
+
log.info({ upload: id, agent: rec.agentId, bytes }, 'uploads: stored');
|
|
181
|
+
sendJson(req, res, status, payload(rec));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function handleCreate(
|
|
185
|
+
req: IncomingMessage,
|
|
186
|
+
res: ServerResponse,
|
|
187
|
+
): Promise<void> {
|
|
188
|
+
const allowed = identityScope(req);
|
|
189
|
+
if (allowed.size === 0) throw new ApiError('unauthorized', 401);
|
|
190
|
+
const name = safeFileName(query(req, 'name'));
|
|
191
|
+
const id = createUploadSlot(ownerFromScope(req, allowed), {
|
|
192
|
+
name,
|
|
193
|
+
mime: mimeFor(req, name),
|
|
194
|
+
});
|
|
195
|
+
try {
|
|
196
|
+
await fill(req, res, id, 201);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
removeUpload(id);
|
|
199
|
+
throw err;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function authorizeItem(req: IncomingMessage, id: string): string {
|
|
204
|
+
const slot = uploadSlot(id);
|
|
205
|
+
if (slot === undefined) throw new ApiError('no such upload', 404);
|
|
206
|
+
if (uploadTicketAllows(id, slot.agentId, extractToken(req) ?? ''))
|
|
207
|
+
return slot.agentId;
|
|
208
|
+
if (identityScope(req).has(slot.agentId)) return slot.agentId;
|
|
209
|
+
throw new ApiError('no such upload', 404);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function relabelSlot(id: string, name: string, mime: string | undefined): void {
|
|
213
|
+
const slot = uploadSlot(id);
|
|
214
|
+
const path = uploadPath(id, '.meta');
|
|
215
|
+
if (slot === undefined || path === null) return;
|
|
216
|
+
writeSecure(
|
|
217
|
+
path,
|
|
218
|
+
JSON.stringify({
|
|
219
|
+
name,
|
|
220
|
+
mime: mime !== undefined && mime !== '' ? mime : guessMime(name),
|
|
221
|
+
createdAt: slot.createdAt,
|
|
222
|
+
}),
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function handleFill(
|
|
227
|
+
req: IncomingMessage,
|
|
228
|
+
res: ServerResponse,
|
|
229
|
+
id: string,
|
|
230
|
+
): Promise<void> {
|
|
231
|
+
authorizeItem(req, id);
|
|
232
|
+
if (readUpload(id) !== undefined)
|
|
233
|
+
throw new ApiError('this upload already holds bytes', 409);
|
|
234
|
+
const name = query(req, 'name');
|
|
235
|
+
if (name !== undefined)
|
|
236
|
+
relabelSlot(id, safeFileName(name), query(req, 'mime'));
|
|
237
|
+
await fill(req, res, id, 200);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function handleDelete(
|
|
241
|
+
req: IncomingMessage,
|
|
242
|
+
res: ServerResponse,
|
|
243
|
+
id: string,
|
|
244
|
+
): void {
|
|
245
|
+
authorizeItem(req, id);
|
|
246
|
+
removeUpload(id);
|
|
247
|
+
log.info({ upload: id }, 'uploads: deleted');
|
|
248
|
+
sendJson(req, res, 200, { id, deleted: true });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function dispatch(
|
|
252
|
+
req: IncomingMessage,
|
|
253
|
+
res: ServerResponse,
|
|
254
|
+
tgt: { kind: 'collection' } | { kind: 'item'; id: string },
|
|
255
|
+
): Promise<void> {
|
|
256
|
+
if (tgt.kind === 'collection') return handleCreate(req, res);
|
|
257
|
+
if (req.method === 'DELETE') handleDelete(req, res, tgt.id);
|
|
258
|
+
else await handleFill(req, res, tgt.id);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function closeIfBodyUnread(req: IncomingMessage, res: ServerResponse): void {
|
|
262
|
+
if (req.readableEnded) return;
|
|
263
|
+
const drop = (): void => {
|
|
264
|
+
req.socket?.destroy();
|
|
265
|
+
};
|
|
266
|
+
if (res.writableFinished) drop();
|
|
267
|
+
else res.once('finish', drop);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function handleUploadRequest(
|
|
271
|
+
req: IncomingMessage,
|
|
272
|
+
res: ServerResponse,
|
|
273
|
+
): boolean {
|
|
274
|
+
const tgt = target((req.url ?? '').split('?')[0] ?? '');
|
|
275
|
+
if (tgt === null) return false;
|
|
276
|
+
if (req.method === 'OPTIONS') {
|
|
277
|
+
res.writeHead(204, cors(req)).end();
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
if (tgt.kind === 'unknown') {
|
|
281
|
+
sendJson(req, res, 404, { error: 'no such upload' });
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
const methods = tgt.kind === 'collection' ? ['POST'] : ITEM_METHODS;
|
|
285
|
+
if (!methods.includes(req.method ?? '')) {
|
|
286
|
+
sendJson(req, res, 405, { error: 'method not allowed' });
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
dispatch(req, res, tgt).catch((err: unknown) => {
|
|
290
|
+
apiFailure(req, res, err, 'upload-api');
|
|
291
|
+
closeIfBodyUnread(req, res);
|
|
292
|
+
});
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { readdirSync, rmSync, statSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { ensureSecureDir, readJson, writeSecure } from './secure-fs.js';
|
|
6
|
+
import { grantAllowsPath, issueGrant } from './attach-grant.js';
|
|
7
|
+
import { ownerOf, recordOwner } from './attach-owner.js';
|
|
8
|
+
import { errMsg, log } from './log.js';
|
|
9
|
+
|
|
10
|
+
export const UPLOAD_TTL_MS = 30 * 60 * 1000;
|
|
11
|
+
export const MAX_UPLOAD_BYTES = 64 * 1024 * 1024;
|
|
12
|
+
export const MAX_LIVE_UPLOAD_BYTES = 512 * 1024 * 1024;
|
|
13
|
+
export const UPLOAD_ID_RE = /^up_[A-Za-z0-9_-]{22}$/;
|
|
14
|
+
|
|
15
|
+
const SUFFIXES = ['', '.part', '.meta', '.owner', '.grant'] as const;
|
|
16
|
+
const SWEEP_MS = 60_000;
|
|
17
|
+
|
|
18
|
+
export const uploadDir = (): string =>
|
|
19
|
+
process.env.METRO_UPLOAD_DIR ?? join(tmpdir(), 'metro-uploads');
|
|
20
|
+
|
|
21
|
+
export const newUploadId = (): string =>
|
|
22
|
+
`up_${randomBytes(16).toString('base64url')}`;
|
|
23
|
+
|
|
24
|
+
export function uploadPath(id: string, suffix = ''): string | null {
|
|
25
|
+
return UPLOAD_ID_RE.test(id) ? `${uploadDir()}/${id}${suffix}` : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface UploadMeta {
|
|
29
|
+
name: string;
|
|
30
|
+
mime: string;
|
|
31
|
+
createdAt: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface UploadRecord extends UploadMeta {
|
|
35
|
+
id: string;
|
|
36
|
+
agentId: string;
|
|
37
|
+
bytes: number;
|
|
38
|
+
path: string;
|
|
39
|
+
expiresAt: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const sizeOf = (path: string): number | undefined => {
|
|
43
|
+
try {
|
|
44
|
+
return statSync(path).size;
|
|
45
|
+
} catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function readMeta(id: string): UploadMeta | undefined {
|
|
51
|
+
const path = uploadPath(id, '.meta');
|
|
52
|
+
if (path === null) return undefined;
|
|
53
|
+
const meta = readJson<UploadMeta | null>(path, null);
|
|
54
|
+
if (meta === null || typeof meta.createdAt !== 'number') return undefined;
|
|
55
|
+
return meta;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface UploadSlot extends UploadMeta {
|
|
59
|
+
id: string;
|
|
60
|
+
agentId: string;
|
|
61
|
+
expiresAt: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function uploadSlot(id: string): UploadSlot | undefined {
|
|
65
|
+
const base = uploadPath(id);
|
|
66
|
+
if (base === null) return undefined;
|
|
67
|
+
const meta = readMeta(id);
|
|
68
|
+
const agentId = ownerOf(base);
|
|
69
|
+
if (meta === undefined || agentId === undefined) return undefined;
|
|
70
|
+
const expiresAt = meta.createdAt + UPLOAD_TTL_MS;
|
|
71
|
+
return expiresAt <= Date.now() ? undefined : { ...meta, id, agentId, expiresAt };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function readUpload(id: string): UploadRecord | undefined {
|
|
75
|
+
const slot = uploadSlot(id);
|
|
76
|
+
const path = uploadPath(id);
|
|
77
|
+
if (slot === undefined || path === null) return undefined;
|
|
78
|
+
const bytes = sizeOf(path);
|
|
79
|
+
return bytes === undefined ? undefined : { ...slot, bytes, path };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function createUploadSlot(
|
|
83
|
+
agentId: string,
|
|
84
|
+
meta: { name: string; mime: string },
|
|
85
|
+
): string {
|
|
86
|
+
ensureSecureDir(uploadDir());
|
|
87
|
+
const id = newUploadId();
|
|
88
|
+
const base = `${uploadDir()}/${id}`;
|
|
89
|
+
writeSecure(
|
|
90
|
+
`${base}.meta`,
|
|
91
|
+
JSON.stringify({ ...meta, createdAt: Date.now() }),
|
|
92
|
+
);
|
|
93
|
+
recordOwner(base, agentId);
|
|
94
|
+
return id;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const issueUploadTicket = (
|
|
98
|
+
id: string,
|
|
99
|
+
agentId: string,
|
|
100
|
+
): string | undefined => {
|
|
101
|
+
const base = uploadPath(id);
|
|
102
|
+
return base === null ? undefined : issueGrant(base, agentId, 'ut');
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const uploadTicketAllows = (
|
|
106
|
+
id: string,
|
|
107
|
+
owner: string,
|
|
108
|
+
presented: string,
|
|
109
|
+
): boolean => {
|
|
110
|
+
const base = uploadPath(id);
|
|
111
|
+
return base === null ? false : grantAllowsPath(base, owner, presented);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export function removeUpload(id: string): void {
|
|
115
|
+
const base = uploadPath(id);
|
|
116
|
+
if (base === null) return;
|
|
117
|
+
for (const suffix of SUFFIXES)
|
|
118
|
+
rmSync(`${base}${suffix}`, { force: true, recursive: false });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const idsInDir = (): string[] => {
|
|
122
|
+
let entries: string[];
|
|
123
|
+
try {
|
|
124
|
+
entries = readdirSync(uploadDir());
|
|
125
|
+
} catch {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
const ids = new Set<string>();
|
|
129
|
+
for (const entry of entries) {
|
|
130
|
+
const id = entry.split('.')[0] ?? '';
|
|
131
|
+
if (UPLOAD_ID_RE.test(id)) ids.add(id);
|
|
132
|
+
}
|
|
133
|
+
return [...ids];
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export function liveUploadBytes(): number {
|
|
137
|
+
let total = 0;
|
|
138
|
+
for (const id of idsInDir())
|
|
139
|
+
for (const suffix of ['', '.part'])
|
|
140
|
+
total += sizeOf(`${uploadDir()}/${id}${suffix}`) ?? 0;
|
|
141
|
+
return total;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function ageOf(id: string, now: number): number {
|
|
145
|
+
const created = readMeta(id)?.createdAt;
|
|
146
|
+
if (created !== undefined) return now - created;
|
|
147
|
+
let newest = 0;
|
|
148
|
+
for (const suffix of SUFFIXES) {
|
|
149
|
+
let mtime: number;
|
|
150
|
+
try {
|
|
151
|
+
mtime = statSync(`${uploadDir()}/${id}${suffix}`).mtimeMs;
|
|
152
|
+
} catch {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
newest = Math.max(newest, mtime);
|
|
156
|
+
}
|
|
157
|
+
return newest === 0 ? 0 : now - newest;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function sweepUploads(now = Date.now()): number {
|
|
161
|
+
let removed = 0;
|
|
162
|
+
for (const id of idsInDir()) {
|
|
163
|
+
if (ageOf(id, now) <= UPLOAD_TTL_MS) continue;
|
|
164
|
+
removeUpload(id);
|
|
165
|
+
removed += 1;
|
|
166
|
+
}
|
|
167
|
+
return removed;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function startUploadReaper(): void {
|
|
171
|
+
const run = (): void => {
|
|
172
|
+
try {
|
|
173
|
+
const removed = sweepUploads();
|
|
174
|
+
if (removed > 0) log.info({ removed }, 'uploads: reaped expired uploads');
|
|
175
|
+
} catch (err) {
|
|
176
|
+
log.warn({ err: errMsg(err) }, 'uploads: sweep failed');
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
run();
|
|
180
|
+
setInterval(run, SWEEP_MS).unref?.();
|
|
181
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import { errMsg, log } from './log.js';
|
|
3
|
+
import { apiFailure, cors, readJsonBody, sendJson } from './api-http.js';
|
|
4
|
+
import { parseId } from '../db/ids.js';
|
|
5
|
+
import { ENVELOPE_MAX, type VaultBundle, type VaultEntry } from './vault-types.js';
|
|
6
|
+
import { signedIdentity } from './signed-identity.js';
|
|
7
|
+
|
|
8
|
+
const PREFIX = '/api/vault';
|
|
9
|
+
const BODY_MAX = ENVELOPE_MAX + 64 * 1024;
|
|
10
|
+
|
|
11
|
+
export interface VaultApiDeps {
|
|
12
|
+
list: (subject: string) => Promise<VaultEntry[]>;
|
|
13
|
+
put: (subject: string, id: string, body: unknown) => Promise<VaultEntry>;
|
|
14
|
+
get: (subject: string, id: string) => Promise<VaultBundle>;
|
|
15
|
+
remove: (subject: string, id: string) => Promise<{ id: string; name: string }>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type Target = { kind: 'index' } | { kind: 'bundle'; id: string } | { kind: 'unknown' } | null;
|
|
19
|
+
|
|
20
|
+
function target(path: string): Target {
|
|
21
|
+
if (path === PREFIX || path === `${PREFIX}/`) return { kind: 'index' };
|
|
22
|
+
if (!path.startsWith(`${PREFIX}/`)) return null;
|
|
23
|
+
const segments = path.slice(PREFIX.length + 1).split('/').filter(Boolean);
|
|
24
|
+
const id = parseId(segments[0] ?? '');
|
|
25
|
+
return id === null || segments.length !== 1 ? { kind: 'unknown' } : { kind: 'bundle', id };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ALLOWED: Record<'index' | 'bundle', string[]> = { index: ['GET'], bundle: ['GET', 'PUT', 'DELETE'] };
|
|
29
|
+
|
|
30
|
+
async function route(
|
|
31
|
+
req: IncomingMessage,
|
|
32
|
+
res: ServerResponse,
|
|
33
|
+
deps: VaultApiDeps,
|
|
34
|
+
tgt: { kind: 'index' } | { kind: 'bundle'; id: string },
|
|
35
|
+
): Promise<void> {
|
|
36
|
+
try {
|
|
37
|
+
const owner = await signedIdentity(req);
|
|
38
|
+
if (owner === null) {
|
|
39
|
+
sendJson(req, res, 401, { error: 'unauthorized' });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (tgt.kind === 'index') {
|
|
43
|
+
sendJson(req, res, 200, { entries: await deps.list(owner) });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (req.method === 'GET') sendJson(req, res, 200, await deps.get(owner, tgt.id));
|
|
47
|
+
else if (req.method === 'DELETE') sendJson(req, res, 200, await deps.remove(owner, tgt.id));
|
|
48
|
+
else {
|
|
49
|
+
const saved = await deps.put(owner, tgt.id, await readJsonBody(req, BODY_MAX));
|
|
50
|
+
log.info({ id: saved.id, name: saved.name }, 'vault-api: bundle stored');
|
|
51
|
+
sendJson(req, res, 200, saved);
|
|
52
|
+
}
|
|
53
|
+
} catch (err) {
|
|
54
|
+
apiFailure(req, res, err, 'vault-api');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function handleVaultApiRequest(req: IncomingMessage, res: ServerResponse, deps: VaultApiDeps): boolean {
|
|
59
|
+
const tgt = target((req.url ?? '').split('?')[0] ?? '');
|
|
60
|
+
if (tgt === null) return false;
|
|
61
|
+
if (req.method === 'OPTIONS') {
|
|
62
|
+
res.writeHead(204, cors(req)).end();
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
if (tgt.kind === 'unknown') {
|
|
66
|
+
sendJson(req, res, 404, { error: 'no such bundle' });
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
if (!ALLOWED[tgt.kind].includes(req.method ?? '')) {
|
|
70
|
+
sendJson(req, res, 405, { error: 'method not allowed' });
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
route(req, res, deps, tgt).catch((err: unknown) => {
|
|
74
|
+
log.warn({ err: errMsg(err) }, 'vault-api: unhandled error');
|
|
75
|
+
if (!res.headersSent) sendJson(req, res, 500, { error: 'vault api failed' });
|
|
76
|
+
});
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { STATIONS, type StationName } from './stations.js';
|
|
2
|
+
|
|
3
|
+
const ACCOUNT_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
4
|
+
|
|
5
|
+
export interface AccountRef {
|
|
6
|
+
agentId: string;
|
|
7
|
+
station: StationName;
|
|
8
|
+
accountId: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isStationName(raw: unknown): raw is StationName {
|
|
12
|
+
return (
|
|
13
|
+
typeof raw === 'string' && (STATIONS as readonly string[]).includes(raw)
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function parseAccountId(raw: string): string | null {
|
|
18
|
+
return ACCOUNT_ID_RE.test(raw) ? raw : null;
|
|
19
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { ApiError } from '../daemon/api-error.js';
|
|
3
|
+
import { AGENT_NAME_RE } from './ids.js';
|
|
4
|
+
|
|
5
|
+
export class AgentAdminError extends ApiError {}
|
|
6
|
+
|
|
7
|
+
export interface AgentSummary {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
owned: boolean;
|
|
11
|
+
key: string | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CreatedAgent {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
key: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface OwnedAgent {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type DeletedAgent = OwnedAgent;
|
|
26
|
+
|
|
27
|
+
export interface ResetAgentKey extends OwnedAgent {
|
|
28
|
+
key: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function normalizeAgentName(raw: unknown): string {
|
|
32
|
+
const name = typeof raw === 'string' ? raw.trim() : '';
|
|
33
|
+
if (!AGENT_NAME_RE.test(name))
|
|
34
|
+
throw new AgentAdminError(
|
|
35
|
+
'name must be 2-32 characters of A-Z, a-z, 0-9, - or _, starting with a letter or digit',
|
|
36
|
+
400,
|
|
37
|
+
);
|
|
38
|
+
return name;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function newApiKey(): string {
|
|
42
|
+
return `mk_${randomBytes(32).toString('base64url')}`;
|
|
43
|
+
}
|