bridgerator 1.0.5 → 1.0.7

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/README.md CHANGED
@@ -1,107 +1,109 @@
1
- # Bridgerator
2
-
3
- Connect your Windows PC to ChatGPT. The assistant can read and edit files, search folders and run
4
- PowerShell commands on **your** computer — through a bridge that only makes an **outbound**
5
- connection. No inbound ports, no remote-desktop software.
6
-
7
- ```
8
- ChatGPT ──OAuth 2.1 + MCP (HTTPS)──► relay (mcp.bridgerator.recondeck.com) ──WebSocket──► bridgerator on your PC ──stdio──► bundled MCP engine
9
- ```
10
-
11
- This repository is the part that runs on your PC. It is small on purpose so you (or an AI agent
12
- installing it for you) can read all of it: `src/bridge.mjs` (forwarder), `src/policy.mjs` (access
13
- rules), `src/dc-isolate.mjs` (separate engine config), `src/cli.mjs` (setup/autostart), `install.ps1` (installer).
14
-
15
- ## What people use it for
16
-
17
- - "Sort my Downloads folder by type and month" — ChatGPT lists, plans and moves the files on your PC.
18
- - "Find every invoice PDF in Documents and rename it by date" — search and rename in plain words.
19
- - "Why is drive C almost full?" — disk usage, big folders, what is safe to delete.
20
- - "Check the event log for errors since yesterday" / "Read the end of this log" — diagnostics.
21
- - Work on code: "explain how D:\\Projects\\app is built and run its tests".
22
-
23
- Everything runs as your Windows user and only inside the folders you allow.
24
-
25
- ## Plans
26
-
27
- Free: 15,000 tool calls per month per account, any number of PCs. Pro: $10/month, unlimited.
28
- Details: https://bridgerator.recondeck.com/pricing
29
-
30
- ## Install (Windows)
31
-
32
- 1. Create an account and a device key at https://bridgerator.recondeck.com.
33
- 2. In PowerShell (no admin rights needed):
34
- ```powershell
35
- & ([scriptblock]::Create((irm https://bridgerator.recondeck.com/install.ps1))) -Token <device key>
36
- ```
37
- It downloads portable Node.js LTS if missing (checksum-verified, no admin), installs this package and runs `bridgerator setup`.
38
- Update later with the same line without `-Token` (the key and folder settings are kept).
39
- Prefer to read first? Download `install.ps1`, read it, run it with `-Token`.
40
- 3. In ChatGPT: Settings → Apps & Connectors → Advanced → Developer mode → Create → URL
41
- `https://mcp.bridgerator.recondeck.com/mcp`, authentication OAuth, sign in with your account.
42
-
43
- Manual install (PowerShell): `$env:PUPPETEER_SKIP_DOWNLOAD=1; npm install -g bridgerator`, then `bridgerator setup --token <key>`. The variable skips a ~700 MB Chrome download that only PDF export needs; PDF export fetches a browser itself on first use.
44
-
45
- ## Access rules — what the assistant can and cannot touch
46
-
47
- Config: `%APPDATA%\Bridgerator\config.json` (written by `setup`, edit and restart to change).
48
-
49
- | Key | Meaning | Default |
50
- |---|---|---|
51
- | `allowedDirectories` | File tools work only inside these folders (enforced by the engine). Empty = whole disk. | your user folder |
52
- | `deniedDirectories` | Folders excluded **inside** the allowed ones (enforced by Bridgerator, see below). | `~\.ssh`, `~\AppData` |
53
- | `blockedCommands` | Commands the engine refuses to run. Leave it out to keep the built-in list. | built-in list (`format`, `diskpart`, `reg`, `net`, `sc`, `runas`, `netsh`, `takeown`, …) |
54
- | `disabledTools` | Tools never shown to the assistant, e.g. `["start_process","interact_with_process"]` to turn the terminal off. | none |
55
-
56
- Full file, search and terminal toolset, including ChatGPT widgets (file preview,
57
- config view). Hidden: `set_config_value` (would let the assistant change its own limits), feedback and
58
- usage-stats tools.
59
-
60
- How exclusions work (`src/policy.mjs`): every path argument is resolved (symlinks and junctions
61
- included) and refused if it lies inside an excluded folder; result lines that mention an excluded
62
- path (search hits, recursive listings) are removed; `set_config_value` is never exposed, so the
63
- assistant cannot change these limits.
64
-
65
- ⚠ **The terminal is not a folder sandbox.** A shell command runs with your Windows user's rights
66
- and can read anything you can. Bridgerator refuses commands that literally name an excluded folder,
67
- but that is a courtesy, not a boundary. If folder limits must be strict, add the terminal tools to
68
- `disabledTools`.
69
-
70
- The bridge runs as **your** Windows user (hidden scheduled task at logon), never as SYSTEM.
71
-
72
- Bridgerator gives its bundled engine its **own** config folder (`%APPDATA%\Bridgerator\dc`, see `src/dc-isolate.mjs`): if you already run the same open-source MCP server for Claude or another app, its settings and folder limits are not touched. The engine keeps a local log of recent tool calls in that folder; it never leaves your PC.
73
-
74
- ## Commands
75
-
76
- ```
77
- bridgerator setup --token <key> [--allow <dir>]... [--deny <dir>]... # config + autostart + start
78
- bridgerator run # foreground (what autostart runs)
79
- bridgerator status # config (token masked) + log tail
80
- bridgerator uninstall # stop + remove autostart
81
- ```
82
- Log: `%APPDATA%\Bridgerator\bridge.log`. Stopping the bridge cuts access immediately.
83
-
84
- ## Privacy
85
-
86
- File contents and command output go to ChatGPT (OpenAI) as tool results; the relay forwards them
87
- in memory and does not store them. Engine telemetry, remote feature flags and any browser pop-ups are turned off by the bridge.
88
- Policy: https://bridgerator.recondeck.com/privacy
89
-
90
- ## Development
91
-
92
- `npm test` — policy unit tests plus an end-to-end run against the real bundled engine
93
- (throw-away folders; asserts that an existing config of the same engine stays byte-identical).
94
-
95
- ## License
96
-
97
- MIT. The bundled open-source MCP engine is MIT-licensed as well; its copyright notice is kept in
98
- `LICENSE` and the dependency is listed in `package.json`.
99
-
100
- ---
101
-
102
- ### По-русски коротко
103
- Bridgerator подключает ваш ПК к ChatGPT: файлы, поиск, терминал. Мост только исходящий, входящих
104
- портов нет. Установка — одна строка PowerShell с ключом устройства из кабинета (см. выше).
105
- Разрешённые папки — `allowedDirectories`, исключения внутри них — `deniedDirectories`
106
- (исключения обеспечивает сам Bridgerator). Терминал папками не
107
- ограничивается — для строгих границ выключите его через `disabledTools`.
1
+ # Bridgerator
2
+
3
+ Connect your Windows PC to ChatGPT. The assistant can read and edit files, search folders and run
4
+ PowerShell commands on **your** computer — through a bridge that only makes an **outbound**
5
+ connection. No inbound ports, no remote-desktop software.
6
+
7
+ ```
8
+ ChatGPT ──OAuth 2.1 + MCP (HTTPS)──► relay (mcp.bridgerator.recondeck.com) ──WebSocket──► bridgerator on your PC ──stdio──► bundled MCP engine
9
+ ```
10
+
11
+ This repository is the part that runs on your PC. It is small on purpose so you (or an AI agent
12
+ installing it for you) can read all of it: `src/bridge.mjs` (forwarder), `src/policy.mjs` (access
13
+ rules), `src/dc-isolate.mjs` (separate engine config), `src/cli.mjs` (setup/autostart), `src/pair.mjs` (browser pairing), `install.ps1` (installer).
14
+
15
+ ## What people use it for
16
+
17
+ - "Sort my Downloads folder by type and month" — ChatGPT lists, plans and moves the files on your PC.
18
+ - "Find every invoice PDF in Documents and rename it by date" — search and rename in plain words.
19
+ - "Why is drive C almost full?" — disk usage, big folders, what is safe to delete.
20
+ - "Check the event log for errors since yesterday" / "Read the end of this log" — diagnostics.
21
+ - Work on code: "explain how D:\\Projects\\app is built and run its tests".
22
+
23
+ Everything runs as your Windows user and only inside the folders you allow.
24
+
25
+ ## Plans
26
+
27
+ Free: 10,000 tool calls per month per account, any number of PCs. Pro: $10/month, unlimited.
28
+ Details: https://bridgerator.recondeck.com/pricing
29
+
30
+ ## Install (Windows)
31
+
32
+ 1. In ChatGPT, add the Bridgerator app and sign in with Google (an account is created for you), or sign up at https://bridgerator.recondeck.com.
33
+ 2. On the PC, in PowerShell (no admin rights needed):
34
+ ```powershell
35
+ & ([scriptblock]::Create((irm https://bridgerator.recondeck.com/install.ps1)))
36
+ ```
37
+ It downloads portable Node.js LTS if missing (checksum-verified, no admin), installs this package and runs `bridgerator.cmd setup`.
38
+ 3. A browser tab opens with a code: check that it matches the one in PowerShell and click **Connect this computer**. Done — no key to copy.
39
+
40
+ Update later with the same line (the key and folder settings are kept). Prefer to read first? Download `install.ps1`, read it, then run it.
41
+ Servers or PCs without a browser: create a device key in your account (Add a computer → Advanced) and add `-Token <device key>` to the line.
42
+
43
+ Manual install (PowerShell): `$env:PUPPETEER_SKIP_DOWNLOAD=1; npm install -g bridgerator`, then `bridgerator.cmd setup` (or `bridgerator.cmd setup --token <key>`). The variable skips a ~700 MB Chrome download that only PDF export needs; PDF export fetches a browser itself on first use.
44
+
45
+ Other MCP clients: server URL `https://mcp.bridgerator.recondeck.com/mcp`, OAuth sign-in.
46
+
47
+ ## Access rules — what the assistant can and cannot touch
48
+
49
+ Config: `%APPDATA%\Bridgerator\config.json` (written by `setup`, edit and restart to change).
50
+
51
+ | Key | Meaning | Default |
52
+ |---|---|---|
53
+ | `allowedDirectories` | File tools work only inside these folders (enforced by the engine). Empty = whole disk. | your user folder |
54
+ | `deniedDirectories` | Folders excluded **inside** the allowed ones (enforced by Bridgerator, see below). | `~\.ssh`, `~\AppData` |
55
+ | `blockedCommands` | Commands the engine refuses to run. Leave it out to keep the built-in list. | built-in list (`format`, `diskpart`, `reg`, `net`, `sc`, `runas`, `netsh`, `takeown`, …) |
56
+ | `disabledTools` | Tools never shown to the assistant, e.g. `["start_process","interact_with_process"]` to turn the terminal off. | none |
57
+
58
+ Full file, search and terminal toolset, including ChatGPT widgets (file preview,
59
+ config view). Hidden: `set_config_value` (would let the assistant change its own limits), feedback and
60
+ usage-stats tools.
61
+
62
+ How exclusions work (`src/policy.mjs`): every path argument is resolved (symlinks and junctions
63
+ included) and refused if it lies inside an excluded folder; result lines that mention an excluded
64
+ path (search hits, recursive listings) are removed; `set_config_value` is never exposed, so the
65
+ assistant cannot change these limits.
66
+
67
+ ⚠ **The terminal is not a folder sandbox.** A shell command runs with your Windows user's rights
68
+ and can read anything you can. Bridgerator refuses commands that literally name an excluded folder,
69
+ but that is a courtesy, not a boundary. If folder limits must be strict, add the terminal tools to
70
+ `disabledTools`.
71
+
72
+ The bridge runs as **your** Windows user (hidden scheduled task at logon), never as SYSTEM.
73
+
74
+ Bridgerator gives its bundled engine its **own** config folder (`%APPDATA%\Bridgerator\dc`, see `src/dc-isolate.mjs`): if you already run the same open-source MCP server for Claude or another app, its settings and folder limits are not touched. The engine keeps a local log of recent tool calls in that folder; it never leaves your PC.
75
+
76
+ ## Commands
77
+
78
+ ```
79
+ bridgerator.cmd setup [--token <key> | --pair] [--allow <dir>]... [--deny <dir>]... # pair in the browser (no key yet / --pair) + config + autostart + start
80
+ bridgerator.cmd run # foreground (what autostart runs)
81
+ bridgerator.cmd status # config (token masked) + log tail
82
+ bridgerator.cmd uninstall # stop + remove autostart
83
+ ```
84
+ Log: `%APPDATA%\Bridgerator\bridge.log`. Stopping the bridge cuts access immediately.
85
+
86
+ ## Privacy
87
+
88
+ File contents and command output go to ChatGPT (OpenAI) as tool results; the relay forwards them
89
+ in memory and does not store them. Engine telemetry, remote feature flags and any browser pop-ups are turned off by the bridge.
90
+ Policy: https://bridgerator.recondeck.com/privacy
91
+
92
+ ## Development
93
+
94
+ `npm test` — policy unit tests plus an end-to-end run against the real bundled engine
95
+ (throw-away folders; asserts that an existing config of the same engine stays byte-identical).
96
+
97
+ ## License
98
+
99
+ MIT. The bundled open-source MCP engine is MIT-licensed as well; its copyright notice is kept in
100
+ `LICENSE` and the dependency is listed in `package.json`.
101
+
102
+ ---
103
+
104
+ ### По-русски коротко
105
+ Bridgerator подключает ваш ПК к ChatGPT: файлы, поиск, терминал. Мост только исходящий, входящих
106
+ портов нет. Установка — одна строка PowerShell, затем подтверждение компьютера во вкладке браузера (см. выше).
107
+ Разрешённые папки — `allowedDirectories`, исключения внутри них — `deniedDirectories`
108
+ (исключения обеспечивает сам Bridgerator). Терминал папками не
109
+ ограничивается — для строгих границ выключите его через `disabledTools`.
package/install.ps1 CHANGED
@@ -1,9 +1,11 @@
1
1
  # Bridgerator installer for Windows (PowerShell 5.1+). Source: npm package "bridgerator" (https://www.npmjs.com/package/bridgerator)
