tina4-nodejs 3.13.136 → 3.13.138

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 (243) hide show
  1. package/CLAUDE.md +33 -3
  2. package/COMMERCIAL-LICENSE.md +8 -0
  3. package/LICENSE +373 -0
  4. package/NOTICE +11 -0
  5. package/README.md +24 -2
  6. package/package.json +44 -6
  7. package/packages/cli/dist/bin.js +19440 -18017
  8. package/packages/cli/src/bin.ts +8 -0
  9. package/packages/cli/src/commands/build.ts +8 -0
  10. package/packages/cli/src/commands/generate.ts +8 -0
  11. package/packages/cli/src/commands/init.ts +8 -0
  12. package/packages/cli/src/commands/lint.ts +8 -0
  13. package/packages/cli/src/commands/migrate.ts +8 -0
  14. package/packages/cli/src/commands/migrateCreate.ts +8 -0
  15. package/packages/cli/src/commands/migrateRollback.ts +8 -0
  16. package/packages/cli/src/commands/migrateStatus.ts +8 -0
  17. package/packages/cli/src/commands/queue.ts +8 -0
  18. package/packages/cli/src/commands/routes.ts +8 -0
  19. package/packages/cli/src/commands/seed.ts +8 -0
  20. package/packages/cli/src/commands/serve.ts +11 -0
  21. package/packages/cli/src/commands/test.ts +8 -0
  22. package/packages/core/dist/index.js +19432 -18012
  23. package/packages/core/gallery/auth/src/routes/api/gallery/auth/login/post.ts +8 -0
  24. package/packages/core/gallery/auth/src/routes/api/gallery/auth/verify/get.ts +8 -0
  25. package/packages/core/gallery/auth/src/routes/gallery/auth/get.ts +8 -0
  26. package/packages/core/gallery/database/src/routes/api/gallery/db/notes/get.ts +8 -0
  27. package/packages/core/gallery/database/src/routes/api/gallery/db/notes/post.ts +8 -0
  28. package/packages/core/gallery/database/src/routes/api/gallery/db/tables/get.ts +8 -0
  29. package/packages/core/gallery/error-overlay/src/routes/api/gallery/crash/get.ts +8 -0
  30. package/packages/core/gallery/orm/src/routes/api/gallery/products/get.ts +8 -0
  31. package/packages/core/gallery/orm/src/routes/api/gallery/products/post.ts +8 -0
  32. package/packages/core/gallery/queue/src/lib/queueDb.ts +8 -0
  33. package/packages/core/gallery/queue/src/routes/api/gallery/queue/consume/post.ts +8 -0
  34. package/packages/core/gallery/queue/src/routes/api/gallery/queue/fail/post.ts +8 -0
  35. package/packages/core/gallery/queue/src/routes/api/gallery/queue/produce/post.ts +8 -0
  36. package/packages/core/gallery/queue/src/routes/api/gallery/queue/retry/post.ts +8 -0
  37. package/packages/core/gallery/queue/src/routes/api/gallery/queue/status/get.ts +8 -0
  38. package/packages/core/gallery/queue/src/routes/gallery/queue/get.ts +8 -0
  39. package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/get.ts +8 -0
  40. package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/post.ts +8 -0
  41. package/packages/core/gallery/templates/src/routes/gallery/page/get.ts +8 -0
  42. package/packages/core/gallery/templates/src/templates/gallery_page.twig +8 -0
  43. package/packages/core/public/THIRD-PARTY-NOTICES.md +55 -0
  44. package/packages/core/public/bundled-component-provenance.json +48 -0
  45. package/packages/core/public/js/tina4js.min.js +13 -3
  46. package/packages/core/public/licenses/swagger-ui-3.10.0-original-notice.txt +11 -0
  47. package/packages/core/public/licenses/swagger-ui-5.17.14-NOTICE.txt +2 -0
  48. package/packages/core/public/licenses/swagger-ui-Apache-2.0.txt +202 -0
  49. package/packages/core/public/licenses/tina4-css-MIT.txt +21 -0
  50. package/packages/core/public/licenses/tina4-dev-admin-components.json +240 -0
  51. package/packages/core/public/licenses/tina4-dev-admin-third-party.txt +919 -0
  52. package/packages/core/public/licenses/tina4-js-MIT.txt +20 -0
  53. package/packages/core/src/__feedback/widget.js +8 -0
  54. package/packages/core/src/_missing.ts +8 -0
  55. package/packages/core/src/ai.ts +8 -0
  56. package/packages/core/src/aiClient.ts +22 -7
  57. package/packages/core/src/api.ts +15 -3
  58. package/packages/core/src/auth.ts +21 -8
  59. package/packages/core/src/authGate.ts +8 -0
  60. package/packages/core/src/background.ts +8 -0
  61. package/packages/core/src/cache.ts +43 -4
  62. package/packages/core/src/constants.ts +8 -0
  63. package/packages/core/src/container.ts +8 -0
  64. package/packages/core/src/context/chunker.ts +8 -0
  65. package/packages/core/src/context/index.ts +8 -0
  66. package/packages/core/src/devAdmin.ts +185 -59
  67. package/packages/core/src/devMailbox.ts +8 -0
  68. package/packages/core/src/dispatchPipeline.ts +8 -0
  69. package/packages/core/src/docs.ts +8 -0
  70. package/packages/core/src/docsAutoDiscovery.ts +8 -0
  71. package/packages/core/src/docsParser.ts +8 -0
  72. package/packages/core/src/docsScanner.ts +44 -0
  73. package/packages/core/src/docsSignatures.ts +8 -0
  74. package/packages/core/src/dotenv.ts +8 -0
  75. package/packages/core/src/env.ts +8 -0
  76. package/packages/core/src/errorOverlay.ts +8 -0
  77. package/packages/core/src/events.ts +8 -0
  78. package/packages/core/src/fakeData.ts +8 -0
  79. package/packages/core/src/feedback.ts +8 -0
  80. package/packages/core/src/graphql.ts +16 -5
  81. package/packages/core/src/health.ts +8 -0
  82. package/packages/core/src/htmlElement.ts +8 -0
  83. package/packages/core/src/i18n.ts +8 -0
  84. package/packages/core/src/index.ts +8 -0
  85. package/packages/core/src/job.ts +8 -0
  86. package/packages/core/src/logger.ts +8 -0
  87. package/packages/core/src/mcp.test.ts +8 -0
  88. package/packages/core/src/mcp.ts +8 -0
  89. package/packages/core/src/messenger.ts +143 -86
  90. package/packages/core/src/metrics.ts +13 -0
  91. package/packages/core/src/middleware.ts +1178 -1096
  92. package/packages/core/src/mqtt.ts +8 -0
  93. package/packages/core/src/mqttMessage.ts +8 -0
  94. package/packages/core/src/optionalPackage.ts +48 -0
  95. package/packages/core/src/plan.ts +8 -0
  96. package/packages/core/src/portTakeover.ts +8 -0
  97. package/packages/core/src/projectIndex.ts +8 -0
  98. package/packages/core/src/projectIndexExtractors.ts +8 -0
  99. package/packages/core/src/projectIndexStorage.ts +8 -0
  100. package/packages/core/src/push.ts +8 -0
  101. package/packages/core/src/queue.ts +8 -0
  102. package/packages/core/src/queueBackends/kafkaBackend.ts +8 -0
  103. package/packages/core/src/queueBackends/liteBackend.ts +8 -0
  104. package/packages/core/src/queueBackends/mongoBackend.ts +21 -8
  105. package/packages/core/src/queueBackends/rabbitmqBackend.ts +8 -0
  106. package/packages/core/src/rateLimiter.ts +8 -0
  107. package/packages/core/src/request.ts +59 -10
  108. package/packages/core/src/response.ts +79 -5
  109. package/packages/core/src/routeDiscovery.ts +8 -0
  110. package/packages/core/src/router.ts +8 -0
  111. package/packages/core/src/server.ts +73 -24
  112. package/packages/core/src/service.ts +8 -0
  113. package/packages/core/src/session.ts +8 -0
  114. package/packages/core/src/sessionHandlers/childError.ts +8 -0
  115. package/packages/core/src/sessionHandlers/databaseHandler.ts +8 -0
  116. package/packages/core/src/sessionHandlers/memcachedHandler.ts +8 -0
  117. package/packages/core/src/sessionHandlers/mongoClient.ts +8 -0
  118. package/packages/core/src/sessionHandlers/mongoHandler.ts +8 -0
  119. package/packages/core/src/sessionHandlers/respClient.ts +8 -0
  120. package/packages/core/src/sessionHandlers/sqlClient.ts +8 -0
  121. package/packages/core/src/sessionHandlers/syncBridge.ts +8 -0
  122. package/packages/core/src/sessionHandlers/syncSocket.ts +8 -0
  123. package/packages/core/src/sessionHandlers/valkeyHandler.ts +8 -0
  124. package/packages/core/src/sso.ts +8 -0
  125. package/packages/core/src/static.ts +8 -0
  126. package/packages/core/src/test.ts +8 -0
  127. package/packages/core/src/testClient.ts +8 -0
  128. package/packages/core/src/testing.ts +8 -0
  129. package/packages/core/src/transport.ts +369 -0
  130. package/packages/core/src/trustedProxy.ts +8 -0
  131. package/packages/core/src/types.ts +13 -0
  132. package/packages/core/src/validator.ts +8 -0
  133. package/packages/core/src/version.ts +8 -0
  134. package/packages/core/src/websocket.ts +8 -0
  135. package/packages/core/src/websocketBackplane.ts +49 -32
  136. package/packages/core/src/websocketConnection.ts +8 -0
  137. package/packages/core/src/wsdl.ts +197 -115
  138. package/packages/core/templates/errors/302.twig +8 -0
  139. package/packages/core/templates/errors/401.twig +8 -0
  140. package/packages/core/templates/errors/403.twig +8 -0
  141. package/packages/core/templates/errors/404.twig +8 -0
  142. package/packages/core/templates/errors/500.twig +8 -0
  143. package/packages/core/templates/errors/502.twig +8 -0
  144. package/packages/core/templates/errors/503.twig +8 -0
  145. package/packages/core/templates/errors/base.twig +8 -0
  146. package/packages/frond/dist/index.js +151 -17
  147. package/packages/frond/src/engine.ts +177 -17
  148. package/packages/frond/src/index.ts +8 -0
  149. package/packages/orm/dist/index.js +8135 -6713
  150. package/packages/orm/src/adapters/firebird.ts +50 -3
  151. package/packages/orm/src/adapters/mongodb.ts +8 -0
  152. package/packages/orm/src/adapters/mssql.ts +29 -7
  153. package/packages/orm/src/adapters/mysql.ts +17 -1
  154. package/packages/orm/src/adapters/odbc.ts +11 -0
  155. package/packages/orm/src/adapters/pg-types.d.ts +8 -0
  156. package/packages/orm/src/adapters/postgres.ts +34 -8
  157. package/packages/orm/src/adapters/sqlDialect.ts +57 -0
  158. package/packages/orm/src/adapters/sqlite.ts +26 -0
  159. package/packages/orm/src/autoCrud.ts +78 -36
  160. package/packages/orm/src/baseModel.ts +61 -29
  161. package/packages/orm/src/cachedDatabase.ts +67 -1
  162. package/packages/orm/src/connectTimeout.ts +8 -0
  163. package/packages/orm/src/database.ts +649 -401
  164. package/packages/orm/src/databaseResult.ts +8 -0
  165. package/packages/orm/src/databaseUrl.ts +8 -0
  166. package/packages/orm/src/docstore.ts +23 -1
  167. package/packages/orm/src/fakeData.ts +8 -0
  168. package/packages/orm/src/graph/adapters/arango.ts +8 -0
  169. package/packages/orm/src/graph/adapters/bolt.ts +8 -0
  170. package/packages/orm/src/graph/adapters/ultipa.ts +8 -0
  171. package/packages/orm/src/graph/connectTimeout.ts +8 -0
  172. package/packages/orm/src/graph/errors.ts +8 -0
  173. package/packages/orm/src/graph/graphAdapter.ts +8 -0
  174. package/packages/orm/src/graph/graphDatabase.ts +8 -0
  175. package/packages/orm/src/graph/graphUrl.ts +10 -1
  176. package/packages/orm/src/graph/shapes.ts +8 -0
  177. package/packages/orm/src/index.ts +147 -139
  178. package/packages/orm/src/migration.ts +21 -0
  179. package/packages/orm/src/model.ts +8 -0
  180. package/packages/orm/src/modelCollection.ts +8 -0
  181. package/packages/orm/src/point.ts +8 -0
  182. package/packages/orm/src/query.ts +149 -33
  183. package/packages/orm/src/queryBuilder.ts +8 -0
  184. package/packages/orm/src/realtime/index.ts +8 -0
  185. package/packages/orm/src/realtime/models/attachment.ts +8 -0
  186. package/packages/orm/src/realtime/models/channel.ts +8 -0
  187. package/packages/orm/src/realtime/models/channelMember.ts +8 -0
  188. package/packages/orm/src/realtime/models/message.ts +8 -0
  189. package/packages/orm/src/realtime/models/workspace.ts +8 -0
  190. package/packages/orm/src/realtime/realtime.ts +8 -0
  191. package/packages/orm/src/realtime/storage.ts +43 -12
  192. package/packages/orm/src/seeder.ts +8 -0
  193. package/packages/orm/src/seederTable.ts +8 -0
  194. package/packages/orm/src/seederTypes.ts +8 -0
  195. package/packages/orm/src/sqlTranslator.ts +185 -8
  196. package/packages/orm/src/types.ts +15 -0
  197. package/packages/orm/src/validation.ts +8 -0
  198. package/packages/swagger/src/generator.ts +8 -0
  199. package/packages/swagger/src/index.ts +8 -0
  200. package/packages/swagger/src/ui.ts +8 -0
  201. package/types/core/src/aiClient.d.ts +2 -2
  202. package/types/core/src/background.d.ts +0 -16
  203. package/types/core/src/cache.d.ts +6 -0
  204. package/types/core/src/devAdmin.d.ts +18 -12
  205. package/types/core/src/docs.d.ts +0 -17
  206. package/types/core/src/errorOverlay.d.ts +0 -24
  207. package/types/core/src/events.d.ts +0 -12
  208. package/types/core/src/feedback.d.ts +0 -23
  209. package/types/core/src/graphql.d.ts +0 -21
  210. package/types/core/src/htmlElement.d.ts +0 -15
  211. package/types/core/src/messenger.d.ts +12 -1
  212. package/types/core/src/metrics.d.ts +2 -0
  213. package/types/core/src/middleware.d.ts +45 -1
  214. package/types/core/src/mqtt.d.ts +0 -33
  215. package/types/core/src/optionalPackage.d.ts +25 -0
  216. package/types/core/src/plan.d.ts +0 -10
  217. package/types/core/src/projectIndex.d.ts +0 -10
  218. package/types/core/src/queueBackends/mongoBackend.d.ts +2 -0
  219. package/types/core/src/request.d.ts +12 -0
  220. package/types/core/src/response.d.ts +13 -0
  221. package/types/core/src/server.d.ts +19 -0
  222. package/types/core/src/sessionHandlers/childError.d.ts +0 -15
  223. package/types/core/src/transport.d.ts +28 -0
  224. package/types/core/src/trustedProxy.d.ts +0 -16
  225. package/types/core/src/types.d.ts +5 -0
  226. package/types/core/src/websocketBackplane.d.ts +1 -0
  227. package/types/core/src/wsdl.d.ts +8 -22
  228. package/types/frond/src/engine.d.ts +7 -1
  229. package/types/orm/src/adapters/firebird.d.ts +12 -0
  230. package/types/orm/src/adapters/mssql.d.ts +11 -0
  231. package/types/orm/src/adapters/mysql.d.ts +2 -0
  232. package/types/orm/src/adapters/postgres.d.ts +11 -0
  233. package/types/orm/src/adapters/sqlDialect.d.ts +25 -0
  234. package/types/orm/src/adapters/sqlite.d.ts +7 -0
  235. package/types/orm/src/baseModel.d.ts +5 -0
  236. package/types/orm/src/cachedDatabase.d.ts +27 -0
  237. package/types/orm/src/database.d.ts +46 -7
  238. package/types/orm/src/graph/graphUrl.d.ts +0 -8
  239. package/types/orm/src/index.d.ts +1 -1
  240. package/types/orm/src/query.d.ts +48 -2
  241. package/types/orm/src/realtime/storage.d.ts +2 -1
  242. package/types/orm/src/sqlTranslator.d.ts +70 -18
  243. package/types/orm/src/types.d.ts +6 -0
