webspresso 0.0.104 → 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 (132) 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 +1 -1
  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/project.js +2 -2
  19. package/core/auth/hash.js +5 -4
  20. package/core/auth/jwt.js +5 -54
  21. package/core/auth/magic-link.js +4 -5
  22. package/core/auth/manager.js +3 -5
  23. package/core/auth/oauth.js +8 -17
  24. package/core/auth/totp.js +2 -1
  25. package/core/cache/byte-size.js +123 -0
  26. package/core/cache/index.js +21 -0
  27. package/core/cache/lru.js +477 -0
  28. package/core/cache/tagged-lru.js +346 -0
  29. package/core/commands/registry.js +6 -20
  30. package/core/early-hints/index.js +267 -0
  31. package/core/errors/domain.js +48 -0
  32. package/core/errors/http.js +31 -0
  33. package/core/errors/index.js +10 -0
  34. package/core/errors/normalize.js +88 -2
  35. package/core/events/discovery.js +6 -24
  36. package/core/events/dispatcher.js +2 -7
  37. package/core/health/index.js +41 -0
  38. package/core/health/middleware.js +213 -0
  39. package/core/health/normalize.js +140 -0
  40. package/core/health/probes.js +203 -0
  41. package/core/limits/index.js +34 -0
  42. package/core/limits/middleware.js +317 -0
  43. package/core/limits/normalize.js +222 -0
  44. package/core/orm/batch.js +70 -0
  45. package/core/orm/cache/fingerprint.js +2 -2
  46. package/core/orm/cache/memory-provider.js +27 -110
  47. package/core/orm/index.js +71 -8
  48. package/core/orm/json-fields.js +22 -8
  49. package/core/orm/model.js +10 -0
  50. package/core/orm/query-builder.js +63 -9
  51. package/core/orm/repository.js +318 -19
  52. package/core/orm/schema-helpers.js +8 -10
  53. package/core/orm/scopes.js +321 -1
  54. package/core/orm/transaction.js +10 -2
  55. package/core/orm/utils.js +4 -86
  56. package/core/queue/adapters/database.js +4 -8
  57. package/core/queue/manager.js +55 -41
  58. package/core/resilience/circuit-breaker.js +391 -0
  59. package/core/resilience/errors.js +63 -0
  60. package/core/resilience/index.js +53 -0
  61. package/core/resilience/registry.js +97 -0
  62. package/core/resilience/retry.js +203 -0
  63. package/core/schema-cache.js +9 -2
  64. package/core/shutdown/http-adapter.js +4 -1
  65. package/core/shutdown/shutdown-manager.js +120 -19
  66. package/core/utils/array.js +12 -0
  67. package/core/utils/base64url.js +8 -0
  68. package/core/utils/crypto.js +118 -0
  69. package/core/utils/ensure-leading-slash.js +6 -0
  70. package/core/utils/escape-regexp.js +7 -0
  71. package/core/utils/fetch.js +6 -5
  72. package/core/utils/fs.js +144 -0
  73. package/core/utils/hmac.js +6 -0
  74. package/core/utils/json.js +98 -0
  75. package/core/utils/object.js +106 -0
  76. package/core/utils/random.js +9 -0
  77. package/core/utils/regex.js +77 -0
  78. package/core/utils/sha256.js +6 -0
  79. package/core/utils/string.js +87 -0
  80. package/core/utils/strip-trailing-slash.js +6 -0
  81. package/core/utils/time.js +96 -0
  82. package/core/utils/timeout.js +348 -0
  83. package/core/utils/url.js +106 -0
  84. package/core/utils/walk.js +7 -0
  85. package/index.d.ts +616 -1
  86. package/index.js +148 -0
  87. package/package.json +1 -2
  88. package/plugins/admin-panel/core/api-extensions.js +2 -5
  89. package/plugins/admin-panel/index.js +1 -1
  90. package/plugins/admin-panel/modules/menu.js +1 -1
  91. package/plugins/api-key/crypto.js +3 -3
  92. package/plugins/api-key/scopes.js +5 -7
  93. package/plugins/audit-log/api-handlers.js +6 -15
  94. package/plugins/audit-log/parse.js +3 -3
  95. package/plugins/csrf/index.js +5 -9
  96. package/plugins/dashboard/app.js +2 -2
  97. package/plugins/dashboard/index.js +5 -5
  98. package/plugins/data-exchange/record-selection.js +2 -5
  99. package/plugins/email/api-handlers.js +5 -18
  100. package/plugins/email/auth-bridge.js +3 -2
  101. package/plugins/health-check.js +2 -1
  102. package/plugins/index.js +2 -0
  103. package/plugins/lazy.js +5 -6
  104. package/plugins/mcp/discovery.js +18 -17
  105. package/plugins/polar/src/api.js +2 -1
  106. package/plugins/polar/src/webhooks.js +5 -6
  107. package/plugins/rate-limit/index.js +3 -1
  108. package/plugins/redirect/index.js +9 -7
  109. package/plugins/rls/index.js +170 -0
  110. package/plugins/site-analytics/tracking.js +2 -2
  111. package/plugins/sitemap.js +3 -2
  112. package/plugins/swagger.js +4 -3
  113. package/plugins/upload/index.js +4 -3
  114. package/plugins/upload/orphan-pruner.js +4 -3
  115. package/plugins/webhooks/crypto.js +6 -11
  116. package/plugins/webhooks/dispatcher.js +8 -8
  117. package/plugins/webhooks/index.js +6 -0
  118. package/plugins/webhooks/matcher.js +7 -11
  119. package/plugins/webhooks/sender.js +8 -11
  120. package/src/app-context.js +41 -5
  121. package/src/dev/banner.js +6 -2
  122. package/src/file-router.js +1 -3
  123. package/src/helpers.js +159 -24
  124. package/src/modules/module-discovery.js +1 -9
  125. package/src/pages/page-loader.js +53 -1
  126. package/src/plugin-manager.js +90 -17
  127. package/src/server.js +136 -25
  128. package/src/services/discovery.js +19 -40
  129. package/src/services/executor.js +86 -25
  130. package/src/services/memoize.js +3 -25
  131. package/src/services/registry.js +6 -0
  132. package/src/template/precompile.js +8 -19
