@dbx-tools/tunnel 0.6.60 → 0.6.61
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 +59 -56
- package/index.ts +6 -3
- package/lib/index.d.ts +6 -3
- package/lib/index.js +5 -3
- package/lib/src/{app.d.ts → code-email.d.ts} +10 -61
- package/lib/src/code-email.js +108 -0
- package/lib/src/gate.d.ts +60 -0
- package/lib/src/gate.js +196 -0
- package/lib/src/interceptor.d.ts +4 -4
- package/lib/src/interceptor.js +5 -5
- package/lib/src/plugin.d.ts +57 -3
- package/lib/src/plugin.js +77 -6
- package/lib/src/send-code.d.ts +28 -0
- package/lib/src/send-code.js +68 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +14 -7
- package/src/code-email.ts +118 -0
- package/src/gate.ts +241 -0
- package/src/interceptor.ts +4 -4
- package/src/plugin.ts +136 -6
- package/src/send-code.ts +112 -0
- package/lib/src/app.js +0 -258
- package/lib/src/proxy.d.ts +0 -49
- package/lib/src/proxy.js +0 -247
- package/src/app.ts +0 -292
- package/src/proxy.ts +0 -299
package/README.md
CHANGED
|
@@ -18,8 +18,8 @@ so the app and portr live and die as one (concurrently-style - signals pass
|
|
|
18
18
|
through, either death tears the pair down). The app is the process; the tunnel
|
|
19
19
|
rides along inside it. Access is granted per email address against an allow-list,
|
|
20
20
|
verified by a code sent over [`@dbx-tools/email`](../../node/email); the gate
|
|
21
|
-
itself is the `authGate` AppKit plugin
|
|
22
|
-
|
|
21
|
+
itself is the `authGate` AppKit plugin, which registers the login routes and a
|
|
22
|
+
gating middleware on the app's OWN Express server (no separate proxy process).
|
|
23
23
|
|
|
24
24
|
**Key features:**
|
|
25
25
|
|
|
@@ -83,18 +83,22 @@ exported here for an app that wants to gate the tunnelled traffic.
|
|
|
83
83
|
`x-forwarded-access-token`, which would otherwise let anyone drive the app's
|
|
84
84
|
workspace calls with a pasted token. Add an app's own headers with
|
|
85
85
|
`TUNNEL_FORWARD_HEADERS`.
|
|
86
|
-
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
- Only PORTR traffic is gated. The gate identifies tunnel requests by their
|
|
87
|
+
`Host` header (`TUNNEL_PUBLIC_DOMAIN`), which portr's client preserves from the
|
|
88
|
+
public visitor. Platform front-door traffic and any other local caller carry a
|
|
89
|
+
different `Host` and pass through UNGATED, so health checks and the workspace UI
|
|
90
|
+
keep working. (There is no portr-injected identifying header and no source-IP
|
|
91
|
+
signal - the client dials the app over plain loopback - so `Host` is the signal.)
|
|
90
92
|
- SPA-aware gating: static assets and the login routes stay open so the browser
|
|
91
93
|
can load the client and render the login form; every other `/api/*` needs a
|
|
92
|
-
valid session cookie or gets `401`. WebSocket
|
|
94
|
+
valid session cookie or gets `401`. A WebSocket handshake is an ordinary `GET`
|
|
95
|
+
and runs through the same gate.
|
|
93
96
|
- Supervised teardown - `tunnelInterceptor` binds portr through the `createApp`
|
|
94
97
|
interceptor context, so the app and the tunnel are tied together: if either
|
|
95
98
|
exits, `bindProcess` brings the whole set down and passes signals through.
|
|
96
99
|
- The gate fails fast when email is not configured for SMTP, because a gate that
|
|
97
|
-
cannot send codes locks everyone out
|
|
100
|
+
cannot send codes locks everyone out. `@dbx-tools/email` is an OPTIONAL peer
|
|
101
|
+
dependency, imported lazily; the app that mounts the gate provides it.
|
|
98
102
|
|
|
99
103
|
## Why This Over An Ad-Hoc Tunnel
|
|
100
104
|
|
|
@@ -109,24 +113,35 @@ supervise and no wrapper command to thread flags through.
|
|
|
109
113
|
|
|
110
114
|
```ts
|
|
111
115
|
import { createApp } from "@dbx-tools/appkit";
|
|
116
|
+
import { email } from "@dbx-tools/email";
|
|
112
117
|
import { server } from "@databricks/appkit";
|
|
113
|
-
import { interceptor } from "@dbx-tools/tunnel";
|
|
118
|
+
import { interceptor, plugin } from "@dbx-tools/tunnel";
|
|
114
119
|
|
|
115
120
|
const { tunnelInterceptor } = interceptor;
|
|
121
|
+
const { authGate } = plugin;
|
|
116
122
|
|
|
117
123
|
await createApp.createApp({
|
|
118
|
-
plugins: [
|
|
124
|
+
plugins: [
|
|
125
|
+
server({ host, staticPath }),
|
|
126
|
+
// Delivers the OTP codes; the gate reuses this shared transport.
|
|
127
|
+
email(),
|
|
128
|
+
// The OTP gate: login routes + a gating middleware on THIS server. Gates only
|
|
129
|
+
// portr traffic (Host === TUNNEL_PUBLIC_DOMAIN); the platform front door passes
|
|
130
|
+
// through. Inert when no tunnel domain is configured.
|
|
131
|
+
authGate({}),
|
|
132
|
+
],
|
|
119
133
|
// Applies DATABRICKS_HOST, launches portr at the app's public port, and binds
|
|
120
134
|
// it to the app. No-op when no PORTR_TOKEN / TUNNEL_PUBLIC_DOMAIN is set.
|
|
121
135
|
interceptor: tunnelInterceptor(),
|
|
122
136
|
});
|
|
123
137
|
```
|
|
124
138
|
|
|
125
|
-
`tunnelInterceptor(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
`
|
|
139
|
+
Two pieces, one process: `tunnelInterceptor()` runs portr (a child, bound to the
|
|
140
|
+
app), and `authGate()` is the in-app gate. `tunnelInterceptor(opts)` takes optional
|
|
141
|
+
`publicDomain` / `subdomain` / `port`, each falling back to env (and `port` to the
|
|
142
|
+
`DATABRICKS_APP_PORT` contract); `authGate` reads `TUNNEL_AUTH_ALLOW` /
|
|
143
|
+
`TUNNEL_PUBLIC_DOMAIN` from the environment, so a deployment usually passes nothing.
|
|
144
|
+
See [Use The Gate](#use-the-gate) for the gate on its own.
|
|
130
145
|
|
|
131
146
|
## Options
|
|
132
147
|
|
|
@@ -274,7 +289,7 @@ configuration:
|
|
|
274
289
|
| `x-requested-with` | the conventional AJAX marker |
|
|
275
290
|
|
|
276
291
|
Add an app's own headers with `TUNNEL_FORWARD_HEADERS` (or `forwardHeaders` on
|
|
277
|
-
`
|
|
292
|
+
`authGate`). Each entry is a literal name, a shell-style glob, or a `/regex/` -
|
|
278
293
|
the same three shapes the email allow-list takes - and the configured list is
|
|
279
294
|
UNIONED with the defaults, so extending it never silently breaks the built-in
|
|
280
295
|
surfaces:
|
|
@@ -299,63 +314,52 @@ The identity headers are AppKit's OBO contract; the rest are the
|
|
|
299
314
|
[`X-Forwarded-*` set Databricks Apps documents passing to an
|
|
300
315
|
app](https://docs.databricks.com/aws/en/dev-tools/databricks-apps/http-headers),
|
|
301
316
|
plus the conventional `x-forwarded-proto`/`-port`/`x-real-ip` a library may read
|
|
302
|
-
anyway. Dropping the transport headers costs nothing:
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
317
|
+
anyway. Dropping the transport headers costs nothing: `xfwd` re-adds them from the
|
|
318
|
+
real socket after the policy runs, so the app sees the honest values instead of the
|
|
319
|
+
caller's claim. Rate limiting reads the client IP before stripping, and takes the
|
|
320
|
+
**rightmost** `x-forwarded-for` entry - the only one portr appended rather than a
|
|
321
|
+
client supplied.
|
|
307
322
|
|
|
308
323
|
## Use The Gate
|
|
309
324
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
325
|
+
`authGate()` is the gate. Register it in your app's `createApp` plugins and it
|
|
326
|
+
mounts the `/api/email/auth/*` login routes plus a gating middleware on your app's
|
|
327
|
+
own server (via AppKit's `this.context`). By default it delivers codes through the
|
|
328
|
+
app's shared `@dbx-tools/email` transport - so the minimal wiring is just an
|
|
329
|
+
`allow` list (or `TUNNEL_AUTH_ALLOW`):
|
|
315
330
|
|
|
316
331
|
```ts
|
|
317
332
|
import { createApp } from "@dbx-tools/appkit";
|
|
333
|
+
import { email } from "@dbx-tools/email";
|
|
318
334
|
import { authGate } from "@dbx-tools/tunnel";
|
|
319
|
-
import { brand, email, sender, transport } from "@dbx-tools/email";
|
|
320
335
|
|
|
321
|
-
|
|
336
|
+
await createApp.createApp({
|
|
322
337
|
plugins: [
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
sendCode: async (to, code, opts) => {
|
|
327
|
-
const runtime = transport.getEmailRuntime();
|
|
328
|
-
await transport.sendEmail(
|
|
329
|
-
{
|
|
330
|
-
to: [to],
|
|
331
|
-
subject: opts.subject,
|
|
332
|
-
body: `${opts.message}\n\n## ${code}`,
|
|
333
|
-
},
|
|
334
|
-
// The app's configured sender; a code email has no on-behalf-of user.
|
|
335
|
-
sender.resolveSenderAddress(runtime.config, undefined),
|
|
336
|
-
);
|
|
337
|
-
},
|
|
338
|
-
}),
|
|
338
|
+
server({ host, staticPath }),
|
|
339
|
+
email(), // the gate reuses this transport to send codes
|
|
340
|
+
authGate({ allow: ["example.com"] }),
|
|
339
341
|
],
|
|
340
342
|
});
|
|
341
|
-
|
|
342
|
-
// The gate exposes handlers rather than routes - call them from your own server.
|
|
343
|
-
const status = await handle.authGate.status(sessionCookieValue);
|
|
344
343
|
```
|
|
345
344
|
|
|
346
|
-
`sendCode`
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
needs
|
|
345
|
+
Override `sendCode` only to deliver through something other than the shared
|
|
346
|
+
transport; the default builds the OTP email (code in the subject + preheader for
|
|
347
|
+
mobile autofill) and sends it as the system sender. `@dbx-tools/email` is an
|
|
348
|
+
OPTIONAL peer dependency - a tunnel without the gate needs no mail - so the app
|
|
349
|
+
that mounts `authGate` must include it (or run `TUNNEL_INSECURE=true` to skip the
|
|
350
|
+
gate); a missing transport fails fast at boot.
|
|
350
351
|
|
|
351
352
|
## Modules
|
|
352
353
|
|
|
353
354
|
- `interceptor` - `tunnelInterceptor()`, the `createApp` interceptor that applies
|
|
354
355
|
`DATABRICKS_HOST`, launches portr, and binds it to the app.
|
|
355
|
-
- `plugin` - `authGate()`, its config/env resolution,
|
|
356
|
-
|
|
357
|
-
- `
|
|
358
|
-
|
|
356
|
+
- `plugin` - `authGate()`, its config/env resolution, the `AuthGateApi` handlers,
|
|
357
|
+
and the `setup()` that mounts the gate on the app's server.
|
|
358
|
+
- `gate` - the login routes + gating middleware (`mountGate`, `isTunnelHost`):
|
|
359
|
+
`Host`-based tunnel classification, session enforcement, and identity injection.
|
|
360
|
+
- `send-code` - the default OTP delivery through the shared email transport
|
|
361
|
+
(lazily imported) and the SMTP fail-fast.
|
|
362
|
+
- `code-email` - pure builders for the code email's subject/preheader/bodies.
|
|
359
363
|
- `otp` - the `CacheManager`-backed code store and the session JWT.
|
|
360
364
|
- `signingKey` - the cache-persisted HS256 session key (30-day TTL, get/generate/
|
|
361
365
|
re-read convergence) and the `TUNNEL_AUTH_SESSION_CUTOFF` force-clear cutoff.
|
|
@@ -366,7 +370,6 @@ needs it by hand.
|
|
|
366
370
|
distributed).
|
|
367
371
|
- `portr` - portr install, config rendering, and child launch.
|
|
368
372
|
- `env` - the environment-variable names, each with its deprecated aliases.
|
|
369
|
-
- `app` - boots the minimal gate AppKit app and returns the `AuthGateApi`.
|
|
370
373
|
|
|
371
374
|
Browser-safe login wire schemas (the request/verify payloads and the session
|
|
372
375
|
cookie name) live in [`@dbx-tools/shared-email`](../../shared/email); the React
|
package/index.ts
CHANGED
|
@@ -3,17 +3,21 @@
|
|
|
3
3
|
// Hand edits are overwritten on the next watch; this file is read-only.
|
|
4
4
|
|
|
5
5
|
export * as allowlist from "./src/allowlist.ts";
|
|
6
|
-
export * as
|
|
6
|
+
export * as codeEmail from "./src/code-email.ts";
|
|
7
7
|
export * as env from "./src/env.ts";
|
|
8
|
+
export * as gate from "./src/gate.ts";
|
|
8
9
|
export * as headers from "./src/headers.ts";
|
|
9
10
|
export * as interceptor from "./src/interceptor.ts";
|
|
10
11
|
export * as otp from "./src/otp.ts";
|
|
11
12
|
export * as plugin from "./src/plugin.ts";
|
|
12
13
|
export * as portr from "./src/portr.ts";
|
|
13
|
-
export * as proxy from "./src/proxy.ts";
|
|
14
14
|
export * as rateLimit from "./src/rate-limit.ts";
|
|
15
|
+
export * as sendCode from "./src/send-code.ts";
|
|
15
16
|
export * as signingKey from "./src/signing-key.ts";
|
|
17
|
+
export type { CodeCopy } from "./src/code-email.ts";
|
|
16
18
|
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";
|
|
19
|
+
export { AUTH_PREFIX } from "./src/gate.ts";
|
|
20
|
+
export type { GateOptions } from "./src/gate.ts";
|
|
17
21
|
export { PROTECTED_HEADERS, DEFAULT_FORWARD_HEADERS } from "./src/headers.ts";
|
|
18
22
|
export type { HeaderPolicy } from "./src/headers.ts";
|
|
19
23
|
export type { TunnelInterceptorOptions } from "./src/interceptor.ts";
|
|
@@ -22,7 +26,6 @@ export type { VerifyOutcome } from "./src/otp.ts";
|
|
|
22
26
|
export { AuthGatePlugin, authGate } from "./src/plugin.ts";
|
|
23
27
|
export type { AuthGateConfig, SendCodeOptions, ResolvedAuthGateConfig, AuthGateApi } from "./src/plugin.ts";
|
|
24
28
|
export type { PortrConfig } from "./src/portr.ts";
|
|
25
|
-
export type { ProxyOptions } from "./src/proxy.ts";
|
|
26
29
|
export { RateLimiter } from "./src/rate-limit.ts";
|
|
27
30
|
export { KEY_TTL_SECONDS } from "./src/signing-key.ts";
|
|
28
31
|
export type { SigningKey } from "./src/signing-key.ts";
|
package/lib/index.d.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
export * as allowlist from "./src/allowlist.ts";
|
|
2
|
-
export * as
|
|
2
|
+
export * as codeEmail from "./src/code-email.ts";
|
|
3
3
|
export * as env from "./src/env.ts";
|
|
4
|
+
export * as gate from "./src/gate.ts";
|
|
4
5
|
export * as headers from "./src/headers.ts";
|
|
5
6
|
export * as interceptor from "./src/interceptor.ts";
|
|
6
7
|
export * as otp from "./src/otp.ts";
|
|
7
8
|
export * as plugin from "./src/plugin.ts";
|
|
8
9
|
export * as portr from "./src/portr.ts";
|
|
9
|
-
export * as proxy from "./src/proxy.ts";
|
|
10
10
|
export * as rateLimit from "./src/rate-limit.ts";
|
|
11
|
+
export * as sendCode from "./src/send-code.ts";
|
|
11
12
|
export * as signingKey from "./src/signing-key.ts";
|
|
13
|
+
export type { CodeCopy } from "./src/code-email.ts";
|
|
12
14
|
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";
|
|
15
|
+
export { AUTH_PREFIX } from "./src/gate.ts";
|
|
16
|
+
export type { GateOptions } from "./src/gate.ts";
|
|
13
17
|
export { PROTECTED_HEADERS, DEFAULT_FORWARD_HEADERS } from "./src/headers.ts";
|
|
14
18
|
export type { HeaderPolicy } from "./src/headers.ts";
|
|
15
19
|
export type { TunnelInterceptorOptions } from "./src/interceptor.ts";
|
|
@@ -18,7 +22,6 @@ export type { VerifyOutcome } from "./src/otp.ts";
|
|
|
18
22
|
export { AuthGatePlugin, authGate } from "./src/plugin.ts";
|
|
19
23
|
export type { AuthGateConfig, SendCodeOptions, ResolvedAuthGateConfig, AuthGateApi } from "./src/plugin.ts";
|
|
20
24
|
export type { PortrConfig } from "./src/portr.ts";
|
|
21
|
-
export type { ProxyOptions } from "./src/proxy.ts";
|
|
22
25
|
export { RateLimiter } from "./src/rate-limit.ts";
|
|
23
26
|
export { KEY_TTL_SECONDS } from "./src/signing-key.ts";
|
|
24
27
|
export type { SigningKey } from "./src/signing-key.ts";
|
package/lib/index.js
CHANGED
|
@@ -2,20 +2,22 @@
|
|
|
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
|
export * as allowlist from "./src/allowlist.js";
|
|
5
|
-
export * as
|
|
5
|
+
export * as codeEmail from "./src/code-email.js";
|
|
6
6
|
export * as env from "./src/env.js";
|
|
7
|
+
export * as gate from "./src/gate.js";
|
|
7
8
|
export * as headers from "./src/headers.js";
|
|
8
9
|
export * as interceptor from "./src/interceptor.js";
|
|
9
10
|
export * as otp from "./src/otp.js";
|
|
10
11
|
export * as plugin from "./src/plugin.js";
|
|
11
12
|
export * as portr from "./src/portr.js";
|
|
12
|
-
export * as proxy from "./src/proxy.js";
|
|
13
13
|
export * as rateLimit from "./src/rate-limit.js";
|
|
14
|
+
export * as sendCode from "./src/send-code.js";
|
|
14
15
|
export * as signingKey from "./src/signing-key.js";
|
|
15
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 { AUTH_PREFIX } from "./src/gate.js";
|
|
16
18
|
export { PROTECTED_HEADERS, DEFAULT_FORWARD_HEADERS } from "./src/headers.js";
|
|
17
19
|
export { CodeStore } from "./src/otp.js";
|
|
18
20
|
export { AuthGatePlugin, authGate } from "./src/plugin.js";
|
|
19
21
|
export { RateLimiter } from "./src/rate-limit.js";
|
|
20
22
|
export { KEY_TTL_SECONDS } from "./src/signing-key.js";
|
|
21
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
23
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwyQ0FBMkM7QUFDM0MsbURBQW1EO0FBQ25ELHdFQUF3RTtBQUV4RSxPQUFPLEtBQUssU0FBUyxNQUFNLG9CQUFvQixDQUFDO0FBQ2hELE9BQU8sS0FBSyxTQUFTLE1BQU0scUJBQXFCLENBQUM7QUFDakQsT0FBTyxLQUFLLEdBQUcsTUFBTSxjQUFjLENBQUM7QUFDcEMsT0FBTyxLQUFLLElBQUksTUFBTSxlQUFlLENBQUM7QUFDdEMsT0FBTyxLQUFLLE9BQU8sTUFBTSxrQkFBa0IsQ0FBQztBQUM1QyxPQUFPLEtBQUssV0FBVyxNQUFNLHNCQUFzQixDQUFDO0FBQ3BELE9BQU8sS0FBSyxHQUFHLE1BQU0sY0FBYyxDQUFDO0FBQ3BDLE9BQU8sS0FBSyxNQUFNLE1BQU0saUJBQWlCLENBQUM7QUFDMUMsT0FBTyxLQUFLLEtBQUssTUFBTSxnQkFBZ0IsQ0FBQztBQUN4QyxPQUFPLEtBQUssU0FBUyxNQUFNLHFCQUFxQixDQUFDO0FBQ2pELE9BQU8sS0FBSyxRQUFRLE1BQU0sb0JBQW9CLENBQUM7QUFDL0MsT0FBTyxLQUFLLFVBQVUsTUFBTSxzQkFBc0IsQ0FBQztBQUVuRCxPQUFPLEVBQUUsU0FBUyxFQUFFLFdBQVcsRUFBRSxjQUFjLEVBQUUsV0FBVyxFQUFFLGVBQWUsRUFBRSxZQUFZLEVBQUUsY0FBYyxFQUFFLGtCQUFrQixFQUFFLGlCQUFpQixFQUFFLFlBQVksRUFBRSxtQkFBbUIsRUFBRSxNQUFNLGNBQWMsQ0FBQztBQUM1TSxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0sZUFBZSxDQUFDO0FBRTVDLE9BQU8sRUFBRSxpQkFBaUIsRUFBRSx1QkFBdUIsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBRzlFLE9BQU8sRUFBRSxTQUFTLEVBQUUsTUFBTSxjQUFjLENBQUM7QUFFekMsT0FBTyxFQUFFLGNBQWMsRUFBRSxRQUFRLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUczRCxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0scUJBQXFCLENBQUM7QUFDbEQsT0FBTyxFQUFFLGVBQWUsRUFBRSxNQUFNLHNCQUFzQixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLy8gR0VORVJBVEVEIGJ5IHByb2plbiB3YXRjaCAtIERPIE5PVCBFRElULlxuLy8gUmVnZW5lcmF0ZWQgZnJvbSB0aGUgZXhwb3J0aW5nIG1vZHVsZXMgaW4gLi9zcmMuXG4vLyBIYW5kIGVkaXRzIGFyZSBvdmVyd3JpdHRlbiBvbiB0aGUgbmV4dCB3YXRjaDsgdGhpcyBmaWxlIGlzIHJlYWQtb25seS5cblxuZXhwb3J0ICogYXMgYWxsb3dsaXN0IGZyb20gXCIuL3NyYy9hbGxvd2xpc3QudHNcIjtcbmV4cG9ydCAqIGFzIGNvZGVFbWFpbCBmcm9tIFwiLi9zcmMvY29kZS1lbWFpbC50c1wiO1xuZXhwb3J0ICogYXMgZW52IGZyb20gXCIuL3NyYy9lbnYudHNcIjtcbmV4cG9ydCAqIGFzIGdhdGUgZnJvbSBcIi4vc3JjL2dhdGUudHNcIjtcbmV4cG9ydCAqIGFzIGhlYWRlcnMgZnJvbSBcIi4vc3JjL2hlYWRlcnMudHNcIjtcbmV4cG9ydCAqIGFzIGludGVyY2VwdG9yIGZyb20gXCIuL3NyYy9pbnRlcmNlcHRvci50c1wiO1xuZXhwb3J0ICogYXMgb3RwIGZyb20gXCIuL3NyYy9vdHAudHNcIjtcbmV4cG9ydCAqIGFzIHBsdWdpbiBmcm9tIFwiLi9zcmMvcGx1Z2luLnRzXCI7XG5leHBvcnQgKiBhcyBwb3J0ciBmcm9tIFwiLi9zcmMvcG9ydHIudHNcIjtcbmV4cG9ydCAqIGFzIHJhdGVMaW1pdCBmcm9tIFwiLi9zcmMvcmF0ZS1saW1pdC50c1wiO1xuZXhwb3J0ICogYXMgc2VuZENvZGUgZnJvbSBcIi4vc3JjL3NlbmQtY29kZS50c1wiO1xuZXhwb3J0ICogYXMgc2lnbmluZ0tleSBmcm9tIFwiLi9zcmMvc2lnbmluZy1rZXkudHNcIjtcbmV4cG9ydCB0eXBlIHsgQ29kZUNvcHkgfSBmcm9tIFwiLi9zcmMvY29kZS1lbWFpbC50c1wiO1xuZXhwb3J0IHsgQUxMT1dfRU5WLCBTVUJKRUNUX0VOViwgQlJBTkRfTkFNRV9FTlYsIE1FU1NBR0VfRU5WLCBTRVNTSU9OX1RUTF9FTlYsIENPREVfVFRMX0VOViwgSldUX1NFQ1JFVF9FTlYsIFNFU1NJT05fQ1VUT0ZGX0VOViwgUFVCTElDX0RPTUFJTl9FTlYsIElOU0VDVVJFX0VOViwgRk9SV0FSRF9IRUFERVJTX0VOViB9IGZyb20gXCIuL3NyYy9lbnYudHNcIjtcbmV4cG9ydCB7IEFVVEhfUFJFRklYIH0gZnJvbSBcIi4vc3JjL2dhdGUudHNcIjtcbmV4cG9ydCB0eXBlIHsgR2F0ZU9wdGlvbnMgfSBmcm9tIFwiLi9zcmMvZ2F0ZS50c1wiO1xuZXhwb3J0IHsgUFJPVEVDVEVEX0hFQURFUlMsIERFRkFVTFRfRk9SV0FSRF9IRUFERVJTIH0gZnJvbSBcIi4vc3JjL2hlYWRlcnMudHNcIjtcbmV4cG9ydCB0eXBlIHsgSGVhZGVyUG9saWN5IH0gZnJvbSBcIi4vc3JjL2hlYWRlcnMudHNcIjtcbmV4cG9ydCB0eXBlIHsgVHVubmVsSW50ZXJjZXB0b3JPcHRpb25zIH0gZnJvbSBcIi4vc3JjL2ludGVyY2VwdG9yLnRzXCI7XG5leHBvcnQgeyBDb2RlU3RvcmUgfSBmcm9tIFwiLi9zcmMvb3RwLnRzXCI7XG5leHBvcnQgdHlwZSB7IFZlcmlmeU91dGNvbWUgfSBmcm9tIFwiLi9zcmMvb3RwLnRzXCI7XG5leHBvcnQgeyBBdXRoR2F0ZVBsdWdpbiwgYXV0aEdhdGUgfSBmcm9tIFwiLi9zcmMvcGx1Z2luLnRzXCI7XG5leHBvcnQgdHlwZSB7IEF1dGhHYXRlQ29uZmlnLCBTZW5kQ29kZU9wdGlvbnMsIFJlc29sdmVkQXV0aEdhdGVDb25maWcsIEF1dGhHYXRlQXBpIH0gZnJvbSBcIi4vc3JjL3BsdWdpbi50c1wiO1xuZXhwb3J0IHR5cGUgeyBQb3J0ckNvbmZpZyB9IGZyb20gXCIuL3NyYy9wb3J0ci50c1wiO1xuZXhwb3J0IHsgUmF0ZUxpbWl0ZXIgfSBmcm9tIFwiLi9zcmMvcmF0ZS1saW1pdC50c1wiO1xuZXhwb3J0IHsgS0VZX1RUTF9TRUNPTkRTIH0gZnJvbSBcIi4vc3JjL3NpZ25pbmcta2V5LnRzXCI7XG5leHBvcnQgdHlwZSB7IFNpZ25pbmdLZXkgfSBmcm9tIFwiLi9zcmMvc2lnbmluZy1rZXkudHNcIjtcbiJdfQ==
|
|
@@ -1,30 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Pure builders for the OTP code email's copy - subject, preheader, and the HTML
|
|
3
|
+
* and text bodies. No transport, no `@dbx-tools/email` import, so this module is
|
|
4
|
+
* safe to load whether or not the optional email dependency is present; the actual
|
|
5
|
+
* send lives in `./send-code`.
|
|
3
6
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* which the OTP `CodeStore` and the session signing key use for storage + TTL
|
|
8
|
-
* eviction;
|
|
9
|
-
* - the `email()` transport (SMTP / outbox), primed from env, so `sendCode` can
|
|
10
|
-
* deliver the one-time code;
|
|
11
|
-
* - dbx-tools auto-configuration + branding via `@dbx-tools/appkit`.
|
|
12
|
-
*
|
|
13
|
-
* It returns the {@link AuthGateApi} the proxy drives. `sendCode` is wired here
|
|
14
|
-
* (the plugin can't resolve HOW to send on its own) to the email plugin's
|
|
15
|
-
* transport. A sign-in code is SYSTEM mail - no on-behalf-of user asked for it
|
|
16
|
-
* and no reply to it reaches anyone - so it sends from the email config's
|
|
17
|
-
* do-not-reply address (`no-reply@EMAIL_DOMAIN` unless EMAIL_SYSTEM_FROM names
|
|
18
|
-
* another).
|
|
19
|
-
*
|
|
20
|
-
* FAIL FAST: a gate that can't email a code is useless, so if email does not
|
|
21
|
-
* resolve to SMTP mode (real delivery), this throws - unless `insecure` is set
|
|
22
|
-
* (`--insecure` / `TUNNEL_INSECURE=true`), in which case the caller runs the
|
|
23
|
-
* tunnel OPEN with no gate.
|
|
7
|
+
* The shapes here are load-bearing for mobile autofill: the code rides in the
|
|
8
|
+
* SUBJECT and the PREHEADER (the whole of a push notification), and the text part
|
|
9
|
+
* keeps the prompt and code on ONE line. See each function for why.
|
|
24
10
|
*
|
|
25
11
|
* @module
|
|
26
12
|
*/
|
|
27
|
-
import
|
|
13
|
+
import type { SendCodeOptions } from "./plugin.ts";
|
|
14
|
+
/** The parts of {@link SendCodeOptions} the code email's copy is built from. */
|
|
15
|
+
export type CodeCopy = Pick<SendCodeOptions, "message" | "codeTtlSeconds">;
|
|
28
16
|
/**
|
|
29
17
|
* A code TTL as the plain phrase the email states ("10 minutes", "45 seconds").
|
|
30
18
|
*
|
|
@@ -33,8 +21,6 @@ import { type AuthGateApi, type AuthGateConfig, type SendCodeOptions } from "./p
|
|
|
33
21
|
* never told the code lives longer than it does.
|
|
34
22
|
*/
|
|
35
23
|
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
24
|
/**
|
|
39
25
|
* The HTML part's source: the full branded template, with the code as a large
|
|
40
26
|
* styled heading (`## ` is what makes it prominent in an inbox).
|
|
@@ -91,40 +77,3 @@ export declare function codeEmailSubject(code: string, subject: string): string;
|
|
|
91
77
|
* it recognizes.
|
|
92
78
|
*/
|
|
93
79
|
export declare function codeEmailPreview(code: string, opts: CodeCopy): string;
|
|
94
|
-
/**
|
|
95
|
-
* Fill in the Lakebase connection env AppKit's cache needs, so `CacheManager`
|
|
96
|
-
* chooses PERSISTENT storage instead of memory.
|
|
97
|
-
*
|
|
98
|
-
* This is what makes the gate's session signing key and outstanding one-time
|
|
99
|
-
* codes survive a restart. `applyLakebaseEnv` is the SHARED helper AppKit
|
|
100
|
-
* auto-configuration uses, so the gate gets exactly the env a pool needs -
|
|
101
|
-
* `LAKEBASE_ENDPOINT`, `PGHOST`, `PGDATABASE`, and `PGUSER` - rather than a
|
|
102
|
-
* hand-rolled subset. All four matter: `createLakebasePool()` throws without any
|
|
103
|
-
* one of them, and a Databricks App `postgres` resource binding supplies only the
|
|
104
|
-
* first. Without them the pool cannot be built, the cache silently degrades to
|
|
105
|
-
* in-memory, and every redeploy signs out every user (the exact symptom this
|
|
106
|
-
* exists to prevent).
|
|
107
|
-
*
|
|
108
|
-
* It is called here rather than left to `createApp`'s `autoConfigure` because that
|
|
109
|
-
* gates on a `lakebase()` plugin being registered - and this app registers none,
|
|
110
|
-
* having no server to mount Lakebase routes on. Calling the helper directly also
|
|
111
|
-
* lets the gate be stricter than `autoConfigure` is:
|
|
112
|
-
*
|
|
113
|
-
* - It runs ONLY when a Lakebase env var is present. A tunnel is a wrapper
|
|
114
|
-
* around someone else's app and must not invent infrastructure, so with
|
|
115
|
-
* nothing bound it skips rather than falling through the resolver's
|
|
116
|
-
* list-or-CREATE-a-project path.
|
|
117
|
-
* - `autoCreate: false` for the same reason, in case a project happens to exist.
|
|
118
|
-
* - Every failure is a WARNING, never a throw. The cache is an optimization for
|
|
119
|
-
* session durability; admission still requires a code delivered to an
|
|
120
|
-
* allow-listed address, so a gate with a memory cache is safe, just forgetful.
|
|
121
|
-
*/
|
|
122
|
-
export declare function resolveCacheStorageEnv(): Promise<boolean>;
|
|
123
|
-
/**
|
|
124
|
-
* Boot the gate app and return the API the proxy calls. Throws when email is not
|
|
125
|
-
* in SMTP mode (no way to deliver a code) so a misconfigured gate fails fast at
|
|
126
|
-
* startup rather than silently accepting nobody; the caller may catch this and
|
|
127
|
-
* fall back to insecure/open mode when the operator opted in.
|
|
128
|
-
*/
|
|
129
|
-
export declare function startGateApp(config: AuthGateConfig): Promise<AuthGateApi>;
|
|
130
|
-
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure builders for the OTP code email's copy - subject, preheader, and the HTML
|
|
3
|
+
* and text bodies. No transport, no `@dbx-tools/email` import, so this module is
|
|
4
|
+
* safe to load whether or not the optional email dependency is present; the actual
|
|
5
|
+
* send lives in `./send-code`.
|
|
6
|
+
*
|
|
7
|
+
* The shapes here are load-bearing for mobile autofill: the code rides in the
|
|
8
|
+
* SUBJECT and the PREHEADER (the whole of a push notification), and the text part
|
|
9
|
+
* keeps the prompt and code on ONE line. See each function for why.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
import { string } from "@dbx-tools/shared-core";
|
|
14
|
+
/** The reassurance line closing both parts. */
|
|
15
|
+
const IGNORE_LINE = "If you did not request this code, you can ignore this email.";
|
|
16
|
+
/**
|
|
17
|
+
* A code TTL as the plain phrase the email states ("10 minutes", "45 seconds").
|
|
18
|
+
*
|
|
19
|
+
* Whole minutes read as minutes; anything else stays in seconds rather than
|
|
20
|
+
* rounding, so a 90-second TTL is not advertised as "1 minute" and a recipient is
|
|
21
|
+
* never told the code lives longer than it does.
|
|
22
|
+
*/
|
|
23
|
+
export function expiresIn(seconds) {
|
|
24
|
+
return seconds >= 60 && seconds % 60 === 0
|
|
25
|
+
? string.pluralize(seconds / 60, "minute")
|
|
26
|
+
: string.pluralize(seconds, "second");
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The HTML part's source: the full branded template, with the code as a large
|
|
30
|
+
* styled heading (`## ` is what makes it prominent in an inbox).
|
|
31
|
+
*/
|
|
32
|
+
export function codeEmailHtmlBody(code, opts) {
|
|
33
|
+
return [
|
|
34
|
+
opts.message,
|
|
35
|
+
"",
|
|
36
|
+
`## ${code}`,
|
|
37
|
+
"",
|
|
38
|
+
`This code expires in ${expiresIn(opts.codeTtlSeconds)}.`,
|
|
39
|
+
"",
|
|
40
|
+
IGNORE_LINE,
|
|
41
|
+
].join("\n");
|
|
42
|
+
}
|
|
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 function codeEmailTextBody(code, opts) {
|
|
63
|
+
return [
|
|
64
|
+
`${opts.message} ${code}`,
|
|
65
|
+
`This code expires in ${expiresIn(opts.codeTtlSeconds)}.`,
|
|
66
|
+
"",
|
|
67
|
+
IGNORE_LINE,
|
|
68
|
+
].join("\n");
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The SUBJECT line, with the code in it: `"123456 is your verification code"`.
|
|
72
|
+
*
|
|
73
|
+
* The code has to be here, not only in the body, because of what mobile autofill
|
|
74
|
+
* actually reads. iOS offers a code from an incoming NOTIFICATION - natively for
|
|
75
|
+
* Messages and Mail, and since iOS 26 for any app's notification text, which is
|
|
76
|
+
* what finally made Gmail work - and a notification contains the sender, the
|
|
77
|
+
* subject, and a short snippet. Nothing else. A code that lives in the body is
|
|
78
|
+
* invisible to it, however cleanly the body is formatted, which is why a perfectly
|
|
79
|
+
* shaped `text/plain` part still produced no autofill prompt in Gmail.
|
|
80
|
+
*
|
|
81
|
+
* `<code> is your <thing>` rather than `<thing>: <code>` because the leading code
|
|
82
|
+
* survives TRUNCATION: a notification and an inbox list both cut the subject, and
|
|
83
|
+
* the platform heuristics want the code in the same sentence as a recognized
|
|
84
|
+
* prompt ("code", "verification"). Putting it first keeps both intact no matter
|
|
85
|
+
* where the cut lands.
|
|
86
|
+
*
|
|
87
|
+
* `subject` is the configured line ("Your verification code"), lower-cased at its
|
|
88
|
+
* first word so the sentence reads naturally, and left ALONE when it does not look
|
|
89
|
+
* like the conventional phrasing - an operator who set a deliberate subject gets
|
|
90
|
+
* theirs with the code prefixed, not a mangled hybrid.
|
|
91
|
+
*/
|
|
92
|
+
export function codeEmailSubject(code, subject) {
|
|
93
|
+
const trimmed = subject.trim();
|
|
94
|
+
const conventional = /^your\s+/i.exec(trimmed);
|
|
95
|
+
const rest = conventional ? trimmed.slice(conventional[0].length) : trimmed;
|
|
96
|
+
return conventional ? `${code} is your ${rest}` : `${code} - ${trimmed}`;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The PREHEADER: the snippet beside the subject in an inbox list, and the body of
|
|
100
|
+
* the push notification. Carries the code for the same reason the subject does -
|
|
101
|
+
* it is the other half of what a notification shows - and repeats the prompt
|
|
102
|
+
* wording so a heuristic scanning the snippet alone finds a code next to a phrase
|
|
103
|
+
* it recognizes.
|
|
104
|
+
*/
|
|
105
|
+
export function codeEmailPreview(code, opts) {
|
|
106
|
+
return `${opts.message} ${code}`;
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29kZS1lbWFpbC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9jb2RlLWVtYWlsLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7Ozs7OztHQVdHO0FBRUgsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLHdCQUF3QixDQUFDO0FBTWhELCtDQUErQztBQUMvQyxNQUFNLFdBQVcsR0FBRyw4REFBOEQsQ0FBQztBQUVuRjs7Ozs7O0dBTUc7QUFDSCxNQUFNLFVBQVUsU0FBUyxDQUFDLE9BQWU7SUFDdkMsT0FBTyxPQUFPLElBQUksRUFBRSxJQUFJLE9BQU8sR0FBRyxFQUFFLEtBQUssQ0FBQztRQUN4QyxDQUFDLENBQUMsTUFBTSxDQUFDLFNBQVMsQ0FBQyxPQUFPLEdBQUcsRUFBRSxFQUFFLFFBQVEsQ0FBQztRQUMxQyxDQUFDLENBQUMsTUFBTSxDQUFDLFNBQVMsQ0FBQyxPQUFPLEVBQUUsUUFBUSxDQUFDLENBQUM7QUFDMUMsQ0FBQztBQUVEOzs7R0FHRztBQUNILE1BQU0sVUFBVSxpQkFBaUIsQ0FBQyxJQUFZLEVBQUUsSUFBYztJQUM1RCxPQUFPO1FBQ0wsSUFBSSxDQUFDLE9BQU87UUFDWixFQUFFO1FBQ0YsTUFBTSxJQUFJLEVBQUU7UUFDWixFQUFFO1FBQ0Ysd0JBQXdCLFNBQVMsQ0FBQyxJQUFJLENBQUMsY0FBYyxDQUFDLEdBQUc7UUFDekQsRUFBRTtRQUNGLFdBQVc7S0FDWixDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQztBQUNmLENBQUM7QUFFRDs7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBa0JHO0FBQ0gsTUFBTSxVQUFVLGlCQUFpQixDQUFDLElBQVksRUFBRSxJQUFjO0lBQzVELE9BQU87UUFDTCxHQUFHLElBQUksQ0FBQyxPQUFPLElBQUksSUFBSSxFQUFFO1FBQ3pCLHdCQUF3QixTQUFTLENBQUMsSUFBSSxDQUFDLGNBQWMsQ0FBQyxHQUFHO1FBQ3pELEVBQUU7UUFDRixXQUFXO0tBQ1osQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7QUFDZixDQUFDO0FBRUQ7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztHQXFCRztBQUNILE1BQU0sVUFBVSxnQkFBZ0IsQ0FBQyxJQUFZLEVBQUUsT0FBZTtJQUM1RCxNQUFNLE9BQU8sR0FBRyxPQUFPLENBQUMsSUFBSSxFQUFFLENBQUM7SUFDL0IsTUFBTSxZQUFZLEdBQUcsV0FBVyxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsQ0FBQztJQUMvQyxNQUFNLElBQUksR0FBRyxZQUFZLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUMsWUFBWSxDQUFDLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUM7SUFDNUUsT0FBTyxZQUFZLENBQUMsQ0FBQyxDQUFDLEdBQUcsSUFBSSxZQUFZLElBQUksRUFBRSxDQUFDLENBQUMsQ0FBQyxHQUFHLElBQUksTUFBTSxPQUFPLEVBQUUsQ0FBQztBQUMzRSxDQUFDO0FBRUQ7Ozs7OztHQU1HO0FBQ0gsTUFBTSxVQUFVLGdCQUFnQixDQUFDLElBQVksRUFBRSxJQUFjO0lBQzNELE9BQU8sR0FBRyxJQUFJLENBQUMsT0FBTyxJQUFJLElBQUksRUFBRSxDQUFDO0FBQ25DLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIFB1cmUgYnVpbGRlcnMgZm9yIHRoZSBPVFAgY29kZSBlbWFpbCdzIGNvcHkgLSBzdWJqZWN0LCBwcmVoZWFkZXIsIGFuZCB0aGUgSFRNTFxuICogYW5kIHRleHQgYm9kaWVzLiBObyB0cmFuc3BvcnQsIG5vIGBAZGJ4LXRvb2xzL2VtYWlsYCBpbXBvcnQsIHNvIHRoaXMgbW9kdWxlIGlzXG4gKiBzYWZlIHRvIGxvYWQgd2hldGhlciBvciBub3QgdGhlIG9wdGlvbmFsIGVtYWlsIGRlcGVuZGVuY3kgaXMgcHJlc2VudDsgdGhlIGFjdHVhbFxuICogc2VuZCBsaXZlcyBpbiBgLi9zZW5kLWNvZGVgLlxuICpcbiAqIFRoZSBzaGFwZXMgaGVyZSBhcmUgbG9hZC1iZWFyaW5nIGZvciBtb2JpbGUgYXV0b2ZpbGw6IHRoZSBjb2RlIHJpZGVzIGluIHRoZVxuICogU1VCSkVDVCBhbmQgdGhlIFBSRUhFQURFUiAodGhlIHdob2xlIG9mIGEgcHVzaCBub3RpZmljYXRpb24pLCBhbmQgdGhlIHRleHQgcGFydFxuICoga2VlcHMgdGhlIHByb21wdCBhbmQgY29kZSBvbiBPTkUgbGluZS4gU2VlIGVhY2ggZnVuY3Rpb24gZm9yIHdoeS5cbiAqXG4gKiBAbW9kdWxlXG4gKi9cblxuaW1wb3J0IHsgc3RyaW5nIH0gZnJvbSBcIkBkYngtdG9vbHMvc2hhcmVkLWNvcmVcIjtcbmltcG9ydCB0eXBlIHsgU2VuZENvZGVPcHRpb25zIH0gZnJvbSBcIi4vcGx1Z2luLnRzXCI7XG5cbi8qKiBUaGUgcGFydHMgb2Yge0BsaW5rIFNlbmRDb2RlT3B0aW9uc30gdGhlIGNvZGUgZW1haWwncyBjb3B5IGlzIGJ1aWx0IGZyb20uICovXG5leHBvcnQgdHlwZSBDb2RlQ29weSA9IFBpY2s8U2VuZENvZGVPcHRpb25zLCBcIm1lc3NhZ2VcIiB8IFwiY29kZVR0bFNlY29uZHNcIj47XG5cbi8qKiBUaGUgcmVhc3N1cmFuY2UgbGluZSBjbG9zaW5nIGJvdGggcGFydHMuICovXG5jb25zdCBJR05PUkVfTElORSA9IFwiSWYgeW91IGRpZCBub3QgcmVxdWVzdCB0aGlzIGNvZGUsIHlvdSBjYW4gaWdub3JlIHRoaXMgZW1haWwuXCI7XG5cbi8qKlxuICogQSBjb2RlIFRUTCBhcyB0aGUgcGxhaW4gcGhyYXNlIHRoZSBlbWFpbCBzdGF0ZXMgKFwiMTAgbWludXRlc1wiLCBcIjQ1IHNlY29uZHNcIikuXG4gKlxuICogV2hvbGUgbWludXRlcyByZWFkIGFzIG1pbnV0ZXM7IGFueXRoaW5nIGVsc2Ugc3RheXMgaW4gc2Vjb25kcyByYXRoZXIgdGhhblxuICogcm91bmRpbmcsIHNvIGEgOTAtc2Vjb25kIFRUTCBpcyBub3QgYWR2ZXJ0aXNlZCBhcyBcIjEgbWludXRlXCIgYW5kIGEgcmVjaXBpZW50IGlzXG4gKiBuZXZlciB0b2xkIHRoZSBjb2RlIGxpdmVzIGxvbmdlciB0aGFuIGl0IGRvZXMuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBleHBpcmVzSW4oc2Vjb25kczogbnVtYmVyKTogc3RyaW5nIHtcbiAgcmV0dXJuIHNlY29uZHMgPj0gNjAgJiYgc2Vjb25kcyAlIDYwID09PSAwXG4gICAgPyBzdHJpbmcucGx1cmFsaXplKHNlY29uZHMgLyA2MCwgXCJtaW51dGVcIilcbiAgICA6IHN0cmluZy5wbHVyYWxpemUoc2Vjb25kcywgXCJzZWNvbmRcIik7XG59XG5cbi8qKlxuICogVGhlIEhUTUwgcGFydCdzIHNvdXJjZTogdGhlIGZ1bGwgYnJhbmRlZCB0ZW1wbGF0ZSwgd2l0aCB0aGUgY29kZSBhcyBhIGxhcmdlXG4gKiBzdHlsZWQgaGVhZGluZyAoYCMjIGAgaXMgd2hhdCBtYWtlcyBpdCBwcm9taW5lbnQgaW4gYW4gaW5ib3gpLlxuICovXG5leHBvcnQgZnVuY3Rpb24gY29kZUVtYWlsSHRtbEJvZHkoY29kZTogc3RyaW5nLCBvcHRzOiBDb2RlQ29weSk6IHN0cmluZyB7XG4gIHJldHVybiBbXG4gICAgb3B0cy5tZXNzYWdlLFxuICAgIFwiXCIsXG4gICAgYCMjICR7Y29kZX1gLFxuICAgIFwiXCIsXG4gICAgYFRoaXMgY29kZSBleHBpcmVzIGluICR7ZXhwaXJlc0luKG9wdHMuY29kZVR0bFNlY29uZHMpfS5gLFxuICAgIFwiXCIsXG4gICAgSUdOT1JFX0xJTkUsXG4gIF0uam9pbihcIlxcblwiKTtcbn1cblxuLyoqXG4gKiBUaGUgYHRleHQvcGxhaW5gIHBhcnQsIHN1cHBsaWVkIEVYUExJQ0lUTFkgcmF0aGVyIHRoYW4gcmVuZGVyZWQgZnJvbSB0aGUgdHJlZVxuICogYWJvdmUuIEJvdGggcGFydHMgc2F5IHRoZSBzYW1lIHRoaW5nOyBvbmx5IHRoZSBsaW5lIGxheW91dCBkaWZmZXJzLlxuICpcbiAqIFRoZSBwcm9tcHQgYW5kIHRoZSBjb2RlIHNoYXJlIE9ORSBsaW5lIChcIllvdXIgdmVyaWZpY2F0aW9uIGNvZGUgaXM6IDEyMzQ1NlwiKS5cbiAqIFRoYXQgc2luZ2xlLWxpbmUgc2hhcGUgaXMgd2hhdCBpT1MsIEdtYWlsLCBPdXRsb29rLCBhbmQgQW5kcm9pZCBjb2RlIGRldGVjdGlvblxuICoga2V5cyBvbiBtb3N0IHJlbGlhYmx5IC0gdGhlIGhldXJpc3RpY3MgbG9vayBmb3IgYSBjb2RlIGluIHRoZSBzYW1lIHNlbnRlbmNlIGFzXG4gKiBhIHJlY29nbml6ZWQgcHJvbXB0LCBzbyBzcGxpdHRpbmcgdGhlbSBhY3Jvc3MgbGluZXMgbWFrZXMgZGV0ZWN0aW9uIGRlcGVuZGVudFxuICogb24gdGhlIGNsaWVudCwgYW5kIGFueSBibGFuayBsaW5lIGJldHdlZW4gdGhlbSBkZWZlYXRzIGl0IG91dHJpZ2h0LlxuICpcbiAqIFRoZSBHRU5FUkFURUQgdGV4dCBwYXJ0IGNhbm5vdCBob2xkIHRoYXQgc2hhcGUgYXQgYWxsOiBpdCBpcyBhIHJlbmRlcmluZyBvZiB0aGVcbiAqIEhUTUwsIHNvIGl0IGNhcnJpZXMgdGhlIGJyYW5kIGhlYWRlci9mb290ZXIgYW5kIHR1cm5zIHRoZSBjb2RlIGhlYWRpbmcncyBDU1NcbiAqIG1hcmdpbiBpbnRvIGJsYW5rIGxpbmVzLCBhcnJpdmluZyBhcyBgcHJvbXB0XFxuXFxuXFxuY29kZWAuXG4gKlxuICogVGhlIGNvZGUgaXMgdmlzaWJsZSB0ZXh0IGluIEJPVEggcGFydHMsIG5ldmVyIGFuIGltYWdlLCBzbyBhIGNsaWVudCBzY3JhcGluZ1xuICogZWl0aGVyIG9uZSBmaW5kcyBpdC4gTm8gdHJhaWxlciBsaW5lIGZvbGxvd3MgdGhlIGNvcHkgLSBBcHBsZSdzIGRvbWFpbi1ib3VuZFxuICogYEBkb21haW4gI2NvZGVgIGZvb3RlciBpcyBkZWxpYmVyYXRlbHkgTk9UIGVtaXR0ZWQsIHNpbmNlIGl0IGNvbnN0cmFpbnMgdGhlXG4gKiBjb2RlIHRvIG9uZSBvcmlnaW4gYW5kIGlzIG5vdCB3aGF0IHRoZSBicm9hZGx5LWNvbXBhdGlibGUgc2hhcGUgbmVlZHMuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBjb2RlRW1haWxUZXh0Qm9keShjb2RlOiBzdHJpbmcsIG9wdHM6IENvZGVDb3B5KTogc3RyaW5nIHtcbiAgcmV0dXJuIFtcbiAgICBgJHtvcHRzLm1lc3NhZ2V9ICR7Y29kZX1gLFxuICAgIGBUaGlzIGNvZGUgZXhwaXJlcyBpbiAke2V4cGlyZXNJbihvcHRzLmNvZGVUdGxTZWNvbmRzKX0uYCxcbiAgICBcIlwiLFxuICAgIElHTk9SRV9MSU5FLFxuICBdLmpvaW4oXCJcXG5cIik7XG59XG5cbi8qKlxuICogVGhlIFNVQkpFQ1QgbGluZSwgd2l0aCB0aGUgY29kZSBpbiBpdDogYFwiMTIzNDU2IGlzIHlvdXIgdmVyaWZpY2F0aW9uIGNvZGVcImAuXG4gKlxuICogVGhlIGNvZGUgaGFzIHRvIGJlIGhlcmUsIG5vdCBvbmx5IGluIHRoZSBib2R5LCBiZWNhdXNlIG9mIHdoYXQgbW9iaWxlIGF1dG9maWxsXG4gKiBhY3R1YWxseSByZWFkcy4gaU9TIG9mZmVycyBhIGNvZGUgZnJvbSBhbiBpbmNvbWluZyBOT1RJRklDQVRJT04gLSBuYXRpdmVseSBmb3JcbiAqIE1lc3NhZ2VzIGFuZCBNYWlsLCBhbmQgc2luY2UgaU9TIDI2IGZvciBhbnkgYXBwJ3Mgbm90aWZpY2F0aW9uIHRleHQsIHdoaWNoIGlzXG4gKiB3aGF0IGZpbmFsbHkgbWFkZSBHbWFpbCB3b3JrIC0gYW5kIGEgbm90aWZpY2F0aW9uIGNvbnRhaW5zIHRoZSBzZW5kZXIsIHRoZVxuICogc3ViamVjdCwgYW5kIGEgc2hvcnQgc25pcHBldC4gTm90aGluZyBlbHNlLiBBIGNvZGUgdGhhdCBsaXZlcyBpbiB0aGUgYm9keSBpc1xuICogaW52aXNpYmxlIHRvIGl0LCBob3dldmVyIGNsZWFubHkgdGhlIGJvZHkgaXMgZm9ybWF0dGVkLCB3aGljaCBpcyB3aHkgYSBwZXJmZWN0bHlcbiAqIHNoYXBlZCBgdGV4dC9wbGFpbmAgcGFydCBzdGlsbCBwcm9kdWNlZCBubyBhdXRvZmlsbCBwcm9tcHQgaW4gR21haWwuXG4gKlxuICogYDxjb2RlPiBpcyB5b3VyIDx0aGluZz5gIHJhdGhlciB0aGFuIGA8dGhpbmc+OiA8Y29kZT5gIGJlY2F1c2UgdGhlIGxlYWRpbmcgY29kZVxuICogc3Vydml2ZXMgVFJVTkNBVElPTjogYSBub3RpZmljYXRpb24gYW5kIGFuIGluYm94IGxpc3QgYm90aCBjdXQgdGhlIHN1YmplY3QsIGFuZFxuICogdGhlIHBsYXRmb3JtIGhldXJpc3RpY3Mgd2FudCB0aGUgY29kZSBpbiB0aGUgc2FtZSBzZW50ZW5jZSBhcyBhIHJlY29nbml6ZWRcbiAqIHByb21wdCAoXCJjb2RlXCIsIFwidmVyaWZpY2F0aW9uXCIpLiBQdXR0aW5nIGl0IGZpcnN0IGtlZXBzIGJvdGggaW50YWN0IG5vIG1hdHRlclxuICogd2hlcmUgdGhlIGN1dCBsYW5kcy5cbiAqXG4gKiBgc3ViamVjdGAgaXMgdGhlIGNvbmZpZ3VyZWQgbGluZSAoXCJZb3VyIHZlcmlmaWNhdGlvbiBjb2RlXCIpLCBsb3dlci1jYXNlZCBhdCBpdHNcbiAqIGZpcnN0IHdvcmQgc28gdGhlIHNlbnRlbmNlIHJlYWRzIG5hdHVyYWxseSwgYW5kIGxlZnQgQUxPTkUgd2hlbiBpdCBkb2VzIG5vdCBsb29rXG4gKiBsaWtlIHRoZSBjb252ZW50aW9uYWwgcGhyYXNpbmcgLSBhbiBvcGVyYXRvciB3aG8gc2V0IGEgZGVsaWJlcmF0ZSBzdWJqZWN0IGdldHNcbiAqIHRoZWlycyB3aXRoIHRoZSBjb2RlIHByZWZpeGVkLCBub3QgYSBtYW5nbGVkIGh5YnJpZC5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGNvZGVFbWFpbFN1YmplY3QoY29kZTogc3RyaW5nLCBzdWJqZWN0OiBzdHJpbmcpOiBzdHJpbmcge1xuICBjb25zdCB0cmltbWVkID0gc3ViamVjdC50cmltKCk7XG4gIGNvbnN0IGNvbnZlbnRpb25hbCA9IC9eeW91clxccysvaS5leGVjKHRyaW1tZWQpO1xuICBjb25zdCByZXN0ID0gY29udmVudGlvbmFsID8gdHJpbW1lZC5zbGljZShjb252ZW50aW9uYWxbMF0ubGVuZ3RoKSA6IHRyaW1tZWQ7XG4gIHJldHVybiBjb252ZW50aW9uYWwgPyBgJHtjb2RlfSBpcyB5b3VyICR7cmVzdH1gIDogYCR7Y29kZX0gLSAke3RyaW1tZWR9YDtcbn1cblxuLyoqXG4gKiBUaGUgUFJFSEVBREVSOiB0aGUgc25pcHBldCBiZXNpZGUgdGhlIHN1YmplY3QgaW4gYW4gaW5ib3ggbGlzdCwgYW5kIHRoZSBib2R5IG9mXG4gKiB0aGUgcHVzaCBub3RpZmljYXRpb24uIENhcnJpZXMgdGhlIGNvZGUgZm9yIHRoZSBzYW1lIHJlYXNvbiB0aGUgc3ViamVjdCBkb2VzIC1cbiAqIGl0IGlzIHRoZSBvdGhlciBoYWxmIG9mIHdoYXQgYSBub3RpZmljYXRpb24gc2hvd3MgLSBhbmQgcmVwZWF0cyB0aGUgcHJvbXB0XG4gKiB3b3JkaW5nIHNvIGEgaGV1cmlzdGljIHNjYW5uaW5nIHRoZSBzbmlwcGV0IGFsb25lIGZpbmRzIGEgY29kZSBuZXh0IHRvIGEgcGhyYXNlXG4gKiBpdCByZWNvZ25pemVzLlxuICovXG5leHBvcnQgZnVuY3Rpb24gY29kZUVtYWlsUHJldmlldyhjb2RlOiBzdHJpbmcsIG9wdHM6IENvZGVDb3B5KTogc3RyaW5nIHtcbiAgcmV0dXJuIGAke29wdHMubWVzc2FnZX0gJHtjb2RlfWA7XG59XG4iXX0=
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tunnel's in-app AUTH GATE - Express middleware + login routes the
|
|
3
|
+
* {@link AuthGatePlugin} registers on the app's OWN server via `this.context`.
|
|
4
|
+
*
|
|
5
|
+
* This replaces the old standalone reverse-proxy (`proxy.ts`): the app is now the
|
|
6
|
+
* process, so there is nothing to forward to. Instead the gate is middleware that
|
|
7
|
+
* either short-circuits (401, or answers the open login routes) or calls `next()`
|
|
8
|
+
* to let the app's real handlers run. It is the "stands in for AppKit auth" path:
|
|
9
|
+
* a portr caller proves an email via OTP, and on success the gate injects the
|
|
10
|
+
* identity headers AppKit reads, so a gated request runs like a front-door one.
|
|
11
|
+
*
|
|
12
|
+
* WHICH TRAFFIC IS GATED - the `Host` header, not the socket. portr's client
|
|
13
|
+
* forwards with Go's `httputil.NewSingleHostReverseProxy` and a `Director` that
|
|
14
|
+
* PRESERVES the original `Host`, so tunnel requests arrive with
|
|
15
|
+
* `Host: <subdomain>.<server>` (the public domain). A local process hitting the
|
|
16
|
+
* app directly sends `Host: 127.0.0.1:<port>` / `localhost`. So ONLY requests whose
|
|
17
|
+
* `Host` matches the configured public domain are gated; the platform front door
|
|
18
|
+
* and any other local client pass through untouched. There is no portr-injected
|
|
19
|
+
* identifying header and no TCP/source-IP signal to use instead (the client dials
|
|
20
|
+
* the target over plain loopback).
|
|
21
|
+
*
|
|
22
|
+
* @module
|
|
23
|
+
*/
|
|
24
|
+
import type { Request, RequestHandler } from "express";
|
|
25
|
+
import type { AuthGateApi } from "./plugin.ts";
|
|
26
|
+
/** Route prefix the login flow lives under (open, answered in-process). */
|
|
27
|
+
export declare const AUTH_PREFIX = "/api/email/auth";
|
|
28
|
+
/** Options for {@link mountGate}. */
|
|
29
|
+
export interface GateOptions {
|
|
30
|
+
/** The in-process gate API (session/request/verify/status). */
|
|
31
|
+
gate: AuthGateApi;
|
|
32
|
+
/**
|
|
33
|
+
* The public `<subdomain>.<server>` that identifies portr traffic by `Host`.
|
|
34
|
+
* When absent, no request is ever classified as tunnel traffic and the gate is
|
|
35
|
+
* inert (everything passes through) - a tunnel with no public domain gates
|
|
36
|
+
* nothing.
|
|
37
|
+
*/
|
|
38
|
+
publicDomain?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Extra `x-` request headers tunnel traffic may forward (unioned with the
|
|
41
|
+
* built-in allow-list). Every other `x-` header is stripped from tunnel traffic.
|
|
42
|
+
*/
|
|
43
|
+
forwardHeaders?: readonly string[];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* True when the request's `Host` is the tunnel's public domain - i.e. it came in
|
|
47
|
+
* over portr. Case-insensitive; the optional `:port` is ignored. When no public
|
|
48
|
+
* domain is configured, nothing is tunnel traffic.
|
|
49
|
+
*/
|
|
50
|
+
export declare function isTunnelHost(req: Request, publicDomain: string | undefined): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Register the login routes and the gating middleware on the app's Express
|
|
53
|
+
* instance. Called from {@link AuthGatePlugin} with the router AppKit hands
|
|
54
|
+
* `injectRoutes`, plus `this.context.addMiddleware` for the global gate.
|
|
55
|
+
*
|
|
56
|
+
* `addRoute`/`addMiddleware` are used (via the passed callbacks) because the login
|
|
57
|
+
* routes live at an ABSOLUTE path (`/api/email/auth/*`, the client's contract),
|
|
58
|
+
* not under the plugin's `/api/authGate` base.
|
|
59
|
+
*/
|
|
60
|
+
export declare function mountGate(opts: GateOptions, addRoute: (method: "get" | "post", path: string, handler: RequestHandler) => void, addMiddleware: (path: string, handler: RequestHandler) => void): void;
|