@stage-labs/metro 0.1.0-beta.304 → 0.1.0-beta.306
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/cli.js +1 -1
- package/dist/memory.js +2 -3
- package/dist/runtime-install.js +3 -11
- package/dist/voice.js +1 -1
- package/{runtime/marketplace/plugin/bin → hooks}/guard.mjs +9 -4
- package/{runtime/marketplace/plugin/bin → hooks}/policy.mjs +1 -32
- package/{runtime/marketplace/plugin/bin → hooks}/session-start.mjs +2 -2
- package/package.json +4 -2
- package/runtime/node_modules/@metro-labs/core/package.json +0 -1
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/home-fs.ts +2 -29
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/provision.ts +2 -15
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/user.ts +0 -1
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/view.ts +3 -3
- package/runtime/node_modules/@metro-labs/daemon/src/agents/api.ts +3 -5
- package/runtime/node_modules/@metro-labs/daemon/src/approvals/pending.ts +1 -7
- package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +16 -17
- package/runtime/node_modules/@metro-labs/daemon/src/claude/cli-files.ts +28 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/hooks.ts +60 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/plugin-cleanup.ts +121 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +2 -4
- package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +38 -30
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/enroll-api.ts +0 -2
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/index.ts +6 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/instructions.ts +4 -2
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-prompt.ts +1 -13
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-gate.ts +2 -1
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-snapshot.ts +2 -16
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/server-approvals.ts +15 -16
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session.ts +4 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-catalog.ts +20 -8
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-dispatch.ts +14 -8
- package/runtime/node_modules/@metro-labs/daemon/src/mcp-server/announced.ts +68 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp-server/config.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/mcp-server/connector-tools.ts +114 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp-server/link.ts +12 -1
- package/runtime/node_modules/@metro-labs/daemon/src/mcp-server/rpc.ts +67 -0
- package/runtime/node_modules/@metro-labs/daemon/src/policy/policy.ts +6 -5
- package/runtime/node_modules/@metro-labs/daemon/src/routes/http.ts +0 -2
- package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +2 -27
- package/runtime/node_modules/@metro-labs/daemon/src/routes/session-apis.ts +0 -5
- package/runtime/node_modules/@metro-labs/http/package.json +3 -3
- package/runtime/node_modules/@metro-labs/http/src/connector-grant.ts +0 -51
- package/runtime/node_modules/@metro-labs/http/src/mcp-server-protocol.ts +9 -4
- package/runtime/node_modules/@metro-labs/http/src/mcp-server-tools.ts +93 -0
- package/runtime/runtime.json +1 -1
- package/runtime/sdk-runner/node_modules/@metro-labs/core/package.json +0 -1
- package/runtime/sdk-runner/runner.json +1 -1
- package/{runtime/marketplace/plugin → skills}/METRO.md +8 -3
- package/runtime/marketplace/.claude-plugin/marketplace.json +0 -12
- package/runtime/marketplace/plugin/.claude-plugin/plugin.json +0 -17
- package/runtime/marketplace/plugin/.mcp.json +0 -1
- package/runtime/marketplace/plugin/bin/metro-plugin.mjs +0 -48
- package/runtime/marketplace/plugin/hooks/hooks.json +0 -25
- package/runtime/node_modules/@metro-labs/core/src/connector-sync.ts +0 -17
- package/runtime/node_modules/@metro-labs/daemon/src/claude/plugin-install.ts +0 -149
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/gates.ts +0 -104
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/org-adopt.ts +0 -57
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/org-relay.ts +0 -61
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/org-runtime-discovery.ts +0 -115
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/org-runtime.ts +0 -271
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/org-sync-api.ts +0 -25
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/org-sync-control.ts +0 -173
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/org-sync-setting.ts +0 -60
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/org-sync-watch.ts +0 -38
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/org-sync.ts +0 -208
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/plugin-sync.ts +0 -145
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/publication.ts +0 -40
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-id.ts +0 -2
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-policy.ts +0 -90
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-session.ts +0 -10
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-target.ts +0 -4
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay.ts +0 -287
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/server-keys.ts +0 -59
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/tools.ts +0 -1
- package/runtime/node_modules/@metro-labs/http/src/connector-delivery-validation.ts +0 -116
- package/runtime/node_modules/@metro-labs/http/src/connector-delivery.ts +0 -19
- package/runtime/node_modules/@metro-labs/http/src/connector-migration.ts +0 -178
- package/runtime/node_modules/@metro-labs/http/src/transfer-crypto.ts +0 -44
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/connector-sync.ts +0 -17
- /package/{runtime/marketplace/plugin → skills}/MEMORY.md +0 -0
- /package/{runtime/marketplace/plugin → skills}/STAGE.md +0 -0
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
-
import type { RelaySession } from './relay-session.js';
|
|
3
|
-
import { exchangeOrg } from './org-relay.js';
|
|
4
|
-
import { ApiError } from '@metro-labs/http/api-error';
|
|
5
|
-
import { log } from '@metro-labs/core/log';
|
|
6
|
-
import { answerBlocked, mergeBlocked } from './relay-policy.js';
|
|
7
|
-
|
|
8
|
-
export interface RelayApiDeps {
|
|
9
|
-
identify: (req: IncomingMessage) => { subject: string; agentId: string } | null;
|
|
10
|
-
open: (connectorId: string) => RelaySession | Promise<RelaySession>;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const ID_PATH_RE = /^\/relay\/(org-[A-Za-z0-9][A-Za-z0-9_-]{10})$/;
|
|
14
|
-
const METHODS = new Set(['POST', 'GET', 'DELETE']);
|
|
15
|
-
const RELAY_BODY_MAX = 8 * 1024 * 1024;
|
|
16
|
-
const FIRST_BYTE_MS = 120_000;
|
|
17
|
-
const PASS_REQ = [
|
|
18
|
-
'accept',
|
|
19
|
-
'content-type',
|
|
20
|
-
'mcp-session-id',
|
|
21
|
-
'mcp-protocol-version',
|
|
22
|
-
'last-event-id',
|
|
23
|
-
] as const;
|
|
24
|
-
const PASS_RES = [
|
|
25
|
-
'content-type',
|
|
26
|
-
'mcp-session-id',
|
|
27
|
-
'mcp-protocol-version',
|
|
28
|
-
'cache-control',
|
|
29
|
-
] as const;
|
|
30
|
-
|
|
31
|
-
const keepaliveMs = (): number =>
|
|
32
|
-
Number(process.env.METRO_RELAY_KEEPALIVE_MS) || 25_000;
|
|
33
|
-
|
|
34
|
-
const isRelaySse = (contentType: string | undefined): boolean => /^text\/event-stream\s*(?:;|$)/i.test(contentType ?? '');
|
|
35
|
-
|
|
36
|
-
function answer(
|
|
37
|
-
res: ServerResponse,
|
|
38
|
-
status: number,
|
|
39
|
-
body: Record<string, unknown>,
|
|
40
|
-
): void {
|
|
41
|
-
if (res.headersSent) return;
|
|
42
|
-
res.writeHead(status, { 'content-type': 'application/json' });
|
|
43
|
-
res.end(JSON.stringify(body));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function closeIfBodyUnread(req: IncomingMessage, res: ServerResponse): void {
|
|
47
|
-
if (req.readableEnded) return;
|
|
48
|
-
const drop = (): void => {
|
|
49
|
-
req.socket?.destroy();
|
|
50
|
-
};
|
|
51
|
-
if (res.writableFinished) drop();
|
|
52
|
-
else res.once('finish', drop);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async function readCapped(req: IncomingMessage): Promise<Uint8Array<ArrayBuffer>> {
|
|
56
|
-
const declared = Number(req.headers['content-length']);
|
|
57
|
-
if (Number.isFinite(declared) && declared > RELAY_BODY_MAX)
|
|
58
|
-
throw new ApiError('relay body exceeds 8 MiB', 413);
|
|
59
|
-
const chunks: Buffer[] = [];
|
|
60
|
-
let total = 0;
|
|
61
|
-
for await (const chunk of req) {
|
|
62
|
-
const buf = chunk as Buffer;
|
|
63
|
-
total += buf.length;
|
|
64
|
-
if (total > RELAY_BODY_MAX)
|
|
65
|
-
throw new ApiError('relay body exceeds 8 MiB', 413);
|
|
66
|
-
chunks.push(buf);
|
|
67
|
-
}
|
|
68
|
-
const merged = Buffer.concat(chunks);
|
|
69
|
-
const out = new Uint8Array(new ArrayBuffer(merged.byteLength));
|
|
70
|
-
out.set(merged);
|
|
71
|
-
return out;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function upstreamHeaders(
|
|
75
|
-
req: IncomingMessage,
|
|
76
|
-
injected: Record<string, string>,
|
|
77
|
-
): Record<string, string> {
|
|
78
|
-
const out: Record<string, string> = {};
|
|
79
|
-
for (const name of PASS_REQ) {
|
|
80
|
-
const value = req.headers[name];
|
|
81
|
-
if (typeof value === 'string') out[name] = value;
|
|
82
|
-
}
|
|
83
|
-
return { ...out, ...injected };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async function forward(
|
|
87
|
-
req: IncomingMessage,
|
|
88
|
-
target: { url: string; headers: Record<string, string> },
|
|
89
|
-
body: Uint8Array<ArrayBuffer> | null,
|
|
90
|
-
signal: AbortSignal,
|
|
91
|
-
): Promise<Response> {
|
|
92
|
-
const firstByte = setTimeout(() => {
|
|
93
|
-
log.warn('relay: upstream sent no response in time');
|
|
94
|
-
}, FIRST_BYTE_MS);
|
|
95
|
-
firstByte.unref?.();
|
|
96
|
-
try {
|
|
97
|
-
return await fetch(target.url, {
|
|
98
|
-
method: req.method,
|
|
99
|
-
headers: upstreamHeaders(req, target.headers),
|
|
100
|
-
...(body === null ? {} : { body }),
|
|
101
|
-
signal,
|
|
102
|
-
redirect: 'manual',
|
|
103
|
-
});
|
|
104
|
-
} finally {
|
|
105
|
-
clearTimeout(firstByte);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
interface Keepalive {
|
|
110
|
-
touch: () => void;
|
|
111
|
-
stop: () => void;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function startKeepalive(res: ServerResponse): Keepalive {
|
|
115
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
116
|
-
const arm = (): void => {
|
|
117
|
-
timer = setTimeout(() => {
|
|
118
|
-
try {
|
|
119
|
-
res.write(': keepalive\n\n');
|
|
120
|
-
} catch {
|
|
121
|
-
log.debug('relay: keepalive write failed');
|
|
122
|
-
}
|
|
123
|
-
arm();
|
|
124
|
-
}, keepaliveMs());
|
|
125
|
-
timer.unref?.();
|
|
126
|
-
};
|
|
127
|
-
arm();
|
|
128
|
-
return {
|
|
129
|
-
touch: (): void => {
|
|
130
|
-
if (timer) clearTimeout(timer);
|
|
131
|
-
arm();
|
|
132
|
-
},
|
|
133
|
-
stop: (): void => {
|
|
134
|
-
if (timer) clearTimeout(timer);
|
|
135
|
-
},
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function drainedOrClosed(res: ServerResponse): Promise<void> {
|
|
140
|
-
return new Promise((resolve) => {
|
|
141
|
-
const done = (): void => {
|
|
142
|
-
res.off('drain', done);
|
|
143
|
-
res.off('close', done);
|
|
144
|
-
resolve();
|
|
145
|
-
};
|
|
146
|
-
res.once('drain', done);
|
|
147
|
-
res.once('close', done);
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
async function pumpBody(
|
|
152
|
-
res: ServerResponse,
|
|
153
|
-
body: ReadableStream<Uint8Array>,
|
|
154
|
-
touch: () => void,
|
|
155
|
-
): Promise<void> {
|
|
156
|
-
const reader = body.getReader();
|
|
157
|
-
for (;;) {
|
|
158
|
-
const { done, value } = await reader.read();
|
|
159
|
-
if (done) return;
|
|
160
|
-
touch();
|
|
161
|
-
if (!res.write(value)) await drainedOrClosed(res);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function passedHeaders(upstream: Response): Record<string, string> {
|
|
166
|
-
const headers: Record<string, string> = {};
|
|
167
|
-
for (const name of PASS_RES) {
|
|
168
|
-
const value = upstream.headers.get(name);
|
|
169
|
-
if (value !== null) headers[name] = value;
|
|
170
|
-
}
|
|
171
|
-
return headers;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
async function pipe(res: ServerResponse, upstream: Response, answers: Record<string, unknown>[]): Promise<void> {
|
|
175
|
-
const headers = passedHeaders(upstream);
|
|
176
|
-
if (answers.length > 0 && (upstream.status === 200 || upstream.status === 202)) {
|
|
177
|
-
await mergeBlocked(res, upstream, headers, answers);
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
const sse = isRelaySse(headers['content-type']);
|
|
181
|
-
if (sse) headers['x-accel-buffering'] = 'no';
|
|
182
|
-
res.writeHead(upstream.status, headers);
|
|
183
|
-
if (sse) res.flushHeaders();
|
|
184
|
-
if (upstream.body === null) {
|
|
185
|
-
res.end();
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
const keepalive = sse ? startKeepalive(res) : null;
|
|
189
|
-
try {
|
|
190
|
-
await pumpBody(res, upstream.body, () => keepalive?.touch());
|
|
191
|
-
} finally {
|
|
192
|
-
keepalive?.stop();
|
|
193
|
-
}
|
|
194
|
-
res.end();
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
async function deliver(res: ServerResponse, upstream: Response, answers: Record<string, unknown>[]): Promise<void> {
|
|
198
|
-
if (upstream.status >= 300 && upstream.status < 400) {
|
|
199
|
-
await upstream.body?.cancel();
|
|
200
|
-
answer(res, 502, { error: 'the connector redirected; metro does not follow' });
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
await pipe(res, upstream, answers);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
async function relayOnce(
|
|
207
|
-
req: IncomingMessage,
|
|
208
|
-
res: ServerResponse,
|
|
209
|
-
connectorId: string,
|
|
210
|
-
deps: RelayApiDeps,
|
|
211
|
-
signal: AbortSignal,
|
|
212
|
-
): Promise<void> {
|
|
213
|
-
const read = req.method === 'POST' ? await readCapped(req) : null;
|
|
214
|
-
const session = await deps.open(connectorId);
|
|
215
|
-
try {
|
|
216
|
-
const out = await exchangeOrg(connectorId, session, read, signal, (target, body, guard) => forward(req, target, body, guard));
|
|
217
|
-
if (out.kind === 'blocked') answerBlocked(res, out.screened);
|
|
218
|
-
else await deliver(res, out.upstream, out.answers);
|
|
219
|
-
} finally { session.close(); }
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
async function relayExchange(
|
|
223
|
-
req: IncomingMessage,
|
|
224
|
-
res: ServerResponse,
|
|
225
|
-
connectorId: string,
|
|
226
|
-
deps: RelayApiDeps,
|
|
227
|
-
): Promise<void> {
|
|
228
|
-
const control = new AbortController();
|
|
229
|
-
const bail = (): void => {
|
|
230
|
-
if (!res.writableEnded) control.abort();
|
|
231
|
-
};
|
|
232
|
-
res.once('close', bail);
|
|
233
|
-
res.socket?.once('close', bail);
|
|
234
|
-
res.once('finish', () => {
|
|
235
|
-
res.socket?.removeListener('close', bail);
|
|
236
|
-
});
|
|
237
|
-
try {
|
|
238
|
-
await relayOnce(req, res, connectorId, deps, control.signal);
|
|
239
|
-
} catch (err) {
|
|
240
|
-
if (!control.signal.aborted) throw err;
|
|
241
|
-
log.debug({ connector: connectorId }, 'relay: the client left before the answer ended');
|
|
242
|
-
if (!res.writableEnded) res.end();
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function dispatch(
|
|
247
|
-
req: IncomingMessage,
|
|
248
|
-
res: ServerResponse,
|
|
249
|
-
connectorId: string,
|
|
250
|
-
deps: RelayApiDeps,
|
|
251
|
-
): void {
|
|
252
|
-
const who = deps.identify(req);
|
|
253
|
-
if (who === null) {
|
|
254
|
-
answer(res, 401, { error: 'unauthorized' });
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
relayExchange(req, res, connectorId, deps)
|
|
258
|
-
.catch((err: unknown) => {
|
|
259
|
-
if (err instanceof ApiError) answer(res, err.status, { error: err.message });
|
|
260
|
-
else {
|
|
261
|
-
log.warn({ connector: connectorId }, 'organization relay: request failed');
|
|
262
|
-
answer(res, 503, { error: 'The organization connector is unavailable. Try again.' });
|
|
263
|
-
}
|
|
264
|
-
if (!res.writableEnded) res.end();
|
|
265
|
-
closeIfBodyUnread(req, res);
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
export function handleRelayRequest(
|
|
270
|
-
req: IncomingMessage,
|
|
271
|
-
res: ServerResponse,
|
|
272
|
-
deps: RelayApiDeps,
|
|
273
|
-
): boolean {
|
|
274
|
-
const path = (req.url ?? '').split('?')[0] ?? '';
|
|
275
|
-
if (path !== '/relay' && !path.startsWith('/relay/')) return false;
|
|
276
|
-
const match = ID_PATH_RE.exec(path);
|
|
277
|
-
if (match?.[1] === undefined) {
|
|
278
|
-
answer(res, 404, { error: 'no such connector' });
|
|
279
|
-
return true;
|
|
280
|
-
}
|
|
281
|
-
if (!METHODS.has(req.method ?? '')) {
|
|
282
|
-
answer(res, 405, { error: 'method not allowed' });
|
|
283
|
-
return true;
|
|
284
|
-
}
|
|
285
|
-
dispatch(req, res, match[1], deps);
|
|
286
|
-
return true;
|
|
287
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { isRecord } from '@metro-labs/core/is-record';
|
|
4
|
-
import { writeSecure } from '@metro-labs/core/secure-fs';
|
|
5
|
-
import { agentsDir } from '../agents/files.js';
|
|
6
|
-
import { serverKeysOf, type PluginRow } from './plugin-sync.js';
|
|
7
|
-
|
|
8
|
-
const invalid = (): Error => new Error('Invalid connector server keys. Restore the saved namespace mapping and initialization marker.');
|
|
9
|
-
|
|
10
|
-
function readKeys(path: string, marker: string): Map<string, string> | undefined {
|
|
11
|
-
try {
|
|
12
|
-
const marked = existsSync(marker);
|
|
13
|
-
if (marked && readFileSync(marker, 'utf8') !== '1\n') throw invalid();
|
|
14
|
-
if (!existsSync(path)) {
|
|
15
|
-
if (marked) throw invalid();
|
|
16
|
-
return undefined;
|
|
17
|
-
}
|
|
18
|
-
if (!marked) throw invalid();
|
|
19
|
-
return parseKeys(readFileSync(path, 'utf8'));
|
|
20
|
-
} catch { throw invalid(); }
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function parseKeys(text: string): Map<string, string> {
|
|
24
|
-
const raw: unknown = JSON.parse(text);
|
|
25
|
-
if (!isRecord(raw) || raw.version !== 1 || !isRecord(raw.keys)) throw invalid();
|
|
26
|
-
const keys = new Map<string, string>();
|
|
27
|
-
const reserved = new Set<string>();
|
|
28
|
-
for (const [id, key] of Object.entries(raw.keys)) {
|
|
29
|
-
if (typeof key !== 'string' || !/^[a-z0-9-]+$/.test(key) || reserved.has(key)) throw invalid();
|
|
30
|
-
reserved.add(key);
|
|
31
|
-
keys.set(id, key);
|
|
32
|
-
}
|
|
33
|
-
return keys;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const sameKeys = (a: ReadonlyMap<string, string>, b: ReadonlyMap<string, string>): boolean =>
|
|
37
|
-
a.size === b.size && [...a].every(([id, key]) => b.get(id) === key);
|
|
38
|
-
|
|
39
|
-
function serverKeyFiles(dir = agentsDir()): { path: string; marker: string } {
|
|
40
|
-
return { path: join(dir, 'connector-server-keys.json'), marker: join(dir, 'connector-server-keys.initialized') };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function readServerKeys(dir = agentsDir()): Map<string, string> | undefined {
|
|
44
|
-
const { path, marker } = serverKeyFiles(dir);
|
|
45
|
-
return readKeys(path, marker);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function writeServerKeys(keys: ReadonlyMap<string, string>, dir = agentsDir()): void {
|
|
49
|
-
const { path, marker } = serverKeyFiles(dir);
|
|
50
|
-
if (!existsSync(marker)) writeSecure(marker, '1\n');
|
|
51
|
-
writeSecure(path, `${JSON.stringify({ version: 1, keys: Object.fromEntries(keys) }, null, 2)}\n`);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function persistentServerKeys(rows: PluginRow[], dir = agentsDir()): Map<string, string> {
|
|
55
|
-
const saved = readServerKeys(dir);
|
|
56
|
-
const keys = serverKeysOf(rows, saved);
|
|
57
|
-
if (saved === undefined ? keys.size > 0 : !sameKeys(saved, keys)) writeServerKeys(keys, dir);
|
|
58
|
-
return keys;
|
|
59
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { listRemoteTools, type RemoteTool } from '@metro-labs/http/connector-tools';
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import { parseId } from '@metro-labs/core/ids';
|
|
2
|
-
import { isRecord } from '@metro-labs/core/is-record';
|
|
3
|
-
import { connectorName } from './connector-input.js';
|
|
4
|
-
import { connectorPolicyLayersOf, connectorPolicyOf, type ConnectorPolicy, type ConnectorPolicyLayers } from './connector-policy.js';
|
|
5
|
-
import { parsePublicConnectorUrl } from './connector-public-url.js';
|
|
6
|
-
import { isOrganizationId } from './workos-token.js';
|
|
7
|
-
|
|
8
|
-
export const CONNECTOR_DELIVERY_LEASE_MS = 90_000;
|
|
9
|
-
export const CONNECTOR_DELIVERY_MAX_BYTES = 1024 * 1024;
|
|
10
|
-
export const DELIVERY_PLAINTEXT_MAX = Math.floor((CONNECTOR_DELIVERY_MAX_BYTES - 1024) * 3 / 4);
|
|
11
|
-
|
|
12
|
-
export interface DeliveryContext {
|
|
13
|
-
owner: string;
|
|
14
|
-
agent: string;
|
|
15
|
-
keyId: string;
|
|
16
|
-
requestId: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface DeliveredConnector {
|
|
20
|
-
id: string;
|
|
21
|
-
name: string;
|
|
22
|
-
url: string;
|
|
23
|
-
auth: 'none' | 'header' | 'oauth';
|
|
24
|
-
headers: Record<string, string>;
|
|
25
|
-
policy: ConnectorPolicy;
|
|
26
|
-
policyLayers?: ConnectorPolicyLayers;
|
|
27
|
-
revision: string;
|
|
28
|
-
state: 'ready' | 'signin' | 'unavailable';
|
|
29
|
-
expiresAt?: number;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface DeliverySnapshot {
|
|
33
|
-
version: 1;
|
|
34
|
-
issuedAt: number;
|
|
35
|
-
expiresAt: number;
|
|
36
|
-
connectors: DeliveredConnector[];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function deliveryBytes(value: unknown, length?: number): Buffer {
|
|
40
|
-
if (typeof value !== 'string' || !/^[A-Za-z0-9_-]+$/.test(value)) throw new Error('Invalid connector delivery encoding.');
|
|
41
|
-
const bytes = Buffer.from(value, 'base64url');
|
|
42
|
-
if (bytes.toString('base64url') !== value || (length !== undefined && bytes.length !== length)) throw new Error('Invalid connector delivery encoding.');
|
|
43
|
-
return bytes;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function deliveryRecord(value: unknown, fields: string[]): Record<string, unknown> {
|
|
47
|
-
if (!isRecord(value) || Object.keys(value).some((key) => !fields.includes(key))) throw new Error('Invalid connector delivery.');
|
|
48
|
-
return value;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function deliveryContextBytes(context: DeliveryContext): Buffer {
|
|
52
|
-
const value = deliveryRecord(context, ['owner', 'agent', 'keyId', 'requestId']);
|
|
53
|
-
if (typeof value.owner !== 'string' || !isOrganizationId(value.owner) || parseId(value.agent) === null) throw new Error('Invalid connector delivery context.');
|
|
54
|
-
deliveryBytes(value.keyId, 32);
|
|
55
|
-
deliveryBytes(value.requestId, 32);
|
|
56
|
-
return Buffer.from(JSON.stringify(['metro-connector-delivery/v1', value.owner, value.agent, value.keyId, value.requestId]));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const timestamp = (value: unknown): value is number => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
60
|
-
|
|
61
|
-
function headerValue(name: string, text: unknown, auth: DeliveredConnector['auth']): string {
|
|
62
|
-
if (!/^[A-Za-z0-9!#$%&'*+.^_`|~-]{1,64}$/.test(name) || ['__proto__', 'constructor', 'prototype'].includes(name) || typeof text !== 'string' || !/^[\x20-\x7e]{1,16384}$/.test(text)) throw new Error('Invalid connector delivery headers.');
|
|
63
|
-
if (auth === 'oauth' && (name !== 'Authorization' || !/^Bearer [\x21-\x7e]+$/.test(text))) throw new Error('Invalid connector delivery access token.');
|
|
64
|
-
return text;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function headersOf(value: unknown, auth: DeliveredConnector['auth'], state: DeliveredConnector['state']): Record<string, string> {
|
|
68
|
-
if (!isRecord(value)) throw new Error('Invalid connector delivery headers.');
|
|
69
|
-
const entries = Object.entries(value);
|
|
70
|
-
const expected = auth !== 'none' && state === 'ready' ? 1 : 0;
|
|
71
|
-
if (entries.length !== expected) throw new Error('Invalid connector delivery headers.');
|
|
72
|
-
const out: Record<string, string> = {};
|
|
73
|
-
for (const [name, text] of entries) out[name] = headerValue(name, text, auth);
|
|
74
|
-
return out;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function authenticationOf(value: unknown): DeliveredConnector['auth'] {
|
|
78
|
-
if (value !== 'none' && value !== 'header' && value !== 'oauth') throw new Error('Invalid delivered connector authentication.');
|
|
79
|
-
return value;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function stateOf(value: unknown, auth: DeliveredConnector['auth']): DeliveredConnector['state'] {
|
|
83
|
-
if (value !== 'ready' && value !== 'signin' && value !== 'unavailable') throw new Error('Invalid delivered connector state.');
|
|
84
|
-
if (value === 'signin' && auth !== 'oauth') throw new Error('Invalid delivered connector state.');
|
|
85
|
-
return value;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function urlOf(value: unknown): string {
|
|
89
|
-
parsePublicConnectorUrl(value);
|
|
90
|
-
if (typeof value !== 'string') throw new Error('Invalid delivered connector URL.');
|
|
91
|
-
return value;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function connectorOf(raw: unknown): DeliveredConnector {
|
|
95
|
-
const value = deliveryRecord(raw, ['id', 'name', 'url', 'auth', 'headers', 'policy', 'policyLayers', 'revision', 'state', 'expiresAt']);
|
|
96
|
-
const id = parseId(value.id);
|
|
97
|
-
if (id === null || typeof value.name !== 'string' || connectorName(value.name) !== value.name) throw new Error('Invalid delivered connector.');
|
|
98
|
-
const url = urlOf(value.url);
|
|
99
|
-
const auth = authenticationOf(value.auth);
|
|
100
|
-
const state = stateOf(value.state, auth);
|
|
101
|
-
deliveryBytes(value.revision, 32);
|
|
102
|
-
if (Object.hasOwn(value, 'expiresAt') && !timestamp(value.expiresAt)) throw new Error('Invalid delivered connector expiry.');
|
|
103
|
-
return { id, name: value.name, url, auth, state, revision: String(value.revision), headers: headersOf(value.headers, auth, state), policy: connectorPolicyOf(value.policy), ...(Object.hasOwn(value, 'policyLayers') ? { policyLayers: connectorPolicyLayersOf(value.policyLayers) } : {}), ...(timestamp(value.expiresAt) ? { expiresAt: value.expiresAt } : {}) };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function deliverySnapshotOf(raw: unknown): DeliverySnapshot {
|
|
107
|
-
const value = deliveryRecord(raw, ['version', 'issuedAt', 'expiresAt', 'connectors']);
|
|
108
|
-
if (value.version !== 1 || !timestamp(value.issuedAt) || !timestamp(value.expiresAt) || value.expiresAt <= value.issuedAt || value.expiresAt - value.issuedAt > CONNECTOR_DELIVERY_LEASE_MS || !Array.isArray(value.connectors)) throw new Error('Invalid connector delivery snapshot.');
|
|
109
|
-
const issuedAt = value.issuedAt;
|
|
110
|
-
const connectors = value.connectors.map((entry: unknown) => connectorOf(entry));
|
|
111
|
-
if (new Set(connectors.map((connector) => connector.id)).size !== connectors.length) throw new Error('Duplicate delivered connector.');
|
|
112
|
-
if (connectors.some((connector) => connector.state === 'ready' && connector.expiresAt !== undefined && connector.expiresAt <= issuedAt)) throw new Error('Expired delivered connector.');
|
|
113
|
-
const snapshot: DeliverySnapshot = { version: 1, issuedAt: value.issuedAt, expiresAt: value.expiresAt, connectors };
|
|
114
|
-
if (Buffer.byteLength(JSON.stringify(snapshot)) > DELIVERY_PLAINTEXT_MAX) throw new Error('Connector delivery is too large.');
|
|
115
|
-
return snapshot;
|
|
116
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { KeyObject } from 'node:crypto';
|
|
2
|
-
import { CONNECTOR_DELIVERY_MAX_BYTES, DELIVERY_PLAINTEXT_MAX, deliveryContextBytes, deliverySnapshotOf, type DeliveryContext, type DeliverySnapshot } from './connector-delivery-validation.js';
|
|
3
|
-
import { openTransfer, sealTransfer, type SealedTransfer } from './transfer-crypto.js';
|
|
4
|
-
|
|
5
|
-
export { CONNECTOR_DELIVERY_LEASE_MS, CONNECTOR_DELIVERY_MAX_BYTES, DELIVERY_PLAINTEXT_MAX, deliverySnapshotOf, type DeliveryContext, type DeliverySnapshot, type DeliveredConnector } from './connector-delivery-validation.js';
|
|
6
|
-
export { combineConnectorPolicies, connectorPolicyOf, effectiveConnectorPolicy, connectorPoliciesBlocked, validConnectorToolName, type ConnectorPolicy, type ConnectorPolicyLayers } from './connector-policy.js';
|
|
7
|
-
export type SealedDelivery = SealedTransfer;
|
|
8
|
-
|
|
9
|
-
export function sealConnectorDelivery(publicKey: string, context: DeliveryContext, snapshot: DeliverySnapshot): SealedDelivery {
|
|
10
|
-
const sealed = sealTransfer(publicKey, deliveryContextBytes(context), Buffer.from(JSON.stringify(deliverySnapshotOf(snapshot))));
|
|
11
|
-
if (Buffer.byteLength(JSON.stringify({ sealed })) > CONNECTOR_DELIVERY_MAX_BYTES) throw new Error('Connector delivery is too large.');
|
|
12
|
-
return sealed;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function openConnectorDelivery(privateKey: KeyObject, context: DeliveryContext, envelope: unknown): DeliverySnapshot {
|
|
16
|
-
if (Buffer.byteLength(JSON.stringify({ sealed: envelope })) > CONNECTOR_DELIVERY_MAX_BYTES) throw new Error('Connector delivery is too large.');
|
|
17
|
-
const plaintext = openTransfer(privateKey, deliveryContextBytes(context), envelope, DELIVERY_PLAINTEXT_MAX);
|
|
18
|
-
return deliverySnapshotOf(JSON.parse(plaintext.toString('utf8')) as unknown);
|
|
19
|
-
}
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
import type { KeyObject } from 'node:crypto';
|
|
2
|
-
import { parseId } from '@metro-labs/core/ids';
|
|
3
|
-
import { sealingKeyOf } from './agent-signature.js';
|
|
4
|
-
import { deliveryBytes, deliveryRecord } from './connector-delivery-validation.js';
|
|
5
|
-
import { connectorAuth, connectorClient, connectorName } from './connector-input.js';
|
|
6
|
-
import type { OAuthClient } from './connector-oauth.js';
|
|
7
|
-
import { portableGrant, type PortableGrant } from './connector-grant.js';
|
|
8
|
-
import { connectorPolicyOf, type ConnectorPolicy } from './connector-policy.js';
|
|
9
|
-
import { boundOAuthResource, parsePublicConnectorUrl } from './connector-public-url.js';
|
|
10
|
-
import { openTransfer, sealTransfer, type SealedTransfer } from './transfer-crypto.js';
|
|
11
|
-
import { isOrganizationId } from './workos-token.js';
|
|
12
|
-
|
|
13
|
-
export const CONNECTOR_MIGRATION_MAX_BYTES = 96 * 1024;
|
|
14
|
-
const PAYLOAD_MAX = 60 * 1024;
|
|
15
|
-
|
|
16
|
-
export interface MigrationContext {
|
|
17
|
-
owner: string;
|
|
18
|
-
agent: string;
|
|
19
|
-
keyId: string;
|
|
20
|
-
operationId: string;
|
|
21
|
-
sourceId: string;
|
|
22
|
-
destinationId: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface MigrationPayload {
|
|
26
|
-
name: string;
|
|
27
|
-
url: string;
|
|
28
|
-
auth: { kind: 'none' } | { kind: 'header'; name: string; value: string } | { kind: 'oauth'; grant?: PortableGrant };
|
|
29
|
-
client: OAuthClient | null;
|
|
30
|
-
oauthResource?: string;
|
|
31
|
-
policy: ConnectorPolicy;
|
|
32
|
-
sourceStamp: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface MigrationView {
|
|
36
|
-
operationId: string;
|
|
37
|
-
sourceId: string;
|
|
38
|
-
destinationId: string;
|
|
39
|
-
phase: 'preparing' | 'prepared' | 'active' | 'rolled_back';
|
|
40
|
-
readiness: 'pending' | 'ready' | 'signin' | 'changed' | 'unavailable' | 'refresh_uncertain';
|
|
41
|
-
grantImported?: boolean;
|
|
42
|
-
name: string | null;
|
|
43
|
-
auth: 'none' | 'header' | 'oauth' | null;
|
|
44
|
-
policy: ConnectorPolicy | null;
|
|
45
|
-
sourceStamp: string | null;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface MigrationResponse {
|
|
49
|
-
migration: MigrationView;
|
|
50
|
-
receiving?: { publicKey: string; context: MigrationContext };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export type MigrationRequest = { action: 'begin' | 'status' | 'activate' | 'rollback'; operationId: string; sourceId: string } | { action: 'prepare' | 'handover'; operationId: string; sourceId: string; sealed: unknown };
|
|
54
|
-
|
|
55
|
-
const invalid = (): Error => new Error('Invalid connector migration.');
|
|
56
|
-
const idOf = (value: unknown): string => {
|
|
57
|
-
const id = parseId(value);
|
|
58
|
-
if (id === null) throw invalid();
|
|
59
|
-
return id;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
export function migrationRequestOf(raw: unknown): MigrationRequest {
|
|
63
|
-
const value = deliveryRecord(raw, ['action', 'operationId', 'sourceId', 'sealed']);
|
|
64
|
-
const operationId = idOf(value.operationId);
|
|
65
|
-
const sourceId = idOf(value.sourceId);
|
|
66
|
-
if ((value.action === 'prepare' || value.action === 'handover') && Object.hasOwn(value, 'sealed')) return { action: value.action, operationId, sourceId, sealed: value.sealed };
|
|
67
|
-
if (Object.hasOwn(value, 'sealed')) throw invalid();
|
|
68
|
-
if (value.action !== 'begin' && value.action !== 'status' && value.action !== 'activate' && value.action !== 'rollback') throw invalid();
|
|
69
|
-
return { action: value.action, operationId, sourceId };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function contextOf(raw: unknown): MigrationContext {
|
|
73
|
-
const value = deliveryRecord(raw, ['owner', 'agent', 'keyId', 'operationId', 'sourceId', 'destinationId']);
|
|
74
|
-
if (typeof value.owner !== 'string' || !isOrganizationId(value.owner) || typeof value.keyId !== 'string') throw invalid();
|
|
75
|
-
deliveryBytes(value.keyId, 32);
|
|
76
|
-
return { owner: value.owner, agent: idOf(value.agent), keyId: value.keyId, operationId: idOf(value.operationId), sourceId: idOf(value.sourceId), destinationId: idOf(value.destinationId) };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function contextBytes(context: MigrationContext): Buffer {
|
|
80
|
-
const value = contextOf(context);
|
|
81
|
-
return Buffer.from(JSON.stringify(['metro-connector-migration/v1', value.owner, value.agent, value.keyId, value.operationId, value.sourceId, value.destinationId]));
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function headerOf(value: Record<string, unknown>): MigrationPayload['auth'] {
|
|
85
|
-
const auth = connectorAuth(value.name, value.value);
|
|
86
|
-
if (auth.kind !== 'header' || auth.name !== value.name || auth.value !== value.value || ['__proto__', 'constructor', 'prototype'].includes(auth.name)) throw invalid();
|
|
87
|
-
return auth;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function authOf(raw: unknown, target: string): MigrationPayload['auth'] {
|
|
91
|
-
const value = deliveryRecord(raw, ['kind', 'name', 'value', 'grant']);
|
|
92
|
-
if (value.kind !== 'oauth' && Object.hasOwn(value, 'grant')) throw invalid();
|
|
93
|
-
if (value.kind === 'header') return headerOf(value);
|
|
94
|
-
if ((value.kind !== 'none' && value.kind !== 'oauth') || Object.hasOwn(value, 'name') || Object.hasOwn(value, 'value')) throw invalid();
|
|
95
|
-
if (value.kind === 'none' || value.grant === undefined) return { kind: value.kind };
|
|
96
|
-
return { kind: 'oauth', grant: portableGrant(value.grant, target) };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function clientOf(raw: unknown): OAuthClient | null {
|
|
100
|
-
if (raw === null) return null;
|
|
101
|
-
const value = deliveryRecord(raw, ['clientId', 'clientSecret']);
|
|
102
|
-
const client = connectorClient(value.clientId, value.clientSecret);
|
|
103
|
-
if (client === null || client.clientId !== value.clientId || client.clientSecret !== value.clientSecret) throw invalid();
|
|
104
|
-
return client;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function migrationClient(auth: MigrationPayload['auth'], input: Record<string, unknown>): OAuthClient | null {
|
|
108
|
-
const client = clientOf(input.client);
|
|
109
|
-
if (auth.kind !== 'oauth' && (client !== null || Object.hasOwn(input, 'oauthResource'))) throw invalid();
|
|
110
|
-
return client;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export function migrationPayloadOf(raw: unknown): MigrationPayload {
|
|
114
|
-
const value = deliveryRecord(raw, ['name', 'url', 'auth', 'client', 'oauthResource', 'policy', 'sourceStamp']);
|
|
115
|
-
if (typeof value.name !== 'string' || connectorName(value.name) !== value.name || typeof value.url !== 'string' || typeof value.sourceStamp !== 'string') throw invalid();
|
|
116
|
-
parsePublicConnectorUrl(value.url);
|
|
117
|
-
deliveryBytes(value.sourceStamp, 32);
|
|
118
|
-
const auth = authOf(value.auth, value.url);
|
|
119
|
-
const client = migrationClient(auth, value);
|
|
120
|
-
const payload = { name: value.name, url: value.url, auth, client, ...(Object.hasOwn(value, 'oauthResource') ? { oauthResource: boundOAuthResource(value.url, value.oauthResource) } : {}), policy: connectorPolicyOf(value.policy), sourceStamp: value.sourceStamp };
|
|
121
|
-
if (Buffer.byteLength(JSON.stringify(payload)) > PAYLOAD_MAX) throw invalid();
|
|
122
|
-
return payload;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function phaseOf(value: unknown): MigrationView['phase'] {
|
|
126
|
-
if (value !== 'preparing' && value !== 'prepared' && value !== 'active' && value !== 'rolled_back') throw invalid();
|
|
127
|
-
return value;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function readinessOf(value: unknown): MigrationView['readiness'] {
|
|
131
|
-
if (value !== 'pending' && value !== 'ready' && value !== 'signin' && value !== 'changed' && value !== 'unavailable' && value !== 'refresh_uncertain') throw invalid();
|
|
132
|
-
return value;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function authKindOf(value: unknown): MigrationView['auth'] {
|
|
136
|
-
if (value !== null && value !== 'none' && value !== 'header' && value !== 'oauth') throw invalid();
|
|
137
|
-
return value;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function nullableName(value: unknown): string | null {
|
|
141
|
-
if (value === null) return null;
|
|
142
|
-
if (typeof value !== 'string' || connectorName(value) !== value) throw invalid();
|
|
143
|
-
return value;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function nullableStamp(value: unknown): string | null {
|
|
147
|
-
if (value === null) return null;
|
|
148
|
-
if (typeof value !== 'string') throw invalid();
|
|
149
|
-
deliveryBytes(value, 32);
|
|
150
|
-
return value;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function viewOf(raw: unknown): MigrationView {
|
|
154
|
-
const view = deliveryRecord(raw, ['operationId', 'sourceId', 'destinationId', 'phase', 'readiness', 'name', 'auth', 'policy', 'sourceStamp', 'grantImported']);
|
|
155
|
-
if (view.grantImported !== undefined && typeof view.grantImported !== 'boolean') throw invalid();
|
|
156
|
-
return { ...(view.grantImported === undefined ? {} : { grantImported: view.grantImported }), operationId: idOf(view.operationId), sourceId: idOf(view.sourceId), destinationId: idOf(view.destinationId), phase: phaseOf(view.phase), readiness: readinessOf(view.readiness), name: nullableName(view.name), auth: authKindOf(view.auth), policy: view.policy === null ? null : connectorPolicyOf(view.policy), sourceStamp: nullableStamp(view.sourceStamp) };
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export function migrationResponseOf(raw: unknown): MigrationResponse {
|
|
160
|
-
const value = deliveryRecord(raw, ['migration', 'receiving']);
|
|
161
|
-
const migration = viewOf(value.migration);
|
|
162
|
-
if (!Object.hasOwn(value, 'receiving')) return { migration };
|
|
163
|
-
const receiving = deliveryRecord(value.receiving, ['publicKey', 'context']);
|
|
164
|
-
if (typeof receiving.publicKey !== 'string' || sealingKeyOf(receiving.publicKey) === null) throw invalid();
|
|
165
|
-
const context = contextOf(receiving.context);
|
|
166
|
-
if (context.operationId !== migration.operationId || context.sourceId !== migration.sourceId || context.destinationId !== migration.destinationId) throw invalid();
|
|
167
|
-
return { migration, receiving: { publicKey: receiving.publicKey, context } };
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export function sealConnectorMigration(publicKey: string, context: MigrationContext, payload: MigrationPayload): SealedTransfer {
|
|
171
|
-
return sealTransfer(publicKey, contextBytes(context), Buffer.from(JSON.stringify(migrationPayloadOf(payload))));
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export function openConnectorMigration(privateKey: KeyObject, context: MigrationContext, sealed: unknown): MigrationPayload {
|
|
175
|
-
if (Buffer.byteLength(JSON.stringify(sealed)) > CONNECTOR_MIGRATION_MAX_BYTES) throw invalid();
|
|
176
|
-
const plain = openTransfer(privateKey, contextBytes(context), sealed, PAYLOAD_MAX);
|
|
177
|
-
return migrationPayloadOf(JSON.parse(plain.toString('utf8')) as unknown);
|
|
178
|
-
}
|