@dbx-tools/cli-tunnel 0.6.59 → 0.6.85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -364
- package/index.ts +2 -19
- package/lib/index.d.ts +2 -19
- package/lib/index.js +2 -15
- package/lib/src/app.d.ts +12 -123
- package/lib/src/app.js +22 -250
- package/lib/src/cli.d.ts +19 -22
- package/lib/src/cli.js +143 -128
- package/lib/src/options.d.ts +46 -0
- package/lib/src/options.js +51 -0
- package/lib/src/proxy.d.ts +27 -37
- package/lib/src/proxy.js +115 -220
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +15 -81
- package/src/app.ts +20 -282
- package/src/cli.ts +152 -162
- package/src/options.ts +85 -0
- package/src/proxy.ts +139 -261
- package/bin/dbx-tools-tunnel.ts +0 -13
- package/lib/bin/dbx-tools-tunnel.d.ts +0 -2
- package/lib/bin/dbx-tools-tunnel.js +0 -14
- package/lib/src/allowlist.d.ts +0 -32
- package/lib/src/allowlist.js +0 -57
- package/lib/src/env.d.ts +0 -57
- package/lib/src/env.js +0 -60
- package/lib/src/headers.d.ts +0 -108
- package/lib/src/headers.js +0 -140
- package/lib/src/otp.d.ts +0 -49
- package/lib/src/otp.js +0 -124
- package/lib/src/plugin.d.ts +0 -147
- package/lib/src/plugin.js +0 -138
- package/lib/src/portr.d.ts +0 -40
- package/lib/src/portr.js +0 -93
- package/lib/src/rate-limit.d.ts +0 -35
- package/lib/src/rate-limit.js +0 -53
- package/lib/src/signing-key.d.ts +0 -86
- package/lib/src/signing-key.js +0 -170
- package/src/allowlist.ts +0 -60
- package/src/env.ts +0 -72
- package/src/headers.ts +0 -155
- package/src/otp.ts +0 -137
- package/src/plugin.ts +0 -269
- package/src/portr.ts +0 -113
- package/src/rate-limit.ts +0 -59
- package/src/signing-key.ts +0 -201
package/lib/src/signing-key.d.ts
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The gate's HS256 session-signing key, persisted in AppKit's cache.
|
|
3
|
-
*
|
|
4
|
-
* The key decides whether a session COOKIE still verifies. Before this module the
|
|
5
|
-
* fallback was `randomBytes(32)` per process, so every restart invalidated every
|
|
6
|
-
* outstanding cookie and each signed-in user had to request a new code - painful
|
|
7
|
-
* for a tunnel, which restarts whenever the app it wraps does. Now the key is
|
|
8
|
-
* stored in the cache AppKit already configured (memory, or Lakebase when the
|
|
9
|
-
* host wires a persistent `CacheStorage`), so with persistent storage a restart
|
|
10
|
-
* keeps sessions alive for {@link KEY_TTL_SECONDS}.
|
|
11
|
-
*
|
|
12
|
-
* An explicitly configured `TUNNEL_AUTH_JWT_SECRET` still wins outright. That is
|
|
13
|
-
* the right answer for a fleet: an operator-held secret needs no shared cache and
|
|
14
|
-
* no convergence, and it survives a cache flush.
|
|
15
|
-
*
|
|
16
|
-
* ## get / generate / get
|
|
17
|
-
*
|
|
18
|
-
* Two instances booting at once both miss, so both would generate - and the loser
|
|
19
|
-
* would sign cookies with a key the winner rejects. Resolution is a re-READ after
|
|
20
|
-
* the write: whatever the cache holds afterwards is the key everyone adopts, so
|
|
21
|
-
* the instances converge on ONE value instead of trusting the one they minted.
|
|
22
|
-
* (`set` is not conditional in the cache API - there is no `setnx` to lean on -
|
|
23
|
-
* and this runs once per process, so the extra round-trip is free.)
|
|
24
|
-
*
|
|
25
|
-
* A pathological interleave can still cost a key: if A writes between B's write
|
|
26
|
-
* and B's re-read, B adopts A's key while A adopts its own. The cost is bounded -
|
|
27
|
-
* a cookie minted in that window fails to verify and the holder signs in again -
|
|
28
|
-
* and it cannot produce a key one instance TRUSTS but another rejects for longer
|
|
29
|
-
* than the window itself. Set `TUNNEL_AUTH_JWT_SECRET` to remove the race
|
|
30
|
-
* entirely.
|
|
31
|
-
*
|
|
32
|
-
* ## Forcing every session to end
|
|
33
|
-
*
|
|
34
|
-
* {@link resolveSessionCutoff} reads a date from `TUNNEL_AUTH_SESSION_CUTOFF` (or
|
|
35
|
-
* `--session-cutoff`), and that date is part of the cache KEY. Moving it forward
|
|
36
|
-
* makes every prior key unreachable, so every cookie signed against it stops
|
|
37
|
-
* verifying - the log-everyone-out switch, without having to find and flush a
|
|
38
|
-
* cache entry. The cutoff is also asserted against each token's `iat`, so a
|
|
39
|
-
* cookie that predates it is refused even if it was signed with the key that is
|
|
40
|
-
* somehow still current.
|
|
41
|
-
*
|
|
42
|
-
* @module
|
|
43
|
-
*/
|
|
44
|
-
/**
|
|
45
|
-
* How long a cached signing key lives: 30 days.
|
|
46
|
-
*
|
|
47
|
-
* This is the ceiling on how long a session cookie can stay valid across
|
|
48
|
-
* restarts, so it is deliberately >= the default session TTL - a key that expired
|
|
49
|
-
* before the cookies it signed would log everyone out for no reason.
|
|
50
|
-
*/
|
|
51
|
-
export declare const KEY_TTL_SECONDS: number;
|
|
52
|
-
/**
|
|
53
|
-
* Resolve the force-clear cutoff as epoch MILLISECONDS, or `0` when unset.
|
|
54
|
-
*
|
|
55
|
-
* Accepts whatever `object.toDate` accepts - a `Date`, `2026-08-02`, an ISO
|
|
56
|
-
* instant, epoch seconds or millis from `date +%s`, or a relative duration
|
|
57
|
-
* (`-30d`, `7 days ago`), which is the spelling an operator reaching for this
|
|
58
|
-
* usually wants: sign out everyone who signed in more than a month ago.
|
|
59
|
-
*
|
|
60
|
-
* An UNPARSEABLE value is ignored with a warning rather than throwing: this is
|
|
61
|
-
* the switch that logs a fleet back in, and failing to boot over a typo is worse
|
|
62
|
-
* than not rotating.
|
|
63
|
-
*/
|
|
64
|
-
export declare function resolveSessionCutoff(configured?: string | number | Date): number;
|
|
65
|
-
/** A resolved key plus the cutoff it is scoped to. */
|
|
66
|
-
export interface SigningKey {
|
|
67
|
-
key: Uint8Array;
|
|
68
|
-
/** Force-clear cutoff in ms; `0` when unset. Tokens older than this are refused. */
|
|
69
|
-
cutoffMs: number;
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* The signing key for this gate, resolved once per process.
|
|
73
|
-
*
|
|
74
|
-
* Resolved ONCE, so `configuredCutoff` is honoured only on the first call - the
|
|
75
|
-
* plugin's `setup()` passes its resolved value there, before any request can
|
|
76
|
-
* reach the lazy path.
|
|
77
|
-
*
|
|
78
|
-
* `TUNNEL_AUTH_JWT_SECRET` when set, else the cache-backed key. A cache that is
|
|
79
|
-
* unavailable degrades to an ephemeral per-process key (the previous behaviour)
|
|
80
|
-
* rather than refusing to sign: the key only validates an ALREADY-issued session,
|
|
81
|
-
* so losing it costs sessions, never admission - a caller still needs a code
|
|
82
|
-
* delivered to an allow-listed address.
|
|
83
|
-
*/
|
|
84
|
-
export declare function signingKey(configuredCutoff?: string | number | Date): Promise<SigningKey>;
|
|
85
|
-
/** Reset the per-process key (tests, or after changing the env in-process). */
|
|
86
|
-
export declare function resetSigningKey(): void;
|
package/lib/src/signing-key.js
DELETED
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The gate's HS256 session-signing key, persisted in AppKit's cache.
|
|
3
|
-
*
|
|
4
|
-
* The key decides whether a session COOKIE still verifies. Before this module the
|
|
5
|
-
* fallback was `randomBytes(32)` per process, so every restart invalidated every
|
|
6
|
-
* outstanding cookie and each signed-in user had to request a new code - painful
|
|
7
|
-
* for a tunnel, which restarts whenever the app it wraps does. Now the key is
|
|
8
|
-
* stored in the cache AppKit already configured (memory, or Lakebase when the
|
|
9
|
-
* host wires a persistent `CacheStorage`), so with persistent storage a restart
|
|
10
|
-
* keeps sessions alive for {@link KEY_TTL_SECONDS}.
|
|
11
|
-
*
|
|
12
|
-
* An explicitly configured `TUNNEL_AUTH_JWT_SECRET` still wins outright. That is
|
|
13
|
-
* the right answer for a fleet: an operator-held secret needs no shared cache and
|
|
14
|
-
* no convergence, and it survives a cache flush.
|
|
15
|
-
*
|
|
16
|
-
* ## get / generate / get
|
|
17
|
-
*
|
|
18
|
-
* Two instances booting at once both miss, so both would generate - and the loser
|
|
19
|
-
* would sign cookies with a key the winner rejects. Resolution is a re-READ after
|
|
20
|
-
* the write: whatever the cache holds afterwards is the key everyone adopts, so
|
|
21
|
-
* the instances converge on ONE value instead of trusting the one they minted.
|
|
22
|
-
* (`set` is not conditional in the cache API - there is no `setnx` to lean on -
|
|
23
|
-
* and this runs once per process, so the extra round-trip is free.)
|
|
24
|
-
*
|
|
25
|
-
* A pathological interleave can still cost a key: if A writes between B's write
|
|
26
|
-
* and B's re-read, B adopts A's key while A adopts its own. The cost is bounded -
|
|
27
|
-
* a cookie minted in that window fails to verify and the holder signs in again -
|
|
28
|
-
* and it cannot produce a key one instance TRUSTS but another rejects for longer
|
|
29
|
-
* than the window itself. Set `TUNNEL_AUTH_JWT_SECRET` to remove the race
|
|
30
|
-
* entirely.
|
|
31
|
-
*
|
|
32
|
-
* ## Forcing every session to end
|
|
33
|
-
*
|
|
34
|
-
* {@link resolveSessionCutoff} reads a date from `TUNNEL_AUTH_SESSION_CUTOFF` (or
|
|
35
|
-
* `--session-cutoff`), and that date is part of the cache KEY. Moving it forward
|
|
36
|
-
* makes every prior key unreachable, so every cookie signed against it stops
|
|
37
|
-
* verifying - the log-everyone-out switch, without having to find and flush a
|
|
38
|
-
* cache entry. The cutoff is also asserted against each token's `iat`, so a
|
|
39
|
-
* cookie that predates it is refused even if it was signed with the key that is
|
|
40
|
-
* somehow still current.
|
|
41
|
-
*
|
|
42
|
-
* @module
|
|
43
|
-
*/
|
|
44
|
-
import { randomBytes } from "node:crypto";
|
|
45
|
-
import { CacheManager } from "@databricks/appkit";
|
|
46
|
-
import { env, log, object } from "@dbx-tools/shared-core";
|
|
47
|
-
import { JWT_SECRET_ENV, SESSION_CUTOFF_ENV } from "./env.js";
|
|
48
|
-
const logger = log.logger("tunnel:signing-key");
|
|
49
|
-
/**
|
|
50
|
-
* How long a cached signing key lives: 30 days.
|
|
51
|
-
*
|
|
52
|
-
* This is the ceiling on how long a session cookie can stay valid across
|
|
53
|
-
* restarts, so it is deliberately >= the default session TTL - a key that expired
|
|
54
|
-
* before the cookies it signed would log everyone out for no reason.
|
|
55
|
-
*/
|
|
56
|
-
export const KEY_TTL_SECONDS = 30 * 24 * 60 * 60;
|
|
57
|
-
/** Cache-key prefix for the signing key, namespaced away from other cache use. */
|
|
58
|
-
const KEY_PREFIX = "tunnel:auth:signing-key:";
|
|
59
|
-
/** Bytes of entropy in a generated key (256-bit, matching HS256's hash width). */
|
|
60
|
-
const KEY_BYTES = 32;
|
|
61
|
-
/**
|
|
62
|
-
* Resolve the force-clear cutoff as epoch MILLISECONDS, or `0` when unset.
|
|
63
|
-
*
|
|
64
|
-
* Accepts whatever `object.toDate` accepts - a `Date`, `2026-08-02`, an ISO
|
|
65
|
-
* instant, epoch seconds or millis from `date +%s`, or a relative duration
|
|
66
|
-
* (`-30d`, `7 days ago`), which is the spelling an operator reaching for this
|
|
67
|
-
* usually wants: sign out everyone who signed in more than a month ago.
|
|
68
|
-
*
|
|
69
|
-
* An UNPARSEABLE value is ignored with a warning rather than throwing: this is
|
|
70
|
-
* the switch that logs a fleet back in, and failing to boot over a typo is worse
|
|
71
|
-
* than not rotating.
|
|
72
|
-
*/
|
|
73
|
-
export function resolveSessionCutoff(configured) {
|
|
74
|
-
const raw = configured ?? env.text(SESSION_CUTOFF_ENV) ?? undefined;
|
|
75
|
-
if (raw === undefined || raw === null || raw === "")
|
|
76
|
-
return 0;
|
|
77
|
-
const date = object.toDate(raw);
|
|
78
|
-
if (!date) {
|
|
79
|
-
logger.warn(`ignoring unparseable ${env.name(SESSION_CUTOFF_ENV)}`, { value: String(raw) });
|
|
80
|
-
return 0;
|
|
81
|
-
}
|
|
82
|
-
return clampToPast(date.getTime());
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Hold the cutoff at "now", because a FUTURE cutoff would refuse the sessions it
|
|
86
|
-
* is about to mint as well as the old ones - an app nobody can sign in to, from a
|
|
87
|
-
* mistyped year, with the fix hidden behind understanding this flag. Clamped, a
|
|
88
|
-
* future date means what an operator setting it always meant: clear everything
|
|
89
|
-
* outstanding, then carry on.
|
|
90
|
-
*/
|
|
91
|
-
function clampToPast(cutoffMs) {
|
|
92
|
-
const now = Date.now();
|
|
93
|
-
if (cutoffMs <= now)
|
|
94
|
-
return cutoffMs;
|
|
95
|
-
logger.warn(`${env.name(SESSION_CUTOFF_ENV)} is in the future - clamping to now`, {
|
|
96
|
-
configured: new Date(cutoffMs).toISOString(),
|
|
97
|
-
});
|
|
98
|
-
return now;
|
|
99
|
-
}
|
|
100
|
-
/** The cache key for one cutoff, so moving the cutoff orphans every earlier key. */
|
|
101
|
-
function cacheKey(cutoffMs) {
|
|
102
|
-
return `${KEY_PREFIX}${cutoffMs}`;
|
|
103
|
-
}
|
|
104
|
-
function decode(stored) {
|
|
105
|
-
return new Uint8Array(Buffer.from(stored.secret, "base64url"));
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* Load the signing key for `cutoffMs` from the cache, minting and storing one
|
|
109
|
-
* when absent. See the module docs for why the write is followed by a re-read.
|
|
110
|
-
*/
|
|
111
|
-
async function loadFromCache(cutoffMs) {
|
|
112
|
-
const cache = CacheManager.getInstanceSync();
|
|
113
|
-
const key = cacheKey(cutoffMs);
|
|
114
|
-
const existing = await cache.get(key);
|
|
115
|
-
if (existing?.secret) {
|
|
116
|
-
logger.info("reusing cached signing key", { createdAt: existing.createdAt });
|
|
117
|
-
return decode(existing);
|
|
118
|
-
}
|
|
119
|
-
const minted = {
|
|
120
|
-
secret: Buffer.from(randomBytes(KEY_BYTES)).toString("base64url"),
|
|
121
|
-
createdAt: new Date().toISOString(),
|
|
122
|
-
};
|
|
123
|
-
await cache.set(key, minted, { ttl: KEY_TTL_SECONDS });
|
|
124
|
-
// Re-read rather than trusting `minted`: another instance that raced this boot
|
|
125
|
-
// may have written first, and adopting whatever is stored is what makes the two
|
|
126
|
-
// converge on one key.
|
|
127
|
-
const settled = (await cache.get(key)) ?? minted;
|
|
128
|
-
logger.info("stored a new signing key", {
|
|
129
|
-
createdAt: settled.createdAt,
|
|
130
|
-
ttlSeconds: KEY_TTL_SECONDS,
|
|
131
|
-
adopted: settled.createdAt === minted.createdAt ? "own" : "concurrent-instance",
|
|
132
|
-
});
|
|
133
|
-
return decode(settled);
|
|
134
|
-
}
|
|
135
|
-
let pending;
|
|
136
|
-
/**
|
|
137
|
-
* The signing key for this gate, resolved once per process.
|
|
138
|
-
*
|
|
139
|
-
* Resolved ONCE, so `configuredCutoff` is honoured only on the first call - the
|
|
140
|
-
* plugin's `setup()` passes its resolved value there, before any request can
|
|
141
|
-
* reach the lazy path.
|
|
142
|
-
*
|
|
143
|
-
* `TUNNEL_AUTH_JWT_SECRET` when set, else the cache-backed key. A cache that is
|
|
144
|
-
* unavailable degrades to an ephemeral per-process key (the previous behaviour)
|
|
145
|
-
* rather than refusing to sign: the key only validates an ALREADY-issued session,
|
|
146
|
-
* so losing it costs sessions, never admission - a caller still needs a code
|
|
147
|
-
* delivered to an allow-listed address.
|
|
148
|
-
*/
|
|
149
|
-
export function signingKey(configuredCutoff) {
|
|
150
|
-
pending ??= (async () => {
|
|
151
|
-
const cutoffMs = resolveSessionCutoff(configuredCutoff);
|
|
152
|
-
const configured = env.text(JWT_SECRET_ENV);
|
|
153
|
-
if (configured) {
|
|
154
|
-
return { key: new TextEncoder().encode(configured), cutoffMs };
|
|
155
|
-
}
|
|
156
|
-
try {
|
|
157
|
-
return { key: await loadFromCache(cutoffMs), cutoffMs };
|
|
158
|
-
}
|
|
159
|
-
catch (error) {
|
|
160
|
-
logger.warn(`no ${env.name(JWT_SECRET_ENV)} and the cache is unavailable - using an ephemeral per-process key; sessions will not survive a restart`, { error });
|
|
161
|
-
return { key: new Uint8Array(randomBytes(KEY_BYTES)), cutoffMs };
|
|
162
|
-
}
|
|
163
|
-
})();
|
|
164
|
-
return pending;
|
|
165
|
-
}
|
|
166
|
-
/** Reset the per-process key (tests, or after changing the env in-process). */
|
|
167
|
-
export function resetSigningKey() {
|
|
168
|
-
pending = undefined;
|
|
169
|
-
}
|
|
170
|
-
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2lnbmluZy1rZXkuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvc2lnbmluZy1rZXkudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztHQTBDRztBQUVILE9BQU8sRUFBRSxXQUFXLEVBQUUsTUFBTSxhQUFhLENBQUM7QUFDMUMsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBQ2xELE9BQU8sRUFBRSxHQUFHLEVBQUUsR0FBRyxFQUFFLE1BQU0sRUFBRSxNQUFNLHdCQUF3QixDQUFDO0FBQzFELE9BQU8sRUFBRSxjQUFjLEVBQUUsa0JBQWtCLEVBQUUsTUFBTSxVQUFVLENBQUM7QUFFOUQsTUFBTSxNQUFNLEdBQUcsR0FBRyxDQUFDLE1BQU0sQ0FBQyxvQkFBb0IsQ0FBQyxDQUFDO0FBRWhEOzs7Ozs7R0FNRztBQUNILE1BQU0sQ0FBQyxNQUFNLGVBQWUsR0FBRyxFQUFFLEdBQUcsRUFBRSxHQUFHLEVBQUUsR0FBRyxFQUFFLENBQUM7QUFFakQsa0ZBQWtGO0FBQ2xGLE1BQU0sVUFBVSxHQUFHLDBCQUEwQixDQUFDO0FBRTlDLGtGQUFrRjtBQUNsRixNQUFNLFNBQVMsR0FBRyxFQUFFLENBQUM7QUFVckI7Ozs7Ozs7Ozs7O0dBV0c7QUFDSCxNQUFNLFVBQVUsb0JBQW9CLENBQUMsVUFBbUM7SUFDdEUsTUFBTSxHQUFHLEdBQUcsVUFBVSxJQUFJLEdBQUcsQ0FBQyxJQUFJLENBQUMsa0JBQWtCLENBQUMsSUFBSSxTQUFTLENBQUM7SUFDcEUsSUFBSSxHQUFHLEtBQUssU0FBUyxJQUFJLEdBQUcsS0FBSyxJQUFJLElBQUksR0FBRyxLQUFLLEVBQUU7UUFBRSxPQUFPLENBQUMsQ0FBQztJQUU5RCxNQUFNLElBQUksR0FBRyxNQUFNLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDO0lBQ2hDLElBQUksQ0FBQyxJQUFJLEVBQUUsQ0FBQztRQUNWLE1BQU0sQ0FBQyxJQUFJLENBQUMsd0JBQXdCLEdBQUcsQ0FBQyxJQUFJLENBQUMsa0JBQWtCLENBQUMsRUFBRSxFQUFFLEVBQUUsS0FBSyxFQUFFLE1BQU0sQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFDLENBQUM7UUFDNUYsT0FBTyxDQUFDLENBQUM7SUFDWCxDQUFDO0lBQ0QsT0FBTyxXQUFXLENBQUMsSUFBSSxDQUFDLE9BQU8sRUFBRSxDQUFDLENBQUM7QUFDckMsQ0FBQztBQUVEOzs7Ozs7R0FNRztBQUNILFNBQVMsV0FBVyxDQUFDLFFBQWdCO0lBQ25DLE1BQU0sR0FBRyxHQUFHLElBQUksQ0FBQyxHQUFHLEVBQUUsQ0FBQztJQUN2QixJQUFJLFFBQVEsSUFBSSxHQUFHO1FBQUUsT0FBTyxRQUFRLENBQUM7SUFDckMsTUFBTSxDQUFDLElBQUksQ0FBQyxHQUFHLEdBQUcsQ0FBQyxJQUFJLENBQUMsa0JBQWtCLENBQUMscUNBQXFDLEVBQUU7UUFDaEYsVUFBVSxFQUFFLElBQUksSUFBSSxDQUFDLFFBQVEsQ0FBQyxDQUFDLFdBQVcsRUFBRTtLQUM3QyxDQUFDLENBQUM7SUFDSCxPQUFPLEdBQUcsQ0FBQztBQUNiLENBQUM7QUFFRCxvRkFBb0Y7QUFDcEYsU0FBUyxRQUFRLENBQUMsUUFBZ0I7SUFDaEMsT0FBTyxHQUFHLFVBQVUsR0FBRyxRQUFRLEVBQUUsQ0FBQztBQUNwQyxDQUFDO0FBRUQsU0FBUyxNQUFNLENBQUMsTUFBaUI7SUFDL0IsT0FBTyxJQUFJLFVBQVUsQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxNQUFNLEVBQUUsV0FBVyxDQUFDLENBQUMsQ0FBQztBQUNqRSxDQUFDO0FBRUQ7OztHQUdHO0FBQ0gsS0FBSyxVQUFVLGFBQWEsQ0FBQyxRQUFnQjtJQUMzQyxNQUFNLEtBQUssR0FBRyxZQUFZLENBQUMsZUFBZSxFQUFFLENBQUM7SUFDN0MsTUFBTSxHQUFHLEdBQUcsUUFBUSxDQUFDLFFBQVEsQ0FBQyxDQUFDO0lBRS9CLE1BQU0sUUFBUSxHQUFHLE1BQU0sS0FBSyxDQUFDLEdBQUcsQ0FBWSxHQUFHLENBQUMsQ0FBQztJQUNqRCxJQUFJLFFBQVEsRUFBRSxNQUFNLEVBQUUsQ0FBQztRQUNyQixNQUFNLENBQUMsSUFBSSxDQUFDLDRCQUE0QixFQUFFLEVBQUUsU0FBUyxFQUFFLFFBQVEsQ0FBQyxTQUFTLEVBQUUsQ0FBQyxDQUFDO1FBQzdFLE9BQU8sTUFBTSxDQUFDLFFBQVEsQ0FBQyxDQUFDO0lBQzFCLENBQUM7SUFFRCxNQUFNLE1BQU0sR0FBYztRQUN4QixNQUFNLEVBQUUsTUFBTSxDQUFDLElBQUksQ0FBQyxXQUFXLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUMsV0FBVyxDQUFDO1FBQ2pFLFNBQVMsRUFBRSxJQUFJLElBQUksRUFBRSxDQUFDLFdBQVcsRUFBRTtLQUNwQyxDQUFDO0lBQ0YsTUFBTSxLQUFLLENBQUMsR0FBRyxDQUFDLEdBQUcsRUFBRSxNQUFNLEVBQUUsRUFBRSxHQUFHLEVBQUUsZUFBZSxFQUFFLENBQUMsQ0FBQztJQUV2RCwrRUFBK0U7SUFDL0UsZ0ZBQWdGO0lBQ2hGLHVCQUF1QjtJQUN2QixNQUFNLE9BQU8sR0FBRyxDQUFDLE1BQU0sS0FBSyxDQUFDLEdBQUcsQ0FBWSxHQUFHLENBQUMsQ0FBQyxJQUFJLE1BQU0sQ0FBQztJQUM1RCxNQUFNLENBQUMsSUFBSSxDQUFDLDBCQUEwQixFQUFFO1FBQ3RDLFNBQVMsRUFBRSxPQUFPLENBQUMsU0FBUztRQUM1QixVQUFVLEVBQUUsZUFBZTtRQUMzQixPQUFPLEVBQUUsT0FBTyxDQUFDLFNBQVMsS0FBSyxNQUFNLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLHFCQUFxQjtLQUNoRixDQUFDLENBQUM7SUFDSCxPQUFPLE1BQU0sQ0FBQyxPQUFPLENBQUMsQ0FBQztBQUN6QixDQUFDO0FBU0QsSUFBSSxPQUF3QyxDQUFDO0FBRTdDOzs7Ozs7Ozs7Ozs7R0FZRztBQUNILE1BQU0sVUFBVSxVQUFVLENBQUMsZ0JBQXlDO0lBQ2xFLE9BQU8sS0FBSyxDQUFDLEtBQUssSUFBSSxFQUFFO1FBQ3RCLE1BQU0sUUFBUSxHQUFHLG9CQUFvQixDQUFDLGdCQUFnQixDQUFDLENBQUM7UUFDeEQsTUFBTSxVQUFVLEdBQUcsR0FBRyxDQUFDLElBQUksQ0FBQyxjQUFjLENBQUMsQ0FBQztRQUM1QyxJQUFJLFVBQVUsRUFBRSxDQUFDO1lBQ2YsT0FBTyxFQUFFLEdBQUcsRUFBRSxJQUFJLFdBQVcsRUFBRSxDQUFDLE1BQU0sQ0FBQyxVQUFVLENBQUMsRUFBRSxRQUFRLEVBQUUsQ0FBQztRQUNqRSxDQUFDO1FBQ0QsSUFBSSxDQUFDO1lBQ0gsT0FBTyxFQUFFLEdBQUcsRUFBRSxNQUFNLGFBQWEsQ0FBQyxRQUFRLENBQUMsRUFBRSxRQUFRLEVBQUUsQ0FBQztRQUMxRCxDQUFDO1FBQUMsT0FBTyxLQUFLLEVBQUUsQ0FBQztZQUNmLE1BQU0sQ0FBQyxJQUFJLENBQ1QsTUFBTSxHQUFHLENBQUMsSUFBSSxDQUFDLGNBQWMsQ0FBQyx5R0FBeUcsRUFDdkksRUFBRSxLQUFLLEVBQUUsQ0FDVixDQUFDO1lBQ0YsT0FBTyxFQUFFLEdBQUcsRUFBRSxJQUFJLFVBQVUsQ0FBQyxXQUFXLENBQUMsU0FBUyxDQUFDLENBQUMsRUFBRSxRQUFRLEVBQUUsQ0FBQztRQUNuRSxDQUFDO0lBQ0gsQ0FBQyxDQUFDLEVBQUUsQ0FBQztJQUNMLE9BQU8sT0FBTyxDQUFDO0FBQ2pCLENBQUM7QUFFRCwrRUFBK0U7QUFDL0UsTUFBTSxVQUFVLGVBQWU7SUFDN0IsT0FBTyxHQUFHLFNBQVMsQ0FBQztBQUN0QixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBUaGUgZ2F0ZSdzIEhTMjU2IHNlc3Npb24tc2lnbmluZyBrZXksIHBlcnNpc3RlZCBpbiBBcHBLaXQncyBjYWNoZS5cbiAqXG4gKiBUaGUga2V5IGRlY2lkZXMgd2hldGhlciBhIHNlc3Npb24gQ09PS0lFIHN0aWxsIHZlcmlmaWVzLiBCZWZvcmUgdGhpcyBtb2R1bGUgdGhlXG4gKiBmYWxsYmFjayB3YXMgYHJhbmRvbUJ5dGVzKDMyKWAgcGVyIHByb2Nlc3MsIHNvIGV2ZXJ5IHJlc3RhcnQgaW52YWxpZGF0ZWQgZXZlcnlcbiAqIG91dHN0YW5kaW5nIGNvb2tpZSBhbmQgZWFjaCBzaWduZWQtaW4gdXNlciBoYWQgdG8gcmVxdWVzdCBhIG5ldyBjb2RlIC0gcGFpbmZ1bFxuICogZm9yIGEgdHVubmVsLCB3aGljaCByZXN0YXJ0cyB3aGVuZXZlciB0aGUgYXBwIGl0IHdyYXBzIGRvZXMuIE5vdyB0aGUga2V5IGlzXG4gKiBzdG9yZWQgaW4gdGhlIGNhY2hlIEFwcEtpdCBhbHJlYWR5IGNvbmZpZ3VyZWQgKG1lbW9yeSwgb3IgTGFrZWJhc2Ugd2hlbiB0aGVcbiAqIGhvc3Qgd2lyZXMgYSBwZXJzaXN0ZW50IGBDYWNoZVN0b3JhZ2VgKSwgc28gd2l0aCBwZXJzaXN0ZW50IHN0b3JhZ2UgYSByZXN0YXJ0XG4gKiBrZWVwcyBzZXNzaW9ucyBhbGl2ZSBmb3Ige0BsaW5rIEtFWV9UVExfU0VDT05EU30uXG4gKlxuICogQW4gZXhwbGljaXRseSBjb25maWd1cmVkIGBUVU5ORUxfQVVUSF9KV1RfU0VDUkVUYCBzdGlsbCB3aW5zIG91dHJpZ2h0LiBUaGF0IGlzXG4gKiB0aGUgcmlnaHQgYW5zd2VyIGZvciBhIGZsZWV0OiBhbiBvcGVyYXRvci1oZWxkIHNlY3JldCBuZWVkcyBubyBzaGFyZWQgY2FjaGUgYW5kXG4gKiBubyBjb252ZXJnZW5jZSwgYW5kIGl0IHN1cnZpdmVzIGEgY2FjaGUgZmx1c2guXG4gKlxuICogIyMgZ2V0IC8gZ2VuZXJhdGUgLyBnZXRcbiAqXG4gKiBUd28gaW5zdGFuY2VzIGJvb3RpbmcgYXQgb25jZSBib3RoIG1pc3MsIHNvIGJvdGggd291bGQgZ2VuZXJhdGUgLSBhbmQgdGhlIGxvc2VyXG4gKiB3b3VsZCBzaWduIGNvb2tpZXMgd2l0aCBhIGtleSB0aGUgd2lubmVyIHJlamVjdHMuIFJlc29sdXRpb24gaXMgYSByZS1SRUFEIGFmdGVyXG4gKiB0aGUgd3JpdGU6IHdoYXRldmVyIHRoZSBjYWNoZSBob2xkcyBhZnRlcndhcmRzIGlzIHRoZSBrZXkgZXZlcnlvbmUgYWRvcHRzLCBzb1xuICogdGhlIGluc3RhbmNlcyBjb252ZXJnZSBvbiBPTkUgdmFsdWUgaW5zdGVhZCBvZiB0cnVzdGluZyB0aGUgb25lIHRoZXkgbWludGVkLlxuICogKGBzZXRgIGlzIG5vdCBjb25kaXRpb25hbCBpbiB0aGUgY2FjaGUgQVBJIC0gdGhlcmUgaXMgbm8gYHNldG54YCB0byBsZWFuIG9uIC1cbiAqIGFuZCB0aGlzIHJ1bnMgb25jZSBwZXIgcHJvY2Vzcywgc28gdGhlIGV4dHJhIHJvdW5kLXRyaXAgaXMgZnJlZS4pXG4gKlxuICogQSBwYXRob2xvZ2ljYWwgaW50ZXJsZWF2ZSBjYW4gc3RpbGwgY29zdCBhIGtleTogaWYgQSB3cml0ZXMgYmV0d2VlbiBCJ3Mgd3JpdGVcbiAqIGFuZCBCJ3MgcmUtcmVhZCwgQiBhZG9wdHMgQSdzIGtleSB3aGlsZSBBIGFkb3B0cyBpdHMgb3duLiBUaGUgY29zdCBpcyBib3VuZGVkIC1cbiAqIGEgY29va2llIG1pbnRlZCBpbiB0aGF0IHdpbmRvdyBmYWlscyB0byB2ZXJpZnkgYW5kIHRoZSBob2xkZXIgc2lnbnMgaW4gYWdhaW4gLVxuICogYW5kIGl0IGNhbm5vdCBwcm9kdWNlIGEga2V5IG9uZSBpbnN0YW5jZSBUUlVTVFMgYnV0IGFub3RoZXIgcmVqZWN0cyBmb3IgbG9uZ2VyXG4gKiB0aGFuIHRoZSB3aW5kb3cgaXRzZWxmLiBTZXQgYFRVTk5FTF9BVVRIX0pXVF9TRUNSRVRgIHRvIHJlbW92ZSB0aGUgcmFjZVxuICogZW50aXJlbHkuXG4gKlxuICogIyMgRm9yY2luZyBldmVyeSBzZXNzaW9uIHRvIGVuZFxuICpcbiAqIHtAbGluayByZXNvbHZlU2Vzc2lvbkN1dG9mZn0gcmVhZHMgYSBkYXRlIGZyb20gYFRVTk5FTF9BVVRIX1NFU1NJT05fQ1VUT0ZGYCAob3JcbiAqIGAtLXNlc3Npb24tY3V0b2ZmYCksIGFuZCB0aGF0IGRhdGUgaXMgcGFydCBvZiB0aGUgY2FjaGUgS0VZLiBNb3ZpbmcgaXQgZm9yd2FyZFxuICogbWFrZXMgZXZlcnkgcHJpb3Iga2V5IHVucmVhY2hhYmxlLCBzbyBldmVyeSBjb29raWUgc2lnbmVkIGFnYWluc3QgaXQgc3RvcHNcbiAqIHZlcmlmeWluZyAtIHRoZSBsb2ctZXZlcnlvbmUtb3V0IHN3aXRjaCwgd2l0aG91dCBoYXZpbmcgdG8gZmluZCBhbmQgZmx1c2ggYVxuICogY2FjaGUgZW50cnkuIFRoZSBjdXRvZmYgaXMgYWxzbyBhc3NlcnRlZCBhZ2FpbnN0IGVhY2ggdG9rZW4ncyBgaWF0YCwgc28gYVxuICogY29va2llIHRoYXQgcHJlZGF0ZXMgaXQgaXMgcmVmdXNlZCBldmVuIGlmIGl0IHdhcyBzaWduZWQgd2l0aCB0aGUga2V5IHRoYXQgaXNcbiAqIHNvbWVob3cgc3RpbGwgY3VycmVudC5cbiAqXG4gKiBAbW9kdWxlXG4gKi9cblxuaW1wb3J0IHsgcmFuZG9tQnl0ZXMgfSBmcm9tIFwibm9kZTpjcnlwdG9cIjtcbmltcG9ydCB7IENhY2hlTWFuYWdlciB9IGZyb20gXCJAZGF0YWJyaWNrcy9hcHBraXRcIjtcbmltcG9ydCB7IGVudiwgbG9nLCBvYmplY3QgfSBmcm9tIFwiQGRieC10b29scy9zaGFyZWQtY29yZVwiO1xuaW1wb3J0IHsgSldUX1NFQ1JFVF9FTlYsIFNFU1NJT05fQ1VUT0ZGX0VOViB9IGZyb20gXCIuL2Vudi50c1wiO1xuXG5jb25zdCBsb2dnZXIgPSBsb2cubG9nZ2VyKFwidHVubmVsOnNpZ25pbmcta2V5XCIpO1xuXG4vKipcbiAqIEhvdyBsb25nIGEgY2FjaGVkIHNpZ25pbmcga2V5IGxpdmVzOiAzMCBkYXlzLlxuICpcbiAqIFRoaXMgaXMgdGhlIGNlaWxpbmcgb24gaG93IGxvbmcgYSBzZXNzaW9uIGNvb2tpZSBjYW4gc3RheSB2YWxpZCBhY3Jvc3NcbiAqIHJlc3RhcnRzLCBzbyBpdCBpcyBkZWxpYmVyYXRlbHkgPj0gdGhlIGRlZmF1bHQgc2Vzc2lvbiBUVEwgLSBhIGtleSB0aGF0IGV4cGlyZWRcbiAqIGJlZm9yZSB0aGUgY29va2llcyBpdCBzaWduZWQgd291bGQgbG9nIGV2ZXJ5b25lIG91dCBmb3Igbm8gcmVhc29uLlxuICovXG5leHBvcnQgY29uc3QgS0VZX1RUTF9TRUNPTkRTID0gMzAgKiAyNCAqIDYwICogNjA7XG5cbi8qKiBDYWNoZS1rZXkgcHJlZml4IGZvciB0aGUgc2lnbmluZyBrZXksIG5hbWVzcGFjZWQgYXdheSBmcm9tIG90aGVyIGNhY2hlIHVzZS4gKi9cbmNvbnN0IEtFWV9QUkVGSVggPSBcInR1bm5lbDphdXRoOnNpZ25pbmcta2V5OlwiO1xuXG4vKiogQnl0ZXMgb2YgZW50cm9weSBpbiBhIGdlbmVyYXRlZCBrZXkgKDI1Ni1iaXQsIG1hdGNoaW5nIEhTMjU2J3MgaGFzaCB3aWR0aCkuICovXG5jb25zdCBLRVlfQllURVMgPSAzMjtcblxuLyoqIFdoYXQgdGhlIGNhY2hlIHN0b3JlczogdGhlIGtleSBwbHVzIHdoZW4gaXQgd2FzIG1pbnRlZCwgZm9yIG9ic2VydmFiaWxpdHkuICovXG5pbnRlcmZhY2UgU3RvcmVkS2V5IHtcbiAgLyoqIEJhc2U2NHVybCBvZiB0aGUgcmF3IGtleSBieXRlcy4gKi9cbiAgc2VjcmV0OiBzdHJpbmc7XG4gIC8qKiBXaGVuIHRoaXMga2V5IHdhcyBnZW5lcmF0ZWQsIElTTy04NjAxLiAqL1xuICBjcmVhdGVkQXQ6IHN0cmluZztcbn1cblxuLyoqXG4gKiBSZXNvbHZlIHRoZSBmb3JjZS1jbGVhciBjdXRvZmYgYXMgZXBvY2ggTUlMTElTRUNPTkRTLCBvciBgMGAgd2hlbiB1bnNldC5cbiAqXG4gKiBBY2NlcHRzIHdoYXRldmVyIGBvYmplY3QudG9EYXRlYCBhY2NlcHRzIC0gYSBgRGF0ZWAsIGAyMDI2LTA4LTAyYCwgYW4gSVNPXG4gKiBpbnN0YW50LCBlcG9jaCBzZWNvbmRzIG9yIG1pbGxpcyBmcm9tIGBkYXRlICslc2AsIG9yIGEgcmVsYXRpdmUgZHVyYXRpb25cbiAqIChgLTMwZGAsIGA3IGRheXMgYWdvYCksIHdoaWNoIGlzIHRoZSBzcGVsbGluZyBhbiBvcGVyYXRvciByZWFjaGluZyBmb3IgdGhpc1xuICogdXN1YWxseSB3YW50czogc2lnbiBvdXQgZXZlcnlvbmUgd2hvIHNpZ25lZCBpbiBtb3JlIHRoYW4gYSBtb250aCBhZ28uXG4gKlxuICogQW4gVU5QQVJTRUFCTEUgdmFsdWUgaXMgaWdub3JlZCB3aXRoIGEgd2FybmluZyByYXRoZXIgdGhhbiB0aHJvd2luZzogdGhpcyBpc1xuICogdGhlIHN3aXRjaCB0aGF0IGxvZ3MgYSBmbGVldCBiYWNrIGluLCBhbmQgZmFpbGluZyB0byBib290IG92ZXIgYSB0eXBvIGlzIHdvcnNlXG4gKiB0aGFuIG5vdCByb3RhdGluZy5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHJlc29sdmVTZXNzaW9uQ3V0b2ZmKGNvbmZpZ3VyZWQ/OiBzdHJpbmcgfCBudW1iZXIgfCBEYXRlKTogbnVtYmVyIHtcbiAgY29uc3QgcmF3ID0gY29uZmlndXJlZCA/PyBlbnYudGV4dChTRVNTSU9OX0NVVE9GRl9FTlYpID8/IHVuZGVmaW5lZDtcbiAgaWYgKHJhdyA9PT0gdW5kZWZpbmVkIHx8IHJhdyA9PT0gbnVsbCB8fCByYXcgPT09IFwiXCIpIHJldHVybiAwO1xuXG4gIGNvbnN0IGRhdGUgPSBvYmplY3QudG9EYXRlKHJhdyk7XG4gIGlmICghZGF0ZSkge1xuICAgIGxvZ2dlci53YXJuKGBpZ25vcmluZyB1bnBhcnNlYWJsZSAke2Vudi5uYW1lKFNFU1NJT05fQ1VUT0ZGX0VOVil9YCwgeyB2YWx1ZTogU3RyaW5nKHJhdykgfSk7XG4gICAgcmV0dXJuIDA7XG4gIH1cbiAgcmV0dXJuIGNsYW1wVG9QYXN0KGRhdGUuZ2V0VGltZSgpKTtcbn1cblxuLyoqXG4gKiBIb2xkIHRoZSBjdXRvZmYgYXQgXCJub3dcIiwgYmVjYXVzZSBhIEZVVFVSRSBjdXRvZmYgd291bGQgcmVmdXNlIHRoZSBzZXNzaW9ucyBpdFxuICogaXMgYWJvdXQgdG8gbWludCBhcyB3ZWxsIGFzIHRoZSBvbGQgb25lcyAtIGFuIGFwcCBub2JvZHkgY2FuIHNpZ24gaW4gdG8sIGZyb20gYVxuICogbWlzdHlwZWQgeWVhciwgd2l0aCB0aGUgZml4IGhpZGRlbiBiZWhpbmQgdW5kZXJzdGFuZGluZyB0aGlzIGZsYWcuIENsYW1wZWQsIGFcbiAqIGZ1dHVyZSBkYXRlIG1lYW5zIHdoYXQgYW4gb3BlcmF0b3Igc2V0dGluZyBpdCBhbHdheXMgbWVhbnQ6IGNsZWFyIGV2ZXJ5dGhpbmdcbiAqIG91dHN0YW5kaW5nLCB0aGVuIGNhcnJ5IG9uLlxuICovXG5mdW5jdGlvbiBjbGFtcFRvUGFzdChjdXRvZmZNczogbnVtYmVyKTogbnVtYmVyIHtcbiAgY29uc3Qgbm93ID0gRGF0ZS5ub3coKTtcbiAgaWYgKGN1dG9mZk1zIDw9IG5vdykgcmV0dXJuIGN1dG9mZk1zO1xuICBsb2dnZXIud2FybihgJHtlbnYubmFtZShTRVNTSU9OX0NVVE9GRl9FTlYpfSBpcyBpbiB0aGUgZnV0dXJlIC0gY2xhbXBpbmcgdG8gbm93YCwge1xuICAgIGNvbmZpZ3VyZWQ6IG5ldyBEYXRlKGN1dG9mZk1zKS50b0lTT1N0cmluZygpLFxuICB9KTtcbiAgcmV0dXJuIG5vdztcbn1cblxuLyoqIFRoZSBjYWNoZSBrZXkgZm9yIG9uZSBjdXRvZmYsIHNvIG1vdmluZyB0aGUgY3V0b2ZmIG9ycGhhbnMgZXZlcnkgZWFybGllciBrZXkuICovXG5mdW5jdGlvbiBjYWNoZUtleShjdXRvZmZNczogbnVtYmVyKTogc3RyaW5nIHtcbiAgcmV0dXJuIGAke0tFWV9QUkVGSVh9JHtjdXRvZmZNc31gO1xufVxuXG5mdW5jdGlvbiBkZWNvZGUoc3RvcmVkOiBTdG9yZWRLZXkpOiBVaW50OEFycmF5IHtcbiAgcmV0dXJuIG5ldyBVaW50OEFycmF5KEJ1ZmZlci5mcm9tKHN0b3JlZC5zZWNyZXQsIFwiYmFzZTY0dXJsXCIpKTtcbn1cblxuLyoqXG4gKiBMb2FkIHRoZSBzaWduaW5nIGtleSBmb3IgYGN1dG9mZk1zYCBmcm9tIHRoZSBjYWNoZSwgbWludGluZyBhbmQgc3RvcmluZyBvbmVcbiAqIHdoZW4gYWJzZW50LiBTZWUgdGhlIG1vZHVsZSBkb2NzIGZvciB3aHkgdGhlIHdyaXRlIGlzIGZvbGxvd2VkIGJ5IGEgcmUtcmVhZC5cbiAqL1xuYXN5bmMgZnVuY3Rpb24gbG9hZEZyb21DYWNoZShjdXRvZmZNczogbnVtYmVyKTogUHJvbWlzZTxVaW50OEFycmF5PiB7XG4gIGNvbnN0IGNhY2hlID0gQ2FjaGVNYW5hZ2VyLmdldEluc3RhbmNlU3luYygpO1xuICBjb25zdCBrZXkgPSBjYWNoZUtleShjdXRvZmZNcyk7XG5cbiAgY29uc3QgZXhpc3RpbmcgPSBhd2FpdCBjYWNoZS5nZXQ8U3RvcmVkS2V5PihrZXkpO1xuICBpZiAoZXhpc3Rpbmc/LnNlY3JldCkge1xuICAgIGxvZ2dlci5pbmZvKFwicmV1c2luZyBjYWNoZWQgc2lnbmluZyBrZXlcIiwgeyBjcmVhdGVkQXQ6IGV4aXN0aW5nLmNyZWF0ZWRBdCB9KTtcbiAgICByZXR1cm4gZGVjb2RlKGV4aXN0aW5nKTtcbiAgfVxuXG4gIGNvbnN0IG1pbnRlZDogU3RvcmVkS2V5ID0ge1xuICAgIHNlY3JldDogQnVmZmVyLmZyb20ocmFuZG9tQnl0ZXMoS0VZX0JZVEVTKSkudG9TdHJpbmcoXCJiYXNlNjR1cmxcIiksXG4gICAgY3JlYXRlZEF0OiBuZXcgRGF0ZSgpLnRvSVNPU3RyaW5nKCksXG4gIH07XG4gIGF3YWl0IGNhY2hlLnNldChrZXksIG1pbnRlZCwgeyB0dGw6IEtFWV9UVExfU0VDT05EUyB9KTtcblxuICAvLyBSZS1yZWFkIHJhdGhlciB0aGFuIHRydXN0aW5nIGBtaW50ZWRgOiBhbm90aGVyIGluc3RhbmNlIHRoYXQgcmFjZWQgdGhpcyBib290XG4gIC8vIG1heSBoYXZlIHdyaXR0ZW4gZmlyc3QsIGFuZCBhZG9wdGluZyB3aGF0ZXZlciBpcyBzdG9yZWQgaXMgd2hhdCBtYWtlcyB0aGUgdHdvXG4gIC8vIGNvbnZlcmdlIG9uIG9uZSBrZXkuXG4gIGNvbnN0IHNldHRsZWQgPSAoYXdhaXQgY2FjaGUuZ2V0PFN0b3JlZEtleT4oa2V5KSkgPz8gbWludGVkO1xuICBsb2dnZXIuaW5mbyhcInN0b3JlZCBhIG5ldyBzaWduaW5nIGtleVwiLCB7XG4gICAgY3JlYXRlZEF0OiBzZXR0bGVkLmNyZWF0ZWRBdCxcbiAgICB0dGxTZWNvbmRzOiBLRVlfVFRMX1NFQ09ORFMsXG4gICAgYWRvcHRlZDogc2V0dGxlZC5jcmVhdGVkQXQgPT09IG1pbnRlZC5jcmVhdGVkQXQgPyBcIm93blwiIDogXCJjb25jdXJyZW50LWluc3RhbmNlXCIsXG4gIH0pO1xuICByZXR1cm4gZGVjb2RlKHNldHRsZWQpO1xufVxuXG4vKiogQSByZXNvbHZlZCBrZXkgcGx1cyB0aGUgY3V0b2ZmIGl0IGlzIHNjb3BlZCB0by4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgU2lnbmluZ0tleSB7XG4gIGtleTogVWludDhBcnJheTtcbiAgLyoqIEZvcmNlLWNsZWFyIGN1dG9mZiBpbiBtczsgYDBgIHdoZW4gdW5zZXQuIFRva2VucyBvbGRlciB0aGFuIHRoaXMgYXJlIHJlZnVzZWQuICovXG4gIGN1dG9mZk1zOiBudW1iZXI7XG59XG5cbmxldCBwZW5kaW5nOiBQcm9taXNlPFNpZ25pbmdLZXk+IHwgdW5kZWZpbmVkO1xuXG4vKipcbiAqIFRoZSBzaWduaW5nIGtleSBmb3IgdGhpcyBnYXRlLCByZXNvbHZlZCBvbmNlIHBlciBwcm9jZXNzLlxuICpcbiAqIFJlc29sdmVkIE9OQ0UsIHNvIGBjb25maWd1cmVkQ3V0b2ZmYCBpcyBob25vdXJlZCBvbmx5IG9uIHRoZSBmaXJzdCBjYWxsIC0gdGhlXG4gKiBwbHVnaW4ncyBgc2V0dXAoKWAgcGFzc2VzIGl0cyByZXNvbHZlZCB2YWx1ZSB0aGVyZSwgYmVmb3JlIGFueSByZXF1ZXN0IGNhblxuICogcmVhY2ggdGhlIGxhenkgcGF0aC5cbiAqXG4gKiBgVFVOTkVMX0FVVEhfSldUX1NFQ1JFVGAgd2hlbiBzZXQsIGVsc2UgdGhlIGNhY2hlLWJhY2tlZCBrZXkuIEEgY2FjaGUgdGhhdCBpc1xuICogdW5hdmFpbGFibGUgZGVncmFkZXMgdG8gYW4gZXBoZW1lcmFsIHBlci1wcm9jZXNzIGtleSAodGhlIHByZXZpb3VzIGJlaGF2aW91cilcbiAqIHJhdGhlciB0aGFuIHJlZnVzaW5nIHRvIHNpZ246IHRoZSBrZXkgb25seSB2YWxpZGF0ZXMgYW4gQUxSRUFEWS1pc3N1ZWQgc2Vzc2lvbixcbiAqIHNvIGxvc2luZyBpdCBjb3N0cyBzZXNzaW9ucywgbmV2ZXIgYWRtaXNzaW9uIC0gYSBjYWxsZXIgc3RpbGwgbmVlZHMgYSBjb2RlXG4gKiBkZWxpdmVyZWQgdG8gYW4gYWxsb3ctbGlzdGVkIGFkZHJlc3MuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBzaWduaW5nS2V5KGNvbmZpZ3VyZWRDdXRvZmY/OiBzdHJpbmcgfCBudW1iZXIgfCBEYXRlKTogUHJvbWlzZTxTaWduaW5nS2V5PiB7XG4gIHBlbmRpbmcgPz89IChhc3luYyAoKSA9PiB7XG4gICAgY29uc3QgY3V0b2ZmTXMgPSByZXNvbHZlU2Vzc2lvbkN1dG9mZihjb25maWd1cmVkQ3V0b2ZmKTtcbiAgICBjb25zdCBjb25maWd1cmVkID0gZW52LnRleHQoSldUX1NFQ1JFVF9FTlYpO1xuICAgIGlmIChjb25maWd1cmVkKSB7XG4gICAgICByZXR1cm4geyBrZXk6IG5ldyBUZXh0RW5jb2RlcigpLmVuY29kZShjb25maWd1cmVkKSwgY3V0b2ZmTXMgfTtcbiAgICB9XG4gICAgdHJ5IHtcbiAgICAgIHJldHVybiB7IGtleTogYXdhaXQgbG9hZEZyb21DYWNoZShjdXRvZmZNcyksIGN1dG9mZk1zIH07XG4gICAgfSBjYXRjaCAoZXJyb3IpIHtcbiAgICAgIGxvZ2dlci53YXJuKFxuICAgICAgICBgbm8gJHtlbnYubmFtZShKV1RfU0VDUkVUX0VOVil9IGFuZCB0aGUgY2FjaGUgaXMgdW5hdmFpbGFibGUgLSB1c2luZyBhbiBlcGhlbWVyYWwgcGVyLXByb2Nlc3Mga2V5OyBzZXNzaW9ucyB3aWxsIG5vdCBzdXJ2aXZlIGEgcmVzdGFydGAsXG4gICAgICAgIHsgZXJyb3IgfSxcbiAgICAgICk7XG4gICAgICByZXR1cm4geyBrZXk6IG5ldyBVaW50OEFycmF5KHJhbmRvbUJ5dGVzKEtFWV9CWVRFUykpLCBjdXRvZmZNcyB9O1xuICAgIH1cbiAgfSkoKTtcbiAgcmV0dXJuIHBlbmRpbmc7XG59XG5cbi8qKiBSZXNldCB0aGUgcGVyLXByb2Nlc3Mga2V5ICh0ZXN0cywgb3IgYWZ0ZXIgY2hhbmdpbmcgdGhlIGVudiBpbi1wcm9jZXNzKS4gKi9cbmV4cG9ydCBmdW5jdGlvbiByZXNldFNpZ25pbmdLZXkoKTogdm9pZCB7XG4gIHBlbmRpbmcgPSB1bmRlZmluZWQ7XG59XG4iXX0=
|
package/src/allowlist.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unified access allow-list matching for the email-OTP gate.
|
|
3
|
-
*
|
|
4
|
-
* Each pattern in the configured list is one of three shapes:
|
|
5
|
-
*
|
|
6
|
-
* - **domain shortcut** - `example.com` or `@example.com`: matches any
|
|
7
|
-
* address whose domain equals it. This is the gate's OWN semantic (a bare
|
|
8
|
-
* value means "the domain", not "the whole address"), so it is handled here.
|
|
9
|
-
* - **glob** - contains `*` or `?`, e.g. `*@example.com`: matched against the
|
|
10
|
-
* WHOLE address with shell-style wildcards.
|
|
11
|
-
* - **regex** - wrapped in slashes, `/.../ [flags]`: tested against the whole
|
|
12
|
-
* address. An invalid regex never matches (it is skipped with a warning
|
|
13
|
-
* rather than throwing).
|
|
14
|
-
*
|
|
15
|
-
* Only the first shape is this module's business: the glob and regex shapes are
|
|
16
|
-
* delegated to `@dbx-tools/shared-core`'s {@link pattern.toPattern}, which is
|
|
17
|
-
* where that compilation lives for every allow-list in the repo (the tunnel's
|
|
18
|
-
* inbound-header policy uses the same one). Matching is case-insensitive
|
|
19
|
-
* throughout.
|
|
20
|
-
*
|
|
21
|
-
* An EMPTY list matches nobody (fail closed): an app that enables the gate but
|
|
22
|
-
* configures no patterns lets no one in, which is the safe default.
|
|
23
|
-
*
|
|
24
|
-
* @module
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
import { pattern } from "@dbx-tools/shared-core";
|
|
28
|
-
|
|
29
|
-
/** True when `email` matches a single allow-list `pattern`. */
|
|
30
|
-
function matchesPattern(email: string, entry: string): boolean {
|
|
31
|
-
const trimmed = entry.trim();
|
|
32
|
-
if (!trimmed) return false;
|
|
33
|
-
const address = email.trim().toLowerCase();
|
|
34
|
-
|
|
35
|
-
// A bare value with no wildcard and no regex delimiters is a DOMAIN shortcut,
|
|
36
|
-
// the one shape shared-core cannot infer: there it would mean whole-string
|
|
37
|
-
// equality against the address, which is never what an operator writing
|
|
38
|
-
// `example.com` in an access list intends.
|
|
39
|
-
if (!trimmed.startsWith("/") && !trimmed.includes("*") && !trimmed.includes("?")) {
|
|
40
|
-
const domain = trimmed.replace(/^@/, "").toLowerCase();
|
|
41
|
-
const at = address.lastIndexOf("@");
|
|
42
|
-
return at >= 0 && address.slice(at + 1) === domain;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return pattern.toPattern(trimmed)?.(address) ?? false;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* True when `email` is allowed by ANY pattern in `patterns`. An empty (or
|
|
50
|
-
* missing) list allows nobody - the gate fails closed.
|
|
51
|
-
*/
|
|
52
|
-
export function matchesAllowlist(email: string, patterns: readonly string[] | undefined): boolean {
|
|
53
|
-
if (!email || !patterns || patterns.length === 0) return false;
|
|
54
|
-
return patterns.some((entry) => matchesPattern(email, entry));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Rough shape check so a clearly-invalid address is rejected before any work. */
|
|
58
|
-
export function looksLikeEmail(value: string): boolean {
|
|
59
|
-
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim());
|
|
60
|
-
}
|
package/src/env.ts
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
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
|
-
|
|
32
|
-
import type { EnvKey } from "@dbx-tools/shared-core";
|
|
33
|
-
|
|
34
|
-
/** Access allow-list patterns (domain / glob / `/regex/`). */
|
|
35
|
-
export const ALLOW_ENV: EnvKey = ["TUNNEL_AUTH_ALLOW", "EMAIL_AUTH_ALLOW"];
|
|
36
|
-
|
|
37
|
-
/** Subject line for the code email. */
|
|
38
|
-
export const SUBJECT_ENV: EnvKey = ["TUNNEL_AUTH_SUBJECT", "AUTH_SUBJECT"];
|
|
39
|
-
|
|
40
|
-
/** Display name used in the code email copy. */
|
|
41
|
-
export const BRAND_NAME_ENV: EnvKey = ["TUNNEL_AUTH_BRAND_NAME", "AUTH_BRAND_NAME"];
|
|
42
|
-
|
|
43
|
-
/** Line shown immediately above the code in the email. */
|
|
44
|
-
export const MESSAGE_ENV: EnvKey = ["TUNNEL_AUTH_MESSAGE", "AUTH_MESSAGE"];
|
|
45
|
-
|
|
46
|
-
/** Session lifetime, in seconds. */
|
|
47
|
-
export const SESSION_TTL_ENV: EnvKey = ["TUNNEL_AUTH_SESSION_TTL", "AUTH_SESSION_TTL"];
|
|
48
|
-
|
|
49
|
-
/** One-time-code lifetime, in seconds. */
|
|
50
|
-
export const CODE_TTL_ENV: EnvKey = ["TUNNEL_AUTH_CODE_TTL", "AUTH_CODE_TTL"];
|
|
51
|
-
|
|
52
|
-
/** HS256 signing secret for the session JWT. */
|
|
53
|
-
export const JWT_SECRET_ENV: EnvKey = ["TUNNEL_AUTH_JWT_SECRET", "AUTH_JWT_SECRET"];
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Force-clear cutoff: every session issued BEFORE it stops verifying. Anything
|
|
57
|
-
* `object.toDate` accepts - a date, an ISO instant, epoch seconds/millis, or a
|
|
58
|
-
* relative duration (`-30d`, `7 days ago`).
|
|
59
|
-
*/
|
|
60
|
-
export const SESSION_CUTOFF_ENV: EnvKey = [
|
|
61
|
-
"TUNNEL_AUTH_SESSION_CUTOFF",
|
|
62
|
-
"TUNNEL_AUTH_SESSION_EPOCH",
|
|
63
|
-
];
|
|
64
|
-
|
|
65
|
-
/** The public `<subdomain>.<server>` portr should serve on. */
|
|
66
|
-
export const PUBLIC_DOMAIN_ENV: EnvKey = ["TUNNEL_PUBLIC_DOMAIN", "PUBLIC_DOMAIN"];
|
|
67
|
-
|
|
68
|
-
/** Run the tunnel OPEN, with no gate. Ignored unless truthy. */
|
|
69
|
-
export const INSECURE_ENV: EnvKey = "TUNNEL_INSECURE";
|
|
70
|
-
|
|
71
|
-
/** Extra `x-` request headers tunnel traffic may forward. */
|
|
72
|
-
export const FORWARD_HEADERS_ENV: EnvKey = "TUNNEL_FORWARD_HEADERS";
|
package/src/headers.ts
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Inbound header policy for tunnel traffic.
|
|
3
|
-
*
|
|
4
|
-
* Everything the gate forwards arrives from the PUBLIC internet through the
|
|
5
|
-
* portr client, so every header on it is attacker-controlled. The headers an app
|
|
6
|
-
* trusts are precisely the ones a caller must not be able to write, because the
|
|
7
|
-
* app cannot tell a header the Databricks front door set from one a browser
|
|
8
|
-
* typed.
|
|
9
|
-
*
|
|
10
|
-
* ## Policy shape: strip by default, allow by pattern
|
|
11
|
-
*
|
|
12
|
-
* Enumerating what to remove is a losing game - a deny-list is only correct until
|
|
13
|
-
* the platform adds a header or a library starts trusting another one - so the
|
|
14
|
-
* policy is inverted. EVERY `x-`-prefixed request header is dropped from tunnel
|
|
15
|
-
* traffic unless it matches a configured pattern. That fails CLOSED: a header
|
|
16
|
-
* nobody thought about is removed rather than trusted.
|
|
17
|
-
*
|
|
18
|
-
* The allow-list is zero-to-many literals, globs, or `/regex/`es, compiled by
|
|
19
|
-
* shared-core's {@link pattern.toPatternMatcher}, and is UNIONED with
|
|
20
|
-
* {@link DEFAULT_FORWARD_HEADERS} so extending it never silently breaks the
|
|
21
|
-
* built-in surfaces. Configure it with `forwardHeaders` /
|
|
22
|
-
* `TUNNEL_FORWARD_HEADERS`.
|
|
23
|
-
*
|
|
24
|
-
* Non-`x-` headers are untouched. Standard ones (`content-type`, `accept`,
|
|
25
|
-
* `authorization`, `cookie`, ...) are the app's normal input and the gate has no
|
|
26
|
-
* business rewriting them.
|
|
27
|
-
*
|
|
28
|
-
* ## The headers no pattern can forward
|
|
29
|
-
*
|
|
30
|
-
* {@link PROTECTED_HEADERS} is stripped BEFORE the allow-list is consulted, so a
|
|
31
|
-
* permissive pattern (`x-*`, or a careless `*`) cannot re-open impersonation:
|
|
32
|
-
*
|
|
33
|
-
* | Header | What an app does with it | Why spoofing it matters |
|
|
34
|
-
* | -------------------------------- | ------------------------------- | ----------------------- |
|
|
35
|
-
* | `x-forwarded-access-token` | OBO auth (AppKit `asUser`) | Paste any workspace token and every call runs as its owner. The gate's verified email says nothing about who a pasted credential belongs to. |
|
|
36
|
-
* | `x-forwarded-user` | Caller identity | Impersonate another user. The gate sets this itself, from a verified session. |
|
|
37
|
-
* | `x-forwarded-email` | Caller identity | Same. |
|
|
38
|
-
* | `x-forwarded-preferred-username` | Display name from the IdP | Same. |
|
|
39
|
-
* | `x-forwarded-host` | The originally-requested host | Poison absolute URLs the app builds (the classic reset-link attack). |
|
|
40
|
-
* | `x-forwarded-proto` / `-port` | Original scheme / port | Convince the app a plaintext request arrived over TLS. |
|
|
41
|
-
* | `x-forwarded-for` | Client IP | Forge the audit trail, and fan out per-IP rate-limit keys (see below). |
|
|
42
|
-
* | `x-real-ip` | Client IP | Same. |
|
|
43
|
-
* | `x-request-id` | Request correlation UUID | Forge or collide trace ids, making logs unreliable. |
|
|
44
|
-
*
|
|
45
|
-
* The identity four are AppKit's OBO contract; the rest are the `X-Forwarded-*`
|
|
46
|
-
* set the Databricks Apps reverse proxy documents passing to an app
|
|
47
|
-
* ({@link https://docs.databricks.com/aws/en/dev-tools/databricks-apps/http-headers}),
|
|
48
|
-
* plus the conventional `x-forwarded-proto`/`-port`/`x-real-ip` an app or one of
|
|
49
|
-
* its libraries may read even though the table omits them.
|
|
50
|
-
*
|
|
51
|
-
* Stripping the `x-forwarded-*` transport trio is safe because `http-proxy-3` is
|
|
52
|
-
* configured with `xfwd: true` and re-adds them AFTER this policy runs - from the
|
|
53
|
-
* real socket, not from the caller's claim. The app therefore sees the honest
|
|
54
|
-
* (loopback) values instead of whatever the internet asserted. The gate reads the
|
|
55
|
-
* client IP for rate limiting from the raw inbound headers BEFORE stripping, and
|
|
56
|
-
* takes the RIGHTMOST `x-forwarded-for` entry, which is the only one a proxy
|
|
57
|
-
* appended rather than a client supplied.
|
|
58
|
-
*
|
|
59
|
-
* @module
|
|
60
|
-
*/
|
|
61
|
-
|
|
62
|
-
import { pattern, token, type Predicate } from "@dbx-tools/shared-core";
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Headers the gate strips UNCONDITIONALLY, before the allow-list is consulted -
|
|
66
|
-
* the platform-shaped set documented in this module's table.
|
|
67
|
-
*
|
|
68
|
-
* These answer WHO a request is and WHERE it came from, and on tunnel traffic
|
|
69
|
-
* only the gate may answer that: it injects {@link token.USER_ID_HEADER} /
|
|
70
|
-
* {@link token.USER_EMAIL_HEADER} itself after verifying a session, and the proxy
|
|
71
|
-
* re-derives the transport headers from the real socket.
|
|
72
|
-
*
|
|
73
|
-
* The identity names come from the shared `token` constants, so a renamed wire
|
|
74
|
-
* contract cannot leave a stale spelling here. `x-forwarded-preferred-username`
|
|
75
|
-
* has no constant because nothing in this repo READS it - it is listed precisely
|
|
76
|
-
* so a host app that does read it cannot be fed one.
|
|
77
|
-
*/
|
|
78
|
-
export const PROTECTED_HEADERS: readonly string[] = [
|
|
79
|
-
token.ACCESS_TOKEN_HEADER,
|
|
80
|
-
token.USER_ID_HEADER,
|
|
81
|
-
token.USER_EMAIL_HEADER,
|
|
82
|
-
"x-forwarded-preferred-username",
|
|
83
|
-
"x-forwarded-host",
|
|
84
|
-
"x-forwarded-proto",
|
|
85
|
-
"x-forwarded-port",
|
|
86
|
-
"x-forwarded-for",
|
|
87
|
-
"x-real-ip",
|
|
88
|
-
"x-request-id",
|
|
89
|
-
];
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* The `x-` headers forwarded when a deployment configures nothing.
|
|
93
|
-
*
|
|
94
|
-
* These are the header NAMESPACES this repo's own UI sends and its own server
|
|
95
|
-
* reads - Mastra thread/model routing (`x-mastra-thread-id`, `x-mastra-model`)
|
|
96
|
-
* and MLflow trace correlation (`x-mlflow-trace-id`) - so a dbx-tools app keeps
|
|
97
|
-
* working behind the tunnel with no configuration.
|
|
98
|
-
*
|
|
99
|
-
* Deliberately GLOBS rather than the exact constants from
|
|
100
|
-
* `@dbx-tools/shared-mastra`: importing them would make the gate - a
|
|
101
|
-
* transport-level component that has no other opinion about Mastra - depend on
|
|
102
|
-
* an agent package for three strings, and a namespace glob also covers the next
|
|
103
|
-
* header those packages add. The namespaces are the stable part of the contract.
|
|
104
|
-
*/
|
|
105
|
-
export const DEFAULT_FORWARD_HEADERS: readonly string[] = [
|
|
106
|
-
"x-mastra-*",
|
|
107
|
-
"x-mlflow-*",
|
|
108
|
-
// Sent by fetch/XHR wrappers to mark an AJAX request; harmless and widely read.
|
|
109
|
-
"x-requested-with",
|
|
110
|
-
];
|
|
111
|
-
|
|
112
|
-
/** A compiled inbound-header policy. Build one with {@link toHeaderPolicy}. */
|
|
113
|
-
export interface HeaderPolicy {
|
|
114
|
-
/** The allow-list entries backing this policy (for diagnostics and tests). */
|
|
115
|
-
readonly patterns: readonly string[];
|
|
116
|
-
/** Whether `name` survives on tunnel traffic. */
|
|
117
|
-
forwards(name: string): boolean;
|
|
118
|
-
/**
|
|
119
|
-
* Delete every disallowed header from a mutable Node header bag, returning the
|
|
120
|
-
* lower-cased names removed (for debug logging).
|
|
121
|
-
*/
|
|
122
|
-
apply(headers: Record<string, unknown>): string[];
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Compile the inbound-header policy. `configured` entries are UNIONED with
|
|
127
|
-
* {@link DEFAULT_FORWARD_HEADERS}; each may be a literal name, a glob
|
|
128
|
-
* (`x-myapp-*`), or a `/regex/`. Matching is case-insensitive.
|
|
129
|
-
*/
|
|
130
|
-
export function toHeaderPolicy(configured: readonly string[] = []): HeaderPolicy {
|
|
131
|
-
const patterns = [...DEFAULT_FORWARD_HEADERS, ...configured];
|
|
132
|
-
const allowed: Predicate<string> = pattern.toPatternMatcher(patterns);
|
|
133
|
-
const protectedNames = new Set(PROTECTED_HEADERS);
|
|
134
|
-
|
|
135
|
-
const forwards = (name: string): boolean => {
|
|
136
|
-
const lower = name.toLowerCase();
|
|
137
|
-
if (protectedNames.has(lower)) return false;
|
|
138
|
-
if (!lower.startsWith("x-")) return true;
|
|
139
|
-
return allowed(lower);
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
return {
|
|
143
|
-
patterns,
|
|
144
|
-
forwards,
|
|
145
|
-
apply: (headers) => {
|
|
146
|
-
const removed: string[] = [];
|
|
147
|
-
for (const name of Object.keys(headers)) {
|
|
148
|
-
if (forwards(name)) continue;
|
|
149
|
-
delete headers[name];
|
|
150
|
-
removed.push(name.toLowerCase());
|
|
151
|
-
}
|
|
152
|
-
return removed;
|
|
153
|
-
},
|
|
154
|
-
};
|
|
155
|
-
}
|