@betterportal/tunnels 1.0.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.
Files changed (116) hide show
  1. package/README.md +76 -0
  2. package/SPEC.md +524 -0
  3. package/lib/.bsb/clients/service-tunnel-client-api.d.ts +133 -0
  4. package/lib/.bsb/clients/service-tunnel-client-api.d.ts.map +1 -0
  5. package/lib/.bsb/clients/service-tunnel-client-api.js +102 -0
  6. package/lib/.bsb/clients/service-tunnel-client-api.js.map +1 -0
  7. package/lib/.bsb/clients/service-tunnel-web.d.ts +43 -0
  8. package/lib/.bsb/clients/service-tunnel-web.d.ts.map +1 -0
  9. package/lib/.bsb/clients/service-tunnel-web.js +47 -0
  10. package/lib/.bsb/clients/service-tunnel-web.js.map +1 -0
  11. package/lib/auth.d.ts +12 -0
  12. package/lib/auth.d.ts.map +1 -0
  13. package/lib/auth.js +59 -0
  14. package/lib/auth.js.map +1 -0
  15. package/lib/cli.d.ts +2 -0
  16. package/lib/cli.d.ts.map +1 -0
  17. package/lib/cli.js +295 -0
  18. package/lib/cli.js.map +1 -0
  19. package/lib/plugins/service-betterportal/.bp-generated/registry.d.ts +3 -0
  20. package/lib/plugins/service-betterportal/.bp-generated/registry.d.ts.map +1 -0
  21. package/lib/plugins/service-betterportal/.bp-generated/registry.js +127 -0
  22. package/lib/plugins/service-betterportal/.bp-generated/registry.js.map +1 -0
  23. package/lib/plugins/service-betterportal/assets/brand/bettertunnels-horizontal-light.svg +18 -0
  24. package/lib/plugins/service-betterportal/assets/brand/bettertunnels-square-dark-1024x1024.png +0 -0
  25. package/lib/plugins/service-betterportal/assets/brand/favicon.svg +12 -0
  26. package/lib/plugins/service-betterportal/betterportal-logo.png +0 -0
  27. package/lib/plugins/service-betterportal/bp-routes/cli-auth/verify/_theme.bootstrap1/index.d.ts +5 -0
  28. package/lib/plugins/service-betterportal/bp-routes/cli-auth/verify/_theme.bootstrap1/index.d.ts.map +1 -0
  29. package/lib/plugins/service-betterportal/bp-routes/cli-auth/verify/_theme.bootstrap1/index.js +6 -0
  30. package/lib/plugins/service-betterportal/bp-routes/cli-auth/verify/_theme.bootstrap1/index.js.map +1 -0
  31. package/lib/plugins/service-betterportal/bp-routes/cli-auth/verify/index.d.ts +25 -0
  32. package/lib/plugins/service-betterportal/bp-routes/cli-auth/verify/index.d.ts.map +1 -0
  33. package/lib/plugins/service-betterportal/bp-routes/cli-auth/verify/index.js +59 -0
  34. package/lib/plugins/service-betterportal/bp-routes/cli-auth/verify/index.js.map +1 -0
  35. package/lib/plugins/service-betterportal/bp-routes/dashboard/_theme.bootstrap1/index.d.ts +5 -0
  36. package/lib/plugins/service-betterportal/bp-routes/dashboard/_theme.bootstrap1/index.d.ts.map +1 -0
  37. package/lib/plugins/service-betterportal/bp-routes/dashboard/_theme.bootstrap1/index.js +22 -0
  38. package/lib/plugins/service-betterportal/bp-routes/dashboard/_theme.bootstrap1/index.js.map +1 -0
  39. package/lib/plugins/service-betterportal/bp-routes/dashboard/index.d.ts +40 -0
  40. package/lib/plugins/service-betterportal/bp-routes/dashboard/index.d.ts.map +1 -0
  41. package/lib/plugins/service-betterportal/bp-routes/dashboard/index.js +57 -0
  42. package/lib/plugins/service-betterportal/bp-routes/dashboard/index.js.map +1 -0
  43. package/lib/plugins/service-betterportal/bp-routes/downloads/_theme.bootstrap1/index.d.ts +5 -0
  44. package/lib/plugins/service-betterportal/bp-routes/downloads/_theme.bootstrap1/index.d.ts.map +1 -0
  45. package/lib/plugins/service-betterportal/bp-routes/downloads/_theme.bootstrap1/index.js +5 -0
  46. package/lib/plugins/service-betterportal/bp-routes/downloads/_theme.bootstrap1/index.js.map +1 -0
  47. package/lib/plugins/service-betterportal/bp-routes/downloads/index.d.ts +30 -0
  48. package/lib/plugins/service-betterportal/bp-routes/downloads/index.d.ts.map +1 -0
  49. package/lib/plugins/service-betterportal/bp-routes/downloads/index.js +45 -0
  50. package/lib/plugins/service-betterportal/bp-routes/downloads/index.js.map +1 -0
  51. package/lib/plugins/service-betterportal/bp-routes/landing/_theme.bootstrap1/index.d.ts +5 -0
  52. package/lib/plugins/service-betterportal/bp-routes/landing/_theme.bootstrap1/index.d.ts.map +1 -0
  53. package/lib/plugins/service-betterportal/bp-routes/landing/_theme.bootstrap1/index.js +5 -0
  54. package/lib/plugins/service-betterportal/bp-routes/landing/_theme.bootstrap1/index.js.map +1 -0
  55. package/lib/plugins/service-betterportal/bp-routes/landing/index.d.ts +40 -0
  56. package/lib/plugins/service-betterportal/bp-routes/landing/index.d.ts.map +1 -0
  57. package/lib/plugins/service-betterportal/bp-routes/landing/index.js +63 -0
  58. package/lib/plugins/service-betterportal/bp-routes/landing/index.js.map +1 -0
  59. package/lib/plugins/service-betterportal/bp-routes/tunnels/_theme.bootstrap1/index.d.ts +5 -0
  60. package/lib/plugins/service-betterportal/bp-routes/tunnels/_theme.bootstrap1/index.d.ts.map +1 -0
  61. package/lib/plugins/service-betterportal/bp-routes/tunnels/_theme.bootstrap1/index.js +5 -0
  62. package/lib/plugins/service-betterportal/bp-routes/tunnels/_theme.bootstrap1/index.js.map +1 -0
  63. package/lib/plugins/service-betterportal/bp-routes/tunnels/index.d.ts +44 -0
  64. package/lib/plugins/service-betterportal/bp-routes/tunnels/index.d.ts.map +1 -0
  65. package/lib/plugins/service-betterportal/bp-routes/tunnels/index.js +59 -0
  66. package/lib/plugins/service-betterportal/bp-routes/tunnels/index.js.map +1 -0
  67. package/lib/plugins/service-betterportal/brand-assets.d.ts +7 -0
  68. package/lib/plugins/service-betterportal/brand-assets.d.ts.map +1 -0
  69. package/lib/plugins/service-betterportal/brand-assets.js +24 -0
  70. package/lib/plugins/service-betterportal/brand-assets.js.map +1 -0
  71. package/lib/plugins/service-betterportal/index.d.ts +57 -0
  72. package/lib/plugins/service-betterportal/index.d.ts.map +1 -0
  73. package/lib/plugins/service-betterportal/index.js +108 -0
  74. package/lib/plugins/service-betterportal/index.js.map +1 -0
  75. package/lib/plugins/service-tunnel-client-api/ids.d.ts +6 -0
  76. package/lib/plugins/service-tunnel-client-api/ids.d.ts.map +1 -0
  77. package/lib/plugins/service-tunnel-client-api/ids.js +17 -0
  78. package/lib/plugins/service-tunnel-client-api/ids.js.map +1 -0
  79. package/lib/plugins/service-tunnel-client-api/index.d.ts +158 -0
  80. package/lib/plugins/service-tunnel-client-api/index.d.ts.map +1 -0
  81. package/lib/plugins/service-tunnel-client-api/index.js +591 -0
  82. package/lib/plugins/service-tunnel-client-api/index.js.map +1 -0
  83. package/lib/plugins/service-tunnel-client-api/registry.d.ts +36 -0
  84. package/lib/plugins/service-tunnel-client-api/registry.d.ts.map +1 -0
  85. package/lib/plugins/service-tunnel-client-api/registry.js +14 -0
  86. package/lib/plugins/service-tunnel-client-api/registry.js.map +1 -0
  87. package/lib/plugins/service-tunnel-client-api/schemas.d.ts +66 -0
  88. package/lib/plugins/service-tunnel-client-api/schemas.d.ts.map +1 -0
  89. package/lib/plugins/service-tunnel-client-api/schemas.js +56 -0
  90. package/lib/plugins/service-tunnel-client-api/schemas.js.map +1 -0
  91. package/lib/plugins/service-tunnel-web/http.d.ts +4 -0
  92. package/lib/plugins/service-tunnel-web/http.d.ts.map +1 -0
  93. package/lib/plugins/service-tunnel-web/http.js +119 -0
  94. package/lib/plugins/service-tunnel-web/http.js.map +1 -0
  95. package/lib/plugins/service-tunnel-web/index.d.ts +79 -0
  96. package/lib/plugins/service-tunnel-web/index.d.ts.map +1 -0
  97. package/lib/plugins/service-tunnel-web/index.js +278 -0
  98. package/lib/plugins/service-tunnel-web/index.js.map +1 -0
  99. package/lib/plugins/service-tunnel-web/verification.d.ts +27 -0
  100. package/lib/plugins/service-tunnel-web/verification.d.ts.map +1 -0
  101. package/lib/plugins/service-tunnel-web/verification.js +209 -0
  102. package/lib/plugins/service-tunnel-web/verification.js.map +1 -0
  103. package/lib/prisma.d.ts +6 -0
  104. package/lib/prisma.d.ts.map +1 -0
  105. package/lib/prisma.js +10 -0
  106. package/lib/prisma.js.map +1 -0
  107. package/lib/schemas/service-betterportal.json +109 -0
  108. package/lib/schemas/service-tunnel-client-api.json +300 -0
  109. package/lib/schemas/service-tunnel-web.json +140 -0
  110. package/package.json +60 -0
  111. package/prisma/migrations/0001_init/migration.sql +63 -0
  112. package/prisma/migrations/0002_account_plan/migration.sql +6 -0
  113. package/prisma/migrations/0003_tunnel_owner_server/migration.sql +1 -0
  114. package/prisma/migrations/20260701000000_add_client_auth/migration.sql +46 -0
  115. package/prisma/migrations/migration_lock.toml +3 -0
  116. package/prisma/schema.prisma +141 -0
