@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.
Files changed (29) hide show
  1. package/package.json +1 -1
  2. package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
  3. package/runtime/marketplace/plugin/bin/policy.mjs +4 -1
  4. package/runtime/node_modules/@metro-labs/daemon/src/connectors/config.ts +2 -0
  5. package/runtime/node_modules/@metro-labs/daemon/src/connectors/gates.ts +27 -23
  6. package/runtime/node_modules/@metro-labs/daemon/src/connectors/migration-api.ts +1 -1
  7. package/runtime/node_modules/@metro-labs/daemon/src/connectors/migration-control.ts +107 -30
  8. package/runtime/node_modules/@metro-labs/daemon/src/connectors/migration-guard.ts +17 -13
  9. package/runtime/node_modules/@metro-labs/daemon/src/connectors/migration-operations.ts +36 -6
  10. package/runtime/node_modules/@metro-labs/daemon/src/connectors/migration-preview.ts +29 -13
  11. package/runtime/node_modules/@metro-labs/daemon/src/connectors/migration-source.ts +18 -2
  12. package/runtime/node_modules/@metro-labs/daemon/src/connectors/migration-state.ts +20 -3
  13. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-routes.ts +2 -2
  14. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth.ts +2 -1
  15. package/runtime/node_modules/@metro-labs/daemon/src/connectors/plugin-sync.ts +39 -12
  16. package/runtime/node_modules/@metro-labs/daemon/src/connectors/publication.ts +22 -11
  17. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-passive.ts +96 -0
  18. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay.ts +19 -10
  19. package/runtime/node_modules/@metro-labs/daemon/src/connectors/server-keys.ts +46 -0
  20. package/runtime/node_modules/@metro-labs/daemon/src/connectors/store.ts +21 -24
  21. package/runtime/node_modules/@metro-labs/daemon/src/connectors/transfer-store.ts +1 -4
  22. package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-prompt.ts +6 -2
  23. package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-gate.ts +1 -2
  24. package/runtime/node_modules/@metro-labs/http/package.json +2 -1
  25. package/runtime/node_modules/@metro-labs/http/src/connector-grant.ts +59 -0
  26. package/runtime/node_modules/@metro-labs/http/src/connector-mcp.ts +2 -0
  27. package/runtime/node_modules/@metro-labs/http/src/connector-migration.ts +23 -16
  28. package/runtime/runtime.json +1 -1
  29. 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, row.name, dir);
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 || r.name === row.name));
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, row.name, dir);
163
+ for (const row of imported) assertLocalMigrationEdit(row.id, dir);
161
164
  const ids = new Set(imported.map((r) => r.id));
162
- const names = new Set(imported.map((r) => r.name));
163
- writeRows(dir, [...current.filter((r) => !ids.has(r.id) && !names.has(r.name)), ...imported]);
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, name);
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, row.name, dir);
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 !== 'rollback' || !localMigrationIdle(id, dir)) throw new ConnectorError('The connector migration is not ready to roll back.', 409);
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) for (const path of [filePath(dir), dir]) {
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, readLocalConnectors, type LocalConnectorRow } from './store.js';
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) return connectorLines(connector.gate.name, connector.tool, input);
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.endsWith(`__${name}`) && connectorToolOf(tool) === undefined;
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
+ }
@@ -17,6 +17,8 @@ export interface OAuthAuth extends OAuthTokens {
17
17
  clientId: string;
18
18
  clientSecret?: string;
19
19
  scope?: string;
20
+ resource?: string;
21
+ tokenEndpointAuthMethod?: string;
20
22
  }
21
23
 
22
24
  export type ConnectorAuth =
@@ -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: 'prepare', operationId, sourceId, sealed: value.sealed };
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 authOf(raw: unknown): MigrationPayload['auth'] {
83
- const value = deliveryRecord(raw, ['kind', 'name', 'value']);
84
- if (value.kind === 'header') {
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
- }
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
- return { 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) };
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 {
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.1.0-beta.291"
2
+ "version": "0.1.0-beta.293"
3
3
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.0-beta.291",
2
+ "version": "0.1.0-beta.293",
3
3
  "dependencies": {
4
4
  "@anthropic-ai/claude-agent-sdk": "0.3.287",
5
5
  "@modelcontextprotocol/sdk": "^1.29.0",