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,479 @@
1
+ /**
2
+ * Tina4 RabbitMQ Queue Backend — AMQP 0-9-1 via raw TCP, zero dependencies.
3
+ *
4
+ * Implements the same interface as the file-based queue but uses RabbitMQ
5
+ * for message storage and delivery.
6
+ *
7
+ * Configure via environment variables:
8
+ * TINA4_RABBITMQ_HOST (default: "localhost")
9
+ * TINA4_RABBITMQ_PORT (default: 5672)
10
+ * TINA4_RABBITMQ_USERNAME (default: "guest")
11
+ * TINA4_RABBITMQ_PASSWORD (default: "guest")
12
+ * TINA4_RABBITMQ_VHOST (default: "/")
13
+ */
14
+ import net from "node:net";
15
+ import { randomUUID } from "node:crypto";
16
+ import type { QueueJob } from "../queue.js";
17
+
18
+ // ── Types ────────────────────────────────────────────────────
19
+
20
+ export interface RabbitMQConfig {
21
+ host?: string;
22
+ port?: number;
23
+ username?: string;
24
+ password?: string;
25
+ vhost?: string;
26
+ }
27
+
28
+ export interface QueueBackend {
29
+ push(queue: string, payload: unknown, delay?: number): string;
30
+ pop(queue: string): QueueJob | null;
31
+ size(queue: string): number;
32
+ clear(queue: string): void;
33
+ }
34
+
35
+ // ── AMQP 0-9-1 Constants ────────────────────────────────────
36
+
37
+ const AMQP_PROTOCOL_HEADER = Buffer.from([65, 77, 81, 80, 0, 0, 9, 1]); // "AMQP" + 0.9.1
38
+
39
+ // Frame types
40
+ const FRAME_METHOD = 1;
41
+ const FRAME_HEADER = 2;
42
+ const FRAME_BODY = 3;
43
+ const FRAME_HEARTBEAT = 8;
44
+ const FRAME_END = 0xce;
45
+
46
+ // Class/method IDs
47
+ const CONNECTION_START = (10 << 16) | 10;
48
+ const CONNECTION_START_OK = (10 << 16) | 11;
49
+ const CONNECTION_TUNE = (10 << 16) | 30;
50
+ const CONNECTION_TUNE_OK = (10 << 16) | 31;
51
+ const CONNECTION_OPEN = (10 << 16) | 40;
52
+ const CONNECTION_OPEN_OK = (10 << 16) | 41;
53
+ const CONNECTION_CLOSE = (10 << 16) | 50;
54
+ const CONNECTION_CLOSE_OK = (10 << 16) | 51;
55
+ const CHANNEL_OPEN = (20 << 16) | 10;
56
+ const CHANNEL_OPEN_OK = (20 << 16) | 11;
57
+ const CHANNEL_CLOSE = (20 << 16) | 40;
58
+ const CHANNEL_CLOSE_OK = (20 << 16) | 41;
59
+ const QUEUE_DECLARE = (50 << 16) | 10;
60
+ const QUEUE_DECLARE_OK = (50 << 16) | 11;
61
+ const BASIC_PUBLISH = (60 << 16) | 40;
62
+ const BASIC_GET = (60 << 16) | 70;
63
+ const BASIC_GET_OK = (60 << 16) | 71;
64
+ const BASIC_GET_EMPTY = (60 << 16) | 72;
65
+ const BASIC_ACK = (60 << 16) | 80;
66
+
67
+ // ── AMQP Helpers ─────────────────────────────────────────────
68
+
69
+ function writeShortString(buf: Buffer, offset: number, str: string): number {
70
+ const len = Buffer.byteLength(str, "utf-8");
71
+ buf.writeUInt8(len, offset);
72
+ buf.write(str, offset + 1, len, "utf-8");
73
+ return offset + 1 + len;
74
+ }
75
+
76
+ function writeLongString(buf: Buffer, offset: number, str: string): number {
77
+ const len = Buffer.byteLength(str, "utf-8");
78
+ buf.writeUInt32BE(len, offset);
79
+ buf.write(str, offset + 4, len, "utf-8");
80
+ return offset + 4 + len;
81
+ }
82
+
83
+ function writeTable(table: Record<string, string>): Buffer {
84
+ const parts: Buffer[] = [];
85
+ for (const [key, value] of Object.entries(table)) {
86
+ const keyBuf = Buffer.alloc(1 + Buffer.byteLength(key));
87
+ writeShortString(keyBuf, 0, key);
88
+ parts.push(keyBuf);
89
+
90
+ // Type 'S' for long string
91
+ const valBuf = Buffer.alloc(1 + 4 + Buffer.byteLength(value));
92
+ valBuf.writeUInt8(83, 0); // 'S'
93
+ writeLongString(valBuf, 1, value);
94
+ parts.push(valBuf);
95
+ }
96
+ const tableData = Buffer.concat(parts);
97
+ const result = Buffer.alloc(4 + tableData.length);
98
+ result.writeUInt32BE(tableData.length, 0);
99
+ tableData.copy(result, 4);
100
+ return result;
101
+ }
102
+
103
+ function buildMethodFrame(channel: number, classMethod: number, payload: Buffer): Buffer {
104
+ const framePayload = Buffer.alloc(4 + payload.length);
105
+ framePayload.writeUInt16BE((classMethod >> 16) & 0xffff, 0);
106
+ framePayload.writeUInt16BE(classMethod & 0xffff, 2);
107
+ payload.copy(framePayload, 4);
108
+
109
+ const frame = Buffer.alloc(7 + framePayload.length + 1);
110
+ frame.writeUInt8(FRAME_METHOD, 0);
111
+ frame.writeUInt16BE(channel, 1);
112
+ frame.writeUInt32BE(framePayload.length, 3);
113
+ framePayload.copy(frame, 7);
114
+ frame.writeUInt8(FRAME_END, 7 + framePayload.length);
115
+ return frame;
116
+ }
117
+
118
+ // ── RabbitMQ Backend ─────────────────────────────────────────
119
+
120
+ /**
121
+ * RabbitMQ queue backend using raw AMQP 0-9-1 protocol.
122
+ *
123
+ * Uses synchronous-style communication by spawning a child process
124
+ * for each operation, similar to the Redis session handler pattern.
125
+ * This keeps the interface synchronous as required by the Queue class.
126
+ */
127
+ export class RabbitMQBackend implements QueueBackend {
128
+ private host: string;
129
+ private port: number;
130
+ private username: string;
131
+ private password: string;
132
+ private vhost: string;
133
+
134
+ constructor(config?: RabbitMQConfig) {
135
+ this.host = config?.host ?? process.env.TINA4_RABBITMQ_HOST ?? "localhost";
136
+ this.port = config?.port
137
+ ?? (process.env.TINA4_RABBITMQ_PORT ? parseInt(process.env.TINA4_RABBITMQ_PORT, 10) : 5672);
138
+ this.username = config?.username ?? process.env.TINA4_RABBITMQ_USERNAME ?? "guest";
139
+ this.password = config?.password ?? process.env.TINA4_RABBITMQ_PASSWORD ?? "guest";
140
+ this.vhost = config?.vhost ?? process.env.TINA4_RABBITMQ_VHOST ?? "/";
141
+ }
142
+
143
+ /**
144
+ * Execute an AMQP operation synchronously via a child process.
145
+ */
146
+ private execSync(operation: string, queue: string, data?: string): string {
147
+ const { execFileSync } = require("node:child_process");
148
+
149
+ const script = `
150
+ const net = require("node:net");
151
+ const host = ${JSON.stringify(this.host)};
152
+ const port = ${this.port};
153
+ const username = ${JSON.stringify(this.username)};
154
+ const password = ${JSON.stringify(this.password)};
155
+ const vhost = ${JSON.stringify(this.vhost)};
156
+ const operation = ${JSON.stringify(operation)};
157
+ const queueName = ${JSON.stringify(queue)};
158
+ const data = ${JSON.stringify(data ?? "")};
159
+
160
+ // Simplified AMQP interaction — connect, perform operation, disconnect
161
+ const sock = net.createConnection({ host, port }, () => {
162
+ // Send protocol header
163
+ sock.write(Buffer.from([65, 77, 81, 80, 0, 0, 9, 1]));
164
+ });
165
+
166
+ let buffer = Buffer.alloc(0);
167
+ let step = "handshake";
168
+ let deliveryTag = null;
169
+
170
+ sock.on("data", (chunk) => {
171
+ buffer = Buffer.concat([buffer, chunk]);
172
+ processFrames();
173
+ });
174
+
175
+ function processFrames() {
176
+ while (buffer.length >= 7) {
177
+ const frameType = buffer.readUInt8(0);
178
+ const channel = buffer.readUInt16BE(1);
179
+ const size = buffer.readUInt32BE(3);
180
+
181
+ if (buffer.length < 7 + size + 1) return; // Incomplete frame
182
+
183
+ const payload = buffer.subarray(7, 7 + size);
184
+ buffer = buffer.subarray(7 + size + 1);
185
+
186
+ if (frameType === 1) { // METHOD frame
187
+ const classId = payload.readUInt16BE(0);
188
+ const methodId = payload.readUInt16BE(2);
189
+ handleMethod(classId, methodId, payload.subarray(4), channel);
190
+ } else if (frameType === 2) { // HEADER frame
191
+ // Content header — skip for basic.get
192
+ } else if (frameType === 3) { // BODY frame
193
+ // Content body
194
+ const body = payload.toString("utf-8");
195
+ process.stdout.write(body);
196
+ }
197
+ }
198
+ }
199
+
200
+ function handleMethod(classId, methodId, args, channel) {
201
+ if (classId === 10 && methodId === 10) {
202
+ // Connection.Start → send Connection.Start-Ok
203
+ const props = buildTable({ product: "Tina4", version: "1.0" });
204
+ const mechanism = "PLAIN";
205
+ const saslData = "\\x00" + username + "\\x00" + password;
206
+ const locale = "en_US";
207
+
208
+ const payload = Buffer.alloc(4096);
209
+ let offset = 0;
210
+
211
+ // Client properties (table)
212
+ props.copy(payload, offset);
213
+ offset += props.length;
214
+
215
+ // Mechanism (short string)
216
+ const mechBuf = Buffer.from(mechanism, "utf-8");
217
+ payload.writeUInt8(mechBuf.length, offset); offset++;
218
+ mechBuf.copy(payload, offset); offset += mechBuf.length;
219
+
220
+ // Response (long string — SASL PLAIN)
221
+ const saslBuf = Buffer.from(saslData, "utf-8");
222
+ // Fix null bytes for PLAIN auth
223
+ saslBuf[0] = 0;
224
+ const userLen = Buffer.byteLength(username);
225
+ saslBuf[1 + userLen] = 0;
226
+ payload.writeUInt32BE(saslBuf.length, offset); offset += 4;
227
+ saslBuf.copy(payload, offset); offset += saslBuf.length;
228
+
229
+ // Locale (short string)
230
+ const localeBuf = Buffer.from(locale, "utf-8");
231
+ payload.writeUInt8(localeBuf.length, offset); offset++;
232
+ localeBuf.copy(payload, offset); offset += localeBuf.length;
233
+
234
+ sendMethod(0, 10, 11, payload.subarray(0, offset));
235
+ }
236
+ else if (classId === 10 && methodId === 30) {
237
+ // Connection.Tune → send Connection.Tune-Ok + Connection.Open
238
+ const tuneOk = Buffer.alloc(12);
239
+ tuneOk.writeUInt16BE(0, 0); // channel-max
240
+ tuneOk.writeUInt32BE(131072, 2); // frame-max
241
+ tuneOk.writeUInt16BE(60, 6); // heartbeat
242
+ sendMethod(0, 10, 31, tuneOk);
243
+
244
+ // Connection.Open
245
+ const vhostBuf = Buffer.from(vhost, "utf-8");
246
+ const openPayload = Buffer.alloc(3 + vhostBuf.length);
247
+ openPayload.writeUInt8(vhostBuf.length, 0);
248
+ vhostBuf.copy(openPayload, 1);
249
+ openPayload.writeUInt8(0, 1 + vhostBuf.length); // reserved
250
+ openPayload.writeUInt8(0, 2 + vhostBuf.length); // reserved
251
+ sendMethod(0, 10, 40, openPayload);
252
+ }
253
+ else if (classId === 10 && methodId === 41) {
254
+ // Connection.Open-Ok → open channel
255
+ const chanOpen = Buffer.alloc(1);
256
+ chanOpen.writeUInt8(0, 0);
257
+ sendMethod(1, 20, 10, chanOpen);
258
+ }
259
+ else if (classId === 20 && methodId === 11) {
260
+ // Channel.Open-Ok → declare queue
261
+ const qBuf = Buffer.from(queueName, "utf-8");
262
+ const declPayload = Buffer.alloc(7 + qBuf.length);
263
+ declPayload.writeUInt16BE(0, 0); // reserved
264
+ declPayload.writeUInt8(qBuf.length, 2);
265
+ qBuf.copy(declPayload, 3);
266
+ declPayload.writeUInt8(2, 3 + qBuf.length); // durable=true
267
+ declPayload.writeUInt32BE(0, 4 + qBuf.length); // arguments (empty table)
268
+ sendMethod(1, 50, 10, declPayload);
269
+ }
270
+ else if (classId === 50 && methodId === 11) {
271
+ // Queue.Declare-Ok → perform operation
272
+ if (operation === "publish") {
273
+ // Basic.Publish
274
+ const qBuf = Buffer.from(queueName, "utf-8");
275
+ const pubPayload = Buffer.alloc(5 + qBuf.length);
276
+ pubPayload.writeUInt16BE(0, 0); // reserved
277
+ pubPayload.writeUInt8(0, 2); // exchange (empty = default)
278
+ pubPayload.writeUInt8(qBuf.length, 3);
279
+ qBuf.copy(pubPayload, 4);
280
+ pubPayload.writeUInt8(0, 4 + qBuf.length); // mandatory=false
281
+
282
+ sendMethod(1, 60, 40, pubPayload);
283
+
284
+ // Content header
285
+ const bodyBuf = Buffer.from(data, "utf-8");
286
+ const header = Buffer.alloc(18);
287
+ header.writeUInt16BE(60, 0); // class = basic
288
+ header.writeUInt16BE(0, 2); // weight
289
+ // body size (64-bit, we only use lower 32)
290
+ header.writeUInt32BE(0, 4);
291
+ header.writeUInt32BE(bodyBuf.length, 8);
292
+ header.writeUInt16BE(0x6000, 12); // property flags: delivery-mode + content-type
293
+ // content-type
294
+ const ct = Buffer.from("application/json");
295
+ header.writeUInt8(ct.length, 14);
296
+
297
+ const fullHeader = Buffer.alloc(14 + 1 + ct.length + 1);
298
+ fullHeader.writeUInt16BE(60, 0);
299
+ fullHeader.writeUInt16BE(0, 2);
300
+ fullHeader.writeUInt32BE(0, 4);
301
+ fullHeader.writeUInt32BE(bodyBuf.length, 8);
302
+ fullHeader.writeUInt16BE(0x0000, 12); // no properties for simplicity
303
+
304
+ // Send header frame
305
+ const hFrame = Buffer.alloc(7 + fullHeader.length + 1);
306
+ hFrame.writeUInt8(2, 0); // header frame
307
+ hFrame.writeUInt16BE(1, 1); // channel
308
+ hFrame.writeUInt32BE(fullHeader.length, 3);
309
+ fullHeader.copy(hFrame, 7);
310
+ hFrame.writeUInt8(0xce, 7 + fullHeader.length);
311
+ sock.write(hFrame);
312
+
313
+ // Send body frame
314
+ const bFrame = Buffer.alloc(7 + bodyBuf.length + 1);
315
+ bFrame.writeUInt8(3, 0); // body frame
316
+ bFrame.writeUInt16BE(1, 1); // channel
317
+ bFrame.writeUInt32BE(bodyBuf.length, 3);
318
+ bodyBuf.copy(bFrame, 7);
319
+ bFrame.writeUInt8(0xce, 7 + bodyBuf.length);
320
+ sock.write(bFrame);
321
+
322
+ process.stdout.write("__PUBLISHED__");
323
+ closeConnection();
324
+ }
325
+ else if (operation === "get") {
326
+ // Basic.Get
327
+ const qBuf = Buffer.from(queueName, "utf-8");
328
+ const getPayload = Buffer.alloc(4 + qBuf.length);
329
+ getPayload.writeUInt16BE(0, 0); // reserved
330
+ getPayload.writeUInt8(qBuf.length, 2);
331
+ qBuf.copy(getPayload, 3);
332
+ getPayload.writeUInt8(1, 3 + qBuf.length); // no-ack=true
333
+ sendMethod(1, 60, 70, getPayload);
334
+ }
335
+ else if (operation === "size") {
336
+ // Queue.Declare-Ok already has message count
337
+ const msgCount = args.readUInt32BE(args.readUInt8(0) + 1);
338
+ process.stdout.write(String(msgCount));
339
+ closeConnection();
340
+ }
341
+ else if (operation === "purge") {
342
+ // Queue.Purge
343
+ const qBuf = Buffer.from(queueName, "utf-8");
344
+ const purgePayload = Buffer.alloc(4 + qBuf.length);
345
+ purgePayload.writeUInt16BE(0, 0);
346
+ purgePayload.writeUInt8(qBuf.length, 2);
347
+ qBuf.copy(purgePayload, 3);
348
+ purgePayload.writeUInt8(0, 3 + qBuf.length); // no-wait=false
349
+ sendMethod(1, 50, 30, purgePayload);
350
+ }
351
+ }
352
+ else if (classId === 60 && methodId === 71) {
353
+ // Basic.Get-Ok — message body will follow in content frames
354
+ // Body comes next via BODY frames handled above
355
+ }
356
+ else if (classId === 60 && methodId === 72) {
357
+ // Basic.Get-Empty
358
+ process.stdout.write("__EMPTY__");
359
+ closeConnection();
360
+ }
361
+ else if (classId === 50 && methodId === 31) {
362
+ // Queue.Purge-Ok
363
+ process.stdout.write("__PURGED__");
364
+ closeConnection();
365
+ }
366
+ else if (classId === 10 && methodId === 50) {
367
+ // Connection.Close → send Connection.Close-Ok
368
+ sendMethod(0, 10, 51, Buffer.alloc(0));
369
+ sock.destroy();
370
+ }
371
+ }
372
+
373
+ function sendMethod(channel, classId, methodId, payload) {
374
+ const mp = Buffer.alloc(4 + payload.length);
375
+ mp.writeUInt16BE(classId, 0);
376
+ mp.writeUInt16BE(methodId, 2);
377
+ payload.copy(mp, 4);
378
+
379
+ const frame = Buffer.alloc(7 + mp.length + 1);
380
+ frame.writeUInt8(1, 0);
381
+ frame.writeUInt16BE(channel, 1);
382
+ frame.writeUInt32BE(mp.length, 3);
383
+ mp.copy(frame, 7);
384
+ frame.writeUInt8(0xce, 7 + mp.length);
385
+ sock.write(frame);
386
+ }
387
+
388
+ function buildTable(obj) {
389
+ const parts = [];
390
+ for (const [k, v] of Object.entries(obj)) {
391
+ const keyBuf = Buffer.alloc(1 + Buffer.byteLength(k));
392
+ keyBuf.writeUInt8(Buffer.byteLength(k), 0);
393
+ keyBuf.write(k, 1, "utf-8");
394
+ parts.push(keyBuf);
395
+ const valBuf = Buffer.alloc(5 + Buffer.byteLength(v));
396
+ valBuf.writeUInt8(83, 0); // 'S'
397
+ valBuf.writeUInt32BE(Buffer.byteLength(v), 1);
398
+ valBuf.write(v, 5, "utf-8");
399
+ parts.push(valBuf);
400
+ }
401
+ const tableData = Buffer.concat(parts);
402
+ const result = Buffer.alloc(4 + tableData.length);
403
+ result.writeUInt32BE(tableData.length, 0);
404
+ tableData.copy(result, 4);
405
+ return result;
406
+ }
407
+
408
+ function closeConnection() {
409
+ // Send Connection.Close
410
+ const closePayload = Buffer.alloc(6);
411
+ closePayload.writeUInt16BE(200, 0); // reply code
412
+ closePayload.writeUInt8(0, 2); // reply text (empty)
413
+ closePayload.writeUInt16BE(0, 3); // class
414
+ closePayload.writeUInt16BE(0, 5); // method
415
+ sendMethod(0, 10, 50, closePayload);
416
+ setTimeout(() => sock.destroy(), 500);
417
+ }
418
+
419
+ sock.on("error", (err) => {
420
+ process.stderr.write(err.message);
421
+ process.exit(1);
422
+ });
423
+
424
+ setTimeout(() => { sock.destroy(); process.exit(1); }, 10000);
425
+ `;
426
+
427
+ try {
428
+ const result = execFileSync(process.execPath, ["-e", script], {
429
+ encoding: "utf-8",
430
+ timeout: 15000,
431
+ stdio: ["pipe", "pipe", "pipe"],
432
+ });
433
+ return result;
434
+ } catch {
435
+ return "";
436
+ }
437
+ }
438
+
439
+ push(queue: string, payload: unknown, _delay?: number): string {
440
+ const id = randomUUID();
441
+ const now = new Date().toISOString();
442
+
443
+ const job: QueueJob = {
444
+ id,
445
+ payload,
446
+ status: "pending",
447
+ createdAt: now,
448
+ attempts: 0,
449
+ delayUntil: null,
450
+ };
451
+
452
+ const result = this.execSync("publish", queue, JSON.stringify(job));
453
+ if (!result.includes("__PUBLISHED__")) {
454
+ throw new Error("RabbitMQ publish failed");
455
+ }
456
+ return id;
457
+ }
458
+
459
+ pop(queue: string): QueueJob | null {
460
+ const result = this.execSync("get", queue);
461
+ if (!result || result === "__EMPTY__") return null;
462
+
463
+ try {
464
+ return JSON.parse(result) as QueueJob;
465
+ } catch {
466
+ return null;
467
+ }
468
+ }
469
+
470
+ size(queue: string): number {
471
+ const result = this.execSync("size", queue);
472
+ const num = parseInt(result, 10);
473
+ return isNaN(num) ? 0 : num;
474
+ }
475
+
476
+ clear(queue: string): void {
477
+ this.execSync("purge", queue);
478
+ }
479
+ }
@@ -0,0 +1,107 @@
1
+ import type { Middleware } from "./types.js";
2
+
3
+ /** Per-IP sliding window entry */
4
+ interface RateLimitEntry {
5
+ /** Timestamps of requests within the current window */
6
+ timestamps: number[];
7
+ }
8
+
9
+ /** Configuration for the rate limiter */
10
+ export interface RateLimiterConfig {
11
+ /** Maximum number of requests per window. Default: 100 (or TINA4_RATE_LIMIT env) */
12
+ limit?: number;
13
+ /** Window duration in seconds. Default: 60 (or TINA4_RATE_WINDOW env) */
14
+ windowSeconds?: number;
15
+ /** Cleanup interval in milliseconds. Default: 60000 (1 minute) */
16
+ cleanupIntervalMs?: number;
17
+ }
18
+
19
+ /**
20
+ * Create a rate limiter middleware using a sliding window algorithm.
21
+ * Tracks requests per IP in an in-memory Map.
22
+ *
23
+ * Response headers:
24
+ * X-RateLimit-Limit — Maximum requests per window
25
+ * X-RateLimit-Remaining — Requests remaining in the current window
26
+ * X-RateLimit-Reset — Unix timestamp (seconds) when the window resets
27
+ * Retry-After — Seconds to wait (only when rate limited)
28
+ *
29
+ * Returns 429 Too Many Requests when the limit is exceeded.
30
+ */
31
+ export function rateLimiter(config?: RateLimiterConfig): Middleware {
32
+ const limit = config?.limit
33
+ ?? (process.env.TINA4_RATE_LIMIT ? parseInt(process.env.TINA4_RATE_LIMIT, 10) : 100);
34
+ const windowSeconds = config?.windowSeconds
35
+ ?? (process.env.TINA4_RATE_WINDOW ? parseInt(process.env.TINA4_RATE_WINDOW, 10) : 60);
36
+ const cleanupIntervalMs = config?.cleanupIntervalMs ?? 60_000;
37
+
38
+ const windowMs = windowSeconds * 1000;
39
+ const store = new Map<string, RateLimitEntry>();
40
+
41
+ // Periodic cleanup of expired entries
42
+ const cleanupTimer = setInterval(() => {
43
+ const now = Date.now();
44
+ const cutoff = now - windowMs;
45
+ for (const [ip, entry] of store) {
46
+ entry.timestamps = entry.timestamps.filter((t) => t > cutoff);
47
+ if (entry.timestamps.length === 0) {
48
+ store.delete(ip);
49
+ }
50
+ }
51
+ }, cleanupIntervalMs);
52
+
53
+ // Don't let the cleanup timer keep the process alive
54
+ if (cleanupTimer.unref) {
55
+ cleanupTimer.unref();
56
+ }
57
+
58
+ return (req, res, next) => {
59
+ const now = Date.now();
60
+ const cutoff = now - windowMs;
61
+
62
+ // Extract client IP — check x-forwarded-for, then socket
63
+ const forwarded = req.headers["x-forwarded-for"];
64
+ const ip = (typeof forwarded === "string" ? forwarded.split(",")[0].trim() : undefined)
65
+ ?? req.socket?.remoteAddress
66
+ ?? "unknown";
67
+
68
+ // Get or create entry
69
+ let entry = store.get(ip);
70
+ if (!entry) {
71
+ entry = { timestamps: [] };
72
+ store.set(ip, entry);
73
+ }
74
+
75
+ // Prune old timestamps outside the window
76
+ entry.timestamps = entry.timestamps.filter((t) => t > cutoff);
77
+
78
+ // Calculate reset time (end of window from the oldest request, or now + window)
79
+ const resetTimestamp = entry.timestamps.length > 0
80
+ ? Math.ceil((entry.timestamps[0] + windowMs) / 1000)
81
+ : Math.ceil((now + windowMs) / 1000);
82
+
83
+ const remaining = Math.max(0, limit - entry.timestamps.length);
84
+
85
+ // Set rate limit headers
86
+ res.header("X-RateLimit-Limit", String(limit));
87
+ res.header("X-RateLimit-Remaining", String(Math.max(0, remaining - 1)));
88
+ res.header("X-RateLimit-Reset", String(resetTimestamp));
89
+
90
+ if (entry.timestamps.length >= limit) {
91
+ // Rate limited
92
+ const retryAfter = Math.max(1, resetTimestamp - Math.ceil(now / 1000));
93
+ res.header("Retry-After", String(retryAfter));
94
+ res.header("X-RateLimit-Remaining", "0");
95
+ res({
96
+ error: "Too Many Requests",
97
+ statusCode: 429,
98
+ message: `Rate limit exceeded. Try again in ${retryAfter} seconds.`,
99
+ }, 429);
100
+ return;
101
+ }
102
+
103
+ // Record this request
104
+ entry.timestamps.push(now);
105
+ next();
106
+ };
107
+ }