@dollhousemcp/mcp-server 2.0.10 → 2.0.11
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/dist/auto-dollhouse/portDiscovery.d.ts +23 -0
- package/dist/auto-dollhouse/portDiscovery.d.ts.map +1 -0
- package/dist/auto-dollhouse/portDiscovery.js +77 -0
- package/dist/cli/console-token.d.ts +18 -0
- package/dist/cli/console-token.d.ts.map +1 -0
- package/dist/cli/console-token.js +187 -0
- package/dist/generated/version.d.ts +2 -2
- package/dist/generated/version.js +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -5
- package/dist/web/console/consoleToken.d.ts +403 -0
- package/dist/web/console/consoleToken.d.ts.map +1 -0
- package/dist/web/console/consoleToken.js +930 -0
- package/dist/web/middleware/authMiddleware.d.ts +64 -0
- package/dist/web/middleware/authMiddleware.d.ts.map +1 -0
- package/dist/web/middleware/authMiddleware.js +174 -0
- package/dist/web/routes/consoleRouteHelpers.d.ts +33 -0
- package/dist/web/routes/consoleRouteHelpers.d.ts.map +1 -0
- package/dist/web/routes/consoleRouteHelpers.js +60 -0
- package/dist/web/routes/tokenRoutes.d.ts +37 -0
- package/dist/web/routes/tokenRoutes.d.ts.map +1 -0
- package/dist/web/routes/tokenRoutes.js +95 -0
- package/dist/web/routes/totpRoutes.d.ts +45 -0
- package/dist/web/routes/totpRoutes.d.ts.map +1 -0
- package/dist/web/routes/totpRoutes.js +187 -0
- package/package.json +1 -1
- package/server.json +2 -2
- package/dist/constants/version.d.ts +0 -3
- package/dist/constants/version.d.ts.map +0 -1
- package/dist/constants/version.js +0 -4
- package/dist/logging/sinks/SSELogSink.d.ts +0 -35
- package/dist/logging/sinks/SSELogSink.d.ts.map +0 -1
- package/dist/logging/sinks/SSELogSink.js +0 -181
- package/dist/logging/viewer/viewerHtml.d.ts +0 -8
- package/dist/logging/viewer/viewerHtml.d.ts.map +0 -1
- package/dist/logging/viewer/viewerHtml.js +0 -204
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express middleware for console Bearer token authentication (#1780).
|
|
3
|
+
*
|
|
4
|
+
* Checks the `Authorization: Bearer <token>` header on requests to protected
|
|
5
|
+
* endpoints, with a `?token=<token>` query parameter fallback for SSE streams
|
|
6
|
+
* (EventSource cannot set custom headers).
|
|
7
|
+
*
|
|
8
|
+
* Behavior is gated on the `DOLLHOUSE_WEB_AUTH_ENABLED` env var. When the flag
|
|
9
|
+
* is false (the default during Phase 1 rollout) the middleware is a no-op —
|
|
10
|
+
* requests pass through unconditionally. When true, every protected request
|
|
11
|
+
* must carry a valid token or receive a 401.
|
|
12
|
+
*
|
|
13
|
+
* Phase 1 design notes:
|
|
14
|
+
* - Every valid token is treated as admin-scoped. Scope enforcement is a
|
|
15
|
+
* stubbed hook (`authorizeScope`) that always returns true. Phase 2 swaps
|
|
16
|
+
* in real scope checks without touching any route handler.
|
|
17
|
+
* - Element boundaries and tenant filtering are similarly stubbed for Phase 3.
|
|
18
|
+
* - The middleware attaches the matched token entry to `res.locals.tokenEntry`
|
|
19
|
+
* so downstream handlers can inspect it (audit logs, scope decisions, etc.).
|
|
20
|
+
*
|
|
21
|
+
* @since v2.1.0 — Issue #1780
|
|
22
|
+
*/
|
|
23
|
+
import type { RequestHandler } from 'express';
|
|
24
|
+
import type { ConsoleTokenStore } from '../console/consoleToken.js';
|
|
25
|
+
/**
|
|
26
|
+
* Options for the auth middleware factory.
|
|
27
|
+
*/
|
|
28
|
+
export interface AuthMiddlewareOptions {
|
|
29
|
+
/** The token store holding valid tokens. */
|
|
30
|
+
store: ConsoleTokenStore;
|
|
31
|
+
/** Whether auth is enforced. When false, middleware is a no-op. */
|
|
32
|
+
enabled: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Path prefixes that are never protected. A request whose URL path starts
|
|
35
|
+
* with any of these strings will skip auth and be passed through to the
|
|
36
|
+
* next handler. Used to exempt health checks, version info, client detection,
|
|
37
|
+
* and similar public metadata endpoints.
|
|
38
|
+
*
|
|
39
|
+
* Paths are compared against `req.path` (the route-relative path), so include
|
|
40
|
+
* the full pathname starting with `/` — e.g. `/api/health`, `/api/setup/version`.
|
|
41
|
+
*/
|
|
42
|
+
publicPathPrefixes?: string[];
|
|
43
|
+
/** Optional label for log messages (e.g. "api" or "sse"). */
|
|
44
|
+
label?: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Create the core authentication middleware.
|
|
48
|
+
*
|
|
49
|
+
* The returned handler enforces Bearer token auth on every request it sees.
|
|
50
|
+
* Mount it with `app.use(createAuthMiddleware(...))` before protected routers,
|
|
51
|
+
* or attach it to individual routes that need protection.
|
|
52
|
+
*
|
|
53
|
+
* When `enabled: false`, the handler immediately calls `next()` — allowing
|
|
54
|
+
* the infrastructure to land with the default-off feature flag without
|
|
55
|
+
* breaking existing traffic.
|
|
56
|
+
*
|
|
57
|
+
* Phase 3 hardening (tracked in #1789): Add 401 rate limiting to prevent
|
|
58
|
+
* DoS from floods of bad-token requests. Brute-forcing a 256-bit token is
|
|
59
|
+
* infeasible, but an attacker flooding /api with wrong tokens could saturate
|
|
60
|
+
* the verify path. A sliding-window limiter keyed on the requesting IP is
|
|
61
|
+
* the right shape.
|
|
62
|
+
*/
|
|
63
|
+
export declare function createAuthMiddleware(options: AuthMiddlewareOptions): RequestHandler;
|
|
64
|
+
//# sourceMappingURL=authMiddleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authMiddleware.d.ts","sourceRoot":"","sources":["../../../src/web/middleware/authMiddleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAmC,cAAc,EAAE,MAAM,SAAS,CAAC;AAC/E,OAAO,KAAK,EAAE,iBAAiB,EAAqB,MAAM,4BAA4B,CAAC;AAsEvF;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,4CAA4C;IAC5C,KAAK,EAAE,iBAAiB,CAAC;IACzB,mEAAmE;IACnE,OAAO,EAAE,OAAO,CAAC;IACjB;;;;;;;;OAQG;IACH,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,cAAc,CAyCnF"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express middleware for console Bearer token authentication (#1780).
|
|
3
|
+
*
|
|
4
|
+
* Checks the `Authorization: Bearer <token>` header on requests to protected
|
|
5
|
+
* endpoints, with a `?token=<token>` query parameter fallback for SSE streams
|
|
6
|
+
* (EventSource cannot set custom headers).
|
|
7
|
+
*
|
|
8
|
+
* Behavior is gated on the `DOLLHOUSE_WEB_AUTH_ENABLED` env var. When the flag
|
|
9
|
+
* is false (the default during Phase 1 rollout) the middleware is a no-op —
|
|
10
|
+
* requests pass through unconditionally. When true, every protected request
|
|
11
|
+
* must carry a valid token or receive a 401.
|
|
12
|
+
*
|
|
13
|
+
* Phase 1 design notes:
|
|
14
|
+
* - Every valid token is treated as admin-scoped. Scope enforcement is a
|
|
15
|
+
* stubbed hook (`authorizeScope`) that always returns true. Phase 2 swaps
|
|
16
|
+
* in real scope checks without touching any route handler.
|
|
17
|
+
* - Element boundaries and tenant filtering are similarly stubbed for Phase 3.
|
|
18
|
+
* - The middleware attaches the matched token entry to `res.locals.tokenEntry`
|
|
19
|
+
* so downstream handlers can inspect it (audit logs, scope decisions, etc.).
|
|
20
|
+
*
|
|
21
|
+
* @since v2.1.0 — Issue #1780
|
|
22
|
+
*/
|
|
23
|
+
import { UnicodeValidator } from '../../security/validators/unicodeValidator.js';
|
|
24
|
+
import { logger } from '../../utils/logger.js';
|
|
25
|
+
/** Query parameter name used as a fallback for SSE streams. */
|
|
26
|
+
const TOKEN_QUERY_PARAM = 'token';
|
|
27
|
+
/** Header name we look at for Bearer tokens. */
|
|
28
|
+
const AUTH_HEADER = 'authorization';
|
|
29
|
+
/** Prefix expected on the Authorization header value. */
|
|
30
|
+
const BEARER_PREFIX = 'Bearer ';
|
|
31
|
+
/**
|
|
32
|
+
* Strict format for console tokens — 64 lowercase hex characters (256 bits).
|
|
33
|
+
* Any presented token that does not match this pattern is rejected before it
|
|
34
|
+
* reaches the constant-time comparison. This blocks any non-ASCII Unicode
|
|
35
|
+
* payload (homographs, zero-width, bidi overrides, etc.) from ever touching
|
|
36
|
+
* the verify path. DMCP-SEC-004 mitigation.
|
|
37
|
+
*/
|
|
38
|
+
const TOKEN_FORMAT = /^[0-9a-f]{64}$/;
|
|
39
|
+
/**
|
|
40
|
+
* Sanitize a raw token string pulled from a request.
|
|
41
|
+
*
|
|
42
|
+
* - Normalizes to NFC via UnicodeValidator (DMCP-SEC-004)
|
|
43
|
+
* - Validates against the strict hex format
|
|
44
|
+
* - Returns the cleaned value, or null if anything looks wrong
|
|
45
|
+
*
|
|
46
|
+
* Any failure here results in a 401 at the call site. Legitimate tokens are
|
|
47
|
+
* 64-char lowercase hex, so normalization and format validation are effectively
|
|
48
|
+
* no-ops for valid input and hard rejections for anything malicious.
|
|
49
|
+
*/
|
|
50
|
+
function sanitizePresentedToken(raw) {
|
|
51
|
+
if (!raw)
|
|
52
|
+
return null;
|
|
53
|
+
const normalized = UnicodeValidator.normalize(raw).normalizedContent;
|
|
54
|
+
if (!TOKEN_FORMAT.test(normalized))
|
|
55
|
+
return null;
|
|
56
|
+
return normalized;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Extract a Bearer token from a request.
|
|
60
|
+
* Checks Authorization header first, then query parameter.
|
|
61
|
+
* Applies Unicode normalization and strict format validation before returning.
|
|
62
|
+
* Returns the sanitized token string, or null if none was found or the value
|
|
63
|
+
* failed validation.
|
|
64
|
+
*/
|
|
65
|
+
function extractToken(req) {
|
|
66
|
+
// Preferred: Authorization: Bearer <token>
|
|
67
|
+
const header = req.headers[AUTH_HEADER];
|
|
68
|
+
if (typeof header === 'string' && header.startsWith(BEARER_PREFIX)) {
|
|
69
|
+
const value = header.slice(BEARER_PREFIX.length).trim();
|
|
70
|
+
if (value) {
|
|
71
|
+
const sanitized = sanitizePresentedToken(value);
|
|
72
|
+
if (sanitized)
|
|
73
|
+
return sanitized;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Fallback for EventSource: ?token=<token>
|
|
77
|
+
const q = req.query[TOKEN_QUERY_PARAM];
|
|
78
|
+
if (typeof q === 'string' && q.length > 0) {
|
|
79
|
+
return sanitizePresentedToken(q);
|
|
80
|
+
}
|
|
81
|
+
if (Array.isArray(q) && q.length > 0 && typeof q[0] === 'string') {
|
|
82
|
+
return sanitizePresentedToken(q[0]);
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Create the core authentication middleware.
|
|
88
|
+
*
|
|
89
|
+
* The returned handler enforces Bearer token auth on every request it sees.
|
|
90
|
+
* Mount it with `app.use(createAuthMiddleware(...))` before protected routers,
|
|
91
|
+
* or attach it to individual routes that need protection.
|
|
92
|
+
*
|
|
93
|
+
* When `enabled: false`, the handler immediately calls `next()` — allowing
|
|
94
|
+
* the infrastructure to land with the default-off feature flag without
|
|
95
|
+
* breaking existing traffic.
|
|
96
|
+
*
|
|
97
|
+
* Phase 3 hardening (tracked in #1789): Add 401 rate limiting to prevent
|
|
98
|
+
* DoS from floods of bad-token requests. Brute-forcing a 256-bit token is
|
|
99
|
+
* infeasible, but an attacker flooding /api with wrong tokens could saturate
|
|
100
|
+
* the verify path. A sliding-window limiter keyed on the requesting IP is
|
|
101
|
+
* the right shape.
|
|
102
|
+
*/
|
|
103
|
+
export function createAuthMiddleware(options) {
|
|
104
|
+
const { store, enabled, label = 'console' } = options;
|
|
105
|
+
const publicPaths = options.publicPathPrefixes ?? [];
|
|
106
|
+
return (req, res, next) => {
|
|
107
|
+
if (!enabled) {
|
|
108
|
+
return next();
|
|
109
|
+
}
|
|
110
|
+
// Public path allowlist — skip auth for whitelisted prefixes.
|
|
111
|
+
// Use originalUrl.pathname so we match regardless of mount point.
|
|
112
|
+
const pathToCheck = req.originalUrl.split('?')[0];
|
|
113
|
+
for (const prefix of publicPaths) {
|
|
114
|
+
if (pathToCheck === prefix || pathToCheck.startsWith(prefix + '/')) {
|
|
115
|
+
return next();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const presented = extractToken(req);
|
|
119
|
+
if (!presented) {
|
|
120
|
+
return respondUnauthorized(res, 'missing_token', label, store, 0);
|
|
121
|
+
}
|
|
122
|
+
const entry = store.verify(presented);
|
|
123
|
+
if (!entry) {
|
|
124
|
+
// Log presented-length only — never the presented value itself.
|
|
125
|
+
// Distinguishes "token missing" from "token wrong length" from
|
|
126
|
+
// "length matches but content differs" when troubleshooting.
|
|
127
|
+
return respondUnauthorized(res, 'invalid_token', label, store, presented.length);
|
|
128
|
+
}
|
|
129
|
+
// Stubbed authorization hook — Phase 2 flips real scope checks on here.
|
|
130
|
+
if (!authorizeScope(entry, req)) {
|
|
131
|
+
return respondForbidden(res, 'scope_denied', label, entry);
|
|
132
|
+
}
|
|
133
|
+
// Stash the matched entry for downstream handlers and log success at debug.
|
|
134
|
+
res.locals.tokenEntry = entry;
|
|
135
|
+
logger.debug(`[Auth:${label}] verified token id=${entry.id} name="${entry.name}" route=${req.method} ${req.originalUrl.split('?')[0]}`);
|
|
136
|
+
return next();
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Scope authorization hook.
|
|
141
|
+
*
|
|
142
|
+
* Phase 1: every valid token is treated as admin — returns true unconditionally.
|
|
143
|
+
* Phase 2: this function will check `entry.scopes` against the route's required
|
|
144
|
+
* scopes (which can be attached via `res.locals.requiredScope` or a route
|
|
145
|
+
* metadata system).
|
|
146
|
+
*/
|
|
147
|
+
function authorizeScope(_entry, _req) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Respond with 401 Unauthorized and a helpful hint about where to find the token.
|
|
152
|
+
* Includes presented-token length at debug level for troubleshooting — never the value.
|
|
153
|
+
*/
|
|
154
|
+
function respondUnauthorized(res, reason, label, store, presentedLength) {
|
|
155
|
+
logger.debug(`[Auth:${label}] 401 ${reason} presentedLength=${presentedLength}`);
|
|
156
|
+
res.status(401).json({
|
|
157
|
+
error: 'Authentication required',
|
|
158
|
+
reason,
|
|
159
|
+
hint: `Token file: ${store.getFilePath()}. Send 'Authorization: Bearer <token>' header, or append ?token=<token> for SSE streams.`,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Respond with 403 Forbidden — token was valid but scope did not permit this route.
|
|
164
|
+
* Phase 1 never reaches here because `authorizeScope` always returns true, but
|
|
165
|
+
* the code path exists so Phase 2 can wire it up without changing the middleware shape.
|
|
166
|
+
*/
|
|
167
|
+
function respondForbidden(res, reason, label, entry) {
|
|
168
|
+
logger.debug(`[Auth:${label}] 403 ${reason}`, { tokenId: entry.id, scopes: entry.scopes });
|
|
169
|
+
res.status(403).json({
|
|
170
|
+
error: 'Token scope does not permit this action',
|
|
171
|
+
reason,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXV0aE1pZGRsZXdhcmUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvd2ViL21pZGRsZXdhcmUvYXV0aE1pZGRsZXdhcmUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztHQXFCRztBQUlILE9BQU8sRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLCtDQUErQyxDQUFDO0FBQ2pGLE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSx1QkFBdUIsQ0FBQztBQUUvQywrREFBK0Q7QUFDL0QsTUFBTSxpQkFBaUIsR0FBRyxPQUFPLENBQUM7QUFFbEMsZ0RBQWdEO0FBQ2hELE1BQU0sV0FBVyxHQUFHLGVBQWUsQ0FBQztBQUVwQyx5REFBeUQ7QUFDekQsTUFBTSxhQUFhLEdBQUcsU0FBUyxDQUFDO0FBRWhDOzs7Ozs7R0FNRztBQUNILE1BQU0sWUFBWSxHQUFHLGdCQUFnQixDQUFDO0FBRXRDOzs7Ozs7Ozs7O0dBVUc7QUFDSCxTQUFTLHNCQUFzQixDQUFDLEdBQVc7SUFDekMsSUFBSSxDQUFDLEdBQUc7UUFBRSxPQUFPLElBQUksQ0FBQztJQUN0QixNQUFNLFVBQVUsR0FBRyxnQkFBZ0IsQ0FBQyxTQUFTLENBQUMsR0FBRyxDQUFDLENBQUMsaUJBQWlCLENBQUM7SUFDckUsSUFBSSxDQUFDLFlBQVksQ0FBQyxJQUFJLENBQUMsVUFBVSxDQUFDO1FBQUUsT0FBTyxJQUFJLENBQUM7SUFDaEQsT0FBTyxVQUFVLENBQUM7QUFDcEIsQ0FBQztBQUVEOzs7Ozs7R0FNRztBQUNILFNBQVMsWUFBWSxDQUFDLEdBQVk7SUFDaEMsMkNBQTJDO0lBQzNDLE1BQU0sTUFBTSxHQUFHLEdBQUcsQ0FBQyxPQUFPLENBQUMsV0FBVyxDQUFDLENBQUM7SUFDeEMsSUFBSSxPQUFPLE1BQU0sS0FBSyxRQUFRLElBQUksTUFBTSxDQUFDLFVBQVUsQ0FBQyxhQUFhLENBQUMsRUFBRSxDQUFDO1FBQ25FLE1BQU0sS0FBSyxHQUFH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QUFDIiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBFeHByZXNzIG1pZGRsZXdhcmUgZm9yIGNvbnNvbGUgQmVhcmVyIHRva2VuIGF1dGhlbnRpY2F0aW9uICgjMTc4MCkuXG4gKlxuICogQ2hlY2tzIHRoZSBgQXV0aG9yaXphdGlvbjogQmVhcmVyIDx0b2tlbj5gIGhlYWRlciBvbiByZXF1ZXN0cyB0byBwcm90ZWN0ZWRcbiAqIGVuZHBvaW50cywgd2l0aCBhIGA/dG9rZW49PHRva2VuPmAgcXVlcnkgcGFyYW1ldGVyIGZhbGxiYWNrIGZvciBTU0Ugc3RyZWFtc1xuICogKEV2ZW50U291cmNlIGNhbm5vdCBzZXQgY3VzdG9tIGhlYWRlcnMpLlxuICpcbiAqIEJlaGF2aW9yIGlzIGdhdGVkIG9uIHRoZSBgRE9MTEhPVVNFX1dFQl9BVVRIX0VOQUJMRURgIGVudiB2YXIuIFdoZW4gdGhlIGZsYWdcbiAqIGlzIGZhbHNlICh0aGUgZGVmYXVsdCBkdXJpbmcgUGhhc2UgMSByb2xsb3V0KSB0aGUgbWlkZGxld2FyZSBpcyBhIG5vLW9wIOKAlFxuICogcmVxdWVzdHMgcGFzcyB0aHJvdWdoIHVuY29uZGl0aW9uYWxseS4gV2hlbiB0cnVlLCBldmVyeSBwcm90ZWN0ZWQgcmVxdWVzdFxuICogbXVzdCBjYXJyeSBhIHZhbGlkIHRva2VuIG9yIHJlY2VpdmUgYSA0MDEuXG4gKlxuICogUGhhc2UgMSBkZXNpZ24gbm90ZXM6XG4gKiAtIEV2ZXJ5IHZhbGlkIHRva2VuIGlzIHRyZWF0ZWQgYXMgYWRtaW4tc2NvcGVkLiBTY29wZSBlbmZvcmNlbWVudCBpcyBhXG4gKiAgIHN0dWJiZWQgaG9vayAoYGF1dGhvcml6ZVNjb3BlYCkgdGhhdCBhbHdheXMgcmV0dXJucyB0cnVlLiBQaGFzZSAyIHN3YXBzXG4gKiAgIGluIHJlYWwgc2NvcGUgY2hlY2tzIHdpdGhvdXQgdG91Y2hpbmcgYW55IHJvdXRlIGhhbmRsZXIuXG4gKiAtIEVsZW1lbnQgYm91bmRhcmllcyBhbmQgdGVuYW50IGZpbHRlcmluZyBhcmUgc2ltaWxhcmx5IHN0dWJiZWQgZm9yIFBoYXNlIDMuXG4gKiAtIFRoZSBtaWRkbGV3YXJlIGF0dGFjaGVzIHRoZSBtYXRjaGVkIHRva2VuIGVudHJ5IHRvIGByZXMubG9jYWxzLnRva2VuRW50cnlgXG4gKiAgIHNvIGRvd25zdHJlYW0gaGFuZGxlcnMgY2FuIGluc3BlY3QgaXQgKGF1ZGl0IGxvZ3MsIHNjb3BlIGRlY2lzaW9ucywgZXRjLikuXG4gKlxuICogQHNpbmNlIHYyLjEuMCDigJQgSXNzdWUgIzE3ODBcbiAqL1xuXG5pbXBvcnQgdHlwZSB7IFJlcXVlc3QsIFJlc3BvbnNlLCBOZXh0RnVuY3Rpb24sIFJlcXVlc3RIYW5kbGVyIH0gZnJvbSAnZXhwcmVzcyc7XG5pbXBvcnQgdHlwZSB7IENvbnNvbGVUb2tlblN0b3JlLCBDb25zb2xlVG9rZW5FbnRyeSB9IGZyb20gJy4uL2NvbnNvbGUvY29uc29sZVRva2VuLmpzJztcbmltcG9ydCB7IFVuaWNvZGVWYWxpZGF0b3IgfSBmcm9tICcuLi8uLi9zZWN1cml0eS92YWxpZGF0b3JzL3VuaWNvZGVWYWxpZGF0b3IuanMnO1xuaW1wb3J0IHsgbG9nZ2VyIH0gZnJvbSAnLi4vLi4vdXRpbHMvbG9nZ2VyLmpzJztcblxuLyoqIFF1ZXJ5IHBhcmFtZXRlciBuYW1lIHVzZWQgYXMgYSBmYWxsYmFjayBmb3IgU1NFIHN0cmVhbXMuICovXG5jb25zdCBUT0tFTl9RVUVSWV9QQVJBTSA9ICd0b2tlbic7XG5cbi8qKiBIZWFkZXIgbmFtZSB3ZSBsb29rIGF0IGZvciBCZWFyZXIgdG9rZW5zLiAqL1xuY29uc3QgQVVUSF9IRUFERVIgPSAnYXV0aG9yaXphdGlvbic7XG5cbi8qKiBQcmVmaXggZXhwZWN0ZWQgb24gdGhlIEF1dGhvcml6YXRpb24gaGVhZGVyIHZhbHVlLiAqL1xuY29uc3QgQkVBUkVSX1BSRUZJWCA9ICdCZWFyZXIgJztcblxuLyoqXG4gKiBTdHJpY3QgZm9ybWF0IGZvciBjb25zb2xlIHRva2VucyDigJQgNjQgbG93ZXJjYXNlIGhleCBjaGFyYWN0ZXJzICgyNTYgYml0cykuXG4gKiBBbnkgcHJlc2VudGVkIHRva2VuIHRoYXQgZG9lcyBub3QgbWF0Y2ggdGhpcyBwYXR0ZXJuIGlzIHJlamVjdGVkIGJlZm9yZSBpdFxuICogcmVhY2hlcyB0aGUgY29uc3RhbnQtdGltZSBjb21wYXJpc29uLiBUaGlzIGJsb2NrcyBhbnkgbm9uLUFTQ0lJIFVuaWNvZGVcbiAqIHBheWxvYWQgKGhvbW9ncmFwaHMsIHplcm8td2lkdGgsIGJpZGkgb3ZlcnJpZGVzLCBldGMuKSBmcm9tIGV2ZXIgdG91Y2hpbmdcbiAqIHRoZSB2ZXJpZnkgcGF0aC4gRE1DUC1TRUMtMDA0IG1pdGlnYXRpb24uXG4gKi9cbmNvbnN0IFRPS0VOX0ZPUk1BVCA9IC9eWzAtOWEtZl17NjR9JC87XG5cbi8qKlxuICogU2FuaXRpemUgYSByYXcgdG9rZW4gc3RyaW5nIHB1bGxlZCBmcm9tIGEgcmVxdWVzdC5cbiAqXG4gKiAtIE5vcm1hbGl6ZXMgdG8gTkZDIHZpYSBVbmljb2RlVmFsaWRhdG9yIChETUNQLVNFQy0wMDQpXG4gKiAtIFZhbGlkYXRlcyBhZ2FpbnN0IHRoZSBzdHJpY3QgaGV4IGZvcm1hdFxuICogLSBSZXR1cm5zIHRoZSBjbGVhbmVkIHZhbHVlLCBvciBudWxsIGlmIGFueXRoaW5nIGxvb2tzIHdyb25nXG4gKlxuICogQW55IGZhaWx1cmUgaGVyZSByZXN1bHRzIGluIGEgNDAxIGF0IHRoZSBjYWxsIHNpdGUuIExlZ2l0aW1hdGUgdG9rZW5zIGFyZVxuICogNjQtY2hhciBsb3dlcmNhc2UgaGV4LCBzbyBub3JtYWxpemF0aW9uIGFuZCBmb3JtYXQgdmFsaWRhdGlvbiBhcmUgZWZmZWN0aXZlbHlcbiAqIG5vLW9wcyBmb3IgdmFsaWQgaW5wdXQgYW5kIGhhcmQgcmVqZWN0aW9ucyBmb3IgYW55dGhpbmcgbWFsaWNpb3VzLlxuICovXG5mdW5jdGlvbiBzYW5pdGl6ZVByZXNlbnRlZFRva2VuKHJhdzogc3RyaW5nKTogc3RyaW5nIHwgbnVsbCB7XG4gIGlmICghcmF3KSByZXR1cm4gbnVsbDtcbiAgY29uc3Qgbm9ybWFsaXplZCA9IFVuaWNvZGVWYWxpZGF0b3Iubm9ybWFsaXplKHJhdykubm9ybWFsaXplZENvbnRlbnQ7XG4gIGlmICghVE9LRU5fRk9STUFULnRlc3Qobm9ybWFsaXplZCkpIHJldHVybiBudWxsO1xuICByZXR1cm4gbm9ybWFsaXplZDtcbn1cblxuLyoqXG4gKiBFeHRyYWN0IGEgQmVhcmVyIHRva2VuIGZyb20gYSByZXF1ZXN0LlxuICogQ2hlY2tzIEF1dGhvcml6YXRpb24gaGVhZGVyIGZpcnN0LCB0aGVuIHF1ZXJ5IHBhcmFtZXRlci5cbiAqIEFwcGxpZXMgVW5pY29kZSBub3JtYWxpemF0aW9uIGFuZCBzdHJpY3QgZm9ybWF0IHZhbGlkYXRpb24gYmVmb3JlIHJldHVybmluZy5cbiAqIFJldHVybnMgdGhlIHNhbml0aXplZCB0b2tlbiBzdHJpbmcsIG9yIG51bGwgaWYgbm9uZSB3YXMgZm91bmQgb3IgdGhlIHZhbHVlXG4gKiBmYWlsZWQgdmFsaWRhdGlvbi5cbiAqL1xuZnVuY3Rpb24gZXh0cmFjdFRva2VuKHJlcTogUmVxdWVzdCk6IHN0cmluZyB8IG51bGwge1xuICAvLyBQcmVmZXJyZWQ6IEF1dGhvcml6YXRpb246IEJlYXJlciA8dG9rZW4+XG4gIGNvbnN0IGhlYWRlciA9IHJlcS5oZWFkZXJzW0FVVEhfSEVBREVSXTtcbiAgaWYgKHR5cGVvZiBoZWFkZXIgPT09ICdzdHJpbmcnICYmIGhlYWRlci5zdGFydHNXaXRoKEJFQVJFUl9QUkVGSVgpKSB7XG4gICAgY29uc3QgdmFsdWUgPSBoZWFkZXIuc2xpY2UoQkVBUkVSX1BSRUZJWC5sZW5ndGgpLnRyaW0oKTtcbiAgICBpZiAodmFsdWUpIHtcbiAgICAgIGNvbnN0IHNhbml0aXplZCA9IHNhbml0aXplUHJlc2VudGVkVG9rZW4odmFsdWUpO1xuICAgICAgaWYgKHNhbml0aXplZCkgcmV0dXJuIHNhbml0aXplZDtcbiAgICB9XG4gIH1cblxuICAvLyBGYWxsYmFjayBmb3IgRXZlbnRTb3VyY2U6ID90b2tlbj08dG9rZW4+XG4gIGNvbnN0IHEgPSByZXEucXVlcnlbVE9LRU5fUVVFUllfUEFSQU1dO1xuICBpZiAodHlwZW9mIHEgPT09ICdzdHJpbmcnICYmIHEubGVuZ3RoID4gMCkge1xuICAgIHJldHVybiBzYW5pdGl6ZVByZXNlbnRlZFRva2VuKHEpO1xuICB9XG4gIGlmIChBcnJheS5pc0FycmF5KHEpICYmIHEubGVuZ3RoID4gMCAmJiB0eXBlb2YgcVswXSA9PT0gJ3N0cmluZycpIHtcbiAgICByZXR1cm4gc2FuaXRpemVQcmVzZW50ZWRUb2tlbihxWzBdKTtcbiAgfVxuXG4gIHJldHVybiBudWxsO1xufVxuXG4vKipcbiAqIE9wdGlvbnMgZm9yIHRoZSBhdXRoIG1pZGRsZXdhcmUgZmFjdG9yeS5cbiAqL1xuZXhwb3J0IGludGVyZmFjZSBBdXRoTWlkZGxld2FyZU9wdGlvbnMge1xuICAvKiogVGhlIHRva2VuIHN0b3JlIGhvbGRpbmcgdmFsaWQgdG9rZW5zLiAqL1xuICBzdG9yZTogQ29uc29sZVRva2VuU3RvcmU7XG4gIC8qKiBXaGV0aGVyIGF1dGggaXMgZW5mb3JjZWQuIFdoZW4gZmFsc2UsIG1pZGRsZXdhcmUgaXMgYSBuby1vcC4gKi9cbiAgZW5hYmxlZDogYm9vbGVhbjtcbiAgLyoqXG4gICAqIFBhdGggcHJlZml4ZXMgdGhhdCBhcmUgbmV2ZXIgcHJvdGVjdGVkLiBBIHJlcXVlc3Qgd2hvc2UgVVJMIHBhdGggc3RhcnRzXG4gICAqIHdpdGggYW55IG9mIHRoZXNlIHN0cmluZ3Mgd2lsbCBza2lwIGF1dGggYW5kIGJlIHBhc3NlZCB0aHJvdWdoIHRvIHRoZVxuICAgKiBuZXh0IGhhbmRsZXIuIFVzZWQgdG8gZXhlbXB0IGhlYWx0aCBjaGVja3MsIHZlcnNpb24gaW5mbywgY2xpZW50IGRldGVjdGlvbixcbiAgICogYW5kIHNpbWlsYXIgcHVibGljIG1ldGFkYXRhIGVuZHBvaW50cy5cbiAgICpcbiAgICogUGF0aHMgYXJlIGNvbXBhcmVkIGFnYWluc3QgYHJlcS5wYXRoYCAodGhlIHJvdXRlLXJlbGF0aXZlIHBhdGgpLCBzbyBpbmNsdWRlXG4gICAqIHRoZSBmdWxsIHBhdGhuYW1lIHN0YXJ0aW5nIHdpdGggYC9gIOKAlCBlLmcuIGAvYXBpL2hlYWx0aGAsIGAvYXBpL3NldHVwL3ZlcnNpb25gLlxuICAgKi9cbiAgcHVibGljUGF0aFByZWZpeGVzPzogc3RyaW5nW107XG4gIC8qKiBPcHRpb25hbCBsYWJlbCBmb3IgbG9nIG1lc3NhZ2VzIChlLmcuIFwiYXBpXCIgb3IgXCJzc2VcIikuICovXG4gIGxhYmVsPzogc3RyaW5nO1xufVxuXG4vKipcbiAqIENyZWF0ZSB0aGUgY29yZSBhdXRoZW50aWNhdGlvbiBtaWRkbGV3YXJlLlxuICpcbiAqIFRoZSByZXR1cm5lZCBoYW5kbGVyIGVuZm9yY2VzIEJlYXJlciB0b2tlbiBhdXRoIG9uIGV2ZXJ5IHJlcXVlc3QgaXQgc2Vlcy5cbiAqIE1vdW50IGl0IHdpdGggYGFwcC51c2UoY3JlYXRlQXV0aE1pZGRsZXdhcmUoLi4uKSlgIGJlZm9yZSBwcm90ZWN0ZWQgcm91dGVycyxcbiAqIG9yIGF0dGFjaCBpdCB0byBpbmRpdmlkdWFsIHJvdXRlcyB0aGF0IG5lZWQgcHJvdGVjdGlvbi5cbiAqXG4gKiBXaGVuIGBlbmFibGVkOiBmYWxzZWAsIHRoZSBoYW5kbGVyIGltbWVkaWF0ZWx5IGNhbGxzIGBuZXh0KClgIOKAlCBhbGxvd2luZ1xuICogdGhlIGluZnJhc3RydWN0dXJlIHRvIGxhbmQgd2l0aCB0aGUgZGVmYXVsdC1vZmYgZmVhdHVyZSBmbGFnIHdpdGhvdXRcbiAqIGJyZWFraW5nIGV4aXN0aW5nIHRyYWZmaWMuXG4gKlxuICogUGhhc2UgMyBoYXJkZW5pbmcgKHRyYWNrZWQgaW4gIzE3ODkpOiBBZGQgNDAxIHJhdGUgbGltaXRpbmcgdG8gcHJldmVudFxuICogRG9TIGZyb20gZmxvb2RzIG9mIGJhZC10b2tlbiByZXF1ZXN0cy4gQnJ1dGUtZm9yY2luZyBhIDI1Ni1iaXQgdG9rZW4gaXNcbiAqIGluZmVhc2libGUsIGJ1dCBhbiBhdHRhY2tlciBmbG9vZGluZyAvYXBpIHdpdGggd3JvbmcgdG9rZW5zIGNvdWxkIHNhdHVyYXRlXG4gKiB0aGUgdmVyaWZ5IHBhdGguIEEgc2xpZGluZy13aW5kb3cgbGltaXRlciBrZXllZCBvbiB0aGUgcmVxdWVzdGluZyBJUCBpc1xuICogdGhlIHJpZ2h0IHNoYXBlLlxuICovXG5leHBvcnQgZnVuY3Rpb24gY3JlYXRlQXV0aE1pZGRsZXdhcmUob3B0aW9uczogQXV0aE1pZGRsZXdhcmVPcHRpb25zKTogUmVxdWVzdEhhbmRsZXIge1xuICBjb25zdCB7IHN0b3JlLCBlbmFibGVkLCBsYWJlbCA9ICdjb25zb2xlJyB9ID0gb3B0aW9ucztcbiAgY29uc3QgcHVibGljUGF0aHMgPSBvcHRpb25zLnB1YmxpY1BhdGhQcmVmaXhlcyA/PyBbXTtcblxuICByZXR1cm4gKHJlcTogUmVxdWVzdCwgcmVzOiBSZXNwb25zZSwgbmV4dDogTmV4dEZ1bmN0aW9uKSA9PiB7XG4gICAgaWYgKCFlbmFibGVkKSB7XG4gICAgICByZXR1cm4gbmV4dCgpO1xuICAgIH1cblxuICAgIC8vIFB1YmxpYyBwYXRoIGFsbG93bGlzdCDigJQgc2tpcCBhdXRoIGZvciB3aGl0ZWxpc3RlZCBwcmVmaXhlcy5cbiAgICAvLyBVc2Ugb3JpZ2luYWxVcmwucGF0aG5hbWUgc28gd2UgbWF0Y2ggcmVnYXJkbGVzcyBvZiBtb3VudCBwb2ludC5cbiAgICBjb25zdCBwYXRoVG9DaGVjayA9IHJlcS5vcmlnaW5hbFVybC5zcGxpdCgnPycpWzBdO1xuICAgIGZvciAoY29uc3QgcHJlZml4IG9mIHB1YmxpY1BhdGhzKSB7XG4gICAgICBpZiAocGF0aFRvQ2hlY2sgPT09IHByZWZpeCB8fCBwYXRoVG9DaGVjay5zdGFydHNXaXRoKHByZWZpeCArICcvJykpIHtcbiAgICAgICAgcmV0dXJuIG5leHQoKTtcbiAgICAgIH1cbiAgICB9XG5cbiAgICBjb25zdCBwcmVzZW50ZWQgPSBleHRyYWN0VG9rZW4ocmVxKTtcbiAgICBpZiAoIXByZXNlbnRlZCkge1xuICAgICAgcmV0dXJuIHJlc3BvbmRVbmF1dGhvcml6ZWQocmVzLCAnbWlzc2luZ190b2tlbicsIGxhYmVsLCBzdG9yZSwgMCk7XG4gICAgfVxuXG4gICAgY29uc3QgZW50cnkgPSBzdG9yZS52ZXJpZnkocHJlc2VudGVkKTtcbiAgICBpZiAoIWVudHJ5KSB7XG4gICAgICAvLyBMb2cgcHJlc2VudGVkLWxlbmd0aCBvbmx5IOKAlCBuZXZlciB0aGUgcHJlc2VudGVkIHZhbHVlIGl0c2VsZi5cbiAgICAgIC8vIERpc3Rpbmd1aXNoZXMgXCJ0b2tlbiBtaXNzaW5nXCIgZnJvbSBcInRva2VuIHdyb25nIGxlbmd0aFwiIGZyb21cbiAgICAgIC8vIFwibGVuZ3RoIG1hdGNoZXMgYnV0IGNvbnRlbnQgZGlmZmVyc1wiIHdoZW4gdHJvdWJsZXNob290aW5nLlxuICAgICAgcmV0dXJuIHJlc3BvbmRVbmF1dGhvcml6ZWQocmVzLCAnaW52YWxpZF90b2tlbicsIGxhYmVsLCBzdG9yZSwgcHJlc2VudGVkLmxlbmd0aCk7XG4gICAgfVxuXG4gICAgLy8gU3R1YmJlZCBhdXRob3JpemF0aW9uIGhvb2sg4oCUIFBoYXNlIDIgZmxpcHMgcmVhbCBzY29wZSBjaGVja3Mgb24gaGVyZS5cbiAgICBpZiAoIWF1dGhvcml6ZVNjb3BlKGVudHJ5LCByZXEpKSB7XG4gICAgICByZXR1cm4gcmVzcG9uZEZvcmJpZGRlbihyZXMsICdzY29wZV9kZW5pZWQnLCBsYWJlbCwgZW50cnkpO1xuICAgIH1cblxuICAgIC8vIFN0YXNoIHRoZSBtYXRjaGVkIGVudHJ5IGZvciBkb3duc3RyZWFtIGhhbmRsZXJzIGFuZCBsb2cgc3VjY2VzcyBhdCBkZWJ1Zy5cbiAgICByZXMubG9jYWxzLnRva2VuRW50cnkgPSBlbnRyeTtcbiAgICBsb2dnZXIuZGVidWcoYFtBdXRoOiR7bGFiZWx9XSB2ZXJpZmllZCB0b2tlbiBpZD0ke2VudHJ5LmlkfSBuYW1lPVwiJHtlbnRyeS5uYW1lfVwiIHJvdXRlPSR7cmVxLm1ldGhvZH0gJHtyZXEub3JpZ2luYWxVcmwuc3BsaXQoJz8nKVswXX1gKTtcbiAgICByZXR1cm4gbmV4dCgpO1xuICB9O1xufVxuXG4vKipcbiAqIFNjb3BlIGF1dGhvcml6YXRpb24gaG9vay5cbiAqXG4gKiBQaGFzZSAxOiBldmVyeSB2YWxpZCB0b2tlbiBpcyB0cmVhdGVkIGFzIGFkbWluIOKAlCByZXR1cm5zIHRydWUgdW5jb25kaXRpb25hbGx5LlxuICogUGhhc2UgMjogdGhpcyBmdW5jdGlvbiB3aWxsIGNoZWNrIGBlbnRyeS5zY29wZXNgIGFnYWluc3QgdGhlIHJvdXRlJ3MgcmVxdWlyZWRcbiAqIHNjb3BlcyAod2hpY2ggY2FuIGJlIGF0dGFjaGVkIHZpYSBgcmVzLmxvY2Fscy5yZXF1aXJlZFNjb3BlYCBvciBhIHJvdXRlXG4gKiBtZXRhZGF0YSBzeXN0ZW0pLlxuICovXG5mdW5jdGlvbiBhdXRob3JpemVTY29wZShfZW50cnk6IENvbnNvbGVUb2tlbkVudHJ5LCBfcmVxOiBSZXF1ZXN0KTogYm9vbGVhbiB7XG4gIHJldHVybiB0cnVlO1xufVxuXG4vKipcbiAqIFJlc3BvbmQgd2l0aCA0MDEgVW5hdXRob3JpemVkIGFuZCBhIGhlbHBmdWwgaGludCBhYm91dCB3aGVyZSB0byBmaW5kIHRoZSB0b2tlbi5cbiAqIEluY2x1ZGVzIHByZXNlbnRlZC10b2tlbiBsZW5ndGggYXQgZGVidWcgbGV2ZWwgZm9yIHRyb3VibGVzaG9vdGluZyDigJQgbmV2ZXIgdGhlIHZhbHVlLlxuICovXG5mdW5jdGlvbiByZXNwb25kVW5hdXRob3JpemVkKFxuICByZXM6IFJlc3BvbnNlLFxuICByZWFzb246ICdtaXNzaW5nX3Rva2VuJyB8ICdpbnZhbGlkX3Rva2VuJyxcbiAgbGFiZWw6IHN0cmluZyxcbiAgc3RvcmU6IENvbnNvbGVUb2tlblN0b3JlLFxuICBwcmVzZW50ZWRMZW5ndGg6IG51bWJlcixcbik6IHZvaWQge1xuICBsb2dnZXIuZGVidWcoYFtBdXRoOiR7bGFiZWx9XSA0MDEgJHtyZWFzb259IHByZXNlbnRlZExlbmd0aD0ke3ByZXNlbnRlZExlbmd0aH1gKTtcbiAgcmVzLnN0YXR1cyg0MDEpLmpzb24oe1xuICAgIGVycm9yOiAnQXV0aGVudGljYXRpb24gcmVxdWlyZWQnLFxuICAgIHJlYXNvbixcbiAgICBoaW50OiBgVG9rZW4gZmlsZTogJHtzdG9yZS5nZXRGaWxlUGF0aCgpfS4gU2VuZCAnQXV0aG9yaXphdGlvbjogQmVhcmVyIDx0b2tlbj4nIGhlYWRlciwgb3IgYXBwZW5kID90b2tlbj08dG9rZW4+IGZvciBTU0Ugc3RyZWFtcy5gLFxuICB9KTtcbn1cblxuLyoqXG4gKiBSZXNwb25kIHdpdGggNDAzIEZvcmJpZGRlbiDigJQgdG9rZW4gd2FzIHZhbGlkIGJ1dCBzY29wZSBkaWQgbm90IHBlcm1pdCB0aGlzIHJvdXRlLlxuICogUGhhc2UgMSBuZXZlciByZWFjaGVzIGhlcmUgYmVjYXVzZSBgYXV0aG9yaXplU2NvcGVgIGFsd2F5cyByZXR1cm5zIHRydWUsIGJ1dFxuICogdGhlIGNvZGUgcGF0aCBleGlzdHMgc28gUGhhc2UgMiBjYW4gd2lyZSBpdCB1cCB3aXRob3V0IGNoYW5naW5nIHRoZSBtaWRkbGV3YXJlIHNoYXBlLlxuICovXG5mdW5jdGlvbiByZXNwb25kRm9yYmlkZGVuKFxuICByZXM6IFJlc3BvbnNlLFxuICByZWFzb246IHN0cmluZyxcbiAgbGFiZWw6IHN0cmluZyxcbiAgZW50cnk6IENvbnNvbGVUb2tlbkVudHJ5LFxuKTogdm9pZCB7XG4gIGxvZ2dlci5kZWJ1ZyhgW0F1dGg6JHtsYWJlbH1dIDQwMyAke3JlYXNvbn1gLCB7IHRva2VuSWQ6IGVudHJ5LmlkLCBzY29wZXM6IGVudHJ5LnNjb3BlcyB9KTtcbiAgcmVzLnN0YXR1cyg0MDMpLmpzb24oe1xuICAgIGVycm9yOiAnVG9rZW4gc2NvcGUgZG9lcyBub3QgcGVybWl0IHRoaXMgYWN0aW9uJyxcbiAgICByZWFzb24sXG4gIH0pO1xufVxuIl19
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for console auth route handlers (TOTP + token management).
|
|
3
|
+
*
|
|
4
|
+
* Extracted to eliminate duplication between totpRoutes.ts and tokenRoutes.ts.
|
|
5
|
+
* Both routers need the same error-mapping, structured-error-response, and
|
|
6
|
+
* body-field-extraction logic.
|
|
7
|
+
*
|
|
8
|
+
* @since v2.1.0 — Issue #1795
|
|
9
|
+
*/
|
|
10
|
+
import type { Response } from 'express';
|
|
11
|
+
import type { TotpErrorCode } from '../console/consoleToken.js';
|
|
12
|
+
/**
|
|
13
|
+
* Maps a store error code to the appropriate HTTP status.
|
|
14
|
+
* Centralized so all console auth endpoints return consistent status codes
|
|
15
|
+
* for the same failure class. The `satisfies never` exhaustiveness check
|
|
16
|
+
* ensures new TotpErrorCode values get mapped at compile time.
|
|
17
|
+
*/
|
|
18
|
+
export declare function httpStatusForStoreError(code: TotpErrorCode): number;
|
|
19
|
+
/**
|
|
20
|
+
* Send a structured error response with both a human-readable message and
|
|
21
|
+
* a machine-readable code. Shape is stable so the CLI and Security tab UI
|
|
22
|
+
* can branch on `code` instead of string-matching the message.
|
|
23
|
+
*/
|
|
24
|
+
export declare function sendStoreError(res: Response, status: number, code: string, message: string): void;
|
|
25
|
+
/**
|
|
26
|
+
* Safely extract a string field from an unknown request body and NFC-normalize
|
|
27
|
+
* it (DMCP-SEC-004). Returns null if the body is not an object or the field
|
|
28
|
+
* is missing / not a string. Normalization blocks homograph, bidi, and
|
|
29
|
+
* zero-width abuse before the value reaches downstream parsers (TOTP codes,
|
|
30
|
+
* otpauth URI labels, pending-id lookups).
|
|
31
|
+
*/
|
|
32
|
+
export declare function getNormalizedStringField(body: unknown, field: string): string | null;
|
|
33
|
+
//# sourceMappingURL=consoleRouteHelpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consoleRouteHelpers.d.ts","sourceRoot":"","sources":["../../../src/web/routes/consoleRouteHelpers.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAGhE;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,CAmBnE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,QAAQ,EACb,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GACd,IAAI,CAEN;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAKpF"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for console auth route handlers (TOTP + token management).
|
|
3
|
+
*
|
|
4
|
+
* Extracted to eliminate duplication between totpRoutes.ts and tokenRoutes.ts.
|
|
5
|
+
* Both routers need the same error-mapping, structured-error-response, and
|
|
6
|
+
* body-field-extraction logic.
|
|
7
|
+
*
|
|
8
|
+
* @since v2.1.0 — Issue #1795
|
|
9
|
+
*/
|
|
10
|
+
import { UnicodeValidator } from '../../security/validators/unicodeValidator.js';
|
|
11
|
+
/**
|
|
12
|
+
* Maps a store error code to the appropriate HTTP status.
|
|
13
|
+
* Centralized so all console auth endpoints return consistent status codes
|
|
14
|
+
* for the same failure class. The `satisfies never` exhaustiveness check
|
|
15
|
+
* ensures new TotpErrorCode values get mapped at compile time.
|
|
16
|
+
*/
|
|
17
|
+
export function httpStatusForStoreError(code) {
|
|
18
|
+
switch (code) {
|
|
19
|
+
case 'ALREADY_ENROLLED':
|
|
20
|
+
return 409;
|
|
21
|
+
case 'NOT_ENROLLED':
|
|
22
|
+
case 'PENDING_NOT_FOUND':
|
|
23
|
+
case 'INVALID_TOTP_CODE':
|
|
24
|
+
return 400;
|
|
25
|
+
case 'TOO_MANY_PENDING':
|
|
26
|
+
return 429;
|
|
27
|
+
case 'TOTP_REQUIRED':
|
|
28
|
+
return 403;
|
|
29
|
+
case 'STORE_NOT_INITIALIZED':
|
|
30
|
+
return 503;
|
|
31
|
+
default: {
|
|
32
|
+
code;
|
|
33
|
+
return 400;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Send a structured error response with both a human-readable message and
|
|
39
|
+
* a machine-readable code. Shape is stable so the CLI and Security tab UI
|
|
40
|
+
* can branch on `code` instead of string-matching the message.
|
|
41
|
+
*/
|
|
42
|
+
export function sendStoreError(res, status, code, message) {
|
|
43
|
+
res.status(status).json({ error: message, code });
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Safely extract a string field from an unknown request body and NFC-normalize
|
|
47
|
+
* it (DMCP-SEC-004). Returns null if the body is not an object or the field
|
|
48
|
+
* is missing / not a string. Normalization blocks homograph, bidi, and
|
|
49
|
+
* zero-width abuse before the value reaches downstream parsers (TOTP codes,
|
|
50
|
+
* otpauth URI labels, pending-id lookups).
|
|
51
|
+
*/
|
|
52
|
+
export function getNormalizedStringField(body, field) {
|
|
53
|
+
if (!body || typeof body !== 'object')
|
|
54
|
+
return null;
|
|
55
|
+
const val = body[field];
|
|
56
|
+
if (typeof val !== 'string')
|
|
57
|
+
return null;
|
|
58
|
+
return UnicodeValidator.normalize(val).normalizedContent;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29uc29sZVJvdXRlSGVscGVycy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy93ZWIvcm91dGVzL2NvbnNvbGVSb3V0ZUhlbHBlcnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7O0dBUUc7QUFJSCxPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSwrQ0FBK0MsQ0FBQztBQUVqRjs7Ozs7R0FLRztBQUNILE1BQU0sVUFBVSx1QkFBdUIsQ0FBQyxJQUFtQjtJQUN6RCxRQUFRLElBQUksRUFBRSxDQUFDO1FBQ2IsS0FBSyxrQkFBa0I7WUFDckIsT0FBTyxHQUFHLENBQUM7UUFDYixLQUFLLGNBQWMsQ0FBQztRQUNwQixLQUFLLG1CQUFtQixDQUFDO1FBQ3pCLEtBQUssbUJBQW1CO1lBQ3RCLE9BQU8sR0FBRyxDQUFDO1FBQ2IsS0FBSyxrQkFBa0I7WUFDckIsT0FBTyxHQUFHLENBQUM7UUFDYixLQUFLLGVBQWU7WUFDbEIsT0FBTyxHQUFHLENBQUM7UUFDYixLQUFLLHVCQUF1QjtZQUMxQixPQUFPLEdBQUcsQ0FBQztRQUNiLE9BQU8sQ0FBQyxDQUFDLENBQUM7WUFDUixJQUFvQixDQUFDO1lBQ3JCLE9BQU8sR0FBRyxDQUFDO1FBQ2IsQ0FBQztJQUNILENBQUM7QUFDSCxDQUFDO0FBRUQ7Ozs7R0FJRztBQUNILE1BQU0sVUFBVSxjQUFjLENBQzVCLEdBQWEsRUFDYixNQUFjLEVBQ2QsSUFBWSxFQUNaLE9BQWU7SUFFZixHQUFHLENBQUMsTUFBTSxDQUFDLE1BQU0sQ0FBQyxDQUFDLElBQUksQ0FBQyxFQUFFLEtBQUssRUFBRSxPQUFPLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQztBQUNwRCxDQUFDO0FBRUQ7Ozs7OztHQU1HO0FBQ0gsTUFBTSxVQUFVLHdCQUF3QixDQUFDLElBQWEsRUFBRSxLQUFhO0lBQ25FLElBQUksQ0FBQyxJQUFJLElBQUksT0FBTyxJQUFJLEtBQUssUUFBUTtRQUFFLE9BQU8sSUFBSSxDQUFDO0lBQ25ELE1BQU0sR0FBRyxHQUFJLElBQWdDLENBQUMsS0FBSyxDQUFDLENBQUM7SUFDckQsSUFBSSxPQUFPLEdBQUcsS0FBSyxRQUFRO1FBQUUsT0FBTyxJQUFJLENBQUM7SUFDekMsT0FBTyxnQkFBZ0IsQ0FBQyxTQUFTLENBQUMsR0FBRyxDQUFDLENBQUMsaUJBQWlCLENBQUM7QUFDM0QsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogU2hhcmVkIGhlbHBlcnMgZm9yIGNvbnNvbGUgYXV0aCByb3V0ZSBoYW5kbGVycyAoVE9UUCArIHRva2VuIG1hbmFnZW1lbnQpLlxuICpcbiAqIEV4dHJhY3RlZCB0byBlbGltaW5hdGUgZHVwbGljYXRpb24gYmV0d2VlbiB0b3RwUm91dGVzLnRzIGFuZCB0b2tlblJvdXRlcy50cy5cbiAqIEJvdGggcm91dGVycyBuZWVkIHRoZSBzYW1lIGVycm9yLW1hcHBpbmcsIHN0cnVjdHVyZWQtZXJyb3ItcmVzcG9uc2UsIGFuZFxuICogYm9keS1maWVsZC1leHRyYWN0aW9uIGxvZ2ljLlxuICpcbiAqIEBzaW5jZSB2Mi4xLjAg4oCUIElzc3VlICMxNzk1XG4gKi9cblxuaW1wb3J0IHR5cGUgeyBSZXNwb25zZSB9IGZyb20gJ2V4cHJlc3MnO1xuaW1wb3J0IHR5cGUgeyBUb3RwRXJyb3JDb2RlIH0gZnJvbSAnLi4vY29uc29sZS9jb25zb2xlVG9rZW4uanMnO1xuaW1wb3J0IHsgVW5pY29kZVZhbGlkYXRvciB9IGZyb20gJy4uLy4uL3NlY3VyaXR5L3ZhbGlkYXRvcnMvdW5pY29kZVZhbGlkYXRvci5qcyc7XG5cbi8qKlxuICogTWFwcyBhIHN0b3JlIGVycm9yIGNvZGUgdG8gdGhlIGFwcHJvcHJpYXRlIEhUVFAgc3RhdHVzLlxuICogQ2VudHJhbGl6ZWQgc28gYWxsIGNvbnNvbGUgYXV0aCBlbmRwb2ludHMgcmV0dXJuIGNvbnNpc3RlbnQgc3RhdHVzIGNvZGVzXG4gKiBmb3IgdGhlIHNhbWUgZmFpbHVyZSBjbGFzcy4gVGhlIGBzYXRpc2ZpZXMgbmV2ZXJgIGV4aGF1c3RpdmVuZXNzIGNoZWNrXG4gKiBlbnN1cmVzIG5ldyBUb3RwRXJyb3JDb2RlIHZhbHVlcyBnZXQgbWFwcGVkIGF0IGNvbXBpbGUgdGltZS5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGh0dHBTdGF0dXNGb3JTdG9yZUVycm9yKGNvZGU6IFRvdHBFcnJvckNvZGUpOiBudW1iZXIge1xuICBzd2l0Y2ggKGNvZGUpIHtcbiAgICBjYXNlICdBTFJFQURZX0VOUk9MTEVEJzpcbiAgICAgIHJldHVybiA0MDk7XG4gICAgY2FzZSAnTk9UX0VOUk9MTEVEJzpcbiAgICBjYXNlICdQRU5ESU5HX05PVF9GT1VORCc6XG4gICAgY2FzZSAnSU5WQUxJRF9UT1RQX0NPREUnOlxuICAgICAgcmV0dXJuIDQwMDtcbiAgICBjYXNlICdUT09fTUFOWV9QRU5ESU5HJzpcbiAgICAgIHJldHVybiA0Mjk7XG4gICAgY2FzZSAnVE9UUF9SRVFVSVJFRCc6XG4gICAgICByZXR1cm4gNDAzO1xuICAgIGNhc2UgJ1NUT1JFX05PVF9JTklUSUFMSVpFRCc6XG4gICAgICByZXR1cm4gNTAzO1xuICAgIGRlZmF1bHQ6IHtcbiAgICAgIGNvZGUgc2F0aXNmaWVzIG5ldmVyO1xuICAgICAgcmV0dXJuIDQwMDtcbiAgICB9XG4gIH1cbn1cblxuLyoqXG4gKiBTZW5kIGEgc3RydWN0dXJlZCBlcnJvciByZXNwb25zZSB3aXRoIGJvdGggYSBodW1hbi1yZWFkYWJsZSBtZXNzYWdlIGFuZFxuICogYSBtYWNoaW5lLXJlYWRhYmxlIGNvZGUuIFNoYXBlIGlzIHN0YWJsZSBzbyB0aGUgQ0xJIGFuZCBTZWN1cml0eSB0YWIgVUlcbiAqIGNhbiBicmFuY2ggb24gYGNvZGVgIGluc3RlYWQgb2Ygc3RyaW5nLW1hdGNoaW5nIHRoZSBtZXNzYWdlLlxuICovXG5leHBvcnQgZnVuY3Rpb24gc2VuZFN0b3JlRXJyb3IoXG4gIHJlczogUmVzcG9uc2UsXG4gIHN0YXR1czogbnVtYmVyLFxuICBjb2RlOiBzdHJpbmcsXG4gIG1lc3NhZ2U6IHN0cmluZyxcbik6IHZvaWQge1xuICByZXMuc3RhdHVzKHN0YXR1cykuanNvbih7IGVycm9yOiBtZXNzYWdlLCBjb2RlIH0pO1xufVxuXG4vKipcbiAqIFNhZmVseSBleHRyYWN0IGEgc3RyaW5nIGZpZWxkIGZyb20gYW4gdW5rbm93biByZXF1ZXN0IGJvZHkgYW5kIE5GQy1ub3JtYWxpemVcbiAqIGl0IChETUNQLVNFQy0wMDQpLiBSZXR1cm5zIG51bGwgaWYgdGhlIGJvZHkgaXMgbm90IGFuIG9iamVjdCBvciB0aGUgZmllbGRcbiAqIGlzIG1pc3NpbmcgLyBub3QgYSBzdHJpbmcuIE5vcm1hbGl6YXRpb24gYmxvY2tzIGhvbW9ncmFwaCwgYmlkaSwgYW5kXG4gKiB6ZXJvLXdpZHRoIGFidXNlIGJlZm9yZSB0aGUgdmFsdWUgcmVhY2hlcyBkb3duc3RyZWFtIHBhcnNlcnMgKFRPVFAgY29kZXMsXG4gKiBvdHBhdXRoIFVSSSBsYWJlbHMsIHBlbmRpbmctaWQgbG9va3VwcykuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBnZXROb3JtYWxpemVkU3RyaW5nRmllbGQoYm9keTogdW5rbm93biwgZmllbGQ6IHN0cmluZyk6IHN0cmluZyB8IG51bGwge1xuICBpZiAoIWJvZHkgfHwgdHlwZW9mIGJvZHkgIT09ICdvYmplY3QnKSByZXR1cm4gbnVsbDtcbiAgY29uc3QgdmFsID0gKGJvZHkgYXMgUmVjb3JkPHN0cmluZywgdW5rbm93bj4pW2ZpZWxkXTtcbiAgaWYgKHR5cGVvZiB2YWwgIT09ICdzdHJpbmcnKSByZXR1cm4gbnVsbDtcbiAgcmV0dXJuIFVuaWNvZGVWYWxpZGF0b3Iubm9ybWFsaXplKHZhbCkubm9ybWFsaXplZENvbnRlbnQ7XG59XG4iXX0=
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console token management HTTP routes — #1795.
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - POST /api/console/token/rotate — rotate the primary token with TOTP confirmation
|
|
6
|
+
*
|
|
7
|
+
* Security model:
|
|
8
|
+
* - All endpoints require a valid existing console token. Enforcement
|
|
9
|
+
* happens via an always-on `createAuthMiddleware` instance mounted at the
|
|
10
|
+
* top of this router, independent of `DOLLHOUSE_WEB_AUTH_ENABLED`.
|
|
11
|
+
* - Rotation additionally requires TOTP confirmation (Pattern B). Pattern A
|
|
12
|
+
* (OS dialog fallback) is deferred to a follow-up issue.
|
|
13
|
+
* - A sliding-window rate limit throttles rotation attempts so a bad actor
|
|
14
|
+
* with a live session can't brute-force TOTP codes by flooding rotations.
|
|
15
|
+
*
|
|
16
|
+
* @since v2.1.0 — Issue #1795
|
|
17
|
+
*/
|
|
18
|
+
import { Router } from 'express';
|
|
19
|
+
import { type ConsoleTokenStore } from '../console/consoleToken.js';
|
|
20
|
+
/**
|
|
21
|
+
* Options for the token routes factory.
|
|
22
|
+
*/
|
|
23
|
+
export interface TokenRoutesOptions {
|
|
24
|
+
store: ConsoleTokenStore;
|
|
25
|
+
/** Maximum rotation attempts per window. Default: 10. */
|
|
26
|
+
rateLimitMax?: number;
|
|
27
|
+
/** Rate limit window in milliseconds. Default: 60_000 (1 minute). */
|
|
28
|
+
rateLimitWindowMs?: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Build the Express router exposing token management endpoints. The returned
|
|
32
|
+
* router should be mounted at `/api/console/token`; the caller does not need
|
|
33
|
+
* to add additional auth middleware — this router enforces its own auth
|
|
34
|
+
* regardless of the global feature flag.
|
|
35
|
+
*/
|
|
36
|
+
export declare function createTokenRoutes(options: TokenRoutesOptions): Router;
|
|
37
|
+
//# sourceMappingURL=tokenRoutes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokenRoutes.d.ts","sourceRoot":"","sources":["../../../src/web/routes/tokenRoutes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAgB,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAE1C,OAAO,EAAa,KAAK,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAiB/E;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,iBAAiB,CAAC;IACzB,yDAAyD;IACzD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qEAAqE;IACrE,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,MAAM,CAyDrE"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console token management HTTP routes — #1795.
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - POST /api/console/token/rotate — rotate the primary token with TOTP confirmation
|
|
6
|
+
*
|
|
7
|
+
* Security model:
|
|
8
|
+
* - All endpoints require a valid existing console token. Enforcement
|
|
9
|
+
* happens via an always-on `createAuthMiddleware` instance mounted at the
|
|
10
|
+
* top of this router, independent of `DOLLHOUSE_WEB_AUTH_ENABLED`.
|
|
11
|
+
* - Rotation additionally requires TOTP confirmation (Pattern B). Pattern A
|
|
12
|
+
* (OS dialog fallback) is deferred to a follow-up issue.
|
|
13
|
+
* - A sliding-window rate limit throttles rotation attempts so a bad actor
|
|
14
|
+
* with a live session can't brute-force TOTP codes by flooding rotations.
|
|
15
|
+
*
|
|
16
|
+
* @since v2.1.0 — Issue #1795
|
|
17
|
+
*/
|
|
18
|
+
import express, { Router } from 'express';
|
|
19
|
+
import { TotpError } from '../console/consoleToken.js';
|
|
20
|
+
import { createAuthMiddleware } from '../middleware/authMiddleware.js';
|
|
21
|
+
import { SlidingWindowRateLimiter } from '../../utils/SlidingWindowRateLimiter.js';
|
|
22
|
+
import { httpStatusForStoreError, sendStoreError, getNormalizedStringField } from './consoleRouteHelpers.js';
|
|
23
|
+
import { logger } from '../../utils/logger.js';
|
|
24
|
+
/** JSON body size limit — rotation requests are tiny. */
|
|
25
|
+
const BODY_LIMIT = '1kb';
|
|
26
|
+
/**
|
|
27
|
+
* Rate limit for the rotation endpoint: 10 attempts per minute.
|
|
28
|
+
* Same rationale as the TOTP enrollment confirm limiter — brute-force
|
|
29
|
+
* success probability stays well below 5e-5/min.
|
|
30
|
+
*/
|
|
31
|
+
const DEFAULT_RATE_LIMIT_MAX = 10;
|
|
32
|
+
const DEFAULT_RATE_LIMIT_WINDOW_MS = 60_000;
|
|
33
|
+
/**
|
|
34
|
+
* Build the Express router exposing token management endpoints. The returned
|
|
35
|
+
* router should be mounted at `/api/console/token`; the caller does not need
|
|
36
|
+
* to add additional auth middleware — this router enforces its own auth
|
|
37
|
+
* regardless of the global feature flag.
|
|
38
|
+
*/
|
|
39
|
+
export function createTokenRoutes(options) {
|
|
40
|
+
const { store } = options;
|
|
41
|
+
const router = Router();
|
|
42
|
+
const jsonParser = express.json({ limit: BODY_LIMIT, type: 'application/json' });
|
|
43
|
+
const rateLimitMax = options.rateLimitMax ?? DEFAULT_RATE_LIMIT_MAX;
|
|
44
|
+
const rateLimitWindowMs = options.rateLimitWindowMs ?? DEFAULT_RATE_LIMIT_WINDOW_MS;
|
|
45
|
+
const rotateLimiter = new SlidingWindowRateLimiter(rateLimitMax, rateLimitWindowMs);
|
|
46
|
+
// Always-on auth — same pattern as the TOTP router.
|
|
47
|
+
const auth = createAuthMiddleware({
|
|
48
|
+
store,
|
|
49
|
+
enabled: true,
|
|
50
|
+
label: 'token',
|
|
51
|
+
});
|
|
52
|
+
router.use(auth);
|
|
53
|
+
/** GET /info — token metadata + TOTP status for the Security tab UI (#1791). */
|
|
54
|
+
router.get('/info', (_req, res) => {
|
|
55
|
+
const masked = store.listMasked();
|
|
56
|
+
const totpStatus = store.getTotpStatus();
|
|
57
|
+
res.json({
|
|
58
|
+
tokens: masked,
|
|
59
|
+
totp: totpStatus,
|
|
60
|
+
filePath: store.getFilePath(),
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
/** POST /rotate — rotate the primary console token with TOTP confirmation. */
|
|
64
|
+
router.post('/rotate', jsonParser, async (req, res) => {
|
|
65
|
+
if (!rotateLimiter.tryAcquire()) {
|
|
66
|
+
sendStoreError(res, 429, 'RATE_LIMITED', 'Too many rotation attempts — slow down');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const confirmationCode = getNormalizedStringField(req.body, 'confirmationCode');
|
|
70
|
+
if (!confirmationCode) {
|
|
71
|
+
sendStoreError(res, 400, 'MISSING_FIELDS', 'confirmationCode is required');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const result = await store.rotatePrimary(confirmationCode);
|
|
76
|
+
res.json({
|
|
77
|
+
token: result.token,
|
|
78
|
+
rotatedAt: result.rotatedAt,
|
|
79
|
+
graceUntil: result.graceUntil,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
const message = err instanceof Error ? err.message : 'Rotation failed';
|
|
84
|
+
logger.debug(`[Token] rotate failed: ${message}`);
|
|
85
|
+
if (err instanceof TotpError) {
|
|
86
|
+
sendStoreError(res, httpStatusForStoreError(err.code), err.code, err.message);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
sendStoreError(res, 500, 'INTERNAL', message);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
return router;
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidG9rZW5Sb3V0ZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvd2ViL3JvdXRlcy90b2tlblJvdXRlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7Ozs7Ozs7Ozs7OztHQWdCRztBQUVILE9BQU8sT0FBTyxFQUFFLEVBQUUsTUFBTSxFQUFFLE1BQU0sU0FBUyxDQUFDO0FBRTFDLE9BQU8sRUFBRSxTQUFTLEVBQTBCLE1BQU0sNEJBQTRCLENBQUM7QUFDL0UsT0FBTyxFQUFFLG9CQUFvQixFQUFFLE1BQU0saUNBQWlDLENBQUM7QUFDdkUsT0FBTyxFQUFFLHdCQUF3QixFQUFFLE1BQU0seUNBQXlDLENBQUM7QUFDbkYsT0FBTyxFQUFFLHVCQUF1QixFQUFFLGNBQWMsRUFBRSx3QkFBd0IsRUFBRSxNQUFNLDBCQUEwQixDQUFDO0FBQzdHLE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSx1QkFBdUIsQ0FBQztBQUUvQyx5REFBeUQ7QUFDekQsTUFBTSxVQUFVLEdBQUcsS0FBSyxDQUFDO0FBRXpCOzs7O0dBSUc7QUFDSCxNQUFNLHNCQUFzQixHQUFHLEVBQUUsQ0FBQztBQUNsQyxNQUFNLDRCQUE0QixHQUFHLE1BQU0sQ0FBQztBQWE1Qzs7Ozs7R0FLRztBQUNILE1BQU0sVUFBVSxpQkFBaUIsQ0FBQyxPQUEyQjtJQUMzRCxNQUFNLEVBQUUsS0FBSyxFQUFFLEdBQUcsT0FBTyxDQUFDO0lBQzFCLE1BQU0sTUFBTSxHQUFHLE1BQU0sRUFBRSxDQUFDO0lBQ3hCLE1BQU0sVUFBVSxHQUFHLE9BQU8sQ0FBQyxJQUFJLENBQUMsRUFBRSxLQUFLLEVBQUUsVUFBVSxFQUFFLElBQUksRUFBRSxrQkFBa0IsRUFBRSxDQUFDLENBQUM7SUFDakYsTUFBTSxZQUFZLEdBQUcsT0FBTyxDQUFDLFlBQVksSUFBSSxzQkFBc0IsQ0FBQztJQUNwRSxNQUFNLGlCQUFpQixHQUFHLE9BQU8sQ0FBQyxpQkFBaUIsSUFBSSw0QkFBNEIsQ0FBQztJQUNwRixNQUFNLGFBQWEsR0FBRyxJQUFJLHdCQUF3QixDQUFDLFlBQVksRUFBRSxpQkFBaUIsQ0FBQyxDQUFDO0lBRXBGLG9EQUFvRDtJQUNwRCxNQUFNLElBQUksR0FBRyxvQkFBb0IsQ0FBQztRQUNoQyxLQUFLO1FBQ0wsT0FBTyxFQUFFLElBQUk7UUFDYixLQUFLLEVBQUUsT0FBTztLQUNmLENBQUMsQ0FBQztJQUNILE1BQU0sQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLENBQUM7SUFFakIsZ0ZBQWdGO0lBQ2hGLE1BQU0sQ0FBQyxHQUFHLENBQUMsT0FBTyxFQUFFLENBQUMsSUFBYSxFQUFFLEdBQWEsRUFBRSxFQUFFO1FBQ25ELE1BQU0sTUFBTSxHQUFHLEtBQUssQ0FBQyxVQUFVLEVBQUUsQ0FBQztRQUNsQyxNQUFNLFVBQVUsR0FBRyxLQUFLLENBQUMsYUFBYSxFQUFFLENBQUM7UUFDekMsR0FBRyxDQUFDLElBQUksQ0FBQztZQUNQLE1BQU0sRUFBRSxNQUFNO1lBQ2QsSUFBSSxFQUFFLFVBQVU7WUFDaEIsUUFBUSxFQUFFLEtBQUssQ0FBQyxXQUFXLEVBQUU7U0FDOUIsQ0FBQyxDQUFDO0lBQ0wsQ0FBQyxDQUFDLENBQUM7SUFFSCw4RUFBOEU7SUFDOUUsTUFBTSxDQUFDLElBQUksQ0FBQyxTQUFTLEVBQUUsVUFBVSxFQUFFLEtBQUssRUFBRSxHQUFZLEVBQUUsR0FBYSxFQUFFLEVBQUU7UUFDdkUsSUFBSSxDQUFDLGFBQWEsQ0FBQyxVQUFVLEVBQUUsRUFBRSxDQUFDO1lBQ2hDLGNBQWMsQ0FBQyxHQUFHLEVBQUUsR0FBRyxFQUFFLGNBQWMsRUFBRSx3Q0FBd0MsQ0FBQyxDQUFDO1lBQ25GLE9BQU87UUFDVCxDQUFDO1FBQ0QsTUFBTSxnQkFBZ0IsR0FBRyx3QkFBd0IsQ0FBQyxHQUFHLENBQUMsSUFBSSxFQUFFLGtCQUFrQixDQUFDLENBQUM7UUFDaEYsSUFBSSxDQUFDLGdCQUFnQixFQUFFLENBQUM7WUFDdEIsY0FBYyxDQUFDLEdBQUcsRUFBRSxHQUFHLEVBQUUsZ0JBQWdCLEVBQUUsOEJBQThCLENBQUMsQ0FBQztZQUMzRSxPQUFPO1FBQ1QsQ0FBQztRQUNELElBQUksQ0FBQztZQUNILE1BQU0sTUFBTSxHQUFHLE1BQU0sS0FBSyxDQUFDLGFBQWEsQ0FBQyxnQkFBZ0IsQ0FBQyxDQUFDO1lBQzNELEdBQUcsQ0FBQyxJQUFJLENBQUM7Z0JBQ1AsS0FBSyxFQUFFLE1BQU0sQ0FBQyxLQUFLO2dCQUNuQixTQUFTLEVBQUUsTUFBTSxDQUFDLFNBQVM7Z0JBQzNCLFVBQVUsRUFBRSxNQUFNLENBQUMsVUFBVTthQUM5QixDQUFDLENBQUM7UUFDTCxDQUFDO1FBQUMsT0FBTyxHQUFHLEVBQUUsQ0FBQztZQUNiLE1BQU0sT0FBTyxHQUFHLEdBQUcsWUFBWSxLQUFLLENBQUMsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLGlCQUFpQixDQUFDO1lBQ3ZFLE1BQU0sQ0FBQyxLQUFLLENBQUMsMEJBQTBCLE9BQU8sRUFBRSxDQUFDLENBQUM7WUFDbEQsSUFBSSxHQUFHLFlBQVksU0FBUyxFQUFFLENBQUM7Z0JBQzdCLGNBQWMsQ0FBQyxHQUFHLEVBQUUsdUJBQXVCLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxFQUFFLEdBQUcsQ0FBQyxJQUFJLEVBQUUsR0FBRyxDQUFDLE9BQU8sQ0FBQyxDQUFDO1lBQ2hGLENBQUM7aUJBQU0sQ0FBQztnQkFDTixjQUFjLENBQUMsR0FBRyxFQUFFLEdBQUcsRUFBRSxVQUFVLEVBQUUsT0FBTyxDQUFDLENBQUM7WUFDaEQsQ0FBQztRQUNILENBQUM7SUFDSCxDQUFDLENBQUMsQ0FBQztJQUVILE9BQU8sTUFBTSxDQUFDO0FBQ2hCLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIENvbnNvbGUgdG9rZW4gbWFuYWdlbWVudCBIVFRQIHJvdXRlcyDigJQgIzE3OTUuXG4gKlxuICogUHJvdmlkZXM6XG4gKiAtIFBPU1QgL2FwaS9jb25zb2xlL3Rva2VuL3JvdGF0ZSDigJQgcm90YXRlIHRoZSBwcmltYXJ5IHRva2VuIHdpdGggVE9UUCBjb25maXJtYXRpb25cbiAqXG4gKiBTZWN1cml0eSBtb2RlbDpcbiAqIC0gQWxsIGVuZHBvaW50cyByZXF1aXJlIGEgdmFsaWQgZXhpc3RpbmcgY29uc29sZSB0b2tlbi4gRW5mb3JjZW1lbnRcbiAqICAgaGFwcGVucyB2aWEgYW4gYWx3YXlzLW9uIGBjcmVhdGVBdXRoTWlkZGxld2FyZWAgaW5zdGFuY2UgbW91bnRlZCBhdCB0aGVcbiAqICAgdG9wIG9mIHRoaXMgcm91dGVyLCBpbmRlcGVuZGVudCBvZiBgRE9MTEhPVVNFX1dFQl9BVVRIX0VOQUJMRURgLlxuICogLSBSb3RhdGlvbiBhZGRpdGlvbmFsbHkgcmVxdWlyZXMgVE9UUCBjb25maXJtYXRpb24gKFBhdHRlcm4gQikuIFBhdHRlcm4gQVxuICogICAoT1MgZGlhbG9nIGZhbGxiYWNrKSBpcyBkZWZlcnJlZCB0byBhIGZvbGxvdy11cCBpc3N1ZS5cbiAqIC0gQSBzbGlkaW5nLXdpbmRvdyByYXRlIGxpbWl0IHRocm90dGxlcyByb3RhdGlvbiBhdHRlbXB0cyBzbyBhIGJhZCBhY3RvclxuICogICB3aXRoIGEgbGl2ZSBzZXNzaW9uIGNhbid0IGJydXRlLWZvcmNlIFRPVFAgY29kZXMgYnkgZmxvb2Rpbmcgcm90YXRpb25zLlxuICpcbiAqIEBzaW5jZSB2Mi4xLjAg4oCUIElzc3VlICMxNzk1XG4gKi9cblxuaW1wb3J0IGV4cHJlc3MsIHsgUm91dGVyIH0gZnJvbSAnZXhwcmVzcyc7XG5pbXBvcnQgdHlwZSB7IFJlcXVlc3QsIFJlc3BvbnNlIH0gZnJvbSAnZXhwcmVzcyc7XG5pbXBvcnQgeyBUb3RwRXJyb3IsIHR5cGUgQ29uc29sZVRva2VuU3RvcmUgfSBmcm9tICcuLi9jb25zb2xlL2NvbnNvbGVUb2tlbi5qcyc7XG5pbXBvcnQgeyBjcmVhdGVBdXRoTWlkZGxld2FyZSB9IGZyb20gJy4uL21pZGRsZXdhcmUvYXV0aE1pZGRsZXdhcmUuanMnO1xuaW1wb3J0IHsgU2xpZGluZ1dpbmRvd1JhdGVMaW1pdGVyIH0gZnJvbSAnLi4vLi4vdXRpbHMvU2xpZGluZ1dpbmRvd1JhdGVMaW1pdGVyLmpzJztcbmltcG9ydCB7IGh0dHBTdGF0dXNGb3JTdG9yZUVycm9yLCBzZW5kU3RvcmVFcnJvciwgZ2V0Tm9ybWFsaXplZFN0cmluZ0ZpZWxkIH0gZnJvbSAnLi9jb25zb2xlUm91dGVIZWxwZXJzLmpzJztcbmltcG9ydCB7IGxvZ2dlciB9IGZyb20gJy4uLy4uL3V0aWxzL2xvZ2dlci5qcyc7XG5cbi8qKiBKU09OIGJvZHkgc2l6ZSBsaW1pdCDigJQgcm90YXRpb24gcmVxdWVzdHMgYXJlIHRpbnkuICovXG5jb25zdCBCT0RZX0xJTUlUID0gJzFrYic7XG5cbi8qKlxuICogUmF0ZSBsaW1pdCBmb3IgdGhlIHJvdGF0aW9uIGVuZHBvaW50OiAxMCBhdHRlbXB0cyBwZXIgbWludXRlLlxuICogU2FtZSByYXRpb25hbGUgYXMgdGhlIFRPVFAgZW5yb2xsbWVudCBjb25maXJtIGxpbWl0ZXIg4oCUIGJydXRlLWZvcmNlXG4gKiBzdWNjZXNzIHByb2JhYmlsaXR5IHN0YXlzIHdlbGwgYmVsb3cgNWUtNS9taW4uXG4gKi9cbmNvbnN0IERFRkFVTFRfUkFURV9MSU1JVF9NQVggPSAxMDtcbmNvbnN0IERFRkFVTFRfUkFURV9MSU1JVF9XSU5ET1dfTVMgPSA2MF8wMDA7XG5cbi8qKlxuICogT3B0aW9ucyBmb3IgdGhlIHRva2VuIHJvdXRlcyBmYWN0b3J5LlxuICovXG5leHBvcnQgaW50ZXJmYWNlIFRva2VuUm91dGVzT3B0aW9ucyB7XG4gIHN0b3JlOiBDb25zb2xlVG9rZW5TdG9yZTtcbiAgLyoqIE1heGltdW0gcm90YXRpb24gYXR0ZW1wdHMgcGVyIHdpbmRvdy4gRGVmYXVsdDogMTAuICovXG4gIHJhdGVMaW1pdE1heD86IG51bWJlcjtcbiAgLyoqIFJhdGUgbGltaXQgd2luZG93IGluIG1pbGxpc2Vjb25kcy4gRGVmYXVsdDogNjBfMDAwICgxIG1pbnV0ZSkuICovXG4gIHJhdGVMaW1pdFdpbmRvd01zPzogbnVtYmVyO1xufVxuXG4vKipcbiAqIEJ1aWxkIHRoZSBFeHByZXNzIHJvdXRlciBleHBvc2luZyB0b2tlbiBtYW5hZ2VtZW50IGVuZHBvaW50cy4gVGhlIHJldHVybmVkXG4gKiByb3V0ZXIgc2hvdWxkIGJlIG1vdW50ZWQgYXQgYC9hcGkvY29uc29sZS90b2tlbmA7IHRoZSBjYWxsZXIgZG9lcyBub3QgbmVlZFxuICogdG8gYWRkIGFkZGl0aW9uYWwgYXV0aCBtaWRkbGV3YXJlIOKAlCB0aGlzIHJvdXRlciBlbmZvcmNlcyBpdHMgb3duIGF1dGhcbiAqIHJlZ2FyZGxlc3Mgb2YgdGhlIGdsb2JhbCBmZWF0dXJlIGZsYWcuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBjcmVhdGVUb2tlblJvdXRlcyhvcHRpb25zOiBUb2tlblJvdXRlc09wdGlvbnMpOiBSb3V0ZXIge1xuICBjb25zdCB7IHN0b3JlIH0gPSBvcHRpb25zO1xuICBjb25zdCByb3V0ZXIgPSBSb3V0ZXIoKTtcbiAgY29uc3QganNvblBhcnNlciA9IGV4cHJlc3MuanNvbih7IGxpbWl0OiBCT0RZX0xJTUlULCB0eXBlOiAnYXBwbGljYXRpb24vanNvbicgfSk7XG4gIGNvbnN0IHJhdGVMaW1pdE1heCA9IG9wdGlvbnMucmF0ZUxpbWl0TWF4ID8/IERFRkFVTFRfUkFURV9MSU1JVF9NQVg7XG4gIGNvbnN0IHJhdGVMaW1pdFdpbmRvd01zID0gb3B0aW9ucy5yYXRlTGltaXRXaW5kb3dNcyA/PyBERUZBVUxUX1JBVEVfTElNSVRfV0lORE9XX01TO1xuICBjb25zdCByb3RhdGVMaW1pdGVyID0gbmV3IFNsaWRpbmdXaW5kb3dSYXRlTGltaXRlcihyYXRlTGltaXRNYXgsIHJhdGVMaW1pdFdpbmRvd01zKTtcblxuICAvLyBBbHdheXMtb24gYXV0aCDigJQgc2FtZSBwYXR0ZXJuIGFzIHRoZSBUT1RQIHJvdXRlci5cbiAgY29uc3QgYXV0aCA9IGNyZWF0ZUF1dGhNaWRkbGV3YXJlKHtcbiAgICBzdG9yZSxcbiAgICBlbmFibGVkOiB0cnVlLFxuICAgIGxhYmVsOiAndG9rZW4nLFxuICB9KTtcbiAgcm91dGVyLnVzZShhdXRoKTtcblxuICAvKiogR0VUIC9pbmZvIOKAlCB0b2tlbiBtZXRhZGF0YSArIFRPVFAgc3RhdHVzIGZvciB0aGUgU2VjdXJpdHkgdGFiIFVJICgjMTc5MSkuICovXG4gIHJvdXRlci5nZXQoJy9pbmZvJywgKF9yZXE6IFJlcXVlc3QsIHJlczogUmVzcG9uc2UpID0+IHtcbiAgICBjb25zdCBtYXNrZWQgPSBzdG9yZS5saXN0TWFza2VkKCk7XG4gICAgY29uc3QgdG90cFN0YXR1cyA9IHN0b3JlLmdldFRvdHBTdGF0dXMoKTtcbiAgICByZXMuanNvbih7XG4gICAgICB0b2tlbnM6IG1hc2tlZCxcbiAgICAgIHRvdHA6IHRvdHBTdGF0dXMsXG4gICAgICBmaWxlUGF0aDogc3RvcmUuZ2V0RmlsZVBhdGgoKSxcbiAgICB9KTtcbiAgfSk7XG5cbiAgLyoqIFBPU1QgL3JvdGF0ZSDigJQgcm90YXRlIHRoZSBwcmltYXJ5IGNvbnNvbGUgdG9rZW4gd2l0aCBUT1RQIGNvbmZpcm1hdGlvbi4gKi9cbiAgcm91dGVyLnBvc3QoJy9yb3RhdGUnLCBqc29uUGFyc2VyLCBhc3luYyAocmVxOiBSZXF1ZXN0LCByZXM6IFJlc3BvbnNlKSA9PiB7XG4gICAgaWYgKCFyb3RhdGVMaW1pdGVyLnRyeUFjcXVpcmUoKSkge1xuICAgICAgc2VuZFN0b3JlRXJyb3IocmVzLCA0MjksICdSQVRFX0xJTUlURUQnLCAnVG9vIG1hbnkgcm90YXRpb24gYXR0ZW1wdHMg4oCUIHNsb3cgZG93bicpO1xuICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICBjb25zdCBjb25maXJtYXRpb25Db2RlID0gZ2V0Tm9ybWFsaXplZFN0cmluZ0ZpZWxkKHJlcS5ib2R5LCAnY29uZmlybWF0aW9uQ29kZScpO1xuICAgIGlmICghY29uZmlybWF0aW9uQ29kZSkge1xuICAgICAgc2VuZFN0b3JlRXJyb3IocmVzLCA0MDAsICdNSVNTSU5HX0ZJRUxEUycsICdjb25maXJtYXRpb25Db2RlIGlzIHJlcXVpcmVkJyk7XG4gICAgICByZXR1cm47XG4gICAgfVxuICAgIHRyeSB7XG4gICAgICBjb25zdCByZXN1bHQgPSBhd2FpdCBzdG9yZS5yb3RhdGVQcmltYXJ5KGNvbmZpcm1hdGlvbkNvZGUpO1xuICAgICAgcmVzLmpzb24oe1xuICAgICAgICB0b2tlbjogcmVzdWx0LnRva2VuLFxuICAgICAgICByb3RhdGVkQXQ6IHJlc3VsdC5yb3RhdGVkQXQsXG4gICAgICAgIGdyYWNlVW50aWw6IHJlc3VsdC5ncmFjZVVudGlsLFxuICAgICAgfSk7XG4gICAgfSBjYXRjaCAoZXJyKSB7XG4gICAgICBjb25zdCBtZXNzYWdlID0gZXJyIGluc3RhbmNlb2YgRXJyb3IgPyBlcnIubWVzc2FnZSA6ICdSb3RhdGlvbiBmYWlsZWQnO1xuICAgICAgbG9nZ2VyLmRlYnVnKGBbVG9rZW5dIHJvdGF0ZSBmYWlsZWQ6ICR7bWVzc2FnZX1gKTtcbiAgICAgIGlmIChlcnIgaW5zdGFuY2VvZiBUb3RwRXJyb3IpIHtcbiAgICAgICAgc2VuZFN0b3JlRXJyb3IocmVzLCBodHRwU3RhdHVzRm9yU3RvcmVFcnJvcihlcnIuY29kZSksIGVyci5jb2RlLCBlcnIubWVzc2FnZSk7XG4gICAgICB9IGVsc2Uge1xuICAgICAgICBzZW5kU3RvcmVFcnJvcihyZXMsIDUwMCwgJ0lOVEVSTkFMJywgbWVzc2FnZSk7XG4gICAgICB9XG4gICAgfVxuICB9KTtcblxuICByZXR1cm4gcm91dGVyO1xufVxuIl19
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TOTP (authenticator) enrollment HTTP routes — Phase 2 of #1780 (#1794).
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - GET /api/console/totp/status — enrollment state (no secrets)
|
|
6
|
+
* - POST /api/console/totp/enroll/begin — generate secret, return QR + otpauth URI
|
|
7
|
+
* - POST /api/console/totp/enroll/confirm — verify code, persist, return backup codes (once)
|
|
8
|
+
* - POST /api/console/totp/disable — verify code, clear enrollment
|
|
9
|
+
*
|
|
10
|
+
* Security model:
|
|
11
|
+
* - All endpoints require a valid existing console token. The caller must
|
|
12
|
+
* prove they already hold the token before they can enroll a second
|
|
13
|
+
* factor — otherwise an attacker with local port access could pre-enroll
|
|
14
|
+
* their own authenticator and lock the legitimate user out.
|
|
15
|
+
* - Enforcement happens via an always-on `createAuthMiddleware` instance
|
|
16
|
+
* mounted at the top of this router, independent of the global
|
|
17
|
+
* DOLLHOUSE_WEB_AUTH_ENABLED flag.
|
|
18
|
+
* - Backup codes are returned in plaintext exactly once (confirm response)
|
|
19
|
+
* and only their sha256 hashes are retained by the store.
|
|
20
|
+
* - A sliding-window rate limit throttles confirm/disable attempts on a
|
|
21
|
+
* per-IP basis so a bad actor with a live session can't brute-force a
|
|
22
|
+
* TOTP window by flooding requests.
|
|
23
|
+
*
|
|
24
|
+
* @since v2.1.0 — Issue #1794
|
|
25
|
+
*/
|
|
26
|
+
import { Router } from 'express';
|
|
27
|
+
import { type ConsoleTokenStore } from '../console/consoleToken.js';
|
|
28
|
+
/**
|
|
29
|
+
* Options for the TOTP routes factory.
|
|
30
|
+
*/
|
|
31
|
+
export interface TotpRoutesOptions {
|
|
32
|
+
store: ConsoleTokenStore;
|
|
33
|
+
/** Maximum code-verification attempts per window. Default: 10. */
|
|
34
|
+
rateLimitMax?: number;
|
|
35
|
+
/** Rate limit window in milliseconds. Default: 60_000 (1 minute). */
|
|
36
|
+
rateLimitWindowMs?: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Build the Express router exposing TOTP endpoints. The returned router
|
|
40
|
+
* should be mounted at `/api/console/totp`; the caller does not need to
|
|
41
|
+
* add additional auth middleware — this router enforces its own auth
|
|
42
|
+
* regardless of the global feature flag.
|
|
43
|
+
*/
|
|
44
|
+
export declare function createTotpRoutes(options: TotpRoutesOptions): Router;
|
|
45
|
+
//# sourceMappingURL=totpRoutes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"totpRoutes.d.ts","sourceRoot":"","sources":["../../../src/web/routes/totpRoutes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAgB,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAG1C,OAAO,EAAa,KAAK,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AA4C/E;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,iBAAiB,CAAC;IACzB,kEAAkE;IAClE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qEAAqE;IACrE,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,MAAM,CAgHnE"}
|