@mcp-abap-adt/connection 3.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.
Files changed (73) hide show
  1. package/CHANGELOG.md +337 -1
  2. package/README.md +74 -31
  3. package/dist/auth/IAuthProvider.d.ts +84 -0
  4. package/dist/auth/IAuthProvider.d.ts.map +1 -0
  5. package/dist/auth/IAuthProvider.js +21 -0
  6. package/dist/auth/providers.d.ts +95 -0
  7. package/dist/auth/providers.d.ts.map +1 -0
  8. package/dist/auth/providers.js +140 -0
  9. package/dist/connection/AbstractAbapConnection.d.ts +222 -17
  10. package/dist/connection/AbstractAbapConnection.d.ts.map +1 -1
  11. package/dist/connection/AbstractAbapConnection.js +555 -31
  12. package/dist/connection/AdtCloudConnector.d.ts +29 -0
  13. package/dist/connection/AdtCloudConnector.d.ts.map +1 -0
  14. package/dist/connection/AdtCloudConnector.js +31 -0
  15. package/dist/connection/AdtOnPremConnector.d.ts +30 -0
  16. package/dist/connection/AdtOnPremConnector.d.ts.map +1 -0
  17. package/dist/connection/AdtOnPremConnector.js +32 -0
  18. package/dist/connection/BaseAbapConnection.d.ts +7 -1
  19. package/dist/connection/BaseAbapConnection.d.ts.map +1 -1
  20. package/dist/connection/BaseAbapConnection.js +8 -2
  21. package/dist/connection/CertificateAbapConnection.d.ts +11 -1
  22. package/dist/connection/CertificateAbapConnection.d.ts.map +1 -1
  23. package/dist/connection/CertificateAbapConnection.js +14 -2
  24. package/dist/connection/CredentialAbapConnection.d.ts +88 -0
  25. package/dist/connection/CredentialAbapConnection.d.ts.map +1 -0
  26. package/dist/connection/CredentialAbapConnection.js +195 -0
  27. package/dist/connection/JwtAbapConnection.d.ts +102 -9
  28. package/dist/connection/JwtAbapConnection.d.ts.map +1 -1
  29. package/dist/connection/JwtAbapConnection.js +266 -82
  30. package/dist/connection/KerberosAbapConnection.d.ts +9 -1
  31. package/dist/connection/KerberosAbapConnection.d.ts.map +1 -1
  32. package/dist/connection/KerberosAbapConnection.js +9 -1
  33. package/dist/connection/RfcAbapConnection.d.ts +0 -5
  34. package/dist/connection/RfcAbapConnection.d.ts.map +1 -1
  35. package/dist/connection/RfcAbapConnection.js +0 -7
  36. package/dist/connection/SamlAbapConnection.d.ts +7 -1
  37. package/dist/connection/SamlAbapConnection.d.ts.map +1 -1
  38. package/dist/connection/SamlAbapConnection.js +8 -2
  39. package/dist/connection/connectionFactory.d.ts +16 -0
  40. package/dist/connection/connectionFactory.d.ts.map +1 -1
  41. package/dist/connection/connectionFactory.js +52 -0
  42. package/dist/index.d.ts +4 -0
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +10 -1
  45. package/dist/session/CloudSecuritySessionStrategy.d.ts +32 -0
  46. package/dist/session/CloudSecuritySessionStrategy.d.ts.map +1 -0
  47. package/dist/session/CloudSecuritySessionStrategy.js +135 -0
  48. package/dist/session/IcfSessionStrategy.d.ts +27 -0
  49. package/dist/session/IcfSessionStrategy.d.ts.map +1 -0
  50. package/dist/session/IcfSessionStrategy.js +62 -0
  51. package/dist/session/SessionLifecycle.d.ts +17 -2
  52. package/dist/session/SessionLifecycle.d.ts.map +1 -1
  53. package/dist/session/SessionLifecycle.js +17 -2
  54. package/dist/session/SessionStrategy.d.ts +86 -0
  55. package/dist/session/SessionStrategy.d.ts.map +1 -0
  56. package/dist/session/SessionStrategy.js +33 -0
  57. package/dist/utils/cookies.d.ts +12 -0
  58. package/dist/utils/cookies.d.ts.map +1 -0
  59. package/dist/utils/cookies.js +24 -0
  60. package/dist/utils/timeouts.d.ts +16 -0
  61. package/dist/utils/timeouts.d.ts.map +1 -1
  62. package/dist/utils/timeouts.js +19 -0
  63. package/docs/INDEX.md +5 -0
  64. package/docs/INSTALLATION.md +6 -2
  65. package/docs/MIGRATION-2.0.md +1 -1
  66. package/docs/MIGRATION-4.0.md +95 -0
  67. package/docs/MIGRATION-5.0.md +116 -0
  68. package/docs/STATEFUL_SESSION_GUIDE.md +83 -11
  69. package/docs/USAGE.md +115 -24
  70. package/docs/superpowers/specs/2026-08-21-platform-connectors.md +108 -0
  71. package/examples/README.md +2 -1
  72. package/examples/jwt-with-token-refresh.js +11 -3
  73. package/package.json +1 -1
@@ -1,38 +1,131 @@
1
- import type { IAdtResponse, ITokenRefresher } from '@mcp-abap-adt/interfaces';
1
+ import { type IAdtResponse, type ITokenRefresher } from '@mcp-abap-adt/interfaces';
2
2
  import type { SapConfig } from '../config/sapConfig.js';
3
3
  import type { ILogger } from '../logger.js';
