@absolutejs/absolute 0.19.0-beta.1024 → 0.19.0-beta.1026

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.
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  var __require = import.meta.require;
3
3
 
4
- // .angular-partial-tmp-NzO1xY/src/core/streamingSlotRegistrar.ts
4
+ // .angular-partial-tmp-aiDi6P/src/core/streamingSlotRegistrar.ts
5
5
  var STREAMING_SLOT_REGISTRAR_KEY = Symbol.for("absolutejs.streamingSlotRegistrar");
6
6
  var STREAMING_SLOT_WARNING_STORAGE_KEY = Symbol.for("absolutejs.streamingSlotWarningController");
7
7
  var STREAMING_SLOT_COLLECTION_STORAGE_KEY = Symbol.for("absolutejs.streamingSlotCollectionController");
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  var __require = import.meta.require;
3
3
 
4
- // .angular-partial-tmp-NzO1xY/src/core/streamingSlotRegistrar.ts
4
+ // .angular-partial-tmp-aiDi6P/src/core/streamingSlotRegistrar.ts
5
5
  var STREAMING_SLOT_REGISTRAR_KEY = Symbol.for("absolutejs.streamingSlotRegistrar");
6
6
  var STREAMING_SLOT_WARNING_STORAGE_KEY = Symbol.for("absolutejs.streamingSlotWarningController");
7
7
  var STREAMING_SLOT_COLLECTION_STORAGE_KEY = Symbol.for("absolutejs.streamingSlotCollectionController");
@@ -48,7 +48,7 @@ var warnMissingStreamingSlotCollector = (primitiveName) => {
48
48
  getWarningController()?.maybeWarn(primitiveName);
49
49
  };
50
50
 
51
- // .angular-partial-tmp-NzO1xY/src/core/streamingSlotRegistry.ts
51
+ // .angular-partial-tmp-aiDi6P/src/core/streamingSlotRegistry.ts
52
52
  var STREAMING_SLOT_STORAGE_KEY = Symbol.for("absolutejs.streamingSlotAsyncLocalStorage");
53
53
  var isObjectRecord2 = (value) => Boolean(value) && typeof value === "object";
54
54
  var isAsyncLocalStorage = (value) => isObjectRecord2(value) && ("getStore" in value) && typeof value.getStore === "function" && ("run" in value) && typeof value.run === "function";
package/dist/cli/index.js CHANGED
@@ -55,6 +55,16 @@ var init_constants = __esm(() => {
55
55
  TWO_THIRDS = 2 / 3;
56
56
  });
57
57
 
58
+ // src/dev/tunnel/protocol.ts
59
+ var TUNNEL_CONTROL_PATH = "/__abs_tunnel/control", TUNNEL_FORWARDED_HOST_HEADER = "x-absolute-tunnel-host", decodeTunnelMessage = (raw) => {
60
+ try {
61
+ const parsed = JSON.parse(raw);
62
+ return parsed;
63
+ } catch {
64
+ return null;
65
+ }
66
+ }, encodeTunnelMessage = (message) => JSON.stringify(message);
67
+
58
68
  // src/utils/getDurationString.ts
59
69
  var getDurationString = (duration) => {
60
70
  let durationString;
@@ -172658,14 +172668,337 @@ var init_typecheck = __esm(() => {
172658
172668
  ];
172659
172669
  });
172660
172670
 
