tina4-nodejs 3.0.0-rc.2

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 (119) hide show
  1. package/BENCHMARK_REPORT.md +96 -0
  2. package/CARBONAH.md +140 -0
  3. package/CLAUDE.md +599 -0
  4. package/COMPARISON.md +194 -0
  5. package/README.md +595 -0
  6. package/package.json +59 -0
  7. package/packages/cli/src/bin.ts +110 -0
  8. package/packages/cli/src/commands/init.ts +194 -0
  9. package/packages/cli/src/commands/migrate.ts +96 -0
  10. package/packages/cli/src/commands/migrateCreate.ts +59 -0
  11. package/packages/cli/src/commands/routes.ts +61 -0
  12. package/packages/cli/src/commands/serve.ts +58 -0
  13. package/packages/cli/src/commands/test.ts +83 -0
  14. package/packages/core/gallery/auth/meta.json +1 -0
  15. package/packages/core/gallery/auth/src/routes/api/gallery/auth/login/post.ts +22 -0
  16. package/packages/core/gallery/auth/src/routes/api/gallery/auth/verify/get.ts +16 -0
  17. package/packages/core/gallery/auth/src/routes/gallery/auth/get.ts +97 -0
  18. package/packages/core/gallery/database/meta.json +1 -0
  19. package/packages/core/gallery/database/src/routes/api/gallery/db/notes/get.ts +13 -0
  20. package/packages/core/gallery/database/src/routes/api/gallery/db/notes/post.ts +17 -0
  21. package/packages/core/gallery/database/src/routes/api/gallery/db/tables/get.ts +23 -0
  22. package/packages/core/gallery/error-overlay/meta.json +1 -0
  23. package/packages/core/gallery/error-overlay/src/routes/api/gallery/crash/get.ts +17 -0
  24. package/packages/core/gallery/orm/meta.json +1 -0
  25. package/packages/core/gallery/orm/src/routes/api/gallery/products/get.ts +12 -0
  26. package/packages/core/gallery/orm/src/routes/api/gallery/products/post.ts +7 -0
  27. package/packages/core/gallery/queue/meta.json +1 -0
  28. package/packages/core/gallery/queue/src/routes/api/gallery/queue/produce/post.ts +16 -0
  29. package/packages/core/gallery/queue/src/routes/api/gallery/queue/status/get.ts +10 -0
  30. package/packages/core/gallery/rest-api/meta.json +1 -0
  31. package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/get.ts +6 -0
  32. package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/post.ts +7 -0
  33. package/packages/core/gallery/templates/meta.json +1 -0
  34. package/packages/core/gallery/templates/src/routes/gallery/page/get.ts +15 -0
  35. package/packages/core/gallery/templates/src/templates/gallery_page.twig +257 -0
  36. package/packages/core/public/css/tina4.css +2463 -0
  37. package/packages/core/public/css/tina4.min.css +1 -0
  38. package/packages/core/public/favicon.ico +0 -0
  39. package/packages/core/public/images/logo.svg +5 -0
  40. package/packages/core/public/images/tina4-logo-icon.webp +0 -0
  41. package/packages/core/public/js/frond.min.js +420 -0
  42. package/packages/core/public/js/tina4-dev-admin.min.js +327 -0
  43. package/packages/core/public/js/tina4.min.js +93 -0
  44. package/packages/core/public/swagger/index.html +90 -0
  45. package/packages/core/public/swagger/oauth2-redirect.html +63 -0
  46. package/packages/core/src/ai.ts +359 -0
  47. package/packages/core/src/api.ts +248 -0
  48. package/packages/core/src/auth.ts +287 -0
  49. package/packages/core/src/cache.ts +121 -0
  50. package/packages/core/src/constants.ts +48 -0
  51. package/packages/core/src/container.ts +90 -0
  52. package/packages/core/src/devAdmin.ts +2024 -0
  53. package/packages/core/src/devMailbox.ts +316 -0
  54. package/packages/core/src/dotenv.ts +172 -0
  55. package/packages/core/src/errorOverlay.test.ts +122 -0
  56. package/packages/core/src/errorOverlay.ts +278 -0
  57. package/packages/core/src/events.ts +112 -0
  58. package/packages/core/src/fakeData.ts +309 -0
  59. package/packages/core/src/graphql.ts +812 -0
  60. package/packages/core/src/health.ts +31 -0
  61. package/packages/core/src/htmlElement.ts +172 -0
  62. package/packages/core/src/i18n.ts +136 -0
  63. package/packages/core/src/index.ts +88 -0
  64. package/packages/core/src/logger.ts +226 -0
  65. package/packages/core/src/messenger.ts +822 -0
  66. package/packages/core/src/middleware.ts +138 -0
  67. package/packages/core/src/queue.ts +481 -0
  68. package/packages/core/src/queueBackends/kafkaBackend.ts +348 -0
  69. package/packages/core/src/queueBackends/rabbitmqBackend.ts +479 -0
  70. package/packages/core/src/rateLimiter.ts +107 -0
  71. package/packages/core/src/request.ts +189 -0
  72. package/packages/core/src/response.ts +146 -0
  73. package/packages/core/src/routeDiscovery.ts +87 -0
  74. package/packages/core/src/router.ts +398 -0
  75. package/packages/core/src/scss.ts +366 -0
  76. package/packages/core/src/server.ts +610 -0
  77. package/packages/core/src/service.ts +380 -0
  78. package/packages/core/src/session.ts +480 -0
  79. package/packages/core/src/sessionHandlers/mongoHandler.ts +286 -0
  80. package/packages/core/src/sessionHandlers/valkeyHandler.ts +184 -0
  81. package/packages/core/src/static.ts +58 -0
  82. package/packages/core/src/testing.ts +233 -0
  83. package/packages/core/src/types.ts +98 -0
  84. package/packages/core/src/watcher.ts +37 -0
  85. package/packages/core/src/websocket.ts +408 -0
  86. package/packages/core/src/wsdl.ts +546 -0
  87. package/packages/core/templates/errors/302.twig +14 -0
  88. package/packages/core/templates/errors/401.twig +9 -0
  89. package/packages/core/templates/errors/403.twig +29 -0
  90. package/packages/core/templates/errors/404.twig +29 -0
  91. package/packages/core/templates/errors/500.twig +38 -0
  92. package/packages/core/templates/errors/502.twig +9 -0
  93. package/packages/core/templates/errors/503.twig +12 -0
  94. package/packages/core/templates/errors/base.twig +37 -0
  95. package/packages/frond/src/engine.ts +1475 -0
  96. package/packages/frond/src/index.ts +2 -0
  97. package/packages/orm/src/adapters/firebird.ts +455 -0
  98. package/packages/orm/src/adapters/mssql.ts +440 -0
  99. package/packages/orm/src/adapters/mysql.ts +355 -0
  100. package/packages/orm/src/adapters/postgres.ts +362 -0
  101. package/packages/orm/src/adapters/sqlite.ts +270 -0
  102. package/packages/orm/src/autoCrud.ts +231 -0
  103. package/packages/orm/src/baseModel.ts +536 -0
  104. package/packages/orm/src/database.ts +321 -0
  105. package/packages/orm/src/fakeData.ts +118 -0
  106. package/packages/orm/src/index.ts +49 -0
  107. package/packages/orm/src/migration.ts +392 -0
  108. package/packages/orm/src/model.ts +56 -0
  109. package/packages/orm/src/query.ts +113 -0
  110. package/packages/orm/src/seeder.ts +120 -0
  111. package/packages/orm/src/sqlTranslation.ts +272 -0
  112. package/packages/orm/src/types.ts +110 -0
  113. package/packages/orm/src/validation.ts +93 -0
  114. package/packages/swagger/src/generator.ts +189 -0
  115. package/packages/swagger/src/index.ts +2 -0
  116. package/packages/swagger/src/ui.ts +48 -0
  117. package/skills/tina4-developer.skill +0 -0
  118. package/skills/tina4-js.skill +0 -0
  119. package/skills/tina4-maintainer.skill +0 -0
