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.
- package/CLAUDE.md +2 -2
- package/package.json +5 -9
- package/packages/cli/dist/bin.js +1045 -518
- package/packages/core/dist/index.js +1042 -513
- package/packages/core/src/aiClient.ts +2 -1
- package/packages/core/src/api.ts +2 -1
- package/packages/core/src/auth.ts +1 -1
- package/packages/core/src/cache.ts +6 -1
- package/packages/core/src/devAdmin.ts +176 -123
- package/packages/core/src/dispatchPipeline.ts +4 -0
- package/packages/core/src/docs.ts +5 -3
- package/packages/core/src/errorOverlay.ts +18 -9
- package/packages/core/src/index.ts +2 -1
- package/packages/core/src/mcp.ts +53 -16
- package/packages/core/src/messenger.ts +22 -30
- package/packages/core/src/middleware.ts +8 -1
- package/packages/core/src/optionalPackage.ts +2 -2
- package/packages/core/src/plan.ts +3 -3
- package/packages/core/src/push.ts +1 -1
- package/packages/core/src/request.ts +185 -25
- package/packages/core/src/router.ts +3 -2
- package/packages/core/src/server.ts +32 -10
- package/packages/core/src/session.ts +44 -0
- package/packages/core/src/slashTrim.ts +39 -0
- package/packages/core/src/wsdl.ts +66 -48
- package/packages/core/templates/crud/form.twig +5 -5
- package/packages/core/templates/crud/table.twig +2 -2
- package/packages/core/templates/errors/302.twig +4 -3
- package/packages/frond/dist/index.js +206 -64
- package/packages/frond/src/engine.ts +278 -69
- package/packages/frond/src/index.ts +1 -1
- package/packages/orm/dist/index.js +1026 -499
- package/packages/orm/src/crudRender.ts +5 -5
- package/packages/orm/src/crudTable.ts +1 -1
- package/packages/orm/src/database.ts +15 -29
- package/packages/orm/src/databaseResult.ts +11 -1
- package/packages/orm/src/realtime/index.ts +1 -0
- package/packages/orm/src/realtime/realtime.ts +2 -1
- package/packages/orm/src/realtime/s3Client.ts +276 -0
- package/packages/orm/src/realtime/storage.ts +19 -60
- package/packages/orm/src/sqlTranslator.ts +58 -42
- package/packages/swagger/dist/index.js +25 -4
- package/packages/swagger/src/generator.ts +7 -4
- package/packages/swagger/src/ui.ts +2 -1
- package/types/core/src/devAdmin.d.ts +40 -0
- package/types/core/src/index.d.ts +2 -1
- package/types/core/src/middleware.d.ts +1 -0
- package/types/core/src/optionalPackage.d.ts +2 -2
- package/types/core/src/request.d.ts +5 -1
- package/types/core/src/session.d.ts +25 -0
- package/types/core/src/slashTrim.d.ts +6 -0
- package/types/frond/src/engine.d.ts +11 -0
- package/types/frond/src/index.d.ts +1 -1
- package/types/orm/src/realtime/index.d.ts +1 -0
- package/types/orm/src/realtime/s3Client.d.ts +48 -0
- 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
|
-
|
|
154
|
-
|
|
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 >
|
|
157
|
-
const raw = xml.slice(
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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,
|
|
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
|
-
(
|
|
9
|
-
wrap - render the <form> wrapper;
|
|
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{{
|
|
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 }}"{
|
|
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,
|
|
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"{{
|
|
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={{
|
|
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">{{
|
|
21
|
-
<a href="{{
|
|
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 %}
|