@mcp-abap-adt/auth-broker 4.1.0 → 5.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.
package/CHANGELOG.md CHANGED
@@ -9,7 +9,144 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  Thank you to all contributors! See [CONTRIBUTORS.md](../../CONTRIBUTORS.md) for the complete list.
11
11
 
12
- ## [Unreleased]
12
+ ## [5.0.0] - 2026-10-09
13
+
14
+ The broker on the auth chain's 6.0 contracts (auth-providers 6, interfaces-auth 7, auth-errors
15
+ 2). A major: what every failure is changes class, two options become required for token
16
+ destinations, the session binding gains a versioned record, and every wait can be cancelled.
17
+ Released with `@mcp-abap-adt/auth-broker-cli` 3.0.0. What a 4.x consumer must do, row by row:
18
+ the README's *Migrating to 5.0.0*.
19
+
20
+ ### Breaking
21
+
22
+ - **`renewal` is required for every token row.** `AuthBrokerConfig.renewal` — `(destination,
23
+ grant: TokenGrant) => IRenewalStrategy` — is called once per build of every UAA, OIDC and
24
+ SAML row, after every other check and before the provider's constructor; its answer is the
25
+ provider's `renewal`, unchanged. No default: a token row without it is a
26
+ `DestinationConfigError` naming `renewal`, beside every other missing field. A `renewal` that
27
+ throws is `DestinationConfigError(['renewal'])`, "the renewal option failed", carrying what it
28
+ threw. Not called for `basic`, `snc`, `none`, nor for the token API's `provider`.
29
+ `() => refreshThenLogin()` is 4.x's behaviour.
30
+ - **`onWriteFailure: 'fail' | 'continue'` is required** for every destination that writes a
31
+ secret — a token row `getProvider` builds, and every call of the token API with a `provider`.
32
+ Without it: `DestinationConfigError` naming `onWriteFailure`. `'fail'`: the call whose write
33
+ did not land fails (`unknown`, `persisting-tokens`), and while the destination's last write is
34
+ pending (failed, queued or in flight) `getProvider`, `getToken` and `refreshToken` retry it on
35
+ entry and once more before success, and are refused while it fails. `'continue'`: logged, the
36
+ call goes on. A moment of a provider already handed out that commits nothing is not refused.
37
+ - **Failures are `AuthProviderFailure`s, read through `@mcp-abap-adt/auth-errors`.** A
38
+ provider's failure reaches `getToken()` / `refreshToken()` as the same object. The broker reads
39
+ a caught value only through `readFailure` / `isAuthProviderFailure` — no `instanceof`,
40
+ `message` or `name` of an error is read in `src`. The broker's own failures are minted by
41
+ auth-errors: an aborted wait (`interactive-login`, `aborted`), a write that did not land under
42
+ `'fail'` (`unknown`, `persisting-tokens`, an allowlisted `code`), a provider that answered no
43
+ token (`request-failed`, `token-source`, `no-access-token` — replacing "Token provider did not
44
+ return authorization token …").
45
+ - **`DestinationConfigError` carries the provider's failure.** New `error?: IAuthProviderError`
46
+ (kind, facts, rendered words, admitted diagnostics; no `cause`) when a provider's or a
47
+ strategy's failure caused the refusal; the message then ends `: <error.reason>`. New
48
+ `isDestinationConfigError(value)` — structural, no `instanceof`; true for a JSON copy. A
49
+ provider constructor that throws inside a build — any row, not only SNC — is a
50
+ `DestinationConfigError` naming the store fields its configuration facts name. The SNC row
51
+ decides by kind (`configuration` → its fields), not by `instanceof ValidationError`. The
52
+ broker's own copy of the three certificate phrases is gone: `clientAuthentication`'s refusal
53
+ reads `the clientAuthentication strategy failed: <reason>` with auth-providers'
54
+ `client-certificate` failure carried. New rows: `renewal`, `onWriteFailure`, `provider` (an
55
+ instance after the destination's identity changed), a store field of a shape the broker
56
+ cannot take, `samlIdpInitiated`, `oidcScopes` and `oidcActorTokenType` of the wrong shape.
57
+ - **`issuedBy` is a versioned binding record**: `mcp-abap-adt-binding/2;<row>;<eleven address
58
+ fields>;<trust>` — the row (`authType/grantType`, or `provider/…` for the token API's consumer
59
+ provider), the exact string of every server address the row hands its provider
60
+ (`encodeURIComponent`-encoded, never canonicalised: `clientId`, `uaaUrl`, `oidcIssuerUrl`, the
61
+ three OIDC endpoints, `oidcAudience`, `samlIdpSsoUrl`, `samlAcsUrl`, `samlTokenUrl`, the
62
+ certificate client's `certUrl`) and the SHA-256 of the row's non-secret trust input (SAML
63
+ trust, OIDC scopes, the password grant's user, the token-exchange token types, the certificate
64
+ client's public certificate). Compared by exact equality, never parsed; no secret and no hash
65
+ of one in it. `issuedFor` keeps 4.x's canonical form. **Every session written before 5.0.0
66
+ reads as unbound**: its stored token and refresh token are not used, and the provider's first
67
+ renewal is a login (or a token request). A `none` row compares `issuedBy` always and refuses a
68
+ 4.x binding naming `issuedBy`. `bindingOf(means, client)` keeps its signature and returns the
69
+ version-2 record.
70
+ - **A stored secret seeds only a fully stated binding**: one whose record holds the client the
71
+ row authenticates and every address its provider sends a credential to — so an issuer-less
72
+ OIDC row with explicit endpoints and a client is now seeded, and `token_exchange` and the
73
+ token API's consumer provider never are.
74
+ - **A provider is never changed.** Every call of `getProvider` and the token API re-reads what
75
+ the destination's provider was built from (the means, the client, the certificate client when
76
+ read) and compares everything the build read, secrets included (in memory only); anything
77
+ changed builds a new provider that starts with nothing. 4.x cached a destination's provider
78
+ for the broker's life. An instance `provider` whose destination's identity changed is refused
79
+ until a new broker.
80
+ - **Two paths, resolved and cached apart.** The row path (`getProvider`, and the token API
81
+ without `provider`) and the consumer path (the token API with `provider`) each have their own
82
+ resolution, cache, identity and binding; only the destination's write queue is shared.
83
+ - **The token API's factory is handed no stored secret**: `authConfig` is the client
84
+ (`refreshToken` a key, never a value), `connConfig` the allowlisted connection means
85
+ (`serviceUrl`, `sapClient`, `language`, `authType`, `grantType`), the fourth argument the
86
+ strategy's answer and the client identity (`refreshToken` never set). The consumer path is
87
+ never seeded. A consumer provider's result without a refresh token writes `refreshToken: ''`
88
+ (the stored one is no longer carried).
89
+ - **Session writes state every field** (auth-stores 4 merges): a credential write states the
90
+ token or cookies, `expiresAt` (the provider's report), the refresh token the provider owns or
91
+ `''`, `issuedFor` (`''` when the means state no `serviceUrl`) and `issuedBy`; `saml2_pure`'s
92
+ cookies write `refreshToken: ''`; a refresh token discarded before any credential writes only
93
+ `refreshToken: ''`. A refresh token is written only by the provider that obtained it or was
94
+ seeded with it and has not discarded it since — never read from the store at write time.
95
+ - **The write queue has no retry timer.** One queue per destination: one write at a time, in
96
+ order; a replaced provider's late write is dropped once the new one has written; a failed
97
+ write stays pending until the destination's next write or `flush()`. The 1 s → 60 s retry
98
+ timer is gone. The store's contract, documented: `saveSession` settles.
99
+ - **`flush()` rejects with an `AggregateError` of `SessionWriteFailure`s** (`destination`,
100
+ `error`: the store's error classified `persisting-tokens`) instead of
101
+ `Error("<dest>": <class>)`; the message keeps 4.x's lead.
102
+ - **The built providers persist through auth-providers' `refreshStatePersistence`** (`onTokens`
103
+ is gone from every row), with the consumer's `onWriteFailure`.
104
+ - **`AuthBrokerConfig`'s optional fields are declared `?: T | undefined`.**
105
+
106
+ ### Added
107
+
108
+ - **Cancellation.** `BrokerCallOptions { signal? }` on `getProvider`, `getToken`,
109
+ `refreshToken`, `flush` and `createTokenRefresher`. Every wait is a waiter of auth-errors'
110
+ `sharedAttempt`: an abort releases that caller alone (`aborted`), the work runs on; when every
111
+ caller of a build aborted, the build is not cached and writes nothing. `getProvider`'s signal
112
+ is attached to the token or SNC provider it answers, so a login that provider starts later is
113
+ aborted once every holder's signal aborted; the token API never attaches and passes its signal
114
+ to `getTokens({ signal })` / `refreshTokens({ signal })`. `ClientAuthenticationContext.signal`:
115
+ the build's attempt. The broker sets no timeout and has no timer.
116
+ - **`authDebug?: boolean`** — passed to every token provider the broker builds, on only for
117
+ `true`, never read from the environment.
118
+ - Exports: `isDestinationConfigError`, `DestinationConfigErrorLike`, `SessionWriteFailure`,
119
+ `BrokerCallOptions`, `TokenGrant`, and the re-exported `IRenewalStrategy`.
120
+
121
+ ### Changed
122
+
123
+ - **Logging**: every line goes through a guard — a logger that throws or rejects changes no
124
+ outcome. A failed write is one `warn` line with auth-errors' `logFields`, never a class name or
125
+ message. A stored secret not used is a `warn` only where a stored secret could ever be seeded,
126
+ a `debug` line otherwise.
127
+ - **The certificate client is re-read on every call** when the build read it: a rotated
128
+ certificate builds a new provider at the next call (4.x pinned it for the broker's life).
129
+ - **Built under a stricter compiler.** Both packages, sources and tests, compile with
130
+ `noImplicitReturns`, `noFallthroughCasesInSwitch`, `noImplicitOverride`,
131
+ `noUncheckedIndexedAccess` and `exactOptionalPropertyTypes`; objects handed to a consumer keep
132
+ exactly their own keys (`src/contractShape.ts`). Lint: `noExplicitAny` is an error outside the
133
+ tests, and `lint:check` fails on any warning.
134
+ - **The broker stand and the live suites** run on the 6.0 chain and on
135
+ `@mcp-abap-adt/connection` 14; each states `renewal` and `onWriteFailure`.
136
+ - `tools/check-provider-shape.mjs`, copied byte for byte from auth-errors 2.1.1, runs with
137
+ rules 4, 5, 6 over both packages (`npm run check:shape`).
138
+
139
+ ### Dependencies
140
+
141
+ - `@mcp-abap-adt/auth-providers` `^5.3.0` → `^6.0.0`; `@mcp-abap-adt/interfaces-auth` `^3.2.0`
142
+ → `^7.5.0`; `@mcp-abap-adt/interfaces-auth-sap` `^2.0.0` → `^3.3.0`;
143
+ `@mcp-abap-adt/interfaces-auth-broker` `^1.2.0` → `^1.3.0`; new `@mcp-abap-adt/auth-errors`
144
+ `^2.1.1`. Dev: `@mcp-abap-adt/auth-stores` `^4.0.0`, `@mcp-abap-adt/connection` `^14.0.0`.
145
+
146
+ ### Measured, and not
147
+
148
+ - Not yet re-run on 5.0.0: the x509 live check (`npm run test:live:x509`, last run 2026-10-05 on
149
+ 4.1.0) and the live suite (`npm run test:live`).
13
150
 
14
151
  ## [4.1.0] - 2026-10-05
15
152