tina4-nodejs 3.13.150 → 3.13.151

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 (56) hide show
  1. package/CLAUDE.md +2 -2
  2. package/package.json +5 -9
  3. package/packages/cli/dist/bin.js +1045 -518
  4. package/packages/core/dist/index.js +1042 -513
  5. package/packages/core/src/aiClient.ts +2 -1
  6. package/packages/core/src/api.ts +2 -1
  7. package/packages/core/src/auth.ts +1 -1
  8. package/packages/core/src/cache.ts +6 -1
  9. package/packages/core/src/devAdmin.ts +176 -123
  10. package/packages/core/src/dispatchPipeline.ts +4 -0
  11. package/packages/core/src/docs.ts +5 -3
  12. package/packages/core/src/errorOverlay.ts +18 -9
  13. package/packages/core/src/index.ts +2 -1
  14. package/packages/core/src/mcp.ts +53 -16
  15. package/packages/core/src/messenger.ts +22 -30
  16. package/packages/core/src/middleware.ts +8 -1
  17. package/packages/core/src/optionalPackage.ts +2 -2
  18. package/packages/core/src/plan.ts +3 -3
  19. package/packages/core/src/push.ts +1 -1
  20. package/packages/core/src/request.ts +185 -25
  21. package/packages/core/src/router.ts +3 -2
  22. package/packages/core/src/server.ts +32 -10
  23. package/packages/core/src/session.ts +44 -0
  24. package/packages/core/src/slashTrim.ts +39 -0
  25. package/packages/core/src/wsdl.ts +66 -48
  26. package/packages/core/templates/crud/form.twig +5 -5
  27. package/packages/core/templates/crud/table.twig +2 -2
  28. package/packages/core/templates/errors/302.twig +4 -3
  29. package/packages/frond/dist/index.js +206 -64
  30. package/packages/frond/src/engine.ts +278 -69
  31. package/packages/frond/src/index.ts +1 -1
  32. package/packages/orm/dist/index.js +1026 -499
  33. package/packages/orm/src/crudRender.ts +5 -5
  34. package/packages/orm/src/crudTable.ts +1 -1
  35. package/packages/orm/src/database.ts +15 -29
  36. package/packages/orm/src/databaseResult.ts +11 -1
  37. package/packages/orm/src/realtime/index.ts +1 -0
  38. package/packages/orm/src/realtime/realtime.ts +2 -1
  39. package/packages/orm/src/realtime/s3Client.ts +276 -0
  40. package/packages/orm/src/realtime/storage.ts +19 -60
  41. package/packages/orm/src/sqlTranslator.ts +58 -42
  42. package/packages/swagger/dist/index.js +25 -4
  43. package/packages/swagger/src/generator.ts +7 -4
  44. package/packages/swagger/src/ui.ts +2 -1
  45. package/types/core/src/devAdmin.d.ts +40 -0
  46. package/types/core/src/index.d.ts +2 -1
  47. package/types/core/src/middleware.d.ts +1 -0
  48. package/types/core/src/optionalPackage.d.ts +2 -2
  49. package/types/core/src/request.d.ts +5 -1
  50. package/types/core/src/session.d.ts +25 -0
  51. package/types/core/src/slashTrim.d.ts +6 -0
  52. package/types/frond/src/engine.d.ts +11 -0
  53. package/types/frond/src/index.d.ts +1 -1
  54. package/types/orm/src/realtime/index.d.ts +1 -0
  55. package/types/orm/src/realtime/s3Client.d.ts +48 -0
  56. package/types/orm/src/realtime/storage.d.ts +9 -7
