@betterportal/tunnels 1.0.19 → 1.0.24

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 (152) hide show
  1. package/package.json +1 -1
  2. package/lib/.bsb/clients/service-tunnels-client.d.ts +0 -133
  3. package/lib/.bsb/clients/service-tunnels-client.d.ts.map +0 -1
  4. package/lib/.bsb/clients/service-tunnels-client.js +0 -102
  5. package/lib/.bsb/clients/service-tunnels-client.js.map +0 -1
  6. package/lib/.bsb/clients/service-tunnels-proxy.d.ts +0 -43
  7. package/lib/.bsb/clients/service-tunnels-proxy.d.ts.map +0 -1
  8. package/lib/.bsb/clients/service-tunnels-proxy.js +0 -47
  9. package/lib/.bsb/clients/service-tunnels-proxy.js.map +0 -1
  10. package/lib/auth.d.ts +0 -12
  11. package/lib/auth.d.ts.map +0 -1
  12. package/lib/auth.js +0 -59
  13. package/lib/auth.js.map +0 -1
  14. package/lib/cli.d.ts +0 -2
  15. package/lib/cli.d.ts.map +0 -1
  16. package/lib/cli.js +0 -406
  17. package/lib/cli.js.map +0 -1
  18. package/lib/generated/prisma/client.d.ts +0 -1
  19. package/lib/generated/prisma/client.js +0 -5
  20. package/lib/generated/prisma/default.d.ts +0 -1
  21. package/lib/generated/prisma/default.js +0 -5
  22. package/lib/generated/prisma/edge.d.ts +0 -1
  23. package/lib/generated/prisma/edge.js +0 -278
  24. package/lib/generated/prisma/index-browser.js +0 -304
  25. package/lib/generated/prisma/index.d.ts +0 -13885
  26. package/lib/generated/prisma/index.js +0 -278
  27. package/lib/generated/prisma/package.json +0 -144
  28. package/lib/generated/prisma/query_compiler_fast_bg.js +0 -2
  29. package/lib/generated/prisma/query_compiler_fast_bg.wasm +0 -0
  30. package/lib/generated/prisma/query_compiler_fast_bg.wasm-base64.js +0 -2
  31. package/lib/generated/prisma/runtime/client.d.ts +0 -3386
  32. package/lib/generated/prisma/runtime/client.js +0 -86
  33. package/lib/generated/prisma/runtime/index-browser.d.ts +0 -90
  34. package/lib/generated/prisma/runtime/index-browser.js +0 -6
  35. package/lib/generated/prisma/runtime/wasm-compiler-edge.js +0 -76
  36. package/lib/generated/prisma/schema.prisma +0 -142
  37. package/lib/generated/prisma/wasm-edge-light-loader.mjs +0 -5
  38. package/lib/generated/prisma/wasm-worker-loader.mjs +0 -5
  39. package/lib/plugins/service-tunnels-admin/.bp-generated/registry.d.ts +0 -3
  40. package/lib/plugins/service-tunnels-admin/.bp-generated/registry.d.ts.map +0 -1
  41. package/lib/plugins/service-tunnels-admin/.bp-generated/registry.js +0 -148
  42. package/lib/plugins/service-tunnels-admin/.bp-generated/registry.js.map +0 -1
  43. package/lib/plugins/service-tunnels-admin/assets/brand/bettertunnels-horizontal-light.svg +0 -18
  44. package/lib/plugins/service-tunnels-admin/assets/brand/bettertunnels-square-dark-1024x1024.png +0 -0
  45. package/lib/plugins/service-tunnels-admin/assets/brand/favicon.svg +0 -12
  46. package/lib/plugins/service-tunnels-admin/betterportal-logo.png +0 -0
  47. package/lib/plugins/service-tunnels-admin/bp-routes/cli-auth/verify/GET.d.ts +0 -20
  48. package/lib/plugins/service-tunnels-admin/bp-routes/cli-auth/verify/GET.d.ts.map +0 -1
  49. package/lib/plugins/service-tunnels-admin/bp-routes/cli-auth/verify/GET.js +0 -51
  50. package/lib/plugins/service-tunnels-admin/bp-routes/cli-auth/verify/GET.js.map +0 -1
  51. package/lib/plugins/service-tunnels-admin/bp-routes/cli-auth/verify/_theme.bootstrap1/GET.d.ts +0 -5
  52. package/lib/plugins/service-tunnels-admin/bp-routes/cli-auth/verify/_theme.bootstrap1/GET.d.ts.map +0 -1
  53. package/lib/plugins/service-tunnels-admin/bp-routes/cli-auth/verify/_theme.bootstrap1/GET.js +0 -6
  54. package/lib/plugins/service-tunnels-admin/bp-routes/cli-auth/verify/_theme.bootstrap1/GET.js.map +0 -1
  55. package/lib/plugins/service-tunnels-admin/bp-routes/cli-auth/verify/index.d.ts +0 -7
  56. package/lib/plugins/service-tunnels-admin/bp-routes/cli-auth/verify/index.d.ts.map +0 -1
  57. package/lib/plugins/service-tunnels-admin/bp-routes/cli-auth/verify/index.js +0 -12
  58. package/lib/plugins/service-tunnels-admin/bp-routes/cli-auth/verify/index.js.map +0 -1
  59. package/lib/plugins/service-tunnels-admin/bp-routes/dashboard/GET.d.ts +0 -35
  60. package/lib/plugins/service-tunnels-admin/bp-routes/dashboard/GET.d.ts.map +0 -1
  61. package/lib/plugins/service-tunnels-admin/bp-routes/dashboard/GET.js +0 -44
  62. package/lib/plugins/service-tunnels-admin/bp-routes/dashboard/GET.js.map +0 -1
  63. package/lib/plugins/service-tunnels-admin/bp-routes/dashboard/_theme.bootstrap1/GET.d.ts +0 -5
  64. package/lib/plugins/service-tunnels-admin/bp-routes/dashboard/_theme.bootstrap1/GET.d.ts.map +0 -1
  65. package/lib/plugins/service-tunnels-admin/bp-routes/dashboard/_theme.bootstrap1/GET.js +0 -22
  66. package/lib/plugins/service-tunnels-admin/bp-routes/dashboard/_theme.bootstrap1/GET.js.map +0 -1
  67. package/lib/plugins/service-tunnels-admin/bp-routes/dashboard/index.d.ts +0 -7
  68. package/lib/plugins/service-tunnels-admin/bp-routes/dashboard/index.d.ts.map +0 -1
  69. package/lib/plugins/service-tunnels-admin/bp-routes/dashboard/index.js +0 -14
  70. package/lib/plugins/service-tunnels-admin/bp-routes/dashboard/index.js.map +0 -1
  71. package/lib/plugins/service-tunnels-admin/bp-routes/downloads/GET.d.ts +0 -25
  72. package/lib/plugins/service-tunnels-admin/bp-routes/downloads/GET.d.ts.map +0 -1
  73. package/lib/plugins/service-tunnels-admin/bp-routes/downloads/GET.js +0 -34
  74. package/lib/plugins/service-tunnels-admin/bp-routes/downloads/GET.js.map +0 -1
  75. package/lib/plugins/service-tunnels-admin/bp-routes/downloads/_theme.bootstrap1/GET.d.ts +0 -5
  76. package/lib/plugins/service-tunnels-admin/bp-routes/downloads/_theme.bootstrap1/GET.d.ts.map +0 -1
  77. package/lib/plugins/service-tunnels-admin/bp-routes/downloads/_theme.bootstrap1/GET.js +0 -5
  78. package/lib/plugins/service-tunnels-admin/bp-routes/downloads/_theme.bootstrap1/GET.js.map +0 -1
  79. package/lib/plugins/service-tunnels-admin/bp-routes/downloads/index.d.ts +0 -7
  80. package/lib/plugins/service-tunnels-admin/bp-routes/downloads/index.d.ts.map +0 -1
  81. package/lib/plugins/service-tunnels-admin/bp-routes/downloads/index.js +0 -12
  82. package/lib/plugins/service-tunnels-admin/bp-routes/downloads/index.js.map +0 -1
  83. package/lib/plugins/service-tunnels-admin/bp-routes/landing/GET.d.ts +0 -35
  84. package/lib/plugins/service-tunnels-admin/bp-routes/landing/GET.d.ts.map +0 -1
  85. package/lib/plugins/service-tunnels-admin/bp-routes/landing/GET.js +0 -52
  86. package/lib/plugins/service-tunnels-admin/bp-routes/landing/GET.js.map +0 -1
  87. package/lib/plugins/service-tunnels-admin/bp-routes/landing/_theme.bootstrap1/GET.d.ts +0 -5
  88. package/lib/plugins/service-tunnels-admin/bp-routes/landing/_theme.bootstrap1/GET.d.ts.map +0 -1
  89. package/lib/plugins/service-tunnels-admin/bp-routes/landing/_theme.bootstrap1/GET.js +0 -5
  90. package/lib/plugins/service-tunnels-admin/bp-routes/landing/_theme.bootstrap1/GET.js.map +0 -1
  91. package/lib/plugins/service-tunnels-admin/bp-routes/landing/index.d.ts +0 -7
  92. package/lib/plugins/service-tunnels-admin/bp-routes/landing/index.d.ts.map +0 -1
  93. package/lib/plugins/service-tunnels-admin/bp-routes/landing/index.js +0 -12
  94. package/lib/plugins/service-tunnels-admin/bp-routes/landing/index.js.map +0 -1
  95. package/lib/plugins/service-tunnels-admin/bp-routes/tunnels/GET.d.ts +0 -39
  96. package/lib/plugins/service-tunnels-admin/bp-routes/tunnels/GET.d.ts.map +0 -1
  97. package/lib/plugins/service-tunnels-admin/bp-routes/tunnels/GET.js +0 -46
  98. package/lib/plugins/service-tunnels-admin/bp-routes/tunnels/GET.js.map +0 -1
  99. package/lib/plugins/service-tunnels-admin/bp-routes/tunnels/_theme.bootstrap1/GET.d.ts +0 -5
  100. package/lib/plugins/service-tunnels-admin/bp-routes/tunnels/_theme.bootstrap1/GET.d.ts.map +0 -1
  101. package/lib/plugins/service-tunnels-admin/bp-routes/tunnels/_theme.bootstrap1/GET.js +0 -5
  102. package/lib/plugins/service-tunnels-admin/bp-routes/tunnels/_theme.bootstrap1/GET.js.map +0 -1
  103. package/lib/plugins/service-tunnels-admin/bp-routes/tunnels/index.d.ts +0 -7
  104. package/lib/plugins/service-tunnels-admin/bp-routes/tunnels/index.d.ts.map +0 -1
  105. package/lib/plugins/service-tunnels-admin/bp-routes/tunnels/index.js +0 -14
  106. package/lib/plugins/service-tunnels-admin/bp-routes/tunnels/index.js.map +0 -1
  107. package/lib/plugins/service-tunnels-admin/brand-assets.d.ts +0 -7
  108. package/lib/plugins/service-tunnels-admin/brand-assets.d.ts.map +0 -1
  109. package/lib/plugins/service-tunnels-admin/brand-assets.js +0 -24
  110. package/lib/plugins/service-tunnels-admin/brand-assets.js.map +0 -1
  111. package/lib/plugins/service-tunnels-admin/index.d.ts +0 -64
  112. package/lib/plugins/service-tunnels-admin/index.d.ts.map +0 -1
  113. package/lib/plugins/service-tunnels-admin/index.js +0 -116
  114. package/lib/plugins/service-tunnels-admin/index.js.map +0 -1
  115. package/lib/plugins/service-tunnels-client/ids.d.ts +0 -6
  116. package/lib/plugins/service-tunnels-client/ids.d.ts.map +0 -1
  117. package/lib/plugins/service-tunnels-client/ids.js +0 -17
  118. package/lib/plugins/service-tunnels-client/ids.js.map +0 -1
  119. package/lib/plugins/service-tunnels-client/index.d.ts +0 -166
  120. package/lib/plugins/service-tunnels-client/index.d.ts.map +0 -1
  121. package/lib/plugins/service-tunnels-client/index.js +0 -698
  122. package/lib/plugins/service-tunnels-client/index.js.map +0 -1
  123. package/lib/plugins/service-tunnels-client/registry.d.ts +0 -36
  124. package/lib/plugins/service-tunnels-client/registry.d.ts.map +0 -1
  125. package/lib/plugins/service-tunnels-client/registry.js +0 -14
  126. package/lib/plugins/service-tunnels-client/registry.js.map +0 -1
  127. package/lib/plugins/service-tunnels-client/schemas.d.ts +0 -91
  128. package/lib/plugins/service-tunnels-client/schemas.d.ts.map +0 -1
  129. package/lib/plugins/service-tunnels-client/schemas.js +0 -73
  130. package/lib/plugins/service-tunnels-client/schemas.js.map +0 -1
  131. package/lib/plugins/service-tunnels-proxy/http.d.ts +0 -5
  132. package/lib/plugins/service-tunnels-proxy/http.d.ts.map +0 -1
  133. package/lib/plugins/service-tunnels-proxy/http.js +0 -125
  134. package/lib/plugins/service-tunnels-proxy/http.js.map +0 -1
  135. package/lib/plugins/service-tunnels-proxy/index.d.ts +0 -85
  136. package/lib/plugins/service-tunnels-proxy/index.d.ts.map +0 -1
  137. package/lib/plugins/service-tunnels-proxy/index.js +0 -288
  138. package/lib/plugins/service-tunnels-proxy/index.js.map +0 -1
  139. package/lib/plugins/service-tunnels-proxy/verification.d.ts +0 -31
  140. package/lib/plugins/service-tunnels-proxy/verification.d.ts.map +0 -1
  141. package/lib/plugins/service-tunnels-proxy/verification.js +0 -211
  142. package/lib/plugins/service-tunnels-proxy/verification.js.map +0 -1
  143. package/lib/prisma.d.ts +0 -7
  144. package/lib/prisma.d.ts.map +0 -1
  145. package/lib/prisma.js +0 -149
  146. package/lib/prisma.js.map +0 -1
  147. package/lib/schemas/service-tunnels-admin.json +0 -129
  148. package/lib/schemas/service-tunnels-admin.plugin.json +0 -140
  149. package/lib/schemas/service-tunnels-client.json +0 -320
  150. package/lib/schemas/service-tunnels-client.plugin.json +0 -97
  151. package/lib/schemas/service-tunnels-proxy.json +0 -160
  152. package/lib/schemas/service-tunnels-proxy.plugin.json +0 -123