172671
+ // src/dev/tunnel/relay.ts
172672
+ var DEFAULT_RELAY_PORT = 8787, DEFAULT_REQUEST_TIMEOUT_MS = 30000, headersToObject = (headers) => {
172673
+ const out = {};
172674
+ headers.forEach((value, key) => {
172675
+ out[key] = value;
172676
+ });
172677
+ return out;
172678
+ }, startTunnelRelay = (options) => {
172679
+ const port = options.port ?? (Number(process.env.PORT) || DEFAULT_RELAY_PORT);
172680
+ const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
172681
+ let client = null;
172682
+ const pending = new Map;
172683
+ const publicSockets = new Map;
172684
+ const resolvePublicUrl = (request) => {
172685
+ if (options.publicUrl)
172686
+ return options.publicUrl.replace(/\/$/, "");
172687
+ const url = new URL(request.url);
172688
+ const host = request.headers.get("x-forwarded-host") ?? url.host;
172689
+ const proto = request.headers.get("x-forwarded-proto") ?? url.protocol.replace(":", "");
172690
+ return `${proto}://${host}`;
172691
+ };
172692
+ const isWebSocketUpgrade = (request) => request.headers.get("upgrade")?.toLowerCase() === "websocket";
172693
+ const server2 = Bun.serve({
172694
+ port,
172695
+ websocket: {
172696
+ close(ws) {
172697
+ if (ws.data.control) {
172698
+ if (client === ws)
172699
+ client = null;
172700
+ return;
172701
+ }
172702
+ publicSockets.delete(ws.data.id);
172703
+ client?.send(encodeTunnelMessage({ id: ws.data.id, type: "ws_close" }));
172704
+ },
172705
+ message(ws, raw) {
172706
+ if (!ws.data.control) {
172707
+ const binary = typeof raw !== "string";
172708
+ const bytes = typeof raw === "string" ? Buffer.from(raw, "utf8") : Buffer.from(raw);
172709
+ client?.send(encodeTunnelMessage({
172710
+ binary,
172711
+ dataBase64: bytes.toString("base64"),
172712
+ id: ws.data.id,
172713
+ type: "ws_data"
172714
+ }));
172715
+ return;
172716
+ }
172717
+ const message = decodeTunnelMessage(typeof raw === "string" ? raw : raw.toString());
172718
+ if (!message)
172719
+ return;
172720
+ switch (message.type) {
172721
+ case "ping":
172722
+ ws.send(encodeTunnelMessage({ type: "pong" }));
172723
+ break;
172724
+ case "response":
172725
+ case "error":
172726
+ pending.get(message.id)?.(message);
172727
+ break;
172728
+ case "ws_open_ack":
172729
+ if (!message.ok)
172730
+ publicSockets.get(message.id)?.close();
172731
+ break;
172732
+ case "ws_data": {
172733
+ const target = publicSockets.get(message.id);
172734
+ const bytes = Buffer.from(message.dataBase64, "base64");
172735
+ target?.send(message.binary ? bytes : bytes.toString("utf8"));
172736
+ break;
172737
+ }
172738
+ case "ws_close":
172739
+ publicSockets.get(message.id)?.close(message.code, message.reason);
172740
+ publicSockets.delete(message.id);
172741
+ break;
172742
+ default:
172743
+ break;
172744
+ }
172745
+ },
172746
+ open(ws) {
172747
+ if (ws.data.control) {
172748
+ client = ws;
172749
+ ws.send(encodeTunnelMessage({ publicUrl: options.publicUrl ?? "", type: "ready" }));
172750
+ return;
172751
+ }
172752
+ publicSockets.set(ws.data.id, ws);
172753
+ client?.send(encodeTunnelMessage({
172754
+ headers: ws.data.headers,
172755
+ id: ws.data.id,
172756
+ type: "ws_open",
172757
+ url: ws.data.url
172758
+ }));
172759
+ }
172760
+ },
172761
+ async fetch(request, srv) {
172762
+ const url = new URL(request.url);
172763
+ if (url.pathname === TUNNEL_CONTROL_PATH) {
172764
+ if (url.searchParams.get("token") !== options.token) {
172765
+ return new Response("Forbidden", { status: 403 });
172766
+ }
172767
+ const upgraded = srv.upgrade(request, { data: { control: true } });
172768
+ return upgraded ? undefined : new Response("Upgrade failed", { status: 426 });
172769
+ }
172770
+ if (!client) {
172771
+ return new Response("Tunnel offline: no dev client connected.", { status: 503 });
172772
+ }
172773
+ if (isWebSocketUpgrade(request)) {
172774
+ const id2 = crypto.randomUUID();
172775
+ const upgraded = srv.upgrade(request, {
172776
+ data: {
172777
+ control: false,
172778
+ headers: headersToObject(request.headers),
172779
+ id: id2,
172780
+ url: url.pathname + url.search
172781
+ }
172782
+ });
172783
+ return upgraded ? undefined : new Response("Upgrade failed", { status: 426 });
172784
+ }
172785
+ const id = crypto.randomUUID();
172786
+ const bodyBytes = ["GET", "HEAD"].includes(request.method) ? null : new Uint8Array(await request.arrayBuffer());
172787
+ const headers = headersToObject(request.headers);
172788
+ headers[TUNNEL_FORWARDED_HOST_HEADER] = resolvePublicUrl(request);
172789
+ const message = {
172790
+ headers,
172791
+ id,
172792
+ method: request.method,
172793
+ type: "request",
172794
+ url: url.pathname + url.search,
172795
+ ...bodyBytes && bodyBytes.length > 0 ? { bodyBase64: Buffer.from(bodyBytes).toString("base64") } : {}
172796
+ };
172797
+ const responsePromise = new Promise((resolve14) => {
172798
+ pending.set(id, resolve14);
172799
+ });
172800
+ client.send(encodeTunnelMessage(message));
172801
+ const timeout = new Promise((resolve14) => setTimeout(() => resolve14({ id, message: "timeout", type: "error" }), requestTimeoutMs));
172802
+ const result = await Promise.race([responsePromise, timeout]);
172803
+ pending.delete(id);
172804
+ if (result.type === "error") {
172805
+ return new Response(`Tunnel error: ${result.message}`, { status: 504 });
172806
+ }
172807
+ if (result.type !== "response") {
172808
+ return new Response("Tunnel protocol error", { status: 502 });
172809
+ }
172810
+ return new Response(result.bodyBase64 ? Buffer.from(result.bodyBase64, "base64") : null, { headers: result.headers, status: result.status });
172811
+ }
172812
+ });
172813
+ console.info(`[tunnel-relay] listening on :${server2.port} (control ${TUNNEL_CONTROL_PATH})`);
172814
+ return server2;
172815
+ };
172816
+ var init_relay = () => {};
172817
+
172818
+ // src/cli/scripts/tunnelRelay.ts
172819
+ var exports_tunnelRelay = {};
172820
+ __export(exports_tunnelRelay, {
172821
+ tunnelRelay: () => tunnelRelay
172822
+ });
172823
+ var tunnelRelay = () => {
172824
+ const token = process.env.ABSOLUTE_TUNNEL_TOKEN;
172825
+ if (!token) {
172826
+ console.error("[tunnel-relay] ABSOLUTE_TUNNEL_TOKEN is required.");
172827
+ process.exit(1);
172828
+ }
172829
+ startTunnelRelay({
172830
+ port: Number(process.env.PORT) || undefined,
172831
+ publicUrl: process.env.ABSOLUTE_TUNNEL_PUBLIC_URL,
172832
+ token
172833
+ });
172834
+ };
172835
+ var init_tunnelRelay = __esm(() => {
172836
+ init_relay();
172837
+ });
172838
+
172661
172839
  // src/cli/scripts/dev.ts
