@postman-cse/onboarding-bootstrap 1.0.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
@@ -49627,8 +49627,6 @@ var COMPATIBLE_SCHEMA_URIS = /* @__PURE__ */ new Set([
49627
49627
  `${DRAFT_2020_12_SCHEMA_URI}#`
49628
49628
  ]);
49629
49629
  var ASSERTION_KEYS = /* @__PURE__ */ new Set([
49630
- "$defs",
49631
- "$id",
49632
49630
  "$ref",
49633
49631
  "$schema",
49634
49632
  "additionalItems",
@@ -49673,6 +49671,10 @@ var ASSERTION_KEYS = /* @__PURE__ */ new Set([
49673
49671
  ]);
49674
49672
  var STRIP_KEYS = /* @__PURE__ */ new Set([
49675
49673
  "title",
49674
+ "$comment",
49675
+ "$defs",
49676
+ "definitions",
49677
+ "$id",
49676
49678
  "description",
49677
49679
  "default",
49678
49680
  "example",
@@ -49683,8 +49685,32 @@ var STRIP_KEYS = /* @__PURE__ */ new Set([
49683
49685
  "xml",
49684
49686
  "externalDocs",
49685
49687
  "discriminator",
49686
- "format"
49688
+ "contentEncoding",
49689
+ "contentMediaType",
49690
+ "contentSchema"
49691
+ ]);
49692
+ var ASSERTED_FORMATS = /* @__PURE__ */ new Set([
49693
+ "date-time",
49694
+ "date",
49695
+ "time",
49696
+ "duration",
49697
+ "email",
49698
+ "hostname",
49699
+ "ipv4",
49700
+ "ipv6",
49701
+ "uri",
49702
+ "uri-reference",
49703
+ "uri-template",
49704
+ "uuid",
49705
+ "json-pointer",
49706
+ "relative-json-pointer",
49707
+ "regex"
49687
49708
  ]);
49709
+ var INT32_MIN = -2147483648;
49710
+ var INT32_MAX = 2147483647;
49711
+ var MAX_REFERENCED_SCHEMAS = 400;
49712
+ var DRAFT_2020_12_ONLY_KEYS = /* @__PURE__ */ new Set(["prefixItems", "dependentRequired", "dependentSchemas", "minContains", "maxContains", "unevaluatedItems", "unevaluatedProperties"]);
49713
+ var DRAFT_07_ONLY_KEYS = /* @__PURE__ */ new Set(["dependencies", "additionalItems"]);
49688
49714
  function asRecord3(value) {
49689
49715
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
49690
49716
  return value;
@@ -49702,8 +49728,8 @@ function resolvePointer(root, ref) {
49702
49728
  function unsupported(message) {
49703
49729
  return { unsupported: message };
49704
49730
  }
49705
- function mergeRequiredWithoutWriteOnly(required, writeOnlyProperties) {
49706
- const values = asArray(required).map((entry) => String(entry)).filter((entry) => !writeOnlyProperties.has(entry));
49731
+ function mergeRequiredWithoutStripped(required, strippedProperties) {
49732
+ const values = asArray(required).map((entry) => String(entry)).filter((entry) => !strippedProperties.has(entry));
49707
49733
  return values.length > 0 ? values : void 0;
49708
49734
  }
49709
49735
  function hasUnsupported(child2) {
@@ -49719,11 +49745,31 @@ function rootDialect(root, version, schemaRecord) {
49719
49745
  if (declared.includes("draft-07")) return DRAFT_07_SCHEMA_URI;
49720
49746
  return DRAFT_2020_12_SCHEMA_URI;
49721
49747
  }
49722
- function normalizeSchema(root, schema2, options) {
49723
- const depth = options.depth ?? 0;
49724
- if (depth > 50) return unsupported("OpenAPI schema reference depth exceeded 50");
49748
+ function registerDef(ctx, ref) {
49749
+ const existing = ctx.defs.get(ref);
49750
+ if (existing) return existing;
49751
+ if (ctx.defs.size >= MAX_REFERENCED_SCHEMAS) {
49752
+ const entry2 = { name: "overflow", unsupported: `OpenAPI schema reference graph exceeded ${MAX_REFERENCED_SCHEMAS} referenced schemas` };
49753
+ return entry2;
49754
+ }
49755
+ const entry = { name: `d${ctx.defs.size}` };
49756
+ ctx.defs.set(ref, entry);
49757
+ const resolved = resolvePointer(ctx.root, ref);
49758
+ if (resolved === void 0) {
49759
+ entry.unsupported = `Unresolved OpenAPI $ref: ${ref}`;
49760
+ return entry;
49761
+ }
49762
+ const normalized = normalizeSchema(ctx, resolved, { depth: 0, rootSchema: false });
49763
+ const bad = hasUnsupported(normalized);
49764
+ if (bad) entry.unsupported = bad;
49765
+ else entry.schema = normalized;
49766
+ return entry;
49767
+ }
49768
+ function normalizeSchema(ctx, schema2, options) {
49769
+ const depth = options.depth;
49770
+ if (depth > 50) return unsupported("OpenAPI schema nesting depth exceeded 50");
49725
49771
  if (Array.isArray(schema2)) {
49726
- const normalized2 = schema2.map((entry) => normalizeSchema(root, entry, { ...options, depth: depth + 1, rootSchema: false }));
49772
+ const normalized2 = schema2.map((entry) => normalizeSchema(ctx, entry, { depth: depth + 1, rootSchema: false }));
49727
49773
  const bad = normalized2.map(hasUnsupported).find(Boolean);
49728
49774
  return bad ? unsupported(bad) : normalized2;
49729
49775
  }
@@ -49732,40 +49778,74 @@ function normalizeSchema(root, schema2, options) {
49732
49778
  const ref = typeof record.$ref === "string" ? record.$ref : "";
49733
49779
  if (ref) {
49734
49780
  if (!ref.startsWith("#/")) return unsupported(`External OpenAPI $ref remained unresolved: ${ref}`);
49735
- const stack = options.stack ?? [];
49736
- if (stack.includes(ref)) return unsupported(`Recursive OpenAPI $ref is unsupported: ${ref}`);
49737
- const resolved = resolvePointer(root, ref);
49738
- if (resolved === void 0) return unsupported(`Unresolved OpenAPI $ref: ${ref}`);
49739
- const siblingEntries = Object.entries(record).filter(([key]) => key !== "$ref");
49740
- const resolvedSchema = normalizeSchema(root, resolved, {
49741
- ...options,
49742
- depth: depth + 1,
49743
- rootSchema: false,
49744
- stack: [...stack, ref]
49745
- });
49746
- const resolvedUnsupported = hasUnsupported(resolvedSchema);
49747
- if (resolvedUnsupported) return unsupported(resolvedUnsupported);
49748
- if (options.version === "3.0" || siblingEntries.length === 0) return resolvedSchema;
49749
- const siblingSchema = normalizeSchema(root, Object.fromEntries(siblingEntries), {
49750
- ...options,
49751
- depth: depth + 1,
49752
- rootSchema: false,
49753
- stack: [...stack, ref]
49754
- });
49781
+ const siblingEntries = ctx.version === "3.1" ? Object.entries(record).filter(([key]) => key !== "$ref") : [];
49782
+ const inlineStack = options.inlineStack ?? [];
49783
+ if (options.rootSchema && siblingEntries.length === 0 && !inlineStack.includes(ref)) {
49784
+ const resolved = resolvePointer(ctx.root, ref);
49785
+ if (resolved === void 0) return unsupported(`Unresolved OpenAPI $ref: ${ref}`);
49786
+ const inlined = normalizeSchema(ctx, resolved, { depth, rootSchema: true, inlineStack: [...inlineStack, ref] });
49787
+ const bad = hasUnsupported(inlined);
49788
+ return bad ? unsupported(bad) : inlined;
49789
+ }
49790
+ const entry = registerDef(ctx, ref);
49791
+ if (entry.unsupported) return unsupported(entry.unsupported);
49792
+ const refNode = { $ref: `#/$defs/${entry.name}` };
49793
+ if (siblingEntries.length === 0) {
49794
+ if (options.rootSchema) refNode.$schema = ctx.dialect;
49795
+ return refNode;
49796
+ }
49797
+ const siblingSchema = normalizeSchema(ctx, Object.fromEntries(siblingEntries), { depth: depth + 1, rootSchema: false });
49755
49798
  const siblingUnsupported = hasUnsupported(siblingSchema);
49756
49799
  if (siblingUnsupported) return unsupported(siblingUnsupported);
49757
- const wrapper = { allOf: [resolvedSchema, siblingSchema] };
49758
- if (options.rootSchema) wrapper.$schema = options.dialect ?? rootDialect(root, options.version, record);
49800
+ const wrapper = { allOf: [refNode, siblingSchema] };
49801
+ if (options.rootSchema) wrapper.$schema = ctx.dialect;
49759
49802
  return wrapper;
49760
49803
  }
49761
49804
  const sourceSchema = { ...record };
49762
- const nullable = sourceSchema.nullable === true && options.version === "3.0";
49805
+ const nullable = sourceSchema.nullable === true && ctx.version === "3.0";
49763
49806
  delete sourceSchema.nullable;
49764
- const writeOnlyProperties = /* @__PURE__ */ new Set();
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
+ }
49839
+ const directionStrippedFlag = ctx.direction === "request" ? "readOnly" : "writeOnly";
49840
+ const strippedProperties = /* @__PURE__ */ new Set();
49765
49841
  const rawProperties = asRecord3(sourceSchema.properties);
49766
49842
  if (rawProperties) {
49767
49843
  for (const [propertyName, propertySchema] of Object.entries(rawProperties)) {
49768
- if (asRecord3(propertySchema)?.writeOnly === true) writeOnlyProperties.add(propertyName);
49844
+ let flagSource = asRecord3(propertySchema);
49845
+ if (typeof flagSource?.$ref === "string" && flagSource.$ref.startsWith("#/")) {
49846
+ flagSource = asRecord3(resolvePointer(ctx.root, flagSource.$ref)) ?? flagSource;
49847
+ }
49848
+ if (flagSource?.[directionStrippedFlag] === true) strippedProperties.add(propertyName);
49769
49849
  }
49770
49850
  }
49771
49851
  const normalized = {};
@@ -49792,17 +49872,69 @@ function normalizeSchema(root, schema2, options) {
49792
49872
  delete normalized.maximum;
49793
49873
  continue;
49794
49874
  }
49875
+ if (key === "format") {
49876
+ if (typeof value === "string" && ASSERTED_FORMATS.has(value)) normalized.format = value;
49877
+ continue;
49878
+ }
49795
49879
  if (!ASSERTION_KEYS.has(key)) return unsupported(`Unsupported OpenAPI schema keyword: ${key}`);
49796
- if (key === "items" && Array.isArray(value) && options.version === "3.0") {
49880
+ if (ctx.dialect === DRAFT_07_SCHEMA_URI && DRAFT_2020_12_ONLY_KEYS.has(key)) {
49881
+ return unsupported(`${key} requires the JSON Schema 2020-12 dialect`);
49882
+ }
49883
+ if (ctx.dialect === DRAFT_2020_12_SCHEMA_URI && DRAFT_07_ONLY_KEYS.has(key)) {
49884
+ return unsupported(`${key} is a draft-07 keyword and is unsupported under JSON Schema 2020-12`);
49885
+ }
49886
+ if (key === "items" && Array.isArray(value) && ctx.version === "3.0") {
49797
49887
  return unsupported("Tuple array items are unsupported in OpenAPI 3.0");
49798
49888
  }
49889
+ if (key === "patternProperties" || key === "dependentSchemas") {
49890
+ const map2 = asRecord3(value);
49891
+ if (!map2) continue;
49892
+ const next = {};
49893
+ for (const [mapKey, mapSchema] of Object.entries(map2)) {
49894
+ const child3 = normalizeSchema(ctx, mapSchema, { depth: depth + 1, rootSchema: false });
49895
+ const bad2 = hasUnsupported(child3);
49896
+ if (bad2) return unsupported(bad2);
49897
+ next[mapKey] = child3;
49898
+ }
49899
+ normalized[key] = next;
49900
+ continue;
49901
+ }
49902
+ if (key === "dependentRequired") {
49903
+ const map2 = asRecord3(value);
49904
+ if (!map2) continue;
49905
+ for (const names of Object.values(map2)) {
49906
+ if (!Array.isArray(names) || names.some((name) => typeof name !== "string")) {
49907
+ return unsupported("dependentRequired requires string array values");
49908
+ }
49909
+ }
49910
+ normalized.dependentRequired = value;
49911
+ continue;
49912
+ }
49913
+ if (key === "dependencies") {
49914
+ const map2 = asRecord3(value);
49915
+ if (!map2) continue;
49916
+ const next = {};
49917
+ for (const [mapKey, dependency] of Object.entries(map2)) {
49918
+ if (Array.isArray(dependency)) {
49919
+ if (dependency.some((name) => typeof name !== "string")) return unsupported("dependencies requires string array or schema values");
49920
+ next[mapKey] = dependency;
49921
+ continue;
49922
+ }
49923
+ const child3 = normalizeSchema(ctx, dependency, { depth: depth + 1, rootSchema: false });
49924
+ const bad2 = hasUnsupported(child3);
49925
+ if (bad2) return unsupported(bad2);
49926
+ next[mapKey] = child3;
49927
+ }
49928
+ normalized.dependencies = next;
49929
+ continue;
49930
+ }
49799
49931
  if (key === "properties") {
49800
49932
  const properties = asRecord3(value);
49801
49933
  if (!properties) continue;
49802
49934
  const nextProperties = {};
49803
49935
  for (const [propertyName, propertySchema] of Object.entries(properties)) {
49804
- if (writeOnlyProperties.has(propertyName)) continue;
49805
- const child3 = normalizeSchema(root, propertySchema, { ...options, depth: depth + 1, rootSchema: false });
49936
+ if (strippedProperties.has(propertyName)) continue;
49937
+ const child3 = normalizeSchema(ctx, propertySchema, { depth: depth + 1, rootSchema: false });
49806
49938
  const bad2 = hasUnsupported(child3);
49807
49939
  if (bad2) return unsupported(bad2);
49808
49940
  nextProperties[propertyName] = child3;
@@ -49811,16 +49943,46 @@ function normalizeSchema(root, schema2, options) {
49811
49943
  continue;
49812
49944
  }
49813
49945
  if (key === "required") {
49814
- const required = mergeRequiredWithoutWriteOnly(value, writeOnlyProperties);
49946
+ const required = mergeRequiredWithoutStripped(value, strippedProperties);
49815
49947
  if (required) normalized.required = required;
49816
49948
  continue;
49817
49949
  }
49818
- const child2 = normalizeSchema(root, value, { ...options, depth: depth + 1, rootSchema: false });
49950
+ const child2 = normalizeSchema(ctx, value, { depth: depth + 1, rootSchema: false });
49819
49951
  const bad = hasUnsupported(child2);
49820
49952
  if (bad) return unsupported(bad);
49821
49953
  normalized[key] = child2;
49822
49954
  }
49823
- if (options.rootSchema) normalized.$schema = options.dialect ?? rootDialect(root, options.version, record);
49955
+ if (typeof normalized.minimum === "number" && typeof normalized.exclusiveMinimum === "number") {
49956
+ if (normalized.exclusiveMinimum >= normalized.minimum) delete normalized.minimum;
49957
+ else delete normalized.exclusiveMinimum;
49958
+ }
49959
+ if (typeof normalized.maximum === "number" && typeof normalized.exclusiveMaximum === "number") {
49960
+ if (normalized.exclusiveMaximum <= normalized.maximum) delete normalized.maximum;
49961
+ else delete normalized.exclusiveMaximum;
49962
+ }
49963
+ if (sourceSchema.format === "int32") {
49964
+ const type2 = normalized.type;
49965
+ const isInteger2 = type2 === "integer" || Array.isArray(type2) && type2.includes("integer");
49966
+ if (isInteger2) {
49967
+ if (typeof normalized.exclusiveMinimum === "number") {
49968
+ if (normalized.exclusiveMinimum < INT32_MIN) {
49969
+ delete normalized.exclusiveMinimum;
49970
+ normalized.minimum = INT32_MIN;
49971
+ }
49972
+ } else if (typeof normalized.minimum !== "number" || normalized.minimum < INT32_MIN) {
49973
+ normalized.minimum = INT32_MIN;
49974
+ }
49975
+ if (typeof normalized.exclusiveMaximum === "number") {
49976
+ if (normalized.exclusiveMaximum > INT32_MAX) {
49977
+ delete normalized.exclusiveMaximum;
49978
+ normalized.maximum = INT32_MAX;
49979
+ }
49980
+ } else if (typeof normalized.maximum !== "number" || normalized.maximum > INT32_MAX) {
49981
+ normalized.maximum = INT32_MAX;
49982
+ }
49983
+ }
49984
+ }
49985
+ if (options.rootSchema) normalized.$schema = ctx.dialect;
49824
49986
  if (nullable) {
49825
49987
  if (typeof normalized.type === "string") {
49826
49988
  normalized.type = [normalized.type, "null"];
@@ -49832,23 +49994,108 @@ function normalizeSchema(root, schema2, options) {
49832
49994
  const wrappedSchema = { ...normalized };
49833
49995
  delete wrappedSchema.$schema;
49834
49996
  const wrapper = { anyOf: [{ type: "null" }, wrappedSchema] };
49835
- if (options.rootSchema) wrapper.$schema = options.dialect ?? rootDialect(root, options.version, record);
49997
+ if (options.rootSchema) wrapper.$schema = ctx.dialect;
49836
49998
  return wrapper;
49837
49999
  }
49838
50000
  }
49839
50001
  return normalized;
49840
50002
  }
49841
- function packSchema(root, schema2, version) {
50003
+ function isSchemaGraphOverflow(packed) {
50004
+ return typeof packed.unsupported === "string" && packed.unsupported.startsWith("OpenAPI schema reference graph exceeded");
50005
+ }
50006
+ function packSchema(root, schema2, version, direction = "response") {
49842
50007
  try {
50008
+ if (schema2 === true) return { schema: { $schema: rootDialect(root, version) } };
50009
+ if (schema2 === false) return unsupported("Boolean false JSON Schema rejects every instance and is unsupported");
49843
50010
  const dialect = rootDialect(root, version, asRecord3(schema2) ?? void 0);
49844
- const normalized = normalizeSchema(root, schema2, { version, rootSchema: true, dialect });
50011
+ const ctx = { root, version, direction, dialect, defs: /* @__PURE__ */ new Map(), notes: /* @__PURE__ */ new Set() };
50012
+ const normalized = normalizeSchema(ctx, schema2, { depth: 0, rootSchema: true });
49845
50013
  const message = hasUnsupported(normalized);
49846
- return message ? unsupported(message) : { schema: normalized };
50014
+ if (message) return unsupported(message);
50015
+ const aliasTargets = /* @__PURE__ */ new Map();
50016
+ for (const entry of ctx.defs.values()) {
50017
+ if (entry.unsupported) return unsupported(entry.unsupported);
50018
+ const entrySchema = asRecord3(entry.schema);
50019
+ const ref = entrySchema && Object.keys(entrySchema).length === 1 && typeof entrySchema.$ref === "string" ? entrySchema.$ref : "";
50020
+ if (ref.startsWith("#/$defs/")) aliasTargets.set(entry.name, ref.slice("#/$defs/".length));
50021
+ }
50022
+ for (const start of aliasTargets.keys()) {
50023
+ const seen = /* @__PURE__ */ new Set();
50024
+ let current = start;
50025
+ while (current !== void 0 && aliasTargets.has(current)) {
50026
+ if (seen.has(current)) return unsupported("Self-referential alias schema is unsupported");
50027
+ seen.add(current);
50028
+ current = aliasTargets.get(current);
50029
+ }
50030
+ }
50031
+ if (ctx.defs.size > 0) {
50032
+ const normalizedRecord = asRecord3(normalized);
50033
+ if (!normalizedRecord) return unsupported("Referenced schemas require an object root schema");
50034
+ normalizedRecord.$defs = Object.fromEntries([...ctx.defs.values()].map((entry) => [entry.name, entry.schema]));
50035
+ }
50036
+ const packed = { schema: normalized };
50037
+ if (ctx.notes.size > 0) packed.notes = [...ctx.notes].sort();
50038
+ return packed;
49847
50039
  } catch (error2) {
49848
50040
  return unsupported(error2 instanceof Error ? error2.message : String(error2));
49849
50041
  }
49850
50042
  }
49851
50043
 
50044
+ // src/lib/spec/schema-validator-code.ts
50045
+ var import_schemasafe = __toESM(require_src(), 1);
50046
+ function compileSchemaValidator(schema2) {
50047
+ try {
50048
+ const validate2 = (0, import_schemasafe.validator)(schema2, {
50049
+ includeErrors: false,
50050
+ // Real-world specs legally mix enum/const with other keywords; without
50051
+ // this flag schemasafe refuses to compile them (observed in Stripe,
50052
+ // Plaid, and DigitalOcean public specs).
50053
+ allowUnusedKeywords: true,
50054
+ contentValidation: false,
50055
+ formatAssertion: true,
50056
+ isJSON: true,
50057
+ mode: "default",
50058
+ removeAdditional: false,
50059
+ requireSchema: true,
50060
+ requireStringValidation: false,
50061
+ useDefaults: false
50062
+ });
50063
+ return (value) => validate2(value);
50064
+ } catch {
50065
+ return null;
50066
+ }
50067
+ }
50068
+ function compileSchemaValidatorCode(schema2) {
50069
+ try {
50070
+ const validate2 = (0, import_schemasafe.validator)(schema2, {
50071
+ includeErrors: true,
50072
+ allErrors: true,
50073
+ // Real-world specs legally mix enum/const with other keywords; without
50074
+ // this flag schemasafe refuses to compile them (observed in Stripe,
50075
+ // Plaid, and DigitalOcean public specs).
50076
+ allowUnusedKeywords: true,
50077
+ contentValidation: false,
50078
+ formatAssertion: true,
50079
+ isJSON: true,
50080
+ mode: "default",
50081
+ removeAdditional: false,
50082
+ requireSchema: true,
50083
+ requireStringValidation: false,
50084
+ useDefaults: false
50085
+ });
50086
+ const source = validate2.toModule();
50087
+ if (/\beval\s*\(/.test(source) || /new\s+Function\b/.test(source)) {
50088
+ throw new Error("schemasafe generated forbidden dynamic code");
50089
+ }
50090
+ return source;
50091
+ } catch (error2) {
50092
+ throw new Error(
50093
+ `CONTRACT_SCHEMA_COMPILE_FAILED: ${error2 instanceof Error ? error2.message : String(error2)}`,
50094
+ { cause: error2 }
50095
+ );
50096
+ }
50097
+ }
50098
+
49852
50099
  // src/lib/spec/contract-index.ts
49853
50100
  var HTTP_METHODS = /* @__PURE__ */ new Set(["get", "put", "post", "delete", "options", "head", "patch", "trace"]);
49854
50101
  function asRecord4(value) {
@@ -49924,7 +50171,7 @@ function collectSecurityApiKeys(root, operation) {
49924
50171
  for (const requirement of requirements.map((entry) => asRecord4(entry)).filter(Boolean)) {
49925
50172
  for (const schemeName of Object.keys(requirement)) {
49926
50173
  const scheme = resolveInternalRef(root, securitySchemes?.[schemeName]);
49927
- if (scheme?.type === "apiKey" && typeof scheme.name === "string" && ["query", "header"].includes(String(scheme.in))) {
50174
+ if (scheme?.type === "apiKey" && typeof scheme.name === "string" && ["query", "header", "cookie"].includes(String(scheme.in))) {
49928
50175
  names.add(`${String(scheme.in)}:${scheme.name.toLowerCase()}`);
49929
50176
  }
49930
50177
  }
@@ -49946,12 +50193,197 @@ function collectSecuritySchemeWarnings(root, operation) {
49946
50193
  for (const schemeName of Object.keys(requirement)) {
49947
50194
  const scheme = resolveInternalRef(root, securitySchemes?.[schemeName]);
49948
50195
  warnings.add(
49949
- `CONTRACT_SECURITY_NOT_VALIDATED: security scheme ${schemeName} (${securitySchemeKind(scheme)}) is not runtime-proven by dynamic contract tests`
50196
+ `CONTRACT_SECURITY_NOT_VALIDATED: security scheme ${schemeName} (${securitySchemeKind(scheme)}) is not runtime-proven beyond credential presence by dynamic contract tests`
49950
50197
  );
49951
50198
  }
49952
50199
  }
49953
50200
  return [...warnings];
49954
50201
  }
50202
+ function securityCheckFor(schemeName, scheme) {
50203
+ const kind = securitySchemeKind(scheme);
50204
+ if (scheme?.type === "apiKey" && typeof scheme.name === "string" && ["header", "query", "cookie"].includes(String(scheme.in))) {
50205
+ return { scheme: schemeName, kind, checkable: true, in: String(scheme.in), name: scheme.name };
50206
+ }
50207
+ if (scheme?.type === "http") {
50208
+ const httpScheme = String(scheme.scheme || "").toLowerCase();
50209
+ if (httpScheme === "basic") return { scheme: schemeName, kind, checkable: true, prefix: "Basic " };
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)} ` };
50212
+ return { scheme: schemeName, kind, checkable: true, in: "header", name: "Authorization" };
50213
+ }
50214
+ if (scheme?.type === "oauth2" || scheme?.type === "openIdConnect") {
50215
+ return { scheme: schemeName, kind, checkable: true, in: "header", name: "Authorization" };
50216
+ }
50217
+ return { scheme: schemeName, kind, checkable: false };
50218
+ }
50219
+ function collectSecurityRuntimeChecks(root, operation) {
50220
+ const securitySchemes = asRecord4(asRecord4(root.components)?.securitySchemes);
50221
+ const requirements = operation.security === void 0 ? asArray2(root.security) : asArray2(operation.security);
50222
+ const alternatives = [];
50223
+ for (const requirement of requirements.map((entry) => asRecord4(entry)).filter(Boolean)) {
50224
+ const schemeNames = Object.keys(requirement);
50225
+ if (schemeNames.length === 0) return void 0;
50226
+ alternatives.push(schemeNames.map((schemeName) => securityCheckFor(schemeName, resolveInternalRef(root, securitySchemes?.[schemeName]))));
50227
+ }
50228
+ if (alternatives.some((alternative) => alternative.length > 0 && alternative.every((check) => !check.checkable))) return void 0;
50229
+ return alternatives.length > 0 ? alternatives : void 0;
50230
+ }
50231
+ function resolvedParameters(root, pathItem, operation) {
50232
+ return [...asArray2(pathItem.parameters), ...asArray2(operation.parameters)].map((rawParam) => {
50233
+ try {
50234
+ return resolveInternalRef(root, rawParam);
50235
+ } catch {
50236
+ return null;
50237
+ }
50238
+ }).filter((param) => Boolean(param));
50239
+ }
50240
+ var DEFAULT_PARAM_STYLES = { query: "form", path: "simple", header: "simple", cookie: "form" };
50241
+ var IGNORED_HEADER_PARAMS = /* @__PURE__ */ new Set(["accept", "content-type", "authorization"]);
50242
+ function isIgnoredParameter(location2, name) {
50243
+ return location2 === "header" && IGNORED_HEADER_PARAMS.has(name.toLowerCase());
50244
+ }
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) {
50257
+ const warnings = [];
50258
+ for (const param of resolvedParameters(root, pathItem, operation)) {
50259
+ const location2 = String(param.in || "").toLowerCase();
50260
+ const name = String(param.name || "");
50261
+ const defaultStyle = DEFAULT_PARAM_STYLES[location2];
50262
+ if (!name || !defaultStyle || isIgnoredParameter(location2, name)) continue;
50263
+ const style = typeof param.style === "string" ? param.style : defaultStyle;
50264
+ const defaultExplode = style === "form";
50265
+ const explode = typeof param.explode === "boolean" ? param.explode : defaultExplode;
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;
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`);
50270
+ }
50271
+ }
50272
+ return warnings;
50273
+ }
50274
+ var SCALAR_SCHEMA_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean", "null"]);
50275
+ function packedScalarSchema(packed) {
50276
+ if (packed.unsupported || packed.schema === void 0) return void 0;
50277
+ const record = asRecord4(packed.schema);
50278
+ if (!record) return void 0;
50279
+ const types2 = Array.isArray(record.type) ? record.type : [record.type];
50280
+ if (!types2.every((entry) => typeof entry === "string" && SCALAR_SCHEMA_TYPES.has(entry))) return void 0;
50281
+ return packed.schema;
50282
+ }
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) {
50304
+ const securityKeys = collectSecurityApiKeys(root, operation);
50305
+ const checks = [];
50306
+ const seen = /* @__PURE__ */ new Set();
50307
+ const orderedParams = [...asArray2(operation.parameters), ...asArray2(pathItem.parameters)].map((rawParam) => {
50308
+ try {
50309
+ return resolveInternalRef(root, rawParam);
50310
+ } catch {
50311
+ return null;
50312
+ }
50313
+ }).filter((param) => Boolean(param));
50314
+ for (const param of orderedParams) {
50315
+ const location2 = String(param.in || "").toLowerCase();
50316
+ if (location2 !== "query" && location2 !== "header" && location2 !== "path" && location2 !== "cookie") continue;
50317
+ const name = String(param.name || "");
50318
+ if (!name || isIgnoredParameter(location2, name)) continue;
50319
+ const key = `${location2}:${name.toLowerCase()}`;
50320
+ if (seen.has(key)) continue;
50321
+ seen.add(key);
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
+ }
50336
+ if (param.content !== void 0 || param.schema === void 0) continue;
50337
+ const defaultStyle = DEFAULT_PARAM_STYLES[location2];
50338
+ const style = typeof param.style === "string" ? param.style : defaultStyle;
50339
+ const defaultExplode = style === "form";
50340
+ const explode = typeof param.explode === "boolean" ? param.explode : defaultExplode;
50341
+ const defaultSerialization = style === defaultStyle && explode === defaultExplode;
50342
+ const packed = packSchema(root, param.schema, version);
50343
+ const noteWarnings = packNoteWarnings(packed, `parameter ${location2}:${name} of ${operationId}`);
50344
+ if (defaultSerialization) warnings.push(...noteWarnings);
50345
+ if (packed.unsupported) {
50346
+ if (defaultSerialization) warnings.push(`CONTRACT_SCHEMA_NOT_COMPILED: parameter ${location2}:${name} schema on ${operationId} skipped (${packed.unsupported})`);
50347
+ continue;
50348
+ }
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 };
50365
+ if (location2 === "query" && param.allowEmptyValue === true) check.allowEmptyValue = true;
50366
+ checks.push(check);
50367
+ }
50368
+ return checks.length > 0 ? checks : void 0;
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
+ }
50375
+ function collectDeclaredQueryParameters(root, pathItem, operation) {
50376
+ const names = /* @__PURE__ */ new Set();
50377
+ for (const param of resolvedParameters(root, pathItem, operation)) {
50378
+ if (String(param.in || "").toLowerCase() !== "query") continue;
50379
+ const name = String(param.name || "");
50380
+ if (name) names.add(name.toLowerCase());
50381
+ }
50382
+ for (const key of collectSecurityApiKeys(root, operation)) {
50383
+ if (key.startsWith("query:")) names.add(key.slice("query:".length));
50384
+ }
50385
+ return [...names];
50386
+ }
49955
50387
  function collectParameters(root, pathItem, operation) {
49956
50388
  const securityKeys = collectSecurityApiKeys(root, operation);
49957
50389
  const requirements = [];
@@ -49961,9 +50393,9 @@ function collectParameters(root, pathItem, operation) {
49961
50393
  const param = resolveInternalRef(root, rawParam);
49962
50394
  if (!param) continue;
49963
50395
  const location2 = String(param.in || "").toLowerCase();
49964
- if (!["path", "query", "header"].includes(location2)) continue;
50396
+ if (!["path", "query", "header", "cookie"].includes(location2)) continue;
49965
50397
  const name = String(param.name || "");
49966
- if (!name || param.required !== true) continue;
50398
+ if (!name || param.required !== true || isIgnoredParameter(location2, name)) continue;
49967
50399
  const key = `${location2}:${name.toLowerCase()}`;
49968
50400
  if (seen.has(key)) continue;
49969
50401
  seen.add(key);
@@ -49975,36 +50407,263 @@ function collectParameters(root, pathItem, operation) {
49975
50407
  }
49976
50408
  return requirements;
49977
50409
  }
49978
- function collectRequestBody(root, operation) {
50410
+ var BODY_FIELD_RULE_TYPES = /* @__PURE__ */ new Set(["application/x-www-form-urlencoded", "multipart/form-data"]);
50411
+ function isJsonBaseType(base) {
50412
+ return base === "application/json" || /\+json$/.test(base);
50413
+ }
50414
+ function mergeObjectSchema(root, rawSchema, depth) {
50415
+ if (depth > 10) return null;
50416
+ let schema2;
50417
+ try {
50418
+ schema2 = resolveInternalRef(root, rawSchema);
50419
+ } catch {
50420
+ return null;
50421
+ }
50422
+ if (!schema2) return null;
50423
+ const merged = {
50424
+ required: asArray2(schema2.required).map((entry) => String(entry)).filter(Boolean),
50425
+ properties: { ...asRecord4(schema2.properties) ?? {} }
50426
+ };
50427
+ for (const member of asArray2(schema2.allOf)) {
50428
+ const child2 = mergeObjectSchema(root, member, depth + 1);
50429
+ if (!child2) continue;
50430
+ merged.required.push(...child2.required);
50431
+ merged.properties = { ...merged.properties, ...child2.properties };
50432
+ }
50433
+ merged.required = [...new Set(merged.required)];
50434
+ return merged;
50435
+ }
50436
+ function propertyIsReadOnly(root, properties, name) {
50437
+ try {
50438
+ return resolveInternalRef(root, properties[name])?.readOnly === true;
50439
+ } catch {
50440
+ return false;
50441
+ }
50442
+ }
50443
+ function propertyIsBinary(root, properties, name) {
50444
+ let schema2;
50445
+ try {
50446
+ schema2 = resolveInternalRef(root, properties[name]);
50447
+ } catch {
50448
+ return false;
50449
+ }
50450
+ if (!schema2) return false;
50451
+ if (schema2.format === "binary") return true;
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/");
50456
+ }
50457
+ function fieldEncodings(root, base, mediaObject, properties) {
50458
+ const declared = asRecord4(mediaObject?.encoding);
50459
+ const encodings = {};
50460
+ for (const name of Object.keys(properties)) {
50461
+ if (base === "multipart/form-data" && propertyIsBinary(root, properties, name)) {
50462
+ encodings[name] = { ...encodings[name], binary: true };
50463
+ }
50464
+ }
50465
+ if (declared) {
50466
+ for (const [name, rawEncoding] of Object.entries(declared)) {
50467
+ const encoding = asRecord4(rawEncoding);
50468
+ if (!encoding) continue;
50469
+ const entry = { ...encodings[name] };
50470
+ if (typeof encoding.contentType === "string" && encoding.contentType.trim()) {
50471
+ entry.contentType = encoding.contentType.toLowerCase();
50472
+ }
50473
+ if (base === "multipart/form-data" && asRecord4(encoding.headers)) {
50474
+ entry.hasHeaders = true;
50475
+ }
50476
+ if (base === "application/x-www-form-urlencoded") {
50477
+ const style = typeof encoding.style === "string" ? encoding.style : "form";
50478
+ const explode = typeof encoding.explode === "boolean" ? encoding.explode : style === "form";
50479
+ if (style !== "form" || explode !== (style === "form") || encoding.allowReserved === true) {
50480
+ entry.nonDefaultSerialization = true;
50481
+ }
50482
+ }
50483
+ if (Object.keys(entry).length > 0) encodings[name] = entry;
50484
+ }
50485
+ }
50486
+ return Object.keys(encodings).length > 0 ? encodings : void 0;
50487
+ }
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) {
50504
+ const rules = {};
50505
+ for (const [contentType, mediaObject] of Object.entries(content)) {
50506
+ const base = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
50507
+ if (!isJsonBaseType(base) && !BODY_FIELD_RULE_TYPES.has(base)) continue;
50508
+ const mediaRecord = asRecord4(mediaObject);
50509
+ const merged = mergeObjectSchema(root, mediaRecord?.schema, 0);
50510
+ if (!merged) continue;
50511
+ const readOnly = Object.keys(merged.properties).filter((name) => propertyIsReadOnly(root, merged.properties, name));
50512
+ const required = merged.required.filter((name) => !propertyIsReadOnly(root, merged.properties, name));
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) {
50517
+ const rule = { required, readOnly };
50518
+ if (encodings) rule.encodings = encodings;
50519
+ if (fieldSchemas) rule.fieldSchemas = fieldSchemas;
50520
+ rules[base] = rule;
50521
+ }
50522
+ }
50523
+ return Object.keys(rules).length > 0 ? rules : void 0;
50524
+ }
50525
+ function requestBodyJsonSchemas(root, content, version, operationId, warnings) {
50526
+ const schemas = {};
50527
+ const exampleWarnings = /* @__PURE__ */ new Set();
50528
+ for (const [contentType, mediaObject] of Object.entries(content)) {
50529
+ const base = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
50530
+ const mediaRecord = asRecord4(mediaObject);
50531
+ const schema2 = mediaRecord?.schema;
50532
+ if (!isJsonBaseType(base)) {
50533
+ if (schema2 !== void 0 && !BODY_FIELD_RULE_TYPES.has(base)) {
50534
+ warnings.push(`CONTRACT_NONJSON_SCHEMA_NOT_VALIDATED: request body schema for ${contentType} on ${operationId} is not validated at runtime`);
50535
+ }
50536
+ continue;
50537
+ }
50538
+ if (schema2 === void 0) continue;
50539
+ const packed = packSchema(root, schema2, version, "request");
50540
+ warnings.push(...packNoteWarnings(packed, `request body ${contentType} of ${operationId}`));
50541
+ if (mediaRecord) validateExamples(root, mediaRecord, packed, contentType, operationId, exampleWarnings);
50542
+ if (packed.unsupported) {
50543
+ warnings.push(`CONTRACT_REQUEST_SCHEMA_NOT_VALIDATED: request body schema for ${contentType} on ${operationId} is not validated (${packed.unsupported})`);
50544
+ continue;
50545
+ }
50546
+ if (packed.schema !== void 0) schemas[base] = packed.schema;
50547
+ }
50548
+ warnings.push(...exampleWarnings);
50549
+ return Object.keys(schemas).length > 0 ? schemas : void 0;
50550
+ }
50551
+ function collectRequestBody(root, operation, version, operationId, warnings) {
49979
50552
  const body = resolveInternalRef(root, operation.requestBody);
49980
- if (!body || body.required !== true) return void 0;
50553
+ if (!body) return void 0;
49981
50554
  const content = asRecord4(body.content);
50555
+ const fieldRules = content ? requestBodyFieldRules(root, content, version, operationId, warnings) : void 0;
50556
+ if (fieldRules) {
50557
+ for (const [base, rule] of Object.entries(fieldRules)) {
50558
+ for (const [field, encoding] of Object.entries(rule.encodings ?? {})) {
50559
+ if (encoding.nonDefaultSerialization) {
50560
+ warnings.push(
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`
50562
+ );
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
+ }
50569
+ }
50570
+ }
50571
+ }
49982
50572
  return {
49983
- required: true,
49984
- contentTypes: content ? Object.keys(content) : []
50573
+ required: body.required === true,
50574
+ contentTypes: content ? Object.keys(content) : [],
50575
+ fieldRules,
50576
+ jsonSchemas: content ? requestBodyJsonSchemas(root, content, version, operationId, warnings) : void 0
49985
50577
  };
49986
50578
  }
49987
- function responseContent(root, version, response) {
50579
+ function exampleCandidates(root, mediaObject) {
50580
+ const candidates = [];
50581
+ if ("example" in mediaObject) candidates.push({ label: "example", value: mediaObject.example });
50582
+ const examples = asRecord4(mediaObject.examples);
50583
+ if (examples) {
50584
+ for (const [name, rawExample] of Object.entries(examples)) {
50585
+ let example;
50586
+ try {
50587
+ example = resolveInternalRef(root, rawExample);
50588
+ } catch {
50589
+ continue;
50590
+ }
50591
+ if (example && "value" in example) candidates.push({ label: `examples.${name}`, value: example.value });
50592
+ }
50593
+ }
50594
+ return candidates;
50595
+ }
50596
+ function validateExamples(root, mediaObject, packed, contentType, context, warnings) {
50597
+ if (packed.schema === void 0 || packed.unsupported) return;
50598
+ const candidates = exampleCandidates(root, mediaObject);
50599
+ if (candidates.length === 0) return;
50600
+ const validate2 = compileSchemaValidator(packed.schema);
50601
+ if (!validate2) return;
50602
+ for (const candidate of candidates) {
50603
+ if (!validate2(candidate.value)) {
50604
+ warnings.add(`CONTRACT_EXAMPLE_SCHEMA_MISMATCH: ${candidate.label} for ${contentType} on ${context} does not match its schema`);
50605
+ }
50606
+ }
50607
+ }
50608
+ function responseContent(root, version, response, context, warnings) {
49988
50609
  const content = asRecord4(response.content);
49989
50610
  if (!content) return {};
49990
50611
  const media = {};
49991
50612
  for (const [contentType, mediaObject] of Object.entries(content)) {
49992
- const schema2 = asRecord4(mediaObject)?.schema;
49993
- media[contentType] = schema2 === void 0 ? {} : packSchema(root, schema2, version);
50613
+ const mediaRecord = asRecord4(mediaObject);
50614
+ const schema2 = mediaRecord?.schema;
50615
+ let packed = schema2 === void 0 ? {} : packSchema(root, schema2, version);
50616
+ for (const warning2 of packNoteWarnings(packed, `response ${contentType} of ${context}`)) warnings.add(warning2);
50617
+ if (isSchemaGraphOverflow(packed)) {
50618
+ warnings.add(`CONTRACT_SCHEMA_NOT_COMPILED: response schema for ${contentType} on ${context} skipped (${packed.unsupported})`);
50619
+ packed = {};
50620
+ }
50621
+ const base = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
50622
+ if (mediaRecord && isJsonBaseType(base)) validateExamples(root, mediaRecord, packed, contentType, context, warnings);
50623
+ media[contentType] = packed;
49994
50624
  }
49995
50625
  return media;
49996
50626
  }
49997
- function responseHeaders(root, version, response) {
50627
+ function responseHeaders(root, version, response, context, warnings) {
49998
50628
  const headers = asRecord4(response.headers);
49999
50629
  if (!headers) return [];
50000
- return Object.entries(headers).map(([name, rawHeader]) => {
50630
+ const entries = [];
50631
+ for (const [name, rawHeader] of Object.entries(headers)) {
50632
+ if (name.toLowerCase() === "content-type") continue;
50001
50633
  const header = resolveInternalRef(root, rawHeader);
50002
- if (!header) return { name, required: true, unsupported: "Unresolved response header" };
50634
+ if (!header) {
50635
+ entries.push({ name, required: true, unsupported: "Unresolved response header" });
50636
+ continue;
50637
+ }
50003
50638
  const required = header.required === true;
50004
- if (header.content) return { name, required, unsupported: "OpenAPI response header content is unsupported" };
50005
- if (!header.schema) return { name, required };
50006
- return { name, required, ...packSchema(root, header.schema, version) };
50007
- });
50639
+ if (header.content) {
50640
+ entries.push({ name, required, unsupported: "OpenAPI response header content is unsupported" });
50641
+ continue;
50642
+ }
50643
+ if (!header.schema) {
50644
+ entries.push({ name, required });
50645
+ continue;
50646
+ }
50647
+ const packed = packSchema(root, header.schema, version);
50648
+ for (const warning2 of packNoteWarnings(packed, `response header ${name} of ${context}`)) warnings.add(warning2);
50649
+ if (isSchemaGraphOverflow(packed)) {
50650
+ warnings.add(`CONTRACT_SCHEMA_NOT_COMPILED: response header ${name} schema on ${context} skipped (${packed.unsupported})`);
50651
+ entries.push({ name, required });
50652
+ continue;
50653
+ }
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
+ }
50660
+ warnings.add(`CONTRACT_HEADER_SCHEMA_NOT_VALIDATED: response header ${name} on ${context} declares a non-scalar schema and its value is not validated`);
50661
+ entries.push({ name, required });
50662
+ continue;
50663
+ }
50664
+ entries.push({ name, required, ...packed });
50665
+ }
50666
+ return entries;
50008
50667
  }
50009
50668
  function buildContractIndex(root) {
50010
50669
  if (root.swagger === "2.0") throw new Error("CONTRACT_UNSUPPORTED_OPENAPI_VERSION: Dynamic contract tests require OpenAPI 3.0 or 3.1 (found swagger 2.0)");
@@ -50029,11 +50688,23 @@ function buildContractIndex(root) {
50029
50688
  throw new Error(`CONTRACT_OPERATION_NO_RESPONSES: ${lowerMethod.toUpperCase()} ${path8} must define at least one response`);
50030
50689
  }
50031
50690
  const contractResponses = {};
50691
+ const responseWarnings = /* @__PURE__ */ new Set();
50032
50692
  for (const [status, rawResponse] of Object.entries(responses)) {
50033
50693
  const response = resolveInternalRef(root, rawResponse);
50034
50694
  if (!response) continue;
50035
- const content = responseContent(root, version, response);
50036
- const headers = responseHeaders(root, version, response);
50695
+ if (asRecord4(response.links)) {
50696
+ responseWarnings.add(`CONTRACT_LINKS_NOT_VALIDATED: response links are not validated for ${lowerMethod.toUpperCase()} ${path8}`);
50697
+ }
50698
+ const responseContext = `${lowerMethod.toUpperCase()} ${path8} status ${status}`;
50699
+ const content = responseContent(root, version, response, responseContext, responseWarnings);
50700
+ for (const [contentType, media] of Object.entries(content)) {
50701
+ const base = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
50702
+ const schemaType = asRecord4(media.schema)?.type;
50703
+ if (!isJsonBaseType(base) && media.schema !== void 0 && !media.unsupported && schemaType !== "string") {
50704
+ responseWarnings.add(`CONTRACT_NONJSON_SCHEMA_NOT_VALIDATED: response schema for ${contentType} on ${responseContext} is not validated at runtime`);
50705
+ }
50706
+ }
50707
+ const headers = responseHeaders(root, version, response, responseContext, responseWarnings);
50037
50708
  contractResponses[normalizeResponseKey(status)] = {
50038
50709
  content,
50039
50710
  hasBody: Object.keys(content).length > 0,
@@ -50044,21 +50715,45 @@ function buildContractIndex(root) {
50044
50715
  path8,
50045
50716
  ...operationServers(root, pathItem, operation).map((server) => joinPaths(serverPathPrefix(server), path8))
50046
50717
  ].map(normalizePath))];
50718
+ const operationId = `${lowerMethod.toUpperCase()} ${path8}`;
50047
50719
  const opWarnings = [];
50720
+ opWarnings.push(...responseWarnings);
50048
50721
  opWarnings.push(...collectSecuritySchemeWarnings(root, 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));
50726
+ if (operation.deprecated === true) {
50727
+ opWarnings.push(`CONTRACT_OPERATION_DEPRECATED: ${lowerMethod.toUpperCase()} ${path8} is marked deprecated in the OpenAPI document`);
50728
+ }
50049
50729
  const requiredParameters = collectParameters(root, pathItem, operation);
50050
50730
  for (const parameter of requiredParameters.filter((entry) => entry.securityDerived)) {
50051
50731
  opWarnings.push(`CONTRACT_SECURITY_NOT_VALIDATED: security parameter ${parameter.in}:${parameter.name} is not statically required in generated requests`);
50052
50732
  }
50733
+ for (const parameter of requiredParameters.filter((entry) => entry.in === "cookie" && !entry.securityDerived)) {
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`);
50735
+ }
50736
+ const pathParamWarnings = /* @__PURE__ */ new Set();
50737
+ for (const param of resolvedParameters(root, pathItem, operation)) {
50738
+ if (String(param.in || "").toLowerCase() !== "path") continue;
50739
+ const name = String(param.name || "");
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
+ }
50743
+ }
50744
+ opWarnings.push(...pathParamWarnings);
50053
50745
  operations.push({
50054
- id: `${lowerMethod.toUpperCase()} ${path8}`,
50746
+ id: operationId,
50055
50747
  method: lowerMethod.toUpperCase(),
50056
50748
  path: path8,
50057
50749
  pointer: `/paths/${path8.replace(/~/g, "~0").replace(/\//g, "~1")}/${lowerMethod}`,
50058
50750
  candidates,
50059
50751
  responses: contractResponses,
50060
50752
  requiredParameters,
50061
- requestBody: collectRequestBody(root, operation),
50753
+ declaredQueryParameters: collectDeclaredQueryParameters(root, pathItem, operation),
50754
+ parameterChecks,
50755
+ requestBody: collectRequestBody(root, operation, version, operationId, opWarnings),
50756
+ security: collectSecurityRuntimeChecks(root, operation),
50062
50757
  warnings: opWarnings
50063
50758
  });
50064
50759
  }
@@ -50079,35 +50774,6 @@ function buildContractIndex(root) {
50079
50774
  return { operations, version, warnings };
50080
50775
  }
50081
50776
 
50082
- // src/lib/spec/schema-validator-code.ts
50083
- var import_schemasafe = __toESM(require_src(), 1);
50084
- function compileSchemaValidatorCode(schema2) {
50085
- try {
50086
- const validate2 = (0, import_schemasafe.validator)(schema2, {
50087
- includeErrors: true,
50088
- allErrors: true,
50089
- contentValidation: false,
50090
- formatAssertion: true,
50091
- isJSON: true,
50092
- mode: "default",
50093
- removeAdditional: false,
50094
- requireSchema: true,
50095
- requireStringValidation: false,
50096
- useDefaults: false
50097
- });
50098
- const source = validate2.toModule();
50099
- if (/\beval\s*\(/.test(source) || /new\s+Function\b/.test(source)) {
50100
- throw new Error("schemasafe generated forbidden dynamic code");
50101
- }
50102
- return source;
50103
- } catch (error2) {
50104
- throw new Error(
50105
- `CONTRACT_SCHEMA_COMPILE_FAILED: ${error2 instanceof Error ? error2.message : String(error2)}`,
50106
- { cause: error2 }
50107
- );
50108
- }
50109
- }
50110
-
50111
50777
  // src/lib/spec/collection-contracts.ts
50112
50778
  var CONTRACT_SIZE_LIMITS = {
50113
50779
  warnTestScriptBytes: 256e3,
@@ -50196,29 +50862,51 @@ function matchOperation(index, request) {
50196
50862
  function assignValidator(lines, target, source) {
50197
50863
  lines.push(`${target} = ${source};`);
50198
50864
  }
50199
- function buildValidatorAssignments(operation) {
50865
+ function tryCompile(target, schema2, lines, warnings, context) {
50866
+ try {
50867
+ assignValidator(lines, target, compileSchemaValidatorCode(schema2));
50868
+ } catch (error2) {
50869
+ lines.push(`${target} = { skip: true };`);
50870
+ warnings.push(`CONTRACT_SCHEMA_NOT_COMPILED: ${context} could not be compiled into a runtime validator (${error2 instanceof Error ? error2.message.slice(0, 160) : String(error2)})`);
50871
+ }
50872
+ }
50873
+ function buildValidatorAssignments(operation, warnings) {
50200
50874
  const lines = ["var validators = {};"];
50875
+ const parameterChecks = operation.parameterChecks ?? [];
50876
+ if (parameterChecks.length > 0) {
50877
+ lines.push("var paramValidators = {};");
50878
+ for (const check of parameterChecks) {
50879
+ tryCompile(`paramValidators[${JSON.stringify(`${check.in}:${check.name.toLowerCase()}`)}]`, check.schema, lines, warnings, `parameter ${check.in}:${check.name} schema on ${operation.id}`);
50880
+ }
50881
+ }
50882
+ const bodySchemas = Object.entries(operation.requestBody?.jsonSchemas ?? {});
50883
+ if (bodySchemas.length > 0) {
50884
+ lines.push("var requestBodyValidators = {};");
50885
+ for (const [base, schema2] of bodySchemas) {
50886
+ tryCompile(`requestBodyValidators[${JSON.stringify(base)}]`, schema2, lines, warnings, `request body schema for ${base} on ${operation.id}`);
50887
+ }
50888
+ }
50201
50889
  for (const [status, response] of Object.entries(operation.responses)) {
50202
50890
  lines.push(`validators[${JSON.stringify(status)}] = validators[${JSON.stringify(status)}] || {};`);
50203
50891
  for (const [mediaType, media] of Object.entries(response.content)) {
50204
50892
  if (media.schema !== void 0 && !media.unsupported) {
50205
- assignValidator(lines, `validators[${JSON.stringify(status)}][${JSON.stringify(mediaType)}]`, compileSchemaValidatorCode(media.schema));
50893
+ tryCompile(`validators[${JSON.stringify(status)}][${JSON.stringify(mediaType)}]`, media.schema, lines, warnings, `response schema for ${mediaType} on ${operation.id} status ${status}`);
50206
50894
  }
50207
50895
  }
50208
50896
  for (const header of response.headers) {
50209
50897
  if (header.schema !== void 0 && !header.unsupported) {
50210
50898
  lines.push(`validators[${JSON.stringify(status)}].__headers = validators[${JSON.stringify(status)}].__headers || {};`);
50211
- assignValidator(lines, `validators[${JSON.stringify(status)}].__headers[${JSON.stringify(header.name.toLowerCase())}]`, compileSchemaValidatorCode(header.schema));
50899
+ tryCompile(`validators[${JSON.stringify(status)}].__headers[${JSON.stringify(header.name.toLowerCase())}]`, header.schema, lines, warnings, `response header ${header.name} schema on ${operation.id} status ${status}`);
50212
50900
  }
50213
50901
  }
50214
50902
  }
50215
50903
  return lines;
50216
50904
  }
50217
- function createContractScript(operation) {
50218
- const contract = { method: operation.method, path: operation.path, responses: operation.responses };
50905
+ function createContractScript(operation, warnings = []) {
50906
+ const contract = { method: operation.method, path: operation.path, responses: operation.responses, security: operation.security, parameters: operation.parameterChecks };
50219
50907
  return [
50220
50908
  `var contract = JSON.parse(${JSON.stringify(JSON.stringify(contract))});`,
50221
- ...buildValidatorAssignments(operation),
50909
+ ...buildValidatorAssignments(operation, warnings),
50222
50910
  "function selectedResponseContract() {",
50223
50911
  " var status = String(pm.response.code);",
50224
50912
  " if (contract.responses[status]) return { key: status, value: contract.responses[status] };",
@@ -50232,6 +50920,15 @@ function createContractScript(operation) {
50232
50920
  'function mediaBase(value) { return String(value || "").toLowerCase().split(";")[0].trim(); }',
50233
50921
  'function mediaParts(value) { var base = mediaBase(value); var parts = base.split("/"); return { raw: base, type: parts[0] || "", subtype: parts[1] || "" }; }',
50234
50922
  'function isJsonSubtype(subtype) { return subtype === "json" || /\\+json$/.test(subtype); }',
50923
+ "function coerceBySchema(value, schema) {",
50924
+ " var type = schema && schema.type;",
50925
+ " var types = Array.isArray(type) ? type : [type];",
50926
+ ' if ((types.indexOf("integer") !== -1 || types.indexOf("number") !== -1) && /^-?[0-9]+([.][0-9]+)?([eE][+-]?[0-9]+)?$/.test(String(value).trim())) return Number(value);',
50927
+ ' if (types.indexOf("boolean") !== -1 && (value === "true" || value === "false")) return value === "true";',
50928
+ " return value;",
50929
+ "}",
50930
+ 'function requestHeader(name) { var value = ""; pm.request.headers.each(function (header) { if (header && header.disabled !== true && String(header.key).toLowerCase() === String(name).toLowerCase()) value = String(header.value); }); return value; }',
50931
+ "function hasQueryParam(name) { var found = false; pm.request.url.query.each(function (param) { if (param && param.disabled !== true && String(param.key).toLowerCase() === String(name).toLowerCase()) found = true; }); return found; }",
50235
50932
  "function mediaScore(expected, actual) {",
50236
50933
  " var e = mediaParts(expected); var a = mediaParts(actual);",
50237
50934
  " if (!e.raw || !a.raw) return 0;",
@@ -50262,7 +50959,11 @@ function createContractScript(operation) {
50262
50959
  " if (!actual) return;",
50263
50960
  ' if (header.unsupported) pm.expect.fail("OpenAPI response header unsupported for " + contract.method + " " + contract.path + ": " + header.unsupported);',
50264
50961
  " var headerValidator = validators[selected.key] && validators[selected.key].__headers && validators[selected.key].__headers[String(header.name).toLowerCase()];",
50265
- ' if (headerValidator && !headerValidator(actual)) 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 || []));',
50266
50967
  " });",
50267
50968
  "});",
50268
50969
  "pm.test('Response body matches OpenAPI body contract', function () {",
@@ -50290,11 +50991,122 @@ function createContractScript(operation) {
50290
50991
  ' if (media.media.unsupported) pm.expect.fail("OpenAPI schema unsupported for " + contract.method + " " + contract.path + " status " + pm.response.code + ": " + media.media.unsupported);',
50291
50992
  " if (!media.media.schema) { return; }",
50292
50993
  " var validate = validators[selected.key] && validators[selected.key][media.expected];",
50994
+ " if (validate && validate.skip) return;",
50293
50995
  ' if (!validate) pm.expect.fail("OpenAPI schema validator was not generated for " + media.expected);',
50294
50996
  ' var actual = mediaParts(pm.response.headers.get("Content-Type") || "");',
50295
50997
  " var value = isJsonSubtype(actual.subtype) ? pm.response.json() : responseText();",
50296
- ' if (!isJsonSubtype(actual.subtype) && media.media.schema && media.media.schema.type !== "string") pm.expect.fail("Non-JSON response schema validation unsupported for " + contract.method + " " + contract.path);',
50998
+ // Non-JSON object-schema bodies skip schema validation instead of
50999
+ // failing; the index emits CONTRACT_NONJSON_SCHEMA_NOT_VALIDATED so the
51000
+ // skip is visible at instrumentation time.
51001
+ ' if (!isJsonSubtype(actual.subtype) && media.media.schema && media.media.schema.type !== "string") { return; }',
50297
51002
  ' if (!validate(value)) pm.expect.fail("OpenAPI schema validation failed for " + contract.method + " " + contract.path + " status " + pm.response.code + ": " + JSON.stringify(validate.errors || []));',
51003
+ "});",
51004
+ ...operation.security ? [
51005
+ "pm.test('Request carries credentials required by OpenAPI security', function () {",
51006
+ " function satisfied(check) {",
51007
+ " if (!check.checkable) return true;",
51008
+ ' if (check.in === "query") return hasQueryParam(check.name);',
51009
+ ' if (check.in === "cookie") { if (pm.cookies && pm.cookies.has && pm.cookies.has(String(check.name))) return true; return requestHeader("Cookie").split(";").some(function (part) { return part.split("=")[0].trim() === String(check.name); }); }',
51010
+ ' if (check.prefix) { return requestHeader("Authorization").toLowerCase().indexOf(check.prefix.toLowerCase()) === 0; }',
51011
+ ' if (check.kind === "oauth2" || check.kind === "openIdConnect") { return Boolean(requestHeader("Authorization")) || hasQueryParam("access_token"); }',
51012
+ " return Boolean(requestHeader(check.name));",
51013
+ " }",
51014
+ " var alternatives = contract.security || [];",
51015
+ " var ok = alternatives.some(function (alternative) { return alternative.every(function (check) { return satisfied(check); }); });",
51016
+ ' if (!ok) pm.expect.fail("Request did not carry credentials for any OpenAPI security requirement of " + contract.method + " " + contract.path + ": " + alternatives.map(function (alternative) { return alternative.map(function (check) { return check.scheme + " (" + check.kind + ")"; }).join(" + "); }).join(" | "));',
51017
+ "});"
51018
+ ] : [],
51019
+ ...operation.parameterChecks && operation.parameterChecks.length > 0 ? [
51020
+ "pm.test('Request parameters match OpenAPI schemas', function () {",
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; }',
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; }',
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; } }",
51032
+ " (contract.parameters || []).forEach(function (param) {",
51033
+ ' var key = param.in + ":" + String(param.name).toLowerCase();',
51034
+ " var validate = paramValidators[key];",
51035
+ " if (!validate || validate.skip) return;",
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 || []));',
51080
+ " });",
51081
+ "});"
51082
+ ] : [],
51083
+ ...operation.requestBody?.jsonSchemas && Object.keys(operation.requestBody.jsonSchemas).length > 0 ? [
51084
+ "pm.test('Request body matches OpenAPI request schema', function () {",
51085
+ " var body = pm.request.body;",
51086
+ ' var raw = body && body.mode === "raw" && typeof body.raw === "string" ? body.raw : "";',
51087
+ " if (!raw.trim()) return;",
51088
+ ' if (/"<[^"<>]*>"/.test(raw) || raw.indexOf("{{") !== -1) return;',
51089
+ ' var validate = requestBodyValidators[mediaBase(requestHeader("Content-Type"))];',
51090
+ " if (!validate || validate.skip) return;",
51091
+ " var parsed;",
51092
+ // Unquoted generator placeholders such as {"count": <long>} break
51093
+ // JSON.parse; a parse failure alongside an angle-bracket token is
51094
+ // treated as a placeholder body rather than drift.
51095
+ ' try { parsed = JSON.parse(raw); } catch (error) { if (/<[A-Za-z][A-Za-z0-9_ -]*>/.test(raw)) return; pm.expect.fail("Request body for " + contract.method + " " + contract.path + " is not valid JSON: " + error); return; }',
51096
+ ' if (!validate(parsed)) pm.expect.fail("Request body failed OpenAPI request schema validation for " + contract.method + " " + contract.path + ": " + JSON.stringify(validate.errors || []));',
51097
+ "});"
51098
+ ] : [],
51099
+ "pm.test('Content-Length is consistent with OpenAPI body expectations', function () {",
51100
+ ' var raw = pm.response.headers.get("Content-Length");',
51101
+ " if (raw === null || raw === undefined) return;",
51102
+ // Combined duplicate Content-Length values ("5, 5") fail the integer
51103
+ // syntax check on purpose: RFC 9110 tolerates identical repeats, but a
51104
+ // contract test surfacing them is strict by design.
51105
+ ' if (!/^[0-9]+$/.test(String(raw).trim())) pm.expect.fail("Content-Length header is not a non-negative integer: " + raw);',
51106
+ ' if (contract.method === "HEAD" || pm.response.code === 304) return;',
51107
+ ' if (pm.response.headers.get("Content-Encoding")) return;',
51108
+ " var mustBeEmpty = isBodyless() || (selected && Object.keys(selected.value.content || {}).length === 0);",
51109
+ ' if (mustBeEmpty && Number(String(raw).trim()) !== 0) pm.expect.fail("OpenAPI defines no response body for " + contract.method + " " + contract.path + " status " + pm.response.code + " but Content-Length was " + raw);',
50298
51110
  "});"
50299
51111
  ];
50300
51112
  }
@@ -50387,14 +51199,160 @@ function assertStaticRequestShape(operation, request) {
50387
51199
  if (!contentType) {
50388
51200
  throw new Error(`CONTRACT_STATIC_REQUEST_CHECK_FAILED: ${operation.id} missing required request Content-Type`);
50389
51201
  }
50390
- const actual = contentType.toLowerCase().split(";")[0]?.trim();
50391
- 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));
50392
51204
  if (!matches) {
50393
51205
  throw new Error(
50394
51206
  `CONTRACT_STATIC_REQUEST_CHECK_FAILED: ${operation.id} request Content-Type ${contentType} does not match ${operation.requestBody.contentTypes.join(", ")}`
50395
51207
  );
50396
51208
  }
50397
51209
  }
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;
51221
+ }
51222
+ function requestBodyFieldNames(request, base) {
51223
+ const body = asRecord5(request.body);
51224
+ if (!body) return void 0;
51225
+ if (base === "application/json" || /\+json$/.test(base)) {
51226
+ if (body.mode !== "raw" || typeof body.raw !== "string") return void 0;
51227
+ let parsed;
51228
+ try {
51229
+ parsed = JSON.parse(body.raw);
51230
+ } catch {
51231
+ return void 0;
51232
+ }
51233
+ const record = asRecord5(parsed);
51234
+ return record ? Object.keys(record) : void 0;
51235
+ }
51236
+ const mode = base === "application/x-www-form-urlencoded" ? "urlencoded" : base === "multipart/form-data" ? "formdata" : "";
51237
+ if (!mode || !Array.isArray(body[mode])) return void 0;
51238
+ return body[mode].map((entry) => asRecord5(entry)).filter((entry) => Boolean(entry)).filter((entry) => entry.disabled !== true).map((entry) => String(entry.key || "")).filter(Boolean);
51239
+ }
51240
+ function requestBodyEntries(request, base) {
51241
+ const body = asRecord5(request.body);
51242
+ const mode = base === "application/x-www-form-urlencoded" ? "urlencoded" : base === "multipart/form-data" ? "formdata" : "";
51243
+ if (!body || !mode || !Array.isArray(body[mode])) return void 0;
51244
+ return body[mode].map((entry) => asRecord5(entry)).filter((entry) => Boolean(entry)).filter((entry) => entry.disabled !== true);
51245
+ }
51246
+ function mediaTypeMatchesPattern(pattern, actual) {
51247
+ return pattern.split(",").some((candidate) => {
51248
+ const entry = (candidate.split(";")[0] ?? "").trim();
51249
+ if (!entry) return false;
51250
+ if (entry === "*/*" || entry === actual) return true;
51251
+ if (entry.endsWith("/*")) return actual.startsWith(entry.slice(0, -1));
51252
+ return false;
51253
+ });
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
+ }
51264
+ function collectStaticEncodingWarnings(operation, request, base, rule) {
51265
+ const encodings = rule.encodings;
51266
+ if (!encodings) return [];
51267
+ const multipart = base === "multipart/form-data";
51268
+ const entries = requestBodyEntries(request, base);
51269
+ if (!entries) return [];
51270
+ const warnings = [];
51271
+ const part = multipart ? "multipart" : "urlencoded";
51272
+ for (const [field, encoding] of Object.entries(encodings)) {
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`);
51329
+ }
51330
+ }
51331
+ }
51332
+ return warnings;
51333
+ }
51334
+ function collectStaticBodyWarnings(operation, request, contentType) {
51335
+ const rules = operation.requestBody?.fieldRules;
51336
+ if (!rules) return [];
51337
+ const base = (contentType || "").toLowerCase().split(";")[0]?.trim() ?? "";
51338
+ const rule = rules[base];
51339
+ if (!rule) return [];
51340
+ const warnings = [
51341
+ ...collectStaticEncodingWarnings(operation, request, base, rule),
51342
+ ...collectStaticFieldSchemaWarnings(operation, request, base, rule)
51343
+ ];
51344
+ const names = requestBodyFieldNames(request, base);
51345
+ if (!names) return warnings;
51346
+ const present = new Set(names);
51347
+ const missing = rule.required.filter((name) => !present.has(name));
51348
+ if (missing.length > 0) {
51349
+ warnings.push(`CONTRACT_REQUEST_BODY_INCOMPLETE: ${operation.id} generated request body is missing required properties: ${missing.join(", ")}`);
51350
+ }
51351
+ const readOnlySent = rule.readOnly.filter((name) => present.has(name));
51352
+ if (readOnlySent.length > 0) {
51353
+ warnings.push(`CONTRACT_READONLY_PROPERTY_IN_REQUEST: ${operation.id} generated request body includes readOnly properties: ${readOnlySent.join(", ")}`);
51354
+ }
51355
+ return warnings;
50398
51356
  }
50399
51357
  function validateScript(script) {
50400
51358
  const source = script.join("\n");
@@ -50443,9 +51401,12 @@ function instrumentContractCollection(collection, index) {
50443
51401
  if (result.operation) {
50444
51402
  const previous = covered.get(result.operation.id);
50445
51403
  if (previous) throw new Error(`CONTRACT_DUPLICATE_OPERATION_REQUEST: ${result.operation.id} matched more than one generated request (${previous}, ${String(item.name || "<unnamed>")})`);
50446
- assertStaticRequestShape(result.operation, request);
51404
+ warnings.push(...assertStaticRequestShape(result.operation, request));
51405
+ for (const name of [...requestQueryNames(request)].filter((entry) => !result.operation.declaredQueryParameters.includes(entry))) {
51406
+ warnings.push(`CONTRACT_UNDOCUMENTED_QUERY_PARAM: ${result.operation.id} generated request sends query parameter ${name} that the OpenAPI operation does not declare`);
51407
+ }
50447
51408
  covered.set(result.operation.id, String(item.name || "<unnamed>"));
50448
- script = createContractScript(result.operation);
51409
+ script = createContractScript(result.operation, warnings);
50449
51410
  } else if (result.ambiguous && result.ambiguous.length > 0) {
50450
51411
  script = createMappingFailureScript(`Ambiguous OpenAPI operation match for request ${result.method} ${result.path}: ${result.ambiguous.map((entry) => entry.id).join(", ")}`);
50451
51412
  } else {