@dbx-tools/tunnel 0.6.60 → 0.6.62

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/lib/src/proxy.js DELETED
@@ -1,247 +0,0 @@
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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHJveHkuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvcHJveHkudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7R0F5Qkc7QUFFSCxPQUFPLEVBQUUsWUFBWSxFQUE2QyxNQUFNLFdBQVcsQ0FBQztBQUVwRixPQUFPLEVBQUUsSUFBSSxFQUFFLElBQUksRUFBRSxHQUFHLEVBQUUsS0FBSyxFQUFFLE1BQU0sd0JBQXdCLENBQUM7QUFDaEUsT0FBTyxFQUFFLGlCQUFpQixFQUFFLGdCQUFnQixFQUFFLG1CQUFtQixFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFDbkcsT0FBTyxXQUFXLE1BQU0sY0FBYyxDQUFDO0FBQ3ZDLE9BQU8sRUFBRSxjQUFjLEVBQXFCLE1BQU0sY0FBYyxDQUFDO0FBR2pFLE1BQU0sTUFBTSxHQUFHLEdBQUcsQ0FBQyxNQUFNLENBQUMsY0FBYyxDQUFDLENBQUM7QUFFMUMsMkVBQTJFO0FBQzNFLE1BQU0sV0FBVyxHQUFHLGlCQUFpQixDQUFDO0FBRXRDLGlGQUFpRjtBQUNqRixTQUFTLFVBQVUsQ0FBQyxJQUF3QjtJQUMxQyxJQUFJLENBQUMsSUFBSTtRQUFFLE9BQU8sS0FBSyxDQUFDO0lBQ3hCLDBFQUEwRTtJQUMxRSxNQUFNLENBQUMsR0FBRyxJQUFJLENBQUMsT0FBTyxDQUFDLFVBQVUsRUFBRSxFQUFFLENBQUMsQ0FBQztJQUN2QyxPQUFPLENBQUMsS0FBSyxXQUFXLElBQUksQ0FBQyxDQUFDLFVBQVUsQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLEtBQUssS0FBSyxJQUFJLENBQUMsS0FBSyxXQUFXLENBQUM7QUFDdkYsQ0FBQztBQUVELHNFQUFzRTtBQUN0RSxTQUFTLFFBQVEsQ0FBQyxHQUFvQjtJQUNwQyxPQUFPLElBQUksT0FBTyxDQUFDLENBQUMsT0FBTyxFQUFFLEVBQUU7UUFDN0IsSUFBSSxJQUFJLEdBQUcsRUFBRSxDQUFDO1FBQ2QsR0FBRyxDQUFDLEVBQUUsQ0FBQyxNQUFNLEVBQUUsQ0FBQyxLQUFLLEVBQUUsRUFBRSxDQUFDLENBQUMsSUFBSSxJQUFJLEtBQUssQ0FBQyxDQUFDLENBQUM7UUFDM0MsR0FBRyxDQUFDLEVBQUUsQ0FBQyxLQUFLLEVBQUUsR0FBRyxFQUFFLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUM7UUFDbkMsR0FBRyxDQUFDLEVBQUUsQ0FBQyxPQUFPLEVBQUUsR0FBRyxFQUFFLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUM7SUFDdkMsQ0FBQyxDQUFDLENBQUM7QUFDTCxDQUFDO0FBRUQsU0FBUyxRQUFRLENBQUMsR0FBbUIsRUFBRSxNQUFjLEVBQUUsSUFBYSxFQUFFLFNBQWtCO0lBQ3RGLE1BQU0sT0FBTyxHQUEyQixFQUFFLGNBQWMsRUFBRSxrQkFBa0IsRUFBRSxDQUFDO0lBQy9FLElBQUksU0FBUztRQUFFLE9BQU8sQ0FBQyxZQUFZLENBQUMsR0FBRyxTQUFTLENBQUM7SUFDakQsR0FBRyxDQUFDLFNBQVMsQ0FBQyxNQUFNLEVBQUUsT0FBTyxDQUFDLENBQUM7SUFDL0IsR0FBRyxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUM7QUFDaEMsQ0FBQztBQXVCRCwyREFBMkQ7QUFDM0QsTUFBTSxVQUFVLFVBQVUsQ0FBQyxFQUN6QixVQUFVLEVBQ1YsT0FBTyxFQUNQLElBQUksRUFDSixjQUFjLEdBQ0Q7SUFDYixNQUFNLEtBQUssR0FBRyxXQUFXLENBQUMsaUJBQWlCLENBQUM7UUFDMUMsTUFBTSxFQUFFLEVBQUUsSUFBSSxFQUFFLFdBQVcsRUFBRSxJQUFJLEVBQUUsT0FBTyxFQUFFO1FBQzVDLEVBQUUsRUFBRSxJQUFJO1FBQ1IsSUFBSSxFQUFFLElBQUk7S0FDWCxDQUFDLENBQUM7SUFDSCxLQUFLLENBQUMsRUFBRSxDQUFDLE9BQU8sRUFBRSxDQUFDLEdBQVUsRUFBRSxJQUFhLEVBQUUsR0FBWSxFQUFFLEVBQUU7UUFDNUQsTUFBTSxDQUFDLElBQUksQ0FBQyxzQkFBc0IsRUFBRSxFQUFFLEtBQUssRUFBRSxHQUFHLENBQUMsT0FBTyxFQUFFLENBQUMsQ0FBQztRQUM1RCxNQUFNLENBQUMsR0FBRyxHQUFpQyxDQUFDO1FBQzVDLElBQUksQ0FBQyxJQUFJLFdBQVcsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsV0FBVyxFQUFFLENBQUM7WUFDNUMsUUFBUSxDQUFDLENBQUMsRUFBRSxHQUFHLEVBQUUsRUFBRSxLQUFLLEVBQUUsc0JBQXNCLEVBQUUsQ0FBQyxDQUFDO1FBQ3RELENBQUM7SUFDSCxDQUFDLENBQUMsQ0FBQztJQUVILCtFQUErRTtJQUMvRSxNQUFNLFlBQVksR0FBRyxjQUFjLENBQUMsY0FBYyxDQUFDLENBQUM7SUFDcEQsTUFBTSxDQUFDLEtBQUssQ0FBQyx1QkFBdUIsRUFBRSxFQUFFLE9BQU8sRUFBRSxZQUFZLENBQUMsUUFBUSxFQUFFLENBQUMsQ0FBQztJQUUxRSx1RUFBdUU7SUFDdkUsTUFBTSxhQUFhLEdBQUcsQ0FBQyxLQUFhLEVBQUUsYUFBcUIsRUFBVSxFQUFFLENBQ3JFO1FBQ0UsR0FBRyxtQkFBbUIsSUFBSSxLQUFLLEVBQUU7UUFDakMsUUFBUTtRQUNSLFVBQVU7UUFDVixjQUFjO1FBQ2QsV0FBVyxhQUFhLEVBQUU7UUFDMUIsT0FBTyxDQUFDLEdBQUcsQ0FBQyxRQUFRLEtBQUssWUFBWSxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLEVBQUU7S0FDdEQ7U0FDRSxNQUFNLENBQUMsT0FBTyxDQUFDO1NBQ2YsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDO0lBRWhCOzs7Ozs7Ozs7OztPQVdHO0lBQ0gsTUFBTSxRQUFRLEdBQUcsQ0FBQyxHQUFvQixFQUFVLEVBQUU7UUFDaEQsTUFBTSxTQUFTLEdBQUcsR0FBRyxDQUFDLE9BQU8sQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO1FBQ2pELE1BQU0sS0FBSyxHQUFHLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxTQUFTLElBQUksRUFBRSxDQUFDLENBQUM7YUFDL0UsS0FBSyxDQUFDLEdBQUcsQ0FBQzthQUNWLEdBQUcsQ0FBQyxDQUFDLEtBQUssRUFBRSxFQUFFLENBQUMsS0FBSyxDQUFDLElBQUksRUFBRSxDQUFDO2FBQzVCLE1BQU0sQ0FBQyxPQUFPLENBQUMsQ0FBQztRQUNuQixPQUFPLEtBQUssQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDLENBQUMsSUFBSSxHQUFHLENBQUMsTUFBTSxDQUFDLGFBQWEsSUFBSSxTQUFTLENBQUM7SUFDL0QsQ0FBQyxDQUFDO0lBRUY7Ozs7T0FJRztJQUNILE1BQU0sa0JBQWtCLEdBQUcsQ0FBQyxHQUFvQixFQUFRLEVBQUU7UUFDeEQsTUFBTSxHQUFHLEdBQUcsR0FBRyxDQUFDLE9BQU8sQ0FBQyxNQUFNLENBQUM7UUFDL0IsSUFBSSxDQUFDLEdBQUc7WUFBRSxPQUFPO1FBQ2pCLE1BQU0sSUFBSSxHQUFHLEdBQUc7YUFDYixLQUFLLENBQUMsR0FBRyxDQUFDO2FBQ1YsR0FBRyxDQUFDLENBQUMsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFDLENBQUMsSUFBSSxFQUFFLENBQUM7YUFDcEIsTUFBTSxDQUFDLENBQUMsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsVUFBVSxDQUFDLEdBQUcsbUJBQW1CLEdBQUcsQ0FBQyxDQUFDLENBQUM7UUFDaEUsSUFBSSxJQUFJLENBQUMsTUFBTTtZQUFFLEdBQUcsQ0FBQyxPQUFPLENBQUMsTUFBTSxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7O1lBQ2pELE9BQU8sR0FBRyxDQUFDLE9BQU8sQ0FBQyxNQUFNLENBQUM7SUFDakMsQ0FBQyxDQUFDO0lBRUY7Ozs7Ozs7Ozs7Ozs7T0FhRztJQUNILE1BQU0sY0FBYyxHQUFHLENBQUMsR0FBb0IsRUFBRSxLQUFhLEVBQVEsRUFBRTtRQUNuRSxHQUFHLENBQUMsT0FBTyxDQUFDLEtBQUssQ0FBQyxjQUFjLENBQUMsR0FBRyxLQUFLLENBQUM7UUFDMUMsR0FBRyxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUMsaUJBQWlCLENBQUMsR0FBRyxLQUFLLENBQUM7SUFDL0MsQ0FBQyxDQUFDO0lBRUY7Ozs7Ozs7Ozs7O09BV0c7SUFDSCxNQUFNLGlCQUFpQixHQUFHLENBQUMsR0FBb0IsRUFBUSxFQUFFO1FBQ3ZELE1BQU0sT0FBTyxHQUFHLFlBQVksQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLE9BQWtDLENBQUMsQ0FBQztRQUMzRSxJQUFJLE9BQU8sQ0FBQyxNQUFNO1lBQUUsTUFBTSxDQUFDLEtBQUssQ0FBQywwQkFBMEIsRUFBRSxFQUFFLE9BQU8sRUFBRSxDQUFDLENBQUM7SUFDNUUsQ0FBQyxDQUFDO0lBRUYsTUFBTSxNQUFNLEdBQUcsWUFBWSxDQUFDLEtBQUssRUFBRSxHQUFHLEVBQUUsR0FBRyxFQUFFLEVBQUU7UUFDN0MsTUFBTSxJQUFJLEdBQUcsQ0FBQyxHQUFHLENBQUMsR0FBRyxJQUFJLEdBQUcsQ0FBQyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUUsQ0FBQztRQUU3QyxvRUFBb0U7UUFDcEUsMkRBQTJEO1FBQzNELElBQUksQ0FBQyxJQUFJLElBQUksQ0FBQyxVQUFVLENBQUMsR0FBRyxDQUFDLE1BQU0sQ0FBQyxhQUFhLENBQUMsRUFBRSxDQUFDO1lBQ25ELEtBQUssQ0FBQyxHQUFHLENBQUMsR0FBRyxFQUFFLEdBQUcsQ0FBQyxDQUFDO1lBQ3BCLE9BQU87UUFDVCxDQUFDO1FBRUQsMENBQTBDO1FBRTFDLCtFQUErRTtRQUMvRSxJQUFJLElBQUksS0FBSyxHQUFHLFdBQVcsU0FBUyxFQUFFLENBQUM7WUFDckMsTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLFlBQVksQ0FBQyxHQUFHLENBQUMsT0FBTyxDQUFDLE1BQU0sSUFBSSxJQUFJLENBQUMsQ0FBQyxtQkFBbUIsQ0FBQyxDQUFDO1lBQ2pGLFFBQVEsQ0FBQyxHQUFHLEVBQUUsR0FBRyxFQUFFLE1BQU0sSUFBSSxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDO1lBQzdDLE9BQU87UUFDVCxDQUFDO1FBQ0QsSUFBSSxJQUFJLEtBQUssR0FBRyxXQUFXLFVBQVUsSUFBSSxHQUFHLENBQUMsTUFBTSxLQUFLLE1BQU0sRUFBRSxDQUFDO1lBQy9ELE1BQU0sTUFBTSxHQUFHLGlCQUFpQixDQUFDLFNBQVMsQ0FBQyxJQUFJLENBQUMsV0FBVyxDQUFDLE1BQU0sUUFBUSxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsQ0FBQztZQUNsRixJQUFJLENBQUMsTUFBTSxDQUFDLE9BQU87Z0JBQUUsT0FBTyxRQUFRLENBQUMsR0FBRyxFQUFFLEdBQUcsRUFBRSxFQUFFLEVBQUUsRUFBRSxJQUFJLEVBQUUsQ0FBQyxDQUFDLENBQUMsbUJBQW1CO1lBQ2pGLE9BQU8sUUFBUSxDQUFDLEdBQUcsRUFBRSxHQUFHLEVBQUUsTUFBTSxJQUFJLENBQUMsT0FBTyxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsS0FBSyxFQUFFLFFBQVEsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUM7UUFDbEYsQ0FBQztRQUNELElBQUksSUFBSSxLQUFLLEdBQUcsV0FBVyxTQUFTLElBQUksR0FBRyxDQUFDLE1BQU0sS0FBSyxNQUFNLEVBQUUsQ0FBQztZQUM5RCxNQUFNLE1BQU0sR0FBRyxnQkFBZ0IsQ0FBQyxTQUFTLENBQUMsSUFBSSxDQUFDLFdBQVcsQ0FBQyxNQUFNLFFBQVEsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUM7WUFDakYsSUFBSSxDQUFDLE1BQU0sQ0FBQyxPQUFPO2dCQUFFLE9BQU8sUUFBUSxDQUFDLEdBQUcsRUFBRSxHQUFHLEVBQUUsRUFBRSxFQUFFLEVBQUUsS0FBSyxFQUFFLENBQUMsQ0FBQztZQUM5RCxNQUFNLE1BQU0sR0FBRyxNQUFNLElBQUksQ0FBQyxNQUFNLENBQUMsTUFBTSxDQUFDLElBQUksQ0FBQyxLQUFLLEVBQUUsTUFBTSxDQUFDLElBQUksQ0FBQyxJQUFJLEVBQUUsUUFBUSxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUM7WUFDckYsTUFBTSxNQUFNLEdBQ1YsTUFBTSxDQUFDLEVBQUUsSUFBSSxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxhQUFhLENBQUMsTUFBTSxDQUFDLEtBQUssRUFBRSxJQUFJLENBQUMsaUJBQWlCLENBQUMsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDO1lBQzlGLE9BQU8sUUFBUSxDQUNiLEdBQUcsRUFDSCxHQUFHLEVBQ0gsRUFBRSxFQUFFLEVBQUUsTUFBTSxDQUFDLEVBQUUsRUFBRSxHQUFHLENBQUMsTUFBTSxDQUFDLFVBQVUsQ0FBQyxDQUFDLENBQUMsRUFBRSxVQUFVLEVBQUUsTUFBTSxDQUFDLFVBQVUsRUFBRSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsRUFBRSxFQUNsRixNQUFNLENBQ1AsQ0FBQztRQUNKLENBQUM7UUFDRCxJQUFJLElBQUksS0FBSyxHQUFHLFdBQVcsU0FBUyxJQUFJLEdBQUcsQ0FBQyxNQUFNLEtBQUssTUFBTSxFQUFFLENBQUM7WUFDOUQsT0FBTyxRQUFRLENBQ2IsR0FBRyxFQUNILEdBQUcsRUFDSCxFQUFFLEVBQUUsRUFBRSxJQUFJLEVBQUUsRUFDWixHQUFHLG1CQUFtQixnQ0FBZ0MsQ0FDdkQsQ0FBQztRQUNKLENBQUM7UUFFRCw4RUFBOEU7UUFDOUUseUVBQXlFO1FBQ3pFLGlCQUFpQixDQUFDLEdBQUcsQ0FBQyxDQUFDO1FBRXZCLDhFQUE4RTtRQUM5RSwwREFBMEQ7UUFDMUQsSUFBSSxDQUFDLElBQUksQ0FBQyxVQUFVLENBQUMsT0FBTyxDQUFDLEVBQUUsQ0FBQztZQUM5QixrQkFBa0IsQ0FBQyxHQUFHLENBQUMsQ0FBQztZQUN4QixLQUFLLENBQUMsR0FBRyxDQUFDLEdBQUcsRUFBRSxHQUFHLENBQUMsQ0FBQztZQUNwQixPQUFPO1FBQ1QsQ0FBQztRQUVELCtDQUErQztRQUMvQyxNQUFNLEtBQUssR0FBRyxJQUFJLENBQUMsWUFBWSxDQUFDLEdBQUcsQ0FBQyxPQUFPLENBQUMsTUFBTSxJQUFJLElBQUksQ0FBQyxDQUFDLG1CQUFtQixDQUFDLENBQUM7UUFDakYsTUFBTSxLQUFLLEdBQUcsTUFBTSxJQUFJLENBQUMsT0FBTyxDQUFDLEtBQUssQ0FBQyxDQUFDO1FBQ3hDLElBQUksS0FBSyxFQUFFLENBQUM7WUFDVixxRUFBcUU7WUFDckUsNkRBQTZEO1lBQzdELGNBQWMsQ0FBQyxHQUFHLEVBQUUsS0FBSyxDQUFDLENBQUM7WUFDM0Isa0JBQWtCLENBQUMsR0FBRyxDQUFDLENBQUM7WUFDeEIsS0FBSyxDQUFDLEdBQUcsQ0FBQyxHQUFHLEVBQUUsR0FBRyxDQUFDLENBQUM7UUFDdEIsQ0FBQzthQUFNLENBQUM7WUFDTixRQUFRLENBQUMsR0FBRyxFQUFFLEdBQUcsRUFBRSxFQUFFLEtBQUssRUFBRSx5QkFBeUIsRUFBRSxTQUFTLEVBQUUsV0FBVyxFQUFFLENBQUMsQ0FBQztRQUNuRixDQUFDO0lBQ0gsQ0FBQyxDQUFDLENBQUM7SUFFSCwwRUFBMEU7SUFDMUUsOEVBQThFO0lBQzlFLE1BQU0sQ0FBQyxFQUFFLENBQUMsU0FBUyxFQUFFLENBQUMsR0FBb0IsRUFBRSxNQUFjLEVBQUUsSUFBWSxFQUFFLEVBQUU7UUFDMUUsSUFBSSxDQUFDLElBQUksSUFBSSxDQUFDLFVBQVUsQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDLGFBQWEsQ0FBQyxFQUFFLENBQUM7WUFDbkQsS0FBSyxDQUFDLEVBQUUsQ0FBQyxHQUFHLEVBQUUsTUFBTSxFQUFFLElBQUksQ0FBQyxDQUFDO1lBQzVCLE9BQU87UUFDVCxDQUFDO1FBQ0QsaUJBQWlCLENBQUMsR0FBRyxDQUFDLENBQUM7UUFDdkIsSUFBSSxDQUFDLENBQUMsR0FBRyxDQUFDLEdBQUcsSUFBSSxFQUFFLENBQUMsQ0FBQyxVQUFVLENBQUMsT0FBTyxDQUFDLEVBQUUsQ0FBQztZQUN6QyxrQkFBa0IsQ0FBQyxHQUFHLENBQUMsQ0FBQztZQUN4QixLQUFLLENBQUMsRUFBRSxDQUFDLEdBQUcsRUFBRSxNQUFNLEVBQUUsSUFBSSxDQUFDLENBQUM7WUFDNUIsT0FBTztRQUNULENBQUM7UUFDRCxNQUFNLEtBQUssR0FBRyxJQUFJLENBQUMsWUFBWSxDQUFDLEdBQUcsQ0FBQyxPQUFPLENBQUMsTUFBTSxJQUFJLElBQUksQ0FBQyxDQUFDLG1CQUFtQixDQUFDLENBQUM7UUFDakYsS0FBSyxJQUFJLENBQUMsT0FBTyxDQUFDLEtBQUssQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDLEtBQUssRUFBRSxFQUFFO1lBQ3RDLElBQUksQ0FBQyxLQUFLLEVBQUUsQ0FBQztnQkFDWCxNQUFNLENBQUMsT0FBTyxFQUFFLENBQUM7Z0JBQ2pCLE9BQU87WUFDVCxDQUFDO1lBQ0QsY0FBYyxDQUFDLEdBQUcsRUFBRSxLQUFLLENBQUMsQ0FBQztZQUMzQixrQkFBa0IsQ0FBQyxHQUFHLENBQUMsQ0FBQztZQUN4QixLQUFLLENBQUMsRUFBRSxDQUFDLEdBQUcsRUFBRSxNQUFNLEVBQUUsSUFBSSxDQUFDLENBQUM7UUFDOUIsQ0FBQyxDQUFDLENBQUM7SUFDTCxDQUFDLENBQUMsQ0FBQztJQUVILE9BQU8sSUFBSSxPQUFPLENBQUMsQ0FBQyxPQUFPLEVBQUUsRUFBRTtRQUM3QixNQUFNLENBQUMsTUFBTSxDQUFDLFVBQVUsRUFBRSxTQUFTLEVBQUUsR0FBRyxFQUFFO1lBQ3hDLE1BQU0sQ0FBQyxJQUFJLENBQUMseUJBQXlCLFVBQVUscUJBQXFCLE9BQU8sRUFBRSxDQUFDLENBQUM7WUFDL0UsT0FBTyxFQUFFLENBQUM7UUFDWixDQUFDLENBQUMsQ0FBQztJQUNMLENBQUMsQ0FBQyxDQUFDO0FBQ0wsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogVGhlIHR1bm5lbCBnYXRlIHJldmVyc2UtcHJveHkuXG4gKlxuICogQmluZHMgdGhlIFBVQkxJQyBwb3J0ICh0aGUgY29udGFpbmVyJ3MgYERBVEFCUklDS1NfQVBQX1BPUlRgKSBhbmQgZm9yd2FyZHMgdG9cbiAqIHRoZSByZWFsIGFwcCBvbiBhIHByaXZhdGUgbG9vcGJhY2sgcG9ydC4gSXQgaXMgdGhlIHNpbmdsZSBmcm9udCBkb29yIGZvciBib3RoXG4gKiB0cmFmZmljIHBhdGhzIGludG8gdGhlIGNvbnRhaW5lcjpcbiAqXG4gKiAgIC0gKipwb3J0ciBjbGllbnQqKiAoc2FtZSBjb250YWluZXIsIGNvbm5lY3RzIG92ZXIgTE9PUEJBQ0spIC0gdGhlIHB1YmxpY1xuICogICAgIHR1bm5lbC4gVGhpcyBpcyB3aGF0IHRoZSBnYXRlIHByb3RlY3RzLlxuICogICAtICoqdGhlIGhvc3RpbmcgcGxhdGZvcm0ncyBmcm9udCBkb29yKiogKERhdGFicmlja3MgQXBwcycgY29udHJvbCBwbGFuZSwgb3JcbiAqICAgICBhbnkgb3RoZXIgaG9zdCByZWFjaGluZyB0aGUgYDAuMC4wLjBgIHBvcnQgZnJvbSBhIE5PTi1sb29wYmFja1xuICogICAgIGNvbnRhaW5lci1uZXR3b3JrIGFkZHJlc3MpIC0gcGFzc2VkIHRocm91Z2ggVU5HQVRFRCwgcGVyIHRoZSBydWxlIFwiaWYgaXQnc1xuICogICAgIG5vdCBmcm9tIHRoZSBwb3J0ciBjbGllbnQsIGxldCBpdCBpblwiLiBUaGUgcGxhdGZvcm0gYWxyZWFkeSBhdXRoZW50aWNhdGVzXG4gKiAgICAgdGhhdCBwYXRoOyB0aGUgZ2F0ZSBleGlzdHMgZm9yIHRoZSB0dW5uZWwsIHdoaWNoIG5vdGhpbmcgZWxzZSBwcm90ZWN0cy5cbiAqXG4gKiBUaGUgZGlzdGluZ3Vpc2hlciBpcyB0aGUgY29ubmVjdGlvbidzIHNvdXJjZSBhZGRyZXNzOiBhIGxvb3BiYWNrXG4gKiBgcmVxLnNvY2tldC5yZW1vdGVBZGRyZXNzYCBpcyB0aGUgcG9ydHIgY2xpZW50OyBhbnl0aGluZyBlbHNlIGlzIHRoZSBwbGF0Zm9ybS5cbiAqXG4gKiBGb3IgcG9ydHIgdHJhZmZpYyB0aGUgZ2F0ZSBpcyBhIHN0YW5kYXJkIFNQQSBnYXRlOiBzdGF0aWMgYXNzZXRzICsgdGhlIGxvZ2luXG4gKiBmbG93IChgL2FwaS9lbWFpbC9hdXRoLypgLCBhbnN3ZXJlZCBJTi1QUk9DRVNTIGJ5IHRoaXMgcHJveHkgdmlhIHRoZVxuICoge0BsaW5rIEF1dGhHYXRlQXBpfSkgYXJlIG9wZW4gc28gdGhlIGJyb3dzZXIgY2FuIGxvYWQgdGhlIGNsaWVudCBhbmQgcmVuZGVyIHRoZVxuICogYDxBdXRoR2F0ZT5gOyBldmVyeSBvdGhlciBgL2FwaS8qYCBuZWVkcyBhIHZhbGlkIHNlc3Npb24gY29va2llIG9yIGdldHMgNDAxLlxuICogV2ViU29ja2V0IHVwZ3JhZGVzIGFyZSBnYXRlZCB0aGUgc2FtZSB3YXkgYW5kIGZvcndhcmRlZCB3aXRoIGBodHRwLXByb3h5LTNgLlxuICpcbiAqIEBtb2R1bGVcbiAqL1xuXG5pbXBvcnQgeyBjcmVhdGVTZXJ2ZXIsIHR5cGUgSW5jb21pbmdNZXNzYWdlLCB0eXBlIFNlcnZlclJlc3BvbnNlIH0gZnJvbSBcIm5vZGU6aHR0cFwiO1xuaW1wb3J0IHR5cGUgeyBTb2NrZXQgfSBmcm9tIFwibm9kZTpuZXRcIjtcbmltcG9ydCB7IGh0dHAsIGpzb24sIGxvZywgdG9rZW4gfSBmcm9tIFwiQGRieC10b29scy9zaGFyZWQtY29yZVwiO1xuaW1wb3J0IHsgYXV0aFJlcXVlc3RTY2hlbWEsIGF1dGhWZXJpZnlTY2hlbWEsIFNFU1NJT05fQ09PS0lFX05BTUUgfSBmcm9tIFwiQGRieC10b29scy9zaGFyZWQtZW1haWxcIjtcbmltcG9ydCBQcm94eU1vZHVsZSBmcm9tIFwiaHR0cC1wcm94eS0zXCI7XG5pbXBvcnQgeyB0b0hlYWRlclBvbGljeSwgdHlwZSBIZWFkZXJQb2xpY3kgfSBmcm9tIFwiLi9oZWFkZXJzLnRzXCI7XG5pbXBvcnQgdHlwZSB7IEF1dGhHYXRlQXBpIH0gZnJvbSBcIi4vcGx1Z2luLnRzXCI7XG5cbmNvbnN0IGxvZ2dlciA9IGxvZy5sb2dnZXIoXCJ0dW5uZWw6cHJveHlcIik7XG5cbi8qKiBSb3V0ZSBwcmVmaXggdGhlIGxvZ2luIGZsb3cgbGl2ZXMgdW5kZXIgKG9wZW4sIGFuc3dlcmVkIGluLXByb2Nlc3MpLiAqL1xuY29uc3QgQVVUSF9QUkVGSVggPSBcIi9hcGkvZW1haWwvYXV0aFwiO1xuXG4vKiogVHJ1ZSB3aGVuIGEgc29ja2V0IGFkZHJlc3MgaXMgbG9vcGJhY2sgKHRoZSBwb3J0ciBjbGllbnQsIHNhbWUgY29udGFpbmVyKS4gKi9cbmZ1bmN0aW9uIGlzTG9vcGJhY2soYWRkcjogc3RyaW5nIHwgdW5kZWZpbmVkKTogYm9vbGVhbiB7XG4gIGlmICghYWRkcikgcmV0dXJuIGZhbHNlO1xuICAvLyBOb3JtYWxpemUgSVB2NC1tYXBwZWQgSVB2NiAoYDo6ZmZmZjoxMjcuMC4wLjFgKSBhbmQgYmFyZSBJUHY2IGxvb3BiYWNrLlxuICBjb25zdCBhID0gYWRkci5yZXBsYWNlKC9eOjpmZmZmOi8sIFwiXCIpO1xuICByZXR1cm4gYSA9PT0gXCIxMjcuMC4wLjFcIiB8fCBhLnN0YXJ0c1dpdGgoXCIxMjcuXCIpIHx8IGEgPT09IFwiOjoxXCIgfHwgYSA9PT0gXCJsb2NhbGhvc3RcIjtcbn1cblxuLyoqIFJlYWQgdGhlIHdob2xlIHJlcXVlc3QgYm9keSBhcyB0ZXh0IChzbWFsbCBKU09OIHBheWxvYWRzIG9ubHkpLiAqL1xuZnVuY3Rpb24gcmVhZEJvZHkocmVxOiBJbmNvbWluZ01lc3NhZ2UpOiBQcm9taXNlPHN0cmluZz4ge1xuICByZXR1cm4gbmV3IFByb21pc2UoKHJlc29sdmUpID0+IHtcbiAgICBsZXQgZGF0YSA9IFwiXCI7XG4gICAgcmVxLm9uKFwiZGF0YVwiLCAoY2h1bmspID0+IChkYXRhICs9IGNodW5rKSk7XG4gICAgcmVxLm9uKFwiZW5kXCIsICgpID0+IHJlc29sdmUoZGF0YSkpO1xuICAgIHJlcS5vbihcImVycm9yXCIsICgpID0+IHJlc29sdmUoZGF0YSkpO1xuICB9KTtcbn1cblxuZnVuY3Rpb24gc2VuZEpzb24ocmVzOiBTZXJ2ZXJSZXNwb25zZSwgc3RhdHVzOiBudW1iZXIsIGJvZHk6IHVua25vd24sIHNldENvb2tpZT86IHN0cmluZyk6IHZvaWQge1xuICBjb25zdCBoZWFkZXJzOiBSZWNvcmQ8c3RyaW5nLCBzdHJpbmc+ID0geyBcImNvbnRlbnQtdHlwZVwiOiBcImFwcGxpY2F0aW9uL2pzb25cIiB9O1xuICBpZiAoc2V0Q29va2llKSBoZWFkZXJzW1wic2V0LWNvb2tpZVwiXSA9IHNldENvb2tpZTtcbiAgcmVzLndyaXRlSGVhZChzdGF0dXMsIGhlYWRlcnMpO1xuICByZXMuZW5kKEpTT04uc3RyaW5naWZ5KGJvZHkpKTtcbn1cblxuLyoqIE9wdGlvbnMgZm9yIHtAbGluayBzdGFydFByb3h5fS4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgUHJveHlPcHRpb25zIHtcbiAgLyoqIFB1YmxpYyBwb3J0IHRvIGxpc3RlbiBvbiAodGhlIGNvbnRhaW5lcidzIERBVEFCUklDS1NfQVBQX1BPUlQpLiAqL1xuICBwdWJsaWNQb3J0OiBudW1iZXI7XG4gIC8qKiBQcml2YXRlIHBvcnQgdGhlIHJlYWwgYXBwIGxpc3RlbnMgb24gKGxvb3BiYWNrKS4gKi9cbiAgYXBwUG9ydDogbnVtYmVyO1xuICAvKipcbiAgICogVGhlIGluLXByb2Nlc3MgZ2F0ZSBBUEksIG9yIGB1bmRlZmluZWRgIHRvIHJ1biBPUEVOIChpbnNlY3VyZSBtb2RlKTogZXZlcnlcbiAgICogcmVxdWVzdCAtIGluY2x1ZGluZyBwb3J0ciB0cmFmZmljIC0gaXMgZm9yd2FyZGVkIHVuZ2F0ZWQuIFVzZWQgd2hlbiB0aGVcbiAgICogb3BlcmF0b3IgcGFzc2VkIGAtLWluc2VjdXJlYCAvIGBUVU5ORUxfSU5TRUNVUkU9dHJ1ZWAuXG4gICAqL1xuICBnYXRlPzogQXV0aEdhdGVBcGk7XG4gIC8qKlxuICAgKiBFeHRyYSBgeC1gIHJlcXVlc3QgaGVhZGVycyB0dW5uZWwgdHJhZmZpYyBtYXkgZm9yd2FyZCwgYXMgbGl0ZXJhbHMsIGdsb2JzLCBvclxuICAgKiBgL3JlZ2V4L2BlcyAtIHVuaW9uZWQgd2l0aCB7QGxpbmsgREVGQVVMVF9GT1JXQVJEX0hFQURFUlN9LiBFdmVyeSBvdGhlciBgeC1gXG4gICAqIGhlYWRlciBpcyBzdHJpcHBlZCwgYW5kIHtAbGluayBQUk9URUNURURfSEVBREVSU30gaXMgc3RyaXBwZWQgcmVnYXJkbGVzcy4gU2VlXG4gICAqIGAuL2hlYWRlcnMudHNgIGZvciB3aHkgdGhlIHBvbGljeSBpcyBhbiBhbGxvdy1saXN0LlxuICAgKi9cbiAgZm9yd2FyZEhlYWRlcnM/OiByZWFkb25seSBzdHJpbmdbXTtcbn1cblxuLyoqIFN0YXJ0IHRoZSBnYXRlIHByb3h5LiBSZXNvbHZlcyBvbmNlIGl0IGlzIGxpc3RlbmluZy4gKi9cbmV4cG9ydCBmdW5jdGlvbiBzdGFydFByb3h5KHtcbiAgcHVibGljUG9ydCxcbiAgYXBwUG9ydCxcbiAgZ2F0ZSxcbiAgZm9yd2FyZEhlYWRlcnMsXG59OiBQcm94eU9wdGlvbnMpOiBQcm9taXNlPHZvaWQ+IHtcbiAgY29uc3QgcHJveHkgPSBQcm94eU1vZHVsZS5jcmVhdGVQcm94eVNlcnZlcih7XG4gICAgdGFyZ2V0OiB7IGhvc3Q6IFwiMTI3LjAuMC4xXCIsIHBvcnQ6IGFwcFBvcnQgfSxcbiAgICB3czogdHJ1ZSxcbiAgICB4ZndkOiB0cnVlLFxuICB9KTtcbiAgcHJveHkub24oXCJlcnJvclwiLCAoZXJyOiBFcnJvciwgX3JlcTogdW5rbm93biwgcmVzOiB1bmtub3duKSA9PiB7XG4gICAgbG9nZ2VyLndhcm4oXCJ1cHN0cmVhbSBwcm94eSBlcnJvclwiLCB7IGVycm9yOiBlcnIubWVzc2FnZSB9KTtcbiAgICBjb25zdCByID0gcmVzIGFzIFNlcnZlclJlc3BvbnNlIHwgdW5kZWZpbmVkO1xuICAgIGlmIChyICYmIFwid3JpdGVIZWFkXCIgaW4gciAmJiAhci5oZWFkZXJzU2VudCkge1xuICAgICAgc2VuZEpzb24ociwgNTAyLCB7IGVycm9yOiBcInVwc3RyZWFtIHVuYXZhaWxhYmxlXCIgfSk7XG4gICAgfVxuICB9KTtcblxuICAvLyBDb21waWxlZCBvbmNlOiB0aGUgaW5ib3VuZC1oZWFkZXIgYWxsb3ctbGlzdCBhcHBsaWVkIHRvIGV2ZXJ5IGdhdGVkIHJlcXVlc3QuXG4gIGNvbnN0IGhlYWRlclBvbGljeSA9IHRvSGVhZGVyUG9saWN5KGZvcndhcmRIZWFkZXJzKTtcbiAgbG9nZ2VyLmRlYnVnKFwiaW5ib3VuZCBoZWFkZXIgcG9saWN5XCIsIHsgZm9yd2FyZDogaGVhZGVyUG9saWN5LnBhdHRlcm5zIH0pO1xuXG4gIC8qKiBUaGUgc2Vzc2lvbiBjb29raWUgZm9yIGEgdmVyaWZpZWQgZW1haWwsIGFzIGEgU2V0LUNvb2tpZSBzdHJpbmcuICovXG4gIGNvbnN0IHNlc3Npb25Db29raWUgPSAodG9rZW46IHN0cmluZywgbWF4QWdlU2Vjb25kczogbnVtYmVyKTogc3RyaW5nID0+XG4gICAgW1xuICAgICAgYCR7U0VTU0lPTl9DT09LSUVfTkFNRX09JHt0b2tlbn1gLFxuICAgICAgXCJQYXRoPS9cIixcbiAgICAgIFwiSHR0cE9ubHlcIixcbiAgICAgIFwiU2FtZVNpdGU9TGF4XCIsXG4gICAgICBgTWF4LUFnZT0ke21heEFnZVNlY29uZHN9YCxcbiAgICAgIHByb2Nlc3MuZW52Lk5PREVfRU5WID09PSBcInByb2R1Y3Rpb25cIiA/IFwiU2VjdXJlXCIgOiBcIlwiLFxuICAgIF1cbiAgICAgIC5maWx0ZXIoQm9vbGVhbilcbiAgICAgIC5qb2luKFwiOyBcIik7XG5cbiAgLyoqXG4gICAqIENsaWVudCBJUCBmb3IgcmF0ZS1saW1pdGluZzogdGhlIHBvcnRyIGNsaWVudCdzIGZvcndhcmRlZCBYRkYsIGVsc2UgdGhlIHNvY2tldC5cbiAgICpcbiAgICogRGVsaWJlcmF0ZWx5IHRoZSBSSUdIVE1PU1QgYHgtZm9yd2FyZGVkLWZvcmAgZW50cnksIG5vdCB0aGUgbGVmdG1vc3QuIFRoZVxuICAgKiBsaXN0IGdyb3dzIGxlZnQtdG8tcmlnaHQgYXMgZWFjaCBob3AgYXBwZW5kcywgc28gdGhlIGxhc3QgZW50cnkgaXMgdGhlIG9uZVxuICAgKiB0aGUgbmVhcmVzdCB0cnVzdGVkIHByb3h5IChwb3J0cikgd3JvdGUgYW5kIGV2ZXJ5IGVhcmxpZXIgZW50cnkgaXMgYSB2YWx1ZVxuICAgKiB0aGUgY2FsbGVyIGNvdWxkIGhhdmUgc2VudC4gUmVhZGluZyB0aGUgbGVmdG1vc3QgbGV0cyBhIGNsaWVudCB2YXJ5IG9uZVxuICAgKiBoZWFkZXIgdG8gZ2V0IGEgZnJlc2ggcmF0ZS1saW1pdCBidWNrZXQgcGVyIHJlcXVlc3QsIHdoaWNoIGRlZmVhdHMgYm90aCB0aGVcbiAgICogcGVyLUlQIGNvZGUtcmVxdWVzdCBhbmQgdmVyaWZ5LWF0dGVtcHQgbGltaXRlcnMuIENhbGxlZCBCRUZPUkVcbiAgICoge0BsaW5rIEhlYWRlclBvbGljeS5hcHBseX0gc3RyaXBzIHRoZSBoZWFkZXIsIHNvIHRoZSBob25lc3QgaG9wIHZhbHVlIGlzXG4gICAqIHN0aWxsIGF2YWlsYWJsZSBoZXJlLlxuICAgKi9cbiAgY29uc3QgY2xpZW50SXAgPSAocmVxOiBJbmNvbWluZ01lc3NhZ2UpOiBzdHJpbmcgPT4ge1xuICAgIGNvbnN0IGZvcndhcmRlZCA9IHJlcS5oZWFkZXJzW1wieC1mb3J3YXJkZWQtZm9yXCJdO1xuICAgIGNvbnN0IGNoYWluID0gKEFycmF5LmlzQXJyYXkoZm9yd2FyZGVkKSA/IGZvcndhcmRlZC5qb2luKFwiLFwiKSA6IChmb3J3YXJkZWQgPz8gXCJcIikpXG4gICAgICAuc3BsaXQoXCIsXCIpXG4gICAgICAubWFwKChlbnRyeSkgPT4gZW50cnkudHJpbSgpKVxuICAgICAgLmZpbHRlcihCb29sZWFuKTtcbiAgICByZXR1cm4gY2hhaW4uYXQoLTEpID8/IHJlcS5zb2NrZXQucmVtb3RlQWRkcmVzcyA/PyBcInVua25vd25cIjtcbiAgfTtcblxuICAvKipcbiAgICogUmVtb3ZlIHRoZSBnYXRlJ3Mgc2Vzc2lvbiBjb29raWUgZnJvbSB0aGUgYENvb2tpZWAgaGVhZGVyIGJlZm9yZSBmb3J3YXJkaW5nLFxuICAgKiBzbyB0aGUgYXBwIG5ldmVyIHNlZXMgYGRieF9hdXRoYCAoaXQgaXMgdGhlIHByb3h5J3MgY29uY2Vybiwgbm90IHRoZSBhcHAncykuXG4gICAqIFByZXNlcnZlcyBhbnkgb3RoZXIgY29va2llcy4gUmVtb3ZlcyB0aGUgaGVhZGVyIGVudGlyZWx5IHdoZW4gaXQgYmVjb21lcyBlbXB0eS5cbiAgICovXG4gIGNvbnN0IHN0cmlwU2Vzc2lvbkNvb2tpZSA9IChyZXE6IEluY29taW5nTWVzc2FnZSk6IHZvaWQgPT4ge1xuICAgIGNvbnN0IHJhdyA9IHJlcS5oZWFkZXJzLmNvb2tpZTtcbiAgICBpZiAoIXJhdykgcmV0dXJuO1xuICAgIGNvbnN0IGtlcHQgPSByYXdcbiAgICAgIC5zcGxpdChcIjtcIilcbiAgICAgIC5tYXAoKGMpID0+IGMudHJpbSgpKVxuICAgICAgLmZpbHRlcigoYykgPT4gYyAmJiAhYy5zdGFydHNXaXRoKGAke1NFU1NJT05fQ09PS0lFX05BTUV9PWApKTtcbiAgICBpZiAoa2VwdC5sZW5ndGgpIHJlcS5oZWFkZXJzLmNvb2tpZSA9IGtlcHQuam9pbihcIjsgXCIpO1xuICAgIGVsc2UgZGVsZXRlIHJlcS5oZWFkZXJzLmNvb2tpZTtcbiAgfTtcblxuICAvKipcbiAgICogUHJlc2VudCBhbiBPVFAtYXV0aGVudGljYXRlZCBjYWxsZXIgdG8gdGhlIGFwcCB0aGUgU0FNRSB3YXkgYSBwbGF0Zm9ybSBmcm9udFxuICAgKiBkb29yIGRvZXM6IHNldCB0aGUgZnJvbnQgZG9vcidzIG93biBpZGVudGl0eSBoZWFkZXJzIHRvIHRoZSB2ZXJpZmllZCBhZGRyZXNzXG4gICAqIChBcHBLaXQgcmVhZHMge0BsaW5rIHRva2VuLlVTRVJfSURfSEVBREVSfSBmb3IgdGhlIE9CTyB1c2VyIGlkKSwgc28gdGhlIGFwcFxuICAgKiBuZWVkcyBubyBnYXRlLXNwZWNpZmljIGNvZGUgcGF0aC4gVGhlIG5hbWVzIGNvbWUgZnJvbVxuICAgKiBgQGRieC10b29scy9zaGFyZWQtY29yZWAncyBgdG9rZW5gIG1vZHVsZSAtIHRoZSBzYW1lIGNvbnN0YW50cyBBcHBLaXQtc2lkZVxuICAgKiBjb2RlIHJlYWRzIHRoZW0gYnkgLSByYXRoZXIgdGhhbiBiZWluZyByZS1zcGVsbGVkIGhlcmUuXG4gICAqXG4gICAqIFdoYXQgdGhlIGdhdGUgQ0FOTk9UIHNldCBpcyB7QGxpbmsgdG9rZW4uQUNDRVNTX1RPS0VOX0hFQURFUn06IGFuIE9UUCBzZXNzaW9uXG4gICAqIHByb3ZlcyBhbiBlbWFpbCBhZGRyZXNzLCBub3QgcG9zc2Vzc2lvbiBvZiBhIERhdGFicmlja3MgY3JlZGVudGlhbCwgYW5kXG4gICAqIHRoZXJlIGlzIG5vIHdheSB0byBtaW50IG9uZSBmb3IgdGhlIGNhbGxlci4gSXRzIGFic2VuY2UgaXMgZXhhY3RseSB3aGF0XG4gICAqIGBAZGJ4LXRvb2xzL2FwcGtpdGAncyBgaWRlbnRpdHlgIGBcImF1dG9cImAgbW9kZSBkZXRlY3RzLCBzbyBhIGdhdGVkIHJlcXVlc3RcbiAgICogcnVucyBhcyB0aGUgYXBwJ3Mgc2VydmljZSBwcmluY2lwYWwgaW5zdGVhZCBvZiB0aHJvd2luZy5cbiAgICovXG4gIGNvbnN0IGluamVjdElkZW50aXR5ID0gKHJlcTogSW5jb21pbmdNZXNzYWdlLCBlbWFpbDogc3RyaW5nKTogdm9pZCA9PiB7XG4gICAgcmVxLmhlYWRlcnNbdG9rZW4uVVNFUl9JRF9IRUFERVJdID0gZW1haWw7XG4gICAgcmVxLmhlYWRlcnNbdG9rZW4uVVNFUl9FTUFJTF9IRUFERVJdID0gZW1haWw7XG4gIH07XG5cbiAgLyoqXG4gICAqIEFwcGx5IHRoZSBpbmJvdW5kLWhlYWRlciBwb2xpY3kgdG8gYSB0dW5uZWwgcmVxdWVzdDogZXZlcnkgYHgtYCBoZWFkZXIgdGhlXG4gICAqIGFsbG93LWxpc3QgZG9lcyBub3QgbmFtZSBpcyBkZWxldGVkLCBhbmQgdGhlIHBsYXRmb3JtIGlkZW50aXR5L3RyYW5zcG9ydCBzZXRcbiAgICogaXMgZGVsZXRlZCByZWdhcmRsZXNzLiBTZWUgYC4vaGVhZGVycy50c2AgZm9yIHRoZSByZWFzb25pbmc7IHRoZVxuICAgKiBzZWN1cml0eS1jcml0aWNhbCBjYXNlIGlzIHtAbGluayB0b2tlbi5BQ0NFU1NfVE9LRU5fSEVBREVSfSwgd2hpY2ggdGhlIGdhdGVcbiAgICogbmV2ZXIgc2V0cyBidXQgYW4gYXBwIHJ1bm5pbmcgYGlkZW50aXR5OiBcImF1dG9cImAgdHJlYXRzIGFzIHByb29mIHRoZSByZXF1ZXN0XG4gICAqIGNhbiBkbyBPQk8uXG4gICAqXG4gICAqIGB4ZndkOiB0cnVlYCBvbiB0aGUgcHJveHkgcmUtYWRkcyBgeC1mb3J3YXJkZWQtZm9yYC9gLXByb3RvYC9gLXBvcnRgL2AtaG9zdGBcbiAgICogYWZ0ZXJ3YXJkcyBmcm9tIHRoZSByZWFsIHNvY2tldCwgc28gdGhlIGFwcCBzdGlsbCBzZWVzIHRob3NlIC0gaXQganVzdCBzZWVzXG4gICAqIHRoZSBob25lc3QgdmFsdWVzIHJhdGhlciB0aGFuIHRoZSBjYWxsZXIncyBjbGFpbS5cbiAgICovXG4gIGNvbnN0IGFwcGx5SGVhZGVyUG9saWN5ID0gKHJlcTogSW5jb21pbmdNZXNzYWdlKTogdm9pZCA9PiB7XG4gICAgY29uc3QgcmVtb3ZlZCA9IGhlYWRlclBvbGljeS5hcHBseShyZXEuaGVhZGVycyBhcyBSZWNvcmQ8c3RyaW5nLCB1bmtub3duPik7XG4gICAgaWYgKHJlbW92ZWQubGVuZ3RoKSBsb2dnZXIuZGVidWcoXCJzdHJpcHBlZCBpbmJvdW5kIGhlYWRlcnNcIiwgeyByZW1vdmVkIH0pO1xuICB9O1xuXG4gIGNvbnN0IHNlcnZlciA9IGNyZWF0ZVNlcnZlcihhc3luYyAocmVxLCByZXMpID0+IHtcbiAgICBjb25zdCBwYXRoID0gKHJlcS51cmwgPz8gXCIvXCIpLnNwbGl0KFwiP1wiKVswXSE7XG5cbiAgICAvLyBJbnNlY3VyZS9vcGVuIG1vZGUgKG5vIGdhdGUpIE9SIG5vbi1sb29wYmFjayB0cmFmZmljICh0aGUgaG9zdGluZ1xuICAgIC8vIHBsYXRmb3JtJ3MgZnJvbnQgZG9vciAvIGNvbnRyb2wgcGxhbmUpOiBmb3J3YXJkIHVuZ2F0ZWQuXG4gICAgaWYgKCFnYXRlIHx8ICFpc0xvb3BiYWNrKHJlcS5zb2NrZXQucmVtb3RlQWRkcmVzcykpIHtcbiAgICAgIHByb3h5LndlYihyZXEsIHJlcyk7XG4gICAgICByZXR1cm47XG4gICAgfVxuXG4gICAgLy8gLS0tIHBvcnRyIHRyYWZmaWM6IHRoZSBnYXRlIGFwcGxpZXMgLS0tXG5cbiAgICAvLyBUaGUgbG9naW4gZmxvdyBpcyBhbnN3ZXJlZCBJTi1QUk9DRVNTICh0aGUgYXBwIGhhcyBubyBzZXJ2ZXIgdG8gZm9yd2FyZCB0bykuXG4gICAgaWYgKHBhdGggPT09IGAke0FVVEhfUFJFRklYfS9zdGF0dXNgKSB7XG4gICAgICBjb25zdCB0b2tlbiA9IGh0dHAucGFyc2VDb29raWVzKHJlcS5oZWFkZXJzLmNvb2tpZSA/PyBudWxsKVtTRVNTSU9OX0NPT0tJRV9OQU1FXTtcbiAgICAgIHNlbmRKc29uKHJlcywgMjAwLCBhd2FpdCBnYXRlLnN0YXR1cyh0b2tlbikpO1xuICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICBpZiAocGF0aCA9PT0gYCR7QVVUSF9QUkVGSVh9L3JlcXVlc3RgICYmIHJlcS5tZXRob2QgPT09IFwiUE9TVFwiKSB7XG4gICAgICBjb25zdCBwYXJzZWQgPSBhdXRoUmVxdWVzdFNjaGVtYS5zYWZlUGFyc2UoanNvbi5wYXJzZVJlY29yZChhd2FpdCByZWFkQm9keShyZXEpKSk7XG4gICAgICBpZiAoIXBhcnNlZC5zdWNjZXNzKSByZXR1cm4gc2VuZEpzb24ocmVzLCAyMDAsIHsgb2s6IHRydWUgfSk7IC8vIGFudGktZW51bWVyYXRpb25cbiAgICAgIHJldHVybiBzZW5kSnNvbihyZXMsIDIwMCwgYXdhaXQgZ2F0ZS5yZXF1ZXN0KHBhcnNlZC5kYXRhLmVtYWlsLCBjbGllbnRJcChyZXEpKSk7XG4gICAgfVxuICAgIGlmIChwYXRoID09PSBgJHtBVVRIX1BSRUZJWH0vdmVyaWZ5YCAmJiByZXEubWV0aG9kID09PSBcIlBPU1RcIikge1xuICAgICAgY29uc3QgcGFyc2VkID0gYXV0aFZlcmlmeVNjaGVtYS5zYWZlUGFyc2UoanNvbi5wYXJzZVJlY29yZChhd2FpdCByZWFkQm9keShyZXEpKSk7XG4gICAgICBpZiAoIXBhcnNlZC5zdWNjZXNzKSByZXR1cm4gc2VuZEpzb24ocmVzLCAyMDAsIHsgb2s6IGZhbHNlIH0pO1xuICAgICAgY29uc3QgcmVzdWx0ID0gYXdhaXQgZ2F0ZS52ZXJpZnkocGFyc2VkLmRhdGEuZW1haWwsIHBhcnNlZC5kYXRhLmNvZGUsIGNsaWVudElwKHJlcSkpO1xuICAgICAgY29uc3QgY29va2llID1cbiAgICAgICAgcmVzdWx0Lm9rICYmIHJlc3VsdC50b2tlbiA/IHNlc3Npb25Db29raWUocmVzdWx0LnRva2VuLCBnYXRlLnNlc3Npb25UdGxTZWNvbmRzKSA6IHVuZGVmaW5lZDtcbiAgICAgIHJldHVybiBzZW5kSnNvbihcbiAgICAgICAgcmVzLFxuICAgICAgICAyMDAsXG4gICAgICAgIHsgb2s6IHJlc3VsdC5vaywgLi4uKHJlc3VsdC5yZXRyeUFmdGVyID8geyByZXRyeUFmdGVyOiByZXN1bHQucmV0cnlBZnRlciB9IDoge30pIH0sXG4gICAgICAgIGNvb2tpZSxcbiAgICAgICk7XG4gICAgfVxuICAgIGlmIChwYXRoID09PSBgJHtBVVRIX1BSRUZJWH0vbG9nb3V0YCAmJiByZXEubWV0aG9kID09PSBcIlBPU1RcIikge1xuICAgICAgcmV0dXJuIHNlbmRKc29uKFxuICAgICAgICByZXMsXG4gICAgICAgIDIwMCxcbiAgICAgICAgeyBvazogdHJ1ZSB9LFxuICAgICAgICBgJHtTRVNTSU9OX0NPT0tJRV9OQU1FfT07IFBhdGg9LzsgSHR0cE9ubHk7IE1heC1BZ2U9MGAsXG4gICAgICApO1xuICAgIH1cblxuICAgIC8vIEFudGktc3Bvb2Y6IGFwcGx5IHRoZSBpbmJvdW5kLWhlYWRlciBhbGxvdy1saXN0IHRvIHBvcnRyIHRyYWZmaWMgLSBvbmx5IHRoZVxuICAgIC8vIGdhdGUgbWF5IGFzc2VydCBpZGVudGl0eSwgYW5kIGl0IGRvZXMgc28gYmVsb3cgZm9yIGEgdmVyaWZpZWQgc2Vzc2lvbi5cbiAgICBhcHBseUhlYWRlclBvbGljeShyZXEpO1xuXG4gICAgLy8gU3RhdGljIChub24tQVBJKSBsb2FkcyBmcmVlbHkgc28gdGhlIFNQQSArIDxBdXRoR2F0ZT4gY2FuIHJlbmRlci4gU3RyaXAgdGhlXG4gICAgLy8gc2Vzc2lvbiBjb29raWUgc28gaXQgbmV2ZXIgbGVha3MgdG8gdGhlIHN0YXRpYyBoYW5kbGVyLlxuICAgIGlmICghcGF0aC5zdGFydHNXaXRoKFwiL2FwaS9cIikpIHtcbiAgICAgIHN0cmlwU2Vzc2lvbkNvb2tpZShyZXEpO1xuICAgICAgcHJveHkud2ViKHJlcSwgcmVzKTtcbiAgICAgIHJldHVybjtcbiAgICB9XG5cbiAgICAvLyBFdmVyeSBvdGhlciAvYXBpLyogcmVxdWlyZXMgYSB2YWxpZCBzZXNzaW9uLlxuICAgIGNvbnN0IHRva2VuID0gaHR0cC5wYXJzZUNvb2tpZXMocmVxLmhlYWRlcnMuY29va2llID8/IG51bGwpW1NFU1NJT05fQ09PS0lFX05BTUVdO1xuICAgIGNvbnN0IGVtYWlsID0gYXdhaXQgZ2F0ZS5zZXNzaW9uKHRva2VuKTtcbiAgICBpZiAoZW1haWwpIHtcbiAgICAgIC8vIFByZXNlbnQgdGhlIE9UUCB1c2VyIGxpa2UgYSBwbGF0Zm9ybSBmcm9udCBkb29yLCBhbmQgZHJvcCB0aGUgZ2F0ZVxuICAgICAgLy8gY29va2llIHNvIHRoZSBhcHAgc2VlcyBhIGNsZWFuLCBmcm9udC1kb29yLXNoYXBlZCByZXF1ZXN0LlxuICAgICAgaW5qZWN0SWRlbnRpdHkocmVxLCBlbWFpbCk7XG4gICAgICBzdHJpcFNlc3Npb25Db29raWUocmVxKTtcbiAgICAgIHByb3h5LndlYihyZXEsIHJlcyk7XG4gICAgfSBlbHNlIHtcbiAgICAgIHNlbmRKc29uKHJlcywgNDAxLCB7IGVycm9yOiBcImF1dGhlbnRpY2F0aW9uIHJlcXVpcmVkXCIsIGxvZ2luUGF0aDogQVVUSF9QUkVGSVggfSk7XG4gICAgfVxuICB9KTtcblxuICAvLyBXZWJTb2NrZXQgdXBncmFkZXM6IGZvcndhcmQgZnJvbnQtZG9vciB0cmFmZmljIHVudG91Y2hlZDsgZ2F0ZSBsb29wYmFja1xuICAvLyAocG9ydHIpIEFQSSB1cGdyYWRlcyB0aGUgc2FtZSB3YXkgYXMgSFRUUCAoc3RyaXAgY29va2llICsgaW5qZWN0IGlkZW50aXR5KS5cbiAgc2VydmVyLm9uKFwidXBncmFkZVwiLCAocmVxOiBJbmNvbWluZ01lc3NhZ2UsIHNvY2tldDogU29ja2V0LCBoZWFkOiBCdWZmZXIpID0+IHtcbiAgICBpZiAoIWdhdGUgfHwgIWlzTG9vcGJhY2socmVxLnNvY2tldC5yZW1vdGVBZGRyZXNzKSkge1xuICAgICAgcHJveHkud3MocmVxLCBzb2NrZXQsIGhlYWQpO1xuICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICBhcHBseUhlYWRlclBvbGljeShyZXEpO1xuICAgIGlmICghKHJlcS51cmwgPz8gXCJcIikuc3RhcnRzV2l0aChcIi9hcGkvXCIpKSB7XG4gICAgICBzdHJpcFNlc3Npb25Db29raWUocmVxKTtcbiAgICAgIHByb3h5LndzKHJlcSwgc29ja2V0LCBoZWFkKTtcbiAgICAgIHJldHVybjtcbiAgICB9XG4gICAgY29uc3QgdG9rZW4gPSBodHRwLnBhcnNlQ29va2llcyhyZXEuaGVhZGVycy5jb29raWUgPz8gbnVsbClbU0VTU0lPTl9DT09LSUVfTkFNRV07XG4gICAgdm9pZCBnYXRlLnNlc3Npb24odG9rZW4pLnRoZW4oKGVtYWlsKSA9PiB7XG4gICAgICBpZiAoIWVtYWlsKSB7XG4gICAgICAgIHNvY2tldC5kZXN0cm95KCk7XG4gICAgICAgIHJldHVybjtcbiAgICAgIH1cbiAgICAgIGluamVjdElkZW50aXR5KHJlcSwgZW1haWwpO1xuICAgICAgc3RyaXBTZXNzaW9uQ29va2llKHJlcSk7XG4gICAgICBwcm94eS53cyhyZXEsIHNvY2tldCwgaGVhZCk7XG4gICAgfSk7XG4gIH0pO1xuXG4gIHJldHVybiBuZXcgUHJvbWlzZSgocmVzb2x2ZSkgPT4ge1xuICAgIHNlcnZlci5saXN0ZW4ocHVibGljUG9ydCwgXCIwLjAuMC4wXCIsICgpID0+IHtcbiAgICAgIGxvZ2dlci5pbmZvKGBnYXRlIHByb3h5IG9uIDAuMC4wLjA6JHtwdWJsaWNQb3J0fSAtPiBhcHAgMTI3LjAuMC4xOiR7YXBwUG9ydH1gKTtcbiAgICAgIHJlc29sdmUoKTtcbiAgICB9KTtcbiAgfSk7XG59XG4iXX0=
package/src/app.ts DELETED
@@ -1,292 +0,0 @@
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` (Lakebase when this process can reach it, else memory) -
7
- * which the OTP `CodeStore` and the session signing key use for storage + TTL
8
- * eviction;
9
- * - the `email()` transport (SMTP / outbox), primed from env, so `sendCode` can
10
- * deliver the one-time code;
11
- * - dbx-tools auto-configuration + branding via `@dbx-tools/appkit`.
12
- *
13
- * It returns the {@link AuthGateApi} the proxy drives. `sendCode` is wired here
14
- * (the plugin can't resolve HOW to send on its own) to the email plugin's
15
- * transport. A sign-in code is SYSTEM mail - no on-behalf-of user asked for it
16
- * and no reply to it reaches anyone - so it sends from the email config's
17
- * do-not-reply address (`no-reply@EMAIL_DOMAIN` unless EMAIL_SYSTEM_FROM names
18
- * another).
19
- *
20
- * FAIL FAST: a gate that can't email a code is useless, so if email does not
21
- * resolve to SMTP mode (real delivery), this throws - unless `insecure` is set
22
- * (`--insecure` / `TUNNEL_INSECURE=true`), in which case the caller runs the
23
- * tunnel OPEN with no gate.
24
- *
25
- * @module
26
- */
27
-
28
- import { createApp as createAppNs, lakebaseResolver } from "@dbx-tools/appkit";
29
- import { brand as nodeBrand } from "@dbx-tools/core";
30
- import { brand as emailBrand, email, sender, transport } from "@dbx-tools/email";
31
- import { env, log, string } from "@dbx-tools/shared-core";
32
- import { authGate, type AuthGateApi, type AuthGateConfig, type SendCodeOptions } from "./plugin.ts";
33
-
34
- const logger = log.logger("tunnel:app");
35
-
36
- /**
37
- * A code TTL as the plain phrase the email states ("10 minutes", "45 seconds").
38
- *
39
- * Whole minutes read as minutes; anything else stays in seconds rather than
40
- * rounding, so a 90-second TTL is not advertised as "1 minute" and a recipient is
41
- * never told the code lives longer than it does.
42
- */
43
- export function expiresIn(seconds: number): string {
44
- return seconds >= 60 && seconds % 60 === 0
45
- ? string.pluralize(seconds / 60, "minute")
46
- : string.pluralize(seconds, "second");
47
- }
48
-
49
- /** The parts of {@link SendCodeOptions} the code email's copy is built from. */
50
- type CodeCopy = Pick<SendCodeOptions, "message" | "codeTtlSeconds">;
51
-
52
- /** The reassurance line closing both parts. */
53
- const IGNORE_LINE = "If you did not request this code, you can ignore this email.";
54
-
55
- /**
56
- * The HTML part's source: the full branded template, with the code as a large
57
- * styled heading (`## ` is what makes it prominent in an inbox).
58
- */
59
- export function codeEmailHtmlBody(code: string, opts: CodeCopy): string {
60
- return [
61
- opts.message,
62
- "",
63
- `## ${code}`,
64
- "",
65
- `This code expires in ${expiresIn(opts.codeTtlSeconds)}.`,
66
- "",
67
- IGNORE_LINE,
68
- ].join("\n");
69
- }
70
-
71
- /**
72
- * The `text/plain` part, supplied EXPLICITLY rather than rendered from the tree
73
- * above. Both parts say the same thing; only the line layout differs.
74
- *
75
- * The prompt and the code share ONE line ("Your verification code is: 123456").
76
- * That single-line shape is what iOS, Gmail, Outlook, and Android code detection
77
- * keys on most reliably - the heuristics look for a code in the same sentence as
78
- * a recognized prompt, so splitting them across lines makes detection dependent
79
- * on the client, and any blank line between them defeats it outright.
80
- *
81
- * The GENERATED text part cannot hold that shape at all: it is a rendering of the
82
- * HTML, so it carries the brand header/footer and turns the code heading's CSS
83
- * margin into blank lines, arriving as `prompt\n\n\ncode`.
84
- *
85
- * The code is visible text in BOTH parts, never an image, so a client scraping
86
- * either one finds it. No trailer line follows the copy - Apple's domain-bound
87
- * `@domain #code` footer is deliberately NOT emitted, since it constrains the
88
- * code to one origin and is not what the broadly-compatible shape needs.
89
- */
90
- export function codeEmailTextBody(code: string, opts: CodeCopy): string {
91
- return [
92
- `${opts.message} ${code}`,
93
- `This code expires in ${expiresIn(opts.codeTtlSeconds)}.`,
94
- "",
95
- IGNORE_LINE,
96
- ].join("\n");
97
- }
98
-
99
- /**
100
- * The SUBJECT line, with the code in it: `"123456 is your verification code"`.
101
- *
102
- * The code has to be here, not only in the body, because of what mobile autofill
103
- * actually reads. iOS offers a code from an incoming NOTIFICATION - natively for
104
- * Messages and Mail, and since iOS 26 for any app's notification text, which is
105
- * what finally made Gmail work - and a notification contains the sender, the
106
- * subject, and a short snippet. Nothing else. A code that lives in the body is
107
- * invisible to it, however cleanly the body is formatted, which is why a perfectly
108
- * shaped `text/plain` part still produced no autofill prompt in Gmail.
109
- *
110
- * `<code> is your <thing>` rather than `<thing>: <code>` because the leading code
111
- * survives TRUNCATION: a notification and an inbox list both cut the subject, and
112
- * the platform heuristics want the code in the same sentence as a recognized
113
- * prompt ("code", "verification"). Putting it first keeps both intact no matter
114
- * where the cut lands.
115
- *
116
- * `subject` is the configured line ("Your verification code"), lower-cased at its
117
- * first word so the sentence reads naturally, and left ALONE when it does not look
118
- * like the conventional phrasing - an operator who set a deliberate subject gets
119
- * theirs with the code prefixed, not a mangled hybrid.
120
- */
121
- export function codeEmailSubject(code: string, subject: string): string {
122
- const trimmed = subject.trim();
123
- const conventional = /^your\s+/i.exec(trimmed);
124
- const rest = conventional ? trimmed.slice(conventional[0].length) : trimmed;
125
- return conventional ? `${code} is your ${rest}` : `${code} - ${trimmed}`;
126
- }
127
-
128
- /**
129
- * The PREHEADER: the snippet beside the subject in an inbox list, and the body of
130
- * the push notification. Carries the code for the same reason the subject does -
131
- * it is the other half of what a notification shows - and repeats the prompt
132
- * wording so a heuristic scanning the snippet alone finds a code next to a phrase
133
- * it recognizes.
134
- */
135
- export function codeEmailPreview(code: string, opts: CodeCopy): string {
136
- return `${opts.message} ${code}`;
137
- }
138
-
139
- const { createApp } = createAppNs;
140
- const { applyLakebaseEnv } = lakebaseResolver;
141
- const { resolveSystemSenderAddress } = sender;
142
- const { getEmailRuntime, sendEmail } = transport;
143
- const { emailBrandFromContext } = emailBrand;
144
- const { loadBrandContext } = nodeBrand;
145
-
146
- /**
147
- * Env vars whose presence means a Lakebase database was bound to this deployment.
148
- * `LAKEBASE_ENDPOINT` is what a Databricks App `postgres` resource binding sets;
149
- * `PGHOST` is the local/manual spelling.
150
- */
151
- const LAKEBASE_ENV = ["LAKEBASE_ENDPOINT", "PGHOST"] as const;
152
-
153
- /**
154
- * Upper bound on resolving the Lakebase connection. The gate is in the request
155
- * path for every visitor, so a slow workspace API must not hold up boot
156
- * indefinitely; a memory cache is a degraded gate, an unbooted one is no gate.
157
- */
158
- const LAKEBASE_RESOLVE_TIMEOUT_MS = 60_000;
159
-
160
- /**
161
- * Fill in the Lakebase connection env AppKit's cache needs, so `CacheManager`
162
- * chooses PERSISTENT storage instead of memory.
163
- *
164
- * This is what makes the gate's session signing key and outstanding one-time
165
- * codes survive a restart. `applyLakebaseEnv` is the SHARED helper AppKit
166
- * auto-configuration uses, so the gate gets exactly the env a pool needs -
167
- * `LAKEBASE_ENDPOINT`, `PGHOST`, `PGDATABASE`, and `PGUSER` - rather than a
168
- * hand-rolled subset. All four matter: `createLakebasePool()` throws without any
169
- * one of them, and a Databricks App `postgres` resource binding supplies only the
170
- * first. Without them the pool cannot be built, the cache silently degrades to
171
- * in-memory, and every redeploy signs out every user (the exact symptom this
172
- * exists to prevent).
173
- *
174
- * It is called here rather than left to `createApp`'s `autoConfigure` because that
175
- * gates on a `lakebase()` plugin being registered - and this app registers none,
176
- * having no server to mount Lakebase routes on. Calling the helper directly also
177
- * lets the gate be stricter than `autoConfigure` is:
178
- *
179
- * - It runs ONLY when a Lakebase env var is present. A tunnel is a wrapper
180
- * around someone else's app and must not invent infrastructure, so with
181
- * nothing bound it skips rather than falling through the resolver's
182
- * list-or-CREATE-a-project path.
183
- * - `autoCreate: false` for the same reason, in case a project happens to exist.
184
- * - Every failure is a WARNING, never a throw. The cache is an optimization for
185
- * session durability; admission still requires a code delivered to an
186
- * allow-listed address, so a gate with a memory cache is safe, just forgetful.
187
- */
188
- export async function resolveCacheStorageEnv(): Promise<boolean> {
189
- if (!env.text(LAKEBASE_ENV)) {
190
- logger.info(
191
- `no ${env.name(LAKEBASE_ENV)} - the gate cache stays in memory; sessions and codes will not survive a restart`,
192
- );
193
- return false;
194
- }
195
- try {
196
- const { resolved, user } = await applyLakebaseEnv(
197
- { autoCreate: false },
198
- AbortSignal.timeout(LAKEBASE_RESOLVE_TIMEOUT_MS),
199
- );
200
- logger.info("lakebase resolved for the gate cache", {
201
- host: resolved.host,
202
- database: resolved.database,
203
- endpoint: resolved.endpoint,
204
- user,
205
- });
206
- return true;
207
- } catch (error) {
208
- logger.warn(
209
- "could not resolve lakebase - the gate cache stays in memory; sessions and codes will not survive a restart",
210
- { error },
211
- );
212
- return false;
213
- }
214
- }
215
-
216
- /**
217
- * Boot the gate app and return the API the proxy calls. Throws when email is not
218
- * in SMTP mode (no way to deliver a code) so a misconfigured gate fails fast at
219
- * startup rather than silently accepting nobody; the caller may catch this and
220
- * fall back to insecure/open mode when the operator opted in.
221
- */
222
- export async function startGateApp(config: AuthGateConfig): Promise<AuthGateApi> {
223
- // The host app's own brand (`branding/brand.yaml` discovered from cwd) or the
224
- // dbx-tools default. This is the ONE brand source for the gate: it styles the
225
- // code email (accent band, font, logo) via the email plugin AND supplies the
226
- // display name the copy uses, so a deployment that themes its app themes its
227
- // sign-in email with it. An explicit `brandName` still wins (see below).
228
- const context = await loadBrandContext();
229
-
230
- // Before `createApp`, because AppKit resolves the cache's storage during it and
231
- // reads the connection out of `process.env` when it does. See the function's
232
- // docs for why the gate resolves this itself.
233
- await resolveCacheStorageEnv();
234
-
235
- // `sendCode` delivers the OTP through the email plugin's SHARED transport, which
236
- // the `email()` plugin primes during its `setup()` (awaited by `createApp`
237
- // below). Using the module-level `sendEmail` avoids a circular dependency on the
238
- // app handle's inferred type. The `From` is the SYSTEM sender: a verification
239
- // code is machine-generated and unanswerable, so it must not arrive from a
240
- // person's address inviting a reply.
241
- const sendCode: NonNullable<AuthGateConfig["sendCode"]> = async (to, code, opts) => {
242
- const from = resolveSystemSenderAddress(getEmailRuntime().config);
243
- await sendEmail(
244
- {
245
- to: [to],
246
- // The code rides in the SUBJECT and the preheader, not just the body:
247
- // those two strings are the whole of a push notification, and a
248
- // notification is what mobile autofill reads. See `codeEmailSubject`.
249
- subject: codeEmailSubject(code, opts.subject),
250
- body: codeEmailHtmlBody(code, opts),
251
- },
252
- from,
253
- undefined,
254
- { text: codeEmailTextBody(code, opts), preview: codeEmailPreview(code, opts) },
255
- );
256
- };
257
-
258
- // `createApp` namespaces each plugin's exports on the handle by manifest name;
259
- // `handle.authGate` is the in-process gate API the proxy drives.
260
- //
261
- // `autoConfigure: false` because {@link resolveCacheStorageEnv} above already
262
- // ran the one piece of it this app wants (`applyLakebaseEnv`), on this app's
263
- // terms - no auto-create, and a warning instead of a throw.
264
- const handle = await createApp({
265
- autoConfigure: false,
266
- plugins: [
267
- // Brand the code email from the resolved context, the same bridge every
268
- // other dbx-tools email surface uses (accent + font inlined, logo only when
269
- // it is a fetchable URL - a package-export path cannot load in an inbox).
270
- email({ brand: emailBrandFromContext(context) }),
271
- // `brandName` falls back to the resolved context's display name, so the
272
- // email copy names the app rather than a generic placeholder. Spelled as an
273
- // explicit `??` rather than a key before `...config`: commander sets
274
- // `brandName` on the options object whether or not the flag was passed, so
275
- // spreading it would clobber the context name with `undefined`.
276
- authGate({ ...config, brandName: config.brandName ?? context.name, sendCode }),
277
- ],
278
- });
279
-
280
- // Fail fast: the gate needs SMTP to email codes. `getEmailRuntime()` resolves to
281
- // `mode: "file"` (outbox) or throws when no SMTP creds are configured - neither
282
- // can deliver to a real inbox, so refuse to bring up a gate that lets nobody in.
283
- if (getEmailRuntime().config.mode !== "smtp") {
284
- throw new Error(
285
- "email is not configured for SMTP delivery - the OTP gate cannot send codes. " +
286
- "Set SMTP_HOST/SMTP_USER/SMTP_PASSWORD, or pass --insecure / TUNNEL_INSECURE=true to run the tunnel open.",
287
- );
288
- }
289
-
290
- logger.info("gate app ready (no server; proxy-driven)");
291
- return handle.authGate as AuthGateApi;
292
- }