@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.
@@ -0,0 +1,130 @@
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
+ import { type AuthGateApi, type AuthGateConfig, type SendCodeOptions } from "./plugin.ts";
28
+ /**
29
+ * A code TTL as the plain phrase the email states ("10 minutes", "45 seconds").
30
+ *
31
+ * Whole minutes read as minutes; anything else stays in seconds rather than
32
+ * rounding, so a 90-second TTL is not advertised as "1 minute" and a recipient is
33
+ * never told the code lives longer than it does.
34
+ */
35
+ export declare function expiresIn(seconds: number): string;
36
+ /** The parts of {@link SendCodeOptions} the code email's copy is built from. */
37
+ type CodeCopy = Pick<SendCodeOptions, "message" | "codeTtlSeconds">;
38
+ /**
39
+ * The HTML part's source: the full branded template, with the code as a large
40
+ * styled heading (`## ` is what makes it prominent in an inbox).
41
+ */
42
+ export declare function codeEmailHtmlBody(code: string, opts: CodeCopy): string;
43
+ /**
44
+ * The `text/plain` part, supplied EXPLICITLY rather than rendered from the tree
45
+ * above. Both parts say the same thing; only the line layout differs.
46
+ *
47
+ * The prompt and the code share ONE line ("Your verification code is: 123456").
48
+ * That single-line shape is what iOS, Gmail, Outlook, and Android code detection
49
+ * keys on most reliably - the heuristics look for a code in the same sentence as
50
+ * a recognized prompt, so splitting them across lines makes detection dependent
51
+ * on the client, and any blank line between them defeats it outright.
52
+ *
53
+ * The GENERATED text part cannot hold that shape at all: it is a rendering of the
54
+ * HTML, so it carries the brand header/footer and turns the code heading's CSS
55
+ * margin into blank lines, arriving as `prompt\n\n\ncode`.
56
+ *
57
+ * The code is visible text in BOTH parts, never an image, so a client scraping
58
+ * either one finds it. No trailer line follows the copy - Apple's domain-bound
59
+ * `@domain #code` footer is deliberately NOT emitted, since it constrains the
60
+ * code to one origin and is not what the broadly-compatible shape needs.
61
+ */
62
+ export declare function codeEmailTextBody(code: string, opts: CodeCopy): string;
63
+ /**
64
+ * The SUBJECT line, with the code in it: `"123456 is your verification code"`.
65
+ *
66
+ * The code has to be here, not only in the body, because of what mobile autofill
67
+ * actually reads. iOS offers a code from an incoming NOTIFICATION - natively for
68
+ * Messages and Mail, and since iOS 26 for any app's notification text, which is
69
+ * what finally made Gmail work - and a notification contains the sender, the
70
+ * subject, and a short snippet. Nothing else. A code that lives in the body is
71
+ * invisible to it, however cleanly the body is formatted, which is why a perfectly
72
+ * shaped `text/plain` part still produced no autofill prompt in Gmail.
73
+ *
74
+ * `<code> is your <thing>` rather than `<thing>: <code>` because the leading code
75
+ * survives TRUNCATION: a notification and an inbox list both cut the subject, and
76
+ * the platform heuristics want the code in the same sentence as a recognized
77
+ * prompt ("code", "verification"). Putting it first keeps both intact no matter
78
+ * where the cut lands.
79
+ *
80
+ * `subject` is the configured line ("Your verification code"), lower-cased at its
81
+ * first word so the sentence reads naturally, and left ALONE when it does not look
82
+ * like the conventional phrasing - an operator who set a deliberate subject gets
83
+ * theirs with the code prefixed, not a mangled hybrid.
84
+ */
85
+ export declare function codeEmailSubject(code: string, subject: string): string;
86
+ /**
87
+ * The PREHEADER: the snippet beside the subject in an inbox list, and the body of
88
+ * the push notification. Carries the code for the same reason the subject does -
89
+ * it is the other half of what a notification shows - and repeats the prompt
90
+ * wording so a heuristic scanning the snippet alone finds a code next to a phrase
91
+ * it recognizes.
92
+ */
93
+ export declare function codeEmailPreview(code: string, opts: CodeCopy): string;
94
+ /**
95
+ * Fill in the Lakebase connection env AppKit's cache needs, so `CacheManager`
96
+ * chooses PERSISTENT storage instead of memory.
97
+ *
98
+ * This is what makes the gate's session signing key and outstanding one-time
99
+ * codes survive a restart. `applyLakebaseEnv` is the SHARED helper AppKit
100
+ * auto-configuration uses, so the gate gets exactly the env a pool needs -
101
+ * `LAKEBASE_ENDPOINT`, `PGHOST`, `PGDATABASE`, and `PGUSER` - rather than a
102
+ * hand-rolled subset. All four matter: `createLakebasePool()` throws without any
103
+ * one of them, and a Databricks App `postgres` resource binding supplies only the
104
+ * first. Without them the pool cannot be built, the cache silently degrades to
105
+ * in-memory, and every redeploy signs out every user (the exact symptom this
106
+ * exists to prevent).
107
+ *
108
+ * It is called here rather than left to `createApp`'s `autoConfigure` because that
109
+ * gates on a `lakebase()` plugin being registered - and this app registers none,
110
+ * having no server to mount Lakebase routes on. Calling the helper directly also
111
+ * lets the gate be stricter than `autoConfigure` is:
112
+ *
113
+ * - It runs ONLY when a Lakebase env var is present. A tunnel is a wrapper
114
+ * around someone else's app and must not invent infrastructure, so with
115
+ * nothing bound it skips rather than falling through the resolver's
116
+ * list-or-CREATE-a-project path.
117
+ * - `autoCreate: false` for the same reason, in case a project happens to exist.
118
+ * - Every failure is a WARNING, never a throw. The cache is an optimization for
119
+ * session durability; admission still requires a code delivered to an
120
+ * allow-listed address, so a gate with a memory cache is safe, just forgetful.
121
+ */
122
+ export declare function resolveCacheStorageEnv(): Promise<boolean>;
123
+ /**
124
+ * Boot the gate app and return the API the proxy calls. Throws when email is not
125
+ * in SMTP mode (no way to deliver a code) so a misconfigured gate fails fast at
126
+ * startup rather than silently accepting nobody; the caller may catch this and
127
+ * fall back to insecure/open mode when the operator opted in.
128
+ */
129
+ export declare function startGateApp(config: AuthGateConfig): Promise<AuthGateApi>;
130
+ export {};
package/lib/src/app.js ADDED
@@ -0,0 +1,258 @@
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
+ import { createApp as createAppNs, lakebaseResolver } from "@dbx-tools/appkit";
28
+ import { brand as nodeBrand } from "@dbx-tools/core";
29
+ import { brand as emailBrand, email, sender, transport } from "@dbx-tools/email";
30
+ import { env, log, string } from "@dbx-tools/shared-core";
31
+ import { authGate } from "./plugin.js";
32
+ const logger = log.logger("tunnel:app");
33
+ /**
34
+ * A code TTL as the plain phrase the email states ("10 minutes", "45 seconds").
35
+ *
36
+ * Whole minutes read as minutes; anything else stays in seconds rather than
37
+ * rounding, so a 90-second TTL is not advertised as "1 minute" and a recipient is
38
+ * never told the code lives longer than it does.
39
+ */
40
+ export function expiresIn(seconds) {
41
+ return seconds >= 60 && seconds % 60 === 0
42
+ ? string.pluralize(seconds / 60, "minute")
43
+ : string.pluralize(seconds, "second");
44
+ }
45
+ /** The reassurance line closing both parts. */
46
+ const IGNORE_LINE = "If you did not request this code, you can ignore this email.";
47
+ /**
48
+ * The HTML part's source: the full branded template, with the code as a large
49
+ * styled heading (`## ` is what makes it prominent in an inbox).
50
+ */
51
+ export function codeEmailHtmlBody(code, opts) {
52
+ return [
53
+ opts.message,
54
+ "",
55
+ `## ${code}`,
56
+ "",
57
+ `This code expires in ${expiresIn(opts.codeTtlSeconds)}.`,
58
+ "",
59
+ IGNORE_LINE,
60
+ ].join("\n");
61
+ }
62
+ /**
63
+ * The `text/plain` part, supplied EXPLICITLY rather than rendered from the tree
64
+ * above. Both parts say the same thing; only the line layout differs.
65
+ *
66
+ * The prompt and the code share ONE line ("Your verification code is: 123456").
67
+ * That single-line shape is what iOS, Gmail, Outlook, and Android code detection
68
+ * keys on most reliably - the heuristics look for a code in the same sentence as
69
+ * a recognized prompt, so splitting them across lines makes detection dependent
70
+ * on the client, and any blank line between them defeats it outright.
71
+ *
72
+ * The GENERATED text part cannot hold that shape at all: it is a rendering of the
73
+ * HTML, so it carries the brand header/footer and turns the code heading's CSS
74
+ * margin into blank lines, arriving as `prompt\n\n\ncode`.
75
+ *
76
+ * The code is visible text in BOTH parts, never an image, so a client scraping
77
+ * either one finds it. No trailer line follows the copy - Apple's domain-bound
78
+ * `@domain #code` footer is deliberately NOT emitted, since it constrains the
79
+ * code to one origin and is not what the broadly-compatible shape needs.
80
+ */
81
+ export function codeEmailTextBody(code, opts) {
82
+ return [
83
+ `${opts.message} ${code}`,
84
+ `This code expires in ${expiresIn(opts.codeTtlSeconds)}.`,
85
+ "",
86
+ IGNORE_LINE,
87
+ ].join("\n");
88
+ }
89
+ /**
90
+ * The SUBJECT line, with the code in it: `"123456 is your verification code"`.
91
+ *
92
+ * The code has to be here, not only in the body, because of what mobile autofill
93
+ * actually reads. iOS offers a code from an incoming NOTIFICATION - natively for
94
+ * Messages and Mail, and since iOS 26 for any app's notification text, which is
95
+ * what finally made Gmail work - and a notification contains the sender, the
96
+ * subject, and a short snippet. Nothing else. A code that lives in the body is
97
+ * invisible to it, however cleanly the body is formatted, which is why a perfectly
98
+ * shaped `text/plain` part still produced no autofill prompt in Gmail.
99
+ *
100
+ * `<code> is your <thing>` rather than `<thing>: <code>` because the leading code
101
+ * survives TRUNCATION: a notification and an inbox list both cut the subject, and
102
+ * the platform heuristics want the code in the same sentence as a recognized
103
+ * prompt ("code", "verification"). Putting it first keeps both intact no matter
104
+ * where the cut lands.
105
+ *
106
+ * `subject` is the configured line ("Your verification code"), lower-cased at its
107
+ * first word so the sentence reads naturally, and left ALONE when it does not look
108
+ * like the conventional phrasing - an operator who set a deliberate subject gets
109
+ * theirs with the code prefixed, not a mangled hybrid.
110
+ */
111
+ export function codeEmailSubject(code, subject) {
112
+ const trimmed = subject.trim();
113
+ const conventional = /^your\s+/i.exec(trimmed);
114
+ const rest = conventional ? trimmed.slice(conventional[0].length) : trimmed;
115
+ return conventional ? `${code} is your ${rest}` : `${code} - ${trimmed}`;
116
+ }
117
+ /**
118
+ * The PREHEADER: the snippet beside the subject in an inbox list, and the body of
119
+ * the push notification. Carries the code for the same reason the subject does -
120
+ * it is the other half of what a notification shows - and repeats the prompt
121
+ * wording so a heuristic scanning the snippet alone finds a code next to a phrase
122
+ * it recognizes.
123
+ */
124
+ export function codeEmailPreview(code, opts) {
125
+ return `${opts.message} ${code}`;
126
+ }
127
+ const { createApp } = createAppNs;
128
+ const { applyLakebaseEnv } = lakebaseResolver;
129
+ const { resolveSystemSenderAddress } = sender;
130
+ const { getEmailRuntime, sendEmail } = transport;
131
+ const { emailBrandFromContext } = emailBrand;
132
+ const { loadBrandContext } = nodeBrand;
133
+ /**
134
+ * Env vars whose presence means a Lakebase database was bound to this deployment.
135
+ * `LAKEBASE_ENDPOINT` is what a Databricks App `postgres` resource binding sets;
136
+ * `PGHOST` is the local/manual spelling.
137
+ */
138
+ const LAKEBASE_ENV = ["LAKEBASE_ENDPOINT", "PGHOST"];
139
+ /**
140
+ * Upper bound on resolving the Lakebase connection. The gate is in the request
141
+ * path for every visitor, so a slow workspace API must not hold up boot
142
+ * indefinitely; a memory cache is a degraded gate, an unbooted one is no gate.
143
+ */
144
+ const LAKEBASE_RESOLVE_TIMEOUT_MS = 60_000;
145
+ /**
146
+ * Fill in the Lakebase connection env AppKit's cache needs, so `CacheManager`
147
+ * chooses PERSISTENT storage instead of memory.
148
+ *
149
+ * This is what makes the gate's session signing key and outstanding one-time
150
+ * codes survive a restart. `applyLakebaseEnv` is the SHARED helper AppKit
151
+ * auto-configuration uses, so the gate gets exactly the env a pool needs -
152
+ * `LAKEBASE_ENDPOINT`, `PGHOST`, `PGDATABASE`, and `PGUSER` - rather than a
153
+ * hand-rolled subset. All four matter: `createLakebasePool()` throws without any
154
+ * one of them, and a Databricks App `postgres` resource binding supplies only the
155
+ * first. Without them the pool cannot be built, the cache silently degrades to
156
+ * in-memory, and every redeploy signs out every user (the exact symptom this
157
+ * exists to prevent).
158
+ *
159
+ * It is called here rather than left to `createApp`'s `autoConfigure` because that
160
+ * gates on a `lakebase()` plugin being registered - and this app registers none,
161
+ * having no server to mount Lakebase routes on. Calling the helper directly also
162
+ * lets the gate be stricter than `autoConfigure` is:
163
+ *
164
+ * - It runs ONLY when a Lakebase env var is present. A tunnel is a wrapper
165
+ * around someone else's app and must not invent infrastructure, so with
166
+ * nothing bound it skips rather than falling through the resolver's
167
+ * list-or-CREATE-a-project path.
168
+ * - `autoCreate: false` for the same reason, in case a project happens to exist.
169
+ * - Every failure is a WARNING, never a throw. The cache is an optimization for
170
+ * session durability; admission still requires a code delivered to an
171
+ * allow-listed address, so a gate with a memory cache is safe, just forgetful.
172
+ */
173
+ export async function resolveCacheStorageEnv() {
174
+ if (!env.text(LAKEBASE_ENV)) {
175
+ logger.info(`no ${env.name(LAKEBASE_ENV)} - the gate cache stays in memory; sessions and codes will not survive a restart`);
176
+ return false;
177
+ }
178
+ try {
179
+ const { resolved, user } = await applyLakebaseEnv({ autoCreate: false }, AbortSignal.timeout(LAKEBASE_RESOLVE_TIMEOUT_MS));
180
+ logger.info("lakebase resolved for the gate cache", {
181
+ host: resolved.host,
182
+ database: resolved.database,
183
+ endpoint: resolved.endpoint,
184
+ user,
185
+ });
186
+ return true;
187
+ }
188
+ catch (error) {
189
+ logger.warn("could not resolve lakebase - the gate cache stays in memory; sessions and codes will not survive a restart", { error });
190
+ return false;
191
+ }
192
+ }
193
+ /**
194
+ * Boot the gate app and return the API the proxy calls. Throws when email is not
195
+ * in SMTP mode (no way to deliver a code) so a misconfigured gate fails fast at
196
+ * startup rather than silently accepting nobody; the caller may catch this and
197
+ * fall back to insecure/open mode when the operator opted in.
198
+ */
199
+ export async function startGateApp(config) {
200
+ // The host app's own brand (`branding/brand.yaml` discovered from cwd) or the
201
+ // dbx-tools default. This is the ONE brand source for the gate: it styles the
202
+ // code email (accent band, font, logo) via the email plugin AND supplies the
203
+ // display name the copy uses, so a deployment that themes its app themes its
204
+ // sign-in email with it. An explicit `brandName` still wins (see below).
205
+ const context = await loadBrandContext();
206
+ // Before `createApp`, because AppKit resolves the cache's storage during it and
207
+ // reads the connection out of `process.env` when it does. See the function's
208
+ // docs for why the gate resolves this itself.
209
+ await resolveCacheStorageEnv();
210
+ // `sendCode` delivers the OTP through the email plugin's SHARED transport, which
211
+ // the `email()` plugin primes during its `setup()` (awaited by `createApp`
212
+ // below). Using the module-level `sendEmail` avoids a circular dependency on the
213
+ // app handle's inferred type. The `From` is the SYSTEM sender: a verification
214
+ // code is machine-generated and unanswerable, so it must not arrive from a
215
+ // person's address inviting a reply.
216
+ const sendCode = async (to, code, opts) => {
217
+ const from = resolveSystemSenderAddress(getEmailRuntime().config);
218
+ await sendEmail({
219
+ to: [to],
220
+ // The code rides in the SUBJECT and the preheader, not just the body:
221
+ // those two strings are the whole of a push notification, and a
222
+ // notification is what mobile autofill reads. See `codeEmailSubject`.
223
+ subject: codeEmailSubject(code, opts.subject),
224
+ body: codeEmailHtmlBody(code, opts),
225
+ }, from, undefined, { text: codeEmailTextBody(code, opts), preview: codeEmailPreview(code, opts) });
226
+ };
227
+ // `createApp` namespaces each plugin's exports on the handle by manifest name;
228
+ // `handle.authGate` is the in-process gate API the proxy drives.
229
+ //
230
+ // `autoConfigure: false` because {@link resolveCacheStorageEnv} above already
231
+ // ran the one piece of it this app wants (`applyLakebaseEnv`), on this app's
232
+ // terms - no auto-create, and a warning instead of a throw.
233
+ const handle = await createApp({
234
+ autoConfigure: false,
235
+ plugins: [
236
+ // Brand the code email from the resolved context, the same bridge every
237
+ // other dbx-tools email surface uses (accent + font inlined, logo only when
238
+ // it is a fetchable URL - a package-export path cannot load in an inbox).
239
+ email({ brand: emailBrandFromContext(context) }),
240
+ // `brandName` falls back to the resolved context's display name, so the
241
+ // email copy names the app rather than a generic placeholder. Spelled as an
242
+ // explicit `??` rather than a key before `...config`: commander sets
243
+ // `brandName` on the options object whether or not the flag was passed, so
244
+ // spreading it would clobber the context name with `undefined`.
245
+ authGate({ ...config, brandName: config.brandName ?? context.name, sendCode }),
246
+ ],
247
+ });
248
+ // Fail fast: the gate needs SMTP to email codes. `getEmailRuntime()` resolves to
249
+ // `mode: "file"` (outbox) or throws when no SMTP creds are configured - neither
250
+ // can deliver to a real inbox, so refuse to bring up a gate that lets nobody in.
251
+ if (getEmailRuntime().config.mode !== "smtp") {
252
+ throw new Error("email is not configured for SMTP delivery - the OTP gate cannot send codes. " +
253
+ "Set SMTP_HOST/SMTP_USER/SMTP_PASSWORD, or pass --insecure / TUNNEL_INSECURE=true to run the tunnel open.");
254
+ }
255
+ logger.info("gate app ready (no server; proxy-driven)");
256
+ return handle.authGate;
257
+ }
258
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXBwLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2FwcC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztHQXlCRztBQUVILE9BQU8sRUFBRSxTQUFTLElBQUksV0FBVyxFQUFFLGdCQUFnQixFQUFFLE1BQU0sbUJBQW1CLENBQUM7QUFDL0UsT0FBTyxFQUFFLEtBQUssSUFBSSxTQUFTLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUNyRCxPQUFPLEVBQUUsS0FBSyxJQUFJLFVBQVUsRUFBRSxLQUFLLEVBQUUsTUFBTSxFQUFFLFNBQVMsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBQ2pGLE9BQU8sRUFBRSxHQUFHLEVBQUUsR0FBRyxFQUFFLE1BQU0sRUFBRSxNQUFNLHdCQUF3QixDQUFDO0FBQzFELE9BQU8sRUFBRSxRQUFRLEVBQStELE1BQU0sYUFBYSxDQUFDO0FBRXBHLE1BQU0sTUFBTSxHQUFHLEdBQUcsQ0FBQyxNQUFNLENBQUMsWUFBWSxDQUFDLENBQUM7QUFFeEM7Ozs7OztHQU1HO0FBQ0gsTUFBTSxVQUFVLFNBQVMsQ0FBQyxPQUFlO0lBQ3ZDLE9BQU8sT0FBTyxJQUFJLEVBQUUsSUFBSSxPQUFPLEdBQUcsRUFBRSxLQUFLLENBQUM7UUFDeEMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxTQUFTLENBQUMsT0FBTyxHQUFHLEVBQUUsRUFBRSxRQUFRLENBQUM7UUFDMUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxTQUFTLENBQUMsT0FBTyxFQUFFLFFBQVEsQ0FBQyxDQUFDO0FBQzFDLENBQUM7QUFLRCwrQ0FBK0M7QUFDL0MsTUFBTSxXQUFXLEdBQUcsOERBQThELENBQUM7QUFFbkY7OztHQUdHO0FBQ0gsTUFBTSxVQUFVLGlCQUFpQixDQUFDLElBQVksRUFBRSxJQUFjO0lBQzVELE9BQU87UUFDTCxJQUFJLENBQUMsT0FBTztRQUNaLEVBQUU7UUFDRixNQUFNLElBQUksRUFBRTtRQUNa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RSxJQUFJLENBQUMsRUFBRSxDQUMvRSxDQUFDO0lBQ0osQ0FBQyxDQUFDO0lBRUYsK0VBQStFO0lBQy9FLGlFQUFpRTtJQUNqRSxFQUFFO0lBQ0YsOEVBQThFO0lBQzlFLDZFQUE2RTtJQUM3RSw0REFBNEQ7SUFDNUQsTUFBTSxNQUFNLEdBQUcsTUFBTSxTQUFTLENBQUM7UUFDN0IsYUFBYSxFQUFFLEtBQUs7UUFDcEIsT0FBTyxFQUFFO1lBQ1Asd0VBQXdFO1lBQ3hFLDRFQUE0RTtZQUM1RSwwRUFBMEU7WUFDMUUsS0FBSyxDQUFDLEVBQUUsS0FBSyxFQUFFLHFCQUFxQixDQUFDLE9BQU8sQ0FBQyxFQUFFLENBQUM7WUFDaEQsd0VBQXdFO1lBQ3hFLDRFQUE0RTtZQUM1RSxxRUFBcUU7WUFDckUsMkVBQTJFO1lBQzNFLGdFQUFnRTtZQUNoRSxRQUFRLENBQUMsRUFBRSxHQUFHLE1BQU0sRUFBRSxTQUFTLEVBQUUsTUFBTSxDQUFDLFNBQVMsSUFBSSxPQUFPLENBQUMsSUFBSSxFQUFFLFFBQVEsRUFBRSxDQUFDO1NBQy9FO0tBQ0YsQ0FBQyxDQUFDO0lBRUgsaUZBQWlGO0lBQ2pGLGdGQUFnRjtJQUNoRixpRkFBaUY7SUFDakYsSUFBSSxlQUFlLEVBQUUsQ0FBQyxNQUFNLENBQUMsSUFBSSxLQUFLLE1BQU0sRUFBRSxDQUFDO1FBQzdDLE1BQU0sSUFBSSxLQUFLLENBQ2IsOEVBQThFO1lBQzVFLDBHQUEwRyxDQUM3RyxDQUFDO0lBQ0osQ0FBQztJQUVELE1BQU0sQ0FBQyxJQUFJLENBQUMsMENBQTBDLENBQUMsQ0FBQztJQUN4RCxPQUFPLE1BQU0sQ0FBQyxRQUF1QixDQUFDO0FBQ3hDLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIFRoZSB0dW5uZWwgZ2F0ZSdzIHRpbnkgQXBwS2l0IFwiYXBwXCIgLSBgY3JlYXRlQXBwYCBXSVRIT1VUIGEgYHNlcnZlcigpYCBwbHVnaW4uXG4gKlxuICogVGhlcmUgaXMgbm8gSFRUUCBzZXJ2ZXIgaGVyZTogdGhlIHR1bm5lbCBwcm94eSBpcyB0aGUgc2VydmVyLCBhbmQgaXQgY2FsbHMgdGhlXG4gKiBnYXRlIGhhbmRsZXJzIGluLXByb2Nlc3MuIGBjcmVhdGVBcHBgIGlzIHVzZWQgb25seSBmb3Igd2hhdCBpdCBhdXRvLXdpcmVzOlxuICogICAtIGBDYWNoZU1hbmFnZXJgIChMYWtlYmFzZSB3aGVuIHRoaXMgcHJvY2VzcyBjYW4gcmVhY2ggaXQsIGVsc2UgbWVtb3J5KSAtXG4gKiAgICAgd2hpY2ggdGhlIE9UUCBgQ29kZVN0b3JlYCBhbmQgdGhlIHNlc3Npb24gc2lnbmluZyBrZXkgdXNlIGZvciBzdG9yYWdlICsgVFRMXG4gKiAgICAgZXZpY3Rpb247XG4gKiAgIC0gdGhlIGBlbWFpbCgpYCB0cmFuc3BvcnQgKFNNVFAgLyBvdXRib3gpLCBwcmltZWQgZnJvbSBlbnYsIHNvIGBzZW5kQ29kZWAgY2FuXG4gKiAgICAgZGVsaXZlciB0aGUgb25lLXRpbWUgY29kZTtcbiAqICAgLSBkYngtdG9vbHMgYXV0by1jb25maWd1cmF0aW9uICsgYnJhbmRpbmcgdmlhIGBAZGJ4LXRvb2xzL2FwcGtpdGAuXG4gKlxuICogSXQgcmV0dXJucyB0aGUge0BsaW5rIEF1dGhHYXRlQXBpfSB0aGUgcHJveHkgZHJpdmVzLiBgc2VuZENvZGVgIGlzIHdpcmVkIGhlcmVcbiAqICh0aGUgcGx1Z2luIGNhbid0IHJlc29sdmUgSE9XIHRvIHNlbmQgb24gaXRzIG93bikgdG8gdGhlIGVtYWlsIHBsdWdpbidzXG4gKiB0cmFuc3BvcnQuIEEgc2lnbi1pbiBjb2RlIGlzIFNZU1RFTSBtYWlsIC0gbm8gb24tYmVoYWxmLW9mIHVzZXIgYXNrZWQgZm9yIGl0XG4gKiBhbmQgbm8gcmVwbHkgdG8gaXQgcmVhY2hlcyBhbnlvbmUgLSBzbyBpdCBzZW5kcyBmcm9tIHRoZSBlbWFpbCBjb25maWcnc1xuICogZG8tbm90LXJlcGx5IGFkZHJlc3MgKGBuby1yZXBseUBFTUFJTF9ET01BSU5gIHVubGVzcyBFTUFJTF9TWVNURU1fRlJPTSBuYW1lc1xuICogYW5vdGhlcikuXG4gKlxuICogRkFJTCBGQVNUOiBhIGdhdGUgdGhhdCBjYW4ndCBlbWFpbCBhIGNvZGUgaXMgdXNlbGVzcywgc28gaWYgZW1haWwgZG9lcyBub3RcbiAqIHJlc29sdmUgdG8gU01UUCBtb2RlIChyZWFsIGRlbGl2ZXJ5KSwgdGhpcyB0aHJvd3MgLSB1bmxlc3MgYGluc2VjdXJlYCBpcyBzZXRcbiAqIChgLS1pbnNlY3VyZWAgLyBgVFVOTkVMX0lOU0VDVVJFPXRydWVgKSwgaW4gd2hpY2ggY2FzZSB0aGUgY2FsbGVyIHJ1bnMgdGhlXG4gKiB0dW5uZWwgT1BFTiB3aXRoIG5vIGdhdGUuXG4gKlxuICogQG1vZHVsZVxuICovXG5cbmltcG9ydCB7IGNyZWF0ZUFwcCBhcyBjcmVhdGVBcHBOcywgbGFrZWJhc2VSZXNvbHZlciB9IGZyb20gXCJAZGJ4LXRvb2xzL2FwcGtpdFwiO1xuaW1wb3J0IHsgYnJhbmQgYXMgbm9kZUJyYW5kIH0gZnJvbSBcIkBkYngtdG9vbHMvY29yZVwiO1xuaW1wb3J0IHsgYnJhbmQgYXMgZW1haWxCcmFuZCwgZW1haWwsIHNlbmRlciwgdHJhbnNwb3J0IH0gZnJvbSBcIkBkYngtdG9vbHMvZW1haWxcIjtcbmltcG9ydCB7IGVudiwgbG9nLCBzdHJpbmcgfSBmcm9tIFwiQGRieC10b29scy9zaGFyZWQtY29yZVwiO1xuaW1wb3J0IHsgYXV0aEdhdGUsIHR5cGUgQXV0aEdhdGVBcGksIHR5cGUgQXV0aEdhdGVDb25maWcsIHR5cGUgU2VuZENvZGVPcHRpb25zIH0gZnJvbSBcIi4vcGx1Z2luLnRzXCI7XG5cbmNvbnN0IGxvZ2dlciA9IGxvZy5sb2dnZXIoXCJ0dW5uZWw6YXBwXCIpO1xuXG4vKipcbiAqIEEgY29kZSBUVEwgYXMgdGhlIHBsYWluIHBocmFzZSB0aGUgZW1haWwgc3RhdGVzIChcIjEwIG1pbnV0ZXNcIiwgXCI0NSBzZWNvbmRzXCIpLlxuICpcbiAqIFdob2xlIG1pbnV0ZXMgcmVhZCBhcyBtaW51dGVzOyBhbnl0aGluZyBlbHNlIHN0YXlzIGluIHNlY29uZHMgcmF0aGVyIHRoYW5cbiAqIHJvdW5kaW5nLCBzbyBhIDkwLXNlY29uZCBUVEwgaXMgbm90IGFkdmVydGlzZWQgYXMgXCIxIG1pbnV0ZVwiIGFuZCBhIHJlY2lwaWVudCBpc1xuICogbmV2ZXIgdG9sZCB0aGUgY29kZSBsaXZlcyBsb25nZXIgdGhhbiBpdCBkb2VzLlxuICovXG5leHBvcnQgZnVuY3Rpb24gZXhwaXJlc0luKHNlY29uZHM6IG51bWJlcik6IHN0cmluZyB7XG4gIHJldHVybiBzZWNvbmRzID49IDYwICYmIHNlY29uZHMgJSA2MCA9PT0gMFxuICAgID8gc3RyaW5nLnBsdXJhbGl6ZShzZWNvbmRzIC8gNjAsIFwibWludXRlXCIpXG4gICAgOiBzdHJpbmcucGx1cmFsaXplKHNlY29uZHMsIFwic2Vjb25kXCIpO1xufVxuXG4vKiogVGhlIHBhcnRzIG9mIHtAbGluayBTZW5kQ29kZU9wdGlvbnN9IHRoZSBjb2RlIGVtYWlsJ3MgY29weSBpcyBidWlsdCBmcm9tLiAqL1xudHlwZSBDb2RlQ29weSA9IFBpY2s8U2VuZENvZGVPcHRpb25zLCBcIm1lc3NhZ2VcIiB8IFwiY29kZVR0bFNlY29uZHNcIj47XG5cbi8qKiBUaGUgcmVhc3N1cmFuY2UgbGluZSBjbG9zaW5nIGJvdGggcGFydHMuICovXG5jb25zdCBJR05PUkVfTElORSA9IFwiSWYgeW91IGRpZCBub3QgcmVxdWVzdCB0aGlzIGNvZGUsIHlvdSBjYW4gaWdub3JlIHRoaXMgZW1haWwuXCI7XG5cbi8qKlxuICogVGhlIEhUTUwgcGFydCdzIHNvdXJjZTogdGhlIGZ1bGwgYnJhbmRlZCB0ZW1wbGF0ZSwgd2l0aCB0aGUgY29kZSBhcyBhIGxhcmdlXG4gKiBzdHlsZWQgaGVhZGluZyAoYCMjIGAgaXMgd2hhdCBtYWtlcyBpdCBwcm9taW5lbnQgaW4gYW4gaW5ib3gpLlxuICovXG5leHBvcnQgZnVuY3Rpb24gY29kZUVtYWlsSHRtbEJvZHkoY29kZTogc3RyaW5nLCBvcHRzOiBDb2RlQ29weSk6IHN0cmluZyB7XG4gIHJldHVybiBbXG4gICAgb3B0cy5tZXNzYWdlLFxuICAgIFwiXCIsXG4gICAgYCMjICR7Y29kZX1gLFxuICAgIFwiXCIsXG4gICAgYFRoaXMgY29kZSBleHBpcmVzIGluICR7ZXhwaXJlc0luKG9wdHMuY29kZVR0bFNlY29uZHMpfS5gLFxuICAgIFwiXCIsXG4gICAgSUdOT1JFX0xJTkUsXG4gIF0uam9pbihcIlxcblwiKTtcbn1cblxuLyoqXG4gKiBUaGUgYHRleHQvcGxhaW5gIHBhcnQsIHN1cHBsaWVkIEVYUExJQ0lUTFkgcmF0aGVyIHRoYW4gcmVuZGVyZWQgZnJvbSB0aGUgdHJlZVxuICogYWJvdmUuIEJvdGggcGFydHMgc2F5IHRoZSBzYW1lIHRoaW5nOyBvbmx5IHRoZSBsaW5lIGxheW91dCBkaWZmZXJzLlxuICpcbiAqIFRoZSBwcm9tcHQgYW5kIHRoZSBjb2RlIHNoYXJlIE9ORSBsaW5lIChcIllvdXIgdmVyaWZpY2F0aW9uIGNvZGUgaXM6IDEyMzQ1NlwiKS5cbiAqIFRoYXQgc2luZ2xlLWxpbmUgc2hhcGUgaXMgd2hhdCBpT1MsIEdtYWlsLCBPdXRsb29rLCBhbmQgQW5kcm9pZCBjb2RlIGRldGVjdGlvblxuICoga2V5cyBvbiBtb3N0IHJlbGlhYmx5IC0gdGhlIGhldXJpc3RpY3MgbG9vayBmb3IgYSBjb2RlIGluIHRoZSBzYW1lIHNlbnRlbmNlIGFzXG4gKiBhIHJlY29nbml6ZWQgcHJvbXB0LCBzbyBzcGxpdHRpbmcgdGhlbSBhY3Jvc3MgbGluZXMgbWFrZXMgZGV0ZWN0aW9uIGRlcGVuZGVudFxuICogb24gdGhlIGNsaWVudCwgYW5kIGFueSBibGFuayBsaW5lIGJldHdlZW4gdGhlbSBkZWZlYXRzIGl0IG91dHJpZ2h0LlxuICpcbiAqIFRoZSBHRU5FUkFURUQgdGV4dCBwYXJ0IGNhbm5vdCBob2xkIHRoYXQgc2hhcGUgYXQgYWxsOiBpdCBpcyBhIHJlbmRlcmluZyBvZiB0aGVcbiAqIEhUTUwsIHNvIGl0IGNhcnJpZXMgdGhlIGJyYW5kIGhlYWRlci9mb290ZXIgYW5kIHR1cm5zIHRoZSBjb2RlIGhlYWRpbmcncyBDU1NcbiAqIG1hcmdpbiBpbnRvIGJsYW5rIGxpbmVzLCBhcnJpdmluZyBhcyBgcHJvbXB0XFxuXFxuXFxuY29kZWAuXG4gKlxuICogVGhlIGNvZGUgaXMgdmlzaWJsZSB0ZXh0IGluIEJPVEggcGFydHMsIG5ldmVyIGFuIGltYWdlLCBzbyBhIGNsaWVudCBzY3JhcGluZ1xuICogZWl0aGVyIG9uZSBmaW5kcyBpdC4gTm8gdHJhaWxlciBsaW5lIGZvbGxvd3MgdGhlIGNvcHkgLSBBcHBsZSdzIGRvbWFpbi1ib3VuZFxuICogYEBkb21haW4gI2NvZGVgIGZvb3RlciBpcyBkZWxpYmVyYXRlbHkgTk9UIGVtaXR0ZWQsIHNpbmNlIGl0IGNvbnN0cmFpbnMgdGhlXG4gKiBjb2RlIHRvIG9uZSBvcmlnaW4gYW5kIGlzIG5vdCB3aGF0IHRoZSBicm9hZGx5LWNvbXBhdGlibGUgc2hhcGUgbmVlZHMuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBjb2RlRW1haWxUZXh0Qm9keShjb2RlOiBzdHJpbmcsIG9wdHM6IENvZGVDb3B5KTogc3RyaW5nIHtcbiAgcmV0dXJuIFtcbiAgICBgJHtvcHRzLm1lc3NhZ2V9ICR7Y29kZX1gLFxuICAgIGBUaGlzIGNvZGUgZXhwaXJlcyBpbiAke2V4cGlyZXNJbihvcHRzLmNvZGVUdGxTZWNvbmRzKX0uYCxcbiAgICBcIlwiLFxuICAgIElHTk9SRV9MSU5FLFxuICBdLmpvaW4oXCJcXG5cIik7XG59XG5cbi8qKlxuICogVGhlIFNVQkpFQ1QgbGluZSwgd2l0aCB0aGUgY29kZSBpbiBpdDogYFwiMTIzNDU2IGlzIHlvdXIgdmVyaWZpY2F0aW9uIGNvZGVcImAuXG4gKlxuICogVGhlIGNvZGUgaGFzIHRvIGJlIGhlcmUsIG5vdCBvbmx5IGluIHRoZSBib2R5LCBiZWNhdXNlIG9mIHdoYXQgbW9iaWxlIGF1dG9maWxsXG4gKiBhY3R1YWxseSByZWFkcy4gaU9TIG9mZmVycyBhIGNvZGUgZnJvbSBhbiBpbmNvbWluZyBOT1RJRklDQVRJT04gLSBuYXRpdmVseSBmb3JcbiAqIE1lc3NhZ2VzIGFuZCBNYWlsLCBhbmQgc2luY2UgaU9TIDI2IGZvciBhbnkgYXBwJ3Mgbm90aWZpY2F0aW9uIHRleHQsIHdoaWNoIGlzXG4gKiB3aGF0IGZpbmFsbHkgbWFkZSBHbWFpbCB3b3JrIC0gYW5kIGEgbm90aWZpY2F0aW9uIGNvbnRhaW5zIHRoZSBzZW5kZXIsIHRoZVxuICogc3ViamVjdCwgYW5kIGEgc2hvcnQgc25pcHBldC4gTm90aGluZyBlbHNlLiBBIGNvZGUgdGhhdCBsaXZlcyBpbiB0aGUgYm9keSBpc1xuICogaW52aXNpYmxlIHRvIGl0LCBob3dldmVyIGNsZWFubHkgdGhlIGJvZHkgaXMgZm9ybWF0dGVkLCB3aGljaCBpcyB3aHkgYSBwZXJmZWN0bHlcbiAqIHNoYXBlZCBgdGV4dC9wbGFpbmAgcGFydCBzdGlsbCBwcm9kdWNlZCBubyBhdXRvZmlsbCBwcm9tcHQgaW4gR21haWwuXG4gKlxuICogYDxjb2RlPiBpcyB5b3VyIDx0aGluZz5gIHJhdGhlciB0aGFuIGA8dGhpbmc+OiA8Y29kZT5gIGJlY2F1c2UgdGhlIGxlYWRpbmcgY29kZVxuICogc3Vydml2ZXMgVFJVTkNBVElPTjogYSBub3RpZmljYXRpb24gYW5kIGFuIGluYm94IGxpc3QgYm90aCBjdXQgdGhlIHN1YmplY3QsIGFuZFxuICogdGhlIHBsYXRmb3JtIGhldXJpc3RpY3Mgd2FudCB0aGUgY29kZSBpbiB0aGUgc2FtZSBzZW50ZW5jZSBhcyBhIHJlY29nbml6ZWRcbiAqIHByb21wdCAoXCJjb2RlXCIsIFwidmVyaWZpY2F0aW9uXCIpLiBQdXR0aW5nIGl0IGZpcnN0IGtlZXBzIGJvdGggaW50YWN0IG5vIG1hdHRlclxuICogd2hlcmUgdGhlIGN1dCBsYW5kcy5cbiAqXG4gKiBgc3ViamVjdGAgaXMgdGhlIGNvbmZpZ3VyZWQgbGluZSAoXCJZb3VyIHZlcmlmaWNhdGlvbiBjb2RlXCIpLCBsb3dlci1jYXNlZCBhdCBpdHNcbiAqIGZpcnN0IHdvcmQgc28gdGhlIHNlbnRlbmNlIHJlYWRzIG5hdHVyYWxseSwgYW5kIGxlZnQgQUxPTkUgd2hlbiBpdCBkb2VzIG5vdCBsb29rXG4gKiBsaWtlIHRoZSBjb252ZW50aW9uYWwgcGhyYXNpbmcgLSBhbiBvcGVyYXRvciB3aG8gc2V0IGEgZGVsaWJlcmF0ZSBzdWJqZWN0IGdldHNcbiAqIHRoZWlycyB3aXRoIHRoZSBjb2RlIHByZWZpeGVkLCBub3QgYSBtYW5nbGVkIGh5YnJpZC5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGNvZGVFbWFpbFN1YmplY3QoY29kZTogc3RyaW5nLCBzdWJqZWN0OiBzdHJpbmcpOiBzdHJpbmcge1xuICBjb25zdCB0cmltbWVkID0gc3ViamVjdC50cmltKCk7XG4gIGNvbnN0IGNvbnZlbnRpb25hbCA9IC9eeW91clxccysvaS5leGVjKHRyaW1tZWQpO1xuICBjb25zdCByZXN0ID0gY29udmVudGlvbmFsID8gdHJpbW1lZC5zbGljZShjb252ZW50aW9uYWxbMF0ubGVuZ3RoKSA6IHRyaW1tZWQ7XG4gIHJldHVybiBjb252ZW50aW9uYWwgPyBgJHtjb2RlfSBpcyB5b3VyICR7cmVzdH1gIDogYCR7Y29kZX0gLSAke3RyaW1tZWR9YDtcbn1cblxuLyoqXG4gKiBUaGUgUFJFSEVBREVSOiB0aGUgc25pcHBldCBiZXNpZGUgdGhlIHN1YmplY3QgaW4gYW4gaW5ib3ggbGlzdCwgYW5kIHRoZSBib2R5IG9mXG4gKiB0aGUgcHVzaCBub3RpZmljYXRpb24uIENhcnJpZXMgdGhlIGNvZGUgZm9yIHRoZSBzYW1lIHJlYXNvbiB0aGUgc3ViamVjdCBkb2VzIC1cbiAqIGl0IGlzIHRoZSBvdGhlciBoYWxmIG9mIHdoYXQgYSBub3RpZmljYXRpb24gc2hvd3MgLSBhbmQgcmVwZWF0cyB0aGUgcHJvbXB0XG4gKiB3b3JkaW5nIHNvIGEgaGV1cmlzdGljIHNjYW5uaW5nIHRoZSBzbmlwcGV0IGFsb25lIGZpbmRzIGEgY29kZSBuZXh0IHRvIGEgcGhyYXNlXG4gKiBpdCByZWNvZ25pemVzLlxuICovXG5leHBvcnQgZnVuY3Rpb24gY29kZUVtYWlsUHJldmlldyhjb2RlOiBzdHJpbmcsIG9wdHM6IENvZGVDb3B5KTogc3RyaW5nIHtcbiAgcmV0dXJuIGAke29wdHMubWVzc2FnZX0gJHtjb2RlfWA7XG59XG5cbmNvbnN0IHsgY3JlYXRlQXBwIH0gPSBjcmVhdGVBcHBOcztcbmNvbnN0IHsgYXBwbHlMYWtlYmFzZUVudiB9ID0gbGFrZWJhc2VSZXNvbHZlcjtcbmNvbnN0IHsgcmVzb2x2ZVN5c3RlbVNlbmRlckFkZHJlc3MgfSA9IHNlbmRlcjtcbmNvbnN0IHsgZ2V0RW1haWxSdW50aW1lLCBzZW5kRW1haWwgfSA9IHRyYW5zcG9ydDtcbmNvbnN0IHsgZW1haWxCcmFuZEZyb21Db250ZXh0IH0gPSBlbWFpbEJyYW5kO1xuY29uc3QgeyBsb2FkQnJhbmRDb250ZXh0IH0gPSBub2RlQnJhbmQ7XG5cbi8qKlxuICogRW52IHZhcnMgd2hvc2UgcHJlc2VuY2UgbWVhbnMgYSBMYWtlYmFzZSBkYXRhYmFzZSB3YXMgYm91bmQgdG8gdGhpcyBkZXBsb3ltZW50LlxuICogYExBS0VCQVNFX0VORFBPSU5UYCBpcyB3aGF0IGEgRGF0YWJyaWNrcyBBcHAgYHBvc3RncmVzYCByZXNvdXJjZSBiaW5kaW5nIHNldHM7XG4gKiBgUEdIT1NUYCBpcyB0aGUgbG9jYWwvbWFudWFsIHNwZWxsaW5nLlxuICovXG5jb25zdCBMQUtFQkFTRV9FTlYgPSBbXCJMQUtFQkFTRV9FTkRQT0lOVFwiLCBcIlBHSE9TVFwiXSBhcyBjb25zdDtcblxuLyoqXG4gKiBVcHBlciBib3VuZCBvbiByZXNvbHZpbmcgdGhlIExha2ViYXNlIGNvbm5lY3Rpb24uIFRoZSBnYXRlIGlzIGluIHRoZSByZXF1ZXN0XG4gKiBwYXRoIGZvciBldmVyeSB2aXNpdG9yLCBzbyBhIHNsb3cgd29ya3NwYWNlIEFQSSBtdXN0IG5vdCBob2xkIHVwIGJvb3RcbiAqIGluZGVmaW5pdGVseTsgYSBtZW1vcnkgY2FjaGUgaXMgYSBkZWdyYWRlZCBnYXRlLCBhbiB1bmJvb3RlZCBvbmUgaXMgbm8gZ2F0ZS5cbiAqL1xuY29uc3QgTEFLRUJBU0VfUkVTT0xWRV9USU1FT1VUX01TID0gNjBfMDAwO1xuXG4vKipcbiAqIEZpbGwgaW4gdGhlIExha2ViYXNlIGNvbm5lY3Rpb24gZW52IEFwcEtpdCdzIGNhY2hlIG5lZWRzLCBzbyBgQ2FjaGVNYW5hZ2VyYFxuICogY2hvb3NlcyBQRVJTSVNURU5UIHN0b3JhZ2UgaW5zdGVhZCBvZiBtZW1vcnkuXG4gKlxuICogVGhpcyBpcyB3aGF0IG1ha2VzIHRoZSBnYXRlJ3Mgc2Vzc2lvbiBzaWduaW5nIGtleSBhbmQgb3V0c3RhbmRpbmcgb25lLXRpbWVcbiAqIGNvZGVzIHN1cnZpdmUgYSByZXN0YXJ0LiBgYXBwbHlMYWtlYmFzZUVudmAgaXMgdGhlIFNIQVJFRCBoZWxwZXIgQXBwS2l0XG4gKiBhdXRvLWNvbmZpZ3VyYXRpb24gdXNlcywgc28gdGhlIGdhdGUgZ2V0cyBleGFjdGx5IHRoZSBlbnYgYSBwb29sIG5lZWRzIC1cbiAqIGBMQUtFQkFTRV9FTkRQT0lOVGAsIGBQR0hPU1RgLCBgUEdEQVRBQkFTRWAsIGFuZCBgUEdVU0VSYCAtIHJhdGhlciB0aGFuIGFcbiAqIGhhbmQtcm9sbGVkIHN1YnNldC4gQWxsIGZvdXIgbWF0dGVyOiBgY3JlYXRlTGFrZWJhc2VQb29sKClgIHRocm93cyB3aXRob3V0IGFueVxuICogb25lIG9mIHRoZW0sIGFuZCBhIERhdGFicmlja3MgQXBwIGBwb3N0Z3Jlc2AgcmVzb3VyY2UgYmluZGluZyBzdXBwbGllcyBvbmx5IHRoZVxuICogZmlyc3QuIFdpdGhvdXQgdGhlbSB0aGUgcG9vbCBjYW5ub3QgYmUgYnVpbHQsIHRoZSBjYWNoZSBzaWxlbnRseSBkZWdyYWRlcyB0b1xuICogaW4tbWVtb3J5LCBhbmQgZXZlcnkgcmVkZXBsb3kgc2lnbnMgb3V0IGV2ZXJ5IHVzZXIgKHRoZSBleGFjdCBzeW1wdG9tIHRoaXNcbiAqIGV4aXN0cyB0byBwcmV2ZW50KS5cbiAqXG4gKiBJdCBpcyBjYWxsZWQgaGVyZSByYXRoZXIgdGhhbiBsZWZ0IHRvIGBjcmVhdGVBcHBgJ3MgYGF1dG9Db25maWd1cmVgIGJlY2F1c2UgdGhhdFxuICogZ2F0ZXMgb24gYSBgbGFrZWJhc2UoKWAgcGx1Z2luIGJlaW5nIHJlZ2lzdGVyZWQgLSBhbmQgdGhpcyBhcHAgcmVnaXN0ZXJzIG5vbmUsXG4gKiBoYXZpbmcgbm8gc2VydmVyIHRvIG1vdW50IExha2ViYXNlIHJvdXRlcyBvbi4gQ2FsbGluZyB0aGUgaGVscGVyIGRpcmVjdGx5IGFsc29cbiAqIGxldHMgdGhlIGdhdGUgYmUgc3RyaWN0ZXIgdGhhbiBgYXV0b0NvbmZpZ3VyZWAgaXM6XG4gKlxuICogICAtIEl0IHJ1bnMgT05MWSB3aGVuIGEgTGFrZWJhc2UgZW52IHZhciBpcyBwcmVzZW50LiBBIHR1bm5lbCBpcyBhIHdyYXBwZXJcbiAqICAgICBhcm91bmQgc29tZW9uZSBlbHNlJ3MgYXBwIGFuZCBtdXN0IG5vdCBpbnZlbnQgaW5mcmFzdHJ1Y3R1cmUsIHNvIHdpdGhcbiAqICAgICBub3RoaW5nIGJvdW5kIGl0IHNraXBzIHJhdGhlciB0aGFuIGZhbGxpbmcgdGhyb3VnaCB0aGUgcmVzb2x2ZXInc1xuICogICAgIGxpc3Qtb3ItQ1JFQVRFLWEtcHJvamVjdCBwYXRoLlxuICogICAtIGBhdXRvQ3JlYXRlOiBmYWxzZWAgZm9yIHRoZSBzYW1lIHJlYXNvbiwgaW4gY2FzZSBhIHByb2plY3QgaGFwcGVucyB0byBleGlzdC5cbiAqICAgLSBFdmVyeSBmYWlsdXJlIGlzIGEgV0FSTklORywgbmV2ZXIgYSB0aHJvdy4gVGhlIGNhY2hlIGlzIGFuIG9wdGltaXphdGlvbiBmb3JcbiAqICAgICBzZXNzaW9uIGR1cmFiaWxpdHk7IGFkbWlzc2lvbiBzdGlsbCByZXF1aXJlcyBhIGNvZGUgZGVsaXZlcmVkIHRvIGFuXG4gKiAgICAgYWxsb3ctbGlzdGVkIGFkZHJlc3MsIHNvIGEgZ2F0ZSB3aXRoIGEgbWVtb3J5IGNhY2hlIGlzIHNhZmUsIGp1c3QgZm9yZ2V0ZnVsLlxuICovXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gcmVzb2x2ZUNhY2hlU3RvcmFnZUVudigpOiBQcm9taXNlPGJvb2xlYW4+IHtcbiAgaWYgKCFlbnYudGV4dChMQUtFQkFTRV9FTlYpKSB7XG4gICAgbG9nZ2VyLmluZm8oXG4gICAgICBgbm8gJHtlbnYubmFtZShMQUtFQkFTRV9FTlYpfSAtIHRoZSBnYXRlIGNhY2hlIHN0YXlzIGluIG1lbW9yeTsgc2Vzc2lvbnMgYW5kIGNvZGVzIHdpbGwgbm90IHN1cnZpdmUgYSByZXN0YXJ0YCxcbiAgICApO1xuICAgIHJldHVybiBmYWxzZTtcbiAgfVxuICB0cnkge1xuICAgIGNvbnN0IHsgcmVzb2x2ZWQsIHVzZXIgfSA9IGF3YWl0IGFwcGx5TGFrZWJhc2VFbnYoXG4gICAgICB7IGF1dG9DcmVhdGU6IGZhbHNlIH0sXG4gICAgICBBYm9ydFNpZ25hbC50aW1lb3V0KExBS0VCQVNFX1JFU09MVkVfVElNRU9VVF9NUyksXG4gICAgKTtcbiAgICBsb2dnZXIuaW5mbyhcImxha2ViYXNlIHJlc29sdmVkIGZvciB0aGUgZ2F0ZSBjYWNoZVwiLCB7XG4gICAgICBob3N0OiByZXNvbHZlZC5ob3N0LFxuICAgICAgZGF0YWJhc2U6IHJlc29sdmVkLmRhdGFiYXNlLFxuICAgICAgZW5kcG9pbnQ6IHJlc29sdmVkLmVuZHBvaW50LFxuICAgICAgdXNlcixcbiAgICB9KTtcbiAgICByZXR1cm4gdHJ1ZTtcbiAgfSBjYXRjaCAoZXJyb3IpIHtcbiAgICBsb2dnZXIud2FybihcbiAgICAgIFwiY291bGQgbm90IHJlc29sdmUgbGFrZWJhc2UgLSB0aGUgZ2F0ZSBjYWNoZSBzdGF5cyBpbiBtZW1vcnk7IHNlc3Npb25zIGFuZCBjb2RlcyB3aWxsIG5vdCBzdXJ2aXZlIGEgcmVzdGFydFwiLFxuICAgICAgeyBlcnJvciB9LFxuICAgICk7XG4gICAgcmV0dXJuIGZhbHNlO1xuICB9XG59XG5cbi8qKlxuICogQm9vdCB0aGUgZ2F0ZSBhcHAgYW5kIHJldHVybiB0aGUgQVBJIHRoZSBwcm94eSBjYWxscy4gVGhyb3dzIHdoZW4gZW1haWwgaXMgbm90XG4gKiBpbiBTTVRQIG1vZGUgKG5vIHdheSB0byBkZWxpdmVyIGEgY29kZSkgc28gYSBtaXNjb25maWd1cmVkIGdhdGUgZmFpbHMgZmFzdCBhdFxuICogc3RhcnR1cCByYXRoZXIgdGhhbiBzaWxlbnRseSBhY2NlcHRpbmcgbm9ib2R5OyB0aGUgY2FsbGVyIG1heSBjYXRjaCB0aGlzIGFuZFxuICogZmFsbCBiYWNrIHRvIGluc2VjdXJlL29wZW4gbW9kZSB3aGVuIHRoZSBvcGVyYXRvciBvcHRlZCBpbi5cbiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIHN0YXJ0R2F0ZUFwcChjb25maWc6IEF1dGhHYXRlQ29uZmlnKTogUHJvbWlzZTxBdXRoR2F0ZUFwaT4ge1xuICAvLyBUaGUgaG9zdCBhcHAncyBvd24gYnJhbmQgKGBicmFuZGluZy9icmFuZC55YW1sYCBkaXNjb3ZlcmVkIGZyb20gY3dkKSBvciB0aGVcbiAgLy8gZGJ4LXRvb2xzIGRlZmF1bHQuIFRoaXMgaXMgdGhlIE9ORSBicmFuZCBzb3VyY2UgZm9yIHRoZSBnYXRlOiBpdCBzdHlsZXMgdGhlXG4gIC8vIGNvZGUgZW1haWwgKGFjY2VudCBiYW5kLCBmb250LCBsb2dvKSB2aWEgdGhlIGVtYWlsIHBsdWdpbiBBTkQgc3VwcGxpZXMgdGhlXG4gIC8vIGRpc3BsYXkgbmFtZSB0aGUgY29weSB1c2VzLCBzbyBhIGRlcGxveW1lbnQgdGhhdCB0aGVtZXMgaXRzIGFwcCB0aGVtZXMgaXRzXG4gIC8vIHNpZ24taW4gZW1haWwgd2l0aCBpdC4gQW4gZXhwbGljaXQgYGJyYW5kTmFtZWAgc3RpbGwgd2lucyAoc2VlIGJlbG93KS5cbiAgY29uc3QgY29udGV4dCA9IGF3YWl0IGxvYWRCcmFuZENvbnRleHQoKTtcblxuICAvLyBCZWZvcmUgYGNyZWF0ZUFwcGAsIGJlY2F1c2UgQXBwS2l0IHJlc29sdmVzIHRoZSBjYWNoZSdzIHN0b3JhZ2UgZHVyaW5nIGl0IGFuZFxuICAvLyByZWFkcyB0aGUgY29ubmVjdGlvbiBvdXQgb2YgYHByb2Nlc3MuZW52YCB3aGVuIGl0IGRvZXMuIFNlZSB0aGUgZnVuY3Rpb24nc1xuICAvLyBkb2NzIGZvciB3aHkgdGhlIGdhdGUgcmVzb2x2ZXMgdGhpcyBpdHNlbGYuXG4gIGF3YWl0IHJlc29sdmVDYWNoZVN0b3JhZ2VFbnYoKTtcblxuICAvLyBgc2VuZENvZGVgIGRlbGl2ZXJzIHRoZSBPVFAgdGhyb3VnaCB0aGUgZW1haWwgcGx1Z2luJ3MgU0hBUkVEIHRyYW5zcG9ydCwgd2hpY2hcbiAgLy8gdGhlIGBlbWFpbCgpYCBwbHVnaW4gcHJpbWVzIGR1cmluZyBpdHMgYHNldHVwKClgIChhd2FpdGVkIGJ5IGBjcmVhdGVBcHBgXG4gIC8vIGJlbG93KS4gVXNpbmcgdGhlIG1vZHVsZS1sZXZlbCBgc2VuZEVtYWlsYCBhdm9pZHMgYSBjaXJjdWxhciBkZXBlbmRlbmN5IG9uIHRoZVxuICAvLyBhcHAgaGFuZGxlJ3MgaW5mZXJyZWQgdHlwZS4gVGhlIGBGcm9tYCBpcyB0aGUgU1lTVEVNIHNlbmRlcjogYSB2ZXJpZmljYXRpb25cbiAgLy8gY29kZSBpcyBtYWNoaW5lLWdlbmVyYXRlZCBhbmQgdW5hbnN3ZXJhYmxlLCBzbyBpdCBtdXN0IG5vdCBhcnJpdmUgZnJvbSBhXG4gIC8vIHBlcnNvbidzIGFkZHJlc3MgaW52aXRpbmcgYSByZXBseS5cbiAgY29uc3Qgc2VuZENvZGU6IE5vbk51bGxhYmxlPEF1dGhHYXRlQ29uZmlnW1wic2VuZENvZGVcIl0+ID0gYXN5bmMgKHRvLCBjb2RlLCBvcHRzKSA9PiB7XG4gICAgY29uc3QgZnJvbSA9IHJlc29sdmVTeXN0ZW1TZW5kZXJBZGRyZXNzKGdldEVtYWlsUnVudGltZSgpLmNvbmZpZyk7XG4gICAgYXdhaXQgc2VuZEVtYWlsKFxuICAgICAge1xuICAgICAgICB0bzogW3RvXSxcbiAgICAgICAgLy8gVGhlIGNvZGUgcmlkZXMgaW4gdGhlIFNVQkpFQ1QgYW5kIHRoZSBwcmVoZWFkZXIsIG5vdCBqdXN0IHRoZSBib2R5OlxuICAgICAgICAvLyB0aG9zZSB0d28gc3RyaW5ncyBhcmUgdGhlIHdob2xlIG9mIGEgcHVzaCBub3RpZmljYXRpb24sIGFuZCBhXG4gICAgICAgIC8vIG5vdGlmaWNhdGlvbiBpcyB3aGF0IG1vYmlsZSBhdXRvZmlsbCByZWFkcy4gU2VlIGBjb2RlRW1haWxTdWJqZWN0YC5cbiAgICAgICAgc3ViamVjdDogY29kZUVtYWlsU3ViamVjdChjb2RlLCBvcHRzLnN1YmplY3QpLFxuICAgICAgICBib2R5OiBjb2RlRW1haWxIdG1sQm9keShjb2RlLCBvcHRzKSxcbiAgICAgIH0sXG4gICAgICBmcm9tLFxuICAgICAgdW5kZWZpbmVkLFxuICAgICAgeyB0ZXh0OiBjb2RlRW1haWxUZXh0Qm9keShjb2RlLCBvcHRzKSwgcHJldmlldzogY29kZUVtYWlsUHJldmlldyhjb2RlLCBvcHRzKSB9LFxuICAgICk7XG4gIH07XG5cbiAgLy8gYGNyZWF0ZUFwcGAgbmFtZXNwYWNlcyBlYWNoIHBsdWdpbidzIGV4cG9ydHMgb24gdGhlIGhhbmRsZSBieSBtYW5pZmVzdCBuYW1lO1xuICAvLyBgaGFuZGxlLmF1dGhHYXRlYCBpcyB0aGUgaW4tcHJvY2VzcyBnYXRlIEFQSSB0aGUgcHJveHkgZHJpdmVzLlxuICAvL1xuICAvLyBgYXV0b0NvbmZpZ3VyZTogZmFsc2VgIGJlY2F1c2Uge0BsaW5rIHJlc29sdmVDYWNoZVN0b3JhZ2VFbnZ9IGFib3ZlIGFscmVhZHlcbiAgLy8gcmFuIHRoZSBvbmUgcGllY2Ugb2YgaXQgdGhpcyBhcHAgd2FudHMgKGBhcHBseUxha2ViYXNlRW52YCksIG9uIHRoaXMgYXBwJ3NcbiAgLy8gdGVybXMgLSBubyBhdXRvLWNyZWF0ZSwgYW5kIGEgd2FybmluZyBpbnN0ZWFkIG9mIGEgdGhyb3cuXG4gIGNvbnN0IGhhbmRsZSA9IGF3YWl0IGNyZWF0ZUFwcCh7XG4gICAgYXV0b0NvbmZpZ3VyZTogZmFsc2UsXG4gICAgcGx1Z2luczogW1xuICAgICAgLy8gQnJhbmQgdGhlIGNvZGUgZW1haWwgZnJvbSB0aGUgcmVzb2x2ZWQgY29udGV4dCwgdGhlIHNhbWUgYnJpZGdlIGV2ZXJ5XG4gICAgICAvLyBvdGhlciBkYngtdG9vbHMgZW1haWwgc3VyZmFjZSB1c2VzIChhY2NlbnQgKyBmb250IGlubGluZWQsIGxvZ28gb25seSB3aGVuXG4gICAgICAvLyBpdCBpcyBhIGZldGNoYWJsZSBVUkwgLSBhIHBhY2thZ2UtZXhwb3J0IHBhdGggY2Fubm90IGxvYWQgaW4gYW4gaW5ib3gpLlxuICAgICAgZW1haWwoeyBicmFuZDogZW1haWxCcmFuZEZyb21Db250ZXh0KGNvbnRleHQpIH0pLFxuICAgICAgLy8gYGJyYW5kTmFtZWAgZmFsbHMgYmFjayB0byB0aGUgcmVzb2x2ZWQgY29udGV4dCdzIGRpc3BsYXkgbmFtZSwgc28gdGhlXG4gICAgICAvLyBlbWFpbCBjb3B5IG5hbWVzIHRoZSBhcHAgcmF0aGVyIHRoYW4gYSBnZW5lcmljIHBsYWNlaG9sZGVyLiBTcGVsbGVkIGFzIGFuXG4gICAgICAvLyBleHBsaWNpdCBgPz9gIHJhdGhlciB0aGFuIGEga2V5IGJlZm9yZSBgLi4uY29uZmlnYDogY29tbWFuZGVyIHNldHNcbiAgICAgIC8vIGBicmFuZE5hbWVgIG9uIHRoZSBvcHRpb25zIG9iamVjdCB3aGV0aGVyIG9yIG5vdCB0aGUgZmxhZyB3YXMgcGFzc2VkLCBzb1xuICAgICAgLy8gc3ByZWFkaW5nIGl0IHdvdWxkIGNsb2JiZXIgdGhlIGNvbnRleHQgbmFtZSB3aXRoIGB1bmRlZmluZWRgLlxuICAgICAgYXV0aEdhdGUoeyAuLi5jb25maWcsIGJyYW5kTmFtZTogY29uZmlnLmJyYW5kTmFtZSA/PyBjb250ZXh0Lm5hbWUsIHNlbmRDb2RlIH0pLFxuICAgIF0sXG4gIH0pO1xuXG4gIC8vIEZhaWwgZmFzdDogdGhlIGdhdGUgbmVlZHMgU01UUCB0byBlbWFpbCBjb2Rlcy4gYGdldEVtYWlsUnVudGltZSgpYCByZXNvbHZlcyB0b1xuICAvLyBgbW9kZTogXCJmaWxlXCJgIChvdXRib3gpIG9yIHRocm93cyB3aGVuIG5vIFNNVFAgY3JlZHMgYXJlIGNvbmZpZ3VyZWQgLSBuZWl0aGVyXG4gIC8vIGNhbiBkZWxpdmVyIHRvIGEgcmVhbCBpbmJveCwgc28gcmVmdXNlIHRvIGJyaW5nIHVwIGEgZ2F0ZSB0aGF0IGxldHMgbm9ib2R5IGluLlxuICBpZiAoZ2V0RW1haWxSdW50aW1lKCkuY29uZmlnLm1vZGUgIT09IFwic210cFwiKSB7XG4gICAgdGhyb3cgbmV3IEVycm9yKFxuICAgICAgXCJlbWFpbCBpcyBub3QgY29uZmlndXJlZCBmb3IgU01UUCBkZWxpdmVyeSAtIHRoZSBPVFAgZ2F0ZSBjYW5ub3Qgc2VuZCBjb2Rlcy4gXCIgK1xuICAgICAgICBcIlNldCBTTVRQX0hPU1QvU01UUF9VU0VSL1NNVFBfUEFTU1dPUkQsIG9yIHBhc3MgLS1pbnNlY3VyZSAvIFRVTk5FTF9JTlNFQ1VSRT10cnVlIHRvIHJ1biB0aGUgdHVubmVsIG9wZW4uXCIsXG4gICAgKTtcbiAgfVxuXG4gIGxvZ2dlci5pbmZvKFwiZ2F0ZSBhcHAgcmVhZHkgKG5vIHNlcnZlcjsgcHJveHktZHJpdmVuKVwiKTtcbiAgcmV0dXJuIGhhbmRsZS5hdXRoR2F0ZSBhcyBBdXRoR2F0ZUFwaTtcbn1cbiJdfQ==
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Environment-variable names the tunnel reads.
3
+ *
4
+ * Every setting is `TUNNEL_`-prefixed, matching the repo convention of naming a
5
+ * variable after the package that owns it (`MASTRA_*`, `TEAMS_*`,
6
+ * `WEB_SEARCH_*`). The gate's original names were unprefixed (`AUTH_SUBJECT`,
7
+ * `PUBLIC_DOMAIN`, ...), which is a real hazard for a package that runs as a
8
+ * WRAPPER: the tunnel and the app it wraps share one environment, so a generic
9
+ * name is one the wrapped app may already use for something else, and
10
+ * `PUBLIC_DOMAIN` in particular reads like an app-wide setting rather than a
11
+ * portr detail. `EMAIL_AUTH_ALLOW` was worse than generic - it sat in
12
+ * `@dbx-tools/email`'s `EMAIL_*` namespace while configuring the gate, not email.
13
+ *
14
+ * Each entry is an {@link EnvKey} list, EARLIEST-WINS, whose first element is the
15
+ * current name and whose remaining elements are the deprecated originals. A
16
+ * deployment set up against the old names keeps working; nothing needs a
17
+ * coordinated rename. Read them through `env.string` / `env.positiveInt` /
18
+ * `env.list`, which accept the list directly.
19
+ *
20
+ * Not renamed:
21
+ *
22
+ * - `DATABRICKS_APP_PORT` - the Databricks Apps runtime contract. The platform
23
+ * sets it; the gate honours it.
24
+ * - `PORTR_TOKEN` / `PORTR_SERVER` / `PORTR_AUTO_ADD_PATH` - upstream
25
+ * [portr](https://github.com/amalshaji/portr)'s own namespace, and
26
+ * `PORTR_AUTO_ADD_PATH` is passed straight to that binary. Renaming these
27
+ * would rename someone else's contract.
28
+ *
29
+ * @module
30
+ */
31
+ import type { EnvKey } from "@dbx-tools/shared-core";
32
+ /** Access allow-list patterns (domain / glob / `/regex/`). */
33
+ export declare const ALLOW_ENV: EnvKey;
34
+ /** Subject line for the code email. */
35
+ export declare const SUBJECT_ENV: EnvKey;
36
+ /** Display name used in the code email copy. */
37
+ export declare const BRAND_NAME_ENV: EnvKey;
38
+ /** Line shown immediately above the code in the email. */
39
+ export declare const MESSAGE_ENV: EnvKey;
40
+ /** Session lifetime, in seconds. */
41
+ export declare const SESSION_TTL_ENV: EnvKey;
42
+ /** One-time-code lifetime, in seconds. */
43
+ export declare const CODE_TTL_ENV: EnvKey;
44
+ /** HS256 signing secret for the session JWT. */
45
+ export declare const JWT_SECRET_ENV: EnvKey;
46
+ /**
47
+ * Force-clear cutoff: every session issued BEFORE it stops verifying. Anything
48
+ * `object.toDate` accepts - a date, an ISO instant, epoch seconds/millis, or a
49
+ * relative duration (`-30d`, `7 days ago`).
50
+ */
51
+ export declare const SESSION_CUTOFF_ENV: EnvKey;
52
+ /** The public `<subdomain>.<server>` portr should serve on. */
53
+ export declare const PUBLIC_DOMAIN_ENV: EnvKey;
54
+ /** Run the tunnel OPEN, with no gate. Ignored unless truthy. */
55
+ export declare const INSECURE_ENV: EnvKey;
56
+ /** Extra `x-` request headers tunnel traffic may forward. */
57
+ export declare const FORWARD_HEADERS_ENV: EnvKey;
package/lib/src/env.js ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Environment-variable names the tunnel reads.
3
+ *
4
+ * Every setting is `TUNNEL_`-prefixed, matching the repo convention of naming a
5
+ * variable after the package that owns it (`MASTRA_*`, `TEAMS_*`,
6
+ * `WEB_SEARCH_*`). The gate's original names were unprefixed (`AUTH_SUBJECT`,
7
+ * `PUBLIC_DOMAIN`, ...), which is a real hazard for a package that runs as a
8
+ * WRAPPER: the tunnel and the app it wraps share one environment, so a generic
9
+ * name is one the wrapped app may already use for something else, and
10
+ * `PUBLIC_DOMAIN` in particular reads like an app-wide setting rather than a
11
+ * portr detail. `EMAIL_AUTH_ALLOW` was worse than generic - it sat in
12
+ * `@dbx-tools/email`'s `EMAIL_*` namespace while configuring the gate, not email.
13
+ *
14
+ * Each entry is an {@link EnvKey} list, EARLIEST-WINS, whose first element is the
15
+ * current name and whose remaining elements are the deprecated originals. A
16
+ * deployment set up against the old names keeps working; nothing needs a
17
+ * coordinated rename. Read them through `env.string` / `env.positiveInt` /
18
+ * `env.list`, which accept the list directly.
19
+ *
20
+ * Not renamed:
21
+ *
22
+ * - `DATABRICKS_APP_PORT` - the Databricks Apps runtime contract. The platform
23
+ * sets it; the gate honours it.
24
+ * - `PORTR_TOKEN` / `PORTR_SERVER` / `PORTR_AUTO_ADD_PATH` - upstream
25
+ * [portr](https://github.com/amalshaji/portr)'s own namespace, and
26
+ * `PORTR_AUTO_ADD_PATH` is passed straight to that binary. Renaming these
27
+ * would rename someone else's contract.
28
+ *
29
+ * @module
30
+ */
31
+ /** Access allow-list patterns (domain / glob / `/regex/`). */
32
+ export const ALLOW_ENV = ["TUNNEL_AUTH_ALLOW", "EMAIL_AUTH_ALLOW"];
33
+ /** Subject line for the code email. */
34
+ export const SUBJECT_ENV = ["TUNNEL_AUTH_SUBJECT", "AUTH_SUBJECT"];
35
+ /** Display name used in the code email copy. */
36
+ export const BRAND_NAME_ENV = ["TUNNEL_AUTH_BRAND_NAME", "AUTH_BRAND_NAME"];
37
+ /** Line shown immediately above the code in the email. */
38
+ export const MESSAGE_ENV = ["TUNNEL_AUTH_MESSAGE", "AUTH_MESSAGE"];
39
+ /** Session lifetime, in seconds. */
40
+ export const SESSION_TTL_ENV = ["TUNNEL_AUTH_SESSION_TTL", "AUTH_SESSION_TTL"];
41
+ /** One-time-code lifetime, in seconds. */
42
+ export const CODE_TTL_ENV = ["TUNNEL_AUTH_CODE_TTL", "AUTH_CODE_TTL"];
43
+ /** HS256 signing secret for the session JWT. */
44
+ export const JWT_SECRET_ENV = ["TUNNEL_AUTH_JWT_SECRET", "AUTH_JWT_SECRET"];
45
+ /**
46
+ * Force-clear cutoff: every session issued BEFORE it stops verifying. Anything
47
+ * `object.toDate` accepts - a date, an ISO instant, epoch seconds/millis, or a
48
+ * relative duration (`-30d`, `7 days ago`).
49
+ */
50
+ export const SESSION_CUTOFF_ENV = [
51
+ "TUNNEL_AUTH_SESSION_CUTOFF",
52
+ "TUNNEL_AUTH_SESSION_EPOCH",
53
+ ];
54
+ /** The public `<subdomain>.<server>` portr should serve on. */
55
+ export const PUBLIC_DOMAIN_ENV = ["TUNNEL_PUBLIC_DOMAIN", "PUBLIC_DOMAIN"];
56
+ /** Run the tunnel OPEN, with no gate. Ignored unless truthy. */
57
+ export const INSECURE_ENV = "TUNNEL_INSECURE";
58
+ /** Extra `x-` request headers tunnel traffic may forward. */
59
+ export const FORWARD_HEADERS_ENV = "TUNNEL_FORWARD_HEADERS";
60
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZW52LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2Vudi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7R0E2Qkc7QUFJSCw4REFBOEQ7QUFDOUQsTUFBTSxDQUFDLE1BQU0sU0FBUyxHQUFXLENBQUMsbUJBQW1CLEVBQUUsa0JBQWtCLENBQUMsQ0FBQztBQUUzRSx1Q0FBdUM7QUFDdkMsTUFBTSxDQUFDLE1BQU0sV0FBVyxHQUFXLENBQUMscUJBQXFCLEVBQUUsY0FBYyxDQUFDLENBQUM7QUFFM0UsZ0RBQWdEO0FBQ2hELE1BQU0sQ0FBQyxNQUFNLGNBQWMsR0FBVyxDQUFDLHdCQUF3QixFQUFFLGlCQUFpQixDQUFDLENBQUM7QUFFcEYsMERBQTBEO0FBQzFELE1BQU0sQ0FBQyxNQUFNLFdBQVcsR0FBVyxDQUFDLHFCQUFxQixFQUFFLGNBQWMsQ0FBQyxDQUFDO0FBRTNFLG9DQUFvQztBQUNwQyxNQUFNLENBQUMsTUFBTSxlQUFlLEdBQVcsQ0FBQyx5QkFBeUIsRUFBRSxrQkFBa0IsQ0FBQyxDQUFDO0FBRXZGLDBDQUEwQztBQUMxQyxNQUFNLENBQUMsTUFBTSxZQUFZLEdBQVcsQ0FBQyxzQkFBc0IsRUFBRSxlQUFlLENBQUMsQ0FBQztBQUU5RSxnREFBZ0Q7QUFDaEQsTUFBTSxDQUFDLE1BQU0sY0FBYyxHQUFXLENBQUMsd0JBQXdCLEVBQUUsaUJBQWlCLENBQUMsQ0FBQztBQUVwRjs7OztHQUlHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sa0JBQWtCLEdBQVc7SUFDeEMsNEJBQTRCO0lBQzVCLDJCQUEyQjtDQUM1QixDQUFDO0FBRUYsK0RBQStEO0FBQy9ELE1BQU0sQ0FBQyxNQUFNLGlCQUFpQixHQUFXLENBQUMsc0JBQXNCLEVBQUUsZUFBZSxDQUFDLENBQUM7QUFFbkYsZ0VBQWdFO0FBQ2hFLE1BQU0sQ0FBQyxNQUFNLFlBQVksR0FBVyxpQkFBaUIsQ0FBQztBQUV0RCw2REFBNkQ7QUFDN0QsTUFBTSxDQUFDLE1BQU0sbUJBQW1CLEdBQVcsd0JBQXdCLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIEVudmlyb25tZW50LXZhcmlhYmxlIG5hbWVzIHRoZSB0dW5uZWwgcmVhZHMuXG4gKlxuICogRXZlcnkgc2V0dGluZyBpcyBgVFVOTkVMX2AtcHJlZml4ZWQsIG1hdGNoaW5nIHRoZSByZXBvIGNvbnZlbnRpb24gb2YgbmFtaW5nIGFcbiAqIHZhcmlhYmxlIGFmdGVyIHRoZSBwYWNrYWdlIHRoYXQgb3ducyBpdCAoYE1BU1RSQV8qYCwgYFRFQU1TXypgLFxuICogYFdFQl9TRUFSQ0hfKmApLiBUaGUgZ2F0ZSdzIG9yaWdpbmFsIG5hbWVzIHdlcmUgdW5wcmVmaXhlZCAoYEFVVEhfU1VCSkVDVGAsXG4gKiBgUFVCTElDX0RPTUFJTmAsIC4uLiksIHdoaWNoIGlzIGEgcmVhbCBoYXphcmQgZm9yIGEgcGFja2FnZSB0aGF0IHJ1bnMgYXMgYVxuICogV1JBUFBFUjogdGhlIHR1bm5lbCBhbmQgdGhlIGFwcCBpdCB3cmFwcyBzaGFyZSBvbmUgZW52aXJvbm1lbnQsIHNvIGEgZ2VuZXJpY1xuICogbmFtZSBpcyBvbmUgdGhlIHdyYXBwZWQgYXBwIG1heSBhbHJlYWR5IHVzZSBmb3Igc29tZXRoaW5nIGVsc2UsIGFuZFxuICogYFBVQkxJQ19ET01BSU5gIGluIHBhcnRpY3VsYXIgcmVhZHMgbGlrZSBhbiBhcHAtd2lkZSBzZXR0aW5nIHJhdGhlciB0aGFuIGFcbiAqIHBvcnRyIGRldGFpbC4gYEVNQUlMX0FVVEhfQUxMT1dgIHdhcyB3b3JzZSB0aGFuIGdlbmVyaWMgLSBpdCBzYXQgaW5cbiAqIGBAZGJ4LXRvb2xzL2VtYWlsYCdzIGBFTUFJTF8qYCBuYW1lc3BhY2Ugd2hpbGUgY29uZmlndXJpbmcgdGhlIGdhdGUsIG5vdCBlbWFpbC5cbiAqXG4gKiBFYWNoIGVudHJ5IGlzIGFuIHtAbGluayBFbnZLZXl9IGxpc3QsIEVBUkxJRVNULVdJTlMsIHdob3NlIGZpcnN0IGVsZW1lbnQgaXMgdGhlXG4gKiBjdXJyZW50IG5hbWUgYW5kIHdob3NlIHJlbWFpbmluZyBlbGVtZW50cyBhcmUgdGhlIGRlcHJlY2F0ZWQgb3JpZ2luYWxzLiBBXG4gKiBkZXBsb3ltZW50IHNldCB1cCBhZ2FpbnN0IHRoZSBvbGQgbmFtZXMga2VlcHMgd29ya2luZzsgbm90aGluZyBuZWVkcyBhXG4gKiBjb29yZGluYXRlZCByZW5hbWUuIFJlYWQgdGhlbSB0aHJvdWdoIGBlbnYuc3RyaW5nYCAvIGBlbnYucG9zaXRpdmVJbnRgIC9cbiAqIGBlbnYubGlzdGAsIHdoaWNoIGFjY2VwdCB0aGUgbGlzdCBkaXJlY3RseS5cbiAqXG4gKiBOb3QgcmVuYW1lZDpcbiAqXG4gKiAgIC0gYERBVEFCUklDS1NfQVBQX1BPUlRgIC0gdGhlIERhdGFicmlja3MgQXBwcyBydW50aW1lIGNvbnRyYWN0LiBUaGUgcGxhdGZvcm1cbiAqICAgICBzZXRzIGl0OyB0aGUgZ2F0ZSBob25vdXJzIGl0LlxuICogICAtIGBQT1JUUl9UT0tFTmAgLyBgUE9SVFJfU0VSVkVSYCAvIGBQT1JUUl9BVVRPX0FERF9QQVRIYCAtIHVwc3RyZWFtXG4gKiAgICAgW3BvcnRyXShodHRwczovL2dpdGh1Yi5jb20vYW1hbHNoYWppL3BvcnRyKSdzIG93biBuYW1lc3BhY2UsIGFuZFxuICogICAgIGBQT1JUUl9BVVRPX0FERF9QQVRIYCBpcyBwYXNzZWQgc3RyYWlnaHQgdG8gdGhhdCBiaW5hcnkuIFJlbmFtaW5nIHRoZXNlXG4gKiAgICAgd291bGQgcmVuYW1lIHNvbWVvbmUgZWxzZSdzIGNvbnRyYWN0LlxuICpcbiAqIEBtb2R1bGVcbiAqL1xuXG5pbXBvcnQgdHlwZSB7IEVudktleSB9IGZyb20gXCJAZGJ4LXRvb2xzL3NoYXJlZC1jb3JlXCI7XG5cbi8qKiBBY2Nlc3MgYWxsb3ctbGlzdCBwYXR0ZXJucyAoZG9tYWluIC8gZ2xvYiAvIGAvcmVnZXgvYCkuICovXG5leHBvcnQgY29uc3QgQUxMT1dfRU5WOiBFbnZLZXkgPSBbXCJUVU5ORUxfQVVUSF9BTExPV1wiLCBcIkVNQUlMX0FVVEhfQUxMT1dcIl07XG5cbi8qKiBTdWJqZWN0IGxpbmUgZm9yIHRoZSBjb2RlIGVtYWlsLiAqL1xuZXhwb3J0IGNvbnN0IFNVQkpFQ1RfRU5WOiBFbnZLZXkgPSBbXCJUVU5ORUxfQVVUSF9TVUJKRUNUXCIsIFwiQVVUSF9TVUJKRUNUXCJdO1xuXG4vKiogRGlzcGxheSBuYW1lIHVzZWQgaW4gdGhlIGNvZGUgZW1haWwgY29weS4gKi9cbmV4cG9ydCBjb25zdCBCUkFORF9OQU1FX0VOVjogRW52S2V5ID0gW1wiVFVOTkVMX0FVVEhfQlJBTkRfTkFNRVwiLCBcIkFVVEhfQlJBTkRfTkFNRVwiXTtcblxuLyoqIExpbmUgc2hvd24gaW1tZWRpYXRlbHkgYWJvdmUgdGhlIGNvZGUgaW4gdGhlIGVtYWlsLiAqL1xuZXhwb3J0IGNvbnN0IE1FU1NBR0VfRU5WOiBFbnZLZXkgPSBbXCJUVU5ORUxfQVVUSF9NRVNTQUdFXCIsIFwiQVVUSF9NRVNTQUdFXCJdO1xuXG4vKiogU2Vzc2lvbiBsaWZldGltZSwgaW4gc2Vjb25kcy4gKi9cbmV4cG9ydCBjb25zdCBTRVNTSU9OX1RUTF9FTlY6IEVudktleSA9IFtcIlRVTk5FTF9BVVRIX1NFU1NJT05fVFRMXCIsIFwiQVVUSF9TRVNTSU9OX1RUTFwiXTtcblxuLyoqIE9uZS10aW1lLWNvZGUgbGlmZXRpbWUsIGluIHNlY29uZHMuICovXG5leHBvcnQgY29uc3QgQ09ERV9UVExfRU5WOiBFbnZLZXkgPSBbXCJUVU5ORUxfQVVUSF9DT0RFX1RUTFwiLCBcIkFVVEhfQ09ERV9UVExcIl07XG5cbi8qKiBIUzI1NiBzaWduaW5nIHNlY3JldCBmb3IgdGhlIHNlc3Npb24gSldULiAqL1xuZXhwb3J0IGNvbnN0IEpXVF9TRUNSRVRfRU5WOiBFbnZLZXkgPSBbXCJUVU5ORUxfQVVUSF9KV1RfU0VDUkVUXCIsIFwiQVVUSF9KV1RfU0VDUkVUXCJdO1xuXG4vKipcbiAqIEZvcmNlLWNsZWFyIGN1dG9mZjogZXZlcnkgc2Vzc2lvbiBpc3N1ZWQgQkVGT1JFIGl0IHN0b3BzIHZlcmlmeWluZy4gQW55dGhpbmdcbiAqIGBvYmplY3QudG9EYXRlYCBhY2NlcHRzIC0gYSBkYXRlLCBhbiBJU08gaW5zdGFudCwgZXBvY2ggc2Vjb25kcy9taWxsaXMsIG9yIGFcbiAqIHJlbGF0aXZlIGR1cmF0aW9uIChgLTMwZGAsIGA3IGRheXMgYWdvYCkuXG4gKi9cbmV4cG9ydCBjb25zdCBTRVNTSU9OX0NVVE9GRl9FTlY6IEVudktleSA9IFtcbiAgXCJUVU5ORUxfQVVUSF9TRVNTSU9OX0NVVE9GRlwiLFxuICBcIlRVTk5FTF9BVVRIX1NFU1NJT05fRVBPQ0hcIixcbl07XG5cbi8qKiBUaGUgcHVibGljIGA8c3ViZG9tYWluPi48c2VydmVyPmAgcG9ydHIgc2hvdWxkIHNlcnZlIG9uLiAqL1xuZXhwb3J0IGNvbnN0IFBVQkxJQ19ET01BSU5fRU5WOiBFbnZLZXkgPSBbXCJUVU5ORUxfUFVCTElDX0RPTUFJTlwiLCBcIlBVQkxJQ19ET01BSU5cIl07XG5cbi8qKiBSdW4gdGhlIHR1bm5lbCBPUEVOLCB3aXRoIG5vIGdhdGUuIElnbm9yZWQgdW5sZXNzIHRydXRoeS4gKi9cbmV4cG9ydCBjb25zdCBJTlNFQ1VSRV9FTlY6IEVudktleSA9IFwiVFVOTkVMX0lOU0VDVVJFXCI7XG5cbi8qKiBFeHRyYSBgeC1gIHJlcXVlc3QgaGVhZGVycyB0dW5uZWwgdHJhZmZpYyBtYXkgZm9yd2FyZC4gKi9cbmV4cG9ydCBjb25zdCBGT1JXQVJEX0hFQURFUlNfRU5WOiBFbnZLZXkgPSBcIlRVTk5FTF9GT1JXQVJEX0hFQURFUlNcIjtcbiJdfQ==