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.
Files changed (165) hide show
  1. package/.agents/AGENTS.md +6 -0
  2. package/.agents/ARCHITECTURE.md +23 -0
  3. package/.agents/CACHE.md +147 -0
  4. package/.agents/CLI.md +1 -1
  5. package/.agents/EARLY_HINTS.md +120 -0
  6. package/.agents/HEALTH.md +118 -0
  7. package/.agents/LIMITS.md +109 -0
  8. package/.agents/ORM.md +155 -1
  9. package/.agents/PLUGINS.md +28 -0
  10. package/.agents/RESILIENCE.md +235 -0
  11. package/AGENTS.md +6 -0
  12. package/README.md +2 -2
  13. package/bin/commands/add-docker.js +6 -6
  14. package/bin/commands/dev.js +4 -3
  15. package/bin/commands/new.js +5 -5
  16. package/bin/commands/openapi.js +1 -1
  17. package/bin/commands/start.js +1 -1
  18. package/bin/utils/orm-map-html.js +2 -8
  19. package/bin/utils/project.js +2 -2
  20. package/core/auth/hash.js +5 -4
  21. package/core/auth/jwt.js +6 -57
  22. package/core/auth/magic-link.js +4 -5
  23. package/core/auth/manager.js +3 -5
  24. package/core/auth/oauth.js +45 -30
  25. package/core/auth/totp.js +5 -16
  26. package/core/auth/user-agent.js +1 -21
  27. package/core/cache/byte-size.js +123 -0
  28. package/core/cache/index.js +21 -0
  29. package/core/cache/lru.js +477 -0
  30. package/core/cache/tagged-lru.js +346 -0
  31. package/core/commands/registry.js +6 -20
  32. package/core/content/renderer.js +1 -13
  33. package/core/early-hints/index.js +267 -0
  34. package/core/errors/domain.js +48 -0
  35. package/core/errors/http.js +31 -0
  36. package/core/errors/index.js +10 -0
  37. package/core/errors/middleware.js +2 -8
  38. package/core/errors/normalize.js +88 -2
  39. package/core/events/discovery.js +6 -24
  40. package/core/events/dispatcher.js +2 -7
  41. package/core/forms/form-builder.js +1 -9
  42. package/core/health/index.js +41 -0
  43. package/core/health/middleware.js +213 -0
  44. package/core/health/normalize.js +140 -0
  45. package/core/health/probes.js +203 -0
  46. package/core/limits/index.js +34 -0
  47. package/core/limits/middleware.js +317 -0
  48. package/core/limits/normalize.js +222 -0
  49. package/core/orm/batch.js +70 -0
  50. package/core/orm/cache/fingerprint.js +2 -2
  51. package/core/orm/cache/memory-provider.js +27 -110
  52. package/core/orm/index.js +76 -8
  53. package/core/orm/json-fields.js +22 -8
  54. package/core/orm/model.js +10 -0
  55. package/core/orm/query-builder.js +63 -9
  56. package/core/orm/query-filters.js +393 -0
  57. package/core/orm/repository.js +318 -19
  58. package/core/orm/schema-helpers.js +8 -10
  59. package/core/orm/scopes.js +321 -1
  60. package/core/orm/transaction.js +10 -2
  61. package/core/orm/utils.js +4 -86
  62. package/core/queue/adapters/database.js +4 -8
  63. package/core/queue/manager.js +55 -41
  64. package/core/resilience/circuit-breaker.js +391 -0
  65. package/core/resilience/errors.js +63 -0
  66. package/core/resilience/index.js +53 -0
  67. package/core/resilience/registry.js +97 -0
  68. package/core/resilience/retry.js +203 -0
  69. package/core/schema-cache.js +9 -2
  70. package/core/shutdown/http-adapter.js +4 -1
  71. package/core/shutdown/shutdown-manager.js +132 -31
  72. package/core/utils/array.js +12 -0
  73. package/core/utils/base64url.js +8 -0
  74. package/core/utils/crypto.js +191 -0
  75. package/core/utils/ensure-leading-slash.js +6 -0
  76. package/core/utils/escape-html.js +7 -0
  77. package/core/utils/escape-regexp.js +7 -0
  78. package/core/utils/fetch.js +121 -0
  79. package/core/utils/fs.js +144 -0
  80. package/core/utils/get-client-ip.js +6 -0
  81. package/core/utils/hmac.js +6 -0
  82. package/core/utils/html.js +72 -0
  83. package/core/utils/ip.js +86 -0
  84. package/core/utils/json.js +98 -0
  85. package/core/utils/object.js +106 -0
  86. package/core/utils/random.js +9 -0
  87. package/core/utils/regex.js +77 -0
  88. package/core/utils/safe-compare.js +3 -0
  89. package/core/utils/sha256.js +6 -0
  90. package/core/utils/string.js +87 -0
  91. package/core/utils/strip-trailing-slash.js +6 -0
  92. package/core/utils/time.js +96 -0
  93. package/core/utils/timeout.js +348 -0
  94. package/core/utils/url.js +106 -0
  95. package/core/utils/walk.js +7 -0
  96. package/index.d.ts +627 -2
  97. package/index.js +158 -0
  98. package/package.json +1 -2
  99. package/plugins/admin-panel/auth.js +15 -7
  100. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +82 -10
  101. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +4 -2
  102. package/plugins/admin-panel/client/parts/08-record-list.js +3 -2
  103. package/plugins/admin-panel/client/parts/10-export-registry.js +6 -0
  104. package/plugins/admin-panel/core/api-extensions.js +3 -80
  105. package/plugins/admin-panel/index.js +1 -1
  106. package/plugins/admin-panel/lib/login-rate-limiter.js +15 -13
  107. package/plugins/admin-panel/modules/menu.js +1 -1
  108. package/plugins/api-key/crypto.js +3 -26
  109. package/plugins/api-key/scopes.js +5 -7
  110. package/plugins/audit-log/api-handlers.js +8 -7
  111. package/plugins/audit-log/middleware.js +11 -2
  112. package/plugins/audit-log/parse.js +3 -3
  113. package/plugins/basic-auth/index.js +2 -26
  114. package/plugins/cors.js +2 -2
  115. package/plugins/csrf/index.js +7 -23
  116. package/plugins/dashboard/app.js +2 -2
  117. package/plugins/dashboard/index.js +5 -5
  118. package/plugins/data-exchange/record-selection.js +2 -5
  119. package/plugins/email/api-handlers.js +5 -18
  120. package/plugins/email/auth-bridge.js +3 -2
  121. package/plugins/file-manager/admin-component.js +3 -1
  122. package/plugins/file-manager/core.js +34 -4
  123. package/plugins/health-check.js +2 -1
  124. package/plugins/index.js +2 -0
  125. package/plugins/lazy.js +5 -6
  126. package/plugins/mcp/auth.js +1 -13
  127. package/plugins/mcp/discovery.js +18 -17
  128. package/plugins/mcp/index.js +23 -1
  129. package/plugins/mcp/transports/sse.js +10 -1
  130. package/plugins/orm-cache-admin/admin-component.js +3 -2
  131. package/plugins/polar/src/api.js +11 -3
  132. package/plugins/polar/src/webhooks.js +7 -13
  133. package/plugins/rate-limit/index.js +3 -1
  134. package/plugins/recaptcha/helpers.js +1 -7
  135. package/plugins/recaptcha/verify.js +37 -23
  136. package/plugins/redirect/index.js +57 -7
  137. package/plugins/rest-resources/index.js +1 -207
  138. package/plugins/rls/index.js +170 -0
  139. package/plugins/schema-explorer/admin-component.js +4 -2
  140. package/plugins/schema-explorer.js +40 -29
  141. package/plugins/seo-checker/index.js +5 -2
  142. package/plugins/site-analytics/tracking.js +21 -18
  143. package/plugins/sitemap.js +21 -17
  144. package/plugins/swagger.js +13 -17
  145. package/plugins/upload/index.js +4 -3
  146. package/plugins/upload/orphan-pruner.js +4 -3
  147. package/plugins/webhooks/crypto.js +6 -28
  148. package/plugins/webhooks/dispatcher.js +14 -9
  149. package/plugins/webhooks/index.js +6 -0
  150. package/plugins/webhooks/matcher.js +28 -17
  151. package/plugins/webhooks/sender.js +40 -13
  152. package/src/app-context.js +41 -5
  153. package/src/dev/banner.js +6 -2
  154. package/src/dev-reload/index.js +47 -6
  155. package/src/file-router.js +1 -3
  156. package/src/helpers.js +180 -33
  157. package/src/modules/module-discovery.js +1 -9
  158. package/src/pages/page-loader.js +53 -1
  159. package/src/plugin-manager.js +90 -17
  160. package/src/server.js +137 -26
  161. package/src/services/discovery.js +19 -40
  162. package/src/services/executor.js +86 -25
  163. package/src/services/memoize.js +3 -25
  164. package/src/services/registry.js +6 -0
  165. 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
