tina4-nodejs 3.10.76 → 3.10.84

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.
package/CLAUDE.md CHANGED
@@ -121,7 +121,7 @@ The HTTP foundation. Handles request/response lifecycle, route matching, middlew
121
121
  - `auth.ts` — Authentication helpers
122
122
  - `cache.ts` — In-memory caching
123
123
  - `session.ts` — Session management with pluggable handlers. `TINA4_SESSION_SAMESITE` env var (default: Lax)
124
- - `websocket.ts` — WebSocket support with backplane for scaling via Redis pub/sub (`TINA4_WS_BACKPLANE`, `TINA4_WS_BACKPLANE_URL`)
124
+ - `websocket.ts` — WebSocket support with backplane for scaling via Redis pub/sub (`TINA4_WS_BACKPLANE`, `TINA4_WS_BACKPLANE_URL`). Rooms API: `wss.joinRoom(clientId, room)`, `wss.leaveRoom(clientId, room)`, `wss.broadcastToRoom(room, msg, excludeIds?)`, `wss.getRoomConnections(room)`, `wss.roomCount(room)`, `wss.getClientRooms(clientId)`
125
125
  - `queue.ts` — Queue system with pluggable backends
126
126
  - `graphql.ts` — GraphQL engine
127
127
  - `i18n.ts` — Internationalization / localization
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tina4-nodejs",
3
- "version": "3.10.76",
3
+ "version": "3.10.84",
4
4
  "type": "module",
5
5
  "description": "Tina4 for Node.js/TypeScript — 54 built-in features, zero dependencies",
6
6
  "keywords": ["tina4", "framework", "web", "api", "orm", "graphql", "websocket", "typescript"],
