@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,373 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
rmSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from 'node:fs';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { ID_RE } from './ids.js';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { eq } from 'drizzle-orm';
|
|
12
|
+
import { log } from '../daemon/log.js';
|
|
13
|
+
import { writeSecure } from '../daemon/secure-fs.js';
|
|
14
|
+
import { isLocalMode, trainsDir } from '../daemon/paths.js';
|
|
15
|
+
import { closeDb, databaseUrl, getDb } from './client.js';
|
|
16
|
+
import {
|
|
17
|
+
setAgentMap,
|
|
18
|
+
setAllowlistMap,
|
|
19
|
+
type AgentMap,
|
|
20
|
+
type AgentNameMap,
|
|
21
|
+
type AllowlistMap,
|
|
22
|
+
} from './agent-map.js';
|
|
23
|
+
import { setKeyMap } from './key-map.js';
|
|
24
|
+
import { carryTokenStores, PREVIOUS_ACCOUNT_ID } from './token-carry.js';
|
|
25
|
+
import { STATIONS, stations, agents, type StationName } from './schema.js';
|
|
26
|
+
|
|
27
|
+
interface StationTarget {
|
|
28
|
+
file: string;
|
|
29
|
+
fileEnv: string;
|
|
30
|
+
trainImport: string | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface LoadedAccount {
|
|
34
|
+
station: StationName;
|
|
35
|
+
id: string;
|
|
36
|
+
allowlist: string[] | null;
|
|
37
|
+
config: Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface LoadedAgent {
|
|
41
|
+
id: string;
|
|
42
|
+
name: string;
|
|
43
|
+
accounts: LoadedAccount[];
|
|
44
|
+
key: string | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const METRO_DIR = join(homedir(), '.metro');
|
|
48
|
+
|
|
49
|
+
const STATION_TARGETS: Record<StationName, StationTarget> = {
|
|
50
|
+
xmtp: {
|
|
51
|
+
file: 'xmtp-accounts.json',
|
|
52
|
+
fileEnv: 'XMTP_ACCOUNTS_FILE',
|
|
53
|
+
trainImport: '@metro-labs/xmtp/train',
|
|
54
|
+
},
|
|
55
|
+
'telegram-bot': {
|
|
56
|
+
file: 'telegram-bot-accounts.json',
|
|
57
|
+
fileEnv: 'TELEGRAM_BOT_ACCOUNTS_FILE',
|
|
58
|
+
trainImport: '@metro-labs/telegram-bot/train',
|
|
59
|
+
},
|
|
60
|
+
'telegram': {
|
|
61
|
+
file: 'telegram-accounts.json',
|
|
62
|
+
fileEnv: 'TELEGRAM_ACCOUNTS_FILE',
|
|
63
|
+
trainImport: '@metro-labs/telegram/train',
|
|
64
|
+
},
|
|
65
|
+
'discord-bot': {
|
|
66
|
+
file: 'discord-bot-accounts.json',
|
|
67
|
+
fileEnv: 'DISCORD_BOT_ACCOUNTS_FILE',
|
|
68
|
+
trainImport: '@metro-labs/discord-bot/train',
|
|
69
|
+
},
|
|
70
|
+
whatsapp: {
|
|
71
|
+
file: 'whatsapp-accounts.json',
|
|
72
|
+
fileEnv: 'WHATSAPP_ACCOUNTS_FILE',
|
|
73
|
+
trainImport: '@metro-labs/whatsapp/train',
|
|
74
|
+
},
|
|
75
|
+
webhook: {
|
|
76
|
+
file: 'webhook-accounts.json',
|
|
77
|
+
fileEnv: 'WEBHOOK_ACCOUNTS_FILE',
|
|
78
|
+
trainImport: null,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
function accountFilePath(station: StationName): string {
|
|
83
|
+
const target = STATION_TARGETS[station];
|
|
84
|
+
return process.env[target.fileEnv] ?? join(METRO_DIR, target.file);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function agentFilter(): string | undefined {
|
|
88
|
+
const v = process.env.METRO_AGENT?.trim();
|
|
89
|
+
if (!v) return undefined;
|
|
90
|
+
if (!ID_RE.test(v))
|
|
91
|
+
throw new Error(`METRO_AGENT must be an 11-character agent id, got '${v}'`);
|
|
92
|
+
return v;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type StationSource = () => Promise<LoadedAgent[]>;
|
|
96
|
+
|
|
97
|
+
export const MOVABLE_STATIONS = new Set<StationName>(
|
|
98
|
+
STATIONS.filter((s) => s !== 'webhook'),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
export function unmovableStations(list: LoadedAccount[]): StationName[] {
|
|
102
|
+
return [
|
|
103
|
+
...new Set(
|
|
104
|
+
list
|
|
105
|
+
.map((a) => a.station)
|
|
106
|
+
.filter((station) => !MOVABLE_STATIONS.has(station)),
|
|
107
|
+
),
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function loadAllStationsFor(
|
|
112
|
+
agentId: string,
|
|
113
|
+
): Promise<LoadedAccount[]> {
|
|
114
|
+
const rows = await getDb()
|
|
115
|
+
.select()
|
|
116
|
+
.from(stations)
|
|
117
|
+
.where(eq(stations.agentId, agentId));
|
|
118
|
+
return rows.map((r) => ({
|
|
119
|
+
station: r.station,
|
|
120
|
+
id: r.id,
|
|
121
|
+
allowlist: r.allowlist,
|
|
122
|
+
config: r.config as Record<string, unknown>,
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function loadAgentForRuntime(
|
|
127
|
+
agentId: string,
|
|
128
|
+
): Promise<LoadedAgent> {
|
|
129
|
+
const db = getDb();
|
|
130
|
+
const rows = await db.select().from(agents).where(eq(agents.id, agentId));
|
|
131
|
+
const a = rows[0];
|
|
132
|
+
if (a === undefined) throw new Error(`no such agent '${agentId}'`);
|
|
133
|
+
const acctRows = await db
|
|
134
|
+
.select()
|
|
135
|
+
.from(stations)
|
|
136
|
+
.where(eq(stations.agentId, a.id));
|
|
137
|
+
return {
|
|
138
|
+
id: a.id,
|
|
139
|
+
name: a.name,
|
|
140
|
+
key: a.key,
|
|
141
|
+
accounts: acctRows
|
|
142
|
+
.filter((r) => MOVABLE_STATIONS.has(r.station))
|
|
143
|
+
.map((r) => ({
|
|
144
|
+
station: r.station,
|
|
145
|
+
id: r.id,
|
|
146
|
+
allowlist: r.allowlist,
|
|
147
|
+
config: r.config as Record<string, unknown>,
|
|
148
|
+
})),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export const pgSource: StationSource = () => loadAgents();
|
|
153
|
+
|
|
154
|
+
async function loadAgents(): Promise<LoadedAgent[]> {
|
|
155
|
+
const db = getDb();
|
|
156
|
+
const only = agentFilter();
|
|
157
|
+
const agentRows =
|
|
158
|
+
only !== undefined
|
|
159
|
+
? await db.select().from(agents).where(eq(agents.id, only))
|
|
160
|
+
: await db.select().from(agents);
|
|
161
|
+
if (only !== undefined && agentRows.length === 0)
|
|
162
|
+
throw new Error(`METRO_AGENT=${only} does not match any agent`);
|
|
163
|
+
|
|
164
|
+
const out: LoadedAgent[] = [];
|
|
165
|
+
for (const a of agentRows) {
|
|
166
|
+
const acctRows = await db
|
|
167
|
+
.select()
|
|
168
|
+
.from(stations)
|
|
169
|
+
.where(eq(stations.agentId, a.id));
|
|
170
|
+
out.push({
|
|
171
|
+
id: a.id,
|
|
172
|
+
name: a.name,
|
|
173
|
+
accounts: acctRows.map((r) => ({
|
|
174
|
+
station: r.station,
|
|
175
|
+
id: r.id,
|
|
176
|
+
allowlist: r.allowlist,
|
|
177
|
+
config: r.config as Record<string, unknown>,
|
|
178
|
+
})),
|
|
179
|
+
key: a.runtimeId === null ? a.key : null,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function trainConfig(
|
|
186
|
+
config: Record<string, unknown>,
|
|
187
|
+
): Record<string, unknown> {
|
|
188
|
+
return Object.fromEntries(
|
|
189
|
+
Object.entries(config).filter(([key]) => key !== PREVIOUS_ACCOUNT_ID),
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function trainStubPath(station: StationName): string {
|
|
194
|
+
return join(trainsDir(), `${station}.ts`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function currentText(path: string): string | null {
|
|
198
|
+
try {
|
|
199
|
+
return readFileSync(path, 'utf8');
|
|
200
|
+
} catch {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function writeIfChanged(path: string, content: string): boolean {
|
|
206
|
+
if (currentText(path) === content) return false;
|
|
207
|
+
writeFileSync(path, content);
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export const isLocalRuntime = (): boolean =>
|
|
212
|
+
(process.env.METRO_RUN_TOKEN?.trim() ?? '') !== '';
|
|
213
|
+
|
|
214
|
+
export const stationRunsHere = (station: StationName): boolean =>
|
|
215
|
+
isLocalRuntime() || isLocalMode() || !MOVABLE_STATIONS.has(station);
|
|
216
|
+
|
|
217
|
+
interface WrittenStations {
|
|
218
|
+
active: Map<StationName, number>;
|
|
219
|
+
changed: StationName[];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function writeStations(list: LoadedAgent[]): WrittenStations {
|
|
223
|
+
mkdirSync(METRO_DIR, { recursive: true });
|
|
224
|
+
mkdirSync(trainsDir(), { recursive: true });
|
|
225
|
+
|
|
226
|
+
const byStation = new Map<StationName, LoadedAccount[]>();
|
|
227
|
+
const map: AgentMap = {};
|
|
228
|
+
const names: AgentNameMap = {};
|
|
229
|
+
const allow: AllowlistMap = {};
|
|
230
|
+
for (const agent of list) {
|
|
231
|
+
names[agent.id] = agent.name;
|
|
232
|
+
for (const a of agent.accounts) {
|
|
233
|
+
if (!(a.station in STATION_TARGETS)) {
|
|
234
|
+
log.error(
|
|
235
|
+
{ station: a.station, id: a.id },
|
|
236
|
+
'unknown station — this metro is older or newer than the row; skipping it',
|
|
237
|
+
);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
map[`${a.station}/${a.id}`] = agent.id;
|
|
241
|
+
if (a.allowlist) allow[`${a.station}/${a.id}`] = a.allowlist;
|
|
242
|
+
if (stationRunsHere(a.station)) {
|
|
243
|
+
const cur = byStation.get(a.station);
|
|
244
|
+
if (cur) cur.push(a);
|
|
245
|
+
else byStation.set(a.station, [a]);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
setAgentMap(map, names);
|
|
250
|
+
setAllowlistMap(allow);
|
|
251
|
+
carryTokenStores(list.flatMap((agent) => agent.accounts));
|
|
252
|
+
|
|
253
|
+
const active = new Map<StationName, number>();
|
|
254
|
+
const changed: StationName[] = [];
|
|
255
|
+
for (const [station, accts] of byStation) {
|
|
256
|
+
const records = accts.map((a) => ({ id: a.id, ...trainConfig(a.config) }));
|
|
257
|
+
const path = accountFilePath(station);
|
|
258
|
+
const content = JSON.stringify(records, null, 2);
|
|
259
|
+
if (currentText(path) !== content) {
|
|
260
|
+
writeSecure(path, content);
|
|
261
|
+
changed.push(station);
|
|
262
|
+
}
|
|
263
|
+
const { trainImport } = STATION_TARGETS[station];
|
|
264
|
+
if (trainImport !== null)
|
|
265
|
+
writeIfChanged(trainStubPath(station), `import '${trainImport}';\n`);
|
|
266
|
+
active.set(station, accts.length);
|
|
267
|
+
}
|
|
268
|
+
return { active, changed };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const BLANKED = '[]\n';
|
|
272
|
+
|
|
273
|
+
function pruneStations(present: Map<StationName, number>): StationName[] {
|
|
274
|
+
const removed: StationName[] = [];
|
|
275
|
+
for (const station of Object.keys(STATION_TARGETS) as StationName[]) {
|
|
276
|
+
if (present.has(station)) continue;
|
|
277
|
+
const hasTrain = STATION_TARGETS[station].trainImport !== null;
|
|
278
|
+
const stub = trainStubPath(station);
|
|
279
|
+
const stubGone = !hasTrain || !existsSync(stub);
|
|
280
|
+
const file = currentText(accountFilePath(station));
|
|
281
|
+
const fileInert = file === null || file === BLANKED;
|
|
282
|
+
if (stubGone && fileInert) continue;
|
|
283
|
+
if (!fileInert) writeSecure(accountFilePath(station), BLANKED);
|
|
284
|
+
if (hasTrain) rmSync(stub, { force: true });
|
|
285
|
+
removed.push(station);
|
|
286
|
+
}
|
|
287
|
+
return removed;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const stationLabels = (m: Map<StationName, number>): string[] =>
|
|
291
|
+
[...m].map(([station, n]) => `${station}(${n})`);
|
|
292
|
+
|
|
293
|
+
function applyKeyMap(list: LoadedAgent[]): void {
|
|
294
|
+
lastKey = list.find((a) => a.key !== null)?.key ?? null;
|
|
295
|
+
setKeyMap(
|
|
296
|
+
list.flatMap((agent) =>
|
|
297
|
+
agent.key === null ? [] : [{ key: agent.key, agentId: agent.id }],
|
|
298
|
+
),
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
let lastKey: string | null = null;
|
|
303
|
+
|
|
304
|
+
export function localAgentKey(): string | null {
|
|
305
|
+
return lastKey;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function loadAndWrite(
|
|
309
|
+
source: StationSource,
|
|
310
|
+
): Promise<WrittenStations & { agents: number }> {
|
|
311
|
+
const list = await source();
|
|
312
|
+
applyKeyMap(list);
|
|
313
|
+
return { ...writeStations(list), agents: list.length };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export interface MaterializeOptions {
|
|
317
|
+
allowEmpty?: boolean;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export async function materializeFrom(
|
|
321
|
+
source: StationSource,
|
|
322
|
+
opts: MaterializeOptions = {},
|
|
323
|
+
): Promise<void> {
|
|
324
|
+
const { active, agents: found } = await loadAndWrite(source);
|
|
325
|
+
|
|
326
|
+
if (found === 0 && opts.allowEmpty !== true)
|
|
327
|
+
throw new Error('no agents found — nothing to materialize');
|
|
328
|
+
const removed = pruneStations(active);
|
|
329
|
+
log.info(
|
|
330
|
+
{ stations: stationLabels(active), agents: found, removed },
|
|
331
|
+
'materialized station accounts',
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export async function materializeFromDb(): Promise<void> {
|
|
336
|
+
if (!databaseUrl())
|
|
337
|
+
throw new Error('DATABASE_URL is not set — accounts load from Postgres');
|
|
338
|
+
try {
|
|
339
|
+
await materializeFrom(pgSource);
|
|
340
|
+
} finally {
|
|
341
|
+
await closeDb();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export interface ReloadedStations {
|
|
346
|
+
active: StationName[];
|
|
347
|
+
removed: StationName[];
|
|
348
|
+
changed: StationName[];
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export async function reloadFrom(
|
|
352
|
+
source: StationSource,
|
|
353
|
+
): Promise<ReloadedStations> {
|
|
354
|
+
const { active, changed } = await loadAndWrite(source);
|
|
355
|
+
const removed = pruneStations(active);
|
|
356
|
+
if (removed.length > 0 || changed.length > 0)
|
|
357
|
+
log.info(
|
|
358
|
+
{ stations: stationLabels(active), removed, changed },
|
|
359
|
+
'reloaded station accounts',
|
|
360
|
+
);
|
|
361
|
+
return { active: [...active.keys()], removed, changed };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export async function reloadAccountsFromDb(): Promise<ReloadedStations> {
|
|
365
|
+
if (!databaseUrl())
|
|
366
|
+
throw new Error('DATABASE_URL is not set — accounts load from Postgres');
|
|
367
|
+
return reloadFrom(pgSource);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (import.meta.main) {
|
|
371
|
+
await materializeFromDb();
|
|
372
|
+
process.exit(0);
|
|
373
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ensureUserByAddress,
|
|
3
|
+
normalizeAddress,
|
|
4
|
+
userIdForSubject,
|
|
5
|
+
} from './users.js';
|
|
6
|
+
import { and, asc, eq } from 'drizzle-orm';
|
|
7
|
+
import { ApiError } from '../daemon/api-error.js';
|
|
8
|
+
import { getDb } from './client.js';
|
|
9
|
+
import { newId } from './ids.js';
|
|
10
|
+
import { projectMembers, projects, users, type ProjectRole } from './schema.js';
|
|
11
|
+
|
|
12
|
+
export class ProjectError extends ApiError {}
|
|
13
|
+
|
|
14
|
+
export interface Project {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
isDefault: boolean;
|
|
18
|
+
owner: boolean;
|
|
19
|
+
role: ProjectRole;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface Member {
|
|
23
|
+
id: string;
|
|
24
|
+
address: string | null;
|
|
25
|
+
email: string | null;
|
|
26
|
+
role: ProjectRole;
|
|
27
|
+
owner: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const NAME_MAX = 64;
|
|
31
|
+
const CONTROL = /[\u0000-\u001f\u007f]/;
|
|
32
|
+
|
|
33
|
+
const missing = (): ProjectError => new ProjectError('no such project', 404);
|
|
34
|
+
const forbidden = (why: string): ProjectError => new ProjectError(why, 403);
|
|
35
|
+
|
|
36
|
+
export function projectName(raw: unknown): string {
|
|
37
|
+
const name = typeof raw === 'string' ? raw.trim() : '';
|
|
38
|
+
if (name === '' || name.length > NAME_MAX)
|
|
39
|
+
throw new ProjectError(
|
|
40
|
+
`a project name is 1-${String(NAME_MAX)} characters`,
|
|
41
|
+
400,
|
|
42
|
+
);
|
|
43
|
+
if (CONTROL.test(name))
|
|
44
|
+
throw new ProjectError('a project name cannot carry control characters', 400);
|
|
45
|
+
return name;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface Access {
|
|
49
|
+
userId: string;
|
|
50
|
+
projectId: string;
|
|
51
|
+
ownerId: string;
|
|
52
|
+
isDefault: boolean;
|
|
53
|
+
name: string;
|
|
54
|
+
role: ProjectRole;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function memberAccessOrThrow(
|
|
58
|
+
subject: string,
|
|
59
|
+
projectId: string,
|
|
60
|
+
): Promise<Access> {
|
|
61
|
+
const userId = await userIdForSubject(subject);
|
|
62
|
+
if (userId === null) throw missing();
|
|
63
|
+
const rows = await getDb()
|
|
64
|
+
.select({
|
|
65
|
+
id: projects.id,
|
|
66
|
+
name: projects.name,
|
|
67
|
+
ownerId: projects.ownerId,
|
|
68
|
+
isDefault: projects.isDefault,
|
|
69
|
+
role: projectMembers.role,
|
|
70
|
+
})
|
|
71
|
+
.from(projects)
|
|
72
|
+
.innerJoin(projectMembers, eq(projectMembers.projectId, projects.id))
|
|
73
|
+
.where(and(eq(projects.id, projectId), eq(projectMembers.userId, userId)));
|
|
74
|
+
const row = rows[0];
|
|
75
|
+
if (row === undefined) throw missing();
|
|
76
|
+
return {
|
|
77
|
+
userId,
|
|
78
|
+
projectId: row.id,
|
|
79
|
+
ownerId: row.ownerId,
|
|
80
|
+
isDefault: row.isDefault,
|
|
81
|
+
name: row.name,
|
|
82
|
+
role: row.role,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function projectIdOrThrow(
|
|
87
|
+
subject: string,
|
|
88
|
+
projectId: string,
|
|
89
|
+
): Promise<string> {
|
|
90
|
+
return (await memberAccessOrThrow(subject, projectId)).projectId;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function addMemberRow(
|
|
94
|
+
projectId: string,
|
|
95
|
+
userId: string,
|
|
96
|
+
role: ProjectRole,
|
|
97
|
+
): Promise<void> {
|
|
98
|
+
await getDb()
|
|
99
|
+
.insert(projectMembers)
|
|
100
|
+
.values({ id: newId(), projectId, userId, role })
|
|
101
|
+
.onConflictDoNothing();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function ensureDefaultProject(userId: string): Promise<string> {
|
|
105
|
+
const existing = await getDb()
|
|
106
|
+
.select({ id: projects.id })
|
|
107
|
+
.from(projects)
|
|
108
|
+
.where(and(eq(projects.ownerId, userId), eq(projects.isDefault, true)));
|
|
109
|
+
const found = existing[0];
|
|
110
|
+
if (found !== undefined) return found.id;
|
|
111
|
+
const id = newId();
|
|
112
|
+
await getDb()
|
|
113
|
+
.insert(projects)
|
|
114
|
+
.values({ id, name: 'Personal', ownerId: userId, isDefault: true });
|
|
115
|
+
await addMemberRow(id, userId, 'admin');
|
|
116
|
+
return id;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function ensureUserWithProject(address: string): Promise<string> {
|
|
120
|
+
const userId = await ensureUserByAddress(address);
|
|
121
|
+
await ensureDefaultProject(userId);
|
|
122
|
+
return userId;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function listProjectsForUser(subject: string): Promise<Project[]> {
|
|
126
|
+
const userId = await userIdForSubject(subject);
|
|
127
|
+
if (userId === null) return [];
|
|
128
|
+
const rows = await getDb()
|
|
129
|
+
.select({
|
|
130
|
+
id: projects.id,
|
|
131
|
+
name: projects.name,
|
|
132
|
+
isDefault: projects.isDefault,
|
|
133
|
+
ownerId: projects.ownerId,
|
|
134
|
+
role: projectMembers.role,
|
|
135
|
+
})
|
|
136
|
+
.from(projects)
|
|
137
|
+
.innerJoin(projectMembers, eq(projectMembers.projectId, projects.id))
|
|
138
|
+
.where(eq(projectMembers.userId, userId))
|
|
139
|
+
.orderBy(asc(projects.id));
|
|
140
|
+
return rows.map((r) => ({
|
|
141
|
+
id: r.id,
|
|
142
|
+
name: r.name,
|
|
143
|
+
isDefault: r.isDefault,
|
|
144
|
+
owner: r.ownerId === userId,
|
|
145
|
+
role: r.role,
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function createProjectForUser(
|
|
150
|
+
subject: string,
|
|
151
|
+
raw: unknown,
|
|
152
|
+
): Promise<Project> {
|
|
153
|
+
const name = projectName(raw);
|
|
154
|
+
const userId = await userIdForSubject(subject);
|
|
155
|
+
if (userId === null) throw new ProjectError('unknown account', 401);
|
|
156
|
+
const id = newId();
|
|
157
|
+
await getDb().insert(projects).values({ id, name, ownerId: userId });
|
|
158
|
+
await addMemberRow(id, userId, 'admin');
|
|
159
|
+
return { id, name, isDefault: false, owner: true, role: 'admin' };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function renameProjectForUser(
|
|
163
|
+
subject: string,
|
|
164
|
+
projectId: string,
|
|
165
|
+
raw: unknown,
|
|
166
|
+
): Promise<Project> {
|
|
167
|
+
const name = projectName(raw);
|
|
168
|
+
const access = await memberAccessOrThrow(subject, projectId);
|
|
169
|
+
if (access.role !== 'admin')
|
|
170
|
+
throw forbidden('only an admin can rename a project');
|
|
171
|
+
await getDb()
|
|
172
|
+
.update(projects)
|
|
173
|
+
.set({ name })
|
|
174
|
+
.where(eq(projects.id, access.projectId));
|
|
175
|
+
return {
|
|
176
|
+
id: access.projectId,
|
|
177
|
+
name,
|
|
178
|
+
isDefault: access.isDefault,
|
|
179
|
+
owner: access.ownerId === access.userId,
|
|
180
|
+
role: access.role,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function deleteProjectForUser(
|
|
185
|
+
subject: string,
|
|
186
|
+
projectId: string,
|
|
187
|
+
): Promise<{ id: string; name: string }> {
|
|
188
|
+
const access = await memberAccessOrThrow(subject, projectId);
|
|
189
|
+
if (access.ownerId !== access.userId)
|
|
190
|
+
throw forbidden('only the owner can delete a project');
|
|
191
|
+
if (access.isDefault)
|
|
192
|
+
throw forbidden('your default project cannot be deleted');
|
|
193
|
+
try {
|
|
194
|
+
await getDb().delete(projects).where(eq(projects.id, access.projectId));
|
|
195
|
+
} catch {
|
|
196
|
+
throw new ProjectError(
|
|
197
|
+
'this project still holds agents or connectors',
|
|
198
|
+
409,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
return { id: access.projectId, name: access.name };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function listMembersForUser(
|
|
205
|
+
subject: string,
|
|
206
|
+
projectId: string,
|
|
207
|
+
): Promise<Member[]> {
|
|
208
|
+
const access = await memberAccessOrThrow(subject, projectId);
|
|
209
|
+
const rows = await getDb()
|
|
210
|
+
.select({
|
|
211
|
+
id: projectMembers.id,
|
|
212
|
+
address: users.address,
|
|
213
|
+
email: users.email,
|
|
214
|
+
role: projectMembers.role,
|
|
215
|
+
userId: projectMembers.userId,
|
|
216
|
+
})
|
|
217
|
+
.from(projectMembers)
|
|
218
|
+
.innerJoin(users, eq(users.id, projectMembers.userId))
|
|
219
|
+
.where(eq(projectMembers.projectId, access.projectId))
|
|
220
|
+
.orderBy(asc(projectMembers.id));
|
|
221
|
+
return rows.map((r) => ({
|
|
222
|
+
id: r.id,
|
|
223
|
+
address: r.address,
|
|
224
|
+
email: r.email,
|
|
225
|
+
role: r.role,
|
|
226
|
+
owner: r.userId === access.ownerId,
|
|
227
|
+
}));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function readRole(raw: unknown): ProjectRole {
|
|
231
|
+
if (raw === 'admin' || raw === 'member') return raw;
|
|
232
|
+
throw new ProjectError("a role is 'admin' or 'member'", 400);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function adminAccessOrThrow(
|
|
236
|
+
subject: string,
|
|
237
|
+
projectId: string,
|
|
238
|
+
): Promise<Access> {
|
|
239
|
+
const access = await memberAccessOrThrow(subject, projectId);
|
|
240
|
+
if (access.role !== 'admin')
|
|
241
|
+
throw forbidden('only an admin can manage members');
|
|
242
|
+
return access;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export async function addMemberForUser(
|
|
246
|
+
subject: string,
|
|
247
|
+
projectId: string,
|
|
248
|
+
raw: unknown,
|
|
249
|
+
rawRole: unknown,
|
|
250
|
+
): Promise<Member[]> {
|
|
251
|
+
const access = await adminAccessOrThrow(subject, projectId);
|
|
252
|
+
const role = readRole(rawRole);
|
|
253
|
+
const invited = normalizeAddress(typeof raw === 'string' ? raw : '');
|
|
254
|
+
if (invited === null)
|
|
255
|
+
throw new ProjectError('an Ethereum address is required', 400);
|
|
256
|
+
const invitedId = await ensureUserByAddress(invited);
|
|
257
|
+
await ensureDefaultProject(invitedId);
|
|
258
|
+
await addMemberRow(access.projectId, invitedId, role);
|
|
259
|
+
return listMembersForUser(subject, projectId);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function memberRowOrThrow(
|
|
263
|
+
projectId: string,
|
|
264
|
+
memberId: string,
|
|
265
|
+
): Promise<{ userId: string }> {
|
|
266
|
+
const rows = await getDb()
|
|
267
|
+
.select({ userId: projectMembers.userId })
|
|
268
|
+
.from(projectMembers)
|
|
269
|
+
.where(
|
|
270
|
+
and(
|
|
271
|
+
eq(projectMembers.id, memberId),
|
|
272
|
+
eq(projectMembers.projectId, projectId),
|
|
273
|
+
),
|
|
274
|
+
);
|
|
275
|
+
const row = rows[0];
|
|
276
|
+
if (row === undefined) throw new ProjectError('no such member', 404);
|
|
277
|
+
return row;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export async function setMemberRoleForUser(
|
|
281
|
+
subject: string,
|
|
282
|
+
projectId: string,
|
|
283
|
+
memberId: string,
|
|
284
|
+
rawRole: unknown,
|
|
285
|
+
): Promise<Member[]> {
|
|
286
|
+
const access = await adminAccessOrThrow(subject, projectId);
|
|
287
|
+
const role = readRole(rawRole);
|
|
288
|
+
const member = await memberRowOrThrow(access.projectId, memberId);
|
|
289
|
+
if (member.userId === access.ownerId)
|
|
290
|
+
throw forbidden('the owner is always an admin');
|
|
291
|
+
await getDb()
|
|
292
|
+
.update(projectMembers)
|
|
293
|
+
.set({ role })
|
|
294
|
+
.where(eq(projectMembers.id, memberId));
|
|
295
|
+
return listMembersForUser(subject, projectId);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export async function removeMemberForUser(
|
|
299
|
+
subject: string,
|
|
300
|
+
projectId: string,
|
|
301
|
+
memberId: string,
|
|
302
|
+
): Promise<Member[]> {
|
|
303
|
+
const access = await adminAccessOrThrow(subject, projectId);
|
|
304
|
+
const member = await memberRowOrThrow(access.projectId, memberId);
|
|
305
|
+
if (member.userId === access.ownerId)
|
|
306
|
+
throw forbidden('the owner cannot be removed from their own project');
|
|
307
|
+
await getDb().delete(projectMembers).where(eq(projectMembers.id, memberId));
|
|
308
|
+
return listMembersForUser(subject, projectId);
|
|
309
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ownedAgentOrThrow } from './agent-admin.js';
|
|
2
|
+
import { mintAgentCode } from '../daemon/agent-pair.js';
|
|
3
|
+
import { loadAllStationsFor, unmovableStations } from './materialize.js';
|
|
4
|
+
import { releaseRuntime } from './runtimes.js';
|
|
5
|
+
|
|
6
|
+
export async function mintAgentCodeForUser(
|
|
7
|
+
subject: string,
|
|
8
|
+
agentId: string,
|
|
9
|
+
): Promise<{ code: string; expiresAt: number; agent: string }> {
|
|
10
|
+
const { agent } = await ownedAgentOrThrow(subject, agentId);
|
|
11
|
+
return { ...mintAgentCode({ subject, agentId: agent.id }), agent: agent.name };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function blockedStationsFor(agentId: string): Promise<string[]> {
|
|
15
|
+
return unmovableStations(await loadAllStationsFor(agentId));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function releaseRuntimeForUser(
|
|
19
|
+
subject: string,
|
|
20
|
+
agentId: string,
|
|
21
|
+
): Promise<void> {
|
|
22
|
+
const { agent } = await ownedAgentOrThrow(subject, agentId);
|
|
23
|
+
await releaseRuntime(agent.id);
|
|
24
|
+
}
|