@umami/shiso 1.18.0 → 1.19.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.
Files changed (44) hide show
  1. package/dist/chunks/App.js +1081 -47
  2. package/dist/chunks/architectureDiagram-5GKGNRK7.js +1 -1
  3. package/dist/chunks/chunk-GMAD6QVW.js +1 -1
  4. package/dist/chunks/cose-bilkent-JH36ORCC.js +1 -1
  5. package/dist/chunks/dist.js +1 -1
  6. package/dist/chunks/docs.js +183 -8
  7. package/dist/chunks/ganttDiagram-EL5Y4UJY.js +1 -1
  8. package/dist/chunks/src.js +1 -1
  9. package/dist/components.js +1 -1
  10. package/dist/entry-client.js +1 -1
  11. package/dist/entry-server.js +1 -1
  12. package/docs.schema.json +1164 -1135
  13. package/package.json +1 -2
  14. package/scripts/check-content.mjs +42 -15
  15. package/scripts/expand-openapi-navigation.mjs +47 -21
  16. package/scripts/generate-openapi.mjs +36 -11
  17. package/scripts/generate-search-index.mjs +19 -18
  18. package/scripts/lib/openapi-project.mjs +197 -0
  19. package/scripts/lib/openapi.mjs +257 -111
  20. package/scripts/lib/request-samples.mjs +323 -0
  21. package/scripts/load-docs-config.mjs +23 -15
  22. package/scripts/prerender.mjs +17 -14
  23. package/scripts/vite-docs-config.mjs +1 -0
  24. package/src/components/ApiPlayground.tsx +522 -0
  25. package/src/components/DocContent.tsx +15 -4
  26. package/src/components/Docs.tsx +18 -3
  27. package/src/components/LanguageSwitcher.tsx +26 -30
  28. package/src/components/OpenApiOperation.tsx +58 -12
  29. package/src/components/OpenApiSchema.tsx +97 -0
  30. package/src/components/SideNav.tsx +2 -2
  31. package/src/lib/openapi.generated.ts +2 -1
  32. package/src/lib/openapi.ts +104 -10
  33. package/src/lib/site-model.ts +12 -0
  34. package/src/lib/translations/de.json +26 -1
  35. package/src/lib/translations/en.json +26 -1
  36. package/src/lib/translations/es.json +26 -1
  37. package/src/lib/translations/fr.json +26 -1
  38. package/src/lib/translations/ja.json +26 -1
  39. package/src/lib/translations/zh-Hans.json +26 -1
  40. package/src/lib/translations/zh-Hant.json +26 -1
  41. package/src/lib/types.ts +89 -5
  42. package/types/labels.d.ts +25 -0
  43. package/vite.config.ts +3 -4
  44. package/CHANGELOG.md +0 -8
@@ -11,8 +11,11 @@
11
11
  import fs from 'node:fs/promises';
12
12
  import path from 'node:path';
13
13
  import { parse as parseYaml } from 'yaml';
14
+ import { buildCodeSamples } from './request-samples.mjs';
14
15
  import { slugify } from './slug.mjs';
15
16
 
17
+ export { buildCodeSamples, buildRequest } from './request-samples.mjs';
18
+
16
19
  const METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'];
17
20
  const MAX_DEPTH = 8;
18
21
  const MAX_CHILDREN = 100;
@@ -287,21 +290,68 @@ function formatExample(value) {
287
290
  return value === undefined ? undefined : JSON.stringify(value, null, 2);
288
291
  }
289
292
 