@@ -1,698 +0,0 @@
1
- import { BSBService, bsb, optional, createConfigSchema, createEventSchemas, createFireAndForgetEvent, createReturnableEvent } from "@bsb/base";
2
- import * as av from "anyvali";
3
- import { createServer } from "node:http";
4
- import { readFile } from "node:fs/promises";
5
- import { join } from "node:path";
6
- import { H3, toNodeHandler } from "h3";
7
- import { WebSocketServer } from "ws";
8
- import { TunnelCreateSchema, ClientFrameSchema, AuthStartRequestSchema, AuthStartResponseSchema, AuthStatusResponseSchema } from "./schemas.js";
9
- import { buildTunnelSubdomain, hashValue, randomPrefix } from "./ids.js";
10
- import { prisma } from "../../prisma.js";
11
- import { initializePrisma } from "../../prisma.js";
12
- import { AUTH_SESSION_TTL_MS, DEVICE_TOKEN_TTL_MS, bearerToken, hashSecret, normalizeIpRange, randomToken, shortBrowserKey, verifySecret } from "../../auth.js";
13
- import { TunnelRegistry } from "./registry.js";
14
- import TunnelWebClient from "../../.bsb/clients/service-tunnels-proxy.js";
15
- export const Config = createConfigSchema({
16
- name: "service-tunnels-client",
17
- description: "Client websocket/API service",
18
- tags: ["service", "websocket", "tunnel"]
19
- }, av.object({
20
- database: av.object({
21
- connectionString: av.string().minLength(1).describe("PostgreSQL connection string")
22
- }, { unknownKeys: "strip" }).describe("Database configuration"),
23
- port: av.number().default(8081).describe("Client API listener port"),
24
- domain: av.string().default("tunnels.betterportal.dev").describe("Default public tunnel domain"),
25
- publicUrl: av.string().default("https://tunnels.betterportal.dev").describe("Public base URL"),
26
- authAppBaseUrl: av.string().default("https://betterportal.cloud").describe("BetterPortal app URL used for CLI auth"),
27
- authVerifyPath: av.string().default("/cli-auth/verify").describe("BetterPortal CLI auth verify route")
28
- }).describe("service-tunnels-client config"));
29
- export const EventSchemas = createEventSchemas({
30
- emitEvents: {},
31
- onEvents: {
32
- "ws.toOrigin": createFireAndForgetEvent(bsb.object({
33
- publicServerId: bsb.string({ description: "Web service instance that owns the public websocket" }),
34
- publicSocketId: bsb.string({ description: "Web service local websocket id" }),
35
- hostname: bsb.string({ description: "Public tunnel hostname" }),
36
- path: bsb.string({ description: "Websocket path and query" }),
37
- frameType: bsb.string({ description: "msg, ack, or event" }),
38
- event: optional(bsb.string({ description: "open or close" })),
39
- body: optional(bsb.string({ description: "Base64 websocket payload" }))
40
- }, "Public websocket frame to origin"), "Relay a public websocket frame to the connected tunnel client"),
41
- "proxy.metrics": createFireAndForgetEvent(bsb.object({
42
- hostname: bsb.string({ description: "Public tunnel hostname" }),
43
- requestId: bsb.string({ description: "Request id" }),
44
- totalMs: bsb.number({ description: "Public request total duration" }),
45
- clientApiRoundtripMs: optional(bsb.number({ description: "Client API to CLI roundtrip duration" })),
46
- cliOverheadMs: optional(bsb.number({ description: "CLI work outside origin read duration" })),
47
- originMs: optional(bsb.number({ description: "CLI to origin/read duration" })),
48
- internalServerMs: optional(bsb.number({ description: "Remaining server-side duration" }))
49
- }, "Proxy request metrics"), "Send final proxy request timings to the connected tunnel client")
50
- },
51
- emitReturnableEvents: {},
52
- onReturnableEvents: {
53
- "proxy.request": createReturnableEvent(bsb.object({
54
- hostname: bsb.string({ description: "Public request hostname" }),
55
- method: bsb.string({ description: "HTTP method" }),
56
- path: bsb.string({ description: "Path and query" }),
57
- headers: bsb.record(bsb.string({ description: "Header name" }), bsb.string({ description: "Header value" }), "Headers"),
58
- body: optional(bsb.string({ description: "Base64 encoded request body" })),
59
- webStartedAt: bsb.number({ description: "Web service request start timestamp" })
60
- }, "Proxy request"), bsb.object({
61
- requestId: bsb.string({ description: "Request id" }),
62
- ownerServerId: bsb.string({ description: "Client API server id" }),
63
- status: bsb.number({ description: "HTTP status" }),
64
- headers: bsb.record(bsb.string({ description: "Header name" }), bsb.string({ description: "Header value" }), "Headers"),
65
- body: bsb.string({ description: "Base64 encoded response body" }),
66
- webStartedAt: bsb.number({ description: "Web service request start timestamp" }),
67
- clientApiRoundtripMs: bsb.number({ description: "Client API to CLI roundtrip duration" }),
68
- cliOverheadMs: optional(bsb.number({ description: "CLI work outside origin read duration" })),
69
- originMs: optional(bsb.number({ description: "Origin/read duration" }))
70
- }, "Proxy response"), "Proxy an HTTP request through a connected tunnel client", 60)
71
- },
72
- emitBroadcast: {},
73
- onBroadcast: {}
74
- });
75
- export class Plugin extends BSBService {
76
- static Config = Config;
77
- static EventSchemas = EventSchemas;
78
- initBeforePlugins = [];
79
- initAfterPlugins = [];
80
- runBeforePlugins = [];
81
- runAfterPlugins = [];
82
- registry = new TunnelRegistry();
83
- app;
84
- server;
85
- wss;
86
- web;
87
- pluginPackageVersion = "0.0.0";
88
- constructor(cfg) {
89
- super(cfg);
90
- this.app = new H3();
91
- this.server = createServer(toNodeHandler(this.app));
92
- this.wss = new WebSocketServer({ noServer: true });
93
- this.web = new TunnelWebClient(this);
94
- }
95
- async init(obs) {
96
- await initializePrisma(this.config.database.connectionString, obs);
97
- this.pluginPackageVersion = await loadPluginPackageVersion(this.packageCwd);
98
- obs.log.info("init {plugin}", { plugin: this.pluginName });
99
- await this.events.onReturnableEvent("proxy.request", obs, async (_handlerObs, input) => {
100
- _handlerObs.log.info("CLIENT API proxy.request {method} {hostname}{path}", {
101
- method: input.method,
102
- hostname: input.hostname,
103
- path: input.path
104
- });
105
- const tunnel = this.registry.get(input.hostname);
106
- const unavailable = {
107
- requestId: crypto.randomUUID(),
108
- ownerServerId: this.appId,
109
- status: 503,
110
- headers: {},
111
- body: Buffer.from("Tunnel unavailable").toString("base64"),
112
- webStartedAt: input.webStartedAt,
113
- clientApiRoundtripMs: 0
114
- };
115
- if (!tunnel)
116
- return unavailable;
117
- if (tunnel.ws.readyState !== tunnel.ws.OPEN)
118
- return unavailable;
119
- return this.proxyRequest(tunnel, input.method, input.path, input.headers, input.body ?? "", input.webStartedAt);
120
- });
121
- await this.events.onEventSpecific("proxy.metrics", this.appId, obs, async (_handlerObs, input) => {
122
- const tunnel = this.registry.get(input.hostname);
123
- if (!tunnel || tunnel.ws.readyState !== tunnel.ws.OPEN)
124
- return;
125
- tunnel.ws.send(JSON.stringify({
126
- type: "request.metrics",
127
- requestId: input.requestId,
128
- totalMs: input.totalMs,
129
- clientApiRoundtripMs: input.clientApiRoundtripMs,
130
- cliOverheadMs: input.cliOverheadMs,
131
- originMs: input.originMs,
132
- internalServerMs: input.internalServerMs
133
- }));
134
- });
135
- await this.events.onEventSpecific("ws.toOrigin", this.appId, obs, async (_handlerObs, input) => {
136
- const tunnel = this.registry.get(input.hostname);
137
- if (!tunnel || tunnel.ws.readyState !== tunnel.ws.OPEN) {
138
- await this.web.wsFromOriginSpecific(input.publicServerId, _handlerObs, {
139
- publicSocketId: input.publicSocketId,
140
- frameType: "event",
141
- event: "close"
142
- });
143
- return;
144
- }
145
- tunnel.ws.send(JSON.stringify({
146
- type: "ws.toOrigin",
147
- publicServerId: input.publicServerId,
148
- publicSocketId: input.publicSocketId,
149
- path: input.path,
150
- frameType: input.frameType,
151
- message: input.event,
152
- body: input.body
153
- }));
154
- });
155
- const domain = this.config.domain;
156
- const publicUrl = this.config.publicUrl;
157
- this.app.get("/health", () => new Response("ok\n", {
158
- headers: {
159
- "content-type": "text/plain; charset=utf-8",
160
- "x-bettertunnels-version": this.pluginPackageVersion
161
- }
162
- }));
163
- this.app.get("/api/client/status", async (event) => {
164
- const token = bearerToken(event.req.headers);
165
- const auth = await validateDeviceToken(token, proxyClientIpFromHeaders(event.req.headers), event.req.headers.get("user-agent") ?? "");
166
- if (!auth)
167
- return json({ status: "unauthenticated" }, 401);
168
- return json({
169
- status: "authenticated",
170
- serverVersion: this.pluginPackageVersion,
171
- tenantId: auth.tenantId,
172
- bpUserSubject: auth.bpUserSubject,
173
- tokenExpiresAt: auth.expiresAt.toISOString(),
174
- limits: {
175
- tunnelTtlHours: 24,
176
- requestTimeoutSeconds: 300,
177
- idleTimeoutSeconds: 60,
178
- customPrefixes: true
179
- }
180
- });
181
- });
182
- this.app.post("/api/client/auth/start", async (event) => {
183
- const body = AuthStartRequestSchema.parse(await safeJson(event.req));
184
- const browserKey = shortBrowserKey();
185
- const pollSecret = randomToken();
186
- const expiresAt = new Date(Date.now() + AUTH_SESSION_TTL_MS);
187
- const authBase = body.authAppBaseUrl ?? this.config.authAppBaseUrl;
188
- const browserUrl = buildAuthUrl(authBase, this.config.authVerifyPath, browserKey);
189
- const clientIp = proxyClientIpFromHeaders(event.req.headers);
190
- const session = await prisma.clientAuthSession.create({
191
- data: {
192
- browserKeyHash: hashSecret(browserKey),
193
- pollSecretHash: hashSecret(pollSecret),
194
- clientIpHash: hashSecret(clientIp),
195
- userAgentHash: hashSecret(event.req.headers.get("user-agent") ?? ""),
196
- expiresAt
197
- }
198
- });
199
- const url = new URL(browserUrl);
200
- url.searchParams.set("session", session.id);
201
- url.searchParams.set("key", browserKey);
202
- return json(AuthStartResponseSchema.parse({
203
- sessionId: session.id,
204
- pollSecret,
205
- browserUrl: url.toString(),
206
- expiresAt: expiresAt.toISOString()
207
- }));
208
- });
209
- this.app.get("/api/client/auth/status", async (event) => {
210
- const url = new URL(event.req.url);
211
- const sessionId = url.searchParams.get("sessionId") ?? "";
212
- const pollSecret = bearerToken(event.req.headers) ?? "";
213
- if (!sessionId || !pollSecret) {
214
- return json(AuthStatusResponseSchema.parse({ status: "invalid", message: "Missing auth session credentials." }), 401);
215
- }
216
- const session = await prisma.clientAuthSession.findUnique({ where: { id: sessionId } });
217
- if (!session || !verifySecret(pollSecret, session.pollSecretHash)) {
218
- return json(AuthStatusResponseSchema.parse({ status: "invalid", message: "Invalid auth session." }), 401);
219
- }
220
- if (session.expiresAt <= new Date() && session.status === "pending") {
221
- await prisma.clientAuthSession.update({ where: { id: session.id }, data: { status: "expired" } });
222
- return json(AuthStatusResponseSchema.parse({ status: "expired", message: "Authentication session expired." }), 410);
223
- }
224
- if (session.status === "pending") {
225
- return json(AuthStatusResponseSchema.parse({ status: "pending" }));
226
- }
227
- if (session.status !== "approved" || !session.tenantId || !session.bpUserSubject) {
228
- return json(AuthStatusResponseSchema.parse({ status: session.status, message: "Authentication session is not approved." }), 409);
229
- }
230
- const clientIp = proxyClientIpFromHeaders(event.req.headers);
231
- const ipRange = normalizeIpRange(clientIp);
232
- if (!ipRange) {
233
- return json(AuthStatusResponseSchema.parse({ status: "invalid", message: "Unable to bind token to public IP range." }), 400);
234
- }
235
- const deviceToken = `bt_${randomToken(32)}`;
236
- const tokenExpiresAt = new Date(Date.now() + DEVICE_TOKEN_TTL_MS);
237
- await prisma.$transaction([
238
- prisma.clientDeviceToken.create({
239
- data: {
240
- tenantId: session.tenantId,
241
- bpUserSubject: session.bpUserSubject,
242
- tokenHash: hashSecret(deviceToken),
243
- name: "BetterTunnels CLI",
244
- ipRangeHash: hashSecret(ipRange.cidr),
245
- ipFamily: ipRange.family,
246
- userAgentHash: hashSecret(event.req.headers.get("user-agent") ?? ""),
247
- expiresAt: tokenExpiresAt
248
- }
249
- }),
250
- prisma.clientAuthSession.update({
251
- where: { id: session.id },
252
- data: { status: "consumed", consumedAt: new Date() }
253
- })
254
- ]);
255
- return json(AuthStatusResponseSchema.parse({
256
- status: "approved",
257
- token: deviceToken,
258
- expiresAt: tokenExpiresAt.toISOString(),
259
- tenantId: session.tenantId,
260
- bpUserSubject: session.bpUserSubject,
261
- bpUserEmail: session.bpUserEmail ?? undefined
262
- }));
263
- });
264
- this.server.on("upgrade", (request, socket, head) => {
265
- const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`);
266
- if (url.pathname !== "/api/client/ws") {
267
- socket.destroy();
268
- return;
269
- }
270
- this.wss.handleUpgrade(request, socket, head, (ws) => {
271
- obs.log.info("CLIENT WS upgrade {path}", { path: url.pathname });
272
- void this.handleTunnelSocket(ws, url, request.headers["user-agent"] ?? "", proxyClientIp(request.headers), domain, publicUrl, obs);
273
- });
274
- });
275
- }
276
- async run(obs) {
277
- const port = this.config.port;
278
- await new Promise((resolve) => {
279
- this.server.listen(port, "0.0.0.0", () => resolve());
280
- });
281
- obs.log.info(`client-api listening on port ${port}`);
282
- }
283
- async dispose() {
284
- this.wss.close();
285
- await new Promise((resolve, reject) => {
286
- this.server.close((error) => error ? reject(error) : resolve());
287
- });
288
- }
289
- async handleTunnelSocket(ws, url, userAgent, clientIp, domain, publicUrl, obs) {
290
- const input = TunnelCreateSchema.parse({
291
- sessionId: url.searchParams.get("sessionId") ?? "",
292
- targetHost: url.searchParams.get("targetHost") ?? "127.0.0.1",
293
- targetPort: Number(url.searchParams.get("targetPort") ?? "0"),
294
- clientVersion: url.searchParams.get("clientVersion") ?? undefined,
295
- authenticated: url.searchParams.get("authenticated") === "true",
296
- prefix: url.searchParams.get("prefix") ?? undefined,
297
- validation: url.searchParams.get("validation") ?? undefined,
298
- token: url.searchParams.get("token") ?? undefined
299
- });
300
- const versionIssue = incompatibleMajor(input.clientVersion, this.pluginPackageVersion);
301
- if (versionIssue) {
302
- ws.close(1008, versionIssue);
303
- obs.log.warn("CLIENT SESSION rejected incompatible version client={clientVersion} server={serverVersion} from {clientIp}", {
304
- clientVersion: input.clientVersion ?? "unknown",
305
- serverVersion: this.pluginPackageVersion,
306
- clientIp
307
- });
308
- return;
309
- }
310
- const authContext = input.authenticated
311
- ? await validateDeviceToken(input.token, clientIp, Array.isArray(userAgent) ? userAgent.join(" ") : userAgent)
312
- : undefined;
313
- if (input.authenticated && !authContext) {
314
- ws.close(1008, "reauth required");
315
- obs.log.warn("CLIENT SESSION rejected authenticated tunnel from {clientIp}", { clientIp });
316
- return;
317
- }
318
- const authenticated = !!authContext;
319
- const validation = authenticated && input.validation === "ip" ? "ip" : "cookie";
320
- let subdomain;
321
- if (!(authenticated && input.prefix)) {
322
- // Reuse the prior subdomain for this session so reconnects keep the same public URL.
323
- const prior = await prisma.tunnel.findFirst({
324
- where: {
325
- sessionId: input.sessionId,
326
- targetHost: input.targetHost,
327
- targetPort: input.targetPort,
328
- expiresAt: { gt: new Date() }
329
- },
330
- orderBy: { expiresAt: "desc" }
331
- });
332
- subdomain = prior?.subdomain;
333
- }
334
- if (!subdomain) {
335
- const prefix = authenticated && input.prefix ? input.prefix : randomPrefix();
336
- subdomain = buildTunnelSubdomain(prefix, input.targetPort, clientIp);
337
- }
338
- const expiresAt = new Date(Date.now() + (authenticated ? 24 * 60 * 60 * 1000 : 6 * 60 * 60 * 1000));
339
- const sessionObs = this.createTrace("bt.client.session", {
340
- "bt.client.session_id": input.sessionId,
341
- "bt.tunnel.subdomain": subdomain,
342
- "bt.tunnel.authenticated": authenticated,
343
- "client.address": clientIp
344
- });
345
- sessionObs.log.info("CLIENT SESSION connect {subdomain} from {clientIp}", {
346
- subdomain,
347
- clientIp
348
- });
349
- const session = await prisma.clientSession.upsert({
350
- where: { id: input.sessionId },
351
- create: {
352
- id: input.sessionId,
353
- accountId: authContext?.accountId,
354
- userAgent: Array.isArray(userAgent) ? userAgent.join(" ") : userAgent,
355
- ipHash: hashValue(clientIp),
356
- expiresAt
357
- },
358
- update: { expiresAt }
359
- });
360
- const tunnelRow = await prisma.tunnel.upsert({
361
- where: { subdomain },
362
- create: {
363
- sessionId: session.id,
364
- subdomain,
365
- ownerServerId: this.appId,
366
- targetHost: input.targetHost,
367
- targetPort: input.targetPort,
368
- authenticated,
369
- validation,
370
- accountId: authContext?.accountId,
371
- status: "active",
372
- expiresAt
373
- },
374
- update: {
375
- sessionId: session.id,
376
- ownerServerId: this.appId,
377
- targetHost: input.targetHost,
378
- targetPort: input.targetPort,
379
- authenticated,
380
- validation,
381
- accountId: authContext?.accountId,
382
- status: "active",
383
- expiresAt
384
- }
385
- });
386
- const tunnel = {
387
- id: tunnelRow.id,
388
- subdomain,
389
- targetHost: input.targetHost,
390
- targetPort: input.targetPort,
391
- authenticated,
392
- expiresAt,
393
- ws,
394
- pending: new Map(),
395
- sessionObs
396
- };
397
- this.registry.set(tunnel);
398
- sessionObs.log.info("CLIENT TUNNEL active {subdomain} -> {host}:{port}", {
399
- subdomain,
400
- host: input.targetHost,
401
- port: input.targetPort
402
- });
403
- ws.send(JSON.stringify({
404
- type: "tunnel.ready",
405
- publicUrl: publicUrl.replace(/\/$/, "").replace(`://${domain}`, `://${subdomain}.${domain}`),
406
- subdomain,
407
- expiresAt: expiresAt.toISOString(),
408
- serverVersion: this.pluginPackageVersion
409
- }));
410
- ws.on("message", (raw) => this.handleClientFrame(tunnel, raw.toString()));
411
- ws.on("close", (code, reason) => {
412
- sessionObs.log.warn("CLIENT TUNNEL disconnected {subdomain}", { subdomain });
413
- for (const [requestId, pending] of tunnel.pending.entries()) {
414
- this.failPending(tunnel, requestId, pending, new Error("Tunnel client disconnected."));
415
- }
416
- // A reconnect may already own this subdomain; only tear down if this socket is still the registered one.
417
- if (this.registry.get(subdomain)?.ws === ws) {
418
- this.registry.delete(subdomain);
419
- void prisma.tunnel.update({ where: { id: tunnel.id }, data: { status: "disconnected" } }).catch(() => undefined);
420
- }
421
- sessionObs.end({ "bt.client.disconnect": true });
422
- sessionObs.log.warn("CLIENT TUNNEL socket closed {subdomain} code={code} reason={reason}", {
423
- subdomain,
424
- code,
425
- reason: reason.toString()
426
- });
427
- });
428
- ws.on("error", (error) => {
429
- sessionObs.log.error("CLIENT TUNNEL socket error {subdomain}: {message}", {
430
- subdomain,
431
- message: error.message
432
- });
433
- });
434
- }
435
- async proxyRequest(tunnel, method, path, headers, body, webStartedAt) {
436
- const requestId = crypto.randomUUID();
437
- const clientApiSentAt = Date.now();
438
- const timeoutMs = tunnel.authenticated ? 5 * 60 * 1000 : 60 * 1000;
439
- const idleTimeoutMs = tunnel.authenticated ? 60 * 1000 : 30 * 1000;
440
- const requestObs = tunnel.sessionObs.startSpan("bt.client.proxy", {
441
- "bt.tunnel.id": tunnel.id,
442
- "bt.tunnel.subdomain": tunnel.subdomain,
443
- "http.request.method": method,
444
- "url.path": path
445
- });
446
- requestObs.log.info("CLIENT PROXY start {method} {path} via {subdomain}", {
447
- method,
448
- path,
449
- subdomain: tunnel.subdomain
450
- });
451
- const responsePromise = new Promise((resolve, reject) => {
452
- const pending = {
453
- resolve: (response) => {
454
- void response.arrayBuffer().then((arrayBuffer) => {
455
- const responseHeaders = {};
456
- response.headers.forEach((value, key) => {
457
- responseHeaders[key] = value;
458
- });
459
- const output = {
460
- requestId,
461
- ownerServerId: this.appId,
462
- status: response.status,
463
- headers: responseHeaders,
464
- body: Buffer.from(arrayBuffer).toString("base64"),
465
- webStartedAt: pending.webStartedAt,
466
- clientApiRoundtripMs: pending.clientApiRoundtripMs ?? Date.now() - pending.clientApiSentAt,
467
- cliOverheadMs: pending.cliOverheadMs,
468
- originMs: pending.originMs
469
- };
470
- requestObs.log.info("CLIENT PROXY complete {method} {path} -> {status}", {
471
- method,
472
- path,
473
- status: response.status
474
- });
475
- requestObs.end({ "http.response.status_code": response.status });
476
- resolve(output);
477
- }).catch(reject);
478
- },
479
- reject: (error) => {
480
- requestObs.error(error);
481
- requestObs.end({ "status": "failed" });
482
- reject(error);
483
- },
484
- chunks: [],
485
- webStartedAt,
486
- clientApiSentAt,
487
- totalTimer: setTimeout(() => this.failPending(tunnel, requestId, pending, new Error("Local app timed out.")), timeoutMs),
488
- firstByteTimer: setTimeout(() => this.failPending(tunnel, requestId, pending, new Error("Local app did not acknowledge request within 5s.")), 5 * 1000),
489
- idleTimeoutMs
490
- };
491
- tunnel.pending.set(requestId, pending);
492
- });
493
- tunnel.ws.send(JSON.stringify({
494
- type: "request.start",
495
- requestId,
496
- method,
497
- path,
498
- headers,
499
- body,
500
- webStartedAt,
501
- clientApiSentAt
502
- }));
503
- return responsePromise;
504
- }
505
- handleClientFrame(tunnel, raw) {
506
- let frame;
507
- try {
508
- frame = ClientFrameSchema.parse(JSON.parse(raw));
509
- }
510
- catch (error) {
511
- tunnel.sessionObs.error(error instanceof Error ? error : new Error(String(error)));
512
- tunnel.ws.close(1002, "invalid frame");
513
- return;
514
- }
515
- if (frame.type === "ws.fromOrigin" && frame.publicServerId && frame.publicSocketId && frame.frameType) {
516
- void this.web.wsFromOriginSpecific(frame.publicServerId, tunnel.sessionObs, {
517
- publicSocketId: frame.publicSocketId,
518
- frameType: frame.frameType,
519
- event: frame.message,
520
- body: frame.body
521
- });
522
- return;
523
- }
524
- if (!frame.requestId)
525
- return;
526
- const pending = tunnel.pending.get(frame.requestId);
527
- if (!pending)
528
- return;
529
- if (frame.type === "response.start") {
530
- clearTimeout(pending.firstByteTimer);
531
- this.resetIdleTimer(tunnel, frame.requestId, pending);
532
- pending.status = frame.status ?? 200;
533
- pending.headers = frame.headers ?? {};
534
- return;
535
- }
536
- if (frame.type === "response.body" && frame.body) {
537
- this.resetIdleTimer(tunnel, frame.requestId, pending);
538
- pending.chunks.push(frame.body);
539
- return;
540
- }
541
- if (frame.type === "response.end") {
542
- pending.clientApiRoundtripMs = Date.now() - pending.clientApiSentAt;
543
- pending.cliOverheadMs = frame.cliOverheadMs;
544
- pending.originMs = frame.originMs;
545
- this.clearPendingTimers(pending);
546
- tunnel.pending.delete(frame.requestId);
547
- const buffer = Buffer.concat(pending.chunks.map((chunk) => Buffer.from(chunk, "base64")));
548
- try {
549
- pending.resolve(buildProxyResponse(buffer, pending.status ?? 200, stripResponseHeaders(pending.headers ?? {})));
550
- }
551
- catch (error) {
552
- pending.reject(error instanceof Error ? error : new Error(String(error)));
553
- }
554
- return;
555
- }
556
- if (frame.type === "error") {
557
- this.failPending(tunnel, frame.requestId, pending, new Error(frame.message ?? "Tunnel client error."));
558
- }
559
- }
560
- resetIdleTimer(tunnel, requestId, pending) {
561
- if (pending.idleTimer)
562
- clearTimeout(pending.idleTimer);
563
- pending.idleTimer = setTimeout(() => {
564
- this.failPending(tunnel, requestId, pending, new Error("Local app response stalled."));
565
- }, pending.idleTimeoutMs);
566
- }
567
- failPending(tunnel, requestId, pending, error) {
568
- if (!tunnel.pending.has(requestId))
569
- return;
570
- this.clearPendingTimers(pending);
571
- tunnel.pending.delete(requestId);
572
- pending.reject(error);
573
- }
574
- clearPendingTimers(pending) {
575
- clearTimeout(pending.totalTimer);
576
- clearTimeout(pending.firstByteTimer);
577
- if (pending.idleTimer)
578
- clearTimeout(pending.idleTimer);
579
- }
580
- }
581
- function incompatibleMajor(clientVersion, serverVersion) {
582
- const client = majorVersion(clientVersion);
583
- const server = majorVersion(serverVersion);
584
- if (client === undefined || server === undefined)
585
- return undefined;
586
- if (client !== server) {
587
- return `BetterTunnels CLI major version ${client} is incompatible with server major version ${server}. Update the CLI.`;
588
- }
589
- return undefined;
590
- }
591
- function majorVersion(value) {
592
- const normalized = value?.trim().replace(/^v/, "");
593
- if (!normalized || normalized === "dev")
594
- return undefined;
595
- const major = Number.parseInt(normalized.split(".")[0] ?? "", 10);
596
- return Number.isFinite(major) ? major : undefined;
597
- }
598
- async function safeJson(request) {
599
- try {
600
- return await request.json();
601
- }
602
- catch {
603
- return {};
604
- }
605
- }
606
- async function loadPluginPackageVersion(packageCwd) {
607
- try {
608
- const raw = await readFile(join(packageCwd, "package.json"), "utf8");
609
- const parsed = JSON.parse(raw);
610
- return typeof parsed.version === "string" && parsed.version ? parsed.version : "0.0.0";
611
- }
612
- catch {
613
- return "0.0.0";
614
- }
615
- }
616
- // Response() throws on null-body statuses with a body, and on statuses outside 200-599.
617
- const NULL_BODY_STATUSES = new Set([101, 204, 205, 304]);
618
- export function buildProxyResponse(body, status, headers) {
619
- const safeStatus = status >= 200 && status <= 599 ? status : 502;
620
- return new Response(NULL_BODY_STATUSES.has(safeStatus) ? null : new Uint8Array(body), { status: safeStatus, headers });
621
- }
622
- function json(value, status = 200) {
623
- return new Response(JSON.stringify(value), {
624
- status,
625
- headers: { "content-type": "application/json; charset=utf-8" }
626
- });
627
- }
628
- function buildAuthUrl(base, path, browserKey) {
629
- const url = new URL(path.startsWith("/") ? path : `/${path}`, base);
630
- url.searchParams.set("key", browserKey);
631
- return url.toString();
632
- }
633
- async function validateDeviceToken(token, clientIp, userAgent) {
634
- if (!token)
635
- return undefined;
636
- const ipRange = normalizeIpRange(clientIp);
637
- if (!ipRange)
638
- return undefined;
639
- const row = await prisma.clientDeviceToken.findUnique({
640
- where: { tokenHash: hashSecret(token) }
641
- });
642
- if (!row || row.revokedAt || row.expiresAt <= new Date())
643
- return undefined;
644
- if (row.ipFamily !== ipRange.family || row.ipRangeHash !== hashSecret(ipRange.cidr))
645
- return undefined;
646
- if (row.userAgentHash && row.userAgentHash !== hashSecret(userAgent))
647
- return undefined;
648
- await prisma.clientDeviceToken.update({
649
- where: { id: row.id },
650
- data: { lastUsedAt: new Date() }
651
- });
652
- return {
653
- accountId: row.accountId ?? undefined,
654
- tenantId: row.tenantId,
655
- bpUserSubject: row.bpUserSubject,
656
- expiresAt: row.expiresAt
657
- };
658
- }
659
- function proxyClientIp(headers) {
660
- const forwardedFor = firstPublicForwardedIp(firstHeader(headers["x-forwarded-for"]));
661
- if (forwardedFor)
662
- return forwardedFor;
663
- return firstHeader(headers["x-real-ip"])?.trim() || "unknown";
664
- }
665
- function proxyClientIpFromHeaders(headers) {
666
- const forwardedFor = firstPublicForwardedIp(headers.get("x-forwarded-for") ?? undefined);
667
- if (forwardedFor)
668
- return forwardedFor;
669
- return headers.get("x-real-ip")?.trim() || "unknown";
670
- }
671
- function firstHeader(value) {
672
- return Array.isArray(value) ? value[0] : value;
673
- }
674
- function firstPublicForwardedIp(value) {
675
- return value?.split(",").map((ip) => ip.trim()).find((ip) => ip && !isPrivateIp(ip));
676
- }
677
- function isPrivateIp(ip) {
678
- return ip === "unknown"
679
- || ip.startsWith("10.")
680
- || ip.startsWith("192.168.")
681
- || /^172\.(1[6-9]|2\d|3[0-1])\./.test(ip)
682
- || ip.startsWith("127.")
683
- || ip.startsWith("::1")
684
- || ip.startsWith("fc")
685
- || ip.startsWith("fd")
686
- || ip.startsWith("fe80:");
687
- }
688
- function stripResponseHeaders(headers) {
689
- const out = new Headers();
690
- for (const [name, value] of Object.entries(headers)) {
691
- const key = name.toLowerCase();
692
- if (key === "set-cookie" || key === "connection" || key === "transfer-encoding")
693
- continue;
694
- out.set(name, value);
695
- }
696
- return out;
697
- }
698
- //# sourceMappingURL=index.js.map