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 +21 -0
- package/README.md +255 -0
- package/cds-plugin.js +31 -0
- package/lib/CsrfTokenCache.d.ts +56 -0
- package/lib/CsrfTokenCache.d.ts.map +1 -0
- package/lib/CsrfTokenCache.js +108 -0
- package/lib/CsrfTokenCache.js.map +1 -0
- package/lib/attachCsrfCache.d.ts +29 -0
- package/lib/attachCsrfCache.d.ts.map +1 -0
- package/lib/attachCsrfCache.js +126 -0
- package/lib/attachCsrfCache.js.map +1 -0
- package/lib/capDestinationTokenFetcher.d.ts +30 -0
- package/lib/capDestinationTokenFetcher.d.ts.map +1 -0
- package/lib/capDestinationTokenFetcher.js +103 -0
- package/lib/capDestinationTokenFetcher.js.map +1 -0
- package/lib/csrfFetch.d.ts +23 -0
- package/lib/csrfFetch.d.ts.map +1 -0
- package/lib/csrfFetch.js +64 -0
- package/lib/csrfFetch.js.map +1 -0
- package/lib/index.d.ts +9 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +20 -0
- package/lib/index.js.map +1 -0
- package/lib/sharedCsrfCaches.d.ts +48 -0
- package/lib/sharedCsrfCaches.d.ts.map +1 -0
- package/lib/sharedCsrfCaches.js +64 -0
- package/lib/sharedCsrfCaches.js.map +1 -0
- package/package.json +49 -0
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"}
|
package/lib/csrfFetch.js
ADDED
|
@@ -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
|
package/lib/index.js.map
ADDED
|
@@ -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
|
+
}
|