290
- function securitySummaries(spec, operation) {
293
+ const SECURITY_TYPES = new Set(['http', 'apiKey', 'oauth2', 'openIdConnect']);
294
+
295
+ /** Human-readable label for a security scheme, e.g. "bearerAuth (http bearer)". */
296
+ export function securityLabel(scheme) {
297
+ const detail = [scheme.type !== 'unknown' ? scheme.type : undefined, scheme.scheme]
298
+ .filter(Boolean)
299
+ .join(' ');
300
+ return detail ? `${scheme.name} (${detail})` : scheme.name;
301
+ }
302
+
303
+ /**
304
+ * Resolves the security schemes an operation accepts. Every scheme named by
305
+ * any requirement alternative is listed once, in declaration order, so the
306
+ * playground can offer an input for each and samples show how it is sent.
307
+ */
308
+ function securitySchemes(spec, operation) {
291
309
  const requirements = operation.security ?? spec.security ?? [];
292
- const schemes = spec.components?.securitySchemes || {};
293
- const names = new Set();
310
+ const definitions = spec.components?.securitySchemes || {};
311
+ const schemes = new Map();
294
312
 
295
313
  for (const requirement of requirements) {
296
314
  for (const name of Object.keys(requirement || {})) {
297
- const scheme = deref(spec, schemes[name]);
298
- names.add(
299
- scheme ? `${name} (${[scheme.type, scheme.scheme].filter(Boolean).join(' ')})` : name,
300
- );
315
+ if (schemes.has(name)) continue;
316
+ const definition = deref(spec, definitions[name]);
317
+ const type = SECURITY_TYPES.has(definition?.type) ? definition.type : 'unknown';
318
+ const scheme = { name, type };
319
+ if (type === 'http' && typeof definition.scheme === 'string') {
320
+ scheme.scheme = definition.scheme.toLowerCase();
321
+ }
322
+ if (type === 'apiKey') {
323
+ scheme.in = ['query', 'cookie'].includes(definition.in) ? definition.in : 'header';
324
+ scheme.paramName = definition.name || name;
325
+ }
326
+ if (typeof definition?.description === 'string') scheme.description = definition.description;
327
+ scheme.label = securityLabel(scheme);
328
+ schemes.set(name, scheme);
301
329
  }
302
330
  }
303
331
 
304
- return [...names];
332
+ return [...schemes.values()];
333
+ }
334
+
335
+ /** Fills server URL variables with their default values. */
336
+ function expandServerUrl(server) {
337
+ return String(server.url || '').replace(/\{([^}]+)\}/g, (match, name) => {
338
+ const variable = server.variables?.[name];
339
+ return variable?.default !== undefined ? String(variable.default) : match;
340
+ });
341
+ }
342
+
343
+ /** Servers for an operation: operation-level, then path-level, then global. */
344
+ function resolveServers(spec, pathItem, operation) {
345
+ const list = [operation.servers, pathItem.servers, spec.servers].find(
346
+ candidate => Array.isArray(candidate) && candidate.length > 0,
347
+ );
348
+ const servers = (list || [])
349
+ .filter(server => server && typeof server === 'object' && typeof server.url === 'string')
350
+ .map(server => ({
351
+ url: expandServerUrl(server),
352
+ ...(server.description ? { description: server.description } : {}),
353
+ }));
354
+ return servers.length ? servers : [{ url: FALLBACK_SERVER }];
305
355
  }
306
356
 
307
357
  function parameterNode(spec, parameter) {
@@ -312,36 +362,57 @@ function parameterNode(spec, parameter) {
312
362
  });
313
363
  if (resolved.description && !node.description) node.description = resolved.description;
314
364
  if (resolved.deprecated === true) node.deprecated = true;
315
- return { location: resolved.in, node, example: exampleFromSchema(spec, resolved.schema || {}) };
365
+ const example =
366
+ resolved.example !== undefined
367
+ ? resolved.example
368
+ : exampleFromSchema(spec, resolved.schema || {});
369
+ if (example !== null && example !== undefined) node.example = stringifyValue(example);
370
+ return { location: resolved.in, node };
316
371
  }
317
372
 
