@dbx-tools/email 0.6.44 → 0.6.46

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/src/plugin.ts CHANGED
@@ -52,10 +52,7 @@ import {
52
52
  type EmailResult,
53
53
  type EmailSenders,
54
54
  } from "@dbx-tools/shared-email";
55
- import type express from "express";
56
- import { authRequestSchema, authVerifySchema } from "@dbx-tools/shared-email";
57
- import { EMAIL_CONFIG_SCHEMA, resolveAuthConfig, type EmailPluginConfig } from "./config.ts";
58
- import { AuthGate, SESSION_COOKIE } from "./auth/gate.ts";
55
+ import { EMAIL_CONFIG_SCHEMA, type EmailPluginConfig } from "./config.ts";
59
56
  import { EMAIL_SENDERS_SETTINGS, EMAIL_VERIFY_SETTINGS } from "./defaults.ts";
60
57
  import { isSenderAllowed, listSenderOptions, resolveSenderAddress } from "./sender.ts";
61
58
  import { SEND_EMAIL_DESCRIPTION } from "./tool.ts";
@@ -96,9 +93,6 @@ const logger = log.logger("email");
96
93
  * ```
97
94
  */
98
95
  export class EmailPlugin extends Plugin<EmailPluginConfig> implements ToolProvider {
99
- /** The email-OTP access gate, constructed in {@link setup} when `auth.enabled`. */
100
- private authGate?: AuthGate;
101
-
102
96
  static manifest = {
103
97
  name: "email",
104
98
  displayName: "Email",
@@ -149,7 +143,6 @@ export class EmailPlugin extends Plugin<EmailPluginConfig> implements ToolProvid
149
143
  override async setup(): Promise<void> {
150
144
  const { transporter, config } = getEmailRuntime(this.config);
151
145
  setEmailExecutor((fn, settings) => this.execute(fn, settings));
152
- this.setupAuthGate();
153
146
  const policy = {
154
147
  mode: config.mode,
155
148
  senderPolicy: config.senderPolicy,
@@ -225,85 +218,6 @@ export class EmailPlugin extends Plugin<EmailPluginConfig> implements ToolProvid
225
218
  res.json(result.data);
226
219
  },
227
220
  });
228
- this.injectAuthRoutes(router);
229
- }
230
-
231
- /**
232
- * Mount the email-OTP login flow under `/api/email/auth/*` when the gate is
233
- * enabled. These routes are the ONLY ones the gate middleware leaves open (see
234
- * {@link AuthGate.middleware}); everything else requires the session cookie
235
- * they establish. No-op when auth is off.
236
- */
237
- private injectAuthRoutes(router: IAppRouter): void {
238
- const gate = this.authGate;
239
- if (!gate) return;
240
-
241
- // `request` and `verify` take a raw email/code, not an OBO user, so they are
242
- // NOT wrapped in `asUser` - the caller is anonymous until verified.
243
- this.route(router, {
244
- name: "authRequest",
245
- method: "post",
246
- path: "/auth/request",
247
- handler: async (req, res) => {
248
- const parsed = authRequestSchema.safeParse(req.body);
249
- if (!parsed.success) {
250
- // Even a malformed body reports success (anti-enumeration).
251
- res.json({ ok: true });
252
- return;
253
- }
254
- const result = await gate.handleRequest(parsed.data.email, this.clientIp(req));
255
- res.json(result);
256
- },
257
- });
258
-
259
- this.route(router, {
260
- name: "authVerify",
261
- method: "post",
262
- path: "/auth/verify",
263
- handler: async (req, res) => {
264
- const parsed = authVerifySchema.safeParse(req.body);
265
- if (!parsed.success) {
266
- res.json({ ok: false });
267
- return;
268
- }
269
- const result = await gate.handleVerify(
270
- parsed.data.email,
271
- parsed.data.code,
272
- this.clientIp(req),
273
- req.secure,
274
- );
275
- if (result.ok && result.token && result.cookieOptions) {
276
- res.cookie(SESSION_COOKIE, result.token, result.cookieOptions);
277
- }
278
- res.json({ ok: result.ok, ...(result.retryAfter ? { retryAfter: result.retryAfter } : {}) });
279
- },
280
- });
281
-
282
- this.route(router, {
283
- name: "authLogout",
284
- method: "post",
285
- path: "/auth/logout",
286
- handler: async (_req, res) => {
287
- res.clearCookie(SESSION_COOKIE, { path: "/" });
288
- res.json({ ok: true });
289
- },
290
- });
291
-
292
- this.route(router, {
293
- name: "authStatus",
294
- method: "get",
295
- path: "/auth/status",
296
- handler: async (req, res) => {
297
- res.json(await gate.status(req));
298
- },
299
- });
300
- }
301
-
302
- /** Client IP for rate-limiting, honoring the Apps ingress `x-forwarded-for`. */
303
- private clientIp(req: express.Request): string {
304
- const fwd = req.headers["x-forwarded-for"];
305
- const first = Array.isArray(fwd) ? fwd[0] : fwd?.split(",")[0];
306
- return (first ?? req.ip ?? "unknown").trim();
307
221
  }
308
222
 
309
223
  override exports() {
@@ -354,82 +268,6 @@ export class EmailPlugin extends Plugin<EmailPluginConfig> implements ToolProvid
354
268
  return sendEmail(message, from ?? this.resolveSender(), signal);
355
269
  }
356
270
 
357
- /**
358
- * Build the email-OTP access gate when `auth.enabled`, wiring its code
359
- * delivery through this plugin's own transport. Called from {@link setup}; a
360
- * no-op (leaves {@link authGate} undefined) when the gate is off.
361
- */
362
- private setupAuthGate(): void {
363
- const auth = resolveAuthConfig(this.config.auth);
364
- if (!auth) return;
365
- const gate = new AuthGate({
366
- allow: auth.allow,
367
- sessionTtlSeconds: auth.sessionTtlSeconds,
368
- codeTtlSeconds: auth.codeTtlSeconds,
369
- maxAttempts: auth.maxAttempts,
370
- secureCookies: process.env.NODE_ENV === "production",
371
- sendCode: (email, code) => this.sendOtpEmail(email, code),
372
- });
373
- this.authGate = gate;
374
-
375
- // The gate must protect the WHOLE app, but a plugin's own `injectRoutes`
376
- // router only covers `/api/email/*`. AppKit's `server` plugin exposes
377
- // `addExtension(fn)` for exactly this: an app-level middleware applied before
378
- // the server listens. The lookup is deferred to the `setup:complete`
379
- // lifecycle event because sibling plugins (including `server`) are not all
380
- // registered during this plugin's own `setup()` - the same coordination
381
- // `appkit-mastra` uses to wait for `lakebase`. The login routes are exempted
382
- // by the middleware's open-prefix (`/api/email/auth`).
383
- const registerGate = () => {
384
- const server = this.context?.getPlugins().get("server") as
385
- | { addExtension?: (fn: (app: express.Application) => void) => void }
386
- | undefined;
387
- if (server?.addExtension) {
388
- server.addExtension((app) =>
389
- app.use(gate.middleware(`/api/${EmailPlugin.manifest.name}`)),
390
- );
391
- logger.info("auth:enabled", {
392
- patterns: auth.allow.length,
393
- sessionTtlSeconds: auth.sessionTtlSeconds,
394
- });
395
- } else {
396
- // No server plugin to gate through - the login routes still work, but the
397
- // rest of the app would be ungated, so disable rather than half-enable.
398
- this.authGate = undefined;
399
- logger.warn(
400
- "auth:disabled - the `server` plugin is required to gate the app; add server() to your plugins",
401
- );
402
- }
403
- };
404
- if (this.context?.onLifecycle) {
405
- this.context.onLifecycle("setup:complete", registerGate);
406
- } else {
407
- registerGate();
408
- }
409
- }
410
-
411
- /**
412
- * Deliver a one-time code to `email` through this plugin's transport. Throws
413
- * on failure (the gate swallows it, so a delivery error never leaks whether
414
- * the address was allow-listed).
415
- */
416
- private async sendOtpEmail(email: string, code: string): Promise<void> {
417
- await this.send(
418
- {
419
- to: [email],
420
- subject: `Your sign-in code: ${code}`,
421
- body: [
422
- `Your one-time sign-in code is:`,
423
- ``,
424
- `## ${code}`,
425
- ``,
426
- `It expires shortly. If you didn't request this, you can ignore this email.`,
427
- ].join("\n"),
428
- },
429
- undefined,
430
- );
431
- }
432
-
433
271
  /** Run the sender-options lookup through the plugin's interceptor chain. */