172662
172840
  init_constants();
172663
- init_startupBanner();
172664
172841
  var {$: $2, env } = globalThis.Bun;
172665
172842
  import { spawn as nodeSpawn } from "child_process";
172666
172843
  import { createWriteStream, existsSync as existsSync5, readFileSync as readFileSync7 } from "fs";
172667
172844
  import { resolve as resolve3 } from "path";
172668
172845
 
172846
+ // src/dev/tunnel/client.ts
172847
+ var RECONNECT_DELAY_MS = 2000;
172848
+ var controlSocketUrl = (relayUrl, token) => {
172849
+ const url = new URL(relayUrl);
172850
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
172851
+ url.pathname = TUNNEL_CONTROL_PATH;
172852
+ url.search = `?token=${encodeURIComponent(token)}`;
172853
+ return url.toString();
172854
+ };
172855
+ var STRIPPED_REQUEST_HEADERS = new Set(["host", "connection", "content-length", TUNNEL_FORWARDED_HOST_HEADER]);
172856
+ var startTunnelClient = (options) => {
172857
+ const publicUrl = options.relayUrl.replace(/\/$/, "");
172858
+ const localWsOrigin = options.localOrigin.replace(/^http/, "ws");
172859
+ let socket = null;
172860
+ let closed = false;
172861
+ let reconnectTimer = null;
172862
+ const localSockets = new Map;
172863
+ const sendFrameToRelay = (id, data, binary) => {
172864
+ const bytes = typeof data === "string" ? Buffer.from(data, "utf8") : data;
172865
+ const message = {
172866
+ binary,
172867
+ dataBase64: bytes.toString("base64"),
172868
+ id,
172869
+ type: "ws_data"
172870
+ };
172871
+ socket?.send(encodeTunnelMessage(message));
172872
+ };
172873
+ const openLocalWs = (id, url) => {
172874
+ const local = new WebSocket(`${localWsOrigin}${url}`);
172875
+ local.binaryType = "arraybuffer";
172876
+ const entry = { pending: [], ready: false, ws: local };
172877
+ localSockets.set(id, entry);
172878
+ local.addEventListener("open", () => {
172879
+ entry.ready = true;
172880
+ socket?.send(encodeTunnelMessage({ id, ok: true, type: "ws_open_ack" }));
172881
+ for (const frame of entry.pending) {
172882
+ local.send(frame.binary ? frame.bytes : frame.bytes.toString("utf8"));
172883
+ }
172884
+ entry.pending = [];
172885
+ });
172886
+ local.addEventListener("message", (event) => {
172887
+ if (typeof event.data === "string") {
172888
+ sendFrameToRelay(id, event.data, false);
172889
+ } else if (event.data instanceof ArrayBuffer) {
172890
+ sendFrameToRelay(id, Buffer.from(event.data), true);
172891
+ }
172892
+ });
172893
+ local.addEventListener("close", (event) => {
172894
+ localSockets.delete(id);
172895
+ socket?.send(encodeTunnelMessage({ code: event.code, id, type: "ws_close" }));
172896
+ });
172897
+ local.addEventListener("error", () => {
172898
+ if (!entry.ready) {
172899
+ socket?.send(encodeTunnelMessage({ error: "local ws failed", id, ok: false, type: "ws_open_ack" }));
172900
+ }
172901
+ });
172902
+ };
172903
+ const forwardFrameToLocal = (message) => {
172904
+ const entry = localSockets.get(message.id);
172905
+ if (!entry)
172906
+ return;
172907
+ const bytes = Buffer.from(message.dataBase64, "base64");
172908
+ if (!entry.ready) {
172909
+ entry.pending.push({ binary: message.binary, bytes });
172910
+ return;
172911
+ }
172912
+ entry.ws.send(message.binary ? bytes : bytes.toString("utf8"));
172913
+ };
172914
+ const handleRequest = async (message) => {
172915
+ const headers = {};
172916
+ for (const [key, value] of Object.entries(message.headers)) {
172917
+ if (!STRIPPED_REQUEST_HEADERS.has(key.toLowerCase()))
172918
+ headers[key] = value;
172919
+ }
172920
+ try {
172921
+ const response = await fetch(`${options.localOrigin}${message.url}`, {
172922
+ body: message.bodyBase64 ? Buffer.from(message.bodyBase64, "base64") : undefined,
172923
+ headers,
172924
+ method: message.method,
172925
+ redirect: "manual"
172926
+ });
172927
+ const bodyBytes = new Uint8Array(await response.arrayBuffer());
172928
+ const responseHeaders = {};
172929
+ response.headers.forEach((value, key) => {
172930
+ responseHeaders[key] = value;
172931
+ });
172932
+ const reply = {
172933
+ headers: responseHeaders,
172934
+ id: message.id,
172935
+ status: response.status,
172936
+ type: "response",
172937
+ ...bodyBytes.length > 0 ? { bodyBase64: Buffer.from(bodyBytes).toString("base64") } : {}
172938
+ };
172939
+ socket?.send(encodeTunnelMessage(reply));
172940
+ } catch (error) {
172941
+ socket?.send(encodeTunnelMessage({
172942
+ id: message.id,
172943
+ message: error instanceof Error ? error.message : String(error),
172944
+ type: "error"
172945
+ }));
172946
+ }
172947
+ };
172948
+ const connect = () => {
172949
+ if (closed)
172950
+ return;
172951
+ socket = new WebSocket(controlSocketUrl(options.relayUrl, options.token));
172952
+ socket.addEventListener("message", (event) => {
172953
+ const message = decodeTunnelMessage(String(event.data));
172954
+ if (!message)
172955
+ return;
172956
+ switch (message.type) {
172957
+ case "request":
172958
+ handleRequest(message);
172959
+ break;
172960
+ case "ready":
172961
+ options.onReady?.(publicUrl);
172962
+ break;
172963
+ case "ping":
172964
+ socket?.send(encodeTunnelMessage({ type: "pong" }));
172965
+ break;
172966
+ case "ws_open":
172967
+ openLocalWs(message.id, message.url);
172968
+ break;
172969
+ case "ws_data":
172970
+ forwardFrameToLocal(message);
172971
+ break;
172972
+ case "ws_close":
172973
+ localSockets.get(message.id)?.ws.close();
172974
+ localSockets.delete(message.id);
172975
+ break;
172976
+ default:
172977
+ break;
172978
+ }
172979
+ });
172980
+ socket.addEventListener("close", () => {
172981
+ if (closed)
172982
+ return;
172983
+ reconnectTimer = setTimeout(connect, RECONNECT_DELAY_MS);
172984
+ });
172985
+ socket.addEventListener("error", () => {});
172986
+ };
172987
+ connect();
172988
+ return {
172989
+ publicUrl,
172990
+ close() {
172991
+ closed = true;
172992
+ if (reconnectTimer)
172993
+ clearTimeout(reconnectTimer);
172994
+ socket?.close();
172995
+ }
172996
+ };
172997
+ };
172998
+
172999
+ // src/cli/scripts/dev.ts
173000
+ init_startupBanner();
173001
+
172669
173002
  // src/cli/interactive.ts
