@dbx-tools/cli-tunnel 0.6.59 → 0.6.85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -364
- package/index.ts +2 -19
- package/lib/index.d.ts +2 -19
- package/lib/index.js +2 -15
- package/lib/src/app.d.ts +12 -123
- package/lib/src/app.js +22 -250
- package/lib/src/cli.d.ts +19 -22
- package/lib/src/cli.js +143 -128
- package/lib/src/options.d.ts +46 -0
- package/lib/src/options.js +51 -0
- package/lib/src/proxy.d.ts +27 -37
- package/lib/src/proxy.js +115 -220
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +15 -81
- package/src/app.ts +20 -282
- package/src/cli.ts +152 -162
- package/src/options.ts +85 -0
- package/src/proxy.ts +139 -261
- package/bin/dbx-tools-tunnel.ts +0 -13
- package/lib/bin/dbx-tools-tunnel.d.ts +0 -2
- package/lib/bin/dbx-tools-tunnel.js +0 -14
- package/lib/src/allowlist.d.ts +0 -32
- package/lib/src/allowlist.js +0 -57
- package/lib/src/env.d.ts +0 -57
- package/lib/src/env.js +0 -60
- package/lib/src/headers.d.ts +0 -108
- package/lib/src/headers.js +0 -140
- package/lib/src/otp.d.ts +0 -49
- package/lib/src/otp.js +0 -124
- package/lib/src/plugin.d.ts +0 -147
- package/lib/src/plugin.js +0 -138
- package/lib/src/portr.d.ts +0 -40
- package/lib/src/portr.js +0 -93
- package/lib/src/rate-limit.d.ts +0 -35
- package/lib/src/rate-limit.js +0 -53
- package/lib/src/signing-key.d.ts +0 -86
- package/lib/src/signing-key.js +0 -170
- package/src/allowlist.ts +0 -60
- package/src/env.ts +0 -72
- package/src/headers.ts +0 -155
- package/src/otp.ts +0 -137
- package/src/plugin.ts +0 -269
- package/src/portr.ts +0 -113
- package/src/rate-limit.ts +0 -59
- package/src/signing-key.ts +0 -201
package/src/proxy.ts
CHANGED
|
@@ -1,298 +1,176 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The reverse proxy that makes the wrapper path possible.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The wrapper claims the PUBLIC port and the wrapped app runs as a child process
|
|
5
|
+
* on a private loopback port, so - unlike the in-process plugin - there is no
|
|
6
|
+
* middleware chain to insert the gate into. This proxy is that insertion point: it
|
|
7
|
+
* answers the login routes itself, applies the gate to everything else, and
|
|
8
|
+
* forwards what survives to the child.
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* that path; the gate exists for the tunnel, which nothing else protects.
|
|
15
|
-
*
|
|
16
|
-
* The distinguisher is the connection's source address: a loopback
|
|
17
|
-
* `req.socket.remoteAddress` is the portr client; anything else is the platform.
|
|
18
|
-
*
|
|
19
|
-
* For portr traffic the gate is a standard SPA gate: static assets + the login
|
|
20
|
-
* flow (`/api/email/auth/*`, answered IN-PROCESS by this proxy via the
|
|
21
|
-
* {@link AuthGateApi}) are open so the browser can load the client and render the
|
|
22
|
-
* `<AuthGate>`; every other `/api/*` needs a valid session cookie or gets 401.
|
|
23
|
-
* WebSocket upgrades are gated the same way and forwarded with `http-proxy-3`.
|
|
10
|
+
* The gating DECISION is not reimplemented here. `@dbx-tools/tunnel`'s
|
|
11
|
+
* `gate.gateRequest` makes it - the same function the Express middleware calls -
|
|
12
|
+
* and this module only differs in how the outcome is written: a proxied request
|
|
13
|
+
* instead of `next()`, a `writeHead` instead of `res.json`. That is deliberate:
|
|
14
|
+
* two independent implementations of "which requests are gated and which headers
|
|
15
|
+
* are stripped" is the one way this package could become a security bug.
|
|
24
16
|
*
|
|
25
17
|
* @module
|
|
26
18
|
*/
|
|
27
19
|
|
|
28
20
|
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
|
29
21
|
import type { Socket } from "node:net";
|
|
30
|
-
import { http, json, log
|
|
22
|
+
import { http, json, log } from "@dbx-tools/shared-core";
|
|
31
23
|
import { authRequestSchema, authVerifySchema, SESSION_COOKIE_NAME } from "@dbx-tools/shared-email";
|
|
24
|
+
import { gate as gateModule, headers as headersModule, type AuthGateApi } from "@dbx-tools/tunnel";
|
|
32
25
|
import ProxyModule from "http-proxy-3";
|
|
33
|
-
import { toHeaderPolicy, type HeaderPolicy } from "./headers.ts";
|
|
34
|
-
import type { AuthGateApi } from "./plugin.ts";
|
|
35
26
|
|
|
36
27
|
const logger = log.logger("tunnel:proxy");
|
|
37
28
|
|
|
38
|
-
/** Route prefix the login flow lives under (open, answered in-process). */
|
|
39
|
-
const AUTH_PREFIX = "/api/email/auth";
|
|
40
|
-
|
|
41
|
-
/** True when a socket address is loopback (the portr client, same container). */
|
|
42
|
-
function isLoopback(addr: string | undefined): boolean {
|
|
43
|
-
if (!addr) return false;
|
|
44
|
-
// Normalize IPv4-mapped IPv6 (`::ffff:127.0.0.1`) and bare IPv6 loopback.
|
|
45
|
-
const a = addr.replace(/^::ffff:/, "");
|
|
46
|
-
return a === "127.0.0.1" || a.startsWith("127.") || a === "::1" || a === "localhost";
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Read the whole request body as text (small JSON payloads only). */
|
|
50
|
-
function readBody(req: IncomingMessage): Promise<string> {
|
|
51
|
-
return new Promise((resolve) => {
|
|
52
|
-
let data = "";
|
|
53
|
-
req.on("data", (chunk) => (data += chunk));
|
|
54
|
-
req.on("end", () => resolve(data));
|
|
55
|
-
req.on("error", () => resolve(data));
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function sendJson(res: ServerResponse, status: number, body: unknown, setCookie?: string): void {
|
|
60
|
-
const headers: Record<string, string> = { "content-type": "application/json" };
|
|
61
|
-
if (setCookie) headers["set-cookie"] = setCookie;
|
|
62
|
-
res.writeHead(status, headers);
|
|
63
|
-
res.end(JSON.stringify(body));
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Options for {@link startProxy}. */
|
|
67
29
|
export interface ProxyOptions {
|
|
68
|
-
/**
|
|
30
|
+
/** The port this proxy listens on - the port portr and the platform route to. */
|
|
69
31
|
publicPort: number;
|
|
70
|
-
/**
|
|
32
|
+
/** The private loopback port the wrapped app listens on. */
|
|
71
33
|
appPort: number;
|
|
72
|
-
/**
|
|
73
|
-
* The in-process gate API, or `undefined` to run OPEN (insecure mode): every
|
|
74
|
-
* request - including portr traffic - is forwarded ungated. Used when the
|
|
75
|
-
* operator passed `--insecure` / `TUNNEL_INSECURE=true`.
|
|
76
|
-
*/
|
|
34
|
+
/** The gate handlers. Omitted (an `--insecure` run) forwards everything. */
|
|
77
35
|
gate?: AuthGateApi;
|
|
78
|
-
/**
|
|
79
|
-
* Extra `x-` request headers tunnel traffic may forward, as literals, globs, or
|
|
80
|
-
* `/regex/`es - unioned with {@link DEFAULT_FORWARD_HEADERS}. Every other `x-`
|
|
81
|
-
* header is stripped, and {@link PROTECTED_HEADERS} is stripped regardless. See
|
|
82
|
-
* `./headers.ts` for why the policy is an allow-list.
|
|
83
|
-
*/
|
|
36
|
+
/** Extra `x-` headers tunnel traffic may forward. */
|
|
84
37
|
forwardHeaders?: readonly string[];
|
|
85
38
|
}
|
|
86
39
|
|
|
87
|
-
/**
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const proxy = ProxyModule.createProxyServer({
|
|
95
|
-
target: { host: "127.0.0.1", port: appPort },
|
|
96
|
-
ws: true,
|
|
97
|
-
xfwd: true,
|
|
98
|
-
});
|
|
99
|
-
proxy.on("error", (err: Error, _req: unknown, res: unknown) => {
|
|
100
|
-
logger.warn("upstream proxy error", { error: err.message });
|
|
101
|
-
const r = res as ServerResponse | undefined;
|
|
102
|
-
if (r && "writeHead" in r && !r.headersSent) {
|
|
103
|
-
sendJson(r, 502, { error: "upstream unavailable" });
|
|
104
|
-
}
|
|
40
|
+
/** Read a request body as text; a transport error yields whatever arrived. */
|
|
41
|
+
function readBody(request: IncomingMessage): Promise<string> {
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
let body = "";
|
|
44
|
+
request.on("data", (chunk) => (body += chunk));
|
|
45
|
+
request.on("end", () => resolve(body));
|
|
46
|
+
request.on("error", () => resolve(body));
|
|
105
47
|
});
|
|
48
|
+
}
|
|
106
49
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
process.env.NODE_ENV === "production" ? "Secure" : "",
|
|
120
|
-
]
|
|
121
|
-
.filter(Boolean)
|
|
122
|
-
.join("; ");
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Client IP for rate-limiting: the portr client's forwarded XFF, else the socket.
|
|
126
|
-
*
|
|
127
|
-
* Deliberately the RIGHTMOST `x-forwarded-for` entry, not the leftmost. The
|
|
128
|
-
* list grows left-to-right as each hop appends, so the last entry is the one
|
|
129
|
-
* the nearest trusted proxy (portr) wrote and every earlier entry is a value
|
|
130
|
-
* the caller could have sent. Reading the leftmost lets a client vary one
|
|
131
|
-
* header to get a fresh rate-limit bucket per request, which defeats both the
|
|
132
|
-
* per-IP code-request and verify-attempt limiters. Called BEFORE
|
|
133
|
-
* {@link HeaderPolicy.apply} strips the header, so the honest hop value is
|
|
134
|
-
* still available here.
|
|
135
|
-
*/
|
|
136
|
-
const clientIp = (req: IncomingMessage): string => {
|
|
137
|
-
const forwarded = req.headers["x-forwarded-for"];
|
|
138
|
-
const chain = (Array.isArray(forwarded) ? forwarded.join(",") : (forwarded ?? ""))
|
|
139
|
-
.split(",")
|
|
140
|
-
.map((entry) => entry.trim())
|
|
141
|
-
.filter(Boolean);
|
|
142
|
-
return chain.at(-1) ?? req.socket.remoteAddress ?? "unknown";
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Remove the gate's session cookie from the `Cookie` header before forwarding,
|
|
147
|
-
* so the app never sees `dbx_auth` (it is the proxy's concern, not the app's).
|
|
148
|
-
* Preserves any other cookies. Removes the header entirely when it becomes empty.
|
|
149
|
-
*/
|
|
150
|
-
const stripSessionCookie = (req: IncomingMessage): void => {
|
|
151
|
-
const raw = req.headers.cookie;
|
|
152
|
-
if (!raw) return;
|
|
153
|
-
const kept = raw
|
|
154
|
-
.split(";")
|
|
155
|
-
.map((c) => c.trim())
|
|
156
|
-
.filter((c) => c && !c.startsWith(`${SESSION_COOKIE_NAME}=`));
|
|
157
|
-
if (kept.length) req.headers.cookie = kept.join("; ");
|
|
158
|
-
else delete req.headers.cookie;
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Present an OTP-authenticated caller to the app the SAME way a platform front
|
|
163
|
-
* door does: set the front door's own identity headers to the verified address
|
|
164
|
-
* (AppKit reads {@link token.USER_ID_HEADER} for the OBO user id), so the app
|
|
165
|
-
* needs no gate-specific code path. The names come from
|
|
166
|
-
* `@dbx-tools/shared-core`'s `token` module - the same constants AppKit-side
|
|
167
|
-
* code reads them by - rather than being re-spelled here.
|
|
168
|
-
*
|
|
169
|
-
* What the gate CANNOT set is {@link token.ACCESS_TOKEN_HEADER}: an OTP session
|
|
170
|
-
* proves an email address, not possession of a Databricks credential, and
|
|
171
|
-
* there is no way to mint one for the caller. Its absence is exactly what
|
|
172
|
-
* `@dbx-tools/appkit`'s `identity` `"auto"` mode detects, so a gated request
|
|
173
|
-
* runs as the app's service principal instead of throwing.
|
|
174
|
-
*/
|
|
175
|
-
const injectIdentity = (req: IncomingMessage, email: string): void => {
|
|
176
|
-
req.headers[token.USER_ID_HEADER] = email;
|
|
177
|
-
req.headers[token.USER_EMAIL_HEADER] = email;
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Apply the inbound-header policy to a tunnel request: every `x-` header the
|
|
182
|
-
* allow-list does not name is deleted, and the platform identity/transport set
|
|
183
|
-
* is deleted regardless. See `./headers.ts` for the reasoning; the
|
|
184
|
-
* security-critical case is {@link token.ACCESS_TOKEN_HEADER}, which the gate
|
|
185
|
-
* never sets but an app running `identity: "auto"` treats as proof the request
|
|
186
|
-
* can do OBO.
|
|
187
|
-
*
|
|
188
|
-
* `xfwd: true` on the proxy re-adds `x-forwarded-for`/`-proto`/`-port`/`-host`
|
|
189
|
-
* afterwards from the real socket, so the app still sees those - it just sees
|
|
190
|
-
* the honest values rather than the caller's claim.
|
|
191
|
-
*/
|
|
192
|
-
const applyHeaderPolicy = (req: IncomingMessage): void => {
|
|
193
|
-
const removed = headerPolicy.apply(req.headers as Record<string, unknown>);
|
|
194
|
-
if (removed.length) logger.debug("stripped inbound headers", { removed });
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
const server = createServer(async (req, res) => {
|
|
198
|
-
const path = (req.url ?? "/").split("?")[0]!;
|
|
199
|
-
|
|
200
|
-
// Insecure/open mode (no gate) OR non-loopback traffic (the hosting
|
|
201
|
-
// platform's front door / control plane): forward ungated.
|
|
202
|
-
if (!gate || !isLoopback(req.socket.remoteAddress)) {
|
|
203
|
-
proxy.web(req, res);
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// --- portr traffic: the gate applies ---
|
|
208
|
-
|
|
209
|
-
// The login flow is answered IN-PROCESS (the app has no server to forward to).
|
|
210
|
-
if (path === `${AUTH_PREFIX}/status`) {
|
|
211
|
-
const token = http.parseCookies(req.headers.cookie ?? null)[SESSION_COOKIE_NAME];
|
|
212
|
-
sendJson(res, 200, await gate.status(token));
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
if (path === `${AUTH_PREFIX}/request` && req.method === "POST") {
|
|
216
|
-
const parsed = authRequestSchema.safeParse(json.parseRecord(await readBody(req)));
|
|
217
|
-
if (!parsed.success) return sendJson(res, 200, { ok: true }); // anti-enumeration
|
|
218
|
-
return sendJson(res, 200, await gate.request(parsed.data.email, clientIp(req)));
|
|
219
|
-
}
|
|
220
|
-
if (path === `${AUTH_PREFIX}/verify` && req.method === "POST") {
|
|
221
|
-
const parsed = authVerifySchema.safeParse(json.parseRecord(await readBody(req)));
|
|
222
|
-
if (!parsed.success) return sendJson(res, 200, { ok: false });
|
|
223
|
-
const result = await gate.verify(parsed.data.email, parsed.data.code, clientIp(req));
|
|
224
|
-
const cookie =
|
|
225
|
-
result.ok && result.token ? sessionCookie(result.token, gate.sessionTtlSeconds) : undefined;
|
|
226
|
-
return sendJson(
|
|
227
|
-
res,
|
|
228
|
-
200,
|
|
229
|
-
{ ok: result.ok, ...(result.retryAfter ? { retryAfter: result.retryAfter } : {}) },
|
|
230
|
-
cookie,
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
if (path === `${AUTH_PREFIX}/logout` && req.method === "POST") {
|
|
234
|
-
return sendJson(
|
|
235
|
-
res,
|
|
236
|
-
200,
|
|
237
|
-
{ ok: true },
|
|
238
|
-
`${SESSION_COOKIE_NAME}=; Path=/; HttpOnly; Max-Age=0`,
|
|
239
|
-
);
|
|
240
|
-
}
|
|
50
|
+
function sendJson(
|
|
51
|
+
response: ServerResponse,
|
|
52
|
+
status: number,
|
|
53
|
+
body: unknown,
|
|
54
|
+
setCookie?: string,
|
|
55
|
+
): void {
|
|
56
|
+
response.writeHead(status, {
|
|
57
|
+
"content-type": "application/json",
|
|
58
|
+
...(setCookie ? { "set-cookie": setCookie } : {}),
|
|
59
|
+
});
|
|
60
|
+
response.end(JSON.stringify(body));
|
|
61
|
+
}
|
|
241
62
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
63
|
+
function sessionCookieValue(request: IncomingMessage): string | undefined {
|
|
64
|
+
return http.parseCookies(request.headers.cookie ?? null)[SESSION_COOKIE_NAME];
|
|
65
|
+
}
|
|
245
66
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Answer one of the four login routes, or return `false` when the request is not
|
|
69
|
+
* one of them. Mirrors the plugin's route handlers: `request` always reports
|
|
70
|
+
* success (anti-enumeration) and `verify` sets the session cookie on success.
|
|
71
|
+
*/
|
|
72
|
+
async function handleAuthRoute(
|
|
73
|
+
request: IncomingMessage,
|
|
74
|
+
response: ServerResponse,
|
|
75
|
+
path: string,
|
|
76
|
+
gate: AuthGateApi,
|
|
77
|
+
): Promise<boolean> {
|
|
78
|
+
const prefix = gateModule.AUTH_PREFIX;
|
|
79
|
+
const ip = gateModule.clientIp(request);
|
|
80
|
+
|
|
81
|
+
if (path === `${prefix}/status`) {
|
|
82
|
+
sendJson(response, 200, await gate.status(sessionCookieValue(request)));
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
if (path === `${prefix}/request` && request.method === "POST") {
|
|
86
|
+
const parsed = authRequestSchema.safeParse(json.parseRecord(await readBody(request)));
|
|
87
|
+
sendJson(response, 200, parsed.success ? await gate.request(parsed.data.email, ip) : { ok: true });
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
if (path === `${prefix}/verify` && request.method === "POST") {
|
|
91
|
+
const parsed = authVerifySchema.safeParse(json.parseRecord(await readBody(request)));
|
|
92
|
+
if (!parsed.success) {
|
|
93
|
+
sendJson(response, 200, { ok: false });
|
|
94
|
+
return true;
|
|
252
95
|
}
|
|
96
|
+
const result = await gate.verify(parsed.data.email, parsed.data.code, ip);
|
|
97
|
+
const cookie =
|
|
98
|
+
result.ok && result.token
|
|
99
|
+
? gateModule.sessionSetCookie(result.token, gate.sessionTtlSeconds)
|
|
100
|
+
: undefined;
|
|
101
|
+
sendJson(
|
|
102
|
+
response,
|
|
103
|
+
200,
|
|
104
|
+
{ ok: result.ok, ...(result.retryAfter ? { retryAfter: result.retryAfter } : {}) },
|
|
105
|
+
cookie,
|
|
106
|
+
);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
if (path === `${prefix}/logout` && request.method === "POST") {
|
|
110
|
+
sendJson(response, 200, { ok: true }, gateModule.LOGOUT_SET_COOKIE);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
253
115
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
116
|
+
/**
|
|
117
|
+
* Start the proxy and resolve once it is listening.
|
|
118
|
+
*
|
|
119
|
+
* `publicDomain` is intentionally NOT passed to `gateRequest`: on this path every
|
|
120
|
+
* request arrived on the public port, so it IS tunnel traffic by construction -
|
|
121
|
+
* whereas the in-process gate shares a port with the platform front door and has
|
|
122
|
+
* to tell them apart by `Host`. Passing the request's own host keeps the shared
|
|
123
|
+
* decision function's contract satisfied without weakening it.
|
|
124
|
+
*/
|
|
125
|
+
export function startProxy(options: ProxyOptions): Promise<void> {
|
|
126
|
+
const proxy = ProxyModule.createProxyServer({
|
|
127
|
+
target: `http://127.0.0.1:${options.appPort}`,
|
|
128
|
+
ws: true,
|
|
129
|
+
xfwd: true,
|
|
266
130
|
});
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
if (
|
|
284
|
-
|
|
131
|
+
const headerPolicy = headersModule.toHeaderPolicy(options.forwardHeaders);
|
|
132
|
+
const gate = options.gate;
|
|
133
|
+
|
|
134
|
+
const decide = (request: IncomingMessage): Promise<gateModule.GateAction> =>
|
|
135
|
+
gate
|
|
136
|
+
? gateModule.gateRequest(request, {
|
|
137
|
+
gate,
|
|
138
|
+
publicDomain: (request.headers.host ?? "").split(":")[0] || "localhost",
|
|
139
|
+
headerPolicy,
|
|
140
|
+
})
|
|
141
|
+
: Promise.resolve<gateModule.GateAction>("pass");
|
|
142
|
+
|
|
143
|
+
const server = createServer((request, response) => {
|
|
144
|
+
void (async () => {
|
|
145
|
+
const path = (request.url ?? "/").split("?")[0] ?? "/";
|
|
146
|
+
if (gate && (await handleAuthRoute(request, response, path, gate))) return;
|
|
147
|
+
if ((await decide(request)) === "deny") {
|
|
148
|
+
sendJson(response, 401, gateModule.UNAUTHORIZED_BODY);
|
|
285
149
|
return;
|
|
286
150
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
151
|
+
proxy.web(request, response);
|
|
152
|
+
})().catch((error: unknown) => {
|
|
153
|
+
logger.error("proxy request failed", { error });
|
|
154
|
+
if (!response.headersSent) sendJson(response, 502, { error: "bad gateway" });
|
|
155
|
+
else response.end();
|
|
290
156
|
});
|
|
291
157
|
});
|
|
292
158
|
|
|
159
|
+
// A websocket upgrade cannot be answered with a 401 body, so a denied one is
|
|
160
|
+
// destroyed - the client sees the handshake fail, which is what a browser's
|
|
161
|
+
// WebSocket error handler expects.
|
|
162
|
+
server.on("upgrade", (request: IncomingMessage, socket: Socket, head: Buffer) => {
|
|
163
|
+
void decide(request)
|
|
164
|
+
.then((action) => {
|
|
165
|
+
if (action === "deny") socket.destroy();
|
|
166
|
+
else proxy.ws(request, socket, head);
|
|
167
|
+
})
|
|
168
|
+
.catch(() => socket.destroy());
|
|
169
|
+
});
|
|
170
|
+
|
|
293
171
|
return new Promise((resolve) => {
|
|
294
|
-
server.listen(publicPort, "0.0.0.0", () => {
|
|
295
|
-
logger.info(
|
|
172
|
+
server.listen(options.publicPort, "0.0.0.0", () => {
|
|
173
|
+
logger.info("proxy listening", { publicPort: options.publicPort, appPort: options.appPort });
|
|
296
174
|
resolve();
|
|
297
175
|
});
|
|
298
176
|
});
|
package/bin/dbx-tools-tunnel.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* `dbx-tools-tunnel` / `dbxt-tunnel` entry: front an app with a public
|
|
4
|
-
* portr tunnel + email-OTP gate, wrapping the start command after `--`.
|
|
5
|
-
* Delegates to the commander program in `../src/cli`.
|
|
6
|
-
*/
|
|
7
|
-
import { CommanderError, runCli } from "../src/cli.ts";
|
|
8
|
-
|
|
9
|
-
runCli(process.argv).catch((err: unknown) => {
|
|
10
|
-
if (err instanceof CommanderError) process.exit(err.exitCode);
|
|
11
|
-
process.stderr.write(`${err instanceof Error ? (err.stack ?? err.message) : String(err)}\n`);
|
|
12
|
-
process.exit(1);
|
|
13
|
-
});
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* `dbx-tools-tunnel` / `dbxt-tunnel` entry: front an app with a public
|
|
4
|
-
* portr tunnel + email-OTP gate, wrapping the start command after `--`.
|
|
5
|
-
* Delegates to the commander program in `../src/cli`.
|
|
6
|
-
*/
|
|
7
|
-
import { CommanderError, runCli } from "../src/cli.js";
|
|
8
|
-
runCli(process.argv).catch((err) => {
|
|
9
|
-
if (err instanceof CommanderError)
|
|
10
|
-
process.exit(err.exitCode);
|
|
11
|
-
process.stderr.write(`${err instanceof Error ? (err.stack ?? err.message) : String(err)}\n`);
|
|
12
|
-
process.exit(1);
|
|
13
|
-
});
|
|
14
|
-
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGJ4LXRvb2xzLXR1bm5lbC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL2Jpbi9kYngtdG9vbHMtdHVubmVsLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFDQTs7OztHQUlHO0FBQ0gsT0FBTyxFQUFFLGNBQWMsRUFBRSxNQUFNLEVBQUUsTUFBTSxlQUFlLENBQUM7QUFFdkQsTUFBTSxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FBQyxLQUFLLENBQUMsQ0FBQyxHQUFZLEVBQUUsRUFBRTtJQUMxQyxJQUFJLEdBQUcsWUFBWSxjQUFjO1FBQUUsT0FBTyxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsUUFBUSxDQUFDLENBQUM7SUFDOUQsT0FBTyxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsR0FBRyxHQUFHLFlBQVksS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxLQUFLLElBQUksR0FBRyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxDQUFDO0lBQzdGLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFDbEIsQ0FBQyxDQUFDLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIjIS91c3IvYmluL2VudiBub2RlXG4vKipcbiAqIGBkYngtdG9vbHMtdHVubmVsYCAvIGBkYnh0LXR1bm5lbGAgZW50cnk6IGZyb250IGFuIGFwcCB3aXRoIGEgcHVibGljXG4gKiBwb3J0ciB0dW5uZWwgKyBlbWFpbC1PVFAgZ2F0ZSwgd3JhcHBpbmcgdGhlIHN0YXJ0IGNvbW1hbmQgYWZ0ZXIgYC0tYC5cbiAqIERlbGVnYXRlcyB0byB0aGUgY29tbWFuZGVyIHByb2dyYW0gaW4gYC4uL3NyYy9jbGlgLlxuICovXG5pbXBvcnQgeyBDb21tYW5kZXJFcnJvciwgcnVuQ2xpIH0gZnJvbSBcIi4uL3NyYy9jbGkudHNcIjtcblxucnVuQ2xpKHByb2Nlc3MuYXJndikuY2F0Y2goKGVycjogdW5rbm93bikgPT4ge1xuICBpZiAoZXJyIGluc3RhbmNlb2YgQ29tbWFuZGVyRXJyb3IpIHByb2Nlc3MuZXhpdChlcnIuZXhpdENvZGUpO1xuICBwcm9jZXNzLnN0ZGVyci53cml0ZShgJHtlcnIgaW5zdGFuY2VvZiBFcnJvciA/IChlcnIuc3RhY2sgPz8gZXJyLm1lc3NhZ2UpIDogU3RyaW5nKGVycil9XFxuYCk7XG4gIHByb2Nlc3MuZXhpdCgxKTtcbn0pO1xuIl19
|
package/lib/src/allowlist.d.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unified access allow-list matching for the email-OTP gate.
|
|
3
|
-
*
|
|
4
|
-
* Each pattern in the configured list is one of three shapes:
|
|
5
|
-
*
|
|
6
|
-
* - **domain shortcut** - `example.com` or `@example.com`: matches any
|
|
7
|
-
* address whose domain equals it. This is the gate's OWN semantic (a bare
|
|
8
|
-
* value means "the domain", not "the whole address"), so it is handled here.
|
|
9
|
-
* - **glob** - contains `*` or `?`, e.g. `*@example.com`: matched against the
|
|
10
|
-
* WHOLE address with shell-style wildcards.
|
|
11
|
-
* - **regex** - wrapped in slashes, `/.../ [flags]`: tested against the whole
|
|
12
|
-
* address. An invalid regex never matches (it is skipped with a warning
|
|
13
|
-
* rather than throwing).
|
|
14
|
-
*
|
|
15
|
-
* Only the first shape is this module's business: the glob and regex shapes are
|
|
16
|
-
* delegated to `@dbx-tools/shared-core`'s {@link pattern.toPattern}, which is
|
|
17
|
-
* where that compilation lives for every allow-list in the repo (the tunnel's
|
|
18
|
-
* inbound-header policy uses the same one). Matching is case-insensitive
|
|
19
|
-
* throughout.
|
|
20
|
-
*
|
|
21
|
-
* An EMPTY list matches nobody (fail closed): an app that enables the gate but
|
|
22
|
-
* configures no patterns lets no one in, which is the safe default.
|
|
23
|
-
*
|
|
24
|
-
* @module
|
|
25
|
-
*/
|
|
26
|
-
/**
|
|
27
|
-
* True when `email` is allowed by ANY pattern in `patterns`. An empty (or
|
|
28
|
-
* missing) list allows nobody - the gate fails closed.
|
|
29
|
-
*/
|
|
30
|
-
export declare function matchesAllowlist(email: string, patterns: readonly string[] | undefined): boolean;
|
|
31
|
-
/** Rough shape check so a clearly-invalid address is rejected before any work. */
|
|
32
|
-
export declare function looksLikeEmail(value: string): boolean;
|
package/lib/src/allowlist.js
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unified access allow-list matching for the email-OTP gate.
|
|
3
|
-
*
|
|
4
|
-
* Each pattern in the configured list is one of three shapes:
|
|
5
|
-
*
|
|
6
|
-
* - **domain shortcut** - `example.com` or `@example.com`: matches any
|
|
7
|
-
* address whose domain equals it. This is the gate's OWN semantic (a bare
|
|
8
|
-
* value means "the domain", not "the whole address"), so it is handled here.
|
|
9
|
-
* - **glob** - contains `*` or `?`, e.g. `*@example.com`: matched against the
|
|
10
|
-
* WHOLE address with shell-style wildcards.
|
|
11
|
-
* - **regex** - wrapped in slashes, `/.../ [flags]`: tested against the whole
|
|
12
|
-
* address. An invalid regex never matches (it is skipped with a warning
|
|
13
|
-
* rather than throwing).
|
|
14
|
-
*
|
|
15
|
-
* Only the first shape is this module's business: the glob and regex shapes are
|
|
16
|
-
* delegated to `@dbx-tools/shared-core`'s {@link pattern.toPattern}, which is
|
|
17
|
-
* where that compilation lives for every allow-list in the repo (the tunnel's
|
|
18
|
-
* inbound-header policy uses the same one). Matching is case-insensitive
|
|
19
|
-
* throughout.
|
|
20
|
-
*
|
|
21
|
-
* An EMPTY list matches nobody (fail closed): an app that enables the gate but
|
|
22
|
-
* configures no patterns lets no one in, which is the safe default.
|
|
23
|
-
*
|
|
24
|
-
* @module
|
|
25
|
-
*/
|
|
26
|
-
import { pattern } from "@dbx-tools/shared-core";
|
|
27
|
-
/** True when `email` matches a single allow-list `pattern`. */
|
|
28
|
-
function matchesPattern(email, entry) {
|
|
29
|
-
const trimmed = entry.trim();
|
|
30
|
-
if (!trimmed)
|
|
31
|
-
return false;
|
|
32
|
-
const address = email.trim().toLowerCase();
|
|
33
|
-
// A bare value with no wildcard and no regex delimiters is a DOMAIN shortcut,
|
|
34
|
-
// the one shape shared-core cannot infer: there it would mean whole-string
|
|
35
|
-
// equality against the address, which is never what an operator writing
|
|
36
|
-
// `example.com` in an access list intends.
|
|
37
|
-
if (!trimmed.startsWith("/") && !trimmed.includes("*") && !trimmed.includes("?")) {
|
|
38
|
-
const domain = trimmed.replace(/^@/, "").toLowerCase();
|
|
39
|
-
const at = address.lastIndexOf("@");
|
|
40
|
-
return at >= 0 && address.slice(at + 1) === domain;
|
|
41
|
-
}
|
|
42
|
-
return pattern.toPattern(trimmed)?.(address) ?? false;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* True when `email` is allowed by ANY pattern in `patterns`. An empty (or
|
|
46
|
-
* missing) list allows nobody - the gate fails closed.
|
|
47
|
-
*/
|
|
48
|
-
export function matchesAllowlist(email, patterns) {
|
|
49
|
-
if (!email || !patterns || patterns.length === 0)
|
|
50
|
-
return false;
|
|
51
|
-
return patterns.some((entry) => matchesPattern(email, entry));
|
|
52
|
-
}
|
|
53
|
-
/** Rough shape check so a clearly-invalid address is rejected before any work. */
|
|
54
|
-
export function looksLikeEmail(value) {
|
|
55
|
-
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim());
|
|
56
|
-
}
|
|
57
|
-
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYWxsb3dsaXN0LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2FsbG93bGlzdC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBd0JHO0FBRUgsT0FBTyxFQUFFLE9BQU8sRUFBRSxNQUFNLHdCQUF3QixDQUFDO0FBRWpELCtEQUErRDtBQUMvRCxTQUFTLGNBQWMsQ0FBQyxLQUFhLEVBQUUsS0FBYTtJQUNsRCxNQUFNLE9BQU8sR0FBRyxLQUFLLENBQUMsSUFBSSxFQUFFLENBQUM7SUFDN0IsSUFBSSxDQUFDLE9BQU87UUFBRSxPQUFPLEtBQUssQ0FBQztJQUMzQixNQUFNLE9BQU8sR0FBRyxLQUFLLENBQUMsSUFBSSxFQUFFLENBQUMsV0FBVyxFQUFFLENBQUM7SUFFM0MsOEVBQThFO0lBQzlFLDJFQUEyRTtJQUMzRSx3RUFBd0U7SUFDeEUsMkNBQTJDO0lBQzNDLElBQUksQ0FBQyxPQUFPLENBQUMsVUFBVSxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLFFBQVEsQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxRQUFRLENBQUMsR0FBRyxDQUFDLEVBQUUsQ0FBQztRQUNqRixNQUFNLE1BQU0sR0FBRyxPQUFPLENBQUMsT0FBTyxDQUFDLElBQUksRUFBRSxFQUFFLENBQUMsQ0FBQyxXQUFXLEVBQUUsQ0FBQztRQUN2RCxNQUFNLEVBQUUsR0FBRyxPQUFPLENBQUMsV0FBVyxDQUFDLEdBQUcsQ0FBQyxDQUFDO1FBQ3BDLE9BQU8sRUFBRSxJQUFJLENBQUMsSUFBSSxPQUFPLENBQUMsS0FBSyxDQUFDLEVBQUUsR0FBRyxDQUFDLENBQUMsS0FBSyxNQUFNLENBQUM7SUFDckQsQ0FBQztJQUVELE9BQU8sT0FBTyxDQUFDLFNBQVMsQ0FBQyxPQUFPLENBQUMsRUFBRSxDQUFDLE9BQU8sQ0FBQyxJQUFJLEtBQUssQ0FBQztBQUN4RCxDQUFDO0FBRUQ7OztHQUdHO0FBQ0gsTUFBTSxVQUFVLGdCQUFnQixDQUFDLEtBQWEsRUFBRSxRQUF1QztJQUNyRixJQUFJLENBQUMsS0FBSyxJQUFJLENBQUMsUUFBUSxJQUFJLFFBQVEsQ0FBQyxNQUFNLEtBQUssQ0FBQztRQUFFLE9BQU8sS0FBSyxDQUFDO0lBQy9ELE9BQU8sUUFBUSxDQUFDLElBQUksQ0FBQyxDQUFDLEtBQUssRUFBRSxFQUFFLENBQUMsY0FBYyxDQUFDLEtBQUssRUFBRSxLQUFLLENBQUMsQ0FBQyxDQUFDO0FBQ2hFLENBQUM7QUFFRCxrRkFBa0Y7QUFDbEYsTUFBTSxVQUFVLGNBQWMsQ0FBQyxLQUFhO0lBQzFDLE9BQU8sNEJBQTRCLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDO0FBQ3pELENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIFVuaWZpZWQgYWNjZXNzIGFsbG93LWxpc3QgbWF0Y2hpbmcgZm9yIHRoZSBlbWFpbC1PVFAgZ2F0ZS5cbiAqXG4gKiBFYWNoIHBhdHRlcm4gaW4gdGhlIGNvbmZpZ3VyZWQgbGlzdCBpcyBvbmUgb2YgdGhyZWUgc2hhcGVzOlxuICpcbiAqICAgLSAqKmRvbWFpbiBzaG9ydGN1dCoqIC0gYGV4YW1wbGUuY29tYCBvciBgQGV4YW1wbGUuY29tYDogbWF0Y2hlcyBhbnlcbiAqICAgICBhZGRyZXNzIHdob3NlIGRvbWFpbiBlcXVhbHMgaXQuIFRoaXMgaXMgdGhlIGdhdGUncyBPV04gc2VtYW50aWMgKGEgYmFyZVxuICogICAgIHZhbHVlIG1lYW5zIFwidGhlIGRvbWFpblwiLCBub3QgXCJ0aGUgd2hvbGUgYWRkcmVzc1wiKSwgc28gaXQgaXMgaGFuZGxlZCBoZXJlLlxuICogICAtICoqZ2xvYioqIC0gY29udGFpbnMgYCpgIG9yIGA/YCwgZS5nLiBgKkBleGFtcGxlLmNvbWA6IG1hdGNoZWQgYWdhaW5zdCB0aGVcbiAqICAgICBXSE9MRSBhZGRyZXNzIHdpdGggc2hlbGwtc3R5bGUgd2lsZGNhcmRzLlxuICogICAtICoqcmVnZXgqKiAtIHdyYXBwZWQgaW4gc2xhc2hlcywgYC8uLi4vIFtmbGFnc11gOiB0ZXN0ZWQgYWdhaW5zdCB0aGUgd2hvbGVcbiAqICAgICBhZGRyZXNzLiBBbiBpbnZhbGlkIHJlZ2V4IG5ldmVyIG1hdGNoZXMgKGl0IGlzIHNraXBwZWQgd2l0aCBhIHdhcm5pbmdcbiAqICAgICByYXRoZXIgdGhhbiB0aHJvd2luZykuXG4gKlxuICogT25seSB0aGUgZmlyc3Qgc2hhcGUgaXMgdGhpcyBtb2R1bGUncyBidXNpbmVzczogdGhlIGdsb2IgYW5kIHJlZ2V4IHNoYXBlcyBhcmVcbiAqIGRlbGVnYXRlZCB0byBgQGRieC10b29scy9zaGFyZWQtY29yZWAncyB7QGxpbmsgcGF0dGVybi50b1BhdHRlcm59LCB3aGljaCBpc1xuICogd2hlcmUgdGhhdCBjb21waWxhdGlvbiBsaXZlcyBmb3IgZXZlcnkgYWxsb3ctbGlzdCBpbiB0aGUgcmVwbyAodGhlIHR1bm5lbCdzXG4gKiBpbmJvdW5kLWhlYWRlciBwb2xpY3kgdXNlcyB0aGUgc2FtZSBvbmUpLiBNYXRjaGluZyBpcyBjYXNlLWluc2Vuc2l0aXZlXG4gKiB0aHJvdWdob3V0LlxuICpcbiAqIEFuIEVNUFRZIGxpc3QgbWF0Y2hlcyBub2JvZHkgKGZhaWwgY2xvc2VkKTogYW4gYXBwIHRoYXQgZW5hYmxlcyB0aGUgZ2F0ZSBidXRcbiAqIGNvbmZpZ3VyZXMgbm8gcGF0dGVybnMgbGV0cyBubyBvbmUgaW4sIHdoaWNoIGlzIHRoZSBzYWZlIGRlZmF1bHQuXG4gKlxuICogQG1vZHVsZVxuICovXG5cbmltcG9ydCB7IHBhdHRlcm4gfSBmcm9tIFwiQGRieC10b29scy9zaGFyZWQtY29yZVwiO1xuXG4vKiogVHJ1ZSB3aGVuIGBlbWFpbGAgbWF0Y2hlcyBhIHNpbmdsZSBhbGxvdy1saXN0IGBwYXR0ZXJuYC4gKi9cbmZ1bmN0aW9uIG1hdGNoZXNQYXR0ZXJuKGVtYWlsOiBzdHJpbmcsIGVudHJ5OiBzdHJpbmcpOiBib29sZWFuIHtcbiAgY29uc3QgdHJpbW1lZCA9IGVudHJ5LnRyaW0oKTtcbiAgaWYgKCF0cmltbWVkKSByZXR1cm4gZmFsc2U7XG4gIGNvbnN0IGFkZHJlc3MgPSBlbWFpbC50cmltKCkudG9Mb3dlckNhc2UoKTtcblxuICAvLyBBIGJhcmUgdmFsdWUgd2l0aCBubyB3aWxkY2FyZCBhbmQgbm8gcmVnZXggZGVsaW1pdGVycyBpcyBhIERPTUFJTiBzaG9ydGN1dCxcbiAgLy8gdGhlIG9uZSBzaGFwZSBzaGFyZWQtY29yZSBjYW5ub3QgaW5mZXI6IHRoZXJlIGl0IHdvdWxkIG1lYW4gd2hvbGUtc3RyaW5nXG4gIC8vIGVxdWFsaXR5IGFnYWluc3QgdGhlIGFkZHJlc3MsIHdoaWNoIGlzIG5ldmVyIHdoYXQgYW4gb3BlcmF0b3Igd3JpdGluZ1xuICAvLyBgZXhhbXBsZS5jb21gIGluIGFuIGFjY2VzcyBsaXN0IGludGVuZHMuXG4gIGlmICghdHJpbW1lZC5zdGFydHNXaXRoKFwiL1wiKSAmJiAhdHJpbW1lZC5pbmNsdWRlcyhcIipcIikgJiYgIXRyaW1tZWQuaW5jbHVkZXMoXCI/XCIpKSB7XG4gICAgY29uc3QgZG9tYWluID0gdHJpbW1lZC5yZXBsYWNlKC9eQC8sIFwiXCIpLnRvTG93ZXJDYXNlKCk7XG4gICAgY29uc3QgYXQgPSBhZGRyZXNzLmxhc3RJbmRleE9mKFwiQFwiKTtcbiAgICByZXR1cm4gYXQgPj0gMCAmJiBhZGRyZXNzLnNsaWNlKGF0ICsgMSkgPT09IGRvbWFpbjtcbiAgfVxuXG4gIHJldHVybiBwYXR0ZXJuLnRvUGF0dGVybih0cmltbWVkKT8uKGFkZHJlc3MpID8/IGZhbHNlO1xufVxuXG4vKipcbiAqIFRydWUgd2hlbiBgZW1haWxgIGlzIGFsbG93ZWQgYnkgQU5ZIHBhdHRlcm4gaW4gYHBhdHRlcm5zYC4gQW4gZW1wdHkgKG9yXG4gKiBtaXNzaW5nKSBsaXN0IGFsbG93cyBub2JvZHkgLSB0aGUgZ2F0ZSBmYWlscyBjbG9zZWQuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBtYXRjaGVzQWxsb3dsaXN0KGVtYWlsOiBzdHJpbmcsIHBhdHRlcm5zOiByZWFkb25seSBzdHJpbmdbXSB8IHVuZGVmaW5lZCk6IGJvb2xlYW4ge1xuICBpZiAoIWVtYWlsIHx8ICFwYXR0ZXJucyB8fCBwYXR0ZXJucy5sZW5ndGggPT09IDApIHJldHVybiBmYWxzZTtcbiAgcmV0dXJuIHBhdHRlcm5zLnNvbWUoKGVudHJ5KSA9PiBtYXRjaGVzUGF0dGVybihlbWFpbCwgZW50cnkpKTtcbn1cblxuLyoqIFJvdWdoIHNoYXBlIGNoZWNrIHNvIGEgY2xlYXJseS1pbnZhbGlkIGFkZHJlc3MgaXMgcmVqZWN0ZWQgYmVmb3JlIGFueSB3b3JrLiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGxvb2tzTGlrZUVtYWlsKHZhbHVlOiBzdHJpbmcpOiBib29sZWFuIHtcbiAgcmV0dXJuIC9eW15cXHNAXStAW15cXHNAXStcXC5bXlxcc0BdKyQvLnRlc3QodmFsdWUudHJpbSgpKTtcbn1cbiJdfQ==
|
package/lib/src/env.d.ts
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Environment-variable names the tunnel reads.
|
|
3
|
-
*
|
|
4
|
-
* Every setting is `TUNNEL_`-prefixed, matching the repo convention of naming a
|
|
5
|
-
* variable after the package that owns it (`MASTRA_*`, `TEAMS_*`,
|
|
6
|
-
* `WEB_SEARCH_*`). The gate's original names were unprefixed (`AUTH_SUBJECT`,
|
|
7
|
-
* `PUBLIC_DOMAIN`, ...), which is a real hazard for a package that runs as a
|
|
8
|
-
* WRAPPER: the tunnel and the app it wraps share one environment, so a generic
|
|
9
|
-
* name is one the wrapped app may already use for something else, and
|
|
10
|
-
* `PUBLIC_DOMAIN` in particular reads like an app-wide setting rather than a
|
|
11
|
-
* portr detail. `EMAIL_AUTH_ALLOW` was worse than generic - it sat in
|
|
12
|
-
* `@dbx-tools/email`'s `EMAIL_*` namespace while configuring the gate, not email.
|
|
13
|
-
*
|
|
14
|
-
* Each entry is an {@link EnvKey} list, EARLIEST-WINS, whose first element is the
|
|
15
|
-
* current name and whose remaining elements are the deprecated originals. A
|
|
16
|
-
* deployment set up against the old names keeps working; nothing needs a
|
|
17
|
-
* coordinated rename. Read them through `env.string` / `env.positiveInt` /
|
|
18
|
-
* `env.list`, which accept the list directly.
|
|
19
|
-
*
|
|
20
|
-
* Not renamed:
|
|
21
|
-
*
|
|
22
|
-
* - `DATABRICKS_APP_PORT` - the Databricks Apps runtime contract. The platform
|
|
23
|
-
* sets it; the gate honours it.
|
|
24
|
-
* - `PORTR_TOKEN` / `PORTR_SERVER` / `PORTR_AUTO_ADD_PATH` - upstream
|
|
25
|
-
* [portr](https://github.com/amalshaji/portr)'s own namespace, and
|
|
26
|
-
* `PORTR_AUTO_ADD_PATH` is passed straight to that binary. Renaming these
|
|
27
|
-
* would rename someone else's contract.
|
|
28
|
-
*
|
|
29
|
-
* @module
|
|
30
|
-
*/
|
|
31
|
-
import type { EnvKey } from "@dbx-tools/shared-core";
|
|
32
|
-
/** Access allow-list patterns (domain / glob / `/regex/`). */
|
|
33
|
-
export declare const ALLOW_ENV: EnvKey;
|
|
34
|
-
/** Subject line for the code email. */
|
|
35
|
-
export declare const SUBJECT_ENV: EnvKey;
|
|
36
|
-
/** Display name used in the code email copy. */
|
|
37
|
-
export declare const BRAND_NAME_ENV: EnvKey;
|
|
38
|
-
/** Line shown immediately above the code in the email. */
|
|
39
|
-
export declare const MESSAGE_ENV: EnvKey;
|
|
40
|
-
/** Session lifetime, in seconds. */
|
|
41
|
-
export declare const SESSION_TTL_ENV: EnvKey;
|
|
42
|
-
/** One-time-code lifetime, in seconds. */
|
|
43
|
-
export declare const CODE_TTL_ENV: EnvKey;
|
|
44
|
-
/** HS256 signing secret for the session JWT. */
|
|
45
|
-
export declare const JWT_SECRET_ENV: EnvKey;
|
|
46
|
-
/**
|
|
47
|
-
* Force-clear cutoff: every session issued BEFORE it stops verifying. Anything
|
|
48
|
-
* `object.toDate` accepts - a date, an ISO instant, epoch seconds/millis, or a
|
|
49
|
-
* relative duration (`-30d`, `7 days ago`).
|
|
50
|
-
*/
|
|
51
|
-
export declare const SESSION_CUTOFF_ENV: EnvKey;
|
|
52
|
-
/** The public `<subdomain>.<server>` portr should serve on. */
|
|
53
|
-
export declare const PUBLIC_DOMAIN_ENV: EnvKey;
|
|
54
|
-
/** Run the tunnel OPEN, with no gate. Ignored unless truthy. */
|
|
55
|
-
export declare const INSECURE_ENV: EnvKey;
|
|
56
|
-
/** Extra `x-` request headers tunnel traffic may forward. */
|
|
57
|
-
export declare const FORWARD_HEADERS_ENV: EnvKey;
|