raffel 1.1.74 → 1.1.76

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.
Files changed (105) hide show
  1. package/README.md +7 -0
  2. package/dist/docs/docs-state.d.ts +2 -0
  3. package/dist/docs/docs-state.d.ts.map +1 -1
  4. package/dist/docs/docs-state.js +1 -0
  5. package/dist/docs/docs-state.js.map +1 -1
  6. package/dist/docs/generators/http-generator.d.ts +1 -1
  7. package/dist/docs/generators/http-generator.js +2 -2
  8. package/dist/docs/generators/http-generator.js.map +1 -1
  9. package/dist/docs/generators/usd-generator.d.ts +5 -1
  10. package/dist/docs/generators/usd-generator.d.ts.map +1 -1
  11. package/dist/docs/generators/usd-generator.js +5 -1
  12. package/dist/docs/generators/usd-generator.js.map +1 -1
  13. package/dist/docs/index.d.ts +1 -1
  14. package/dist/docs/index.d.ts.map +1 -1
  15. package/dist/docs/index.js +1 -1
  16. package/dist/docs/index.js.map +1 -1
  17. package/dist/docs/try-it-proxy.d.ts +16 -0
  18. package/dist/docs/try-it-proxy.d.ts.map +1 -0
  19. package/dist/docs/try-it-proxy.js +138 -0
  20. package/dist/docs/try-it-proxy.js.map +1 -0
  21. package/dist/docs/ui/assets/raffel-docs.css +266 -10
  22. package/dist/docs/ui/assets/raffel-docs.js +1167 -49
  23. package/dist/docs/ui/client-script/index.d.ts +1 -1
  24. package/dist/docs/ui/client-script/index.d.ts.map +1 -1
  25. package/dist/docs/ui/client-script/index.js +4 -2
  26. package/dist/docs/ui/client-script/index.js.map +1 -1
  27. package/dist/docs/ui/html-builder.d.ts.map +1 -1
  28. package/dist/docs/ui/html-builder.js +58 -2
  29. package/dist/docs/ui/html-builder.js.map +1 -1
  30. package/dist/docs/ui/response-examples.d.ts +10 -0
  31. package/dist/docs/ui/response-examples.d.ts.map +1 -0
  32. package/dist/docs/ui/response-examples.js +113 -0
  33. package/dist/docs/ui/response-examples.js.map +1 -0
  34. package/dist/docs/ui/runtime/index.js +1167 -49
  35. package/dist/docs/ui/runtime/index.js.map +1 -1
  36. package/dist/docs/ui/style-sections/schema-code.d.ts +1 -1
  37. package/dist/docs/ui/style-sections/schema-code.d.ts.map +1 -1
  38. package/dist/docs/ui/style-sections/schema-code.js +249 -0
  39. package/dist/docs/ui/style-sections/schema-code.js.map +1 -1
  40. package/dist/docs/ui/style-sections/shell.d.ts.map +1 -1
  41. package/dist/docs/ui/style-sections/shell.js +17 -10
  42. package/dist/docs/ui/style-sections/shell.js.map +1 -1
  43. package/dist/docs/ui/types.d.ts +23 -2
  44. package/dist/docs/ui/types.d.ts.map +1 -1
  45. package/dist/docs/usd-middleware.d.ts +23 -2
  46. package/dist/docs/usd-middleware.d.ts.map +1 -1
  47. package/dist/docs/usd-middleware.js +142 -6
  48. package/dist/docs/usd-middleware.js.map +1 -1
  49. package/dist/http/route-table.d.ts.map +1 -1
  50. package/dist/http/route-table.js +19 -0
  51. package/dist/http/route-table.js.map +1 -1
  52. package/dist/server/builder/execution-http-docs.d.ts.map +1 -1
  53. package/dist/server/builder/execution-http-docs.js +35 -3
  54. package/dist/server/builder/execution-http-docs.js.map +1 -1
  55. package/dist/server/builder.d.ts.map +1 -1
  56. package/dist/server/builder.js +4 -1
  57. package/dist/server/builder.js.map +1 -1
  58. package/dist/server/rest-middleware.d.ts +1 -1
  59. package/dist/server/rest-middleware.d.ts.map +1 -1
  60. package/dist/server/rest-middleware.js +36 -16
  61. package/dist/server/rest-middleware.js.map +1 -1
  62. package/dist/server/types/lifecycle-docs-types.d.ts +13 -2
  63. package/dist/server/types/lifecycle-docs-types.d.ts.map +1 -1
  64. package/dist/ui/docs/docs-state.d.ts +2 -0
  65. package/dist/ui/docs/docs-state.d.ts.map +1 -1
  66. package/dist/ui/docs/generators/http-generator.d.ts +1 -1
  67. package/dist/ui/docs/generators/usd-generator.d.ts +5 -1
  68. package/dist/ui/docs/generators/usd-generator.d.ts.map +1 -1
  69. package/dist/ui/docs/index.d.ts +1 -1
  70. package/dist/ui/docs/index.d.ts.map +1 -1
  71. package/dist/ui/docs/try-it-proxy.d.ts +16 -0
  72. package/dist/ui/docs/try-it-proxy.d.ts.map +1 -0
  73. package/dist/ui/docs/ui/client-script/index.d.ts +1 -1
  74. package/dist/ui/docs/ui/client-script/index.d.ts.map +1 -1
  75. package/dist/ui/docs/ui/html-builder.d.ts.map +1 -1
  76. package/dist/ui/docs/ui/response-examples.d.ts +10 -0
  77. package/dist/ui/docs/ui/response-examples.d.ts.map +1 -0
  78. package/dist/ui/docs/ui/style-sections/schema-code.d.ts +1 -1
  79. package/dist/ui/docs/ui/style-sections/schema-code.d.ts.map +1 -1
  80. package/dist/ui/docs/ui/style-sections/shell.d.ts.map +1 -1
  81. package/dist/ui/docs/ui/types.d.ts +23 -2
  82. package/dist/ui/docs/ui/types.d.ts.map +1 -1
  83. package/dist/ui/docs/usd-middleware.d.ts +23 -2
  84. package/dist/ui/docs/usd-middleware.d.ts.map +1 -1
  85. package/dist/ui/usd/export/openapi.d.ts.map +1 -1
  86. package/dist/ui/usd/index.d.ts +1 -1
  87. package/dist/ui/usd/index.d.ts.map +1 -1
  88. package/dist/ui/usd/spec/types.d.ts +43 -0
  89. package/dist/ui/usd/spec/types.d.ts.map +1 -1
  90. package/dist/ui/usd/validator/schema.d.ts +40 -0
  91. package/dist/ui/usd/validator/schema.d.ts.map +1 -1
  92. package/dist/usd/export/openapi.d.ts.map +1 -1
  93. package/dist/usd/export/openapi.js +4 -1
  94. package/dist/usd/export/openapi.js.map +1 -1
  95. package/dist/usd/index.d.ts +1 -1
  96. package/dist/usd/index.d.ts.map +1 -1
  97. package/dist/usd/index.js.map +1 -1
  98. package/dist/usd/spec/types.d.ts +43 -0
  99. package/dist/usd/spec/types.d.ts.map +1 -1
  100. package/dist/usd/spec/types.js.map +1 -1
  101. package/dist/usd/validator/schema.d.ts +40 -0
  102. package/dist/usd/validator/schema.d.ts.map +1 -1
  103. package/dist/usd/validator/schema.js +21 -0
  104. package/dist/usd/validator/schema.js.map +1 -1
  105. package/package.json +2 -1
@@ -32,6 +32,8 @@ const footerMarkdown = data.footerMarkdown ?? null;
32
32
  const tocConfig = data.tocConfig ?? {};
33
33
  const markdownConfig = data.markdownConfig ?? {};
