webspresso 0.0.91 → 0.0.94
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/polar-migrate.js +50 -0
- 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 +4 -0
- package/core/auth/tokens.js +16 -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/cache/layer.js +1 -1
- package/core/orm/eager-loader.js +54 -9
- package/core/orm/index.js +42 -3
- package/core/orm/model.js +11 -2
- package/core/orm/transaction.js +3 -1
- 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/ssr/stream.js +58 -14
- 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/auth.js +10 -0
- 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/content/index.js +5 -3
- package/plugins/data-exchange/export-xlsx.js +28 -6
- package/plugins/data-exchange/import.js +120 -58
- 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 +6 -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 +156 -0
- package/plugins/mcp/transports/stdio.js +108 -0
- package/plugins/polar/.env.example +12 -0
- package/plugins/polar/CHANGELOG.md +38 -0
- package/plugins/polar/README.md +158 -0
- package/plugins/polar/index.js +144 -0
- package/plugins/polar/src/api.js +54 -0
- package/plugins/polar/src/checkout.js +111 -0
- package/plugins/polar/src/config.js +88 -0
- package/plugins/polar/src/env.schema.js +36 -0
- package/plugins/polar/src/middleware.js +37 -0
- package/plugins/polar/src/migration.js +58 -0
- package/plugins/polar/src/nunjucks-filters.js +56 -0
- package/plugins/polar/src/portal.js +51 -0
- package/plugins/polar/src/rate-limit.js +70 -0
- package/plugins/polar/src/routes.js +212 -0
- package/plugins/polar/src/sync.js +138 -0
- package/plugins/polar/src/urls.js +117 -0
- package/plugins/polar/src/user-resolver.js +195 -0
- package/plugins/polar/src/webhooks.js +248 -0
- package/src/file-router.js +203 -125
- package/src/helpers.js +60 -13
- 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 +7 -4
|
@@ -3,8 +3,13 @@
|
|
|
3
3
|
* Reset admin user password via CLI
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
const
|
|
6
|
+
let _inquirer;
|
|
7
|
+
const inquirer = {
|
|
8
|
+
prompt: (...args) => {
|
|
9
|
+
if (!_inquirer) _inquirer = require('inquirer');
|
|
10
|
+
return _inquirer.prompt(...args);
|
|
11
|
+
},
|
|
12
|
+
};
|
|
8
13
|
const { loadDbConfig, createDbInstance } = require('../utils/db');
|
|
9
14
|
|
|
10
15
|
function registerCommand(program) {
|
|
@@ -86,6 +91,7 @@ function registerCommand(program) {
|
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
// Hash the password (same rounds as admin panel setup)
|
|
94
|
+
const { hash } = require('../../core/auth/hash');
|
|
89
95
|
const hashedPassword = await hash(password, 10);
|
|
90
96
|
|
|
91
97
|
// Update the password
|
package/bin/commands/api.js
CHANGED
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
* Add a new API endpoint to the current project
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
let _inquirer;
|
|
7
|
+
const inquirer = {
|
|
8
|
+
prompt: (...args) => {
|
|
9
|
+
if (!_inquirer) _inquirer = require('inquirer');
|
|
10
|
+
return _inquirer.prompt(...args);
|
|
11
|
+
},
|
|
12
|
+
};
|
|
7
13
|
const fs = require('fs');
|
|
8
14
|
const path = require('path');
|
|
9
15
|
|
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const path = require('path');
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
function getSharp() {
|
|
9
|
+
return process.env.NODE_ENV === 'test'
|
|
10
|
+
? require('../../tests/mocks/sharp')
|
|
11
|
+
: require('sharp');
|
|
12
|
+
}
|
|
11
13
|
|
|
12
14
|
const APPLE_SIZES = [57, 60, 72, 76, 114, 120, 144, 152, 180];
|
|
13
15
|
const ANDROID_SIZE = 192;
|
|
@@ -54,7 +56,7 @@ function registerCommand(program) {
|
|
|
54
56
|
const allSizes = [...new Set([...APPLE_SIZES, ANDROID_SIZE, ...FAVICON_SIZES, MS_TILE_SIZE])].sort((a, b) => a - b);
|
|
55
57
|
|
|
56
58
|
for (const size of allSizes) {
|
|
57
|
-
const buffer = await
|
|
59
|
+
const buffer = await getSharp()(sourcePath).resize(size, size).png().toBuffer();
|
|
58
60
|
|
|
59
61
|
if (APPLE_SIZES.includes(size)) {
|
|
60
62
|
const filename = `apple-icon-${size}x${size}.png`;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso CLI: MCP Command
|
|
3
|
+
* Runs the Webspresso Model Context Protocol (MCP) server over standard I/O for IDEs (Antigravity, Claude, Cursor)
|
|
4
|
+
* @module bin/commands/mcp
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Register MCP command in Commander CLI
|
|
13
|
+
* @param {import('commander').Command} program
|
|
14
|
+
*/
|
|
15
|
+
function registerCommand(program) {
|
|
16
|
+
program
|
|
17
|
+
.command('mcp')
|
|
18
|
+
.description('Start Model Context Protocol (MCP) server over Stdio for AI coding assistants')
|
|
19
|
+
.option('-d, --dir <path>', 'Project root directory', process.cwd())
|
|
20
|
+
.option('-c, --config <path>', 'Database config path (knexfile.js or webspresso.db.js)')
|
|
21
|
+
.option('-E, --env <environment>', 'Environment (development, production)', 'development')
|
|
22
|
+
.option('--read-only', 'Restrict tools to read-only queries (blocks mutations/inserts)', false)
|
|
23
|
+
.option('--no-services', 'Disable auto-bridging of services/ directory')
|
|
24
|
+
.option('--no-orm', 'Disable auto-bridging of ORM models and schema')
|
|
25
|
+
.option('--print-config', 'Print sample Claude Desktop / Cursor MCP configuration JSON snippet')
|
|
26
|
+
.action(async (options) => {
|
|
27
|
+
// 1. If print-config flag is passed, print snippet and exit cleanly
|
|
28
|
+
if (options.printConfig) {
|
|
29
|
+
const configSnippet = {
|
|
30
|
+
mcpServers: {
|
|
31
|
+
webspresso: {
|
|
32
|
+
command: 'npx',
|
|
33
|
+
args: ['webspresso', 'mcp'],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
console.log(JSON.stringify(configSnippet, null, 2));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const rootDir = path.resolve(process.cwd(), options.dir || '.');
|
|
42
|
+
const isReadOnly = Boolean(options.readOnly);
|
|
43
|
+
|
|
44
|
+
const {
|
|
45
|
+
McpServer,
|
|
46
|
+
createServiceTools,
|
|
47
|
+
createOrmTools,
|
|
48
|
+
createOrmResources,
|
|
49
|
+
createSystemResources,
|
|
50
|
+
createBuiltinPrompts,
|
|
51
|
+
startStdioTransport,
|
|
52
|
+
discoverMcpDirectory,
|
|
53
|
+
} = require('../../plugins/mcp');
|
|
54
|
+
const { createServiceRegistry } = require('../../src/services');
|
|
55
|
+
const { resolveDbConfigIfExists, createDbInstance } = require('../utils/db');
|
|
56
|
+
const { getAllModels } = require('../../core/orm/model');
|
|
57
|
+
|
|
58
|
+
// Create Server instance
|
|
59
|
+
const server = new McpServer({
|
|
60
|
+
name: 'webspresso-mcp',
|
|
61
|
+
version: require('../../package.json').version,
|
|
62
|
+
instructions: `Webspresso MCP server running in ${rootDir} (readOnly: ${isReadOnly}). Exposes business logic services and ORM models.`,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Register built-in prompts
|
|
66
|
+
const prompts = createBuiltinPrompts();
|
|
67
|
+
for (const p of prompts) {
|
|
68
|
+
server.registerPrompt(p);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let db = null;
|
|
72
|
+
let registry = null;
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
// 2. Resolve database if not disabled
|
|
76
|
+
if (options.orm !== false) {
|
|
77
|
+
const dbConfigResult = resolveDbConfigIfExists(options.config);
|
|
78
|
+
if (dbConfigResult) {
|
|
79
|
+
try {
|
|
80
|
+
db = await createDbInstance(dbConfigResult.config, options.env);
|
|
81
|
+
server.context.db = db;
|
|
82
|
+
|
|
83
|
+
const ormTools = createOrmTools({
|
|
84
|
+
db,
|
|
85
|
+
readOnly: isReadOnly,
|
|
86
|
+
});
|
|
87
|
+
server.registerTools(ormTools);
|
|
88
|
+
|
|
89
|
+
const { resources, templates } = createOrmResources({ db });
|
|
90
|
+
for (const r of resources) {
|
|
91
|
+
server.registerResource(r);
|
|
92
|
+
}
|
|
93
|
+
for (const t of templates) {
|
|
94
|
+
server.registerResourceTemplate(t);
|
|
95
|
+
}
|
|
96
|
+
} catch (dbErr) {
|
|
97
|
+
process.stderr.write(`[webspresso-mcp] DB initialization skipped: ${dbErr.message}\n`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 3. Resolve services if not disabled
|
|
103
|
+
if (options.services !== false) {
|
|
104
|
+
const servicesDir = path.join(rootDir, 'services');
|
|
105
|
+
if (fs.existsSync(servicesDir)) {
|
|
106
|
+
registry = createServiceRegistry({
|
|
107
|
+
servicesDir,
|
|
108
|
+
isDev: options.env !== 'production',
|
|
109
|
+
});
|
|
110
|
+
server.context.serviceRegistry = registry;
|
|
111
|
+
|
|
112
|
+
const serviceTools = createServiceTools({
|
|
113
|
+
registry,
|
|
114
|
+
readOnly: isReadOnly,
|
|
115
|
+
role: 'admin',
|
|
116
|
+
});
|
|
117
|
+
server.registerTools(serviceTools);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 4. Register system resources
|
|
122
|
+
const sysResources = createSystemResources({
|
|
123
|
+
serviceRegistry: registry,
|
|
124
|
+
db,
|
|
125
|
+
});
|
|
126
|
+
for (const r of sysResources) {
|
|
127
|
+
server.registerResource(r);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 5. Discover custom mcp/ components
|
|
131
|
+
discoverMcpDirectory(server, rootDir);
|
|
132
|
+
|
|
133
|
+
// 6. Start Stdio transport
|
|
134
|
+
const transport = startStdioTransport(server, {
|
|
135
|
+
redirectStderr: true,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
process.stderr.write(
|
|
139
|
+
`[webspresso-mcp] Server started (Stdio transport). Registered ${server.tools.size} tools, ${server.resources.size} resources, ${server.prompts.size} prompts.\n`
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// Handle process termination
|
|
143
|
+
const cleanup = async () => {
|
|
144
|
+
transport.close();
|
|
145
|
+
if (db && typeof db.destroy === 'function') {
|
|
146
|
+
await db.destroy();
|
|
147
|
+
}
|
|
148
|
+
process.exit(0);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
process.on('SIGINT', cleanup);
|
|
152
|
+
process.on('SIGTERM', cleanup);
|
|
153
|
+
} catch (err) {
|
|
154
|
+
process.stderr.write(`[webspresso-mcp] Fatal error: ${err.message}\n`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = {
|
|
161
|
+
registerCommand,
|
|
162
|
+
};
|
package/bin/commands/new.js
CHANGED
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
* Creates a new Webspresso project
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
let _inquirer;
|
|
7
|
+
const inquirer = {
|
|
8
|
+
prompt: (...args) => {
|
|
9
|
+
if (!_inquirer) _inquirer = require('inquirer');
|
|
10
|
+
return _inquirer.prompt(...args);
|
|
11
|
+
},
|
|
12
|
+
};
|
|
7
13
|
const fs = require('fs');
|
|
8
14
|
const path = require('path');
|
|
9
15
|
const { runInstallation, startDevServer } = require('../utils/project');
|
package/bin/commands/page.js
CHANGED
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
* Add a new page to the current project
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
let _inquirer;
|
|
7
|
+
const inquirer = {
|
|
8
|
+
prompt: (...args) => {
|
|
9
|
+
if (!_inquirer) _inquirer = require('inquirer');
|
|
10
|
+
return _inquirer.prompt(...args);
|
|
11
|
+
},
|
|
12
|
+
};
|
|
7
13
|
const fs = require('fs');
|
|
8
14
|
const path = require('path');
|
|
9
15
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* polar:migrate — write Polar billing migration to migrations/
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { loadDbConfig } = require('../utils/db');
|
|
10
|
+
const { generatePolarMigration } = require('../../plugins/polar/src/migration');
|
|
11
|
+
|
|
12
|
+
function registerCommand(program) {
|
|
13
|
+
program
|
|
14
|
+
.command('polar:migrate')
|
|
15
|
+
.description('Generate idempotent Polar billing migration for users table')
|
|
16
|
+
.option('-c, --config <path>', 'Path to database config file')
|
|
17
|
+
.option('-t, --table <name>', 'User table name', 'users')
|
|
18
|
+
.option('-o, --output <path>', 'Output migration file path (default: migrations/<timestamp>_polar_billing.js)')
|
|
19
|
+
.action(async (options) => {
|
|
20
|
+
const { config, path: configPath } = loadDbConfig(options.config);
|
|
21
|
+
console.log(`\n📦 Using config: ${configPath}\n`);
|
|
22
|
+
|
|
23
|
+
const migrationDir = config.migrations?.directory || './migrations';
|
|
24
|
+
if (!fs.existsSync(migrationDir)) {
|
|
25
|
+
fs.mkdirSync(migrationDir, { recursive: true });
|
|
26
|
+
console.log(`Created directory: ${migrationDir}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let filepath = options.output;
|
|
30
|
+
if (!filepath) {
|
|
31
|
+
const now = new Date();
|
|
32
|
+
const timestamp = [
|
|
33
|
+
now.getFullYear(),
|
|
34
|
+
String(now.getMonth() + 1).padStart(2, '0'),
|
|
35
|
+
String(now.getDate()).padStart(2, '0'),
|
|
36
|
+
String(now.getHours()).padStart(2, '0'),
|
|
37
|
+
String(now.getMinutes()).padStart(2, '0'),
|
|
38
|
+
String(now.getSeconds()).padStart(2, '0'),
|
|
39
|
+
].join('');
|
|
40
|
+
filepath = path.join(migrationDir, `${timestamp}_polar_billing.js`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const content = generatePolarMigration({ tableName: options.table });
|
|
44
|
+
fs.writeFileSync(filepath, content, 'utf8');
|
|
45
|
+
console.log(`✅ Polar migration written: ${filepath}`);
|
|
46
|
+
console.log('\nNext: webspresso db:migrate\n');
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { registerCommand };
|
package/bin/commands/seed.js
CHANGED
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
* Run database seeders to populate database with fake data
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
let _inquirer;
|
|
7
|
+
const inquirer = {
|
|
8
|
+
prompt: (...args) => {
|
|
9
|
+
if (!_inquirer) _inquirer = require('inquirer');
|
|
10
|
+
return _inquirer.prompt(...args);
|
|
11
|
+
},
|
|
12
|
+
};
|
|
7
13
|
const fs = require('fs');
|
|
8
14
|
const path = require('path');
|
|
9
15
|
const { execSync } = require('child_process');
|
package/bin/commands/service.js
CHANGED
|
@@ -5,8 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const path = require('path');
|
|
8
|
-
|
|
9
|
-
const
|
|
8
|
+
let _inquirer;
|
|
9
|
+
const inquirer = {
|
|
10
|
+
prompt: (...args) => {
|
|
11
|
+
if (!_inquirer) _inquirer = require('inquirer');
|
|
12
|
+
return _inquirer.prompt(...args);
|
|
13
|
+
},
|
|
14
|
+
};
|
|
10
15
|
const { resolveDbConfigIfExists, createDbInstance } = require('../utils/db');
|
|
11
16
|
|
|
12
17
|
/**
|
|
@@ -69,6 +74,7 @@ function registerCommand(program) {
|
|
|
69
74
|
? path.resolve(process.cwd(), options.dir)
|
|
70
75
|
: path.resolve(process.cwd(), 'services');
|
|
71
76
|
|
|
77
|
+
const { createServiceRegistry } = require('../../src/services');
|
|
72
78
|
const registry = createServiceRegistry({
|
|
73
79
|
servicesDir,
|
|
74
80
|
isDev: true,
|
package/bin/commands/skill.js
CHANGED
|
@@ -5,7 +5,13 @@
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const os = require('os');
|
|
8
|
-
|
|
8
|
+
let _inquirer;
|
|
9
|
+
const inquirer = {
|
|
10
|
+
prompt: (...args) => {
|
|
11
|
+
if (!_inquirer) _inquirer = require('inquirer');
|
|
12
|
+
return _inquirer.prompt(...args);
|
|
13
|
+
},
|
|
14
|
+
};
|
|
9
15
|
|
|
10
16
|
const NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$|^[a-z0-9]$/;
|
|
11
17
|
|
package/bin/webspresso.js
CHANGED
|
@@ -34,6 +34,8 @@ const { registerCommand: registerSkill } = require('./commands/skill');
|
|
|
34
34
|
const { registerCommand: registerUpgrade } = require('./commands/upgrade');
|
|
35
35
|
const { registerCommand: registerOrmMap } = require('./commands/orm-map');
|
|
36
36
|
const { registerCommand: registerService } = require('./commands/service');
|
|
37
|
+
const { registerCommand: registerMcp } = require('./commands/mcp');
|
|
38
|
+
const { registerCommand: registerPolarMigrate } = require('./commands/polar-migrate');
|
|
37
39
|
|
|
38
40
|
registerNew(program);
|
|
39
41
|
registerPage(program);
|
|
@@ -56,6 +58,8 @@ registerSkill(program);
|
|
|
56
58
|
registerUpgrade(program);
|
|
57
59
|
registerOrmMap(program);
|
|
58
60
|
registerService(program);
|
|
61
|
+
registerMcp(program);
|
|
62
|
+
registerPolarMigrate(program);
|
|
59
63
|
|
|
60
64
|
// Parse arguments
|
|
61
65
|
program.parse();
|
package/core/auth/tokens.js
CHANGED
|
@@ -69,6 +69,9 @@ function createKnexAuthTokensAdapter(db) {
|
|
|
69
69
|
if (type) q.where({ type });
|
|
70
70
|
await q.delete();
|
|
71
71
|
},
|
|
72
|
+
purgeExpired: async () => {
|
|
73
|
+
return knex('auth_tokens').where('expires_at', '<', new Date()).delete();
|
|
74
|
+
},
|
|
72
75
|
};
|
|
73
76
|
}
|
|
74
77
|
|
|
@@ -123,6 +126,18 @@ async function consumeAuthToken(authTokensAdapter, tokenHash) {
|
|
|
123
126
|
await authTokensAdapter.delete(tokenHash);
|
|
124
127
|
}
|
|
125
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Purge expired auth tokens
|
|
131
|
+
* @param {Object} authTokensAdapter
|
|
132
|
+
* @returns {Promise<number>} Number of deleted rows
|
|
133
|
+
*/
|
|
134
|
+
async function purgeExpiredAuthTokens(authTokensAdapter) {
|
|
135
|
+
if (authTokensAdapter && typeof authTokensAdapter.purgeExpired === 'function') {
|
|
136
|
+
return authTokensAdapter.purgeExpired();
|
|
137
|
+
}
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
126
141
|
module.exports = {
|
|
127
142
|
TOKEN_TYPES,
|
|
128
143
|
createAuthTokensTable,
|
|
@@ -131,5 +146,6 @@ module.exports = {
|
|
|
131
146
|
createAuthToken,
|
|
132
147
|
verifyAuthToken,
|
|
133
148
|
consumeAuthToken,
|
|
149
|
+
purgeExpiredAuthTokens,
|
|
134
150
|
hash,
|
|
135
151
|
};
|
package/core/compileSchema.js
CHANGED
|
@@ -3,13 +3,9 @@
|
|
|
3
3
|
* Compiles schema definitions from API files
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const z = require('
|
|
7
|
-
const { extendZ } = require('./orm/utils/nanoid');
|
|
6
|
+
const { z: zForApi } = require('./validation');
|
|
8
7
|
const schemaCache = require('../utils/schemaCache');
|
|
9
8
|
|
|
10
|
-
/** Zod plus `z.nanoid()` for API schemas (does not mutate the global `z`). */
|
|
11
|
-
const zForApi = extendZ(z);
|
|
12
|
-
|
|
13
9
|
/**
|
|
14
10
|
* Compile schema from an API module
|
|
15
11
|
* @param {string} filePath - Absolute file path to API module
|
|
@@ -10,6 +10,9 @@ const COMPRESSIBLE_TYPE_REGEX = /^(?:text\/[a-z0-9.+_-]+|application\/(?:javascr
|
|
|
10
10
|
// Regex patterns for already-compressed or non-compressible types
|
|
11
11
|
const INCOMPRESSIBLE_TYPE_REGEX = /^(?: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
|
+
const compressibleCache = new Map();
|
|
14
|
+
const MAX_MIME_CACHE_SIZE = 256;
|
|
15
|
+
|
|
13
16
|
/**
|
|
14
17
|
* Determine if a Content-Type is compressible
|
|
15
18
|
* @param {string|undefined|null} contentType - Raw Content-Type header (e.g. 'text/html; charset=utf-8')
|
|
@@ -20,30 +23,39 @@ function isCompressible(contentType) {
|
|
|
20
23
|
return false;
|
|
21
24
|
}
|
|
22
25
|
|
|
26
|
+
const cached = compressibleCache.get(contentType);
|
|
27
|
+
if (cached !== undefined) {
|
|
28
|
+
return cached;
|
|
29
|
+
}
|
|
30
|
+
|
|
23
31
|
// Extract base MIME type (strip parameters like ; charset=utf-8)
|
|
24
32
|
const semiIndex = contentType.indexOf(';');
|
|
25
33
|
const mime = (semiIndex !== -1 ? contentType.slice(0, semiIndex) : contentType).trim().toLowerCase();
|
|
26
34
|
|
|
27
35
|
if (!mime) {
|
|
36
|
+
if (compressibleCache.size < MAX_MIME_CACHE_SIZE) compressibleCache.set(contentType, false);
|
|
28
37
|
return false;
|
|
29
38
|
}
|
|
30
39
|
|
|
31
40
|
// Fast negative check
|
|
32
41
|
if (INCOMPRESSIBLE_TYPE_REGEX.test(mime)) {
|
|
42
|
+
if (compressibleCache.size < MAX_MIME_CACHE_SIZE) compressibleCache.set(contentType, false);
|
|
33
43
|
return false;
|
|
34
44
|
}
|
|
35
45
|
|
|
36
46
|
// Check compressible match
|
|
47
|
+
let result = false;
|
|
37
48
|
if (COMPRESSIBLE_TYPE_REGEX.test(mime)) {
|
|
38
|
-
|
|
49
|
+
result = true;
|
|
50
|
+
} else if (mime.startsWith('text/') || mime.endsWith('+json') || mime.endsWith('+xml') || mime.endsWith('+yaml')) {
|
|
51
|
+
result = true;
|
|
39
52
|
}
|
|
40
53
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return true;
|
|
54
|
+
if (compressibleCache.size < MAX_MIME_CACHE_SIZE) {
|
|
55
|
+
compressibleCache.set(contentType, result);
|
|
44
56
|
}
|
|
45
57
|
|
|
46
|
-
return
|
|
58
|
+
return result;
|
|
47
59
|
}
|
|
48
60
|
|
|
49
61
|
module.exports = {
|
|
@@ -34,7 +34,7 @@ function createCompressionStream(encoding, options = {}) {
|
|
|
34
34
|
// Brotli quality 4 is standard for dynamic HTTP compression (fast & high ratio)
|
|
35
35
|
brotliOpts.params[zlib.constants.BROTLI_PARAM_QUALITY] = typeof options.brotli?.quality === 'number'
|
|
36
36
|
? options.brotli.quality
|
|
37
|
-
: (options.
|
|
37
|
+
: (typeof options.brotliQuality === 'number' ? options.brotliQuality : 4);
|
|
38
38
|
}
|
|
39
39
|
return zlib.createBrotliCompress(brotliOpts);
|
|
40
40
|
}
|
|
@@ -262,6 +262,15 @@ function createCompressionMiddleware(options = {}) {
|
|
|
262
262
|
// Check if total length satisfies threshold and compressibility
|
|
263
263
|
if (shouldCompress(bufferedLength)) {
|
|
264
264
|
startCompression();
|
|
265
|
+
if (bufferedChunks.length === 1) {
|
|
266
|
+
if (typeof callback === 'function') {
|
|
267
|
+
compressStream.once('end', callback);
|
|
268
|
+
}
|
|
269
|
+
const singleBuf = bufferedChunks[0];
|
|
270
|
+
bufferedChunks = [];
|
|
271
|
+
compressStream.end(singleBuf);
|
|
272
|
+
return res;
|
|
273
|
+
}
|
|
265
274
|
for (const b of bufferedChunks) {
|
|
266
275
|
compressStream.write(b);
|
|
267
276
|
}
|
|
@@ -69,6 +69,9 @@ function parseAcceptEncoding(header) {
|
|
|
69
69
|
return results;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
const selectEncodingCache = new Map();
|
|
73
|
+
const MAX_ENCODING_CACHE_SIZE = 128;
|
|
74
|
+
|
|
72
75
|
/**
|
|
73
76
|
* Select the best compression encoding given client header and server supported list
|
|
74
77
|
* @param {string|undefined|null} acceptEncoding - The Accept-Encoding header value
|
|
@@ -76,16 +79,25 @@ function parseAcceptEncoding(header) {
|
|
|
76
79
|
* @returns {string|null} The selected encoding ('br', 'gzip', 'deflate', 'identity', or null)
|
|
77
80
|
*/
|
|
78
81
|
function selectEncoding(acceptEncoding, supportedEncodings) {
|
|
82
|
+
if (!acceptEncoding) {
|
|
83
|
+
return 'identity';
|
|
84
|
+
}
|
|
85
|
+
|
|
79
86
|
const supported = Array.isArray(supportedEncodings) && supportedEncodings.length > 0
|
|
80
87
|
? supportedEncodings.map(e => e.toLowerCase()).filter(e => e !== 'br' || supportsBrotli())
|
|
81
88
|
: getDefaultSupportedEncodings();
|
|
82
89
|
|
|
83
|
-
|
|
84
|
-
|
|
90
|
+
const cacheKey = `${acceptEncoding}|${supported.join(',')}`;
|
|
91
|
+
const cached = selectEncodingCache.get(cacheKey);
|
|
92
|
+
if (cached !== undefined) {
|
|
93
|
+
return cached;
|
|
85
94
|
}
|
|
86
95
|
|
|
87
96
|
const clientPreferences = parseAcceptEncoding(acceptEncoding);
|
|
88
97
|
if (clientPreferences.length === 0) {
|
|
98
|
+
if (selectEncodingCache.size < MAX_ENCODING_CACHE_SIZE) {
|
|
99
|
+
selectEncodingCache.set(cacheKey, 'identity');
|
|
100
|
+
}
|
|
89
101
|
return 'identity';
|
|
90
102
|
}
|
|
91
103
|
|
|
@@ -129,20 +141,20 @@ function selectEncoding(acceptEncoding, supportedEncodings) {
|
|
|
129
141
|
}
|
|
130
142
|
}
|
|
131
143
|
|
|
144
|
+
let result = null;
|
|
132
145
|
if (bestEncoding && bestQ > 0) {
|
|
133
|
-
|
|
146
|
+
result = bestEncoding;
|
|
147
|
+
} else if (prefMap.has('identity')) {
|
|
148
|
+
result = prefMap.get('identity') > 0 ? 'identity' : null;
|
|
149
|
+
} else if (wildcardQ !== null) {
|
|
150
|
+
result = wildcardQ > 0 ? 'identity' : null;
|
|
134
151
|
}
|
|
135
152
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (prefMap.has('identity')) {
|
|
139
|
-
return prefMap.get('identity') > 0 ? 'identity' : null;
|
|
140
|
-
}
|
|
141
|
-
if (wildcardQ !== null) {
|
|
142
|
-
return wildcardQ > 0 ? 'identity' : null;
|
|
153
|
+
if (selectEncodingCache.size < MAX_ENCODING_CACHE_SIZE) {
|
|
154
|
+
selectEncodingCache.set(cacheKey, result);
|
|
143
155
|
}
|
|
144
156
|
|
|
145
|
-
return
|
|
157
|
+
return result;
|
|
146
158
|
}
|
|
147
159
|
|
|
148
160
|
module.exports = {
|
package/core/kernel/app.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Minimal extensible application kernel (event bus, views, flows, plugins).
|
|
2
|
+
* Minimal extensible standalone application kernel (event bus, views, flows, plugins).
|
|
3
|
+
* Isolated experimental subsystem — not the primary Express SSR createApp.
|
|
3
4
|
* @module core/kernel/app
|
|
4
5
|
*/
|
|
5
6
|
|
package/core/kernel/index.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Standalone Experimental Application Kernel
|
|
3
|
+
* In-process event bus, simulated repository hooks, plugin shell, and minimal view resolver.
|
|
4
|
+
*
|
|
5
|
+
* NOTE: This is an isolated, experimental standalone subsystem. It does NOT integrate with
|
|
6
|
+
* or replace Webspresso's Express-based SSR createApp(), Knex ORM, or PluginManager.
|
|
3
7
|
* @module core/kernel
|
|
4
8
|
*/
|
|
5
9
|
|
|
@@ -12,10 +12,28 @@ const crypto = require('crypto');
|
|
|
12
12
|
function stableValue(v) {
|
|
13
13
|
if (v === null || v === undefined) return v;
|
|
14
14
|
if (typeof v !== 'object') return v;
|
|
15
|
-
if (Array.isArray(v))
|
|
16
|
-
|
|
15
|
+
if (Array.isArray(v)) {
|
|
16
|
+
const len = v.length;
|
|
17
|
+
if (len === 0) return v;
|
|
18
|
+
const out = new Array(len);
|
|
19
|
+
for (let i = 0; i < len; i++) {
|
|
20
|
+
out[i] = stableValue(v[i]);
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
const keys = Object.keys(v);
|
|
25
|
+
const keyLen = keys.length;
|
|
26
|
+
if (keyLen === 0) return v;
|
|
27
|
+
if (keyLen === 1) {
|
|
28
|
+
const k = keys[0];
|
|
29
|
+
return { [k]: stableValue(v[k]) };
|
|
30
|
+
}
|
|
31
|
+
keys.sort();
|
|
17
32
|
const out = {};
|
|
18
|
-
for (
|
|
33
|
+
for (let i = 0; i < keyLen; i++) {
|
|
34
|
+
const k = keys[i];
|
|
35
|
+
out[k] = stableValue(v[k]);
|
|
36
|
+
}
|
|
19
37
|
return out;
|
|
20
38
|
}
|
|
21
39
|
|
|
@@ -24,9 +42,9 @@ function stableValue(v) {
|
|
|
24
42
|
*/
|
|
25
43
|
function scopeFingerprint(scopeContext) {
|
|
26
44
|
return {
|
|
27
|
-
tenantId: scopeContext.tenantId,
|
|
28
|
-
withTrashed: !!scopeContext.withTrashed,
|
|
29
45
|
onlyTrashed: !!scopeContext.onlyTrashed,
|
|
46
|
+
tenantId: scopeContext.tenantId ?? null,
|
|
47
|
+
withTrashed: !!scopeContext.withTrashed,
|
|
30
48
|
};
|
|
31
49
|
}
|
|
32
50
|
|
package/core/orm/cache/layer.js
CHANGED
|
@@ -9,7 +9,7 @@ const { hashKey, serializeWheres } = require('./fingerprint');
|
|
|
9
9
|
* @param {*} knex
|
|
10
10
|
*/
|
|
11
11
|
function isTransactionKnex(knex) {
|
|
12
|
-
return !!(knex && knex.isTransaction);
|
|
12
|
+
return !!(knex && (knex.isTransaction || typeof knex.commit === 'function'));
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/**
|