@babtab/relay 0.1.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,30 @@
1
+ # Changelog — @babtab/relay
2
+
3
+ All notable changes to the public npm package `@babtab/relay`
4
+ are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
5
+ and [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [0.1.0] — 2026-09-28
8
+
9
+ Initial public release. Run the local bridge without cloning the monorepo:
10
+
11
+ ```bash
12
+ npx @babtab/relay
13
+ ```
14
+
15
+ ### Added
16
+
17
+ - Local relay: auth (bearer scopes), 6-digit pairing flow, WebSocket
18
+ device routing, Streamable HTTP MCP endpoint (`POST /mcp`).
19
+ - Single-file bundle `dist/relay.cjs` (includes `ws`, zero runtime
20
+ dependencies, Node >= 20).
21
+ - `babtab-relay` bin + `GET /health` for Side Panel checks.
22
+ - Localized READMEs: en, zh-TW, ja, ko, es, pt-BR, hi.
23
+ - SECURITY.md and CHANGELOG.md.
24
+
25
+ ### Notes
26
+
27
+ - Binds `http://127.0.0.1:3000` by default; override with `PORT`.
28
+ - Token file via `BABTAB_TOKEN_FILE` (default `./data/relay-tokens.json`).
29
+ - Native binaries (`babtab-relay-<os>-<arch>`) ship via GitHub Releases,
30
+ not the npm tarball.
package/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/README.es.md ADDED
@@ -0,0 +1,97 @@
1
+ # `@babtab/relay`
2
+
3
+ [English](README.md) | [繁體中文](README.zh-TW.md) | [日本語](README.ja.md) | [한국어](README.ko.md) | [Español](README.es.md) | [Português (BR)](README.pt-BR.md) | [हिन्दी](README.hi.md)
4
+
5
+ [![Versión](https://img.shields.io/npm/v/@babtab/relay.svg)](https://www.npmjs.com/package/@babtab/relay)
6
+ [![Licencia](https://img.shields.io/badge/license-Apache--2.0-green.svg)](LICENSE)
7
+
8
+ El **servidor relay local** para la extensión Babtab de Chrome: conecta tu agente de IA (Cursor / Pi / Claude Code / …) con tu Chrome real.
9
+
10
+ Las extensiones MV3 no pueden escuchar en un puerto, así que este pequeño programa actúa como puente. Se ejecuta en `localhost`, por lo que el tráfico nunca sale de tu máquina. El relay solo reenvía mensajes — **no puede ver el contenido de tus páginas**.
11
+
12
+ ## Cómo se conecta (3 roles)
13
+
14
+ ```text
15
+ Agente de IA (Cursor / Pi …) ←→ Relay (local :3000) ←→ Extensión de Chrome (con Side Panel)
16
+ se conecta vía config MCP solo reenvío + emparejamiento hace el trabajo real en tus pestañas
17
+ ```
18
+
19
+ ## Inicio rápido (sin clonar, sin instalar)
20
+
21
+ ### Paso 1: Instala la extensión de Chrome
22
+
23
+ `chrome://extensions` → activa el **modo de desarrollador** → **Cargar descomprimida** → selecciona la carpeta `dist`.
24
+
25
+ > Cuando se publique en la Chrome Web Store, este paso será "instalar desde la tienda".
26
+
27
+ ### Paso 2: Inicia el relay (elige una, mismo resultado)
28
+
29
+ ```bash
30
+ # A. ¿Tienes Node 20+? Sin instalar nada, solo ejecuta:
31
+ npx @babtab/relay
32
+
33
+ # B. ¿Sin Node? Descarga el binario de tu SO desde GitHub Releases:
34
+ ./babtab-relay-darwin-arm64 # p. ej. macOS Apple Silicon
35
+ ```
36
+
37
+ Sabrás que funciona cuando veas (puerto por defecto `3000`):
38
+
39
+ ```text
40
+ [babtab-relay] listening on http://127.0.0.1:3000
41
+ ```
42
+
43
+ Avanzado (puerto personalizado / ubicación del archivo de tokens):
44
+
45
+ ```bash
46
+ PORT=3001 npx @babtab/relay
47
+ BABTAB_TOKEN_FILE=~/.babtab/relay-tokens.json npx @babtab/relay
48
+ ```
49
+
50
+ > `npx` no instala nada — descarga y ejecuta una sola vez. Cerrar la terminal detiene el relay.
51
+
52
+ ### Paso 3: Conecta la extensión al relay
53
+
54
+ 1. Abre cualquier sitio web en Chrome, haz clic en el icono de Babtab para abrir el **Side Panel**
55
+ 2. La Relay URL ya viene con `ws://127.0.0.1:3000` — déjala así
56
+ 3. Haz clic en **"Save & Connect" (Guardar y conectar)**
57
+
58
+ Esto registra tu Chrome como un dispositivo en el relay.
59
+
60
+ ### Paso 4: Conecta tu agente de IA al relay (ejemplo con Cursor)
61
+
62
+ 1. En la misma página del Side Panel, elige tu agente → haz clic en **"Copy config" (Copiar configuración)**
63
+ 2. Pégalo en `mcpServers` dentro de `~/.cursor/mcp.json` (o en el `.cursor/mcp.json` del proyecto para un solo proyecto):
64
+
65
+ ```json
66
+ {
67
+ "mcpServers": {
68
+ "babtab": {
69
+ "url": "http://127.0.0.1:3000/mcp",
70
+ "headers": { "Authorization": "Bearer el-token-que-acabas-de-obtener" }
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ 3. Cuando el panel muestre `Controlled by: cursor`, ya estás conectado.
77
+
78
+ Una frase para verificar (pídesela a tu agente):
79
+
80
+ > Usa browser_observe para ver las pestañas abiertas en mi Chrome y dime sus títulos y URLs.
81
+
82
+ ## Preguntas frecuentes
83
+
84
+ - **¿"Guardar y conectar" no hace nada?** Revisa primero que la terminal del relay muestre `listening`, y confirma que la URL sea `ws://127.0.0.1:3000` con el puerto correcto.
85
+ - **¿Caducó el código de emparejamiento?** Los códigos duran poco — pulsa "Copiar configuración" de nuevo.
86
+ - **¿Quieres un token nuevo?** Repetir el paso 4 genera un token nuevo; recuerda actualizar `mcp.json`.
87
+ - **¿Agente y Chrome en máquinas distintas?** (Avanzado) Pon el relay en un VPS y cambia la Relay URL del Side Panel a tu `wss://…`. El flujo es el mismo.
88
+
89
+ ## Privacidad
90
+
91
+ - En `localhost` es una conexión puramente local — los paquetes nunca salen de tu computadora.
92
+ - El relay solo reenvía comandos y resultados. Nunca analiza ni almacena el contenido de las páginas.
93
+ - El Side Panel puede **pausar / tomar el control / desconectar** en cualquier momento — el humano siempre tiene el control final.
94
+
95
+ ## Desarrolladores
96
+
97
+ Este repo solo contiene artefactos de release (un bundle ofuscado + binarios), no el código fuente de desarrollo. Abre issues y discusiones aquí mismo.
package/README.hi.md ADDED
@@ -0,0 +1,97 @@
1
+ # `@babtab/relay`
2
+
3
+ [English](README.md) | [繁體中文](README.zh-TW.md) | [日本語](README.ja.md) | [한국어](README.ko.md) | [Español](README.es.md) | [Português (BR)](README.pt-BR.md) | [हिन्दी](README.hi.md)
4
+
5
+ [![संस्करण](https://img.shields.io/npm/v/@babtab/relay.svg)](https://www.npmjs.com/package/@babtab/relay)
6
+ [![लाइसेंस](https://img.shields.io/badge/license-Apache--2.0-green.svg)](LICENSE)
7
+
8
+ Babtab Chrome एक्सटेंशन के लिए **लोकल relay सर्वर**: यह आपके AI एजेंट (Cursor / Pi / Claude Code / …) को आपके असली Chrome से जोड़ता है।
9
+
10
+ MV3 एक्सटेंशन स्वयं किसी पोर्ट पर listen नहीं कर सकते, इसलिए यह छोटा प्रोग्राम bridge का काम करता है। यह `localhost` पर चलता है, इसलिए ट्रैफ़िक आपकी मशीन से बाहर कभी नहीं जाता। Relay सिर्फ़ संदेश आगे बढ़ाता है — **यह आपकी पेज सामग्री नहीं देख सकता**।
11
+
12
+ ## यह कैसे जुड़ता है (3 भूमिकाएँ)
13
+
14
+ ```text
15
+ AI एजेंट (Cursor / Pi …) ←→ Relay (लोकल :3000) ←→ Chrome एक्सटेंशन (Side Panel सहित)
16
+ MCP कॉन्फ़िग से जुड़ता है सिर्फ़ फ़ॉरवर्डिंग + पेयरिंग आपके टैब में असली काम करता है
17
+ ```
18
+
19
+ ## त्वरित शुरुआत (clone नहीं, install नहीं)
20
+
21
+ ### चरण 1: Chrome एक्सटेंशन इंस्टॉल करें
22
+
23
+ `chrome://extensions` → **Developer mode** चालू करें → **Load unpacked** → `dist` फ़ोल्डर चुनें।
24
+
25
+ > Chrome Web Store पर प्रकाशित होने के बाद यह चरण "स्टोर से इंस्टॉल करें" हो जाएगा।
26
+
27
+ ### चरण 2: Relay शुरू करें (कोई एक चुनें, परिणाम समान)
28
+
29
+ ```bash
30
+ # A. Node 20+ है? इंस्टॉल किए बिना सीधे चलाएँ:
31
+ npx @babtab/relay
32
+
33
+ # B. Node नहीं है? GitHub Releases से अपने OS की binary डाउनलोड करें:
34
+ ./babtab-relay-darwin-arm64 # उदा. macOS Apple Silicon
35
+ ```
36
+
37
+ जब यह दिखे तो समझें चल पड़ा (डिफ़ॉल्ट पोर्ट `3000`):
38
+
39
+ ```text
40
+ [babtab-relay] listening on http://127.0.0.1:3000
41
+ ```
42
+
43
+ उन्नत (कस्टम पोर्ट / टोकन फ़ाइल स्थान):
44
+
45
+ ```bash
46
+ PORT=3001 npx @babtab/relay
47
+ BABTAB_TOKEN_FILE=~/.babtab/relay-tokens.json npx @babtab/relay
48
+ ```
49
+
50
+ > `npx` कुछ इंस्टॉल नहीं करता — यह एक बार डाउनलोड करके चलाता है। टर्मिनल बंद करते ही relay रुक जाता है।
51
+
52
+ ### चरण 3: एक्सटेंशन को relay से जोड़ें
53
+
54
+ 1. Chrome में कोई भी वेबसाइट खोलें, Babtab टूलबार आइकन पर क्लिक करके **Side Panel** खोलें
55
+ 2. Relay URL में पहले से `ws://127.0.0.1:3000` भरा है — वैसे ही रहने दें
56
+ 3. "Save & Connect" पर क्लिक करें
57
+
58
+ इससे आपका Chrome relay पर एक device के रूप में पंजीकृत हो जाता है।
59
+
60
+ ### चरण 4: AI एजेंट को relay से जोड़ें (Cursor उदाहरण सहित)
61
+
62
+ 1. Side Panel के उसी पेज पर अपना एजेंट चुनें → "Copy config" पर क्लिक करें
63
+ 2. `~/.cursor/mcp.json` के `mcpServers` में paste करें (सिर्फ़ एक प्रोजेक्ट हेतु हो तो उस प्रोजेक्ट की `.cursor/mcp.json` में):
64
+
65
+ ```json
66
+ {
67
+ "mcpServers": {
68
+ "babtab": {
69
+ "url": "http://127.0.0.1:3000/mcp",
70
+ "headers": { "Authorization": "Bearer अभी-मिला-टोकन" }
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ 3. जब पैनल में `Controlled by: cursor` दिखे, तो कनेक्शन हो गया।
77
+
78
+ सत्यापन हेतु एक वाक्य (अपने एजेंट से कहें):
79
+
80
+ > browser_observe से मेरे Chrome में खुले टैब देखो, फिर उनके title और URL बताओ।
81
+
82
+ ## अक्सर पूछे जाने वाले प्रश्न
83
+
84
+ - **"Save & Connect" दबाने पर कुछ नहीं होता?** पहले relay टर्मिनल में `listening` देखें, फिर पुष्टि करें कि URL `ws://127.0.0.1:3000` है और पोर्ट मेल खाता है।
85
+ - **पेयरिंग कोड समाप्त हो गया?** कोड थोड़ी देर ही चलते हैं — "Copy config" दोबारा दबाएँ।
86
+ - **नया टोकन चाहिए?** चरण 4 दोहराने पर नया टोकन मिलता है; `mcp.json` अपडेट करना न भूलें।
87
+ - **एजेंट और Chrome अलग मशीनों पर हैं?** (उन्नत) Relay को VPS पर रखें और Side Panel की Relay URL को अपने `wss://…` पर बदल दें। प्रक्रिया वही रहती है।
88
+
89
+ ## गोपनीयता
90
+
91
+ - `localhost` पर यह पूरी तरह लोकल कनेक्शन है — पैकेट आपके कंप्यूटर से बाहर कभी नहीं जाते।
92
+ - Relay सिर्फ़ कमांड और परिणाम आगे बढ़ाता है। यह पेज सामग्री को कभी parse या store नहीं करता।
93
+ - Side Panel कभी भी **Pause / Take Over / Disconnect** कर सकता है — अंतिम नियंत्रण हमेशा इंसान के पास रहता है।
94
+
95
+ ## डेवलपर्स
96
+
97
+ इस repo में सिर्फ़ release artifacts हैं (एक obfuscated bundle + binaries), development source नहीं। Issues और चर्चाएँ यहीं खोलें।
package/README.ja.md ADDED
@@ -0,0 +1,97 @@
1
+ # `@babtab/relay`
2
+
3
+ [English](README.md) | [繁體中文](README.zh-TW.md) | [日本語](README.ja.md) | [한국어](README.ko.md) | [Español](README.es.md) | [Português (BR)](README.pt-BR.md) | [हिन्दी](README.hi.md)
4
+
5
+ [![バージョン](https://img.shields.io/npm/v/@babtab/relay.svg)](https://www.npmjs.com/package/@babtab/relay)
6
+ [![ライセンス](https://img.shields.io/badge/license-Apache--2.0-green.svg)](LICENSE)
7
+
8
+ Babtab Chrome 拡張機能のための**ローカル中継サーバー**です。あなたの AI エージェント(Cursor / Pi / Claude Code / …)を、実際の Chrome につなぎます。
9
+
10
+ MV3 の拡張機能は自分でポートを listen できないため、この小さなプログラムが橋渡しをします。`localhost` 上で動作するので、通信が PC の外に出ることはありません。Relay は転送だけを行い、**ページの内容を見ることはできません**。
11
+
12
+ ## 接続方法(3つの役割)
13
+
14
+ ```text
15
+ AI エージェント(Cursor / Pi …) ←→ Relay(ローカル :3000) ←→ Chrome 拡張機能(Side Panel 付き)
16
+ MCP 設定で接続 転送とペアリングのみ タブ上で実際に操作する
17
+ ```
18
+
19
+ ## クイックスタート(clone も install も不要)
20
+
21
+ ### ステップ 1:Chrome 拡張機能をインストール
22
+
23
+ `chrome://extensions` → **デベロッパーモード**を有効化 → **パッケージ化されていない拡張機能を読み込む** → `dist` フォルダを選択。
24
+
25
+ > Chrome ウェブストア公開後は、この手順は「ストアからインストール」に変わります。
26
+
27
+ ### ステップ 2:Relay を起動する(どちらか一方、結果は同じ)
28
+
29
+ ```bash
30
+ # A. Node 20+ がある場合:インストール不要、そのまま実行
31
+ npx @babtab/relay
32
+
33
+ # B. Node を入れたくない場合:GitHub Releases から OS に対応する実行ファイルをダウンロード
34
+ ./babtab-relay-darwin-arm64 # 例:macOS Apple Silicon
35
+ ```
36
+
37
+ 次のメッセージが表示されたら起動成功です(デフォルトポート `3000`):
38
+
39
+ ```text
40
+ [babtab-relay] listening on http://127.0.0.1:3000
41
+ ```
42
+
43
+ 応用(ポート変更・トークン保存先の指定):
44
+
45
+ ```bash
46
+ PORT=3001 npx @babtab/relay
47
+ BABTAB_TOKEN_FILE=~/.babtab/relay-tokens.json npx @babtab/relay
48
+ ```
49
+
50
+ > `npx` はインストールしません。「ダウンロードして一度だけ実行」するもので、ターミナルを閉じれば Relay は停止します。
51
+
52
+ ### ステップ 3:拡張機能を Relay に接続する
53
+
54
+ 1. Chrome で任意のサイトを開き、ツールバーの Babtab アイコンをクリックして **Side Panel** を開く
55
+ 2. Relay URL にはデフォルトで `ws://127.0.0.1:3000` が入力済みなので、そのまま
56
+ 3. **"Save & Connect"(保存して接続)**をクリック
57
+
58
+ この手順で、あなたの Chrome が Relay 上のデバイスとして登録されます。
59
+
60
+ ### ステップ 4:AI エージェントを Relay に接続する(Cursor の例)
61
+
62
+ 1. Side Panel の同じページでエージェントを選択 → **"Copy config"(設定をコピー)**をクリック
63
+ 2. `~/.cursor/mcp.json` の `mcpServers` に貼り付け(単一プロジェクト専用にする場合は当該プロジェクトの `.cursor/mcp.json`):
64
+
65
+ ```json
66
+ {
67
+ "mcpServers": {
68
+ "babtab": {
69
+ "url": "http://127.0.0.1:3000/mcp",
70
+ "headers": { "Authorization": "Bearer 取得したトークン" }
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ 3. パネルに `Controlled by: cursor` と表示されたら接続完了です。
77
+
78
+ 動作確認の一言(エージェントに依頼):
79
+
80
+ > browser_observe で今 Chrome で開いているタブを確認して、タイトルと URL を教えて
81
+
82
+ ## よくある質問
83
+
84
+ - **"Save & Connect"を押しても反応がない?** まず Relay のターミナルに `listening` と表示されているか確認し、URL が `ws://127.0.0.1:3000` でポートが一致しているか確認してください。
85
+ - **ペアリングコードの有効期限が切れた?** コードの有効期間は短いので、"Copy config"をもう一度押してください。
86
+ - **トークンを変えたい?** ステップ 4 を再実行すると新しいトークンが発行されます。`mcp.json` の更新をお忘れなく。
87
+ - **エージェントと Chrome が別マシン?**(応用)Relay を VPS 上に置き、Side Panel の Relay URL を `wss://…` に変更するだけです。手順は同じです。
88
+
89
+ ## プライバシー
90
+
91
+ - `localhost` 上では完全なローカル接続であり、パケットが PC の外に出ることはありません。
92
+ - Relay はコマンドと結果を転送するだけで、ページ内容の解析・保存は一切行いません。
93
+ - Side Panel からいつでも **Pause(一時停止)/ Take Over(操作の引き継ぎ)/ 断線**できます。最終的な制御権は常に人間にあります。
94
+
95
+ ## 開発者向け
96
+
97
+ このリポジトリにはリリース成果物(難読化済みの単一バンドル+実行ファイル)のみが含まれており、開発ソースは含まれません。Issue やディスカッションはこのリポジトリに直接お願いします。
package/README.ko.md ADDED
@@ -0,0 +1,97 @@
1
+ # `@babtab/relay`
2
+
3
+ [English](README.md) | [繁體中文](README.zh-TW.md) | [日本語](README.ja.md) | [한국어](README.ko.md) | [Español](README.es.md) | [Português (BR)](README.pt-BR.md) | [हिन्दी](README.hi.md)
4
+
5
+ [![버전](https://img.shields.io/npm/v/@babtab/relay.svg)](https://www.npmjs.com/package/@babtab/relay)
6
+ [![라이선스](https://img.shields.io/badge/license-Apache--2.0-green.svg)](LICENSE)
7
+
8
+ Babtab Chrome 확장 프로그램을 위한 **로컬 중계 서버**입니다. AI 에이전트(Cursor / Pi / Claude Code / …)를 실제 Chrome에 연결해 줍니다.
9
+
10
+ MV3 확장 프로그램은 스스로 포트를 listen 할 수 없기 때문에, 이 작은 프로그램이 다리 역할을 합니다. `localhost`에서 동작하므로 트래픽이 컴퓨터 밖으로 나가지 않습니다. Relay는 전달만 담당하며, **페이지 내용을 볼 수 없습니다**.
11
+
12
+ ## 연결 방식 (3가지 역할)
13
+
14
+ ```text
15
+ AI 에이전트(Cursor / Pi …) ←→ Relay(로컬 :3000) ←→ Chrome 확장 프로그램(Side Panel 포함)
16
+ MCP 설정으로 연결 전달 + 페어링만 담당 탭에서 실제로 동작하는 주체
17
+ ```
18
+
19
+ ## 빠른 시작 (clone도 install도 불필요)
20
+
21
+ ### 1단계: Chrome 확장 프로그램 설치
22
+
23
+ `chrome://extensions` → **개발자 모드** 사용 → **압축해제된 확장 프로그램 로드** → `dist` 폴더 선택.
24
+
25
+ > Chrome 웹스토어에 게시되면 이 단계는 "스토어에서 설치"로 바뀝니다.
26
+
27
+ ### 2단계: Relay 실행 (둘 중 하나, 결과는 동일)
28
+
29
+ ```bash
30
+ # A. Node 20+가 있는 경우: 설치 없이 바로 실행
31
+ npx @babtab/relay
32
+
33
+ # B. Node를 설치하고 싶지 않은 경우: GitHub Releases에서 OS에 맞는 실행 파일 다운로드
34
+ ./babtab-relay-darwin-arm64 # 예: macOS Apple Silicon
35
+ ```
36
+
37
+ 다음 메시지가 보이면 실행 성공입니다(기본 포트 `3000`):
38
+
39
+ ```text
40
+ [babtab-relay] listening on http://127.0.0.1:3000
41
+ ```
42
+
43
+ 고급 사용법(포트 변경, 토큰 저장 위치 지정):
44
+
45
+ ```bash
46
+ PORT=3001 npx @babtab/relay
47
+ BABTAB_TOKEN_FILE=~/.babtab/relay-tokens.json npx @babtab/relay
48
+ ```
49
+
50
+ > `npx`는 설치가 아닙니다. "다운로드해서 한 번 실행"하는 것으로, 터미널을 닫으면 Relay가 종료됩니다.
51
+
52
+ ### 3단계: 확장 프로그램을 Relay에 연결
53
+
54
+ 1. Chrome에서 아무 웹사이트나 열고, 툴바의 Babtab 아이콘을 클릭해 **Side Panel** 열기
55
+ 2. Relay URL에는 기본값 `ws://127.0.0.1:3000`이 이미 입력되어 있으니 그대로 두기
56
+ 3. **"Save & Connect" (저장 후 연결)** 클릭
57
+
58
+ 이 단계에서 내 Chrome이 Relay의 디바이스 하나로 등록됩니다.
59
+
60
+ ### 4단계: AI 에이전트를 Relay에 연결 (Cursor 예시)
61
+
62
+ 1. Side Panel 같은 페이지에서 에이전트 선택 → **"Copy config" (설정 복사)** 클릭
63
+ 2. `~/.cursor/mcp.json`의 `mcpServers`에 붙여넣기(단일 프로젝트 전용으로 쓰려면 해당 프로젝트의 `.cursor/mcp.json`):
64
+
65
+ ```json
66
+ {
67
+ "mcpServers": {
68
+ "babtab": {
69
+ "url": "http://127.0.0.1:3000/mcp",
70
+ "headers": { "Authorization": "Bearer 발급받은-토큰" }
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ 3. 패널에 `Controlled by: cursor`가 표시되면 연결 완료.
77
+
78
+ 동작 확인용 한 마디(에이전트에게 요청):
79
+
80
+ > browser_observe로 지금 Chrome에 열려 있는 탭을 확인한 뒤, 제목과 URL을 알려줘
81
+
82
+ ## 자주 묻는 질문
83
+
84
+ - **"Save & Connect"을 눌러도 반응이 없어요.** 먼저 Relay 터미널에 `listening`이 표시되는지 확인하고, URL이 `ws://127.0.0.1:3000`인지, 포트가 일치하는지 확인하세요.
85
+ - **페어링 코드가 만료됐어요.** 코드 유효 시간이 짧으니 "Copy config"을 다시 눌러주세요.
86
+ - **토큰을 바꾸고 싶어요.** 4단계를 다시 실행하면 새 토큰이 발급됩니다. `mcp.json` 업데이트를 잊지 마세요.
87
+ - **에이전트와 Chrome이 서로 다른 컴퓨터에 있어요.** (고급) Relay를 VPS에 올리고 Side Panel의 Relay URL을 `wss://…`로 바꾸면 됩니다. 과정은 동일합니다.
88
+
89
+ ## 개인정보 보호
90
+
91
+ - `localhost`에서는 완전한 로컬 연결이라 패킷이 컴퓨터 밖으로 나가지 않습니다.
92
+ - Relay는 명령과 결과 전달만 하며, 페이지 내용을 파싱하거나 저장하지 않습니다.
93
+ - Side Panel에서 언제든 **Pause(일시정지) / Take Over(제어권 가져오기) / 연결 끊기**가 가능합니다. 최종 제어권은 항상 사람에게 있습니다.
94
+
95
+ ## 개발자 안내
96
+
97
+ 이 저장소에는 릴리스 산출물(난독화된 단일 번들 + 실행 파일)만 포함되며, 개발 소스는 포함되지 않습니다. Issue와 토론은 이 저장소에 직접 올려주세요.
package/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # `@babtab/relay`
2
+
3
+ [English](README.md) | [繁體中文](README.zh-TW.md) | [日本語](README.ja.md) | [한국어](README.ko.md) | [Español](README.es.md) | [Português (BR)](README.pt-BR.md) | [हिन्दी](README.hi.md)
4
+
5
+ [![Version](https://img.shields.io/npm/v/@babtab/relay.svg)](https://www.npmjs.com/package/@babtab/relay)
6
+ [![License](https://img.shields.io/badge/license-Apache--2.0-green.svg)](LICENSE)
7
+
8
+ The **local relay server** for the Babtab Chrome extension: it connects your AI agent (Cursor / Pi / Claude Code / …) to your real Chrome.
9
+
10
+ MV3 extensions cannot listen on a port, so this tiny program acts as the bridge. It runs on `localhost`, so traffic never leaves your machine. The relay only forwards messages — **it cannot see your page content**.
11
+
12
+ ## How it connects (3 roles)
13
+
14
+ ```text
15
+ AI agent (Cursor / Pi …) ←→ Relay (local :3000) ←→ Chrome extension (with Side Panel)
16
+ connects via MCP config forwarding + pairing only does the real work in your tabs
17
+ ```
18
+
19
+ ## Quick start (no clone, no install)
20
+
21
+ ### Step 1: Install the Chrome extension
22
+
23
+ `chrome://extensions` → enable **Developer mode** → **Load unpacked** → select the `dist` folder.
24
+
25
+ > Once published on the Chrome Web Store, this step becomes "install from the store".
26
+
27
+ ### Step 2: Start the relay (pick one, same result)
28
+
29
+ ```bash
30
+ # A. Have Node 20+? No install needed, just run:
31
+ npx @babtab/relay
32
+
33
+ # B. No Node? Download the binary for your OS from GitHub Releases:
34
+ ./babtab-relay-darwin-arm64 # e.g. macOS Apple Silicon
35
+ ```
36
+
37
+ You're up when you see (default port `3000`):
38
+
39
+ ```text
40
+ [babtab-relay] listening on http://127.0.0.1:3000
41
+ ```
42
+
43
+ Advanced (custom port / token file location):
44
+
45
+ ```bash
46
+ PORT=3001 npx @babtab/relay
47
+ BABTAB_TOKEN_FILE=~/.babtab/relay-tokens.json npx @babtab/relay
48
+ ```
49
+
50
+ > `npx` doesn't install anything — it downloads and runs once. Closing the terminal stops the relay.
51
+
52
+ ### Step 3: Connect the extension to the relay
53
+
54
+ 1. Open any website in Chrome, click the Babtab toolbar icon to open the **Side Panel**
55
+ 2. Relay URL is pre-filled with `ws://127.0.0.1:3000` — leave it
56
+ 3. Click **"Save & Connect"**
57
+
58
+ This registers your Chrome as a device on the relay.
59
+
60
+ ### Step 4: Connect your AI agent to the relay (Cursor as example)
61
+
62
+ 1. On the same Side Panel page, pick your agent → click **"Copy config"**
63
+ 2. Paste into `mcpServers` in `~/.cursor/mcp.json` (or the project's `.cursor/mcp.json` for single-project use):
64
+
65
+ ```json
66
+ {
67
+ "mcpServers": {
68
+ "babtab": {
69
+ "url": "http://127.0.0.1:3000/mcp",
70
+ "headers": { "Authorization": "Bearer the-token-you-just-got" }
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ 3. When the panel shows `Controlled by: cursor`, you're connected.
77
+
78
+ One sentence to verify (ask your agent):
79
+
80
+ > Use browser_observe to look at the tabs open in my Chrome, then tell me their titles and URLs.
81
+
82
+ ## FAQ
83
+
84
+ - **"Save & Connect" does nothing?** Check the relay terminal for `listening` first, then confirm the URL is `ws://127.0.0.1:3000` with a matching port.
85
+ - **Pairing code expired?** Codes are short-lived — just hit "Copy config" again.
86
+ - **Want a new token?** Re-running step 4 issues a new token; remember to update `mcp.json`.
87
+ - **Agent and Chrome on different machines?** (Advanced) Put the relay on a VPS and change the Side Panel's Relay URL to your `wss://…`. The flow stays the same.
88
+
89
+ ## Privacy
90
+
91
+ - On `localhost` it's a purely local connection — packets never leave your computer.
92
+ - The relay only forwards commands and results. It never parses or stores page content.
93
+ - The Side Panel can **Pause / Take Over / Disconnect** at any time — the human always has final control.
94
+
95
+ ## Developers
96
+
97
+ This repo only contains release artifacts (one obfuscated bundle + binaries), not the development source. Please file issues and discussions right here.
@@ -0,0 +1,97 @@
1
+ # `@babtab/relay`
2
+
3
+ [English](README.md) | [繁體中文](README.zh-TW.md) | [日本語](README.ja.md) | [한국어](README.ko.md) | [Español](README.es.md) | [Português (BR)](README.pt-BR.md) | [हिन्दी](README.hi.md)
4
+
5
+ [![Versão](https://img.shields.io/npm/v/@babtab/relay.svg)](https://www.npmjs.com/package/@babtab/relay)
6
+ [![Licença](https://img.shields.io/badge/license-Apache--2.0-green.svg)](LICENSE)
7
+
8
+ O **servidor relay local** para a extensão Babtab do Chrome: conecta seu agente de IA (Cursor / Pi / Claude Code / …) ao seu Chrome de verdade.
9
+
10
+ Extensões MV3 não conseguem escutar em uma porta, então este programinha age como ponte. Roda em `localhost`, por isso o tráfego nunca sai da sua máquina. O relay só encaminha mensagens — **não consegue ver o conteúdo das suas páginas**.
11
+
12
+ ## Como se conecta (3 papéis)
13
+
14
+ ```text
15
+ Agente de IA (Cursor / Pi …) ←→ Relay (local :3000) ←→ Extensão do Chrome (com Side Panel)
16
+ conecta via config MCP só encaminhamento + pareamento faz o trabalho real nas suas abas
17
+ ```
18
+
19
+ ## Início rápido (sem clonar, sem instalar)
20
+
21
+ ### Passo 1: Instale a extensão do Chrome
22
+
23
+ `chrome://extensions` → ative o **modo do desenvolvedor** → **Carregar sem compactação** → selecione a pasta `dist`.
24
+
25
+ > Quando for publicada na Chrome Web Store, este passo vira "instalar pela loja".
26
+
27
+ ### Passo 2: Inicie o relay (escolha um, mesmo resultado)
28
+
29
+ ```bash
30
+ # A. Tem Node 20+? Sem instalar nada, só rode:
31
+ npx @babtab/relay
32
+
33
+ # B. Sem Node? Baixe o binário do seu SO em GitHub Releases:
34
+ ./babtab-relay-darwin-arm64 # ex. macOS Apple Silicon
35
+ ```
36
+
37
+ Está no ar quando você vir (porta padrão `3000`):
38
+
39
+ ```text
40
+ [babtab-relay] listening on http://127.0.0.1:3000
41
+ ```
42
+
43
+ Avançado (porta personalizada / local do arquivo de tokens):
44
+
45
+ ```bash
46
+ PORT=3001 npx @babtab/relay
47
+ BABTAB_TOKEN_FILE=~/.babtab/relay-tokens.json npx @babtab/relay
48
+ ```
49
+
50
+ > `npx` não instala nada — baixa e executa uma única vez. Fechar o terminal para o relay.
51
+
52
+ ### Passo 3: Conecte a extensão ao relay
53
+
54
+ 1. Abra qualquer site no Chrome, clique no ícone do Babtab para abrir o **Side Panel**
55
+ 2. A Relay URL já vem preenchida com `ws://127.0.0.1:3000` — deixe assim
56
+ 3. Clique em **"Save & Connect" (Salvar e conectar)**
57
+
58
+ Isso registra seu Chrome como um dispositivo no relay.
59
+
60
+ ### Passo 4: Conecte seu agente de IA ao relay (exemplo com Cursor)
61
+
62
+ 1. Na mesma página do Side Panel, escolha seu agente → clique em **"Copy config" (Copiar configuração)**
63
+ 2. Cole em `mcpServers` dentro de `~/.cursor/mcp.json` (ou no `.cursor/mcp.json` do projeto para uso em um só projeto):
64
+
65
+ ```json
66
+ {
67
+ "mcpServers": {
68
+ "babtab": {
69
+ "url": "http://127.0.0.1:3000/mcp",
70
+ "headers": { "Authorization": "Bearer o-token-que-voce-acabou-de-obter" }
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ 3. Quando o painel mostrar `Controlled by: cursor`, está conectado.
77
+
78
+ Uma frase para verificar (peça ao seu agente):
79
+
80
+ > Use o browser_observe para ver as abas abertas no meu Chrome e me diga os títulos e URLs delas.
81
+
82
+ ## Perguntas frequentes
83
+
84
+ - **"Salvar e conectar" não faz nada?** Confira primeiro se o terminal do relay mostra `listening`, depois confirme que a URL é `ws://127.0.0.1:3000` com a porta certa.
85
+ - **Código de pareamento expirou?** Os códigos duram pouco — clique em "Copiar configuração" de novo.
86
+ - **Quer um token novo?** Repetir o passo 4 gera um token novo; lembre-se de atualizar o `mcp.json`.
87
+ - **Agente e Chrome em máquinas diferentes?** (Avançado) Coloque o relay num VPS e troque a Relay URL do Side Panel para o seu `wss://…`. O fluxo é o mesmo.
88
+
89
+ ## Privacidade
90
+
91
+ - Em `localhost` é uma conexão puramente local — os pacotes nunca saem do seu computador.
92
+ - O relay só encaminha comandos e resultados. Nunca analisa nem armazena o conteúdo das páginas.
93
+ - O Side Panel pode **pausar / assumir o controle / desconectar** a qualquer momento — o humano sempre tem o controle final.
94
+
95
+ ## Desenvolvedores
96
+
97
+ Este repo contém apenas artefatos de release (um bundle ofuscado + binários), não o código-fonte de desenvolvimento. Abra issues e discussões aqui mesmo.
@@ -0,0 +1,97 @@
1
+ # `@babtab/relay`
2
+
3
+ [English](README.md) | [繁體中文](README.zh-TW.md) | [日本語](README.ja.md) | [한국어](README.ko.md) | [Español](README.es.md) | [Português (BR)](README.pt-BR.md) | [हिन्दी](README.hi.md)
4
+
5
+ [![版本](https://img.shields.io/npm/v/@babtab/relay.svg)](https://www.npmjs.com/package/@babtab/relay)
6
+ [![授權](https://img.shields.io/badge/license-Apache--2.0-green.svg)](LICENSE)
7
+
8
+ Babtab Chrome 擴充功能的**本機中轉伺服器**:把你的 AI Agent(Cursor / Pi / Claude Code / …)接到你真正的 Chrome 上。
9
+
10
+ MV3 擴充功能自己不能 listen port,所以需要這支小程式當橋樑。跑在 `localhost`,流量不出你的電腦;Relay 只負責轉發,**看不到你的頁面內容**。
11
+
12
+ ## 連線方式(3 個角色)
13
+
14
+ ```text
15
+ AI Agent(Cursor / Pi …) ←→ Relay(本機 :3000) ←→ Chrome 擴充功能(含 Side Panel)
16
+ 用 MCP 設定連過來 只做轉發與配對 真正在分頁上動手的人
17
+ ```
18
+
19
+ ## 使用者快速開始(不用 clone、不用 install)
20
+
21
+ ### 步驟 1:安裝 Chrome 擴充功能
22
+
23
+ `chrome://extensions` → 開啟**開發者模式** → **載入未封裝項目** → 選 `dist` 資料夾。
24
+
25
+ > 上架 Chrome 商店後,這步會變成「去商店按安裝」。
26
+
27
+ ### 步驟 2:啟動 Relay(二選一,效果一樣)
28
+
29
+ ```bash
30
+ # A. 有 Node 20+ 的人:免安裝,直接跑
31
+ npx @babtab/relay
32
+
33
+ # B. 不想裝 Node 的人:去 GitHub Release 下載對應系統的執行檔
34
+ ./babtab-relay-darwin-arm64 # macOS Apple Silicon 為例
35
+ ```
36
+
37
+ 看到以下訊息就是起來了(預設 port `3000`):
38
+
39
+ ```text
40
+ [babtab-relay] listening on http://127.0.0.1:3000
41
+ ```
42
+
43
+ 進階用法(換 port、指定 token 存檔位置):
44
+
45
+ ```bash
46
+ PORT=3001 npx @babtab/relay
47
+ BABTAB_TOKEN_FILE=~/.babtab/relay-tokens.json npx @babtab/relay
48
+ ```
49
+
50
+ > `npx` 不是安裝,是「下載來跑一次」,關掉終端機就停了。
51
+
52
+ ### 步驟 3:擴充功能連上 Relay
53
+
54
+ 1. 在 Chrome 開任意網站,點工具列的 Babtab 圖示開啟 **Side Panel**
55
+ 2. Relay URL 預設已填好 `ws://127.0.0.1:3000`,不用改
56
+ 3. 按 **"Save & Connect"**
57
+
58
+ 這一步是把你的 Chrome 註冊成 Relay 上的一台 device。
59
+
60
+ ### 步驟 4:AI Agent 連上 Relay(以 Cursor 為例)
61
+
62
+ 1. 在 Side Panel 同一頁選你的 Agent → 按 **"Copy config"**
63
+ 2. 貼到 `~/.cursor/mcp.json` 的 `mcpServers`(只想給單一專案用,就放該專案的 `.cursor/mcp.json`):
64
+
65
+ ```json
66
+ {
67
+ "mcpServers": {
68
+ "babtab": {
69
+ "url": "http://127.0.0.1:3000/mcp",
70
+ "headers": { "Authorization": "Bearer 剛拿到的token" }
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ 3. Panel 出現 `Controlled by: cursor` 就通了。
77
+
78
+ 驗證一句話(在 Agent 裡下):
79
+
80
+ > 用 browser_observe 看我現在 Chrome 開的分頁,然後告訴我標題和網址
81
+
82
+ ## 常見問題
83
+
84
+ - **按 "Save & Connect"沒反應?** 先看 Relay 終端機有沒有 `listening`,再確認 URL 是 `ws://127.0.0.1:3000` 且 port 跟 Relay 一致。
85
+ - **配對碼過期?** 配對碼只有短暫有效,超時就重新按一次 "Copy config"。
86
+ - **想換 token?** 重跑一次步驟 4 會產生新 token,記得同步更新 `mcp.json`。
87
+ - **Agent 跟 Chrome 不在同一台電腦?**(進階)把 Relay 放到 VPS 上,Side Panel 的 Relay URL 改成你的 `wss://…` 即可,流程不變。
88
+
89
+ ## 隱私說明
90
+
91
+ - 跑在 `localhost` 就是純本地連線,封包不出電腦。
92
+ - Relay 只轉發指令與結果,不解析、不儲存頁面內容。
93
+ - Side Panel 隨時可以 **Pause / Take Over / 斷線**,人類永遠有最終控制權。
94
+
95
+ ## 開發者
96
+
97
+ 本倉僅含發布產物(混淆後的單一 bundle + 執行檔),不含開發源碼。Issue 與討論請直接開在本倉。
package/SECURITY.md ADDED
@@ -0,0 +1,55 @@
1
+ # Security Policy — @babtab/relay
2
+
3
+ Local-first relay. No hosted service, no telemetry, no account.
4
+
5
+ ## Supported versions
6
+
7
+ | Version | Supported |
8
+ |---|---|
9
+ | 0.1.x | Yes |
10
+
11
+ Older versions: upgrade via `npx @babtab/relay@latest`.
12
+
13
+ ## What the relay sees (and never sees)
14
+
15
+ The relay routes **metadata only**: device/client IDs, method names
16
+ (`browser_observe`, `browser_click`, …), pairing codes, auth tokens.
17
+
18
+ It **never** receives, stores, or logs:
19
+
20
+ - page DOM, screenshots, or extracted page content,
21
+ - browser credentials, cookies, or API keys belonging to other tools,
22
+ - fill/select input values (the extension redacts them before display,
23
+ and the relay never persists request bodies).
24
+
25
+ Token store (`BABTAB_TOKEN_FILE`, default `./data/relay-tokens.json`)
26
+ holds random bearer tokens + scope + expiry only. Treat that file like
27
+ a password: `chmod 600`, never commit it, never send it to anyone.
28
+
29
+ ## Running it safely
30
+
31
+ - Bind is localhost by default (`http://127.0.0.1:3000`). Do not expose
32
+ it to a LAN / public IP unless you understand that anyone who can
33
+ reach the port can attempt pairing.
34
+ - Pairing codes are 6-digit, single-use, and expire quickly. Approve
35
+ only codes you just requested; reject anything you don't recognize.
36
+ - HIGH-risk browser actions (submit, pay, delete, publish, password
37
+ fill) always require explicit approval in the extension Side Panel —
38
+ the relay cannot bypass that gate.
39
+ - Prefer `npx @babtab/relay` (no install) or the GitHub Release binary
40
+ over cloning the monorepo.
41
+
42
+ ## Reporting a vulnerability
43
+
44
+ **Do not open a public issue for a suspected vulnerability.**
45
+
46
+ Email the maintainer or open a
47
+ [private security advisory](https://github.com/Poseidoncode/babtab-relay/security/advisories/new)
48
+ with:
49
+
50
+ 1. affected version (`npx @babtab/relay --version` or tarball hash),
51
+ 2. minimal reproduction steps,
52
+ 3. impact assessment (what an attacker could / could not obtain).
53
+
54
+ We aim to acknowledge within 72 hours and will credit reporters
55
+ unless anonymity is requested.
package/dist/relay.cjs ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ "use strict";var mr=Object.create;var Qe=Object.defineProperty;var gr=Object.getOwnPropertyDescriptor;var yr=Object.getOwnPropertyNames;var Sr=Object.getPrototypeOf,xr=Object.prototype.hasOwnProperty;var S=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports);var Er=(t,e,r,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of yr(e))!xr.call(t,i)&&i!==r&&Qe(t,i,{get:()=>e[i],enumerable:!(s=gr(e,i))||s.enumerable});return t};var P=(t,e,r)=>(r=t!=null?mr(Sr(t)):{},Er(e||!t||!t.__esModule?Qe(r,"default",{value:t,enumerable:!0}):r,t));var T=S((oi,rt)=>{"use strict";var et=["nodebuffer","arraybuffer","fragments"],tt=typeof Blob<"u";tt&&et.push("blob");rt.exports={BINARY_TYPES:et,CLOSE_TIMEOUT:3e4,EMPTY_BUFFER:Buffer.alloc(0),GUID:"258EAFA5-E914-47DA-95CA-C5AB0DC85B11",hasBlob:tt,kForOnEventAttribute:Symbol("kIsForOnEventAttribute"),kListener:Symbol("kListener"),kStatusCode:Symbol("status-code"),kWebSocket:Symbol("websocket"),NOOP:()=>{}}});var X=S((ai,ne)=>{"use strict";var{EMPTY_BUFFER:br}=T(),ve=Buffer[Symbol.species];function vr(t,e){if(t.length===0)return br;if(t.length===1)return t[0];let r=Buffer.allocUnsafe(e),s=0;for(let i=0;i<t.length;i++){let n=t[i];r.set(n,s),s+=n.length}return s<e?new ve(r.buffer,r.byteOffset,s):r}function st(t,e,r,s,i){for(let n=0;n<i;n++)r[s+n]=t[n]^e[n&3]}function it(t,e){for(let r=0;r<t.length;r++)t[r]^=e[r&3]}function wr(t){return t.length===t.buffer.byteLength?t.buffer:t.buffer.slice(t.byteOffset,t.byteOffset+t.length)}function we(t){if(we.readOnly=!0,Buffer.isBuffer(t))return t;let e;return t instanceof ArrayBuffer?e=new ve(t):ArrayBuffer.isView(t)?e=new ve(t.buffer,t.byteOffset,t.byteLength):(e=Buffer.from(t),we.readOnly=!1),e}ne.exports={concat:vr,mask:st,toArrayBuffer:wr,toBuffer:we,unmask:it};if(!process.env.WS_NO_BUFFER_UTIL)try{let t=require("bufferutil");ne.exports.mask=function(e,r,s,i,n){n<48?st(e,r,s,i,n):t.mask(e,r,s,i,n)},ne.exports.unmask=function(e,r){e.length<32?it(e,r):t.unmask(e,r)}}catch{}});var at=S((ci,ot)=>{"use strict";var nt=Symbol("kDone"),Te=Symbol("kRun"),Oe=class{constructor(e){this[nt]=()=>{this.pending--,this[Te]()},this.concurrency=e||1/0,this.jobs=[],this.pending=0}add(e){this.jobs.push(e),this[Te]()}[Te](){if(this.pending!==this.concurrency&&this.jobs.length){let e=this.jobs.shift();this.pending++,e(this[nt])}}};ot.exports=Oe});var G=S((li,ht)=>{"use strict";var Z=require("zlib"),ct=X(),Tr=at(),{kStatusCode:lt}=T(),Or=Buffer[Symbol.species],kr=Buffer.from([0,0,255,255]),ae=Symbol("permessage-deflate"),O=Symbol("total-length"),j=Symbol("callback"),L=Symbol("buffers"),$=Symbol("error"),oe,ke=class{constructor(e){if(this._options=e||{},this._threshold=this._options.threshold!==void 0?this._options.threshold:1024,this._maxPayload=this._options.maxPayload|0,this._isServer=!!this._options.isServer,this._deflate=null,this._inflate=null,this.params=null,!oe){let r=this._options.concurrencyLimit!==void 0?this._options.concurrencyLimit:10;oe=new Tr(r)}}static get extensionName(){return"permessage-deflate"}offer(){let e={};return this._options.serverNoContextTakeover&&(e.server_no_context_takeover=!0),this._options.clientNoContextTakeover&&(e.client_no_context_takeover=!0),this._options.serverMaxWindowBits&&(e.server_max_window_bits=this._options.serverMaxWindowBits),this._options.clientMaxWindowBits?e.client_max_window_bits=this._options.clientMaxWindowBits:this._options.clientMaxWindowBits==null&&(e.client_max_window_bits=!0),e}accept(e){return e=this.normalizeParams(e),this.params=this._isServer?this.acceptAsServer(e):this.acceptAsClient(e),this.params}cleanup(){if(this._inflate&&(this._inflate.close(),this._inflate=null),this._deflate){let e=this._deflate[j];this._deflate.close(),this._deflate=null,e&&e(new Error("The deflate stream was closed while data was being processed"))}}acceptAsServer(e){let r=this._options,s=e.find(i=>!(r.serverNoContextTakeover===!1&&i.server_no_context_takeover||i.server_max_window_bits&&(r.serverMaxWindowBits===!1||typeof r.serverMaxWindowBits=="number"&&r.serverMaxWindowBits>i.server_max_window_bits)||typeof r.clientMaxWindowBits=="number"&&(typeof i.client_max_window_bits=="number"?r.clientMaxWindowBits>i.client_max_window_bits:!i.client_max_window_bits)));if(!s)throw new Error("None of the extension offers can be accepted");return r.serverNoContextTakeover&&(s.server_no_context_takeover=!0),r.clientNoContextTakeover&&(s.client_no_context_takeover=!0),typeof r.serverMaxWindowBits=="number"&&(s.server_max_window_bits=r.serverMaxWindowBits),typeof r.clientMaxWindowBits=="number"?s.client_max_window_bits=r.clientMaxWindowBits:(s.client_max_window_bits===!0||r.clientMaxWindowBits===!1)&&delete s.client_max_window_bits,s}acceptAsClient(e){let r=e[0];if(this._options.clientNoContextTakeover===!1&&r.client_no_context_takeover)throw new Error('Unexpected parameter "client_no_context_takeover"');if(!r.client_max_window_bits)typeof this._options.clientMaxWindowBits=="number"&&(r.client_max_window_bits=this._options.clientMaxWindowBits);else if(this._options.clientMaxWindowBits===!1||typeof this._options.clientMaxWindowBits=="number"&&r.client_max_window_bits>this._options.clientMaxWindowBits)throw new Error('Unexpected or invalid parameter "client_max_window_bits"');return r}normalizeParams(e){return e.forEach(r=>{Object.keys(r).forEach(s=>{let i=r[s];if(i.length>1)throw new Error(`Parameter "${s}" must have only a single value`);if(i=i[0],s==="client_max_window_bits"){if(i!==!0){let n=+i;if(!Number.isInteger(n)||n<8||n>15)throw new TypeError(`Invalid value for parameter "${s}": ${i}`);i=n}else if(!this._isServer)throw new TypeError(`Invalid value for parameter "${s}": ${i}`)}else if(s==="server_max_window_bits"){let n=+i;if(!Number.isInteger(n)||n<8||n>15)throw new TypeError(`Invalid value for parameter "${s}": ${i}`);i=n}else if(s==="client_no_context_takeover"||s==="server_no_context_takeover"){if(i!==!0)throw new TypeError(`Invalid value for parameter "${s}": ${i}`)}else throw new Error(`Unknown parameter "${s}"`);r[s]=i})}),e}decompress(e,r,s){oe.add(i=>{this._decompress(e,r,(n,o)=>{i(),s(n,o)})})}compress(e,r,s){oe.add(i=>{this._compress(e,r,(n,o)=>{i(),s(n,o)})})}_decompress(e,r,s){let i=this._isServer?"client":"server";if(!this._inflate){let n=`${i}_max_window_bits`,o=typeof this.params[n]!="number"?Z.Z_DEFAULT_WINDOWBITS:this.params[n];this._inflate=Z.createInflateRaw({...this._options.zlibInflateOptions,windowBits:o}),this._inflate[ae]=this,this._inflate[O]=0,this._inflate[L]=[],this._inflate.on("error",Cr),this._inflate.on("data",ft)}this._inflate[j]=s,this._inflate.write(e),r&&this._inflate.write(kr),this._inflate.flush(()=>{let n=this._inflate[$];if(n){this._inflate.close(),this._inflate=null,s(n);return}let o=ct.concat(this._inflate[L],this._inflate[O]);this._inflate._readableState.endEmitted?(this._inflate.close(),this._inflate=null):(this._inflate[O]=0,this._inflate[L]=[],r&&this.params[`${i}_no_context_takeover`]&&this._inflate.reset()),s(null,o)})}_compress(e,r,s){let i=this._isServer?"server":"client";if(!this._deflate){let n=`${i}_max_window_bits`,o=typeof this.params[n]!="number"?Z.Z_DEFAULT_WINDOWBITS:this.params[n];this._deflate=Z.createDeflateRaw({...this._options.zlibDeflateOptions,windowBits:o}),this._deflate[O]=0,this._deflate[L]=[],this._deflate.on("data",Nr)}this._deflate[j]=s,this._deflate.write(e),this._deflate.flush(Z.Z_SYNC_FLUSH,()=>{if(!this._deflate)return;let n=ct.concat(this._deflate[L],this._deflate[O]);r&&(n=new Or(n.buffer,n.byteOffset,n.length-4)),this._deflate[j]=null,this._deflate[O]=0,this._deflate[L]=[],r&&this.params[`${i}_no_context_takeover`]&&this._deflate.reset(),s(null,n)})}};ht.exports=ke;function Nr(t){this[L].push(t),this[O]+=t.length}function ft(t){if(this[O]+=t.length,this[ae]._maxPayload<1||this[O]<=this[ae]._maxPayload){this[L].push(t);return}this[$]=new RangeError("Max payload size exceeded"),this[$].code="WS_ERR_UNSUPPORTED_MESSAGE_LENGTH",this[$][lt]=1009,this.removeListener("data",ft),this.reset()}function Cr(t){if(this[ae]._inflate=null,this[$]){this[j](this[$]);return}t[lt]=1007,this[j](t)}});var q=S((fi,ce)=>{"use strict";var{isUtf8:ut}=require("buffer"),{hasBlob:Pr}=T(),Lr=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,1,1,1,1,0,0,1,1,0,1,1,0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,1,0];function Rr(t){return t>=1e3&&t<=1014&&t!==1004&&t!==1005&&t!==1006||t>=3e3&&t<=4999}function Ne(t){let e=t.length,r=0;for(;r<e;)if(!(t[r]&128))r++;else if((t[r]&224)===192){if(r+1===e||(t[r+1]&192)!==128||(t[r]&254)===192)return!1;r+=2}else if((t[r]&240)===224){if(r+2>=e||(t[r+1]&192)!==128||(t[r+2]&192)!==128||t[r]===224&&(t[r+1]&224)===128||t[r]===237&&(t[r+1]&224)===160)return!1;r+=3}else if((t[r]&248)===240){if(r+3>=e||(t[r+1]&192)!==128||(t[r+2]&192)!==128||(t[r+3]&192)!==128||t[r]===240&&(t[r+1]&240)===128||t[r]===244&&t[r+1]>143||t[r]>244)return!1;r+=4}else return!1;return!0}function Br(t){return Pr&&typeof t=="object"&&typeof t.arrayBuffer=="function"&&typeof t.type=="string"&&typeof t.stream=="function"&&(t[Symbol.toStringTag]==="Blob"||t[Symbol.toStringTag]==="File")}ce.exports={isBlob:Br,isValidStatusCode:Rr,isValidUTF8:Ne,tokenChars:Lr};if(ut)ce.exports.isValidUTF8=function(t){return t.length<24?Ne(t):ut(t)};else if(!process.env.WS_NO_UTF_8_VALIDATE)try{let t=require("utf-8-validate");ce.exports.isValidUTF8=function(e){return e.length<32?Ne(e):t(e)}}catch{}});var Be=S((hi,St)=>{"use strict";var{Writable:Ir}=require("stream"),dt=G(),{BINARY_TYPES:Dr,EMPTY_BUFFER:pt,kStatusCode:Ur,kWebSocket:Ar}=T(),{concat:Ce,toArrayBuffer:Mr,unmask:Fr}=X(),{isValidStatusCode:Wr,isValidUTF8:_t}=q(),le=Buffer[Symbol.species],E=0,mt=1,gt=2,yt=3,Pe=4,Le=5,fe=6,Re=class extends Ir{constructor(e={}){super(),this._allowSynchronousEvents=e.allowSynchronousEvents!==void 0?e.allowSynchronousEvents:!0,this._binaryType=e.binaryType||Dr[0],this._extensions=e.extensions||{},this._isServer=!!e.isServer,this._maxBufferedChunks=e.maxBufferedChunks|0,this._maxFragments=e.maxFragments|0,this._maxPayload=e.maxPayload|0,this._skipUTF8Validation=!!e.skipUTF8Validation,this[Ar]=void 0,this._bufferedBytes=0,this._buffers=[],this._compressed=!1,this._payloadLength=0,this._mask=void 0,this._fragmented=0,this._masked=!1,this._fin=!1,this._opcode=0,this._totalPayloadLength=0,this._messageLength=0,this._numFragments=0,this._fragments=[],this._errored=!1,this._loop=!1,this._state=E}_write(e,r,s){if(this._opcode===8&&this._state==E)return s();if(this._maxBufferedChunks>0&&this._buffers.length>=this._maxBufferedChunks){s(this.createError(RangeError,"Too many buffered chunks",!1,1008,"WS_ERR_TOO_MANY_BUFFERED_PARTS"));return}this._bufferedBytes+=e.length,this._buffers.push(e),this.startLoop(s)}consume(e){if(this._bufferedBytes-=e,e===this._buffers[0].length)return this._buffers.shift();if(e<this._buffers[0].length){let s=this._buffers[0];return this._buffers[0]=new le(s.buffer,s.byteOffset+e,s.length-e),new le(s.buffer,s.byteOffset,e)}let r=Buffer.allocUnsafe(e);do{let s=this._buffers[0],i=r.length-e;e>=s.length?r.set(this._buffers.shift(),i):(r.set(new Uint8Array(s.buffer,s.byteOffset,e),i),this._buffers[0]=new le(s.buffer,s.byteOffset+e,s.length-e)),e-=s.length}while(e>0);return r}startLoop(e){this._loop=!0;do switch(this._state){case E:this.getInfo(e);break;case mt:this.getPayloadLength16(e);break;case gt:this.getPayloadLength64(e);break;case yt:this.getMask();break;case Pe:this.getData(e);break;case Le:case fe:this._loop=!1;return}while(this._loop);this._errored||e()}getInfo(e){if(this._bufferedBytes<2){this._loop=!1;return}let r=this.consume(2);if(r[0]&48){let i=this.createError(RangeError,"RSV2 and RSV3 must be clear",!0,1002,"WS_ERR_UNEXPECTED_RSV_2_3");e(i);return}let s=(r[0]&64)===64;if(s&&!this._extensions[dt.extensionName]){let i=this.createError(RangeError,"RSV1 must be clear",!0,1002,"WS_ERR_UNEXPECTED_RSV_1");e(i);return}if(this._fin=(r[0]&128)===128,this._opcode=r[0]&15,this._payloadLength=r[1]&127,this._opcode===0){if(s){let i=this.createError(RangeError,"RSV1 must be clear",!0,1002,"WS_ERR_UNEXPECTED_RSV_1");e(i);return}if(!this._fragmented){let i=this.createError(RangeError,"invalid opcode 0",!0,1002,"WS_ERR_INVALID_OPCODE");e(i);return}this._opcode=this._fragmented}else if(this._opcode===1||this._opcode===2){if(this._fragmented){let i=this.createError(RangeError,`invalid opcode ${this._opcode}`,!0,1002,"WS_ERR_INVALID_OPCODE");e(i);return}this._compressed=s}else if(this._opcode>7&&this._opcode<11){if(!this._fin){let i=this.createError(RangeError,"FIN must be set",!0,1002,"WS_ERR_EXPECTED_FIN");e(i);return}if(s){let i=this.createError(RangeError,"RSV1 must be clear",!0,1002,"WS_ERR_UNEXPECTED_RSV_1");e(i);return}if(this._payloadLength>125||this._opcode===8&&this._payloadLength===1){let i=this.createError(RangeError,`invalid payload length ${this._payloadLength}`,!0,1002,"WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH");e(i);return}}else{let i=this.createError(RangeError,`invalid opcode ${this._opcode}`,!0,1002,"WS_ERR_INVALID_OPCODE");e(i);return}if(!this._fin&&!this._fragmented&&(this._fragmented=this._opcode),this._masked=(r[1]&128)===128,this._isServer){if(!this._masked){let i=this.createError(RangeError,"MASK must be set",!0,1002,"WS_ERR_EXPECTED_MASK");e(i);return}}else if(this._masked){let i=this.createError(RangeError,"MASK must be clear",!0,1002,"WS_ERR_UNEXPECTED_MASK");e(i);return}this._payloadLength===126?this._state=mt:this._payloadLength===127?this._state=gt:this.haveLength(e)}getPayloadLength16(e){if(this._bufferedBytes<2){this._loop=!1;return}this._payloadLength=this.consume(2).readUInt16BE(0),this.haveLength(e)}getPayloadLength64(e){if(this._bufferedBytes<8){this._loop=!1;return}let r=this.consume(8),s=r.readUInt32BE(0);if(s>Math.pow(2,21)-1){let i=this.createError(RangeError,"Unsupported WebSocket frame: payload length > 2^53 - 1",!1,1009,"WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH");e(i);return}this._payloadLength=s*Math.pow(2,32)+r.readUInt32BE(4),this.haveLength(e)}haveLength(e){if(this._payloadLength&&this._opcode<8&&(this._totalPayloadLength+=this._payloadLength,this._totalPayloadLength>this._maxPayload&&this._maxPayload>0)){let r=this.createError(RangeError,"Max payload size exceeded",!1,1009,"WS_ERR_UNSUPPORTED_MESSAGE_LENGTH");e(r);return}this._masked?this._state=yt:this._state=Pe}getMask(){if(this._bufferedBytes<4){this._loop=!1;return}this._mask=this.consume(4),this._state=Pe}getData(e){let r=pt;if(this._payloadLength){if(this._bufferedBytes<this._payloadLength){this._loop=!1;return}r=this.consume(this._payloadLength),this._masked&&this._mask[0]|this._mask[1]|this._mask[2]|this._mask[3]&&Fr(r,this._mask)}if(this._opcode>7){this.controlMessage(r,e);return}if(this._maxFragments>0&&++this._numFragments>this._maxFragments){let s=this.createError(RangeError,"Too many message fragments",!1,1008,"WS_ERR_TOO_MANY_BUFFERED_PARTS");e(s);return}if(this._compressed){this._state=Le,this.decompress(r,e);return}r.length&&(this._messageLength=this._totalPayloadLength,this._fragments.push(r)),this.dataMessage(e)}decompress(e,r){this._extensions[dt.extensionName].decompress(e,this._fin,(i,n)=>{if(i)return r(i);if(n.length){if(this._messageLength+=n.length,this._messageLength>this._maxPayload&&this._maxPayload>0){let o=this.createError(RangeError,"Max payload size exceeded",!1,1009,"WS_ERR_UNSUPPORTED_MESSAGE_LENGTH");r(o);return}this._fragments.push(n)}this.dataMessage(r),this._state===E&&this.startLoop(r)})}dataMessage(e){if(!this._fin){this._state=E;return}let r=this._messageLength,s=this._fragments;if(this._totalPayloadLength=0,this._messageLength=0,this._fragmented=0,this._numFragments=0,this._fragments=[],this._opcode===2){let i;this._binaryType==="nodebuffer"?i=Ce(s,r):this._binaryType==="arraybuffer"?i=Mr(Ce(s,r)):this._binaryType==="blob"?i=new Blob(s):i=s,this._allowSynchronousEvents?(this.emit("message",i,!0),this._state=E):(this._state=fe,setImmediate(()=>{this.emit("message",i,!0),this._state=E,this.startLoop(e)}))}else{let i=Ce(s,r);if(!this._skipUTF8Validation&&!_t(i)){let n=this.createError(Error,"invalid UTF-8 sequence",!0,1007,"WS_ERR_INVALID_UTF8");e(n);return}this._state===Le||this._allowSynchronousEvents?(this.emit("message",i,!1),this._state=E):(this._state=fe,setImmediate(()=>{this.emit("message",i,!1),this._state=E,this.startLoop(e)}))}}controlMessage(e,r){if(this._opcode===8){if(e.length===0)this._loop=!1,this.emit("conclude",1005,pt),this.end();else{let s=e.readUInt16BE(0);if(!Wr(s)){let n=this.createError(RangeError,`invalid status code ${s}`,!0,1002,"WS_ERR_INVALID_CLOSE_CODE");r(n);return}let i=new le(e.buffer,e.byteOffset+2,e.length-2);if(!this._skipUTF8Validation&&!_t(i)){let n=this.createError(Error,"invalid UTF-8 sequence",!0,1007,"WS_ERR_INVALID_UTF8");r(n);return}this._loop=!1,this.emit("conclude",s,i),this.end()}this._state=E;return}this._allowSynchronousEvents?(this.emit(this._opcode===9?"ping":"pong",e),this._state=E):(this._state=fe,setImmediate(()=>{this.emit(this._opcode===9?"ping":"pong",e),this._state=E,this.startLoop(r)}))}createError(e,r,s,i,n){this._loop=!1,this._errored=!0;let o=new e(s?`Invalid WebSocket frame: ${r}`:r);return Error.captureStackTrace(o,this.createError),o.code=n,o[Ur]=i,o}};St.exports=Re});var Ue=S((di,bt)=>{"use strict";var{Duplex:ui}=require("stream"),{randomFillSync:jr}=require("crypto"),{types:{isUint8Array:$r}}=require("util"),xt=G(),{EMPTY_BUFFER:Gr,kWebSocket:qr,NOOP:Vr}=T(),{isBlob:V,isValidStatusCode:Hr}=q(),{mask:Et,toBuffer:I}=X(),b=Symbol("kByteLength"),zr=Buffer.alloc(4),he=8*1024,D,H=he,v=0,Jr=1,Yr=2,Ie=class t{constructor(e,r,s){this._extensions=r||{},s&&(this._generateMask=s,this._maskBuffer=Buffer.alloc(4)),this._socket=e,this._firstFragment=!0,this._compress=!1,this._bufferedBytes=0,this._queue=[],this._state=v,this.onerror=Vr,this[qr]=void 0}static frame(e,r){let s,i=!1,n=2,o=!1;r.mask&&(s=r.maskBuffer||zr,r.generateMask?r.generateMask(s):(H===he&&(D===void 0&&(D=Buffer.alloc(he)),jr(D,0,he),H=0),s[0]=D[H++],s[1]=D[H++],s[2]=D[H++],s[3]=D[H++]),o=(s[0]|s[1]|s[2]|s[3])===0,n=6);let c;typeof e=="string"?(!r.mask||o)&&r[b]!==void 0?c=r[b]:(e=Buffer.from(e),c=e.length):(c=e.length,i=r.mask&&r.readOnly&&!o);let a=c;c>=65536?(n+=8,a=127):c>125&&(n+=2,a=126);let l=Buffer.allocUnsafe(i?c+n:n);return l[0]=r.fin?r.opcode|128:r.opcode,r.rsv1&&(l[0]|=64),l[1]=a,a===126?l.writeUInt16BE(c,2):a===127&&(l[2]=l[3]=0,l.writeUIntBE(c,4,6)),r.mask?(l[1]|=128,l[n-4]=s[0],l[n-3]=s[1],l[n-2]=s[2],l[n-1]=s[3],o?[l,e]:i?(Et(e,s,l,n,c),[l]):(Et(e,s,e,0,c),[l,e])):[l,e]}close(e,r,s,i){let n;if(e===void 0)n=Gr;else{if(typeof e!="number"||!Hr(e))throw new TypeError("First argument must be a valid error code number");if(r===void 0||!r.length)n=Buffer.allocUnsafe(2),n.writeUInt16BE(e,0);else{let c=Buffer.byteLength(r);if(c>123)throw new RangeError("The message must not be greater than 123 bytes");if(n=Buffer.allocUnsafe(2+c),n.writeUInt16BE(e,0),typeof r=="string")n.write(r,2);else if($r(r))n.set(r,2);else throw new TypeError("Second argument must be a string or a Uint8Array")}}let o={[b]:n.length,fin:!0,generateMask:this._generateMask,mask:s,maskBuffer:this._maskBuffer,opcode:8,readOnly:!1,rsv1:!1};this._state!==v?this.enqueue([this.dispatch,n,!1,o,i]):this.sendFrame(t.frame(n,o),i)}ping(e,r,s){let i,n;if(typeof e=="string"?(i=Buffer.byteLength(e),n=!1):V(e)?(i=e.size,n=!1):(e=I(e),i=e.length,n=I.readOnly),i>125)throw new RangeError("The data size must not be greater than 125 bytes");let o={[b]:i,fin:!0,generateMask:this._generateMask,mask:r,maskBuffer:this._maskBuffer,opcode:9,readOnly:n,rsv1:!1};V(e)?this._state!==v?this.enqueue([this.getBlobData,e,!1,o,s]):this.getBlobData(e,!1,o,s):this._state!==v?this.enqueue([this.dispatch,e,!1,o,s]):this.sendFrame(t.frame(e,o),s)}pong(e,r,s){let i,n;if(typeof e=="string"?(i=Buffer.byteLength(e),n=!1):V(e)?(i=e.size,n=!1):(e=I(e),i=e.length,n=I.readOnly),i>125)throw new RangeError("The data size must not be greater than 125 bytes");let o={[b]:i,fin:!0,generateMask:this._generateMask,mask:r,maskBuffer:this._maskBuffer,opcode:10,readOnly:n,rsv1:!1};V(e)?this._state!==v?this.enqueue([this.getBlobData,e,!1,o,s]):this.getBlobData(e,!1,o,s):this._state!==v?this.enqueue([this.dispatch,e,!1,o,s]):this.sendFrame(t.frame(e,o),s)}send(e,r,s){let i=this._extensions[xt.extensionName],n=r.binary?2:1,o=r.compress,c,a;typeof e=="string"?(c=Buffer.byteLength(e),a=!1):V(e)?(c=e.size,a=!1):(e=I(e),c=e.length,a=I.readOnly),this._firstFragment?(this._firstFragment=!1,o&&i&&i.params[i._isServer?"server_no_context_takeover":"client_no_context_takeover"]&&(o=c>=i._threshold),this._compress=o):(o=!1,n=0),r.fin&&(this._firstFragment=!0);let l={[b]:c,fin:r.fin,generateMask:this._generateMask,mask:r.mask,maskBuffer:this._maskBuffer,opcode:n,readOnly:a,rsv1:o};V(e)?this._state!==v?this.enqueue([this.getBlobData,e,this._compress,l,s]):this.getBlobData(e,this._compress,l,s):this._state!==v?this.enqueue([this.dispatch,e,this._compress,l,s]):this.dispatch(e,this._compress,l,s)}getBlobData(e,r,s,i){this._bufferedBytes+=s[b],this._state=Yr,e.arrayBuffer().then(n=>{if(this._socket.destroyed){let c=new Error("The socket was closed while the blob was being read");process.nextTick(De,this,c,i);return}this._bufferedBytes-=s[b];let o=I(n);r?this.dispatch(o,r,s,i):(this._state=v,this.sendFrame(t.frame(o,s),i),this.dequeue())}).catch(n=>{process.nextTick(Kr,this,n,i)})}dispatch(e,r,s,i){if(!r){this.sendFrame(t.frame(e,s),i);return}let n=this._extensions[xt.extensionName];this._bufferedBytes+=s[b],this._state=Jr,n.compress(e,s.fin,(o,c)=>{if(this._socket.destroyed){let a=new Error("The socket was closed while data was being compressed");De(this,a,i);return}this._bufferedBytes-=s[b],this._state=v,s.readOnly=!1,this.sendFrame(t.frame(c,s),i),this.dequeue()})}dequeue(){for(;this._state===v&&this._queue.length;){let e=this._queue.shift();this._bufferedBytes-=e[3][b],Reflect.apply(e[0],this,e.slice(1))}}enqueue(e){this._bufferedBytes+=e[3][b],this._queue.push(e)}sendFrame(e,r){e.length===2?(this._socket.cork(),this._socket.write(e[0]),this._socket.write(e[1],r),this._socket.uncork()):this._socket.write(e[0],r)}};bt.exports=Ie;function De(t,e,r){typeof r=="function"&&r(e);for(let s=0;s<t._queue.length;s++){let i=t._queue[s],n=i[i.length-1];typeof n=="function"&&n(e)}}function Kr(t,e,r){De(t,e,r),t.onerror(e)}});var Lt=S((pi,Pt)=>{"use strict";var{kForOnEventAttribute:Q,kListener:Ae}=T(),vt=Symbol("kCode"),wt=Symbol("kData"),Tt=Symbol("kError"),Ot=Symbol("kMessage"),kt=Symbol("kReason"),z=Symbol("kTarget"),Nt=Symbol("kType"),Ct=Symbol("kWasClean"),k=class{constructor(e){this[z]=null,this[Nt]=e}get target(){return this[z]}get type(){return this[Nt]}};Object.defineProperty(k.prototype,"target",{enumerable:!0});Object.defineProperty(k.prototype,"type",{enumerable:!0});var U=class extends k{constructor(e,r={}){super(e),this[vt]=r.code===void 0?0:r.code,this[kt]=r.reason===void 0?"":r.reason,this[Ct]=r.wasClean===void 0?!1:r.wasClean}get code(){return this[vt]}get reason(){return this[kt]}get wasClean(){return this[Ct]}};Object.defineProperty(U.prototype,"code",{enumerable:!0});Object.defineProperty(U.prototype,"reason",{enumerable:!0});Object.defineProperty(U.prototype,"wasClean",{enumerable:!0});var J=class extends k{constructor(e,r={}){super(e),this[Tt]=r.error===void 0?null:r.error,this[Ot]=r.message===void 0?"":r.message}get error(){return this[Tt]}get message(){return this[Ot]}};Object.defineProperty(J.prototype,"error",{enumerable:!0});Object.defineProperty(J.prototype,"message",{enumerable:!0});var ee=class extends k{constructor(e,r={}){super(e),this[wt]=r.data===void 0?null:r.data}get data(){return this[wt]}};Object.defineProperty(ee.prototype,"data",{enumerable:!0});var Xr={addEventListener(t,e,r={}){for(let i of this.listeners(t))if(!r[Q]&&i[Ae]===e&&!i[Q])return;let s;if(t==="message")s=function(n,o){let c=new ee("message",{data:o?n:n.toString()});c[z]=this,ue(e,this,c)};else if(t==="close")s=function(n,o){let c=new U("close",{code:n,reason:o.toString(),wasClean:this._closeFrameReceived&&this._closeFrameSent});c[z]=this,ue(e,this,c)};else if(t==="error")s=function(n){let o=new J("error",{error:n,message:n.message});o[z]=this,ue(e,this,o)};else if(t==="open")s=function(){let n=new k("open");n[z]=this,ue(e,this,n)};else return;s[Q]=!!r[Q],s[Ae]=e,r.once?this.once(t,s):this.on(t,s)},removeEventListener(t,e){for(let r of this.listeners(t))if(r[Ae]===e&&!r[Q]){this.removeListener(t,r);break}}};Pt.exports={CloseEvent:U,ErrorEvent:J,Event:k,EventTarget:Xr,MessageEvent:ee};function ue(t,e,r){typeof t=="object"&&t.handleEvent?t.handleEvent.call(t,r):t.call(e,r)}});var de=S((_i,Rt)=>{"use strict";var{tokenChars:te}=q();function w(t,e,r){t[e]===void 0?t[e]=[r]:t[e].push(r)}function Zr(t){let e=Object.create(null),r=Object.create(null),s=!1,i=!1,n=!1,o,c,a=-1,l=-1,f=-1,h=0;for(;h<t.length;h++)if(l=t.charCodeAt(h),o===void 0)if(f===-1&&te[l]===1)a===-1&&(a=h);else if(h!==0&&(l===32||l===9))f===-1&&a!==-1&&(f=h);else if(l===59||l===44){if(a===-1)throw new SyntaxError(`Unexpected character at index ${h}`);f===-1&&(f=h);let m=t.slice(a,f);l===44?(w(e,m,r),r=Object.create(null)):o=m,a=f=-1}else throw new SyntaxError(`Unexpected character at index ${h}`);else if(c===void 0)if(f===-1&&te[l]===1)a===-1&&(a=h);else if(l===32||l===9)f===-1&&a!==-1&&(f=h);else if(l===59||l===44){if(a===-1)throw new SyntaxError(`Unexpected character at index ${h}`);f===-1&&(f=h),w(r,t.slice(a,f),!0),l===44&&(w(e,o,r),r=Object.create(null),o=void 0),a=f=-1}else if(l===61&&a!==-1&&f===-1)c=t.slice(a,h),a=f=-1;else throw new SyntaxError(`Unexpected character at index ${h}`);else if(i){if(te[l]!==1)throw new SyntaxError(`Unexpected character at index ${h}`);a===-1?a=h:s||(s=!0),i=!1}else if(n)if(te[l]===1)a===-1&&(a=h);else if(l===34&&a!==-1)n=!1,f=h;else if(l===92)i=!0;else throw new SyntaxError(`Unexpected character at index ${h}`);else if(l===34&&t.charCodeAt(h-1)===61)n=!0;else if(f===-1&&te[l]===1)a===-1&&(a=h);else if(a!==-1&&(l===32||l===9))f===-1&&(f=h);else if(l===59||l===44){if(a===-1)throw new SyntaxError(`Unexpected character at index ${h}`);f===-1&&(f=h);let m=t.slice(a,f);s&&(m=m.replace(/\\/g,""),s=!1),w(r,c,m),l===44&&(w(e,o,r),r=Object.create(null),o=void 0),c=void 0,a=f=-1}else throw new SyntaxError(`Unexpected character at index ${h}`);if(a===-1||n||l===32||l===9)throw new SyntaxError("Unexpected end of input");f===-1&&(f=h);let d=t.slice(a,f);return o===void 0?w(e,d,r):(c===void 0?w(r,d,!0):s?w(r,c,d.replace(/\\/g,"")):w(r,c,d),w(e,o,r)),e}function Qr(t){return Object.keys(t).map(e=>{let r=t[e];return Array.isArray(r)||(r=[r]),r.map(s=>[e].concat(Object.keys(s).map(i=>{let n=s[i];return Array.isArray(n)||(n=[n]),n.map(o=>o===!0?i:`${i}=${o}`).join("; ")})).join("; ")).join(", ")}).join(", ")}Rt.exports={format:Qr,parse:Zr}});var ge=S((yi,qt)=>{"use strict";var es=require("events"),ts=require("https"),rs=require("http"),Dt=require("net"),ss=require("tls"),{randomBytes:is,createHash:ns}=require("crypto"),{Duplex:mi,Readable:gi}=require("stream"),{URL:Me}=require("url"),R=G(),os=Be(),as=Ue(),{isBlob:cs}=q(),{BINARY_TYPES:Bt,CLOSE_TIMEOUT:ls,EMPTY_BUFFER:pe,GUID:fs,kForOnEventAttribute:Fe,kListener:hs,kStatusCode:us,kWebSocket:g,NOOP:Ut}=T(),{EventTarget:{addEventListener:ds,removeEventListener:ps}}=Lt(),{format:_s,parse:ms}=de(),{toBuffer:gs}=X(),At=Symbol("kAborted"),We=[8,13],N=["CONNECTING","OPEN","CLOSING","CLOSED"],ys=/^[!#$%&'*+\-.0-9A-Z^_`|a-z~]+$/,p=class t extends es{constructor(e,r,s){super(),this._binaryType=Bt[0],this._closeCode=1006,this._closeFrameReceived=!1,this._closeFrameSent=!1,this._closeMessage=pe,this._closeTimer=null,this._errorEmitted=!1,this._extensions={},this._paused=!1,this._protocol="",this._readyState=t.CONNECTING,this._receiver=null,this._sender=null,this._socket=null,e!==null?(this._bufferedAmount=0,this._isServer=!1,this._redirects=0,r===void 0?r=[]:Array.isArray(r)||(typeof r=="object"&&r!==null?(s=r,r=[]):r=[r]),Mt(this,e,r,s)):(this._autoPong=s.autoPong,this._closeTimeout=s.closeTimeout,this._isServer=!0)}get binaryType(){return this._binaryType}set binaryType(e){Bt.includes(e)&&(this._binaryType=e,this._receiver&&(this._receiver._binaryType=e))}get bufferedAmount(){return this._socket?this._socket._writableState.length+this._sender._bufferedBytes:this._bufferedAmount}get extensions(){return Object.keys(this._extensions).join()}get isPaused(){return this._paused}get onclose(){return null}get onerror(){return null}get onopen(){return null}get onmessage(){return null}get protocol(){return this._protocol}get readyState(){return this._readyState}get url(){return this._url}setSocket(e,r,s){let i=new os({allowSynchronousEvents:s.allowSynchronousEvents,binaryType:this.binaryType,extensions:this._extensions,isServer:this._isServer,maxBufferedChunks:s.maxBufferedChunks,maxFragments:s.maxFragments,maxPayload:s.maxPayload,skipUTF8Validation:s.skipUTF8Validation}),n=new as(e,this._extensions,s.generateMask);this._receiver=i,this._sender=n,this._socket=e,i[g]=this,n[g]=this,e[g]=this,i.on("conclude",Es),i.on("drain",bs),i.on("error",vs),i.on("message",ws),i.on("ping",Ts),i.on("pong",Os),n.onerror=ks,e.setTimeout&&e.setTimeout(0),e.setNoDelay&&e.setNoDelay(),r.length>0&&e.unshift(r),e.on("close",jt),e.on("data",me),e.on("end",$t),e.on("error",Gt),this._readyState=t.OPEN,this.emit("open")}emitClose(){if(!this._socket){this._readyState=t.CLOSED,this.emit("close",this._closeCode,this._closeMessage);return}this._extensions[R.extensionName]&&this._extensions[R.extensionName].cleanup(),this._receiver.removeAllListeners(),this._readyState=t.CLOSED,this.emit("close",this._closeCode,this._closeMessage)}close(e,r){if(this.readyState!==t.CLOSED){if(this.readyState===t.CONNECTING){x(this,this._req,"WebSocket was closed before the connection was established");return}if(this.readyState===t.CLOSING){this._closeFrameSent&&(this._closeFrameReceived||this._receiver._writableState.errorEmitted)&&this._socket.end();return}this._readyState=t.CLOSING,this._sender.close(e,r,!this._isServer,s=>{s||(this._closeFrameSent=!0,(this._closeFrameReceived||this._receiver._writableState.errorEmitted)&&this._socket.end())}),Wt(this)}}pause(){this.readyState===t.CONNECTING||this.readyState===t.CLOSED||(this._paused=!0,this._socket.pause())}ping(e,r,s){if(this.readyState===t.CONNECTING)throw new Error("WebSocket is not open: readyState 0 (CONNECTING)");if(typeof e=="function"?(s=e,e=r=void 0):typeof r=="function"&&(s=r,r=void 0),typeof e=="number"&&(e=e.toString()),this.readyState!==t.OPEN){je(this,e,s);return}r===void 0&&(r=!this._isServer),this._sender.ping(e||pe,r,s)}pong(e,r,s){if(this.readyState===t.CONNECTING)throw new Error("WebSocket is not open: readyState 0 (CONNECTING)");if(typeof e=="function"?(s=e,e=r=void 0):typeof r=="function"&&(s=r,r=void 0),typeof e=="number"&&(e=e.toString()),this.readyState!==t.OPEN){je(this,e,s);return}r===void 0&&(r=!this._isServer),this._sender.pong(e||pe,r,s)}resume(){this.readyState===t.CONNECTING||this.readyState===t.CLOSED||(this._paused=!1,this._receiver._writableState.needDrain||this._socket.resume())}send(e,r,s){if(this.readyState===t.CONNECTING)throw new Error("WebSocket is not open: readyState 0 (CONNECTING)");if(typeof r=="function"&&(s=r,r={}),typeof e=="number"&&(e=e.toString()),this.readyState!==t.OPEN){je(this,e,s);return}let i={binary:typeof e!="string",mask:!this._isServer,compress:!0,fin:!0,...r};this._extensions[R.extensionName]||(i.compress=!1),this._sender.send(e||pe,i,s)}terminate(){if(this.readyState!==t.CLOSED){if(this.readyState===t.CONNECTING){x(this,this._req,"WebSocket was closed before the connection was established");return}this._socket&&(this._readyState=t.CLOSING,this._socket.destroy())}}};Object.defineProperty(p,"CONNECTING",{enumerable:!0,value:N.indexOf("CONNECTING")});Object.defineProperty(p.prototype,"CONNECTING",{enumerable:!0,value:N.indexOf("CONNECTING")});Object.defineProperty(p,"OPEN",{enumerable:!0,value:N.indexOf("OPEN")});Object.defineProperty(p.prototype,"OPEN",{enumerable:!0,value:N.indexOf("OPEN")});Object.defineProperty(p,"CLOSING",{enumerable:!0,value:N.indexOf("CLOSING")});Object.defineProperty(p.prototype,"CLOSING",{enumerable:!0,value:N.indexOf("CLOSING")});Object.defineProperty(p,"CLOSED",{enumerable:!0,value:N.indexOf("CLOSED")});Object.defineProperty(p.prototype,"CLOSED",{enumerable:!0,value:N.indexOf("CLOSED")});["binaryType","bufferedAmount","extensions","isPaused","protocol","readyState","url"].forEach(t=>{Object.defineProperty(p.prototype,t,{enumerable:!0})});["open","error","close","message"].forEach(t=>{Object.defineProperty(p.prototype,`on${t}`,{enumerable:!0,get(){for(let e of this.listeners(t))if(e[Fe])return e[hs];return null},set(e){for(let r of this.listeners(t))if(r[Fe]){this.removeListener(t,r);break}typeof e=="function"&&this.addEventListener(t,e,{[Fe]:!0})}})});p.prototype.addEventListener=ds;p.prototype.removeEventListener=ps;qt.exports=p;function Mt(t,e,r,s){let i={allowSynchronousEvents:!0,autoPong:!0,closeTimeout:ls,protocolVersion:We[1],maxBufferedChunks:262144,maxFragments:16384,maxPayload:104857600,skipUTF8Validation:!1,perMessageDeflate:!0,followRedirects:!1,maxRedirects:10,...s,socketPath:void 0,hostname:void 0,protocol:void 0,timeout:void 0,method:"GET",host:void 0,path:void 0,port:void 0};if(t._autoPong=i.autoPong,t._closeTimeout=i.closeTimeout,!We.includes(i.protocolVersion))throw new RangeError(`Unsupported protocol version: ${i.protocolVersion} (supported versions: ${We.join(", ")})`);let n;if(e instanceof Me)n=e;else try{n=new Me(e)}catch{throw new SyntaxError(`Invalid URL: ${e}`)}n.protocol==="http:"?n.protocol="ws:":n.protocol==="https:"&&(n.protocol="wss:"),t._url=n.href;let o=n.protocol==="wss:",c=n.protocol==="ws+unix:",a;if(n.protocol!=="ws:"&&!o&&!c?a=`The URL's protocol must be one of "ws:", "wss:", "http:", "https:", or "ws+unix:"`:c&&!n.pathname?a="The URL's pathname is empty":n.hash&&(a="The URL contains a fragment identifier"),a){let u=new SyntaxError(a);if(t._redirects===0)throw u;_e(t,u);return}let l=o?443:80,f=is(16).toString("base64"),h=o?ts.request:rs.request,d=new Set,m;if(i.createConnection=i.createConnection||(o?xs:Ss),i.defaultPort=i.defaultPort||l,i.port=n.port||l,i.host=n.hostname.startsWith("[")?n.hostname.slice(1,-1):n.hostname,i.headers={...i.headers,"Sec-WebSocket-Version":i.protocolVersion,"Sec-WebSocket-Key":f,Connection:"Upgrade",Upgrade:"websocket"},i.path=n.pathname+n.search,i.timeout=i.handshakeTimeout,i.perMessageDeflate&&(m=new R({...i.perMessageDeflate,isServer:!1,maxPayload:i.maxPayload}),i.headers["Sec-WebSocket-Extensions"]=_s({[R.extensionName]:m.offer()})),r.length){for(let u of r){if(typeof u!="string"||!ys.test(u)||d.has(u))throw new SyntaxError("An invalid or duplicated subprotocol was specified");d.add(u)}i.headers["Sec-WebSocket-Protocol"]=r.join(",")}if(i.origin&&(i.protocolVersion<13?i.headers["Sec-WebSocket-Origin"]=i.origin:i.headers.Origin=i.origin),(n.username||n.password)&&(i.auth=`${n.username}:${n.password}`),c){let u=i.path.split(":");i.socketPath=u[0],i.path=u[1]}let _;if(i.followRedirects){if(t._redirects===0){t._originalIpc=c,t._originalSecure=o,t._originalHostOrSocketPath=c?i.socketPath:n.host;let u=s&&s.headers;if(s={...s,headers:{}},u)for(let[y,F]of Object.entries(u))s.headers[y.toLowerCase()]=F}else if(t.listenerCount("redirect")===0){let u=c?t._originalIpc?i.socketPath===t._originalHostOrSocketPath:!1:t._originalIpc?!1:n.host===t._originalHostOrSocketPath;(!u||t._originalSecure&&!o)&&(delete i.headers.authorization,delete i.headers.cookie,u||delete i.headers.host,i.auth=void 0)}i.auth&&!s.headers.authorization&&(s.headers.authorization="Basic "+Buffer.from(i.auth).toString("base64")),_=t._req=h(i),t._redirects&&t.emit("redirect",t.url,_)}else _=t._req=h(i);i.timeout&&_.on("timeout",()=>{x(t,_,"Opening handshake has timed out")}),_.on("error",u=>{_===null||_[At]||(_=t._req=null,_e(t,u))}),_.on("response",u=>{let y=u.headers.location,F=u.statusCode;if(y&&i.followRedirects&&F>=300&&F<400){if(++t._redirects>i.maxRedirects){x(t,_,"Maximum redirects exceeded");return}_.abort();let Y;try{Y=new Me(y,e)}catch{let W=new SyntaxError(`Invalid URL: ${y}`);_e(t,W);return}Mt(t,Y,r,s)}else t.emit("unexpected-response",_,u)||x(t,_,`Unexpected server response: ${u.statusCode}`)}),_.on("upgrade",(u,y,F)=>{if(t.emit("upgrade",u),t.readyState!==p.CONNECTING)return;_=t._req=null;let Y=u.headers.upgrade;if(Y===void 0||Y.toLowerCase()!=="websocket"){x(t,y,"Invalid Upgrade header");return}let Ke=ns("sha1").update(f+fs).digest("base64");if(u.headers["sec-websocket-accept"]!==Ke){x(t,y,"Invalid Sec-WebSocket-Accept header");return}let W=u.headers["sec-websocket-protocol"],K;if(W!==void 0?d.size?d.has(W)||(K="Server sent an invalid subprotocol"):K="Server sent a subprotocol but none was requested":d.size&&(K="Server sent no subprotocol"),K){x(t,y,K);return}W&&(t._protocol=W);let Xe=u.headers["sec-websocket-extensions"];if(Xe!==void 0){if(!m){x(t,y,"Server sent a Sec-WebSocket-Extensions header but no extension was requested");return}let Ee;try{Ee=ms(Xe)}catch{x(t,y,"Invalid Sec-WebSocket-Extensions header");return}let Ze=Object.keys(Ee);if(Ze.length!==1||Ze[0]!==R.extensionName){x(t,y,"Server indicated an extension that was not requested");return}try{m.accept(Ee[R.extensionName])}catch{x(t,y,"Invalid Sec-WebSocket-Extensions header");return}t._extensions[R.extensionName]=m}t.setSocket(y,F,{allowSynchronousEvents:i.allowSynchronousEvents,generateMask:i.generateMask,maxBufferedChunks:i.maxBufferedChunks,maxFragments:i.maxFragments,maxPayload:i.maxPayload,skipUTF8Validation:i.skipUTF8Validation})}),i.finishRequest?i.finishRequest(_,t):_.end()}function _e(t,e){t._readyState=p.CLOSING,t._errorEmitted=!0,t.emit("error",e),t.emitClose()}function Ss(t){return t.path=t.socketPath,Dt.connect(t)}function xs(t){return t.path=void 0,!t.servername&&t.servername!==""&&(t.servername=Dt.isIP(t.host)?"":t.host),ss.connect(t)}function x(t,e,r){t._readyState=p.CLOSING;let s=new Error(r);Error.captureStackTrace(s,x),e.setHeader?(e[At]=!0,e.abort(),e.socket&&!e.socket.destroyed&&e.socket.destroy(),process.nextTick(_e,t,s)):(e.destroy(s),e.once("error",t.emit.bind(t,"error")),e.once("close",t.emitClose.bind(t)))}function je(t,e,r){if(e){let s=cs(e)?e.size:gs(e).length;t._socket?t._sender._bufferedBytes+=s:t._bufferedAmount+=s}if(r){let s=new Error(`WebSocket is not open: readyState ${t.readyState} (${N[t.readyState]})`);process.nextTick(r,s)}}function Es(t,e){let r=this[g];r._closeFrameReceived=!0,r._closeMessage=e,r._closeCode=t,r._socket[g]!==void 0&&(r._socket.removeListener("data",me),process.nextTick(Ft,r._socket),t===1005?r.close():r.close(t,e))}function bs(){let t=this[g];t.isPaused||t._socket.resume()}function vs(t){let e=this[g];e._socket[g]!==void 0&&(e._socket.removeListener("data",me),process.nextTick(Ft,e._socket),e.close(t[us])),e._errorEmitted||(e._errorEmitted=!0,e.emit("error",t))}function It(){this[g].emitClose()}function ws(t,e){this[g].emit("message",t,e)}function Ts(t){let e=this[g];e._autoPong&&e.pong(t,!this._isServer,Ut),e.emit("ping",t)}function Os(t){this[g].emit("pong",t)}function Ft(t){t.resume()}function ks(t){let e=this[g];e.readyState!==p.CLOSED&&(e.readyState===p.OPEN&&(e._readyState=p.CLOSING,Wt(e)),this._socket.end(),e._errorEmitted||(e._errorEmitted=!0,e.emit("error",t)))}function Wt(t){t._closeTimer=setTimeout(t._socket.destroy.bind(t._socket),t._closeTimeout)}function jt(){let t=this[g];if(this.removeListener("close",jt),this.removeListener("data",me),this.removeListener("end",$t),t._readyState=p.CLOSING,!this._readableState.endEmitted&&!t._closeFrameReceived&&!t._receiver._writableState.errorEmitted&&this._readableState.length!==0){let e=this.read(this._readableState.length);t._receiver.write(e)}t._receiver.end(),this[g]=void 0,clearTimeout(t._closeTimer),t._receiver._writableState.finished||t._receiver._writableState.errorEmitted?t.emitClose():(t._receiver.on("error",It),t._receiver.on("finish",It))}function me(t){this[g]._receiver.write(t)||this.pause()}function $t(){let t=this[g];t._readyState=p.CLOSING,t._receiver.end(),this.end()}function Gt(){let t=this[g];this.removeListener("error",Gt),this.on("error",Ut),t&&(t._readyState=p.CLOSING,this.destroy())}});var Jt=S((xi,zt)=>{"use strict";var Si=ge(),{Duplex:Ns}=require("stream");function Vt(t){t.emit("close")}function Cs(){!this.destroyed&&this._writableState.finished&&this.destroy()}function Ht(t){this.removeListener("error",Ht),this.destroy(),this.listenerCount("error")===0&&this.emit("error",t)}function Ps(t,e){let r=!0,s=new Ns({...e,autoDestroy:!1,emitClose:!1,objectMode:!1,writableObjectMode:!1});return t.on("message",function(n,o){let c=!o&&s._readableState.objectMode?n.toString():n;s.push(c)||t.pause()}),t.once("error",function(n){s.destroyed||(r=!1,s.destroy(n))}),t.once("close",function(){s.destroyed||s.push(null)}),s._destroy=function(i,n){if(t.readyState===t.CLOSED){n(i),process.nextTick(Vt,s);return}let o=!1;t.once("error",function(a){o=!0,n(a)}),t.once("close",function(){o||n(i),process.nextTick(Vt,s)}),r&&t.terminate()},s._final=function(i){if(t.readyState===t.CONNECTING){t.once("open",function(){s._final(i)});return}t._socket!==null&&(t._socket._writableState.finished?(i(),s._readableState.endEmitted&&s.destroy()):(t._socket.once("finish",function(){i()}),t.close()))},s._read=function(){t.isPaused&&t.resume()},s._write=function(i,n,o){if(t.readyState===t.CONNECTING){t.once("open",function(){s._write(i,n,o)});return}t.send(i,o)},s.on("end",Cs),s.on("error",Ht),s}zt.exports=Ps});var $e=S((Ei,Yt)=>{"use strict";var{tokenChars:Ls}=q();function Rs(t){let e=new Set,r=-1,s=-1,i=0;for(i;i<t.length;i++){let o=t.charCodeAt(i);if(s===-1&&Ls[o]===1)r===-1&&(r=i);else if(i!==0&&(o===32||o===9))s===-1&&r!==-1&&(s=i);else if(o===44){if(r===-1)throw new SyntaxError(`Unexpected character at index ${i}`);s===-1&&(s=i);let c=t.slice(r,s);if(e.has(c))throw new SyntaxError(`The "${c}" subprotocol is duplicated`);e.add(c),r=s=-1}else throw new SyntaxError(`Unexpected character at index ${i}`)}if(r===-1||s!==-1)throw new SyntaxError("Unexpected end of input");let n=t.slice(r,i);if(e.has(n))throw new SyntaxError(`The "${n}" subprotocol is duplicated`);return e.add(n),e}Yt.exports={parse:Rs}});var rr=S((vi,tr)=>{"use strict";var Bs=require("events"),ye=require("http"),{Duplex:bi}=require("stream"),{createHash:Is}=require("crypto"),Kt=de(),A=G(),Ds=$e(),Us=ge(),{CLOSE_TIMEOUT:As,GUID:Ms,kWebSocket:Fs}=T(),Ws=/^[+/0-9A-Za-z]{22}==$/,Xt=0,Zt=1,er=2,Ge=class extends Bs{constructor(e,r){if(super(),e={allowSynchronousEvents:!0,autoPong:!0,maxBufferedChunks:256*1024,maxFragments:16*1024,maxPayload:100*1024*1024,skipUTF8Validation:!1,perMessageDeflate:!1,handleProtocols:null,clientTracking:!0,closeTimeout:As,verifyClient:null,noServer:!1,backlog:null,server:null,host:null,path:null,port:null,WebSocket:Us,...e},e.port==null&&!e.server&&!e.noServer||e.port!=null&&(e.server||e.noServer)||e.server&&e.noServer)throw new TypeError('One and only one of the "port", "server", or "noServer" options must be specified');if(e.port!=null?(this._server=ye.createServer((s,i)=>{let n=ye.STATUS_CODES[426];i.writeHead(426,{"Content-Length":n.length,"Content-Type":"text/plain"}),i.end(n)}),this._server.listen(e.port,e.host,e.backlog,r)):e.server&&(this._server=e.server),this._server){let s=this.emit.bind(this,"connection");this._removeListeners=js(this._server,{listening:this.emit.bind(this,"listening"),error:this.emit.bind(this,"error"),upgrade:(i,n,o)=>{this.handleUpgrade(i,n,o,s)}})}e.perMessageDeflate===!0&&(e.perMessageDeflate={}),e.clientTracking&&(this.clients=new Set,this._shouldEmitClose=!1),this.options=e,this._state=Xt}address(){if(this.options.noServer)throw new Error('The server is operating in "noServer" mode');return this._server?this._server.address():null}close(e){if(this._state===er){e&&this.once("close",()=>{e(new Error("The server is not running"))}),process.nextTick(re,this);return}if(e&&this.once("close",e),this._state!==Zt)if(this._state=Zt,this.options.noServer||this.options.server)this._server&&(this._removeListeners(),this._removeListeners=this._server=null),this.clients?this.clients.size?this._shouldEmitClose=!0:process.nextTick(re,this):process.nextTick(re,this);else{let r=this._server;this._removeListeners(),this._removeListeners=this._server=null,r.close(()=>{re(this)})}}shouldHandle(e){if(this.options.path){let r=e.url.indexOf("?");if((r!==-1?e.url.slice(0,r):e.url)!==this.options.path)return!1}return!0}handleUpgrade(e,r,s,i){r.on("error",Qt);let n=e.headers["sec-websocket-key"],o=e.headers.upgrade,c=+e.headers["sec-websocket-version"];if(e.method!=="GET"){M(this,e,r,405,"Invalid HTTP method");return}if(o===void 0||o.toLowerCase()!=="websocket"){M(this,e,r,400,"Invalid Upgrade header");return}if(n===void 0||!Ws.test(n)){M(this,e,r,400,"Missing or invalid Sec-WebSocket-Key header");return}if(c!==13&&c!==8){M(this,e,r,400,"Missing or invalid Sec-WebSocket-Version header",{"Sec-WebSocket-Version":"13, 8"});return}if(!this.shouldHandle(e)){se(r,400);return}let a=e.headers["sec-websocket-protocol"],l=new Set;if(a!==void 0)try{l=Ds.parse(a)}catch{M(this,e,r,400,"Invalid Sec-WebSocket-Protocol header");return}let f=e.headers["sec-websocket-extensions"],h={};if(this.options.perMessageDeflate&&f!==void 0){let d=new A({...this.options.perMessageDeflate,isServer:!0,maxPayload:this.options.maxPayload});try{let m=Kt.parse(f);m[A.extensionName]&&(d.accept(m[A.extensionName]),h[A.extensionName]=d)}catch{M(this,e,r,400,"Invalid or unacceptable Sec-WebSocket-Extensions header");return}}if(this.options.verifyClient){let d={origin:e.headers[`${c===8?"sec-websocket-origin":"origin"}`],secure:!!(e.socket.authorized||e.socket.encrypted),req:e};if(this.options.verifyClient.length===2){this.options.verifyClient(d,(m,_,u,y)=>{if(!m)return se(r,_||401,u,y);this.completeUpgrade(h,n,l,e,r,s,i)});return}if(!this.options.verifyClient(d))return se(r,401)}this.completeUpgrade(h,n,l,e,r,s,i)}completeUpgrade(e,r,s,i,n,o,c){if(!n.readable||!n.writable)return n.destroy();if(n[Fs])throw new Error("server.handleUpgrade() was called more than once with the same socket, possibly due to a misconfiguration");if(this._state>Xt)return se(n,503);let l=["HTTP/1.1 101 Switching Protocols","Upgrade: websocket","Connection: Upgrade",`Sec-WebSocket-Accept: ${Is("sha1").update(r+Ms).digest("base64")}`],f=new this.options.WebSocket(null,void 0,this.options);if(s.size){let h=this.options.handleProtocols?this.options.handleProtocols(s,i):s.values().next().value;h&&(l.push(`Sec-WebSocket-Protocol: ${h}`),f._protocol=h)}if(e[A.extensionName]){let h=e[A.extensionName].params,d=Kt.format({[A.extensionName]:[h]});l.push(`Sec-WebSocket-Extensions: ${d}`),f._extensions=e}this.emit("headers",l,i),n.write(l.concat(`\r
3
+ `).join(`\r
4
+ `)),n.removeListener("error",Qt),f.setSocket(n,o,{allowSynchronousEvents:this.options.allowSynchronousEvents,maxBufferedChunks:this.options.maxBufferedChunks,maxFragments:this.options.maxFragments,maxPayload:this.options.maxPayload,skipUTF8Validation:this.options.skipUTF8Validation}),this.clients&&(this.clients.add(f),f.on("close",()=>{this.clients.delete(f),this._shouldEmitClose&&!this.clients.size&&process.nextTick(re,this)})),c(f,i)}};tr.exports=Ge;function js(t,e){for(let r of Object.keys(e))t.on(r,e[r]);return function(){for(let s of Object.keys(e))t.removeListener(s,e[s])}}function re(t){t._state=er,t.emit("close")}function Qt(){this.destroy()}function se(t,e,r,s){r=r||ye.STATUS_CODES[e],s={Connection:"close","Content-Type":"text/html","Content-Length":Buffer.byteLength(r),...s},t.once("finish",t.destroy),t.end(`HTTP/1.1 ${e} ${ye.STATUS_CODES[e]}\r
5
+ `+Object.keys(s).map(i=>`${i}: ${s[i]}`).join(`\r
6
+ `)+`\r
7
+ \r
8
+ `+r)}function M(t,e,r,s,i,n){if(t.listenerCount("wsClientError")){let o=new Error(i);Error.captureStackTrace(o,M),t.emit("wsClientError",o,r,e)}else se(r,s,i,n)}});var hr=require("node:http");var $s=P(Jt(),1),Gs=P(de(),1),qs=P(G(),1),Vs=P(Be(),1),Hs=P(Ue(),1),zs=P($e(),1),Js=P(ge(),1),qe=P(rr(),1);function sr(){return`m_${Date.now().toString(36)}_${Math.random().toString(36).slice(2,10)}`}function Ve(t,e,r){return{id:t,kind:"response",error:{code:e,message:r}}}var nr=require("node:crypto"),B=require("node:fs"),or=require("node:path");function ir(t=Math.random){return String(Math.floor(1e5+t()*9e5))}var Se=class{sessions=new Map;tokens=new Map;persistPath;constructor(e={}){if(this.persistPath=e.persistPath,this.persistPath&&(0,B.existsSync)(this.persistPath))try{let r=JSON.parse((0,B.readFileSync)(this.persistPath,"utf8"));for(let s of r)s?.token&&s?.deviceId&&this.tokens.set(s.token,s)}catch{}}persist(){if(this.persistPath)try{(0,B.mkdirSync)((0,or.dirname)(this.persistPath),{recursive:!0}),(0,B.writeFileSync)(this.persistPath,JSON.stringify([...this.tokens.values()]),{mode:384})}catch{}}requestPairing(e,r,s,i=Date.now(),n){this.scrub(i);let o=ir(n);for(;this.sessions.has(o);)o=ir(n);let c={code:o,deviceId:e,clientName:r,scopes:s,createdAt:i,expiresAt:i+6e5,approved:!1};return this.sessions.set(o,c),c}approvePairing(e,r,s=Date.now()){let i=this.sessions.get(e);if(!i||i.deviceId!==r||i.expiresAt<=s)return null;if(i.approved&&i.token)return i.token;let n=(0,nr.randomBytes)(24).toString("hex");return i.approved=!0,i.token=n,this.tokens.set(n,{token:n,deviceId:i.deviceId,clientName:i.clientName,scopes:i.scopes,createdAt:s}),this.persist(),n}claimPairing(e,r=Date.now()){let s=this.sessions.get(e);return!s||s.expiresAt<=r?{status:"expired"}:s.approved&&s.token?{status:"approved",token:s.token}:{status:"pending"}}verifyToken(e){return this.tokens.get(e)??null}revokeDevice(e){let r=0;for(let[s,i]of this.tokens)i.deviceId===e&&(this.tokens.delete(s),r++);return r>0&&this.persist(),r}pendingForDevice(e,r=Date.now()){return this.scrub(r),[...this.sessions.values()].filter(s=>s.deviceId===e&&!s.approved&&s.expiresAt>r)}scrub(e){for(let[r,s]of this.sessions)s.expiresAt<=e&&this.sessions.delete(r)}};var xe=class{devices=new Map;pending=new Map;registerDevice(e,r){this.devices.set(e,r)}unregisterDevice(e,r){(!r||this.devices.get(e)===r)&&this.devices.delete(e)}isDeviceOnline(e){return this.devices.has(e)}onlineDevices(){return[...this.devices.keys()]}routeToDevice(e,r,s,i=3e4,n){let o=this.devices.get(e);if(!o)return Promise.reject(Object.assign(new Error(`DISCONNECTED: device ${e} offline`),{code:"DISCONNECTED"}));let c=sr(),a={id:c,kind:"request",method:r,params:s,agentName:n?.agentName};return new Promise((l,f)=>{let h=setTimeout(()=>{this.pending.delete(c),f(Object.assign(new Error(`TIMEOUT waiting for device ${e}`),{code:"TIMEOUT"}))},i);h.unref?.(),this.pending.set(c,{resolve:l,reject:f,timer:h});try{o(JSON.stringify(a))}catch(d){this.pending.delete(c),clearTimeout(h),f(d instanceof Error?d:new Error(String(d)))}})}settleResponse(e){let r=this.pending.get(e.id);return r?(this.pending.delete(e.id),clearTimeout(r.timer),e.error?r.reject(Object.assign(new Error(`${e.error.code}: ${e.error.message}`),{code:e.error.code})):r.resolve(e.result),!0):!1}pendingCount(){return this.pending.size}};function He(t){let e=Array.isArray(t)?t[0]:t;if(!e)return null;let r=/^Bearer\s+(.+)$/i.exec(e.trim());return r?r[1].trim():null}function Xs(t,e){return t.includes("*")||t.includes(e)}function ar(t,e){return t?Xs(t.scopes,e):!1}var ie={type:"object",description:"Semantic target: durable ref (@Hxxxx) or role/name/region query.",properties:{ref:{type:"string"},role:{type:"string"},name:{type:"string"},nameContains:{type:"string"},label:{type:"string"},region:{type:"string"}}},ze=[{name:"browser_observe",description:"Compact page/region digest of the controlled tab.",inputSchema:{type:"object",properties:{level:{type:"string"},regionId:{type:"string"},includeRecentDiff:{type:"boolean"}}},scope:"browser:read"},{name:"browser_find",description:"Fast semantic lookup returning durable refs.",inputSchema:{type:"object",properties:{role:{type:"string"},name:{type:"string"},nameContains:{type:"string"},region:{type:"string"}}},scope:"browser:read"},{name:"browser_click",description:"Click a semantic target (guarded, verified).",inputSchema:{type:"object",properties:{target:ie,expect:{type:"array"}},required:["target"]},scope:"browser:act"},{name:"browser_fill",description:"Fill a field and verify the value.",inputSchema:{type:"object",properties:{target:ie,value:{type:"string"},expect:{type:"array"}},required:["target","value"]},scope:"browser:act"},{name:"browser_select",description:"Choose a select option.",inputSchema:{type:"object",properties:{target:ie,value:{type:"string"}},required:["target","value"]},scope:"browser:act"},{name:"browser_press",description:"Press a key on a target or the focused element.",inputSchema:{type:"object",properties:{target:ie,key:{type:"string"}}},scope:"browser:act"},{name:"browser_scroll",description:"Scroll a target into view or scroll the page.",inputSchema:{type:"object",properties:{target:ie,direction:{type:"string"}}},scope:"browser:act"},{name:"browser_navigate",description:"Navigate the controlled tab (site grant required).",inputSchema:{type:"object",properties:{url:{type:"string"}},required:["url"]},scope:"browser:act"},{name:"browser_back",description:"Tab history back.",inputSchema:{type:"object",properties:{}},scope:"browser:act"},{name:"browser_forward",description:"Tab history forward.",inputSchema:{type:"object",properties:{}},scope:"browser:act"},{name:"browser_tabs",description:"List open tabs.",inputSchema:{type:"object",properties:{}},scope:"browser:read"},{name:"browser_switch_tab",description:"Switch control to another tab.",inputSchema:{type:"object",properties:{tabId:{type:"number"}},required:["tabId"]},scope:"browser:act"},{name:"browser_screenshot",description:"Capture the visible tab (rate-limited fallback).",inputSchema:{type:"object",properties:{}},scope:"browser:read"},{name:"browser_extract",description:"Extract text/table data plus article body paragraphs (passwords never returned).",inputSchema:{type:"object",properties:{regionId:{type:"string"},maxRows:{type:"number"}}},scope:"browser:read"},{name:"browser_wait",description:"Wait for a condition or timeout.",inputSchema:{type:"object",properties:{timeoutMs:{type:"number"},urlMatches:{type:"string"}}},scope:"browser:read"},{name:"browser_plan",description:"Guarded micro-plan: multiple verified steps, one decision.",inputSchema:{type:"object",properties:{steps:{type:"array"},abortOn:{type:"array"}},required:["steps"]},scope:"browser:act"}];function cr(t){return ze.find(e=>e.name===t)}var Zs="2024-11-05";function Qs(t){let e=new URL(t.url??"/","http://localhost");return t.method==="POST"&&e.pathname==="/mcp"}async function lr(t,e,r){if(!Qs(t))return!1;let s=r.pairings.verifyToken(He(t.headers.authorization)??"");if(!s)return C(e,t,null,{error:{code:-32001,message:"Unauthorized: invalid or missing pairing token"}},401);let n=await ei(t);if(n.jsonrpc!=="2.0"||typeof n.method!="string")return C(e,t,n.id??null,{error:{code:-32600,message:"Invalid JSON-RPC request"}},400);if(n.method==="initialize")return C(e,t,n.id??null,{result:{protocolVersion:Zs,capabilities:{tools:{}},serverInfo:{name:"babtab-relay",version:"0.1.0"}}});if(n.method.startsWith("notifications/"))return e.writeHead(202).end(),!0;if(n.method==="tools/list")return C(e,t,n.id??null,{result:{tools:ze.map(o=>({name:o.name,description:o.description,inputSchema:o.inputSchema}))}});if(n.method==="tools/call"){let o=String(n.params?.name??""),c=n.params?.arguments??{},a=cr(o);if(!a)return C(e,t,n.id??null,{result:Je(`Unknown tool: ${o}`)});if(!ar(s,a.scope))return C(e,t,n.id??null,{result:Je(`Token lacks scope ${a.scope} for ${o}`)});try{let l=await r.registry.routeToDevice(s.deviceId,o,c,3e4,{agentName:s.clientName});return C(e,t,n.id??null,{result:{content:[{type:"text",text:JSON.stringify(l).slice(0,32e3)}]}})}catch(l){let f=l;return C(e,t,n.id??null,{result:Je(`${f.code??"DISCONNECTED"}: ${f.message}`,!0)})}}return C(e,t,n.id??null,{error:{code:-32601,message:`Method not found: ${n.method}`}},404)}function Je(t,e=!1){return{content:[{type:"text",text:t}],isError:!0,_meta:{retryable:e}}}function C(t,e,r,s,i=200){let n=JSON.stringify({jsonrpc:"2.0",id:r,...s});return String(e.headers.accept??"application/json").includes("text/event-stream")?(t.writeHead(i,{"content-type":"text/event-stream","cache-control":"no-cache","access-control-allow-origin":"*"}),t.end(`event: message
9
+ data: ${n}
10
+
11
+ `),!0):(t.writeHead(i,{"content-type":"application/json","access-control-allow-origin":"*"}),t.end(n),!0)}function ei(t){return new Promise(e=>{let r="";t.on("data",s=>{r+=s,r.length>256e3&&t.destroy()}),t.on("end",()=>{try{e(JSON.parse(r||"{}"))}catch{e({})}})})}function ur(t={}){let e=new Se({persistPath:process.env.BABTAB_TOKEN_FILE??"./data/relay-tokens.json"}),r=new xe,s,i=(0,hr.createServer)((a,l)=>void ri(a,l,s)),n=new qe.default({server:i});n.on("connection",(a,l)=>{let f=new URL(l.url??"/","http://localhost");f.pathname==="/device"?si(a,f.searchParams.get("deviceId")??"",e,r):f.pathname==="/client"?ii(a,f.searchParams.get("token")??"",e,r):a.close(1008,"unknown path")});let o=setInterval(()=>{for(let a of n.clients){if(a.isAlive===!1){a.terminate();continue}a.isAlive=!1,a.ping()}},2e4);o.unref?.(),n.on("connection",a=>{a.isAlive=!0,a.on("pong",()=>{a.isAlive=!0})});let c=t.port??0;return i.listen(c),s={server:i,pairings:e,registry:r,close:()=>new Promise((a,l)=>{clearInterval(o),n.close(()=>i.close(f=>f?l(f):a()))})},s}async function ri(t,e,r){let s=new URL(t.url??"/","http://localhost");if(t.method==="OPTIONS"){e.writeHead(204,{"access-control-allow-origin":"*","access-control-allow-methods":"GET, POST, OPTIONS","access-control-allow-headers":"content-type, authorization"}),e.end();return}let i=(n,o)=>{e.writeHead(n,{"content-type":"application/json","access-control-allow-origin":"*"}),e.end(JSON.stringify(o))};if(t.method==="GET"&&s.pathname==="/health"){i(200,{ok:!0,devices:r.registry.onlineDevices().length});return}if(t.method==="POST"&&s.pathname==="/pairing/request"){let n=await fr(t),o=String(n.deviceId??"");if(!o){i(400,{error:"deviceId required"});return}let c=Array.isArray(n.scopes)?n.scopes.map(String):["browser:read","browser:act"],a=r.pairings.requestPairing(o,String(n.clientName??"agent"),c);i(200,{code:a.code,expiresAt:a.expiresAt,deviceOnline:r.registry.isDeviceOnline(o)});return}if(t.method==="POST"&&s.pathname==="/pairing/claim"){let n=await fr(t);i(200,r.pairings.claimPairing(String(n.code??"")));return}await lr(t,e,r)||i(404,{error:"not found"})}function si(t,e,r,s){if(!e){t.close(1008,"deviceId required");return}let i=n=>{t.readyState===t.OPEN&&t.send(n)};s.registerDevice(e,i),t.on("message",n=>{let o;try{o=JSON.parse(String(n))}catch{return}if(o.kind==="response"){s.settleResponse(o);return}if(o.kind!=="pong"&&o.kind==="request"&&o.id){if(o.method==="pairing:pending"){let c=r.pendingForDevice(e).map(a=>({code:a.code,clientName:a.clientName,scopes:a.scopes,createdAt:a.createdAt,expiresAt:a.expiresAt}));t.send(JSON.stringify({id:o.id,kind:"response",result:c}));return}if(o.method==="pairing:approve"){let c=String(o.params?.code??""),a=r.approvePairing(c,e)!=null;t.send(JSON.stringify({id:o.id,kind:"response",result:{ok:a}}));return}t.send(JSON.stringify(Ve(o.id,"UNSUPPORTED_OPERATION",`Unknown device method ${o.method}`)))}}),t.on("close",()=>s.unregisterDevice(e,i))}function ii(t,e,r,s){let i=r.verifyToken(e);if(!i){t.close(1008,"invalid token");return}t.on("message",n=>{let o;try{o=JSON.parse(String(n))}catch{return}o.kind!=="request"||!o.method||s.routeToDevice(i.deviceId,o.method,o.params,3e4,{agentName:i.clientName}).then(c=>t.send(JSON.stringify({id:o.id,kind:"response",result:c}))).catch(c=>t.send(JSON.stringify(Ve(o.id,c.code??"DISCONNECTED",c.message))))})}function fr(t){return new Promise(e=>{let r="";t.on("data",s=>{r+=s,r.length>256e3&&t.destroy()}),t.on("end",()=>{try{e(JSON.parse(r||"{}"))}catch{e({})}})})}var dr=Number(process.env.PORT??3e3),Ye=ur({port:dr});Ye.server.on("listening",()=>{let t=Ye.server.address(),e=typeof t=="object"&&t?t.port:dr;console.log(`[babtab-relay] listening on http://127.0.0.1:${e}`),console.log("[babtab-relay] health: GET /health \xB7 mcp: POST /mcp"),console.log(`[babtab-relay] Side Panel \u2192 Relay URL = ws://127.0.0.1:${e} \u2192 Save & Connect`)});var pr=()=>{Ye.close().then(()=>process.exit(0))};process.on("SIGINT",pr);process.on("SIGTERM",pr);
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@babtab/relay",
3
+ "version": "0.1.0",
4
+ "description": "Babtab local relay: auth, pairing, WSS routing, Streamable HTTP MCP for the Babtab Chrome extension. No DOM, no grounding, no page storage.",
5
+ "type": "module",
6
+ "bin": {
7
+ "babtab-relay": "./dist/relay.cjs"
8
+ },
9
+ "files": [
10
+ "dist/relay.cjs",
11
+ "README.zh-TW.md",
12
+ "README.ja.md",
13
+ "README.ko.md",
14
+ "README.es.md",
15
+ "README.pt-BR.md",
16
+ "README.hi.md",
17
+ "SECURITY.md",
18
+ "CHANGELOG.md"
19
+ ],
20
+ "scripts": {
21
+ "start": "node dist/relay.cjs",
22
+ "prepublishOnly": "echo skip"
23
+ },
24
+ "engines": {
25
+ "node": ">=20"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "repository": "github:Poseidoncode/babtab-relay",
31
+ "homepage": "https://github.com/Poseidoncode/babtab-relay#readme",
32
+ "bugs": "https://github.com/Poseidoncode/babtab-relay/issues"
33
+ }