webspresso 0.0.91 → 0.0.93
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/README.md +103 -2864
- package/bin/commands/admin-password.js +8 -2
- package/bin/commands/api.js +7 -1
- package/bin/commands/favicon-generate.js +6 -4
- package/bin/commands/mcp.js +162 -0
- package/bin/commands/new.js +7 -1
- package/bin/commands/page.js +7 -1
- package/bin/commands/seed.js +7 -1
- package/bin/commands/service.js +8 -2
- package/bin/commands/skill.js +7 -1
- package/bin/webspresso.js +2 -0
- package/core/compileSchema.js +1 -5
- package/core/compression/compressible.js +17 -5
- package/core/compression/compression-stream.js +1 -1
- package/core/compression/middleware.js +9 -0
- package/core/compression/negotiate.js +23 -11
- package/core/kernel/app.js +2 -1
- package/core/kernel/index.js +5 -1
- package/core/orm/cache/fingerprint.js +23 -5
- package/core/orm/eager-loader.js +54 -9
- package/core/orm/index.js +38 -2
- package/core/orm/model.js +11 -2
- package/core/queue/adapters/database.js +46 -13
- package/core/queue/adapters/memory.js +21 -0
- package/core/queue/adapters/redis.js +21 -0
- package/core/queue/manager.js +46 -20
- package/core/validation/index.js +70 -0
- package/index.d.ts +99 -5
- package/index.js +7 -1
- package/package.json +9 -1
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +6 -1
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +18 -8
- package/plugins/admin-panel/client/parts/07-model-list.js +9 -4
- package/plugins/admin-panel/client/parts/08-record-list.js +20 -2
- package/plugins/admin-panel/index.js +65 -6
- package/plugins/admin-panel/modules/bulk-actions.js +9 -2
- package/plugins/admin-panel/modules/dashboard.js +8 -1
- package/plugins/data-exchange/export-xlsx.js +28 -6
- package/plugins/data-exchange/import.js +45 -20
- package/plugins/data-exchange/index.js +5 -0
- package/plugins/data-exchange/parse-table.js +14 -5
- package/plugins/data-exchange/record-selection.js +2 -0
- package/plugins/index.js +4 -0
- package/plugins/mcp/auth.js +104 -0
- package/plugins/mcp/discovery.js +88 -0
- package/plugins/mcp/index.js +208 -0
- package/plugins/mcp/prompts.js +149 -0
- package/plugins/mcp/providers/orm.js +325 -0
- package/plugins/mcp/providers/services.js +190 -0
- package/plugins/mcp/providers/system.js +87 -0
- package/plugins/mcp/server.js +472 -0
- package/plugins/mcp/transports/sse.js +139 -0
- package/plugins/mcp/transports/stdio.js +108 -0
- package/src/file-router.js +184 -123
- package/src/njk-frontmatter.js +24 -14
- package/src/server.js +30 -12
- package/src/services/discovery.js +2 -1
- package/src/services/registry.js +2 -0
- package/src/services/validator.js +2 -53
- package/templates/skills/webspresso-usage/REFERENCE-framework.md +6 -4
package/core/orm/index.js
CHANGED
|
@@ -74,10 +74,46 @@ function createDatabase(config) {
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
// Create Knex instance
|
|
77
|
+
// Create Knex instance (default useNullAsDefault: true for SQLite to prevent unneeded warnings)
|
|
78
|
+
const isSqlite = client === 'better-sqlite3' || client === 'sqlite3';
|
|
79
|
+
const knexConfig = {
|
|
80
|
+
...(isSqlite && config.useNullAsDefault === undefined ? { useNullAsDefault: true } : {}),
|
|
81
|
+
...config,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (isSqlite) {
|
|
85
|
+
const userAfterCreate = knexConfig.pool?.afterCreate;
|
|
86
|
+
knexConfig.pool = {
|
|
87
|
+
...(knexConfig.pool || {}),
|
|
88
|
+
afterCreate: (conn, cb) => {
|
|
89
|
+
try {
|
|
90
|
+
if (typeof conn.pragma === 'function') {
|
|
91
|
+
const isMemory =
|
|
92
|
+
knexConfig.connection === ':memory:' ||
|
|
93
|
+
knexConfig.connection?.filename === ':memory:' ||
|
|
94
|
+
knexConfig.connection?.database === ':memory:';
|
|
95
|
+
|
|
96
|
+
if (!isMemory) {
|
|
97
|
+
try { conn.pragma('journal_mode = WAL'); } catch (e) {}
|
|
98
|
+
}
|
|
99
|
+
try { conn.pragma('synchronous = NORMAL'); } catch (e) {}
|
|
100
|
+
try { conn.pragma('busy_timeout = 5000'); } catch (e) {}
|
|
101
|
+
try { conn.pragma('foreign_keys = ON'); } catch (e) {}
|
|
102
|
+
}
|
|
103
|
+
} catch (e) {}
|
|
104
|
+
|
|
105
|
+
if (typeof userAfterCreate === 'function') {
|
|
106
|
+
userAfterCreate(conn, cb);
|
|
107
|
+
} else {
|
|
108
|
+
cb(null, conn);
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
78
114
|
let knexInstance;
|
|
79
115
|
try {
|
|
80
|
-
knexInstance = knex(
|
|
116
|
+
knexInstance = knex(knexConfig);
|
|
81
117
|
} catch (e) {
|
|
82
118
|
// Provide helpful error message
|
|
83
119
|
if (e.message && (e.message.includes('Cannot find module') || e.message.includes('npm install'))) {
|
package/core/orm/model.js
CHANGED
|
@@ -46,13 +46,22 @@ function defineModel(options) {
|
|
|
46
46
|
throw new Error('Model schema is required and must be a Zod schema');
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
let normalizedSchema = schema;
|
|
50
|
+
if (schema && schema.shape) {
|
|
51
|
+
for (const [key, val] of Object.entries(schema.shape)) {
|
|
52
|
+
if (val && typeof val._finalize === 'function') {
|
|
53
|
+
schema.shape[key] = val._finalize();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
49
58
|
// Check for duplicate registration
|
|
50
59
|
if (modelRegistry.has(name)) {
|
|
51
60
|
throw new Error(`Model "${name}" is already defined`);
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
// Extract column metadata from schema
|
|
55
|
-
const columns = extractColumnsFromSchema(
|
|
64
|
+
const columns = extractColumnsFromSchema(normalizedSchema);
|
|
56
65
|
|
|
57
66
|
// Validate relations
|
|
58
67
|
const normalizedRelations = {};
|
|
@@ -80,7 +89,7 @@ function defineModel(options) {
|
|
|
80
89
|
const model = {
|
|
81
90
|
name,
|
|
82
91
|
table,
|
|
83
|
-
schema,
|
|
92
|
+
schema: normalizedSchema,
|
|
84
93
|
primaryKey,
|
|
85
94
|
relations: normalizedRelations,
|
|
86
95
|
scopes: {
|
|
@@ -46,6 +46,7 @@ class DatabaseQueueAdapter {
|
|
|
46
46
|
table.dateTime('run_at').notNullable().index();
|
|
47
47
|
table.dateTime('created_at').notNullable();
|
|
48
48
|
table.dateTime('updated_at').notNullable();
|
|
49
|
+
table.index(['status', 'run_at', 'priority']);
|
|
49
50
|
});
|
|
50
51
|
}
|
|
51
52
|
|
|
@@ -88,8 +89,20 @@ class DatabaseQueueAdapter {
|
|
|
88
89
|
* @returns {Promise<Job|null>}
|
|
89
90
|
*/
|
|
90
91
|
async dequeue(jobNames = []) {
|
|
92
|
+
const jobs = await this.dequeueMany(jobNames, 1);
|
|
93
|
+
return jobs.length > 0 ? jobs[0] : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Dequeue multiple pending jobs up to limit in a single transaction
|
|
98
|
+
* @param {string[]} [jobNames]
|
|
99
|
+
* @param {number} [limit=1]
|
|
100
|
+
* @returns {Promise<Job[]>}
|
|
101
|
+
*/
|
|
102
|
+
async dequeueMany(jobNames = [], limit = 1) {
|
|
91
103
|
await this.ensureTable();
|
|
92
104
|
const now = new Date();
|
|
105
|
+
const take = Math.max(1, limit || 1);
|
|
93
106
|
|
|
94
107
|
return this.knex.transaction(async (trx) => {
|
|
95
108
|
let query = trx(this.tableName)
|
|
@@ -97,14 +110,14 @@ class DatabaseQueueAdapter {
|
|
|
97
110
|
.where('run_at', '<=', now)
|
|
98
111
|
.orderBy('priority', 'desc')
|
|
99
112
|
.orderBy('run_at', 'asc')
|
|
100
|
-
.limit(
|
|
113
|
+
.limit(take);
|
|
101
114
|
|
|
102
115
|
if (jobNames && jobNames.length > 0) {
|
|
103
116
|
query = query.whereIn('name', jobNames);
|
|
104
117
|
}
|
|
105
118
|
|
|
106
119
|
// If Postgres or MySQL support forUpdate / skipLocked
|
|
107
|
-
const client = this.knex.client
|
|
120
|
+
const client = this.knex.client?.config?.client;
|
|
108
121
|
if (client === 'pg' || client === 'postgres' || client === 'mysql' || client === 'mysql2') {
|
|
109
122
|
try {
|
|
110
123
|
query = query.forUpdate().skipLocked();
|
|
@@ -112,26 +125,46 @@ class DatabaseQueueAdapter {
|
|
|
112
125
|
}
|
|
113
126
|
|
|
114
127
|
const rows = await query;
|
|
115
|
-
if (!rows || rows.length === 0) return
|
|
128
|
+
if (!rows || rows.length === 0) return [];
|
|
116
129
|
|
|
117
|
-
const
|
|
118
|
-
const nextAttempts = Number(row.attempts) + 1;
|
|
130
|
+
const ids = rows.map((r) => r.id);
|
|
119
131
|
const updatedAt = new Date();
|
|
120
132
|
|
|
133
|
+
if (take === 1) {
|
|
134
|
+
const row = rows[0];
|
|
135
|
+
const nextAttempts = Number(row.attempts) + 1;
|
|
136
|
+
await trx(this.tableName)
|
|
137
|
+
.where('id', row.id)
|
|
138
|
+
.update({
|
|
139
|
+
status: 'running',
|
|
140
|
+
attempts: nextAttempts,
|
|
141
|
+
updated_at: updatedAt,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return [this._rowToJob({
|
|
145
|
+
...row,
|
|
146
|
+
status: 'running',
|
|
147
|
+
attempts: nextAttempts,
|
|
148
|
+
updated_at: updatedAt,
|
|
149
|
+
})];
|
|
150
|
+
}
|
|
151
|
+
|
|
121
152
|
await trx(this.tableName)
|
|
122
|
-
.
|
|
153
|
+
.whereIn('id', ids)
|
|
123
154
|
.update({
|
|
124
155
|
status: 'running',
|
|
125
|
-
attempts:
|
|
156
|
+
attempts: trx.raw('attempts + 1'),
|
|
126
157
|
updated_at: updatedAt,
|
|
127
158
|
});
|
|
128
159
|
|
|
129
|
-
return
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
160
|
+
return rows.map((row) =>
|
|
161
|
+
this._rowToJob({
|
|
162
|
+
...row,
|
|
163
|
+
status: 'running',
|
|
164
|
+
attempts: Number(row.attempts) + 1,
|
|
165
|
+
updated_at: updatedAt,
|
|
166
|
+
})
|
|
167
|
+
);
|
|
135
168
|
});
|
|
136
169
|
}
|
|
137
170
|
|
|
@@ -60,6 +60,27 @@ class MemoryQueueAdapter {
|
|
|
60
60
|
return null;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Dequeue multiple pending jobs up to limit
|
|
65
|
+
* @param {string[]} [jobNames]
|
|
66
|
+
* @param {number} [limit=1]
|
|
67
|
+
* @returns {Promise<Job[]>}
|
|
68
|
+
*/
|
|
69
|
+
async dequeueMany(jobNames = [], limit = 1) {
|
|
70
|
+
if (limit <= 1) {
|
|
71
|
+
const single = await this.dequeue(jobNames);
|
|
72
|
+
return single ? [single] : [];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const jobs = [];
|
|
76
|
+
for (let i = 0; i < limit; i++) {
|
|
77
|
+
const job = await this.dequeue(jobNames);
|
|
78
|
+
if (!job) break;
|
|
79
|
+
jobs.push(job);
|
|
80
|
+
}
|
|
81
|
+
return jobs;
|
|
82
|
+
}
|
|
83
|
+
|
|
63
84
|
/**
|
|
64
85
|
* Mark job as completed
|
|
65
86
|
* @param {string|number} jobId
|
|
@@ -157,6 +157,27 @@ class RedisQueueAdapter {
|
|
|
157
157
|
return null;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Dequeue multiple pending jobs up to limit
|
|
162
|
+
* @param {string[]} [jobNames]
|
|
163
|
+
* @param {number} [limit=1]
|
|
164
|
+
* @returns {Promise<Job[]>}
|
|
165
|
+
*/
|
|
166
|
+
async dequeueMany(jobNames = [], limit = 1) {
|
|
167
|
+
if (limit <= 1) {
|
|
168
|
+
const single = await this.dequeue(jobNames);
|
|
169
|
+
return single ? [single] : [];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const jobs = [];
|
|
173
|
+
for (let i = 0; i < limit; i++) {
|
|
174
|
+
const job = await this.dequeue(jobNames);
|
|
175
|
+
if (!job) break;
|
|
176
|
+
jobs.push(job);
|
|
177
|
+
}
|
|
178
|
+
return jobs;
|
|
179
|
+
}
|
|
180
|
+
|
|
160
181
|
/**
|
|
161
182
|
* Mark job as completed
|
|
162
183
|
* @param {string|number} jobId
|
package/core/queue/manager.js
CHANGED
|
@@ -26,6 +26,8 @@ class QueueManager extends EventEmitter {
|
|
|
26
26
|
this.adapter = options.adapter || new MemoryQueueAdapter();
|
|
27
27
|
this.concurrency = Math.max(1, options.concurrency || 2);
|
|
28
28
|
this.pollInterval = Math.max(50, options.pollInterval || 500);
|
|
29
|
+
this.maxPollInterval = Math.max(this.pollInterval, options.maxPollInterval || (this.pollInterval * 5));
|
|
30
|
+
this._currentPollInterval = this.pollInterval;
|
|
29
31
|
this.jobsDir = options.jobsDir || null;
|
|
30
32
|
|
|
31
33
|
/** @type {Map<string, { handler: Function, options: Object }>} */
|
|
@@ -125,7 +127,8 @@ class QueueManager extends EventEmitter {
|
|
|
125
127
|
const enqueued = await this.adapter.enqueue(job);
|
|
126
128
|
this.emit('job:queued', enqueued);
|
|
127
129
|
|
|
128
|
-
//
|
|
130
|
+
// Reset polling backoff and trigger immediate poll if capacity is available
|
|
131
|
+
this._currentPollInterval = this.pollInterval;
|
|
129
132
|
if (this.isRunning && !this.isPaused && this.runningCount < this.concurrency) {
|
|
130
133
|
setImmediate(() => this._tick());
|
|
131
134
|
}
|
|
@@ -140,6 +143,7 @@ class QueueManager extends EventEmitter {
|
|
|
140
143
|
if (this.isRunning) return this;
|
|
141
144
|
this.isRunning = true;
|
|
142
145
|
this.isPaused = false;
|
|
146
|
+
this._currentPollInterval = this.pollInterval;
|
|
143
147
|
this._scheduleNextTick(0);
|
|
144
148
|
this.emit('start');
|
|
145
149
|
return this;
|
|
@@ -164,6 +168,7 @@ class QueueManager extends EventEmitter {
|
|
|
164
168
|
resume() {
|
|
165
169
|
if (!this.isRunning) return this.start();
|
|
166
170
|
this.isPaused = false;
|
|
171
|
+
this._currentPollInterval = this.pollInterval;
|
|
167
172
|
this._scheduleNextTick(0);
|
|
168
173
|
this.emit('resume');
|
|
169
174
|
return this;
|
|
@@ -268,7 +273,7 @@ class QueueManager extends EventEmitter {
|
|
|
268
273
|
* Schedule next tick
|
|
269
274
|
* @private
|
|
270
275
|
*/
|
|
271
|
-
_scheduleNextTick(delay = this.
|
|
276
|
+
_scheduleNextTick(delay = this._currentPollInterval) {
|
|
272
277
|
if (!this.isRunning || this.isPaused || this.isDraining) return;
|
|
273
278
|
if (this._timer) clearTimeout(this._timer);
|
|
274
279
|
this._timer = setTimeout(() => this._tick(), delay);
|
|
@@ -281,28 +286,45 @@ class QueueManager extends EventEmitter {
|
|
|
281
286
|
async _tick() {
|
|
282
287
|
if (!this.isRunning || this.isPaused || this.isDraining) return;
|
|
283
288
|
|
|
284
|
-
|
|
285
|
-
|
|
289
|
+
const availableSlots = this.concurrency - this.runningCount;
|
|
290
|
+
if (availableSlots <= 0) return;
|
|
286
291
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
292
|
+
const registeredNames = Array.from(this.handlers.keys());
|
|
293
|
+
if (registeredNames.length === 0) {
|
|
294
|
+
this._scheduleNextTick(this._currentPollInterval);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
291
297
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
+
let jobs = [];
|
|
299
|
+
try {
|
|
300
|
+
if (typeof this.adapter.dequeueMany === 'function' && availableSlots > 1) {
|
|
301
|
+
jobs = await this.adapter.dequeueMany(registeredNames, availableSlots);
|
|
302
|
+
} else {
|
|
303
|
+
const single = await this.adapter.dequeue(registeredNames);
|
|
304
|
+
if (single) jobs = [single];
|
|
298
305
|
}
|
|
306
|
+
} catch (err) {
|
|
307
|
+
this.emit('error', err);
|
|
308
|
+
this._scheduleNextTick(this._currentPollInterval);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
299
311
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
312
|
+
if (!jobs || jobs.length === 0) {
|
|
313
|
+
// No jobs ready -> back off polling interval
|
|
314
|
+
this._currentPollInterval = Math.min(
|
|
315
|
+
this.maxPollInterval,
|
|
316
|
+
Math.round(this._currentPollInterval * 1.5)
|
|
317
|
+
);
|
|
318
|
+
this._scheduleNextTick(this._currentPollInterval);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Found job(s) -> reset polling interval
|
|
323
|
+
this._currentPollInterval = this.pollInterval;
|
|
324
|
+
|
|
325
|
+
for (const job of jobs) {
|
|
326
|
+
if (this.isPaused || this.isDraining) break;
|
|
304
327
|
|
|
305
|
-
// Found a job -> process it
|
|
306
328
|
this.runningCount++;
|
|
307
329
|
this.activeJobs.set(job.id, job);
|
|
308
330
|
|
|
@@ -320,7 +342,11 @@ class QueueManager extends EventEmitter {
|
|
|
320
342
|
});
|
|
321
343
|
}
|
|
322
344
|
|
|
323
|
-
this.
|
|
345
|
+
if (this.runningCount < this.concurrency) {
|
|
346
|
+
this._scheduleNextTick(0);
|
|
347
|
+
} else {
|
|
348
|
+
this._scheduleNextTick(this.pollInterval);
|
|
349
|
+
}
|
|
324
350
|
}
|
|
325
351
|
|
|
326
352
|
/**
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso Unified Validation Core
|
|
3
|
+
* Centralized Zod extensions, prototype pollution defense, and schema utilities
|
|
4
|
+
* @module core/validation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const z = require('zod');
|
|
8
|
+
const { extendZ, generateNanoid, zodNanoid } = require('../orm/utils/nanoid');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Standard Zod instance extended with z.nanoid() for framework-wide validation.
|
|
12
|
+
*/
|
|
13
|
+
const zExtended = extendZ(z);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if an object contains dangerous prototype pollution keys
|
|
17
|
+
* @param {*} obj
|
|
18
|
+
* @returns {boolean}
|
|
19
|
+
*/
|
|
20
|
+
function hasDangerousKeys(obj) {
|
|
21
|
+
if (obj === null || typeof obj !== 'object') {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
if (Array.isArray(obj)) {
|
|
25
|
+
return obj.some(item => hasDangerousKeys(item));
|
|
26
|
+
}
|
|
27
|
+
for (const key of Object.keys(obj)) {
|
|
28
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
if (typeof obj[key] === 'object' && obj[key] !== null && hasDangerousKeys(obj[key])) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Sanitize input payload against prototype pollution
|
|
40
|
+
* @param {*} input
|
|
41
|
+
* @returns {*} Sanitized input
|
|
42
|
+
*/
|
|
43
|
+
function sanitizeInput(input) {
|
|
44
|
+
if (input === null || typeof input !== 'object') {
|
|
45
|
+
return input;
|
|
46
|
+
}
|
|
47
|
+
if (!hasDangerousKeys(input)) {
|
|
48
|
+
return input;
|
|
49
|
+
}
|
|
50
|
+
if (Array.isArray(input)) {
|
|
51
|
+
return input.map(item => sanitizeInput(item));
|
|
52
|
+
}
|
|
53
|
+
const clean = {};
|
|
54
|
+
for (const key of Object.keys(input)) {
|
|
55
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
clean[key] = sanitizeInput(input[key]);
|
|
59
|
+
}
|
|
60
|
+
return clean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = {
|
|
64
|
+
z: zExtended,
|
|
65
|
+
extendZ,
|
|
66
|
+
generateNanoid,
|
|
67
|
+
zodNanoid,
|
|
68
|
+
hasDangerousKeys,
|
|
69
|
+
sanitizeInput,
|
|
70
|
+
};
|
package/index.d.ts
CHANGED
|
@@ -34,7 +34,21 @@ export interface CreateAppOptions {
|
|
|
34
34
|
version?: string;
|
|
35
35
|
manifestPath?: string;
|
|
36
36
|
prefix?: string;
|
|
37
|
-
|
|
37
|
+
publicDir?: string;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* If truthy, route load() return values for stylesheets and scripts are promoted to pageHead
|
|
41
|
+
*/
|
|
42
|
+
pageAssets?:
|
|
43
|
+
| boolean
|
|
44
|
+
| {
|
|
45
|
+
enabled?: boolean;
|
|
46
|
+
stylesheets?: boolean;
|
|
47
|
+
scripts?: boolean;
|
|
48
|
+
};
|
|
49
|
+
/** Trust proxy configuration (default: 1) */
|
|
50
|
+
trustProxy?: boolean | number | string;
|
|
51
|
+
/**
|
|
38
52
|
* Custom 404 / 500 / 503 handlers. File-based route errors are forwarded with `next(err)` and hit `serverError` / `timeout`.
|
|
39
53
|
* When `serverError` / `timeout` is a template path, it is not used for paths under `/api` (default JSON instead).
|
|
40
54
|
*/
|
|
@@ -56,6 +70,7 @@ export interface CreateAppOptions {
|
|
|
56
70
|
port?: number;
|
|
57
71
|
shutdown?: ShutdownOptions;
|
|
58
72
|
compression?: boolean | CompressionOptions;
|
|
73
|
+
trustProxy?: boolean | number | string;
|
|
59
74
|
[key: string]: unknown;
|
|
60
75
|
};
|
|
61
76
|
/** Opt-in Alpine / swup assets under `/__webspresso/client-runtime/*`. Env: WEBSPRESSO_ALPINE, WEBSPRESSO_SWUP. */
|
|
@@ -76,7 +91,14 @@ export interface ServiceContext {
|
|
|
76
91
|
req?: Request;
|
|
77
92
|
res?: Response;
|
|
78
93
|
db?: DatabaseInstance | null;
|
|
79
|
-
|
|
94
|
+
fsy?: Record<string, unknown>;
|
|
95
|
+
locale?: string;
|
|
96
|
+
t?: unknown;
|
|
97
|
+
service?: {
|
|
98
|
+
<T = unknown>(name: string, input?: unknown, options?: unknown): Promise<T>;
|
|
99
|
+
invalidate?: (name: string, input?: unknown, ctx?: unknown) => boolean;
|
|
100
|
+
clearCache?: (name?: string) => boolean;
|
|
101
|
+
};
|
|
80
102
|
[key: string]: unknown;
|
|
81
103
|
}
|
|
82
104
|
|
|
@@ -89,9 +111,16 @@ export interface ServiceDefinition<TInput = unknown, TOutput = unknown> {
|
|
|
89
111
|
schema?: import('zod').ZodType<any> | Record<string, unknown> | ((input: unknown) => unknown) | null;
|
|
90
112
|
handler: ServiceHandler<TInput, TOutput>;
|
|
91
113
|
metadata?: Record<string, unknown>;
|
|
92
|
-
auth?: boolean | string | string[];
|
|
93
|
-
cache?:
|
|
94
|
-
|
|
114
|
+
auth?: boolean | string | string[] | ((user: unknown, ctx: ServiceContext) => boolean | Promise<boolean>);
|
|
115
|
+
cache?:
|
|
116
|
+
| boolean
|
|
117
|
+
| string
|
|
118
|
+
| number
|
|
119
|
+
| {
|
|
120
|
+
ttl?: string | number;
|
|
121
|
+
key?: string | ((input: unknown, ctx: ServiceContext) => string);
|
|
122
|
+
};
|
|
123
|
+
timeout?: number | string;
|
|
95
124
|
transaction?: boolean;
|
|
96
125
|
[key: string]: unknown;
|
|
97
126
|
}
|
|
@@ -274,6 +303,7 @@ export interface WebspressoApplication extends Application {
|
|
|
274
303
|
server?: unknown;
|
|
275
304
|
readonly isShuttingDown: boolean;
|
|
276
305
|
shutdownManager: ShutdownManager;
|
|
306
|
+
serviceRegistry: ServiceRegistry;
|
|
277
307
|
onShutdown(fn: () => Promise<void> | void): this;
|
|
278
308
|
close(reason?: string): Promise<void>;
|
|
279
309
|
enableShutdownHooks(): this;
|
|
@@ -1583,4 +1613,68 @@ export const ssr: {
|
|
|
1583
1613
|
renderStream: typeof renderStream;
|
|
1584
1614
|
};
|
|
1585
1615
|
|
|
1616
|
+
// --- Model Context Protocol (MCP) Plugin ---
|
|
1617
|
+
|
|
1618
|
+
export interface McpPluginOptions {
|
|
1619
|
+
path?: string;
|
|
1620
|
+
enabled?: boolean;
|
|
1621
|
+
auth?: {
|
|
1622
|
+
token?: string;
|
|
1623
|
+
localhostOnly?: boolean;
|
|
1624
|
+
verify?: (req: import('express').Request) => boolean | Promise<boolean>;
|
|
1625
|
+
};
|
|
1626
|
+
services?: boolean | {
|
|
1627
|
+
include?: string[];
|
|
1628
|
+
exclude?: string[];
|
|
1629
|
+
readOnly?: boolean;
|
|
1630
|
+
role?: string | string[];
|
|
1631
|
+
};
|
|
1632
|
+
orm?: boolean | {
|
|
1633
|
+
readOnly?: boolean;
|
|
1634
|
+
models?: string[];
|
|
1635
|
+
};
|
|
1636
|
+
system?: boolean;
|
|
1637
|
+
tools?: Array<{
|
|
1638
|
+
name: string;
|
|
1639
|
+
description?: string;
|
|
1640
|
+
inputSchema?: Record<string, unknown>;
|
|
1641
|
+
handler: (args: any, ctx: any) => Promise<any> | any;
|
|
1642
|
+
}>;
|
|
1643
|
+
resources?: Array<{
|
|
1644
|
+
uri?: string;
|
|
1645
|
+
uriTemplate?: string;
|
|
1646
|
+
name?: string;
|
|
1647
|
+
description?: string;
|
|
1648
|
+
mimeType?: string;
|
|
1649
|
+
handler: (uri: string, paramsOrCtx: any, ctx?: any) => Promise<any> | any;
|
|
1650
|
+
}>;
|
|
1651
|
+
prompts?: Array<{
|
|
1652
|
+
name: string;
|
|
1653
|
+
description?: string;
|
|
1654
|
+
arguments?: Array<{ name: string; description?: string; required?: boolean }>;
|
|
1655
|
+
handler: (args: any, ctx: any) => Promise<any> | any;
|
|
1656
|
+
}>;
|
|
1657
|
+
discovery?: boolean;
|
|
1658
|
+
name?: string;
|
|
1659
|
+
version?: string;
|
|
1660
|
+
instructions?: string;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
export class McpServer {
|
|
1664
|
+
name: string;
|
|
1665
|
+
version: string;
|
|
1666
|
+
instructions: string;
|
|
1667
|
+
constructor(options?: { name?: string; version?: string; instructions?: string; context?: Record<string, unknown> });
|
|
1668
|
+
registerTool(toolDef: any): this;
|
|
1669
|
+
registerTools(toolDefs: any[]): this;
|
|
1670
|
+
unregisterTool(name: string): boolean;
|
|
1671
|
+
registerResource(resourceDef: any): this;
|
|
1672
|
+
registerResourceTemplate(templateDef: any): this;
|
|
1673
|
+
registerPrompt(promptDef: any): this;
|
|
1674
|
+
handleMessage(message: any, callContext?: any): Promise<any>;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
export function mcpPlugin(options?: McpPluginOptions): WebspressoPlugin;
|
|
1678
|
+
export function createMcpServer(options?: McpPluginOptions): McpServer;
|
|
1679
|
+
|
|
1586
1680
|
|
package/index.js
CHANGED
|
@@ -55,7 +55,7 @@ const {
|
|
|
55
55
|
// ORM exports (lazy loaded)
|
|
56
56
|
const orm = require('./core/orm');
|
|
57
57
|
|
|
58
|
-
/**
|
|
58
|
+
/** Standalone experimental application kernel (event bus, plugin shell, view resolver, flows). Distinct from framework SSR `createApp`. */
|
|
59
59
|
const kernel = require('./core/kernel');
|
|
60
60
|
|
|
61
61
|
// Built-in plugins
|
|
@@ -87,6 +87,9 @@ const {
|
|
|
87
87
|
socketIo,
|
|
88
88
|
redis,
|
|
89
89
|
createRedisAdapter,
|
|
90
|
+
mcpPlugin,
|
|
91
|
+
createMcpServer,
|
|
92
|
+
McpServer,
|
|
90
93
|
} = require('./plugins');
|
|
91
94
|
|
|
92
95
|
const content = require('./core/content');
|
|
@@ -237,6 +240,9 @@ module.exports = {
|
|
|
237
240
|
queue: require('./core/queue'),
|
|
238
241
|
createQueueManager: require('./core/queue').createQueueManager,
|
|
239
242
|
QueueManager: require('./core/queue').QueueManager,
|
|
243
|
+
mcpPlugin,
|
|
244
|
+
createMcpServer,
|
|
245
|
+
McpServer,
|
|
240
246
|
|
|
241
247
|
// Schema-driven CMS core (framework-agnostic)
|
|
242
248
|
content,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webspresso",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.93",
|
|
4
4
|
"description": "Minimal, production-ready SSR framework for Node.js with file-based routing, Nunjucks templating, built-in i18n, and CLI tooling",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -42,6 +42,10 @@
|
|
|
42
42
|
"bench:ci:local": "npm run bench:baseline && npm run bench:ci",
|
|
43
43
|
"check:types": "tsc --project tests/ts-smoke/tsconfig.json",
|
|
44
44
|
"rebuild:native": "npm rebuild better-sqlite3 bcrypt sharp",
|
|
45
|
+
"docs:dev": "docboot dev ./docs",
|
|
46
|
+
"docs:build": "docboot build ./docs",
|
|
47
|
+
"docs:serve": "docboot serve dist",
|
|
48
|
+
"docs:doctor": "docboot doctor ./docs",
|
|
45
49
|
"release": "release-it"
|
|
46
50
|
},
|
|
47
51
|
"keywords": [
|
|
@@ -134,6 +138,7 @@
|
|
|
134
138
|
"@vitest/coverage-v8": "^3.0.0",
|
|
135
139
|
"better-sqlite3": "^11.10.0",
|
|
136
140
|
"chokidar": "^3.5.3",
|
|
141
|
+
"docboot": "^0.3.0",
|
|
137
142
|
"dotenv": "^16.3.1",
|
|
138
143
|
"express-rate-limit": "^8.4.1",
|
|
139
144
|
"release-it": "^20.2.1",
|
|
@@ -149,5 +154,8 @@
|
|
|
149
154
|
"tar": "^7.5.11",
|
|
150
155
|
"undici": "^6.24.0",
|
|
151
156
|
"uuid": "^11.1.1"
|
|
157
|
+
},
|
|
158
|
+
"allowScripts": {
|
|
159
|
+
"better-sqlite3@11.10.0": true
|
|
152
160
|
}
|
|
153
161
|
}
|
|
@@ -59,10 +59,15 @@ async function downloadDataExchangeXlsx(modelName, payload) {
|
|
|
59
59
|
const blob = await res.blob();
|
|
60
60
|
var url = URL.createObjectURL(blob);
|
|
61
61
|
var a = document.createElement('a');
|
|
62
|
+
a.style.display = 'none';
|
|
62
63
|
a.href = url;
|
|
63
64
|
a.download = modelName + '-export.xlsx';
|
|
65
|
+
document.body.appendChild(a);
|
|
64
66
|
a.click();
|
|
65
|
-
|
|
67
|
+
setTimeout(function () {
|
|
68
|
+
if (a.parentNode) a.parentNode.removeChild(a);
|
|
69
|
+
URL.revokeObjectURL(url);
|
|
70
|
+
}, 1000);
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
// Helper: Capitalize first letter of each word
|