4
+ import type { SessionStrategy } from '../session/SessionStrategy.js';
4
5
  import type { AbapRequestOptions } from './AbapConnection.js';
5
6
  import { AbstractAbapConnection } from './AbstractAbapConnection.js';
6
7
  /**
7
- * JWT Authentication connection for SAP BTP Cloud systems
8
+ * The class you take should say which SYSTEM you are dialling, not which
9
+ * credential you hold — a session mechanism that rides along with the
10
+ * credential gets one of them wrong. `AdtCloudConnector` and
11
+ * `AdtOnPremConnector` are how that is said.
8
12
  *
9
- * Supports automatic token refresh via ITokenRefresher injection:
10
- * - If tokenRefresher is provided, 401/403 errors trigger automatic token refresh
11
- * - If tokenRefresher is not provided, 401/403 errors throw an error (legacy behavior)
13
+ * **This class is NOT deprecated yet, and must not be replaced by
14
+ * `AdtCloudConnector` + `TokenAuthProvider`.** What it does beyond
15
+ * authenticating has not moved: a 401 caught mid-operation, one refresh shared
16
+ * by every operation in flight, the session rebuilt behind it, generation
17
+ * fencing so a dead session's response cannot land, and the request retried
18
+ * once. A `TokenAuthProvider` fetches a token at establishment and nothing
19
+ * after it, so swapping today trades all of that for a connection that dies on
20
+ * the first expiry.
21
+ *
22
+ * Use this class for JWT until that machinery moves; the split is the
23
+ * platform-connectors spec's next step.
12
24
  */
13
25
  export declare class JwtAbapConnection extends AbstractAbapConnection {
14
26
  private tokenRefresher?;
15
27
  private currentToken;
28
+ /** Bumped by any token refresh, token-only ones included. */
29
+ private tokenGeneration;
30
+ /** Single-flight over the token fetch alone. Touches no session state. */
31
+ private tokenRefreshInFlight?;
32
+ /**
33
+ * The `tokenGeneration` a completed session recovery was built for. Only
34
+ * `performRenewal` moves it, and only after `recoverSession` resolves.
35
+ *
36
+ * Separate from `tokenGeneration` because a token-only refresh — which
37
+ * `fetchCsrfToken` does, from inside an establishment — bumps that counter
38
+ * without rebuilding anything. Reading it as "the session is settled" made
39
+ * the outer handler retry a request whose session was never rebuilt.
40
+ */
41
+ private recoveredGeneration;
42
+ /**
43
+ * Single-flight over the whole credential renewal, session included.
44
+ *
45
+ * Sharing only the token fetch leaves the expensive half racing: `recover`
46
+ * and `cleanup` never join (`SessionLifecycle.transition`), so two concurrent
47
+ * renewals queue as cleanup A → recover A → cleanup B → recover B, and A's
48
+ * retry meets a session B has just torn down.
49
+ */
50
+ private renewalInFlight?;
51
+ /**
52
+ * Per connection, deliberately NOT static. `baseline` is compared against
53
+ * `this.tokenGeneration`, which is instance state — a store shared between
54
+ * instances would let one connection's operation hand its baseline to
55
+ * another's, and the comparison would be between unrelated counters.
56
+ */
57
+ private readonly recoveryScope;
16
58
  constructor(config: SapConfig, logger?: ILogger | null, sessionId?: string, tokenRefresher?: ITokenRefresher);
17
59
  protected buildAuthorizationHeader(): string;
18
60
  /**
19
- * Refresh the JWT token using the injected tokenRefresher
20
- * @returns true if token was refreshed, false if no refresher available
61
+ * For the public entry point: this call is its own operation, always.
62
+ *
63
+ * A re-entrant `makeAdtRequest` — from a logger or a refresher callback the
64
+ * connection itself invokes while a scope is live — is a new caller-visible
65
+ * operation. Inheriting there would hand it a baseline from somebody else's
66
+ * refresh, which reads as "already refreshed for me" and skips a refresh it
67
+ * needs.
68
+ */
69
+ private inNewRecoveryScope;
70
+ /** For the inner levels: join the operation in progress, or start one. */
71
+ private inRecoveryScope;
72
+ /**
73
+ * The baseline this operation is reasoning from.
74
+ *
75
+ * The `active` check here and the one in `inRecoveryScope` OVERLAP: each
76
+ * compensates for the other, and the stale-context test only fails when both
77
+ * are removed. Kept as two because they answer different questions — "may I
78
+ * join this scope" and "may I trust this baseline" — and a reader who finds
79
+ * one redundant would be deleting half a guarantee.
21
80
  */
22
- private tryRefreshToken;
81
+ private currentBaseline;
82
+ /**
83
+ * Fetch a new token, unless somebody already did for this operation.
84
+ *
85
+ * Single-flighted so two concurrent handlers — including two nested
86
+ * `fetchCsrfToken` calls, which is a level `renewalInFlight` cannot reach —
87
+ * share one network call instead of racing and leaving `currentToken` as
88
+ * whichever settled last.
89
+ *
90
+ * @returns true when the caller may retry.
91
+ */
92
+ private refreshTokenOnce;
93
+ private performTokenRefresh;
94
+ /**
95
+ * Renew the credential and the session it belongs to, once, shared.
96
+ */
97
+ private renewCredential;
98
+ private performRenewal;
99
+ /**
100
+ * May this operation retry?
101
+ *
102
+ * The order of the checks is the design, not style.
103
+ */
104
+ private ensureRecovered;
23
105
  /**
24
106
  * Establishes the session for this auth type. Called by
25
107
  * AbstractAbapConnection.connect(), which owns the lifecycle around it.
26
108
  */
109
+ /**
110
+ * Cloud manages sessions through ADT: a session resource is opened at
111
+ * `/sap/bc/adt/core/http/sessions` and given back by `DELETE` on the address
112
+ * the server publishes. This is the one connection that does it — on-prem
113
+ * keeps the platform's ICF logoff, which is what it has always used.
114
+ */
115
+ protected createSessionStrategy(): SessionStrategy;
27
116
  protected establishSession(): Promise<void>;
28
117
  /**
29
118
  * Override makeAdtRequest to handle JWT auth errors with automatic token refresh
30
119
  */
31
120
  makeAdtRequest<T = any, D = any>(options: AbapRequestOptions): Promise<IAdtResponse<T, D>>;
121
+ private attemptRequest;
32
122
  /**
33
123
  * Override fetchCsrfToken to handle JWT auth errors with automatic token refresh
34
124
  */
35
- protected fetchCsrfToken(url: string, retryCount?: number, retryDelay?: number): Promise<string>;
125
+ protected fetchCsrfToken(url: string, retryCount?: number, retryDelay?: number,
126
+ /** Fences the response effects; omitted during connect(), which has no lease. */
127
+ generation?: number): Promise<string>;
128
+ private attemptCsrfToken;
36
129
  private static validateConfig;
37
130
  }
