@stage-labs/metro 0.1.0-beta.52 → 0.1.0-beta.54
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/local.js +26 -0
- package/dist/runtime-install.js +84 -0
- package/dist/serve.js +8 -5
- package/package.json +1 -22
- package/runtime/node_modules/@metro-labs/mcp/package.json +1 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +5 -5
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-connector-api.ts +9 -6
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +6 -13
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +1 -2
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +3 -1
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +13 -16
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/bundle-api.ts +6 -7
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/claude-api.ts +3 -7
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +5 -5
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +2 -2
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity-key.ts +33 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity-registry.ts +13 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity-routes.ts +54 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-mode.ts +2 -3
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/{session-config.ts → return-to.ts} +0 -1
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-deps.ts +82 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-api.ts +9 -7
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +2 -2
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/signed-identity.ts +36 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/update-api.ts +2 -5
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +1 -2
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/vault-api.ts +3 -33
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/vault-types.ts +12 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/db/address.ts +6 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-scope.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/db/file-admin.ts +2 -12
- package/runtime/node_modules/@metro-labs/mcp/src/db/file-source.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +2 -16
- package/runtime/node_modules/@metro-labs/mcp/src/db/stations.ts +12 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/vault.ts +4 -14
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/accounts.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/index.ts +1 -2
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/request-identity.ts +2 -28
- package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +2 -5
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +2 -0
- package/runtime/runtime.json +1 -1
- package/runtime/stations.json +35 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-secret.ts +0 -21
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +0 -96
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-auth.ts +0 -73
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-nonces.ts +0 -43
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-routes.ts +0 -89
- package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +0 -45
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { log } from './log.js';
|
|
5
|
+
import { isRecord } from './is-record.js';
|
|
6
|
+
import type { StationName } from '../db/stations.js';
|
|
7
|
+
import { knownAccounts } from '../db/agent-map.js';
|
|
8
|
+
|
|
9
|
+
const INSTALL_TIMEOUT_MS = 15 * 60_000;
|
|
10
|
+
|
|
11
|
+
export interface RuntimeManifest {
|
|
12
|
+
core: Record<string, string>;
|
|
13
|
+
stations: Record<string, Record<string, string>>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface RuntimeStore {
|
|
17
|
+
dir: string;
|
|
18
|
+
manifest: RuntimeManifest;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const ranges = (raw: unknown): Record<string, string> =>
|
|
22
|
+
isRecord(raw)
|
|
23
|
+
? Object.fromEntries(Object.entries(raw).filter((e): e is [string, string] => typeof e[1] === 'string'))
|
|
24
|
+
: {};
|
|
25
|
+
|
|
26
|
+
export function readManifest(path: string): RuntimeManifest {
|
|
27
|
+
const raw: unknown = JSON.parse(readFileSync(path, 'utf8'));
|
|
28
|
+
if (!isRecord(raw)) throw new Error(`${path} is not a runtime manifest`);
|
|
29
|
+
const stations = isRecord(raw.stations) ? raw.stations : {};
|
|
30
|
+
return {
|
|
31
|
+
core: ranges(raw.core),
|
|
32
|
+
stations: Object.fromEntries(Object.entries(stations).map(([name, deps]) => [name, ranges(deps)])),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function runtimeStore(): RuntimeStore | null {
|
|
37
|
+
const dir = process.env.METRO_RUNTIME_STORE?.trim() ?? '';
|
|
38
|
+
const manifest = process.env.METRO_RUNTIME_MANIFEST?.trim() ?? '';
|
|
39
|
+
if (dir === '' || manifest === '') return null;
|
|
40
|
+
return { dir, manifest: readManifest(manifest) };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function dependenciesFor(manifest: RuntimeManifest, stations: Iterable<string>): Record<string, string> {
|
|
44
|
+
const out: Record<string, string> = { ...manifest.core };
|
|
45
|
+
for (const station of new Set(stations)) Object.assign(out, manifest.stations[station] ?? {});
|
|
46
|
+
return Object.fromEntries(Object.entries(out).sort(([a], [b]) => a.localeCompare(b)));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const packageText = (deps: Record<string, string>): string =>
|
|
50
|
+
`${JSON.stringify({ name: 'metro-runtime', private: true, dependencies: deps }, null, 2)}\n`;
|
|
51
|
+
|
|
52
|
+
function current(dir: string): string | null {
|
|
53
|
+
try {
|
|
54
|
+
return readFileSync(join(dir, 'package.json'), 'utf8');
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function installRuntime(store: RuntimeStore, stations: Iterable<string>): boolean {
|
|
61
|
+
const wanted = packageText(dependenciesFor(store.manifest, stations));
|
|
62
|
+
const installed = existsSync(join(store.dir, 'node_modules', '.metro-installed'));
|
|
63
|
+
if (installed && current(store.dir) === wanted) return false;
|
|
64
|
+
mkdirSync(store.dir, { recursive: true });
|
|
65
|
+
writeFileSync(join(store.dir, 'package.json'), wanted);
|
|
66
|
+
log.info({ dir: store.dir }, 'runtime: installing the channel SDKs this machine needs');
|
|
67
|
+
const run = spawnSync('bun', ['install', '--no-summary', '--no-progress'], {
|
|
68
|
+
cwd: store.dir,
|
|
69
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
70
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
71
|
+
});
|
|
72
|
+
if (run.error !== undefined || run.status !== 0)
|
|
73
|
+
throw new Error(`bun install failed in ${store.dir}: ${run.error?.message ?? `exit ${String(run.status)}`}`);
|
|
74
|
+
writeFileSync(join(store.dir, 'node_modules', '.metro-installed'), `${new Date().toISOString()}\n`);
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function ensureStationDeps(station: StationName): void {
|
|
79
|
+
const store = runtimeStore();
|
|
80
|
+
if (store === null) return;
|
|
81
|
+
installRuntime(store, [...knownAccounts().map((a) => a.station), station]);
|
|
82
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
-
import { apiSession, cors, sendJson } from './api-http.js';
|
|
2
|
+
import { apiFailure, apiSession, cors, sendJson } from './api-http.js';
|
|
3
3
|
|
|
4
4
|
const PATH = '/api/session';
|
|
5
5
|
|
|
@@ -17,11 +17,13 @@ export function handleSessionApiRequest(
|
|
|
17
17
|
sendJson(req, res, 405, { error: 'method not allowed' });
|
|
18
18
|
return true;
|
|
19
19
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
apiSession(req)
|
|
21
|
+
.then((session) => {
|
|
22
|
+
if (!session) sendJson(req, res, 401, { error: 'unauthorized' });
|
|
23
|
+
else sendJson(req, res, 200, { subject: session.subject });
|
|
24
|
+
})
|
|
25
|
+
.catch((err: unknown) => {
|
|
26
|
+
apiFailure(req, res, err, 'session');
|
|
27
|
+
});
|
|
26
28
|
return true;
|
|
27
29
|
}
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
handleAgentConnectorRequest,
|
|
11
11
|
type AgentConnectorApiDeps,
|
|
12
12
|
} from './agent-connector-api.js';
|
|
13
|
-
import type {
|
|
13
|
+
import type { IdentityRouteDeps } from './identity-routes.js';
|
|
14
14
|
import { handleLocalCliRequest, type LocalCliDeps } from './local-cli-api.js';
|
|
15
15
|
import { handleClaudeRequest, type ClaudeApiDeps } from './claude-api.js';
|
|
16
16
|
import type { ModeInfo } from './mode-api.js';
|
|
@@ -28,7 +28,7 @@ export interface SessionApis {
|
|
|
28
28
|
claudeApi?: ClaudeApiDeps;
|
|
29
29
|
connectorApi?: ConnectorApiDeps;
|
|
30
30
|
relayApi?: RelayApiDeps;
|
|
31
|
-
|
|
31
|
+
identity?: IdentityRouteDeps;
|
|
32
32
|
mode?: () => ModeInfo;
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { IncomingMessage } from 'node:http';
|
|
2
|
+
import { verifyMessage } from 'viem';
|
|
3
|
+
import { normalizeAddress } from '../db/address.js';
|
|
4
|
+
|
|
5
|
+
export const AUTH_SCHEME = 'Metro';
|
|
6
|
+
const SKEW_MS = 5 * 60_000;
|
|
7
|
+
const SIGNATURE_RE = /^0x[0-9a-fA-F]{130}$/;
|
|
8
|
+
|
|
9
|
+
export const identityChallenge = (method: string, path: string, at: number): string =>
|
|
10
|
+
`metro-auth\n${method} ${path}\n${String(at)}`;
|
|
11
|
+
|
|
12
|
+
export interface SignedHeader {
|
|
13
|
+
address: string;
|
|
14
|
+
at: number;
|
|
15
|
+
signature: `0x${string}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function parseIdentityHeader(header: string): SignedHeader | null {
|
|
19
|
+
const [scheme, rawAddress, rawAt, signature] = header.trim().split(/\s+/);
|
|
20
|
+
if (scheme !== AUTH_SCHEME || rawAddress === undefined || rawAt === undefined || signature === undefined) return null;
|
|
21
|
+
const address = normalizeAddress(rawAddress);
|
|
22
|
+
const at = Number(rawAt);
|
|
23
|
+
if (address === null || !Number.isFinite(at) || !SIGNATURE_RE.test(signature)) return null;
|
|
24
|
+
return { address, at, signature: signature as `0x${string}` };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function signedIdentity(req: IncomingMessage, now = Date.now()): Promise<string | null> {
|
|
28
|
+
const proof = parseIdentityHeader(req.headers.authorization ?? '');
|
|
29
|
+
if (proof === null || Math.abs(now - proof.at) > SKEW_MS) return null;
|
|
30
|
+
const path = (req.url ?? '').split('?')[0] ?? '';
|
|
31
|
+
const message = identityChallenge(req.method ?? '', path, proof.at);
|
|
32
|
+
const ok = await verifyMessage({ address: proof.address as `0x${string}`, message, signature: proof.signature }).catch(
|
|
33
|
+
() => false,
|
|
34
|
+
);
|
|
35
|
+
return ok ? proof.address : null;
|
|
36
|
+
}
|
|
@@ -93,13 +93,10 @@ export function handleUpdateRequest(req: IncomingMessage, res: ServerResponse, d
|
|
|
93
93
|
sendJson(req, res, 405, { error: 'method not allowed' });
|
|
94
94
|
return true;
|
|
95
95
|
}
|
|
96
|
-
const session = apiSession(req);
|
|
97
|
-
if (!session) {
|
|
98
|
-
sendJson(req, res, 401, { error: 'unauthorized' });
|
|
99
|
-
return true;
|
|
100
|
-
}
|
|
101
96
|
Promise.resolve()
|
|
102
97
|
.then(async (): Promise<unknown> => {
|
|
98
|
+
const session = await apiSession(req);
|
|
99
|
+
if (!session) throw new ApiError('unauthorized', 401);
|
|
103
100
|
deps.authorize(session.subject);
|
|
104
101
|
const bin = binOrThrow(deps);
|
|
105
102
|
if (req.method === 'GET') return { ...(await check(bin)), running: METRO_VERSION };
|
|
@@ -4,7 +4,6 @@ import { guessMime } from '../stations/attachments.js';
|
|
|
4
4
|
import { safeFileName } from '../stations/attach-inline.js';
|
|
5
5
|
import {
|
|
6
6
|
allowedAgents,
|
|
7
|
-
authConfigFromEnv,
|
|
8
7
|
authenticate,
|
|
9
8
|
extractToken,
|
|
10
9
|
} from '../mcp/request-identity.js';
|
|
@@ -69,7 +68,7 @@ const query = (req: IncomingMessage, key: string): string | undefined =>
|
|
|
69
68
|
new URL(req.url ?? '/', 'http://localhost').searchParams.get(key) ?? undefined;
|
|
70
69
|
|
|
71
70
|
const identityScope = (req: IncomingMessage): Set<string> =>
|
|
72
|
-
allowedAgents(authenticate(req
|
|
71
|
+
allowedAgents(authenticate(req) ?? undefined);
|
|
73
72
|
|
|
74
73
|
function ownerFromScope(req: IncomingMessage, allowed: Set<string>): string {
|
|
75
74
|
const requested = query(req, 'agent');
|
|
@@ -1,42 +1,12 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
2
|
import { errMsg, log } from './log.js';
|
|
3
|
-
import { verifyMessage } from 'viem';
|
|
4
3
|
import { apiFailure, cors, readJsonBody, sendJson } from './api-http.js';
|
|
5
|
-
import { normalizeAddress } from '../db/users.js';
|
|
6
4
|
import { parseId } from '../db/ids.js';
|
|
7
|
-
import { ENVELOPE_MAX, type VaultBundle, type VaultEntry } from '
|
|
5
|
+
import { ENVELOPE_MAX, type VaultBundle, type VaultEntry } from './vault-types.js';
|
|
6
|
+
import { signedIdentity } from './signed-identity.js';
|
|
8
7
|
|
|
9
8
|
const PREFIX = '/api/vault';
|
|
10
9
|
const BODY_MAX = ENVELOPE_MAX + 64 * 1024;
|
|
11
|
-
const SKEW_MS = 5 * 60_000;
|
|
12
|
-
const SIGNATURE_RE = /^0x[0-9a-fA-F]{130}$/;
|
|
13
|
-
|
|
14
|
-
export const vaultChallenge = (method: string, path: string, at: number): string =>
|
|
15
|
-
`metro-vault\n${method} ${path}\n${String(at)}`;
|
|
16
|
-
|
|
17
|
-
interface VaultProof {
|
|
18
|
-
address: string;
|
|
19
|
-
at: number;
|
|
20
|
-
signature: `0x${string}`;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function parseVaultHeader(header: string): VaultProof | null {
|
|
24
|
-
const [scheme, rawAddress, rawAt, signature] = header.trim().split(/\s+/);
|
|
25
|
-
if (scheme !== 'Vault' || rawAddress === undefined || rawAt === undefined || signature === undefined) return null;
|
|
26
|
-
const address = normalizeAddress(rawAddress);
|
|
27
|
-
const at = Number(rawAt);
|
|
28
|
-
if (address === null || !Number.isFinite(at) || !SIGNATURE_RE.test(signature)) return null;
|
|
29
|
-
return { address, at, signature: signature as `0x${string}` };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export async function vaultIdentity(req: IncomingMessage, now = Date.now()): Promise<string | null> {
|
|
33
|
-
const proof = parseVaultHeader(req.headers.authorization ?? '');
|
|
34
|
-
if (proof === null || Math.abs(now - proof.at) > SKEW_MS) return null;
|
|
35
|
-
const path = (req.url ?? '').split('?')[0] ?? '';
|
|
36
|
-
const message = vaultChallenge(req.method ?? '', path, proof.at);
|
|
37
|
-
const ok = await verifyMessage({ address: proof.address as `0x${string}`, message, signature: proof.signature }).catch(() => false);
|
|
38
|
-
return ok ? proof.address : null;
|
|
39
|
-
}
|
|
40
10
|
|
|
41
11
|
export interface VaultApiDeps {
|
|
42
12
|
list: (subject: string) => Promise<VaultEntry[]>;
|
|
@@ -64,7 +34,7 @@ async function route(
|
|
|
64
34
|
tgt: { kind: 'index' } | { kind: 'bundle'; id: string },
|
|
65
35
|
): Promise<void> {
|
|
66
36
|
try {
|
|
67
|
-
const owner = await
|
|
37
|
+
const owner = await signedIdentity(req);
|
|
68
38
|
if (owner === null) {
|
|
69
39
|
sendJson(req, res, 401, { error: 'unauthorized' });
|
|
70
40
|
return;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ConnectorAuth, VerifiedRecord } from '../daemon/connector-verify.js';
|
|
2
2
|
import { readConfig, signInState, type ConnectorSignIn } from './connector-config.js';
|
|
3
|
-
import type { ConnectorTransport } from './
|
|
3
|
+
import type { ConnectorTransport } from './stations.js';
|
|
4
4
|
|
|
5
5
|
export interface Connector {
|
|
6
6
|
id: string;
|
|
@@ -25,8 +25,8 @@ import {
|
|
|
25
25
|
import { newId } from './ids.js';
|
|
26
26
|
import { registerKey, rotateAgentKey, unregisterAgentKey } from './key-map.js';
|
|
27
27
|
import { MOVABLE_STATIONS, type LoadedAgent } from './materialize.js';
|
|
28
|
-
import type { StationName } from './
|
|
29
|
-
import { normalizeAddress } from './
|
|
28
|
+
import type { StationName } from './stations.js';
|
|
29
|
+
import { normalizeAddress } from './address.js';
|
|
30
30
|
|
|
31
31
|
export const LOCAL_PROJECT_ID = 'localdaemon';
|
|
32
32
|
const OWNER_FILE = '.owner';
|
|
@@ -54,16 +54,6 @@ export function setLocalOwner(raw: string, dir = agentsDir()): string {
|
|
|
54
54
|
return address;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
export function ownerSignIn(raw: string, dir = agentsDir()): Promise<string> {
|
|
58
|
-
const owner = localOwner(dir);
|
|
59
|
-
if (owner === null)
|
|
60
|
-
return Promise.reject(
|
|
61
|
-
new ApiError('this machine has no owner yet: start it with metro serve --owner <address>', 403),
|
|
62
|
-
);
|
|
63
|
-
if (owner !== normalizeAddress(raw))
|
|
64
|
-
return Promise.reject(new ApiError('this machine belongs to another wallet', 403));
|
|
65
|
-
return Promise.resolve(owner);
|
|
66
|
-
}
|
|
67
57
|
|
|
68
58
|
function isOwner(subject: string, dir: string): boolean {
|
|
69
59
|
const owner = localOwner(dir);
|
|
@@ -2,7 +2,7 @@ import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { AGENT_NAME_RE, ID_RE } from './ids.js';
|
|
5
|
-
import { STATIONS, type StationName } from './
|
|
5
|
+
import { STATIONS, type StationName } from './stations.js';
|
|
6
6
|
import type { LoadedAccount, LoadedAgent, StationSource } from './materialize.js';
|
|
7
7
|
import { isRecord } from '../daemon/is-record.js';
|
|
8
8
|
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
type AllowlistMap,
|
|
19
19
|
} from './agent-map.js';
|
|
20
20
|
import { setKeyMap } from './key-map.js';
|
|
21
|
-
import { STATIONS, type StationName } from './
|
|
21
|
+
import { STATIONS, type StationName } from './stations.js';
|
|
22
22
|
|
|
23
23
|
interface StationTarget {
|
|
24
24
|
file: string;
|
|
@@ -1,25 +1,11 @@
|
|
|
1
1
|
import { index, jsonb, pgTable, text } from 'drizzle-orm/pg-core';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
'xmtp',
|
|
5
|
-
'telegram-bot',
|
|
6
|
-
'telegram',
|
|
7
|
-
'discord-bot',
|
|
8
|
-
'whatsapp',
|
|
9
|
-
'webhook',
|
|
10
|
-
] as const;
|
|
3
|
+
import { STATIONS, type ConnectorTransport, type StationName } from './stations.js';
|
|
11
4
|
|
|
12
|
-
export type
|
|
13
|
-
|
|
14
|
-
export const users = pgTable('users', {
|
|
15
|
-
id: text('id').primaryKey(),
|
|
16
|
-
email: text('email').unique(),
|
|
17
|
-
address: text('address').unique(),
|
|
18
|
-
});
|
|
5
|
+
export { STATIONS, type ConnectorTransport, type StationName };
|
|
19
6
|
|
|
20
7
|
export type ProjectRole = 'admin' | 'member';
|
|
21
8
|
|
|
22
|
-
export type ConnectorTransport = 'http' | 'sse';
|
|
23
9
|
|
|
24
10
|
export const vault = pgTable(
|
|
25
11
|
'vault',
|
|
@@ -3,29 +3,19 @@ import { ApiError } from '../daemon/api-error.js';
|
|
|
3
3
|
import { isRecord } from '../daemon/is-record.js';
|
|
4
4
|
import { getDb } from './client.js';
|
|
5
5
|
import { AGENT_NAME_RE, parseId } from './ids.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { vault } from './schema.js';
|
|
7
|
+
import { STATIONS } from './stations.js';
|
|
8
|
+
import { normalizeAddress } from './address.js';
|
|
8
9
|
|
|
9
10
|
export class VaultError extends ApiError {}
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
id: string;
|
|
13
|
-
name: string;
|
|
14
|
-
stations: string[];
|
|
15
|
-
syncedAt: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface VaultBundle extends VaultEntry {
|
|
19
|
-
envelope: Record<string, unknown>;
|
|
20
|
-
}
|
|
12
|
+
import { ENVELOPE_MAX, type VaultBundle, type VaultEntry } from '../daemon/vault-types.js';
|
|
21
13
|
|
|
22
14
|
export interface VaultInput {
|
|
23
15
|
name: string;
|
|
24
16
|
stations: string[];
|
|
25
17
|
envelope: Record<string, unknown>;
|
|
26
18
|
}
|
|
27
|
-
|
|
28
|
-
export const ENVELOPE_MAX = 2 * 1024 * 1024;
|
|
29
19
|
const STATION_NAMES = new Set<string>(STATIONS);
|
|
30
20
|
|
|
31
21
|
const missing = (): VaultError => new VaultError('no such bundle', 404);
|
|
@@ -7,7 +7,7 @@ import { listEndpoints } from '../daemon/tunnel.js';
|
|
|
7
7
|
import { hookUrl } from '../stations/attach.js';
|
|
8
8
|
import { agentIdForAccount, knownAccounts, type KnownAccount } from '../db/agent-map.js';
|
|
9
9
|
import { stationRunsHere } from '../db/materialize.js';
|
|
10
|
-
import { type StationName } from '../db/
|
|
10
|
+
import { type StationName } from '../db/stations.js';
|
|
11
11
|
|
|
12
12
|
const accountId = (acc: unknown): string | undefined => {
|
|
13
13
|
const id = (acc as { id?: unknown }).id;
|
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
} from '../daemon/http.js';
|
|
6
6
|
import {
|
|
7
7
|
allowedAgents,
|
|
8
|
-
authConfigFromEnv,
|
|
9
8
|
authenticate,
|
|
10
9
|
runWithIdentity,
|
|
11
10
|
type RequestIdentity,
|
|
@@ -156,7 +155,7 @@ export async function createMetroMcp(): Promise<{
|
|
|
156
155
|
req: IncomingMessage,
|
|
157
156
|
res: ServerResponse,
|
|
158
157
|
): Promise<void> => {
|
|
159
|
-
const identity = authenticate(req
|
|
158
|
+
const identity = authenticate(req);
|
|
160
159
|
if (!identity) {
|
|
161
160
|
res.writeHead(401).end('unauthorized');
|
|
162
161
|
return;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
2
|
import type { IncomingMessage } from 'node:http';
|
|
3
|
-
import { verifySession } from '../daemon/session.js';
|
|
4
3
|
import { agentIdForKey } from '../db/key-map.js';
|
|
5
4
|
|
|
6
5
|
export type RequestIdentity =
|
|
@@ -20,14 +19,6 @@ export function currentIdentity(): RequestIdentity | undefined {
|
|
|
20
19
|
return storage.getStore();
|
|
21
20
|
}
|
|
22
21
|
|
|
23
|
-
export interface AuthConfig {
|
|
24
|
-
sessionSecret: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function authConfigFromEnv(): AuthConfig {
|
|
28
|
-
return { sessionSecret: process.env.METRO_SESSION_SECRET?.trim() ?? '' };
|
|
29
|
-
}
|
|
30
|
-
|
|
31
22
|
export function extractToken(req: IncomingMessage): string | undefined {
|
|
32
23
|
const header = req.headers.authorization;
|
|
33
24
|
if (header?.startsWith('Bearer ')) {
|
|
@@ -40,28 +31,11 @@ export function extractToken(req: IncomingMessage): string | undefined {
|
|
|
40
31
|
return qt ?? undefined;
|
|
41
32
|
}
|
|
42
33
|
|
|
43
|
-
|
|
44
|
-
/^[\w-]+\.[\w-]+\.[\w-]+$/.test(token);
|
|
45
|
-
|
|
46
|
-
export function authenticate(
|
|
47
|
-
req: IncomingMessage,
|
|
48
|
-
cfg: AuthConfig,
|
|
49
|
-
): RequestIdentity | null {
|
|
34
|
+
export function authenticate(req: IncomingMessage): RequestIdentity | null {
|
|
50
35
|
const token = extractToken(req);
|
|
51
36
|
if (!token) return null;
|
|
52
|
-
|
|
53
37
|
const agentId = agentIdForKey(token);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (cfg.sessionSecret !== '' && looksLikeJwt(token)) {
|
|
57
|
-
try {
|
|
58
|
-
const { subject, agentIds } = verifySession(token, cfg.sessionSecret);
|
|
59
|
-
return { kind: 'session', subject, agentIds };
|
|
60
|
-
} catch {
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return null;
|
|
38
|
+
return agentId === undefined ? null : { kind: 'agent', agentId };
|
|
65
39
|
}
|
|
66
40
|
|
|
67
41
|
export function allowedAgents(
|
|
@@ -6,7 +6,6 @@ import { callTargetDenied, eventInScope } from '../db/agent-scope.js';
|
|
|
6
6
|
import { hasAnyKey } from '../db/key-map.js';
|
|
7
7
|
import {
|
|
8
8
|
allowedAgents,
|
|
9
|
-
authConfigFromEnv,
|
|
10
9
|
authenticate,
|
|
11
10
|
} from '../mcp/request-identity.js';
|
|
12
11
|
import { METRO_VERSION } from '../daemon/version.js';
|
|
@@ -20,9 +19,7 @@ export type MonitorCall = (
|
|
|
20
19
|
const KEEPALIVE_MS = 25_000;
|
|
21
20
|
const CALL_BODY_MAX = 256 * 1024;
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
return hasAnyKey() || authConfigFromEnv().sessionSecret !== '';
|
|
25
|
-
}
|
|
22
|
+
const monitorEnabled = (): boolean => hasAnyKey();
|
|
26
23
|
|
|
27
24
|
function cors(req: IncomingMessage): Record<string, string> {
|
|
28
25
|
return {
|
|
@@ -230,7 +227,7 @@ export function handleMonitorRequest(
|
|
|
230
227
|
const path = (req.url ?? '').split('?', 2)[0] ?? '';
|
|
231
228
|
if (!path.startsWith('/api/')) return false;
|
|
232
229
|
if (preflight(req, res, path)) return true;
|
|
233
|
-
const identity = authenticate(req
|
|
230
|
+
const identity = authenticate(req);
|
|
234
231
|
if (!identity) {
|
|
235
232
|
sendJson(res, req, 401, { error: 'unauthorized' });
|
|
236
233
|
return true;
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
verifyTelegramBotToken,
|
|
9
9
|
} from '@metro-labs/telegram-bot/verify';
|
|
10
10
|
import { ApiError } from '../daemon/api-error.js';
|
|
11
|
+
import { ensureStationDeps } from '../daemon/runtime-deps.js';
|
|
11
12
|
import { publicBaseOrDefault } from '../daemon/attach-serve.js';
|
|
12
13
|
import {
|
|
13
14
|
discardXmtpDb,
|
|
@@ -175,6 +176,7 @@ export async function prepareAccount(
|
|
|
175
176
|
input: AttachInput,
|
|
176
177
|
verify: VerifyXmtpKey = verifyXmtpKeyOutOfProcess,
|
|
177
178
|
): Promise<PreparedAccount> {
|
|
179
|
+
ensureStationDeps(input.station);
|
|
178
180
|
if (input.station === 'discord-bot') return prepareDiscord(input.token);
|
|
179
181
|
if (input.station === 'telegram-bot') return prepareTelegram(input.token);
|
|
180
182
|
if (input.station === 'webhook') return prepareWebhook();
|
package/runtime/runtime.json
CHANGED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"core": {
|
|
3
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
4
|
+
"@noble/curves": "^1.9.7",
|
|
5
|
+
"pino": "^9.5.0",
|
|
6
|
+
"pino-pretty": "^13.1.3",
|
|
7
|
+
"viem": "2.52.2",
|
|
8
|
+
"zod": "^3.23.8"
|
|
9
|
+
},
|
|
10
|
+
"stations": {
|
|
11
|
+
"xmtp": {
|
|
12
|
+
"@xmtp/content-type-primitives": "^3.0.1",
|
|
13
|
+
"@xmtp/content-type-reaction": "^2.0.2",
|
|
14
|
+
"@xmtp/content-type-remote-attachment": "^2.0.4",
|
|
15
|
+
"@xmtp/content-type-reply": "^2.0.3",
|
|
16
|
+
"@xmtp/content-type-wallet-send-calls": "^2.0.0",
|
|
17
|
+
"@xmtp/node-bindings": "1.9.1",
|
|
18
|
+
"@xmtp/node-sdk": "^6.0.0",
|
|
19
|
+
"viem": "^2.52.2"
|
|
20
|
+
},
|
|
21
|
+
"telegram-bot": {},
|
|
22
|
+
"telegram": {
|
|
23
|
+
"@mtcute/bun": "0.30.1"
|
|
24
|
+
},
|
|
25
|
+
"discord-bot": {
|
|
26
|
+
"@discordjs/voice": "^0.19.2",
|
|
27
|
+
"discord.js": "^14.26.4",
|
|
28
|
+
"prism-media": "^1.3.5"
|
|
29
|
+
},
|
|
30
|
+
"whatsapp": {
|
|
31
|
+
"baileys": "7.0.0-rc14"
|
|
32
|
+
},
|
|
33
|
+
"webhook": {}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { randomBytes } from 'node:crypto';
|
|
2
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { agentsDir } from '../db/file-source.js';
|
|
5
|
-
import { ensureSecureDir, writeSecure } from './secure-fs.js';
|
|
6
|
-
|
|
7
|
-
const SECRET_FILE = '.session-secret';
|
|
8
|
-
|
|
9
|
-
export function ensureLocalSessionSecret(dir = agentsDir()): string {
|
|
10
|
-
const configured = process.env.METRO_SESSION_SECRET?.trim() ?? '';
|
|
11
|
-
if (configured !== '') return configured;
|
|
12
|
-
const path = join(dir, SECRET_FILE);
|
|
13
|
-
let secret = existsSync(path) ? readFileSync(path, 'utf8').trim() : '';
|
|
14
|
-
if (secret === '') {
|
|
15
|
-
secret = randomBytes(32).toString('base64url');
|
|
16
|
-
ensureSecureDir(dir);
|
|
17
|
-
writeSecure(path, `${secret}\n`);
|
|
18
|
-
}
|
|
19
|
-
process.env.METRO_SESSION_SECRET = secret;
|
|
20
|
-
return secret;
|
|
21
|
-
}
|