172670
173003
  init_constants();
172671
173004
  import { openSync } from "fs";
@@ -173037,13 +173370,18 @@ var setupHttpsCert = async () => {
173037
173370
  }
173038
173371
  await setupCertWithPrompt(ensureDevCert2, setupMkcert2);
173039
173372
  };
173040
- var resolveDevConfig = (configDev) => ({
173041
- port: Number(env.ABSOLUTE_PORT) || Number(env.PORT) || configDev?.port || DEFAULT_PORT,
173042
- portRange: Number(env.ABSOLUTE_PORT_RANGE) || configDev?.portRange || DEFAULT_PORT_RANGE,
173043
- strictPort: env.ABSOLUTE_STRICT_PORT === "true" || configDev?.strictPort === true,
173044
- host: env.ABSOLUTE_HOST ?? configDev?.host ?? "localhost",
173045
- https: env.ABSOLUTE_HTTPS === "true" || configDev?.https === true
173046
- });
173373
+ var resolveDevConfig = (configDev) => {
173374
+ const relay = env.ABSOLUTE_TUNNEL_RELAY ?? configDev?.tunnel?.relay;
173375
+ const token = env.ABSOLUTE_TUNNEL_TOKEN ?? configDev?.tunnel?.token;
173376
+ return {
173377
+ host: env.ABSOLUTE_HOST ?? configDev?.host ?? "localhost",
173378
+ https: env.ABSOLUTE_HTTPS === "true" || configDev?.https === true,
173379
+ port: Number(env.ABSOLUTE_PORT) || Number(env.PORT) || configDev?.port || DEFAULT_PORT,
173380
+ portRange: Number(env.ABSOLUTE_PORT_RANGE) || configDev?.portRange || DEFAULT_PORT_RANGE,
173381
+ strictPort: env.ABSOLUTE_STRICT_PORT === "true" || configDev?.strictPort === true,
173382
+ ...relay && token ? { tunnel: { relay, token } } : {}
173383
+ };
173384
+ };
173047
173385
  var dev = async (serverEntry, configPath2) => {
173048
173386
  let httpsEnabled = false;
173049
173387
  let resolvedDev;
@@ -173078,7 +173416,7 @@ var dev = async (serverEntry, configPath2) => {
173078
173416
  console.error(cliTag("\x1B[31m", String(err.message ?? err)));
173079
173417
  process.exit(1);
173080
173418
  });
