@postman-cse/onboarding-bootstrap 2.2.0 → 2.3.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
@@ -296642,7 +296642,11 @@ function securityCheckFor(schemeName, scheme) {
296642
296642
  if (scheme?.type === "http") {
296643
296643
  const httpScheme = String(scheme.scheme || "").toLowerCase();
296644
296644
  if (httpScheme === "basic") return { scheme: schemeName, kind, checkable: true, prefix: "Basic " };
296645
- if (httpScheme === "bearer") return { scheme: schemeName, kind, checkable: true, prefix: "Bearer " };
296645
+ if (httpScheme === "bearer") {
296646
+ const check = { scheme: schemeName, kind, checkable: true, prefix: "Bearer " };
296647
+ if (typeof scheme.bearerFormat === "string" && scheme.bearerFormat) check.bearerFormat = scheme.bearerFormat;
296648
+ return check;
296649
+ }
296646
296650
  if (httpScheme) return { scheme: schemeName, kind, checkable: true, prefix: `${httpScheme.charAt(0).toUpperCase()}${httpScheme.slice(1)} ` };
296647
296651
  return { scheme: schemeName, kind, checkable: true, in: "header", name: "Authorization" };
296648
296652
  }
@@ -296781,6 +296785,7 @@ function collectParameterChecks(root, pathItem, operation, version2, operationId
296781
296785
  if (defaultSerialization) warnings.push(`CONTRACT_SCHEMA_NOT_COMPILED: parameter ${location2}:${name} schema on ${operationId} skipped (${packed.unsupported})`);
296782
296786
  continue;
296783
296787
  }
296788
+ validateParameterExamples(root, param, packed, `${location2}:${name} of ${operationId}`, warnings);
296784
296789
  if (location2 === "path") {
296785
296790
  const containingSegment = pathTemplate.split("/").find((segment) => segment.includes(`{${name}}`));
296786
296791
  if (containingSegment !== void 0 && containingSegment !== `{${name}}`) {
@@ -296797,12 +296802,32 @@ function collectParameterChecks(root, pathItem, operation, version2, operationId
296797
296802
  }
296798
296803
  const scalarSchema = packedScalarSchema(packed);
296799
296804
  if (scalarSchema !== void 0) {
296800
- if (!defaultSerialization) continue;
296805
+ const decodablePathStyle = location2 === "path" && (style === "label" || style === "matrix") && !explode ? style : void 0;
296806
+ if (!defaultSerialization && !decodablePathStyle) continue;
296807
+ if (!defaultSerialization) warnings.push(...noteWarnings);
296801
296808
  const check2 = { in: location2, name, required: param.required === true, schema: scalarSchema };
296809
+ if (decodablePathStyle) check2.pathStyle = decodablePathStyle;
296802
296810
  if (location2 === "query" && param.allowEmptyValue === true) check2.allowEmptyValue = true;
296803
296811
  checks.push(check2);
296804
296812
  continue;
296805
296813
  }
296814
+ if (location2 === "query" && style === "deepObject" && explode) {
296815
+ const objectSchema = asRecord5(packed.schema);
296816
+ const properties = objectSchema ? asRecord5(objectSchema.properties) : null;
296817
+ const allScalar = properties !== null && Object.keys(properties).length > 0 && Object.values(properties).every((prop) => {
296818
+ const record = asRecord5(prop);
296819
+ if (!record) return false;
296820
+ const types2 = Array.isArray(record.type) ? record.type : [record.type];
296821
+ return types2.every((entry) => typeof entry === "string" && SCALAR_SCHEMA_TYPES.has(entry));
296822
+ });
296823
+ if (allScalar) {
296824
+ warnings.push(...noteWarnings);
296825
+ const check2 = { in: "query", name, required: param.required === true, schema: packed.schema, decode: "deepObject" };
296826
+ if (param.allowEmptyValue === true) check2.allowEmptyValue = true;
296827
+ checks.push(check2);
296828
+ continue;
296829
+ }
296830
+ }
296806
296831
  if (location2 !== "query" && location2 !== "header") continue;
296807
296832
  const items = packedArrayItemsSchema(packed);
296808
296833
  if (items === void 0) continue;
@@ -297113,6 +297138,185 @@ function responseHeaders(root, version2, response, context, warnings) {
297113
297138
  }
297114
297139
  return entries;
297115
297140
  }
297141
+ var IANA_HTTP_AUTH_SCHEMES = /* @__PURE__ */ new Set([
297142
+ "basic",
297143
+ "bearer",
297144
+ "concealed",
297145
+ "digest",
297146
+ "dpop",
297147
+ "gnap",
297148
+ "hoba",
297149
+ "mutual",
297150
+ "negotiate",
297151
+ "oauth",
297152
+ "privatetoken",
297153
+ "scram-sha-1",
297154
+ "scram-sha-256",
297155
+ "vapid"
297156
+ ]);
297157
+ function httpsUrlLint(value, label, schemeName) {
297158
+ if (typeof value !== "string" || !value) return void 0;
297159
+ try {
297160
+ const parsed = new URL(value);
297161
+ if (parsed.protocol !== "https:") return `CONTRACT_SECURITY_SCHEME_URL: security scheme ${schemeName} ${label} ${value} is not an HTTPS URL`;
297162
+ } catch {
297163
+ return `CONTRACT_SECURITY_SCHEME_URL: security scheme ${schemeName} ${label} ${value} is not a valid URL`;
297164
+ }
297165
+ return void 0;
297166
+ }
297167
+ function collectSecurityStaticLints(root, operation) {
297168
+ const securitySchemes = asRecord5(asRecord5(root.components)?.securitySchemes);
297169
+ const requirements = operation.security === void 0 ? asArray3(root.security) : asArray3(operation.security);
297170
+ const warnings = /* @__PURE__ */ new Set();
297171
+ for (const requirement of requirements.map((entry) => asRecord5(entry)).filter((entry) => Boolean(entry))) {
297172
+ for (const [schemeName, requiredScopes] of Object.entries(requirement)) {
297173
+ let scheme;
297174
+ try {
297175
+ scheme = resolveInternalRef(root, securitySchemes?.[schemeName]);
297176
+ } catch {
297177
+ scheme = null;
297178
+ }
297179
+ if (!scheme) continue;
297180
+ if (scheme.type === "http") {
297181
+ const httpScheme = String(scheme.scheme || "").toLowerCase();
297182
+ if (httpScheme && !IANA_HTTP_AUTH_SCHEMES.has(httpScheme)) {
297183
+ warnings.add(`CONTRACT_UNKNOWN_HTTP_AUTH_SCHEME: security scheme ${schemeName} uses "${httpScheme}", which is not in the IANA HTTP Authentication Scheme registry`);
297184
+ }
297185
+ }
297186
+ if (scheme.type === "apiKey" && String(scheme.in) === "query") {
297187
+ warnings.add(`CONTRACT_CREDENTIALS_IN_QUERY: security scheme ${schemeName} sends credentials in the query string, which leaks into logs and referrers`);
297188
+ }
297189
+ if (scheme.type === "openIdConnect") {
297190
+ const urlWarning = httpsUrlLint(scheme.openIdConnectUrl, "openIdConnectUrl", schemeName);
297191
+ if (urlWarning) warnings.add(urlWarning);
297192
+ else if (typeof scheme.openIdConnectUrl === "string" && !scheme.openIdConnectUrl.endsWith("/.well-known/openid-configuration")) {
297193
+ warnings.add(`CONTRACT_SECURITY_SCHEME_URL: security scheme ${schemeName} openIdConnectUrl does not end in /.well-known/openid-configuration`);
297194
+ }
297195
+ }
297196
+ if (scheme.type === "oauth2") {
297197
+ const flows = asRecord5(scheme.flows) ?? {};
297198
+ const declaredScopes = /* @__PURE__ */ new Set();
297199
+ for (const [flowName, rawFlow] of Object.entries(flows)) {
297200
+ const flow = asRecord5(rawFlow);
297201
+ if (!flow) continue;
297202
+ for (const scope of Object.keys(asRecord5(flow.scopes) ?? {})) declaredScopes.add(scope);
297203
+ const urlFields = [["refreshUrl", flow.refreshUrl]];
297204
+ if (flowName === "implicit" || flowName === "authorizationCode") urlFields.push(["authorizationUrl", flow.authorizationUrl]);
297205
+ if (flowName === "password" || flowName === "clientCredentials" || flowName === "authorizationCode") urlFields.push(["tokenUrl", flow.tokenUrl]);
297206
+ for (const [label, value] of urlFields) {
297207
+ const urlWarning = httpsUrlLint(value, `${flowName} ${label}`, schemeName);
297208
+ if (urlWarning) warnings.add(urlWarning);
297209
+ }
297210
+ }
297211
+ for (const scope of asArray3(requiredScopes).filter((entry) => typeof entry === "string")) {
297212
+ if (!declaredScopes.has(scope)) {
297213
+ warnings.add(`CONTRACT_OAUTH2_UNDECLARED_SCOPE: operation requires scope "${scope}" of ${schemeName}, which no flow of the scheme declares`);
297214
+ }
297215
+ }
297216
+ }
297217
+ }
297218
+ }
297219
+ return [...warnings];
297220
+ }
297221
+ function collectSecurityResponseLints(root, operation, responses, operationId) {
297222
+ const warnings = [];
297223
+ const requirements = operation.security === void 0 ? asArray3(root.security) : asArray3(operation.security);
297224
+ const requirementRecords = requirements.map((entry) => asRecord5(entry)).filter((entry) => Boolean(entry));
297225
+ const secured = requirementRecords.length > 0 && requirementRecords.every((entry) => Object.keys(entry).length > 0);
297226
+ const statusKeys = new Set(Object.keys(responses));
297227
+ const hasCatchAll = statusKeys.has("default") || statusKeys.has("4XX");
297228
+ if (secured && !statusKeys.has("401") && !hasCatchAll) {
297229
+ warnings.push(`CONTRACT_SECURITY_RESPONSES_INCOMPLETE: ${operationId} requires authentication but documents no 401 (or 4XX/default) response`);
297230
+ }
297231
+ const usesScopes = requirementRecords.some((entry) => Object.values(entry).some((scopes) => Array.isArray(scopes) && scopes.length > 0));
297232
+ if (secured && usesScopes && !statusKeys.has("403") && !hasCatchAll) {
297233
+ warnings.push(`CONTRACT_SECURITY_RESPONSES_INCOMPLETE: ${operationId} requires scopes but documents no 403 (or 4XX/default) response`);
297234
+ }
297235
+ if (requirementRecords.length === 0) {
297236
+ for (const status of ["401", "403"]) {
297237
+ if (statusKeys.has(status)) warnings.push(`CONTRACT_UNSECURED_AUTH_RESPONSES: ${operationId} documents a ${status} response but declares no security requirement`);
297238
+ }
297239
+ }
297240
+ return warnings;
297241
+ }
297242
+ function collectLinkExpressions(root, response, operationId, warnings) {
297243
+ const links = asRecord5(response.links);
297244
+ if (!links) return [];
297245
+ const expressions = [];
297246
+ let unevaluated = false;
297247
+ for (const [linkName, rawLink] of Object.entries(links)) {
297248
+ let link;
297249
+ try {
297250
+ link = resolveInternalRef(root, rawLink);
297251
+ } catch {
297252
+ link = null;
297253
+ }
297254
+ if (!link) {
297255
+ unevaluated = true;
297256
+ continue;
297257
+ }
297258
+ const values = Object.values(asRecord5(link.parameters) ?? {});
297259
+ if (link.requestBody !== void 0) values.push(link.requestBody);
297260
+ if (values.length === 0) unevaluated = true;
297261
+ for (const value of values) {
297262
+ if (typeof value !== "string" || !value.startsWith("$")) continue;
297263
+ const bodyMatch = value.match(/^\$response\.body#(\/.*)$/);
297264
+ if (bodyMatch) {
297265
+ expressions.push({ link: linkName, kind: "body", pointer: bodyMatch[1] });
297266
+ continue;
297267
+ }
297268
+ const headerMatch = value.match(/^\$response\.header\.([!#$%&'*+.^_`|~0-9A-Za-z-]+)$/);
297269
+ if (headerMatch) {
297270
+ expressions.push({ link: linkName, kind: "header", header: headerMatch[1] });
297271
+ continue;
297272
+ }
297273
+ unevaluated = true;
297274
+ }
297275
+ }
297276
+ if (expressions.length === 0) {
297277
+ if (unevaluated) warnings.add(`CONTRACT_LINKS_NOT_VALIDATED: response links are not validated for ${operationId}`);
297278
+ } else if (unevaluated) {
297279
+ warnings.add(`CONTRACT_LINKS_PARTIALLY_VALIDATED: some link expressions for ${operationId} are not runtime-evaluable and are skipped`);
297280
+ }
297281
+ return expressions;
297282
+ }
297283
+ function escapeRegExpLiteral(value) {
297284
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
297285
+ }
297286
+ function serverAdvisoryPatterns(root, pathItem, operation) {
297287
+ const serverLists = [asArray3(operation.servers), asArray3(pathItem.servers), asArray3(root.servers)];
297288
+ const servers = serverLists.find((list) => list.length > 0) ?? [];
297289
+ const patterns = [];
297290
+ for (const rawServer of servers) {
297291
+ const server = asRecord5(rawServer);
297292
+ if (!server) continue;
297293
+ const url = typeof server.url === "string" ? server.url.trim() : "";
297294
+ if (!url || url === "/") continue;
297295
+ const variables = asRecord5(server.variables);
297296
+ const pattern = url.split(/(\{[^}]+\})/).map((part) => {
297297
+ const varMatch = part.match(/^\{([^}]+)\}$/);
297298
+ if (!varMatch) return escapeRegExpLiteral(part);
297299
+ const variable = asRecord5(variables?.[varMatch[1]]);
297300
+ const enumValues = asArray3(variable?.enum).filter((entry) => typeof entry === "string");
297301
+ if (enumValues.length > 0) return `(${enumValues.map(escapeRegExpLiteral).join("|")})`;
297302
+ return "[^/]*";
297303
+ }).join("");
297304
+ patterns.push(`^${pattern}`);
297305
+ }
297306
+ return patterns.length > 0 ? patterns : void 0;
297307
+ }
297308
+ function validateParameterExamples(root, param, packed, context, warnings) {
297309
+ if (packed.schema === void 0 || packed.unsupported) return;
297310
+ const candidates = exampleCandidates(root, param);
297311
+ if (candidates.length === 0) return;
297312
+ const validate3 = compileSchemaValidator(packed.schema);
297313
+ if (!validate3) return;
297314
+ for (const candidate of candidates) {
297315
+ if (!validate3(candidate.value)) {
297316
+ warnings.push(`CONTRACT_EXAMPLE_SCHEMA_MISMATCH: ${candidate.label} for parameter ${context} does not match its schema`);
297317
+ }
297318
+ }
297319
+ }
297116
297320
  function buildContractIndex(root) {
297117
297321
  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)");
297118
297322
  if (!("openapi" in root)) throw new Error("CONTRACT_UNSUPPORTED_OPENAPI_VERSION: Dynamic contract tests require OpenAPI 3.0 or 3.1 (missing openapi)");
@@ -297131,6 +297335,9 @@ function buildContractIndex(root) {
297131
297335
  const operation = resolveInternalRef(root, rawOperation);
297132
297336
  if (!operation) continue;
297133
297337
  if (operation.callbacks) warnings.push(`CONTRACT_CALLBACKS_NOT_VALIDATED: callbacks are not validated for ${lowerMethod.toUpperCase()} ${path8}`);
297338
+ if (operation.requestBody !== void 0 && ["get", "head", "delete"].includes(lowerMethod)) {
297339
+ warnings.push(`CONTRACT_METHOD_BODY_SEMANTICS: ${lowerMethod.toUpperCase()} ${path8} declares a request body; RFC 9110 defines no request-body semantics for ${lowerMethod.toUpperCase()}`);
297340
+ }
297134
297341
  const responses = asRecord5(operation.responses);
297135
297342
  if (!responses || Object.keys(responses).length === 0) {
297136
297343
  throw new Error(`CONTRACT_OPERATION_NO_RESPONSES: ${lowerMethod.toUpperCase()} ${path8} must define at least one response`);
@@ -297140,11 +297347,15 @@ function buildContractIndex(root) {
297140
297347
  for (const [status, rawResponse] of Object.entries(responses)) {
297141
297348
  const response = resolveInternalRef(root, rawResponse);
297142
297349
  if (!response) continue;
297143
- if (asRecord5(response.links)) {
297144
- responseWarnings.add(`CONTRACT_LINKS_NOT_VALIDATED: response links are not validated for ${lowerMethod.toUpperCase()} ${path8}`);
297350
+ if (status !== "default" && !/^[1-5]XX$/.test(status) && !/^[1-5][0-9][0-9]$/.test(status)) {
297351
+ responseWarnings.add(`CONTRACT_INVALID_STATUS_CODE: ${lowerMethod.toUpperCase()} ${path8} declares response status "${status}" outside RFC 9110's 100-599, 1XX-5XX, or default forms`);
297145
297352
  }
297353
+ const linkExpressions = collectLinkExpressions(root, response, `${lowerMethod.toUpperCase()} ${path8}`, responseWarnings);
297146
297354
  const responseContext = `${lowerMethod.toUpperCase()} ${path8} status ${status}`;
297147
297355
  const content = responseContent(root, version2, response, responseContext, responseWarnings);
297356
+ if ((status === "204" || status === "205" || status === "304") && Object.keys(content).length > 0) {
297357
+ responseWarnings.add(`CONTRACT_BODYLESS_STATUS_WITH_CONTENT: ${lowerMethod.toUpperCase()} ${path8} declares content for status ${status}, which RFC 9110 forbids on the wire`);
297358
+ }
297148
297359
  for (const [contentType2, media] of Object.entries(content)) {
297149
297360
  const base = contentType2.toLowerCase().split(";")[0]?.trim() ?? "";
297150
297361
  const schemaType = asRecord5(media.schema)?.type;
@@ -297156,7 +297367,8 @@ function buildContractIndex(root) {
297156
297367
  contractResponses[normalizeResponseKey(status)] = {
297157
297368
  content,
297158
297369
  hasBody: Object.keys(content).length > 0,
297159
- headers
297370
+ headers,
297371
+ ...linkExpressions.length > 0 ? { links: linkExpressions } : {}
297160
297372
  };
297161
297373
  }
297162
297374
  const candidates = [...new Set([
@@ -297169,11 +297381,13 @@ function buildContractIndex(root) {
297169
297381
  opWarnings.push(...collectSecuritySchemeWarnings(root, operation));
297170
297382
  const parameterChecks = collectParameterChecks(root, pathItem, operation, version2, operationId, path8, opWarnings);
297171
297383
  const checkedKeys = new Set((parameterChecks ?? []).map((check) => `${check.in}:${check.name.toLowerCase()}`));
297172
- const decodedKeys = new Set((parameterChecks ?? []).filter((check) => check.decode).map((check) => `${check.in}:${check.name.toLowerCase()}`));
297384
+ const decodedKeys = new Set((parameterChecks ?? []).filter((check) => check.decode || check.pathStyle).map((check) => `${check.in}:${check.name.toLowerCase()}`));
297173
297385
  opWarnings.push(...collectSerializationWarnings(root, pathItem, operation, decodedKeys));
297174
297386
  if (operation.deprecated === true) {
297175
297387
  opWarnings.push(`CONTRACT_OPERATION_DEPRECATED: ${lowerMethod.toUpperCase()} ${path8} is marked deprecated in the OpenAPI document`);
297176
297388
  }
297389
+ opWarnings.push(...collectSecurityStaticLints(root, operation));
297390
+ opWarnings.push(...collectSecurityResponseLints(root, operation, responses, operationId));
297177
297391
  const requiredParameters = collectParameters(root, pathItem, operation);
297178
297392
  for (const parameter of requiredParameters.filter((entry) => entry.securityDerived)) {
297179
297393
  opWarnings.push(`CONTRACT_SECURITY_NOT_VALIDATED: security parameter ${parameter.in}:${parameter.name} is not statically required in generated requests`);
@@ -297203,6 +297417,8 @@ function buildContractIndex(root) {
297203
297417
  requestBody: collectRequestBody(root, operation, version2, operationId, opWarnings),
297204
297418
  security: collectSecurityRuntimeChecks(root, operation),
297205
297419
  pathMethods: Object.keys(pathItem).filter((key) => HTTP_METHODS.has(key)).map((key) => key.toUpperCase()),
297420
+ deprecated: operation.deprecated === true || void 0,
297421
+ servers: serverAdvisoryPatterns(root, pathItem, operation),
297206
297422
  warnings: opWarnings
297207
297423
  });
297208
297424
  }
@@ -297379,7 +297595,7 @@ function buildValidatorAssignments(operation, warnings, skipped) {
297379
297595
  return lines;
297380
297596
  }
297381
297597
  function createContractScript(operation, warnings = []) {
297382
- const contract = { method: operation.method, path: operation.path, responses: operation.responses, security: operation.security, parameters: operation.parameterChecks, pathMethods: operation.pathMethods };
297598
+ const contract = { method: operation.method, path: operation.path, responses: operation.responses, security: operation.security, parameters: operation.parameterChecks, pathMethods: operation.pathMethods, deprecated: operation.deprecated, servers: operation.servers };
297383
297599
  const skipped = [];
297384
297600
  const validatorLines = buildValidatorAssignments(operation, warnings, skipped);
297385
297601
  return [
@@ -297498,6 +297714,13 @@ function createContractScript(operation, warnings = []) {
297498
297714
  ' if (!challenge) pm.expect.fail("RFC 9110 requires WWW-Authenticate on 401 responses");',
297499
297715
  ' var wantsBearer = (contract.security || []).some(function (alternative) { return alternative.some(function (check) { return check.prefix && check.prefix.toLowerCase().indexOf("bearer") === 0; }); });',
297500
297716
  ' if (wantsBearer && challenge && challenge.toLowerCase().indexOf("bearer") === -1) pm.expect.fail("RFC 6750 expects a Bearer challenge on 401 for bearer-secured operations; got: " + challenge);',
297717
+ ' if (challenge && /\\bbasic\\b/i.test(challenge) && !/realm\\s*=/i.test(challenge)) pm.expect.fail("RFC 7617 requires a realm parameter on Basic challenges: " + challenge);',
297718
+ ' if (challenge && /\\bdigest\\b/i.test(challenge) && (!/realm\\s*=/i.test(challenge) || !/nonce\\s*=/i.test(challenge))) pm.expect.fail("RFC 7616 requires realm and nonce on Digest challenges: " + challenge);',
297719
+ " }",
297720
+ " if (code === 401 || code === 403) {",
297721
+ ' var authChallenge = respHeader("WWW-Authenticate");',
297722
+ ' var bearerError = authChallenge && /\\bbearer\\b/i.test(authChallenge) ? authChallenge.match(/\\berror\\s*=\\s*"?([A-Za-z0-9_]+)"?/i) : null;',
297723
+ ' if (bearerError && ["invalid_request", "invalid_token", "insufficient_scope"].indexOf(bearerError[1]) === -1) pm.expect.fail("RFC 6750 Bearer error code must be invalid_request, invalid_token, or insufficient_scope; got " + bearerError[1]);',
297501
297724
  " }",
297502
297725
  " if (code === 405) {",
297503
297726
  ' var allow = respHeader("Allow");',
@@ -297506,11 +297729,6 @@ function createContractScript(operation, warnings = []) {
297506
297729
  ' (contract.pathMethods || []).forEach(function (method) { if (allowed.indexOf(method) === -1) pm.expect.fail("Allow on 405 must list every method the OpenAPI path declares (RFC 9110); missing " + method + " in: " + allow); });',
297507
297730
  " }",
297508
297731
  ' if (code === 304 && responseText().trim().length > 0) pm.expect.fail("RFC 9110 forbids content in a 304 response");',
297509
- " if (code === 206) {",
297510
- ' var range = respHeader("Content-Range");',
297511
- ' if (!range) pm.expect.fail("RFC 9110 requires Content-Range on a single-part 206 response");',
297512
- ' if (range && !/^\\S+ (\\d+-\\d+|\\*)\\/(\\d+|\\*)$/.test(range)) pm.expect.fail("Content-Range is not a valid RFC 9110 range: " + range);',
297513
- " }",
297514
297732
  ' var retryAfter = respHeader("Retry-After");',
297515
297733
  " if (retryAfter && (code === 429 || code === 503 || (code >= 300 && code < 400))) {",
297516
297734
  ' if (!/^\\d+$/.test(retryAfter.trim()) && isNaN(Date.parse(retryAfter))) pm.expect.fail("Retry-After must be delay-seconds or an HTTP-date (RFC 9110): " + retryAfter);',
@@ -297528,6 +297746,7 @@ function createContractScript(operation, warnings = []) {
297528
297746
  ' try { problem = pm.response.json(); } catch (error) { pm.expect.fail("application/problem+json body is not valid JSON (RFC 9457): " + error); }',
297529
297747
  ' if (!problem || typeof problem !== "object" || Array.isArray(problem)) pm.expect.fail("problem details must be a JSON object (RFC 9457)");',
297530
297748
  ' ["type", "title", "detail", "instance"].forEach(function (member) { if (problem[member] !== undefined && typeof problem[member] !== "string") pm.expect.fail("RFC 9457 " + member + " member must be a string; got " + typeof problem[member]); });',
297749
+ ' ["type", "instance"].forEach(function (member) { if (typeof problem[member] === "string" && /\\s/.test(problem[member].trim())) pm.expect.fail("RFC 9457 " + member + " member must be a URI-reference (RFC 3986): " + problem[member]); });',
297531
297750
  " if (problem.status !== undefined) {",
297532
297751
  ' if (typeof problem.status !== "number") pm.expect.fail("RFC 9457 status member must be a number; got " + typeof problem.status);',
297533
297752
  ' else if (problem.status !== pm.response.code) pm.expect.fail("RFC 9457 status member (" + problem.status + ") must match the HTTP status code (" + pm.response.code + ")");',
@@ -297545,6 +297764,359 @@ function createContractScript(operation, warnings = []) {
297545
297764
  " });",
297546
297765
  " }",
297547
297766
  "});",
297767
+ "var rfcAdvisories = [];",
297768
+ "function rfcAdvise(message) { if (rfcAdvisories.indexOf(message) === -1) rfcAdvisories.push(message); }",
297769
+ 'function rfcRespHeader(name) { return pm.response.headers.get(name) || ""; }',
297770
+ "function rfcHeaderAll(name) { var out = []; pm.response.headers.each(function (header) { if (header && String(header.key).toLowerCase() === String(name).toLowerCase()) out.push(String(header.value)); }); return out; }",
297771
+ "function rfcIsHttpDate(value) { return /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), [0-3][0-9] (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{4} [0-2][0-9]:[0-5][0-9]:[0-5][0-9] GMT$/.test(String(value).trim()) && !isNaN(Date.parse(value)); }",
297772
+ "function rfcIsToken(value) { return /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(String(value)); }",
297773
+ 'function rfcIsEntityTag(value) { return /^(W\\/)?"[\\x21\\x23-\\x7e\\x80-\\xff]*"$/.test(String(value).trim()); }',
297774
+ "function rfcIsFieldContent(value) { return /^[\\t \\x21-\\x7e\\x80-\\xff]*$/.test(String(value)); }",
297775
+ 'function rfcTokenList(value) { var parts = String(value).split(","); for (var i = 0; i < parts.length; i += 1) { if (!rfcIsToken(parts[i].trim())) return false; } return true; }',
297776
+ `function rfcSplitList(value) { var out = []; var current = ""; var inQuote = false; for (var i = 0; i < value.length; i += 1) { var ch = value.charAt(i); if (ch === "\\\\" && inQuote) { current += ch + (value.charAt(i + 1) || ""); i += 1; continue; } if (ch === '"') inQuote = !inQuote; if (ch === "," && !inQuote) { out.push(current); current = ""; continue; } current += ch; } out.push(current); return out; }`,
297777
+ 'function rfcBase64Decode(value) { var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; var clean = String(value).replace(/=+$/, ""); if (clean.length === 0 || /[^A-Za-z0-9+\\/]/.test(clean)) return null; var bits = 0, buffer = 0, out = ""; for (var i = 0; i < clean.length; i += 1) { buffer = (buffer << 6) | alphabet.indexOf(clean.charAt(i)); bits += 6; if (bits >= 8) { bits -= 8; out += String.fromCharCode((buffer >> bits) & 255); } } return out; }',
297778
+ "function rfcSfParse(input, kind) {",
297779
+ " var s = String(input), i = 0;",
297780
+ ' function ws() { while (i < s.length && (s.charAt(i) === " " || s.charAt(i) === "\\t")) i += 1; }',
297781
+ " function key() { if (!/[a-z*]/.test(s.charAt(i))) return null; var start = i; i += 1; while (i < s.length && /[a-z0-9_.*-]/.test(s.charAt(i))) i += 1; return s.slice(start, i); }",
297782
+ " function bareItem() {",
297783
+ " var ch = s.charAt(i);",
297784
+ ` if (ch === '"') { i += 1; while (i < s.length) { var c = s.charAt(i); if (c === "\\\\") { if (!/["\\\\]/.test(s.charAt(i + 1))) return null; i += 2; continue; } if (c === '"') { i += 1; return true; } if (c < " " || c > "~") return null; i += 1; } return null; }`,
297785
+ ' if (ch === ":") { i += 1; var start = i; while (i < s.length && s.charAt(i) !== ":") i += 1; if (s.charAt(i) !== ":") return null; var body = s.slice(start, i); i += 1; return /^[A-Za-z0-9+\\/=]*$/.test(body) ? true : null; }',
297786
+ ' if (ch === "?") { i += 1; if (s.charAt(i) !== "0" && s.charAt(i) !== "1") return null; i += 1; return true; }',
297787
+ ' if (ch === "@") { i += 1; if (s.charAt(i) === "-") i += 1; if (!/[0-9]/.test(s.charAt(i))) return null; while (i < s.length && /[0-9]/.test(s.charAt(i))) i += 1; return true; }',
297788
+ ' if (/[-0-9]/.test(ch)) { if (ch === "-") i += 1; if (!/[0-9]/.test(s.charAt(i))) return null; while (i < s.length && /[0-9]/.test(s.charAt(i))) i += 1; if (s.charAt(i) === ".") { i += 1; if (!/[0-9]/.test(s.charAt(i))) return null; while (i < s.length && /[0-9]/.test(s.charAt(i))) i += 1; } return true; }',
297789
+ " if (/[A-Za-z*]/.test(ch)) { i += 1; while (i < s.length && /[!#$%&'*+.^_`|~:\\/0-9A-Za-z-]/.test(s.charAt(i))) i += 1; return true; }",
297790
+ " return null;",
297791
+ " }",
297792
+ ' function params() { while (s.charAt(i) === ";") { i += 1; ws(); if (key() === null) return null; if (s.charAt(i) === "=") { i += 1; if (bareItem() === null) return null; } } return true; }',
297793
+ ' function item() { if (s.charAt(i) === "(") { i += 1; ws(); while (s.charAt(i) !== ")") { if (i >= s.length) return null; if (bareItem() === null || params() === null) return null; ws(); } i += 1; return params(); } if (bareItem() === null) return null; return params(); }',
297794
+ " ws();",
297795
+ ' if (kind === "item") { if (item() === null) return false; ws(); return i === s.length; }',
297796
+ " if (i === s.length) return true;",
297797
+ " while (i < s.length) {",
297798
+ ' if (kind === "dict") { if (key() === null) return false; if (s.charAt(i) === "=") { i += 1; if (item() === null) return false; } else if (params() === null) return false; }',
297799
+ " else if (item() === null) return false;",
297800
+ " ws();",
297801
+ " if (i === s.length) return true;",
297802
+ ' if (s.charAt(i) !== ",") return false;',
297803
+ " i += 1; ws();",
297804
+ " if (i === s.length) return false;",
297805
+ " }",
297806
+ " return true;",
297807
+ "}",
297808
+ "pm.test('Response header fields satisfy RFC 9110 field syntax', function () {",
297809
+ " pm.response.headers.each(function (header) {",
297810
+ " if (!header) return;",
297811
+ ' if (!rfcIsToken(String(header.key))) pm.expect.fail("Response header name is not a valid RFC 9110 token: " + header.key);',
297812
+ ' if (!rfcIsFieldContent(String(header.value))) pm.expect.fail("Response header value contains characters forbidden by RFC 9110 field-content: " + header.key);',
297813
+ " });",
297814
+ ' ["content-type", "content-length", "etag", "location", "date", "age", "expires", "last-modified", "retry-after"].forEach(function (name) {',
297815
+ " var values = rfcHeaderAll(name);",
297816
+ ' for (var i = 1; i < values.length; i += 1) { if (values[i] !== values[0]) pm.expect.fail("Singleton response header " + name + " appears " + values.length + " times with differing values (RFC 9110)"); }',
297817
+ " });",
297818
+ "});",
297819
+ "pm.test('Response header values satisfy their RFC grammars', function () {",
297820
+ ' var date = rfcRespHeader("Date");',
297821
+ ' if (date && !rfcIsHttpDate(date)) pm.expect.fail("Date must be an IMF-fixdate (RFC 9110): " + date);',
297822
+ ' if (!date) rfcAdvise("RFC 9110: origin servers SHOULD send a Date header");',
297823
+ ' var etag = rfcRespHeader("ETag");',
297824
+ ' if (etag && !rfcIsEntityTag(etag)) pm.expect.fail("ETag is not a valid entity-tag (RFC 9110): " + etag);',
297825
+ ' var lastModified = rfcRespHeader("Last-Modified");',
297826
+ " if (lastModified) {",
297827
+ ' if (!rfcIsHttpDate(lastModified)) pm.expect.fail("Last-Modified must be a valid HTTP-date (RFC 9110): " + lastModified);',
297828
+ ' else if (date && rfcIsHttpDate(date) && Date.parse(lastModified) > Date.parse(date)) pm.expect.fail("Last-Modified must not be later than Date (RFC 9110): " + lastModified + " > " + date);',
297829
+ " }",
297830
+ ' var vary = rfcRespHeader("Vary");',
297831
+ " if (vary) {",
297832
+ ' var varyMembers = vary.split(",").map(function (entry) { return entry.trim(); });',
297833
+ ' if (varyMembers.indexOf("*") !== -1 && varyMembers.length > 1) pm.expect.fail("Vary: * must not be combined with other members (RFC 9110): " + vary);',
297834
+ ' varyMembers.forEach(function (member) { if (member !== "*" && !rfcIsToken(member)) pm.expect.fail("Vary member is not a field-name token (RFC 9110): " + member); });',
297835
+ " }",
297836
+ ' var contentLocation = rfcRespHeader("Content-Location");',
297837
+ ' if (contentLocation && (/\\s/.test(contentLocation.trim()) || contentLocation.trim().length === 0)) pm.expect.fail("Content-Location must be a valid URI-reference (RFC 9110): " + contentLocation);',
297838
+ ' var acceptRanges = rfcRespHeader("Accept-Ranges");',
297839
+ ' if (acceptRanges && !rfcTokenList(acceptRanges)) pm.expect.fail("Accept-Ranges must be a list of range-unit tokens (RFC 9110): " + acceptRanges);',
297840
+ ' var contentLanguage = rfcRespHeader("Content-Language");',
297841
+ ' if (contentLanguage) contentLanguage.split(",").forEach(function (tag) { if (!/^[A-Za-z]{1,8}(-[A-Za-z0-9]{1,8})*$/.test(tag.trim())) pm.expect.fail("Content-Language carries a malformed BCP 47 language-tag (RFC 5646): " + tag.trim()); });',
297842
+ ' var allow = rfcRespHeader("Allow");',
297843
+ ' if (allow && allow.trim() && !rfcTokenList(allow)) pm.expect.fail("Allow must be a comma-separated list of method tokens (RFC 9110): " + allow);',
297844
+ ' if (allow && contract.method === "OPTIONS" && pm.response.code >= 200 && pm.response.code < 300) {',
297845
+ ' var optionsAllowed = allow.split(",").map(function (entry) { return entry.trim().toUpperCase(); });',
297846
+ ' (contract.pathMethods || []).forEach(function (method) { if (optionsAllowed.indexOf(method) === -1) pm.expect.fail("Allow on an OPTIONS response must list every method the OpenAPI path declares (RFC 9110); missing " + method + " in: " + allow); });',
297847
+ " }",
297848
+ ' var age = rfcRespHeader("Age");',
297849
+ ' if (age && !/^[0-9]+$/.test(age.trim())) pm.expect.fail("Age must be a non-negative integer of delta-seconds (RFC 9111): " + age);',
297850
+ ' var expires = rfcRespHeader("Expires");',
297851
+ ' if (expires && !rfcIsHttpDate(expires)) rfcAdvise("RFC 9111: Expires is not a valid HTTP-date and will be treated as already expired: " + expires);',
297852
+ ' if (rfcHeaderAll("warning").length > 0) rfcAdvise("RFC 9111 obsoleted the Warning header; the server still emits it");',
297853
+ ' var cacheControl = rfcRespHeader("Cache-Control");',
297854
+ " if (cacheControl) {",
297855
+ " var seenDirectives = {};",
297856
+ " rfcSplitList(cacheControl).forEach(function (entry) {",
297857
+ " var directive = entry.trim();",
297858
+ ' if (!directive) { pm.expect.fail("Cache-Control contains an empty directive (RFC 9111): " + cacheControl); return; }',
297859
+ ' var eq = directive.indexOf("=");',
297860
+ " var name = (eq === -1 ? directive : directive.slice(0, eq)).trim().toLowerCase();",
297861
+ " var argument = eq === -1 ? undefined : directive.slice(eq + 1).trim();",
297862
+ ' if (!rfcIsToken(name)) pm.expect.fail("Cache-Control directive name is not a token (RFC 9111): " + directive);',
297863
+ " seenDirectives[name] = argument === undefined ? true : argument;",
297864
+ ' if (["max-age", "s-maxage", "stale-while-revalidate", "stale-if-error"].indexOf(name) !== -1 && (argument === undefined || !/^"?[0-9]+"?$/.test(argument))) pm.expect.fail("Cache-Control " + name + " requires a delta-seconds argument (RFC 9111): " + directive);',
297865
+ " });",
297866
+ ' if (seenDirectives["no-store"] && seenDirectives["max-age"] !== undefined) pm.expect.fail("Cache-Control combines no-store with max-age; the directives contradict (RFC 9111): " + cacheControl);',
297867
+ " }",
297868
+ ' var acceptPatch = rfcRespHeader("Accept-Patch");',
297869
+ ' if (acceptPatch) acceptPatch.split(",").forEach(function (entry) { var parts = mediaParts(entry); if (!parts.type || !parts.subtype) pm.expect.fail("Accept-Patch must be a list of media types (RFC 5789): " + entry.trim()); });',
297870
+ ' var deprecation = rfcRespHeader("Deprecation");',
297871
+ ' if (deprecation && !/^@-?[0-9]+$/.test(deprecation.trim())) pm.expect.fail("Deprecation must be an RFC 9745 Date structured field (@unix-timestamp): " + deprecation);',
297872
+ ' var sunset = rfcRespHeader("Sunset");',
297873
+ " if (sunset) {",
297874
+ ' if (!rfcIsHttpDate(sunset)) pm.expect.fail("Sunset must be a valid HTTP-date (RFC 8594): " + sunset);',
297875
+ ' else if (Date.parse(sunset) < Date.now()) rfcAdvise("RFC 8594: Sunset date is already in the past: " + sunset);',
297876
+ " }",
297877
+ ' var preferenceApplied = rfcRespHeader("Preference-Applied");',
297878
+ " if (preferenceApplied) {",
297879
+ ' var requestPrefer = requestHeader("Prefer").toLowerCase();',
297880
+ " rfcSplitList(preferenceApplied).forEach(function (entry) {",
297881
+ ' var token = entry.split("=")[0].trim().toLowerCase();',
297882
+ ' if (token && requestPrefer.indexOf(token) === -1) pm.expect.fail("Preference-Applied echoes a preference the request never sent (RFC 7240): " + entry.trim());',
297883
+ " });",
297884
+ " }",
297885
+ "});",
297886
+ "pm.test('Response satisfies RFC 9110 message framing requirements', function () {",
297887
+ " var code = pm.response.code;",
297888
+ ' if ((code === 204 || code < 200) && rfcRespHeader("Content-Length")) pm.expect.fail("RFC 9110 forbids Content-Length on 1xx and 204 responses");',
297889
+ ' if ([301, 302, 303, 307, 308].indexOf(code) !== -1 && !rfcRespHeader("Location")) pm.expect.fail("RFC 9110 expects Location on a " + code + " redirect response");',
297890
+ " if (code === 416) {",
297891
+ ' var unsatisfiedRange = rfcRespHeader("Content-Range");',
297892
+ ' if (!unsatisfiedRange) pm.expect.fail("RFC 9110 requires Content-Range (unsatisfied-range form) on 416 responses");',
297893
+ ' else if (!/^\\S+ \\*\\/[0-9]+$/.test(unsatisfiedRange.trim())) pm.expect.fail("Content-Range on 416 must use the unsatisfied-range form <unit> */<complete-length> (RFC 9110): " + unsatisfiedRange);',
297894
+ " }",
297895
+ " if (code === 206) {",
297896
+ ' var contentRange = rfcRespHeader("Content-Range");',
297897
+ ' var responseMedia = mediaParts(rfcRespHeader("Content-Type"));',
297898
+ ' var isByteranges = responseMedia.type === "multipart" && responseMedia.subtype === "byteranges";',
297899
+ ' if (!contentRange && !isByteranges) pm.expect.fail("RFC 9110 requires Content-Range on a single-part 206 response (or multipart/byteranges for multi-range)");',
297900
+ ' if (contentRange && isByteranges) pm.expect.fail("RFC 9110 forbids Content-Range on a multipart/byteranges 206 response");',
297901
+ " if (contentRange) {",
297902
+ " var rangeParts = contentRange.trim().match(/^(\\S+) (?:([0-9]+)-([0-9]+)|\\*)\\/([0-9]+|\\*)$/);",
297903
+ ' if (!rangeParts) pm.expect.fail("Content-Range is not a valid RFC 9110 range: " + contentRange);',
297904
+ " else {",
297905
+ ' if (rangeParts[1] !== "bytes") rfcAdvise("RFC 9110: 206 Content-Range uses a non-bytes range unit: " + rangeParts[1]);',
297906
+ " if (rangeParts[2] !== undefined) {",
297907
+ ' if (Number(rangeParts[2]) > Number(rangeParts[3])) pm.expect.fail("Content-Range first-byte-pos must be <= last-byte-pos (RFC 9110): " + contentRange);',
297908
+ ' if (rangeParts[4] !== "*" && Number(rangeParts[3]) >= Number(rangeParts[4])) pm.expect.fail("Content-Range last-byte-pos must be < complete-length (RFC 9110): " + contentRange);',
297909
+ " }",
297910
+ " }",
297911
+ " }",
297912
+ " }",
297913
+ ' if (code === 407 && !rfcRespHeader("Proxy-Authenticate")) pm.expect.fail("RFC 9110 requires Proxy-Authenticate on 407 responses");',
297914
+ ' if (code === 415 && contract.method === "PATCH" && !rfcRespHeader("Accept-Patch")) rfcAdvise("RFC 5789: a 415 response to PATCH SHOULD carry Accept-Patch");',
297915
+ "});",
297916
+ "pm.test('Response media type is acceptable under the request Accept header', function () {",
297917
+ " if (pm.response.code < 200 || pm.response.code >= 300 || isBodyless()) return;",
297918
+ ' var accept = requestHeader("Accept");',
297919
+ ' if (!accept || accept.indexOf("{{") !== -1) return;',
297920
+ ' var actual = mediaParts(rfcRespHeader("Content-Type"));',
297921
+ " if (!actual.type) return;",
297922
+ " var acceptable = false;",
297923
+ " var jsonSoftened = false;",
297924
+ " rfcSplitList(accept).forEach(function (entry) {",
297925
+ " var range = mediaParts(entry);",
297926
+ ' var qMatch = entry.match(/;\\s*q\\s*=\\s*"?([0-9.]+)"?/i);',
297927
+ " if (qMatch && Number(qMatch[1]) <= 0) return;",
297928
+ ' if ((range.type === "*" && range.subtype === "*") || (range.type === actual.type && (range.subtype === "*" || range.subtype === actual.subtype))) acceptable = true;',
297929
+ ' if (range.type === actual.type && range.subtype === "json" && isJsonSubtype(actual.subtype)) jsonSoftened = true;',
297930
+ " });",
297931
+ ' if (!acceptable && jsonSoftened) { rfcAdvise("Content negotiation: response " + actual.raw + " is a +json type while the request only accepted application/json"); return; }',
297932
+ ' if (!acceptable) pm.expect.fail("Response Content-Type " + actual.raw + " is not acceptable under the request Accept header (RFC 9110): " + accept);',
297933
+ "});",
297934
+ "pm.test('Response body satisfies its media type RFC conventions', function () {",
297935
+ ' var contentTypeValue = rfcRespHeader("Content-Type");',
297936
+ " var media = mediaParts(contentTypeValue);",
297937
+ " var text = responseText();",
297938
+ ' if (pm.response.code === 406 && !text.trim()) rfcAdvise("RFC 9110: a 406 response SHOULD include a list of available representations");',
297939
+ " if (!text) return;",
297940
+ " if (isJsonSubtype(media.subtype)) {",
297941
+ ' if (text.charCodeAt(0) === 65279) pm.expect.fail("RFC 8259 forbids a byte order mark at the start of JSON text");',
297942
+ ' var charsetParam = contentTypeValue.match(/charset\\s*=\\s*"?([^";\\s]+)"?/i);',
297943
+ ' if (charsetParam) rfcAdvise("RFC 8259 defines no charset parameter for JSON media types; got charset=" + charsetParam[1]);',
297944
+ " }",
297945
+ ' if (media.raw === "application/x-ndjson" || media.raw === "application/jsonl" || media.raw === "application/x-jsonlines") {',
297946
+ ' text.split(/\\r?\\n/).forEach(function (line, lineNumber) { if (!line.trim()) return; try { JSON.parse(line); } catch (error) { pm.expect.fail("NDJSON line " + (lineNumber + 1) + " is not valid JSON: " + error); } });',
297947
+ " }",
297948
+ ' if (media.raw === "text/event-stream") {',
297949
+ ' text.split(/\\r?\\n/).forEach(function (line) { if (!line || line.charAt(0) === ":") return; var field = line.split(":")[0]; if (["data", "event", "id", "retry"].indexOf(field) === -1) pm.expect.fail("SSE line does not start with a known field or comment: " + line); if (field === "retry" && !/^retry:\\s*[0-9]+\\s*$/.test(line)) pm.expect.fail("SSE retry field must be an integer: " + line); });',
297950
+ " }",
297951
+ ' if (media.type === "multipart") {',
297952
+ ' var boundary = contentTypeValue.match(/;\\s*boundary=(?:"([^"]*)"|([^;]*))/i);',
297953
+ ' var boundaryValue = boundary ? (boundary[1] !== undefined ? boundary[1] : boundary[2].trim()) : "";',
297954
+ ' if (!boundaryValue) pm.expect.fail("multipart responses must carry a boundary parameter (RFC 2046): " + contentTypeValue);',
297955
+ " else {",
297956
+ ' if (boundaryValue.length > 70) pm.expect.fail("multipart boundary must be 1-70 characters (RFC 2046): " + boundaryValue);',
297957
+ ` if (!/^[0-9A-Za-z'()+_,\\-.\\/:=? ]*[0-9A-Za-z'()+_,\\-.\\/:=?]$/.test(boundaryValue)) pm.expect.fail("multipart boundary contains characters outside RFC 2046 bchars or ends with a space: " + boundaryValue);`,
297958
+ " }",
297959
+ " }",
297960
+ ' if (media.raw === "application/hal+json") {',
297961
+ " var hal; try { hal = JSON.parse(text); } catch (error) { hal = null; }",
297962
+ ' if (hal && typeof hal === "object" && !Array.isArray(hal)) {',
297963
+ " var halLinks = hal._links;",
297964
+ ' if (halLinks !== undefined && (typeof halLinks !== "object" || Array.isArray(halLinks) || halLinks === null)) pm.expect.fail("HAL _links must be an object of link relations");',
297965
+ ' if (halLinks) Object.keys(halLinks).forEach(function (rel) { var linkValue = halLinks[rel]; (Array.isArray(linkValue) ? linkValue : [linkValue]).forEach(function (linkObject) { if (!linkObject || typeof linkObject !== "object" || typeof linkObject.href !== "string") pm.expect.fail("HAL link relation " + rel + " must be a Link Object (or array of them) with a string href"); }); });',
297966
+ " var halEmbedded = hal._embedded;",
297967
+ ' if (halEmbedded !== undefined && (typeof halEmbedded !== "object" || Array.isArray(halEmbedded) || halEmbedded === null)) pm.expect.fail("HAL _embedded must be an object of resource names");',
297968
+ " }",
297969
+ " }",
297970
+ ' if (media.raw === "application/vnd.api+json") {',
297971
+ " var jsonApi; try { jsonApi = JSON.parse(text); } catch (error) { jsonApi = null; }",
297972
+ ' if (jsonApi && typeof jsonApi === "object" && !Array.isArray(jsonApi)) {',
297973
+ ' if (jsonApi.data === undefined && jsonApi.errors === undefined && jsonApi.meta === undefined) pm.expect.fail("JSON:API documents must contain at least one of data, errors, meta");',
297974
+ ' if (jsonApi.data !== undefined && jsonApi.errors !== undefined) pm.expect.fail("JSON:API forbids data and errors in the same document");',
297975
+ " }",
297976
+ " }",
297977
+ ' if (media.subtype === "problem+xml") {',
297978
+ ' if (text.indexOf("urn:ietf:rfc:7807") === -1) rfcAdvise("application/problem+xml body does not reference the urn:ietf:rfc:7807 namespace");',
297979
+ " var xmlStatus = text.match(/<status[^>]*>\\s*([0-9]+)\\s*<\\/status>/);",
297980
+ ' if (xmlStatus && Number(xmlStatus[1]) !== pm.response.code) pm.expect.fail("RFC 9457 status member (" + xmlStatus[1] + ") must match the HTTP status code (" + pm.response.code + ")");',
297981
+ " }",
297982
+ "});",
297983
+ "pm.test('Structured field response headers parse per RFC 8941', function () {",
297984
+ ' [["Cache-Status", "list"], ["Proxy-Status", "list"], ["Priority", "dict"], ["RateLimit", "dict"], ["RateLimit-Policy", "dict"], ["Signature", "dict"], ["Signature-Input", "dict"]].forEach(function (pair) {',
297985
+ ' var value = rfcHeaderAll(pair[0]).join(", ");',
297986
+ " if (!value) return;",
297987
+ ' if (!rfcSfParse(value, pair[1])) pm.expect.fail(pair[0] + " is not a valid RFC 8941 structured field (" + pair[1] + "): " + value);',
297988
+ " });",
297989
+ "});",
297990
+ "pm.test('Content-Digest and Repr-Digest match the response body (RFC 9530)', function () {",
297991
+ " var cryptoLib = null;",
297992
+ ' try { cryptoLib = require("crypto-js"); } catch (error) { cryptoLib = null; }',
297993
+ ' ["Content-Digest", "Repr-Digest"].forEach(function (name) {',
297994
+ " var value = rfcRespHeader(name);",
297995
+ " if (!value) return;",
297996
+ ' if (!rfcSfParse(value, "dict")) { pm.expect.fail(name + " is not a valid RFC 8941 dictionary (RFC 9530): " + value); return; }',
297997
+ ' if (!cryptoLib || rfcRespHeader("Content-Encoding")) return;',
297998
+ ' var media = mediaParts(rfcRespHeader("Content-Type"));',
297999
+ ' if (media.type !== "text" && !isJsonSubtype(media.subtype) && !/xml$/.test(media.subtype)) return;',
298000
+ " rfcSplitList(value).forEach(function (entry) {",
298001
+ " var match = entry.trim().match(/^(sha-256|sha-512)=:([A-Za-z0-9+\\/=]+):$/);",
298002
+ " if (!match) return;",
298003
+ ' var computed = match[1] === "sha-256" ? cryptoLib.SHA256(responseText()) : cryptoLib.SHA512(responseText());',
298004
+ " var encoded = cryptoLib.enc.Base64.stringify(computed);",
298005
+ ' if (encoded !== match[2]) pm.expect.fail(name + " " + match[1] + " does not match the response body (RFC 9530): computed " + encoded + " but header carries " + match[2]);',
298006
+ " });",
298007
+ " });",
298008
+ "});",
298009
+ "pm.test('Request credentials are well-formed per their authentication scheme RFCs', function () {",
298010
+ ' var authorization = requestHeader("Authorization");',
298011
+ ' if (authorization && authorization.indexOf("{{") === -1) {',
298012
+ " var schemeMatch = authorization.match(/^(\\S+)(?:\\s+([\\s\\S]*))?$/);",
298013
+ ' var authScheme = schemeMatch ? schemeMatch[1].toLowerCase() : "";',
298014
+ ' var authParams = schemeMatch && schemeMatch[2] !== undefined ? schemeMatch[2].trim() : "";',
298015
+ ' if (authScheme === "basic") {',
298016
+ " var decoded = rfcBase64Decode(authParams);",
298017
+ ' if (decoded === null) pm.expect.fail("Basic credentials must be base64 (RFC 7617)");',
298018
+ ' else if (decoded.indexOf(":") === -1) pm.expect.fail("Basic credentials must decode to user-id:password (RFC 7617)");',
298019
+ " }",
298020
+ ' if (authScheme === "bearer" && !/^[A-Za-z0-9\\-._~+\\/]+=*$/.test(authParams)) pm.expect.fail("Bearer token does not match the b64token grammar (RFC 6750)");',
298021
+ ' if (authScheme === "digest") {',
298022
+ ' rfcSplitList(authParams).forEach(function (entry) { var param = entry.trim(); if (param && !/^[!#$%&\'*+.^_`|~0-9A-Za-z-]+\\s*=\\s*("([^"\\\\]|\\\\.)*"|[!#$%&\'*+.^_`|~0-9A-Za-z-]+)$/.test(param)) pm.expect.fail("Digest auth-param is malformed (RFC 7616): " + param); });',
298023
+ ' var digestResponse = authParams.match(/\\bresponse\\s*=\\s*"?([^",\\s]+)"?/i);',
298024
+ ' if (digestResponse && !/^[0-9a-fA-F]+$/.test(digestResponse[1])) pm.expect.fail("Digest response parameter must be hex (RFC 7616): " + digestResponse[1]);',
298025
+ " }",
298026
+ " }",
298027
+ ' var wantsJwt = (contract.security || []).some(function (alternative) { return alternative.some(function (check) { return check.prefix === "Bearer " && String(check.bearerFormat || "").toUpperCase() === "JWT"; }); });',
298028
+ ' if (wantsJwt && authorization && authorization.indexOf("{{") === -1 && authorization.toLowerCase().indexOf("bearer ") === 0) {',
298029
+ " var jwtToken = authorization.slice(7).trim();",
298030
+ ' var jwtSegments = jwtToken.split(".");',
298031
+ ' if (jwtSegments.length !== 3) pm.expect.fail("bearerFormat JWT tokens must have three base64url segments (RFC 7519); got " + jwtSegments.length);',
298032
+ ' else if (!jwtSegments.every(function (segment) { return /^[A-Za-z0-9_-]+$/.test(segment); })) pm.expect.fail("JWT segments must be base64url (RFC 7515)");',
298033
+ " else {",
298034
+ ' var jwtDecode = function (segment) { var padded = segment.replace(/-/g, "+").replace(/_/g, "/"); while (padded.length % 4 !== 0) padded += "="; return rfcBase64Decode(padded); };',
298035
+ " var jwtHeader = null; var jwtPayload = null;",
298036
+ ' try { jwtHeader = JSON.parse(jwtDecode(jwtSegments[0])); } catch (error) { pm.expect.fail("JWT header segment does not decode to JSON (RFC 7515)"); }',
298037
+ ' try { jwtPayload = JSON.parse(jwtDecode(jwtSegments[1])); } catch (error) { pm.expect.fail("JWT payload segment does not decode to JSON (RFC 7519)"); }',
298038
+ ' if (jwtHeader && typeof jwtHeader.alg !== "string") pm.expect.fail("JWT header must carry a string alg member (RFC 7515)");',
298039
+ " if (jwtPayload) {",
298040
+ ' ["exp", "nbf", "iat"].forEach(function (claim) { if (jwtPayload[claim] !== undefined && typeof jwtPayload[claim] !== "number") pm.expect.fail("JWT " + claim + " claim must be numeric (RFC 7519)"); });',
298041
+ ' if (typeof jwtPayload.exp === "number" && jwtPayload.exp * 1000 < Date.now()) rfcAdvise("RFC 7519: the outgoing JWT exp claim is already in the past");',
298042
+ " }",
298043
+ " }",
298044
+ " }",
298045
+ ' if (hasQueryParam("access_token")) rfcAdvise("RFC 6750: bearer tokens SHOULD NOT travel in the query string");',
298046
+ " (contract.security || []).forEach(function (alternative) {",
298047
+ " alternative.forEach(function (check) {",
298048
+ " if (!check.checkable || !check.name) return;",
298049
+ ' if (check.in === "query") { if (hasQueryParam(check.name)) rfcAdvise("Security scheme " + check.scheme + " sends credentials in the query string"); return; }',
298050
+ ' if (check.in !== "header" || String(check.name).toLowerCase() === "authorization") return;',
298051
+ " var apiKeyValue = requestHeader(check.name);",
298052
+ ' if (!apiKeyValue || apiKeyValue.indexOf("{{") !== -1) return;',
298053
+ ' if (apiKeyValue !== apiKeyValue.trim()) pm.expect.fail("API key header " + check.name + " carries leading or trailing whitespace");',
298054
+ ' if (!rfcIsFieldContent(apiKeyValue)) pm.expect.fail("API key header " + check.name + " contains characters forbidden by RFC 9110 field-content");',
298055
+ " });",
298056
+ " });",
298057
+ "});",
298058
+ "pm.test('Request preconditions, preferences, and patch bodies follow their RFCs', function () {",
298059
+ ' ["If-Match", "If-None-Match"].forEach(function (name) {',
298060
+ " var value = requestHeader(name);",
298061
+ ' if (!value || value.indexOf("{{") !== -1 || value.trim() === "*") return;',
298062
+ ' rfcSplitList(value).forEach(function (entry) { if (entry.trim() && !rfcIsEntityTag(entry.trim())) pm.expect.fail(name + " must be * or a list of entity-tags (RFC 9110): " + entry.trim()); });',
298063
+ " });",
298064
+ ' var prefer = requestHeader("Prefer");',
298065
+ ' if (prefer && prefer.indexOf("{{") === -1) {',
298066
+ ' rfcSplitList(prefer).forEach(function (entry) { var token = entry.split("=")[0].split(";")[0].trim(); if (token && !rfcIsToken(token)) pm.expect.fail("Prefer preference name must be a token (RFC 7240): " + entry.trim()); });',
298067
+ " }",
298068
+ ' var requestContentType = mediaBase(requestHeader("Content-Type"));',
298069
+ " var body = pm.request.body;",
298070
+ ' var raw = body && body.mode === "raw" && typeof body.raw === "string" ? body.raw : "";',
298071
+ ' if (!raw.trim() || raw.indexOf("{{") !== -1 || /"<[^"<>]*>"/.test(raw)) return;',
298072
+ ' if (requestContentType === "application/json-patch+json") {',
298073
+ ' var patch; try { patch = JSON.parse(raw); } catch (error) { if (/<[A-Za-z][A-Za-z0-9_ -]*>/.test(raw)) return; pm.expect.fail("application/json-patch+json request body is not valid JSON (RFC 6902): " + error); return; }',
298074
+ ' if (!Array.isArray(patch)) { pm.expect.fail("A JSON Patch document must be an array of operations (RFC 6902)"); return; }',
298075
+ " patch.forEach(function (operation, operationIndex) {",
298076
+ ' if (!operation || typeof operation !== "object" || Array.isArray(operation)) { pm.expect.fail("JSON Patch operation " + operationIndex + " must be an object (RFC 6902)"); return; }',
298077
+ ' if (["add", "remove", "replace", "move", "copy", "test"].indexOf(operation.op) === -1) pm.expect.fail("JSON Patch operation " + operationIndex + " has an invalid op (RFC 6902): " + operation.op);',
298078
+ " var pointerPattern = /^(\\/([^\\/~]|~[01])*)*$/;",
298079
+ ' if (typeof operation.path !== "string" || !pointerPattern.test(operation.path)) pm.expect.fail("JSON Patch operation " + operationIndex + " path must be an RFC 6901 JSON Pointer");',
298080
+ ' if (["add", "replace", "test"].indexOf(operation.op) !== -1 && operation.value === undefined) pm.expect.fail("JSON Patch " + operation.op + " operation " + operationIndex + " requires a value member (RFC 6902)");',
298081
+ ' if (["move", "copy"].indexOf(operation.op) !== -1 && (typeof operation.from !== "string" || !pointerPattern.test(operation.from))) pm.expect.fail("JSON Patch " + operation.op + " operation " + operationIndex + " requires an RFC 6901 from pointer (RFC 6902)");',
298082
+ " });",
298083
+ " }",
298084
+ ' if (requestContentType === "application/merge-patch+json") {',
298085
+ ' try { JSON.parse(raw); } catch (error) { if (/<[A-Za-z][A-Za-z0-9_ -]*>/.test(raw)) return; pm.expect.fail("application/merge-patch+json request body must be valid JSON (RFC 7386): " + error); }',
298086
+ " }",
298087
+ "});",
298088
+ "pm.test('Deprecated operation signals deprecation in the response', function () {",
298089
+ " if (!contract.deprecated) return;",
298090
+ ' if (!rfcRespHeader("Deprecation") && !rfcRespHeader("Sunset")) rfcAdvise("RFC 9745: the OpenAPI document deprecates this operation but the response carries neither Deprecation nor Sunset");',
298091
+ "});",
298092
+ "pm.test('OpenAPI link expressions resolve against the response', function () {",
298093
+ " if (!selected || !selected.value.links || selected.value.links.length === 0) return;",
298094
+ " var linkBody = null; var linkBodyParsed = false;",
298095
+ " selected.value.links.forEach(function (expression) {",
298096
+ ' if (expression.kind === "header") {',
298097
+ ' if (!pm.response.headers.get(expression.header)) pm.expect.fail("OpenAPI link " + expression.link + " references response header " + expression.header + " which is absent");',
298098
+ " return;",
298099
+ " }",
298100
+ " if (!linkBodyParsed) { linkBodyParsed = true; try { linkBody = JSON.parse(responseText()); } catch (error) { linkBody = null; } }",
298101
+ ' if (linkBody === null) { pm.expect.fail("OpenAPI link " + expression.link + " references the response body but the body is not JSON"); return; }',
298102
+ " var target = linkBody;",
298103
+ ' var tokens = String(expression.pointer).split("/").slice(1).map(function (token) { return token.replace(/~1/g, "/").replace(/~0/g, "~"); });',
298104
+ " for (var t = 0; t < tokens.length; t += 1) {",
298105
+ ' if (target !== null && typeof target === "object") target = Array.isArray(target) ? target[Number(tokens[t])] : target[tokens[t]];',
298106
+ " else { target = undefined; break; }",
298107
+ " }",
298108
+ ' if (target === undefined) pm.expect.fail("OpenAPI link " + expression.link + " expression $response.body#" + expression.pointer + " does not resolve in the response body");',
298109
+ " });",
298110
+ "});",
298111
+ "pm.test('Request URL conforms to an OpenAPI servers entry', function () {",
298112
+ " if (!contract.servers || contract.servers.length === 0) return;",
298113
+ ' var requestUrl = "";',
298114
+ ' try { requestUrl = String(pm.request.url.toString()); } catch (ignored) { requestUrl = ""; }',
298115
+ ' if (!requestUrl || requestUrl.indexOf("{{") !== -1) return;',
298116
+ ' var pathOnly = requestUrl.replace(/^[a-z][a-z0-9+.-]*:\\/\\/[^\\/]+/i, "");',
298117
+ ' var matched = contract.servers.some(function (pattern) { try { var serverPattern = new RegExp(pattern, "i"); return serverPattern.test(requestUrl) || serverPattern.test(pathOnly); } catch (ignored) { return true; } });',
298118
+ ' if (!matched) rfcAdvise("Request URL does not match any OpenAPI servers entry: " + requestUrl);',
298119
+ "});",
297548
298120
  ...operation.security ? [
297549
298121
  "pm.test('Request carries credentials required by OpenAPI security', function () {",
297550
298122
  " function satisfied(check) {",
@@ -297584,6 +298156,12 @@ function createContractScript(operation, warnings = []) {
297584
298156
  ' if (param.allowEmptyValue && entries.length === 1 && entries[0] === "") return;',
297585
298157
  " if (entries.some(isPlaceholder)) return;",
297586
298158
  " value = entries.map(function (entry) { return coerceBySchema(decodeComponent(entry), param.items); });",
298159
+ ' } else if (param.decode === "deepObject") {',
298160
+ " var deepValue = {}; var deepFound = false; var deepPlaceholder = false;",
298161
+ ' pm.request.url.query.each(function (queryParam) { if (!queryParam || queryParam.disabled === true) return; var deepMatch = String(queryParam.key).match(/^([^\\[]+)\\[([^\\]]+)\\]$/); if (!deepMatch || deepMatch[1].toLowerCase() !== String(param.name).toLowerCase()) return; deepFound = true; var deepRaw = queryParam.value === null || queryParam.value === undefined ? "" : String(queryParam.value); if (isPlaceholder(deepRaw)) { deepPlaceholder = true; return; } deepValue[deepMatch[2]] = coerceBySchema(decodeComponent(deepRaw), (param.schema && param.schema.properties && param.schema.properties[deepMatch[2]]) || {}); });',
298162
+ ' if (!deepFound) { if (param.required) pm.expect.fail("Required parameter " + param.in + ":" + param.name + " was not sent for " + contract.method + " " + contract.path); return; }',
298163
+ " if (deepPlaceholder) return;",
298164
+ " value = deepValue;",
297587
298165
  " } else if (param.decode) {",
297588
298166
  ' var joined = param.in === "query" ? queryValue(String(param.name).toLowerCase()) : headerValue(param.name);',
297589
298167
  ' if (joined === undefined) { if (param.required) pm.expect.fail("Required parameter " + param.in + ":" + param.name + " was not sent for " + contract.method + " " + contract.path); return; }',
@@ -297600,6 +298178,8 @@ function createContractScript(operation, warnings = []) {
297600
298178
  ' } else if (param.in === "path") {',
297601
298179
  " value = pathParamValue(param.name);",
297602
298180
  " if (value === undefined) return;",
298181
+ ' if (param.pathStyle === "label") { if (String(value).charAt(0) !== ".") return; value = String(value).slice(1); }',
298182
+ ' if (param.pathStyle === "matrix") { var matrixPrefix = ";" + param.name + "="; if (String(value).indexOf(matrixPrefix) !== 0) return; value = String(value).slice(matrixPrefix.length); }',
297603
298183
  ' if (isPlaceholder(value) || value.charAt(0) === ":" || value.charAt(0) === "{") return;',
297604
298184
  " value = coerceBySchema(value, param.schema);",
297605
298185
  ' } else if (param.in === "cookie") {',
@@ -297651,6 +298231,9 @@ function createContractScript(operation, warnings = []) {
297651
298231
  ' if (pm.response.headers.get("Content-Encoding")) return;',
297652
298232
  " var mustBeEmpty = isBodyless() || (selected && Object.keys(selected.value.content || {}).length === 0);",
297653
298233
  ' 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);',
298234
+ "});",
298235
+ "pm.test('RFC SHOULD-level advisories are documented', function () {",
298236
+ ' pm.expect(rfcAdvisories, "SHOULD-level findings (advisory, non-failing): " + rfcAdvisories.join("; ")).to.be.an("array");',
297654
298237
  "});"
297655
298238
  ];
297656
298239
  }
@@ -297720,6 +298303,9 @@ function assertStaticRequestShape(operation, request) {
297720
298303
  }
297721
298304
  }
297722
298305
  const warnings = collectStaticBodyWarnings(operation, request, contentType2);
298306
+ if (["GET", "HEAD", "DELETE"].includes(operation.method) && hasRequestBody(request)) {
298307
+ warnings.push(`CONTRACT_METHOD_BODY_SEMANTICS: ${operation.id} sends a request body with ${operation.method}; RFC 9110 defines no request-body semantics for this method`);
298308
+ }
297723
298309
  if (operation.requestBody && !operation.requestBody.required && operation.requestBody.contentTypes.length > 0 && hasRequestBody(request) && contentType2) {
297724
298310
  const actual = contentType2.toLowerCase().split(";")[0]?.trim() ?? "";
297725
298311
  const matches = operation.requestBody.contentTypes.some((expected) => mediaTypeMatchesPattern(expected.toLowerCase(), actual));