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,118 @@
1
+ /**
2
+ * Realtime Reconnection & Exponential Backoff Manager
3
+ * @module core/realtime/reconnect-manager
4
+ */
5
+
6
+ class ReconnectManager {
7
+ /**
8
+ * @param {Object} [options]
9
+ * @param {boolean} [options.enabled=true] - Whether automatic reconnection is enabled
10
+ * @param {number} [options.maxAttempts=10] - Maximum reconnection attempts before giving up
11
+ * @param {number} [options.baseDelay=500] - Initial reconnection delay in ms
12
+ * @param {number} [options.maxDelay=30000] - Maximum reconnection delay in ms
13
+ * @param {number} [options.factor=2] - Exponential growth factor
14
+ * @param {boolean} [options.jitter=true] - Whether to apply randomized jitter
15
+ */
16
+ constructor(options = {}) {
17
+ this.enabled = options.enabled !== false;
18
+ this.maxAttempts = options.maxAttempts ?? 10;
19
+ this.baseDelay = options.baseDelay ?? 500;
20
+ this.maxDelay = options.maxDelay ?? 30000;
21
+ this.factor = options.factor ?? 2;
22
+ this.jitter = options.jitter !== false;
23
+
24
+ this.attempts = 0;
25
+ /** @type {ReturnType<typeof setTimeout> | null} */
26
+ this._timer = null;
27
+ this._isReconnecting = false;
28
+ this._isAborted = false;
29
+ }
30
+
31
+ calculateDelay(attempt) {
32
+ const rawDelay = this.baseDelay * Math.pow(this.factor, attempt);
33
+ const capped = Math.min(rawDelay, this.maxDelay);
34
+
35
+ if (!this.jitter) {
36
+ return Math.round(capped);
37
+ }
38
+
39
+ const randomJitter = capped * 0.2 * (Math.random() * 2 - 1);
40
+ return Math.max(0, Math.round(capped + randomJitter));
41
+ }
42
+
43
+ canReconnect() {
44
+ if (!this.enabled || this._isAborted) return false;
45
+ if (this.maxAttempts > 0 && this.attempts >= this.maxAttempts) {
46
+ return false;
47
+ }
48
+ return true;
49
+ }
50
+
51
+ isReconnecting() {
52
+ return this._isReconnecting || this._timer !== null;
53
+ }
54
+
55
+ schedule(reconnectFn, onAttempt) {
56
+ if (!this.canReconnect()) {
57
+ return false;
58
+ }
59
+
60
+ if (this.isReconnecting()) {
61
+ return false;
62
+ }
63
+
64
+ const currentAttempt = this.attempts + 1;
65
+ const delay = this.calculateDelay(this.attempts);
66
+ this.attempts = currentAttempt;
67
+
68
+ if (typeof onAttempt === 'function') {
69
+ try {
70
+ onAttempt({ attempt: currentAttempt, delay, maxAttempts: this.maxAttempts });
71
+ } catch (e) {
72
+ console.error('[realtime] Error in onAttempt callback:', e);
73
+ }
74
+ }
75
+
76
+ this._timer = setTimeout(async () => {
77
+ this._timer = null;
78
+ if (this._isAborted) return;
79
+
80
+ this._isReconnecting = true;
81
+ try {
82
+ await reconnectFn();
83
+ this.reset();
84
+ } catch (err) {
85
+ this._isReconnecting = false;
86
+ if (this._isAborted) return;
87
+
88
+ if (this.canReconnect()) {
89
+ this.schedule(reconnectFn, onAttempt);
90
+ }
91
+ } finally {
92
+ this._isReconnecting = false;
93
+ }
94
+ }, delay);
95
+
96
+ if (this._timer && typeof this._timer.unref === 'function') {
97
+ this._timer.unref();
98
+ }
99
+
100
+ return true;
101
+ }
102
+
103
+ reset() {
104
+ if (this._timer) {
105
+ clearTimeout(this._timer);
106
+ this._timer = null;
107
+ }
108
+ this.attempts = 0;
109
+ this._isReconnecting = false;
110
+ }
111
+
112
+ cancel() {
113
+ this._isAborted = true;
114
+ this.reset();
115
+ }
116
+ }
117
+
118
+ module.exports = { ReconnectManager };
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Realtime Subscription Registry & Subscription Handle
3
+ * @module core/realtime/registry
4
+ */
5
+
6
+ const { RealtimeError } = require('./errors');
7
+
8
+ /**
9
+ * Generate a deterministic composite key for a subscription from identifier and params
10
+ *
11
+ * @param {string} identifier
12
+ * @param {Object} [params={}]
13
+ * @returns {string}
14
+ */
15
+ function createSubscriptionKey(identifier, params = {}) {
16
+ if (!identifier || typeof identifier !== 'string') {
17
+ throw new RealtimeError('INVALID_ARGUMENT', 'Subscription identifier must be a non-empty string');
18
+ }
19
+
20
+ let serializedParams = '';
21
+ if (params && typeof params === 'object' && Object.keys(params).length > 0) {
22
+ const sortedKeys = Object.keys(params).sort();
23
+ const sortedObj = {};
24
+ for (const key of sortedKeys) {
25
+ sortedObj[key] = params[key];
26
+ }
27
+ serializedParams = JSON.stringify(sortedObj);
28
+ }
29
+
30
+ return `${identifier}::${serializedParams}`;
31
+ }
32
+
33
+ /**
34
+ * Realtime Subscription Handle
35
+ */
36
+ class RealtimeSubscription {
37
+ /**
38
+ * @param {Object} options
39
+ * @param {string} options.key - Unique subscription key
40
+ * @param {string} options.identifier - Topic/Channel identifier (e.g. "project:123")
41
+ * @param {Object} [options.params={}] - Parameters passed to subscription
42
+ * @param {Object} [options.callbacks={}] - Callbacks { connected, disconnected, rejected, received }
43
+ * @param {Function} options.onUnsubscribe - Internal unsubscribe callback
44
+ * @param {Function} options.onSend - Internal send handler
45
+ * @param {Function} options.onPerform - Internal perform handler
46
+ */
47
+ constructor({ key, identifier, params = {}, callbacks = {}, onUnsubscribe, onSend, onPerform }) {
48
+ this.key = key;
49
+ this.identifier = identifier;
50
+ this.params = params;
51
+ this.callbacks = {
52
+ connected: typeof callbacks.connected === 'function' ? callbacks.connected : null,
53
+ disconnected: typeof callbacks.disconnected === 'function' ? callbacks.disconnected : null,
54
+ rejected: typeof callbacks.rejected === 'function' ? callbacks.rejected : null,
55
+ received: typeof callbacks.received === 'function' ? callbacks.received : null,
56
+ };
57
+ this._onUnsubscribe = onUnsubscribe;
58
+ this._onSend = onSend;
59
+ this._onPerform = onPerform;
60
+ this._isUnsubscribed = false;
61
+ this._adapterSub = null;
62
+ }
63
+
64
+ /**
65
+ * Send data to this subscription
66
+ * @param {unknown} data
67
+ */
68
+ send(data) {
69
+ if (this._isUnsubscribed) {
70
+ throw new RealtimeError('SUBSCRIPTION_REJECTED', 'Cannot send data on an unsubscribed channel');
71
+ }
72
+ if (typeof this._onSend === 'function') {
73
+ this._onSend(this, data);
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Perform an action on this subscription
79
+ * @param {string} action
80
+ * @param {unknown} [data]
81
+ */
82
+ perform(action, data) {
83
+ if (this._isUnsubscribed) {
84
+ throw new RealtimeError('SUBSCRIPTION_REJECTED', 'Cannot perform action on an unsubscribed channel');
85
+ }
86
+ if (typeof this._onPerform === 'function') {
87
+ this._onPerform(this, action, data);
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Unsubscribe from this channel
93
+ */
94
+ unsubscribe() {
95
+ if (this._isUnsubscribed) return;
96
+ this._isUnsubscribed = true;
97
+ if (typeof this._onUnsubscribe === 'function') {
98
+ this._onUnsubscribe(this);
99
+ }
100
+ }
101
+
102
+ notifyReceived(data) {
103
+ if (this._isUnsubscribed) return;
104
+ if (this.callbacks.received) {
105
+ try {
106
+ this.callbacks.received(data);
107
+ } catch (err) {
108
+ console.error(`[realtime] Error in received callback for "${this.identifier}":`, err);
109
+ }
110
+ }
111
+ }
112
+
113
+ notifyConnected() {
114
+ if (this._isUnsubscribed) return;
115
+ if (this.callbacks.connected) {
116
+ try {
117
+ this.callbacks.connected();
118
+ } catch (err) {
119
+ console.error(`[realtime] Error in connected callback for "${this.identifier}":`, err);
120
+ }
121
+ }
122
+ }
123
+
124
+ notifyDisconnected() {
125
+ if (this._isUnsubscribed) return;
126
+ if (this.callbacks.disconnected) {
127
+ try {
128
+ this.callbacks.disconnected();
129
+ } catch (err) {
130
+ console.error(`[realtime] Error in disconnected callback for "${this.identifier}":`, err);
131
+ }
132
+ }
133
+ }
134
+
135
+ notifyRejected(error) {
136
+ if (this._isUnsubscribed) return;
137
+ if (this.callbacks.rejected) {
138
+ try {
139
+ this.callbacks.rejected(error);
140
+ } catch (err) {
141
+ console.error(`[realtime] Error in rejected callback for "${this.identifier}":`, err);
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Subscription Registry
149
+ */
150
+ class SubscriptionRegistry {
151
+ constructor() {
152
+ /** @type {Map<string, RealtimeSubscription>} */
153
+ this.subscriptions = new Map();
154
+ }
155
+
156
+ has(key) {
157
+ return this.subscriptions.has(key);
158
+ }
159
+
160
+ get(key) {
161
+ return this.subscriptions.get(key);
162
+ }
163
+
164
+ register({ identifier, params = {}, callbacks = {}, onSend, onPerform, onAdapterSubscribe, onAdapterUnsubscribe }) {
165
+ const key = createSubscriptionKey(identifier, params);
166
+
167
+ // If already registered and active, return existing subscription
168
+ if (this.subscriptions.has(key)) {
169
+ const existing = this.subscriptions.get(key);
170
+ if (!existing._isUnsubscribed) {
171
+ if (callbacks.received) existing.callbacks.received = callbacks.received;
172
+ if (callbacks.connected) existing.callbacks.connected = callbacks.connected;
173
+ if (callbacks.disconnected) existing.callbacks.disconnected = callbacks.disconnected;
174
+ if (callbacks.rejected) existing.callbacks.rejected = callbacks.rejected;
175
+ return existing;
176
+ }
177
+ }
178
+
179
+ const subscription = new RealtimeSubscription({
180
+ key,
181
+ identifier,
182
+ params,
183
+ callbacks,
184
+ onUnsubscribe: (sub) => {
185
+ this.subscriptions.delete(key);
186
+ if (typeof onAdapterUnsubscribe === 'function') {
187
+ onAdapterUnsubscribe(sub);
188
+ }
189
+ },
190
+ onSend,
191
+ onPerform,
192
+ });
193
+
194
+ this.subscriptions.set(key, subscription);
195
+
196
+ if (typeof onAdapterSubscribe === 'function') {
197
+ subscription._adapterSub = onAdapterSubscribe(subscription);
198
+ }
199
+
200
+ return subscription;
201
+ }
202
+
203
+ restoreAll(onAdapterSubscribe) {
204
+ if (typeof onAdapterSubscribe !== 'function') return;
205
+
206
+ for (const [key, sub] of this.subscriptions.entries()) {
207
+ if (!sub._isUnsubscribed) {
208
+ try {
209
+ sub._adapterSub = onAdapterSubscribe(sub);
210
+ } catch (err) {
211
+ console.error(`[realtime] Failed to restore subscription "${sub.identifier}":`, err);
212
+ }
213
+ } else {
214
+ this.subscriptions.delete(key);
215
+ }
216
+ }
217
+ }
218
+
219
+ notifyAllDisconnected() {
220
+ for (const sub of this.subscriptions.values()) {
221
+ if (!sub._isUnsubscribed) {
222
+ sub.notifyDisconnected();
223
+ }
224
+ }
225
+ }
226
+
227
+ clear() {
228
+ for (const sub of this.subscriptions.values()) {
229
+ sub.unsubscribe();
230
+ }
231
+ this.subscriptions.clear();
232
+ }
233
+
234
+ getAll() {
235
+ return Array.from(this.subscriptions.values()).filter((s) => !s._isUnsubscribed);
236
+ }
237
+ }
238
+
239
+ module.exports = {
240
+ createSubscriptionKey,
241
+ RealtimeSubscription,
242
+ SubscriptionRegistry,
243
+ };
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Node.js HTTP Server Adapter
3
+ * Provides server-level connection tracking, graceful closing, and forced termination
4
+ * @module core/shutdown/http-adapter
5
+ */
6
+
7
+ class NodeHttpAdapter {
8
+ /**
9
+ * @param {import('http').Server|import('https').Server} server
10
+ */
11
+ constructor(server) {
12
+ if (!server) {
13
+ throw new Error('NodeHttpAdapter requires an HTTP server instance');
14
+ }
15
+ this.server = server;
16
+ /** @type {Set<import('net').Socket>} */
17
+ this.sockets = new Set();
18
+ this._closed = false;
19
+
20
+ this._onConnection = this._onConnection.bind(this);
21
+ this._onSecureConnection = this._onSecureConnection.bind(this);
22
+
23
+ this._attachSocketListeners();
24
+ }
25
+
26
+ _attachSocketListeners() {
27
+ this.server.on('connection', this._onConnection);
28
+ this.server.on('secureConnection', this._onSecureConnection);
29
+ }
30
+
31
+ _trackSocket(socket) {
32
+ if (!socket || this.sockets.has(socket)) return;
33
+ this.sockets.add(socket);
34
+
35
+ const cleanup = () => {
36
+ this.sockets.delete(socket);
37
+ socket.removeListener('close', cleanup);
38
+ socket.removeListener('error', cleanup);
39
+ };
40
+
41
+ socket.once('close', cleanup);
42
+ socket.once('error', cleanup);
43
+ }
44
+
45
+ _onConnection(socket) {
46
+ this._trackSocket(socket);
47
+ }
48
+
49
+ _onSecureConnection(socket) {
50
+ this._trackSocket(socket);
51
+ }
52
+
53
+ /**
54
+ * Check if the server is currently listening
55
+ * @returns {boolean}
56
+ */
57
+ get isListening() {
58
+ return Boolean(this.server && this.server.listening);
59
+ }
60
+
61
+ /**
62
+ * Stop accepting new connections and gracefully close existing connections
63
+ * @returns {Promise<void>}
64
+ */
65
+ close() {
66
+ return new Promise((resolve) => {
67
+ if (!this.server || !this.server.listening) {
68
+ this._closed = true;
69
+ return resolve();
70
+ }
71
+
72
+ this.closeIdleConnections();
73
+
74
+ this.server.close(() => {
75
+ this._closed = true;
76
+ resolve();
77
+ });
78
+ });
79
+ }
80
+
81
+ /**
82
+ * Close idle keep-alive connections if supported by Node.js
83
+ */
84
+ closeIdleConnections() {
85
+ if (this.server && typeof this.server.closeIdleConnections === 'function') {
86
+ try {
87
+ this.server.closeIdleConnections();
88
+ } catch {
89
+ // Ignore errors during idle closing
90
+ }
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Immediately terminate all active connections
96
+ */
97
+ forceClose() {
98
+ // 1. Try Node.js native closeAllConnections (Node 18.2+)
99
+ if (this.server && typeof this.server.closeAllConnections === 'function') {
100
+ try {
101
+ this.server.closeAllConnections();
102
+ } catch {
103
+ // Fall back to manual socket destruction below
104
+ }
105
+ }
106
+
107
+ // 2. Destroy all tracked sockets with an explicit error to notify clients
108
+ for (const socket of this.sockets) {
109
+ try {
110
+ if (!socket.destroyed) {
111
+ socket.destroy(new Error('Connection closed by server shutdown'));
112
+ }
113
+ } catch {
114
+ // Ignore socket destroy errors
115
+ }
116
+ }
117
+ this.sockets.clear();
118
+ }
119
+
120
+ /**
121
+ * Detach all listeners from the server
122
+ */
123
+ destroy() {
124
+ if (this.server) {
125
+ this.server.removeListener('connection', this._onConnection);
126
+ this.server.removeListener('secureConnection', this._onSecureConnection);
127
+ }
128
+ this.sockets.clear();
129
+ }
130
+ }
131
+
132
+ module.exports = {
133
+ NodeHttpAdapter,
134
+ };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Webspresso Shutdown & Lifecycle Management
3
+ * @module core/shutdown
4
+ */
5
+
6
+ const { ShutdownManager } = require('./shutdown-manager');
7
+ const { NodeHttpAdapter } = require('./http-adapter');
8
+
9
+ module.exports = {
10
+ ShutdownManager,
11
+ NodeHttpAdapter,
12
+ };