@mcp-abap-adt/auth-broker 4.1.0 → 5.0.1

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