@mcp-abap-adt/auth-broker 3.1.0 → 4.1.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 (39) hide show
  1. package/CHANGELOG.md +531 -0
  2. package/README.md +1062 -220
  3. package/dist/AuthBroker.d.ts +249 -30
  4. package/dist/AuthBroker.d.ts.map +1 -1
  5. package/dist/AuthBroker.js +681 -115
  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 +117 -0
  13. package/dist/binding.d.ts.map +1 -0
  14. package/dist/binding.js +223 -0
  15. package/dist/bindingOf.d.ts +37 -0
  16. package/dist/bindingOf.d.ts.map +1 -0
  17. package/dist/bindingOf.js +53 -0
  18. package/dist/clientAuthentication.d.ts +109 -0
  19. package/dist/clientAuthentication.d.ts.map +1 -0
  20. package/dist/clientAuthentication.js +218 -0
  21. package/dist/destinations.d.ts +164 -0
  22. package/dist/destinations.d.ts.map +1 -0
  23. package/dist/destinations.js +473 -0
  24. package/dist/index.d.ts +10 -4
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +14 -7
  27. package/dist/providers/ITokenProvider.d.ts +2 -1
  28. package/dist/providers/ITokenProvider.d.ts.map +1 -1
  29. package/dist/providers/index.d.ts +2 -2
  30. package/dist/providers/index.js +2 -2
  31. package/dist/stores/index.d.ts +2 -2
  32. package/dist/stores/index.js +2 -2
  33. package/dist/stores/interfaces.d.ts +4 -3
  34. package/dist/stores/interfaces.d.ts.map +1 -1
  35. package/dist/stores/interfaces.js +2 -2
  36. package/dist/types.d.ts +5 -2
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/types.js +3 -1
  39. package/package.json +16 -8
