@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/cli.cjs CHANGED
@@ -47930,8 +47930,6 @@ var COMPATIBLE_SCHEMA_URIS = /* @__PURE__ */ new Set([
47930
47930
  `${DRAFT_2020_12_SCHEMA_URI}#`
47931
47931
  ]);
47932
47932
  var ASSERTION_KEYS = /* @__PURE__ */ new Set([
47933
- "$defs",
47934
- "$id",
47935
47933
  "$ref",
47936
47934
  "$schema",
47937
47935
  "additionalItems",
@@ -47976,6 +47974,10 @@ var ASSERTION_KEYS = /* @__PURE__ */ new Set([
47976
47974
  ]);
47977
47975
  var STRIP_KEYS = /* @__PURE__ */ new Set([
47978
47976
  "title",
47977
+ "$comment",
47978
+ "$defs",
47979
+ "definitions",
47980
+ "$id",
47979
47981
  "description",
47980
47982
  "default",
47981
47983
  "example",
@@ -47986,8 +47988,32 @@ var STRIP_KEYS = /* @__PURE__ */ new Set([
47986
47988
  "xml",
47987
47989
  "externalDocs",
47988
47990
  "discriminator",
47989
- "format"
47991
+ "contentEncoding",
47992
+ "contentMediaType",
47993
+ "contentSchema"
47994
+ ]);
47995
+ var ASSERTED_FORMATS = /* @__PURE__ */ new Set([
47996
+ "date-time",
47997
+ "date",
47998
+ "time",
47999
+ "duration",
48000
+ "email",
48001
+ "hostname",
48002
+ "ipv4",
48003
+ "ipv6",
48004
+ "uri",
48005
+ "uri-reference",
48006
+ "uri-template",
48007
+ "uuid",
48008
+ "json-pointer",
48009
+ "relative-json-pointer",
48010
+ "regex"
47990
48011
  ]);
48012
+ var INT32_MIN = -2147483648;
48013
+ var INT32_MAX = 2147483647;
48014
+ var MAX_REFERENCED_SCHEMAS = 400;
48015
+ var DRAFT_2020_12_ONLY_KEYS = /* @__PURE__ */ new Set(["prefixItems", "dependentRequired", "dependentSchemas", "minContains", "maxContains", "unevaluatedItems", "unevaluatedProperties"]);
48016
+ var DRAFT_07_ONLY_KEYS = /* @__PURE__ */ new Set(["dependencies", "additionalItems"]);
47991
48017
  function asRecord3(value) {
47992
48018
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
47993
48019
  return value;
@@ -48005,8 +48031,8 @@ function resolvePointer(root, ref) {
48005
48031
  function unsupported(message) {
48006
48032
  return { unsupported: message };
48007
48033
  }
48008
- function mergeRequiredWithoutWriteOnly(required, writeOnlyProperties) {
48009
- const values = asArray(required).map((entry) => String(entry)).filter((entry) => !writeOnlyProperties.has(entry));
48034
+ function mergeRequiredWithoutStripped(required, strippedProperties) {
48035
+ const values = asArray(required).map((entry) => String(entry)).filter((entry) => !strippedProperties.has(entry));
48010
48036
  return values.length > 0 ? values : void 0;
48011
48037
  }
48012
48038
  function hasUnsupported(child) {
@@ -48022,11 +48048,31 @@ function rootDialect(root, version, schemaRecord) {
48022
48048
  if (declared.includes("draft-07")) return DRAFT_07_SCHEMA_URI;
48023
48049
  return DRAFT_2020_12_SCHEMA_URI;
48024
48050
  }
48025
- function normalizeSchema(root, schema2, options) {
48026
- const depth = options.depth ?? 0;
48027
- if (depth > 50) return unsupported("OpenAPI schema reference depth exceeded 50");
48051
+ function registerDef(ctx, ref) {
48052
+ const existing = ctx.defs.get(ref);
48053
+ if (existing) return existing;
48054
+ if (ctx.defs.size >= MAX_REFERENCED_SCHEMAS) {
48055
+ const entry2 = { name: "overflow", unsupported: `OpenAPI schema reference graph exceeded ${MAX_REFERENCED_SCHEMAS} referenced schemas` };
48056
+ return entry2;
48057
+ }
48058
+ const entry = { name: `d${ctx.defs.size}` };
48059
+ ctx.defs.set(ref, entry);
48060
+ const resolved = resolvePointer(ctx.root, ref);
48061
+ if (resolved === void 0) {
48062
+ entry.unsupported = `Unresolved OpenAPI $ref: ${ref}`;
48063
+ return entry;
48064
+ }
48065
+ const normalized = normalizeSchema(ctx, resolved, { depth: 0, rootSchema: false });
48066
+ const bad = hasUnsupported(normalized);
48067
+ if (bad) entry.unsupported = bad;
48068
+ else entry.schema = normalized;
48069
+ return entry;
48070
+ }
48071
+ function normalizeSchema(ctx, schema2, options) {
48072
+ const depth = options.depth;
48073
+ if (depth > 50) return unsupported("OpenAPI schema nesting depth exceeded 50");
48028
48074
  if (Array.isArray(schema2)) {
48029
- const normalized2 = schema2.map((entry) => normalizeSchema(root, entry, { ...options, depth: depth + 1, rootSchema: false }));
48075
+ const normalized2 = schema2.map((entry) => normalizeSchema(ctx, entry, { depth: depth + 1, rootSchema: false }));
48030
48076
  const bad = normalized2.map(hasUnsupported).find(Boolean);
48031
48077
  return bad ? unsupported(bad) : normalized2;
48032
48078
  }
@@ -48035,40 +48081,74 @@ function normalizeSchema(root, schema2, options) {
48035
48081
  const ref = typeof record.$ref === "string" ? record.$ref : "";
48036
48082
  if (ref) {
48037
48083
  if (!ref.startsWith("#/")) return unsupported(`External OpenAPI $ref remained unresolved: ${ref}`);
48038
- const stack = options.stack ?? [];
48039
- if (stack.includes(ref)) return unsupported(`Recursive OpenAPI $ref is unsupported: ${ref}`);
48040
- const resolved = resolvePointer(root, ref);
48041
- if (resolved === void 0) return unsupported(`Unresolved OpenAPI $ref: ${ref}`);
48042
- const siblingEntries = Object.entries(record).filter(([key]) => key !== "$ref");
48043
- const resolvedSchema = normalizeSchema(root, resolved, {
48044
- ...options,
48045
- depth: depth + 1,
48046
- rootSchema: false,
48047
- stack: [...stack, ref]
48048
- });
48049
- const resolvedUnsupported = hasUnsupported(resolvedSchema);
48050
- if (resolvedUnsupported) return unsupported(resolvedUnsupported);
48051
- if (options.version === "3.0" || siblingEntries.length === 0) return resolvedSchema;
48052
- const siblingSchema = normalizeSchema(root, Object.fromEntries(siblingEntries), {
48053
- ...options,
48054
- depth: depth + 1,
48055
- rootSchema: false,
48056
- stack: [...stack, ref]
48057
- });
48084
+ const siblingEntries = ctx.version === "3.1" ? Object.entries(record).filter(([key]) => key !== "$ref") : [];
48085
+ const inlineStack = options.inlineStack ?? [];
48086
+ if (options.rootSchema && siblingEntries.length === 0 && !inlineStack.includes(ref)) {
48087
+ const resolved = resolvePointer(ctx.root, ref);
48088
+ if (resolved === void 0) return unsupported(`Unresolved OpenAPI $ref: ${ref}`);
48089
+ const inlined = normalizeSchema(ctx, resolved, { depth, rootSchema: true, inlineStack: [...inlineStack, ref] });
48090
+ const bad = hasUnsupported(inlined);
48091
+ return bad ? unsupported(bad) : inlined;
48092
+ }
48093
+ const entry = registerDef(ctx, ref);
48094
+ if (entry.unsupported) return unsupported(entry.unsupported);
48095
+ const refNode = { $ref: `#/$defs/${entry.name}` };
48096
+ if (siblingEntries.length === 0) {
48097
+ if (options.rootSchema) refNode.$schema = ctx.dialect;
48098
+ return refNode;
48099
+ }
48100
+ const siblingSchema = normalizeSchema(ctx, Object.fromEntries(siblingEntries), { depth: depth + 1, rootSchema: false });
48058
48101
  const siblingUnsupported = hasUnsupported(siblingSchema);
48059
48102
  if (siblingUnsupported) return unsupported(siblingUnsupported);
48060
- const wrapper = { allOf: [resolvedSchema, siblingSchema] };
48061
- if (options.rootSchema) wrapper.$schema = options.dialect ?? rootDialect(root, options.version, record);
48103
+ const wrapper = { allOf: [refNode, siblingSchema] };
48104
+ if (options.rootSchema) wrapper.$schema = ctx.dialect;
48062
48105
  return wrapper;
48063
48106
  }
48064
48107
  const sourceSchema = { ...record };
48065
- const nullable = sourceSchema.nullable === true && options.version === "3.0";
48108
+ const nullable = sourceSchema.nullable === true && ctx.version === "3.0";
48066
48109
  delete sourceSchema.nullable;
48067
- const writeOnlyProperties = /* @__PURE__ */ new Set();
48110
+ const discriminator = asRecord3(sourceSchema.discriminator);
48111
+ if (discriminator && typeof discriminator.propertyName === "string" && discriminator.propertyName) {
48112
+ const propertyName = discriminator.propertyName;
48113
+ const branchKey = Array.isArray(sourceSchema.oneOf) ? "oneOf" : Array.isArray(sourceSchema.anyOf) ? "anyOf" : "";
48114
+ const members = branchKey ? sourceSchema[branchKey] : [];
48115
+ const memberRefs = members.map((member) => {
48116
+ const memberRecord = asRecord3(member);
48117
+ const memberRef = memberRecord && Object.keys(memberRecord).length === 1 && typeof memberRecord.$ref === "string" ? memberRecord.$ref : "";
48118
+ return memberRef.startsWith("#/") ? memberRef : "";
48119
+ });
48120
+ if (branchKey && members.length > 0 && memberRefs.every(Boolean)) {
48121
+ const valuesByRef = /* @__PURE__ */ new Map();
48122
+ const refByMappingKey = /* @__PURE__ */ new Map();
48123
+ for (const [value, target] of Object.entries(asRecord3(discriminator.mapping) ?? {})) {
48124
+ if (typeof target !== "string" || !target) continue;
48125
+ const targetRef = target.startsWith("#/") ? target : `#/components/schemas/${target}`;
48126
+ valuesByRef.set(targetRef, [...valuesByRef.get(targetRef) ?? [], value]);
48127
+ refByMappingKey.set(value, targetRef);
48128
+ }
48129
+ sourceSchema[branchKey] = members.map((member, index) => {
48130
+ const memberRef = memberRefs[index] ?? "";
48131
+ const tail = (memberRef.split("/").pop() ?? "").replace(/~1/g, "/").replace(/~0/g, "~");
48132
+ const values = [...valuesByRef.get(memberRef) ?? []];
48133
+ const tailClaimedElsewhere = refByMappingKey.has(tail) && refByMappingKey.get(tail) !== memberRef;
48134
+ if (tail && !tailClaimedElsewhere && !values.includes(tail)) values.push(tail);
48135
+ const dispatch = values.length === 1 ? { const: values[0] } : { enum: values };
48136
+ return { allOf: [member, { type: "object", required: [propertyName], properties: { [propertyName]: dispatch } }] };
48137
+ });
48138
+ } else {
48139
+ ctx.notes.add("discriminator");
48140
+ }
48141
+ }
48142
+ const directionStrippedFlag = ctx.direction === "request" ? "readOnly" : "writeOnly";
48143
+ const strippedProperties = /* @__PURE__ */ new Set();
48068
48144
  const rawProperties = asRecord3(sourceSchema.properties);
48069
48145
  if (rawProperties) {
48070
48146
  for (const [propertyName, propertySchema] of Object.entries(rawProperties)) {
48071
- if (asRecord3(propertySchema)?.writeOnly === true) writeOnlyProperties.add(propertyName);
48147
+ let flagSource = asRecord3(propertySchema);
48148
+ if (typeof flagSource?.$ref === "string" && flagSource.$ref.startsWith("#/")) {
48149
+ flagSource = asRecord3(resolvePointer(ctx.root, flagSource.$ref)) ?? flagSource;
48150
+ }
48151
+ if (flagSource?.[directionStrippedFlag] === true) strippedProperties.add(propertyName);
48072
48152
  }
48073
48153
  }
48074
48154
  const normalized = {};
@@ -48095,17 +48175,69 @@ function normalizeSchema(root, schema2, options) {
48095
48175
  delete normalized.maximum;
48096
48176
  continue;
48097
48177
  }
48178
+ if (key === "format") {
48179
+ if (typeof value === "string" && ASSERTED_FORMATS.has(value)) normalized.format = value;
48180
+ continue;
48181
+ }
48098
48182
  if (!ASSERTION_KEYS.has(key)) return unsupported(`Unsupported OpenAPI schema keyword: ${key}`);
48099
- if (key === "items" && Array.isArray(value) && options.version === "3.0") {
48183
+ if (ctx.dialect === DRAFT_07_SCHEMA_URI && DRAFT_2020_12_ONLY_KEYS.has(key)) {
48184
+ return unsupported(`${key} requires the JSON Schema 2020-12 dialect`);
48185
+ }
48186
+ if (ctx.dialect === DRAFT_2020_12_SCHEMA_URI && DRAFT_07_ONLY_KEYS.has(key)) {
48187
+ return unsupported(`${key} is a draft-07 keyword and is unsupported under JSON Schema 2020-12`);
48188
+ }
48189
+ if (key === "items" && Array.isArray(value) && ctx.version === "3.0") {
48100
48190
  return unsupported("Tuple array items are unsupported in OpenAPI 3.0");
48101
48191
  }
48192
+ if (key === "patternProperties" || key === "dependentSchemas") {
48193
+ const map2 = asRecord3(value);
48194
+ if (!map2) continue;
48195
+ const next = {};
48196
+ for (const [mapKey, mapSchema] of Object.entries(map2)) {
48197
+ const child2 = normalizeSchema(ctx, mapSchema, { depth: depth + 1, rootSchema: false });
48198
+ const bad2 = hasUnsupported(child2);
48199
+ if (bad2) return unsupported(bad2);
48200
+ next[mapKey] = child2;
48201
+ }
48202
+ normalized[key] = next;
48203
+ continue;
48204
+ }
48205
+ if (key === "dependentRequired") {
48206
+ const map2 = asRecord3(value);
48207
+ if (!map2) continue;
48208
+ for (const names of Object.values(map2)) {
48209
+ if (!Array.isArray(names) || names.some((name) => typeof name !== "string")) {
48210
+ return unsupported("dependentRequired requires string array values");
48211
+ }
48212
+ }
48213
+ normalized.dependentRequired = value;
48214
+ continue;
48215
+ }
48216
+ if (key === "dependencies") {
48217
+ const map2 = asRecord3(value);
48218
+ if (!map2) continue;
48219
+ const next = {};
48220
+ for (const [mapKey, dependency] of Object.entries(map2)) {
48221
+ if (Array.isArray(dependency)) {
48222
+ if (dependency.some((name) => typeof name !== "string")) return unsupported("dependencies requires string array or schema values");
48223
+ next[mapKey] = dependency;
48224
+ continue;
48225
+ }
48226
+ const child2 = normalizeSchema(ctx, dependency, { depth: depth + 1, rootSchema: false });
48227
+ const bad2 = hasUnsupported(child2);
48228
+ if (bad2) return unsupported(bad2);
48229
+ next[mapKey] = child2;
48230
+ }
48231
+ normalized.dependencies = next;
48232
+ continue;
48233
+ }
48102
48234
  if (key === "properties") {
48103
48235
  const properties = asRecord3(value);
48104
48236
  if (!properties) continue;
48105
48237
  const nextProperties = {};
48106
48238
  for (const [propertyName, propertySchema] of Object.entries(properties)) {
48107
- if (writeOnlyProperties.has(propertyName)) continue;
48108
- const child2 = normalizeSchema(root, propertySchema, { ...options, depth: depth + 1, rootSchema: false });
48239
+ if (strippedProperties.has(propertyName)) continue;
48240
+ const child2 = normalizeSchema(ctx, propertySchema, { depth: depth + 1, rootSchema: false });
48109
48241
  const bad2 = hasUnsupported(child2);
48110
48242
  if (bad2) return unsupported(bad2);
48111
48243
  nextProperties[propertyName] = child2;
@@ -48114,16 +48246,46 @@ function normalizeSchema(root, schema2, options) {
48114
48246
  continue;
48115
48247
  }
48116
48248
  if (key === "required") {
48117
- const required = mergeRequiredWithoutWriteOnly(value, writeOnlyProperties);
48249
+ const required = mergeRequiredWithoutStripped(value, strippedProperties);
48118
48250
  if (required) normalized.required = required;
48119
48251
  continue;
48120
48252
  }
48121
- const child = normalizeSchema(root, value, { ...options, depth: depth + 1, rootSchema: false });
48253
+ const child = normalizeSchema(ctx, value, { depth: depth + 1, rootSchema: false });
48122
48254
  const bad = hasUnsupported(child);
48123
48255
  if (bad) return unsupported(bad);
48124
48256
  normalized[key] = child;
48125
48257
  }
48126
- if (options.rootSchema) normalized.$schema = options.dialect ?? rootDialect(root, options.version, record);
48258
+ if (typeof normalized.minimum === "number" && typeof normalized.exclusiveMinimum === "number") {
48259
+ if (normalized.exclusiveMinimum >= normalized.minimum) delete normalized.minimum;
48260
+ else delete normalized.exclusiveMinimum;
48261
+ }
48262
+ if (typeof normalized.maximum === "number" && typeof normalized.exclusiveMaximum === "number") {
48263
+ if (normalized.exclusiveMaximum <= normalized.maximum) delete normalized.maximum;
48264
+ else delete normalized.exclusiveMaximum;
48265
+ }
48266
+ if (sourceSchema.format === "int32") {
48267
+ const type2 = normalized.type;
48268
+ const isInteger2 = type2 === "integer" || Array.isArray(type2) && type2.includes("integer");
48269
+ if (isInteger2) {
48270
+ if (typeof normalized.exclusiveMinimum === "number") {
48271
+ if (normalized.exclusiveMinimum < INT32_MIN) {
48272
+ delete normalized.exclusiveMinimum;
48273
+ normalized.minimum = INT32_MIN;
48274
+ }
48275
+ } else if (typeof normalized.minimum !== "number" || normalized.minimum < INT32_MIN) {
48276
+ normalized.minimum = INT32_MIN;
48277
+ }
48278
+ if (typeof normalized.exclusiveMaximum === "number") {
48279
+ if (normalized.exclusiveMaximum > INT32_MAX) {
48280
+ delete normalized.exclusiveMaximum;
48281
+ normalized.maximum = INT32_MAX;
48282
+ }
48283
+ } else if (typeof normalized.maximum !== "number" || normalized.maximum > INT32_MAX) {
48284
+ normalized.maximum = INT32_MAX;
48285
+ }
48286
+ }
48287
+ }
48288
+ if (options.rootSchema) normalized.$schema = ctx.dialect;
48127
48289
  if (nullable) {
48128
48290
  if (typeof normalized.type === "string") {
48129
48291
  normalized.type = [normalized.type, "null"];
@@ -48135,23 +48297,108 @@ function normalizeSchema(root, schema2, options) {
48135
48297
  const wrappedSchema = { ...normalized };
48136
48298
  delete wrappedSchema.$schema;
48137
48299
  const wrapper = { anyOf: [{ type: "null" }, wrappedSchema] };
48138
- if (options.rootSchema) wrapper.$schema = options.dialect ?? rootDialect(root, options.version, record);
48300
+ if (options.rootSchema) wrapper.$schema = ctx.dialect;
48139
48301
  return wrapper;
48140
48302
  }
48141
48303
  }
48142
48304
  return normalized;
48143
48305
  }
48144
- function packSchema(root, schema2, version) {
48306
+ function isSchemaGraphOverflow(packed) {
48307
+ return typeof packed.unsupported === "string" && packed.unsupported.startsWith("OpenAPI schema reference graph exceeded");
48308
+ }
48309
+ function packSchema(root, schema2, version, direction = "response") {
48145
48310
  try {
48311
+ if (schema2 === true) return { schema: { $schema: rootDialect(root, version) } };
48312
+ if (schema2 === false) return unsupported("Boolean false JSON Schema rejects every instance and is unsupported");
48146
48313
  const dialect = rootDialect(root, version, asRecord3(schema2) ?? void 0);
48147
- const normalized = normalizeSchema(root, schema2, { version, rootSchema: true, dialect });
48314
+ const ctx = { root, version, direction, dialect, defs: /* @__PURE__ */ new Map(), notes: /* @__PURE__ */ new Set() };
48315
+ const normalized = normalizeSchema(ctx, schema2, { depth: 0, rootSchema: true });
48148
48316
  const message = hasUnsupported(normalized);
48149
- return message ? unsupported(message) : { schema: normalized };
48317
+ if (message) return unsupported(message);
48318
+ const aliasTargets = /* @__PURE__ */ new Map();
48319
+ for (const entry of ctx.defs.values()) {
48320
+ if (entry.unsupported) return unsupported(entry.unsupported);
48321
+ const entrySchema = asRecord3(entry.schema);
48322
+ const ref = entrySchema && Object.keys(entrySchema).length === 1 && typeof entrySchema.$ref === "string" ? entrySchema.$ref : "";
48323
+ if (ref.startsWith("#/$defs/")) aliasTargets.set(entry.name, ref.slice("#/$defs/".length));
48324
+ }
48325
+ for (const start of aliasTargets.keys()) {
48326
+ const seen = /* @__PURE__ */ new Set();
48327
+ let current = start;
48328
+ while (current !== void 0 && aliasTargets.has(current)) {
48329
+ if (seen.has(current)) return unsupported("Self-referential alias schema is unsupported");
48330
+ seen.add(current);
48331
+ current = aliasTargets.get(current);
48332
+ }
48333
+ }
48334
+ if (ctx.defs.size > 0) {
48335
+ const normalizedRecord = asRecord3(normalized);
48336
+ if (!normalizedRecord) return unsupported("Referenced schemas require an object root schema");
48337
+ normalizedRecord.$defs = Object.fromEntries([...ctx.defs.values()].map((entry) => [entry.name, entry.schema]));
48338
+ }
48339
+ const packed = { schema: normalized };
48340
+ if (ctx.notes.size > 0) packed.notes = [...ctx.notes].sort();
48341
+ return packed;
48150
48342
  } catch (error) {
48151
48343
  return unsupported(error instanceof Error ? error.message : String(error));
48152
48344
  }
48153
48345
  }
48154
48346
 
48347
+ // src/lib/spec/schema-validator-code.ts
48348
+ var import_schemasafe = __toESM(require_src(), 1);
48349
+ function compileSchemaValidator(schema2) {
48350
+ try {
48351
+ const validate2 = (0, import_schemasafe.validator)(schema2, {
48352
+ includeErrors: false,
48353
+ // Real-world specs legally mix enum/const with other keywords; without
48354
+ // this flag schemasafe refuses to compile them (observed in Stripe,
48355
+ // Plaid, and DigitalOcean public specs).
48356
+ allowUnusedKeywords: true,
48357
+ contentValidation: false,
48358
+ formatAssertion: true,
48359
+ isJSON: true,
48360
+ mode: "default",
48361
+ removeAdditional: false,
48362
+ requireSchema: true,
48363
+ requireStringValidation: false,
48364
+ useDefaults: false
48365
+ });
48366
+ return (value) => validate2(value);
48367
+ } catch {
48368
+ return null;
48369
+ }
48370
+ }
48371
+ function compileSchemaValidatorCode(schema2) {
48372
+ try {
48373
+ const validate2 = (0, import_schemasafe.validator)(schema2, {
48374
+ includeErrors: true,
48375
+ allErrors: true,
48376
+ // Real-world specs legally mix enum/const with other keywords; without
48377
+ // this flag schemasafe refuses to compile them (observed in Stripe,
48378
+ // Plaid, and DigitalOcean public specs).
48379
+ allowUnusedKeywords: true,
48380
+ contentValidation: false,
48381
+ formatAssertion: true,
48382
+ isJSON: true,
48383
+ mode: "default",
48384
+ removeAdditional: false,
48385
+ requireSchema: true,
48386
+ requireStringValidation: false,
48387
+ useDefaults: false
48388
+ });
48389
+ const source = validate2.toModule();
48390
+ if (/\beval\s*\(/.test(source) || /new\s+Function\b/.test(source)) {
48391
+ throw new Error("schemasafe generated forbidden dynamic code");
48392
+ }
48393
+ return source;
48394
+ } catch (error) {
48395
+ throw new Error(
48396
+ `CONTRACT_SCHEMA_COMPILE_FAILED: ${error instanceof Error ? error.message : String(error)}`,
48397
+ { cause: error }
48398
+ );
48399
+ }
48400
+ }
48401
+
48155
48402
  // src/lib/spec/contract-index.ts
48156
48403
  var HTTP_METHODS = /* @__PURE__ */ new Set(["get", "put", "post", "delete", "options", "head", "patch", "trace"]);
48157
48404
  function asRecord4(value) {
@@ -48227,7 +48474,7 @@ function collectSecurityApiKeys(root, operation) {
48227
48474
  for (const requirement of requirements.map((entry) => asRecord4(entry)).filter(Boolean)) {
48228
48475
  for (const schemeName of Object.keys(requirement)) {
48229
48476
  const scheme = resolveInternalRef(root, securitySchemes?.[schemeName]);
48230
- if (scheme?.type === "apiKey" && typeof scheme.name === "string" && ["query", "header"].includes(String(scheme.in))) {
48477
+ if (scheme?.type === "apiKey" && typeof scheme.name === "string" && ["query", "header", "cookie"].includes(String(scheme.in))) {
48231
48478
  names.add(`${String(scheme.in)}:${scheme.name.toLowerCase()}`);
48232
48479
  }
48233
48480
  }
@@ -48249,12 +48496,197 @@ function collectSecuritySchemeWarnings(root, operation) {
48249
48496
  for (const schemeName of Object.keys(requirement)) {
48250
48497
  const scheme = resolveInternalRef(root, securitySchemes?.[schemeName]);
48251
48498
  warnings.add(
48252
- `CONTRACT_SECURITY_NOT_VALIDATED: security scheme ${schemeName} (${securitySchemeKind(scheme)}) is not runtime-proven by dynamic contract tests`
48499
+ `CONTRACT_SECURITY_NOT_VALIDATED: security scheme ${schemeName} (${securitySchemeKind(scheme)}) is not runtime-proven beyond credential presence by dynamic contract tests`
48253
48500
  );
48254
48501
  }
48255
48502
  }
48256
48503
  return [...warnings];
48257
48504
  }
48505
+ function securityCheckFor(schemeName, scheme) {
48506
+ const kind = securitySchemeKind(scheme);
48507
+ if (scheme?.type === "apiKey" && typeof scheme.name === "string" && ["header", "query", "cookie"].includes(String(scheme.in))) {
48508
+ return { scheme: schemeName, kind, checkable: true, in: String(scheme.in), name: scheme.name };
48509
+ }
48510
+ if (scheme?.type === "http") {
48511
+ const httpScheme = String(scheme.scheme || "").toLowerCase();
48512
+ if (httpScheme === "basic") return { scheme: schemeName, kind, checkable: true, prefix: "Basic " };
48513
+ if (httpScheme === "bearer") return { scheme: schemeName, kind, checkable: true, prefix: "Bearer " };
48514
+ if (httpScheme) return { scheme: schemeName, kind, checkable: true, prefix: `${httpScheme.charAt(0).toUpperCase()}${httpScheme.slice(1)} ` };
48515
+ return { scheme: schemeName, kind, checkable: true, in: "header", name: "Authorization" };
48516
+ }
48517
+ if (scheme?.type === "oauth2" || scheme?.type === "openIdConnect") {
48518
+ return { scheme: schemeName, kind, checkable: true, in: "header", name: "Authorization" };
48519
+ }
48520
+ return { scheme: schemeName, kind, checkable: false };
48521
+ }
48522
+ function collectSecurityRuntimeChecks(root, operation) {
48523
+ const securitySchemes = asRecord4(asRecord4(root.components)?.securitySchemes);
48524
+ const requirements = operation.security === void 0 ? asArray2(root.security) : asArray2(operation.security);
48525
+ const alternatives = [];
48526
+ for (const requirement of requirements.map((entry) => asRecord4(entry)).filter(Boolean)) {
48527
+ const schemeNames = Object.keys(requirement);
48528
+ if (schemeNames.length === 0) return void 0;
48529
+ alternatives.push(schemeNames.map((schemeName) => securityCheckFor(schemeName, resolveInternalRef(root, securitySchemes?.[schemeName]))));
48530
+ }
48531
+ if (alternatives.some((alternative) => alternative.length > 0 && alternative.every((check) => !check.checkable))) return void 0;
48532
+ return alternatives.length > 0 ? alternatives : void 0;
48533
+ }
48534
+ function resolvedParameters(root, pathItem, operation) {
48535
+ return [...asArray2(pathItem.parameters), ...asArray2(operation.parameters)].map((rawParam) => {
48536
+ try {
48537
+ return resolveInternalRef(root, rawParam);
48538
+ } catch {
48539
+ return null;
48540
+ }
48541
+ }).filter((param) => Boolean(param));
48542
+ }
48543
+ var DEFAULT_PARAM_STYLES = { query: "form", path: "simple", header: "simple", cookie: "form" };
48544
+ var IGNORED_HEADER_PARAMS = /* @__PURE__ */ new Set(["accept", "content-type", "authorization"]);
48545
+ function isIgnoredParameter(location2, name) {
48546
+ return location2 === "header" && IGNORED_HEADER_PARAMS.has(name.toLowerCase());
48547
+ }
48548
+ function jsonContentParameterMedia(param) {
48549
+ const content = asRecord4(param.content);
48550
+ if (!content) return void 0;
48551
+ const entries = Object.entries(content);
48552
+ if (entries.length !== 1) return void 0;
48553
+ const [contentType, mediaObject] = entries[0];
48554
+ const base = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
48555
+ if (!isJsonBaseType(base)) return void 0;
48556
+ const schema2 = asRecord4(mediaObject)?.schema;
48557
+ return schema2 === void 0 ? void 0 : schema2;
48558
+ }
48559
+ function collectSerializationWarnings(root, pathItem, operation, decodedKeys) {
48560
+ const warnings = [];
48561
+ for (const param of resolvedParameters(root, pathItem, operation)) {
48562
+ const location2 = String(param.in || "").toLowerCase();
48563
+ const name = String(param.name || "");
48564
+ const defaultStyle = DEFAULT_PARAM_STYLES[location2];
48565
+ if (!name || !defaultStyle || isIgnoredParameter(location2, name)) continue;
48566
+ const style = typeof param.style === "string" ? param.style : defaultStyle;
48567
+ const defaultExplode = style === "form";
48568
+ const explode = typeof param.explode === "boolean" ? param.explode : defaultExplode;
48569
+ const unvalidatedContent = param.content !== void 0 && (jsonContentParameterMedia(param) === void 0 || location2 !== "query" && location2 !== "header");
48570
+ if (style !== defaultStyle || explode !== defaultExplode || param.allowReserved === true || unvalidatedContent) {
48571
+ if (decodedKeys.has(`${location2}:${name.toLowerCase()}`) && param.allowReserved !== true && param.content === void 0) continue;
48572
+ warnings.push(`CONTRACT_PARAM_SERIALIZATION_NOT_VALIDATED: parameter ${location2}:${name} declares non-default style, explode, allowReserved, or content and its serialization is not validated`);
48573
+ }
48574
+ }
48575
+ return warnings;
48576
+ }
48577
+ var SCALAR_SCHEMA_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean", "null"]);
48578
+ function packedScalarSchema(packed) {
48579
+ if (packed.unsupported || packed.schema === void 0) return void 0;
48580
+ const record = asRecord4(packed.schema);
48581
+ if (!record) return void 0;
48582
+ const types2 = Array.isArray(record.type) ? record.type : [record.type];
48583
+ if (!types2.every((entry) => typeof entry === "string" && SCALAR_SCHEMA_TYPES.has(entry))) return void 0;
48584
+ return packed.schema;
48585
+ }
48586
+ function packedArrayItemsSchema(packed) {
48587
+ if (packed.unsupported || packed.schema === void 0) return void 0;
48588
+ const record = asRecord4(packed.schema);
48589
+ if (!record) return void 0;
48590
+ const types2 = Array.isArray(record.type) ? record.type : [record.type];
48591
+ if (types2.length !== 1 || types2[0] !== "array") return void 0;
48592
+ if (record.prefixItems !== void 0 || Array.isArray(record.items)) return void 0;
48593
+ if (record.items === void 0) return {};
48594
+ const items = asRecord4(record.items);
48595
+ if (!items || typeof items.$ref === "string") return void 0;
48596
+ const itemTypes = Array.isArray(items.type) ? items.type : [items.type];
48597
+ if (!itemTypes.every((entry) => typeof entry === "string" && SCALAR_SCHEMA_TYPES.has(entry))) return void 0;
48598
+ return items;
48599
+ }
48600
+ var QUERY_ARRAY_DECODES = {
48601
+ "form:true": "multi",
48602
+ "form:false": "csv",
48603
+ "spaceDelimited:false": "ssv",
48604
+ "pipeDelimited:false": "pipes"
48605
+ };
48606
+ function collectParameterChecks(root, pathItem, operation, version, operationId, pathTemplate, warnings) {
48607
+ const securityKeys = collectSecurityApiKeys(root, operation);
48608
+ const checks = [];
48609
+ const seen = /* @__PURE__ */ new Set();
48610
+ const orderedParams = [...asArray2(operation.parameters), ...asArray2(pathItem.parameters)].map((rawParam) => {
48611
+ try {
48612
+ return resolveInternalRef(root, rawParam);
48613
+ } catch {
48614
+ return null;
48615
+ }
48616
+ }).filter((param) => Boolean(param));
48617
+ for (const param of orderedParams) {
48618
+ const location2 = String(param.in || "").toLowerCase();
48619
+ if (location2 !== "query" && location2 !== "header" && location2 !== "path" && location2 !== "cookie") continue;
48620
+ const name = String(param.name || "");
48621
+ if (!name || isIgnoredParameter(location2, name)) continue;
48622
+ const key = `${location2}:${name.toLowerCase()}`;
48623
+ if (seen.has(key)) continue;
48624
+ seen.add(key);
48625
+ if (securityKeys.has(key)) continue;
48626
+ const contentMedia = jsonContentParameterMedia(param);
48627
+ if (contentMedia !== void 0 && (location2 === "query" || location2 === "header")) {
48628
+ const packed2 = packSchema(root, contentMedia, version, "request");
48629
+ warnings.push(...packNoteWarnings(packed2, `parameter ${location2}:${name} of ${operationId}`));
48630
+ if (packed2.unsupported) {
48631
+ warnings.push(`CONTRACT_SCHEMA_NOT_COMPILED: parameter ${location2}:${name} schema on ${operationId} skipped (${packed2.unsupported})`);
48632
+ } else if (packed2.schema !== void 0) {
48633
+ const check2 = { in: location2, name, required: param.required === true, content: true, schema: packed2.schema };
48634
+ if (location2 === "query" && param.allowEmptyValue === true) check2.allowEmptyValue = true;
48635
+ checks.push(check2);
48636
+ }
48637
+ continue;
48638
+ }
48639
+ if (param.content !== void 0 || param.schema === void 0) continue;
48640
+ const defaultStyle = DEFAULT_PARAM_STYLES[location2];
48641
+ const style = typeof param.style === "string" ? param.style : defaultStyle;
48642
+ const defaultExplode = style === "form";
48643
+ const explode = typeof param.explode === "boolean" ? param.explode : defaultExplode;
48644
+ const defaultSerialization = style === defaultStyle && explode === defaultExplode;
48645
+ const packed = packSchema(root, param.schema, version);
48646
+ const noteWarnings = packNoteWarnings(packed, `parameter ${location2}:${name} of ${operationId}`);
48647
+ if (defaultSerialization) warnings.push(...noteWarnings);
48648
+ if (packed.unsupported) {
48649
+ if (defaultSerialization) warnings.push(`CONTRACT_SCHEMA_NOT_COMPILED: parameter ${location2}:${name} schema on ${operationId} skipped (${packed.unsupported})`);
48650
+ continue;
48651
+ }
48652
+ if (location2 === "path" && !pathTemplate.split("/").includes(`{${name}}`)) continue;
48653
+ const scalarSchema = packedScalarSchema(packed);
48654
+ if (scalarSchema !== void 0) {
48655
+ if (!defaultSerialization) continue;
48656
+ const check2 = { in: location2, name, required: param.required === true, schema: scalarSchema };
48657
+ if (location2 === "query" && param.allowEmptyValue === true) check2.allowEmptyValue = true;
48658
+ checks.push(check2);
48659
+ continue;
48660
+ }
48661
+ if (location2 !== "query" && location2 !== "header") continue;
48662
+ const items = packedArrayItemsSchema(packed);
48663
+ if (items === void 0) continue;
48664
+ const decode = location2 === "query" ? QUERY_ARRAY_DECODES[`${style}:${explode}`] : style === "simple" && !explode ? "csv" : void 0;
48665
+ if (!decode) continue;
48666
+ if (!defaultSerialization) warnings.push(...noteWarnings);
48667
+ const check = { in: location2, name, required: param.required === true, schema: packed.schema, decode, items };
48668
+ if (location2 === "query" && param.allowEmptyValue === true) check.allowEmptyValue = true;
48669
+ checks.push(check);
48670
+ }
48671
+ return checks.length > 0 ? checks : void 0;
48672
+ }
48673
+ function packNoteWarnings(packed, context) {
48674
+ return (packed.notes ?? []).map(
48675
+ (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`
48676
+ );
48677
+ }
48678
+ function collectDeclaredQueryParameters(root, pathItem, operation) {
48679
+ const names = /* @__PURE__ */ new Set();
48680
+ for (const param of resolvedParameters(root, pathItem, operation)) {
48681
+ if (String(param.in || "").toLowerCase() !== "query") continue;
48682
+ const name = String(param.name || "");
48683
+ if (name) names.add(name.toLowerCase());
48684
+ }
48685
+ for (const key of collectSecurityApiKeys(root, operation)) {
48686
+ if (key.startsWith("query:")) names.add(key.slice("query:".length));
48687
+ }
48688
+ return [...names];
48689
+ }
48258
48690
  function collectParameters(root, pathItem, operation) {
48259
48691
  const securityKeys = collectSecurityApiKeys(root, operation);
48260
48692
  const requirements = [];
@@ -48264,9 +48696,9 @@ function collectParameters(root, pathItem, operation) {
48264
48696
  const param = resolveInternalRef(root, rawParam);
48265
48697
  if (!param) continue;
48266
48698
  const location2 = String(param.in || "").toLowerCase();
48267
- if (!["path", "query", "header"].includes(location2)) continue;
48699
+ if (!["path", "query", "header", "cookie"].includes(location2)) continue;
48268
48700
  const name = String(param.name || "");
48269
- if (!name || param.required !== true) continue;
48701
+ if (!name || param.required !== true || isIgnoredParameter(location2, name)) continue;
48270
48702
  const key = `${location2}:${name.toLowerCase()}`;
48271
48703
  if (seen.has(key)) continue;
48272
48704
  seen.add(key);
@@ -48278,36 +48710,263 @@ function collectParameters(root, pathItem, operation) {
48278
48710
  }
48279
48711
  return requirements;
48280
48712
  }
48281
- function collectRequestBody(root, operation) {
48713
+ var BODY_FIELD_RULE_TYPES = /* @__PURE__ */ new Set(["application/x-www-form-urlencoded", "multipart/form-data"]);
48714
+ function isJsonBaseType(base) {
48715
+ return base === "application/json" || /\+json$/.test(base);
48716
+ }
48717
+ function mergeObjectSchema(root, rawSchema, depth) {
48718
+ if (depth > 10) return null;
48719
+ let schema2;
48720
+ try {
48721
+ schema2 = resolveInternalRef(root, rawSchema);
48722
+ } catch {
48723
+ return null;
48724
+ }
48725
+ if (!schema2) return null;
48726
+ const merged = {
48727
+ required: asArray2(schema2.required).map((entry) => String(entry)).filter(Boolean),
48728
+ properties: { ...asRecord4(schema2.properties) ?? {} }
48729
+ };
48730
+ for (const member of asArray2(schema2.allOf)) {
48731
+ const child = mergeObjectSchema(root, member, depth + 1);
48732
+ if (!child) continue;
48733
+ merged.required.push(...child.required);
48734
+ merged.properties = { ...merged.properties, ...child.properties };
48735
+ }
48736
+ merged.required = [...new Set(merged.required)];
48737
+ return merged;
48738
+ }
48739
+ function propertyIsReadOnly(root, properties, name) {
48740
+ try {
48741
+ return resolveInternalRef(root, properties[name])?.readOnly === true;
48742
+ } catch {
48743
+ return false;
48744
+ }
48745
+ }
48746
+ function propertyIsBinary(root, properties, name) {
48747
+ let schema2;
48748
+ try {
48749
+ schema2 = resolveInternalRef(root, properties[name]);
48750
+ } catch {
48751
+ return false;
48752
+ }
48753
+ if (!schema2) return false;
48754
+ if (schema2.format === "binary") return true;
48755
+ if (typeof schema2.contentMediaType !== "string" || typeof schema2.contentEncoding === "string") return false;
48756
+ const media = schema2.contentMediaType.toLowerCase().split(";")[0]?.trim() ?? "";
48757
+ if (!media) return false;
48758
+ return media !== "application/json" && !media.endsWith("+json") && !media.startsWith("text/");
48759
+ }
48760
+ function fieldEncodings(root, base, mediaObject, properties) {
48761
+ const declared = asRecord4(mediaObject?.encoding);
48762
+ const encodings = {};
48763
+ for (const name of Object.keys(properties)) {
48764
+ if (base === "multipart/form-data" && propertyIsBinary(root, properties, name)) {
48765
+ encodings[name] = { ...encodings[name], binary: true };
48766
+ }
48767
+ }
48768
+ if (declared) {
48769
+ for (const [name, rawEncoding] of Object.entries(declared)) {
48770
+ const encoding = asRecord4(rawEncoding);
48771
+ if (!encoding) continue;
48772
+ const entry = { ...encodings[name] };
48773
+ if (typeof encoding.contentType === "string" && encoding.contentType.trim()) {
48774
+ entry.contentType = encoding.contentType.toLowerCase();
48775
+ }
48776
+ if (base === "multipart/form-data" && asRecord4(encoding.headers)) {
48777
+ entry.hasHeaders = true;
48778
+ }
48779
+ if (base === "application/x-www-form-urlencoded") {
48780
+ const style = typeof encoding.style === "string" ? encoding.style : "form";
48781
+ const explode = typeof encoding.explode === "boolean" ? encoding.explode : style === "form";
48782
+ if (style !== "form" || explode !== (style === "form") || encoding.allowReserved === true) {
48783
+ entry.nonDefaultSerialization = true;
48784
+ }
48785
+ }
48786
+ if (Object.keys(entry).length > 0) encodings[name] = entry;
48787
+ }
48788
+ }
48789
+ return Object.keys(encodings).length > 0 ? encodings : void 0;
48790
+ }
48791
+ function formFieldSchemas(root, version, properties, context, warnings) {
48792
+ const schemas = {};
48793
+ for (const name of Object.keys(properties)) {
48794
+ const packed = packSchema(root, properties[name], version, "request");
48795
+ warnings.push(...packNoteWarnings(packed, `field ${name} of ${context}`));
48796
+ if (packed.unsupported) {
48797
+ warnings.push(`CONTRACT_SCHEMA_NOT_COMPILED: field ${name} of ${context} skipped (${packed.unsupported})`);
48798
+ continue;
48799
+ }
48800
+ if (packed.schema === void 0) continue;
48801
+ const schema2 = packedScalarSchema(packed);
48802
+ if (schema2 !== void 0) schemas[name] = schema2;
48803
+ }
48804
+ return Object.keys(schemas).length > 0 ? schemas : void 0;
48805
+ }
48806
+ function requestBodyFieldRules(root, content, version, operationId, warnings) {
48807
+ const rules = {};
48808
+ for (const [contentType, mediaObject] of Object.entries(content)) {
48809
+ const base = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
48810
+ if (!isJsonBaseType(base) && !BODY_FIELD_RULE_TYPES.has(base)) continue;
48811
+ const mediaRecord = asRecord4(mediaObject);
48812
+ const merged = mergeObjectSchema(root, mediaRecord?.schema, 0);
48813
+ if (!merged) continue;
48814
+ const readOnly = Object.keys(merged.properties).filter((name) => propertyIsReadOnly(root, merged.properties, name));
48815
+ const required = merged.required.filter((name) => !propertyIsReadOnly(root, merged.properties, name));
48816
+ const formRules = BODY_FIELD_RULE_TYPES.has(base);
48817
+ const encodings = formRules ? fieldEncodings(root, base, mediaRecord, merged.properties) : void 0;
48818
+ const fieldSchemas = formRules ? formFieldSchemas(root, version, merged.properties, `request body ${contentType} of ${operationId}`, warnings) : void 0;
48819
+ if (required.length > 0 || readOnly.length > 0 || encodings || fieldSchemas) {
48820
+ const rule = { required, readOnly };
48821
+ if (encodings) rule.encodings = encodings;
48822
+ if (fieldSchemas) rule.fieldSchemas = fieldSchemas;
48823
+ rules[base] = rule;
48824
+ }
48825
+ }
48826
+ return Object.keys(rules).length > 0 ? rules : void 0;
48827
+ }
48828
+ function requestBodyJsonSchemas(root, content, version, operationId, warnings) {
48829
+ const schemas = {};
48830
+ const exampleWarnings = /* @__PURE__ */ new Set();
48831
+ for (const [contentType, mediaObject] of Object.entries(content)) {
48832
+ const base = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
48833
+ const mediaRecord = asRecord4(mediaObject);
48834
+ const schema2 = mediaRecord?.schema;
48835
+ if (!isJsonBaseType(base)) {
48836
+ if (schema2 !== void 0 && !BODY_FIELD_RULE_TYPES.has(base)) {
48837
+ warnings.push(`CONTRACT_NONJSON_SCHEMA_NOT_VALIDATED: request body schema for ${contentType} on ${operationId} is not validated at runtime`);
48838
+ }
48839
+ continue;
48840
+ }
48841
+ if (schema2 === void 0) continue;
48842
+ const packed = packSchema(root, schema2, version, "request");
48843
+ warnings.push(...packNoteWarnings(packed, `request body ${contentType} of ${operationId}`));
48844
+ if (mediaRecord) validateExamples(root, mediaRecord, packed, contentType, operationId, exampleWarnings);
48845
+ if (packed.unsupported) {
48846
+ warnings.push(`CONTRACT_REQUEST_SCHEMA_NOT_VALIDATED: request body schema for ${contentType} on ${operationId} is not validated (${packed.unsupported})`);
48847
+ continue;
48848
+ }
48849
+ if (packed.schema !== void 0) schemas[base] = packed.schema;
48850
+ }
48851
+ warnings.push(...exampleWarnings);
48852
+ return Object.keys(schemas).length > 0 ? schemas : void 0;
48853
+ }
48854
+ function collectRequestBody(root, operation, version, operationId, warnings) {
48282
48855
  const body = resolveInternalRef(root, operation.requestBody);
48283
- if (!body || body.required !== true) return void 0;
48856
+ if (!body) return void 0;
48284
48857
  const content = asRecord4(body.content);
48858
+ const fieldRules = content ? requestBodyFieldRules(root, content, version, operationId, warnings) : void 0;
48859
+ if (fieldRules) {
48860
+ for (const [base, rule] of Object.entries(fieldRules)) {
48861
+ for (const [field, encoding] of Object.entries(rule.encodings ?? {})) {
48862
+ if (encoding.nonDefaultSerialization) {
48863
+ warnings.push(
48864
+ `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`
48865
+ );
48866
+ }
48867
+ if (encoding.hasHeaders) {
48868
+ warnings.push(
48869
+ `CONTRACT_ENCODING_HEADERS_NOT_VALIDATED: ${base} request body field ${field} on ${operationId} declares per-part headers that generated formdata entries cannot carry`
48870
+ );
48871
+ }
48872
+ }
48873
+ }
48874
+ }
48285
48875
  return {
48286
- required: true,
48287
- contentTypes: content ? Object.keys(content) : []
48876
+ required: body.required === true,
48877
+ contentTypes: content ? Object.keys(content) : [],
48878
+ fieldRules,
48879
+ jsonSchemas: content ? requestBodyJsonSchemas(root, content, version, operationId, warnings) : void 0
48288
48880
  };
48289
48881
  }
48290
- function responseContent(root, version, response) {
48882
+ function exampleCandidates(root, mediaObject) {
48883
+ const candidates = [];
48884
+ if ("example" in mediaObject) candidates.push({ label: "example", value: mediaObject.example });
48885
+ const examples = asRecord4(mediaObject.examples);
48886
+ if (examples) {
48887
+ for (const [name, rawExample] of Object.entries(examples)) {
48888
+ let example;
48889
+ try {
48890
+ example = resolveInternalRef(root, rawExample);
48891
+ } catch {
48892
+ continue;
48893
+ }
48894
+ if (example && "value" in example) candidates.push({ label: `examples.${name}`, value: example.value });
48895
+ }
48896
+ }
48897
+ return candidates;
48898
+ }
48899
+ function validateExamples(root, mediaObject, packed, contentType, context, warnings) {
48900
+ if (packed.schema === void 0 || packed.unsupported) return;
48901
+ const candidates = exampleCandidates(root, mediaObject);
48902
+ if (candidates.length === 0) return;
48903
+ const validate2 = compileSchemaValidator(packed.schema);
48904
+ if (!validate2) return;
48905
+ for (const candidate of candidates) {
48906
+ if (!validate2(candidate.value)) {
48907
+ warnings.add(`CONTRACT_EXAMPLE_SCHEMA_MISMATCH: ${candidate.label} for ${contentType} on ${context} does not match its schema`);
48908
+ }
48909
+ }
48910
+ }
48911
+ function responseContent(root, version, response, context, warnings) {
48291
48912
  const content = asRecord4(response.content);
48292
48913
  if (!content) return {};
48293
48914
  const media = {};
48294
48915
  for (const [contentType, mediaObject] of Object.entries(content)) {
48295
- const schema2 = asRecord4(mediaObject)?.schema;
48296
- media[contentType] = schema2 === void 0 ? {} : packSchema(root, schema2, version);
48916
+ const mediaRecord = asRecord4(mediaObject);
48917
+ const schema2 = mediaRecord?.schema;
48918
+ let packed = schema2 === void 0 ? {} : packSchema(root, schema2, version);
48919
+ for (const warning of packNoteWarnings(packed, `response ${contentType} of ${context}`)) warnings.add(warning);
48920
+ if (isSchemaGraphOverflow(packed)) {
48921
+ warnings.add(`CONTRACT_SCHEMA_NOT_COMPILED: response schema for ${contentType} on ${context} skipped (${packed.unsupported})`);
48922
+ packed = {};
48923
+ }
48924
+ const base = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
48925
+ if (mediaRecord && isJsonBaseType(base)) validateExamples(root, mediaRecord, packed, contentType, context, warnings);
48926
+ media[contentType] = packed;
48297
48927
  }
48298
48928
  return media;
48299
48929
  }
48300
- function responseHeaders(root, version, response) {
48930
+ function responseHeaders(root, version, response, context, warnings) {
48301
48931
  const headers = asRecord4(response.headers);
48302
48932
  if (!headers) return [];
48303
- return Object.entries(headers).map(([name, rawHeader]) => {
48933
+ const entries = [];
48934
+ for (const [name, rawHeader] of Object.entries(headers)) {
48935
+ if (name.toLowerCase() === "content-type") continue;
48304
48936
  const header = resolveInternalRef(root, rawHeader);
48305
- if (!header) return { name, required: true, unsupported: "Unresolved response header" };
48937
+ if (!header) {
48938
+ entries.push({ name, required: true, unsupported: "Unresolved response header" });
48939
+ continue;
48940
+ }
48306
48941
  const required = header.required === true;
48307
- if (header.content) return { name, required, unsupported: "OpenAPI response header content is unsupported" };
48308
- if (!header.schema) return { name, required };
48309
- return { name, required, ...packSchema(root, header.schema, version) };
48310
- });
48942
+ if (header.content) {
48943
+ entries.push({ name, required, unsupported: "OpenAPI response header content is unsupported" });
48944
+ continue;
48945
+ }
48946
+ if (!header.schema) {
48947
+ entries.push({ name, required });
48948
+ continue;
48949
+ }
48950
+ const packed = packSchema(root, header.schema, version);
48951
+ for (const warning of packNoteWarnings(packed, `response header ${name} of ${context}`)) warnings.add(warning);
48952
+ if (isSchemaGraphOverflow(packed)) {
48953
+ warnings.add(`CONTRACT_SCHEMA_NOT_COMPILED: response header ${name} schema on ${context} skipped (${packed.unsupported})`);
48954
+ entries.push({ name, required });
48955
+ continue;
48956
+ }
48957
+ if (!packed.unsupported && packed.schema !== void 0 && packedScalarSchema(packed) === void 0) {
48958
+ const items = packedArrayItemsSchema(packed);
48959
+ if (items !== void 0) {
48960
+ entries.push({ name, required, schema: packed.schema, items });
48961
+ continue;
48962
+ }
48963
+ warnings.add(`CONTRACT_HEADER_SCHEMA_NOT_VALIDATED: response header ${name} on ${context} declares a non-scalar schema and its value is not validated`);
48964
+ entries.push({ name, required });
48965
+ continue;
48966
+ }
48967
+ entries.push({ name, required, ...packed });
48968
+ }
48969
+ return entries;
48311
48970
  }
48312
48971
  function buildContractIndex(root) {
48313
48972
  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)");
@@ -48332,11 +48991,23 @@ function buildContractIndex(root) {
48332
48991
  throw new Error(`CONTRACT_OPERATION_NO_RESPONSES: ${lowerMethod.toUpperCase()} ${path6} must define at least one response`);
48333
48992
  }
48334
48993
  const contractResponses = {};
48994
+ const responseWarnings = /* @__PURE__ */ new Set();
48335
48995
  for (const [status, rawResponse] of Object.entries(responses)) {
48336
48996
  const response = resolveInternalRef(root, rawResponse);
48337
48997
  if (!response) continue;
48338
- const content = responseContent(root, version, response);
48339
- const headers = responseHeaders(root, version, response);
48998
+ if (asRecord4(response.links)) {
48999
+ responseWarnings.add(`CONTRACT_LINKS_NOT_VALIDATED: response links are not validated for ${lowerMethod.toUpperCase()} ${path6}`);
49000
+ }
49001
+ const responseContext = `${lowerMethod.toUpperCase()} ${path6} status ${status}`;
49002
+ const content = responseContent(root, version, response, responseContext, responseWarnings);
49003
+ for (const [contentType, media] of Object.entries(content)) {
49004
+ const base = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
49005
+ const schemaType = asRecord4(media.schema)?.type;
49006
+ if (!isJsonBaseType(base) && media.schema !== void 0 && !media.unsupported && schemaType !== "string") {
49007
+ responseWarnings.add(`CONTRACT_NONJSON_SCHEMA_NOT_VALIDATED: response schema for ${contentType} on ${responseContext} is not validated at runtime`);
49008
+ }
49009
+ }
49010
+ const headers = responseHeaders(root, version, response, responseContext, responseWarnings);
48340
49011
  contractResponses[normalizeResponseKey(status)] = {
48341
49012
  content,
48342
49013
  hasBody: Object.keys(content).length > 0,
@@ -48347,21 +49018,45 @@ function buildContractIndex(root) {
48347
49018
  path6,
48348
49019
  ...operationServers(root, pathItem, operation).map((server) => joinPaths(serverPathPrefix(server), path6))
48349
49020
  ].map(normalizePath))];
49021
+ const operationId = `${lowerMethod.toUpperCase()} ${path6}`;
48350
49022
  const opWarnings = [];
49023
+ opWarnings.push(...responseWarnings);
48351
49024
  opWarnings.push(...collectSecuritySchemeWarnings(root, operation));
49025
+ const parameterChecks = collectParameterChecks(root, pathItem, operation, version, operationId, path6, opWarnings);
49026
+ const checkedKeys = new Set((parameterChecks ?? []).map((check) => `${check.in}:${check.name.toLowerCase()}`));
49027
+ const decodedKeys = new Set((parameterChecks ?? []).filter((check) => check.decode).map((check) => `${check.in}:${check.name.toLowerCase()}`));
49028
+ opWarnings.push(...collectSerializationWarnings(root, pathItem, operation, decodedKeys));
49029
+ if (operation.deprecated === true) {
49030
+ opWarnings.push(`CONTRACT_OPERATION_DEPRECATED: ${lowerMethod.toUpperCase()} ${path6} is marked deprecated in the OpenAPI document`);
49031
+ }
48352
49032
  const requiredParameters = collectParameters(root, pathItem, operation);
48353
49033
  for (const parameter of requiredParameters.filter((entry) => entry.securityDerived)) {
48354
49034
  opWarnings.push(`CONTRACT_SECURITY_NOT_VALIDATED: security parameter ${parameter.in}:${parameter.name} is not statically required in generated requests`);
48355
49035
  }
49036
+ for (const parameter of requiredParameters.filter((entry) => entry.in === "cookie" && !entry.securityDerived)) {
49037
+ 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`);
49038
+ }
49039
+ const pathParamWarnings = /* @__PURE__ */ new Set();
49040
+ for (const param of resolvedParameters(root, pathItem, operation)) {
49041
+ if (String(param.in || "").toLowerCase() !== "path") continue;
49042
+ const name = String(param.name || "");
49043
+ if (name && !checkedKeys.has(`path:${name.toLowerCase()}`)) {
49044
+ pathParamWarnings.add(`CONTRACT_PATH_PARAM_NOT_VALIDATED: path parameter ${name} value is not validated at runtime`);
49045
+ }
49046
+ }
49047
+ opWarnings.push(...pathParamWarnings);
48356
49048
  operations.push({
48357
- id: `${lowerMethod.toUpperCase()} ${path6}`,
49049
+ id: operationId,
48358
49050
  method: lowerMethod.toUpperCase(),
48359
49051
  path: path6,
48360
49052
  pointer: `/paths/${path6.replace(/~/g, "~0").replace(/\//g, "~1")}/${lowerMethod}`,
48361
49053
  candidates,
48362
49054
  responses: contractResponses,
48363
49055
  requiredParameters,
48364
- requestBody: collectRequestBody(root, operation),
49056
+ declaredQueryParameters: collectDeclaredQueryParameters(root, pathItem, operation),
49057
+ parameterChecks,
49058
+ requestBody: collectRequestBody(root, operation, version, operationId, opWarnings),
49059
+ security: collectSecurityRuntimeChecks(root, operation),
48365
49060
  warnings: opWarnings
48366
49061
  });
48367
49062
  }
@@ -48382,35 +49077,6 @@ function buildContractIndex(root) {
48382
49077
  return { operations, version, warnings };
48383
49078
  }
48384
49079
 
48385
- // src/lib/spec/schema-validator-code.ts
48386
- var import_schemasafe = __toESM(require_src(), 1);
48387
- function compileSchemaValidatorCode(schema2) {
48388
- try {
48389
- const validate2 = (0, import_schemasafe.validator)(schema2, {
48390
- includeErrors: true,
48391
- allErrors: true,
48392
- contentValidation: false,
48393
- formatAssertion: true,
48394
- isJSON: true,
48395
- mode: "default",
48396
- removeAdditional: false,
48397
- requireSchema: true,
48398
- requireStringValidation: false,
48399
- useDefaults: false
48400
- });
48401
- const source = validate2.toModule();
48402
- if (/\beval\s*\(/.test(source) || /new\s+Function\b/.test(source)) {
48403
- throw new Error("schemasafe generated forbidden dynamic code");
48404
- }
48405
- return source;
48406
- } catch (error) {
48407
- throw new Error(
48408
- `CONTRACT_SCHEMA_COMPILE_FAILED: ${error instanceof Error ? error.message : String(error)}`,
48409
- { cause: error }
48410
- );
48411
- }
48412
- }
48413
-
48414
49080
  // src/lib/spec/collection-contracts.ts
48415
49081
  var CONTRACT_SIZE_LIMITS = {
48416
49082
  warnTestScriptBytes: 256e3,
@@ -48499,29 +49165,51 @@ function matchOperation(index, request) {
48499
49165
  function assignValidator(lines, target, source) {
48500
49166
  lines.push(`${target} = ${source};`);
48501
49167
  }
48502
- function buildValidatorAssignments(operation) {
49168
+ function tryCompile(target, schema2, lines, warnings, context) {
49169
+ try {
49170
+ assignValidator(lines, target, compileSchemaValidatorCode(schema2));
49171
+ } catch (error) {
49172
+ lines.push(`${target} = { skip: true };`);
49173
+ warnings.push(`CONTRACT_SCHEMA_NOT_COMPILED: ${context} could not be compiled into a runtime validator (${error instanceof Error ? error.message.slice(0, 160) : String(error)})`);
49174
+ }
49175
+ }
49176
+ function buildValidatorAssignments(operation, warnings) {
48503
49177
  const lines = ["var validators = {};"];
49178
+ const parameterChecks = operation.parameterChecks ?? [];
49179
+ if (parameterChecks.length > 0) {
49180
+ lines.push("var paramValidators = {};");
49181
+ for (const check of parameterChecks) {
49182
+ tryCompile(`paramValidators[${JSON.stringify(`${check.in}:${check.name.toLowerCase()}`)}]`, check.schema, lines, warnings, `parameter ${check.in}:${check.name} schema on ${operation.id}`);
49183
+ }
49184
+ }
49185
+ const bodySchemas = Object.entries(operation.requestBody?.jsonSchemas ?? {});
49186
+ if (bodySchemas.length > 0) {
49187
+ lines.push("var requestBodyValidators = {};");
49188
+ for (const [base, schema2] of bodySchemas) {
49189
+ tryCompile(`requestBodyValidators[${JSON.stringify(base)}]`, schema2, lines, warnings, `request body schema for ${base} on ${operation.id}`);
49190
+ }
49191
+ }
48504
49192
  for (const [status, response] of Object.entries(operation.responses)) {
48505
49193
  lines.push(`validators[${JSON.stringify(status)}] = validators[${JSON.stringify(status)}] || {};`);
48506
49194
  for (const [mediaType, media] of Object.entries(response.content)) {
48507
49195
  if (media.schema !== void 0 && !media.unsupported) {
48508
- assignValidator(lines, `validators[${JSON.stringify(status)}][${JSON.stringify(mediaType)}]`, compileSchemaValidatorCode(media.schema));
49196
+ tryCompile(`validators[${JSON.stringify(status)}][${JSON.stringify(mediaType)}]`, media.schema, lines, warnings, `response schema for ${mediaType} on ${operation.id} status ${status}`);
48509
49197
  }
48510
49198
  }
48511
49199
  for (const header of response.headers) {
48512
49200
  if (header.schema !== void 0 && !header.unsupported) {
48513
49201
  lines.push(`validators[${JSON.stringify(status)}].__headers = validators[${JSON.stringify(status)}].__headers || {};`);
48514
- assignValidator(lines, `validators[${JSON.stringify(status)}].__headers[${JSON.stringify(header.name.toLowerCase())}]`, compileSchemaValidatorCode(header.schema));
49202
+ 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}`);
48515
49203
  }
48516
49204
  }
48517
49205
  }
48518
49206
  return lines;
48519
49207
  }
48520
- function createContractScript(operation) {
48521
- const contract = { method: operation.method, path: operation.path, responses: operation.responses };
49208
+ function createContractScript(operation, warnings = []) {
49209
+ const contract = { method: operation.method, path: operation.path, responses: operation.responses, security: operation.security, parameters: operation.parameterChecks };
48522
49210
  return [
48523
49211
  `var contract = JSON.parse(${JSON.stringify(JSON.stringify(contract))});`,
48524
- ...buildValidatorAssignments(operation),
49212
+ ...buildValidatorAssignments(operation, warnings),
48525
49213
  "function selectedResponseContract() {",
48526
49214
  " var status = String(pm.response.code);",
48527
49215
  " if (contract.responses[status]) return { key: status, value: contract.responses[status] };",
@@ -48535,6 +49223,15 @@ function createContractScript(operation) {
48535
49223
  'function mediaBase(value) { return String(value || "").toLowerCase().split(";")[0].trim(); }',
48536
49224
  'function mediaParts(value) { var base = mediaBase(value); var parts = base.split("/"); return { raw: base, type: parts[0] || "", subtype: parts[1] || "" }; }',
48537
49225
  'function isJsonSubtype(subtype) { return subtype === "json" || /\\+json$/.test(subtype); }',
49226
+ "function coerceBySchema(value, schema) {",
49227
+ " var type = schema && schema.type;",
49228
+ " var types = Array.isArray(type) ? type : [type];",
49229
+ ' if ((types.indexOf("integer") !== -1 || types.indexOf("number") !== -1) && /^-?[0-9]+([.][0-9]+)?([eE][+-]?[0-9]+)?$/.test(String(value).trim())) return Number(value);',
49230
+ ' if (types.indexOf("boolean") !== -1 && (value === "true" || value === "false")) return value === "true";',
49231
+ " return value;",
49232
+ "}",
49233
+ '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; }',
49234
+ "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; }",
48538
49235
  "function mediaScore(expected, actual) {",
48539
49236
  " var e = mediaParts(expected); var a = mediaParts(actual);",
48540
49237
  " if (!e.raw || !a.raw) return 0;",
@@ -48565,7 +49262,11 @@ function createContractScript(operation) {
48565
49262
  " if (!actual) return;",
48566
49263
  ' if (header.unsupported) pm.expect.fail("OpenAPI response header unsupported for " + contract.method + " " + contract.path + ": " + header.unsupported);',
48567
49264
  " var headerValidator = validators[selected.key] && validators[selected.key].__headers && validators[selected.key].__headers[String(header.name).toLowerCase()];",
48568
- ' if (headerValidator && !headerValidator(actual)) pm.expect.fail("OpenAPI response header validation failed for " + header.name + ": " + JSON.stringify(headerValidator.errors || []));',
49265
+ " if (!headerValidator || headerValidator.skip) return;",
49266
+ " var expected;",
49267
+ ' if (header.items) { var joined = String(actual).trim(); expected = joined === "" ? [] : joined.split(",").map(function (entry) { return coerceBySchema(entry.trim(), header.items); }); }',
49268
+ " else expected = coerceBySchema(actual, header.schema);",
49269
+ ' if (!headerValidator(expected)) pm.expect.fail("OpenAPI response header validation failed for " + header.name + ": " + JSON.stringify(headerValidator.errors || []));',
48569
49270
  " });",
48570
49271
  "});",
48571
49272
  "pm.test('Response body matches OpenAPI body contract', function () {",
@@ -48593,11 +49294,122 @@ function createContractScript(operation) {
48593
49294
  ' if (media.media.unsupported) pm.expect.fail("OpenAPI schema unsupported for " + contract.method + " " + contract.path + " status " + pm.response.code + ": " + media.media.unsupported);',
48594
49295
  " if (!media.media.schema) { return; }",
48595
49296
  " var validate = validators[selected.key] && validators[selected.key][media.expected];",
49297
+ " if (validate && validate.skip) return;",
48596
49298
  ' if (!validate) pm.expect.fail("OpenAPI schema validator was not generated for " + media.expected);',
48597
49299
  ' var actual = mediaParts(pm.response.headers.get("Content-Type") || "");',
48598
49300
  " var value = isJsonSubtype(actual.subtype) ? pm.response.json() : responseText();",
48599
- ' 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);',
49301
+ // Non-JSON object-schema bodies skip schema validation instead of
49302
+ // failing; the index emits CONTRACT_NONJSON_SCHEMA_NOT_VALIDATED so the
49303
+ // skip is visible at instrumentation time.
49304
+ ' if (!isJsonSubtype(actual.subtype) && media.media.schema && media.media.schema.type !== "string") { return; }',
48600
49305
  ' if (!validate(value)) pm.expect.fail("OpenAPI schema validation failed for " + contract.method + " " + contract.path + " status " + pm.response.code + ": " + JSON.stringify(validate.errors || []));',
49306
+ "});",
49307
+ ...operation.security ? [
49308
+ "pm.test('Request carries credentials required by OpenAPI security', function () {",
49309
+ " function satisfied(check) {",
49310
+ " if (!check.checkable) return true;",
49311
+ ' if (check.in === "query") return hasQueryParam(check.name);',
49312
+ ' 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); }); }',
49313
+ ' if (check.prefix) { return requestHeader("Authorization").toLowerCase().indexOf(check.prefix.toLowerCase()) === 0; }',
49314
+ ' if (check.kind === "oauth2" || check.kind === "openIdConnect") { return Boolean(requestHeader("Authorization")) || hasQueryParam("access_token"); }',
49315
+ " return Boolean(requestHeader(check.name));",
49316
+ " }",
49317
+ " var alternatives = contract.security || [];",
49318
+ " var ok = alternatives.some(function (alternative) { return alternative.every(function (check) { return satisfied(check); }); });",
49319
+ ' 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(" | "));',
49320
+ "});"
49321
+ ] : [],
49322
+ ...operation.parameterChecks && operation.parameterChecks.length > 0 ? [
49323
+ "pm.test('Request parameters match OpenAPI schemas', function () {",
49324
+ ' 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; }',
49325
+ ' 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; }',
49326
+ ' 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; }',
49327
+ ' 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; }',
49328
+ ' 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; }); }',
49329
+ // Server path prefixes sit ahead of the template segments, so the
49330
+ // template aligns against the trailing request segments.
49331
+ ' 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; }',
49332
+ ' function isPlaceholder(value) { var text = String(value).trim(); return /^<[^<>]*>$/.test(text) || text.indexOf("{{") !== -1; }',
49333
+ ' function splitDelimited(value, decode) { if (decode === "csv") return value.split(","); if (decode === "ssv") return value.split(/%20| /); return value.split(/%7C|\\|/i); }',
49334
+ " function decodeComponent(value) { try { return decodeURIComponent(value); } catch (ignored) { return value; } }",
49335
+ " (contract.parameters || []).forEach(function (param) {",
49336
+ ' var key = param.in + ":" + String(param.name).toLowerCase();',
49337
+ " var validate = paramValidators[key];",
49338
+ " if (!validate || validate.skip) return;",
49339
+ " var value;",
49340
+ ' if (param.decode === "multi") {',
49341
+ " var entries = queryValues(String(param.name).toLowerCase());",
49342
+ ' 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; }',
49343
+ ' if (param.allowEmptyValue && entries.length === 1 && entries[0] === "") return;',
49344
+ " if (entries.some(isPlaceholder)) return;",
49345
+ " value = entries.map(function (entry) { return coerceBySchema(decodeComponent(entry), param.items); });",
49346
+ " } else if (param.decode) {",
49347
+ ' var joined = param.in === "query" ? queryValue(String(param.name).toLowerCase()) : headerValue(param.name);',
49348
+ ' if (joined === undefined) { if (param.required) pm.expect.fail("Required parameter " + param.in + ":" + param.name + " was not sent for " + contract.method + " " + contract.path); return; }',
49349
+ ' if (joined === "" && param.allowEmptyValue) return;',
49350
+ " if (isPlaceholder(joined)) return;",
49351
+ ' var parts = joined === "" ? [] : splitDelimited(String(joined), param.decode);',
49352
+ // HTTP header lists allow optional whitespace after the comma, so
49353
+ // header-sourced items are trimmed; query values stay literal.
49354
+ ' if (param.in === "header") parts = parts.map(function (entry) { return entry.trim(); });',
49355
+ " if (parts.some(isPlaceholder)) return;",
49356
+ // Query values arrive percent-encoded while the server validates the
49357
+ // decoded form; header values are never percent-encoded.
49358
+ ' value = parts.map(function (entry) { return coerceBySchema(param.in === "query" ? decodeComponent(entry) : entry, param.items); });',
49359
+ ' } else if (param.in === "path") {',
49360
+ " value = pathParamValue(param.name);",
49361
+ " if (value === undefined) return;",
49362
+ ' if (isPlaceholder(value) || value.charAt(0) === ":" || value.charAt(0) === "{") return;',
49363
+ " value = coerceBySchema(value, param.schema);",
49364
+ ' } else if (param.in === "cookie") {',
49365
+ " value = cookieValue(param.name);",
49366
+ ' if (value === undefined) { if (param.required) pm.expect.fail("Required cookie parameter " + param.name + " was not sent for " + contract.method + " " + contract.path); return; }',
49367
+ " if (isPlaceholder(value)) return;",
49368
+ " value = coerceBySchema(value, param.schema);",
49369
+ " } else {",
49370
+ ' value = param.in === "query" ? queryValue(String(param.name).toLowerCase()) : headerValue(param.name);',
49371
+ ' if (value === undefined) { if (param.required) pm.expect.fail("Required parameter " + param.in + ":" + param.name + " was not sent for " + contract.method + " " + contract.path); return; }',
49372
+ ' if (value === "" && param.allowEmptyValue) return;',
49373
+ " if (isPlaceholder(value)) return;",
49374
+ " if (param.content) {",
49375
+ " var parsed;",
49376
+ ' 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; }',
49377
+ ' if (!validate(parsed)) pm.expect.fail("Parameter " + param.in + ":" + param.name + " failed OpenAPI schema validation for " + contract.method + " " + contract.path + ": " + JSON.stringify(validate.errors || []));',
49378
+ " return;",
49379
+ " }",
49380
+ " value = coerceBySchema(value, param.schema);",
49381
+ " }",
49382
+ ' if (!validate(value)) pm.expect.fail("Parameter " + param.in + ":" + param.name + " failed OpenAPI schema validation for " + contract.method + " " + contract.path + ": " + JSON.stringify(validate.errors || []));',
49383
+ " });",
49384
+ "});"
49385
+ ] : [],
49386
+ ...operation.requestBody?.jsonSchemas && Object.keys(operation.requestBody.jsonSchemas).length > 0 ? [
49387
+ "pm.test('Request body matches OpenAPI request schema', function () {",
49388
+ " var body = pm.request.body;",
49389
+ ' var raw = body && body.mode === "raw" && typeof body.raw === "string" ? body.raw : "";',
49390
+ " if (!raw.trim()) return;",
49391
+ ' if (/"<[^"<>]*>"/.test(raw) || raw.indexOf("{{") !== -1) return;',
49392
+ ' var validate = requestBodyValidators[mediaBase(requestHeader("Content-Type"))];',
49393
+ " if (!validate || validate.skip) return;",
49394
+ " var parsed;",
49395
+ // Unquoted generator placeholders such as {"count": <long>} break
49396
+ // JSON.parse; a parse failure alongside an angle-bracket token is
49397
+ // treated as a placeholder body rather than drift.
49398
+ ' 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; }',
49399
+ ' if (!validate(parsed)) pm.expect.fail("Request body failed OpenAPI request schema validation for " + contract.method + " " + contract.path + ": " + JSON.stringify(validate.errors || []));',
49400
+ "});"
49401
+ ] : [],
49402
+ "pm.test('Content-Length is consistent with OpenAPI body expectations', function () {",
49403
+ ' var raw = pm.response.headers.get("Content-Length");',
49404
+ " if (raw === null || raw === undefined) return;",
49405
+ // Combined duplicate Content-Length values ("5, 5") fail the integer
49406
+ // syntax check on purpose: RFC 9110 tolerates identical repeats, but a
49407
+ // contract test surfacing them is strict by design.
49408
+ ' if (!/^[0-9]+$/.test(String(raw).trim())) pm.expect.fail("Content-Length header is not a non-negative integer: " + raw);',
49409
+ ' if (contract.method === "HEAD" || pm.response.code === 304) return;',
49410
+ ' if (pm.response.headers.get("Content-Encoding")) return;',
49411
+ " var mustBeEmpty = isBodyless() || (selected && Object.keys(selected.value.content || {}).length === 0);",
49412
+ ' 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);',
48601
49413
  "});"
48602
49414
  ];
48603
49415
  }
@@ -48690,14 +49502,160 @@ function assertStaticRequestShape(operation, request) {
48690
49502
  if (!contentType) {
48691
49503
  throw new Error(`CONTRACT_STATIC_REQUEST_CHECK_FAILED: ${operation.id} missing required request Content-Type`);
48692
49504
  }
48693
- const actual = contentType.toLowerCase().split(";")[0]?.trim();
48694
- const matches = operation.requestBody.contentTypes.some((expected) => expected.toLowerCase() === actual);
49505
+ const actual = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
49506
+ const matches = operation.requestBody.contentTypes.some((expected) => mediaTypeMatchesPattern(expected.toLowerCase(), actual));
48695
49507
  if (!matches) {
48696
49508
  throw new Error(
48697
49509
  `CONTRACT_STATIC_REQUEST_CHECK_FAILED: ${operation.id} request Content-Type ${contentType} does not match ${operation.requestBody.contentTypes.join(", ")}`
48698
49510
  );
48699
49511
  }
48700
49512
  }
49513
+ const warnings = collectStaticBodyWarnings(operation, request, contentType);
49514
+ if (operation.requestBody && !operation.requestBody.required && operation.requestBody.contentTypes.length > 0 && hasRequestBody(request) && contentType) {
49515
+ const actual = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
49516
+ const matches = operation.requestBody.contentTypes.some((expected) => mediaTypeMatchesPattern(expected.toLowerCase(), actual));
49517
+ if (!matches) {
49518
+ warnings.push(
49519
+ `CONTRACT_STATIC_REQUEST_CHECK_FAILED: ${operation.id} optional request body Content-Type ${contentType} does not match ${operation.requestBody.contentTypes.join(", ")}`
49520
+ );
49521
+ }
49522
+ }
49523
+ return warnings;
49524
+ }
49525
+ function requestBodyFieldNames(request, base) {
49526
+ const body = asRecord5(request.body);
49527
+ if (!body) return void 0;
49528
+ if (base === "application/json" || /\+json$/.test(base)) {
49529
+ if (body.mode !== "raw" || typeof body.raw !== "string") return void 0;
49530
+ let parsed;
49531
+ try {
49532
+ parsed = JSON.parse(body.raw);
49533
+ } catch {
49534
+ return void 0;
49535
+ }
49536
+ const record = asRecord5(parsed);
49537
+ return record ? Object.keys(record) : void 0;
49538
+ }
49539
+ const mode = base === "application/x-www-form-urlencoded" ? "urlencoded" : base === "multipart/form-data" ? "formdata" : "";
49540
+ if (!mode || !Array.isArray(body[mode])) return void 0;
49541
+ return body[mode].map((entry) => asRecord5(entry)).filter((entry) => Boolean(entry)).filter((entry) => entry.disabled !== true).map((entry) => String(entry.key || "")).filter(Boolean);
49542
+ }
49543
+ function requestBodyEntries(request, base) {
49544
+ const body = asRecord5(request.body);
49545
+ const mode = base === "application/x-www-form-urlencoded" ? "urlencoded" : base === "multipart/form-data" ? "formdata" : "";
49546
+ if (!body || !mode || !Array.isArray(body[mode])) return void 0;
49547
+ return body[mode].map((entry) => asRecord5(entry)).filter((entry) => Boolean(entry)).filter((entry) => entry.disabled !== true);
49548
+ }
49549
+ function mediaTypeMatchesPattern(pattern, actual) {
49550
+ return pattern.split(",").some((candidate) => {
49551
+ const entry = (candidate.split(";")[0] ?? "").trim();
49552
+ if (!entry) return false;
49553
+ if (entry === "*/*" || entry === actual) return true;
49554
+ if (entry.endsWith("/*")) return actual.startsWith(entry.slice(0, -1));
49555
+ return false;
49556
+ });
49557
+ }
49558
+ function isJsonEncodingContentType(declared) {
49559
+ return declared.split(",").some((candidate) => {
49560
+ const entry = (candidate.split(";")[0] ?? "").trim();
49561
+ return entry === "application/json" || entry.endsWith("+json");
49562
+ });
49563
+ }
49564
+ function isPlaceholderValue(value) {
49565
+ return /^<[^>]*>$/.test(value.trim()) || value.includes("{{");
49566
+ }
49567
+ function collectStaticEncodingWarnings(operation, request, base, rule) {
49568
+ const encodings = rule.encodings;
49569
+ if (!encodings) return [];
49570
+ const multipart = base === "multipart/form-data";
49571
+ const entries = requestBodyEntries(request, base);
49572
+ if (!entries) return [];
49573
+ const warnings = [];
49574
+ const part = multipart ? "multipart" : "urlencoded";
49575
+ for (const [field, encoding] of Object.entries(encodings)) {
49576
+ for (const entry of entries.filter((candidate) => String(candidate.key || "") === field)) {
49577
+ if (multipart && encoding.binary && String(entry.type || "") !== "file") {
49578
+ warnings.push(`CONTRACT_ENCODING_MISMATCH: ${operation.id} generated multipart field ${field} should be a file part per its binary schema`);
49579
+ }
49580
+ if (multipart && encoding.contentType) {
49581
+ const actual = typeof entry.contentType === "string" ? (entry.contentType.toLowerCase().split(";")[0] ?? "").trim() : "";
49582
+ if (!actual) {
49583
+ warnings.push(
49584
+ `CONTRACT_ENCODING_MISMATCH: ${operation.id} generated multipart field ${field} does not declare Content-Type ${encoding.contentType} from its encoding object`
49585
+ );
49586
+ } else if (!mediaTypeMatchesPattern(encoding.contentType, actual)) {
49587
+ warnings.push(
49588
+ `CONTRACT_ENCODING_MISMATCH: ${operation.id} generated multipart field ${field} Content-Type ${actual} does not match declared encoding ${encoding.contentType}`
49589
+ );
49590
+ }
49591
+ }
49592
+ if (encoding.contentType && isJsonEncodingContentType(encoding.contentType) && String(entry.type || "text") !== "file") {
49593
+ const value = typeof entry.value === "string" ? entry.value : "";
49594
+ if (value.trim() && !isPlaceholderValue(value)) {
49595
+ try {
49596
+ JSON.parse(value);
49597
+ } catch {
49598
+ warnings.push(
49599
+ `CONTRACT_ENCODING_MISMATCH: ${operation.id} generated ${part} field ${field} declares JSON encoding ${encoding.contentType} but its value is not parseable JSON`
49600
+ );
49601
+ }
49602
+ }
49603
+ }
49604
+ }
49605
+ }
49606
+ return warnings;
49607
+ }
49608
+ function coerceFormValue(value, schema2) {
49609
+ const record = asRecord5(schema2);
49610
+ const type2 = record?.type;
49611
+ const types2 = Array.isArray(type2) ? type2 : [type2];
49612
+ if ((types2.includes("integer") || types2.includes("number")) && /^-?[0-9]+([.][0-9]+)?([eE][+-]?[0-9]+)?$/.test(value.trim())) return Number(value);
49613
+ if (types2.includes("boolean") && (value === "true" || value === "false")) return value === "true";
49614
+ return value;
49615
+ }
49616
+ function collectStaticFieldSchemaWarnings(operation, request, base, rule) {
49617
+ const fieldSchemas = rule.fieldSchemas;
49618
+ if (!fieldSchemas) return [];
49619
+ const entries = requestBodyEntries(request, base);
49620
+ if (!entries) return [];
49621
+ const part = base === "multipart/form-data" ? "multipart" : "urlencoded";
49622
+ const warnings = [];
49623
+ for (const [field, schema2] of Object.entries(fieldSchemas)) {
49624
+ const validate2 = compileSchemaValidator(schema2);
49625
+ if (!validate2) continue;
49626
+ for (const entry of entries.filter((candidate) => String(candidate.key || "") === field)) {
49627
+ if (String(entry.type || "text") === "file") continue;
49628
+ const value = typeof entry.value === "string" ? entry.value : "";
49629
+ if (!value.trim() || isPlaceholderValue(value)) continue;
49630
+ if (!validate2(coerceFormValue(value, schema2))) {
49631
+ warnings.push(`CONTRACT_FORM_FIELD_SCHEMA_MISMATCH: ${operation.id} generated ${part} field ${field} value does not match its schema`);
49632
+ }
49633
+ }
49634
+ }
49635
+ return warnings;
49636
+ }
49637
+ function collectStaticBodyWarnings(operation, request, contentType) {
49638
+ const rules = operation.requestBody?.fieldRules;
49639
+ if (!rules) return [];
49640
+ const base = (contentType || "").toLowerCase().split(";")[0]?.trim() ?? "";
49641
+ const rule = rules[base];
49642
+ if (!rule) return [];
49643
+ const warnings = [
49644
+ ...collectStaticEncodingWarnings(operation, request, base, rule),
49645
+ ...collectStaticFieldSchemaWarnings(operation, request, base, rule)
49646
+ ];
49647
+ const names = requestBodyFieldNames(request, base);
49648
+ if (!names) return warnings;
49649
+ const present = new Set(names);
49650
+ const missing = rule.required.filter((name) => !present.has(name));
49651
+ if (missing.length > 0) {
49652
+ warnings.push(`CONTRACT_REQUEST_BODY_INCOMPLETE: ${operation.id} generated request body is missing required properties: ${missing.join(", ")}`);
49653
+ }
49654
+ const readOnlySent = rule.readOnly.filter((name) => present.has(name));
49655
+ if (readOnlySent.length > 0) {
49656
+ warnings.push(`CONTRACT_READONLY_PROPERTY_IN_REQUEST: ${operation.id} generated request body includes readOnly properties: ${readOnlySent.join(", ")}`);
49657
+ }
49658
+ return warnings;
48701
49659
  }
48702
49660
  function validateScript(script) {
48703
49661
  const source = script.join("\n");
@@ -48746,9 +49704,12 @@ function instrumentContractCollection(collection, index) {
48746
49704
  if (result.operation) {
48747
49705
  const previous = covered.get(result.operation.id);
48748
49706
  if (previous) throw new Error(`CONTRACT_DUPLICATE_OPERATION_REQUEST: ${result.operation.id} matched more than one generated request (${previous}, ${String(item.name || "<unnamed>")})`);
48749
- assertStaticRequestShape(result.operation, request);
49707
+ warnings.push(...assertStaticRequestShape(result.operation, request));
49708
+ for (const name of [...requestQueryNames(request)].filter((entry) => !result.operation.declaredQueryParameters.includes(entry))) {
49709
+ warnings.push(`CONTRACT_UNDOCUMENTED_QUERY_PARAM: ${result.operation.id} generated request sends query parameter ${name} that the OpenAPI operation does not declare`);
49710
+ }
48750
49711
  covered.set(result.operation.id, String(item.name || "<unnamed>"));
48751
- script = createContractScript(result.operation);
49712
+ script = createContractScript(result.operation, warnings);
48752
49713
  } else if (result.ambiguous && result.ambiguous.length > 0) {
48753
49714
  script = createMappingFailureScript(`Ambiguous OpenAPI operation match for request ${result.method} ${result.path}: ${result.ambiguous.map((entry) => entry.id).join(", ")}`);
48754
49715
  } else {