myfirstdemoprojectkulliax 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ankul
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,255 @@
1
+ # cds-csrf-cache
2
+
3
+ A CAP plugin that caches the CSRF token for every destination-backed S/4 remote service, instead
4
+ of letting a fresh token be fetched on every single write.
5
+
6
+ ## Why
7
+
8
+ The SAP Cloud SDK's CSRF middleware (used internally by `cds.RemoteService` for a destination with
9
+ `csrf` configured) fetches a brand-new token before every non-GET request. S/4 tokens are valid for
10
+ 30 minutes, so almost all of those fetches are redundant - they just double the number of requests
11
+ sent for every `POST`/`PUT`/`DELETE`.
12
+
13
+ This plugin fetches the token once, keeps it in memory, and refreshes it proactively in the
14
+ background before it expires, so request-path latency no longer includes a token fetch except for
15
+ the very first request after startup (or after an unexpected rejection).
16
+
17
+ ## How it works
18
+
19
+ - **`CsrfTokenCache`** (`src/CsrfTokenCache.ts`) is the actual cache. It is framework-agnostic - it
20
+ only needs an async `fetchToken()` function and knows nothing about HTTP or CAP. It:
21
+ - serves the cached token as long as it is within `validitySeconds` of being fetched,
22
+ - schedules a background refresh `bufferSeconds` before that limit, so the buffer only decides
23
+ *when* to refresh early - it does not shrink how long a token may still be used if that
24
+ background refresh fails; the cache keeps serving the last known token until the real
25
+ `validitySeconds` limit is hit,
26
+ - deduplicates concurrent callers into a single in-flight fetch,
27
+ - exposes `invalidate()` for a caller that learns the token was rejected out of band.
28
+
29
+ - **Two independent token-fetch implementations** plug into that cache, matching how the SAP Cloud
30
+ SDK and native `fetch` are already treated as separate paths elsewhere in this project - they are
31
+ never merged into one "smart" client:
32
+ - `buildCsrfTokenFetcher` (`src/csrfFetch.ts`) uses Node's native `fetch` directly against a plain
33
+ URL. It has no SAP Cloud SDK or CAP dependency and works against any plain HTTP(S) endpoint -
34
+ the piece to reach for outside of any CAP service.
35
+ - `buildCapDestinationCsrfTokenFetcher` (`src/capDestinationTokenFetcher.ts`) is what
36
+ `attachCsrfCache` uses for a connected `cds.RemoteService`. It does **not** hardcode a client:
37
+ it reuses CAP's own `shouldUseCloudSdk()` decision
38
+ (`@sap/cds/libx/_runtime/remote/utils/cloudSdkProvider.js`) - the same function
39
+ `cds.RemoteService` itself calls for every other request against that destination - and picks
40
+ the SAP Cloud SDK or CAP's own native-fetch executor accordingly. A BTP destination that routes
41
+ through Cloud Connector (on-premise) or requires a client certificate always resolves to the
42
+ Cloud SDK here too, for the same reason `cds.RemoteService` always uses it for that destination:
43
+ that connectivity configuration only resolves into an Axios/Node-`https.Agent` shape, which
44
+ native `fetch` (undici) cannot consume. Both `@sap/cds` internals this reaches into are
45
+ exported-but-undocumented; if a future `@sap/cds` release moves them, the require fails
46
+ gracefully and the fetcher tries the Cloud SDK first instead.
47
+
48
+ The SAP Cloud SDK itself stays optional throughout, exactly as it is for `@sap/cds` - a project
49
+ that only talks to plain/local destinations can run without `@sap-cloud-sdk/http-client` installed
50
+ at all. `@sap-cloud-sdk/http-client` is only ever `require()`'d lazily, at the point a token fetch
51
+ actually needs it, never as a static import; if it isn't installed, CAP's own `shouldUseCloudSdk()`
52
+ already says so (via its own `isCloudSdkInstalled()` check) and the token fetch goes through native
53
+ fetch instead - the plugin's `package.json` marks it an optional peer dependency accordingly.
54
+
55
+ - **`createCsrfFetch`** (`src/csrfFetch.ts`) wraps any fetch-compatible function so every mutating
56
+ request is armed with the cached token (and its session cookie), and retried exactly once with a
57
+ freshly fetched token if the backend answers `403` + `x-csrf-token: Required`. This half is fully
58
+ usable on its own, outside of CAP, against any endpoint reachable via native `fetch`.
59
+
60
+ - **`attachCsrfCache`** (`src/attachCsrfCache.ts`) is the CAP-specific glue for a `cds.RemoteService`:
61
+ it registers a `before("*")` handler that injects the cached token/cookie into every non-safe
62
+ (non-`GET`/`HEAD`) outgoing request, and wraps `send()` so a `403` CSRF rejection invalidates the
63
+ cache and retries once. It reads the very same `csrf` config CAP itself reads
64
+ (`.cdsrc.json`'s `requires.<service>.csrf`, a sibling of `credentials` - see Configuration below).
65
+
66
+ - **`src/sharedCsrfCaches.ts`** is the registry behind `csrf.share`: instead of one cache per service,
67
+ every service on the same destination can be handed *one* `CsrfTokenCache`, so a single token
68
+ fetch serves all of them - and a token rejected on one of them is refreshed once for all of them.
69
+ An S/4 CSRF token is bound to the HTTP session behind the destination, not to the OData service
70
+ path it was fetched from, which is what makes this valid; see Configuration below for what
71
+ exactly has to match before two services are allowed to share.
72
+
73
+ - **`cds-plugin.js`** is the actual CAP plugin entry point. CAP auto-detects and loads it for every
74
+ package listed as a dependency that has this exact file name next to its `package.json`. It
75
+ listens for the `served` lifecycle event and calls `attachCsrfCache` on every connected
76
+ `cds.RemoteService` that has a destination and a `csrf` configuration - no service needs to call
77
+ anything itself. A service without that configuration, or with `csrf.cache: false`, is left
78
+ untouched (e.g. a `--with-mocks` stand-in used in dev/test). It `require`s the implementation
79
+ lazily, inside that handler, and never at load time: `cds-env` requires this file in *every*
80
+ tool that merely loads `@sap/cds` - `cds-typer`, `cds build`, eslint - and those run in a plain
81
+ CJS runtime, so a top-level `require` of the compiled implementation would run just as well, but
82
+ the lazy load also means a consuming project that pulls in this package before its `lib/` build
83
+ output exists (e.g. a `file:`/git dependency checked out from source) fails gracefully instead of
84
+ taking the whole host application down with it.
85
+
86
+ ## Setup
87
+
88
+ 1. Install the package:
89
+ ```sh
90
+ npm install cds-csrf-cache
91
+ ```
92
+ CAP auto-detects the `cds-plugin.js` next to its `package.json` in `node_modules` - this is what
93
+ makes the plugin loader discover it, no further wiring required.
94
+ 2. Every remote service already configured with a destination and a `csrf` entry (`.cdsrc.json`'s
95
+ `requires.<service>.csrf`, either `{ "url": "..." }` or `true`) picks up the cache automatically
96
+ the next time the server starts.
97
+
98
+ ## Sample application
99
+
100
+ [`sample/`](sample/) is a minimal runnable CAP app that uses this plugin against a fake CSRF-protected
101
+ backend - see [`sample/README.md`](sample/README.md) for how to run it and what to look at.
102
+
103
+ ## Configuration
104
+
105
+ ### Per-service, via the existing `csrf` config
106
+
107
+ This plugin extends the very `csrf` object CAP itself already reads
108
+ (`.cdsrc.json`'s `requires.<service>.csrf`) with three extra, plugin-specific settings:
109
+
110
+ ```json
111
+ {
112
+ "requires": {
113
+ "zsd_o2c_order_processing": {
114
+ "csrf": {
115
+ "method": "get",
116
+ "url": "/sap/opu/odata4/.../zsd_o2c_order_processing/0001/",
117
+ "cache": true,
118
+ "validitySeconds": 1500,
119
+ "autoRefresh": true,
120
+ "share": true
121
+ }
122
+ }
123
+ }
124
+ }
125
+ ```
126
+
127
+ | Field | Read by | Meaning |
128
+ |-------------------|--------------|--------------------------------------------------------------------------------------------|
129
+ | `url` | CAP | Path the CSRF preflight is sent to. |
130
+ | `method` | CAP + plugin | Verb for the preflight. CAP itself defaults to `head` if unset; this plugin defaults to `get` (broader OData compatibility) when it isn't given. |
131
+ | `cache` | plugin only | Set to `false` to leave this service on CAP's default per-request CSRF handling instead of caching. Defaults to `true`. |
132
+ | `validitySeconds` | plugin only | How long a fetched token is trusted before this plugin proactively re-fetches it. Falls back to the environment variable / hardcoded default below when unset. |
133
+ | `autoRefresh` | plugin only | Set to `false` to only fetch a replacement token lazily, on the first request after it went stale, instead of proactively in the background. Defaults to `true`. |
134
+ | `share` | plugin only | Set to `true` to share one cached token with every other service on the same destination instead of caching one per service, or to a string to share only within that named group. Defaults to the `csrf_token_share` environment variable, i.e. to `false`. |
135
+
136
+ `csrf: true` (no object, e.g. `zapi_sales_order_srv`) is also supported, exactly like CAP's own
137
+ default: the fetch URL falls back to the service's `credentials.path`, and every plugin-only field
138
+ falls back to its default.
139
+
140
+ ### One token for several services on the same destination (`share`)
141
+
142
+ By default every remote service keeps a token of its own - four services on `s4-o2c-100` mean four
143
+ token fetches, four background refreshes, and four separate recoveries after an expiry. `share`
144
+ collapses those into one:
145
+
146
+ ```json
147
+ {
148
+ "requires": {
149
+ "zsd_o2c_order_processing": { "csrf": { "url": "...", "share": true } },
150
+ "api_purchaseorder_2": { "csrf": { "url": "...", "share": true } },
151
+ "api_purchaserequisition_2":{ "csrf": { "url": "...", "share": true } },
152
+ "zapi_sales_order_srv": { "csrf": { "url": "...", "share": true } }
153
+ }
154
+ }
155
+ ```
156
+
157
+ This is safe because an S/4 CSRF token is bound to the HTTP session behind the destination, not to
158
+ the OData service path it was fetched from: a token fetched via `zsd_o2c_order_processing`'s URL is
159
+ accepted on `api_purchaseorder_2` just as well, as long as both requests really run against the
160
+ same session. Two services therefore only share when **all** of this matches:
161
+
162
+ - the **destination name** (`credentials.destination`) - a token from another system or another
163
+ backend client is not valid, and is never shared across destinations, even with `share: true` on
164
+ both sides;
165
+ - the **`destinationOptions`** - they decide which concrete destination, and therefore which
166
+ backend user/session, a destination *name* resolves to (`selectionStrategy`, `jwt`, ...); a
167
+ different resolution can mean a different session, so it counts as a different token. The
168
+ comparison is order-insensitive, so writing the same options in a different order still shares;
169
+ - the **share group**, if used: `"share": "writes"` shares only with other services configured with
170
+ exactly that name. Use it to keep one service's token separate while the rest share one, e.g.
171
+ when two services on one destination address different backend clients.
172
+
173
+ The first service served creates the shared cache, and its `url`, `method`, `validitySeconds` and
174
+ `autoRefresh` are the ones the shared cache runs with; a service joining with different settings is
175
+ served the existing cache and logged as a warning (a second cache on the same destination is exactly
176
+ what `share` was turned on to avoid). Consequently `invalidate()` after a `403` on *any* of the
177
+ participating services refreshes the token for all of them.
178
+
179
+ Turn it on for everything at once - without touching each service - with the `csrf_token_share`
180
+ environment variable below. A single service can still opt out again with `"share": false`.
181
+
182
+ ### Environment-wide defaults
183
+
184
+ Used whenever a service's `csrf` config doesn't set the corresponding field:
185
+
186
+ | Environment variable | Default | Meaning |
187
+ |--------------------------------|---------|---------------------------------------------------------------------|
188
+ | `csrf_token_validity_seconds` | `1800` | How long a token stays valid on the backend (S/4 default: 30 min) |
189
+ | `csrf_token_buffer_seconds` | `60` | How long before that limit the cache proactively refreshes |
190
+ | `csrf_token_share` | `false` | Default for `csrf.share` - set to `true`/`1`/`yes`/`on` to let all services of a destination share one token |
191
+
192
+ ### Programmatic override
193
+
194
+ For cases the config can't express (mainly tests), pass options directly to `attachCsrfCache` - they
195
+ win over both the `csrf` config and the environment defaults:
196
+
197
+ ```ts
198
+ import { attachCsrfCache } from "cds-csrf-cache"
199
+
200
+ attachCsrfCache(srv, { validitySeconds: 900, bufferSeconds: 30, share: true })
201
+ ```
202
+
203
+ `share` works the same way here as in the config, and wins over it - `{ share: false }` keeps a
204
+ service on its own cache even though its `csrf` config asks for a shared one. `resetSharedCsrfCaches()`
205
+ disposes every shared cache and empties the registry; tests that attach shared caches should call it
206
+ between cases.
207
+
208
+ ## Using the native-fetch half directly
209
+
210
+ Outside of any CAP service - e.g. a plain script or a non-CAP integration - the cache and the
211
+ native-fetch adapter can be used standalone:
212
+
213
+ ```ts
214
+ import { CsrfTokenCache, buildCsrfTokenFetcher, createCsrfFetch } from "cds-csrf-cache"
215
+
216
+ const cache = new CsrfTokenCache(buildCsrfTokenFetcher("https://example.com/service/"))
217
+ const protectedFetch = createCsrfFetch(cache)
218
+
219
+ await protectedFetch("https://example.com/service/Entities", { method: "POST", body: "{}" })
220
+ ```
221
+
222
+ ## Tests
223
+
224
+ - `test/*.spec.ts` - unit tests (Vitest, run via `npm test`), covering the cache's timing
225
+ behavior (caching, proactive refresh, hard expiry, invalidate, concurrent dedup), the native-fetch
226
+ adapter, the CAP-client selection in `capDestinationTokenFetcher`, the shared-cache registry
227
+ (scope identity, first-one-wins, reset) in `sharedCsrfCaches`, and the CAP wiring (config
228
+ parsing, header injection, retry-on-403, sharing) in `attachCsrfCache`.
229
+ - `test/CsrfCache.e2e.test.ts` - an end-to-end test (run via `npm run test.e2e`) that exercises
230
+ `CsrfTokenCache` and `createCsrfFetch` against a real local HTTP server standing in for an S/4
231
+ gateway, over real sockets and real timers: caching across repeated requests, a timed proactive
232
+ refresh, and recovery from an out-of-band token rejection.
233
+
234
+ ## Development
235
+
236
+ - `npm run build` compiles `src/**/*.ts` to `lib/` (the shipped `main`/`types` entry point and what
237
+ `cds-plugin.js` requires at runtime) via `tsc`. `npm publish` runs it automatically
238
+ (`prepublishOnly`).
239
+ - `npm test` / `npm run test.e2e` run straight against the TypeScript sources in `src/` via Vitest -
240
+ no build needed for that.
241
+
242
+ ## Release
243
+
244
+ `.github/workflows/publish.yml` runs the test suite (Node 22/24) on every push and pull request
245
+ against `main`, and additionally publishes to npm when a tag matching `v*.*.*` is pushed:
246
+
247
+ 1. Bump `version` in `package.json` (e.g. `npm version minor`, which also creates the matching git
248
+ tag).
249
+ 2. Push the commit and the tag: `git push && git push --tags`.
250
+ 3. The `publish` job checks the pushed tag against `package.json`'s version, builds, and runs
251
+ `npm publish --provenance --access public`.
252
+
253
+ This requires an `NPM_TOKEN` repository secret - an npm automation token with publish rights for
254
+ `cds-csrf-cache` - and, since `--provenance` is used, the workflow running from this repository on
255
+ GitHub (provenance ties the published package to the exact commit/workflow run that built it).
package/cds-plugin.js ADDED
@@ -0,0 +1,31 @@
1
+ // Auto-detected and loaded by CAP's plugin loader (`cds serve` / `cds watch`) because this package
2
+ // is listed as a dependency and provides this exact file name next to its package.json
3
+ // (see capire docs "CDS Plugin Packages"). Wires the csrf token cache into every served
4
+ // destination-backed remote service - no service needs to call the plugin itself.
5
+ const cds = require("@sap/cds")
6
+
7
+ /**
8
+ * Loaded lazily, inside the `served` handler, and never at plugin-load time: this file is required
9
+ * by `cds-env` itself, i.e. by *every* tool that merely loads `@sap/cds` - `cds-typer`, `cds build`,
10
+ * eslint - and those load it in a plain CJS runtime with no TypeScript loader registered, where a
11
+ * top-level `require` of the compiled `./lib/attachCsrfCache.js` would otherwise run just as well,
12
+ * but wrapping it keeps a missing/un-built `lib/` (e.g. before this package's own `npm run build`
13
+ * has ever run) from taking down the whole host application - it just leaves CAP's default
14
+ * per-request csrf handling in place.
15
+ */
16
+ function loadAttachCsrfCache() {
17
+ try {
18
+ return require("./lib/attachCsrfCache").attachCsrfCache
19
+ } catch (error) {
20
+ cds.log("csrf-cache").error("could not load the csrf token cache, remote services keep CAP's per-request csrf handling", error)
21
+ return undefined
22
+ }
23
+ }
24
+
25
+ cds.on("served", all => {
26
+ const attachCsrfCache = loadAttachCsrfCache()
27
+ if (!attachCsrfCache) return
28
+
29
+ for (const srv of Object.values(all))
30
+ if (srv instanceof cds.RemoteService) attachCsrfCache(srv)
31
+ })
@@ -0,0 +1,56 @@
1
+ export type CsrfToken = {
2
+ token: string;
3
+ cookies: string[];
4
+ };
5
+ export type CsrfTokenFetcher = () => Promise<CsrfToken>;
6
+ export type CsrfTokenCacheOptions = {
7
+ /** How long a token stays valid on the backend, in seconds. S/4 defaults to 1800 (30 minutes). */
8
+ validitySeconds?: number;
9
+ /** How long before `validitySeconds` runs out the cache proactively re-fetches in the background. */
10
+ bufferSeconds?: number;
11
+ /** Proactively re-fetch `bufferSeconds` before expiry instead of only fetching lazily on demand. Defaults to `true`. */
12
+ autoRefresh?: boolean;
13
+ };
14
+ /** Shared by every token-fetch implementation and by `attachCsrfCache`'s 403 detection, so the header name and its "Required" rejection value are spelled out exactly once. */
15
+ export declare const CSRF_TOKEN_HEADER = "x-csrf-token";
16
+ /** A cookie's path/domain/etc. attributes are not valid in a request `Cookie` header - only `name=value` survives. */
17
+ export declare function toCookieHeader(cookies: string[]): string | undefined;
18
+ /** True for the exact rejection S/4 (and CAP itself) use to signal an expired/invalid token: `403` + `x-csrf-token: Required`. */
19
+ export declare function isCsrfRequiredRejection(status: number | undefined, csrfTokenHeaderValue: string | null | undefined): boolean;
20
+ /** Thrown by every token-fetch implementation when the preflight didn't come back with a usable token - one wording, so the two call sites can't drift apart. */
21
+ export declare function missingCsrfTokenError(url: string, status: number): Error;
22
+ /**
23
+ * Caches a CSRF token obtained from `fetchToken` and proactively re-fetches it `bufferSeconds`
24
+ * before `validitySeconds` (the backend-side lifetime, 30 minutes on S/4) runs out - the buffer is
25
+ * only a safety margin for *when* to refresh early, not a cut into the token's usable lifetime:
26
+ * `getToken()` keeps serving the last known token up to the full `validitySeconds`, even if a
27
+ * background refresh attempt failed, and only fetches synchronously once that hard limit is hit.
28
+ * Concurrent `getToken()` calls share a single in-flight fetch.
29
+ *
30
+ * Framework-agnostic by design: `fetchToken` can be backed by native `fetch` ({@link buildCsrfTokenFetcher})
31
+ * or by a connected `cds.RemoteService`'s destination ({@link buildCapDestinationCsrfTokenFetcher}) -
32
+ * the cache itself has no HTTP dependency.
33
+ */
34
+ export declare class CsrfTokenCache {
35
+ private readonly log;
36
+ private readonly validitySeconds;
37
+ private readonly bufferSeconds;
38
+ private readonly autoRefresh;
39
+ private readonly fetchToken;
40
+ private cached?;
41
+ private pending?;
42
+ private refreshTimer?;
43
+ constructor(fetchToken: CsrfTokenFetcher, options?: CsrfTokenCacheOptions);
44
+ /** Returns a valid token, fetching (and caching) one first if necessary - and logs which of the two it was, so the cache's effect is visible in the request log. */
45
+ getToken(): Promise<CsrfToken>;
46
+ /** Drops the cached token, forcing the next `getToken()` call to fetch a fresh one. */
47
+ invalidate(): void;
48
+ /** Clears the proactive refresh timer. Call on shutdown/in tests so the process/test can exit cleanly. */
49
+ dispose(): void;
50
+ /** Shared by `getToken()` and the proactive refresh timer, so a request racing the timer joins the same fetch - and logs whether it started that fetch or joined a running one. */
51
+ private triggerRefresh;
52
+ private refresh;
53
+ private scheduleRefresh;
54
+ private clearRefreshTimer;
55
+ }
56
+ //# sourceMappingURL=CsrfTokenCache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CsrfTokenCache.d.ts","sourceRoot":"","sources":["../src/CsrfTokenCache.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,SAAS,GAAG;IACpB,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,EAAE,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG,MAAM,OAAO,CAAC,SAAS,CAAC,CAAA;AAEvD,MAAM,MAAM,qBAAqB,GAAG;IAChC,kGAAkG;IAClG,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,qGAAqG;IACrG,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,wHAAwH;IACxH,WAAW,CAAC,EAAE,OAAO,CAAA;CACxB,CAAA;AAID,+KAA+K;AAC/K,eAAO,MAAM,iBAAiB,iBAAiB,CAAA;AAE/C,sHAAsH;AACtH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAEpE;AAED,kIAAkI;AAClI,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,oBAAoB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAE5H;AAED,iKAAiK;AACjK,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,KAAK,CAExE;AAKD;;;;;;;;;;;GAWG;AACH,qBAAa,cAAc;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA4B;IAChD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAQ;IACxC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAQ;IACtC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IAErC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAkB;IAC7C,OAAO,CAAC,MAAM,CAAC,CAAa;IAC5B,OAAO,CAAC,OAAO,CAAC,CAAoB;IACpC,OAAO,CAAC,YAAY,CAAC,CAA+B;gBAExC,UAAU,EAAE,gBAAgB,EAAE,OAAO,GAAE,qBAA0B;IAW7E,oKAAoK;IAC9J,QAAQ,IAAI,OAAO,CAAC,SAAS,CAAC;IAQpC,uFAAuF;IACvF,UAAU,IAAI,IAAI;IAKlB,0GAA0G;IAC1G,OAAO,IAAI,IAAI;IAIf,mLAAmL;IACnL,OAAO,CAAC,cAAc;YAUR,OAAO;IASrB,OAAO,CAAC,eAAe;IAYvB,OAAO,CAAC,iBAAiB;CAI5B"}
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.CsrfTokenCache = exports.CSRF_TOKEN_HEADER = void 0;
7
+ exports.toCookieHeader = toCookieHeader;
8
+ exports.isCsrfRequiredRejection = isCsrfRequiredRejection;
9
+ exports.missingCsrfTokenError = missingCsrfTokenError;
10
+ const cds_1 = __importDefault(require("@sap/cds"));
11
+ /** Shared by every token-fetch implementation and by `attachCsrfCache`'s 403 detection, so the header name and its "Required" rejection value are spelled out exactly once. */
12
+ exports.CSRF_TOKEN_HEADER = "x-csrf-token";
13
+ /** A cookie's path/domain/etc. attributes are not valid in a request `Cookie` header - only `name=value` survives. */
14
+ function toCookieHeader(cookies) {
15
+ return cookies.length ? cookies.map(cookie => cookie.split(";")[0]).join("; ") : undefined;
16
+ }
17
+ /** True for the exact rejection S/4 (and CAP itself) use to signal an expired/invalid token: `403` + `x-csrf-token: Required`. */
18
+ function isCsrfRequiredRejection(status, csrfTokenHeaderValue) {
19
+ return status === 403 && csrfTokenHeaderValue?.toLowerCase() === "required";
20
+ }
21
+ /** Thrown by every token-fetch implementation when the preflight didn't come back with a usable token - one wording, so the two call sites can't drift apart. */
22
+ function missingCsrfTokenError(url, status) {
23
+ return new Error(`Could not obtain a CSRF token from ${url} (HTTP ${status})`);
24
+ }
25
+ const DEFAULT_VALIDITY_SECONDS = Number(process.env.csrf_token_validity_seconds ?? 1800);
26
+ const DEFAULT_BUFFER_SECONDS = Number(process.env.csrf_token_buffer_seconds ?? 60);
27
+ /**
28
+ * Caches a CSRF token obtained from `fetchToken` and proactively re-fetches it `bufferSeconds`
29
+ * before `validitySeconds` (the backend-side lifetime, 30 minutes on S/4) runs out - the buffer is
30
+ * only a safety margin for *when* to refresh early, not a cut into the token's usable lifetime:
31
+ * `getToken()` keeps serving the last known token up to the full `validitySeconds`, even if a
32
+ * background refresh attempt failed, and only fetches synchronously once that hard limit is hit.
33
+ * Concurrent `getToken()` calls share a single in-flight fetch.
34
+ *
35
+ * Framework-agnostic by design: `fetchToken` can be backed by native `fetch` ({@link buildCsrfTokenFetcher})
36
+ * or by a connected `cds.RemoteService`'s destination ({@link buildCapDestinationCsrfTokenFetcher}) -
37
+ * the cache itself has no HTTP dependency.
38
+ */
39
+ class CsrfTokenCache {
40
+ log;
41
+ validitySeconds;
42
+ bufferSeconds;
43
+ autoRefresh;
44
+ fetchToken;
45
+ cached;
46
+ pending;
47
+ refreshTimer;
48
+ constructor(fetchToken, options = {}) {
49
+ this.fetchToken = fetchToken;
50
+ this.validitySeconds = options.validitySeconds ?? DEFAULT_VALIDITY_SECONDS;
51
+ this.bufferSeconds = options.bufferSeconds ?? DEFAULT_BUFFER_SECONDS;
52
+ this.autoRefresh = options.autoRefresh ?? true;
53
+ this.log = cds_1.default.log("csrf-cache");
54
+ if (this.bufferSeconds < 0 || this.bufferSeconds >= this.validitySeconds)
55
+ throw new Error(`csrf token buffer (${this.bufferSeconds}s) must be >= 0 and smaller than its validity (${this.validitySeconds}s)`);
56
+ }
57
+ /** Returns a valid token, fetching (and caching) one first if necessary - and logs which of the two it was, so the cache's effect is visible in the request log. */
58
+ async getToken() {
59
+ if (this.cached && this.cached.expiresAt > Date.now()) {
60
+ this.log.debug(`csrf token taken from cache, valid until ${new Date(this.cached.expiresAt).toISOString()}`);
61
+ return this.cached;
62
+ }
63
+ return this.triggerRefresh();
64
+ }
65
+ /** Drops the cached token, forcing the next `getToken()` call to fetch a fresh one. */
66
+ invalidate() {
67
+ this.cached = undefined;
68
+ this.clearRefreshTimer();
69
+ }
70
+ /** Clears the proactive refresh timer. Call on shutdown/in tests so the process/test can exit cleanly. */
71
+ dispose() {
72
+ this.clearRefreshTimer();
73
+ }
74
+ /** Shared by `getToken()` and the proactive refresh timer, so a request racing the timer joins the same fetch - and logs whether it started that fetch or joined a running one. */
75
+ triggerRefresh() {
76
+ if (this.pending) {
77
+ this.log.debug("csrf token fetch already in flight, joining it");
78
+ return this.pending;
79
+ }
80
+ this.log.debug("requesting a new csrf token");
81
+ return this.pending = this.refresh().finally(() => { this.pending = undefined; });
82
+ }
83
+ async refresh() {
84
+ const fetchedAt = Date.now();
85
+ const token = await this.fetchToken();
86
+ this.cached = { ...token, expiresAt: fetchedAt + this.validitySeconds * 1000 };
87
+ this.log.info(`fetched a new csrf token, valid until ${new Date(this.cached.expiresAt).toISOString()}`);
88
+ this.scheduleRefresh(fetchedAt);
89
+ return this.cached;
90
+ }
91
+ scheduleRefresh(fetchedAt) {
92
+ this.clearRefreshTimer();
93
+ if (!this.autoRefresh)
94
+ return;
95
+ const refreshAt = fetchedAt + (this.validitySeconds - this.bufferSeconds) * 1000;
96
+ this.refreshTimer = setTimeout(() => {
97
+ this.triggerRefresh().catch(error => this.log.warn("proactive csrf token refresh failed, keeping the last known token until it expires", error));
98
+ }, Math.max(0, refreshAt - Date.now()));
99
+ this.refreshTimer.unref?.();
100
+ }
101
+ clearRefreshTimer() {
102
+ if (this.refreshTimer)
103
+ clearTimeout(this.refreshTimer);
104
+ this.refreshTimer = undefined;
105
+ }
106
+ }
107
+ exports.CsrfTokenCache = CsrfTokenCache;
108
+ //# sourceMappingURL=CsrfTokenCache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CsrfTokenCache.js","sourceRoot":"","sources":["../src/CsrfTokenCache.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ import cds from "@sap/cds";
2
+ import { CsrfTokenCache, CsrfTokenCacheOptions } from "./CsrfTokenCache";
3
+ /**
4
+ * Programmatic overrides for {@link attachCsrfCache} - the cache's own timing options, plus this
5
+ * plugin's `share` switch, which the `csrf` config expresses as `csrf.share`.
6
+ */
7
+ export type AttachCsrfCacheOptions = CsrfTokenCacheOptions & {
8
+ /** Overrides `csrf.share` for this service: share the destination's token cache (optionally within the named group) instead of caching a token of its own. */
9
+ share?: boolean | string;
10
+ };
11
+ /**
12
+ * Attaches a {@link CsrfTokenCache} to a connected `cds.RemoteService`: caches the CSRF token
13
+ * instead of letting CAP fetch a fresh one on every write (its csrf middleware - Cloud SDK or
14
+ * native fetch alike - only skips its own preflight once `x-csrf-token` is already present on the
15
+ * outgoing request), and retries once, with a fresh token, if the backend ever rejects the cached
16
+ * one as expired.
17
+ *
18
+ * A service without a destination, without any `csrf` configuration, or with `csrf.cache: false`
19
+ * (e.g. a `--with-mocks` stand-in used in dev/test) is left untouched - there is nothing to cache a
20
+ * token for, or the config explicitly opted out. Called automatically by this package's
21
+ * `cds-plugin.js` for every served remote service; only call it directly for a service the plugin's
22
+ * auto-discovery does not reach (e.g. a service served in a separate process).
23
+ *
24
+ * With `csrf.share` (or `options.share`) turned on, the service does not get a cache of its own but
25
+ * joins the one shared by every other service on the same destination - one token fetch for all of
26
+ * them instead of one each; see {@link acquireSharedCsrfCache}.
27
+ */
28
+ export declare function attachCsrfCache(srv: cds.RemoteService, options?: AttachCsrfCacheOptions): CsrfTokenCache | undefined;
29
+ //# sourceMappingURL=attachCsrfCache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachCsrfCache.d.ts","sourceRoot":"","sources":["../src/attachCsrfCache.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,UAAU,CAAA;AAC1B,OAAO,EAAqB,cAAc,EAAE,qBAAqB,EAA2C,MAAM,kBAAkB,CAAA;AAgDpI;;;GAGG;AACH,MAAM,MAAM,sBAAsB,GAAG,qBAAqB,GAAG;IACzD,8JAA8J;IAC9J,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,CAAA;CAC3B,CAAA;AAiCD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,GAAG,CAAC,aAAa,EAAE,OAAO,GAAE,sBAA2B,GAAG,cAAc,GAAG,SAAS,CA6DxH"}
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.attachCsrfCache = attachCsrfCache;
7
+ const cds_1 = __importDefault(require("@sap/cds"));
8
+ const CsrfTokenCache_1 = require("./CsrfTokenCache");
9
+ const capDestinationTokenFetcher_1 = require("./capDestinationTokenFetcher");
10
+ const sharedCsrfCaches_1 = require("./sharedCsrfCaches");
11
+ const LOG = cds_1.default.log("csrf-cache");
12
+ const SAFE_METHODS = new Set(["GET", "HEAD"]);
13
+ /** Environment-wide default for `csrf.share`, so all services of a destination can be switched to one token without touching each of them. */
14
+ const DEFAULT_SHARE = /^(true|1|yes|on)$/i.test(process.env.csrf_token_share ?? "");
15
+ /** `csrf.share` as a boolean plus an optional group name; an empty/whitespace-only string is treated as a plain `true`, not as a group called "". */
16
+ function resolveShare(share) {
17
+ if (share === undefined)
18
+ return { enabled: DEFAULT_SHARE };
19
+ if (typeof share === "string") {
20
+ const group = share.trim();
21
+ return group ? { enabled: true, group } : { enabled: true };
22
+ }
23
+ return { enabled: share };
24
+ }
25
+ /** The owner settings a shared cache actually runs with, so a service joining with different ones can be named in the warning. */
26
+ function describeSettings(csrfUrl, method, csrfConfig) {
27
+ return `url=${csrfUrl}, method=${method}, validitySeconds=${csrfConfig.validitySeconds ?? "default"}, autoRefresh=${csrfConfig.autoRefresh ?? "default"}`;
28
+ }
29
+ /** `csrf: true` (zapi_sales_order_srv) has no explicit fetch URL - the service's own root path is used, matching CAP's own default. */
30
+ function resolveCsrfConfig(srv) {
31
+ if (srv.csrf === true)
32
+ return { url: srv.path };
33
+ if (srv.csrf && typeof srv.csrf === "object")
34
+ return srv.csrf;
35
+ return undefined;
36
+ }
37
+ /** Only `get`/`head` make sense for a CSRF preflight; anything else falls back to `get`, same as an unset `csrf.method`. */
38
+ function normalizeCsrfMethod(method) {
39
+ return method?.toLowerCase() === "head" ? "head" : "get";
40
+ }
41
+ function isCsrfRejection(error) {
42
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
43
+ const response = error?.reason?.response;
44
+ const header = response?.headers?.[CsrfTokenCache_1.CSRF_TOKEN_HEADER] ?? response?.headers?.["X-CSRF-Token"];
45
+ return (0, CsrfTokenCache_1.isCsrfRequiredRejection)(response?.status, header);
46
+ }
47
+ /**
48
+ * Attaches a {@link CsrfTokenCache} to a connected `cds.RemoteService`: caches the CSRF token
49
+ * instead of letting CAP fetch a fresh one on every write (its csrf middleware - Cloud SDK or
50
+ * native fetch alike - only skips its own preflight once `x-csrf-token` is already present on the
51
+ * outgoing request), and retries once, with a fresh token, if the backend ever rejects the cached
52
+ * one as expired.
53
+ *
54
+ * A service without a destination, without any `csrf` configuration, or with `csrf.cache: false`
55
+ * (e.g. a `--with-mocks` stand-in used in dev/test) is left untouched - there is nothing to cache a
56
+ * token for, or the config explicitly opted out. Called automatically by this package's
57
+ * `cds-plugin.js` for every served remote service; only call it directly for a service the plugin's
58
+ * auto-discovery does not reach (e.g. a service served in a separate process).
59
+ *
60
+ * With `csrf.share` (or `options.share`) turned on, the service does not get a cache of its own but
61
+ * joins the one shared by every other service on the same destination - one token fetch for all of
62
+ * them instead of one each; see {@link acquireSharedCsrfCache}.
63
+ */
64
+ function attachCsrfCache(srv, options = {}) {
65
+ const { share: shareOverride, ...cacheOptions } = options;
66
+ const internals = srv;
67
+ const csrfConfig = resolveCsrfConfig(internals);
68
+ if (!internals.destination || !csrfConfig?.url || csrfConfig.cache === false) {
69
+ LOG.debug(`service '${srv.name}' has no cacheable destination-backed csrf configuration, skipping csrf token cache`);
70
+ return undefined;
71
+ }
72
+ const destination = internals.destination;
73
+ const destinationOptions = internals.destinationOptions ?? {};
74
+ const csrfUrl = csrfConfig.url;
75
+ const method = normalizeCsrfMethod(csrfConfig.method);
76
+ // Built inside `createCache` so a service that only joins an existing shared cache never
77
+ // constructs a token fetcher it would have no use for.
78
+ const createCache = () => new CsrfTokenCache_1.CsrfTokenCache((0, capDestinationTokenFetcher_1.buildCapDestinationCsrfTokenFetcher)(destination, destinationOptions, csrfUrl, method), {
79
+ validitySeconds: csrfConfig.validitySeconds,
80
+ autoRefresh: csrfConfig.autoRefresh,
81
+ ...cacheOptions
82
+ });
83
+ const share = resolveShare(shareOverride ?? csrfConfig.share);
84
+ const shared = share.enabled
85
+ ? (0, sharedCsrfCaches_1.acquireSharedCsrfCache)({ destination, destinationOptions, group: share.group }, srv.name, describeSettings(csrfUrl, method, csrfConfig), createCache)
86
+ : undefined;
87
+ const cache = shared?.cache ?? createCache();
88
+ srv.before("*", async (req) => {
89
+ if (SAFE_METHODS.has((req.method ?? "").toUpperCase()))
90
+ return;
91
+ try {
92
+ const token = await cache.getToken();
93
+ req.headers ??= {};
94
+ req.headers[CsrfTokenCache_1.CSRF_TOKEN_HEADER] = token.token;
95
+ const cookieHeader = (0, CsrfTokenCache_1.toCookieHeader)(token.cookies);
96
+ if (cookieHeader)
97
+ req.headers["cookie"] = cookieHeader;
98
+ }
99
+ catch (error) {
100
+ LOG.warn("could not obtain a cached csrf token, letting the request go through without one", error);
101
+ }
102
+ });
103
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
104
+ const send = srv.send.bind(srv);
105
+ srv.send = (async (...args) => {
106
+ try {
107
+ return await send(...args);
108
+ }
109
+ catch (error) {
110
+ if (!isCsrfRejection(error))
111
+ throw error;
112
+ LOG.warn(`service '${srv.name}' rejected the cached csrf token as expired, refreshing and retrying once`);
113
+ cache.invalidate();
114
+ return await send(...args);
115
+ }
116
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
117
+ });
118
+ if (!shared)
119
+ LOG.info(`csrf token cache attached to service '${srv.name}'`);
120
+ else if (shared.joined)
121
+ LOG.info(`service '${srv.name}' joined the csrf token cache shared for destination '${destination}' (created by '${shared.owner}')`);
122
+ else
123
+ LOG.info(`csrf token cache attached to service '${srv.name}', shared with every other service on destination '${destination}'`);
124
+ return cache;
125
+ }
126
+ //# sourceMappingURL=attachCsrfCache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachCsrfCache.js","sourceRoot":"","sources":["../src/attachCsrfCache.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ import { CsrfTokenFetcher } from "./CsrfTokenCache";
2
+ type NativeFetchResponse = {
3
+ data: unknown;
4
+ headers: Record<string, string | string[] | undefined>;
5
+ status: number;
6
+ };
7
+ type CloudSdkResponse = {
8
+ status: number;
9
+ headers: Record<string, string | string[] | undefined>;
10
+ };
11
+ type CloudSdkExecutor = (destination: unknown, requestConfig: unknown) => Promise<CloudSdkResponse>;
12
+ export type CapInternals = {
13
+ shouldUseCloudSdk: (destination: unknown) => boolean;
14
+ nativeFetch: (destination: unknown, requestConfig: unknown) => Promise<NativeFetchResponse>;
15
+ };
16
+ export type CsrfPreflightMethod = "get" | "head";
17
+ export type CapDestinationTokenFetcherOverrides = {
18
+ capInternals?: CapInternals | null;
19
+ cloudSdkFetch?: CloudSdkExecutor | null;
20
+ };
21
+ /**
22
+ * Builds a {@link CsrfTokenFetcher} for a connected `cds.RemoteService`'s destination, letting CAP's
23
+ * own `shouldUseCloudSdk()` decide between the SAP Cloud SDK and native `fetch` - see the module
24
+ * comment above for why that decision must not be hardcoded to one client, and why the Cloud SDK
25
+ * itself must stay optional. `overrides` exists so tests can supply fakes directly instead of
26
+ * mocking a runtime `require()` of a deep `node_modules` path.
27
+ */
28
+ export declare function buildCapDestinationCsrfTokenFetcher(destination: string, destinationOptions: Record<string, unknown>, csrfUrl: string, method?: CsrfPreflightMethod, overrides?: CapDestinationTokenFetcherOverrides): CsrfTokenFetcher;
29
+ export {};
30
+ //# sourceMappingURL=capDestinationTokenFetcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capDestinationTokenFetcher.d.ts","sourceRoot":"","sources":["../src/capDestinationTokenFetcher.ts"],"names":[],"mappings":"AACA,OAAO,EAAqB,gBAAgB,EAAyB,MAAM,kBAAkB,CAAA;AAI7F,KAAK,mBAAmB,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AACpH,KAAK,gBAAgB,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAA;CAAE,CAAA;AAGlG,KAAK,gBAAgB,GAAG,CAAC,WAAW,EAAE,OAAO,EAAE,aAAa,EAAE,OAAO,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAAA;AAEnG,MAAM,MAAM,YAAY,GAAG;IACvB,iBAAiB,EAAE,CAAC,WAAW,EAAE,OAAO,KAAK,OAAO,CAAA;IACpD,WAAW,EAAE,CAAC,WAAW,EAAE,OAAO,EAAE,aAAa,EAAE,OAAO,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAAA;CAC9F,CAAA;AAsED,MAAM,MAAM,mBAAmB,GAAG,KAAK,GAAG,MAAM,CAAA;AAEhD,MAAM,MAAM,mCAAmC,GAAG;IAC9C,YAAY,CAAC,EAAE,YAAY,GAAG,IAAI,CAAA;IAClC,aAAa,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAA;CAC1C,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,mCAAmC,CAC/C,WAAW,EAAE,MAAM,EACnB,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3C,OAAO,EAAE,MAAM,EACf,MAAM,GAAE,mBAA2B,EACnC,SAAS,GAAE,mCAAwC,GACpD,gBAAgB,CAwBlB"}
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.buildCapDestinationCsrfTokenFetcher = buildCapDestinationCsrfTokenFetcher;
7
+ const cds_1 = __importDefault(require("@sap/cds"));
8
+ const CsrfTokenCache_1 = require("./CsrfTokenCache");
9
+ const LOG = cds_1.default.log("csrf-cache");
10
+ /* eslint-enable no-unused-vars */
11
+ /**
12
+ * `cds.RemoteService` decides per request whether to route through the SAP Cloud SDK or Node's
13
+ * native `fetch`, via `shouldUseCloudSdk()` in `@sap/cds/libx/_runtime/remote/utils/cloudSdkProvider.js`
14
+ * (a BTP destination without a locally-resolvable URL, or with anything other than Basic/No auth,
15
+ * always forces the Cloud SDK - that is what makes an on-premise/Cloud-Connector/mTLS destination
16
+ * safe to call at all, since only the Cloud SDK resolves that connectivity configuration).
17
+ *
18
+ * `buildCapDestinationCsrfTokenFetcher` reuses that exact decision instead of hardcoding a client,
19
+ * so the token fetch always goes through the same path CAP itself would pick for the real request
20
+ * on this service. Neither module is part of `@sap/cds`'s public entry point - both are
21
+ * internal-but-exported (`module.exports` on their own files, just not re-exported from `@sap/cds`'s
22
+ * `index.js`), so a future `@sap/cds` release could move them without a deprecation notice.
23
+ * `resolveCapInternals` isolates that risk: if the require ever fails, every caller falls back to
24
+ * treating the SAP Cloud SDK as the only option - the same one CAP would use once `useCloudSdk`
25
+ * can no longer be asked for.
26
+ */
27
+ let capInternals;
28
+ function resolveCapInternals() {
29
+ if (capInternals !== undefined)
30
+ return capInternals;
31
+ try {
32
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- must be a runtime require: not part of @sap/cds's public entry point, see the module comment above
33
+ const { shouldUseCloudSdk } = require("@sap/cds/libx/_runtime/remote/utils/cloudSdkProvider");
34
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
35
+ const { executeHttpRequest: nativeFetch } = require("@sap/cds/libx/_runtime/remote/utils/fetchClient");
36
+ return capInternals = { shouldUseCloudSdk, nativeFetch };
37
+ }
38
+ catch (error) {
39
+ LOG.warn("could not load @sap/cds's internal client-selection module, always trying the Cloud SDK first for csrf token fetches", error);
40
+ return capInternals = null;
41
+ }
42
+ }
43
+ /**
44
+ * The SAP Cloud SDK is an optional peer dependency, exactly as it is for `@sap/cds` itself
45
+ * (`cloudSdkProvider.js`'s `getCloudSdk()`/`isCloudSdkInstalled()`): a project that only talks to
46
+ * plain/local destinations can run without `@sap-cloud-sdk/http-client` installed at all, and
47
+ * `shouldUseCloudSdk()` already returns `false` in that case. A static top-level import of the
48
+ * package would defeat that - it fails at module load time regardless of whether this branch is
49
+ * ever taken - so this, too, is a lazy, cached, failure-tolerant require.
50
+ */
51
+ let cloudSdkExecutor;
52
+ function resolveCloudSdkExecutor() {
53
+ if (cloudSdkExecutor !== undefined)
54
+ return cloudSdkExecutor;
55
+ try {
56
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
57
+ const { executeHttpRequest } = require("@sap-cloud-sdk/http-client");
58
+ return cloudSdkExecutor = executeHttpRequest;
59
+ }
60
+ catch {
61
+ return cloudSdkExecutor = null;
62
+ }
63
+ }
64
+ function firstCookie(setCookie) {
65
+ if (!setCookie)
66
+ return [];
67
+ return Array.isArray(setCookie) ? setCookie : [setCookie];
68
+ }
69
+ function tokenFrom(headers, status, csrfUrl) {
70
+ const token = headers[CsrfTokenCache_1.CSRF_TOKEN_HEADER];
71
+ if (status !== 200 || typeof token !== "string")
72
+ throw (0, CsrfTokenCache_1.missingCsrfTokenError)(csrfUrl, status);
73
+ return { token, cookies: firstCookie(headers["set-cookie"]) };
74
+ }
75
+ /**
76
+ * Builds a {@link CsrfTokenFetcher} for a connected `cds.RemoteService`'s destination, letting CAP's
77
+ * own `shouldUseCloudSdk()` decide between the SAP Cloud SDK and native `fetch` - see the module
78
+ * comment above for why that decision must not be hardcoded to one client, and why the Cloud SDK
79
+ * itself must stay optional. `overrides` exists so tests can supply fakes directly instead of
80
+ * mocking a runtime `require()` of a deep `node_modules` path.
81
+ */
82
+ function buildCapDestinationCsrfTokenFetcher(destination, destinationOptions, csrfUrl, method = "get", overrides = {}) {
83
+ const destinationRef = { destinationName: destination, ...destinationOptions };
84
+ const nativeMethod = method === "head" ? "HEAD" : "GET";
85
+ return async () => {
86
+ const internals = "capInternals" in overrides ? overrides.capInternals ?? null : resolveCapInternals();
87
+ // No way to ask CAP - default to trying the Cloud SDK first, same as when internals resolve fine and it turns out to be installed.
88
+ const useCloudSdk = internals ? internals.shouldUseCloudSdk(destination) : true;
89
+ if (useCloudSdk) {
90
+ const cloudSdkFetch = "cloudSdkFetch" in overrides ? overrides.cloudSdkFetch ?? null : resolveCloudSdkExecutor();
91
+ if (cloudSdkFetch) {
92
+ const response = await cloudSdkFetch(destinationRef, { method, url: csrfUrl, headers: { [CsrfTokenCache_1.CSRF_TOKEN_HEADER]: "Fetch" } });
93
+ return tokenFrom(response.headers, response.status, csrfUrl);
94
+ }
95
+ }
96
+ if (internals) {
97
+ const response = await internals.nativeFetch(destinationRef, { method: nativeMethod, url: csrfUrl, headers: { [CsrfTokenCache_1.CSRF_TOKEN_HEADER]: "Fetch" } });
98
+ return tokenFrom(response.headers, response.status, csrfUrl);
99
+ }
100
+ throw new Error(`Could not fetch a CSRF token from ${csrfUrl}: neither the SAP Cloud SDK nor @sap/cds's native fetch client is available.`);
101
+ };
102
+ }
103
+ //# sourceMappingURL=capDestinationTokenFetcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capDestinationTokenFetcher.js","sourceRoot":"","sources":["../src/capDestinationTokenFetcher.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import { CsrfTokenCache, CsrfTokenFetcher } from "./CsrfTokenCache";
2
+ /**
3
+ * Native-`fetch` token fetcher - kept deliberately separate from {@link buildCapDestinationCsrfTokenFetcher}
4
+ * (see that module for why): this one has no SAP Cloud SDK dependency at all and works with any
5
+ * plain HTTP(S) endpoint reachable via Node's global `fetch`.
6
+ *
7
+ * Builds a {@link CsrfTokenFetcher}: a GET request carrying `x-csrf-token: Fetch` against `url`,
8
+ * answered with the token in the same response header and the session cookie(s) needed to present
9
+ * it back on the next write. GET (not HEAD) is used because it is the verb every SAP gateway/OData
10
+ * implementation is guaranteed to answer a CSRF-fetch on, including services that don't implement HEAD.
11
+ */
12
+ export declare function buildCsrfTokenFetcher(url: string, init?: RequestInit, fetchImpl?: typeof fetch): CsrfTokenFetcher;
13
+ /**
14
+ * Wraps a fetch-compatible function so every non-safe request is transparently armed with the
15
+ * cached CSRF token, and retried exactly once - with a freshly fetched token - if the backend
16
+ * rejects it as expired (`403` + `x-csrf-token: Required`). Safe methods pass through untouched.
17
+ *
18
+ * This is the piece that makes the cache usable with plain native `fetch` calls, independent of
19
+ * any CAP/Cloud SDK service - see {@link buildCsrfTokenFetcher} for a matching token fetcher, and
20
+ * `attachCsrfCache` for the equivalent wiring against a `cds.RemoteService`.
21
+ */
22
+ export declare function createCsrfFetch(cache: CsrfTokenCache, fetchImpl?: typeof fetch): typeof fetch;
23
+ //# sourceMappingURL=csrfFetch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csrfFetch.d.ts","sourceRoot":"","sources":["../src/csrfFetch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgC,cAAc,EAAE,gBAAgB,EAAkE,MAAM,kBAAkB,CAAA;AAIjK;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,WAAgB,EAAE,SAAS,GAAE,OAAO,KAAa,GAAG,gBAAgB,CAc5H;AAcD;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,cAAc,EAAE,SAAS,GAAE,OAAO,KAAa,GAAG,OAAO,KAAK,CAepG"}
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildCsrfTokenFetcher = buildCsrfTokenFetcher;
4
+ exports.createCsrfFetch = createCsrfFetch;
5
+ const CsrfTokenCache_1 = require("./CsrfTokenCache");
6
+ const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS", "TRACE"]);
7
+ /**
8
+ * Native-`fetch` token fetcher - kept deliberately separate from {@link buildCapDestinationCsrfTokenFetcher}
9
+ * (see that module for why): this one has no SAP Cloud SDK dependency at all and works with any
10
+ * plain HTTP(S) endpoint reachable via Node's global `fetch`.
11
+ *
12
+ * Builds a {@link CsrfTokenFetcher}: a GET request carrying `x-csrf-token: Fetch` against `url`,
13
+ * answered with the token in the same response header and the session cookie(s) needed to present
14
+ * it back on the next write. GET (not HEAD) is used because it is the verb every SAP gateway/OData
15
+ * implementation is guaranteed to answer a CSRF-fetch on, including services that don't implement HEAD.
16
+ */
17
+ function buildCsrfTokenFetcher(url, init = {}, fetchImpl = fetch) {
18
+ return async () => {
19
+ const response = await fetchImpl(url, {
20
+ ...init,
21
+ method: "GET",
22
+ headers: { ...init.headers, [CsrfTokenCache_1.CSRF_TOKEN_HEADER]: "Fetch" }
23
+ });
24
+ const token = response.headers.get(CsrfTokenCache_1.CSRF_TOKEN_HEADER);
25
+ if (!response.ok || !token)
26
+ throw (0, CsrfTokenCache_1.missingCsrfTokenError)(url, response.status);
27
+ return { token, cookies: response.headers.getSetCookie?.() ?? [] };
28
+ };
29
+ }
30
+ function withCsrfHeaders(init, token) {
31
+ const headers = new Headers(init?.headers);
32
+ headers.set(CsrfTokenCache_1.CSRF_TOKEN_HEADER, token.token);
33
+ const cookieHeader = (0, CsrfTokenCache_1.toCookieHeader)(token.cookies);
34
+ if (cookieHeader)
35
+ headers.set("cookie", cookieHeader);
36
+ return headers;
37
+ }
38
+ function isCsrfRejection(response) {
39
+ return (0, CsrfTokenCache_1.isCsrfRequiredRejection)(response.status, response.headers.get(CsrfTokenCache_1.CSRF_TOKEN_HEADER));
40
+ }
41
+ /**
42
+ * Wraps a fetch-compatible function so every non-safe request is transparently armed with the
43
+ * cached CSRF token, and retried exactly once - with a freshly fetched token - if the backend
44
+ * rejects it as expired (`403` + `x-csrf-token: Required`). Safe methods pass through untouched.
45
+ *
46
+ * This is the piece that makes the cache usable with plain native `fetch` calls, independent of
47
+ * any CAP/Cloud SDK service - see {@link buildCsrfTokenFetcher} for a matching token fetcher, and
48
+ * `attachCsrfCache` for the equivalent wiring against a `cds.RemoteService`.
49
+ */
50
+ function createCsrfFetch(cache, fetchImpl = fetch) {
51
+ return (async (input, init) => {
52
+ const method = (init?.method ?? "GET").toUpperCase();
53
+ if (SAFE_METHODS.has(method))
54
+ return fetchImpl(input, init);
55
+ const token = await cache.getToken();
56
+ const response = await fetchImpl(input, { ...init, method, headers: withCsrfHeaders(init, token) });
57
+ if (!isCsrfRejection(response))
58
+ return response;
59
+ cache.invalidate();
60
+ const freshToken = await cache.getToken();
61
+ return fetchImpl(input, { ...init, method, headers: withCsrfHeaders(init, freshToken) });
62
+ });
63
+ }
64
+ //# sourceMappingURL=csrfFetch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csrfFetch.js","sourceRoot":"","sources":["../src/csrfFetch.ts"],"names":[],"mappings":";;AAcA,sDAcC;AAuBD,0CAeC;AAlED,qDAAiK;AAEjK,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC,CAAA;AAEjE;;;;;;;;;GASG;AACH,SAAgB,qBAAqB,CAAC,GAAW,EAAE,OAAoB,EAAE,EAAE,YAA0B,KAAK;IACtG,OAAO,KAAK,IAAI,EAAE;QACd,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE;YAClC,GAAG,IAAI;YACP,MAAM,EAAE,KAAK;YACb,OAAO,EAAE,EAAE,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,kCAAiB,CAAC,EAAE,OAAO,EAAE;SAC7D,CAAC,CAAA;QAEF,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,kCAAiB,CAAC,CAAA;QACrD,IAAI,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC,KAAK;YACtB,MAAM,IAAA,sCAAqB,EAAC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAA;QAErD,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,IAAI,EAAE,EAAE,CAAA;IACtE,CAAC,CAAA;AACL,CAAC;AAED,SAAS,eAAe,CAAC,IAA6B,EAAE,KAAgB;IACpE,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;IAC1C,OAAO,CAAC,GAAG,CAAC,kCAAiB,EAAE,KAAK,CAAC,KAAK,CAAC,CAAA;IAC3C,MAAM,YAAY,GAAG,IAAA,+BAAc,EAAC,KAAK,CAAC,OAAO,CAAC,CAAA;IAClD,IAAI,YAAY;QAAE,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAA;IACrD,OAAO,OAAO,CAAA;AAClB,CAAC;AAED,SAAS,eAAe,CAAC,QAAkB;IACvC,OAAO,IAAA,wCAAuB,EAAC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,kCAAiB,CAAC,CAAC,CAAA;AAC5F,CAAC;AAED;;;;;;;;GAQG;AACH,SAAgB,eAAe,CAAC,KAAqB,EAAE,YAA0B,KAAK;IAClF,OAAO,CAAC,KAAK,EAAE,KAAwB,EAAE,IAAkB,EAAE,EAAE;QAC3D,MAAM,MAAM,GAAG,CAAC,IAAI,EAAE,MAAM,IAAI,KAAK,CAAC,CAAC,WAAW,EAAE,CAAA;QACpD,IAAI,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC;YACxB,OAAO,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;QAEjC,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,QAAQ,EAAE,CAAA;QACpC,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,KAAK,EAAE,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC,CAAA;QACnG,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC;YAC1B,OAAO,QAAQ,CAAA;QAEnB,KAAK,CAAC,UAAU,EAAE,CAAA;QAClB,MAAM,UAAU,GAAG,MAAM,KAAK,CAAC,QAAQ,EAAE,CAAA;QACzC,OAAO,SAAS,CAAC,KAAK,EAAE,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,CAAC,IAAI,EAAE,UAAU,CAAC,EAAE,CAAC,CAAA;IAC5F,CAAC,CAAiB,CAAA;AACtB,CAAC"}
package/lib/index.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ export { CSRF_TOKEN_HEADER, CsrfTokenCache, toCookieHeader } from "./CsrfTokenCache";
2
+ export type { CsrfToken, CsrfTokenFetcher, CsrfTokenCacheOptions } from "./CsrfTokenCache";
3
+ export { buildCsrfTokenFetcher, createCsrfFetch } from "./csrfFetch";
4
+ export { buildCapDestinationCsrfTokenFetcher } from "./capDestinationTokenFetcher";
5
+ export { attachCsrfCache } from "./attachCsrfCache";
6
+ export type { AttachCsrfCacheOptions } from "./attachCsrfCache";
7
+ export { acquireSharedCsrfCache, resetSharedCsrfCaches, sharedCsrfCacheKey, sharedCsrfCacheMembers } from "./sharedCsrfCaches";
8
+ export type { SharedCsrfCache, SharedCsrfCacheScope } from "./sharedCsrfCaches";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AACpF,YAAY,EAAE,SAAS,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAA;AAC1F,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AACpE,OAAO,EAAE,mCAAmC,EAAE,MAAM,8BAA8B,CAAA;AAClF,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACnD,YAAY,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAA;AAC/D,OAAO,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AAC9H,YAAY,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA"}
package/lib/index.js ADDED
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sharedCsrfCacheMembers = exports.sharedCsrfCacheKey = exports.resetSharedCsrfCaches = exports.acquireSharedCsrfCache = exports.attachCsrfCache = exports.buildCapDestinationCsrfTokenFetcher = exports.createCsrfFetch = exports.buildCsrfTokenFetcher = exports.toCookieHeader = exports.CsrfTokenCache = exports.CSRF_TOKEN_HEADER = void 0;
4
+ var CsrfTokenCache_1 = require("./CsrfTokenCache");
5
+ Object.defineProperty(exports, "CSRF_TOKEN_HEADER", { enumerable: true, get: function () { return CsrfTokenCache_1.CSRF_TOKEN_HEADER; } });
6
+ Object.defineProperty(exports, "CsrfTokenCache", { enumerable: true, get: function () { return CsrfTokenCache_1.CsrfTokenCache; } });
7
+ Object.defineProperty(exports, "toCookieHeader", { enumerable: true, get: function () { return CsrfTokenCache_1.toCookieHeader; } });
8
+ var csrfFetch_1 = require("./csrfFetch");
9
+ Object.defineProperty(exports, "buildCsrfTokenFetcher", { enumerable: true, get: function () { return csrfFetch_1.buildCsrfTokenFetcher; } });
10
+ Object.defineProperty(exports, "createCsrfFetch", { enumerable: true, get: function () { return csrfFetch_1.createCsrfFetch; } });
11
+ var capDestinationTokenFetcher_1 = require("./capDestinationTokenFetcher");
12
+ Object.defineProperty(exports, "buildCapDestinationCsrfTokenFetcher", { enumerable: true, get: function () { return capDestinationTokenFetcher_1.buildCapDestinationCsrfTokenFetcher; } });
13
+ var attachCsrfCache_1 = require("./attachCsrfCache");
14
+ Object.defineProperty(exports, "attachCsrfCache", { enumerable: true, get: function () { return attachCsrfCache_1.attachCsrfCache; } });
15
+ var sharedCsrfCaches_1 = require("./sharedCsrfCaches");
16
+ Object.defineProperty(exports, "acquireSharedCsrfCache", { enumerable: true, get: function () { return sharedCsrfCaches_1.acquireSharedCsrfCache; } });
17
+ Object.defineProperty(exports, "resetSharedCsrfCaches", { enumerable: true, get: function () { return sharedCsrfCaches_1.resetSharedCsrfCaches; } });
18
+ Object.defineProperty(exports, "sharedCsrfCacheKey", { enumerable: true, get: function () { return sharedCsrfCaches_1.sharedCsrfCacheKey; } });
19
+ Object.defineProperty(exports, "sharedCsrfCacheMembers", { enumerable: true, get: function () { return sharedCsrfCaches_1.sharedCsrfCacheMembers; } });
20
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,mDAAoF;AAA3E,mHAAA,iBAAiB,OAAA;AAAE,gHAAA,cAAc,OAAA;AAAE,gHAAA,cAAc,OAAA;AAE1D,yCAAoE;AAA3D,kHAAA,qBAAqB,OAAA;AAAE,4GAAA,eAAe,OAAA;AAC/C,2EAAkF;AAAzE,iJAAA,mCAAmC,OAAA;AAC5C,qDAAmD;AAA1C,kHAAA,eAAe,OAAA;AAExB,uDAA8H;AAArH,0HAAA,sBAAsB,OAAA;AAAE,yHAAA,qBAAqB,OAAA;AAAE,sHAAA,kBAAkB,OAAA;AAAE,0HAAA,sBAAsB,OAAA"}
@@ -0,0 +1,48 @@
1
+ import { CsrfTokenCache } from "./CsrfTokenCache";
2
+ /**
3
+ * Everything that has to match before two remote services may present the *same* CSRF token.
4
+ *
5
+ * An S/4 CSRF token is bound to the HTTP session behind the destination, not to the OData service
6
+ * path it was fetched from - so a token fetched for `zsd_o2c_order_processing` is equally valid on
7
+ * `api_purchaseorder_2` as long as both go to the same system, through the same destination, with
8
+ * the same destination resolution (`destinationOptions` decide *which* concrete destination, and
9
+ * therefore which backend user/session, a name resolves to - a different `selectionStrategy` or a
10
+ * different `jwt` can mean a different session entirely, so they are part of the identity here).
11
+ *
12
+ * `group` narrows that further, for the case where two services share a destination but must not
13
+ * share a token (different backend clients behind one destination name, or simply to keep one
14
+ * service's token isolated while the rest share one).
15
+ */
16
+ export type SharedCsrfCacheScope = {
17
+ destination: string;
18
+ destinationOptions?: Record<string, unknown>;
19
+ group?: string;
20
+ };
21
+ /** The identity of a shared cache - see {@link SharedCsrfCacheScope} for why each part is in it. */
22
+ export declare function sharedCsrfCacheKey(scope: SharedCsrfCacheScope): string;
23
+ export type SharedCsrfCache = {
24
+ cache: CsrfTokenCache;
25
+ key: string;
26
+ /** `true` if an existing cache was joined, `false` if this call created it. */
27
+ joined: boolean;
28
+ /** The service that created the cache (the caller itself when `joined` is `false`). */
29
+ owner: string;
30
+ };
31
+ /**
32
+ * Returns the {@link CsrfTokenCache} for `scope`, creating it via `create` on first use and handing
33
+ * the very same instance to every later caller with the same scope. That is what lets several
34
+ * remote services on one destination fetch a single token instead of one each - and it also means
35
+ * an `invalidate()` after a rejected token (from *any* of them) refreshes the token for all of
36
+ * them at once.
37
+ *
38
+ * The first service in wins: the shared cache keeps fetching from that service's `csrf.url`, with
39
+ * its method and validity. A service joining with different settings is served the existing cache
40
+ * and warned about, rather than silently getting a second one - two caches on one destination is
41
+ * exactly what sharing was turned on to avoid.
42
+ */
43
+ export declare function acquireSharedCsrfCache(scope: SharedCsrfCacheScope, member: string, settings: string, create: () => CsrfTokenCache): SharedCsrfCache;
44
+ /** The services sharing the cache under `key`, owner first - for diagnostics and tests. */
45
+ export declare function sharedCsrfCacheMembers(key: string): string[];
46
+ /** Disposes every shared cache (stopping its refresh timer) and empties the registry. For tests and for a clean shutdown/restart of the server process. */
47
+ export declare function resetSharedCsrfCaches(): void;
48
+ //# sourceMappingURL=sharedCsrfCaches.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sharedCsrfCaches.d.ts","sourceRoot":"","sources":["../src/sharedCsrfCaches.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAIjD;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,oBAAoB,GAAG;IAC/B,WAAW,EAAE,MAAM,CAAA;IACnB,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB,CAAA;AAuBD,oGAAoG;AACpG,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,oBAAoB,GAAG,MAAM,CAEtE;AAED,MAAM,MAAM,eAAe,GAAG;IAC1B,KAAK,EAAE,cAAc,CAAA;IACrB,GAAG,EAAE,MAAM,CAAA;IACX,+EAA+E;IAC/E,MAAM,EAAE,OAAO,CAAA;IACf,uFAAuF;IACvF,KAAK,EAAE,MAAM,CAAA;CAChB,CAAA;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,sBAAsB,CAClC,KAAK,EAAE,oBAAoB,EAC3B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,cAAc,GAC7B,eAAe,CAejB;AAED,2FAA2F;AAC3F,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAE5D;AAED,2JAA2J;AAC3J,wBAAgB,qBAAqB,IAAI,IAAI,CAG5C"}
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.sharedCsrfCacheKey = sharedCsrfCacheKey;
7
+ exports.acquireSharedCsrfCache = acquireSharedCsrfCache;
8
+ exports.sharedCsrfCacheMembers = sharedCsrfCacheMembers;
9
+ exports.resetSharedCsrfCaches = resetSharedCsrfCaches;
10
+ const cds_1 = __importDefault(require("@sap/cds"));
11
+ const LOG = cds_1.default.log("csrf-cache");
12
+ const registry = new Map();
13
+ /** `JSON.stringify` with sorted keys, so two equal `destinationOptions` objects can't produce two different keys just because they were written in a different order. */
14
+ function stableStringify(value) {
15
+ if (value === null || typeof value !== "object")
16
+ return JSON.stringify(value) ?? "null";
17
+ if (Array.isArray(value))
18
+ return `[${value.map(stableStringify).join(",")}]`;
19
+ const entries = Object.keys(value).sort()
20
+ .map(key => `${JSON.stringify(key)}:${stableStringify(value[key])}`);
21
+ return `{${entries.join(",")}}`;
22
+ }
23
+ /** The identity of a shared cache - see {@link SharedCsrfCacheScope} for why each part is in it. */
24
+ function sharedCsrfCacheKey(scope) {
25
+ return `${scope.destination}::${stableStringify(scope.destinationOptions ?? {})}::${scope.group ?? ""}`;
26
+ }
27
+ /**
28
+ * Returns the {@link CsrfTokenCache} for `scope`, creating it via `create` on first use and handing
29
+ * the very same instance to every later caller with the same scope. That is what lets several
30
+ * remote services on one destination fetch a single token instead of one each - and it also means
31
+ * an `invalidate()` after a rejected token (from *any* of them) refreshes the token for all of
32
+ * them at once.
33
+ *
34
+ * The first service in wins: the shared cache keeps fetching from that service's `csrf.url`, with
35
+ * its method and validity. A service joining with different settings is served the existing cache
36
+ * and warned about, rather than silently getting a second one - two caches on one destination is
37
+ * exactly what sharing was turned on to avoid.
38
+ */
39
+ function acquireSharedCsrfCache(scope, member, settings, create) {
40
+ const key = sharedCsrfCacheKey(scope);
41
+ const existing = registry.get(key);
42
+ if (existing) {
43
+ if (!existing.members.includes(member))
44
+ existing.members.push(member);
45
+ if (existing.settings !== settings)
46
+ LOG.warn(`service '${member}' joins the shared csrf token cache of destination '${scope.destination}' with different settings (${settings}) `
47
+ + `than its owner '${existing.owner}' (${existing.settings}); the owner's settings stay in effect`);
48
+ return { cache: existing.cache, key, joined: true, owner: existing.owner };
49
+ }
50
+ const registration = { cache: create(), owner: member, members: [member], settings };
51
+ registry.set(key, registration);
52
+ return { cache: registration.cache, key, joined: false, owner: member };
53
+ }
54
+ /** The services sharing the cache under `key`, owner first - for diagnostics and tests. */
55
+ function sharedCsrfCacheMembers(key) {
56
+ return [...registry.get(key)?.members ?? []];
57
+ }
58
+ /** Disposes every shared cache (stopping its refresh timer) and empties the registry. For tests and for a clean shutdown/restart of the server process. */
59
+ function resetSharedCsrfCaches() {
60
+ for (const { cache } of registry.values())
61
+ cache.dispose();
62
+ registry.clear();
63
+ }
64
+ //# sourceMappingURL=sharedCsrfCaches.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sharedCsrfCaches.js","sourceRoot":"","sources":["../src/sharedCsrfCaches.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "myfirstdemoprojectkulliax",
3
+ "version": "0.1.0",
4
+ "description": "CAP plugin that caches SAP CSRF tokens per destination-backed remote service, injects them into outgoing requests, and refreshes them in the background before they expire.",
5
+ "keywords": [
6
+ "sap",
7
+ "cap",
8
+ "cds",
9
+ "csrf",
10
+ "cds-plugin"
11
+ ],
12
+ "license": "MIT",
13
+ "main": "./lib/index.js",
14
+ "types": "./lib/index.d.ts",
15
+ "files": [
16
+ "lib",
17
+ "cds-plugin.js",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "engines": {
22
+ "node": ">=22"
23
+ },
24
+ "scripts": {
25
+ "build": "tsc -p tsconfig.json",
26
+ "prepublishOnly": "npm run build",
27
+ "test": "vitest run",
28
+ "test.watch": "vitest",
29
+ "test.e2e": "vitest run --config vitest.e2e.config.ts"
30
+ },
31
+ "peerDependencies": {
32
+ "@sap/cds": "^10",
33
+ "@sap-cloud-sdk/http-client": "^4"
34
+ },
35
+ "peerDependenciesMeta": {
36
+ "@sap-cloud-sdk/http-client": {
37
+ "optional": true
38
+ }
39
+ },
40
+ "devDependencies": {
41
+ "@cap-js/cds-types": "^0",
42
+ "@sap-cloud-sdk/http-client": "^4",
43
+ "@sap/cds": "^10",
44
+ "@sap/cds-dk": "^10",
45
+ "@types/node": "^24",
46
+ "typescript": "^6",
47
+ "vitest": "^4"
48
+ }
49
+ }