@dbx-tools/tunnel 0.6.60 → 0.6.62

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 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 plus the `startProxy` reverse-proxy, both
22
- exported here for an app that wants to gate the tunnelled traffic.
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
 
@@ -27,11 +27,9 @@ exported here for an app that wants to gate the tunnelled traffic.
27
27
  no separate process: `createApp({ interceptor: tunnelInterceptor() })`. A no-op
28
28
  when no `PORTR_TOKEN` / `TUNNEL_PUBLIC_DOMAIN` is set, so it is safe to register
29
29
  unconditionally.
30
- - Branded from the repo-wide brand context: the code email's accent colour, font,
31
- logo, and display name come from the app's own `branding/brand.yaml` (via
32
- `@dbx-tools/core`'s `loadBrandContext()`), falling back to the dbx-tools
33
- default - so the sign-in email looks like the app it fronts with nothing to
34
- configure. `TUNNEL_AUTH_BRAND_NAME` overrides just the name.
30
+ - Uses the shared email-template brand for the code email's color, font, and
31
+ logo. The displayed app name defaults to the dbx-tools brand and can be set
32
+ with `brandName` or `TUNNEL_AUTH_BRAND_NAME`.
35
33
  - Conventional one-time-code copy, so platform autofill works: the email is
36
34
  `Your verification code is: / <code> / This code expires in N minutes`, and the
37
35
  code input carries `autocomplete="one-time-code"`. iOS, Gmail, Outlook, and
@@ -83,18 +81,22 @@ exported here for an app that wants to gate the tunnelled traffic.
83
81
  `x-forwarded-access-token`, which would otherwise let anyone drive the app's
84
82
  workspace calls with a pasted token. Add an app's own headers with
85
83
  `TUNNEL_FORWARD_HEADERS`.
86
- - Platform traffic passes through UNGATED. The gate distinguishes the portr
87
- client (a loopback source address, same container) from the hosting platform's
88
- front door (a non-loopback container-network address), so health checks and the
89
- workspace UI keep working while public tunnel traffic is gated.
84
+ - Only PORTR traffic is gated. The gate identifies tunnel requests by their
85
+ `Host` header (`TUNNEL_PUBLIC_DOMAIN`), which portr's client preserves from the
86
+ public visitor. Platform front-door traffic and any other local caller carry a
87
+ different `Host` and pass through UNGATED, so health checks and the workspace UI
88
+ keep working. (There is no portr-injected identifying header and no source-IP
89
+ signal - the client dials the app over plain loopback - so `Host` is the signal.)
90
90
  - SPA-aware gating: static assets and the login routes stay open so the browser
91
91
  can load the client and render the login form; every other `/api/*` needs a
92
- valid session cookie or gets `401`. WebSocket upgrades are gated the same way.
92
+ valid session cookie or gets `401`. A WebSocket handshake is an ordinary `GET`
93
+ and runs through the same gate.
93
94
  - Supervised teardown - `tunnelInterceptor` binds portr through the `createApp`
94
95
  interceptor context, so the app and the tunnel are tied together: if either
95
96
  exits, `bindProcess` brings the whole set down and passes signals through.
96
97
  - The gate fails fast when email is not configured for SMTP, because a gate that
97
- cannot send codes locks everyone out (see `startGateApp` in the `app` module).
98
+ cannot send codes locks everyone out. `@dbx-tools/email` is an OPTIONAL peer
99
+ dependency, imported lazily; the app that mounts the gate provides it.
98
100
 
99
101
  ## Why This Over An Ad-Hoc Tunnel
100
102
 
@@ -109,24 +111,35 @@ supervise and no wrapper command to thread flags through.
109
111
 
