@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.
Files changed (43) hide show
  1. package/README.md +13 -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 +19 -1
  10. package/dist/client/checkedDirectRuntime.js +27 -2
  11. package/dist/client/directClientRuntime.d.ts +25 -1
  12. package/dist/client/directClientRuntime.js +23 -5
  13. package/dist/client/directClientVersion.d.ts +4 -0
  14. package/dist/client/directClientVersion.js +4 -0
  15. package/dist/client/generateDirectClient.js +14 -9
  16. package/dist/compiler/componentRecordProjection.d.ts +1 -0
  17. package/dist/compiler/componentRecordProjection.js +15 -4
  18. package/dist/compiler/directCodec.js +4 -12
  19. package/dist/compiler/directOperation.d.ts +2 -1
  20. package/dist/compiler/directOperation.js +27 -11
  21. package/dist/compiler/directSoap.d.ts +87 -12
  22. package/dist/compiler/directSoap.js +316 -94
  23. package/dist/compiler/directSoapSource.js +3 -2
  24. package/dist/compiler/directSourceCapsule.js +12 -3
  25. package/dist/compiler/soapHeaders.d.ts +10 -4
  26. package/dist/compiler/soapHeaders.js +28 -9
  27. package/dist/compiler/sourceCorrections.d.ts +118 -0
  28. package/dist/compiler/sourceCorrections.js +794 -0
  29. package/dist/gateway/generateCheckedGateway.js +27 -8
  30. package/dist/index.d.ts +3 -1
  31. package/dist/index.js +2 -1
  32. package/dist/util/readBoundedFile.d.ts +9 -0
  33. package/dist/util/readBoundedFile.js +36 -0
  34. package/dist/util/xmlBytes.d.ts +9 -0
  35. package/dist/util/xmlBytes.js +24 -0
  36. package/docs/api-reference.md +28 -1
  37. package/docs/checked-gateway.md +52 -0
  38. package/docs/checked-generation.md +4 -4
  39. package/docs/cli-reference.md +16 -1
  40. package/docs/migration.md +20 -0
  41. package/docs/start-here.md +1 -1
  42. package/docs/supported-patterns.md +6 -0
  43. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import { types } from 'node:util';
2
- import { directClientRuntime, componentClientRuntime } from './directClientRuntime.js';
3
- import { DIRECT_CLIENT_RUNTIME_VERSION, DIRECT_CLIENT_RUNTIME_REPAIR, COMPONENT_CLIENT_RUNTIME_VERSION, COMPONENT_CLIENT_RUNTIME_REPAIR } from './directClientVersion.js';
2
+ import { directClientRuntime, componentClientRuntime, componentSoap11ClientRuntime, componentSoap12ClientRuntime } from './directClientRuntime.js';
3
+ import { DIRECT_CLIENT_RUNTIME_VERSION, DIRECT_CLIENT_RUNTIME_REPAIR, COMPONENT_CLIENT_RUNTIME_VERSION, COMPONENT_CLIENT_RUNTIME_REPAIR, COMPONENT_SOAP11_CLIENT_RUNTIME_VERSION, COMPONENT_SOAP12_CLIENT_RUNTIME_VERSION } from './directClientVersion.js';
4
4
  function dataOptions(options, allowed) {
5
5
  return !!options && typeof options === 'object' && !types.isProxy(options)
6
6
  && [null, Object.prototype].includes(Object.getPrototypeOf(options))
@@ -41,3 +41,28 @@ export const checkedComponentRuntimeV1 = Object.freeze({
41
41
  return componentClientRuntime.execute(expectedBase64, input, options);
42
42
  },
43
43
  });
44
+ function checkedProtocolRuntime(runtime) {
45
+ return Object.freeze({
46
+ version: runtime.version,
47
+ async execute(expectedBase64, input, options, version) {
48
+ if (version !== runtime.version)
49
+ return Object.freeze({ kind: 'incompatible-artifact',
50
+ rule: 'generated-runtime-version', repair: COMPONENT_CLIENT_RUNTIME_REPAIR, usage: Object.freeze({ nodes: 0, work: 0 }) });
51
+ if (!dataOptions(options, ['localSource', 'fileRoots', 'capsule', 'soap', 'sourceResources', 'sourceOrigins', 'sourceUri', 'namespaceLocations', 'standardLibraries', 'recordSources'])) {
52
+ return Object.freeze({ kind: 'unsupported', rule: 'data-only checked component options required', usage: Object.freeze({ nodes: 0, work: 0 }) });
53
+ }
54
+ return runtime.execute(expectedBase64, input, options);
55
+ },
56
+ });
57
+ }
58
+ /** Additive SOAP 1.2 binding permits protocol faults without declared application detail.
59
+ * @since 1.3.0
60
+ * @remarks Source reconstruction, endpoint restrictions and native validation use the shared component runtime.
61
+ * @returns Native component values, SOAP 1.2 protocol faults or explicit refusal; no full-profile admission.
62
+ */
63
+ export const checkedComponentRuntimeV2 = checkedProtocolRuntime(componentSoap12ClientRuntime);
64
+ /** SOAP 1.1 binding for generated clients that distinguish protocol faults from declared detail values.
65
+ * @since 1.4.0
66
+ * @returns Native-validated values, SOAP 1.1 protocol faults with their original bytes, or explicit refusal.
67
+ */
68
+ export const checkedComponentRuntimeV3 = checkedProtocolRuntime(componentSoap11ClientRuntime);
@@ -1,6 +1,6 @@
1
1
  import type { SemanticBudgetLimits, ResolverUsage } from '../compiler/semanticBudget.js';
2
2
  import type { DirectCapsuleFailure } from '../compiler/directSourceCapsule.js';
3
- import type { DirectSoapOptions, ComponentSoapOptions, DirectSoapFailure } from '../compiler/directSoap.js';
3
+ import type { DirectSoapOptions, ComponentSoapOptions, DirectSoapFailure, ComponentProtocolFault, DeclaredSoapFault, Soap11FaultMetadata, Soap12FaultMetadata } from '../compiler/directSoap.js';
4
4
  import type { DirectOperationSelector } from '../compiler/directOperation.js';
