@dbx-tools/cli-tunnel 0.6.45
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 -0
- package/bin/dbx-tools-tunnel.ts +13 -0
- package/index.ts +20 -0
- package/lib/bin/dbx-tools-tunnel.d.ts +2 -0
- package/lib/bin/dbx-tools-tunnel.js +14 -0
- package/lib/index.d.ts +16 -0
- package/lib/index.js +16 -0
- package/lib/src/allowlist.d.ts +27 -0
- package/lib/src/allowlist.js +82 -0
- package/lib/src/app.d.ts +31 -0
- package/lib/src/app.js +71 -0
- package/lib/src/cli.d.ts +28 -0
- package/lib/src/cli.js +147 -0
- package/lib/src/otp.d.ts +50 -0
- package/lib/src/otp.js +134 -0
- package/lib/src/plugin.d.ts +99 -0
- package/lib/src/plugin.js +123 -0
- package/lib/src/portr.d.ts +39 -0
- package/lib/src/portr.js +90 -0
- package/lib/src/proxy.d.ts +40 -0
- package/lib/src/proxy.js +204 -0
- package/lib/src/rate-limit.d.ts +35 -0
- package/lib/src/rate-limit.js +53 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/package.json +117 -0
- package/src/allowlist.ts +88 -0
- package/src/app.ts +82 -0
- package/src/cli.ts +176 -0
- package/src/otp.ts +150 -0
- package/src/plugin.ts +200 -0
- package/src/portr.ts +110 -0
- package/src/proxy.ts +243 -0
- package/src/rate-limit.ts +59 -0
package/lib/src/proxy.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tunnel gate reverse-proxy.
|
|
3
|
+
*
|
|
4
|
+
* Binds the PUBLIC port (the container's `DATABRICKS_APP_PORT`) and forwards to
|
|
5
|
+
* the real app on a private loopback port. It is the single front door for both
|
|
6
|
+
* traffic paths into the container:
|
|
7
|
+
*
|
|
8
|
+
* - **portr client** (same container, connects over LOOPBACK) - the public
|
|
9
|
+
* tunnel. This is what the gate protects.
|
|
10
|
+
* - **Databricks control plane / front door** (reaches the `0.0.0.0` port from
|
|
11
|
+
* a NON-loopback container-network address) - passed through UNGATED, per the
|
|
12
|
+
* rule "if it's not from the portr client, let it in".
|
|
13
|
+
*
|
|
14
|
+
* The distinguisher is the connection's source address: a loopback
|
|
15
|
+
* `req.socket.remoteAddress` is the portr client; anything else is the platform.
|
|
16
|
+
*
|
|
17
|
+
* For portr traffic the gate is a standard SPA gate: static assets + the login
|
|
18
|
+
* flow (`/api/email/auth/*`, answered IN-PROCESS by this proxy via the
|
|
19
|
+
* {@link AuthGateApi}) are open so the browser can load the client and render the
|
|
20
|
+
* `<AuthGate>`; every other `/api/*` needs a valid session cookie or gets 401.
|
|
21
|
+
* WebSocket upgrades are gated the same way and forwarded with `http-proxy-3`.
|
|
22
|
+
*
|
|
23
|
+
* @module
|
|
24
|
+
*/
|
|
25
|
+
import { createServer } from "node:http";
|
|
26
|
+
import { http, json, log } from "@dbx-tools/shared-core";
|
|
27
|
+
import { authRequestSchema, authVerifySchema, SESSION_COOKIE_NAME } from "@dbx-tools/shared-email";
|
|
28
|
+
import ProxyModule from "http-proxy-3";
|
|
29
|
+
const logger = log.logger("tunnel:proxy");
|
|
30
|
+
/** Route prefix the login flow lives under (open, answered in-process). */
|
|
31
|
+
const AUTH_PREFIX = "/api/email/auth";
|
|
32
|
+
/** True when a socket address is loopback (the portr client, same container). */
|
|
33
|
+
function isLoopback(addr) {
|
|
34
|
+
if (!addr)
|
|
35
|
+
return false;
|
|
36
|
+
// Normalize IPv4-mapped IPv6 (`::ffff:127.0.0.1`) and bare IPv6 loopback.
|
|
37
|
+
const a = addr.replace(/^::ffff:/, "");
|
|
38
|
+
return a === "127.0.0.1" || a.startsWith("127.") || a === "::1" || a === "localhost";
|
|
39
|
+
}
|
|
40
|
+
/** Read the whole request body as text (small JSON payloads only). */
|
|
41
|
+
function readBody(req) {
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
let data = "";
|
|
44
|
+
req.on("data", (chunk) => (data += chunk));
|
|
45
|
+
req.on("end", () => resolve(data));
|
|
46
|
+
req.on("error", () => resolve(data));
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function sendJson(res, status, body, setCookie) {
|
|
50
|
+
const headers = { "content-type": "application/json" };
|
|
51
|
+
if (setCookie)
|
|
52
|
+
headers["set-cookie"] = setCookie;
|
|
53
|
+
res.writeHead(status, headers);
|
|
54
|
+
res.end(JSON.stringify(body));
|
|
55
|
+
}
|
|
56
|
+
/** Start the gate proxy. Resolves once it is listening. */
|
|
57
|
+
export function startProxy({ publicPort, appPort, gate }) {
|
|
58
|
+
const proxy = ProxyModule.createProxyServer({
|
|
59
|
+
target: { host: "127.0.0.1", port: appPort },
|
|
60
|
+
ws: true,
|
|
61
|
+
xfwd: true,
|
|
62
|
+
});
|
|
63
|
+
proxy.on("error", (err, _req, res) => {
|
|
64
|
+
logger.warn("upstream proxy error", { error: err.message });
|
|
65
|
+
const r = res;
|
|
66
|
+
if (r && "writeHead" in r && !r.headersSent) {
|
|
67
|
+
sendJson(r, 502, { error: "upstream unavailable" });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
/** The session cookie for a verified email, as a Set-Cookie string. */
|
|
71
|
+
const sessionCookie = (token, maxAgeSeconds) => [
|
|
72
|
+
`${SESSION_COOKIE_NAME}=${token}`,
|
|
73
|
+
"Path=/",
|
|
74
|
+
"HttpOnly",
|
|
75
|
+
"SameSite=Lax",
|
|
76
|
+
`Max-Age=${maxAgeSeconds}`,
|
|
77
|
+
process.env.NODE_ENV === "production" ? "Secure" : "",
|
|
78
|
+
]
|
|
79
|
+
.filter(Boolean)
|
|
80
|
+
.join("; ");
|
|
81
|
+
/** Client IP for rate-limiting: the portr-forwarded XFF, else the socket. */
|
|
82
|
+
const clientIp = (req) => {
|
|
83
|
+
const fwd = req.headers["x-forwarded-for"];
|
|
84
|
+
const first = Array.isArray(fwd) ? fwd[0] : fwd?.split(",")[0];
|
|
85
|
+
return (first ?? req.socket.remoteAddress ?? "unknown").trim();
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Remove the gate's session cookie from the `Cookie` header before forwarding,
|
|
89
|
+
* so the app never sees `dbx_auth` (it is the proxy's concern, not the app's).
|
|
90
|
+
* Preserves any other cookies. Removes the header entirely when it becomes empty.
|
|
91
|
+
*/
|
|
92
|
+
const stripSessionCookie = (req) => {
|
|
93
|
+
const raw = req.headers.cookie;
|
|
94
|
+
if (!raw)
|
|
95
|
+
return;
|
|
96
|
+
const kept = raw
|
|
97
|
+
.split(";")
|
|
98
|
+
.map((c) => c.trim())
|
|
99
|
+
.filter((c) => c && !c.startsWith(`${SESSION_COOKIE_NAME}=`));
|
|
100
|
+
if (kept.length)
|
|
101
|
+
req.headers.cookie = kept.join("; ");
|
|
102
|
+
else
|
|
103
|
+
delete req.headers.cookie;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Present an OTP-authenticated caller to the app the SAME way the Databricks
|
|
107
|
+
* front door does: set `x-forwarded-user` / `x-forwarded-email` to the verified
|
|
108
|
+
* address (AppKit reads `x-forwarded-user` for the OBO user id). Any inbound
|
|
109
|
+
* copies are overwritten so a client can't spoof identity through the gate.
|
|
110
|
+
*/
|
|
111
|
+
const injectIdentity = (req, email) => {
|
|
112
|
+
req.headers["x-forwarded-user"] = email;
|
|
113
|
+
req.headers["x-forwarded-email"] = email;
|
|
114
|
+
};
|
|
115
|
+
const server = createServer(async (req, res) => {
|
|
116
|
+
const path = (req.url ?? "/").split("?")[0];
|
|
117
|
+
// Insecure/open mode (no gate) OR non-loopback traffic (the Databricks front
|
|
118
|
+
// door / control plane): forward ungated.
|
|
119
|
+
if (!gate || !isLoopback(req.socket.remoteAddress)) {
|
|
120
|
+
proxy.web(req, res);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// --- portr traffic: the gate applies ---
|
|
124
|
+
// The login flow is answered IN-PROCESS (the app has no server to forward to).
|
|
125
|
+
if (path === `${AUTH_PREFIX}/status`) {
|
|
126
|
+
const token = http.parseCookies(req.headers.cookie ?? null)[SESSION_COOKIE_NAME];
|
|
127
|
+
sendJson(res, 200, await gate.status(token));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (path === `${AUTH_PREFIX}/request` && req.method === "POST") {
|
|
131
|
+
const parsed = authRequestSchema.safeParse(json.parseRecord(await readBody(req)));
|
|
132
|
+
if (!parsed.success)
|
|
133
|
+
return sendJson(res, 200, { ok: true }); // anti-enumeration
|
|
134
|
+
return sendJson(res, 200, await gate.request(parsed.data.email, clientIp(req)));
|
|
135
|
+
}
|
|
136
|
+
if (path === `${AUTH_PREFIX}/verify` && req.method === "POST") {
|
|
137
|
+
const parsed = authVerifySchema.safeParse(json.parseRecord(await readBody(req)));
|
|
138
|
+
if (!parsed.success)
|
|
139
|
+
return sendJson(res, 200, { ok: false });
|
|
140
|
+
const result = await gate.verify(parsed.data.email, parsed.data.code, clientIp(req));
|
|
141
|
+
const cookie = result.ok && result.token ? sessionCookie(result.token, gate.sessionTtlSeconds) : undefined;
|
|
142
|
+
return sendJson(res, 200, { ok: result.ok, ...(result.retryAfter ? { retryAfter: result.retryAfter } : {}) }, cookie);
|
|
143
|
+
}
|
|
144
|
+
if (path === `${AUTH_PREFIX}/logout` && req.method === "POST") {
|
|
145
|
+
return sendJson(res, 200, { ok: true }, `${SESSION_COOKIE_NAME}=; Path=/; HttpOnly; Max-Age=0`);
|
|
146
|
+
}
|
|
147
|
+
// Anti-spoof: strip any inbound identity headers on portr traffic - only the
|
|
148
|
+
// gate may set them, below, for a verified session.
|
|
149
|
+
delete req.headers["x-forwarded-user"];
|
|
150
|
+
delete req.headers["x-forwarded-email"];
|
|
151
|
+
// Static (non-API) loads freely so the SPA + <AuthGate> can render. Strip the
|
|
152
|
+
// session cookie so it never leaks to the static handler.
|
|
153
|
+
if (!path.startsWith("/api/")) {
|
|
154
|
+
stripSessionCookie(req);
|
|
155
|
+
proxy.web(req, res);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
// Every other /api/* requires a valid session.
|
|
159
|
+
const token = http.parseCookies(req.headers.cookie ?? null)[SESSION_COOKIE_NAME];
|
|
160
|
+
const email = await gate.session(token);
|
|
161
|
+
if (email) {
|
|
162
|
+
// Present the OTP user like the Databricks front door, and drop the gate
|
|
163
|
+
// cookie so the app sees a clean, front-door-shaped request.
|
|
164
|
+
injectIdentity(req, email);
|
|
165
|
+
stripSessionCookie(req);
|
|
166
|
+
proxy.web(req, res);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
sendJson(res, 401, { error: "authentication required", loginPath: AUTH_PREFIX });
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
// WebSocket upgrades: forward front-door traffic untouched; gate loopback
|
|
173
|
+
// (portr) API upgrades the same way as HTTP (strip cookie + inject identity).
|
|
174
|
+
server.on("upgrade", (req, socket, head) => {
|
|
175
|
+
if (!gate || !isLoopback(req.socket.remoteAddress)) {
|
|
176
|
+
proxy.ws(req, socket, head);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
delete req.headers["x-forwarded-user"];
|
|
180
|
+
delete req.headers["x-forwarded-email"];
|
|
181
|
+
if (!(req.url ?? "").startsWith("/api/")) {
|
|
182
|
+
stripSessionCookie(req);
|
|
183
|
+
proxy.ws(req, socket, head);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const token = http.parseCookies(req.headers.cookie ?? null)[SESSION_COOKIE_NAME];
|
|
187
|
+
void gate.session(token).then((email) => {
|
|
188
|
+
if (!email) {
|
|
189
|
+
socket.destroy();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
injectIdentity(req, email);
|
|
193
|
+
stripSessionCookie(req);
|
|
194
|
+
proxy.ws(req, socket, head);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
return new Promise((resolve) => {
|
|
198
|
+
server.listen(publicPort, "0.0.0.0", () => {
|
|
199
|
+
logger.info(`gate proxy on 0.0.0.0:${publicPort} -> app 127.0.0.1:${appPort}`);
|
|
200
|
+
resolve();
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHJveHkuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvcHJveHkudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBdUJHO0FBRUgsT0FBTyxFQUFFLFlBQVksRUFBNkMsTUFBTSxXQUFXLENBQUM7QUFFcEYsT0FBTyxFQUFFLElBQUksRUFBRSxJQUFJLEVBQUUsR0FBRyxFQUFFLE1BQU0sd0JBQXdCLENBQUM7QUFDekQsT0FBTyxFQUFFLGlCQUFpQixFQUFFLGdCQUFnQixFQUFFLG1CQUFtQixFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFDbkcsT0FBTyxXQUFXLE1BQU0sY0FBYyxDQUFDO0FBR3ZDLE1BQU0sTUFBTSxHQUFHLEdBQUcsQ0FBQyxNQUFNLENBQUMsY0FBYyxDQUFDLENBQUM7QUFFMUMsMkVBQTJFO0FBQzNFLE1BQU0sV0FBVyxHQUFHLGlCQUFpQixDQUFDO0FBRXRDLGlGQUFpRjtBQUNqRixTQUFTLFVBQVUsQ0FBQyxJQUF3QjtJQUMxQyxJQUFJLENBQUMsSUFBSTtRQUFFLE9BQU8sS0FBSyxDQUFDO0lBQ3hCLDBFQUEwRTtJQUMxRSxNQUFNLENBQUMsR0FBRyxJQUFJLENBQUMsT0FBTyxDQUFDLFVBQVUsRUFBRSxFQUFFLENBQUMsQ0FBQztJQUN2QyxPQUFPLENBQUMsS0FBSyxXQUFXLElBQUksQ0FBQyxDQUFDLFVBQVUsQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLEtBQUssS0FBSyxJQUFJLENBQUMsS0FBSyxXQUFXLENBQUM7QUFDdkYsQ0FBQztBQUVELHNFQUFzRTtBQUN0RSxTQUFTLFFBQVEsQ0FBQyxHQUFvQjtJQUNwQyxPQUFPLElBQUksT0FBTyxDQUFDLENBQUMsT0FBTyxFQUFFLEVBQUU7UUFDN0IsSUFBSSxJQUFJLEdBQUcsRUFBRSxDQUFDO1FBQ2QsR0FBRyxDQUFDLEVBQUUsQ0FBQyxNQUFNLEVBQUUsQ0FBQyxLQUFLLEVBQUUsRUFBRSxDQUFDLENBQUMs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LENBQUMsQ0FBQztJQUVILE9BQU8sSUFBSSxPQUFPLENBQUMsQ0FBQyxPQUFPLEVBQUUsRUFBRTtRQUM3QixNQUFNLENBQUMsTUFBTSxDQUFDLFVBQVUsRUFBRSxTQUFTLEVBQUUsR0FBRyxFQUFFO1lBQ3hDLE1BQU0sQ0FBQyxJQUFJLENBQUMseUJBQXlCLFVBQVUscUJBQXFCLE9BQU8sRUFBRSxDQUFDLENBQUM7WUFDL0UsT0FBTyxFQUFFLENBQUM7UUFDWixDQUFDLENBQUMsQ0FBQztJQUNMLENBQUMsQ0FBQyxDQUFDO0FBQ0wsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogVGhlIHR1bm5lbCBnYXRlIHJldmVyc2UtcHJveHkuXG4gKlxuICogQmluZHMgdGhlIFBVQkxJQyBwb3J0ICh0aGUgY29udGFpbmVyJ3MgYERBVEFCUklDS1NfQVBQX1BPUlRgKSBhbmQgZm9yd2FyZHMgdG9cbiAqIHRoZSByZWFsIGFwcCBvbiBhIHByaXZhdGUgbG9vcGJhY2sgcG9ydC4gSXQgaXMgdGhlIHNpbmdsZSBmcm9udCBkb29yIGZvciBib3RoXG4gKiB0cmFmZmljIHBhdGhzIGludG8gdGhlIGNvbnRhaW5lcjpcbiAqXG4gKiAgIC0gKipwb3J0ciBjbGllbnQqKiAoc2FtZSBjb250YWluZXIsIGNvbm5lY3RzIG92ZXIgTE9PUEJBQ0spIC0gdGhlIHB1YmxpY1xuICogICAgIHR1bm5lbC4gVGhpcyBpcyB3aGF0IHRoZSBnYXRlIHByb3RlY3RzLlxuICogICAtICoqRGF0YWJyaWNrcyBjb250cm9sIHBsYW5lIC8gZnJvbnQgZG9vcioqIChyZWFjaGVzIHRoZSBgMC4wLjAuMGAgcG9ydCBmcm9tXG4gKiAgICAgYSBOT04tbG9vcGJhY2sgY29udGFpbmVyLW5ldHdvcmsgYWRkcmVzcykgLSBwYXNzZWQgdGhyb3VnaCBVTkdBVEVELCBwZXIgdGhlXG4gKiAgICAgcnVsZSBcImlmIGl0J3Mgbm90IGZyb20gdGhlIHBvcnRyIGNsaWVudCwgbGV0IGl0IGluXCIuXG4gKlxuICogVGhlIGRpc3Rpbmd1aXNoZXIgaXMgdGhlIGNvbm5lY3Rpb24ncyBzb3VyY2UgYWRkcmVzczogYSBsb29wYmFja1xuICogYHJlcS5zb2NrZXQucmVtb3RlQWRkcmVzc2AgaXMgdGhlIHBvcnRyIGNsaWVudDsgYW55dGhpbmcgZWxzZSBpcyB0aGUgcGxhdGZvcm0uXG4gKlxuICogRm9yIHBvcnRyIHRyYWZmaWMgdGhlIGdhdGUgaXMgYSBzdGFuZGFyZCBTUEEgZ2F0ZTogc3RhdGljIGFzc2V0cyArIHRoZSBsb2dpblxuICogZmxvdyAoYC9hcGkvZW1haWwvYXV0aC8qYCwgYW5zd2VyZWQgSU4tUFJPQ0VTUyBieSB0aGlzIHByb3h5IHZpYSB0aGVcbiAqIHtAbGluayBBdXRoR2F0ZUFwaX0pIGFyZSBvcGVuIHNvIHRoZSBicm93c2VyIGNhbiBsb2FkIHRoZSBjbGllbnQgYW5kIHJlbmRlciB0aGVcbiAqIGA8QXV0aEdhdGU+YDsgZXZlcnkgb3RoZXIgYC9hcGkvKmAgbmVlZHMgYSB2YWxpZCBzZXNzaW9uIGNvb2tpZSBvciBnZXRzIDQwMS5cbiAqIFdlYlNvY2tldCB1cGdyYWRlcyBhcmUgZ2F0ZWQgdGhlIHNhbWUgd2F5IGFuZCBmb3J3YXJkZWQgd2l0aCBgaHR0cC1wcm94eS0zYC5cbiAqXG4gKiBAbW9kdWxlXG4gKi9cblxuaW1wb3J0IHsgY3JlYXRlU2VydmVyLCB0eXBlIEluY29taW5nTWVzc2FnZSwgdHlwZSBTZXJ2ZXJSZXNwb25zZSB9IGZyb20gXCJub2RlOmh0dHBcIjtcbmltcG9ydCB0eXBlIHsgU29ja2V0IH0gZnJvbSBcIm5vZGU6bmV0XCI7XG5pbXBvcnQgeyBodHRwLCBqc29uLCBsb2cgfSBmcm9tIFwiQGRieC10b29scy9zaGFyZWQtY29yZVwiO1xuaW1wb3J0IHsgYXV0aFJlcXVlc3RTY2hlbWEsIGF1dGhWZXJpZnlTY2hlbWEsIFNFU1NJT05fQ09PS0lFX05BTUUgfSBmcm9tIFwiQGRieC10b29scy9zaGFyZWQtZW1haWxcIjtcbmltcG9ydCBQcm94eU1vZHVsZSBmcm9tIFwiaHR0cC1wcm94eS0zXCI7XG5pbXBvcnQgdHlwZSB7IEF1dGhHYXRlQXBpIH0gZnJvbSBcIi4vcGx1Z2luLnRzXCI7XG5cbmNvbnN0IGxvZ2dlciA9IGxvZy5sb2dnZXIoXCJ0dW5uZWw6cHJveHlcIik7XG5cbi8qKiBSb3V0ZSBwcmVmaXggdGhlIGxvZ2luIGZsb3cgbGl2ZXMgdW5kZXIgKG9wZW4sIGFuc3dlcmVkIGluLXByb2Nlc3MpLiAqL1xuY29uc3QgQVVUSF9QUkVGSVggPSBcIi9hcGkvZW1haWwvYXV0aFwiO1xuXG4vKiogVHJ1ZSB3aGVuIGEgc29ja2V0IGFkZHJlc3MgaXMgbG9vcGJhY2sgKHRoZSBwb3J0ciBjbGllbnQsIHNhbWUgY29udGFpbmVyKS4gKi9cbmZ1bmN0aW9uIGlzTG9vcGJhY2soYWRkcjogc3RyaW5nIHwgdW5kZWZpbmVkKTogYm9vbGVhbiB7XG4gIGlmICghYWRkcikgcmV0dXJuIGZhbHNlO1xuICAvLyBOb3JtYWxpemUgSVB2NC1tYXBwZWQgSVB2NiAoYDo6ZmZmZjoxMjcuMC4wLjFgKSBhbmQgYmFyZSBJUHY2IGxvb3BiYWNrLlxuICBjb25zdCBhID0gYWRkci5yZXBsYWNlKC9eOjpmZmZmOi8sIFwiXCIpO1xuICByZXR1cm4gYSA9PT0gXCIxMjcuMC4wLjFcIiB8fCBhLnN0YXJ0c1dpdGgoXCIxMjcuXCIpIHx8IGEgPT09IFwiOjoxXCIgfHwgYSA9PT0gXCJsb2NhbGhvc3RcIjtcbn1cblxuLyoqIFJlYWQgdGhlIHdob2xlIHJlcXVlc3QgYm9keSBhcyB0ZXh0IChzbWFsbCBKU09OIHBheWxvYWRzIG9ubHkpLiAqL1xuZnVuY3Rpb24gcmVhZEJvZHkocmVxOiBJbmNvbWluZ01lc3NhZ2UpOiBQcm9taXNlPHN0cmluZz4ge1xuICByZXR1cm4gbmV3IFByb21pc2UoKHJlc29sdmUpID0+IHtcbiAgICBsZXQgZGF0YSA9IFwiXCI7XG4gICAgcmVxLm9uKFwiZGF0YVwiLCAoY2h1bmspID0+IChkYXRhICs9IGNodW5rKSk7XG4gICAgcmVxLm9uKFwiZW5kXCIsICgpID0+IHJlc29sdmUoZGF0YSkpO1xuICAgIHJlcS5vbihcImVycm9yXCIsICgpID0+IHJlc29sdmUoZGF0YSkpO1xuICB9KTtcbn1cblxuZnVuY3Rpb24gc2VuZEpzb24ocmVzOiBTZXJ2ZXJSZXNwb25zZSwgc3RhdHVzOiBudW1iZXIsIGJvZHk6IHVua25vd24sIHNldENvb2tpZT86IHN0cmluZyk6IHZvaWQge1xuICBjb25zdCBoZWFkZXJzOiBSZWNvcmQ8c3RyaW5nLCBzdHJpbmc+ID0geyBcImNvbnRlbnQtdHlwZVwiOiBcImFwcGxpY2F0aW9uL2pzb25cIiB9O1xuICBpZiAoc2V0Q29va2llKSBoZWFkZXJzW1wic2V0LWNvb2tpZVwiXSA9IHNldENvb2tpZTtcbiAgcmVzLndyaXRlSGVhZChzdGF0dXMsIGhlYWRlcnMpO1xuICByZXMuZW5kKEpTT04uc3RyaW5naWZ5KGJvZHkpKTtcbn1cblxuLyoqIE9wdGlvbnMgZm9yIHtAbGluayBzdGFydFByb3h5fS4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgUHJveHlPcHRpb25zIHtcbiAgLyoqIFB1YmxpYyBwb3J0IHRvIGxpc3RlbiBvbiAodGhlIGNvbnRhaW5lcidzIERBVEFCUklDS1NfQVBQX1BPUlQpLiAqL1xuICBwdWJsaWNQb3J0OiBudW1iZXI7XG4gIC8qKiBQcml2YXRlIHBvcnQgdGhlIHJlYWwgYXBwIGxpc3RlbnMgb24gKGxvb3BiYWNrKS4gKi9cbiAgYXBwUG9ydDogbnVtYmVyO1xuICAvKipcbiAgICogVGhlIGluLXByb2Nlc3MgZ2F0ZSBBUEksIG9yIGB1bmRlZmluZWRgIHRvIHJ1biBPUEVOIChpbnNlY3VyZSBtb2RlKTogZXZlcnlcbiAgICogcmVxdWVzdCAtIGluY2x1ZGluZyBwb3J0ciB0cmFmZmljIC0gaXMgZm9yd2FyZGVkIHVuZ2F0ZWQuIFVzZWQgd2hlbiB0aGVcbiAgICogb3BlcmF0b3IgcGFzc2VkIGAtLWluc2VjdXJlYCAvIGBUVU5ORUxfSU5TRUNVUkU9dHJ1ZWAuXG4gICAqL1xuICBnYXRlPzogQXV0aEdhdGVBcGk7XG59XG5cbi8qKiBTdGFydCB0aGUgZ2F0ZSBwcm94eS4gUmVzb2x2ZXMgb25jZSBpdCBpcyBsaXN0ZW5pbmcuICovXG5leHBvcnQgZnVuY3Rpb24gc3RhcnRQcm94eSh7IHB1YmxpY1BvcnQsIGFwcFBvcnQsIGdhdGUgfTogUHJveHlPcHRpb25zKTogUHJvbWlzZTx2b2lkPiB7XG4gIGNvbnN0IHByb3h5ID0gUHJveHlNb2R1bGUuY3JlYXRlUHJveHlTZXJ2ZXIoe1xuICAgIHRhcmdldDogeyBob3N0OiBcIjEyNy4wLjAuMVwiLCBwb3J0OiBhcHBQb3J0IH0sXG4gICAgd3M6IHRydWUsXG4gICAgeGZ3ZDogdHJ1ZSxcbiAgfSk7XG4gIHByb3h5Lm9uKFwiZXJyb3JcIiwgKGVycjogRXJyb3IsIF9yZXE6IHVua25vd24sIHJlczogdW5rbm93bikgPT4ge1xuICAgIGxvZ2dlci53YXJuKFwidXBzdHJlYW0gcHJveHkgZXJyb3JcIiwgeyBlcnJvcjogZXJyLm1lc3NhZ2UgfSk7XG4gICAgY29uc3QgciA9IHJlcyBhcyBTZXJ2ZXJSZXNwb25zZSB8IHVuZGVmaW5lZDtcbiAgICBpZiAociAmJiBcIndyaXRlSGVhZFwiIGluIHIgJiYgIXIuaGVhZGVyc1NlbnQpIHtcbiAgICAgIHNlbmRKc29uKHIsIDUwMiwgeyBlcnJvcjogXCJ1cHN0cmVhbSB1bmF2YWlsYWJsZVwiIH0pO1xuICAgIH1cbiAgfSk7XG5cbiAgLyoqIFRoZSBzZXNzaW9uIGNvb2tpZSBmb3IgYSB2ZXJpZmllZCBlbWFpbCwgYXMgYSBTZXQtQ29va2llIHN0cmluZy4gKi9cbiAgY29uc3Qgc2Vzc2lvbkNvb2tpZSA9ICh0b2tlbjogc3RyaW5nLCBtYXhBZ2VTZWNvbmRzOiBudW1iZXIpOiBzdHJpbmcgPT5cbiAgICBbXG4gICAgICBgJHtTRVNTSU9OX0NPT0tJRV9OQU1FfT0ke3Rva2VufWAsXG4gICAgICBcIlBhdGg9L1wiLFxuICAgICAgXCJIdHRwT25seVwiLFxuICAgICAgXCJTYW1lU2l0ZT1MYXhcIixcbiAgICAgIGBNYXgtQWdlPSR7bWF4QWdlU2Vjb25kc31gLFxuICAgICAgcHJvY2Vzcy5lbnYuTk9ERV9FTlYgPT09IFwicHJvZHVjdGlvblwiID8gXCJTZWN1cmVcIiA6IFwiXCIsXG4gICAgXVxuICAgICAgLmZpbHRlcihCb29sZWFuKVxuICAgICAgLmpvaW4oXCI7IFwiKTtcblxuICAvKiogQ2xpZW50IElQIGZvciByYXRlLWxpbWl0aW5nOiB0aGUgcG9ydHItZm9yd2FyZGVkIFhGRiwgZWxzZSB0aGUgc29ja2V0LiAqL1xuICBjb25zdCBjbGllbnRJcCA9IChyZXE6IEluY29taW5nTWVzc2FnZSk6IHN0cmluZyA9PiB7XG4gICAgY29uc3QgZndkID0gcmVxLmhlYWRlcnNbXCJ4LWZvcndhcmRlZC1mb3JcIl07XG4gICAgY29uc3QgZmlyc3QgPSBBcnJheS5pc0FycmF5KGZ3ZCkgPyBmd2RbMF0gOiBmd2Q/LnNwbGl0KFwiLFwiKVswXTtcbiAgICByZXR1cm4gKGZpcnN0ID8/IHJlcS5zb2NrZXQucmVtb3RlQWRkcmVzcyA/PyBcInVua25vd25cIikudHJpbSgpO1xuICB9O1xuXG4gIC8qKlxuICAgKiBSZW1vdmUgdGhlIGdhdGUncyBzZXNzaW9uIGNvb2tpZSBmcm9tIHRoZSBgQ29va2llYCBoZWFkZXIgYmVmb3JlIGZvcndhcmRpbmcsXG4gICAqIHNvIHRoZSBhcHAgbmV2ZXIgc2VlcyBgZGJ4X2F1dGhgIChpdCBpcyB0aGUgcHJveHkncyBjb25jZXJuLCBub3QgdGhlIGFwcCdzKS5cbiAgICogUHJlc2VydmVzIGFueSBvdGhlciBjb29raWVzLiBSZW1vdmVzIHRoZSBoZWFkZXIgZW50aXJlbHkgd2hlbiBpdCBiZWNvbWVzIGVtcHR5LlxuICAgKi9cbiAgY29uc3Qgc3RyaXBTZXNzaW9uQ29va2llID0gKHJlcTogSW5jb21pbmdNZXNzYWdlKTogdm9pZCA9PiB7XG4gICAgY29uc3QgcmF3ID0gcmVxLmhlYWRlcnMuY29va2llO1xuICAgIGlmICghcmF3KSByZXR1cm47XG4gICAgY29uc3Qga2VwdCA9IHJhd1xuICAgICAgLnNwbGl0KFwiO1wiKVxuICAgICAgLm1hcCgoYykgPT4gYy50cmltKCkpXG4gICAgICAuZmlsdGVyKChjKSA9PiBjICYmICFjLnN0YXJ0c1dpdGgoYCR7U0VTU0lPTl9DT09LSUVfTkFNRX09YCkpO1xuICAgIGlmIChrZXB0Lmxlbmd0aCkgcmVxLmhlYWRlcnMuY29va2llID0ga2VwdC5qb2luKFwiOyBcIik7XG4gICAgZWxzZSBkZWxldGUgcmVxLmhlYWRlcnMuY29va2llO1xuICB9O1xuXG4gIC8qKlxuICAgKiBQcmVzZW50IGFuIE9UUC1hdXRoZW50aWNhdGVkIGNhbGxlciB0byB0aGUgYXBwIHRoZSBTQU1FIHdheSB0aGUgRGF0YWJyaWNrc1xuICAgKiBmcm9udCBkb29yIGRvZXM6IHNldCBgeC1mb3J3YXJkZWQtdXNlcmAgLyBgeC1mb3J3YXJkZWQtZW1haWxgIHRvIHRoZSB2ZXJpZmllZFxuICAgKiBhZGRyZXNzIChBcHBLaXQgcmVhZHMgYHgtZm9yd2FyZGVkLXVzZXJgIGZvciB0aGUgT0JPIHVzZXIgaWQpLiBBbnkgaW5ib3VuZFxuICAgKiBjb3BpZXMgYXJlIG92ZXJ3cml0dGVuIHNvIGEgY2xpZW50IGNhbid0IHNwb29mIGlkZW50aXR5IHRocm91Z2ggdGhlIGdhdGUuXG4gICAqL1xuICBjb25zdCBpbmplY3RJZGVudGl0eSA9IChyZXE6IEluY29taW5nTWVzc2FnZSwgZW1haWw6IHN0cmluZyk6IHZvaWQgPT4ge1xuICAgIHJlcS5oZWFkZXJzW1wieC1mb3J3YXJkZWQtdXNlclwiXSA9IGVtYWlsO1xuICAgIHJlcS5oZWFkZXJzW1wieC1mb3J3YXJkZWQtZW1haWxcIl0gPSBlbWFpbDtcbiAgfTtcblxuICBjb25zdCBzZXJ2ZXIgPSBjcmVhdGVTZXJ2ZXIoYXN5bmMgKHJlcSwgcmVzKSA9PiB7XG4gICAgY29uc3QgcGF0aCA9IChyZXEudXJsID8/IFwiL1wiKS5zcGxpdChcIj9cIilbMF0hO1xuXG4gICAgLy8gSW5zZWN1cmUvb3BlbiBtb2RlIChubyBnYXRlKSBPUiBub24tbG9vcGJhY2sgdHJhZmZpYyAodGhlIERhdGFicmlja3MgZnJvbnRcbiAgICAvLyBkb29yIC8gY29udHJvbCBwbGFuZSk6IGZvcndhcmQgdW5nYXRlZC5cbiAgICBpZiAoIWdhdGUgfHwgIWlzTG9vcGJhY2socmVxLnNvY2tldC5yZW1vdGVBZGRyZXNzKSkge1xuICAgICAgcHJveHkud2ViKHJlcSwgcmVzKTtcbiAgICAgIHJldHVybjtcbiAgICB9XG5cbiAgICAvLyAtLS0gcG9ydHIgdHJhZmZpYzogdGhlIGdhdGUgYXBwbGllcyAtLS1cblxuICAgIC8vIFRoZSBsb2dpbiBmbG93IGlzIGFuc3dlcmVkIElOLVBST0NFU1MgKHRoZSBhcHAgaGFzIG5vIHNlcnZlciB0byBmb3J3YXJkIHRvKS5cbiAgICBpZiAocGF0aCA9PT0gYCR7QVVUSF9QUkVGSVh9L3N0YXR1c2ApIHtcbiAgICAgIGNvbnN0IHRva2VuID0gaHR0cC5wYXJzZUNvb2tpZXMocmVxLmhlYWRlcnMuY29va2llID8/IG51bGwpW1NFU1NJT05fQ09PS0lFX05BTUVdO1xuICAgICAgc2VuZEpzb24ocmVzLCAyMDAsIGF3YWl0IGdhdGUuc3RhdHVzKHRva2VuKSk7XG4gICAgICByZXR1cm47XG4gICAgfVxuICAgIGlmIChwYXRoID09PSBgJHtBVVRIX1BSRUZJWH0vcmVxdWVzdGAgJiYgcmVxLm1ldGhvZCA9PT0gXCJQT1NUXCIpIHtcbiAgICAgIGNvbnN0IHBhcnNlZCA9IGF1dGhSZXF1ZXN0U2NoZW1hLnNhZmVQYXJzZShqc29uLnBhcnNlUmVjb3JkKGF3YWl0IHJlYWRCb2R5KHJlcSkpKTtcbiAgICAgIGlmICghcGFyc2VkLnN1Y2Nlc3MpIHJldHVybiBzZW5kSnNvbihyZXMsIDIwMCwgeyBvazogdHJ1ZSB9KTsgLy8gYW50aS1lbnVtZXJhdGlvblxuICAgICAgcmV0dXJuIHNlbmRKc29uKHJlcywgMjAwLCBhd2FpdCBnYXRlLnJlcXVlc3QocGFyc2VkLmRhdGEuZW1haWwsIGNsaWVudElwKHJlcSkpKTtcbiAgICB9XG4gICAgaWYgKHBhdGggPT09IGAke0FVVEhfUFJFRklYfS92ZXJpZnlgICYmIHJlcS5tZXRob2QgPT09IFwiUE9TVFwiKSB7XG4gICAgICBjb25zdCBwYXJzZWQgPSBhdXRoVmVyaWZ5U2NoZW1hLnNhZmVQYXJzZShqc29uLnBhcnNlUmVjb3JkKGF3YWl0IHJlYWRCb2R5KHJlcSkpKTtcbiAgICAgIGlmICghcGFyc2VkLnN1Y2Nlc3MpIHJldHVybiBzZW5kSnNvbihyZXMsIDIwMCwgeyBvazogZmFsc2UgfSk7XG4gICAgICBjb25zdCByZXN1bHQgPSBhd2FpdCBnYXRlLnZlcmlmeShwYXJzZWQuZGF0YS5lbWFpbCwgcGFyc2VkLmRhdGEuY29kZSwgY2xpZW50SXAocmVxKSk7XG4gICAgICBjb25zdCBjb29raWUgPVxuICAgICAgICByZXN1bHQub2sgJiYgcmVzdWx0LnRva2VuID8gc2Vzc2lvbkNvb2tpZShyZXN1bHQudG9rZW4sIGdhdGUuc2Vzc2lvblR0bFNlY29uZHMpIDogdW5kZWZpbmVkO1xuICAgICAgcmV0dXJuIHNlbmRKc29uKFxuICAgICAgICByZXMsXG4gICAgICAgIDIwMCxcbiAgICAgICAgeyBvazogcmVzdWx0Lm9rLCAuLi4ocmVzdWx0LnJldHJ5QWZ0ZXIgPyB7IHJldHJ5QWZ0ZXI6IHJlc3VsdC5yZXRyeUFmdGVyIH0gOiB7fSkgfSxcbiAgICAgICAgY29va2llLFxuICAgICAgKTtcbiAgICB9XG4gICAgaWYgKHBhdGggPT09IGAke0FVVEhfUFJFRklYfS9sb2dvdXRgICYmIHJlcS5tZXRob2QgPT09IFwiUE9TVFwiKSB7XG4gICAgICByZXR1cm4gc2VuZEpzb24oXG4gICAgICAgIHJlcyxcbiAgICAgICAgMjAwLFxuICAgICAgICB7IG9rOiB0cnVlIH0sXG4gICAgICAgIGAke1NFU1NJT05fQ09PS0lFX05BTUV9PTsgUGF0aD0vOyBIdHRwT25seTsgTWF4LUFnZT0wYCxcbiAgICAgICk7XG4gICAgfVxuXG4gICAgLy8gQW50aS1zcG9vZjogc3RyaXAgYW55IGluYm91bmQgaWRlbnRpdHkgaGVhZGVycyBvbiBwb3J0ciB0cmFmZmljIC0gb25seSB0aGVcbiAgICAvLyBnYXRlIG1heSBzZXQgdGhlbSwgYmVsb3csIGZvciBhIHZlcmlmaWVkIHNlc3Npb24uXG4gICAgZGVsZXRlIHJlcS5oZWFkZXJzW1wieC1mb3J3YXJkZWQtdXNlclwiXTtcbiAgICBkZWxldGUgcmVxLmhlYWRlcnNbXCJ4LWZvcndhcmRlZC1lbWFpbFwiXTtcblxuICAgIC8vIFN0YXRpYyAobm9uLUFQSSkgbG9hZHMgZnJlZWx5IHNvIHRoZSBTUEEgKyA8QXV0aEdhdGU+IGNhbiByZW5kZXIuIFN0cmlwIHRoZVxuICAgIC8vIHNlc3Npb24gY29va2llIHNvIGl0IG5ldmVyIGxlYWtzIHRvIHRoZSBzdGF0aWMgaGFuZGxlci5cbiAgICBpZiAoIXBhdGguc3RhcnRzV2l0aChcIi9hcGkvXCIpKSB7XG4gICAgICBzdHJpcFNlc3Npb25Db29raWUocmVxKTtcbiAgICAgIHByb3h5LndlYihyZXEsIHJlcyk7XG4gICAgICByZXR1cm47XG4gICAgfVxuXG4gICAgLy8gRXZlcnkgb3RoZXIgL2FwaS8qIHJlcXVpcmVzIGEgdmFsaWQgc2Vzc2lvbi5cbiAgICBjb25zdCB0b2tlbiA9IGh0dHAucGFyc2VDb29raWVzKHJlcS5oZWFkZXJzLmNvb2tpZSA/PyBudWxsKVtTRVNTSU9OX0NPT0tJRV9OQU1FXTtcbiAgICBjb25zdCBlbWFpbCA9IGF3YWl0IGdhdGUuc2Vzc2lvbih0b2tlbik7XG4gICAgaWYgKGVtYWlsKSB7XG4gICAgICAvLyBQcmVzZW50IHRoZSBPVFAgdXNlciBsaWtlIHRoZSBEYXRhYnJpY2tzIGZyb250IGRvb3IsIGFuZCBkcm9wIHRoZSBnYXRlXG4gICAgICAvLyBjb29raWUgc28gdGhlIGFwcCBzZWVzIGEgY2xlYW4sIGZyb250LWRvb3Itc2hhcGVkIHJlcXVlc3QuXG4gICAgICBpbmplY3RJZGVudGl0eShyZXEsIGVtYWlsKTtcbiAgICAgIHN0cmlwU2Vzc2lvbkNvb2tpZShyZXEpO1xuICAgICAgcHJveHkud2ViKHJlcSwgcmVzKTtcbiAgICB9IGVsc2Uge1xuICAgICAgc2VuZEpzb24ocmVzLCA0MDEsIHsgZXJyb3I6IFwiYXV0aGVudGljYXRpb24gcmVxdWlyZWRcIiwgbG9naW5QYXRoOiBBVVRIX1BSRUZJWCB9KTtcbiAgICB9XG4gIH0pO1xuXG4gIC8vIFdlYlNvY2tldCB1cGdyYWRlczogZm9yd2FyZCBmcm9udC1kb29yIHRyYWZmaWMgdW50b3VjaGVkOyBnYXRlIGxvb3BiYWNrXG4gIC8vIChwb3J0cikgQVBJIHVwZ3JhZGVzIHRoZSBzYW1lIHdheSBhcyBIVFRQIChzdHJpcCBjb29raWUgKyBpbmplY3QgaWRlbnRpdHkpLlxuICBzZXJ2ZXIub24oXCJ1cGdyYWRlXCIsIChyZXE6IEluY29taW5nTWVzc2FnZSwgc29ja2V0OiBTb2NrZXQsIGhlYWQ6IEJ1ZmZlcikgPT4ge1xuICAgIGlmICghZ2F0ZSB8fCAhaXNMb29wYmFjayhyZXEuc29ja2V0LnJlbW90ZUFkZHJlc3MpKSB7XG4gICAgICBwcm94eS53cyhyZXEsIHNvY2tldCwgaGVhZCk7XG4gICAgICByZXR1cm47XG4gICAgfVxuICAgIGRlbGV0ZSByZXEuaGVhZGVyc1tcIngtZm9yd2FyZGVkLXVzZXJcIl07XG4gICAgZGVsZXRlIHJlcS5oZWFkZXJzW1wieC1mb3J3YXJkZWQtZW1haWxcIl07XG4gICAgaWYgKCEocmVxLnVybCA/PyBcIlwiKS5zdGFydHNXaXRoKFwiL2FwaS9cIikpIHtcbiAgICAgIHN0cmlwU2Vzc2lvbkNvb2tpZShyZXEpO1xuICAgICAgcHJveHkud3MocmVxLCBzb2NrZXQsIGhlYWQpO1xuICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICBjb25zdCB0b2tlbiA9IGh0dHAucGFyc2VDb29raWVzKHJlcS5oZWFkZXJzLmNvb2tpZSA/PyBudWxsKVtTRVNTSU9OX0NPT0tJRV9OQU1FXTtcbiAgICB2b2lkIGdhdGUuc2Vzc2lvbih0b2tlbikudGhlbigoZW1haWwpID0+IHtcbiAgICAgIGlmICghZW1haWwpIHtcbiAgICAgICAgc29ja2V0LmRlc3Ryb3koKTtcbiAgICAgICAgcmV0dXJuO1xuICAgICAgfVxuICAgICAgaW5qZWN0SWRlbnRpdHkocmVxLCBlbWFpbCk7XG4gICAgICBzdHJpcFNlc3Npb25Db29raWUocmVxKTtcbiAgICAgIHByb3h5LndzKHJlcSwgc29ja2V0LCBoZWFkKTtcbiAgICB9KTtcbiAgfSk7XG5cbiAgcmV0dXJuIG5ldyBQcm9taXNlKChyZXNvbHZlKSA9PiB7XG4gICAgc2VydmVyLmxpc3RlbihwdWJsaWNQb3J0LCBcIjAuMC4wLjBcIiwgKCkgPT4ge1xuICAgICAgbG9nZ2VyLmluZm8oYGdhdGUgcHJveHkgb24gMC4wLjAuMDoke3B1YmxpY1BvcnR9IC0+IGFwcCAxMjcuMC4wLjE6JHthcHBQb3J0fWApO1xuICAgICAgcmVzb2x2ZSgpO1xuICAgIH0pO1xuICB9KTtcbn1cbiJdfQ==
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small in-memory fixed-window rate limiter for the email-OTP gate.
|
|
3
|
+
*
|
|
4
|
+
* Keyed by an arbitrary string (an email address or a client IP). Each key gets
|
|
5
|
+
* `max` hits per `windowMs`; the window resets on first use after it elapses.
|
|
6
|
+
* `hit()` returns whether the call is allowed and, when not, how many seconds
|
|
7
|
+
* until the window resets so a caller can surface a cooldown.
|
|
8
|
+
*
|
|
9
|
+
* In-memory is intentional and sufficient for a single-app-instance gate: an
|
|
10
|
+
* app behind a portr tunnel serves from one process. It is NOT a distributed
|
|
11
|
+
* limiter; a multi-replica deployment would need shared state. Entries are
|
|
12
|
+
* pruned lazily on access, so an idle key costs nothing after its window.
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
/** A fixed-window rate limiter over string keys. */
|
|
17
|
+
export declare class RateLimiter {
|
|
18
|
+
private readonly max;
|
|
19
|
+
private readonly windowMs;
|
|
20
|
+
private readonly windows;
|
|
21
|
+
constructor(max: number, windowMs: number);
|
|
22
|
+
/**
|
|
23
|
+
* Record a hit for `key`. Returns `{ allowed }`, plus `retryAfter` (seconds)
|
|
24
|
+
* when the limit is exceeded. A limit of `<= 0` disables limiting (always
|
|
25
|
+
* allowed), which lets a config turn it off without special-casing callers.
|
|
26
|
+
*/
|
|
27
|
+
hit(key: string, now?: number): {
|
|
28
|
+
allowed: boolean;
|
|
29
|
+
retryAfter?: number;
|
|
30
|
+
};
|
|
31
|
+
/** Forget a key (e.g. clear a caller's window after a successful verify). */
|
|
32
|
+
reset(key: string): void;
|
|
33
|
+
/** Drop every window (tests). */
|
|
34
|
+
clear(): void;
|
|
35
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small in-memory fixed-window rate limiter for the email-OTP gate.
|
|
3
|
+
*
|
|
4
|
+
* Keyed by an arbitrary string (an email address or a client IP). Each key gets
|
|
5
|
+
* `max` hits per `windowMs`; the window resets on first use after it elapses.
|
|
6
|
+
* `hit()` returns whether the call is allowed and, when not, how many seconds
|
|
7
|
+
* until the window resets so a caller can surface a cooldown.
|
|
8
|
+
*
|
|
9
|
+
* In-memory is intentional and sufficient for a single-app-instance gate: an
|
|
10
|
+
* app behind a portr tunnel serves from one process. It is NOT a distributed
|
|
11
|
+
* limiter; a multi-replica deployment would need shared state. Entries are
|
|
12
|
+
* pruned lazily on access, so an idle key costs nothing after its window.
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
/** A fixed-window rate limiter over string keys. */
|
|
17
|
+
export class RateLimiter {
|
|
18
|
+
max;
|
|
19
|
+
windowMs;
|
|
20
|
+
windows = new Map();
|
|
21
|
+
constructor(max, windowMs) {
|
|
22
|
+
this.max = max;
|
|
23
|
+
this.windowMs = windowMs;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Record a hit for `key`. Returns `{ allowed }`, plus `retryAfter` (seconds)
|
|
27
|
+
* when the limit is exceeded. A limit of `<= 0` disables limiting (always
|
|
28
|
+
* allowed), which lets a config turn it off without special-casing callers.
|
|
29
|
+
*/
|
|
30
|
+
hit(key, now = Date.now()) {
|
|
31
|
+
if (this.max <= 0)
|
|
32
|
+
return { allowed: true };
|
|
33
|
+
const existing = this.windows.get(key);
|
|
34
|
+
if (!existing || now >= existing.resetAt) {
|
|
35
|
+
this.windows.set(key, { count: 1, resetAt: now + this.windowMs });
|
|
36
|
+
return { allowed: true };
|
|
37
|
+
}
|
|
38
|
+
if (existing.count < this.max) {
|
|
39
|
+
existing.count += 1;
|
|
40
|
+
return { allowed: true };
|
|
41
|
+
}
|
|
42
|
+
return { allowed: false, retryAfter: Math.ceil((existing.resetAt - now) / 1000) };
|
|
43
|
+
}
|
|
44
|
+
/** Forget a key (e.g. clear a caller's window after a successful verify). */
|
|
45
|
+
reset(key) {
|
|
46
|
+
this.windows.delete(key);
|
|
47
|
+
}
|
|
48
|
+
/** Drop every window (tests). */
|
|
49
|
+
clear() {
|
|
50
|
+
this.windows.clear();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmF0ZS1saW1pdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9yYXRlLWxpbWl0LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7Ozs7Ozs7OztHQWNHO0FBT0gsb0RBQW9EO0FBQ3BELE1BQU0sT0FBTyxXQUFXO0lBSUg7SUFDQTtJQUpGLE9BQU8sR0FBRyxJQUFJLEdBQUcsRUFBa0IsQ0FBQztJQUVyRCxZQUNtQixHQUFXLEVBQ1gsUUFBZ0I7UUFEaEIsUUFBRyxHQUFILEdBQUcsQ0FBUTtRQUNYLGFBQVEsR0FBUixRQUFRLENBQVE7SUFDaEMsQ0FBQztJQUVKOzs7O09BSUc7SUFDSCxHQUFHLENBQUMsR0FBVyxFQUFFLE1BQWMsSUFBSSxDQUFDLEdBQUcsRUFBRTtRQUN2QyxJQUFJLElBQUksQ0FBQyxHQUFHLElBQUksQ0FBQztZQUFFLE9BQU8sRUFBRSxPQUFPLEVBQUUsSUFBSSxFQUFFLENBQUM7UUFDNUMsTUFBTSxRQUFRLEdBQUcsSUFBSSxDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLENBQUM7UUFDdkMsSUFBSSxDQUFDLFFBQVEsSUFBSSxHQUFHLElBQUksUUFBUSxDQUFDLE9BQU8sRUFBRSxDQUFDO1lBQ3pDLElBQUksQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLEdBQUcsRUFBRSxFQUFFLEtBQUssRUFBRSxDQUFDLEVBQUUsT0FBTyxFQUFFLEdBQUcsR0FBRyxJQUFJLENBQUMsUUFBUSxFQUFFLENBQUMsQ0FBQztZQUNsRSxPQUFPLEVBQUUsT0FBTyxFQUFFLElBQUksRUFBRSxDQUFDO1FBQzNCLENBQUM7UUFDRCxJQUFJLFFBQVEsQ0FBQyxLQUFLLEdBQUcsSUFBSSxDQUFDLEdBQUcsRUFBRSxDQUFDO1lBQzlCLFFBQVEsQ0FBQyxLQUFLLElBQUksQ0FBQyxDQUFDO1lBQ3BCLE9BQU8sRUFBRSxPQUFPLEVBQUUsSUFBSSxFQUFFLENBQUM7UUFDM0IsQ0FBQztRQUNELE9BQU8sRUFBRSxPQUFPLEVBQUUsS0FBSyxFQUFFLFVBQVUsRUFBRSxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUMsUUFBUSxDQUFDLE9BQU8sR0FBRyxHQUFHLENBQUMsR0FBRyxJQUFJLENBQUMsRUFBRSxDQUFDO0lBQ3BGLENBQUM7SUFFRCw2RUFBNkU7SUFDN0UsS0FBSyxDQUFDLEdBQVc7UUFDZixJQUFJLENBQUMsT0FBTyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQztJQUMzQixDQUFDO0lBRUQsaUNBQWlDO0lBQ2pDLEtBQUs7UUFDSCxJQUFJLENBQUMsT0FBTyxDQUFDLEtBQUssRUFBRSxDQUFDO0lBQ3ZCLENBQUM7Q0FDRiIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogQSBzbWFsbCBpbi1tZW1vcnkgZml4ZWQtd2luZG93IHJhdGUgbGltaXRlciBmb3IgdGhlIGVtYWlsLU9UUCBnYXRlLlxuICpcbiAqIEtleWVkIGJ5IGFuIGFyYml0cmFyeSBzdHJpbmcgKGFuIGVtYWlsIGFkZHJlc3Mgb3IgYSBjbGllbnQgSVApLiBFYWNoIGtleSBnZXRzXG4gKiBgbWF4YCBoaXRzIHBlciBgd2luZG93TXNgOyB0aGUgd2luZG93IHJlc2V0cyBvbiBmaXJzdCB1c2UgYWZ0ZXIgaXQgZWxhcHNlcy5cbiAqIGBoaXQoKWAgcmV0dXJucyB3aGV0aGVyIHRoZSBjYWxsIGlzIGFsbG93ZWQgYW5kLCB3aGVuIG5vdCwgaG93IG1hbnkgc2Vjb25kc1xuICogdW50aWwgdGhlIHdpbmRvdyByZXNldHMgc28gYSBjYWxsZXIgY2FuIHN1cmZhY2UgYSBjb29sZG93bi5cbiAqXG4gKiBJbi1tZW1vcnkgaXMgaW50ZW50aW9uYWwgYW5kIHN1ZmZpY2llbnQgZm9yIGEgc2luZ2xlLWFwcC1pbnN0YW5jZSBnYXRlOiBhblxuICogYXBwIGJlaGluZCBhIHBvcnRyIHR1bm5lbCBzZXJ2ZXMgZnJvbSBvbmUgcHJvY2Vzcy4gSXQgaXMgTk9UIGEgZGlzdHJpYnV0ZWRcbiAqIGxpbWl0ZXI7IGEgbXVsdGktcmVwbGljYSBkZXBsb3ltZW50IHdvdWxkIG5lZWQgc2hhcmVkIHN0YXRlLiBFbnRyaWVzIGFyZVxuICogcHJ1bmVkIGxhemlseSBvbiBhY2Nlc3MsIHNvIGFuIGlkbGUga2V5IGNvc3RzIG5vdGhpbmcgYWZ0ZXIgaXRzIHdpbmRvdy5cbiAqXG4gKiBAbW9kdWxlXG4gKi9cblxuaW50ZXJmYWNlIFdpbmRvdyB7XG4gIGNvdW50OiBudW1iZXI7XG4gIHJlc2V0QXQ6IG51bWJlcjtcbn1cblxuLyoqIEEgZml4ZWQtd2luZG93IHJhdGUgbGltaXRlciBvdmVyIHN0cmluZyBrZXlzLiAqL1xuZXhwb3J0IGNsYXNzIFJhdGVMaW1pdGVyIHtcbiAgcHJpdmF0ZSByZWFkb25seSB3aW5kb3dzID0gbmV3IE1hcDxzdHJpbmcsIFdpbmRvdz4oKTtcblxuICBjb25zdHJ1Y3RvcihcbiAgICBwcml2YXRlIHJlYWRvbmx5IG1heDogbnVtYmVyLFxuICAgIHByaXZhdGUgcmVhZG9ubHkgd2luZG93TXM6IG51bWJlcixcbiAgKSB7fVxuXG4gIC8qKlxuICAgKiBSZWNvcmQgYSBoaXQgZm9yIGBrZXlgLiBSZXR1cm5zIGB7IGFsbG93ZWQgfWAsIHBsdXMgYHJldHJ5QWZ0ZXJgIChzZWNvbmRzKVxuICAgKiB3aGVuIHRoZSBsaW1pdCBpcyBleGNlZWRlZC4gQSBsaW1pdCBvZiBgPD0gMGAgZGlzYWJsZXMgbGltaXRpbmcgKGFsd2F5c1xuICAgKiBhbGxvd2VkKSwgd2hpY2ggbGV0cyBhIGNvbmZpZyB0dXJuIGl0IG9mZiB3aXRob3V0IHNwZWNpYWwtY2FzaW5nIGNhbGxlcnMuXG4gICAqL1xuICBoaXQoa2V5OiBzdHJpbmcsIG5vdzogbnVtYmVyID0gRGF0ZS5ub3coKSk6IHsgYWxsb3dlZDogYm9vbGVhbjsgcmV0cnlBZnRlcj86IG51bWJlciB9IHtcbiAgICBpZiAodGhpcy5tYXggPD0gMCkgcmV0dXJuIHsgYWxsb3dlZDogdHJ1ZSB9O1xuICAgIGNvbnN0IGV4aXN0aW5nID0gdGhpcy53aW5kb3dzLmdldChrZXkpO1xuICAgIGlmICghZXhpc3RpbmcgfHwgbm93ID49IGV4aXN0aW5nLnJlc2V0QXQpIHtcbiAgICAgIHRoaXMud2luZG93cy5zZXQoa2V5LCB7IGNvdW50OiAxLCByZXNldEF0OiBub3cgKyB0aGlzLndpbmRvd01zIH0pO1xuICAgICAgcmV0dXJuIHsgYWxsb3dlZDogdHJ1ZSB9O1xuICAgIH1cbiAgICBpZiAoZXhpc3RpbmcuY291bnQgPCB0aGlzLm1heCkge1xuICAgICAgZXhpc3RpbmcuY291bnQgKz0gMTtcbiAgICAgIHJldHVybiB7IGFsbG93ZWQ6IHRydWUgfTtcbiAgICB9XG4gICAgcmV0dXJuIHsgYWxsb3dlZDogZmFsc2UsIHJldHJ5QWZ0ZXI6IE1hdGguY2VpbCgoZXhpc3RpbmcucmVzZXRBdCAtIG5vdykgLyAxMDAwKSB9O1xuICB9XG5cbiAgLyoqIEZvcmdldCBhIGtleSAoZS5nLiBjbGVhciBhIGNhbGxlcidzIHdpbmRvdyBhZnRlciBhIHN1Y2Nlc3NmdWwgdmVyaWZ5KS4gKi9cbiAgcmVzZXQoa2V5OiBzdHJpbmcpOiB2b2lkIHtcbiAgICB0aGlzLndpbmRvd3MuZGVsZXRlKGtleSk7XG4gIH1cblxuICAvKiogRHJvcCBldmVyeSB3aW5kb3cgKHRlc3RzKS4gKi9cbiAgY2xlYXIoKTogdm9pZCB7XG4gICAgdGhpcy53aW5kb3dzLmNsZWFyKCk7XG4gIH1cbn1cbiJdfQ==
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["../src/allowlist.ts","../src/app.ts","../src/cli.ts","../src/otp.ts","../src/plugin.ts","../src/portr.ts","../src/proxy.ts","../src/rate-limit.ts","../index.ts","../bin/dbx-tools-tunnel.ts"],"version":"5.9.3"}
|
package/package.json
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dbx-tools/cli-tunnel",
|
|
3
|
+
"repository": {
|
|
4
|
+
"type": "git",
|
|
5
|
+
"url": "git+https://github.com/reggie-db/dbx-tools.git",
|
|
6
|
+
"directory": "packages/cli/tunnel"
|
|
7
|
+
},
|
|
8
|
+
"bin": {
|
|
9
|
+
"dbx-tools-tunnel": "./bin/dbx-tools-tunnel.ts",
|
|
10
|
+
"dbxt-tunnel": "./bin/dbx-tools-tunnel.ts"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "projen build",
|
|
14
|
+
"compile": "projen compile",
|
|
15
|
+
"default": "projen default",
|
|
16
|
+
"package": "projen package",
|
|
17
|
+
"post-compile": "projen post-compile",
|
|
18
|
+
"pre-compile": "projen pre-compile",
|
|
19
|
+
"prepack": "projen prepack",
|
|
20
|
+
"test": "projen test",
|
|
21
|
+
"watch": "projen watch",
|
|
22
|
+
"projen": "projen"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/bun": "^1.3.14",
|
|
26
|
+
"@types/node": "^24.6.0",
|
|
27
|
+
"typescript": "^5.9.3"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@clack/prompts": "^1.7.0",
|
|
31
|
+
"@databricks/appkit": "^0.43.0",
|
|
32
|
+
"@dbx-tools/appkit": "0.6.45",
|
|
33
|
+
"@dbx-tools/email": "0.6.45",
|
|
34
|
+
"@dbx-tools/shared-core": "0.6.45",
|
|
35
|
+
"@dbx-tools/shared-email": "0.6.45",
|
|
36
|
+
"commander": "^15.0.0",
|
|
37
|
+
"http-proxy-3": "^1.23.3",
|
|
38
|
+
"jose": "^6.2.3"
|
|
39
|
+
},
|
|
40
|
+
"main": "index.ts",
|
|
41
|
+
"license": "UNLICENSED",
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public",
|
|
44
|
+
"main": "./lib/index.js",
|
|
45
|
+
"types": "./lib/index.d.ts",
|
|
46
|
+
"bin": {
|
|
47
|
+
"dbx-tools-tunnel": "./lib/bin/dbx-tools-tunnel.js",
|
|
48
|
+
"dbxt-tunnel": "./lib/bin/dbx-tools-tunnel.js"
|
|
49
|
+
},
|
|
50
|
+
"exports": {
|
|
51
|
+
".": {
|
|
52
|
+
"types": "./lib/index.d.ts",
|
|
53
|
+
"default": "./lib/index.js"
|
|
54
|
+
},
|
|
55
|
+
"./allowlist": {
|
|
56
|
+
"types": "./lib/src/allowlist.d.ts",
|
|
57
|
+
"default": "./lib/src/allowlist.js"
|
|
58
|
+
},
|
|
59
|
+
"./app": {
|
|
60
|
+
"types": "./lib/src/app.d.ts",
|
|
61
|
+
"default": "./lib/src/app.js"
|
|
62
|
+
},
|
|
63
|
+
"./cli": {
|
|
64
|
+
"types": "./lib/src/cli.d.ts",
|
|
65
|
+
"default": "./lib/src/cli.js"
|
|
66
|
+
},
|
|
67
|
+
"./otp": {
|
|
68
|
+
"types": "./lib/src/otp.d.ts",
|
|
69
|
+
"default": "./lib/src/otp.js"
|
|
70
|
+
},
|
|
71
|
+
"./plugin": {
|
|
72
|
+
"types": "./lib/src/plugin.d.ts",
|
|
73
|
+
"default": "./lib/src/plugin.js"
|
|
74
|
+
},
|
|
75
|
+
"./portr": {
|
|
76
|
+
"types": "./lib/src/portr.d.ts",
|
|
77
|
+
"default": "./lib/src/portr.js"
|
|
78
|
+
},
|
|
79
|
+
"./proxy": {
|
|
80
|
+
"types": "./lib/src/proxy.d.ts",
|
|
81
|
+
"default": "./lib/src/proxy.js"
|
|
82
|
+
},
|
|
83
|
+
"./rate-limit": {
|
|
84
|
+
"types": "./lib/src/rate-limit.d.ts",
|
|
85
|
+
"default": "./lib/src/rate-limit.js"
|
|
86
|
+
},
|
|
87
|
+
"./package.json": "./package.json"
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"version": "0.6.45",
|
|
91
|
+
"types": "index.ts",
|
|
92
|
+
"type": "module",
|
|
93
|
+
"exports": {
|
|
94
|
+
".": "./index.ts",
|
|
95
|
+
"./allowlist": "./src/allowlist.ts",
|
|
96
|
+
"./app": "./src/app.ts",
|
|
97
|
+
"./cli": "./src/cli.ts",
|
|
98
|
+
"./otp": "./src/otp.ts",
|
|
99
|
+
"./plugin": "./src/plugin.ts",
|
|
100
|
+
"./portr": "./src/portr.ts",
|
|
101
|
+
"./proxy": "./src/proxy.ts",
|
|
102
|
+
"./rate-limit": "./src/rate-limit.ts",
|
|
103
|
+
"./package.json": "./package.json"
|
|
104
|
+
},
|
|
105
|
+
"files": [
|
|
106
|
+
"index.ts",
|
|
107
|
+
"src",
|
|
108
|
+
"bin",
|
|
109
|
+
"lib"
|
|
110
|
+
],
|
|
111
|
+
"dbxToolsConfig": {
|
|
112
|
+
"tags": [
|
|
113
|
+
"cli"
|
|
114
|
+
]
|
|
115
|
+
},
|
|
116
|
+
"//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"bunx projen\"."
|
|
117
|
+
}
|
package/src/allowlist.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
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, tried in order:
|
|
5
|
+
*
|
|
6
|
+
* - **domain shortcut** - `databricks.com` or `@databricks.com`: matches any
|
|
7
|
+
* address whose domain equals it (case-insensitive). The leading `@` is
|
|
8
|
+
* optional and stripped.
|
|
9
|
+
* - **glob** - contains `*` or `?`, e.g. `*.databricks.com` or
|
|
10
|
+
* `*@databricks.com`: matched against the WHOLE address with shell-style
|
|
11
|
+
* wildcards (`*` = any run, `?` = one char).
|
|
12
|
+
* - **regex** - wrapped in slashes, `/.../ [flags]`: compiled and tested
|
|
13
|
+
* against the whole address. An invalid regex never matches (it is skipped
|
|
14
|
+
* with a warning rather than throwing).
|
|
15
|
+
*
|
|
16
|
+
* An EMPTY list matches nobody (fail closed): an app that enables the gate but
|
|
17
|
+
* configures no patterns lets no one in, which is the safe default.
|
|
18
|
+
*
|
|
19
|
+
* @module
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { log } from "@dbx-tools/shared-core";
|
|
23
|
+
|
|
24
|
+
const logger = log.logger("tunnel:allowlist");
|
|
25
|
+
|
|
26
|
+
/** Escape a string for literal use inside a `RegExp`. */
|
|
27
|
+
function escapeRegExp(value: string): string {
|
|
28
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Compile a shell-style glob (`*`, `?`) into an anchored, case-insensitive RegExp. */
|
|
32
|
+
function globToRegExp(glob: string): RegExp {
|
|
33
|
+
const body = glob
|
|
34
|
+
.split(/([*?])/)
|
|
35
|
+
.map((part) => (part === "*" ? ".*" : part === "?" ? "." : escapeRegExp(part)))
|
|
36
|
+
.join("");
|
|
37
|
+
return new RegExp(`^${body}$`, "i");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Parse a `/pattern/flags` string into a RegExp, or `undefined` if malformed. */
|
|
41
|
+
function parseRegexLiteral(pattern: string): RegExp | undefined {
|
|
42
|
+
const match = /^\/(.+)\/([a-z]*)$/is.exec(pattern);
|
|
43
|
+
if (!match) return undefined;
|
|
44
|
+
try {
|
|
45
|
+
const flags = match[2]!.includes("i") ? match[2]! : `${match[2]!}i`;
|
|
46
|
+
return new RegExp(match[1]!, flags);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
logger.warn("ignoring invalid regex allow-list pattern", { pattern, error });
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** True when `email` matches a single allow-list `pattern`. */
|
|
54
|
+
function matchesPattern(email: string, pattern: string): boolean {
|
|
55
|
+
const trimmed = pattern.trim();
|
|
56
|
+
if (!trimmed) return false;
|
|
57
|
+
const address = email.trim().toLowerCase();
|
|
58
|
+
|
|
59
|
+
// Regex literal: /.../
|
|
60
|
+
if (trimmed.startsWith("/")) {
|
|
61
|
+
const re = parseRegexLiteral(trimmed);
|
|
62
|
+
return re ? re.test(address) : false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Glob: contains a wildcard.
|
|
66
|
+
if (trimmed.includes("*") || trimmed.includes("?")) {
|
|
67
|
+
return globToRegExp(trimmed).test(address);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Domain shortcut: `@d.com` or `d.com` -> match the address's domain.
|
|
71
|
+
const domain = trimmed.replace(/^@/, "").toLowerCase();
|
|
72
|
+
const at = address.lastIndexOf("@");
|
|
73
|
+
return at >= 0 && address.slice(at + 1) === domain;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* True when `email` is allowed by ANY pattern in `patterns`. An empty (or
|
|
78
|
+
* missing) list allows nobody - the gate fails closed.
|
|
79
|
+
*/
|
|
80
|
+
export function matchesAllowlist(email: string, patterns: readonly string[] | undefined): boolean {
|
|
81
|
+
if (!email || !patterns || patterns.length === 0) return false;
|
|
82
|
+
return patterns.some((pattern) => matchesPattern(email, pattern));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Rough shape check so a clearly-invalid address is rejected before any work. */
|
|
86
|
+
export function looksLikeEmail(value: string): boolean {
|
|
87
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim());
|
|
88
|
+
}
|
package/src/app.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tunnel gate's tiny AppKit "app" - `createApp` WITHOUT a `server()` plugin.
|
|
3
|
+
*
|
|
4
|
+
* There is no HTTP server here: the tunnel proxy is the server, and it calls the
|
|
5
|
+
* gate handlers in-process. `createApp` is used only for what it auto-wires:
|
|
6
|
+
* - `CacheManager` (memory, or Lakebase when the deploy configures persistent
|
|
7
|
+
* cache storage) - which the OTP `CodeStore` uses for storage + TTL eviction;
|
|
8
|
+
* - the `email()` transport (SMTP / outbox), primed from env, so `sendCode` can
|
|
9
|
+
* deliver the one-time code;
|
|
10
|
+
* - dbx-tools auto-configuration + branding via `@dbx-tools/appkit`.
|
|
11
|
+
*
|
|
12
|
+
* It returns the {@link AuthGateApi} the proxy drives. `sendCode` is wired here
|
|
13
|
+
* (the plugin can't resolve HOW to send on its own) to the email plugin's
|
|
14
|
+
* transport, resolving the `From` from the email runtime config (EMAIL_FROM /
|
|
15
|
+
* EMAIL_DOMAIN) since an OTP request has no on-behalf-of user.
|
|
16
|
+
*
|
|
17
|
+
* FAIL FAST: a gate that can't email a code is useless, so if email does not
|
|
18
|
+
* resolve to SMTP mode (real delivery), this throws - unless `insecure` is set
|
|
19
|
+
* (`--insecure` / `TUNNEL_INSECURE=true`), in which case the caller runs the
|
|
20
|
+
* tunnel OPEN with no gate.
|
|
21
|
+
*
|
|
22
|
+
* @module
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { createApp as createAppNs } from "@dbx-tools/appkit";
|
|
26
|
+
import { email, sender, transport } from "@dbx-tools/email";
|
|
27
|
+
import { log } from "@dbx-tools/shared-core";
|
|
28
|
+
import { authGate, type AuthGateApi, type AuthGateConfig } from "./plugin.ts";
|
|
29
|
+
|
|
30
|
+
const logger = log.logger("tunnel:app");
|
|
31
|
+
|
|
32
|
+
const { createApp } = createAppNs;
|
|
33
|
+
const { resolveSenderAddress } = sender;
|
|
34
|
+
const { getEmailRuntime, sendEmail } = transport;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Boot the gate app and return the API the proxy calls. Throws when email is not
|
|
38
|
+
* in SMTP mode (no way to deliver a code) so a misconfigured gate fails fast at
|
|
39
|
+
* startup rather than silently accepting nobody; the caller may catch this and
|
|
40
|
+
* fall back to insecure/open mode when the operator opted in.
|
|
41
|
+
*/
|
|
42
|
+
export async function startGateApp(config: AuthGateConfig): Promise<AuthGateApi> {
|
|
43
|
+
// `sendCode` delivers the OTP through the email plugin's SHARED transport, which
|
|
44
|
+
// the `email()` plugin primes during its `setup()` (awaited by `createApp`
|
|
45
|
+
// below). Using the module-level `sendEmail` avoids a circular dependency on the
|
|
46
|
+
// app handle's inferred type. `From` is the app's configured sender (EMAIL_FROM /
|
|
47
|
+
// EMAIL_DOMAIN); the code email has no OBO user, so it resolves with `undefined`.
|
|
48
|
+
const sendCode: NonNullable<AuthGateConfig["sendCode"]> = async (to, code, opts) => {
|
|
49
|
+
const from = resolveSenderAddress(getEmailRuntime().config, undefined);
|
|
50
|
+
await sendEmail(
|
|
51
|
+
{
|
|
52
|
+
to: [to],
|
|
53
|
+
subject: opts.subject,
|
|
54
|
+
body: [
|
|
55
|
+
opts.message,
|
|
56
|
+
"",
|
|
57
|
+
`## ${code}`,
|
|
58
|
+
"",
|
|
59
|
+
"It expires shortly. If you didn't request this, ignore this email.",
|
|
60
|
+
].join("\n"),
|
|
61
|
+
},
|
|
62
|
+
from,
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// `createApp` namespaces each plugin's exports on the handle by manifest name;
|
|
67
|
+
// `handle.authGate` is the in-process gate API the proxy drives.
|
|
68
|
+
const handle = await createApp({ plugins: [email(), authGate({ ...config, sendCode })] });
|
|
69
|
+
|
|
70
|
+
// Fail fast: the gate needs SMTP to email codes. `getEmailRuntime()` resolves to
|
|
71
|
+
// `mode: "file"` (outbox) or throws when no SMTP creds are configured - neither
|
|
72
|
+
// can deliver to a real inbox, so refuse to bring up a gate that lets nobody in.
|
|
73
|
+
if (getEmailRuntime().config.mode !== "smtp") {
|
|
74
|
+
throw new Error(
|
|
75
|
+
"email is not configured for SMTP delivery - the OTP gate cannot send codes. " +
|
|
76
|
+
"Set SMTP_HOST/SMTP_USER/SMTP_PASSWORD, or pass --insecure / TUNNEL_INSECURE=true to run the tunnel open.",
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
logger.info("gate app ready (no server; proxy-driven)");
|
|
81
|
+
return handle.authGate as AuthGateApi;
|
|
82
|
+
}
|