@stage-labs/metro 0.1.0-beta.45 → 0.1.0-beta.47
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 +5 -5
- package/dist/bedrock-proxy.js +1 -1
- package/dist/cli.js +7 -7
- package/dist/plugin.js +1 -1
- package/dist/prompt.js +1 -1
- package/dist/runtime.js +1 -1
- package/dist/serve.js +35 -5
- package/package.json +1 -1
- package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +0 -1
- package/runtime/node_modules/@metro-labs/mcp/package.json +1 -5
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-import.ts +2 -2
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-grant.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +2 -5
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +13 -26
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/claude-files.ts +1 -3
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +2 -4
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connect-hint.ts +11 -10
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +2 -2
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-tools.ts +3 -5
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +1 -4
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/events.ts +2 -2
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +1 -2
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +4 -83
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/is-record.ts +2 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-cli-api.ts +2 -2
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-mode.ts +3 -39
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-owner.ts +14 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-client.ts +1 -4
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-discovery.ts +1 -4
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +0 -33
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/protocol.ts +3 -3
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +2 -2
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-config.ts +1 -6
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +1 -42
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-auth.ts +2 -11
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-routes.ts +2 -2
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +8 -90
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/version.ts +1 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-connectors.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-config.ts +1 -4
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-relay.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/db/file-admin.ts +20 -34
- package/runtime/node_modules/@metro-labs/mcp/src/db/file-source.ts +1 -3
- package/runtime/node_modules/@metro-labs/mcp/src/db/local-connectors.ts +11 -22
- package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +3 -13
- package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +3 -3
- package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +2 -4
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/event-store.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +1 -21
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-resolve.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +1 -1
- package/runtime/node_modules/@metro-labs/telegram/package.json +0 -2
- package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +0 -1
- package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +0 -1
- package/runtime/node_modules/@metro-labs/webhook/src/station.ts +2 -5
- package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +0 -1
- package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +0 -1
- package/runtime/runtime.json +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/db/token-carry.ts +0 -45
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
VerifiedServer,
|
|
6
6
|
} from '../daemon/connector-verify.js';
|
|
7
7
|
import { readStoredTools } from '../daemon/connector-tools.js';
|
|
8
|
+
import { isRecord } from '../daemon/is-record.js';
|
|
8
9
|
|
|
9
10
|
const CONNECTOR_NAME_MAX = 64;
|
|
10
11
|
const HEADER_NAME_RE = /^[A-Za-z0-9!#$%&'*+.^_`|~-]{1,64}$/;
|
|
@@ -22,10 +23,6 @@ export interface ConnectorConfig {
|
|
|
22
23
|
oauth: boolean;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
26
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
26
|
function text(value: unknown): string {
|
|
30
27
|
return typeof value === 'string' ? value : '';
|
|
31
28
|
}
|
|
@@ -25,7 +25,7 @@ export function staleUsable(auth: OAuthAuth, now = Date.now()): boolean {
|
|
|
25
25
|
return auth.expiresAt === undefined || auth.expiresAt > now;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
function headerAuthHeaders(
|
|
29
29
|
auth: ConnectorAuth,
|
|
30
30
|
): Record<string, string> | null {
|
|
31
31
|
return auth.kind === 'header' ? { [auth.name]: auth.value } : null;
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
closeSync,
|
|
3
|
-
existsSync,
|
|
4
|
-
openSync,
|
|
5
|
-
readFileSync,
|
|
6
|
-
rmdirSync,
|
|
7
|
-
rmSync,
|
|
8
|
-
writeSync,
|
|
9
|
-
} from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync, rmdirSync, rmSync } from 'node:fs';
|
|
10
2
|
import { join } from 'node:path';
|
|
11
3
|
import { ApiError } from '../daemon/api-error.js';
|
|
12
4
|
import { ensureSecureDir, writeSecure } from '../daemon/secure-fs.js';
|
|
@@ -54,31 +46,23 @@ export function localOwner(dir = agentsDir()): string | null {
|
|
|
54
46
|
}
|
|
55
47
|
}
|
|
56
48
|
|
|
57
|
-
function
|
|
58
|
-
let fd: number;
|
|
59
|
-
try {
|
|
60
|
-
fd = openSync(path, 'wx', 0o600);
|
|
61
|
-
} catch (err) {
|
|
62
|
-
if ((err as NodeJS.ErrnoException).code === 'EEXIST') return false;
|
|
63
|
-
throw err;
|
|
64
|
-
}
|
|
65
|
-
writeSync(fd, `${address}\n`);
|
|
66
|
-
closeSync(fd);
|
|
67
|
-
return true;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function claimLocalOwner(raw: string, dir = agentsDir()): Promise<string> {
|
|
49
|
+
export function setLocalOwner(raw: string, dir = agentsDir()): string {
|
|
71
50
|
const address = normalizeAddress(raw);
|
|
72
|
-
if (address === null)
|
|
73
|
-
return Promise.reject(new ApiError('an Ethereum address is required', 400));
|
|
51
|
+
if (address === null) throw new ApiError(`'${raw}' is not an Ethereum address`, 400);
|
|
74
52
|
ensureSecureDir(dir);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
53
|
+
writeSecure(join(dir, OWNER_FILE), `${address}\n`);
|
|
54
|
+
return address;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function ownerSignIn(raw: string, dir = agentsDir()): Promise<string> {
|
|
58
|
+
const owner = localOwner(dir);
|
|
59
|
+
if (owner === null)
|
|
78
60
|
return Promise.reject(
|
|
79
|
-
new ApiError('this machine
|
|
61
|
+
new ApiError('this machine has no owner yet: start it with metro serve --owner <address>', 403),
|
|
80
62
|
);
|
|
81
|
-
|
|
63
|
+
if (owner !== normalizeAddress(raw))
|
|
64
|
+
return Promise.reject(new ApiError('this machine belongs to another wallet', 403));
|
|
65
|
+
return Promise.resolve(owner);
|
|
82
66
|
}
|
|
83
67
|
|
|
84
68
|
function isOwner(subject: string, dir: string): boolean {
|
|
@@ -278,10 +262,12 @@ export function localSetAgentConnectors(
|
|
|
278
262
|
save(stored);
|
|
279
263
|
}
|
|
280
264
|
|
|
281
|
-
export function
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
.
|
|
265
|
+
export function localHoldEverywhere(connectorId: string, dir = agentsDir()): void {
|
|
266
|
+
for (const stored of storedAgents(dir)) {
|
|
267
|
+
if (stored.file.connectors.includes(connectorId)) continue;
|
|
268
|
+
stored.file.connectors = [...stored.file.connectors, connectorId];
|
|
269
|
+
save(stored);
|
|
270
|
+
}
|
|
285
271
|
}
|
|
286
272
|
|
|
287
273
|
export function localDropConnectorEverywhere(connectorId: string, dir = agentsDir()): void {
|
|
@@ -4,6 +4,7 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { AGENT_NAME_RE, ID_RE } from './ids.js';
|
|
5
5
|
import { STATIONS, type StationName } from './schema.js';
|
|
6
6
|
import type { LoadedAccount, LoadedAgent, StationSource } from './materialize.js';
|
|
7
|
+
import { isRecord } from '../daemon/is-record.js';
|
|
7
8
|
|
|
8
9
|
export const AGENT_FILE = 'agent.json';
|
|
9
10
|
|
|
@@ -30,9 +31,6 @@ export function agentsDir(): string {
|
|
|
30
31
|
: join(homedir(), '.metro', 'agents');
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
34
|
-
typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
35
|
-
|
|
36
34
|
function fail(path: string, reason: string): never {
|
|
37
35
|
throw new AgentFileError(`${path}: ${reason}`);
|
|
38
36
|
}
|
|
@@ -41,8 +41,8 @@ import {
|
|
|
41
41
|
assertLocalOwner,
|
|
42
42
|
connectorIdsOfLocalAgent,
|
|
43
43
|
localAgentConnectorIds,
|
|
44
|
-
localAgentsWith,
|
|
45
44
|
localDropConnectorEverywhere,
|
|
45
|
+
localHoldEverywhere,
|
|
46
46
|
localOwnedAgentOrThrow,
|
|
47
47
|
localSetAgentConnectors,
|
|
48
48
|
LOCAL_PROJECT_ID,
|
|
@@ -70,7 +70,7 @@ const isRow = (v: unknown): v is { id: string; name: string; url: string; config
|
|
|
70
70
|
const asList = (value: unknown): unknown[] =>
|
|
71
71
|
Array.isArray(value) ? value.map((item: unknown) => item) : [];
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
function readLocalConnectors(dir = agentsDir()): LocalConnectorRow[] {
|
|
74
74
|
const raw = readJson<{ connectors?: unknown }>(filePath(dir), {}, {
|
|
75
75
|
warn: 'connectors.json: malformed, ignoring',
|
|
76
76
|
});
|
|
@@ -125,9 +125,16 @@ export async function localGetConnector(subject: string, id: string, dir = agent
|
|
|
125
125
|
return Promise.resolve(connectorFromRow(rowOrThrow(subject, id, dir)));
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
function assertNameFree(name: string, exceptId: string | null, dir: string): void {
|
|
129
|
+
if (readLocalConnectors(dir).some((r) => r.id !== exceptId && r.name === name))
|
|
130
|
+
throw new ConnectorError(`a connector named '${name}' already exists on this daemon`, 409);
|
|
131
|
+
}
|
|
132
|
+
|
|
128
133
|
function insert(dir: string, name: string, url: URL, config: ConnectorConfig): Connector {
|
|
134
|
+
assertNameFree(name, null, dir);
|
|
129
135
|
const row: LocalConnectorRow = { id: newId(), name, url: connectorUrlText(url), transport: 'http', config };
|
|
130
136
|
writeRows(dir, [...readLocalConnectors(dir), row]);
|
|
137
|
+
localHoldEverywhere(row.id, dir);
|
|
131
138
|
return connectorFromRow(row);
|
|
132
139
|
}
|
|
133
140
|
|
|
@@ -201,24 +208,10 @@ export async function localVerifyConnector(subject: string, id: string, dir = ag
|
|
|
201
208
|
}
|
|
202
209
|
}
|
|
203
210
|
|
|
204
|
-
const nameClash = (name: string, agent: string): ConnectorError =>
|
|
205
|
-
new ConnectorError(`the agent '${agent}' already has a connector named '${name}'`, 409);
|
|
206
|
-
|
|
207
|
-
function clashFor(name: string, exceptId: string, agentIds: string[], dir: string): string | null {
|
|
208
|
-
const rows = readLocalConnectors(dir);
|
|
209
|
-
for (const agentId of agentIds) {
|
|
210
|
-
const held = connectorIdsOfLocalAgent(agentId, dir) ?? [];
|
|
211
|
-
const other = rows.find((r) => r.id !== exceptId && held.includes(r.id) && r.name === name);
|
|
212
|
-
if (other !== undefined) return localAgentsWith(other.id, dir).find((a) => a.id === agentId)?.name ?? agentId;
|
|
213
|
-
}
|
|
214
|
-
return null;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
211
|
export async function localRenameConnector(subject: string, id: string, raw: string, dir = agentsDir()): Promise<Connector> {
|
|
218
212
|
const name = connectorName(raw);
|
|
219
213
|
const row = rowOrThrow(subject, id, dir);
|
|
220
|
-
|
|
221
|
-
if (clash !== null) throw nameClash(name, clash);
|
|
214
|
+
assertNameFree(name, id, dir);
|
|
222
215
|
return Promise.resolve(replace(dir, { ...row, name }));
|
|
223
216
|
}
|
|
224
217
|
|
|
@@ -252,11 +245,7 @@ export async function localAddConnector(subject: string, agentId: string, connec
|
|
|
252
245
|
const row = readLocalConnectors(dir).find((r) => r.id === connectorId);
|
|
253
246
|
if (row === undefined) throw missing();
|
|
254
247
|
const held = localAgentConnectorIds(subject, agentId, dir);
|
|
255
|
-
if (!held.includes(connectorId))
|
|
256
|
-
const clash = clashFor(row.name, connectorId, [agentId], dir);
|
|
257
|
-
if (clash !== null) throw nameClash(row.name, clash);
|
|
258
|
-
localSetAgentConnectors(subject, agentId, [...held, connectorId], dir);
|
|
259
|
-
}
|
|
248
|
+
if (!held.includes(connectorId)) localSetAgentConnectors(subject, agentId, [...held, connectorId], dir);
|
|
260
249
|
return { ...agent, connectorIds: localAgentConnectorIds(subject, agentId, dir) };
|
|
261
250
|
}
|
|
262
251
|
|
|
@@ -21,7 +21,6 @@ import {
|
|
|
21
21
|
type AllowlistMap,
|
|
22
22
|
} from './agent-map.js';
|
|
23
23
|
import { setKeyMap } from './key-map.js';
|
|
24
|
-
import { carryTokenStores, PREVIOUS_ACCOUNT_ID } from './token-carry.js';
|
|
25
24
|
import { STATIONS, stations, agents, type StationName } from './schema.js';
|
|
26
25
|
|
|
27
26
|
interface StationTarget {
|
|
@@ -149,7 +148,7 @@ export async function loadAgentForRuntime(
|
|
|
149
148
|
};
|
|
150
149
|
}
|
|
151
150
|
|
|
152
|
-
|
|
151
|
+
const pgSource: StationSource = () => loadAgents();
|
|
153
152
|
|
|
154
153
|
async function loadAgents(): Promise<LoadedAgent[]> {
|
|
155
154
|
const db = getDb();
|
|
@@ -182,14 +181,6 @@ async function loadAgents(): Promise<LoadedAgent[]> {
|
|
|
182
181
|
return out;
|
|
183
182
|
}
|
|
184
183
|
|
|
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
184
|
function trainStubPath(station: StationName): string {
|
|
194
185
|
return join(trainsDir(), `${station}.ts`);
|
|
195
186
|
}
|
|
@@ -208,7 +199,7 @@ export function writeIfChanged(path: string, content: string): boolean {
|
|
|
208
199
|
return true;
|
|
209
200
|
}
|
|
210
201
|
|
|
211
|
-
|
|
202
|
+
const isLocalRuntime = (): boolean =>
|
|
212
203
|
(process.env.METRO_RUN_TOKEN?.trim() ?? '') !== '';
|
|
213
204
|
|
|
214
205
|
export const stationRunsHere = (station: StationName): boolean =>
|
|
@@ -248,12 +239,11 @@ function writeStations(list: LoadedAgent[]): WrittenStations {
|
|
|
248
239
|
}
|
|
249
240
|
setAgentMap(map, names);
|
|
250
241
|
setAllowlistMap(allow);
|
|
251
|
-
carryTokenStores(list.flatMap((agent) => agent.accounts));
|
|
252
242
|
|
|
253
243
|
const active = new Map<StationName, number>();
|
|
254
244
|
const changed: StationName[] = [];
|
|
255
245
|
for (const [station, accts] of byStation) {
|
|
256
|
-
const records = accts.map((a) => ({ id: a.id, ...
|
|
246
|
+
const records = accts.map((a) => ({ id: a.id, ...a.config }));
|
|
257
247
|
const path = accountFilePath(station);
|
|
258
248
|
const content = JSON.stringify(records, null, 2);
|
|
259
249
|
if (currentText(path) !== content) {
|
|
@@ -33,7 +33,7 @@ const CONTROL = /[\u0000-\u001f\u007f]/;
|
|
|
33
33
|
const missing = (): ProjectError => new ProjectError('no such project', 404);
|
|
34
34
|
const forbidden = (why: string): ProjectError => new ProjectError(why, 403);
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
function projectName(raw: unknown): string {
|
|
37
37
|
const name = typeof raw === 'string' ? raw.trim() : '';
|
|
38
38
|
if (name === '' || name.length > NAME_MAX)
|
|
39
39
|
throw new ProjectError(
|
|
@@ -54,7 +54,7 @@ export interface Access {
|
|
|
54
54
|
role: ProjectRole;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
async function memberAccessOrThrow(
|
|
58
58
|
subject: string,
|
|
59
59
|
projectId: string,
|
|
60
60
|
): Promise<Access> {
|
|
@@ -101,7 +101,7 @@ async function addMemberRow(
|
|
|
101
101
|
.onConflictDoNothing();
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
async function ensureDefaultProject(userId: string): Promise<string> {
|
|
105
105
|
const existing = await getDb()
|
|
106
106
|
.select({ id: projects.id })
|
|
107
107
|
.from(projects)
|
|
@@ -91,7 +91,7 @@ export async function runtimeLabels(
|
|
|
91
91
|
return out;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
async function runtimeFor(agentId: string): Promise<RuntimeView | null> {
|
|
95
95
|
const rows = await getDb()
|
|
96
96
|
.select({
|
|
97
97
|
id: runtimes.id,
|
|
@@ -23,9 +23,8 @@ export const users = pgTable('users', {
|
|
|
23
23
|
address: text('address').unique(),
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
export const PROJECT_ROLES = ['admin', 'member'] as const;
|
|
27
26
|
|
|
28
|
-
export type ProjectRole =
|
|
27
|
+
export type ProjectRole = 'admin' | 'member';
|
|
29
28
|
|
|
30
29
|
export const projects = pgTable('projects', {
|
|
31
30
|
id: text('id').primaryKey(),
|
|
@@ -80,9 +79,8 @@ export const stations = pgTable('stations', {
|
|
|
80
79
|
config: jsonb('config').notNull(),
|
|
81
80
|
});
|
|
82
81
|
|
|
83
|
-
export const CONNECTOR_TRANSPORTS = ['http', 'sse'] as const;
|
|
84
82
|
|
|
85
|
-
export type ConnectorTransport =
|
|
83
|
+
export type ConnectorTransport = 'http' | 'sse';
|
|
86
84
|
|
|
87
85
|
export const connectors = pgTable(
|
|
88
86
|
'connectors',
|
|
@@ -6,7 +6,7 @@ import type {
|
|
|
6
6
|
import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
|
|
7
7
|
import { frameInScope, frameLine } from './frame-scope.js';
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
const EVENT_STORE_MAX = 500;
|
|
10
10
|
|
|
11
11
|
interface StoredEvent {
|
|
12
12
|
eventId: EventId;
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
authConfigFromEnv,
|
|
10
10
|
authenticate,
|
|
11
11
|
} from '../mcp/request-identity.js';
|
|
12
|
+
import { METRO_VERSION } from '../daemon/version.js';
|
|
12
13
|
|
|
13
14
|
export type MonitorCall = (
|
|
14
15
|
train: string,
|
|
@@ -18,7 +19,6 @@ export type MonitorCall = (
|
|
|
18
19
|
|
|
19
20
|
const KEEPALIVE_MS = 25_000;
|
|
20
21
|
const CALL_BODY_MAX = 256 * 1024;
|
|
21
|
-
const METRO_VERSION = process.env.npm_package_version ?? '0.1.0-beta.15';
|
|
22
22
|
|
|
23
23
|
function monitorEnabled(): boolean {
|
|
24
24
|
return hasAnyKey() || authConfigFromEnv().sessionSecret !== '';
|
|
@@ -7,7 +7,6 @@ export type Die = (msg: string) => never;
|
|
|
7
7
|
export interface MakeLoaderOpts<T> {
|
|
8
8
|
prefix: string;
|
|
9
9
|
file: string;
|
|
10
|
-
allowlistEnv: string[];
|
|
11
10
|
validate: (raw: T[], die: Die) => void;
|
|
12
11
|
}
|
|
13
12
|
|
|
@@ -24,17 +23,6 @@ export function makeAccountStore<T extends { id: string }>(
|
|
|
24
23
|
process.exit(2);
|
|
25
24
|
};
|
|
26
25
|
|
|
27
|
-
let allowlistRaw: string | undefined;
|
|
28
|
-
for (const k of opts.allowlistEnv) {
|
|
29
|
-
allowlistRaw ??= process.env[k];
|
|
30
|
-
}
|
|
31
|
-
const allowlist = new Set(
|
|
32
|
-
(allowlistRaw ?? '')
|
|
33
|
-
.split(',')
|
|
34
|
-
.map((s) => s.trim())
|
|
35
|
-
.filter(Boolean),
|
|
36
|
-
);
|
|
37
|
-
|
|
38
26
|
function loadAccounts(): T[] {
|
|
39
27
|
if (!existsSync(opts.file))
|
|
40
28
|
return die(
|
|
@@ -50,15 +38,7 @@ export function makeAccountStore<T extends { id: string }>(
|
|
|
50
38
|
if (!Array.isArray(raw) || raw.length === 0)
|
|
51
39
|
die(`${opts.file} must be a non-empty array`);
|
|
52
40
|
opts.validate(raw, die);
|
|
53
|
-
|
|
54
|
-
? raw.filter((a) => allowlist.has(a.id))
|
|
55
|
-
: raw;
|
|
56
|
-
if (selected.length === 0) {
|
|
57
|
-
die(
|
|
58
|
-
`no accounts match ${opts.allowlistEnv[0]} (${[...allowlist].join(', ')})`,
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
return selected;
|
|
41
|
+
return raw;
|
|
62
42
|
}
|
|
63
43
|
|
|
64
44
|
return { die, loadAccounts };
|
|
@@ -151,7 +151,7 @@ const sourcesOf = (a: CanonicalAttachment): string[] =>
|
|
|
151
151
|
return typeof v === 'string' && v.length > 0;
|
|
152
152
|
});
|
|
153
153
|
|
|
154
|
-
|
|
154
|
+
async function resolveAttachment(
|
|
155
155
|
a: CanonicalAttachment,
|
|
156
156
|
budget: InlineBudget = { used: 0 },
|
|
157
157
|
opts: ResolveOptions = {},
|
|
@@ -5,7 +5,7 @@ const PREFIX = 'metro://';
|
|
|
5
5
|
const build = (station: string, ...seg: (string | number)[]): Line =>
|
|
6
6
|
asLine(`${PREFIX}${station}/${seg.map(String).join('/')}`);
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
function parseAccountScoped(
|
|
9
9
|
line: Line | string,
|
|
10
10
|
station: string,
|
|
11
11
|
validate?: (resource: string) => boolean,
|
|
@@ -29,7 +29,6 @@ function validateAccount(a: UserAccount, seen: Set<string>, die: Die): void {
|
|
|
29
29
|
export const { loadAccounts } = makeAccountStore<UserAccount>({
|
|
30
30
|
prefix: 'telegram',
|
|
31
31
|
file: ACCOUNTS_FILE,
|
|
32
|
-
allowlistEnv: ['TELEGRAM_ONLY_ACCOUNTS', 'TELEGRAM_ACCOUNTS'],
|
|
33
32
|
validate(raw, die) {
|
|
34
33
|
const seen = new Set<string>();
|
|
35
34
|
for (const a of raw) validateAccount(a, seen, die);
|
|
@@ -19,7 +19,6 @@ export interface AccountConfig {
|
|
|
19
19
|
export const { loadAccounts } = makeAccountStore<AccountConfig>({
|
|
20
20
|
prefix: 'telegram-bot',
|
|
21
21
|
file: ACCOUNTS_FILE,
|
|
22
|
-
allowlistEnv: ['TELEGRAM_BOT_ONLY_ACCOUNTS', 'TELEGRAM_BOT_ACCOUNTS'],
|
|
23
22
|
validate(raw, die) {
|
|
24
23
|
const seenId = new Set<string>();
|
|
25
24
|
const seenTok = new Set<string>();
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { Line
|
|
2
|
+
import { Line } from '@metro-labs/mcp/lines';
|
|
3
3
|
import { mintId, type MetroEvent } from '@metro-labs/mcp/events';
|
|
4
4
|
import type { Endpoint } from '@metro-labs/mcp/endpoints';
|
|
5
5
|
import type { Station, Verb } from '@metro-labs/mcp/stations/types';
|
|
6
6
|
|
|
7
|
-
const sessionOwner = (sessionId: string): Line =>
|
|
8
|
-
asLine(`metro://session/${sessionId}`);
|
|
9
|
-
|
|
10
7
|
export const webhookStation: Station = {
|
|
11
8
|
name: 'webhook',
|
|
12
9
|
hasAccounts: true,
|
|
@@ -31,7 +28,7 @@ export function webhookEntry(
|
|
|
31
28
|
line,
|
|
32
29
|
lineName: endpoint.label,
|
|
33
30
|
from: line,
|
|
34
|
-
to:
|
|
31
|
+
to: line,
|
|
35
32
|
messageId:
|
|
36
33
|
headers['x-github-delivery'] ?? headers['x-request-id'] ?? randomUUID(),
|
|
37
34
|
text: `${headers['x-github-event'] ?? headers['x-intercom-topic'] ?? 'event'} ${method} ${url}`,
|
|
@@ -28,7 +28,6 @@ function validateAccount(
|
|
|
28
28
|
export const { loadAccounts } = makeAccountStore<WhatsAppAccount>({
|
|
29
29
|
prefix: 'whatsapp',
|
|
30
30
|
file: ACCOUNTS_FILE,
|
|
31
|
-
allowlistEnv: ['WHATSAPP_ONLY_ACCOUNTS', 'WHATSAPP_ACCOUNTS'],
|
|
32
31
|
validate(raw, die) {
|
|
33
32
|
const seen = new Set<string>();
|
|
34
33
|
for (const a of raw) validateAccount(a, seen, die);
|
|
@@ -28,7 +28,6 @@ export interface AccountConfig {
|
|
|
28
28
|
export const { loadAccounts } = makeAccountStore<AccountConfig>({
|
|
29
29
|
prefix: 'xmtp',
|
|
30
30
|
file: ACCOUNTS_FILE,
|
|
31
|
-
allowlistEnv: ['XMTP_ONLY_ACCOUNTS', 'XMTP_ACCOUNTS'],
|
|
32
31
|
validate(raw, die) {
|
|
33
32
|
const seen = new Set<string>();
|
|
34
33
|
for (const a of raw) {
|
package/runtime/runtime.json
CHANGED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { tokenStorePath } from '@metro-labs/whatsapp/tokens';
|
|
3
|
-
import { errMsg, log } from '../daemon/log.js';
|
|
4
|
-
import { writeSecure } from '../daemon/secure-fs.js';
|
|
5
|
-
|
|
6
|
-
export const PREVIOUS_ACCOUNT_ID = 'previousAccountId';
|
|
7
|
-
|
|
8
|
-
export interface CarriedStation {
|
|
9
|
-
station: string;
|
|
10
|
-
id: string;
|
|
11
|
-
config: Record<string, unknown>;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function previousAccountId(
|
|
15
|
-
config: Record<string, unknown>,
|
|
16
|
-
): string | undefined {
|
|
17
|
-
const raw = config[PREVIOUS_ACCOUNT_ID];
|
|
18
|
-
return typeof raw === 'string' && raw !== '' ? raw : undefined;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function carryTokenStores(list: CarriedStation[]): number {
|
|
22
|
-
let carried = 0;
|
|
23
|
-
for (const s of list) {
|
|
24
|
-
if (s.station !== 'whatsapp') continue;
|
|
25
|
-
const previous = previousAccountId(s.config);
|
|
26
|
-
if (previous === undefined || previous === s.id) continue;
|
|
27
|
-
const from = tokenStorePath(previous);
|
|
28
|
-
const to = tokenStorePath(s.id);
|
|
29
|
-
if (!existsSync(from) || existsSync(to)) continue;
|
|
30
|
-
try {
|
|
31
|
-
writeSecure(to, readFileSync(from, 'utf8'));
|
|
32
|
-
carried += 1;
|
|
33
|
-
log.info(
|
|
34
|
-
{ from: previous, to: s.id },
|
|
35
|
-
'db: carried the whatsapp token store to the station id',
|
|
36
|
-
);
|
|
37
|
-
} catch (err) {
|
|
38
|
-
log.error(
|
|
39
|
-
{ from: previous, to: s.id, err: errMsg(err) },
|
|
40
|
-
'db: could not carry the whatsapp token store',
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return carried;
|
|
45
|
-
}
|