@stage-labs/metro 0.1.0-beta.291 → 0.1.0-beta.293
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
- package/runtime/marketplace/plugin/bin/policy.mjs +4 -1
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/config.ts +2 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/gates.ts +27 -23
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/migration-api.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/migration-control.ts +107 -30
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/migration-guard.ts +17 -13
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/migration-operations.ts +36 -6
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/migration-preview.ts +29 -13
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/migration-source.ts +18 -2
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/migration-state.ts +20 -3
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-routes.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth.ts +2 -1
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/plugin-sync.ts +39 -12
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/publication.ts +22 -11
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-passive.ts +96 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay.ts +19 -10
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/server-keys.ts +46 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/store.ts +21 -24
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/transfer-store.ts +1 -4
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-prompt.ts +6 -2
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-gate.ts +1 -2
- package/runtime/node_modules/@metro-labs/http/package.json +2 -1
- package/runtime/node_modules/@metro-labs/http/src/connector-grant.ts +59 -0
- package/runtime/node_modules/@metro-labs/http/src/connector-mcp.ts +2 -0
- package/runtime/node_modules/@metro-labs/http/src/connector-migration.ts +23 -16
- package/runtime/runtime.json +1 -1
- package/runtime/sdk-runner/runner.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { closeSync, fsyncSync, openSync } from 'node:fs';
|
|
3
3
|
import { publishLocalConnectors } from './publication.js';
|
|
4
|
+
import { persistentServerKeys } from './server-keys.js';
|
|
4
5
|
import { assertLocalMigrationEdit, localMigrationEnabled, localMigrationIdle, migrationSource, withLocalMigrationOperation } from './migration-guard.js';
|
|
5
6
|
import type { FetchLike } from '@metro-labs/core/stations/oauth';
|
|
6
7
|
import type { RelayTarget } from './relay-target.js';
|
|
@@ -116,6 +117,8 @@ export function readLocalConnectors(dir = agentsDir()): LocalConnectorRow[] {
|
|
|
116
117
|
}
|
|
117
118
|
|
|
118
119
|
function writeRows(dir: string, rows: LocalConnectorRow[]): void {
|
|
120
|
+
persistentServerKeys(readLocalConnectors(dir), dir);
|
|
121
|
+
persistentServerKeys(rows, dir);
|
|
119
122
|
writeSecure(filePath(dir), `${JSON.stringify({ version: 1, connectors: rows }, null, 2)}\n`);
|
|
120
123
|
if (dir !== agentsDir()) return;
|
|
121
124
|
publishLocalConnectors(rows);
|
|
@@ -130,7 +133,7 @@ function rowOrThrow(id: string, dir: string): LocalConnectorRow {
|
|
|
130
133
|
}
|
|
131
134
|
|
|
132
135
|
function replace(dir: string, row: LocalConnectorRow, refresh = false): Connector {
|
|
133
|
-
if (!refresh) assertLocalMigrationEdit(row.id,
|
|
136
|
+
if (!refresh) assertLocalMigrationEdit(row.id, dir);
|
|
134
137
|
writeRows(dir, readLocalConnectors(dir).map((r) => (r.id === row.id ? row : r)));
|
|
135
138
|
return connectorFromRow(row);
|
|
136
139
|
}
|
|
@@ -150,17 +153,17 @@ export function localImportConnectors(
|
|
|
150
153
|
const fresh =
|
|
151
154
|
mode === 'overwrite'
|
|
152
155
|
? rows
|
|
153
|
-
: rows.filter((row) => !current.some((r) => r.id === row.id
|
|
156
|
+
: rows.filter((row) => !current.some((r) => r.id === row.id));
|
|
154
157
|
const imported: LocalConnectorRow[] = fresh.map((row) => ({
|
|
155
158
|
id: row.id,
|
|
156
159
|
name: row.name,
|
|
157
160
|
url: row.url,
|
|
158
161
|
config: readConfig(row.config),
|
|
159
162
|
}));
|
|
160
|
-
for (const row of imported) assertLocalMigrationEdit(row.id,
|
|
163
|
+
for (const row of imported) assertLocalMigrationEdit(row.id, dir);
|
|
161
164
|
const ids = new Set(imported.map((r) => r.id));
|
|
162
|
-
|
|
163
|
-
writeRows(dir, [...current.filter((r) => !ids.has(r.id)
|
|
165
|
+
if (ids.size !== imported.length) throw new ConnectorError('The import contains duplicate connector IDs.', 400);
|
|
166
|
+
writeRows(dir, [...current.filter((r) => !ids.has(r.id)), ...imported]);
|
|
164
167
|
return imported.length;
|
|
165
168
|
}
|
|
166
169
|
|
|
@@ -172,14 +175,7 @@ export async function localGetConnector(id: string, dir = agentsDir()): Promise<
|
|
|
172
175
|
return Promise.resolve(connectorFromRow(rowOrThrow(id, dir)));
|
|
173
176
|
}
|
|
174
177
|
|
|
175
|
-
function assertNameFree(name: string, exceptId: string | null, dir: string): void {
|
|
176
|
-
if (readLocalConnectors(dir).some((r) => r.id !== exceptId && r.name === name))
|
|
177
|
-
throw new ConnectorError(`a connector named '${name}' already exists on this daemon`, 409);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
178
|
function insert(dir: string, name: string, url: URL, config: ConnectorConfig): Connector {
|
|
181
|
-
assertLocalMigrationEdit('', name, dir);
|
|
182
|
-
assertNameFree(name, null, dir);
|
|
183
179
|
const row: LocalConnectorRow = { id: newId(), name, url: connectorUrlText(url), config };
|
|
184
180
|
writeRows(dir, [...readLocalConnectors(dir), row]);
|
|
185
181
|
return connectorFromRow(row);
|
|
@@ -193,7 +189,7 @@ export async function localCreateConnector(input: ConnectorInput, dir = agentsDi
|
|
|
193
189
|
return withLocalMigrationOperation('', dir, async (operation) => {
|
|
194
190
|
const verified = stamp(await verifyRemoteMcp(url, auth, operation.fetch));
|
|
195
191
|
return insert(dir, name, url, { auth, createdAt: new Date().toISOString(), verified, oauth: await oauthCapable(url, auth, operation.fetch), client });
|
|
196
|
-
}, true
|
|
192
|
+
}, true);
|
|
197
193
|
}
|
|
198
194
|
|
|
199
195
|
export async function localCreatePendingConnector(input: PendingConnectorInput, dir = agentsDir()): Promise<Connector> {
|
|
@@ -309,13 +305,12 @@ export async function loadConnectorPolicies(dir = agentsDir()): Promise<void> {
|
|
|
309
305
|
export async function localRenameConnector(id: string, raw: string, dir = agentsDir()): Promise<Connector> {
|
|
310
306
|
const name = connectorName(raw);
|
|
311
307
|
const row = rowOrThrow(id, dir);
|
|
312
|
-
assertNameFree(name, id, dir);
|
|
313
308
|
return Promise.resolve(replace(dir, { ...row, name }));
|
|
314
309
|
}
|
|
315
310
|
|
|
316
311
|
export async function localDeleteConnector(id: string, dir = agentsDir()): Promise<DeletedConnector> {
|
|
317
312
|
const row = rowOrThrow(id, dir);
|
|
318
|
-
assertLocalMigrationEdit(id,
|
|
313
|
+
assertLocalMigrationEdit(id, dir);
|
|
319
314
|
writeRows(dir, readLocalConnectors(dir).filter((r) => r.id !== id));
|
|
320
315
|
return Promise.resolve({ id: row.id, name: row.name });
|
|
321
316
|
}
|
|
@@ -327,8 +322,10 @@ function refreshOnce(row: LocalConnectorRow, auth: OAuthAuth, dir: string): Prom
|
|
|
327
322
|
const running = inflight.get(key);
|
|
328
323
|
if (running !== undefined) return running;
|
|
329
324
|
const job = withLocalMigrationOperation(row.id, dir, async (operation) => {
|
|
330
|
-
const fresh = await refreshOAuth(auth, parseConnectorUrl(row.url).toString(), operation.fetch);
|
|
325
|
+
const fresh = await refreshOAuth(auth, parseConnectorUrl(row.url).toString(), (input, init) => operation.dispatch(() => fetch(input, init), { refresh: true }));
|
|
331
326
|
updateRow(dir, row.id, { auth: fresh }, true);
|
|
327
|
+
flushMigrationSource(dir);
|
|
328
|
+
operation.persistedRefresh();
|
|
332
329
|
return fresh;
|
|
333
330
|
}).finally(() => { inflight.delete(key); });
|
|
334
331
|
inflight.set(key, job);
|
|
@@ -369,11 +366,7 @@ async function oauthTarget(row: LocalConnectorRow, auth: OAuthAuth, force: boole
|
|
|
369
366
|
}
|
|
370
367
|
}
|
|
371
368
|
|
|
372
|
-
export async function localRelayTarget(
|
|
373
|
-
connectorId: string,
|
|
374
|
-
force: boolean,
|
|
375
|
-
dir = agentsDir(),
|
|
376
|
-
): Promise<RelayTarget> {
|
|
369
|
+
export async function localRelayTarget(connectorId: string, force: boolean, dir = agentsDir()): Promise<RelayTarget> {
|
|
377
370
|
return withLocalMigrationOperation(connectorId, dir, async (operation) => {
|
|
378
371
|
const row = readLocalConnectors(dir).find((r) => r.id === connectorId);
|
|
379
372
|
if (row === undefined) return { kind: 'missing' };
|
|
@@ -385,11 +378,15 @@ export async function localRelayTarget(
|
|
|
385
378
|
});
|
|
386
379
|
}
|
|
387
380
|
|
|
388
|
-
export function localRollbackMigrationSource(id: string, signOutOAuth: boolean, dir = agentsDir()): void {
|
|
389
|
-
if (migrationSource(id, dir)?.phase
|
|
381
|
+
export function localRollbackMigrationSource(id: string, signOutOAuth: boolean, dir = agentsDir(), discardUncertain = false): void {
|
|
382
|
+
if (!['rollback', 'recovering'].includes(migrationSource(id, dir)?.phase ?? '') || !localMigrationIdle(id, dir, discardUncertain && signOutOAuth)) throw new ConnectorError('The connector migration is not ready to roll back.', 409);
|
|
390
383
|
const row = rowOrThrow(id, dir);
|
|
391
384
|
if (signOutOAuth) replace(dir, signedOut(row), true);
|
|
392
|
-
if (signOutOAuth)
|
|
385
|
+
if (signOutOAuth) flushMigrationSource(dir);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export function flushMigrationSource(dir = agentsDir()): void {
|
|
389
|
+
for (const path of [filePath(dir), dir]) {
|
|
393
390
|
const fd = openSync(path, 'r');
|
|
394
391
|
try { fsyncSync(fd); } finally { closeSync(fd); }
|
|
395
392
|
}
|
|
@@ -6,7 +6,7 @@ import { connectorAuth, connectorName } from '@metro-labs/http/connector-input';
|
|
|
6
6
|
import { readConfig } from './config.js';
|
|
7
7
|
import { parseConnectorUrl } from '@metro-labs/http/connector-url';
|
|
8
8
|
import { normalizePolicy } from '../policy/policy.js';
|
|
9
|
-
import { localImportConnectors,
|
|
9
|
+
import { localImportConnectors, type LocalConnectorRow } from './store.js';
|
|
10
10
|
|
|
11
11
|
export interface ConnectorCopyResult {
|
|
12
12
|
sourceId: string | null;
|
|
@@ -28,7 +28,6 @@ function copyRow(raw: unknown): LocalConnectorRow {
|
|
|
28
28
|
export function copyConnectorRows(value: unknown, dir?: string): ConnectorCopyResult[] {
|
|
29
29
|
if (!isRecord(value) || value.version !== 1 || !Array.isArray(value.connectors) || value.connectors.length > 500)
|
|
30
30
|
throw new ApiError('invalid connector transfer', 400);
|
|
31
|
-
const names = new Set(readLocalConnectors(dir).map((row) => row.name));
|
|
32
31
|
const imported: LocalConnectorRow[] = [];
|
|
33
32
|
const results: ConnectorCopyResult[] = value.connectors.map((raw: unknown) => {
|
|
34
33
|
const sourceId = isRecord(raw) && typeof raw.id === 'string' ? parseId(raw.id) : null;
|
|
@@ -38,8 +37,6 @@ export function copyConnectorRows(value: unknown, dir?: string): ConnectorCopyRe
|
|
|
38
37
|
} catch {
|
|
39
38
|
return { sourceId, status: 'invalid' };
|
|
40
39
|
}
|
|
41
|
-
if (names.has(row.name)) return { sourceId, status: 'skipped' };
|
|
42
|
-
names.add(row.name);
|
|
43
40
|
const id = newId();
|
|
44
41
|
imported.push({ ...row, id });
|
|
45
42
|
return { sourceId, id, status: 'copied' };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { str } from '@metro-labs/core/str';
|
|
2
2
|
import { readPreview, shownValue } from '../approvals/preview.js';
|
|
3
|
-
import { connectorToolOf } from '../connectors/gates.js';
|
|
3
|
+
import { connectorGates, connectorToolOf } from '../connectors/gates.js';
|
|
4
4
|
import { stationByName } from '../stations/registry.js';
|
|
5
5
|
import { stationOfAccount } from './read-tool.js';
|
|
6
6
|
|
|
@@ -92,7 +92,11 @@ function bodyLines(params: PermissionParams): string[] {
|
|
|
92
92
|
const raw = params.input_preview.trim() === '' ? [] : [shorten(params.input_preview, RAW_MAX)];
|
|
93
93
|
if (metro !== undefined) return input === undefined ? [`Approval needed: ${metro}`, ...raw] : metroLines(metro, input);
|
|
94
94
|
const connector = connectorToolOf(params.tool_name);
|
|
95
|
-
if (connector !== undefined)
|
|
95
|
+
if (connector !== undefined) {
|
|
96
|
+
const { gate, tool } = connector;
|
|
97
|
+
const duplicate = connectorGates().some((entry) => entry.id !== gate.id && entry.name === gate.name);
|
|
98
|
+
return connectorLines(duplicate ? `${gate.name} (${gate.id})` : gate.name, tool, input);
|
|
99
|
+
}
|
|
96
100
|
return [`Claude wants to run ${params.tool_name}: ${shorten(params.description)}`, '', ...raw];
|
|
97
101
|
}
|
|
98
102
|
|
|
@@ -4,7 +4,6 @@ import { accountFromLine, knownAccounts } from '../agents/map.js';
|
|
|
4
4
|
import { policyFor, storedPolicy, strictest, type Access, type PolicyTarget } from '../policy/policy.js';
|
|
5
5
|
import { errResult } from './ctx.js';
|
|
6
6
|
import { NEEDS_APPROVAL, takeGrant } from '../approvals/pending.js';
|
|
7
|
-
import { connectorToolOf } from '../connectors/gates.js';
|
|
8
7
|
import { profileScopeLine } from './profile-lookup.js';
|
|
9
8
|
import { stationOfAccount } from './read-tool.js';
|
|
10
9
|
import { stationForTool, toolGroupOf } from './tool-catalog.js';
|
|
@@ -56,7 +55,7 @@ export function channelDecision(name: string, a: Record<string, unknown>): Polic
|
|
|
56
55
|
return verdict;
|
|
57
56
|
}
|
|
58
57
|
|
|
59
|
-
const isChannelTool = (name: string) => (tool: string): boolean => tool
|
|
58
|
+
const isChannelTool = (name: string) => (tool: string): boolean => tool === `mcp__metro__${name}`;
|
|
60
59
|
|
|
61
60
|
export function policyGate(name: string, a: Record<string, unknown>): ToolResult | undefined {
|
|
62
61
|
const { access, target } = channelDecision(name, a);
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"./connector-oauth": "./src/connector-oauth.ts",
|
|
23
23
|
"./connector-input": "./src/connector-input.ts",
|
|
24
24
|
"./connector-delivery": "./src/connector-delivery.ts",
|
|
25
|
-
"./connector-migration": "./src/connector-migration.ts"
|
|
25
|
+
"./connector-migration": "./src/connector-migration.ts",
|
|
26
|
+
"./connector-grant": "./src/connector-grant.ts"
|
|
26
27
|
},
|
|
27
28
|
"scripts": {
|
|
28
29
|
"build": "tsc",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { deliveryRecord } from './connector-delivery-validation.js';
|
|
2
|
+
import type { OAuthAuth } from './connector-mcp.js';
|
|
3
|
+
import { boundOAuthResource, parsePublicConnectorUrl } from './connector-public-url.js';
|
|
4
|
+
|
|
5
|
+
export interface PortableGrant extends OAuthAuth {
|
|
6
|
+
resource: string;
|
|
7
|
+
tokenEndpointAuthMethod: 'none' | 'client_secret_post';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const invalid = (): never => { throw new Error('This OAuth grant cannot be transferred. Its client authentication or provider binding is unsupported. Sign in separately.'); };
|
|
11
|
+
|
|
12
|
+
function text(raw: unknown, max = 16384): string {
|
|
13
|
+
if (typeof raw !== 'string' || raw.length === 0 || raw.length > max || /[^\x21-\x7e]/.test(raw)) return invalid();
|
|
14
|
+
return raw;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function endpoint(raw: unknown): string {
|
|
18
|
+
const value = text(raw, 2048);
|
|
19
|
+
parsePublicConnectorUrl(value);
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function grantResource(target: string, raw: unknown): string {
|
|
24
|
+
const transport = parsePublicConnectorUrl(target);
|
|
25
|
+
const resource = parsePublicConnectorUrl(raw);
|
|
26
|
+
if (resource.toString() === transport.toString() && typeof raw === 'string') return raw;
|
|
27
|
+
return boundOAuthResource(target, raw);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function binding(value: Record<string, unknown>): Pick<PortableGrant, 'clientId' | 'clientSecret' | 'tokenEndpointAuthMethod'> {
|
|
31
|
+
const clientSecret = value.clientSecret === undefined ? undefined : text(value.clientSecret);
|
|
32
|
+
const method = value.tokenEndpointAuthMethod ?? (clientSecret === undefined ? 'none' : 'client_secret_post');
|
|
33
|
+
if (method !== 'none' && method !== 'client_secret_post') return invalid();
|
|
34
|
+
if ((method === 'client_secret_post') !== (clientSecret !== undefined)) return invalid();
|
|
35
|
+
return { clientId: text(value.clientId), tokenEndpointAuthMethod: method, ...(clientSecret === undefined ? {} : { clientSecret }) };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function expiry(raw: unknown): Pick<PortableGrant, 'expiresAt'> {
|
|
39
|
+
if (raw === undefined) return {};
|
|
40
|
+
if (typeof raw !== 'number' || !Number.isSafeInteger(raw) || raw < 0) return invalid();
|
|
41
|
+
return { expiresAt: raw };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function scopeOf(raw: unknown): Pick<PortableGrant, 'scope'> {
|
|
45
|
+
if (raw === undefined) return {};
|
|
46
|
+
if (typeof raw !== 'string' || raw.length > 16384 || /[^\x20-\x7e]/.test(raw)) return invalid();
|
|
47
|
+
return { scope: raw };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function portableGrant(raw: unknown, target: string): PortableGrant {
|
|
51
|
+
const value = deliveryRecord(raw, ['kind', 'accessToken', 'refreshToken', 'expiresAt', 'clientId', 'clientSecret', 'issuer', 'tokenEndpoint', 'scope', 'resource', 'tokenEndpointAuthMethod']);
|
|
52
|
+
if (value.kind !== 'oauth') return invalid();
|
|
53
|
+
return {
|
|
54
|
+
kind: 'oauth', accessToken: text(value.accessToken, 16377), issuer: endpoint(value.issuer), tokenEndpoint: endpoint(value.tokenEndpoint),
|
|
55
|
+
resource: grantResource(target, value.resource ?? parsePublicConnectorUrl(target).toString()),
|
|
56
|
+
...binding(value), ...expiry(value.expiresAt), ...scopeOf(value.scope),
|
|
57
|
+
...(value.refreshToken === undefined ? {} : { refreshToken: text(value.refreshToken) }),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -4,6 +4,7 @@ import { sealingKeyOf } from './agent-signature.js';
|
|
|
4
4
|
import { deliveryBytes, deliveryRecord } from './connector-delivery-validation.js';
|
|
5
5
|
import { connectorAuth, connectorClient, connectorName } from './connector-input.js';
|
|
6
6
|
import type { OAuthClient } from './connector-oauth.js';
|
|
7
|
+
import { portableGrant, type PortableGrant } from './connector-grant.js';
|
|
7
8
|
import { connectorPolicyOf, type ConnectorPolicy } from './connector-policy.js';
|
|
8
9
|
import { boundOAuthResource, parsePublicConnectorUrl } from './connector-public-url.js';
|
|
9
10
|
import { openTransfer, sealTransfer, type SealedTransfer } from './transfer-crypto.js';
|
|
@@ -24,7 +25,7 @@ export interface MigrationContext {
|
|
|
24
25
|
export interface MigrationPayload {
|
|
25
26
|
name: string;
|
|
26
27
|
url: string;
|
|
27
|
-
auth: { kind: 'none' } | { kind: 'header'; name: string; value: string } | { kind: 'oauth' };
|
|
28
|
+
auth: { kind: 'none' } | { kind: 'header'; name: string; value: string } | { kind: 'oauth'; grant?: PortableGrant };
|
|
28
29
|
client: OAuthClient | null;
|
|
29
30
|
oauthResource?: string;
|
|
30
31
|
policy: ConnectorPolicy;
|
|
@@ -36,7 +37,8 @@ export interface MigrationView {
|
|
|
36
37
|
sourceId: string;
|
|
37
38
|
destinationId: string;
|
|
38
39
|
phase: 'preparing' | 'prepared' | 'active' | 'rolled_back';
|
|
39
|
-
readiness: 'pending' | 'ready' | 'signin' | 'changed' | 'unavailable';
|
|
40
|
+
readiness: 'pending' | 'ready' | 'signin' | 'changed' | 'unavailable' | 'refresh_uncertain';
|
|
41
|
+
grantImported?: boolean;
|
|
40
42
|
name: string | null;
|
|
41
43
|
auth: 'none' | 'header' | 'oauth' | null;
|
|
42
44
|
policy: ConnectorPolicy | null;
|
|
@@ -48,7 +50,7 @@ export interface MigrationResponse {
|
|
|
48
50
|
receiving?: { publicKey: string; context: MigrationContext };
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
export type MigrationRequest = { action: 'begin' | 'status' | 'activate' | 'rollback'; operationId: string; sourceId: string } | { action: 'prepare'; operationId: string; sourceId: string; sealed: unknown };
|
|
53
|
+
export type MigrationRequest = { action: 'begin' | 'status' | 'activate' | 'rollback'; operationId: string; sourceId: string } | { action: 'prepare' | 'handover'; operationId: string; sourceId: string; sealed: unknown };
|
|
52
54
|
|
|
53
55
|
const invalid = (): Error => new Error('Invalid connector migration.');
|
|
54
56
|
const idOf = (value: unknown): string => {
|
|
@@ -61,7 +63,7 @@ export function migrationRequestOf(raw: unknown): MigrationRequest {
|
|
|
61
63
|
const value = deliveryRecord(raw, ['action', 'operationId', 'sourceId', 'sealed']);
|
|
62
64
|
const operationId = idOf(value.operationId);
|
|
63
65
|
const sourceId = idOf(value.sourceId);
|
|
64
|
-
if (value.action === 'prepare' && Object.hasOwn(value, 'sealed')) return { action:
|
|
66
|
+
if ((value.action === 'prepare' || value.action === 'handover') && Object.hasOwn(value, 'sealed')) return { action: value.action, operationId, sourceId, sealed: value.sealed };
|
|
65
67
|
if (Object.hasOwn(value, 'sealed')) throw invalid();
|
|
66
68
|
if (value.action !== 'begin' && value.action !== 'status' && value.action !== 'activate' && value.action !== 'rollback') throw invalid();
|
|
67
69
|
return { action: value.action, operationId, sourceId };
|
|
@@ -79,15 +81,19 @@ function contextBytes(context: MigrationContext): Buffer {
|
|
|
79
81
|
return Buffer.from(JSON.stringify(['metro-connector-migration/v1', value.owner, value.agent, value.keyId, value.operationId, value.sourceId, value.destinationId]));
|
|
80
82
|
}
|
|
81
83
|
|
|
82
|
-
function
|
|
83
|
-
const
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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);
|
|
89
94
|
if ((value.kind !== 'none' && value.kind !== 'oauth') || Object.hasOwn(value, 'name') || Object.hasOwn(value, 'value')) throw invalid();
|
|
90
|
-
return { kind: value.kind };
|
|
95
|
+
if (value.kind === 'none' || value.grant === undefined) return { kind: value.kind };
|
|
96
|
+
return { kind: 'oauth', grant: portableGrant(value.grant, target) };
|
|
91
97
|
}
|
|
92
98
|
|
|
93
99
|
function clientOf(raw: unknown): OAuthClient | null {
|
|
@@ -109,7 +115,7 @@ export function migrationPayloadOf(raw: unknown): MigrationPayload {
|
|
|
109
115
|
if (typeof value.name !== 'string' || connectorName(value.name) !== value.name || typeof value.url !== 'string' || typeof value.sourceStamp !== 'string') throw invalid();
|
|
110
116
|
parsePublicConnectorUrl(value.url);
|
|
111
117
|
deliveryBytes(value.sourceStamp, 32);
|
|
112
|
-
const auth = authOf(value.auth);
|
|
118
|
+
const auth = authOf(value.auth, value.url);
|
|
113
119
|
const client = migrationClient(auth, value);
|
|
114
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 };
|
|
115
121
|
if (Buffer.byteLength(JSON.stringify(payload)) > PAYLOAD_MAX) throw invalid();
|
|
@@ -122,7 +128,7 @@ function phaseOf(value: unknown): MigrationView['phase'] {
|
|
|
122
128
|
}
|
|
123
129
|
|
|
124
130
|
function readinessOf(value: unknown): MigrationView['readiness'] {
|
|
125
|
-
if (value !== 'pending' && value !== 'ready' && value !== 'signin' && value !== 'changed' && value !== 'unavailable') throw invalid();
|
|
131
|
+
if (value !== 'pending' && value !== 'ready' && value !== 'signin' && value !== 'changed' && value !== 'unavailable' && value !== 'refresh_uncertain') throw invalid();
|
|
126
132
|
return value;
|
|
127
133
|
}
|
|
128
134
|
|
|
@@ -145,8 +151,9 @@ function nullableStamp(value: unknown): string | null {
|
|
|
145
151
|
}
|
|
146
152
|
|
|
147
153
|
function viewOf(raw: unknown): MigrationView {
|
|
148
|
-
const view = deliveryRecord(raw, ['operationId', 'sourceId', 'destinationId', 'phase', 'readiness', 'name', 'auth', 'policy', 'sourceStamp']);
|
|
149
|
-
|
|
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) };
|
|
150
157
|
}
|
|
151
158
|
|
|
152
159
|
export function migrationResponseOf(raw: unknown): MigrationResponse {
|
package/runtime/runtime.json
CHANGED