tina4-nodejs 3.13.141 → 3.13.143

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 (67) hide show
  1. package/CLAUDE.md +28 -2
  2. package/package.json +1 -1
  3. package/packages/cli/dist/bin.js +817 -538
  4. package/packages/cli/src/bin.ts +4 -14
  5. package/packages/cli/src/commands/build.ts +2 -18
  6. package/packages/cli/src/commands/generate.ts +9 -24
  7. package/packages/cli/src/commands/init.ts +18 -8
  8. package/packages/cli/src/commands/lint.ts +3 -13
  9. package/packages/cli/src/commands/migrate.ts +2 -6
  10. package/packages/cli/src/commands/migrateRollback.ts +2 -6
  11. package/packages/cli/src/commands/migrateStatus.ts +2 -6
  12. package/packages/cli/src/commands/queue.ts +2 -30
  13. package/packages/cli/src/util.ts +90 -0
  14. package/packages/core/dist/index.js +592 -262
  15. package/packages/core/gallery/auth/src/routes/gallery/auth/get.ts +27 -11
  16. package/packages/core/gallery/queue/src/lib/queueDb.ts +11 -0
  17. package/packages/core/gallery/queue/src/routes/api/gallery/queue/consume/post.ts +2 -5
  18. package/packages/core/gallery/queue/src/routes/api/gallery/queue/fail/post.ts +2 -5
  19. package/packages/core/gallery/queue/src/routes/gallery/queue/get.ts +20 -7
  20. package/packages/core/gallery/templates/src/templates/gallery_page.twig +34 -15
  21. package/packages/core/src/csp.ts +138 -0
  22. package/packages/core/src/devAdmin.ts +3 -12
  23. package/packages/core/src/dispatchPipeline.ts +2 -18
  24. package/packages/core/src/errorOverlay.ts +80 -37
  25. package/packages/core/src/etag.ts +24 -0
  26. package/packages/core/src/feedback.ts +2 -11
  27. package/packages/core/src/index.ts +10 -0
  28. package/packages/core/src/middleware.ts +20 -6
  29. package/packages/core/src/response.ts +14 -3
  30. package/packages/core/src/server.ts +52 -15
  31. package/packages/core/src/session.ts +174 -71
  32. package/packages/core/src/sessionHandlers/respSessionHandler.ts +94 -0
  33. package/packages/core/src/sessionHandlers/valkeyHandler.ts +22 -73
  34. package/packages/core/src/sso.ts +1 -1
  35. package/packages/core/src/static.ts +1 -13
  36. package/packages/core/src/types.ts +8 -0
  37. package/packages/core/src/upstreamResponse.ts +31 -0
  38. package/packages/core/src/websocket.ts +48 -7
  39. package/packages/core/templates/errors/302.twig +1 -1
  40. package/packages/core/templates/errors/401.twig +2 -2
  41. package/packages/core/templates/errors/403.twig +1 -1
  42. package/packages/core/templates/errors/404.twig +1 -1
  43. package/packages/core/templates/errors/500.twig +1 -1
  44. package/packages/core/templates/errors/502.twig +2 -2
  45. package/packages/core/templates/errors/503.twig +2 -2
  46. package/packages/core/templates/errors/base.twig +4 -1
  47. package/packages/frond/dist/index.js +2 -0
  48. package/packages/frond/src/engine.ts +23 -0
  49. package/packages/orm/dist/index.js +593 -271
  50. package/packages/orm/src/graph/adapters/bolt.ts +8 -29
  51. package/packages/orm/src/graph/adapters/rowMapping.ts +57 -0
  52. package/packages/orm/src/graph/adapters/ultipa.ts +8 -30
  53. package/packages/swagger/dist/index.js +4 -4
  54. package/packages/swagger/src/ui.ts +7 -4
  55. package/types/cli/src/util.d.ts +28 -0
  56. package/types/core/src/csp.d.ts +42 -0
  57. package/types/core/src/etag.d.ts +9 -0
  58. package/types/core/src/index.d.ts +1 -0
  59. package/types/core/src/middleware.d.ts +6 -0
  60. package/types/core/src/session.d.ts +64 -21
  61. package/types/core/src/sessionHandlers/respSessionHandler.d.ts +52 -0
  62. package/types/core/src/sessionHandlers/valkeyHandler.d.ts +2 -26
  63. package/types/core/src/types.d.ts +8 -0
  64. package/types/core/src/upstreamResponse.d.ts +10 -0
  65. package/types/core/src/websocket.d.ts +21 -0
  66. package/types/frond/src/engine.d.ts +7 -0
  67. package/types/orm/src/graph/adapters/rowMapping.d.ts +26 -0
@@ -321,6 +321,33 @@ export function parseFrame(
321
321
  return { fin: !!fin, opcode, payload: Buffer.from(payload), bytesConsumed: offset + payloadLen };
322
322
  }
