webspresso 0.0.103 → 0.0.105
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/.agents/AGENTS.md +6 -0
- package/.agents/ARCHITECTURE.md +23 -0
- package/.agents/CACHE.md +147 -0
- package/.agents/CLI.md +1 -1
- package/.agents/EARLY_HINTS.md +120 -0
- package/.agents/HEALTH.md +118 -0
- package/.agents/LIMITS.md +109 -0
- package/.agents/ORM.md +155 -1
- package/.agents/PLUGINS.md +28 -0
- package/.agents/RESILIENCE.md +235 -0
- package/AGENTS.md +6 -0
- package/README.md +2 -2
- package/bin/commands/add-docker.js +6 -6
- package/bin/commands/dev.js +4 -3
- package/bin/commands/new.js +5 -5
- package/bin/commands/openapi.js +1 -1
- package/bin/commands/start.js +1 -1
- package/bin/utils/orm-map-html.js +2 -8
- package/bin/utils/project.js +2 -2
- package/core/auth/hash.js +5 -4
- package/core/auth/jwt.js +6 -57
- package/core/auth/magic-link.js +4 -5
- package/core/auth/manager.js +3 -5
- package/core/auth/oauth.js +45 -30
- package/core/auth/totp.js +5 -16
- package/core/auth/user-agent.js +1 -21
- package/core/cache/byte-size.js +123 -0
- package/core/cache/index.js +21 -0
- package/core/cache/lru.js +477 -0
- package/core/cache/tagged-lru.js +346 -0
- package/core/commands/registry.js +6 -20
- package/core/content/renderer.js +1 -13
- package/core/early-hints/index.js +267 -0
- package/core/errors/domain.js +48 -0
- package/core/errors/http.js +31 -0
- package/core/errors/index.js +10 -0
- package/core/errors/middleware.js +2 -8
- package/core/errors/normalize.js +88 -2
- package/core/events/discovery.js +6 -24
- package/core/events/dispatcher.js +2 -7
- package/core/forms/form-builder.js +1 -9
- package/core/health/index.js +41 -0
- package/core/health/middleware.js +213 -0
- package/core/health/normalize.js +140 -0
- package/core/health/probes.js +203 -0
- package/core/limits/index.js +34 -0
- package/core/limits/middleware.js +317 -0
- package/core/limits/normalize.js +222 -0
- package/core/orm/batch.js +70 -0
- package/core/orm/cache/fingerprint.js +2 -2
- package/core/orm/cache/memory-provider.js +27 -110
- package/core/orm/index.js +76 -8
- package/core/orm/json-fields.js +22 -8
- package/core/orm/model.js +10 -0
- package/core/orm/query-builder.js +63 -9
- package/core/orm/query-filters.js +393 -0
- package/core/orm/repository.js +318 -19
- package/core/orm/schema-helpers.js +8 -10
- package/core/orm/scopes.js +321 -1
- package/core/orm/transaction.js +10 -2
- package/core/orm/utils.js +4 -86
- package/core/queue/adapters/database.js +4 -8
- package/core/queue/manager.js +55 -41
- package/core/resilience/circuit-breaker.js +391 -0
- package/core/resilience/errors.js +63 -0
- package/core/resilience/index.js +53 -0
- package/core/resilience/registry.js +97 -0
- package/core/resilience/retry.js +203 -0
- package/core/schema-cache.js +9 -2
- package/core/shutdown/http-adapter.js +4 -1
- package/core/shutdown/shutdown-manager.js +132 -31
- package/core/utils/array.js +12 -0
- package/core/utils/base64url.js +8 -0
- package/core/utils/crypto.js +191 -0
- package/core/utils/ensure-leading-slash.js +6 -0
- package/core/utils/escape-html.js +7 -0
- package/core/utils/escape-regexp.js +7 -0
- package/core/utils/fetch.js +121 -0
- package/core/utils/fs.js +144 -0
- package/core/utils/get-client-ip.js +6 -0
- package/core/utils/hmac.js +6 -0
- package/core/utils/html.js +72 -0
- package/core/utils/ip.js +86 -0
- package/core/utils/json.js +98 -0
- package/core/utils/object.js +106 -0
- package/core/utils/random.js +9 -0
- package/core/utils/regex.js +77 -0
- package/core/utils/safe-compare.js +3 -0
- package/core/utils/sha256.js +6 -0
- package/core/utils/string.js +87 -0
- package/core/utils/strip-trailing-slash.js +6 -0
- package/core/utils/time.js +96 -0
- package/core/utils/timeout.js +348 -0
- package/core/utils/url.js +106 -0
- package/core/utils/walk.js +7 -0
- package/index.d.ts +627 -2
- package/index.js +158 -0
- package/package.json +1 -2
- package/plugins/admin-panel/auth.js +15 -7
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +82 -10
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +4 -2
- package/plugins/admin-panel/client/parts/08-record-list.js +3 -2
- package/plugins/admin-panel/client/parts/10-export-registry.js +6 -0
- package/plugins/admin-panel/core/api-extensions.js +3 -80
- package/plugins/admin-panel/index.js +1 -1
- package/plugins/admin-panel/lib/login-rate-limiter.js +15 -13
- package/plugins/admin-panel/modules/menu.js +1 -1
- package/plugins/api-key/crypto.js +3 -26
- package/plugins/api-key/scopes.js +5 -7
- package/plugins/audit-log/api-handlers.js +8 -7
- package/plugins/audit-log/middleware.js +11 -2
- package/plugins/audit-log/parse.js +3 -3
- package/plugins/basic-auth/index.js +2 -26
- package/plugins/cors.js +2 -2
- package/plugins/csrf/index.js +7 -23
- package/plugins/dashboard/app.js +2 -2
- package/plugins/dashboard/index.js +5 -5
- package/plugins/data-exchange/record-selection.js +2 -5
- package/plugins/email/api-handlers.js +5 -18
- package/plugins/email/auth-bridge.js +3 -2
- package/plugins/file-manager/admin-component.js +3 -1
- package/plugins/file-manager/core.js +34 -4
- package/plugins/health-check.js +2 -1
- package/plugins/index.js +2 -0
- package/plugins/lazy.js +5 -6
- package/plugins/mcp/auth.js +1 -13
- package/plugins/mcp/discovery.js +18 -17
- package/plugins/mcp/index.js +23 -1
- package/plugins/mcp/transports/sse.js +10 -1
- package/plugins/orm-cache-admin/admin-component.js +3 -2
- package/plugins/polar/src/api.js +11 -3
- package/plugins/polar/src/webhooks.js +7 -13
- package/plugins/rate-limit/index.js +3 -1
- package/plugins/recaptcha/helpers.js +1 -7
- package/plugins/recaptcha/verify.js +37 -23
- package/plugins/redirect/index.js +57 -7
- package/plugins/rest-resources/index.js +1 -207
- package/plugins/rls/index.js +170 -0
- package/plugins/schema-explorer/admin-component.js +4 -2
- package/plugins/schema-explorer.js +40 -29
- package/plugins/seo-checker/index.js +5 -2
- package/plugins/site-analytics/tracking.js +21 -18
- package/plugins/sitemap.js +21 -17
- package/plugins/swagger.js +13 -17
- package/plugins/upload/index.js +4 -3
- package/plugins/upload/orphan-pruner.js +4 -3
- package/plugins/webhooks/crypto.js +6 -28
- package/plugins/webhooks/dispatcher.js +14 -9
- package/plugins/webhooks/index.js +6 -0
- package/plugins/webhooks/matcher.js +28 -17
- package/plugins/webhooks/sender.js +40 -13
- package/src/app-context.js +41 -5
- package/src/dev/banner.js +6 -2
- package/src/dev-reload/index.js +47 -6
- package/src/file-router.js +1 -3
- package/src/helpers.js +180 -33
- package/src/modules/module-discovery.js +1 -9
- package/src/pages/page-loader.js +53 -1
- package/src/plugin-manager.js +90 -17
- package/src/server.js +137 -26
- package/src/services/discovery.js +19 -40
- package/src/services/executor.js +86 -25
- package/src/services/memoize.js +3 -25
- package/src/services/registry.js +6 -0
- package/src/template/precompile.js +8 -19
package/core/auth/hash.js
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* @module core/auth/hash
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
const { sha256, randomToken } = require('../utils/crypto');
|
|
8
|
+
|
|
7
9
|
let bcrypt = null;
|
|
8
10
|
let bcryptChecked = false;
|
|
9
11
|
function getBcrypt() {
|
|
@@ -96,8 +98,7 @@ function needsRehash(hashedPassword, rounds = DEFAULT_ROUNDS) {
|
|
|
96
98
|
* @returns {string} Hex-encoded random token
|
|
97
99
|
*/
|
|
98
100
|
function generateToken(length = 32) {
|
|
99
|
-
|
|
100
|
-
return crypto.randomBytes(length).toString('hex');
|
|
101
|
+
return randomToken(length, 'hex');
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
/**
|
|
@@ -111,9 +112,9 @@ function generateToken(length = 32) {
|
|
|
111
112
|
* @returns {string} Hex-encoded SHA-256 digest
|
|
112
113
|
*/
|
|
113
114
|
function hashToken(token) {
|
|
114
|
-
|
|
115
|
+
if (!token) return '';
|
|
115
116
|
// CodeQL [js/insufficient-password-hash] False positive: token is a 256-bit random string, not a user password
|
|
116
|
-
return
|
|
117
|
+
return sha256(String(token));
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
module.exports = {
|
package/core/auth/jwt.js
CHANGED
|
@@ -5,59 +5,17 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const crypto = require('crypto');
|
|
8
|
+
const { safeCompare, hmacSha256, base64UrlEncode, base64UrlDecode } = require('../utils/crypto');
|
|
9
|
+
const { parseDurationSec } = require('../utils/time');
|
|
8
10
|
|
|
9
11
|
const HS256_HEADER_BASE64URL = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9';
|
|
10
12
|
|
|
11
|
-
/**
|
|
12
|
-
* Base64URL encode string or Buffer
|
|
13
|
-
* @param {string|Buffer} input
|
|
14
|
-
* @returns {string}
|
|
15
|
-
*/
|
|
16
|
-
function base64UrlEncode(input) {
|
|
17
|
-
return Buffer.isBuffer(input)
|
|
18
|
-
? input.toString('base64url')
|
|
19
|
-
: Buffer.from(String(input), 'utf8').toString('base64url');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Base64URL decode string
|
|
24
|
-
* @param {string} input
|
|
25
|
-
* @returns {string}
|
|
26
|
-
*/
|
|
27
|
-
function base64UrlDecode(input) {
|
|
28
|
-
return Buffer.from(String(input), 'base64url').toString('utf8');
|
|
29
|
-
}
|
|
30
|
-
|
|
31
13
|
/**
|
|
32
14
|
* Parse duration string (e.g. '1h', '7d', '15m', '3600') into seconds
|
|
33
15
|
* @param {string|number} duration
|
|
34
16
|
* @returns {number} Seconds
|
|
35
17
|
*/
|
|
36
|
-
|
|
37
|
-
if (typeof duration === 'number') {
|
|
38
|
-
return duration;
|
|
39
|
-
}
|
|
40
|
-
if (typeof duration !== 'string') {
|
|
41
|
-
return 3600; // Default 1 hour
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const match = duration.trim().match(/^(\d+)\s*([smhd])?$/i);
|
|
45
|
-
if (!match) {
|
|
46
|
-
const parsed = parseInt(duration, 10);
|
|
47
|
-
return isNaN(parsed) ? 3600 : parsed;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const value = parseInt(match[1], 10);
|
|
51
|
-
const unit = (match[2] || 's').toLowerCase();
|
|
52
|
-
|
|
53
|
-
switch (unit) {
|
|
54
|
-
case 's': return value;
|
|
55
|
-
case 'm': return value * 60;
|
|
56
|
-
case 'h': return value * 3600;
|
|
57
|
-
case 'd': return value * 86400;
|
|
58
|
-
default: return value;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
18
|
+
const parseDuration = parseDurationSec;
|
|
61
19
|
|
|
62
20
|
/**
|
|
63
21
|
* Sign payload to create HS256 JWT string
|
|
@@ -109,10 +67,7 @@ function signJwt(payload, secret, options = {}) {
|
|
|
109
67
|
const encodedPayload = base64UrlEncode(JSON.stringify(fullPayload));
|
|
110
68
|
|
|
111
69
|
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
112
|
-
const encodedSignature =
|
|
113
|
-
.createHmac('sha256', secret)
|
|
114
|
-
.update(signingInput)
|
|
115
|
-
.digest('base64url');
|
|
70
|
+
const encodedSignature = hmacSha256(signingInput, secret, 'base64url');
|
|
116
71
|
|
|
117
72
|
return `${signingInput}.${encodedSignature}`;
|
|
118
73
|
}
|
|
@@ -165,17 +120,11 @@ function verifyJwt(token, secret, options = {}) {
|
|
|
165
120
|
|
|
166
121
|
// Re-compute signature
|
|
167
122
|
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
168
|
-
const expectedSignatureBuf =
|
|
169
|
-
.createHmac('sha256', secret)
|
|
170
|
-
.update(signingInput)
|
|
171
|
-
.digest();
|
|
123
|
+
const expectedSignatureBuf = hmacSha256(signingInput, secret, 'buffer');
|
|
172
124
|
|
|
173
125
|
const actualSignatureBuf = Buffer.from(encodedSignature, 'base64url');
|
|
174
126
|
|
|
175
|
-
if (
|
|
176
|
-
expectedSignatureBuf.length !== actualSignatureBuf.length ||
|
|
177
|
-
!crypto.timingSafeEqual(expectedSignatureBuf, actualSignatureBuf)
|
|
178
|
-
) {
|
|
127
|
+
if (!safeCompare(expectedSignatureBuf, actualSignatureBuf)) {
|
|
179
128
|
throw new Error('Invalid JWT signature');
|
|
180
129
|
}
|
|
181
130
|
|
package/core/auth/magic-link.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
'use strict';
|
|
8
8
|
|
|
9
|
+
const { stripTrailingSlash, ensureLeadingSlash } = require('../utils/url');
|
|
10
|
+
|
|
9
11
|
/**
|
|
10
12
|
* Construct full magic link verification URL
|
|
11
13
|
*
|
|
@@ -16,10 +18,7 @@
|
|
|
16
18
|
* @returns {string}
|
|
17
19
|
*/
|
|
18
20
|
function buildMagicLinkUrl(options = {}) {
|
|
19
|
-
|
|
20
|
-
while (baseUrl.endsWith('/')) {
|
|
21
|
-
baseUrl = baseUrl.slice(0, -1);
|
|
22
|
-
}
|
|
21
|
+
const baseUrl = stripTrailingSlash(options.baseUrl || process.env.BASE_URL || 'http://localhost:7331');
|
|
23
22
|
const path = options.path || '/auth/magic-link/verify';
|
|
24
23
|
const token = options.token;
|
|
25
24
|
|
|
@@ -27,7 +26,7 @@ function buildMagicLinkUrl(options = {}) {
|
|
|
27
26
|
throw new Error('Token is required to build magic link URL');
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
const cleanPath = path
|
|
29
|
+
const cleanPath = ensureLeadingSlash(path);
|
|
31
30
|
const sep = cleanPath.includes('?') ? '&' : '?';
|
|
32
31
|
return `${baseUrl}${cleanPath}${sep}token=${encodeURIComponent(token)}`;
|
|
33
32
|
}
|
package/core/auth/manager.js
CHANGED
|
@@ -15,6 +15,7 @@ const {
|
|
|
15
15
|
consumeAuthToken,
|
|
16
16
|
hash: hashPassword,
|
|
17
17
|
} = require('./tokens');
|
|
18
|
+
const { stripTrailingSlash, ensureLeadingSlash } = require('../utils/url');
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Authentication error
|
|
@@ -959,12 +960,9 @@ class AuthManager extends EventEmitter {
|
|
|
959
960
|
tokenTtlMs
|
|
960
961
|
);
|
|
961
962
|
|
|
962
|
-
|
|
963
|
-
while (baseUrl.endsWith('/')) {
|
|
964
|
-
baseUrl = baseUrl.slice(0, -1);
|
|
965
|
-
}
|
|
963
|
+
const baseUrl = stripTrailingSlash(options.baseUrl || this.config.baseUrl || process.env.BASE_URL || 'http://localhost:7331');
|
|
966
964
|
const pathSegment = options.path || '/auth/magic-link/verify';
|
|
967
|
-
const cleanPath = pathSegment
|
|
965
|
+
const cleanPath = ensureLeadingSlash(pathSegment);
|
|
968
966
|
const sep = cleanPath.includes('?') ? '&' : '?';
|
|
969
967
|
|
|
970
968
|
const hasCustomUrl = typeof options.buildMagicLinkUrl === 'function' || Boolean(options.path) || Boolean(options.baseUrl);
|
package/core/auth/oauth.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const crypto = require('crypto');
|
|
9
|
+
const { fetchWithTimeout } = require('../utils/fetch');
|
|
10
|
+
const { safeCompare, sha256, hmacSha256, base64UrlEncode, base64UrlDecode, randomToken } = require('../utils/crypto');
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Custom error class for OAuth operations
|
|
@@ -57,14 +59,17 @@ const DEFAULT_PROVIDERS = {
|
|
|
57
59
|
userEmailsUrl: 'https://api.github.com/user/emails',
|
|
58
60
|
defaultScopes: ['read:user', 'user:email'],
|
|
59
61
|
defaultParams: {},
|
|
60
|
-
async fetchExtra(tokens) {
|
|
62
|
+
async fetchExtra(tokens, options = {}) {
|
|
61
63
|
try {
|
|
62
|
-
const
|
|
64
|
+
const timeout = typeof options?.timeout === 'number' ? options.timeout : 10000;
|
|
65
|
+
const res = await fetchWithTimeout('https://api.github.com/user/emails', {
|
|
63
66
|
headers: {
|
|
64
67
|
Authorization: `Bearer ${tokens.access_token}`,
|
|
65
68
|
Accept: 'application/vnd.github.v3+json',
|
|
66
69
|
'User-Agent': 'Webspresso-OAuth',
|
|
67
70
|
},
|
|
71
|
+
timeout,
|
|
72
|
+
signal: options?.signal,
|
|
68
73
|
});
|
|
69
74
|
if (res.ok) {
|
|
70
75
|
const emails = await res.json();
|
|
@@ -78,7 +83,7 @@ const DEFAULT_PROVIDERS = {
|
|
|
78
83
|
}
|
|
79
84
|
}
|
|
80
85
|
} catch {
|
|
81
|
-
// Fallback gracefully if email endpoint is unreachable
|
|
86
|
+
// Fallback gracefully if email endpoint is unreachable or times out
|
|
82
87
|
}
|
|
83
88
|
return null;
|
|
84
89
|
},
|
|
@@ -105,11 +110,8 @@ const DEFAULT_PROVIDERS = {
|
|
|
105
110
|
* @returns {{ codeVerifier: string, codeChallenge: string, codeChallengeMethod: 'S256' }}
|
|
106
111
|
*/
|
|
107
112
|
function generatePKCE() {
|
|
108
|
-
const codeVerifier =
|
|
109
|
-
const codeChallenge =
|
|
110
|
-
.createHash('sha256')
|
|
111
|
-
.update(codeVerifier)
|
|
112
|
-
.digest('base64url');
|
|
113
|
+
const codeVerifier = randomToken(32, 'base64url');
|
|
114
|
+
const codeChallenge = sha256(codeVerifier, 'base64url');
|
|
113
115
|
|
|
114
116
|
return {
|
|
115
117
|
codeVerifier,
|
|
@@ -132,14 +134,11 @@ function createSignedState(payload, secret) {
|
|
|
132
134
|
const dataWithMeta = {
|
|
133
135
|
...payload,
|
|
134
136
|
timestamp: Date.now(),
|
|
135
|
-
nonce:
|
|
137
|
+
nonce: randomToken(16, 'hex'),
|
|
136
138
|
};
|
|
137
139
|
|
|
138
|
-
const body =
|
|
139
|
-
const signature =
|
|
140
|
-
.createHmac('sha256', secret)
|
|
141
|
-
.update(body)
|
|
142
|
-
.digest('base64url');
|
|
140
|
+
const body = base64UrlEncode(JSON.stringify(dataWithMeta));
|
|
141
|
+
const signature = hmacSha256(body, secret, 'base64url');
|
|
143
142
|
|
|
144
143
|
return `${body}.${signature}`;
|
|
145
144
|
}
|
|
@@ -162,21 +161,15 @@ function verifySignedState(stateString, secret, maxAgeMs = 10 * 60 * 1000) {
|
|
|
162
161
|
}
|
|
163
162
|
|
|
164
163
|
const [body, signature] = parts;
|
|
165
|
-
const expectedSignature =
|
|
166
|
-
.createHmac('sha256', secret)
|
|
167
|
-
.update(body)
|
|
168
|
-
.digest('base64url');
|
|
164
|
+
const expectedSignature = hmacSha256(body, secret, 'base64url');
|
|
169
165
|
|
|
170
|
-
|
|
171
|
-
const bufB = Buffer.from(expectedSignature);
|
|
172
|
-
|
|
173
|
-
if (bufA.length !== bufB.length || !crypto.timingSafeEqual(bufA, bufB)) {
|
|
166
|
+
if (!safeCompare(signature, expectedSignature)) {
|
|
174
167
|
throw new OAuthError('OAuth state signature verification failed', 'INVALID_STATE');
|
|
175
168
|
}
|
|
176
169
|
|
|
177
170
|
let payload;
|
|
178
171
|
try {
|
|
179
|
-
const jsonStr =
|
|
172
|
+
const jsonStr = base64UrlDecode(body);
|
|
180
173
|
payload = JSON.parse(jsonStr);
|
|
181
174
|
} catch {
|
|
182
175
|
throw new OAuthError('Failed to parse OAuth state payload', 'INVALID_STATE');
|
|
@@ -202,6 +195,9 @@ class OAuthClient {
|
|
|
202
195
|
constructor(config = {}) {
|
|
203
196
|
this.secret = config.secret || process.env.OAUTH_SECRET || process.env.SESSION_SECRET || 'webspresso-oauth-secret';
|
|
204
197
|
this.stateTtlMs = config.stateTtlMs || 10 * 60 * 1000;
|
|
198
|
+
this.timeout = typeof (config.timeout || config.requestTimeout) === 'number'
|
|
199
|
+
? (config.timeout || config.requestTimeout)
|
|
200
|
+
: 10000;
|
|
205
201
|
this.providers = {};
|
|
206
202
|
|
|
207
203
|
if (config.providers) {
|
|
@@ -227,6 +223,7 @@ class OAuthClient {
|
|
|
227
223
|
redirectUri: options.redirectUri || options.redirect_uri || builtin.redirectUri,
|
|
228
224
|
scopes: options.scopes || options.scope || builtin.defaultScopes || [],
|
|
229
225
|
params: { ...(builtin.defaultParams || {}), ...(options.params || {}) },
|
|
226
|
+
timeout: typeof options.timeout === 'number' ? options.timeout : (builtin.timeout || this.timeout || 10000),
|
|
230
227
|
};
|
|
231
228
|
return this;
|
|
232
229
|
}
|
|
@@ -330,7 +327,7 @@ class OAuthClient {
|
|
|
330
327
|
* @param {string} [params.codeVerifier]
|
|
331
328
|
* @returns {Promise<Object>} Token response
|
|
332
329
|
*/
|
|
333
|
-
async exchangeCode(providerName, { code, redirectUri, codeVerifier }) {
|
|
330
|
+
async exchangeCode(providerName, { code, redirectUri, codeVerifier, timeout, signal } = {}) {
|
|
334
331
|
const provider = this.getProvider(providerName);
|
|
335
332
|
|
|
336
333
|
if (!code) {
|
|
@@ -354,9 +351,13 @@ class OAuthClient {
|
|
|
354
351
|
bodyParams.set('code_verifier', codeVerifier);
|
|
355
352
|
}
|
|
356
353
|
|
|
354
|
+
const requestTimeout = typeof timeout === 'number'
|
|
355
|
+
? timeout
|
|
356
|
+
: (provider.timeout || this.timeout || 10000);
|
|
357
|
+
|
|
357
358
|
let res;
|
|
358
359
|
try {
|
|
359
|
-
res = await
|
|
360
|
+
res = await fetchWithTimeout(provider.tokenUrl, {
|
|
360
361
|
method: 'POST',
|
|
361
362
|
headers: {
|
|
362
363
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
@@ -364,6 +365,8 @@ class OAuthClient {
|
|
|
364
365
|
'User-Agent': 'Webspresso-OAuth',
|
|
365
366
|
},
|
|
366
367
|
body: bodyParams.toString(),
|
|
368
|
+
timeout: requestTimeout,
|
|
369
|
+
signal,
|
|
367
370
|
});
|
|
368
371
|
} catch (err) {
|
|
369
372
|
throw new OAuthError(`Token exchange network request failed: ${err.message}`, 'NETWORK_ERROR', err);
|
|
@@ -398,7 +401,7 @@ class OAuthClient {
|
|
|
398
401
|
* @param {Object} tokens - Tokens object returned from exchangeCode
|
|
399
402
|
* @returns {Promise<Object>} Normalized user profile
|
|
400
403
|
*/
|
|
401
|
-
async fetchUserProfile(providerName, tokens) {
|
|
404
|
+
async fetchUserProfile(providerName, tokens, options = {}) {
|
|
402
405
|
const provider = this.getProvider(providerName);
|
|
403
406
|
const accessToken = tokens.access_token || tokens.accessToken;
|
|
404
407
|
|
|
@@ -406,19 +409,26 @@ class OAuthClient {
|
|
|
406
409
|
throw new OAuthError('Missing access_token for user profile retrieval', 'MISSING_TOKEN');
|
|
407
410
|
}
|
|
408
411
|
|
|
412
|
+
const requestTimeout = typeof options?.timeout === 'number'
|
|
413
|
+
? options.timeout
|
|
414
|
+
: (provider.timeout || this.timeout || 10000);
|
|
415
|
+
const signal = options?.signal;
|
|
416
|
+
|
|
409
417
|
let extraData = null;
|
|
410
418
|
if (typeof provider.fetchExtra === 'function') {
|
|
411
|
-
extraData = await provider.fetchExtra(tokens);
|
|
419
|
+
extraData = await provider.fetchExtra(tokens, { timeout: requestTimeout, signal });
|
|
412
420
|
}
|
|
413
421
|
|
|
414
422
|
let res;
|
|
415
423
|
try {
|
|
416
|
-
res = await
|
|
424
|
+
res = await fetchWithTimeout(provider.userInfoUrl, {
|
|
417
425
|
headers: {
|
|
418
426
|
Authorization: `Bearer ${accessToken}`,
|
|
419
427
|
Accept: 'application/json',
|
|
420
428
|
'User-Agent': 'Webspresso-OAuth',
|
|
421
429
|
},
|
|
430
|
+
timeout: requestTimeout,
|
|
431
|
+
signal,
|
|
422
432
|
});
|
|
423
433
|
} catch (err) {
|
|
424
434
|
throw new OAuthError(`User profile fetch failed: ${err.message}`, 'NETWORK_ERROR', err);
|
|
@@ -457,7 +467,7 @@ class OAuthClient {
|
|
|
457
467
|
* @param {string} [params.redirectUri] - Redirect URI
|
|
458
468
|
* @returns {Promise<{ provider: string, user: Object, tokens: Object, stateData: Object }>}
|
|
459
469
|
*/
|
|
460
|
-
async handleCallback(providerName, { code, state, redirectUri }) {
|
|
470
|
+
async handleCallback(providerName, { code, state, redirectUri, timeout, signal } = {}) {
|
|
461
471
|
if (!state) {
|
|
462
472
|
throw new OAuthError('Missing OAuth state in callback', 'INVALID_STATE');
|
|
463
473
|
}
|
|
@@ -483,10 +493,15 @@ class OAuthClient {
|
|
|
483
493
|
code,
|
|
484
494
|
redirectUri: effectiveRedirectUri,
|
|
485
495
|
codeVerifier,
|
|
496
|
+
timeout,
|
|
497
|
+
signal,
|
|
486
498
|
});
|
|
487
499
|
|
|
488
500
|
// Fetch normalized profile
|
|
489
|
-
const user = await this.fetchUserProfile(providerName, tokens
|
|
501
|
+
const user = await this.fetchUserProfile(providerName, tokens, {
|
|
502
|
+
timeout,
|
|
503
|
+
signal,
|
|
504
|
+
});
|
|
490
505
|
|
|
491
506
|
return {
|
|
492
507
|
provider: providerName,
|
package/core/auth/totp.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const crypto = require('crypto');
|
|
8
|
+
const { sha256 } = require('../utils/crypto');
|
|
8
9
|
const { generateQrSvg, generateQrDataUri, createQrMatrix } = require('./qrcode');
|
|
9
10
|
|
|
10
11
|
const RFC4648_BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
@@ -163,20 +164,8 @@ function generateHotp(secret, counter, options = {}) {
|
|
|
163
164
|
return token;
|
|
164
165
|
}
|
|
165
166
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
* @param {string} a
|
|
169
|
-
* @param {string} b
|
|
170
|
-
* @returns {boolean}
|
|
171
|
-
*/
|
|
172
|
-
function timingSafeTokenCompare(a, b) {
|
|
173
|
-
if (typeof a !== 'string' || typeof b !== 'string') return false;
|
|
174
|
-
if (a.length !== b.length) return false;
|
|
175
|
-
|
|
176
|
-
const bufA = Buffer.from(a);
|
|
177
|
-
const bufB = Buffer.from(b);
|
|
178
|
-
return crypto.timingSafeEqual(bufA, bufB);
|
|
179
|
-
}
|
|
167
|
+
const { safeCompare } = require('../utils/crypto');
|
|
168
|
+
const timingSafeTokenCompare = safeCompare;
|
|
180
169
|
|
|
181
170
|
/**
|
|
182
171
|
* Verify HOTP token against counter
|
|
@@ -388,7 +377,7 @@ function hashRecoveryCode(code) {
|
|
|
388
377
|
throw new TypeError('Recovery code must be a string');
|
|
389
378
|
}
|
|
390
379
|
const clean = code.toLowerCase().replace(/[\s-]/g, '');
|
|
391
|
-
return
|
|
380
|
+
return sha256(clean);
|
|
392
381
|
}
|
|
393
382
|
|
|
394
383
|
/**
|
|
@@ -412,7 +401,7 @@ function verifyRecoveryCode(code, hashedCodes = []) {
|
|
|
412
401
|
continue;
|
|
413
402
|
}
|
|
414
403
|
const candidateBuf = Buffer.from(candidateHash, 'hex');
|
|
415
|
-
if (
|
|
404
|
+
if (safeCompare(targetBuf, candidateBuf)) {
|
|
416
405
|
matchedIndex = i;
|
|
417
406
|
break;
|
|
418
407
|
}
|
package/core/auth/user-agent.js
CHANGED
|
@@ -17,27 +17,7 @@
|
|
|
17
17
|
* @property {boolean} isBot
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
* Extract client IP address handling proxies
|
|
22
|
-
* @param {Object} req - Express / Node.js HTTP request
|
|
23
|
-
* @returns {string}
|
|
24
|
-
*/
|
|
25
|
-
function getClientIp(req) {
|
|
26
|
-
if (!req) return '127.0.0.1';
|
|
27
|
-
|
|
28
|
-
const forwarded = req.headers?.['x-forwarded-for'];
|
|
29
|
-
if (forwarded) {
|
|
30
|
-
const ips = typeof forwarded === 'string' ? forwarded.split(',') : forwarded;
|
|
31
|
-
const first = Array.isArray(ips) ? ips[0] : ips;
|
|
32
|
-
if (first) return first.trim();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return req.headers?.['x-real-ip'] ||
|
|
36
|
-
req.ip ||
|
|
37
|
-
req.connection?.remoteAddress ||
|
|
38
|
-
req.socket?.remoteAddress ||
|
|
39
|
-
'127.0.0.1';
|
|
40
|
-
}
|
|
20
|
+
const { getClientIp } = require('../utils/ip');
|
|
41
21
|
|
|
42
22
|
/**
|
|
43
23
|
* Parse raw User-Agent header string into structured device info
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso - Zero-Dependency Approximate Byte Sizer
|
|
5
|
+
* Fast, non-blocking memory footprint calculator for LRU cache entries.
|
|
6
|
+
* Handles strings, numbers, buffers, dates, arrays, and objects with recursion depth limits.
|
|
7
|
+
* @module core/cache/byte-size
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const MAX_RECURSION_DEPTH = 6;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Approximates memory byte size of any JavaScript value.
|
|
14
|
+
*
|
|
15
|
+
* @param {*} value - Value to calculate size for
|
|
16
|
+
* @param {number} [depth=0] - Current recursion depth
|
|
17
|
+
* @param {Set} [seen=null] - Circular reference protection set
|
|
18
|
+
* @returns {number} Approximate size in bytes
|
|
19
|
+
*/
|
|
20
|
+
function approximateByteSize(value, depth = 0, seen = null) {
|
|
21
|
+
if (value === null || value === undefined) {
|
|
22
|
+
return 8;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const type = typeof value;
|
|
26
|
+
|
|
27
|
+
if (type === 'boolean') {
|
|
28
|
+
return 4;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (type === 'number') {
|
|
32
|
+
return 8;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (type === 'string') {
|
|
36
|
+
// UTF-8 byte length (fast Buffer approximation)
|
|
37
|
+
return Buffer.byteLength(value, 'utf8') + 8;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (type === 'bigint') {
|
|
41
|
+
return 16;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (type === 'symbol') {
|
|
45
|
+
return 16;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (type === 'function') {
|
|
49
|
+
return 32;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Buffers and TypedArrays
|
|
53
|
+
if (Buffer.isBuffer(value)) {
|
|
54
|
+
return value.length + 32;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (ArrayBuffer.isView(value)) {
|
|
58
|
+
return value.byteLength + 32;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (value instanceof ArrayBuffer) {
|
|
62
|
+
return value.byteLength + 32;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Guard against deep nesting
|
|
66
|
+
if (depth > MAX_RECURSION_DEPTH) {
|
|
67
|
+
return 64;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (type === 'object') {
|
|
71
|
+
if (!seen) {
|
|
72
|
+
seen = new Set();
|
|
73
|
+
}
|
|
74
|
+
if (seen.has(value)) {
|
|
75
|
+
return 0; // Prevent infinite recursion on circular references
|
|
76
|
+
}
|
|
77
|
+
seen.add(value);
|
|
78
|
+
|
|
79
|
+
let bytes = 32; // V8 object header baseline
|
|
80
|
+
|
|
81
|
+
if (Array.isArray(value)) {
|
|
82
|
+
for (let i = 0; i < value.length; i++) {
|
|
83
|
+
bytes += approximateByteSize(value[i], depth + 1, seen);
|
|
84
|
+
}
|
|
85
|
+
return bytes;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (value instanceof Date) {
|
|
89
|
+
return 16;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (value instanceof RegExp) {
|
|
93
|
+
return value.source.length * 2 + 32;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (value instanceof Map) {
|
|
97
|
+
for (const [k, v] of value.entries()) {
|
|
98
|
+
bytes += approximateByteSize(k, depth + 1, seen) + approximateByteSize(v, depth + 1, seen);
|
|
99
|
+
}
|
|
100
|
+
return bytes;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (value instanceof Set) {
|
|
104
|
+
for (const item of value.values()) {
|
|
105
|
+
bytes += approximateByteSize(item, depth + 1, seen);
|
|
106
|
+
}
|
|
107
|
+
return bytes;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const keys = Object.keys(value);
|
|
111
|
+
for (let i = 0; i < keys.length; i++) {
|
|
112
|
+
const k = keys[i];
|
|
113
|
+
bytes += Buffer.byteLength(k, 'utf8') + 8 + approximateByteSize(value[k], depth + 1, seen);
|
|
114
|
+
}
|
|
115
|
+
return bytes;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return 16;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = {
|
|
122
|
+
approximateByteSize,
|
|
123
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso - High-Performance LRU & Tagged Cache Engine
|
|
5
|
+
* Zero-dependency, memory-bounded, leak-free cache primitives with stampede protection.
|
|
6
|
+
* @module core/cache
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { LruCache, createLruCache, safeClone } = require('./lru');
|
|
10
|
+
const { TaggedLruCache, createTaggedLruCache, normalizeTags } = require('./tagged-lru');
|
|
11
|
+
const { approximateByteSize } = require('./byte-size');
|
|
12
|
+
|
|
13
|
+
module.exports = {
|
|
14
|
+
LruCache,
|
|
15
|
+
createLruCache,
|
|
16
|
+
TaggedLruCache,
|
|
17
|
+
createTaggedLruCache,
|
|
18
|
+
approximateByteSize,
|
|
19
|
+
safeClone,
|
|
20
|
+
normalizeTags,
|
|
21
|
+
};
|