arazzo-runner 0.0.15 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -206,6 +206,21 @@ You will need to provide inputs for your clientId and clientSecret:
206
206
 
207
207
  `clientId` and `clientSecret` are reserved name and will be used when oauth or openId authentication is set.
208
208
 
209
+ ## Request Bodies
210
+
211
+ This can handle the encoding and sending of:
212
+
213
+ - application/json
214
+ - application/xml
215
+ - text/xml
216
+ - text/html
217
+ - text/plain
218
+ - application/x-www-form-urlencoded
219
+ - multipart/form-data
220
+ - application/octet-stream
221
+
222
+ For anything outside of this range, it will attempt to encode as JSON if you specify an object, otherwise it will encode as plain text.
223
+
209
224
  ## Logging And Reporting
210
225
 
211
226
  ### Logging
@@ -243,7 +258,3 @@ Accessing an OpenAPI operation by Operation Path `'{$sourceDescriptions.petstore
243
258
  ### Non application/json Responses
244
259
 
245
260
  Responses that do not conform to application/json do not work
246
-
247
- ### Non application/json Requests
248
-
249
- Requests that do not conform to application/json do not work
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arazzo-runner",
3
- "version": "0.0.15",
3
+ "version": "0.0.17",
4
4
  "description": "A runner to run through Arazzo Document workflows",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/src/Arazzo.js CHANGED
@@ -9,6 +9,7 @@ const path = require("node:path");
9
9
 
10
10
  const Document = require("./Document");
11
11
  const Expression = require("./Expression");
12
+ const Operation = require('./Operation');
12
13
  const Rules = require("./Rules");
13
14
 
14
15
  class Arazzo extends Document {
@@ -254,255 +255,15 @@ class Arazzo extends Document {
254
255
  this.step,
255
256
  );
256
257
 
257
- // await this.sourceDescriptionFile.getSecurity();
258
-
259
- if (Object.keys(this.sourceDescriptionFile.securitySchemes).length === 1) {
260
- for (const [key, value] of Object.entries(
261
- this.sourceDescriptionFile.securitySchemes,
262
- )) {
263
- if (value.type.toLowerCase() === "mutualtls")
264
- this.operation.mutualTLS = true;
265
- }
266
- }
267
-
268
- this.mapInputs();
269
-
270
- await this.runOperation();
271
- }
272
-
273
- /**
274
- *
275
- * @private
276
- * @param {*} retry
277
- * @param {*} retryAfter
278
- */
279
- async runOperation(retry = 0, retryAfter = 0) {
280
- const sleep = function (ms) {
281
- return new Promise((resolve) => setTimeout(resolve, ms));
282
- };
283
-
284
- const parseRetryAfter = function (retryAfter) {
285
- if (!retryAfter || typeof retryAfter !== "string") {
286
- return null;
287
- }
288
-
289
- const trimmed = retryAfter.trim();
290
-
291
- // Try parsing as a number (seconds format)
292
- const asNumber = parseInt(trimmed, 10);
293
- if (!isNaN(asNumber) && asNumber >= 0 && String(asNumber) === trimmed) {
294
- return asNumber;
295
- }
296
-
297
- // Try parsing as HTTP date format
298
- try {
299
- const date = new Date(trimmed);
300
-
301
- // Check if date is valid
302
- if (isNaN(date.getTime())) {
303
- return null;
304
- }
305
-
306
- // Calculate seconds from now until the date
307
- const now = new Date();
308
- const secondsUntil = Math.ceil((date.getTime() - now.getTime()) / 1000);
309
-
310
- // Return the delay, but don't return negative values
311
- return secondsUntil >= 0 ? secondsUntil : 0;
312
- } catch (err) {
313
- return null;
314
- }
315
- };
316
-
317
- let url = this.operation.url;
318
-
319
- if (this.operation.queryParams.size) {
320
- url += `?${this.operation.queryParams}`;
321
- }
322
-
323
- const options = {
324
- method: this.operation.method,
325
- headers: this.operation.headers,
258
+ const miniStep = {
259
+ stepId: this.step.stepId,
260
+ parameters: this.step.parameters || [],
261
+ requestBody: this.step.requestBody || undefined,
326
262
  };
327
263
 
328
- if (this.operation.data) {
329
- options.body = this.operation.data;
330
- }
331
-
332
- if (this.retryAfter) {
333
- this.logger.notice(
334
- `retryAfter was set: waiting ${this.retryAfter * 1000} seconds`,
335
- );
336
- await sleep(this.retryAfter * 1000);
337
- }
338
-
339
- this.expression.addToContext("url", url);
340
- this.expression.addToContext("method", options.method);
341
-
342
- this.logger.notice(`Making a ${options.method.toUpperCase()} to ${url}`);
343
-
344
- let response;
345
- if (this.operation?.mutualTLS) {
346
- this.logger.verbose("Using mutualTLS call");
347
- response = await this.mutualTLS();
348
- } else {
349
- this.logger.verbose("Using fetch call");
350
-
351
- this.logger.verbose(`request url: ${url}`);
352
- this.logger.verbose(`request method: ${options.method}`);
353
- this.logger.verbose("request headers:");
354
- for (const [key, value] of options.headers.entries()) {
355
- this.logger.verbose(`${key}: ${value}`);
356
- }
357
-
358
- this.logger.verbose(`body: ${options.body}`);
359
-
360
- response = await fetch(url, options);
361
- }
362
-
363
- this.logger.verbose(`received StatusCode: ${response.status}`);
364
- this.logger.verbose(`received headers:`);
365
- for (const [key, value] of response.headers.entries()) {
366
- this.logger.verbose(`${key}: ${value}`);
367
- }
368
-
369
- if (response.headers.has("retry-after")) {
370
- const retryAfter = parseRetryAfter(response.headers.get("retry-after"));
371
- if (retryAfter !== null) {
372
- this.retryAfter = retryAfter;
373
- }
374
- }
375
-
376
- this.addParamsToContext(response.headers, "header", "response");
377
- this.expression.addToContext("statusCode", response.status);
378
-
379
- this.logger.notice(`${url} responded with a: ${response.status}`);
380
-
381
- await this.dealWithResponse(response);
382
- }
383
-
384
- async mutualTLS() {
385
- let clientKeyPath;
386
- try {
387
- clientKeyPath = path.resolve(this.inputs.key);
388
- this.logger.verbose(`clientKey Path: ${clientKeyPath}`);
389
- } catch (err) {
390
- this.logger.error(`could not resolve clientKey`);
391
- throw err;
392
- }
393
-
394
- let clientCertPath;
395
- try {
396
- clientCertPath = path.resolve(this.inputs.cert);
397
- this.logger.verbose(`clientCert Path: ${clientCertPath}`);
398
- } catch (err) {
399
- this.logger.error(`could not resolve clientCert`);
400
- throw err;
401
- }
402
-
403
- let url = this.operation.url;
404
-
405
- if (this.operation.queryParams.size) {
406
- url += `?${this.operation.queryParams}`;
407
- }
408
-
409
- const opUrl = new URL(url);
410
-
411
- this.logger.verbose(`url: ${opUrl.pathname + opUrl.search}`);
412
- this.logger.verbose(`method: ${this.operation.method}`);
413
- this.logger.verbose("headers:");
414
- const headersObj = {};
415
- for (const [key, value] of this.operation.headers.entries()) {
416
- this.logger.verbose(`${key}: ${value}`);
417
- Object.assign(headersObj, { [key]: value });
418
- }
419
-
420
- this.logger.verbose(`body: ${this.operation.data}`);
421
-
422
- return new Promise((resolve, reject) => {
423
- const options = {
424
- key: fs.readFileSync(clientKeyPath),
425
- cert: fs.readFileSync(clientCertPath),
426
- method: this.operation.method,
427
- headers: headersObj,
428
- rejectUnauthorized: true,
429
- hostname: opUrl.hostname,
430
- path: opUrl.pathname + opUrl.search,
431
- };
432
-
433
- if (this.operation.data) {
434
- options.headers = options.headers || {};
435
- if (!options.headers["content-length"]) {
436
- const bodyBuffer = Buffer.from(
437
- typeof this.operation.data === "string"
438
- ? this.operation.data
439
- : JSON.stringify(this.operation.data),
440
- );
441
- options.headers["content-length"] = bodyBuffer.length;
442
- }
443
- }
444
-
445
- const headers = new Headers();
446
- const chunks = [];
447
-
448
- const req = https.request(options, (res) => {
449
- for (const [name, value] of Object.entries(res.headers)) {
450
- headers.append(name, value);
451
- }
452
-
453
- // Collect data chunks as buffers for proper encoding
454
- res.on("data", (chunk) => chunks.push(chunk));
455
-
456
- res.on("end", () => {
457
- // Concatenate buffers and decode based on content-type
458
- const buffer = Buffer.concat(chunks);
459
- let body;
460
-
461
- const contentType = res.headers["content-type"] || "";
462
-
463
- if (contentType.includes("application/json")) {
464
- try {
465
- body = JSON.parse(buffer.toString("utf8"));
466
- } catch (err) {
467
- body = buffer.toString("utf8");
468
- }
469
- } else {
470
- body = buffer.toString("utf8");
471
- }
472
-
473
- const response = new Response(buffer, {
474
- status: res.status,
475
- statusText: res.statusMessage,
476
- headers,
477
- });
478
- // resolve({
479
- // headers: headers,
480
- // body: body,
481
-
482
- // status: res.statusCode,
483
- // statusText: res.statusMessage,
484
- // ok: res.statusCode >= 200 && res.statusCode < 300,
485
- // });
486
- resolve(response);
487
- });
488
- });
489
-
490
- req.on("error", (err) => {
491
- this.logger.error(`mTLS Error: ${err.message}`);
492
- reject(new Error(`mTLS request failed: ${err.message}`));
493
- });
494
-
495
- // Write request body if present
496
- if (this.operation.data) {
497
- const body =
498
- typeof this.operation.data === "string"
499
- ? this.operation.data
500
- : JSON.stringify(this.operation.data);
501
- req.write(body);
502
- }
503
-
504
- req.end();
505
- });
264
+ const apiOperation = new Operation(this.operation, this.sourceDescriptionFile, this.expression, this.inputs, this.logger, miniStep);
265
+ const response = await apiOperation.runOperation()
266
+ await this.dealWithResponse(response)
506
267
  }
507
268
 
508
269
  /**
@@ -510,24 +271,6 @@ class Arazzo extends Document {
510
271
  * @param {*} response
511
272
  */
512
273
  async dealWithResponse(response) {
513
- if (
514
- response.headers.has("Content-Type") &&
515
- response.headers.get("Content-Type") === "application/json"
516
- ) {
517
- const json = await response?.json().catch((err) => {
518
- this.logger.error(
519
- `Error trying to resolve ${this.step.stepId} outputs`,
520
- );
521
- throw new Error(err);
522
- });
523
-
524
- this.expression.addToContext("response.body", json);
525
- } else {
526
- const body = await response.body;
527
-
528
- this.expression.addToContext("response.body", body);
529
- }
530
-
531
274
  this.doNotProcessStep = false;
532
275
  this.alreadyProcessingOnFailure = false;
533
276
 
@@ -878,195 +621,6 @@ class Arazzo extends Document {
878
621
  });
879
622
  }
880
623
 
881
- /**
882
- * @private
883
- */
884
- mapInputs() {
885
- this.mapParameters();
886
- this.mapRequestBody();
887
-
888
- this.addParamsToContext(this.operation.headers, "headers", "request");
889
- this.addParamsToContext(this.operation.queryParams, "query", "request");
890
- }
891
-
892
- /**
893
- * @private
894
- */
895
- mapParameters() {
896
- const headersObj = new Headers();
897
- const headers = new URLParams();
898
- const queryParams = new URLParams();
899
- const pathParams = new URLParams();
900
-
901
- for (const param of this.step?.parameters || []) {
902
- const operationDetailParam =
903
- this.sourceDescriptionFile.operationDetails?.parameters
904
- .filter((obj) => obj.name === param.name && obj.in === param.in)
905
- .at(0);
906
-
907
- let value = this.expression.resolveExpression(param.value);
908
-
909
- switch (param.in) {
910
- case "header":
911
- let headerStyle = "simple";
912
- let headerExplode = false;
913
-
914
- if (
915
- operationDetailParam?.style &&
916
- ["accept", "authorization", "content-type"].includes(
917
- operationDetailParam.name.toLowerCase() === false,
918
- )
919
- ) {
920
- headerStyle = operationDetailParam.style;
921
- }
922
-
923
- if (
924
- operationDetailParam?.explode &&
925
- ["accept", "authorization", "content-type"].includes(
926
- operationDetailParam.name.toLowerCase() === false,
927
- )
928
- ) {
929
- headerExplode = operationDetailParam.explode;
930
- }
931
-
932
- if (this.operation.security) {
933
- // console.log(this.operation.security);
934
- for (const key in this.sourceDescriptionFile.securitySchemes) {
935
- if (
936
- this.sourceDescriptionFile.securitySchemes[key].type ===
937
- "http" &&
938
- param.name === key
939
- ) {
940
- if (
941
- this.sourceDescriptionFile.securitySchemes[
942
- key
943
- ].scheme.toLowerCase() === "bearer"
944
- ) {
945
- value = `Bearer ${value}`;
946
- } else {
947
- const basicPass = Buffer.from(
948
- this.expression.resolveExpression(value),
949
- ).toString("base64");
950
- value = `Basic ${basicPass}`;
951
- }
952
- }
953
- }
954
-
955
- // const authSchemaName = Object.keys(
956
- // this.operation.security.at(0),
957
- // ).at(0);
958
-
959
- // const securityScheme =
960
- // this.sourceDescriptionFile.securitySchemes[authSchemaName];
961
- // console.log(securityScheme);
962
-
963
- // if (
964
- // securityScheme.type === "http" &&
965
- // securityScheme?.scheme?.toLowerCase() === "bearer"
966
- // ) {
967
- // value = `Bearer ${value}`;
968
- // } else if (
969
- // securityScheme.type === "http" &&
970
- // securityScheme?.scheme?.toLowerCase() === "basic"
971
- // ) {
972
- // const basicPass = Buffer.from(
973
- // this.expression.resolveExpression(value),
974
- // ).toString("base64");
975
- // value = `Basic ${basicPass}`;
976
- // }
977
- }
978
-
979
- headers.append(param.name, value, {
980
- style: headerStyle,
981
- explode: headerExplode,
982
- });
983
-
984
- for (const [header, value] of headers) {
985
- if (header === param.name) {
986
- headersObj.append(param.name, value);
987
- }
988
- }
989
-
990
- break;
991
-
992
- case "path":
993
- const pathStyle = operationDetailParam?.style || "simple";
994
- const pathExplode = operationDetailParam?.explode || false;
995
-
996
- pathParams.append(param.name, value, {
997
- style: pathStyle,
998
- explode: pathExplode,
999
- });
1000
-
1001
- for (const [name, value] of pathParams.entries()) {
1002
- this.operation.url = this.operation.url.replace(`{${name}}`, value);
1003
- }
1004
-
1005
- break;
1006
-
1007
- case "query":
1008
- // queryParams.append(param.name, value);
1009
- const style = operationDetailParam?.style || "form";
1010
- let explode = false;
1011
- if (Object.hasOwn(operationDetailParam, "explode")) {
1012
- explode = operationDetailParam.explode;
1013
- } else {
1014
- if (style === "form") {
1015
- explode = true;
1016
- }
1017
- }
1018
- // const explode = operationDetailParam?.explode || false;
1019
- queryParams.append(param.name, value, {
1020
- style: style,
1021
- explode: explode,
1022
- });
1023
- break;
1024
- }
1025
- }
1026
-
1027
- this.addParamsToContext(pathParams, "path", "request");
1028
-
1029
- this.operation.headers = headersObj;
1030
- this.operation.queryParams = queryParams;
1031
- }
1032
-
1033
- buildHeaderParams() {}
1034
-
1035
- /**
1036
- * @private
1037
- * @param {*} params
1038
- * @param {*} paramType
1039
- * @param {*} contextType
1040
- */
1041
- addParamsToContext(params, paramType, contextType) {
1042
- const parameters = {};
1043
- for (const [key, value] of params.entries()) {
1044
- Object.assign(parameters, { [key]: value });
1045
- }
1046
-
1047
- this.expression.addToContext(contextType, { [paramType]: parameters });
1048
- }
1049
-
1050
- /**
1051
- * @private
1052
- */
1053
- mapRequestBody() {
1054
- if (this.step?.requestBody) {
1055
- const payload = this.expression.resolveExpression(
1056
- this.step.requestBody.payload,
1057
- );
1058
-
1059
- if (this.step.requestBody.contentType) {
1060
- this.operation.headers.append(
1061
- "accept",
1062
- this.step.requestBody.contentType,
1063
- );
1064
- }
1065
-
1066
- this.operation.data = payload;
1067
- }
1068
- }
1069
-
1070
624
  /**
1071
625
  * @private
1072
626
  */
@@ -0,0 +1,685 @@
1
+ 'use strict';
2
+
3
+ const URLParams = require("openapi-params");
4
+ const client = require("openid-client");
5
+
6
+ const fs = require("node:fs");
7
+ const https = require("node:https");
8
+ const path = require("node:path");
9
+
10
+ class Operation {
11
+ constructor(operation, sourceDescriptionFile, expression, inputs, logger, step) {
12
+ this.sourceDescriptionFile = sourceDescriptionFile;
13
+ this.expression = expression;
14
+ this.inputs = inputs;
15
+ this.logger = logger;
16
+ this.step = step;
17
+ this.operation = operation;
18
+ }
19
+
20
+ async runOperation() {
21
+ this.buildOperation()
22
+
23
+ return await this.makeRequest()
24
+ }
25
+
26
+ async makeRequest() {
27
+ const sleep = function (ms) {
28
+ return new Promise((resolve) => setTimeout(resolve, ms));
29
+ };
30
+
31
+ const parseRetryAfter = function (retryAfter) {
32
+ if (!retryAfter || typeof retryAfter !== "string") {
33
+ return null;
34
+ }
35
+
36
+ const trimmed = retryAfter.trim();
37
+
38
+ // Try parsing as a number (seconds format)
39
+ const asNumber = parseInt(trimmed, 10);
40
+ if (!isNaN(asNumber) && asNumber >= 0 && String(asNumber) === trimmed) {
41
+ return asNumber;
42
+ }
43
+
44
+ // Try parsing as HTTP date format
45
+ try {
46
+ const date = new Date(trimmed);
47
+
48
+ // Check if date is valid
49
+ if (isNaN(date.getTime())) {
50
+ return null;
51
+ }
52
+
53
+ // Calculate seconds from now until the date
54
+ const now = new Date();
55
+ const secondsUntil = Math.ceil((date.getTime() - now.getTime()) / 1000);
56
+
57
+ // Return the delay, but don't return negative values
58
+ return secondsUntil >= 0 ? secondsUntil : 0;
59
+ } catch (err) {
60
+ return null;
61
+ }
62
+ };
63
+
64
+ let url = this.operation.url;
65
+
66
+ if (this.operation.queryParams.size) {
67
+ url += `?${this.operation.queryParams}`;
68
+ }
69
+
70
+ const options = {
71
+ method: this.operation.method,
72
+ headers: this.operation.headers,
73
+ };
74
+
75
+ if (this.operation.data) {
76
+ options.body = this.operation.data;
77
+ }
78
+
79
+ if (this.retryAfter) {
80
+ this.logger.notice(
81
+ `retryAfter was set: waiting ${this.retryAfter * 1000} seconds`,
82
+ );
83
+ await sleep(this.retryAfter * 1000);
84
+ }
85
+
86
+ this.expression.addToContext("url", url);
87
+ this.expression.addToContext("method", options.method);
88
+
89
+ this.logger.notice(`Making a ${options.method.toUpperCase()} to ${url}`);
90
+
91
+ let response;
92
+ if (this.operation?.mutualTLS) {
93
+ this.logger.verbose("Using mutualTLS call");
94
+ response = await this.mutualTLS();
95
+ } else {
96
+ this.logger.verbose("Using fetch call");
97
+
98
+ this.logger.verbose(`request url: ${url}`);
99
+ this.logger.verbose(`request method: ${options.method}`);
100
+ this.logger.verbose("request headers:");
101
+ for (const [key, value] of options.headers.entries()) {
102
+ this.logger.verbose(`${key}: ${value}`);
103
+ }
104
+
105
+ this.logger.verbose(`body: ${options.body}`);
106
+
107
+ response = await fetch(url, options);
108
+ }
109
+
110
+ this.logger.verbose(`received StatusCode: ${response.status}`);
111
+ this.logger.verbose(`received headers:`);
112
+ for (const [key, value] of response.headers.entries()) {
113
+ this.logger.verbose(`${key}: ${value}`);
114
+ }
115
+
116
+ if (response.headers.has("retry-after")) {
117
+ const retryAfter = parseRetryAfter(response.headers.get("retry-after"));
118
+ if (retryAfter !== null) {
119
+ this.retryAfter = retryAfter;
120
+ }
121
+ }
122
+
123
+ this.addParamsToContext(response.headers, "header", "response");
124
+ this.expression.addToContext("statusCode", response.status);
125
+
126
+ this.logger.notice(`${url} responded with a: ${response.status}`);
127
+
128
+ await this.dealWithResponse(response);
129
+
130
+ return response;
131
+ }
132
+
133
+ async mutualTLS() {
134
+ let clientKeyPath;
135
+ try {
136
+ clientKeyPath = path.resolve(this.inputs.key);
137
+ this.logger.verbose(`clientKey Path: ${clientKeyPath}`);
138
+ } catch (err) {
139
+ this.logger.error(`could not resolve clientKey`);
140
+ throw err;
141
+ }
142
+
143
+ let clientCertPath;
144
+ try {
145
+ clientCertPath = path.resolve(this.inputs.cert);
146
+ this.logger.verbose(`clientCert Path: ${clientCertPath}`);
147
+ } catch (err) {
148
+ this.logger.error(`could not resolve clientCert`);
149
+ throw err;
150
+ }
151
+
152
+ let url = this.operation.url;
153
+
154
+ if (this.operation.queryParams.size) {
155
+ url += `?${this.operation.queryParams}`;
156
+ }
157
+
158
+ const opUrl = new URL(url);
159
+
160
+ this.logger.verbose(`url: ${opUrl.pathname + opUrl.search}`);
161
+ this.logger.verbose(`method: ${this.operation.method}`);
162
+ this.logger.verbose("headers:");
163
+ const headersObj = {};
164
+ for (const [key, value] of this.operation.headers.entries()) {
165
+ this.logger.verbose(`${key}: ${value}`);
166
+ Object.assign(headersObj, { [key]: value });
167
+ }
168
+
169
+ this.logger.verbose(`body: ${this.operation.data}`);
170
+
171
+ return new Promise((resolve, reject) => {
172
+ const options = {
173
+ key: fs.readFileSync(clientKeyPath),
174
+ cert: fs.readFileSync(clientCertPath),
175
+ method: this.operation.method,
176
+ headers: headersObj,
177
+ rejectUnauthorized: true,
178
+ hostname: opUrl.hostname,
179
+ path: opUrl.pathname + opUrl.search,
180
+ };
181
+
182
+ if (this.operation.data) {
183
+ options.headers = options.headers || {};
184
+ if (!options.headers["content-length"]) {
185
+ const bodyBuffer = Buffer.from(
186
+ typeof this.operation.data === "string"
187
+ ? this.operation.data
188
+ : JSON.stringify(this.operation.data),
189
+ );
190
+ options.headers["content-length"] = bodyBuffer.length;
191
+ }
192
+ }
193
+
194
+ const headers = new Headers();
195
+ const chunks = [];
196
+
197
+ const req = https.request(options, (res) => {
198
+ for (const [name, value] of Object.entries(res.headers)) {
199
+ headers.append(name, value);
200
+ }
201
+
202
+ // Collect data chunks as buffers for proper encoding
203
+ res.on("data", (chunk) => chunks.push(chunk));
204
+
205
+ res.on("end", () => {
206
+ // Concatenate buffers and decode based on content-type
207
+ const buffer = Buffer.concat(chunks);
208
+ let body;
209
+
210
+ const contentType = res.headers["content-type"] || "";
211
+
212
+ if (contentType.includes("application/json")) {
213
+ try {
214
+ body = JSON.parse(buffer.toString("utf8"));
215
+ } catch (err) {
216
+ body = buffer.toString("utf8");
217
+ }
218
+ } else {
219
+ body = buffer.toString("utf8");
220
+ }
221
+
222
+ const response = new Response(buffer, {
223
+ status: res.status,
224
+ statusText: res.statusMessage,
225
+ headers,
226
+ });
227
+ // resolve({
228
+ // headers: headers,
229
+ // body: body,
230
+
231
+ // status: res.statusCode,
232
+ // statusText: res.statusMessage,
233
+ // ok: res.statusCode >= 200 && res.statusCode < 300,
234
+ // });
235
+ resolve(response);
236
+ });
237
+ });
238
+
239
+ req.on("error", (err) => {
240
+ this.logger.error(`mTLS Error: ${err.message}`);
241
+ reject(new Error(`mTLS request failed: ${err.message}`));
242
+ });
243
+
244
+ // Write request body if present
245
+ if (this.operation.data) {
246
+ const body =
247
+ typeof this.operation.data === "string"
248
+ ? this.operation.data
249
+ : JSON.stringify(this.operation.data);
250
+ req.write(body);
251
+ }
252
+
253
+ req.end();
254
+ });
255
+ }
256
+
257
+ async dealWithResponse(response) {
258
+ if (
259
+ response.headers.has("Content-Type") &&
260
+ response.headers.get("Content-Type") === "application/json"
261
+ ) {
262
+ const json = await response?.json().catch((err) => {
263
+ this.logger.error(
264
+ `Error trying to resolve ${this.step.stepId} outputs`,
265
+ );
266
+ throw new Error(err);
267
+ });
268
+
269
+ this.expression.addToContext("response.body", json);
270
+ } else {
271
+ const body = await response.body;
272
+
273
+ this.expression.addToContext("response.body", body);
274
+ }
275
+ }
276
+
277
+ buildOperation() {
278
+ if (Object.keys(this.sourceDescriptionFile.securitySchemes).length === 1) {
279
+ for (const [key, value] of Object.entries(
280
+ this.sourceDescriptionFile.securitySchemes,
281
+ )) {
282
+ if (value.type.toLowerCase() === "mutualtls")
283
+ this.operation.mutualTLS = true;
284
+ }
285
+ }
286
+
287
+ this.mapInputs()
288
+ }
289
+
290
+ mapInputs() {
291
+ this.mapParameters();
292
+ this.encodeRequestBody();
293
+
294
+ this.addParamsToContext(this.operation.headers, "headers", "request");
295
+ this.addParamsToContext(this.operation.queryParams, "query", "request");
296
+ }
297
+
298
+ mapParameters() {
299
+ const headersObj = new Headers();
300
+ const headers = new URLParams();
301
+ const queryParams = new URLParams();
302
+ const pathParams = new URLParams();
303
+
304
+ for (const param of this.step?.parameters || []) {
305
+ const operationDetailParam =
306
+ this.sourceDescriptionFile.operationDetails?.parameters
307
+ .filter((obj) => obj.name === param.name && obj.in === param.in)
308
+ .at(0);
309
+
310
+ let value = this.expression.resolveExpression(param.value);
311
+
312
+ switch (param.in) {
313
+ case "header":
314
+ let headerStyle = "simple";
315
+ let headerExplode = false;
316
+
317
+ if (
318
+ operationDetailParam?.style &&
319
+ ["accept", "authorization", "content-type"].includes(
320
+ operationDetailParam.name.toLowerCase() === false,
321
+ )
322
+ ) {
323
+ headerStyle = operationDetailParam.style;
324
+ }
325
+
326
+ if (
327
+ operationDetailParam?.explode &&
328
+ ["accept", "authorization", "content-type"].includes(
329
+ operationDetailParam.name.toLowerCase() === false,
330
+ )
331
+ ) {
332
+ headerExplode = operationDetailParam.explode;
333
+ }
334
+
335
+ if (this.operation.security) {
336
+ // console.log(this.operation.security);
337
+ for (const key in this.sourceDescriptionFile.securitySchemes) {
338
+ if (
339
+ this.sourceDescriptionFile.securitySchemes[key].type ===
340
+ "http" &&
341
+ param.name === key
342
+ ) {
343
+ if (
344
+ this.sourceDescriptionFile.securitySchemes[
345
+ key
346
+ ].scheme.toLowerCase() === "bearer"
347
+ ) {
348
+ value = `Bearer ${value}`;
349
+ } else {
350
+ const basicPass = Buffer.from(
351
+ this.expression.resolveExpression(value),
352
+ ).toString("base64");
353
+ value = `Basic ${basicPass}`;
354
+ }
355
+ }
356
+ }
357
+
358
+ // const authSchemaName = Object.keys(
359
+ // this.operation.security.at(0),
360
+ // ).at(0);
361
+
362
+ // const securityScheme =
363
+ // this.sourceDescriptionFile.securitySchemes[authSchemaName];
364
+ // console.log(securityScheme);
365
+
366
+ // if (
367
+ // securityScheme.type === "http" &&
368
+ // securityScheme?.scheme?.toLowerCase() === "bearer"
369
+ // ) {
370
+ // value = `Bearer ${value}`;
371
+ // } else if (
372
+ // securityScheme.type === "http" &&
373
+ // securityScheme?.scheme?.toLowerCase() === "basic"
374
+ // ) {
375
+ // const basicPass = Buffer.from(
376
+ // this.expression.resolveExpression(value),
377
+ // ).toString("base64");
378
+ // value = `Basic ${basicPass}`;
379
+ // }
380
+ }
381
+
382
+ headers.append(param.name, value, {
383
+ style: headerStyle,
384
+ explode: headerExplode,
385
+ });
386
+
387
+ for (const [header, value] of headers) {
388
+ if (header === param.name) {
389
+ headersObj.append(param.name, value);
390
+ }
391
+ }
392
+
393
+ break;
394
+
395
+ case "path":
396
+ const pathStyle = operationDetailParam?.style || "simple";
397
+ const pathExplode = operationDetailParam?.explode || false;
398
+
399
+ pathParams.append(param.name, value, {
400
+ style: pathStyle,
401
+ explode: pathExplode,
402
+ });
403
+
404
+
405
+ for (const [name, value] of pathParams.entries()) {
406
+ this.operation.url = this.operation.url.replace(`{${name}}`, value);
407
+ }
408
+
409
+
410
+ break;
411
+
412
+ case "query":
413
+ // queryParams.append(param.name, value);
414
+ const style = operationDetailParam?.style || "form";
415
+ let explode = false;
416
+ if (Object.hasOwn(operationDetailParam, "explode")) {
417
+ explode = operationDetailParam.explode;
418
+ } else {
419
+ if (style === "form") {
420
+ explode = true;
421
+ }
422
+ }
423
+ // const explode = operationDetailParam?.explode || false;
424
+ queryParams.append(param.name, value, {
425
+ style: style,
426
+ explode: explode,
427
+ });
428
+ break;
429
+ }
430
+ }
431
+
432
+ this.addParamsToContext(pathParams, "path", "request");
433
+
434
+ this.operation.headers = headersObj;
435
+ this.operation.queryParams = queryParams;
436
+ }
437
+
438
+ addParamsToContext(params, paramType, contextType) {
439
+ const parameters = {};
440
+ for (const [key, value] of params.entries()) {
441
+ Object.assign(parameters, { [key]: value });
442
+ }
443
+
444
+ this.expression.addToContext(contextType, { [paramType]: parameters });
445
+ }
446
+
447
+ /**
448
+ * Encode request body based on Content-Type
449
+ * Returns { body } with properly encoded body and headers
450
+ *
451
+ * @param {*} data - The data to encode
452
+ * @param {string} contentType - The Content-Type header value
453
+ * @param {Headers} headers - Existing headers object (will be modified)
454
+ * @returns {{body: *, headers: Headers}} Encoded body and updated headers
455
+ */
456
+ encodeRequestBody() {
457
+ if (this.step?.requestBody) {
458
+ const payload = this.expression.resolveExpression(
459
+ this.step.requestBody.payload,
460
+ );
461
+
462
+ if (payload === null || payload === undefined) {
463
+ return { body: null };
464
+ }
465
+
466
+ // Normalize content type (remove charset, parameters)
467
+ // const normalizedType = contentType.split(';')[0].trim().toLowerCase();
468
+ let normalizedType
469
+ if (this.step.requestBody.contentType) {
470
+ normalizedType = this.step.requestBody.contentType.split(';')[0].trim().toLowerCase();
471
+ }
472
+
473
+ switch (normalizedType) {
474
+ case 'application/json':
475
+ this.encodeJSON(payload);
476
+ break;
477
+
478
+ case 'application/x-www-form-urlencoded':
479
+ this.encodeFormURLEncoded(payload);
480
+ break;
481
+
482
+ case 'multipart/form-data':
483
+ this.encodeMultipartFormData(payload);
484
+ break;
485
+
486
+ case 'text/plain':
487
+ this.encodeTextPlain(payload);
488
+ break;
489
+
490
+ case 'text/xml':
491
+ case 'application/xml':
492
+ this.encodeXML(payload, normalizedType);
493
+ break;
494
+
495
+ case 'text/html':
496
+ this.encodeHTML(payload);
497
+ break;
498
+
499
+ case 'application/octet-stream':
500
+ this.encodeBinary(payload);
501
+ break;
502
+
503
+ default:
504
+ // For unknown types, try JSON if it's an object, otherwise text
505
+ if (typeof payload === 'object' && !(payload instanceof Blob) && !(payload instanceof ArrayBuffer)) {
506
+ this.encodeJSON(payload);
507
+ }
508
+ this.encodeTextPlain(payload);
509
+ break;
510
+ }
511
+ }
512
+ }
513
+
514
+ /**
515
+ * Encode as JSON
516
+ */
517
+ encodeJSON(data) {
518
+ this.operation.data = typeof data === 'string' ? data : JSON.stringify(data);
519
+ this.operation.headers.set('Content-Type', 'application/json');
520
+ this.operation.headers.set('Content-Length', String(Buffer.byteLength(this.operation.data, 'utf8')));
521
+ }
522
+
523
+ /**
524
+ * Encode as application/x-www-form-urlencoded
525
+ */
526
+ encodeFormURLEncoded(data) {
527
+ let body;
528
+
529
+ if (typeof data === 'string') {
530
+ body = data;
531
+ } else if (data instanceof URLSearchParams) {
532
+ body = data.toString();
533
+ } else if (typeof data === 'object') {
534
+ const params = new URLSearchParams();
535
+ for (const [key, value] of Object.entries(data)) {
536
+ if (Array.isArray(value)) {
537
+ value.forEach(v => params.append(key, String(v)));
538
+ } else {
539
+ params.append(key, String(value));
540
+ }
541
+ }
542
+ body = params.toString();
543
+ } else {
544
+ body = String(data);
545
+ }
546
+
547
+ this.operation.headers.set('Content-Type', 'application/x-www-form-urlencoded');
548
+ this.operation.headers.set('Content-Length', String(Buffer.byteLength(body, 'utf8')));
549
+ this.operation.data = body;
550
+ }
551
+
552
+ /**
553
+ * Encode as multipart/form-data
554
+ */
555
+ encodeMultipartFormData(data) {
556
+ const formData = new FormData();
557
+
558
+ if (data instanceof FormData) {
559
+ this.operation.data = data; // Let fetch handle the boundary
560
+ return;
561
+ }
562
+
563
+ if (typeof data === 'object') {
564
+ for (const [key, value] of Object.entries(data)) {
565
+ if (value instanceof Blob || value instanceof File) {
566
+ formData.append(key, value);
567
+ } else if (Array.isArray(value)) {
568
+ value.forEach(v => formData.append(key, v));
569
+ } else {
570
+ formData.append(key, String(value));
571
+ }
572
+ }
573
+ }
574
+
575
+ // Do NOT set Content-Type - let fetch set it with the boundary
576
+ // headers.set('Content-Type', 'multipart/form-data'); // WRONG!
577
+ this.operation.headers.delete('Content-Type'); // Let fetch handle it
578
+
579
+ this.operation.data = formData;
580
+ }
581
+
582
+ /**
583
+ * Encode as plain text
584
+ */
585
+ encodeTextPlain(data) {
586
+ const body = typeof data === 'string' ? data : String(data);
587
+ this.operation.headers.set('Content-Type', 'text/plain; charset=utf-8');
588
+ this.operation.headers.set('Content-Length', String(Buffer.byteLength(body, 'utf8')));
589
+ this.operation.data = body;
590
+ }
591
+
592
+ /**
593
+ * Encode as XML
594
+ */
595
+ encodeXML(data, contentType) {
596
+ let body;
597
+
598
+ if (typeof data === 'string') {
599
+ body = data;
600
+ } else if (typeof data === 'object') {
601
+ // Simple object to XML conversion
602
+ body = this.objectToXML(data);
603
+ } else {
604
+ body = String(data);
605
+ }
606
+
607
+ this.operation.headers.set('Content-Type', `${contentType}; charset=utf-8`);
608
+ this.operation.headers.set('Content-Length', String(Buffer.byteLength(body, 'utf8')));
609
+ this.operation.data = body;
610
+ }
611
+
612
+ /**
613
+ * Encode as HTML
614
+ */
615
+ encodeHTML(data) {
616
+ const body = typeof data === 'string' ? data : String(data);
617
+ this.operation.headers.set('Content-Type', 'text/html; charset=utf-8');
618
+ this.operation.headers.set('Content-Length', String(Buffer.byteLength(body, 'utf8')));
619
+ this.operation.data = body;
620
+ }
621
+
622
+ /**
623
+ * Encode as binary (octet-stream)
624
+ */
625
+ encodeBinary(data) {
626
+ let body;
627
+
628
+ if (data instanceof Blob || data instanceof ArrayBuffer || Buffer.isBuffer(data)) {
629
+ body = data;
630
+ } else if (typeof data === 'string') {
631
+ body = Buffer.from(data);
632
+ } else {
633
+ body = Buffer.from(JSON.stringify(data));
634
+ }
635
+
636
+ this.operation.headers.set('Content-Type', 'application/octet-stream');
637
+
638
+ if (Buffer.isBuffer(body)) {
639
+ this.operation.headers.set('Content-Length', String(body.length));
640
+ }
641
+
642
+ this.operation.data = body;
643
+ }
644
+
645
+ /**
646
+ * Simple object to XML converter
647
+ */
648
+ objectToXML(obj, rootName = 'root') {
649
+ let xml = '<?xml version="1.0" encoding="UTF-8"?>';
650
+
651
+ function buildXML(data, tagName) {
652
+ if (data === null || data === undefined) {
653
+ return `<${tagName}/>`;
654
+ }
655
+
656
+ if (typeof data === 'object' && !Array.isArray(data)) {
657
+ let result = `<${tagName}>`;
658
+ for (const [key, value] of Object.entries(data)) {
659
+ result += buildXML(value, key);
660
+ }
661
+ result += `</${tagName}>`;
662
+ return result;
663
+ }
664
+
665
+ if (Array.isArray(data)) {
666
+ return data.map(item => buildXML(item, tagName)).join('');
667
+ }
668
+
669
+ // Escape XML special characters
670
+ const escaped = String(data)
671
+ .replace(/&/g, '&amp;')
672
+ .replace(/</g, '&lt;')
673
+ .replace(/>/g, '&gt;')
674
+ .replace(/"/g, '&quot;')
675
+ .replace(/'/g, '&apos;');
676
+
677
+ return `<${tagName}>${escaped}</${tagName}>`;
678
+ }
679
+
680
+ xml += buildXML(obj, rootName);
681
+ return xml;
682
+ }
683
+ }
684
+
685
+ module.exports = Operation