@@ -0,0 +1,39 @@
1
+ /*
2
+ Copyright (c) 2026 Code Infinity
3
+ SPDX-License-Identifier: MPL-2.0
4
+ This Source Code Form is subject to the terms of the Mozilla Public
5
+ License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
+ */
8
+
9
+ // ReDoS-safe edge-character trimming, shared by core/orm/swagger.
10
+ //
11
+ // `/\/+$/` and `/^\/+|\/+$/g` (and the slug's `/^-+|-+$/g`) are polynomial on V8:
12
+ // a long run of the character followed by a different tail makes the anchored
13
+ // `+` backtrack O(n^2), so a crafted request path of ~80k slashes hangs the
14
+ // event loop for seconds. These run on attacker-reachable paths (the router, the
15
+ // server pathname), so the fix matters. A single character scan strips the same
16
+ // run in one linear pass with no backtracking.
17
+
18
+ const SLASH = 47; // '/'.charCodeAt(0)
19
+
20
+ /** Strip the given char code from the end only (the `/\/+$/` replacement). */
21
+ export function rtrimSlashes(s: string): string {
22
+ let end = s.length;
23
+ while (end > 0 && s.charCodeAt(end - 1) === SLASH) end--;
24
+ return end === s.length ? s : s.slice(0, end);
25
+ }
26
+
27
+ /** Strip the given char code from both ends (linear, no backtracking). */
28
+ export function trimChar(s: string, code: number): string {
29
+ let start = 0;
30
+ let end = s.length;
31
+ while (start < end && s.charCodeAt(start) === code) start++;
32
+ while (end > start && s.charCodeAt(end - 1) === code) end--;
33
+ return start === 0 && end === s.length ? s : s.slice(start, end);
34
+ }
35
+
36
+ /** Strip leading AND trailing '/' (the ReDoS-safe `/^\/+|\/+$/g` replacement). */
37
+ export function trimSlashes(s: string): string {
38
+ return trimChar(s, SLASH);
39
+ }
@@ -143,65 +143,83 @@ const ATTRIBUTE_VALUE = /=\s*(?:"([^"<]*)"|'([^'<]*)')/g;
143
143
  function parseXmlDocument(xml: string): XmlElement {
144
144
  const stack: XmlElement[] = [];
145
145
  let root: XmlElement | null = null;
146
- let position = 0;
147
146
 
148
147
  const addText = (text: string): void => {
149
148
  const current = stack[stack.length - 1];
150
149
  if (current.children.length === 0) current.text += text;
151
150
  };
152
151
 
153
- while (position < xml.length) {
154
- const nextTag = xml.indexOf("<", position);
152
+ // One handler per markup construct. Each is entered at the "<" that opens the
153
+ // construct and returns the position just past it; they share stack/root/addText
154
+ // through this closure. Splitting the old six-arm if/else out of the scan loop
155
+ // keeps each rule small and named (cyclomatic complexity stays low per function).
156
+ const skipComment = (at: number): number => {
157
+ const close = xml.indexOf("-->", at + 4);
158
+ if (close === -1) malformed();
159
+ return close + 3;
160
+ };
161
+ const readCData = (at: number): number => {
162
+ const close = xml.indexOf("]]>", at + 9);
163
+ if (close === -1 || stack.length === 0) malformed();
164
+ addText(xml.slice(at + 9, close));
165
+ return close + 3;
166
+ };
167
+ const skipProcessingInstruction = (at: number): number => {
168
+ const close = xml.indexOf("?>", at + 2);
169
+ if (close === -1) malformed();
170
+ const target = xml.slice(at + 2, close).split(/\s/, 1)[0];
171
+ // The XML declaration is only legal as the very first thing in the document.
172
+ if (target.toLowerCase() === "xml" && at !== 0) malformed();
173
+ return close + 2;
174
+ };
175
+ const closeElement = (at: number): number => {
176
+ END_TAG.lastIndex = at;
177
+ const match = END_TAG.exec(xml);
178
+ const open = stack.pop();
179
+ if (!match || !open || open.qualifiedName !== match[1]) malformed();
180
+ return END_TAG.lastIndex;
181
+ };
182
+ const openElement = (at: number): number => {
183
+ START_TAG.lastIndex = at;
184
+ const match = START_TAG.exec(xml);
185
+ if (!match || (stack.length === 0 && root !== null)) malformed(); // bad tag, or a second root
186
+ for (const attribute of match[2].matchAll(ATTRIBUTE_VALUE)) decodeEntities(attribute[1] ?? attribute[2]);
187
+ const qualifiedName = match[1];
188
+ const element: XmlElement = {
189
+ name: qualifiedName.slice(qualifiedName.indexOf(":") + 1),
190
+ qualifiedName,
191
+ children: [],
192
+ text: "",
193
+ };
194
+ if (stack.length > 0) stack[stack.length - 1].children.push(element);
195
+ else root = element;
196
+ if (match[3] !== "/") stack.push(element);
197
+ return START_TAG.lastIndex;
198
+ };
199
+
200
+ // Consume the text run before the next "<" (entities decoded inside an element,
201
+ // only whitespace tolerated outside the root), then dispatch on the construct.
202
+ const readText = (at: number): number => {
203
+ const nextTag = xml.indexOf("<", at);
155
204
  const textEnd = nextTag === -1 ? xml.length : nextTag;
156
- if (textEnd > position) {
157
- const raw = xml.slice(position, textEnd);
205
+ if (textEnd > at) {
206
+ const raw = xml.slice(at, textEnd);
158
207
  if (stack.length > 0) addText(decodeEntities(raw));
159
208
  else if (raw.trim() !== "") malformed(); // text outside the root element
160
- position = textEnd;
161
- if (nextTag === -1) break;
162
209
  }
210
+ return textEnd;
211
+ };
163
212
 
164
- if (xml.startsWith("<!--", position)) {
165
- const close = xml.indexOf("-->", position + 4);
166
- if (close === -1) malformed();
167
- position = close + 3;
168
- } else if (xml.startsWith("<![CDATA[", position)) {
169
- const close = xml.indexOf("]]>", position + 9);
170
- if (close === -1 || stack.length === 0) malformed();
171
- addText(xml.slice(position + 9, close));
172
- position = close + 3;
173
- } else if (xml.startsWith("<?", position)) {
174
- const close = xml.indexOf("?>", position + 2);
175
- if (close === -1) malformed();
176
- const target = xml.slice(position + 2, close).split(/\s/, 1)[0];
177
- // The XML declaration is only legal as the very first thing in the document.
178
- if (target.toLowerCase() === "xml" && position !== 0) malformed();
179
- position = close + 2;
180
- } else if (xml.startsWith("<!", position)) {
181
- malformed(); // any other declaration; DOCTYPE was refused before parsing
182
- } else if (xml.startsWith("</", position)) {
183
- END_TAG.lastIndex = position;
184
- const match = END_TAG.exec(xml);
185
- const open = stack.pop();
186
- if (!match || !open || open.qualifiedName !== match[1]) malformed();
187
- position = END_TAG.lastIndex;
188
- } else {
189
- START_TAG.lastIndex = position;
190
- const match = START_TAG.exec(xml);
191
- if (!match || (stack.length === 0 && root !== null)) malformed(); // bad tag, or a second root
192
- for (const attribute of match[2].matchAll(ATTRIBUTE_VALUE)) decodeEntities(attribute[1] ?? attribute[2]);
193
- const qualifiedName = match[1];
194
- const element: XmlElement = {
195
- name: qualifiedName.slice(qualifiedName.indexOf(":") + 1),
196
- qualifiedName,
197
- children: [],
198
- text: "",
199
- };
200
- if (stack.length > 0) stack[stack.length - 1].children.push(element);
201
- else root = element;
202
- if (match[3] !== "/") stack.push(element);
203
- position = START_TAG.lastIndex;
204
- }
213
+ let position = 0;
214
+ while (position < xml.length) {
215
+ position = readText(position);
216
+ if (position >= xml.length) break;
217
+ if (xml.startsWith("<!--", position)) position = skipComment(position);
218
+ else if (xml.startsWith("<![CDATA[", position)) position = readCData(position);
219
+ else if (xml.startsWith("<?", position)) position = skipProcessingInstruction(position);
220
+ else if (xml.startsWith("<!", position)) malformed(); // any other declaration; DOCTYPE refused before parsing
221
+ else if (xml.startsWith("</", position)) position = closeElement(position);
222
+ else position = openElement(position);
205
223
  }
206
224
 
207
225
  if (root === null || stack.length > 0) malformed();
@@ -3,23 +3,23 @@
3
3
 
4
4
  Override: drop your own src/templates/crud/form.twig to change how inputs render.
5
5
 
6
- fields - one per input: { id, name, label, value, placeholder, required_attr,
6
+ fields - one per input: { id, name, label, value, placeholder, required (bool),
7
7
  and exactly ONE control flag: input (with type) | textarea | checkbox
8
- (checked_attr) | select (options_html) }.
9
- wrap - render the <form> wrapper; form_id_attr/action/form_method set it
8
+ (checked bool) | select (options_html) }.
9
+ wrap - render the <form> wrapper; form_id/action/form_method set it
10
10
  edit - add the hidden primary-key input (edit modal)
11
11
  modal_footer- render the Cancel/Save modal buttons; submit_button - a plain Submit
12
12
 
13
13
  No inline styles or on*= handlers: the modal buttons carry data-crud-action and
14
14
  are wired by the page script's delegated listener (ADR-0088).
15
15
  #}
16
- {% if wrap %}<form{{ form_id_attr|raw }} action="{{ action }}" method="{{ form_method }}" class="needs-validation" data-crud-form="1">{% endif %}
16
+ {% if wrap %}<form{% if form_id %} id="{{ form_id }}"{% endif %} action="{{ action }}" method="{{ form_method }}" class="needs-validation" data-crud-form="1">{% endif %}
17
17
  {% if method_override %}<input type="hidden" name="_method" value="{{ method_override }}">{% endif %}
18
18
  {% if edit %}<input type="hidden" id="{{ mode }}-pk-value" name="{{ pk }}">{% endif %}
19
19
  {% if modal_footer %}<div class="crud-errors" data-crud-errors hidden></div>{% endif %}
20
20
  {% for field in fields %}<div class="mb-3">
21
21
  <label for="{{ field.id }}" class="form-label">{{ field.label }}</label>
22
- {% if field.textarea %}<textarea class="form-control" id="{{ field.id }}" name="{{ field.name }}"{{ field.required_attr|raw }}>{{ field.value }}</textarea>{% endif %}{% if field.checkbox %}<div class="form-check"><input class="form-check-input" type="checkbox" id="{{ field.id }}" name="{{ field.name }}"{{ field.checked_attr|raw }}></div>{% endif %}{% if field.select %}<select class="form-select" id="{{ field.id }}" name="{{ field.name }}"{{ field.required_attr|raw }}>{{ field.options_html|raw }}</select>{% endif %}{% if field.input %}<input type="{{ field.type }}" class="form-control" id="{{ field.id }}" name="{{ field.name }}" value="{{ field.value }}" placeholder="{{ field.placeholder }}"{{ field.required_attr|raw }}>{% endif %}
22
+ {% if field.textarea %}<textarea class="form-control" id="{{ field.id }}" name="{{ field.name }}"{% if field.required %} required{% endif %}>{{ field.value }}</textarea>{% endif %}{% if field.checkbox %}<div class="form-check"><input class="form-check-input" type="checkbox" id="{{ field.id }}" name="{{ field.name }}"{% if field.checked %} checked{% endif %}></div>{% endif %}{% if field.select %}<select class="form-select" id="{{ field.id }}" name="{{ field.name }}"{% if field.required %} required{% endif %}>{{ field.options_html|raw }}</select>{% endif %}{% if field.input %}<input type="{{ field.type }}" class="form-control" id="{{ field.id }}" name="{{ field.name }}" value="{{ field.value }}" placeholder="{{ field.placeholder }}"{% if field.required %} required{% endif %}>{% endif %}
23
23
  </div>
24
24
  {% endfor %}
25
25
  {% if modal_footer %}<div class="modal-footer"><button type="button" class="btn btn-secondary" data-crud-action="close" data-crud-modal="{{ mode }}">Cancel</button><button type="button" class="btn btn-primary" data-crud-action="save" data-crud-mode="{{ mode }}">Save</button></div>{% endif %}{% if submit_button %}<button type="submit" class="btn btn-primary">Submit</button>{% endif %}
@@ -11,7 +11,7 @@
11
11
  empty - true when there are no rows
12
12
  colspan - column count + 1 (for the empty row)
13
13
  editable - inline-edit table (Save/Delete); readonly - view table (Edit/Delete)
14
- table_name, table_id_attr, inline_script
14
+ table_name, table_id, inline_script
15
15
 
16
16
  Alignment: each header/cell carries a tina4-css class (text-start for text,
17
17
  text-end for numbers). Actions is always text-end. No inline styles (ADR-0088).
@@ -20,7 +20,7 @@
20
20
  re-renders <tbody data-crud-body> over fetch() as the user searches/sorts/pages.
21
21
  #}
22
22
  <div class="table-responsive">
23
- <table class="table table-striped table-hover"{{ table_id_attr|raw }}>
23
+ <table class="table table-striped table-hover"{% if table_id %} id="{{ table_id }}"{% endif %}>
24
24
  <thead class="table-dark"><tr>
25
25
  {% for header in headers %}{% if header.sortable %}<th class="{{ header.align }}"><a class="sort-link" href="{{ header.url }}" data-crud-sort="{{ header.col }}" data-crud-dir="{{ header.next_dir }}">{{ header.label }}{{ header.indicator|raw }}</a></th>{% endif %}{% if header.plain %}<th class="{{ header.align }}">{{ header.label }}</th>{% endif %}{% endfor %}<th class="text-end">Actions</th>
26
26
  </tr></thead>
@@ -8,15 +8,16 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/.
8
8
 
9
9
  {% extends "errors/base.twig" %}
10
10
  {% block title %}Redirecting…{% endblock %}
11
- {% block meta %}<meta http-equiv="refresh" content="0;url={{ redirect_url }}">{% endblock %}
11
+ {% block meta %}{% set p1 = redirect_url|slice(0,1) %}{% set p2 = redirect_url|slice(0,2) %}{% set p7 = redirect_url|slice(0,7) %}{% set p8 = redirect_url|slice(0,8) %}{% set rel = (p1 == '/') ? ((p2 == '//') ? 'no' : 'yes') : 'no' %}{% set safe_redirect_url = (rel == 'yes' or p7 == 'http://' or p8 == 'https://') ? redirect_url : '/' %}<meta http-equiv="refresh" content="0;url={{ safe_redirect_url }}">{% endblock %}
12
12
  {% block extra_styles %}
13
13
  .spinner { display: inline-block; width: 1.5rem; height: 1.5rem; border: 3px solid var(--border); border-top-color: var(--primary); border-radius: 50%; animation: spin 0.8s linear infinite; margin-bottom: 1rem; }
14
14
  @keyframes spin { to { transform: rotate(360deg); } }
15
15
  {% endblock %}
16
16
  {% block content %}
17
+ {% set p1 = redirect_url|slice(0,1) %}{% set p2 = redirect_url|slice(0,2) %}{% set p7 = redirect_url|slice(0,7) %}{% set p8 = redirect_url|slice(0,8) %}{% set rel = (p1 == '/') ? ((p2 == '//') ? 'no' : 'yes') : 'no' %}{% set safe_redirect_url = (rel == 'yes' or p7 == 'http://' or p8 == 'https://') ? redirect_url : '/' %}
17
18
  <div class="spinner"></div>
18
19
  <div class="error-title">Redirecting…</div>
19
20
  <div class="error-msg">You are being redirected to a new location.</div>
20
- <div class="error-path c-primary">{{ redirect_url }}</div>
21
- <a href="{{ redirect_url }}" class="error-home">Click here if not redirected</a>
21
+ <div class="error-path c-primary">{{ safe_redirect_url }}</div>
22
+ <a href="{{ safe_redirect_url }}" class="error-home">Click here if not redirected</a>
22
23
  {% endblock %}