38
131
  //# sourceMappingURL=JwtAbapConnection.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"JwtAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/JwtAbapConnection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAE9E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAErE;;;;;;GAMG;AACH,qBAAa,iBAAkB,SAAQ,sBAAsB;IAC3D,OAAO,CAAC,cAAc,CAAC,CAAkB;IACzC,OAAO,CAAC,YAAY,CAAS;gBAG3B,MAAM,EAAE,SAAS,EACjB,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,EACvB,SAAS,CAAC,EAAE,MAAM,EAClB,cAAc,CAAC,EAAE,eAAe;IAWlC,SAAS,CAAC,wBAAwB,IAAI,MAAM;IAW5C;;;OAGG;YACW,eAAe;IA0B7B;;;OAGG;cACa,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IA2DjD;;OAEG;IACG,cAAc,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC,GAAG,GAAG,EACnC,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAiE9B;;OAEG;cACa,cAAc,CAC5B,GAAG,EAAE,MAAM,EACX,UAAU,SAAI,EACd,UAAU,SAAO,GAChB,OAAO,CAAC,MAAM,CAAC;IA2ClB,OAAO,CAAC,MAAM,CAAC,cAAc;CAa9B"}
1
+ {"version":3,"file":"JwtAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/JwtAbapConnection.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,eAAe,EACrB,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAG5C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAuBrE;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,iBAAkB,SAAQ,sBAAsB;IAC3D,OAAO,CAAC,cAAc,CAAC,CAAkB;IACzC,OAAO,CAAC,YAAY,CAAS;IAE7B,6DAA6D;IAC7D,OAAO,CAAC,eAAe,CAAK;IAE5B,0EAA0E;IAC1E,OAAO,CAAC,oBAAoB,CAAC,CAAmB;IAEhD;;;;;;;;OAQG;IACH,OAAO,CAAC,mBAAmB,CAAK;IAEhC;;;;;;;OAOG;IACH,OAAO,CAAC,eAAe,CAAC,CAAmB;IAE3C;;;;;OAKG;IACH,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA2C;gBAGvE,MAAM,EAAE,SAAS,EACjB,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,EACvB,SAAS,CAAC,EAAE,MAAM,EAClB,cAAc,CAAC,EAAE,eAAe;IAWlC,SAAS,CAAC,wBAAwB,IAAI,MAAM;IAW5C;;;;;;;;OAQG;IACH,OAAO,CAAC,kBAAkB;IAc1B,0EAA0E;IAC1E,OAAO,CAAC,eAAe;IASvB;;;;;;;;OAQG;IACH,OAAO,CAAC,eAAe;IAQvB;;;;;;;;;OASG;IACH,OAAO,CAAC,gBAAgB;YAsBV,mBAAmB;IAoBjC;;OAEG;IACH,OAAO,CAAC,eAAe;YAiBT,cAAc;IAwB5B;;;;OAIG;YACW,eAAe;IAyB7B;;;OAGG;IACH;;;;;OAKG;cACgB,qBAAqB,IAAI,eAAe;cAI3C,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IA4BjD;;OAEG;IACG,cAAc,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC,GAAG,GAAG,EACnC,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;YAKhB,cAAc;IA8D5B;;OAEG;cACa,cAAc,CAC5B,GAAG,EAAE,MAAM,EACX,UAAU,GAAE,MAAgC,EAC5C,UAAU,GAAE,MAAgC;IAC5C,iFAAiF;IACjF,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,CAAC;YASJ,gBAAgB;IAkD9B,OAAO,CAAC,MAAM,CAAC,cAAc;CAa9B"}
@@ -1,18 +1,75 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.JwtAbapConnection = void 0;
4
+ const node_async_hooks_1 = require("node:async_hooks");
5
+ const interfaces_1 = require("@mcp-abap-adt/interfaces");
4
6
  const axios_1 = require("axios");
7
+ const CloudSecuritySessionStrategy_js_1 = require("../session/CloudSecuritySessionStrategy.js");
8
+ const SessionLifecycle_js_1 = require("../session/SessionLifecycle.js");
5
9
  const AbstractAbapConnection_js_1 = require("./AbstractAbapConnection.js");
