@mcp-abap-adt/connection 5.0.0 → 6.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 (91) hide show
  1. package/CHANGELOG.md +134 -1
  2. package/README.md +204 -48
  3. package/dist/auth/providers.d.ts +38 -9
  4. package/dist/auth/providers.d.ts.map +1 -1
  5. package/dist/auth/providers.js +46 -3
  6. package/dist/connection/AbstractAbapConnection.d.ts +83 -136
  7. package/dist/connection/AbstractAbapConnection.d.ts.map +1 -1
  8. package/dist/connection/AbstractAbapConnection.js +241 -763
  9. package/dist/connection/AdtCloudConnector.d.ts +12 -7
  10. package/dist/connection/AdtCloudConnector.d.ts.map +1 -1
  11. package/dist/connection/AdtCloudConnector.js +2 -6
  12. package/dist/connection/AdtOnPremConnector.d.ts +20 -7
  13. package/dist/connection/AdtOnPremConnector.d.ts.map +1 -1
  14. package/dist/connection/AdtOnPremConnector.js +2 -6
  15. package/dist/connection/CloudHttpTransport.d.ts +37 -0
  16. package/dist/connection/CloudHttpTransport.d.ts.map +1 -0
  17. package/dist/{session/CloudSecuritySessionStrategy.js → connection/CloudHttpTransport.js} +59 -49
  18. package/dist/connection/CredentialAbapConnection.d.ts +8 -41
  19. package/dist/connection/CredentialAbapConnection.d.ts.map +1 -1
  20. package/dist/connection/CredentialAbapConnection.js +44 -111
  21. package/dist/connection/HttpTransport.d.ts +178 -0
  22. package/dist/connection/HttpTransport.d.ts.map +1 -0
  23. package/dist/connection/HttpTransport.js +402 -0
  24. package/dist/connection/IAdtTransport.d.ts +232 -0
  25. package/dist/connection/IAdtTransport.d.ts.map +1 -0
  26. package/dist/connection/IAdtTransport.js +28 -0
  27. package/dist/connection/LegacyOnPremHttpTransport.d.ts +40 -0
  28. package/dist/connection/LegacyOnPremHttpTransport.d.ts.map +1 -0
  29. package/dist/connection/LegacyOnPremHttpTransport.js +57 -0
  30. package/dist/connection/OnPremHttpTransport.d.ts +45 -0
  31. package/dist/connection/OnPremHttpTransport.d.ts.map +1 -0
  32. package/dist/connection/OnPremHttpTransport.js +91 -0
  33. package/dist/connection/RfcTransport.d.ts +89 -0
  34. package/dist/connection/RfcTransport.d.ts.map +1 -0
  35. package/dist/connection/RfcTransport.js +256 -0
  36. package/dist/connection/rfcConversation.d.ts +44 -0
  37. package/dist/connection/rfcConversation.d.ts.map +1 -0
  38. package/dist/connection/rfcConversation.js +71 -0
  39. package/dist/index.d.ts +7 -8
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +21 -18
  42. package/dist/utils/timeouts.d.ts +6 -19
  43. package/dist/utils/timeouts.d.ts.map +1 -1
  44. package/dist/utils/timeouts.js +6 -22
  45. package/docs/INDEX.md +5 -2
  46. package/docs/INSTALLATION.md +28 -10
  47. package/docs/JWT_AUTH_TOOLS.md +20 -4
  48. package/docs/MIGRATION-6.0.md +359 -0
  49. package/docs/SCOPE.md +1 -1
  50. package/docs/STATEFUL_SESSION_GUIDE.md +86 -17
  51. package/docs/USAGE.md +260 -119
  52. package/examples/basic-connection.js +15 -3
  53. package/examples/jwt-with-token-refresh.js +15 -7
  54. package/examples/saml-connection.js +15 -2
  55. package/package.json +12 -10
  56. package/dist/__tests__/helpers/session.d.ts +0 -15
  57. package/dist/__tests__/helpers/session.d.ts.map +0 -1
  58. package/dist/__tests__/helpers/session.js +0 -19
  59. package/dist/auth/IAuthProvider.d.ts +0 -84
  60. package/dist/auth/IAuthProvider.d.ts.map +0 -1
  61. package/dist/auth/IAuthProvider.js +0 -21
  62. package/dist/connection/BaseAbapConnection.d.ts +0 -29
  63. package/dist/connection/BaseAbapConnection.d.ts.map +0 -1
  64. package/dist/connection/BaseAbapConnection.js +0 -81
  65. package/dist/connection/CertificateAbapConnection.d.ts +0 -35
  66. package/dist/connection/CertificateAbapConnection.d.ts.map +0 -1
  67. package/dist/connection/CertificateAbapConnection.js +0 -91
  68. package/dist/connection/JwtAbapConnection.d.ts +0 -131
  69. package/dist/connection/JwtAbapConnection.d.ts.map +0 -1
  70. package/dist/connection/JwtAbapConnection.js +0 -376
  71. package/dist/connection/KerberosAbapConnection.d.ts +0 -32
  72. package/dist/connection/KerberosAbapConnection.d.ts.map +0 -1
  73. package/dist/connection/KerberosAbapConnection.js +0 -128
  74. package/dist/connection/RfcAbapConnection.d.ts +0 -44
  75. package/dist/connection/RfcAbapConnection.d.ts.map +0 -1
  76. package/dist/connection/RfcAbapConnection.js +0 -324
  77. package/dist/connection/SamlAbapConnection.d.ts +0 -31
  78. package/dist/connection/SamlAbapConnection.d.ts.map +0 -1
  79. package/dist/connection/SamlAbapConnection.js +0 -81
  80. package/dist/connection/connectionFactory.d.ts +0 -25
  81. package/dist/connection/connectionFactory.d.ts.map +0 -1
  82. package/dist/connection/connectionFactory.js +0 -84
  83. package/dist/session/CloudSecuritySessionStrategy.d.ts +0 -32
  84. package/dist/session/CloudSecuritySessionStrategy.d.ts.map +0 -1
  85. package/dist/session/IcfSessionStrategy.d.ts +0 -27
  86. package/dist/session/IcfSessionStrategy.d.ts.map +0 -1
  87. package/dist/session/IcfSessionStrategy.js +0 -62
  88. package/dist/session/SessionStrategy.d.ts +0 -86
  89. package/dist/session/SessionStrategy.d.ts.map +0 -1
  90. package/dist/session/SessionStrategy.js +0 -33
  91. package/docs/superpowers/specs/2026-08-21-platform-connectors.md +0 -108
