@stage-labs/metro 0.1.0-beta.50 → 0.1.0-beta.52
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 +8 -142
- package/dist/cli.js +27 -131
- package/dist/control.js +5 -104
- package/dist/plugin.js +2 -4
- package/dist/runtime.js +18 -72
- package/dist/serve.js +3 -2
- package/dist/tail.js +7 -8
- package/dist/update.js +13 -1
- package/package.json +1 -1
- package/runtime/node_modules/@metro-labs/mcp/package.json +1 -2
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +3 -35
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-connector-api.ts +7 -22
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +4 -54
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +1 -2
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +29 -208
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +0 -4
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-mode.ts +2 -37
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +0 -3
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/relay.ts +4 -6
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +3 -14
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +0 -81
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-routes.ts +2 -2
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/update-api.ts +119 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/vault-api.ts +42 -12
- package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +1 -73
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +1 -172
- package/runtime/node_modules/@metro-labs/mcp/src/db/client.ts +0 -5
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-relay.ts +2 -89
- package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +3 -262
- package/runtime/node_modules/@metro-labs/mcp/src/db/local-connectors.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +2 -140
- package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +1 -89
- package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +14 -41
- package/runtime/node_modules/@metro-labs/mcp/src/db/vault.ts +5 -5
- package/runtime/node_modules/@metro-labs/whatsapp/package.json +0 -2
- package/runtime/runtime.json +1 -1
- package/dist/prompt.js +0 -61
- package/dist/store.js +0 -93
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-import.ts +0 -76
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-pair.ts +0 -20
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +0 -141
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/import-api.ts +0 -68
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/pair-codes.ts +0 -58
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/project-api.ts +0 -214
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +0 -139
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-source.ts +0 -105
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-connectors.ts +0 -143
- package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +0 -309
- package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +0 -24
- package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +0 -105
- package/runtime/node_modules/@metro-labs/mcp/src/db/whatsapp-login.ts +0 -18
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import { errMsg, log } from './log.js';
|
|
2
|
-
import type { LoadedAgent, StationSource } from '../db/materialize.js';
|
|
3
|
-
|
|
4
|
-
const POLL_MS = 30_000;
|
|
5
|
-
const BACKOFF_MS = 5 * 60_000;
|
|
6
|
-
const FETCH_TIMEOUT_MS = 15_000;
|
|
7
|
-
|
|
8
|
-
export class RuntimeRevoked extends Error {}
|
|
9
|
-
|
|
10
|
-
const LOOPBACK = new Set(['127.0.0.1', '::1', 'localhost']);
|
|
11
|
-
|
|
12
|
-
export function carriesSecretsSafely(base: string): boolean {
|
|
13
|
-
let url: URL;
|
|
14
|
-
try {
|
|
15
|
-
url = new URL(base);
|
|
16
|
-
} catch {
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
19
|
-
if (url.protocol === 'https:') return true;
|
|
20
|
-
return url.protocol === 'http:' && LOOPBACK.has(url.hostname);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface RuntimeConfig {
|
|
24
|
-
url: string;
|
|
25
|
-
token: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function runtimeConfigFromEnv(): RuntimeConfig | null {
|
|
29
|
-
const token = process.env.METRO_RUN_TOKEN?.trim() ?? '';
|
|
30
|
-
if (token === '') return null;
|
|
31
|
-
const raw = (process.env.METRO_URL?.trim() ?? '').replace(/\/+$/, '');
|
|
32
|
-
const url = raw === '' ? 'https://mcp.metro.box' : raw;
|
|
33
|
-
if (!carriesSecretsSafely(url))
|
|
34
|
-
throw new Error(
|
|
35
|
-
`refusing to fetch station credentials from ${url} in the clear — ` +
|
|
36
|
-
'use https, or a loopback address',
|
|
37
|
-
);
|
|
38
|
-
return { url, token };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function httpSource(cfg: RuntimeConfig): StationSource {
|
|
42
|
-
return async () => {
|
|
43
|
-
const res = await fetch(`${cfg.url}/api/run/config`, {
|
|
44
|
-
headers: { authorization: `Bearer ${cfg.token}` },
|
|
45
|
-
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
46
|
-
redirect: 'manual',
|
|
47
|
-
});
|
|
48
|
-
if (res.status === 401 || res.status === 403 || res.status === 409)
|
|
49
|
-
throw new RuntimeRevoked(
|
|
50
|
-
`metro no longer recognises this machine as the holder (${String(res.status)})`,
|
|
51
|
-
);
|
|
52
|
-
if (!res.ok)
|
|
53
|
-
throw new Error(`metro answered ${String(res.status)} for the run config`);
|
|
54
|
-
const body = (await res.json()) as { agent?: LoadedAgent };
|
|
55
|
-
if (!body.agent) throw new Error('metro returned no agent');
|
|
56
|
-
return [body.agent];
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export interface PollerDeps {
|
|
61
|
-
sync: () => Promise<void>;
|
|
62
|
-
stopAll: () => Promise<void>;
|
|
63
|
-
everyMs?: number;
|
|
64
|
-
backoffMs?: number;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function startRuntimePoller(deps: PollerDeps): () => void {
|
|
68
|
-
const everyMs = deps.everyMs ?? POLL_MS;
|
|
69
|
-
const backoffMs = deps.backoffMs ?? BACKOFF_MS;
|
|
70
|
-
let stopped = false;
|
|
71
|
-
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
72
|
-
|
|
73
|
-
const arm = (ms: number): void => {
|
|
74
|
-
if (stopped) return;
|
|
75
|
-
timer = setTimeout(() => {
|
|
76
|
-
tick().catch((err: unknown) => {
|
|
77
|
-
log.error({ err: errMsg(err) }, 'runtime: poller failed');
|
|
78
|
-
});
|
|
79
|
-
}, ms);
|
|
80
|
-
timer.unref?.();
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
async function tick(): Promise<void> {
|
|
84
|
-
if (stopped) return;
|
|
85
|
-
try {
|
|
86
|
-
await deps.sync();
|
|
87
|
-
arm(everyMs);
|
|
88
|
-
} catch (err) {
|
|
89
|
-
if (err instanceof RuntimeRevoked) {
|
|
90
|
-
stopped = true;
|
|
91
|
-
log.error({ reason: err.message }, 'runtime: revoked — stopping stations');
|
|
92
|
-
await deps.stopAll();
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
log.warn({ err: errMsg(err) }, 'runtime: could not reach metro; retrying');
|
|
96
|
-
arm(backoffMs);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
arm(everyMs);
|
|
101
|
-
return () => {
|
|
102
|
-
stopped = true;
|
|
103
|
-
if (timer !== null) clearTimeout(timer);
|
|
104
|
-
};
|
|
105
|
-
}
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import { and, asc, eq, inArray, ne } from 'drizzle-orm';
|
|
2
|
-
import { getDb } from './client.js';
|
|
3
|
-
import { newId } from './ids.js';
|
|
4
|
-
import { isUniqueViolation } from './users.js';
|
|
5
|
-
import { ownedAgentOrThrow } from './agent-admin.js';
|
|
6
|
-
import { ConnectorError } from './connector-config.js';
|
|
7
|
-
import { agentConnectors, agents, connectors } from './schema.js';
|
|
8
|
-
|
|
9
|
-
export interface AgentConnectors {
|
|
10
|
-
id: string;
|
|
11
|
-
name: string;
|
|
12
|
-
connectorIds: string[];
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const nameClash = (name: string, agent: string): ConnectorError =>
|
|
16
|
-
new ConnectorError(
|
|
17
|
-
`the agent '${agent}' already has a connector named '${name}'`,
|
|
18
|
-
409,
|
|
19
|
-
);
|
|
20
|
-
|
|
21
|
-
async function collidingAgent(
|
|
22
|
-
agentIds: string[],
|
|
23
|
-
name: string,
|
|
24
|
-
exceptConnectorId: string,
|
|
25
|
-
): Promise<string | null> {
|
|
26
|
-
if (agentIds.length === 0) return null;
|
|
27
|
-
const rows = await getDb()
|
|
28
|
-
.select({ agent: agents.name })
|
|
29
|
-
.from(agentConnectors)
|
|
30
|
-
.innerJoin(connectors, eq(connectors.id, agentConnectors.connectorId))
|
|
31
|
-
.innerJoin(agents, eq(agents.id, agentConnectors.agentId))
|
|
32
|
-
.where(
|
|
33
|
-
and(
|
|
34
|
-
inArray(agentConnectors.agentId, agentIds),
|
|
35
|
-
eq(connectors.name, name),
|
|
36
|
-
ne(connectors.id, exceptConnectorId),
|
|
37
|
-
),
|
|
38
|
-
)
|
|
39
|
-
.limit(1);
|
|
40
|
-
return rows[0]?.agent ?? null;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export async function assertRenameFreeOfClash(
|
|
44
|
-
connectorId: string,
|
|
45
|
-
name: string,
|
|
46
|
-
): Promise<void> {
|
|
47
|
-
const rows = await getDb()
|
|
48
|
-
.select({ agentId: agentConnectors.agentId })
|
|
49
|
-
.from(agentConnectors)
|
|
50
|
-
.where(eq(agentConnectors.connectorId, connectorId));
|
|
51
|
-
const clash = await collidingAgent(
|
|
52
|
-
rows.map((r) => r.agentId),
|
|
53
|
-
name,
|
|
54
|
-
connectorId,
|
|
55
|
-
);
|
|
56
|
-
if (clash !== null) throw nameClash(name, clash);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async function connectorIdsOfAgent(agentId: string): Promise<string[]> {
|
|
60
|
-
const rows = await getDb()
|
|
61
|
-
.select({ connectorId: agentConnectors.connectorId })
|
|
62
|
-
.from(agentConnectors)
|
|
63
|
-
.where(eq(agentConnectors.agentId, agentId))
|
|
64
|
-
.orderBy(asc(agentConnectors.id));
|
|
65
|
-
return rows.map((r) => r.connectorId);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export async function connectorIdsByAgent(
|
|
69
|
-
agentIds: string[],
|
|
70
|
-
): Promise<Map<string, string[]>> {
|
|
71
|
-
const out = new Map<string, string[]>();
|
|
72
|
-
if (agentIds.length === 0) return out;
|
|
73
|
-
const rows = await getDb()
|
|
74
|
-
.select({
|
|
75
|
-
agentId: agentConnectors.agentId,
|
|
76
|
-
connectorId: agentConnectors.connectorId,
|
|
77
|
-
})
|
|
78
|
-
.from(agentConnectors)
|
|
79
|
-
.where(inArray(agentConnectors.agentId, agentIds))
|
|
80
|
-
.orderBy(asc(agentConnectors.id));
|
|
81
|
-
for (const row of rows) {
|
|
82
|
-
const list = out.get(row.agentId) ?? [];
|
|
83
|
-
list.push(row.connectorId);
|
|
84
|
-
out.set(row.agentId, list);
|
|
85
|
-
}
|
|
86
|
-
return out;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export async function agentConnectorsForUser(
|
|
90
|
-
subject: string,
|
|
91
|
-
agentId: string,
|
|
92
|
-
): Promise<AgentConnectors> {
|
|
93
|
-
const { agent } = await ownedAgentOrThrow(subject, agentId);
|
|
94
|
-
return { ...agent, connectorIds: await connectorIdsOfAgent(agent.id) };
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async function connectorNameIn(
|
|
98
|
-
projectId: string,
|
|
99
|
-
connectorId: string,
|
|
100
|
-
): Promise<string | null> {
|
|
101
|
-
const rows = await getDb()
|
|
102
|
-
.select({ name: connectors.name })
|
|
103
|
-
.from(connectors)
|
|
104
|
-
.where(and(eq(connectors.id, connectorId), eq(connectors.projectId, projectId)));
|
|
105
|
-
return rows[0]?.name ?? null;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export async function addConnectorToAgentForUser(
|
|
109
|
-
subject: string,
|
|
110
|
-
agentId: string,
|
|
111
|
-
connectorId: string,
|
|
112
|
-
): Promise<AgentConnectors> {
|
|
113
|
-
const { agent, projectId } = await ownedAgentOrThrow(subject, agentId);
|
|
114
|
-
const name = await connectorNameIn(projectId, connectorId);
|
|
115
|
-
if (name === null) throw new ConnectorError('no such connector', 404);
|
|
116
|
-
const clash = await collidingAgent([agent.id], name, connectorId);
|
|
117
|
-
if (clash !== null) throw nameClash(name, clash);
|
|
118
|
-
try {
|
|
119
|
-
await getDb()
|
|
120
|
-
.insert(agentConnectors)
|
|
121
|
-
.values({ id: newId(), agentId: agent.id, connectorId });
|
|
122
|
-
} catch (err) {
|
|
123
|
-
if (!isUniqueViolation(err)) throw err;
|
|
124
|
-
}
|
|
125
|
-
return { ...agent, connectorIds: await connectorIdsOfAgent(agent.id) };
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export async function removeConnectorFromAgentForUser(
|
|
129
|
-
subject: string,
|
|
130
|
-
agentId: string,
|
|
131
|
-
connectorId: string,
|
|
132
|
-
): Promise<AgentConnectors> {
|
|
133
|
-
const { agent } = await ownedAgentOrThrow(subject, agentId);
|
|
134
|
-
await getDb()
|
|
135
|
-
.delete(agentConnectors)
|
|
136
|
-
.where(
|
|
137
|
-
and(
|
|
138
|
-
eq(agentConnectors.agentId, agent.id),
|
|
139
|
-
eq(agentConnectors.connectorId, connectorId),
|
|
140
|
-
),
|
|
141
|
-
);
|
|
142
|
-
return { ...agent, connectorIds: await connectorIdsOfAgent(agent.id) };
|
|
143
|
-
}
|
|
@@ -1,309 +0,0 @@
|
|
|
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
|
-
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
|
-
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
|
-
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
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import { and, eq, isNull } from 'drizzle-orm';
|
|
2
|
-
import { getDb } from './client.js';
|
|
3
|
-
import { newId } from './ids.js';
|
|
4
|
-
import { agents, runtimes } from './schema.js';
|
|
5
|
-
import { ApiError } from '../daemon/api-error.js';
|
|
6
|
-
|
|
7
|
-
export interface RuntimeLease {
|
|
8
|
-
runtimeId: string;
|
|
9
|
-
agentId: string;
|
|
10
|
-
label: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface RuntimeView {
|
|
14
|
-
id: string;
|
|
15
|
-
label: string;
|
|
16
|
-
lastSeenAt: string | null;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const nowIso = (): string => new Date().toISOString();
|
|
20
|
-
|
|
21
|
-
const stale = (): ApiError =>
|
|
22
|
-
new ApiError('this runtime no longer holds the agent', 409);
|
|
23
|
-
|
|
24
|
-
export async function claimRuntime(
|
|
25
|
-
agentId: string,
|
|
26
|
-
label: string,
|
|
27
|
-
): Promise<RuntimeLease> {
|
|
28
|
-
const db = getDb();
|
|
29
|
-
const id = newId();
|
|
30
|
-
return db.transaction(async (tx) => {
|
|
31
|
-
await tx
|
|
32
|
-
.insert(runtimes)
|
|
33
|
-
.values({ id, agentId, label, createdAt: nowIso(), lastSeenAt: nowIso() });
|
|
34
|
-
const rows = await tx
|
|
35
|
-
.update(agents)
|
|
36
|
-
.set({ runtimeId: id })
|
|
37
|
-
.where(eq(agents.id, agentId))
|
|
38
|
-
.returning({ id: agents.id });
|
|
39
|
-
if (rows[0] === undefined) throw new ApiError('no such agent', 404);
|
|
40
|
-
return { runtimeId: id, agentId, label };
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export async function fenceRuntime(
|
|
45
|
-
runtimeId: string,
|
|
46
|
-
agentId: string,
|
|
47
|
-
): Promise<void> {
|
|
48
|
-
const rows = await getDb()
|
|
49
|
-
.select({ holder: agents.runtimeId })
|
|
50
|
-
.from(agents)
|
|
51
|
-
.innerJoin(runtimes, eq(runtimes.id, agents.runtimeId))
|
|
52
|
-
.where(
|
|
53
|
-
and(
|
|
54
|
-
eq(agents.id, agentId),
|
|
55
|
-
eq(agents.runtimeId, runtimeId),
|
|
56
|
-
isNull(runtimes.revokedAt),
|
|
57
|
-
),
|
|
58
|
-
);
|
|
59
|
-
if (rows[0] === undefined) throw stale();
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export async function touchRuntime(runtimeId: string): Promise<void> {
|
|
63
|
-
await getDb()
|
|
64
|
-
.update(runtimes)
|
|
65
|
-
.set({ lastSeenAt: nowIso() })
|
|
66
|
-
.where(eq(runtimes.id, runtimeId));
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export async function releaseRuntime(agentId: string): Promise<void> {
|
|
70
|
-
const db = getDb();
|
|
71
|
-
await db.transaction(async (tx) => {
|
|
72
|
-
await tx
|
|
73
|
-
.update(runtimes)
|
|
74
|
-
.set({ revokedAt: nowIso() })
|
|
75
|
-
.where(and(eq(runtimes.agentId, agentId), isNull(runtimes.revokedAt)));
|
|
76
|
-
await tx
|
|
77
|
-
.update(agents)
|
|
78
|
-
.set({ runtimeId: null })
|
|
79
|
-
.where(eq(agents.id, agentId));
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export async function runtimeLabels(
|
|
84
|
-
agentIds: string[],
|
|
85
|
-
): Promise<Map<string, string>> {
|
|
86
|
-
const out = new Map<string, string>();
|
|
87
|
-
for (const id of agentIds) {
|
|
88
|
-
const found = await runtimeFor(id);
|
|
89
|
-
if (found !== null) out.set(id, found.label);
|
|
90
|
-
}
|
|
91
|
-
return out;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async function runtimeFor(agentId: string): Promise<RuntimeView | null> {
|
|
95
|
-
const rows = await getDb()
|
|
96
|
-
.select({
|
|
97
|
-
id: runtimes.id,
|
|
98
|
-
label: runtimes.label,
|
|
99
|
-
lastSeenAt: runtimes.lastSeenAt,
|
|
100
|
-
})
|
|
101
|
-
.from(runtimes)
|
|
102
|
-
.innerJoin(agents, eq(agents.runtimeId, runtimes.id))
|
|
103
|
-
.where(and(eq(agents.id, agentId), isNull(runtimes.revokedAt)));
|
|
104
|
-
return rows[0] ?? null;
|
|
105
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { and, eq, sql } from 'drizzle-orm';
|
|
2
|
-
import { getDb } from './client.js';
|
|
3
|
-
import { stations } from './schema.js';
|
|
4
|
-
|
|
5
|
-
export async function writeWhatsappCredentials(
|
|
6
|
-
accountId: string,
|
|
7
|
-
credentials: unknown,
|
|
8
|
-
): Promise<void> {
|
|
9
|
-
const db = getDb();
|
|
10
|
-
await db
|
|
11
|
-
.update(stations)
|
|
12
|
-
.set({
|
|
13
|
-
config: sql`${stations.config} || ${JSON.stringify({ credentials })}::jsonb`,
|
|
14
|
-
})
|
|
15
|
-
.where(
|
|
16
|
-
and(eq(stations.station, 'whatsapp'), eq(stations.id, accountId)),
|
|
17
|
-
);
|
|
18
|
-
}
|