tina4-nodejs 3.0.0-rc.2

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 (119) hide show
  1. package/BENCHMARK_REPORT.md +96 -0
  2. package/CARBONAH.md +140 -0
  3. package/CLAUDE.md +599 -0
  4. package/COMPARISON.md +194 -0
  5. package/README.md +595 -0
  6. package/package.json +59 -0
  7. package/packages/cli/src/bin.ts +110 -0
  8. package/packages/cli/src/commands/init.ts +194 -0
  9. package/packages/cli/src/commands/migrate.ts +96 -0
  10. package/packages/cli/src/commands/migrateCreate.ts +59 -0
  11. package/packages/cli/src/commands/routes.ts +61 -0
  12. package/packages/cli/src/commands/serve.ts +58 -0
  13. package/packages/cli/src/commands/test.ts +83 -0
  14. package/packages/core/gallery/auth/meta.json +1 -0
  15. package/packages/core/gallery/auth/src/routes/api/gallery/auth/login/post.ts +22 -0
  16. package/packages/core/gallery/auth/src/routes/api/gallery/auth/verify/get.ts +16 -0
  17. package/packages/core/gallery/auth/src/routes/gallery/auth/get.ts +97 -0
  18. package/packages/core/gallery/database/meta.json +1 -0
  19. package/packages/core/gallery/database/src/routes/api/gallery/db/notes/get.ts +13 -0
  20. package/packages/core/gallery/database/src/routes/api/gallery/db/notes/post.ts +17 -0
  21. package/packages/core/gallery/database/src/routes/api/gallery/db/tables/get.ts +23 -0
  22. package/packages/core/gallery/error-overlay/meta.json +1 -0
  23. package/packages/core/gallery/error-overlay/src/routes/api/gallery/crash/get.ts +17 -0
  24. package/packages/core/gallery/orm/meta.json +1 -0
  25. package/packages/core/gallery/orm/src/routes/api/gallery/products/get.ts +12 -0
  26. package/packages/core/gallery/orm/src/routes/api/gallery/products/post.ts +7 -0
  27. package/packages/core/gallery/queue/meta.json +1 -0
  28. package/packages/core/gallery/queue/src/routes/api/gallery/queue/produce/post.ts +16 -0
  29. package/packages/core/gallery/queue/src/routes/api/gallery/queue/status/get.ts +10 -0
  30. package/packages/core/gallery/rest-api/meta.json +1 -0
  31. package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/get.ts +6 -0
  32. package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/post.ts +7 -0
  33. package/packages/core/gallery/templates/meta.json +1 -0
  34. package/packages/core/gallery/templates/src/routes/gallery/page/get.ts +15 -0
  35. package/packages/core/gallery/templates/src/templates/gallery_page.twig +257 -0
  36. package/packages/core/public/css/tina4.css +2463 -0
  37. package/packages/core/public/css/tina4.min.css +1 -0
  38. package/packages/core/public/favicon.ico +0 -0
  39. package/packages/core/public/images/logo.svg +5 -0
  40. package/packages/core/public/images/tina4-logo-icon.webp +0 -0
  41. package/packages/core/public/js/frond.min.js +420 -0
  42. package/packages/core/public/js/tina4-dev-admin.min.js +327 -0
  43. package/packages/core/public/js/tina4.min.js +93 -0
  44. package/packages/core/public/swagger/index.html +90 -0
  45. package/packages/core/public/swagger/oauth2-redirect.html +63 -0
  46. package/packages/core/src/ai.ts +359 -0
  47. package/packages/core/src/api.ts +248 -0
  48. package/packages/core/src/auth.ts +287 -0
  49. package/packages/core/src/cache.ts +121 -0
  50. package/packages/core/src/constants.ts +48 -0
  51. package/packages/core/src/container.ts +90 -0
  52. package/packages/core/src/devAdmin.ts +2024 -0
  53. package/packages/core/src/devMailbox.ts +316 -0
  54. package/packages/core/src/dotenv.ts +172 -0
  55. package/packages/core/src/errorOverlay.test.ts +122 -0
  56. package/packages/core/src/errorOverlay.ts +278 -0
  57. package/packages/core/src/events.ts +112 -0
  58. package/packages/core/src/fakeData.ts +309 -0
  59. package/packages/core/src/graphql.ts +812 -0
  60. package/packages/core/src/health.ts +31 -0
  61. package/packages/core/src/htmlElement.ts +172 -0
  62. package/packages/core/src/i18n.ts +136 -0
  63. package/packages/core/src/index.ts +88 -0
  64. package/packages/core/src/logger.ts +226 -0
  65. package/packages/core/src/messenger.ts +822 -0
  66. package/packages/core/src/middleware.ts +138 -0
  67. package/packages/core/src/queue.ts +481 -0
  68. package/packages/core/src/queueBackends/kafkaBackend.ts +348 -0
  69. package/packages/core/src/queueBackends/rabbitmqBackend.ts +479 -0
  70. package/packages/core/src/rateLimiter.ts +107 -0
  71. package/packages/core/src/request.ts +189 -0
  72. package/packages/core/src/response.ts +146 -0
  73. package/packages/core/src/routeDiscovery.ts +87 -0
  74. package/packages/core/src/router.ts +398 -0
  75. package/packages/core/src/scss.ts +366 -0
  76. package/packages/core/src/server.ts +610 -0
  77. package/packages/core/src/service.ts +380 -0
  78. package/packages/core/src/session.ts +480 -0
  79. package/packages/core/src/sessionHandlers/mongoHandler.ts +286 -0
  80. package/packages/core/src/sessionHandlers/valkeyHandler.ts +184 -0
  81. package/packages/core/src/static.ts +58 -0
  82. package/packages/core/src/testing.ts +233 -0
  83. package/packages/core/src/types.ts +98 -0
  84. package/packages/core/src/watcher.ts +37 -0
  85. package/packages/core/src/websocket.ts +408 -0
  86. package/packages/core/src/wsdl.ts +546 -0
  87. package/packages/core/templates/errors/302.twig +14 -0
  88. package/packages/core/templates/errors/401.twig +9 -0
  89. package/packages/core/templates/errors/403.twig +29 -0
  90. package/packages/core/templates/errors/404.twig +29 -0
  91. package/packages/core/templates/errors/500.twig +38 -0
  92. package/packages/core/templates/errors/502.twig +9 -0
  93. package/packages/core/templates/errors/503.twig +12 -0
  94. package/packages/core/templates/errors/base.twig +37 -0
  95. package/packages/frond/src/engine.ts +1475 -0
  96. package/packages/frond/src/index.ts +2 -0
  97. package/packages/orm/src/adapters/firebird.ts +455 -0
  98. package/packages/orm/src/adapters/mssql.ts +440 -0
  99. package/packages/orm/src/adapters/mysql.ts +355 -0
  100. package/packages/orm/src/adapters/postgres.ts +362 -0
  101. package/packages/orm/src/adapters/sqlite.ts +270 -0
  102. package/packages/orm/src/autoCrud.ts +231 -0
  103. package/packages/orm/src/baseModel.ts +536 -0
  104. package/packages/orm/src/database.ts +321 -0
  105. package/packages/orm/src/fakeData.ts +118 -0
  106. package/packages/orm/src/index.ts +49 -0
  107. package/packages/orm/src/migration.ts +392 -0
  108. package/packages/orm/src/model.ts +56 -0
  109. package/packages/orm/src/query.ts +113 -0
  110. package/packages/orm/src/seeder.ts +120 -0
  111. package/packages/orm/src/sqlTranslation.ts +272 -0
  112. package/packages/orm/src/types.ts +110 -0
  113. package/packages/orm/src/validation.ts +93 -0
  114. package/packages/swagger/src/generator.ts +189 -0
  115. package/packages/swagger/src/index.ts +2 -0
  116. package/packages/swagger/src/ui.ts +48 -0
  117. package/skills/tina4-developer.skill +0 -0
  118. package/skills/tina4-js.skill +0 -0
  119. package/skills/tina4-maintainer.skill +0 -0
