@mcp-abap-adt/auth-broker 3.0.4 → 4.0.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +448 -1
  2. package/README.md +723 -533
  3. package/dist/AuthBroker.d.ts +178 -29
  4. package/dist/AuthBroker.d.ts.map +1 -1
  5. package/dist/AuthBroker.js +494 -101
  6. package/dist/DestinationConfigError.d.ts +27 -0
  7. package/dist/DestinationConfigError.d.ts.map +1 -0
  8. package/dist/DestinationConfigError.js +36 -0
  9. package/dist/SessionWriter.d.ts +54 -0
  10. package/dist/SessionWriter.d.ts.map +1 -0
  11. package/dist/SessionWriter.js +135 -0
  12. package/dist/binding.d.ts +95 -0
  13. package/dist/binding.d.ts.map +1 -0
  14. package/dist/binding.js +207 -0
  15. package/dist/bindingOf.d.ts +36 -0
  16. package/dist/bindingOf.d.ts.map +1 -0
  17. package/dist/bindingOf.js +53 -0
  18. package/dist/destinations.d.ts +139 -0
  19. package/dist/destinations.d.ts.map +1 -0
  20. package/dist/destinations.js +429 -0
  21. package/dist/index.d.ts +7 -3
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +11 -7
  24. package/dist/providers/ITokenProvider.d.ts +2 -1
  25. package/dist/providers/ITokenProvider.d.ts.map +1 -1
  26. package/dist/providers/index.d.ts +2 -2
  27. package/dist/providers/index.js +2 -2
  28. package/dist/stores/index.d.ts +2 -2
  29. package/dist/stores/index.js +2 -2
  30. package/dist/stores/interfaces.d.ts +4 -3
  31. package/dist/stores/interfaces.d.ts.map +1 -1
  32. package/dist/stores/interfaces.js +2 -2
  33. package/dist/types.d.ts +5 -2
  34. package/dist/types.d.ts.map +1 -1
  35. package/dist/types.js +3 -1
  36. package/package.json +25 -45
  37. package/CONTRIBUTORS.md +0 -33
  38. package/dist/__tests__/helpers/configHelpers.d.ts +0 -59
  39. package/dist/__tests__/helpers/configHelpers.d.ts.map +0 -1
  40. package/dist/__tests__/helpers/configHelpers.js +0 -206
  41. package/dist/__tests__/helpers/netHelpers.d.ts +0 -3
  42. package/dist/__tests__/helpers/netHelpers.d.ts.map +0 -1
  43. package/dist/__tests__/helpers/netHelpers.js +0 -63
  44. package/dist/__tests__/helpers/testLogger.d.ts +0 -7
  45. package/dist/__tests__/helpers/testLogger.d.ts.map +0 -1
  46. package/dist/__tests__/helpers/testLogger.js +0 -54
  47. package/dist/bin/mcp-auth.js +0 -752
  48. package/dist/bin/mcp-sso.js +0 -825
  49. package/dist/bin/mcpSsoConfig.js +0 -566
  50. package/dist/bin/samlMetadata.js +0 -175
  51. package/dist/bin/workDir.js +0 -79
package/CHANGELOG.md CHANGED
@@ -7,7 +7,454 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## Contributors
9
9
 
