@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/dist/binding.js CHANGED
@@ -1,200 +1,373 @@
1
1
  "use strict";
2
2
  /**
3
- * A secret is bound to the resource it was obtained for and to who issued it.
3
+ * A secret is bound to the resource it was obtained for and to everything that
4
+ * decides where it goes and whose it is.
4
5
  *
5
- * The session store keeps two strings beside the secret — `issuedFor`, the
6
- * resource, and `issuedBy`, the issuer and its client — and the broker uses a
7
- * stored secret only when both equal what this destination's means give. The
8
- * store keeps them as given; the broker canonicalises **both sides** before it
9
- * compares, with the one function here (the broker's only, decided), so a store
10
- * need not canonicalise and a legacy file's composed URI compares equal.
6
+ * The session store keeps two strings beside the secret, and the broker uses a
7
+ * stored secret only when both equal what this destination's means give:
11
8
  *
12
- * The canonical URI:
13
- * - scheme and host lower-cased (by `URL`, for http and https);
14
- * - the port explicit — `443` for `https`, `80` for `http` when the URL has
15
- * none; another scheme keeps the port it states;
16
- * - the path without a trailing `/` (the root path is empty), its case kept;
17
- * - one query parameter at most — `sap-client=<n>` for a resource,
18
- * `client_id=<id>` for an issuer, none for a SAML ACS — parsed and
19
- * re-encoded with `URLSearchParams`, so a value percent-encoded on one side
20
- * and plain on the other compares equal;
21
- * - no user info, no other parameter, no fragment.
9
+ * - **`issuedFor`**, the resource: `serviceUrl` with the SAP client, in the
10
+ * canonical form 4.x wrote (`resourceUri`). Both sides are canonicalised
11
+ * before they are compared, so a stored value written by any 4.x-era
12
+ * consumer compares equal.
13
+ * - **`issuedBy`**, a versioned record only the broker produces
14
+ * (`bindingRecord`):
22
15
  *
23
- * A URL that does not parse, or has no host, has no canonical form: the
24
- * binding is then absent, never guessed.
16
+ * ```
17
+ * mcp-abap-adt-binding/2;<row>;<eleven address fields>;<trust>
18
+ * ```
19
+ *
20
+ * `row` is `authType/grantType` (or `provider/…` for the token API's
21
+ * consumer provider); each address field is the exact string the row hands
22
+ * its provider — `encodeURIComponent`-encoded, `""` when it hands none, never
23
+ * canonicalised — in the fixed order of `RECORD_FIELDS`; `trust` is the
24
+ * SHA-256 of the row's non-secret trust input (`trustDigest`), or `""`.
25
+ * The record is compared by exact equality and never parsed, split or
26
+ * matched; no regular expression touches it.
27
+ *
28
+ * No secret takes part in either string, nor a hash of one: a hash in a
29
+ * session file can be checked offline against guesses.
25
30
  */
26
31
  Object.defineProperty(exports, "__esModule", { value: true });
32
+ exports.RECORD_FIELDS = void 0;
33
+ exports.bindingRecord = bindingRecord;
34
+ exports.trustDigest = trustDigest;
27
35
  exports.resourceUri = resourceUri;
28
- exports.issuerUri = issuerUri;
29
- exports.acsUri = acsUri;
30
36
  exports.uaaBinding = uaaBinding;
31
37
  exports.oidcBinding = oidcBinding;
32
38
  exports.samlPureBinding = samlPureBinding;
39
+ exports.samlBearerBinding = samlBearerBinding;
40
+ exports.handedOverBinding = handedOverBinding;
33
41
  exports.consumerBinding = consumerBinding;
34
42
  exports.strategyBinding = strategyBinding;
35
- exports.handedOverBinding = handedOverBinding;
36
43
  exports.sameResource = sameResource;
37
- exports.sameIssuer = sameIssuer;
44
+ exports.sameRecord = sameRecord;
45
+ exports.couldBeSeeded = couldBeSeeded;
38
46
  exports.boundHere = boundHere;
