@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.
- package/README.md +22 -3
- package/dist/checkedPlanning.js +27 -4
- package/dist/cli.js +17 -1
- package/dist/client/checkedDirectRuntime.d.ts +11 -1
- package/dist/client/checkedDirectRuntime.js +19 -2
- package/dist/client/directClientRuntime.d.ts +18 -1
- package/dist/client/directClientRuntime.js +15 -3
- package/dist/client/directClientVersion.d.ts +2 -0
- package/dist/client/directClientVersion.js +2 -0
- package/dist/client/generateClient.js +72 -16
- package/dist/client/generateDirectClient.js +14 -9
- package/dist/client/generateOperations.js +30 -11
- package/dist/client/generateUtils.js +5 -1
- package/dist/compiler/canonicalGraph.d.ts +4 -15
- package/dist/compiler/canonicalGraph.js +1 -8
- package/dist/compiler/componentRecordProjection.d.ts +3 -0
- package/dist/compiler/componentRecordProjection.js +10 -0
- package/dist/compiler/declarationIdentity.d.ts +22 -0
- package/dist/compiler/declarationIdentity.js +9 -0
- package/dist/compiler/directCodec.d.ts +22 -2
- package/dist/compiler/directCodec.js +40 -7
- package/dist/compiler/directOperation.d.ts +28 -5
- package/dist/compiler/directOperation.js +84 -14
- package/dist/compiler/directSoap.d.ts +40 -10
- package/dist/compiler/directSoap.js +151 -18
- package/dist/compiler/directSoapSource.js +3 -2
- package/dist/compiler/directSourceCapsule.js +69 -4
- package/dist/compiler/directStructure.js +6 -2
- package/dist/compiler/directStructurePlan.d.ts +10 -1
- package/dist/compiler/directStructurePlan.js +27 -7
- package/dist/compiler/directValues.d.ts +15 -3
- package/dist/compiler/directValues.js +20 -4
- package/dist/compiler/legacyDeclarations.d.ts +26 -0
- package/dist/compiler/legacyDeclarations.js +144 -0
- package/dist/compiler/projectionScalar.d.ts +18 -1
- package/dist/compiler/projectionScalar.js +47 -13
- package/dist/compiler/rpcStringValues.d.ts +30 -0
- package/dist/compiler/rpcStringValues.js +196 -0
- package/dist/compiler/rpcTypedValues.d.ts +48 -0
- package/dist/compiler/rpcTypedValues.js +190 -0
- package/dist/compiler/schemaCompiler.js +44 -109
- package/dist/compiler/semanticCatalog.d.ts +2 -2
- package/dist/compiler/soapHeaders.d.ts +10 -4
- package/dist/compiler/soapHeaders.js +28 -9
- package/dist/gateway/generateCheckedGateway.js +26 -6
- package/dist/gateway/generateComposedGateway.d.ts +16 -0
- package/dist/gateway/generateComposedGateway.js +65 -0
- package/dist/gateway/generateGateway.js +1 -0
- package/dist/gateway/generators.js +19 -4
- package/dist/gateway/helpers.d.ts +1 -0
- package/dist/gateway/helpers.js +15 -0
- package/dist/generation/componentShape.js +25 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -1
- package/dist/loader/orderedSyntax.d.ts +4 -0
- package/dist/loader/orderedSyntax.js +18 -4
- package/dist/multiServicePipeline.d.ts +41 -0
- package/dist/multiServicePipeline.js +287 -0
- package/dist/openapi/composeOpenAPI.d.ts +29 -0
- package/dist/openapi/composeOpenAPI.js +322 -0
- package/dist/pipeline.d.ts +1 -1
- package/dist/pipeline.js +2 -0
- package/docs/agent-skill.md +10 -1
- package/docs/api-reference.md +127 -9
- package/docs/checked-gateway.md +10 -0
- package/docs/checked-generation.md +11 -5
- package/docs/cli-reference.md +25 -5
- package/docs/concepts.md +4 -5
- package/docs/configuration.md +10 -3
- package/docs/corpus.md +1 -1
- package/docs/gateway-guide.md +104 -27
- package/docs/generated-code.md +12 -6
- package/docs/migration-playbook.md +4 -0
- package/docs/migration.md +134 -6
- package/docs/output-anatomy.md +20 -17
- package/docs/production.md +7 -5
- package/docs/start-here.md +12 -2
- package/docs/supported-patterns.md +23 -0
- package/docs/testing.md +2 -0
- package/docs/troubleshooting.md +3 -1
- package/llms.txt +9 -1
- package/package.json +2 -2
- package/src/runtime/clientStreamMethods.tpl.txt +156 -148
- package/src/runtime/operationsStreamHelper.tpl.txt +6 -6
- 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
|
-
|
|
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
|
-
//
|
|
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
|
|
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,
|
|
252
|
-
const soapOperation = exactly(bound,
|
|
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
|
-
|
|
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
|
|
262
|
-
if (
|
|
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,
|
|
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:
|
|
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
|
-
|
|
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?:
|
|
32
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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',
|
|
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 !==
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 ===
|
|
34
|
-
if (addresses.length !== 1 || elements(ports[0]).some(n => !(n.name.namespace ===
|
|
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
|
}
|