@stage-labs/metro 0.1.0-beta.4 → 0.1.0-beta.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.js +131 -0
- package/dist/bedrock-eventstream.js +120 -0
- package/dist/bedrock-proxy.js +260 -0
- package/dist/bedrock-upstream.js +72 -0
- package/dist/bedrock.js +106 -0
- package/dist/claude.js +15 -0
- package/dist/cli.js +181 -207
- package/dist/control.js +173 -0
- package/dist/plugin.js +82 -0
- package/dist/prompt.js +61 -0
- package/dist/runtime.js +117 -0
- package/dist/serve.js +77 -0
- package/dist/store.js +93 -0
- package/dist/tail.js +120 -0
- package/dist/update.js +33 -0
- package/dist/version.js +97 -0
- package/package.json +34 -21
- package/runtime/node_modules/@metro-labs/discord-bot/package.json +28 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +119 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +388 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/api-base.ts +1 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/attachments.ts +29 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +210 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/group-actions.ts +50 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/group.ts +115 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +127 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +215 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +43 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/station.ts +19 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +82 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/voice-speak.ts +195 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/voice-transcribe.ts +292 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/voice.ts +197 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +5 -0
- package/runtime/node_modules/@metro-labs/mcp/package.json +67 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/dedupe.ts +24 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/inbound.ts +367 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/media-note.ts +85 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/pending.ts +75 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/relay.ts +129 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/webhook-note.ts +51 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +311 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +334 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-connector-api.ts +150 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-import.ts +76 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-pair.ts +20 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-error.ts +8 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +153 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-grant.ts +99 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-owner.ts +32 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +133 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +278 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +383 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +143 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connect-hint.ts +33 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +315 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-json.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth-routes.ts +157 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +127 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-tools.ts +100 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-url.ts +57 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +333 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/cors.ts +29 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/crash-guard.ts +49 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/events.ts +117 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/hosted-connectors.ts +118 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +369 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +86 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/import-api.ts +67 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-connectors-api.ts +56 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-mode.ts +187 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-secret.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/log.ts +42 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/mode-api.ts +31 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-client.ts +162 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-discovery.ts +120 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-pending.ts +65 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/pair-codes.ts +58 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +145 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/project-api.ts +214 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/protocol.ts +224 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/relay.ts +303 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +139 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-source.ts +105 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/secure-fs.ts +57 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-api.ts +27 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +63 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-config.ts +27 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +218 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-auth.ts +82 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-nonces.ts +43 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-routes.ts +89 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor-io.ts +97 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor.ts +320 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-call.ts +22 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-error.ts +53 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +257 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +295 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-store.ts +181 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +91 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +214 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-connectors.ts +143 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-map.ts +84 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-scope.ts +64 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/client.ts +37 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-config.ts +147 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-relay.ts +126 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +326 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/file-admin.ts +319 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/file-source.ts +152 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/ids.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/key-map.ts +45 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +373 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +309 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +24 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +105 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +118 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/token-carry.ts +45 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +72 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/whatsapp-login.ts +18 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/accounts.ts +141 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/call-tools.ts +203 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/channel-owner.ts +29 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/ctx.ts +62 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/event-store.ts +91 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/frame-scope.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/group-tools.ts +129 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/index.ts +192 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/member-tools.ts +72 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/permission-relay.ts +66 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/raw-get-stream.ts +207 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/request-identity.ts +73 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-registry.ts +172 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-route.ts +43 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/session.ts +211 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/str.ts +5 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-dispatch.ts +212 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +397 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/upload-tool.ts +66 -0
- package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +240 -0
- package/runtime/node_modules/@metro-labs/mcp/src/server.ts +2 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +80 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-inline.ts +100 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-interactive.ts +161 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-resolve.ts +200 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-xmtp.ts +111 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +182 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attachments.ts +196 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +82 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/messaging-normalize.ts +88 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/registry.ts +35 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/station-runtime.ts +64 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/types.ts +107 -0
- package/runtime/node_modules/@metro-labs/telegram/package.json +28 -0
- package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +81 -0
- package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +254 -0
- package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +87 -0
- package/runtime/node_modules/@metro-labs/telegram/src/client.ts +77 -0
- package/runtime/node_modules/@metro-labs/telegram/src/format.ts +141 -0
- package/runtime/node_modules/@metro-labs/telegram/src/group.ts +261 -0
- package/runtime/node_modules/@metro-labs/telegram/src/history.ts +52 -0
- package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +78 -0
- package/runtime/node_modules/@metro-labs/telegram/src/index.ts +46 -0
- package/runtime/node_modules/@metro-labs/telegram/src/login.ts +166 -0
- package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +80 -0
- package/runtime/node_modules/@metro-labs/telegram/src/members.ts +75 -0
- package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +22 -0
- package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +167 -0
- package/runtime/node_modules/@metro-labs/telegram/src/station.ts +24 -0
- package/runtime/node_modules/@metro-labs/telegram/src/types.ts +7 -0
- package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +1 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/package.json +25 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +120 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +295 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +172 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +103 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +166 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +17 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +48 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/verify.ts +38 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +3 -0
- package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
- package/runtime/node_modules/@metro-labs/webhook/src/station.ts +40 -0
- package/runtime/node_modules/@metro-labs/whatsapp/package.json +29 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +67 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +178 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +72 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +129 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +318 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +132 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +57 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +40 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +59 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/logger.ts +60 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +176 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +84 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +22 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/outbox.ts +38 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +141 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +17 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +98 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +6 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +5 -0
- package/runtime/node_modules/@metro-labs/xmtp/package.json +33 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +113 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +110 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +299 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +157 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +87 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +375 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +107 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +171 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +239 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +55 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +333 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +78 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +34 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +56 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +33 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +125 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +157 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +349 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +13 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +126 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +212 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +90 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +84 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +39 -0
- package/runtime/runtime.json +3 -0
- package/runtime/trains/.keep +0 -0
- package/LICENSE +0 -21
- package/README.md +0 -117
- package/dist/agents/claude.js +0 -207
- package/dist/agents/codex.js +0 -207
- package/dist/agents/types.js +0 -2
- package/dist/channels/discord.js +0 -104
- package/dist/channels/telegram.js +0 -189
- package/dist/helpers/scope-cache.js +0 -65
- package/dist/helpers/streaming.js +0 -209
- package/dist/helpers/telegram-format.js +0 -39
- package/dist/helpers/turn.js +0 -40
- package/dist/log.js +0 -10
- package/dist/orchestrator.js +0 -208
- package/dist/paths.js +0 -68
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { and, asc, eq, inArray } from 'drizzle-orm';
|
|
3
|
+
import { ApiError } from '../daemon/api-error.js';
|
|
4
|
+
import { getDb } from './client.js';
|
|
5
|
+
import { registerKey, rotateAgentKey, unregisterAgentKey } from './key-map.js';
|
|
6
|
+
import { AGENT_NAME_RE, newId, parseId } from './ids.js';
|
|
7
|
+
import { agents, stations } from './schema.js';
|
|
8
|
+
import { isUniqueViolation } from './users.js';
|
|
9
|
+
import { projectIdOrThrow } from './projects.js';
|
|
10
|
+
|
|
11
|
+
const KEY_ATTEMPTS = 5;
|
|
12
|
+
|
|
13
|
+
export class AgentAdminError extends ApiError {}
|
|
14
|
+
|
|
15
|
+
export interface AgentSummary {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
owned: boolean;
|
|
19
|
+
key: string | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CreatedAgent {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
key: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface OwnedAgent {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type DeletedAgent = OwnedAgent;
|
|
34
|
+
|
|
35
|
+
export interface ResetAgentKey extends OwnedAgent {
|
|
36
|
+
key: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function normalizeAgentName(raw: unknown): string {
|
|
40
|
+
const name = typeof raw === 'string' ? raw.trim() : '';
|
|
41
|
+
if (!AGENT_NAME_RE.test(name))
|
|
42
|
+
throw new AgentAdminError(
|
|
43
|
+
'name must be 2-32 characters of A-Z, a-z, 0-9, - or _, starting with a letter or digit',
|
|
44
|
+
400,
|
|
45
|
+
);
|
|
46
|
+
return name;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function newApiKey(): string {
|
|
50
|
+
return `mk_${randomBytes(32).toString('base64url')}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const agentPin = (): string => process.env.METRO_AGENT?.trim() ?? '';
|
|
54
|
+
|
|
55
|
+
export function servesEveryAgent(): boolean {
|
|
56
|
+
return agentPin() === '';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function daemonServesAgent(id: string): boolean {
|
|
60
|
+
const pin = agentPin();
|
|
61
|
+
return pin === '' || pin === id;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface AgentRow {
|
|
65
|
+
id: string;
|
|
66
|
+
name: string;
|
|
67
|
+
projectId: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface KeyRow {
|
|
71
|
+
agentId: string;
|
|
72
|
+
key: string | null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function toAgentSummaries(
|
|
76
|
+
rows: AgentRow[],
|
|
77
|
+
keyRows: KeyRow[],
|
|
78
|
+
): AgentSummary[] {
|
|
79
|
+
return rows.map((r) => ({
|
|
80
|
+
id: r.id,
|
|
81
|
+
name: r.name,
|
|
82
|
+
owned: true,
|
|
83
|
+
key: keyRows.find((k) => k.agentId === r.id)?.key ?? null,
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function selectKeyRows(ids: string[]): Promise<KeyRow[]> {
|
|
88
|
+
if (ids.length === 0) return [];
|
|
89
|
+
return getDb()
|
|
90
|
+
.select({ agentId: agents.id, key: agents.key })
|
|
91
|
+
.from(agents)
|
|
92
|
+
.where(inArray(agents.id, ids));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function listAgentsForUser(
|
|
96
|
+
subject: string,
|
|
97
|
+
project: string,
|
|
98
|
+
): Promise<AgentSummary[]> {
|
|
99
|
+
const projectId = await projectIdOrThrow(subject, project);
|
|
100
|
+
const rows = await getDb()
|
|
101
|
+
.select({ id: agents.id, name: agents.name, projectId: agents.projectId })
|
|
102
|
+
.from(agents)
|
|
103
|
+
.where(eq(agents.projectId, projectId))
|
|
104
|
+
.orderBy(asc(agents.name), asc(agents.id));
|
|
105
|
+
if (rows.length === 0) return [];
|
|
106
|
+
const keyRows = await selectKeyRows(rows.map((r) => r.id));
|
|
107
|
+
return toAgentSummaries(rows, keyRows);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function insertAgent(
|
|
111
|
+
projectId: string,
|
|
112
|
+
name: string,
|
|
113
|
+
key: string,
|
|
114
|
+
): Promise<string> {
|
|
115
|
+
const inserted = await getDb()
|
|
116
|
+
.insert(agents)
|
|
117
|
+
.values({ id: newId(), name, projectId, key })
|
|
118
|
+
.returning({ id: agents.id });
|
|
119
|
+
const id = inserted[0]?.id;
|
|
120
|
+
if (id === undefined)
|
|
121
|
+
throw new AgentAdminError('agent insert returned no id', 500);
|
|
122
|
+
return id;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function createAgentForUser(
|
|
126
|
+
subject: string,
|
|
127
|
+
project: string,
|
|
128
|
+
rawName: string,
|
|
129
|
+
): Promise<CreatedAgent> {
|
|
130
|
+
const name = normalizeAgentName(rawName);
|
|
131
|
+
const key = newApiKey();
|
|
132
|
+
const id = await insertAgent(await projectIdOrThrow(subject, project), name, key);
|
|
133
|
+
if (servesEveryAgent()) registerKey(key, id);
|
|
134
|
+
return { id, name, key };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function parseAgentId(raw: string): string | null {
|
|
138
|
+
return parseId(raw);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
interface Deletable {
|
|
142
|
+
agent: DeletedAgent;
|
|
143
|
+
projectId: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function ownedAgentOrThrow(
|
|
147
|
+
subject: string,
|
|
148
|
+
id: string,
|
|
149
|
+
): Promise<Deletable> {
|
|
150
|
+
const rows = await getDb().select().from(agents).where(eq(agents.id, id));
|
|
151
|
+
const row = rows[0];
|
|
152
|
+
const missing = new AgentAdminError('no such agent', 404);
|
|
153
|
+
if (!row) throw missing;
|
|
154
|
+
try {
|
|
155
|
+
await projectIdOrThrow(subject, row.projectId);
|
|
156
|
+
} catch {
|
|
157
|
+
throw missing;
|
|
158
|
+
}
|
|
159
|
+
return { agent: { id: row.id, name: row.name }, projectId: row.projectId };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function writeNewKey(id: string, projectId: string): Promise<string> {
|
|
163
|
+
for (let attempt = 0; attempt < KEY_ATTEMPTS; attempt += 1) {
|
|
164
|
+
const key = newApiKey();
|
|
165
|
+
try {
|
|
166
|
+
const changed = await getDb()
|
|
167
|
+
.update(agents)
|
|
168
|
+
.set({ key })
|
|
169
|
+
.where(and(eq(agents.id, id), eq(agents.projectId, projectId)))
|
|
170
|
+
.returning({ id: agents.id });
|
|
171
|
+
if (changed.length === 0)
|
|
172
|
+
throw new AgentAdminError('no such agent', 404);
|
|
173
|
+
return key;
|
|
174
|
+
} catch (err) {
|
|
175
|
+
if (!isUniqueViolation(err)) throw err;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
throw new AgentAdminError('could not allocate a free api key', 500);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function resetAgentKeyForUser(
|
|
182
|
+
subject: string,
|
|
183
|
+
id: string,
|
|
184
|
+
): Promise<ResetAgentKey> {
|
|
185
|
+
const { agent, projectId } = await ownedAgentOrThrow(subject, id);
|
|
186
|
+
const key = await writeNewKey(agent.id, projectId);
|
|
187
|
+
rotateAgentKey(agent.id, daemonServesAgent(agent.id) ? key : null);
|
|
188
|
+
return { id: agent.id, name: agent.name, key };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function deleteAgentForUser(
|
|
192
|
+
subject: string,
|
|
193
|
+
id: string,
|
|
194
|
+
): Promise<DeletedAgent> {
|
|
195
|
+
const { agent, projectId } = await ownedAgentOrThrow(subject, id);
|
|
196
|
+
await getDb().transaction(async (tx) => {
|
|
197
|
+
const attached = await tx
|
|
198
|
+
.select({ id: stations.id })
|
|
199
|
+
.from(stations)
|
|
200
|
+
.where(eq(stations.agentId, id));
|
|
201
|
+
if (attached.length > 0)
|
|
202
|
+
throw new AgentAdminError(
|
|
203
|
+
`agent '${agent.name}' still has ${attached.length} station account(s) attached — an operator must remove them first`,
|
|
204
|
+
409,
|
|
205
|
+
);
|
|
206
|
+
const gone = await tx
|
|
207
|
+
.delete(agents)
|
|
208
|
+
.where(and(eq(agents.id, id), eq(agents.projectId, projectId)))
|
|
209
|
+
.returning({ id: agents.id });
|
|
210
|
+
if (gone.length === 0) throw new AgentAdminError('no such agent', 404);
|
|
211
|
+
});
|
|
212
|
+
unregisterAgentKey(id);
|
|
213
|
+
return agent;
|
|
214
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { and, asc, eq, inArray, ne } from 'drizzle-orm';
|
|
2
|
+
import { getDb } from './client.js';
|
|
3
|
+
import { newId } from './ids.js';
|
|
4
|
+
import { isUniqueViolation } from './users.js';
|
|
5
|
+
import { ownedAgentOrThrow } from './agent-admin.js';
|
|
6
|
+
import { ConnectorError } from './connector-config.js';
|
|
7
|
+
import { agentConnectors, agents, connectors } from './schema.js';
|
|
8
|
+
|
|
9
|
+
export interface AgentConnectors {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
connectorIds: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const nameClash = (name: string, agent: string): ConnectorError =>
|
|
16
|
+
new ConnectorError(
|
|
17
|
+
`the agent '${agent}' already has a connector named '${name}'`,
|
|
18
|
+
409,
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
async function collidingAgent(
|
|
22
|
+
agentIds: string[],
|
|
23
|
+
name: string,
|
|
24
|
+
exceptConnectorId: string,
|
|
25
|
+
): Promise<string | null> {
|
|
26
|
+
if (agentIds.length === 0) return null;
|
|
27
|
+
const rows = await getDb()
|
|
28
|
+
.select({ agent: agents.name })
|
|
29
|
+
.from(agentConnectors)
|
|
30
|
+
.innerJoin(connectors, eq(connectors.id, agentConnectors.connectorId))
|
|
31
|
+
.innerJoin(agents, eq(agents.id, agentConnectors.agentId))
|
|
32
|
+
.where(
|
|
33
|
+
and(
|
|
34
|
+
inArray(agentConnectors.agentId, agentIds),
|
|
35
|
+
eq(connectors.name, name),
|
|
36
|
+
ne(connectors.id, exceptConnectorId),
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
.limit(1);
|
|
40
|
+
return rows[0]?.agent ?? null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function assertRenameFreeOfClash(
|
|
44
|
+
connectorId: string,
|
|
45
|
+
name: string,
|
|
46
|
+
): Promise<void> {
|
|
47
|
+
const rows = await getDb()
|
|
48
|
+
.select({ agentId: agentConnectors.agentId })
|
|
49
|
+
.from(agentConnectors)
|
|
50
|
+
.where(eq(agentConnectors.connectorId, connectorId));
|
|
51
|
+
const clash = await collidingAgent(
|
|
52
|
+
rows.map((r) => r.agentId),
|
|
53
|
+
name,
|
|
54
|
+
connectorId,
|
|
55
|
+
);
|
|
56
|
+
if (clash !== null) throw nameClash(name, clash);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function connectorIdsOfAgent(agentId: string): Promise<string[]> {
|
|
60
|
+
const rows = await getDb()
|
|
61
|
+
.select({ connectorId: agentConnectors.connectorId })
|
|
62
|
+
.from(agentConnectors)
|
|
63
|
+
.where(eq(agentConnectors.agentId, agentId))
|
|
64
|
+
.orderBy(asc(agentConnectors.id));
|
|
65
|
+
return rows.map((r) => r.connectorId);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function connectorIdsByAgent(
|
|
69
|
+
agentIds: string[],
|
|
70
|
+
): Promise<Map<string, string[]>> {
|
|
71
|
+
const out = new Map<string, string[]>();
|
|
72
|
+
if (agentIds.length === 0) return out;
|
|
73
|
+
const rows = await getDb()
|
|
74
|
+
.select({
|
|
75
|
+
agentId: agentConnectors.agentId,
|
|
76
|
+
connectorId: agentConnectors.connectorId,
|
|
77
|
+
})
|
|
78
|
+
.from(agentConnectors)
|
|
79
|
+
.where(inArray(agentConnectors.agentId, agentIds))
|
|
80
|
+
.orderBy(asc(agentConnectors.id));
|
|
81
|
+
for (const row of rows) {
|
|
82
|
+
const list = out.get(row.agentId) ?? [];
|
|
83
|
+
list.push(row.connectorId);
|
|
84
|
+
out.set(row.agentId, list);
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function agentConnectorsForUser(
|
|
90
|
+
subject: string,
|
|
91
|
+
agentId: string,
|
|
92
|
+
): Promise<AgentConnectors> {
|
|
93
|
+
const { agent } = await ownedAgentOrThrow(subject, agentId);
|
|
94
|
+
return { ...agent, connectorIds: await connectorIdsOfAgent(agent.id) };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function connectorNameIn(
|
|
98
|
+
projectId: string,
|
|
99
|
+
connectorId: string,
|
|
100
|
+
): Promise<string | null> {
|
|
101
|
+
const rows = await getDb()
|
|
102
|
+
.select({ name: connectors.name })
|
|
103
|
+
.from(connectors)
|
|
104
|
+
.where(and(eq(connectors.id, connectorId), eq(connectors.projectId, projectId)));
|
|
105
|
+
return rows[0]?.name ?? null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function addConnectorToAgentForUser(
|
|
109
|
+
subject: string,
|
|
110
|
+
agentId: string,
|
|
111
|
+
connectorId: string,
|
|
112
|
+
): Promise<AgentConnectors> {
|
|
113
|
+
const { agent, projectId } = await ownedAgentOrThrow(subject, agentId);
|
|
114
|
+
const name = await connectorNameIn(projectId, connectorId);
|
|
115
|
+
if (name === null) throw new ConnectorError('no such connector', 404);
|
|
116
|
+
const clash = await collidingAgent([agent.id], name, connectorId);
|
|
117
|
+
if (clash !== null) throw nameClash(name, clash);
|
|
118
|
+
try {
|
|
119
|
+
await getDb()
|
|
120
|
+
.insert(agentConnectors)
|
|
121
|
+
.values({ id: newId(), agentId: agent.id, connectorId });
|
|
122
|
+
} catch (err) {
|
|
123
|
+
if (!isUniqueViolation(err)) throw err;
|
|
124
|
+
}
|
|
125
|
+
return { ...agent, connectorIds: await connectorIdsOfAgent(agent.id) };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function removeConnectorFromAgentForUser(
|
|
129
|
+
subject: string,
|
|
130
|
+
agentId: string,
|
|
131
|
+
connectorId: string,
|
|
132
|
+
): Promise<AgentConnectors> {
|
|
133
|
+
const { agent } = await ownedAgentOrThrow(subject, agentId);
|
|
134
|
+
await getDb()
|
|
135
|
+
.delete(agentConnectors)
|
|
136
|
+
.where(
|
|
137
|
+
and(
|
|
138
|
+
eq(agentConnectors.agentId, agent.id),
|
|
139
|
+
eq(agentConnectors.connectorId, connectorId),
|
|
140
|
+
),
|
|
141
|
+
);
|
|
142
|
+
return { ...agent, connectorIds: await connectorIdsOfAgent(agent.id) };
|
|
143
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export type AgentMap = Record<string, string>;
|
|
2
|
+
export type AgentNameMap = Record<string, string>;
|
|
3
|
+
export type AllowlistMap = Record<string, string[]>;
|
|
4
|
+
|
|
5
|
+
const mapKey = (station: string, accountId: string): string =>
|
|
6
|
+
`${station}/${accountId}`;
|
|
7
|
+
|
|
8
|
+
let agentMap: AgentMap = {};
|
|
9
|
+
let agentNames: AgentNameMap = {};
|
|
10
|
+
let allowlistMap: AllowlistMap = {};
|
|
11
|
+
|
|
12
|
+
export function setAgentMap(map: AgentMap, names: AgentNameMap): void {
|
|
13
|
+
agentMap = map;
|
|
14
|
+
agentNames = names;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function setAllowlistMap(map: AllowlistMap): void {
|
|
18
|
+
allowlistMap = map;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function accountFromLine(
|
|
22
|
+
line: string,
|
|
23
|
+
): { station: string; accountId: string } | undefined {
|
|
24
|
+
const parts = line.split('/');
|
|
25
|
+
const station = parts[2];
|
|
26
|
+
const accountId = parts[3];
|
|
27
|
+
if (!station || !accountId) return undefined;
|
|
28
|
+
return { station, accountId };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function agentIdForAccount(
|
|
32
|
+
station: string,
|
|
33
|
+
accountId: string,
|
|
34
|
+
): string | undefined {
|
|
35
|
+
return agentMap[mapKey(station, accountId)];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function agentIdForLine(line: string): string | undefined {
|
|
39
|
+
const a = accountFromLine(line);
|
|
40
|
+
return a ? agentIdForAccount(a.station, a.accountId) : undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function agentForLine(line: string): string | undefined {
|
|
44
|
+
const id = agentIdForLine(line);
|
|
45
|
+
return id === undefined ? undefined : agentNames[id];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface KnownAccount {
|
|
49
|
+
station: string;
|
|
50
|
+
id: string;
|
|
51
|
+
agentId: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function knownAccounts(): KnownAccount[] {
|
|
55
|
+
return Object.entries(agentMap).flatMap(([key, agentId]) => {
|
|
56
|
+
const cut = key.indexOf('/');
|
|
57
|
+
return cut === -1 ? [] : [{ station: key.slice(0, cut), id: key.slice(cut + 1), agentId }];
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function stationAgentIds(station: string): string[] {
|
|
62
|
+
const prefix = `${station}/`;
|
|
63
|
+
return Object.entries(agentMap)
|
|
64
|
+
.filter(([key]) => key.startsWith(prefix))
|
|
65
|
+
.map(([, id]) => id);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function allowlistForLine(line: string): string[] | undefined {
|
|
69
|
+
const a = accountFromLine(line);
|
|
70
|
+
return a ? allowlistMap[mapKey(a.station, a.accountId)] : undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function senderMatchesAllowlist(
|
|
74
|
+
allowlist: string[],
|
|
75
|
+
from: string,
|
|
76
|
+
): boolean {
|
|
77
|
+
if (allowlist.length === 0 || allowlist.includes('*')) return true;
|
|
78
|
+
const f = (from ?? '').toLowerCase();
|
|
79
|
+
const id = f.split('/').pop() ?? f;
|
|
80
|
+
return allowlist.some((a) => {
|
|
81
|
+
const v = a.toLowerCase();
|
|
82
|
+
return v === f || v === id;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import {
|
|
2
|
+
accountFromLine,
|
|
3
|
+
agentIdForAccount,
|
|
4
|
+
stationAgentIds,
|
|
5
|
+
} from './agent-map.js';
|
|
6
|
+
import { STATIONS } from './schema.js';
|
|
7
|
+
|
|
8
|
+
const ACCOUNT_STATIONS = new Set<string>(STATIONS);
|
|
9
|
+
|
|
10
|
+
const stationHasAccounts = (station: string): boolean =>
|
|
11
|
+
ACCOUNT_STATIONS.has(station);
|
|
12
|
+
|
|
13
|
+
const accountInScope = (
|
|
14
|
+
allowed: Set<string>,
|
|
15
|
+
station: string,
|
|
16
|
+
accountId: string,
|
|
17
|
+
): boolean => {
|
|
18
|
+
const agentId = agentIdForAccount(station, accountId);
|
|
19
|
+
return agentId !== undefined && allowed.has(agentId);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function lineTargetDenied(
|
|
23
|
+
allowed: Set<string>,
|
|
24
|
+
args: Record<string, unknown>,
|
|
25
|
+
station?: string,
|
|
26
|
+
): boolean {
|
|
27
|
+
const line = typeof args.line === 'string' ? args.line : undefined;
|
|
28
|
+
if (line === undefined) return false;
|
|
29
|
+
const acct = accountFromLine(line);
|
|
30
|
+
if (!acct) return true;
|
|
31
|
+
if (station !== undefined && acct.station !== station) return true;
|
|
32
|
+
if (!accountInScope(allowed, acct.station, acct.accountId)) return true;
|
|
33
|
+
const override = typeof args.account === 'string' ? args.account : undefined;
|
|
34
|
+
return override !== undefined
|
|
35
|
+
? !accountInScope(allowed, acct.station, override)
|
|
36
|
+
: false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function stationFullyScoped(
|
|
40
|
+
allowed: Set<string>,
|
|
41
|
+
station: string,
|
|
42
|
+
): boolean {
|
|
43
|
+
const ids = stationAgentIds(station);
|
|
44
|
+
return ids.length > 0 && ids.every((id) => allowed.has(id));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function callTargetDenied(
|
|
48
|
+
allowed: Set<string>,
|
|
49
|
+
station: string,
|
|
50
|
+
args: Record<string, unknown>,
|
|
51
|
+
): boolean {
|
|
52
|
+
if (typeof args.line === 'string')
|
|
53
|
+
return lineTargetDenied(allowed, args, station);
|
|
54
|
+
if (typeof args.account === 'string')
|
|
55
|
+
return !accountInScope(allowed, station, args.account);
|
|
56
|
+
return !stationFullyScoped(allowed, station);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function eventInScope(allowed: Set<string>, line: string): boolean {
|
|
60
|
+
const acct = accountFromLine(line);
|
|
61
|
+
if (!acct) return false;
|
|
62
|
+
if (!stationHasAccounts(acct.station)) return true;
|
|
63
|
+
return accountInScope(allowed, acct.station, acct.accountId);
|
|
64
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { drizzle, type PostgresJsDatabase } from 'drizzle-orm/postgres-js';
|
|
2
|
+
import postgres from 'postgres';
|
|
3
|
+
import { log } from '../daemon/log.js';
|
|
4
|
+
import * as schema from './schema.js';
|
|
5
|
+
|
|
6
|
+
export type Db = PostgresJsDatabase<typeof schema>;
|
|
7
|
+
|
|
8
|
+
let sql: ReturnType<typeof postgres> | null = null;
|
|
9
|
+
let db: Db | null = null;
|
|
10
|
+
|
|
11
|
+
export function databaseUrl(): string | undefined {
|
|
12
|
+
const url = process.env.DATABASE_URL?.trim();
|
|
13
|
+
if (!url) return undefined;
|
|
14
|
+
return url;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getDb(): Db {
|
|
18
|
+
const url = databaseUrl();
|
|
19
|
+
if (!url) throw new Error('DATABASE_URL is not set');
|
|
20
|
+
if (!db) {
|
|
21
|
+
sql = postgres(url, {
|
|
22
|
+
max: 4,
|
|
23
|
+
prepare: false,
|
|
24
|
+
onnotice: (notice) => {
|
|
25
|
+
log.debug({ notice: notice.message }, 'postgres notice');
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
db = drizzle(sql, { schema });
|
|
29
|
+
}
|
|
30
|
+
return db;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function closeDb(): Promise<void> {
|
|
34
|
+
if (sql) await sql.end({ timeout: 5 });
|
|
35
|
+
sql = null;
|
|
36
|
+
db = null;
|
|
37
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { ApiError } from '../daemon/api-error.js';
|
|
2
|
+
import type {
|
|
3
|
+
ConnectorAuth,
|
|
4
|
+
VerifiedRecord,
|
|
5
|
+
VerifiedServer,
|
|
6
|
+
} from '../daemon/connector-verify.js';
|
|
7
|
+
import { readStoredTools } from '../daemon/connector-tools.js';
|
|
8
|
+
|
|
9
|
+
const CONNECTOR_NAME_MAX = 64;
|
|
10
|
+
const HEADER_NAME_RE = /^[A-Za-z0-9!#$%&'*+.^_`|~-]{1,64}$/;
|
|
11
|
+
const HEADER_VALUE_RE = /^[\x20-\x7e]{1,4096}$/;
|
|
12
|
+
const DEFAULT_HEADER = 'Authorization';
|
|
13
|
+
|
|
14
|
+
export class ConnectorError extends ApiError {}
|
|
15
|
+
|
|
16
|
+
export type ConnectorSignIn = 'connected' | 'disconnected' | null;
|
|
17
|
+
|
|
18
|
+
export interface ConnectorConfig {
|
|
19
|
+
auth: ConnectorAuth;
|
|
20
|
+
createdAt: string;
|
|
21
|
+
verified: VerifiedRecord;
|
|
22
|
+
oauth: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
26
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function text(value: unknown): string {
|
|
30
|
+
return typeof value === 'string' ? value : '';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function hasControlChar(value: string): boolean {
|
|
34
|
+
for (const ch of value) {
|
|
35
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
36
|
+
if (code < 0x20 || code === 0x7f) return true;
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function connectorName(raw: unknown): string {
|
|
42
|
+
const name = typeof raw === 'string' ? raw.trim() : '';
|
|
43
|
+
if (name === '') throw new ConnectorError('a connector needs a name', 400);
|
|
44
|
+
if (name.length > CONNECTOR_NAME_MAX)
|
|
45
|
+
throw new ConnectorError(
|
|
46
|
+
`a connector name must be ${String(CONNECTOR_NAME_MAX)} characters or fewer`,
|
|
47
|
+
400,
|
|
48
|
+
);
|
|
49
|
+
if (hasControlChar(name))
|
|
50
|
+
throw new ConnectorError(
|
|
51
|
+
'a connector name must not contain control characters',
|
|
52
|
+
400,
|
|
53
|
+
);
|
|
54
|
+
return name;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function connectorAuth(
|
|
58
|
+
rawHeader: unknown,
|
|
59
|
+
rawValue: unknown,
|
|
60
|
+
): ConnectorAuth {
|
|
61
|
+
const header = text(rawHeader).trim();
|
|
62
|
+
const value = text(rawValue).trim();
|
|
63
|
+
if (header === '' && value === '') return { kind: 'none' };
|
|
64
|
+
if (value === '')
|
|
65
|
+
throw new ConnectorError(
|
|
66
|
+
'that header has no value — give both a header name and its value, or neither',
|
|
67
|
+
400,
|
|
68
|
+
);
|
|
69
|
+
const name = header === '' ? DEFAULT_HEADER : header;
|
|
70
|
+
if (!HEADER_NAME_RE.test(name))
|
|
71
|
+
throw new ConnectorError('that is not a valid HTTP header name', 400);
|
|
72
|
+
if (!HEADER_VALUE_RE.test(value))
|
|
73
|
+
throw new ConnectorError('that header value is not sendable', 400);
|
|
74
|
+
return { kind: 'header', name, value };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function readOAuth(raw: Record<string, unknown>): ConnectorAuth {
|
|
78
|
+
const accessToken = text(raw.accessToken);
|
|
79
|
+
const clientId = text(raw.clientId);
|
|
80
|
+
const tokenEndpoint = text(raw.tokenEndpoint);
|
|
81
|
+
if (accessToken === '' || clientId === '' || tokenEndpoint === '')
|
|
82
|
+
return { kind: 'none' };
|
|
83
|
+
const refreshToken = text(raw.refreshToken);
|
|
84
|
+
const clientSecret = text(raw.clientSecret);
|
|
85
|
+
return {
|
|
86
|
+
kind: 'oauth',
|
|
87
|
+
accessToken,
|
|
88
|
+
clientId,
|
|
89
|
+
tokenEndpoint,
|
|
90
|
+
issuer: text(raw.issuer),
|
|
91
|
+
...(refreshToken === '' ? {} : { refreshToken }),
|
|
92
|
+
...(clientSecret === '' ? {} : { clientSecret }),
|
|
93
|
+
...(typeof raw.expiresAt === 'number' ? { expiresAt: raw.expiresAt } : {}),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function readAuth(raw: unknown): ConnectorAuth {
|
|
98
|
+
if (!isRecord(raw)) return { kind: 'none' };
|
|
99
|
+
if (raw.kind === 'oauth') return readOAuth(raw);
|
|
100
|
+
if (raw.kind !== 'header') return { kind: 'none' };
|
|
101
|
+
const name = text(raw.name);
|
|
102
|
+
const value = text(raw.value);
|
|
103
|
+
if (name === '' || value === '') return { kind: 'none' };
|
|
104
|
+
return { kind: 'header', name, value };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function readVerified(raw: unknown): VerifiedRecord {
|
|
108
|
+
const record = isRecord(raw) ? raw : {};
|
|
109
|
+
const catalog = readStoredTools(record.catalog);
|
|
110
|
+
return {
|
|
111
|
+
at: text(record.at),
|
|
112
|
+
server: text(record.server),
|
|
113
|
+
version: text(record.version),
|
|
114
|
+
protocol: text(record.protocol),
|
|
115
|
+
icon: text(record.icon),
|
|
116
|
+
tools: typeof record.tools === 'number' ? record.tools : 0,
|
|
117
|
+
catalog,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function readConfig(raw: unknown): ConnectorConfig {
|
|
122
|
+
const record = isRecord(raw) ? raw : {};
|
|
123
|
+
const auth = readAuth(record.auth);
|
|
124
|
+
return {
|
|
125
|
+
auth,
|
|
126
|
+
createdAt: text(record.createdAt),
|
|
127
|
+
verified: readVerified(record.verified),
|
|
128
|
+
oauth: record.oauth === true || auth.kind === 'oauth',
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function signInState(config: ConnectorConfig): ConnectorSignIn {
|
|
133
|
+
if (config.auth.kind === 'oauth') return 'connected';
|
|
134
|
+
return config.oauth ? 'disconnected' : null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function stamp(server: VerifiedServer): VerifiedRecord {
|
|
138
|
+
return {
|
|
139
|
+
at: new Date().toISOString(),
|
|
140
|
+
server: server.server,
|
|
141
|
+
version: server.version,
|
|
142
|
+
protocol: server.protocol,
|
|
143
|
+
icon: server.icon,
|
|
144
|
+
tools: server.tools,
|
|
145
|
+
catalog: server.catalog,
|
|
146
|
+
};
|
|
147
|
+
}
|