package/core/auth/jwt.js CHANGED
@@ -5,60 +5,17 @@
5
5
  */
6
6
 
7
7
  const crypto = require('crypto');
8
- const { safeCompare } = require('../utils/crypto');
8
+ const { safeCompare, hmacSha256, base64UrlEncode, base64UrlDecode } = require('../utils/crypto');
9
+ const { parseDurationSec } = require('../utils/time');
9
10
 
10
11
  const HS256_HEADER_BASE64URL = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9';
11
12
 
12
- /**
13
- * Base64URL encode string or Buffer
14
- * @param {string|Buffer} input
15
- * @returns {string}
16
- */
17
- function base64UrlEncode(input) {
18
- return Buffer.isBuffer(input)
19
- ? input.toString('base64url')
20
- : Buffer.from(String(input), 'utf8').toString('base64url');
21
- }
22
-
23
- /**
24
- * Base64URL decode string
25
- * @param {string} input
26
- * @returns {string}
27
- */
28
- function base64UrlDecode(input) {
29
- return Buffer.from(String(input), 'base64url').toString('utf8');
30
- }
31
-
32
13
  /**
33
14
  * Parse duration string (e.g. '1h', '7d', '15m', '3600') into seconds
34
15
  * @param {string|number} duration
35
16
  * @returns {number} Seconds
36
17
  */
37
- function parseDuration(duration) {
38
- if (typeof duration === 'number') {
39
- return duration;
40
- }
41
- if (typeof duration !== 'string') {
42
- return 3600; // Default 1 hour
43
- }
44
-
45
- const match = duration.trim().match(/^(\d+)\s*([smhd])?$/i);
46
- if (!match) {
47
- const parsed = parseInt(duration, 10);
48
- return isNaN(parsed) ? 3600 : parsed;
49
- }
50
-
51
- const value = parseInt(match[1], 10);
52
- const unit = (match[2] || 's').toLowerCase();
53
-
54
- switch (unit) {
55
- case 's': return value;
56
- case 'm': return value * 60;
57
- case 'h': return value * 3600;
58
- case 'd': return value * 86400;
59
- default: return value;
60
- }
61
- }
18
+ const parseDuration = parseDurationSec;
62
19
 
