@usearazzo/runner 1.0.1-alpha.12 → 1.0.1-alpha.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usearazzo/runner",
3
- "version": "1.0.1-alpha.12",
3
+ "version": "1.0.1-alpha.14",
4
4
  "description": "Runner for Arazzo Workflows.",
5
5
  "keywords": [
6
6
  "arazzo",
@@ -69,11 +69,12 @@
69
69
  "@speclynx/apidom-traverse": "^6.1.0",
70
70
  "@swaggerexpert/arazzo-criterion": "^1.0.1",
71
71
  "@swaggerexpert/arazzo-runtime-expression": "^3.3.0",
72
+ "@swaggerexpert/cookie": "^2.0.2",
72
73
  "@swaggerexpert/json-pointer": "^3.0.2",
73
74
  "@swaggerexpert/jsonpath": "^4.0.5",
74
75
  "@swaggerexpert/media-types": "^1.0.1",
75
- "@usearazzo/parser": "1.0.1-alpha.12",
76
- "@usearazzo/resolver": "1.0.1-alpha.12",
76
+ "@usearazzo/parser": "1.0.1-alpha.14",
77
+ "@usearazzo/resolver": "1.0.1-alpha.14",
77
78
  "@xmldom/xmldom": "^0.9.12",
78
79
  "fontoxpath": "^3.34.0",
79
80
  "ramda": "^0.32.0",
@@ -93,5 +94,5 @@
93
94
  "devDependencies": {
94
95
  "swagger-client": "^3.38.4"
95
96
  },
96
- "gitHead": "cc5d9b2fb744662650068a5993ebcdd19e8568c2"
97
+ "gitHead": "40a494596c0eed2f984fb87f544f3cf69488a6a7"
97
98
  }
@@ -75,6 +75,14 @@ class ParameterIdentity {
75
75
  if (location === undefined || (0, _apidomDatamodel.isNullElement)(location)) return undefined;
76
76
  return (0, _apidomDatamodel.isStringElement)(location) ? (0, _apidomCore.toValue)(location) : null;
77
77
  }
78
+
79
+ /**
80
+ * The locations the Parameter Object defines: Arazzo 1.0.1's closed set plus
81
+ * 1.1.0's `querystring`. 1.0.0's field description also listed `body`, which
82
+ * its own summary of locations did not; 1.0.1 removed it as an erratum, so it
83
+ * is not a location in any version (a payload is the step's `requestBody`).
84
+ */
85
+ static LOCATIONS = ['path', 'query', 'header', 'cookie', 'querystring'];
78
86
  name;
79
87
  location;
