@spinabot/brigade 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +19 -0
  2. package/dist/buildstamp.json +1 -1
  3. package/dist/cli/commands/expose.d.ts +40 -0
  4. package/dist/cli/commands/expose.d.ts.map +1 -0
  5. package/dist/cli/commands/expose.js +200 -0
  6. package/dist/cli/commands/expose.js.map +1 -0
  7. package/dist/cli/program/build-program.d.ts.map +1 -1
  8. package/dist/cli/program/build-program.js +61 -0
  9. package/dist/cli/program/build-program.js.map +1 -1
  10. package/dist/config/io.d.ts +41 -0
  11. package/dist/config/io.d.ts.map +1 -1
  12. package/dist/config/io.js.map +1 -1
  13. package/dist/core/tunnel/auth-proxy.d.ts +55 -0
  14. package/dist/core/tunnel/auth-proxy.d.ts.map +1 -0
  15. package/dist/core/tunnel/auth-proxy.js +179 -0
  16. package/dist/core/tunnel/auth-proxy.js.map +1 -0
  17. package/dist/core/tunnel/manager.d.ts +42 -0
  18. package/dist/core/tunnel/manager.d.ts.map +1 -0
  19. package/dist/core/tunnel/manager.js +102 -0
  20. package/dist/core/tunnel/manager.js.map +1 -0
  21. package/dist/core/tunnel/providers/bore.d.ts +18 -0
  22. package/dist/core/tunnel/providers/bore.d.ts.map +1 -0
  23. package/dist/core/tunnel/providers/bore.js +117 -0
  24. package/dist/core/tunnel/providers/bore.js.map +1 -0
  25. package/dist/core/tunnel/providers/cloudflared.d.ts +24 -0
  26. package/dist/core/tunnel/providers/cloudflared.d.ts.map +1 -0
  27. package/dist/core/tunnel/providers/cloudflared.js +179 -0
  28. package/dist/core/tunnel/providers/cloudflared.js.map +1 -0
  29. package/dist/core/tunnel/providers/custom.d.ts +21 -0
  30. package/dist/core/tunnel/providers/custom.d.ts.map +1 -0
  31. package/dist/core/tunnel/providers/custom.js +124 -0
  32. package/dist/core/tunnel/providers/custom.js.map +1 -0
  33. package/dist/core/tunnel/registry.d.ts +15 -0
  34. package/dist/core/tunnel/registry.d.ts.map +1 -0
  35. package/dist/core/tunnel/registry.js +26 -0
  36. package/dist/core/tunnel/registry.js.map +1 -0
  37. package/dist/core/tunnel/state.d.ts +39 -0
  38. package/dist/core/tunnel/state.d.ts.map +1 -0
  39. package/dist/core/tunnel/state.js +57 -0
  40. package/dist/core/tunnel/state.js.map +1 -0
  41. package/dist/core/tunnel/types.d.ts +61 -0
  42. package/dist/core/tunnel/types.d.ts.map +1 -0
  43. package/dist/core/tunnel/types.js +20 -0
  44. package/dist/core/tunnel/types.js.map +1 -0
  45. package/package.json +5 -1
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Cloudflare provider — anonymous TryCloudflare quick tunnel.
3
+ *
4
+ * The default `brigade expose` backend. No account, no signup: cloudflared
5
+ * dials Cloudflare's edge and we get a random `https://<x>.trycloudflare.com`
6
+ * URL that terminates TLS at the edge and proxies straight to our local
7
+ * auth-proxy. WebSockets work by default (the gateway is WS-first).
8
+ *
9
+ * The `cloudflared` binary (Apache-2.0) is NOT a hard npm dependency — that
10
+ * would add a ~30 MB download to every Brigade install. Instead we resolve it
11
+ * lazily at expose time:
12
+ * 1. `$BRIGADE_CLOUDFLARED_BIN`
13
+ * 2. `cloudflared` on PATH (system install)
14
+ * 3. a Brigade-managed copy in the OS cache dir
15
+ * 4. download the official release into the cache dir (once)
16
+ *
17
+ * Caveat (documented in the research): TryCloudflare quick tunnels don't
18
+ * support SSE and cap in-flight requests; that's fine for a personal WS
19
+ * gateway. For a stable URL the operator can point a self-host provider at
20
+ * their own relay instead.
21
+ */
22
+ import { spawn, spawnSync } from "node:child_process";
23
+ import * as fs from "node:fs";
24
+ import * as fsAsync from "node:fs/promises";
25
+ import * as path from "node:path";
26
+ import { resolveOsCacheDir } from "../../../config/paths.js";
27
+ const QUICK_URL_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/i;
28
+ const URL_WAIT_MS = 30_000;
29
+ function cloudflaredAssetName() {
30
+ const arch = process.arch === "x64"
31
+ ? "amd64"
32
+ : process.arch === "arm64"
33
+ ? "arm64"
34
+ : process.arch === "ia32"
35
+ ? "386"
36
+ : process.arch === "arm"
37
+ ? "arm"
38
+ : "amd64";
39
+ if (process.platform === "win32")
40
+ return `cloudflared-windows-${arch === "arm64" ? "amd64" : arch}.exe`;
41
+ if (process.platform === "darwin")
42
+ return `cloudflared-darwin-${arch}.tgz`;
43
+ return `cloudflared-linux-${arch}`;
44
+ }
45
+ function managedBinPath() {
46
+ const name = process.platform === "win32" ? "cloudflared.exe" : "cloudflared";
47
+ return path.join(resolveOsCacheDir(), "cloudflared", name);
48
+ }
49
+ /** First binary that exists across env → PATH → managed copy. */
50
+ function findExistingBinary() {
51
+ const envBin = process.env.BRIGADE_CLOUDFLARED_BIN?.trim();
52
+ if (envBin && fs.existsSync(envBin))
53
+ return envBin;
54
+ const probe = process.platform === "win32" ? "where" : "which";
55
+ try {
56
+ const res = spawnSync(probe, ["cloudflared"], { encoding: "utf8" });
57
+ if (res.status === 0) {
58
+ const first = res.stdout.split(/\r?\n/).map((l) => l.trim()).find(Boolean);
59
+ if (first && fs.existsSync(first))
60
+ return first;
61
+ }
62
+ }
63
+ catch {
64
+ // probe tool missing — fall through to managed copy
65
+ }
66
+ const managed = managedBinPath();
67
+ if (fs.existsSync(managed))
68
+ return managed;
69
+ return undefined;
70
+ }
71
+ /** Download the official cloudflared release into the cache dir. */
72
+ async function downloadBinary(onLog) {
73
+ const asset = cloudflaredAssetName();
74
+ const url = `https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}`;
75
+ const dest = managedBinPath();
76
+ await fsAsync.mkdir(path.dirname(dest), { recursive: true });
77
+ onLog?.(`downloading cloudflared (${asset})…`);
78
+ const res = await fetch(url, { redirect: "follow" });
79
+ if (!res.ok || !res.body) {
80
+ throw new Error(`cloudflared download failed (HTTP ${res.status}) from ${url}`);
81
+ }
82
+ const buf = Buffer.from(await res.arrayBuffer());
83
+ if (asset.endsWith(".tgz")) {
84
+ // macOS ships a gzipped tarball containing the `cloudflared` binary.
85
+ const tmpTgz = `${dest}.tgz`;
86
+ await fsAsync.writeFile(tmpTgz, buf);
87
+ const tar = await import("tar");
88
+ await tar.x({ file: tmpTgz, cwd: path.dirname(dest) });
89
+ await fsAsync.rm(tmpTgz, { force: true });
90
+ // The tarball extracts a file literally named `cloudflared`.
91
+ const extracted = path.join(path.dirname(dest), "cloudflared");
92
+ if (extracted !== dest && fs.existsSync(extracted)) {
93
+ await fsAsync.rename(extracted, dest);
94
+ }
95
+ }
96
+ else {
97
+ await fsAsync.writeFile(dest, buf);
98
+ }
99
+ if (process.platform !== "win32")
100
+ await fsAsync.chmod(dest, 0o755);
101
+ onLog?.(`cloudflared ready at ${dest}`);
102
+ return dest;
103
+ }
104
+ async function resolveBinary(onLog) {
105
+ return findExistingBinary() ?? (await downloadBinary(onLog));
106
+ }
107
+ export const cloudflareProvider = {
108
+ name: "cloudflare",
109
+ label: "Cloudflare (anonymous TryCloudflare quick tunnel)",
110
+ async isAvailable() {
111
+ // Always available — the binary is auto-downloaded on first use. We only
112
+ // surface a soft note when there's no pre-existing copy.
113
+ return { ok: true };
114
+ },
115
+ async start(opts) {
116
+ const bin = await resolveBinary(opts.onLog);
117
+ const target = `http://${opts.localHost}:${opts.localPort}`;
118
+ const args = ["tunnel", "--no-autoupdate", "--url", target];
119
+ const child = spawn(bin, args, { stdio: ["ignore", "pipe", "pipe"] });
120
+ const url = await new Promise((resolve, reject) => {
121
+ let settled = false;
122
+ const timer = setTimeout(() => {
123
+ if (settled)
124
+ return;
125
+ settled = true;
126
+ try {
127
+ child.kill();
128
+ }
129
+ catch { /* ignore */ }
130
+ reject(new Error(`cloudflared did not produce a public URL within ${URL_WAIT_MS / 1000}s`));
131
+ }, URL_WAIT_MS);
132
+ const scan = (chunk) => {
133
+ const text = chunk.toString();
134
+ for (const line of text.split(/\r?\n/)) {
135
+ if (line.trim())
136
+ opts.onLog?.(line.trim());
137
+ }
138
+ const m = text.match(QUICK_URL_RE);
139
+ if (m && !settled) {
140
+ settled = true;
141
+ clearTimeout(timer);
142
+ resolve(m[0]);
143
+ }
144
+ };
145
+ // cloudflared logs the URL to stderr; scan both to be safe.
146
+ child.stdout?.on("data", scan);
147
+ child.stderr?.on("data", scan);
148
+ child.on("error", (err) => {
149
+ if (settled)
150
+ return;
151
+ settled = true;
152
+ clearTimeout(timer);
153
+ reject(err);
154
+ });
155
+ child.on("exit", (code) => {
156
+ if (settled)
157
+ return;
158
+ settled = true;
159
+ clearTimeout(timer);
160
+ reject(new Error(`cloudflared exited (code ${code ?? "?"}) before publishing a URL`));
161
+ });
162
+ });
163
+ let stopped = false;
164
+ return {
165
+ provider: "cloudflare",
166
+ url,
167
+ async stop() {
168
+ if (stopped)
169
+ return;
170
+ stopped = true;
171
+ try {
172
+ child.kill();
173
+ }
174
+ catch { /* ignore */ }
175
+ },
176
+ };
177
+ },
178
+ };
179
+ //# sourceMappingURL=cloudflared.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloudflared.js","sourceRoot":"","sources":["../../../../src/core/tunnel/providers/cloudflared.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,KAAK,EAAE,SAAS,EAAqB,MAAM,oBAAoB,CAAC;AACzE,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,OAAO,MAAM,kBAAkB,CAAC;AAC5C,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAG7D,MAAM,YAAY,GAAG,2CAA2C,CAAC;AACjE,MAAM,WAAW,GAAG,MAAM,CAAC;AAE3B,SAAS,oBAAoB;IAC3B,MAAM,IAAI,GACR,OAAO,CAAC,IAAI,KAAK,KAAK;QACpB,CAAC,CAAC,OAAO;QACT,CAAC,CAAC,OAAO,CAAC,IAAI,KAAK,OAAO;YACxB,CAAC,CAAC,OAAO;YACT,CAAC,CAAC,OAAO,CAAC,IAAI,KAAK,MAAM;gBACvB,CAAC,CAAC,KAAK;gBACP,CAAC,CAAC,OAAO,CAAC,IAAI,KAAK,KAAK;oBACtB,CAAC,CAAC,KAAK;oBACP,CAAC,CAAC,OAAO,CAAC;IACpB,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO;QAAE,OAAO,uBAAuB,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC;IACxG,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ;QAAE,OAAO,sBAAsB,IAAI,MAAM,CAAC;IAC3E,OAAO,qBAAqB,IAAI,EAAE,CAAC;AACrC,CAAC;AAED,SAAS,cAAc;IACrB,MAAM,IAAI,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,aAAa,CAAC;IAC9E,OAAO,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,EAAE,aAAa,EAAE,IAAI,CAAC,CAAC;AAC7D,CAAC;AAED,iEAAiE;AACjE,SAAS,kBAAkB;IACzB,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,uBAAuB,EAAE,IAAI,EAAE,CAAC;IAC3D,IAAI,MAAM,IAAI,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC;QAAE,OAAO,MAAM,CAAC;IAEnD,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;IAC/D,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,SAAS,CAAC,KAAK,EAAE,CAAC,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;QACpE,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACrB,MAAM,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC3E,IAAI,KAAK,IAAI,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC;gBAAE,OAAO,KAAK,CAAC;QAClD,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,oDAAoD;IACtD,CAAC;IAED,MAAM,OAAO,GAAG,cAAc,EAAE,CAAC;IACjC,IAAI,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC;QAAE,OAAO,OAAO,CAAC;IAC3C,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,oEAAoE;AACpE,KAAK,UAAU,cAAc,CAAC,KAA8B;IAC1D,MAAM,KAAK,GAAG,oBAAoB,EAAE,CAAC;IACrC,MAAM,GAAG,GAAG,sEAAsE,KAAK,EAAE,CAAC;IAC1F,MAAM,IAAI,GAAG,cAAc,EAAE,CAAC;IAC9B,MAAM,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7D,KAAK,EAAE,CAAC,4BAA4B,KAAK,IAAI,CAAC,CAAC;IAE/C,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;IACrD,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,qCAAqC,GAAG,CAAC,MAAM,UAAU,GAAG,EAAE,CAAC,CAAC;IAClF,CAAC;IACD,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC;IAEjD,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3B,qEAAqE;QACrE,MAAM,MAAM,GAAG,GAAG,IAAI,MAAM,CAAC;QAC7B,MAAM,OAAO,CAAC,SAAS,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QACrC,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,CAAC;QAChC,MAAM,GAAG,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACvD,MAAM,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1C,6DAA6D;QAC7D,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,aAAa,CAAC,CAAC;QAC/D,IAAI,SAAS,KAAK,IAAI,IAAI,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YACnD,MAAM,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;QACxC,CAAC;IACH,CAAC;SAAM,CAAC;QACN,MAAM,OAAO,CAAC,SAAS,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IACrC,CAAC;IACD,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO;QAAE,MAAM,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACnE,KAAK,EAAE,CAAC,wBAAwB,IAAI,EAAE,CAAC,CAAC;IACxC,OAAO,IAAI,CAAC;AACd,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,KAA8B;IACzD,OAAO,kBAAkB,EAAE,IAAI,CAAC,MAAM,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;AAC/D,CAAC;AAED,MAAM,CAAC,MAAM,kBAAkB,GAAmB;IAChD,IAAI,EAAE,YAAY;IAClB,KAAK,EAAE,mDAAmD;IAE1D,KAAK,CAAC,WAAW;QACf,yEAAyE;QACzE,yDAAyD;QACzD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;IACtB,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,IAAwB;QAClC,MAAM,GAAG,GAAG,MAAM,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC5C,MAAM,MAAM,GAAG,UAAU,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;QAC5D,MAAM,IAAI,GAAG,CAAC,QAAQ,EAAE,iBAAiB,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;QAE5D,MAAM,KAAK,GAAiB,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;QAEpF,MAAM,GAAG,GAAG,MAAM,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACxD,IAAI,OAAO,GAAG,KAAK,CAAC;YACpB,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;gBAC5B,IAAI,OAAO;oBAAE,OAAO;gBACpB,OAAO,GAAG,IAAI,CAAC;gBACf,IAAI,CAAC;oBAAC,KAAK,CAAC,IAAI,EAAE,CAAC;gBAAC,CAAC;gBAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;gBAC5C,MAAM,CAAC,IAAI,KAAK,CAAC,mDAAmD,WAAW,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC;YAC9F,CAAC,EAAE,WAAW,CAAC,CAAC;YAEhB,MAAM,IAAI,GAAG,CAAC,KAAa,EAAQ,EAAE;gBACnC,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,EAAE,CAAC;gBAC9B,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;oBACvC,IAAI,IAAI,CAAC,IAAI,EAAE;wBAAE,IAAI,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;gBAC7C,CAAC;gBACD,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;gBACnC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;oBAClB,OAAO,GAAG,IAAI,CAAC;oBACf,YAAY,CAAC,KAAK,CAAC,CAAC;oBACpB,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;gBAChB,CAAC;YACH,CAAC,CAAC;YACF,4DAA4D;YAC5D,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;YAC/B,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;YAC/B,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;gBACxB,IAAI,OAAO;oBAAE,OAAO;gBACpB,OAAO,GAAG,IAAI,CAAC;gBACf,YAAY,CAAC,KAAK,CAAC,CAAC;gBACpB,MAAM,CAAC,GAAG,CAAC,CAAC;YACd,CAAC,CAAC,CAAC;YACH,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;gBACxB,IAAI,OAAO;oBAAE,OAAO;gBACpB,OAAO,GAAG,IAAI,CAAC;gBACf,YAAY,CAAC,KAAK,CAAC,CAAC;gBACpB,MAAM,CAAC,IAAI,KAAK,CAAC,4BAA4B,IAAI,IAAI,GAAG,2BAA2B,CAAC,CAAC,CAAC;YACxF,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,OAAO;YACL,QAAQ,EAAE,YAAY;YACtB,GAAG;YACH,KAAK,CAAC,IAAI;gBACR,IAAI,OAAO;oBAAE,OAAO;gBACpB,OAAO,GAAG,IAAI,CAAC;gBACf,IAAI,CAAC;oBAAC,KAAK,CAAC,IAAI,EAAE,CAAC;gBAAC,CAAC;gBAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;YAC9C,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * custom provider — run any tunnel command and scrape its public URL.
3
+ *
4
+ * The "bring your own OSS tunnel" escape hatch: frp, sish, chisel, zrok,
5
+ * localhost.run / ssh -R, pinggy, … anything that prints a public URL to
6
+ * stdout/stderr. The operator supplies a command template via `--command`
7
+ * (or `cfg.gateway.tunnel.command`); `{port}` is replaced with the local
8
+ * auth-proxy port.
9
+ *
10
+ * Examples:
11
+ * --command "ssh -R 80:localhost:{port} nokey@localhost.run"
12
+ * --command "sish-client ... -R {port}"
13
+ * --command "frpc http --local-port {port} --server-addr my.relay --sd brigade"
14
+ *
15
+ * We parse the first `http(s)://…` URL the command emits. If a custom tool
16
+ * uses a different scheme/format, set `--relay` to override the printed URL
17
+ * outright (we then skip URL detection and just keep the process alive).
18
+ */
19
+ import type { TunnelProvider } from "../types.js";
20
+ export declare const customProvider: TunnelProvider;
21
+ //# sourceMappingURL=custom.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"custom.d.ts","sourceRoot":"","sources":["../../../../src/core/tunnel/providers/custom.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,OAAO,KAAK,EAAoC,cAAc,EAAsB,MAAM,aAAa,CAAC;AAcxG,eAAO,MAAM,cAAc,EAAE,cAmF5B,CAAC"}
@@ -0,0 +1,124 @@
1
+ /**
2
+ * custom provider — run any tunnel command and scrape its public URL.
3
+ *
4
+ * The "bring your own OSS tunnel" escape hatch: frp, sish, chisel, zrok,
5
+ * localhost.run / ssh -R, pinggy, … anything that prints a public URL to
6
+ * stdout/stderr. The operator supplies a command template via `--command`
7
+ * (or `cfg.gateway.tunnel.command`); `{port}` is replaced with the local
8
+ * auth-proxy port.
9
+ *
10
+ * Examples:
11
+ * --command "ssh -R 80:localhost:{port} nokey@localhost.run"
12
+ * --command "sish-client ... -R {port}"
13
+ * --command "frpc http --local-port {port} --server-addr my.relay --sd brigade"
14
+ *
15
+ * We parse the first `http(s)://…` URL the command emits. If a custom tool
16
+ * uses a different scheme/format, set `--relay` to override the printed URL
17
+ * outright (we then skip URL detection and just keep the process alive).
18
+ */
19
+ import { spawn } from "node:child_process";
20
+ const ANY_URL_RE = /\bhttps?:\/\/[^\s'"]+/i;
21
+ const URL_WAIT_MS = 30_000;
22
+ /** Split a command string into argv, honouring simple double-quotes. */
23
+ function tokenize(cmd) {
24
+ const out = [];
25
+ const re = /"([^"]*)"|(\S+)/g;
26
+ let m;
27
+ while ((m = re.exec(cmd)) !== null)
28
+ out.push(m[1] ?? m[2] ?? "");
29
+ return out;
30
+ }
31
+ export const customProvider = {
32
+ name: "custom",
33
+ label: "custom (user-supplied tunnel command)",
34
+ async isAvailable(opts) {
35
+ if (opts?.command && opts.command.trim())
36
+ return { ok: true };
37
+ return {
38
+ ok: false,
39
+ reason: "the custom provider needs a command — pass --command \"…{port}…\" or set cfg.gateway.tunnel.command.",
40
+ };
41
+ },
42
+ async start(opts) {
43
+ const template = opts.command?.trim();
44
+ if (!template)
45
+ throw new Error("custom provider requires --command");
46
+ const rendered = template.replaceAll("{port}", String(opts.localPort));
47
+ const argv = tokenize(rendered);
48
+ if (argv.length === 0)
49
+ throw new Error("custom provider command is empty");
50
+ const child = spawn(argv[0], argv.slice(1), {
51
+ stdio: ["ignore", "pipe", "pipe"],
52
+ shell: false,
53
+ });
54
+ // If the operator pre-declares the public URL via --relay, trust it and
55
+ // skip detection (covers tools that don't print a parseable URL).
56
+ const forced = opts.relay?.trim();
57
+ const url = await new Promise((resolve, reject) => {
58
+ let settled = false;
59
+ const done = (u) => {
60
+ if (settled)
61
+ return;
62
+ settled = true;
63
+ clearTimeout(timer);
64
+ resolve(u);
65
+ };
66
+ const timer = setTimeout(() => {
67
+ if (settled)
68
+ return;
69
+ settled = true;
70
+ try {
71
+ child.kill();
72
+ }
73
+ catch { /* ignore */ }
74
+ reject(new Error(`custom tunnel did not emit a URL within ${URL_WAIT_MS / 1000}s (use --relay to set it explicitly)`));
75
+ }, URL_WAIT_MS);
76
+ const scan = (chunk) => {
77
+ const text = chunk.toString();
78
+ for (const line of text.split(/\r?\n/)) {
79
+ if (line.trim())
80
+ opts.onLog?.(line.trim());
81
+ }
82
+ if (!forced) {
83
+ const m = text.match(ANY_URL_RE);
84
+ if (m)
85
+ done(m[0]);
86
+ }
87
+ };
88
+ child.stdout?.on("data", scan);
89
+ child.stderr?.on("data", scan);
90
+ child.on("error", (err) => {
91
+ if (settled)
92
+ return;
93
+ settled = true;
94
+ clearTimeout(timer);
95
+ reject(err);
96
+ });
97
+ child.on("exit", (code) => {
98
+ if (settled)
99
+ return;
100
+ settled = true;
101
+ clearTimeout(timer);
102
+ reject(new Error(`custom tunnel exited (code ${code ?? "?"}) before emitting a URL`));
103
+ });
104
+ // Forced URL: resolve as soon as the process is spawned (give it a tick).
105
+ if (forced)
106
+ setTimeout(() => done(forced), 250);
107
+ });
108
+ let stopped = false;
109
+ return {
110
+ provider: "custom",
111
+ url,
112
+ async stop() {
113
+ if (stopped)
114
+ return;
115
+ stopped = true;
116
+ try {
117
+ child.kill();
118
+ }
119
+ catch { /* ignore */ }
120
+ },
121
+ };
122
+ },
123
+ };
124
+ //# sourceMappingURL=custom.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"custom.js","sourceRoot":"","sources":["../../../../src/core/tunnel/providers/custom.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Tunnel provider registry — maps a provider name to its implementation.
3
+ *
4
+ * `cloudflare` is the default (zero-config, anonymous, auto-managed binary).
5
+ * Add a provider by importing it here and listing it in `PROVIDERS`; the CLI's
6
+ * `--provider` flag and `cfg.gateway.tunnel.provider` both resolve through this.
7
+ */
8
+ import type { TunnelProvider } from "./types.js";
9
+ /** The default provider used when none is configured. */
10
+ export declare const DEFAULT_PROVIDER = "cloudflare";
11
+ /** All known provider names, in display order. */
12
+ export declare function listProviderNames(): string[];
13
+ /** Resolve a provider by name. Throws with a helpful message on unknown names. */
14
+ export declare function getProvider(name: string): TunnelProvider;
15
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/core/tunnel/registry.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,yDAAyD;AACzD,eAAO,MAAM,gBAAgB,eAAe,CAAC;AAI7C,kDAAkD;AAClD,wBAAgB,iBAAiB,IAAI,MAAM,EAAE,CAE5C;AAED,kFAAkF;AAClF,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CAQxD"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Tunnel provider registry — maps a provider name to its implementation.
3
+ *
4
+ * `cloudflare` is the default (zero-config, anonymous, auto-managed binary).
5
+ * Add a provider by importing it here and listing it in `PROVIDERS`; the CLI's
6
+ * `--provider` flag and `cfg.gateway.tunnel.provider` both resolve through this.
7
+ */
8
+ import { boreProvider } from "./providers/bore.js";
9
+ import { cloudflareProvider } from "./providers/cloudflared.js";
10
+ import { customProvider } from "./providers/custom.js";
11
+ /** The default provider used when none is configured. */
12
+ export const DEFAULT_PROVIDER = "cloudflare";
13
+ const PROVIDERS = [cloudflareProvider, boreProvider, customProvider];
14
+ /** All known provider names, in display order. */
15
+ export function listProviderNames() {
16
+ return PROVIDERS.map((p) => p.name);
17
+ }
18
+ /** Resolve a provider by name. Throws with a helpful message on unknown names. */
19
+ export function getProvider(name) {
20
+ const found = PROVIDERS.find((p) => p.name === name);
21
+ if (!found) {
22
+ throw new Error(`unknown tunnel provider "${name}". Known providers: ${listProviderNames().join(", ")}.`);
23
+ }
24
+ return found;
25
+ }
26
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../../src/core/tunnel/registry.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAGvD,yDAAyD;AACzD,MAAM,CAAC,MAAM,gBAAgB,GAAG,YAAY,CAAC;AAE7C,MAAM,SAAS,GAA8B,CAAC,kBAAkB,EAAE,YAAY,EAAE,cAAc,CAAC,CAAC;AAEhG,kDAAkD;AAClD,MAAM,UAAU,iBAAiB;IAC/B,OAAO,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;AACtC,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;IACrD,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,4BAA4B,IAAI,uBAAuB,iBAAiB,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CACzF,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Tunnel runtime state — lets `brigade expose status` / `stop` (separate
3
+ * short-lived processes) find a tunnel started by a long-running
4
+ * `brigade expose`.
5
+ *
6
+ * Stored in the OS cache dir (NOT under ~/.brigade) because it is ephemeral
7
+ * machine-local coordination — the same reasoning `resolveOsCacheDir` uses for
8
+ * the gateway lock in convex mode, and it keeps `~/.brigade` clean under the
9
+ * strict-zero guard. The file is unlinked on clean shutdown; a stale file is
10
+ * reconciled by the pid liveness check in `readTunnelState`.
11
+ */
12
+ export interface TunnelState {
13
+ /** Public URL without the token. */
14
+ url: string;
15
+ /** Public URL with `?token=` appended (when token-gated). */
16
+ urlWithToken: string;
17
+ /** Provider name. */
18
+ provider: string;
19
+ /** PID of the owning `brigade expose` process. */
20
+ pid: number;
21
+ /** Local auth-proxy port. */
22
+ proxyPort: number;
23
+ /** Gateway port being exposed. */
24
+ gatewayPort: number;
25
+ /** Whether a token gate is active. */
26
+ secured: boolean;
27
+ /** Epoch ms when the tunnel came up. */
28
+ startedAt: number;
29
+ }
30
+ /** Atomically persist tunnel state (tempfile + rename). */
31
+ export declare function writeTunnelState(state: TunnelState): Promise<void>;
32
+ /**
33
+ * Read tunnel state. Returns `undefined` when missing/unparseable. Does NOT
34
+ * verify liveness — callers that care use `isProcessAlive(state.pid)`.
35
+ */
36
+ export declare function readTunnelState(): TunnelState | undefined;
37
+ /** Remove the state file. Silent on missing file. */
38
+ export declare function clearTunnelState(): Promise<void>;
39
+ //# sourceMappingURL=state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../src/core/tunnel/state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAQH,MAAM,WAAW,WAAW;IAC1B,oCAAoC;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,6DAA6D;IAC7D,YAAY,EAAE,MAAM,CAAC;IACrB,qBAAqB;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,GAAG,EAAE,MAAM,CAAC;IACZ,6BAA6B;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,kCAAkC;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,OAAO,EAAE,OAAO,CAAC;IACjB,wCAAwC;IACxC,SAAS,EAAE,MAAM,CAAC;CACnB;AAMD,2DAA2D;AAC3D,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAMxE;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,WAAW,GAAG,SAAS,CAgBzD;AAED,qDAAqD;AACrD,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAMtD"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Tunnel runtime state — lets `brigade expose status` / `stop` (separate
3
+ * short-lived processes) find a tunnel started by a long-running
4
+ * `brigade expose`.
5
+ *
6
+ * Stored in the OS cache dir (NOT under ~/.brigade) because it is ephemeral
7
+ * machine-local coordination — the same reasoning `resolveOsCacheDir` uses for
8
+ * the gateway lock in convex mode, and it keeps `~/.brigade` clean under the
9
+ * strict-zero guard. The file is unlinked on clean shutdown; a stale file is
10
+ * reconciled by the pid liveness check in `readTunnelState`.
11
+ */
12
+ import * as fs from "node:fs";
13
+ import * as fsAsync from "node:fs/promises";
14
+ import * as path from "node:path";
15
+ import { resolveOsCacheDir } from "../../config/paths.js";
16
+ function tunnelStatePath() {
17
+ return path.join(resolveOsCacheDir(), "gateway-tunnel.json");
18
+ }
19
+ /** Atomically persist tunnel state (tempfile + rename). */
20
+ export async function writeTunnelState(state) {
21
+ const file = tunnelStatePath();
22
+ await fsAsync.mkdir(path.dirname(file), { recursive: true });
23
+ const tmp = `${file}.tmp`;
24
+ await fsAsync.writeFile(tmp, JSON.stringify(state, null, 2), "utf8");
25
+ await fsAsync.rename(tmp, file);
26
+ }
27
+ /**
28
+ * Read tunnel state. Returns `undefined` when missing/unparseable. Does NOT
29
+ * verify liveness — callers that care use `isProcessAlive(state.pid)`.
30
+ */
31
+ export function readTunnelState() {
32
+ try {
33
+ const raw = fs.readFileSync(tunnelStatePath(), "utf8");
34
+ const parsed = JSON.parse(raw);
35
+ if (typeof parsed.url === "string" &&
36
+ typeof parsed.provider === "string" &&
37
+ typeof parsed.pid === "number" &&
38
+ typeof parsed.proxyPort === "number") {
39
+ return parsed;
40
+ }
41
+ return undefined;
42
+ }
43
+ catch {
44
+ return undefined;
45
+ }
46
+ }
47
+ /** Remove the state file. Silent on missing file. */
48
+ export async function clearTunnelState() {
49
+ try {
50
+ await fsAsync.unlink(tunnelStatePath());
51
+ }
52
+ catch (err) {
53
+ if (err.code !== "ENOENT")
54
+ throw err;
55
+ }
56
+ }
57
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.js","sourceRoot":"","sources":["../../../src/core/tunnel/state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,OAAO,MAAM,kBAAkB,CAAC;AAC5C,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAqB1D,SAAS,eAAe;IACtB,OAAO,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,EAAE,qBAAqB,CAAC,CAAC;AAC/D,CAAC;AAED,2DAA2D;AAC3D,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,KAAkB;IACvD,MAAM,IAAI,GAAG,eAAe,EAAE,CAAC;IAC/B,MAAM,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7D,MAAM,GAAG,GAAG,GAAG,IAAI,MAAM,CAAC;IAC1B,MAAM,OAAO,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IACrE,MAAM,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;AAClC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe;IAC7B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,eAAe,EAAE,EAAE,MAAM,CAAC,CAAC;QACvD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAyB,CAAC;QACvD,IACE,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ;YAC9B,OAAO,MAAM,CAAC,QAAQ,KAAK,QAAQ;YACnC,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ;YAC9B,OAAO,MAAM,CAAC,SAAS,KAAK,QAAQ,EACpC,CAAC;YACD,OAAO,MAAqB,CAAC;QAC/B,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,qDAAqD;AACrD,MAAM,CAAC,KAAK,UAAU,gBAAgB;IACpC,IAAI,CAAC;QACH,MAAM,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,CAAC,CAAC;IAC1C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ;YAAE,MAAM,GAAG,CAAC;IAClE,CAAC;AACH,CAAC"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Tunnel provider contract — the seam behind `brigade expose`.
3
+ *
4
+ * A provider's only job is to take a LOCAL port (where Brigade's
5
+ * token-checking auth-proxy is listening) and publish it on the internet,
6
+ * returning the public URL. Providers never touch the gateway directly: the
7
+ * auth-proxy already sits between the public URL and the (unauthenticated,
8
+ * loopback-only) gateway, so a provider just forwards bytes.
9
+ *
10
+ * Built-in providers live in `./providers/`:
11
+ * - `cloudflare` — anonymous TryCloudflare quick tunnel (auto-managed binary)
12
+ * - `bore` — the OSS `bore` client (self-hostable relay)
13
+ * - `custom` — a user-supplied command template (`{port}` placeholder)
14
+ *
15
+ * Authoring a new provider = implement `TunnelProvider` and register it in
16
+ * `./registry.ts`. Everything else (auth-proxy, token gate, state file,
17
+ * lifecycle, CLI) is provider-agnostic.
18
+ */
19
+ /** Options handed to a provider's `start()`. */
20
+ export interface TunnelStartOptions {
21
+ /** Loopback host the auth-proxy is bound to (always `127.0.0.1`). */
22
+ localHost: string;
23
+ /** Port the auth-proxy is listening on — the tunnel points HERE, never at
24
+ * the raw gateway port. */
25
+ localPort: number;
26
+ /** Self-hosted relay address (`bore` / `custom`). Provider-defined default
27
+ * when omitted. */
28
+ relay?: string;
29
+ /** `custom` provider command template; `{port}` → `localPort`. */
30
+ command?: string;
31
+ /** Sink for provider log lines (binary stdout/stderr, status). */
32
+ onLog?: (line: string) => void;
33
+ }
34
+ /** A running tunnel. `stop()` must be idempotent. */
35
+ export interface TunnelHandle {
36
+ /** Provider name that produced this handle. */
37
+ provider: string;
38
+ /** Public URL (scheme included), e.g. `https://lively-fox-42.trycloudflare.com`
39
+ * or `http://bore.pub:41734`. Does NOT include the auth token. */
40
+ url: string;
41
+ /** Tear down the tunnel process/connection. Safe to call more than once. */
42
+ stop(): Promise<void>;
43
+ }
44
+ /** A tunnel backend. */
45
+ export interface TunnelProvider {
46
+ /** Stable id used in config + the `--provider` flag. */
47
+ name: string;
48
+ /** Human label for status output. */
49
+ label: string;
50
+ /** Whether this provider can run right now on this machine (binary present,
51
+ * command supplied, …). Cheap; called before `start()`. */
52
+ isAvailable(opts?: TunnelStartOptions): Promise<TunnelAvailability>;
53
+ /** Establish the tunnel. Rejects if the public URL can't be obtained. */
54
+ start(opts: TunnelStartOptions): Promise<TunnelHandle>;
55
+ }
56
+ export interface TunnelAvailability {
57
+ ok: boolean;
58
+ /** Operator-facing reason when `ok === false` (how to fix it). */
59
+ reason?: string;
60
+ }
61
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/tunnel/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,gDAAgD;AAChD,MAAM,WAAW,kBAAkB;IACjC,qEAAqE;IACrE,SAAS,EAAE,MAAM,CAAC;IAClB;gCAC4B;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB;wBACoB;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kEAAkE;IAClE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kEAAkE;IAClE,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAChC;AAED,qDAAqD;AACrD,MAAM,WAAW,YAAY;IAC3B,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,CAAC;IACjB;uEACmE;IACnE,GAAG,EAAE,MAAM,CAAC;IACZ,4EAA4E;IAC5E,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,wBAAwB;AACxB,MAAM,WAAW,cAAc;IAC7B,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAC;IACb,qCAAqC;IACrC,KAAK,EAAE,MAAM,CAAC;IACd;gEAC4D;IAC5D,WAAW,CAAC,IAAI,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACpE,yEAAyE;IACzE,KAAK,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;CACxD;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,OAAO,CAAC;IACZ,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Tunnel provider contract — the seam behind `brigade expose`.
3
+ *
4
+ * A provider's only job is to take a LOCAL port (where Brigade's
5
+ * token-checking auth-proxy is listening) and publish it on the internet,
6
+ * returning the public URL. Providers never touch the gateway directly: the
7
+ * auth-proxy already sits between the public URL and the (unauthenticated,
8
+ * loopback-only) gateway, so a provider just forwards bytes.
9
+ *
10
+ * Built-in providers live in `./providers/`:
11
+ * - `cloudflare` — anonymous TryCloudflare quick tunnel (auto-managed binary)
12
+ * - `bore` — the OSS `bore` client (self-hostable relay)
13
+ * - `custom` — a user-supplied command template (`{port}` placeholder)
14
+ *
15
+ * Authoring a new provider = implement `TunnelProvider` and register it in
16
+ * `./registry.ts`. Everything else (auth-proxy, token gate, state file,
17
+ * lifecycle, CLI) is provider-agnostic.
18
+ */
19
+ export {};
20
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/core/tunnel/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spinabot/brigade",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "Brigade — your personal AI crew",
5
5
  "homepage": "https://brigade.spinabot.com",
6
6
  "author": "Spinabot <hello@brigade-agent.ai>",
@@ -144,6 +144,10 @@
144
144
  "gateway:install": "node scripts/run-brigade.mjs gateway install",
145
145
  "gateway:uninstall": "node scripts/run-brigade.mjs gateway uninstall",
146
146
  "gateway:restart": "node scripts/run-brigade.mjs gateway restart",
147
+ "expose": "node scripts/run-brigade.mjs expose",
148
+ "expose:status": "node scripts/run-brigade.mjs expose status",
149
+ "expose:stop": "node scripts/run-brigade.mjs expose stop",
150
+ "bloody benchmark": "node scripts/run-brigade.mjs bloody benchmark",
147
151
  "sessions": "node scripts/run-brigade.mjs sessions list",
148
152
  "sessions:list": "node scripts/run-brigade.mjs sessions list",
149
153
  "sessions:cleanup": "node scripts/run-brigade.mjs sessions cleanup",