@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.
Files changed (34) hide show
  1. package/README.md +8 -0
  2. package/dist/checkedPipeline.d.ts +73 -2
  3. package/dist/checkedPipeline.js +118 -8
  4. package/dist/checkedPlanning.d.ts +5 -2
  5. package/dist/checkedPlanning.js +30 -5
  6. package/dist/checkedResponseInspection.d.ts +100 -0
  7. package/dist/checkedResponseInspection.js +499 -0
  8. package/dist/cli.js +55 -1
  9. package/dist/client/checkedDirectRuntime.d.ts +8 -0
  10. package/dist/client/checkedDirectRuntime.js +22 -14
  11. package/dist/client/directClientRuntime.d.ts +10 -3
  12. package/dist/client/directClientRuntime.js +14 -8
  13. package/dist/client/directClientVersion.d.ts +2 -0
  14. package/dist/client/directClientVersion.js +2 -0
  15. package/dist/client/generateDirectClient.js +7 -7
  16. package/dist/compiler/componentRecordProjection.d.ts +1 -0
  17. package/dist/compiler/componentRecordProjection.js +15 -4
  18. package/dist/compiler/directCodec.js +3 -11
  19. package/dist/compiler/directSoap.d.ts +50 -5
  20. package/dist/compiler/directSoap.js +204 -98
  21. package/dist/compiler/sourceCorrections.d.ts +118 -0
  22. package/dist/compiler/sourceCorrections.js +794 -0
  23. package/dist/gateway/generateCheckedGateway.js +9 -10
  24. package/dist/index.d.ts +3 -1
  25. package/dist/index.js +2 -1
  26. package/dist/util/readBoundedFile.d.ts +9 -0
  27. package/dist/util/readBoundedFile.js +36 -0
  28. package/dist/util/xmlBytes.d.ts +9 -0
  29. package/dist/util/xmlBytes.js +24 -0
  30. package/docs/api-reference.md +28 -1
  31. package/docs/checked-gateway.md +44 -0
  32. package/docs/cli-reference.md +16 -1
  33. package/docs/migration.md +10 -0
  34. 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 shape = (node, encodedScope = false) => {
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
- if (observed.encoding && observed.encoding.toLowerCase() !== 'identity')
324
- return fail('unsupported', 'HTTP compression');
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));
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
- catch { /* Invalid media types are refused below. */ }
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 1.2 protocol fault assessed');
351
- const decodeResponse = () => profile.decode(contract, output, observed.bytes, { signal, limits, uri: endpoint.href,
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 (observed.status !== 200)
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 (observed.status === 400 && code.local !== 'Sender')
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
- if (names.join(',') !== 'faultcode,faultstring,detail' && names.join(',') !== 'faultcode,faultstring,faultactor,detail')
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, detail[0].name));
467
- if (matches.length !== 1)
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, 'response');
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
- responseBytes: new Uint8Array(observed.bytes), status: observed.status, ...(faultInfo ? { fault: faultInfo } : {}),
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', 'request-budget');
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
- finally {
518
- if (deadline)
519
- clearTimeout(deadline);
520
- signal?.removeEventListener('abort', abort);
521
- controller?.abort();
522
- agent?.destroy();
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 {};