@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/README.md
CHANGED
|
@@ -1,364 +1 @@
|
|
|
1
|
-
#
|
|
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
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
14
|
-
*
|
|
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
|
|
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 {};
|