@mcp-abap-adt/auth-broker 3.0.4 → 4.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +448 -1
  2. package/README.md +723 -533
  3. package/dist/AuthBroker.d.ts +178 -29
  4. package/dist/AuthBroker.d.ts.map +1 -1
  5. package/dist/AuthBroker.js +494 -101
  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 +95 -0
  13. package/dist/binding.d.ts.map +1 -0
  14. package/dist/binding.js +207 -0
  15. package/dist/bindingOf.d.ts +36 -0
  16. package/dist/bindingOf.d.ts.map +1 -0
  17. package/dist/bindingOf.js +53 -0
  18. package/dist/destinations.d.ts +139 -0
  19. package/dist/destinations.d.ts.map +1 -0
  20. package/dist/destinations.js +429 -0
  21. package/dist/index.d.ts +7 -3
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +11 -7
  24. package/dist/providers/ITokenProvider.d.ts +2 -1
  25. package/dist/providers/ITokenProvider.d.ts.map +1 -1
  26. package/dist/providers/index.d.ts +2 -2
  27. package/dist/providers/index.js +2 -2
  28. package/dist/stores/index.d.ts +2 -2
  29. package/dist/stores/index.js +2 -2
  30. package/dist/stores/interfaces.d.ts +4 -3
  31. package/dist/stores/interfaces.d.ts.map +1 -1
  32. package/dist/stores/interfaces.js +2 -2
  33. package/dist/types.d.ts +5 -2
  34. package/dist/types.d.ts.map +1 -1
  35. package/dist/types.js +3 -1
  36. package/package.json +25 -45
  37. package/CONTRIBUTORS.md +0 -33
  38. package/dist/__tests__/helpers/configHelpers.d.ts +0 -59
  39. package/dist/__tests__/helpers/configHelpers.d.ts.map +0 -1
  40. package/dist/__tests__/helpers/configHelpers.js +0 -206
  41. package/dist/__tests__/helpers/netHelpers.d.ts +0 -3
  42. package/dist/__tests__/helpers/netHelpers.d.ts.map +0 -1
  43. package/dist/__tests__/helpers/netHelpers.js +0 -63
  44. package/dist/__tests__/helpers/testLogger.d.ts +0 -7
  45. package/dist/__tests__/helpers/testLogger.d.ts.map +0 -1
  46. package/dist/__tests__/helpers/testLogger.js +0 -54
  47. package/dist/bin/mcp-auth.js +0 -752
  48. package/dist/bin/mcp-sso.js +0 -825
  49. package/dist/bin/mcpSsoConfig.js +0 -566
  50. package/dist/bin/samlMetadata.js +0 -175
  51. package/dist/bin/workDir.js +0 -79