@@ -0,0 +1,546 @@
1
+ /**
2
+ * Tina4 WSDL/SOAP — SOAP 1.1 / WSDL 1.1 service base class.
3
+ *
4
+ * Auto-generates WSDL definitions and handles SOAP XML requests.
5
+ * Zero external dependencies — uses simple string parsing for XML.
6
+ *
7
+ * Matches the PHP reference implementation (Tina4\WSDL).
8
+ *
9
+ * import { WSDLService, WSDLOp } from "@tina4/core";
10
+ *
11
+ * class Calculator extends WSDLService {
12
+ * serviceName = "Calculator";
13
+ * serviceUrl = "/api/calculator";
14
+ *
15
+ * @WSDLOp({ output: { Result: "int" } })
16
+ * async Add(a: number, b: number): Promise<Record<string, unknown>> {
17
+ * return { Result: a + b };
18
+ * }
19
+ * }
20
+ */
21
+
22
+ // ── Types ────────────────────────────────────────────────────
23
+
24
+ export interface WSDLOperation {
25
+ name: string;
26
+ description?: string;
27
+ input?: Record<string, string>; // param name -> type
28
+ output?: Record<string, string>; // return name -> type
29
+ }
30
+
31
+ interface WSDLOpConfig {
32
+ description?: string;
33
+ input?: Record<string, string>;
34
+ output?: Record<string, string>;
35
+ }
36
+
37
+ // ── Namespace constants ──────────────────────────────────────
38
+
39
+ const NS_SOAP = "http://schemas.xmlsoap.org/wsdl/soap/";
40
+ const NS_WSDL = "http://schemas.xmlsoap.org/wsdl/";
41
+ const NS_XSD = "http://www.w3.org/2001/XMLSchema";
42
+ const NS_SOAP_ENV = "http://schemas.xmlsoap.org/soap/envelope/";
43
+
44
+ /** TypeScript/JavaScript type name to XSD type mapping. */
45
+ const TYPE_MAP: Record<string, string> = {
46
+ int: "xsd:int",
47
+ integer: "xsd:int",
48
+ float: "xsd:float",
49
+ double: "xsd:double",
50
+ number: "xsd:double",
51
+ numeric: "xsd:double",
52
+ string: "xsd:string",
53
+ bool: "xsd:boolean",
54
+ boolean: "xsd:boolean",
55
+ };
56
+
57
+ // ── XML helpers ──────────────────────────────────────────────
58
+
59
+ /**
60
+ * Escape special XML characters.
61
+ */
62
+ function escapeXml(value: string): string {
63
+ return value
64
+ .replace(/&/g, "&amp;")
65
+ .replace(/</g, "&lt;")
66
+ .replace(/>/g, "&gt;")
67
+ .replace(/"/g, "&quot;")
68
+ .replace(/'/g, "&apos;");
69
+ }
70
+
71
+ /**
72
+ * Extract text content from an XML element by tag name (simple string parser).
73
+ * Returns the text content of the first matching element, or null.
74
+ */
75
+ function extractElement(xml: string, tagName: string): string | null {
76
+ // Try with namespace prefix variations
77
+ const patterns = [
78
+ new RegExp(`<(?:[a-zA-Z0-9]+:)?${tagName}[^>]*>([\\s\\S]*?)</(?:[a-zA-Z0-9]+:)?${tagName}>`, "i"),
79
+ new RegExp(`<${tagName}[^>]*>([\\s\\S]*?)</${tagName}>`, "i"),
80
+ ];
81
+
82
+ for (const pattern of patterns) {
83
+ const match = xml.match(pattern);
84
+ if (match) return match[1];
85
+ }
86
+
87
+ return null;
88
+ }
89
+
90
+ /**
91
+ * Extract all direct child elements with their tag names and text content.
92
+ * Returns an array of { name, value } pairs.
93
+ */
94
+ function extractChildren(xml: string): Array<{ name: string; value: string }> {
95
+ const results: Array<{ name: string; value: string }> = [];
96
+ // Match opening tags, capturing name (strip namespace prefix) and content
97
+ const pattern = /<(?:[a-zA-Z0-9]+:)?([a-zA-Z0-9_]+)[^>]*>([\s\S]*?)<\/(?:[a-zA-Z0-9]+:)?(\1)[^>]*>/g;
98
+
99
+ let match: RegExpExecArray | null;
100
+ while ((match = pattern.exec(xml)) !== null) {
101
+ results.push({ name: match[1], value: match[2].trim() });
102
+ }
103
+
104
+ return results;
105
+ }
106
+
107
+ /**
108
+ * Extract the SOAP Body content from a SOAP envelope.
109
+ */
110
+ function extractSoapBody(xml: string): string | null {
111
+ return extractElement(xml, "Body");
112
+ }
113
+
114
+ /**
115
+ * Extract the operation element from the SOAP body.
116
+ * Returns { name, content } or null.
117
+ */
118
+ function extractOperation(bodyXml: string): { name: string; content: string } | null {
119
+ // The first child element of Body is the operation
120
+ const match = bodyXml.match(/<(?:[a-zA-Z0-9]+:)?([a-zA-Z0-9_]+)[^>]*>([\s\S]*?)<\/(?:[a-zA-Z0-9]+:)?\1[^>]*>/i);
121
+ if (match) {
122
+ return { name: match[1], content: match[2] };
123
+ }
124
+ return null;
125
+ }
126
+
127
+ // ── Metadata storage ─────────────────────────────────────────
128
+
129
+ /** Symbol key for storing operation metadata on class prototypes. */
130
+ const WSDL_OPS_KEY = Symbol("wsdl_operations");
131
+
132
+ /**
133
+ * Decorator function for marking methods as WSDL operations.
134
+ *
135
+ * @WSDLOp({ description: "Add two numbers", input: { a: "int", b: "int" }, output: { Result: "int" } })
136
+ * async Add(a: number, b: number): Promise<Record<string, unknown>> { ... }
137
+ */
138
+ export function WSDLOp(config?: WSDLOpConfig) {
139
+ return function (_target: unknown, propertyKey: string, descriptor: PropertyDescriptor) {
140
+ // Store metadata on the method itself
141
+ const op: WSDLOperation = {
142
+ name: propertyKey,
143
+ description: config?.description,
144
+ input: config?.input,
145
+ output: config?.output,
146
+ };
147
+
148
+ if (!descriptor.value._wsdlOp) {
149
+ descriptor.value._wsdlOp = op;
150
+ }
151
+
152
+ return descriptor;
153
+ };
154
+ }
155
+
156
+ // ── WSDLService ──────────────────────────────────────────────
157
+
158
+ export abstract class WSDLService {
159
+ abstract serviceName: string;
160
+ abstract serviceUrl: string;
161
+
162
+ protected namespace: string = "http://tina4.com/wsdl";
163
+
164
+ /** Discovered operations (populated on first use). */
165
+ private _operations: Map<string, WSDLOperation> | null = null;
166
+
167
+ /**
168
+ * Discover operations by scanning for methods with _wsdlOp metadata.
169
+ */
170
+ private discoverOperations(): Map<string, WSDLOperation> {
171
+ if (this._operations) return this._operations;
172
+
173
+ this._operations = new Map();
174
+
175
+ // Walk the prototype chain to find decorated methods
176
+ let proto = Object.getPrototypeOf(this);
177
+ while (proto && proto !== WSDLService.prototype && proto !== Object.prototype) {
178
+ const names = Object.getOwnPropertyNames(proto);
179
+ for (const name of names) {
180
+ if (name === "constructor") continue;
181
+ try {
182
+ const method = (this as Record<string, unknown>)[name];
183
+ if (typeof method === "function" && (method as unknown as Record<string, unknown>)._wsdlOp) {
184
+ const op = (method as unknown as Record<string, unknown>)._wsdlOp as WSDLOperation;
185
+ if (!this._operations.has(name)) {
186
+ this._operations.set(name, op);
187
+ }
188
+ }
189
+ } catch {
190
+ // skip non-accessible properties
191
+ }
192
+ }
193
+ proto = Object.getPrototypeOf(proto);
194
+ }
195
+
196
+ return this._operations;
197
+ }
198
+
199
+ /**
200
+ * Map a type name to an XSD type string.
201
+ */
202
+ private typeToXsd(typeName: string): string {
203
+ return TYPE_MAP[typeName] ?? "xsd:string";
204
+ }
205
+
206
+ /**
207
+ * Convert a string value from XML to the target type.
208
+ */
209
+ private convertValue(value: string, typeName: string): unknown {
210
+ switch (typeName) {
211
+ case "int":
212
+ case "integer":
213
+ return parseInt(value, 10);
214
+ case "float":
215
+ case "double":
216
+ case "number":
217
+ case "numeric":
218
+ return parseFloat(value);
219
+ case "bool":
220
+ case "boolean":
221
+ return ["true", "1", "yes"].includes(value.toLowerCase());
222
+ default:
223
+ return value;
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Generate WSDL 1.1 XML document.
229
+ */
230
+ generateWSDL(endpointUrl?: string): string {
231
+ const ops = this.discoverOperations();
232
+ const tns = `urn:${this.serviceName}`;
233
+ const url = endpointUrl ?? this.serviceUrl;
234
+ const parts: string[] = [];
235
+
236
+ parts.push('<?xml version="1.0" encoding="UTF-8"?>');
237
+ parts.push(`<definitions name="${this.serviceName}"`);
238
+ parts.push(` targetNamespace="${tns}"`);
239
+ parts.push(` xmlns:tns="${tns}"`);
240
+ parts.push(` xmlns:soap="${NS_SOAP}"`);
241
+ parts.push(` xmlns:xsd="${NS_XSD}"`);
242
+ parts.push(` xmlns="${NS_WSDL}">`);
243
+ parts.push("");
244
+
245
+ // Types
246
+ parts.push(" <types>");
247
+ parts.push(` <xsd:schema targetNamespace="${tns}">`);
248
+
249
+ for (const [opName, op] of ops) {
250
+ // Request element
251
+ parts.push(` <xsd:element name="${opName}">`);
252
+ parts.push(" <xsd:complexType>");
253
+ parts.push(" <xsd:sequence>");
254
+
255
+ if (op.input) {
256
+ for (const [paramName, paramType] of Object.entries(op.input)) {
257
+ const xsdType = this.typeToXsd(paramType);
258
+ parts.push(` <xsd:element name="${paramName}" type="${xsdType}"/>`);
259
+ }
260
+ }
261
+
262
+ parts.push(" </xsd:sequence>");
263
+ parts.push(" </xsd:complexType>");
264
+ parts.push(` </xsd:element>`);
265
+
266
+ // Response element
267
+ parts.push(` <xsd:element name="${opName}Response">`);
268
+ parts.push(" <xsd:complexType>");
269
+ parts.push(" <xsd:sequence>");
270
+
271
+ if (op.output) {
272
+ for (const [retName, retType] of Object.entries(op.output)) {
273
+ const xsdType = this.typeToXsd(retType);
274
+ parts.push(` <xsd:element name="${retName}" type="${xsdType}"/>`);
275
+ }
276
+ }
277
+
278
+ parts.push(" </xsd:sequence>");
279
+ parts.push(" </xsd:complexType>");
280
+ parts.push(` </xsd:element>`);
281
+ }
282
+
283
+ parts.push(" </xsd:schema>");
284
+ parts.push(" </types>");
285
+ parts.push("");
286
+
287
+ // Messages
288
+ for (const [opName] of ops) {
289
+ parts.push(` <message name="${opName}Input">`);
290
+ parts.push(` <part name="parameters" element="tns:${opName}"/>`);
291
+ parts.push(" </message>");
292
+ parts.push(` <message name="${opName}Output">`);
293
+ parts.push(` <part name="parameters" element="tns:${opName}Response"/>`);
294
+ parts.push(" </message>");
295
+ }
296
+ parts.push("");
297
+
298
+ // PortType
299
+ parts.push(` <portType name="${this.serviceName}PortType">`);
300
+ for (const [opName] of ops) {
301
+ parts.push(` <operation name="${opName}">`);
302
+ parts.push(` <input message="tns:${opName}Input"/>`);
303
+ parts.push(` <output message="tns:${opName}Output"/>`);
304
+ parts.push(" </operation>");
305
+ }
306
+ parts.push(" </portType>");
307
+ parts.push("");
308
+
309
+ // Binding
310
+ parts.push(` <binding name="${this.serviceName}Binding" type="tns:${this.serviceName}PortType">`);
311
+ parts.push(' <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>');
312
+ for (const [opName] of ops) {
313
+ parts.push(` <operation name="${opName}">`);
314
+ parts.push(` <soap:operation soapAction="${tns}/${opName}"/>`);
315
+ parts.push(' <input><soap:body use="literal"/></input>');
316
+ parts.push(' <output><soap:body use="literal"/></output>');
317
+ parts.push(" </operation>");
318
+ }
319
+ parts.push(" </binding>");
320
+ parts.push("");
321
+
322
+ // Service
323
+ parts.push(` <service name="${this.serviceName}">`);
324
+ parts.push(` <port name="${this.serviceName}Port" binding="tns:${this.serviceName}Binding">`);
325
+ parts.push(` <soap:address location="${url}"/>`);
326
+ parts.push(" </port>");
327
+ parts.push(" </service>");
328
+
329
+ parts.push("</definitions>");
330
+
331
+ return parts.join("\n");
332
+ }
333
+
334
+ /**
335
+ * Handle incoming SOAP request (parse XML, dispatch to method, return SOAP response).
336
+ */
337
+ async handleRequest(soapXml: string): Promise<string> {
338
+ const ops = this.discoverOperations();
339
+
340
+ // Parse SOAP body
341
+ const body = extractSoapBody(soapXml);
342
+ if (!body) {
343
+ return this.soapFault("Client", "Missing SOAP Body");
344
+ }
345
+
346
+ // Extract operation
347
+ const operation = extractOperation(body);
348
+ if (!operation) {
349
+ return this.soapFault("Client", "Empty SOAP Body");
350
+ }
351
+
352
+ const opName = operation.name;
353
+ const opMeta = ops.get(opName);
354
+ if (!opMeta) {
355
+ return this.soapFault("Client", `Unknown operation: ${opName}`);
356
+ }
357
+
358
+ // Check the method exists on this instance
359
+ const method = (this as Record<string, unknown>)[opName];
360
+ if (typeof method !== "function") {
361
+ return this.soapFault("Client", `Operation not implemented: ${opName}`);
362
+ }
363
+
364
+ // Extract parameters from the operation element
365
+ const children = extractChildren(operation.content);
366
+ const params: unknown[] = [];
367
+
368
+ if (opMeta.input) {
369
+ for (const [paramName, paramType] of Object.entries(opMeta.input)) {
370
+ const child = children.find((c) => c.name === paramName);
371
+ if (child) {
372
+ params.push(this.convertValue(child.value, paramType));
373
+ } else {
374
+ params.push(null);
375
+ }
376
+ }
377
+ }
378
+
379
+ // Invoke the method
380
+ try {
381
+ const result = await (method as (...args: unknown[]) => Promise<unknown>).call(this, ...params);
382
+ return this.soapResponse(opName, result as Record<string, unknown>);
383
+ } catch (err) {
384
+ const errMsg = err instanceof Error ? err.message : String(err);
385
+ return this.soapFault("Server", errMsg);
386
+ }
387
+ }
388
+
389
+ /**
390
+ * Register this service's routes on a router.
391
+ * GET /service-url?wsdl -> WSDL XML
392
+ * POST /service-url -> Handle SOAP request
393
+ */
394
+ register(router: {
395
+ addRoute?: (method: string, path: string, handler: (req: unknown, res: unknown) => void) => void;
396
+ }): void {
397
+ if (!router.addRoute) {
398
+ // Try to use the router as an object with get/post methods
399
+ const r = router as Record<string, unknown>;
400
+
401
+ // Register GET for WSDL
402
+ if (typeof r.get === "function") {
403
+ (r.get as Function)(this.serviceUrl, (req: Record<string, unknown>, res: Record<string, unknown>) => {
404
+ this.handleGetRequest(req, res);
405
+ });
406
+ }
407
+
408
+ // Register POST for SOAP
409
+ if (typeof r.post === "function") {
410
+ (r.post as Function)(this.serviceUrl, async (req: Record<string, unknown>, res: Record<string, unknown>) => {
411
+ await this.handlePostRequest(req, res);
412
+ });
413
+ }
414
+
415
+ return;
416
+ }
417
+
418
+ // Use addRoute if available
419
+ router.addRoute("GET", this.serviceUrl, (req, res) => {
420
+ this.handleGetRequest(req as Record<string, unknown>, res as Record<string, unknown>);
421
+ });
422
+
423
+ router.addRoute("POST", this.serviceUrl, async (req, res) => {
424
+ await this.handlePostRequest(req as Record<string, unknown>, res as Record<string, unknown>);
425
+ });
426
+ }
427
+
428
+ /**
429
+ * Handle GET request — return WSDL XML.
430
+ */
431
+ private handleGetRequest(req: Record<string, unknown>, res: Record<string, unknown>): void {
432
+ // Infer endpoint URL from request if possible
433
+ let endpointUrl = this.serviceUrl;
434
+ if (req.headers && typeof req.headers === "object") {
435
+ const headers = req.headers as Record<string, string>;
436
+ const host = headers.host ?? "localhost";
437
+ const protocol = headers["x-forwarded-proto"] ?? "http";
438
+ endpointUrl = `${protocol}://${host}${this.serviceUrl}`;
439
+ }
440
+
441
+ const wsdl = this.generateWSDL(endpointUrl);
442
+
443
+ if (typeof res.send === "function") {
444
+ // Set content type if possible
445
+ if (typeof res.setHeader === "function") {
446
+ (res.setHeader as Function)("Content-Type", "text/xml; charset=UTF-8");
447
+ }
448
+ (res.send as Function)(wsdl);
449
+ } else if (typeof res.end === "function") {
450
+ if (typeof res.writeHead === "function") {
451
+ (res.writeHead as Function)(200, { "Content-Type": "text/xml; charset=UTF-8" });
452
+ }
453
+ (res.end as Function)(wsdl);
454
+ }
455
+ }
456
+
457
+ /**
458
+ * Handle POST request — process SOAP XML.
459
+ */
460
+ private async handlePostRequest(req: Record<string, unknown>, res: Record<string, unknown>): Promise<void> {
461
+ let xmlBody = "";
462
+
463
+ // Try to get body from request object
464
+ if (typeof req.rawBody === "string") {
465
+ xmlBody = req.rawBody;
466
+ } else if (typeof req.body === "string") {
467
+ xmlBody = req.body;
468
+ } else if (typeof req.body === "object" && req.body !== null) {
469
+ xmlBody = JSON.stringify(req.body);
470
+ }
471
+
472
+ if (!xmlBody) {
473
+ const fault = this.soapFault("Client", "Empty request body");
474
+ if (typeof res.send === "function") {
475
+ if (typeof res.status === "function") (res.status as Function)(400);
476
+ if (typeof res.setHeader === "function") {
477
+ (res.setHeader as Function)("Content-Type", "text/xml; charset=UTF-8");
478
+ }
479
+ (res.send as Function)(fault);
480
+ }
481
+ return;
482
+ }
483
+
484
+ const soapResponse = await this.handleRequest(xmlBody);
485
+
486
+ if (typeof res.send === "function") {
487
+ if (typeof res.setHeader === "function") {
488
+ (res.setHeader as Function)("Content-Type", "text/xml; charset=UTF-8");
489
+ }
490
+ (res.send as Function)(soapResponse);
491
+ } else if (typeof res.end === "function") {
492
+ if (typeof res.writeHead === "function") {
493
+ (res.writeHead as Function)(200, { "Content-Type": "text/xml; charset=UTF-8" });
494
+ }
495
+ (res.end as Function)(soapResponse);
496
+ }
497
+ }
498
+
499
+ /**
500
+ * Build a SOAP response XML envelope.
501
+ */
502
+ private soapResponse(opName: string, result: Record<string, unknown>): string {
503
+ const parts: string[] = [];
504
+ parts.push('<?xml version="1.0" encoding="UTF-8"?>');
505
+ parts.push(`<soap:Envelope xmlns:soap="${NS_SOAP_ENV}">`);
506
+ parts.push("<soap:Body>");
507
+ parts.push(`<${opName}Response>`);
508
+
509
+ if (result && typeof result === "object") {
510
+ for (const [key, value] of Object.entries(result)) {
511
+ if (value === null || value === undefined) {
512
+ parts.push(`<${key} xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>`);
513
+ } else if (Array.isArray(value)) {
514
+ for (const item of value) {
515
+ parts.push(`<${key}>${escapeXml(String(item))}</${key}>`);
516
+ }
517
+ } else if (typeof value === "boolean") {
518
+ parts.push(`<${key}>${value ? "true" : "false"}</${key}>`);
519
+ } else {
520
+ parts.push(`<${key}>${escapeXml(String(value))}</${key}>`);
521
+ }
522
+ }
523
+ }
524
+
525
+ parts.push(`</${opName}Response>`);
526
+ parts.push("</soap:Body>");
527
+ parts.push("</soap:Envelope>");
528
+
529
+ return parts.join("\n");
530
+ }
531
+
532
+ /**
533
+ * Build a SOAP fault response XML.
534
+ */
535
+ private soapFault(code: string, message: string): string {
536
+ return '<?xml version="1.0" encoding="UTF-8"?>'
537
+ + `<soap:Envelope xmlns:soap="${NS_SOAP_ENV}">`
538
+ + "<soap:Body>"
539
+ + "<soap:Fault>"
540
+ + `<faultcode>${code}</faultcode>`
541
+ + `<faultstring>${escapeXml(message)}</faultstring>`
542
+ + "</soap:Fault>"
543
+ + "</soap:Body>"
544
+ + "</soap:Envelope>";
545
+ }
546
+ }
@@ -0,0 +1,14 @@
1
+ {% extends "errors/base.twig" %}
2
+ {% block title %}Redirecting…{% endblock %}
3
+ {% block meta %}<meta http-equiv="refresh" content="0;url={{ redirect_url }}">{% endblock %}
4
+ {% block extra_styles %}
5
+ .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; }
6
+ @keyframes spin { to { transform: rotate(360deg); } }
7
+ {% endblock %}
8
+ {% block content %}
9
+ <div class="spinner"></div>
10
+ <div class="error-title">Redirecting…</div>
11
+ <div class="error-msg">You are being redirected to a new location.</div>
12
+ <div class="error-path" style="color:var(--primary)">{{ redirect_url }}</div>
13
+ <a href="{{ redirect_url }}" class="error-home">Click here if not redirected</a>
14
+ {% endblock %}
@@ -0,0 +1,9 @@
1
+ {% extends "errors/base.twig" %}
2
+ {% block title %}401 — Unauthorized{% endblock %}
3
+ {% block content %}
4
+ <div class="error-code" style="color:var(--danger)">401</div>
5
+ <div class="error-title">Unauthorized</div>
6
+ <div class="error-msg">You need to sign in to access this resource.</div>
7
+ <div class="error-path" style="color:var(--danger)">{{ path }}</div>
8
+ <a href="/" class="error-home">Go Home</a>
9
+ {% endblock %}
@@ -0,0 +1,29 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>403 — Forbidden</title>
7
+ <style>
8
+ * { box-sizing: border-box; margin: 0; padding: 0; }
9
+ body { font-family: system-ui, -apple-system, sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
10
+ .error-card { background: #1e293b; border: 1px solid #334155; border-radius: 1rem; padding: 3rem; text-align: center; max-width: 520px; width: 90%; }
11
+ .error-code { font-size: 8rem; font-weight: 900; color: #f59e0b; opacity: 0.6; line-height: 1; margin-bottom: 0.5rem; }
12
+ .error-title { font-size: 1.5rem; font-weight: 700; margin-bottom: 0.75rem; }
13
+ .error-msg { color: #94a3b8; font-size: 1rem; margin-bottom: 1.5rem; line-height: 1.5; }
14
+ .error-path { font-family: 'SF Mono', monospace; background: #0f172a; color: #f59e0b; padding: 0.5rem 1rem; border-radius: 0.5rem; font-size: 0.85rem; word-break: break-all; margin-bottom: 1.5rem; display: inline-block; }
15
+ .error-home { display: inline-block; padding: 0.6rem 2rem; background: #3b82f6; color: #fff; text-decoration: none; border-radius: 0.5rem; font-size: 0.9rem; font-weight: 600; }
16
+ .error-home:hover { opacity: 0.9; }
17
+ </style>
18
+ </head>
19
+ <body>
20
+ <div class="error-card">
21
+ <div class="error-code">403</div>
22
+ <div class="error-title">Forbidden</div>
23
+ <div class="error-msg">You don't have permission to access this resource.</div>
24
+ <div class="error-path">{{ path }}</div>
25
+ <br>
26
+ <a href="/" class="error-home">Go Home</a>
27
+ </div>
28
+ </body>
29
+ </html>
@@ -0,0 +1,29 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>404 — Not Found</title>
7
+ <style>
8
+ * { box-sizing: border-box; margin: 0; padding: 0; }
9
+ body { font-family: system-ui, -apple-system, sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
10
+ .error-card { background: #1e293b; border: 1px solid #334155; border-radius: 1rem; padding: 3rem; text-align: center; max-width: 520px; width: 90%; }
11
+ .error-code { font-size: 8rem; font-weight: 900; color: #3b82f6; opacity: 0.6; line-height: 1; margin-bottom: 0.5rem; }
12
+ .error-title { font-size: 1.5rem; font-weight: 700; margin-bottom: 0.75rem; }
13
+ .error-msg { color: #94a3b8; font-size: 1rem; margin-bottom: 1.5rem; line-height: 1.5; }
14
+ .error-path { font-family: 'SF Mono', monospace; background: #0f172a; color: #3b82f6; padding: 0.5rem 1rem; border-radius: 0.5rem; font-size: 0.85rem; word-break: break-all; margin-bottom: 1.5rem; display: inline-block; }
15
+ .error-home { display: inline-block; padding: 0.6rem 2rem; background: #3b82f6; color: #fff; text-decoration: none; border-radius: 0.5rem; font-size: 0.9rem; font-weight: 600; }
16
+ .error-home:hover { opacity: 0.9; }
17
+ </style>
18
+ </head>
19
+ <body>
20
+ <div class="error-card">
21
+ <div class="error-code">404</div>
22
+ <div class="error-title">Page Not Found</div>
23
+ <div class="error-msg">The page you're looking for doesn't exist or has been moved. Check the URL and try again.</div>
24
+ <div class="error-path">{{ path }}</div>
25
+ <br>
26
+ <a href="/" class="error-home">Go Home</a>
27
+ </div>
28
+ </body>
29
+ </html>
@@ -0,0 +1,38 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>500 — Server Error</title>
7
+ <style>
8
+ * { box-sizing: border-box; margin: 0; padding: 0; }
9
+ body { font-family: system-ui, -apple-system, sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
10
+ .error-card { background: #1e293b; border: 1px solid #334155; border-radius: 1rem; padding: 3rem; text-align: left; max-width: 700px; width: 90%; }
11
+ .error-header { display: flex; align-items: center; gap: 1rem; margin-bottom: 1.5rem; }
12
+ .error-code { font-size: 3rem; font-weight: 900; color: #ef4444; opacity: 0.7; }
13
+ .error-title { font-size: 1.3rem; font-weight: 700; }
14
+ .error-msg { color: #94a3b8; font-size: 0.95rem; margin-bottom: 1.5rem; line-height: 1.5; }
15
+ .error-trace { background: #0f172a; border: 1px solid #334155; border-radius: 0.5rem; padding: 1rem; font-family: 'SF Mono', monospace; font-size: 0.8rem; line-height: 1.5; overflow-x: auto; max-height: 400px; overflow-y: auto; white-space: pre-wrap; color: #ef4444; margin-bottom: 1.5rem; }
16
+ .error-footer { display: flex; justify-content: space-between; align-items: center; }
17
+ .error-hint { color: #64748b; font-size: 0.75rem; }
18
+ .error-id { color: #64748b; font-family: 'SF Mono', monospace; font-size: 0.75rem; }
19
+ .error-home { display: inline-block; padding: 0.5rem 1.5rem; background: #3b82f6; color: #fff; text-decoration: none; border-radius: 0.5rem; font-size: 0.85rem; font-weight: 600; }
20
+ .error-home:hover { opacity: 0.9; }
21
+ </style>
22
+ </head>
23
+ <body>
24
+ <div class="error-card">
25
+ <div class="error-header">
26
+ <div class="error-code">500</div>
27
+ <div class="error-title">Server Error</div>
28
+ </div>
29
+ <div class="error-msg">Something went wrong while processing your request.</div>
30
+ <pre class="error-trace">{{ error_message }}</pre>
31
+ <div class="error-footer">
32
+ <span class="error-hint">Fix the error and save to auto-reload</span>
33
+ <span class="error-id">{{ request_id }}</span>
34
+ <a href="/" class="error-home">Go Home</a>
35
+ </div>
36
+ </div>
37
+ </body>
38
+ </html>
@@ -0,0 +1,9 @@
1
+ {% extends "errors/base.twig" %}
2
+ {% block title %}502 — Bad Gateway{% endblock %}
3
+ {% block content %}
4
+ <div class="error-code" style="color:var(--danger)">502</div>
5
+ <div class="error-title">Bad Gateway</div>
6
+ <div class="error-msg">The upstream server returned an invalid response.</div>
7
+ <div class="error-path" style="color:var(--danger)">{{ path }}</div>
8
+ <a href="/" class="error-home">Go Home</a>
9
+ {% endblock %}