@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.
- package/README.md +76 -0
- package/SPEC.md +524 -0
- package/lib/.bsb/clients/service-tunnel-client-api.d.ts +133 -0
- package/lib/.bsb/clients/service-tunnel-client-api.d.ts.map +1 -0
- package/lib/.bsb/clients/service-tunnel-client-api.js +102 -0
- package/lib/.bsb/clients/service-tunnel-client-api.js.map +1 -0
- package/lib/.bsb/clients/service-tunnel-web.d.ts +43 -0
- package/lib/.bsb/clients/service-tunnel-web.d.ts.map +1 -0
- package/lib/.bsb/clients/service-tunnel-web.js +47 -0
- package/lib/.bsb/clients/service-tunnel-web.js.map +1 -0
- package/lib/auth.d.ts +12 -0
- package/lib/auth.d.ts.map +1 -0
- package/lib/auth.js +59 -0
- package/lib/auth.js.map +1 -0
- package/lib/cli.d.ts +2 -0
- package/lib/cli.d.ts.map +1 -0
- package/lib/cli.js +295 -0
- package/lib/cli.js.map +1 -0
- package/lib/plugins/service-betterportal/.bp-generated/registry.d.ts +3 -0
- package/lib/plugins/service-betterportal/.bp-generated/registry.d.ts.map +1 -0
- package/lib/plugins/service-betterportal/.bp-generated/registry.js +127 -0
- package/lib/plugins/service-betterportal/.bp-generated/registry.js.map +1 -0
- package/lib/plugins/service-betterportal/assets/brand/bettertunnels-horizontal-light.svg +18 -0
- package/lib/plugins/service-betterportal/assets/brand/bettertunnels-square-dark-1024x1024.png +0 -0
- package/lib/plugins/service-betterportal/assets/brand/favicon.svg +12 -0
- package/lib/plugins/service-betterportal/betterportal-logo.png +0 -0
- package/lib/plugins/service-betterportal/bp-routes/cli-auth/verify/_theme.bootstrap1/index.d.ts +5 -0
- package/lib/plugins/service-betterportal/bp-routes/cli-auth/verify/_theme.bootstrap1/index.d.ts.map +1 -0
- package/lib/plugins/service-betterportal/bp-routes/cli-auth/verify/_theme.bootstrap1/index.js +6 -0
- package/lib/plugins/service-betterportal/bp-routes/cli-auth/verify/_theme.bootstrap1/index.js.map +1 -0
- package/lib/plugins/service-betterportal/bp-routes/cli-auth/verify/index.d.ts +25 -0
- package/lib/plugins/service-betterportal/bp-routes/cli-auth/verify/index.d.ts.map +1 -0
- package/lib/plugins/service-betterportal/bp-routes/cli-auth/verify/index.js +59 -0
- package/lib/plugins/service-betterportal/bp-routes/cli-auth/verify/index.js.map +1 -0
- package/lib/plugins/service-betterportal/bp-routes/dashboard/_theme.bootstrap1/index.d.ts +5 -0
- package/lib/plugins/service-betterportal/bp-routes/dashboard/_theme.bootstrap1/index.d.ts.map +1 -0
- package/lib/plugins/service-betterportal/bp-routes/dashboard/_theme.bootstrap1/index.js +22 -0
- package/lib/plugins/service-betterportal/bp-routes/dashboard/_theme.bootstrap1/index.js.map +1 -0
- package/lib/plugins/service-betterportal/bp-routes/dashboard/index.d.ts +40 -0
- package/lib/plugins/service-betterportal/bp-routes/dashboard/index.d.ts.map +1 -0
- package/lib/plugins/service-betterportal/bp-routes/dashboard/index.js +57 -0
- package/lib/plugins/service-betterportal/bp-routes/dashboard/index.js.map +1 -0
- package/lib/plugins/service-betterportal/bp-routes/downloads/_theme.bootstrap1/index.d.ts +5 -0
- package/lib/plugins/service-betterportal/bp-routes/downloads/_theme.bootstrap1/index.d.ts.map +1 -0
- package/lib/plugins/service-betterportal/bp-routes/downloads/_theme.bootstrap1/index.js +5 -0
- package/lib/plugins/service-betterportal/bp-routes/downloads/_theme.bootstrap1/index.js.map +1 -0
- package/lib/plugins/service-betterportal/bp-routes/downloads/index.d.ts +30 -0
- package/lib/plugins/service-betterportal/bp-routes/downloads/index.d.ts.map +1 -0
- package/lib/plugins/service-betterportal/bp-routes/downloads/index.js +45 -0
- package/lib/plugins/service-betterportal/bp-routes/downloads/index.js.map +1 -0
- package/lib/plugins/service-betterportal/bp-routes/landing/_theme.bootstrap1/index.d.ts +5 -0
- package/lib/plugins/service-betterportal/bp-routes/landing/_theme.bootstrap1/index.d.ts.map +1 -0
- package/lib/plugins/service-betterportal/bp-routes/landing/_theme.bootstrap1/index.js +5 -0
- package/lib/plugins/service-betterportal/bp-routes/landing/_theme.bootstrap1/index.js.map +1 -0
- package/lib/plugins/service-betterportal/bp-routes/landing/index.d.ts +40 -0
- package/lib/plugins/service-betterportal/bp-routes/landing/index.d.ts.map +1 -0
- package/lib/plugins/service-betterportal/bp-routes/landing/index.js +63 -0
- package/lib/plugins/service-betterportal/bp-routes/landing/index.js.map +1 -0
- package/lib/plugins/service-betterportal/bp-routes/tunnels/_theme.bootstrap1/index.d.ts +5 -0
- package/lib/plugins/service-betterportal/bp-routes/tunnels/_theme.bootstrap1/index.d.ts.map +1 -0
- package/lib/plugins/service-betterportal/bp-routes/tunnels/_theme.bootstrap1/index.js +5 -0
- package/lib/plugins/service-betterportal/bp-routes/tunnels/_theme.bootstrap1/index.js.map +1 -0
- package/lib/plugins/service-betterportal/bp-routes/tunnels/index.d.ts +44 -0
- package/lib/plugins/service-betterportal/bp-routes/tunnels/index.d.ts.map +1 -0
- package/lib/plugins/service-betterportal/bp-routes/tunnels/index.js +59 -0
- package/lib/plugins/service-betterportal/bp-routes/tunnels/index.js.map +1 -0
- package/lib/plugins/service-betterportal/brand-assets.d.ts +7 -0
- package/lib/plugins/service-betterportal/brand-assets.d.ts.map +1 -0
- package/lib/plugins/service-betterportal/brand-assets.js +24 -0
- package/lib/plugins/service-betterportal/brand-assets.js.map +1 -0
- package/lib/plugins/service-betterportal/index.d.ts +57 -0
- package/lib/plugins/service-betterportal/index.d.ts.map +1 -0
- package/lib/plugins/service-betterportal/index.js +108 -0
- package/lib/plugins/service-betterportal/index.js.map +1 -0
- package/lib/plugins/service-tunnel-client-api/ids.d.ts +6 -0
- package/lib/plugins/service-tunnel-client-api/ids.d.ts.map +1 -0
- package/lib/plugins/service-tunnel-client-api/ids.js +17 -0
- package/lib/plugins/service-tunnel-client-api/ids.js.map +1 -0
- package/lib/plugins/service-tunnel-client-api/index.d.ts +158 -0
- package/lib/plugins/service-tunnel-client-api/index.d.ts.map +1 -0
- package/lib/plugins/service-tunnel-client-api/index.js +591 -0
- package/lib/plugins/service-tunnel-client-api/index.js.map +1 -0
- package/lib/plugins/service-tunnel-client-api/registry.d.ts +36 -0
- package/lib/plugins/service-tunnel-client-api/registry.d.ts.map +1 -0
- package/lib/plugins/service-tunnel-client-api/registry.js +14 -0
- package/lib/plugins/service-tunnel-client-api/registry.js.map +1 -0
- package/lib/plugins/service-tunnel-client-api/schemas.d.ts +66 -0
- package/lib/plugins/service-tunnel-client-api/schemas.d.ts.map +1 -0
- package/lib/plugins/service-tunnel-client-api/schemas.js +56 -0
- package/lib/plugins/service-tunnel-client-api/schemas.js.map +1 -0
- package/lib/plugins/service-tunnel-web/http.d.ts +4 -0
- package/lib/plugins/service-tunnel-web/http.d.ts.map +1 -0
- package/lib/plugins/service-tunnel-web/http.js +119 -0
- package/lib/plugins/service-tunnel-web/http.js.map +1 -0
- package/lib/plugins/service-tunnel-web/index.d.ts +79 -0
- package/lib/plugins/service-tunnel-web/index.d.ts.map +1 -0
- package/lib/plugins/service-tunnel-web/index.js +278 -0
- package/lib/plugins/service-tunnel-web/index.js.map +1 -0
- package/lib/plugins/service-tunnel-web/verification.d.ts +27 -0
- package/lib/plugins/service-tunnel-web/verification.d.ts.map +1 -0
- package/lib/plugins/service-tunnel-web/verification.js +209 -0
- package/lib/plugins/service-tunnel-web/verification.js.map +1 -0
- package/lib/prisma.d.ts +6 -0
- package/lib/prisma.d.ts.map +1 -0
- package/lib/prisma.js +10 -0
- package/lib/prisma.js.map +1 -0
- package/lib/schemas/service-betterportal.json +109 -0
- package/lib/schemas/service-tunnel-client-api.json +300 -0
- package/lib/schemas/service-tunnel-web.json +140 -0
- package/package.json +60 -0
- package/prisma/migrations/0001_init/migration.sql +63 -0
- package/prisma/migrations/0002_account_plan/migration.sql +6 -0
- package/prisma/migrations/0003_tunnel_owner_server/migration.sql +1 -0
- package/prisma/migrations/20260701000000_add_client_auth/migration.sql +46 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- 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.
|