@dbx-tools/cli-tunnel 0.6.59 → 0.6.86

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.
Files changed (45) hide show
  1. package/README.md +1 -364
  2. package/index.ts +2 -19
  3. package/lib/index.d.ts +2 -19
  4. package/lib/index.js +2 -15
  5. package/lib/src/app.d.ts +12 -123
  6. package/lib/src/app.js +22 -250
  7. package/lib/src/cli.d.ts +19 -22
  8. package/lib/src/cli.js +143 -128
  9. package/lib/src/options.d.ts +46 -0
  10. package/lib/src/options.js +51 -0
  11. package/lib/src/proxy.d.ts +27 -37
  12. package/lib/src/proxy.js +115 -220
  13. package/lib/tsconfig.tsbuildinfo +1 -1
  14. package/package.json +15 -81
  15. package/src/app.ts +20 -282
  16. package/src/cli.ts +152 -162
  17. package/src/options.ts +85 -0
  18. package/src/proxy.ts +139 -261
  19. package/bin/dbx-tools-tunnel.ts +0 -13
  20. package/lib/bin/dbx-tools-tunnel.d.ts +0 -2
  21. package/lib/bin/dbx-tools-tunnel.js +0 -14
  22. package/lib/src/allowlist.d.ts +0 -32
  23. package/lib/src/allowlist.js +0 -57
  24. package/lib/src/env.d.ts +0 -57
  25. package/lib/src/env.js +0 -60
  26. package/lib/src/headers.d.ts +0 -108
  27. package/lib/src/headers.js +0 -140
  28. package/lib/src/otp.d.ts +0 -49
  29. package/lib/src/otp.js +0 -124
  30. package/lib/src/plugin.d.ts +0 -147
  31. package/lib/src/plugin.js +0 -138
  32. package/lib/src/portr.d.ts +0 -40
  33. package/lib/src/portr.js +0 -93
  34. package/lib/src/rate-limit.d.ts +0 -35
  35. package/lib/src/rate-limit.js +0 -53
  36. package/lib/src/signing-key.d.ts +0 -86
  37. package/lib/src/signing-key.js +0 -170
  38. package/src/allowlist.ts +0 -60
  39. package/src/env.ts +0 -72
  40. package/src/headers.ts +0 -155
  41. package/src/otp.ts +0 -137
  42. package/src/plugin.ts +0 -269
  43. package/src/portr.ts +0 -113
  44. package/src/rate-limit.ts +0 -59
  45. package/src/signing-key.ts +0 -201