10
+ const csrfConfig_js_1 = require("./csrfConfig.js");
6
11
  /**
7
- * JWT Authentication connection for SAP BTP Cloud systems
12
+ * Is this worth refreshing a token for?
8
13
  *
9
- * Supports automatic token refresh via ITokenRefresher injection:
10
- * - If tokenRefresher is provided, 401/403 errors trigger automatic token refresh
11
- * - If tokenRefresher is not provided, 401/403 errors throw an error (legacy behavior)
14
+ * 401 only. A 403 means the server authenticated the caller and refused the
15
+ * action anyway — a new token is the same caller, so refreshing answers a
16
+ * question nobody asked and, worse, used to end with the original error
17
+ * replaced by "JWT token has expired". See issue #30.
18
+ */
19
+ function isTokenExpiryCandidate(error) {
20
+ return error instanceof axios_1.AxiosError && error.response?.status === 401;
21
+ }
22
+ /**
23
+ * The class you take should say which SYSTEM you are dialling, not which
24
+ * credential you hold — a session mechanism that rides along with the
25
+ * credential gets one of them wrong. `AdtCloudConnector` and
26
+ * `AdtOnPremConnector` are how that is said.
27
+ *
28
+ * **This class is NOT deprecated yet, and must not be replaced by
29
+ * `AdtCloudConnector` + `TokenAuthProvider`.** What it does beyond
30
+ * authenticating has not moved: a 401 caught mid-operation, one refresh shared
31
+ * by every operation in flight, the session rebuilt behind it, generation
32
+ * fencing so a dead session's response cannot land, and the request retried
33
+ * once. A `TokenAuthProvider` fetches a token at establishment and nothing
34
+ * after it, so swapping today trades all of that for a connection that dies on
35
+ * the first expiry.
36
+ *
37
+ * Use this class for JWT until that machinery moves; the split is the
38
+ * platform-connectors spec's next step.
12
39
  */