@@ -0,0 +1,408 @@
1
+ /**
2
+ * Tina4 WebSocket — Zero-dependency RFC 6455 implementation.
3
+ *
4
+ * Native WebSocket server using Node.js built-in `http` module.
5
+ *
6
+ * import { WebSocketServer } from "@tina4/core";
7
+ *
8
+ * const wss = new WebSocketServer({ port: 8080 });
9
+ * wss.on("connection", (client) => {
10
+ * console.log("Connected:", client.id);
11
+ * });
12
+ * wss.on("message", (client, message) => {
13
+ * wss.broadcast(message);
14
+ * });
15
+ * await wss.start();
16
+ *
17
+ * Supported:
18
+ * - HTTP Upgrade handshake (RFC 6455 Sec-WebSocket-Accept)
19
+ * - Frame protocol: text, binary, close, ping, pong
20
+ * - Masking / unmasking (client->server)
21
+ * - Extended payload lengths (7-bit, 16-bit, 64-bit)
22
+ * - Connection manager with broadcast
23
+ */
24
+ import { createServer, IncomingMessage, ServerResponse } from "node:http";
25
+ import { createHash } from "node:crypto";
26
+ import { randomUUID } from "node:crypto";
27
+ import type { Socket } from "node:net";
28
+ import type { Server } from "node:http";
29
+
30
+ // ── Constants ────────────────────────────────────────────────
31
+
32
+ const MAGIC_STRING = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
33
+
34
+ // Opcodes
35
+ export const OP_CONTINUATION = 0x0;
36
+ export const OP_TEXT = 0x1;
37
+ export const OP_BINARY = 0x2;
38
+ export const OP_CLOSE = 0x8;
39
+ export const OP_PING = 0x9;
40
+ export const OP_PONG = 0xa;
41
+
42
+ // Close codes
43
+ export const CLOSE_NORMAL = 1000;
44
+ export const CLOSE_GOING_AWAY = 1001;
45
+ export const CLOSE_PROTOCOL_ERROR = 1002;
46
+
47
+ // ── Types ────────────────────────────────────────────────────
48
+
49
+ export interface WebSocketClient {
50
+ id: string;
51
+ socket: Socket;
52
+ ip: string;
53
+ connectedAt: number;
54
+ closed: boolean;
55
+ }
56
+
57
+ type EventHandler = (...args: unknown[]) => void;
58
+
59
+ // ── Frame Utilities (exported for testing) ───────────────────
60
+
61
+ /**
62
+ * Compute Sec-WebSocket-Accept from Sec-WebSocket-Key per RFC 6455.
63
+ */
64
+ export function computeAcceptKey(key: string): string {
65
+ return createHash("sha1")
66
+ .update(key + MAGIC_STRING)
67
+ .digest("base64");
68
+ }
69
+
70
+ /**
71
+ * Parse HTTP headers from raw upgrade request data.
72
+ */
73
+ export function parseUpgradeHeaders(raw: string): Record<string, string> {
74
+ const headers: Record<string, string> = {};
75
+ const lines = raw.split("\r\n");
76
+ const requestLine = lines[0] ?? "";
77
+ const parts = requestLine.split(" ");
78
+ if (parts.length >= 2) {
79
+ headers["_method"] = parts[0];
80
+ headers["_path"] = parts[1];
81
+ }
82
+ for (let i = 1; i < lines.length; i++) {
83
+ const line = lines[i];
84
+ const colonIdx = line.indexOf(":");
85
+ if (colonIdx > 0) {
86
+ const key = line.slice(0, colonIdx).trim().toLowerCase();
87
+ const value = line.slice(colonIdx + 1).trim();
88
+ headers[key] = value;
89
+ }
90
+ }
91
+ return headers;
92
+ }
93
+
94
+ /**
95
+ * Build a WebSocket frame (server->client, never masked).
96
+ */
97
+ export function buildFrame(opcode: number, payload: Buffer, fin: boolean = true): Buffer {
98
+ const frame: number[] = [];
99
+ const firstByte = (fin ? 0x80 : 0x00) | opcode;
100
+ frame.push(firstByte);
101
+
102
+ const length = payload.length;
103
+ if (length < 126) {
104
+ frame.push(length);
105
+ } else if (length < 65536) {
106
+ frame.push(126);
107
+ frame.push((length >> 8) & 0xff);
108
+ frame.push(length & 0xff);
109
+ } else {
110
+ frame.push(127);
111
+ // 8 bytes for 64-bit length
112
+ const buf = Buffer.alloc(8);
113
+ buf.writeBigUInt64BE(BigInt(length));
114
+ for (let i = 0; i < 8; i++) {
115
+ frame.push(buf[i]);
116
+ }
117
+ }
118
+
119
+ const header = Buffer.from(frame);
120
+ return Buffer.concat([header, payload]);
121
+ }
122
+
123
+ /**
124
+ * Parse a WebSocket frame from a buffer.
125
+ * Returns { fin, opcode, payload, bytesConsumed } or null if not enough data.
126
+ */
127
+ export function parseFrame(
128
+ data: Buffer,
129
+ ): { fin: boolean; opcode: number; payload: Buffer; bytesConsumed: number } | null {
130
+ if (data.length < 2) return null;
131
+
132
+ const fin = (data[0] >> 7) & 1;
133
+ const opcode = data[0] & 0x0f;
134
+ const masked = (data[1] >> 7) & 1;
135
+ let payloadLen = data[1] & 0x7f;
136
+ let offset = 2;
137
+
138
+ if (payloadLen === 126) {
139
+ if (data.length < 4) return null;
140
+ payloadLen = data.readUInt16BE(2);
141
+ offset = 4;
142
+ } else if (payloadLen === 127) {
143
+ if (data.length < 10) return null;
144
+ payloadLen = Number(data.readBigUInt64BE(2));
145
+ offset = 10;
146
+ }
147
+
148
+ if (masked) {
149
+ if (data.length < offset + 4 + payloadLen) return null;
150
+ const maskKey = data.subarray(offset, offset + 4);
151
+ offset += 4;
152
+ const payload = Buffer.alloc(payloadLen);
153
+ for (let i = 0; i < payloadLen; i++) {
154
+ payload[i] = data[offset + i] ^ maskKey[i % 4];
155
+ }
156
+ return { fin: !!fin, opcode, payload, bytesConsumed: offset + payloadLen };
157
+ }
158
+
159
+ if (data.length < offset + payloadLen) return null;
160
+ const payload = data.subarray(offset, offset + payloadLen);
161
+ return { fin: !!fin, opcode, payload: Buffer.from(payload), bytesConsumed: offset + payloadLen };
162
+ }
163
+
164
+ // ── WebSocket Server ─────────────────────────────────────────
165
+
166
+ export class WebSocketServer {
167
+ private port: number;
168
+ private server: Server | null = null;
169
+ private clients: Map<string, WebSocketClient> = new Map();
170
+ private handlers: Map<string, EventHandler[]> = new Map();
171
+
172
+ constructor(options?: { port?: number }) {
173
+ this.port = options?.port ?? parseInt(process.env.TINA4_WS_PORT ?? "8080", 10);
174
+ }
175
+
176
+ /**
177
+ * Register an event handler.
178
+ */
179
+ on(event: string, handler: Function): WebSocketServer {
180
+ const list = this.handlers.get(event) ?? [];
181
+ list.push(handler as EventHandler);
182
+ this.handlers.set(event, list);
183
+ return this;
184
+ }
185
+
186
+ /**
187
+ * Broadcast a message to all connected clients.
188
+ */
189
+ broadcast(message: string, excludeIds?: string[]): void {
190
+ const frame = buildFrame(OP_TEXT, Buffer.from(message, "utf-8"));
191
+ const exclude = new Set(excludeIds ?? []);
192
+
193
+ for (const [id, client] of this.clients) {
194
+ if (exclude.has(id)) continue;
195
+ if (client.closed) continue;
196
+ try {
197
+ client.socket.write(frame);
198
+ } catch {
199
+ // client disconnected
200
+ }
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Send a message to a specific client.
206
+ */
207
+ send(clientId: string, message: string): void {
208
+ const client = this.clients.get(clientId);
209
+ if (!client || client.closed) return;
210
+
211
+ const frame = buildFrame(OP_TEXT, Buffer.from(message, "utf-8"));
212
+ try {
213
+ client.socket.write(frame);
214
+ } catch {
215
+ // client disconnected
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Start the WebSocket server.
221
+ */
222
+ async start(): Promise<void> {
223
+ return new Promise((resolve, reject) => {
224
+ this.server = createServer((req: IncomingMessage, res: ServerResponse) => {
225
+ res.writeHead(426, { "Content-Type": "text/plain" });
226
+ res.end("Upgrade Required");
227
+ });
228
+
229
+ this.server.on("upgrade", (req: IncomingMessage, socket: Socket, head: Buffer) => {
230
+ this.handleUpgrade(req, socket, head);
231
+ });
232
+
233
+ this.server.listen(this.port, () => {
234
+ resolve();
235
+ });
236
+
237
+ this.server.on("error", (err) => {
238
+ this.emit("error", err);
239
+ reject(err);
240
+ });
241
+ });
242
+ }
243
+
244
+ /**
245
+ * Stop the server and disconnect all clients.
246
+ */
247
+ stop(): void {
248
+ // Close all client connections
249
+ for (const [id, client] of this.clients) {
250
+ if (!client.closed) {
251
+ try {
252
+ const closeFrame = buildFrame(OP_CLOSE, Buffer.from([0x03, 0xe8])); // 1000
253
+ client.socket.write(closeFrame);
254
+ client.socket.end();
255
+ } catch {
256
+ // already closed
257
+ }
258
+ client.closed = true;
259
+ }
260
+ }
261
+ this.clients.clear();
262
+
263
+ if (this.server) {
264
+ this.server.close();
265
+ this.server = null;
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Get all connected clients.
271
+ */
272
+ getClients(): Map<string, WebSocketClient> {
273
+ return this.clients;
274
+ }
275
+
276
+ // ── Private ────────────────────────────────────────────────
277
+
278
+ private emit(event: string, ...args: unknown[]): void {
279
+ const handlers = this.handlers.get(event) ?? [];
280
+ for (const handler of handlers) {
281
+ handler(...args);
282
+ }
283
+ }
284
+
285
+ private handleUpgrade(req: IncomingMessage, socket: Socket, head: Buffer): void {
286
+ const wsKey = req.headers["sec-websocket-key"];
287
+ if (!wsKey) {
288
+ socket.write("HTTP/1.1 400 Bad Request\r\n\r\n");
289
+ socket.destroy();
290
+ return;
291
+ }
292
+
293
+ const wsVersion = req.headers["sec-websocket-version"];
294
+ if (wsVersion && wsVersion !== "13") {
295
+ socket.write("HTTP/1.1 426 Upgrade Required\r\nSec-WebSocket-Version: 13\r\n\r\n");
296
+ socket.destroy();
297
+ return;
298
+ }
299
+
300
+ // Compute accept key and send upgrade response
301
+ const acceptKey = computeAcceptKey(wsKey);
302
+ const response = [
303
+ "HTTP/1.1 101 Switching Protocols",
304
+ "Upgrade: websocket",
305
+ "Connection: Upgrade",
306
+ `Sec-WebSocket-Accept: ${acceptKey}`,
307
+ "",
308
+ "",
309
+ ].join("\r\n");
310
+
311
+ socket.write(response);
312
+
313
+ // Create client
314
+ const clientId = randomUUID().slice(0, 8);
315
+ const client: WebSocketClient = {
316
+ id: clientId,
317
+ socket,
318
+ ip: (socket.remoteAddress ?? "unknown"),
319
+ connectedAt: Date.now(),
320
+ closed: false,
321
+ };
322
+
323
+ this.clients.set(clientId, client);
324
+ this.emit("connection", client);
325
+
326
+ // Handle incoming data
327
+ let buffer = Buffer.alloc(0);
328
+ if (head.length > 0) {
329
+ buffer = Buffer.concat([buffer, head]);
330
+ }
331
+
332
+ socket.on("data", (chunk: Buffer) => {
333
+ buffer = Buffer.concat([buffer, chunk]);
334
+ this.processBuffer(client, buffer, (remaining) => {
335
+ buffer = remaining;
336
+ });
337
+ });
338
+
339
+ socket.on("close", () => {
340
+ client.closed = true;
341
+ this.clients.delete(clientId);
342
+ this.emit("close", client);
343
+ });
344
+
345
+ socket.on("error", (err) => {
346
+ client.closed = true;
347
+ this.clients.delete(clientId);
348
+ this.emit("error", err, client);
349
+ });
350
+ }
351
+
352
+ private processBuffer(
353
+ client: WebSocketClient,
354
+ buffer: Buffer,
355
+ setBuffer: (remaining: Buffer) => void,
356
+ ): void {
357
+ let remaining = buffer;
358
+
359
+ while (remaining.length > 0) {
360
+ const frame = parseFrame(remaining);
361
+ if (!frame) break;
362
+
363
+ remaining = remaining.subarray(frame.bytesConsumed);
364
+
365
+ switch (frame.opcode) {
366
+ case OP_TEXT:
367
+ this.emit("message", client, frame.payload.toString("utf-8"));
368
+ break;
369
+
370
+ case OP_BINARY:
371
+ this.emit("message", client, frame.payload);
372
+ break;
373
+
374
+ case OP_PING: {
375
+ const pongFrame = buildFrame(OP_PONG, frame.payload);
376
+ try {
377
+ client.socket.write(pongFrame);
378
+ } catch {
379
+ // client disconnected
380
+ }
381
+ break;
382
+ }
383
+
384
+ case OP_PONG:
385
+ // ignore
386
+ break;
387
+
388
+ case OP_CLOSE: {
389
+ if (!client.closed) {
390
+ client.closed = true;
391
+ const closeFrame = buildFrame(OP_CLOSE, Buffer.from([0x03, 0xe8]));
392
+ try {
393
+ client.socket.write(closeFrame);
394
+ client.socket.end();
395
+ } catch {
396
+ // already closed
397
+ }
398
+ this.clients.delete(client.id);
399
+ this.emit("close", client);
400
+ }
401
+ break;
402
+ }
403
+ }
404
+ }
405
+
406
+ setBuffer(remaining);
407
+ }
408
+ }