webspresso 0.0.90 → 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.
Files changed (125) hide show
  1. package/README.md +98 -2509
  2. package/bin/commands/admin-password.js +8 -2
  3. package/bin/commands/api.js +7 -1
  4. package/bin/commands/favicon-generate.js +6 -4
  5. package/bin/commands/mcp.js +162 -0
  6. package/bin/commands/new.js +7 -1
  7. package/bin/commands/page.js +7 -1
  8. package/bin/commands/seed.js +7 -1
  9. package/bin/commands/service.js +199 -0
  10. package/bin/commands/skill.js +7 -1
  11. package/bin/webspresso.js +4 -0
  12. package/core/auth/middleware.js +9 -0
  13. package/core/compileSchema.js +1 -5
  14. package/core/compression/compressible.js +63 -0
  15. package/core/compression/compression-stream.js +59 -0
  16. package/core/compression/index.js +25 -0
  17. package/core/compression/middleware.js +310 -0
  18. package/core/compression/negotiate.js +165 -0
  19. package/core/errors/base.js +31 -0
  20. package/core/errors/domain.js +193 -0
  21. package/core/errors/http.js +133 -0
  22. package/core/errors/index.js +77 -0
  23. package/core/errors/middleware.js +162 -0
  24. package/core/errors/normalize.js +171 -0
  25. package/core/kernel/app.js +2 -1
  26. package/core/kernel/index.js +5 -1
  27. package/core/orm/cache/fingerprint.js +23 -5
  28. package/core/orm/complexity.js +120 -0
  29. package/core/orm/eager-loader.js +54 -9
  30. package/core/orm/events.js +64 -14
  31. package/core/orm/index.js +83 -12
  32. package/core/orm/model.js +13 -2
  33. package/core/orm/query-builder.js +30 -4
  34. package/core/orm/repository.js +57 -25
  35. package/core/orm/transaction.js +39 -5
  36. package/core/orm/utils.js +6 -3
  37. package/core/queue/adapters/database.js +317 -0
  38. package/core/queue/adapters/memory.js +176 -0
  39. package/core/queue/adapters/redis.js +357 -0
  40. package/core/queue/index.js +21 -0
  41. package/core/queue/job.js +103 -0
  42. package/core/queue/manager.js +413 -0
  43. package/core/realtime/adapters/redis.js +431 -0
  44. package/core/realtime/adapters/socket-io.js +203 -0
  45. package/core/realtime/adapters/sse.js +210 -0
  46. package/core/realtime/adapters/websocket.js +337 -0
  47. package/core/realtime/auth-manager.js +140 -0
  48. package/core/realtime/core.js +359 -0
  49. package/core/realtime/emitter.js +98 -0
  50. package/core/realtime/errors.js +71 -0
  51. package/core/realtime/index.js +79 -0
  52. package/core/realtime/reconnect-manager.js +118 -0
  53. package/core/realtime/registry.js +243 -0
  54. package/core/shutdown/http-adapter.js +134 -0
  55. package/core/shutdown/index.js +12 -0
  56. package/core/shutdown/shutdown-manager.js +312 -0
  57. package/core/ssr/index.js +13 -0
  58. package/core/ssr/stream.js +174 -0
  59. package/core/validation/index.js +70 -0
  60. package/index.d.ts +924 -53
  61. package/index.js +110 -1
  62. package/package.json +30 -2
  63. package/plugins/admin-panel/api.js +37 -4
  64. package/plugins/admin-panel/app.js +57 -17
  65. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +10 -2
  66. package/plugins/admin-panel/client/parts/04-field-renderers.js +102 -58
  67. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +51 -27
  68. package/plugins/admin-panel/client/parts/07-model-list.js +9 -4
  69. package/plugins/admin-panel/client/parts/08-record-list.js +37 -26
  70. package/plugins/admin-panel/client/parts/09-record-form.js +168 -19
  71. package/plugins/admin-panel/core/admin-module.js +2 -2
  72. package/plugins/admin-panel/core/api-extensions.js +149 -0
  73. package/plugins/admin-panel/field-renderers/rich-text.js +3 -2
  74. package/plugins/admin-panel/index.js +155 -23
  75. package/plugins/admin-panel/modules/bulk-actions.js +9 -2
  76. package/plugins/admin-panel/modules/custom-pages.js +144 -45
  77. package/plugins/admin-panel/modules/dashboard.js +8 -1
  78. package/plugins/admin-panel/modules/menu.js +85 -0
  79. package/plugins/admin-panel/modules/user-management.js +5 -2
  80. package/plugins/admin-panel/vendor/mithril.min.js +1 -0
  81. package/plugins/admin-panel/vendor/quill.min.js +7 -0
  82. package/plugins/admin-panel/vendor/quill.snow.min.css +945 -0
  83. package/plugins/admin-panel/vendor/tailwind.min.js +65 -0
  84. package/plugins/basic-auth/index.js +293 -0
  85. package/plugins/data-exchange/export-xlsx.js +28 -2
  86. package/plugins/data-exchange/import.js +55 -16
  87. package/plugins/data-exchange/index.js +19 -0
  88. package/plugins/data-exchange/parse-table.js +14 -5
  89. package/plugins/data-exchange/record-selection.js +2 -0
  90. package/plugins/email/index.js +14 -1
  91. package/plugins/index.js +33 -0
  92. package/plugins/mcp/auth.js +104 -0
  93. package/plugins/mcp/discovery.js +88 -0
  94. package/plugins/mcp/index.js +208 -0
  95. package/plugins/mcp/prompts.js +149 -0
  96. package/plugins/mcp/providers/orm.js +325 -0
  97. package/plugins/mcp/providers/services.js +190 -0
  98. package/plugins/mcp/providers/system.js +87 -0
  99. package/plugins/mcp/server.js +472 -0
  100. package/plugins/mcp/transports/sse.js +139 -0
  101. package/plugins/mcp/transports/stdio.js +108 -0
  102. package/plugins/queue/index.js +108 -0
  103. package/plugins/realtime/index.js +150 -0
  104. package/plugins/recaptcha/index.js +11 -0
  105. package/plugins/redirect/index.js +9 -1
  106. package/plugins/rest-resources/index.js +280 -11
  107. package/plugins/upload/index.js +16 -0
  108. package/src/app-context.js +46 -6
  109. package/src/file-router.js +366 -130
  110. package/src/helpers.js +20 -2
  111. package/src/njk-frontmatter.js +24 -14
  112. package/src/plugin-manager.js +124 -4
  113. package/src/server.js +332 -54
  114. package/src/services/builtins/auth.js +273 -0
  115. package/src/services/builtins/exchange.js +229 -0
  116. package/src/services/builtins/mail.js +134 -0
  117. package/src/services/builtins/media.js +196 -0
  118. package/src/services/builtins/system.js +221 -0
  119. package/src/services/discovery.js +141 -0
  120. package/src/services/executor.js +292 -0
  121. package/src/services/index.js +70 -0
  122. package/src/services/memoize.js +256 -0
  123. package/src/services/registry.js +290 -0
  124. package/src/services/validator.js +230 -0
  125. package/templates/skills/webspresso-usage/REFERENCE-framework.md +6 -4