2
2
  # Usage (one line, from your Bridgerator account page):
3
- # & ([scriptblock]::Create((irm https://bridgerator.recondeck.com/install.ps1))) -Token <device key>
4
- # Update an installed bridge (keeps its key and folders): the same line without -Token.
3
+ # & ([scriptblock]::Create((irm https://bridgerator.recondeck.com/install.ps1)))
4
+ # A browser tab opens: sign in and confirm this computer — no key to paste. Run again to update
5
+ # (keeps the key and folders). Servers / headless PCs: add -Token <device key> from the account page.
5
6
  # What it does: installs Node.js LTS if missing (portable zip, no admin), installs the bridgerator npm package,
6
- # then runs `bridgerator setup` (config + hidden autostart at logon for THIS user, no admin rights),
7
+ # then runs `bridgerator setup` (pairing in the browser if there is no key yet, config + hidden autostart
8
+ # at logon for THIS user, no admin rights),
7
9
  # then opens the welcome page in the default browser (skip with -NoBrowser).
8
10
  param(
9
11
  [string]$Token,
@@ -13,7 +15,7 @@ param(
13
15
  [switch]$NoBrowser
14
16
  )
15
17
  $ErrorActionPreference = 'Stop'
16
- if (-not $Token -and -not (Test-Path (Join-Path $env:APPDATA 'Bridgerator\config.json'))) { throw 'Add -Token <device key> from your Bridgerator account page (https://bridgerator.recondeck.com/app).' }
18
+ $paired = $Token -or (Test-Path (Join-Path $env:APPDATA 'Bridgerator\config.json'))
17
19
 
18
20
  function Refresh-Path {
19
21
  $env:Path = [Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [Environment]::GetEnvironmentVariable('Path', 'User')
@@ -58,8 +60,8 @@ if ($major -lt 20) { throw "Node.js 20+ required, found $(& node -v). Update fro
58
60
  # The engine's PDF export brings puppeteer, whose postinstall downloads Chrome (~700 MB);
59
61
  # skipped here, PDF export fetches a browser itself the first time it is used.
60
62
  $env:PUPPETEER_SKIP_DOWNLOAD = '1'
61
- Write-Host "Installing bridgerator from $Package ..."
62
- & npm install -g $Package --no-audit --no-fund --loglevel=error
63
+ Write-Host "Installing Bridgerator from $Package - this takes 2-4 minutes, please wait..."
64
+ & npm.cmd install -g $Package --no-audit --no-fund --loglevel=error
63
65
  if ($LASTEXITCODE -ne 0) { throw 'npm install failed' }
64
66
  Refresh-Path
65
67
 
@@ -67,9 +69,10 @@ $setupArgs = @('setup')
67
69
  if ($Token) { $setupArgs += @('--token', $Token) }
68
70
  foreach ($a in $Allow) { $setupArgs += @('--allow', $a) }
69
71
  foreach ($d in $Deny) { $setupArgs += @('--deny', $d) }
70
- & bridgerator @setupArgs
72
+ if (-not $paired) { Write-Host 'Next: a browser tab opens - sign in with the same account you use for Bridgerator and click "Connect this computer".' }
73
+ & bridgerator.cmd @setupArgs
71
74
  if ($LASTEXITCODE -ne 0) { throw 'bridgerator setup failed' }
72
- Write-Host 'Done. Check with: bridgerator status'
75
+ Write-Host 'Done. Bridgerator starts in the background in about 10 seconds. Check any time with: bridgerator.cmd status'
73
76
 
74
77
  # Next steps (connect ChatGPT, example prompts) on the welcome page; skipped for -NoBrowser and
75
78
  # non-interactive sessions (scheduled tasks, remote shells). Never fails the install.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bridgerator",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Connect ChatGPT to your Windows PC: files, search and terminal through an outbound-only bridge with folder limits. No inbound ports, no admin rights.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/bridge.mjs CHANGED
@@ -88,7 +88,7 @@ export function connect(cfg, dc, log = console.log) {
88
88
  setTimeout(() => connect(cfg, dc, log), RECONNECT_MS);
89
89
  };
90
90
  ws.on('close', (code) => {
91
- if (code === 4003) { log('[bridgerator] relay rejected the device token — run `bridgerator setup --token <new token>`'); process.exit(2); }
91
+ if (code === 4003) { log('[bridgerator] relay rejected the device token — run the install command from your account page again (or `bridgerator.cmd setup --pair`)'); process.exit(2); }
92
92
  die(`close ${code}`);
93
93
  });
94
94
  ws.on('error', (e) => { if (!alive) die(`connect: ${e.message}`); });
package/src/cli.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
- // bridgerator setup --token <t> [--relay <wss url>] [--allow <dir>]... [--deny <dir>]... → config + autostart + start
2
+ // bridgerator setup [--token <t> | --pair] [--relay <wss url>] [--allow <dir>]... [--deny <dir>]... → config + autostart + start
3
+ // (no key yet: pairs this PC through the browser — the signed-in owner confirms a code)
3
4
  // bridgerator run | status | uninstall
4
5
  import fs from 'node:fs';
5
6
  import os from 'node:os';
@@ -7,6 +8,7 @@ import path from 'node:path';
7
8
  import { spawn, execFileSync } from 'node:child_process';
8
9
  import { fileURLToPath } from 'node:url';
9
10
  import { startDesktopCommander, connect } from './bridge.mjs';
11
+ import { pair, siteOf } from './pair.mjs';
10
12
 
11
13
  const DEFAULT_RELAY = 'wss://mcp.bridgerator.recondeck.com/link';
12
14
  const TASK = 'Bridgerator';
@@ -82,10 +84,14 @@ async function main() {
82
84
  const cmd = o._[0];
83
85
  if (cmd === 'setup') {
84
86
  const prev = readConfig() || {};
85
- const token = o.token || prev.token;
86
- if (!token || token === true) throw new Error('setup needs --token <device key from your Bridgerator account>');
87
+ if (o.token === true) throw new Error('--token needs the device key from your Bridgerator account');
88
+ const relay = o.relay || prev.relay || DEFAULT_RELAY;
89
+ // --pair: get a new key through the browser even if one is configured (computer removed in the account).
90
+ const paired = !o.token && (o.pair || !prev.token);
91
+ const token = o.token || (paired ? (await pair(siteOf(relay), os.hostname())).token : prev.token);
92
+ if (paired) console.log('Confirmed — this computer is linked to your account.');
87
93
  const cfg = {
88
- relay: o.relay || prev.relay || DEFAULT_RELAY,
94
+ relay,
89
95
  token,
90
96
  allowedDirectories: o.allow || prev.allowedDirectories || [home],
91
97
  deniedDirectories: o.deny || prev.deniedDirectories || [path.join(home, '.ssh'), path.join(home, 'AppData')],
@@ -102,12 +108,12 @@ async function main() {
102
108
  stopRunning();
103
109
  startHidden();
104
110
  console.log(`started in background — log: ${LOG}`);
105
- console.log('Next: in ChatGPT add the connector https://mcp.bridgerator.recondeck.com/mcp and sign in.');
111
+ console.log('Next: in ChatGPT add the Bridgerator app and sign in with the same account.');
106
112
  return;
107
113
  }
108
114
  if (cmd === 'run') {
109
115
  const cfg = readConfig();
110
- if (!cfg) throw new Error(`no config at ${CONFIG} — run: bridgerator setup --token <device key>`);
116
+ if (!cfg) throw new Error(`no config at ${CONFIG} — run: bridgerator setup`);
111
117
  const dc = await startDesktopCommander({ ...cfg, dcConfigDir: path.join(DIR, 'dc') }, log);
112
118
  connect(cfg, dc, log);
113
119
  return;
@@ -125,7 +131,7 @@ async function main() {
125
131
  console.log(`stopped and autostart removed. Config kept at ${CONFIG}. Remove the package: npm rm -g bridgerator`);
126
132
  return;
127
133
  }
128
- console.log('usage: bridgerator setup --token <key> [--allow <dir>]... [--deny <dir>]... | run | status | uninstall');
134
+ console.log('usage: bridgerator setup [--token <key> | --pair] [--allow <dir>]... [--deny <dir>]... | run | status | uninstall');
129
135
  }
130
136
 
131
137
  main().catch((e) => { console.error(`[bridgerator] ${e?.message || e}`); process.exit(1); });
package/src/notice.mjs CHANGED
@@ -11,7 +11,7 @@ const xml = (s) => String(s).replace(/[<>&"']/g, (c) => ({ '<': '&lt;', '>': '&g
11
11
  export function noticeText(n) {
12
12
  const t = {
13
13
  limit: ['Bridgerator: monthly limit reached', `${n.calls} of ${n.limit} tool calls used. ChatGPT can't use this PC until the 1st. Turn on Pro in your account.`],
14
- expired: ['Bridgerator Pro has ended', "You're on Free now (15,000 tool calls a month). Renew Pro in your account."],
14
+ expired: ['Bridgerator Pro has ended', "You're on Free now (10,000 tool calls a month). Renew Pro in your account."],
15
15
  expiring: [`Bridgerator Pro ends on ${day(n.until)}`, 'Pay for 30 more days in your account to keep unlimited calls.'],
16
16
  }[n.kind];
17
17
  return t && { title: t[0], body: t[1], button: 'Open account' };
package/src/pair.mjs ADDED
@@ -0,0 +1,43 @@
1
+ // Key-less setup: ask the relay for a pairing code, open the confirmation page in the browser, wait
2
+ // until the signed-in owner confirms this computer there, and get this PC's device key back.
3
+ import { spawn } from 'node:child_process';
4
+
5
+ // wss://mcp.bridgerator.recondeck.com/link → https://bridgerator.recondeck.com (the site host, where the account pages live).
6
+ export const siteOf = (relay) => { const u = new URL(relay); return `${u.protocol === 'ws:' ? 'http' : 'https'}://${u.host.replace(/^mcp\./, '')}`; };
7
+
8
+ // Opens the URL in the default browser. Only a URL we checked ourselves reaches cmd's `start`.
9
+ export function openBrowser(url) {
10
+ if (!/^https?:\/\/[A-Za-z0-9.:-]+\/pair\?code=[A-Z0-9]{4}-[A-Z0-9]{4}$/.test(url)) return;
11
+ try {
12
+ const p = process.platform === 'win32'
13
+ ? spawn('cmd', ['/c', `start "" "${url}"`], { windowsHide: true, windowsVerbatimArguments: true, stdio: 'ignore', detached: true })
14
+ : spawn(process.platform === 'darwin' ? 'open' : 'xdg-open', [url], { stdio: 'ignore', detached: true });
15
+ p.on('error', () => {});
16
+ p.unref();
17
+ } catch { /* no browser: the URL is printed */ }
18
+ }
19
+
20
+ /** → { token, name } once the owner confirmed; throws when the code expires. */
21
+ export async function pair(site, name, { fetch: f = globalThis.fetch, open = openBrowser, log = console.log, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), everyMs = 3000 } = {}) {
22
+ const post = (path, body) => f(`${site}${path}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), signal: AbortSignal.timeout(20000) });
23
+ const r = await post('/pair/start', { name });
24
+ if (!r.ok) throw new Error(`could not start pairing (HTTP ${r.status}) — try again in a minute`);
25
+ const s = await r.json();
26
+ const url = `${site}/pair?code=${s.code}`;
27
+ if (!/^[A-Z0-9]{4}-[A-Z0-9]{4}$/.test(String(s.code)) || typeof s.poll !== 'string') throw new Error('unexpected answer from the relay');
28
+ log('');
29
+ log(` Confirm this computer in your browser: ${url}`);
30
+ log(` Code: ${s.code} (check that the page shows the same code)`);
31
+ log('');
32
+ open(url);
33
+ log('Waiting for the confirmation (up to 15 minutes)...');
34
+ const until = Date.now() + (Number(s.expiresIn) || 900) * 1000;
35
+ while (Date.now() < until) {
36
+ await sleep(everyMs);
37
+ let p;
38
+ try { p = await post('/pair/poll', { poll: s.poll }); } catch { continue; } // network blip: try again
39
+ if (p.status === 200) { const j = await p.json(); if (j.token) return { token: j.token, name: j.name || name }; }
40
+ if (p.status === 410) break;
41
+ }
42
+ throw new Error('the code expired before it was confirmed — run the command again');
43
+ }