@mcp-abap-adt/auth-broker 4.0.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,207 +1,406 @@
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;
33
- exports.consumerBinding = consumerBinding;
39
+ exports.samlBearerBinding = samlBearerBinding;
34
40
  exports.handedOverBinding = handedOverBinding;
41
+ exports.consumerBinding = consumerBinding;
42
+ exports.strategyBinding = strategyBinding;
35
43
  exports.sameResource = sameResource;
36
- exports.sameIssuer = sameIssuer;
44
+ exports.sameRecord = sameRecord;
45
+ exports.couldBeSeeded = couldBeSeeded;
37
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
+ }
38
156
  /** The default port a URL without one is taken to mean. */
39
157
  const DEFAULT_PORTS = {
40
158
  'https:': '443',
41
159
  'http:': '80',
42
160
  };
43
- function present(value) {
44
- 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);
45
167
  }
46
168
  /**
47
- * The canonical URI of `raw`, keeping only the query parameter `param` — its
48
- * 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`.
49
177
  */
50
- function canonical(raw, param, override) {
51
- if (!present(raw))
178
+ function resourceUri(serviceUrl, sapClient) {
179
+ if (!present(serviceUrl))
52
180
  return undefined;
53
181
  let url;
54
182
  try {
55
- url = new URL(raw);
183
+ url = new URL(serviceUrl);
56
184
  }
57
185
  catch {
58
186
  return undefined;
59
187
  }
60
- // `URL` lower-cases the scheme, and the host of an http(s) URL. Another
61
- // scheme's host is kept as written: no form beyond http(s) is defined yet.
62
188
  const host = url.hostname;
63
189
  if (host === '')
64
190
  return undefined;
65
191
  const scheme = url.protocol;
66
192
  const port = url.port || DEFAULT_PORTS[scheme] || '';
67
- const path = url.pathname.replace(/\/+$/, '');
68
- let query = '';
69
- if (param) {
70
- const value = present(override) ? override : url.searchParams.get(param);
71
- if (present(value)) {
72
- query = `?${new URLSearchParams([[param, value]]).toString()}`;
73
- }
74
- }
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
+ : '';
75
200
  return `${scheme}//${host}${port ? `:${port}` : ''}${path}${query}`;
76
201
  }
77
- /**
78
- * The resource: `serviceUrl` with the SAP client as `sap-client` — the
79
- * destination's `sapClient` when it states one, over a `sap-client` already in
80
- * the URL. For a stored value, call it without `sapClient`.
81
- */
82
- function resourceUri(serviceUrl, sapClient) {
83
- return canonical(serviceUrl, 'sap-client', sapClient);
84
- }
85
- /**
86
- * The issuer and its client: the authorization server's base URL with
87
- * `client_id`. For a stored value, call it without `clientId`.
88
- */
89
- function issuerUri(issuerUrl, clientId) {
90
- return canonical(issuerUrl, 'client_id', clientId);
91
- }
92
- /** The ACS of the system that set SAML session cookies: origin and path. */
93
- function acsUri(acsUrl) {
94
- return canonical(acsUrl, undefined);
95
- }
96
202
  /** The resource the means name: `serviceUrl` with `sapClient`. */
97
203
  function resourceOf(means) {
98
204
  return resourceUri(means.serviceUrl, present(means.sapClient) ? means.sapClient : undefined);
99
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
+ }
100
214
  /**
101
- * The UAA grants (`authorization_code`, `client_credentials`, `passcode`) and
102
- * `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`.
103
220
  */
104
- function uaaBinding(means, client) {
105
- const stated = present(client?.uaaUrl) && present(client?.uaaClientId);
221
+ function uaaBinding(grant, means, client, certificate) {
106
222
  return {
107
223
  issuedFor: resourceOf(means),
108
- issuedBy: stated
109
- ? issuerUri(client?.uaaUrl, client?.uaaClientId)
110
- : undefined,
111
- issuerKind: 'issuer',
112
- 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),
113
232
  };
114
233
  }
115
234
  /**
116
- * The OIDC grants: the resource, and the issuer — `oidcIssuerUrl`, else
117
- * `uaaUrl` — with `uaaClientId`; both required to match. Means that state
118
- * neither issuer (explicit endpoints alone) bind no stored secret: one is then
119
- * 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.
120
245
  */
121
- function oidcBinding(means, client) {
122
- const issuer = present(means.oidcIssuerUrl)
123
- ? means.oidcIssuerUrl
124
- : client?.uaaUrl;
125
- 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)]);
126
256
  return {
127
257
  issuedFor: resourceOf(means),
128
- issuedBy: stated ? issuerUri(issuer, client?.uaaClientId) : undefined,
129
- issuerKind: 'issuer',
130
- 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),
131
279
  };
132
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
+ }
133
291
  /**
134
- * `saml2_pure`: the resource, and the ACS of the system that sets the
135
- * 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.
136
294
  */
137
295
  function samlPureBinding(means) {
138
296
  return {
139
297
  issuedFor: resourceOf(means),
140
- issuedBy: acsUri(means.samlAcsUrl),
141
- issuerKind: 'acs',
142
- 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),
143
300
  };
144
301
  }
145
302
  /**
146
- * The token API with a consumer's provider: what the broker handed
147
- * that provider — the `serviceUrl` it resolved, with the SAP client, and for a
148
- * factory the client it resolved (`uaaUrl` with `uaaClientId`). An instance is
149
- * handed no client, so no issuer is claimed for what it obtains: `issuedBy` is
150
- * 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`.
151
307
  */
152
- function consumerBinding(serviceUrl, sapClient, client) {
153
- 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
+ };
154
327
  }
