@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.
|
|
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
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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 = (
|
|
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;
|
package/runtime/runtime.json
CHANGED