63
20
  /**
64
21
  * Sign payload to create HS256 JWT string
@@ -110,10 +67,7 @@ function signJwt(payload, secret, options = {}) {
110
67
  const encodedPayload = base64UrlEncode(JSON.stringify(fullPayload));
111
68
 
112
69
  const signingInput = `${encodedHeader}.${encodedPayload}`;
113
- const encodedSignature = crypto
114
- .createHmac('sha256', secret)
115
- .update(signingInput)
116
- .digest('base64url');
70
+ const encodedSignature = hmacSha256(signingInput, secret, 'base64url');
117
71
 
118
72
  return `${signingInput}.${encodedSignature}`;
119
73
  }
@@ -166,10 +120,7 @@ function verifyJwt(token, secret, options = {}) {
166
120
 
167
121
  // Re-compute signature
168
122
  const signingInput = `${encodedHeader}.${encodedPayload}`;
169
- const expectedSignatureBuf = crypto
170
- .createHmac('sha256', secret)
171
- .update(signingInput)
172
- .digest();
123
+ const expectedSignatureBuf = hmacSha256(signingInput, secret, 'buffer');
173
124
 
174
125
  const actualSignatureBuf = Buffer.from(encodedSignature, 'base64url');
175
126
 
@@ -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);
@@ -7,7 +7,7 @@
7
7
 
8
8
  const crypto = require('crypto');
9
9
  const { fetchWithTimeout } = require('../utils/fetch');
10
- const { safeCompare } = require('../utils/crypto');
10
+ const { safeCompare, sha256, hmacSha256, base64UrlEncode, base64UrlDecode, randomToken } = require('../utils/crypto');
11
11
 
12
12
  /**
13
13
  * Custom error class for OAuth operations
@@ -110,11 +110,8 @@ const DEFAULT_PROVIDERS = {
110
110
  * @returns {{ codeVerifier: string, codeChallenge: string, codeChallengeMethod: 'S256' }}
111
111
  */
112
112
  function generatePKCE() {
113
- const codeVerifier = crypto.randomBytes(32).toString('base64url');
114
- const codeChallenge = crypto
115
- .createHash('sha256')
116
- .update(codeVerifier)
117
- .digest('base64url');
113
+ const codeVerifier = randomToken(32, 'base64url');
114
+ const codeChallenge = sha256(codeVerifier, 'base64url');
118
115
 
119
116
  return {
120
117
  codeVerifier,
@@ -137,14 +134,11 @@ function createSignedState(payload, secret) {
137
134
  const dataWithMeta = {
138
135
  ...payload,
139
136
  timestamp: Date.now(),
140
- nonce: crypto.randomBytes(16).toString('hex'),
137
+ nonce: randomToken(16, 'hex'),
141
138
  };
142
139
 
143
- const body = Buffer.from(JSON.stringify(dataWithMeta), 'utf8').toString('base64url');
144
- const signature = crypto
145
- .createHmac('sha256', secret)
146
- .update(body)
147
- .digest('base64url');
140
+ const body = base64UrlEncode(JSON.stringify(dataWithMeta));
141
+ const signature = hmacSha256(body, secret, 'base64url');
148
142
 
149
143
  return `${body}.${signature}`;
150
144
  }
@@ -167,10 +161,7 @@ function verifySignedState(stateString, secret, maxAgeMs = 10 * 60 * 1000) {
167
161
  }
168
162
 
169
163
  const [body, signature] = parts;
170
- const expectedSignature = crypto
171
- .createHmac('sha256', secret)
172
- .update(body)
173
- .digest('base64url');
164
+ const expectedSignature = hmacSha256(body, secret, 'base64url');
174
165
 
175
166
  if (!safeCompare(signature, expectedSignature)) {
176
167
  throw new OAuthError('OAuth state signature verification failed', 'INVALID_STATE');
@@ -178,7 +169,7 @@ function verifySignedState(stateString, secret, maxAgeMs = 10 * 60 * 1000) {
178
169
 
179
170
  let payload;
180
171
  try {
181
- const jsonStr = Buffer.from(body, 'base64url').toString('utf8');
172
+ const jsonStr = base64UrlDecode(body);
182
173
  payload = JSON.parse(jsonStr);
183
174
  } catch {
184
175
  throw new OAuthError('Failed to parse OAuth state payload', 'INVALID_STATE');
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';
@@ -376,7 +377,7 @@ function hashRecoveryCode(code) {
376
377
  throw new TypeError('Recovery code must be a string');
377
378
  }
378
379
  const clean = code.toLowerCase().replace(/[\s-]/g, '');
379
- return crypto.createHash('sha256').update(clean, 'utf8').digest('hex');
380
+ return sha256(clean);
380
381
  }
381
382
 
382
383
  /**
@@ -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
+ };