@absolutejs/absolute 0.20.0-beta.164 → 0.20.0-beta.166

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.
@@ -5,3 +5,4 @@ export declare const resolveVueFeatureFlags: (development: boolean) => {
5
5
  __VUE_PROD_DEVTOOLS__: string;
6
6
  __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: string;
7
7
  };
8
+ export declare const resolveClientDefine: (development: boolean, define?: Record<string, string>) => Record<string, string>;
@@ -4,6 +4,7 @@
4
4
  * it with `dev: { tunnel: { relay, token } }`. Reads:
5
5
  * - PORT (App Platform injects this; default 8787)
6
6
  * - ABSOLUTE_TUNNEL_TOKEN (required shared secret)
7
+ * - ABSOLUTE_TUNNEL_PUBLIC_ROUTES (optional JSON array of exact {path, method})
7
8
  * - ABSOLUTE_TUNNEL_PUBLIC_URL (optional; the relay's public base URL)
8
9
  */
9
10
  export declare const tunnelRelay: () => void;
@@ -1,4 +1,16 @@
1
+ export type TunnelPublicRoute = {
2
+ /** Exact, canonical pathname. Queries do not affect route matching. */
3
+ path: string;
4
+ /** Exact uppercase HTTP method. */
5
+ method: string;
6
+ };
1
7
  type RelayOptions = {
8
+ /** Optional relay process liveness endpoint; does not assert that the local client is online. */
9
+ healthPath?: string;
10
+ /** Restrict public forwarding to these HTTP routes. An empty list denies all.
11
+ * When supplied, public WebSockets are disabled. The authenticated control
12
+ * channel remains available. Omit to preserve full development tunneling. */
13
+ publicRoutes?: readonly TunnelPublicRoute[];
2
14
  /** Port the relay listens on (App Platform injects PORT). */
3
15
  port?: number;
4
16
  /** Shared secret the dev client must present (?token=). */
@@ -19,6 +31,10 @@ type PublicSocketData = {
19
31
  headers: Record<string, string>;
20
32
  };
21
33
  type SocketData = ControlSocketData | PublicSocketData;
34
+ export declare const parseTunnelPublicRoutes: (value: string) => {
35
+ method: string;
36
+ path: string;
37
+ }[];
22
38
  /**
23
39
  * Start the public reverse-tunnel relay. Holds one dev-client control socket
24
40
  * and forwards every other inbound HTTP request — and public WebSocket — down
@@ -0,0 +1,2 @@
1
+ /** Server-side reverse-tunnel relay for isolated callback deployments. */
2
+ export { startTunnelRelay, parseTunnelPublicRoutes, type TunnelPublicRoute } from './dev/tunnel/relay';
package/dist/tunnel.js ADDED
@@ -0,0 +1,193 @@
1
+ // @bun
2
+ // src/dev/tunnel/protocol.ts
3
+ var TUNNEL_CONTROL_PATH = "/__abs_tunnel/control";
4
+ var TUNNEL_FORWARDED_HOST_HEADER = "x-absolute-tunnel-host";
5
+ var decodeTunnelMessage = (raw) => {
6
+ try {
7
+ const parsed = JSON.parse(raw);
8
+ return parsed;
9
+ } catch {
10
+ return null;
11
+ }
12
+ };
13
+ var encodeTunnelMessage = (message) => JSON.stringify(message);
14
+
15
+ // src/dev/tunnel/relay.ts
16
+ var parseTunnelPublicRoutes = (value) => {
17
+ const routes = JSON.parse(value);
18
+ if (!Array.isArray(routes))
19
+ throw new Error("Tunnel public routes must be an array");
20
+ return routes.map((route) => {
21
+ if (!route || typeof route !== "object" || !("path" in route) || !("method" in route) || typeof route.path !== "string" || typeof route.method !== "string" || !/^[A-Z]+$/.test(route.method) || !/^\/(?:[A-Za-z0-9_.~-]+\/?)*$/.test(route.path) || new URL(route.path, "https://relay.invalid").pathname !== route.path || route.path === TUNNEL_CONTROL_PATH)
22
+ throw new Error("Tunnel public routes require an exact canonical path and uppercase method");
23
+ return { method: route.method, path: route.path };
24
+ });
25
+ };
26
+ var DEFAULT_RELAY_PORT = 8787;
27
+ var DEFAULT_REQUEST_TIMEOUT_MS = 30000;
28
+ var headersToObject = (headers) => {
29
+ const out = {};
30
+ headers.forEach((value, key) => {
31
+ out[key] = value;
32
+ });
33
+ return out;
34
+ };
35
+ var startTunnelRelay = (options) => {
36
+ if (options.healthPath !== undefined)
37
+ parseTunnelPublicRoutes(JSON.stringify([{ method: "GET", path: options.healthPath }]));
38
+ const publicRoutes = options.publicRoutes === undefined ? undefined : parseTunnelPublicRoutes(JSON.stringify(options.publicRoutes));
39
+ const port = options.port ?? (Number(process.env.PORT) || DEFAULT_RELAY_PORT);
40
+ const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
41
+ let client = null;
42
+ const pending = new Map;
43
+ const publicSockets = new Map;
44
+ const resolvePublicUrl = (request) => {
45
+ if (options.publicUrl)
46
+ return options.publicUrl.replace(/\/$/, "");
47
+ const url = new URL(request.url);
48
+ const host = request.headers.get("x-forwarded-host") ?? url.host;
49
+ const proto = request.headers.get("x-forwarded-proto") ?? url.protocol.replace(":", "");
50
+ return `${proto}://${host}`;
51
+ };
52
+ const isWebSocketUpgrade = (request) => request.headers.get("upgrade")?.toLowerCase() === "websocket";
53
+ const server = Bun.serve({
54
+ port,
55
+ websocket: {
56
+ close(ws) {
57
+ if (ws.data.control) {
58
+ if (client === ws)
59
+ client = null;
60
+ return;
61
+ }
62
+ publicSockets.delete(ws.data.id);
63
+ client?.send(encodeTunnelMessage({ id: ws.data.id, type: "ws_close" }));
64
+ },
65
+ message(ws, raw) {
66
+ if (!ws.data.control) {
67
+ const binary = typeof raw !== "string";
68
+ const bytes = typeof raw === "string" ? Buffer.from(raw, "utf8") : Buffer.from(raw);
69
+ client?.send(encodeTunnelMessage({
70
+ binary,
71
+ dataBase64: bytes.toString("base64"),
72
+ id: ws.data.id,
73
+ type: "ws_data"
74
+ }));
75
+ return;
76
+ }
77
+ const message = decodeTunnelMessage(typeof raw === "string" ? raw : raw.toString());
78
+ if (!message)
79
+ return;
80
+ switch (message.type) {
81
+ case "ping":
82
+ ws.send(encodeTunnelMessage({ type: "pong" }));
83
+ break;
84
+ case "response":
85
+ case "error":
86
+ pending.get(message.id)?.(message);
87
+ break;
88
+ case "ws_open_ack":
89
+ if (!message.ok)
90
+ publicSockets.get(message.id)?.close();
91
+ break;
92
+ case "ws_data": {
93
+ const target = publicSockets.get(message.id);
94
+ const bytes = Buffer.from(message.dataBase64, "base64");
95
+ target?.send(message.binary ? bytes : bytes.toString("utf8"));
96
+ break;
97
+ }
98
+ case "ws_close":
99
+ publicSockets.get(message.id)?.close(message.code, message.reason);
100
+ publicSockets.delete(message.id);
101
+ break;
102
+ default:
103
+ break;
104
+ }
105
+ },
106
+ open(ws) {
107
+ if (ws.data.control) {
108
+ client = ws;
109
+ ws.send(encodeTunnelMessage({
110
+ publicUrl: options.publicUrl ?? "",
111
+ type: "ready"
112
+ }));
113
+ return;
114
+ }
115
+ publicSockets.set(ws.data.id, ws);
116
+ client?.send(encodeTunnelMessage({
117
+ headers: ws.data.headers,
118
+ id: ws.data.id,
119
+ type: "ws_open",
120
+ url: ws.data.url
121
+ }));
122
+ }
123
+ },
124
+ async fetch(request, srv) {
125
+ const url = new URL(request.url);
126
+ if (options.healthPath && url.pathname === options.healthPath && request.method === "GET")
127
+ return new Response(null, { status: 204 });
128
+ if (url.pathname === TUNNEL_CONTROL_PATH) {
129
+ if (url.searchParams.get("token") !== options.token) {
130
+ return new Response("Forbidden", { status: 403 });
131
+ }
132
+ const upgraded = srv.upgrade(request, {
133
+ data: { control: true }
134
+ });
135
+ return upgraded ? undefined : new Response("Upgrade failed", { status: 426 });
136
+ }
137
+ if (publicRoutes && (isWebSocketUpgrade(request) || !publicRoutes.some((route) => route.path === url.pathname && route.method === request.method)))
138
+ return new Response("Not found", { status: 404 });
139
+ if (!client) {
140
+ return new Response("Tunnel offline: no dev client connected.", { status: 503 });
141
+ }
142
+ if (isWebSocketUpgrade(request)) {
143
+ const id = crypto.randomUUID();
144
+ const upgraded = srv.upgrade(request, {
145
+ data: {
146
+ control: false,
147
+ headers: headersToObject(request.headers),
148
+ id,
149
+ url: url.pathname + url.search
150
+ }
151
+ });
152
+ return upgraded ? undefined : new Response("Upgrade failed", { status: 426 });
153
+ }
154
+ const id = crypto.randomUUID();
155
+ const bodyBytes = ["GET", "HEAD"].includes(request.method) ? null : new Uint8Array(await request.arrayBuffer());
156
+ const headers = headersToObject(request.headers);
157
+ headers[TUNNEL_FORWARDED_HOST_HEADER] = resolvePublicUrl(request);
158
+ const message = {
159
+ headers,
160
+ id,
161
+ method: request.method,
162
+ type: "request",
163
+ url: url.pathname + url.search,
164
+ ...bodyBytes && bodyBytes.length > 0 ? { bodyBase64: Buffer.from(bodyBytes).toString("base64") } : {}
165
+ };
166
+ const responsePromise = new Promise((resolve) => {
167
+ pending.set(id, resolve);
168
+ });
169
+ client.send(encodeTunnelMessage(message));
170
+ const timeout = new Promise((resolve) => setTimeout(() => resolve({ id, message: "timeout", type: "error" }), requestTimeoutMs));
171
+ const result = await Promise.race([responsePromise, timeout]);
172
+ pending.delete(id);
173
+ if (result.type === "error") {
174
+ return new Response(`Tunnel error: ${result.message}`, {
175
+ status: 504
176
+ });
177
+ }
178
+ if (result.type !== "response") {
179
+ return new Response("Tunnel protocol error", { status: 502 });
180
+ }
181
+ return new Response(result.bodyBase64 ? Buffer.from(result.bodyBase64, "base64") : null, { headers: result.headers, status: result.status });
182
+ }
183
+ });
184
+ console.info(`[tunnel-relay] listening on :${server.port} (control ${TUNNEL_CONTROL_PATH})`);
185
+ return server;
186
+ };
187
+ export {
188
+ parseTunnelPublicRoutes,
189
+ startTunnelRelay
190
+ };
191
+
192
+ //# debugId=03C0B3D966BB1DD164756E2164756E21
193
+ //# sourceMappingURL=tunnel.js.map
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/dev/tunnel/protocol.ts", "../src/dev/tunnel/relay.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * Reverse-tunnel wire protocol (pure Bun, no third-party deps).\n *\n * A dev machine behind NAT cannot accept inbound connections, so the dev\n * `client` dials OUT to a public `relay` over a single control WebSocket. The\n * relay receives public HTTP requests and forwards each one down that socket;\n * the client replays it against the local app and sends the response back.\n *\n * Stage 1 covers HTTP request/response (webhooks). WebSocket passthrough for\n * telephony media streams is layered on later with additional message types.\n */\n\n/** Control-channel path the client connects to on the relay. */\nexport const TUNNEL_CONTROL_PATH = '/__abs_tunnel/control';\n\n/** Header the relay strips/sets; lets the app know its public origin. */\nexport const TUNNEL_FORWARDED_HOST_HEADER = 'x-absolute-tunnel-host';\n\n/** A forwarded HTTP request (relay → client). Body is base64 (binary-safe). */\nexport type TunnelRequestMessage = {\n\ttype: 'request';\n\tid: string;\n\tmethod: string;\n\t/** Path + query, e.g. `/v1/sms/intake?x=1`. */\n\turl: string;\n\theaders: Record<string, string>;\n\tbodyBase64?: string;\n};\n\n/** The client's response to a forwarded request (client → relay). */\nexport type TunnelResponseMessage = {\n\ttype: 'response';\n\tid: string;\n\tstatus: number;\n\theaders: Record<string, string>;\n\tbodyBase64?: string;\n};\n\n/** Client could not produce a response (local app down, fetch threw). */\nexport type TunnelErrorMessage = {\n\ttype: 'error';\n\tid: string;\n\tmessage: string;\n};\n\n/** Sent by the relay right after a successful auth handshake. */\nexport type TunnelReadyMessage = {\n\ttype: 'ready';\n\t/** Public base URL the relay is reachable at, e.g. `https://x.app`. */\n\tpublicUrl: string;\n};\n\n// --- WebSocket passthrough (Stage 2) -----------------------------------------\n// A public WebSocket (e.g. a Twilio Media Stream) is tunneled by id over the\n// same control channel: the relay asks the client to open a matching local WS,\n// then both sides forward frames tagged with that id until either end closes.\n\n/** A public WS connection opened; client should dial the local app (relay → client). */\nexport type TunnelWsOpenMessage = {\n\ttype: 'ws_open';\n\tid: string;\n\t/** Path + query of the upgraded request. */\n\turl: string;\n\theaders: Record<string, string>;\n};\n\n/** Result of the client opening the local WS (client → relay). */\nexport type TunnelWsOpenAckMessage = {\n\ttype: 'ws_open_ack';\n\tid: string;\n\tok: boolean;\n\terror?: string;\n};\n\n/** One WS frame, either direction. Binary frames are base64. */\nexport type TunnelWsDataMessage = {\n\ttype: 'ws_data';\n\tid: string;\n\tdataBase64: string;\n\tbinary: boolean;\n};\n\n/** A WS closed, either direction. */\nexport type TunnelWsCloseMessage = {\n\ttype: 'ws_close';\n\tid: string;\n\tcode?: number;\n\treason?: string;\n};\n\n/** App-level keepalive (either direction). */\nexport type TunnelPingMessage = { type: 'ping' };\nexport type TunnelPongMessage = { type: 'pong' };\n\nexport type TunnelClientMessage =\n\t| TunnelResponseMessage\n\t| TunnelErrorMessage\n\t| TunnelWsOpenAckMessage\n\t| TunnelWsDataMessage\n\t| TunnelWsCloseMessage\n\t| TunnelPongMessage\n\t| TunnelPingMessage;\nexport type TunnelServerMessage =\n\t| TunnelRequestMessage\n\t| TunnelReadyMessage\n\t| TunnelWsOpenMessage\n\t| TunnelWsDataMessage\n\t| TunnelWsCloseMessage\n\t| TunnelPingMessage\n\t| TunnelPongMessage;\n\nexport const decodeTunnelMessage = (raw: string) => {\n\ttry {\n\t\tconst parsed: TunnelClientMessage | TunnelServerMessage =\n\t\t\tJSON.parse(raw);\n\n\t\treturn parsed;\n\t} catch {\n\t\treturn null;\n\t}\n};\nexport const encodeTunnelMessage = (\n\tmessage: TunnelClientMessage | TunnelServerMessage\n) => JSON.stringify(message);\n",
6
+ "import type { ServerWebSocket } from 'bun';\nimport {\n\tdecodeTunnelMessage,\n\tencodeTunnelMessage,\n\tTUNNEL_CONTROL_PATH,\n\tTUNNEL_FORWARDED_HOST_HEADER,\n\ttype TunnelClientMessage,\n\ttype TunnelRequestMessage\n} from './protocol';\n\nexport type TunnelPublicRoute = {\n\t/** Exact, canonical pathname. Queries do not affect route matching. */\n\tpath: string;\n\t/** Exact uppercase HTTP method. */\n\tmethod: string;\n};\n\ntype RelayOptions = {\n\t/** Optional relay process liveness endpoint; does not assert that the local client is online. */\n\thealthPath?: string;\n\t/** Restrict public forwarding to these HTTP routes. An empty list denies all.\n\t * When supplied, public WebSockets are disabled. The authenticated control\n\t * channel remains available. Omit to preserve full development tunneling. */\n\tpublicRoutes?: readonly TunnelPublicRoute[];\n\t/** Port the relay listens on (App Platform injects PORT). */\n\tport?: number;\n\t/** Shared secret the dev client must present (?token=). */\n\ttoken: string;\n\t/** Public base URL the relay is reachable at. Falls back to the request\n\t * origin when omitted (App Platform: set APP_URL or pass explicitly). */\n\tpublicUrl?: string;\n\t/** How long to wait for the dev client to answer a forwarded request. */\n\trequestTimeoutMs?: number;\n};\n\ntype ControlSocketData = { control: true };\ntype PublicSocketData = {\n\tcontrol: false;\n\tid: string;\n\turl: string;\n\theaders: Record<string, string>;\n};\ntype SocketData = ControlSocketData | PublicSocketData;\n\nexport const parseTunnelPublicRoutes = (value: string) => {\n\tconst routes: unknown = JSON.parse(value);\n\tif (!Array.isArray(routes))\n\t\tthrow new Error('Tunnel public routes must be an array');\n\n\treturn routes.map((route: unknown) => {\n\t\tif (\n\t\t\t!route ||\n\t\t\ttypeof route !== 'object' ||\n\t\t\t!('path' in route) ||\n\t\t\t!('method' in route) ||\n\t\t\ttypeof route.path !== 'string' ||\n\t\t\ttypeof route.method !== 'string' ||\n\t\t\t!/^[A-Z]+$/.test(route.method) ||\n\t\t\t!/^\\/(?:[A-Za-z0-9_.~-]+\\/?)*$/.test(route.path) ||\n\t\t\tnew URL(route.path, 'https://relay.invalid').pathname !==\n\t\t\t\troute.path ||\n\t\t\troute.path === TUNNEL_CONTROL_PATH\n\t\t)\n\t\t\tthrow new Error(\n\t\t\t\t'Tunnel public routes require an exact canonical path and uppercase method'\n\t\t\t);\n\n\t\treturn { method: route.method, path: route.path };\n\t});\n};\n\nconst DEFAULT_RELAY_PORT = 8787;\nconst DEFAULT_REQUEST_TIMEOUT_MS = 30_000;\n\nconst headersToObject = (headers: Headers) => {\n\tconst out: Record<string, string> = {};\n\theaders.forEach((value, key) => {\n\t\tout[key] = value;\n\t});\n\n\treturn out;\n};\n\n/**\n * Start the public reverse-tunnel relay. Holds one dev-client control socket\n * and forwards every other inbound HTTP request — and public WebSocket — down\n * it. Single-tenant: the shared `token` gates the control channel.\n */\nexport const startTunnelRelay = (options: RelayOptions) => {\n\tif (options.healthPath !== undefined)\n\t\tparseTunnelPublicRoutes(\n\t\t\tJSON.stringify([{ method: 'GET', path: options.healthPath }])\n\t\t);\n\tconst publicRoutes =\n\t\toptions.publicRoutes === undefined\n\t\t\t? undefined\n\t\t\t: parseTunnelPublicRoutes(JSON.stringify(options.publicRoutes));\n\tconst port =\n\t\toptions.port ?? (Number(process.env.PORT) || DEFAULT_RELAY_PORT);\n\tconst requestTimeoutMs =\n\t\toptions.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n\n\tlet client: ServerWebSocket<SocketData> | null = null;\n\tconst pending = new Map<string, (message: TunnelClientMessage) => void>();\n\tconst publicSockets = new Map<string, ServerWebSocket<SocketData>>();\n\n\tconst resolvePublicUrl = (request: Request) => {\n\t\tif (options.publicUrl) return options.publicUrl.replace(/\\/$/, '');\n\t\tconst url = new URL(request.url);\n\t\tconst host = request.headers.get('x-forwarded-host') ?? url.host;\n\t\tconst proto =\n\t\t\trequest.headers.get('x-forwarded-proto') ??\n\t\t\turl.protocol.replace(':', '');\n\n\t\treturn `${proto}://${host}`;\n\t};\n\n\tconst isWebSocketUpgrade = (request: Request) =>\n\t\trequest.headers.get('upgrade')?.toLowerCase() === 'websocket';\n\n\tconst server = Bun.serve<SocketData>({\n\t\tport,\n\t\twebsocket: {\n\t\t\tclose(ws) {\n\t\t\t\tif (ws.data.control) {\n\t\t\t\t\tif (client === ws) client = null;\n\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tpublicSockets.delete(ws.data.id);\n\t\t\t\tclient?.send(\n\t\t\t\t\tencodeTunnelMessage({ id: ws.data.id, type: 'ws_close' })\n\t\t\t\t);\n\t\t\t},\n\t\t\tmessage(ws, raw) {\n\t\t\t\t// Public socket: forward the raw frame to the dev client.\n\t\t\t\tif (!ws.data.control) {\n\t\t\t\t\tconst binary = typeof raw !== 'string';\n\t\t\t\t\tconst bytes =\n\t\t\t\t\t\ttypeof raw === 'string'\n\t\t\t\t\t\t\t? Buffer.from(raw, 'utf8')\n\t\t\t\t\t\t\t: Buffer.from(raw);\n\t\t\t\t\tclient?.send(\n\t\t\t\t\t\tencodeTunnelMessage({\n\t\t\t\t\t\t\tbinary,\n\t\t\t\t\t\t\tdataBase64: bytes.toString('base64'),\n\t\t\t\t\t\t\tid: ws.data.id,\n\t\t\t\t\t\t\ttype: 'ws_data'\n\t\t\t\t\t\t})\n\t\t\t\t\t);\n\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Control socket: messages from the dev client.\n\t\t\t\tconst message = decodeTunnelMessage(\n\t\t\t\t\ttypeof raw === 'string' ? raw : raw.toString()\n\t\t\t\t);\n\t\t\t\tif (!message) return;\n\t\t\t\tswitch (message.type) {\n\t\t\t\t\tcase 'ping':\n\t\t\t\t\t\tws.send(encodeTunnelMessage({ type: 'pong' }));\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 'response':\n\t\t\t\t\tcase 'error':\n\t\t\t\t\t\tpending.get(message.id)?.(message);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 'ws_open_ack':\n\t\t\t\t\t\tif (!message.ok) publicSockets.get(message.id)?.close();\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 'ws_data': {\n\t\t\t\t\t\tconst target = publicSockets.get(message.id);\n\t\t\t\t\t\tconst bytes = Buffer.from(message.dataBase64, 'base64');\n\t\t\t\t\t\ttarget?.send(\n\t\t\t\t\t\t\tmessage.binary ? bytes : bytes.toString('utf8')\n\t\t\t\t\t\t);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tcase 'ws_close':\n\t\t\t\t\t\tpublicSockets\n\t\t\t\t\t\t\t.get(message.id)\n\t\t\t\t\t\t\t?.close(message.code, message.reason);\n\t\t\t\t\t\tpublicSockets.delete(message.id);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t},\n\t\t\topen(ws) {\n\t\t\t\tif (ws.data.control) {\n\t\t\t\t\t// Single-tenant: a new client replaces any stale one.\n\t\t\t\t\tclient = ws;\n\t\t\t\t\tws.send(\n\t\t\t\t\t\tencodeTunnelMessage({\n\t\t\t\t\t\t\tpublicUrl: options.publicUrl ?? '',\n\t\t\t\t\t\t\ttype: 'ready'\n\t\t\t\t\t\t})\n\t\t\t\t\t);\n\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// Public WS: register it and ask the dev client to open the local peer.\n\t\t\t\tpublicSockets.set(ws.data.id, ws);\n\t\t\t\tclient?.send(\n\t\t\t\t\tencodeTunnelMessage({\n\t\t\t\t\t\theaders: ws.data.headers,\n\t\t\t\t\t\tid: ws.data.id,\n\t\t\t\t\t\ttype: 'ws_open',\n\t\t\t\t\t\turl: ws.data.url\n\t\t\t\t\t})\n\t\t\t\t);\n\t\t\t}\n\t\t},\n\t\tasync fetch(request, srv) {\n\t\t\tconst url = new URL(request.url);\n\n\t\t\tif (\n\t\t\t\toptions.healthPath &&\n\t\t\t\turl.pathname === options.healthPath &&\n\t\t\t\trequest.method === 'GET'\n\t\t\t)\n\t\t\t\treturn new Response(null, { status: 204 });\n\n\t\t\t// Dev client connects here to open the control channel.\n\t\t\tif (url.pathname === TUNNEL_CONTROL_PATH) {\n\t\t\t\tif (url.searchParams.get('token') !== options.token) {\n\t\t\t\t\treturn new Response('Forbidden', { status: 403 });\n\t\t\t\t}\n\t\t\t\tconst upgraded = srv.upgrade(request, {\n\t\t\t\t\tdata: { control: true }\n\t\t\t\t});\n\n\t\t\t\treturn upgraded\n\t\t\t\t\t? undefined\n\t\t\t\t\t: new Response('Upgrade failed', { status: 426 });\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\tpublicRoutes &&\n\t\t\t\t(isWebSocketUpgrade(request) ||\n\t\t\t\t\t!publicRoutes.some(\n\t\t\t\t\t\t(route) =>\n\t\t\t\t\t\t\troute.path === url.pathname &&\n\t\t\t\t\t\t\troute.method === request.method\n\t\t\t\t\t))\n\t\t\t)\n\t\t\t\treturn new Response('Not found', { status: 404 });\n\n\t\t\tif (!client) {\n\t\t\t\treturn new Response(\n\t\t\t\t\t'Tunnel offline: no dev client connected.',\n\t\t\t\t\t{ status: 503 }\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Public WebSocket (e.g. Twilio Media Stream) → tunnel it by id.\n\t\t\tif (isWebSocketUpgrade(request)) {\n\t\t\t\tconst id = crypto.randomUUID();\n\t\t\t\tconst upgraded = srv.upgrade(request, {\n\t\t\t\t\tdata: {\n\t\t\t\t\t\tcontrol: false,\n\t\t\t\t\t\theaders: headersToObject(request.headers),\n\t\t\t\t\t\tid,\n\t\t\t\t\t\turl: url.pathname + url.search\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t\treturn upgraded\n\t\t\t\t\t? undefined\n\t\t\t\t\t: new Response('Upgrade failed', { status: 426 });\n\t\t\t}\n\n\t\t\t// Public HTTP request → forward and await the dev client's response.\n\t\t\tconst id = crypto.randomUUID();\n\t\t\tconst bodyBytes = ['GET', 'HEAD'].includes(request.method)\n\t\t\t\t? null\n\t\t\t\t: new Uint8Array(await request.arrayBuffer());\n\t\t\tconst headers = headersToObject(request.headers);\n\t\t\theaders[TUNNEL_FORWARDED_HOST_HEADER] = resolvePublicUrl(request);\n\n\t\t\tconst message: TunnelRequestMessage = {\n\t\t\t\theaders,\n\t\t\t\tid,\n\t\t\t\tmethod: request.method,\n\t\t\t\ttype: 'request',\n\t\t\t\turl: url.pathname + url.search,\n\t\t\t\t...(bodyBytes && bodyBytes.length > 0\n\t\t\t\t\t? { bodyBase64: Buffer.from(bodyBytes).toString('base64') }\n\t\t\t\t\t: {})\n\t\t\t};\n\n\t\t\tconst responsePromise = new Promise<TunnelClientMessage>(\n\t\t\t\t(resolve) => {\n\t\t\t\t\tpending.set(id, resolve);\n\t\t\t\t}\n\t\t\t);\n\t\t\tclient.send(encodeTunnelMessage(message));\n\n\t\t\tconst timeout = new Promise<TunnelClientMessage>((resolve) =>\n\t\t\t\tsetTimeout(\n\t\t\t\t\t() => resolve({ id, message: 'timeout', type: 'error' }),\n\t\t\t\t\trequestTimeoutMs\n\t\t\t\t)\n\t\t\t);\n\t\t\tconst result = await Promise.race([responsePromise, timeout]);\n\t\t\tpending.delete(id);\n\n\t\t\tif (result.type === 'error') {\n\t\t\t\treturn new Response(`Tunnel error: ${result.message}`, {\n\t\t\t\t\tstatus: 504\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (result.type !== 'response') {\n\t\t\t\treturn new Response('Tunnel protocol error', { status: 502 });\n\t\t\t}\n\n\t\t\treturn new Response(\n\t\t\t\tresult.bodyBase64\n\t\t\t\t\t? Buffer.from(result.bodyBase64, 'base64')\n\t\t\t\t\t: null,\n\t\t\t\t{ headers: result.headers, status: result.status }\n\t\t\t);\n\t\t}\n\t});\n\n\tconsole.info(\n\t\t`[tunnel-relay] listening on :${server.port} (control ${TUNNEL_CONTROL_PATH})`\n\t);\n\n\treturn server;\n};\n"
7
+ ],
8
+ "mappings": "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",
9
+ "debugId": "03C0B3D966BB1DD164756E2164756E21",
10
+ "names": []
11
+ }
package/package.json CHANGED
@@ -278,6 +278,10 @@
278
278
  "import": "./dist/svelte/server.js",
279
279
  "types": "./dist/src/svelte/server.d.ts"
280
280
  },
281
+ "./tunnel": {
282
+ "import": "./dist/tunnel.js",
283
+ "types": "./dist/src/tunnel.d.ts"
284
+ },
281
285
  "./vue": {
282
286
  "browser": "./dist/vue/browser.js",
283
287
  "import": "./dist/vue/index.js",
@@ -553,6 +557,9 @@
553
557
  "svelte/*": [
554
558
  "dist/src/svelte/*"
555
559
  ],
560
+ "tunnel": [
561
+ "dist/src/tunnel.d.ts"
562
+ ],
556
563
  "vue": [
557
564
  "dist/src/vue/index.d.ts"
558
565
  ],
@@ -561,7 +568,7 @@
561
568
  ]
562
569
  }
563
570
  },
564
- "version": "0.20.0-beta.164",
571
+ "version": "0.20.0-beta.166",
565
572
  "workspaces": [
566
573
  "tests/fixtures/*",
567
574
  "tests/fixtures/_packages/*"