@postman-cse/onboarding-bootstrap 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -49804,6 +49804,38 @@ function normalizeSchema(ctx, schema2, options) {
49804
49804
  const sourceSchema = { ...record };
49805
49805
  const nullable = sourceSchema.nullable === true && ctx.version === "3.0";
49806
49806
  delete sourceSchema.nullable;
49807
+ const discriminator = asRecord3(sourceSchema.discriminator);
49808
+ if (discriminator && typeof discriminator.propertyName === "string" && discriminator.propertyName) {
49809
+ const propertyName = discriminator.propertyName;
49810
+ const branchKey = Array.isArray(sourceSchema.oneOf) ? "oneOf" : Array.isArray(sourceSchema.anyOf) ? "anyOf" : "";
49811
+ const members = branchKey ? sourceSchema[branchKey] : [];
49812
+ const memberRefs = members.map((member) => {
49813
+ const memberRecord = asRecord3(member);
49814
+ const memberRef = memberRecord && Object.keys(memberRecord).length === 1 && typeof memberRecord.$ref === "string" ? memberRecord.$ref : "";
49815
+ return memberRef.startsWith("#/") ? memberRef : "";
49816
+ });
49817
+ if (branchKey && members.length > 0 && memberRefs.every(Boolean)) {
49818
+ const valuesByRef = /* @__PURE__ */ new Map();
49819
+ const refByMappingKey = /* @__PURE__ */ new Map();
49820
+ for (const [value, target] of Object.entries(asRecord3(discriminator.mapping) ?? {})) {
49821
+ if (typeof target !== "string" || !target) continue;
49822
+ const targetRef = target.startsWith("#/") ? target : `#/components/schemas/${target}`;
49823
+ valuesByRef.set(targetRef, [...valuesByRef.get(targetRef) ?? [], value]);
49824
+ refByMappingKey.set(value, targetRef);
49825
+ }
49826
+ sourceSchema[branchKey] = members.map((member, index) => {
49827
+ const memberRef = memberRefs[index] ?? "";
49828
+ const tail = (memberRef.split("/").pop() ?? "").replace(/~1/g, "/").replace(/~0/g, "~");
49829
+ const values = [...valuesByRef.get(memberRef) ?? []];
49830
+ const tailClaimedElsewhere = refByMappingKey.has(tail) && refByMappingKey.get(tail) !== memberRef;
49831
+ if (tail && !tailClaimedElsewhere && !values.includes(tail)) values.push(tail);
49832
+ const dispatch = values.length === 1 ? { const: values[0] } : { enum: values };
49833
+ return { allOf: [member, { type: "object", required: [propertyName], properties: { [propertyName]: dispatch } }] };
49834
+ });
49835
+ } else {
49836
+ ctx.notes.add("discriminator");
49837
+ }
49838
+ }
49807
49839
  const directionStrippedFlag = ctx.direction === "request" ? "readOnly" : "writeOnly";
49808
49840
  const strippedProperties = /* @__PURE__ */ new Set();
49809
49841
  const rawProperties = asRecord3(sourceSchema.properties);
@@ -49976,7 +50008,7 @@ function packSchema(root, schema2, version, direction = "response") {
49976
50008
  if (schema2 === true) return { schema: { $schema: rootDialect(root, version) } };
49977
50009
  if (schema2 === false) return unsupported("Boolean false JSON Schema rejects every instance and is unsupported");
49978
50010
  const dialect = rootDialect(root, version, asRecord3(schema2) ?? void 0);
49979
- const ctx = { root, version, direction, dialect, defs: /* @__PURE__ */ new Map() };
50011
+ const ctx = { root, version, direction, dialect, defs: /* @__PURE__ */ new Map(), notes: /* @__PURE__ */ new Set() };
49980
50012
  const normalized = normalizeSchema(ctx, schema2, { depth: 0, rootSchema: true });
49981
50013
  const message = hasUnsupported(normalized);
49982
50014
  if (message) return unsupported(message);
@@ -50001,7 +50033,9 @@ function packSchema(root, schema2, version, direction = "response") {
50001
50033
  if (!normalizedRecord) return unsupported("Referenced schemas require an object root schema");
50002
50034
  normalizedRecord.$defs = Object.fromEntries([...ctx.defs.values()].map((entry) => [entry.name, entry.schema]));
50003
50035
  }
50004
- return { schema: normalized };
50036
+ const packed = { schema: normalized };
50037
+ if (ctx.notes.size > 0) packed.notes = [...ctx.notes].sort();
50038
+ return packed;
50005
50039
  } catch (error2) {
50006
50040
  return unsupported(error2 instanceof Error ? error2.message : String(error2));
50007
50041
  }
@@ -50174,6 +50208,7 @@ function securityCheckFor(schemeName, scheme) {
50174
50208
  const httpScheme = String(scheme.scheme || "").toLowerCase();
50175
50209
  if (httpScheme === "basic") return { scheme: schemeName, kind, checkable: true, prefix: "Basic " };
50176
50210
  if (httpScheme === "bearer") return { scheme: schemeName, kind, checkable: true, prefix: "Bearer " };
50211
+ if (httpScheme) return { scheme: schemeName, kind, checkable: true, prefix: `${httpScheme.charAt(0).toUpperCase()}${httpScheme.slice(1)} ` };
50177
50212
  return { scheme: schemeName, kind, checkable: true, in: "header", name: "Authorization" };
50178
50213
  }
50179
50214
  if (scheme?.type === "oauth2" || scheme?.type === "openIdConnect") {
@@ -50207,7 +50242,18 @@ var IGNORED_HEADER_PARAMS = /* @__PURE__ */ new Set(["accept", "content-type", "
50207
50242
  function isIgnoredParameter(location2, name) {
50208
50243
  return location2 === "header" && IGNORED_HEADER_PARAMS.has(name.toLowerCase());
50209
50244
  }
50210
- function collectSerializationWarnings(root, pathItem, operation) {
50245
+ function jsonContentParameterMedia(param) {
50246
+ const content = asRecord4(param.content);
50247
+ if (!content) return void 0;
50248
+ const entries = Object.entries(content);
50249
+ if (entries.length !== 1) return void 0;
50250
+ const [contentType, mediaObject] = entries[0];
50251
+ const base = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
50252
+ if (!isJsonBaseType(base)) return void 0;
50253
+ const schema2 = asRecord4(mediaObject)?.schema;
50254
+ return schema2 === void 0 ? void 0 : schema2;
50255
+ }
50256
+ function collectSerializationWarnings(root, pathItem, operation, decodedKeys) {
50211
50257
  const warnings = [];
50212
50258
  for (const param of resolvedParameters(root, pathItem, operation)) {
50213
50259
  const location2 = String(param.in || "").toLowerCase();
@@ -50217,7 +50263,9 @@ function collectSerializationWarnings(root, pathItem, operation) {
50217
50263
  const style = typeof param.style === "string" ? param.style : defaultStyle;
50218
50264
  const defaultExplode = style === "form";
50219
50265
  const explode = typeof param.explode === "boolean" ? param.explode : defaultExplode;
50220
- if (style !== defaultStyle || explode !== defaultExplode || param.allowReserved === true || param.content !== void 0) {
50266
+ const unvalidatedContent = param.content !== void 0 && (jsonContentParameterMedia(param) === void 0 || location2 !== "query" && location2 !== "header");
50267
+ if (style !== defaultStyle || explode !== defaultExplode || param.allowReserved === true || unvalidatedContent) {
50268
+ if (decodedKeys.has(`${location2}:${name.toLowerCase()}`) && param.allowReserved !== true && param.content === void 0) continue;
50221
50269
  warnings.push(`CONTRACT_PARAM_SERIALIZATION_NOT_VALIDATED: parameter ${location2}:${name} declares non-default style, explode, allowReserved, or content and its serialization is not validated`);
50222
50270
  }
50223
50271
  }
@@ -50232,7 +50280,27 @@ function packedScalarSchema(packed) {
50232
50280
  if (!types2.every((entry) => typeof entry === "string" && SCALAR_SCHEMA_TYPES.has(entry))) return void 0;
50233
50281
  return packed.schema;
50234
50282
  }
50235
- function collectParameterChecks(root, pathItem, operation, version, operationId, warnings) {
50283
+ function packedArrayItemsSchema(packed) {
50284
+ if (packed.unsupported || packed.schema === void 0) return void 0;
50285
+ const record = asRecord4(packed.schema);
50286
+ if (!record) return void 0;
50287
+ const types2 = Array.isArray(record.type) ? record.type : [record.type];
50288
+ if (types2.length !== 1 || types2[0] !== "array") return void 0;
50289
+ if (record.prefixItems !== void 0 || Array.isArray(record.items)) return void 0;
50290
+ if (record.items === void 0) return {};
50291
+ const items = asRecord4(record.items);
50292
+ if (!items || typeof items.$ref === "string") return void 0;
50293
+ const itemTypes = Array.isArray(items.type) ? items.type : [items.type];
50294
+ if (!itemTypes.every((entry) => typeof entry === "string" && SCALAR_SCHEMA_TYPES.has(entry))) return void 0;
50295
+ return items;
50296
+ }
50297
+ var QUERY_ARRAY_DECODES = {
50298
+ "form:true": "multi",
50299
+ "form:false": "csv",
50300
+ "spaceDelimited:false": "ssv",
50301
+ "pipeDelimited:false": "pipes"
50302
+ };
50303
+ function collectParameterChecks(root, pathItem, operation, version, operationId, pathTemplate, warnings) {
50236
50304
  const securityKeys = collectSecurityApiKeys(root, operation);
50237
50305
  const checks = [];
50238
50306
  const seen = /* @__PURE__ */ new Set();
@@ -50245,32 +50313,65 @@ function collectParameterChecks(root, pathItem, operation, version, operationId,
50245
50313
  }).filter((param) => Boolean(param));
50246
50314
  for (const param of orderedParams) {
50247
50315
  const location2 = String(param.in || "").toLowerCase();
50248
- if (location2 !== "query" && location2 !== "header") continue;
50316
+ if (location2 !== "query" && location2 !== "header" && location2 !== "path" && location2 !== "cookie") continue;
50249
50317
  const name = String(param.name || "");
50250
50318
  if (!name || isIgnoredParameter(location2, name)) continue;
50251
50319
  const key = `${location2}:${name.toLowerCase()}`;
50252
50320
  if (seen.has(key)) continue;
50253
50321
  seen.add(key);
50254
50322
  if (securityKeys.has(key)) continue;
50323
+ const contentMedia = jsonContentParameterMedia(param);
50324
+ if (contentMedia !== void 0 && (location2 === "query" || location2 === "header")) {
50325
+ const packed2 = packSchema(root, contentMedia, version, "request");
50326
+ warnings.push(...packNoteWarnings(packed2, `parameter ${location2}:${name} of ${operationId}`));
50327
+ if (packed2.unsupported) {
50328
+ warnings.push(`CONTRACT_SCHEMA_NOT_COMPILED: parameter ${location2}:${name} schema on ${operationId} skipped (${packed2.unsupported})`);
50329
+ } else if (packed2.schema !== void 0) {
50330
+ const check2 = { in: location2, name, required: param.required === true, content: true, schema: packed2.schema };
50331
+ if (location2 === "query" && param.allowEmptyValue === true) check2.allowEmptyValue = true;
50332
+ checks.push(check2);
50333
+ }
50334
+ continue;
50335
+ }
50255
50336
  if (param.content !== void 0 || param.schema === void 0) continue;
50256
50337
  const defaultStyle = DEFAULT_PARAM_STYLES[location2];
50257
50338
  const style = typeof param.style === "string" ? param.style : defaultStyle;
50258
50339
  const defaultExplode = style === "form";
50259
50340
  const explode = typeof param.explode === "boolean" ? param.explode : defaultExplode;
50260
- if (style !== defaultStyle || explode !== defaultExplode) continue;
50341
+ const defaultSerialization = style === defaultStyle && explode === defaultExplode;
50261
50342
  const packed = packSchema(root, param.schema, version);
50343
+ const noteWarnings = packNoteWarnings(packed, `parameter ${location2}:${name} of ${operationId}`);
50344
+ if (defaultSerialization) warnings.push(...noteWarnings);
50262
50345
  if (packed.unsupported) {
50263
- warnings.push(`CONTRACT_SCHEMA_NOT_COMPILED: parameter ${location2}:${name} schema on ${operationId} skipped (${packed.unsupported})`);
50346
+ if (defaultSerialization) warnings.push(`CONTRACT_SCHEMA_NOT_COMPILED: parameter ${location2}:${name} schema on ${operationId} skipped (${packed.unsupported})`);
50264
50347
  continue;
50265
50348
  }
50266
- const schema2 = packedScalarSchema(packed);
50267
- if (schema2 === void 0) continue;
50268
- const check = { in: location2, name, required: param.required === true, schema: schema2 };
50349
+ if (location2 === "path" && !pathTemplate.split("/").includes(`{${name}}`)) continue;
50350
+ const scalarSchema = packedScalarSchema(packed);
50351
+ if (scalarSchema !== void 0) {
50352
+ if (!defaultSerialization) continue;
50353
+ const check2 = { in: location2, name, required: param.required === true, schema: scalarSchema };
50354
+ if (location2 === "query" && param.allowEmptyValue === true) check2.allowEmptyValue = true;
50355
+ checks.push(check2);
50356
+ continue;
50357
+ }
50358
+ if (location2 !== "query" && location2 !== "header") continue;
50359
+ const items = packedArrayItemsSchema(packed);
50360
+ if (items === void 0) continue;
50361
+ const decode = location2 === "query" ? QUERY_ARRAY_DECODES[`${style}:${explode}`] : style === "simple" && !explode ? "csv" : void 0;
50362
+ if (!decode) continue;
50363
+ if (!defaultSerialization) warnings.push(...noteWarnings);
50364
+ const check = { in: location2, name, required: param.required === true, schema: packed.schema, decode, items };
50269
50365
  if (location2 === "query" && param.allowEmptyValue === true) check.allowEmptyValue = true;
50270
50366
  checks.push(check);
50271
50367
  }
50272
50368
  return checks.length > 0 ? checks : void 0;
50273
50369
  }
50370
+ function packNoteWarnings(packed, context) {
50371
+ return (packed.notes ?? []).map(
50372
+ (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`
50373
+ );
50374
+ }
50274
50375
  function collectDeclaredQueryParameters(root, pathItem, operation) {
50275
50376
  const names = /* @__PURE__ */ new Set();
50276
50377
  for (const param of resolvedParameters(root, pathItem, operation)) {
@@ -50348,7 +50449,10 @@ function propertyIsBinary(root, properties, name) {
50348
50449
  }
50349
50450
  if (!schema2) return false;
50350
50451
  if (schema2.format === "binary") return true;
50351
- return typeof schema2.contentMediaType === "string" && schema2.contentMediaType.toLowerCase().startsWith("application/octet-stream");
50452
+ if (typeof schema2.contentMediaType !== "string" || typeof schema2.contentEncoding === "string") return false;
50453
+ const media = schema2.contentMediaType.toLowerCase().split(";")[0]?.trim() ?? "";
50454
+ if (!media) return false;
50455
+ return media !== "application/json" && !media.endsWith("+json") && !media.startsWith("text/");
50352
50456
  }
50353
50457
  function fieldEncodings(root, base, mediaObject, properties) {
50354
50458
  const declared = asRecord4(mediaObject?.encoding);
@@ -50366,6 +50470,9 @@ function fieldEncodings(root, base, mediaObject, properties) {
50366
50470
  if (typeof encoding.contentType === "string" && encoding.contentType.trim()) {
50367
50471
  entry.contentType = encoding.contentType.toLowerCase();
50368
50472
  }
50473
+ if (base === "multipart/form-data" && asRecord4(encoding.headers)) {
50474
+ entry.hasHeaders = true;
50475
+ }
50369
50476
  if (base === "application/x-www-form-urlencoded") {
50370
50477
  const style = typeof encoding.style === "string" ? encoding.style : "form";
50371
50478
  const explode = typeof encoding.explode === "boolean" ? encoding.explode : style === "form";
@@ -50378,7 +50485,22 @@ function fieldEncodings(root, base, mediaObject, properties) {
50378
50485
  }
50379
50486
  return Object.keys(encodings).length > 0 ? encodings : void 0;
50380
50487
  }
50381
- function requestBodyFieldRules(root, content) {
50488
+ function formFieldSchemas(root, version, properties, context, warnings) {
50489
+ const schemas = {};
50490
+ for (const name of Object.keys(properties)) {
50491
+ const packed = packSchema(root, properties[name], version, "request");
50492
+ warnings.push(...packNoteWarnings(packed, `field ${name} of ${context}`));
50493
+ if (packed.unsupported) {
50494
+ warnings.push(`CONTRACT_SCHEMA_NOT_COMPILED: field ${name} of ${context} skipped (${packed.unsupported})`);
50495
+ continue;
50496
+ }
50497
+ if (packed.schema === void 0) continue;
50498
+ const schema2 = packedScalarSchema(packed);
50499
+ if (schema2 !== void 0) schemas[name] = schema2;
50500
+ }
50501
+ return Object.keys(schemas).length > 0 ? schemas : void 0;
50502
+ }
50503
+ function requestBodyFieldRules(root, content, version, operationId, warnings) {
50382
50504
  const rules = {};
50383
50505
  for (const [contentType, mediaObject] of Object.entries(content)) {
50384
50506
  const base = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
@@ -50388,10 +50510,13 @@ function requestBodyFieldRules(root, content) {
50388
50510
  if (!merged) continue;
50389
50511
  const readOnly = Object.keys(merged.properties).filter((name) => propertyIsReadOnly(root, merged.properties, name));
50390
50512
  const required = merged.required.filter((name) => !propertyIsReadOnly(root, merged.properties, name));
50391
- const encodings = BODY_FIELD_RULE_TYPES.has(base) ? fieldEncodings(root, base, mediaRecord, merged.properties) : void 0;
50392
- if (required.length > 0 || readOnly.length > 0 || encodings) {
50513
+ const formRules = BODY_FIELD_RULE_TYPES.has(base);
50514
+ const encodings = formRules ? fieldEncodings(root, base, mediaRecord, merged.properties) : void 0;
50515
+ const fieldSchemas = formRules ? formFieldSchemas(root, version, merged.properties, `request body ${contentType} of ${operationId}`, warnings) : void 0;
50516
+ if (required.length > 0 || readOnly.length > 0 || encodings || fieldSchemas) {
50393
50517
  const rule = { required, readOnly };
50394
50518
  if (encodings) rule.encodings = encodings;
50519
+ if (fieldSchemas) rule.fieldSchemas = fieldSchemas;
50395
50520
  rules[base] = rule;
50396
50521
  }
50397
50522
  }
@@ -50412,6 +50537,7 @@ function requestBodyJsonSchemas(root, content, version, operationId, warnings) {
50412
50537
  }
50413
50538
  if (schema2 === void 0) continue;
50414
50539
  const packed = packSchema(root, schema2, version, "request");
50540
+ warnings.push(...packNoteWarnings(packed, `request body ${contentType} of ${operationId}`));
50415
50541
  if (mediaRecord) validateExamples(root, mediaRecord, packed, contentType, operationId, exampleWarnings);
50416
50542
  if (packed.unsupported) {
50417
50543
  warnings.push(`CONTRACT_REQUEST_SCHEMA_NOT_VALIDATED: request body schema for ${contentType} on ${operationId} is not validated (${packed.unsupported})`);
@@ -50426,7 +50552,7 @@ function collectRequestBody(root, operation, version, operationId, warnings) {
50426
50552
  const body = resolveInternalRef(root, operation.requestBody);
50427
50553
  if (!body) return void 0;
50428
50554
  const content = asRecord4(body.content);
50429
- const fieldRules = content ? requestBodyFieldRules(root, content) : void 0;
50555
+ const fieldRules = content ? requestBodyFieldRules(root, content, version, operationId, warnings) : void 0;
50430
50556
  if (fieldRules) {
50431
50557
  for (const [base, rule] of Object.entries(fieldRules)) {
50432
50558
  for (const [field, encoding] of Object.entries(rule.encodings ?? {})) {
@@ -50435,6 +50561,11 @@ function collectRequestBody(root, operation, version, operationId, warnings) {
50435
50561
  `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`
50436
50562
  );
50437
50563
  }
50564
+ if (encoding.hasHeaders) {
50565
+ warnings.push(
50566
+ `CONTRACT_ENCODING_HEADERS_NOT_VALIDATED: ${base} request body field ${field} on ${operationId} declares per-part headers that generated formdata entries cannot carry`
50567
+ );
50568
+ }
50438
50569
  }
50439
50570
  }
50440
50571
  }
@@ -50482,6 +50613,7 @@ function responseContent(root, version, response, context, warnings) {
50482
50613
  const mediaRecord = asRecord4(mediaObject);
50483
50614
  const schema2 = mediaRecord?.schema;
50484
50615
  let packed = schema2 === void 0 ? {} : packSchema(root, schema2, version);
50616
+ for (const warning2 of packNoteWarnings(packed, `response ${contentType} of ${context}`)) warnings.add(warning2);
50485
50617
  if (isSchemaGraphOverflow(packed)) {
50486
50618
  warnings.add(`CONTRACT_SCHEMA_NOT_COMPILED: response schema for ${contentType} on ${context} skipped (${packed.unsupported})`);
50487
50619
  packed = {};
@@ -50513,12 +50645,18 @@ function responseHeaders(root, version, response, context, warnings) {
50513
50645
  continue;
50514
50646
  }
50515
50647
  const packed = packSchema(root, header.schema, version);
50648
+ for (const warning2 of packNoteWarnings(packed, `response header ${name} of ${context}`)) warnings.add(warning2);
50516
50649
  if (isSchemaGraphOverflow(packed)) {
50517
50650
  warnings.add(`CONTRACT_SCHEMA_NOT_COMPILED: response header ${name} schema on ${context} skipped (${packed.unsupported})`);
50518
50651
  entries.push({ name, required });
50519
50652
  continue;
50520
50653
  }
50521
50654
  if (!packed.unsupported && packed.schema !== void 0 && packedScalarSchema(packed) === void 0) {
50655
+ const items = packedArrayItemsSchema(packed);
50656
+ if (items !== void 0) {
50657
+ entries.push({ name, required, schema: packed.schema, items });
50658
+ continue;
50659
+ }
50522
50660
  warnings.add(`CONTRACT_HEADER_SCHEMA_NOT_VALIDATED: response header ${name} on ${context} declares a non-scalar schema and its value is not validated`);
50523
50661
  entries.push({ name, required });
50524
50662
  continue;
@@ -50577,10 +50715,14 @@ function buildContractIndex(root) {
50577
50715
  path8,
50578
50716
  ...operationServers(root, pathItem, operation).map((server) => joinPaths(serverPathPrefix(server), path8))
50579
50717
  ].map(normalizePath))];
50718
+ const operationId = `${lowerMethod.toUpperCase()} ${path8}`;
50580
50719
  const opWarnings = [];
50581
50720
  opWarnings.push(...responseWarnings);
50582
50721
  opWarnings.push(...collectSecuritySchemeWarnings(root, operation));
50583
- opWarnings.push(...collectSerializationWarnings(root, pathItem, operation));
50722
+ const parameterChecks = collectParameterChecks(root, pathItem, operation, version, operationId, path8, opWarnings);
50723
+ const checkedKeys = new Set((parameterChecks ?? []).map((check) => `${check.in}:${check.name.toLowerCase()}`));
50724
+ const decodedKeys = new Set((parameterChecks ?? []).filter((check) => check.decode).map((check) => `${check.in}:${check.name.toLowerCase()}`));
50725
+ opWarnings.push(...collectSerializationWarnings(root, pathItem, operation, decodedKeys));
50584
50726
  if (operation.deprecated === true) {
50585
50727
  opWarnings.push(`CONTRACT_OPERATION_DEPRECATED: ${lowerMethod.toUpperCase()} ${path8} is marked deprecated in the OpenAPI document`);
50586
50728
  }
@@ -50589,17 +50731,19 @@ function buildContractIndex(root) {
50589
50731
  opWarnings.push(`CONTRACT_SECURITY_NOT_VALIDATED: security parameter ${parameter.in}:${parameter.name} is not statically required in generated requests`);
50590
50732
  }
50591
50733
  for (const parameter of requiredParameters.filter((entry) => entry.in === "cookie" && !entry.securityDerived)) {
50592
- opWarnings.push(`CONTRACT_COOKIE_PARAM_NOT_VALIDATED: required cookie parameter ${parameter.name} is not statically required in generated requests`);
50734
+ 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`);
50593
50735
  }
50594
50736
  const pathParamWarnings = /* @__PURE__ */ new Set();
50595
50737
  for (const param of resolvedParameters(root, pathItem, operation)) {
50596
50738
  if (String(param.in || "").toLowerCase() !== "path") continue;
50597
50739
  const name = String(param.name || "");
50598
- if (name) pathParamWarnings.add(`CONTRACT_PATH_PARAM_NOT_VALIDATED: path parameter ${name} value is not validated at runtime`);
50740
+ if (name && !checkedKeys.has(`path:${name.toLowerCase()}`)) {
50741
+ pathParamWarnings.add(`CONTRACT_PATH_PARAM_NOT_VALIDATED: path parameter ${name} value is not validated at runtime`);
50742
+ }
50599
50743
  }
50600
50744
  opWarnings.push(...pathParamWarnings);
50601
50745
  operations.push({
50602
- id: `${lowerMethod.toUpperCase()} ${path8}`,
50746
+ id: operationId,
50603
50747
  method: lowerMethod.toUpperCase(),
50604
50748
  path: path8,
50605
50749
  pointer: `/paths/${path8.replace(/~/g, "~0").replace(/\//g, "~1")}/${lowerMethod}`,
@@ -50607,8 +50751,8 @@ function buildContractIndex(root) {
50607
50751
  responses: contractResponses,
50608
50752
  requiredParameters,
50609
50753
  declaredQueryParameters: collectDeclaredQueryParameters(root, pathItem, operation),
50610
- parameterChecks: collectParameterChecks(root, pathItem, operation, version, `${lowerMethod.toUpperCase()} ${path8}`, opWarnings),
50611
- requestBody: collectRequestBody(root, operation, version, `${lowerMethod.toUpperCase()} ${path8}`, opWarnings),
50754
+ parameterChecks,
50755
+ requestBody: collectRequestBody(root, operation, version, operationId, opWarnings),
50612
50756
  security: collectSecurityRuntimeChecks(root, operation),
50613
50757
  warnings: opWarnings
50614
50758
  });
@@ -50815,8 +50959,11 @@ function createContractScript(operation, warnings = []) {
50815
50959
  " if (!actual) return;",
50816
50960
  ' if (header.unsupported) pm.expect.fail("OpenAPI response header unsupported for " + contract.method + " " + contract.path + ": " + header.unsupported);',
50817
50961
  " var headerValidator = validators[selected.key] && validators[selected.key].__headers && validators[selected.key].__headers[String(header.name).toLowerCase()];",
50818
- " if (headerValidator && headerValidator.skip) return;",
50819
- ' if (headerValidator && !headerValidator(coerceBySchema(actual, header.schema))) pm.expect.fail("OpenAPI response header validation failed for " + header.name + ": " + JSON.stringify(headerValidator.errors || []));',
50962
+ " if (!headerValidator || headerValidator.skip) return;",
50963
+ " var expected;",
50964
+ ' if (header.items) { var joined = String(actual).trim(); expected = joined === "" ? [] : joined.split(",").map(function (entry) { return coerceBySchema(entry.trim(), header.items); }); }',
50965
+ " else expected = coerceBySchema(actual, header.schema);",
50966
+ ' if (!headerValidator(expected)) pm.expect.fail("OpenAPI response header validation failed for " + header.name + ": " + JSON.stringify(headerValidator.errors || []));',
50820
50967
  " });",
50821
50968
  "});",
50822
50969
  "pm.test('Response body matches OpenAPI body contract', function () {",
@@ -50872,17 +51019,64 @@ function createContractScript(operation, warnings = []) {
50872
51019
  ...operation.parameterChecks && operation.parameterChecks.length > 0 ? [
50873
51020
  "pm.test('Request parameters match OpenAPI schemas', function () {",
50874
51021
  ' 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; }',
51022
+ ' 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; }',
50875
51023
  ' 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; }',
51024
+ ' 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; }',
51025
+ ' 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; }); }',
51026
+ // Server path prefixes sit ahead of the template segments, so the
51027
+ // template aligns against the trailing request segments.
51028
+ ' 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; }',
50876
51029
  ' function isPlaceholder(value) { var text = String(value).trim(); return /^<[^<>]*>$/.test(text) || text.indexOf("{{") !== -1; }',
51030
+ ' function splitDelimited(value, decode) { if (decode === "csv") return value.split(","); if (decode === "ssv") return value.split(/%20| /); return value.split(/%7C|\\|/i); }',
51031
+ " function decodeComponent(value) { try { return decodeURIComponent(value); } catch (ignored) { return value; } }",
50877
51032
  " (contract.parameters || []).forEach(function (param) {",
50878
51033
  ' var key = param.in + ":" + String(param.name).toLowerCase();',
50879
51034
  " var validate = paramValidators[key];",
50880
51035
  " if (!validate || validate.skip) return;",
50881
- ' var value = param.in === "query" ? queryValue(String(param.name).toLowerCase()) : headerValue(param.name);',
50882
- ' if (value === undefined) { if (param.required) pm.expect.fail("Required parameter " + param.in + ":" + param.name + " was not sent for " + contract.method + " " + contract.path); return; }',
50883
- ' if (value === "" && param.allowEmptyValue) return;',
50884
- " if (isPlaceholder(value)) return;",
50885
- ' 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 || []));',
51036
+ " var value;",
51037
+ ' if (param.decode === "multi") {',
51038
+ " var entries = queryValues(String(param.name).toLowerCase());",
51039
+ ' 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; }',
51040
+ ' if (param.allowEmptyValue && entries.length === 1 && entries[0] === "") return;',
51041
+ " if (entries.some(isPlaceholder)) return;",
51042
+ " value = entries.map(function (entry) { return coerceBySchema(decodeComponent(entry), param.items); });",
51043
+ " } else if (param.decode) {",
51044
+ ' var joined = param.in === "query" ? queryValue(String(param.name).toLowerCase()) : headerValue(param.name);',
51045
+ ' if (joined === undefined) { if (param.required) pm.expect.fail("Required parameter " + param.in + ":" + param.name + " was not sent for " + contract.method + " " + contract.path); return; }',
51046
+ ' if (joined === "" && param.allowEmptyValue) return;',
51047
+ " if (isPlaceholder(joined)) return;",
51048
+ ' var parts = joined === "" ? [] : splitDelimited(String(joined), param.decode);',
51049
+ // HTTP header lists allow optional whitespace after the comma, so
51050
+ // header-sourced items are trimmed; query values stay literal.
51051
+ ' if (param.in === "header") parts = parts.map(function (entry) { return entry.trim(); });',
51052
+ " if (parts.some(isPlaceholder)) return;",
51053
+ // Query values arrive percent-encoded while the server validates the
51054
+ // decoded form; header values are never percent-encoded.
51055
+ ' value = parts.map(function (entry) { return coerceBySchema(param.in === "query" ? decodeComponent(entry) : entry, param.items); });',
51056
+ ' } else if (param.in === "path") {',
51057
+ " value = pathParamValue(param.name);",
51058
+ " if (value === undefined) return;",
51059
+ ' if (isPlaceholder(value) || value.charAt(0) === ":" || value.charAt(0) === "{") return;',
51060
+ " value = coerceBySchema(value, param.schema);",
51061
+ ' } else if (param.in === "cookie") {',
51062
+ " value = cookieValue(param.name);",
51063
+ ' if (value === undefined) { if (param.required) pm.expect.fail("Required cookie parameter " + param.name + " was not sent for " + contract.method + " " + contract.path); return; }',
51064
+ " if (isPlaceholder(value)) return;",
51065
+ " value = coerceBySchema(value, param.schema);",
51066
+ " } else {",
51067
+ ' value = param.in === "query" ? queryValue(String(param.name).toLowerCase()) : headerValue(param.name);',
51068
+ ' if (value === undefined) { if (param.required) pm.expect.fail("Required parameter " + param.in + ":" + param.name + " was not sent for " + contract.method + " " + contract.path); return; }',
51069
+ ' if (value === "" && param.allowEmptyValue) return;',
51070
+ " if (isPlaceholder(value)) return;",
51071
+ " if (param.content) {",
51072
+ " var parsed;",
51073
+ ' 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; }',
51074
+ ' if (!validate(parsed)) pm.expect.fail("Parameter " + param.in + ":" + param.name + " failed OpenAPI schema validation for " + contract.method + " " + contract.path + ": " + JSON.stringify(validate.errors || []));',
51075
+ " return;",
51076
+ " }",
51077
+ " value = coerceBySchema(value, param.schema);",
51078
+ " }",
51079
+ ' if (!validate(value)) pm.expect.fail("Parameter " + param.in + ":" + param.name + " failed OpenAPI schema validation for " + contract.method + " " + contract.path + ": " + JSON.stringify(validate.errors || []));',
50886
51080
  " });",
50887
51081
  "});"
50888
51082
  ] : [],
@@ -51005,15 +51199,25 @@ function assertStaticRequestShape(operation, request) {
51005
51199
  if (!contentType) {
51006
51200
  throw new Error(`CONTRACT_STATIC_REQUEST_CHECK_FAILED: ${operation.id} missing required request Content-Type`);
51007
51201
  }
51008
- const actual = contentType.toLowerCase().split(";")[0]?.trim();
51009
- const matches = operation.requestBody.contentTypes.some((expected) => expected.toLowerCase() === actual);
51202
+ const actual = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
51203
+ const matches = operation.requestBody.contentTypes.some((expected) => mediaTypeMatchesPattern(expected.toLowerCase(), actual));
51010
51204
  if (!matches) {
51011
51205
  throw new Error(
51012
51206
  `CONTRACT_STATIC_REQUEST_CHECK_FAILED: ${operation.id} request Content-Type ${contentType} does not match ${operation.requestBody.contentTypes.join(", ")}`
51013
51207
  );
51014
51208
  }
51015
51209
  }
51016
- return collectStaticBodyWarnings(operation, request, contentType);
51210
+ const warnings = collectStaticBodyWarnings(operation, request, contentType);
51211
+ if (operation.requestBody && !operation.requestBody.required && operation.requestBody.contentTypes.length > 0 && hasRequestBody(request) && contentType) {
51212
+ const actual = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
51213
+ const matches = operation.requestBody.contentTypes.some((expected) => mediaTypeMatchesPattern(expected.toLowerCase(), actual));
51214
+ if (!matches) {
51215
+ warnings.push(
51216
+ `CONTRACT_STATIC_REQUEST_CHECK_FAILED: ${operation.id} optional request body Content-Type ${contentType} does not match ${operation.requestBody.contentTypes.join(", ")}`
51217
+ );
51218
+ }
51219
+ }
51220
+ return warnings;
51017
51221
  }
51018
51222
  function requestBodyFieldNames(request, base) {
51019
51223
  const body = asRecord5(request.body);
@@ -51048,28 +51252,80 @@ function mediaTypeMatchesPattern(pattern, actual) {
51048
51252
  return false;
51049
51253
  });
51050
51254
  }
51255
+ function isJsonEncodingContentType(declared) {
51256
+ return declared.split(",").some((candidate) => {
51257
+ const entry = (candidate.split(";")[0] ?? "").trim();
51258
+ return entry === "application/json" || entry.endsWith("+json");
51259
+ });
51260
+ }
51261
+ function isPlaceholderValue(value) {
51262
+ return /^<[^>]*>$/.test(value.trim()) || value.includes("{{");
51263
+ }
51051
51264
  function collectStaticEncodingWarnings(operation, request, base, rule) {
51052
51265
  const encodings = rule.encodings;
51053
- if (!encodings || base !== "multipart/form-data") return [];
51266
+ if (!encodings) return [];
51267
+ const multipart = base === "multipart/form-data";
51054
51268
  const entries = requestBodyEntries(request, base);
51055
51269
  if (!entries) return [];
51056
51270
  const warnings = [];
51271
+ const part = multipart ? "multipart" : "urlencoded";
51057
51272
  for (const [field, encoding] of Object.entries(encodings)) {
51058
- const entry = entries.find((candidate) => String(candidate.key || "") === field);
51059
- if (!entry) continue;
51060
- if (encoding.binary && String(entry.type || "") !== "file") {
51061
- warnings.push(`CONTRACT_ENCODING_MISMATCH: ${operation.id} generated multipart field ${field} should be a file part per its binary schema`);
51062
- }
51063
- if (encoding.contentType) {
51064
- const actual = typeof entry.contentType === "string" ? (entry.contentType.toLowerCase().split(";")[0] ?? "").trim() : "";
51065
- if (!actual) {
51066
- warnings.push(
51067
- `CONTRACT_ENCODING_MISMATCH: ${operation.id} generated multipart field ${field} does not declare Content-Type ${encoding.contentType} from its encoding object`
51068
- );
51069
- } else if (!mediaTypeMatchesPattern(encoding.contentType, actual)) {
51070
- warnings.push(
51071
- `CONTRACT_ENCODING_MISMATCH: ${operation.id} generated multipart field ${field} Content-Type ${actual} does not match declared encoding ${encoding.contentType}`
51072
- );
51273
+ for (const entry of entries.filter((candidate) => String(candidate.key || "") === field)) {
51274
+ if (multipart && encoding.binary && String(entry.type || "") !== "file") {
51275
+ warnings.push(`CONTRACT_ENCODING_MISMATCH: ${operation.id} generated multipart field ${field} should be a file part per its binary schema`);
51276
+ }
51277
+ if (multipart && encoding.contentType) {
51278
+ const actual = typeof entry.contentType === "string" ? (entry.contentType.toLowerCase().split(";")[0] ?? "").trim() : "";
51279
+ if (!actual) {
51280
+ warnings.push(
51281
+ `CONTRACT_ENCODING_MISMATCH: ${operation.id} generated multipart field ${field} does not declare Content-Type ${encoding.contentType} from its encoding object`
51282
+ );
51283
+ } else if (!mediaTypeMatchesPattern(encoding.contentType, actual)) {
51284
+ warnings.push(
51285
+ `CONTRACT_ENCODING_MISMATCH: ${operation.id} generated multipart field ${field} Content-Type ${actual} does not match declared encoding ${encoding.contentType}`
51286
+ );
51287
+ }
51288
+ }
51289
+ if (encoding.contentType && isJsonEncodingContentType(encoding.contentType) && String(entry.type || "text") !== "file") {
51290
+ const value = typeof entry.value === "string" ? entry.value : "";
51291
+ if (value.trim() && !isPlaceholderValue(value)) {
51292
+ try {
51293
+ JSON.parse(value);
51294
+ } catch {
51295
+ warnings.push(
51296
+ `CONTRACT_ENCODING_MISMATCH: ${operation.id} generated ${part} field ${field} declares JSON encoding ${encoding.contentType} but its value is not parseable JSON`
51297
+ );
51298
+ }
51299
+ }
51300
+ }
51301
+ }
51302
+ }
51303
+ return warnings;
51304
+ }
51305
+ function coerceFormValue(value, schema2) {
51306
+ const record = asRecord5(schema2);
51307
+ const type2 = record?.type;
51308
+ const types2 = Array.isArray(type2) ? type2 : [type2];
51309
+ if ((types2.includes("integer") || types2.includes("number")) && /^-?[0-9]+([.][0-9]+)?([eE][+-]?[0-9]+)?$/.test(value.trim())) return Number(value);
51310
+ if (types2.includes("boolean") && (value === "true" || value === "false")) return value === "true";
51311
+ return value;
51312
+ }
51313
+ function collectStaticFieldSchemaWarnings(operation, request, base, rule) {
51314
+ const fieldSchemas = rule.fieldSchemas;
51315
+ if (!fieldSchemas) return [];
51316
+ const entries = requestBodyEntries(request, base);
51317
+ if (!entries) return [];
51318
+ const part = base === "multipart/form-data" ? "multipart" : "urlencoded";
51319
+ const warnings = [];
51320
+ for (const [field, schema2] of Object.entries(fieldSchemas)) {
51321
+ const validate2 = compileSchemaValidator(schema2);
51322
+ if (!validate2) continue;
51323
+ for (const entry of entries.filter((candidate) => String(candidate.key || "") === field)) {
51324
+ if (String(entry.type || "text") === "file") continue;
51325
+ const value = typeof entry.value === "string" ? entry.value : "";
51326
+ if (!value.trim() || isPlaceholderValue(value)) continue;
51327
+ if (!validate2(coerceFormValue(value, schema2))) {
51328
+ warnings.push(`CONTRACT_FORM_FIELD_SCHEMA_MISMATCH: ${operation.id} generated ${part} field ${field} value does not match its schema`);
51073
51329
  }
51074
51330
  }
51075
51331
  }
@@ -51081,7 +51337,10 @@ function collectStaticBodyWarnings(operation, request, contentType) {
51081
51337
  const base = (contentType || "").toLowerCase().split(";")[0]?.trim() ?? "";
51082
51338
  const rule = rules[base];
51083
51339
  if (!rule) return [];
51084
- const warnings = [...collectStaticEncodingWarnings(operation, request, base, rule)];
51340
+ const warnings = [
51341
+ ...collectStaticEncodingWarnings(operation, request, base, rule),
51342
+ ...collectStaticFieldSchemaWarnings(operation, request, base, rule)
51343
+ ];
51085
51344
  const names = requestBodyFieldNames(request, base);
51086
51345
  if (!names) return warnings;
51087
51346
  const present = new Set(names);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@postman-cse/onboarding-bootstrap",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Public customer preview Postman bootstrap GitHub Action.",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",