@techspokes/typescript-wsdl-client 1.1.8 → 1.3.0

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 (85) hide show
  1. package/README.md +22 -3
  2. package/dist/checkedPlanning.js +27 -4
  3. package/dist/cli.js +17 -1
  4. package/dist/client/checkedDirectRuntime.d.ts +11 -1
  5. package/dist/client/checkedDirectRuntime.js +19 -2
  6. package/dist/client/directClientRuntime.d.ts +18 -1
  7. package/dist/client/directClientRuntime.js +15 -3
  8. package/dist/client/directClientVersion.d.ts +2 -0
  9. package/dist/client/directClientVersion.js +2 -0
  10. package/dist/client/generateClient.js +72 -16
  11. package/dist/client/generateDirectClient.js +14 -9
  12. package/dist/client/generateOperations.js +30 -11
  13. package/dist/client/generateUtils.js +5 -1
  14. package/dist/compiler/canonicalGraph.d.ts +4 -15
  15. package/dist/compiler/canonicalGraph.js +1 -8
  16. package/dist/compiler/componentRecordProjection.d.ts +3 -0
  17. package/dist/compiler/componentRecordProjection.js +10 -0
  18. package/dist/compiler/declarationIdentity.d.ts +22 -0
  19. package/dist/compiler/declarationIdentity.js +9 -0
  20. package/dist/compiler/directCodec.d.ts +22 -2
  21. package/dist/compiler/directCodec.js +40 -7
  22. package/dist/compiler/directOperation.d.ts +28 -5
  23. package/dist/compiler/directOperation.js +84 -14
  24. package/dist/compiler/directSoap.d.ts +40 -10
  25. package/dist/compiler/directSoap.js +151 -18
  26. package/dist/compiler/directSoapSource.js +3 -2
  27. package/dist/compiler/directSourceCapsule.js +69 -4
  28. package/dist/compiler/directStructure.js +6 -2
  29. package/dist/compiler/directStructurePlan.d.ts +10 -1
  30. package/dist/compiler/directStructurePlan.js +27 -7
  31. package/dist/compiler/directValues.d.ts +15 -3
  32. package/dist/compiler/directValues.js +20 -4
  33. package/dist/compiler/legacyDeclarations.d.ts +26 -0
  34. package/dist/compiler/legacyDeclarations.js +144 -0
  35. package/dist/compiler/projectionScalar.d.ts +18 -1
  36. package/dist/compiler/projectionScalar.js +47 -13
  37. package/dist/compiler/rpcStringValues.d.ts +30 -0
  38. package/dist/compiler/rpcStringValues.js +196 -0
  39. package/dist/compiler/rpcTypedValues.d.ts +48 -0
  40. package/dist/compiler/rpcTypedValues.js +190 -0
  41. package/dist/compiler/schemaCompiler.js +44 -109
  42. package/dist/compiler/semanticCatalog.d.ts +2 -2
  43. package/dist/compiler/soapHeaders.d.ts +10 -4
  44. package/dist/compiler/soapHeaders.js +28 -9
  45. package/dist/gateway/generateCheckedGateway.js +26 -6
  46. package/dist/gateway/generateComposedGateway.d.ts +16 -0
  47. package/dist/gateway/generateComposedGateway.js +65 -0
  48. package/dist/gateway/generateGateway.js +1 -0
  49. package/dist/gateway/generators.js +19 -4
  50. package/dist/gateway/helpers.d.ts +1 -0
  51. package/dist/gateway/helpers.js +15 -0
  52. package/dist/generation/componentShape.js +25 -1
  53. package/dist/index.d.ts +5 -1
  54. package/dist/index.js +3 -1
  55. package/dist/loader/orderedSyntax.d.ts +4 -0
  56. package/dist/loader/orderedSyntax.js +18 -4
  57. package/dist/multiServicePipeline.d.ts +41 -0
  58. package/dist/multiServicePipeline.js +287 -0
  59. package/dist/openapi/composeOpenAPI.d.ts +29 -0
  60. package/dist/openapi/composeOpenAPI.js +322 -0
  61. package/dist/pipeline.d.ts +1 -1
  62. package/dist/pipeline.js +2 -0
  63. package/docs/agent-skill.md +10 -1
  64. package/docs/api-reference.md +127 -9
  65. package/docs/checked-gateway.md +10 -0
  66. package/docs/checked-generation.md +11 -5
  67. package/docs/cli-reference.md +25 -5
  68. package/docs/concepts.md +4 -5
  69. package/docs/configuration.md +10 -3
  70. package/docs/corpus.md +1 -1
  71. package/docs/gateway-guide.md +104 -27
  72. package/docs/generated-code.md +12 -6
  73. package/docs/migration-playbook.md +4 -0
  74. package/docs/migration.md +134 -6
  75. package/docs/output-anatomy.md +20 -17
  76. package/docs/production.md +7 -5
  77. package/docs/start-here.md +12 -2
  78. package/docs/supported-patterns.md +23 -0
  79. package/docs/testing.md +2 -0
  80. package/docs/troubleshooting.md +3 -1
  81. package/llms.txt +9 -1
  82. package/package.json +2 -2
  83. package/src/runtime/clientStreamMethods.tpl.txt +156 -148
  84. package/src/runtime/operationsStreamHelper.tpl.txt +6 -6
  85. package/src/runtime/streamXml.ts +48 -20
@@ -1,6 +1,7 @@
1
1
  import { getChildrenWithLocalName, getFirstWithLocalName, normalizeArray, pascal, resolveQName, } from "../util/tools.js";
