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,286 @@
1
+ /**
2
+ * Tina4 MongoDB Session Handler — MongoDB wire protocol via raw TCP, zero dependencies.
3
+ *
4
+ * Stores session data in MongoDB using the MongoDB wire protocol directly.
5
+ * No `mongodb` or `mongoose` npm package required.
6
+ *
7
+ * Configure via environment variables:
8
+ * TINA4_SESSION_MONGO_HOST (default: "127.0.0.1")
9
+ * TINA4_SESSION_MONGO_PORT (default: 27017)
10
+ * TINA4_SESSION_MONGO_URI (overrides host/port if set)
11
+ * TINA4_SESSION_MONGO_USERNAME (optional)
12
+ * TINA4_SESSION_MONGO_PASSWORD (optional)
13
+ * TINA4_SESSION_MONGO_DB (default: "tina4_sessions")
14
+ * TINA4_SESSION_MONGO_COLLECTION (default: "sessions")
15
+ */
16
+ import { execFileSync } from "node:child_process";
17
+ import type { SessionHandler } from "../session.js";
18
+
19
+ interface SessionData {
20
+ _created: number;
21
+ _accessed: number;
22
+ [key: string]: unknown;
23
+ }
24
+
25
+ export interface MongoSessionConfig {
26
+ host?: string;
27
+ port?: number;
28
+ uri?: string;
29
+ username?: string;
30
+ password?: string;
31
+ database?: string;
32
+ collection?: string;
33
+ }
34
+
35
+ /**
36
+ * MongoDB session handler using raw TCP (MongoDB wire protocol).
37
+ *
38
+ * Uses synchronous socket communication via child process — no external
39
+ * MongoDB client library required. Stores session data as BSON documents
40
+ * with TTL index support.
41
+ */
42
+ export class MongoSessionHandler implements SessionHandler {
43
+ private host: string;
44
+ private port: number;
45
+ private uri: string;
46
+ private username: string;
47
+ private password: string;
48
+ private database: string;
49
+ private collection: string;
50
+
51
+ constructor(config?: MongoSessionConfig) {
52
+ this.host = config?.host
53
+ ?? process.env.TINA4_SESSION_MONGO_HOST
54
+ ?? "127.0.0.1";
55
+ this.port = config?.port
56
+ ?? (process.env.TINA4_SESSION_MONGO_PORT
57
+ ? parseInt(process.env.TINA4_SESSION_MONGO_PORT, 10)
58
+ : 27017);
59
+ this.uri = config?.uri
60
+ ?? process.env.TINA4_SESSION_MONGO_URI
61
+ ?? "";
62
+ this.username = config?.username
63
+ ?? process.env.TINA4_SESSION_MONGO_USERNAME
64
+ ?? "";
65
+ this.password = config?.password
66
+ ?? process.env.TINA4_SESSION_MONGO_PASSWORD
67
+ ?? "";
68
+ this.database = config?.database
69
+ ?? process.env.TINA4_SESSION_MONGO_DB
70
+ ?? "tina4_sessions";
71
+ this.collection = config?.collection
72
+ ?? process.env.TINA4_SESSION_MONGO_COLLECTION
73
+ ?? "sessions";
74
+ }
75
+
76
+ /**
77
+ * Execute a MongoDB command synchronously via a child process.
78
+ *
79
+ * Uses the MongoDB wire protocol (OP_MSG) to communicate with the server.
80
+ * For simplicity, we use the `runCommand` approach with JSON serialization
81
+ * of BSON documents using a minimal BSON encoder.
82
+ */
83
+ private execSync(command: string, args: string): string {
84
+ const script = `
85
+ const net = require("node:net");
86
+ const host = ${JSON.stringify(this.uri ? this.parseUri().host : this.host)};
87
+ const port = ${this.uri ? this.parseUri().port : this.port};
88
+ const database = ${JSON.stringify(this.database)};
89
+ const collection = ${JSON.stringify(this.collection)};
90
+ const command = ${JSON.stringify(command)};
91
+ const args = ${JSON.stringify(args)};
92
+
93
+ // Minimal BSON encoder/decoder for session operations
94
+ function encodeBsonDocument(obj) {
95
+ const parts = [];
96
+ for (const [key, value] of Object.entries(obj)) {
97
+ if (typeof value === "string") {
98
+ const keyBuf = Buffer.from(key + "\\0", "utf-8");
99
+ const valBuf = Buffer.from(value, "utf-8");
100
+ const entry = Buffer.alloc(1 + keyBuf.length + 4 + valBuf.length + 1);
101
+ entry.writeUInt8(2, 0); // string type
102
+ keyBuf.copy(entry, 1);
103
+ entry.writeInt32LE(valBuf.length + 1, 1 + keyBuf.length);
104
+ valBuf.copy(entry, 1 + keyBuf.length + 4);
105
+ entry.writeUInt8(0, entry.length - 1);
106
+ parts.push(entry);
107
+ } else if (typeof value === "number") {
108
+ const keyBuf = Buffer.from(key + "\\0", "utf-8");
109
+ if (Number.isInteger(value)) {
110
+ const entry = Buffer.alloc(1 + keyBuf.length + 4);
111
+ entry.writeUInt8(16, 0); // int32 type
112
+ keyBuf.copy(entry, 1);
113
+ entry.writeInt32LE(value, 1 + keyBuf.length);
114
+ parts.push(entry);
115
+ } else {
116
+ const entry = Buffer.alloc(1 + keyBuf.length + 8);
117
+ entry.writeUInt8(1, 0); // double type
118
+ keyBuf.copy(entry, 1);
119
+ entry.writeDoubleBE(value, 1 + keyBuf.length);
120
+ parts.push(entry);
121
+ }
122
+ } else if (typeof value === "object" && value !== null) {
123
+ const keyBuf = Buffer.from(key + "\\0", "utf-8");
124
+ const subDoc = encodeBsonDocument(value);
125
+ const entry = Buffer.alloc(1 + keyBuf.length + subDoc.length);
126
+ entry.writeUInt8(3, 0); // document type
127
+ keyBuf.copy(entry, 1);
128
+ subDoc.copy(entry, 1 + keyBuf.length);
129
+ parts.push(entry);
130
+ }
131
+ }
132
+ const body = Buffer.concat(parts);
133
+ const doc = Buffer.alloc(4 + body.length + 1);
134
+ doc.writeInt32LE(doc.length, 0);
135
+ body.copy(doc, 4);
136
+ doc.writeUInt8(0, doc.length - 1);
137
+ return doc;
138
+ }
139
+
140
+ // Use MongoDB OP_MSG (opcode 2013) with runCommand
141
+ function buildOpMsg(dbName, cmd) {
142
+ const cmdDoc = Object.assign({ "$db": dbName }, cmd);
143
+ const body = encodeBsonDocument(cmdDoc);
144
+
145
+ // OP_MSG: flagBits(4) + kind(1) + body
146
+ const section = Buffer.alloc(1 + body.length);
147
+ section.writeUInt8(0, 0); // kind 0 = body
148
+ body.copy(section, 1);
149
+
150
+ const msgHeader = Buffer.alloc(16 + 4 + section.length);
151
+ msgHeader.writeInt32LE(msgHeader.length, 0); // messageLength
152
+ msgHeader.writeInt32LE(1, 4); // requestID
153
+ msgHeader.writeInt32LE(0, 8); // responseTo
154
+ msgHeader.writeInt32LE(2013, 12); // opCode = OP_MSG
155
+ msgHeader.writeUInt32LE(0, 16); // flagBits
156
+ section.copy(msgHeader, 20);
157
+
158
+ return msgHeader;
159
+ }
160
+
161
+ // For this simplified implementation, we use a TCP connection
162
+ // and send/receive raw OP_MSG frames
163
+ const sock = net.createConnection({ host, port }, () => {
164
+ let cmd;
165
+ const parsedArgs = JSON.parse(args);
166
+
167
+ if (command === "find") {
168
+ cmd = {
169
+ find: collection,
170
+ filter: parsedArgs.filter,
171
+ limit: 1
172
+ };
173
+ } else if (command === "update") {
174
+ cmd = {
175
+ update: collection,
176
+ updates: [{ q: parsedArgs.filter, u: { "$set": parsedArgs.data }, upsert: true }]
177
+ };
178
+ } else if (command === "delete") {
179
+ cmd = {
180
+ delete: collection,
181
+ deletes: [{ q: parsedArgs.filter, limit: 1 }]
182
+ };
183
+ }
184
+
185
+ const msg = buildOpMsg(database, cmd);
186
+ sock.write(msg);
187
+ });
188
+
189
+ let buffer = Buffer.alloc(0);
190
+ sock.on("data", (chunk) => {
191
+ buffer = Buffer.concat([buffer, chunk]);
192
+ if (buffer.length >= 4) {
193
+ const msgLen = buffer.readInt32LE(0);
194
+ if (buffer.length >= msgLen) {
195
+ // Parse response — extract body section
196
+ // Header is 16 bytes + 4 bytes flagBits + 1 byte section kind + BSON doc
197
+ if (buffer.length > 21) {
198
+ const bsonStart = 21;
199
+ const bsonLen = buffer.readInt32LE(bsonStart);
200
+ const bsonDoc = buffer.subarray(bsonStart, bsonStart + bsonLen);
201
+
202
+ // Simple BSON parser — just look for the session data string
203
+ const rawStr = bsonDoc.toString("utf-8", 4);
204
+
205
+ if (command === "find") {
206
+ // Look for cursor.firstBatch documents
207
+ // For simplicity, extract JSON-like data from response
208
+ process.stdout.write(rawStr);
209
+ } else {
210
+ process.stdout.write("__OK__");
211
+ }
212
+ } else {
213
+ process.stdout.write("__EMPTY__");
214
+ }
215
+ sock.destroy();
216
+ }
217
+ }
218
+ });
219
+
220
+ sock.on("error", (err) => {
221
+ process.stderr.write(err.message);
222
+ process.exit(1);
223
+ });
224
+
225
+ setTimeout(() => { sock.destroy(); process.exit(1); }, 5000);
226
+ `;
227
+
228
+ try {
229
+ const result = execFileSync(process.execPath, ["-e", script], {
230
+ encoding: "utf-8",
231
+ timeout: 8000,
232
+ stdio: ["pipe", "pipe", "pipe"],
233
+ });
234
+ return result;
235
+ } catch {
236
+ return "";
237
+ }
238
+ }
239
+
240
+ private parseUri(): { host: string; port: number } {
241
+ if (!this.uri) return { host: this.host, port: this.port };
242
+ try {
243
+ // mongodb://host:port/db
244
+ const match = this.uri.match(/mongodb:\/\/([^/:]+):?(\d+)?/);
245
+ if (match) {
246
+ return {
247
+ host: match[1],
248
+ port: match[2] ? parseInt(match[2], 10) : 27017,
249
+ };
250
+ }
251
+ } catch { /* ignore */ }
252
+ return { host: this.host, port: this.port };
253
+ }
254
+
255
+ read(sessionId: string): SessionData | null {
256
+ const result = this.execSync("find", JSON.stringify({
257
+ filter: { _id: sessionId },
258
+ }));
259
+
260
+ if (!result || result === "__EMPTY__") return null;
261
+
262
+ // Try to extract session data from the BSON response
263
+ try {
264
+ // Look for the JSON data pattern in the response
265
+ const dataMatch = result.match(/"data"\s*:\s*(\{[^}]+\})/);
266
+ if (dataMatch) {
267
+ return JSON.parse(dataMatch[1]) as SessionData;
268
+ }
269
+ } catch { /* ignore */ }
270
+
271
+ return null;
272
+ }
273
+
274
+ write(sessionId: string, data: SessionData, _ttl: number): void {
275
+ this.execSync("update", JSON.stringify({
276
+ filter: { _id: sessionId },
277
+ data: { _id: sessionId, data: JSON.stringify(data), updatedAt: new Date().toISOString() },
278
+ }));
279
+ }
280
+
281
+ destroy(sessionId: string): void {
282
+ this.execSync("delete", JSON.stringify({
283
+ filter: { _id: sessionId },
284
+ }));
285
+ }
286
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Tina4 Valkey Session Handler — Valkey (Redis-compatible) via raw TCP, zero dependencies.
3
+ *
4
+ * Same as the Redis handler but uses VALKEY-prefixed configuration variables.
5
+ * Valkey is a Redis-compatible key-value store fork.
6
+ *
7
+ * Configure via environment variables:
8
+ * TINA4_SESSION_VALKEY_HOST (default: "127.0.0.1")
9
+ * TINA4_SESSION_VALKEY_PORT (default: 6379)
10
+ * TINA4_SESSION_VALKEY_PASSWORD (optional)
11
+ * TINA4_SESSION_VALKEY_PREFIX (default: "tina4:session:")
12
+ * TINA4_SESSION_VALKEY_DB (default: 0)
13
+ */
14
+ import { execFileSync } from "node:child_process";
15
+ import type { SessionHandler } from "../session.js";
16
+
17
+ interface SessionData {
18
+ _created: number;
19
+ _accessed: number;
20
+ [key: string]: unknown;
21
+ }
22
+
23
+ export interface ValkeySessionConfig {
24
+ host?: string;
25
+ port?: number;
26
+ password?: string;
27
+ prefix?: string;
28
+ db?: number;
29
+ }
30
+
31
+ /**
32
+ * Valkey session handler using raw TCP (RESP protocol).
33
+ *
34
+ * Uses synchronous socket communication — no external Valkey/Redis client required.
35
+ * Stores session data as JSON strings with Valkey TTL for automatic expiry.
36
+ *
37
+ * Valkey uses the same RESP protocol as Redis, so this handler is functionally
38
+ * identical to RedisSessionHandler but with VALKEY config variable names.
39
+ */
40
+ export class ValkeySessionHandler implements SessionHandler {
41
+ private host: string;
42
+ private port: number;
43
+ private password: string;
44
+ private prefix: string;
45
+ private db: number;
46
+
47
+ constructor(config?: ValkeySessionConfig) {
48
+ this.host = config?.host
49
+ ?? process.env.TINA4_SESSION_VALKEY_HOST
50
+ ?? "127.0.0.1";
51
+ this.port = config?.port
52
+ ?? (process.env.TINA4_SESSION_VALKEY_PORT
53
+ ? parseInt(process.env.TINA4_SESSION_VALKEY_PORT, 10)
54
+ : 6379);
55
+ this.password = config?.password
56
+ ?? process.env.TINA4_SESSION_VALKEY_PASSWORD
57
+ ?? "";
58
+ this.prefix = config?.prefix
59
+ ?? process.env.TINA4_SESSION_VALKEY_PREFIX
60
+ ?? "tina4:session:";
61
+ this.db = config?.db
62
+ ?? (process.env.TINA4_SESSION_VALKEY_DB
63
+ ? parseInt(process.env.TINA4_SESSION_VALKEY_DB, 10)
64
+ : 0);
65
+ }
66
+
67
+ /**
68
+ * Execute a RESP command synchronously via a short-lived TCP connection.
69
+ *
70
+ * Returns the raw RESP response string.
71
+ */
72
+ private execSync(args: string[]): string {
73
+ const script = `
74
+ const net = require("node:net");
75
+ const host = ${JSON.stringify(this.host)};
76
+ const port = ${this.port};
77
+ const password = ${JSON.stringify(this.password)};
78
+ const db = ${this.db};
79
+ const args = ${JSON.stringify(args)};
80
+
81
+ function buildCommand(a) {
82
+ let cmd = "*" + a.length + "\\r\\n";
83
+ for (const s of a) cmd += "$" + Buffer.byteLength(s) + "\\r\\n" + s + "\\r\\n";
84
+ return cmd;
85
+ }
86
+
87
+ function parseResp(buf) {
88
+ const str = buf.toString("utf-8");
89
+ if (str.startsWith("+")) return str.slice(1).split("\\r\\n")[0];
90
+ if (str.startsWith("-")) return "ERR:" + str.slice(1).split("\\r\\n")[0];
91
+ if (str.startsWith(":")) return str.slice(1).split("\\r\\n")[0];
92
+ if (str.startsWith("$-1")) return null;
93
+ if (str.startsWith("$")) {
94
+ const nl = str.indexOf("\\r\\n");
95
+ const len = parseInt(str.slice(1, nl), 10);
96
+ const start = nl + 2;
97
+ return str.slice(start, start + len);
98
+ }
99
+ return str;
100
+ }
101
+
102
+ const sock = net.createConnection({ host, port }, () => {
103
+ let commands = "";
104
+ if (password) commands += buildCommand(["AUTH", password]);
105
+ if (db !== 0) commands += buildCommand(["SELECT", String(db)]);
106
+ commands += buildCommand(args);
107
+ sock.write(commands);
108
+ });
109
+
110
+ let buffer = Buffer.alloc(0);
111
+ sock.on("data", (chunk) => {
112
+ buffer = Buffer.concat([buffer, chunk]);
113
+ });
114
+ sock.on("end", () => {
115
+ // Parse last response (skip AUTH/SELECT responses)
116
+ const lines = buffer.toString("utf-8").split("\\r\\n");
117
+ let responses = [];
118
+ let i = 0;
119
+ while (i < lines.length) {
120
+ const line = lines[i];
121
+ if (!line) { i++; continue; }
122
+ if (line.startsWith("+") || line.startsWith("-") || line.startsWith(":")) {
123
+ responses.push(line);
124
+ i++;
125
+ } else if (line.startsWith("$")) {
126
+ const len = parseInt(line.slice(1), 10);
127
+ if (len === -1) { responses.push(null); i++; }
128
+ else { responses.push(lines[i+1] || ""); i += 2; }
129
+ } else { i++; }
130
+ }
131
+ // The last response is our actual command result
132
+ const result = responses[responses.length - 1];
133
+ if (result === null) process.stdout.write("__NULL__");
134
+ else if (typeof result === "string" && result.startsWith("-")) process.stdout.write("__ERR__" + result);
135
+ else process.stdout.write(String(result ?? "__NULL__"));
136
+ });
137
+ sock.on("error", (err) => {
138
+ process.stderr.write(err.message);
139
+ process.exit(1);
140
+ });
141
+ setTimeout(() => { sock.destroy(); process.exit(1); }, 3000);
142
+ `;
143
+
144
+ try {
145
+ const result = execFileSync(process.execPath, ["-e", script], {
146
+ encoding: "utf-8",
147
+ timeout: 5000,
148
+ stdio: ["pipe", "pipe", "pipe"],
149
+ });
150
+ if (result === "__NULL__") return "";
151
+ if (result.startsWith("__ERR__")) return "";
152
+ return result;
153
+ } catch {
154
+ return "";
155
+ }
156
+ }
157
+
158
+ private key(sessionId: string): string {
159
+ return `${this.prefix}${sessionId}`;
160
+ }
161
+
162
+ read(sessionId: string): SessionData | null {
163
+ const raw = this.execSync(["GET", this.key(sessionId)]);
164
+ if (!raw) return null;
165
+ try {
166
+ return JSON.parse(raw) as SessionData;
167
+ } catch {
168
+ return null;
169
+ }
170
+ }
171
+
172
+ write(sessionId: string, data: SessionData, ttl: number): void {
173
+ const json = JSON.stringify(data);
174
+ if (ttl > 0) {
175
+ this.execSync(["SETEX", this.key(sessionId), String(ttl), json]);
176
+ } else {
177
+ this.execSync(["SET", this.key(sessionId), json]);
178
+ }
179
+ }
180
+
181
+ destroy(sessionId: string): void {
182
+ this.execSync(["DEL", this.key(sessionId)]);
183
+ }
184
+ }
@@ -0,0 +1,58 @@
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
+ import { join, extname } from "node:path";
3
+ import type { Tina4Request, Tina4Response } from "./types.js";
4
+
5
+ const MIME_TYPES: Record<string, string> = {
6
+ ".html": "text/html; charset=utf-8",
7
+ ".css": "text/css; charset=utf-8",
8
+ ".js": "application/javascript; charset=utf-8",
9
+ ".json": "application/json; charset=utf-8",
10
+ ".png": "image/png",
11
+ ".jpg": "image/jpeg",
12
+ ".jpeg": "image/jpeg",
13
+ ".gif": "image/gif",
14
+ ".webp": "image/webp",
15
+ ".svg": "image/svg+xml",
16
+ ".ico": "image/x-icon",
17
+ ".woff": "font/woff",
18
+ ".woff2": "font/woff2",
19
+ ".ttf": "font/ttf",
20
+ ".txt": "text/plain; charset=utf-8",
21
+ ".xml": "application/xml",
22
+ ".pdf": "application/pdf",
23
+ };
24
+
25
+ export function tryServeStatic(
26
+ staticDir: string,
27
+ req: Tina4Request,
28
+ res: Tina4Response
29
+ ): boolean {
30
+ const url = req.url ?? "/";
31
+ const pathname = url.split("?")[0];
32
+
33
+ // Try exact file match, then index.html for directory requests
34
+ const candidates = [
35
+ join(staticDir, pathname),
36
+ join(staticDir, pathname, "index.html"),
37
+ ];
38
+
39
+ for (const filePath of candidates) {
40
+ if (!existsSync(filePath)) continue;
41
+
42
+ const stat = statSync(filePath);
43
+ if (!stat.isFile()) continue;
44
+
45
+ // Prevent directory traversal
46
+ if (!filePath.startsWith(staticDir)) continue;
47
+
48
+ const ext = extname(filePath);
49
+ const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
50
+
51
+ res.raw.setHeader("Content-Type", contentType);
52
+ res.raw.setHeader("Content-Length", stat.size);
53
+ res.raw.end(readFileSync(filePath));
54
+ return true;
55
+ }
56
+
57
+ return false;
58
+ }