318
- /** Normalizes every operation in the spec into a serializable shape. */
319
- export function normalizeOperations(spec) {
373
+ const HTTP_METHODS = new Set(METHODS.map(method => method.toUpperCase()));
374
+
375
+ /**
376
+ * Normalizes every operation in the spec into a serializable shape. OpenAPI
377
+ * 3.1 `webhooks` (and the `x-webhooks` extension) become operations flagged
378
+ * `webhook: true`, keyed `WEBHOOK <name>`: they describe payloads the API
379
+ * sends, so they carry no servers, samples, or playground.
380
+ */
381
+ export function normalizeOperations(spec, { specId } = {}) {
320
382
  const operations = [];
383
+ const sources = [
384
+ ...Object.entries(spec.paths || {}).map(([name, item]) => [name, item, false]),
385
+ ...Object.entries(spec.webhooks || spec['x-webhooks'] || {}).map(([name, item]) => [
386
+ name,
387
+ item,
388
+ true,
389
+ ]),
390
+ ];
321
391
 
322
- for (const [pathName, pathItem] of Object.entries(spec.paths || {})) {
392
+ for (const [pathName, pathItem, webhook] of sources) {
323
393
  const resolvedPath = deref(spec, pathItem);
324
394
  if (!resolvedPath || typeof resolvedPath !== 'object') continue;
325
395
 
326
396
  for (const method of METHODS) {
327
397
  const operation = resolvedPath[method];
328
398
  if (!operation || typeof operation !== 'object') continue;
399
+ // A webhook name maps to one page; a second method on the same
400
+ // webhook would collide with it, so only the first is documented.
401
+ if (webhook && operations.some(item => item.webhook && item.path === pathName)) break;
329
402
 
330
403
  const upper = method.toUpperCase();
331
404
  const parameters = { query: [], path: [], header: [], cookie: [] };
332
405
  const merged = [...(resolvedPath.parameters || []), ...(operation.parameters || [])];
333
406
  const seenParams = new Set();
334
- const paramExamples = {};
335
407
 
336
408
  // Operation-level parameters override path-level ones with the same
337
409
  // name and location, so walk the merged list from the end.
338
410
  for (const parameter of merged.reverse()) {
339
- const { location, node, example } = parameterNode(spec, parameter);
411
+ const { location, node } = parameterNode(spec, parameter);
340
412
  const dedupeKey = `${location}:${node.name}`;
341
413
  if (!parameters[location] || seenParams.has(dedupeKey)) continue;
342
414
  seenParams.add(dedupeKey);
343
415
  parameters[location].unshift(node);
344
- if (example !== null && example !== undefined) paramExamples[dedupeKey] = example;
345
416
  }
346
417
 
347
418
  const bodySource = deref(spec, operation.requestBody);
@@ -369,18 +440,22 @@ export function normalizeOperations(spec) {
369
440
  };
370
441
  });
371
442
 
372
- const serverUrl = operation.servers?.[0]?.url || spec.servers?.[0]?.url || FALLBACK_SERVER;
443
+ const servers = webhook ? [] : resolveServers(spec, resolvedPath, operation);
444
+ const kebab = value =>
445
+ value
446
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
447
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
448
+ .replace(/[_\s.]+/g, '-');
449
+ const idSource = operation.operationId
450
+ ? kebab(operation.operationId)
451
+ : webhook
452
+ ? `webhook-${kebab(pathName)}`
453
+ : `${method}-${pathName}`;
373
454
  const normalized = {
374
- id: slugify(
375
- operation.operationId
376
- ? operation.operationId
377
- .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
378
- .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
379
- .replace(/[_\s]+/g, '-')
380
- : `${method}-${pathName}`,
381
- `${method}-${slugify(pathName, 'root')}`,
382
- ),
383
- key: `${upper} ${pathName}`,
455
+ id: slugify(idSource, `${webhook ? 'webhook' : method}-${slugify(pathName, 'root')}`),
456
+ key: webhook ? `WEBHOOK ${pathName}` : `${upper} ${pathName}`,
457
+ ...(specId ? { spec: specId } : {}),
458
+ ...(webhook ? { webhook: true } : {}),
384
459
  method: upper,
385
460
  path: pathName,
386
461
  summary: operation.summary || undefined,
@@ -390,12 +465,13 @@ export function normalizeOperations(spec) {
390
465
  parameters,
391
466
  requestBody,
392
467
  responses,
393
- security: securitySummaries(spec, operation),
394
- serverUrl,
468
+ security: webhook ? [] : securitySchemes(spec, operation),
469
+ servers,
470
+ serverUrl: servers[0]?.url || '',
395
471
  samples: [],
396
472
  };
397
473
 
398
- normalized.samples = buildCodeSamples(normalized, paramExamples);
474
+ if (!webhook) normalized.samples = buildCodeSamples(normalized);
399
475
  operations.push(normalized);
400
476
  }
401
477
  }
@@ -415,70 +491,6 @@ export function normalizeOperations(spec) {
415
491
  return operations;
416
492
  }
417
493
 
418
- function queryString(operation, paramExamples) {
419
- const pairs = operation.parameters.query
420
- .filter(parameter => parameter.required)
421
- .map(parameter => {
422
- const example = paramExamples[`query:${parameter.name}`];
423
- return `${parameter.name}=${encodeURIComponent(String(example ?? ''))}`;
424
- });
425
- return pairs.length ? `?${pairs.join('&')}` : '';
426
- }
427
-
428
- function pythonLiteral(json) {
429
- return json
430
- .replace(/"([^"]+)":/g, "'$1':")
431
- .replace(/"/g, "'")
432
- .replace(/\btrue\b/g, 'True')
433
- .replace(/\bfalse\b/g, 'False')
434
- .replace(/\bnull\b/g, 'None');
435
- }
436
-
437
- /** Builds curl, JavaScript, and Python request samples for an operation. */
438
- export function buildCodeSamples(operation, paramExamples = {}) {
439
- const url = `${operation.serverUrl.replace(/\/$/, '')}${operation.path}${queryString(operation, paramExamples)}`;
440
- const hasAuth = operation.security.length > 0;
441
- const body = operation.requestBody?.example;
442
- const method = operation.method;
443
-
444
- const escapedBody = body ? body.replace(/'/g, `'\\''`) : undefined;
445
- const curl = [
446
- `curl -X ${method} '${url}'`,
447
- ...(hasAuth ? [` -H 'Authorization: Bearer <token>'`] : []),
448
- ...(body ? [` -H 'Content-Type: application/json'`, ` -d '${escapedBody}'`] : []),
449
- ].join(' \\\n');
450
-
451
- const headers = [
452
- ...(body ? [` 'Content-Type': 'application/json',`] : []),
453
- ...(hasAuth ? [` Authorization: 'Bearer <token>',`] : []),
454
- ];
455
- const javascript = [
456
- `const response = await fetch('${url}', {`,
457
- ` method: '${method}',`,
458
- ...(headers.length ? [' headers: {', ...headers, ' },'] : []),
459
- ...(body ? [` body: JSON.stringify(${body}),`] : []),
460
- '});',
461
- 'const data = await response.json();',
462
- ].join('\n');
463
-
464
- const python = [
465
- 'import requests',
466
- '',
467
- `response = requests.${method.toLowerCase()}(`,
468
- ` '${url}',`,
469
- ...(hasAuth ? [` headers={'Authorization': 'Bearer <token>'},`] : []),
470
- ...(body ? [` json=${pythonLiteral(body)},`] : []),
471
- ')',
472
- 'print(response.json())',
473
- ].join('\n');
474
-
475
- return [
476
- { language: 'bash', label: 'cURL', source: curl },
477
- { language: 'javascript', label: 'JavaScript', source: javascript },
478
- { language: 'python', label: 'Python', source: python },
479
- ];
480
- }
481
-
482
494
  function markdownSchemaLines(node, depth = 0) {
483
495
  if (!node) return [];
484
496
  const indent = ' '.repeat(depth);
@@ -493,10 +505,18 @@ function markdownSchemaLines(node, depth = 0) {
493
505
 
494
506
  /** Renders an operation as markdown for the .md export and llms-full.txt. */
495
507
  export function operationToMarkdown(operation) {
496
- const lines = [`## ${operation.method} ${operation.path}`, ''];
508
+ const lines = [
509
+ operation.webhook
510
+ ? `## Webhook: ${operation.path}`
511
+ : `## ${operation.method} ${operation.path}`,
512
+ '',
513
+ ];
497
514
 
498
515
  if (operation.summary) lines.push(operation.summary, '');
499
516
  if (operation.description) lines.push(operation.description, '');
517
+ if (operation.security.length) {
518
+ lines.push(`Authentication: ${operation.security.map(scheme => scheme.label).join(', ')}`, '');
519
+ }
500
520
 
501
521
  const allParameters = ['path', 'query', 'header', 'cookie'].flatMap(location =>
502
522
  operation.parameters[location].map(parameter => ({ location, parameter })),
@@ -514,7 +534,12 @@ export function operationToMarkdown(operation) {
514
534
  }
515
535
 
516
536
  if (operation.requestBody) {
517
- lines.push('### Request body', '', ...markdownSchemaLines(operation.requestBody.schema), '');
537
+ lines.push(
538
+ operation.webhook ? '### Payload' : '### Request body',
539
+ '',
540
+ ...markdownSchemaLines(operation.requestBody.schema),
541
+ '',
542
+ );
518
543
  if (operation.requestBody.example) {
519
544
  lines.push('```json', operation.requestBody.example, '```', '');
520
545
  }
@@ -541,20 +566,99 @@ export function operationToMarkdown(operation) {
541
566
  return lines.join('\n').trim();
542
567
  }
543
568
 
569
+ /**
570
+ * Normalizes every named schema under components.schemas into a page-ready
571
+ * shape, keyed by its name (and `<spec> <name>` on multi-spec sites).
572
+ */
573
+ export function normalizeSchemas(spec, { specId } = {}) {
574
+ const schemas = [];
575
+
576
+ for (const [name, schema] of Object.entries(spec.components?.schemas || {})) {
577
+ if (!schema || typeof schema !== 'object') continue;
578
+ const resolved = deref(spec, schema);
579
+ const example = exampleFromSchema(spec, schema);
580
+ schemas.push({
581
+ name,
582
+ key: name,
583
+ ...(specId ? { spec: specId } : {}),
584
+ title: typeof resolved?.title === 'string' ? resolved.title : undefined,
585
+ description: typeof resolved?.description === 'string' ? resolved.description : undefined,
586
+ schema: schemaTree(spec, schema),
587
+ example: formatExample(example === null ? undefined : example),
588
+ });
589
+ }
590
+
591
+ return schemas;
592
+ }
593
+
594
+ /** Anchor ids for a schema page's generated sections; mirrors src/lib/openapi.ts. */
595
+ export function schemaAnchors(page) {
596
+ return [
597
+ ...(page.schema?.children?.length ? ['properties'] : []),
598
+ ...(page.example ? ['example'] : []),
599
+ ];
600
+ }
601
+
602
+ /** Search-index sections for a schema page, matching schemaAnchors ids. */
603
+ export function schemaSearchSections(page) {
604
+ return [
605
+ {
606
+ heading: undefined,
607
+ id: undefined,
608
+ text: [page.name, page.description].filter(Boolean).join(' '),
609
+ },
610
+ { heading: 'Properties', id: 'properties', text: schemaText(page.schema) },
611
+ ].filter(section => section.text.replace(/\s+/g, ' ').trim());
612
+ }
613
+
614
+ /** Renders a schema page as markdown for the .md export and llms-full.txt. */
615
+ export function schemaToMarkdown(page) {
616
+ const lines = [`## ${page.name}`, ''];
617
+ if (page.description) lines.push(page.description, '');
618
+ if (page.schema?.children?.length) {
619
+ lines.push(
620
+ '### Properties',
621
+ '',
622
+ ...markdownSchemaLines(page.schema)
623
+ .slice(1)
624
+ .map(line => line.slice(2)),
625
+ '',
626
+ );
627
+ }
628
+ if (page.example) lines.push('### Example', '', '```json', page.example, '```', '');
629
+ return lines.join('\n').trim();
630
+ }
631
+
544
632
  function yamlString(value) {
545
633
  return JSON.stringify(String(value).split('\n')[0]);
546
634
  }
547
635
 
636
+ /** The frontmatter value that binds a page to an operation. */
637
+ export function operationReference(operation, prefixSpec = false) {
638
+ const key = operation.webhook ? `webhook ${operation.path}` : operation.key;
639
+ return prefixSpec && operation.spec ? `${operation.spec} ${key}` : key;
640
+ }
641
+
548
642
  /**
549
643
  * Writes one stub .mdx page per operation, skipping files that already exist
550
644
  * so authors can customize titles or add prose above the generated reference.
551
645
  */
552
- export async function generateOpenApiStubs({ root, contentDir, directory, operations }) {
553
- const target = path.join(path.resolve(root), contentDir, directory);
554
- await fs.mkdir(target, { recursive: true });
646
+ export async function generateOpenApiStubs({
647
+ root,
648
+ contentDir,
649
+ directory,
650
+ operations,
651
+ prefixSpec = false,
652
+ }) {
555
653
  const created = [];
556
654
 
557
655
  for (const operation of operations) {
656
+ const target = path.join(
657
+ path.resolve(root),
658
+ contentDir,
659
+ operation.directory || directory || DEFAULT_API_DIRECTORY,
660
+ );
661
+ await fs.mkdir(target, { recursive: true });
558
662
  const filePath = path.join(target, `${operation.id}.mdx`);
559
663
 
560
664
  try {
@@ -568,13 +672,13 @@ export async function generateOpenApiStubs({ root, contentDir, directory, operat
568
672
  '---',
569
673
  `title: ${yamlString(operation.summary || `${operation.method} ${operation.path}`)}`,
570
674
  ...(operation.description ? [`description: ${yamlString(operation.description)}`] : []),
571
- `openapi: ${operation.method} ${operation.path}`,
675
+ `openapi: ${operationReference(operation, prefixSpec)}`,
572
676
  '---',
573
677
  '',
574
678
  ].join('\n');
575
679
 
576
680
  await fs.writeFile(filePath, frontmatter);
577
- created.push(`${directory}/${operation.id}`);
681
+ created.push(operation.pageRef || `${directory}/${operation.id}`);
578
682
  }
579
683
 
580
684
  return created;
@@ -601,6 +705,15 @@ export function resolveApiDirectory(api) {
601
705
  return directory;
602
706
  }
603
707
 
708
+ /** Where a scheme's credential travels; mirrors securityLocation in src/lib/openapi.ts. */
709
+ export function securityLocation(scheme) {
710
+ return scheme.type === 'apiKey' && scheme.in ? scheme.in : 'header';
711
+ }
712
+
713
+ function securityForLocation(operation, location) {
714
+ return operation.security.filter(scheme => securityLocation(scheme) === location);
715
+ }
716
+
604
717
  /** True when the operation renders a Parameters section (incl. auth). */
605
718
  export function hasOperationParameters(operation) {
606
719
  const { query, path: pathParams, header, cookie } = operation.parameters;
@@ -619,7 +732,7 @@ function operationParameterSections(operation) {
619
732
  ].filter(
620
733
  ({ location }) =>
621
734
  operation.parameters[location].length > 0 ||
622
- (location === 'header' && operation.security.length > 0),
735
+ securityForLocation(operation, location).length > 0,
623
736
  );
624
737
  }
625
738
 
@@ -627,10 +740,11 @@ function operationParameterSections(operation) {
627
740
  * Anchor ids for the generated sections, in render order. Mirrors
628
741
  * operationSections in src/lib/openapi.ts (asserted by tests/openapi.test.mjs).
629
742
  */
630
- export function operationAnchors(operation) {
743
+ export function operationAnchors(operation, { playground = false } = {}) {
631
744
  return [
745
+ ...(playground && !operation.webhook ? ['try-it'] : []),
632
746
  ...operationParameterSections(operation).map(section => section.id),
633
- ...(operation.requestBody ? ['request-body'] : []),
747
+ ...(operation.requestBody ? [operation.webhook ? 'payload' : 'request-body'] : []),
634
748
  ...(operation.responses.length ? ['responses'] : []),
635
749
  ...(operation.samples.length ? ['code-samples'] : []),
636
750
  ];
@@ -643,11 +757,41 @@ function schemaText(node) {
643
757
  .join(' ');
644
758
  }
645
759
 
646
- /** Normalizes an openapi frontmatter value into the operation lookup key. */
760
+ /**
761
+ * Whether an endpoint page renders the "Try it" panel. Mirrors
762
+ * resolvePlaygroundDisplay in src/lib/openapi.ts: the page's `playground`
763
+ * frontmatter wins over `api.playground.display`, and only "interactive"
764
+ * (the default) renders the panel.
765
+ */
766
+ export function hasPlayground(api, frontmatterValue) {
767
+ const override = String(frontmatterValue ?? '').trim();
768
+ if (['interactive', 'simple', 'none'].includes(override)) return override === 'interactive';
769
+ const display = api?.playground?.display;
770
+ return display !== 'simple' && display !== 'none';
771
+ }
772
+
773
+ /**
774
+ * Normalizes an openapi frontmatter value into the operation lookup key:
775
+ * "get /users" -> "GET /users", "webhook userCreated" -> "WEBHOOK userCreated",
776
+ * and "users.yaml GET /users" -> "users.yaml GET /users" (a spec-qualified key
777
+ * for multi-spec sites). Returns undefined for blank values.
778
+ */
647
779
  export function normalizeOperationKey(value) {
648
780
  if (typeof value !== 'string' || !value.trim()) return undefined;
649
- const [method, ...rest] = value.trim().split(/\s+/);
650
- return `${method.toUpperCase()} ${rest.join(' ')}`;
781
+ const [first, ...rest] = value.trim().split(/\s+/);
782
+ if (!rest.length) return undefined;
783
+ const upper = first.toUpperCase();
784
+ if (upper === 'WEBHOOK' || HTTP_METHODS.has(upper)) {
785
+ return `${upper} ${rest.join(' ')}`;
786
+ }
787
+ const inner = normalizeOperationKey(rest.join(' '));
788
+ return inner ? `${first} ${inner}` : undefined;
789
+ }
790
+
791
+ /** Normalizes an openapi-schema frontmatter value: "User" or "users.yaml User". */
792
+ export function normalizeSchemaKey(value) {
793
+ if (typeof value !== 'string' || !value.trim()) return undefined;
794
+ return value.trim().split(/\s+/).join(' ');
651
795
  }
652
796
 
653
797
  /** Search-index sections for an operation, matching operationAnchors ids. */
@@ -665,14 +809,16 @@ export function operationSearchSections(operation) {
665
809
  id,
666
810
  text: [
667
811
  ...operation.parameters[location].map(schemaText),
668
- ...(location === 'header' && operation.security.length > 0
669
- ? ['Authorization Authentication credentials']
670
- : []),
812
+ ...securityForLocation(operation, location).flatMap(scheme => [
813
+ scheme.type === 'apiKey' ? scheme.paramName || scheme.name : 'Authorization',
814
+ 'Authentication credentials',
815
+ scheme.label,
816
+ ]),
671
817
  ].join(' '),
672
818
  })),
673
819
  {
674
- heading: 'Request body',
675
- id: 'request-body',
820
+ heading: operation.webhook ? 'Payload' : 'Request body',
821
+ id: operation.webhook ? 'payload' : 'request-body',
676
822
  text: schemaText(operation.requestBody?.schema),
677
823
  },
678
824
  {