@@ -1,3 +1,11 @@
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
+
1
9
  /**
2
10
  * Tina4 WSDL/SOAP — SOAP 1.1 / WSDL 1.1 service base class.
3
11
  *
@@ -71,122 +79,193 @@ function escapeXml(value: string): string {
71
79
  .replace(/'/g, "'");
72
80
  }
73
81
 
74
- // ── Minimal zero-dep XML DOM parser ────────────────────────────
75
- // Stack-based parser that builds a tree. Handles namespaces by
76
- // stripping prefixes. No external dependencies.
77
-
78
- interface XmlNode {
79
- tag: string;
80
- children: XmlNode[];
82
+ // ── Minimal zero-dep XML parser ──────────────────────────────
83
+ // A small well-formedness-checking parser, enough for a SOAP envelope: it
84
+ // matches start and end tags, decodes the five predefined entities, decimal
85
+ // and hexadecimal character references and CDATA, skips comments and
86
+ // processing instructions, and refuses anything else (an undefined entity, a
87
+ // bare "&", a mismatched tag, a second root) the way Python's ElementTree
88
+ // does, so the handler can answer "Malformed XML" exactly where the reference
89
+ // does. Namespace prefixes are stripped to the local name. DOCTYPE is refused
90
+ // before this parser ever runs (see handle()).
91
+
92
+ interface XmlElement {
93
+ /** Local name, prefix stripped ("soap:Body" -> "Body"). */
94
+ name: string;
95
+ /** Qualified name as written, used to match the end tag. */
96
+ qualifiedName: string;
97
+ children: XmlElement[];
98
+ /** Text before the first child element - ElementTree's `.text`. */
81
99
  text: string;
