@techspokes/typescript-wsdl-client 1.2.0 → 1.4.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 +13 -0
- package/dist/checkedPipeline.d.ts +73 -2
- package/dist/checkedPipeline.js +118 -8
- package/dist/checkedPlanning.d.ts +5 -2
- package/dist/checkedPlanning.js +30 -5
- package/dist/checkedResponseInspection.d.ts +100 -0
- package/dist/checkedResponseInspection.js +499 -0
- package/dist/cli.js +55 -1
- package/dist/client/checkedDirectRuntime.d.ts +19 -1
- package/dist/client/checkedDirectRuntime.js +27 -2
- package/dist/client/directClientRuntime.d.ts +25 -1
- package/dist/client/directClientRuntime.js +23 -5
- package/dist/client/directClientVersion.d.ts +4 -0
- package/dist/client/directClientVersion.js +4 -0
- package/dist/client/generateDirectClient.js +14 -9
- package/dist/compiler/componentRecordProjection.d.ts +1 -0
- package/dist/compiler/componentRecordProjection.js +15 -4
- package/dist/compiler/directCodec.js +4 -12
- package/dist/compiler/directOperation.d.ts +2 -1
- package/dist/compiler/directOperation.js +27 -11
- package/dist/compiler/directSoap.d.ts +87 -12
- package/dist/compiler/directSoap.js +316 -94
- package/dist/compiler/directSoapSource.js +3 -2
- package/dist/compiler/directSourceCapsule.js +12 -3
- package/dist/compiler/soapHeaders.d.ts +10 -4
- package/dist/compiler/soapHeaders.js +28 -9
- package/dist/compiler/sourceCorrections.d.ts +118 -0
- package/dist/compiler/sourceCorrections.js +794 -0
- package/dist/gateway/generateCheckedGateway.js +27 -8
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/dist/util/readBoundedFile.d.ts +9 -0
- package/dist/util/readBoundedFile.js +36 -0
- package/dist/util/xmlBytes.d.ts +9 -0
- package/dist/util/xmlBytes.js +24 -0
- package/docs/api-reference.md +28 -1
- package/docs/checked-gateway.md +52 -0
- package/docs/checked-generation.md +4 -4
- package/docs/cli-reference.md +16 -1
- package/docs/migration.md +20 -0
- package/docs/start-here.md +1 -1
- package/docs/supported-patterns.md +6 -0
- package/package.json +1 -1
|
@@ -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,13 +54,16 @@ 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
|
-
export async function exchangeComponentSoap(contract, inputMapping, application, options, responseProjection) {
|
|
61
|
+
export async function exchangeComponentSoap(contract, inputMapping, application, options, responseProjection, soap11ProtocolFaults = true) {
|
|
59
62
|
const { mapping: input } = inspectComponentValueMapping(inputMapping, contract);
|
|
60
63
|
if (responseProjection !== undefined) {
|
|
61
64
|
inspectComponentRecordProjection(responseProjection, contract);
|
|
62
65
|
return exchangeSoap(contract, input, application, options, {
|
|
63
|
-
component: true,
|
|
66
|
+
component: true, soap11ProtocolFaults,
|
|
64
67
|
encode(owner, mapping, value, controls) {
|
|
65
68
|
if (mapping.kind !== 'component-mapping')
|
|
66
69
|
throw new TypeError('SOAP input requires the native input mapping');
|
|
@@ -83,12 +86,13 @@ export async function exchangeComponentSoap(contract, inputMapping, application,
|
|
|
83
86
|
});
|
|
84
87
|
}
|
|
85
88
|
return exchangeSoap(contract, input, application, options, {
|
|
86
|
-
component: true, encode: encodeComponentXml, decode: decodeComponentXml,
|
|
89
|
+
component: true, soap11ProtocolFaults, encode: encodeComponentXml, decode: decodeComponentXml,
|
|
87
90
|
create: (owner, direction, controls) => createComponentDirectionalMapping(owner, input, direction, controls),
|
|
88
91
|
prepare: (selection, check) => prepareComponentSoapWsdl(contract, selection, check),
|
|
89
92
|
});
|
|
90
93
|
}
|
|
91
94
|
async function exchangeSoap(contract, input, application, options, profile) {
|
|
95
|
+
const soap12 = profile.component && contract.binding.version === '1.2';
|
|
92
96
|
const { context } = inspectDirectOperation(contract);
|
|
93
97
|
if (input.direction.kind !== 'input')
|
|
94
98
|
throw new TypeError('SOAP request requires the exact input mapping');
|
|
@@ -120,7 +124,7 @@ async function exchangeSoap(contract, input, application, options, profile) {
|
|
|
120
124
|
const { source } = inspectDirectOperation(contract);
|
|
121
125
|
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
126
|
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'));
|
|
127
|
+
const addresses = ports.flatMap(p => syntaxElements(p, soap12 ? 'http://schemas.xmlsoap.org/wsdl/soap12/' : 'http://schemas.xmlsoap.org/wsdl/soap/', 'address'));
|
|
124
128
|
if (services.length !== 1 || ports.length !== 1 || addresses.length !== 1 || !syntaxAttribute(addresses[0], 'location')) {
|
|
125
129
|
return fail('unsupported', 'Select one SOAP service/port address or supply an endpoint override');
|
|
126
130
|
}
|
|
@@ -174,10 +178,6 @@ async function exchangeSoap(contract, input, application, options, profile) {
|
|
|
174
178
|
const rpc = contract.binding.style === 'rpc';
|
|
175
179
|
if (/[^\x20-\x21\x23-\x7e]|\\/.test(action))
|
|
176
180
|
return fail('unsupported', 'unsafe SOAPAction');
|
|
177
|
-
const elements = (node) => {
|
|
178
|
-
check();
|
|
179
|
-
return node.children.filter((child) => child.kind === 'element');
|
|
180
|
-
};
|
|
181
181
|
const encoded = profile.encode(contract, input, application, { signal, limits });
|
|
182
182
|
if (!('bytes' in encoded))
|
|
183
183
|
return fail(encoded.kind, encoded.rule, 'input');
|
|
@@ -192,42 +192,7 @@ async function exchangeSoap(contract, input, application, options, profile) {
|
|
|
192
192
|
return fail(mapping.kind, mapping.rule);
|
|
193
193
|
faults.set(fault.name, mapping);
|
|
194
194
|
}
|
|
195
|
-
const
|
|
196
|
-
const children = elements(node);
|
|
197
|
-
if (node.attributes.some(a => !(encodedScope && a.name.namespace === SOAP && a.name.local === 'encodingStyle'
|
|
198
|
-
&& a.value === 'http://schemas.xmlsoap.org/soap/encoding/')) || node.children.some(n => n.kind === 'text' && /[^\t\r\n ]/.test(n.value)))
|
|
199
|
-
stop('protocol', 'SOAP protocol attributes or text');
|
|
200
|
-
return children;
|
|
201
|
-
};
|
|
202
|
-
const ignoredHeaders = [];
|
|
203
|
-
const bodyRoot = (root, incoming = false) => {
|
|
204
|
-
if (!same(root.name, { namespace: SOAP, local: 'Envelope' }))
|
|
205
|
-
stop('protocol', 'SOAP 1.1 Envelope required');
|
|
206
|
-
const children = shape(root, rpc);
|
|
207
|
-
if (children[0]?.name.namespace === SOAP && children[0].name.local === 'Header') {
|
|
208
|
-
if (profile.component && incoming) {
|
|
209
|
-
const assessed = assessComponentSoapHeader(children[0], check);
|
|
210
|
-
if (assessed.kind !== 'ignored-optional-headers')
|
|
211
|
-
stop(assessed.kind, assessed.rule);
|
|
212
|
-
else
|
|
213
|
-
for (const name of assessed.headers) {
|
|
214
|
-
check();
|
|
215
|
-
ignoredHeaders.push(name);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
else if (shape(children[0]).length)
|
|
219
|
-
stop('unsupported', 'incoming SOAP headers');
|
|
220
|
-
children.shift();
|
|
221
|
-
}
|
|
222
|
-
if (children.length !== 1 || !same(children[0].name, { namespace: SOAP, local: 'Body' }))
|
|
223
|
-
stop('protocol', 'one SOAP Body required');
|
|
224
|
-
const body = shape(children[0], rpc);
|
|
225
|
-
if (rpc && !body.some(n => same(n.name, { namespace: SOAP, local: 'Fault' })))
|
|
226
|
-
return children[0];
|
|
227
|
-
if (body.length !== 1)
|
|
228
|
-
stop('protocol', 'one SOAP Body payload required');
|
|
229
|
-
return body[0];
|
|
230
|
-
};
|
|
195
|
+
const { bodyRoot } = soapProtocolSelection(contract, profile.component, check, stop);
|
|
231
196
|
const wsdl = await profile.prepare(config, check);
|
|
232
197
|
check();
|
|
233
198
|
// @why node-soap's RPC writer would wrap `_xml` as a parameter. This private
|
|
@@ -255,7 +220,8 @@ async function exchangeSoap(contract, input, application, options, profile) {
|
|
|
255
220
|
request(url, data, callback, headers, _options) {
|
|
256
221
|
try {
|
|
257
222
|
check();
|
|
258
|
-
if (url !== endpoint.href || headers.SOAPAction !==
|
|
223
|
+
if (url !== endpoint.href || (soap12 ? headers.SOAPAction !== undefined || headers['Content-Type'] !== requestContentType
|
|
224
|
+
: headers.SOAPAction !== `"${action}"` || headers['Content-Type'] !== 'text/xml; charset=utf-8'))
|
|
259
225
|
stop('protocol', 'dependency dispatch mismatch');
|
|
260
226
|
if (Buffer.byteLength(data) > limits.maxBytes)
|
|
261
227
|
stop('resource-limit', 'SOAP request byte limit');
|
|
@@ -294,7 +260,8 @@ async function exchangeSoap(contract, input, application, options, profile) {
|
|
|
294
260
|
maxBodyLength: limits.maxBytes, proxy: false, httpAgent: agent, httpsAgent: agent, signal: controller.signal, timeout: config.timeoutMs });
|
|
295
261
|
}
|
|
296
262
|
}
|
|
297
|
-
const
|
|
263
|
+
const requestContentType = soap12 ? 'application/soap+xml; charset=utf-8' + (action ? `; action="${action}"` : '') : 'text/xml; charset=utf-8';
|
|
264
|
+
const client = new Client(wsdl, endpoint.href, { httpClient: new BufferedHttp(), ...(soap12 ? { forceSoap12Headers: true } : {}) });
|
|
298
265
|
// Every lookup is own and explicit; the dependency's first-port shortcut is
|
|
299
266
|
// never consulted. Source checks above bind these names to the native handle.
|
|
300
267
|
const own = (object, key) => Object.getOwnPropertyDescriptor(object, key)?.value;
|
|
@@ -304,7 +271,7 @@ async function exchangeSoap(contract, input, application, options, profile) {
|
|
|
304
271
|
if (typeof method !== 'function')
|
|
305
272
|
return fail('unsupported', 'dependency selected method unavailable');
|
|
306
273
|
check();
|
|
307
|
-
await new Promise((resolve, reject) => method(rpc ? rpcArguments : { _xml: Buffer.from(encoded.bytes).toString('utf8') }, (error) => error === complete ? resolve() : reject(error), {}, { SOAPAction: `"${action}"` }));
|
|
274
|
+
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}"` }));
|
|
308
275
|
check();
|
|
309
276
|
if (timedOut)
|
|
310
277
|
return fail('transport', 'SOAP deadline exceeded');
|
|
@@ -313,35 +280,238 @@ async function exchangeSoap(contract, input, application, options, profile) {
|
|
|
313
280
|
if (!response)
|
|
314
281
|
return fail('transport', 'incomplete HTTP response');
|
|
315
282
|
const observed = response;
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
283
|
+
const assessed = assessSoapResponse(contract, output, faults, observed.bytes, profile, { signal, limits, uri: endpoint.href,
|
|
284
|
+
responseMetadata: { status: observed.status, contentType: observed.contentType, contentEncoding: observed.encoding } });
|
|
285
|
+
if (!('admitted' in assessed))
|
|
286
|
+
return assessed;
|
|
287
|
+
if (assessed.decoded === undefined) {
|
|
288
|
+
if (!profile.component || !assessed.fault || 'name' in assessed.fault)
|
|
289
|
+
throw new TypeError('Missing decoded SOAP response');
|
|
290
|
+
return Object.freeze({ kind: 'soap-fault', admitted: false, fault: assessed.fault,
|
|
291
|
+
responseBytes: new Uint8Array(observed.bytes), status: observed.status, usage: assessed.usage,
|
|
292
|
+
...(assessed.ignoredHeaders ? { ignoredHeaders: assessed.ignoredHeaders } : {}) });
|
|
293
|
+
}
|
|
294
|
+
if (assessed.fault && !('name' in assessed.fault))
|
|
295
|
+
throw new TypeError('Decoded fault requires its declared application mapping');
|
|
296
|
+
return Object.freeze({ kind: assessed.kind, admitted: false, decoded: assessed.decoded,
|
|
297
|
+
responseBytes: new Uint8Array(observed.bytes), status: observed.status, usage: assessed.usage,
|
|
298
|
+
...(assessed.fault && 'name' in assessed.fault ? { fault: assessed.fault } : {}),
|
|
299
|
+
...(assessed.ignoredHeaders ? { ignoredHeaders: assessed.ignoredHeaders } : {}) });
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
if (budget.exhausted())
|
|
303
|
+
return fail('resource-limit', 'request-budget');
|
|
304
|
+
if (error instanceof DataOwnershipLimitError)
|
|
305
|
+
return fail('resource-limit', error.message);
|
|
306
|
+
if (signal?.aborted)
|
|
307
|
+
return fail('cancelled', 'cooperative-cancellation');
|
|
308
|
+
if (error instanceof DirectSoapSourceError)
|
|
309
|
+
return fail(error.kind, error.message);
|
|
310
|
+
if (error instanceof Stop)
|
|
311
|
+
return fail(error.kind, error.rule);
|
|
312
|
+
if (error instanceof SchemaLoadingError)
|
|
313
|
+
return fail(error.category === 'resource-limit' ? 'resource-limit' : error.category === 'invalid-schema' ? 'protocol' : 'unsupported', 'SOAP protocol XML');
|
|
314
|
+
if (error instanceof Error && 'code' in error) {
|
|
315
|
+
if (error.code === 'ERR_BAD_RESPONSE' && error.message.startsWith('maxContentLength size of '))
|
|
316
|
+
return fail('resource-limit', 'HTTP response byte limit');
|
|
317
|
+
return fail('transport', error.code === 'ERR_CANCELED' ? 'SOAP deadline exceeded' : 'HTTP exchange failed');
|
|
318
|
+
}
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
finally {
|
|
322
|
+
if (deadline)
|
|
323
|
+
clearTimeout(deadline);
|
|
324
|
+
signal?.removeEventListener('abort', abort);
|
|
325
|
+
controller?.abort();
|
|
326
|
+
agent?.destroy();
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/** Assess caller-observed response bytes through the same protocol and native decoder used by real exchanges.
|
|
330
|
+
* @param contract Authenticated selected component operation.
|
|
331
|
+
* @param inputMapping Exact operation input mapping, used only to derive response/fault mappings.
|
|
332
|
+
* @param bytes Actual observed SOAP entity bytes; no request or transport is constructed.
|
|
333
|
+
* @param options XML controls, optional actual HTTP metadata and exact response projection.
|
|
334
|
+
* @returns Native response/fault assessment or semantic failure; HTTP remains unassessed when metadata is absent.
|
|
335
|
+
* @throws TypeError For foreign native handles; RangeError for invalid explicit XML limits.
|
|
336
|
+
* @constraints This response-only helper performs no transport, endpoint authorization or request-value assessment.
|
|
337
|
+
*/
|
|
338
|
+
export function inspectComponentSoapResponse(contract, inputMapping, bytes, options = {}) {
|
|
339
|
+
const { mapping: input } = inspectComponentValueMapping(inputMapping, contract);
|
|
340
|
+
const { context } = inspectDirectOperation(contract);
|
|
341
|
+
const failure = (kind, rule) => Object.freeze({ kind, rule, phase: 'response', usage: context.budget.usage() });
|
|
342
|
+
if (options.signal?.aborted)
|
|
343
|
+
return failure('cancelled', 'cooperative-cancellation');
|
|
344
|
+
const limits = { ...DIRECT_XML_LIMITS, ...options.limits };
|
|
345
|
+
for (const [key, value] of Object.entries(limits)) {
|
|
346
|
+
if (!['maxBytes', 'maxDepth', 'maxNodes', 'maxText'].includes(key) || !Number.isSafeInteger(value) || value < 1)
|
|
347
|
+
throw new RangeError('Invalid SOAP XML limit');
|
|
348
|
+
}
|
|
349
|
+
if (options.responseProjection)
|
|
350
|
+
inspectComponentRecordProjection(options.responseProjection, contract);
|
|
351
|
+
const output = options.responseProjection ?? createComponentDirectionalMapping(contract, input, { kind: 'output' }, { signal: options.signal });
|
|
352
|
+
if (!('direction' in output))
|
|
353
|
+
return output;
|
|
354
|
+
const faults = new Map();
|
|
355
|
+
for (const fault of contract.faults) {
|
|
356
|
+
const mapping = createComponentDirectionalMapping(contract, input, { kind: 'fault', name: fault.name }, { signal: options.signal });
|
|
357
|
+
if (mapping.kind !== 'component-mapping')
|
|
358
|
+
return mapping;
|
|
359
|
+
faults.set(fault.name, mapping);
|
|
360
|
+
}
|
|
361
|
+
return assessSoapResponse(contract, output, faults, bytes, { component: true, soap11ProtocolFaults: true,
|
|
362
|
+
decode(owner, mapping, observed, controls) {
|
|
363
|
+
if (mapping.kind === 'component-record-projection')
|
|
364
|
+
return decodeComponentRecordXml(owner, mapping, observed, controls);
|
|
365
|
+
return decodeComponentXml(owner, mapping, observed, { ...controls, selectPayload(root) {
|
|
366
|
+
const selected = controls.selectPayload(root);
|
|
367
|
+
if (selected.mapping.kind !== 'component-mapping')
|
|
368
|
+
throw new TypeError('Normal component decoding requires its exact mapping');
|
|
369
|
+
return { payload: selected.payload, mapping: selected.mapping };
|
|
370
|
+
} });
|
|
371
|
+
},
|
|
372
|
+
}, { ...options, limits });
|
|
373
|
+
}
|
|
374
|
+
/** Shared response episode for offline inspection and actual HTTP exchanges. */
|
|
375
|
+
function assessSoapResponse(contract, output, faults, bytes, profile, options) {
|
|
376
|
+
const { context } = inspectDirectOperation(contract), { budget } = context;
|
|
377
|
+
const signal = options.signal, limits = options.limits;
|
|
378
|
+
const soap12 = profile.component && contract.binding.version === '1.2';
|
|
379
|
+
const SOAP = soap12 ? 'http://www.w3.org/2003/05/soap-envelope' : SOAP11;
|
|
380
|
+
const fail = (kind, rule, phase) => Object.freeze({ kind, rule, ...(phase ? { phase } : {}), usage: Object.freeze({ ...budget.usage() }) });
|
|
381
|
+
const check = () => { checkSemanticBudget(budget); if (signal?.aborted)
|
|
382
|
+
throw new Stop('cancelled', 'cooperative-cancellation'); };
|
|
383
|
+
const stop = (kind, rule) => { throw new Stop(kind, rule); };
|
|
384
|
+
try {
|
|
385
|
+
check();
|
|
386
|
+
const { elements, shape, bodyRoot, ignoredHeaders } = soapProtocolSelection(contract, profile.component, check, stop);
|
|
387
|
+
let charset;
|
|
388
|
+
const metadata = options.responseMetadata;
|
|
389
|
+
if (metadata) {
|
|
390
|
+
if ((metadata.contentEncoding ?? '') && (metadata.contentEncoding ?? '').toLowerCase() !== 'identity')
|
|
391
|
+
return fail('unsupported', 'HTTP compression');
|
|
392
|
+
let soap12Charset;
|
|
393
|
+
let validSoap12Media = false;
|
|
394
|
+
if (soap12) {
|
|
395
|
+
try {
|
|
396
|
+
const mime = new MIMEType(metadata.contentType);
|
|
397
|
+
soap12Charset = mime.params.get('charset')?.toLowerCase();
|
|
398
|
+
validSoap12Media = mime.essence === 'application/soap+xml' && (!soap12Charset || ['utf-8', 'utf-16', 'utf-16le', 'utf-16be', 'iso-8859-1'].includes(soap12Charset));
|
|
399
|
+
}
|
|
400
|
+
catch { /* Invalid media types are refused below. */ }
|
|
401
|
+
}
|
|
402
|
+
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(metadata.contentType);
|
|
403
|
+
if (!media)
|
|
404
|
+
return fail('protocol', 'SOAP response Content-Type');
|
|
405
|
+
if (metadata.status !== 200 && metadata.status !== 500 && !(soap12 && metadata.status === 400))
|
|
406
|
+
return fail('transport', 'SOAP HTTP status');
|
|
407
|
+
// Header charset is a protocol assertion, checked against actual BOM bytes.
|
|
408
|
+
charset = (media[1] ?? media[2])?.toLowerCase();
|
|
409
|
+
if (!profile.component && charset === 'iso-8859-1')
|
|
410
|
+
return fail('protocol', 'SOAP response Content-Type');
|
|
411
|
+
const bom = bytes[0] === 0xff && bytes[1] === 0xfe ? 'utf-16le'
|
|
412
|
+
: bytes[0] === 0xfe && bytes[1] === 0xff ? 'utf-16be' : 'utf-8';
|
|
413
|
+
if (!profile.component && charset && charset !== bom && !(charset === 'utf-16' && bom.startsWith('utf-16')))
|
|
414
|
+
return fail('protocol', 'SOAP charset disagrees with bytes');
|
|
415
|
+
}
|
|
331
416
|
let faultInfo;
|
|
332
|
-
|
|
417
|
+
let protocolFault;
|
|
418
|
+
const protocolFaultSignal = new Error('SOAP protocol fault assessed');
|
|
419
|
+
const decodeResponse = () => profile.decode(contract, output, bytes, { signal, limits, uri: options.uri ?? 'urn:typescript-wsdl-client:observed-soap-response',
|
|
333
420
|
...(profile.component ? { componentResponseEncoding: { charset } } : {}),
|
|
334
421
|
selectPayload: root => {
|
|
335
422
|
const payload = bodyRoot(root, true);
|
|
336
423
|
if (!same(payload.name, { namespace: SOAP, local: 'Fault' })) {
|
|
337
|
-
if (
|
|
424
|
+
if (options.responseMetadata && options.responseMetadata.status !== 200)
|
|
338
425
|
stop('protocol', 'HTTP 500 requires SOAP Fault');
|
|
339
426
|
return { payload, mapping: output };
|
|
340
427
|
}
|
|
428
|
+
if (soap12) {
|
|
429
|
+
const children = shape(payload);
|
|
430
|
+
const names = children.map(n => n.name.namespace === SOAP ? n.name.local : '');
|
|
431
|
+
if (!/^Code,Reason(?:,Node)?(?:,Role)?(?:,Detail)?$/.test(names.join(',')))
|
|
432
|
+
stop('protocol', 'SOAP 1.2 Fault children');
|
|
433
|
+
const scalar = (node, language = false) => {
|
|
434
|
+
check();
|
|
435
|
+
if (elements(node).length || node.attributes.some(a => !(language && a.name.namespace === 'http://www.w3.org/XML/1998/namespace' && a.name.local === 'lang')))
|
|
436
|
+
stop('protocol', 'SOAP 1.2 Fault scalar child');
|
|
437
|
+
return node.children.map(n => n.kind === 'text' ? n.value : '').join('');
|
|
438
|
+
};
|
|
439
|
+
const qname = (node) => {
|
|
440
|
+
const lexical = scalar(node);
|
|
441
|
+
try {
|
|
442
|
+
const name = resolveLexicalQName(lexical, node);
|
|
443
|
+
return name;
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
if (error instanceof SchemaLoadingError)
|
|
447
|
+
return stop('protocol', 'SOAP Fault code QName');
|
|
448
|
+
throw error;
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
const codes = [];
|
|
452
|
+
let codeNode;
|
|
453
|
+
let codeContainer = children[0];
|
|
454
|
+
while (codeContainer) {
|
|
455
|
+
check();
|
|
456
|
+
const nested = shape(codeContainer);
|
|
457
|
+
if (nested.length < 1 || nested.length > 2 || !same(nested[0].name, { namespace: SOAP, local: 'Value' })
|
|
458
|
+
|| nested[1] && !same(nested[1].name, { namespace: SOAP, local: 'Subcode' }))
|
|
459
|
+
stop('protocol', 'SOAP 1.2 Fault Code');
|
|
460
|
+
codeNode ??= nested[0];
|
|
461
|
+
codes.push(qname(nested[0]));
|
|
462
|
+
codeContainer = nested[1];
|
|
463
|
+
}
|
|
464
|
+
const code = codes[0];
|
|
465
|
+
if (code.namespace !== SOAP || !['VersionMismatch', 'MustUnderstand', 'DataEncodingUnknown', 'Sender', 'Receiver'].includes(code.local))
|
|
466
|
+
stop('protocol', 'SOAP 1.2 Fault code');
|
|
467
|
+
if (options.responseMetadata?.status === 400 && code.local !== 'Sender')
|
|
468
|
+
stop('protocol', 'HTTP 400 requires SOAP Sender Fault');
|
|
469
|
+
const reasonChildren = shape(children[1]);
|
|
470
|
+
if (!reasonChildren.length)
|
|
471
|
+
stop('protocol', 'SOAP 1.2 Fault Reason');
|
|
472
|
+
const reasons = reasonChildren.map(node => {
|
|
473
|
+
check();
|
|
474
|
+
if (!same(node.name, { namespace: SOAP, local: 'Text' }))
|
|
475
|
+
stop('protocol', 'SOAP 1.2 Fault Reason Text');
|
|
476
|
+
const text = scalar(node, true), lang = syntaxAttribute(node, 'lang', 'http://www.w3.org/XML/1998/namespace');
|
|
477
|
+
if (lang === undefined || !/^[A-Za-z]{1,8}(?:-[A-Za-z0-9]{1,8})*$/.test(lang))
|
|
478
|
+
stop('protocol', 'SOAP 1.2 Fault language');
|
|
479
|
+
return { lang: lang, text };
|
|
480
|
+
});
|
|
481
|
+
const uriField = (local) => {
|
|
482
|
+
const node = children.find(n => n.name.local === local);
|
|
483
|
+
if (!node)
|
|
484
|
+
return undefined;
|
|
485
|
+
const value = scalar(node).trim();
|
|
486
|
+
if (!/^[A-Za-z][A-Za-z0-9+.-]*:[^\s]*$/.test(value))
|
|
487
|
+
stop('protocol', 'SOAP 1.2 Fault URI');
|
|
488
|
+
return value;
|
|
489
|
+
};
|
|
490
|
+
const node = uriField('Node'), role = uriField('Role');
|
|
491
|
+
const metadata = { version: '1.2', code, subcodes: codes.slice(1), reasons, string: reasons[0].text,
|
|
492
|
+
...(node === undefined ? {} : { node }), ...(role === undefined ? {} : { role }) };
|
|
493
|
+
const detailNode = children.find(n => n.name.local === 'Detail');
|
|
494
|
+
// @see SOAP 1.2 section 5.4.5 permits attributes on Detail; these protocol-wrapper attributes stay in the original bytes.
|
|
495
|
+
if (detailNode?.children.some(n => n.kind === 'text' && /[^\t\r\n ]/.test(n.value)))
|
|
496
|
+
stop('protocol', 'SOAP 1.2 Fault Detail text');
|
|
497
|
+
const detail = detailNode ? elements(detailNode) : [];
|
|
498
|
+
const matches = contract.faults.filter(f => detail.some(n => same(f.root.element.name, n.name)));
|
|
499
|
+
if (matches.length) {
|
|
500
|
+
if (matches.length !== 1 || detail.length !== 1)
|
|
501
|
+
stop('protocol', 'ambiguous SOAP Fault detail');
|
|
502
|
+
faultInfo = directDataOwnership(budget).freeze({ ...metadata, name: matches[0].name, codeNode: codeNode });
|
|
503
|
+
return { payload: detail[0], mapping: faults.get(faultInfo.name) };
|
|
504
|
+
}
|
|
505
|
+
protocolFault = directDataOwnership(budget).freeze(metadata);
|
|
506
|
+
// @constraints Only protocol grammar was assessed; no fabricated application receipt is published.
|
|
507
|
+
throw protocolFaultSignal;
|
|
508
|
+
}
|
|
341
509
|
const children = shape(payload);
|
|
342
510
|
const names = children.map(n => n.name.namespace === '' ? n.name.local : '');
|
|
343
|
-
|
|
511
|
+
const hasDetail = names.at(-1) === 'detail';
|
|
512
|
+
if (!/^faultcode,faultstring(?:,faultactor)?(?:,detail)?$/.test(names.join(',')) || !hasDetail && !profile.soap11ProtocolFaults) {
|
|
344
513
|
stop('protocol', 'SOAP Fault children');
|
|
514
|
+
}
|
|
345
515
|
const text = (node) => {
|
|
346
516
|
if (node.attributes.some(a => !(node.name.local === 'faultstring' && a.name.namespace === 'http://www.w3.org/XML/1998/namespace' && a.name.local === 'lang')) || elements(node).length)
|
|
347
517
|
stop('protocol', 'SOAP Fault scalar child');
|
|
@@ -360,49 +530,101 @@ async function exchangeSoap(contract, input, application, options, profile) {
|
|
|
360
530
|
stop('protocol', 'SOAP Fault code namespace required');
|
|
361
531
|
const string = text(children[1]);
|
|
362
532
|
const actor = names[2] === 'faultactor' ? text(children[2]) : undefined;
|
|
363
|
-
const detail = shape(children.at(-1));
|
|
364
|
-
if (detail.length !== 1)
|
|
533
|
+
const detail = hasDetail ? shape(children.at(-1)) : [];
|
|
534
|
+
if (!profile.soap11ProtocolFaults && detail.length !== 1)
|
|
365
535
|
stop('protocol', 'one declared SOAP Fault detail required');
|
|
366
|
-
const matches = contract.faults.filter(f => same(f.root.element.name,
|
|
367
|
-
if (matches.length
|
|
536
|
+
const matches = contract.faults.filter(f => detail.some(node => same(f.root.element.name, node.name)));
|
|
537
|
+
if (!matches.length && profile.soap11ProtocolFaults) {
|
|
538
|
+
protocolFault = directDataOwnership(budget).freeze({ version: '1.1', code, string, ...(actor === undefined ? {} : { actor }) });
|
|
539
|
+
throw protocolFaultSignal;
|
|
540
|
+
}
|
|
541
|
+
if (detail.length !== 1 || matches.length !== 1)
|
|
368
542
|
stop('protocol', 'unknown or ambiguous SOAP Fault detail');
|
|
369
543
|
faultInfo = { name: matches[0].name, code, codeNode: children[0], string, ...(actor === undefined ? {} : { actor }) };
|
|
370
544
|
directDataOwnership(budget).freeze(faultInfo);
|
|
371
545
|
return { payload: detail[0], mapping: faults.get(faultInfo.name) };
|
|
372
546
|
} });
|
|
547
|
+
let decoded;
|
|
548
|
+
try {
|
|
549
|
+
decoded = decodeResponse();
|
|
550
|
+
}
|
|
551
|
+
catch (error) {
|
|
552
|
+
if (error !== protocolFaultSignal)
|
|
553
|
+
throw error;
|
|
554
|
+
}
|
|
555
|
+
if (protocolFault && decoded === undefined) {
|
|
556
|
+
check();
|
|
557
|
+
return Object.freeze({ kind: 'soap-fault', admitted: false, fault: protocolFault,
|
|
558
|
+
...(ignoredHeaders.length ? { ignoredHeaders: Object.freeze(ignoredHeaders) } : {}), usage: Object.freeze({ ...budget.usage() }) });
|
|
559
|
+
}
|
|
560
|
+
if (!decoded)
|
|
561
|
+
throw new TypeError('Missing decoded SOAP response');
|
|
373
562
|
if (!('validated' in decoded))
|
|
374
|
-
return fail(decoded.kind, decoded.rule
|
|
563
|
+
return profile.component ? Object.freeze({ ...decoded, phase: 'response' }) : fail(decoded.kind, decoded.rule);
|
|
375
564
|
check();
|
|
376
565
|
check();
|
|
377
566
|
return Object.freeze({ kind: faultInfo ? 'soap-fault' : 'soap-response', admitted: false, decoded,
|
|
378
|
-
|
|
567
|
+
...(faultInfo ? { fault: faultInfo } : {}),
|
|
379
568
|
...(ignoredHeaders.length ? { ignoredHeaders: Object.freeze(ignoredHeaders) } : {}), usage: Object.freeze({ ...budget.usage() }) });
|
|
380
569
|
}
|
|
381
570
|
catch (error) {
|
|
382
571
|
if (budget.exhausted())
|
|
383
|
-
return fail('resource-limit', '
|
|
572
|
+
return fail('resource-limit', 'response-budget', 'response');
|
|
384
573
|
if (error instanceof DataOwnershipLimitError)
|
|
385
|
-
return fail('resource-limit', error.message);
|
|
574
|
+
return fail('resource-limit', error.message, 'response');
|
|
386
575
|
if (signal?.aborted)
|
|
387
|
-
return fail('cancelled', 'cooperative-cancellation');
|
|
388
|
-
if (error instanceof DirectSoapSourceError)
|
|
389
|
-
return fail(error.kind, error.message);
|
|
576
|
+
return fail('cancelled', 'cooperative-cancellation', 'response');
|
|
390
577
|
if (error instanceof Stop)
|
|
391
|
-
return fail(error.kind, error.rule);
|
|
578
|
+
return fail(error.kind, error.rule, 'response');
|
|
392
579
|
if (error instanceof SchemaLoadingError)
|
|
393
|
-
return fail(error.category === 'resource-limit' ? 'resource-limit' : error.category === 'invalid-schema' ? 'protocol' : 'unsupported', 'SOAP protocol XML');
|
|
394
|
-
if (error instanceof Error && 'code' in error) {
|
|
395
|
-
if (error.code === 'ERR_BAD_RESPONSE' && error.message.startsWith('maxContentLength size of '))
|
|
396
|
-
return fail('resource-limit', 'HTTP response byte limit');
|
|
397
|
-
return fail('transport', error.code === 'ERR_CANCELED' ? 'SOAP deadline exceeded' : 'HTTP exchange failed');
|
|
398
|
-
}
|
|
580
|
+
return fail(error.category === 'resource-limit' ? 'resource-limit' : error.category === 'invalid-schema' ? 'protocol' : 'unsupported', 'SOAP protocol XML', 'response');
|
|
399
581
|
throw error;
|
|
400
582
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
583
|
+
}
|
|
584
|
+
/** Select protocol-owned nodes without relabeling observed XML or granting value authority. */
|
|
585
|
+
function soapProtocolSelection(contract, component, check, stop) {
|
|
586
|
+
const soap12 = component && contract.binding.version === '1.2';
|
|
587
|
+
const SOAP = soap12 ? 'http://www.w3.org/2003/05/soap-envelope' : SOAP11;
|
|
588
|
+
const rpc = contract.binding.style === 'rpc';
|
|
589
|
+
const elements = (node) => {
|
|
590
|
+
check();
|
|
591
|
+
return node.children.filter((child) => child.kind === 'element');
|
|
592
|
+
};
|
|
593
|
+
const shape = (node, encodedScope = false) => {
|
|
594
|
+
const children = elements(node);
|
|
595
|
+
if (node.attributes.some(a => !(encodedScope && a.name.namespace === SOAP && a.name.local === 'encodingStyle'
|
|
596
|
+
&& a.value === 'http://schemas.xmlsoap.org/soap/encoding/')) || node.children.some(n => n.kind === 'text' && /[^\t\r\n ]/.test(n.value)))
|
|
597
|
+
stop('protocol', 'SOAP protocol attributes or text');
|
|
598
|
+
return children;
|
|
599
|
+
};
|
|
600
|
+
const ignoredHeaders = [];
|
|
601
|
+
const bodyRoot = (root, incoming = false) => {
|
|
602
|
+
if (!same(root.name, { namespace: SOAP, local: 'Envelope' }))
|
|
603
|
+
stop('protocol', `SOAP ${contract.binding.version} Envelope required`);
|
|
604
|
+
const children = shape(root, rpc);
|
|
605
|
+
if (children[0]?.name.namespace === SOAP && children[0].name.local === 'Header') {
|
|
606
|
+
if (component && incoming) {
|
|
607
|
+
const assessed = assessComponentSoapHeader(children[0], check, contract.binding.version);
|
|
608
|
+
if (assessed.kind !== 'ignored-optional-headers')
|
|
609
|
+
stop(assessed.kind, assessed.rule);
|
|
610
|
+
else
|
|
611
|
+
for (const name of assessed.headers) {
|
|
612
|
+
check();
|
|
613
|
+
ignoredHeaders.push(name);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
else if (shape(children[0]).length)
|
|
617
|
+
stop('unsupported', 'incoming SOAP headers');
|
|
618
|
+
children.shift();
|
|
619
|
+
}
|
|
620
|
+
if (children.length !== 1 || !same(children[0].name, { namespace: SOAP, local: 'Body' }))
|
|
621
|
+
stop('protocol', 'one SOAP Body required');
|
|
622
|
+
const body = shape(children[0], rpc);
|
|
623
|
+
if (rpc && !body.some(n => same(n.name, { namespace: SOAP, local: 'Fault' })))
|
|
624
|
+
return children[0];
|
|
625
|
+
if (body.length !== 1)
|
|
626
|
+
stop('protocol', 'one SOAP Body payload required');
|
|
627
|
+
return body[0];
|
|
628
|
+
};
|
|
629
|
+
return { elements, shape, bodyRoot, ignoredHeaders };
|
|
408
630
|
}
|
|
@@ -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
|
}
|
|
@@ -316,6 +316,8 @@ export function checkDirectSourceCompatibility(expected, contract, selection, si
|
|
|
316
316
|
const componentVersions = Object.freeze({ ...versions, scope: 'component-soap11-utf8-graph-v1', mapping: 'component-values-v1',
|
|
317
317
|
runtimePolicy: 'explicit-http-verified-https-transport-v1' });
|
|
318
318
|
const scopedComponentVersions = Object.freeze({ ...componentVersions, scope: 'component-soap11-utf8-graph-v2', mapping: 'component-values-and-record-scope-v1' });
|
|
319
|
+
const soap12ComponentVersions = Object.freeze({ ...componentVersions, scope: 'component-soap12-document-literal-v1' });
|
|
320
|
+
const scopedSoap12ComponentVersions = Object.freeze({ ...scopedComponentVersions, scope: 'component-soap12-document-literal-record-v1' });
|
|
319
321
|
const rpcComponentVersions = Object.freeze({ ...componentVersions, scope: 'component-soap11-rpc-strings-v1', mapping: 'component-rpc-string-values-v1' });
|
|
320
322
|
const typedRpcComponentVersions = Object.freeze({ ...componentVersions, scope: 'component-soap11-rpc-typed-latin1-v1', mapping: 'component-rpc-typed-values-v1' });
|
|
321
323
|
const componentReconstructions = new WeakMap();
|
|
@@ -380,6 +382,7 @@ async function componentDescriptor(contract, selection, signal, responseProjecti
|
|
|
380
382
|
const input = make({ kind: 'input' }), output = make({ kind: 'output' });
|
|
381
383
|
const faults = contract.faults.map(fault => ({ name: fault.name, root: make({ kind: 'fault', name: fault.name }) }));
|
|
382
384
|
const compatibility = { selector: contract.selector, service: owned.service, port: owned.port,
|
|
385
|
+
...(contract.binding.version === '1.2' ? { binding: { version: '1.2', actionRequired: contract.binding.actionRequired } } : {}),
|
|
383
386
|
action: contract.binding.action, input, output, faults };
|
|
384
387
|
let projection = null;
|
|
385
388
|
if (responseProjection) {
|
|
@@ -431,7 +434,7 @@ function componentCapsule(derived, budget) {
|
|
|
431
434
|
const typedRpc = rpc && compatibility.input.parts.some(part => part.type.namespace !== 'http://www.w3.org/2001/XMLSchema' || part.type.local !== 'string');
|
|
432
435
|
if (rpc && (extended || derived.applicationGroups))
|
|
433
436
|
throw new Stop('unsupported', 'rpc-artifact-extension');
|
|
434
|
-
const value = { versions: typedRpc ? typedRpcComponentVersions : rpc ? rpcComponentVersions : extended ? scopedComponentVersions : componentVersions, resources, compatibility,
|
|
437
|
+
const value = { versions: compatibility.binding?.version === '1.2' ? extended ? scopedSoap12ComponentVersions : soap12ComponentVersions : typedRpc ? typedRpcComponentVersions : rpc ? rpcComponentVersions : extended ? scopedComponentVersions : componentVersions, resources, compatibility,
|
|
435
438
|
...(extended ? { libraries, projection } : {}), ...(derived.applicationGroups ? { applicationGroups: derived.applicationGroups, applicationGroupIdentity: derived.applicationGroupIdentity } : {}) };
|
|
436
439
|
return { ...value, fingerprint: hash(json(value, budget)) };
|
|
437
440
|
}
|
|
@@ -493,7 +496,8 @@ function readComponent(bytes, budget, signal) {
|
|
|
493
496
|
const rpc = typedRpc || !!parsed.versions && typeof parsed.versions === 'object' && parsed.versions.scope === rpcComponentVersions.scope;
|
|
494
497
|
if (rpc && (extended || grouped))
|
|
495
498
|
reject('rpc-artifact-extension');
|
|
496
|
-
const
|
|
499
|
+
const soap12 = !!parsed.versions && typeof parsed.versions === 'object' && ['component-soap12-document-literal-v1', 'component-soap12-document-literal-record-v1'].includes(parsed.versions.scope);
|
|
500
|
+
const expectedVersions = soap12 ? extended ? scopedSoap12ComponentVersions : soap12ComponentVersions : typedRpc ? typedRpcComponentVersions : rpc ? rpcComponentVersions : extended ? scopedComponentVersions : componentVersions;
|
|
497
501
|
const actual = record(parsed.versions, Object.keys(expectedVersions));
|
|
498
502
|
for (const key of Object.keys(expectedVersions))
|
|
499
503
|
if (actual[key] !== expectedVersions[key])
|
|
@@ -564,7 +568,12 @@ function readComponent(bytes, budget, signal) {
|
|
|
564
568
|
if (!parsed.libraries.length && parsed.projection === null)
|
|
565
569
|
reject('empty-component-extension');
|
|
566
570
|
}
|
|
567
|
-
const c = record(parsed.compatibility, ['selector', 'service', 'port', 'action', 'input', 'output', 'faults']);
|
|
571
|
+
const c = record(parsed.compatibility, ['selector', 'service', 'port', 'action', 'input', 'output', 'faults', ...(soap12 ? ['binding'] : [])]);
|
|
572
|
+
if (soap12) {
|
|
573
|
+
const binding = record(c.binding, ['version', 'actionRequired']);
|
|
574
|
+
if (binding.version !== '1.2' || typeof binding.actionRequired !== 'boolean')
|
|
575
|
+
reject('soap12-binding');
|
|
576
|
+
}
|
|
568
577
|
selector(c.selector);
|
|
569
578
|
name(c.service);
|
|
570
579
|
ncname(c.port);
|