- const crypto = require('crypto');
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
- const crypto = require('crypto');
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 crypto.createHash('sha256').update(String(token)).digest('hex');
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
- function parseDuration(duration) {
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 = crypto
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 = crypto
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
 
@@ -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
- let baseUrl = String(options.baseUrl || process.env.BASE_URL || 'http://localhost:3000');
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.startsWith('/') ? path : `/${path}`;
29
+ const cleanPath = ensureLeadingSlash(path);
31
30
  const sep = cleanPath.includes('?') ? '&' : '?';
32
31
  return `${baseUrl}${cleanPath}${sep}token=${encodeURIComponent(token)}`;
33
32
  }
@@ -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
- let baseUrl = String(options.baseUrl || this.config.baseUrl || process.env.BASE_URL || 'http://localhost:3000');
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.startsWith('/') ? pathSegment : `/${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);
@@ -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 res = await fetch('https://api.github.com/user/emails', {
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 = crypto.randomBytes(32).toString('base64url');
109
- const codeChallenge = crypto
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: crypto.randomBytes(16).toString('hex'),
137
+ nonce: randomToken(16, 'hex'),
136
138
  };
137
139
 
138
- const body = Buffer.from(JSON.stringify(dataWithMeta), 'utf8').toString('base64url');
139
- const signature = crypto
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 = crypto
166
- .createHmac('sha256', secret)
167
- .update(body)
168
- .digest('base64url');
164
+ const expectedSignature = hmacSha256(body, secret, 'base64url');
169
165
 
170
- const bufA = Buffer.from(signature);
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 = Buffer.from(body, 'base64url').toString('utf8');
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 fetch(provider.tokenUrl, {
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 fetch(provider.userInfoUrl, {
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
- * Timing-safe token equality check
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 crypto.createHash('sha256').update(clean, 'utf8').digest('hex');
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 (crypto.timingSafeEqual(targetBuf, candidateBuf)) {
404
+ if (safeCompare(targetBuf, candidateBuf)) {
416
405
  matchedIndex = i;
417
406
  break;
418
407
  }
@@ -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
+ };