@dbx-tools/email 0.6.41 → 0.6.42
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 +10 -1
- package/lib/index.d.ts +10 -1
- package/lib/index.js +8 -1
- package/lib/src/auth/allowlist.d.ts +27 -0
- package/lib/src/auth/allowlist.js +82 -0
- package/lib/src/auth/gate.d.ts +83 -0
- package/lib/src/auth/gate.js +134 -0
- package/lib/src/auth/otp.d.ts +49 -0
- package/lib/src/auth/otp.js +135 -0
- package/lib/src/auth/rate-limit.d.ts +35 -0
- package/lib/src/auth/rate-limit.js +53 -0
- package/lib/src/config.d.ts +40 -0
- package/lib/src/config.js +54 -2
- package/lib/src/plugin.d.ts +23 -0
- package/lib/src/plugin.js +134 -2
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -3
- package/src/auth/allowlist.ts +88 -0
- package/src/auth/gate.ts +163 -0
- package/src/auth/otp.ts +151 -0
- package/src/auth/rate-limit.ts +59 -0
- package/src/config.ts +90 -1
- package/src/plugin.ts +153 -1
package/index.ts
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
// Regenerated from the exporting modules in ./src.
|
|
3
3
|
// Hand edits are overwritten on the next watch; this file is read-only.
|
|
4
4
|
|
|
5
|
+
export * as authAllowlist from "./src/auth/allowlist.ts";
|
|
6
|
+
export * as authGate from "./src/auth/gate.ts";
|
|
7
|
+
export * as authOtp from "./src/auth/otp.ts";
|
|
8
|
+
export * as authRateLimit from "./src/auth/rate-limit.ts";
|
|
5
9
|
export * as brand from "./src/brand.ts";
|
|
6
10
|
export * as config from "./src/config.ts";
|
|
7
11
|
export * as defaults from "./src/defaults.ts";
|
|
@@ -12,10 +16,15 @@ export * as plugin from "./src/plugin.ts";
|
|
|
12
16
|
export * as sender from "./src/sender.ts";
|
|
13
17
|
export * as tool from "./src/tool.ts";
|
|
14
18
|
export * as transport from "./src/transport.ts";
|
|
19
|
+
export { SESSION_COOKIE, AuthGate } from "./src/auth/gate.ts";
|
|
20
|
+
export type { AuthGateOptions } from "./src/auth/gate.ts";
|
|
21
|
+
export { CodeStore } from "./src/auth/otp.ts";
|
|
22
|
+
export type { VerifyOutcome } from "./src/auth/otp.ts";
|
|
23
|
+
export { RateLimiter } from "./src/auth/rate-limit.ts";
|
|
15
24
|
export { defaultEmailBrand } from "./src/brand.ts";
|
|
16
25
|
export type { EmailBrand } from "./src/brand.ts";
|
|
17
26
|
export { DEFAULT_SMTP_PORT, IMPLICIT_TLS_SMTP_PORT, EMAIL_CONFIG_SCHEMA } from "./src/config.ts";
|
|
18
|
-
export type { SenderPolicy, SmtpConfig, EmailPluginConfig, ResolvedSmtpConfig, ResolvedFileConfig, ResolvedEmailConfig } from "./src/config.ts";
|
|
27
|
+
export type { SenderPolicy, SmtpConfig, EmailPluginConfig, AuthConfig, ResolvedSmtpConfig, ResolvedFileConfig, ResolvedEmailConfig, ResolvedAuthConfig } from "./src/config.ts";
|
|
19
28
|
export { SEND_TIMEOUT_MS, VERIFY_TIMEOUT_MS, VERIFY_ATTEMPTS, MAX_ATTACHMENT_BYTES, MAX_ATTACHMENTS_TOTAL_BYTES, MAX_ATTACHMENT_COUNT, MAX_BODY_CHARS, EMAIL_SEND_SETTINGS, EMAIL_VERIFY_SETTINGS, EMAIL_SENDERS_SETTINGS } from "./src/defaults.ts";
|
|
20
29
|
export type { EmailExecuteConfig, EmailExecutionSettings } from "./src/defaults.ts";
|
|
21
30
|
export { escapeHtml } from "./src/email-html.ts";
|
package/lib/index.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
export * as authAllowlist from "./src/auth/allowlist.ts";
|
|
2
|
+
export * as authGate from "./src/auth/gate.ts";
|
|
3
|
+
export * as authOtp from "./src/auth/otp.ts";
|
|
4
|
+
export * as authRateLimit from "./src/auth/rate-limit.ts";
|
|
1
5
|
export * as brand from "./src/brand.ts";
|
|
2
6
|
export * as config from "./src/config.ts";
|
|
3
7
|
export * as defaults from "./src/defaults.ts";
|
|
@@ -8,10 +12,15 @@ export * as plugin from "./src/plugin.ts";
|
|
|
8
12
|
export * as sender from "./src/sender.ts";
|
|
9
13
|
export * as tool from "./src/tool.ts";
|
|
10
14
|
export * as transport from "./src/transport.ts";
|
|
15
|
+
export { SESSION_COOKIE, AuthGate } from "./src/auth/gate.ts";
|
|
16
|
+
export type { AuthGateOptions } from "./src/auth/gate.ts";
|
|
17
|
+
export { CodeStore } from "./src/auth/otp.ts";
|
|
18
|
+
export type { VerifyOutcome } from "./src/auth/otp.ts";
|
|
19
|
+
export { RateLimiter } from "./src/auth/rate-limit.ts";
|
|
11
20
|
export { defaultEmailBrand } from "./src/brand.ts";
|
|
12
21
|
export type { EmailBrand } from "./src/brand.ts";
|
|
13
22
|
export { DEFAULT_SMTP_PORT, IMPLICIT_TLS_SMTP_PORT, EMAIL_CONFIG_SCHEMA } from "./src/config.ts";
|
|
14
|
-
export type { SenderPolicy, SmtpConfig, EmailPluginConfig, ResolvedSmtpConfig, ResolvedFileConfig, ResolvedEmailConfig } from "./src/config.ts";
|
|
23
|
+
export type { SenderPolicy, SmtpConfig, EmailPluginConfig, AuthConfig, ResolvedSmtpConfig, ResolvedFileConfig, ResolvedEmailConfig, ResolvedAuthConfig } from "./src/config.ts";
|
|
15
24
|
export { SEND_TIMEOUT_MS, VERIFY_TIMEOUT_MS, VERIFY_ATTEMPTS, MAX_ATTACHMENT_BYTES, MAX_ATTACHMENTS_TOTAL_BYTES, MAX_ATTACHMENT_COUNT, MAX_BODY_CHARS, EMAIL_SEND_SETTINGS, EMAIL_VERIFY_SETTINGS, EMAIL_SENDERS_SETTINGS } from "./src/defaults.ts";
|
|
16
25
|
export type { EmailExecuteConfig, EmailExecutionSettings } from "./src/defaults.ts";
|
|
17
26
|
export { escapeHtml } from "./src/email-html.ts";
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// GENERATED by projen watch - DO NOT EDIT.
|
|
2
2
|
// Regenerated from the exporting modules in ./src.
|
|
3
3
|
// Hand edits are overwritten on the next watch; this file is read-only.
|
|
4
|
+
export * as authAllowlist from "./src/auth/allowlist.js";
|
|
5
|
+
export * as authGate from "./src/auth/gate.js";
|
|
6
|
+
export * as authOtp from "./src/auth/otp.js";
|
|
7
|
+
export * as authRateLimit from "./src/auth/rate-limit.js";
|
|
4
8
|
export * as brand from "./src/brand.js";
|
|
5
9
|
export * as config from "./src/config.js";
|
|
6
10
|
export * as defaults from "./src/defaults.js";
|
|
@@ -11,10 +15,13 @@ export * as plugin from "./src/plugin.js";
|
|
|
11
15
|
export * as sender from "./src/sender.js";
|
|
12
16
|
export * as tool from "./src/tool.js";
|
|
13
17
|
export * as transport from "./src/transport.js";
|
|
18
|
+
export { SESSION_COOKIE, AuthGate } from "./src/auth/gate.js";
|
|
19
|
+
export { CodeStore } from "./src/auth/otp.js";
|
|
20
|
+
export { RateLimiter } from "./src/auth/rate-limit.js";
|
|
14
21
|
export { defaultEmailBrand } from "./src/brand.js";
|
|
15
22
|
export { DEFAULT_SMTP_PORT, IMPLICIT_TLS_SMTP_PORT, EMAIL_CONFIG_SCHEMA } from "./src/config.js";
|
|
16
23
|
export { SEND_TIMEOUT_MS, VERIFY_TIMEOUT_MS, VERIFY_ATTEMPTS, MAX_ATTACHMENT_BYTES, MAX_ATTACHMENTS_TOTAL_BYTES, MAX_ATTACHMENT_COUNT, MAX_BODY_CHARS, EMAIL_SEND_SETTINGS, EMAIL_VERIFY_SETTINGS, EMAIL_SENDERS_SETTINGS } from "./src/defaults.js";
|
|
17
24
|
export { escapeHtml } from "./src/email-html.js";
|
|
18
25
|
export { EmailPlugin, email } from "./src/plugin.js";
|
|
19
26
|
export { SEND_EMAIL_DESCRIPTION } from "./src/tool.js";
|
|
20
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
27
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwyQ0FBMkM7QUFDM0MsbURBQW1EO0FBQ25ELHdFQUF3RTtBQUV4RSxPQUFPLEtBQUssYUFBYSxNQUFNLHlCQUF5QixDQUFDO0FBQ3pELE9BQU8sS0FBSyxRQUFRLE1BQU0sb0JBQW9CLENBQUM7QUFDL0MsT0FBTyxLQUFLLE9BQU8sTUFBTSxtQkFBbUIsQ0FBQztBQUM3QyxPQUFPLEtBQUssYUFBYSxNQUFNLDBCQUEwQixDQUFDO0FBQzFELE9BQU8sS0FBSyxLQUFLLE1BQU0sZ0JBQWdCLENBQUM7QUFDeEMsT0FBTyxLQUFLLE1BQU0sTUFBTSxpQkFBaUIsQ0FBQztBQUMxQyxPQUFPLEtBQUssUUFBUSxNQUFNLG1CQUFtQixDQUFDO0FBQzlDLE9BQU8sS0FBSyxTQUFTLE1BQU0scUJBQXFCLENBQUM7QUFDakQsT0FBTyxLQUFLLFFBQVEsTUFBTSxtQkFBbUIsQ0FBQztBQUM5QyxPQUFPLEtBQUssTUFBTSxNQUFNLGlCQUFpQixDQUFDO0FBQzFDLE9BQU8sS0FBSyxNQUFNLE1BQU0saUJBQWlCLENBQUM7QUFDMUMsT0FBTyxLQUFLLE1BQU0sTUFBTSxpQkFBaUIsQ0FBQztBQUMxQyxPQUFPLEtBQUssSUFBSSxNQUFNLGVBQWUsQ0FBQztBQUN0QyxPQUFPLEtBQUssU0FBUyxNQUFNLG9CQUFvQixDQUFDO0FBQ2hELE9BQU8sRUFBRSxjQUFjLEVBQUUsUUFBUSxFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFFOUQsT0FBTyxFQUFFLFNBQVMsRUFBRSxNQUFNLG1CQUFtQixDQUFDO0FBRTlDLE9BQU8sRUFBRSxXQUFXLEVBQUUsTUFBTSwwQkFBMEIsQ0FBQztBQUN2RCxPQUFPLEVBQUUsaUJBQWlCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUVuRCxPQUFPLEVBQUUsaUJBQWlCLEVBQUUsc0JBQXNCLEVBQUUsbUJBQW1CLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUVqRyxPQUFPLEVBQUUsZUFBZSxFQUFFLGlCQUFpQixFQUFFLGVBQWUsRUFBRSxvQkFBb0IsRUFBRSwyQkFBMkIsRUFBRSxvQkFBb0IsRUFBRSxjQUFjLEVBQUUsbUJBQW1CLEVBQUUscUJBQXFCLEVBQUUsc0JBQXNCLEVBQUUsTUFBTSxtQkFBbUIsQ0FBQztBQUVyUCxPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0scUJBQXFCLENBQUM7QUFFakQsT0FBTyxFQUFFLFdBQVcsRUFBRSxLQUFLLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUNyRCxPQUFPLEVBQUUsc0JBQXNCLEVBQUUsTUFBTSxlQUFlLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBHRU5FUkFURUQgYnkgcHJvamVuIHdhdGNoIC0gRE8gTk9UIEVESVQuXG4vLyBSZWdlbmVyYXRlZCBmcm9tIHRoZSBleHBvcnRpbmcgbW9kdWxlcyBpbiAuL3NyYy5cbi8vIEhhbmQgZWRpdHMgYXJlIG92ZXJ3cml0dGVuIG9uIHRoZSBuZXh0IHdhdGNoOyB0aGlzIGZpbGUgaXMgcmVhZC1vbmx5LlxuXG5leHBvcnQgKiBhcyBhdXRoQWxsb3dsaXN0IGZyb20gXCIuL3NyYy9hdXRoL2FsbG93bGlzdC50c1wiO1xuZXhwb3J0ICogYXMgYXV0aEdhdGUgZnJvbSBcIi4vc3JjL2F1dGgvZ2F0ZS50c1wiO1xuZXhwb3J0ICogYXMgYXV0aE90cCBmcm9tIFwiLi9zcmMvYXV0aC9vdHAudHNcIjtcbmV4cG9ydCAqIGFzIGF1dGhSYXRlTGltaXQgZnJvbSBcIi4vc3JjL2F1dGgvcmF0ZS1saW1pdC50c1wiO1xuZXhwb3J0ICogYXMgYnJhbmQgZnJvbSBcIi4vc3JjL2JyYW5kLnRzXCI7XG5leHBvcnQgKiBhcyBjb25maWcgZnJvbSBcIi4vc3JjL2NvbmZpZy50c1wiO1xuZXhwb3J0ICogYXMgZGVmYXVsdHMgZnJvbSBcIi4vc3JjL2RlZmF1bHRzLnRzXCI7XG5leHBvcnQgKiBhcyBlbWFpbEh0bWwgZnJvbSBcIi4vc3JjL2VtYWlsLWh0bWwudHNcIjtcbmV4cG9ydCAqIGFzIG1hcmtkb3duIGZyb20gXCIuL3NyYy9tYXJrZG93bi50c1wiO1xuZXhwb3J0ICogYXMgb3V0Ym94IGZyb20gXCIuL3NyYy9vdXRib3gudHNcIjtcbmV4cG9ydCAqIGFzIHBsdWdpbiBmcm9tIFwiLi9zcmMvcGx1Z2luLnRzXCI7XG5leHBvcnQgKiBhcyBzZW5kZXIgZnJvbSBcIi4vc3JjL3NlbmRlci50c1wiO1xuZXhwb3J0ICogYXMgdG9vbCBmcm9tIFwiLi9zcmMvdG9vbC50c1wiO1xuZXhwb3J0ICogYXMgdHJhbnNwb3J0IGZyb20gXCIuL3NyYy90cmFuc3BvcnQudHNcIjtcbmV4cG9ydCB7IFNFU1NJT05fQ09PS0lFLCBBdXRoR2F0ZSB9IGZyb20gXCIuL3NyYy9hdXRoL2dhdGUudHNcIjtcbmV4cG9ydCB0eXBlIHsgQXV0aEdhdGVPcHRpb25zIH0gZnJvbSBcIi4vc3JjL2F1dGgvZ2F0ZS50c1wiO1xuZXhwb3J0IHsgQ29kZVN0b3JlIH0gZnJvbSBcIi4vc3JjL2F1dGgvb3RwLnRzXCI7XG5leHBvcnQgdHlwZSB7IFZlcmlmeU91dGNvbWUgfSBmcm9tIFwiLi9zcmMvYXV0aC9vdHAudHNcIjtcbmV4cG9ydCB7IFJhdGVMaW1pdGVyIH0gZnJvbSBcIi4vc3JjL2F1dGgvcmF0ZS1saW1pdC50c1wiO1xuZXhwb3J0IHsgZGVmYXVsdEVtYWlsQnJhbmQgfSBmcm9tIFwiLi9zcmMvYnJhbmQudHNcIjtcbmV4cG9ydCB0eXBlIHsgRW1haWxCcmFuZCB9IGZyb20gXCIuL3NyYy9icmFuZC50c1wiO1xuZXhwb3J0IHsgREVGQVVMVF9TTVRQX1BPUlQsIElNUExJQ0lUX1RMU19TTVRQX1BPUlQsIEVNQUlMX0NPTkZJR19TQ0hFTUEgfSBmcm9tIFwiLi9zcmMvY29uZmlnLnRzXCI7XG5leHBvcnQgdHlwZSB7IFNlbmRlclBvbGljeSwgU210cENvbmZpZywgRW1haWxQbHVnaW5Db25maWcsIEF1dGhDb25maWcsIFJlc29sdmVkU210cENvbmZpZywgUmVzb2x2ZWRGaWxlQ29uZmlnLCBSZXNvbHZlZEVtYWlsQ29uZmlnLCBSZXNvbHZlZEF1dGhDb25maWcgfSBmcm9tIFwiLi9zcmMvY29uZmlnLnRzXCI7XG5leHBvcnQgeyBTRU5EX1RJTUVPVVRfTVMsIFZFUklGWV9USU1FT1VUX01TLCBWRVJJRllfQVRURU1QVFMsIE1BWF9BVFRBQ0hNRU5UX0JZVEVTLCBNQVhfQVRUQUNITUVOVFNfVE9UQUxfQllURVMsIE1BWF9BVFRBQ0hNRU5UX0NPVU5ULCBNQVhfQk9EWV9DSEFSUywgRU1BSUxfU0VORF9TRVRUSU5HUywgRU1BSUxfVkVSSUZZX1NFVFRJTkdTLCBFTUFJTF9TRU5ERVJTX1NFVFRJTkdTIH0gZnJvbSBcIi4vc3JjL2RlZmF1bHRzLnRzXCI7XG5leHBvcnQgdHlwZSB7IEVtYWlsRXhlY3V0ZUNvbmZpZywgRW1haWxFeGVjdXRpb25TZXR0aW5ncyB9IGZyb20gXCIuL3NyYy9kZWZhdWx0cy50c1wiO1xuZXhwb3J0IHsgZXNjYXBlSHRtbCB9IGZyb20gXCIuL3NyYy9lbWFpbC1odG1sLnRzXCI7XG5leHBvcnQgdHlwZSB7IEVtYWlsSHRtbE9wdGlvbnMgfSBmcm9tIFwiLi9zcmMvZW1haWwtaHRtbC50c1wiO1xuZXhwb3J0IHsgRW1haWxQbHVnaW4sIGVtYWlsIH0gZnJvbSBcIi4vc3JjL3BsdWdpbi50c1wiO1xuZXhwb3J0IHsgU0VORF9FTUFJTF9ERVNDUklQVElPTiB9IGZyb20gXCIuL3NyYy90b29sLnRzXCI7XG5leHBvcnQgdHlwZSB7IEVtYWlsVG9vbE9wdGlvbnMgfSBmcm9tIFwiLi9zcmMvdG9vbC50c1wiO1xuZXhwb3J0IHR5cGUgeyBFbWFpbEV4ZWN1dG9yLCBFbWFpbFJ1bnRpbWUgfSBmcm9tIFwiLi9zcmMvdHJhbnNwb3J0LnRzXCI7XG4iXX0=
|
|
@@ -0,0 +1,27 @@
|
|
|
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;
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
|
@@ -0,0 +1,83 @@
|
|
|
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 requests on the APP router (mounted at the app
|
|
77
|
+
* root). Requests to the login flow (`<emailBase>/auth/*`) pass through so a
|
|
78
|
+
* caller can authenticate; the app's own API/asset requests need a valid
|
|
79
|
+
* session or get a 401. `emailBase` is the plugin's mount path (e.g.
|
|
80
|
+
* `/api/email`) so the open prefix is matched against the full request path.
|
|
81
|
+
*/
|
|
82
|
+
middleware(emailBase: string): (req: Request, res: Response, next: NextFunction) => void;
|
|
83
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
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 requests on the APP router (mounted at the app
|
|
111
|
+
* root). Requests to the login flow (`<emailBase>/auth/*`) pass through so a
|
|
112
|
+
* caller can authenticate; the app's own API/asset requests need a valid
|
|
113
|
+
* session or get a 401. `emailBase` is the plugin's mount path (e.g.
|
|
114
|
+
* `/api/email`) so the open prefix is matched against the full request path.
|
|
115
|
+
*/
|
|
116
|
+
middleware(emailBase) {
|
|
117
|
+
const openPrefix = `${emailBase}${AUTH_PATH_PREFIX}`;
|
|
118
|
+
return (req, res, next) => {
|
|
119
|
+
if (req.path.startsWith(openPrefix)) {
|
|
120
|
+
next();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
void this.authenticate(req).then((email) => {
|
|
124
|
+
if (email) {
|
|
125
|
+
next();
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
res.status(401).json({ error: "authentication required", loginPath: openPrefix });
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZ2F0ZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9hdXRoL2dhdGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7R0FtQkc7QUFHSCxPQUFPLEVBQUUsSUFBSSxFQUFFLEdBQUcsRUFBRSxNQUFNLHdCQUF3QixDQUFDO0FBRW5ELE9BQU8sRUFBRSxjQUFjLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUNsRSxPQUFPLEVBQUUsU0FBUyxFQUFFLFdBQVcsRUFBRSxhQUFhLEVBQUUsTUFBTSxVQUFVLENBQUM7QUFDakUsT0FBTyxFQUFFLFdBQVcsRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBRTlDLE1BQU0sTUFBTSxHQUFHLEdBQUcsQ0FBQyxNQUFNLENBQUMsWUFBWSxDQUFDLENBQUM7QUFFeEMsdUNBQXVDO0FBQ3ZDLE1BQU0sQ0FBQyxNQUFNLGNBQWMsR0FBRyxVQUFVLENBQUM7QUFFekMsOEVBQThFO0FBQzlFLE1BQU0sZ0JBQWdCLEdBQUcsT0FBTyxDQUFDO0FBa0JqQyxzQ0FBc0M7QUFDdEMsTUFBTSxPQUFPLFFBQVE7SUFPVTtJQU5aLEtBQUssQ0FBWTtJQUNsQyxpRkFBaUY7SUFDakYsb0ZBQW9GO0lBQ25FLGNBQWMsR0FBRyxJQUFJLFdBQVcsQ0FBQyxDQUFDLEVBQUUsRUFBRSxHQUFHLEVBQUUsR0FBRyxJQUFJLENBQUMsQ0FBQztJQUNwRCxhQUFhLEdBQUcsSUFBSSxXQUFXLENBQUMsRUFBRSxFQUFFLEVBQUUsR0FBRyxFQUFFLEdBQUcsSUFBSSxDQUFDLENBQUM7SUFFckUsWUFBNkIsT0FBd0I7UUFBeEIsWUFBTyxHQUFQLE9BQU8sQ0FBaUI7UUFDbkQsSUFBSSxDQUFDLEtBQUssR0FBRyxJQUFJLFNBQVMsQ0FBQyxPQUFPLENBQUMsY0FBYyxHQUFHLElBQUksRUFBRSxPQUFPLENBQUMsV0FBVyxDQUFDLENBQUM7SUFDakYsQ0FBQztJQUVEOzs7OztP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QUUsQ0FBQztvQkFDVixJQUFJLEVBQUUsQ0FBQztnQkFDVCxDQUFDO3FCQUFNLENBQUM7b0JBQ04sR0FBRyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQyxJQUFJLENBQUMsRUFBRSxLQUFLLEVBQUUseUJBQXlCLEVBQUUsU0FBUyxFQUFFLFVBQVUsRUFBRSxDQUFDLENBQUM7Z0JBQ3BGLENBQUM7WUFDSCxDQUFDLENBQUMsQ0FBQztRQUNMLENBQUMsQ0FBQztJQUNKLENBQUM7Q0FDRiIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogVGhlIGVtYWlsLU9UUCBhY2Nlc3MgZ2F0ZTogcnVudGltZSArIEV4cHJlc3MgbWlkZGxld2FyZSArIHJlcXVlc3QgaGFuZGxlcnMuXG4gKlxuICoge0BsaW5rIEF1dGhHYXRlfSBvd25zIHRoZSBhbGxvdy1saXN0LCByYXRlIGxpbWl0ZXJzLCBjb2RlIHN0b3JlLCBhbmQgc2Vzc2lvblxuICogc2lnbmluZy4gVGhlIHBsdWdpbiB3aXJlcyBmb3VyIHJvdXRlcyB0byBpdCAoYHJlcXVlc3RgL2B2ZXJpZnlgL2Bsb2dvdXRgL1xuICogYHN0YXR1c2ApIGFuZCBtb3VudHMge0BsaW5rIEF1dGhHYXRlLm1pZGRsZXdhcmV9IHNvIGV2ZXJ5IE9USEVSIHJlcXVlc3QgbmVlZHMgYVxuICogdmFsaWQgc2Vzc2lvbiBjb29raWUuXG4gKlxuICogRGVzaWduIHBvaW50czpcbiAqICAgLSBgcmVxdWVzdGAgQUxXQVlTIHJlc29sdmVzIGB7IG9rOiB0cnVlIH1gIChhbnRpLWVudW1lcmF0aW9uKTogYSBjb2RlIGlzIG9ubHlcbiAqICAgICBnZW5lcmF0ZWQgKyBlbWFpbGVkIHdoZW4gdGhlIGFkZHJlc3MgaXMgYWxsb3ctbGlzdGVkIEFORCB1bmRlciB0aGUgcmF0ZVxuICogICAgIGxpbWl0LCBidXQgdGhlIGNhbGxlciBjYW4ndCB0ZWxsIHRoZSBkaWZmZXJlbmNlLlxuICogICAtIHRoZSBzZXNzaW9uIGxpdmVzIGluIGFuIEh0dHBPbmx5ICsgU2FtZVNpdGU9TGF4IGNvb2tpZSAoU2VjdXJlIGluIHByb2QpLCBzb1xuICogICAgIGl0IHN1cnZpdmVzIHJlbG9hZHMgYW5kIGlzIG5vdCByZWFkYWJsZSBieSBwYWdlIHNjcmlwdHMuXG4gKiAgIC0gZmFpbC1vcGVuIG9uIGEgbWlzc2luZyBzaWduaW5nIHNlY3JldCAoc2VlIGBvdHAudHNgKTogYSBEYXRhYnJpY2tzIEFwcCBpc1xuICogICAgIGFscmVhZHkgYWNjZXNzLWxpbWl0ZWQ7IGFuIHVuc2V0IGBBVVRIX0pXVF9TRUNSRVRgIGRlZ3JhZGVzIHRvXG4gKiAgICAgbm9uLWR1cmFibGUgc2Vzc2lvbnMsIG5vdCBhIGxvY2tvdXQuXG4gKlxuICogQG1vZHVsZVxuICovXG5cbmltcG9ydCB0eXBlIHsgQ29va2llT3B0aW9ucywgTmV4dEZ1bmN0aW9uLCBSZXF1ZXN0LCBSZXNwb25zZSB9IGZyb20gXCJleHByZXNzXCI7XG5pbXBvcnQgeyBodHRwLCBsb2cgfSBmcm9tIFwiQGRieC10b29scy9zaGFyZWQtY29yZVwiO1xuaW1wb3J0IHR5cGUgeyBBdXRoU3RhdHVzIH0gZnJvbSBcIkBkYngtdG9vbHMvc2hhcmVkLWVtYWlsXCI7XG5pbXBvcnQgeyBsb29rc0xpa2VFbWFpbCwgbWF0Y2hlc0FsbG93bGlzdCB9IGZyb20gXCIuL2FsbG93bGlzdC50c1wiO1xuaW1wb3J0IHsgQ29kZVN0b3JlLCBzaWduU2Vzc2lvbiwgdmVyaWZ5U2Vzc2lvbiB9IGZyb20gXCIuL290cC50c1wiO1xuaW1wb3J0IHsgUmF0ZUxpbWl0ZXIgfSBmcm9tIFwiLi9yYXRlLWxpbWl0LnRzXCI7XG5cbmNvbnN0IGxvZ2dlciA9IGxvZy5sb2dnZXIoXCJlbWFpbDphdXRoXCIpO1xuXG4vKiogQ29va2llIHRoZSBzZXNzaW9uIEpXVCByaWRlcyBpbi4gKi9cbmV4cG9ydCBjb25zdCBTRVNTSU9OX0NPT0tJRSA9IFwiZGJ4X2F1dGhcIjtcblxuLyoqIFJvdXRlIHByZWZpeCAodW5kZXIgdGhlIHBsdWdpbiBiYXNlIGAvYXBpL2VtYWlsYCkgdGhlIGdhdGUgbGVhdmVzIG9wZW4uICovXG5jb25zdCBBVVRIX1BBVEhfUFJFRklYID0gXCIvYXV0aFwiO1xuXG4vKiogUmVzb2x2ZWQgZ2F0ZSBjb25maWd1cmF0aW9uLiAqL1xuZXhwb3J0IGludGVyZmFjZSBBdXRoR2F0ZU9wdGlvbnMge1xuICAvKiogQWxsb3ctbGlzdCBwYXR0ZXJucyAoZG9tYWluIC8gZ2xvYiAvIGAvcmVnZXgvYCkuIEVtcHR5ID0gYWxsb3cgbm9ib2R5LiAqL1xuICByZWFkb25seSBhbGxvdzogcmVhZG9ubHkgc3RyaW5nW107XG4gIC8qKiBTZXNzaW9uIGxpZmV0aW1lIGluIHNlY29uZHMuICovXG4gIHJlYWRvbmx5IHNlc3Npb25UdGxTZWNvbmRzOiBudW1iZXI7XG4gIC8qKiBPbmUtdGltZSBjb2RlIGxpZmV0aW1lIGluIHNlY29uZHMuICovXG4gIHJlYWRvbmx5IGNvZGVUdGxTZWNvbmRzOiBudW1iZXI7XG4gIC8qKiBNYXggdmVyaWZ5IGF0dGVtcHRzIHBlciBpc3N1ZWQgY29kZS4gKi9cbiAgcmVhZG9ubHkgbWF4QXR0ZW1wdHM6IG51bWJlcjtcbiAgLyoqIFNlbmQgdGhlIGNvZGUgZW1haWwuIFJldHVybnMgbm90aGluZzsgZmFpbHVyZXMgYXJlIGxvZ2dlZCwgbm90IHN1cmZhY2VkLiAqL1xuICByZWFkb25seSBzZW5kQ29kZTogKGVtYWlsOiBzdHJpbmcsIGNvZGU6IHN0cmluZykgPT4gUHJvbWlzZTx2b2lkPjtcbiAgLyoqIFRydWUgaW4gcHJvZHVjdGlvbiAoc2V0cyB0aGUgYFNlY3VyZWAgY29va2llIGZsYWcpLiAqL1xuICByZWFkb25seSBzZWN1cmVDb29raWVzOiBib29sZWFuO1xufVxuXG4vKiogVGhlIGVtYWlsLU9UUCBnYXRlIGZvciBvbmUgYXBwLiAqL1xuZXhwb3J0IGNsYXNzIEF1dGhHYXRlIHtcbiAgcHJpdmF0ZSByZWFkb25seSBjb2RlczogQ29kZVN0b3JlO1xuICAvLyBTZXBhcmF0ZSBsaW1pdGVyczogcmVxdWVzdGluZyBhIGNvZGUgaXMgY2hlYXAtdG8tYWJ1c2UgKGVtYWlsIHNwYW0pLCB2ZXJpZnlpbmdcbiAgLy8gaXMgYSBicnV0ZS1mb3JjZSBzdXJmYWNlLiBQZXItZW1haWwgQU5EIHBlci1JUCBzbyBuZWl0aGVyIGF4aXMgYWxvbmUgaXMgYSBieXBhc3MuXG4gIHByaXZhdGUgcmVhZG9ubHkgcmVxdWVzdExpbWl0ZXIgPSBuZXcgUmF0ZUxpbWl0ZXIoNSwgMTUgKiA2MCAqIDEwMDApO1xuICBwcml2YXRlIHJlYWRvbmx5IHZlcmlmeUxpbWl0ZXIgPSBuZXcgUmF0ZUxpbWl0ZXIoMTAsIDE1ICogNjAgKiAxMDAwKTtcblxuICBjb25zdHJ1Y3Rvcihwcml2YXRlIHJlYWRvbmx5IG9wdGlvbnM6IEF1dGhHYXRlT3B0aW9ucykge1xuICAgIHRoaXMuY29kZXMgPSBuZXcgQ29kZVN0b3JlKG9wdGlvbnMuY29kZVR0bFNlY29uZHMgKiAxMDAwLCBvcHRpb25zLm1heEF0dGVtcHRzKTtcbiAgfVxuXG4gIC8qKlxuICAgKiBIYW5kbGUgYFBPU1QgL2F1dGgvcmVxdWVzdGAuIEFsd2F5cyByZXNvbHZlcyBgeyBvazogdHJ1ZSB9YCAocGx1cyBhXG4gICAqIGByZXRyeUFmdGVyYCB3aGVuIHJhdGUtbGltaXRlZCk7IGEgY29kZSBpcyBpc3N1ZWQgKyBlbWFpbGVkIG9ubHkgZm9yIGFuXG4gICAqIGFsbG93LWxpc3RlZCBhZGRyZXNzIHVuZGVyIHRoZSBsaW1pdC4gRXJyb3JzIGluIHNlbmRpbmcgYXJlIHN3YWxsb3dlZCBzbyB0aGVcbiAgICogcmVzcG9uc2UgbmV2ZXIgcmV2ZWFscyB3aGV0aGVyIGFuIGFkZHJlc3MgZXhpc3RzIC8gaXMgYWxsb3dlZC5cbiAgICovXG4gIGFzeW5jIGhhbmRsZVJlcXVlc3QoZW1haWw6IHN0cmluZywgaXA6IHN0cmluZyk6IFByb21pc2U8eyBvazogdHJ1ZTsgcmV0cnlBZnRlcj86IG51bWJlciB9PiB7XG4gICAgY29uc3QgYWRkcmVzcyA9IGVtYWlsLnRyaW0oKS50b0xvd2VyQ2FzZSgpO1xuICAgIGNvbnN0IGJ5SXAgPSB0aGlzLnJlcXVlc3RMaW1pdGVyLmhpdChgaXA6JHtpcH1gKTtcbiAgICBjb25zdCBieUVtYWlsID0gdGhpcy5yZXF1ZXN0TGltaXRlci5oaXQoYGVtYWlsOiR7YWRkcmVzc31gKTtcbiAgICBpZiAoIWJ5SXAuYWxsb3dlZCB8fCAhYnlFbWFpbC5hbGxvd2VkKSB7XG4gICAgICByZXR1cm4geyBvazogdHJ1ZSwgcmV0cnlBZnRlcjogYnlJcC5yZXRyeUFmdGVyID8/IGJ5RW1haWwucmV0cnlBZnRlciB9O1xuICAgIH1cbiAgICBpZiAobG9va3NMaWtlRW1haWwoYWRkcmVzcykgJiYgbWF0Y2hlc0FsbG93bGlzdChhZGRyZXNzLCB0aGlzLm9wdGlvbnMuYWxsb3cpKSB7XG4gICAgICBjb25zdCBjb2RlID0gdGhpcy5jb2Rlcy5pc3N1ZShhZGRyZXNzKTtcbiAgICAgIHRyeSB7XG4gICAgICAgIGF3YWl0IHRoaXMub3B0aW9ucy5zZW5kQ29kZShhZGRyZXNzLCBjb2RlKTtcbiAgICAgIH0gY2F0Y2ggKGVycm9yKSB7XG4gICAgICAgIGxvZ2dlci53YXJuKFwiZmFpbGVkIHRvIHNlbmQgT1RQIGVtYWlsXCIsIHsgZXJyb3IgfSk7XG4gICAgICB9XG4gICAgfVxuICAgIHJldHVybiB7IG9rOiB0cnVlIH07XG4gIH1cblxuICAvKipcbiAgICogSGFuZGxlIGBQT1NUIC9hdXRoL3ZlcmlmeWAuIE9uIGEgY29ycmVjdCBjb2RlLCByZXR1cm5zIHRoZSBzZXNzaW9uIGB0b2tlbmBcbiAgICogcGx1cyB0aGUgYGNvb2tpZU9wdGlvbnNgIGZvciBpdCBzbyB0aGUgcm91dGUgc2V0cyBpdCB3aXRoIEV4cHJlc3MncyBvd25cbiAgICogYHJlcy5jb29raWUoU0VTU0lPTl9DT09LSUUsIHRva2VuLCBvcHRpb25zKWAgKG5vIGhhbmQtcm9sbGVkIFNldC1Db29raWUpLlxuICAgKiBGYWlsdXJlcyByZXR1cm4gYHsgb2s6IGZhbHNlIH1gIHdpdGggYSBnZW5lcmljIHJlYXNvbi5cbiAgICovXG4gIGFzeW5jIGhhbmRsZVZlcmlmeShcbiAgICBlbWFpbDogc3RyaW5nLFxuICAgIGNvZGU6IHN0cmluZyxcbiAgICBpcDogc3RyaW5nLFxuICAgIHNlY3VyZTogYm9vbGVhbixcbiAgKTogUHJvbWlzZTx7IG9rOiBib29sZWFuOyB0b2tlbj86IHN0cmluZzsgY29va2llT3B0aW9ucz86IENvb2tpZU9wdGlvbnM7IHJldHJ5QWZ0ZXI/OiBudW1iZXIgfT4ge1xuICAgIGNvbnN0IGFkZHJlc3MgPSBlbWFpbC50cmltKCkudG9Mb3dlckNhc2UoKTtcbiAgICBjb25zdCBieUlwID0gdGhpcy52ZXJpZnlMaW1pdGVyLmhpdChgaXA6JHtpcH1gKTtcbiAgICBjb25zdCBieUVtYWlsID0gdGhpcy52ZXJpZnlMaW1pdGVyLmhpdChgZW1haWw6JHthZGRyZXNzfWApO1xuICAgIGlmICghYnlJcC5hbGxvd2VkIHx8ICFieUVtYWlsLmFsbG93ZWQpIHtcbiAgICAgIHJldHVybiB7IG9rOiBmYWxzZSwgcmV0cnlBZnRlcjogYnlJcC5yZXRyeUFmdGVyID8/IGJ5RW1haWwucmV0cnlBZnRlciB9O1xuICAgIH1cbiAgICBpZiAodGhpcy5jb2Rlcy52ZXJpZnkoYWRkcmVzcywgY29kZS50cmltKCkpICE9PSBcIm9rXCIpIHJldHVybiB7IG9rOiBmYWxzZSB9O1xuICAgIC8vIENvcnJlY3QgY29kZTogY2xlYXIgdGhlIGNhbGxlcidzIHJlcXVlc3QvdmVyaWZ5IGJ1ZGdldCBhbmQgbWludCBhIHNlc3Npb24uXG4gICAgdGhpcy5yZXF1ZXN0TGltaXRlci5yZXNldChgZW1haWw6JHthZGRyZXNzfWApO1xuICAgIHRoaXMudmVyaWZ5TGltaXRlci5yZXNldChgZW1haWw6JHthZGRyZXNzfWApO1xuICAgIGNvbnN0IHRva2VuID0gYXdhaXQgc2lnblNlc3Npb24oYWRkcmVzcywgdGhpcy5vcHRpb25zLnNlc3Npb25UdGxTZWNvbmRzKTtcbiAgICByZXR1cm4geyBvazogdHJ1ZSwgdG9rZW4sIGNvb2tpZU9wdGlvbnM6IHRoaXMuY29va2llT3B0aW9ucyhzZWN1cmUpIH07XG4gIH1cblxuICAvKiogRXhwcmVzcyBjb29raWUgb3B0aW9ucyBmb3IgdGhlIHNlc3Npb24gKHVzZWQgd2l0aCBgcmVzLmNvb2tpZWAvYHJlcy5jbGVhckNvb2tpZWApLiAqL1xuICBjb29raWVPcHRpb25zKHNlY3VyZTogYm9vbGVhbik6IENvb2tpZU9wdGlvbnMge1xuICAgIHJldHVybiB7XG4gICAgICBodHRwT25seTogdHJ1ZSxcbiAgICAgIHNhbWVTaXRlOiBcImxheFwiLFxuICAgICAgc2VjdXJlOiBzZWN1cmUgfHwgdGhpcy5vcHRpb25zLnNlY3VyZUNvb2tpZXMsXG4gICAgICBwYXRoOiBcIi9cIixcbiAgICAgIG1heEFnZTogdGhpcy5vcHRpb25zLnNlc3Npb25UdGxTZWNvbmRzICogMTAwMCxcbiAgICB9O1xuICB9XG5cbiAgLyoqIFJlc29sdmUgdGhlIGF1dGhlbnRpY2F0ZWQgZW1haWwgZm9yIGEgcmVxdWVzdCwgb3IgYHVuZGVmaW5lZGAuICovXG4gIGFzeW5jIGF1dGhlbnRpY2F0ZShyZXE6IFJlcXVlc3QpOiBQcm9taXNlPHN0cmluZyB8IHVuZGVmaW5lZD4ge1xuICAgIC8vIFJldXNlIHNoYXJlZC1jb3JlJ3MgY29va2llIHBhcnNlciAoYWNjZXB0cyBhbiBFeHByZXNzIHJlcSBkaXJlY3RseSkgcmF0aGVyXG4gICAgLy8gdGhhbiByZS1pbXBsZW1lbnRpbmcgaGVhZGVyIHNwbGl0dGluZyBoZXJlLlxuICAgIGNvbnN0IHRva2VuID0gaHR0cC5wYXJzZUNvb2tpZXMocmVxKVtTRVNTSU9OX0NPT0tJRV07XG4gICAgcmV0dXJuIHZlcmlmeVNlc3Npb24odG9rZW4pO1xuICB9XG5cbiAgLyoqIFRoZSBgR0VUIC9hdXRoL3N0YXR1c2AgcGF5bG9hZCBmb3IgYSByZXF1ZXN0LiAqL1xuICBhc3luYyBzdGF0dXMocmVxOiBSZXF1ZXN0KTogUHJvbWlzZTxBdXRoU3RhdHVzPiB7XG4gICAgY29uc3QgZW1haWwgPSBhd2FpdCB0aGlzLmF1dGhlbnRpY2F0ZShyZXEpO1xuICAgIHJldHVybiB7IGF1dGhlbnRpY2F0ZWQ6IEJvb2xlYW4oZW1haWwpLCBlbWFpbCwgZW5hYmxlZDogdHJ1ZSB9O1xuICB9XG5cbiAgLyoqXG4gICAqIEV4cHJlc3MgbWlkZGxld2FyZSBnYXRpbmcgcmVxdWVzdHMgb24gdGhlIEFQUCByb3V0ZXIgKG1vdW50ZWQgYXQgdGhlIGFwcFxuICAgKiByb290KS4gUmVxdWVzdHMgdG8gdGhlIGxvZ2luIGZsb3cgKGA8ZW1haWxCYXNlPi9hdXRoLypgKSBwYXNzIHRocm91Z2ggc28gYVxuICAgKiBjYWxsZXIgY2FuIGF1dGhlbnRpY2F0ZTsgdGhlIGFwcCdzIG93biBBUEkvYXNzZXQgcmVxdWVzdHMgbmVlZCBhIHZhbGlkXG4gICAqIHNlc3Npb24gb3IgZ2V0IGEgNDAxLiBgZW1haWxCYXNlYCBpcyB0aGUgcGx1Z2luJ3MgbW91bnQgcGF0aCAoZS5nLlxuICAgKiBgL2FwaS9lbWFpbGApIHNvIHRoZSBvcGVuIHByZWZpeCBpcyBtYXRjaGVkIGFnYWluc3QgdGhlIGZ1bGwgcmVxdWVzdCBwYXRoLlxuICAgKi9cbiAgbWlkZGxld2FyZShlbWFpbEJhc2U6IHN0cmluZyk6IChyZXE6IFJlcXVlc3QsIHJlczogUmVzcG9uc2UsIG5leHQ6IE5leHRGdW5jdGlvbikgPT4gdm9pZCB7XG4gICAgY29uc3Qgb3BlblByZWZpeCA9IGAke2VtYWlsQmFzZX0ke0FVVEhfUEFUSF9QUkVGSVh9YDtcbiAgICByZXR1cm4gKHJlcSwgcmVzLCBuZXh0KSA9PiB7XG4gICAgICBpZiAocmVxLnBhdGguc3RhcnRzV2l0aChvcGVuUHJlZml4KSkge1xuICAgICAgICBuZXh0KCk7XG4gICAgICAgIHJldHVybjtcbiAgICAgIH1cbiAgICAgIHZvaWQgdGhpcy5hdXRoZW50aWNhdGUocmVxKS50aGVuKChlbWFpbCkgPT4ge1xuICAgICAgICBpZiAoZW1haWwpIHtcbiAgICAgICAgICBuZXh0KCk7XG4gICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgcmVzLnN0YXR1cyg0MDEpLmpzb24oeyBlcnJvcjogXCJhdXRoZW50aWNhdGlvbiByZXF1aXJlZFwiLCBsb2dpblBhdGg6IG9wZW5QcmVmaXggfSk7XG4gICAgICAgIH1cbiAgICAgIH0pO1xuICAgIH07XG4gIH1cbn1cbiJdfQ==
|
|
@@ -0,0 +1,49 @@
|
|
|
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>;
|