5
5
  import type { DirectApplicationValue } from '../compiler/directValues.js';
6
6
  import { DIRECT_CLIENT_RUNTIME_VERSION } from './directClientVersion.js';
@@ -65,6 +65,16 @@ export interface DirectClientRuntime {
65
65
  readonly version: typeof DIRECT_CLIENT_RUNTIME_VERSION;
66
66
  execute(expectedBase64: string, input: unknown, options: DirectClientOptions): Promise<DirectClientResult>;
67
67
  }
68
+ /** Component-only protocol faults retain raw bytes without claiming an application value.
69
+ * @since 1.3.0
70
+ */
71
+ export type ComponentClientResult<F extends Soap11FaultMetadata | Soap12FaultMetadata = Soap12FaultMetadata> = Exclude<DirectClientResult, {
72
+ kind: 'soap-fault';
73
+ }> | ComponentProtocolFault<F> | Readonly<Omit<Extract<DirectClientResult, {
74
+ kind: 'soap-fault';
75
+ }>, 'fault'> & {
76
+ fault: Omit<DeclaredSoapFault, 'codeNode'>;
77
+ }>;
68
78
  /** The generated literal is the expected artifact; caller bytes never choose its static contract. */
69
79
  export declare const directClientRuntime: DirectClientRuntime;
70
80
  /** Same artifact/execution chain, with explicit physical dependency snapshots and component-profile reconstruction. */
@@ -72,3 +82,17 @@ export declare const componentClientRuntime: Readonly<{
72
82
  version: "checked-component-runtime-v2";
73
83
  execute: (expectedBase64: string, input: unknown, options: ComponentClientOptions) => Promise<DirectClientResult>;
74
84
  }>;
85
+ /** Additive SOAP 1.2 injection shares reconstruction and transport with SOAP 1.1.
86
+ * @since 1.3.0
87
+ */
88
+ export declare const componentSoap12ClientRuntime: Readonly<{
89
+ version: "checked-component-soap12-runtime-v1";
90
+ execute: (expectedBase64: string, input: unknown, options: ComponentClientOptions) => Promise<ComponentClientResult>;
91
+ }>;
92
+ /** SOAP 1.1 protocol faults require a generated client that can represent a missing application value.
93
+ * @since 1.4.0
94
+ */
95
+ export declare const componentSoap11ClientRuntime: Readonly<{
96
+ version: "checked-component-soap11-runtime-v1";
97
+ execute: (expectedBase64: string, input: unknown, options: ComponentClientOptions) => Promise<ComponentClientResult<Soap11FaultMetadata>>;
98
+ }>;
@@ -6,7 +6,7 @@ import { types } from 'node:util';
6
6
  import { createApplicationBudget } from '../compiler/semanticBudget.js';
7
7
  import { reconstructDirectSourceCapsuleInRequest, reconstructComponentSourceCapsuleInRequest, checkDirectSourceCompatibility, DIRECT_CAPSULE_LIMITS } from '../compiler/directSourceCapsule.js';
8
8
  import { exchangeDirectSoap, exchangeComponentSoap, directSoapOptionsShape } from '../compiler/directSoap.js';
9
- import { DIRECT_CLIENT_RUNTIME_VERSION, COMPONENT_CLIENT_RUNTIME_VERSION } from './directClientVersion.js';
9
+ import { DIRECT_CLIENT_RUNTIME_VERSION, COMPONENT_CLIENT_RUNTIME_VERSION, COMPONENT_SOAP11_CLIENT_RUNTIME_VERSION, COMPONENT_SOAP12_CLIENT_RUNTIME_VERSION } from './directClientVersion.js';
10
10
  import { prepareSchemaSnapshotFiles } from '../loader/schemaInput.js';
11
11
  import { SchemaLoadingError } from '../loader/orderedSyntax.js';
12
12
  /** The generated literal is the expected artifact; caller bytes never choose its static contract. */
@@ -19,7 +19,21 @@ export const componentClientRuntime = Object.freeze({
19
19
  version: COMPONENT_CLIENT_RUNTIME_VERSION,
20
20
  execute: (expectedBase64, input, options) => executeClient(expectedBase64, input, options, true),
21
21
  });