47
+ const node_crypto_1 = require("node:crypto");
48
+ /** The record's version tag; a later format is `mcp-abap-adt-binding/3`. */
49
+ const RECORD_VERSION = 'mcp-abap-adt-binding/2';
50
+ /** What the trust input is hashed under, before its JSON. */
51
+ const TRUST_PREFIX = 'mcp-abap-adt-binding/2/trust\n';
52
+ /** The eleven address fields of the record, in their fixed order. */
53
+ exports.RECORD_FIELDS = [
54
+ 'clientId',
55
+ 'uaaUrl',
56
+ 'oidcIssuerUrl',
57
+ 'oidcTokenEndpoint',
58
+ 'oidcAuthorizationEndpoint',
59
+ 'oidcDeviceAuthorizationEndpoint',
60
+ 'oidcAudience',
61
+ 'samlIdpSsoUrl',
62
+ 'samlAcsUrl',
63
+ 'samlTokenUrl',
64
+ 'certUrl',
65
+ ];
66
+ function present(value) {
67
+ return typeof value === 'string' && value !== '';
68
+ }
69
+ const HIGH_SURROGATE_FIRST = 0xd800;
70
+ const LOW_SURROGATE_FIRST = 0xdc00;
71
+ const SURROGATE_LAST = 0xdfff;
72
+ /**
73
+ * `encodeURIComponent(value)`, total: a lone surrogate — on which the
74
+ * platform function throws — is written `%uXXXX`, which no
75
+ * `encodeURIComponent` output contains (its `%` is always followed by two hex
76
+ * digits), so two different strings never encode equal. Linear in the length.
77
+ */
78
+ function encodeField(value) {
79
+ try {
80
+ return encodeURIComponent(value);
81
+ }
82
+ catch {
83
+ // A lone surrogate: encode unit by unit below.
84
+ }
85
+ let out = '';
86
+ for (let i = 0; i < value.length; i += 1) {
87
+ const unit = value.charCodeAt(i);
88
+ if (unit >= HIGH_SURROGATE_FIRST && unit < LOW_SURROGATE_FIRST) {
89
+ const next = i + 1 < value.length ? value.charCodeAt(i + 1) : 0;
90
+ if (next >= LOW_SURROGATE_FIRST && next <= SURROGATE_LAST) {
91
+ out += encodeURIComponent(value.slice(i, i + 2));
92
+ i += 1;
93
+ continue;
94
+ }
95
+ }
96
+ if (unit >= HIGH_SURROGATE_FIRST && unit <= SURROGATE_LAST) {
97
+ out += `%u${unit.toString(16).toUpperCase()}`;
98
+ continue;
99
+ }
100
+ out += encodeURIComponent(value.charAt(i));
101
+ }
102
+ return out;
103
+ }
104
+ /**
105
+ * The `issuedBy` record: the version, the row, the eleven address fields in
106
+ * their fixed order — each the exact string, encoded, `""` when absent or
107
+ * `''` — and the trust digest. Every field is always present, and none holds
108
+ * `;`, so the string is unambiguous without being parsed.
109
+ */
110
+ function bindingRecord(row, fields, trust) {
111
+ const parts = [RECORD_VERSION, row];
112
+ for (const name of exports.RECORD_FIELDS) {
113
+ const value = fields[name];
114
+ parts.push(present(value) ? encodeField(value) : '');
115
+ }
116
+ parts.push(trust);
117
+ return parts.join(';');
118
+ }
119
+ /**
120
+ * A value of the trust input as the means state it — a string, a number, a
121
+ * boolean, an array of strings — or `null` for an absent one. A row refuses a
122
+ * trust input of any other shape before it builds (`trustShaped`,
123
+ * `samlRefusal`, the certificate check in `getProvider`), so `null` never
124
+ * stands for a malformed value a built provider received; only `bindingOf`,
125
+ * whose means `getProvider` would refuse, can reach it.
126
+ */
127
+ function trustValue(value) {
128
+ if (typeof value === 'string' || typeof value === 'boolean')
129
+ return value;
130
+ if (typeof value === 'number')
131
+ return Number.isFinite(value) ? value : null;
132
+ if (Array.isArray(value)) {
133
+ const copy = [];
134
+ for (const element of value) {
135
+ if (typeof element !== 'string')
136
+ return null;
137
+ copy.push(element);
138
+ }
139
+ return copy;
140
+ }
141
+ return null;
142
+ }
143
+ /**
144
+ * The trust digest: lower-case hex SHA-256 of the UTF-8 bytes of
145
+ * `"mcp-abap-adt-binding/2/trust\n"` and the JSON of the `[name, value]` pairs
146
+ * in the row's fixed order — no trimming, no canonicalisation. JSON of arrays
147
+ * and primitives has one output (lone surrogates escaped), so equal inputs
148
+ * give equal bytes.
149
+ */
150
+ function trustDigest(pairs) {
151
+ const serialised = JSON.stringify(pairs.map(([name, value]) => [name, trustValue(value)]));
152
+ return (0, node_crypto_1.createHash)('sha256')
153
+ .update(`${TRUST_PREFIX}${serialised}`, 'utf8')
154
+ .digest('hex');
155
+ }
39
156
  /** The default port a URL without one is taken to mean. */