@@ -0,0 +1,27 @@
1
+ /**
2
+ * A destination that lacks what its type needs.
3
+ *
4
+ * Thrown by `getProvider` before any provider is asked: a missing field is a
5
+ * fault no renewal cures, so it surfaces where the consumer builds its
6
+ * connector, not at the first `connect()`.
7
+ *
8
+ * **It carries names, never values.** `missingFields` holds store field names
9
+ * (`authType`, `username`, `sncQop`) or broker option names
10
+ * (`serviceKeyStore`, `provider`); the message is fixed wording plus the
11
+ * destination's name and those names. A stored value — a password, a token,
12
+ * even an `authType` that is not one of the four — never reaches it. There is
13
+ * no `cause`: a provider's own error quotes the value it refused (`qop … got
14
+ * '7'`), so it is not carried; its field names are mapped instead.
15
+ */
16
+ export declare class DestinationConfigError extends Error {
17
+ readonly code: "DESTINATION_CONFIG";
18
+ readonly destination: string;
19
+ readonly missingFields: string[];
20
+ /**
21
+ * @param destination The destination's name, as the caller passed it.
22
+ * @param missingFields Field or option names only.
23
+ * @param reason Fixed wording: what is wrong, never a stored value.
24
+ */
25
+ constructor(destination: string, missingFields: string[], reason: string);
26
+ }
27
+ //# sourceMappingURL=DestinationConfigError.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DestinationConfigError.d.ts","sourceRoot":"","sources":["../src/DestinationConfigError.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C,QAAQ,CAAC,IAAI,EAAG,oBAAoB,CAAU;IAC9C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,EAAE,CAAC;IAEjC;;;;OAIG;gBACS,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM;CASzE"}
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DestinationConfigError = void 0;
4
+ /**
5
+ * A destination that lacks what its type needs.
6
+ *
7
+ * Thrown by `getProvider` before any provider is asked: a missing field is a
8
+ * fault no renewal cures, so it surfaces where the consumer builds its
9
+ * connector, not at the first `connect()`.
10
+ *
11
+ * **It carries names, never values.** `missingFields` holds store field names
12
+ * (`authType`, `username`, `sncQop`) or broker option names
13
+ * (`serviceKeyStore`, `provider`); the message is fixed wording plus the
14
+ * destination's name and those names. A stored value — a password, a token,
15
+ * even an `authType` that is not one of the four — never reaches it. There is
16
+ * no `cause`: a provider's own error quotes the value it refused (`qop … got
17
+ * '7'`), so it is not carried; its field names are mapped instead.
18
+ */
19
+ class DestinationConfigError extends Error {
20
+ code = 'DESTINATION_CONFIG';
21
+ destination;
22
+ missingFields;
23
+ /**
24
+ * @param destination The destination's name, as the caller passed it.
25
+ * @param missingFields Field or option names only.
26
+ * @param reason Fixed wording: what is wrong, never a stored value.
27
+ */
28
+ constructor(destination, missingFields, reason) {
29
+ super(`Destination "${destination}": ${reason} (${missingFields.join(', ')})`);
30
+ this.name = 'DestinationConfigError';
31
+ this.destination = destination;
32
+ this.missingFields = [...missingFields];
33
+ Object.setPrototypeOf(this, DestinationConfigError.prototype);
34
+ }
35
+ }
36
+ exports.DestinationConfigError = DestinationConfigError;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The broker's writes of a destination's session secret, retried until the
3
+ * store takes them.
4
+ *
5
+ * A store failure is the storage's or a broker bug, never the token's, so it
6
+ * does not fail the authentication: the result stays pending for its
7
+ * destination and the writer tries again on its own — it does not wait for the
8
+ * provider to be called again, since a request may be the process's last.
9
+ *
10
+ * - **One pending result per destination, the latest.** A newer result
11
+ * replaces the one waiting; an older one is never written after it.
12
+ * - **Attempts for one destination never overlap.** Each runs after the
13
+ * previous one has settled.
14
+ * - **A growing delay:** one second after the first failure, doubling, capped
15
+ * at one minute; reset once a write lands. The timer is `unref()`ed, so a
16
+ * pending write never keeps a process alive.
17
+ * - **Failures are logged by class name** — never a message: the store holds
18
+ * tokens, and its errors are foreign text.
19
+ * - **`flush()`** gives every pending result one more attempt and rejects,
20
+ * naming the destinations, if any is still not written.
21
+ */
22
+ import type { ILogger } from '@mcp-abap-adt/interfaces-utils';
23
+ /** A thrown value's class, for a log line: never its message. */
24
+ export declare function classLabel(error: unknown): string;
25
+ /** `T`: what one write takes — the result, with what the broker writes beside it. */
26
+ export declare class SessionWriter<T> {
27
+ private readonly write;
28
+ private readonly logger;
29
+ private readonly queues;
30
+ /**
31
+ * @param write Writes one result for a destination; throws when the store
32
+ * does not take it.
33
+ */
34
+ constructor(write: (destination: string, result: T) => Promise<void>, logger: ILogger);
35
+ /**
36
+ * Take a new result for the destination and try to write it now. Never
37
+ * throws: resolves once this attempt has settled, written or left pending.
38
+ */
39
+ submit(destination: string, result: T): Promise<void>;
40
+ /**
41
+ * One more attempt for every pending result; resolves when all are written,
42
+ * rejects naming the destinations still pending — each failure as its
43
+ * destination and its error's class only, never the store's message. The
44
+ * retries go on after a rejection.
45
+ */
46
+ flush(): Promise<void>;
47
+ private queueOf;
48
+ /** Runs after the destination's previous attempt; never rejects. */
49
+ private attempt;
50
+ private writePending;
51
+ private schedule;
52
+ private cancelTimer;
53
+ }
54
+ //# sourceMappingURL=SessionWriter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SessionWriter.d.ts","sourceRoot":"","sources":["../src/SessionWriter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAgB9D,iEAAiE;AACjE,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAKjD;AAED,qFAAqF;AACrF,qBAAa,aAAa,CAAC,CAAC;IAQxB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM;IARzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA+B;IAEtD;;;OAGG;gBAEgB,KAAK,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,EACxD,MAAM,EAAE,OAAO;IAGlC;;;OAGG;IACG,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAO3D;;;;;OAKG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAyB5B,OAAO,CAAC,OAAO;IASf,oEAAoE;IACpE,OAAO,CAAC,OAAO;YAMD,YAAY;IAgC1B,OAAO,CAAC,QAAQ;IAUhB,OAAO,CAAC,WAAW;CAMpB"}
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ /**
3
+ * The broker's writes of a destination's session secret, retried until the
4
+ * store takes them.
5
+ *
6
+ * A store failure is the storage's or a broker bug, never the token's, so it
7
+ * does not fail the authentication: the result stays pending for its
8
+ * destination and the writer tries again on its own — it does not wait for the
9
+ * provider to be called again, since a request may be the process's last.
10
+ *
11
+ * - **One pending result per destination, the latest.** A newer result
12
+ * replaces the one waiting; an older one is never written after it.
13
+ * - **Attempts for one destination never overlap.** Each runs after the
14
+ * previous one has settled.
15
+ * - **A growing delay:** one second after the first failure, doubling, capped
16
+ * at one minute; reset once a write lands. The timer is `unref()`ed, so a
17
+ * pending write never keeps a process alive.
18
+ * - **Failures are logged by class name** — never a message: the store holds
19
+ * tokens, and its errors are foreign text.
20
+ * - **`flush()`** gives every pending result one more attempt and rejects,
21
+ * naming the destinations, if any is still not written.
22
+ */
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.SessionWriter = void 0;
25
+ exports.classLabel = classLabel;
26
+ const FIRST_DELAY_MS = 1_000;
27
+ const MAX_DELAY_MS = 60_000;
28
+ /** A thrown value's class, for a log line: never its message. */
29
+ function classLabel(error) {
30
+ if (error instanceof Error) {
31
+ return error.constructor?.name || 'Error';
32
+ }
33
+ return typeof error;
34
+ }
35
+ /** `T`: what one write takes — the result, with what the broker writes beside it. */
36
+ class SessionWriter {
37
+ write;
38
+ logger;
39
+ queues = new Map();
40
+ /**
41
+ * @param write Writes one result for a destination; throws when the store
42
+ * does not take it.
43
+ */
44
+ constructor(write, logger) {
45
+ this.write = write;
46
+ this.logger = logger;
47
+ }
48
+ /**
49
+ * Take a new result for the destination and try to write it now. Never
50
+ * throws: resolves once this attempt has settled, written or left pending.
51
+ */
52
+ async submit(destination, result) {
53
+ const queue = this.queueOf(destination);
54
+ queue.pending = result;
55
+ this.cancelTimer(queue);
56
+ await this.attempt(destination, queue);
57
+ }
58
+ /**
59
+ * One more attempt for every pending result; resolves when all are written,
60
+ * rejects naming the destinations still pending — each failure as its
61
+ * destination and its error's class only, never the store's message. The
62
+ * retries go on after a rejection.
63
+ */
64
+ async flush() {
65
+ const queues = [...this.queues];
66
+ await Promise.all(queues.map(([destination, queue]) => {
67
+ if (queue.pending === undefined)
68
+ return queue.tail;
69
+ this.cancelTimer(queue);
70
+ return this.attempt(destination, queue);
71
+ }));
72
+ const failed = queues.filter(([, queue]) => queue.pending !== undefined);
73
+ if (failed.length > 0) {
74
+ // The stores' own errors are not carried: their messages, causes and
75
+ // properties are foreign text that may quote what was being written.
76
+ throw new AggregateError(failed.map(([destination, queue]) => new Error(`"${destination}": ${classLabel(queue.lastError)}`)), `Session writes still failing for ${failed
77
+ .map(([destination]) => `"${destination}"`)
78
+ .join(', ')}; the broker keeps retrying them`);
79
+ }
80
+ }
81
+ queueOf(destination) {
82
+ let queue = this.queues.get(destination);
83
+ if (!queue) {
84
+ queue = { tail: Promise.resolve(), failures: 0 };
85
+ this.queues.set(destination, queue);
86
+ }
87
+ return queue;
88
+ }
89
+ /** Runs after the destination's previous attempt; never rejects. */
90
+ attempt(destination, queue) {
91
+ const run = queue.tail.then(() => this.writePending(destination, queue));
92
+ queue.tail = run;
93
+ return run;
94
+ }
95
+ async writePending(destination, queue) {
96
+ const result = queue.pending;
97
+ if (result === undefined)
98
+ return;
99
+ try {
100
+ await this.write(destination, result);
101
+ }
102
+ catch (error) {
103
+ queue.failures += 1;
104
+ queue.lastError = error;
105
+ const delay = Math.min(FIRST_DELAY_MS * 2 ** (queue.failures - 1), MAX_DELAY_MS);
106
+ this.logger.warn(`[AuthBroker] Session write for ${destination} failed; the token stands, the write is retried in ${delay} ms`, { error: classLabel(error), attempt: queue.failures });
107
+ this.schedule(destination, queue, delay);
108
+ return;
109
+ }
110
+ queue.failures = 0;
111
+ queue.lastError = undefined;
112
+ // A newer result that arrived meanwhile stays pending: its own attempt is
113
+ // queued after this one.
114
+ if (queue.pending === result) {
115
+ queue.pending = undefined;
116
+ this.cancelTimer(queue);
117
+ }
118
+ }
119
+ schedule(destination, queue, delay) {
120
+ this.cancelTimer(queue);
121
+ const timer = setTimeout(() => {
122
+ queue.timer = undefined;
123
+ void this.attempt(destination, queue);
124
+ }, delay);
125
+ timer.unref();
126
+ queue.timer = timer;
127
+ }
128
+ cancelTimer(queue) {
129
+ if (queue.timer) {
130
+ clearTimeout(queue.timer);
131
+ queue.timer = undefined;
132
+ }
133
+ }
134
+ }
135
+ exports.SessionWriter = SessionWriter;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * A secret is bound to the resource it was obtained for and to who issued it.
3
+ *
4
+ * The session store keeps two strings beside the secret — `issuedFor`, the
5
+ * resource, and `issuedBy`, the issuer and its client — and the broker uses a
6
+ * stored secret only when both equal what this destination's means give. The
7
+ * store keeps them as given; the broker canonicalises **both sides** before it
8
+ * compares, with the one function here (the broker's only, decided), so a store
9
+ * need not canonicalise and a legacy file's composed URI compares equal.
10
+ *
11
+ * The canonical URI:
12
+ * - scheme and host lower-cased (by `URL`, for http and https);
13
+ * - the port explicit — `443` for `https`, `80` for `http` when the URL has
14
+ * none; another scheme keeps the port it states;
15
+ * - the path without a trailing `/` (the root path is empty), its case kept;
16
+ * - one query parameter at most — `sap-client=<n>` for a resource,
17
+ * `client_id=<id>` for an issuer, none for a SAML ACS — parsed and
18
+ * re-encoded with `URLSearchParams`, so a value percent-encoded on one side
19
+ * and plain on the other compares equal;
20
+ * - no user info, no other parameter, no fragment.
21
+ *
22
+ * A URL that does not parse, or has no host, has no canonical form: the
23
+ * binding is then absent, never guessed.
24
+ */
25
+ import type { IConfig, IConnectionConfig } from '@mcp-abap-adt/interfaces-auth-broker';
26
+ import type { IAuthorizationConfig } from '@mcp-abap-adt/interfaces-auth-sap';
27
+ /** What a binding reads of a client: who it is, never its secret. */
28
+ export type BoundClient = Pick<IAuthorizationConfig, 'uaaUrl' | 'uaaClientId'>;
29
+ /**
30
+ * The resource: `serviceUrl` with the SAP client as `sap-client` — the
31
+ * destination's `sapClient` when it states one, over a `sap-client` already in
32
+ * the URL. For a stored value, call it without `sapClient`.
33
+ */
34
+ export declare function resourceUri(serviceUrl: unknown, sapClient?: string): string | undefined;
35
+ /**
36
+ * The issuer and its client: the authorization server's base URL with
37
+ * `client_id`. For a stored value, call it without `clientId`.
38
+ */
39
+ export declare function issuerUri(issuerUrl: unknown, clientId?: string): string | undefined;
40
+ /** The ACS of the system that set SAML session cookies: origin and path. */
41
+ export declare function acsUri(acsUrl: unknown): string | undefined;
42
+ /** How a stored `issuedBy` is read: as an issuer with its client, or as an ACS. */
43
+ export type IssuerKind = 'issuer' | 'acs';
44
+ /** What this destination's means bind a secret to, canonical; absent where they cannot say. */
45
+ export interface Binding {
46
+ issuedFor?: string;
47
+ issuedBy?: string;
48
+ /** How the stored `issuedBy` is canonicalised before it is compared. */
49
+ issuerKind: IssuerKind;
50
+ /**
51
+ * The means state an issuer — for a `none` row, whether `issuedBy` is
52
+ * compared at all. True even when the stated URL does not parse: then it
53
+ * matches nothing.
54
+ */
55
+ issuerStated: boolean;
56
+ /**
57
+ * Set on the `clientAuthentication` strategy path only: a resource neither
58
+ * side states — no `issuedFor` computed (no service URL, or one that does
59
+ * not parse, as the CLI's XSUAA placeholder) and none stored — matches, so
60
+ * the issuer and client alone decide. A resource stated on one side only
61
+ * never matches. Without it (the 4.0.0 path) an absent resource matches
62
+ * nothing.
63
+ */
64
+ unstatedResourceMatches?: true;
65
+ }
66
+ /**
67
+ * The UAA grants (`authorization_code`, `client_credentials`, `passcode`) and
68
+ * `saml2_bearer`, whose token UAA issues to its client: the resource, and `uaaUrl` with `uaaClientId` — both required to match.
69
+ */
70
+ export declare function uaaBinding(means: IConnectionConfig, client: BoundClient | null): Binding;
71
+ /**
72
+ * The OIDC grants: the resource, and the issuer — `oidcIssuerUrl`, else
73
+ * `uaaUrl` — with `uaaClientId`; both required to match. Means that state
74
+ * neither issuer (explicit endpoints alone) bind no stored secret: one is then
75
+ * never seeded.
76
+ */
77
+ export declare function oidcBinding(means: IConnectionConfig, client: BoundClient | null): Binding;
78
+ /**
79
+ * `saml2_pure`: the resource, and the ACS of the system that sets the
80
+ * cookies — `samlAcsUrl`, origin and path; both required to match.
81
+ */
82
+ export declare function samlPureBinding(means: IConnectionConfig): Binding;
83
+ /**
84
+ * The token API with a consumer's provider: what the broker handed
85
+ * that provider — the `serviceUrl` it resolved, with the SAP client, and for a
86
+ * factory the client it resolved (`uaaUrl` with `uaaClientId`). An instance is
87
+ * handed no client, so no issuer is claimed for what it obtains: `issuedBy` is
88
+ * then absent, and `getProvider` never seeds from that secret.
89
+ */
90
+ export declare function consumerBinding(serviceUrl: string, sapClient: string | undefined, client: BoundClient | null): Binding;
91
+ /**
92
+ * A binding on the `clientAuthentication` strategy path: a resource that
93
+ * neither the means nor the stored session state matches, so the issuer and
94
+ * client alone decide (`Binding.unstatedResourceMatches`).
95
+ */
96
+ export declare function strategyBinding(binding: Binding): Binding;
97
+ /**
98
+ * A `none` row: the resource always; the issuer only when the means state one
99
+ * — `oidcIssuerUrl` (with `uaaClientId` when stated) or the client (`uaaUrl`
100
+ * and `uaaClientId`) for `jwt`, `samlAcsUrl` for `saml`.
101
+ */
102
+ export declare function handedOverBinding(authType: 'jwt' | 'saml', means: IConnectionConfig, client: BoundClient | null): Binding;
103
+ /**
104
+ * The stored `issuedFor`, canonicalised, equals the destination's — or, with
105
+ * `unstatedResourceMatches`, neither states one.
106
+ */
107
+ export declare function sameResource(stored: IConfig | null, binding: Binding): boolean;
108
+ /** The stored `issuedBy`, canonicalised as the destination's kind, equals it. */
109
+ export declare function sameIssuer(stored: IConfig | null, binding: Binding): boolean;
110
+ /**
111
+ * An obtained secret is this destination's only when **both** stored values
112
+ * equal the computed ones; either absent, on either side, is not a match —
113
+ * save a resource absent on both sides of a strategy-path binding
114
+ * (`unstatedResourceMatches`).
115
+ */
116
+ export declare function boundHere(stored: IConfig | null, binding: Binding): boolean;
117
+ //# sourceMappingURL=binding.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"binding.d.ts","sourceRoot":"","sources":["../src/binding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EACV,OAAO,EACP,iBAAiB,EAClB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAE9E,qEAAqE;AACrE,MAAM,MAAM,WAAW,GAAG,IAAI,CAAC,oBAAoB,EAAE,QAAQ,GAAG,aAAa,CAAC,CAAC;AA6C/E;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,UAAU,EAAE,OAAO,EACnB,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,GAAG,SAAS,CAEpB;AAED;;;GAGG;AACH,wBAAgB,SAAS,CACvB,SAAS,EAAE,OAAO,EAClB,QAAQ,CAAC,EAAE,MAAM,GAChB,MAAM,GAAG,SAAS,CAEpB;AAED,4EAA4E;AAC5E,wBAAgB,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAE1D;AAED,mFAAmF;AACnF,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,KAAK,CAAC;AAE1C,+FAA+F;AAC/F,MAAM,WAAW,OAAO;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,UAAU,EAAE,UAAU,CAAC;IACvB;;;;OAIG;IACH,YAAY,EAAE,OAAO,CAAC;IACtB;;;;;;;OAOG;IACH,uBAAuB,CAAC,EAAE,IAAI,CAAC;CAChC;AAUD;;;GAGG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,iBAAiB,EACxB,MAAM,EAAE,WAAW,GAAG,IAAI,GACzB,OAAO,CAUT;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,iBAAiB,EACxB,MAAM,EAAE,WAAW,GAAG,IAAI,GACzB,OAAO,CAWT;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAOjE;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,MAAM,EAAE,WAAW,GAAG,IAAI,GACzB,OAAO,CAET;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAEzD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,KAAK,GAAG,MAAM,EACxB,KAAK,EAAE,iBAAiB,EACxB,MAAM,EAAE,WAAW,GAAG,IAAI,GACzB,OAAO,CA4BT;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,OAAO,GAAG,IAAI,EACtB,OAAO,EAAE,OAAO,GACf,OAAO,CAOT;AAED,iFAAiF;AACjF,wBAAgB,UAAU,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAI5E;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAE3E"}
@@ -0,0 +1,223 @@
1
+ "use strict";
2
+ /**
3
+ * A secret is bound to the resource it was obtained for and to who issued it.
4
+ *
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.
11
+ *
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.
22
+ *
23
+ * A URL that does not parse, or has no host, has no canonical form: the
24
+ * binding is then absent, never guessed.
25
+ */
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.resourceUri = resourceUri;
28
+ exports.issuerUri = issuerUri;
29
+ exports.acsUri = acsUri;
30
+ exports.uaaBinding = uaaBinding;
31
+ exports.oidcBinding = oidcBinding;
32
+ exports.samlPureBinding = samlPureBinding;
33
+ exports.consumerBinding = consumerBinding;
34
+ exports.strategyBinding = strategyBinding;
35
+ exports.handedOverBinding = handedOverBinding;
36
+ exports.sameResource = sameResource;
37
+ exports.sameIssuer = sameIssuer;
38
+ exports.boundHere = boundHere;
39
+ /** The default port a URL without one is taken to mean. */
40
+ const DEFAULT_PORTS = {
41
+ 'https:': '443',
42
+ 'http:': '80',
43
+ };
44
+ function present(value) {
45
+ return typeof value === 'string' && value !== '';
46
+ }
47
+ /**
48
+ * The canonical URI of `raw`, keeping only the query parameter `param` — its
49
+ * value `override` when given, else the one `raw` carries.
50
+ */
51
+ function canonical(raw, param, override) {
52
+ if (!present(raw))
53
+ return undefined;
54
+ let url;
55
+ try {
56
+ url = new URL(raw);
57
+ }
58
+ catch {
59
+ return undefined;
60
+ }
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
+ const host = url.hostname;
64
+ if (host === '')
65
+ return undefined;
66
+ const scheme = url.protocol;
67
+ 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
+ }
76
+ return `${scheme}//${host}${port ? `:${port}` : ''}${path}${query}`;
77
+ }
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
+ /** The resource the means name: `serviceUrl` with `sapClient`. */
98
+ function resourceOf(means) {
99
+ return resourceUri(means.serviceUrl, present(means.sapClient) ? means.sapClient : undefined);
100
+ }
101
+ /**
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.
104
+ */
105
+ function uaaBinding(means, client) {
106
+ const stated = present(client?.uaaUrl) && present(client?.uaaClientId);
107
+ return {
108
+ issuedFor: resourceOf(means),
109
+ issuedBy: stated
110
+ ? issuerUri(client?.uaaUrl, client?.uaaClientId)
111
+ : undefined,
112
+ issuerKind: 'issuer',
113
+ issuerStated: stated,
114
+ };
115
+ }
116
+ /**
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.
121
+ */
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);
127
+ return {
128
+ issuedFor: resourceOf(means),
129
+ issuedBy: stated ? issuerUri(issuer, client?.uaaClientId) : undefined,
130
+ issuerKind: 'issuer',
131
+ issuerStated: stated,
132
+ };
133
+ }
134
+ /**
135
+ * `saml2_pure`: the resource, and the ACS of the system that sets the
136
+ * cookies — `samlAcsUrl`, origin and path; both required to match.
137
+ */
138
+ function samlPureBinding(means) {
139
+ return {
140
+ issuedFor: resourceOf(means),
141
+ issuedBy: acsUri(means.samlAcsUrl),
142
+ issuerKind: 'acs',
143
+ issuerStated: present(means.samlAcsUrl),
144
+ };
145
+ }
146
+ /**
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.
152
+ */
153
+ function consumerBinding(serviceUrl, sapClient, client) {
154
+ return uaaBinding({ serviceUrl, sapClient }, client);
155
+ }
156
+ /**
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`).
160
+ */
161
+ function strategyBinding(binding) {
162
+ return { ...binding, unstatedResourceMatches: true };
163
+ }
164
+ /**
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`.
168
+ */
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;
191
+ return {
192
+ issuedFor,
193
+ issuedBy: stated ? issuerUri(client?.uaaUrl, clientId) : undefined,
194
+ issuerKind: 'issuer',
195
+ issuerStated: stated,
196
+ };
197
+ }
198
+ /**
199
+ * The stored `issuedFor`, canonicalised, equals the destination's — or, with
200
+ * `unstatedResourceMatches`, neither states one.
201
+ */
202
+ function sameResource(stored, binding) {
203
+ if (binding.issuedFor === undefined) {
204
+ return (binding.unstatedResourceMatches === true && !present(stored?.issuedFor));
205
+ }
206
+ return resourceUri(stored?.issuedFor) === binding.issuedFor;
207
+ }
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;
214
+ }
215
+ /**
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`).
220
+ */
221
+ function boundHere(stored, binding) {
222
+ return sameResource(stored, binding) && sameIssuer(stored, binding);
223
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * What a destination's means bind its secret to: the one
3
+ * place the broker chooses which binding a destination has. `getProvider`
4
+ * builds each provider with it, and `persist` writes what it gives beside every
5
+ * secret; `bindingOf` hands the same to a consumer.
6
+ */
7
+ import type { DestinationGrant, IConnectionConfig } from '@mcp-abap-adt/interfaces-auth-broker';
8
+ import type { IAuthorizationConfig } from '@mcp-abap-adt/interfaces-auth-sap';
9
+ import type { BoundClient } from './binding';
10
+ import { type Binding } from './binding';
11
+ /**
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.
16
+ */
17
+ export declare function destinationBinding(authType: 'jwt' | 'saml', grant: DestinationGrant, means: IConnectionConfig, client: BoundClient | null): Binding;
18
+ /** The two strings a session store keeps beside a secret; absent where the means cannot say. */
19
+ export interface SecretBinding {
20
+ issuedFor?: string;
21
+ issuedBy?: string;
22
+ }
23
+ /**
24
+ * 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
+ *
31
+ * `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.
35
+ */
36
+ export declare function bindingOf(means: IConnectionConfig, client?: IAuthorizationConfig | null): SecretBinding;
37
+ //# sourceMappingURL=bindingOf.d.ts.map
@@ -0,0 +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"}