@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,15 +1,23 @@
1
1
  import { directLimit, directLoadingLimits } from './directExecutionPolicy.js';
2
2
  /** Transient source-owned schema selection. This module grants no runtime admission. */
3
3
  import { loadSemanticSchemaInput } from '../loader/schemaInput.js';
4
- import { SchemaLoadingError, WSDL_NAMESPACE, syntaxAttribute, syntaxElements, isXmlNCName } from '../loader/orderedSyntax.js';
4
+ import { SchemaLoadingError, WSDL_NAMESPACE, XSD_NAMESPACE, syntaxAttribute, syntaxElements, isXmlNCName } from '../loader/orderedSyntax.js';
5
5
  import { globalId } from './canonicalGraph.js';
6
6
  import { createApplicationBudget, DEFAULT_MAX_NODES } from './semanticBudget.js';
7
7
  import { qualifyDirectSource } from './checkedSourcePreparation.js';
8
8
  import { qualifyComponentSource, inspectComponentSourceQualification } from './checkedComponentSource.js';
9
9
  import { prepareWsdlPolicyRequirements, assertWsdlPolicyRequirements } from './wsdlPolicyRequirements.js';
10
10
  const SOAP = 'http://schemas.xmlsoap.org/wsdl/soap/';
11
+ const SOAP12 = 'http://schemas.xmlsoap.org/wsdl/soap12/';
11
12
  const HTTP = 'http://schemas.xmlsoap.org/soap/http';
12
13
  const ADDRESSING_WSDL = 'http://www.w3.org/2006/05/addressing/wsdl';
14
+ /** Distinguish typed RPC parameters without inventing a document element.
15
+ * @param root Selected native message.
16
+ * @returns Whether the message uses the bounded RPC profile.
17
+ */
18
+ export function isRpcMessageRoot(root) {
19
+ return 'kind' in root && root.kind === 'rpc-typed-parts-v1';
20
+ }
13
21
  const issued = new WeakMap();
14
22
  /** Object identity authenticates the exact issued roots; context IDs and digests do not. */
15
23
  export function inspectDirectOperation(contract, expectedContext) {
@@ -164,7 +172,8 @@ async function selectOperationInRequest(source, selector, options, budget, profi
164
172
  }
165
173
  };
166
174
  const ws = (local) => `${WSDL_NAMESPACE}#${local}`;
167
- const soap = (local) => `${SOAP}#${local}`;
175
+ let soapNamespace = SOAP;
176
+ const soap = (local) => `${soapNamespace}#${local}`;
168
177
  const attrs = (node, allowed) => {
169
178
  children(node);
170
179
  for (const a of node.attributes)
@@ -172,7 +181,7 @@ async function selectOperationInRequest(source, selector, options, budget, profi
172
181
  && (a.name.namespace !== '' || !allowed.includes(a.name.local)))
173
182
  refuse('unsupported', 'binding-attribute-shape');
174
183
  };
175
- // Conservative extension policy: only known WSDL 1.1/SOAP 1.1 syntax, even when optional.
184
+ // @constraints Component selection admits known SOAP 1.2 syntax; the direct profile remains SOAP 1.1 only.
176
185
  // Documentation payload and XSD syntax are inert here and owned separately.
177
186
  budget.chargeWork(64 + input.schemas.length * 32);
178
187
  const qualifiedSchemas = new Set(input.schemas.map(schema => schema.syntax));
@@ -181,7 +190,7 @@ async function selectOperationInRequest(source, selector, options, budget, profi
181
190
  portType: ['name'], operation: ['name', 'parameterOrder'], input: ['name', 'message'], output: ['name', 'message'], fault: ['name', 'message'],
182
191
  binding: ['name', 'type'], service: ['name'], port: ['name', 'binding'],
183
192
  };