40
157
  const DEFAULT_PORTS = {
41
158
  'https:': '443',
42
159
  'http:': '80',
43
160
  };
44
- function present(value) {
45
- return typeof value === 'string' && value !== '';
161
+ /** `value` without the slashes it ends with, in one backwards pass. */
162
+ function withoutTrailingSlashes(value) {
163
+ let end = value.length;
164
+ while (end > 0 && value.charCodeAt(end - 1) === 0x2f)
165
+ end -= 1;
166
+ return value.slice(0, end);
46
167
  }
47
168
  /**
48
- * The canonical URI of `raw`, keeping only the query parameter `param` — its
49
- * value `override` when given, else the one `raw` carries.
169
+ * The canonical resource URI, as 4.x wrote it: scheme and host lower-cased
170
+ * (by `URL`, for http and https); the port explicit (`443` / `80` by
171
+ * default); the path without a trailing `/`, its case kept; one query
172
+ * parameter, `sap-client=<n>` — `sapClient` when given, else the one the URL
173
+ * carries — re-encoded with `URLSearchParams`; no user info, no other
174
+ * parameter, no fragment. A URL that does not parse, or has no host, has no
175
+ * canonical form: `undefined`, never guessed. For a stored value, call it
176
+ * without `sapClient`.
50
177
  */
51
- function canonical(raw, param, override) {
52
- if (!present(raw))
178
+ function resourceUri(serviceUrl, sapClient) {
179
+ if (!present(serviceUrl))
53
180
  return undefined;
54
181
  let url;
55
182
  try {
56
- url = new URL(raw);
183
+ url = new URL(serviceUrl);
57
184
  }
58
185
  catch {
59
186
  return undefined;
60
187
  }
61
- // `URL` lower-cases the scheme, and the host of an http(s) URL. Another
62
- // scheme's host is kept as written: no form beyond http(s) is defined yet.
63
188
  const host = url.hostname;
64
189
  if (host === '')
65
190
  return undefined;
66
191
  const scheme = url.protocol;
67
192
  const port = url.port || DEFAULT_PORTS[scheme] || '';
68
- const path = url.pathname.replace(/\/+$/, '');
69
- let query = '';
70
- if (param) {
71
- const value = present(override) ? override : url.searchParams.get(param);
72
- if (present(value)) {
73
- query = `?${new URLSearchParams([[param, value]]).toString()}`;
74
- }
75
- }
193
+ const path = withoutTrailingSlashes(url.pathname);
194
+ const client = present(sapClient)
195
+ ? sapClient
196
+ : url.searchParams.get('sap-client');
197
+ const query = present(client)
198
+ ? `?${new URLSearchParams([['sap-client', client]]).toString()}`
199
+ : '';
76
200
  return `${scheme}//${host}${port ? `:${port}` : ''}${path}${query}`;
77
201
  }
78
- /**
79
- * The resource: `serviceUrl` with the SAP client as `sap-client` — the
80
- * destination's `sapClient` when it states one, over a `sap-client` already in
81
- * the URL. For a stored value, call it without `sapClient`.
82
- */
83
- function resourceUri(serviceUrl, sapClient) {
84
- return canonical(serviceUrl, 'sap-client', sapClient);
85
- }
86
- /**
87
- * The issuer and its client: the authorization server's base URL with
88
- * `client_id`. For a stored value, call it without `clientId`.
89
- */
90
- function issuerUri(issuerUrl, clientId) {
91
- return canonical(issuerUrl, 'client_id', clientId);
92
- }
93
- /** The ACS of the system that set SAML session cookies: origin and path. */
94
- function acsUri(acsUrl) {
95
- return canonical(acsUrl, undefined);
96
- }
97
202
  /** The resource the means name: `serviceUrl` with `sapClient`. */
98
203
  function resourceOf(means) {
99
204
  return resourceUri(means.serviceUrl, present(means.sapClient) ? means.sapClient : undefined);
100
205
  }
206
+ /** The certificate client's public certificate, for the trust input. */
207
+ function certificateOf(certificate) {
208
+ return certificate ? certificate.certificate : null;
209
+ }
210
+ /** `certUrl` is stated whenever the build read a certificate client. */
211
+ function certificateStated(certificate) {
212
+ return !certificate || present(certificate.certUrl);
213
+ }
101
214
  /**
102
- * The UAA grants (`authorization_code`, `client_credentials`, `passcode`) and
103
- * `saml2_bearer`, whose token UAA issues to its client: the resource, and `uaaUrl` with `uaaClientId` — both required to match.
215
+ * The UAA grants (`authorization_code`, `client_credentials`, `passcode`):
216
+ * the client id and `uaaUrl` as the provider receives them, and `certUrl`
217
+ * when the build read the certificate client; trust is the client
218
+ * certificate. Fully stated with the client, `uaaUrl` and — when read —
219
+ * `certUrl`.
104
220
  */
105
- function uaaBinding(means, client) {
106
- const stated = present(client?.uaaUrl) && present(client?.uaaClientId);
221
+ function uaaBinding(grant, means, client, certificate) {
107
222
  return {
108
223
  issuedFor: resourceOf(means),
109
- issuedBy: stated
110
- ? issuerUri(client?.uaaUrl, client?.uaaClientId)
111
- : undefined,
112
- issuerKind: 'issuer',
113
- issuerStated: stated,
224
+ issuedBy: bindingRecord(`jwt/${grant}`, {
225
+ clientId: client?.uaaClientId,
226
+ uaaUrl: client?.uaaUrl,
227
+ certUrl: certificate?.certUrl,
228
+ }, trustDigest([['clientCertificate', certificateOf(certificate)]])),
229
+ fullyStated: present(client?.uaaClientId) &&
230
+ present(client?.uaaUrl) &&
231
+ certificateStated(certificate),
114
232
  };
115
233
  }
116
234
  /**
117
- * The OIDC grants: the resource, and the issuer — `oidcIssuerUrl`, else
118
- * `uaaUrl` — with `uaaClientId`; both required to match. Means that state
119
- * neither issuer (explicit endpoints alone) bind no stored secret: one is then
120
- * never seeded.
235
+ * The OIDC grants: the client id, `oidcIssuerUrl`, `oidcTokenEndpoint`, and
236
+ * the grant's own endpoint (`oidcAuthorizationEndpoint`,
237
+ * `oidcDeviceAuthorizationEndpoint`) or audience (`oidcAudience`), and
238
+ * `certUrl` when read; trust is the scopes, the `password` grant's
239
+ * `username`, the token-exchange token types, and the client certificate.
240
+ *
241
+ * Fully stated with the client, each endpoint the grant sends to — explicit,
242
+ * or the issuer it is discovered from — and `certUrl` when read;
243
+ * `token_exchange` never: its subject token, a secret, decides whose
244
+ * credential it obtains.
121
245
  */
122
- function oidcBinding(means, client) {
123
- const issuer = present(means.oidcIssuerUrl)
124
- ? means.oidcIssuerUrl
125
- : client?.uaaUrl;
126
- const stated = present(issuer) && present(client?.uaaClientId);
246
+ function oidcBinding(grant, means, client, certificate) {
247
+ const issuer = present(means.oidcIssuerUrl);
248
+ const reaches = (endpoint) => issuer || present(endpoint);
249
+ const trust = [['oidcScopes', means.oidcScopes]];
250
+ if (grant === 'password')
251
+ trust.push(['username', means.username]);
252
+ if (grant === 'token_exchange') {
253
+ trust.push(['oidcSubjectTokenType', means.oidcSubjectTokenType], ['oidcActorTokenType', means.oidcActorTokenType]);
254
+ }
255
+ trust.push(['clientCertificate', certificateOf(certificate)]);
127
256
  return {
128
257
  issuedFor: resourceOf(means),
129
- issuedBy: stated ? issuerUri(issuer, client?.uaaClientId) : undefined,
130
- issuerKind: 'issuer',
131
- issuerStated: stated,
258
+ issuedBy: bindingRecord(`jwt/${grant}`, {
259
+ clientId: client?.uaaClientId,
260
+ oidcIssuerUrl: means.oidcIssuerUrl,
261
+ oidcTokenEndpoint: means.oidcTokenEndpoint,
262
+ oidcAuthorizationEndpoint: grant === 'oidc_authorization_code'
263
+ ? means.oidcAuthorizationEndpoint
264
+ : undefined,
265
+ oidcDeviceAuthorizationEndpoint: grant === 'device_code'
266
+ ? means.oidcDeviceAuthorizationEndpoint
267
+ : undefined,
268
+ oidcAudience: grant === 'token_exchange' ? means.oidcAudience : undefined,
269
+ certUrl: certificate?.certUrl,
270
+ }, trustDigest(trust)),
271
+ fullyStated: grant !== 'token_exchange' &&
272
+ present(client?.uaaClientId) &&
273
+ reaches(means.oidcTokenEndpoint) &&
274
+ (grant !== 'oidc_authorization_code' ||
275
+ reaches(means.oidcAuthorizationEndpoint)) &&
276
+ (grant !== 'device_code' ||
277
+ reaches(means.oidcDeviceAuthorizationEndpoint)) &&
278
+ certificateStated(certificate),
132
279
  };
133
280
  }
281
+ /** The SAML trust input both SAML grants share, in order. */
282
+ function samlTrust(means) {
283
+ return [
284
+ ['samlIdpCertificates', means.samlIdpCertificates],
285
+ ['samlIdpEntityId', means.samlIdpEntityId],
286
+ ['samlSpEntityId', means.samlSpEntityId],
287
+ ['samlClockSkewMs', means.samlClockSkewMs],
288
+ ['samlIdpInitiated', means.samlIdpInitiated],
289
+ ];
290
+ }
134
291
  /**
135
- * `saml2_pure`: the resource, and the ACS of the system that sets the
136
- * cookies — `samlAcsUrl`, origin and path; both required to match.
292
+ * `saml2_pure`: the IdP's SSO URL and the ACS of the system that sets the
293
+ * cookies; trust is the SAML trust. Fully stated with both.
137
294
  */
138
295
  function samlPureBinding(means) {
139
296
  return {
140
297
  issuedFor: resourceOf(means),
141
- issuedBy: acsUri(means.samlAcsUrl),
142
- issuerKind: 'acs',
143
- issuerStated: present(means.samlAcsUrl),
298
+ issuedBy: bindingRecord('saml/saml2_pure', { samlIdpSsoUrl: means.samlIdpSsoUrl, samlAcsUrl: means.samlAcsUrl }, trustDigest(samlTrust(means))),
299
+ fullyStated: present(means.samlIdpSsoUrl) && present(means.samlAcsUrl),
144
300
  };
145
301
  }
146
302
  /**
147
- * The token API with a consumer's provider: what the broker handed
148
- * that provider — the `serviceUrl` it resolved, with the SAP client, and for a
149
- * factory the client it resolved (`uaaUrl` with `uaaClientId`). An instance is
150
- * handed no client, so no issuer is claimed for what it obtains: `issuedBy` is
151
- * then absent, and `getProvider` never seeds from that secret.
303
+ * `saml2_bearer`: the client id, `uaaUrl`, the IdP's SSO URL, the ACS,
304
+ * `samlTokenUrl`, and `certUrl` when read; trust is the SAML trust and the
305
+ * client certificate. Fully stated with the client, the IdP, the token
306
+ * endpoint (`samlTokenUrl`, or `uaaUrl`) and — when read — `certUrl`.
152
307
  */
153
- function consumerBinding(serviceUrl, sapClient, client) {
154
- return uaaBinding({ serviceUrl, sapClient }, client);
308
+ function samlBearerBinding(means, client, certificate) {
309
+ return {
310
+ issuedFor: resourceOf(means),
311
+ issuedBy: bindingRecord('saml/saml2_bearer', {
312
+ clientId: client?.uaaClientId,
313
+ uaaUrl: client?.uaaUrl,
314
+ samlIdpSsoUrl: means.samlIdpSsoUrl,
315
+ samlAcsUrl: means.samlAcsUrl,
316
+ samlTokenUrl: means.samlTokenUrl,
317
+ certUrl: certificate?.certUrl,
318
+ }, trustDigest([
319
+ ...samlTrust(means),
320
+ ['clientCertificate', certificateOf(certificate)],
321
+ ])),
322
+ fullyStated: present(client?.uaaClientId) &&
323
+ present(means.samlIdpSsoUrl) &&
324
+ (present(means.samlTokenUrl) || present(client?.uaaUrl)) &&
325
+ certificateStated(certificate),
326
+ };
155
327
  }
156
328
  /**
157
- * A binding on the `clientAuthentication` strategy path: a resource that
158
- * neither the means nor the stored session state matches, so the issuer and
159
- * client alone decide (`Binding.unstatedResourceMatches`).
329
+ * A `none` row: the resource, and the record — for `jwt` the client id,
330
+ * `uaaUrl` and `oidcIssuerUrl` as the means state them, for `saml` the ACS;
331
+ * no trust. A handed-over credential is compared by exact equality of the
332
+ * whole record; it is never seeded into a renewal, so it is not
333
+ * "fully stated".
160
334
  */
161
- function strategyBinding(binding) {
162
- return { ...binding, unstatedResourceMatches: true };
335
+ function handedOverBinding(authType, means, client) {
336
+ const fields = authType === 'saml'
337
+ ? { samlAcsUrl: means.samlAcsUrl }
338
+ : {
339
+ clientId: client?.uaaClientId,
340
+ uaaUrl: client?.uaaUrl,
341
+ oidcIssuerUrl: means.oidcIssuerUrl,
342
+ };
343
+ return {
344
+ issuedFor: resourceOf(means),
345
+ issuedBy: bindingRecord(`${authType}/none`, fields, ''),
346
+ fullyStated: false,
347
+ };
163
348
  }
164
349
  /**
165
- * A `none` row: the resource always; the issuer only when the means state one
166
- * — `oidcIssuerUrl` (with `uaaClientId` when stated) or the client (`uaaUrl`
167
- * and `uaaClientId`) for `jwt`, `samlAcsUrl` for `saml`.
350
+ * The token API with a consumer's provider: the `serviceUrl` it resolved,
351
+ * with the SAP client, and the record `provider/<row>` — for a factory the
352
+ * client id and `uaaUrl` of the client it was handed, for an instance (handed
353
+ * no client) none; no trust. Never fully stated: the broker cannot know what
354
+ * the factory composes, and an instance is handed no client.
168
355
  */
169
- function handedOverBinding(authType, means, client) {
170
- const issuedFor = resourceOf(means);
171
- if (authType === 'saml') {
172
- return {
173
- issuedFor,
174
- issuedBy: acsUri(means.samlAcsUrl),
175
- issuerKind: 'acs',
176
- issuerStated: present(means.samlAcsUrl),
177
- };
178
- }
179
- const clientId = present(client?.uaaClientId)
180
- ? client.uaaClientId
181
- : undefined;
182
- if (present(means.oidcIssuerUrl)) {
183
- return {
184
- issuedFor,
185
- issuedBy: issuerUri(means.oidcIssuerUrl, clientId),
186
- issuerKind: 'issuer',
187
- issuerStated: true,
188
- };
189
- }
190
- const stated = present(client?.uaaUrl) && clientId !== undefined;
356
+ function consumerBinding(row, serviceUrl, sapClient, client) {
191
357
  return {
192
- issuedFor,
193
- issuedBy: stated ? issuerUri(client?.uaaUrl, clientId) : undefined,
194
- issuerKind: 'issuer',
195
- issuerStated: stated,
358
+ issuedFor: resourceUri(serviceUrl, sapClient),
359
+ issuedBy: bindingRecord(row, { clientId: client?.uaaClientId, uaaUrl: client?.uaaUrl }, ''),
360
+ fullyStated: false,
196
361
  };
197
362
  }
363
+ /**
364
+ * A binding on the `clientAuthentication` strategy path: a resource that
365
+ * neither the means nor the stored session state matches
366
+ * (`Binding.unstatedResourceMatches`).
367
+ */
368
+ function strategyBinding(binding) {
369
+ return { ...binding, unstatedResourceMatches: true };
370
+ }
198
371
  /**
199
372
  * The stored `issuedFor`, canonicalised, equals the destination's — or, with
200
373
  * `unstatedResourceMatches`, neither states one.
@@ -205,19 +378,29 @@ function sameResource(stored, binding) {
205
378
  }
206
379
  return resourceUri(stored?.issuedFor) === binding.issuedFor;
207
380
  }
208
- /** The stored `issuedBy`, canonicalised as the destination's kind, equals it. */
209
- function sameIssuer(stored, binding) {
210
- if (binding.issuedBy === undefined)
211
- return false;
212
- const read = binding.issuerKind === 'acs' ? acsUri : issuerUri;
213
- return read(stored?.issuedBy) === binding.issuedBy;
381
+ /** The stored `issuedBy` is exactly this binding's record. */
382
+ function sameRecord(stored, binding) {
383
+ return stored?.issuedBy === binding.issuedBy;
384
+ }
385
+ /**
386
+ * Whether any stored secret could ever seed this binding's provider: the
387
+ * binding is fully stated and its resource can match — stated, or matching
388
+ * when neither side states one (`unstatedResourceMatches`). The condition
389
+ * `boundHere` needs beside the stored values.
390
+ */
391
+ function couldBeSeeded(binding) {
392
+ return (binding.fullyStated &&
393
+ (binding.issuedFor !== undefined ||
394
+ binding.unstatedResourceMatches === true));
214
395
  }
215
396
  /**
216
- * An obtained secret is this destination's only when **both** stored values
217
- * equal the computed ones; either absent, on either side, is not a match —
218
- * save a resource absent on both sides of a strategy-path binding
219
- * (`unstatedResourceMatches`).
397
+ * A stored secret may seed this binding's provider — or have its refresh
398
+ * token carried — only when the binding is fully stated, the resource
399
+ * matches, and the stored `issuedBy` is exactly the record. A session with no
400
+ * `issuedBy`, or a 4.x one, never matches.
220
401
  */
221
402
  function boundHere(stored, binding) {
222
- return sameResource(stored, binding) && sameIssuer(stored, binding);
403
+ return (binding.fullyStated &&
404
+ sameResource(stored, binding) &&
405
+ sameRecord(stored, binding));
223
406
  }
@@ -6,15 +6,20 @@
6
6
  */
7
7
  import type { DestinationGrant, IConnectionConfig } from '@mcp-abap-adt/interfaces-auth-broker';
8
8
  import type { IAuthorizationConfig } from '@mcp-abap-adt/interfaces-auth-sap';
9
- import type { BoundClient } from './binding';
10
- import { type Binding } from './binding';
9
+ import { type Binding, type BoundCertificate, type BoundClient } from './binding';
11
10
  /**
12
- * The binding of a `jwt` or `saml` destination with the grant it states: the
13
- * UAA grants and `saml2_bearer` by `uaaUrl` and the client, the OIDC grants by
14
- * the issuer and the client, `saml2_pure` by its ACS, and `none` by what the
15
- * means state.
11
+ * The binding of a `jwt` or `saml` destination with the grant it states — a
12
+ * pair of the closed list, checked before (`statedGrant`): the UAA grants,
13
+ * the OIDC grants, `saml2_pure`, `saml2_bearer` and `none`, each with the
14
+ * fields of its row. `certificate` is the certificate client the build
15
+ * read for the `clientAuthentication` strategy, or `null` when it read none.
16
16
  */
17
- export declare function destinationBinding(authType: 'jwt' | 'saml', grant: DestinationGrant, means: IConnectionConfig, client: BoundClient | null): Binding;
17
+ export declare function destinationBinding(authType: 'jwt' | 'saml', grant: DestinationGrant, means: IConnectionConfig, client: BoundClient | null, certificate?: BoundCertificate | null): Binding;
18
+ /**
19
+ * The row of the token API's consumer provider: `provider/<authType>/<grant>`
20
+ * when the means state a pair of the closed list, else `provider/-`.
21
+ */
22
+ export declare function consumerRow(means: IConnectionConfig | null): string;
18
23
  /** The two strings a session store keeps beside a secret; absent where the means cannot say. */
19
24
  export interface SecretBinding {
20
25
  issuedFor?: string;
@@ -22,16 +27,29 @@ export interface SecretBinding {
22
27
  }
23
28
  /**
24
29
  * The binding the broker writes beside a secret for a destination with these
25
- * means and this client — canonical, exactly what `getProvider` compares a
26
- * stored secret against. For a consumer that hands over a credential the
27
- * broker cannot obtain (a `none` destination: a token or cookies it was
28
- * given) and writes it to the session store itself: written with this binding,
29
- * `getProvider` presents it; written with any other, it refuses.
30
+ * means and this client: `issuedFor` the canonical resource, `issuedBy` the
31
+ * version-2 record of the row the means state, built from `means` and
32
+ * `client` alone.
33
+ *
34
+ * It is exactly what `getProvider` compares for a `none` row — its use: a
35
+ * consumer that hands over a credential the broker cannot obtain (a token or
36
+ * cookies it was given) writes it with this binding, and `getProvider`
37
+ * presents it; written with any other — a 4.x `bindingOf`'s included — it
38
+ * refuses. It is also exactly what `getProvider` writes for a token row built
39
+ * without a `clientAuthentication` strategy.
40
+ *
41
+ * It is **not** what a token row writes beside a `clientAuthentication`
42
+ * strategy: there the row's record also holds what the build read of the
43
+ * certificate client — its `certUrl`, and its certificate in the trust
44
+ * digest — and its client identity may be the certificate client's, none of
45
+ * which `bindingOf` is given. Nor is it the token API's record for a
46
+ * consumer's provider (`provider/…`).
30
47
  *
31
48
  * `means` are the key store's (`authType`, `grantType`, `serviceUrl`,
32
- * `sapClient`, the issuer or ACS); `client` is the key store's client, when the
33
- * destination has one. A destination that states no `jwt` / `saml` type or no
34
- * grant binds nothing: `{}`. Never logs, never throws.
49
+ * `sapClient`, the addresses and trust the row reads); `client` is the key
50
+ * store's client, when the destination has one. A destination that states no
51
+ * `jwt` / `saml` type, no grant, or a pair outside the closed list binds
52
+ * nothing: `{}`. Never logs, never throws.
35
53
  */
36
54
  export declare function bindingOf(means: IConnectionConfig, client?: IAuthorizationConfig | null): SecretBinding;
37
55
  //# sourceMappingURL=bindingOf.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bindingOf.d.ts","sourceRoot":"","sources":["../src/bindingOf.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,gBAAgB,EAChB,iBAAiB,EAClB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EACL,KAAK,OAAO,EAKb,MAAM,WAAW,CAAC;AAGnB;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,KAAK,GAAG,MAAM,EACxB,KAAK,EAAE,gBAAgB,EACvB,KAAK,EAAE,iBAAiB,EACxB,MAAM,EAAE,WAAW,GAAG,IAAI,GACzB,OAAO,CAKT;AAED,gGAAgG;AAChG,MAAM,WAAW,aAAa;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,SAAS,CACvB,KAAK,EAAE,iBAAiB,EACxB,MAAM,GAAE,oBAAoB,GAAG,IAAW,GACzC,aAAa,CAcf"}
1
+ {"version":3,"file":"bindingOf.d.ts","sourceRoot":"","sources":["../src/bindingOf.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,gBAAgB,EAChB,iBAAiB,EAClB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAMjB,MAAM,WAAW,CAAC;AAGnB;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,KAAK,GAAG,MAAM,EACxB,KAAK,EAAE,gBAAgB,EACvB,KAAK,EAAE,iBAAiB,EACxB,MAAM,EAAE,WAAW,GAAG,IAAI,EAC1B,WAAW,GAAE,gBAAgB,GAAG,IAAW,GAC1C,OAAO,CAUT;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,GAAG,MAAM,CAMnE;AAED,gGAAgG;AAChG,MAAM,WAAW,aAAa;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,SAAS,CACvB,KAAK,EAAE,iBAAiB,EACxB,MAAM,GAAE,oBAAoB,GAAG,IAAW,GACzC,aAAa,CAcf"}