@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 +5 -0
- package/dist/client/checkedDirectRuntime.d.ts +11 -1
- package/dist/client/checkedDirectRuntime.js +19 -2
- package/dist/client/directClientRuntime.d.ts +18 -1
- package/dist/client/directClientRuntime.js +15 -3
- package/dist/client/directClientVersion.d.ts +2 -0
- package/dist/client/directClientVersion.js +2 -0
- package/dist/client/generateDirectClient.js +14 -9
- package/dist/compiler/directCodec.js +1 -1
- package/dist/compiler/directOperation.d.ts +2 -1
- package/dist/compiler/directOperation.js +27 -11
- package/dist/compiler/directSoap.d.ts +40 -10
- package/dist/compiler/directSoap.js +129 -13
- package/dist/compiler/directSoapSource.js +3 -2
- package/dist/compiler/directSourceCapsule.js +12 -3
- package/dist/compiler/soapHeaders.d.ts +10 -4
- package/dist/compiler/soapHeaders.js +28 -9
- package/dist/gateway/generateCheckedGateway.js +26 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/docs/api-reference.md +1 -1
- package/docs/checked-gateway.md +8 -0
- package/docs/checked-generation.md +4 -4
- package/docs/migration.md +10 -0
- package/docs/start-here.md +1 -1
- package/docs/supported-patterns.md +6 -0
- package/package.json +1 -1
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
196
|
-
`export interface Runtime {readonly version: ${literal(
|
|
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)}
|
|
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
|
-
...(
|
|
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(
|
|
212
|
-
` return await runtime.execute(expectedCapsuleBase64, input, options, ${literal(
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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,
|
|
259
|
-
const soapOperation = exactly(bound,
|
|
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
|
|
270
|
-
if (
|
|
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,
|
|
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
|
-
|
|
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?:
|
|
32
|
-
|
|
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
|
-
|
|
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
|
|
3
|
+
/** Shared retained-operation SOAP exchange; profile-specific authority stays explicit. */
|
|
4
4
|
import { checkSemanticBudget } from './semanticBudget.js';
|
|
5
5
|
import { DataOwnershipLimitError } from './dataOwnership.js';
|
|
6
6
|
import { Agent } from 'node:http';
|
|
7
7
|
import { Agent as HttpsAgent } from 'node:https';
|
|
8
|
-
import { types } from 'node:util';
|
|
8
|
+
import { types, MIMEType } from 'node:util';
|
|
9
9
|
import { WSDL } from 'soap/lib/wsdl/index.js';
|
|
10
10
|
import { Client } from 'soap/lib/client.js';
|
|
11
11
|
import { HttpClient } from 'soap/lib/http.js';
|
|
@@ -16,7 +16,7 @@ import { prepareDirectSoapSource, prepareComponentSoapWsdl, DirectSoapSourceErro
|
|
|
16
16
|
import { isXmlNCName, resolveLexicalQName, SchemaLoadingError, syntaxElements, syntaxAttribute, WSDL_NAMESPACE } from '../loader/orderedSyntax.js';
|
|
17
17
|
import { decodeComponentRecordXml, inspectComponentRecordProjection } from './componentRecordProjection.js';
|
|
18
18
|
import { assessComponentSoapHeader } from './soapHeaders.js';
|
|
19
|
-
const
|
|
19
|
+
const SOAP11 = 'http://schemas.xmlsoap.org/soap/envelope/';
|
|
20
20
|
class Stop extends Error {
|
|
21
21
|
kind;
|
|
22
22
|
rule;
|
|
@@ -44,7 +44,7 @@ export function directSoapOptionsShape(options, check, additional = []) {
|
|
|
44
44
|
/** Authentication and fresh-value validation precede dependency bootstrap and all HTTP I/O. */
|
|
45
45
|
export async function exchangeDirectSoap(contract, inputMapping, application, options) {
|
|
46
46
|
const input = inspectDirectValueMapping(inputMapping, contract);
|
|
47
|
-
|
|
47
|
+
const result = await exchangeSoap(contract, input, application, options, {
|
|
48
48
|
component: false, encode: encodeDirectXml, decode: decodeDirectXml, create: createDirectValueMapping,
|
|
49
49
|
prepare: async (selection, check) => {
|
|
50
50
|
const { source } = inspectDirectOperation(contract);
|
|
@@ -54,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',
|
|
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 !==
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 ===
|
|
34
|
-
if (addresses.length !== 1 || elements(ports[0]).some(n => !(n.name.namespace ===
|
|
34
|
+
const addresses = elements(ports[0]).filter(n => n.name.namespace === addressNamespace && n.name.local === 'address');
|
|
35
|
+
if (addresses.length !== 1 || elements(ports[0]).some(n => !(n.name.namespace === addressNamespace && n.name.local === 'address')
|
|
35
36
|
&& !(n.name.namespace === WSDL_NAMESPACE && n.name.local === 'documentation')))
|
|
36
37
|
throw new DirectSoapSourceError('SOAP port extension');
|
|
37
38
|
}
|
|
@@ -316,6 +316,8 @@ export function checkDirectSourceCompatibility(expected, contract, selection, si
|
|
|
316
316
|
const componentVersions = Object.freeze({ ...versions, scope: 'component-soap11-utf8-graph-v1', mapping: 'component-values-v1',
|
|
317
317
|
runtimePolicy: 'explicit-http-verified-https-transport-v1' });
|
|
318
318
|
const scopedComponentVersions = Object.freeze({ ...componentVersions, scope: 'component-soap11-utf8-graph-v2', mapping: 'component-values-and-record-scope-v1' });
|
|
319
|
+
const soap12ComponentVersions = Object.freeze({ ...componentVersions, scope: 'component-soap12-document-literal-v1' });
|
|
320
|
+
const scopedSoap12ComponentVersions = Object.freeze({ ...scopedComponentVersions, scope: 'component-soap12-document-literal-record-v1' });
|
|
319
321
|
const rpcComponentVersions = Object.freeze({ ...componentVersions, scope: 'component-soap11-rpc-strings-v1', mapping: 'component-rpc-string-values-v1' });
|
|
320
322
|
const typedRpcComponentVersions = Object.freeze({ ...componentVersions, scope: 'component-soap11-rpc-typed-latin1-v1', mapping: 'component-rpc-typed-values-v1' });
|
|
321
323
|
const componentReconstructions = new WeakMap();
|
|
@@ -380,6 +382,7 @@ async function componentDescriptor(contract, selection, signal, responseProjecti
|
|
|
380
382
|
const input = make({ kind: 'input' }), output = make({ kind: 'output' });
|
|
381
383
|
const faults = contract.faults.map(fault => ({ name: fault.name, root: make({ kind: 'fault', name: fault.name }) }));
|
|
382
384
|
const compatibility = { selector: contract.selector, service: owned.service, port: owned.port,
|
|
385
|
+
...(contract.binding.version === '1.2' ? { binding: { version: '1.2', actionRequired: contract.binding.actionRequired } } : {}),
|
|
383
386
|
action: contract.binding.action, input, output, faults };
|
|
384
387
|
let projection = null;
|
|
385
388
|
if (responseProjection) {
|
|
@@ -431,7 +434,7 @@ function componentCapsule(derived, budget) {
|
|
|
431
434
|
const typedRpc = rpc && compatibility.input.parts.some(part => part.type.namespace !== 'http://www.w3.org/2001/XMLSchema' || part.type.local !== 'string');
|
|
432
435
|
if (rpc && (extended || derived.applicationGroups))
|
|
433
436
|
throw new Stop('unsupported', 'rpc-artifact-extension');
|
|
434
|
-
const value = { versions: typedRpc ? typedRpcComponentVersions : rpc ? rpcComponentVersions : extended ? scopedComponentVersions : componentVersions, resources, compatibility,
|
|
437
|
+
const value = { versions: compatibility.binding?.version === '1.2' ? extended ? scopedSoap12ComponentVersions : soap12ComponentVersions : typedRpc ? typedRpcComponentVersions : rpc ? rpcComponentVersions : extended ? scopedComponentVersions : componentVersions, resources, compatibility,
|
|
435
438
|
...(extended ? { libraries, projection } : {}), ...(derived.applicationGroups ? { applicationGroups: derived.applicationGroups, applicationGroupIdentity: derived.applicationGroupIdentity } : {}) };
|
|
436
439
|
return { ...value, fingerprint: hash(json(value, budget)) };
|
|
437
440
|
}
|
|
@@ -493,7 +496,8 @@ function readComponent(bytes, budget, signal) {
|
|
|
493
496
|
const rpc = typedRpc || !!parsed.versions && typeof parsed.versions === 'object' && parsed.versions.scope === rpcComponentVersions.scope;
|
|
494
497
|
if (rpc && (extended || grouped))
|
|
495
498
|
reject('rpc-artifact-extension');
|
|
496
|
-
const
|
|
499
|
+
const soap12 = !!parsed.versions && typeof parsed.versions === 'object' && ['component-soap12-document-literal-v1', 'component-soap12-document-literal-record-v1'].includes(parsed.versions.scope);
|
|
500
|
+
const expectedVersions = soap12 ? extended ? scopedSoap12ComponentVersions : soap12ComponentVersions : typedRpc ? typedRpcComponentVersions : rpc ? rpcComponentVersions : extended ? scopedComponentVersions : componentVersions;
|
|
497
501
|
const actual = record(parsed.versions, Object.keys(expectedVersions));
|
|
498
502
|
for (const key of Object.keys(expectedVersions))
|
|
499
503
|
if (actual[key] !== expectedVersions[key])
|
|
@@ -564,7 +568,12 @@ function readComponent(bytes, budget, signal) {
|
|
|
564
568
|
if (!parsed.libraries.length && parsed.projection === null)
|
|
565
569
|
reject('empty-component-extension');
|
|
566
570
|
}
|
|
567
|
-
const c = record(parsed.compatibility, ['selector', 'service', 'port', 'action', 'input', 'output', 'faults']);
|
|
571
|
+
const c = record(parsed.compatibility, ['selector', 'service', 'port', 'action', 'input', 'output', 'faults', ...(soap12 ? ['binding'] : [])]);
|
|
572
|
+
if (soap12) {
|
|
573
|
+
const binding = record(c.binding, ['version', 'actionRequired']);
|
|
574
|
+
if (binding.version !== '1.2' || typeof binding.actionRequired !== 'boolean')
|
|
575
|
+
reject('soap12-binding');
|
|
576
|
+
}
|
|
568
577
|
selector(c.selector);
|
|
569
578
|
name(c.service);
|
|
570
579
|
ncname(c.port);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** SOAP
|
|
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.
|
|
13
|
-
*
|
|
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.
|
|
5
|
-
*
|
|
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
|
-
|
|
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 !==
|
|
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 (
|
|
31
|
-
return Object.freeze({ kind: 'protocol', rule:
|
|
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 = {
|
|
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
|
package/docs/api-reference.md
CHANGED
|
@@ -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 [
|
|
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
|
|
package/docs/checked-gateway.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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,
|
|
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
|
package/docs/start-here.md
CHANGED
|
@@ -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 [
|
|
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.
|
|
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",
|