10
- Thank you to all contributors! See [CONTRIBUTORS.md](CONTRIBUTORS.md) for the complete list.
10
+ Thank you to all contributors! See [CONTRIBUTORS.md](../../CONTRIBUTORS.md) for the complete list.
11
+
12
+ ## [Unreleased]
13
+
14
+ ## [4.0.0] - 2026-10-02
15
+
16
+ **What 4.0.0 is.** The broker hands a process **the credential for a
17
+ destination, ready to use**: `getProvider(destination)` returns the
18
+ `IAuthProvider` (`@mcp-abap-adt/interfaces-auth` 3) the destination states —
19
+ basic, SNC, a UAA, OIDC or SAML grant, or a credential handed over — which a
20
+ `@mcp-abap-adt/connection` 10 connector takes as it is, and whatever that
21
+ provider obtains or renews is stored in the session store, whoever triggered
22
+ it. The token API (`getToken`, `refreshToken`, `createTokenRefresher`) stays.
23
+ Released with `@mcp-abap-adt/auth-broker-cli` 2.0.0, the commands on this
24
+ version.
25
+
26
+ ### Breaking
27
+
28
+ 1. **The two stores have one role each.** The service key store holds the
29
+ *means* — `authType`, `grantType`, the client, basic's user and password,
30
+ the SNC, OIDC and SAML settings, `serviceUrl`; the session store holds the
31
+ *secret* — the token or cookies, `expiresAt`, the refresh token, and what
32
+ the secret is bound to (`issuedFor`, `issuedBy`). The broker writes the
33
+ secret alone, in one `saveSession`, and never a means field: no
34
+ `serviceUrl`, `authType` or client is written to the session any more
35
+ (3.x called `setConnectionConfig` and `setAuthorizationConfig`).
36
+ `getProvider` reads the means from the key store only: a destination
37
+ whose means only a session answers is a `DestinationConfigError`.
38
+ 2. **The session store must take a write of the secret alone:
39
+ `@mcp-abap-adt/auth-stores` 3 or later**, or a store of your own that does.
40
+ auth-stores 1.x and 2.x are not supported: their `AbapSessionStore`
41
+ throws on a `saveSession` without `serviceUrl` and their
42
+ `SafeAbapSessionStore` refuses it (measured on 1.2.3, 1.2.4 and 2.0.0), so
43
+ a token would be obtained and then never stored. auth-stores 3.1.0 or
44
+ later for the binding (item 4).
45
+ 3. **`getConnectionConfig` / `getAuthorizationConfig` compose the two
46
+ stores** — the key store's means with the session's secret laid over them
47
+ — instead of answering the session's configuration whole.
48
+ 4. **A stored secret is used only for the resource and issuer it was
49
+ obtained for** (`issuedFor`, `issuedBy`): one bound elsewhere, or not
50
+ bound at all, is not reused — the provider logs in afresh, and a `none`
51
+ destination is refused.
52
+ 5. **The token API refuses a destination the key store states as `basic` or
53
+ `snc`** (`DestinationConfigError`, `authType`), instead of writing a token
54
+ over its session.
55
+ 6. **Contracts and dependencies.** `@mcp-abap-adt/interfaces-auth` `^3.0.0`,
56
+ `@mcp-abap-adt/interfaces-auth-sap` `^2.0.0`,
57
+ `@mcp-abap-adt/interfaces-auth-broker` `^1.1.0` (the store contracts, new),
58
+ `@mcp-abap-adt/auth-providers` `^5.2.1` (now a runtime dependency). Types
59
+ of `interfaces-auth` 2.x and providers of auth-providers 4.x no longer mix
60
+ with this version.
61
+
62
+ ### Migrating from 3.x
63
+
64
+ - **Stores.** Use `@mcp-abap-adt/auth-stores` `^3.1.0` (this version is
65
+ tested against 3.2.0), or stores of your own on the
66
+ `@mcp-abap-adt/interfaces-auth-broker` contracts. State the means in a key
67
+ store: an `EnvDestinationStore(dir, { fallback })` over
68
+ `<destination>.env`, or `AbapServiceKeyStore(dir, { grantType })` /
69
+ `XsuaaServiceKeyStore(dir, { grantType })` over a SAP service key — a key
70
+ cannot state its grant, so whoever builds the store states it. The session
71
+ store keeps the secret: `AbapSessionStore`, `XsuaaSessionStore`,
72
+ `EnvFileSessionStore`, or the in-memory `Safe*` ones. A custom
73
+ `ISessionStore` must persist `issuedFor` and `issuedBy` beside the secret,
74
+ or the broker never reuses its sessions (a login every start).
75
+ - **3.x session files** hold means and secret together. Point an
76
+ `EnvDestinationStore` and a session store at the same directory: each reads
77
+ its own keys where they are. A `basic` or `snc` file works once it states
78
+ `SAP_AUTH_TYPE`; a `jwt` or `saml` file needs `SAP_GRANT_TYPE` (3.x never
79
+ wrote one) — run the `auth-broker-cli` command that produced it again, or
80
+ add the line. Its stored token is reused when its `SAP_URL` (+
81
+ `SAP_CLIENT`) and `SAP_UAA_URL` + `SAP_UAA_CLIENT_ID` are the destination's
82
+ (auth-stores 3.1.0 reads them as the binding); otherwise the first start
83
+ logs in once. XSUAA sessions written with an empty URL log in once.
84
+ - **A `@mcp-abap-adt/connection` 10 connector:** build it from
85
+ `await broker.getProvider(destination)` instead of a token and a token
86
+ refresher. Construct the broker with a `serviceKeyStore` and without a
87
+ `provider`, and give it the collaborators its destinations' grants use —
88
+ `authorization`, `oidcAuthorization`, `deviceCodePresenter`, `samlCookies`,
89
+ `assertionReplayStore`; there is no default. A headless process gives ones
90
+ that refuse. The connector's URL comes from your key store
91
+ (`getConnectionConfig`); `getProvider` does not need it.
92
+ - **The token API with a provider of your own** (calm-server's way): see
93
+ *The token API* below — the same calls, reads and answers; what it writes
94
+ is the secret alone.
95
+ - **`DestinationConfigError`** (new, exported): catch it where the process
96
+ builds its connector or asks for a token. `missingFields` names what the
97
+ destination lacks (a field, or a collaborator option) — never a value.
98
+ - **`flush()`** (new): a session write that fails is retried by the broker
99
+ instead of failing the authentication. Call `await broker.flush()` on
100
+ shutdown (`SIGTERM`, before a stdio transport closes) to learn whether
101
+ every secret is stored; it rejects naming the destinations still failing.
102
+ - **`bindingOf(means, client?)`** (new): a consumer that writes a handed-over
103
+ token or cookies (a `none` destination) to the session store itself writes
104
+ `bindingOf(means)` beside them, or `getProvider` refuses the destination.
105
+ - **Imports.** The re-exported types keep their names. If you import
106
+ `ISessionStore`, `IServiceKeyStore`, `IConnectionConfig` or `IConfig` from
107
+ `@mcp-abap-adt/interfaces-auth-sap` yourself, take them from
108
+ `@mcp-abap-adt/interfaces-auth-broker`; `IAuthorizationConfig` and
109
+ `AuthType` stay in `interfaces-auth-sap`. Providers are auth-providers 5:
110
+ the interactive ones take their `authorization` strategy explicitly.
111
+ - **The commands** are `@mcp-abap-adt/auth-broker-cli` (since 3.1.0); its
112
+ 2.0.0 is the version on this library — what changed for its flags, files
113
+ and exit codes is in its CHANGELOG, *Migrating from 1.0.0*.
114
+
115
+ #### The token API (`getToken`, `refreshToken`, `createTokenRefresher`)
116
+
117
+ **What stays.** A consumer that passes its own `provider` (an instance or a
118
+ `TokenProviderFactory`) calls the same methods with the same signatures and
119
+ gets the same answers: the provider is asked as before, seeded from the same
120
+ reads in the same order (the session first, then the key store), and its
121
+ errors — and a store's — reach the caller unchanged. `createTokenRefresher`
122
+ is unchanged and stays supported (calm-server injects it into its own
123
+ connection).
124
+
125
+ **What changes for it.**
126
+
127
+ 1. **The session gets the secret alone** — the token or cookies, `expiresAt`,
128
+ the refresh token, and what the secret is bound to (`issuedFor`,
129
+ `issuedBy`) — in one `saveSession`. No `serviceUrl`, `authType` or client
130
+ is written back any more, so a wrapper that only kept the written-back
131
+ `serviceUrl` out of the session (calm-server's `TargetUrlSessionStore`)
132
+ has nothing left to filter. A store that merges a write into what it holds
133
+ — auth-stores 3, and auth-stores 1.x/2.x's `XsuaaSessionStore` and
134
+ `EnvFileSessionStore` (measured on 1.2.4 and 2.0.0) — keeps what it holds.
135
+ 2. **The session store must accept a write of the secret alone.**
136
+ Measured: auth-stores **1.2.3, 1.2.4 and 2.0.0**'s `AbapSessionStore`
137
+ throws a `TypeError` on a `saveSession` without `serviceUrl`, and their
138
+ `SafeAbapSessionStore` refuses it (`missing required field: serviceUrl`) —
139
+ even when the session already holds its URL. On those stores `getToken`
140
+ obtains a token and then throws the store's error, and the broker's retry
141
+ cannot succeed. Use auth-stores **3** (`^3.1.0` for the binding) — or a
142
+ store of your own that takes the secret alone. (1.x/2.x's
143
+ `SafeXsuaaSessionStore` replaces the session with what is written: the
144
+ URL or client it held in memory is gone after the first token, so state
145
+ them in the key store.)
146
+ 3. **A destination the key store states as `basic` or `snc` is refused**
147
+ (`DestinationConfigError`, `missingFields: ['authType']`) instead of
148
+ getting a token written over its credential.
149
+ 4. **A failed write is retried.** It still reaches `getToken`'s caller; the
150
+ broker then retries it on its own, and `flush()` reports what is still
151
+ pending — call it on shutdown.
152
+
153
+ **Without a `provider`,** the token API serves what the destination states,
154
+ on the provider `getProvider` builds for it (see *Added*): the consumer
155
+ composes the key store (the means, `grantType` included) and the
156
+ collaborators instead of a provider.
157
+
158
+ ### Added
159
+
160
+ - **`bindingOf(means, client?)`** — the binding (`issuedFor`, `issuedBy`) the
161
+ broker writes beside a secret for a destination with those means, for a
162
+ consumer that hands over a credential (a `none` destination's token or
163
+ cookies) and writes it to the session store itself. It is the same function
164
+ `getProvider` checks a stored secret against and `persist` writes
165
+ (`destinationBinding`, `src/bindingOf.ts`), so what a consumer writes and
166
+ what the broker expects cannot diverge, and no consumer canonicalises a URI.
167
+ `@mcp-abap-adt/auth-broker-cli`'s `--cookie` uses it.
168
+
169
+ - **`getProvider(destination): Promise<IAuthProvider>`** — the credential the
170
+ destination states, for a `@mcp-abap-adt/connection` 10 connector. The
171
+ destination's `authType` (and `grantType`, for `jwt` and `saml`) chooses the
172
+ provider and nothing else does; the means are read from the service key
173
+ store only, the secret (token or cookies) from the session store only. Built
174
+ in this version: `basic` → `BasicAuthProvider`, `snc` →
175
+ `SncLogonProvider.forSecureLoginClient` (from `sncPartnerName`, `sncQop`,
176
+ `sncLib`, `sncMyName`), `jwt` / `none` → `TokenAuthProvider.fixed` with the
177
+ session's token, `saml` / `none` → `SamlAuthProvider` with the session's
178
+ cookies, the UAA grants, the OIDC grants and the SAML grants (below) —
179
+ every pair of the table. One provider per destination: the promise of the build is cached before the
180
+ first read, so concurrent first calls build once, and dropped when the build
181
+ throws, so the next call retries.
182
+ - **The UAA grants in `getProvider`**: `jwt` / `authorization_code` →
183
+ `AuthorizationCodeProvider`, `jwt` / `client_credentials` →
184
+ `ClientCredentialsProvider`, `jwt` / `passcode` → `UaaPasscodeProvider`. The
185
+ client (`uaaUrl`, `uaaClientId`, `uaaClientSecret`) comes from the key
186
+ store's `getAuthorizationConfig`; `authorization_code` and `passcode` are
187
+ seeded from the session — the token, the refresh token and `expiresAt` —
188
+ and without a session log in at `prepare()`; `client_credentials` takes the
189
+ client alone. `uaaClientSecret: ''` is a public client, which `passcode`
190
+ takes as no secret. No row needs `serviceUrl`: the resource URL is not
191
+ authorization data — no token provider reads it — so the connector takes it
192
+ from the key store, and `getProvider` neither requires it nor passes it to a
193
+ provider; it reads it only to bind a stored secret (below).
194
+ - **The OIDC grants in `getProvider`**: `jwt` / `oidc_authorization_code` →
195
+ `OidcBrowserProvider` (PKCE), `device_code` → `OidcDeviceFlowProvider`,
196
+ `password` → `OidcPasswordProvider`, `token_exchange` →
197
+ `OidcTokenExchangeProvider`. The client id and secret come from the key
198
+ store's client (`uaaClientSecret: ''` is a public client, sent with no
199
+ secret); the endpoints from `oidcIssuerUrl` (discovery) or, without it,
200
+ every explicit endpoint the row reads (`oidcAuthorizationEndpoint`,
201
+ `oidcDeviceAuthorizationEndpoint`, `oidcTokenEndpoint`); `oidcScopes` as
202
+ given, joined by one space into `token_exchange`'s `scope`; `password`'s
203
+ user and `token_exchange`'s subject, audience and actor token from the
204
+ means — sent, never written. Seeded from the session like the UAA rows;
205
+ `issuedBy` is `oidcIssuerUrl`, else `uaaUrl`, with `client_id`.
206
+ - **The SAML grants in `getProvider`**: `saml` / `saml2_pure` →
207
+ `Saml2PureProvider`, `saml2_bearer` → `Saml2BearerProvider`. The broker
208
+ composes the assertion validator from the destination's trust —
209
+ `createSignedResponseValidator` for `saml2_pure`,
210
+ `createSignedAssertionValidator` for `saml2_bearer`, from
211
+ `samlIdpCertificates`, `samlClockSkewMs` and the consumer's
212
+ `assertionReplayStore` — and passes `samlIdpEntityId` as the expected
213
+ issuer, with `samlIdpSsoUrl`, `samlSpEntityId`, `samlAcsUrl`,
214
+ `samlRelayState` and `samlIdpInitiated`. `saml2_pure` is seeded with the
215
+ stored cookies and `expiresAt` (auth-providers 5.1.0's seed) and bound to
216
+ `samlAcsUrl` (origin and path); `saml2_bearer` takes the client and
217
+ `samlTokenUrl`, is seeded with the token, refresh token and `expiresAt`, and
218
+ is bound to `uaaUrl` with `client_id`.
219
+ - **The `authorization` option is used**: `(destination, grant) =>
220
+ IAuthorizationStrategy<string>`, the interactive half of
221
+ `authorization_code` and `passcode` (for `passcode` the strategy is handed
222
+ `<uaaUrl>/passcode`). Called once per destination's build, never disposed
223
+ by the broker; required only by those two grants and the two SAML grants
224
+ (which it conducts with the grant passed), with no default.
225
+ - **The other collaborator options are used**: `oidcAuthorization` by
226
+ `oidc_authorization_code`, `deviceCodePresenter` by `device_code`,
227
+ `samlCookies` (the validated SAMLResponse → the system's session cookies)
228
+ by `saml2_pure`, `assertionReplayStore` by both SAML grants — each a
229
+ function of the destination, called once per build, never disposed, and
230
+ required only by its rows: missing, a `DestinationConfigError` naming it.
231
+ - **What a `getProvider` provider obtains is stored.** Every token provider
232
+ the broker builds gets `onTokens`, so a token obtained at `prepare()`, on
233
+ expiry, or in `rejected()` after a 401 is in the session store before the
234
+ provider answers. The write is the session secret alone — one
235
+ `saveSession(destination, { authorizationToken, expiresAt, refreshToken,
236
+ issuedFor, issuedBy })`, the stored refresh token carried forward when the
237
+ result has none and the stored session is bound where the new secret is —
238
+ and never a means field (no `serviceUrl`, no `authType`, no client secret,
239
+ no subject token, no password). A `saml2_pure` result (`tokenType:
240
+ 'saml'`) is written as cookies — `{ sessionCookies, expiresAt, issuedFor,
241
+ issuedBy }`, no refresh token; a `saml2_bearer` result as a token.
242
+ A destination the key store states as `basic` or `snc` is not written.
243
+ - **A stored secret is used only where it is bound** (`issuedFor` /
244
+ `issuedBy`, `@mcp-abap-adt/interfaces-auth-broker` 1.1.0). The broker
245
+ computes from the means the resource a secret is for (`serviceUrl` with
246
+ `sap-client`) and who issued it to which client (`uaaUrl` with
247
+ `client_id` for the UAA grants and `saml2_bearer`; the OIDC issuer, else
248
+ `uaaUrl`, with `client_id` for the OIDC grants; the ACS for `saml2_pure`),
249
+ canonicalises them and the session's values with one function
250
+ — scheme and host lower-cased, the port explicit (`443` / `80`), the path
251
+ without a trailing `/`, only `sap-client` (the means' `sapClient` first) or
252
+ `client_id` kept and re-encoded, so percent-encoding compares equal — and
253
+ writes both with every secret, each left out when the means lack its source.
254
+ The grants that obtain a secret (UAA, OIDC, SAML) are seeded only when both
255
+ stored values match; otherwise the
256
+ stored secret, refresh token included, is not used: the provider logs in
257
+ afresh and the log says only `<destination>: secret bound to another
258
+ resource, discarded`. A `none` destination is refused instead: a
259
+ `DestinationConfigError` naming `issuedFor` when the stored resource is not
260
+ the destination's or is absent, and naming `issuedBy` when the means state
261
+ an issuer (`oidcIssuerUrl` or the client for `jwt`, `samlAcsUrl` for `saml`)
262
+ and the stored one is not it. **What a consumer meets:** a custom
263
+ `ISessionStore` must persist both fields beside the secret, else every
264
+ process start is a fresh login; a headless process whose strategy refuses
265
+ logins answers Oops on a mismatch instead of presenting a foreign secret; a
266
+ changed URL, SAP client, UAA or client costs one login. Session files
267
+ written before `@mcp-abap-adt/auth-stores` 3.1.0 keep working: its stores
268
+ answer the binding from the file's `SAP_URL` (+ `SAP_CLIENT`) and
269
+ `SAP_UAA_URL` + `SAP_UAA_CLIENT_ID`; XSUAA sessions written with an empty
270
+ URL have none and log in once.
271
+ - **A failed session write does not fail the authentication**: it stays
272
+ pending for its destination and the broker retries it on its own — one
273
+ second, doubling, capped at one minute, on a timer that does not keep the
274
+ process alive. A newer result replaces the pending one; writes for one
275
+ destination never overlap; failures are logged by class name, never their
276
+ message.
277
+ - **`flush(): Promise<void>`** — one more attempt for every pending session
278
+ write; resolves when all have landed, rejects with an `AggregateError`
279
+ naming the destinations whose store still refuses (the retries go on). Call
280
+ it on shutdown.
281
+ - **`DestinationConfigError`** (`code: 'DESTINATION_CONFIG'`, `destination`,
282
+ `missingFields`) — a destination that lacks what its type needs:
283
+ no `serviceKeyStore` option, no means, no or an unknown `authType`, a
284
+ `jwt`/`saml` destination without a `grantType` or with a pair outside the
285
+ table, a missing field of its row (`''` counts as missing; for the UAA
286
+ grants `uaaUrl`, `uaaClientId`, and `uaaClientSecret` for
287
+ `authorization_code` / `client_credentials`), a missing collaborator option
288
+ (`authorization`; for the OIDC and SAML grants `oidcAuthorization`,
289
+ `deviceCodePresenter`, `samlCookies`, `assertionReplayStore`), an OIDC
290
+ grant with neither `oidcIssuerUrl` nor its endpoints (`oidcIssuerUrl` and
291
+ each endpoint missing), a SAML grant without its trust (`samlIdpSsoUrl`,
292
+ `samlSpEntityId`, `samlIdpEntityId`, `samlIdpCertificates` — an empty list
293
+ counts as missing), a certificate the validator cannot read
294
+ (`samlIdpCertificates`) or a `samlClockSkewMs` that is not a whole,
295
+ non-negative number, a `none` credential not bound to the destination
296
+ (`issuedFor`, `issuedBy`), or a provider
297
+ constructor's own `ValidationError` (an `sncQop` outside `1|2|3|8|9`, named
298
+ as the store field; the provider's error, which quotes the value, is not
299
+ kept). It names fields and options, never a stored value.
300
+ - **The token API shares `getProvider`'s provider.** Without a consumer
301
+ `provider`, `getToken`, `refreshToken` and `createTokenRefresher` ask the
302
+ very provider `getProvider` hands out for the destination — one per
303
+ destination, so one token, one refresh token and one renewal in flight
304
+ serve the connector and the token API alike (a `refreshToken()` during a
305
+ renewal in `rejected()` joins it). They write nothing themselves: the
306
+ provider's `onTokens` does, as for any `getProvider` provider. They serve the
307
+ destinations whose provider obtains a token — every grant but `none` — and
308
+ throw `DestinationConfigError` naming `provider` for a `none` destination,
309
+ before any provider is built. A failed write still reaches the caller:
310
+ `getToken` / `refreshToken` throw the store's error for the token they
311
+ received — as 3.x did — while the broker keeps retrying the write.
312
+ - The collaborator options `authorization`, `oidcAuthorization`,
313
+ `deviceCodePresenter`, `samlCookies`, `assertionReplayStore` on
314
+ `AuthBrokerConfig`, and the `StrategyGrant` type.
315
+ - **The broker's own test stand** (`tests/stand/`, `npm run test:stand`):
316
+ Cloud Foundry UAA and Keycloak in Docker, copied from auth-providers' stand,
317
+ with suites running the UAA grants end to end through auth-stores 3.1.0's
318
+ file stores — the binding asserted in the session file, and a session bound
319
+ to another URL not reused — the OIDC grants against Keycloak, and the SAML
320
+ grants with Keycloak as identity provider and UAA as service provider
321
+ (`saml2_pure`'s cookies a real UAA session from its web SSO ACS;
322
+ `saml2_bearer` exchanged at UAA's bearer grant); a CI workflow runs the build-and-test gate and the stand on every
323
+ push and pull request. Not shipped in the package.
324
+
325
+ ### Changed
326
+
327
+ - **`provider` is optional.** `getProvider` does not use it. With it, the
328
+ token API asks it, as in 3.x; without it, the token API asks
329
+ `getProvider`'s provider (above). With neither a `provider` nor a
330
+ `serviceKeyStore`, `getToken` and `refreshToken` throw
331
+ `DestinationConfigError` naming both.
332
+ - **Breaking: the token API refuses a destination stated `basic` or `snc`.**
333
+ It reads the destination's `authType` from the key store first and, for
334
+ `basic` or `snc`, throws `DestinationConfigError` naming `authType` before
335
+ any provider is asked — 3.x asked the provider and wrote `authType: 'jwt'`
336
+ and a token over the session of a destination that holds no token. A key
337
+ store that states no `authType` (a 3.x setup with a consumer `provider`),
338
+ or no key store at all, is served as before.
339
+ - **Breaking: the token API writes the session secret alone.** With a
340
+ consumer `provider` it still writes after every `getTokens()` /
341
+ `refreshTokens()`, cache hits included, but through the same write path as
342
+ `getProvider`'s providers: one `saveSession(destination, {
343
+ authorizationToken | sessionCookies, expiresAt, refreshToken, issuedFor,
344
+ issuedBy })`, retried when the store fails and awaited by `flush()`. 3.x
345
+ wrote the session's connection config back with `serviceUrl` and a derived
346
+ `authType` (`setConnectionConfig`), and a client the session held with the
347
+ new refresh token (`setAuthorizationConfig`); neither method is called any
348
+ more. A result without a refresh token carries the stored one forward.
349
+ `issuedFor` is the `serviceUrl` the token API resolved, with the SAP client
350
+ (the session's, else the key store's); `issuedBy` is the client a factory
351
+ was handed (`uaaUrl` with `client_id`) — an instance is handed no client,
352
+ so its tokens are bound to no issuer and `getProvider` never seeds from
353
+ them. Both are fixed when the provider is built for the destination (an
354
+ instance: at its first call for it) and kept with it, so a URL or client
355
+ changed later never re-labels a token obtained before the change; a new
356
+ broker picks the change up. A failed write is thrown to the caller as the store raised it, as in
357
+ 3.x, and retried.
358
+ - The token API's reads are 3.x's, and so are its answers: the session first,
359
+ then the key store, for `serviceUrl` and the client a factory is seeded
360
+ with; the factory called once per destination — now cached as the promise
361
+ of its build, so concurrent first calls build once — and again after one
362
+ that threw; an instance used for every destination; the provider's errors
363
+ and a store's failures propagate unchanged; a result without a token is an
364
+ error. `createTokenRefresher(destination)` is unchanged and not
365
+ deprecated.
366
+ - **Breaking: `getConnectionConfig` / `getAuthorizationConfig` compose the two
367
+ stores** instead of answering the session's configuration whole (and the
368
+ key's only when the session had none). `getConnectionConfig` is the key
369
+ store's means with the session's `authorizationToken`, `sessionCookies` and
370
+ `expiresAt` laid over them; `getAuthorizationConfig` is the key store's
371
+ client with the session's refresh token, `null` without a client in the key
372
+ store. Means a session store answers are not read, nor a secret a key store
373
+ answers. A consumer that kept the URL or the client only in its session
374
+ store gets them from a key store now.
375
+ - **Contracts from their 2026-10 packages.** `@mcp-abap-adt/interfaces-auth`
376
+ `^3.0.0` (was `^2.1.0`), `@mcp-abap-adt/interfaces-auth-sap` `^2.0.0` (was
377
+ `^1.0.1`), and `@mcp-abap-adt/interfaces-auth-broker` `^1.1.0` (new; 1.1.0
378
+ for `issuedFor` / `issuedBy`). The
379
+ store contracts — `IConfig`, `IConnectionConfig`, `ISessionStore`,
380
+ `IServiceKeyStore` — moved out of `interfaces-auth-sap` into
381
+ `interfaces-auth-broker`; this package now takes them from there.
382
+ `IAuthorizationConfig` and `AuthType` stay in `interfaces-auth-sap`. The
383
+ re-exported names are unchanged; only where they come from. A consumer that
384
+ imports them from this package changes nothing; one that imports
385
+ `ISessionStore` and friends from `interfaces-auth-sap` itself takes them
386
+ from `interfaces-auth-broker` from its 2.0.0 on.
387
+ - `@mcp-abap-adt/auth-providers` `^5.2.1` (was `^4.2.0`, a dev dependency) is
388
+ a runtime dependency: `getProvider` constructs its classes. None is
389
+ re-exported. 5.2.0 because `ClientCredentialsProvider` takes a `logger`
390
+ from it: the broker passes its own, so a `client_credentials` destination's
391
+ token lifecycle is logged like the other grants' (tokens redacted by the
392
+ provider); 5.2.1 because 5.2.0 skipped OIDC discovery unless every
393
+ device-flow endpoint was missing, so a `device_code` destination stating
394
+ its issuer and one explicit endpoint was refused at `prepare()`. The library's tests run against `@mcp-abap-adt/auth-stores`
395
+ `^3.2.0` (a dev dependency, was `^1.2.3`; the `auth-stores-3` alias the
396
+ stand, live and binding suites used is gone): its session stores hold the
397
+ secret alone, which is all the broker writes now. The carried-over on-disk
398
+ case reads the means from an `EnvDestinationStore` and asserts the session
399
+ file holds the secret and its binding and nothing else. Dev dependency
400
+ `@mcp-abap-adt/connection` `^10.0.3` (was `^10.0.2`), for the end-to-end
401
+ test: a connector built from `getProvider`, a 401 renewed in `rejected()`,
402
+ the new token in the session file, and the token API answering it.
403
+ - Comments no longer name `@mcp-abap-adt/auth-stores-btp` / `-xsuaa` (which do
404
+ not exist) or the deleted `@mcp-abap-adt/interfaces` facade.
405
+ - Dev dependency `@mcp-abap-adt/auth-mocks` `^0.3.0` (new): the SAML grants'
406
+ unit tests validate assertions its identity provider signs — one signing
407
+ the Response, one the Assertion alone — rather than documents the suite
408
+ signs itself. Not shipped.
409
+
410
+ ## [3.1.0] - 2026-10-01
411
+
412
+ **Why a minor.** The library's API is unchanged: every export, signature and
413
+ behaviour of 3.0.4 stays. What changed is where the commands install from —
414
+ `mcp-auth` and `mcp-sso` are now `@mcp-abap-adt/auth-broker-cli` 1.0.0 — and
415
+ which dependencies this package pulls in. Read the migration below if you
416
+ install this package for its commands, or import `auth-stores` or
417
+ `auth-providers` without declaring them.
418
+
419
+ ### Migration
420
+
421
+ - **You run `npm i -g @mcp-abap-adt/auth-broker` for `mcp-auth` / `mcp-sso`:**
422
+ this version has no `bin`. Install the commands from their own package:
423
+
424
+ ```bash
425
+ npm uninstall -g @mcp-abap-adt/auth-broker && npm i -g @mcp-abap-adt/auth-broker-cli
426
+ ```
427
+
428
+ Commands and flags are the same.
429
+ - **Your code imports `@mcp-abap-adt/auth-stores` or
430
+ `@mcp-abap-adt/auth-providers` and found them only because this package
431
+ depended on them:** declare them in your own `package.json`. This package no
432
+ longer installs either.
433
+
434
+ ### Changed
435
+
436
+ - **The repository is a workspace** (the `mcp-abap-adt-interfaces` layout): this
437
+ package is `packages/auth-broker`, and the commands are
438
+ `packages/auth-broker-cli`, published as `@mcp-abap-adt/auth-broker-cli`. Tags
439
+ are `auth-broker-v<version>` from here on; the `v*` tags are this package's
440
+ history up to 3.0.4. Run everything from the repository root: `npm run build`,
441
+ `npm test`, `npm run check`.
442
+
443
+ ### Removed
444
+
445
+ - **`bin`: `mcp-auth` and `mcp-sso`** move to `@mcp-abap-adt/auth-broker-cli`,
446
+ commands and flags unchanged (see *Migration*).
447
+ - **`axios`** from the dependencies: nothing imports it.
448
+ - **`@mcp-abap-adt/logger`** from the dependencies: only the commands use it
449
+ at run time; the tests keep it as a dev dependency.
450
+ - **`@mcp-abap-adt/auth-stores` and `@mcp-abap-adt/auth-providers`** from the
451
+ dependencies: the library imports neither, only its tests do (dev
452
+ dependencies now). `tools/check-graph.js` keeps it that way for auth-stores.
453
+ A consumer that imported either without declaring it must declare it (see
454
+ *Migration*).
455
+ - **The test helpers from `dist/`**: `dist/__tests__/helpers` shipped in the
456
+ tarball; the build no longer compiles anything under `src/__tests__`.
457
+ - **`CONTRIBUTORS.md` from the tarball**: it stays at the repository root.
11
458
 
12
459
  ## [3.0.4] - 2026-09-28
13
460