@dbx-tools/cli-tunnel 0.6.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/src/otp.js ADDED
@@ -0,0 +1,134 @@
1
+ /**
2
+ * One-time-code store + session JWT for the email-OTP tunnel gate.
3
+ *
4
+ * The code store is backed by AppKit's `CacheManager` (auto-configured to memory,
5
+ * or Lakebase when the app wires a persistent `CacheStorage`), so TTL EXPIRY and
6
+ * eviction are the cache's job - no hand-rolled Map or timers. A 6-digit code is
7
+ * generated with `crypto.randomInt` and stored as a SHA-256 hash with an attempt
8
+ * counter (never the plaintext, never in the JWT); `verify` is constant-time on
9
+ * the hash, and the entry is deleted on success or once attempts are exhausted.
10
+ *
11
+ * The session JWT is a short-lived HS256 token (via `jose`) carrying only the
12
+ * email. Its signing key comes from `AUTH_JWT_SECRET`; when unset the gate FAILS
13
+ * OPEN with an ephemeral per-process key (sessions reset on restart) rather than
14
+ * refusing service - a Databricks App is already access-limited, so an unset
15
+ * secret degrades to "sessions don't survive restarts", not "nobody can log in".
16
+ *
17
+ * @module
18
+ */
19
+ import { createHash, randomBytes, randomInt, timingSafeEqual } from "node:crypto";
20
+ import { CacheManager } from "@databricks/appkit";
21
+ import { log } from "@dbx-tools/shared-core";
22
+ import { jwtVerify, SignJWT } from "jose";
23
+ const logger = log.logger("tunnel:otp");
24
+ /** JWT issuer/audience so a token minted for this gate isn't accepted elsewhere. */
25
+ const JWT_AUD = "dbx-tools-tunnel-auth";
26
+ /** Cache-key prefix for pending codes, namespaced away from any other cache use. */
27
+ const CODE_PREFIX = "tunnel:otp:";
28
+ /** SHA-256 hex of a value. */
29
+ function sha256(value) {
30
+ return createHash("sha256").update(value).digest("hex");
31
+ }
32
+ /** Constant-time compare of two equal-length hex digests. */
33
+ function safeEqualHex(a, b) {
34
+ if (a.length !== b.length)
35
+ return false;
36
+ return timingSafeEqual(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
37
+ }
38
+ /**
39
+ * Pending one-time codes, stored in AppKit's cache keyed by lowercased email.
40
+ * Expiry is the cache's TTL (no manual clock); a miss means expired-or-never.
41
+ */
42
+ export class CodeStore {
43
+ ttlSeconds;
44
+ maxAttempts;
45
+ constructor(ttlSeconds, maxAttempts) {
46
+ this.ttlSeconds = ttlSeconds;
47
+ this.maxAttempts = maxAttempts;
48
+ }
49
+ cache() {
50
+ return CacheManager.getInstanceSync();
51
+ }
52
+ key(email) {
53
+ return `${CODE_PREFIX}${email.toLowerCase()}`;
54
+ }
55
+ /**
56
+ * Generate, store (hashed, with the cache TTL), and RETURN a fresh 6-digit
57
+ * code. The caller emails the returned plaintext; only the hash is retained.
58
+ * Replaces any pending code for the address.
59
+ */
60
+ async issue(email) {
61
+ const code = String(randomInt(0, 1_000_000)).padStart(6, "0");
62
+ const entry = { hash: sha256(code), attempts: 0 };
63
+ await this.cache().set(this.key(email), entry, { ttl: this.ttlSeconds });
64
+ return code;
65
+ }
66
+ /**
67
+ * Check `code` for `email`. A cache miss is `expired` (TTL elapsed or never
68
+ * issued). Deletes the entry on success or when attempts are exhausted, so a
69
+ * code is single-use and can't be brute-forced past the cap. An attempt
70
+ * increments the stored counter (re-persisted with a fresh TTL window).
71
+ */
72
+ async verify(email, code) {
73
+ const key = this.key(email);
74
+ const entry = await this.cache().get(key);
75
+ if (!entry)
76
+ return "expired";
77
+ const attempts = entry.attempts + 1;
78
+ if (safeEqualHex(entry.hash, sha256(code))) {
79
+ await this.cache().delete(key);
80
+ return "ok";
81
+ }
82
+ if (attempts >= this.maxAttempts) {
83
+ await this.cache().delete(key);
84
+ return "too-many-attempts";
85
+ }
86
+ await this.cache().set(key, { ...entry, attempts }, { ttl: this.ttlSeconds });
87
+ return "invalid";
88
+ }
89
+ }
90
+ /**
91
+ * Resolve the HS256 signing key. Prefers `AUTH_JWT_SECRET`; when unset, mints an
92
+ * ephemeral per-process key (fail-open) and warns once. Memoized.
93
+ */
94
+ let cachedKey;
95
+ function signingKey() {
96
+ if (cachedKey)
97
+ return cachedKey;
98
+ const secret = process.env.AUTH_JWT_SECRET?.trim();
99
+ if (secret) {
100
+ cachedKey = new TextEncoder().encode(secret);
101
+ }
102
+ else {
103
+ logger.warn("AUTH_JWT_SECRET is not set - using an ephemeral per-process key; sessions will not survive a restart");
104
+ cachedKey = randomBytes(32);
105
+ }
106
+ return cachedKey;
107
+ }
108
+ /** Reset the memoized key (tests, or after changing the env in-process). */
109
+ export function resetSigningKey() {
110
+ cachedKey = undefined;
111
+ }
112
+ /** Mint a short-lived session JWT for `email`, expiring in `ttlSeconds`. */
113
+ export async function signSession(email, ttlSeconds) {
114
+ return new SignJWT({ email })
115
+ .setProtectedHeader({ alg: "HS256" })
116
+ .setSubject(email)
117
+ .setAudience(JWT_AUD)
118
+ .setIssuedAt()
119
+ .setExpirationTime(`${ttlSeconds}s`)
120
+ .sign(signingKey());
121
+ }
122
+ /** Validate a session JWT, returning the email it was minted for, or `undefined`. */
123
+ export async function verifySession(token) {
124
+ if (!token)
125
+ return undefined;
126
+ try {
127
+ const { payload } = await jwtVerify(token, signingKey(), { audience: JWT_AUD });
128
+ return typeof payload.email === "string" ? payload.email : undefined;
129
+ }
130
+ catch {
131
+ return undefined;
132
+ }
133
+ }
134
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3RwLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL290cC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7Ozs7Ozs7Ozs7Ozs7R0FpQkc7QUFFSCxPQUFPLEVBQUUsVUFBVSxFQUFFLFdBQVcsRUFBRSxTQUFTLEVBQUUsZUFBZSxFQUFFLE1BQU0sYUFBYSxDQUFDO0FBQ2xGLE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQztBQUNsRCxPQUFPLEVBQUUsR0FBRyxFQUFFLE1BQU0sd0JBQXdCLENBQUM7QUFDN0MsT0FBTyxFQUFFLFNBQVMsRUFBRSxPQUFPLEVBQUUsTUFBTSxNQUFNLENBQUM7QUFFMUMsTUFBTSxNQUFNLEdBQUcsR0FBRyxDQUFDLE1BQU0sQ0FBQyxZQUFZLENBQUMsQ0FBQztBQUV4QyxvRkFBb0Y7QUFDcEYsTUFBTSxPQUFPLEdBQUcsdUJBQXVCLENBQUM7QUFFeEMsb0ZBQW9GO0FBQ3BGLE1BQU0sV0FBVyxHQUFHLGFBQWEsQ0FBQztBQUVsQyw4QkFBOEI7QUFDOUIsU0FBUyxNQUFNLENBQUMsS0FBYTtJQUMzQixPQUFPLFVBQVUsQ0FBQyxRQUFRLENBQUMsQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDO0FBQzFELENBQUM7QUFFRCw2REFBNkQ7QUFDN0QsU0FBUyxZQUFZLENBQUMsQ0FBUyxFQUFFLENBQVM7SUFDeEMsSUFBSSxDQUFDLENBQUMsTUFBTSxLQUFLLENBQUMsQ0FBQyxNQUFNO1FBQUUsT0FBTyxLQUFLLENBQUM7SUFDeEMsT0FBTyxlQUFlLENBQUMsTUFBTSxDQUFDLElBQUksQ0FBQyxDQUFDLEVBQUUsS0FBSyxDQUFDLEVBQUUsTUFBTSxDQUFDLElBQUksQ0FBQyxDQUFDLEVBQUUsS0FBSyxDQUFDLENBQUMsQ0FBQztBQUN2RSxDQUFDO0FBVUQ7OztHQUdHO0FBQ0gsTUFBTSxPQUFPLFNBQVM7SUFFRDtJQUNBO0lB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LFVBQVUsZUFBZTtJQUM3QixTQUFTLEdBQUcsU0FBUyxDQUFDO0FBQ3hCLENBQUM7QUFFRCw0RUFBNEU7QUFDNUUsTUFBTSxDQUFDLEtBQUssVUFBVSxXQUFXLENBQUMsS0FBYSxFQUFFLFVBQWtCO0lBQ2pFLE9BQU8sSUFBSSxPQUFPLENBQUMsRUFBRSxLQUFLLEVBQUUsQ0FBQztTQUMxQixrQkFBa0IsQ0FBQyxFQUFFLEdBQUcsRUFBRSxPQUFPLEVBQUUsQ0FBQztTQUNwQyxVQUFVLENBQUMsS0FBSyxDQUFDO1NBQ2pCLFdBQVcsQ0FBQyxPQUFPLENBQUM7U0FDcEIsV0FBVyxFQUFFO1NBQ2IsaUJBQWlCLENBQUMsR0FBRyxVQUFVLEdBQUcsQ0FBQztTQUNuQyxJQUFJLENBQUMsVUFBVSxFQUFFLENBQUMsQ0FBQztBQUN4QixDQUFDO0FBRUQscUZBQXFGO0FBQ3JGLE1BQU0sQ0FBQyxLQUFLLFVBQVUsYUFBYSxDQUFDLEtBQXlCO0lBQzNELElBQUksQ0FBQyxLQUFLO1FBQUUsT0FBTyxTQUFTLENBQUM7SUFDN0IsSUFBSSxDQUFDO1FBQ0gsTUFBTSxFQUFFLE9BQU8sRUFBRSxHQUFHLE1BQU0sU0FBUyxDQUFDLEtBQUssRUFBRSxVQUFVLEVBQUUsRUFBRSxFQUFFLFFBQVEsRUFBRSxPQUFPLEVBQUUsQ0FBQyxDQUFDO1FBQ2hGLE9BQU8sT0FBTyxPQUFPLENBQUMsS0FBSyxLQUFLLFFBQVEsQ0FBQyxDQUFDLENBQUMsT0FBTyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDO0lBQ3ZFLENBQUM7SUFBQyxNQUFNLENBQUM7UUFDUCxPQUFPLFNBQVMsQ0FBQztJQUNuQixDQUFDO0FBQ0gsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogT25lLXRpbWUtY29kZSBzdG9yZSArIHNlc3Npb24gSldUIGZvciB0aGUgZW1haWwtT1RQIHR1bm5lbCBnYXRlLlxuICpcbiAqIFRoZSBjb2RlIHN0b3JlIGlzIGJhY2tlZCBieSBBcHBLaXQncyBgQ2FjaGVNYW5hZ2VyYCAoYXV0by1jb25maWd1cmVkIHRvIG1lbW9yeSxcbiAqIG9yIExha2ViYXNlIHdoZW4gdGhlIGFwcCB3aXJlcyBhIHBlcnNpc3RlbnQgYENhY2hlU3RvcmFnZWApLCBzbyBUVEwgRVhQSVJZIGFuZFxuICogZXZpY3Rpb24gYXJlIHRoZSBjYWNoZSdzIGpvYiAtIG5vIGhhbmQtcm9sbGVkIE1hcCBvciB0aW1lcnMuIEEgNi1kaWdpdCBjb2RlIGlzXG4gKiBnZW5lcmF0ZWQgd2l0aCBgY3J5cHRvLnJhbmRvbUludGAgYW5kIHN0b3JlZCBhcyBhIFNIQS0yNTYgaGFzaCB3aXRoIGFuIGF0dGVtcHRcbiAqIGNvdW50ZXIgKG5ldmVyIHRoZSBwbGFpbnRleHQsIG5ldmVyIGluIHRoZSBKV1QpOyBgdmVyaWZ5YCBpcyBjb25zdGFudC10aW1lIG9uXG4gKiB0aGUgaGFzaCwgYW5kIHRoZSBlbnRyeSBpcyBkZWxldGVkIG9uIHN1Y2Nlc3Mgb3Igb25jZSBhdHRlbXB0cyBhcmUgZXhoYXVzdGVkLlxuICpcbiAqIFRoZSBzZXNzaW9uIEpXVCBpcyBhIHNob3J0LWxpdmVkIEhTMjU2IHRva2VuICh2aWEgYGpvc2VgKSBjYXJyeWluZyBvbmx5IHRoZVxuICogZW1haWwuIEl0cyBzaWduaW5nIGtleSBjb21lcyBmcm9tIGBBVVRIX0pXVF9TRUNSRVRgOyB3aGVuIHVuc2V0IHRoZSBnYXRlIEZBSUxTXG4gKiBPUEVOIHdpdGggYW4gZXBoZW1lcmFsIHBlci1wcm9jZXNzIGtleSAoc2Vzc2lvbnMgcmVzZXQgb24gcmVzdGFydCkgcmF0aGVyIHRoYW5cbiAqIHJlZnVzaW5nIHNlcnZpY2UgLSBhIERhdGFicmlja3MgQXBwIGlzIGFscmVhZHkgYWNjZXNzLWxpbWl0ZWQsIHNvIGFuIHVuc2V0XG4gKiBzZWNyZXQgZGVncmFkZXMgdG8gXCJzZXNzaW9ucyBkb24ndCBzdXJ2aXZlIHJlc3RhcnRzXCIsIG5vdCBcIm5vYm9keSBjYW4gbG9nIGluXCIuXG4gKlxuICogQG1vZHVsZVxuICovXG5cbmltcG9ydCB7IGNyZWF0ZUhhc2gsIHJhbmRvbUJ5dGVzLCByYW5kb21JbnQsIHRpbWluZ1NhZmVFcXVhbCB9IGZyb20gXCJub2RlOmNyeXB0b1wiO1xuaW1wb3J0IHsgQ2FjaGVNYW5hZ2VyIH0gZnJvbSBcIkBkYXRhYnJpY2tzL2FwcGtpdFwiO1xuaW1wb3J0IHsgbG9nIH0gZnJvbSBcIkBkYngtdG9vbHMvc2hhcmVkLWNvcmVcIjtcbmltcG9ydCB7IGp3dFZlcmlmeSwgU2lnbkpXVCB9IGZyb20gXCJqb3NlXCI7XG5cbmNvbnN0IGxvZ2dlciA9IGxvZy5sb2dnZXIoXCJ0dW5uZWw6b3RwXCIpO1xuXG4vKiogSldUIGlzc3Vlci9hdWRpZW5jZSBzbyBhIHRva2VuIG1pbnRlZCBmb3IgdGhpcyBnYXRlIGlzbid0IGFjY2VwdGVkIGVsc2V3aGVyZS4gKi9cbmNvbnN0IEpXVF9BVUQgPSBcImRieC10b29scy10dW5uZWwtYXV0aFwiO1xuXG4vKiogQ2FjaGUta2V5IHByZWZpeCBmb3IgcGVuZGluZyBjb2RlcywgbmFtZXNwYWNlZCBhd2F5IGZyb20gYW55IG90aGVyIGNhY2hlIHVzZS4gKi9cbmNvbnN0IENPREVfUFJFRklYID0gXCJ0dW5uZWw6b3RwOlwiO1xuXG4vKiogU0hBLTI1NiBoZXggb2YgYSB2YWx1ZS4gKi9cbmZ1bmN0aW9uIHNoYTI1Nih2YWx1ZTogc3RyaW5nKTogc3RyaW5nIHtcbiAgcmV0dXJuIGNyZWF0ZUhhc2goXCJzaGEyNTZcIikudXBkYXRlKHZhbHVlKS5kaWdlc3QoXCJoZXhcIik7XG59XG5cbi8qKiBDb25zdGFudC10aW1lIGNvbXBhcmUgb2YgdHdvIGVxdWFsLWxlbmd0aCBoZXggZGlnZXN0cy4gKi9cbmZ1bmN0aW9uIHNhZmVFcXVhbEhleChhOiBzdHJpbmcsIGI6IHN0cmluZyk6IGJvb2xlYW4ge1xuICBpZiAoYS5sZW5ndGggIT09IGIubGVuZ3RoKSByZXR1cm4gZmFsc2U7XG4gIHJldHVybiB0aW1pbmdTYWZlRXF1YWwoQnVmZmVyLmZyb20oYSwgXCJoZXhcIiksIEJ1ZmZlci5mcm9tKGIsIFwiaGV4XCIpKTtcbn1cblxuaW50ZXJmYWNlIENvZGVFbnRyeSB7XG4gIGhhc2g6IHN0cmluZztcbiAgYXR0ZW1wdHM6IG51bWJlcjtcbn1cblxuLyoqIFJlc3VsdCBvZiB7QGxpbmsgQ29kZVN0b3JlLnZlcmlmeX0uICovXG5leHBvcnQgdHlwZSBWZXJpZnlPdXRjb21lID0gXCJva1wiIHwgXCJpbnZhbGlkXCIgfCBcImV4cGlyZWRcIiB8IFwidG9vLW1hbnktYXR0ZW1wdHNcIjtcblxuLyoqXG4gKiBQZW5kaW5nIG9uZS10aW1lIGNvZGVzLCBzdG9yZWQgaW4gQXBwS2l0J3MgY2FjaGUga2V5ZWQgYnkgbG93ZXJjYXNlZCBlbWFpbC5cbiAqIEV4cGlyeSBpcyB0aGUgY2FjaGUncyBUVEwgKG5vIG1hbnVhbCBjbG9jayk7IGEgbWlzcyBtZWFucyBleHBpcmVkLW9yLW5ldmVyLlxuICovXG5leHBvcnQgY2xhc3MgQ29kZVN0b3JlIHtcbiAgY29uc3RydWN0b3IoXG4gICAgcHJpdmF0ZSByZWFkb25seSB0dGxTZWNvbmRzOiBudW1iZXIsXG4gICAgcHJpdmF0ZSByZWFkb25seSBtYXhBdHRlbXB0czogbnVtYmVyLFxuICApIHt9XG5cbiAgcHJpdmF0ZSBjYWNoZSgpOiBDYWNoZU1hbmFnZXIge1xuICAgIHJldHVybiBDYWNoZU1hbmFnZXIuZ2V0SW5zdGFuY2VTeW5jKCk7XG4gIH1cblxuICBwcml2YXRlIGtleShlbWFpbDogc3RyaW5nKTogc3RyaW5nIHtcbiAgICByZXR1cm4gYCR7Q09ERV9QUkVGSVh9JHtlbWFpbC50b0xvd2VyQ2FzZSgpfWA7XG4gIH1cblxuICAvKipcbiAgICogR2VuZXJhdGUsIHN0b3JlIChoYXNoZWQsIHdpdGggdGhlIGNhY2hlIFRUTCksIGFuZCBSRVRVUk4gYSBmcmVzaCA2LWRpZ2l0XG4gICAqIGNvZGUuIFRoZSBjYWxsZXIgZW1haWxzIHRoZSByZXR1cm5lZCBwbGFpbnRleHQ7IG9ubHkgdGhlIGhhc2ggaXMgcmV0YWluZWQuXG4gICAqIFJlcGxhY2VzIGFueSBwZW5kaW5nIGNvZGUgZm9yIHRoZSBhZGRyZXNzLlxuICAgKi9cbiAgYXN5bmMgaXNzdWUoZW1haWw6IHN0cmluZyk6IFByb21pc2U8c3RyaW5nPiB7XG4gICAgY29uc3QgY29kZSA9IFN0cmluZyhyYW5kb21JbnQoMCwgMV8wMDBfMDAwKSkucGFkU3RhcnQoNiwgXCIwXCIpO1xuICAgIGNvbnN0IGVudHJ5OiBDb2RlRW50cnkgPSB7IGhhc2g6IHNoYTI1Nihjb2RlKSwgYXR0ZW1wdHM6IDAgfTtcbiAgICBhd2FpdCB0aGlzLmNhY2hlKCkuc2V0KHRoaXMua2V5KGVtYWlsKSwgZW50cnksIHsgdHRsOiB0aGlzLnR0bFNlY29uZHMgfSk7XG4gICAgcmV0dXJuIGNvZGU7XG4gIH1cblxuICAvKipcbiAgICogQ2hlY2sgYGNvZGVgIGZvciBgZW1haWxgLiBBIGNhY2hlIG1pc3MgaXMgYGV4cGlyZWRgIChUVEwgZWxhcHNlZCBvciBuZXZlclxuICAgKiBpc3N1ZWQpLiBEZWxldGVzIHRoZSBlbnRyeSBvbiBzdWNjZXNzIG9yIHdoZW4gYXR0ZW1wdHMgYXJlIGV4aGF1c3RlZCwgc28gYVxuICAgKiBjb2RlIGlzIHNpbmdsZS11c2UgYW5kIGNhbid0IGJlIGJydXRlLWZvcmNlZCBwYXN0IHRoZSBjYXAuIEFuIGF0dGVtcHRcbiAgICogaW5jcmVtZW50cyB0aGUgc3RvcmVkIGNvdW50ZXIgKHJlLXBlcnNpc3RlZCB3aXRoIGEgZnJlc2ggVFRMIHdpbmRvdykuXG4gICAqL1xuICBhc3luYyB2ZXJpZnkoZW1haWw6IHN0cmluZywgY29kZTogc3RyaW5nKTogUHJvbWlzZTxWZXJpZnlPdXRjb21lPiB7XG4gICAgY29uc3Qga2V5ID0gdGhpcy5rZXkoZW1haWwpO1xuICAgIGNvbnN0IGVudHJ5ID0gYXdhaXQgdGhpcy5jYWNoZSgpLmdldDxDb2RlRW50cnk+KGtleSk7XG4gICAgaWYgKCFlbnRyeSkgcmV0dXJuIFwiZXhwaXJlZFwiO1xuICAgIGNvbnN0IGF0dGVtcHRzID0gZW50cnkuYXR0ZW1wdHMgKyAxO1xuICAgIGlmIChzYWZlRXF1YWxIZXgoZW50cnkuaGFzaCwgc2hhMjU2KGNvZGUpKSkge1xuICAgICAgYXdhaXQgdGhpcy5jYWNoZSgpLmRlbGV0ZShrZXkpO1xuICAgICAgcmV0dXJuIFwib2tcIjtcbiAgICB9XG4gICAgaWYgKGF0dGVtcHRzID49IHRoaXMubWF4QXR0ZW1wdHMpIHtcbiAgICAgIGF3YWl0IHRoaXMuY2FjaGUoKS5kZWxldGUoa2V5KTtcbiAgICAgIHJldHVybiBcInRvby1tYW55LWF0dGVtcHRzXCI7XG4gICAgfVxuICAgIGF3YWl0IHRoaXMuY2FjaGUoKS5zZXQoa2V5LCB7IC4uLmVudHJ5LCBhdHRlbXB0cyB9LCB7IHR0bDogdGhpcy50dGxTZWNvbmRzIH0pO1xuICAgIHJldHVybiBcImludmFsaWRcIjtcbiAgfVxufVxuXG4vKipcbiAqIFJlc29sdmUgdGhlIEhTMjU2IHNpZ25pbmcga2V5LiBQcmVmZXJzIGBBVVRIX0pXVF9TRUNSRVRgOyB3aGVuIHVuc2V0LCBtaW50cyBhblxuICogZXBoZW1lcmFsIHBlci1wcm9jZXNzIGtleSAoZmFpbC1vcGVuKSBhbmQgd2FybnMgb25jZS4gTWVtb2l6ZWQuXG4gKi9cbmxldCBjYWNoZWRLZXk6IFVpbnQ4QXJyYXkgfCB1bmRlZmluZWQ7XG5mdW5jdGlvbiBzaWduaW5nS2V5KCk6IFVpbnQ4QXJyYXkge1xuICBpZiAoY2FjaGVkS2V5KSByZXR1cm4gY2FjaGVkS2V5O1xuICBjb25zdCBzZWNyZXQgPSBwcm9jZXNzLmVudi5BVVRIX0pXVF9TRUNSRVQ/LnRyaW0oKTtcbiAgaWYgKHNlY3JldCkge1xuICAgIGNhY2hlZEtleSA9IG5ldyBUZXh0RW5jb2RlcigpLmVuY29kZShzZWNyZXQpO1xuICB9IGVsc2Uge1xuICAgIGxvZ2dlci53YXJuKFxuICAgICAgXCJBVVRIX0pXVF9TRUNSRVQgaXMgbm90IHNldCAtIHVzaW5nIGFuIGVwaGVtZXJhbCBwZXItcHJvY2VzcyBrZXk7IHNlc3Npb25zIHdpbGwgbm90IHN1cnZpdmUgYSByZXN0YXJ0XCIsXG4gICAgKTtcbiAgICBjYWNoZWRLZXkgPSByYW5kb21CeXRlcygzMik7XG4gIH1cbiAgcmV0dXJuIGNhY2hlZEtleTtcbn1cblxuLyoqIFJlc2V0IHRoZSBtZW1vaXplZCBrZXkgKHRlc3RzLCBvciBhZnRlciBjaGFuZ2luZyB0aGUgZW52IGluLXByb2Nlc3MpLiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHJlc2V0U2lnbmluZ0tleSgpOiB2b2lkIHtcbiAgY2FjaGVkS2V5ID0gdW5kZWZpbmVkO1xufVxuXG4vKiogTWludCBhIHNob3J0LWxpdmVkIHNlc3Npb24gSldUIGZvciBgZW1haWxgLCBleHBpcmluZyBpbiBgdHRsU2Vjb25kc2AuICovXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gc2lnblNlc3Npb24oZW1haWw6IHN0cmluZywgdHRsU2Vjb25kczogbnVtYmVyKTogUHJvbWlzZTxzdHJpbmc+IHtcbiAgcmV0dXJuIG5ldyBTaWduSldUKHsgZW1haWwgfSlcbiAgICAuc2V0UHJvdGVjdGVkSGVhZGVyKHsgYWxnOiBcIkhTMjU2XCIgfSlcbiAgICAuc2V0U3ViamVjdChlbWFpbClcbiAgICAuc2V0QXVkaWVuY2UoSldUX0FVRClcbiAgICAuc2V0SXNzdWVkQXQoKVxuICAgIC5zZXRFeHBpcmF0aW9uVGltZShgJHt0dGxTZWNvbmRzfXNgKVxuICAgIC5zaWduKHNpZ25pbmdLZXkoKSk7XG59XG5cbi8qKiBWYWxpZGF0ZSBhIHNlc3Npb24gSldULCByZXR1cm5pbmcgdGhlIGVtYWlsIGl0IHdhcyBtaW50ZWQgZm9yLCBvciBgdW5kZWZpbmVkYC4gKi9cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiB2ZXJpZnlTZXNzaW9uKHRva2VuOiBzdHJpbmcgfCB1bmRlZmluZWQpOiBQcm9taXNlPHN0cmluZyB8IHVuZGVmaW5lZD4ge1xuICBpZiAoIXRva2VuKSByZXR1cm4gdW5kZWZpbmVkO1xuICB0cnkge1xuICAgIGNvbnN0IHsgcGF5bG9hZCB9ID0gYXdhaXQgand0VmVyaWZ5KHRva2VuLCBzaWduaW5nS2V5KCksIHsgYXVkaWVuY2U6IEpXVF9BVUQgfSk7XG4gICAgcmV0dXJuIHR5cGVvZiBwYXlsb2FkLmVtYWlsID09PSBcInN0cmluZ1wiID8gcGF5bG9hZC5lbWFpbCA6IHVuZGVmaW5lZDtcbiAgfSBjYXRjaCB7XG4gICAgcmV0dXJuIHVuZGVmaW5lZDtcbiAgfVxufVxuIl19
@@ -0,0 +1,99 @@
1
+ /**
2
+ * `authGate()` - the AppKit plugin behind the tunnel's email-OTP gate.
3
+ *
4
+ * It has NO routes of its own: the tunnel PROXY (not an HTTP server) calls the
5
+ * handlers this plugin exposes via {@link AuthGatePlugin.exports}. The plugin
6
+ * owns the allow-list, the per-email/per-IP rate limiters, the CacheManager-backed
7
+ * one-time-code store, and the session JWT. `createApp` (with no `server()`) is
8
+ * used only to auto-init `CacheManager` + prime the sibling `email` transport;
9
+ * this plugin is where the gate logic lives.
10
+ *
11
+ * Options come from CLI flags OR env, with sensible defaults - see
12
+ * {@link resolveAuthGateConfig}. The one runtime dependency the plugin can't
13
+ * resolve itself is HOW to email the code, so it takes a `sendCode` callback the
14
+ * app wires to the email plugin.
15
+ *
16
+ * @module
17
+ */
18
+ import { Plugin, type BasePluginConfig } from "@databricks/appkit";
19
+ import type { AuthStatus } from "@dbx-tools/shared-email";
20
+ /** Options for the {@link authGate} plugin (all resolvable from env - see below). */
21
+ export interface AuthGateConfig extends BasePluginConfig {
22
+ /** Allow-list patterns (domain / glob / `/regex/`). Empty = allow nobody. Env EMAIL_AUTH_ALLOW. */
23
+ allow?: string | string[];
24
+ /** Subject line for the code email. Env AUTH_SUBJECT. */
25
+ subject?: string;
26
+ /** Product/brand name used in the email + login copy. Env AUTH_BRAND_NAME. */
27
+ brandName?: string;
28
+ /** One-line message shown above the code in the email. Env AUTH_MESSAGE. */
29
+ message?: string;
30
+ /** Session lifetime (seconds). Env AUTH_SESSION_TTL. Default 43200 (12h). */
31
+ sessionTtlSeconds?: number;
32
+ /** One-time-code lifetime (seconds). Env AUTH_CODE_TTL. Default 600 (10m). */
33
+ codeTtlSeconds?: number;
34
+ /** Max verify attempts per issued code. Default 5. */
35
+ maxAttempts?: number;
36
+ /** Deliver a code to an address. Wired by the app to the email plugin. */
37
+ sendCode?: (email: string, code: string, opts: SendCodeOptions) => Promise<void>;
38
+ }
39
+ /** Branding/messaging passed to {@link AuthGateConfig.sendCode}. */
40
+ export interface SendCodeOptions {
41
+ subject: string;
42
+ brandName: string;
43
+ message: string;
44
+ }
45
+ /** Resolved gate config with env fallbacks + defaults applied. */
46
+ export interface ResolvedAuthGateConfig {
47
+ allow: string[];
48
+ subject: string;
49
+ brandName: string;
50
+ message: string;
51
+ sessionTtlSeconds: number;
52
+ codeTtlSeconds: number;
53
+ maxAttempts: number;
54
+ }
55
+ /** Merge {@link AuthGateConfig} over env over defaults into a resolved config. */
56
+ export declare function resolveAuthGateConfig(config: AuthGateConfig): ResolvedAuthGateConfig;
57
+ /** The handlers the proxy calls in-process (returned by {@link AuthGatePlugin.exports}). */
58
+ export interface AuthGateApi {
59
+ /** Handle a code request. Always resolves `{ ok: true }` (anti-enumeration). */
60
+ request(email: string, ip: string): Promise<{
61
+ ok: true;
62
+ retryAfter?: number;
63
+ }>;
64
+ /** Handle a code verification. On success returns the session token to cookie. */
65
+ verify(email: string, code: string, ip: string): Promise<{
66
+ ok: boolean;
67
+ token?: string;
68
+ retryAfter?: number;
69
+ }>;
70
+ /** Resolve the authenticated email for a session token, or undefined. */
71
+ session(token: string | undefined): Promise<string | undefined>;
72
+ /** Session TTL in seconds (for the cookie Max-Age). */
73
+ readonly sessionTtlSeconds: number;
74
+ /** The gate status payload (`enabled` is always true when this plugin runs). */
75
+ status(token: string | undefined): Promise<AuthStatus>;
76
+ }
77
+ /** AppKit plugin owning the email-OTP gate's logic (no HTTP routes; proxy-driven). */
78
+ export declare class AuthGatePlugin extends Plugin<AuthGateConfig> {
79
+ static manifest: {
80
+ name: "authGate";
81
+ displayName: string;
82
+ description: string;
83
+ stability: "beta";
84
+ resources: {
85
+ required: never[];
86
+ optional: never[];
87
+ };
88
+ };
89
+ private resolved;
90
+ private codes;
91
+ private readonly requestLimiter;
92
+ private readonly verifyLimiter;
93
+ setup(): Promise<void>;
94
+ exports(): AuthGateApi;
95
+ private handleRequest;
96
+ private handleVerify;
97
+ }
98
+ /** Factory: `authGate({ allow, subject, ... })` for an AppKit `plugins` array. */
99
+ export declare const authGate: import("@databricks/appkit").ToPlugin<typeof AuthGatePlugin, AuthGateConfig, "authGate">;
@@ -0,0 +1,123 @@
1
+ /**
2
+ * `authGate()` - the AppKit plugin behind the tunnel's email-OTP gate.
3
+ *
4
+ * It has NO routes of its own: the tunnel PROXY (not an HTTP server) calls the
5
+ * handlers this plugin exposes via {@link AuthGatePlugin.exports}. The plugin
6
+ * owns the allow-list, the per-email/per-IP rate limiters, the CacheManager-backed
7
+ * one-time-code store, and the session JWT. `createApp` (with no `server()`) is
8
+ * used only to auto-init `CacheManager` + prime the sibling `email` transport;
9
+ * this plugin is where the gate logic lives.
10
+ *
11
+ * Options come from CLI flags OR env, with sensible defaults - see
12
+ * {@link resolveAuthGateConfig}. The one runtime dependency the plugin can't
13
+ * resolve itself is HOW to email the code, so it takes a `sendCode` callback the
14
+ * app wires to the email plugin.
15
+ *
16
+ * @module
17
+ */
18
+ import { Plugin, toPlugin } from "@databricks/appkit";
19
+ import { log, string } from "@dbx-tools/shared-core";
20
+ import { looksLikeEmail, matchesAllowlist } from "./allowlist.js";
21
+ import { CodeStore, signSession, verifySession } from "./otp.js";
22
+ import { RateLimiter } from "./rate-limit.js";
23
+ const logger = log.logger("tunnel:auth");
24
+ const DEFAULTS = {
25
+ subject: "Your sign-in code",
26
+ brandName: "This app",
27
+ message: "Your one-time sign-in code is:",
28
+ sessionTtlSeconds: 43200,
29
+ codeTtlSeconds: 600,
30
+ maxAttempts: 5,
31
+ };
32
+ /** Positive finite number from a value / env string, else the fallback. */
33
+ function num(value, env, fallback) {
34
+ const raw = value ?? (env ? Number(env) : undefined);
35
+ return typeof raw === "number" && Number.isFinite(raw) && raw > 0 ? raw : fallback;
36
+ }
37
+ /** Merge {@link AuthGateConfig} over env over defaults into a resolved config. */
38
+ export function resolveAuthGateConfig(config) {
39
+ return {
40
+ allow: [...string.parseList(config.allow), ...string.parseList(process.env.EMAIL_AUTH_ALLOW)],
41
+ subject: config.subject ?? process.env.AUTH_SUBJECT?.trim() ?? DEFAULTS.subject,
42
+ brandName: config.brandName ?? process.env.AUTH_BRAND_NAME?.trim() ?? DEFAULTS.brandName,
43
+ message: config.message ?? process.env.AUTH_MESSAGE?.trim() ?? DEFAULTS.message,
44
+ sessionTtlSeconds: num(config.sessionTtlSeconds, process.env.AUTH_SESSION_TTL, DEFAULTS.sessionTtlSeconds),
45
+ codeTtlSeconds: num(config.codeTtlSeconds, process.env.AUTH_CODE_TTL, DEFAULTS.codeTtlSeconds),
46
+ maxAttempts: config.maxAttempts ?? DEFAULTS.maxAttempts,
47
+ };
48
+ }
49
+ /** AppKit plugin owning the email-OTP gate's logic (no HTTP routes; proxy-driven). */
50
+ export class AuthGatePlugin extends Plugin {
51
+ static manifest = {
52
+ name: "authGate",
53
+ displayName: "Auth Gate",
54
+ description: "Email one-time-password access gate for a public tunnel.",
55
+ stability: "beta",
56
+ resources: { required: [], optional: [] },
57
+ };
58
+ resolved;
59
+ codes;
60
+ // Requesting a code is email-spam-prone; verifying is a brute-force surface.
61
+ // Per-email AND per-IP so neither axis alone is a bypass.
62
+ requestLimiter = new RateLimiter(5, 15 * 60 * 1000);
63
+ verifyLimiter = new RateLimiter(10, 15 * 60 * 1000);
64
+ async setup() {
65
+ this.resolved = resolveAuthGateConfig(this.config);
66
+ this.codes = new CodeStore(this.resolved.codeTtlSeconds, this.resolved.maxAttempts);
67
+ logger.info("ready", {
68
+ patterns: this.resolved.allow.length,
69
+ sessionTtlSeconds: this.resolved.sessionTtlSeconds,
70
+ });
71
+ }
72
+ exports() {
73
+ return {
74
+ sessionTtlSeconds: this.resolved.sessionTtlSeconds,
75
+ request: (email, ip) => this.handleRequest(email, ip),
76
+ verify: (email, code, ip) => this.handleVerify(email, code, ip),
77
+ session: (token) => verifySession(token),
78
+ status: async (token) => ({
79
+ authenticated: Boolean(await verifySession(token)),
80
+ email: (await verifySession(token)) ?? undefined,
81
+ enabled: true,
82
+ }),
83
+ };
84
+ }
85
+ async handleRequest(email, ip) {
86
+ const address = email.trim().toLowerCase();
87
+ const byIp = this.requestLimiter.hit(`ip:${ip}`);
88
+ const byEmail = this.requestLimiter.hit(`email:${address}`);
89
+ if (!byIp.allowed || !byEmail.allowed) {
90
+ return { ok: true, retryAfter: byIp.retryAfter ?? byEmail.retryAfter };
91
+ }
92
+ if (looksLikeEmail(address) && matchesAllowlist(address, this.resolved.allow)) {
93
+ const code = await this.codes.issue(address);
94
+ try {
95
+ await this.config.sendCode?.(address, code, {
96
+ subject: this.resolved.subject,
97
+ brandName: this.resolved.brandName,
98
+ message: this.resolved.message,
99
+ });
100
+ }
101
+ catch (error) {
102
+ logger.warn("failed to send OTP email", { error });
103
+ }
104
+ }
105
+ return { ok: true };
106
+ }
107
+ async handleVerify(email, code, ip) {
108
+ const address = email.trim().toLowerCase();
109
+ const byIp = this.verifyLimiter.hit(`ip:${ip}`);
110
+ const byEmail = this.verifyLimiter.hit(`email:${address}`);
111
+ if (!byIp.allowed || !byEmail.allowed) {
112
+ return { ok: false, retryAfter: byIp.retryAfter ?? byEmail.retryAfter };
113
+ }
114
+ if ((await this.codes.verify(address, code.trim())) !== "ok")
115
+ return { ok: false };
116
+ this.requestLimiter.reset(`email:${address}`);
117
+ this.verifyLimiter.reset(`email:${address}`);
118
+ return { ok: true, token: await signSession(address, this.resolved.sessionTtlSeconds) };
119
+ }
120
+ }
121
+ /** Factory: `authGate({ allow, subject, ... })` for an AppKit `plugins` array. */
122
+ export const authGate = toPlugin(AuthGatePlugin);
123
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGx1Z2luLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3BsdWdpbi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7Ozs7Ozs7Ozs7OztHQWdCRztBQUVILE9BQU8sRUFBRSxNQUFNLEVBQUUsUUFBUSxFQUE4QyxNQUFNLG9CQUFvQixDQUFDO0FBQ2xHLE9BQU8sRUFBRSxHQUFHLEVBQUUsTUFBTSxFQUFFLE1BQU0sd0JBQXdCLENBQUM7QUFFckQsT0FBTyxFQUFFLGNBQWMsRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLGdCQUFnQixDQUFDO0FBQ2xFLE9BQU8sRUFBRSxTQUFTLEVBQUUsV0FBVyxFQUFFLGFBQWEsRUFBRSxNQUFNLFVBQVUsQ0FBQztBQUNqRSxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0saUJBQWlCLENBQUM7QUFFOUMsTUFBTSxNQUFNLEdBQUcsR0FBRyxDQUFDLE1BQU0sQ0FBQyxhQUFhLENBQUMsQ0FBQztBQXdDekMsTUFBTSxRQUFRLEdBQUc7SUFDZixPQUFPLEVBQUUsbUJBQW1CO0lBQzVCLFNBQVMsRUFBRSxVQUFVO0lBQ3JCLE9BQU8sRUFBRSxnQ0FBZ0M7SUFDekMsaUJBQWlCLEVBQUUsS0FBSztJQUN4QixjQUFjLEVBQUUsR0FBRztJQUNuQixXQUFXLEVBQUUsQ0FBQztDQUNmLENBQUM7QUFFRiwyRUFBMkU7QUFDM0UsU0FBUyxHQUFHLENBQUMsS0FBeUIsRUFBRSxHQUF1QixFQUFFLFFBQWdCO0lBQy9FLE1BQU0sR0FBRyxHQUFHLEtBQUssSUFBSSxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUMsQ0FBQztJQUNyRCxPQUFPLE9BQU8sR0FBRyxLQUFLLFFBQVEsSUFBSSxNQUFNLENBQUMsUUFBUSxDQUFDLEdBQUcsQ0FBQyxJQUFJLEdBQUcsR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENB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SSxFQUFFLENBQUMsQ0FBQyxLQUFLLElBQUk7WUFBRSxPQUFPLEVBQUUsRUFBRSxFQUFFLEtBQUssRUFBRSxDQUFDO1FBQ25GLElBQUksQ0FBQyxjQUFjLENBQUMsS0FBSyxDQUFDLFNBQVMsT0FBTyxFQUFFLENBQUMsQ0FBQztRQUM5QyxJQUFJLENBQUMsYUFBYSxDQUFDLEtBQUssQ0FBQyxTQUFTLE9BQU8sRUFBRSxDQUFDLENBQUM7UUFDN0MsT0FBTyxFQUFFLEVBQUUsRUFBRSxJQUFJLEVBQUUsS0FBSyxFQUFFLE1BQU0sV0FBVyxDQUFDLE9BQU8sRUFBRSxJQUFJLENBQUMsUUFBUSxDQUFDLGlCQUFpQixDQUFDLEVBQUUsQ0FBQztJQUMxRixDQUFDOztBQUdILGtGQUFrRjtBQUNsRixNQUFNLENBQUMsTUFBTSxRQUFRLEdBQUcsUUFBUSxDQUFDLGNBQWMsQ0FBQyxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBgYXV0aEdhdGUoKWAgLSB0aGUgQXBwS2l0IHBsdWdpbiBiZWhpbmQgdGhlIHR1bm5lbCdzIGVtYWlsLU9UUCBnYXRlLlxuICpcbiAqIEl0IGhhcyBOTyByb3V0ZXMgb2YgaXRzIG93bjogdGhlIHR1bm5lbCBQUk9YWSAobm90IGFuIEhUVFAgc2VydmVyKSBjYWxscyB0aGVcbiAqIGhhbmRsZXJzIHRoaXMgcGx1Z2luIGV4cG9zZXMgdmlhIHtAbGluayBBdXRoR2F0ZVBsdWdpbi5leHBvcnRzfS4gVGhlIHBsdWdpblxuICogb3ducyB0aGUgYWxsb3ctbGlzdCwgdGhlIHBlci1lbWFpbC9wZXItSVAgcmF0ZSBsaW1pdGVycywgdGhlIENhY2hlTWFuYWdlci1iYWNrZWRcbiAqIG9uZS10aW1lLWNvZGUgc3RvcmUsIGFuZCB0aGUgc2Vzc2lvbiBKV1QuIGBjcmVhdGVBcHBgICh3aXRoIG5vIGBzZXJ2ZXIoKWApIGlzXG4gKiB1c2VkIG9ubHkgdG8gYXV0by1pbml0IGBDYWNoZU1hbmFnZXJgICsgcHJpbWUgdGhlIHNpYmxpbmcgYGVtYWlsYCB0cmFuc3BvcnQ7XG4gKiB0aGlzIHBsdWdpbiBpcyB3aGVyZSB0aGUgZ2F0ZSBsb2dpYyBsaXZlcy5cbiAqXG4gKiBPcHRpb25zIGNvbWUgZnJvbSBDTEkgZmxhZ3MgT1IgZW52LCB3aXRoIHNlbnNpYmxlIGRlZmF1bHRzIC0gc2VlXG4gKiB7QGxpbmsgcmVzb2x2ZUF1dGhHYXRlQ29uZmlnfS4gVGhlIG9uZSBydW50aW1lIGRlcGVuZGVuY3kgdGhlIHBsdWdpbiBjYW4ndFxuICogcmVzb2x2ZSBpdHNlbGYgaXMgSE9XIHRvIGVtYWlsIHRoZSBjb2RlLCBzbyBpdCB0YWtlcyBhIGBzZW5kQ29kZWAgY2FsbGJhY2sgdGhlXG4gKiBhcHAgd2lyZXMgdG8gdGhlIGVtYWlsIHBsdWdpbi5cbiAqXG4gKiBAbW9kdWxlXG4gKi9cblxuaW1wb3J0IHsgUGx1Z2luLCB0b1BsdWdpbiwgdHlwZSBCYXNlUGx1Z2luQ29uZmlnLCB0eXBlIFBsdWdpbk1hbmlmZXN0IH0gZnJvbSBcIkBkYXRhYnJpY2tzL2FwcGtpdFwiO1xuaW1wb3J0IHsgbG9nLCBzdHJpbmcgfSBmcm9tIFwiQGRieC10b29scy9zaGFyZWQtY29yZVwiO1xuaW1wb3J0IHR5cGUgeyBBdXRoU3RhdHVzIH0gZnJvbSBcIkBkYngtdG9vbHMvc2hhcmVkLWVtYWlsXCI7XG5pbXBvcnQgeyBsb29rc0xpa2VFbWFpbCwgbWF0Y2hlc0FsbG93bGlzdCB9IGZyb20gXCIuL2FsbG93bGlzdC50c1wiO1xuaW1wb3J0IHsgQ29kZVN0b3JlLCBzaWduU2Vzc2lvbiwgdmVyaWZ5U2Vzc2lvbiB9IGZyb20gXCIuL290cC50c1wiO1xuaW1wb3J0IHsgUmF0ZUxpbWl0ZXIgfSBmcm9tIFwiLi9yYXRlLWxpbWl0LnRzXCI7XG5cbmNvbnN0IGxvZ2dlciA9IGxvZy5sb2dnZXIoXCJ0dW5uZWw6YXV0aFwiKTtcblxuLyoqIE9wdGlvbnMgZm9yIHRoZSB7QGxpbmsgYXV0aEdhdGV9IHBsdWdpbiAoYWxsIHJlc29sdmFibGUgZnJvbSBlbnYgLSBzZWUgYmVsb3cpLiAqL1xuZXhwb3J0IGludGVyZmFjZSBBdXRoR2F0ZUNvbmZpZyBleHRlbmRzIEJhc2VQbHVnaW5Db25maWcge1xuICAvKiogQWxsb3ctbGlzdCBwYXR0ZXJucyAoZG9tYWluIC8gZ2xvYiAvIGAvcmVnZXgvYCkuIEVtcHR5ID0gYWxsb3cgbm9ib2R5LiBFbnYgRU1BSUxfQVVUSF9BTExPVy4gKi9cbiAgYWxsb3c/OiBzdHJpbmcgfCBzdHJpbmdbXTtcbiAgLyoqIFN1YmplY3QgbGluZSBmb3IgdGhlIGNvZGUgZW1haWwuIEVudiBBVVRIX1NVQkpFQ1QuICovXG4gIHN1YmplY3Q/OiBzdHJpbmc7XG4gIC8qKiBQcm9kdWN0L2JyYW5kIG5hbWUgdXNlZCBpbiB0aGUgZW1haWwgKyBsb2dpbiBjb3B5LiBFbnYgQVVUSF9CUkFORF9OQU1FLiAqL1xuICBicmFuZE5hbWU/OiBzdHJpbmc7XG4gIC8qKiBPbmUtbGluZSBtZXNzYWdlIHNob3duIGFib3ZlIHRoZSBjb2RlIGluIHRoZSBlbWFpbC4gRW52IEFVVEhfTUVTU0FHRS4gKi9cbiAgbWVzc2FnZT86IHN0cmluZztcbiAgLyoqIFNlc3Npb24gbGlmZXRpbWUgKHNlY29uZHMpLiBFbnYgQVVUSF9TRVNTSU9OX1RUTC4gRGVmYXVsdCA0MzIwMCAoMTJoKS4gKi9cbiAgc2Vzc2lvblR0bFNlY29uZHM/OiBudW1iZXI7XG4gIC8qKiBPbmUtdGltZS1jb2RlIGxpZmV0aW1lIChzZWNvbmRzKS4gRW52IEFVVEhfQ09ERV9UVEwuIERlZmF1bHQgNjAwICgxMG0pLiAqL1xuICBjb2RlVHRsU2Vjb25kcz86IG51bWJlcjtcbiAgLyoqIE1heCB2ZXJpZnkgYXR0ZW1wdHMgcGVyIGlzc3VlZCBjb2RlLiBEZWZhdWx0IDUuICovXG4gIG1heEF0dGVtcHRzPzogbnVtYmVyO1xuICAvKiogRGVsaXZlciBhIGNvZGUgdG8gYW4gYWRkcmVzcy4gV2lyZWQgYnkgdGhlIGFwcCB0byB0aGUgZW1haWwgcGx1Z2luLiAqL1xuICBzZW5kQ29kZT86IChlbWFpbDogc3RyaW5nLCBjb2RlOiBzdHJpbmcsIG9wdHM6IFNlbmRDb2RlT3B0aW9ucykgPT4gUHJvbWlzZTx2b2lkPjtcbn1cblxuLyoqIEJyYW5kaW5nL21lc3NhZ2luZyBwYXNzZWQgdG8ge0BsaW5rIEF1dGhHYXRlQ29uZmlnLnNlbmRDb2RlfS4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgU2VuZENvZGVPcHRpb25zIHtcbiAgc3ViamVjdDogc3RyaW5nO1xuICBicmFuZE5hbWU6IHN0cmluZztcbiAgbWVzc2FnZTogc3RyaW5nO1xufVxuXG4vKiogUmVzb2x2ZWQgZ2F0ZSBjb25maWcgd2l0aCBlbnYgZmFsbGJhY2tzICsgZGVmYXVsdHMgYXBwbGllZC4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgUmVzb2x2ZWRBdXRoR2F0ZUNvbmZpZyB7XG4gIGFsbG93OiBzdHJpbmdbXTtcbiAgc3ViamVjdDogc3RyaW5nO1xuICBicmFuZE5hbWU6IHN0cmluZztcbiAgbWVzc2FnZTogc3RyaW5nO1xuICBzZXNzaW9uVHRsU2Vjb25kczogbnVtYmVyO1xuICBjb2RlVHRsU2Vjb25kczogbnVtYmVyO1xuICBtYXhBdHRlbXB0czogbnVtYmVyO1xufVxuXG5jb25zdCBERUZBVUxUUyA9IHtcbiAgc3ViamVjdDogXCJZb3VyIHNpZ24taW4gY29kZVwiLFxuICBicmFuZE5hbWU6IFwiVGhpcyBhcHBcIixcbiAgbWVzc2FnZTogXCJZb3VyIG9uZS10aW1lIHNpZ24taW4gY29kZSBpczpcIixcbiAgc2Vzc2lvblR0bFNlY29uZHM6IDQzMjAwLFxuICBjb2RlVHRsU2Vjb25kczogNjAwLFxuICBtYXhBdHRlbXB0czogNSxcbn07XG5cbi8qKiBQb3NpdGl2ZSBmaW5pdGUgbnVtYmVyIGZyb20gYSB2YWx1ZSAvIGVudiBzdHJpbmcsIGVsc2UgdGhlIGZhbGxiYWNrLiAqL1xuZnVuY3Rpb24gbnVtKHZhbHVlOiBudW1iZXIgfCB1bmRlZmluZWQsIGVudjogc3RyaW5nIHwgdW5kZWZpbmVkLCBmYWxsYmFjazogbnVtYmVyKTogbnVtYmVyIHtcbiAgY29uc3QgcmF3ID0gdmFsdWUgPz8gKGVudiA/IE51bWJlcihlbnYpIDogdW5kZWZpbmVkKTtcbiAgcmV0dXJuIHR5cGVvZiByYXcgPT09IFwibnVtYmVyXCIgJiYgTnVtYmVyLmlzRmluaXRlKHJhdykgJiYgcmF3ID4gMCA/IHJhdyA6IGZhbGxiYWNrO1xufVxuXG4vKiogTWVyZ2Uge0BsaW5rIEF1dGhHYXRlQ29uZmlnfSBvdmVyIGVudiBvdmVyIGRlZmF1bHRzIGludG8gYSByZXNvbHZlZCBjb25maWcuICovXG5leHBvcnQgZnVuY3Rpb24gcmVzb2x2ZUF1dGhHYXRlQ29uZmlnKGNvbmZpZzogQXV0aEdhdGVDb25maWcpOiBSZXNvbHZlZEF1dGhHYXRlQ29uZmlnIHtcbiAgcmV0dXJuIHtcbiAgICBhbGxvdzogWy4uLnN0cmluZy5wYXJzZUxpc3QoY29uZmlnLmFsbG93KSwgLi4uc3RyaW5nLnBhcnNlTGlzdChwcm9jZXNzLmVudi5FTUFJTF9BVVRIX0FMTE9XKV0sXG4gICAgc3ViamVjdDogY29uZmlnLnN1YmplY3QgPz8gcHJvY2Vzcy5lbnYuQVVUSF9TVUJKRUNUPy50cmltKCkgPz8gREVGQVVMVFMuc3ViamVjdCxcbiAgICBicmFuZE5hbWU6IGNvbmZpZy5icmFuZE5hbWUgPz8gcHJvY2Vzcy5lbnYuQVVUSF9CUkFORF9OQU1FPy50cmltKCkgPz8gREVGQVVMVFMuYnJhbmROYW1lLFxuICAgIG1lc3NhZ2U6IGNvbmZpZy5tZXNzYWdlID8/IHByb2Nlc3MuZW52LkFVVEhfTUVTU0FHRT8udHJpbSgpID8/IERFRkFVTFRTLm1lc3NhZ2UsXG4gICAgc2Vzc2lvblR0bFNlY29uZHM6IG51bShcbiAgICAgIGNvbmZpZy5zZXNzaW9uVHRsU2Vjb25kcyxcbiAgICAgIHByb2Nlc3MuZW52LkFVVEhfU0VTU0lPTl9UVEwsXG4gICAgICBERUZBVUxUUy5zZXNzaW9uVHRsU2Vjb25kcyxcbiAgICApLFxuICAgIGNvZGVUdGxTZWNvbmRzOiBudW0oY29uZmlnLmNvZGVUdGxTZWNvbmRzLCBwcm9jZXNzLmVudi5BVVRIX0NPREVfVFRMLCBERUZBVUxUUy5jb2RlVHRsU2Vjb25kcyksXG4gICAgbWF4QXR0ZW1wdHM6IGNvbmZpZy5tYXhBdHRlbXB0cyA/PyBERUZBVUxUUy5tYXhBdHRlbXB0cyxcbiAgfTtcbn1cblxuLyoqIFRoZSBoYW5kbGVycyB0aGUgcHJveHkgY2FsbHMgaW4tcHJvY2VzcyAocmV0dXJuZWQgYnkge0BsaW5rIEF1dGhHYXRlUGx1Z2luLmV4cG9ydHN9KS4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgQXV0aEdhdGVBcGkge1xuICAvKiogSGFuZGxlIGEgY29kZSByZXF1ZXN0LiBBbHdheXMgcmVzb2x2ZXMgYHsgb2s6IHRydWUgfWAgKGFudGktZW51bWVyYXRpb24pLiAqL1xuICByZXF1ZXN0KGVtYWlsOiBzdHJpbmcsIGlwOiBzdHJpbmcpOiBQcm9taXNlPHsgb2s6IHRydWU7IHJldHJ5QWZ0ZXI/OiBudW1iZXIgfT47XG4gIC8qKiBIYW5kbGUgYSBjb2RlIHZlcmlmaWNhdGlvbi4gT24gc3VjY2VzcyByZXR1cm5zIHRoZSBzZXNzaW9uIHRva2VuIHRvIGNvb2tpZS4gKi9cbiAgdmVyaWZ5KFxuICAgIGVtYWlsOiBzdHJpbmcsXG4gICAgY29kZTogc3RyaW5nLFxuICAgIGlwOiBzdHJpbmcsXG4gICk6IFByb21pc2U8eyBvazogYm9vbGVhbjsgdG9rZW4/OiBzdHJpbmc7IHJldHJ5QWZ0ZXI/OiBudW1iZXIgfT47XG4gIC8qKiBSZXNvbHZlIHRoZSBhdXRoZW50aWNhdGVkIGVtYWlsIGZvciBhIHNlc3Npb24gdG9rZW4sIG9yIHVuZGVmaW5lZC4gKi9cbiAgc2Vzc2lvbih0b2tlbjogc3RyaW5nIHwgdW5kZWZpbmVkKTogUHJvbWlzZTxzdHJpbmcgfCB1bmRlZmluZWQ+O1xuICAvKiogU2Vzc2lvbiBUVEwgaW4gc2Vjb25kcyAoZm9yIHRoZSBjb29raWUgTWF4LUFnZSkuICovXG4gIHJlYWRvbmx5IHNlc3Npb25UdGxTZWNvbmRzOiBudW1iZXI7XG4gIC8qKiBUaGUgZ2F0ZSBzdGF0dXMgcGF5bG9hZCAoYGVuYWJsZWRgIGlzIGFsd2F5cyB0cnVlIHdoZW4gdGhpcyBwbHVnaW4gcnVucykuICovXG4gIHN0YXR1cyh0b2tlbjogc3RyaW5nIHwgdW5kZWZpbmVkKTogUHJvbWlzZTxBdXRoU3RhdHVzPjtcbn1cblxuLyoqIEFwcEtpdCBwbHVnaW4gb3duaW5nIHRoZSBlbWFpbC1PVFAgZ2F0ZSdzIGxvZ2ljIChubyBIVFRQIHJvdXRlczsgcHJveHktZHJpdmVuKS4gKi9cbmV4cG9ydCBjbGFzcyBBdXRoR2F0ZVBsdWdpbiBleHRlbmRzIFBsdWdpbjxBdXRoR2F0ZUNvbmZpZz4ge1xuICBzdGF0aWMgbWFuaWZlc3QgPSB7XG4gICAgbmFtZTogXCJhdXRoR2F0ZVwiLFxuICAgIGRpc3BsYXlOYW1lOiBcIkF1dGggR2F0ZVwiLFxuICAgIGRlc2NyaXB0aW9uOiBcIkVtYWlsIG9uZS10aW1lLXBhc3N3b3JkIGFjY2VzcyBnYXRlIGZvciBhIHB1YmxpYyB0dW5uZWwuXCIsXG4gICAgc3RhYmlsaXR5OiBcImJldGFcIixcbiAgICByZXNvdXJjZXM6IHsgcmVxdWlyZWQ6IFtdLCBvcHRpb25hbDogW10gfSxcbiAgfSBzYXRpc2ZpZXMgUGx1Z2luTWFuaWZlc3Q8XCJhdXRoR2F0ZVwiPjtcblxuICBwcml2YXRlIHJlc29sdmVkITogUmVzb2x2ZWRBdXRoR2F0ZUNvbmZpZztcbiAgcHJpdmF0ZSBjb2RlcyE6IENvZGVTdG9yZTtcbiAgLy8gUmVxdWVzdGluZyBhIGNvZGUgaXMgZW1haWwtc3BhbS1wcm9uZTsgdmVyaWZ5aW5nIGlzIGEgYnJ1dGUtZm9yY2Ugc3VyZmFjZS5cbiAgLy8gUGVyLWVtYWlsIEFORCBwZXItSVAgc28gbmVpdGhlciBheGlzIGFsb25lIGlzIGEgYnlwYXNzLlxuICBwcml2YXRlIHJlYWRvbmx5IHJlcXVlc3RMaW1pdGVyID0gbmV3IFJhdGVMaW1pdGVyKDUsIDE1ICogNjAgKiAxMDAwKTtcbiAgcHJpdmF0ZSByZWFkb25seSB2ZXJpZnlMaW1pdGVyID0gbmV3IFJhdGVMaW1pdGVyKDEwLCAxNSAqIDYwICogMTAwMCk7XG5cbiAgb3ZlcnJpZGUgYXN5bmMgc2V0dXAoKTogUHJvbWlzZTx2b2lkPiB7XG4gICAgdGhpcy5yZXNvbHZlZCA9IHJlc29sdmVBdXRoR2F0ZUNvbmZpZyh0aGlzLmNvbmZpZyk7XG4gICAgdGhpcy5jb2RlcyA9IG5ldyBDb2RlU3RvcmUodGhpcy5yZXNvbHZlZC5jb2RlVHRsU2Vjb25kcywgdGhpcy5yZXNvbHZlZC5tYXhBdHRlbXB0cyk7XG4gICAgbG9nZ2VyLmluZm8oXCJyZWFkeVwiLCB7XG4gICAgICBwYXR0ZXJuczogdGhpcy5yZXNvbHZlZC5hbGxvdy5sZW5ndGgsXG4gICAgICBzZXNzaW9uVHRsU2Vjb25kczogdGhpcy5yZXNvbHZlZC5zZXNzaW9uVHRsU2Vjb25kcyxcbiAgICB9KTtcbiAgfVxuXG4gIG92ZXJyaWRlIGV4cG9ydHMoKTogQXV0aEdhdGVBcGkge1xuICAgIHJldHVybiB7XG4gICAgICBzZXNzaW9uVHRsU2Vjb25kczogdGhpcy5yZXNvbHZlZC5zZXNzaW9uVHRsU2Vjb25kcyxcbiAgICAgIHJlcXVlc3Q6IChlbWFpbCwgaXApID0+IHRoaXMuaGFuZGxlUmVxdWVzdChlbWFpbCwgaXApLFxuICAgICAgdmVyaWZ5OiAoZW1haWwsIGNvZGUsIGlwKSA9PiB0aGlzLmhhbmRsZVZlcmlmeShlbWFpbCwgY29kZSwgaXApLFxuICAgICAgc2Vzc2lvbjogKHRva2VuKSA9PiB2ZXJpZnlTZXNzaW9uKHRva2VuKSxcbiAgICAgIHN0YXR1czogYXN5bmMgKHRva2VuKSA9PiAoe1xuICAgICAgICBhdXRoZW50aWNhdGVkOiBCb29sZWFuKGF3YWl0IHZlcmlmeVNlc3Npb24odG9rZW4pKSxcbiAgICAgICAgZW1haWw6IChhd2FpdCB2ZXJpZnlTZXNzaW9uKHRva2VuKSkgPz8gdW5kZWZpbmVkLFxuICAgICAgICBlbmFibGVkOiB0cnVlLFxuICAgICAgfSksXG4gICAgfTtcbiAgfVxuXG4gIHByaXZhdGUgYXN5bmMgaGFuZGxlUmVxdWVzdChcbiAgICBlbWFpbDogc3RyaW5nLFxuICAgIGlwOiBzdHJpbmcsXG4gICk6IFByb21pc2U8eyBvazogdHJ1ZTsgcmV0cnlBZnRlcj86IG51bWJlciB9PiB7XG4gICAgY29uc3QgYWRkcmVzcyA9IGVtYWlsLnRyaW0oKS50b0xvd2VyQ2FzZSgpO1xuICAgIGNvbnN0IGJ5SXAgPSB0aGlzLnJlcXVlc3RMaW1pdGVyLmhpdChgaXA6JHtpcH1gKTtcbiAgICBjb25zdCBieUVtYWlsID0gdGhpcy5yZXF1ZXN0TGltaXRlci5oaXQoYGVtYWlsOiR7YWRkcmVzc31gKTtcbiAgICBpZiAoIWJ5SXAuYWxsb3dlZCB8fCAhYnlFbWFpbC5hbGxvd2VkKSB7XG4gICAgICByZXR1cm4geyBvazogdHJ1ZSwgcmV0cnlBZnRlcjogYnlJcC5yZXRyeUFmdGVyID8/IGJ5RW1haWwucmV0cnlBZnRlciB9O1xuICAgIH1cbiAgICBpZiAobG9va3NMaWtlRW1haWwoYWRkcmVzcykgJiYgbWF0Y2hlc0FsbG93bGlzdChhZGRyZXNzLCB0aGlzLnJlc29sdmVkLmFsbG93KSkge1xuICAgICAgY29uc3QgY29kZSA9IGF3YWl0IHRoaXMuY29kZXMuaXNzdWUoYWRkcmVzcyk7XG4gICAgICB0cnkge1xuICAgICAgICBhd2FpdCB0aGlzLmNvbmZpZy5zZW5kQ29kZT8uKGFkZHJlc3MsIGNvZGUsIHtcbiAgICAgICAgICBzdWJqZWN0OiB0aGlzLnJlc29sdmVkLnN1YmplY3QsXG4gICAgICAgICAgYnJhbmROYW1lOiB0aGlzLnJlc29sdmVkLmJyYW5kTmFtZSxcbiAgICAgICAgICBtZXNzYWdlOiB0aGlzLnJlc29sdmVkLm1lc3NhZ2UsXG4gICAgICAgIH0pO1xuICAgICAgfSBjYXRjaCAoZXJyb3IpIHtcbiAgICAgICAgbG9nZ2VyLndhcm4oXCJmYWlsZWQgdG8gc2VuZCBPVFAgZW1haWxcIiwgeyBlcnJvciB9KTtcbiAgICAgIH1cbiAgICB9XG4gICAgcmV0dXJuIHsgb2s6IHRydWUgfTtcbiAgfVxuXG4gIHByaXZhdGUgYXN5bmMgaGFuZGxlVmVyaWZ5KFxuICAgIGVtYWlsOiBzdHJpbmcsXG4gICAgY29kZTogc3RyaW5nLFxuICAgIGlwOiBzdHJpbmcsXG4gICk6IFByb21pc2U8eyBvazogYm9vbGVhbjsgdG9rZW4/OiBzdHJpbmc7IHJldHJ5QWZ0ZXI/OiBudW1iZXIgfT4ge1xuICAgIGNvbnN0IGFkZHJlc3MgPSBlbWFpbC50cmltKCkudG9Mb3dlckNhc2UoKTtcbiAgICBjb25zdCBieUlwID0gdGhpcy52ZXJpZnlMaW1pdGVyLmhpdChgaXA6JHtpcH1gKTtcbiAgICBjb25zdCBieUVtYWlsID0gdGhpcy52ZXJpZnlMaW1pdGVyLmhpdChgZW1haWw6JHthZGRyZXNzfWApO1xuICAgIGlmICghYnlJcC5hbGxvd2VkIHx8ICFieUVtYWlsLmFsbG93ZWQpIHtcbiAgICAgIHJldHVybiB7IG9rOiBmYWxzZSwgcmV0cnlBZnRlcjogYnlJcC5yZXRyeUFmdGVyID8/IGJ5RW1haWwucmV0cnlBZnRlciB9O1xuICAgIH1cbiAgICBpZiAoKGF3YWl0IHRoaXMuY29kZXMudmVyaWZ5KGFkZHJlc3MsIGNvZGUudHJpbSgpKSkgIT09IFwib2tcIikgcmV0dXJuIHsgb2s6IGZhbHNlIH07XG4gICAgdGhpcy5yZXF1ZXN0TGltaXRlci5yZXNldChgZW1haWw6JHthZGRyZXNzfWApO1xuICAgIHRoaXMudmVyaWZ5TGltaXRlci5yZXNldChgZW1haWw6JHthZGRyZXNzfWApO1xuICAgIHJldHVybiB7IG9rOiB0cnVlLCB0b2tlbjogYXdhaXQgc2lnblNlc3Npb24oYWRkcmVzcywgdGhpcy5yZXNvbHZlZC5zZXNzaW9uVHRsU2Vjb25kcykgfTtcbiAgfVxufVxuXG4vKiogRmFjdG9yeTogYGF1dGhHYXRlKHsgYWxsb3csIHN1YmplY3QsIC4uLiB9KWAgZm9yIGFuIEFwcEtpdCBgcGx1Z2luc2AgYXJyYXkuICovXG5leHBvcnQgY29uc3QgYXV0aEdhdGUgPSB0b1BsdWdpbihBdXRoR2F0ZVBsdWdpbik7XG4iXX0=
@@ -0,0 +1,39 @@
1
+ /**
2
+ * portr install + config + launch for the tunnel CLI.
3
+ *
4
+ * On a Databricks App the container's `$HOME` is read-only on cold start, so the
5
+ * portr binary and its config are placed under a writable, cwd-rooted `.home`.
6
+ * The install is idempotent (the installer skips when the on-PATH binary is
7
+ * current). The config is rendered from `PUBLIC_DOMAIN` (`<subdomain>.<server>`)
8
+ * + `PORTR_TOKEN` and points portr at the PUBLIC port (the proxy listens there).
9
+ *
10
+ * @module
11
+ */
12
+ import { spawn } from "node:child_process";
13
+ /** Resolved portr wiring, or `undefined` when no tunnel is configured. */
14
+ export interface PortrConfig {
15
+ subdomain: string;
16
+ server: string;
17
+ token: string;
18
+ port: number;
19
+ }
20
+ /**
21
+ * Resolve portr config from `PUBLIC_DOMAIN` + `PORTR_TOKEN`, or an explicit
22
+ * `subdomain`. `PUBLIC_DOMAIN` is `<subdomain>.<server>` (e.g.
23
+ * `demo.apps.dbx.tools`). Returns `undefined` (no tunnel) when the token or a
24
+ * usable domain is absent.
25
+ */
26
+ export declare function resolvePortrConfig(opts: {
27
+ publicDomain?: string;
28
+ subdomain?: string;
29
+ token?: string;
30
+ port: number;
31
+ }): PortrConfig | undefined;
32
+ /** Install portr (idempotent) into the cwd-rooted home and return the child env. */
33
+ export declare function installPortr(): NodeJS.ProcessEnv;
34
+ /** Render `~/.portr/config.yaml` for the resolved tunnel. */
35
+ export declare function writePortrConfig(config: PortrConfig, env: NodeJS.ProcessEnv): void;
36
+ /** Launch `portr start` as a child process (caller supervises + kills it). */
37
+ export declare function startPortr(config: PortrConfig, env: NodeJS.ProcessEnv): ReturnType<typeof spawn>;
38
+ /** True when the installed portr binary path exists (post-install sanity). */
39
+ export declare function portrInstalled(env: NodeJS.ProcessEnv): boolean;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * portr install + config + launch for the tunnel CLI.
3
+ *
4
+ * On a Databricks App the container's `$HOME` is read-only on cold start, so the
5
+ * portr binary and its config are placed under a writable, cwd-rooted `.home`.
6
+ * The install is idempotent (the installer skips when the on-PATH binary is
7
+ * current). The config is rendered from `PUBLIC_DOMAIN` (`<subdomain>.<server>`)
8
+ * + `PORTR_TOKEN` and points portr at the PUBLIC port (the proxy listens there).
9
+ *
10
+ * @module
11
+ */
12
+ import { spawn, spawnSync } from "node:child_process";
13
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
14
+ import { delimiter, join } from "node:path";
15
+ import { log } from "@dbx-tools/shared-core";
16
+ const logger = log.logger("tunnel:portr");
17
+ /**
18
+ * Resolve portr config from `PUBLIC_DOMAIN` + `PORTR_TOKEN`, or an explicit
19
+ * `subdomain`. `PUBLIC_DOMAIN` is `<subdomain>.<server>` (e.g.
20
+ * `demo.apps.dbx.tools`). Returns `undefined` (no tunnel) when the token or a
21
+ * usable domain is absent.
22
+ */
23
+ export function resolvePortrConfig(opts) {
24
+ const token = opts.token ?? process.env.PORTR_TOKEN;
25
+ const domain = opts.publicDomain ?? process.env.PUBLIC_DOMAIN;
26
+ if (!token)
27
+ return undefined;
28
+ let subdomain = opts.subdomain;
29
+ let server;
30
+ if (domain) {
31
+ subdomain ??= domain.split(".")[0];
32
+ server = domain.slice(domain.indexOf(".") + 1);
33
+ }
34
+ server ??= process.env.PORTR_SERVER;
35
+ if (!subdomain || !server || server === domain)
36
+ return undefined;
37
+ return { subdomain, server, token, port: opts.port };
38
+ }
39
+ /** The writable home portr installs + configures under (Apps `$HOME` is read-only). */
40
+ function portrHome() {
41
+ const home = join(process.cwd(), ".home");
42
+ mkdirSync(join(home, ".portr", "bin"), { recursive: true });
43
+ return home;
44
+ }
45
+ /** Install portr (idempotent) into the cwd-rooted home and return the child env. */
46
+ export function installPortr() {
47
+ const home = portrHome();
48
+ const env = {
49
+ ...process.env,
50
+ HOME: home,
51
+ PORTR_AUTO_ADD_PATH: "no",
52
+ PATH: [join(home, ".portr", "bin"), process.env.PATH ?? ""].join(delimiter),
53
+ };
54
+ logger.info("installing portr (idempotent)");
55
+ const res = spawnSync("bash", ["-c", "curl -sSf https://install.portr.dev | sh"], {
56
+ env,
57
+ stdio: "inherit",
58
+ });
59
+ if (res.status !== 0)
60
+ throw new Error("portr install failed");
61
+ return env;
62
+ }
63
+ /** Render `~/.portr/config.yaml` for the resolved tunnel. */
64
+ export function writePortrConfig(config, env) {
65
+ const path = join(env.HOME, ".portr", "config.yaml");
66
+ writeFileSync(path, [
67
+ `server_url: ${config.server}`,
68
+ `ssh_url: ${config.server}:4444`,
69
+ `secret_key: ${config.token}`,
70
+ "disable_dashboard: true",
71
+ "disable_tui: true",
72
+ "tunnels:",
73
+ ` - name: ${config.subdomain}`,
74
+ ` subdomain: ${config.subdomain}`,
75
+ ` port: ${config.port}`,
76
+ "",
77
+ ].join("\n"));
78
+ }
79
+ /** Launch `portr start` as a child process (caller supervises + kills it). */
80
+ export function startPortr(config, env) {
81
+ // Reclaim the subdomain from any portr left by a previous boot in this container.
82
+ spawnSync("pkill", ["-x", "portr"], { stdio: "ignore" });
83
+ logger.info(`portr tunneling https://${config.subdomain}.${config.server} -> :${config.port}`);
84
+ return spawn("portr", ["start"], { env, stdio: "inherit" });
85
+ }
86
+ /** True when the installed portr binary path exists (post-install sanity). */
87
+ export function portrInstalled(env) {
88
+ return existsSync(join(env.HOME, ".portr", "bin", "portr"));
89
+ }
90
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicG9ydHIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvcG9ydHIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7Ozs7R0FVRztBQUVILE9BQU8sRUFBRSxLQUFLLEVBQUUsU0FBUyxFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFDdEQsT0FBTyxFQUFFLFVBQVUsRUFBRSxTQUFTLEVBQUUsYUFBYSxFQUFFLE1BQU0sU0FBUyxDQUFDO0FBQy9ELE9BQU8sRUFBRSxTQUFTLEVBQUUsSUFBSSxFQUFFLE1BQU0sV0FBVyxDQUFDO0FBQzVDLE9BQU8sRUFBRSxHQUFHLEVBQUUsTUFBTSx3QkFBd0IsQ0FBQztBQUU3QyxNQUFNLE1BQU0sR0FBRyxHQUFHLENBQUMsTUFBTSxDQUFDLGNBQWMsQ0FBQyxDQUFDO0FBVTFDOzs7OztHQUtHO0FBQ0gsTUFBTSxVQUFVLGtCQUFrQixDQUFDLElBS2xDO0lBQ0MsTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLEtBQUssSUFBSSxPQUFPLENBQUMsR0FBRyxDQUFDLFdBQVcsQ0FBQztJQUNwRCxNQUFNLE1BQU0sR0FBRyxJQUFJLENBQUMsWUFBWSxJQUFJLE9BQU8sQ0FBQyxHQUFHLENBQUMsYUFBYSxDQUFDO0lBQzlELElBQUksQ0FBQyxLQUFLO1FBQUUsT0FBTyxTQUFTLENBQUM7SUFDN0IsSUFBSSxTQUFTLEdBQUcsSUFBSSxDQUFDLFNBQVMsQ0FBQztJQUMvQixJQUFJLE1BQTBCLENBQUM7SUFDL0IsSUFBSSxNQUFNLEVBQUUsQ0FBQztRQUNYLFNBQVMsS0FBSyxNQUFNLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO1FBQ25DLE1BQU0sR0FBRyxNQUFNLENBQUMsS0FBSyxDQUFDLE1BQU0sQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUM7SUFDakQsQ0FBQztJQUNELE1BQU0sS0FBSyxPQUFPLENBQUMsR0FBRyxDQUFD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QUFDOUUsTUFBTSxVQUFVLGNBQWMsQ0FBQyxHQUFzQjtJQUNuRCxPQUFPLFVBQVUsQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLElBQUssRUFBRSxRQUFRLEVBQUUsS0FBSyxFQUFFLE9BQU8sQ0FBQyxDQUFDLENBQUM7QUFDL0QsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogcG9ydHIgaW5zdGFsbCArIGNvbmZpZyArIGxhdW5jaCBmb3IgdGhlIHR1bm5lbCBDTEkuXG4gKlxuICogT24gYSBEYXRhYnJpY2tzIEFwcCB0aGUgY29udGFpbmVyJ3MgYCRIT01FYCBpcyByZWFkLW9ubHkgb24gY29sZCBzdGFydCwgc28gdGhlXG4gKiBwb3J0ciBiaW5hcnkgYW5kIGl0cyBjb25maWcgYXJlIHBsYWNlZCB1bmRlciBhIHdyaXRhYmxlLCBjd2Qtcm9vdGVkIGAuaG9tZWAuXG4gKiBUaGUgaW5zdGFsbCBpcyBpZGVtcG90ZW50ICh0aGUgaW5zdGFsbGVyIHNraXBzIHdoZW4gdGhlIG9uLVBBVEggYmluYXJ5IGlzXG4gKiBjdXJyZW50KS4gVGhlIGNvbmZpZyBpcyByZW5kZXJlZCBmcm9tIGBQVUJMSUNfRE9NQUlOYCAoYDxzdWJkb21haW4+LjxzZXJ2ZXI+YClcbiAqICsgYFBPUlRSX1RPS0VOYCBhbmQgcG9pbnRzIHBvcnRyIGF0IHRoZSBQVUJMSUMgcG9ydCAodGhlIHByb3h5IGxpc3RlbnMgdGhlcmUpLlxuICpcbiAqIEBtb2R1bGVcbiAqL1xuXG5pbXBvcnQgeyBzcGF3biwgc3Bhd25TeW5jIH0gZnJvbSBcIm5vZGU6Y2hpbGRfcHJvY2Vzc1wiO1xuaW1wb3J0IHsgZXhpc3RzU3luYywgbWtkaXJTeW5jLCB3cml0ZUZpbGVTeW5jIH0gZnJvbSBcIm5vZGU6ZnNcIjtcbmltcG9ydCB7IGRlbGltaXRlciwgam9pbiB9IGZyb20gXCJub2RlOnBhdGhcIjtcbmltcG9ydCB7IGxvZyB9IGZyb20gXCJAZGJ4LXRvb2xzL3NoYXJlZC1jb3JlXCI7XG5cbmNvbnN0IGxvZ2dlciA9IGxvZy5sb2dnZXIoXCJ0dW5uZWw6cG9ydHJcIik7XG5cbi8qKiBSZXNvbHZlZCBwb3J0ciB3aXJpbmcsIG9yIGB1bmRlZmluZWRgIHdoZW4gbm8gdHVubmVsIGlzIGNvbmZpZ3VyZWQuICovXG5leHBvcnQgaW50ZXJmYWNlIFBvcnRyQ29uZmlnIHtcbiAgc3ViZG9tYWluOiBzdHJpbmc7XG4gIHNlcnZlcjogc3RyaW5nO1xuICB0b2tlbjogc3RyaW5nO1xuICBwb3J0OiBudW1iZXI7XG59XG5cbi8qKlxuICogUmVzb2x2ZSBwb3J0ciBjb25maWcgZnJvbSBgUFVCTElDX0RPTUFJTmAgKyBgUE9SVFJfVE9LRU5gLCBvciBhbiBleHBsaWNpdFxuICogYHN1YmRvbWFpbmAuIGBQVUJMSUNfRE9NQUlOYCBpcyBgPHN1YmRvbWFpbj4uPHNlcnZlcj5gIChlLmcuXG4gKiBgZGVtby5hcHBzLmRieC50b29sc2ApLiBSZXR1cm5zIGB1bmRlZmluZWRgIChubyB0dW5uZWwpIHdoZW4gdGhlIHRva2VuIG9yIGFcbiAqIHVzYWJsZSBkb21haW4gaXMgYWJzZW50LlxuICovXG5leHBvcnQgZnVuY3Rpb24gcmVzb2x2ZVBvcnRyQ29uZmlnKG9wdHM6IHtcbiAgcHVibGljRG9tYWluPzogc3RyaW5nO1xuICBzdWJkb21haW4/OiBzdHJpbmc7XG4gIHRva2VuPzogc3RyaW5nO1xuICBwb3J0OiBudW1iZXI7XG59KTogUG9ydHJDb25maWcgfCB1bmRlZmluZWQge1xuICBjb25zdCB0b2tlbiA9IG9wdHMudG9rZW4gPz8gcHJvY2Vzcy5lbnYuUE9SVFJfVE9LRU47XG4gIGNvbnN0IGRvbWFpbiA9IG9wdHMucHVibGljRG9tYWluID8/IHByb2Nlc3MuZW52LlBVQkxJQ19ET01BSU47XG4gIGlmICghdG9rZW4pIHJldHVybiB1bmRlZmluZWQ7XG4gIGxldCBzdWJkb21haW4gPSBvcHRzLnN1YmRvbWFpbjtcbiAgbGV0IHNlcnZlcjogc3RyaW5nIHwgdW5kZWZpbmVkO1xuICBpZiAoZG9tYWluKSB7XG4gICAgc3ViZG9tYWluID8/PSBkb21haW4uc3BsaXQoXCIuXCIpWzBdO1xuICAgIHNlcnZlciA9IGRvbWFpbi5zbGljZShkb21haW4uaW5kZXhPZihcIi5cIikgKyAxKTtcbiAgfVxuICBzZXJ2ZXIgPz89IHByb2Nlc3MuZW52LlBPUlRSX1NFUlZFUjtcbiAgaWYgKCFzdWJkb21haW4gfHwgIXNlcnZlciB8fCBzZXJ2ZXIgPT09IGRvbWFpbikgcmV0dXJuIHVuZGVmaW5lZDtcbiAgcmV0dXJuIHsgc3ViZG9tYWluLCBzZXJ2ZXIsIHRva2VuLCBwb3J0OiBvcHRzLnBvcnQgfTtcbn1cblxuLyoqIFRoZSB3cml0YWJsZSBob21lIHBvcnRyIGluc3RhbGxzICsgY29uZmlndXJlcyB1bmRlciAoQXBwcyBgJEhPTUVgIGlzIHJlYWQtb25seSkuICovXG5mdW5jdGlvbiBwb3J0ckhvbWUoKTogc3RyaW5nIHtcbiAgY29uc3QgaG9tZSA9IGpvaW4ocHJvY2Vzcy5jd2QoKSwgXCIuaG9tZVwiKTtcbiAgbWtkaXJTeW5jKGpvaW4oaG9tZSwgXCIucG9ydHJcIiwgXCJiaW5cIiksIHsgcmVjdXJzaXZlOiB0cnVlIH0pO1xuICByZXR1cm4gaG9tZTtcbn1cblxuLyoqIEluc3RhbGwgcG9ydHIgKGlkZW1wb3RlbnQpIGludG8gdGhlIGN3ZC1yb290ZWQgaG9tZSBhbmQgcmV0dXJuIHRoZSBjaGlsZCBlbnYuICovXG5leHBvcnQgZnVuY3Rpb24gaW5zdGFsbFBvcnRyKCk6IE5vZGVKUy5Qcm9jZXNzRW52IHtcbiAgY29uc3QgaG9tZSA9IHBvcnRySG9tZSgpO1xuICBjb25zdCBlbnY6IE5vZGVKUy5Qcm9jZXNzRW52ID0ge1xuICAgIC4uLnByb2Nlc3MuZW52LFxuICAgIEhPTUU6IGhvbWUsXG4gICAgUE9SVFJfQVVUT19BRERfUEFUSDogXCJub1wiLFxuICAgIFBBVEg6IFtqb2luKGhvbWUsIFwiLnBvcnRyXCIsIFwiYmluXCIpLCBwcm9jZXNzLmVudi5QQVRIID8/IFwiXCJdLmpvaW4oZGVsaW1pdGVyKSxcbiAgfTtcbiAgbG9nZ2VyLmluZm8oXCJpbnN0YWxsaW5nIHBvcnRyIChpZGVtcG90ZW50KVwiKTtcbiAgY29uc3QgcmVzID0gc3Bhd25TeW5jKFwiYmFzaFwiLCBbXCItY1wiLCBcImN1cmwgLXNTZiBodHRwczovL2luc3RhbGwucG9ydHIuZGV2IHwgc2hcIl0sIHtcbiAgICBlbnYsXG4gICAgc3RkaW86IFwiaW5oZXJpdFwiLFxuICB9KTtcbiAgaWYgKHJlcy5zdGF0dXMgIT09IDApIHRocm93IG5ldyBFcnJvcihcInBvcnRyIGluc3RhbGwgZmFpbGVkXCIpO1xuICByZXR1cm4gZW52O1xufVxuXG4vKiogUmVuZGVyIGB+Ly5wb3J0ci9jb25maWcueWFtbGAgZm9yIHRoZSByZXNvbHZlZCB0dW5uZWwuICovXG5leHBvcnQgZnVuY3Rpb24gd3JpdGVQb3J0ckNvbmZpZyhjb25maWc6IFBvcnRyQ29uZmlnLCBlbnY6IE5vZGVKUy5Qcm9jZXNzRW52KTogdm9pZCB7XG4gIGNvbnN0IHBhdGggPSBqb2luKGVudi5IT01FISwgXCIucG9ydHJcIiwgXCJjb25maWcueWFtbFwiKTtcbiAgd3JpdGVGaWxlU3luYyhcbiAgICBwYXRoLFxuICAgIFtcbiAgICAgIGBzZXJ2ZXJfdXJsOiAke2NvbmZpZy5zZXJ2ZXJ9YCxcbiAgICAgIGBzc2hfdXJsOiAke2NvbmZpZy5zZXJ2ZXJ9OjQ0NDRgLFxuICAgICAgYHNlY3JldF9rZXk6ICR7Y29uZmlnLnRva2VufWAsXG4gICAgICBcImRpc2FibGVfZGFzaGJvYXJkOiB0cnVlXCIsXG4gICAgICBcImRpc2FibGVfdHVpOiB0cnVlXCIsXG4gICAgICBcInR1bm5lbHM6XCIsXG4gICAgICBgICAtIG5hbWU6ICR7Y29uZmlnLnN1YmRvbWFpbn1gLFxuICAgICAgYCAgICBzdWJkb21haW46ICR7Y29uZmlnLnN1YmRvbWFpbn1gLFxuICAgICAgYCAgICBwb3J0OiAke2NvbmZpZy5wb3J0fWAsXG4gICAgICBcIlwiLFxuICAgIF0uam9pbihcIlxcblwiKSxcbiAgKTtcbn1cblxuLyoqIExhdW5jaCBgcG9ydHIgc3RhcnRgIGFzIGEgY2hpbGQgcHJvY2VzcyAoY2FsbGVyIHN1cGVydmlzZXMgKyBraWxscyBpdCkuICovXG5leHBvcnQgZnVuY3Rpb24gc3RhcnRQb3J0cihjb25maWc6IFBvcnRyQ29uZmlnLCBlbnY6IE5vZGVKUy5Qcm9jZXNzRW52KTogUmV0dXJuVHlwZTx0eXBlb2Ygc3Bhd24+IHtcbiAgLy8gUmVjbGFpbSB0aGUgc3ViZG9tYWluIGZyb20gYW55IHBvcnRyIGxlZnQgYnkgYSBwcmV2aW91cyBib290IGluIHRoaXMgY29udGFpbmVyLlxuICBzcGF3blN5bmMoXCJwa2lsbFwiLCBbXCIteFwiLCBcInBvcnRyXCJdLCB7IHN0ZGlvOiBcImlnbm9yZVwiIH0pO1xuICBsb2dnZXIuaW5mbyhgcG9ydHIgdHVubmVsaW5nIGh0dHBzOi8vJHtjb25maWcuc3ViZG9tYWlufS4ke2NvbmZpZy5zZXJ2ZXJ9IC0+IDoke2NvbmZpZy5wb3J0fWApO1xuICByZXR1cm4gc3Bhd24oXCJwb3J0clwiLCBbXCJzdGFydFwiXSwgeyBlbnYsIHN0ZGlvOiBcImluaGVyaXRcIiB9KTtcbn1cblxuLyoqIFRydWUgd2hlbiB0aGUgaW5zdGFsbGVkIHBvcnRyIGJpbmFyeSBwYXRoIGV4aXN0cyAocG9zdC1pbnN0YWxsIHNhbml0eSkuICovXG5leHBvcnQgZnVuY3Rpb24gcG9ydHJJbnN0YWxsZWQoZW52OiBOb2RlSlMuUHJvY2Vzc0Vudik6IGJvb2xlYW4ge1xuICByZXR1cm4gZXhpc3RzU3luYyhqb2luKGVudi5IT01FISwgXCIucG9ydHJcIiwgXCJiaW5cIiwgXCJwb3J0clwiKSk7XG59XG4iXX0=
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The tunnel gate reverse-proxy.
3
+ *
4
+ * Binds the PUBLIC port (the container's `DATABRICKS_APP_PORT`) and forwards to
5
+ * the real app on a private loopback port. It is the single front door for both
6
+ * traffic paths into the container:
7
+ *
8
+ * - **portr client** (same container, connects over LOOPBACK) - the public
9
+ * tunnel. This is what the gate protects.
10
+ * - **Databricks control plane / front door** (reaches the `0.0.0.0` port from
11
+ * a NON-loopback container-network address) - passed through UNGATED, per the
12
+ * rule "if it's not from the portr client, let it in".
13
+ *
14
+ * The distinguisher is the connection's source address: a loopback
15
+ * `req.socket.remoteAddress` is the portr client; anything else is the platform.
16
+ *
17
+ * For portr traffic the gate is a standard SPA gate: static assets + the login
18
+ * flow (`/api/email/auth/*`, answered IN-PROCESS by this proxy via the
19
+ * {@link AuthGateApi}) are open so the browser can load the client and render the
20
+ * `<AuthGate>`; every other `/api/*` needs a valid session cookie or gets 401.
21
+ * WebSocket upgrades are gated the same way and forwarded with `http-proxy-3`.
22
+ *
23
+ * @module
24
+ */
25
+ import type { AuthGateApi } from "./plugin.ts";
26
+ /** Options for {@link startProxy}. */
27
+ export interface ProxyOptions {
28
+ /** Public port to listen on (the container's DATABRICKS_APP_PORT). */
29
+ publicPort: number;
30
+ /** Private port the real app listens on (loopback). */
31
+ appPort: number;
32
+ /**
33
+ * The in-process gate API, or `undefined` to run OPEN (insecure mode): every
34
+ * request - including portr traffic - is forwarded ungated. Used when the
35
+ * operator passed `--insecure` / `TUNNEL_INSECURE=true`.
36
+ */
37
+ gate?: AuthGateApi;
38
+ }
39
+ /** Start the gate proxy. Resolves once it is listening. */
40
+ export declare function startProxy({ publicPort, appPort, gate }: ProxyOptions): Promise<void>;