@stage-labs/metro 0.1.0-beta.59 → 0.1.0-beta.60

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.59",
3
+ "version": "0.1.0-beta.60",
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": {
@@ -1,10 +1,12 @@
1
1
  import { execFile, spawn, type ChildProcess } from 'node:child_process';
2
2
  import { timingSafeEqual } from 'node:crypto';
3
3
  import { Resolver } from 'node:dns/promises';
4
+ import { request as httpsRequest } from 'node:https';
4
5
  import { homedir } from 'node:os';
5
6
  import { join } from 'node:path';
6
7
  import { errMsg, log } from './log.js';
7
8
  import { readJson } from './secure-fs.js';
9
+ import { localOwner } from '../db/file-admin.js';
8
10
 
9
11
  const RESTART_DELAY_MS = 2_000;
10
12
  const RESTART_DELAY_MAX_MS = 30_000;
@@ -40,25 +42,54 @@ const runText = (command: string, args: string[]): Promise<string> =>
40
42
  });
41
43
  });
42
44
 
43
- export type Probe = (url: string) => Promise<boolean>;
45
+ export type Probe = (url: string, owner: string | null) => Promise<boolean>;
44
46
 
45
47
  const PROBE_MS = 5_000;
46
48
 
47
- export async function daemonAnswersAt(url: string): Promise<boolean> {
48
- const controller = new AbortController();
49
- const timer = setTimeout(() => {
50
- controller.abort();
51
- }, PROBE_MS);
52
- try {
53
- const res = await fetch(`${url}/api/mode`, { signal: controller.signal });
54
- if (!res.ok) return false;
55
- const body: unknown = await res.json();
56
- return typeof body === 'object' && body !== null && (body as { mode?: unknown }).mode === 'local';
57
- } catch {
58
- return false;
59
- } finally {
60
- clearTimeout(timer);
61
- }
49
+ async function publicAddresses(host: string): Promise<string[]> {
50
+ const found = await Promise.all(
51
+ PUBLIC_RESOLVERS.map(async (server) => {
52
+ const resolver = new Resolver();
53
+ resolver.setServers([server]);
54
+ const [v4, v6] = await Promise.all([resolver.resolve4(host).catch(() => []), resolver.resolve6(host).catch(() => [])]);
55
+ return [...v4, ...v6];
56
+ }),
57
+ );
58
+ return [...new Set(found.flat())];
59
+ }
60
+
61
+ function modeThrough(ip: string, host: string): Promise<{ mode?: unknown; owner?: unknown } | null> {
62
+ return new Promise((resolve) => {
63
+ const req = httpsRequest(
64
+ { host: ip, servername: host, port: 443, path: '/api/mode', method: 'GET', headers: { host }, timeout: PROBE_MS },
65
+ (res) => {
66
+ const chunks: Buffer[] = [];
67
+ res.on('data', (c: Buffer) => chunks.push(c));
68
+ res.on('end', () => {
69
+ try {
70
+ resolve(res.statusCode === 200 ? (JSON.parse(Buffer.concat(chunks).toString('utf8')) as { mode?: unknown }) : null);
71
+ } catch {
72
+ resolve(null);
73
+ }
74
+ });
75
+ },
76
+ );
77
+ req.on('timeout', () => {
78
+ req.destroy();
79
+ });
80
+ req.on('error', () => {
81
+ resolve(null);
82
+ });
83
+ req.end();
84
+ });
85
+ }
86
+
87
+ export async function daemonAnswersAt(url: string, owner: string | null): Promise<boolean> {
88
+ const host = new URL(url).host;
89
+ const addresses = await publicAddresses(host);
90
+ if (addresses.length === 0) return false;
91
+ const body = await modeThrough(addresses[0] ?? '', host);
92
+ return body !== null && body.mode === 'local' && (owner === null || body.owner === owner);
62
93
  }
63
94
 
64
95
  export function nodeNameIn(statusJson: string): string | null {
@@ -72,11 +103,11 @@ export function nodeNameIn(statusJson: string): string | null {
72
103
  }
73
104
  }
74
105
 
75
- async function adoptFunnel(bin: string, port: number, probe: Probe): Promise<Adopted> {
106
+ async function adoptFunnel(bin: string, port: number, probe: Probe, owner: () => string | null): Promise<Adopted> {
76
107
  const name = nodeNameIn(await runText(bin, ['status', '--json']));
77
108
  if (name !== null) {
78
109
  const url = `https://${name}`;
79
- if (await probe(url)) return { url, hint: `a Funnel already publishes this daemon at ${url}; using it` };
110
+ if (await probe(url, owner())) return { url, hint: `a Funnel already publishes this daemon at ${url}; using it` };
80
111
  }
81
112
  return funnelAlreadyServing(await runText(bin, ['funnel', 'status']), port);
82
113
  }
@@ -104,13 +135,18 @@ function tailscaleBin(): string {
104
135
  return configured === '' ? 'tailscale' : configured;
105
136
  }
106
137
 
107
- export const funnelDriver = (port: number, bin = tailscaleBin(), probe: Probe = daemonAnswersAt): TunnelDriver => ({
138
+ export const funnelDriver = (
139
+ port: number,
140
+ bin = tailscaleBin(),
141
+ probe: Probe = daemonAnswersAt,
142
+ owner: () => string | null = localOwner,
143
+ ): TunnelDriver => ({
108
144
  name: 'tailscale funnel',
109
145
  command: bin,
110
146
  args: ['funnel', String(port)],
111
147
  urlIn: funnelUrlIn,
112
148
  waitsForDns: true,
113
- adopt: () => adoptFunnel(bin, port, probe),
149
+ adopt: () => adoptFunnel(bin, port, probe, owner),
114
150
  });
115
151
 
116
152
  let liveUrl: string | null = null;
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.1.0-beta.59"
2
+ "version": "0.1.0-beta.60"
3
3
  }