@postman-cse/onboarding-bootstrap 1.1.0 → 1.2.1

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/action.yml CHANGED
@@ -1,5 +1,5 @@
1
- name: postman-bootstrap-action
2
- description: Create or reuse a Postman workspace, upload an OpenAPI spec, and generate baseline, smoke, and contract collections.
1
+ name: Postman Workspace Bootstrap
2
+ description: Provision a Postman workspace from an OpenAPI spec and generate baseline, smoke, and contract collections in one step.
3
3
  author: Postman
4
4
  branding:
5
5
  icon: box
package/dist/action.cjs CHANGED
@@ -49788,6 +49788,38 @@ function normalizeSchema(ctx, schema2, options) {
49788
49788
  const sourceSchema = { ...record };
49789
49789
  const nullable = sourceSchema.nullable === true && ctx.version === "3.0";
49790
49790
  delete sourceSchema.nullable;
49791
+ const discriminator = asRecord3(sourceSchema.discriminator);
49792
+ if (discriminator && typeof discriminator.propertyName === "string" && discriminator.propertyName) {
49793
+ const propertyName = discriminator.propertyName;
49794
+ const branchKey = Array.isArray(sourceSchema.oneOf) ? "oneOf" : Array.isArray(sourceSchema.anyOf) ? "anyOf" : "";
49795
+ const members = branchKey ? sourceSchema[branchKey] : [];
49796
+ const memberRefs = members.map((member) => {
49797
+ const memberRecord = asRecord3(member);
49798
+ const memberRef = memberRecord && Object.keys(memberRecord).length === 1 && typeof memberRecord.$ref === "string" ? memberRecord.$ref : "";
49799
+ return memberRef.startsWith("#/") ? memberRef : "";
49800
+ });
49801
+ if (branchKey && members.length > 0 && memberRefs.every(Boolean)) {
49802
+ const valuesByRef = /* @__PURE__ */ new Map();
49803
+ const refByMappingKey = /* @__PURE__ */ new Map();
49804
+ for (const [value, target] of Object.entries(asRecord3(discriminator.mapping) ?? {})) {
49805
+ if (typeof target !== "string" || !target) continue;
49806
+ const targetRef = target.startsWith("#/") ? target : `#/components/schemas/${target}`;
49807
+ valuesByRef.set(targetRef, [...valuesByRef.get(targetRef) ?? [], value]);
49808
+ refByMappingKey.set(value, targetRef);
49809
+ }
49810
+ sourceSchema[branchKey] = members.map((member, index) => {
49811
+ const memberRef = memberRefs[index] ?? "";
49812
+ const tail = (memberRef.split("/").pop() ?? "").replace(/~1/g, "/").replace(/~0/g, "~");
49813
+ const values = [...valuesByRef.get(memberRef) ?? []];
49814
+ const tailClaimedElsewhere = refByMappingKey.has(tail) && refByMappingKey.get(tail) !== memberRef;
49815
+ if (tail && !tailClaimedElsewhere && !values.includes(tail)) values.push(tail);
49816
+ const dispatch = values.length === 1 ? { const: values[0] } : { enum: values };
49817
+ return { allOf: [member, { type: "object", required: [propertyName], properties: { [propertyName]: dispatch } }] };
49818
+ });
49819
+ } else {
49820
+ ctx.notes.add("discriminator");
49821
+ }
49822
+ }
49791
49823
  const directionStrippedFlag = ctx.direction === "request" ? "readOnly" : "writeOnly";
49792
49824
  const strippedProperties = /* @__PURE__ */ new Set();
49793
49825
  const rawProperties = asRecord3(sourceSchema.properties);
@@ -49960,7 +49992,7 @@ function packSchema(root, schema2, version, direction = "response") {
49960
49992
  if (schema2 === true) return { schema: { $schema: rootDialect(root, version) } };
49961
49993
  if (schema2 === false) return unsupported("Boolean false JSON Schema rejects every instance and is unsupported");
49962
49994
  const dialect = rootDialect(root, version, asRecord3(schema2) ?? void 0);
49963
- const ctx = { root, version, direction, dialect, defs: /* @__PURE__ */ new Map() };
49995
+ const ctx = { root, version, direction, dialect, defs: /* @__PURE__ */ new Map(), notes: /* @__PURE__ */ new Set() };
49964
49996
  const normalized = normalizeSchema(ctx, schema2, { depth: 0, rootSchema: true });
49965
49997
  const message = hasUnsupported(normalized);
49966
49998
  if (message) return unsupported(message);
@@ -49985,7 +50017,9 @@ function packSchema(root, schema2, version, direction = "response") {
49985
50017
  if (!normalizedRecord) return unsupported("Referenced schemas require an object root schema");
49986
50018
  normalizedRecord.$defs = Object.fromEntries([...ctx.defs.values()].map((entry) => [entry.name, entry.schema]));
49987
50019
  }
49988
- return { schema: normalized };
50020
+ const packed = { schema: normalized };
50021
+ if (ctx.notes.size > 0) packed.notes = [...ctx.notes].sort();
50022
+ return packed;
49989
50023
  } catch (error2) {
49990
50024
  return unsupported(error2 instanceof Error ? error2.message : String(error2));
49991
50025
  }
@@ -50158,6 +50192,7 @@ function securityCheckFor(schemeName, scheme) {
50158
50192
  const httpScheme = String(scheme.scheme || "").toLowerCase();
50159
50193
  if (httpScheme === "basic") return { scheme: schemeName, kind, checkable: true, prefix: "Basic " };
50160
50194
  if (httpScheme === "bearer") return { scheme: schemeName, kind, checkable: true, prefix: "Bearer " };
50195
+ if (httpScheme) return { scheme: schemeName, kind, checkable: true, prefix: `${httpScheme.charAt(0).toUpperCase()}${httpScheme.slice(1)} ` };
50161
50196
  return { scheme: schemeName, kind, checkable: true, in: "header", name: "Authorization" };
50162
50197
  }
50163
50198
  if (scheme?.type === "oauth2" || scheme?.type === "openIdConnect") {
@@ -50191,7 +50226,18 @@ var IGNORED_HEADER_PARAMS = /* @__PURE__ */ new Set(["accept", "content-type", "
50191
50226
  function isIgnoredParameter(location2, name) {
50192
50227
  return location2 === "header" && IGNORED_HEADER_PARAMS.has(name.toLowerCase());
50193
50228
  }
50194
- function collectSerializationWarnings(root, pathItem, operation) {
50229
+ function jsonContentParameterMedia(param) {
50230
+ const content = asRecord4(param.content);
50231
+ if (!content) return void 0;
50232
+ const entries = Object.entries(content);
50233
+ if (entries.length !== 1) return void 0;
50234
+ const [contentType, mediaObject] = entries[0];
50235
+ const base = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
50236
+ if (!isJsonBaseType(base)) return void 0;
50237
+ const schema2 = asRecord4(mediaObject)?.schema;
50238
+ return schema2 === void 0 ? void 0 : schema2;
50239
+ }
50240
+ function collectSerializationWarnings(root, pathItem, operation, decodedKeys) {
50195
50241
  const warnings = [];
50196
50242
  for (const param of resolvedParameters(root, pathItem, operation)) {
50197
50243
  const location2 = String(param.in || "").toLowerCase();
@@ -50201,7 +50247,9 @@ function collectSerializationWarnings(root, pathItem, operation) {
50201
50247
  const style = typeof param.style === "string" ? param.style : defaultStyle;
50202
50248
  const defaultExplode = style === "form";
50203
50249
  const explode = typeof param.explode === "boolean" ? param.explode : defaultExplode;
50204
- if (style !== defaultStyle || explode !== defaultExplode || param.allowReserved === true || param.content !== void 0) {
50250
+ const unvalidatedContent = param.content !== void 0 && (jsonContentParameterMedia(param) === void 0 || location2 !== "query" && location2 !== "header");
50251
+ if (style !== defaultStyle || explode !== defaultExplode || param.allowReserved === true || unvalidatedContent) {
50252
+ if (decodedKeys.has(`${location2}:${name.toLowerCase()}`) && param.allowReserved !== true && param.content === void 0) continue;
50205
50253
  warnings.push(`CONTRACT_PARAM_SERIALIZATION_NOT_VALIDATED: parameter ${location2}:${name} declares non-default style, explode, allowReserved, or content and its serialization is not validated`);
50206
50254
  }
50207
50255
  }
@@ -50216,7 +50264,27 @@ function packedScalarSchema(packed) {
50216
50264
  if (!types2.every((entry) => typeof entry === "string" && SCALAR_SCHEMA_TYPES.has(entry))) return void 0;
50217
50265
  return packed.schema;
50218
50266
  }
50219
- function collectParameterChecks(root, pathItem, operation, version, operationId, warnings) {
50267
+ function packedArrayItemsSchema(packed) {
50268
+ if (packed.unsupported || packed.schema === void 0) return void 0;
50269
+ const record = asRecord4(packed.schema);
50270
+ if (!record) return void 0;
50271
+ const types2 = Array.isArray(record.type) ? record.type : [record.type];
50272
+ if (types2.length !== 1 || types2[0] !== "array") return void 0;
50273
+ if (record.prefixItems !== void 0 || Array.isArray(record.items)) return void 0;
50274
+ if (record.items === void 0) return {};
50275
+ const items = asRecord4(record.items);
50276
+ if (!items || typeof items.$ref === "string") return void 0;
50277
+ const itemTypes = Array.isArray(items.type) ? items.type : [items.type];
50278
+ if (!itemTypes.every((entry) => typeof entry === "string" && SCALAR_SCHEMA_TYPES.has(entry))) return void 0;
50279
+ return items;
50280
+ }
50281
+ var QUERY_ARRAY_DECODES = {
50282
+ "form:true": "multi",
50283
+ "form:false": "csv",
50284
+ "spaceDelimited:false": "ssv",
50285
+ "pipeDelimited:false": "pipes"
50286
+ };
50287
+ function collectParameterChecks(root, pathItem, operation, version, operationId, pathTemplate, warnings) {
50220
50288
  const securityKeys = collectSecurityApiKeys(root, operation);
50221
50289
  const checks = [];
50222
50290
  const seen = /* @__PURE__ */ new Set();
@@ -50229,32 +50297,65 @@ function collectParameterChecks(root, pathItem, operation, version, operationId,
50229
50297
  }).filter((param) => Boolean(param));
50230
50298
  for (const param of orderedParams) {
50231
50299
  const location2 = String(param.in || "").toLowerCase();
50232
- if (location2 !== "query" && location2 !== "header") continue;
50300
+ if (location2 !== "query" && location2 !== "header" && location2 !== "path" && location2 !== "cookie") continue;
50233
50301
  const name = String(param.name || "");
50234
50302
  if (!name || isIgnoredParameter(location2, name)) continue;
50235
50303
  const key = `${location2}:${name.toLowerCase()}`;
50236
50304
  if (seen.has(key)) continue;
50237
50305
  seen.add(key);
50238
50306
  if (securityKeys.has(key)) continue;
50307
+ const contentMedia = jsonContentParameterMedia(param);
50308
+ if (contentMedia !== void 0 && (location2 === "query" || location2 === "header")) {
50309
+ const packed2 = packSchema(root, contentMedia, version, "request");
50310
+ warnings.push(...packNoteWarnings(packed2, `parameter ${location2}:${name} of ${operationId}`));
50311
+ if (packed2.unsupported) {
50312
+ warnings.push(`CONTRACT_SCHEMA_NOT_COMPILED: parameter ${location2}:${name} schema on ${operationId} skipped (${packed2.unsupported})`);
50313
+ } else if (packed2.schema !== void 0) {
50314
+ const check2 = { in: location2, name, required: param.required === true, content: true, schema: packed2.schema };
50315
+ if (location2 === "query" && param.allowEmptyValue === true) check2.allowEmptyValue = true;
50316
+ checks.push(check2);
50317
+ }
50318
+ continue;
50319
+ }
50239
50320
  if (param.content !== void 0 || param.schema === void 0) continue;
50240
50321
  const defaultStyle = DEFAULT_PARAM_STYLES[location2];
50241
50322
  const style = typeof param.style === "string" ? param.style : defaultStyle;
50242
50323
  const defaultExplode = style === "form";
50243
50324
  const explode = typeof param.explode === "boolean" ? param.explode : defaultExplode;
50244
- if (style !== defaultStyle || explode !== defaultExplode) continue;
50325
+ const defaultSerialization = style === defaultStyle && explode === defaultExplode;
50245
50326
  const packed = packSchema(root, param.schema, version);
50327
+ const noteWarnings = packNoteWarnings(packed, `parameter ${location2}:${name} of ${operationId}`);
50328
+ if (defaultSerialization) warnings.push(...noteWarnings);
50246
50329
  if (packed.unsupported) {
50247
- warnings.push(`CONTRACT_SCHEMA_NOT_COMPILED: parameter ${location2}:${name} schema on ${operationId} skipped (${packed.unsupported})`);
50330
+ if (defaultSerialization) warnings.push(`CONTRACT_SCHEMA_NOT_COMPILED: parameter ${location2}:${name} schema on ${operationId} skipped (${packed.unsupported})`);
50248
50331
  continue;
50249
50332
  }
50250
- const schema2 = packedScalarSchema(packed);
50251
- if (schema2 === void 0) continue;
50252
- const check = { in: location2, name, required: param.required === true, schema: schema2 };
50333
+ if (location2 === "path" && !pathTemplate.split("/").includes(`{${name}}`)) continue;
50334
+ const scalarSchema = packedScalarSchema(packed);
50335
+ if (scalarSchema !== void 0) {
50336
+ if (!defaultSerialization) continue;
50337
+ const check2 = { in: location2, name, required: param.required === true, schema: scalarSchema };
50338
+ if (location2 === "query" && param.allowEmptyValue === true) check2.allowEmptyValue = true;
50339
+ checks.push(check2);
50340
+ continue;
50341
+ }
50342
+ if (location2 !== "query" && location2 !== "header") continue;
50343
+ const items = packedArrayItemsSchema(packed);
50344
+ if (items === void 0) continue;
50345
+ const decode = location2 === "query" ? QUERY_ARRAY_DECODES[`${style}:${explode}`] : style === "simple" && !explode ? "csv" : void 0;
50346
+ if (!decode) continue;
50347
+ if (!defaultSerialization) warnings.push(...noteWarnings);
50348
+ const check = { in: location2, name, required: param.required === true, schema: packed.schema, decode, items };
50253
50349
  if (location2 === "query" && param.allowEmptyValue === true) check.allowEmptyValue = true;
50254
50350
  checks.push(check);
50255
50351
  }
50256
50352
  return checks.length > 0 ? checks : void 0;
50257
50353
  }
50354
+ function packNoteWarnings(packed, context) {
50355
+ return (packed.notes ?? []).map(
50356
+ (note) => note === "discriminator" ? `CONTRACT_DISCRIMINATOR_NOT_VALIDATED: discriminator on ${context} has no sibling oneOf/anyOf of internal $ref members and is not validated` : `CONTRACT_SCHEMA_NOT_COMPILED: ${note} on ${context} is not validated`
50357
+ );
50358
+ }
50258
50359
  function collectDeclaredQueryParameters(root, pathItem, operation) {
50259
50360
  const names = /* @__PURE__ */ new Set();
50260
50361
  for (const param of resolvedParameters(root, pathItem, operation)) {
@@ -50332,7 +50433,10 @@ function propertyIsBinary(root, properties, name) {
50332
50433
  }
50333
50434
  if (!schema2) return false;
50334
50435
  if (schema2.format === "binary") return true;
50335
- return typeof schema2.contentMediaType === "string" && schema2.contentMediaType.toLowerCase().startsWith("application/octet-stream");
50436
+ if (typeof schema2.contentMediaType !== "string" || typeof schema2.contentEncoding === "string") return false;
50437
+ const media = schema2.contentMediaType.toLowerCase().split(";")[0]?.trim() ?? "";
50438
+ if (!media) return false;
50439
+ return media !== "application/json" && !media.endsWith("+json") && !media.startsWith("text/");
50336
50440
  }
50337
50441
  function fieldEncodings(root, base, mediaObject, properties) {
50338
50442
  const declared = asRecord4(mediaObject?.encoding);
@@ -50350,6 +50454,9 @@ function fieldEncodings(root, base, mediaObject, properties) {
50350
50454
  if (typeof encoding.contentType === "string" && encoding.contentType.trim()) {
50351
50455
  entry.contentType = encoding.contentType.toLowerCase();
50352
50456
  }
50457
+ if (base === "multipart/form-data" && asRecord4(encoding.headers)) {
50458
+ entry.hasHeaders = true;
50459
+ }
50353
50460
  if (base === "application/x-www-form-urlencoded") {
50354
50461
  const style = typeof encoding.style === "string" ? encoding.style : "form";
50355
50462
  const explode = typeof encoding.explode === "boolean" ? encoding.explode : style === "form";
@@ -50362,7 +50469,22 @@ function fieldEncodings(root, base, mediaObject, properties) {
50362
50469
  }
50363
50470
  return Object.keys(encodings).length > 0 ? encodings : void 0;
50364
50471
  }
50365
- function requestBodyFieldRules(root, content) {
50472
+ function formFieldSchemas(root, version, properties, context, warnings) {
50473
+ const schemas = {};
50474
+ for (const name of Object.keys(properties)) {
50475
+ const packed = packSchema(root, properties[name], version, "request");
50476
+ warnings.push(...packNoteWarnings(packed, `field ${name} of ${context}`));
50477
+ if (packed.unsupported) {
50478
+ warnings.push(`CONTRACT_SCHEMA_NOT_COMPILED: field ${name} of ${context} skipped (${packed.unsupported})`);
50479
+ continue;
50480
+ }
50481
+ if (packed.schema === void 0) continue;
50482
+ const schema2 = packedScalarSchema(packed);
50483
+ if (schema2 !== void 0) schemas[name] = schema2;
50484
+ }
50485
+ return Object.keys(schemas).length > 0 ? schemas : void 0;
50486
+ }
50487
+ function requestBodyFieldRules(root, content, version, operationId, warnings) {
50366
50488
  const rules = {};
50367
50489
  for (const [contentType, mediaObject] of Object.entries(content)) {
50368
50490
  const base = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
@@ -50372,10 +50494,13 @@ function requestBodyFieldRules(root, content) {
50372
50494
  if (!merged) continue;
50373
50495
  const readOnly = Object.keys(merged.properties).filter((name) => propertyIsReadOnly(root, merged.properties, name));
50374
50496
  const required = merged.required.filter((name) => !propertyIsReadOnly(root, merged.properties, name));
50375
- const encodings = BODY_FIELD_RULE_TYPES.has(base) ? fieldEncodings(root, base, mediaRecord, merged.properties) : void 0;
50376
- if (required.length > 0 || readOnly.length > 0 || encodings) {
50497
+ const formRules = BODY_FIELD_RULE_TYPES.has(base);
50498
+ const encodings = formRules ? fieldEncodings(root, base, mediaRecord, merged.properties) : void 0;
50499
+ const fieldSchemas = formRules ? formFieldSchemas(root, version, merged.properties, `request body ${contentType} of ${operationId}`, warnings) : void 0;
50500
+ if (required.length > 0 || readOnly.length > 0 || encodings || fieldSchemas) {
50377
50501
  const rule = { required, readOnly };
50378
50502
  if (encodings) rule.encodings = encodings;
50503
+ if (fieldSchemas) rule.fieldSchemas = fieldSchemas;
50379
50504
  rules[base] = rule;
50380
50505
  }
50381
50506
  }
@@ -50396,6 +50521,7 @@ function requestBodyJsonSchemas(root, content, version, operationId, warnings) {
50396
50521
  }
50397
50522
  if (schema2 === void 0) continue;
50398
50523
  const packed = packSchema(root, schema2, version, "request");
50524
+ warnings.push(...packNoteWarnings(packed, `request body ${contentType} of ${operationId}`));
50399
50525
  if (mediaRecord) validateExamples(root, mediaRecord, packed, contentType, operationId, exampleWarnings);
50400
50526
  if (packed.unsupported) {
50401
50527
  warnings.push(`CONTRACT_REQUEST_SCHEMA_NOT_VALIDATED: request body schema for ${contentType} on ${operationId} is not validated (${packed.unsupported})`);
@@ -50410,7 +50536,7 @@ function collectRequestBody(root, operation, version, operationId, warnings) {
50410
50536
  const body = resolveInternalRef(root, operation.requestBody);
50411
50537
  if (!body) return void 0;
50412
50538
  const content = asRecord4(body.content);
50413
- const fieldRules = content ? requestBodyFieldRules(root, content) : void 0;
50539
+ const fieldRules = content ? requestBodyFieldRules(root, content, version, operationId, warnings) : void 0;
50414
50540
  if (fieldRules) {
50415
50541
  for (const [base, rule] of Object.entries(fieldRules)) {
50416
50542
  for (const [field, encoding] of Object.entries(rule.encodings ?? {})) {
@@ -50419,6 +50545,11 @@ function collectRequestBody(root, operation, version, operationId, warnings) {
50419
50545
  `CONTRACT_PARAM_SERIALIZATION_NOT_VALIDATED: ${base} request body field ${field} on ${operationId} declares non-default encoding style, explode, or allowReserved and its serialization is not validated`
50420
50546
  );
50421
50547
  }
50548
+ if (encoding.hasHeaders) {
50549
+ warnings.push(
50550
+ `CONTRACT_ENCODING_HEADERS_NOT_VALIDATED: ${base} request body field ${field} on ${operationId} declares per-part headers that generated formdata entries cannot carry`
50551
+ );
50552
+ }
50422
50553
  }
50423
50554
  }
50424
50555
  }
@@ -50466,6 +50597,7 @@ function responseContent(root, version, response, context, warnings) {
50466
50597
  const mediaRecord = asRecord4(mediaObject);
50467
50598
  const schema2 = mediaRecord?.schema;
50468
50599
  let packed = schema2 === void 0 ? {} : packSchema(root, schema2, version);
50600
+ for (const warning2 of packNoteWarnings(packed, `response ${contentType} of ${context}`)) warnings.add(warning2);
50469
50601
  if (isSchemaGraphOverflow(packed)) {
50470
50602
  warnings.add(`CONTRACT_SCHEMA_NOT_COMPILED: response schema for ${contentType} on ${context} skipped (${packed.unsupported})`);
50471
50603
  packed = {};
@@ -50497,12 +50629,18 @@ function responseHeaders(root, version, response, context, warnings) {
50497
50629
  continue;
50498
50630
  }
50499
50631
  const packed = packSchema(root, header.schema, version);
50632
+ for (const warning2 of packNoteWarnings(packed, `response header ${name} of ${context}`)) warnings.add(warning2);
50500
50633
  if (isSchemaGraphOverflow(packed)) {
50501
50634
  warnings.add(`CONTRACT_SCHEMA_NOT_COMPILED: response header ${name} schema on ${context} skipped (${packed.unsupported})`);
50502
50635
  entries.push({ name, required });
50503
50636
  continue;
50504
50637
  }
50505
50638
  if (!packed.unsupported && packed.schema !== void 0 && packedScalarSchema(packed) === void 0) {
50639
+ const items = packedArrayItemsSchema(packed);
50640
+ if (items !== void 0) {
50641
+ entries.push({ name, required, schema: packed.schema, items });
50642
+ continue;
50643
+ }
50506
50644
  warnings.add(`CONTRACT_HEADER_SCHEMA_NOT_VALIDATED: response header ${name} on ${context} declares a non-scalar schema and its value is not validated`);
50507
50645
  entries.push({ name, required });
50508
50646
  continue;
@@ -50561,10 +50699,14 @@ function buildContractIndex(root) {
50561
50699
  path8,
50562
50700
  ...operationServers(root, pathItem, operation).map((server) => joinPaths(serverPathPrefix(server), path8))
50563
50701
  ].map(normalizePath))];
50702
+ const operationId = `${lowerMethod.toUpperCase()} ${path8}`;
50564
50703
  const opWarnings = [];
50565
50704
  opWarnings.push(...responseWarnings);
50566
50705
  opWarnings.push(...collectSecuritySchemeWarnings(root, operation));
50567
- opWarnings.push(...collectSerializationWarnings(root, pathItem, operation));
50706
+ const parameterChecks = collectParameterChecks(root, pathItem, operation, version, operationId, path8, opWarnings);
50707
+ const checkedKeys = new Set((parameterChecks ?? []).map((check) => `${check.in}:${check.name.toLowerCase()}`));
50708
+ const decodedKeys = new Set((parameterChecks ?? []).filter((check) => check.decode).map((check) => `${check.in}:${check.name.toLowerCase()}`));
50709
+ opWarnings.push(...collectSerializationWarnings(root, pathItem, operation, decodedKeys));
50568
50710
  if (operation.deprecated === true) {
50569
50711
  opWarnings.push(`CONTRACT_OPERATION_DEPRECATED: ${lowerMethod.toUpperCase()} ${path8} is marked deprecated in the OpenAPI document`);
50570
50712
  }
@@ -50573,17 +50715,19 @@ function buildContractIndex(root) {
50573
50715
  opWarnings.push(`CONTRACT_SECURITY_NOT_VALIDATED: security parameter ${parameter.in}:${parameter.name} is not statically required in generated requests`);
50574
50716
  }
50575
50717
  for (const parameter of requiredParameters.filter((entry) => entry.in === "cookie" && !entry.securityDerived)) {
50576
- opWarnings.push(`CONTRACT_COOKIE_PARAM_NOT_VALIDATED: required cookie parameter ${parameter.name} is not statically required in generated requests`);
50718
+ opWarnings.push(checkedKeys.has(`cookie:${parameter.name.toLowerCase()}`) ? `CONTRACT_COOKIE_PARAM_NOT_VALIDATED: required cookie parameter ${parameter.name} is not included in generated requests; the runtime test fails until the cookie is supplied at send time` : `CONTRACT_COOKIE_PARAM_NOT_VALIDATED: required cookie parameter ${parameter.name} is not included in generated requests and its value is not runtime-validated`);
50577
50719
  }
50578
50720
  const pathParamWarnings = /* @__PURE__ */ new Set();
50579
50721
  for (const param of resolvedParameters(root, pathItem, operation)) {
50580
50722
  if (String(param.in || "").toLowerCase() !== "path") continue;
50581
50723
  const name = String(param.name || "");
50582
- if (name) pathParamWarnings.add(`CONTRACT_PATH_PARAM_NOT_VALIDATED: path parameter ${name} value is not validated at runtime`);
50724
+ if (name && !checkedKeys.has(`path:${name.toLowerCase()}`)) {
50725
+ pathParamWarnings.add(`CONTRACT_PATH_PARAM_NOT_VALIDATED: path parameter ${name} value is not validated at runtime`);
50726
+ }
50583
50727
  }
50584
50728
  opWarnings.push(...pathParamWarnings);
50585
50729
  operations.push({
50586
- id: `${lowerMethod.toUpperCase()} ${path8}`,
50730
+ id: operationId,
50587
50731
  method: lowerMethod.toUpperCase(),
50588
50732
  path: path8,
50589
50733
  pointer: `/paths/${path8.replace(/~/g, "~0").replace(/\//g, "~1")}/${lowerMethod}`,
@@ -50591,8 +50735,8 @@ function buildContractIndex(root) {
50591
50735
  responses: contractResponses,
50592
50736
  requiredParameters,
50593
50737
  declaredQueryParameters: collectDeclaredQueryParameters(root, pathItem, operation),
50594
- parameterChecks: collectParameterChecks(root, pathItem, operation, version, `${lowerMethod.toUpperCase()} ${path8}`, opWarnings),
50595
- requestBody: collectRequestBody(root, operation, version, `${lowerMethod.toUpperCase()} ${path8}`, opWarnings),
50738
+ parameterChecks,
50739
+ requestBody: collectRequestBody(root, operation, version, operationId, opWarnings),
50596
50740
  security: collectSecurityRuntimeChecks(root, operation),
50597
50741
  warnings: opWarnings
50598
50742
  });
@@ -50799,8 +50943,11 @@ function createContractScript(operation, warnings = []) {
50799
50943
  " if (!actual) return;",
50800
50944
  ' if (header.unsupported) pm.expect.fail("OpenAPI response header unsupported for " + contract.method + " " + contract.path + ": " + header.unsupported);',
50801
50945
  " var headerValidator = validators[selected.key] && validators[selected.key].__headers && validators[selected.key].__headers[String(header.name).toLowerCase()];",
50802
- " if (headerValidator && headerValidator.skip) return;",
50803
- ' if (headerValidator && !headerValidator(coerceBySchema(actual, header.schema))) pm.expect.fail("OpenAPI response header validation failed for " + header.name + ": " + JSON.stringify(headerValidator.errors || []));',
50946
+ " if (!headerValidator || headerValidator.skip) return;",
50947
+ " var expected;",
50948
+ ' if (header.items) { var joined = String(actual).trim(); expected = joined === "" ? [] : joined.split(",").map(function (entry) { return coerceBySchema(entry.trim(), header.items); }); }',
50949
+ " else expected = coerceBySchema(actual, header.schema);",
50950
+ ' if (!headerValidator(expected)) pm.expect.fail("OpenAPI response header validation failed for " + header.name + ": " + JSON.stringify(headerValidator.errors || []));',
50804
50951
  " });",
50805
50952
  "});",
50806
50953
  "pm.test('Response body matches OpenAPI body contract', function () {",
@@ -50856,17 +51003,64 @@ function createContractScript(operation, warnings = []) {
50856
51003
  ...operation.parameterChecks && operation.parameterChecks.length > 0 ? [
50857
51004
  "pm.test('Request parameters match OpenAPI schemas', function () {",
50858
51005
  ' function queryValue(name) { var value; pm.request.url.query.each(function (param) { if (param && param.disabled !== true && String(param.key).toLowerCase() === name) value = param.value === null || param.value === undefined ? "" : String(param.value); }); return value; }',
51006
+ ' function queryValues(name) { var values = []; pm.request.url.query.each(function (param) { if (param && param.disabled !== true && String(param.key).toLowerCase() === name) values.push(param.value === null || param.value === undefined ? "" : String(param.value)); }); return values; }',
50859
51007
  ' function headerValue(name) { var value; pm.request.headers.each(function (header) { if (header && header.disabled !== true && String(header.key).toLowerCase() === String(name).toLowerCase()) value = header.value === null || header.value === undefined ? "" : String(header.value); }); return value; }',
51008
+ ' function cookieValue(name) { try { if (pm.cookies && pm.cookies.get) { var jar = pm.cookies.get(String(name)); if (jar !== null && jar !== undefined) return String(jar); } } catch (ignored) {} var raw = requestHeader("Cookie"); if (!raw) return undefined; var found; raw.split(";").forEach(function (part) { var split = part.indexOf("="); if (split === -1) return; if (part.slice(0, split).trim() === String(name)) found = part.slice(split + 1).trim(); }); return found; }',
51009
+ ' function requestPathSegments() { var raw = ""; try { raw = typeof pm.request.url.getPath === "function" ? String(pm.request.url.getPath() || "") : ""; } catch (ignored) {} if (!raw) { var path = pm.request.url.path; raw = Array.isArray(path) ? "/" + path.join("/") : String(path || ""); } return raw.split("?")[0].split("#")[0].split("/").filter(function (segment) { return segment.length > 0; }); }',
51010
+ // Server path prefixes sit ahead of the template segments, so the
51011
+ // template aligns against the trailing request segments.
51012
+ ' function pathParamValue(name) { var template = String(contract.path).split("/").filter(function (segment) { return segment.length > 0; }); var actual = requestPathSegments(); var offset = actual.length - template.length; if (offset < 0) return undefined; for (var i = 0; i < template.length; i += 1) { if (template[i] === "{" + name + "}") { var segment = actual[offset + i]; try { return decodeURIComponent(segment); } catch (ignored) { return segment; } } } return undefined; }',
50860
51013
  ' function isPlaceholder(value) { var text = String(value).trim(); return /^<[^<>]*>$/.test(text) || text.indexOf("{{") !== -1; }',
51014
+ ' function splitDelimited(value, decode) { if (decode === "csv") return value.split(","); if (decode === "ssv") return value.split(/%20| /); return value.split(/%7C|\\|/i); }',
51015
+ " function decodeComponent(value) { try { return decodeURIComponent(value); } catch (ignored) { return value; } }",
50861
51016
  " (contract.parameters || []).forEach(function (param) {",
50862
51017
  ' var key = param.in + ":" + String(param.name).toLowerCase();',
50863
51018
  " var validate = paramValidators[key];",
50864
51019
  " if (!validate || validate.skip) return;",
50865
- ' var value = param.in === "query" ? queryValue(String(param.name).toLowerCase()) : headerValue(param.name);',
50866
- ' if (value === undefined) { if (param.required) pm.expect.fail("Required parameter " + param.in + ":" + param.name + " was not sent for " + contract.method + " " + contract.path); return; }',
50867
- ' if (value === "" && param.allowEmptyValue) return;',
50868
- " if (isPlaceholder(value)) return;",
50869
- ' if (!validate(coerceBySchema(value, param.schema))) pm.expect.fail("Parameter " + param.in + ":" + param.name + " failed OpenAPI schema validation for " + contract.method + " " + contract.path + ": " + JSON.stringify(validate.errors || []));',
51020
+ " var value;",
51021
+ ' if (param.decode === "multi") {',
51022
+ " var entries = queryValues(String(param.name).toLowerCase());",
51023
+ ' if (entries.length === 0) { if (param.required) pm.expect.fail("Required parameter " + param.in + ":" + param.name + " was not sent for " + contract.method + " " + contract.path); return; }',
51024
+ ' if (param.allowEmptyValue && entries.length === 1 && entries[0] === "") return;',
51025
+ " if (entries.some(isPlaceholder)) return;",
51026
+ " value = entries.map(function (entry) { return coerceBySchema(decodeComponent(entry), param.items); });",
51027
+ " } else if (param.decode) {",
51028
+ ' var joined = param.in === "query" ? queryValue(String(param.name).toLowerCase()) : headerValue(param.name);',
51029
+ ' if (joined === undefined) { if (param.required) pm.expect.fail("Required parameter " + param.in + ":" + param.name + " was not sent for " + contract.method + " " + contract.path); return; }',
51030
+ ' if (joined === "" && param.allowEmptyValue) return;',
51031
+ " if (isPlaceholder(joined)) return;",
51032
+ ' var parts = joined === "" ? [] : splitDelimited(String(joined), param.decode);',
51033
+ // HTTP header lists allow optional whitespace after the comma, so
51034
+ // header-sourced items are trimmed; query values stay literal.
51035
+ ' if (param.in === "header") parts = parts.map(function (entry) { return entry.trim(); });',
51036
+ " if (parts.some(isPlaceholder)) return;",
51037
+ // Query values arrive percent-encoded while the server validates the
51038
+ // decoded form; header values are never percent-encoded.
51039
+ ' value = parts.map(function (entry) { return coerceBySchema(param.in === "query" ? decodeComponent(entry) : entry, param.items); });',
51040
+ ' } else if (param.in === "path") {',
51041
+ " value = pathParamValue(param.name);",
51042
+ " if (value === undefined) return;",
51043
+ ' if (isPlaceholder(value) || value.charAt(0) === ":" || value.charAt(0) === "{") return;',
51044
+ " value = coerceBySchema(value, param.schema);",
51045
+ ' } else if (param.in === "cookie") {',
51046
+ " value = cookieValue(param.name);",
51047
+ ' if (value === undefined) { if (param.required) pm.expect.fail("Required cookie parameter " + param.name + " was not sent for " + contract.method + " " + contract.path); return; }',
51048
+ " if (isPlaceholder(value)) return;",
51049
+ " value = coerceBySchema(value, param.schema);",
51050
+ " } else {",
51051
+ ' value = param.in === "query" ? queryValue(String(param.name).toLowerCase()) : headerValue(param.name);',
51052
+ ' if (value === undefined) { if (param.required) pm.expect.fail("Required parameter " + param.in + ":" + param.name + " was not sent for " + contract.method + " " + contract.path); return; }',
51053
+ ' if (value === "" && param.allowEmptyValue) return;',
51054
+ " if (isPlaceholder(value)) return;",
51055
+ " if (param.content) {",
51056
+ " var parsed;",
51057
+ ' try { parsed = JSON.parse(value); } catch (parseError) { pm.expect.fail("Parameter " + param.in + ":" + param.name + " declares JSON content but its value is not parseable JSON for " + contract.method + " " + contract.path); return; }',
51058
+ ' if (!validate(parsed)) pm.expect.fail("Parameter " + param.in + ":" + param.name + " failed OpenAPI schema validation for " + contract.method + " " + contract.path + ": " + JSON.stringify(validate.errors || []));',
51059
+ " return;",
51060
+ " }",
51061
+ " value = coerceBySchema(value, param.schema);",
51062
+ " }",
51063
+ ' if (!validate(value)) pm.expect.fail("Parameter " + param.in + ":" + param.name + " failed OpenAPI schema validation for " + contract.method + " " + contract.path + ": " + JSON.stringify(validate.errors || []));',
50870
51064
  " });",
50871
51065
  "});"
50872
51066
  ] : [],
@@ -50989,15 +51183,25 @@ function assertStaticRequestShape(operation, request) {
50989
51183
  if (!contentType) {
50990
51184
  throw new Error(`CONTRACT_STATIC_REQUEST_CHECK_FAILED: ${operation.id} missing required request Content-Type`);
50991
51185
  }
50992
- const actual = contentType.toLowerCase().split(";")[0]?.trim();
50993
- const matches = operation.requestBody.contentTypes.some((expected) => expected.toLowerCase() === actual);
51186
+ const actual = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
51187
+ const matches = operation.requestBody.contentTypes.some((expected) => mediaTypeMatchesPattern(expected.toLowerCase(), actual));
50994
51188
  if (!matches) {
50995
51189
  throw new Error(
50996
51190
  `CONTRACT_STATIC_REQUEST_CHECK_FAILED: ${operation.id} request Content-Type ${contentType} does not match ${operation.requestBody.contentTypes.join(", ")}`
50997
51191
  );
50998
51192
  }
50999
51193
  }
51000
- return collectStaticBodyWarnings(operation, request, contentType);
51194
+ const warnings = collectStaticBodyWarnings(operation, request, contentType);
51195
+ if (operation.requestBody && !operation.requestBody.required && operation.requestBody.contentTypes.length > 0 && hasRequestBody(request) && contentType) {
51196
+ const actual = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
51197
+ const matches = operation.requestBody.contentTypes.some((expected) => mediaTypeMatchesPattern(expected.toLowerCase(), actual));
51198
+ if (!matches) {
51199
+ warnings.push(
51200
+ `CONTRACT_STATIC_REQUEST_CHECK_FAILED: ${operation.id} optional request body Content-Type ${contentType} does not match ${operation.requestBody.contentTypes.join(", ")}`
51201
+ );
51202
+ }
51203
+ }
51204
+ return warnings;
51001
51205
  }
51002
51206
  function requestBodyFieldNames(request, base) {
51003
51207
  const body = asRecord5(request.body);
@@ -51032,28 +51236,80 @@ function mediaTypeMatchesPattern(pattern, actual) {
51032
51236
  return false;
51033
51237
  });
51034
51238
  }
51239
+ function isJsonEncodingContentType(declared) {
51240
+ return declared.split(",").some((candidate) => {
51241
+ const entry = (candidate.split(";")[0] ?? "").trim();
51242
+ return entry === "application/json" || entry.endsWith("+json");
51243
+ });
51244
+ }
51245
+ function isPlaceholderValue(value) {
51246
+ return /^<[^>]*>$/.test(value.trim()) || value.includes("{{");
51247
+ }
51035
51248
  function collectStaticEncodingWarnings(operation, request, base, rule) {
51036
51249
  const encodings = rule.encodings;
51037
- if (!encodings || base !== "multipart/form-data") return [];
51250
+ if (!encodings) return [];
51251
+ const multipart = base === "multipart/form-data";
51038
51252
  const entries = requestBodyEntries(request, base);
51039
51253
  if (!entries) return [];
51040
51254
  const warnings = [];
51255
+ const part = multipart ? "multipart" : "urlencoded";
51041
51256
  for (const [field, encoding] of Object.entries(encodings)) {
51042
- const entry = entries.find((candidate) => String(candidate.key || "") === field);
51043
- if (!entry) continue;
51044
- if (encoding.binary && String(entry.type || "") !== "file") {
51045
- warnings.push(`CONTRACT_ENCODING_MISMATCH: ${operation.id} generated multipart field ${field} should be a file part per its binary schema`);
51046
- }
51047
- if (encoding.contentType) {
51048
- const actual = typeof entry.contentType === "string" ? (entry.contentType.toLowerCase().split(";")[0] ?? "").trim() : "";
51049
- if (!actual) {
51050
- warnings.push(
51051
- `CONTRACT_ENCODING_MISMATCH: ${operation.id} generated multipart field ${field} does not declare Content-Type ${encoding.contentType} from its encoding object`
51052
- );
51053
- } else if (!mediaTypeMatchesPattern(encoding.contentType, actual)) {
51054
- warnings.push(
51055
- `CONTRACT_ENCODING_MISMATCH: ${operation.id} generated multipart field ${field} Content-Type ${actual} does not match declared encoding ${encoding.contentType}`
51056
- );
51257
+ for (const entry of entries.filter((candidate) => String(candidate.key || "") === field)) {
51258
+ if (multipart && encoding.binary && String(entry.type || "") !== "file") {
51259
+ warnings.push(`CONTRACT_ENCODING_MISMATCH: ${operation.id} generated multipart field ${field} should be a file part per its binary schema`);
51260
+ }
51261
+ if (multipart && encoding.contentType) {
51262
+ const actual = typeof entry.contentType === "string" ? (entry.contentType.toLowerCase().split(";")[0] ?? "").trim() : "";
51263
+ if (!actual) {
51264
+ warnings.push(
51265
+ `CONTRACT_ENCODING_MISMATCH: ${operation.id} generated multipart field ${field} does not declare Content-Type ${encoding.contentType} from its encoding object`
51266
+ );
51267
+ } else if (!mediaTypeMatchesPattern(encoding.contentType, actual)) {
51268
+ warnings.push(
51269
+ `CONTRACT_ENCODING_MISMATCH: ${operation.id} generated multipart field ${field} Content-Type ${actual} does not match declared encoding ${encoding.contentType}`
51270
+ );
51271
+ }
51272
+ }
51273
+ if (encoding.contentType && isJsonEncodingContentType(encoding.contentType) && String(entry.type || "text") !== "file") {
51274
+ const value = typeof entry.value === "string" ? entry.value : "";
51275
+ if (value.trim() && !isPlaceholderValue(value)) {
51276
+ try {
51277
+ JSON.parse(value);
51278
+ } catch {
51279
+ warnings.push(
51280
+ `CONTRACT_ENCODING_MISMATCH: ${operation.id} generated ${part} field ${field} declares JSON encoding ${encoding.contentType} but its value is not parseable JSON`
51281
+ );
51282
+ }
51283
+ }
51284
+ }
51285
+ }
51286
+ }
51287
+ return warnings;
51288
+ }
51289
+ function coerceFormValue(value, schema2) {
51290
+ const record = asRecord5(schema2);
51291
+ const type2 = record?.type;
51292
+ const types2 = Array.isArray(type2) ? type2 : [type2];
51293
+ if ((types2.includes("integer") || types2.includes("number")) && /^-?[0-9]+([.][0-9]+)?([eE][+-]?[0-9]+)?$/.test(value.trim())) return Number(value);
51294
+ if (types2.includes("boolean") && (value === "true" || value === "false")) return value === "true";
51295
+ return value;
51296
+ }
51297
+ function collectStaticFieldSchemaWarnings(operation, request, base, rule) {
51298
+ const fieldSchemas = rule.fieldSchemas;
51299
+ if (!fieldSchemas) return [];
51300
+ const entries = requestBodyEntries(request, base);
51301
+ if (!entries) return [];
51302
+ const part = base === "multipart/form-data" ? "multipart" : "urlencoded";
51303
+ const warnings = [];
51304
+ for (const [field, schema2] of Object.entries(fieldSchemas)) {
51305
+ const validate2 = compileSchemaValidator(schema2);
51306
+ if (!validate2) continue;
51307
+ for (const entry of entries.filter((candidate) => String(candidate.key || "") === field)) {
51308
+ if (String(entry.type || "text") === "file") continue;
51309
+ const value = typeof entry.value === "string" ? entry.value : "";
51310
+ if (!value.trim() || isPlaceholderValue(value)) continue;
51311
+ if (!validate2(coerceFormValue(value, schema2))) {
51312
+ warnings.push(`CONTRACT_FORM_FIELD_SCHEMA_MISMATCH: ${operation.id} generated ${part} field ${field} value does not match its schema`);
51057
51313
  }
51058
51314
  }
51059
51315
  }
@@ -51065,7 +51321,10 @@ function collectStaticBodyWarnings(operation, request, contentType) {
51065
51321
  const base = (contentType || "").toLowerCase().split(";")[0]?.trim() ?? "";
51066
51322
  const rule = rules[base];
51067
51323
  if (!rule) return [];
51068
- const warnings = [...collectStaticEncodingWarnings(operation, request, base, rule)];
51324
+ const warnings = [
51325
+ ...collectStaticEncodingWarnings(operation, request, base, rule),
51326
+ ...collectStaticFieldSchemaWarnings(operation, request, base, rule)
51327
+ ];
51069
51328
  const names = requestBodyFieldNames(request, base);
51070
51329
  if (!names) return warnings;
51071
51330
  const present = new Set(names);