2
2
  import { xsdToTsPrimitive } from "../xsd/primitives.js";
3
3
  import { WsdlCompilationError } from "../util/errors.js";
4
+ import { LegacyDeclarations } from "./legacyDeclarations.js";
4
5
  // XML Schema namespace constant
5
6
  const XS = "http://www.w3.org/2001/XMLSchema";
6
7
  /**
@@ -13,25 +14,6 @@ const XS = "http://www.w3.org/2001/XMLSchema";
13
14
  function qkey(q) {
14
15
  return `{${q.ns}}${q.local}`;
15
16
  }
16
- /**
17
- * Generates a name for an inline complex type
18
- *
19
- * This function creates appropriate TypeScript names for anonymous types
20
- * that are defined inline within element declarations.
21
- *
22
- * @param {string} parentTypeName - Name of the parent type
23
- * @param {string} [propName] - Name of the property containing the inline type
24
- * @param {number|"unbounded"} [_max] - Maximum occurrence (unused but kept for API compatibility)
25
- * @returns {string} - Generated TypeScript name for the inline type
26
- */
27
- function makeInlineTypeName(parentTypeName, propName, _max) {
28
- const base = pascal(parentTypeName || "AnonParent");
29
- const prop = pascal(propName || "");
30
- if (prop) {
31
- return prop;
32
- }
33
- return `${base}Anon`;
34
- }
35
17
  // --- Minimal WS-Policy scanning (inline policies only; PolicyReference not resolved) ---
36
18
  /**
37
19
  * Extracts security requirements from WS-Policy nodes
@@ -255,9 +237,13 @@ export function compileCatalog(cat, options, streamConfig) {
255
237
  }
256
238
  }
257
239
  }
258
- // outputs & state
259
- const compiledMap = new Map(); // key: ns|name
260
- const aliasMap = new Map(); // key: ns|name
240
+ const declarations = new LegacyDeclarations([
241
+ ...Array.from(complexTypes.values(), rec => ({ kind: "complex", name: String(rec.node["@_name"]), ns: rec.tns, node: rec.node, prefixes: rec.prefixes })),
242
+ ...Array.from(simpleTypes.values(), rec => ({ kind: "simple", name: String(rec.node["@_name"]), ns: rec.tns, node: rec.node, prefixes: rec.prefixes })),
243
+ ...Array.from(elements.values(), rec => ({ kind: "element", name: String(rec.node["@_name"]), ns: rec.tns, node: rec.node, prefixes: rec.prefixes })),
244
+ ]);
245
+ const compiledMap = new Map();
246
+ const aliasMap = new Map();
261
247
  const inProgress = new Set();
262
248
  // meta accumulators
263
249
  const attrSpec = {};
@@ -314,22 +300,23 @@ export function compileCatalog(cat, options, streamConfig) {
314
300
  const list = getFirstWithLocalName(simpleNode, "list");
315
301
  if (list?.["@_itemType"]) {
316
302
  const q = resolveQName(list["@_itemType"], schemaNS, prefixes);
317
- const declared = q.ns === XS ? `xs:${q.local}` : `{${q.ns}}${q.local}`;
318
- return { tsType: `${xsdToTsPrimitive(declared, options?.primitive)}[]`, declared };
303
+ const item = resolveTypeRef(q, schemaNS, prefixes);
304
+ const itemType = item.tsType.includes("|") ? `(${item.tsType})` : item.tsType;
305
+ return { tsType: `${itemType}[]`, declared: item.declared };
319
306
  }
320
307
  // fallback
321
308
  return { tsType: "string", declared: "xs:string" };
322
309
  }
323
310
  /** Compile and cache a named simpleType */