@@ -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,95 @@
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
+ /**
28
+ * The resource: `serviceUrl` with the SAP client as `sap-client` — the
29
+ * destination's `sapClient` when it states one, over a `sap-client` already in
30
+ * the URL. For a stored value, call it without `sapClient`.
31
+ */
32
+ export declare function resourceUri(serviceUrl: unknown, sapClient?: string): string | undefined;
33
+ /**
34
+ * The issuer and its client: the authorization server's base URL with
35
+ * `client_id`. For a stored value, call it without `clientId`.
36
+ */
37
+ export declare function issuerUri(issuerUrl: unknown, clientId?: string): string | undefined;
38
+ /** The ACS of the system that set SAML session cookies: origin and path. */
39
+ export declare function acsUri(acsUrl: unknown): string | undefined;
40
+ /** How a stored `issuedBy` is read: as an issuer with its client, or as an ACS. */
41
+ export type IssuerKind = 'issuer' | 'acs';
42
+ /** What this destination's means bind a secret to, canonical; absent where they cannot say. */
43
+ export interface Binding {
44
+ issuedFor?: string;
45
+ issuedBy?: string;
46
+ /** How the stored `issuedBy` is canonicalised before it is compared. */
47
+ issuerKind: IssuerKind;
48
+ /**
49
+ * The means state an issuer — for a `none` row, whether `issuedBy` is
50
+ * compared at all. True even when the stated URL does not parse: then it
51
+ * matches nothing.
52
+ */
53
+ issuerStated: boolean;
54
+ }
55
+ /**
56
+ * The UAA grants (`authorization_code`, `client_credentials`, `passcode`) and
57
+ * `saml2_bearer`, whose token UAA issues to its client: the resource, and `uaaUrl` with `uaaClientId` — both required to match.
58
+ */
59
+ export declare function uaaBinding(means: IConnectionConfig, client: IAuthorizationConfig | null): Binding;
60
+ /**
61
+ * The OIDC grants: the resource, and the issuer — `oidcIssuerUrl`, else
62
+ * `uaaUrl` — with `uaaClientId`; both required to match. Means that state
63
+ * neither issuer (explicit endpoints alone) bind no stored secret: one is then
64
+ * never seeded.
65
+ */
66
+ export declare function oidcBinding(means: IConnectionConfig, client: IAuthorizationConfig | null): Binding;
67
+ /**
68
+ * `saml2_pure`: the resource, and the ACS of the system that sets the
69
+ * cookies — `samlAcsUrl`, origin and path; both required to match.
70
+ */
71
+ export declare function samlPureBinding(means: IConnectionConfig): Binding;
72
+ /**
73
+ * The token API with a consumer's provider: what the broker handed
74
+ * that provider — the `serviceUrl` it resolved, with the SAP client, and for a
75
+ * factory the client it resolved (`uaaUrl` with `uaaClientId`). An instance is
76
+ * handed no client, so no issuer is claimed for what it obtains: `issuedBy` is
77
+ * then absent, and `getProvider` never seeds from that secret.
78
+ */
79
+ export declare function consumerBinding(serviceUrl: string, sapClient: string | undefined, client: IAuthorizationConfig | null): Binding;
80
+ /**
81
+ * A `none` row: the resource always; the issuer only when the means state one
82
+ * — `oidcIssuerUrl` (with `uaaClientId` when stated) or the client (`uaaUrl`
83
+ * and `uaaClientId`) for `jwt`, `samlAcsUrl` for `saml`.
84
+ */
85
+ export declare function handedOverBinding(authType: 'jwt' | 'saml', means: IConnectionConfig, client: IAuthorizationConfig | null): Binding;
86
+ /** The stored `issuedFor`, canonicalised, equals the destination's. */
87
+ export declare function sameResource(stored: IConfig | null, binding: Binding): boolean;
88
+ /** The stored `issuedBy`, canonicalised as the destination's kind, equals it. */
89
+ export declare function sameIssuer(stored: IConfig | null, binding: Binding): boolean;
90
+ /**
91
+ * An obtained secret is this destination's only when **both** stored values
92
+ * equal the computed ones; either absent, on either side, is not a match.
93
+ */
94
+ export declare function boundHere(stored: IConfig | null, binding: Binding): boolean;
95
+ //# 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;AA6C9E;;;;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;CACvB;AAUD;;;GAGG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,iBAAiB,EACxB,MAAM,EAAE,oBAAoB,GAAG,IAAI,GAClC,OAAO,CAUT;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,iBAAiB,EACxB,MAAM,EAAE,oBAAoB,GAAG,IAAI,GAClC,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,oBAAoB,GAAG,IAAI,GAClC,OAAO,CAET;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,KAAK,GAAG,MAAM,EACxB,KAAK,EAAE,iBAAiB,EACxB,MAAM,EAAE,oBAAoB,GAAG,IAAI,GAClC,OAAO,CA4BT;AAED,uEAAuE;AACvE,wBAAgB,YAAY,CAC1B,MAAM,EAAE,OAAO,GAAG,IAAI,EACtB,OAAO,EAAE,OAAO,GACf,OAAO,CAKT;AAED,iFAAiF;AACjF,wBAAgB,UAAU,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAI5E;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAE3E"}
@@ -0,0 +1,207 @@
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.handedOverBinding = handedOverBinding;
35
+ exports.sameResource = sameResource;
36
+ exports.sameIssuer = sameIssuer;
37
+ exports.boundHere = boundHere;
38
+ /** The default port a URL without one is taken to mean. */
39
+ const DEFAULT_PORTS = {
40
+ 'https:': '443',
41
+ 'http:': '80',
42
+ };
43
+ function present(value) {
44
+ return typeof value === 'string' && value !== '';
45
+ }
46
+ /**
47
+ * The canonical URI of `raw`, keeping only the query parameter `param` — its
48
+ * value `override` when given, else the one `raw` carries.
49
+ */
50
+ function canonical(raw, param, override) {
51
+ if (!present(raw))
52
+ return undefined;
53
+ let url;
54
+ try {
55
+ url = new URL(raw);
56
+ }
57
+ catch {
58
+ return undefined;
59
+ }
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
+ const host = url.hostname;
63
+ if (host === '')
64
+ return undefined;
65
+ const scheme = url.protocol;
66
+ 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
+ }
75
+ return `${scheme}//${host}${port ? `:${port}` : ''}${path}${query}`;
76
+ }
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
+ /** The resource the means name: `serviceUrl` with `sapClient`. */
97
+ function resourceOf(means) {
98
+ return resourceUri(means.serviceUrl, present(means.sapClient) ? means.sapClient : undefined);
99
+ }
100
+ /**
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.
103
+ */
104
+ function uaaBinding(means, client) {
105
+ const stated = present(client?.uaaUrl) && present(client?.uaaClientId);
106
+ return {
107
+ issuedFor: resourceOf(means),
108
+ issuedBy: stated
109
+ ? issuerUri(client?.uaaUrl, client?.uaaClientId)
110
+ : undefined,
111
+ issuerKind: 'issuer',
112
+ issuerStated: stated,
113
+ };
114
+ }
115
+ /**
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.
120
+ */
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);
126
+ return {
127
+ issuedFor: resourceOf(means),
128
+ issuedBy: stated ? issuerUri(issuer, client?.uaaClientId) : undefined,
129
+ issuerKind: 'issuer',
130
+ issuerStated: stated,
131
+ };
132
+ }
133
+ /**
134
+ * `saml2_pure`: the resource, and the ACS of the system that sets the
135
+ * cookies — `samlAcsUrl`, origin and path; both required to match.
136
+ */
137
+ function samlPureBinding(means) {
138
+ return {
139
+ issuedFor: resourceOf(means),
140
+ issuedBy: acsUri(means.samlAcsUrl),
141
+ issuerKind: 'acs',
142
+ issuerStated: present(means.samlAcsUrl),
143
+ };
144
+ }
145
+ /**
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.
151
+ */
152
+ function consumerBinding(serviceUrl, sapClient, client) {
153
+ return uaaBinding({ serviceUrl, sapClient }, client);
154
+ }
155
+ /**
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`.
159
+ */
160
+ 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,
179
+ };
180
+ }
181
+ const stated = present(client?.uaaUrl) && clientId !== undefined;
182
+ return {
183
+ issuedFor,
184
+ issuedBy: stated ? issuerUri(client?.uaaUrl, clientId) : undefined,
185
+ issuerKind: 'issuer',
186
+ issuerStated: stated,
187
+ };
188
+ }
189
+ /** The stored `issuedFor`, canonicalised, equals the destination's. */
190
+ function sameResource(stored, binding) {
191
+ return (binding.issuedFor !== undefined &&
192
+ resourceUri(stored?.issuedFor) === binding.issuedFor);
193
+ }
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;
200
+ }
201
+ /**
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.
204
+ */
205
+ function boundHere(stored, binding) {
206
+ return sameResource(stored, binding) && sameIssuer(stored, binding);
207
+ }
@@ -0,0 +1,36 @@
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 Binding } from './binding';
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.
15
+ */
16
+ export declare function destinationBinding(authType: 'jwt' | 'saml', grant: DestinationGrant, means: IConnectionConfig, client: IAuthorizationConfig | null): Binding;
17
+ /** The two strings a session store keeps beside a secret; absent where the means cannot say. */
18
+ export interface SecretBinding {
19
+ issuedFor?: string;
20
+ issuedBy?: string;
21
+ }
22
+ /**
23
+ * 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.
29
+ *
30
+ * `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.
34
+ */
35
+ export declare function bindingOf(means: IConnectionConfig, client?: IAuthorizationConfig | null): SecretBinding;
36
+ //# 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,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"}
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ /**
3
+ * What a destination's means bind its secret to: the one
4
+ * place the broker chooses which binding a destination has. `getProvider`
5
+ * builds each provider with it, and `persist` writes what it gives beside every
6
+ * secret; `bindingOf` hands the same to a consumer.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.destinationBinding = destinationBinding;
10
+ exports.bindingOf = bindingOf;
11
+ const binding_1 = require("./binding");
12
+ const destinations_1 = require("./destinations");
13
+ /**
14
+ * The binding of a `jwt` or `saml` destination with the grant it states: the
15
+ * UAA grants and `saml2_bearer` by `uaaUrl` and the client, the OIDC grants by
16
+ * the issuer and the client, `saml2_pure` by its ACS, and `none` by what the
17
+ * means state.
18
+ */
19
+ function destinationBinding(authType, grant, means, client) {
20
+ if (grant === 'none')
21
+ return (0, binding_1.handedOverBinding)(authType, means, client);
22
+ if ((0, destinations_1.isOidcGrant)(grant))
23
+ return (0, binding_1.oidcBinding)(means, client);
24
+ if (grant === 'saml2_pure')
25
+ return (0, binding_1.samlPureBinding)(means);
26
+ return (0, binding_1.uaaBinding)(means, client);
27
+ }
28
+ /**
29
+ * The binding the broker writes beside a secret for a destination with these
30
+ * means and this client — canonical, exactly what `getProvider` compares a
31
+ * stored secret against. For a consumer that hands over a credential the
32
+ * broker cannot obtain (a `none` destination: a token or cookies it was
33
+ * given) and writes it to the session store itself: written with this binding,
34
+ * `getProvider` presents it; written with any other, it refuses.
35
+ *
36
+ * `means` are the key store's (`authType`, `grantType`, `serviceUrl`,
37
+ * `sapClient`, the issuer or ACS); `client` is the key store's client, when the
38
+ * destination has one. A destination that states no `jwt` / `saml` type or no
39
+ * grant binds nothing: `{}`. Never logs, never throws.
40
+ */
41
+ function bindingOf(means, client = null) {
42
+ const authType = means.authType;
43
+ const grant = means.grantType;
44
+ if ((authType !== 'jwt' && authType !== 'saml') || !grant)
45
+ return {};
46
+ const { issuedFor, issuedBy } = destinationBinding(authType, grant, means, client);
47
+ const binding = {};
48
+ if (issuedFor !== undefined)
49
+ binding.issuedFor = issuedFor;
50
+ if (issuedBy !== undefined)
51
+ binding.issuedBy = issuedBy;
52
+ return binding;
53
+ }