@stage-labs/metro 0.1.0-beta.69 → 0.1.0-beta.70

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage-labs/metro",
3
- "version": "0.1.0-beta.69",
3
+ "version": "0.1.0-beta.70",
4
4
  "description": "The metro command line. Sign in once per machine, then hand your MCP connector list to Claude Code without the credentials touching disk, argv or shell history.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -32,7 +32,6 @@
32
32
  "scripts": {
33
33
  "start": "bun src/server.ts",
34
34
  "build": "tsc",
35
- "db:generate": "drizzle-kit generate",
36
35
  "db:migrate": "drizzle-kit migrate",
37
36
  "test": "tsc --noEmit && DYLD_FALLBACK_LIBRARY_PATH=/usr/lib METRO_STATE_DIR=\"$(mktemp -d /tmp/metro-test.XXXXXX)\" bun test test/"
38
37
  },
@@ -1,4 +1,3 @@
1
- import type { ConnectorSummary } from '../db/connectors.js';
2
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
3
2
  import { errMsg, log } from './log.js';
4
3
  import {
@@ -12,7 +11,6 @@ import {
12
11
  type ApiSession,
13
12
  } from './api-http.js';
14
13
  import { parseId } from '../db/ids.js';
15
- import type { RelayServerEntry } from './connector-json.js';
16
14
  import {
17
15
  handleCallback,
18
16
  handleConnect,
@@ -35,8 +33,6 @@ const asText = (value: unknown): string =>
35
33
 
36
34
  export interface ConnectorApiDeps extends OAuthRouteDeps {
37
35
  listConnectors: (subject: string, project: string) => Promise<Connector[]>;
38
- connectorSummariesByIds: (ids: string[]) => Promise<ConnectorSummary[]>;
39
- connectorNamesByIds: (ids: string[]) => Promise<RelayServerEntry[]>;
40
36
  createConnector: (
41
37
  subject: string,
42
38
  project: string,
@@ -4,14 +4,12 @@ import { cors, sendJson, type AgentIdentity } from './api-http.js';
4
4
  import { publicBaseOrDefault } from './attach-serve.js';
5
5
  import { relayServersJson, type RelayServerEntry } from './connector-json.js';
6
6
  import { agentIdForKey } from '../db/key-map.js';
7
- import type { ConnectorSummary } from '../db/connectors.js';
8
7
 
9
- const PREFIX = '/api/cli';
8
+ const PATH = '/api/cli/mcp';
10
9
 
11
10
  export interface LocalCliDeps {
12
11
  agentName: (agentId: string) => string | null;
13
12
  connectorEntries: (agentId: string) => Promise<RelayServerEntry[]>;
14
- connectorSummaries: (agentId: string) => Promise<ConnectorSummary[]>;
15
13
  }
16
14
 
17
15
  function keyOf(req: IncomingMessage): string {
@@ -27,25 +25,18 @@ export function keyIdentity(req: IncomingMessage): AgentIdentity | null {
27
25
  return agentId === undefined ? null : { subject: 'agent-key', agentId };
28
26
  }
29
27
 
30
- async function answer(path: string, key: string, agentId: string, deps: LocalCliDeps): Promise<unknown> {
31
- const agent = deps.agentName(agentId) ?? '';
32
- if (path === `${PREFIX}/session`) return { subject: 'this machine', agent };
33
- if (path === `${PREFIX}/connectors`) return { agent, connectors: await deps.connectorSummaries(agentId) };
34
- return { json: relayServersJson(await deps.connectorEntries(agentId), publicBaseOrDefault(), key), agent };
35
- }
36
-
37
28
  export function handleLocalCliRequest(
38
29
  req: IncomingMessage,
39
30
  res: ServerResponse,
40
31
  deps: LocalCliDeps,
41
32
  ): boolean {
42
33
  const path = (req.url ?? '').split('?')[0] ?? '';
43
- if (path !== PREFIX && !path.startsWith(`${PREFIX}/`)) return false;
34
+ if (path !== PATH && !path.startsWith('/api/cli/')) return false;
44
35
  if (req.method === 'OPTIONS') {
45
36
  res.writeHead(204, cors(req)).end();
46
37
  return true;
47
38
  }
48
- if (![`${PREFIX}/mcp`, `${PREFIX}/session`, `${PREFIX}/connectors`].includes(path)) {
39
+ if (path !== PATH) {
49
40
  sendJson(req, res, 404, { error: 'not on a local daemon' });
50
41
  return true;
51
42
  }
@@ -58,9 +49,11 @@ export function handleLocalCliRequest(
58
49
  sendJson(req, res, 401, { error: 'unauthorized' });
59
50
  return true;
60
51
  }
61
- answer(path, keyOf(req), who.agentId, deps)
62
- .then((body) => {
63
- sendJson(req, res, 200, body);
52
+ deps
53
+ .connectorEntries(who.agentId)
54
+ .then((entries) => {
55
+ const agent = deps.agentName(who.agentId) ?? '';
56
+ sendJson(req, res, 200, { json: relayServersJson(entries, publicBaseOrDefault(), keyOf(req)), agent });
64
57
  })
65
58
  .catch((err: unknown) => {
66
59
  log.warn({ err: errMsg(err) }, 'local cli: request failed');
@@ -3,7 +3,6 @@ import { errMsg, log } from './log.js';
3
3
  import { AttachSessions } from './attach-session.js';
4
4
  import type { AgentApiDeps } from './agent-api.js';
5
5
  import { ATTACHABLE, type AccountApiDeps } from './account-api.js';
6
- import type { AgentConnectorApiDeps } from './agent-connector-api.js';
7
6
  import type { SessionApis } from './session-apis.js';
8
7
  import type { ModeInfo } from './mode-api.js';
9
8
  import { loadedAgentOf, type AgentBundle, type BundleApiDeps } from './bundle-api.js';
@@ -15,10 +14,7 @@ import type { RelayApiDeps } from './relay.js';
15
14
  import {
16
15
  readLocalConnectors,
17
16
  localImportConnectors,
18
- localAddConnector,
19
- localAgentConnectors,
20
17
  localConnectorNamesByIds,
21
- localConnectorSummariesByIds,
22
18
  localCreateConnector,
23
19
  localCreatePendingConnector,
24
20
  localDeleteConnector,
@@ -27,13 +23,11 @@ import {
27
23
  localListConnectors,
28
24
  localReconnectConnector,
29
25
  localRelayTarget,
30
- localRemoveConnector,
31
26
  localRenameConnector,
32
27
  localVerifyConnector,
33
28
  } from '../db/local-connectors.js';
34
29
  import {
35
30
  assertLocalOwner,
36
- connectorIdsOfLocalAgent,
37
31
  LOCAL_PROJECT_ID,
38
32
  localAttachAccount,
39
33
  localCreateAgent,
@@ -106,20 +100,15 @@ function agentApi(deps: LocalModeDeps): AgentApiDeps {
106
100
  };
107
101
  }
108
102
 
103
+ const allConnectorIds = (): string[] => readLocalConnectors().map((c) => c.id);
104
+
109
105
  function connectorIdsOfLocalAgents(ids: string[]): Promise<Map<string, string[]>> {
110
- return Promise.resolve(new Map(ids.map((id) => [id, connectorIdsOfLocalAgent(id) ?? []] as const)));
106
+ const all = allConnectorIds();
107
+ return Promise.resolve(new Map(ids.map((id) => [id, all] as const)));
111
108
  }
112
109
 
113
- const agentConnectorApi: AgentConnectorApiDeps = {
114
- agentConnectors: localAgentConnectors,
115
- addConnector: localAddConnector,
116
- removeConnector: localRemoveConnector,
117
- };
118
-
119
110
  const connectorApi: ConnectorApiDeps = {
120
111
  listConnectors: localListConnectors,
121
- connectorSummariesByIds: (ids) => localConnectorSummariesByIds(ids),
122
- connectorNamesByIds: (ids) => localConnectorNamesByIds(ids),
123
112
  createConnector: localCreateConnector,
124
113
  verifyConnector: localVerifyConnector,
125
114
  disconnectConnector: localDisconnectConnector,
@@ -131,7 +120,7 @@ const connectorApi: ConnectorApiDeps = {
131
120
  };
132
121
 
133
122
  const relayApi: RelayApiDeps = {
134
- target: (agentId, connectorId, force) => localRelayTarget(agentId, connectorId, force),
123
+ target: (connectorId, force) => localRelayTarget(connectorId, force),
135
124
  identify: keyIdentity,
136
125
  };
137
126
 
@@ -143,8 +132,7 @@ function agentNameOf(agentId: string): string | null {
143
132
 
144
133
  const localCli: LocalCliDeps = {
145
134
  agentName: agentNameOf,
146
- connectorEntries: (agentId) => localConnectorNamesByIds(connectorIdsOfLocalAgent(agentId) ?? []),
147
- connectorSummaries: (agentId) => localConnectorSummariesByIds(connectorIdsOfLocalAgent(agentId) ?? []),
135
+ connectorEntries: () => localConnectorNamesByIds(allConnectorIds()),
148
136
  };
149
137
 
150
138
  function bundleApi(deps: LocalModeDeps): BundleApiDeps {
@@ -152,13 +140,10 @@ function bundleApi(deps: LocalModeDeps): BundleApiDeps {
152
140
  bundle: async (subject, agentId) => {
153
141
  const { agent } = await localOwnedAgentOrThrow(subject, agentId);
154
142
  const file = readLocalAgentFile(agentId);
155
- const held = new Set(file.connectors);
156
143
  const bundle: AgentBundle = {
157
144
  version: 1,
158
145
  agent: { id: file.id, name: file.name, key: file.key ?? '', stations: file.stations },
159
- connectors: readLocalConnectors()
160
- .filter((c) => held.has(c.id))
161
- .map((c) => ({ id: c.id, name: c.name, url: c.url, transport: c.transport, config: { ...c.config } })),
146
+ connectors: readLocalConnectors().map((c) => ({ id: c.id, name: c.name, url: c.url, transport: c.transport, config: { ...c.config } })),
162
147
  };
163
148
  if (bundle.agent.key === '') throw new ApiError(`agent '${agent.name}' has no key to bundle`, 400);
164
149
  return bundle;
@@ -184,7 +169,6 @@ export function localSessionApis(deps: LocalModeDeps): SessionApis {
184
169
  allowLocalConnectors(true);
185
170
  return {
186
171
  agentApi: agentApi(deps),
187
- agentConnectorApi,
188
172
  bundleApi: bundleApi(deps),
189
173
  connectorApi,
190
174
  relayApi,
@@ -5,11 +5,7 @@ import { ApiError } from './api-error.js';
5
5
  import { errMsg, log } from './log.js';
6
6
 
7
7
  export interface RelayApiDeps {
8
- target: (
9
- agentId: string,
10
- connectorId: string,
11
- force: boolean,
12
- ) => Promise<RelayTarget>;
8
+ target: (connectorId: string, force: boolean) => Promise<RelayTarget>;
13
9
  identify: (req: IncomingMessage) => AgentIdentity | null;
14
10
  }
15
11
 
@@ -197,18 +193,17 @@ type Exchanged =
197
193
 
198
194
  async function exchange(
199
195
  req: IncomingMessage,
200
- agentId: string,
201
196
  connectorId: string,
202
197
  deps: RelayApiDeps,
203
198
  body: Uint8Array<ArrayBuffer> | null,
204
199
  signal: AbortSignal,
205
200
  ): Promise<Exchanged> {
206
- const target = await deps.target(agentId, connectorId, false);
201
+ const target = await deps.target(connectorId, false);
207
202
  if (target.kind !== 'ok') return { kind: target.kind };
208
203
  let upstream = await forward(req, target, body, signal);
209
204
  if (!authFailed(upstream.status)) return { kind: 'response', upstream };
210
205
  await upstream.body?.cancel();
211
- const fresh = await deps.target(agentId, connectorId, true);
206
+ const fresh = await deps.target(connectorId, true);
212
207
  if (fresh.kind !== 'ok') return { kind: 'signin' };
213
208
  upstream = await forward(req, fresh, body, signal);
214
209
  if (!authFailed(upstream.status)) return { kind: 'response', upstream };
@@ -219,7 +214,6 @@ async function exchange(
219
214
  async function relayExchange(
220
215
  req: IncomingMessage,
221
216
  res: ServerResponse,
222
- agentId: string,
223
217
  connectorId: string,
224
218
  deps: RelayApiDeps,
225
219
  ): Promise<void> {
@@ -235,7 +229,6 @@ async function relayExchange(
235
229
  const body = req.method === 'POST' ? await readCapped(req) : null;
236
230
  const out = await exchange(
237
231
  req,
238
- agentId,
239
232
  connectorId,
240
233
  deps,
241
234
  body,
@@ -268,7 +261,7 @@ function dispatch(
268
261
  answer(res, 401, { error: 'unauthorized' });
269
262
  return;
270
263
  }
271
- relayExchange(req, res, who.agentId, connectorId, deps)
264
+ relayExchange(req, res, connectorId, deps)
272
265
  .catch((err: unknown) => {
273
266
  if (err instanceof ApiError) {
274
267
  answer(res, err.status, { error: err.message });
@@ -6,10 +6,6 @@ import {
6
6
  type ConnectorApiDeps,
7
7
  } from './connector-api.js';
8
8
  import type { RelayApiDeps } from './relay.js';
9
- import {
10
- handleAgentConnectorRequest,
11
- type AgentConnectorApiDeps,
12
- } from './agent-connector-api.js';
13
9
  import type { IdentityRouteDeps } from './identity-routes.js';
14
10
  import { handleLocalCliRequest, type LocalCliDeps } from './local-cli-api.js';
15
11
  import { handleClaudeRequest, type ClaudeApiDeps } from './claude-api.js';
@@ -24,7 +20,6 @@ import { handleTerminalRequest, type TerminalApiDeps } from './terminal-api.js';
24
20
 
25
21
  export interface SessionApis {
26
22
  agentApi?: AgentApiDeps;
27
- agentConnectorApi?: AgentConnectorApiDeps;
28
23
  bundleApi?: BundleApiDeps;
29
24
  vaultApi?: VaultApiDeps;
30
25
  serversApi?: ServersApiDeps;
@@ -60,7 +55,6 @@ export function handleSessionApis(
60
55
  ...when(apis.machineApi, (d) => handleMachineRequest(req, res, d)),
61
56
  ...when(apis.terminalApi, (d) => handleTerminalRequest(req, res, d)),
62
57
  ...when(apis.claudeApi, (d) => handleClaudeRequest(req, res, d)),
63
- ...when(apis.agentConnectorApi, (d) => handleAgentConnectorRequest(req, res, d)),
64
58
  ...when(apis.agentApi, (d) => handleAgentApiRequest(req, res, d)),
65
59
  ];
66
60
  return routes.some((run) => run());
@@ -58,14 +58,6 @@ export function connectorFromRow(row: ConnectorLike): Connector {
58
58
  };
59
59
  }
60
60
 
61
- export interface ConnectorSummary {
62
- id: string;
63
- name: string;
64
- url: string;
65
- transport: string;
66
- signIn: ConnectorSignIn;
67
- }
68
-
69
61
  export const UNVERIFIED = {
70
62
  at: '',
71
63
  server: '',
@@ -267,42 +267,6 @@ export function readLocalAgentFile(agentId: string, dir = agentsDir()): AgentFil
267
267
  return found.file;
268
268
  }
269
269
 
270
- export function connectorIdsOfLocalAgent(agentId: string, dir = agentsDir()): string[] | null {
271
- const found = storedAgents(dir).find((s) => s.file.id === agentId);
272
- return found === undefined ? null : found.file.connectors;
273
- }
274
-
275
- export function localAgentConnectorIds(subject: string, agentId: string, dir = agentsDir()): string[] {
276
- return ownedOrThrow(subject, agentId, dir).file.connectors;
277
- }
278
-
279
- export function localSetAgentConnectors(
280
- subject: string,
281
- agentId: string,
282
- ids: string[],
283
- dir = agentsDir(),
284
- ): void {
285
- const stored = ownedOrThrow(subject, agentId, dir);
286
- stored.file.connectors = ids;
287
- save(stored);
288
- }
289
-
290
- export function localHoldEverywhere(connectorId: string, dir = agentsDir()): void {
291
- for (const stored of storedAgents(dir)) {
292
- if (stored.file.connectors.includes(connectorId)) continue;
293
- stored.file.connectors = [...stored.file.connectors, connectorId];
294
- save(stored);
295
- }
296
- }
297
-
298
- export function localDropConnectorEverywhere(connectorId: string, dir = agentsDir()): void {
299
- for (const stored of storedAgents(dir)) {
300
- if (!stored.file.connectors.includes(connectorId)) continue;
301
- stored.file.connectors = stored.file.connectors.filter((id) => id !== connectorId);
302
- save(stored);
303
- }
304
- }
305
-
306
270
  function removeIfEmpty(folder: string): void {
307
271
  try {
308
272
  rmdirSync(folder);
@@ -70,14 +70,8 @@ function optionalMatch(
70
70
  return value;
71
71
  }
72
72
 
73
- function connectorsOf(raw: unknown, path: string): string[] {
74
- if (raw === undefined || raw === null) return [];
75
- const list: unknown[] = Array.isArray(raw) ? raw.map((item: unknown) => item) : [];
76
- const ids = list.filter((id): id is string => typeof id === 'string' && ID_RE.test(id));
77
- if (!Array.isArray(raw) || ids.length !== list.length)
78
- fail(path, 'connectors is not a list of 11-character ids');
79
- return ids;
80
- }
73
+ const connectorsOf = (raw: unknown): string[] =>
74
+ Array.isArray(raw) ? raw.filter((id): id is string => typeof id === 'string' && ID_RE.test(id)) : [];
81
75
 
82
76
  export function parseAgentFile(raw: string, path: string): AgentFile {
83
77
  let parsed: unknown;
@@ -108,7 +102,7 @@ export function parseAgentFile(raw: string, path: string): AgentFile {
108
102
  key,
109
103
  owner,
110
104
  stations: stations.map((s, i) => stationOf(s, path, i)),
111
- connectors: connectorsOf(parsed.connectors, path),
105
+ connectors: connectorsOf(parsed.connectors),
112
106
  };
113
107
  }
114
108
 
@@ -16,7 +16,6 @@ import {
16
16
  connectorAuth,
17
17
  connectorName,
18
18
  readConfig,
19
- signInState,
20
19
  stamp,
21
20
  type ConnectorConfig,
22
21
  } from './connector-config.js';
@@ -32,21 +31,10 @@ import {
32
31
  type Connector,
33
32
  type ConnectorCheck,
34
33
  type ConnectorInput,
35
- type ConnectorSummary,
36
34
  type DeletedConnector,
37
35
  } from './connectors.js';
38
- import type { AgentConnectors } from '../daemon/agent-connector-api.js';
39
36
  import { agentsDir } from './file-source.js';
40
- import {
41
- assertLocalOwner,
42
- connectorIdsOfLocalAgent,
43
- localAgentConnectorIds,
44
- localDropConnectorEverywhere,
45
- localHoldEverywhere,
46
- localOwnedAgentOrThrow,
47
- localSetAgentConnectors,
48
- LOCAL_PROJECT_ID,
49
- } from './file-admin.js';
37
+ import { assertLocalOwner, LOCAL_PROJECT_ID } from './file-admin.js';
50
38
  import { newId } from './ids.js';
51
39
  import type { LoadedConnector } from './materialize.js';
52
40
 
@@ -130,7 +118,6 @@ export function localImportConnectors(rows: LoadedConnector[], dir = agentsDir()
130
118
  }));
131
119
  const ids = new Set(imported.map((r) => r.id));
132
120
  writeRows(dir, [...current.filter((r) => !ids.has(r.id)), ...imported]);
133
- for (const row of imported) localHoldEverywhere(row.id, dir);
134
121
  return imported.length;
135
122
  }
136
123
 
@@ -155,7 +142,6 @@ function insert(dir: string, name: string, url: URL, config: ConnectorConfig): C
155
142
  assertNameFree(name, null, dir);
156
143
  const row: LocalConnectorRow = { id: newId(), name, url: connectorUrlText(url), transport: 'http', config };
157
144
  writeRows(dir, [...readLocalConnectors(dir), row]);
158
- localHoldEverywhere(row.id, dir);
159
145
  return connectorFromRow(row);
160
146
  }
161
147
 
@@ -239,44 +225,16 @@ export async function localRenameConnector(subject: string, id: string, raw: str
239
225
  export async function localDeleteConnector(subject: string, id: string, dir = agentsDir()): Promise<DeletedConnector> {
240
226
  const row = rowOrThrow(subject, id, dir);
241
227
  writeRows(dir, readLocalConnectors(dir).filter((r) => r.id !== id));
242
- localDropConnectorEverywhere(id, dir);
243
228
  return Promise.resolve({ id: row.id, name: row.name });
244
229
  }
245
230
 
246
231
  const byIds = (ids: string[], dir: string): LocalConnectorRow[] =>
247
232
  readLocalConnectors(dir).filter((r) => ids.includes(r.id));
248
233
 
249
- export async function localConnectorSummariesByIds(ids: string[], dir = agentsDir()): Promise<ConnectorSummary[]> {
250
- return Promise.resolve(
251
- byIds(ids, dir).map((r) => ({ id: r.id, name: r.name, url: r.url, transport: r.transport, signIn: signInState(r.config) })),
252
- );
253
- }
254
-
255
234
  export async function localConnectorNamesByIds(ids: string[], dir = agentsDir()): Promise<{ id: string; name: string }[]> {
256
235
  return Promise.resolve(byIds(ids, dir).map((r) => ({ id: r.id, name: r.name })));
257
236
  }
258
237
 
259
- export async function localAgentConnectors(subject: string, agentId: string, dir = agentsDir()): Promise<AgentConnectors> {
260
- const { agent } = await localOwnedAgentOrThrow(subject, agentId, dir);
261
- return { ...agent, connectorIds: localAgentConnectorIds(subject, agentId, dir) };
262
- }
263
-
264
- export async function localAddConnector(subject: string, agentId: string, connectorId: string, dir = agentsDir()): Promise<AgentConnectors> {
265
- const { agent } = await localOwnedAgentOrThrow(subject, agentId, dir);
266
- const row = readLocalConnectors(dir).find((r) => r.id === connectorId);
267
- if (row === undefined) throw missing();
268
- const held = localAgentConnectorIds(subject, agentId, dir);
269
- if (!held.includes(connectorId)) localSetAgentConnectors(subject, agentId, [...held, connectorId], dir);
270
- return { ...agent, connectorIds: localAgentConnectorIds(subject, agentId, dir) };
271
- }
272
-
273
- export async function localRemoveConnector(subject: string, agentId: string, connectorId: string, dir = agentsDir()): Promise<AgentConnectors> {
274
- const { agent } = await localOwnedAgentOrThrow(subject, agentId, dir);
275
- const held = localAgentConnectorIds(subject, agentId, dir);
276
- localSetAgentConnectors(subject, agentId, held.filter((id) => id !== connectorId), dir);
277
- return { ...agent, connectorIds: localAgentConnectorIds(subject, agentId, dir) };
278
- }
279
-
280
238
  const inflight = new Map<string, Promise<OAuthAuth>>();
281
239
 
282
240
  function refreshOnce(row: LocalConnectorRow, auth: OAuthAuth, dir: string): Promise<OAuthAuth> {
@@ -306,13 +264,10 @@ async function oauthTarget(row: LocalConnectorRow, auth: OAuthAuth, force: boole
306
264
  }
307
265
 
308
266
  export async function localRelayTarget(
309
- agentId: string,
310
267
  connectorId: string,
311
268
  force: boolean,
312
269
  dir = agentsDir(),
313
270
  ): Promise<RelayTarget> {
314
- const held = connectorIdsOfLocalAgent(agentId, dir);
315
- if (!held?.includes(connectorId)) return { kind: 'missing' };
316
271
  const row = readLocalConnectors(dir).find((r) => r.id === connectorId);
317
272
  if (row === undefined) return { kind: 'missing' };
318
273
  parseConnectorUrl(row.url);
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.1.0-beta.69"
2
+ "version": "0.1.0-beta.70"
3
3
  }
@@ -1,138 +0,0 @@
1
- import type { IncomingMessage, ServerResponse } from 'node:http';
2
- import { errMsg, log } from './log.js';
3
- import {
4
- apiFailure,
5
- apiSession,
6
- bodyField,
7
- cors,
8
- readJsonBody,
9
- sendJson,
10
- type ApiSession,
11
- } from './api-http.js';
12
- import { parseId } from '../db/ids.js';
13
-
14
- const PREFIX = '/api/agents';
15
-
16
- export interface AgentConnectors {
17
- id: string;
18
- name: string;
19
- connectorIds: string[];
20
- }
21
-
22
- export interface AgentConnectorApiDeps {
23
- agentConnectors: (subject: string, agentId: string) => Promise<AgentConnectors>;
24
- addConnector: (
25
- subject: string,
26
- agentId: string,
27
- connectorId: string,
28
- ) => Promise<AgentConnectors>;
29
- removeConnector: (
30
- subject: string,
31
- agentId: string,
32
- connectorId: string,
33
- ) => Promise<AgentConnectors>;
34
- }
35
-
36
- type Routable =
37
- | { kind: 'connectors'; id: string }
38
- | { kind: 'connector'; id: string; connectorId: string };
39
-
40
- type Target = Routable | { kind: 'unknown' } | null;
41
-
42
- const ALLOWED: Record<Routable['kind'], string[]> = {
43
- connectors: ['GET', 'POST'],
44
- connector: ['DELETE'],
45
- };
46
-
47
- function connectorTarget(id: string, rest: string[]): Target {
48
- if (rest.length === 1) return { kind: 'connectors', id };
49
- if (rest.length > 2) return { kind: 'unknown' };
50
- const connectorId = parseId(rest[1] ?? '');
51
- return connectorId === null
52
- ? { kind: 'unknown' }
53
- : { kind: 'connector', id, connectorId };
54
- }
55
-
56
- export function target(path: string): Target {
57
- if (!path.startsWith(`${PREFIX}/`)) return null;
58
- const segments = path.slice(PREFIX.length + 1).split('/').filter(Boolean);
59
- const id = parseId(segments[0] ?? '');
60
- const head = segments[1];
61
- if (id === null || head === undefined) return null;
62
- if (head === 'connectors') return connectorTarget(id, segments.slice(1));
63
- return null;
64
- }
65
-
66
- async function handleConnectors(
67
- req: IncomingMessage,
68
- res: ServerResponse,
69
- deps: AgentConnectorApiDeps,
70
- session: ApiSession,
71
- id: string,
72
- ): Promise<void> {
73
- if (req.method === 'GET') {
74
- sendJson(req, res, 200, await deps.agentConnectors(session.subject, id));
75
- return;
76
- }
77
- const connectorId = bodyField(await readJsonBody(req), 'connectorId');
78
- if (typeof connectorId !== 'string' || parseId(connectorId) === null) {
79
- sendJson(req, res, 400, { error: 'connectorId is required' });
80
- return;
81
- }
82
- sendJson(req, res, 200, await deps.addConnector(session.subject, id, connectorId));
83
- }
84
-
85
- async function route(
86
- req: IncomingMessage,
87
- res: ServerResponse,
88
- deps: AgentConnectorApiDeps,
89
- session: ApiSession,
90
- tgt: Routable,
91
- ): Promise<void> {
92
- try {
93
- if (tgt.kind === 'connectors') await handleConnectors(req, res, deps, session, tgt.id);
94
- else
95
- sendJson(
96
- req,
97
- res,
98
- 200,
99
- await deps.removeConnector(session.subject, tgt.id, tgt.connectorId),
100
- );
101
- } catch (err) {
102
- apiFailure(req, res, err, 'agent-api');
103
- }
104
- }
105
-
106
- export function handleAgentConnectorRequest(
107
- req: IncomingMessage,
108
- res: ServerResponse,
109
- deps: AgentConnectorApiDeps,
110
- ): boolean {
111
- const tgt = target((req.url ?? '').split('?')[0] ?? '');
112
- if (tgt === null) return false;
113
- if (req.method === 'OPTIONS') {
114
- res.writeHead(204, cors(req)).end();
115
- return true;
116
- }
117
- if (tgt.kind === 'unknown') {
118
- sendJson(req, res, 404, { error: 'no such connector' });
119
- return true;
120
- }
121
- if (!ALLOWED[tgt.kind].includes(req.method ?? '')) {
122
- sendJson(req, res, 405, { error: 'method not allowed' });
123
- return true;
124
- }
125
- apiSession(req)
126
- .then((session) => {
127
- if (!session) {
128
- sendJson(req, res, 401, { error: 'unauthorized' });
129
- return;
130
- }
131
- return route(req, res, deps, session, tgt);
132
- })
133
- .catch((err: unknown) => {
134
- log.warn({ err: errMsg(err) }, 'agent-api: unhandled error');
135
- if (!res.headersSent) sendJson(req, res, 500, { error: 'agent api failed' });
136
- });
137
- return true;
138
- }