@stage-labs/metro 0.1.0-beta.40 → 0.1.0-beta.41

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage-labs/metro",
3
- "version": "0.1.0-beta.40",
3
+ "version": "0.1.0-beta.41",
4
4
  "description": "The metro command line. Sign in once per machine, then hand your MCP connector list to Claude Code without the credentials touching disk, argv or shell history.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -45,11 +45,16 @@ function credential(file: Record<string, unknown> | null): HostedCredential | nu
45
45
  return { token: file.token, url: typeof file.url === 'string' && file.url !== '' ? file.url : metroBaseUrl() };
46
46
  }
47
47
 
48
- export function hostedCredentialFor(agentId: string, dir = configDir()): HostedCredential | null {
49
- const runtime = join(dir, `runtime-${agentId}.json`);
50
- if (existsSync(runtime)) return credential(readJsonFile(runtime));
48
+ export function hostedCredentialsFor(agentId: string, dir = configDir()): HostedCredential[] {
49
+ const out: HostedCredential[] = [];
51
50
  const login = credential(readJsonFile(join(dir, 'credentials.json')));
52
- return login !== null && agentOfToken(login.token) === agentId ? login : null;
51
+ if (login !== null && agentOfToken(login.token) === agentId) out.push(login);
52
+ const runtime = join(dir, `runtime-${agentId}.json`);
53
+ if (existsSync(runtime)) {
54
+ const cred = credential(readJsonFile(runtime));
55
+ if (cred !== null) out.push(cred);
56
+ }
57
+ return out;
53
58
  }
54
59
 
55
60
  const isConnector = (v: unknown): v is HostedConnector =>
@@ -58,26 +63,48 @@ const isConnector = (v: unknown): v is HostedConnector =>
58
63
  typeof (v as { id?: unknown }).id === 'string' &&
59
64
  typeof (v as { name?: unknown }).name === 'string';
60
65
 
66
+ class Refused extends Error {}
67
+
61
68
  async function fetchHosted(cred: HostedCredential): Promise<HostedConnector[]> {
62
69
  const res = await fetch(`${cred.url}/api/cli/connectors`, {
63
70
  headers: { authorization: `Bearer ${cred.token}` },
64
71
  signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
65
72
  redirect: 'manual',
66
73
  });
74
+ if (res.status === 401 || res.status === 403 || res.status === 409) {
75
+ await res.arrayBuffer().catch(() => undefined);
76
+ throw new Refused(`metro no longer accepts this credential (${String(res.status)})`);
77
+ }
67
78
  if (!res.ok) throw new Error(`metro answered ${String(res.status)} for the connectors`);
68
79
  const body = (await res.json()) as { connectors?: unknown };
69
80
  return Array.isArray(body.connectors) ? body.connectors.filter(isConnector) : [];
70
81
  }
71
82
 
83
+ async function firstAccepted(agentId: string, creds: HostedCredential[]): Promise<HostedConnector[] | null> {
84
+ for (const cred of creds) {
85
+ try {
86
+ return await fetchHosted(cred);
87
+ } catch (err) {
88
+ if (!(err instanceof Refused)) throw err;
89
+ log.warn({ agent: agentId, reason: errMsg(err) }, 'hosted connectors: credential refused, trying the next');
90
+ }
91
+ }
92
+ return null;
93
+ }
94
+
72
95
  const cache = new Map<string, { at: number; list: HostedConnector[] }>();
73
96
 
74
97
  export async function hostedConnectorsFor(agentId: string, dir = configDir()): Promise<HostedConnector[]> {
75
98
  const hit = cache.get(agentId);
76
99
  if (hit !== undefined && Date.now() - hit.at < CACHE_MS) return hit.list;
77
- const cred = hostedCredentialFor(agentId, dir);
78
- if (cred === null) return [];
100
+ const creds = hostedCredentialsFor(agentId, dir);
101
+ if (creds.length === 0) return [];
79
102
  try {
80
- const list = await fetchHosted(cred);
103
+ const list = await firstAccepted(agentId, creds);
104
+ if (list === null) {
105
+ log.warn({ agent: agentId }, 'hosted connectors: every stored credential is stale; metro login again');
106
+ return [];
107
+ }
81
108
  cache.set(agentId, { at: Date.now(), list });
82
109
  return list;
83
110
  } catch (err) {
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.1.0-beta.40"
2
+ "version": "0.1.0-beta.41"
3
3
  }