323
323
 
324
+ /**
325
+ * Reassembles fragmented messages (RFC 6455 section 5.4). A browser sends a large message - a pasted image in a chat,
326
+ * base64 inside JSON - as a TEXT or BINARY frame with FIN clear, then CONTINUATION frames; control frames may arrive
327
+ * between the pieces. Returns a complete message when one is done, a control frame as it comes, and null while a
328
+ * message is still arriving. A CONTINUATION with no message started is dropped. Mirrors the Python master
329
+ * (tina4_python.websocket _handle_frame). One per connection.
330
+ */
331
+ export class MessageAssembler {
332
+ private parts: Buffer[] = [];
333
+ private opcode = 0;
334
+
335
+ push(frame: { fin: boolean; opcode: number; payload: Buffer }): { opcode: number; payload: Buffer } | null {
336
+ if (frame.opcode >= 0x8) return { opcode: frame.opcode, payload: frame.payload }; // control frames are never fragmented
337
+ if (frame.opcode === OP_CONTINUATION) {
338
+ if (!this.opcode) return null;
339
+ this.parts.push(frame.payload);
340
+ if (!frame.fin) return null;
341
+ const whole = { opcode: this.opcode, payload: Buffer.concat(this.parts) };
342
+ this.parts = []; this.opcode = 0;
343
+ return whole;
344
+ }
345
+ if (frame.fin) { this.parts = []; this.opcode = 0; return { opcode: frame.opcode, payload: frame.payload }; }
346
+ this.opcode = frame.opcode; this.parts = [frame.payload];
347
+ return null;
348
+ }
349
+ }
350
+
324
351
  // ── WebSocket Server ─────────────────────────────────────────
325
352
 
326
353
  export class WebSocketServer {
@@ -854,6 +881,13 @@ export class WebSocketServer {
854
881
  });
855
882
  }
856
883
 