155
328
  /**
156
- * A `none` row: the resource always; the issuer only when the means state one
157
- * — `oidcIssuerUrl` (with `uaaClientId` when stated) or the client (`uaaUrl`
158
- * and `uaaClientId`) for `jwt`, `samlAcsUrl` for `saml`.
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".
159
334
  */
160
335
  function handedOverBinding(authType, means, client) {
161
- const issuedFor = resourceOf(means);
162
- if (authType === 'saml') {
163
- return {
164
- issuedFor,
165
- issuedBy: acsUri(means.samlAcsUrl),
166
- issuerKind: 'acs',
167
- issuerStated: present(means.samlAcsUrl),
168
- };
169
- }
170
- const clientId = present(client?.uaaClientId)
171
- ? client.uaaClientId
172
- : undefined;
173
- if (present(means.oidcIssuerUrl)) {
174
- return {
175
- issuedFor,
176
- issuedBy: issuerUri(means.oidcIssuerUrl, clientId),
177
- issuerKind: 'issuer',
178
- issuerStated: true,
336
+ const fields = authType === 'saml'
337
+ ? { samlAcsUrl: means.samlAcsUrl }
338
+ : {
339
+ clientId: client?.uaaClientId,
340
+ uaaUrl: client?.uaaUrl,
341
+ oidcIssuerUrl: means.oidcIssuerUrl,
179
342
  };
180
- }
181
- const stated = present(client?.uaaUrl) && clientId !== undefined;
182
343
  return {
183
- issuedFor,
184
- issuedBy: stated ? issuerUri(client?.uaaUrl, clientId) : undefined,
185
- issuerKind: 'issuer',
186
- issuerStated: stated,
344
+ issuedFor: resourceOf(means),
345
+ issuedBy: bindingRecord(`${authType}/none`, fields, ''),
346
+ fullyStated: false,
187
347
  };
188
348
  }
189
- /** The stored `issuedFor`, canonicalised, equals the destination's. */
349
+ /**
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.
355
+ */
356
+ function consumerBinding(row, serviceUrl, sapClient, client) {
357
+ return {
358
+ issuedFor: resourceUri(serviceUrl, sapClient),
359
+ issuedBy: bindingRecord(row, { clientId: client?.uaaClientId, uaaUrl: client?.uaaUrl }, ''),
360
+ fullyStated: false,
361
+ };
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
+ }
371
+ /**
372
+ * The stored `issuedFor`, canonicalised, equals the destination's — or, with
373
+ * `unstatedResourceMatches`, neither states one.
374
+ */
190
375
  function sameResource(stored, binding) {
191
- return (binding.issuedFor !== undefined &&
192
- resourceUri(stored?.issuedFor) === binding.issuedFor);
376
+ if (binding.issuedFor === undefined) {
377
+ return (binding.unstatedResourceMatches === true && !present(stored?.issuedFor));
378
+ }
379
+ return resourceUri(stored?.issuedFor) === binding.issuedFor;
380
+ }
381
+ /** The stored `issuedBy` is exactly this binding's record. */
382
+ function sameRecord(stored, binding) {
383
+ return stored?.issuedBy === binding.issuedBy;
193
384
  }
194
- /** The stored `issuedBy`, canonicalised as the destination's kind, equals it. */
195
- function sameIssuer(stored, binding) {
196
- if (binding.issuedBy === undefined)
197
- return false;
198
- const read = binding.issuerKind === 'acs' ? acsUri : issuerUri;
199
- return read(stored?.issuedBy) === binding.issuedBy;
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));
200
395
  }
201
396
  /**
202
- * An obtained secret is this destination's only when **both** stored values
203
- * equal the computed ones; either absent, on either side, is not a match.
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.
204
401
  */
205
402
  function boundHere(stored, binding) {
206
- return sameResource(stored, binding) && sameIssuer(stored, binding);
403
+ return (binding.fullyStated &&
404
+ sameResource(stored, binding) &&
405
+ sameRecord(stored, binding));
207
406
  }
@@ -6,14 +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 Binding } from './binding';
9
+ import { type Binding, type BoundCertificate, type BoundClient } from './binding';
10
10
  /**
11
- * The binding of a `jwt` or `saml` destination with the grant it states: the
12
- * UAA grants and `saml2_bearer` by `uaaUrl` and the client, the OIDC grants by
13
- * the issuer and the client, `saml2_pure` by its ACS, and `none` by what the
14
- * 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.
15
16
  */
16
- export declare function destinationBinding(authType: 'jwt' | 'saml', grant: DestinationGrant, means: IConnectionConfig, client: IAuthorizationConfig | 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;
17
23
  /** The two strings a session store keeps beside a secret; absent where the means cannot say. */
18
24
  export interface SecretBinding {
19
25
  issuedFor?: string;
@@ -21,16 +27,29 @@ export interface SecretBinding {
21
27
  }
22
28
  /**
23
29
  * The binding the broker writes beside a secret for a destination with these
24
- * means and this client — canonical, exactly what `getProvider` compares a
25
- * stored secret against. For a consumer that hands over a credential the
26
- * broker cannot obtain (a `none` destination: a token or cookies it was
27
- * given) and writes it to the session store itself: written with this binding,
28
- * `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/…`).
29
47
  *
30
48
  * `means` are the key store's (`authType`, `grantType`, `serviceUrl`,
31
- * `sapClient`, the issuer or ACS); `client` is the key store's client, when the
32
- * destination has one. A destination that states no `jwt` / `saml` type or no
33
- * 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.
34
53
  */
35
54
  export declare function bindingOf(means: IConnectionConfig, client?: IAuthorizationConfig | null): SecretBinding;
36
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,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,oBAAoB,GAAG,IAAI,GAClC,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"}