arazzo-runner 0.0.8 → 0.0.11

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
@@ -34,6 +34,9 @@ arazzo-runner -a arazzo.json -i input.json
34
34
  # With URL
35
35
  arazzo-runner -a https://example.com/arazzo.json -i ./input.json
36
36
 
37
+ # With Verbose Logging
38
+ arazzo-runner -a https://example.com/arazzo.json -i ./input.json --verbose
39
+
37
40
  # Show help
38
41
  arazzo-runner --help
39
42
 
@@ -65,7 +68,7 @@ Obviously, if you have a lot of secret variables that need adding as inputs, the
65
68
 
66
69
  ## OpenAPI Servers
67
70
 
68
- OpenAPI Documents allow you to specify servers at the root, [path](https://spec.openapis.org/oas/latest.html#path-item-object) and [operation](https://spec.openapis.org/oas/latest.html#operation-object) level. They allow you to specify multiple servers, however the OpenAPI specification is opinionated that all servers specified in a Document should return the same thing.
71
+ OpenAPI Documents allow you to specify servers at the root, [path](https://spec.openapis.org/oas/latest.html#path-item-object) and [operation](https://spec.openapis.org/oas/latest.html#operation-object) level. They allow you to specify multiple servers, however the OpenAPI specification is opinionated that all servers specified in a Document should return the same thing and this Arazzo Runner will follow this opinion and only attempt one of the specified servers.
69
72
 
70
73
  This Arazzo Runner will pick the first server it comes across in the array of servers and run the operation against that.
71
74
 
@@ -77,10 +80,114 @@ It will attempt to map to the [Server Variables](https://spec.openapis.org/oas/l
77
80
 
78
81
  ## OpenAPI Parameters
79
82
 
80
- OpenAPI Documents allow you to specify [`header`, `path` and `query` parameters](https://spec.openapis.org/oas/latest.html#parameter-object) in myriad of styles. This Arazzo Runner will respect your styling and send the format to the server as specified by your OpenAPI document.
83
+ OpenAPI Documents allow you to specify [`header`, `path` and `query` parameters](https://spec.openapis.org/oas/latest.html#parameter-object) in myriad of styles. This Arazzo Runner will respect your styling (unless you specify stylings for `Accept`, `Authorization` or `Content-Type` headers, then it will ignore the stylings, as per the OpenAPI specification) and send the format to the server as specified by your OpenAPI Document.
81
84
 
82
85
  It currently does not follow the `allowEmptyValue`, `allowReserved` or the `content` keywords currently.
83
86
 
87
+ ## OpenAPI Security
88
+
89
+ OpenAPI Document security is supported. There are a couple of ways that you will have to document your Arazzo Workflow for certain documentation types.
90
+
91
+ ### Basic
92
+
93
+ For HTTP Basic authentication, you should document your Arazzo like:
94
+
95
+ **arazzo.json**
96
+
97
+ ```json
98
+ "steps": [
99
+ {
100
+ "stepId": "deleteUser",
101
+ "operationId": "deleteUser",
102
+ "parameters": [
103
+ {
104
+ "name": "Authorization",
105
+ "in": "header",
106
+ "value": "{$inputs.username}:{$inputs.password}"
107
+ },
108
+ { "name": "username", "in": "path", "value": "$inputs.username" }
109
+ ]
110
+ }
111
+ ]
112
+ ```
113
+
114
+ The Runner will correctly encode and prepend `Basic` to the Authorization Header.
115
+
116
+ ### Bearer
117
+
118
+ For HTTP Bearer authentication, you should document your Arazzo like:
119
+
120
+ **arazzo.json**
121
+
122
+ ```json
123
+ {
124
+ "stepId": "LoginExistingUser",
125
+ "operationId": "loginUser",
126
+ "requestBody": {
127
+ "contentType": "application/json",
128
+ "payload": {
129
+ "username": "$inputs.username",
130
+ "password": "$inputs.password"
131
+ }
132
+ },
133
+ "outputs": { "AccessToken": "$response.body#/AccessToken" }
134
+ },
135
+ {
136
+ "stepId": "deleteUser",
137
+ "operationId": "deleteUser",
138
+ "parameters": [
139
+ {
140
+ "name": "Authorization",
141
+ "in": "header",
142
+ "value": "$steps.LoginExistingUser.outputs.AccessToken"
143
+ },
144
+ { "name": "username", "in": "path", "value": "$inputs.username" }
145
+ ]
146
+ }
147
+ ```
148
+
149
+ The Runner will prepend `Bearer` for you.
150
+
151
+ ### mutualTLS
152
+
153
+ > mutualTLS is quite a complex authorization topic. I have written a naive way of dealing with it that I am unsure will actually work in production. If you are using mutualTLS and this Arazzo Runner and find that you run into bugs/issues, please do feel free to opena. report. The more I know and understand mutualTLS the better I can support it.
154
+
155
+ You will need to provide inputs for your ClientKey and ClientCert as their path locations:
156
+
157
+ **input.json**
158
+
159
+ ```json
160
+ {
161
+ "deleteCurrentUser-mutualTLS": {
162
+ "username": "jack",
163
+ "key": "./client-key.pem",
164
+ "cert": "./client-cert.pem"
165
+ }
166
+ }
167
+ ```
168
+
169
+ `key` and `cert` are reserved names when used in an OpenAPI Document with `mutualTLS` as the authentication method. The Runner will error out if they are not found.
170
+
171
+ ### UNSUPPORTED oauth/openId
172
+
173
+ **CURRENTLY UNSUPPORTED**
174
+
175
+ You will need to provide inputs for your clientId and clientSecret:
176
+
177
+ **input.json**
178
+
179
+ ```json
180
+ {
181
+ "deleteCurrentUser-mutualTLS": {
182
+ "username": "jack",
183
+ "clientId": "abc123",
184
+ "clientSecret": "123abc"
185
+ }
186
+ }
187
+ ```
188
+
189
+ `clientId` and `clientSecret` are reserved name and will be used when oauth or openId authentication is set.
190
+
84
191
  ## Logging And Reporting
85
192
 
86
193
  ### Logging
@@ -111,10 +218,6 @@ Work on Reporting still needs completeing.
111
218
 
112
219
  ## Still unsupported
113
220
 
114
- ### Security
115
-
116
- OpenAPI security is still not fully supported
117
-
118
221
  ### PathOperation
119
222
 
120
223
  Accessing an OpenAPI operation by Operation Path `'{$sourceDescriptions.petstoreDescription.url}#/paths/~1pet~1findByStatus/get'` does not work currently
package/cli.js CHANGED
@@ -17,6 +17,9 @@ const options = {
17
17
  type: "string",
18
18
  short: "i",
19
19
  },
20
+ verbose: {
21
+ type: "boolean",
22
+ },
20
23
  help: {
21
24
  type: "boolean",
22
25
  short: "h",
@@ -40,6 +43,7 @@ OPTIONS:
40
43
  -i, --input <file> Path to input JSON file (required)
41
44
  -h, --help Show this help message
42
45
  -v, --version Show version number
46
+ -vv, --verbose Verbose Logging
43
47
 
44
48
  EXAMPLES:
45
49
  # Run with local files
@@ -139,7 +143,7 @@ async function main() {
139
143
  console.log("");
140
144
 
141
145
  try {
142
- const runner = new Runner();
146
+ const runner = new Runner({ verbose: values?.verbose || false });
143
147
  await runner.runArazzo(arazzoPath, inputPath);
144
148
 
145
149
  console.log("");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arazzo-runner",
3
- "version": "0.0.8",
3
+ "version": "0.0.11",
4
4
  "description": "A runner to run through Arazzo Document workflows",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -43,6 +43,7 @@
43
43
  "jsonpath": "^1.1.1",
44
44
  "openapi-params": "^0.0.5",
45
45
  "openapi-server-url-templating": "^1.3.0",
46
+ "openid-client": "^6.8.1",
46
47
  "stream-chain": "^3.4.0",
47
48
  "stream-json": "^1.9.1"
48
49
  }
package/src/Arazzo.js CHANGED
@@ -1,11 +1,13 @@
1
1
  "use strict";
2
2
 
3
3
  const URLParams = require("openapi-params");
4
+ const client = require("openid-client");
4
5
 
6
+ const fs = require("node:fs");
7
+ const https = require("node:https");
5
8
  const path = require("node:path");
6
9
 
7
10
  const Document = require("./Document");
8
- // const docFactory = require("./DocFactory");
9
11
  const Expression = require("./Expression");
10
12
  const Rules = require("./Rules");
11
13
 
@@ -252,6 +254,17 @@ class Arazzo extends Document {
252
254
  this.step,
253
255
  );
254
256
 
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
+
255
268
  this.mapInputs();
256
269
 
257
270
  await this.runOperation();
@@ -328,7 +341,24 @@ class Arazzo extends Document {
328
341
 
329
342
  this.logger.notice(`Making a ${options.method.toUpperCase()} to ${url}`);
330
343
 
331
- const response = await fetch(url, options);
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(`url: ${url}`);
352
+ this.logger.verbose(`method: ${options.method}`);
353
+ this.logger.verbose("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
+ }
332
362
 
333
363
  if (response.headers.has("retry-after")) {
334
364
  const retryAfter = parseRetryAfter(response.headers.get("retry-after"));
@@ -345,6 +375,130 @@ class Arazzo extends Document {
345
375
  await this.dealWithResponse(response);
346
376
  }
347
377
 
378
+ async mutualTLS() {
379
+ let clientKeyPath;
380
+ try {
381
+ clientKeyPath = path.resolve(this.inputs.key);
382
+ this.logger.verbose(`clientKey Path: ${clientKeyPath}`);
383
+ } catch (err) {
384
+ this.logger.error(`could not resolve clientKey`);
385
+ throw err;
386
+ }
387
+
388
+ let clientCertPath;
389
+ try {
390
+ clientCertPath = path.resolve(this.inputs.cert);
391
+ this.logger.verbose(`clientCert Path: ${clientCertPath}`);
392
+ } catch (err) {
393
+ this.logger.error(`could not resolve clientCert`);
394
+ throw err;
395
+ }
396
+
397
+ let url = this.operation.url;
398
+
399
+ if (this.operation.queryParams.size) {
400
+ url += `?${this.operation.queryParams}`;
401
+ }
402
+
403
+ const opUrl = new URL(url);
404
+
405
+ this.logger.verbose(`url: ${opUrl.pathname + opUrl.search}`);
406
+ this.logger.verbose(`method: ${this.operation.method}`);
407
+ this.logger.verbose("headers:");
408
+ const headersObj = {};
409
+ for (const [key, value] of this.operation.headers.entries()) {
410
+ this.logger.verbose(`${key}: ${value}`);
411
+ Object.assign(headersObj, { [key]: value });
412
+ }
413
+
414
+ this.logger.verbose(`body: ${this.operation.data}`);
415
+
416
+ return new Promise((resolve, reject) => {
417
+ const options = {
418
+ key: fs.readFileSync(clientKeyPath),
419
+ cert: fs.readFileSync(clientCertPath),
420
+ method: this.operation.method,
421
+ headers: headersObj,
422
+ rejectUnauthorized: true,
423
+ hostname: opUrl.hostname,
424
+ path: opUrl.pathname + opUrl.search,
425
+ };
426
+
427
+ if (this.operation.data) {
428
+ options.headers = options.headers || {};
429
+ if (!options.headers["content-length"]) {
430
+ const bodyBuffer = Buffer.from(
431
+ typeof this.operation.data === "string"
432
+ ? this.operation.data
433
+ : JSON.stringify(this.operation.data),
434
+ );
435
+ options.headers["content-length"] = bodyBuffer.length;
436
+ }
437
+ }
438
+
439
+ const headers = new Headers();
440
+ const chunks = [];
441
+
442
+ const req = https.request(options, (res) => {
443
+ for (const [name, value] of Object.entries(res.headers)) {
444
+ headers.append(name, value);
445
+ }
446
+
447
+ // Collect data chunks as buffers for proper encoding
448
+ res.on("data", (chunk) => chunks.push(chunk));
449
+
450
+ res.on("end", () => {
451
+ // Concatenate buffers and decode based on content-type
452
+ const buffer = Buffer.concat(chunks);
453
+ let body;
454
+
455
+ const contentType = res.headers["content-type"] || "";
456
+
457
+ if (contentType.includes("application/json")) {
458
+ try {
459
+ body = JSON.parse(buffer.toString("utf8"));
460
+ } catch (err) {
461
+ body = buffer.toString("utf8");
462
+ }
463
+ } else {
464
+ body = buffer.toString("utf8");
465
+ }
466
+
467
+ const response = new Response(buffer, {
468
+ status: res.status,
469
+ statusText: res.statusMessage,
470
+ headers,
471
+ });
472
+ // resolve({
473
+ // headers: headers,
474
+ // body: body,
475
+
476
+ // status: res.statusCode,
477
+ // statusText: res.statusMessage,
478
+ // ok: res.statusCode >= 200 && res.statusCode < 300,
479
+ // });
480
+ resolve(response);
481
+ });
482
+ });
483
+
484
+ req.on("error", (err) => {
485
+ this.logger.error(`mTLS Error: ${err.message}`);
486
+ reject(new Error(`mTLS request failed: ${err.message}`));
487
+ });
488
+
489
+ // Write request body if present
490
+ if (this.operation.data) {
491
+ const body =
492
+ typeof this.operation.data === "string"
493
+ ? this.operation.data
494
+ : JSON.stringify(this.operation.data);
495
+ req.write(body);
496
+ }
497
+
498
+ req.end();
499
+ });
500
+ }
501
+
348
502
  /**
349
503
  * @private
350
504
  * @param {*} response
@@ -691,7 +845,6 @@ class Arazzo extends Document {
691
845
  */
692
846
  async dealWithStepOutputs(response) {
693
847
  const json = await response?.json().catch((err) => {
694
- console.error(err);
695
848
  this.logger.error(`Error trying to resolve ${this.step.stepId} outputs`);
696
849
  throw new Error(err);
697
850
  });
@@ -736,16 +889,83 @@ class Arazzo extends Document {
736
889
  .filter((obj) => obj.name === param.name && obj.in === param.in)
737
890
  .at(0);
738
891
 
739
- const value = this.expression.resolveExpression(param.value);
892
+ let value = this.expression.resolveExpression(param.value);
740
893
 
741
894
  switch (param.in) {
742
895
  case "header":
743
- const headerStyle = operationDetailParam?.style || "simple";
744
- const headerExplode = operationDetailParam?.explode || false;
896
+ let headerStyle = "simple";
897
+ let headerExplode = false;
898
+
899
+ if (
900
+ operationDetailParam?.style &&
901
+ ["accept", "authorization", "content-type"].includes(
902
+ operationDetailParam.name.toLowerCase() === false,
903
+ )
904
+ ) {
905
+ headerStyle = operationDetailParam.style;
906
+ }
907
+
908
+ if (
909
+ operationDetailParam?.explode &&
910
+ ["accept", "authorization", "content-type"].includes(
911
+ operationDetailParam.name.toLowerCase() === false,
912
+ )
913
+ ) {
914
+ headerExplode = operationDetailParam.explode;
915
+ }
916
+
917
+ if (this.operation.security) {
918
+ // console.log(this.operation.security);
919
+ for (const key in this.sourceDescriptionFile.securitySchemes) {
920
+ if (
921
+ this.sourceDescriptionFile.securitySchemes[key].type ===
922
+ "http" &&
923
+ param.name === key
924
+ ) {
925
+ if (
926
+ this.sourceDescriptionFile.securitySchemes[
927
+ key
928
+ ].scheme.toLowerCase() === "bearer"
929
+ ) {
930
+ value = `Bearer ${value}`;
931
+ } else {
932
+ const basicPass = Buffer.from(
933
+ this.expression.resolveExpression(value),
934
+ ).toString("base64");
935
+ value = `Basic ${basicPass}`;
936
+ }
937
+ }
938
+ }
939
+
940
+ // const authSchemaName = Object.keys(
941
+ // this.operation.security.at(0),
942
+ // ).at(0);
943
+
944
+ // const securityScheme =
945
+ // this.sourceDescriptionFile.securitySchemes[authSchemaName];
946
+ // console.log(securityScheme);
947
+
948
+ // if (
949
+ // securityScheme.type === "http" &&
950
+ // securityScheme?.scheme?.toLowerCase() === "bearer"
951
+ // ) {
952
+ // value = `Bearer ${value}`;
953
+ // } else if (
954
+ // securityScheme.type === "http" &&
955
+ // securityScheme?.scheme?.toLowerCase() === "basic"
956
+ // ) {
957
+ // const basicPass = Buffer.from(
958
+ // this.expression.resolveExpression(value),
959
+ // ).toString("base64");
960
+ // value = `Basic ${basicPass}`;
961
+ // }
962
+ }
963
+
745
964
  headers.append(param.name, value, {
746
965
  style: headerStyle,
747
966
  explode: headerExplode,
748
967
  });
968
+
749
969
  for (const [header, value] of headers) {
750
970
  if (header === param.name) {
751
971
  headersObj.append(param.name, value);
@@ -795,6 +1015,8 @@ class Arazzo extends Document {
795
1015
  this.operation.queryParams = queryParams;
796
1016
  }
797
1017
 
1018
+ buildHeaderParams() {}
1019
+
798
1020
  /**
799
1021
  * @private
800
1022
  * @param {*} params
package/src/DocFactory.js CHANGED
@@ -6,6 +6,21 @@ const OpenAPI = require("./OpenAPI");
6
6
  class DocumentFactory {
7
7
  constructor() {}
8
8
 
9
+ /**
10
+ * Tests whether a string is a URL or not
11
+ * @private
12
+ * @param {string} str
13
+ * @returns
14
+ */
15
+ isUrl(str) {
16
+ try {
17
+ new URL(str);
18
+ return true;
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
23
+
9
24
  /**
10
25
  * Document Factory to create Arazzo or OpenAPI documents
11
26
  * @function buildDocument
@@ -23,7 +38,9 @@ class DocumentFactory {
23
38
  document = new Arazzo(path, name, options, this);
24
39
  }
25
40
 
26
- await document.loadDocument();
41
+ if (this.isUrl(path)) {
42
+ await document.loadDocument();
43
+ } else document.setMainArazzo();
27
44
 
28
45
  return document;
29
46
  }
package/src/Expression.js CHANGED
@@ -1,5 +1,11 @@
1
1
  "use strict";
2
2
 
3
+ const {
4
+ extractAll,
5
+ extractAndResolve,
6
+ hasRuntimeExpressions,
7
+ } = require("./runtime");
8
+
3
9
  const {
4
10
  parse,
5
11
  test,
@@ -193,6 +199,49 @@ class Expression {
193
199
  }
194
200
  }
195
201
 
202
+ /**
203
+ * Resolves a runtime expression to its value in the context
204
+ * @public
205
+ * @param {string|Object|Array} expression - The runtime expression to resolve
206
+ * @returns {*} The resolved value
207
+ * @throws {Error} If expression is invalid or context path doesn't exist
208
+ */
209
+ // resolveExpression(expression) {
210
+ // // Handle arrays recursively
211
+ // if (Array.isArray(expression)) {
212
+ // return expression.map((item) => this.resolveExpression(item));
213
+ // }
214
+
215
+ // // Handle objects recursively
216
+ // if (typeof expression === "object" && expression !== null) {
217
+ // const resolved = {};
218
+ // for (const [key, value] of Object.entries(expression)) {
219
+ // resolved[key] = this.resolveExpression(value);
220
+ // }
221
+ // return resolved;
222
+ // }
223
+
224
+ // // Handle strings (runtime expressions)
225
+ // if (typeof expression !== "string") {
226
+ // return expression;
227
+ // }
228
+
229
+ // this.expression = expression;
230
+
231
+ // if (this.isARunTimeExpression()) {
232
+ // return this.mapToContext();
233
+ // }
234
+
235
+ // const extractedExpression = extract(expression);
236
+
237
+ // if (extractedExpression !== expression && test(extractedExpression)) {
238
+ // this.expression = extractedExpression;
239
+ // return this.mapToContext();
240
+ // }
241
+
242
+ // return expression;
243
+ // }
244
+
196
245
  /**
197
246
  * Resolves a runtime expression to its value in the context
198
247
  * @public
@@ -222,14 +271,42 @@ class Expression {
222
271
 
223
272
  this.expression = expression;
224
273
 
274
+ // Check if it's a runtime expression
225
275
  if (this.isARunTimeExpression()) {
226
276
  return this.mapToContext();
227
277
  }
228
278
 
229
- const extractedExpression = extract(expression);
230
- if (extractedExpression !== expression && test(extractedExpression)) {
231
- this.expression = extractedExpression;
232
- return this.mapToContext();
279
+ // NEW: Handle multiple templated expressions
280
+ const runtimeExprRegex = /{(\$[^}]+)}/g;
281
+ const matches = [...expression.matchAll(runtimeExprRegex)];
282
+
283
+ if (matches.length > 0) {
284
+ // If entire string is single expression, return resolved value
285
+ if (matches.length === 1 && matches[0][0] === expression) {
286
+ const extractedExpression = matches[0][1];
287
+ if (test(extractedExpression)) {
288
+ this.expression = extractedExpression;
289
+ return this.mapToContext();
290
+ }
291
+ }
292
+
293
+ // Multiple expressions - substitute each one
294
+ let result = expression;
295
+ for (const match of matches) {
296
+ const fullMatch = match[0];
297
+ const expr = match[1];
298
+
299
+ if (test(expr)) {
300
+ this.expression = expr;
301
+ const resolvedValue = this.mapToContext();
302
+ const stringValue =
303
+ typeof resolvedValue === "object"
304
+ ? JSON.stringify(resolvedValue)
305
+ : String(resolvedValue);
306
+ result = result.replace(fullMatch, stringValue);
307
+ }
308
+ }
309
+ return result;
233
310
  }
234
311
 
235
312
  return expression;
package/src/Logger.js CHANGED
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
 
3
3
  class Logger {
4
- constructor() {
4
+ constructor(verboseLogging) {
5
+ this.verboseLogging = verboseLogging || false;
5
6
  this.logOutput = {
6
7
  debug: (message) => console.debug(message),
7
8
  error: (message) => console.error(`❌ ${message}`),
@@ -50,7 +51,7 @@ class Logger {
50
51
  }
51
52
 
52
53
  verbose(str) {
53
- this.log(str, this.logTypes.VERBOSE);
54
+ if (this.verboseLogging) this.log(str, this.logTypes.VERBOSE);
54
55
  }
55
56
 
56
57
  warning(str) {
package/src/OpenAPI.js CHANGED
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
 
3
+ const { evaluate } = require("@swaggerexpert/json-pointer");
3
4
  const { substitute, parse } = require("openapi-server-url-templating");
4
5
 
5
6
  const Document = require("./Document");
@@ -43,7 +44,7 @@ class OpenAPI extends Document {
43
44
  }
44
45
  }
45
46
 
46
- this.buildOperations();
47
+ await this.buildOperationDetails();
47
48
 
48
49
  return this.operation;
49
50
  }
@@ -56,24 +57,26 @@ class OpenAPI extends Document {
56
57
  }
57
58
  }
58
59
 
59
- async getSecurity() {
60
+ async getSecuritySchemes() {
61
+ const componentPipeline = await this.JSONPicker(
62
+ "components",
63
+ this.filePath,
64
+ );
65
+
66
+ for await (const { value } of componentPipeline) {
67
+ if (value.securitySchemes) {
68
+ console.log(value.securitySchemes);
69
+ this.securitySchemes = value.securitySchemes;
70
+ }
71
+ }
72
+ }
73
+
74
+ async getGlobalSecurity() {
60
75
  const pipeline = await this.JSONPicker("security", this.filePath);
61
76
 
62
77
  for await (const { value } of pipeline) {
63
78
  if (value) this.security = value;
64
79
  }
65
-
66
- if (this.security) {
67
- const componentPipeline = await this.JSONPicker(
68
- "components",
69
- this.filePath,
70
- );
71
- for await (const { value } of componentPipeline) {
72
- if (value.securitySchemes) {
73
- console.log(value.securitySchemes);
74
- }
75
- }
76
- }
77
80
  }
78
81
 
79
82
  parseServer() {
@@ -103,7 +106,27 @@ class OpenAPI extends Document {
103
106
  return server;
104
107
  }
105
108
 
106
- buildOperations() {
109
+ async buildOperationSecurity() {
110
+ if (!this.securitySchemes) {
111
+ await this.getSecuritySchemes();
112
+ }
113
+
114
+ if (!this.security) {
115
+ await this.getGlobalSecurity();
116
+ }
117
+
118
+ if (this.security) {
119
+ this.globalSecurity = true;
120
+ }
121
+
122
+ if (this.operationDetails?.security) {
123
+ this.operation.security = [this.operationDetails.security.at(0)];
124
+ } else {
125
+ if (this.security) this.operation.security = [this.security.at(0)];
126
+ }
127
+ }
128
+
129
+ async buildOperationDetails() {
107
130
  this.operation = {};
108
131
 
109
132
  const server = this.parseServer();
@@ -112,6 +135,8 @@ class OpenAPI extends Document {
112
135
  url: `${server.url}${this.path}`,
113
136
  method: this.method,
114
137
  });
138
+
139
+ await this.buildOperationSecurity();
115
140
  }
116
141
  }
117
142
 
package/src/Runner.js CHANGED
@@ -6,8 +6,9 @@ const Input = require("./Input");
6
6
  const Logger = require("./Logger");
7
7
 
8
8
  class Runner {
9
- constructor(logger) {
10
- this.logger = logger || new Logger();
9
+ constructor(options, logger) {
10
+ const verboseLogging = options?.verbose || false;
11
+ this.logger = logger || new Logger(verboseLogging);
11
12
  }
12
13
 
13
14
  /**
package/src/runtime.js ADDED
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Extract and resolve multiple runtime expressions from a template string
3
+ *
4
+ * @param {string} template - Template string with runtime expressions like "{$inputs.user}"
5
+ * @param {Function} resolver - Function to resolve each runtime expression
6
+ * @returns {string} The template with all expressions resolved
7
+ *
8
+ * @example
9
+ * const result = extractAndResolve(
10
+ * "{$inputs.user} has a pet called {$inputs.petName}",
11
+ * (expr) => resolveExpression(expr)
12
+ * );
13
+ * // "Jack has a pet called Jill"
14
+ */
15
+ function extractAndResolve(template, resolver) {
16
+ if (typeof template !== "string") {
17
+ return template;
18
+ }
19
+
20
+ // Match all runtime expressions wrapped in {}
21
+ const runtimeExpressionRegex = /{(\$[^}]+)}/g;
22
+
23
+ let result = template;
24
+ const matches = [...template.matchAll(runtimeExpressionRegex)];
25
+
26
+ if (matches.length === 0) {
27
+ return template;
28
+ }
29
+
30
+ // If the entire string is a single expression like "{$url}", return the resolved value
31
+ if (matches.length === 1 && matches[0][0] === template) {
32
+ return resolver(matches[0][1]);
33
+ }
34
+
35
+ // Multiple expressions or mixed template - replace each one
36
+ for (const match of matches) {
37
+ const fullMatch = match[0]; // e.g., "{$inputs.user}"
38
+ const expression = match[1]; // e.g., "$inputs.user"
39
+
40
+ const resolvedValue = resolver(expression);
41
+
42
+ // Convert resolved value to string for template substitution
43
+ const stringValue =
44
+ typeof resolvedValue === "object"
45
+ ? JSON.stringify(resolvedValue)
46
+ : String(resolvedValue);
47
+
48
+ result = result.replace(fullMatch, stringValue);
49
+ }
50
+
51
+ return result;
52
+ }
53
+
54
+ /**
55
+ * Check if a string contains any runtime expressions
56
+ *
57
+ * @param {string} str - String to check
58
+ * @returns {boolean} True if the string contains runtime expressions
59
+ */
60
+ function hasRuntimeExpressions(str) {
61
+ if (typeof str !== "string") {
62
+ return false;
63
+ }
64
+ return /{(\$[^}]+)}/g.test(str);
65
+ }
66
+
67
+ /**
68
+ * Extract all runtime expressions from a template string
69
+ *
70
+ * @param {string} template - Template string
71
+ * @returns {Array<string>} Array of runtime expressions (without the {} wrapper)
72
+ */
73
+ function extractAll(template) {
74
+ if (typeof template !== "string") {
75
+ return [];
76
+ }
77
+
78
+ const runtimeExpressionRegex = /{(\$[^}]+)}/g;
79
+ const matches = [...template.matchAll(runtimeExpressionRegex)];
80
+
81
+ return matches.map((match) => match[1]);
82
+ }
83
+
84
+ // Example usage
85
+ // const Expression = require("./Expression");
86
+
87
+ // const expression = new Expression();
88
+ // expression.addToContext("inputs", {
89
+ // user: { name: "Jack" },
90
+ // petName: "Jill",
91
+ // pet_id: 1,
92
+ // coupon_code: "abc-def",
93
+ // quantity: 1,
94
+ // });
95
+
96
+ // // Example 1: Multiple expressions in a sentence
97
+ // const result1 = extractAndResolve(
98
+ // "{$inputs.user} has a pet called {$inputs.petName}",
99
+ // (expr) => expression.resolveExpression(expr),
100
+ // );
101
+ // console.log(result1);
102
+ // // Output: {"name":"Jack"} has a pet called Jill
103
+
104
+ // // Example 2: Single expression (returns the resolved value directly)
105
+ // const result2 = extractAndResolve("{$inputs.user}", (expr) =>
106
+ // expression.resolveExpression(expr),
107
+ // );
108
+ // console.log(result2);
109
+ // // Output: { name: 'Jack' }
110
+
111
+ // // Example 3: JSON template with multiple expressions
112
+ // const result3 = extractAndResolve(
113
+ // '{\n "petOrder": {\n "petId": "{$inputs.pet_id}",\n "couponCode": "{$inputs.coupon_code}",\n "quantity": "{$inputs.quantity}"\n }\n}',
114
+ // (expr) => expression.resolveExpression(expr),
115
+ // );
116
+ // console.log(result3);
117
+
118
+ // // Helper functions
119
+ // console.log("\nHelper examples:");
120
+ // console.log("Has expressions:", hasRuntimeExpressions("{$inputs.user} test"));
121
+ // console.log("Extract all:", extractAll("{$inputs.user} and {$inputs.petName}"));
122
+
123
+ module.exports = {
124
+ extractAndResolve,
125
+ hasRuntimeExpressions,
126
+ extractAll,
127
+ };