82
100
  }
83
101
 
84
- function parseXml(xml: string): XmlNode {
85
- const root: XmlNode = { tag: "", children: [], text: "" };
86
- const stack: XmlNode[] = [root];
87
- let i = 0;
88
-
89
- while (i < xml.length) {
90
- if (xml[i] === "<") {
91
- const closeIdx = xml.indexOf(">", i);
92
- if (closeIdx === -1) break;
93
- const tagContent = xml.substring(i + 1, closeIdx).trim();
94
-
95
- if (tagContent.startsWith("/")) {
96
- stack.pop();
97
- } else if (tagContent.startsWith("?") || tagContent.startsWith("!")) {
98
- // PI or comment — skip
99
- } else {
100
- const selfClosing = tagContent.endsWith("/");
101
- const raw = selfClosing ? tagContent.slice(0, -1).trim() : tagContent;
102
- const spaceIdx = raw.indexOf(" ");
103
- const fullTag = spaceIdx === -1 ? raw : raw.substring(0, spaceIdx);
104
- const colonIdx = fullTag.indexOf(":");
105
- const localTag = colonIdx === -1 ? fullTag : fullTag.substring(colonIdx + 1);
106
-
107
- const node: XmlNode = { tag: localTag, children: [], text: "" };
108
- stack[stack.length - 1].children.push(node);
109
- if (!selfClosing) stack.push(node);
110
- }
111
- i = closeIdx + 1;
112
- } else {
113
- const nextTag = xml.indexOf("<", i);
114
- const text = (nextTag === -1 ? xml.substring(i) : xml.substring(i, nextTag)).trim();
115
- if (text && stack.length > 1) {
116
- stack[stack.length - 1].text += text;
117
- }
118
- i = nextTag === -1 ? xml.length : nextTag;
119
- }
120
- }
102
+ class MalformedXmlError extends Error {}
121
103
 
122
- return root;
104
+ function malformed(): never {
105
+ throw new MalformedXmlError("Malformed XML");
123
106
  }
124
107
 
125
- function findNode(node: XmlNode, tagName: string): XmlNode | null {
126
- if (node.tag === tagName) return node;
127
- for (const child of node.children) {
128
- const found = findNode(child, tagName);
129
- if (found) return found;
130
- }
131
- return null;
132
- }
108
+ const PREDEFINED_ENTITIES: Record<string, string> = {
109
+ amp: "&", lt: "<", gt: ">", quot: '"', apos: "'",
110
+ };
133
111
 
134
- function extractElement(xml: string, tagName: string): string | null {
135
- const tree = parseXml(xml);
136
- const node = findNode(tree, tagName);
137
- if (!node) return null;
138
- // Rebuild inner content from children
139
- if (node.children.length === 0) return node.text || null;
140
- // For complex content, fall back to regex on the original XML
141
- const pattern = new RegExp(`<(?:[a-zA-Z0-9]+:)?${tagName}[^>]*>([\\s\\S]*?)</(?:[a-zA-Z0-9]+:)?${tagName}>`, "i");
142
- const match = xml.match(pattern);
143
- return match ? match[1] : node.text || null;
112
+ /** A code point the XML 1.0 Char production allows. */
113
+ function isXmlCharacter(codePoint: number): boolean {
114
+ return codePoint === 0x9 || codePoint === 0xa || codePoint === 0xd
115
+ || (codePoint >= 0x20 && codePoint <= 0xd7ff)
116
+ || (codePoint >= 0xe000 && codePoint <= 0xfffd)
117
+ || (codePoint >= 0x10000 && codePoint <= 0x10ffff);
144
118
  }
145
119
 
146
- function extractChildren(xml: string): Array<{ name: string; value: string }> {
147
- const tree = parseXml(xml);
148
- const target = tree.children.length === 1 ? tree.children[0] : tree;
149
- return target.children.map(c => ({ name: c.tag, value: c.text }));
120
+ /** Any character XML 1.0 forbids (NUL, most C0 controls, U+FFFE/U+FFFF, a lone surrogate). */
121
+ const ILLEGAL_XML_CHARACTER = /[^\t\n\r\u0020-\uD7FF\uE000-\uFFFD\u{10000}-\u{10FFFF}]/u;
122
+
123
+ function decodeEntities(raw: string): string {
124
+ if (!raw.includes("&")) return raw;
125
+ return raw.replace(/&(#x[0-9a-fA-F]+|#[0-9]+|[A-Za-z_][\w.-]*)?(;)?/g, (_whole, reference?: string, semicolon?: string) => {
126
+ if (!reference || !semicolon) malformed();
127
+ if (reference.startsWith("#")) {
128
+ const codePoint = reference[1] === "x" ? parseInt(reference.slice(2), 16) : parseInt(reference.slice(1), 10);
129
+ if (!isXmlCharacter(codePoint)) malformed();
130
+ return String.fromCodePoint(codePoint);
131
+ }
132
+ const value = PREDEFINED_ENTITIES[reference];
133
+ if (value === undefined) malformed();
134
+ return value;
135
+ });
150
136
  }
151
137
 
152
- function extractSoapBody(xml: string): string | null {
153
- return extractElement(xml, "Body");
138
+ const XML_NAME = `[^\\s/<>="'&!?]+`;
139
+ const START_TAG = new RegExp(`<(${XML_NAME})((?:\\s+${XML_NAME}\\s*=\\s*(?:"[^"<]*"|'[^'<]*'))*)\\s*(/?)>`, "y");
140
+ const END_TAG = new RegExp(`</(${XML_NAME})\\s*>`, "y");
141
+ const ATTRIBUTE_VALUE = /=\s*(?:"([^"<]*)"|'([^'<]*)')/g;
142
+
143
+ function parseXmlDocument(xml: string): XmlElement {
144
+ const stack: XmlElement[] = [];
145
+ let root: XmlElement | null = null;
146
+ let position = 0;
147
+
148
+ const addText = (text: string): void => {
149
+ const current = stack[stack.length - 1];
150
+ if (current.children.length === 0) current.text += text;
151
+ };
152
+
153
+ while (position < xml.length) {
154
+ const nextTag = xml.indexOf("<", position);
155
+ const textEnd = nextTag === -1 ? xml.length : nextTag;
156
+ if (textEnd > position) {
157
+ const raw = xml.slice(position, textEnd);
158
+ if (stack.length > 0) addText(decodeEntities(raw));
159
+ else if (raw.trim() !== "") malformed(); // text outside the root element
160
+ position = textEnd;
161
+ if (nextTag === -1) break;
162
+ }
163
+
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
+ }
205
+ }
206
+
207
+ if (root === null || stack.length > 0) malformed();
208
+ return root;
154
209
  }
155
210
 
156
- function extractOperation(bodyXml: string): { name: string; content: string } | null {
157
- const tree = parseXml(bodyXml);
158
- const firstChild = tree.children[0];
159
- if (!firstChild) return null;
160
- const pattern = new RegExp(`<(?:[a-zA-Z0-9]+:)?${firstChild.tag}[^>]*>([\\s\\S]*?)</(?:[a-zA-Z0-9]+:)?${firstChild.tag}>`, "i");
161
- const match = bodyXml.match(pattern);
162
- return { name: firstChild.tag, content: match ? match[1] : "" };
211
+ /** The encoding named in the XML declaration, when the document opens with one. */
212
+ const XML_DECLARED_ENCODING = /^<\?xml\s[^>]*?\bencoding\s*=\s*(["'])(.*?)\1/;
213
+
214
+ /**
215
+ * Turn the request body into text, refusing anything that is not UTF-8 XML
216
+ * BEFORE any parse: bytes that are not valid UTF-8, a byte-order mark (a
217
+ * UTF-16 body starts with one), a character XML forbids (the NULs a UTF-16
218
+ * body leaves behind once decoded as UTF-8), or an XML declaration naming an
219
+ * encoding other than UTF-8. A DOCTYPE hidden in UTF-16 or UTF-7 is therefore
220
+ * refused here, never parsed.
221
+ */
222
+ function decodeSoapBody(body: string | Uint8Array): string {
223
+ let text: string;
224
+ if (typeof body === "string") {
225
+ text = body;
226
+ } else {
227
+ try {
228
+ text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(body);
229
+ } catch {
230
+ malformed();
231
+ }
232
+ }
233
+ if (text.startsWith("\uFEFF") || ILLEGAL_XML_CHARACTER.test(text)) malformed();
234
+ // The body IS UTF-8, so a declaration naming any other encoding (UTF-7,
235
+ // UTF-16, ISO-8859-1, even "UTF8") is refused rather than trusted: a parser
236
+ // that honours it would read a different document from the one checked here.
237
+ const declaredEncoding = XML_DECLARED_ENCODING.exec(text);
238
+ if (declaredEncoding && declaredEncoding[2].toUpperCase() !== "UTF-8") malformed();
239
+ return text;
163
240
  }
164
241
 
165
242
  // ── Metadata storage ─────────────────────────────────────────
166
243
 
167
- /** Symbol key for storing operation metadata on class prototypes. */
168
- const WSDL_OPS_KEY = Symbol("wsdl_operations");
169
-
170
244
  /**
171
245
  * Decorator function for marking methods as WSDL operations.
172
246
  *
173
247
  * @WSDLOperation({ description: "Add two numbers", input: { a: "int", b: "int" }, output: { Result: "int" } })
174
248
  * async Add(a: number, b: number): Promise<Record<string, unknown>> { ... }
249
+ *
250
+ * Works under both decorator flavours: TC39 standard decorators (what tsx and
251
+ * esbuild compile by default: `(method, context)`) and TypeScript's
252
+ * experimentalDecorators (`(prototype, name, descriptor)`).
175
253
  */
176
254
  export function WSDLOperation(config?: WSDLOperationConfig) {
177
- return function (_target: unknown, propertyKey: string, descriptor: PropertyDescriptor) {
255
+ return function (
256
+ target: unknown,
257
+ context: string | symbol | { name: string | symbol },
258
+ descriptor?: PropertyDescriptor,
259
+ ): PropertyDescriptor | undefined {
260
+ const method = (descriptor ? descriptor.value : target) as { _wsdlOp?: WSDLOperationMeta };
261
+ const name = typeof context === "object" ? context.name : context;
178
262
  // Store metadata on the method itself
179
- const op: WSDLOperationMeta = {
180
- name: propertyKey,
263
+ method._wsdlOp ??= {
264
+ name: String(name),
181
265
  description: config?.description,
182
266
  input: config?.input,
183
267
  output: config?.output,
184
268
  };
185
-
186
- if (!descriptor.value._wsdlOp) {
187
- descriptor.value._wsdlOp = op;
188
- }
189
-
190
269
  return descriptor;
191
270
  };
192
271
  }
@@ -403,27 +482,41 @@ export abstract class WSDLService {
403
482
  /**
404
483
  * Handle incoming SOAP request (parse XML, dispatch to method, return SOAP response).
405
484
  */
406
- async handle(soapXml: string = ""): Promise<string> {
485
+ async handle(soapXml: string | Uint8Array = ""): Promise<string> {
407
486
  const ops = this.discoverOperations();
408
487
 
488
+ // Bytes that are not UTF-8 XML (a UTF-16 body, a byte-order mark) are
489
+ // refused before anything looks inside them.
490
+ let xml: string;
491
+ try {
492
+ xml = decodeSoapBody(soapXml);
493
+ } catch {
494
+ return this.soapFault("Client", "Malformed XML");
495
+ }
496
+
409
497
  // SOAP 1.1 (§3) forbids a Document Type Declaration in a SOAP message.
410
498
  // Rejecting any DOCTYPE/DTD up front — BEFORE the body is parsed — also
411
499
  // closes the XML entity-expansion (billion-laughs) and external-entity
412
500
  // (XXE) attack surface regardless of parser internals. The operation
413
- // never runs. (Node's parser is hand-rolled and already immune; this is
414
- // defence in depth + consistent fault behaviour across all 4 frameworks.)
415
- if (/<!DOCTYPE/i.test(soapXml)) {
501
+ // never runs.
502
+ if (/<!DOCTYPE/i.test(xml)) {
416
503
  return this.soapFault("Client", "DOCTYPE declarations are not allowed in SOAP messages");
417
504
  }
418
505
 
419
- // Parse SOAP body
420
- const body = extractSoapBody(soapXml);
506
+ let envelope: XmlElement;
507
+ try {
508
+ envelope = parseXmlDocument(xml);
509
+ } catch {
510
+ return this.soapFault("Client", "Malformed XML");
511
+ }
512
+
513
+ // The Body is a direct child of the envelope; its first child is the operation.
514
+ const body = envelope.children.find((child) => child.name === "Body");
421
515
  if (!body) {
422
516
  return this.soapFault("Client", "Missing SOAP Body");
423
517
  }
424
518
 
425
- // Extract operation
426
- const operation = extractOperation(body);
519
+ const operation = body.children[0];
427
520
  if (!operation) {
428
521
  return this.soapFault("Client", "Empty SOAP Body");
429
522
  }
@@ -447,15 +540,14 @@ export abstract class WSDLService {
447
540
  // non-numeric value for an int/float param (convertValue throws, matching
448
541
  // Python's int()/float() raise) becomes a Server fault — not a silent NaN.
449
542
  try {
450
- // Extract parameters from the operation element
451
- const children = extractChildren(operation.content);
543
+ // Extract parameters from the operation element's children
452
544
  const params: unknown[] = [];
453
545
 
454
546
  if (opMeta.input) {
455
547
  for (const [paramName, paramType] of Object.entries(opMeta.input)) {
456
- const child = children.find((c) => c.name === paramName);
548
+ const child = operation.children.find((c) => c.name === paramName);
457
549
  if (child) {
458
- params.push(this.convertValue(child.value, paramType));
550
+ params.push(this.convertValue(child.text, paramType));
459
551
  } else {
460
552
  params.push(null);
461
553
  }
@@ -549,29 +641,19 @@ export abstract class WSDLService {
549
641
  * Handle POST request — process SOAP XML.
550
642
  */
551
643
  private async handlePostRequest(req: Record<string, unknown>, res: Record<string, unknown>): Promise<void> {
552
- let xmlBody = "";
644
+ let xmlBody: string | Uint8Array = "";
553
645
 
554
- // Try to get body from request object
555
- if (typeof req.rawBody === "string") {
646
+ // Try to get body from request object (raw bytes are checked for UTF-8 in handle())
647
+ if (typeof req.rawBody === "string" || req.rawBody instanceof Uint8Array) {
556
648
  xmlBody = req.rawBody;
557
- } else if (typeof req.body === "string") {
649
+ } else if (typeof req.body === "string" || req.body instanceof Uint8Array) {
558
650
  xmlBody = req.body;
559
651
  } else if (typeof req.body === "object" && req.body !== null) {
560
652
  xmlBody = JSON.stringify(req.body);
561
653
  }
562
654
 
563
- if (!xmlBody) {
564
- const fault = this.soapFault("Client", "Empty request body");
565
- if (typeof res.send === "function") {
566
- if (typeof res.status === "function") (res.status as Function)(400);
567
- if (typeof res.setHeader === "function") {
568
- (res.setHeader as Function)("Content-Type", "text/xml; charset=UTF-8");
569
- }
570
- (res.send as Function)(fault);
571
- }
572
- return;
573
- }
574
-
655
+ // An empty body is not XML: handle() answers it with the same Client
656
+ // "Malformed XML" fault as any other unparsable body (Python parity).
575
657
  const soapResponse = await this.handle(xmlBody);
576
658
 
577
659
  if (typeof res.send === "function") {
@@ -1,3 +1,11 @@
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
+
1
9
  {% extends "errors/base.twig" %}
2
10
  {% block title %}Redirecting…{% endblock %}
3
11
  {% block meta %}<meta http-equiv="refresh" content="0;url={{ redirect_url }}">{% endblock %}
@@ -1,3 +1,11 @@
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
+
1
9
  {% extends "errors/base.twig" %}
2
10
  {% block title %}401 — Unauthorized{% endblock %}
3
11
  {% block content %}
@@ -1,4 +1,12 @@
1
1
  <!DOCTYPE html>
2
+ {#
3
+ Copyright (c) 2026 Code Infinity
4
+ SPDX-License-Identifier: MPL-2.0
5
+ This Source Code Form is subject to the terms of the Mozilla Public
6
+ License, v. 2.0. If a copy of the MPL was not distributed with this
7
+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
8
+ #}
9
+
2
10
  <html lang="en">
3
11
  <head>
4
12
  <meta charset="utf-8">
@@ -1,4 +1,12 @@
1
1
  <!DOCTYPE html>
2
+ {#
3
+ Copyright (c) 2026 Code Infinity
4
+ SPDX-License-Identifier: MPL-2.0
5
+ This Source Code Form is subject to the terms of the Mozilla Public
6
+ License, v. 2.0. If a copy of the MPL was not distributed with this
7
+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
8
+ #}
9
+
2
10
  <html lang="en">
3
11
  <head>
4
12
  <meta charset="utf-8">
@@ -1,4 +1,12 @@
1
1
  <!DOCTYPE html>
2
+ {#
3
+ Copyright (c) 2026 Code Infinity
4
+ SPDX-License-Identifier: MPL-2.0
5
+ This Source Code Form is subject to the terms of the Mozilla Public
6
+ License, v. 2.0. If a copy of the MPL was not distributed with this
7
+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
8
+ #}
9
+
2
10
  <html lang="en">
3
11
  <head>
4
12
  <meta charset="utf-8">
@@ -1,3 +1,11 @@
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
+
1
9
  {% extends "errors/base.twig" %}
2
10
  {% block title %}502 — Bad Gateway{% endblock %}
3
11
  {% block content %}
@@ -1,3 +1,11 @@
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
+
1
9
  {% extends "errors/base.twig" %}
2
10
  {% block title %}503 — Service Unavailable{% endblock %}
3
11
  {% block extra_styles %}
@@ -1,4 +1,12 @@
1
1
  <!DOCTYPE html>
2
+ {#
3
+ Copyright (c) 2026 Code Infinity
4
+ SPDX-License-Identifier: MPL-2.0
5
+ This Source Code Form is subject to the terms of the Mozilla Public
6
+ License, v. 2.0. If a copy of the MPL was not distributed with this
7
+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
8
+ #}
9
+
2
10
  <html lang="en">
3
11
  <head>
4
12
  <meta charset="UTF-8">