184
- const soapAttributes = { binding: ['style', 'transport'], operation: ['soapAction', 'style'], body: ['use', 'parts'], fault: ['name', 'use'], address: ['location'] };
193
+ const soapAttributes = { binding: ['style', 'transport'], operation: ['soapAction', 'style'], body: profile === 'components' ? ['use', 'parts', 'namespace', 'encodingStyle'] : ['use', 'parts'], fault: ['name', 'use'], address: ['location'] };
185
194
  for (const document of input.documents) {
186
195
  const stack = [document.root];
187
196
  while (stack.length) {
@@ -193,7 +202,9 @@ async function selectOperationInRequest(source, selector, options, budget, profi
193
202
  continue;
194
203
  if (node.name.namespace === WSDL_NAMESPACE && node.name.local === 'documentation')
195
204
  continue;
196
- const allowed = node.name.namespace === WSDL_NAMESPACE ? attributes[node.name.local] : node.name.namespace === SOAP ? soapAttributes[node.name.local] : undefined;
205
+ const soap12 = profile === 'components' && node.name.namespace === SOAP12;
206
+ const allowed = node.name.namespace === WSDL_NAMESPACE ? attributes[node.name.local] : node.name.namespace === SOAP || soap12
207
+ ? soap12 && node.name.local === 'operation' ? [...soapAttributes.operation, 'soapActionRequired'] : soapAttributes[node.name.local] : undefined;
197
208
  if (!allowed)
198
209
  return refuse('unsupported', 'wsdl-extension');
199
210
  for (const a of node.attributes) {
@@ -201,7 +212,7 @@ async function selectOperationInRequest(source, selector, options, budget, profi
201
212
  // UsingAddressing remains unsupported, so these attributes are retained metadata, never SOAP header instructions.
202
213
  if (addressingAttribute(node, a.name.namespace, a.name.local))
203
214
  continue;
204
- if (a.name.namespace === WSDL_NAMESPACE && a.name.local === 'required' && node.name.namespace === SOAP && ['true', '1', 'false', '0'].includes(a.value))
215
+ if (a.name.namespace === WSDL_NAMESPACE && a.name.local === 'required' && (node.name.namespace === SOAP || soap12) && ['true', '1', 'false', '0'].includes(a.value))
205
216
  continue;
206
217
  if (a.name.namespace !== '' || !allowed.includes(a.name.local))
207
218
  refuse('unsupported', 'wsdl-attribute');
@@ -246,21 +257,34 @@ async function selectOperationInRequest(source, selector, options, budget, profi
246
257
  return matches[0];
247
258
  };
248
259
  const abstract = operation(portType), bound = operation(binding);
260
+ const bindingExtensions = children(binding.syntax).filter(node => [SOAP, SOAP12].includes(node.name.namespace) && node.name.local === 'binding');
261
+ if (bindingExtensions.length !== 1)
262
+ return refuse('selection-refused', 'expected-one-binding');
263
+ soapNamespace = bindingExtensions[0].name.namespace;
249
264
  attrs(bound, ['name']);
250
265
  shape(binding.syntax, [soap('binding'), ws('operation'), ws('documentation')]);
251
- const soapBinding = exactly(binding.syntax, SOAP, 'binding');
252
- const soapOperation = exactly(bound, SOAP, 'operation');
266
+ const soapBinding = exactly(binding.syntax, soapNamespace, 'binding');
267
+ const soapOperation = exactly(bound, soapNamespace, 'operation');
253
268
  shape(soapBinding, []);
254
269
  shape(soapOperation, []);
255
270
  if (!['document', 'rpc'].includes(attr(soapBinding, 'style') ?? 'document'))
256
271
  return refuse('unsupported', 'soap-binding-style');
257
272
  if (attr(soapBinding, 'transport') !== HTTP)
258
273
  return refuse('unsupported', 'soap-transport');
259
- if ((attr(soapOperation, 'style') ?? attr(soapBinding, 'style') ?? 'document') !== 'document')
274
+ const style = attr(soapOperation, 'style') ?? attr(soapBinding, 'style') ?? 'document';
275
+ if (style !== 'document' && !(profile === 'components' && style === 'rpc' && soapNamespace === SOAP))
260
276
  return refuse('unsupported', 'soap-style');
261
- const action = attr(soapOperation, 'soapAction');
262
- if (action === undefined)
277
+ const required = attr(soapOperation, 'soapActionRequired');
278
+ if (required !== undefined && !['true', 'false', '1', '0'].includes(required))
279
+ return refuse('unsupported', 'soap-action-required-boolean');
280
+ const actionRequired = required !== 'false' && required !== '0';
281
+ const selectedAction = attr(soapOperation, 'soapAction');
282
+ if (selectedAction === undefined && (soapNamespace === SOAP || actionRequired))
263
283
  return refuse('unsupported', 'soap-action-required');
284
+ if (soapNamespace === SOAP12 && selectedAction !== undefined && !/^[A-Za-z][A-Za-z0-9+.-]*:[^\s]*$/.test(selectedAction))
285
+ return refuse('unsupported', 'soap-action-absolute-uri');
286
+ // @why SOAP 1.2 permits an absent optional action; the existing string contract uses an empty value to omit it on HTTP.
287
+ const action = selectedAction ?? '';
264
288
  shape(abstract, [ws('input'), ws('output'), ws('fault'), ws('documentation')]);
265
289
  shape(bound, [soap('operation'), ws('input'), ws('output'), ws('fault'), ws('documentation')]);
266
290
  const abstractInput = exactly(abstract, WSDL_NAMESPACE, 'input'), abstractOutput = exactly(abstract, WSDL_NAMESPACE, 'output');
@@ -281,8 +305,9 @@ async function selectOperationInRequest(source, selector, options, budget, profi
281
305
  attrs(boundSyntax, ['name']);
282
306
  shape(messageSyntax, [ws('documentation')]);
283
307
  shape(boundSyntax, [soap(faultName === undefined ? 'body' : 'fault'), ws('documentation')]);
284
- const body = exactly(boundSyntax, SOAP, faultName === undefined ? 'body' : 'fault');
308
+ const body = exactly(boundSyntax, soapNamespace, faultName === undefined ? 'body' : 'fault');
285
309
  shape(body, []);
310
+ attrs(body, faultName === undefined ? ['use', 'parts'] : ['use', 'name']);
286
311
  if (attr(body, 'use') !== 'literal')
287
312
  return refuse('unsupported', 'literal-required');
288
313
  if (faultName !== undefined && attr(body, 'name') !== faultName)
@@ -309,17 +334,61 @@ async function selectOperationInRequest(source, selector, options, budget, profi
309
334
  return refuse('selection-refused', 'element-type');
310
335
  return Object.freeze({ message: message.name, part: partName, element, type: element.type, source: part.source });
311
336
  };
337
+ const rpcRoot = (messageSyntax, boundSyntax, direction) => {
338
+ attrs(messageSyntax, ['name', 'message']);
339
+ attrs(boundSyntax, ['name']);
340
+ shape(messageSyntax, [ws('documentation')]);
341
+ shape(boundSyntax, [soap('body'), ws('documentation')]);
342
+ const body = exactly(boundSyntax, SOAP, 'body');
343
+ shape(body, []);
344
+ const encoding = 'http://schemas.xmlsoap.org/soap/encoding/';
345
+ if (attr(body, 'use') !== 'encoded' || attr(body, 'encodingStyle')?.trim() !== encoding)
346
+ return refuse('unsupported', 'rpc-string-encoding');
347
+ const bodyNamespace = attr(body, 'namespace');
348
+ if (!bodyNamespace || !/^[A-Za-z][A-Za-z0-9+.-]*:/.test(bodyNamespace))
349
+ return refuse('unsupported', 'rpc-body-namespace');
350
+ const message = reference(portType, messageSyntax, 'message');
351
+ if (message.kind !== 'wsdl' || message.role !== 'message')
352
+ return refuse('selection-refused', 'message-reference');
353
+ shape(message.syntax, [ws('part'), ws('documentation')]);
354
+ const declared = children(message.syntax).filter(c => c.name.namespace === WSDL_NAMESPACE && c.name.local === 'part');
355
+ if (!declared.length || direction === 'output' && declared.length !== 1)
356
+ return refuse('unsupported', 'rpc-single-string-result');
357
+ const names = new Set();
358
+ const parts = declared.map(part => {
359
+ attrs(part, ['name', 'type']);
360
+ shape(part, [ws('documentation')]);
361
+ const name = attr(part, 'name');
362
+ if (!name || !isXmlNCName(name) || names.has(name))
363
+ return refuse('selection-refused', 'rpc-part-name');
364
+ names.add(name);
365
+ const path = part.source.path.slice(message.syntax.source.path.length) || '/';
366
+ const refs = message.references.filter(r => r.path === path && r.attribute === 'type');
367
+ if (refs.length !== 1)
368
+ return refuse('unsupported', 'rpc-part-type-required');
369
+ const resolved = context.resolve(refs[0].reference);
370
+ if (direction === 'output' && (resolved.kind !== 'builtin' || resolved.name.namespace !== XSD_NAMESPACE || resolved.name.local !== 'string'))
371
+ return refuse('unsupported', 'rpc-string-part-required');
372
+ return Object.freeze({ name, type: refs[0].reference, source: part.source });
373
+ });
374
+ if (attr(body, 'parts') !== undefined && attr(body, 'parts').trim().split(/[\t\r\n ]+/).join(' ') !== parts.map(p => p.name).join(' '))
375
+ return refuse('unsupported', 'rpc-body-parts');
376
+ return Object.freeze({ kind: 'rpc-typed-parts-v1', message: message.name, parts, bodyNamespace, encoding,
377
+ wrapper: { namespace: bodyNamespace, local: ownedSelector.operation + (direction === 'output' ? 'Response' : '') }, source: body.source });
378
+ };
312
379
  const io = (direction, declaration) => {
313
380
  const boundIo = exactly(bound, WSDL_NAMESPACE, direction);
314
381
  const boundName = attr(boundIo, 'name');
315
382
  const expectedName = attr(declaration, 'name') ?? `${ownedSelector.operation}${direction === 'input' ? 'Request' : 'Response'}`;
316
383
  if (boundName !== undefined && boundName !== expectedName)
317
384
  return refuse('selection-refused', 'binding-message-name');
318
- return root(declaration, boundIo);
385
+ return style === 'rpc' ? rpcRoot(declaration, boundIo, direction) : root(declaration, boundIo);
319
386
  };
320
387
  const requestRoot = io('input', abstractInput), responseRoot = io('output', abstractOutput);
321
388
  const declaredFaults = children(abstract).filter(c => c.name.namespace === WSDL_NAMESPACE && c.name.local === 'fault');
322
389
  const boundFaults = children(bound).filter(c => c.name.namespace === WSDL_NAMESPACE && c.name.local === 'fault');
390
+ if (style === 'rpc' && (declaredFaults.length || boundFaults.length))
391
+ return refuse('unsupported', 'rpc-declared-fault');
323
392
  if (declaredFaults.length !== boundFaults.length)
324
393
  return refuse('selection-refused', 'fault-mapping');
325
394
  const faultNames = new Set();
@@ -340,7 +409,8 @@ async function selectOperationInRequest(source, selector, options, budget, profi
340
409
  const contract = { selector: ownedSelector, contextId: context.contextId,
341
410
  basis: profile === 'direct' ? 'native-direct-premises-v1' : 'native-component-premises-v1',
342
411
  correspondence: profile === 'direct' ? 'dated-XSD-2004-direct-v1' : 'dated-XSD-2004-components-v1',
343
- binding: { version: '1.1', style: 'document', use: 'literal', transport: HTTP, action, source: binding.context.source },
412
+ binding: { version: soapNamespace === SOAP12 ? '1.2' : '1.1', ...(soapNamespace === SOAP12 ? { actionRequired } : {}), style: style, use: style === 'rpc' ? 'encoded' : 'literal',
413
+ ...(style === 'rpc' ? { encoding: 'http://schemas.xmlsoap.org/soap/encoding/' } : {}), transport: HTTP, action, source: binding.context.source },
344
414
  input: requestRoot, output: responseRoot, faults,
345
415
  ...(policyOwner ? { policy: policyOwner.forBinding(binding.syntax), addressingActions: { required: false,
346
416
  input: attr(abstractInput, 'Action', ADDRESSING_WSDL), output: attr(abstractOutput, 'Action', ADDRESSING_WSDL),
@@ -21,25 +21,55 @@ export interface DirectSoapFailure {
21
21
  readonly usage: ResolverUsage;
22
22
  readonly phase?: 'input' | 'response';
23
23
  }
24
- type SoapResult<D> = DirectSoapFailure | Readonly<{
24
+ /** Version-specific protocol metadata; application detail remains owned by native mappings.
25
+ * @since 1.3.0
26
+ */
27
+ export interface Soap12FaultMetadata {
28
+ readonly version: '1.2';
29
+ readonly code: ExpandedName;
30
+ readonly subcodes: readonly ExpandedName[];
31
+ readonly reasons: readonly Readonly<{
32
+ lang: string;
33
+ text: string;
34
+ }>[];
35
+ readonly string: string;
36
+ readonly node?: string;
37
+ readonly role?: string;
38
+ }
39
+ export type DeclaredSoapFault = Readonly<{
40
+ name: string;
41
+ code: ExpandedName;
42
+ codeNode: SyntaxElement;
43
+ string: string;
44
+ actor?: string;
45
+ }> & Partial<Soap12FaultMetadata>;
46
+ /** A valid SOAP 1.2 fault without a uniquely declared application detail.
47
+ * @since 1.3.0
48
+ * @constraints Unassessed detail remains only in responseBytes; no native value receipt is issued.
49
+ */
50
+ export interface ComponentProtocolFault {
51
+ readonly kind: 'soap-fault';
52
+ readonly admitted: false;
53
+ readonly fault: Soap12FaultMetadata;
54
+ readonly responseBytes: Uint8Array;
55
+ readonly status: number;
56
+ readonly usage: ResolverUsage;
57
+ readonly ignoredHeaders?: readonly ExpandedName[];
58
+ }
59
+ type DecodedSoapResult<D> = DirectSoapFailure | Readonly<{
25
60
  kind: 'soap-response' | 'soap-fault';
26
61
  admitted: false;
27
62
  decoded: D;
28
63
  /** Actual bounded HTTP entity bytes, before any decoding; caller-owned data. */
29
64
  responseBytes: Uint8Array;
30
65
  status: number;
31
- fault?: Readonly<{
32
- name: string;
33
- code: ExpandedName;
34
- codeNode: SyntaxElement;
35
- string: string;
36
- actor?: string;
37
- }>;
38
- /** Qualified optional response blocks ignored under SOAP 1.1; their application semantics were not assessed. */
66
+ fault?: DeclaredSoapFault;
67
+ /** Unprocessed header blocks; their application semantics were not assessed. */
39
68
  ignoredHeaders?: readonly ExpandedName[];
40
69
  usage: ResolverUsage;
41
70
  }>;
42
- export type DirectSoapResult = SoapResult<DirectDecodedXml>;
71
+ type SoapResult<D> = DecodedSoapResult<D> | ComponentProtocolFault;
72
+ export type DirectSoapResult = DecodedSoapResult<DirectDecodedXml>;
43
73
  export type ComponentSoapResult = SoapResult<ComponentDecodedXml>;
44
74
  export type ComponentRecordSoapResult = SoapResult<ComponentDecodedXml | ComponentRecordDecodedXml>;
45
75
  export interface ComponentSoapOptions extends Omit<DirectSoapOptions, 'endpoint' | 'allowEndpoint'> {
@@ -1,11 +1,11 @@
1
1
  import { isApplicationUnmetered } from './semanticBudget.js';
2
2
  import { directDataOwnership, directLimit } from './directExecutionPolicy.js';
3
- /** Shared retained-operation SOAP 1.1 exchange; profile-specific authority stays explicit. */
3
+ /** Shared retained-operation SOAP exchange; profile-specific authority stays explicit. */
4
4
  import { checkSemanticBudget } from './semanticBudget.js';
5
5
  import { DataOwnershipLimitError } from './dataOwnership.js';
6
6
  import { Agent } from 'node:http';
7
7
  import { Agent as HttpsAgent } from 'node:https';
8
- import { types } from 'node:util';
8
+ import { types, MIMEType } from 'node:util';
9
9
  import { WSDL } from 'soap/lib/wsdl/index.js';
10
10
  import { Client } from 'soap/lib/client.js';
11
11
  import { HttpClient } from 'soap/lib/http.js';
@@ -16,7 +16,7 @@ import { prepareDirectSoapSource, prepareComponentSoapWsdl, DirectSoapSourceErro
16
16
  import { isXmlNCName, resolveLexicalQName, SchemaLoadingError, syntaxElements, syntaxAttribute, WSDL_NAMESPACE } from '../loader/orderedSyntax.js';
17
17
  import { decodeComponentRecordXml, inspectComponentRecordProjection } from './componentRecordProjection.js';
18
18
  import { assessComponentSoapHeader } from './soapHeaders.js';
19
- const SOAP = 'http://schemas.xmlsoap.org/soap/envelope/';
19
+ const SOAP11 = 'http://schemas.xmlsoap.org/soap/envelope/';
20
20
  class Stop extends Error {
21
21
  kind;
22
22
  rule;
@@ -44,7 +44,7 @@ export function directSoapOptionsShape(options, check, additional = []) {
44
44
  /** Authentication and fresh-value validation precede dependency bootstrap and all HTTP I/O. */
45
45
  export async function exchangeDirectSoap(contract, inputMapping, application, options) {
46
46
  const input = inspectDirectValueMapping(inputMapping, contract);
47
- return exchangeSoap(contract, input, application, options, {
47
+ const result = await exchangeSoap(contract, input, application, options, {
48
48
  component: false, encode: encodeDirectXml, decode: decodeDirectXml, create: createDirectValueMapping,
49
49
  prepare: async (selection, check) => {
50
50
  const { source } = inspectDirectOperation(contract);
@@ -54,6 +54,9 @@ export async function exchangeDirectSoap(contract, inputMapping, application, op
54
54
  return wsdl;
55
55
  },
56
56
  });
57
+ if (result.kind === 'soap-fault' && !('decoded' in result))
58
+ throw new TypeError('Component fault on direct profile');
59
+ return result;
57
60
  }
58
61
  export async function exchangeComponentSoap(contract, inputMapping, application, options, responseProjection) {
59
62
  const { mapping: input } = inspectComponentValueMapping(inputMapping, contract);
@@ -89,6 +92,8 @@ export async function exchangeComponentSoap(contract, inputMapping, application,
89
92
  });
90
93
  }
91
94
  async function exchangeSoap(contract, input, application, options, profile) {
95
+ const soap12 = profile.component && contract.binding.version === '1.2';
96
+ const SOAP = soap12 ? 'http://www.w3.org/2003/05/soap-envelope' : SOAP11;
92
97
  const { context } = inspectDirectOperation(contract);
93
98
  if (input.direction.kind !== 'input')
94
99
  throw new TypeError('SOAP request requires the exact input mapping');
@@ -120,7 +125,7 @@ async function exchangeSoap(contract, input, application, options, profile) {
120
125
  const { source } = inspectDirectOperation(contract);
121
126
  const services = source.documents.flatMap(d => syntaxElements(d.root, WSDL_NAMESPACE, 'service').filter(n => syntaxAttribute(d.root, 'targetNamespace') === options.service?.namespace && syntaxAttribute(n, 'name') === options.service?.local));
122
127
  const ports = services.flatMap(n => syntaxElements(n, WSDL_NAMESPACE, 'port').filter(p => syntaxAttribute(p, 'name') === options.port));
123
- const addresses = ports.flatMap(p => syntaxElements(p, 'http://schemas.xmlsoap.org/wsdl/soap/', 'address'));
128
+ const addresses = ports.flatMap(p => syntaxElements(p, soap12 ? 'http://schemas.xmlsoap.org/wsdl/soap12/' : 'http://schemas.xmlsoap.org/wsdl/soap/', 'address'));
124
129
  if (services.length !== 1 || ports.length !== 1 || addresses.length !== 1 || !syntaxAttribute(addresses[0], 'location')) {
125
130
  return fail('unsupported', 'Select one SOAP service/port address or supply an endpoint override');
126
131
  }
@@ -171,6 +176,7 @@ async function exchangeSoap(contract, input, application, options, profile) {
171
176
  && (typeof allowEndpoint !== 'function' || allowEndpoint(new URL(endpoint.href)) !== true))
172
177
  return fail('unsupported', 'endpoint authorization');
173
178
  const action = contract.binding.action;
179
+ const rpc = contract.binding.style === 'rpc';
174
180
  if (/[^\x20-\x21\x23-\x7e]|\\/.test(action))
175
181
  return fail('unsupported', 'unsafe SOAPAction');
176
182
  const elements = (node) => {
@@ -191,20 +197,21 @@ async function exchangeSoap(contract, input, application, options, profile) {
191
197
  return fail(mapping.kind, mapping.rule);
192
198
  faults.set(fault.name, mapping);
193
199
  }
194
- const shape = (node) => {
200
+ const shape = (node, encodedScope = false) => {
195
201
  const children = elements(node);
196
- if (node.attributes.length || node.children.some(n => n.kind === 'text' && /[^\t\r\n ]/.test(n.value)))
202
+ if (node.attributes.some(a => !(encodedScope && a.name.namespace === SOAP && a.name.local === 'encodingStyle'
203
+ && a.value === 'http://schemas.xmlsoap.org/soap/encoding/')) || node.children.some(n => n.kind === 'text' && /[^\t\r\n ]/.test(n.value)))
197
204
  stop('protocol', 'SOAP protocol attributes or text');
198
205
  return children;
199
206
  };
200
207
  const ignoredHeaders = [];
201
208
  const bodyRoot = (root, incoming = false) => {
202
209
  if (!same(root.name, { namespace: SOAP, local: 'Envelope' }))
203
- stop('protocol', 'SOAP 1.1 Envelope required');
204
- const children = shape(root);
210
+ stop('protocol', `SOAP ${contract.binding.version} Envelope required`);
211
+ const children = shape(root, rpc);
205
212
  if (children[0]?.name.namespace === SOAP && children[0].name.local === 'Header') {
206
213
  if (profile.component && incoming) {
207
- const assessed = assessComponentSoapHeader(children[0], check);
214
+ const assessed = assessComponentSoapHeader(children[0], check, contract.binding.version);
208
215
  if (assessed.kind !== 'ignored-optional-headers')
209
216
  stop(assessed.kind, assessed.rule);
210
217
  else
@@ -219,13 +226,25 @@ async function exchangeSoap(contract, input, application, options, profile) {
219
226
  }
220
227
  if (children.length !== 1 || !same(children[0].name, { namespace: SOAP, local: 'Body' }))
221
228
  stop('protocol', 'one SOAP Body required');
222
- const body = shape(children[0]);
229
+ const body = shape(children[0], rpc);
230
+ if (rpc && !body.some(n => same(n.name, { namespace: SOAP, local: 'Fault' })))
231
+ return children[0];
223
232
  if (body.length !== 1)
224
233
  stop('protocol', 'one SOAP Body payload required');
225
234
  return body[0];
226
235
  };
227
236
  const wsdl = await profile.prepare(config, check);
228
237
  check();
238
+ // @why node-soap's RPC writer would wrap `_xml` as a parameter. This private
239
+ // instance retains its selected method/transport while native bytes own RPC construction.
240
+ const rpcArguments = Object.freeze({});
241
+ if (rpc)
242
+ wsdl.objectToRpcXML = (name, args) => {
243
+ check();
244
+ if (name !== contract.selector.operation || args !== rpcArguments)
245
+ stop('protocol', 'dependency RPC selection mismatch');
246
+ return Buffer.from(encoded.bytes).toString('utf8');
247
+ };
229
248
  // HTTP and codec byte limits bound buffering and output copies directly.
230
249
  agent = endpoint.protocol === 'https:' ? new HttpsAgent({ keepAlive: false, maxSockets: 1, rejectUnauthorized: true,
231
250
  ...(config.ca === undefined ? {} : { ca: config.ca }) }) : new Agent({ keepAlive: false, maxSockets: 1 });
@@ -241,7 +260,8 @@ async function exchangeSoap(contract, input, application, options, profile) {
241
260
  request(url, data, callback, headers, _options) {
242
261
  try {
243
262
  check();
244
- if (url !== endpoint.href || headers.SOAPAction !== `"${action}"` || headers['Content-Type'] !== 'text/xml; charset=utf-8')
263
+ if (url !== endpoint.href || (soap12 ? headers.SOAPAction !== undefined || headers['Content-Type'] !== requestContentType
264
+ : headers.SOAPAction !== `"${action}"` || headers['Content-Type'] !== 'text/xml; charset=utf-8'))
245
265
  stop('protocol', 'dependency dispatch mismatch');
246
266
  if (Buffer.byteLength(data) > limits.maxBytes)
247
267
  stop('resource-limit', 'SOAP request byte limit');
@@ -280,7 +300,8 @@ async function exchangeSoap(contract, input, application, options, profile) {
280
300
  maxBodyLength: limits.maxBytes, proxy: false, httpAgent: agent, httpsAgent: agent, signal: controller.signal, timeout: config.timeoutMs });
281
301
  }
282
302
  }
283
- const client = new Client(wsdl, endpoint.href, { httpClient: new BufferedHttp() });
303
+ const requestContentType = soap12 ? 'application/soap+xml; charset=utf-8' + (action ? `; action="${action}"` : '') : 'text/xml; charset=utf-8';
304
+ const client = new Client(wsdl, endpoint.href, { httpClient: new BufferedHttp(), ...(soap12 ? { forceSoap12Headers: true } : {}) });
284
305
  // Every lookup is own and explicit; the dependency's first-port shortcut is
285
306
  // never consulted. Source checks above bind these names to the native handle.
286
307
  const own = (object, key) => Object.getOwnPropertyDescriptor(object, key)?.value;
@@ -290,7 +311,7 @@ async function exchangeSoap(contract, input, application, options, profile) {
290
311
  if (typeof method !== 'function')
291
312
  return fail('unsupported', 'dependency selected method unavailable');
292
313
  check();
293
- await new Promise((resolve, reject) => method({ _xml: Buffer.from(encoded.bytes).toString('utf8') }, (error) => error === complete ? resolve() : reject(error), {}, { SOAPAction: `"${action}"` }));
314
+ await new Promise((resolve, reject) => method(rpc ? rpcArguments : { _xml: Buffer.from(encoded.bytes).toString('utf8') }, (error) => error === complete ? resolve() : reject(error), {}, soap12 ? { 'Content-Type': requestContentType } : { SOAPAction: `"${action}"` }));
294
315
  check();
295
316
  if (timedOut)
296
317
  return fail('transport', 'SOAP deadline exceeded');
@@ -301,19 +322,34 @@ async function exchangeSoap(contract, input, application, options, profile) {
301
322
  const observed = response;
302
323
  if (observed.encoding && observed.encoding.toLowerCase() !== 'identity')
303
324
  return fail('unsupported', 'HTTP compression');
304
- const media = /^text\/xml(?:\s*;\s*charset\s*=\s*(?:"(utf-8|utf-16|utf-16le|utf-16be)"|(utf-8|utf-16|utf-16le|utf-16be)))?\s*$/i.exec(observed.contentType);
325
+ let soap12Charset;
326
+ let validSoap12Media = false;
327
+ if (soap12) {
328
+ try {
329
+ const mime = new MIMEType(observed.contentType);
330
+ soap12Charset = mime.params.get('charset')?.toLowerCase();
331
+ validSoap12Media = mime.essence === 'application/soap+xml' && (!soap12Charset || ['utf-8', 'utf-16', 'utf-16le', 'utf-16be', 'iso-8859-1'].includes(soap12Charset));
332
+ }
333
+ catch { /* Invalid media types are refused below. */ }
334
+ }
335
+ const media = soap12 ? validSoap12Media ? ['', soap12Charset] : null : /^text\/xml(?:\s*;\s*charset\s*=\s*(?:"(utf-8|utf-16|utf-16le|utf-16be|iso-8859-1)"|(utf-8|utf-16|utf-16le|utf-16be|iso-8859-1)))?\s*$/i.exec(observed.contentType);
305
336
  if (!media)
306
337
  return fail('protocol', 'SOAP response Content-Type');
307
- if (observed.status !== 200 && observed.status !== 500)
338
+ if (observed.status !== 200 && observed.status !== 500 && !(soap12 && observed.status === 400))
308
339
  return fail('transport', 'SOAP HTTP status');
309
340
  // Header charset is a protocol assertion, checked against actual BOM bytes.
310
341
  const charset = (media[1] ?? media[2])?.toLowerCase();
342
+ if (!profile.component && charset === 'iso-8859-1')
343
+ return fail('protocol', 'SOAP response Content-Type');
311
344
  const bom = observed.bytes[0] === 0xff && observed.bytes[1] === 0xfe ? 'utf-16le'
312
345
  : observed.bytes[0] === 0xfe && observed.bytes[1] === 0xff ? 'utf-16be' : 'utf-8';
313
- if (charset && charset !== bom && !(charset === 'utf-16' && bom.startsWith('utf-16')))
346
+ if (!profile.component && charset && charset !== bom && !(charset === 'utf-16' && bom.startsWith('utf-16')))
314
347
  return fail('protocol', 'SOAP charset disagrees with bytes');
315
348
  let faultInfo;
316
- const decoded = profile.decode(contract, output, observed.bytes, { signal, limits, uri: endpoint.href,
349
+ let protocolFault;
350
+ const protocolFaultSignal = new Error('SOAP 1.2 protocol fault assessed');
351
+ const decodeResponse = () => profile.decode(contract, output, observed.bytes, { signal, limits, uri: endpoint.href,
352
+ ...(profile.component ? { componentResponseEncoding: { charset } } : {}),
317
353
  selectPayload: root => {
318
354
  const payload = bodyRoot(root, true);
319
355
  if (!same(payload.name, { namespace: SOAP, local: 'Fault' })) {
@@ -321,6 +357,87 @@ async function exchangeSoap(contract, input, application, options, profile) {
321
357
  stop('protocol', 'HTTP 500 requires SOAP Fault');
322
358
  return { payload, mapping: output };
323
359
  }
360
+ if (soap12) {
361
+ const children = shape(payload);
362
+ const names = children.map(n => n.name.namespace === SOAP ? n.name.local : '');
363
+ if (!/^Code,Reason(?:,Node)?(?:,Role)?(?:,Detail)?$/.test(names.join(',')))
364
+ stop('protocol', 'SOAP 1.2 Fault children');
365
+ const scalar = (node, language = false) => {
366
+ check();
367
+ if (elements(node).length || node.attributes.some(a => !(language && a.name.namespace === 'http://www.w3.org/XML/1998/namespace' && a.name.local === 'lang')))
368
+ stop('protocol', 'SOAP 1.2 Fault scalar child');
369
+ return node.children.map(n => n.kind === 'text' ? n.value : '').join('');
370
+ };
371
+ const qname = (node) => {
372
+ const lexical = scalar(node);
373
+ try {
374
+ const name = resolveLexicalQName(lexical, node);
375
+ return name;
376
+ }
377
+ catch (error) {
378
+ if (error instanceof SchemaLoadingError)
379
+ return stop('protocol', 'SOAP Fault code QName');
380
+ throw error;
381
+ }
382
+ };
383
+ const codes = [];
384
+ let codeNode;
385
+ let codeContainer = children[0];
386
+ while (codeContainer) {
387
+ check();
388
+ const nested = shape(codeContainer);
389
+ if (nested.length < 1 || nested.length > 2 || !same(nested[0].name, { namespace: SOAP, local: 'Value' })
390
+ || nested[1] && !same(nested[1].name, { namespace: SOAP, local: 'Subcode' }))
391
+ stop('protocol', 'SOAP 1.2 Fault Code');
392
+ codeNode ??= nested[0];
393
+ codes.push(qname(nested[0]));
394
+ codeContainer = nested[1];
395
+ }
396
+ const code = codes[0];
397
+ if (code.namespace !== SOAP || !['VersionMismatch', 'MustUnderstand', 'DataEncodingUnknown', 'Sender', 'Receiver'].includes(code.local))
398
+ stop('protocol', 'SOAP 1.2 Fault code');
399
+ if (observed.status === 400 && code.local !== 'Sender')
400
+ stop('protocol', 'HTTP 400 requires SOAP Sender Fault');
401
+ const reasonChildren = shape(children[1]);
402
+ if (!reasonChildren.length)
403
+ stop('protocol', 'SOAP 1.2 Fault Reason');
404
+ const reasons = reasonChildren.map(node => {
405
+ check();
406
+ if (!same(node.name, { namespace: SOAP, local: 'Text' }))
407
+ stop('protocol', 'SOAP 1.2 Fault Reason Text');
408
+ const text = scalar(node, true), lang = syntaxAttribute(node, 'lang', 'http://www.w3.org/XML/1998/namespace');
409
+ if (lang === undefined || !/^[A-Za-z]{1,8}(?:-[A-Za-z0-9]{1,8})*$/.test(lang))
410
+ stop('protocol', 'SOAP 1.2 Fault language');
411
+ return { lang: lang, text };
412
+ });
413
+ const uriField = (local) => {
414
+ const node = children.find(n => n.name.local === local);
415
+ if (!node)
416
+ return undefined;
417
+ const value = scalar(node).trim();
418
+ if (!/^[A-Za-z][A-Za-z0-9+.-]*:[^\s]*$/.test(value))
419
+ stop('protocol', 'SOAP 1.2 Fault URI');
420
+ return value;
421
+ };
422
+ const node = uriField('Node'), role = uriField('Role');
423
+ const metadata = { version: '1.2', code, subcodes: codes.slice(1), reasons, string: reasons[0].text,
424
+ ...(node === undefined ? {} : { node }), ...(role === undefined ? {} : { role }) };
425
+ const detailNode = children.find(n => n.name.local === 'Detail');
426
+ // @see SOAP 1.2 section 5.4.5 permits attributes on Detail; these protocol-wrapper attributes stay in the original bytes.
427
+ if (detailNode?.children.some(n => n.kind === 'text' && /[^\t\r\n ]/.test(n.value)))
428
+ stop('protocol', 'SOAP 1.2 Fault Detail text');
429
+ const detail = detailNode ? elements(detailNode) : [];
430
+ const matches = contract.faults.filter(f => detail.some(n => same(f.root.element.name, n.name)));
431
+ if (matches.length) {
432
+ if (matches.length !== 1 || detail.length !== 1)
433
+ stop('protocol', 'ambiguous SOAP Fault detail');
434
+ faultInfo = directDataOwnership(budget).freeze({ ...metadata, name: matches[0].name, codeNode: codeNode });
435
+ return { payload: detail[0], mapping: faults.get(faultInfo.name) };
436
+ }
437
+ protocolFault = directDataOwnership(budget).freeze(metadata);
438
+ // @constraints Only protocol grammar was assessed; no fabricated application receipt is published.
439
+ throw protocolFaultSignal;
440
+ }
324
441
  const children = shape(payload);
325
442
  const names = children.map(n => n.name.namespace === '' ? n.name.local : '');
326
443
  if (names.join(',') !== 'faultcode,faultstring,detail' && names.join(',') !== 'faultcode,faultstring,faultactor,detail')
@@ -353,6 +470,22 @@ async function exchangeSoap(contract, input, application, options, profile) {
353
470
  directDataOwnership(budget).freeze(faultInfo);
354
471
  return { payload: detail[0], mapping: faults.get(faultInfo.name) };
355
472
  } });
473
+ let decoded;
474
+ try {
475
+ decoded = decodeResponse();
476
+ }
477
+ catch (error) {
478
+ if (error !== protocolFaultSignal)
479
+ throw error;
480
+ }
481
+ if (protocolFault && decoded === undefined) {
482
+ check();
483
+ return Object.freeze({ kind: 'soap-fault', admitted: false, fault: protocolFault,
484
+ responseBytes: new Uint8Array(observed.bytes), status: observed.status,
485
+ ...(ignoredHeaders.length ? { ignoredHeaders: Object.freeze(ignoredHeaders) } : {}), usage: Object.freeze({ ...budget.usage() }) });
486
+ }
487
+ if (!decoded)
488
+ throw new TypeError('Missing decoded SOAP response');
356
489
  if (!('validated' in decoded))
357
490
  return fail(decoded.kind, decoded.rule, 'response');
358
491
  check();
@@ -16,6 +16,7 @@ export class DirectSoapSourceError extends Error {
16
16
  }
17
17
  }
18
18
  function validateSoapSelection(source, contract, config, check) {
19
+ const addressNamespace = contract.binding.version === '1.2' ? 'http://schemas.xmlsoap.org/wsdl/soap12/' : WSDL_SOAP;
19
20
  const elements = (node) => { check(); return node.children.filter((child) => child.kind === 'element'); };
20
21
  const definitions = source.root.root;
21
22
  if (definitions.name.namespace !== WSDL_NAMESPACE || definitions.name.local !== 'definitions')
@@ -30,8 +31,8 @@ function validateSoapSelection(source, contract, config, check) {
30
31
  const binding = syntaxAttribute(ports[0], 'binding');
31
32
  if (!binding || !same(resolveLexicalQName(binding, ports[0]), contract.selector.binding))
32
33
  throw new DirectSoapSourceError('SOAP binding mismatch');
33
- const addresses = elements(ports[0]).filter(n => n.name.namespace === WSDL_SOAP && n.name.local === 'address');
34
- if (addresses.length !== 1 || elements(ports[0]).some(n => !(n.name.namespace === WSDL_SOAP && n.name.local === 'address')
34
+ const addresses = elements(ports[0]).filter(n => n.name.namespace === addressNamespace && n.name.local === 'address');
35
+ if (addresses.length !== 1 || elements(ports[0]).some(n => !(n.name.namespace === addressNamespace && n.name.local === 'address')
35
36
  && !(n.name.namespace === WSDL_NAMESPACE && n.name.local === 'documentation')))
36
37
  throw new DirectSoapSourceError('SOAP port extension');
37
38
  }