110
112
  ```ts
111
113
  import { createApp } from "@dbx-tools/appkit";
114
+ import { email } from "@dbx-tools/email";
112
115
  import { server } from "@databricks/appkit";
113
- import { interceptor } from "@dbx-tools/tunnel";
116
+ import { interceptor, plugin } from "@dbx-tools/tunnel";
114
117
 
115
118
  const { tunnelInterceptor } = interceptor;
119
+ const { authGate } = plugin;
116
120
 
117
121
  await createApp.createApp({
118
- plugins: [server({ host, staticPath })],
122
+ plugins: [
123
+ server({ host, staticPath }),
124
+ // Delivers the OTP codes; the gate reuses this shared transport.
125
+ email(),
126
+ // The OTP gate: login routes + a gating middleware on THIS server. Gates only
127
+ // portr traffic (Host === TUNNEL_PUBLIC_DOMAIN); the platform front door passes
128
+ // through. Inert when no tunnel domain is configured.
129
+ authGate({}),
130
+ ],
119
131
  // Applies DATABRICKS_HOST, launches portr at the app's public port, and binds
120
132
  // it to the app. No-op when no PORTR_TOKEN / TUNNEL_PUBLIC_DOMAIN is set.
121
133
  interceptor: tunnelInterceptor(),
122
134
  });
123
135
  ```
124
136
 
125
- `tunnelInterceptor(opts)` takes optional `publicDomain` / `subdomain` / `port`;
126
- each falls back to env (and `port` to the `DATABRICKS_APP_PORT` contract), so a
127
- deployment usually passes nothing and configures through the environment. The
128
- OTP gate is a separate concern - see [Use The Gate](#use-the-gate) to mount
129
- `authGate` + `startProxy` for gated traffic.
137
+ Two pieces, one process: `tunnelInterceptor()` runs portr (a child, bound to the
138
+ app), and `authGate()` is the in-app gate. `tunnelInterceptor(opts)` takes optional
139
+ `publicDomain` / `subdomain` / `port`, each falling back to env (and `port` to the
140
+ `DATABRICKS_APP_PORT` contract); `authGate` reads `TUNNEL_AUTH_ALLOW` /
141
+ `TUNNEL_PUBLIC_DOMAIN` from the environment, so a deployment usually passes nothing.
142
+ See [Use The Gate](#use-the-gate) for the gate on its own.
130
143
 
131
144
  ## Options
132
145
 
@@ -274,7 +287,7 @@ configuration:
274
287
  | `x-requested-with` | the conventional AJAX marker |
275
288
 
276
289
  Add an app's own headers with `TUNNEL_FORWARD_HEADERS` (or `forwardHeaders` on
277
- `startProxy`). Each entry is a literal name, a shell-style glob, or a `/regex/` -
290
+ `authGate`). Each entry is a literal name, a shell-style glob, or a `/regex/` -
278
291
  the same three shapes the email allow-list takes - and the configured list is
279
292
  UNIONED with the defaults, so extending it never silently breaks the built-in
280
293
  surfaces:
@@ -299,63 +312,59 @@ The identity headers are AppKit's OBO contract; the rest are the
299
312
  [`X-Forwarded-*` set Databricks Apps documents passing to an
300
313
  app](https://docs.databricks.com/aws/en/dev-tools/databricks-apps/http-headers),
301
314
  plus the conventional `x-forwarded-proto`/`-port`/`x-real-ip` a library may read
302
- anyway. Dropping the transport headers costs nothing: the proxy re-adds them from
303
- the real socket after the policy runs, so the app sees the honest values instead
304
- of the caller's claim. Rate limiting reads the client IP before stripping, and
305
- takes the **rightmost** `x-forwarded-for` entry - the only one a proxy appended
306
- rather than a client supplied.
315
+ anyway. Dropping the transport headers costs nothing: `xfwd` re-adds them from the
316
+ real socket after the policy runs, so the app sees the honest values instead of the
317
+ caller's claim. Rate limiting reads the client IP before stripping, and takes the
318
+ **rightmost** `x-forwarded-for` entry - the only one portr appended rather than a
319
+ client supplied.
307
320
 
308
321
  ## Use The Gate
309
322
 
310
- The gate is an AppKit plugin, so an app that wants the OTP flow can mount it
311
- directly - with or without the portr tunnel. It registers no routes - it exposes
312
- handlers the caller invokes (drive them from the `startProxy` reverse-proxy, or
313
- your own server) - and it takes a `sendCode` callback because delivering mail is
314
- the one thing it cannot resolve on its own:
323
+ `authGate()` is the gate. Register it in your app's `createApp` plugins and it
324
+ mounts the `/api/email/auth/*` login routes plus a gating middleware on your app's
325
+ own server (via AppKit's `this.context`). By default it delivers codes through the
326
+ app's shared `@dbx-tools/email` transport - so the minimal wiring is just an
327
+ `allow` list (or `TUNNEL_AUTH_ALLOW`):
315
328
 
316
329
  ```ts
317
330
  import { createApp } from "@dbx-tools/appkit";
331
+ import { email } from "@dbx-tools/email";
318
332
  import { authGate } from "@dbx-tools/tunnel";
319
- import { brand, email, sender, transport } from "@dbx-tools/email";
320
333
 
321
- const handle = await createApp({
334
+ await createApp.createApp({
322
335
  plugins: [
323
- email({ brand: brand.defaultEmailBrand }),
324
- authGate({
325
- allow: ["example.com"],
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
- }),
336
+ server({ host, staticPath }),
337
+ email(), // the gate reuses this transport to send codes
338
+ authGate({ allow: ["example.com"] }),
339
339
  ],
340
340
  });
341
-
342
- // The gate exposes handlers rather than routes - call them from your own server.
343
- const status = await handle.authGate.status(sessionCookieValue);
344
341
  ```
345
342
 
346
- `sendCode` is the one thing the plugin cannot resolve on its own. The `app`
347
- module's `startGateApp()` wires this same callback and derives the email styling
348
- from the brand context, so mounting the plugin directly is the only case that
349
- needs it by hand.
343
+ Override `sendCode` only to deliver through something other than the shared
344
+ transport; the default builds the OTP email (code in the subject + preheader for
345
+ mobile autofill) and sends it as the system sender. `@dbx-tools/email` is an
346
+ OPTIONAL peer dependency - a tunnel without the gate needs no mail - so the app
347
+ that mounts `authGate` must include it (or run `TUNNEL_INSECURE=true` to skip the
348
+ gate); a missing transport fails fast at boot.
349
+
350
+ `GET /api/email/auth/status` is what a client asks before deciding to render a
351
+ login screen, and it answers per REQUEST, not per deployment: on a non-tunnel
352
+ `Host` it returns `{ authenticated: false, enabled: false }` without looking at
353
+ the cookie, because that request was never going to be gated. So the same running
354
+ app shows the OTP screen to a portr visitor and no login at all to a browser on
355
+ `localhost` or the platform front door.
350
356
 
351
357
  ## Modules
352
358
 
353
359
  - `interceptor` - `tunnelInterceptor()`, the `createApp` interceptor that applies
354
360
  `DATABRICKS_HOST`, launches portr, and binds it to the app.
355
- - `plugin` - `authGate()`, its config/env resolution, and the `AuthGateApi`
356
- handlers the proxy calls in-process.
357
- - `proxy` - the public-port reverse proxy: loopback-vs-platform classification,
358
- open login routes, session enforcement, and WebSocket forwarding.
361
+ - `plugin` - `authGate()`, its config/env resolution, the `AuthGateApi` handlers,
362
+ and the `setup()` that mounts the gate on the app's server.
363
+ - `gate` - the login routes + gating middleware (`mountGate`, `isTunnelHost`):
364
+ `Host`-based tunnel classification, session enforcement, and identity injection.
365
+ - `send-code` - the default OTP delivery through the shared email transport
366
+ (lazily imported) and the SMTP fail-fast.
367
+ - `code-email` - pure builders for the code email's subject/preheader/bodies.
359
368
  - `otp` - the `CacheManager`-backed code store and the session JWT.
360
369
  - `signingKey` - the cache-persisted HS256 session key (30-day TTL, get/generate/
361
370
  re-read convergence) and the `TUNNEL_AUTH_SESSION_CUTOFF` force-clear cutoff.
@@ -366,7 +375,6 @@ needs it by hand.
366
375
  distributed).
367
376
  - `portr` - portr install, config rendering, and child launch.
368
377
  - `env` - the environment-variable names, each with its deprecated aliases.
369
- - `app` - boots the minimal gate AppKit app and returns the `AuthGateApi`.
370
378
 
371
379
  Browser-safe login wire schemas (the request/verify payloads and the session
372
380
  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 app from "./src/app.ts";
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 app from "./src/app.ts";
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 app from "./src/app.js";
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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwyQ0FBMkM7QUFDM0MsbURBQW1EO0FBQ25ELHdFQUF3RTtBQUV4RSxPQUFPLEtBQUssU0FBUyxNQUFNLG9CQUFvQixDQUFDO0FBQ2hELE9BQU8sS0FBSyxHQUFHLE1BQU0sY0FBYyxDQUFDO0FBQ3BDLE9BQU8sS0FBSyxHQUFHLE1BQU0sY0FBYyxDQUFDO0FBQ3BDLE9BQU8sS0FBSyxPQUFPLE1BQU0sa0JBQWtCLENBQUM7QUFDNUMsT0FBTyxLQUFLLFdBQVcsTUFBTSxzQkFBc0IsQ0FBQztBQUNwRCxPQUFPLEtBQUssR0FBRyxNQUFNLGNBQWMsQ0FBQztBQUNwQyxPQUFPLEtBQUssTUFBTSxNQUFNLGlCQUFpQixDQUFDO0FBQzFDLE9BQU8sS0FBSyxLQUFLLE1BQU0sZ0JBQWdCLENBQUM7QUFDeEMsT0FBTyxLQUFLLEtBQUssTUFBTSxnQkFBZ0IsQ0FBQztBQUN4QyxPQUFPLEtBQUssU0FBUyxNQUFNLHFCQUFxQixDQUFDO0FBQ2pELE9BQU8sS0FBSyxVQUFVLE1BQU0sc0JBQXNCLENBQUM7QUFDbkQsT0FBTyxFQUFFLFNBQVMsRUFBRSxXQUFXLEVBQUUsY0FBYyxFQUFFLFdBQVcsRUFBRSxlQUFlLEVBQUUsWUFBWSxFQUFFLGNBQWMsRUFBRSxrQkFBa0IsRUFBRSxpQkFBaUIsRUFBRSxZQUFZLEVBQUUsbUJBQW1CLEVBQUUsTUFBTSxjQUFjLENBQUM7QUFDNU0sT0FBTyxFQUFFLGlCQUFpQixFQUFFLHVCQUF1QixFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFHOUUsT0FBTyxFQUFFLFNBQVMsRUFBRSxNQUFNLGNBQWMsQ0FBQztBQUV6QyxPQUFPLEVBQUUsY0FBYyxFQUFFLFFBQVEsRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBSTNELE9BQU8sRUFBRSxXQUFXLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQUNsRCxPQUFPLEVBQUUsZUFBZSxFQUFFLE1BQU0sc0JBQXNCLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBHRU5FUkFURUQgYnkgcHJvamVuIHdhdGNoIC0gRE8gTk9UIEVESVQuXG4vLyBSZWdlbmVyYXRlZCBmcm9tIHRoZSBleHBvcnRpbmcgbW9kdWxlcyBpbiAuL3NyYy5cbi8vIEhhbmQgZWRpdHMgYXJlIG92ZXJ3cml0dGVuIG9uIHRoZSBuZXh0IHdhdGNoOyB0aGlzIGZpbGUgaXMgcmVhZC1vbmx5LlxuXG5leHBvcnQgKiBhcyBhbGxvd2xpc3QgZnJvbSBcIi4vc3JjL2FsbG93bGlzdC50c1wiO1xuZXhwb3J0ICogYXMgYXBwIGZyb20gXCIuL3NyYy9hcHAudHNcIjtcbmV4cG9ydCAqIGFzIGVudiBmcm9tIFwiLi9zcmMvZW52LnRzXCI7XG5leHBvcnQgKiBhcyBoZWFkZXJzIGZyb20gXCIuL3NyYy9oZWFkZXJzLnRzXCI7XG5leHBvcnQgKiBhcyBpbnRlcmNlcHRvciBmcm9tIFwiLi9zcmMvaW50ZXJjZXB0b3IudHNcIjtcbmV4cG9ydCAqIGFzIG90cCBmcm9tIFwiLi9zcmMvb3RwLnRzXCI7XG5leHBvcnQgKiBhcyBwbHVnaW4gZnJvbSBcIi4vc3JjL3BsdWdpbi50c1wiO1xuZXhwb3J0ICogYXMgcG9ydHIgZnJvbSBcIi4vc3JjL3BvcnRyLnRzXCI7XG5leHBvcnQgKiBhcyBwcm94eSBmcm9tIFwiLi9zcmMvcHJveHkudHNcIjtcbmV4cG9ydCAqIGFzIHJhdGVMaW1pdCBmcm9tIFwiLi9zcmMvcmF0ZS1saW1pdC50c1wiO1xuZXhwb3J0ICogYXMgc2lnbmluZ0tleSBmcm9tIFwiLi9zcmMvc2lnbmluZy1rZXkudHNcIjtcbmV4cG9ydCB7IEFMTE9XX0VOViwgU1VCSkVDVF9FTlYsIEJSQU5EX05BTUVfRU5WLCBNRVNTQUdFX0VOViwgU0VTU0lPTl9UVExfRU5WLCBDT0RFX1RUTF9FTlYsIEpXVF9TRUNSRVRfRU5WLCBTRVNTSU9OX0NVVE9GRl9FTlYsIFBVQkxJQ19ET01BSU5fRU5WLCBJTlNFQ1VSRV9FTlYsIEZPUldBUkRfSEVBREVSU19FTlYgfSBmcm9tIFwiLi9zcmMvZW52LnRzXCI7XG5leHBvcnQgeyBQUk9URUNURURfSEVBREVSUywgREVGQVVMVF9GT1JXQVJEX0hFQURFUlMgfSBmcm9tIFwiLi9zcmMvaGVhZGVycy50c1wiO1xuZXhwb3J0IHR5cGUgeyBIZWFkZXJQb2xpY3kgfSBmcm9tIFwiLi9zcmMvaGVhZGVycy50c1wiO1xuZXhwb3J0IHR5cGUgeyBUdW5uZWxJbnRlcmNlcHRvck9wdGlvbnMgfSBmcm9tIFwiLi9zcmMvaW50ZXJjZXB0b3IudHNcIjtcbmV4cG9ydCB7IENvZGVTdG9yZSB9IGZyb20gXCIuL3NyYy9vdHAudHNcIjtcbmV4cG9ydCB0eXBlIHsgVmVyaWZ5T3V0Y29tZSB9IGZyb20gXCIuL3NyYy9vdHAudHNcIjtcbmV4cG9ydCB7IEF1dGhHYXRlUGx1Z2luLCBhdXRoR2F0ZSB9IGZyb20gXCIuL3NyYy9wbHVnaW4udHNcIjtcbmV4cG9ydCB0eXBlIHsgQXV0aEdhdGVDb25maWcsIFNlbmRDb2RlT3B0aW9ucywgUmVzb2x2ZWRBdXRoR2F0ZUNvbmZpZywgQXV0aEdhdGVBcGkgfSBmcm9tIFwiLi9zcmMvcGx1Z2luLnRzXCI7XG5leHBvcnQgdHlwZSB7IFBvcnRyQ29uZmlnIH0gZnJvbSBcIi4vc3JjL3BvcnRyLnRzXCI7XG5leHBvcnQgdHlwZSB7IFByb3h5T3B0aW9ucyB9IGZyb20gXCIuL3NyYy9wcm94eS50c1wiO1xuZXhwb3J0IHsgUmF0ZUxpbWl0ZXIgfSBmcm9tIFwiLi9zcmMvcmF0ZS1saW1pdC50c1wiO1xuZXhwb3J0IHsgS0VZX1RUTF9TRUNPTkRTIH0gZnJvbSBcIi4vc3JjL3NpZ25pbmcta2V5LnRzXCI7XG5leHBvcnQgdHlwZSB7IFNpZ25pbmdLZXkgfSBmcm9tIFwiLi9zcmMvc2lnbmluZy1rZXkudHNcIjtcbiJdfQ==
23
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwyQ0FBMkM7QUFDM0MsbURBQW1EO0FBQ25ELHdFQUF3RTtBQUV4RSxPQUFPLEtBQUssU0FBUyxNQUFNLG9CQUFvQixDQUFDO0FBQ2hELE9BQU8sS0FBSyxTQUFTLE1BQU0scUJBQXFCLENBQUM7QUFDakQsT0FBTyxLQUFLLEdBQUcsTUFBTSxjQUFjLENBQUM7QUFDcEMsT0FBTyxLQUFLLElBQUksTUFBTSxlQUFlLENBQUM7QUFDdEMsT0FBTyxLQUFLLE9BQU8sTUFBTSxrQkFBa0IsQ0FBQztBQUM1QyxPQUFPLEtBQUssV0FBVyxNQUFNLHNCQUFzQixDQUFDO0FBQ3BELE9BQU8sS0FBSyxHQUFHLE1BQU0sY0FBYyxDQUFDO0FBQ3BDLE9BQU8sS0FBSyxNQUFNLE1BQU0saUJBQWlCLENBQUM7QUFDMUMsT0FBTyxLQUFLLEtBQUssTUFBTSxnQkFBZ0IsQ0FBQztBQUN4QyxPQUFPLEtBQUssU0FBUyxNQUFNLHFCQUFxQixDQUFDO0FBQ2pELE9BQU8sS0FBSyxRQUFRLE1BQU0sb0JBQW9CLENBQUM7QUFDL0MsT0FBTyxLQUFLLFVBQVUsTUFBTSxzQkFBc0IsQ0FBQztBQUVuRCxPQUFPLEVBQUUsU0FBUyxFQUFFLFdBQVcsRUFBRSxjQUFjLEVBQUUsV0FBVyxFQUFFLGVBQWUsRUFBRSxZQUFZLEVBQUUsY0FBYyxFQUFFLGtCQUFrQixFQUFFLGlCQUFpQixFQUFFLFlBQVksRUFBRSxtQkFBbUIsRUFBRSxNQUFNLGNBQWMsQ0FBQztBQUM1TSxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0sZUFBZSxDQUFDO0FBRTVDLE9BQU8sRUFBRSxpQkFBaUIsRUFBRSx1QkFBdUIsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBRzlFLE9BQU8sRUFBRSxTQUFTLEVBQUUsTUFBTSxjQUFjLENBQUM7QUFFekMsT0FBTyxFQUFFLGNBQWMsRUFBRSxRQUFRLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUczRCxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0scUJBQXFCLENBQUM7QUFDbEQsT0FBTyxFQUFFLGVBQWUsRUFBRSxNQUFNLHNCQUFzQixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLy8gR0VORVJBVEVEIGJ5IHByb2plbiB3YXRjaCAtIERPIE5PVCBFRElULlxuLy8gUmVnZW5lcmF0ZWQgZnJvbSB0aGUgZXhwb3J0aW5nIG1vZHVsZXMgaW4gLi9zcmMuXG4vLyBIYW5kIGVkaXRzIGFyZSBvdmVyd3JpdHRlbiBvbiB0aGUgbmV4dCB3YXRjaDsgdGhpcyBmaWxlIGlzIHJlYWQtb25seS5cblxuZXhwb3J0ICogYXMgYWxsb3dsaXN0IGZyb20gXCIuL3NyYy9hbGxvd2xpc3QudHNcIjtcbmV4cG9ydCAqIGFzIGNvZGVFbWFpbCBmcm9tIFwiLi9zcmMvY29kZS1lbWFpbC50c1wiO1xuZXhwb3J0ICogYXMgZW52IGZyb20gXCIuL3NyYy9lbnYudHNcIjtcbmV4cG9ydCAqIGFzIGdhdGUgZnJvbSBcIi4vc3JjL2dhdGUudHNcIjtcbmV4cG9ydCAqIGFzIGhlYWRlcnMgZnJvbSBcIi4vc3JjL2hlYWRlcnMudHNcIjtcbmV4cG9ydCAqIGFzIGludGVyY2VwdG9yIGZyb20gXCIuL3NyYy9pbnRlcmNlcHRvci50c1wiO1xuZXhwb3J0ICogYXMgb3RwIGZyb20gXCIuL3NyYy9vdHAudHNcIjtcbmV4cG9ydCAqIGFzIHBsdWdpbiBmcm9tIFwiLi9zcmMvcGx1Z2luLnRzXCI7XG5leHBvcnQgKiBhcyBwb3J0ciBmcm9tIFwiLi9zcmMvcG9ydHIudHNcIjtcbmV4cG9ydCAqIGFzIHJhdGVMaW1pdCBmcm9tIFwiLi9zcmMvcmF0ZS1saW1pdC50c1wiO1xuZXhwb3J0ICogYXMgc2VuZENvZGUgZnJvbSBcIi4vc3JjL3NlbmQtY29kZS50c1wiO1xuZXhwb3J0ICogYXMgc2lnbmluZ0tleSBmcm9tIFwiLi9zcmMvc2lnbmluZy1rZXkudHNcIjtcbmV4cG9ydCB0eXBlIHsgQ29kZUNvcHkgfSBmcm9tIFwiLi9zcmMvY29kZS1lbWFpbC50c1wiO1xuZXhwb3J0IHsgQUxMT1dfRU5WLCBTVUJKRUNUX0VOViwgQlJBTkRfTkFNRV9FTlYsIE1FU1NBR0VfRU5WLCBTRVNTSU9OX1RUTF9FTlYsIENPREVfVFRMX0VOViwgSldUX1NFQ1JFVF9FTlYsIFNFU1NJT05fQ1VUT0ZGX0VOViwgUFVCTElDX0RPTUFJTl9FTlYsIElOU0VDVVJFX0VOViwgRk9SV0FSRF9IRUFERVJTX0VOViB9IGZyb20gXCIuL3NyYy9lbnYudHNcIjtcbmV4cG9ydCB7IEFVVEhfUFJFRklYIH0gZnJvbSBcIi4vc3JjL2dhdGUudHNcIjtcbmV4cG9ydCB0eXBlIHsgR2F0ZU9wdGlvbnMgfSBmcm9tIFwiLi9zcmMvZ2F0ZS50c1wiO1xuZXhwb3J0IHsgUFJPVEVDVEVEX0hFQURFUlMsIERFRkFVTFRfRk9SV0FSRF9IRUFERVJTIH0gZnJvbSBcIi4vc3JjL2hlYWRlcnMudHNcIjtcbmV4cG9ydCB0eXBlIHsgSGVhZGVyUG9saWN5IH0gZnJvbSBcIi4vc3JjL2hlYWRlcnMudHNcIjtcbmV4cG9ydCB0eXBlIHsgVHVubmVsSW50ZXJjZXB0b3JPcHRpb25zIH0gZnJvbSBcIi4vc3JjL2ludGVyY2VwdG9yLnRzXCI7XG5leHBvcnQgeyBDb2RlU3RvcmUgfSBmcm9tIFwiLi9zcmMvb3RwLnRzXCI7XG5leHBvcnQgdHlwZSB7IFZlcmlmeU91dGNvbWUgfSBmcm9tIFwiLi9zcmMvb3RwLnRzXCI7XG5leHBvcnQgeyBBdXRoR2F0ZVBsdWdpbiwgYXV0aEdhdGUgfSBmcm9tIFwiLi9zcmMvcGx1Z2luLnRzXCI7XG5leHBvcnQgdHlwZSB7IEF1dGhHYXRlQ29uZmlnLCBTZW5kQ29kZU9wdGlvbnMsIFJlc29sdmVkQXV0aEdhdGVDb25maWcsIEF1dGhHYXRlQXBpIH0gZnJvbSBcIi4vc3JjL3BsdWdpbi50c1wiO1xuZXhwb3J0IHR5cGUgeyBQb3J0ckNvbmZpZyB9IGZyb20gXCIuL3NyYy9wb3J0ci50c1wiO1xuZXhwb3J0IHsgUmF0ZUxpbWl0ZXIgfSBmcm9tIFwiLi9zcmMvcmF0ZS1saW1pdC50c1wiO1xuZXhwb3J0IHsgS0VZX1RUTF9TRUNPTkRTIH0gZnJvbSBcIi4vc3JjL3NpZ25pbmcta2V5LnRzXCI7XG5leHBvcnQgdHlwZSB7IFNpZ25pbmdLZXkgfSBmcm9tIFwiLi9zcmMvc2lnbmluZy1rZXkudHNcIjtcbiJdfQ==
@@ -1,30 +1,18 @@
1
1
  /**
2
- * The tunnel gate's tiny AppKit "app" - `createApp` WITHOUT a `server()` plugin.
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
- * 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`.
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 { type AuthGateApi, type AuthGateConfig, type SendCodeOptions } from "./plugin.ts";
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;