80
88
  constructor(name, location) {
@@ -82,6 +90,14 @@ class ParameterIdentity {
82
90
  this.location = location;
83
91
  }
84
92
 
93
+ /**
94
+ * Whether the location is one the Parameter Object defines. An absent or
95
+ * malformed location is not.
96
+ */
97
+ get hasDefinedLocation() {
98
+ return ParameterIdentity.LOCATIONS.includes(this.location);
99
+ }
100
+
85
101
  /**
86
102
  * Whether this and the other identity name the same parameter. A malformed
87
103
  * location equals nothing, its own duplicate included.
@@ -70,6 +70,14 @@ class ParameterIdentity {
70
70
  if (location === undefined || isNullElement(location)) return undefined;
71
71
  return isStringElement(location) ? toValue(location) : null;
72
72
  }
73
+
74
+ /**
75
+ * The locations the Parameter Object defines: Arazzo 1.0.1's closed set plus
76
+ * 1.1.0's `querystring`. 1.0.0's field description also listed `body`, which
77
+ * its own summary of locations did not; 1.0.1 removed it as an erratum, so it
78
+ * is not a location in any version (a payload is the step's `requestBody`).
79
+ */
80
+ static LOCATIONS = ['path', 'query', 'header', 'cookie', 'querystring'];
73
81
  name;
74
82
  location;
75
83
  constructor(name, location) {
@@ -77,6 +85,14 @@ class ParameterIdentity {
77
85
  this.location = location;
78
86
  }
79
87
 
88
+ /**
89
+ * Whether the location is one the Parameter Object defines. An absent or
90
+ * malformed location is not.
91
+ */
92
+ get hasDefinedLocation() {
93
+ return ParameterIdentity.LOCATIONS.includes(this.location);
94
+ }
95
+
80
96
  /**
81
97
  * Whether this and the other identity name the same parameter. A malformed
82
98
  * location equals nothing, its own duplicate included.
@@ -9,6 +9,7 @@ var _apidomCore = require("@speclynx/apidom-core");
9
9
  var _apidomDatamodel = require("@speclynx/apidom-datamodel");
10
10
  var _empty = require("@speclynx/apidom-reference/configuration/empty");
11
11
  var _apidomNsOpenapi = require("@speclynx/apidom-ns-openapi-2");
12
+ var _cookie = require("@swaggerexpert/cookie");
12
13
  var _mediaTypes = require("@swaggerexpert/media-types");
13
14
  var _ramdaAdjunct = require("ramda-adjunct");
14
15
  var _swaggerClient = require("../vendor/swagger-client.cjs");
@@ -30,6 +31,21 @@ var _ClientError = _interopRequireDefault(require("../errors/ClientError.cjs"));
30
31
  * @public
31
32
  */
32
33
 
34
+ /**
35
+ * The plain-JSON shape of the assembled document, as far as declaring a
36
+ * parameter on its one operation needs it.
37
+ */
38
+
39
+ /**
40
+ * A caller-supplied parameter the operation does not declare, in a location
41
+ * it can still be delivered to.
42
+ */
43
+
44
+ const UNDECLARED_LOCATIONS = new Set(['header', 'query', 'cookie', 'path']);
45
+
46
+ // the header parameters OpenAPI 3 says SHALL be ignored, which buildRequest drops
47
+ const IGNORED_HEADERS = new Set(['accept', 'authorization', 'content-type']);
48
+
33
49
  /**
34
50
  * Options for executing an OpenAPI operation.
35
51
  *
@@ -148,9 +164,11 @@ class OpenAPIOperationExecutor {
148
164
  const withServer = this.#applyServerOptions(rest, normalizedOperation, locator.document);
149
165
  const withBody = this.#adaptRequestBody(withServer, normalizedOperation, locator.document, locator.jsonPointer);
150
166
  const buildOptions = this.#adaptRequestContentType(withBody, normalizedOperation, locator.document, locator.jsonPointer);
167
+ const undeclared = this.#undeclaredParameters(buildOptions.parameters, normalizedOperation);
168
+ const spec = assembled.toJSON();
169
+ this.#declareQueryAndPathParameters(spec, undeclared, (0, _apidomCore.toValue)(operation.meta.get('path')), (0, _apidomCore.toValue)(operation.meta.get('http-method')), locator);
151
170
  let request;
152
171
  try {
153
- const spec = assembled.toJSON();
154
172
  request = (0, _swaggerClient.buildRequest)({
155
173
  ...buildOptions,
156
174
  spec,
@@ -170,6 +188,7 @@ class OpenAPIOperationExecutor {
170
188
  if (typeof authoredContentType === 'string' && buildOptions.requestContentType !== authoredContentType) {
171
189
  request.headers['Content-Type'] = authoredContentType;
172
190
  }
191
+ this.#deliverHeadersAndCookies(request, undeclared);
173
192
  request = await this.#finalizeRequest(request, requestInterceptor);
174
193
 
175
194
  // capture the request as sent, so `$url` / `$method` / `$request.*` can be
@@ -195,6 +214,130 @@ class OpenAPIOperationExecutor {
195
214
  return this.#responseNormalizer.normalize(rawResponse, requestInfo);
196
215
  }
197
216
 
217
+ /**
218
+ * The `{in}.{name}`-keyed parameters (see {@link ParameterDelivery}) in a
219
+ * header, query, cookie or path location that the operation does not
220
+ * declare. `buildRequest` delivers only what the operation declares, yet a
221
+ * step's parameters are what its author asked to send — an `Authorization`
222
+ * header carrying a token from an earlier step most of all, which OpenAPI 3
223
+ * cannot even declare. A declared `Accept`, `Authorization` or
224
+ * `Content-Type` header parameter counts as undeclared too: `buildRequest`
225
+ * drops it for OpenAPI 3, as that specification requires, and for 2.0
226
+ * delivering it here sends the same value. Bare keys are left to
227
+ * `buildRequest`: they name no location to deliver to.
228
+ */
229
+ #undeclaredParameters(parameters, operation) {
230
+ if (!(0, _ramdaAdjunct.isPlainObject)(parameters)) return [];
231
+ const declared = new Set(((0, _apidomDatamodel.isArrayElement)(operation.parameters) ? [...operation.parameters] : []).filter(_apidomDatamodel.isObjectElement).map(parameter => `${(0, _apidomCore.toValue)(parameter.get('in'))}.${(0, _apidomCore.toValue)(parameter.get('name'))}`));
232
+ return Object.entries(parameters).flatMap(([key, value]) => {
233
+ const separator = key.indexOf('.');
234
+ const location = key.slice(0, separator);
235
+ const name = key.slice(separator + 1);
236
+ // an unresolved expression or a null delivers nothing, as buildRequest
237
+ // does for a declared parameter
238
+ if (separator === -1 || !UNDECLARED_LOCATIONS.has(location) || value === undefined || value === null) {
239
+ return [];
240
+ }
241
+ const ignored = location === 'header' && IGNORED_HEADERS.has(name.toLowerCase());
242
+ return declared.has(key) && !ignored ? [] : [{
243
+ location,
244
+ name,
245
+ value
246
+ }];
247
+ });
248
+ }
249
+
250
+ /**
251
+ * Declares the undeclared query and path parameters on the assembled
252
+ * operation, so `buildRequest` serializes and encodes them as it does
253
+ * declared ones. A path parameter is declared only when the path has its
254
+ * `{name}` template expression; otherwise there is nowhere to put it, and
255
+ * that authoring error throws {@link ClientError} rather than sending a
256
+ * request without it.
257
+ */
258
+ #declareQueryAndPathParameters(spec, undeclared, path, method, locator) {
259
+ const operation = spec.paths[path][method];
260
+ const isSwagger = (0, _apidomNsOpenapi.isSwaggerElement)(locator.document.parseResult.api);
261
+ for (const {
262
+ location,
263
+ name
264
+ } of undeclared) {
265
+ if (location !== 'query' && location !== 'path') continue;
266
+ if (location === 'path' && !path.includes(`{${name}}`)) {
267
+ throw new _ClientError.default(`Path parameter "${name}" is not declared by the operation and its path "${path}" ` + `has no "{${name}}" to substitute it into`, {
268
+ operationPath: locator.jsonPointer
269
+ });
270
+ }
271
+ const required = location === 'path';
272
+ (operation.parameters ??= []).push(isSwagger ? {
273
+ name,
274
+ in: location,
275
+ required,
276
+ type: 'string'
277
+ } : {
278
+ name,
279
+ in: location,
280
+ required,
281
+ schema: {}
282
+ });
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Sets the undeclared header parameters on the built request and appends the
288
+ * undeclared cookie parameters to its `Cookie` header. A header replaces any
289
+ * value the build gave it under any spelling — the default `Accept`, say —
290
+ * since the step naming it is the more specific intent. OpenAPI 2.0 has no
291
+ * cookie location, so its cookies always go this way.
292
+ */
293
+ #deliverHeadersAndCookies(request, undeclared) {
294
+ const present = name => Object.keys(request.headers).find(header => header.toLowerCase() === name.toLowerCase());
295
+ for (const {
296
+ location,
297
+ name,
298
+ value
299
+ } of undeclared) {
300
+ if (location !== 'header') continue;
301
+ const built = present(name);
302
+ if (built !== undefined) delete request.headers[built];
303
+ request.headers[name] = this.#stringify(value);
304
+ }
305
+ const cookies = undeclared.filter(({
306
+ location
307
+ }) => location === 'cookie').map(({
308
+ name,
309
+ value
310
+ }) => [name, this.#stringify(value)]);
311
+ if (cookies.length === 0) return;
312
+ // serialized as buildRequest serializes a declared cookie, so a `;` in the
313
+ // value is percent-encoded rather than splitting it into further cookies
314
+ const serialized = (0, _cookie.serializeCookie)(cookies, {
315
+ encoders: {
316
+ value: _cookie.cookieValueStrictPercentEncoder
317
+ },
318
+ validators: {
319
+ name: _cookie.cookieNameLenientValidator
320
+ }
321
+ });
322
+ const cookieHeader = present('Cookie') ?? 'Cookie';
323
+ const existing = request.headers[cookieHeader];
324
+ request.headers[cookieHeader] = existing ? `${existing}; ${serialized}` : serialized;
325
+ }
326
+
327
+ /**
328
+ * A header or cookie value with no schema to serialize it by, the way
329
+ * OpenAPI's default for those locations (`simple` and `form`, not exploded)
330
+ * serializes a declared one: an array as `a,b`, an object as `k,v,k2,v2`,
331
+ * anything else via `String()`.
332
+ */
333
+ #stringify(value) {
334
+ if (Array.isArray(value)) return value.map(String).join(',');
335
+ if (typeof value === 'object' && value !== null) {
336
+ return Object.entries(value).flat().map(String).join(',');
337
+ }
338
+ return String(value);
339
+ }
340
+
198
341
  /**
199
342
  * Derives where the request is sent, returning the options to build with.
200
343
  *
@@ -2,6 +2,7 @@ import { toValue } from '@speclynx/apidom-core';
2
2
  import { isArrayElement, isObjectElement } from '@speclynx/apidom-datamodel';
3
3
  import { url as urlUtils } from '@speclynx/apidom-reference/configuration/empty';
4
4
  import { isSwaggerElement, isParameterElement } from '@speclynx/apidom-ns-openapi-2';
5
+ import { serializeCookie, cookieNameLenientValidator, cookieValueStrictPercentEncoder } from '@swaggerexpert/cookie';
5
6
  import { identify, matches, selectBestMatch } from '@swaggerexpert/media-types';
6
7
  import { isPlainObject } from 'ramda-adjunct';
7
8
 
@@ -22,6 +23,19 @@ import ClientError from "../errors/ClientError.mjs";
22
23
  * May be synchronous or asynchronous.
23
24
  * @public
24
25
  */
26
+ /**
27
+ * The plain-JSON shape of the assembled document, as far as declaring a
28
+ * parameter on its one operation needs it.
29
+ */
30
+ /**
31
+ * A caller-supplied parameter the operation does not declare, in a location
32
+ * it can still be delivered to.
33
+ */
34
+ const UNDECLARED_LOCATIONS = new Set(['header', 'query', 'cookie', 'path']);
35
+
36
+ // the header parameters OpenAPI 3 says SHALL be ignored, which buildRequest drops
37
+ const IGNORED_HEADERS = new Set(['accept', 'authorization', 'content-type']);
38
+
25
39
  /**
26
40
  * Options for executing an OpenAPI operation.
27
41
  *
@@ -39,6 +53,7 @@ import ClientError from "../errors/ClientError.mjs";
39
53
  * {@link RequestInterceptor}.
40
54
  * @public
41
55
  */
56
+
42
57
  /**
43
58
  * The execute options forwarded to {@link OpenAPIOperationExecutor.execute}.
44
59
  *
@@ -61,10 +76,12 @@ import ClientError from "../errors/ClientError.mjs";
61
76
  * `responseNormalizer` — reach `buildRequest` but nothing acts on them.
62
77
  * @public
63
78
  */
79
+
64
80
  /**
65
81
  * Options for the OpenAPIOperationExecutor.
66
82
  * @public
67
83
  */
84
+
68
85
  /**
69
86
  * Executes a single OpenAPI operation named by a canonical
70
87
  * {@link OpenAPIOperationLocator}.
@@ -137,9 +154,11 @@ class OpenAPIOperationExecutor {
137
154
  const withServer = this.#applyServerOptions(rest, normalizedOperation, locator.document);
138
155
  const withBody = this.#adaptRequestBody(withServer, normalizedOperation, locator.document, locator.jsonPointer);
139
156
  const buildOptions = this.#adaptRequestContentType(withBody, normalizedOperation, locator.document, locator.jsonPointer);
157
+ const undeclared = this.#undeclaredParameters(buildOptions.parameters, normalizedOperation);
158
+ const spec = assembled.toJSON();
159
+ this.#declareQueryAndPathParameters(spec, undeclared, toValue(operation.meta.get('path')), toValue(operation.meta.get('http-method')), locator);
140
160
  let request;
141
161
  try {
142
- const spec = assembled.toJSON();
143
162
  request = buildRequest({
144
163
  ...buildOptions,
145
164
  spec,
@@ -159,6 +178,7 @@ class OpenAPIOperationExecutor {
159
178
  if (typeof authoredContentType === 'string' && buildOptions.requestContentType !== authoredContentType) {
160
179
  request.headers['Content-Type'] = authoredContentType;
161
180
  }
181
+ this.#deliverHeadersAndCookies(request, undeclared);
162
182
  request = await this.#finalizeRequest(request, requestInterceptor);
163
183
 
164
184
  // capture the request as sent, so `$url` / `$method` / `$request.*` can be
@@ -184,6 +204,130 @@ class OpenAPIOperationExecutor {
184
204
  return this.#responseNormalizer.normalize(rawResponse, requestInfo);
185
205
  }
186
206
 
207
+ /**
208
+ * The `{in}.{name}`-keyed parameters (see {@link ParameterDelivery}) in a
209
+ * header, query, cookie or path location that the operation does not
210
+ * declare. `buildRequest` delivers only what the operation declares, yet a
211
+ * step's parameters are what its author asked to send — an `Authorization`
212
+ * header carrying a token from an earlier step most of all, which OpenAPI 3
213
+ * cannot even declare. A declared `Accept`, `Authorization` or
214
+ * `Content-Type` header parameter counts as undeclared too: `buildRequest`
215
+ * drops it for OpenAPI 3, as that specification requires, and for 2.0
216
+ * delivering it here sends the same value. Bare keys are left to
217
+ * `buildRequest`: they name no location to deliver to.
218
+ */
219
+ #undeclaredParameters(parameters, operation) {
220
+ if (!isPlainObject(parameters)) return [];
221
+ const declared = new Set((isArrayElement(operation.parameters) ? [...operation.parameters] : []).filter(isObjectElement).map(parameter => `${toValue(parameter.get('in'))}.${toValue(parameter.get('name'))}`));
222
+ return Object.entries(parameters).flatMap(([key, value]) => {
223
+ const separator = key.indexOf('.');
224
+ const location = key.slice(0, separator);
225
+ const name = key.slice(separator + 1);
226
+ // an unresolved expression or a null delivers nothing, as buildRequest
227
+ // does for a declared parameter
228
+ if (separator === -1 || !UNDECLARED_LOCATIONS.has(location) || value === undefined || value === null) {
229
+ return [];
230
+ }
231
+ const ignored = location === 'header' && IGNORED_HEADERS.has(name.toLowerCase());
232
+ return declared.has(key) && !ignored ? [] : [{
233
+ location,
234
+ name,
235
+ value
236
+ }];
237
+ });
238
+ }
239
+
240
+ /**
241
+ * Declares the undeclared query and path parameters on the assembled
242
+ * operation, so `buildRequest` serializes and encodes them as it does
243
+ * declared ones. A path parameter is declared only when the path has its
244
+ * `{name}` template expression; otherwise there is nowhere to put it, and
245
+ * that authoring error throws {@link ClientError} rather than sending a
246
+ * request without it.
247
+ */
248
+ #declareQueryAndPathParameters(spec, undeclared, path, method, locator) {
249
+ const operation = spec.paths[path][method];
250
+ const isSwagger = isSwaggerElement(locator.document.parseResult.api);
251
+ for (const {
252
+ location,
253
+ name
254
+ } of undeclared) {
255
+ if (location !== 'query' && location !== 'path') continue;
256
+ if (location === 'path' && !path.includes(`{${name}}`)) {
257
+ throw new ClientError(`Path parameter "${name}" is not declared by the operation and its path "${path}" ` + `has no "{${name}}" to substitute it into`, {
258
+ operationPath: locator.jsonPointer
259
+ });
260
+ }
261
+ const required = location === 'path';
262
+ (operation.parameters ??= []).push(isSwagger ? {
263
+ name,
264
+ in: location,
265
+ required,
266
+ type: 'string'
267
+ } : {
268
+ name,
269
+ in: location,
270
+ required,
271
+ schema: {}
272
+ });
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Sets the undeclared header parameters on the built request and appends the
278
+ * undeclared cookie parameters to its `Cookie` header. A header replaces any
279
+ * value the build gave it under any spelling — the default `Accept`, say —
280
+ * since the step naming it is the more specific intent. OpenAPI 2.0 has no
281
+ * cookie location, so its cookies always go this way.
282
+ */
283
+ #deliverHeadersAndCookies(request, undeclared) {
284
+ const present = name => Object.keys(request.headers).find(header => header.toLowerCase() === name.toLowerCase());
285
+ for (const {
286
+ location,
287
+ name,
288
+ value
289
+ } of undeclared) {
290
+ if (location !== 'header') continue;
291
+ const built = present(name);
292
+ if (built !== undefined) delete request.headers[built];
293
+ request.headers[name] = this.#stringify(value);
294
+ }
295
+ const cookies = undeclared.filter(({
296
+ location
297
+ }) => location === 'cookie').map(({
298
+ name,
299
+ value
300
+ }) => [name, this.#stringify(value)]);
301
+ if (cookies.length === 0) return;
302
+ // serialized as buildRequest serializes a declared cookie, so a `;` in the
303
+ // value is percent-encoded rather than splitting it into further cookies
304
+ const serialized = serializeCookie(cookies, {
305
+ encoders: {
306
+ value: cookieValueStrictPercentEncoder
307
+ },
308
+ validators: {
309
+ name: cookieNameLenientValidator
310
+ }
311
+ });
312
+ const cookieHeader = present('Cookie') ?? 'Cookie';
313
+ const existing = request.headers[cookieHeader];
314
+ request.headers[cookieHeader] = existing ? `${existing}; ${serialized}` : serialized;
315
+ }
316
+
317
+ /**
318
+ * A header or cookie value with no schema to serialize it by, the way
319
+ * OpenAPI's default for those locations (`simple` and `form`, not exploded)
320
+ * serializes a declared one: an array as `a,b`, an object as `k,v,k2,v2`,
321
+ * anything else via `String()`.
322
+ */
323
+ #stringify(value) {
324
+ if (Array.isArray(value)) return value.map(String).join(',');
325
+ if (typeof value === 'object' && value !== null) {
326
+ return Object.entries(value).flat().map(String).join(',');
327
+ }
328
+ return String(value);
329
+ }
330
+
187
331
  /**
188
332
  * Derives where the request is sent, returning the options to build with.
189
333
  *
@@ -35,7 +35,11 @@ var _StepParameterResolver = _interopRequireDefault(require("./StepParameterReso
35
35
  * instead would let it capture *every* declared location of that name — see
36
36
  * `ParameterDelivery` for the lookup order that makes it so. A
37
37
  * non-string location throws for the same reason it deduplicates with nothing
38
- * in the normalizer: it names no location. And two *different* parameters the
38
+ * in the normalizer: it names no location. So does a location outside
39
+ * `path`, `query`, `header`, `cookie` and `querystring`: Arazzo 1.0.1 removed
40
+ * `in: body`, and forwarding one would let it still deliver a request body
41
+ * against an OpenAPI 2.0 operation, bypassing the step's `requestBody`, while
42
+ * an unknown location reaches nothing at all. And two *different* parameters the
39
43
  * map reports as colliding cannot both be delivered, so a collision throws
40
44
  * rather than silently dropping one — whether they collide on one key (the
41
45
  * scheme is not injective: a `querystring` parameter named `header.token` and
@@ -96,6 +100,13 @@ class OpenAPIOperationParameterResolver extends _StepParameterResolver.default {
96
100
  reason: 'malformed-location'
97
101
  });
98
102
  }
103
+ if (!identity.hasDefinedLocation) {
104
+ throw new _ResolverError.default(`Parameter "${name}" has location "${location}", which is not one of ` + `${_ParameterIdentity.default.LOCATIONS.join(', ')}`, {
105
+ ...scope,
106
+ target: name,
107
+ reason: 'invalid-location'
108
+ });
109
+ }
99
110
  const {
100
111
  key,
101
112
  outcome
@@ -28,7 +28,11 @@ import StepParameterResolver from "./StepParameterResolver.mjs";
28
28
  * instead would let it capture *every* declared location of that name — see
29
29
  * `ParameterDelivery` for the lookup order that makes it so. A
30
30
  * non-string location throws for the same reason it deduplicates with nothing
31
- * in the normalizer: it names no location. And two *different* parameters the
31
+ * in the normalizer: it names no location. So does a location outside
32
+ * `path`, `query`, `header`, `cookie` and `querystring`: Arazzo 1.0.1 removed
33
+ * `in: body`, and forwarding one would let it still deliver a request body
34
+ * against an OpenAPI 2.0 operation, bypassing the step's `requestBody`, while
35
+ * an unknown location reaches nothing at all. And two *different* parameters the
32
36
  * map reports as colliding cannot both be delivered, so a collision throws
33
37
  * rather than silently dropping one — whether they collide on one key (the
34
38
  * scheme is not injective: a `querystring` parameter named `header.token` and
@@ -89,6 +93,13 @@ class OpenAPIOperationParameterResolver extends StepParameterResolver {
89
93
  reason: 'malformed-location'
90
94
  });
91
95
  }
96
+ if (!identity.hasDefinedLocation) {
97
+ throw new ResolverError(`Parameter "${name}" has location "${location}", which is not one of ` + `${ParameterIdentity.LOCATIONS.join(', ')}`, {
98
+ ...scope,
99
+ target: name,
100
+ reason: 'invalid-location'
101
+ });
102
+ }
92
103
  const {
93
104
  key,
94
105
  outcome
package/types/runner.d.ts CHANGED
@@ -1196,7 +1196,11 @@ export declare interface OpenAPIOperationNormalizerOptions {
1196
1196
  * instead would let it capture *every* declared location of that name — see
1197
1197
  * `ParameterDelivery` for the lookup order that makes it so. A
1198
1198
  * non-string location throws for the same reason it deduplicates with nothing
1199
- * in the normalizer: it names no location. And two *different* parameters the
1199
+ * in the normalizer: it names no location. So does a location outside
1200
+ * `path`, `query`, `header`, `cookie` and `querystring`: Arazzo 1.0.1 removed
1201
+ * `in: body`, and forwarding one would let it still deliver a request body
1202
+ * against an OpenAPI 2.0 operation, bypassing the step's `requestBody`, while
1203
+ * an unknown location reaches nothing at all. And two *different* parameters the
1200
1204
  * map reports as colliding cannot both be delivered, so a collision throws
1201
1205
  * rather than silently dropping one — whether they collide on one key (the
1202
1206
  * scheme is not injective: a `querystring` parameter named `header.token` and