package/README.md CHANGED
@@ -1,364 +1 @@
1
- # @dbx-tools/cli-tunnel
2
-
3
- Front an app with a public tunnel and an email one-time-code access gate.
4
-
5
- Built on [portr](https://github.com/amalshaji/portr). Run this CLI when an app
6
- needs to be reachable from outside its network - a stakeholder demo, a webhook
7
- sender that has to reach a dev build, an OAuth redirect that cannot point at
8
- `localhost` - without publishing the app to anyone who learns the URL. It is
9
- shaped for Databricks Apps (it honours the `DATABRICKS_APP_PORT` contract and
10
- lets the platform's own front door through ungated) but the gate itself is
11
- platform-neutral. The CLI wraps the app's real start command: it
12
- moves the app onto a private loopback port, binds the public port with a gate
13
- proxy, and brings the tunnel up alongside it. Access is granted per email
14
- address against an allow-list, verified by a code sent over
15
- [`@dbx-tools/email`](../../node/email).
16
-
17
- **Key features:**
18
-
19
- - Wraps an unmodified start command - everything after `--` runs as-is, so the
20
- app needs no tunnel-specific code:
21
- `dbxt-tunnel --allow example.com -- bun src/server.ts`.
22
- - Branded from the repo-wide brand context: the code email's accent colour, font,
23
- logo, and display name come from the app's own `branding/brand.yaml` (via
24
- `@dbx-tools/core`'s `loadBrandContext()`), falling back to the dbx-tools
25
- default - so the sign-in email looks like the app it fronts with nothing to
26
- configure. `--brand-name` overrides just the name.
27
- - Conventional one-time-code copy, so platform autofill works: the email is
28
- `Your verification code is: / <code> / This code expires in N minutes`, and the
29
- code input carries `autocomplete="one-time-code"`. iOS, Gmail, Outlook, and
30
- Android detect a code from that shape and offer it directly from the
31
- notification; novel phrasing is what breaks the detection. Both MIME parts carry
32
- the code as visible text, never an image, and no trailer follows the copy.
33
- - The code rides in the SUBJECT and the preheader, not only the body:
34
- `123456 is your verification code`. Mobile autofill reads an incoming
35
- NOTIFICATION, and a notification contains the sender, the subject, and a short
36
- snippet - nothing else - so a code that lives only in the body is unreachable
37
- however cleanly the body is shaped. See
38
- [Why the code is in the subject](#why-the-code-is-in-the-subject).
39
- - The two MIME parts are built separately, on purpose. The HTML part is the full
40
- branded template with the code as a large styled heading; the `text/plain` part
41
- is authored directly, keeping the prompt and the code on ONE line
42
- (`Your verification code is: 123456`) because that is the shape client code
43
- detection reads most reliably. A generated text part cannot hold it - the text
44
- part is a rendering of the HTML, so the heading's CSS margin arrives as blank
45
- lines and autofill stops being offered while the HTML still looks perfect. See
46
- `codeEmailTextBody` in the `app` module.
47
- - The sign-in code is SYSTEM mail: it sends from `no-reply@EMAIL_DOMAIN` (or
48
- `EMAIL_SYSTEM_FROM`), never a person's address, since a reply to a
49
- machine-generated code reaches nobody. `EMAIL_FROM` is not required - see
50
- [`@dbx-tools/email`](../../node/email#sender-addresses).
51
- - Email one-time-code gate: a 6-digit code stored as a SHA-256 hash with an
52
- attempt counter, verified in constant time, in AppKit's `CacheManager` (Lakebase
53
- when a database is bound to the deployment, else memory) so TTL expiry and
54
- eviction are the cache's job.
55
- - The gate resolves Lakebase for itself, so its cache is actually persistent. The
56
- gate is its own tiny AppKit app with no `lakebase()` plugin (it has no server to
57
- mount routes on), and AppKit only chooses Lakebase for the cache when a pool can
58
- be built from `LAKEBASE_ENDPOINT` **and** `PGHOST` **and** `PGDATABASE` - while a
59
- Databricks App `postgres` binding supplies only the first. The gate fills in the
60
- rest at boot, and skips entirely when nothing is bound rather than creating
61
- infrastructure on someone else's behalf.
62
- - HS256 session JWT (via `jose`) carrying only the email, signed with a key that
63
- is PERSISTED in AppKit's cache for 30 days - so a signed-in browser stays signed
64
- in across the restarts a tunnel sees whenever the app it wraps reloads. An
65
- operator-held `TUNNEL_AUTH_JWT_SECRET` still wins. `TUNNEL_AUTH_SESSION_CUTOFF`
66
- is the log-everyone-out switch, and takes a relative duration (`-30d`) as
67
- readily as a date.
68
- - Allow-list patterns in three shapes, matched in order: a domain shortcut
69
- (`example.com`, `@example.com`), a shell-style glob (`*@example.com`), or a
70
- regex literal (`/^ops-.*@example\.com$/`). An empty list allows nobody.
71
- - Per-email and per-IP fixed-window rate limiting, plus anti-enumeration: a code
72
- request always answers `{ ok: true }`, whether or not the address is allowed.
73
- - Inbound `x-` headers are stripped by default and re-allowed by pattern, so a
74
- public caller cannot spoof the headers the app trusts - above all
75
- `x-forwarded-access-token`, which would otherwise let anyone drive the app's
76
- workspace calls with a pasted token. Add an app's own headers with
77
- `--forward-headers`.
78
- - Platform traffic passes through UNGATED. The gate distinguishes the portr
79
- client (a loopback source address, same container) from the hosting platform's
80
- front door (a non-loopback container-network address), so health checks and the
81
- workspace UI keep working while public tunnel traffic is gated.
82
- - SPA-aware gating: static assets and the login routes stay open so the browser
83
- can load the client and render the login form; every other `/api/*` needs a
84
- valid session cookie or gets `401`. WebSocket upgrades are gated the same way.
85
- - Supervised teardown - the app child, the portr child, and this process are tied
86
- together, so if any one exits the whole tunnel comes down.
87
- - Fails fast when email is not configured for SMTP, because a gate that cannot
88
- send codes locks everyone out. `--insecure` is the explicit opt-out.
89
-
90
- ## Why This Over An Ad-Hoc Tunnel
91
-
92
- A bare tunnel (`ngrok`, `portr` on its own) makes the app reachable by anyone
93
- with the URL. This package keeps the tunnel but puts a gate in front of it,
94
- reusing what the app already has: AppKit's cache for code storage, the
95
- `@dbx-tools/email` transport for delivery, and the app's own `From` policy. There
96
- is nothing to add to the app itself - no route, no middleware, no auth library.
97
-
98
- ## Run It
99
-
100
- ```sh
101
- dbxt-tunnel \
102
- --allow "example.com, *@partner.example" \
103
- --brand-name "Acme Ops" \
104
- -- bun src/server.ts
105
- ```
106
-
107
- The package installs two equivalent commands, `dbx-tools-tunnel` and the shorter
108
- `dbxt-tunnel`. Neither matches the package name, so a one-off run has to name the
109
- command explicitly:
110
-
111
- ```sh
112
- npx --package @dbx-tools/cli-tunnel dbx-tools-tunnel --help
113
- ```
114
-
115
- Everything after `--` is the real start command. The CLI sets
116
- `DATABRICKS_APP_PORT` to a random private port for that child and binds the
117
- original public port itself.
118
-
119
- ## Options
120
-
121
- Every flag has an environment fallback, so a deployment can configure the gate
122
- with no change to its start command.
123
-
124
- | Flag | Env | Default |
125
- | ------------------- | ---------------------------- | ------------------------------ |
126
- | `--allow` | `TUNNEL_AUTH_ALLOW` | empty (allow nobody) |
127
- | `--subject` | `TUNNEL_AUTH_SUBJECT` | `Your verification code` |
128
- | `--brand-name` | `TUNNEL_AUTH_BRAND_NAME` | the brand context `name` |
129
- | `--message` | `TUNNEL_AUTH_MESSAGE` | `Your verification code is:` |
130
- | `--session-ttl` | `TUNNEL_AUTH_SESSION_TTL` | `2592000` (30 days) |
131
- | `--code-ttl` | `TUNNEL_AUTH_CODE_TTL` | `600` (10 minutes) |
132
- | `--session-cutoff` | `TUNNEL_AUTH_SESSION_CUTOFF` | unset (no cutoff) |
133
- | `--subdomain` | - | derived from the public domain |
134
- | `--public-domain` | `TUNNEL_PUBLIC_DOMAIN` | - |
135
- | `--forward-headers` | `TUNNEL_FORWARD_HEADERS` | the built-in `x-` allow-list |
136
- | `--insecure` | `TUNNEL_INSECURE` | off (the gate is required) |
137
- | - | `TUNNEL_AUTH_JWT_SECRET` | an ephemeral per-process key |
138
-
139
- Every variable is `TUNNEL_`-prefixed because the gate runs as a WRAPPER: it and
140
- the app it wraps share one environment, so a generic name is one the app may
141
- already be using. The earlier unprefixed spellings - `AUTH_SUBJECT`,
142
- `AUTH_BRAND_NAME`, `AUTH_MESSAGE`, `AUTH_SESSION_TTL`, `AUTH_CODE_TTL`,
143
- `AUTH_JWT_SECRET`, `EMAIL_AUTH_ALLOW`, `PUBLIC_DOMAIN`, plus
144
- `TUNNEL_AUTH_SESSION_EPOCH` from before the cutoff rename - are still read as
145
- deprecated aliases, with the `TUNNEL_` name winning when both are set, so an
146
- existing deployment needs no coordinated rename. `PORTR_TOKEN` / `PORTR_SERVER`
147
- keep their names: that namespace belongs to portr itself, as does
148
- `DATABRICKS_APP_PORT`, which the platform sets and the gate honours.
149
-
150
- `--allow` and `TUNNEL_AUTH_ALLOW` are UNIONED rather than one overriding the
151
- other, so a deployment-wide allow-list and a per-invocation addition both grant
152
- access.
153
-
154
- ## Sessions That Survive A Restart
155
-
156
- The signing key decides whether an already-issued session COOKIE still verifies,
157
- so where that key comes from is what decides whether a restart signs everyone out.
158
- Resolution order:
159
-
160
- 1. **`TUNNEL_AUTH_JWT_SECRET`**, when set. The right answer for a fleet: an
161
- operator-held secret needs no shared cache, and it survives a cache flush.
162
- 2. **A key persisted in AppKit's cache for 30 days.** With a persistent
163
- `CacheStorage` (Lakebase) the key outlives the process, so cookies stay valid
164
- across restarts - which a tunnel does often, since it restarts whenever the app
165
- it wraps reloads. On the default in-memory cache the key is per-process, the
166
- same as having no secret at all.
167
-
168
- Getting that persistence is not automatic, and the gate does the work at boot
169
- via `lakebaseResolver.applyLakebaseEnv()`: it turns `LAKEBASE_ENDPOINT` into the
170
- `PGHOST` / `PGDATABASE` / `PGUSER` the cache's pool also needs. Without them
171
- AppKit cannot build the pool, silently uses an in-memory cache, and every
172
- redeploy signs everyone out - so the startup log says which one you got
173
- (`lakebase resolved for the gate cache`, or `the gate cache stays in memory`).
174
- Bind a `postgres` resource to the app to get the persistent path.
175
-
176
- 3. **An ephemeral per-process key**, when there is no secret and no reachable
177
- cache. Sessions do not survive a restart, but the gate still serves: the key
178
- only validates an ALREADY-issued session, so losing it costs sessions, never
179
- admission. A caller still needs a code delivered to an allow-listed address.
180
-
181
- The cached key is read, generated-and-stored, then **re-read**. Two instances
182
- booting together both miss the cache, so both generate; adopting whatever is
183
- STORED afterwards is what makes them converge on one key instead of each trusting
184
- the one it minted. Set `TUNNEL_AUTH_JWT_SECRET` to remove the race entirely.
185
-
186
- `TUNNEL_AUTH_SESSION_TTL` defaults to the same 30 days the key is stored for, on
187
- purpose - a key that expired before the cookies it signed would sign everyone out
188
- for no reason.
189
-
190
- ## Why The Code Is In The Subject
191
-
192
- The subject line the gate SENDS is `123456 is your verification code` -
193
- `--subject` is the template the code is spliced into, not the literal line.
194
-
195
- Mobile autofill does not read the email; it reads the NOTIFICATION. iOS scans
196
- incoming notification text for a code and offers to fill it - natively for
197
- Messages and Mail, and since iOS 26 for any app's notification, which is what
198
- finally made Gmail work. A notification carries the sender, the subject, and a
199
- short snippet. That is all. A code sitting in the body is invisible to it no
200
- matter how carefully the body is formatted, which is why a perfectly shaped
201
- `text/plain` part alone produced no prompt in Gmail.
202
-
203
- So the code goes in both strings a notification actually shows:
204
-
205
- - **The subject**, code FIRST, because a notification and an inbox row both
206
- truncate: `123456 is your verification code` survives the cut wherever it lands,
207
- and keeps the code in the same sentence as the words the heuristics look for.
208
- A subject that does not use the conventional `Your ...` phrasing is treated as
209
- deliberate and only prefixed (`123456 - Acme Ops access`).
210
- - **The preheader**, the hidden snippet a client shows beside the subject and puts
211
- in the notification body, repeating the prompt with the code
212
- (`Your verification code is: 123456`).
213
-
214
- The body keeps the code as a large styled heading regardless, for a recipient
215
- reading the mail rather than a notification. This is deliberately NOT Apple's
216
- domain-bound `@domain #code` trailer, which binds a code to a single origin;
217
- subject + preheader works across clients and needs no origin.
218
-
219
- ### Signing everyone out
220
-
221
- `--session-cutoff` / `TUNNEL_AUTH_SESSION_CUTOFF` invalidates every session issued
222
- before a given moment:
223
-
224
- ```sh
225
- dbxt-tunnel --session-cutoff -30d -- bun src/server.ts
226
- dbxt-tunnel --session-cutoff 2026-08-02 -- bun src/server.ts
227
- TUNNEL_AUTH_SESSION_CUTOFF="now" dbxt-tunnel -- bun src/server.ts
228
- ```
229
-
230
- The value goes through `@dbx-tools/shared-core`'s `object.toDate`, so a date, an
231
- ISO instant, epoch seconds or millis from `date +%s`, and a relative duration
232
- (`-30d`, `12 hours ago`) all work - the relative spelling being the one an
233
- operator usually wants, since "sign out anything older than a month" needs no
234
- timestamp arithmetic. It works two ways at once, so it holds however the key was
235
- resolved: the cutoff is part of the key's CACHE KEY (moving it orphans the
236
- previous key), and it is also checked against each token's `iat` (which is what
237
- makes it bite when `TUNNEL_AUTH_JWT_SECRET` is set and there is no key to
238
- rotate).
239
-
240
- A FUTURE date is clamped to now, because an unclamped one would refuse the
241
- sessions it is about to mint as well as the old ones - an app nobody can sign in
242
- to, from a mistyped year. An unparseable value is ignored with a warning rather
243
- than failing startup: this is the switch that gets a fleet back in.
244
-
245
- ## Inbound Header Policy
246
-
247
- Tunnel traffic arrives from the public internet, so every header on it is
248
- attacker-controlled - and the headers an app trusts are exactly the ones a caller
249
- must not be able to write, because the app cannot tell a header the Databricks
250
- front door set from one a browser typed.
251
-
252
- Enumerating headers to remove is a losing game (a deny-list is only correct until
253
- the platform adds a header), so the policy is inverted: **every `x-`-prefixed
254
- request header is stripped from tunnel traffic unless a pattern allows it.** A
255
- header nobody thought about is removed rather than trusted. Non-`x-` headers -
256
- `content-type`, `accept`, `authorization`, `cookie` - are the app's normal input
257
- and pass through untouched.
258
-
259
- The default allow-list covers the `x-` namespaces this repo's own client sends
260
- and its own server reads, so a dbx-tools app works behind the tunnel with no
261
- configuration:
262
-
263
- | Pattern | Why |
264
- | ------------------ | --------------------------------------------------- |
265
- | `x-mastra-*` | thread and model routing for `@dbx-tools/ui-mastra` |
266
- | `x-mlflow-*` | MLflow trace correlation for feedback |
267
- | `x-requested-with` | the conventional AJAX marker |
268
-
269
- Add an app's own headers with `--forward-headers` /`TUNNEL_FORWARD_HEADERS`. Each
270
- entry is a literal name, a shell-style glob, or a `/regex/` - the same three
271
- shapes the email allow-list takes - and the configured list is UNIONED with the
272
- defaults, so extending it never silently breaks the built-in surfaces:
273
-
274
- ```sh
275
- dbxt-tunnel --forward-headers "x-acme-*, /^x-trace-/, x-tenant" -- bun src/server.ts
276
- ```
277
-
278
- Some headers no pattern can forward. These decide who a request is and where it
279
- came from, and on tunnel traffic only the gate may answer that:
280
-
281
- | Header | Why it is never forwarded |
282
- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
283
- | `x-forwarded-access-token` | OBO auth. A pasted workspace token would make every call in the app run as its owner; a verified email proves nothing about who a credential belongs to. |
284
- | `x-forwarded-user`, `-email` | Caller identity. The gate sets these itself, from a verified session. |
285
- | `x-forwarded-preferred-username` | Display identity from the IdP. |
286
- | `x-forwarded-host`, `-proto`, `-port` | Original host/scheme/port. Spoofing them poisons absolute URLs the app builds, or makes a plaintext request look like TLS. |
287
- | `x-forwarded-for`, `x-real-ip` | Client IP. Spoofing forges the audit trail and gives a caller a fresh rate-limit bucket per request. |
288
- | `x-request-id` | Request correlation UUID. Forged or colliding ids make logs unreliable. |
289
-
290
- The identity headers are AppKit's OBO contract; the rest are the
291
- [`X-Forwarded-*` set Databricks Apps documents passing to an
292
- app](https://docs.databricks.com/aws/en/dev-tools/databricks-apps/http-headers),
293
- plus the conventional `x-forwarded-proto`/`-port`/`x-real-ip` a library may read
294
- anyway. Dropping the transport headers costs nothing: the proxy re-adds them from
295
- the real socket after the policy runs, so the app sees the honest values instead
296
- of the caller's claim. Rate limiting reads the client IP before stripping, and
297
- takes the **rightmost** `x-forwarded-for` entry - the only one a proxy appended
298
- rather than a client supplied.
299
-
300
- ## Use The Gate As A Plugin
301
-
302
- The gate is an AppKit plugin, so an app that wants the OTP flow without the
303
- tunnel can mount it directly. It registers no routes - it exposes handlers the
304
- caller invokes - and it takes a `sendCode` callback because delivering mail is
305
- the one thing it cannot resolve on its own:
306
-
307
- ```ts
308
- import { createApp } from "@dbx-tools/appkit";
309
- import { authGate } from "@dbx-tools/cli-tunnel/plugin";
310
- import { brand, email, sender, transport } from "@dbx-tools/email";
311
-
312
- const handle = await createApp({
313
- plugins: [
314
- email({ brand: brand.defaultEmailBrand }),
315
- authGate({
316
- allow: ["example.com"],
317
- sendCode: async (to, code, opts) => {
318
- const runtime = transport.getEmailRuntime();
319
- await transport.sendEmail(
320
- {
321
- to: [to],
322
- subject: opts.subject,
323
- body: `${opts.message}\n\n## ${code}`,
324
- },
325
- // The app's configured sender; a code email has no on-behalf-of user.
326
- sender.resolveSenderAddress(runtime.config, undefined),
327
- );
328
- },
329
- }),
330
- ],
331
- });
332
-
333
- // The gate exposes handlers rather than routes - call them from your own server.
334
- const status = await handle.authGate.status(sessionCookieValue);
335
- ```
336
-
337
- `sendCode` is the one thing the plugin cannot resolve on its own. When the tunnel
338
- CLI boots the gate it wires this same callback, and derives the email styling from
339
- the brand context, so mounting the plugin directly is the only case that needs it
340
- by hand.
341
-
342
- ## Modules
343
-
344
- - `cli` - argv parsing, the wrapped-command split at `--`, and process
345
- supervision.
346
- - `plugin` - `authGate()`, its config/env resolution, and the `AuthGateApi`
347
- handlers the proxy calls in-process.
348
- - `proxy` - the public-port reverse proxy: loopback-vs-platform classification,
349
- open login routes, session enforcement, and WebSocket forwarding.
350
- - `otp` - the `CacheManager`-backed code store and the session JWT.
351
- - `signingKey` - the cache-persisted HS256 session key (30-day TTL, get/generate/
352
- re-read convergence) and the `TUNNEL_AUTH_SESSION_CUTOFF` force-clear cutoff.
353
- - `allowlist` - email domain / glob / regex matching and `looksLikeEmail`.
354
- - `headers` - the inbound-header allow-list: `toHeaderPolicy()`,
355
- `DEFAULT_FORWARD_HEADERS`, and the `PROTECTED_HEADERS` no pattern can forward.
356
- - `rate-limit` - the in-memory fixed-window limiter (single-instance only; not
357
- distributed).
358
- - `portr` - portr install, config rendering, and child launch.
359
- - `env` - the environment-variable names, each with its deprecated aliases.
360
- - `app` - boots the minimal gate AppKit app and returns the `AuthGateApi`.
361
-
362
- Browser-safe login wire schemas (the request/verify payloads and the session
363
- cookie name) live in [`@dbx-tools/shared-email`](../../shared/email); the React
364
- login surface is in [`@dbx-tools/ui-email`](../../ui/email).
1
+ # replace this
package/index.ts CHANGED
@@ -2,27 +2,10 @@
2
2
  // Regenerated from the exporting modules in ./src.
3
3
  // Hand edits are overwritten on the next watch; this file is read-only.
4
4
 
5
- export * as allowlist from "./src/allowlist.ts";
6
5
  export * as app from "./src/app.ts";
7
6
  export * as cli from "./src/cli.ts";
8
- export * as env from "./src/env.ts";
9
- export * as headers from "./src/headers.ts";
10
- export * as otp from "./src/otp.ts";
11
- export * as plugin from "./src/plugin.ts";
12
- export * as portr from "./src/portr.ts";
7
+ export * as options from "./src/options.ts";
13
8
  export * as proxy from "./src/proxy.ts";
14
- export * as rateLimit from "./src/rate-limit.ts";
15
- export * as signingKey from "./src/signing-key.ts";
16
9
  export { CommanderError } from "./src/cli.ts";
17
- export { ALLOW_ENV, SUBJECT_ENV, BRAND_NAME_ENV, MESSAGE_ENV, SESSION_TTL_ENV, CODE_TTL_ENV, JWT_SECRET_ENV, SESSION_CUTOFF_ENV, PUBLIC_DOMAIN_ENV, INSECURE_ENV, FORWARD_HEADERS_ENV } from "./src/env.ts";
18
- export { PROTECTED_HEADERS, DEFAULT_FORWARD_HEADERS } from "./src/headers.ts";
19
- export type { HeaderPolicy } from "./src/headers.ts";
20
- export { CodeStore } from "./src/otp.ts";
21
- export type { VerifyOutcome } from "./src/otp.ts";
22
- export { AuthGatePlugin, authGate } from "./src/plugin.ts";
23
- export type { AuthGateConfig, SendCodeOptions, ResolvedAuthGateConfig, AuthGateApi } from "./src/plugin.ts";
24
- export type { PortrConfig } from "./src/portr.ts";
10
+ export type { TunnelOptions, ResolvedTunnelOptions } from "./src/options.ts";
25
11
  export type { ProxyOptions } from "./src/proxy.ts";
26
- export { RateLimiter } from "./src/rate-limit.ts";
27
- export { KEY_TTL_SECONDS } from "./src/signing-key.ts";
28
- export type { SigningKey } from "./src/signing-key.ts";
package/lib/index.d.ts CHANGED
@@ -1,24 +1,7 @@
1
- export * as allowlist from "./src/allowlist.ts";
2
1
  export * as app from "./src/app.ts";
3
2
  export * as cli from "./src/cli.ts";
4
- export * as env from "./src/env.ts";
5
- export * as headers from "./src/headers.ts";
6
- export * as otp from "./src/otp.ts";
7
- export * as plugin from "./src/plugin.ts";
8
- export * as portr from "./src/portr.ts";
3
+ export * as options from "./src/options.ts";
9
4
  export * as proxy from "./src/proxy.ts";
10
- export * as rateLimit from "./src/rate-limit.ts";
11
- export * as signingKey from "./src/signing-key.ts";
12
5
  export { CommanderError } from "./src/cli.ts";
13
- export { ALLOW_ENV, SUBJECT_ENV, BRAND_NAME_ENV, MESSAGE_ENV, SESSION_TTL_ENV, CODE_TTL_ENV, JWT_SECRET_ENV, SESSION_CUTOFF_ENV, PUBLIC_DOMAIN_ENV, INSECURE_ENV, FORWARD_HEADERS_ENV } from "./src/env.ts";
14
- export { PROTECTED_HEADERS, DEFAULT_FORWARD_HEADERS } from "./src/headers.ts";
15
- export type { HeaderPolicy } from "./src/headers.ts";
16
- export { CodeStore } from "./src/otp.ts";
17
- export type { VerifyOutcome } from "./src/otp.ts";
18
- export { AuthGatePlugin, authGate } from "./src/plugin.ts";
19
- export type { AuthGateConfig, SendCodeOptions, ResolvedAuthGateConfig, AuthGateApi } from "./src/plugin.ts";
20
- export type { PortrConfig } from "./src/portr.ts";
6
+ export type { TunnelOptions, ResolvedTunnelOptions } from "./src/options.ts";
21
7
  export type { ProxyOptions } from "./src/proxy.ts";
22
- export { RateLimiter } from "./src/rate-limit.ts";
23
- export { KEY_TTL_SECONDS } from "./src/signing-key.ts";
24
- export type { SigningKey } from "./src/signing-key.ts";
package/lib/index.js CHANGED
@@ -1,22 +1,9 @@
1
1
  // GENERATED by projen watch - DO NOT EDIT.
2
2
  // Regenerated from the exporting modules in ./src.
3
3
  // Hand edits are overwritten on the next watch; this file is read-only.
4
- export * as allowlist from "./src/allowlist.js";
5
4
  export * as app from "./src/app.js";
6
5
  export * as cli from "./src/cli.js";
7
- export * as env from "./src/env.js";
8
- export * as headers from "./src/headers.js";
9
- export * as otp from "./src/otp.js";
10
- export * as plugin from "./src/plugin.js";
11
- export * as portr from "./src/portr.js";
6
+ export * as options from "./src/options.js";
12
7
  export * as proxy from "./src/proxy.js";
13
- export * as rateLimit from "./src/rate-limit.js";
14
- export * as signingKey from "./src/signing-key.js";
15
8
  export { CommanderError } from "./src/cli.js";
16
- export { ALLOW_ENV, SUBJECT_ENV, BRAND_NAME_ENV, MESSAGE_ENV, SESSION_TTL_ENV, CODE_TTL_ENV, JWT_SECRET_ENV, SESSION_CUTOFF_ENV, PUBLIC_DOMAIN_ENV, INSECURE_ENV, FORWARD_HEADERS_ENV } from "./src/env.js";
17
- export { PROTECTED_HEADERS, DEFAULT_FORWARD_HEADERS } from "./src/headers.js";
18
- export { CodeStore } from "./src/otp.js";
19
- export { AuthGatePlugin, authGate } from "./src/plugin.js";
20
- export { RateLimiter } from "./src/rate-limit.js";
21
- export { KEY_TTL_SECONDS } from "./src/signing-key.js";
22
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwyQ0FBMkM7QUFDM0MsbURBQW1EO0FBQ25ELHdFQUF3RTtBQUV4RSxPQUFPLEtBQUssU0FBUyxNQUFNLG9CQUFvQixDQUFDO0FBQ2hELE9BQU8sS0FBSyxHQUFHLE1BQU0sY0FBYyxDQUFDO0FBQ3BDLE9BQU8sS0FBSyxHQUFHLE1BQU0sY0FBYyxDQUFDO0FBQ3BDLE9BQU8sS0FBSyxHQUFHLE1BQU0sY0FBYyxDQUFDO0FBQ3BDLE9BQU8sS0FBSyxPQUFPLE1BQU0sa0JBQWtCLENBQUM7QUFDNUMsT0FBTyxLQUFLLEdBQUcsTUFBTSxjQUFjLENBQUM7QUFDcEMsT0FBTyxLQUFLLE1BQU0sTUFBTSxpQkFBaUIsQ0FBQztBQUMxQyxPQUFPLEtBQUssS0FBSyxNQUFNLGdCQUFnQixDQUFDO0FBQ3hDLE9BQU8sS0FBSyxLQUFLLE1BQU0sZ0JBQWdCLENBQUM7QUFDeEMsT0FBTyxLQUFLLFNBQVMsTUFBTSxxQkFBcUIsQ0FBQztBQUNqRCxPQUFPLEtBQUssVUFBVSxNQUFNLHNCQUFzQixDQUFDO0FBQ25ELE9BQU8sRUFBRSxjQUFjLEVBQUUsTUFBTSxjQUFjLENBQUM7QUFDOUMsT0FBTyxFQUFFLFNBQVMsRUFBRSxXQUFXLEVBQUUsY0FBYyxFQUFFLFdBQVcsRUFBRSxlQUFlLEVBQUUsWUFBWSxFQUFFLGNBQWMsRUFBRSxrQkFBa0IsRUFBRSxpQkFBaUIsRUFBRSxZQUFZLEVBQUUsbUJBQW1CLEVBQUUsTUFBTSxjQUFjLENBQUM7QUFDNU0sT0FBTyxFQUFFLGlCQUFpQixFQUFFLHVCQUF1QixFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFFOUUsT0FBTyxFQUFFLFNBQVMsRUFBRSxNQUFNLGNBQWMsQ0FBQztBQUV6QyxPQUFPLEVBQUUsY0FBYyxFQUFFLFFBQVEsRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBSTNELE9BQU8sRUFBRSxXQUFXLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQUNsRCxPQUFPLEVBQUUsZUFBZSxFQUFFLE1BQU0sc0JBQXNCLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBHRU5FUkFURUQgYnkgcHJvamVuIHdhdGNoIC0gRE8gTk9UIEVESVQuXG4vLyBSZWdlbmVyYXRlZCBmcm9tIHRoZSBleHBvcnRpbmcgbW9kdWxlcyBpbiAuL3NyYy5cbi8vIEhhbmQgZWRpdHMgYXJlIG92ZXJ3cml0dGVuIG9uIHRoZSBuZXh0IHdhdGNoOyB0aGlzIGZpbGUgaXMgcmVhZC1vbmx5LlxuXG5leHBvcnQgKiBhcyBhbGxvd2xpc3QgZnJvbSBcIi4vc3JjL2FsbG93bGlzdC50c1wiO1xuZXhwb3J0ICogYXMgYXBwIGZyb20gXCIuL3NyYy9hcHAudHNcIjtcbmV4cG9ydCAqIGFzIGNsaSBmcm9tIFwiLi9zcmMvY2xpLnRzXCI7XG5leHBvcnQgKiBhcyBlbnYgZnJvbSBcIi4vc3JjL2Vudi50c1wiO1xuZXhwb3J0ICogYXMgaGVhZGVycyBmcm9tIFwiLi9zcmMvaGVhZGVycy50c1wiO1xuZXhwb3J0ICogYXMgb3RwIGZyb20gXCIuL3NyYy9vdHAudHNcIjtcbmV4cG9ydCAqIGFzIHBsdWdpbiBmcm9tIFwiLi9zcmMvcGx1Z2luLnRzXCI7XG5leHBvcnQgKiBhcyBwb3J0ciBmcm9tIFwiLi9zcmMvcG9ydHIudHNcIjtcbmV4cG9ydCAqIGFzIHByb3h5IGZyb20gXCIuL3NyYy9wcm94eS50c1wiO1xuZXhwb3J0ICogYXMgcmF0ZUxpbWl0IGZyb20gXCIuL3NyYy9yYXRlLWxpbWl0LnRzXCI7XG5leHBvcnQgKiBhcyBzaWduaW5nS2V5IGZyb20gXCIuL3NyYy9zaWduaW5nLWtleS50c1wiO1xuZXhwb3J0IHsgQ29tbWFuZGVyRXJyb3IgfSBmcm9tIFwiLi9zcmMvY2xpLnRzXCI7XG5leHBvcnQgeyBBTExPV19FTlYsIFNVQkpFQ1RfRU5WLCBCUkFORF9OQU1FX0VOViwgTUVTU0FHRV9FTlYsIFNFU1NJT05fVFRMX0VOViwgQ09ERV9UVExfRU5WLCBKV1RfU0VDUkVUX0VOViwgU0VTU0lPTl9DVVRPRkZfRU5WLCBQVUJMSUNfRE9NQUlOX0VOViwgSU5TRUNVUkVfRU5WLCBGT1JXQVJEX0hFQURFUlNfRU5WIH0gZnJvbSBcIi4vc3JjL2Vudi50c1wiO1xuZXhwb3J0IHsgUFJPVEVDVEVEX0hFQURFUlMsIERFRkFVTFRfRk9SV0FSRF9IRUFERVJTIH0gZnJvbSBcIi4vc3JjL2hlYWRlcnMudHNcIjtcbmV4cG9ydCB0eXBlIHsgSGVhZGVyUG9saWN5IH0gZnJvbSBcIi4vc3JjL2hlYWRlcnMudHNcIjtcbmV4cG9ydCB7IENvZGVTdG9yZSB9IGZyb20gXCIuL3NyYy9vdHAudHNcIjtcbmV4cG9ydCB0eXBlIHsgVmVyaWZ5T3V0Y29tZSB9IGZyb20gXCIuL3NyYy9vdHAudHNcIjtcbmV4cG9ydCB7IEF1dGhHYXRlUGx1Z2luLCBhdXRoR2F0ZSB9IGZyb20gXCIuL3NyYy9wbHVnaW4udHNcIjtcbmV4cG9ydCB0eXBlIHsgQXV0aEdhdGVDb25maWcsIFNlbmRDb2RlT3B0aW9ucywgUmVzb2x2ZWRBdXRoR2F0ZUNvbmZpZywgQXV0aEdhdGVBcGkgfSBmcm9tIFwiLi9zcmMvcGx1Z2luLnRzXCI7XG5leHBvcnQgdHlwZSB7IFBvcnRyQ29uZmlnIH0gZnJvbSBcIi4vc3JjL3BvcnRyLnRzXCI7XG5leHBvcnQgdHlwZSB7IFByb3h5T3B0aW9ucyB9IGZyb20gXCIuL3NyYy9wcm94eS50c1wiO1xuZXhwb3J0IHsgUmF0ZUxpbWl0ZXIgfSBmcm9tIFwiLi9zcmMvcmF0ZS1saW1pdC50c1wiO1xuZXhwb3J0IHsgS0VZX1RUTF9TRUNPTkRTIH0gZnJvbSBcIi4vc3JjL3NpZ25pbmcta2V5LnRzXCI7XG5leHBvcnQgdHlwZSB7IFNpZ25pbmdLZXkgfSBmcm9tIFwiLi9zcmMvc2lnbmluZy1rZXkudHNcIjtcbiJdfQ==
9
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwyQ0FBMkM7QUFDM0MsbURBQW1EO0FBQ25ELHdFQUF3RTtBQUV4RSxPQUFPLEtBQUssR0FBRyxNQUFNLGNBQWMsQ0FBQztBQUNwQyxPQUFPLEtBQUssR0FBRyxNQUFNLGNBQWMsQ0FBQztBQUNwQyxPQUFPLEtBQUssT0FBTyxNQUFNLGtCQUFrQixDQUFDO0FBQzVDLE9BQU8sS0FBSyxLQUFLLE1BQU0sZ0JBQWdCLENBQUM7QUFDeEMsT0FBTyxFQUFFLGNBQWMsRUFBRSxNQUFNLGNBQWMsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8vIEdFTkVSQVRFRCBieSBwcm9qZW4gd2F0Y2ggLSBETyBOT1QgRURJVC5cbi8vIFJlZ2VuZXJhdGVkIGZyb20gdGhlIGV4cG9ydGluZyBtb2R1bGVzIGluIC4vc3JjLlxuLy8gSGFuZCBlZGl0cyBhcmUgb3ZlcndyaXR0ZW4gb24gdGhlIG5leHQgd2F0Y2g7IHRoaXMgZmlsZSBpcyByZWFkLW9ubHkuXG5cbmV4cG9ydCAqIGFzIGFwcCBmcm9tIFwiLi9zcmMvYXBwLnRzXCI7XG5leHBvcnQgKiBhcyBjbGkgZnJvbSBcIi4vc3JjL2NsaS50c1wiO1xuZXhwb3J0ICogYXMgb3B0aW9ucyBmcm9tIFwiLi9zcmMvb3B0aW9ucy50c1wiO1xuZXhwb3J0ICogYXMgcHJveHkgZnJvbSBcIi4vc3JjL3Byb3h5LnRzXCI7XG5leHBvcnQgeyBDb21tYW5kZXJFcnJvciB9IGZyb20gXCIuL3NyYy9jbGkudHNcIjtcbmV4cG9ydCB0eXBlIHsgVHVubmVsT3B0aW9ucywgUmVzb2x2ZWRUdW5uZWxPcHRpb25zIH0gZnJvbSBcIi4vc3JjL29wdGlvbnMudHNcIjtcbmV4cG9ydCB0eXBlIHsgUHJveHlPcHRpb25zIH0gZnJvbSBcIi4vc3JjL3Byb3h5LnRzXCI7XG4iXX0=
package/lib/src/app.d.ts CHANGED
@@ -1,130 +1,19 @@
1
1
  /**
2
- * The tunnel gate's tiny AppKit "app" - `createApp` WITHOUT a `server()` plugin.
2
+ * The GATE half of the wrapper: a server-less AppKit app whose only job is to
3
+ * expose the `authGate` handlers the proxy calls.
3
4
  *
4
- * There is no HTTP server here: the tunnel proxy is the server, and it calls the
5
- * gate handlers in-process. `createApp` is used only for what it auto-wires:
6
- * - `CacheManager` (Lakebase when this process can reach it, else memory) -
7
- * which the OTP `CodeStore` and the session signing key use for storage + TTL
8
- * eviction;
9
- * - the `email()` transport (SMTP / outbox), primed from env, so `sendCode` can
10
- * deliver the one-time code;
11
- * - dbx-tools auto-configuration + branding via `@dbx-tools/appkit`.
5
+ * The in-process plugin path (`authGate` in the app's own `plugins`) has a real
6
+ * HTTP server to mount routes on. A wrapper does not - the app it fronts is a
7
+ * separate process it must not reach into - so the gate's login routes live on
8
+ * the proxy instead, and this app exists purely to give the plugin the runtime it
9
+ * needs: a `CacheManager` for the one-time-code store and signing key, and the
10
+ * sibling `email` plugin's transport for delivering a code.
12
11
  *
13
- * It returns the {@link AuthGateApi} the proxy drives. `sendCode` is wired here
14
- * (the plugin can't resolve HOW to send on its own) to the email plugin's
15
- * transport. A sign-in code is SYSTEM mail - no on-behalf-of user asked for it
16
- * and no reply to it reaches anyone - so it sends from the email config's
17
- * do-not-reply address (`no-reply@EMAIL_DOMAIN` unless EMAIL_SYSTEM_FROM names
18
- * another).
19
- *
20
- * FAIL FAST: a gate that can't email a code is useless, so if email does not
21
- * resolve to SMTP mode (real delivery), this throws - unless `insecure` is set
22
- * (`--insecure` / `TUNNEL_INSECURE=true`), in which case the caller runs the
23
- * tunnel OPEN with no gate.
12
+ * Lazily imported by `cli.ts`, so `dbx tunnel --insecure` (and `install` /
13
+ * `status`) never load AppKit, the Databricks SDK, or the SMTP stack.
24
14
  *
25
15
  * @module
26
16
  */
27
- import { type AuthGateApi, type AuthGateConfig, type SendCodeOptions } from "./plugin.ts";
28
- /**
29
- * A code TTL as the plain phrase the email states ("10 minutes", "45 seconds").
30
- *
31
- * Whole minutes read as minutes; anything else stays in seconds rather than
32
- * rounding, so a 90-second TTL is not advertised as "1 minute" and a recipient is
33
- * never told the code lives longer than it does.
34
- */
35
- export declare function expiresIn(seconds: number): string;
36
- /** The parts of {@link SendCodeOptions} the code email's copy is built from. */
37
- type CodeCopy = Pick<SendCodeOptions, "message" | "codeTtlSeconds">;
38
- /**
39
- * The HTML part's source: the full branded template, with the code as a large
40
- * styled heading (`## ` is what makes it prominent in an inbox).
41
- */
42
- export declare function codeEmailHtmlBody(code: string, opts: CodeCopy): string;
43
- /**
44
- * The `text/plain` part, supplied EXPLICITLY rather than rendered from the tree
45
- * above. Both parts say the same thing; only the line layout differs.
46
- *
47
- * The prompt and the code share ONE line ("Your verification code is: 123456").
48
- * That single-line shape is what iOS, Gmail, Outlook, and Android code detection
49
- * keys on most reliably - the heuristics look for a code in the same sentence as
50
- * a recognized prompt, so splitting them across lines makes detection dependent
51
- * on the client, and any blank line between them defeats it outright.
52
- *
53
- * The GENERATED text part cannot hold that shape at all: it is a rendering of the
54
- * HTML, so it carries the brand header/footer and turns the code heading's CSS
55
- * margin into blank lines, arriving as `prompt\n\n\ncode`.
56
- *
57
- * The code is visible text in BOTH parts, never an image, so a client scraping
58
- * either one finds it. No trailer line follows the copy - Apple's domain-bound
59
- * `@domain #code` footer is deliberately NOT emitted, since it constrains the
60
- * code to one origin and is not what the broadly-compatible shape needs.
61
- */
62
- export declare function codeEmailTextBody(code: string, opts: CodeCopy): string;
63
- /**
64
- * The SUBJECT line, with the code in it: `"123456 is your verification code"`.
65
- *
66
- * The code has to be here, not only in the body, because of what mobile autofill
67
- * actually reads. iOS offers a code from an incoming NOTIFICATION - natively for
68
- * Messages and Mail, and since iOS 26 for any app's notification text, which is
69
- * what finally made Gmail work - and a notification contains the sender, the
70
- * subject, and a short snippet. Nothing else. A code that lives in the body is
71
- * invisible to it, however cleanly the body is formatted, which is why a perfectly
72
- * shaped `text/plain` part still produced no autofill prompt in Gmail.
73
- *
74
- * `<code> is your <thing>` rather than `<thing>: <code>` because the leading code
75
- * survives TRUNCATION: a notification and an inbox list both cut the subject, and
76
- * the platform heuristics want the code in the same sentence as a recognized
77
- * prompt ("code", "verification"). Putting it first keeps both intact no matter
78
- * where the cut lands.
79
- *
80
- * `subject` is the configured line ("Your verification code"), lower-cased at its
81
- * first word so the sentence reads naturally, and left ALONE when it does not look
82
- * like the conventional phrasing - an operator who set a deliberate subject gets
83
- * theirs with the code prefixed, not a mangled hybrid.
84
- */
85
- export declare function codeEmailSubject(code: string, subject: string): string;
86
- /**
87
- * The PREHEADER: the snippet beside the subject in an inbox list, and the body of
88
- * the push notification. Carries the code for the same reason the subject does -
89
- * it is the other half of what a notification shows - and repeats the prompt
90
- * wording so a heuristic scanning the snippet alone finds a code next to a phrase
91
- * it recognizes.
92
- */
93
- export declare function codeEmailPreview(code: string, opts: CodeCopy): string;
94
- /**
95
- * Fill in the Lakebase connection env AppKit's cache needs, so `CacheManager`
96
- * chooses PERSISTENT storage instead of memory.
97
- *
98
- * This is what makes the gate's session signing key and outstanding one-time
99
- * codes survive a restart. `applyLakebaseEnv` is the SHARED helper AppKit
100
- * auto-configuration uses, so the gate gets exactly the env a pool needs -
101
- * `LAKEBASE_ENDPOINT`, `PGHOST`, `PGDATABASE`, and `PGUSER` - rather than a
102
- * hand-rolled subset. All four matter: `createLakebasePool()` throws without any
103
- * one of them, and a Databricks App `postgres` resource binding supplies only the
104
- * first. Without them the pool cannot be built, the cache silently degrades to
105
- * in-memory, and every redeploy signs out every user (the exact symptom this
106
- * exists to prevent).
107
- *
108
- * It is called here rather than left to `createApp`'s `autoConfigure` because that
109
- * gates on a `lakebase()` plugin being registered - and this app registers none,
110
- * having no server to mount Lakebase routes on. Calling the helper directly also
111
- * lets the gate be stricter than `autoConfigure` is:
112
- *
113
- * - It runs ONLY when a Lakebase env var is present. A tunnel is a wrapper
114
- * around someone else's app and must not invent infrastructure, so with
115
- * nothing bound it skips rather than falling through the resolver's
116
- * list-or-CREATE-a-project path.
117
- * - `autoCreate: false` for the same reason, in case a project happens to exist.
118
- * - Every failure is a WARNING, never a throw. The cache is an optimization for
119
- * session durability; admission still requires a code delivered to an
120
- * allow-listed address, so a gate with a memory cache is safe, just forgetful.
121
- */
122
- export declare function resolveCacheStorageEnv(): Promise<boolean>;
123
- /**
124
- * Boot the gate app and return the API the proxy calls. Throws when email is not
125
- * in SMTP mode (no way to deliver a code) so a misconfigured gate fails fast at
126
- * startup rather than silently accepting nobody; the caller may catch this and
127
- * fall back to insecure/open mode when the operator opted in.
128
- */
17
+ import { type AuthGateApi, type AuthGateConfig } from "@dbx-tools/tunnel";
18
+ /** Boot the gate app and return the handlers the proxy authenticates against. */
129
19
  export declare function startGateApp(config: AuthGateConfig): Promise<AuthGateApi>;
130
- export {};