13
40
  class JwtAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnection {
14
41
  tokenRefresher;
15
42
  currentToken;
43
+ /** Bumped by any token refresh, token-only ones included. */
44
+ tokenGeneration = 0;
45
+ /** Single-flight over the token fetch alone. Touches no session state. */
46
+ tokenRefreshInFlight;
47
+ /**
48
+ * The `tokenGeneration` a completed session recovery was built for. Only
49
+ * `performRenewal` moves it, and only after `recoverSession` resolves.
50
+ *
51
+ * Separate from `tokenGeneration` because a token-only refresh — which
52
+ * `fetchCsrfToken` does, from inside an establishment — bumps that counter
53
+ * without rebuilding anything. Reading it as "the session is settled" made
54
+ * the outer handler retry a request whose session was never rebuilt.
55
+ */
56
+ recoveredGeneration = 0;
57
+ /**
58
+ * Single-flight over the whole credential renewal, session included.
59
+ *
60
+ * Sharing only the token fetch leaves the expensive half racing: `recover`
61
+ * and `cleanup` never join (`SessionLifecycle.transition`), so two concurrent
62
+ * renewals queue as cleanup A → recover A → cleanup B → recover B, and A's
63
+ * retry meets a session B has just torn down.
64
+ */
65
+ renewalInFlight;
66
+ /**
67
+ * Per connection, deliberately NOT static. `baseline` is compared against
68
+ * `this.tokenGeneration`, which is instance state — a store shared between
69
+ * instances would let one connection's operation hand its baseline to
70
+ * another's, and the comparison would be between unrelated counters.
71
+ */
72
+ recoveryScope = new node_async_hooks_1.AsyncLocalStorage();
16
73
  constructor(config, logger, sessionId, tokenRefresher) {
17
74
  JwtAbapConnection.validateConfig(config);
18
75
  super(config, logger || null, sessionId);
@@ -31,18 +88,90 @@ class JwtAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnecti
31
88
  return `Bearer ${this.currentToken}`;
32
89
  }
33
90
  /**
34
- * Refresh the JWT token using the injected tokenRefresher
35
- * @returns true if token was refreshed, false if no refresher available
91
+ * For the public entry point: this call is its own operation, always.
92
+ *
93
+ * A re-entrant `makeAdtRequest` — from a logger or a refresher callback the
94
+ * connection itself invokes while a scope is live — is a new caller-visible
95
+ * operation. Inheriting there would hand it a baseline from somebody else's
96
+ * refresh, which reads as "already refreshed for me" and skips a refresh it
97
+ * needs.
36
98
  */
37
- async tryRefreshToken() {
99
+ inNewRecoveryScope(fn) {
100
+ const scope = {
101
+ baseline: this.tokenGeneration,
102
+ active: true,
103
+ };
104
+ return this.recoveryScope.run(scope, async () => {
105
+ try {
106
+ return await fn();
107
+ }
108
+ finally {
109
+ scope.active = false;
110
+ }
111
+ });
112
+ }
113
+ /** For the inner levels: join the operation in progress, or start one. */
114
+ inRecoveryScope(fn) {
115
+ const inherited = this.recoveryScope.getStore();
116
+ // Only a scope that is still running. A store reached through an async
117
+ // resource that outlived its operation is stale, and its baseline describes
118
+ // a credential state that has since moved.
119
+ if (inherited?.active)
120
+ return fn();
121
+ return this.inNewRecoveryScope(fn);
122
+ }
123
+ /**
124
+ * The baseline this operation is reasoning from.
125
+ *
126
+ * The `active` check here and the one in `inRecoveryScope` OVERLAP: each
127
+ * compensates for the other, and the stale-context test only fails when both
128
+ * are removed. Kept as two because they answer different questions — "may I
129
+ * join this scope" and "may I trust this baseline" — and a reader who finds
130
+ * one redundant would be deleting half a guarantee.
131
+ */
132
+ currentBaseline() {
133
+ const scope = this.recoveryScope.getStore();
134
+ // No live scope means a caller reached a handler by a path that does not
135
+ // open one, or through a stale async context. Either way, treat it as its
136
+ // own operation rather than trusting a baseline nobody is standing behind.
137
+ return scope?.active ? scope.baseline : this.tokenGeneration;
138
+ }
139
+ /**
140
+ * Fetch a new token, unless somebody already did for this operation.
141
+ *
142
+ * Single-flighted so two concurrent handlers — including two nested
143
+ * `fetchCsrfToken` calls, which is a level `renewalInFlight` cannot reach —
144
+ * share one network call instead of racing and leaving `currentToken` as
145
+ * whichever settled last.
146
+ *
147
+ * @returns true when the caller may retry.
148
+ */
149
+ refreshTokenOnce(baseline) {
150
+ if (this.tokenGeneration > baseline)
151
+ return Promise.resolve(true);
152
+ if (this.tokenRefreshInFlight)
153
+ return this.tokenRefreshInFlight;
38
154
  if (!this.tokenRefresher) {
39
155
  this.logger?.debug(`[DEBUG] JwtAbapConnection - No tokenRefresher available, cannot refresh token`);
40
- return false;
156
+ return Promise.resolve(false);
41
157
  }
158
+ // Identity-checked clear, as SessionLifecycle.transition does with its
159
+ // tail: a joiner settling late must not clear a fetch somebody started
160
+ // after it.
161
+ const inFlight = this.performTokenRefresh().finally(() => {
162
+ if (this.tokenRefreshInFlight === inFlight) {
163
+ this.tokenRefreshInFlight = undefined;
164
+ }
165
+ });
166
+ this.tokenRefreshInFlight = inFlight;
167
+ return inFlight;
168
+ }
169
+ async performTokenRefresh() {
42
170
  try {
43
171
  this.logger?.debug(`[DEBUG] JwtAbapConnection - Refreshing token via tokenRefresher...`);
44
- const newToken = await this.tokenRefresher.refreshToken();
45
- this.currentToken = newToken;
172
+ // biome-ignore lint/style/noNonNullAssertion: refreshTokenOnce checks it
173
+ this.currentToken = await this.tokenRefresher.refreshToken();
174
+ this.tokenGeneration += 1;
46
175
  this.logger?.debug(`[DEBUG] JwtAbapConnection - Token refreshed successfully`);
47
176
  return true;
48
177
  }
@@ -51,17 +180,88 @@ class JwtAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnecti
51
180
  return false;
52
181
  }
53
182
  }
183
+ /**
184
+ * Renew the credential and the session it belongs to, once, shared.
185
+ */
186
+ renewCredential(baselineEpoch, baseline) {
187
+ if (this.renewalInFlight)
188
+ return this.renewalInFlight;
189
+ // Identity-checked clear, as SessionLifecycle.transition does with its
190
+ // tail: a joiner settling late must not clear a renewal somebody started
191
+ // after it.
192
+ const inFlight = this.performRenewal(baselineEpoch, baseline).finally(() => {
193
+ if (this.renewalInFlight === inFlight)
194
+ this.renewalInFlight = undefined;
195
+ });
196
+ this.renewalInFlight = inFlight;
197
+ return inFlight;
198
+ }
199
+ async performRenewal(baselineEpoch, baseline) {
200
+ // Shared with the token-only path, and a no-op when the token is already
201
+ // newer than the one that failed — then what is missing is the session, and
202
+ // a second fetch answers a question nobody asked.
203
+ if (!(await this.refreshTokenOnce(baseline)))
204
+ return false;
205
+ this.logger?.debug(`[DEBUG] JwtAbapConnection - Recovering session after token refresh...`);
206
+ // The renewed credential cannot keep the old ABAP session, so this is a
207
+ // session-lost teardown — internal, or it would cancel the very recovery it
208
+ // is setting up. disconnect() would be the caller-origin one.
209
+ this.discardSession();
210
+ // Re-establish before retrying: the retry goes through admission, and a
211
+ // discarded session admits nothing.
212
+ await this.recoverSession(baselineEpoch);
213
+ // Only here: a session now exists that was built with this token.
214
+ this.recoveredGeneration = this.tokenGeneration;
215
+ return true;
216
+ }
217
+ /**
218
+ * May this operation retry?
219
+ *
220
+ * The order of the checks is the design, not style.
221
+ */
222
+ async ensureRecovered(baselineEpoch) {
223
+ // 1. A renewal in flight is joined REGARDLESS of generation. A newer token
224
+ // is no use while the session it belongs to is still being rebuilt:
225
+ // retrying now is how a caller meets a closed admission door.
226
+ //
227
+ // This overlaps check 2 as the counters stand — `recoveredGeneration`
228
+ // only moves after `recoverSession` resolves, so check 2 cannot report a
229
+ // rebuild that has not finished, and removing either guard alone leaves
230
+ // the concurrency test green. Both are kept on purpose: this one states
231
+ // the rule ("never decide anything while a renewal is running") without
232
+ // depending on when some other counter happens to move, and it is what
233
+ // keeps the invariant true if check 2's counter is ever changed.
234
+ if (this.renewalInFlight)
235
+ return this.renewalInFlight;
236
+ const baseline = this.currentBaseline();
237
+ // 2. A full renewal COMPLETED since this operation began — token, and the
238
+ // session built with it. Not `tokenGeneration`: that moves on a
239
+ // token-only refresh, which rebuilds nothing.
240
+ if (this.recoveredGeneration > baseline)
241
+ return true;
242
+ // 3. Nobody has. Renew, and let everyone else join.
243
+ return this.renewCredential(baselineEpoch, baseline);
244
+ }
54
245
  /**
55
246
  * Establishes the session for this auth type. Called by
56
247
  * AbstractAbapConnection.connect(), which owns the lifecycle around it.
57
248
  */
249
+ /**
250
+ * Cloud manages sessions through ADT: a session resource is opened at
251
+ * `/sap/bc/adt/core/http/sessions` and given back by `DELETE` on the address
252
+ * the server publishes. This is the one connection that does it — on-prem
253
+ * keeps the platform's ICF logoff, which is what it has always used.
254
+ */
255
+ createSessionStrategy() {
256
+ return new CloudSecuritySessionStrategy_js_1.CloudSecuritySessionStrategy(this.logger);
257
+ }
58
258
  async establishSession() {
59
259
  const baseUrl = await this.getBaseUrl();
60
260
  const discoveryUrl = `${baseUrl}/sap/bc/adt/discovery`;
61
261
  this.logger?.debug(`[DEBUG] JwtAbapConnection - Connecting to SAP system: ${discoveryUrl}`);
62
262
  try {
63
263
  // Try to get CSRF token (this will also get cookies)
64
- const token = await this.fetchCsrfToken(discoveryUrl, 3, 1000);
264
+ const token = await this.fetchCsrfToken(discoveryUrl);
65
265
  this.setCsrfToken(token);
66
266
  this.logger?.debug('Successfully connected to SAP system', {
67
267
  hasCsrfToken: !!this.getCsrfToken(),
@@ -70,31 +270,9 @@ class JwtAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnecti
70
270
  });
71
271
  }
72
272
  catch (error) {
73
- // Handle JWT auth errors (401/403) during connect
74
- if (error instanceof axios_1.AxiosError &&
75
- (error.response?.status === 401 || error.response?.status === 403)) {
76
- // Check if this is really an auth error, not a permissions error
77
- const responseData = error.response?.data;
78
- const responseText = typeof responseData === 'string'
79
- ? responseData
80
- : JSON.stringify(responseData || '');
81
- // Don't retry on "No Access" errors
82
- if (responseText.includes('ExceptionResourceNoAccess') ||
83
- responseText.includes('No authorization') ||
84
- responseText.includes('Missing authorization')) {
85
- throw error;
86
- }
87
- // Try to refresh token if tokenRefresher is available
88
- if (await this.tryRefreshToken()) {
89
- // Retry the ESTABLISHMENT, not connect(): connect() runs this as a
90
- // joinable transition, so a nested call joins the transition already
91
- // in flight — this one — and waits for itself forever.
92
- this.logger?.debug(`[DEBUG] JwtAbapConnection - Retrying establishment after token refresh...`);
93
- return this.establishSession();
94
- }
95
- throw new Error('JWT token has expired. Please re-authenticate.');
273
+ if (isTokenExpiryCandidate(error)) {
274
+ this.logger?.error('[ERROR] JwtAbapConnection.establishSession - 401 while establishing; fetchCsrfToken has already refreshed and retried for this');
96
275
  }
97
- // Re-throw other errors
98
276
  throw error;
99
277
  }
100
278
  }
@@ -102,6 +280,10 @@ class JwtAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnecti
102
280
  * Override makeAdtRequest to handle JWT auth errors with automatic token refresh
103
281
  */
104
282
  async makeAdtRequest(options) {
283
+ // A public call is its own operation, whatever scope it starts in.
284
+ return this.inNewRecoveryScope(() => this.attemptRequest(options));
285
+ }
286
+ async attemptRequest(options) {
105
287
  // Captured before the attempt: a recovery asks "has the caller asked to
106
288
  // stop since this request began", not since some later bookkeeping step.
107
289
  const baselineEpoch = this.teardownEpoch;
@@ -113,34 +295,32 @@ class JwtAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnecti
113
295
  }
114
296
  catch (error) {
115
297
  this.logger?.debug(`[DEBUG] JwtAbapConnection.makeAdtRequest - Request failed: ${error instanceof Error ? error.message : String(error)}`);
116
- // Handle JWT auth errors (401/403)
117
- if (error instanceof axios_1.AxiosError &&
118
- (error.response?.status === 401 || error.response?.status === 403)) {
119
- this.logger?.debug(`[DEBUG] JwtAbapConnection.makeAdtRequest - Got ${error.response.status}, attempting token refresh...`);
120
- // Check if this is really an auth error, not a permissions error
121
- const responseData = error.response?.data;
122
- const responseText = typeof responseData === 'string'
123
- ? responseData
124
- : JSON.stringify(responseData || '');
125
- // Don't retry on "No Access" errors - these are permission issues, not auth issues
126
- if (responseText.includes('ExceptionResourceNoAccess') ||
127
- responseText.includes('No authorization') ||
128
- responseText.includes('Missing authorization')) {
129
- throw error;
130
- }
131
- // Try to refresh token if tokenRefresher is available
132
- if (await this.tryRefreshToken()) {
133
- this.logger?.debug(`[DEBUG] JwtAbapConnection.makeAdtRequest - Recovering session after token refresh...`);
134
- // The renewed credential cannot keep the old ABAP session, so this is
135
- // a session-lost teardown — internal, or it would cancel the very
136
- // recovery it is setting up. reset() would be the caller-origin one.
137
- this.discardSession();
138
- // Re-establish before retrying: the retry goes through admission, and
139
- // a discarded session admits nothing.
140
- await this.recoverSession(baselineEpoch);
141
- return super.makeAdtRequest(options);
298
+ if (isTokenExpiryCandidate(error)) {
299
+ this.logger?.debug(`[DEBUG] JwtAbapConnection.makeAdtRequest - Got 401, attempting token refresh...`);
300
+ if (await this.ensureRecovered(baselineEpoch)) {
301
+ // A renewal this operation joined was fenced against the STARTER's
302
+ // baseline, not this one's, and establishAndCommit's own epoch checks
303
+ // belong to the establishment that ran. Between the last of them and
304
+ // this retry there is a gap nothing else watches: the connection can
305
+ // be torn down by its caller and made usable again, and a retry would
306
+ // then go out on a session that caller discarded.
307
+ if (this.teardownEpoch !== baselineEpoch) {
308
+ throw (0, SessionLifecycle_js_1.sessionError)(interfaces_1.ADT_SESSION_ERROR.NOT_CONNECTED, 'Retry abandoned: a teardown was requested for this connection');
309
+ }
310
+ try {
311
+ return await super.makeAdtRequest(options);
312
+ }
313
+ catch (retryError) {
314
+ // A 401 that survived a renewal is the case the deleted message
315
+ // was about, and it only shows up here — the log below fires when
316
+ // the renewal never happened, which is a different fact.
317
+ if (isTokenExpiryCandidate(retryError)) {
318
+ this.logger?.error('[ERROR] JwtAbapConnection.makeAdtRequest - 401 persists after a credential renewal; the credential may need re-authentication');
319
+ }
320
+ throw retryError;
321
+ }
142
322
  }
143
- throw new Error('JWT token has expired. Please re-authenticate.');
323
+ this.logger?.error('[ERROR] JwtAbapConnection.makeAdtRequest - 401 persists and the token could not be refreshed; the credential may need re-authentication');
144
324
  }
145
325
  throw error;
146
326
  }
@@ -148,33 +328,37 @@ class JwtAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnecti
148
328
  /**
149
329
  * Override fetchCsrfToken to handle JWT auth errors with automatic token refresh
150
330
  */
151
- async fetchCsrfToken(url, retryCount = 3, retryDelay = 1000) {
331
+ async fetchCsrfToken(url, retryCount = csrfConfig_js_1.CSRF_CONFIG.RETRY_COUNT, retryDelay = csrfConfig_js_1.CSRF_CONFIG.RETRY_DELAY,
332
+ /** Fences the response effects; omitted during connect(), which has no lease. */
333
+ generation) {
334
+ // An inner level: join the operation in progress, or start one when
335
+ // reached directly — a bare connect() is still an operation with a
336
+ // baseline.
337
+ return this.inRecoveryScope(() => this.attemptCsrfToken(url, retryCount, retryDelay, generation));
338
+ }
339
+ async attemptCsrfToken(url, retryCount, retryDelay, generation) {
152
340
  try {
153
341
  // Try to fetch CSRF token using parent implementation
154
- return await super.fetchCsrfToken(url, retryCount, retryDelay);
342
+ return await super.fetchCsrfToken(url, retryCount, retryDelay, generation);
155
343
  }
156
344
  catch (error) {
157
- // Handle JWT auth errors (401/403) during CSRF token fetch
158
- if (error instanceof axios_1.AxiosError &&
159
- (error.response?.status === 401 || error.response?.status === 403)) {
160
- // Check if this is really an auth error, not a permissions error
161
- const responseData = error.response?.data;
162
- const responseText = typeof responseData === 'string'
163
- ? responseData
164
- : JSON.stringify(responseData || '');
165
- // Don't retry on "No Access" errors
166
- if (responseText.includes('ExceptionResourceNoAccess') ||
167
- responseText.includes('No authorization') ||
168
- responseText.includes('Missing authorization')) {
169
- throw error;
170
- }
171
- // Try to refresh token if tokenRefresher is available
172
- if (await this.tryRefreshToken()) {
345
+ // A 401 here may be an expired token; anything else is not ours to
346
+ // interpret — a 403 least of all, since a new token is the same caller.
347
+ if (isTokenExpiryCandidate(error)) {
348
+ if (await this.refreshTokenOnce(this.currentBaseline())) {
173
349
  // Retry CSRF token fetch with new token
174
350
  this.logger?.debug(`[DEBUG] JwtAbapConnection.fetchCsrfToken - Retrying after token refresh...`);
175
- return super.fetchCsrfToken(url, retryCount, retryDelay);
351
+ try {
352
+ return await super.fetchCsrfToken(url, retryCount, retryDelay, generation);
353
+ }
354
+ catch (retryError) {
355
+ if (isTokenExpiryCandidate(retryError)) {
356
+ this.logger?.error('[ERROR] JwtAbapConnection.fetchCsrfToken - 401 persists after a token refresh; the credential may need re-authentication');
357
+ }
358
+ throw retryError;
359
+ }
176
360
  }
177
- throw new Error('JWT token has expired. Please re-authenticate.');
361
+ this.logger?.error('[ERROR] JwtAbapConnection.fetchCsrfToken - 401 persists and the token could not be refreshed; the credential may need re-authentication');
178
362
  }
179
363
  // Re-throw other errors
180
364
  throw error;
@@ -1,7 +1,15 @@
1
1
  import type { SapConfig } from '../config/sapConfig.js';
2
2
  import type { ILogger } from '../logger.js';
3
3
  import { AbstractAbapConnection } from './AbstractAbapConnection.js';
4
- /** Kerberos / SPNEGO single-leg auth: send Negotiate token, reuse the resulting SAP session cookie. */
4
+ /**
5
+ * @deprecated The class you take should say which SYSTEM you are dialling,
6
+ * not which credential you hold — a session mechanism that rides along with
7
+ * the credential gets one of them wrong. Use `AdtOnPremConnector` with `a Kerberos credential`:
8
+ *
9
+ * not yet available — SPNEGO keeps its own class until its exchange is moved
10
+ *
11
+ * Kept working, and not removed here: existing consumers are unaffected.
12
+ */
5
13
  export declare class KerberosAbapConnection extends AbstractAbapConnection {
6
14
  private spn;
7
15
  private currentToken;
@@ -1 +1 @@
1
- {"version":3,"file":"KerberosAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/KerberosAbapConnection.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAErE,uGAAuG;AACvG,qBAAa,sBAAuB,SAAQ,sBAAsB;IAChE,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,YAAY,CAAM;gBAEd,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,EAAE,SAAS,CAAC,EAAE,MAAM;IAQ1E,OAAO,CAAC,MAAM,CAAC,cAAc;IAa7B,mDAAmD;cACnC,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAK5C;;;;OAIG;IACH;;;OAGG;cACa,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IAqFjD,SAAS,CAAC,wBAAwB,IAAI,MAAM;CAS7C"}
1
+ {"version":3,"file":"KerberosAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/KerberosAbapConnection.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAErE;;;;;;;;GAQG;AACH,qBAAa,sBAAuB,SAAQ,sBAAsB;IAChE,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,YAAY,CAAM;gBAEd,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,EAAE,SAAS,CAAC,EAAE,MAAM;IAQ1E,OAAO,CAAC,MAAM,CAAC,cAAc;IAa7B,mDAAmD;cACnC,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAK5C;;;;OAIG;IACH;;;OAGG;cACa,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IAqFjD,SAAS,CAAC,wBAAwB,IAAI,MAAM;CAS7C"}
@@ -5,7 +5,15 @@ const axios_1 = require("axios");
5
5
  const kerberosSpnego_js_1 = require("../auth/kerberosSpnego.js");
6
6
  const ntlm_js_1 = require("../auth/ntlm.js");
7
7
  const AbstractAbapConnection_js_1 = require("./AbstractAbapConnection.js");
8
- /** Kerberos / SPNEGO single-leg auth: send Negotiate token, reuse the resulting SAP session cookie. */
8
+ /**
9
+ * @deprecated The class you take should say which SYSTEM you are dialling,
10
+ * not which credential you hold — a session mechanism that rides along with
11
+ * the credential gets one of them wrong. Use `AdtOnPremConnector` with `a Kerberos credential`:
12
+ *
13
+ * not yet available — SPNEGO keeps its own class until its exchange is moved
14
+ *
15
+ * Kept working, and not removed here: existing consumers are unaffected.
16
+ */
9
17
  class KerberosAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnection {
10
18
  spn;
11
19
  currentToken = '';
@@ -35,11 +35,6 @@ export declare class RfcAbapConnection implements AbapConnection {
35
35
  getSessionId(): string | null;
36
36
  setSessionType(type: 'stateful' | 'stateless'): void;
37
37
  makeAdtRequest<T = any, D = any>(options: IAbapRequestOptions): Promise<IAdtResponse<T, D>>;
38
- /**
39
- * Reset the connection — for RFC this closes the session.
40
- * Provides interface compatibility with HTTP connections.
41
- */
42
- reset(): void;
43
38
  /**
44
39
  * Close the RFC connection and release the ABAP session.
45
40
  */
@@ -1 +1 @@
1
- {"version":3,"file":"RfcAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/RfcAbapConnection.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,mBAAmB,EACnB,YAAY,EACb,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAkG1D;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,iBAAkB,YAAW,cAAc;IASpD,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IATzB,OAAO,CAAC,SAAS,CAA2B;IAC5C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAsB;IAChD,OAAO,CAAC,WAAW,CAAyC;IAC5D,OAAO,CAAC,aAAa,CAAuB;gBAGzB,MAAM,EAAE,SAAS,EACjB,MAAM,GAAE,OAAO,GAAG,IAAW;IAa1C,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IA8BxB,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC;IAInC,YAAY,IAAI,MAAM,GAAG,IAAI;IAI7B,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,WAAW,GAAG,IAAI;IAU9C,cAAc,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC,GAAG,GAAG,EACnC,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IA8L9B;;;OAGG;IACH,KAAK,IAAI,IAAI;IAIb;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAY5B,OAAO,CAAC,MAAM,CAAC,cAAc;CAqB9B"}
1
+ {"version":3,"file":"RfcAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/RfcAbapConnection.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,mBAAmB,EACnB,YAAY,EACb,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAkG1D;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,iBAAkB,YAAW,cAAc;IASpD,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IATzB,OAAO,CAAC,SAAS,CAA2B;IAC5C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAsB;IAChD,OAAO,CAAC,WAAW,CAAyC;IAC5D,OAAO,CAAC,aAAa,CAAuB;gBAGzB,MAAM,EAAE,SAAS,EACjB,MAAM,GAAE,OAAO,GAAG,IAAW;IAa1C,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IA8BxB,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC;IAInC,YAAY,IAAI,MAAM,GAAG,IAAI;IAI7B,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,WAAW,GAAG,IAAI;IAU9C,cAAc,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC,GAAG,GAAG,EACnC,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IA8L9B;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAY5B,OAAO,CAAC,MAAM,CAAC,cAAc;CAqB9B"}