@dbx-tools/tunnel 0.6.60
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 +373 -0
- package/index.ts +28 -0
- package/lib/index.d.ts +24 -0
- package/lib/index.js +21 -0
- package/lib/src/allowlist.d.ts +32 -0
- package/lib/src/allowlist.js +57 -0
- package/lib/src/app.d.ts +130 -0
- package/lib/src/app.js +258 -0
- package/lib/src/env.d.ts +57 -0
- package/lib/src/env.js +60 -0
- package/lib/src/headers.d.ts +108 -0
- package/lib/src/headers.js +140 -0
- package/lib/src/interceptor.d.ts +56 -0
- package/lib/src/interceptor.js +84 -0
- package/lib/src/otp.d.ts +49 -0
- package/lib/src/otp.js +124 -0
- package/lib/src/plugin.d.ts +147 -0
- package/lib/src/plugin.js +138 -0
- package/lib/src/portr.d.ts +40 -0
- package/lib/src/portr.js +93 -0
- package/lib/src/proxy.d.ts +49 -0
- package/lib/src/proxy.js +247 -0
- package/lib/src/rate-limit.d.ts +35 -0
- package/lib/src/rate-limit.js +53 -0
- package/lib/src/signing-key.d.ts +86 -0
- package/lib/src/signing-key.js +170 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/package.json +70 -0
- package/src/allowlist.ts +60 -0
- package/src/app.ts +292 -0
- package/src/env.ts +72 -0
- package/src/headers.ts +155 -0
- package/src/interceptor.ts +105 -0
- package/src/otp.ts +137 -0
- package/src/plugin.ts +269 -0
- package/src/portr.ts +113 -0
- package/src/proxy.ts +299 -0
- package/src/rate-limit.ts +59 -0
- package/src/signing-key.ts +201 -0
package/lib/src/proxy.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
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
|
+
* - **the hosting platform's front door** (Databricks Apps' control plane, or
|
|
11
|
+
* any other host reaching the `0.0.0.0` port from a NON-loopback
|
|
12
|
+
* container-network address) - passed through UNGATED, per the rule "if it's
|
|
13
|
+
* not from the portr client, let it in". The platform already authenticates
|
|
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`.
|
|
24
|
+
*
|
|
25
|
+
* @module
|
|
26
|
+
*/
|
|
27
|
+
import { createServer } from "node:http";
|
|
28
|
+
import { http, json, log, token } from "@dbx-tools/shared-core";
|
|
29
|
+
import { authRequestSchema, authVerifySchema, SESSION_COOKIE_NAME } from "@dbx-tools/shared-email";
|
|
30
|
+
import ProxyModule from "http-proxy-3";
|
|
31
|
+
import { toHeaderPolicy } from "./headers.js";
|
|
32
|
+
const logger = log.logger("tunnel:proxy");
|
|
33
|
+
/** Route prefix the login flow lives under (open, answered in-process). */
|
|
34
|
+
const AUTH_PREFIX = "/api/email/auth";
|
|
35
|
+
/** True when a socket address is loopback (the portr client, same container). */
|
|
36
|
+
function isLoopback(addr) {
|
|
37
|
+
if (!addr)
|
|
38
|
+
return false;
|
|
39
|
+
// Normalize IPv4-mapped IPv6 (`::ffff:127.0.0.1`) and bare IPv6 loopback.
|
|
40
|
+
const a = addr.replace(/^::ffff:/, "");
|
|
41
|
+
return a === "127.0.0.1" || a.startsWith("127.") || a === "::1" || a === "localhost";
|
|
42
|
+
}
|
|
43
|
+
/** Read the whole request body as text (small JSON payloads only). */
|
|
44
|
+
function readBody(req) {
|
|
45
|
+
return new Promise((resolve) => {
|
|
46
|
+
let data = "";
|
|
47
|
+
req.on("data", (chunk) => (data += chunk));
|
|
48
|
+
req.on("end", () => resolve(data));
|
|
49
|
+
req.on("error", () => resolve(data));
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function sendJson(res, status, body, setCookie) {
|
|
53
|
+
const headers = { "content-type": "application/json" };
|
|
54
|
+
if (setCookie)
|
|
55
|
+
headers["set-cookie"] = setCookie;
|
|
56
|
+
res.writeHead(status, headers);
|
|
57
|
+
res.end(JSON.stringify(body));
|
|
58
|
+
}
|
|
59
|
+
/** Start the gate proxy. Resolves once it is listening. */
|
|
60
|
+
export function startProxy({ publicPort, appPort, gate, forwardHeaders, }) {
|
|
61
|
+
const proxy = ProxyModule.createProxyServer({
|
|
62
|
+
target: { host: "127.0.0.1", port: appPort },
|
|
63
|
+
ws: true,
|
|
64
|
+
xfwd: true,
|
|
65
|
+
});
|
|
66
|
+
proxy.on("error", (err, _req, res) => {
|
|
67
|
+
logger.warn("upstream proxy error", { error: err.message });
|
|
68
|
+
const r = res;
|
|
69
|
+
if (r && "writeHead" in r && !r.headersSent) {
|
|
70
|
+
sendJson(r, 502, { error: "upstream unavailable" });
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
// Compiled once: the inbound-header allow-list applied to every gated request.
|
|
74
|
+
const headerPolicy = toHeaderPolicy(forwardHeaders);
|
|
75
|
+
logger.debug("inbound header policy", { forward: headerPolicy.patterns });
|
|
76
|
+
/** The session cookie for a verified email, as a Set-Cookie string. */
|
|
77
|
+
const sessionCookie = (token, maxAgeSeconds) => [
|
|
78
|
+
`${SESSION_COOKIE_NAME}=${token}`,
|
|
79
|
+
"Path=/",
|
|
80
|
+
"HttpOnly",
|
|
81
|
+
"SameSite=Lax",
|
|
82
|
+
`Max-Age=${maxAgeSeconds}`,
|
|
83
|
+
process.env.NODE_ENV === "production" ? "Secure" : "",
|
|
84
|
+
]
|
|
85
|
+
.filter(Boolean)
|
|
86
|
+
.join("; ");
|
|
87
|
+
/**
|
|
88
|
+
* Client IP for rate-limiting: the portr client's forwarded XFF, else the socket.
|
|
89
|
+
*
|
|
90
|
+
* Deliberately the RIGHTMOST `x-forwarded-for` entry, not the leftmost. The
|
|
91
|
+
* list grows left-to-right as each hop appends, so the last entry is the one
|
|
92
|
+
* the nearest trusted proxy (portr) wrote and every earlier entry is a value
|
|
93
|
+
* the caller could have sent. Reading the leftmost lets a client vary one
|
|
94
|
+
* header to get a fresh rate-limit bucket per request, which defeats both the
|
|
95
|
+
* per-IP code-request and verify-attempt limiters. Called BEFORE
|
|
96
|
+
* {@link HeaderPolicy.apply} strips the header, so the honest hop value is
|
|
97
|
+
* still available here.
|
|
98
|
+
*/
|
|
99
|
+
const clientIp = (req) => {
|
|
100
|
+
const forwarded = req.headers["x-forwarded-for"];
|
|
101
|
+
const chain = (Array.isArray(forwarded) ? forwarded.join(",") : (forwarded ?? ""))
|
|
102
|
+
.split(",")
|
|
103
|
+
.map((entry) => entry.trim())
|
|
104
|
+
.filter(Boolean);
|
|
105
|
+
return chain.at(-1) ?? req.socket.remoteAddress ?? "unknown";
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Remove the gate's session cookie from the `Cookie` header before forwarding,
|
|
109
|
+
* so the app never sees `dbx_auth` (it is the proxy's concern, not the app's).
|
|
110
|
+
* Preserves any other cookies. Removes the header entirely when it becomes empty.
|
|
111
|
+
*/
|
|
112
|
+
const stripSessionCookie = (req) => {
|
|
113
|
+
const raw = req.headers.cookie;
|
|
114
|
+
if (!raw)
|
|
115
|
+
return;
|
|
116
|
+
const kept = raw
|
|
117
|
+
.split(";")
|
|
118
|
+
.map((c) => c.trim())
|
|
119
|
+
.filter((c) => c && !c.startsWith(`${SESSION_COOKIE_NAME}=`));
|
|
120
|
+
if (kept.length)
|
|
121
|
+
req.headers.cookie = kept.join("; ");
|
|
122
|
+
else
|
|
123
|
+
delete req.headers.cookie;
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* Present an OTP-authenticated caller to the app the SAME way a platform front
|
|
127
|
+
* door does: set the front door's own identity headers to the verified address
|
|
128
|
+
* (AppKit reads {@link token.USER_ID_HEADER} for the OBO user id), so the app
|
|
129
|
+
* needs no gate-specific code path. The names come from
|
|
130
|
+
* `@dbx-tools/shared-core`'s `token` module - the same constants AppKit-side
|
|
131
|
+
* code reads them by - rather than being re-spelled here.
|
|
132
|
+
*
|
|
133
|
+
* What the gate CANNOT set is {@link token.ACCESS_TOKEN_HEADER}: an OTP session
|
|
134
|
+
* proves an email address, not possession of a Databricks credential, and
|
|
135
|
+
* there is no way to mint one for the caller. Its absence is exactly what
|
|
136
|
+
* `@dbx-tools/appkit`'s `identity` `"auto"` mode detects, so a gated request
|
|
137
|
+
* runs as the app's service principal instead of throwing.
|
|
138
|
+
*/
|
|
139
|
+
const injectIdentity = (req, email) => {
|
|
140
|
+
req.headers[token.USER_ID_HEADER] = email;
|
|
141
|
+
req.headers[token.USER_EMAIL_HEADER] = email;
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Apply the inbound-header policy to a tunnel request: every `x-` header the
|
|
145
|
+
* allow-list does not name is deleted, and the platform identity/transport set
|
|
146
|
+
* is deleted regardless. See `./headers.ts` for the reasoning; the
|
|
147
|
+
* security-critical case is {@link token.ACCESS_TOKEN_HEADER}, which the gate
|
|
148
|
+
* never sets but an app running `identity: "auto"` treats as proof the request
|
|
149
|
+
* can do OBO.
|
|
150
|
+
*
|
|
151
|
+
* `xfwd: true` on the proxy re-adds `x-forwarded-for`/`-proto`/`-port`/`-host`
|
|
152
|
+
* afterwards from the real socket, so the app still sees those - it just sees
|
|
153
|
+
* the honest values rather than the caller's claim.
|
|
154
|
+
*/
|
|
155
|
+
const applyHeaderPolicy = (req) => {
|
|
156
|
+
const removed = headerPolicy.apply(req.headers);
|
|
157
|
+
if (removed.length)
|
|
158
|
+
logger.debug("stripped inbound headers", { removed });
|
|
159
|
+
};
|
|
160
|
+
const server = createServer(async (req, res) => {
|
|
161
|
+
const path = (req.url ?? "/").split("?")[0];
|
|
162
|
+
// Insecure/open mode (no gate) OR non-loopback traffic (the hosting
|
|
163
|
+
// platform's front door / control plane): forward ungated.
|
|
164
|
+
if (!gate || !isLoopback(req.socket.remoteAddress)) {
|
|
165
|
+
proxy.web(req, res);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
// --- portr traffic: the gate applies ---
|
|
169
|
+
// The login flow is answered IN-PROCESS (the app has no server to forward to).
|
|
170
|
+
if (path === `${AUTH_PREFIX}/status`) {
|
|
171
|
+
const token = http.parseCookies(req.headers.cookie ?? null)[SESSION_COOKIE_NAME];
|
|
172
|
+
sendJson(res, 200, await gate.status(token));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (path === `${AUTH_PREFIX}/request` && req.method === "POST") {
|
|
176
|
+
const parsed = authRequestSchema.safeParse(json.parseRecord(await readBody(req)));
|
|
177
|
+
if (!parsed.success)
|
|
178
|
+
return sendJson(res, 200, { ok: true }); // anti-enumeration
|
|
179
|
+
return sendJson(res, 200, await gate.request(parsed.data.email, clientIp(req)));
|
|
180
|
+
}
|
|
181
|
+
if (path === `${AUTH_PREFIX}/verify` && req.method === "POST") {
|
|
182
|
+
const parsed = authVerifySchema.safeParse(json.parseRecord(await readBody(req)));
|
|
183
|
+
if (!parsed.success)
|
|
184
|
+
return sendJson(res, 200, { ok: false });
|
|
185
|
+
const result = await gate.verify(parsed.data.email, parsed.data.code, clientIp(req));
|
|
186
|
+
const cookie = result.ok && result.token ? sessionCookie(result.token, gate.sessionTtlSeconds) : undefined;
|
|
187
|
+
return sendJson(res, 200, { ok: result.ok, ...(result.retryAfter ? { retryAfter: result.retryAfter } : {}) }, cookie);
|
|
188
|
+
}
|
|
189
|
+
if (path === `${AUTH_PREFIX}/logout` && req.method === "POST") {
|
|
190
|
+
return sendJson(res, 200, { ok: true }, `${SESSION_COOKIE_NAME}=; Path=/; HttpOnly; Max-Age=0`);
|
|
191
|
+
}
|
|
192
|
+
// Anti-spoof: apply the inbound-header allow-list to portr traffic - only the
|
|
193
|
+
// gate may assert identity, and it does so below for a verified session.
|
|
194
|
+
applyHeaderPolicy(req);
|
|
195
|
+
// Static (non-API) loads freely so the SPA + <AuthGate> can render. Strip the
|
|
196
|
+
// session cookie so it never leaks to the static handler.
|
|
197
|
+
if (!path.startsWith("/api/")) {
|
|
198
|
+
stripSessionCookie(req);
|
|
199
|
+
proxy.web(req, res);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
// Every other /api/* requires a valid session.
|
|
203
|
+
const token = http.parseCookies(req.headers.cookie ?? null)[SESSION_COOKIE_NAME];
|
|
204
|
+
const email = await gate.session(token);
|
|
205
|
+
if (email) {
|
|
206
|
+
// Present the OTP user like a platform front door, and drop the gate
|
|
207
|
+
// cookie so the app sees a clean, front-door-shaped request.
|
|
208
|
+
injectIdentity(req, email);
|
|
209
|
+
stripSessionCookie(req);
|
|
210
|
+
proxy.web(req, res);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
sendJson(res, 401, { error: "authentication required", loginPath: AUTH_PREFIX });
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
// WebSocket upgrades: forward front-door traffic untouched; gate loopback
|
|
217
|
+
// (portr) API upgrades the same way as HTTP (strip cookie + inject identity).
|
|
218
|
+
server.on("upgrade", (req, socket, head) => {
|
|
219
|
+
if (!gate || !isLoopback(req.socket.remoteAddress)) {
|
|
220
|
+
proxy.ws(req, socket, head);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
applyHeaderPolicy(req);
|
|
224
|
+
if (!(req.url ?? "").startsWith("/api/")) {
|
|
225
|
+
stripSessionCookie(req);
|
|
226
|
+
proxy.ws(req, socket, head);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const token = http.parseCookies(req.headers.cookie ?? null)[SESSION_COOKIE_NAME];
|
|
230
|
+
void gate.session(token).then((email) => {
|
|
231
|
+
if (!email) {
|
|
232
|
+
socket.destroy();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
injectIdentity(req, email);
|
|
236
|
+
stripSessionCookie(req);
|
|
237
|
+
proxy.ws(req, socket, head);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
return new Promise((resolve) => {
|
|
241
|
+
server.listen(publicPort, "0.0.0.0", () => {
|
|
242
|
+
logger.info(`gate proxy on 0.0.0.0:${publicPort} -> app 127.0.0.1:${appPort}`);
|
|
243
|
+
resolve();
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHJveHkuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvcHJveHkudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7R0F5Qkc7QUFFSCxPQUFPLEVBQUUsWUFBWSxFQUE2QyxNQUFNLFdBQVcsQ0FBQztBQUVwRixPQUFPLEVBQUUsSUFBSSxFQUFFLElBQUksRUFBRSxHQUFHLEVBQUUsS0FBSyxFQUFFLE1BQU0sd0JBQXdCLENBQUM7QUFDaEUsT0FBTyxFQUFFLGlCQUFpQixFQUFFLGdCQUFnQixFQUFFLG1CQUFtQixFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFDbkcsT0FBTyxXQUFXLE1BQU0sY0FBYyxDQUFDO0FBQ3ZDLE9BQU8sRUFBRSxjQUFjLEVBQXFCLE1BQU0sY0FBYyxDQUFDO0FBR2pFLE1BQU0sTUFBTSxHQUFHLEdBQUcsQ0FBQyxNQUFNLENBQUMsY0FBYyxDQUFDLENBQUM7QUFFMUMsMkVBQTJFO0FBQzNFLE1BQU0sV0FBVyxHQUFHLGlCQUFpQixDQUFDO0FBRXRDLGlGQUFpRjtBQUNqRixTQUFTLFVBQVUsQ0FBQyxJQUF3QjtJQUMxQyxJQUFJLENBQUMsSUFBSTtRQUFFLE9BQU8sS0FBSyxDQUFDO0lBQ3hCLDBFQUEwRTtJQUMxRSxNQUFNLENBQUMsR0FBRyxJQUFJLENBQUMsT0FBTyxDQUFDLFVBQVUsRUFBRSxFQUFFLENBQUMsQ0FBQztJQUN2QyxPQUFPLENBQUMsS0FBSyxXQUFXLElBQUksQ0FBQyxDQUFDLFVBQVUsQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLEtBQUssS0FBSyxJQUFJLENBQUMsS0FBSyxXQUFXLENBQUM7QUFDdkYsQ0FBQztBQUVELHNFQUFzRTtBQUN0RSxTQUFTLFFBQVEsQ0FBQyxHQUFvQjtJQUNwQyxPQUFPLElBQUksT0FBTyxDQUFDLENBQUMsT0FBTyxFQUFFLEVBQUU7UUFDN0IsSUFBSSxJQUFJLEdBQUcsRUFBRSxDQUFDO1FBQ2QsR0FBRyxD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LEVBQUUsSUFBSSxDQUFDLENBQUM7WUFDNUIsT0FBTztRQUNULENBQUM7UUFDRCxNQUFNLEtBQUssR0FBRyxJQUFJLENBQUMsWUFBWSxDQUFDLEdBQUcsQ0FBQyxPQUFPLENBQUMsTUFBTSxJQUFJLElBQUksQ0FBQyxDQUFDLG1CQUFtQixDQUFDLENBQUM7UUFDakYsS0FBSyxJQUFJLENBQUMsT0FBTyxDQUFDLEtBQUssQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDLEtBQUssRUFBRSxFQUFFO1lBQ3RDLElBQUksQ0FBQyxLQUFLLEVBQUUsQ0FBQztnQkFDWCxNQUFNLENBQUMsT0FBTyxFQUFFLENBQUM7Z0JBQ2pCLE9BQU87WUFDVCxDQUFDO1lBQ0QsY0FBYyxDQUFDLEdBQUcsRUFBRSxLQUFLLENBQUMsQ0FBQztZQUMzQixrQkFBa0IsQ0FBQyxHQUFHLENBQUMsQ0FBQztZQUN4QixLQUFLLENBQUMsRUFBRSxDQUFDLEdBQUcsRUFBRSxNQUFNLEVBQUUsSUFBSSxDQUFDLENBQUM7UUFDOUIsQ0FBQyxDQUFDLENBQUM7SUFDTCxDQUFDLENBQUMsQ0FBQztJQUVILE9BQU8sSUFBSSxPQUFPLENBQUMsQ0FBQyxPQUFPLEVBQUUsRUFBRTtRQUM3QixNQUFNLENBQUMsTUFBTSxDQUFDLFVBQVUsRUFBRSxTQUFTLEVBQUUsR0FBRyxFQUFFO1lBQ3hDLE1BQU0sQ0FBQyxJQUFJLENBQUMseUJBQXlCLFVBQVUscUJBQXFCLE9BQU8sRUFBRSxDQUFDLENBQUM7WUFDL0UsT0FBTyxFQUFFLENBQUM7UUFDWixDQUFDLENBQUMsQ0FBQztJQUNMLENBQUMsQ0FBQyxDQUFDO0FBQ0wsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogVGhlIHR1bm5lbCBnYXRlIHJldmVyc2UtcHJveHkuXG4gKlxuICogQmluZHMgdGhlIFBVQkxJQyBwb3J0ICh0aGUgY29udGFpbmVyJ3MgYERBVEFCUklDS1NfQVBQX1BPUlRgKSBhbmQgZm9yd2FyZHMgdG9cbiAqIHRoZSByZWFsIGFwcCBvbiBhIHByaXZhdGUgbG9vcGJhY2sgcG9ydC4gSXQgaXMgdGhlIHNpbmdsZSBmcm9udCBkb29yIGZvciBib3RoXG4gKiB0cmFmZmljIHBhdGhzIGludG8gdGhlIGNvbnRhaW5lcjpcbiAqXG4gKiAgIC0gKipwb3J0ciBjbGllbnQqKiAoc2FtZSBjb250YWluZXIsIGNvbm5lY3RzIG92ZXIgTE9PUEJBQ0spIC0gdGhlIHB1YmxpY1xuICogICAgIHR1bm5lbC4gVGhpcyBpcyB3aGF0IHRoZSBnYXRlIHByb3RlY3RzLlxuICogICAtICoqdGhlIGhvc3RpbmcgcGxhdGZvcm0ncyBmcm9udCBkb29yKiogKERhdGFicmlja3MgQXBwcycgY29udHJvbCBwbGFuZSwgb3JcbiAqICAgICBhbnkgb3RoZXIgaG9zdCByZWFjaGluZyB0aGUgYDAuMC4wLjBgIHBvcnQgZnJvbSBhIE5PTi1sb29wYmFja1xuICogICAgIGNvbnRhaW5lci1uZXR3b3JrIGFkZHJlc3MpIC0gcGFzc2VkIHRocm91Z2ggVU5HQVRFRCwgcGVyIHRoZSBydWxlIFwiaWYgaXQnc1xuICogICAgIG5vdCBmcm9tIHRoZSBwb3J0ciBjbGllbnQsIGxldCBpdCBpblwiLiBUaGUgcGxhdGZvcm0gYWxyZWFkeSBhdXRoZW50aWNhdGVzXG4gKiAgICAgdGhhdCBwYXRoOyB0aGUgZ2F0ZSBleGlzdHMgZm9yIHRoZSB0dW5uZWwsIHdoaWNoIG5vdGhpbmcgZWxzZSBwcm90ZWN0cy5cbiAqXG4gKiBUaGUgZGlzdGluZ3Vpc2hlciBpcyB0aGUgY29ubmVjdGlvbidzIHNvdXJjZSBhZGRyZXNzOiBhIGxvb3BiYWNrXG4gKiBgcmVxLnNvY2tldC5yZW1vdGVBZGRyZXNzYCBpcyB0aGUgcG9ydHIgY2xpZW50OyBhbnl0aGluZyBlbHNlIGlzIHRoZSBwbGF0Zm9ybS5cbiAqXG4gKiBGb3IgcG9ydHIgdHJhZmZpYyB0aGUgZ2F0ZSBpcyBhIHN0YW5kYXJkIFNQQSBnYXRlOiBzdGF0aWMgYXNzZXRzICsgdGhlIGxvZ2luXG4gKiBmbG93IChgL2FwaS9lbWFpbC9hdXRoLypgLCBhbnN3ZXJlZCBJTi1QUk9DRVNTIGJ5IHRoaXMgcHJveHkgdmlhIHRoZVxuICoge0BsaW5rIEF1dGhHYXRlQXBpfSkgYXJlIG9wZW4gc28gdGhlIGJyb3dzZXIgY2FuIGxvYWQgdGhlIGNsaWVudCBhbmQgcmVuZGVyIHRoZVxuICogYDxBdXRoR2F0ZT5gOyBldmVyeSBvdGhlciBgL2FwaS8qYCBuZWVkcyBhIHZhbGlkIHNlc3Npb24gY29va2llIG9yIGdldHMgNDAxLlxuICogV2ViU29ja2V0IHVwZ3JhZGVzIGFyZSBnYXRlZCB0aGUgc2FtZSB3YXkgYW5kIGZvcndhcmRlZCB3aXRoIGBodHRwLXByb3h5LTNgLlxuICpcbiAqIEBtb2R1bGVcbiAqL1xuXG5pbXBvcnQgeyBjcmVhdGVTZXJ2ZXIsIHR5cGUgSW5jb21pbmdNZXNzYWdlLCB0eXBlIFNlcnZlclJlc3BvbnNlIH0gZnJvbSBcIm5vZGU6aHR0cFwiO1xuaW1wb3J0IHR5cGUgeyBTb2NrZXQgfSBmcm9tIFwibm9kZTpuZXRcIjtcbmltcG9ydCB7IGh0dHAsIGpzb24sIGxvZywgdG9rZW4gfSBmcm9tIFwiQGRieC10b29scy9zaGFyZWQtY29yZVwiO1xuaW1wb3J0IHsgYXV0aFJlcXVlc3RTY2hlbWEsIGF1dGhWZXJpZnlTY2hlbWEsIFNFU1NJT05fQ09PS0lFX05BTUUgfSBmcm9tIFwiQGRieC10b29scy9zaGFyZWQtZW1haWxcIjtcbmltcG9ydCBQcm94eU1vZHVsZSBmcm9tIFwiaHR0cC1wcm94eS0zXCI7XG5pbXBvcnQgeyB0b0hlYWRlclBvbGljeSwgdHlwZSBIZWFkZXJQb2xpY3kgfSBmcm9tIFwiLi9oZWFkZXJzLnRzXCI7XG5pbXBvcnQgdHlwZSB7IEF1dGhHYXRlQXBpIH0gZnJvbSBcIi4vcGx1Z2luLnRzXCI7XG5cbmNvbnN0IGxvZ2dlciA9IGxvZy5sb2dnZXIoXCJ0dW5uZWw6cHJveHlcIik7XG5cbi8qKiBSb3V0ZSBwcmVmaXggdGhlIGxvZ2luIGZsb3cgbGl2ZXMgdW5kZXIgKG9wZW4sIGFuc3dlcmVkIGluLXByb2Nlc3MpLiAqL1xuY29uc3QgQVVUSF9QUkVGSVggPSBcIi9hcGkvZW1haWwvYXV0aFwiO1xuXG4vKiogVHJ1ZSB3aGVuIGEgc29ja2V0IGFkZHJlc3MgaXMgbG9vcGJhY2sgKHRoZSBwb3J0ciBjbGllbnQsIHNhbWUgY29udGFpbmVyKS4gKi9cbmZ1bmN0aW9uIGlzTG9vcGJhY2soYWRkcjogc3RyaW5nIHwgdW5kZWZpbmVkKTogYm9vbGVhbiB7XG4gIGlmICghYWRkcikgcmV0dXJuIGZhbHNlO1xuICAvLyBOb3JtYWxpemUgSVB2NC1tYXBwZWQgSVB2NiAoYDo6ZmZmZjoxMjcuMC4wLjFgKSBhbmQgYmFyZSBJUHY2IGxvb3BiYWNrLlxuICBjb25zdCBhID0gYWRkci5yZXBsYWNlKC9eOjpmZmZmOi8sIFwiXCIpO1xuICByZXR1cm4gYSA9PT0gXCIxMjcuMC4wLjFcIiB8fCBhLnN0YXJ0c1dpdGgoXCIxMjcuXCIpIHx8IGEgPT09IFwiOjoxXCIgfHwgYSA9PT0gXCJsb2NhbGhvc3RcIjtcbn1cblxuLyoqIFJlYWQgdGhlIHdob2xlIHJlcXVlc3QgYm9keSBhcyB0ZXh0IChzbWFsbCBKU09OIHBheWxvYWRzIG9ubHkpLiAqL1xuZnVuY3Rpb24gcmVhZEJvZHkocmVxOiBJbmNvbWluZ01lc3NhZ2UpOiBQcm9taXNlPHN0cmluZz4ge1xuICByZXR1cm4gbmV3IFByb21pc2UoKHJlc29sdmUpID0+IHtcbiAgICBsZXQgZGF0YSA9IFwiXCI7XG4gICAgcmVxLm9uKFwiZGF0YVwiLCAoY2h1bmspID0+IChkYXRhICs9IGNodW5rKSk7XG4gICAgcmVxLm9uKFwiZW5kXCIsICgpID0+IHJlc29sdmUoZGF0YSkpO1xuICAgIHJlcS5vbihcImVycm9yXCIsICgpID0+IHJlc29sdmUoZGF0YSkpO1xuICB9KTtcbn1cblxuZnVuY3Rpb24gc2VuZEpzb24ocmVzOiBTZXJ2ZXJSZXNwb25zZSwgc3RhdHVzOiBudW1iZXIsIGJvZHk6IHVua25vd24sIHNldENvb2tpZT86IHN0cmluZyk6IHZvaWQge1xuICBjb25zdCBoZWFkZXJzOiBSZWNvcmQ8c3RyaW5nLCBzdHJpbmc+ID0geyBcImNvbnRlbnQtdHlwZVwiOiBcImFwcGxpY2F0aW9uL2pzb25cIiB9O1xuICBpZiAoc2V0Q29va2llKSBoZWFkZXJzW1wic2V0LWNvb2tpZVwiXSA9IHNldENvb2tpZTtcbiAgcmVzLndyaXRlSGVhZChzdGF0dXMsIGhlYWRlcnMpO1xuICByZXMuZW5kKEpTT04uc3RyaW5naWZ5KGJvZHkpKTtcbn1cblxuLyoqIE9wdGlvbnMgZm9yIHtAbGluayBzdGFydFByb3h5fS4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgUHJveHlPcHRpb25zIHtcbiAgLyoqIFB1YmxpYyBwb3J0IHRvIGxpc3RlbiBvbiAodGhlIGNvbnRhaW5lcidzIERBVEFCUklDS1NfQVBQX1BPUlQpLiAqL1xuICBwdWJsaWNQb3J0OiBudW1iZXI7XG4gIC8qKiBQcml2YXRlIHBvcnQgdGhlIHJlYWwgYXBwIGxpc3RlbnMgb24gKGxvb3BiYWNrKS4gKi9cbiAgYXBwUG9ydDogbnVtYmVyO1xuICAvKipcbiAgICogVGhlIGluLXByb2Nlc3MgZ2F0ZSBBUEksIG9yIGB1bmRlZmluZWRgIHRvIHJ1biBPUEVOIChpbnNlY3VyZSBtb2RlKTogZXZlcnlcbiAgICogcmVxdWVzdCAtIGluY2x1ZGluZyBwb3J0ciB0cmFmZmljIC0gaXMgZm9yd2FyZGVkIHVuZ2F0ZWQuIFVzZWQgd2hlbiB0aGVcbiAgICogb3BlcmF0b3IgcGFzc2VkIGAtLWluc2VjdXJlYCAvIGBUVU5ORUxfSU5TRUNVUkU9dHJ1ZWAuXG4gICAqL1xuICBnYXRlPzogQXV0aEdhdGVBcGk7XG4gIC8qKlxuICAgKiBFeHRyYSBgeC1gIHJlcXVlc3QgaGVhZGVycyB0dW5uZWwgdHJhZmZpYyBtYXkgZm9yd2FyZCwgYXMgbGl0ZXJhbHMsIGdsb2JzLCBvclxuICAgKiBgL3JlZ2V4L2BlcyAtIHVuaW9uZWQgd2l0aCB7QGxpbmsgREVGQVVMVF9GT1JXQVJEX0hFQURFUlN9LiBFdmVyeSBvdGhlciBgeC1gXG4gICAqIGhlYWRlciBpcyBzdHJpcHBlZCwgYW5kIHtAbGluayBQUk9URUNURURfSEVBREVSU30gaXMgc3RyaXBwZWQgcmVnYXJkbGVzcy4gU2VlXG4gICAqIGAuL2hlYWRlcnMudHNgIGZvciB3aHkgdGhlIHBvbGljeSBpcyBhbiBhbGxvdy1saXN0LlxuICAgKi9cbiAgZm9yd2FyZEhlYWRlcnM/OiByZWFkb25seSBzdHJpbmdbXTtcbn1cblxuLyoqIFN0YXJ0IHRoZSBnYXRlIHByb3h5LiBSZXNvbHZlcyBvbmNlIGl0IGlzIGxpc3RlbmluZy4gKi9cbmV4cG9ydCBmdW5jdGlvbiBzdGFydFByb3h5KHtcbiAgcHVibGljUG9ydCxcbiAgYXBwUG9ydCxcbiAgZ2F0ZSxcbiAgZm9yd2FyZEhlYWRlcnMsXG59OiBQcm94eU9wdGlvbnMpOiBQcm9taXNlPHZvaWQ+IHtcbiAgY29uc3QgcHJveHkgPSBQcm94eU1vZHVsZS5jcmVhdGVQcm94eVNlcnZlcih7XG4gICAgdGFyZ2V0OiB7IGhvc3Q6IFwiMTI3LjAuMC4xXCIsIHBvcnQ6IGFwcFBvcnQgfSxcbiAgICB3czogdHJ1ZSxcbiAgICB4ZndkOiB0cnVlLFxuICB9KTtcbiAgcHJveHkub24oXCJlcnJvclwiLCAoZXJyOiBFcnJvciwgX3JlcTogdW5rbm93biwgcmVzOiB1bmtub3duKSA9PiB7XG4gICAgbG9nZ2VyLndhcm4oXCJ1cHN0cmVhbSBwcm94eSBlcnJvclwiLCB7IGVycm9yOiBlcnIubWVzc2FnZSB9KTtcbiAgICBjb25zdCByID0gcmVzIGFzIFNlcnZlclJlc3BvbnNlIHwgdW5kZWZpbmVkO1xuICAgIGlmIChyICYmIFwid3JpdGVIZWFkXCIgaW4gciAmJiAhci5oZWFkZXJzU2VudCkge1xuICAgICAgc2VuZEpzb24ociwgNTAyLCB7IGVycm9yOiBcInVwc3RyZWFtIHVuYXZhaWxhYmxlXCIgfSk7XG4gICAgfVxuICB9KTtcblxuICAvLyBDb21waWxlZCBvbmNlOiB0aGUgaW5ib3VuZC1oZWFkZXIgYWxsb3ctbGlzdCBhcHBsaWVkIHRvIGV2ZXJ5IGdhdGVkIHJlcXVlc3QuXG4gIGNvbnN0IGhlYWRlclBvbGljeSA9IHRvSGVhZGVyUG9saWN5KGZvcndhcmRIZWFkZXJzKTtcbiAgbG9nZ2VyLmRlYnVnKFwiaW5ib3VuZCBoZWFkZXIgcG9saWN5XCIsIHsgZm9yd2FyZDogaGVhZGVyUG9saWN5LnBhdHRlcm5zIH0pO1xuXG4gIC8qKiBUaGUgc2Vzc2lvbiBjb29raWUgZm9yIGEgdmVyaWZpZWQgZW1haWwsIGFzIGEgU2V0LUNvb2tpZSBzdHJpbmcuICovXG4gIGNvbnN0IHNlc3Npb25Db29raWUgPSAodG9rZW46IHN0cmluZywgbWF4QWdlU2Vjb25kczogbnVtYmVyKTogc3RyaW5nID0+XG4gICAgW1xuICAgICAgYCR7U0VTU0lPTl9DT09LSUVfTkFNRX09JHt0b2tlbn1gLFxuICAgICAgXCJQYXRoPS9cIixcbiAgICAgIFwiSHR0cE9ubHlcIixcbiAgICAgIFwiU2FtZVNpdGU9TGF4XCIsXG4gICAgICBgTWF4LUFnZT0ke21heEFnZVNlY29uZHN9YCxcbiAgICAgIHByb2Nlc3MuZW52Lk5PREVfRU5WID09PSBcInByb2R1Y3Rpb25cIiA/IFwiU2VjdXJlXCIgOiBcIlwiLFxuICAgIF1cbiAgICAgIC5maWx0ZXIoQm9vbGVhbilcbiAgICAgIC5qb2luKFwiOyBcIik7XG5cbiAgLyoqXG4gICAqIENsaWVudCBJUCBmb3IgcmF0ZS1saW1pdGluZzogdGhlIHBvcnRyIGNsaWVudCdzIGZvcndhcmRlZCBYRkYsIGVsc2UgdGhlIHNvY2tldC5cbiAgICpcbiAgICogRGVsaWJlcmF0ZWx5IHRoZSBSSUdIVE1PU1QgYHgtZm9yd2FyZGVkLWZvcmAgZW50cnksIG5vdCB0aGUgbGVmdG1vc3QuIFRoZVxuICAgKiBsaXN0IGdyb3dzIGxlZnQtdG8tcmlnaHQgYXMgZWFjaCBob3AgYXBwZW5kcywgc28gdGhlIGxhc3QgZW50cnkgaXMgdGhlIG9uZVxuICAgKiB0aGUgbmVhcmVzdCB0cnVzdGVkIHByb3h5IChwb3J0cikgd3JvdGUgYW5kIGV2ZXJ5IGVhcmxpZXIgZW50cnkgaXMgYSB2YWx1ZVxuICAgKiB0aGUgY2FsbGVyIGNvdWxkIGhhdmUgc2VudC4gUmVhZGluZyB0aGUgbGVmdG1vc3QgbGV0cyBhIGNsaWVudCB2YXJ5IG9uZVxuICAgKiBoZWFkZXIgdG8gZ2V0IGEgZnJlc2ggcmF0ZS1saW1pdCBidWNrZXQgcGVyIHJlcXVlc3QsIHdoaWNoIGRlZmVhdHMgYm90aCB0aGVcbiAgICogcGVyLUlQIGNvZGUtcmVxdWVzdCBhbmQgdmVyaWZ5LWF0dGVtcHQgbGltaXRlcnMuIENhbGxlZCBCRUZPUkVcbiAgICoge0BsaW5rIEhlYWRlclBvbGljeS5hcHBseX0gc3RyaXBzIHRoZSBoZWFkZXIsIHNvIHRoZSBob25lc3QgaG9wIHZhbHVlIGlzXG4gICAqIHN0aWxsIGF2YWlsYWJsZSBoZXJlLlxuICAgKi9cbiAgY29uc3QgY2xpZW50SXAgPSAocmVxOiBJbmNvbWluZ01lc3NhZ2UpOiBzdHJpbmcgPT4ge1xuICAgIGNvbnN0IGZvcndhcmRlZCA9IHJlcS5oZWFkZXJzW1wieC1mb3J3YXJkZWQtZm9yXCJdO1xuICAgIGNvbnN0IGNoYWluID0gKEFycmF5LmlzQXJyYXkoZm9yd2FyZGVkKSA/IGZvcndhcmRlZC5qb2luKFwiLFwiKSA6IChmb3J3YXJkZWQgPz8gXCJcIikpXG4gICAgICAuc3BsaXQoXCIsXCIpXG4gICAgICAubWFwKChlbnRyeSkgPT4gZW50cnkudHJpbSgpKVxuICAgICAgLmZpbHRlcihCb29sZWFuKTtcbiAgICByZXR1cm4gY2hhaW4uYXQoLTEpID8/IHJlcS5zb2NrZXQucmVtb3RlQWRkcmVzcyA/PyBcInVua25vd25cIjtcbiAgfTtcblxuICAvKipcbiAgICogUmVtb3ZlIHRoZSBnYXRlJ3Mgc2Vzc2lvbiBjb29raWUgZnJvbSB0aGUgYENvb2tpZWAgaGVhZGVyIGJlZm9yZSBmb3J3YXJkaW5nLFxuICAgKiBzbyB0aGUgYXBwIG5ldmVyIHNlZXMgYGRieF9hdXRoYCAoaXQgaXMgdGhlIHByb3h5J3MgY29uY2Vybiwgbm90IHRoZSBhcHAncykuXG4gICAqIFByZXNlcnZlcyBhbnkgb3RoZXIgY29va2llcy4gUmVtb3ZlcyB0aGUgaGVhZGVyIGVudGlyZWx5IHdoZW4gaXQgYmVjb21lcyBlbXB0eS5cbiAgICovXG4gIGNvbnN0IHN0cmlwU2Vzc2lvbkNvb2tpZSA9IChyZXE6IEluY29taW5nTWVzc2FnZSk6IHZvaWQgPT4ge1xuICAgIGNvbnN0IHJhdyA9IHJlcS5oZWFkZXJzLmNvb2tpZTtcbiAgICBpZiAoIXJhdykgcmV0dXJuO1xuICAgIGNvbnN0IGtlcHQgPSByYXdcbiAgICAgIC5zcGxpdChcIjtcIilcbiAgICAgIC5tYXAoKGMpID0+IGMudHJpbSgpKVxuICAgICAgLmZpbHRlcigoYykgPT4gYyAmJiAhYy5zdGFydHNXaXRoKGAke1NFU1NJT05fQ09PS0lFX05BTUV9PWApKTtcbiAgICBpZiAoa2VwdC5sZW5ndGgpIHJlcS5oZWFkZXJzLmNvb2tpZSA9IGtlcHQuam9pbihcIjsgXCIpO1xuICAgIGVsc2UgZGVsZXRlIHJlcS5oZWFkZXJzLmNvb2tpZTtcbiAgfTtcblxuICAvKipcbiAgICogUHJlc2VudCBhbiBPVFAtYXV0aGVudGljYXRlZCBjYWxsZXIgdG8gdGhlIGFwcCB0aGUgU0FNRSB3YXkgYSBwbGF0Zm9ybSBmcm9udFxuICAgKiBkb29yIGRvZXM6IHNldCB0aGUgZnJvbnQgZG9vcidzIG93biBpZGVudGl0eSBoZWFkZXJzIHRvIHRoZSB2ZXJpZmllZCBhZGRyZXNzXG4gICAqIChBcHBLaXQgcmVhZHMge0BsaW5rIHRva2VuLlVTRVJfSURfSEVBREVSfSBmb3IgdGhlIE9CTyB1c2VyIGlkKSwgc28gdGhlIGFwcFxuICAgKiBuZWVkcyBubyBnYXRlLXNwZWNpZmljIGNvZGUgcGF0aC4gVGhlIG5hbWVzIGNvbWUgZnJvbVxuICAgKiBgQGRieC10b29scy9zaGFyZWQtY29yZWAncyBgdG9rZW5gIG1vZHVsZSAtIHRoZSBzYW1lIGNvbnN0YW50cyBBcHBLaXQtc2lkZVxuICAgKiBjb2RlIHJlYWRzIHRoZW0gYnkgLSByYXRoZXIgdGhhbiBiZWluZyByZS1zcGVsbGVkIGhlcmUuXG4gICAqXG4gICAqIFdoYXQgdGhlIGdhdGUgQ0FOTk9UIHNldCBpcyB7QGxpbmsgdG9rZW4uQUNDRVNTX1RPS0VOX0hFQURFUn06IGFuIE9UUCBzZXNzaW9uXG4gICAqIHByb3ZlcyBhbiBlbWFpbCBhZGRyZXNzLCBub3QgcG9zc2Vzc2lvbiBvZiBhIERhdGFicmlja3MgY3JlZGVudGlhbCwgYW5kXG4gICAqIHRoZXJlIGlzIG5vIHdheSB0byBtaW50IG9uZSBmb3IgdGhlIGNhbGxlci4gSXRzIGFic2VuY2UgaXMgZXhhY3RseSB3aGF0XG4gICAqIGBAZGJ4LXRvb2xzL2FwcGtpdGAncyBgaWRlbnRpdHlgIGBcImF1dG9cImAgbW9kZSBkZXRlY3RzLCBzbyBhIGdhdGVkIHJlcXVlc3RcbiAgICogcnVucyBhcyB0aGUgYXBwJ3Mgc2VydmljZSBwcmluY2lwYWwgaW5zdGVhZCBvZiB0aHJvd2luZy5cbiAgICovXG4gIGNvbnN0IGluamVjdElkZW50aXR5ID0gKHJlcTogSW5jb21pbmdNZXNzYWdlLCBlbWFpbDogc3RyaW5nKTogdm9pZCA9PiB7XG4gICAgcmVxLmhlYWRlcnNbdG9rZW4uVVNFUl9JRF9IRUFERVJdID0gZW1haWw7XG4gICAgcmVxLmhlYWRlcnNbdG9rZW4uVVNFUl9FTUFJTF9IRUFERVJdID0gZW1haWw7XG4gIH07XG5cbiAgLyoqXG4gICAqIEFwcGx5IHRoZSBpbmJvdW5kLWhlYWRlciBwb2xpY3kgdG8gYSB0dW5uZWwgcmVxdWVzdDogZXZlcnkgYHgtYCBoZWFkZXIgdGhlXG4gICAqIGFsbG93LWxpc3QgZG9lcyBub3QgbmFtZSBpcyBkZWxldGVkLCBhbmQgdGhlIHBsYXRmb3JtIGlkZW50aXR5L3RyYW5zcG9ydCBzZXRcbiAgICogaXMgZGVsZXRlZCByZWdhcmRsZXNzLiBTZWUgYC4vaGVhZGVycy50c2AgZm9yIHRoZSByZWFzb25pbmc7IHRoZVxuICAgKiBzZWN1cml0eS1jcml0aWNhbCBjYXNlIGlzIHtAbGluayB0b2tlbi5BQ0NFU1NfVE9LRU5fSEVBREVSfSwgd2hpY2ggdGhlIGdhdGVcbiAgICogbmV2ZXIgc2V0cyBidXQgYW4gYXBwIHJ1bm5pbmcgYGlkZW50aXR5OiBcImF1dG9cImAgdHJlYXRzIGFzIHByb29mIHRoZSByZXF1ZXN0XG4gICAqIGNhbiBkbyBPQk8uXG4gICAqXG4gICAqIGB4ZndkOiB0cnVlYCBvbiB0aGUgcHJveHkgcmUtYWRkcyBgeC1mb3J3YXJkZWQtZm9yYC9gLXByb3RvYC9gLXBvcnRgL2AtaG9zdGBcbiAgICogYWZ0ZXJ3YXJkcyBmcm9tIHRoZSByZWFsIHNvY2tldCwgc28gdGhlIGFwcCBzdGlsbCBzZWVzIHRob3NlIC0gaXQganVzdCBzZWVzXG4gICAqIHRoZSBob25lc3QgdmFsdWVzIHJhdGhlciB0aGFuIHRoZSBjYWxsZXIncyBjbGFpbS5cbiAgICovXG4gIGNvbnN0IGFwcGx5SGVhZGVyUG9saWN5ID0gKHJlcTogSW5jb21pbmdNZXNzYWdlKTogdm9pZCA9PiB7XG4gICAgY29uc3QgcmVtb3ZlZCA9IGhlYWRlclBvbGljeS5hcHBseShyZXEuaGVhZGVycyBhcyBSZWNvcmQ8c3RyaW5nLCB1bmtub3duPik7XG4gICAgaWYgKHJlbW92ZWQubGVuZ3RoKSBsb2dnZXIuZGVidWcoXCJzdHJpcHBlZCBpbmJvdW5kIGhlYWRlcnNcIiwgeyByZW1vdmVkIH0pO1xuICB9O1xuXG4gIGNvbnN0IHNlcnZlciA9IGNyZWF0ZVNlcnZlcihhc3luYyAocmVxLCByZXMpID0+IHtcbiAgICBjb25zdCBwYXRoID0gKHJlcS51cmwgPz8gXCIvXCIpLnNwbGl0KFwiP1wiKVswXSE7XG5cbiAgICAvLyBJbnNlY3VyZS9vcGVuIG1vZGUgKG5vIGdhdGUpIE9SIG5vbi1sb29wYmFjayB0cmFmZmljICh0aGUgaG9zdGluZ1xuICAgIC8vIHBsYXRmb3JtJ3MgZnJvbnQgZG9vciAvIGNvbnRyb2wgcGxhbmUpOiBmb3J3YXJkIHVuZ2F0ZWQuXG4gICAgaWYgKCFnYXRlIHx8ICFpc0xvb3BiYWNrKHJlcS5zb2NrZXQucmVtb3RlQWRkcmVzcykpIHtcbiAgICAgIHByb3h5LndlYihyZXEsIHJlcyk7XG4gICAgICByZXR1cm47XG4gICAgfVxuXG4gICAgLy8gLS0tIHBvcnRyIHRyYWZmaWM6IHRoZSBnYXRlIGFwcGxpZXMgLS0tXG5cbiAgICAvLyBUaGUgbG9naW4gZmxvdyBpcyBhbnN3ZXJlZCBJTi1QUk9DRVNTICh0aGUgYXBwIGhhcyBubyBzZXJ2ZXIgdG8gZm9yd2FyZCB0bykuXG4gICAgaWYgKHBhdGggPT09IGAke0FVVEhfUFJFRklYfS9zdGF0dXNgKSB7XG4gICAgICBjb25zdCB0b2tlbiA9IGh0dHAucGFyc2VDb29raWVzKHJlcS5oZWFkZXJzLmNvb2tpZSA/PyBudWxsKVtTRVNTSU9OX0NPT0tJRV9OQU1FXTtcbiAgICAgIHNlbmRKc29uKHJlcywgMjAwLCBhd2FpdCBnYXRlLnN0YXR1cyh0b2tlbikpO1xuICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICBpZiAocGF0aCA9PT0gYCR7QVVUSF9QUkVGSVh9L3JlcXVlc3RgICYmIHJlcS5tZXRob2QgPT09IFwiUE9TVFwiKSB7XG4gICAgICBjb25zdCBwYXJzZWQgPSBhdXRoUmVxdWVzdFNjaGVtYS5zYWZlUGFyc2UoanNvbi5wYXJzZVJlY29yZChhd2FpdCByZWFkQm9keShyZXEpKSk7XG4gICAgICBpZiAoIXBhcnNlZC5zdWNjZXNzKSByZXR1cm4gc2VuZEpzb24ocmVzLCAyMDAsIHsgb2s6IHRydWUgfSk7IC8vIGFudGktZW51bWVyYXRpb25cbiAgICAgIHJldHVybiBzZW5kSnNvbihyZXMsIDIwMCwgYXdhaXQgZ2F0ZS5yZXF1ZXN0KHBhcnNlZC5kYXRhLmVtYWlsLCBjbGllbnRJcChyZXEpKSk7XG4gICAgfVxuICAgIGlmIChwYXRoID09PSBgJHtBVVRIX1BSRUZJWH0vdmVyaWZ5YCAmJiByZXEubWV0aG9kID09PSBcIlBPU1RcIikge1xuICAgICAgY29uc3QgcGFyc2VkID0gYXV0aFZlcmlmeVNjaGVtYS5zYWZlUGFyc2UoanNvbi5wYXJzZVJlY29yZChhd2FpdCByZWFkQm9keShyZXEpKSk7XG4gICAgICBpZiAoIXBhcnNlZC5zdWNjZXNzKSByZXR1cm4gc2VuZEpzb24ocmVzLCAyMDAsIHsgb2s6IGZhbHNlIH0pO1xuICAgICAgY29uc3QgcmVzdWx0ID0gYXdhaXQgZ2F0ZS52ZXJpZnkocGFyc2VkLmRhdGEuZW1haWwsIHBhcnNlZC5kYXRhLmNvZGUsIGNsaWVudElwKHJlcSkpO1xuICAgICAgY29uc3QgY29va2llID1cbiAgICAgICAgcmVzdWx0Lm9rICYmIHJlc3VsdC50b2tlbiA/IHNlc3Npb25Db29raWUocmVzdWx0LnRva2VuLCBnYXRlLnNlc3Npb25UdGxTZWNvbmRzKSA6IHVuZGVmaW5lZDtcbiAgICAgIHJldHVybiBzZW5kSnNvbihcbiAgICAgICAgcmVzLFxuICAgICAgICAyMDAsXG4gICAgICAgIHsgb2s6IHJlc3VsdC5vaywgLi4uKHJlc3VsdC5yZXRyeUFmdGVyID8geyByZXRyeUFmdGVyOiByZXN1bHQucmV0cnlBZnRlciB9IDoge30pIH0sXG4gICAgICAgIGNvb2tpZSxcbiAgICAgICk7XG4gICAgfVxuICAgIGlmIChwYXRoID09PSBgJHtBVVRIX1BSRUZJWH0vbG9nb3V0YCAmJiByZXEubWV0aG9kID09PSBcIlBPU1RcIikge1xuICAgICAgcmV0dXJuIHNlbmRKc29uKFxuICAgICAgICByZXMsXG4gICAgICAgIDIwMCxcbiAgICAgICAgeyBvazogdHJ1ZSB9LFxuICAgICAgICBgJHtTRVNTSU9OX0NPT0tJRV9OQU1FfT07IFBhdGg9LzsgSHR0cE9ubHk7IE1heC1BZ2U9MGAsXG4gICAgICApO1xuICAgIH1cblxuICAgIC8vIEFudGktc3Bvb2Y6IGFwcGx5IHRoZSBpbmJvdW5kLWhlYWRlciBhbGxvdy1saXN0IHRvIHBvcnRyIHRyYWZmaWMgLSBvbmx5IHRoZVxuICAgIC8vIGdhdGUgbWF5IGFzc2VydCBpZGVudGl0eSwgYW5kIGl0IGRvZXMgc28gYmVsb3cgZm9yIGEgdmVyaWZpZWQgc2Vzc2lvbi5cbiAgICBhcHBseUhlYWRlclBvbGljeShyZXEpO1xuXG4gICAgLy8gU3RhdGljIChub24tQVBJKSBsb2FkcyBmcmVlbHkgc28gdGhlIFNQQSArIDxBdXRoR2F0ZT4gY2FuIHJlbmRlci4gU3RyaXAgdGhlXG4gICAgLy8gc2Vzc2lvbiBjb29raWUgc28gaXQgbmV2ZXIgbGVha3MgdG8gdGhlIHN0YXRpYyBoYW5kbGVyLlxuICAgIGlmICghcGF0aC5zdGFydHNXaXRoKFwiL2FwaS9cIikpIHtcbiAgICAgIHN0cmlwU2Vzc2lvbkNvb2tpZShyZXEpO1xuICAgICAgcHJveHkud2ViKHJlcSwgcmVzKTtcbiAgICAgIHJldHVybjtcbiAgICB9XG5cbiAgICAvLyBFdmVyeSBvdGhlciAvYXBpLyogcmVxdWlyZXMgYSB2YWxpZCBzZXNzaW9uLlxuICAgIGNvbnN0IHRva2VuID0gaHR0cC5wYXJzZUNvb2tpZXMocmVxLmhlYWRlcnMuY29va2llID8/IG51bGwpW1NFU1NJT05fQ09PS0lFX05BTUVdO1xuICAgIGNvbnN0IGVtYWlsID0gYXdhaXQgZ2F0ZS5zZXNzaW9uKHRva2VuKTtcbiAgICBpZiAoZW1haWwpIHtcbiAgICAgIC8vIFByZXNlbnQgdGhlIE9UUCB1c2VyIGxpa2UgYSBwbGF0Zm9ybSBmcm9udCBkb29yLCBhbmQgZHJvcCB0aGUgZ2F0ZVxuICAgICAgLy8gY29va2llIHNvIHRoZSBhcHAgc2VlcyBhIGNsZWFuLCBmcm9udC1kb29yLXNoYXBlZCByZXF1ZXN0LlxuICAgICAgaW5qZWN0SWRlbnRpdHkocmVxLCBlbWFpbCk7XG4gICAgICBzdHJpcFNlc3Npb25Db29raWUocmVxKTtcbiAgICAgIHByb3h5LndlYihyZXEsIHJlcyk7XG4gICAgfSBlbHNlIHtcbiAgICAgIHNlbmRKc29uKHJlcywgNDAxLCB7IGVycm9yOiBcImF1dGhlbnRpY2F0aW9uIHJlcXVpcmVkXCIsIGxvZ2luUGF0aDogQVVUSF9QUkVGSVggfSk7XG4gICAgfVxuICB9KTtcblxuICAvLyBXZWJTb2NrZXQgdXBncmFkZXM6IGZvcndhcmQgZnJvbnQtZG9vciB0cmFmZmljIHVudG91Y2hlZDsgZ2F0ZSBsb29wYmFja1xuICAvLyAocG9ydHIpIEFQSSB1cGdyYWRlcyB0aGUgc2FtZSB3YXkgYXMgSFRUUCAoc3RyaXAgY29va2llICsgaW5qZWN0IGlkZW50aXR5KS5cbiAgc2VydmVyLm9uKFwidXBncmFkZVwiLCAocmVxOiBJbmNvbWluZ01lc3NhZ2UsIHNvY2tldDogU29ja2V0LCBoZWFkOiBCdWZmZXIpID0+IHtcbiAgICBpZiAoIWdhdGUgfHwgIWlzTG9vcGJhY2socmVxLnNvY2tldC5yZW1vdGVBZGRyZXNzKSkge1xuICAgICAgcHJveHkud3MocmVxLCBzb2NrZXQsIGhlYWQpO1xuICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICBhcHBseUhlYWRlclBvbGljeShyZXEpO1xuICAgIGlmICghKHJlcS51cmwgPz8gXCJcIikuc3RhcnRzV2l0aChcIi9hcGkvXCIpKSB7XG4gICAgICBzdHJpcFNlc3Npb25Db29raWUocmVxKTtcbiAgICAgIHByb3h5LndzKHJlcSwgc29ja2V0LCBoZWFkKTtcbiAgICAgIHJldHVybjtcbiAgICB9XG4gICAgY29uc3QgdG9rZW4gPSBodHRwLnBhcnNlQ29va2llcyhyZXEuaGVhZGVycy5jb29raWUgPz8gbnVsbClbU0VTU0lPTl9DT09LSUVfTkFNRV07XG4gICAgdm9pZCBnYXRlLnNlc3Npb24odG9rZW4pLnRoZW4oKGVtYWlsKSA9PiB7XG4gICAgICBpZiAoIWVtYWlsKSB7XG4gICAgICAgIHNvY2tldC5kZXN0cm95KCk7XG4gICAgICAgIHJldHVybjtcbiAgICAgIH1cbiAgICAgIGluamVjdElkZW50aXR5KHJlcSwgZW1haWwpO1xuICAgICAgc3RyaXBTZXNzaW9uQ29va2llKHJlcSk7XG4gICAgICBwcm94eS53cyhyZXEsIHNvY2tldCwgaGVhZCk7XG4gICAgfSk7XG4gIH0pO1xuXG4gIHJldHVybiBuZXcgUHJvbWlzZSgocmVzb2x2ZSkgPT4ge1xuICAgIHNlcnZlci5saXN0ZW4ocHVibGljUG9ydCwgXCIwLjAuMC4wXCIsICgpID0+IHtcbiAgICAgIGxvZ2dlci5pbmZvKGBnYXRlIHByb3h5IG9uIDAuMC4wLjA6JHtwdWJsaWNQb3J0fSAtPiBhcHAgMTI3LjAuMC4xOiR7YXBwUG9ydH1gKTtcbiAgICAgIHJlc29sdmUoKTtcbiAgICB9KTtcbiAgfSk7XG59XG4iXX0=
|
|
@@ -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,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gate's HS256 session-signing key, persisted in AppKit's cache.
|
|
3
|
+
*
|
|
4
|
+
* The key decides whether a session COOKIE still verifies. Before this module the
|
|
5
|
+
* fallback was `randomBytes(32)` per process, so every restart invalidated every
|
|
6
|
+
* outstanding cookie and each signed-in user had to request a new code - painful
|
|
7
|
+
* for a tunnel, which restarts whenever the app it wraps does. Now the key is
|
|
8
|
+
* stored in the cache AppKit already configured (memory, or Lakebase when the
|
|
9
|
+
* host wires a persistent `CacheStorage`), so with persistent storage a restart
|
|
10
|
+
* keeps sessions alive for {@link KEY_TTL_SECONDS}.
|
|
11
|
+
*
|
|
12
|
+
* An explicitly configured `TUNNEL_AUTH_JWT_SECRET` still wins outright. That is
|
|
13
|
+
* the right answer for a fleet: an operator-held secret needs no shared cache and
|
|
14
|
+
* no convergence, and it survives a cache flush.
|
|
15
|
+
*
|
|
16
|
+
* ## get / generate / get
|
|
17
|
+
*
|
|
18
|
+
* Two instances booting at once both miss, so both would generate - and the loser
|
|
19
|
+
* would sign cookies with a key the winner rejects. Resolution is a re-READ after
|
|
20
|
+
* the write: whatever the cache holds afterwards is the key everyone adopts, so
|
|
21
|
+
* the instances converge on ONE value instead of trusting the one they minted.
|
|
22
|
+
* (`set` is not conditional in the cache API - there is no `setnx` to lean on -
|
|
23
|
+
* and this runs once per process, so the extra round-trip is free.)
|
|
24
|
+
*
|
|
25
|
+
* A pathological interleave can still cost a key: if A writes between B's write
|
|
26
|
+
* and B's re-read, B adopts A's key while A adopts its own. The cost is bounded -
|
|
27
|
+
* a cookie minted in that window fails to verify and the holder signs in again -
|
|
28
|
+
* and it cannot produce a key one instance TRUSTS but another rejects for longer
|
|
29
|
+
* than the window itself. Set `TUNNEL_AUTH_JWT_SECRET` to remove the race
|
|
30
|
+
* entirely.
|
|
31
|
+
*
|
|
32
|
+
* ## Forcing every session to end
|
|
33
|
+
*
|
|
34
|
+
* {@link resolveSessionCutoff} reads a date from `TUNNEL_AUTH_SESSION_CUTOFF` (or
|
|
35
|
+
* `--session-cutoff`), and that date is part of the cache KEY. Moving it forward
|
|
36
|
+
* makes every prior key unreachable, so every cookie signed against it stops
|
|
37
|
+
* verifying - the log-everyone-out switch, without having to find and flush a
|
|
38
|
+
* cache entry. The cutoff is also asserted against each token's `iat`, so a
|
|
39
|
+
* cookie that predates it is refused even if it was signed with the key that is
|
|
40
|
+
* somehow still current.
|
|
41
|
+
*
|
|
42
|
+
* @module
|
|
43
|
+
*/
|
|
44
|
+
/**
|
|
45
|
+
* How long a cached signing key lives: 30 days.
|
|
46
|
+
*
|
|
47
|
+
* This is the ceiling on how long a session cookie can stay valid across
|
|
48
|
+
* restarts, so it is deliberately >= the default session TTL - a key that expired
|
|
49
|
+
* before the cookies it signed would log everyone out for no reason.
|
|
50
|
+
*/
|
|
51
|
+
export declare const KEY_TTL_SECONDS: number;
|
|
52
|
+
/**
|
|
53
|
+
* Resolve the force-clear cutoff as epoch MILLISECONDS, or `0` when unset.
|
|
54
|
+
*
|
|
55
|
+
* Accepts whatever `object.toDate` accepts - a `Date`, `2026-08-02`, an ISO
|
|
56
|
+
* instant, epoch seconds or millis from `date +%s`, or a relative duration
|
|
57
|
+
* (`-30d`, `7 days ago`), which is the spelling an operator reaching for this
|
|
58
|
+
* usually wants: sign out everyone who signed in more than a month ago.
|
|
59
|
+
*
|
|
60
|
+
* An UNPARSEABLE value is ignored with a warning rather than throwing: this is
|
|
61
|
+
* the switch that logs a fleet back in, and failing to boot over a typo is worse
|
|
62
|
+
* than not rotating.
|
|
63
|
+
*/
|
|
64
|
+
export declare function resolveSessionCutoff(configured?: string | number | Date): number;
|
|
65
|
+
/** A resolved key plus the cutoff it is scoped to. */
|
|
66
|
+
export interface SigningKey {
|
|
67
|
+
key: Uint8Array;
|
|
68
|
+
/** Force-clear cutoff in ms; `0` when unset. Tokens older than this are refused. */
|
|
69
|
+
cutoffMs: number;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The signing key for this gate, resolved once per process.
|
|
73
|
+
*
|
|
74
|
+
* Resolved ONCE, so `configuredCutoff` is honoured only on the first call - the
|
|
75
|
+
* plugin's `setup()` passes its resolved value there, before any request can
|
|
76
|
+
* reach the lazy path.
|
|
77
|
+
*
|
|
78
|
+
* `TUNNEL_AUTH_JWT_SECRET` when set, else the cache-backed key. A cache that is
|
|
79
|
+
* unavailable degrades to an ephemeral per-process key (the previous behaviour)
|
|
80
|
+
* rather than refusing to sign: the key only validates an ALREADY-issued session,
|
|
81
|
+
* so losing it costs sessions, never admission - a caller still needs a code
|
|
82
|
+
* delivered to an allow-listed address.
|
|
83
|
+
*/
|
|
84
|
+
export declare function signingKey(configuredCutoff?: string | number | Date): Promise<SigningKey>;
|
|
85
|
+
/** Reset the per-process key (tests, or after changing the env in-process). */
|
|
86
|
+
export declare function resetSigningKey(): void;
|