34
34
  const mermaidConfig = data.mermaidConfig ?? { enabled: true, src: 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js', viewer: true };
35
+ const serializedResponseExamples = data.responseExamples ?? {};
36
+ const tryItConfig = data.tryIt ?? { enabled: false, mode: 'direct' };
35
37
  let mermaidLoadPromise = null;
36
38
  const docsRepoConfig = (data.docsRepoConfig ?? null);
37
39
  const breadcrumbsConfig = (data.breadcrumbsConfig && typeof data.breadcrumbsConfig === 'object')
@@ -39,6 +41,7 @@ const breadcrumbsConfig = (data.breadcrumbsConfig && typeof data.breadcrumbsConf
39
41
  : { enabled: true, hideOnHome: true };
40
42
  const pageNavConfig = data.pageNavConfig ?? { enabled: true, hide: [] };
41
43
  const xUsd = spec['x-usd'] ?? {};
44
+ const authenticationConfig = spec['x-usd-authentication'] ?? {};
42
45
  const { websocket: wsSpec = {}, graphql: graphqlSpec = {}, streams: streamsSpec = {}, jsonrpc: jsonrpcSpec = {}, grpc: grpcSpec = {}, tcp: tcpSpec = {}, udp: udpSpec = {} } = xUsd;
43
46
  const docsRouteBase = String(xUsd.documentation?.routeBase ?? '').replace(/^#/, '').replace(/\/+$/, '');
44
47
  const protocolData = detectProtocols();
@@ -52,6 +55,8 @@ const protocolRank = (name) => {
52
55
  };
53
56
  const protocols = Object.keys(protocolData).sort((a, b) => protocolRank(a) - protocolRank(b));
54
57
  let activeProtocol = protocols[0] ?? 'http';
58
+ const environments = resolveEnvironments(spec.servers);
59
+ let selectedEnvironmentUrl = inferEnvironmentUrl(environments);
55
60
  let searchQuery = '';
56
61
  let routeState = parseRouteHash();
57
62
  let activePagePath = resolveDocsAlias(routeState.pagePath);
@@ -859,6 +864,22 @@ function detectProtocols() {
859
864
  out.udp = Object.keys(udpSpec.endpoints).length;
860
865
  return out;
861
866
  }
867
+ function mergePathItemParameters(pathParameters, operationParameters) {
868
+ const resolvedParameters = (parameters) => (Array.isArray(parameters) ? parameters.map(parameter => resolveSchema(parameter) ?? parameter) : []);
869
+ const inherited = resolvedParameters(pathParameters);
870
+ const declared = resolvedParameters(operationParameters);
871
+ const declaredKeys = new Set(declared.map(parameter => {
872
+ const resolved = resolveSchema(parameter);
873
+ return `${String(resolved?.in ?? '')}\0${String(resolved?.name ?? '')}`;
874
+ }));
875
+ return [
876
+ ...inherited.filter(parameter => {
877
+ const resolved = resolveSchema(parameter);
878
+ return !declaredKeys.has(`${String(resolved?.in ?? '')}\0${String(resolved?.name ?? '')}`);
879
+ }),
880
+ ...declared,
881
+ ];
882
+ }
862
883
  function getEndpointsForProtocol(protocol) {
863
884
  const endpoints = [];
864
885
  let id = 0;
@@ -876,7 +897,10 @@ function getEndpointsForProtocol(protocol) {
876
897
  for (const [method, operation] of Object.entries(methods ?? {})) {
877
898
  if (!['get', 'post', 'put', 'patch', 'delete'].includes(method))
878
899
  continue;
879
- add(path, method.toUpperCase(), operation);
900
+ add(path, method.toUpperCase(), {
901
+ ...operation,
902
+ parameters: mergePathItemParameters(methods.parameters, operation.parameters),
903
+ });
880
904
  }
881
905
  }
882
906
  }
@@ -1202,11 +1226,19 @@ function appendSidebarGroup(nav, title, items) {
1202
1226
  group.appendChild(itemContainer);
1203
1227
  nav.appendChild(group);
1204
1228
  }
1205
- function generateExampleFromSchema(schema) {
1229
+ function generateExampleFromSchema(schema, refStack = new Set()) {
1206
1230
  if (!schema)
1207
1231
  return null;
1208
- if (schema.$ref)
1209
- return { $ref: schema.$ref };
1232
+ if (schema.$ref) {
1233
+ const pointer = String(schema.$ref);
1234
+ const name = pointer.split('/').pop() || pointer;
1235
+ if (refStack.has(pointer))
1236
+ return `Recursive schema: ${name}`;
1237
+ const resolved = resolveSchema(schema);
1238
+ if (resolved === schema)
1239
+ return `Unresolved schema reference: ${name}`;
1240
+ return generateExampleFromSchema(resolved, new Set([...refStack, pointer]));
1241
+ }
1210
1242
  if (schema.example !== undefined)
1211
1243
  return schema.example;
1212
1244
  if (schema.default !== undefined)
@@ -1218,20 +1250,79 @@ function generateExampleFromSchema(schema) {
1218
1250
  case 'number': return 0;
1219
1251
  case 'integer': return 0;
1220
1252
  case 'boolean': return true;
1221
- case 'array': return schema.items ? [generateExampleFromSchema(schema.items)] : [];
1253
+ case 'array': return schema.items ? [generateExampleFromSchema(schema.items, refStack)] : [];
1222
1254
  case 'object':
1223
1255
  if (!schema.properties)
1224
1256
  return {};
1225
1257
  const obj = {};
1226
1258
  Object.entries(schema.properties).forEach(([key, prop]) => {
1227
- obj[key] = generateExampleFromSchema(prop);
1259
+ obj[key] = generateExampleFromSchema(prop, refStack);
1228
1260
  });
1229
1261
  return obj;
1230
1262
  default: return null;
1231
1263
  }
1232
1264
  }
1265
+ function resolveEnvironments(servers) {
1266
+ const resolved = [];
1267
+ const list = Array.isArray(servers) && servers.length > 0
1268
+ ? servers
1269
+ : [{ url: win.location?.origin ?? 'http://localhost:3000', description: 'Current origin' }];
1270
+ list.forEach((server, serverIndex) => {
1271
+ const template = String(server?.url ?? '');
1272
+ const variables = server?.variables && typeof server.variables === 'object' ? server.variables : {};
1273
+ let combinations = [{}];
1274
+ for (const [name, definition] of Object.entries(variables)) {
1275
+ const values = Array.isArray(definition?.enum) && definition.enum.length > 0
1276
+ ? definition.enum.map(String)
1277
+ : [String(definition?.default ?? '')];
1278
+ combinations = combinations.flatMap(current => values.map(value => ({ ...current, [name]: value }))).slice(0, 64);
1279
+ }
1280
+ combinations.forEach((values, variantIndex) => {
1281
+ const expandedUrl = Object.entries(values).reduce((current, [name, value]) => current.replaceAll(`{${name}}`, encodeURIComponent(value)), template);
1282
+ const url = String(safeRuntimeUrl(expandedUrl)?.toString() ?? expandedUrl).replace(/\/$/, '');
1283
+ const described = String(server?.description ?? '');
1284
+ const label = Object.entries(values).reduce((current, [name, value]) => current.replaceAll(`{${name}}`, value), described) || Object.values(values).join(' / ') || `Server ${serverIndex + 1}`;
1285
+ resolved.push({ id: `${serverIndex}:${variantIndex}`, label, url, variables: values });
1286
+ });
1287
+ });
1288
+ return resolved;
1289
+ }
1290
+ function inferEnvironmentUrl(options) {
1291
+ if (options.length === 0)
1292
+ return String(win.location?.origin ?? 'http://localhost:3000');
1293
+ const current = safeRuntimeUrl(String(win.location?.href ?? ''));
1294
+ if (!current)
1295
+ return options[0].url;
1296
+ const scored = options.map((environment, index) => {
1297
+ const candidate = safeRuntimeUrl(environment.url);
1298
+ if (!candidate)
1299
+ return { environment, score: -index };
1300
+ let score = -index;
1301
+ if (candidate.origin === current.origin)
1302
+ score += 1000;
1303
+ if (current.href.startsWith(candidate.href.replace(/\/$/, '')))
1304
+ score += 100;
1305
+ if (candidate.hostname === current.hostname)
1306
+ score += 50;
1307
+ score += candidate.pathname === '/' ? 0 : candidate.pathname.length;
1308
+ return { environment, score };
1309
+ });
1310
+ scored.sort((a, b) => b.score - a.score);
1311
+ return scored[0].environment.url;
1312
+ }
1313
+ function safeRuntimeUrl(value) {
1314
+ try {
1315
+ return new URL(value, win.location?.origin ?? 'http://localhost:3000');
1316
+ }
1317
+ catch {
1318
+ return null;
1319
+ }
1320
+ }
1321
+ function selectedEnvironment() {
1322
+ return environments.find(environment => environment.url === selectedEnvironmentUrl) ?? environments[0];
1323
+ }
1233
1324
  function httpBaseUrl() {
1234
- return String(spec?.servers?.[0]?.url ?? 'http://localhost:3000').replace(/\/$/, '');
1325
+ return String(selectedEnvironment()?.url ?? spec?.servers?.[0]?.url ?? 'http://localhost:3000').replace(/\/$/, '');
1235
1326
  }
1236
1327
  // Collect JSON-Schema validation constraints as readable chips.
1237
1328
  function schemaConstraintChips(schema) {
@@ -1320,47 +1411,176 @@ function pyLiteral(value) {
1320
1411
  }
1321
1412
  return 'None';
1322
1413
  }
1323
- // Build request samples in cURL, JavaScript, Python and Rust.
1324
- function buildHttpSamples(method, url, body) {
1414
+ function parameterSample(parameter) {
1415
+ const schema = parameter?.schema ?? {};
1416
+ if (parameter?.example !== undefined)
1417
+ return parameter.example;
1418
+ if (schema.example !== undefined)
1419
+ return schema.example;
1420
+ if (schema.default !== undefined)
1421
+ return schema.default;
1422
+ if (Array.isArray(schema.enum) && schema.enum.length > 0)
1423
+ return schema.enum[0];
1424
+ return parameter?.required ? generateExampleFromSchema(schema) : undefined;
1425
+ }
1426
+ function serializeParameter(value) {
1427
+ if (Array.isArray(value))
1428
+ return value.map(item => serializeParameter(item)).join(',');
1429
+ if (value && typeof value === 'object')
1430
+ return JSON.stringify(value);
1431
+ return String(value ?? '');
1432
+ }
1433
+ function hasStoredCredential(schemeName) {
1434
+ const scheme = spec?.components?.securitySchemes?.[schemeName];
1435
+ const credential = readStoredCredential(schemeName);
1436
+ if (!scheme)
1437
+ return false;
1438
+ if (scheme.type === 'http' && String(scheme.scheme).toLowerCase() === 'bearer')
1439
+ return Boolean(credential.accessToken);
1440
+ if (scheme.type === 'http' && String(scheme.scheme).toLowerCase() === 'basic')
1441
+ return Boolean(credential.username || credential.password);
1442
+ if (scheme.type === 'oauth2' || scheme.type === 'openIdConnect')
1443
+ return Boolean(credential.accessToken);
1444
+ if (scheme.type === 'apiKey')
1445
+ return Boolean(credential.apiKey);
1446
+ return false;
1447
+ }
1448
+ function selectedSecurityRequirement(operation) {
1449
+ const requirements = operation.security === undefined ? (spec.security ?? []) : operation.security;
1450
+ if (!Array.isArray(requirements) || requirements.length === 0)
1451
+ return null;
1452
+ return requirements.find(requirement => (requirement &&
1453
+ typeof requirement === 'object' &&
1454
+ Object.keys(requirement).every(hasStoredCredential))) ?? requirements[0];
1455
+ }
1456
+ function buildStructuredHttpRequest(endpoint, operation) {
1457
+ const method = String(endpoint.method || 'GET').toUpperCase();
1458
+ const requestContent = operation.requestBody?.content;
1459
+ const body = requestContent ? operationRequestExample(operation) : null;
1460
+ const headers = {};
1461
+ let url = `${httpBaseUrl()}${endpoint.path || '/'}`;
1462
+ if (body !== null && body !== undefined && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
1463
+ headers['Content-Type'] = 'application/json';
1464
+ }
1465
+ const query = new URLSearchParams();
1466
+ const cookies = [];
1467
+ for (const parameter of operation.parameters ?? []) {
1468
+ const sample = parameterSample(parameter);
1469
+ if (sample === undefined)
1470
+ continue;
1471
+ const serialized = serializeParameter(sample);
1472
+ if (parameter.in === 'path') {
1473
+ url = url.replaceAll(`{${parameter.name}}`, encodeURIComponent(serialized)).replaceAll(`:${parameter.name}`, encodeURIComponent(serialized));
1474
+ }
1475
+ else if (parameter.in === 'query') {
1476
+ query.set(parameter.name, serialized);
1477
+ }
1478
+ else if (parameter.in === 'header') {
1479
+ headers[parameter.name] = serialized;
1480
+ }
1481
+ else if (parameter.in === 'cookie') {
1482
+ cookies.push(`${parameter.name}=${encodeURIComponent(serialized)}`);
1483
+ }
1484
+ }
1485
+ const queryText = query.toString();
1486
+ if (queryText)
1487
+ url += `${url.includes('?') ? '&' : '?'}${queryText}`;
1488
+ if (cookies.length > 0)
1489
+ headers.Cookie = cookies.join('; ');
1490
+ const requirement = selectedSecurityRequirement(operation);
1491
+ if (requirement && typeof requirement === 'object') {
1492
+ for (const schemeName of Object.keys(requirement)) {
1493
+ const scheme = spec?.components?.securitySchemes?.[schemeName];
1494
+ const credential = readStoredCredential(schemeName);
1495
+ if (!scheme)
1496
+ continue;
1497
+ if (scheme.type === 'http' && String(scheme.scheme).toLowerCase() === 'bearer' && credential.accessToken) {
1498
+ headers.Authorization = `Bearer ${credential.accessToken}`;
1499
+ }
1500
+ else if (scheme.type === 'http' && String(scheme.scheme).toLowerCase() === 'basic' && credential.username) {
1501
+ const encoded = typeof win.btoa === 'function'
1502
+ ? win.btoa(`${credential.username}:${credential.password ?? ''}`)
1503
+ : `${credential.username}:${credential.password ?? ''}`;
1504
+ headers.Authorization = `Basic ${encoded}`;
1505
+ }
1506
+ else if ((scheme.type === 'oauth2' || scheme.type === 'openIdConnect') && credential.accessToken) {
1507
+ headers.Authorization = `${credential.tokenType || 'Bearer'} ${credential.accessToken}`;
1508
+ }
1509
+ else if (scheme.type === 'apiKey' && credential.apiKey) {
1510
+ if (scheme.in === 'query') {
1511
+ const parsed = safeRuntimeUrl(url);
1512
+ if (parsed) {
1513
+ parsed.searchParams.set(scheme.name || schemeName, credential.apiKey);
1514
+ url = parsed.toString();
1515
+ }
1516
+ }
1517
+ else if (scheme.in === 'cookie') {
1518
+ headers.Cookie = `${scheme.name || schemeName}=${credential.apiKey}`;
1519
+ }
1520
+ else {
1521
+ headers[scheme.name || schemeName] = credential.apiKey;
1522
+ }
1523
+ }
1524
+ }
1525
+ }
1526
+ return { method, url, headers, body };
1527
+ }
1528
+ // Build display-only request samples. Execution always uses the structured
1529
+ // fetch request model, never one of these language snippets.
1530
+ function buildHttpSamples(request) {
1531
+ const { method, url, body } = request;
1325
1532
  const hasBody = body !== null && body !== undefined && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method);
1326
1533
  const jsonPretty = hasBody ? JSON.stringify(body, null, 2) : '';
1327
1534
  const samples = {};
1328
1535
  let curl = `curl -X ${method} "${url}"`;
1536
+ Object.entries(request.headers).forEach(([name, value]) => { curl += ` \\\n -H ${JSON.stringify(`${name}: ${value}`)}`; });
1329
1537
  if (hasBody)
1330
- curl += ` \\\n -H "Content-Type: application/json" \\\n -d '${JSON.stringify(body)}'`;
1538
+ curl += ` \\\n -d '${JSON.stringify(body)}'`;
1331
1539
  samples.curl = curl;
1332
1540
  let js = `const res = await fetch(${JSON.stringify(url)}, {\n method: ${JSON.stringify(method)},`;
1541
+ if (Object.keys(request.headers).length > 0)
1542
+ js += `\n headers: ${JSON.stringify(request.headers, null, 2).split('\n').join('\n ')},`;
1333
1543
  if (hasBody)
1334
- js += `\n headers: { "Content-Type": "application/json" },\n body: JSON.stringify(${jsonPretty.split('\n').join('\n ')}),`;
1544
+ js += `\n body: JSON.stringify(${jsonPretty.split('\n').join('\n ')}),`;
1335
1545
  js += `\n});\nconst data = await res.json();\nconsole.log(data);`;
1336
- samples.javascript = js;
1546
+ samples.typescript = js;
1337
1547
  let py = `import requests\n\nres = requests.${method.toLowerCase()}(\n ${JSON.stringify(url)},`;
1548
+ if (Object.keys(request.headers).length > 0)
1549
+ py += `\n headers=${JSON.stringify(request.headers)},`;
1338
1550
  if (hasBody)
1339
1551
  py += `\n json=${pyLiteral(body)},`;
1340
1552
  py += `\n)\nprint(res.json())`;
1341
1553
  samples.python = py;
1342
1554
  let rust = `use reqwest::Client;\n\nlet client = Client::new();\nlet res = client\n .${method.toLowerCase()}(${JSON.stringify(url)})`;
1555
+ Object.entries(request.headers).forEach(([name, value]) => { rust += `\n .header(${JSON.stringify(name)}, ${JSON.stringify(value)})`; });
1343
1556
  if (hasBody)
1344
1557
  rust += `\n .json(&serde_json::json!(${jsonPretty.split('\n').join('\n ')}))`;
1345
1558
  rust += `\n .send()\n .await?;\nlet body = res.text().await?;\nprintln!("{}", body);`;
1346
1559
  samples.rust = rust;
1560
+ const goImports = hasBody ? '"net/http"\n "strings"' : '"net/http"';
1561
+ const goBody = hasBody ? `strings.NewReader(${JSON.stringify(JSON.stringify(body))})` : 'nil';
1562
+ let go = `package main\n\nimport (\n ${goImports}\n)\n\nfunc main() {\n req, err := http.NewRequest(${JSON.stringify(method)}, ${JSON.stringify(url)}, ${goBody})\n if err != nil { panic(err) }`;
1563
+ Object.entries(request.headers).forEach(([name, value]) => { go += `\n req.Header.Set(${JSON.stringify(name)}, ${JSON.stringify(value)})`; });
1564
+ go += `\n res, err := http.DefaultClient.Do(req)\n if err != nil { panic(err) }\n defer res.Body.Close()\n}`;
1565
+ samples.go = go;
1347
1566
  return samples;
1348
1567
  }
1349
1568
  function renderCodeExamples(endpoint, data) {
1350
- const method = (endpoint.method || 'GET').toUpperCase();
1351
- const url = `${httpBaseUrl()}${endpoint.path || '/'}`;
1352
- const reqContent = data.requestBody?.content;
1353
- const reqSchema = reqContent ? reqContent[Object.keys(reqContent)[0]]?.schema : null;
1354
- const bodyExample = reqSchema ? generateExampleFromSchema(reqSchema) : null;
1355
- const samples = buildHttpSamples(method, url, bodyExample);
1356
- const langs = [['curl', 'cURL'], ['javascript', 'JavaScript'], ['python', 'Python'], ['rust', 'Rust']];
1569
+ const samples = buildHttpSamples(buildStructuredHttpRequest(endpoint, data));
1570
+ const langs = [
1571
+ ['curl', 'cURL', 'bash'],
1572
+ ['typescript', 'TypeScript', 'typescript'],
1573
+ ['rust', 'Rust', 'rust'],
1574
+ ['python', 'Python', 'python'],
1575
+ ['go', 'Go', 'go'],
1576
+ ];
1357
1577
  const wrap = doc.createElement('div');
1358
1578
  wrap.className = 'http-code-samples';
1359
1579
  const tabs = doc.createElement('div');
1360
1580
  tabs.className = 'code-tabs';
1361
1581
  const contents = doc.createElement('div');
1362
1582
  contents.className = 'code-contents';
1363
- langs.forEach(([key, label], index) => {
1583
+ langs.forEach(([key, label, language], index) => {
1364
1584
  const tab = doc.createElement('button');
1365
1585
  tab.type = 'button';
1366
1586
  tab.className = `code-tab${index === 0 ? ' active' : ''}`;
@@ -1369,7 +1589,10 @@ function renderCodeExamples(endpoint, data) {
1369
1589
  content.className = `code-content${index === 0 ? ' active' : ''}`;
1370
1590
  const pre = doc.createElement('pre');
1371
1591
  pre.className = 'http-code-sample-pre';
1372
- pre.textContent = samples[key];
1592
+ const code = doc.createElement('code');
1593
+ code.className = `language-${language}`;
1594
+ code.textContent = samples[key];
1595
+ pre.appendChild(code);
1373
1596
  content.appendChild(pre);
1374
1597
  tab.onclick = () => {
1375
1598
  tabs.querySelectorAll('.code-tab').forEach((t) => t.classList.remove('active'));
@@ -1400,6 +1623,71 @@ function renderCodeExamples(endpoint, data) {
1400
1623
  wrap.appendChild(contents);
1401
1624
  return wrap;
1402
1625
  }
1626
+ async function executeStructuredHttpRequest(endpoint, operation, button, result) {
1627
+ button.disabled = true;
1628
+ result.textContent = 'Sending request…';
1629
+ try {
1630
+ await refreshCredentialsBeforeRequest(operation);
1631
+ const request = buildStructuredHttpRequest(endpoint, operation);
1632
+ let status;
1633
+ let statusText;
1634
+ let responseHeaders = {};
1635
+ let responseBody;
1636
+ if (tryItConfig.mode === 'proxy') {
1637
+ const proxyResponse = await win.fetch?.(tryItConfig.proxyUrl, {
1638
+ method: 'POST',
1639
+ headers: { 'Content-Type': 'application/json' },
1640
+ body: JSON.stringify(request),
1641
+ });
1642
+ if (!proxyResponse)
1643
+ throw new Error('Fetch is not available in this browser.');
1644
+ const payload = await proxyResponse.json();
1645
+ if (!proxyResponse.ok)
1646
+ throw new Error(payload?.detail || payload?.message || `Proxy failed with ${proxyResponse.status}.`);
1647
+ status = payload.status;
1648
+ statusText = payload.statusText ?? '';
1649
+ responseHeaders = payload.headers ?? {};
1650
+ responseBody = payload.body ?? '';
1651
+ }
1652
+ else {
1653
+ const response = await win.fetch?.(request.url, {
1654
+ method: request.method,
1655
+ headers: request.headers,
1656
+ body: request.body === null || request.body === undefined ? undefined : JSON.stringify(request.body),
1657
+ credentials: 'include',
1658
+ });
1659
+ if (!response)
1660
+ throw new Error('Fetch is not available in this browser.');
1661
+ status = response.status;
1662
+ statusText = response.statusText;
1663
+ response.headers?.forEach?.((value, name) => { responseHeaders[name] = value; });
1664
+ responseBody = await response.text();
1665
+ }
1666
+ const headerText = Object.entries(responseHeaders).map(([name, value]) => `${name}: ${value}`).join('\n');
1667
+ result.textContent = `${status} ${statusText}`.trim() + `${headerText ? `\n${headerText}` : ''}${responseBody ? `\n\n${responseBody}` : ''}`;
1668
+ }
1669
+ catch (error) {
1670
+ result.textContent = `Request failed: ${String(error?.message ?? error)}`;
1671
+ }
1672
+ finally {
1673
+ button.disabled = false;
1674
+ }
1675
+ }
1676
+ function renderHttpExecutor(endpoint, operation) {
1677
+ const wrapper = doc.createElement('div');
1678
+ wrapper.className = 'http-try';
1679
+ const button = doc.createElement('button');
1680
+ button.type = 'button';
1681
+ button.className = 'http-try-run';
1682
+ button.textContent = 'Run request';
1683
+ const result = doc.createElement('pre');
1684
+ result.className = 'http-try-result';
1685
+ result.textContent = tryItConfig.mode === 'proxy' ? 'Ready · server proxy' : 'Ready · browser fetch';
1686
+ button.onclick = () => { void executeStructuredHttpRequest(endpoint, operation, button, result); };
1687
+ wrapper.appendChild(button);
1688
+ wrapper.appendChild(result);
1689
+ return wrapper;
1690
+ }
1403
1691
  // Append a JSON example block to a container.
1404
1692
  // Syntax-highlight a JSON value into HTML using the .sample-json token classes.
1405
1693
  function highlightJsonHtml(value) {
@@ -1420,7 +1708,7 @@ function highlightJsonHtml(value) {
1420
1708
  }
1421
1709
  // Right-panel response samples: one status tab per response, each showing the
1422
1710
  // generated JSON example (colorized) — ReDoc's response sample switcher.
1423
- function renderResponseSamples(responses) {
1711
+ function renderResponseSamples(responses, endpoint) {
1424
1712
  const wrap = doc.createElement('div');
1425
1713
  const tabs = doc.createElement('div');
1426
1714
  tabs.className = 'sample-tabs';
@@ -1438,22 +1726,46 @@ function renderResponseSamples(responses) {
1438
1726
  tab.textContent = status;
1439
1727
  const content = doc.createElement('div');
1440
1728
  content.className = `sample-content${index === 0 ? ' active' : ''}`;
1441
- const ct = resp.content ? Object.keys(resp.content)[0] : null;
1442
- const schema = ct ? resp.content[ct]?.schema : null;
1443
- const example = schema ? generateExampleFromSchema(schema) : null;
1444
- if (example !== null && example !== undefined) {
1445
- if (ct) {
1446
- const typeLine = doc.createElement('div');
1447
- typeLine.className = 'sample-content-type';
1448
- typeLine.textContent = ct;
1449
- content.appendChild(typeLine);
1729
+ const operationKey = `${String(endpoint.method).toUpperCase()} ${endpoint.path}`;
1730
+ const mediaEntries = Object.entries(resp.content ?? {});
1731
+ mediaEntries.forEach(([mediaType, media]) => {
1732
+ const mediaBlock = doc.createElement('div');
1733
+ mediaBlock.className = 'response-media-sample';
1734
+ const typeLine = doc.createElement('div');
1735
+ typeLine.className = 'sample-content-type';
1736
+ typeLine.textContent = mediaType;
1737
+ mediaBlock.appendChild(typeLine);
1738
+ const prepared = serializedResponseExamples?.[operationKey]?.[status]?.[mediaType];
1739
+ const isToonl = mediaType.toLowerCase().includes('toonl');
1740
+ const examples = prepared?.length
1741
+ ? prepared
1742
+ : isToonl
1743
+ ? [{ name: 'example required', language: 'text', value: 'TOONL examples require an explicit array of flat object records.', error: true }]
1744
+ : media?.schema
1745
+ ? [{ name: 'generated', language: 'json', value: JSON.stringify(generateExampleFromSchema(media.schema), null, 2) }]
1746
+ : [];
1747
+ examples.forEach(example => {
1748
+ const label = doc.createElement('div');
1749
+ label.className = 'response-example-name';
1750
+ label.textContent = example.summary || example.name;
1751
+ mediaBlock.appendChild(label);
1752
+ const pre = doc.createElement('pre');
1753
+ pre.className = `sample-code${example.error ? ' sample-code-error' : ''}`;
1754
+ const code = doc.createElement('code');
1755
+ code.className = `language-${example.language || 'text'}`;
1756
+ code.textContent = example.value;
1757
+ pre.appendChild(code);
1758
+ mediaBlock.appendChild(pre);
1759
+ });
1760
+ if (examples.length === 0) {
1761
+ const empty = doc.createElement('div');
1762
+ empty.className = 'no-example';
1763
+ empty.textContent = 'No example provided.';
1764
+ mediaBlock.appendChild(empty);
1450
1765
  }
1451
- const pre = doc.createElement('pre');
1452
- pre.className = 'sample-json';
1453
- pre.innerHTML = highlightJsonHtml(example);
1454
- content.appendChild(pre);
1455
- }
1456
- else {
1766
+ content.appendChild(mediaBlock);
1767
+ });
1768
+ if (mediaEntries.length === 0) {
1457
1769
  const empty = doc.createElement('div');
1458
1770
  empty.className = 'no-example';
1459
1771
  empty.textContent = resp.description || 'No response body.';
@@ -1475,6 +1787,7 @@ function renderResponseSamples(responses) {
1475
1787
  function renderSchemaTree(parent, schema, depth = 0) {
1476
1788
  if (!schema)
1477
1789
  return;
1790
+ schema = resolveSchema(schema);
1478
1791
  const div = doc.createElement('div');
1479
1792
  div.style.paddingLeft = depth > 0 ? `${depth * 12}px` : '0';
1480
1793
  div.style.marginTop = depth === 0 ? '8px' : '0';
@@ -1482,7 +1795,7 @@ function renderSchemaTree(parent, schema, depth = 0) {
1482
1795
  const refName = String(schema.$ref).split('/').pop();
1483
1796
  const row = doc.createElement('div');
1484
1797
  row.style.padding = '4px 0';
1485
- row.innerHTML = `<span style="color: #888; font-style: italic;">$ref: ${esc(refName)}</span>`;
1798
+ row.innerHTML = `<span style="color: #888; font-style: italic;">Unresolved schema reference: ${esc(refName)}</span>`;
1486
1799
  div.appendChild(row);
1487
1800
  }
1488
1801
  else if (schema.type === 'object' && schema.properties) {
@@ -1603,16 +1916,44 @@ function renderResponseAccordion(status, resp, openByDefault) {
1603
1916
  body.appendChild(block);
1604
1917
  }
1605
1918
  const content = resp.content;
1606
- const contentType = content ? Object.keys(content)[0] : null;
1607
- const schema = contentType ? content[contentType]?.schema : null;
1608
- if (schema) {
1919
+ for (const [contentType, media] of Object.entries(content ?? {})) {
1609
1920
  const block = doc.createElement('div');
1610
1921
  block.className = 'response-block';
1611
1922
  const sub = doc.createElement('div');
1612
1923
  sub.className = 'response-subhead';
1613
- sub.textContent = `Response Body${contentType ? ` · ${contentType}` : ''}`;
1924
+ sub.textContent = `Response Body · ${contentType}`;
1614
1925
  block.appendChild(sub);
1615
- renderSchemaTree(block, schema);
1926
+ if (media?.schema)
1927
+ renderSchemaTree(block, media.schema);
1928
+ else {
1929
+ const note = doc.createElement('div');
1930
+ note.className = 'response-desc-only';
1931
+ note.textContent = media?.examples || 'example' in (media ?? {}) ? 'Example available' : 'No schema provided.';
1932
+ block.appendChild(note);
1933
+ }
1934
+ body.appendChild(block);
1935
+ }
1936
+ if (resp.links && Object.keys(resp.links).length > 0) {
1937
+ const block = doc.createElement('div');
1938
+ block.className = 'response-block response-links';
1939
+ const sub = doc.createElement('div');
1940
+ sub.className = 'response-subhead';
1941
+ sub.textContent = 'Response links';
1942
+ block.appendChild(sub);
1943
+ Object.entries(resp.links).forEach(([name, rawLink]) => {
1944
+ const link = resolveSchema(rawLink);
1945
+ const card = doc.createElement('div');
1946
+ card.className = 'response-link';
1947
+ const target = link?.operationId || link?.operationRef || 'Linked operation';
1948
+ card.innerHTML = `<strong>${esc(name)}</strong><code>${esc(target)}</code>${link?.description ? `<span>${esc(link.description)}</span>` : ''}`;
1949
+ if (link?.parameters && Object.keys(link.parameters).length > 0) {
1950
+ const parameters = doc.createElement('pre');
1951
+ parameters.className = 'response-link-parameters';
1952
+ parameters.textContent = JSON.stringify(link.parameters, null, 2);
1953
+ card.appendChild(parameters);
1954
+ }
1955
+ block.appendChild(card);
1956
+ });
1616
1957
  body.appendChild(block);
1617
1958
  }
1618
1959
  if (!body.children.length) {
@@ -1626,6 +1967,51 @@ function renderResponseAccordion(status, resp, openByDefault) {
1626
1967
  acc.appendChild(body);
1627
1968
  return acc;
1628
1969
  }
1970
+ const OPENAPI_HTTP_METHODS = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'];
1971
+ function appendPathItemContract(container, expression, rawPathItem) {
1972
+ const pathItem = resolveSchema(rawPathItem);
1973
+ const contract = doc.createElement('div');
1974
+ contract.className = 'async-contract';
1975
+ const expressionLine = doc.createElement('code');
1976
+ expressionLine.className = 'async-contract-expression';
1977
+ expressionLine.textContent = expression;
1978
+ contract.appendChild(expressionLine);
1979
+ OPENAPI_HTTP_METHODS.forEach(method => {
1980
+ const operation = pathItem?.[method];
1981
+ if (!operation)
1982
+ return;
1983
+ const row = doc.createElement('div');
1984
+ row.className = 'async-contract-operation';
1985
+ const requestTypes = Object.keys(operation.requestBody?.content ?? {});
1986
+ const statuses = Object.keys(operation.responses ?? {});
1987
+ row.innerHTML = `<span class="badge badge-${method}">${method.toUpperCase()}</span><strong>${esc(operation.summary || operation.operationId || expression)}</strong>${requestTypes.length ? `<span>body: ${esc(requestTypes.join(', '))}</span>` : ''}${statuses.length ? `<span>responses: ${esc(statuses.join(', '))}</span>` : ''}`;
1988
+ contract.appendChild(row);
1989
+ });
1990
+ container.appendChild(contract);
1991
+ }
1992
+ function renderAsyncContracts(title, entries, callbacks = false) {
1993
+ if (!entries || Object.keys(entries).length === 0)
1994
+ return null;
1995
+ const section = doc.createElement('section');
1996
+ section.className = `async-contracts ${title.toLowerCase()}-section`;
1997
+ section.innerHTML = `<div class="subsection-label">${esc(title.toUpperCase())}</div><h2>${esc(title)}</h2><p class="async-contract-note">Read-only contract. Raffel never executes callbacks or webhooks from this page.</p>`;
1998
+ Object.entries(entries).forEach(([name, rawEntry]) => {
1999
+ const entry = resolveSchema(rawEntry);
2000
+ const group = doc.createElement('div');
2001
+ group.className = 'async-contract-group';
2002
+ const heading = doc.createElement('h3');
2003
+ heading.textContent = name;
2004
+ group.appendChild(heading);
2005
+ if (callbacks) {
2006
+ Object.entries(entry ?? {}).forEach(([expression, pathItem]) => appendPathItemContract(group, expression, pathItem));
2007
+ }
2008
+ else {
2009
+ appendPathItemContract(group, name, entry);
2010
+ }
2011
+ section.appendChild(group);
2012
+ });
2013
+ return section;
2014
+ }
1629
2015
  function renderEndpointDetails(endpoint) {
1630
2016
  const container = doc.createElement('div');
1631
2017
  container.className = 'endpoint-details';
@@ -1669,6 +2055,32 @@ function renderEndpointDetails(endpoint) {
1669
2055
  const bodyContent = reqBody.content[contentType];
1670
2056
  if (bodyContent?.schema)
1671
2057
  renderSchemaTree(section, bodyContent.schema);
2058
+ const requestExamples = Object.entries(bodyContent?.examples ?? {});
2059
+ if (requestExamples.length > 0 || bodyContent && 'example' in bodyContent) {
2060
+ const examplesTitle = doc.createElement('div');
2061
+ examplesTitle.className = 'response-subhead request-examples-title';
2062
+ examplesTitle.textContent = 'Request examples';
2063
+ section.appendChild(examplesTitle);
2064
+ const examples = requestExamples.length > 0
2065
+ ? requestExamples
2066
+ : [['example', { value: bodyContent.example }]];
2067
+ examples.forEach(([name, example]) => {
2068
+ const wrapper = doc.createElement('div');
2069
+ wrapper.className = 'request-example';
2070
+ const label = doc.createElement('div');
2071
+ label.className = 'response-example-name';
2072
+ label.textContent = example?.summary || name;
2073
+ const pre = doc.createElement('pre');
2074
+ pre.className = 'sample-code';
2075
+ const code = doc.createElement('code');
2076
+ code.className = 'language-json';
2077
+ code.textContent = JSON.stringify(example?.value, null, 2);
2078
+ pre.appendChild(code);
2079
+ wrapper.appendChild(label);
2080
+ wrapper.appendChild(pre);
2081
+ section.appendChild(wrapper);
2082
+ });
2083
+ }
1672
2084
  left.appendChild(section);
1673
2085
  }
1674
2086
  const responses = data.responses;
@@ -1691,18 +2103,23 @@ function renderEndpointDetails(endpoint) {
1691
2103
  }
1692
2104
  left.appendChild(section);
1693
2105
  }
1694
- // Right column: request samples (cURL / JavaScript / Python / Rust).
2106
+ const callbacks = renderAsyncContracts('Callbacks', data.callbacks ?? {}, true);
2107
+ if (callbacks)
2108
+ left.appendChild(callbacks);
2109
+ // Right column: request samples (cURL / TypeScript / Rust / Python / Go).
1695
2110
  const samplesSection = doc.createElement('div');
1696
2111
  samplesSection.className = 'endpoint-right-section';
1697
2112
  samplesSection.innerHTML = '<div class="endpoint-right-header">Request samples</div>';
1698
2113
  samplesSection.appendChild(renderCodeExamples(endpoint, data));
2114
+ if (tryItConfig.enabled)
2115
+ samplesSection.appendChild(renderHttpExecutor(endpoint, data));
1699
2116
  right.appendChild(samplesSection);
1700
2117
  // Right column: response samples (JSON examples per status code).
1701
2118
  if (responses && Object.keys(responses).length > 0) {
1702
2119
  const respSamples = doc.createElement('div');
1703
2120
  respSamples.className = 'endpoint-right-section';
1704
2121
  respSamples.innerHTML = '<div class="endpoint-right-header">Response samples</div>';
1705
- respSamples.appendChild(renderResponseSamples(responses));
2122
+ respSamples.appendChild(renderResponseSamples(responses, endpoint));
1706
2123
  right.appendChild(respSamples);
1707
2124
  }
1708
2125
  content.appendChild(left);
@@ -1789,8 +2206,13 @@ function resolveMessagePayload(message) {
1789
2206
  function resolveSchema(schema) {
1790
2207
  if (!schema)
1791
2208
  return null;
1792
- if (schema.$ref)
1793
- return resolveRef(schema.$ref) ?? schema;
2209
+ if (schema.$ref) {
2210
+ const resolved = resolveRef(schema.$ref);
2211
+ if (!resolved || typeof resolved !== 'object')
2212
+ return schema;
2213
+ const { $ref: _ref, ...siblings } = schema;
2214
+ return { ...resolved, ...siblings };
2215
+ }
1794
2216
  return schema;
1795
2217
  }
1796
2218
  function resolveRef(ref) {
@@ -1799,6 +2221,683 @@ function resolveRef(ref) {
1799
2221
  return null;
1800
2222
  return pointer.slice(2).split('/').map(part => part.replace(/~1/g, '/').replace(/~0/g, '~')).reduce((current, part) => current?.[part], spec);
1801
2223
  }
2224
+ function credentialStorageKey(schemeName) {
2225
+ return `raffel-docs:auth:${encodeURIComponent(selectedEnvironmentUrl)}:${encodeURIComponent(schemeName)}`;
2226
+ }
2227
+ function readStoredCredential(schemeName) {
2228
+ try {
2229
+ const value = win.sessionStorage?.getItem?.(credentialStorageKey(schemeName));
2230
+ return value ? JSON.parse(value) : {};
2231
+ }
2232
+ catch {
2233
+ return {};
2234
+ }
2235
+ }
2236
+ function storeCredential(schemeName, value) {
2237
+ try {
2238
+ win.sessionStorage?.setItem?.(credentialStorageKey(schemeName), JSON.stringify(value));
2239
+ }
2240
+ catch { /* unavailable storage */ }
2241
+ }
2242
+ function authSchemeKind(scheme) {
2243
+ if (scheme?.type === 'http' && String(scheme.scheme).toLowerCase() === 'bearer')
2244
+ return 'Bearer token';
2245
+ if (scheme?.type === 'http' && String(scheme.scheme).toLowerCase() === 'basic')
2246
+ return 'HTTP Basic';
2247
+ if (scheme?.type === 'apiKey')
2248
+ return 'API key';
2249
+ if (scheme?.type === 'oauth2')
2250
+ return 'OAuth 2.0';
2251
+ if (scheme?.type === 'openIdConnect')
2252
+ return 'OpenID Connect';
2253
+ if (scheme?.type === 'mutualTLS')
2254
+ return 'Mutual TLS';
2255
+ return String(scheme?.type ?? 'Authentication');
2256
+ }
2257
+ function findHttpOperation(operationId) {
2258
+ for (const [path, pathItem] of Object.entries(spec.paths ?? {})) {
2259
+ for (const [method, operation] of Object.entries(pathItem ?? {})) {
2260
+ if (operation?.operationId === operationId)
2261
+ return { path, method: method.toUpperCase(), operation };
2262
+ }
2263
+ }
2264
+ return null;
2265
+ }
2266
+ function operationRequestExample(operation) {
2267
+ const media = Object.values(operation?.requestBody?.content ?? {})[0];
2268
+ if (!media)
2269
+ return {};
2270
+ if ('example' in media)
2271
+ return media.example;
2272
+ const named = Object.values(media.examples ?? {})[0];
2273
+ if (named && typeof named === 'object' && 'value' in named)
2274
+ return named.value;
2275
+ return generateExampleFromSchema(media.schema) ?? {};
2276
+ }
2277
+ function readJsonPointer(value, pointer) {
2278
+ const path = String(pointer ?? '');
2279
+ if (path === '')
2280
+ return value;
2281
+ if (!path.startsWith('/'))
2282
+ return undefined;
2283
+ return path.slice(1).split('/').map(part => part.replace(/~1/g, '/').replace(/~0/g, '~')).reduce((current, part) => current?.[part], value);
2284
+ }
2285
+ function tokenExpiry(response, pointers) {
2286
+ const expiresAt = readJsonPointer(response, pointers?.expiresAt);
2287
+ if (expiresAt !== undefined) {
2288
+ if (typeof expiresAt === 'number')
2289
+ return expiresAt > 1e12 ? expiresAt : expiresAt * 1000;
2290
+ const parsed = Date.parse(String(expiresAt));
2291
+ if (Number.isFinite(parsed))
2292
+ return parsed;
2293
+ }
2294
+ const expiresIn = Number(readJsonPointer(response, pointers?.expiresIn));
2295
+ return Number.isFinite(expiresIn) && expiresIn > 0 ? Date.now() + expiresIn * 1000 : undefined;
2296
+ }
2297
+ async function executeJsonHttpRequest(url, init = {}) {
2298
+ if (tryItConfig.mode === 'proxy') {
2299
+ const proxyResponse = await win.fetch?.(tryItConfig.proxyUrl, {
2300
+ method: 'POST',
2301
+ headers: { 'Content-Type': 'application/json' },
2302
+ body: JSON.stringify({
2303
+ url,
2304
+ method: String(init.method ?? 'GET').toUpperCase(),
2305
+ headers: init.headers ?? {},
2306
+ body: init.body,
2307
+ }),
2308
+ });
2309
+ if (!proxyResponse)
2310
+ throw new Error('Fetch is not available in this browser.');
2311
+ const envelope = await proxyResponse.json();
2312
+ if (!proxyResponse.ok) {
2313
+ throw new Error(envelope?.detail || envelope?.message || `Proxy failed with ${proxyResponse.status}.`);
2314
+ }
2315
+ const status = Number(envelope?.status ?? 502);
2316
+ let payload = {};
2317
+ try {
2318
+ payload = JSON.parse(String(envelope?.body ?? ''));
2319
+ }
2320
+ catch { /* callers report a missing token or discovery field */ }
2321
+ return { ok: status >= 200 && status < 300, status, payload };
2322
+ }
2323
+ const response = await win.fetch?.(url, init);
2324
+ if (!response)
2325
+ throw new Error('Fetch is not available in this browser.');
2326
+ return { ok: response.ok, status: response.status, payload: await response.json() };
2327
+ }
2328
+ async function requestOperationToken(schemeName, recipe, operation, rawBody, status) {
2329
+ try {
2330
+ status.textContent = 'Requesting token…';
2331
+ const parsedBody = rawBody.trim() ? JSON.parse(rawBody) : undefined;
2332
+ const headers = parsedBody === undefined ? {} : { 'Content-Type': 'application/json' };
2333
+ const response = await executeJsonHttpRequest(`${httpBaseUrl()}${operation.path}`, {
2334
+ method: operation.method,
2335
+ headers,
2336
+ body: parsedBody === undefined ? undefined : JSON.stringify(parsedBody),
2337
+ });
2338
+ const payload = response.payload;
2339
+ if (!response.ok)
2340
+ throw new Error(payload?.message || `Token request failed with ${response.status}.`);
2341
+ const accessToken = readJsonPointer(payload, recipe.tokenPointers?.accessToken);
2342
+ if (accessToken === undefined || accessToken === null || accessToken === '') {
2343
+ throw new Error('The access-token JSON Pointer did not match the response.');
2344
+ }
2345
+ storeCredential(schemeName, {
2346
+ accessToken: String(accessToken),
2347
+ refreshToken: readJsonPointer(payload, recipe.tokenPointers?.refreshToken),
2348
+ expiresAt: tokenExpiry(payload, recipe.tokenPointers),
2349
+ tokenType: readJsonPointer(payload, recipe.tokenPointers?.tokenType) ?? 'Bearer',
2350
+ });
2351
+ render();
2352
+ }
2353
+ catch (error) {
2354
+ status.textContent = String(error?.message ?? error);
2355
+ status.classList.add('auth-status-error');
2356
+ }
2357
+ }
2358
+ function oauthFlowLabel(flowName) {
2359
+ if (flowName === 'authorizationCode')
2360
+ return 'Authorization code';
2361
+ if (flowName === 'clientCredentials')
2362
+ return 'Client credentials';
2363
+ if (flowName === 'password')
2364
+ return 'Resource owner password';
2365
+ return 'Implicit';
2366
+ }
2367
+ function oauthScopeValue(flow) {
2368
+ return Object.keys(flow?.scopes ?? {}).join(' ');
2369
+ }
2370
+ async function requestOAuthToken(schemeName, flowName, flow, fieldset, status) {
2371
+ try {
2372
+ const value = (name) => String(fieldset.querySelector(`input[name="${name}"]`)?.value ?? '');
2373
+ const body = new URLSearchParams();
2374
+ body.set('grant_type', flowName === 'clientCredentials' ? 'client_credentials' : 'password');
2375
+ if (value('clientId'))
2376
+ body.set('client_id', value('clientId'));
2377
+ if (value('clientSecret'))
2378
+ body.set('client_secret', value('clientSecret'));
2379
+ if (value('scopes'))
2380
+ body.set('scope', value('scopes'));
2381
+ if (flowName === 'password') {
2382
+ body.set('username', value('username'));
2383
+ body.set('password', value('password'));
2384
+ }
2385
+ status.textContent = 'Requesting token…';
2386
+ const response = await executeJsonHttpRequest(flow.tokenUrl, {
2387
+ method: 'POST',
2388
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
2389
+ body: body.toString(),
2390
+ });
2391
+ const payload = response.payload;
2392
+ if (!response.ok)
2393
+ throw new Error(payload?.error_description || payload?.error || `Token request failed with ${response.status}.`);
2394
+ if (!payload.access_token)
2395
+ throw new Error('The token response did not include access_token.');
2396
+ storeCredential(schemeName, {
2397
+ accessToken: String(payload.access_token),
2398
+ refreshToken: payload.refresh_token,
2399
+ expiresAt: Number(payload.expires_in) > 0 ? Date.now() + Number(payload.expires_in) * 1000 : undefined,
2400
+ tokenType: payload.token_type || 'Bearer',
2401
+ clientId: value('clientId'),
2402
+ clientSecret: value('clientSecret'),
2403
+ scopes: value('scopes'),
2404
+ tokenUrl: flow.tokenUrl,
2405
+ refreshUrl: flow.refreshUrl,
2406
+ });
2407
+ render();
2408
+ }
2409
+ catch (error) {
2410
+ status.textContent = String(error?.message ?? error);
2411
+ status.classList.add('auth-status-error');
2412
+ }
2413
+ }
2414
+ function randomOAuthState() {
2415
+ const values = new Uint32Array(4);
2416
+ try {
2417
+ (globalThis.crypto ?? win.crypto)?.getRandomValues?.(values);
2418
+ }
2419
+ catch {
2420
+ values[0] = Date.now();
2421
+ }
2422
+ return Array.from(values).map(value => value.toString(16)).join('') || String(Date.now());
2423
+ }
2424
+ function base64Url(bytes) {
2425
+ let binary = '';
2426
+ bytes.forEach(byte => { binary += String.fromCharCode(byte); });
2427
+ return win.btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
2428
+ }
2429
+ function randomOAuthVerifier() {
2430
+ const values = new Uint8Array(32);
2431
+ const cryptoApi = globalThis.crypto ?? win.crypto;
2432
+ try {
2433
+ cryptoApi?.getRandomValues?.(values);
2434
+ }
2435
+ catch { /* use the state-based fallback below */ }
2436
+ if (values.every(value => value === 0)) {
2437
+ const fallback = `${randomOAuthState()}${randomOAuthState()}`;
2438
+ values.forEach((_, index) => { values[index] = fallback.charCodeAt(index % fallback.length); });
2439
+ }
2440
+ return base64Url(values);
2441
+ }
2442
+ async function oauthCodeChallenge(verifier) {
2443
+ const cryptoApi = globalThis.crypto ?? win.crypto;
2444
+ if (!cryptoApi?.subtle?.digest)
2445
+ return { challenge: verifier, method: 'plain' };
2446
+ try {
2447
+ const bytes = Uint8Array.from(verifier, character => character.charCodeAt(0));
2448
+ const digest = await cryptoApi.subtle.digest('SHA-256', bytes);
2449
+ return { challenge: base64Url(new Uint8Array(digest)), method: 'S256' };
2450
+ }
2451
+ catch {
2452
+ return { challenge: verifier, method: 'plain' };
2453
+ }
2454
+ }
2455
+ async function startOAuthAuthorization(schemeName, flowName, flow, fieldset) {
2456
+ const value = (name) => String(fieldset.querySelector(`input[name="${name}"]`)?.value ?? '');
2457
+ const redirectUri = `${String(win.location?.href ?? '').split('#')[0].split('?')[0]}?raffel_oauth_callback=1`;
2458
+ const state = randomOAuthState();
2459
+ const authorization = new URL(flow.authorizationUrl);
2460
+ const popup = win.open?.('', 'raffel-oauth', 'popup,width=540,height=720');
2461
+ authorization.searchParams.set('response_type', flowName === 'implicit' ? 'token' : 'code');
2462
+ authorization.searchParams.set('client_id', value('clientId'));
2463
+ authorization.searchParams.set('redirect_uri', redirectUri);
2464
+ authorization.searchParams.set('state', state);
2465
+ if (value('scopes'))
2466
+ authorization.searchParams.set('scope', value('scopes'));
2467
+ let codeVerifier;
2468
+ if (flowName !== 'implicit') {
2469
+ codeVerifier = randomOAuthVerifier();
2470
+ const pkce = await oauthCodeChallenge(codeVerifier);
2471
+ authorization.searchParams.set('code_challenge', pkce.challenge);
2472
+ authorization.searchParams.set('code_challenge_method', pkce.method);
2473
+ }
2474
+ try {
2475
+ win.sessionStorage?.setItem?.(`${credentialStorageKey(schemeName)}:pending`, JSON.stringify({
2476
+ schemeName, flowName, state, redirectUri, tokenUrl: flow.tokenUrl,
2477
+ clientId: value('clientId'), clientSecret: value('clientSecret'), scopes: value('scopes'),
2478
+ codeVerifier,
2479
+ }));
2480
+ }
2481
+ catch { /* unavailable storage */ }
2482
+ if (popup) {
2483
+ if (typeof popup.location?.replace === 'function')
2484
+ popup.location.replace(authorization.toString());
2485
+ else
2486
+ popup.location = authorization.toString();
2487
+ }
2488
+ else {
2489
+ win.location?.assign?.(authorization.toString());
2490
+ }
2491
+ }
2492
+ function appendOAuthFlowFields(form, schemeName, scheme, stored) {
2493
+ Object.entries(scheme.flows ?? {}).forEach(([flowName, flow]) => {
2494
+ const fieldset = doc.createElement('fieldset');
2495
+ fieldset.className = 'auth-oauth-flow';
2496
+ fieldset.dataset.oauthFlow = flowName;
2497
+ const legend = doc.createElement('legend');
2498
+ legend.textContent = oauthFlowLabel(flowName);
2499
+ fieldset.appendChild(legend);
2500
+ fieldset.appendChild(createAuthInput('clientId', 'Client ID', 'text', stored.clientId ?? ''));
2501
+ if (flowName !== 'implicit')
2502
+ fieldset.appendChild(createAuthInput('clientSecret', 'Client secret', 'password', stored.clientSecret ?? ''));
2503
+ if (flowName === 'password') {
2504
+ fieldset.appendChild(createAuthInput('username', 'Username'));
2505
+ fieldset.appendChild(createAuthInput('password', 'Password', 'password'));
2506
+ }
2507
+ fieldset.appendChild(createAuthInput('scopes', 'Scopes', 'text', stored.scopes ?? oauthScopeValue(flow)));
2508
+ const actions = doc.createElement('div');
2509
+ actions.className = 'auth-actions';
2510
+ const button = doc.createElement('button');
2511
+ button.type = 'button';
2512
+ const status = doc.createElement('span');
2513
+ status.className = 'auth-status';
2514
+ status.textContent = stored.accessToken ? 'Ready' : 'Not authenticated';
2515
+ if (flowName === 'clientCredentials' || flowName === 'password') {
2516
+ button.className = 'auth-oauth-token';
2517
+ button.textContent = 'Request token';
2518
+ button.onclick = () => { void requestOAuthToken(schemeName, flowName, flow, fieldset, status); };
2519
+ }
2520
+ else {
2521
+ button.className = 'auth-oauth-authorize';
2522
+ button.textContent = 'Authorize in popup';
2523
+ button.onclick = () => { void startOAuthAuthorization(schemeName, flowName, flow, fieldset); };
2524
+ }
2525
+ actions.appendChild(button);
2526
+ actions.appendChild(status);
2527
+ fieldset.appendChild(actions);
2528
+ form.appendChild(fieldset);
2529
+ });
2530
+ }
2531
+ function appendOidcFields(form, schemeName, scheme, stored) {
2532
+ const fieldset = doc.createElement('fieldset');
2533
+ fieldset.className = 'auth-oauth-flow';
2534
+ fieldset.dataset.oauthFlow = 'oidc';
2535
+ const legend = doc.createElement('legend');
2536
+ legend.textContent = 'OpenID discovery';
2537
+ fieldset.appendChild(legend);
2538
+ fieldset.appendChild(createAuthInput('clientId', 'Client ID', 'text', stored.clientId ?? ''));
2539
+ fieldset.appendChild(createAuthInput('clientSecret', 'Client secret', 'password', stored.clientSecret ?? ''));
2540
+ fieldset.appendChild(createAuthInput('scopes', 'Scopes', 'text', stored.scopes ?? 'openid profile'));
2541
+ const actions = doc.createElement('div');
2542
+ actions.className = 'auth-actions';
2543
+ const button = doc.createElement('button');
2544
+ button.type = 'button';
2545
+ button.className = 'auth-oidc-authorize';
2546
+ button.textContent = 'Discover and authorize';
2547
+ const status = doc.createElement('span');
2548
+ status.className = 'auth-status';
2549
+ status.textContent = stored.accessToken ? 'Ready' : 'Not authenticated';
2550
+ button.onclick = async () => {
2551
+ try {
2552
+ status.textContent = 'Loading discovery…';
2553
+ const response = await executeJsonHttpRequest(scheme.openIdConnectUrl);
2554
+ if (!response.ok)
2555
+ throw new Error('OpenID discovery failed.');
2556
+ const discovery = response.payload;
2557
+ await startOAuthAuthorization(schemeName, 'authorizationCode', {
2558
+ authorizationUrl: discovery.authorization_endpoint,
2559
+ tokenUrl: discovery.token_endpoint,
2560
+ scopes: {},
2561
+ }, fieldset);
2562
+ status.textContent = 'Authorization opened';
2563
+ }
2564
+ catch (error) {
2565
+ status.textContent = String(error?.message ?? error);
2566
+ status.classList.add('auth-status-error');
2567
+ }
2568
+ };
2569
+ actions.appendChild(button);
2570
+ actions.appendChild(status);
2571
+ fieldset.appendChild(actions);
2572
+ form.appendChild(fieldset);
2573
+ }
2574
+ function oauthCallbackParams() {
2575
+ const query = new URLSearchParams(String(win.location?.search ?? ''));
2576
+ const fragment = new URLSearchParams(String(win.location?.hash ?? '').replace(/^#/, ''));
2577
+ if (query.has('code') || query.has('error'))
2578
+ return query;
2579
+ if (fragment.has('access_token') || fragment.has('error'))
2580
+ return fragment;
2581
+ return null;
2582
+ }
2583
+ function findPendingOAuth(state) {
2584
+ if (!state)
2585
+ return null;
2586
+ for (const schemeName of Object.keys(spec?.components?.securitySchemes ?? {})) {
2587
+ try {
2588
+ const key = `${credentialStorageKey(schemeName)}:pending`;
2589
+ const raw = win.sessionStorage?.getItem?.(key);
2590
+ if (!raw)
2591
+ continue;
2592
+ const pending = JSON.parse(raw);
2593
+ if (pending.state === state)
2594
+ return { ...pending, key };
2595
+ }
2596
+ catch { /* ignore malformed pending state */ }
2597
+ }
2598
+ return null;
2599
+ }
2600
+ async function finishOAuthAuthorization(params) {
2601
+ const pending = findPendingOAuth(params.get('state'));
2602
+ if (!pending)
2603
+ return;
2604
+ if (params.get('error'))
2605
+ throw new Error(params.get('error_description') || params.get('error') || 'Authorization failed.');
2606
+ let payload = Object.fromEntries(params.entries());
2607
+ if (!payload.access_token && params.get('code')) {
2608
+ if (!pending.tokenUrl)
2609
+ throw new Error('The authorization-code flow has no tokenUrl.');
2610
+ const body = new URLSearchParams({
2611
+ grant_type: 'authorization_code',
2612
+ code: String(params.get('code')),
2613
+ redirect_uri: pending.redirectUri,
2614
+ client_id: pending.clientId,
2615
+ });
2616
+ if (pending.codeVerifier)
2617
+ body.set('code_verifier', pending.codeVerifier);
2618
+ if (pending.clientSecret)
2619
+ body.set('client_secret', pending.clientSecret);
2620
+ const response = await executeJsonHttpRequest(pending.tokenUrl, {
2621
+ method: 'POST',
2622
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
2623
+ body: body.toString(),
2624
+ });
2625
+ payload = response.payload;
2626
+ if (!response.ok)
2627
+ throw new Error(payload?.error_description || payload?.error || 'Token exchange failed.');
2628
+ }
2629
+ if (!payload.access_token)
2630
+ throw new Error('The authorization response did not include an access token.');
2631
+ storeCredential(pending.schemeName, {
2632
+ accessToken: String(payload.access_token),
2633
+ refreshToken: payload.refresh_token,
2634
+ expiresAt: Number(payload.expires_in) > 0 ? Date.now() + Number(payload.expires_in) * 1000 : undefined,
2635
+ tokenType: payload.token_type || 'Bearer',
2636
+ clientId: pending.clientId,
2637
+ clientSecret: pending.clientSecret,
2638
+ scopes: pending.scopes,
2639
+ tokenUrl: pending.tokenUrl,
2640
+ });
2641
+ try {
2642
+ win.sessionStorage?.removeItem?.(pending.key);
2643
+ }
2644
+ catch { /* unavailable storage */ }
2645
+ render();
2646
+ }
2647
+ function clearOAuthCallbackLocation() {
2648
+ const current = safeRuntimeUrl(String(win.location?.href ?? ''));
2649
+ if (!current)
2650
+ return;
2651
+ ['raffel_oauth_callback', 'code', 'state', 'error', 'error_description'].forEach(name => current.searchParams.delete(name));
2652
+ const fragment = new URLSearchParams(current.hash.replace(/^#/, ''));
2653
+ if (fragment.has('access_token') || fragment.has('error'))
2654
+ current.hash = '';
2655
+ win.history?.replaceState?.(null, '', `${current.pathname}${current.search}${current.hash}`);
2656
+ }
2657
+ function installOAuthCallback() {
2658
+ const params = oauthCallbackParams();
2659
+ if (params) {
2660
+ if (win.opener && win.opener !== win) {
2661
+ ;
2662
+ win.opener.postMessage({ type: 'raffel-oauth-callback', params: params.toString() }, win.location?.origin);
2663
+ win.close?.();
2664
+ }
2665
+ else {
2666
+ void finishOAuthAuthorization(params).catch(() => { }).finally(clearOAuthCallbackLocation);
2667
+ }
2668
+ }
2669
+ win.addEventListener?.('message', (event) => {
2670
+ if (event?.origin !== win.location?.origin || event?.data?.type !== 'raffel-oauth-callback')
2671
+ return;
2672
+ void finishOAuthAuthorization(new URLSearchParams(String(event.data.params ?? ''))).catch(() => { });
2673
+ });
2674
+ }
2675
+ function securitySchemeNames(operation) {
2676
+ const requirement = selectedSecurityRequirement(operation);
2677
+ return requirement && typeof requirement === 'object' ? Object.keys(requirement) : [];
2678
+ }
2679
+ function substituteRefreshToken(value, refreshToken) {
2680
+ if (value === '$refreshToken')
2681
+ return refreshToken;
2682
+ if (Array.isArray(value))
2683
+ return value.map(item => substituteRefreshToken(item, refreshToken));
2684
+ if (value && typeof value === 'object') {
2685
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, substituteRefreshToken(item, refreshToken)]));
2686
+ }
2687
+ return value;
2688
+ }
2689
+ async function refreshCredential(schemeName) {
2690
+ const credential = readStoredCredential(schemeName);
2691
+ if (!credential.accessToken || !credential.refreshToken || !credential.expiresAt)
2692
+ return;
2693
+ if (Number(credential.expiresAt) > Date.now() + 30_000)
2694
+ return;
2695
+ const recipe = authenticationConfig?.schemes?.[schemeName];
2696
+ if (recipe?.strategy === 'operation' && recipe.refreshOperationId) {
2697
+ const operation = findHttpOperation(recipe.refreshOperationId);
2698
+ if (!operation)
2699
+ throw new Error(`Refresh operation ${recipe.refreshOperationId} was not found.`);
2700
+ const body = substituteRefreshToken(recipe.refreshRequestBody ?? { refreshToken: '$refreshToken' }, credential.refreshToken);
2701
+ const response = await executeJsonHttpRequest(`${httpBaseUrl()}${operation.path}`, {
2702
+ method: operation.method,
2703
+ headers: { 'Content-Type': 'application/json' },
2704
+ body: JSON.stringify(body),
2705
+ });
2706
+ const payload = response.payload;
2707
+ if (!response.ok)
2708
+ throw new Error(payload?.message || `Token refresh failed with ${response.status}.`);
2709
+ const accessToken = readJsonPointer(payload, recipe.tokenPointers?.accessToken);
2710
+ if (!accessToken)
2711
+ throw new Error('The refreshed access-token JSON Pointer did not match the response.');
2712
+ storeCredential(schemeName, {
2713
+ ...credential,
2714
+ accessToken: String(accessToken),
2715
+ refreshToken: readJsonPointer(payload, recipe.tokenPointers?.refreshToken) ?? credential.refreshToken,
2716
+ expiresAt: tokenExpiry(payload, recipe.tokenPointers),
2717
+ tokenType: readJsonPointer(payload, recipe.tokenPointers?.tokenType) ?? credential.tokenType ?? 'Bearer',
2718
+ });
2719
+ return;
2720
+ }
2721
+ const scheme = spec?.components?.securitySchemes?.[schemeName];
2722
+ if (scheme?.type === 'oauth2' || scheme?.type === 'openIdConnect') {
2723
+ const tokenUrl = credential.refreshUrl || credential.tokenUrl;
2724
+ if (!tokenUrl)
2725
+ return;
2726
+ const body = new URLSearchParams({ grant_type: 'refresh_token', refresh_token: credential.refreshToken });
2727
+ if (credential.clientId)
2728
+ body.set('client_id', credential.clientId);
2729
+ if (credential.clientSecret)
2730
+ body.set('client_secret', credential.clientSecret);
2731
+ const response = await executeJsonHttpRequest(tokenUrl, {
2732
+ method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body.toString(),
2733
+ });
2734
+ const payload = response.payload;
2735
+ if (!response.ok || !payload.access_token)
2736
+ throw new Error(payload?.error_description || payload?.error || 'Token refresh failed.');
2737
+ storeCredential(schemeName, {
2738
+ ...credential,
2739
+ accessToken: String(payload.access_token),
2740
+ refreshToken: payload.refresh_token ?? credential.refreshToken,
2741
+ expiresAt: Number(payload.expires_in) > 0 ? Date.now() + Number(payload.expires_in) * 1000 : undefined,
2742
+ tokenType: payload.token_type ?? credential.tokenType ?? 'Bearer',
2743
+ });
2744
+ }
2745
+ }
2746
+ async function refreshCredentialsBeforeRequest(operation) {
2747
+ for (const schemeName of securitySchemeNames(operation))
2748
+ await refreshCredential(schemeName);
2749
+ }
2750
+ function createAuthInput(name, labelText, type = 'text', value = '') {
2751
+ const label = doc.createElement('label');
2752
+ label.className = 'auth-field';
2753
+ const caption = doc.createElement('span');
2754
+ caption.className = 'auth-field-label';
2755
+ caption.textContent = labelText;
2756
+ const input = doc.createElement('input');
2757
+ input.className = 'auth-input';
2758
+ input.name = name;
2759
+ input.type = type;
2760
+ input.autocomplete = type === 'password' ? 'off' : 'on';
2761
+ input.value = value;
2762
+ label.appendChild(caption);
2763
+ label.appendChild(input);
2764
+ return label;
2765
+ }
2766
+ function renderAuthenticationSection() {
2767
+ const schemes = spec?.components?.securitySchemes ?? {};
2768
+ if (!schemes || Object.keys(schemes).length === 0)
2769
+ return null;
2770
+ const section = doc.createElement('section');
2771
+ section.className = 'authentication-section';
2772
+ section.id = 'authentication';
2773
+ const heading = doc.createElement('div');
2774
+ heading.className = 'authentication-header';
2775
+ heading.innerHTML = '<div><div class="subsection-label">AUTHENTICATION</div><h2>Authentication</h2><p>Credentials saved here are shared with every protected route in this environment.</p></div>';
2776
+ section.appendChild(heading);
2777
+ const environmentField = doc.createElement('label');
2778
+ environmentField.className = 'auth-environment';
2779
+ environmentField.innerHTML = '<span class="auth-field-label">Current environment</span>';
2780
+ const select = doc.createElement('select');
2781
+ select.className = 'auth-environment-select';
2782
+ environments.forEach(environment => {
2783
+ const option = doc.createElement('option');
2784
+ option.value = environment.url;
2785
+ option.textContent = `${environment.label} — ${environment.url}`;
2786
+ if (environment.url === selectedEnvironmentUrl)
2787
+ option.setAttribute('selected', '');
2788
+ select.appendChild(option);
2789
+ });
2790
+ select.value = selectedEnvironmentUrl;
2791
+ select.onchange = () => {
2792
+ selectedEnvironmentUrl = select.value;
2793
+ render();
2794
+ };
2795
+ environmentField.appendChild(select);
2796
+ section.appendChild(environmentField);
2797
+ const cards = doc.createElement('div');
2798
+ cards.className = 'auth-schemes';
2799
+ Object.entries(schemes).forEach(([schemeName, scheme]) => {
2800
+ const card = doc.createElement('article');
2801
+ card.className = 'auth-scheme';
2802
+ card.dataset.scheme = schemeName;
2803
+ const stored = readStoredCredential(schemeName);
2804
+ const title = doc.createElement('div');
2805
+ title.className = 'auth-scheme-title';
2806
+ title.innerHTML = `<strong>${esc(schemeName)}</strong><span>${esc(authSchemeKind(scheme))}</span>`;
2807
+ card.appendChild(title);
2808
+ if (scheme.description) {
2809
+ const description = doc.createElement('p');
2810
+ description.className = 'auth-scheme-description';
2811
+ description.textContent = scheme.description;
2812
+ card.appendChild(description);
2813
+ }
2814
+ const form = doc.createElement('div');
2815
+ form.className = 'auth-form';
2816
+ const recipe = authenticationConfig?.schemes?.[schemeName];
2817
+ if (recipe?.strategy === 'operation') {
2818
+ const operation = findHttpOperation(recipe.operationId);
2819
+ if (!operation) {
2820
+ const error = doc.createElement('div');
2821
+ error.className = 'auth-status auth-status-error';
2822
+ error.textContent = `Operation ${recipe.operationId} was not found.`;
2823
+ form.appendChild(error);
2824
+ }
2825
+ else {
2826
+ const label = doc.createElement('label');
2827
+ label.className = 'auth-field';
2828
+ label.innerHTML = `<span class="auth-field-label">Request body · ${esc(operation.method)} ${esc(operation.path)}</span>`;
2829
+ const textarea = doc.createElement('textarea');
2830
+ textarea.className = 'auth-operation-body';
2831
+ textarea.value = JSON.stringify(recipe.requestBody ?? operationRequestExample(operation.operation), null, 2);
2832
+ label.appendChild(textarea);
2833
+ form.appendChild(label);
2834
+ const actions = doc.createElement('div');
2835
+ actions.className = 'auth-actions';
2836
+ const requestToken = doc.createElement('button');
2837
+ requestToken.type = 'button';
2838
+ requestToken.className = 'auth-request-token';
2839
+ requestToken.textContent = stored.accessToken ? 'Request new token' : 'Request token';
2840
+ const status = doc.createElement('span');
2841
+ status.className = 'auth-status';
2842
+ status.textContent = stored.accessToken ? 'Ready' : 'Not authenticated';
2843
+ requestToken.onclick = () => { void requestOperationToken(schemeName, recipe, operation, textarea.value, status); };
2844
+ actions.appendChild(requestToken);
2845
+ actions.appendChild(status);
2846
+ form.appendChild(actions);
2847
+ }
2848
+ }
2849
+ else if (scheme.type === 'http' && String(scheme.scheme).toLowerCase() === 'bearer') {
2850
+ form.appendChild(createAuthInput('accessToken', 'Bearer token', 'password', stored.accessToken ?? ''));
2851
+ }
2852
+ else if (scheme.type === 'http' && String(scheme.scheme).toLowerCase() === 'basic') {
2853
+ form.appendChild(createAuthInput('username', 'Username', 'text', stored.username ?? ''));
2854
+ form.appendChild(createAuthInput('password', 'Password', 'password', stored.password ?? ''));
2855
+ }
2856
+ else if (scheme.type === 'apiKey') {
2857
+ form.appendChild(createAuthInput('apiKey', scheme.name || 'API key', 'password', stored.apiKey ?? ''));
2858
+ const location = doc.createElement('div');
2859
+ location.className = 'auth-scheme-meta';
2860
+ location.textContent = `${scheme.in ?? 'header'} · ${scheme.name ?? schemeName}`;
2861
+ form.appendChild(location);
2862
+ }
2863
+ else if (scheme.type === 'oauth2') {
2864
+ appendOAuthFlowFields(form, schemeName, scheme, stored);
2865
+ }
2866
+ else if (scheme.type === 'openIdConnect') {
2867
+ appendOidcFields(form, schemeName, scheme, stored);
2868
+ }
2869
+ else {
2870
+ const note = doc.createElement('div');
2871
+ note.className = 'auth-scheme-meta';
2872
+ note.textContent = 'This authentication method must be configured by the HTTP client.';
2873
+ form.appendChild(note);
2874
+ }
2875
+ if (!recipe && ['http', 'apiKey'].includes(scheme.type)) {
2876
+ const save = doc.createElement('button');
2877
+ save.type = 'button';
2878
+ save.className = 'auth-save';
2879
+ save.textContent = stored.accessToken || stored.apiKey || stored.username ? 'Update credential' : 'Use credential';
2880
+ const status = doc.createElement('span');
2881
+ status.className = 'auth-status';
2882
+ status.textContent = stored.accessToken || stored.apiKey || stored.username ? 'Ready' : 'Not configured';
2883
+ save.onclick = () => {
2884
+ const credential = {};
2885
+ form.querySelectorAll('input[name]').forEach((input) => { credential[input.name] = input.value; });
2886
+ storeCredential(schemeName, credential);
2887
+ render();
2888
+ };
2889
+ const actions = doc.createElement('div');
2890
+ actions.className = 'auth-actions';
2891
+ actions.appendChild(save);
2892
+ actions.appendChild(status);
2893
+ form.appendChild(actions);
2894
+ }
2895
+ card.appendChild(form);
2896
+ cards.appendChild(card);
2897
+ });
2898
+ section.appendChild(cards);
2899
+ return section;
2900
+ }
1802
2901
  function renderContent() {
1803
2902
  const main = byId('mainContent');
1804
2903
  if (!main)
@@ -1832,6 +2931,12 @@ function renderContent() {
1832
2931
  // mid-search, in which case the search results take the surface.
1833
2932
  if (!searchQuery) {
1834
2933
  main.appendChild(renderDocsOverview());
2934
+ const authentication = renderAuthenticationSection();
2935
+ if (authentication)
2936
+ main.appendChild(authentication);
2937
+ const webhooks = renderAsyncContracts('Webhooks', spec.webhooks ?? {});
2938
+ if (webhooks)
2939
+ main.appendChild(webhooks);
1835
2940
  }
1836
2941
  if (searchQuery)
1837
2942
  renderDocsSearch(main);
@@ -1842,7 +2947,9 @@ function renderContent() {
1842
2947
  const section = doc.createElement('section');
1843
2948
  section.className = 'endpoint-section';
1844
2949
  section.id = endpoint.id;
1845
- section.innerHTML = `<div class="endpoint-header"><div><div class="endpoint-method-path"><span class="badge badge-${esc(endpoint.method.toLowerCase())}">${esc(endpoint.method)}</span><span class="endpoint-path">${esc(endpoint.path)}</span></div><h2 class="endpoint-title">${esc(endpoint.summary ?? endpoint.path)}</h2>${endpoint.description ? `<div class="endpoint-description markdown-content">${parseMarkdown(endpoint.description)}</div>` : ''}</div></div>`;
2950
+ const endpointData = (endpoint.data ?? {});
2951
+ const deprecated = endpointData.deprecated ? '<span class="endpoint-deprecated">Deprecated</span>' : '';
2952
+ section.innerHTML = `<div class="endpoint-header"><div><div class="endpoint-method-path"><span class="badge badge-${esc(endpoint.method.toLowerCase())}">${esc(endpoint.method)}</span><span class="endpoint-path">${esc(endpoint.path)}</span>${deprecated}</div><h2 class="endpoint-title">${esc(endpoint.summary ?? endpoint.path)}</h2>${endpoint.description ? `<div class="endpoint-description markdown-content">${parseMarkdown(endpoint.description)}</div>` : ''}</div></div>`;
1846
2953
  section.appendChild(renderEndpointDetails(endpoint));
1847
2954
  main.appendChild(section);
1848
2955
  }
@@ -1894,7 +3001,17 @@ function renderDocsOverview() {
1894
3001
  const desc = s?.description
1895
3002
  ? `<span class="docs-overview-server-desc">${esc(String(s.description))}</span>`
1896
3003
  : '';
1897
- return `<li class="docs-overview-server"><code class="docs-overview-server-url">${url}</code>${desc}</li>`;
3004
+ const variables = Object.entries(s?.variables ?? {}).map(([name, definition]) => {
3005
+ const allowed = Array.isArray(definition?.enum) && definition.enum.length > 0
3006
+ ? `<span>allowed: ${esc(definition.enum.join(', '))}</span>`
3007
+ : '';
3008
+ const description = definition?.description ? `<span>${esc(definition.description)}</span>` : '';
3009
+ return `<li><code>${esc(name)}</code><span>default: ${esc(definition?.default ?? '')}</span>${allowed}${description}</li>`;
3010
+ }).join('');
3011
+ const variableBlock = variables
3012
+ ? `<div class="docs-overview-server-variables"><strong>Server variables</strong><ul>${variables}</ul></div>`
3013
+ : '';
3014
+ return `<li class="docs-overview-server"><code class="docs-overview-server-url">${url}</code>${desc}${variableBlock}</li>`;
1898
3015
  })
1899
3016
  .join('');
1900
3017
  section.innerHTML = `<h2 class="docs-overview-subtitle">${servers.length > 1 ? 'Servers' : 'Server'}</h2><ul class="docs-overview-server-list">${rows}</ul>`;
@@ -2605,6 +3722,7 @@ function init() {
2605
3722
  const storedTheme = win.localStorage?.getItem?.(themeStorageKey);
2606
3723
  if (storedTheme === 'auto' || storedTheme === 'dark' || storedTheme === 'light' || storedTheme === 'custom')
2607
3724
  doc.documentElement.setAttribute('data-theme', storedTheme);
3725
+ installOAuthCallback();
2608
3726
  installDocsPluginApi();
2609
3727
  if (activePagePath && activePagePath !== routeState.pagePath) {
2610
3728
  win.history?.replaceState?.(null, '', routeToHash(activePagePath, activeHeadingId));