324
311
  function getOrCompileAlias(name, sNode, schemaNS, prefixes) {
325
- const key = `${schemaNS}|${name}`;
312
+ const key = declarations.type(schemaNS, name);
326
313
  const present = aliasMap.get(key);
327
314
  if (present) {
328
315
  return present;
329
316
  }
330
317
  const { tsType, declared, jsdoc } = compileSimpleTypeNode(sNode, schemaNS, prefixes);
331
318
  const alias = {
332
- name: pascal(name),
319
+ name: declarations.name(key),
333
320
  ns: schemaNS,
334
321
  tsType,
335
322
  declared,
@@ -373,19 +360,26 @@ export function compileCatalog(cat, options, streamConfig) {
373
360
  const k = qkey(q);
374
361
  return complexTypes.get(k);
375
362
  }
376
- function getOrCompileComplex(name, cnode, schemaNS, prefixes) {
377
- // Early cycle detection: check if we're already processing this raw name + namespace combo
378
- // BEFORE calling pascal() to avoid stack overflow on circular references
379
- const rawKey = `${schemaNS}|${name}`;
380
- if (inProgress.has(rawKey)) {
381
- // Return a minimal stub to break the cycle
382
- const stubName = pascal(name);
383
- return { name: stubName, ns: schemaNS, attrs: [], elems: [] };
363
+ function getOrCompileComplex(name, cnode, schemaNS, prefixes, declarationId = declarations.type(schemaNS, name)) {
364
+ const key = declarationId;
365
+ const outName = declarations.name(key);
366
+ const present = compiledMap.get(key);
367
+ if (present)
368
+ return present;
369
+ if (inProgress.has(key))
370
+ return { name: outName, ns: schemaNS, attrs: [], elems: [] };
371
+ inProgress.add(key);
372
+ try {
373
+ return compileComplexBody(cnode, schemaNS, prefixes, declarationId);
374
+ }
375
+ finally {
376
+ inProgress.delete(key);
384
377
  }
385
- inProgress.add(rawKey);
386
- const outName = pascal(name);
378
+ }
379
+ function compileComplexBody(cnode, schemaNS, prefixes, declarationId) {
380
+ const key = declarationId;
381
+ const outName = declarations.name(key);
387
382
  const typeDoc = extractAnnotationDocumentation(cnode);
388
- const key = `${schemaNS}|${outName}`;
389
383
  // hoisted helpers for merging and collecting
390
384
  const mergeAttrs = (into, list) => {
391
385
  const idx = new Map();
@@ -415,40 +409,6 @@ export function compileCatalog(cat, options, streamConfig) {
415
409
  }
416
410
  }
417
411
  };
418
- const mergeChoiceGroups = (into, list) => {
419
- if (list.length === 0)
420
- return into;
421
- const merged = into ? [...into] : [];
422
- const idx = new Map();
423
- merged.forEach((g, i) => idx.set(g.name, i));
424
- for (const g of list) {
425
- const pos = idx.get(g.name);
426
- if (pos == null) {
427
- idx.set(g.name, merged.length);
428
- merged.push(g);
429
- }
430
- else {
431
- merged[pos] = g;
432
- }
433
- }
434
- return merged;
435
- };
436
- const mergeJsonUnique = (into, list) => {
437
- if (list.length === 0)
438
- return into;
439
- const merged = into ? [...into] : [];
440
- const seen = new Set(merged.map((w) => JSON.stringify(w)));
441
- for (const w of list) {
442
- const key = JSON.stringify(w);
443
- if (!seen.has(key)) {
444
- seen.add(key);
445
- merged.push(w);
446
- }
447
- }
448
- return merged;
449
- };
450
- const mergeWildcards = (into, list) => mergeJsonUnique(into, list);
451
- const mergeAttributeWildcards = (into, list) => mergeJsonUnique(into, list);
452
412
  const collectAttributes = (node) => {
453
413
  const out = [];
454
414
  const attrs = getChildrenWithLocalName(node, "attribute");
@@ -547,7 +507,7 @@ export function compileCatalog(cat, options, streamConfig) {
547
507
  }
548
508
  return out;
549
509
  };
550
- const compileElementParticle = (ownerTypeName, e) => {
510
+ const compileElementParticle = (e) => {
551
511
  const nameOrRef = e["@_name"] || e["@_ref"];
552
512
  if (!nameOrRef)
553
513
  return undefined;
@@ -557,8 +517,8 @@ export function compileCatalog(cat, options, streamConfig) {
557
517
  const inlineComplex = getFirstWithLocalName(e, "complexType");
558
518
  const inlineSimple = getFirstWithLocalName(e, "simpleType");
559
519
  if (inlineComplex) {
560
- const inlineName = makeInlineTypeName(ownerTypeName, propName || nameOrRef, max);
561
- const rec = getOrCompileComplex(inlineName, inlineComplex, schemaNS, prefixes);
520
+ const inlineId = declarations.inline(declarationId, e);
521
+ const rec = getOrCompileComplex(String(propName || nameOrRef), inlineComplex, schemaNS, prefixes, inlineId);
562
522
  return {
563
523
  name: propName || nameOrRef,
564
524
  tsType: rec.name,
@@ -603,7 +563,7 @@ export function compileCatalog(cat, options, streamConfig) {
603
563
  });
604
564
  const recurse = (groupNode, inherited = one, inheritSequenceOccurs = true) => {
605
565
  for (const e of getChildrenWithLocalName(groupNode, "element")) {
606
- const particle = compileElementParticle(ownerTypeName, e);
566
+ const particle = compileElementParticle(e);
607
567
  if (!particle)
608
568
  continue;
609
569
  // @constraints Preserve disabled particles until the content model can represent their absence (#145).
@@ -636,7 +596,7 @@ export function compileCatalog(cat, options, streamConfig) {
636
596
  const branches = [];
637
597
  let branchSourceOrder = 0;
638
598
  for (const e of getChildrenWithLocalName(child.node, "element")) {
639
- const particle = compileElementParticle(ownerTypeName, e);
599
+ const particle = compileElementParticle(e);
640
600
  if (particle) {
641
601
  branches.push({
642
602
  name: particle.name,
@@ -672,29 +632,6 @@ export function compileCatalog(cat, options, streamConfig) {
672
632
  recurse(node);
673
633
  return out;
674
634
  };
675
- const present = compiledMap.get(key);
676
- if (present) {
677
- // On duplicate definitions, merge new attributes and elements
678
- const newAttrs = collectAttributes(cnode);
679
- const newElems = collectParticles(outName, cnode);
680
- const newWildcards = collectWildcards(cnode);
681
- const newAttributeWildcards = collectAttributeWildcards(cnode);
682
- const newChoiceGroups = collectChoiceGroups(outName, cnode);
683
- mergeAttrs(present.attrs, newAttrs);
684
- mergeElems(present.elems, newElems);
685
- present.wildcards = mergeWildcards(present.wildcards, newWildcards);
686
- present.attributeWildcards = mergeAttributeWildcards(present.attributeWildcards, newAttributeWildcards);
687
- const mergedChoiceGroups = mergeChoiceGroups(present.choiceGroups, newChoiceGroups);
688
- if (mergedChoiceGroups && mergedChoiceGroups.length > 0) {
689
- present.choiceGroups = mergedChoiceGroups;
690
- }
691
- if (!present.doc && typeDoc) {
692
- present.doc = typeDoc;
693
- }
694
- // Remove from inProgress since we're done with this cycle
695
- inProgress.delete(rawKey);
696
- return present;
697
- }
698
635
  // result accumulators
699
636
  const attrs = [];
700
637
  const elems = [];
@@ -749,7 +686,6 @@ export function compileCatalog(cat, options, streamConfig) {
749
686
  ...(localChoiceGroups.length > 0 ? { localChoiceGroups } : {}),
750
687
  };
751
688
  compiledMap.set(key, result);
752
- inProgress.delete(key);
753
689
  return result;
754
690
  }
755
691
  }
@@ -784,7 +720,6 @@ export function compileCatalog(cat, options, streamConfig) {
784
720
  ...(attributeWildcards.length > 0 ? { attributeWildcards } : {}),
785
721
  };
786
722
  compiledMap.set(key, result);
787
- inProgress.delete(rawKey);
788
723
  return result;
789
724
  };
790
725
  if (ext) {
@@ -811,20 +746,19 @@ export function compileCatalog(cat, options, streamConfig) {
811
746
  ...(choiceGroups.length > 0 ? { choiceGroups } : {}),
812
747
  };
813
748
  compiledMap.set(key, result);
814
- inProgress.delete(rawKey);
815
749
  return result;
816
750
  }
817
751
  // Helper: compile a global element into a surface type (wrapper)
818
752
  function compileElementAsType(name, enode, schemaNS, prefixes) {
819
- const outName = pascal(name);
753
+ const key = declarations.surface(schemaNS, name);
754
+ const outName = declarations.name(key);
820
755
  const elementDoc = extractAnnotationDocumentation(enode);
821
- const key = `${schemaNS}|${outName}`;
822
756
  const present = compiledMap.get(key);
823
757
  if (present)
824
758
  return present;
825
759
  const inlineComplex = getFirstWithLocalName(enode, "complexType");
826
760
  if (inlineComplex) {
827
- return getOrCompileComplex(name, inlineComplex, schemaNS, prefixes);
761
+ return getOrCompileComplex(name, inlineComplex, schemaNS, prefixes, key);
828
762
  }
829
763
  const inlineSimple = getFirstWithLocalName(enode, "simpleType");
830
764
  if (inlineSimple) {
@@ -871,7 +805,7 @@ export function compileCatalog(cat, options, streamConfig) {
871
805
  // record alias: ElementName -> BaseTypeName
872
806
  const aliasKey = key;
873
807
  const existingAlias = aliasMap.get(aliasKey);
874
- const declared = `{${base.ns}}${base.name}`;
808
+ const declared = `{${base.ns}}${q.local}`;
875
809
  if (!existingAlias) {
876
810
  aliasMap.set(aliasKey, { name: outName, ns: schemaNS, tsType: base.name, declared, doc: elementDoc });
877
811
  }
@@ -1138,9 +1072,10 @@ export function compileCatalog(cat, options, streamConfig) {
1138
1072
  const inputElement = elementOfMessage(inMsg);
1139
1073
  const outputElement = elementOfMessage(outMsg);
1140
1074
  const doc = extractDirectDocumentation(po);
1141
- // Derive TypeScript type names from element local names
1142
- const inputTypeName = inputElement ? pascal(inputElement.local) : undefined;
1143
- const outputTypeName = outputElement ? pascal(outputElement.local) : undefined;
1075
+ const surfaceName = (element) => elements.has(qkey(element))
1076
+ ? declarations.name(declarations.surface(element.ns, element.local)) : pascal(element.local);
1077
+ const inputTypeName = inputElement ? surfaceName(inputElement) : undefined;
1078
+ const outputTypeName = outputElement ? surfaceName(outputElement) : undefined;
1144
1079
  const op = {
1145
1080
  name,
1146
1081
  soapAction: bOps.get(name) || "",
@@ -58,8 +58,8 @@ export declare function deriveCompatibilityView(graph: CanonicalGraph): {
58
58
  model: "xsd10-faithful-v1";
59
59
  symbols: {
60
60
  id: string;
61
- kind: "element" | "attribute" | "wsdl" | "complexType" | "simpleType" | "attributeWildcard" | "group" | "attributeGroup" | "particle" | "attributeUse" | "attributeGroupUse";
62
- identity: import("./canonicalGraph.js").Identity;
61
+ kind: "element" | "attribute" | "wsdl" | "group" | "attributeGroup" | "complexType" | "simpleType" | "attributeWildcard" | "particle" | "attributeUse" | "attributeGroupUse";
62
+ identity: import("./declarationIdentity.js").Identity;
63
63
  }[];
64
64
  };
65
65
  /** Internal faithful companion dispatch. Regeneration never fabricates missing structure. */
@@ -1,4 +1,4 @@
1
- /** SOAP 1.1 response-header clauses for the component transport's default ultimate-receiver role. */
1
+ /** SOAP response-header clauses for the component transport's default ultimate-receiver role. */
2
2
  import type { ExpandedName, SyntaxElement } from '../loader/orderedSyntax.js';
3
3
  export type ComponentSoapHeaderResult = Readonly<{
4
4
  kind: 'ignored-optional-headers';
@@ -9,8 +9,14 @@ export type ComponentSoapHeaderResult = Readonly<{
9
9
  }>;
10
10
  /** Assess borrowed parsed Header syntax without claiming unknown block semantics.
11
11
  * @remarks SOAP 1.1 sections 4.2 through 4.2.3 require qualified block names and define absent mustUnderstand
12
- * as 0. Only immediate block attributes govern header processing. Actor-directed processing and mandatory
13
- * block semantics remain unsupported; no security/header emission capability is acquired here.
12
+ * as 0. SOAP 1.2 additionally defines boolean lexicals and role/relay attributes. Only immediate block
13
+ * attributes govern processing. Unknown mandatory blocks targeted at this receiver remain unsupported.
14
+ * @param header Parsed Header in the selected envelope namespace.
15
+ * @param check Invocation-owned cancellation and work accounting.
16
+ * @param version Selected SOAP binding version; omission retains SOAP 1.1 compatibility.
17
+ * @returns Unprocessed block names or an explicit protocol/capability refusal.
18
+ * @throws Propagates caller cancellation and resource exceptions from check.
14
19
  * @see https://www.w3.org/TR/2000/NOTE-SOAP-20000508/
20
+ * @see https://www.w3.org/TR/soap12-part1/#soapheadblock
15
21
  */
16
- export declare function assessComponentSoapHeader(header: SyntaxElement, check: () => void): ComponentSoapHeaderResult;
22
+ export declare function assessComponentSoapHeader(header: SyntaxElement, check: () => void, version?: '1.1' | '1.2'): ComponentSoapHeaderResult;
@@ -1,13 +1,20 @@
1
1
  const SOAP = 'http://schemas.xmlsoap.org/soap/envelope/';
2
2
  /** Assess borrowed parsed Header syntax without claiming unknown block semantics.
3
3
  * @remarks SOAP 1.1 sections 4.2 through 4.2.3 require qualified block names and define absent mustUnderstand
4
- * as 0. Only immediate block attributes govern header processing. Actor-directed processing and mandatory
5
- * block semantics remain unsupported; no security/header emission capability is acquired here.
4
+ * as 0. SOAP 1.2 additionally defines boolean lexicals and role/relay attributes. Only immediate block
5
+ * attributes govern processing. Unknown mandatory blocks targeted at this receiver remain unsupported.
6
+ * @param header Parsed Header in the selected envelope namespace.
7
+ * @param check Invocation-owned cancellation and work accounting.
8
+ * @param version Selected SOAP binding version; omission retains SOAP 1.1 compatibility.
9
+ * @returns Unprocessed block names or an explicit protocol/capability refusal.
10
+ * @throws Propagates caller cancellation and resource exceptions from check.
6
11
  * @see https://www.w3.org/TR/2000/NOTE-SOAP-20000508/
12
+ * @see https://www.w3.org/TR/soap12-part1/#soapheadblock
7
13
  */
8
- export function assessComponentSoapHeader(header, check) {
14
+ export function assessComponentSoapHeader(header, check, version = '1.1') {
9
15
  check();
10
- if (header.name.namespace !== SOAP || header.name.local !== 'Header' || header.attributes.length)
16
+ const namespace = version === '1.2' ? 'http://www.w3.org/2003/05/soap-envelope' : SOAP;
17
+ if (header.name.namespace !== namespace || header.name.local !== 'Header' || header.attributes.length)
11
18
  return Object.freeze({ kind: 'protocol', rule: 'SOAP Header shape' });
12
19
  const headers = [];
13
20
  for (const child of header.children) {
@@ -20,21 +27,33 @@ export function assessComponentSoapHeader(header, check) {
20
27
  if (!child.name.namespace)
21
28
  return Object.freeze({ kind: 'protocol', rule: 'SOAP header block namespace required' });
22
29
  let mandatory = false;
30
+ let targeted = true;
23
31
  for (const attribute of child.attributes) {
24
32
  check();
25
- if (attribute.name.namespace !== SOAP)
33
+ if (attribute.name.namespace !== namespace)
26
34
  continue;
35
+ if (version === '1.2' && attribute.name.local === 'role') {
36
+ if (!/^[A-Za-z][A-Za-z0-9+.-]*:/.test(attribute.value))
37
+ return Object.freeze({ kind: 'protocol', rule: 'SOAP role URI' });
38
+ targeted = [namespace + '/role/next', namespace + '/role/ultimateReceiver'].includes(attribute.value);
39
+ continue;
40
+ }
41
+ if (version === '1.2' && attribute.name.local === 'relay') {
42
+ if (!['true', 'false', '1', '0'].includes(attribute.value))
43
+ return Object.freeze({ kind: 'protocol', rule: 'SOAP relay boolean' });
44
+ continue;
45
+ }
27
46
  if (attribute.name.local === 'actor')
28
47
  return Object.freeze({ kind: 'unsupported', rule: 'SOAP actor header capability required' });
29
48
  if (attribute.name.local === 'mustUnderstand') {
30
- if (attribute.value !== '0' && attribute.value !== '1')
31
- return Object.freeze({ kind: 'protocol', rule: 'SOAP 1.1 mustUnderstand value' });
32
- mandatory = attribute.value === '1';
49
+ if (!(version === '1.2' ? ['true', 'false', '1', '0'] : ['0', '1']).includes(attribute.value))
50
+ return Object.freeze({ kind: 'protocol', rule: `SOAP ${version} mustUnderstand value` });
51
+ mandatory = attribute.value === '1' || attribute.value === 'true';
33
52
  }
34
53
  else if (attribute.name.local !== 'encodingStyle')
35
54
  return Object.freeze({ kind: 'unsupported', rule: 'unknown SOAP header attribute' });
36
55
  }
37
- if (mandatory)
56
+ if (mandatory && targeted)
38
57
  return Object.freeze({ kind: 'unsupported', rule: 'incoming mandatory SOAP header' });
39
58
  headers.push(Object.freeze({ ...child.name }));
40
59
  }
@@ -22,7 +22,7 @@ async function generateGatewayFromReconstruction(expected, options) {
22
22
  throw new Error('Checked gateway generation cancelled');
23
23
  inspectReconstructedComponentSource(expected);
24
24
  const capsule = { bytes: componentSourceCapsuleBytes(expected, options.signal) };
25
- const runtime = 'checkedComponentRuntimeV1';
25
+ const runtime = expected.contract.binding.version === '1.2' ? 'checkedComponentRuntimeV2' : 'checkedComponentRuntimeV1';
26
26
  if (!options.routePath.startsWith('/') || options.routePath.split('/').slice(1)
27
27
  .some(segment => !segment || !/^(?:[A-Za-z0-9._~-]|%[0-9A-Fa-f]{2})+$/.test(segment))) {
28
28
  throw new TypeError('Checked routePath requires literal nonempty path segments');
@@ -40,6 +40,16 @@ async function generateGatewayFromReconstruction(expected, options) {
40
40
  }).schema;
41
41
  };
42
42
  const request = schema(input, 'Request'), response = schema(output, 'Response');
43
+ const soap12 = expected.contract.binding.version === '1.2';
44
+ const nameSchema = { type: 'object', additionalProperties: false, required: ['namespace', 'local'],
45
+ properties: { namespace: { type: 'string' }, local: { type: 'string' } } };
46
+ const protocolFaultProperties = {
47
+ version: { const: '1.2' }, code: nameSchema, string: { type: 'string' },
48
+ subcodes: { type: 'array', items: nameSchema },
49
+ reasons: { type: 'array', minItems: 1, items: { type: 'object', additionalProperties: false, required: ['lang', 'text'],
50
+ properties: { lang: { type: 'string' }, text: { type: 'string' } } } },
51
+ node: { type: 'string' }, role: { type: 'string' },
52
+ };
43
53
  const faults = expected.mappings.filter(mapping => mapping.direction.kind === 'fault').map(mapping => {
44
54
  if (mapping.direction.kind !== 'fault')
45
55
  throw new TypeError('Declared fault mapping required');
@@ -50,8 +60,8 @@ async function generateGatewayFromReconstruction(expected, options) {
50
60
  properties: {
51
61
  value: fault.value,
52
62
  fault: {
53
- type: 'object', additionalProperties: false, required: ['name', 'code', 'string'],
54
- properties: {
63
+ type: 'object', additionalProperties: false, required: ['name', 'code', 'string', ...(soap12 ? ['version', 'subcodes', 'reasons'] : [])],
64
+ properties: soap12 ? { ...protocolFaultProperties, name: { const: fault.name } } : {
55
65
  name: { const: fault.name }, string: { type: 'string' }, actor: { type: 'string' },
56
66
  code: { type: 'object', additionalProperties: false, required: ['namespace', 'local'],
57
67
  properties: { namespace: { type: 'string' }, local: { type: 'string' } } },
@@ -59,6 +69,11 @@ async function generateGatewayFromReconstruction(expected, options) {
59
69
  },
60
70
  },
61
71
  }));
72
+ if (soap12)
73
+ faultVariants.push({ type: 'object', additionalProperties: false, required: ['fault'], properties: {
74
+ fault: { type: 'object', additionalProperties: false, required: ['version', 'code', 'string', 'subcodes', 'reasons'],
75
+ properties: protocolFaultProperties },
76
+ } });
62
77
  const fault = faultVariants.length ? { oneOf: faultVariants } : { not: {} };
63
78
  const schemas = { request: { $schema: draft07, ...request }, response: { $schema: draft07, ...response }, fault: { $schema: draft07, ...fault } };
64
79
  const grouped = expected.mappings.some(mapping => mapping.kind === 'component-mapping'
@@ -97,7 +112,7 @@ async function generateGatewayFromReconstruction(expected, options) {
97
112
  openapi: '3.1.1', jsonSchemaDialect: 'https://json-schema.org/draft/2020-12/schema',
98
113
  info: { title: expected.selection.service.local, version: '1.0.0' },
99
114
  ...(grouped ? { components: { schemas: components } } : {}),
100
- 'x-checked-model': { model: 'checked-components-v1', scope: 'native-component-structured-soap11',
115
+ 'x-checked-model': { model: 'checked-components-v1', scope: soap12 ? 'native-component-structured-soap12' : 'native-component-structured-soap11',
101
116
  ...(expected.responseProjection
102
117
  ? { responseProjection: { spec: expected.responseProjection.spec, delivery: 'buffered', retainedValue: 'complete-companion-payload' } } : {}),
103
118
  documentationDialect: '2020-12', runtimeDialect: 'draft-07', jsonShapeComplete: false,
@@ -171,9 +186,14 @@ const plugin: FastifyPluginAsync<CheckedGatewayOptions> = async (fastify, option
171
186
  return reply.type('application/json').serializer(value => JSON.stringify(value)).send(result.value);
172
187
  }
173
188
  if (result.kind === 'soap-fault') {
174
- const fault = {name: result.fault.name, code: {namespace: result.fault.code.namespace, local: result.fault.code.local},
189
+ ${soap12 ? `const fault = {version: result.fault.version, code: result.fault.code, string: result.fault.string,
190
+ subcodes: result.fault.subcodes, reasons: result.fault.reasons,
191
+ ...(result.fault.name === undefined ? {} : {name: result.fault.name}),
192
+ ...(result.fault.node === undefined ? {} : {node: result.fault.node}),
193
+ ...(result.fault.role === undefined ? {} : {role: result.fault.role})};
194
+ const value = result.fault.name === undefined ? {fault} : {fault, value: result.value};` : `const fault = {name: result.fault.name, code: {namespace: result.fault.code.namespace, local: result.fault.code.local},
175
195
  string: result.fault.string, ...(result.fault.actor === undefined ? {} : {actor: result.fault.actor})};
176
- const value = {fault, value: result.value};
196
+ const value = {fault, value: result.value};`}
177
197
  if (!validateFault(value)) return reply.code(502).send({kind: 'value-invalid', rule: 'http-fault-shape'});
178
198
  return reply.code(502).type('application/json').serializer(value => JSON.stringify(value)).send(value);
179
199
  }
@@ -0,0 +1,16 @@
1
+ export interface ComposedGatewayService {
2
+ key: string;
3
+ clientDir: string;
4
+ gatewayDir: string;
5
+ clientClassName: string;
6
+ }
7
+ /** Build a typed aggregate over independently generated service routers.
8
+ * @returns Complete generated source; the caller publishes it only after composition succeeds.
9
+ * @constraints Routes already carry generation-time prefixes. Runtime prefixing is refused to keep the served definition accurate.
10
+ */
11
+ export declare function buildComposedGatewaySource(services: readonly ComposedGatewayService[], options: {
12
+ outDir: string;
13
+ openapiFile: string;
14
+ imports?: "js" | "ts" | "bare";
15
+ openapiPath?: string | false;
16
+ }): string;
@@ -0,0 +1,65 @@
1
+ import path from "node:path";
2
+ import { composeGeneratedSource, resolvePreamble } from "../generation/preamble.js";
3
+ /** Build a typed aggregate over independently generated service routers.
4
+ * @returns Complete generated source; the caller publishes it only after composition succeeds.
5
+ * @constraints Routes already carry generation-time prefixes. Runtime prefixing is refused to keep the served definition accurate.
6
+ */
7
+ export function buildComposedGatewaySource(services, options) {
8
+ const suffix = options.imports === "bare" ? "" : `.${options.imports ?? "js"}`;
9
+ const literal = (value) => JSON.stringify(value).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
10
+ const relative = (target) => {
11
+ const resolved = path.relative(options.outDir, target).replaceAll("\\", "/");
12
+ if (path.isAbsolute(resolved) || /^[a-z]:/i.test(resolved))
13
+ throw new Error("Composed outputs must share a filesystem root");
14
+ return resolved.startsWith(".") ? resolved : "./" + resolved;
15
+ };
16
+ const ordered = [...services].sort((a, b) => a.key.localeCompare(b.key));
17
+ const lines = [
18
+ 'import type {FastifyPluginAsync} from "fastify";',
19
+ 'import {types} from "node:util";',
20
+ `import definition from ${literal(relative(options.openapiFile))} with {type: "json"};`,
21
+ ...ordered.flatMap((service, index) => [
22
+ `import {${service.clientClassName} as Client${index}} from ${literal(relative(path.join(service.clientDir, "client" + suffix)))};`,
23
+ `import type {${service.clientClassName}Operations as Operations${index}} from ${literal(relative(path.join(service.clientDir, "operations" + suffix)))};`,
24
+ `import service${index} from ${literal(relative(path.join(service.gatewayDir, "plugin" + suffix)))};`,
25
+ ]),
26
+ "",
27
+ "export const openapi = definition;",
28
+ "export interface MultiServiceClients {",
29
+ ...ordered.map((service, index) => ` ${literal(service.key)}: Operations${index};`),
30
+ "}",
31
+ "export interface MultiServiceClientOptions {",
32
+ ...ordered.map((service, index) => ` ${literal(service.key)}: ConstructorParameters<typeof Client${index}>[0];`),
33
+ "}",
34
+ "export interface MultiServiceGatewayOptions {clients: MultiServiceClients}",
35
+ "function registry(value: unknown): void {",
36
+ ' if (!value || typeof value !== "object" || types.isProxy(value) || ![null, Object.prototype].includes(Object.getPrototypeOf(value) as object | null)) throw new TypeError("Service registry must be a data object");',
37
+ ` const expected = ${literal(ordered.map(service => service.key))};`,
38
+ ' if (Reflect.ownKeys(value).length !== expected.length) throw new TypeError("Service registry must contain exactly the configured services");',
39
+ " for (const key of expected) {",
40
+ " const property = Object.getOwnPropertyDescriptor(value, key);",
41
+ ' if (!property || !Object.hasOwn(property, "value") || !property.value) throw new TypeError("Missing service registry value: " + key);',
42
+ " }",
43
+ "}",
44
+ "",
45
+ "/** Bind each generated SOAP client to its own explicit runtime WSDL and options. */",
46
+ "export function createClients(options: MultiServiceClientOptions): {",
47
+ ...ordered.map((service, index) => ` ${literal(service.key)}: Client${index};`),
48
+ "} {",
49
+ " registry(options);",
50
+ " return {",
51
+ ...ordered.map((service, index) => ` ${literal(service.key)}: new Client${index}(options[${literal(service.key)}]),`),
52
+ " };",
53
+ "}",
54
+ "",
55
+ "const plugin: FastifyPluginAsync<MultiServiceGatewayOptions> = async (fastify, options) => {",
56
+ ' if (fastify.prefix) throw new TypeError("Configure service OpenAPI basePath instead of a runtime aggregate prefix");',
57
+ " registry(options.clients);",
58
+ ...ordered.map((service, index) => ` await fastify.register(async scope => {await scope.register(service${index}, {client: options.clients[${literal(service.key)}]});});`),
59
+ ...(options.openapiPath === false ? [] : [` fastify.get(${literal(options.openapiPath ?? "/openapi.json")}, async () => openapi);`]),
60
+ "};",
61
+ "export default plugin;",
62
+ "",
63
+ ];
64
+ return composeGeneratedSource(lines.join("\n"), resolvePreamble("gateway"));
65
+ }
@@ -131,6 +131,7 @@ export async function generateGateway(opts) {
131
131
  }
132
132
  // Resolve client metadata for full handler generation
133
133
  const clientMeta = resolveClientMeta({
134
+ gatewayDir: opts.outDir,
134
135
  clientDir: opts.clientDir,
135
136
  catalogFile: opts.catalogFile,
136
137
  clientClassName: opts.clientClassName,
@@ -922,7 +922,7 @@ export function emitRouteFilesWithHandlers(outDir, routesDir, versionSlug, servi
922
922
  ? "toJsonArray"
923
923
  : "toNdjson";
924
924
  const runtimeImport = op.stream
925
- ? `import { ${streamHelperName} } from "../runtime${suffix}";`
925
+ ? `import { ${streamHelperName}${hasUnwrap ? ", unwrapArrayWrappers" : ""} } from "../runtime${suffix}";`
926
926
  : hasUnwrap
927
927
  ? `import { buildSuccessEnvelope, unwrapArrayWrappers } from "../runtime${suffix}";`
928
928
  : `import { buildSuccessEnvelope } from "../runtime${suffix}";`;
@@ -942,9 +942,24 @@ export function emitRouteFilesWithHandlers(outDir, routesDir, versionSlug, servi
942
942
  const handlerBody = op.stream
943
943
  ? ` handler: async (request, reply) => {
944
944
  const client = fastify.${clientMeta.decoratorName};
945
- const result = await client.${clientMethod}(${bodyArg});
946
- reply.type(${JSON.stringify(op.stream.mediaType)});
947
- return reply.send(${streamHelperName}(result.records));
945
+ const controller = new AbortController();
946
+ const disconnected = () => { if (!reply.raw.writableEnded) controller.abort(); };
947
+ request.raw.once("aborted", disconnected);
948
+ reply.raw.once("close", disconnected);
949
+ if (request.raw.aborted || reply.raw.destroyed) controller.abort();
950
+ try {
951
+ const result = await client.${clientMethod}(${bodyArg}, { signal: controller.signal });
952
+ reply.type(${JSON.stringify(op.stream.mediaType)});
953
+ ${hasUnwrap ? `async function* mappedRecords() {
954
+ for await (const record of result.records) yield unwrapArrayWrappers(record, ${JSON.stringify(op.stream.recordTypeName ?? "unknown")});
955
+ }
956
+ // @constraints Keep disconnect cancellation active until the response stream finishes.
957
+ return await reply.send(${streamHelperName}(mappedRecords()));` : `// @constraints Keep disconnect cancellation active until the response stream finishes.
958
+ return await reply.send(${streamHelperName}(result.records));`}
959
+ } finally {
960
+ request.raw.removeListener("aborted", disconnected);
961
+ reply.raw.removeListener("close", disconnected);
962
+ }
948
963
  },`
949
964
  : ` handler: async (request) => {
950
965
  const client = fastify.${clientMeta.decoratorName};
@@ -199,6 +199,7 @@ export interface ResolvedOperationMeta {
199
199
  * @property {"js"|"ts"|"bare"} importsMode - Import extension mode
200
200
  */
201
201
  export interface ResolveClientMetaOptions {
202
+ gatewayDir?: string;
202
203
  clientDir?: string;
203
204
  catalogFile?: string;
204
205
  clientClassName?: string;
@@ -11,6 +11,7 @@
11
11
  * - Automatic version/service detection from OpenAPI paths
12
12
  */
13
13
  import { pascal, trimRepeatedEdgeChar } from "../util/tools.js";
14
+ import path from "node:path";
14
15
  /**
15
16
  * Converts a name to a deterministic slug for use in file names and URN IDs
16
17
  *
@@ -393,6 +394,20 @@ export function resolveClientMeta(opts, catalog) {
393
394
  const operationsImportPath = opts.clientDir
394
395
  ? `../client/operations${suffix}`
395
396
  : `./client/operations${suffix}`;
397
+ const modulePath = (from, module) => {
398
+ const relative = path.relative(from, path.join(opts.clientDir, module + suffix)).replaceAll("\\", "/");
399
+ if (path.isAbsolute(relative) || /^[a-z]:/i.test(relative))
400
+ throw new Error("Generated client and gateway modules must share a filesystem root");
401
+ return relative.startsWith(".") ? relative : "./" + relative;
402
+ };
403
+ if (opts.clientDir && opts.gatewayDir)
404
+ return {
405
+ className, decoratorName,
406
+ importPath: modulePath(path.join(opts.gatewayDir, "routes"), "client"),
407
+ typesImportPath: modulePath(path.join(opts.gatewayDir, "routes"), "types"),
408
+ pluginImportPath: modulePath(opts.gatewayDir, "client"),
409
+ operationsImportPath: modulePath(opts.gatewayDir, "operations"),
410
+ };
396
411
  return {
397
412
  className,
398
413
  decoratorName,