@cap-ts/soap-adapter 0.1.11 → 0.2.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
@@ -2,12 +2,12 @@
2
2
 
3
3
  [![Version npm](https://img.shields.io/npm/v/@cap-ts/soap-adapter.svg)](https://www.npmjs.com/package/@cap-ts/soap-adapter)
4
4
 
5
- > **CAP plugin for SOAP Adapter that lets `@sap/cds` services expose OData v4 entities backed by legacy SOAP web services.**
6
- > Node.js ≥ 18, `@sap/cds` v7 / v8 / v9.
5
+ > **CAP plugin that exposes legacy SOAP web services as read-only, OData-style entities of a `@sap/cds` service.**
6
+ > Node.js ≥ 20, `@sap/cds` ^9.9.1 or ^10 (cds 10 itself requires Node.js ≥ 22).
7
7
 
8
8
  ## 📦 About
9
9
 
10
- `@cap-ts/soap-adapter` turns a WSDL operation into a CAP entity: consumers declare the entity in `.cds` with a small set of `@Soap.*` annotations, ship an adapter subclass that shapes the SOAP request/response, and the plugin does the rest — CSRF-safe HTTP routing (`/soap/**`), destination + JWT propagation, filter push-down, XML build/parse, response flattening, and CAP OData v4 exposure.
10
+ `@cap-ts/soap-adapter` turns a WSDL operation into a CAP entity: consumers declare the entity in `.cds` with a small set of `@Soap.*` annotations, ship an adapter subclass that shapes the SOAP request/response, and the plugin does the rest — CSRF-safe HTTP routing (`/soap/**`), destination + JWT propagation, filter push-down, XML build/parse, response flattening, and an OData-style JSON response (`@odata.context`, `@odata.count`, `@odata.nextLink`). It is read only: writes are rejected with 405.
11
11
 
12
12
  ---
13
13
 
@@ -43,14 +43,15 @@ The package registers itself as a CAP plugin via `cds-plugin` — no explicit `r
43
43
 
44
44
  ### Peer requirements
45
45
 
46
- | Peer | Minimum | Notes |
46
+ | Peer | Version | Notes |
47
47
  | --- | --- | --- |
48
- | `@sap/cds` | 7.0 | Tested against 7, 8, 9. |
49
- | Node.js | 18 | Uses `crypto.randomUUID`, `structuredClone`, top-level `require('node:test')` for tests. |
50
- | `@sap-cloud-sdk/http-client` | 4.7 | Auto-installed. |
51
- | `@sap-cloud-sdk/connectivity` | 4.7 | Auto-installed. Provides destination resolution + JWT forwarding. |
52
- | `soap` | 1.10 | Auto-installed. |
53
- | `p-limit` | 6.2 | Auto-installed. |
48
+ | `@sap/cds` | `^9.9.1 \|\| ^10` | Tested against 9.9.3 and 10.1.1. |
49
+ | Node.js | >=20 | cds 10 itself requires Node 22 or later. |
50
+ | `@cap-js/cds-types` | >=0.18.0, optional | TypeScript types only; not needed to run the adapter. |
51
+ | `@sap-cloud-sdk/http-client` | ^4.7.0 | Destination-based HTTP calls. |
52
+ | `@sap-cloud-sdk/connectivity` | ^4.7.0 | Destination resolution and JWT forwarding. |
53
+ | `soap` | ^1.10.0 | Dependency, installed automatically. |
54
+ | `p-limit` | ^3.1.0 | Dependency, installed automatically. |
54
55
 
55
56
  Optional but recommended:
56
57
 
@@ -99,7 +100,7 @@ Optional but recommended:
99
100
  └────────────────────────────────────────────────────────────────────────┘
100
101
  │
101
102
  ▼
102
- CAP OData v4 endpoint at `/soap/<path/service>/**`
103
+ OData-style GET endpoint at `/soap/<service path>/<Entity>`
103
104
  ```
104
105
 
105
106
  Two-word summary: **annotate + subclass**. The plugin needs both — the annotations tell it *what* to route, the subclass tells it *how* to marshal.
@@ -175,30 +176,27 @@ class BP extends soap.ApplicationService {
175
176
  init() {
176
177
  const { BusinessPartner } = this.entities;
177
178
 
178
- this.header(BusinessPartner, (req) => ({
179
- // Strip inbound HTTP auth/session/trace before propagating.
180
- ...soap.ApplicationService.stripSensitiveHeaders(req.headers ?? {}),
181
- 'SOAPAction': '"GetBusinessPartner"'
182
- }));
183
-
184
179
  this.request(BusinessPartner, (req, payload) => {
185
- // Shape the SOAP body from the incoming CAP $filter.
180
+ // Shape the SOAP body (the operation's input message) from the incoming CAP $filter.
186
181
  const key = req.query?.SELECT?.where; // CQN WHERE array
187
182
  return { BusinessPartnerID: extractBpId(key) };
188
183
  });
189
184
 
190
185
  this.response(BusinessPartner, (req, rows) => {
191
- // Rows have already been flattened per `@Soap.path` — massage further if needed.
192
- return rows.map(r => ({ ...r, FullName: r.FullName?.trim() }));
186
+ // Rows are the raw entries under `rootResponse`; `@Soap.path` mapping runs after this hook.
187
+ return rows.filter(r => r != null);
193
188
  });
194
189
  }
195
190
  }
196
191
  module.exports = BP;
197
192
  ```
198
193
 
199
- **4. Start `cds watch`.** The loader will mount:
194
+ **4. Start `cds watch`.** The loader mounts GET routes under `/soap` + the service's `@path`, or, without `@path`,
195
+ its short name in lower case:
200
196
 
201
- - `/soap/BP/BusinessPartner` — raw SOAP-over-HTTP route (@requires-enforced).
197
+ - `/soap/bp/BusinessPartner` — the entity set (`$select`, `$count`, `$filter`, `$orderby`, `$top`, `$skip`; any other
198
+ system query option is a 400). `@requires` on the service is enforced.
199
+ - `/soap/bp/BusinessPartner/<key>` — a read by key (404 when nothing comes back).
202
200
 
203
201
  ---
204
202
 
@@ -206,7 +204,7 @@ module.exports = BP;
206
204
 
207
205
  [↑ Table of Contents](#-table-of-contents)
208
206
 
209
- All six annotations are declared in the package's `index.cds` under the single canonical top-level namespace. They are real CDS typed annotations, so `cds compile` type-checks consumer usage — typos and shape mismatches surface at build time, not at first request.
207
+ All seven annotations (`@Soap` and the six `@Soap.*` below) are declared in the package's `index.cds` under the single canonical top-level namespace. `@Soap` (Boolean) only marks a service in the model; what makes a service SOAP-backed at runtime is `kind: "soap"` in `cds.requires`. They are real CDS typed annotations, so `cds compile` type-checks consumer usage — typos and shape mismatches surface at build time, not at first request.
210
208
 
211
209
  ### `@Soap.binding` *(struct, entity-level)*
212
210
 
@@ -222,8 +220,8 @@ entity BusinessPartner { … }
222
220
 
223
221
  | Field | Required | Notes |
224
222
  | ----- | -------- | ----- |
225
- | `rootRequest` | Yes | SOAP element name used to wrap the outbound payload. Must match the WSDL operation's `<input message>` root. |
226
- | `rootResponse` | No | SOAP response element whose contents become the CAP result. If omitted the loader falls back to `@Soap.rootResponse` (scalar), then to a heuristic derived from the operation name. |
223
+ | `rootRequest` | No | Documents the operation's input element. The runtime does not wrap the payload with it: `node-soap` builds the request envelope from the WSDL operation, and the `request()` hook's return value is passed to it as the input message. |
224
+ | `rootResponse` | No | Dot path of the SOAP response element whose contents become the CAP result. If `@Soap.rootResponse` (scalar) is set it takes precedence; without either, `<operation>Response` is used. |
227
225
 
228
226
  ### `@Soap.operation` *(scalar, entity-level)* — **required**
229
227
 
@@ -238,11 +236,11 @@ An entity without an `@Soap.operation` is invisible to the loader — no route i
238
236
 
239
237
  ### `@Soap.rootResponse` *(scalar, entity-level)* — optional
240
238
 
241
- Only consulted when `@Soap.binding.rootResponse` is absent. Documents the SOAP response element to unwrap.
239
+ Dot path of the SOAP response element to unwrap. When set, it takes precedence over `@Soap.binding.rootResponse`.
242
240
 
243
241
  ### `@Soap.path` *(scalar, element-level)*
244
242
 
245
- Explicit dot-path override applied when the orchestrator walks a raw SOAP response tree to locate the value for this element.
243
+ Dot path, relative to one entry under `rootResponse`, from which the value of this element is read.
246
244
 
247
245
  ```cds
248
246
  entity BusinessPartner {
@@ -252,7 +250,7 @@ entity BusinessPartner {
252
250
  }
253
251
  ```
254
252
 
255
- Without `@Soap.path`, the orchestrator matches by element name (case-sensitive) and falls back to a structural best-guess. `@Soap.path` short-circuits both, which is essential when SOAP responses nest values several levels deep or when element names collide across namespaces.
253
+ Without `@Soap.path`, the element's own name is used as the path (case-sensitive). `$value` wrappers and `item` / `Item` arrays are unwrapped along the way; a missing value becomes `null`. Only elements with a built-in `cds.*` type are mapped. `@Soap.path` is essential when SOAP responses nest values several levels deep or when element names differ from the response.
256
254
 
257
255
  ### `@Soap.filterRestriction` *(struct, entity-level)*
258
256
 
@@ -269,7 +267,7 @@ entity BusinessPartner { … }
269
267
  | Field | Type | Behaviour |
270
268
  | ----- | ---- | --------- |
271
269
  | `mandatoryFields` | `array of String` | Property names that MUST appear in `$filter` for the request to be accepted. Missing fields raise a 400 with a diagnostic message that names the field. |
272
- | `multipleSelection` | `Boolean` (default `true`) | When `false`, `$filter` on any mandatory field must be a single equality clause. `in(…)` / `or` / `ne` combinations are rejected as 400. The runtime expands `key in (a,b,c)` into N parallel SOAP calls only when `multipleSelection: true`. |
270
+ | `multipleSelection` | `Boolean` (default: not set) | Set it to `false` when the SOAP operation accepts only one key value per call. The runtime then expands a `$filter` with `<first key> in (a,b,c)` into one SOAP call per value (in batches of `cds.env.soap.multiSelectionConcurrency`, default 5) and merges the results. A second `in(…)` on the key, or `in(…)` combined with `or`, is a 400 (`UNSUPPORTED_MULTI_SELECTION_FILTER`). When `true` or not set, nothing is expanded and the `request()` hook receives the `in(…)` as is. |
273
271
 
274
272
  See § [Filter push-down & `filterRestriction`](#-filter-push-down--filterrestriction) below.
275
273
 
@@ -332,18 +330,25 @@ Available inside `init()` via `this.*`:
332
330
 
333
331
  ### `this.header(entity, fn)` — outbound HTTP headers
334
332
 
335
- Registers a hook that produces additional HTTP headers for every outbound SOAP call for `entity`.
333
+ Registers a hook (or a static object) that adds a **SOAP header** — an element inside the envelope's `<soap:Header>` — to every outbound call for `entity`. It is passed to `node-soap`'s `addSoapHeader`; it does not set HTTP headers.
336
334
 
337
335
  ```js
338
- this.header(BusinessPartner, (req) => ({
339
- 'SOAPAction': '"GetBusinessPartner"',
340
- 'X-Correlation-ID': req.headers['x-correlation-id'] ?? crypto.randomUUID()
336
+ // Plain map: each key becomes a header element.
337
+ this.header(BusinessPartner, (req) => ({ TraceID: req.id }));
338
+
339
+ // Structured: value plus element name, namespace prefix and URI.
340
+ this.header(BusinessPartner, () => ({
341
+ value: { Username: 'user', Password: 'secret' },
342
+ name: 'UsernameToken', prefix: 'wsse',
343
+ xmlns: 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd'
341
344
  }));
342
345
  ```
343
346
 
344
- - `req` — the incoming CAP `Request` object.
345
- - Return value: a plain `Record<string, string>`. Merged on top of the base headers computed by the runtime. Returning `null` / `undefined` is a no-op.
346
- - Called once per SOAP dispatch (including fan-out calls for `multipleSelection: true`).
347
+ - Hook arguments: `(req, payload)` — the CAP `Request` and the outbound payload.
348
+ - Return value: a plain map, or a structured `{ value, name?, prefix?, xmlns? }`. Returning `null` / `undefined` adds nothing.
349
+ - In a plain map, keys on the forbidden list below are dropped (with a warning), so inbound credentials cannot be copied into the envelope by accident.
350
+ - Without any `header()` hook the runtime adds one default SOAP header, `TraceID`, with the request's correlation id.
351
+ - Called once per SOAP dispatch (including each fan-out call for `multipleSelection: false`).
347
352
 
348
353
  ### `this.request(entity, fn)` — outbound SOAP payload
349
354
 
@@ -357,10 +362,11 @@ this.request(BusinessPartner, (req, payload) => {
357
362
  });
358
363
  ```
359
364
 
360
- - `req` — incoming CAP `Request`.
361
- - `payload` — the pre-built payload object (already includes filter-extracted fields). Mutate or replace.
362
- - Return value: the object to serialise into the SOAP `<Body>` wrapped under `rootRequest`.
363
- - Returning `undefined` falls back to the runtime-built payload.
365
+ - `req` — incoming CAP `Request`; read `$filter` / keys from `req.query.SELECT.where`.
366
+ - `payload` — `req.data` (usually `{}` for a READ) for the first hook, else the previous hook's return value.
367
+ - Return value: the operation's input message, passed to `node-soap`, which builds the `<soap:Body>` from the WSDL.
368
+ - Several `request()` hooks run in registration order, each receiving the previous result. Always return the payload:
369
+ a hook that returns `undefined` sends an empty request.
364
370
 
365
371
  ### `__filterPushedDown` sentinel
366
372
 
@@ -395,13 +401,13 @@ this.mock(BusinessPartner, (req) => {
395
401
  });
396
402
  ```
397
403
 
398
- - Return value: array of raw row objects (same shape as the SOAP response would produce — **before** `@Soap.path` flattening).
404
+ - Return value: the final rows, in the shape of the CAP entity. The mock's result is returned as is: no `response()` hooks, no `@Soap.path` mapping and no in-memory `$filter` / `$orderby` / paging.
399
405
  - When a mock is registered the runtime skips destination resolution, WSDL loading, and HTTP dispatch entirely.
400
- - Mocks are **not** active in production builds (`NODE_ENV=production`); registering one logs a `warn`.
406
+ - A registered mock is always active, in every environment — register it only in test or development code.
401
407
 
402
408
  ### `this.response(entity, fn)` — inbound result transform
403
409
 
404
- Registers a hook that post-processes the result before they are returned to the CAP OData layer.
410
+ Registers a hook that post-processes the SOAP result before it is mapped to the entity.
405
411
 
406
412
  ```js
407
413
  this.response(BusinessPartner, (req, rows) => {
@@ -412,17 +418,20 @@ this.response(BusinessPartner, (req, rows) => {
412
418
  ```
413
419
 
414
420
  - `req` — the incoming CAP `Request`.
415
- - `rows` — array of objects already flattened by `@Soap.path` resolution. Each object has one key per entity element.
416
- - Return value: the final rows array passed to CAP. Must be an array; returning `undefined` falls back to `rows` unchanged.
421
+ - `rows` — the raw entries under `rootResponse` (always an array). `@Soap.path` mapping and the in-memory
422
+ `$filter` / `$orderby` / paging run **after** this hook, so return entries in the raw response shape.
423
+ - Several `response()` hooks run in registration order, each receiving the previous result. Always return an array:
424
+ a hook that returns `undefined` yields an empty result.
417
425
 
418
426
  ### `soap.ApplicationService.stripSensitiveHeaders(headers)` — static helper
419
427
 
420
- Strips inbound HTTP headers that must never be forwarded to a backend SOAP service (auth tokens, session cookies, SAP passport, trace/correlation identifiers).
428
+ Returns a copy of a header map without the entries that must never reach a backend (auth tokens, session cookies, SAP passport, trace/correlation identifiers). Use it when a `header()` hook builds SOAP headers from a map it did not write itself, such as the inbound request headers:
421
429
 
422
430
  ```js
423
431
  this.header(BusinessPartner, (req) => ({
424
- ...soap.ApplicationService.stripSensitiveHeaders(req.headers ?? {}),
425
- 'SOAPAction': '"GetBusinessPartner"'
432
+ // Everything from the inbound request except credentials, cookies and trace ids.
433
+ ...soap.ApplicationService.stripSensitiveHeaders(req.http?.req?.headers ?? {}),
434
+ TraceID: req.id
426
435
  }));
427
436
  ```
428
437
 
@@ -468,13 +477,10 @@ const myForbidden = new Set([
468
477
  ...soap.ApplicationService.FORBIDDEN_SOAP_HEADERS,
469
478
  'x-my-internal-token',
470
479
  ]);
471
- this.header(BusinessPartner, (req) => {
472
- const clean = {};
473
- for (const [k, v] of Object.entries(req.headers ?? {})) {
474
- if (!myForbidden.has(k.toLowerCase())) clean[k] = v;
475
- }
476
- return clean;
477
- });
480
+ const withoutForbidden = (headers) =>
481
+ Object.fromEntries(Object.entries(headers).filter(([k]) => !myForbidden.has(k.toLowerCase())));
482
+
483
+ this.header(BusinessPartner, (req) => withoutForbidden(req.http?.req?.headers ?? {}));
478
484
  ```
479
485
 
480
486
  ---
@@ -533,7 +539,7 @@ const rows = await soap.read(
533
539
  req, // inbound CAP request (source of user, params, headers)
534
540
  req.query, // CQN SELECT query
535
541
  { // optional overrides
536
- headwrs: ...,
542
+ headers: ...,
537
543
  params: ...,
538
544
  outerReq: req,
539
545
  fallbackEntityDef: ...
@@ -588,7 +594,7 @@ Returns `any[]` — deduped rows in original order.
588
594
 
589
595
  ### `soap.isSoapService(serviceName)` — check if a service is SOAP-backed
590
596
 
591
- Returns `true` when the named service is configured as a SOAP service — either `cds.requires[name].kind === 'soap'` or the CSN definition carries the `@soap` annotation. Safe to call before `cds.services` is populated; relies only on `cds.requires` and `cds.model.definitions`.
597
+ Returns `true` when the named service is configured as a SOAP service — either `cds.requires[name].kind === 'soap'` or the CSN definition carries the legacy lower-case `@soap` annotation (the current `@Soap` annotation is not checked here). Safe to call before `cds.services` is populated; relies only on `cds.requires` and `cds.model.definitions`.
592
598
 
593
599
  ```js
594
600
  if (soap.isSoapService('BP')) {
@@ -631,9 +637,9 @@ Global runtime knobs live under `cds.env.soap` (i.e., the `cds.soap` key in `pac
631
637
  {
632
638
  "cds": {
633
639
  "soap": {
634
- "maxConcurrent": 50,
635
640
  "pushFilters": true,
636
641
  "pageSize": 1000,
642
+ "multiSelectionConcurrency": 5,
637
643
  "idempotency": {
638
644
  "enabled": false,
639
645
  "ttl": 300000,
@@ -646,10 +652,11 @@ Global runtime knobs live under `cds.env.soap` (i.e., the `cds.soap` key in `pac
646
652
 
647
653
  | Key | Type | Default | Description |
648
654
  | --- | ---- | ------- | ----------- |
649
- | `soap.maxConcurrent` | `number` | `50` | Maximum number of parallel outbound SOAP calls per service instance. Enforced by `p-limit`. Fan-out calls (from `multipleSelection: true`) share this pool. |
655
+ | `soap.multiSelectionConcurrency` | `number` | `5` | Batch size for the fan-out of `multipleSelection: false` entities: that many SOAP calls run in parallel, batch after batch. |
656
+ | `soap.maxConcurrent` | `number` | `50` | Reserved. A per-destination `p-limit` pool is prepared from it, but the request path does not use it yet. |
650
657
  | `soap.pushFilters` | `boolean` | `true` | Enables the [`__filterPushedDown` sentinel](#__filterpusheddown-sentinel). Set to `false` to force the in-memory `$filter` / `$orderby` / `$top` / `$skip` evaluator even when a `request()` hook sets the sentinel — useful for A/B testing or when a push-down implementation is suspected of misbehaving. |
651
658
  | `soap.pageSize` | `number` | `1000` | Default page size for the bounded in-memory fallback when the adapter does **not** set the `__filterPushedDown` sentinel (or when `pushFilters: false`). When the filtered result exceeds one page, the runtime emits an `@odata.nextLink` header and logs a one-time-per-service warning. Hard-capped at `5000` regardless of override. |
652
- | `soap.idempotency.enabled` | `boolean` | `false` | Feature flag — reserved for write-handler support (not yet shipped). When `true`, duplicate SOAP requests within the TTL window are short-circuited with a cached response. |
659
+ | `soap.idempotency.enabled` | `boolean` | `false` | Reserved for write-handler support, which is not shipped: the adapter is read only, so this has no effect today. |
653
660
  | `soap.idempotency.ttl` | `number` (ms) | `300000` | Cache TTL for idempotency keys (5 minutes). |
654
661
  | `soap.idempotency.maxEntries` | `number` | `1000` | LRU eviction threshold for the idempotency cache. |
655
662
 
@@ -659,9 +666,8 @@ Per-service configuration lives in `cds.requires.<ServiceName>`:
659
666
  | --- | ---- | -------- | ----------- |
660
667
  | `kind` | `"soap"` | **Yes** | Marks the service for SOAP handling. |
661
668
  | `wsdl` | `string` | **Yes** | Path to the WSDL file, relative to `cds.root`. |
662
- | `credentials.destination` | `string` | Yes (BTP) | BTP destination name. Resolved via `@sap-cloud-sdk/connectivity`. |
663
- | `credentials.url` | `string` | Yes (local) | Direct SOAP endpoint URL. Used when no BTP destination is configured (local dev / on-premise). |
664
- | `credentials.username` / `password` | `string` | No | Basic-auth credentials. Only used when `url` is set (not for BTP destinations — those carry credentials in the destination itself). |
669
+ | `credentials.destination` | `string` | **Yes** | Destination name, resolved via `@sap-cloud-sdk/connectivity` (BTP destination service, or the `destinations` environment variable locally). Without it the service fails with `No 'credentials.destination' configured`. |
670
+ | `credentials.path` | `string` | No | Path appended to the destination URL to form the SOAP endpoint. |
665
671
 
666
672
  ---
667
673
 
@@ -674,9 +680,9 @@ In a multi-tenant SaaS application each subscriber has its own BTP destination.
674
680
  ### How it works
675
681
 
676
682
  1. The CAP runtime validates the incoming JWT and attaches the tenant context to `req`.
677
- 2. On each SOAP dispatch the adapter calls `@sap-cloud-sdk/connectivity`'s `getDestination()` with the subscriber tenant token extracted from `req.http.req.headers.authorization`.
678
- 3. The destination service returns the tenant-specific endpoint URL and credentials.
679
- 4. The WSDL is re-fetched (and cached per tenant+destination) if the endpoint differs from a previously seen one.
683
+ 2. On each SOAP dispatch the adapter calls `@sap-cloud-sdk/connectivity`'s `getDestination()` with the caller's JWT (from the request's token info, or the `Authorization: Bearer` header) and tenant.
684
+ 3. The destination service returns the tenant-specific endpoint URL and credentials; the HTTP call goes through `@sap-cloud-sdk/http-client`, so the destination's authentication type (principal propagation, OAuth2, basic, …) applies.
685
+ 4. The SOAP client is built from the local WSDL file and cached per service, destination, endpoint URL and tenant.
680
686
 
681
687
  ### Configuration
682
688
 
@@ -700,37 +706,21 @@ The destination name is the **provider** destination registered in BTP. For mult
700
706
 
701
707
  ### Local development without a BTP destination
702
708
 
703
- Use `credentials.url` + optional `credentials.username` / `credentials.password` in your `cds.env` override (`.cdsrc-private.json` or environment variables):
709
+ Keep `credentials.destination` and define that destination locally through the Cloud SDK's `destinations`
710
+ environment variable (for example in a git-ignored `.env` or `default-env.json`):
704
711
 
705
- ```json
706
- {
707
- "cds": {
708
- "requires": {
709
- "BP": {
710
- "credentials": {
711
- "url": "http://localhost:8080/soap/BP",
712
- "username": "alice",
713
- "password": "Alice@Cap#"
714
- }
715
- }
716
- }
717
- }
718
- }
712
+ ```bash
713
+ destinations='[{"name":"DEST_BP","url":"http://localhost:8080","username":"alice","password":"<password>"}]' cds watch
719
714
  ```
720
715
 
721
- > **Security note.** Never commit `credentials.password` to source control. Use `.cdsrc-private.json` (git-ignored) or the `CDS_REQUIRES_BP_CREDENTIALS_PASSWORD` environment variable pattern.
716
+ > **Security note.** Never commit destination credentials to source control.
722
717
 
723
718
  ### JWT forwarding details
724
719
 
725
- The adapter propagates the subscriber JWT as a Bearer token in the `Authorization` header of the outbound SOAP call **unless** the `header()` hook returns its own `Authorization` key (which takes precedence). Use `stripSensitiveHeaders()` in your `header()` hook if you want to suppress forwarding entirely:
726
-
727
- ```js
728
- this.header(BusinessPartner, (req) => ({
729
- ...soap.ApplicationService.stripSensitiveHeaders(req.headers ?? {}),
730
- // No Authorization key → runtime does NOT forward the inbound JWT.
731
- 'SOAPAction': '"GetBusinessPartner"'
732
- }));
733
- ```
720
+ The caller's JWT is used for the **destination lookup** only. What the backend receives is decided by the
721
+ destination: with principal propagation or OAuth2 user token exchange the Cloud SDK derives the outbound credentials
722
+ from it; with basic authentication or client credentials the destination's own credentials are used. The inbound
723
+ `Authorization` header is never copied into the SOAP call, and a `header()` hook cannot set HTTP headers.
734
724
 
735
725
  ---
736
726
 
@@ -756,10 +746,9 @@ See [`__filterPushedDown` sentinel](#__filterpusheddown-sentinel) for the full c
756
746
  **Path B — Bounded in-memory fallback.**
757
747
 
758
748
  1. The CAP OData layer parses `$filter` into a CQN `WHERE` array.
759
- 2. The loader's filter extractor walks the CQN tree and produces a flat `{ fieldName: value | value[] }` map.
760
- 3. The runtime passes this map to the `request()` hook as part of `payload`.
761
- 4. For `multipleSelection: false` entities with an `in(…)` filter, the runtime fans out into N sequential/parallel SOAP calls (one per value) and merges results.
762
- 5. The SOAP result is filtered / sorted / paginated **in Node memory** by the in-memory evaluator, bounded by `cds.env.soap.pageSize` (default `1000`, capped at `5000`). When the filtered set exceeds one page, an `@odata.nextLink` is emitted so the client can paginate, and a **one-time-per-service** `in-memory pagination fallback truncated …` warning is logged suggesting migration to Path A.
749
+ 2. The `request()` hook reads what it needs from `req.query.SELECT.where` and builds the payload.
750
+ 3. For `multipleSelection: false` entities with `<first key> in (…)`, the runtime fans out into one SOAP call per value (batches of `multiSelectionConcurrency`) and merges the results.
751
+ 4. The SOAP result is filtered / sorted / paginated **in Node memory** by the in-memory evaluator, bounded by `cds.env.soap.pageSize` (default `1000`, capped at `5000`). When the filtered set exceeds one page, an `@odata.nextLink` is emitted so the client can paginate, and a **one-time-per-service** `in-memory pagination fallback truncated …` warning is logged suggesting migration to Path A.
763
752
 
764
753
  ### `@Soap.filterRestriction` reference
765
754
 
@@ -782,13 +771,15 @@ GET /soap/FI/e_FinancialDocument?$filter=FiscalYear eq '2025'
782
771
 
783
772
  | Value | `$filter` clause | Runtime behaviour |
784
773
  | ----- | ---------------- | ----------------- |
785
- | `true` (default) | `BusinessPartnerID in ('BP001','BP002')` | Expands to 2 parallel SOAP calls; results merged. |
786
- | `false` | `BusinessPartnerID in ('BP001','BP002')` | Rejected with HTTP 400. |
774
+ | `false` | `BusinessPartnerID in ('BP001','BP002')` | Expands to 2 SOAP calls (one per value, run in parallel); results merged. |
775
+ | `false` | `BusinessPartnerID in ('BP001') or Name eq 'x'`, or two `in(…)` on the key | Rejected with HTTP 400 `UNSUPPORTED_MULTI_SELECTION_FILTER`. |
787
776
  | `false` | `BusinessPartnerID eq 'BP001'` | Single SOAP call. |
777
+ | `true` / not set | any | Single SOAP call; the `request()` hook handles `in(…)` itself (or the in-memory evaluator filters the result). |
788
778
 
789
- ### Combining mandatory fields with `$expand`
779
+ ### `$expand`
790
780
 
791
- When an entity is used as a value-help target (via `$expand` from a parent entity), the parent's key fields are automatically injected into the filter map — `mandatoryFields` validation still applies. Ensure the parent passes all required keys as association keys.
781
+ The `/soap/**` routes do not accept `$expand` (it is a 400). To combine SOAP entities with others, read them through
782
+ another service, for example `@cap-ts/remote-service-adapter`, which expands associations across backends.
792
783
 
793
784
  ---
794
785
 
@@ -798,10 +789,10 @@ When an entity is used as a value-help target (via `$expand` from a parent entit
798
789
 
799
790
  | Guarantee | Detail |
800
791
  | --- | --- |
801
- | **No credential leakage** | `stripSensitiveHeaders()` removes auth, cookie, and SAP Passport headers before outbound dispatch. Adapters that call it in `header()` are safe by default. |
792
+ | **No credential leakage** | Inbound HTTP headers are never forwarded. Keys on the forbidden list (auth, cookie, SAP Passport, trace headers) are dropped from plain-map `header()` results, and `stripSensitiveHeaders()` removes them from any map you build. |
802
793
  | **JWT tenant isolation** | Destination resolution uses the subscriber JWT; a tenant cannot access another tenant's destination. |
803
794
  | **Filter injection prevention** | The filter extractor operates on the parsed CQN tree (not raw `$filter` string), so SQL/SOAP injection via `$filter` is structurally impossible. |
804
- | **No WSDL caching across tenants** | WSDL cache is keyed by `tenant + destination name + endpoint URL`. A WSDL change on one tenant does not affect others. |
795
+ | **No client sharing across tenants** | The SOAP client cache is keyed by service, destination name, endpoint URL and tenant. |
805
796
  | **`@requires` enforcement** | All routes mounted under `/soap/**` inherit the CAP `@requires` annotation from the service definition. Unauthenticated requests are rejected by the CAP runtime before reaching the adapter. |
806
797
 
807
798
  ### Middleware ordering for consumers
@@ -872,13 +863,15 @@ cds.on('bootstrap', (app: any) => {
872
863
 
873
864
  ### Entity not found / `501 Not Implemented`
874
865
 
875
- **Symptom:** `GET /soap/BP/BusinessPartner` returns `501` or the entity is missing from the service document.
866
+ **Symptom:** `GET /soap/bp/BusinessPartner` returns `404` (no route) or `501` (no adapter class).
876
867
 
877
- **Cause:** The loader skips entities without `@Soap.operation`. Check that:
868
+ **Cause:** The loader skips entities without `@Soap.operation`, and a request fails with 501 when no adapter class was found for the entity. Check that:
878
869
 
879
870
  1. The entity has `@Soap.operation: '<WsdlOperationName>'` in its `.cds` model.
880
871
  2. The service is declared with `kind: "soap"` in `cds.requires`.
881
872
  3. The `.cds` file is reachable from `cds.model` (i.e., it is `using`-imported somewhere in the model graph).
873
+ 4. The URL uses the service's `@path`, or its short name in lower case.
874
+ 5. The adapter class resolves (see [Adapter class not loaded](#adapter-class-not-loaded--typeerror--is-not-a-constructor)).
882
875
 
883
876
  ---
884
877
 
@@ -888,7 +881,7 @@ cds.on('bootstrap', (app: any) => {
888
881
 
889
882
  **Likely causes:**
890
883
 
891
- 1. **Wrong `rootResponse`** — the element name in `@Soap.binding.rootResponse` does not match the actual WSDL response wrapper. Open the raw SOAP response (enable `DEBUG=soap` logging) and verify the element name.
884
+ 1. **Wrong `rootResponse`** — the path in `@Soap.rootResponse` / `@Soap.binding.rootResponse` (default `<operation>Response`) does not match the actual response. Log the raw rows in a `response()` hook to see the shape.
892
885
  2. **Missing `@Soap.path`** — the orchestrator cannot locate the value for an element by name. Add explicit `@Soap.path` annotations.
893
886
  3. **Filter not pushed down** — the `request()` hook is not reading the filter fields correctly. Log `payload` inside the hook to inspect.
894
887
 
@@ -936,7 +929,7 @@ Set the `DEBUG` environment variable before starting `cds watch`:
936
929
  DEBUG=soap cds watch
937
930
  ```
938
931
 
939
- This enables per-request logging of: destination resolution, WSDL load, outbound headers (with sensitive values redacted), raw SOAP request/response XML, and filter extraction.
932
+ This enables the `soap` logger's debug output. Payloads that are logged are redacted. It does not log the raw SOAP XML; use a `response()` hook to inspect raw rows.
940
933
 
941
934
  ---
942
935
 
package/cds-plugin.js CHANGED
@@ -1 +1 @@
1
- require('./src/lib/_loader')();
1
+ require('./src/lib/_loader')();
package/index.cds CHANGED
@@ -1,41 +1,3 @@
1
- // ---------------------------------------------------------------------------
2
- // @cap-ts/soap-adapter — CDS annotation surface
3
- // ---------------------------------------------------------------------------
4
- // A single, dedicated `Soap` namespace declares every runtime annotation
5
- // this package consumes. Consumers set them with the fully-qualified form:
6
- //
7
- // annotate MyService.MyEntity with @Soap.binding: {
8
- // rootRequest : 'GetBusinessPartnerRequest',
9
- // rootResponse : 'GetBusinessPartnerResponse'
10
- // };
11
- // annotate MyService.MyEntity with @Soap.operation: 'GetBusinessPartner';
12
- //
13
- // Design notes.
14
- // - PascalCase root (`Soap`) matches SAP's own annotation conventions
15
- // (`@Common.*`, `@UI.*`, `@Capabilities.*`, `@Analytics.*`, `@Core.*`,
16
- // `@ODM.*`) so this package's annotations sit alongside them naturally
17
- // in consumer `.cds` files.
18
- // - Single top-level namespace, no scoping prefix. The domain name
19
- // `Soap` is specific enough that no other SAP/CAP package will
20
- // realistically collide with it. Earlier iterations shipped
21
- // `@soap.*` (deprecated form — collision risk against any other
22
- // package's top-level `soap` namespace) and `@cap.soap.*` (dual-key
23
- // transitional form). Both are gone: consumers have been informed of
24
- // the hard-cutover migration and the runtime reads `@Soap.<name>`
25
- // directly, with no legacy fallback.
26
- // - Declared as real typed annotations so `cds compile` type-checks
27
- // consumer usage — typos and shape mismatches surface at build time,
28
- // not at first request.
29
- // - Declared at the top level with the fully-qualified name
30
- // `Soap.<x>`, matching the shape of SAP's own annotation packages
31
- // (`@sap/cds/common.cds` for `@Common.*`, `@sap/cds/vocabularies/UI`
32
- // for `@UI.*`). A `namespace Soap;` prefix would produce identical
33
- // CSN keys under `@sap/cds-compiler` v7 (the compiler collapses
34
- // `Soap.Soap.<x>` back to `Soap.<x>` when the annotation name
35
- // starts with the namespace segment), so both forms are equivalent —
36
- // but form (a) reads more clearly and requires no `namespace` line.
37
- // ---------------------------------------------------------------------------
38
-
39
1
  annotation Soap : Boolean default true;
40
2
 
41
3
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-ts/soap-adapter",
3
- "version": "0.1.11",
3
+ "version": "0.2.0",
4
4
  "description": "SOAP Adapter for CAP",
5
5
  "main": "src/index.js",
6
6
  "types": "types/index.d.ts",
@@ -30,34 +30,18 @@
30
30
  "soap": "^1.10.0"
31
31
  },
32
32
  "peerDependencies": {
33
- "@cap-js/cds-types": "^0.18.0",
34
- "@sap/cds": "^9.9.1"
33
+ "@cap-js/cds-types": ">=0.18.0",
34
+ "@sap/cds": "^9.9.1 || ^10",
35
+ "@sap-cloud-sdk/connectivity": "^4.7.0",
36
+ "@sap-cloud-sdk/http-client": "^4.7.0"
35
37
  },
36
- "devDependencies": {
37
- "@cap-js/cds-types": "^0.18.0",
38
- "@sap/cds": "^9.9.1",
39
- "@types/cors": "^2.8.19",
40
- "cross-var": "^1.1.0",
41
- "fs-extra": "^11.3.0",
42
- "javascript-obfuscator": "^5.7.0",
43
- "rimraf": "^6.0.1",
44
- "typescript": "^5.9.3",
45
- "uglify-js": "^3.19.3"
38
+ "peerDependenciesMeta": {
39
+ "@cap-js/cds-types": {
40
+ "optional": true
41
+ }
46
42
  },
47
- "scripts": {
48
- "build": "node build/index.js --build",
49
- "test": "node build/index.js --test",
50
- "prepublishOnly": "git submodule update --init --recursive --remote",
51
- "prepare": "node build/index.js --build-secure",
52
- "postpublish": "node build/index.js --post-publish",
53
- "gitonly": "npm run prepublishOnly && npm run prepare && npm run postpublish",
54
- "gitbuild": "npm run prepare && npm run postpublish",
55
- "patch": "npm version patch",
56
- "minor": "npm version minor",
57
- "major": "npm version major",
58
- "deploy": "npm publish --access public",
59
- "deprecate": "node build/index.js --deprecate",
60
- "unpublish": "node build/index.js --unpublish"
43
+ "engines": {
44
+ "node": ">=20"
61
45
  },
62
46
  "files": [
63
47
  "index.cds",
@@ -65,7 +49,8 @@
65
49
  "src",
66
50
  "types/index.d.ts",
67
51
  "types/lib/index.d.ts",
68
- "src/lib/**/*"
52
+ "src/lib/**/*",
53
+ "!**/.DS_Store"
69
54
  ],
70
55
  "cds": {
71
56
  "file": "index.cds"
package/src/index.js CHANGED
@@ -1 +1 @@
1
- function _0x3f88(_0x144101,_0x3fea4e){_0x144101=_0x144101-0xbe;var _0x5966de=_0x5966();var _0x3f8883=_0x5966de[_0x144101];if(_0x3f88['cnDwGc']===undefined){var _0x5342af=function(_0x4d2cb0){var _0x1875cc='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';var _0x52ffb2='',_0x2594c0='';for(var _0x45732b=0x0,_0x246379,_0x5ad362,_0x1a9070=0x0;_0x5ad362=_0x4d2cb0['charAt'](_0x1a9070++);~_0x5ad362&&(_0x246379=_0x45732b%0x4?_0x246379*0x40+_0x5ad362:_0x5ad362,_0x45732b++%0x4)?_0x52ffb2+=String['fromCharCode'](0xff&_0x246379>>(-0x2*_0x45732b&0x6)):0x0){_0x5ad362=_0x1875cc['indexOf'](_0x5ad362);}for(var _0x240338=0x0,_0x38ded9=_0x52ffb2['length'];_0x240338<_0x38ded9;_0x240338++){_0x2594c0+='%'+('00'+_0x52ffb2['charCodeAt'](_0x240338)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0x2594c0);};var _0x30e96f=function(_0x2e93ad,_0x1ad559){var _0x9459ae=[],_0x1a9cda=0x0,_0x4559c7,_0x2b8ae3='';_0x2e93ad=_0x5342af(_0x2e93ad);var _0x5d2b5a;for(_0x5d2b5a=0x0;_0x5d2b5a<0x100;_0x5d2b5a++){_0x9459ae[_0x5d2b5a]=_0x5d2b5a;}for(_0x5d2b5a=0x0;_0x5d2b5a<0x100;_0x5d2b5a++){_0x1a9cda=(_0x1a9cda+_0x9459ae[_0x5d2b5a]+_0x1ad559['charCodeAt'](_0x5d2b5a%_0x1ad559['length']))%0x100,_0x4559c7=_0x9459ae[_0x5d2b5a],_0x9459ae[_0x5d2b5a]=_0x9459ae[_0x1a9cda],_0x9459ae[_0x1a9cda]=_0x4559c7;}_0x5d2b5a=0x0,_0x1a9cda=0x0;for(var _0x9a8b24=0x0;_0x9a8b24<_0x2e93ad['length'];_0x9a8b24++){_0x5d2b5a=(_0x5d2b5a+0x1)%0x100,_0x1a9cda=(_0x1a9cda+_0x9459ae[_0x5d2b5a])%0x100,_0x4559c7=_0x9459ae[_0x5d2b5a],_0x9459ae[_0x5d2b5a]=_0x9459ae[_0x1a9cda],_0x9459ae[_0x1a9cda]=_0x4559c7,_0x2b8ae3+=String['fromCharCode'](_0x2e93ad['charCodeAt'](_0x9a8b24)^_0x9459ae[(_0x9459ae[_0x5d2b5a]+_0x9459ae[_0x1a9cda])%0x100]);}return _0x2b8ae3;};_0x3f88['XqmfAX']=_0x30e96f,_0x3f88['dFdNRd']={},_0x3f88['cnDwGc']=!![];}var _0xf59f42=_0x5966de[0x0];_0x3f88['cXnVAT']!==_0xf59f42&&(_0x3f88['dFdNRd']={},_0x3f88['cXnVAT']=_0xf59f42);var _0x5522fe=_0x3f88['dFdNRd'][_0x144101];return _0x5522fe===undefined?(_0x3f88['ZqIcnu']===undefined&&(_0x3f88['ZqIcnu']=!![]),_0x3f8883=_0x3f88['XqmfAX'](_0x3f8883,_0x3fea4e),_0x3f88['dFdNRd'][_0x144101]=_0x3f8883):_0x3f8883=_0x5522fe,_0x3f8883;}function _0x5966(){var _0xea2985=['WOtcV3/cPvNcO3JcVYL5pvS8','fcjadYbWW4pdLMHzW55ZFa','WOpdHbeXWQeEWPtdMc8Ocmodaa','W6qJW6VdQCowD8kNEGZdGSkkCG','amk9WR5bWQHIWOS','a8oHW57dRmkW','nMhcU0jLBuNdLq','lCobCSoOkMBcKmkimZtcSCoviG','W6RdP3zup3vEW6avlxZdMWC','k8odESoLk2/cL8kUdq/cPmofhq','oCknW5FcRYGtWRfWW6/cJCkHW6uh','ewLzWOONW7O+WP0','rcRcOZVdMwP2E2hcL8odrSkj','fmoiymky','iSoFyCk3WRNdVrNdJConcCkEtCkZ','v0xcMmoojxu+wCoHoav0W6K','WQK2WRKJdGSqWPBcIh4','WQb4WRlcOa','W7KmEmoeWPpcHJZcTSoyESktxqy','v0hcKmodjNvtDmo9gXjy'];_0x5966=function(){return _0xea2985;};return _0x5966();}var _0x1e954e=_0x3f88;(function(_0x5b0b96,_0xe06130){var _0x1283b9=_0x3f88,_0x1a091d=_0x5b0b96();while(!![]){try{var _0x38c98f=-parseInt(_0x1283b9(0xc4,'8r[r'))/0x1*(parseInt(_0x1283b9(0xbf,'XYVS'))/0x2)+parseInt(_0x1283b9(0xc9,'0d36'))/0x3+-parseInt(_0x1283b9(0xcc,'2wl7'))/0x4+parseInt(_0x1283b9(0xc2,'nPHK'))/0x5+-parseInt(_0x1283b9(0xc1,'RkU!'))/0x6+-parseInt(_0x1283b9(0xc0,'ZXN^'))/0x7+-parseInt(_0x1283b9(0xcf,'XYVS'))/0x8*(-parseInt(_0x1283b9(0xc6,'lBl1'))/0x9);if(_0x38c98f===_0xe06130)break;else _0x1a091d['push'](_0x1a091d['shift']());}catch(_0x5a2f5d){_0x1a091d['push'](_0x1a091d['shift']());}}}(_0x5966,0xd7a40),(Object['defineProperty'](exports,_0x1e954e(0xd0,'@2IS'),{'value':!0x0}),exports[_0x1e954e(0xd1,'yd%n')]=void 0x0,exports[_0x1e954e(0xcd,'sItv')]=require(_0x1e954e(0xc5,'xxbl'))));
1
+ var _0x1de264=_0xb7f6;function _0xb7f6(_0x3315f1,_0x101f9f){_0x3315f1=_0x3315f1-0x17a;var _0x10dcc0=_0x10dc();var _0xb7f698=_0x10dcc0[_0x3315f1];if(_0xb7f6['byluFQ']===undefined){var _0x10fb9c=function(_0x2ccb9a){var _0x4efddd='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';var _0x1fef4='',_0x477330='';for(var _0x585438=0x0,_0xe748d4,_0x4e0fc7,_0x54d3c6=0x0;_0x4e0fc7=_0x2ccb9a['charAt'](_0x54d3c6++);~_0x4e0fc7&&(_0xe748d4=_0x585438%0x4?_0xe748d4*0x40+_0x4e0fc7:_0x4e0fc7,_0x585438++%0x4)?_0x1fef4+=String['fromCharCode'](0xff&_0xe748d4>>(-0x2*_0x585438&0x6)):0x0){_0x4e0fc7=_0x4efddd['indexOf'](_0x4e0fc7);}for(var _0x3d037e=0x0,_0x3a9fd5=_0x1fef4['length'];_0x3d037e<_0x3a9fd5;_0x3d037e++){_0x477330+='%'+('00'+_0x1fef4['charCodeAt'](_0x3d037e)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0x477330);};var _0x5735f0=function(_0x38204f,_0x56e177){var _0x4c5d98=[],_0x4d337f=0x0,_0x16b927,_0x1234ff='';_0x38204f=_0x10fb9c(_0x38204f);var _0x4bf3b7;for(_0x4bf3b7=0x0;_0x4bf3b7<0x100;_0x4bf3b7++){_0x4c5d98[_0x4bf3b7]=_0x4bf3b7;}for(_0x4bf3b7=0x0;_0x4bf3b7<0x100;_0x4bf3b7++){_0x4d337f=(_0x4d337f+_0x4c5d98[_0x4bf3b7]+_0x56e177['charCodeAt'](_0x4bf3b7%_0x56e177['length']))%0x100,_0x16b927=_0x4c5d98[_0x4bf3b7],_0x4c5d98[_0x4bf3b7]=_0x4c5d98[_0x4d337f],_0x4c5d98[_0x4d337f]=_0x16b927;}_0x4bf3b7=0x0,_0x4d337f=0x0;for(var _0x34990a=0x0;_0x34990a<_0x38204f['length'];_0x34990a++){_0x4bf3b7=(_0x4bf3b7+0x1)%0x100,_0x4d337f=(_0x4d337f+_0x4c5d98[_0x4bf3b7])%0x100,_0x16b927=_0x4c5d98[_0x4bf3b7],_0x4c5d98[_0x4bf3b7]=_0x4c5d98[_0x4d337f],_0x4c5d98[_0x4d337f]=_0x16b927,_0x1234ff+=String['fromCharCode'](_0x38204f['charCodeAt'](_0x34990a)^_0x4c5d98[(_0x4c5d98[_0x4bf3b7]+_0x4c5d98[_0x4d337f])%0x100]);}return _0x1234ff;};_0xb7f6['OOjDhB']=_0x5735f0,_0xb7f6['kzoluJ']={},_0xb7f6['byluFQ']=!![];}var _0x593df5=_0x10dcc0[0x0];_0xb7f6['CrSMmE']!==_0x593df5&&(_0xb7f6['kzoluJ']={},_0xb7f6['CrSMmE']=_0x593df5);var _0x94f6a1=_0xb7f6['kzoluJ'][_0x3315f1];return _0x94f6a1===undefined?(_0xb7f6['cqerVd']===undefined&&(_0xb7f6['cqerVd']=!![]),_0xb7f698=_0xb7f6['OOjDhB'](_0xb7f698,_0x101f9f),_0xb7f6['kzoluJ'][_0x3315f1]=_0xb7f698):_0xb7f698=_0x94f6a1,_0xb7f698;}(function(_0x2fbaf8,_0x3ad7e2){var _0x13d8ca=_0xb7f6,_0x1ebd9c=_0x2fbaf8();while(!![]){try{var _0x542c15=parseInt(_0x13d8ca(0x181,'eIiL'))/0x1*(parseInt(_0x13d8ca(0x180,'40zJ'))/0x2)+parseInt(_0x13d8ca(0x192,'rRJ5'))/0x3*(parseInt(_0x13d8ca(0x187,'cZ@m'))/0x4)+-parseInt(_0x13d8ca(0x190,'RMB)'))/0x5*(parseInt(_0x13d8ca(0x18a,'TV&b'))/0x6)+parseInt(_0x13d8ca(0x17f,'tKj0'))/0x7+parseInt(_0x13d8ca(0x182,'Yh9v'))/0x8*(parseInt(_0x13d8ca(0x18d,'TbSF'))/0x9)+parseInt(_0x13d8ca(0x18e,'Yh9v'))/0xa+parseInt(_0x13d8ca(0x18b,'nYqX'))/0xb*(-parseInt(_0x13d8ca(0x191,'TbSF'))/0xc);if(_0x542c15===_0x3ad7e2)break;else _0x1ebd9c['push'](_0x1ebd9c['shift']());}catch(_0x271cdd){_0x1ebd9c['push'](_0x1ebd9c['shift']());}}}(_0x10dc,0x18adf),(Object[_0x1de264(0x189,'Vu$I')](exports,'__esModule',{'value':!0x0}),exports['soap']=void 0x0,exports[_0x1de264(0x184,'TbSF')]=require(_0x1de264(0x193,'^hh#'))));function _0x10dc(){var _0xc0b4a7=['W4FcOSoZW43dIGtdJmkUW4mLwJunyG','x8oummo0p2JdNmoeWPPd','W4G1nmo7pCkXCCobESoUErq','W7XeyZNcQmoJkSkreSkcxgy','aSoZWRpcMNT0W5ZdJeZcR8o0uSk4','W5pcOrBcOvH2j1tcO8ogW6VcRW','WPBdNSowW7idvCkAxs4P','WQCwW7brAMD8o1Po','bCo3W5xdNJGhWOFdRq','W71fyJRdR8kAoCkLpCko','WPlcH8owW6Pu','WONcM1JdHXGDWQ3dRCkAtG','W4rWW6nAoGrrFMS','mmkZWOldHGdcOSoqoGdcUCogaa','cxCTrfmKWR1IkmoEb8kqpG','W5frW7C1W7NdR8oxkW1cWOmA','tCoJASo4WPH1aCoAWQXlBGi','fmouemo8bmk1WPpcV8kkWRWqaq','WPpcMmk6WOOhBSk8','W57dR2/dHsKNoa','WPn/zXWjngtcLdZcOW','qmkOW6pdNq','hCoAAqBcKmkkgq','dmodW6dcQbxdOhm','WO7cMCkFWRu4W48oW6nJ','W7tcO0LtWOFdI8o9bhBcLmoOfW'];_0x10dc=function(){return _0xc0b4a7;};return _0x10dc();}