@techspokes/typescript-wsdl-client 1.3.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 +8 -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 +8 -0
- package/dist/client/checkedDirectRuntime.js +22 -14
- package/dist/client/directClientRuntime.d.ts +10 -3
- package/dist/client/directClientRuntime.js +14 -8
- package/dist/client/directClientVersion.d.ts +2 -0
- package/dist/client/directClientVersion.js +2 -0
- package/dist/client/generateDirectClient.js +7 -7
- package/dist/compiler/componentRecordProjection.d.ts +1 -0
- package/dist/compiler/componentRecordProjection.js +15 -4
- package/dist/compiler/directCodec.js +3 -11
- package/dist/compiler/directSoap.d.ts +50 -5
- package/dist/compiler/directSoap.js +204 -98
- package/dist/compiler/sourceCorrections.d.ts +118 -0
- package/dist/compiler/sourceCorrections.js +794 -0
- package/dist/gateway/generateCheckedGateway.js +9 -10
- 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 +44 -0
- package/docs/cli-reference.md +16 -1
- package/docs/migration.md +10 -0
- package/package.json +1 -1
|
@@ -58,12 +58,12 @@ export async function exchangeDirectSoap(contract, inputMapping, application, op
|
|
|
58
58
|
throw new TypeError('Component fault on direct profile');
|
|
59
59
|
return result;
|
|
60
60
|
}
|
|
61
|
-
export async function exchangeComponentSoap(contract, inputMapping, application, options, responseProjection) {
|
|
61
|
+
export async function exchangeComponentSoap(contract, inputMapping, application, options, responseProjection, soap11ProtocolFaults = true) {
|
|
62
62
|
const { mapping: input } = inspectComponentValueMapping(inputMapping, contract);
|
|
63
63
|
if (responseProjection !== undefined) {
|
|
64
64
|
inspectComponentRecordProjection(responseProjection, contract);
|
|
65
65
|
return exchangeSoap(contract, input, application, options, {
|
|
66
|
-
component: true,
|
|
66
|
+
component: true, soap11ProtocolFaults,
|
|
67
67
|
encode(owner, mapping, value, controls) {
|
|
68
68
|
if (mapping.kind !== 'component-mapping')
|
|
69
69
|
throw new TypeError('SOAP input requires the native input mapping');
|
|
@@ -86,14 +86,13 @@ export async function exchangeComponentSoap(contract, inputMapping, application,
|
|
|
86
86
|
});
|
|
87
87
|
}
|
|
88
88
|
return exchangeSoap(contract, input, application, options, {
|
|
89
|
-
component: true, encode: encodeComponentXml, decode: decodeComponentXml,
|
|
89
|
+
component: true, soap11ProtocolFaults, encode: encodeComponentXml, decode: decodeComponentXml,
|
|
90
90
|
create: (owner, direction, controls) => createComponentDirectionalMapping(owner, input, direction, controls),
|
|
91
91
|
prepare: (selection, check) => prepareComponentSoapWsdl(contract, selection, check),
|
|
92
92
|
});
|
|
93
93
|
}
|
|
94
94
|
async function exchangeSoap(contract, input, application, options, profile) {
|
|
95
95
|
const soap12 = profile.component && contract.binding.version === '1.2';
|
|
96
|
-
const SOAP = soap12 ? 'http://www.w3.org/2003/05/soap-envelope' : SOAP11;
|
|
97
96
|
const { context } = inspectDirectOperation(contract);
|
|
98
97
|
if (input.direction.kind !== 'input')
|
|
99
98
|
throw new TypeError('SOAP request requires the exact input mapping');
|
|
@@ -179,10 +178,6 @@ async function exchangeSoap(contract, input, application, options, profile) {
|
|
|
179
178
|
const rpc = contract.binding.style === 'rpc';
|
|
180
179
|
if (/[^\x20-\x21\x23-\x7e]|\\/.test(action))
|
|
181
180
|
return fail('unsupported', 'unsafe SOAPAction');
|
|
182
|
-
const elements = (node) => {
|
|
183
|
-
check();
|
|
184
|
-
return node.children.filter((child) => child.kind === 'element');
|
|
185
|
-
};
|
|
186
181
|
const encoded = profile.encode(contract, input, application, { signal, limits });
|
|
187
182
|
if (!('bytes' in encoded))
|
|
188
183
|
return fail(encoded.kind, encoded.rule, 'input');
|
|
@@ -197,42 +192,7 @@ async function exchangeSoap(contract, input, application, options, profile) {
|
|
|
197
192
|
return fail(mapping.kind, mapping.rule);
|
|
198
193
|
faults.set(fault.name, mapping);
|
|
199
194
|
}
|
|
200
|
-
const
|
|
201
|
-
const children = elements(node);
|
|
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)))
|
|
204
|
-
stop('protocol', 'SOAP protocol attributes or text');
|
|
205
|
-
return children;
|
|
206
|
-
};
|
|
207
|
-
const ignoredHeaders = [];
|
|
208
|
-
const bodyRoot = (root, incoming = false) => {
|
|
209
|
-
if (!same(root.name, { namespace: SOAP, local: 'Envelope' }))
|
|
210
|
-
stop('protocol', `SOAP ${contract.binding.version} Envelope required`);
|
|
211
|
-
const children = shape(root, rpc);
|
|
212
|
-
if (children[0]?.name.namespace === SOAP && children[0].name.local === 'Header') {
|
|
213
|
-
if (profile.component && incoming) {
|
|
214
|
-
const assessed = assessComponentSoapHeader(children[0], check, contract.binding.version);
|
|
215
|
-
if (assessed.kind !== 'ignored-optional-headers')
|
|
216
|
-
stop(assessed.kind, assessed.rule);
|
|
217
|
-
else
|
|
218
|
-
for (const name of assessed.headers) {
|
|
219
|
-
check();
|
|
220
|
-
ignoredHeaders.push(name);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
else if (shape(children[0]).length)
|
|
224
|
-
stop('unsupported', 'incoming SOAP headers');
|
|
225
|
-
children.shift();
|
|
226
|
-
}
|
|
227
|
-
if (children.length !== 1 || !same(children[0].name, { namespace: SOAP, local: 'Body' }))
|
|
228
|
-
stop('protocol', 'one SOAP Body required');
|
|
229
|
-
const body = shape(children[0], rpc);
|
|
230
|
-
if (rpc && !body.some(n => same(n.name, { namespace: SOAP, local: 'Fault' })))
|
|
231
|
-
return children[0];
|
|
232
|
-
if (body.length !== 1)
|
|
233
|
-
stop('protocol', 'one SOAP Body payload required');
|
|
234
|
-
return body[0];
|
|
235
|
-
};
|
|
195
|
+
const { bodyRoot } = soapProtocolSelection(contract, profile.component, check, stop);
|
|
236
196
|
const wsdl = await profile.prepare(config, check);
|
|
237
197
|
check();
|
|
238
198
|
// @why node-soap's RPC writer would wrap `_xml` as a parameter. This private
|
|
@@ -320,40 +280,148 @@ async function exchangeSoap(contract, input, application, options, profile) {
|
|
|
320
280
|
if (!response)
|
|
321
281
|
return fail('transport', 'incomplete HTTP response');
|
|
322
282
|
const observed = response;
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
if (
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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. */ }
|
|
332
401
|
}
|
|
333
|
-
|
|
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');
|
|
334
415
|
}
|
|
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);
|
|
336
|
-
if (!media)
|
|
337
|
-
return fail('protocol', 'SOAP response Content-Type');
|
|
338
|
-
if (observed.status !== 200 && observed.status !== 500 && !(soap12 && observed.status === 400))
|
|
339
|
-
return fail('transport', 'SOAP HTTP status');
|
|
340
|
-
// Header charset is a protocol assertion, checked against actual BOM bytes.
|
|
341
|
-
const charset = (media[1] ?? media[2])?.toLowerCase();
|
|
342
|
-
if (!profile.component && charset === 'iso-8859-1')
|
|
343
|
-
return fail('protocol', 'SOAP response Content-Type');
|
|
344
|
-
const bom = observed.bytes[0] === 0xff && observed.bytes[1] === 0xfe ? 'utf-16le'
|
|
345
|
-
: observed.bytes[0] === 0xfe && observed.bytes[1] === 0xff ? 'utf-16be' : 'utf-8';
|
|
346
|
-
if (!profile.component && charset && charset !== bom && !(charset === 'utf-16' && bom.startsWith('utf-16')))
|
|
347
|
-
return fail('protocol', 'SOAP charset disagrees with bytes');
|
|
348
416
|
let faultInfo;
|
|
349
417
|
let protocolFault;
|
|
350
|
-
const protocolFaultSignal = new Error('SOAP
|
|
351
|
-
const decodeResponse = () => profile.decode(contract, output,
|
|
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',
|
|
352
420
|
...(profile.component ? { componentResponseEncoding: { charset } } : {}),
|
|
353
421
|
selectPayload: root => {
|
|
354
422
|
const payload = bodyRoot(root, true);
|
|
355
423
|
if (!same(payload.name, { namespace: SOAP, local: 'Fault' })) {
|
|
356
|
-
if (
|
|
424
|
+
if (options.responseMetadata && options.responseMetadata.status !== 200)
|
|
357
425
|
stop('protocol', 'HTTP 500 requires SOAP Fault');
|
|
358
426
|
return { payload, mapping: output };
|
|
359
427
|
}
|
|
@@ -396,7 +464,7 @@ async function exchangeSoap(contract, input, application, options, profile) {
|
|
|
396
464
|
const code = codes[0];
|
|
397
465
|
if (code.namespace !== SOAP || !['VersionMismatch', 'MustUnderstand', 'DataEncodingUnknown', 'Sender', 'Receiver'].includes(code.local))
|
|
398
466
|
stop('protocol', 'SOAP 1.2 Fault code');
|
|
399
|
-
if (
|
|
467
|
+
if (options.responseMetadata?.status === 400 && code.local !== 'Sender')
|
|
400
468
|
stop('protocol', 'HTTP 400 requires SOAP Sender Fault');
|
|
401
469
|
const reasonChildren = shape(children[1]);
|
|
402
470
|
if (!reasonChildren.length)
|
|
@@ -440,8 +508,10 @@ async function exchangeSoap(contract, input, application, options, profile) {
|
|
|
440
508
|
}
|
|
441
509
|
const children = shape(payload);
|
|
442
510
|
const names = children.map(n => n.name.namespace === '' ? n.name.local : '');
|
|
443
|
-
|
|
511
|
+
const hasDetail = names.at(-1) === 'detail';
|
|
512
|
+
if (!/^faultcode,faultstring(?:,faultactor)?(?:,detail)?$/.test(names.join(',')) || !hasDetail && !profile.soap11ProtocolFaults) {
|
|
444
513
|
stop('protocol', 'SOAP Fault children');
|
|
514
|
+
}
|
|
445
515
|
const text = (node) => {
|
|
446
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)
|
|
447
517
|
stop('protocol', 'SOAP Fault scalar child');
|
|
@@ -460,11 +530,15 @@ async function exchangeSoap(contract, input, application, options, profile) {
|
|
|
460
530
|
stop('protocol', 'SOAP Fault code namespace required');
|
|
461
531
|
const string = text(children[1]);
|
|
462
532
|
const actor = names[2] === 'faultactor' ? text(children[2]) : undefined;
|
|
463
|
-
const detail = shape(children.at(-1));
|
|
464
|
-
if (detail.length !== 1)
|
|
533
|
+
const detail = hasDetail ? shape(children.at(-1)) : [];
|
|
534
|
+
if (!profile.soap11ProtocolFaults && detail.length !== 1)
|
|
465
535
|
stop('protocol', 'one declared SOAP Fault detail required');
|
|
466
|
-
const matches = contract.faults.filter(f => same(f.root.element.name,
|
|
467
|
-
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)
|
|
468
542
|
stop('protocol', 'unknown or ambiguous SOAP Fault detail');
|
|
469
543
|
faultInfo = { name: matches[0].name, code, codeNode: children[0], string, ...(actor === undefined ? {} : { actor }) };
|
|
470
544
|
directDataOwnership(budget).freeze(faultInfo);
|
|
@@ -481,44 +555,76 @@ async function exchangeSoap(contract, input, application, options, profile) {
|
|
|
481
555
|
if (protocolFault && decoded === undefined) {
|
|
482
556
|
check();
|
|
483
557
|
return Object.freeze({ kind: 'soap-fault', admitted: false, fault: protocolFault,
|
|
484
|
-
responseBytes: new Uint8Array(observed.bytes), status: observed.status,
|
|
485
558
|
...(ignoredHeaders.length ? { ignoredHeaders: Object.freeze(ignoredHeaders) } : {}), usage: Object.freeze({ ...budget.usage() }) });
|
|
486
559
|
}
|
|
487
560
|
if (!decoded)
|
|
488
561
|
throw new TypeError('Missing decoded SOAP response');
|
|
489
562
|
if (!('validated' in decoded))
|
|
490
|
-
return fail(decoded.kind, decoded.rule
|
|
563
|
+
return profile.component ? Object.freeze({ ...decoded, phase: 'response' }) : fail(decoded.kind, decoded.rule);
|
|
491
564
|
check();
|
|
492
565
|
check();
|
|
493
566
|
return Object.freeze({ kind: faultInfo ? 'soap-fault' : 'soap-response', admitted: false, decoded,
|
|
494
|
-
|
|
567
|
+
...(faultInfo ? { fault: faultInfo } : {}),
|
|
495
568
|
...(ignoredHeaders.length ? { ignoredHeaders: Object.freeze(ignoredHeaders) } : {}), usage: Object.freeze({ ...budget.usage() }) });
|
|
496
569
|
}
|
|
497
570
|
catch (error) {
|
|
498
571
|
if (budget.exhausted())
|
|
499
|
-
return fail('resource-limit', '
|
|
572
|
+
return fail('resource-limit', 'response-budget', 'response');
|
|
500
573
|
if (error instanceof DataOwnershipLimitError)
|
|
501
|
-
return fail('resource-limit', error.message);
|
|
574
|
+
return fail('resource-limit', error.message, 'response');
|
|
502
575
|
if (signal?.aborted)
|
|
503
|
-
return fail('cancelled', 'cooperative-cancellation');
|
|
504
|
-
if (error instanceof DirectSoapSourceError)
|
|
505
|
-
return fail(error.kind, error.message);
|
|
576
|
+
return fail('cancelled', 'cooperative-cancellation', 'response');
|
|
506
577
|
if (error instanceof Stop)
|
|
507
|
-
return fail(error.kind, error.rule);
|
|
578
|
+
return fail(error.kind, error.rule, 'response');
|
|
508
579
|
if (error instanceof SchemaLoadingError)
|
|
509
|
-
return fail(error.category === 'resource-limit' ? 'resource-limit' : error.category === 'invalid-schema' ? 'protocol' : 'unsupported', 'SOAP protocol XML');
|
|
510
|
-
if (error instanceof Error && 'code' in error) {
|
|
511
|
-
if (error.code === 'ERR_BAD_RESPONSE' && error.message.startsWith('maxContentLength size of '))
|
|
512
|
-
return fail('resource-limit', 'HTTP response byte limit');
|
|
513
|
-
return fail('transport', error.code === 'ERR_CANCELED' ? 'SOAP deadline exceeded' : 'HTTP exchange failed');
|
|
514
|
-
}
|
|
580
|
+
return fail(error.category === 'resource-limit' ? 'resource-limit' : error.category === 'invalid-schema' ? 'protocol' : 'unsupported', 'SOAP protocol XML', 'response');
|
|
515
581
|
throw error;
|
|
516
582
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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 };
|
|
524
630
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { SchemaInput } from "../loader/schemaInput.js";
|
|
2
|
+
import type { OfflineSchemaResource } from "../loader/schemaResources.js";
|
|
3
|
+
import type { GraphNode, NodeId } from "./canonicalGraph.js";
|
|
4
|
+
export type SourceCorrectionSelector = Readonly<{
|
|
5
|
+
id: NodeId;
|
|
6
|
+
uri: string;
|
|
7
|
+
digest: string;
|
|
8
|
+
}>;
|
|
9
|
+
export type SourceCorrectionInventoryNode = Readonly<{
|
|
10
|
+
id: NodeId;
|
|
11
|
+
kind: GraphNode["kind"];
|
|
12
|
+
role: string;
|
|
13
|
+
namespace: string;
|
|
14
|
+
name?: Readonly<{
|
|
15
|
+
namespace: string;
|
|
16
|
+
local: string;
|
|
17
|
+
}>;
|
|
18
|
+
syntaxName: Readonly<{
|
|
19
|
+
namespace: string;
|
|
20
|
+
local: string;
|
|
21
|
+
}>;
|
|
22
|
+
uri: string;
|
|
23
|
+
digest: string;
|
|
24
|
+
path: string;
|
|
25
|
+
start: number;
|
|
26
|
+
end: number;
|
|
27
|
+
attributes: readonly Readonly<{
|
|
28
|
+
name: string;
|
|
29
|
+
namespace: string;
|
|
30
|
+
lexicalName: string;
|
|
31
|
+
value: string;
|
|
32
|
+
}>[];
|
|
33
|
+
occurs?: Readonly<{
|
|
34
|
+
min: string;
|
|
35
|
+
max: string | "unbounded";
|
|
36
|
+
}>;
|
|
37
|
+
wildcard?: Readonly<{
|
|
38
|
+
namespace: string;
|
|
39
|
+
processContents: string;
|
|
40
|
+
}>;
|
|
41
|
+
children?: readonly NodeId[];
|
|
42
|
+
parent?: NodeId;
|
|
43
|
+
chameleon: boolean;
|
|
44
|
+
ambiguousContext: boolean;
|
|
45
|
+
}>;
|
|
46
|
+
export type SourceCorrectionInventory = Readonly<{
|
|
47
|
+
version: 1;
|
|
48
|
+
resources: readonly Readonly<{
|
|
49
|
+
uri: string;
|
|
50
|
+
digest: string;
|
|
51
|
+
byteLength: number;
|
|
52
|
+
}>[];
|
|
53
|
+
nodes: readonly SourceCorrectionInventoryNode[];
|
|
54
|
+
}>;
|
|
55
|
+
type OccursCorrection = Readonly<{
|
|
56
|
+
kind: "set-occurs";
|
|
57
|
+
target: SourceCorrectionSelector;
|
|
58
|
+
min: string;
|
|
59
|
+
max: string | "unbounded";
|
|
60
|
+
}>;
|
|
61
|
+
type WildcardCorrection = Readonly<{
|
|
62
|
+
kind: "set-wildcard-namespaces";
|
|
63
|
+
target: SourceCorrectionSelector;
|
|
64
|
+
namespaces: readonly string[];
|
|
65
|
+
}>;
|
|
66
|
+
type InsertPosition = "append" | Readonly<{
|
|
67
|
+
kind: "before" | "after";
|
|
68
|
+
target: SourceCorrectionSelector;
|
|
69
|
+
}>;
|
|
70
|
+
type InsertElement = Readonly<{
|
|
71
|
+
kind: "declaration";
|
|
72
|
+
name: string;
|
|
73
|
+
type: Readonly<{
|
|
74
|
+
namespace: string;
|
|
75
|
+
local: string;
|
|
76
|
+
}>;
|
|
77
|
+
min: string;
|
|
78
|
+
max: string | "unbounded";
|
|
79
|
+
} | {
|
|
80
|
+
kind: "reference";
|
|
81
|
+
target: SourceCorrectionSelector;
|
|
82
|
+
min: string;
|
|
83
|
+
max: string | "unbounded";
|
|
84
|
+
}>;
|
|
85
|
+
type InsertCorrection = Readonly<{
|
|
86
|
+
kind: "insert-element";
|
|
87
|
+
target: SourceCorrectionSelector;
|
|
88
|
+
position: InsertPosition;
|
|
89
|
+
element: InsertElement;
|
|
90
|
+
}>;
|
|
91
|
+
type Correction = OccursCorrection | WildcardCorrection | InsertCorrection;
|
|
92
|
+
export type SourceCorrectionProvenance = Readonly<{
|
|
93
|
+
version: 1;
|
|
94
|
+
canonicalRecipeDigest: string;
|
|
95
|
+
resources: readonly Readonly<{
|
|
96
|
+
uri: string;
|
|
97
|
+
originalDigest: string;
|
|
98
|
+
effectiveDigest: string;
|
|
99
|
+
changed: boolean;
|
|
100
|
+
}>[];
|
|
101
|
+
corrections: readonly Readonly<{
|
|
102
|
+
kind: Correction["kind"];
|
|
103
|
+
target: SourceCorrectionSelector;
|
|
104
|
+
}>[];
|
|
105
|
+
}>;
|
|
106
|
+
export type PreparedCorrectedSourceInputs = Readonly<{
|
|
107
|
+
inventory: SourceCorrectionInventory;
|
|
108
|
+
resources: ReadonlyMap<string, OfflineSchemaResource>;
|
|
109
|
+
provenance: SourceCorrectionProvenance;
|
|
110
|
+
}>;
|
|
111
|
+
export declare class SourceCorrectionError extends Error {
|
|
112
|
+
readonly category: "invalid-recipe" | "stale-source" | "ambiguous-target" | "unsupported-target" | "resource-limit" | "cancelled";
|
|
113
|
+
constructor(category: "invalid-recipe" | "stale-source" | "ambiguous-target" | "unsupported-target" | "resource-limit" | "cancelled", message: string);
|
|
114
|
+
}
|
|
115
|
+
export declare function inspectSourceCorrectionInventory(inputs: readonly SchemaInput[], signal?: AbortSignal): SourceCorrectionInventory;
|
|
116
|
+
/** Validate and apply a versioned correction recipe to an authenticated source union. */
|
|
117
|
+
export declare function prepareCorrectedSourceInputs(inputs: readonly SchemaInput[], recipeValue: unknown, signal?: AbortSignal): PreparedCorrectedSourceInputs;
|
|
118
|
+
export {};
|