884
+ private readonly assemblers = new WeakMap<WebSocketClient, MessageAssembler>();
885
+ private assemblerFor(client: WebSocketClient): MessageAssembler {
886
+ let a = this.assemblers.get(client);
887
+ if (!a) { a = new MessageAssembler(); this.assemblers.set(client, a); }
888
+ return a;
889
+ }
890
+
857
891
  private processBuffer(
858
892
  client: WebSocketClient,
859
893
  buffer: Buffer,
@@ -868,17 +902,20 @@ export class WebSocketServer {
868
902
  remaining = remaining.subarray(frame.bytesConsumed);
869
903
  client.lastActivity = Date.now(); // mark activity for the idle reaper
870
904
 
871
- switch (frame.opcode) {
905
+ // a fragmented message is delivered once, whole (MessageAssembler)
906
+ const whole = this.assemblerFor(client).push(frame);
907
+ if (!whole) continue;
908
+ switch (whole.opcode) {
872
909
  case OP_TEXT:
873
- this.emit("message", client, frame.payload.toString("utf-8"));
910
+ this.emit("message", client, whole.payload.toString("utf-8"));
874
911
  break;
875
912
 
876
913
  case OP_BINARY:
877
- this.emit("message", client, frame.payload);
914
+ this.emit("message", client, whole.payload);
878
915
  break;
879
916
 
880
917
  case OP_PING: {
881
- const pongFrame = buildFrame(OP_PONG, frame.payload);
918
+ const pongFrame = buildFrame(OP_PONG, whole.payload);
882
919
  try {
883
920
  client.socket.write(pongFrame);
884
921
  } catch {
@@ -1243,15 +1280,19 @@ export function serveWebSocketRoute(req: IncomingMessage, socket: Socket, head:
1243
1280
  .finally(() => wsRouteManager.remove(conn.id));
1244
1281
  };
1245
1282
 
1283
+ const assembler = new MessageAssembler();
1246
1284
  socket.on("data", (chunk: Buffer) => {
1247
1285
  buffer = Buffer.concat([buffer, chunk]);
1248
1286
  while (buffer.length > 0) {
1249
1287
  const frame = parseFrame(buffer);
1250
1288
  if (!frame) break;
1251
1289
  buffer = buffer.subarray(frame.bytesConsumed);
1252
- switch (frame.opcode) {
1290
+ // a fragmented message is delivered once, whole (MessageAssembler)
1291
+ const whole = assembler.push(frame);
1292
+ if (!whole) continue;
1293
+ switch (whole.opcode) {
1253
1294
  case OP_TEXT: {
1254
- const text = frame.payload.toString("utf-8");
1295
+ const text = whole.payload.toString("utf-8");
1255
1296
  void Promise.resolve()
1256
1297
  .then(() => (conn._onMessage ? conn._onMessage(text) : handler(conn, "message", text)))
1257
1298
  .catch((e) => Log.error(`WebSocket message handler error: ${(e as Error).message}`));
@@ -1259,7 +1300,7 @@ export function serveWebSocketRoute(req: IncomingMessage, socket: Socket, head:
1259
1300
  }
1260
1301
  case OP_PING:
1261
1302
  try {
1262
- socket.write(buildFrame(OP_PONG, frame.payload));
1303
+ socket.write(buildFrame(OP_PONG, whole.payload));
1263
1304
  } catch {
1264
1305
  /* client gone */
1265
1306
  }
@@ -17,6 +17,6 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/.
17
17
  <div class="spinner"></div>
18
18
  <div class="error-title">Redirecting…</div>
19
19
  <div class="error-msg">You are being redirected to a new location.</div>
20
- <div class="error-path" style="color:var(--primary)">{{ redirect_url }}</div>
20
+ <div class="error-path c-primary">{{ redirect_url }}</div>
21
21
  <a href="{{ redirect_url }}" class="error-home">Click here if not redirected</a>
22
22
  {% endblock %}
@@ -9,9 +9,9 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/.
9
9
  {% extends "errors/base.twig" %}
10
10
  {% block title %}401 — Unauthorized{% endblock %}
11
11
  {% block content %}
12
- <div class="error-code" style="color:var(--danger)">401</div>
12
+ <div class="error-code c-danger">401</div>
13
13
  <div class="error-title">Unauthorized</div>
14
14
  <div class="error-msg">You need to sign in to access this resource.</div>
15
- <div class="error-path" style="color:var(--danger)">{{ path }}</div>
15
+ <div class="error-path c-danger">{{ path }}</div>
16
16
  <a href="/" class="error-home">Go Home</a>
17
17
  {% endblock %}
@@ -12,7 +12,7 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/.
12
12
  <meta charset="utf-8">
13
13
  <meta name="viewport" content="width=device-width, initial-scale=1">
14
14
  <title>403 — Forbidden</title>
15
- <style>
15
+ <style nonce="{{ csp_nonce() }}">
16
16
  * { box-sizing: border-box; margin: 0; padding: 0; }
17
17
  body { font-family: system-ui, -apple-system, sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
18
18
  .error-card { background: #1e293b; border: 1px solid #334155; border-radius: 1rem; padding: 3rem; text-align: center; max-width: 520px; width: 90%; }
@@ -12,7 +12,7 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/.
12
12
  <meta charset="utf-8">
13
13
  <meta name="viewport" content="width=device-width, initial-scale=1">
14
14
  <title>404 — Not Found</title>
15
- <style>
15
+ <style nonce="{{ csp_nonce() }}">
16
16
  * { box-sizing: border-box; margin: 0; padding: 0; }
17
17
  body { font-family: system-ui, -apple-system, sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
18
18
  .error-card { background: #1e293b; border: 1px solid #334155; border-radius: 1rem; padding: 3rem; text-align: center; max-width: 520px; width: 90%; }
@@ -12,7 +12,7 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/.
12
12
  <meta charset="utf-8">
13
13
  <meta name="viewport" content="width=device-width, initial-scale=1">
14
14
  <title>500 — Server Error</title>
15
- <style>
15
+ <style nonce="{{ csp_nonce() }}">
16
16
  * { box-sizing: border-box; margin: 0; padding: 0; }
17
17
  body { font-family: system-ui, -apple-system, sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
18
18
  .error-card { background: #1e293b; border: 1px solid #334155; border-radius: 1rem; padding: 3rem; text-align: left; max-width: 700px; width: 90%; }
@@ -9,9 +9,9 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/.
9
9
  {% extends "errors/base.twig" %}
10
10
  {% block title %}502 — Bad Gateway{% endblock %}
11
11
  {% block content %}
12
- <div class="error-code" style="color:var(--danger)">502</div>
12
+ <div class="error-code c-danger">502</div>
13
13
  <div class="error-title">Bad Gateway</div>
14
14
  <div class="error-msg">The upstream server returned an invalid response.</div>
15
- <div class="error-path" style="color:var(--danger)">{{ path }}</div>
15
+ <div class="error-path c-danger">{{ path }}</div>
16
16
  <a href="/" class="error-home">Go Home</a>
17
17
  {% endblock %}
@@ -12,9 +12,9 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/.
12
12
  .error-code { color: var(--warn); }
13
13
  {% endblock %}
14
14
  {% block content %}
15
- <div class="error-code" style="color:var(--warn)">503</div>
15
+ <div class="error-code c-warn">503</div>
16
16
  <div class="error-title">Service Unavailable</div>
17
17
  <div class="error-msg">The server is temporarily unavailable. Please try again shortly.</div>
18
- <div class="error-path" style="color:var(--warn)">{{ path }}</div>
18
+ <div class="error-path c-warn">{{ path }}</div>
19
19
  <a href="/" class="error-home">Go Home</a>
20
20
  {% endblock %}
@@ -13,7 +13,7 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/.
13
13
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
14
14
  {% block meta %}{% endblock %}
15
15
  <title>{% block title %}Error{% endblock %}</title>
16
- <style>
16
+ <style nonce="{{ csp_nonce() }}">
17
17
  :root {
18
18
  --bg: #0f172a; --surface: #1e293b; --border: #334155;
19
19
  --text: #e2e8f0; --muted: #94a3b8; --primary: #3b82f6;
@@ -31,6 +31,9 @@ body { font-family: var(--font); background: var(--bg); color: var(--text); min-
31
31
  .error-home { display: inline-block; margin-top: 1.5rem; padding: 0.5rem 1.5rem; background: var(--primary); color: #fff; text-decoration: none; border-radius: 0.5rem; font-size: 0.85rem; }
32
32
  .error-home:hover { opacity: 0.9; }
33
33
  .logo { width: 3rem; height: 3rem; margin-bottom: 1rem; opacity: 0.5; }
34
+ .c-danger { color: var(--danger); }
35
+ .c-warn { color: var(--warn); }
36
+ .c-primary { color: var(--primary); }
34
37
  {% block extra_styles %}{% endblock %}
35
38
  </style>
36
39
  </head>
@@ -1413,6 +1413,7 @@ function _b64url(data) {
1413
1413
  return data.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1414
1414
  }
1415
1415
  var _formTokenSessionId = "";
1416
+ var _cspNonceProvider = () => "";
1416
1417
  function _buildFormTokenJwt(descriptor = "") {
1417
1418
  const secret = process.env.TINA4_SECRET ?? "";
1418
1419
  const secretBytes = Buffer.byteLength(secret, "utf8");
@@ -1604,6 +1605,7 @@ var Frond = class _Frond {
1604
1605
  this.globals.formTokenValue = (descriptor) => _generateFormTokenValue(descriptor || "");
1605
1606
  this.globals.form_token_value = (descriptor) => _generateFormTokenValue(descriptor || "");
1606
1607
  this.globals.dump = (value) => renderDump(value);
1608
+ this.globals.csp_nonce = () => _cspNonceProvider();
1607
1609
  for (const [k, v] of _Frond.classFilters) this.filters[k] = v;
1608
1610
  for (const [k, v] of _Frond.classGlobals) this.globals[k] = v;
1609
1611
  for (const [k, v] of _Frond.classTests) this.tests[k] = v;
@@ -1785,6 +1785,21 @@ export function setFormTokenSessionId(sessionId: string): void {
1785
1785
  _formTokenSessionId = sessionId || "";
1786
1786
  }
1787
1787
 
1788
+ // CSP nonce provider (ADR-0088). Frond never imports @tina4/core (that would be
1789
+ // circular — core depends on frond), so core injects the per-response nonce
1790
+ // source through this setter. Default returns "" for standalone Frond use.
1791
+ let _cspNonceProvider: () => string = () => "";
1792
+
1793
+ /**
1794
+ * Register the function that backs the `{{ csp_nonce() }}` template global.
1795
+ * @tina4/core calls this once with its request-scoped nonce source so every
1796
+ * Frond instance (framework templates AND the app's) resolves the same nonce
1797
+ * the Content-Security-Policy header names.
1798
+ */
1799
+ export function setCspNonceProvider(fn: () => string): void {
1800
+ _cspNonceProvider = fn;
1801
+ }
1802
+
1788
1803
  function _buildFormTokenJwt(descriptor: string = ""): string {
1789
1804
  // Fail-closed, IDENTICAL to the validator (auth.ts validToken:
1790
1805
  // `secret ?? process.env.TINA4_SECRET ?? ""`). With TINA4_SECRET unset the
@@ -2014,6 +2029,14 @@ export class Frond {
2014
2029
  // {{ user|dump }} and {{ dump(user) }}
2015
2030
  this.globals.dump = (value: unknown) => renderDump(value);
2016
2031
 
2032
+ // CSP nonce (ADR-0088): {{ csp_nonce() }} returns the current response's
2033
+ // nonce so a template can serve an inline <style>/<script> under the strict
2034
+ // default Content-Security-Policy — <style nonce="{{ csp_nonce() }}">. The
2035
+ // value matches the 'nonce-X' the security middleware puts in the CSP header.
2036
+ // Frond stays zero-dependency: @tina4/core injects the provider (it never
2037
+ // imports core); standalone Frond returns "".
2038
+ this.globals.csp_nonce = () => _cspNonceProvider();
2039
+
2017
2040
  // Drain the class-level registry. This is the key to surviving
2018
2041
  // hot-reloads AND the static-facade pattern: app.ts calls
2019
2042
  // Frond.addFilter("money", fn) once, the class remembers it,