434
272
  private async executeListSenders(): Promise<ExecutionResult<EmailSenders>> {
435
273
  return this.execute(async () => this.listSenders(), EMAIL_SENDERS_SETTINGS);
@@ -1,27 +0,0 @@
1
- /**
2
- * Unified access allow-list matching for the email-OTP gate.
3
- *
4
- * Each pattern in the configured list is one of three shapes, tried in order:
5
- *
6
- * - **domain shortcut** - `databricks.com` or `@databricks.com`: matches any
7
- * address whose domain equals it (case-insensitive). The leading `@` is
8
- * optional and stripped.
9
- * - **glob** - contains `*` or `?`, e.g. `*.databricks.com` or
10
- * `*@databricks.com`: matched against the WHOLE address with shell-style
11
- * wildcards (`*` = any run, `?` = one char).
12
- * - **regex** - wrapped in slashes, `/.../ [flags]`: compiled and tested
13
- * against the whole address. An invalid regex never matches (it is skipped
14
- * with a warning rather than throwing).
15
- *
16
- * An EMPTY list matches nobody (fail closed): an app that enables the gate but
17
- * configures no patterns lets no one in, which is the safe default.
18
- *
19
- * @module
20
- */
21
- /**
22
- * True when `email` is allowed by ANY pattern in `patterns`. An empty (or
23
- * missing) list allows nobody - the gate fails closed.
24
- */
25
- export declare function matchesAllowlist(email: string, patterns: readonly string[] | undefined): boolean;
26
- /** Rough shape check so a clearly-invalid address is rejected before any work. */
27
- export declare function looksLikeEmail(value: string): boolean;
@@ -1,82 +0,0 @@
1
- /**
2
- * Unified access allow-list matching for the email-OTP gate.
3
- *
4
- * Each pattern in the configured list is one of three shapes, tried in order:
5
- *
6
- * - **domain shortcut** - `databricks.com` or `@databricks.com`: matches any
7
- * address whose domain equals it (case-insensitive). The leading `@` is
8
- * optional and stripped.
9
- * - **glob** - contains `*` or `?`, e.g. `*.databricks.com` or
10
- * `*@databricks.com`: matched against the WHOLE address with shell-style
11
- * wildcards (`*` = any run, `?` = one char).
12
- * - **regex** - wrapped in slashes, `/.../ [flags]`: compiled and tested
13
- * against the whole address. An invalid regex never matches (it is skipped
14
- * with a warning rather than throwing).
15
- *
16
- * An EMPTY list matches nobody (fail closed): an app that enables the gate but
17
- * configures no patterns lets no one in, which is the safe default.
18
- *
19
- * @module
20
- */
21
- import { log } from "@dbx-tools/shared-core";
22
- const logger = log.logger("email:auth:allowlist");
23
- /** Escape a string for literal use inside a `RegExp`. */
24
- function escapeRegExp(value) {
25
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
26
- }
27
- /** Compile a shell-style glob (`*`, `?`) into an anchored, case-insensitive RegExp. */
28
- function globToRegExp(glob) {
29
- const body = glob
30
- .split(/([*?])/)
31
- .map((part) => (part === "*" ? ".*" : part === "?" ? "." : escapeRegExp(part)))
32
- .join("");
33
- return new RegExp(`^${body}$`, "i");
34
- }
35
- /** Parse a `/pattern/flags` string into a RegExp, or `undefined` if malformed. */
36
- function parseRegexLiteral(pattern) {
37
- const match = /^\/(.+)\/([a-z]*)$/is.exec(pattern);
38
- if (!match)
39
- return undefined;
40
- try {
41
- const flags = match[2].includes("i") ? match[2] : `${match[2]}i`;
42
- return new RegExp(match[1], flags);
43
- }
44
- catch (error) {
45
- logger.warn("ignoring invalid regex allow-list pattern", { pattern, error });
46
- return undefined;
47
- }
48
- }
49
- /** True when `email` matches a single allow-list `pattern`. */
50
- function matchesPattern(email, pattern) {
51
- const trimmed = pattern.trim();
52
- if (!trimmed)
53
- return false;
54
- const address = email.trim().toLowerCase();
55
- // Regex literal: /.../
56
- if (trimmed.startsWith("/")) {
57
- const re = parseRegexLiteral(trimmed);
58
- return re ? re.test(address) : false;
59
- }
60
- // Glob: contains a wildcard.
61
- if (trimmed.includes("*") || trimmed.includes("?")) {
62
- return globToRegExp(trimmed).test(address);
63
- }
64
- // Domain shortcut: `@d.com` or `d.com` -> match the address's domain.
65
- const domain = trimmed.replace(/^@/, "").toLowerCase();
66
- const at = address.lastIndexOf("@");
67
- return at >= 0 && address.slice(at + 1) === domain;
68
- }
69
- /**
70
- * True when `email` is allowed by ANY pattern in `patterns`. An empty (or
71
- * missing) list allows nobody - the gate fails closed.
72
- */
73
- export function matchesAllowlist(email, patterns) {
74
- if (!email || !patterns || patterns.length === 0)
75
- return false;
76
- return patterns.some((pattern) => matchesPattern(email, pattern));
77
- }
78
- /** Rough shape check so a clearly-invalid address is rejected before any work. */
79
- export function looksLikeEmail(value) {
80
- return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim());
81
- }
82
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYWxsb3dsaXN0LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2F1dGgvYWxsb3dsaXN0LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBbUJHO0FBRUgsT0FBTyxFQUFFLEdBQUcsRUFBRSxNQUFNLHdCQUF3QixDQUFDO0FBRTdDLE1BQU0sTUFBTSxHQUFHLEdBQUcsQ0FBQyxNQUFNLENBQUMsc0JBQXNCLENBQUMsQ0FBQztBQUVsRCx5REFBeUQ7QUFDekQsU0FBUyxZQUFZLENBQUMsS0FBYTtJQUNqQyxPQUFPLEtBQUssQ0FBQyxPQUFPLENBQUMscUJBQXFCLEVBQUUsTUFBTSxDQUFDLENBQUM7QUFDdEQsQ0FBQztBQUVELHVGQUF1RjtBQUN2RixTQUFTLFlBQVksQ0FBQyxJQUFZO0lBQ2hDLE1BQU0sSUFBSSxHQUFHLElBQUk7U0FDZCxLQUFLLENBQUMsUUFBUSxDQUFDO1NBQ2YsR0FBRyxDQUFDLENBQUMsSUFBSSxFQUFFLEVBQUUsQ0FBQyxDQUFDLElBQUksS0FBSyxHQUFHLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsSUFBSSxLQUFLLEdBQUcsQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxZQUFZLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQztTQUM5RSxJQUFJLENBQUMsRUFBRSxDQUFDLENBQUM7SUFDWixPQUFPLElBQUksTUFBTSxDQUFDLElBQUksSUFBSSxHQUFHLEVBQUUsR0FBRyxDQUFDLENBQUM7QUFDdEMsQ0FBQztBQUVELGtGQUFrRjtBQUNsRixTQUFTLGlCQUFpQixDQUFDLE9BQWU7SUFDeEMsTUFBTSxLQUFLLEdBQUcsc0JBQXNCLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxDQUFDO0lBQ25ELElBQUksQ0FBQyxLQUFLO1FBQUUsT0FBTyxTQUFTLENBQUM7SUFDN0IsSUFBSSxDQUFDO1FBQ0gsTUFBTSxLQUFLLEdBQUcsS0FBSyxDQUFDLENBQUMsQ0FBRSxDQUFDLFFBQVEsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBRSxDQUFDLENBQUMsQ0FBQyxHQUFHLEtBQUssQ0FBQyxDQUFDLENBQUUsR0FBRyxDQUFDO1FBQ3BFLE9BQU8sSUFBSSxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBRSxFQUFFLEtBQUssQ0FBQyxDQUFDO0lBQ3RDLENBQUM7SUFBQyxPQUFPLEtBQUssRUFBRSxDQUFDO1FBQ2YsTUFBTSxDQUFDLElBQUksQ0FBQywyQ0FBMkMsRUFBRSxFQUFFLE9BQU8sRUFBRSxLQUFLLEVBQUUsQ0FBQyxDQUFDO1FBQzdFLE9BQU8sU0FBUyxDQUFDO0lBQ25CLENBQUM7QUFDSCxDQUFDO0FBRUQsK0RBQStEO0FBQy9ELFNBQVMsY0FBYyxDQUFDLEtBQWEsRUFBRSxPQUFlO0lBQ3BELE1BQU0sT0FBTyxHQUFHLE9BQU8sQ0FBQyxJQUFJLEVBQUUsQ0FBQztJQUMvQixJQUFJLENBQUMsT0FBTztRQUFFLE9BQU8sS0FBSyxDQUFDO0lBQzNCLE1BQU0sT0FBTyxHQUFHLEtBQUssQ0FBQyxJQUFJLEVBQUUsQ0FBQyxXQUFXLEVBQUUsQ0FBQztJQUUzQyx1QkFBdUI7SUFDdkIsSUFBSSxPQUFPLENBQUMsVUFBVSxDQUFDLEdBQUcsQ0FBQyxFQUFFLENBQUM7UUFDNUIsTUFBTSxFQUFFLEdBQUcsaUJBQWlCLENBQUMsT0FBTyxDQUFDLENBQUM7UUFDdEMsT0FBTyxFQUFFLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQztJQUN2QyxDQUFDO0lBRUQsNkJBQTZCO0lBQzdCLElBQUksT0FBTyxDQUFDLFFBQVEsQ0FBQyxHQUFHLENBQUMsSUFBSSxPQUFPLENBQUMsUUFBUSxDQUFDLEdBQUcsQ0FBQyxFQUFFLENBQUM7UUFDbkQsT0FBTyxZQUFZLENBQUMsT0FBTyxDQUFDLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxDQUFDO0lBQzdDLENBQUM7SUFFRCxzRUFBc0U7SUFDdEUsTUFBTSxNQUFNLEdBQUcsT0FBTyxDQUFDLE9BQU8sQ0FBQyxJQUFJLEVBQUUsRUFBRSxDQUFDLENBQUMsV0FBVyxFQUFFLENBQUM7SUFDdkQsTUFBTSxFQUFFLEdBQUcsT0FBTyxDQUFDLFdBQVcsQ0FBQyxHQUFHLENBQUMsQ0FBQztJQUNwQyxPQUFPLEVBQUUsSUFBSSxDQUFDLElBQUksT0FBTyxDQUFDLEtBQUssQ0FBQyxFQUFFLEdBQUcsQ0FBQyxDQUFDLEtBQUssTUFBTSxDQUFDO0FBQ3JELENBQUM7QUFFRDs7O0dBR0c7QUFDSCxNQUFNLFVBQVUsZ0JBQWdCLENBQUMsS0FBYSxFQUFFLFFBQXVDO0lBQ3JGLElBQUksQ0FBQyxLQUFLLElBQUksQ0FBQyxRQUFRLElBQUksUUFBUSxDQUFDLE1BQU0sS0FBSyxDQUFDO1FBQUUsT0FBTyxLQUFLLENBQUM7SUFDL0QsT0FBTyxRQUFRLENBQUMsSUFBSSxDQUFDLENBQUMsT0FBTyxFQUFFLEVBQUUsQ0FBQyxjQUFjLENBQUMsS0FBSyxFQUFFLE9BQU8sQ0FBQyxDQUFDLENBQUM7QUFDcEUsQ0FBQztBQUVELGtGQUFrRjtBQUNsRixNQUFNLFVBQVUsY0FBYyxDQUFDLEtBQWE7SUFDMUMsT0FBTyw0QkFBNEIsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLElBQUksRUFBRSxDQUFDLENBQUM7QUFDekQsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogVW5pZmllZCBhY2Nlc3MgYWxsb3ctbGlzdCBtYXRjaGluZyBmb3IgdGhlIGVtYWlsLU9UUCBnYXRlLlxuICpcbiAqIEVhY2ggcGF0dGVybiBpbiB0aGUgY29uZmlndXJlZCBsaXN0IGlzIG9uZSBvZiB0aHJlZSBzaGFwZXMsIHRyaWVkIGluIG9yZGVyOlxuICpcbiAqICAgLSAqKmRvbWFpbiBzaG9ydGN1dCoqIC0gYGRhdGFicmlja3MuY29tYCBvciBgQGRhdGFicmlja3MuY29tYDogbWF0Y2hlcyBhbnlcbiAqICAgICBhZGRyZXNzIHdob3NlIGRvbWFpbiBlcXVhbHMgaXQgKGNhc2UtaW5zZW5zaXRpdmUpLiBUaGUgbGVhZGluZyBgQGAgaXNcbiAqICAgICBvcHRpb25hbCBhbmQgc3RyaXBwZWQuXG4gKiAgIC0gKipnbG9iKiogLSBjb250YWlucyBgKmAgb3IgYD9gLCBlLmcuIGAqLmRhdGFicmlja3MuY29tYCBvclxuICogICAgIGAqQGRhdGFicmlja3MuY29tYDogbWF0Y2hlZCBhZ2FpbnN0IHRoZSBXSE9MRSBhZGRyZXNzIHdpdGggc2hlbGwtc3R5bGVcbiAqICAgICB3aWxkY2FyZHMgKGAqYCA9IGFueSBydW4sIGA/YCA9IG9uZSBjaGFyKS5cbiAqICAgLSAqKnJlZ2V4KiogLSB3cmFwcGVkIGluIHNsYXNoZXMsIGAvLi4uLyBbZmxhZ3NdYDogY29tcGlsZWQgYW5kIHRlc3RlZFxuICogICAgIGFnYWluc3QgdGhlIHdob2xlIGFkZHJlc3MuIEFuIGludmFsaWQgcmVnZXggbmV2ZXIgbWF0Y2hlcyAoaXQgaXMgc2tpcHBlZFxuICogICAgIHdpdGggYSB3YXJuaW5nIHJhdGhlciB0aGFuIHRocm93aW5nKS5cbiAqXG4gKiBBbiBFTVBUWSBsaXN0IG1hdGNoZXMgbm9ib2R5IChmYWlsIGNsb3NlZCk6IGFuIGFwcCB0aGF0IGVuYWJsZXMgdGhlIGdhdGUgYnV0XG4gKiBjb25maWd1cmVzIG5vIHBhdHRlcm5zIGxldHMgbm8gb25lIGluLCB3aGljaCBpcyB0aGUgc2FmZSBkZWZhdWx0LlxuICpcbiAqIEBtb2R1bGVcbiAqL1xuXG5pbXBvcnQgeyBsb2cgfSBmcm9tIFwiQGRieC10b29scy9zaGFyZWQtY29yZVwiO1xuXG5jb25zdCBsb2dnZXIgPSBsb2cubG9nZ2VyKFwiZW1haWw6YXV0aDphbGxvd2xpc3RcIik7XG5cbi8qKiBFc2NhcGUgYSBzdHJpbmcgZm9yIGxpdGVyYWwgdXNlIGluc2lkZSBhIGBSZWdFeHBgLiAqL1xuZnVuY3Rpb24gZXNjYXBlUmVnRXhwKHZhbHVlOiBzdHJpbmcpOiBzdHJpbmcge1xuICByZXR1cm4gdmFsdWUucmVwbGFjZSgvWy4qKz9eJHt9KCl8W1xcXVxcXFxdL2csIFwiXFxcXCQmXCIpO1xufVxuXG4vKiogQ29tcGlsZSBhIHNoZWxsLXN0eWxlIGdsb2IgKGAqYCwgYD9gKSBpbnRvIGFuIGFuY2hvcmVkLCBjYXNlLWluc2Vuc2l0aXZlIFJlZ0V4cC4gKi9cbmZ1bmN0aW9uIGdsb2JUb1JlZ0V4cChnbG9iOiBzdHJpbmcpOiBSZWdFeHAge1xuICBjb25zdCBib2R5ID0gZ2xvYlxuICAgIC5zcGxpdCgvKFsqP10pLylcbiAgICAubWFwKChwYXJ0KSA9PiAocGFydCA9PT0gXCIqXCIgPyBcIi4qXCIgOiBwYXJ0ID09PSBcIj9cIiA/IFwiLlwiIDogZXNjYXBlUmVnRXhwKHBhcnQpKSlcbiAgICAuam9pbihcIlwiKTtcbiAgcmV0dXJuIG5ldyBSZWdFeHAoYF4ke2JvZHl9JGAsIFwiaVwiKTtcbn1cblxuLyoqIFBhcnNlIGEgYC9wYXR0ZXJuL2ZsYWdzYCBzdHJpbmcgaW50byBhIFJlZ0V4cCwgb3IgYHVuZGVmaW5lZGAgaWYgbWFsZm9ybWVkLiAqL1xuZnVuY3Rpb24gcGFyc2VSZWdleExpdGVyYWwocGF0dGVybjogc3RyaW5nKTogUmVnRXhwIHwgdW5kZWZpbmVkIHtcbiAgY29uc3QgbWF0Y2ggPSAvXlxcLyguKylcXC8oW2Etel0qKSQvaXMuZXhlYyhwYXR0ZXJuKTtcbiAgaWYgKCFtYXRjaCkgcmV0dXJuIHVuZGVmaW5lZDtcbiAgdHJ5IHtcbiAgICBjb25zdCBmbGFncyA9IG1hdGNoWzJdIS5pbmNsdWRlcyhcImlcIikgPyBtYXRjaFsyXSEgOiBgJHttYXRjaFsyXSF9aWA7XG4gICAgcmV0dXJuIG5ldyBSZWdFeHAobWF0Y2hbMV0hLCBmbGFncyk7XG4gIH0gY2F0Y2ggKGVycm9yKSB7XG4gICAgbG9nZ2VyLndhcm4oXCJpZ25vcmluZyBpbnZhbGlkIHJlZ2V4IGFsbG93LWxpc3QgcGF0dGVyblwiLCB7IHBhdHRlcm4sIGVycm9yIH0pO1xuICAgIHJldHVybiB1bmRlZmluZWQ7XG4gIH1cbn1cblxuLyoqIFRydWUgd2hlbiBgZW1haWxgIG1hdGNoZXMgYSBzaW5nbGUgYWxsb3ctbGlzdCBgcGF0dGVybmAuICovXG5mdW5jdGlvbiBtYXRjaGVzUGF0dGVybihlbWFpbDogc3RyaW5nLCBwYXR0ZXJuOiBzdHJpbmcpOiBib29sZWFuIHtcbiAgY29uc3QgdHJpbW1lZCA9IHBhdHRlcm4udHJpbSgpO1xuICBpZiAoIXRyaW1tZWQpIHJldHVybiBmYWxzZTtcbiAgY29uc3QgYWRkcmVzcyA9IGVtYWlsLnRyaW0oKS50b0xvd2VyQ2FzZSgpO1xuXG4gIC8vIFJlZ2V4IGxpdGVyYWw6IC8uLi4vXG4gIGlmICh0cmltbWVkLnN0YXJ0c1dpdGgoXCIvXCIpKSB7XG4gICAgY29uc3QgcmUgPSBwYXJzZVJlZ2V4TGl0ZXJhbCh0cmltbWVkKTtcbiAgICByZXR1cm4gcmUgPyByZS50ZXN0KGFkZHJlc3MpIDogZmFsc2U7XG4gIH1cblxuICAvLyBHbG9iOiBjb250YWlucyBhIHdpbGRjYXJkLlxuICBpZiAodHJpbW1lZC5pbmNsdWRlcyhcIipcIikgfHwgdHJpbW1lZC5pbmNsdWRlcyhcIj9cIikpIHtcbiAgICByZXR1cm4gZ2xvYlRvUmVnRXhwKHRyaW1tZWQpLnRlc3QoYWRkcmVzcyk7XG4gIH1cblxuICAvLyBEb21haW4gc2hvcnRjdXQ6IGBAZC5jb21gIG9yIGBkLmNvbWAgLT4gbWF0Y2ggdGhlIGFkZHJlc3MncyBkb21haW4uXG4gIGNvbnN0IGRvbWFpbiA9IHRyaW1tZWQucmVwbGFjZSgvXkAvLCBcIlwiKS50b0xvd2VyQ2FzZSgpO1xuICBjb25zdCBhdCA9IGFkZHJlc3MubGFzdEluZGV4T2YoXCJAXCIpO1xuICByZXR1cm4gYXQgPj0gMCAmJiBhZGRyZXNzLnNsaWNlKGF0ICsgMSkgPT09IGRvbWFpbjtcbn1cblxuLyoqXG4gKiBUcnVlIHdoZW4gYGVtYWlsYCBpcyBhbGxvd2VkIGJ5IEFOWSBwYXR0ZXJuIGluIGBwYXR0ZXJuc2AuIEFuIGVtcHR5IChvclxuICogbWlzc2luZykgbGlzdCBhbGxvd3Mgbm9ib2R5IC0gdGhlIGdhdGUgZmFpbHMgY2xvc2VkLlxuICovXG5leHBvcnQgZnVuY3Rpb24gbWF0Y2hlc0FsbG93bGlzdChlbWFpbDogc3RyaW5nLCBwYXR0ZXJuczogcmVhZG9ubHkgc3RyaW5nW10gfCB1bmRlZmluZWQpOiBib29sZWFuIHtcbiAgaWYgKCFlbWFpbCB8fCAhcGF0dGVybnMgfHwgcGF0dGVybnMubGVuZ3RoID09PSAwKSByZXR1cm4gZmFsc2U7XG4gIHJldHVybiBwYXR0ZXJucy5zb21lKChwYXR0ZXJuKSA9PiBtYXRjaGVzUGF0dGVybihlbWFpbCwgcGF0dGVybikpO1xufVxuXG4vKiogUm91Z2ggc2hhcGUgY2hlY2sgc28gYSBjbGVhcmx5LWludmFsaWQgYWRkcmVzcyBpcyByZWplY3RlZCBiZWZvcmUgYW55IHdvcmsuICovXG5leHBvcnQgZnVuY3Rpb24gbG9va3NMaWtlRW1haWwodmFsdWU6IHN0cmluZyk6IGJvb2xlYW4ge1xuICByZXR1cm4gL15bXlxcc0BdK0BbXlxcc0BdK1xcLlteXFxzQF0rJC8udGVzdCh2YWx1ZS50cmltKCkpO1xufVxuIl19
@@ -1,91 +0,0 @@
1
- /**
2
- * The email-OTP access gate: runtime + Express middleware + request handlers.
3
- *
4
- * {@link AuthGate} owns the allow-list, rate limiters, code store, and session
5
- * signing. The plugin wires four routes to it (`request`/`verify`/`logout`/
6
- * `status`) and mounts {@link AuthGate.middleware} so every OTHER request needs a
7
- * valid session cookie.
8
- *
9
- * Design points:
10
- * - `request` ALWAYS resolves `{ ok: true }` (anti-enumeration): a code is only
11
- * generated + emailed when the address is allow-listed AND under the rate
12
- * limit, but the caller can't tell the difference.
13
- * - the session lives in an HttpOnly + SameSite=Lax cookie (Secure in prod), so
14
- * it survives reloads and is not readable by page scripts.
15
- * - fail-open on a missing signing secret (see `otp.ts`): a Databricks App is
16
- * already access-limited; an unset `AUTH_JWT_SECRET` degrades to
17
- * non-durable sessions, not a lockout.
18
- *
19
- * @module
20
- */
21
- import type { CookieOptions, NextFunction, Request, Response } from "express";
22
- import type { AuthStatus } from "@dbx-tools/shared-email";
23
- /** Cookie the session JWT rides in. */
24
- export declare const SESSION_COOKIE = "dbx_auth";
25
- /** Resolved gate configuration. */
26
- export interface AuthGateOptions {
27
- /** Allow-list patterns (domain / glob / `/regex/`). Empty = allow nobody. */
28
- readonly allow: readonly string[];
29
- /** Session lifetime in seconds. */
30
- readonly sessionTtlSeconds: number;
31
- /** One-time code lifetime in seconds. */
32
- readonly codeTtlSeconds: number;
33
- /** Max verify attempts per issued code. */
34
- readonly maxAttempts: number;
35
- /** Send the code email. Returns nothing; failures are logged, not surfaced. */
36
- readonly sendCode: (email: string, code: string) => Promise<void>;
37
- /** True in production (sets the `Secure` cookie flag). */
38
- readonly secureCookies: boolean;
39
- }
40
- /** The email-OTP gate for one app. */
41
- export declare class AuthGate {
42
- private readonly options;
43
- private readonly codes;
44
- private readonly requestLimiter;
45
- private readonly verifyLimiter;
46
- constructor(options: AuthGateOptions);
47
- /**
48
- * Handle `POST /auth/request`. Always resolves `{ ok: true }` (plus a
49
- * `retryAfter` when rate-limited); a code is issued + emailed only for an
50
- * allow-listed address under the limit. Errors in sending are swallowed so the
51
- * response never reveals whether an address exists / is allowed.
52
- */
53
- handleRequest(email: string, ip: string): Promise<{
54
- ok: true;
55
- retryAfter?: number;
56
- }>;
57
- /**
58
- * Handle `POST /auth/verify`. On a correct code, returns the session `token`
59
- * plus the `cookieOptions` for it so the route sets it with Express's own
60
- * `res.cookie(SESSION_COOKIE, token, options)` (no hand-rolled Set-Cookie).
61
- * Failures return `{ ok: false }` with a generic reason.
62
- */
63
- handleVerify(email: string, code: string, ip: string, secure: boolean): Promise<{
64
- ok: boolean;
65
- token?: string;
66
- cookieOptions?: CookieOptions;
67
- retryAfter?: number;
68
- }>;
69
- /** Express cookie options for the session (used with `res.cookie`/`res.clearCookie`). */
70
- cookieOptions(secure: boolean): CookieOptions;
71
- /** Resolve the authenticated email for a request, or `undefined`. */
72
- authenticate(req: Request): Promise<string | undefined>;
73
- /** The `GET /auth/status` payload for a request. */
74
- status(req: Request): Promise<AuthStatus>;
75
- /**
76
- * Express middleware gating the app's DATA APIs behind a session.
77
- *
78
- * It gates `/api/*` and returns 401 for an unauthenticated caller - EXCEPT the
79
- * login flow itself (`<emailBase>/auth/*`), which must stay open so a caller
80
- * can obtain a session. Static assets (the SPA shell, JS/CSS, favicons) are
81
- * NOT gated: the browser has to load the client so the `<AuthGate>` React
82
- * component can render the login screen and call these endpoints. A gated
83
- * `/api` request from the un-logged-in SPA simply 401s, which the client
84
- * treats as "show the login". This is the standard SPA gate shape - protect
85
- * the data, serve the shell.
86
- *
87
- * `emailBase` is the email plugin's mount path (e.g. `/api/email`), so both the
88
- * open login prefix and the gated API prefix are matched on the full path.
89
- */
90
- middleware(emailBase: string): (req: Request, res: Response, next: NextFunction) => void;
91
- }
@@ -1,143 +0,0 @@
1
- /**
2
- * The email-OTP access gate: runtime + Express middleware + request handlers.
3
- *
4
- * {@link AuthGate} owns the allow-list, rate limiters, code store, and session
5
- * signing. The plugin wires four routes to it (`request`/`verify`/`logout`/
6
- * `status`) and mounts {@link AuthGate.middleware} so every OTHER request needs a
7
- * valid session cookie.
8
- *
9
- * Design points:
10
- * - `request` ALWAYS resolves `{ ok: true }` (anti-enumeration): a code is only
11
- * generated + emailed when the address is allow-listed AND under the rate
12
- * limit, but the caller can't tell the difference.
13
- * - the session lives in an HttpOnly + SameSite=Lax cookie (Secure in prod), so
14
- * it survives reloads and is not readable by page scripts.
15
- * - fail-open on a missing signing secret (see `otp.ts`): a Databricks App is
16
- * already access-limited; an unset `AUTH_JWT_SECRET` degrades to
17
- * non-durable sessions, not a lockout.
18
- *
19
- * @module
20
- */
21
- import { http, log } from "@dbx-tools/shared-core";
22
- import { looksLikeEmail, matchesAllowlist } from "./allowlist.js";
23
- import { CodeStore, signSession, verifySession } from "./otp.js";
24
- import { RateLimiter } from "./rate-limit.js";
25
- const logger = log.logger("email:auth");
26
- /** Cookie the session JWT rides in. */
27
- export const SESSION_COOKIE = "dbx_auth";
28
- /** Route prefix (under the plugin base `/api/email`) the gate leaves open. */
29
- const AUTH_PATH_PREFIX = "/auth";
30
- /** The email-OTP gate for one app. */
31
- export class AuthGate {
32
- options;
33
- codes;
34
- // Separate limiters: requesting a code is cheap-to-abuse (email spam), verifying
35
- // is a brute-force surface. Per-email AND per-IP so neither axis alone is a bypass.
36
- requestLimiter = new RateLimiter(5, 15 * 60 * 1000);
37
- verifyLimiter = new RateLimiter(10, 15 * 60 * 1000);
38
- constructor(options) {
39
- this.options = options;
40
- this.codes = new CodeStore(options.codeTtlSeconds * 1000, options.maxAttempts);
41
- }
42
- /**
43
- * Handle `POST /auth/request`. Always resolves `{ ok: true }` (plus a
44
- * `retryAfter` when rate-limited); a code is issued + emailed only for an
45
- * allow-listed address under the limit. Errors in sending are swallowed so the
46
- * response never reveals whether an address exists / is allowed.
47
- */
48
- async handleRequest(email, ip) {
49
- const address = email.trim().toLowerCase();
50
- const byIp = this.requestLimiter.hit(`ip:${ip}`);
51
- const byEmail = this.requestLimiter.hit(`email:${address}`);
52
- if (!byIp.allowed || !byEmail.allowed) {
53
- return { ok: true, retryAfter: byIp.retryAfter ?? byEmail.retryAfter };
54
- }
55
- if (looksLikeEmail(address) && matchesAllowlist(address, this.options.allow)) {
56
- const code = this.codes.issue(address);
57
- try {
58
- await this.options.sendCode(address, code);
59
- }
60
- catch (error) {
61
- logger.warn("failed to send OTP email", { error });
62
- }
63
- }
64
- return { ok: true };
65
- }
66
- /**
67
- * Handle `POST /auth/verify`. On a correct code, returns the session `token`
68
- * plus the `cookieOptions` for it so the route sets it with Express's own
69
- * `res.cookie(SESSION_COOKIE, token, options)` (no hand-rolled Set-Cookie).
70
- * Failures return `{ ok: false }` with a generic reason.
71
- */
72
- async handleVerify(email, code, ip, secure) {
73
- const address = email.trim().toLowerCase();
74
- const byIp = this.verifyLimiter.hit(`ip:${ip}`);
75
- const byEmail = this.verifyLimiter.hit(`email:${address}`);
76
- if (!byIp.allowed || !byEmail.allowed) {
77
- return { ok: false, retryAfter: byIp.retryAfter ?? byEmail.retryAfter };
78
- }
79
- if (this.codes.verify(address, code.trim()) !== "ok")
80
- return { ok: false };
81
- // Correct code: clear the caller's request/verify budget and mint a session.
82
- this.requestLimiter.reset(`email:${address}`);
83
- this.verifyLimiter.reset(`email:${address}`);
84
- const token = await signSession(address, this.options.sessionTtlSeconds);
85
- return { ok: true, token, cookieOptions: this.cookieOptions(secure) };
86
- }
87
- /** Express cookie options for the session (used with `res.cookie`/`res.clearCookie`). */
88
- cookieOptions(secure) {
89
- return {
90
- httpOnly: true,
91
- sameSite: "lax",
92
- secure: secure || this.options.secureCookies,
93
- path: "/",
94
- maxAge: this.options.sessionTtlSeconds * 1000,
95
- };
96
- }
97
- /** Resolve the authenticated email for a request, or `undefined`. */
98
- async authenticate(req) {
99
- // Reuse shared-core's cookie parser (accepts an Express req directly) rather
100
- // than re-implementing header splitting here.
101
- const token = http.parseCookies(req)[SESSION_COOKIE];
102
- return verifySession(token);
103
- }
104
- /** The `GET /auth/status` payload for a request. */
105
- async status(req) {
106
- const email = await this.authenticate(req);
107
- return { authenticated: Boolean(email), email, enabled: true };
108
- }
109
- /**
110
- * Express middleware gating the app's DATA APIs behind a session.
111
- *
112
- * It gates `/api/*` and returns 401 for an unauthenticated caller - EXCEPT the
113
- * login flow itself (`<emailBase>/auth/*`), which must stay open so a caller
114
- * can obtain a session. Static assets (the SPA shell, JS/CSS, favicons) are
115
- * NOT gated: the browser has to load the client so the `<AuthGate>` React
116
- * component can render the login screen and call these endpoints. A gated
117
- * `/api` request from the un-logged-in SPA simply 401s, which the client
118
- * treats as "show the login". This is the standard SPA gate shape - protect
119
- * the data, serve the shell.
120
- *
121
- * `emailBase` is the email plugin's mount path (e.g. `/api/email`), so both the
122
- * open login prefix and the gated API prefix are matched on the full path.
123
- */
124
- middleware(emailBase) {
125
- const openPrefix = `${emailBase}${AUTH_PATH_PREFIX}`;
126
- return (req, res, next) => {
127
- // Only API traffic is gated; static assets load freely so the login UI can.
128
- if (!req.path.startsWith("/api/") || req.path.startsWith(openPrefix)) {
129
- next();
130
- return;
131
- }
132
- void this.authenticate(req).then((email) => {
133
- if (email) {
134
- next();
135
- }
136
- else {
137
- res.status(401).json({ error: "authentication required", loginPath: openPrefix });
138
- }
139
- });
140
- };
141
- }
142
- }
143
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZ2F0ZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9hdXRoL2dhdGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7R0FtQkc7QUFHSCxPQUFPLEVBQUUsSUFBSSxFQUFFLEdBQUcsRUFBRSxNQUFNLHdCQUF3QixDQUFDO0FBRW5ELE9BQU8sRUFBRSxjQUFjLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUNsRSxPQUFPLEVBQUUsU0FBUyxFQUFFLFdBQVcsRUFBRSxhQUFhLEVBQUUsTUFBTSxVQUFVLENBQUM7QUFDakUsT0FBTyxFQUFFLFdBQVcsRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBRTlDLE1BQU0sTUFBTSxHQUFHLEdBQUcsQ0FBQyxNQUFNLENBQUMsWUFBWSxDQUFDLENBQUM7QUFFeEMsdUNBQXVDO0FBQ3ZDLE1BQU0sQ0FBQyxNQUFNLGNBQWMsR0FBRyxVQUFVLENBQUM7QUFFekMsOEVBQThFO0FBQzlFLE1BQU0sZ0JBQWdCLEdBQUcsT0FBTyxDQUFDO0FBa0JqQyxzQ0FBc0M7QUFDdEMsTUFBTSxPQUFPLFFBQVE7SUFPVTtJQU5aLEtBQUssQ0FBWTtJQUNsQyxpRkFBaUY7SUFDakYsb0ZBQW9GO0lBQ25FLGNBQWMsR0FBRyxJQUFJLFdBQVcsQ0FBQyxDQUFDLEVBQUUsRUFBRSxHQUFHLEVBQUUsR0FBRyxJQUFJLENBQUMsQ0FBQztJQUNwRCxhQUFhLEdBQUcsSUFBSSxXQUFXLENBQUMsRUFBRSxFQUFFLEVBQUUsR0FBRyxFQUFFLEdBQUcsSUFBSSxDQUFDLENBQUM7SUFFckUsWUFBNkIsT0FBd0I7UUFBeEIsWUFBTyxHQUFQLE9BQU8sQ0FBaUI7UUFDbkQsSUFBSSxDQUFDLEtBQUssR0FBRyxJQUFJLFNBQVMsQ0FBQyxPQUFPLENBQUMsY0FBYyxHQUFHLElBQUksRUFBRSxPQUFPLENBQUMsV0FBVyxDQUFDLENBQUM7SUFDakYsQ0FBQztJQUVEOzs7OztP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WSxDQUFDLEdBQUcsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDLEtBQUssRUFBRSxFQUFFO2dCQUN6QyxJQUFJLEtBQUssRUFBRSxDQUFDO29CQUNWLElBQUksRUFBRSxDQUFDO2dCQUNULENBQUM7cUJBQU0sQ0FBQztvQkFDTixHQUFHLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDLElBQUksQ0FBQyxFQUFFLEtBQUssRUFBRSx5QkFBeUIsRUFBRSxTQUFTLEVBQUUsVUFBVSxFQUFFLENBQUMsQ0FBQztnQkFDcEYsQ0FBQztZQUNILENBQUMsQ0FBQyxDQUFDO1FBQ0wsQ0FBQyxDQUFDO0lBQ0osQ0FBQztDQUNGIiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBUaGUgZW1haWwtT1RQIGFjY2VzcyBnYXRlOiBydW50aW1lICsgRXhwcmVzcyBtaWRkbGV3YXJlICsgcmVxdWVzdCBoYW5kbGVycy5cbiAqXG4gKiB7QGxpbmsgQXV0aEdhdGV9IG93bnMgdGhlIGFsbG93LWxpc3QsIHJhdGUgbGltaXRlcnMsIGNvZGUgc3RvcmUsIGFuZCBzZXNzaW9uXG4gKiBzaWduaW5nLiBUaGUgcGx1Z2luIHdpcmVzIGZvdXIgcm91dGVzIHRvIGl0IChgcmVxdWVzdGAvYHZlcmlmeWAvYGxvZ291dGAvXG4gKiBgc3RhdHVzYCkgYW5kIG1vdW50cyB7QGxpbmsgQXV0aEdhdGUubWlkZGxld2FyZX0gc28gZXZlcnkgT1RIRVIgcmVxdWVzdCBuZWVkcyBhXG4gKiB2YWxpZCBzZXNzaW9uIGNvb2tpZS5cbiAqXG4gKiBEZXNpZ24gcG9pbnRzOlxuICogICAtIGByZXF1ZXN0YCBBTFdBWVMgcmVzb2x2ZXMgYHsgb2s6IHRydWUgfWAgKGFudGktZW51bWVyYXRpb24pOiBhIGNvZGUgaXMgb25seVxuICogICAgIGdlbmVyYXRlZCArIGVtYWlsZWQgd2hlbiB0aGUgYWRkcmVzcyBpcyBhbGxvdy1saXN0ZWQgQU5EIHVuZGVyIHRoZSByYXRlXG4gKiAgICAgbGltaXQsIGJ1dCB0aGUgY2FsbGVyIGNhbid0IHRlbGwgdGhlIGRpZmZlcmVuY2UuXG4gKiAgIC0gdGhlIHNlc3Npb24gbGl2ZXMgaW4gYW4gSHR0cE9ubHkgKyBTYW1lU2l0ZT1MYXggY29va2llIChTZWN1cmUgaW4gcHJvZCksIHNvXG4gKiAgICAgaXQgc3Vydml2ZXMgcmVsb2FkcyBhbmQgaXMgbm90IHJlYWRhYmxlIGJ5IHBhZ2Ugc2NyaXB0cy5cbiAqICAgLSBmYWlsLW9wZW4gb24gYSBtaXNzaW5nIHNpZ25pbmcgc2VjcmV0IChzZWUgYG90cC50c2ApOiBhIERhdGFicmlja3MgQXBwIGlzXG4gKiAgICAgYWxyZWFkeSBhY2Nlc3MtbGltaXRlZDsgYW4gdW5zZXQgYEFVVEhfSldUX1NFQ1JFVGAgZGVncmFkZXMgdG9cbiAqICAgICBub24tZHVyYWJsZSBzZXNzaW9ucywgbm90IGEgbG9ja291dC5cbiAqXG4gKiBAbW9kdWxlXG4gKi9cblxuaW1wb3J0IHR5cGUgeyBDb29raWVPcHRpb25zLCBOZXh0RnVuY3Rpb24sIFJlcXVlc3QsIFJlc3BvbnNlIH0gZnJvbSBcImV4cHJlc3NcIjtcbmltcG9ydCB7IGh0dHAsIGxvZyB9IGZyb20gXCJAZGJ4LXRvb2xzL3NoYXJlZC1jb3JlXCI7XG5pbXBvcnQgdHlwZSB7IEF1dGhTdGF0dXMgfSBmcm9tIFwiQGRieC10b29scy9zaGFyZWQtZW1haWxcIjtcbmltcG9ydCB7IGxvb2tzTGlrZUVtYWlsLCBtYXRjaGVzQWxsb3dsaXN0IH0gZnJvbSBcIi4vYWxsb3dsaXN0LnRzXCI7XG5pbXBvcnQgeyBDb2RlU3RvcmUsIHNpZ25TZXNzaW9uLCB2ZXJpZnlTZXNzaW9uIH0gZnJvbSBcIi4vb3RwLnRzXCI7XG5pbXBvcnQgeyBSYXRlTGltaXRlciB9IGZyb20gXCIuL3JhdGUtbGltaXQudHNcIjtcblxuY29uc3QgbG9nZ2VyID0gbG9nLmxvZ2dlcihcImVtYWlsOmF1dGhcIik7XG5cbi8qKiBDb29raWUgdGhlIHNlc3Npb24gSldUIHJpZGVzIGluLiAqL1xuZXhwb3J0IGNvbnN0IFNFU1NJT05fQ09PS0lFID0gXCJkYnhfYXV0aFwiO1xuXG4vKiogUm91dGUgcHJlZml4ICh1bmRlciB0aGUgcGx1Z2luIGJhc2UgYC9hcGkvZW1haWxgKSB0aGUgZ2F0ZSBsZWF2ZXMgb3Blbi4gKi9cbmNvbnN0IEFVVEhfUEFUSF9QUkVGSVggPSBcIi9hdXRoXCI7XG5cbi8qKiBSZXNvbHZlZCBnYXRlIGNvbmZpZ3VyYXRpb24uICovXG5leHBvcnQgaW50ZXJmYWNlIEF1dGhHYXRlT3B0aW9ucyB7XG4gIC8qKiBBbGxvdy1saXN0IHBhdHRlcm5zIChkb21haW4gLyBnbG9iIC8gYC9yZWdleC9gKS4gRW1wdHkgPSBhbGxvdyBub2JvZHkuICovXG4gIHJlYWRvbmx5IGFsbG93OiByZWFkb25seSBzdHJpbmdbXTtcbiAgLyoqIFNlc3Npb24gbGlmZXRpbWUgaW4gc2Vjb25kcy4gKi9cbiAgcmVhZG9ubHkgc2Vzc2lvblR0bFNlY29uZHM6IG51bWJlcjtcbiAgLyoqIE9uZS10aW1lIGNvZGUgbGlmZXRpbWUgaW4gc2Vjb25kcy4gKi9cbiAgcmVhZG9ubHkgY29kZVR0bFNlY29uZHM6IG51bWJlcjtcbiAgLyoqIE1heCB2ZXJpZnkgYXR0ZW1wdHMgcGVyIGlzc3VlZCBjb2RlLiAqL1xuICByZWFkb25seSBtYXhBdHRlbXB0czogbnVtYmVyO1xuICAvKiogU2VuZCB0aGUgY29kZSBlbWFpbC4gUmV0dXJucyBub3RoaW5nOyBmYWlsdXJlcyBhcmUgbG9nZ2VkLCBub3Qgc3VyZmFjZWQuICovXG4gIHJlYWRvbmx5IHNlbmRDb2RlOiAoZW1haWw6IHN0cmluZywgY29kZTogc3RyaW5nKSA9PiBQcm9taXNlPHZvaWQ+O1xuICAvKiogVHJ1ZSBpbiBwcm9kdWN0aW9uIChzZXRzIHRoZSBgU2VjdXJlYCBjb29raWUgZmxhZykuICovXG4gIHJlYWRvbmx5IHNlY3VyZUNvb2tpZXM6IGJvb2xlYW47XG59XG5cbi8qKiBUaGUgZW1haWwtT1RQIGdhdGUgZm9yIG9uZSBhcHAuICovXG5leHBvcnQgY2xhc3MgQXV0aEdhdGUge1xuICBwcml2YXRlIHJlYWRvbmx5IGNvZGVzOiBDb2RlU3RvcmU7XG4gIC8vIFNlcGFyYXRlIGxpbWl0ZXJzOiByZXF1ZXN0aW5nIGEgY29kZSBpcyBjaGVhcC10by1hYnVzZSAoZW1haWwgc3BhbSksIHZlcmlmeWluZ1xuICAvLyBpcyBhIGJydXRlLWZvcmNlIHN1cmZhY2UuIFBlci1lbWFpbCBBTkQgcGVyLUlQIHNvIG5laXRoZXIgYXhpcyBhbG9uZSBpcyBhIGJ5cGFzcy5cbiAgcHJpdmF0ZSByZWFkb25seSByZXF1ZXN0TGltaXRlciA9IG5ldyBSYXRlTGltaXRlcig1LCAxNSAqIDYwICogMTAwMCk7XG4gIHByaXZhdGUgcmVhZG9ubHkgdmVyaWZ5TGltaXRlciA9IG5ldyBSYXRlTGltaXRlcigxMCwgMTUgKiA2MCAqIDEwMDApO1xuXG4gIGNvbnN0cnVjdG9yKHByaXZhdGUgcmVhZG9ubHkgb3B0aW9uczogQXV0aEdhdGVPcHRpb25zKSB7XG4gICAgdGhpcy5jb2RlcyA9IG5ldyBDb2RlU3RvcmUob3B0aW9ucy5jb2RlVHRsU2Vjb25kcyAqIDEwMDAsIG9wdGlvbnMubWF4QXR0ZW1wdHMpO1xuICB9XG5cbiAgLyoqXG4gICAqIEhhbmRsZSBgUE9TVCAvYXV0aC9yZXF1ZXN0YC4gQWx3YXlzIHJlc29sdmVzIGB7IG9rOiB0cnVlIH1gIChwbHVzIGFcbiAgICogYHJldHJ5QWZ0ZXJgIHdoZW4gcmF0ZS1saW1pdGVkKTsgYSBjb2RlIGlzIGlzc3VlZCArIGVtYWlsZWQgb25seSBmb3IgYW5cbiAgICogYWxsb3ctbGlzdGVkIGFkZHJlc3MgdW5kZXIgdGhlIGxpbWl0LiBFcnJvcnMgaW4gc2VuZGluZyBhcmUgc3dhbGxvd2VkIHNvIHRoZVxuICAgKiByZXNwb25zZSBuZXZlciByZXZlYWxzIHdoZXRoZXIgYW4gYWRkcmVzcyBleGlzdHMgLyBpcyBhbGxvd2VkLlxuICAgKi9cbiAgYXN5bmMgaGFuZGxlUmVxdWVzdChlbWFpbDogc3RyaW5nLCBpcDogc3RyaW5nKTogUHJvbWlzZTx7IG9rOiB0cnVlOyByZXRyeUFmdGVyPzogbnVtYmVyIH0+IHtcbiAgICBjb25zdCBhZGRyZXNzID0gZW1haWwudHJpbSgpLnRvTG93ZXJDYXNlKCk7XG4gICAgY29uc3QgYnlJcCA9IHRoaXMucmVxdWVzdExpbWl0ZXIuaGl0KGBpcDoke2lwfWApO1xuICAgIGNvbnN0IGJ5RW1haWwgPSB0aGlzLnJlcXVlc3RMaW1pdGVyLmhpdChgZW1haWw6JHthZGRyZXNzfWApO1xuICAgIGlmICghYnlJcC5hbGxvd2VkIHx8ICFieUVtYWlsLmFsbG93ZWQpIHtcbiAgICAgIHJldHVybiB7IG9rOiB0cnVlLCByZXRyeUFmdGVyOiBieUlwLnJldHJ5QWZ0ZXIgPz8gYnlFbWFpbC5yZXRyeUFmdGVyIH07XG4gICAgfVxuICAgIGlmIChsb29rc0xpa2VFbWFpbChhZGRyZXNzKSAmJiBtYXRjaGVzQWxsb3dsaXN0KGFkZHJlc3MsIHRoaXMub3B0aW9ucy5hbGxvdykpIHtcbiAgICAgIGNvbnN0IGNvZGUgPSB0aGlzLmNvZGVzLmlzc3VlKGFkZHJlc3MpO1xuICAgICAgdHJ5IHtcbiAgICAgICAgYXdhaXQgdGhpcy5vcHRpb25zLnNlbmRDb2RlKGFkZHJlc3MsIGNvZGUpO1xuICAgICAgfSBjYXRjaCAoZXJyb3IpIHtcbiAgICAgICAgbG9nZ2VyLndhcm4oXCJmYWlsZWQgdG8gc2VuZCBPVFAgZW1haWxcIiwgeyBlcnJvciB9KTtcbiAgICAgIH1cbiAgICB9XG4gICAgcmV0dXJuIHsgb2s6IHRydWUgfTtcbiAgfVxuXG4gIC8qKlxuICAgKiBIYW5kbGUgYFBPU1QgL2F1dGgvdmVyaWZ5YC4gT24gYSBjb3JyZWN0IGNvZGUsIHJldHVybnMgdGhlIHNlc3Npb24gYHRva2VuYFxuICAgKiBwbHVzIHRoZSBgY29va2llT3B0aW9uc2AgZm9yIGl0IHNvIHRoZSByb3V0ZSBzZXRzIGl0IHdpdGggRXhwcmVzcydzIG93blxuICAgKiBgcmVzLmNvb2tpZShTRVNTSU9OX0NPT0tJRSwgdG9rZW4sIG9wdGlvbnMpYCAobm8gaGFuZC1yb2xsZWQgU2V0LUNvb2tpZSkuXG4gICAqIEZhaWx1cmVzIHJldHVybiBgeyBvazogZmFsc2UgfWAgd2l0aCBhIGdlbmVyaWMgcmVhc29uLlxuICAgKi9cbiAgYXN5bmMgaGFuZGxlVmVyaWZ5KFxuICAgIGVtYWlsOiBzdHJpbmcsXG4gICAgY29kZTogc3RyaW5nLFxuICAgIGlwOiBzdHJpbmcsXG4gICAgc2VjdXJlOiBib29sZWFuLFxuICApOiBQcm9taXNlPHsgb2s6IGJvb2xlYW47IHRva2VuPzogc3RyaW5nOyBjb29raWVPcHRpb25zPzogQ29va2llT3B0aW9uczsgcmV0cnlBZnRlcj86IG51bWJlciB9PiB7XG4gICAgY29uc3QgYWRkcmVzcyA9IGVtYWlsLnRyaW0oKS50b0xvd2VyQ2FzZSgpO1xuICAgIGNvbnN0IGJ5SXAgPSB0aGlzLnZlcmlmeUxpbWl0ZXIuaGl0KGBpcDoke2lwfWApO1xuICAgIGNvbnN0IGJ5RW1haWwgPSB0aGlzLnZlcmlmeUxpbWl0ZXIuaGl0KGBlbWFpbDoke2FkZHJlc3N9YCk7XG4gICAgaWYgKCFieUlwLmFsbG93ZWQgfHwgIWJ5RW1haWwuYWxsb3dlZCkge1xuICAgICAgcmV0dXJuIHsgb2s6IGZhbHNlLCByZXRyeUFmdGVyOiBieUlwLnJldHJ5QWZ0ZXIgPz8gYnlFbWFpbC5yZXRyeUFmdGVyIH07XG4gICAgfVxuICAgIGlmICh0aGlzLmNvZGVzLnZlcmlmeShhZGRyZXNzLCBjb2RlLnRyaW0oKSkgIT09IFwib2tcIikgcmV0dXJuIHsgb2s6IGZhbHNlIH07XG4gICAgLy8gQ29ycmVjdCBjb2RlOiBjbGVhciB0aGUgY2FsbGVyJ3MgcmVxdWVzdC92ZXJpZnkgYnVkZ2V0IGFuZCBtaW50IGEgc2Vzc2lvbi5cbiAgICB0aGlzLnJlcXVlc3RMaW1pdGVyLnJlc2V0KGBlbWFpbDoke2FkZHJlc3N9YCk7XG4gICAgdGhpcy52ZXJpZnlMaW1pdGVyLnJlc2V0KGBlbWFpbDoke2FkZHJlc3N9YCk7XG4gICAgY29uc3QgdG9rZW4gPSBhd2FpdCBzaWduU2Vzc2lvbihhZGRyZXNzLCB0aGlzLm9wdGlvbnMuc2Vzc2lvblR0bFNlY29uZHMpO1xuICAgIHJldHVybiB7IG9rOiB0cnVlLCB0b2tlbiwgY29va2llT3B0aW9uczogdGhpcy5jb29raWVPcHRpb25zKHNlY3VyZSkgfTtcbiAgfVxuXG4gIC8qKiBFeHByZXNzIGNvb2tpZSBvcHRpb25zIGZvciB0aGUgc2Vzc2lvbiAodXNlZCB3aXRoIGByZXMuY29va2llYC9gcmVzLmNsZWFyQ29va2llYCkuICovXG4gIGNvb2tpZU9wdGlvbnMoc2VjdXJlOiBib29sZWFuKTogQ29va2llT3B0aW9ucyB7XG4gICAgcmV0dXJuIHtcbiAgICAgIGh0dHBPbmx5OiB0cnVlLFxuICAgICAgc2FtZVNpdGU6IFwibGF4XCIsXG4gICAgICBzZWN1cmU6IHNlY3VyZSB8fCB0aGlzLm9wdGlvbnMuc2VjdXJlQ29va2llcyxcbiAgICAgIHBhdGg6IFwiL1wiLFxuICAgICAgbWF4QWdlOiB0aGlzLm9wdGlvbnMuc2Vzc2lvblR0bFNlY29uZHMgKiAxMDAwLFxuICAgIH07XG4gIH1cblxuICAvKiogUmVzb2x2ZSB0aGUgYXV0aGVudGljYXRlZCBlbWFpbCBmb3IgYSByZXF1ZXN0LCBvciBgdW5kZWZpbmVkYC4gKi9cbiAgYXN5bmMgYXV0aGVudGljYXRlKHJlcTogUmVxdWVzdCk6IFByb21pc2U8c3RyaW5nIHwgdW5kZWZpbmVkPiB7XG4gICAgLy8gUmV1c2Ugc2hhcmVkLWNvcmUncyBjb29raWUgcGFyc2VyIChhY2NlcHRzIGFuIEV4cHJlc3MgcmVxIGRpcmVjdGx5KSByYXRoZXJcbiAgICAvLyB0aGFuIHJlLWltcGxlbWVudGluZyBoZWFkZXIgc3BsaXR0aW5nIGhlcmUuXG4gICAgY29uc3QgdG9rZW4gPSBodHRwLnBhcnNlQ29va2llcyhyZXEpW1NFU1NJT05fQ09PS0lFXTtcbiAgICByZXR1cm4gdmVyaWZ5U2Vzc2lvbih0b2tlbik7XG4gIH1cblxuICAvKiogVGhlIGBHRVQgL2F1dGgvc3RhdHVzYCBwYXlsb2FkIGZvciBhIHJlcXVlc3QuICovXG4gIGFzeW5jIHN0YXR1cyhyZXE6IFJlcXVlc3QpOiBQcm9taXNlPEF1dGhTdGF0dXM+IHtcbiAgICBjb25zdCBlbWFpbCA9IGF3YWl0IHRoaXMuYXV0aGVudGljYXRlKHJlcSk7XG4gICAgcmV0dXJuIHsgYXV0aGVudGljYXRlZDogQm9vbGVhbihlbWFpbCksIGVtYWlsLCBlbmFibGVkOiB0cnVlIH07XG4gIH1cblxuICAvKipcbiAgICogRXhwcmVzcyBtaWRkbGV3YXJlIGdhdGluZyB0aGUgYXBwJ3MgREFUQSBBUElzIGJlaGluZCBhIHNlc3Npb24uXG4gICAqXG4gICAqIEl0IGdhdGVzIGAvYXBpLypgIGFuZCByZXR1cm5zIDQwMSBmb3IgYW4gdW5hdXRoZW50aWNhdGVkIGNhbGxlciAtIEVYQ0VQVCB0aGVcbiAgICogbG9naW4gZmxvdyBpdHNlbGYgKGA8ZW1haWxCYXNlPi9hdXRoLypgKSwgd2hpY2ggbXVzdCBzdGF5IG9wZW4gc28gYSBjYWxsZXJcbiAgICogY2FuIG9idGFpbiBhIHNlc3Npb24uIFN0YXRpYyBhc3NldHMgKHRoZSBTUEEgc2hlbGwsIEpTL0NTUywgZmF2aWNvbnMpIGFyZVxuICAgKiBOT1QgZ2F0ZWQ6IHRoZSBicm93c2VyIGhhcyB0byBsb2FkIHRoZSBjbGllbnQgc28gdGhlIGA8QXV0aEdhdGU+YCBSZWFjdFxuICAgKiBjb21wb25lbnQgY2FuIHJlbmRlciB0aGUgbG9naW4gc2NyZWVuIGFuZCBjYWxsIHRoZXNlIGVuZHBvaW50cy4gQSBnYXRlZFxuICAgKiBgL2FwaWAgcmVxdWVzdCBmcm9tIHRoZSB1bi1sb2dnZWQtaW4gU1BBIHNpbXBseSA0MDFzLCB3aGljaCB0aGUgY2xpZW50XG4gICAqIHRyZWF0cyBhcyBcInNob3cgdGhlIGxvZ2luXCIuIFRoaXMgaXMgdGhlIHN0YW5kYXJkIFNQQSBnYXRlIHNoYXBlIC0gcHJvdGVjdFxuICAgKiB0aGUgZGF0YSwgc2VydmUgdGhlIHNoZWxsLlxuICAgKlxuICAgKiBgZW1haWxCYXNlYCBpcyB0aGUgZW1haWwgcGx1Z2luJ3MgbW91bnQgcGF0aCAoZS5nLiBgL2FwaS9lbWFpbGApLCBzbyBib3RoIHRoZVxuICAgKiBvcGVuIGxvZ2luIHByZWZpeCBhbmQgdGhlIGdhdGVkIEFQSSBwcmVmaXggYXJlIG1hdGNoZWQgb24gdGhlIGZ1bGwgcGF0aC5cbiAgICovXG4gIG1pZGRsZXdhcmUoZW1haWxCYXNlOiBzdHJpbmcpOiAocmVxOiBSZXF1ZXN0LCByZXM6IFJlc3BvbnNlLCBuZXh0OiBOZXh0RnVuY3Rpb24pID0+IHZvaWQge1xuICAgIGNvbnN0IG9wZW5QcmVmaXggPSBgJHtlbWFpbEJhc2V9JHtBVVRIX1BBVEhfUFJFRklYfWA7XG4gICAgcmV0dXJuIChyZXEsIHJlcywgbmV4dCkgPT4ge1xuICAgICAgLy8gT25seSBBUEkgdHJhZmZpYyBpcyBnYXRlZDsgc3RhdGljIGFzc2V0cyBsb2FkIGZyZWVseSBzbyB0aGUgbG9naW4gVUkgY2FuLlxuICAgICAgaWYgKCFyZXEucGF0aC5zdGFydHNXaXRoKFwiL2FwaS9cIikgfHwgcmVxLnBhdGguc3RhcnRzV2l0aChvcGVuUHJlZml4KSkge1xuICAgICAgICBuZXh0KCk7XG4gICAgICAgIHJldHVybjtcbiAgICAgIH1cbiAgICAgIHZvaWQgdGhpcy5hdXRoZW50aWNhdGUocmVxKS50aGVuKChlbWFpbCkgPT4ge1xuICAgICAgICBpZiAoZW1haWwpIHtcbiAgICAgICAgICBuZXh0KCk7XG4gICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgcmVzLnN0YXR1cyg0MDEpLmpzb24oeyBlcnJvcjogXCJhdXRoZW50aWNhdGlvbiByZXF1aXJlZFwiLCBsb2dpblBhdGg6IG9wZW5QcmVmaXggfSk7XG4gICAgICAgIH1cbiAgICAgIH0pO1xuICAgIH07XG4gIH1cbn1cbiJdfQ==
@@ -1,49 +0,0 @@
1
- /**
2
- * One-time-code store + session JWT for the email-OTP gate.
3
- *
4
- * Two pieces:
5
- *
6
- * - **Code store** - a 6-digit code is generated with `crypto.randomInt` and
7
- * kept server-side as a SHA-256 hash with an expiry and an attempt counter
8
- * (never the plaintext, never in the JWT). `verifyCode` is constant-time on
9
- * the hash, enforces the TTL, and burns the code after too many attempts or
10
- * one success. In-memory `Map` keyed by lowercased email - fine for a
11
- * single-instance app behind a tunnel.
12
- * - **Session JWT** - on a correct code, `signSession` mints a short-lived
13
- * HS256 JWT (via `jose`) carrying only the email; `verifySession` validates
14
- * it. The signing key comes from `AUTH_JWT_SECRET`; when unset the gate
15
- * FAILS OPEN with an ephemeral per-process key (sessions reset on restart)
16
- * rather than refusing service - a Databricks App is already access-limited,
17
- * so an unset secret degrades to "sessions don't survive restarts", not
18
- * "nobody can log in".
19
- *
20
- * @module
21
- */
22
- /** Result of {@link CodeStore.verify}. */
23
- export type VerifyOutcome = "ok" | "invalid" | "expired" | "too-many-attempts";
24
- /** In-memory store of pending one-time codes, keyed by lowercased email. */
25
- export declare class CodeStore {
26
- private readonly ttlMs;
27
- private readonly maxAttempts;
28
- private readonly codes;
29
- constructor(ttlMs: number, maxAttempts: number);
30
- /**
31
- * Generate, store (hashed), and RETURN a fresh 6-digit code for `email`. The
32
- * caller emails the returned plaintext; only the hash is retained. Replaces
33
- * any pending code for the address.
34
- */
35
- issue(email: string, now?: number): string;
36
- /**
37
- * Check `code` for `email`. Consumes the entry on success or when attempts are
38
- * exhausted, so a code is single-use and can't be brute-forced past the cap.
39
- */
40
- verify(email: string, code: string, now?: number): VerifyOutcome;
41
- /** Drop every pending code (tests). */
42
- clear(): void;
43
- }
44
- /** Reset the memoized key (tests, or after changing the env in-process). */
45
- export declare function resetSigningKey(): void;
46
- /** Mint a short-lived session JWT for `email`, expiring in `ttlSeconds`. */
47
- export declare function signSession(email: string, ttlSeconds: number): Promise<string>;
48
- /** Validate a session JWT, returning the email it was minted for, or `undefined`. */
49
- export declare function verifySession(token: string | undefined): Promise<string | undefined>;