@dbx-tools/email 0.6.44 → 0.6.45
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/index.ts +1 -10
- package/lib/index.d.ts +1 -10
- package/lib/index.js +1 -8
- package/lib/src/config.d.ts +0 -40
- package/lib/src/config.js +2 -54
- package/lib/src/plugin.d.ts +0 -23
- package/lib/src/plugin.js +2 -143
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -4
- package/src/config.ts +1 -90
- package/src/plugin.ts +1 -163
- package/lib/src/auth/allowlist.d.ts +0 -27
- package/lib/src/auth/allowlist.js +0 -82
- package/lib/src/auth/gate.d.ts +0 -91
- package/lib/src/auth/gate.js +0 -143
- package/lib/src/auth/otp.d.ts +0 -49
- package/lib/src/auth/otp.js +0 -135
- package/lib/src/auth/rate-limit.d.ts +0 -35
- package/lib/src/auth/rate-limit.js +0 -53
- package/src/auth/allowlist.ts +0 -88
- package/src/auth/gate.ts +0 -172
- package/src/auth/otp.ts +0 -151
- package/src/auth/rate-limit.ts +0 -59
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
|
|
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
|
package/lib/src/auth/gate.d.ts
DELETED
|
@@ -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
|
-
}
|
package/lib/src/auth/gate.js
DELETED
|
@@ -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==
|
package/lib/src/auth/otp.d.ts
DELETED
|
@@ -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>;
|