@@ -15,15 +15,20 @@
15
15
  * inferred — not from the credential, not from the host name, not from what the
16
16
  * server answers.
17
17
  */
18
- import type { IAuthProvider } from '../auth/IAuthProvider.js';
18
+ import type { IAuthProvider } from '@mcp-abap-adt/interfaces';
19
19
  import type { SapConfig } from '../config/sapConfig.js';
20
20
  import type { ILogger } from '../logger.js';
21
- import type { SessionStrategy } from '../session/SessionStrategy.js';
21
+ import type { CloudHttpTransport } from './CloudHttpTransport.js';
22
22
  import { CredentialAbapConnection } from './CredentialAbapConnection.js';
23
- export declare class AdtCloudConnector extends CredentialAbapConnection {
24
- constructor(config: SapConfig, credential: IAuthProvider, logger?: ILogger | null, sessionId?: string, options?: {
25
- skipSessionType?: boolean;
26
- });
27
- protected createSessionStrategy(): SessionStrategy;
23
+ import type { ICloudTransport } from './IAdtTransport.js';
24
+ export declare class AdtCloudConnector<TCredential extends IAuthProvider = IAuthProvider,
25
+ /**
26
+ * Constrained to the cloud wire, so "cloud over RFC" does not compile.
27
+ * There is no such deployment, and a connector that accepted the transport
28
+ * would be offering a combination that cannot exist.
29
+ */
30
+ TTransport extends ICloudTransport = CloudHttpTransport> extends CredentialAbapConnection<TCredential> {
31
+ readonly transport: TTransport;
32
+ constructor(config: SapConfig, credential: TCredential, transport: TTransport, logger?: ILogger | null, sessionId?: string);
28
33
  }
29
34
  //# sourceMappingURL=AdtCloudConnector.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AdtCloudConnector.d.ts","sourceRoot":"","sources":["../../src/connection/AdtCloudConnector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AAEzE,qBAAa,iBAAkB,SAAQ,wBAAwB;gBAE3D,MAAM,EAAE,SAAS,EACjB,UAAU,EAAE,aAAa,EACzB,MAAM,GAAE,OAAO,GAAG,IAAW,EAC7B,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE;cAKtB,qBAAqB,IAAI,eAAe;CAG5D"}
1
+ {"version":3,"file":"AdtCloudConnector.d.ts","sourceRoot":"","sources":["../../src/connection/AdtCloudConnector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAE1D,qBAAa,iBAAiB,CAC5B,WAAW,SAAS,aAAa,GAAG,aAAa;AACjD;;;;GAIG;AACH,UAAU,SAAS,eAAe,GAAG,kBAAkB,CACvD,SAAQ,wBAAwB,CAAC,WAAW,CAAC;IAC7C,SAAiB,SAAS,EAAE,UAAU,CAAC;gBAGrC,MAAM,EAAE,SAAS,EACjB,UAAU,EAAE,WAAW,EACvB,SAAS,EAAE,UAAU,EACrB,MAAM,GAAE,OAAO,GAAG,IAAW,EAC7B,SAAS,CAAC,EAAE,MAAM;CAIrB"}
@@ -18,14 +18,10 @@
18
18
  */
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
20
  exports.AdtCloudConnector = void 0;
21
- const CloudSecuritySessionStrategy_js_1 = require("../session/CloudSecuritySessionStrategy.js");
22
21
  const CredentialAbapConnection_js_1 = require("./CredentialAbapConnection.js");
23
22
  class AdtCloudConnector extends CredentialAbapConnection_js_1.CredentialAbapConnection {
24
- constructor(config, credential, logger = null, sessionId, options) {
25
- super(config, credential, logger, sessionId, options);
26
- }
27
- createSessionStrategy() {
28
- return new CloudSecuritySessionStrategy_js_1.CloudSecuritySessionStrategy(this.logger);
23
+ constructor(config, credential, transport, logger = null, sessionId) {
24
+ super(config, credential, transport, logger, sessionId);
29
25
  }
30
26
  }
31
27
  exports.AdtCloudConnector = AdtCloudConnector;
@@ -16,15 +16,28 @@
16
16
  * asked for this connector — not because of what is in the header, and not
17
17
  * because anything worked out where it was pointing.
18
18
  */
19
- import type { IAuthProvider } from '../auth/IAuthProvider.js';
19
+ import type { IAuthProvider } from '@mcp-abap-adt/interfaces';
20
20
  import type { SapConfig } from '../config/sapConfig.js';
21
21
  import type { ILogger } from '../logger.js';
22
- import type { SessionStrategy } from '../session/SessionStrategy.js';
23
22
  import { CredentialAbapConnection } from './CredentialAbapConnection.js';
24
- export declare class AdtOnPremConnector extends CredentialAbapConnection {
25
- constructor(config: SapConfig, credential: IAuthProvider, logger?: ILogger | null, sessionId?: string, options?: {
26
- skipSessionType?: boolean;
27
- });
28
- protected createSessionStrategy(): SessionStrategy;
23
+ import type { IOnPremTransport } from './IAdtTransport.js';
24
+ import type { OnPremHttpTransport } from './OnPremHttpTransport.js';
25
+ export declare class AdtOnPremConnector<TCredential extends IAuthProvider = IAuthProvider,
26
+ /**
27
+ * The two wires an on-prem system actually offers. Said by the caller: the
28
+ * same ADT call travels over HTTP, or over RFC on a system where stateful
29
+ * HTTP sessions are not usable.
30
+ */
31
+ TTransport extends IOnPremTransport = OnPremHttpTransport> extends CredentialAbapConnection<TCredential> {
32
+ /**
33
+ * The transport this was built with, in the type.
34
+ *
35
+ * `declare` because the base already assigns it — this only narrows what the
36
+ * caller gets back, which is the whole point of the parameter: a signature
37
+ * can ask for `AdtOnPremConnector<IAuthProvider, RfcTransport>` instead of
38
+ * taking any connection and casting.
39
+ */
40
+ readonly transport: TTransport;
41
+ constructor(config: SapConfig, credential: TCredential, transport: TTransport, logger?: ILogger | null, sessionId?: string);
29
42
  }
30
43
  //# sourceMappingURL=AdtOnPremConnector.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AdtOnPremConnector.d.ts","sourceRoot":"","sources":["../../src/connection/AdtOnPremConnector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AAEzE,qBAAa,kBAAmB,SAAQ,wBAAwB;gBAE5D,MAAM,EAAE,SAAS,EACjB,UAAU,EAAE,aAAa,EACzB,MAAM,GAAE,OAAO,GAAG,IAAW,EAC7B,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE;cAKtB,qBAAqB,IAAI,eAAe;CAG5D"}
1
+ {"version":3,"file":"AdtOnPremConnector.d.ts","sourceRoot":"","sources":["../../src/connection/AdtOnPremConnector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAEpE,qBAAa,kBAAkB,CAC7B,WAAW,SAAS,aAAa,GAAG,aAAa;AACjD;;;;GAIG;AACH,UAAU,SAAS,gBAAgB,GAAG,mBAAmB,CACzD,SAAQ,wBAAwB,CAAC,WAAW,CAAC;IAC7C;;;;;;;OAOG;IACH,SAAiB,SAAS,EAAE,UAAU,CAAC;gBAGrC,MAAM,EAAE,SAAS,EACjB,UAAU,EAAE,WAAW,EACvB,SAAS,EAAE,UAAU,EACrB,MAAM,GAAE,OAAO,GAAG,IAAW,EAC7B,SAAS,CAAC,EAAE,MAAM;CAIrB"}
@@ -19,14 +19,10 @@
19
19
  */
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
21
  exports.AdtOnPremConnector = void 0;
22
- const IcfSessionStrategy_js_1 = require("../session/IcfSessionStrategy.js");
23
22
  const CredentialAbapConnection_js_1 = require("./CredentialAbapConnection.js");
24
23
  class AdtOnPremConnector extends CredentialAbapConnection_js_1.CredentialAbapConnection {
25
- constructor(config, credential, logger = null, sessionId, options) {
26
- super(config, credential, logger, sessionId, options);
27
- }
28
- createSessionStrategy() {
29
- return new IcfSessionStrategy_js_1.IcfSessionStrategy(this.logger);
24
+ constructor(config, credential, transport, logger = null, sessionId) {
25
+ super(config, credential, transport, logger, sessionId);
30
26
  }
31
27
  }
32
28
  exports.AdtOnPremConnector = AdtOnPremConnector;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * ABAP Cloud over HTTP: the wire where a session is a resource with an address.
3
+ *
4
+ * Asked for at `/sap/bc/adt/core/http/sessions` with `x-sap-security-session:
5
+ * create` and `sap-adt-purpose: preflight_logon`, and given back by `DELETE` on
6
+ * the address the server publishes in the answer. Reproduced from Eclipse ADT
7
+ * 3.60 and measured against a BTP trial: without that request the same
8
+ * connection gets `sap-usercontext` and `sap-XSRF_*` and no session for a lock
9
+ * to be bound to, which is how "cloud issues no SAP_SESSIONID" came to be
10
+ * believed.
11
+ *
12
+ * **Taking this transport is the consumer saying which system it is dialling.**
13
+ * Nothing here asks the server what it is. The strategy this replaces treated a
14
+ * 404 on the session resource as "then this must be on-prem" and quietly fell
15
+ * back to the platform logoff — an inference, and the only one left in this
16
+ * layer. A consumer that reaches an on-prem system with this transport has
17
+ * taken the wrong one, and finds out rather than being silently downgraded.
18
+ */
19
+ import { HttpTransport } from './HttpTransport.js';
20
+ import type { IAdtSessionContext, ICloudTransport } from './IAdtTransport.js';
21
+ export declare class CloudHttpTransport extends HttpTransport implements ICloudTransport {
22
+ readonly kind = "cloud-http";
23
+ /** Which system this wire is for. Read by the compiler, never at runtime. */
24
+ readonly system: "cloud";
25
+ /** The address this server published for our session — the only close target. */
26
+ private resource;
27
+ open(context: IAdtSessionContext): Promise<void>;
28
+ close(context: IAdtSessionContext): Promise<void>;
29
+ /**
30
+ * The `securitysession` link's href, by its `rel` rather than by position.
31
+ *
32
+ * The document carries three links — the session, the logoff resource and
33
+ * system information — and their order is not a contract.
34
+ */
35
+ private securitySessionHref;
36
+ }
37
+ //# sourceMappingURL=CloudHttpTransport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CloudHttpTransport.d.ts","sourceRoot":"","sources":["../../src/connection/CloudHttpTransport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAKH,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,KAAK,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAa9E,qBAAa,kBACX,SAAQ,aACR,YAAW,eAAe;IAE1B,SAAkB,IAAI,gBAAgB;IAEtC,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,EAAG,OAAO,CAAU;IAEnC,iFAAiF;IACjF,OAAO,CAAC,QAAQ,CAAuB;IAExB,IAAI,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAqDhD,KAAK,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IA4ChE;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;CAW5B"}
@@ -1,102 +1,112 @@
1
1
  "use strict";
2
2
  /**
3
- * ABAP Cloud: a session is a resource, asked for and given back by address.
3
+ * ABAP Cloud over HTTP: the wire where a session is a resource with an address.
4
4
  *
5
- * Captured from Eclipse ADT 3.60 against a BTP trial and reproduced here with a
6
- * bearer token, which is what made the difference visible: **the server issues
7
- * `SAP_SESSIONID` to whoever asks for a session, and we never asked.** The
8
- * request that asks carries `x-sap-security-session: create` — `use` does not
9
- * do it — and `sap-adt-purpose: preflight_logon`. The response sets the cookie
10
- * and publishes the session's own address in a `securitysession` link, which is
11
- * the address a close goes to.
5
+ * Asked for at `/sap/bc/adt/core/http/sessions` with `x-sap-security-session:
6
+ * create` and `sap-adt-purpose: preflight_logon`, and given back by `DELETE` on
7
+ * the address the server publishes in the answer. Reproduced from Eclipse ADT
8
+ * 3.60 and measured against a BTP trial: without that request the same
9
+ * connection gets `sap-usercontext` and `sap-XSRF_*` and no session for a lock
10
+ * to be bound to, which is how "cloud issues no SAP_SESSIONID" came to be
11
+ * believed.
12
12
  *
13
- * Without that request the same connection gets `sap-usercontext` and
14
- * `sap-XSRF_*` and nothing else, no `securitysession` link, and no session for
15
- * a lock to be bound to — which is how "cloud does not issue SAP_SESSIONID"
16
- * came to be believed.
13
+ * **Taking this transport is the consumer saying which system it is dialling.**
14
+ * Nothing here asks the server what it is. The strategy this replaces treated a
15
+ * 404 on the session resource as "then this must be on-prem" and quietly fell
16
+ * back to the platform logoff — an inference, and the only one left in this
17
+ * layer. A consumer that reaches an on-prem system with this transport has
18
+ * taken the wrong one, and finds out rather than being silently downgraded.
17
19
  */
18
20
  Object.defineProperty(exports, "__esModule", { value: true });
19
- exports.CloudSecuritySessionStrategy = void 0;
21
+ exports.CloudHttpTransport = void 0;
20
22
  const cookies_js_1 = require("../utils/cookies.js");
21
23
  const timeouts_js_1 = require("../utils/timeouts.js");
22
- const SessionStrategy_js_1 = require("./SessionStrategy.js");
24
+ const HttpTransport_js_1 = require("./HttpTransport.js");
23
25
  const SESSIONS_PATH = '/sap/bc/adt/core/http/sessions';
24
26
  /** The versioned media types Eclipse asks this resource for, newest first. */
25
27
  const SESSION_ACCEPT = 'application/vnd.sap.adt.core.http.session.v3+xml, ' +
26
28
  'application/vnd.sap.adt.core.http.session.v2+xml, ' +
27
29
  'application/vnd.sap.adt.core.http.session.v1+xml';
28
30
  const SECURITY_SESSION_REL = 'http://www.sap.com/adt/categories/core/http/sessions/securitysession';
29
- class CloudSecuritySessionStrategy extends SessionStrategy_js_1.SessionStrategy {
30
- kind = 'cloud-security-session';
31
+ class CloudHttpTransport extends HttpTransport_js_1.HttpTransport {
32
+ kind = 'cloud-http';
33
+ /** Which system this wire is for. Read by the compiler, never at runtime. */
34
+ system = 'cloud';
31
35
  /** The address this server published for our session — the only close target. */
32
36
  resource = null;
33
- async openSession(transport) {
37
+ async open(context) {
34
38
  try {
35
39
  // Read ONCE. The contract lets a provider answer differently each time —
36
40
  // a token provider renews behind the call — so two reads can build one
37
- // request out of two different credentials, and for a token provider they
38
- // also double the work.
39
- const auth = await transport.authHeaders();
40
- const response = await transport.send({
41
+ // request out of two different credentials.
42
+ const auth = await context.authHeaders();
43
+ const response = await this.send({
41
44
  method: 'GET',
42
45
  // The cache-buster is Eclipse's; kept because this must not be served
43
46
  // from anything's cache — a cached session document would hand back an
44
47
  // address that belongs to a session somebody else already ended.
45
- url: `${transport.baseUrl}${SESSIONS_PATH}?_=${Date.now()}`,
48
+ url: `${context.baseUrl}${SESSIONS_PATH}?_=${Date.now()}`,
46
49
  // Bounded: connect() waits for this one.
47
- timeoutMs: (0, timeouts_js_1.getTimeout)('csrf'),
48
- // Its SAP_SESSIONID is the session.
49
- adoptCookies: true,
50
+ timeout: (0, timeouts_js_1.getTimeout)('csrf'),
50
51
  headers: {
51
52
  ...auth,
53
+ ...context.extraHeaders,
52
54
  Accept: SESSION_ACCEPT,
53
- Cookie: (0, cookies_js_1.mergeCookieHeaders)(auth.Cookie, transport.cookies() ?? undefined),
55
+ Cookie: (0, cookies_js_1.mergeCookieHeaders)(auth.Cookie, this.cookies() ?? undefined),
54
56
  'sap-adt-purpose': 'preflight_logon',
55
57
  'x-sap-security-session': 'create',
56
58
  },
57
59
  });
58
- // 404 is the answer "this system has no session resource", which is what
59
- // on-prem says. Not an error, and not something to retry.
60
- if (response.status === 404) {
61
- return false;
62
- }
60
+ // Its SAP_SESSIONID is the session, so the answer is folded in before
61
+ // anything is read out of it.
62
+ context.observe(response.headers);
63
+ this.ingest(response.headers);
63
64
  const resource = this.securitySessionHref(response.data);
64
65
  if (!resource) {
65
66
  // Answered, but published no session of its own — so there is no
66
- // address to give one back by, and claiming this mechanism would leave
67
- // the close with nothing to send. Falls back to the platform's logoff,
68
- // which needs no address.
67
+ // address to give one back by. Said out loud rather than worked
68
+ // around: the establishment that follows reports what a connection
69
+ // without a session means, and this is the fact it will be reporting.
69
70
  this.logger?.debug(`Session resource answered ${response.status} but published no securitysession link`);
70
- return false;
71
+ return;
71
72
  }
72
73
  this.resource = resource;
73
74
  this.logger?.debug(`Security session opened: ${resource}`);
74
- return true;
75
75
  }
76
76
  catch (error) {
77
- // Never fatal here. If no session was opened, the establishment that
78
- // follows says so with the whole picture; a transport error raised from
79
- // a preflight would replace that with something less useful.
77
+ // Not fatal here. If no session was opened, the establishment that
78
+ // follows says so with the whole picture; an error raised from a
79
+ // preflight would replace that with something less useful.
80
80
  this.logger?.debug(`Could not open a security session: ${error instanceof Error ? error.message : String(error)}`);
81
- return false;
82
81
  }
83
82
  }
84
- async closeSession(transport) {
83
+ async close(context) {
85
84
  const resource = this.resource;
86
- if (!resource || !transport.cookies()) {
85
+ this.resource = null;
86
+ // Read SYNCHRONOUSLY, before the first await. A close is dispatched
87
+ // without being awaited, so the teardown's `forgetSession()` runs while
88
+ // this is suspended — and a goodbye assembled afterwards would carry no
89
+ // cookie, which is the one thing that proves the session is ours to end.
90
+ const cookies = this.cookies();
91
+ const csrf = this.csrfToken();
92
+ // Snapshotted for the same reason: the DELETE must reach the server holding
93
+ // this session, not whichever one the wire is pinned to when it goes out.
94
+ const affinity = this.affinityHeaders();
95
+ if (!resource || !cookies) {
87
96
  // No address to send it to, or no cookie to prove the session is ours —
88
- // holding the cookie is the whole permission.
97
+ // holding the cookie is the whole permission to end one.
89
98
  return;
90
99
  }
91
- const csrf = transport.csrfToken();
92
100
  try {
93
- const auth = await transport.authHeaders();
94
- await transport.send({
101
+ const auth = await context.authHeaders();
102
+ await this.sendDetached({
95
103
  method: 'DELETE',
96
- url: new URL(resource, transport.baseUrl).toString(),
104
+ url: new URL(resource, context.baseUrl).toString(),
97
105
  headers: {
106
+ ...affinity,
98
107
  ...auth,
99
- Cookie: (0, cookies_js_1.mergeCookieHeaders)(auth.Cookie, transport.cookies() ?? undefined),
108
+ ...context.extraHeaders,
109
+ Cookie: (0, cookies_js_1.mergeCookieHeaders)(auth.Cookie, cookies),
100
110
  'x-sap-security-session': 'use',
101
111
  // A state change, so the server wants the token. Without one the
102
112
  // request is refused with 403 — which is still just a message the
@@ -132,4 +142,4 @@ class CloudSecuritySessionStrategy extends SessionStrategy_js_1.SessionStrategy
132
142
  return undefined;
133
143
  }
134
144
  }
135
- exports.CloudSecuritySessionStrategy = CloudSecuritySessionStrategy;
145
+ exports.CloudHttpTransport = CloudHttpTransport;
@@ -13,16 +13,17 @@
13
13
  * chose the subclass, and is never worked out here.
14
14
  */
15
15
  import type { AgentOptions } from 'node:https';
16
- import type { IAbapRequestOptions, IAdtResponse } from '@mcp-abap-adt/interfaces';
17
- import type { IAuthProvider } from '../auth/IAuthProvider.js';
16
+ import type { IAuthProvider } from '@mcp-abap-adt/interfaces';
18
17
  import type { SapConfig } from '../config/sapConfig.js';
19
18
  import type { ILogger } from '../logger.js';
20
19
  import { AbstractAbapConnection } from './AbstractAbapConnection.js';
21
- export declare abstract class CredentialAbapConnection extends AbstractAbapConnection {
22
- protected readonly credential: IAuthProvider;
23
- constructor(config: SapConfig, credential: IAuthProvider, logger?: ILogger | null, sessionId?: string, options?: {
24
- skipSessionType?: boolean;
25
- });
20
+ import { type IAdtTransport } from './IAdtTransport.js';
21
+ export declare abstract class CredentialAbapConnection<TCredential extends IAuthProvider = IAuthProvider> extends AbstractAbapConnection {
22
+ /** Public because the type is the point: a caller can reach what it gave. */
23
+ readonly credential: TCredential;
24
+ constructor(config: SapConfig,
25
+ /** Public because the type is the point: a caller can reach what it gave. */
26
+ credential: TCredential, transport: IAdtTransport, logger?: ILogger | null, sessionId?: string);
26
27
  /**
27
28
  * Whatever the credential needs before the first request goes out.
28
29
  *
@@ -32,8 +33,6 @@ export declare abstract class CredentialAbapConnection extends AbstractAbapConne
32
33
  * evaluated — outside every catch, with no request sent.
33
34
  */
34
35
  /** The header last put on the wire, so a change can be seen. */
35
- private lastAuthorization;
36
- private credentialRenewal?;
37
36
  protected prepareCredential(): Promise<void>;
38
37
  /**
39
38
  * Nothing here: the header is asked for asynchronously in `getAuthHeaders()`,
@@ -51,38 +50,6 @@ export declare abstract class CredentialAbapConnection extends AbstractAbapConne
51
50
  */
52
51
  getAuthHeaders(): Promise<Record<string, string>>;
53
52
  protected getHttpsAgentOptions(): AgentOptions;
54
- /**
55
- * One retry, and only when the credential actually changed.
56
- *
57
- * A provider renews on its own — `BaseTokenProvider` checks expiry and
58
- * refreshes before answering — so a 401 has two very different causes, and
59
- * asking again tells them apart. A different answer means the token was
60
- * stale: the ABAP session built on the old one is dead, so it is rebuilt and
61
- * the request goes once more. The same answer means the server refused these
62
- * credentials, and repeating them would only ask a second time.
63
- *
64
- * This is why no renewal strategy is injected. The provider owns "get me a
65
- * valid credential"; what is left is "the credential changed, so the session
66
- * is gone", which belongs to whoever owns the session and cannot be done from
67
- * outside it.
68
- */
69
- makeAdtRequest<T = any, D = any>(options: IAbapRequestOptions): Promise<IAdtResponse<T, D>>;
70
- /**
71
- * Ask the provider again; if it answers differently, rebuild the session.
72
- *
73
- * Single-flight, because concurrent operations meet the same 401 at the same
74
- * moment and the session must be rebuilt once, not once per request in
75
- * flight. Everyone joins the first one and gets its verdict.
76
- */
77
- private rebuiltAfterCredentialChange;
78
- /**
79
- * The establishing call, shared because it always was.
80
- *
81
- * Failure is a warning rather than a throw, as it has been: the first request
82
- * retries it, and a system that answers the discovery call badly may still
83
- * answer everything else. What decides whether the connection is usable is
84
- * the session check that follows, not this.
85
- */
86
53
  protected establishSession(): Promise<void>;
87
54
  }
88
55
  //# sourceMappingURL=CredentialAbapConnection.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"CredentialAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/CredentialAbapConnection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,KAAK,EACV,mBAAmB,EACnB,YAAY,EACb,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAC9D,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;AASrE,8BAAsB,wBAAyB,SAAQ,sBAAsB;IAGzE,SAAS,CAAC,QAAQ,CAAC,UAAU,EAAE,aAAa;gBAD5C,MAAM,EAAE,SAAS,EACE,UAAU,EAAE,aAAa,EAC5C,MAAM,GAAE,OAAO,GAAG,IAAW,EAC7B,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE;IAKzC;;;;;;;OAOG;IACH,gEAAgE;IAChE,OAAO,CAAC,iBAAiB,CAAM;IAC/B,OAAO,CAAC,iBAAiB,CAAC,CAAmB;cAEpB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;IAI3D;;;;;OAKG;IACH,SAAS,CAAC,wBAAwB,IAAI,MAAM;IAI5C;;;;;;OAMG;IACY,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;cAkB7C,oBAAoB,IAAI,YAAY;IAMvD;;;;;;;;;;;;;;OAcG;IAEY,cAAc,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC,GAAG,GAAG,EAC5C,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAwB9B;;;;;;OAMG;IACH,OAAO,CAAC,4BAA4B;IAqCpC;;;;;;;OAOG;cACa,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;CA4BlD"}
1
+ {"version":3,"file":"CredentialAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/CredentialAbapConnection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,KAAK,EAGV,aAAa,EACd,MAAM,0BAA0B,CAAC;AAClC,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;AACrE,OAAO,EAAE,KAAK,aAAa,EAAa,MAAM,oBAAoB,CAAC;AASnE,8BAAsB,wBAAwB,CAC5C,WAAW,SAAS,aAAa,GAAG,aAAa,CACjD,SAAQ,sBAAsB;IAG5B,6EAA6E;IAC7E,QAAQ,CAAC,UAAU,EAAE,WAAW;gBAFhC,MAAM,EAAE,SAAS;IACjB,6EAA6E;IACpE,UAAU,EAAE,WAAW,EAChC,SAAS,EAAE,aAAa,EACxB,MAAM,GAAE,OAAO,GAAG,IAAW,EAC7B,SAAS,CAAC,EAAE,MAAM;IAKpB;;;;;;;OAOG;IACH,gEAAgE;cAEvC,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;IAI3D;;;;;OAKG;IACH,SAAS,CAAC,wBAAwB,IAAI,MAAM;IAI5C;;;;;;OAMG;IACY,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;cAiB7C,oBAAoB,IAAI,YAAY;cAIvC,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;CAoDlD"}
@@ -15,8 +15,8 @@
15
15
  */
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.CredentialAbapConnection = void 0;
18
- const axios_1 = require("axios");
19
18
  const AbstractAbapConnection_js_1 = require("./AbstractAbapConnection.js");
19
+ const IAdtTransport_js_1 = require("./IAdtTransport.js");
20
20
  /** A 401 from the server, whatever transport shape it arrives in. */
21
21
  function isUnauthorized(error) {
22
22
  const status = error?.response
@@ -25,8 +25,10 @@ function isUnauthorized(error) {
25
25
  }
26
26
  class CredentialAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnection {
27
27
  credential;
28
- constructor(config, credential, logger = null, sessionId, options) {
29
- super(config, logger, sessionId, options);
28
+ constructor(config,
29
+ /** Public because the type is the point: a caller can reach what it gave. */
30
+ credential, transport, logger = null, sessionId) {
31
+ super(config, transport, logger, sessionId);
30
32
  this.credential = credential;
31
33
  }
32
34
  /**
@@ -38,10 +40,8 @@ class CredentialAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapC
38
40
  * evaluated — outside every catch, with no request sent.
39
41
  */
40
42
  /** The header last put on the wire, so a change can be seen. */
41
- lastAuthorization = '';
42
- credentialRenewal;
43
43
  async prepareCredential() {
44
- await this.credential.prepare?.();
44
+ await this.credential.prepare();
45
45
  }
46
46
  /**
47
47
  * Nothing here: the header is asked for asynchronously in `getAuthHeaders()`,
@@ -61,120 +61,42 @@ class CredentialAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapC
61
61
  */
62
62
  async getAuthHeaders() {
63
63
  const headers = await super.getAuthHeaders();
64
- // Asked now, not kept: a token provider renews behind this call, and a
65
- // value held from establishment would be the stale one.
64
+ // Asked per request, never held: a provider renews behind this call, and a
65
+ // value kept here would be the stale one.
66
66
  const authorization = await this.credential.authorizationHeader();
67
67
  if (authorization) {
68
68
  headers.Authorization = authorization;
69
- this.lastAuthorization = authorization;
70
69
  }
71
- const cookies = this.credential.cookies?.();
70
+ const cookies = this.credential.cookies();
72
71
  if (cookies) {
73
72
  headers.Cookie = cookies;
74
73
  }
75
74
  return headers;
76
75
  }
77
76
  getHttpsAgentOptions() {
78
- return (this.credential.httpsAgentOptions?.() ?? super.getHttpsAgentOptions());
77
+ return this.credential.transportMaterial();
79
78
  }
80
- /**
81
- * One retry, and only when the credential actually changed.
82
- *
83
- * A provider renews on its own — `BaseTokenProvider` checks expiry and
84
- * refreshes before answering — so a 401 has two very different causes, and
85
- * asking again tells them apart. A different answer means the token was
86
- * stale: the ABAP session built on the old one is dead, so it is rebuilt and
87
- * the request goes once more. The same answer means the server refused these
88
- * credentials, and repeating them would only ask a second time.
89
- *
90
- * This is why no renewal strategy is injected. The provider owns "get me a
91
- * valid credential"; what is left is "the credential changed, so the session
92
- * is gone", which belongs to whoever owns the session and cannot be done from
93
- * outside it.
94
- */
95
- // biome-ignore lint/suspicious/noExplicitAny: matches the base signature exactly
96
- async makeAdtRequest(options) {
97
- // Which session this request is going out on. A 401 that comes back after
98
- // the session has already been replaced says nothing about the credential:
99
- // it was answered by a server we are no longer talking to.
100
- const sentOn = this.sessionGeneration;
101
- try {
102
- return await super.makeAdtRequest(options);
103
- }
104
- catch (error) {
105
- if (!isUnauthorized(error))
106
- throw error;
107
- if (!(await this.rebuiltAfterCredentialChange(sentOn)))
108
- throw error;
109
- // Exactly once, and to `super` deliberately: reaching for `this` would
110
- // re-enter this method, and a provider that answers differently every
111
- // time — a broken refresher, or a server refusing whatever it is given —
112
- // would look like "the credential changed" forever.
113
- //
114
- // A test pins the request count at two, but it does NOT distinguish the
115
- // two forms: swapping `super` for `this` leaves the suite green, and why
116
- // it does not then recurse is unexplained. Treat this line as guarded by
117
- // review, not by the suite.
118
- return await super.makeAdtRequest(options);
119
- }
120
- }
121
- /**
122
- * Ask the provider again; if it answers differently, rebuild the session.
123
- *
124
- * Single-flight, because concurrent operations meet the same 401 at the same
125
- * moment and the session must be rebuilt once, not once per request in
126
- * flight. Everyone joins the first one and gets its verdict.
127
- */
128
- rebuiltAfterCredentialChange(sentOn) {
129
- if (this.credentialRenewal)
130
- return this.credentialRenewal;
131
- const inFlight = (async () => {
132
- // Somebody else already rebuilt while this request was in flight, so this
133
- // refusal was answered by a session that no longer exists and says
134
- // nothing about the credential in use now. Retry on the new one; renewing
135
- // again would force a second refresh and tear down a healthy session —
136
- // which is what comparing against a connection-wide "last header" did,
137
- // since by then that header was the NEW one.
138
- if (this.sessionGeneration !== sentOn)
139
- return true;
140
- const before = this.lastAuthorization;
141
- // Tell the credential its last answer was refused BEFORE asking again.
142
- // Asking alone is not enough: a token provider returns the cached token
143
- // while it believes it is valid, which after a 401 is precisely what it
144
- // wrongly believes.
145
- await this.credential.renew?.();
146
- const now = await this.credential.authorizationHeader();
147
- if (!now || now === before)
148
- return false;
149
- this.logger?.debug('The credential changed after a 401; the session built on the old one is gone. Rebuilding.');
150
- const baseline = this.teardownEpoch;
151
- this.discardSession();
152
- await this.recoverSession(baseline);
153
- return true;
154
- })().finally(() => {
155
- if (this.credentialRenewal === inFlight) {
156
- this.credentialRenewal = undefined;
157
- }
158
- });
159
- this.credentialRenewal = inFlight;
160
- return inFlight;
161
- }
162
- /**
163
- * The establishing call, shared because it always was.
164
- *
165
- * Failure is a warning rather than a throw, as it has been: the first request
166
- * retries it, and a system that answers the discovery call badly may still
167
- * answer everything else. What decides whether the connection is usable is
168
- * the session check that follows, not this.
169
- */
170
79
  async establishSession() {
171
- const baseUrl = await this.getBaseUrl();
172
- const discoveryUrl = `${baseUrl}/sap/bc/adt/discovery`;
173
80
  try {
174
- const token = this.credential.fetchCsrfToken
175
- ? await this.credential.fetchCsrfToken(discoveryUrl)
176
- : await this.fetchCsrfToken(discoveryUrl);
177
- this.setCsrfToken(token);
81
+ // The wire establishes itself, always. What that means is the wire's:
82
+ // HTTP earns a CSRF token and the cookies that name the session; an RFC
83
+ // conversation was opened before this and already IS the session, so it
84
+ // does nothing and holds no token. Demanding one here was what made
85
+ // `connect()` impossible over RFC.
86
+ //
87
+ // There is no second path. A credential that wanted to run the exchange
88
+ // itself would need the connection to ask which of the two does the work,
89
+ // and a credential whose way in IS a round trip does not need that: the
90
+ // wire asks `authHeaders()` PER ATTEMPT, so a one-shot token is offered
91
+ // on the establishing call and withheld afterwards by the credential
92
+ // itself, with nobody deciding anything.
93
+ await this.transport.establish({
94
+ baseUrl: await this.getBaseUrl(),
95
+ authHeaders: () => this.getAuthHeaders(),
96
+ extraHeaders: { 'sap-adt-connection-id': this.getSessionId() ?? '' },
97
+ observe: (headers) => this.observeResponse(headers),
98
+ isFatal: (error) => this.isSessionVerdict(error),
99
+ });
178
100
  this.logger?.debug('Connected', {
179
101
  credential: this.credential.kind,
180
102
  hasCsrfToken: !!this.getCsrfToken(),
@@ -182,13 +104,24 @@ class CredentialAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapC
182
104
  });
183
105
  }
184
106
  catch (error) {
185
- this.logger?.warn(`Could not establish upfront (${this.credential.kind}): ${error instanceof Error ? error.message : String(error)}. The first request will retry.`);
186
- // A rejecting response can still carry the cookies that matter; they are
187
- // taken by fetchCsrfToken itself, so nothing is read out of the error
107
+ this.logger?.warn(`Could not establish (${this.credential.kind}): ${error instanceof Error ? error.message : String(error)}`);
108
+ // A rejecting response can still carry the cookies that matter; the wire
109
+ // folds them in as it establishes, so nothing is read out of the error
188
110
  // here beyond saying whether any arrived.
189
- if (error instanceof axios_1.AxiosError && error.response?.headers) {
111
+ if ((0, IAdtTransport_js_1.refusalOf)(error)?.headers) {
190
112
  this.logger?.debug(`Cookies after a failed establishment: ${this.getCookies() ? 'present' : 'none'}`);
191
113
  }
114
+ // Rethrow: a resolved connect() must mean a usable session exists.
115
+ //
116
+ // This warned and resolved, on the reasoning that "the first request will
117
+ // retry" — true while establishment could happen lazily, and left behind
118
+ // when connect() became mandatory. Swallowing now leaves a connection
119
+ // that reports success and holds nothing; worse, it skips the debris
120
+ // clearing in establishAndCommit()'s catch, so the Set-Cookie that came
121
+ // with the 401 survives as the session identity. A cookie is proof to
122
+ // every credential that auth is settled, so the NEXT connect() goes out
123
+ // with no credentials at all and fails for an unrelated reason.
124
+ throw error;
192
125
  }
193
126
  }
194
127
  }