package/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # BetterTunnels
2
+
3
+ HTTP/HTTPS development tunnels using BSB, H3, AnyVali, websocket clients, Prisma, and PostgreSQL 18.
4
+
5
+ ## Dev Setup
6
+
7
+ ```powershell
8
+ npm install
9
+ copy .env.example .env
10
+ npm run prisma:generate
11
+ npm run build
12
+ ```
13
+
14
+ Start PostgreSQL 18 with the included compose file when Docker is available:
15
+
16
+ ```powershell
17
+ docker compose up -d postgres
18
+ npm run prisma:migrate
19
+ ```
20
+
21
+ Run the server:
22
+
23
+ ```powershell
24
+ npm run dev
25
+ ```
26
+
27
+ Server startup goes through BSB. Do not start a standalone `src/server.ts`.
28
+
29
+ Run a tunnel:
30
+
31
+ ```powershell
32
+ npm run cli -- http 3300
33
+ ```
34
+
35
+ Build the release CLI:
36
+
37
+ ```powershell
38
+ cd cli
39
+ go build -o ..\dist\btunnel.exe .
40
+ ```
41
+
42
+ ## Notes
43
+
44
+ - Default domain: `tunnels.betterportal.dev`.
45
+ - Anonymous configured prefixes are ignored.
46
+ - Authenticated prefix handling is designed but auth is not in the first runtime slice.
47
+ - The H3/WS runtime is owned by the `service-tunnel-web` BSB plugin.
48
+ - The release CLI lives in `cli/` and builds to single-file Go binaries for Windows, Linux, and macOS.
49
+
50
+ ## Docker
51
+
52
+ Build the BSB runtime image with this plugin preloaded:
53
+
54
+ ```powershell
55
+ docker build -t better-tunnels:dev .
56
+ ```
57
+
58
+ Run it with a production `DATABASE_URL`:
59
+
60
+ ```powershell
61
+ docker run --rm -p 8080:8080 -p 8081:8081 -e DATABASE_URL="postgresql://..." better-tunnels:dev
62
+ ```
63
+
64
+ ## Swarm Deploy
65
+
66
+ On the server, build local images, run migrations, then deploy the stack:
67
+
68
+ ```bash
69
+ docker swarm init
70
+ docker build -t bettertunnels-app:latest .
71
+ docker build -t bettertunnels-migrate:latest --target build .
72
+ docker build -t bettertunnels-caddy:latest -f Dockerfile.caddy .
73
+ docker stack deploy -c docker-stack.yml bettertunnels
74
+ ```
75
+
76
+ `app` runs two replicas with `start-first` rolling updates. Caddy and Postgres are singletons on this one-node swarm. The stack reuses the original Compose volumes by external name.
package/SPEC.md ADDED
@@ -0,0 +1,524 @@
1
+ # BetterTunnels SPEC
2
+
3
+ ## Goal
4
+
5
+ Build an HTTP/HTTPS-only tunnel service for development traffic.
6
+
7
+ The platform has two moving pieces:
8
+
9
+ - Server: accepts public HTTP/HTTPS traffic, validates visitors when needed, rate-limits, and routes requests to connected clients.
10
+ - Client CLI: creates tunnels from local `host:port` targets and keeps a websocket control/data connection open to the server.
11
+
12
+ Long-term service split:
13
+
14
+ 1. Front-facing tunnel service.
15
+ 2. Client API/websocket service.
16
+ 3. BetterPortal management UI/API service.
17
+
18
+ Non-goals:
19
+
20
+ - Raw TCP tunnels.
21
+ - UDP tunnels.
22
+ - SSH/VNC/database protocol tunneling.
23
+ - General-purpose VPN or private network mesh.
24
+
25
+ ## Product Shape
26
+
27
+ Example CLI flow:
28
+
29
+ ```powershell
30
+ btunnel http 3300
31
+ btunnel http 127.0.0.1:3210
32
+ btunnel http 3000 --auth
33
+ ```
34
+
35
+ Example public URLs:
36
+
37
+ ```text
38
+ https://a1b2c3-port3300-127-0-0-1.tunnels.betterportal.dev
39
+ https://x9y8z7-port3210-devhost.tunnels.betterportal.dev
40
+ ```
41
+
42
+ The tunnel creator should bypass visitor verification from the same IP used to create or bring up the tunnel. Other visitors see a development-tunnel verification page first.
43
+
44
+ ## Framework
45
+
46
+ Use `better-service-base` as the service foundation.
47
+
48
+ Core plugins:
49
+
50
+ - `h3-web`: public HTTP/HTTPS edge, visitor verification, rate limiting, error pages.
51
+ - `h3-client-api`: tunnel creation, client websocket control plane, request routing, stream handling.
52
+
53
+ Use BSB's built-in streaming framework for request/response bodies. Do not hand-roll stream buffering except for tiny metadata frames.
54
+
55
+ All data entering or leaving any service boundary must be validated with AnyVali.
56
+
57
+ AnyVali validation applies to:
58
+
59
+ - Public HTTP requests.
60
+ - Client API requests.
61
+ - Websocket frames.
62
+ - CLI config files.
63
+ - Database DTOs where data crosses service/module boundaries.
64
+ - BetterPortal management APIs when added later.
65
+
66
+ ## Transport
67
+
68
+ Public side:
69
+
70
+ - HTTP/1.1 and HTTP/2 required.
71
+ - HTTP/3 desirable if the runtime/proxy supports it cleanly.
72
+ - HTTPS required for public tunnel hostnames.
73
+ - BetterTunnels is public internet-facing infrastructure, not hidden behind Cloudflare proxying.
74
+ - Default public domain is `tunnels.betterportal.dev`.
75
+ - Public tunnel hostnames use `*.tunnels.betterportal.dev`.
76
+
77
+ Client side:
78
+
79
+ - Websocket connection from CLI to server.
80
+ - One websocket per active tunnel for MVP.
81
+ - A CLI process may open multiple tunnel websockets from one `.bettertunnel` config.
82
+ - Requests are multiplexed over the tunnel websocket using request IDs.
83
+ - Each proxied request carries method, path, query, headers, body stream, and response stream.
84
+ - Client sends heartbeat every 30 seconds.
85
+ - Two missed heartbeats force disconnect and session cleanup.
86
+ - Request bodies stream immediately.
87
+ - If the server does not receive a client ACK for `request.start` within 5 seconds, return `502`.
88
+ - If the client disconnects mid-request, return `502`.
89
+ - Idle data timeout: 30 seconds for anonymous tunnels, 60 seconds for authenticated tunnels.
90
+ - Total request timeout: 60 seconds for anonymous tunnels, 5 minutes for authenticated tunnels.
91
+
92
+ Minimum frame types:
93
+
94
+ ```text
95
+ client.register
96
+ client.heartbeat
97
+ tunnel.create
98
+ tunnel.close
99
+ request.start
100
+ request.body
101
+ request.end
102
+ response.start
103
+ response.body
104
+ response.end
105
+ error
106
+ ```
107
+
108
+ ## Wildcard Domains And TLS
109
+
110
+ Wildcard certs are practical with ACME DNS-01.
111
+
112
+ Preferred path:
113
+
114
+ - ACME DNS-01 challenge.
115
+ - Wildcard cert for `*.tunnels.betterportal.dev`.
116
+ - Optional separate cert for apex/admin hostnames.
117
+
118
+ HTTP-01 cannot issue wildcard certificates, so DNS-01 is required.
119
+
120
+ ## Tunnel Lifecycle
121
+
122
+ Tunnel states:
123
+
124
+ ```text
125
+ created -> active -> draining -> closed
126
+ created -> expired
127
+ active -> disconnected -> reconnecting -> active
128
+ active -> disconnected -> expired
129
+ ```
130
+
131
+ Unauthenticated tunnels:
132
+
133
+ - Max lifetime: 6 hours.
134
+ - Lower request rate limit.
135
+ - Lower bandwidth/data transfer limit.
136
+ - Random subdomain only.
137
+ - Creator IP must pass Turnstile once before tunnel becomes public.
138
+
139
+ Registered tunnels:
140
+
141
+ - Higher limits.
142
+ - Longer sessions.
143
+ - Stable random subdomain for the same account/port/IP tuple within a 24 hour window.
144
+ - Authenticated users may set the 6 character prefix.
145
+ - Config-defined prefixes let a repo spin up stable preconfigured tunnels quickly.
146
+ - Turnstile only on tunnel creation/start unless risk checks trigger.
147
+
148
+ Prefix rule:
149
+
150
+ - Anonymous tunnels always ignore configured prefixes.
151
+ - Authenticated tunnels may use configured prefixes.
152
+ - If an authenticated configured prefix is unavailable or invalid, fail clearly instead of silently changing it.
153
+
154
+ ## Visitor Verification
155
+
156
+ Use Cloudflare Turnstile for first access from a visitor IP that is not the tunnel creator IP.
157
+
158
+ Turnstile hostnames cannot cover arbitrary wildcard tunnel hosts, so verification happens on a fixed verification host:
159
+
160
+ ```text
161
+ verify.tunnels.betterportal.dev
162
+ ```
163
+
164
+ Client websocket traffic uses:
165
+
166
+ ```text
167
+ connect.tunnels.betterportal.dev
168
+ ```
169
+
170
+ Verification policy:
171
+
172
+ - Creator IP: bypass while tunnel session is active.
173
+ - Free/anonymous tunnel visitor: redirect to `verify.tunnels.betterportal.dev`, show a server-controlled warning page with a 15 second wait, then Turnstile, then redirect back.
174
+ - Different visitor IP: validate independently.
175
+ - Visitor cookie must match the stored IP hash and user agent hash. Mismatch triggers validation again.
176
+ - Registered tunnel visitor: redirect to verification when needed; if the verification host already has a valid account/session cookie, auto-issue the tunnel validation and redirect back.
177
+
178
+ Verification redirect flow:
179
+
180
+ ```text
181
+ original tunnel URL
182
+ -> verify.tunnels.betterportal.dev?challenge=...
183
+ -> Turnstile / existing verification session
184
+ -> original tunnel URL with signed challenge response
185
+ -> server sets a tunnel-scoped validation cookie
186
+ ```
187
+
188
+ API callers may see multiple redirects on the first request. That is acceptable for MVP.
189
+
190
+ Verification state should be stored server-side with:
191
+
192
+ ```text
193
+ tunnel_id
194
+ visitor_ip_hash
195
+ validated_at
196
+ expires_at
197
+ user_agent_hash
198
+ validation_cookie_hash
199
+ ```
200
+
201
+ Do not store raw visitor IPs unless needed for abuse handling.
202
+
203
+ Anonymous CLI users still get a virtual account:
204
+
205
+ - First CLI use creates or reuses a local API key.
206
+ - The account is marked `anonymous`.
207
+ - Anonymous tunnels, sessions, limits, abuse reports, and verification history are linked to that virtual account.
208
+ - Promotion to a registered account can happen later without losing local tunnel history.
209
+
210
+ The warning page must clearly state:
211
+
212
+ - This is a development tunnel.
213
+ - The site is not Microsoft, Google, a bank, or any other implied third party.
214
+ - The visitor should only continue if they expected a development tunnel.
215
+ - A report-abuse action is available.
216
+
217
+ ## Abuse Controls
218
+
219
+ Per tunnel:
220
+
221
+ - Requests per minute.
222
+ - Concurrent requests.
223
+ - Response body bytes per minute.
224
+ - Total bytes over lifetime.
225
+ - Max request body size.
226
+ - Max response body size for anonymous tunnels.
227
+
228
+ Per account/session:
229
+
230
+ - Active tunnel count.
231
+ - New tunnel creation rate.
232
+ - Reserved subdomain count.
233
+
234
+ Per source IP:
235
+
236
+ - Verification attempts.
237
+ - Failed Turnstile attempts.
238
+ - Request rate before verification.
239
+
240
+ Do not block phishing-looking paths by default. Development apps often legitimately contain login, OAuth, and test payment flows.
241
+
242
+ ## Error Pages
243
+
244
+ Provide Cloudflare-style diagnostic pages with four layers:
245
+
246
+ ```text
247
+ Visitor -> BetterTunnels Edge -> Tunnel Client -> Local App
248
+ ```
249
+
250
+ Common statuses:
251
+
252
+ - `404`: no such tunnel/subdomain.
253
+ - `410`: tunnel expired.
254
+ - `429`: rate limited.
255
+ - `502`: tunnel client disconnected or local app refused connection.
256
+ - `504`: local app timed out.
257
+
258
+ The page should explain that this is a development tunnel and identify which layer failed without leaking private local addresses.
259
+
260
+ ## Routing
261
+
262
+ Incoming request routing key:
263
+
264
+ ```text
265
+ Host header -> tunnel subdomain -> active tunnel_id -> connected client session
266
+ ```
267
+
268
+ Optional subdomain hints:
269
+
270
+ ```text
271
+ {6chars}-port{port}-{ip-or-host}.domain
272
+ a1b2c3-port3300-127-0-0-1.domain
273
+ x9y8z7-port3210-devhost.domain
274
+ ```
275
+
276
+ Subdomain rules:
277
+
278
+ - Anonymous tunnels always get a fresh random 6 character prefix.
279
+ - Authenticated tunnels reuse the same 6 character prefix for the same account, port, and IP/host tuple for up to 24 hours.
280
+ - Custom subdomains are not part of the MVP.
281
+ - The database mapping is authoritative.
282
+
283
+ ## Persistence
284
+
285
+ Use PostgreSQL 18 for MVP data storage.
286
+
287
+ Use Prisma ORM for schema, migrations, and normal CRUD.
288
+
289
+ Use raw SQL only when Prisma is a bad fit, such as atomic usage counter increments or cleanup jobs.
290
+
291
+ Minimum tables:
292
+
293
+ ```text
294
+ accounts
295
+ client_sessions
296
+ tunnels
297
+ tunnel_names
298
+ visitor_validations
299
+ usage_counters
300
+ audit_events
301
+ ```
302
+
303
+ Anonymous users can be represented as ephemeral accounts or session-scoped identities.
304
+
305
+ Persistence stores durable/control data. Active websocket handles and in-flight request streams stay in process memory.
306
+
307
+ ## CLI
308
+
309
+ The MVP may use a Node development CLI while the protocol settles.
310
+
311
+ Final CLI should likely be Rust or Go so releases can compile cleanly for Windows, macOS, and Linux.
312
+
313
+ Commands:
314
+
315
+ ```powershell
316
+ btunnel login
317
+ btunnel logout
318
+ btunnel http <port>
319
+ btunnel http <host:port>
320
+ btunnel list
321
+ btunnel close <name-or-id>
322
+ btunnel status
323
+ ```
324
+
325
+ The CLI should:
326
+
327
+ - Print public URL.
328
+ - Keep running while tunnels are active.
329
+ - Reconnect automatically.
330
+ - Re-register active tunnels after reconnect.
331
+ - Show request logs, response statuses, tunnel TTLs, and local target errors during the session.
332
+ - Support client-side `Host` header override.
333
+ - Read defaults from `.bettertunnel`.
334
+
335
+ Example `.bettertunnel`:
336
+
337
+ ```toml
338
+ [[tunnels]]
339
+ port = 3300
340
+ prefix = "webdv1"
341
+
342
+ [[tunnels]]
343
+ host = "127.0.0.1"
344
+ port = 3210
345
+ prefix = "api001"
346
+ host_header = "localhost:3210"
347
+ ```
348
+
349
+ ## Admin
350
+
351
+ No admin portal in MVP.
352
+
353
+ Future admin/BP integration can cover:
354
+
355
+ - Active tunnels.
356
+ - Connected clients.
357
+ - Traffic usage.
358
+ - Rate-limit events.
359
+ - Verification failures.
360
+ - Abuse reports.
361
+
362
+ ## Security
363
+
364
+ Required:
365
+
366
+ - Validate and normalize forwarded headers.
367
+ - Strip hop-by-hop headers.
368
+ - Add `X-Forwarded-For`, `X-Forwarded-Proto`, `X-Forwarded-Host`.
369
+ - Prevent request smuggling with strict header/body parsing.
370
+ - Enforce max header size and body limits.
371
+ - Anonymous tunnels only allow a conservative header allowlist.
372
+ - Authenticated tunnels allow a broader header set with strict size limits.
373
+ - Anonymous tunnels strip incoming and outgoing `Cookie` / `Set-Cookie` headers.
374
+ - Tunnel responses cannot set broad domain cookies such as `.tunnels.betterportal.dev`.
375
+ - Tunnel responses cannot set cookies for sibling tunnel subdomains.
376
+ - BetterTunnels verification cookies are server-owned and may intentionally span verification flows.
377
+ - Preferred validation cookie model: verification host proves Turnstile/account state, then the original tunnel host sets a narrow, signed, tunnel-scoped cookie from a challenge response.
378
+ - Use signed tunnel/client session tokens.
379
+ - Hash visitor IPs at rest.
380
+
381
+ Forwarding rule:
382
+
383
+ The local app sees normal proxied HTTP requests. It must not receive server-only auth, Turnstile, or internal routing headers.
384
+
385
+ ## Scaling
386
+
387
+ Single-node first.
388
+
389
+ Multi-container later:
390
+
391
+ - Shared DB for tunnel/session state.
392
+ - Shared pub/sub for routing control messages.
393
+ - Sticky websocket sessions or a stream router that can reach the owning client connection.
394
+ - Stateless public edge workers can forward to the node that owns the client session.
395
+ - Use BSB/ABS logging, traces, spans, and metrics for operational visibility.
396
+
397
+ Do not build distributed routing until single-node behavior is stable.
398
+
399
+ ## Engineering Rules
400
+
401
+ Zero duct tape.
402
+
403
+ If something is not working:
404
+
405
+ 1. Stop.
406
+ 2. Analyze the actual failure.
407
+ 3. Write the resolution path.
408
+ 4. Implement the fix only after the cause is understood.
409
+
410
+ Do not force broken behavior to pass with retries, sleeps, hidden fallbacks, duplicated state, schema bypasses, broad catches, or one-off patches.
411
+
412
+ If the cause cannot be found quickly, stop and ask instead of layering workaround code.
413
+
414
+ ## Open Questions
415
+
416
+ - Whether account auth lives inside BetterTunnels or delegates to BetterPortal.
417
+ - Whether multi-tunnel CLI should keep one websocket per tunnel forever or later add a shared control socket.
418
+
419
+ ## Future BetterPortal Integration
420
+
421
+ BetterPortal lives separately at:
422
+
423
+ ```text
424
+ BetterPortal integration lives in the BetterPortal repository and is wired through the BP service plugin.
425
+ ```
426
+
427
+ Future integration ideas:
428
+
429
+ - BetterPortal cover/integration plugin.
430
+ - Let BP services expose development endpoints through BetterTunnels.
431
+ - Tie dev tunnels into a dev BP site.
432
+ - Allow externally accessible service paths or names managed from BP.
433
+ - Centralize account, team, billing, and service deployment flows in BP.
434
+
435
+ Do not build this in the MVP.
436
+
437
+ ## MVP
438
+
439
+ 1. Server accepts wildcard HTTPS requests.
440
+ 2. CLI opens websocket and registers one local HTTP target.
441
+ 3. Server routes public request to CLI and streams response back.
442
+ 4. Unknown visitor IP sees Turnstile first.
443
+ 5. Anonymous tunnel expires after 6 hours.
444
+ 6. Basic limits: request rate, concurrent requests, max body size.
445
+ 7. Diagnostic pages for no tunnel, disconnected client, local app failure.
446
+
447
+ ## Development Workflow
448
+
449
+ Build order:
450
+
451
+ 1. Create the BSB service shell.
452
+ 2. Add H3 runtime inside BSB service plugins only.
453
+ 3. Add `h3-client-api` with websocket tunnel registration.
454
+ 4. Add the CLI command that opens one websocket for one local port.
455
+ 5. Proxy one HTTP request end-to-end with streamed response body.
456
+ 6. Add request body streaming.
457
+ 7. Add wildcard host routing for `*.tunnels.betterportal.dev`.
458
+ 8. Add Turnstile/warning validation.
459
+ 9. Add limits and timeout enforcement.
460
+ 10. Add diagnostic pages.
461
+ 11. Add `.bettertunnel` multi-tunnel startup.
462
+
463
+ Server rule:
464
+
465
+ - No standalone server entrypoint.
466
+ - Server runtime starts through BSB plugin lifecycle.
467
+ - Client CLI may be standalone.
468
+
469
+ MVP local development flow:
470
+
471
+ ```powershell
472
+ btunnel http 3300
473
+ ```
474
+
475
+ Expected output:
476
+
477
+ ```text
478
+ Tunnel active
479
+ Local: http://127.0.0.1:3300
480
+ Public: https://a1b2c3-port3300-127-0-0-1.tunnels.betterportal.dev
481
+ TTL: 6h
482
+ ```
483
+
484
+ MVP repo config flow:
485
+
486
+ ```powershell
487
+ btunnel up
488
+ ```
489
+
490
+ Reads `.bettertunnel`, opens one websocket per tunnel, and prints all active URLs.
491
+
492
+ Anonymous `.bettertunnel` behavior:
493
+
494
+ - `prefix` is ignored.
495
+ - Fresh random 6 character prefix every session.
496
+ - 6 hour TTL.
497
+
498
+ Authenticated `.bettertunnel` behavior:
499
+
500
+ - `prefix` is honored if valid and available.
501
+ - Same prefix can be reused for fast repo startup.
502
+ - Higher limits and longer timeouts apply.
503
+ - Account plan controls feature flags such as IP-based verification, verification bypass, custom prefixes, limits, and retention.
504
+ - Paid/registered users can verify trusted public IPs for automation where browser cookies are not practical.
505
+
506
+ CLI self-hosting behavior:
507
+
508
+ - `btunnel host .` can serve a static directory and expose it publicly without a separate local server.
509
+ - `btunnel host --dev <command>` can run a local dev server and expose its selected port.
510
+ - HTTP websocket upgrades from visitors should pass through to the local target as websocket connections; no path-specific config required.
511
+ - Websocket passthrough must use BSB stream/event ownership between `service-tunnel-web` and `service-tunnel-client-api`; do not emulate it with buffered request events.
512
+
513
+ Smallest useful implementation slice:
514
+
515
+ 1. PostgreSQL 18.
516
+ 2. Prisma schema and migrations.
517
+ 3. No auth.
518
+ 4. In-memory active tunnel/session registry backed by persisted tunnel rows.
519
+ 5. One server process.
520
+ 6. One CLI process.
521
+ 7. One websocket per tunnel.
522
+ 8. One proxied request at a time, then add concurrency.
523
+
524
+ Do not persist in-flight streams or websocket state.