webspresso 0.0.106 → 0.0.108
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 +4 -1
- package/.agents/CACHE.md +1 -0
- package/.agents/CONTEXT.md +152 -0
- package/.agents/ORM.md +74 -0
- package/.agents/SHUTDOWN.md +8 -1
- package/.agents/skills/webspresso-framework/SKILL.md +2 -1
- package/.agents/skills/webspresso-orm-and-database/SKILL.md +9 -0
- package/.agents/skills/webspresso-routing-and-api/SKILL.md +18 -2
- package/.agents/skills/webspresso-services/SKILL.md +36 -3
- package/AGENTS.md +6 -3
- package/bin/webspresso.js +17 -7
- package/core/app.js +45 -11
- package/core/applySchema.js +3 -72
- package/core/auth/index.js +4 -1
- package/core/auth/jwt.js +85 -13
- package/core/auth/magic-link.js +23 -5
- package/core/auth/manager.js +7 -1
- package/core/auth/redis-session.js +62 -25
- package/core/auth/session-manager.js +28 -2
- package/core/auth/sessions.js +182 -52
- package/core/cache/lru.js +10 -0
- package/core/cache/tagged-lru.js +4 -0
- package/core/compileSchema.js +3 -68
- package/core/compression/compressible.js +53 -6
- package/core/compression/negotiate.js +31 -12
- package/core/console/eval.js +4 -1
- package/core/content/service.js +1 -0
- package/core/context/app-context.js +201 -0
- package/core/context/index.js +55 -0
- package/core/context/request.js +241 -0
- package/core/errors/domain.js +20 -2
- package/core/errors/http.js +1 -0
- package/core/events/dispatcher.js +144 -15
- package/core/events/signal.js +9 -0
- package/core/health/probes.js +12 -2
- package/core/http/hono-adapter.js +1 -1
- package/core/http/index.js +10 -1
- package/core/http/pipeline.js +519 -334
- package/core/index.js +36 -2
- package/core/mail/mailer.js +1 -1
- package/core/orm/eager-loader.js +170 -55
- package/core/orm/index.js +25 -47
- package/core/orm/json-fields.js +44 -13
- package/core/orm/model.js +18 -1
- package/core/orm/query-builder.js +33 -15
- package/core/orm/repository.js +59 -12
- package/core/orm/schema-helpers.js +59 -0
- package/core/orm/scopes.js +54 -4
- package/core/orm/transaction.js +109 -14
- package/core/plugins/index.js +8 -0
- package/core/plugins/plugin-manager.js +1098 -0
- package/core/queue/manager.js +55 -27
- package/core/rate-limit/stores/memory.js +23 -10
- package/core/resilience/circuit-breaker.js +34 -14
- package/core/schema-cache.js +2 -64
- package/core/services/builtins/auth.js +273 -0
- package/core/services/builtins/exchange.js +229 -0
- package/core/services/builtins/file-manager.js +178 -0
- package/core/services/builtins/mail.js +134 -0
- package/core/services/builtins/media.js +196 -0
- package/core/services/builtins/system.js +219 -0
- package/core/services/discovery.js +133 -0
- package/core/services/executor.js +629 -0
- package/core/services/index.js +114 -0
- package/core/services/loader.js +161 -0
- package/core/services/memoize.js +408 -0
- package/core/services/registry.js +731 -0
- package/core/services/validator.js +424 -0
- package/core/shutdown/shutdown-manager.js +194 -114
- package/core/ssr/stream.js +109 -16
- package/core/stream/text-stream.js +45 -4
- package/core/studio/inspector.js +9 -1
- package/core/studio/server.js +24 -8
- package/core/utils/index.js +110 -0
- package/core/utils/string.js +77 -16
- package/core/utils/timeout.js +4 -0
- package/core/validation/apply.js +74 -0
- package/core/validation/cache.js +66 -0
- package/core/validation/compiler.js +77 -0
- package/core/validation/index.js +40 -1
- package/index.d.ts +249 -12
- package/index.js +68 -20
- package/package.json +18 -8
- package/plugins/admin-panel/client/parts/07-model-list.js +5 -2
- package/plugins/admin-panel/index.js +24 -11
- package/plugins/admin-panel/modules/user-management.js +3 -2
- package/plugins/csrf/index.js +2 -1
- package/plugins/dashboard/index.js +27 -7
- package/src/app-context.js +4 -188
- package/src/dev/typegen.js +1 -1
- package/src/file-router.js +12 -3
- package/src/plugin-manager.js +5 -861
- package/src/server.js +30 -34
- package/src/services/builtins/auth.js +2 -273
- package/src/services/builtins/exchange.js +2 -229
- package/src/services/builtins/file-manager.js +2 -178
- package/src/services/builtins/mail.js +2 -134
- package/src/services/builtins/media.js +2 -196
- package/src/services/builtins/system.js +2 -219
- package/src/services/discovery.js +2 -125
- package/src/services/executor.js +2 -362
- package/src/services/index.js +4 -60
- package/src/services/loader.js +2 -0
- package/src/services/memoize.js +2 -234
- package/src/services/registry.js +2 -314
- package/src/services/validator.js +2 -273
- package/core/utils/array.js +0 -12
- package/core/utils/base64url.js +0 -8
- package/core/utils/ensure-leading-slash.js +0 -6
- package/core/utils/escape-html.js +0 -7
- package/core/utils/escape-regexp.js +0 -7
- package/core/utils/get-client-ip.js +0 -6
- package/core/utils/hmac.js +0 -6
- package/core/utils/random.js +0 -9
- package/core/utils/safe-compare.js +0 -3
- package/core/utils/sha256.js +0 -6
- package/core/utils/strip-trailing-slash.js +0 -6
- package/core/utils/walk.js +0 -7
package/core/auth/sessions.js
CHANGED
|
@@ -102,29 +102,65 @@ function createKnexSessionTracker(db, options = {}) {
|
|
|
102
102
|
};
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
105
|
+
try {
|
|
106
|
+
const [id] = await knex(tableName).insert({
|
|
107
|
+
session_id: sessionId,
|
|
108
|
+
user_id: userId,
|
|
109
|
+
ip_address: ip,
|
|
110
|
+
user_agent: ua ? ua.slice(0, 1000) : null,
|
|
111
|
+
browser,
|
|
112
|
+
os,
|
|
113
|
+
device,
|
|
114
|
+
last_active_at: now,
|
|
115
|
+
created_at: now,
|
|
116
|
+
});
|
|
116
117
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
118
|
+
return {
|
|
119
|
+
id,
|
|
120
|
+
session_id: sessionId,
|
|
121
|
+
user_id: userId,
|
|
122
|
+
ip_address: ip,
|
|
123
|
+
browser,
|
|
124
|
+
os,
|
|
125
|
+
device,
|
|
126
|
+
last_active_at: now,
|
|
127
|
+
created_at: now,
|
|
128
|
+
};
|
|
129
|
+
} catch (insertErr) {
|
|
130
|
+
// Handle concurrent insert race condition gracefully across database engines
|
|
131
|
+
const isUniqueConstraint =
|
|
132
|
+
insertErr.code === 'SQLITE_CONSTRAINT' ||
|
|
133
|
+
insertErr.code === '23505' || // PostgreSQL
|
|
134
|
+
insertErr.code === 'ER_DUP_ENTRY' || // MySQL
|
|
135
|
+
insertErr.errno === 19 || // SQLite
|
|
136
|
+
insertErr.errno === 1062 || // MySQL
|
|
137
|
+
(typeof insertErr.message === 'string' && /unique|duplicate|constraint/i.test(insertErr.message));
|
|
138
|
+
|
|
139
|
+
if (!isUniqueConstraint) {
|
|
140
|
+
throw insertErr;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
await knex(tableName).where({ session_id: sessionId }).update({
|
|
144
|
+
user_id: userId,
|
|
145
|
+
ip_address: ip,
|
|
146
|
+
user_agent: ua ? ua.slice(0, 1000) : null,
|
|
147
|
+
browser,
|
|
148
|
+
os,
|
|
149
|
+
device,
|
|
150
|
+
last_active_at: now,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const record = await knex(tableName).where({ session_id: sessionId }).first();
|
|
154
|
+
return record || {
|
|
155
|
+
session_id: sessionId,
|
|
156
|
+
user_id: userId,
|
|
157
|
+
ip_address: ip,
|
|
158
|
+
browser,
|
|
159
|
+
os,
|
|
160
|
+
device,
|
|
161
|
+
last_active_at: now,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
128
164
|
},
|
|
129
165
|
|
|
130
166
|
/**
|
|
@@ -415,24 +451,92 @@ function createKvSessionTracker(kv, options = {}) {
|
|
|
415
451
|
return record;
|
|
416
452
|
},
|
|
417
453
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
454
|
+
/**
|
|
455
|
+
* Touch session to update last_active_at and refresh expiration
|
|
456
|
+
* @param {string} sessionId
|
|
457
|
+
* @returns {Promise<boolean>}
|
|
458
|
+
*/
|
|
459
|
+
touch: async (sessionId) => {
|
|
460
|
+
if (!sessionId) return false;
|
|
461
|
+
const record = await kvGet(`${prefix}${sessionId}`);
|
|
462
|
+
if (!record) return false;
|
|
463
|
+
record.last_active_at = new Date().toISOString();
|
|
464
|
+
await kvPut(`${prefix}${sessionId}`, record, defaultTtl);
|
|
465
|
+
return true;
|
|
466
|
+
},
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Get session record by session_id
|
|
470
|
+
* @param {string} sessionId
|
|
471
|
+
* @returns {Promise<Object|null>}
|
|
472
|
+
*/
|
|
473
|
+
get: async (sessionId) => {
|
|
474
|
+
if (!sessionId) return null;
|
|
475
|
+
return (await kvGet(`${prefix}${sessionId}`)) || null;
|
|
476
|
+
},
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* List all active sessions for a user, marking current session
|
|
480
|
+
* @param {string|number} userId
|
|
481
|
+
* @param {string} [currentSessionId]
|
|
482
|
+
* @returns {Promise<Array<Object>>}
|
|
483
|
+
*/
|
|
484
|
+
list: async (userId, currentSessionId = null) => {
|
|
485
|
+
if (!userId) return [];
|
|
486
|
+
if (typeof kv.list !== 'function') return [];
|
|
487
|
+
const listRes = await kv.list({ prefix });
|
|
488
|
+
const keys = listRes.keys || [];
|
|
489
|
+
const items = await Promise.all(
|
|
490
|
+
keys.map((k) => kvGet(k.name).catch(() => null))
|
|
491
|
+
);
|
|
492
|
+
const results = [];
|
|
493
|
+
for (const item of items) {
|
|
494
|
+
if (item && String(item.user_id) === String(userId)) {
|
|
495
|
+
results.push({
|
|
496
|
+
...item,
|
|
497
|
+
is_current: Boolean(currentSessionId && item.session_id === currentSessionId),
|
|
498
|
+
});
|
|
429
499
|
}
|
|
430
|
-
return results;
|
|
431
500
|
}
|
|
432
|
-
return
|
|
501
|
+
return results.sort((a, b) => new Date(b.last_active_at).getTime() - new Date(a.last_active_at).getTime());
|
|
502
|
+
},
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Legacy alias for list()
|
|
506
|
+
* @param {string|number} userId
|
|
507
|
+
* @returns {Promise<Array<Object>>}
|
|
508
|
+
*/
|
|
509
|
+
listUserSessions: async (userId) => {
|
|
510
|
+
if (!userId || typeof kv.list !== 'function') return [];
|
|
511
|
+
const listRes = await kv.list({ prefix });
|
|
512
|
+
const keys = listRes.keys || [];
|
|
513
|
+
const items = await Promise.all(
|
|
514
|
+
keys.map((k) => kvGet(k.name).catch(() => null))
|
|
515
|
+
);
|
|
516
|
+
return items.filter((item) => item && String(item.user_id) === String(userId));
|
|
517
|
+
},
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Check if a session exists and belongs to the given user
|
|
521
|
+
* @param {string} sessionId
|
|
522
|
+
* @param {string|number} [userId]
|
|
523
|
+
* @returns {Promise<boolean>}
|
|
524
|
+
*/
|
|
525
|
+
isSessionValid: async (sessionId, userId = null) => {
|
|
526
|
+
if (!sessionId) return false;
|
|
527
|
+
const record = await kvGet(`${prefix}${sessionId}`);
|
|
528
|
+
if (!record) return false;
|
|
529
|
+
if (userId && String(record.user_id) !== String(userId)) return false;
|
|
530
|
+
return true;
|
|
433
531
|
},
|
|
434
532
|
|
|
435
|
-
|
|
533
|
+
/**
|
|
534
|
+
* Legacy alias for isSessionValid()
|
|
535
|
+
* @param {string} sessionId
|
|
536
|
+
* @param {string|number} [userId]
|
|
537
|
+
* @returns {Promise<boolean>}
|
|
538
|
+
*/
|
|
539
|
+
validate: async (sessionId, userId = null) => {
|
|
436
540
|
if (!sessionId) return false;
|
|
437
541
|
const record = await kvGet(`${prefix}${sessionId}`);
|
|
438
542
|
if (!record) return false;
|
|
@@ -440,38 +544,64 @@ function createKvSessionTracker(kv, options = {}) {
|
|
|
440
544
|
return true;
|
|
441
545
|
},
|
|
442
546
|
|
|
547
|
+
/**
|
|
548
|
+
* Revoke a single session
|
|
549
|
+
* @param {string} sessionId
|
|
550
|
+
* @returns {Promise<boolean>}
|
|
551
|
+
*/
|
|
443
552
|
revoke: async (sessionId) => {
|
|
444
553
|
if (!sessionId) return false;
|
|
445
554
|
await kvDelete(`${prefix}${sessionId}`);
|
|
446
555
|
return true;
|
|
447
556
|
},
|
|
448
557
|
|
|
558
|
+
/**
|
|
559
|
+
* Revoke all other sessions for user, keeping the current session active
|
|
560
|
+
* @param {string|number} userId
|
|
561
|
+
* @param {string} currentSessionId
|
|
562
|
+
* @returns {Promise<number>} Number of revoked sessions
|
|
563
|
+
*/
|
|
449
564
|
revokeOthers: async (userId, currentSessionId) => {
|
|
450
|
-
if (typeof kv.list !== 'function') return 0;
|
|
565
|
+
if (!userId || typeof kv.list !== 'function') return 0;
|
|
451
566
|
const listRes = await kv.list({ prefix });
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
567
|
+
const keys = listRes.keys || [];
|
|
568
|
+
const items = await Promise.all(
|
|
569
|
+
keys.map((k) => kvGet(k.name).then((item) => ({ key: k.name, item })).catch(() => null))
|
|
570
|
+
);
|
|
571
|
+
const toDelete = [];
|
|
572
|
+
for (const entry of items) {
|
|
573
|
+
if (entry?.item && String(entry.item.user_id) === String(userId) && entry.item.session_id !== currentSessionId) {
|
|
574
|
+
toDelete.push(entry.key);
|
|
458
575
|
}
|
|
459
576
|
}
|
|
460
|
-
|
|
577
|
+
if (toDelete.length > 0) {
|
|
578
|
+
await Promise.all(toDelete.map((k) => kvDelete(k)));
|
|
579
|
+
}
|
|
580
|
+
return toDelete.length;
|
|
461
581
|
},
|
|
462
582
|
|
|
583
|
+
/**
|
|
584
|
+
* Revoke all sessions for a user (force logout from all devices)
|
|
585
|
+
* @param {string|number} userId
|
|
586
|
+
* @returns {Promise<number>} Number of revoked sessions
|
|
587
|
+
*/
|
|
463
588
|
revokeAll: async (userId) => {
|
|
464
|
-
if (typeof kv.list !== 'function') return 0;
|
|
589
|
+
if (!userId || typeof kv.list !== 'function') return 0;
|
|
465
590
|
const listRes = await kv.list({ prefix });
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
591
|
+
const keys = listRes.keys || [];
|
|
592
|
+
const items = await Promise.all(
|
|
593
|
+
keys.map((k) => kvGet(k.name).then((item) => ({ key: k.name, item })).catch(() => null))
|
|
594
|
+
);
|
|
595
|
+
const toDelete = [];
|
|
596
|
+
for (const entry of items) {
|
|
597
|
+
if (entry?.item && String(entry.item.user_id) === String(userId)) {
|
|
598
|
+
toDelete.push(entry.key);
|
|
472
599
|
}
|
|
473
600
|
}
|
|
474
|
-
|
|
601
|
+
if (toDelete.length > 0) {
|
|
602
|
+
await Promise.all(toDelete.map((k) => kvDelete(k)));
|
|
603
|
+
}
|
|
604
|
+
return toDelete.length;
|
|
475
605
|
},
|
|
476
606
|
|
|
477
607
|
purgeExpired: async () => {
|
package/core/cache/lru.js
CHANGED
|
@@ -47,6 +47,7 @@ function safeClone(value) {
|
|
|
47
47
|
* @property {string|number} [defaultTtl=0] - Default TTL in ms or string duration ('5m', '1h', 0 = no expiry)
|
|
48
48
|
* @property {function(*, string): number} [sizeCalculation] - Custom byte sizer function
|
|
49
49
|
* @property {boolean} [clone=false] - Whether to deep-clone items on read/write
|
|
50
|
+
* @property {boolean} [trackBytes=false] - Whether to track approximate bytes when maxBytes is 0
|
|
50
51
|
* @property {function(string, *, 'evict'|'expire'|'delete'|'clear'): void} [onEvict] - Eviction callback
|
|
51
52
|
*/
|
|
52
53
|
|
|
@@ -71,6 +72,11 @@ class LruCache {
|
|
|
71
72
|
this.clone = options.clone === true;
|
|
72
73
|
this.onEvict = typeof options.onEvict === 'function' ? options.onEvict : null;
|
|
73
74
|
|
|
75
|
+
// Track bytes only when explicit memory limit exists, custom sizer provided, or trackBytes is explicitly requested
|
|
76
|
+
this.trackBytes = options.trackBytes !== undefined
|
|
77
|
+
? options.trackBytes === true
|
|
78
|
+
: (this.maxBytes > 0 || typeof options.sizeCalculation === 'function');
|
|
79
|
+
|
|
74
80
|
/** @type {Map<string, { value: *, size: number, expiresAt: number }>} */
|
|
75
81
|
this._store = new Map();
|
|
76
82
|
|
|
@@ -115,6 +121,10 @@ class LruCache {
|
|
|
115
121
|
if (this.sizeCalculation) {
|
|
116
122
|
return Math.max(0, this.sizeCalculation(value, key));
|
|
117
123
|
}
|
|
124
|
+
// Fast path: if byte tracking is disabled, avoid expensive recursive object graph traversal
|
|
125
|
+
if (!this.trackBytes) {
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
118
128
|
return Math.max(0, approximateByteSize(value));
|
|
119
129
|
}
|
|
120
130
|
|
package/core/cache/tagged-lru.js
CHANGED
package/core/compileSchema.js
CHANGED
|
@@ -1,69 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Schema Compiler
|
|
3
|
-
* Compiles schema definitions from API files
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const { z: zForApi } = require('./validation');
|
|
7
|
-
const schemaCache = require('./schema-cache');
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Compile schema from an API module
|
|
11
|
-
* @param {string} filePath - Absolute file path to API module
|
|
12
|
-
* @param {Object} apiModule - The loaded API module
|
|
13
|
-
* @returns {Object|null} Compiled schema object or null if no schema
|
|
14
|
-
*/
|
|
15
|
-
function compileSchema(filePath, apiModule) {
|
|
16
|
-
// Return cached schema if exists
|
|
17
|
-
if (schemaCache.has(filePath)) {
|
|
18
|
-
return schemaCache.get(filePath);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Check if module exports schema
|
|
22
|
-
const schemaFn = apiModule.schema;
|
|
23
|
-
|
|
24
|
-
// Schema is optional
|
|
25
|
-
if (schemaFn === undefined) {
|
|
26
|
-
schemaCache.set(filePath, null);
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Schema must be a function
|
|
31
|
-
if (typeof schemaFn !== 'function') {
|
|
32
|
-
throw new Error(`Schema in ${filePath} must be a function`);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Call schema function with { z } (extended with z.nanoid, same as generateNanoid / zdb.nanoid)
|
|
36
|
-
const compiled = schemaFn({ z: zForApi });
|
|
37
|
-
|
|
38
|
-
// Validate compiled schema structure
|
|
39
|
-
if (compiled !== null && typeof compiled !== 'object') {
|
|
40
|
-
throw new Error(`Schema function in ${filePath} must return an object or null`);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Cache and return
|
|
44
|
-
schemaCache.set(filePath, compiled);
|
|
45
|
-
return compiled;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Clear schema cache for a file (for hot-reload)
|
|
50
|
-
* @param {string} filePath - Absolute file path
|
|
51
|
-
*/
|
|
52
|
-
function invalidateSchema(filePath) {
|
|
53
|
-
schemaCache.del(filePath);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Clear all cached schemas
|
|
58
|
-
*/
|
|
59
|
-
function clearAllSchemas() {
|
|
60
|
-
schemaCache.clear();
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
module.exports = {
|
|
64
|
-
compileSchema,
|
|
65
|
-
invalidateSchema,
|
|
66
|
-
clearAllSchemas
|
|
67
|
-
};
|
|
68
|
-
|
|
1
|
+
'use strict';
|
|
69
2
|
|
|
3
|
+
// Backward-compatibility shim
|
|
4
|
+
module.exports = require('./validation/compiler');
|
|
@@ -11,7 +11,56 @@ const COMPRESSIBLE_TYPE_REGEX = /^(?:text\/[a-z0-9.+_-]+|application\/(?:javascr
|
|
|
11
11
|
const INCOMPRESSIBLE_TYPE_REGEX = /^(?:text\/event-stream|image\/(?:jpeg|png|gif|webp|avif|heic|heif)|video\/[a-z0-9.+_-]+|audio\/[a-z0-9.+_-]+|application\/(?:zip|gzip|x-gzip|x-bzip2|x-compress|x-7z-compressed|x-rar-compressed|pdf|wasm|octet-stream))$/i;
|
|
12
12
|
|
|
13
13
|
const compressibleCache = new Map();
|
|
14
|
-
const MAX_MIME_CACHE_SIZE =
|
|
14
|
+
const MAX_MIME_CACHE_SIZE = 512;
|
|
15
|
+
|
|
16
|
+
// Pre-fill standard common web & API MIME types for zero-lookup warmup
|
|
17
|
+
const PREFILLED_MIMES = [
|
|
18
|
+
['text/html', true],
|
|
19
|
+
['text/html; charset=utf-8', true],
|
|
20
|
+
['text/plain', true],
|
|
21
|
+
['text/plain; charset=utf-8', true],
|
|
22
|
+
['text/css', true],
|
|
23
|
+
['text/css; charset=utf-8', true],
|
|
24
|
+
['text/javascript', true],
|
|
25
|
+
['text/javascript; charset=utf-8', true],
|
|
26
|
+
['application/javascript', true],
|
|
27
|
+
['application/javascript; charset=utf-8', true],
|
|
28
|
+
['application/json', true],
|
|
29
|
+
['application/json; charset=utf-8', true],
|
|
30
|
+
['application/ld+json', true],
|
|
31
|
+
['application/manifest+json', true],
|
|
32
|
+
['application/graphql+json', true],
|
|
33
|
+
['application/geo+json', true],
|
|
34
|
+
['application/xml', true],
|
|
35
|
+
['application/atom+xml', true],
|
|
36
|
+
['application/rss+xml', true],
|
|
37
|
+
['application/xhtml+xml', true],
|
|
38
|
+
['image/svg+xml', true],
|
|
39
|
+
['text/event-stream', false],
|
|
40
|
+
['image/png', false],
|
|
41
|
+
['image/jpeg', false],
|
|
42
|
+
['image/webp', false],
|
|
43
|
+
['image/gif', false],
|
|
44
|
+
['image/avif', false],
|
|
45
|
+
['application/pdf', false],
|
|
46
|
+
['application/zip', false],
|
|
47
|
+
['application/gzip', false],
|
|
48
|
+
['application/octet-stream', false],
|
|
49
|
+
['video/mp4', false],
|
|
50
|
+
['audio/mpeg', false],
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
for (const [mime, val] of PREFILLED_MIMES) {
|
|
54
|
+
compressibleCache.set(mime, val);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function setCompressibleCache(key, value) {
|
|
58
|
+
if (compressibleCache.size >= MAX_MIME_CACHE_SIZE) {
|
|
59
|
+
const firstKey = compressibleCache.keys().next().value;
|
|
60
|
+
compressibleCache.delete(firstKey);
|
|
61
|
+
}
|
|
62
|
+
compressibleCache.set(key, value);
|
|
63
|
+
}
|
|
15
64
|
|
|
16
65
|
/**
|
|
17
66
|
* Determine if a Content-Type is compressible
|
|
@@ -33,13 +82,13 @@ function isCompressible(contentType) {
|
|
|
33
82
|
const mime = (semiIndex !== -1 ? contentType.slice(0, semiIndex) : contentType).trim().toLowerCase();
|
|
34
83
|
|
|
35
84
|
if (!mime) {
|
|
36
|
-
|
|
85
|
+
setCompressibleCache(contentType, false);
|
|
37
86
|
return false;
|
|
38
87
|
}
|
|
39
88
|
|
|
40
89
|
// Fast negative check
|
|
41
90
|
if (INCOMPRESSIBLE_TYPE_REGEX.test(mime)) {
|
|
42
|
-
|
|
91
|
+
setCompressibleCache(contentType, false);
|
|
43
92
|
return false;
|
|
44
93
|
}
|
|
45
94
|
|
|
@@ -51,9 +100,7 @@ function isCompressible(contentType) {
|
|
|
51
100
|
result = true;
|
|
52
101
|
}
|
|
53
102
|
|
|
54
|
-
|
|
55
|
-
compressibleCache.set(contentType, result);
|
|
56
|
-
}
|
|
103
|
+
setCompressibleCache(contentType, result);
|
|
57
104
|
|
|
58
105
|
return result;
|
|
59
106
|
}
|
|
@@ -71,9 +71,29 @@ function parseAcceptEncoding(header) {
|
|
|
71
71
|
|
|
72
72
|
const DEFAULT_SUPPORTED_ENCODINGS = Object.freeze(getDefaultSupportedEncodings());
|
|
73
73
|
const DEFAULT_SUPPORTED_STRING = DEFAULT_SUPPORTED_ENCODINGS.join(',');
|
|
74
|
+
const DEFAULT_PREFERRED_ENCODING = DEFAULT_SUPPORTED_ENCODINGS[0] || 'identity';
|
|
74
75
|
|
|
75
76
|
const selectEncodingCache = new Map();
|
|
76
|
-
const MAX_ENCODING_CACHE_SIZE =
|
|
77
|
+
const MAX_ENCODING_CACHE_SIZE = 256;
|
|
78
|
+
|
|
79
|
+
function setSelectEncodingCache(key, value) {
|
|
80
|
+
if (selectEncodingCache.size >= MAX_ENCODING_CACHE_SIZE) {
|
|
81
|
+
const firstKey = selectEncodingCache.keys().next().value;
|
|
82
|
+
selectEncodingCache.delete(firstKey);
|
|
83
|
+
}
|
|
84
|
+
selectEncodingCache.set(key, value);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Pre-fill common browser patterns for default configuration
|
|
88
|
+
if (DEFAULT_SUPPORTED_ENCODINGS.includes('br')) {
|
|
89
|
+
selectEncodingCache.set('gzip, deflate, br', 'br');
|
|
90
|
+
selectEncodingCache.set('gzip, deflate, br, zstd', 'br');
|
|
91
|
+
selectEncodingCache.set('br;q=1.0, gzip;q=0.8, *;q=0.1', 'br');
|
|
92
|
+
}
|
|
93
|
+
selectEncodingCache.set('gzip, deflate', 'gzip');
|
|
94
|
+
selectEncodingCache.set('gzip', 'gzip');
|
|
95
|
+
selectEncodingCache.set('*', DEFAULT_PREFERRED_ENCODING);
|
|
96
|
+
selectEncodingCache.set('identity', 'identity');
|
|
77
97
|
|
|
78
98
|
/**
|
|
79
99
|
* Select the best compression encoding given client header and server supported list
|
|
@@ -82,14 +102,17 @@ const MAX_ENCODING_CACHE_SIZE = 128;
|
|
|
82
102
|
* @returns {string|null} The selected encoding ('br', 'gzip', 'deflate', 'identity', or null)
|
|
83
103
|
*/
|
|
84
104
|
function selectEncoding(acceptEncoding, supportedEncodings) {
|
|
85
|
-
if (!acceptEncoding) {
|
|
105
|
+
if (!acceptEncoding || typeof acceptEncoding !== 'string') {
|
|
106
|
+
return 'identity';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const raw = acceptEncoding.trim();
|
|
110
|
+
if (!raw) {
|
|
86
111
|
return 'identity';
|
|
87
112
|
}
|
|
88
113
|
|
|
89
114
|
const isDefault = !supportedEncodings || !Array.isArray(supportedEncodings) || supportedEncodings.length === 0;
|
|
90
|
-
const cacheKey = isDefault
|
|
91
|
-
? `${acceptEncoding}|${DEFAULT_SUPPORTED_STRING}`
|
|
92
|
-
: `${acceptEncoding}|${supportedEncodings.join(',')}`;
|
|
115
|
+
const cacheKey = isDefault ? raw : `${raw}|${supportedEncodings.join(',')}`;
|
|
93
116
|
|
|
94
117
|
const cached = selectEncodingCache.get(cacheKey);
|
|
95
118
|
if (cached !== undefined) {
|
|
@@ -100,11 +123,9 @@ function selectEncoding(acceptEncoding, supportedEncodings) {
|
|
|
100
123
|
? DEFAULT_SUPPORTED_ENCODINGS
|
|
101
124
|
: supportedEncodings.map(e => e.toLowerCase()).filter(e => e !== 'br' || supportsBrotli());
|
|
102
125
|
|
|
103
|
-
const clientPreferences = parseAcceptEncoding(
|
|
126
|
+
const clientPreferences = parseAcceptEncoding(raw);
|
|
104
127
|
if (clientPreferences.length === 0) {
|
|
105
|
-
|
|
106
|
-
selectEncodingCache.set(cacheKey, 'identity');
|
|
107
|
-
}
|
|
128
|
+
setSelectEncodingCache(cacheKey, 'identity');
|
|
108
129
|
return 'identity';
|
|
109
130
|
}
|
|
110
131
|
|
|
@@ -157,9 +178,7 @@ function selectEncoding(acceptEncoding, supportedEncodings) {
|
|
|
157
178
|
result = wildcardQ > 0 ? 'identity' : null;
|
|
158
179
|
}
|
|
159
180
|
|
|
160
|
-
|
|
161
|
-
selectEncodingCache.set(cacheKey, result);
|
|
162
|
-
}
|
|
181
|
+
setSelectEncodingCache(cacheKey, result);
|
|
163
182
|
|
|
164
183
|
return result;
|
|
165
184
|
}
|
package/core/console/eval.js
CHANGED
|
@@ -15,7 +15,10 @@ function wrapBlock(code) {
|
|
|
15
15
|
const lastLine = lines[lines.length - 1].trim();
|
|
16
16
|
const isStatementKeyword = /^(return|const|let|var|throw|if|while|for|function|class|try|switch|import|export)\b/.test(lastLine);
|
|
17
17
|
if (!isStatementKeyword && lastLine.length > 0) {
|
|
18
|
-
|
|
18
|
+
let expr = lastLine;
|
|
19
|
+
while (expr.endsWith(';')) {
|
|
20
|
+
expr = expr.slice(0, -1);
|
|
21
|
+
}
|
|
19
22
|
lines[lines.length - 1] = `return (${expr});`;
|
|
20
23
|
}
|
|
21
24
|
return lines.join('\n');
|