@techspokes/typescript-wsdl-client 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,6 +9,10 @@ Turn legacy WSDL/SOAP services into typed TypeScript clients and optional REST A
9
9
 
10
10
  Created from production integration work at [TechSpokes](https://www.techspokes.com); built for teams modernizing real enterprise SOAP integrations.
11
11
 
12
+ ## Upgrading to 1.3.0
13
+
14
+ Version 1.3.0 adds opt-in checked SOAP 1.2 document/literal execution through the existing adapter. Existing SOAP 1.1 and direct-runtime bindings retain their contracts. SOAP 1.2 clients use an additive runtime handshake; regenerate matched artifacts when adopting that binding. Read the [1.2.x migration guidance](docs/migration.md#from-12x-to-130) for adoption and evidence limits.
15
+
12
16
  ## Upgrading to 1.2.0
13
17
 
14
18
  Existing projects keep legacy generation unless they explicitly choose another profile. Regeneration can change affected DTO names, stream records and corrected scalar/simple-content results; 1.2.0 is not an unconditional drop-in regeneration upgrade. Read the [1.1.x migration guide](docs/migration.md#from-11x-to-120) before updating, including matched artifact regeneration, scaffold preservation, consumer checks and rollback.
@@ -250,6 +254,7 @@ See [CLI Reference](docs/cli-reference.md) for all flags and examples.
250
254
  | [Agent Skill Artifact](docs/agent-skill.md) | Release ZIP for consumer-project AI agents |
251
255
  | [Version Migration](docs/migration.md) | Upgrading between package versions |
252
256
  | [1.2.0 Release Notes](https://github.com/TechSpokes/typescript-wsdl-client/blob/main/docs/releases/v1.2.0.md) | Composition, consumer compatibility and bounded RPC qualification |
257
+ | [1.3.0 Release Notes](https://github.com/TechSpokes/typescript-wsdl-client/blob/main/docs/releases/v1.3.0.md) | Checked SOAP 1.2 execution and preserved SOAP 1.1 compatibility |
253
258
  | [XML/JSON Mapping Contract](docs/mapping-contract.md) | Checked keyed, namespace, nil and group representations |
254
259
  | [Product Corpus](docs/corpus.md) | Representative public inputs and their executable evidence |
255
260
 
@@ -1,4 +1,5 @@
1
- import { DIRECT_CLIENT_RUNTIME_VERSION, COMPONENT_CLIENT_RUNTIME_VERSION } from './directClientVersion.js';
1
+ import type { ComponentClientResult } from './directClientRuntime.js';
2
+ import { DIRECT_CLIENT_RUNTIME_VERSION, COMPONENT_CLIENT_RUNTIME_VERSION, COMPONENT_SOAP12_CLIENT_RUNTIME_VERSION } from './directClientVersion.js';
2
3
  interface Name {
3
4
  readonly namespace: string;
4
5
  readonly local: string;
@@ -93,4 +94,13 @@ export declare const checkedComponentRuntimeV1: Readonly<{
93
94
  version: typeof COMPONENT_CLIENT_RUNTIME_VERSION;
94
95
  execute(expectedBase64: string, input: unknown, options: ComponentOptions, version: typeof COMPONENT_CLIENT_RUNTIME_VERSION): Promise<Failure | Success>;
95
96
  }>;
97
+ /** Additive SOAP 1.2 binding permits protocol faults without declared application detail.
98
+ * @since 1.3.0
99
+ * @remarks Source reconstruction, endpoint restrictions and native validation use the shared component runtime.
100
+ * @returns Native component values, SOAP 1.2 protocol faults or explicit refusal; no full-profile admission.
101
+ */
102
+ export declare const checkedComponentRuntimeV2: Readonly<{
103
+ version: typeof COMPONENT_SOAP12_CLIENT_RUNTIME_VERSION;
104
+ execute(expectedBase64: string, input: unknown, options: ComponentOptions, version: typeof COMPONENT_SOAP12_CLIENT_RUNTIME_VERSION): Promise<ComponentClientResult>;
105
+ }>;
96
106
  export {};
@@ -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, componentSoap12ClientRuntime } from './directClientRuntime.js';
3
+ import { DIRECT_CLIENT_RUNTIME_VERSION, DIRECT_CLIENT_RUNTIME_REPAIR, COMPONENT_CLIENT_RUNTIME_VERSION, COMPONENT_CLIENT_RUNTIME_REPAIR, 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,20 @@ export const checkedComponentRuntimeV1 = Object.freeze({
41
41
  return componentClientRuntime.execute(expectedBase64, input, options);
42
42
  },
43
43
  });
44
+ /** Additive SOAP 1.2 binding permits protocol faults without declared application detail.
45
+ * @since 1.3.0
46
+ * @remarks Source reconstruction, endpoint restrictions and native validation use the shared component runtime.
47
+ * @returns Native component values, SOAP 1.2 protocol faults or explicit refusal; no full-profile admission.
48
+ */
49
+ export const checkedComponentRuntimeV2 = Object.freeze({
50
+ version: COMPONENT_SOAP12_CLIENT_RUNTIME_VERSION,
51
+ async execute(expectedBase64, input, options, version) {
52
+ if (version !== COMPONENT_SOAP12_CLIENT_RUNTIME_VERSION)
53
+ return Object.freeze({ kind: 'incompatible-artifact',
54
+ rule: 'generated-runtime-version', repair: COMPONENT_CLIENT_RUNTIME_REPAIR, usage: Object.freeze({ nodes: 0, work: 0 }) });
55
+ if (!dataOptions(options, ['localSource', 'fileRoots', 'capsule', 'soap', 'sourceResources', 'sourceOrigins', 'sourceUri', 'namespaceLocations', 'standardLibraries', 'recordSources'])) {
56
+ return Object.freeze({ kind: 'unsupported', rule: 'data-only checked component options required', usage: Object.freeze({ nodes: 0, work: 0 }) });
57
+ }
58
+ return componentSoap12ClientRuntime.execute(expectedBase64, input, options);
59
+ },
60
+ });
@@ -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 } 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 = Exclude<DirectClientResult, {
72
+ kind: 'soap-fault';
73
+ }> | ComponentProtocolFault | 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,10 @@ 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
+ }>;
@@ -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_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,14 @@ 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, true),
28
+ });
29
+ async function executeClient(expectedBase64, input, options, component, allowSoap12 = false) {
23
30
  // Constant-sized entry introspection precedes budget configuration and all reads.
24
31
  const entryFailure = () => ({ kind: 'unsupported', rule: 'data-only client options required', usage: { nodes: 0, work: 0 } });
25
32
  if (!options || typeof options !== 'object' || types.isProxy(options)
@@ -121,6 +128,8 @@ async function executeClient(expectedBase64, input, options, component) {
121
128
  standardLibraries: sources.standards, recordSources: sources.records }, budget);
122
129
  if (fresh.kind !== 'reconstructed-component')
123
130
  return fresh;
131
+ if (fresh.contract.binding.version === '1.2' && !allowSoap12)
132
+ return fail('incompatible-artifact', 'SOAP 1.2 runtime binding required');
124
133
  if (fresh.selection.port !== options.soap.port || fresh.selection.service.namespace !== options.soap.service.namespace
125
134
  || fresh.selection.service.local !== options.soap.service.local)
126
135
  return fail('incompatible-artifact', 'explicit source selection mismatch');
@@ -138,8 +147,11 @@ async function executeClient(expectedBase64, input, options, component) {
138
147
  // by the original adapter; no catch-and-cast turns a failure into typed success.
139
148
  result = await exchangeDirectSoap(fresh.contract, fresh.mappings[0], input, options.soap);
140
149
  }
141
- if (!('decoded' in result))
150
+ if (!('decoded' in result)) {
151
+ if (result.kind === 'soap-fault' && !allowSoap12)
152
+ return fail('incompatible-artifact', 'SOAP 1.2 runtime binding required');
142
153
  return result;
154
+ }
143
155
  if (options.soap.signal?.aborted)
144
156
  return fail('cancelled', 'cooperative-cancellation');
145
157
  const common = { value: result.decoded.validated.value, status: result.status, admitted: false,
@@ -3,4 +3,6 @@ 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";
6
8
  export declare const COMPONENT_CLIENT_RUNTIME_REPAIR = "Regenerate matched component client and source artifacts with the installed runtime.";
@@ -3,4 +3,6 @@ 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';
6
8
  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_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_CLIENT_RUNTIME_VERSION;
173
175
  const lines = [
174
- '// Checked component client; inject checkedComponentRuntimeV1 from the package root.',
176
+ `// Checked component client; inject ${soap12 ? 'checkedComponentRuntimeV2' : 'checkedComponentRuntimeV1'} 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${soap12 ? '?' : ''}: 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${soap12 ? '?' : ''}: 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
+ : faults.length ? [] : [' | (Common & {readonly kind: "soap-fault"; readonly value: never; readonly fault: {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'));
@@ -40,7 +40,7 @@ function runMapping(contract, supplied, options, profile, inspect, work) {
40
40
  && contract.binding.style === 'rpc' && contract.binding.use === 'encoded' && contract.binding.encoding === 'http://schemas.xmlsoap.org/soap/encoding/';
41
41
  if ((!rpc && mapping.version !== profile.version) || mapping.contextId !== contract.contextId
42
42
  || contract.basis !== profile.basis || contract.correspondence !== profile.correspondence
43
- || contract.binding.version !== '1.1' || !rpc && (contract.binding.style !== 'document' || contract.binding.use !== 'literal')
43
+ || !(contract.binding.version === '1.1' || profile === componentProfile && contract.binding.version === '1.2' && !rpc) || !rpc && (contract.binding.style !== 'document' || contract.binding.use !== 'literal')
44
44
  || contract.binding.transport !== 'http://schemas.xmlsoap.org/soap/http')
45
45
  throw new Stop('unsupported', profile.rule);
46
46
  const direction = mapping.direction;
@@ -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,
@@ -21,25 +21,55 @@ export interface DirectSoapFailure {
21
21
  readonly usage: ResolverUsage;
22
22
  readonly phase?: 'input' | 'response';
23
23
  }
24
- type SoapResult<D> = DirectSoapFailure | Readonly<{
24
+ /** Version-specific protocol metadata; application detail remains owned by native mappings.
25
+ * @since 1.3.0
26
+ */
27
+ export interface Soap12FaultMetadata {
28
+ readonly version: '1.2';
29
+ readonly code: ExpandedName;
30
+ readonly subcodes: readonly ExpandedName[];
31
+ readonly reasons: readonly Readonly<{
32
+ lang: string;
33
+ text: string;
34
+ }>[];
35
+ readonly string: string;
36
+ readonly node?: string;
37
+ readonly role?: string;
38
+ }
39
+ export type DeclaredSoapFault = Readonly<{
40
+ name: string;
41
+ code: ExpandedName;
42
+ codeNode: SyntaxElement;
43
+ string: string;
44
+ actor?: string;
45
+ }> & Partial<Soap12FaultMetadata>;
46
+ /** A valid SOAP 1.2 fault without a uniquely declared application detail.
47
+ * @since 1.3.0
48
+ * @constraints Unassessed detail remains only in responseBytes; no native value receipt is issued.
49
+ */
50
+ export interface ComponentProtocolFault {
51
+ readonly kind: 'soap-fault';
52
+ readonly admitted: false;
53
+ readonly fault: Soap12FaultMetadata;
54
+ readonly responseBytes: Uint8Array;
55
+ readonly status: number;
56
+ readonly usage: ResolverUsage;
57
+ readonly ignoredHeaders?: readonly ExpandedName[];
58
+ }
59
+ type DecodedSoapResult<D> = DirectSoapFailure | Readonly<{
25
60
  kind: 'soap-response' | 'soap-fault';
26
61
  admitted: false;
27
62
  decoded: D;
28
63
  /** Actual bounded HTTP entity bytes, before any decoding; caller-owned data. */
29
64
  responseBytes: Uint8Array;
30
65
  status: number;
31
- fault?: 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. */
66
+ fault?: DeclaredSoapFault;
67
+ /** Unprocessed header blocks; their application semantics were not assessed. */
39
68
  ignoredHeaders?: readonly ExpandedName[];
40
69
  usage: ResolverUsage;
41
70
  }>;
42
- export type DirectSoapResult = SoapResult<DirectDecodedXml>;
71
+ type SoapResult<D> = DecodedSoapResult<D> | ComponentProtocolFault;
72
+ export type DirectSoapResult = DecodedSoapResult<DirectDecodedXml>;
43
73
  export type ComponentSoapResult = SoapResult<ComponentDecodedXml>;
44
74
  export type ComponentRecordSoapResult = SoapResult<ComponentDecodedXml | ComponentRecordDecodedXml>;
45
75
  export interface ComponentSoapOptions extends Omit<DirectSoapOptions, 'endpoint' | 'allowEndpoint'> {
@@ -1,11 +1,11 @@
1
1
  import { isApplicationUnmetered } from './semanticBudget.js';
2
2
  import { directDataOwnership, directLimit } from './directExecutionPolicy.js';
3
- /** Shared retained-operation SOAP 1.1 exchange; profile-specific authority stays explicit. */
3
+ /** Shared retained-operation SOAP exchange; profile-specific authority stays explicit. */
4
4
  import { checkSemanticBudget } from './semanticBudget.js';
5
5
  import { DataOwnershipLimitError } from './dataOwnership.js';
6
6
  import { Agent } from 'node:http';
7
7
  import { Agent as HttpsAgent } from 'node:https';
8
- import { types } from 'node:util';
8
+ import { types, MIMEType } from 'node:util';
9
9
  import { WSDL } from 'soap/lib/wsdl/index.js';
10
10
  import { Client } from 'soap/lib/client.js';
11
11
  import { HttpClient } from 'soap/lib/http.js';
@@ -16,7 +16,7 @@ import { prepareDirectSoapSource, prepareComponentSoapWsdl, DirectSoapSourceErro
16
16
  import { isXmlNCName, resolveLexicalQName, SchemaLoadingError, syntaxElements, syntaxAttribute, WSDL_NAMESPACE } from '../loader/orderedSyntax.js';
17
17
  import { decodeComponentRecordXml, inspectComponentRecordProjection } from './componentRecordProjection.js';
18
18
  import { assessComponentSoapHeader } from './soapHeaders.js';
19
- const SOAP = 'http://schemas.xmlsoap.org/soap/envelope/';
19
+ const SOAP11 = 'http://schemas.xmlsoap.org/soap/envelope/';
20
20
  class Stop extends Error {
21
21
  kind;
22
22
  rule;
@@ -44,7 +44,7 @@ export function directSoapOptionsShape(options, check, additional = []) {
44
44
  /** Authentication and fresh-value validation precede dependency bootstrap and all HTTP I/O. */
45
45
  export async function exchangeDirectSoap(contract, inputMapping, application, options) {
46
46
  const input = inspectDirectValueMapping(inputMapping, contract);
47
- return exchangeSoap(contract, input, application, options, {
47
+ const result = await exchangeSoap(contract, input, application, options, {
48
48
  component: false, encode: encodeDirectXml, decode: decodeDirectXml, create: createDirectValueMapping,
49
49
  prepare: async (selection, check) => {
50
50
  const { source } = inspectDirectOperation(contract);
@@ -54,6 +54,9 @@ export async function exchangeDirectSoap(contract, inputMapping, application, op
54
54
  return wsdl;
55
55
  },
56
56
  });
57
+ if (result.kind === 'soap-fault' && !('decoded' in result))
58
+ throw new TypeError('Component fault on direct profile');
59
+ return result;
57
60
  }
58
61
  export async function exchangeComponentSoap(contract, inputMapping, application, options, responseProjection) {
59
62
  const { mapping: input } = inspectComponentValueMapping(inputMapping, contract);
@@ -89,6 +92,8 @@ export async function exchangeComponentSoap(contract, inputMapping, application,
89
92
  });
90
93
  }
91
94
  async function exchangeSoap(contract, input, application, options, profile) {
95
+ const soap12 = profile.component && contract.binding.version === '1.2';
96
+ const SOAP = soap12 ? 'http://www.w3.org/2003/05/soap-envelope' : SOAP11;
92
97
  const { context } = inspectDirectOperation(contract);
93
98
  if (input.direction.kind !== 'input')
94
99
  throw new TypeError('SOAP request requires the exact input mapping');
@@ -120,7 +125,7 @@ async function exchangeSoap(contract, input, application, options, profile) {
120
125
  const { source } = inspectDirectOperation(contract);
121
126
  const services = source.documents.flatMap(d => syntaxElements(d.root, WSDL_NAMESPACE, 'service').filter(n => syntaxAttribute(d.root, 'targetNamespace') === options.service?.namespace && syntaxAttribute(n, 'name') === options.service?.local));
122
127
  const ports = services.flatMap(n => syntaxElements(n, WSDL_NAMESPACE, 'port').filter(p => syntaxAttribute(p, 'name') === options.port));
123
- const addresses = ports.flatMap(p => syntaxElements(p, 'http://schemas.xmlsoap.org/wsdl/soap/', 'address'));
128
+ const addresses = ports.flatMap(p => syntaxElements(p, soap12 ? 'http://schemas.xmlsoap.org/wsdl/soap12/' : 'http://schemas.xmlsoap.org/wsdl/soap/', 'address'));
124
129
  if (services.length !== 1 || ports.length !== 1 || addresses.length !== 1 || !syntaxAttribute(addresses[0], 'location')) {
125
130
  return fail('unsupported', 'Select one SOAP service/port address or supply an endpoint override');
126
131
  }
@@ -202,11 +207,11 @@ async function exchangeSoap(contract, input, application, options, profile) {
202
207
  const ignoredHeaders = [];
203
208
  const bodyRoot = (root, incoming = false) => {
204
209
  if (!same(root.name, { namespace: SOAP, local: 'Envelope' }))
205
- stop('protocol', 'SOAP 1.1 Envelope required');
210
+ stop('protocol', `SOAP ${contract.binding.version} Envelope required`);
206
211
  const children = shape(root, rpc);
207
212
  if (children[0]?.name.namespace === SOAP && children[0].name.local === 'Header') {
208
213
  if (profile.component && incoming) {
209
- const assessed = assessComponentSoapHeader(children[0], check);
214
+ const assessed = assessComponentSoapHeader(children[0], check, contract.binding.version);
210
215
  if (assessed.kind !== 'ignored-optional-headers')
211
216
  stop(assessed.kind, assessed.rule);
212
217
  else
@@ -255,7 +260,8 @@ async function exchangeSoap(contract, input, application, options, profile) {
255
260
  request(url, data, callback, headers, _options) {
256
261
  try {
257
262
  check();
258
- if (url !== endpoint.href || headers.SOAPAction !== `"${action}"` || headers['Content-Type'] !== 'text/xml; charset=utf-8')
263
+ if (url !== endpoint.href || (soap12 ? headers.SOAPAction !== undefined || headers['Content-Type'] !== requestContentType
264
+ : headers.SOAPAction !== `"${action}"` || headers['Content-Type'] !== 'text/xml; charset=utf-8'))
259
265
  stop('protocol', 'dependency dispatch mismatch');
260
266
  if (Buffer.byteLength(data) > limits.maxBytes)
261
267
  stop('resource-limit', 'SOAP request byte limit');
@@ -294,7 +300,8 @@ async function exchangeSoap(contract, input, application, options, profile) {
294
300
  maxBodyLength: limits.maxBytes, proxy: false, httpAgent: agent, httpsAgent: agent, signal: controller.signal, timeout: config.timeoutMs });
295
301
  }
296
302
  }
297
- const client = new Client(wsdl, endpoint.href, { httpClient: new BufferedHttp() });
303
+ const requestContentType = soap12 ? 'application/soap+xml; charset=utf-8' + (action ? `; action="${action}"` : '') : 'text/xml; charset=utf-8';
304
+ const client = new Client(wsdl, endpoint.href, { httpClient: new BufferedHttp(), ...(soap12 ? { forceSoap12Headers: true } : {}) });
298
305
  // Every lookup is own and explicit; the dependency's first-port shortcut is
299
306
  // never consulted. Source checks above bind these names to the native handle.
300
307
  const own = (object, key) => Object.getOwnPropertyDescriptor(object, key)?.value;
@@ -304,7 +311,7 @@ async function exchangeSoap(contract, input, application, options, profile) {
304
311
  if (typeof method !== 'function')
305
312
  return fail('unsupported', 'dependency selected method unavailable');
306
313
  check();
307
- await new Promise((resolve, reject) => method(rpc ? rpcArguments : { _xml: Buffer.from(encoded.bytes).toString('utf8') }, (error) => error === complete ? resolve() : reject(error), {}, { SOAPAction: `"${action}"` }));
314
+ await new Promise((resolve, reject) => method(rpc ? rpcArguments : { _xml: Buffer.from(encoded.bytes).toString('utf8') }, (error) => error === complete ? resolve() : reject(error), {}, soap12 ? { 'Content-Type': requestContentType } : { SOAPAction: `"${action}"` }));
308
315
  check();
309
316
  if (timedOut)
310
317
  return fail('transport', 'SOAP deadline exceeded');
@@ -315,10 +322,20 @@ async function exchangeSoap(contract, input, application, options, profile) {
315
322
  const observed = response;
316
323
  if (observed.encoding && observed.encoding.toLowerCase() !== 'identity')
317
324
  return fail('unsupported', 'HTTP compression');
318
- const media = /^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);
325
+ let soap12Charset;
326
+ let validSoap12Media = false;
327
+ if (soap12) {
328
+ try {
329
+ const mime = new MIMEType(observed.contentType);
330
+ soap12Charset = mime.params.get('charset')?.toLowerCase();
331
+ validSoap12Media = mime.essence === 'application/soap+xml' && (!soap12Charset || ['utf-8', 'utf-16', 'utf-16le', 'utf-16be', 'iso-8859-1'].includes(soap12Charset));
332
+ }
333
+ catch { /* Invalid media types are refused below. */ }
334
+ }
335
+ const media = soap12 ? validSoap12Media ? ['', soap12Charset] : null : /^text\/xml(?:\s*;\s*charset\s*=\s*(?:"(utf-8|utf-16|utf-16le|utf-16be|iso-8859-1)"|(utf-8|utf-16|utf-16le|utf-16be|iso-8859-1)))?\s*$/i.exec(observed.contentType);
319
336
  if (!media)
320
337
  return fail('protocol', 'SOAP response Content-Type');
321
- if (observed.status !== 200 && observed.status !== 500)
338
+ if (observed.status !== 200 && observed.status !== 500 && !(soap12 && observed.status === 400))
322
339
  return fail('transport', 'SOAP HTTP status');
323
340
  // Header charset is a protocol assertion, checked against actual BOM bytes.
324
341
  const charset = (media[1] ?? media[2])?.toLowerCase();
@@ -329,7 +346,9 @@ async function exchangeSoap(contract, input, application, options, profile) {
329
346
  if (!profile.component && charset && charset !== bom && !(charset === 'utf-16' && bom.startsWith('utf-16')))
330
347
  return fail('protocol', 'SOAP charset disagrees with bytes');
331
348
  let faultInfo;
332
- const decoded = profile.decode(contract, output, observed.bytes, { signal, limits, uri: endpoint.href,
349
+ let protocolFault;
350
+ const protocolFaultSignal = new Error('SOAP 1.2 protocol fault assessed');
351
+ const decodeResponse = () => profile.decode(contract, output, observed.bytes, { signal, limits, uri: endpoint.href,
333
352
  ...(profile.component ? { componentResponseEncoding: { charset } } : {}),
334
353
  selectPayload: root => {
335
354
  const payload = bodyRoot(root, true);
@@ -338,6 +357,87 @@ async function exchangeSoap(contract, input, application, options, profile) {
338
357
  stop('protocol', 'HTTP 500 requires SOAP Fault');
339
358
  return { payload, mapping: output };
340
359
  }
360
+ if (soap12) {
361
+ const children = shape(payload);
362
+ const names = children.map(n => n.name.namespace === SOAP ? n.name.local : '');
363
+ if (!/^Code,Reason(?:,Node)?(?:,Role)?(?:,Detail)?$/.test(names.join(',')))
364
+ stop('protocol', 'SOAP 1.2 Fault children');
365
+ const scalar = (node, language = false) => {
366
+ check();
367
+ if (elements(node).length || node.attributes.some(a => !(language && a.name.namespace === 'http://www.w3.org/XML/1998/namespace' && a.name.local === 'lang')))
368
+ stop('protocol', 'SOAP 1.2 Fault scalar child');
369
+ return node.children.map(n => n.kind === 'text' ? n.value : '').join('');
370
+ };
371
+ const qname = (node) => {
372
+ const lexical = scalar(node);
373
+ try {
374
+ const name = resolveLexicalQName(lexical, node);
375
+ return name;
376
+ }
377
+ catch (error) {
378
+ if (error instanceof SchemaLoadingError)
379
+ return stop('protocol', 'SOAP Fault code QName');
380
+ throw error;
381
+ }
382
+ };
383
+ const codes = [];
384
+ let codeNode;
385
+ let codeContainer = children[0];
386
+ while (codeContainer) {
387
+ check();
388
+ const nested = shape(codeContainer);
389
+ if (nested.length < 1 || nested.length > 2 || !same(nested[0].name, { namespace: SOAP, local: 'Value' })
390
+ || nested[1] && !same(nested[1].name, { namespace: SOAP, local: 'Subcode' }))
391
+ stop('protocol', 'SOAP 1.2 Fault Code');
392
+ codeNode ??= nested[0];
393
+ codes.push(qname(nested[0]));
394
+ codeContainer = nested[1];
395
+ }
396
+ const code = codes[0];
397
+ if (code.namespace !== SOAP || !['VersionMismatch', 'MustUnderstand', 'DataEncodingUnknown', 'Sender', 'Receiver'].includes(code.local))
398
+ stop('protocol', 'SOAP 1.2 Fault code');
399
+ if (observed.status === 400 && code.local !== 'Sender')
400
+ stop('protocol', 'HTTP 400 requires SOAP Sender Fault');
401
+ const reasonChildren = shape(children[1]);
402
+ if (!reasonChildren.length)
403
+ stop('protocol', 'SOAP 1.2 Fault Reason');
404
+ const reasons = reasonChildren.map(node => {
405
+ check();
406
+ if (!same(node.name, { namespace: SOAP, local: 'Text' }))
407
+ stop('protocol', 'SOAP 1.2 Fault Reason Text');
408
+ const text = scalar(node, true), lang = syntaxAttribute(node, 'lang', 'http://www.w3.org/XML/1998/namespace');
409
+ if (lang === undefined || !/^[A-Za-z]{1,8}(?:-[A-Za-z0-9]{1,8})*$/.test(lang))
410
+ stop('protocol', 'SOAP 1.2 Fault language');
411
+ return { lang: lang, text };
412
+ });
413
+ const uriField = (local) => {
414
+ const node = children.find(n => n.name.local === local);
415
+ if (!node)
416
+ return undefined;
417
+ const value = scalar(node).trim();
418
+ if (!/^[A-Za-z][A-Za-z0-9+.-]*:[^\s]*$/.test(value))
419
+ stop('protocol', 'SOAP 1.2 Fault URI');
420
+ return value;
421
+ };
422
+ const node = uriField('Node'), role = uriField('Role');
423
+ const metadata = { version: '1.2', code, subcodes: codes.slice(1), reasons, string: reasons[0].text,
424
+ ...(node === undefined ? {} : { node }), ...(role === undefined ? {} : { role }) };
425
+ const detailNode = children.find(n => n.name.local === 'Detail');
426
+ // @see SOAP 1.2 section 5.4.5 permits attributes on Detail; these protocol-wrapper attributes stay in the original bytes.
427
+ if (detailNode?.children.some(n => n.kind === 'text' && /[^\t\r\n ]/.test(n.value)))
428
+ stop('protocol', 'SOAP 1.2 Fault Detail text');
429
+ const detail = detailNode ? elements(detailNode) : [];
430
+ const matches = contract.faults.filter(f => detail.some(n => same(f.root.element.name, n.name)));
431
+ if (matches.length) {
432
+ if (matches.length !== 1 || detail.length !== 1)
433
+ stop('protocol', 'ambiguous SOAP Fault detail');
434
+ faultInfo = directDataOwnership(budget).freeze({ ...metadata, name: matches[0].name, codeNode: codeNode });
435
+ return { payload: detail[0], mapping: faults.get(faultInfo.name) };
436
+ }
437
+ protocolFault = directDataOwnership(budget).freeze(metadata);
438
+ // @constraints Only protocol grammar was assessed; no fabricated application receipt is published.
439
+ throw protocolFaultSignal;
440
+ }
341
441
  const children = shape(payload);
342
442
  const names = children.map(n => n.name.namespace === '' ? n.name.local : '');
343
443
  if (names.join(',') !== 'faultcode,faultstring,detail' && names.join(',') !== 'faultcode,faultstring,faultactor,detail')
@@ -370,6 +470,22 @@ async function exchangeSoap(contract, input, application, options, profile) {
370
470
  directDataOwnership(budget).freeze(faultInfo);
371
471
  return { payload: detail[0], mapping: faults.get(faultInfo.name) };
372
472
  } });
473
+ let decoded;
474
+ try {
475
+ decoded = decodeResponse();
476
+ }
477
+ catch (error) {
478
+ if (error !== protocolFaultSignal)
479
+ throw error;
480
+ }
481
+ if (protocolFault && decoded === undefined) {
482
+ check();
483
+ return Object.freeze({ kind: 'soap-fault', admitted: false, fault: protocolFault,
484
+ responseBytes: new Uint8Array(observed.bytes), status: observed.status,
485
+ ...(ignoredHeaders.length ? { ignoredHeaders: Object.freeze(ignoredHeaders) } : {}), usage: Object.freeze({ ...budget.usage() }) });
486
+ }
487
+ if (!decoded)
488
+ throw new TypeError('Missing decoded SOAP response');
373
489
  if (!('validated' in decoded))
374
490
  return fail(decoded.kind, decoded.rule, 'response');
375
491
  check();
@@ -16,6 +16,7 @@ export class DirectSoapSourceError extends Error {
16
16
  }
17
17
  }
18
18
  function validateSoapSelection(source, contract, config, check) {
19
+ const addressNamespace = contract.binding.version === '1.2' ? 'http://schemas.xmlsoap.org/wsdl/soap12/' : WSDL_SOAP;
19
20
  const elements = (node) => { check(); return node.children.filter((child) => child.kind === 'element'); };
20
21
  const definitions = source.root.root;
21
22
  if (definitions.name.namespace !== WSDL_NAMESPACE || definitions.name.local !== 'definitions')
@@ -30,8 +31,8 @@ function validateSoapSelection(source, contract, config, check) {
30
31
  const binding = syntaxAttribute(ports[0], 'binding');
31
32
  if (!binding || !same(resolveLexicalQName(binding, ports[0]), contract.selector.binding))
32
33
  throw new DirectSoapSourceError('SOAP binding mismatch');
33
- const addresses = elements(ports[0]).filter(n => n.name.namespace === WSDL_SOAP && n.name.local === 'address');
34
- if (addresses.length !== 1 || elements(ports[0]).some(n => !(n.name.namespace === WSDL_SOAP && n.name.local === 'address')
34
+ const addresses = elements(ports[0]).filter(n => n.name.namespace === addressNamespace && n.name.local === 'address');
35
+ if (addresses.length !== 1 || elements(ports[0]).some(n => !(n.name.namespace === addressNamespace && n.name.local === 'address')
35
36
  && !(n.name.namespace === WSDL_NAMESPACE && n.name.local === 'documentation')))
36
37
  throw new DirectSoapSourceError('SOAP port extension');
37
38
  }
@@ -316,6 +316,8 @@ export function checkDirectSourceCompatibility(expected, contract, selection, si
316
316
  const componentVersions = Object.freeze({ ...versions, scope: 'component-soap11-utf8-graph-v1', mapping: 'component-values-v1',
317
317
  runtimePolicy: 'explicit-http-verified-https-transport-v1' });
318
318
  const scopedComponentVersions = Object.freeze({ ...componentVersions, scope: 'component-soap11-utf8-graph-v2', mapping: 'component-values-and-record-scope-v1' });
319
+ const soap12ComponentVersions = Object.freeze({ ...componentVersions, scope: 'component-soap12-document-literal-v1' });
320
+ const scopedSoap12ComponentVersions = Object.freeze({ ...scopedComponentVersions, scope: 'component-soap12-document-literal-record-v1' });
319
321
  const rpcComponentVersions = Object.freeze({ ...componentVersions, scope: 'component-soap11-rpc-strings-v1', mapping: 'component-rpc-string-values-v1' });
320
322
  const typedRpcComponentVersions = Object.freeze({ ...componentVersions, scope: 'component-soap11-rpc-typed-latin1-v1', mapping: 'component-rpc-typed-values-v1' });
321
323
  const componentReconstructions = new WeakMap();
@@ -380,6 +382,7 @@ async function componentDescriptor(contract, selection, signal, responseProjecti
380
382
  const input = make({ kind: 'input' }), output = make({ kind: 'output' });
381
383
  const faults = contract.faults.map(fault => ({ name: fault.name, root: make({ kind: 'fault', name: fault.name }) }));
382
384
  const compatibility = { selector: contract.selector, service: owned.service, port: owned.port,
385
+ ...(contract.binding.version === '1.2' ? { binding: { version: '1.2', actionRequired: contract.binding.actionRequired } } : {}),
383
386
  action: contract.binding.action, input, output, faults };
384
387
  let projection = null;
385
388
  if (responseProjection) {
@@ -431,7 +434,7 @@ function componentCapsule(derived, budget) {
431
434
  const typedRpc = rpc && compatibility.input.parts.some(part => part.type.namespace !== 'http://www.w3.org/2001/XMLSchema' || part.type.local !== 'string');
432
435
  if (rpc && (extended || derived.applicationGroups))
433
436
  throw new Stop('unsupported', 'rpc-artifact-extension');
434
- const value = { versions: typedRpc ? typedRpcComponentVersions : rpc ? rpcComponentVersions : extended ? scopedComponentVersions : componentVersions, resources, compatibility,
437
+ const value = { versions: compatibility.binding?.version === '1.2' ? extended ? scopedSoap12ComponentVersions : soap12ComponentVersions : typedRpc ? typedRpcComponentVersions : rpc ? rpcComponentVersions : extended ? scopedComponentVersions : componentVersions, resources, compatibility,
435
438
  ...(extended ? { libraries, projection } : {}), ...(derived.applicationGroups ? { applicationGroups: derived.applicationGroups, applicationGroupIdentity: derived.applicationGroupIdentity } : {}) };
436
439
  return { ...value, fingerprint: hash(json(value, budget)) };
437
440
  }
@@ -493,7 +496,8 @@ function readComponent(bytes, budget, signal) {
493
496
  const rpc = typedRpc || !!parsed.versions && typeof parsed.versions === 'object' && parsed.versions.scope === rpcComponentVersions.scope;
494
497
  if (rpc && (extended || grouped))
495
498
  reject('rpc-artifact-extension');
496
- const expectedVersions = typedRpc ? typedRpcComponentVersions : rpc ? rpcComponentVersions : extended ? scopedComponentVersions : componentVersions;
499
+ const soap12 = !!parsed.versions && typeof parsed.versions === 'object' && ['component-soap12-document-literal-v1', 'component-soap12-document-literal-record-v1'].includes(parsed.versions.scope);
500
+ const expectedVersions = soap12 ? extended ? scopedSoap12ComponentVersions : soap12ComponentVersions : typedRpc ? typedRpcComponentVersions : rpc ? rpcComponentVersions : extended ? scopedComponentVersions : componentVersions;
497
501
  const actual = record(parsed.versions, Object.keys(expectedVersions));
498
502
  for (const key of Object.keys(expectedVersions))
499
503
  if (actual[key] !== expectedVersions[key])
@@ -564,7 +568,12 @@ function readComponent(bytes, budget, signal) {
564
568
  if (!parsed.libraries.length && parsed.projection === null)
565
569
  reject('empty-component-extension');
566
570
  }
567
- const c = record(parsed.compatibility, ['selector', 'service', 'port', 'action', 'input', 'output', 'faults']);
571
+ const c = record(parsed.compatibility, ['selector', 'service', 'port', 'action', 'input', 'output', 'faults', ...(soap12 ? ['binding'] : [])]);
572
+ if (soap12) {
573
+ const binding = record(c.binding, ['version', 'actionRequired']);
574
+ if (binding.version !== '1.2' || typeof binding.actionRequired !== 'boolean')
575
+ reject('soap12-binding');
576
+ }
568
577
  selector(c.selector);
569
578
  name(c.service);
570
579
  ncname(c.port);
@@ -1,4 +1,4 @@
1
- /** SOAP 1.1 response-header clauses for the component transport's default ultimate-receiver role. */
1
+ /** SOAP response-header clauses for the component transport's default ultimate-receiver role. */
2
2
  import type { ExpandedName, SyntaxElement } from '../loader/orderedSyntax.js';
3
3
  export type ComponentSoapHeaderResult = Readonly<{
4
4
  kind: 'ignored-optional-headers';
@@ -9,8 +9,14 @@ export type ComponentSoapHeaderResult = Readonly<{
9
9
  }>;
10
10
  /** Assess borrowed parsed Header syntax without claiming unknown block semantics.
11
11
  * @remarks SOAP 1.1 sections 4.2 through 4.2.3 require qualified block names and define absent mustUnderstand
12
- * as 0. Only immediate block attributes govern header processing. Actor-directed processing and mandatory
13
- * block semantics remain unsupported; no security/header emission capability is acquired here.
12
+ * as 0. SOAP 1.2 additionally defines boolean lexicals and role/relay attributes. Only immediate block
13
+ * attributes govern processing. Unknown mandatory blocks targeted at this receiver remain unsupported.
14
+ * @param header Parsed Header in the selected envelope namespace.
15
+ * @param check Invocation-owned cancellation and work accounting.
16
+ * @param version Selected SOAP binding version; omission retains SOAP 1.1 compatibility.
17
+ * @returns Unprocessed block names or an explicit protocol/capability refusal.
18
+ * @throws Propagates caller cancellation and resource exceptions from check.
14
19
  * @see https://www.w3.org/TR/2000/NOTE-SOAP-20000508/
20
+ * @see https://www.w3.org/TR/soap12-part1/#soapheadblock
15
21
  */
16
- export declare function assessComponentSoapHeader(header: SyntaxElement, check: () => void): ComponentSoapHeaderResult;
22
+ export declare function assessComponentSoapHeader(header: SyntaxElement, check: () => void, version?: '1.1' | '1.2'): ComponentSoapHeaderResult;
@@ -1,13 +1,20 @@
1
1
  const SOAP = 'http://schemas.xmlsoap.org/soap/envelope/';
2
2
  /** Assess borrowed parsed Header syntax without claiming unknown block semantics.
3
3
  * @remarks SOAP 1.1 sections 4.2 through 4.2.3 require qualified block names and define absent mustUnderstand
4
- * as 0. Only immediate block attributes govern header processing. Actor-directed processing and mandatory
5
- * block semantics remain unsupported; no security/header emission capability is acquired here.
4
+ * as 0. SOAP 1.2 additionally defines boolean lexicals and role/relay attributes. Only immediate block
5
+ * attributes govern processing. Unknown mandatory blocks targeted at this receiver remain unsupported.
6
+ * @param header Parsed Header in the selected envelope namespace.
7
+ * @param check Invocation-owned cancellation and work accounting.
8
+ * @param version Selected SOAP binding version; omission retains SOAP 1.1 compatibility.
9
+ * @returns Unprocessed block names or an explicit protocol/capability refusal.
10
+ * @throws Propagates caller cancellation and resource exceptions from check.
6
11
  * @see https://www.w3.org/TR/2000/NOTE-SOAP-20000508/
12
+ * @see https://www.w3.org/TR/soap12-part1/#soapheadblock
7
13
  */
8
- export function assessComponentSoapHeader(header, check) {
14
+ export function assessComponentSoapHeader(header, check, version = '1.1') {
9
15
  check();
10
- if (header.name.namespace !== SOAP || header.name.local !== 'Header' || header.attributes.length)
16
+ const namespace = version === '1.2' ? 'http://www.w3.org/2003/05/soap-envelope' : SOAP;
17
+ if (header.name.namespace !== namespace || header.name.local !== 'Header' || header.attributes.length)
11
18
  return Object.freeze({ kind: 'protocol', rule: 'SOAP Header shape' });
12
19
  const headers = [];
13
20
  for (const child of header.children) {
@@ -20,21 +27,33 @@ export function assessComponentSoapHeader(header, check) {
20
27
  if (!child.name.namespace)
21
28
  return Object.freeze({ kind: 'protocol', rule: 'SOAP header block namespace required' });
22
29
  let mandatory = false;
30
+ let targeted = true;
23
31
  for (const attribute of child.attributes) {
24
32
  check();
25
- if (attribute.name.namespace !== SOAP)
33
+ if (attribute.name.namespace !== namespace)
26
34
  continue;
35
+ if (version === '1.2' && attribute.name.local === 'role') {
36
+ if (!/^[A-Za-z][A-Za-z0-9+.-]*:/.test(attribute.value))
37
+ return Object.freeze({ kind: 'protocol', rule: 'SOAP role URI' });
38
+ targeted = [namespace + '/role/next', namespace + '/role/ultimateReceiver'].includes(attribute.value);
39
+ continue;
40
+ }
41
+ if (version === '1.2' && attribute.name.local === 'relay') {
42
+ if (!['true', 'false', '1', '0'].includes(attribute.value))
43
+ return Object.freeze({ kind: 'protocol', rule: 'SOAP relay boolean' });
44
+ continue;
45
+ }
27
46
  if (attribute.name.local === 'actor')
28
47
  return Object.freeze({ kind: 'unsupported', rule: 'SOAP actor header capability required' });
29
48
  if (attribute.name.local === 'mustUnderstand') {
30
- if (attribute.value !== '0' && attribute.value !== '1')
31
- return Object.freeze({ kind: 'protocol', rule: 'SOAP 1.1 mustUnderstand value' });
32
- mandatory = attribute.value === '1';
49
+ if (!(version === '1.2' ? ['true', 'false', '1', '0'] : ['0', '1']).includes(attribute.value))
50
+ return Object.freeze({ kind: 'protocol', rule: `SOAP ${version} mustUnderstand value` });
51
+ mandatory = attribute.value === '1' || attribute.value === 'true';
33
52
  }
34
53
  else if (attribute.name.local !== 'encodingStyle')
35
54
  return Object.freeze({ kind: 'unsupported', rule: 'unknown SOAP header attribute' });
36
55
  }
37
- if (mandatory)
56
+ if (mandatory && targeted)
38
57
  return Object.freeze({ kind: 'unsupported', rule: 'incoming mandatory SOAP header' });
39
58
  headers.push(Object.freeze({ ...child.name }));
40
59
  }
@@ -22,7 +22,7 @@ async function generateGatewayFromReconstruction(expected, options) {
22
22
  throw new Error('Checked gateway generation cancelled');
23
23
  inspectReconstructedComponentSource(expected);
24
24
  const capsule = { bytes: componentSourceCapsuleBytes(expected, options.signal) };
25
- const runtime = 'checkedComponentRuntimeV1';
25
+ const runtime = expected.contract.binding.version === '1.2' ? 'checkedComponentRuntimeV2' : 'checkedComponentRuntimeV1';
26
26
  if (!options.routePath.startsWith('/') || options.routePath.split('/').slice(1)
27
27
  .some(segment => !segment || !/^(?:[A-Za-z0-9._~-]|%[0-9A-Fa-f]{2})+$/.test(segment))) {
28
28
  throw new TypeError('Checked routePath requires literal nonempty path segments');
@@ -40,6 +40,16 @@ async function generateGatewayFromReconstruction(expected, options) {
40
40
  }).schema;
41
41
  };
42
42
  const request = schema(input, 'Request'), response = schema(output, 'Response');
43
+ const soap12 = expected.contract.binding.version === '1.2';
44
+ const nameSchema = { type: 'object', additionalProperties: false, required: ['namespace', 'local'],
45
+ properties: { namespace: { type: 'string' }, local: { type: 'string' } } };
46
+ const protocolFaultProperties = {
47
+ version: { const: '1.2' }, code: nameSchema, string: { type: 'string' },
48
+ subcodes: { type: 'array', items: nameSchema },
49
+ reasons: { type: 'array', minItems: 1, items: { type: 'object', additionalProperties: false, required: ['lang', 'text'],
50
+ properties: { lang: { type: 'string' }, text: { type: 'string' } } } },
51
+ node: { type: 'string' }, role: { type: 'string' },
52
+ };
43
53
  const faults = expected.mappings.filter(mapping => mapping.direction.kind === 'fault').map(mapping => {
44
54
  if (mapping.direction.kind !== 'fault')
45
55
  throw new TypeError('Declared fault mapping required');
@@ -50,8 +60,8 @@ async function generateGatewayFromReconstruction(expected, options) {
50
60
  properties: {
51
61
  value: fault.value,
52
62
  fault: {
53
- type: 'object', additionalProperties: false, required: ['name', 'code', 'string'],
54
- properties: {
63
+ type: 'object', additionalProperties: false, required: ['name', 'code', 'string', ...(soap12 ? ['version', 'subcodes', 'reasons'] : [])],
64
+ properties: soap12 ? { ...protocolFaultProperties, name: { const: fault.name } } : {
55
65
  name: { const: fault.name }, string: { type: 'string' }, actor: { type: 'string' },
56
66
  code: { type: 'object', additionalProperties: false, required: ['namespace', 'local'],
57
67
  properties: { namespace: { type: 'string' }, local: { type: 'string' } } },
@@ -59,6 +69,11 @@ async function generateGatewayFromReconstruction(expected, options) {
59
69
  },
60
70
  },
61
71
  }));
72
+ if (soap12)
73
+ faultVariants.push({ type: 'object', additionalProperties: false, required: ['fault'], properties: {
74
+ fault: { type: 'object', additionalProperties: false, required: ['version', 'code', 'string', 'subcodes', 'reasons'],
75
+ properties: protocolFaultProperties },
76
+ } });
62
77
  const fault = faultVariants.length ? { oneOf: faultVariants } : { not: {} };
63
78
  const schemas = { request: { $schema: draft07, ...request }, response: { $schema: draft07, ...response }, fault: { $schema: draft07, ...fault } };
64
79
  const grouped = expected.mappings.some(mapping => mapping.kind === 'component-mapping'
@@ -97,7 +112,7 @@ async function generateGatewayFromReconstruction(expected, options) {
97
112
  openapi: '3.1.1', jsonSchemaDialect: 'https://json-schema.org/draft/2020-12/schema',
98
113
  info: { title: expected.selection.service.local, version: '1.0.0' },
99
114
  ...(grouped ? { components: { schemas: components } } : {}),
100
- 'x-checked-model': { model: 'checked-components-v1', scope: 'native-component-structured-soap11',
115
+ 'x-checked-model': { model: 'checked-components-v1', scope: soap12 ? 'native-component-structured-soap12' : 'native-component-structured-soap11',
101
116
  ...(expected.responseProjection
102
117
  ? { responseProjection: { spec: expected.responseProjection.spec, delivery: 'buffered', retainedValue: 'complete-companion-payload' } } : {}),
103
118
  documentationDialect: '2020-12', runtimeDialect: 'draft-07', jsonShapeComplete: false,
@@ -171,9 +186,14 @@ const plugin: FastifyPluginAsync<CheckedGatewayOptions> = async (fastify, option
171
186
  return reply.type('application/json').serializer(value => JSON.stringify(value)).send(result.value);
172
187
  }
173
188
  if (result.kind === 'soap-fault') {
174
- const fault = {name: result.fault.name, code: {namespace: result.fault.code.namespace, local: result.fault.code.local},
189
+ ${soap12 ? `const fault = {version: result.fault.version, code: result.fault.code, string: result.fault.string,
190
+ subcodes: result.fault.subcodes, reasons: result.fault.reasons,
191
+ ...(result.fault.name === undefined ? {} : {name: result.fault.name}),
192
+ ...(result.fault.node === undefined ? {} : {node: result.fault.node}),
193
+ ...(result.fault.role === undefined ? {} : {role: result.fault.role})};
194
+ const value = result.fault.name === undefined ? {fault} : {fault, value: result.value};` : `const fault = {name: result.fault.name, code: {namespace: result.fault.code.namespace, local: result.fault.code.local},
175
195
  string: result.fault.string, ...(result.fault.actor === undefined ? {} : {actor: result.fault.actor})};
176
- const value = {fault, value: result.value};
196
+ const value = {fault, value: result.value};`}
177
197
  if (!validateFault(value)) return reply.code(502).send({kind: 'value-invalid', rule: 'http-fault-shape'});
178
198
  return reply.code(502).type('application/json').serializer(value => JSON.stringify(value)).send(value);
179
199
  }
package/dist/index.d.ts CHANGED
@@ -15,7 +15,7 @@ export type { CheckedComponentPlan, ComponentPlanBlocker } from './checkedPlanni
15
15
  export type { ComponentRecordProjectionSpec, ComponentRecordProjectionAssociation } from './compiler/componentRecordProjection.js';
16
16
  export { loadStreamConfigFile, parseStreamConfig, StreamConfigError, type OperationStreamMetadata, type ShapeCatalogRef, type StreamConfig, } from "./util/streamConfig.js";
17
17
  export { applyShapeCatalogs, type ApplyShapeCatalogsOptions } from "./compiler/shapeResolver.js";
18
- export { checkedDirectRuntimeV1, checkedComponentRuntimeV1 } from './client/checkedDirectRuntime.js';
18
+ export { checkedDirectRuntimeV1, checkedComponentRuntimeV1, checkedComponentRuntimeV2 } from './client/checkedDirectRuntime.js';
19
19
  /**
20
20
  * Compiles a WSDL file to TypeScript client code
21
21
  *
package/dist/index.js CHANGED
@@ -32,7 +32,7 @@ export { composeOpenApiDocuments } from './openapi/composeOpenAPI.js';
32
32
  export { planCheckedComponents, formatCheckedComponentPlan, writeCheckedPlanningFile, CheckedPlanningError } from './checkedPlanning.js';
33
33
  export { loadStreamConfigFile, parseStreamConfig, StreamConfigError, } from "./util/streamConfig.js";
34
34
  export { applyShapeCatalogs } from "./compiler/shapeResolver.js";
35
- export { checkedDirectRuntimeV1, checkedComponentRuntimeV1 } from './client/checkedDirectRuntime.js';
35
+ export { checkedDirectRuntimeV1, checkedComponentRuntimeV1, checkedComponentRuntimeV2 } from './client/checkedDirectRuntime.js';
36
36
  // noinspection JSUnusedGlobalSymbols
37
37
  /**
38
38
  * Compiles a WSDL file to TypeScript client code
@@ -6,7 +6,7 @@ See the main [README](../README.md) for installation, CLI usage, and project ove
6
6
 
7
7
  All file-producing APIs automatically apply [TypeScript preambles](generated-code.md#generated-preambles-and-eslint). Adding a preamble does not change an API signature or option; existing scaffold skip/force behavior still applies. Resource and TypeScript file-write failures propagate to callers.
8
8
 
9
- Generated-output compatibility is versioned separately from those preambles. Read the [1.1.x-to-1.2.0 migration guide](migration.md#from-11x-to-120) before regenerating existing clients or changing their selected profile.
9
+ Generated-output compatibility is versioned separately from those preambles. Read the [migration guide for the intended version](migration.md), including [1.2.x-to-1.3.0 SOAP binding adoption](migration.md#from-12x-to-130), before regenerating existing clients or changing their selected profile.
10
10
 
11
11
  ## compileWsdlToProject
12
12
 
@@ -4,6 +4,14 @@ Generation offers legacy, the default, and opt-in checked components. Start with
4
4
 
5
5
  ## Component service generation
6
6
 
7
+ SOAP 1.2 document/literal operations use the selected binding's envelope namespace and `application/soap+xml` action parameter. Select service, port and binding through the existing checked configuration when multiple ports are available; a SOAP version flag is unnecessary. SOAP 1.2 artifacts have their own compatibility scope, so regenerate matched clients, capsules and gateways together. Existing SOAP 1.1 and direct-runtime artifacts retain their contracts.
8
+
9
+ Generated SOAP 1.2 clients use the additive `checkedComponentRuntimeV2` package-root binding with protocol `checked-component-soap12-runtime-v1`. Existing SOAP 1.1 clients keep `checkedComponentRuntimeV1` and `checked-component-runtime-v2`, including their TypeScript injection contract. The older binding refuses SOAP 1.2 artifacts before SOAP dispatch.
10
+
11
+ SOAP 1.2 faults retain code, nested subcodes, language-tagged reasons and optional Node/Role metadata. Declared application detail uses the existing native mapping and gateway fault validation. A valid protocol fault without declared application detail carries fault metadata without a typed application value; the client retains original response bytes. Gateways return HTTP 502 for upstream faults, including SOAP Sender faults received on HTTP 400. SOAP 1.2 protocol rules follow [the messaging framework](https://www.w3.org/TR/soap12-part1/#soapfault) and [HTTP binding](https://www.w3.org/TR/soap12-part2/#soapinhttp).
12
+
13
+ SOAP 1.2 header processing uses the default ultimate receiver and next roles. Unknown mandatory blocks targeted at this receiver refuse; other-role and optional blocks may be ignored. The `mustUnderstand` and `relay` attributes accept SOAP 1.2 Boolean lexical forms. This does not enable outgoing application headers, WS-Security, streaming or RPC/encoded SOAP 1.2.
14
+
7
15
  The additive `checked-components-v1` mode uses the same command family and native owners for qualified structured operations, imported schema snapshots, attributes, recursive records, simple content and ordered values. It preserves the shipped direct runtime and legacy defaults. Source, mapping, artifact and transport checks remain authoritative; unsupported schema or security features refuse rather than disappearing from a successful service claim. This is a qualified component fragment, not universal XSD, streaming or provider acceptance.
8
16
 
9
17
  The [bounded SOAP 1.1 RPC/encoded profile](supported-patterns.md#checked-soap-11-rpc-string-profile) adds source-owned scalar and flat Boolean record parts without document element roots. Component responses additionally accept exact Latin1 with encoding agreement. See the [NOAA qualification](https://github.com/TechSpokes/typescript-wsdl-client/blob/main/docs/noaa-ndfd-qualification.md) for native restrictions, opaque string/reference results and separate owned-server/live evidence limits.
@@ -1,18 +1,18 @@
1
1
  # Try opt-in checked generation
2
2
 
3
- The 1.2.0 candidate offers opt-in WSDL-to-checked-gateway generation for supported document/literal SOAP 1.1 contracts and the bounded RPC/encoded string profile. Choose `pipeline --model checked-components-v1`; omitting `--model` keeps legacy generation. See the root [README](../README.md).
3
+ Opt-in checked generation supports qualified SOAP 1.1 and SOAP 1.2 document/literal contracts and the bounded SOAP 1.1 RPC/encoded string profile. Choose `pipeline --model checked-components-v1`; omitting `--model` keeps legacy generation. SOAP 1.2 execution is available starting with 1.3.0; see the [binding and artifact contract](checked-gateway.md#component-service-generation). See the root [README](../README.md).
4
4
 
5
5
  This is a bounded preview, not general WSDL/XSD or production-provider qualification. It adds source-backed request, response, fault and artifact checks for the supported fragment. Generation exposes legacy and components only; the shipped v1.1.5 direct runtime remains a compatibility binding for existing clients. See the [checked gateway reference](checked-gateway.md).
6
6
 
7
7
  ## Install and try the bundled contract
8
8
 
9
- Version 1.2.0 is a release candidate until published. During candidate verification, install the locally packed 1.2.0 tarball instead of requesting an unpublished registry version. Earlier published previews retain their historical contracts.
9
+ The examples target 1.3.0. Before that version is published, install a locally packed 1.3.0 tarball instead of requesting an unpublished registry version. After publication, use the explicit registry version below. Earlier releases retain their own documented contracts.
10
10
 
11
11
  Use Node.js 24 or newer. Run these commands in a new trial directory:
12
12
 
13
13
  ```powershell
14
14
  npm init -y
15
- npm install --save-dev @techspokes/typescript-wsdl-client@1.2.0
15
+ npm install --save-dev @techspokes/typescript-wsdl-client@1.3.0
16
16
  npm install soap
17
17
  npx wsdl-tsc pipeline --model checked-components-v1 --wsdl-source node_modules/@techspokes/typescript-wsdl-client/docs/examples/checked-forecast.wsdl --client-dir .generated/forecast/client --openapi-file .generated/forecast/openapi.json --gateway-dir .generated/forecast/gateway --gateway-version-prefix v1 --gateway-service-name forecast --init-app --app-dir .generated/forecast --test-dir .generated/forecast/test --checked-report .generated/forecast-plan.json
18
18
  ```
@@ -137,7 +137,7 @@ The qualified component fragment includes imported schemas, nested/repeated and
137
137
 
138
138
  The [bounded SOAP 1.1 RPC/encoded profile](supported-patterns.md#checked-soap-11-rpc-string-profile) supports typed scalar parts, native restrictions, one flat required Boolean record and one opaque string result. [NOAA qualification](https://github.com/TechSpokes/typescript-wsdl-client/blob/main/docs/noaa-ndfd-qualification.md) covers all 12 operations against an owned server; ten live operations succeeded and two GML operations returned non-SOAP failures. There is no parsed DWML/GML or geographic correctness claim.
139
139
 
140
- General XSD, SOAP 1.2, general RPC encoding, broader authentication/security, arbitrary wildcard conversion and checked incremental streaming remain outside this preview. OpenAPI describes the JSON shape but does not express every native scalar/order/wildcard rule. Required declarations and strict wildcard failures cannot become successful unchecked replies.
140
+ General XSD, general RPC encoding, broader authentication/security, arbitrary wildcard conversion and checked incremental streaming remain outside this preview. SOAP 1.2 document/literal uses the separately versioned binding described in the [checked gateway reference](checked-gateway.md#component-service-generation); live-provider acceptance is separate from owned-server protocol evidence. OpenAPI describes the JSON shape but does not express every native scalar/order/wildcard rule. Required declarations and strict wildcard failures cannot become successful unchecked replies.
141
141
 
142
142
  [#409](https://github.com/TechSpokes/typescript-wsdl-client/issues/409) supplies the bounded mixed/namespace/open mapping and its schema/serialization slice of [#410](https://github.com/TechSpokes/typescript-wsdl-client/issues/410). Representative unrelated contract qualification remains under [#397](https://github.com/TechSpokes/typescript-wsdl-client/issues/397); broader HTTP translation, source, scalar, group/naming, security and streaming work retains its existing owners. Legacy defaults and full-profile activation remain unchanged. See the [roadmap](https://github.com/TechSpokes/typescript-wsdl-client/blob/main/ROADMAP.md) for direction and [development checkpoint](https://github.com/TechSpokes/typescript-wsdl-client/blob/main/research/docs/content-model-development-checkpoint.md) for detailed evidence.
143
143
 
package/docs/migration.md CHANGED
@@ -16,6 +16,16 @@ These steps apply to every version upgrade:
16
16
  4. Regenerate matched generator-owned artifacts using the existing generation script and options.
17
17
  5. Review the generated diff and run the consumer project's TypeScript build and tests.
18
18
 
19
+ ## From 1.2.x to 1.3.0
20
+
21
+ Version 1.3.0 adds checked SOAP 1.2 document/literal execution. Legacy generation remains the default. Existing SOAP 1.1 component clients keep `checkedComponentRuntimeV1` and `checked-component-runtime-v2`; the shipped direct runtime is unchanged. Updating the package alone does not adopt SOAP 1.2 or require regeneration of existing SOAP 1.1 clients.
22
+
23
+ To adopt SOAP 1.2, use `--model checked-components-v1` and the source-selected SOAP 1.2 service/port/binding. Ambiguous sources still require explicit checked configuration. Regenerate matched clients, capsules, OpenAPI and gateways with 1.3.0; the generated client uses `checkedComponentRuntimeV2` and `checked-component-soap12-runtime-v1`. Do not mix runtime handshakes or substitute artifacts across ports.
24
+
25
+ SOAP 1.2 `soap-fault` results can contain protocol metadata without a declared application `value` or fault `name`. Narrow the result before accessing typed detail. Declared detail retains native validation; gateway faults remain HTTP 502. Keep consumer-owned server/config/test files under the existing skip/force rules.
26
+
27
+ Qualification uses owned HTTP services, generated gateways and isolated installed consumers. It does not establish live-provider acceptance. SOAP 1.2 streaming, RPC/encoded and broader authentication/header capabilities remain separate work; see the [checked gateway contract](checked-gateway.md#component-service-generation).
28
+
19
29
  ## From 1.1.x to 1.2.0
20
30
 
21
31
  ### Published starting points
@@ -6,7 +6,7 @@ See the root [README](../README.md) for installation and the documentation index
6
6
 
7
7
  ## Upgrade an existing project
8
8
 
9
- Read the [1.1.x-to-1.2.0 migration guide](migration.md#from-11x-to-120) before regenerating an existing gateway. Installing the package does not itself rewrite generated code, but consumer build/install scripts can run generation. Legacy remains the default; affected generated names, stream record shapes and corrected scalar/simple-content results need review.
9
+ Read the [migration guide for the intended version](migration.md) before regenerating an existing gateway. The [1.3.0 upgrade](migration.md#from-12x-to-130) adds checked SOAP 1.2; earlier generation changes retain their historical migration guidance. Installing the package does not itself rewrite generated code, but consumer build/install scripts can run generation. Legacy remains the default.
10
10
 
11
11
  Use the existing generation command and preserve app/test customizations first. Adopt native composition or checked generation separately; successful representative release tests do not certify migration of every consumer project.
12
12
 
@@ -6,6 +6,12 @@ The matrix describes the existing legacy generator. The unreleased opt-in [check
6
6
 
7
7
  The qualified checked domain includes [keyed mixed, namespace-collision and source-permitted open values](checked-gateway.md#mixed-namespaced-and-open-values), with production conformance and installed request/response/fault evidence. Native strict/lax/skip checks remain distinct from schema-source wildcard checks and from the legacy rows below.
8
8
 
9
+ ## Checked SOAP 1.2 document/literal
10
+
11
+ Checked component generation executes supported SOAP 1.2 document/literal operations through the existing buffered adapter. The selected service/port/binding determines version and action; mixed SOAP 1.1/1.2 ports remain independent. [The checked gateway reference](checked-gateway.md#component-service-generation) describes fault results, artifact compatibility and header processing. The legacy matrix below continues to describe binding detection rather than checked execution.
12
+
13
+ SOAP 1.2 RPC/encoded, streaming, authentication and outgoing application/security headers remain separate capabilities. Owned-server evidence establishes the exercised protocol combinations, not general provider qualification.
14
+
9
15
  ## Checked SOAP 1.1 RPC string profile
10
16
 
11
17
  Checked component generation supports the bounded RPC/encoded profile below. The [NOAA qualification](https://github.com/TechSpokes/typescript-wsdl-client/blob/main/docs/noaa-ndfd-qualification.md) distinguishes all-12 owned-server execution, offline provider-response replay and the 2026-11-09 live retest: ten operations succeeded, while two GML operations returned non-SOAP provider errors. No geographic correctness claim follows.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techspokes/typescript-wsdl-client",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Turn legacy WSDL/SOAP services into typed TypeScript clients, OpenAPI 3.1 specs, and production-ready Fastify REST gateways. Built for enterprise SOAP modernization.",
5
5
  "keywords": [
6
6
  "wsdl",