173081
- let port = initialPortProbe.port;
173419
+ let { port } = initialPortProbe;
173082
173420
  if (initialPortProbe.fellBack) {
173083
173421
  const displayHost = resolvedDev.host === "0.0.0.0" ? "localhost" : resolvedDev.host;
173084
173422
  console.log(cliTag("\x1B[33m", `Port ${resolvedDev.port} is in use, trying another one... \u2192 http://${displayHost}:${port}/`));
@@ -173123,11 +173461,24 @@ var dev = async (serverEntry, configPath2) => {
173123
173461
  let cleaning = false;
173124
173462
  let interactive = null;
173125
173463
  let serverReady = false;
173464
+ let tunnelClient = null;
173465
+ const startTunnelIfConfigured = () => {
173466
+ if (tunnelClient || !resolvedDev.tunnel)
173467
+ return;
173468
+ tunnelClient = startTunnelClient({
173469
+ localOrigin: `http://localhost:${port}`,
173470
+ relayUrl: resolvedDev.tunnel.relay,
173471
+ token: resolvedDev.tunnel.token,
173472
+ onReady: (publicUrl) => process.stdout.write(` \x1B[32m\u279C\x1B[0m \x1B[1mPublic:\x1B[0m ${publicUrl}/
173473
+ `)
173474
+ });
173475
+ };
173126
173476
  const checkServerReady = (value) => {
173127
173477
  const chunk = value.toString();
173128
173478
  if (!chunk.includes("Local:"))
173129
173479
  return;
173130
173480
  serverReady = true;
173481
+ startTunnelIfConfigured();
173131
173482
  interactive?.showPrompt();
173132
173483
  };
173133
173484
  const RESTART_MARKER = "[abs:restart]";
@@ -173391,6 +173742,7 @@ var dev = async (serverEntry, configPath2) => {
173391
173742
  });
173392
173743
  if (interactive)
173393
173744
  interactive.dispose();
173745
+ tunnelClient?.close();
173394
173746
  if (paused)
173395
173747
  sendSignal("SIGCONT");
173396
173748
  killChildTree("SIGTERM");
@@ -175905,6 +176257,10 @@ if (command === "dev") {
175905
176257
  sendTelemetryEvent("cli:command", { command });
175906
176258
  const { setupMkcert: setupMkcert2 } = await Promise.resolve().then(() => (init_devCert(), exports_devCert));
175907
176259
  setupMkcert2();
176260
+ } else if (command === "tunnel-relay") {
176261
+ sendTelemetryEvent("cli:command", { command });
176262
+ const { tunnelRelay: tunnelRelay2 } = await Promise.resolve().then(() => (init_tunnelRelay(), exports_tunnelRelay));
176263
+ tunnelRelay2();
175908
176264
  } else {
175909
176265
  const message = command ? `Unknown command: ${command}` : "No command specified";
175910
176266
  console.error(message);
@@ -175922,5 +176278,6 @@ if (command === "dev") {
175922
176278
  console.error(" prettier Run Prettier check (cached)");
175923
176279
  console.error(" typecheck Run type checkers for all frameworks");
175924
176280
  console.error(" telemetry Manage anonymous telemetry");
176281
+ console.error(" tunnel-relay Run the public reverse-tunnel relay (for webhook dev)");
175925
176282
  process.exit(1);
175926
176283
  }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * `absolute tunnel-relay` — run the public reverse-tunnel relay (typically on a
3
+ * small always-on host like a DO App Platform service). Dev machines connect to
4
+ * it with `dev: { tunnel: { relay, token } }`. Reads:
5
+ * - PORT (App Platform injects this; default 8787)
6
+ * - ABSOLUTE_TUNNEL_TOKEN (required shared secret)
7
+ * - ABSOLUTE_TUNNEL_PUBLIC_URL (optional; the relay's public base URL)
8
+ */
9
+ export declare const tunnelRelay: () => void;
@@ -0,0 +1,20 @@
1
+ type TunnelClientOptions = {
2
+ /** Public relay base URL, e.g. `https://my-relay.ondigitalocean.app`. */
3
+ relayUrl: string;
4
+ /** Shared secret matching the relay's token. */
5
+ token: string;
6
+ /** Local app origin to replay requests against, e.g. `http://localhost:3000`. */
7
+ localOrigin: string;
8
+ /** Called once the relay confirms the public URL is live. */
9
+ onReady?: (publicUrl: string) => void;
10
+ };
11
+ /**
12
+ * Start the dev-side tunnel client. Dials the relay's control socket and
13
+ * replays each forwarded request against the local app, streaming responses
14
+ * back. Auto-reconnects so an HMR restart or a relay blip self-heals.
15
+ */
16
+ export declare const startTunnelClient: (options: TunnelClientOptions) => {
17
+ publicUrl: string;
18
+ close(): void;
19
+ };
20
+ export {};
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Reverse-tunnel wire protocol (pure Bun, no third-party deps).
3
+ *
4
+ * A dev machine behind NAT cannot accept inbound connections, so the dev
5
+ * `client` dials OUT to a public `relay` over a single control WebSocket. The
6
+ * relay receives public HTTP requests and forwards each one down that socket;
7
+ * the client replays it against the local app and sends the response back.
8
+ *
9
+ * Stage 1 covers HTTP request/response (webhooks). WebSocket passthrough for
10
+ * telephony media streams is layered on later with additional message types.
11
+ */
12
+ /** Control-channel path the client connects to on the relay. */
13
+ export declare const TUNNEL_CONTROL_PATH = "/__abs_tunnel/control";
14
+ /** Header the relay strips/sets; lets the app know its public origin. */
15
+ export declare const TUNNEL_FORWARDED_HOST_HEADER = "x-absolute-tunnel-host";
16
+ /** A forwarded HTTP request (relay → client). Body is base64 (binary-safe). */
17
+ export type TunnelRequestMessage = {
18
+ type: 'request';
19
+ id: string;
20
+ method: string;
21
+ /** Path + query, e.g. `/v1/sms/intake?x=1`. */
22
+ url: string;
23
+ headers: Record<string, string>;
24
+ bodyBase64?: string;
25
+ };
26
+ /** The client's response to a forwarded request (client → relay). */
27
+ export type TunnelResponseMessage = {
28
+ type: 'response';
29
+ id: string;
30
+ status: number;
31
+ headers: Record<string, string>;
32
+ bodyBase64?: string;
33
+ };
34
+ /** Client could not produce a response (local app down, fetch threw). */
35
+ export type TunnelErrorMessage = {
36
+ type: 'error';
37
+ id: string;
38
+ message: string;
39
+ };
40
+ /** Sent by the relay right after a successful auth handshake. */
41
+ export type TunnelReadyMessage = {
42
+ type: 'ready';
43
+ /** Public base URL the relay is reachable at, e.g. `https://x.app`. */
44
+ publicUrl: string;
45
+ };
46
+ /** A public WS connection opened; client should dial the local app (relay → client). */
47
+ export type TunnelWsOpenMessage = {
48
+ type: 'ws_open';
49
+ id: string;
50
+ /** Path + query of the upgraded request. */
51
+ url: string;
52
+ headers: Record<string, string>;
53
+ };
54
+ /** Result of the client opening the local WS (client → relay). */
55
+ export type TunnelWsOpenAckMessage = {
56
+ type: 'ws_open_ack';
57
+ id: string;
58
+ ok: boolean;
59
+ error?: string;
60
+ };
61
+ /** One WS frame, either direction. Binary frames are base64. */
62
+ export type TunnelWsDataMessage = {
63
+ type: 'ws_data';
64
+ id: string;
65
+ dataBase64: string;
66
+ binary: boolean;
67
+ };
68
+ /** A WS closed, either direction. */
69
+ export type TunnelWsCloseMessage = {
70
+ type: 'ws_close';
71
+ id: string;
72
+ code?: number;
73
+ reason?: string;
74
+ };
75
+ /** App-level keepalive (either direction). */
76
+ export type TunnelPingMessage = {
77
+ type: 'ping';
78
+ };
79
+ export type TunnelPongMessage = {
80
+ type: 'pong';
81
+ };
82
+ export type TunnelClientMessage = TunnelResponseMessage | TunnelErrorMessage | TunnelWsOpenAckMessage | TunnelWsDataMessage | TunnelWsCloseMessage | TunnelPongMessage | TunnelPingMessage;
83
+ export type TunnelServerMessage = TunnelRequestMessage | TunnelReadyMessage | TunnelWsOpenMessage | TunnelWsDataMessage | TunnelWsCloseMessage | TunnelPingMessage | TunnelPongMessage;
84
+ export declare const decodeTunnelMessage: (raw: string) => TunnelRequestMessage | TunnelResponseMessage | TunnelErrorMessage | TunnelReadyMessage | TunnelWsOpenMessage | TunnelWsOpenAckMessage | TunnelWsDataMessage | TunnelWsCloseMessage | TunnelPingMessage | TunnelPongMessage | null;
85
+ export declare const encodeTunnelMessage: (message: TunnelClientMessage | TunnelServerMessage) => string;
@@ -0,0 +1,28 @@
1
+ type RelayOptions = {
2
+ /** Port the relay listens on (App Platform injects PORT). */
3
+ port?: number;
4
+ /** Shared secret the dev client must present (?token=). */
5
+ token: string;
6
+ /** Public base URL the relay is reachable at. Falls back to the request
7
+ * origin when omitted (App Platform: set APP_URL or pass explicitly). */
8
+ publicUrl?: string;
9
+ /** How long to wait for the dev client to answer a forwarded request. */
10
+ requestTimeoutMs?: number;
11
+ };
12
+ type ControlSocketData = {
13
+ control: true;
14
+ };
15
+ type PublicSocketData = {
16
+ control: false;
17
+ id: string;
18
+ url: string;
19
+ headers: Record<string, string>;
20
+ };
21
+ type SocketData = ControlSocketData | PublicSocketData;
22
+ /**
23
+ * Start the public reverse-tunnel relay. Holds one dev-client control socket
24
+ * and forwards every other inbound HTTP request — and public WebSocket — down
25
+ * it. Single-tenant: the shared `token` gates the control channel.
26
+ */
27
+ export declare const startTunnelRelay: (options: RelayOptions) => Bun.Server<SocketData>;
28
+ export {};
@@ -35,6 +35,10 @@ export declare const loadConfig: (configPath?: string) => Promise<{
35
35
  host?: string;
36
36
  https?: boolean;
37
37
  watchDirs?: string[];
38
+ tunnel?: {
39
+ relay?: string;
40
+ token?: string;
41
+ };
38
42
  devtools?: {
39
43
  projectRoot?: string;
40
44
  uuid?: string;
@@ -183,6 +183,17 @@ export type BaseBuildConfig = {
183
183
  * conventional source dirs (`src/`, `db/`, `assets/`, `styles/`),
184
184
  * and these `watchDirs` is implicitly ignored. */
185
185
  watchDirs?: string[];
186
+ /** Expose the dev server to the public internet through a self-hosted
187
+ * AbsoluteJS reverse-tunnel relay (for webhooks: Twilio, Stripe, OAuth).
188
+ * Run the relay with `absolute tunnel-relay` on a public host; point a
189
+ * dev client at it here. Prints a `Public:` URL on start. */
190
+ tunnel?: {
191
+ /** Relay base URL, e.g. `https://my-relay.ondigitalocean.app`
192
+ * (env: ABSOLUTE_TUNNEL_RELAY). */
193
+ relay?: string;
194
+ /** Shared secret matching the relay's token (env: ABSOLUTE_TUNNEL_TOKEN). */
195
+ token?: string;
196
+ };
186
197
  devtools?: {
187
198
  projectRoot?: string;
188
199
  uuid?: string;
package/package.json CHANGED
@@ -269,8 +269,7 @@
269
269
  "svelte": "^5.35.2",
270
270
  "tailwindcss": "^4.1.0",
271
271
  "typescript": "^5.9.3",
272
- "vue": "^3.5.27",
273
- "zone.js": "^0.15.0"
272
+ "vue": "^3.5.27"
274
273
  },
275
274
  "peerDependenciesMeta": {
276
275
  "@angular/animations": {
@@ -338,9 +337,6 @@
338
337
  },
339
338
  "vue": {
340
339
  "optional": true
341
- },
342
- "zone.js": {
343
- "optional": true
344
340
  }
345
341
  },
346
342
  "repository": {
@@ -409,5 +405,5 @@
409
405
  ]
410
406
  }
411
407
  },
412
- "version": "0.19.0-beta.1024"
408
+ "version": "0.19.0-beta.1026"
413
409
  }