@@ -32,31 +32,29 @@ function base64urlDecode(str: string): Buffer {
32
32
  /**
33
33
  * Create a signed JWT token.
34
34
  *
35
- * @param payload - Claims to encode (e.g. `{ userId: 1, role: "admin" }`)
36
- * @param secret - HMAC secret (HS256) or PEM private key (RS256)
35
+ * Secret is always read from `process.env.SECRET`.
36
+ * Algorithm is read from `process.env.TINA4_JWT_ALGORITHM` (default "HS256").
37
+ *
38
+ * @param payload - Claims to encode (e.g. `{ userId: 1, role: "admin" }`)
37
39
  * @param expiresIn - Lifetime in seconds (default 3600)
38
- * @param algorithm - "HS256" or "RS256" (default "HS256")
39
40
  * @returns Signed JWT string: header.payload.signature
40
41
  */
41
42
  export function getToken(
42
43
  payload: Record<string, unknown>,
43
- secret?: string,
44
- expiresIn: number = 60,
45
- algorithm: string = "HS256",
44
+ expiresIn: number = 3600,
46
45
  ): string {
46
+ const secret = process.env.SECRET ?? "";
47
47
  if (!secret) {
48
- secret = process.env.SECRET ?? "";
49
- if (!secret) {
50
- console.warn("Auth: SECRET not set in .env — using blank secret (insecure)");
51
- }
48
+ console.warn("Auth: SECRET not set in .env using blank secret (insecure)");
52
49
  }
50
+ const algorithm = process.env.TINA4_JWT_ALGORITHM ?? "HS256";
53
51
 
54
52
  const header = { alg: algorithm, typ: "JWT" };
55
53
  const now = Math.floor(Date.now() / 1000);
56
54
 
57
55
  const claims: Record<string, unknown> = { ...payload, iat: now };
58
56
  if (expiresIn !== 0) {
59
- claims.exp = now + Math.floor(expiresIn * 60);
57
+ claims.exp = now + expiresIn;
60
58
  }
61
59
 
62
60
  const h = base64urlEncode(Buffer.from(JSON.stringify(header)));
@@ -68,39 +66,37 @@ export function getToken(
68
66
  }
69
67
 
70
68
  /**
71
- * Validate a JWT token and return the decoded payload, or null if invalid/expired.
69
+ * Validate a JWT token and return the decoded payload, or false if invalid/expired.
70
+ *
71
+ * Secret is always read from `process.env.SECRET`.
72
+ * Algorithm is read from `process.env.TINA4_JWT_ALGORITHM` (default "HS256").
72
73
  */
73
- export function validToken(
74
- token: string,
75
- secret?: string,
76
- algorithm: string = "HS256",
77
- ): Record<string, unknown> | null {
74
+ export function validToken(token: string): boolean {
75
+ const secret = process.env.SECRET ?? "";
78
76
  if (!secret) {
79
- secret = process.env.SECRET ?? "";
80
- if (!secret) {
81
- console.warn("Auth: SECRET not set in .env — using blank secret (insecure)");
82
- }
77
+ console.warn("Auth: SECRET not set in .env using blank secret (insecure)");
83
78
  }
79
+ const algorithm = process.env.TINA4_JWT_ALGORITHM ?? "HS256";
84
80
  try {
85
81
  const parts = token.split(".");
86
- if (parts.length !== 3) return null;
82
+ if (parts.length !== 3) return false;
87
83
 
88
84
  const [h, p, sig] = parts;
89
85
  const signingInput = `${h}.${p}`;
90
86
 
91
- if (!verifySignature(signingInput, sig, secret, algorithm)) {
92
- return null;
87
+ if (!verifySignature(signingInput, sig, secret as string, algorithm)) {
88
+ return false;
93
89
  }
94
90
 
95
91
  const payload = JSON.parse(base64urlDecode(p).toString()) as Record<string, unknown>;
96
92
 
97
93
  if (typeof payload.exp === "number" && Date.now() / 1000 > payload.exp) {
98
- return null;
94
+ return false;
99
95
  }
100
96
 
101
- return payload;
97
+ return true;
102
98
  } catch {
103
- return null;
99
+ return false;
104
100
  }
105
101
  }
106
102
 
@@ -205,7 +201,7 @@ export function checkPassword(password: string, hash: string): boolean {
205
201
  * Authorization header. On success, attaches the decoded payload to
206
202
  * `(request as any).auth`. On failure, sends a 401 JSON response.
207
203
  */
208
- export function authMiddleware(secret: string, algorithm: string = "HS256"): Middleware {
204
+ export function authMiddleware(secret?: string, algorithm: string = "HS256"): Middleware {
209
205
  return (req: Tina4Request, res: Tina4Response, next: () => void): void => {
210
206
  const authHeader = req.headers.authorization ?? "";
211
207
 
@@ -215,14 +211,12 @@ export function authMiddleware(secret: string, algorithm: string = "HS256"): Mid
215
211
  }
216
212
 
217
213
  const token = authHeader.slice(7);
218
- const payload = validToken(token, secret, algorithm);
219
-
220
- if (payload === null) {
214
+ if (!validToken(token)) {
221
215
  res({ error: "Unauthorized" }, 401);
222
216
  return;
223
217
  }
224
218
 
225
- (req as any).auth = payload;
219
+ (req as any).auth = getPayload(token);
226
220
  next();
227
221
  };
228
222
  }
@@ -233,24 +227,24 @@ export function authMiddleware(secret: string, algorithm: string = "HS256"): Mid
233
227
  * Refresh a JWT token — validate the existing token then re-sign
234
228
  * with a fresh expiry.
235
229
  *
230
+ * Secret is always read from `process.env.SECRET`.
231
+ *
236
232
  * @param token - Existing JWT to refresh
237
- * @param secret - HMAC secret or PEM key
238
233
  * @param expiresIn - New lifetime in seconds (default 3600)
239
- * @param algorithm - "HS256" or "RS256" (default "HS256")
240
234
  * @returns New signed JWT string, or null if the input token is invalid/expired
241
235
  */
242
236
  export function refreshToken(
243
237
  token: string,
244
- secret?: string,
245
- expiresIn: number = 60,
246
- algorithm: string = "HS256",
238
+ expiresIn: number = 3600,
247
239
  ): string | null {
248
- const payload = validToken(token, secret, algorithm);
249
- if (payload === null) return null;
240
+ if (!validToken(token)) return null;
241
+
242
+ const payload = getPayload(token);
243
+ if (!payload) return null;
250
244
 
251
245
  // Strip standard timing claims so getToken sets fresh ones
252
246
  const { iat: _iat, exp: _exp, ...claims } = payload;
253
- return getToken(claims, secret, expiresIn, algorithm);
247
+ return getToken(claims, expiresIn);
254
248
  }
255
249
 
256
250
  // ── Request Authentication ───────────────────────────────────────
@@ -275,9 +269,8 @@ export function authenticateRequest(
275
269
 
276
270
  const token = authHeader.slice(7);
277
271
 
278
- // Try JWT first
279
- const payload = validToken(token, secret, algorithm);
280
- if (payload !== null) return payload;
272
+ // Try JWT first (secret/algorithm params kept for backward compat but validToken reads from env)
273
+ if (validToken(token)) return getPayload(token);
281
274
 
282
275
  // Fallback: treat Bearer value as API key
283
276
  if (validateApiKey(token)) {
@@ -287,14 +280,6 @@ export function authenticateRequest(
287
280
  return null;
288
281
  }
289
282
 
290
- // ── Backward-Compatible Aliases ──────────────────────────────────
291
-
292
- /** Alias for getToken() — kept for backward compatibility. */
293
- export const createToken = getToken;
294
-
295
- /** Alias for validToken() — kept for backward compatibility. */
296
- export const validateToken = validToken;
297
-
298
283
  // ── API Key Validation ───────────────────────────────────────────
299
284
 
300
285
  /**
@@ -341,7 +326,4 @@ export class Auth {
341
326
  static refreshToken = refreshToken;
342
327
  static authenticateRequest = authenticateRequest;
343
328
  static validateApiKey = validateApiKey;
344
- // Legacy aliases
345
- static createToken = getToken;
346
- static validateToken = validToken;
347
329
  }
@@ -38,7 +38,7 @@ export {
38
38
  APPLICATION_OCTET, TEXT_HTML, TEXT_PLAIN, TEXT_CSV, TEXT_XML,
39
39
  } from "./constants.js";
40
40
  export {
41
- getToken, validToken, createToken, validateToken, getPayload,
41
+ getToken, validToken, getPayload,
42
42
  hashPassword, checkPassword,
43
43
  authMiddleware,
44
44
  refreshToken, authenticateRequest, validateApiKey,
@@ -52,6 +52,8 @@ export { ScssCompiler } from "./scss.js";
52
52
  export type { ScssConfig } from "./scss.js";
53
53
  export { Queue } from "./queue.js";
54
54
  export type { QueueConfig, QueueJob, ProcessOptions } from "./queue.js";
55
+ export { createJob } from "./job.js";
56
+ export type { JobData, JobQueueBridge } from "./job.js";
55
57
  export { GraphQL, ParseError } from "./graphql.js";
56
58
  export type { GraphQLField, ResolverFn, GraphQLResult } from "./graphql.js";
57
59
  export {
@@ -79,6 +81,7 @@ export { renderErrorOverlay, renderProductionError, isDebugMode } from "./errorO
79
81
  export { AI_TOOLS, isInstalled, showMenu, installSelected, installAll, generateContext } from "./ai.js";
80
82
  export type { AiTool } from "./ai.js";
81
83
  export type { ImapMessage, ImapFullMessage } from "./messenger.js";
84
+ export { LiteBackend } from "./queueBackends/liteBackend.js";
82
85
  export { RabbitMQBackend } from "./queueBackends/rabbitmqBackend.js";
83
86
  export type { RabbitMQConfig } from "./queueBackends/rabbitmqBackend.js";
84
87
  export { KafkaBackend } from "./queueBackends/kafkaBackend.js";
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Tina4 Queue Job — a single queue job with lifecycle methods.
3
+ */
4
+
5
+ // ── Types ────────────────────────────────────────────────────
6
+
7
+ export interface JobData {
8
+ id: string;
9
+ payload: unknown;
10
+ status: "pending" | "reserved" | "failed" | "dead" | "completed";
11
+ createdAt: string;
12
+ attempts: number;
13
+ delayUntil: string | null;
14
+ priority: number;
15
+ topic: string;
16
+ error?: string;
17
+ }
18
+
19
+ export interface JobLifecycle {
20
+ /** Mark this job as completed. */
21
+ complete(): void;
22
+ /** Mark this job as failed with a reason. */
23
+ fail(reason?: string): void;
24
+ /** Reject this job with a reason. Alias for fail(). */
25
+ reject(reason?: string): void;
26
+ /** Re-queue this job with incremented attempts and optional delay. */
27
+ retry(delaySeconds?: number): void;
28
+ /** Return job fields as a flat array of values. */
29
+ toArray(): unknown[];
30
+ /** Return job as a plain object. */
31
+ toHash(): Record<string, unknown>;
32
+ /** Return job as a JSON string. */
33
+ toJson(): string;
34
+ }
35
+
36
+ export type QueueJob = JobData & JobLifecycle;
37
+
38
+ // ── Job factory ──────────────────────────────────────────────
39
+
40
+ export interface JobQueueBridge {
41
+ _failJob(topic: string, job: QueueJob, reason: string, maxRetries: number): void;
42
+ _retryJob(topic: string, job: QueueJob, delaySeconds?: number): void;
43
+ getMaxRetries(): number;
44
+ }
45
+
46
+ /** Create a QueueJob with lifecycle methods bound to a Queue instance. */
47
+ export function createJob(data: JobData, queue: JobQueueBridge): QueueJob {
48
+ const job: QueueJob = {
49
+ ...data,
50
+ complete() {
51
+ job.status = "completed";
52
+ },
53
+ fail(reason = "") {
54
+ job.status = "failed";
55
+ job.error = reason;
56
+ job.attempts = (job.attempts || 0) + 1;
57
+ queue._failJob(job.topic, job, reason, queue.getMaxRetries());
58
+ },
59
+ reject(reason = "") {
60
+ job.fail(reason);
61
+ },
62
+ retry(delaySeconds?: number) {
63
+ queue._retryJob(job.topic, job, delaySeconds);
64
+ },
65
+ toArray() {
66
+ return [job.id, job.topic, job.payload, job.priority, job.attempts];
67
+ },
68
+ toHash() {
69
+ return {
70
+ id: job.id,
71
+ topic: job.topic,
72
+ payload: job.payload,
73
+ priority: job.priority,
74
+ attempts: job.attempts,
75
+ status: job.status,
76
+ createdAt: job.createdAt,
77
+ delayUntil: job.delayUntil,
78
+ error: job.error,
79
+ };
80
+ },
81
+ toJson() {
82
+ return JSON.stringify(job.toHash());
83
+ },
84
+ };
85
+ return job;
86
+ }
@@ -1,5 +1,5 @@
1
1
  import type { Tina4Request, Tina4Response, Middleware } from "./types.js";
2
- import { validToken } from "./auth.js";
2
+ import { validToken, getPayload } from "./auth.js";
3
3
 
4
4
  export class MiddlewareChain {
5
5
  private middlewares: Middleware[] = [];
@@ -455,9 +455,7 @@ export class CsrfMiddleware {
455
455
  if (authHeader.startsWith("Bearer ")) {
456
456
  const bearerToken = authHeader.slice(7).trim();
457
457
  if (bearerToken) {
458
- const secret = process.env.SECRET || "tina4-default-secret";
459
- const payload = validToken(bearerToken, secret);
460
- if (payload !== null) {
458
+ if (validToken(bearerToken)) {
461
459
  return [req, res];
462
460
  }
463
461
  }
@@ -494,10 +492,7 @@ export class CsrfMiddleware {
494
492
  }
495
493
 
496
494
  // Validate the token
497
- const secret = process.env.SECRET || "tina4-default-secret";
498
- const payload = validToken(token, secret);
499
-
500
- if (payload === null) {
495
+ if (!validToken(token)) {
501
496
  res({
502
497
  error: "CSRF_INVALID",
503
498
  message: "Invalid or missing form token",
@@ -505,6 +500,8 @@ export class CsrfMiddleware {
505
500
  return [req, res];
506
501
  }
507
502
 
503
+ const payload = getPayload(token) ?? {};
504
+
508
505
  // Session binding — if token has session_id, verify it matches
509
506
  const tokenSessionId = payload.session_id as string | undefined;
510
507
  if (tokenSessionId) {