@@ -0,0 +1,312 @@
1
+ /**
2
+ * ShutdownManager
3
+ * Framework-independent lifecycle coordinator for graceful shutdown, forced connection termination,
4
+ * application shutdown hooks, and plugin cleanup disposers.
5
+ * @module core/shutdown/shutdown-manager
6
+ */
7
+
8
+ class ShutdownManager {
9
+ /**
10
+ * @param {Object} [options]
11
+ * @param {boolean} [options.enabled=true] - Enable shutdown management
12
+ * @param {'graceful'|'force'} [options.mode='graceful'] - Shutdown mode ('graceful' or 'force')
13
+ * @param {number} [options.timeout=10000] - Graceful timeout in milliseconds before forcing close
14
+ * @param {string[]} [options.signals=['SIGINT', 'SIGTERM']] - Process signals to listen for
15
+ * @param {Object|null} [options.logger=console] - Logger instance (or null to disable logs)
16
+ * @param {boolean} [options.exitOnSignal=true] - Whether to call process.exit when triggered by a process signal
17
+ */
18
+ constructor(options = {}) {
19
+ this.options = {
20
+ enabled: options.enabled !== false,
21
+ mode: options.mode === 'force' ? 'force' : 'graceful',
22
+ timeout: typeof options.timeout === 'number' && options.timeout >= 0 ? options.timeout : 10_000,
23
+ signals: Array.isArray(options.signals) ? options.signals : ['SIGINT', 'SIGTERM'],
24
+ logger: options.logger === null ? null : (options.logger || console),
25
+ exitOnSignal: options.exitOnSignal !== false,
26
+ };
27
+
28
+ this.isShuttingDown = false;
29
+ this.isClosed = false;
30
+
31
+ /** @type {Array<Function>} */
32
+ this._hooks = [];
33
+
34
+ /** @type {Array<{ name: string, fn: Function }>} */
35
+ this._disposers = [];
36
+
37
+ /** @type {Array<{ close: () => Promise<void>, forceClose?: () => void, closeIdleConnections?: () => void }>} */
38
+ this._adapters = [];
39
+
40
+ /** @type {Promise<void>|null} */
41
+ this._shutdownPromise = null;
42
+
43
+ /** @type {Map<string, Function>} */
44
+ this._signalListeners = new Map();
45
+ }
46
+
47
+ get mode() {
48
+ return this.options.mode;
49
+ }
50
+
51
+ set mode(value) {
52
+ this.options.mode = value === 'force' ? 'force' : 'graceful';
53
+ }
54
+
55
+ get timeout() {
56
+ return this.options.timeout;
57
+ }
58
+
59
+ set timeout(value) {
60
+ if (typeof value === 'number' && value >= 0) {
61
+ this.options.timeout = value;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Safe logger helper
67
+ * @param {'info'|'warn'|'error'|'debug'} level
68
+ * @param {string} message
69
+ * @param {unknown} [extra]
70
+ */
71
+ _log(level, message, extra) {
72
+ const logger = this.options.logger;
73
+ if (!logger) return;
74
+
75
+ const formatted = `[webspresso] ${message}`;
76
+ if (typeof logger[level] === 'function') {
77
+ if (extra !== undefined) {
78
+ logger[level](formatted, extra);
79
+ } else {
80
+ logger[level](formatted);
81
+ }
82
+ } else if (typeof logger.log === 'function') {
83
+ if (extra !== undefined) {
84
+ logger.log(formatted, extra);
85
+ } else {
86
+ logger.log(formatted);
87
+ }
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Register an application shutdown hook
93
+ * @param {Function} fn - Sync or async callback
94
+ * @returns {this}
95
+ */
96
+ onShutdown(fn) {
97
+ if (typeof fn === 'function') {
98
+ this._hooks.push(fn);
99
+ }
100
+ return this;
101
+ }
102
+
103
+ /**
104
+ * Register a plugin disposer / cleanup function
105
+ * @param {string|Function} nameOrFn - Plugin name or disposer function
106
+ * @param {Function} [maybeFn] - Disposer function if name is provided
107
+ * @returns {this}
108
+ */
109
+ registerDisposer(nameOrFn, maybeFn) {
110
+ let name = 'plugin';
111
+ let fn = nameOrFn;
112
+
113
+ if (typeof nameOrFn === 'string' && typeof maybeFn === 'function') {
114
+ name = nameOrFn;
115
+ fn = maybeFn;
116
+ }
117
+
118
+ if (typeof fn === 'function') {
119
+ this._disposers.push({ name, fn });
120
+ }
121
+ return this;
122
+ }
123
+
124
+ /**
125
+ * Register a server adapter (e.g. NodeHttpAdapter)
126
+ * @param {{ close: () => Promise<void>, forceClose?: () => void, closeIdleConnections?: () => void }} adapter
127
+ * @returns {this}
128
+ */
129
+ registerAdapter(adapter) {
130
+ if (adapter && typeof adapter.close === 'function') {
131
+ this._adapters.push(adapter);
132
+ }
133
+ return this;
134
+ }
135
+
136
+ /**
137
+ * Attach signal listeners (SIGINT, SIGTERM) to the Node.js process
138
+ * @returns {this}
139
+ */
140
+ enableShutdownHooks() {
141
+ if (!this.options.enabled || typeof process === 'undefined' || typeof process.on !== 'function') {
142
+ return this;
143
+ }
144
+
145
+ for (const signal of this.options.signals) {
146
+ if (!this._signalListeners.has(signal)) {
147
+ const listener = async () => {
148
+ this._log('info', `${signal} received`);
149
+ try {
150
+ await this.close(signal);
151
+ if (this.options.exitOnSignal) {
152
+ process.exit(0);
153
+ }
154
+ } catch (err) {
155
+ this._log('error', `Shutdown failed following ${signal}:`, err);
156
+ if (this.options.exitOnSignal) {
157
+ process.exit(1);
158
+ }
159
+ }
160
+ };
161
+
162
+ this._signalListeners.set(signal, listener);
163
+ process.on(signal, listener);
164
+ }
165
+ }
166
+
167
+ return this;
168
+ }
169
+
170
+ /**
171
+ * Detach signal listeners from process
172
+ * @returns {this}
173
+ */
174
+ disableShutdownHooks() {
175
+ if (typeof process === 'undefined' || typeof process.removeListener !== 'function') {
176
+ return this;
177
+ }
178
+
179
+ for (const [signal, listener] of this._signalListeners.entries()) {
180
+ process.removeListener(signal, listener);
181
+ }
182
+ this._signalListeners.clear();
183
+ return this;
184
+ }
185
+
186
+ /**
187
+ * Execute the shutdown lifecycle pipeline.
188
+ * Idempotent: safe to call multiple times concurrently or sequentially.
189
+ * Note: manual calls to `close()` never call `process.exit()`.
190
+ * @param {string} [reason='manual']
191
+ * @returns {Promise<void>}
192
+ */
193
+ async close(reason = 'manual') {
194
+ if (this.isClosed) {
195
+ return;
196
+ }
197
+
198
+ if (this._shutdownPromise) {
199
+ return this._shutdownPromise;
200
+ }
201
+
202
+ this._shutdownPromise = (async () => {
203
+ this.isShuttingDown = true;
204
+ const isForce = this.options.mode === 'force';
205
+ const modeLabel = isForce ? 'force' : 'graceful';
206
+
207
+ this._log('info', `Starting ${modeLabel} shutdown (reason: ${reason})`);
208
+
209
+ // 1. Close HTTP server adapters
210
+ if (this._adapters.length > 0) {
211
+ this._log('info', 'Closing HTTP server');
212
+
213
+ for (const adapter of this._adapters) {
214
+ if (isForce) {
215
+ // Force mode: immediately close listener & force terminate connections
216
+ try {
217
+ if (typeof adapter.forceClose === 'function') {
218
+ adapter.forceClose();
219
+ }
220
+ await adapter.close();
221
+ } catch (err) {
222
+ this._log('error', 'Error force closing adapter:', err);
223
+ }
224
+ } else {
225
+ // Graceful mode: close idle connections, close server, and force close on timeout
226
+ try {
227
+ if (typeof adapter.closeIdleConnections === 'function') {
228
+ adapter.closeIdleConnections();
229
+ }
230
+ } catch (e) {}
231
+
232
+ let timer = null;
233
+ let timedOut = false;
234
+
235
+ const closePromise = adapter.close();
236
+
237
+ if (this.options.timeout > 0) {
238
+ const timeoutPromise = new Promise((resolve) => {
239
+ timer = setTimeout(() => {
240
+ timedOut = true;
241
+ this._log('warn', `Graceful shutdown timed out after ${this.options.timeout}ms`);
242
+ this._log('info', 'Force closing active connections');
243
+ try {
244
+ if (typeof adapter.forceClose === 'function') {
245
+ adapter.forceClose();
246
+ }
247
+ } catch (err) {
248
+ this._log('error', 'Error force closing connections after timeout:', err);
249
+ }
250
+ resolve();
251
+ }, this.options.timeout);
252
+
253
+ if (typeof timer.unref === 'function') {
254
+ timer.unref();
255
+ }
256
+ });
257
+
258
+ await Promise.race([closePromise, timeoutPromise]);
259
+ } else {
260
+ await closePromise;
261
+ }
262
+
263
+ if (timer) {
264
+ clearTimeout(timer);
265
+ }
266
+
267
+ // Ensure closePromise has resolved
268
+ await closePromise.catch(() => {});
269
+ }
270
+ }
271
+ }
272
+
273
+ // 2. Run application shutdown hooks (app.onShutdown)
274
+ if (this._hooks.length > 0) {
275
+ this._log('info', `Running ${this._hooks.length} shutdown hooks`);
276
+ for (const hook of this._hooks) {
277
+ try {
278
+ await hook();
279
+ } catch (err) {
280
+ this._log('error', 'Error in shutdown hook:', err);
281
+ }
282
+ }
283
+ }
284
+
285
+ // 3. Run plugin disposers in reverse registration order
286
+ if (this._disposers.length > 0) {
287
+ this._log('info', `Disposing ${this._disposers.length} plugins`);
288
+ // Reverse array to tear down dependencies in reverse order
289
+ const reverseDisposers = this._disposers.slice().reverse();
290
+ for (const { name, fn } of reverseDisposers) {
291
+ try {
292
+ await fn();
293
+ } catch (err) {
294
+ this._log('error', `Error in plugin disposer "${name}":`, err);
295
+ }
296
+ }
297
+ }
298
+
299
+ // 4. Detach signal hooks from process
300
+ this.disableShutdownHooks();
301
+
302
+ this.isClosed = true;
303
+ this._log('info', 'Shutdown completed');
304
+ })();
305
+
306
+ return this._shutdownPromise;
307
+ }
308
+ }
309
+
310
+ module.exports = {
311
+ ShutdownManager,
312
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Webspresso SSR Core Module
3
+ * @module core/ssr
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const { createHtmlStream, renderStream } = require('./stream');
9
+
10
+ module.exports = {
11
+ createHtmlStream,
12
+ renderStream,
13
+ };
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Webspresso SSR Streaming & Chunked Transfer Engine
3
+ * @module core/ssr/stream
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const { Readable } = require('stream');
9
+
10
+ /**
11
+ * Creates a readable HTML stream for Nunjucks templates supporting deferred data slots
12
+ * @param {Object} options
13
+ * @param {import('nunjucks').Environment} options.env - Nunjucks environment
14
+ * @param {string} options.templatePath - Template name or path
15
+ * @param {Object} [options.context={}] - Render context
16
+ * @param {Object} [options.defer] - Map of deferred promises { [slotName]: Promise<any> | { promise: Promise<any>, template?: string } }
17
+ * @returns {Readable}
18
+ */
19
+ function createHtmlStream(options = {}) {
20
+ const { env, templatePath, context = {}, defer = {} } = options;
21
+
22
+ if (!env) {
23
+ throw new Error('createHtmlStream requires a valid Nunjucks environment in options.env');
24
+ }
25
+
26
+ const stream = new Readable({
27
+ read() {}, // No-op, data is pushed asynchronously
28
+ });
29
+
30
+ (async () => {
31
+ try {
32
+ const deferredKeys = Object.keys(defer);
33
+
34
+ if (deferredKeys.length === 0) {
35
+ // Standard template streaming (render and stream)
36
+ const html = env.render(templatePath, context);
37
+ stream.push(html);
38
+ stream.push(null);
39
+ return;
40
+ }
41
+
42
+ // Initial shell rendering with deferred slot placeholders
43
+ const shellContext = {
44
+ ...context,
45
+ __streaming: true,
46
+ };
47
+
48
+ for (const key of deferredKeys) {
49
+ shellContext[key] = null;
50
+ }
51
+
52
+ let shellHtml = env.render(templatePath, shellContext);
53
+
54
+ // Check if template contains streaming slot markers, or split at first slot
55
+ // We push the initial shell chunk immediately to start browser TTFB & CSS download
56
+ stream.push(shellHtml);
57
+
58
+ // Resolve deferred promises in parallel
59
+ const resolutionPromises = deferredKeys.map(async (key) => {
60
+ const deferredItem = defer[key];
61
+ const promise = deferredItem && typeof deferredItem.then === 'function' ? deferredItem : deferredItem.promise;
62
+ const slotTemplate = deferredItem.template || null;
63
+
64
+ try {
65
+ const resolvedData = await promise;
66
+ let slotHtml = '';
67
+ if (slotTemplate) {
68
+ slotHtml = env.render(slotTemplate, { ...context, [key]: resolvedData });
69
+ } else {
70
+ slotHtml = typeof resolvedData === 'string' ? resolvedData : JSON.stringify(resolvedData);
71
+ }
72
+
73
+ // Push an out-of-order streaming chunk with inline replacement script or comment
74
+ const slotChunk = `
75
+ <template data-stream-slot="${key}">${slotHtml}</template>
76
+ <script>
77
+ (function(){
78
+ var t = document.querySelector('template[data-stream-slot="${key}"]');
79
+ var target = document.querySelector('[data-stream-target="${key}"]') || document.getElementById('stream-target-${key}');
80
+ if (t && target) {
81
+ target.innerHTML = '';
82
+ target.appendChild(t.content.cloneNode(true));
83
+ }
84
+ })();
85
+ </script>`;
86
+ stream.push(slotChunk);
87
+ } catch (slotErr) {
88
+ const errorChunk = `
89
+ <template data-stream-slot="${key}">
90
+ <!-- Slot Error (${key}): ${slotErr.message} -->
91
+ </template>`;
92
+ stream.push(errorChunk);
93
+ }
94
+ });
95
+
96
+ await Promise.all(resolutionPromises);
97
+ stream.push(null); // End of stream
98
+ } catch (err) {
99
+ stream.destroy(err);
100
+ }
101
+ })();
102
+
103
+ return stream;
104
+ }
105
+
106
+ /**
107
+ * Render template directly to Express response as a chunked stream
108
+ * @param {import('express').Response} res - Express response object
109
+ * @param {string} templatePath - Template name
110
+ * @param {Object} [context={}] - Context data
111
+ * @param {Object} [options] - Additional options
112
+ * @param {import('nunjucks').Environment} options.env - Nunjucks environment
113
+ * @param {Object} [options.defer] - Deferred slot promises
114
+ * @param {number} [options.status=200] - HTTP status code
115
+ * @param {boolean} [options.flushImmediately=true] - Flush headers immediately
116
+ * @returns {Promise<void>}
117
+ */
118
+ function renderStream(res, templatePath, context = {}, options = {}) {
119
+ return new Promise((resolve, reject) => {
120
+ const {
121
+ env,
122
+ defer = {},
123
+ status = 200,
124
+ flushImmediately = true,
125
+ } = options;
126
+
127
+ if (!env) {
128
+ return reject(new Error('renderStream requires a valid Nunjucks environment in options.env'));
129
+ }
130
+
131
+ if (res.headersSent) {
132
+ return reject(new Error('Cannot render stream: headers already sent'));
133
+ }
134
+
135
+ res.status(status);
136
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
137
+ res.setHeader('Transfer-Encoding', 'chunked');
138
+
139
+ if (flushImmediately && typeof res.flushHeaders === 'function') {
140
+ res.flushHeaders();
141
+ }
142
+
143
+ const stream = createHtmlStream({
144
+ env,
145
+ templatePath,
146
+ context,
147
+ defer,
148
+ });
149
+
150
+ stream.on('data', (chunk) => {
151
+ res.write(chunk);
152
+ });
153
+
154
+ stream.on('end', () => {
155
+ res.end();
156
+ resolve();
157
+ });
158
+
159
+ stream.on('error', (err) => {
160
+ if (!res.headersSent) {
161
+ res.status(500).send('Streaming Render Error');
162
+ } else {
163
+ res.write(`<!-- Streaming Error: ${err.message} -->`);
164
+ res.end();
165
+ }
166
+ reject(err);
167
+ });
168
+ });
169
+ }
170
+
171
+ module.exports = {
172
+ createHtmlStream,
173
+ renderStream,
174
+ };
@@ -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
+ };