22
- async function executeClient(expectedBase64, input, options, component) {
22
+ /** Additive SOAP 1.2 injection shares reconstruction and transport with SOAP 1.1.
23
+ * @since 1.3.0
24
+ */
25
+ export const componentSoap12ClientRuntime = Object.freeze({
26
+ version: COMPONENT_SOAP12_CLIENT_RUNTIME_VERSION,
27
+ execute: (expectedBase64, input, options) => executeClient(expectedBase64, input, options, true, '1.2'),
28
+ });
29
+ /** SOAP 1.1 protocol faults require a generated client that can represent a missing application value.
30
+ * @since 1.4.0
31
+ */
32
+ export const componentSoap11ClientRuntime = Object.freeze({
33
+ version: COMPONENT_SOAP11_CLIENT_RUNTIME_VERSION,
34
+ execute: (expectedBase64, input, options) => executeClient(expectedBase64, input, options, true, '1.1'),
35
+ });
36
+ async function executeClient(expectedBase64, input, options, component, protocol) {
23
37
  // Constant-sized entry introspection precedes budget configuration and all reads.
24
38
  const entryFailure = () => ({ kind: 'unsupported', rule: 'data-only client options required', usage: { nodes: 0, work: 0 } });
25
39
  if (!options || typeof options !== 'object' || types.isProxy(options)
@@ -121,11 +135,12 @@ async function executeClient(expectedBase64, input, options, component) {
121
135
  standardLibraries: sources.standards, recordSources: sources.records }, budget);
122
136
  if (fresh.kind !== 'reconstructed-component')
123
137
  return fresh;
138
+ if (fresh.contract.binding.version === '1.2' && protocol !== '1.2')
139
+ return fail('incompatible-artifact', 'SOAP 1.2 runtime binding required');
124
140
  if (fresh.selection.port !== options.soap.port || fresh.selection.service.namespace !== options.soap.service.namespace
125
141
  || fresh.selection.service.local !== options.soap.service.local)
126
142
  return fail('incompatible-artifact', 'explicit source selection mismatch');
127
- result = fresh.responseProjection ? await exchangeComponentSoap(fresh.contract, fresh.mappings[0], input, supplied.soap, fresh.responseProjection)
128
- : await exchangeComponentSoap(fresh.contract, fresh.mappings[0], input, supplied.soap);
143
+ result = await exchangeComponentSoap(fresh.contract, fresh.mappings[0], input, supplied.soap, fresh.responseProjection, protocol === '1.1');
129
144
  }
130
145
  else {
131
146
  const fresh = await reconstructDirectSourceCapsuleInRequest(bytes, options.localSource, { policy: { fileRoots: options.fileRoots }, limits: options.budget, signal: options.soap.signal }, budget);
@@ -138,8 +153,11 @@ async function executeClient(expectedBase64, input, options, component) {
138
153
  // by the original adapter; no catch-and-cast turns a failure into typed success.
139
154
  result = await exchangeDirectSoap(fresh.contract, fresh.mappings[0], input, options.soap);
140
155
  }
141
- if (!('decoded' in result))
156
+ if (!('decoded' in result)) {
157
+ if (result.kind === 'soap-fault' && result.fault.version !== protocol)
158
+ throw new TypeError('Protocol fault requires its generated runtime binding');
142
159
  return result;
160
+ }
143
161
  if (options.soap.signal?.aborted)
144
162
  return fail('cancelled', 'cooperative-cancellation');
145
163
  const common = { value: result.decoded.validated.value, status: result.status, admitted: false,
@@ -3,4 +3,8 @@ export declare const DIRECT_CLIENT_RUNTIME_VERSION: "checked-direct-runtime-v1";
3
3
  export declare const DIRECT_CLIENT_RUNTIME_REPAIR = "Regenerate matched direct client and capsule with the installed runtime.";
4
4
  /** Component profile never impersonates the supported direct preview protocol. */
5
5
  export declare const COMPONENT_CLIENT_RUNTIME_VERSION: "checked-component-runtime-v2";
6
+ /** SOAP 1.2 can return protocol faults without declared application values. */
7
+ export declare const COMPONENT_SOAP12_CLIENT_RUNTIME_VERSION: "checked-component-soap12-runtime-v1";
8
+ /** SOAP 1.1 component clients with value-less protocol faults use their own injection contract. */
9
+ export declare const COMPONENT_SOAP11_CLIENT_RUNTIME_VERSION: "checked-component-soap11-runtime-v1";
6
10
  export declare const COMPONENT_CLIENT_RUNTIME_REPAIR = "Regenerate matched component client and source artifacts with the installed runtime.";
@@ -3,4 +3,8 @@ export const DIRECT_CLIENT_RUNTIME_VERSION = 'checked-direct-runtime-v1';
3
3
  export const DIRECT_CLIENT_RUNTIME_REPAIR = 'Regenerate matched direct client and capsule with the installed runtime.';
4
4
  /** Component profile never impersonates the supported direct preview protocol. */
5
5
  export const COMPONENT_CLIENT_RUNTIME_VERSION = 'checked-component-runtime-v2';
6
+ /** SOAP 1.2 can return protocol faults without declared application values. */
7
+ export const COMPONENT_SOAP12_CLIENT_RUNTIME_VERSION = 'checked-component-soap12-runtime-v1';
8
+ /** SOAP 1.1 component clients with value-less protocol faults use their own injection contract. */
9
+ export const COMPONENT_SOAP11_CLIENT_RUNTIME_VERSION = 'checked-component-soap11-runtime-v1';
6
10
  export const COMPONENT_CLIENT_RUNTIME_REPAIR = 'Regenerate matched component client and source artifacts with the installed runtime.';
@@ -1,5 +1,5 @@
1
1
  import { directLimit } from '../compiler/directExecutionPolicy.js';
2
- import { DIRECT_CLIENT_RUNTIME_VERSION, DIRECT_CLIENT_RUNTIME_REPAIR, COMPONENT_CLIENT_RUNTIME_VERSION, COMPONENT_CLIENT_RUNTIME_REPAIR } from './directClientVersion.js';
2
+ import { DIRECT_CLIENT_RUNTIME_VERSION, DIRECT_CLIENT_RUNTIME_REPAIR, COMPONENT_SOAP11_CLIENT_RUNTIME_VERSION, COMPONENT_CLIENT_RUNTIME_REPAIR, COMPONENT_SOAP12_CLIENT_RUNTIME_VERSION } from './directClientVersion.js';
3
3
  /** Native-plan generator consumed by the opt-in checked pipeline. */
4
4
  import { checkSemanticBudget } from '../compiler/semanticBudget.js';
5
5
  import { writeFileSync, mkdirSync } from 'node:fs';
@@ -170,12 +170,15 @@ export async function generateComponentClient(expected, signal) {
170
170
  ...response.declarations, `export type Response = ${response.type};`,
171
171
  ...faults.flatMap(({ shape }, index) => [...shape.declarations, `export type Fault_${index} = ${shape.type};`])
172
172
  ];
173
+ const soap12 = expected.contract.binding.version === '1.2';
174
+ const runtimeVersion = soap12 ? COMPONENT_SOAP12_CLIENT_RUNTIME_VERSION : COMPONENT_SOAP11_CLIENT_RUNTIME_VERSION;
173
175
  const lines = [
174
- '// Checked component client; inject checkedComponentRuntimeV1 from the package root.',
176
+ `// Checked component client; inject ${soap12 ? 'checkedComponentRuntimeV2' : 'checkedComponentRuntimeV3'} from the package root.`,
175
177
  '// Scalar facets, ordered occurrence correlation and wildcard membership remain native runtime duties.',
176
178
  ...declarations,
177
179
  'export interface Usage {readonly nodes: number; readonly work: number}',
178
180
  'export interface Name {readonly namespace: string; readonly local: string}',
181
+ ...(soap12 ? ['export interface Soap12Fault {readonly version: "1.2"; readonly code: Name; readonly subcodes: readonly Name[]; readonly reasons: readonly {readonly lang: string; readonly text: string}[]; readonly string: string; readonly node?: string; readonly role?: string}'] : []),
179
182
  'export interface Options {',
180
183
  ' readonly localSource: string; readonly fileRoots: readonly string[]; readonly capsule: Uint8Array;',
181
184
  ' readonly sourceResources: readonly {readonly uri: string; readonly file: string; readonly canonicalUri?: string}[]; readonly sourceOrigins: readonly string[];',
@@ -190,26 +193,28 @@ export async function generateComponentClient(expected, signal) {
190
193
  '}',
191
194
  'export interface Failure {readonly kind: "incompatible-artifact" | "unsupported" | "missing-authority" | "resource-limit" | "cancelled" | "inconclusive" | "invalid-schema" | "scalar-invalid" | "value-invalid" | "xml-invalid" | "mismatch" | "transport" | "protocol";',
192
195
  ' readonly rule: string; readonly repair?: string; readonly phase?: "input" | "response"; readonly usage: Usage}',
193
- 'export interface NativeSuccess {readonly kind: "soap-response" | "soap-fault"; readonly value: unknown; readonly admitted: false;',
196
+ 'export interface NativeSuccess {readonly kind: "soap-response" | "soap-fault"; readonly value?: unknown; readonly admitted: false;',
194
197
  ' readonly status: number; readonly usage: Usage; readonly responseBytes: Uint8Array;',
195
- ' readonly fault?: {readonly name: string; readonly code: Name; readonly string: string; readonly actor?: string}}',
196
- `export interface Runtime {readonly version: ${literal(COMPONENT_CLIENT_RUNTIME_VERSION)}; execute(expectedBase64: string, input: unknown, options: Options, version: ${literal(COMPONENT_CLIENT_RUNTIME_VERSION)}): Promise<Failure | NativeSuccess>}`,
198
+ ' readonly fault?: {readonly name?: string; readonly code: Name; readonly string: string; readonly actor?: string}}',
199
+ `export interface Runtime {readonly version: ${literal(runtimeVersion)}; execute(expectedBase64: string, input: unknown, options: Options, version: ${literal(runtimeVersion)}): Promise<Failure | NativeSuccess>}`,
197
200
  'type Common = {readonly admitted: false; readonly status: number; readonly usage: Usage; readonly responseBytes: Uint8Array};',
198
201
  'export type Result = Failure | (Common & {readonly kind: "soap-response"; readonly value: Response})',
199
202
  ...faults.map(({ mapping }, index) => {
200
203
  if (mapping.direction.kind !== 'fault')
201
204
  throw new TypeError('Native fault mapping required');
202
- return ` | (Common & {readonly kind: "soap-fault"; readonly value: Fault_${index}; readonly fault: {readonly name: ${literal(mapping.direction.name)}; readonly code: Name; readonly string: string; readonly actor?: string}})`;
205
+ return soap12 ? ` | (Common & {readonly kind: "soap-fault"; readonly value: Fault_${index}; readonly fault: Soap12Fault & {readonly name: ${literal(mapping.direction.name)}}})`
206
+ : ` | (Common & {readonly kind: "soap-fault"; readonly value: Fault_${index}; readonly fault: {readonly name: ${literal(mapping.direction.name)}; readonly code: Name; readonly string: string; readonly actor?: string}})`;
203
207
  }),
204
- ...(faults.length ? [] : [' | (Common & {readonly kind: "soap-fault"; readonly value: never; readonly fault: {readonly name: never; readonly code: Name; readonly string: string; readonly actor?: string}})']),
208
+ soap12 ? ' | (Common & {readonly kind: "soap-fault"; readonly value?: never; readonly fault: Soap12Fault & {readonly name?: never}})'
209
+ : ' | (Common & {readonly kind: "soap-fault"; readonly value?: never; readonly fault: {readonly version: "1.1"; readonly name?: never; readonly code: Name; readonly string: string; readonly actor?: string}})',
205
210
  ';',
206
211
  `export const expectedCapsuleBase64 = ${literal(Buffer.from(capsule.bytes).toString('base64'))};`,
207
212
  `export const selection = ${literal(JSON.stringify({ selector: expected.contract.selector, ...expected.selection }))};`,
208
213
  'export function createClient(runtime: Runtime) {',
209
214
  ' return Object.freeze({',
210
215
  ` [${literal(expected.contract.selector.operation)}]: async (input: Request, options: Options): Promise<Result> => {`,
211
- ` if (runtime.version !== ${literal(COMPONENT_CLIENT_RUNTIME_VERSION)}) return {kind: "incompatible-artifact", rule: "generated-runtime-version", repair: ${literal(COMPONENT_CLIENT_RUNTIME_REPAIR)}, usage: {nodes: 0, work: 0}};`,
212
- ` return await runtime.execute(expectedCapsuleBase64, input, options, ${literal(COMPONENT_CLIENT_RUNTIME_VERSION)}) as Result;`,
216
+ ` if (runtime.version !== ${literal(runtimeVersion)}) return {kind: "incompatible-artifact", rule: "generated-runtime-version", repair: ${literal(COMPONENT_CLIENT_RUNTIME_REPAIR)}, usage: {nodes: 0, work: 0}};`,
217
+ ` return await runtime.execute(expectedCapsuleBase64, input, options, ${literal(runtimeVersion)}) as Result;`,
213
218
  ' },', ' });', '}', '',
214
219
  ];
215
220
  const source = composeGeneratedSource(lines.join('\n'), resolvePreamble('direct-client'));
@@ -57,6 +57,7 @@ export interface ComponentRecordDecodedXml {
57
57
  type Leaf = Readonly<{
58
58
  kind: 'standard';
59
59
  declaration: StandardSchemaElementDeclaration;
60
+ optional: boolean;
60
61
  } | {
61
62
  kind: 'boundary';
62
63
  namespace: NamespaceConstraint;
@@ -160,7 +160,9 @@ export function createComponentRecordProjection(outer, companion, supplied, opti
160
160
  while (pending.length) {
161
161
  check();
162
162
  const particle = pending.pop();
163
- if (particle.occurs.min !== '1' || particle.occurs.max !== '1')
163
+ // @why Only metadata may be absent, and only when the authenticated source declares it optional.
164
+ if (particle.occurs.max !== '1' || (particle.occurs.min !== '1'
165
+ && !(particle.standardElement && particle.occurs.min === '0')))
164
166
  stop('record-wrapper-unit-occurrence');
165
167
  if (particle.kind === 'sequence')
166
168
  for (let index = particle.children.length - 1; index >= 0; index--) {
@@ -168,7 +170,7 @@ export function createComponentRecordProjection(outer, companion, supplied, opti
168
170
  pending.push(particle.children[index]);
169
171
  }
170
172
  else if (particle.standardElement)
171
- leaves.push(Object.freeze({ kind: 'standard', declaration: particle.standardElement }));
173
+ leaves.push(Object.freeze({ kind: 'standard', declaration: particle.standardElement, optional: particle.occurs.min === '0' }));
172
174
  else if (particle.kind === 'any' && particle.wildcard?.processContents === 'strict') {
173
175
  if (++boundaries > 1 || !namespaceAllows(particle.wildcard.namespace, spec.payloadPath[0].namespace, budget.chargeWork))
174
176
  stop('record-wrapper-wildcard-boundary');
@@ -266,12 +268,19 @@ export function decodeComponentRecordXml(outer, projection, bytes, options = {})
266
268
  if (wrapper.attributes.length || wrapper.children.some(node => node.kind === 'text' && /[^\t\r\n ]/.test(node.value)))
267
269
  throw new ScopeStop('mismatch', 'record-wrapper-attributes-or-text');
268
270
  const children = syntaxElements(wrapper);
269
- if (children.length !== owned.leaves.length)
271
+ const required = owned.leaves.filter(leaf => leaf.kind === 'boundary' || !leaf.optional).length;
272
+ if (children.length < required || children.length > owned.leaves.length)
270
273
  throw new ScopeStop('mismatch', 'record-wrapper-child-count');
271
274
  let payload;
275
+ let cursor = 0;
272
276
  for (let index = 0; index < owned.leaves.length; index++) {
273
277
  check();
274
- const leaf = owned.leaves[index], child = children[index];
278
+ const leaf = owned.leaves[index], child = children[cursor];
279
+ if (leaf.kind === 'standard' && leaf.optional && (!child || !same(child.name, leaf.declaration.name)))
280
+ continue;
281
+ if (!child)
282
+ throw new ScopeStop('mismatch', 'record-wrapper-child-count');
283
+ cursor++;
275
284
  if (leaf.kind === 'standard') {
276
285
  if (!same(child.name, leaf.declaration.name))
277
286
  throw new ScopeStop('mismatch', 'record-wrapper-standard-order');
@@ -287,6 +296,8 @@ export function decodeComponentRecordXml(outer, projection, bytes, options = {})
287
296
  payload = child;
288
297
  }
289
298
  }
299
+ if (cursor !== children.length)
300
+ throw new ScopeStop('mismatch', 'record-wrapper-child-count');
290
301
  if (!payload)
291
302
  throw new TypeError('Native record boundary unavailable');
292
303
  const decoded = assessObservedComponentPayload(owned.companion, owned.mapping, observation, payload, options);
@@ -3,6 +3,7 @@ import { directDataOwnership, directLimit } from './directExecutionPolicy.js';
3
3
  import { checkSemanticBudget } from './semanticBudget.js';
4
4
  import { DataOwnershipLimitError } from './dataOwnership.js';
5
5
  import { types } from 'node:util';
6
+ import { xmlByteLength, snapshotXmlBytes } from '../util/xmlBytes.js';
6
7
  import { inspectDirectOperation, isRpcMessageRoot } from './directOperation.js';
7
8
  import { inspectDirectValueMapping, expandDirectValue, assessDirectScalars, inspectComponentValueMapping, expandProjectionValue, assessProjectionScalars } from './directValues.js';
8
9
  import { matchDirectOperation, matchDirectProjection } from './directStructure.js';
@@ -40,7 +41,7 @@ function runMapping(contract, supplied, options, profile, inspect, work) {
40
41
  && contract.binding.style === 'rpc' && contract.binding.use === 'encoded' && contract.binding.encoding === 'http://schemas.xmlsoap.org/soap/encoding/';
41
42
  if ((!rpc && mapping.version !== profile.version) || mapping.contextId !== contract.contextId
42
43
  || contract.basis !== profile.basis || contract.correspondence !== profile.correspondence
43
- || contract.binding.version !== '1.1' || !rpc && (contract.binding.style !== 'document' || contract.binding.use !== 'literal')
44
+ || !(contract.binding.version === '1.1' || profile === componentProfile && contract.binding.version === '1.2' && !rpc) || !rpc && (contract.binding.style !== 'document' || contract.binding.use !== 'literal')
44
45
  || contract.binding.transport !== 'http://schemas.xmlsoap.org/soap/http')
45
46
  throw new Stop('unsupported', profile.rule);
46
47
  const direction = mapping.direction;
@@ -262,20 +263,11 @@ export function encodeComponentXml(contract, mapping, application, options = {})
262
263
  }
263
264
  /** Retain the shared byte-copy, XML-version, entity and explicit capacity boundaries for both profiles. */
264
265
  function parseObservedXml(bytes, { budget, limits, check }, sourceUri, componentResponseEncoding) {
265
- if (!types.isUint8Array(bytes) || types.isProxy(bytes))
266
- throw new TypeError('XML requires Uint8Array bytes');
267
- // Use native internal-slot access, avoiding subclass/own length, buffer and
268
- // iterator getters. Shared backing stores are outside the data-only contract.
269
- const typed = Object.getPrototypeOf(Uint8Array.prototype);
270
- const length = Object.getOwnPropertyDescriptor(typed, 'byteLength').get.call(bytes);
271
- const backing = Object.getOwnPropertyDescriptor(typed, 'buffer').get.call(bytes);
272
- if (types.isSharedArrayBuffer(backing))
273
- throw new TypeError('Shared XML bytes are unsupported');
266
+ const length = xmlByteLength(bytes);
274
267
  if (length > limits.maxBytes)
275
268
  throw new Stop('resource-limit', 'XML byte limit');
276
269
  check();
277
- const input = new Uint8Array(length);
278
- Uint8Array.prototype.set.call(input, bytes);
270
+ const input = snapshotXmlBytes(bytes);
279
271
  const uri = sourceUri ?? 'urn:typescript-wsdl-client:observed-direct-xml';
280
272
  if (typeof uri !== 'string')
281
273
  throw new TypeError('XML source URI must be a string');
@@ -46,12 +46,13 @@ export interface DirectOperationContract {
46
46
  readonly basis: 'native-direct-premises-v1' | 'native-component-premises-v1';
47
47
  readonly correspondence: 'dated-XSD-2004-direct-v1' | 'dated-XSD-2004-components-v1';
48
48
  readonly binding: Readonly<{
49
- version: '1.1';
49
+ version: '1.1' | '1.2';
50
50
  style: 'document' | 'rpc';
51
51
  use: 'literal' | 'encoded';
52
52
  encoding?: string;
53
53
  transport: string;
54
54
  action: string;
55
+ actionRequired?: boolean;
55
56
  source: SyntaxSource;
56
57
  }>;
57
58
  readonly input: DirectMessageRoot | RpcMessageRoot;
@@ -8,6 +8,7 @@ import { qualifyDirectSource } from './checkedSourcePreparation.js';
8
8
  import { qualifyComponentSource, inspectComponentSourceQualification } from './checkedComponentSource.js';
9
9
  import { prepareWsdlPolicyRequirements, assertWsdlPolicyRequirements } from './wsdlPolicyRequirements.js';
10
10
  const SOAP = 'http://schemas.xmlsoap.org/wsdl/soap/';
11
+ const SOAP12 = 'http://schemas.xmlsoap.org/wsdl/soap12/';
11
12
  const HTTP = 'http://schemas.xmlsoap.org/soap/http';
12
13
  const ADDRESSING_WSDL = 'http://www.w3.org/2006/05/addressing/wsdl';
13
14
  /** Distinguish typed RPC parameters without inventing a document element.
@@ -171,7 +172,8 @@ async function selectOperationInRequest(source, selector, options, budget, profi
171
172
  }
172
173
  };
173
174
  const ws = (local) => `${WSDL_NAMESPACE}#${local}`;
174
- const soap = (local) => `${SOAP}#${local}`;
175
+ let soapNamespace = SOAP;
176
+ const soap = (local) => `${soapNamespace}#${local}`;
175
177
  const attrs = (node, allowed) => {
176
178
  children(node);
177
179
  for (const a of node.attributes)
@@ -179,7 +181,7 @@ async function selectOperationInRequest(source, selector, options, budget, profi
179
181
  && (a.name.namespace !== '' || !allowed.includes(a.name.local)))
180
182
  refuse('unsupported', 'binding-attribute-shape');
181
183
  };
182
- // Conservative extension policy: only known WSDL 1.1/SOAP 1.1 syntax, even when optional.
184
+ // @constraints Component selection admits known SOAP 1.2 syntax; the direct profile remains SOAP 1.1 only.
183
185
  // Documentation payload and XSD syntax are inert here and owned separately.
184
186
  budget.chargeWork(64 + input.schemas.length * 32);
185
187
  const qualifiedSchemas = new Set(input.schemas.map(schema => schema.syntax));
@@ -200,7 +202,9 @@ async function selectOperationInRequest(source, selector, options, budget, profi
200
202
  continue;
201
203
  if (node.name.namespace === WSDL_NAMESPACE && node.name.local === 'documentation')
202
204
  continue;
203
- const allowed = node.name.namespace === WSDL_NAMESPACE ? attributes[node.name.local] : node.name.namespace === SOAP ? soapAttributes[node.name.local] : undefined;
205
+ const soap12 = profile === 'components' && node.name.namespace === SOAP12;
206
+ const allowed = node.name.namespace === WSDL_NAMESPACE ? attributes[node.name.local] : node.name.namespace === SOAP || soap12
207
+ ? soap12 && node.name.local === 'operation' ? [...soapAttributes.operation, 'soapActionRequired'] : soapAttributes[node.name.local] : undefined;
204
208
  if (!allowed)
205
209
  return refuse('unsupported', 'wsdl-extension');
206
210
  for (const a of node.attributes) {
@@ -208,7 +212,7 @@ async function selectOperationInRequest(source, selector, options, budget, profi
208
212
  // UsingAddressing remains unsupported, so these attributes are retained metadata, never SOAP header instructions.
209
213
  if (addressingAttribute(node, a.name.namespace, a.name.local))
210
214
  continue;
211
- if (a.name.namespace === WSDL_NAMESPACE && a.name.local === 'required' && node.name.namespace === SOAP && ['true', '1', 'false', '0'].includes(a.value))
215
+ if (a.name.namespace === WSDL_NAMESPACE && a.name.local === 'required' && (node.name.namespace === SOAP || soap12) && ['true', '1', 'false', '0'].includes(a.value))
212
216
  continue;
213
217
  if (a.name.namespace !== '' || !allowed.includes(a.name.local))
214
218
  refuse('unsupported', 'wsdl-attribute');
@@ -253,10 +257,14 @@ async function selectOperationInRequest(source, selector, options, budget, profi
253
257
  return matches[0];
254
258
  };
255
259
  const abstract = operation(portType), bound = operation(binding);
260
+ const bindingExtensions = children(binding.syntax).filter(node => [SOAP, SOAP12].includes(node.name.namespace) && node.name.local === 'binding');
261
+ if (bindingExtensions.length !== 1)
262
+ return refuse('selection-refused', 'expected-one-binding');
263
+ soapNamespace = bindingExtensions[0].name.namespace;
256
264
  attrs(bound, ['name']);
257
265
  shape(binding.syntax, [soap('binding'), ws('operation'), ws('documentation')]);
258
- const soapBinding = exactly(binding.syntax, SOAP, 'binding');
259
- const soapOperation = exactly(bound, SOAP, 'operation');
266
+ const soapBinding = exactly(binding.syntax, soapNamespace, 'binding');
267
+ const soapOperation = exactly(bound, soapNamespace, 'operation');
260
268
  shape(soapBinding, []);
261
269
  shape(soapOperation, []);
262
270
  if (!['document', 'rpc'].includes(attr(soapBinding, 'style') ?? 'document'))
@@ -264,11 +272,19 @@ async function selectOperationInRequest(source, selector, options, budget, profi
264
272
  if (attr(soapBinding, 'transport') !== HTTP)
265
273
  return refuse('unsupported', 'soap-transport');
266
274
  const style = attr(soapOperation, 'style') ?? attr(soapBinding, 'style') ?? 'document';
267
- if (style !== 'document' && !(profile === 'components' && style === 'rpc'))
275
+ if (style !== 'document' && !(profile === 'components' && style === 'rpc' && soapNamespace === SOAP))
268
276
  return refuse('unsupported', 'soap-style');
269
- const action = attr(soapOperation, 'soapAction');
270
- if (action === undefined)
277
+ const required = attr(soapOperation, 'soapActionRequired');
278
+ if (required !== undefined && !['true', 'false', '1', '0'].includes(required))
279
+ return refuse('unsupported', 'soap-action-required-boolean');
280
+ const actionRequired = required !== 'false' && required !== '0';
281
+ const selectedAction = attr(soapOperation, 'soapAction');
282
+ if (selectedAction === undefined && (soapNamespace === SOAP || actionRequired))
271
283
  return refuse('unsupported', 'soap-action-required');
284
+ if (soapNamespace === SOAP12 && selectedAction !== undefined && !/^[A-Za-z][A-Za-z0-9+.-]*:[^\s]*$/.test(selectedAction))
285
+ return refuse('unsupported', 'soap-action-absolute-uri');
286
+ // @why SOAP 1.2 permits an absent optional action; the existing string contract uses an empty value to omit it on HTTP.
287
+ const action = selectedAction ?? '';
272
288
  shape(abstract, [ws('input'), ws('output'), ws('fault'), ws('documentation')]);
273
289
  shape(bound, [soap('operation'), ws('input'), ws('output'), ws('fault'), ws('documentation')]);
274
290
  const abstractInput = exactly(abstract, WSDL_NAMESPACE, 'input'), abstractOutput = exactly(abstract, WSDL_NAMESPACE, 'output');
@@ -289,7 +305,7 @@ async function selectOperationInRequest(source, selector, options, budget, profi
289
305
  attrs(boundSyntax, ['name']);
290
306
  shape(messageSyntax, [ws('documentation')]);
291
307
  shape(boundSyntax, [soap(faultName === undefined ? 'body' : 'fault'), ws('documentation')]);
292
- const body = exactly(boundSyntax, SOAP, faultName === undefined ? 'body' : 'fault');
308
+ const body = exactly(boundSyntax, soapNamespace, faultName === undefined ? 'body' : 'fault');
293
309
  shape(body, []);
294
310
  attrs(body, faultName === undefined ? ['use', 'parts'] : ['use', 'name']);
295
311
  if (attr(body, 'use') !== 'literal')
@@ -393,7 +409,7 @@ async function selectOperationInRequest(source, selector, options, budget, profi
393
409
  const contract = { selector: ownedSelector, contextId: context.contextId,
394
410
  basis: profile === 'direct' ? 'native-direct-premises-v1' : 'native-component-premises-v1',
395
411
  correspondence: profile === 'direct' ? 'dated-XSD-2004-direct-v1' : 'dated-XSD-2004-components-v1',
396
- binding: { version: '1.1', style: style, use: style === 'rpc' ? 'encoded' : 'literal',
412
+ binding: { version: soapNamespace === SOAP12 ? '1.2' : '1.1', ...(soapNamespace === SOAP12 ? { actionRequired } : {}), style: style, use: style === 'rpc' ? 'encoded' : 'literal',
397
413
  ...(style === 'rpc' ? { encoding: 'http://schemas.xmlsoap.org/soap/encoding/' } : {}), transport: HTTP, action, source: binding.context.source },
398
414
  input: requestRoot, output: responseRoot, faults,
399
415
  ...(policyOwner ? { policy: policyOwner.forBinding(binding.syntax), addressingActions: { required: false,
@@ -1,6 +1,6 @@
1
1
  import type { DirectOperationContract } from './directOperation.js';
2
2
  import type { DirectCodecFailure, DirectDecodedXml, ComponentDecodedXml, DirectXmlLimits } from './directCodec.js';
3
- import type { ExpandedName, SyntaxElement } from '../loader/orderedSyntax.js';
3
+ import type { ExpandedName, SyntaxElement, SyntaxSource } from '../loader/orderedSyntax.js';
4
4
  import type { ResolverUsage } from './semanticBudget.js';
5
5
  import type { ComponentRecordProjection, ComponentRecordDecodedXml } from './componentRecordProjection.js';
6
6
  export interface DirectSoapOptions {
@@ -20,26 +20,69 @@ export interface DirectSoapFailure {
20
20
  readonly rule: string;
21
21
  readonly usage: ResolverUsage;
22
22
  readonly phase?: 'input' | 'response';
23
+ readonly source?: SyntaxSource;
24
+ readonly schemaPath?: string;
25
+ readonly payloadPath?: string;
23
26
  }
24
- type SoapResult<D> = DirectSoapFailure | Readonly<{
27
+ /** Version-specific protocol metadata; application detail remains owned by native mappings.
28
+ * @since 1.3.0
29
+ */
30
+ export interface Soap12FaultMetadata {
31
+ readonly version: '1.2';
32
+ readonly code: ExpandedName;
33
+ readonly subcodes: readonly ExpandedName[];
34
+ readonly reasons: readonly Readonly<{
35
+ lang: string;
36
+ text: string;
37
+ }>[];
38
+ readonly string: string;
39
+ readonly node?: string;
40
+ readonly role?: string;
41
+ }
42
+ /** SOAP 1.1 protocol fields retained when the selected operation supplies no matching detail.
43
+ * @since 1.4.0
44
+ */
45
+ export interface Soap11FaultMetadata {
46
+ readonly version: '1.1';
47
+ readonly code: ExpandedName;
48
+ readonly string: string;
49
+ readonly actor?: string;
50
+ }
51
+ type ProtocolFaultMetadata = Soap11FaultMetadata | Soap12FaultMetadata;
52
+ export type DeclaredSoapFault = Readonly<{
53
+ name: string;
54
+ code: ExpandedName;
55
+ codeNode: SyntaxElement;
56
+ string: string;
57
+ actor?: string;
58
+ }> & Partial<Soap12FaultMetadata>;
59
+ /** A valid protocol fault without an application detail mapped by the selected operation.
60
+ * @since 1.3.0
61
+ * @constraints Unassessed detail remains only in responseBytes; no native value receipt is issued.
62
+ */
63
+ export interface ComponentProtocolFault<F extends ProtocolFaultMetadata = ProtocolFaultMetadata> {
64
+ readonly kind: 'soap-fault';
65
+ readonly admitted: false;
66
+ readonly fault: F;
67
+ readonly responseBytes: Uint8Array;
68
+ readonly status: number;
69
+ readonly usage: ResolverUsage;
70
+ readonly ignoredHeaders?: readonly ExpandedName[];
71
+ }
72
+ type DecodedSoapResult<D> = DirectSoapFailure | Readonly<{
25
73
  kind: 'soap-response' | 'soap-fault';
26
74
  admitted: false;
27
75
  decoded: D;
28
76
  /** Actual bounded HTTP entity bytes, before any decoding; caller-owned data. */
29
77
  responseBytes: Uint8Array;
30
78
  status: number;
31
- fault?: Readonly<{
32
- name: string;
33
- code: ExpandedName;
34
- codeNode: SyntaxElement;
35
- string: string;
36
- actor?: string;
37
- }>;
38
- /** Qualified optional response blocks ignored under SOAP 1.1; their application semantics were not assessed. */
79
+ fault?: DeclaredSoapFault;
80
+ /** Unprocessed header blocks; their application semantics were not assessed. */
39
81
  ignoredHeaders?: readonly ExpandedName[];
40
82
  usage: ResolverUsage;
41
83
  }>;
42
- export type DirectSoapResult = SoapResult<DirectDecodedXml>;
84
+ type SoapResult<D> = DecodedSoapResult<D> | ComponentProtocolFault;
85
+ export type DirectSoapResult = DecodedSoapResult<DirectDecodedXml>;
43
86
  export type ComponentSoapResult = SoapResult<ComponentDecodedXml>;
44
87
  export type ComponentRecordSoapResult = SoapResult<ComponentDecodedXml | ComponentRecordDecodedXml>;
45
88
  export interface ComponentSoapOptions extends Omit<DirectSoapOptions, 'endpoint' | 'allowEndpoint'> {
@@ -62,5 +105,37 @@ export declare function exchangeDirectSoap(contract: DirectOperationContract, in
62
105
  * @throws TypeError For foreign native handles. Invalid explicit timer/XML capacities throw RangeError.
63
106
  */
64
107
  export declare function exchangeComponentSoap(contract: DirectOperationContract, inputMapping: unknown, application: unknown, options: ComponentSoapOptions): Promise<ComponentSoapResult>;
65
- export declare function exchangeComponentSoap(contract: DirectOperationContract, inputMapping: unknown, application: unknown, options: ComponentSoapOptions, responseProjection: ComponentRecordProjection): Promise<ComponentRecordSoapResult>;
108
+ export declare function exchangeComponentSoap(contract: DirectOperationContract, inputMapping: unknown, application: unknown, options: ComponentSoapOptions, responseProjection: ComponentRecordProjection | undefined, soap11ProtocolFaults?: boolean): Promise<ComponentRecordSoapResult>;
109
+ /** Actual HTTP entity metadata; omission leaves HTTP status and media duties unassessed.
110
+ * @since Unreleased
111
+ */
112
+ export interface ObservedSoapResponseMetadata {
113
+ readonly status: number;
114
+ readonly contentType: string;
115
+ readonly contentEncoding?: string;
116
+ }
117
+ export type ComponentSoapResponseAssessment = DirectSoapFailure | Readonly<{
118
+ kind: 'soap-response' | 'soap-fault';
119
+ admitted: false;
120
+ decoded?: ComponentDecodedXml | ComponentRecordDecodedXml;
121
+ fault?: DeclaredSoapFault | ProtocolFaultMetadata;
122
+ ignoredHeaders?: readonly ExpandedName[];
123
+ usage: ResolverUsage;
124
+ }>;
125
+ /** Assess caller-observed response bytes through the same protocol and native decoder used by real exchanges.
126
+ * @param contract Authenticated selected component operation.
127
+ * @param inputMapping Exact operation input mapping, used only to derive response/fault mappings.
128
+ * @param bytes Actual observed SOAP entity bytes; no request or transport is constructed.
129
+ * @param options XML controls, optional actual HTTP metadata and exact response projection.
130
+ * @returns Native response/fault assessment or semantic failure; HTTP remains unassessed when metadata is absent.
131
+ * @throws TypeError For foreign native handles; RangeError for invalid explicit XML limits.
132
+ * @constraints This response-only helper performs no transport, endpoint authorization or request-value assessment.
133
+ */
134
+ export declare function inspectComponentSoapResponse(contract: DirectOperationContract, inputMapping: unknown, bytes: Uint8Array, options?: Readonly<{
135
+ signal?: AbortSignal;
136
+ limits?: DirectXmlLimits;
137
+ uri?: string;
138
+ responseMetadata?: ObservedSoapResponseMetadata;
139
+ responseProjection?: ComponentRecordProjection;
140
+ }>): ComponentSoapResponseAssessment;
66
141
  export {};