okengine 0.22.0 → 0.23.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 (262) hide show
  1. package/AGENTS.md +7 -0
  2. package/manifest.v1.schema.json +44 -0
  3. package/package.json +32 -19
  4. package/site/content/docs/ai/mcp.mdx +13 -9
  5. package/site/content/docs/ai/skills.mdx +1 -1
  6. package/site/content/docs/elements/ai/agents.mdx +61 -13
  7. package/site/content/docs/elements/ai/decide.mdx +536 -0
  8. package/site/content/docs/elements/ai/events.mdx +373 -0
  9. package/site/content/docs/elements/ai/index.mdx +9 -3
  10. package/site/content/docs/elements/ai/meta.json +1 -1
  11. package/site/content/docs/elements/ai/prompts.mdx +4 -2
  12. package/site/content/docs/elements/gate/auth.mdx +4 -1
  13. package/site/content/docs/elements/store/index.mdx +1 -1
  14. package/site/content/docs/elements/store/kv.mdx +5 -5
  15. package/site/content/docs/recipes/dragonfly.mdx +5 -5
  16. package/site/content/docs/recipes/index.mdx +6 -6
  17. package/site/content/docs/recipes/postgres.mdx +12 -8
  18. package/site/content/docs/recipes/redis.mdx +4 -4
  19. package/site/content/docs/reference/cli.mdx +2 -1
  20. package/site/content/docs/reference/configuration.mdx +9 -7
  21. package/site/content/docs/reference/fx.mdx +7 -6
  22. package/src/auth/api-key-sql.test.ts +44 -0
  23. package/src/auth/api-key-sql.ts +1 -0
  24. package/src/auth/api-keys.ts +30 -12
  25. package/src/auth/config.ts +6 -2
  26. package/src/auth/gate-auth.test.ts +20 -0
  27. package/src/cli/agents-md.test.ts +25 -0
  28. package/src/cli/ask-vault-gaps.test.ts +38 -1
  29. package/src/cli/ask-vault-gaps.ts +35 -2
  30. package/src/cli/decide.ts +174 -0
  31. package/src/cli/decision-lock-watch.test.ts +45 -0
  32. package/src/cli/decision-lock-watch.ts +51 -0
  33. package/src/cli/dev-app-runner.ts +34 -8
  34. package/src/cli/dev-auth-secret.test.ts +49 -0
  35. package/src/cli/dev-auth-secret.ts +49 -0
  36. package/src/cli/dev-hot.test.ts +41 -0
  37. package/src/cli/dev-hot.ts +48 -0
  38. package/src/cli/dev.test.ts +1 -1
  39. package/src/cli/dev.ts +24 -6
  40. package/src/cli/eval.ts +142 -1
  41. package/src/cli/index.ts +5 -0
  42. package/src/cli/load-config.images.test.ts +2 -2
  43. package/src/cli/load-config.ts +1 -1
  44. package/src/cli/mcp-from-console.ts +48 -0
  45. package/src/cli/registry.ts +35 -1
  46. package/src/client/agent.ts +307 -0
  47. package/src/client/transport.test.ts +20 -0
  48. package/src/client/transport.ts +2 -1
  49. package/src/client/types.ts +1 -1
  50. package/src/client-react/index.ts +7 -1
  51. package/src/client-react/use-agent-run.test.ts +271 -0
  52. package/src/client-react/use-agent-run.ts +193 -0
  53. package/src/compiler/ai-approval.test.ts +36 -0
  54. package/src/compiler/ai-repair.test.ts +22 -0
  55. package/src/compiler/decisions.extract.test.ts +164 -0
  56. package/src/compiler/effects-infer.ts +27 -1
  57. package/src/compiler/extract.ts +268 -12
  58. package/src/compiler/response.ts +35 -2
  59. package/src/config/index.ts +21 -3
  60. package/src/console/server/ai-runs-flows.ts +269 -0
  61. package/src/console/server/ai-runs.test.ts +323 -0
  62. package/src/console/server/ai-runs.ts +562 -0
  63. package/src/console/server/ai.ts +12 -1
  64. package/src/console/server/app.ts +10 -1
  65. package/src/console/server/decisions-flows.ts +157 -0
  66. package/src/console/server/decisions.test.ts +66 -0
  67. package/src/console/server/decisions.ts +229 -0
  68. package/src/console/server/flows.ts +50 -2
  69. package/src/console/server/invoke-user-flow.ts +9 -0
  70. package/src/console/server/runs-ingest.ts +16 -0
  71. package/src/console/server/serve.ts +5 -0
  72. package/src/console/server/state.ts +25 -1
  73. package/src/console/server/store-stats.test.ts +47 -0
  74. package/src/console/server/store-stats.ts +92 -18
  75. package/src/console/ui-next/dist/assets/access-page-DDKkhT9v.js +4 -0
  76. package/src/console/ui-next/dist/assets/{agent-disclosure-Bkohc_8X.js → agent-disclosure-62Xts8Xi.js} +1 -1
  77. package/src/console/ui-next/dist/assets/{cache-glyph-rL0lHxat.js → cache-glyph-DyeoKHKA.js} +1 -1
  78. package/src/console/ui-next/dist/assets/{call-pii-button-Deo4PP18.js → call-pii-button-DvfpLXsU.js} +1 -1
  79. package/src/console/ui-next/dist/assets/{collapsible-CoJ6amHf.js → collapsible-BY03SeCg.js} +1 -1
  80. package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
  81. package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
  82. package/src/console/ui-next/dist/assets/decisions-page-CgkKlA56.js +1 -0
  83. package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
  84. package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
  85. package/src/console/ui-next/dist/assets/duration-tone-Y6HLjyJ5.js +9 -0
  86. package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
  87. package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
  88. package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
  89. package/src/console/ui-next/dist/assets/flows-page-DLPfNy-_.js +1 -0
  90. package/src/console/ui-next/dist/assets/{highlighted-json-CPYBzDh0.js → highlighted-json-D7nNzpgB.js} +1 -1
  91. package/src/console/ui-next/dist/assets/{http-method-DTPevKmM.js → http-method-DdL19zzh.js} +1 -1
  92. package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
  93. package/src/console/ui-next/dist/assets/index-C0lc_s9d.js +58 -0
  94. package/src/console/ui-next/dist/assets/observability-page-CRlcyLCC.js +8 -0
  95. package/src/console/ui-next/dist/assets/{replica-lag-BM27lXld.js → replica-lag-Bb3FWUu9.js} +7 -7
  96. package/src/console/ui-next/dist/assets/request-meta-DYBMWhX8.js +1 -0
  97. package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
  98. package/src/console/ui-next/dist/assets/store-page-CcE-SXC8.js +41 -0
  99. package/src/console/ui-next/dist/assets/trace-detail-sheet-CKMSEQ4s.js +2 -0
  100. package/src/console/ui-next/dist/assets/tree-expand-toggle-QOfk0M0H.js +55 -0
  101. package/src/console/ui-next/dist/assets/units-page-DKC1CDDd.js +1 -0
  102. package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
  103. package/src/console/ui-next/dist/assets/vault-page-B54v4Yit.js +2 -0
  104. package/src/console/ui-next/dist/index.html +4 -3
  105. package/src/console/ui-next/seed-parked-approval.ts +126 -0
  106. package/src/console/ui-next/src/client.ts +224 -1
  107. package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
  108. package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
  109. package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
  110. package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
  111. package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
  112. package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
  113. package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
  114. package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
  115. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
  116. package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
  117. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
  118. package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
  119. package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
  120. package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
  121. package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
  122. package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
  123. package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
  124. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
  125. package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
  126. package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
  127. package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
  128. package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
  129. package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
  130. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
  131. package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
  132. package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
  133. package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
  134. package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
  135. package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
  136. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
  137. package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +6 -2
  138. package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
  139. package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
  140. package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
  141. package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
  142. package/src/console/ui-next/src/router.tsx +17 -0
  143. package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
  144. package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
  145. package/src/console/ui-next/vite.config.ts +9 -0
  146. package/src/docker/recipes/dragonfly.ts +3 -2
  147. package/src/docker/recipes/redis.ts +1 -1
  148. package/src/drivers/ai-anthropic.ts +212 -21
  149. package/src/drivers/ai-mock.ts +34 -1
  150. package/src/drivers/ai-openai-compatible.ts +67 -26
  151. package/src/drivers/ai-preconnect.ts +18 -0
  152. package/src/drivers/ai-stream.test.ts +62 -0
  153. package/src/drivers/ai-types.ts +20 -0
  154. package/src/drivers/journal-postgres.ts +289 -2
  155. package/src/drivers/memory-ddl.test.ts +18 -0
  156. package/src/drivers/memory.ts +21 -1
  157. package/src/drivers/pg-rls.test.ts +31 -0
  158. package/src/drivers/pg-rls.ts +15 -0
  159. package/src/drivers/postgres.test.ts +9 -0
  160. package/src/drivers/postgres.ts +49 -11
  161. package/src/elements/ai/agent-event-slot.ts +27 -0
  162. package/src/elements/ai/approval-http.ts +207 -0
  163. package/src/elements/ai/approval.test.ts +741 -0
  164. package/src/elements/ai/approval.ts +228 -0
  165. package/src/elements/ai/decisions/bind.ts +163 -0
  166. package/src/elements/ai/decisions/certificate.test.ts +291 -0
  167. package/src/elements/ai/decisions/certificate.ts +377 -0
  168. package/src/elements/ai/decisions/certify.ts +314 -0
  169. package/src/elements/ai/decisions/decide.test.ts +953 -0
  170. package/src/elements/ai/decisions/e2e.test.ts +399 -0
  171. package/src/elements/ai/decisions/export.test.ts +120 -0
  172. package/src/elements/ai/decisions/export.ts +146 -0
  173. package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
  174. package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
  175. package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
  176. package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
  177. package/src/elements/ai/decisions/http.test.ts +154 -0
  178. package/src/elements/ai/decisions/http.ts +240 -0
  179. package/src/elements/ai/decisions/labels.test.ts +282 -0
  180. package/src/elements/ai/decisions/labels.ts +257 -0
  181. package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
  182. package/src/elements/ai/decisions/openrouter.ts +36 -0
  183. package/src/elements/ai/decisions/provider.ts +102 -0
  184. package/src/elements/ai/decisions/typesafe.ts +36 -0
  185. package/src/elements/ai/declare.ts +252 -1
  186. package/src/elements/ai/events.test.ts +379 -0
  187. package/src/elements/ai/events.ts +138 -0
  188. package/src/elements/ai/run-events.test.ts +461 -0
  189. package/src/elements/ai/run-events.ts +522 -0
  190. package/src/elements/ai/runtime.ts +1186 -105
  191. package/src/elements/ai/stream-turn.ts +214 -0
  192. package/src/elements/ai/stream.live.test.ts +76 -0
  193. package/src/elements/ai/subagent.test.ts +136 -0
  194. package/src/elements/ai.test.ts +497 -3
  195. package/src/elements/ai.ts +16 -1
  196. package/src/elements/clock/durable.ts +27 -1
  197. package/src/elements/store/sql-session.test.ts +51 -0
  198. package/src/elements/store/sql-session.ts +111 -1
  199. package/src/i18n/catalogs/ar.ts +5 -0
  200. package/src/i18n/catalogs/en.ts +5 -0
  201. package/src/index.ts +2 -0
  202. package/src/kernel/agent-event-store.ts +448 -0
  203. package/src/kernel/app.ts +103 -1
  204. package/src/kernel/boot-bind/ai.ts +3 -0
  205. package/src/kernel/boot-bind/store.test.ts +26 -0
  206. package/src/kernel/boot-bind/store.ts +82 -6
  207. package/src/kernel/boot.ts +15 -4
  208. package/src/kernel/builtin-errors.ts +2 -0
  209. package/src/kernel/capability.ts +3 -0
  210. package/src/kernel/decision-label-store.ts +441 -0
  211. package/src/kernel/effects.test.ts +2 -1
  212. package/src/kernel/effects.ts +13 -1
  213. package/src/kernel/element-registries.ts +3 -0
  214. package/src/kernel/errors-text.ts +4 -0
  215. package/src/kernel/errors.ts +2 -0
  216. package/src/kernel/flow.test.ts +2 -3
  217. package/src/kernel/fx-decide.ts +761 -0
  218. package/src/kernel/fx.test.ts +12 -5
  219. package/src/kernel/fx.ts +312 -29
  220. package/src/kernel/http-frame.test.ts +94 -0
  221. package/src/kernel/http-frame.ts +203 -0
  222. package/src/kernel/journal.ts +104 -0
  223. package/src/kernel/json-result.ts +10 -0
  224. package/src/kernel/pipeline.test.ts +45 -0
  225. package/src/kernel/sse-id.ts +32 -0
  226. package/src/manifest/diff.ts +1 -0
  227. package/src/manifest/types.ts +29 -2
  228. package/src/mcp/ai-tools.test.ts +202 -0
  229. package/src/mcp/authorization.ts +66 -0
  230. package/src/mcp/session.ts +3 -0
  231. package/src/mcp/tools.ts +168 -0
  232. package/src/release/http-graph.test.ts +2 -0
  233. package/src/release/http-graph.ts +5 -0
  234. package/src/release/measure.ts +39 -14
  235. package/src/runs/collect.ts +4 -1
  236. package/src/runs/parquet.test.ts +27 -1
  237. package/src/runs/parquet.ts +19 -0
  238. package/src/runs/types.ts +31 -0
  239. package/src/runtime/json-code-block.test.ts +106 -47
  240. package/src/runtime/json-code-block.ts +1209 -46
  241. package/src/term.test.ts +13 -0
  242. package/src/term.ts +81 -49
  243. package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
  244. package/src/console/ui-next/dist/assets/access-page-Ze6ckjdd.js +0 -4
  245. package/src/console/ui-next/dist/assets/copy-inline-button-CWG5_ZOh.js +0 -1
  246. package/src/console/ui-next/dist/assets/detail-header-b_ZzIsmU.js +0 -1
  247. package/src/console/ui-next/dist/assets/dropdown-menu-nwXEO1Ac.js +0 -1
  248. package/src/console/ui-next/dist/assets/duration-tone-BbQ_8z50.js +0 -9
  249. package/src/console/ui-next/dist/assets/element-icons-CwyLpVXz.js +0 -1
  250. package/src/console/ui-next/dist/assets/explorer-empty-C8sSCqYT.js +0 -1
  251. package/src/console/ui-next/dist/assets/flows-page-D2E7BtKK.js +0 -1
  252. package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
  253. package/src/console/ui-next/dist/assets/index-vTuwmeQz.js +0 -58
  254. package/src/console/ui-next/dist/assets/observability-page-SJTSzHFJ.js +0 -4
  255. package/src/console/ui-next/dist/assets/request-meta-BjFT7DNl.js +0 -1
  256. package/src/console/ui-next/dist/assets/shortcut-keys-CUNegEV4.js +0 -1
  257. package/src/console/ui-next/dist/assets/store-page-Qj9ThRE-.js +0 -41
  258. package/src/console/ui-next/dist/assets/trace-detail-sheet-Cmitq3da.js +0 -2
  259. package/src/console/ui-next/dist/assets/tree-expand-toggle-CbDIB-7x.js +0 -55
  260. package/src/console/ui-next/dist/assets/units-page-_PuYqFty.js +0 -1
  261. package/src/console/ui-next/dist/assets/use-vault-list-Cs45Czkt.js +0 -1
  262. package/src/console/ui-next/dist/assets/vault-page-BOMr0go9.js +0 -2
@@ -2,6 +2,7 @@
2
2
  * Browser GET → traces-language JSON page. Clients still get the envelope.
3
3
  */
4
4
 
5
+ import { HTTP_FRAME_REDACTED, sensitiveHeader } from "../kernel/http-frame.ts";
5
6
  import { CONSOLE_PORT } from "./types.ts";
6
7
 
7
8
  /** One highlighted JSON span. */
@@ -32,6 +33,11 @@ export interface JsonCodeBlockRenderOptions {
32
33
  readonly cache?: JsonCodeCache;
33
34
  /** Auth principal that handled the request (traces language). */
34
35
  readonly auth?: JsonCodeAuth;
36
+ /**
37
+ * Response headers for the Fields view. Credential names are shown as
38
+ * `[redacted]`. Omit `content-length` — the page replaces the body.
39
+ */
40
+ readonly headers?: Readonly<Record<string, string>>;
35
41
  }
36
42
 
37
43
  /** Wide-event cache dimension on the browser JSON page. */
@@ -481,6 +487,251 @@ export function jsonCodeLatencyTone(ms: number): JsonCodeLatencyTone {
481
487
  return "critical";
482
488
  }
483
489
 
490
+ /** Scalar or container kind for one response field. */
491
+ type ResponseFieldKind = "string" | "number" | "boolean" | "null" | "object" | "array";
492
+
493
+ /** One expandable row in the response Fields view. */
494
+ interface ResponseFieldRow {
495
+ readonly key: string;
496
+ /** Path from the JSON root, for copy. */
497
+ readonly path: readonly (string | number)[];
498
+ readonly display: string;
499
+ readonly kind: ResponseFieldKind;
500
+ readonly children: readonly ResponseFieldRow[] | null;
501
+ }
502
+
503
+ const PREVIEW_FIELDS = 3;
504
+ const PREVIEW_SCALAR_MAX = 42;
505
+
506
+ const STATUS_REASON: Readonly<Record<number, string>> = {
507
+ 200: "OK",
508
+ 201: "Created",
509
+ 202: "Accepted",
510
+ 204: "No Content",
511
+ 301: "Moved",
512
+ 302: "Found",
513
+ 304: "Not Modified",
514
+ 400: "Bad Request",
515
+ 401: "Unauthorized",
516
+ 403: "Forbidden",
517
+ 404: "Not Found",
518
+ 405: "Method Not Allowed",
519
+ 409: "Conflict",
520
+ 415: "Unsupported Media Type",
521
+ 422: "Unprocessable Content",
522
+ 429: "Too Many Requests",
523
+ 500: "Internal Server Error",
524
+ 502: "Bad Gateway",
525
+ 503: "Service Unavailable",
526
+ };
527
+
528
+ const FIELD_CHEV = `<span class="chev" aria-hidden="true"><svg width="12" height="12" viewBox="0 0 24 24" fill="none"><path d="M6 9.5 12 15.5 18 9.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></span>`;
529
+ const FIELD_COPY = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" aria-hidden="true"><rect x="8" y="8" width="11" height="11" rx="1.5" stroke="currentColor" stroke-width="1.5"/><path d="M5 16V5.5A1.5 1.5 0 0 1 6.5 4H16" stroke="currentColor" stroke-width="1.5"/></svg>`;
530
+
531
+ /**
532
+ * Project a JSON value into field rows. Scalars and empty containers return null.
533
+ *
534
+ * @param value - Parsed response body or header map
535
+ * @param path - Path from the root
536
+ */
537
+ function responseFieldRows(
538
+ value: unknown,
539
+ path: readonly (string | number)[] = [],
540
+ ): readonly ResponseFieldRow[] | null {
541
+ if (Array.isArray(value)) {
542
+ if (value.length === 0) return null;
543
+ return value.map((item, index) => projectResponseField(String(index), item, [...path, index]));
544
+ }
545
+ if (value === null || typeof value !== "object") return null;
546
+ const entries = Object.entries(value as Record<string, unknown>);
547
+ if (entries.length === 0) return null;
548
+ return entries.map(([key, item]) => projectResponseField(key, item, [...path, key]));
549
+ }
550
+
551
+ /**
552
+ * Short shape label for a body or container (`2 fields`, `5 items`).
553
+ *
554
+ * @param value - Parsed JSON value
555
+ */
556
+ function responseShapeHint(value: unknown): string | null {
557
+ if (Array.isArray(value)) {
558
+ return `${value.length} ${value.length === 1 ? "item" : "items"}`;
559
+ }
560
+ if (value !== null && typeof value === "object") {
561
+ const n = Object.keys(value).length;
562
+ return `${n} ${n === 1 ? "field" : "fields"}`;
563
+ }
564
+ return null;
565
+ }
566
+
567
+ /**
568
+ * Compact byte size of the serialized body.
569
+ *
570
+ * @param json - Serialized JSON text
571
+ */
572
+ function responseByteLabel(json: string): string {
573
+ const bytes = new TextEncoder().encode(json).length;
574
+ if (bytes < 1024) return `${bytes} B`;
575
+ return `${(bytes / 1024).toFixed(1)} KB`;
576
+ }
577
+
578
+ /**
579
+ * Fields / JSON control for the response header. Same pair as Console traces.
580
+ *
581
+ * @param hasFields - Whether the body can open as rows
582
+ * @param compact - Compact JSON is the code view
583
+ */
584
+ function responseViewToggleHtml(hasFields: boolean, compact: boolean): string {
585
+ if (!hasFields) return "";
586
+ const fieldsOn = !compact;
587
+ const token = (view: "fields" | "json", label: string, icon: string, on: boolean) =>
588
+ `<button type="button" class="token${on ? " is-on" : ""}" data-response-view="${view}" aria-pressed="${on ? "true" : "false"}"><span class="tok-ico">${icon}</span>${label}</button>`;
589
+ return `<span class="view-toggle" role="group" aria-label="Response view" data-slot="json-code-view-toggle">${token("fields", "Fields", GLYPH.list, fieldsOn)}${token("json", "JSON", GLYPH.braces, !fieldsOn)}</span>`;
590
+ }
591
+
592
+ /**
593
+ * Status, response headers, and an expandable body — the Console response frame.
594
+ *
595
+ * @param options - Envelope + headers
596
+ * @param rows - Projected body rows
597
+ * @param value - Parsed body
598
+ * @param bodyJson - Highlighted body, shown when the header is on JSON
599
+ */
600
+ function responsePaneHtml(
601
+ options: JsonCodeBlockRenderOptions,
602
+ rows: readonly ResponseFieldRow[],
603
+ value: unknown,
604
+ bodyJson: string,
605
+ ): string {
606
+ const tone = options.status >= 500 ? "err" : options.status >= 400 ? "warn" : "ok";
607
+ const reason = STATUS_REASON[options.status] ?? "";
608
+ const hint = [responseShapeHint(value), responseByteLabel(options.json)]
609
+ .filter(Boolean)
610
+ .join(" · ");
611
+ const headers = jsonCodeResponseHeaders(options.headers);
612
+ const headerRows = responseFieldRows(headers);
613
+ const headerBlock =
614
+ headerRows && headerRows.length > 0
615
+ ? `<details class="resp-block" open><summary class="resp-label">${FIELD_CHEV}<span>Headers</span><span class="resp-hint">${headerRows.length} ${headerRows.length === 1 ? "field" : "fields"}</span></summary><ul class="fields" data-slot="json-code-response-headers">${fieldListHtml(headerRows)}</ul></details>`
616
+ : "";
617
+ return `<div class="resp" data-slot="json-code-response"><div class="resp-frame"><div class="resp-rail resp-${tone}" data-slot="json-code-response-rail"></div><div class="resp-main"><div class="resp-status" data-slot="json-code-response-status"><span class="resp-code ${tone}">${options.status}</span>${reason ? `<span class="resp-reason">${escapeHtml(reason)}</span>` : ""}</div>${headerBlock}<div class="resp-body" data-slot="json-code-response-body"><div class="resp-label"><span>Body</span>${hint ? `<span class="resp-hint">${escapeHtml(hint)}</span>` : ""}</div><ul class="fields" data-slot="json-code-fields">${fieldListHtml(rows)}</ul><pre>${bodyJson}</pre></div></div></div></div>`;
618
+ }
619
+
620
+ /**
621
+ * Redacted response headers for the Fields view.
622
+ *
623
+ * @param headers - Outgoing response headers, before the page replaces the body
624
+ */
625
+ export function jsonCodeResponseHeaders(
626
+ headers: Headers | Readonly<Record<string, string>> | undefined,
627
+ ): Record<string, string> {
628
+ const out: Record<string, string> = {};
629
+ const write = (key: string, value: string): void => {
630
+ const name = key.toLowerCase();
631
+ if (name === "content-length") return;
632
+ out[name] = sensitiveHeader(name) ? HTTP_FRAME_REDACTED : value;
633
+ };
634
+ if (headers instanceof Headers) {
635
+ headers.forEach((value, key) => {
636
+ write(key, value);
637
+ });
638
+ return out;
639
+ }
640
+ if (headers) {
641
+ for (const [key, value] of Object.entries(headers)) write(key, value);
642
+ }
643
+ return out;
644
+ }
645
+
646
+ function projectResponseField(
647
+ key: string,
648
+ value: unknown,
649
+ path: readonly (string | number)[],
650
+ ): ResponseFieldRow {
651
+ return {
652
+ key,
653
+ path,
654
+ display: formatResponseField(value),
655
+ kind: responseFieldKind(value),
656
+ children: responseFieldRows(value, path),
657
+ };
658
+ }
659
+
660
+ function responseFieldKind(value: unknown): ResponseFieldKind {
661
+ if (value === null) return "null";
662
+ if (Array.isArray(value)) return "array";
663
+ switch (typeof value) {
664
+ case "string":
665
+ return "string";
666
+ case "number":
667
+ return "number";
668
+ case "boolean":
669
+ return "boolean";
670
+ case "object":
671
+ return "object";
672
+ default:
673
+ return "string";
674
+ }
675
+ }
676
+
677
+ function formatResponseField(value: unknown): string {
678
+ if (value === null) return "null";
679
+ if (typeof value === "string") return value;
680
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
681
+ if (Array.isArray(value)) {
682
+ if (value.length === 0) return "[]";
683
+ return `${value.length} ${value.length === 1 ? "item" : "items"}`;
684
+ }
685
+ if (typeof value === "object") return objectPreview(value as Record<string, unknown>);
686
+ return typeof value;
687
+ }
688
+
689
+ /** Up to three short scalar fields, otherwise a count. */
690
+ function objectPreview(value: Record<string, unknown>): string {
691
+ const keys = Object.keys(value);
692
+ if (keys.length === 0) return "{}";
693
+ const bits: string[] = [];
694
+ for (const [key, item] of Object.entries(value)) {
695
+ if (bits.length >= PREVIEW_FIELDS) break;
696
+ if (typeof item !== "string" && typeof item !== "number" && typeof item !== "boolean") continue;
697
+ const text = typeof item === "string" ? item : String(item);
698
+ if (text.length === 0 || text.length > PREVIEW_SCALAR_MAX) continue;
699
+ bits.push(`${key}: ${text}`);
700
+ }
701
+ if (bits.length > 0) return bits.join(" · ");
702
+ return `${keys.length} ${keys.length === 1 ? "field" : "fields"}`;
703
+ }
704
+
705
+ function fieldListHtml(rows: readonly ResponseFieldRow[]): string {
706
+ return rows.map((row) => fieldRowHtml(row)).join("");
707
+ }
708
+
709
+ function fieldRowHtml(row: ResponseFieldRow): string {
710
+ const depth = Math.max(0, row.path.length - 1);
711
+ const tone =
712
+ row.kind === "string"
713
+ ? " s"
714
+ : row.kind === "number"
715
+ ? " m"
716
+ : row.kind === "boolean"
717
+ ? " l"
718
+ : "";
719
+ const copy = `<button type="button" class="field-copy" data-field-copy="${escapeHtml(JSON.stringify(row.path))}" aria-label="Copy ${escapeHtml(row.key)}">${FIELD_COPY}</button>`;
720
+ const label = `<span class="field-key">${escapeHtml(row.key)}</span><span class="field-val${tone}${row.kind === "null" ? " is-null" : ""}">${escapeHtml(row.display)}</span><span class="field-kind">${row.kind}</span>`;
721
+ if (row.children) {
722
+ return `<li class="field" data-slot="json-code-field" data-kind="${row.kind}"><details><summary class="field-row" style="--depth:${depth}">${FIELD_CHEV}${label}</summary><ul class="field-kids">${fieldListHtml(row.children)}</ul></details>${copy}</li>`;
723
+ }
724
+ return `<li class="field" data-slot="json-code-field" data-kind="${row.kind}"><div class="field-row" style="--depth:${depth}"><span class="chev" aria-hidden="true"></span>${label}</div>${copy}</li>`;
725
+ }
726
+
727
+ function parseJsonValue(json: string): unknown {
728
+ try {
729
+ return JSON.parse(json) as unknown;
730
+ } catch {
731
+ return undefined;
732
+ }
733
+ }
734
+
484
735
  /**
485
736
  * Full-bleed traces-language page for one JSON envelope.
486
737
  *
@@ -488,6 +739,10 @@ export function jsonCodeLatencyTone(ms: number): JsonCodeLatencyTone {
488
739
  */
489
740
  export function renderJsonCodeBlockHtml(options: JsonCodeBlockRenderOptions): string {
490
741
  const compact = options.compact === true;
742
+ const parsed = parseJsonValue(options.json);
743
+ const fieldRows = parsed === undefined ? null : responseFieldRows(parsed);
744
+ const hasFields = fieldRows !== null && fieldRows.length > 0;
745
+ const view = compact ? "raw" : hasFields ? "fields" : "pretty";
491
746
  const code = formatJson(options.json, compact);
492
747
  const tokens = tokenizeJson(code);
493
748
  const lines = splitTokenLines(tokens);
@@ -500,6 +755,8 @@ export function renderJsonCodeBlockHtml(options: JsonCodeBlockRenderOptions): st
500
755
  return `<span class="ln"><span class="n">${n}</span><span class="c${compact ? " wrap" : ""}">${inner}</span></span>`;
501
756
  })
502
757
  .join("");
758
+ const responseHtml =
759
+ hasFields && fieldRows ? responsePaneHtml(options, fieldRows, parsed, rows) : "";
503
760
 
504
761
  return `<!doctype html>
505
762
  <html lang="en">
@@ -548,6 +805,21 @@ export function renderJsonCodeBlockHtml(options: JsonCodeBlockRenderOptions): st
548
805
  .cache-hit { color: oklch(0.685 0.169 237.323); }
549
806
  .cache-miss { color: oklch(0.666 0.179 58.318); }
550
807
  .auth-user, .auth-key, .auth-operator { color: oklch(0.685 0.169 237.323); }
808
+ .st-ok, .meth-get, .meth-query, .icon.meth-get, .icon.meth-query { color: oklch(0.55 0.14 163); }
809
+ .st-info, .meth-post, .icon.meth-post { color: oklch(0.5 0.12 230); }
810
+ .st-warn, .meth-put, .icon.meth-put { color: oklch(0.55 0.14 75); }
811
+ .meth-patch, .icon.meth-patch { color: oklch(0.48 0.16 300); }
812
+ .st-err, .meth-delete, .icon.meth-delete { color: oklch(0.55 0.2 22); }
813
+ .rail-acc[data-rail-section="query"] { --sec: oklch(0.5 0.12 230); }
814
+ .rail-acc[data-rail-section="body"] { --sec: oklch(0.55 0.14 75); }
815
+ .rail-acc[data-rail-section="cookies"] { --sec: oklch(0.55 0.14 55); }
816
+ .rail-acc[data-rail-section="headers"] { --sec: oklch(0.48 0.16 300); }
817
+ .rail-acc[data-rail-section="auth"] { --sec: oklch(0.48 0.12 175); }
818
+ .rail-acc[data-rail-section="path"] { --sec: oklch(0.48 0.12 250); }
819
+ .token[data-slot="json-code-global-auth"] .tok-ico { color: oklch(0.48 0.12 175); }
820
+ .token[data-slot="json-code-global-headers"] .tok-ico { color: oklch(0.48 0.16 300); }
821
+ .view-toggle .token.is-on[data-response-view="fields"] .tok-ico { color: oklch(0.5 0.12 230); }
822
+ .view-toggle .token.is-on[data-response-view="json"] .tok-ico { color: oklch(0.55 0.14 75); }
551
823
  }
552
824
  * { box-sizing: border-box; }
553
825
  html, body {
@@ -582,6 +854,7 @@ body {
582
854
  .count {
583
855
  display: inline-flex;
584
856
  align-items: center;
857
+ gap: .3rem;
585
858
  padding: 0 .5rem;
586
859
  font-size: 10px;
587
860
  font-weight: 500;
@@ -604,6 +877,35 @@ body {
604
877
  .auth-user, .auth-key, .auth-operator { color: oklch(0.746 0.16 232.661); }
605
878
  .auth-public { color: var(--mute); }
606
879
  .auth-none { color: color-mix(in oklab, var(--mute) 40%, transparent); }
880
+ .st-ok { color: var(--ready); }
881
+ .st-info { color: oklch(0.746 0.16 232.661); }
882
+ .st-warn { color: oklch(0.828 0.189 84.429); }
883
+ .st-err { color: var(--fail); }
884
+ .meth-get, .meth-query { color: var(--ready); }
885
+ .meth-post { color: oklch(0.746 0.16 232.661); }
886
+ .meth-put { color: oklch(0.828 0.189 84.429); }
887
+ .meth-patch { color: oklch(0.78 0.14 300); }
888
+ .meth-delete { color: var(--fail); }
889
+ .meth-head, .meth-options, .meth-other { color: var(--mute); }
890
+ .verb { font-weight: 600; }
891
+ .file-path { color: var(--ink); }
892
+ .kind { color: var(--str); }
893
+ .tok-ico, .sec-ico {
894
+ display: inline-flex;
895
+ align-items: center;
896
+ flex-shrink: 0;
897
+ }
898
+ .rail-acc[data-rail-section="query"] { --sec: oklch(0.746 0.16 232.661); }
899
+ .rail-acc[data-rail-section="body"] { --sec: oklch(0.828 0.189 84.429); }
900
+ .rail-acc[data-rail-section="cookies"] { --sec: oklch(0.75 0.16 55); }
901
+ .rail-acc[data-rail-section="headers"] { --sec: oklch(0.78 0.13 300); }
902
+ .rail-acc[data-rail-section="auth"] { --sec: oklch(0.765 0.15 175); }
903
+ .rail-acc[data-rail-section="path"] { --sec: oklch(0.78 0.09 250); }
904
+ .sec-ico { color: var(--sec, var(--mute)); }
905
+ .token[data-slot="json-code-global-auth"] .tok-ico { color: oklch(0.765 0.15 175); }
906
+ .token[data-slot="json-code-global-headers"] .tok-ico { color: oklch(0.78 0.13 300); }
907
+ .view-toggle .token.is-on[data-response-view="fields"] .tok-ico { color: oklch(0.746 0.16 232.661); }
908
+ .view-toggle .token.is-on[data-response-view="json"] .tok-ico { color: oklch(0.828 0.189 84.429); }
607
909
  .auth-label {
608
910
  overflow: hidden;
609
911
  text-overflow: ellipsis;
@@ -617,6 +919,7 @@ body {
617
919
  padding: 0 .5rem;
618
920
  font-size: 10px;
619
921
  font-weight: 500;
922
+ gap: .3rem;
620
923
  letter-spacing: .08em;
621
924
  text-transform: uppercase;
622
925
  text-decoration: none;
@@ -630,6 +933,9 @@ body {
630
933
  }
631
934
  .token:hover { background: var(--hover); color: var(--ink); }
632
935
  .token.is-on { color: var(--ink); }
936
+ .token[aria-expanded="true"] { color: var(--ink); }
937
+ .rail-acc .token.is-on { color: var(--sec, var(--ink)); }
938
+ .rail-acc[open] > .rail-acc-sum .sec-ico { color: var(--sec); }
633
939
  .sep {
634
940
  width: 1px;
635
941
  align-self: stretch;
@@ -643,9 +949,15 @@ body {
643
949
  width: 2rem;
644
950
  color: var(--mute);
645
951
  }
952
+ .icon.meth-get, .icon.meth-query { color: var(--ready); }
953
+ .icon.meth-post { color: oklch(0.746 0.16 232.661); }
954
+ .icon.meth-put { color: oklch(0.828 0.189 84.429); }
955
+ .icon.meth-patch { color: oklch(0.78 0.14 300); }
956
+ .icon.meth-delete { color: var(--fail); }
646
957
  .file {
647
958
  display: inline-flex;
648
959
  align-items: center;
960
+ gap: .4rem;
649
961
  min-width: 0;
650
962
  padding-right: .5rem;
651
963
  overflow: hidden;
@@ -692,6 +1004,97 @@ body {
692
1004
  svg[hidden] { display: none !important; }
693
1005
  .body { display: flex; flex: 1; min-height: 0; }
694
1006
  .view { flex: 1; min-width: 0; min-height: 0; overflow: auto; }
1007
+ .page[data-view="fields"] .resp-body > pre { display: none; }
1008
+ .page:not([data-view="fields"]) .resp-body > .fields { display: none; }
1009
+ .view-toggle { display: inline-flex; align-items: stretch; height: 100%; }
1010
+ .resp { min-height: 100%; }
1011
+ .resp-frame { display: flex; min-height: 100%; }
1012
+ .resp-rail { width: 2px; flex-shrink: 0; }
1013
+ .resp-rail.ok { background: var(--ready); }
1014
+ .resp-rail.warn { background: oklch(0.8 0.12 80); }
1015
+ .resp-rail.err { background: var(--fail); }
1016
+ .resp-main { flex: 1; min-width: 0; }
1017
+ .resp-status { display: flex; align-items: baseline; gap: .5rem; padding: .5rem .625rem; }
1018
+ .resp-code { font: 600 10px/1 ui-monospace, SFMono-Regular, Menlo, monospace; font-variant-numeric: tabular-nums; }
1019
+ .resp-code.ok { color: var(--ready); }
1020
+ .resp-code.warn { color: oklch(0.8 0.12 80); }
1021
+ .resp-code.err { color: var(--fail); }
1022
+ .resp-reason { font: 12px/1.2 ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--ink); }
1023
+ .resp-label {
1024
+ display: flex;
1025
+ align-items: center;
1026
+ gap: .375rem;
1027
+ min-height: 2rem;
1028
+ padding: 0 .5rem;
1029
+ border-top: 1px solid var(--line);
1030
+ font: 600 10px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
1031
+ letter-spacing: .08em;
1032
+ text-transform: uppercase;
1033
+ color: var(--mute);
1034
+ cursor: pointer;
1035
+ }
1036
+ .resp-label::-webkit-details-marker { display: none; }
1037
+ .resp-hint {
1038
+ font-weight: 400;
1039
+ letter-spacing: 0;
1040
+ text-transform: none;
1041
+ color: color-mix(in oklab, var(--mute) 80%, transparent);
1042
+ }
1043
+ .resp-block { border: 0; }
1044
+ .fields, .field-kids { margin: 0; padding: 0; list-style: none; }
1045
+ .field { position: relative; border-bottom: 1px solid var(--line); }
1046
+ .field-row {
1047
+ display: flex;
1048
+ align-items: flex-start;
1049
+ gap: .5rem;
1050
+ padding: .375rem 2rem .375rem calc(.625rem + var(--depth, 0) * .875rem);
1051
+ }
1052
+ .field-row:hover { background: var(--hover); }
1053
+ summary.field-row { cursor: pointer; }
1054
+ summary.field-row::-webkit-details-marker { display: none; }
1055
+ details:not([open]) > summary .chev { transform: rotate(-90deg); }
1056
+ .field-key {
1057
+ width: 7.5rem;
1058
+ flex-shrink: 0;
1059
+ overflow: hidden;
1060
+ text-overflow: ellipsis;
1061
+ font: 500 11px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace;
1062
+ color: var(--key);
1063
+ }
1064
+ .field-val {
1065
+ flex: 1;
1066
+ min-width: 0;
1067
+ overflow-wrap: anywhere;
1068
+ font: 11px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace;
1069
+ color: var(--ink);
1070
+ }
1071
+ .field-val.is-null { color: var(--mute); font-style: italic; }
1072
+ .field-kind {
1073
+ flex-shrink: 0;
1074
+ margin-top: .15rem;
1075
+ font: 9px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
1076
+ letter-spacing: .06em;
1077
+ text-transform: uppercase;
1078
+ color: color-mix(in oklab, var(--mute) 70%, transparent);
1079
+ }
1080
+ .field-copy {
1081
+ position: absolute;
1082
+ top: .15rem;
1083
+ right: .15rem;
1084
+ display: inline-flex;
1085
+ align-items: center;
1086
+ justify-content: center;
1087
+ width: 1.5rem;
1088
+ height: 1.5rem;
1089
+ padding: 0;
1090
+ border: 0;
1091
+ background: transparent;
1092
+ color: var(--mute);
1093
+ cursor: pointer;
1094
+ opacity: 0;
1095
+ }
1096
+ .field:hover > .field-copy, .field:focus-within > .field-copy { opacity: 1; }
1097
+ .field-copy:hover { color: var(--ink); }
695
1098
  .rail-check { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
696
1099
  .rail {
697
1100
  display: flex;
@@ -711,18 +1114,22 @@ svg[hidden] { display: none !important; }
711
1114
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
712
1115
  }
713
1116
  .rail-nav {
714
- flex: 1;
715
- min-height: 0;
716
- overflow: auto;
717
1117
  display: flex;
718
1118
  flex-direction: column;
1119
+ flex-shrink: 0;
1120
+ max-height: 45%;
1121
+ min-height: 0;
1122
+ overflow: auto;
1123
+ border-top: 1px solid var(--line);
719
1124
  }
1125
+ .routes-check { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
720
1126
  .rail-sec-label {
721
1127
  display: flex;
722
1128
  align-items: center;
1129
+ justify-content: space-between;
723
1130
  height: 2.5rem;
724
1131
  margin: 0;
725
- padding: 0 .5rem;
1132
+ padding: 0 .25rem 0 .5rem;
726
1133
  flex-shrink: 0;
727
1134
  border-bottom: 1px solid var(--line);
728
1135
  font-size: 10px;
@@ -731,13 +1138,15 @@ svg[hidden] { display: none !important; }
731
1138
  text-transform: uppercase;
732
1139
  color: var(--mute);
733
1140
  }
1141
+ .routes-check:not(:checked) ~ .rail-list { display: none; }
1142
+ .routes-check:not(:checked) ~ .rail-sec-label { border-bottom: 0; }
1143
+ .routes-check:checked ~ .rail-sec-label .routes-expand { display: none; }
1144
+ .routes-check:not(:checked) ~ .rail-sec-label .routes-collapse { display: none; }
734
1145
  .rail-dock {
735
1146
  display: flex;
736
1147
  flex-direction: column;
737
- flex-shrink: 0;
738
- max-height: 55%;
1148
+ flex: 1;
739
1149
  min-height: 0;
740
- border-top: 1px solid var(--line);
741
1150
  }
742
1151
  .rail-dock-strip {
743
1152
  justify-content: flex-start;
@@ -815,6 +1224,7 @@ svg[hidden] { display: none !important; }
815
1224
  flex-direction: column;
816
1225
  min-height: 0;
817
1226
  }
1227
+ .rail-acc-body[hidden] { display: none; }
818
1228
  .kv-editor {
819
1229
  display: flex;
820
1230
  flex-direction: column;
@@ -854,6 +1264,111 @@ svg[hidden] { display: none !important; }
854
1264
  font-size: 11px;
855
1265
  color: var(--mute);
856
1266
  }
1267
+ .auth-field {
1268
+ display: flex;
1269
+ align-items: stretch;
1270
+ height: 2rem;
1271
+ border-bottom: 1px solid var(--line);
1272
+ }
1273
+ .auth-lab {
1274
+ display: flex;
1275
+ align-items: center;
1276
+ width: 42%;
1277
+ max-width: 42%;
1278
+ flex-shrink: 0;
1279
+ padding: 0 .5rem;
1280
+ border-right: 1px solid var(--line);
1281
+ font: 11px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
1282
+ color: var(--mute);
1283
+ }
1284
+ .auth-field .kv-val { flex: 1; }
1285
+ .auth-field .kv-val::-ms-reveal { display: none; }
1286
+ .auth-reveal {
1287
+ display: grid;
1288
+ place-items: center;
1289
+ flex-shrink: 0;
1290
+ width: 2.5rem;
1291
+ border: 0;
1292
+ border-left: 1px solid var(--line);
1293
+ background: transparent;
1294
+ color: var(--mute);
1295
+ cursor: pointer;
1296
+ }
1297
+ .auth-reveal:hover { color: var(--ink); background: var(--hover); }
1298
+ .auth-reveal .eye-shut { display: none; }
1299
+ .auth-reveal[aria-pressed="true"] .eye-open { display: none; }
1300
+ .auth-reveal[aria-pressed="true"] .eye-shut { display: block; }
1301
+ .auth-in {
1302
+ display: inline-flex;
1303
+ align-items: stretch;
1304
+ flex: 1;
1305
+ min-width: 0;
1306
+ }
1307
+ .auth-pane[hidden] { display: none; }
1308
+ .global-pop {
1309
+ display: flex;
1310
+ align-items: stretch;
1311
+ height: 2.5rem;
1312
+ flex-shrink: 0;
1313
+ background: var(--field);
1314
+ border-bottom: 1px solid var(--line);
1315
+ }
1316
+ .global-pop[hidden] { display: none; }
1317
+ .global-line {
1318
+ display: flex;
1319
+ flex: 1;
1320
+ min-width: 0;
1321
+ align-items: stretch;
1322
+ }
1323
+ .global-line[hidden] { display: none; }
1324
+ .global-pop .auth-editor,
1325
+ .global-pop .auth-pane:not([hidden]) {
1326
+ display: flex;
1327
+ flex: 1;
1328
+ min-width: 0;
1329
+ align-items: stretch;
1330
+ }
1331
+ .global-pop .body-mode-strip { margin: 0; border-right: 1px solid var(--line); }
1332
+ .global-pop .auth-pane[hidden],
1333
+ .global-pop .kv-empty[hidden] { display: none; }
1334
+ .global-pop .auth-field {
1335
+ flex: 1;
1336
+ height: auto;
1337
+ min-width: 8rem;
1338
+ border-bottom: 0;
1339
+ }
1340
+ .global-pop .auth-lab {
1341
+ width: auto;
1342
+ max-width: none;
1343
+ padding: 0 .75rem;
1344
+ color: color-mix(in oklab, var(--mute) 80%, transparent);
1345
+ }
1346
+ .global-pop .kv-empty {
1347
+ display: flex;
1348
+ align-items: center;
1349
+ flex: 1;
1350
+ margin: 0;
1351
+ padding: 0 .75rem;
1352
+ white-space: nowrap;
1353
+ overflow: hidden;
1354
+ text-overflow: ellipsis;
1355
+ }
1356
+ .global-pop .kv-editor,
1357
+ .global-pop .kv-rows {
1358
+ display: flex;
1359
+ flex: 1;
1360
+ min-width: 0;
1361
+ max-height: none;
1362
+ align-items: stretch;
1363
+ overflow-x: auto;
1364
+ }
1365
+ .global-pop .kv-row {
1366
+ flex: 1 0 18rem;
1367
+ height: auto;
1368
+ border-bottom: 0;
1369
+ border-right: 1px solid var(--line);
1370
+ }
1371
+ .global-pop .kv-row:last-child { border-right: 0; }
857
1372
  .body-mode-strip {
858
1373
  display: inline-flex;
859
1374
  align-items: stretch;
@@ -998,6 +1513,12 @@ details:not(.rail-acc):not([open]) .chev { transform: rotate(-90deg); }
998
1513
  text-transform: uppercase;
999
1514
  color: color-mix(in oklab, var(--mute) 70%, transparent);
1000
1515
  }
1516
+ .leaf[data-method="GET"] .leaf-type,
1517
+ .leaf[data-method="QUERY"] .leaf-type { color: var(--ready); }
1518
+ .leaf[data-method="POST"] .leaf-type { color: oklch(0.746 0.16 232.661); }
1519
+ .leaf[data-method="PUT"] .leaf-type { color: oklch(0.828 0.189 84.429); }
1520
+ .leaf[data-method="PATCH"] .leaf-type { color: oklch(0.78 0.14 300); }
1521
+ .leaf[data-method="DELETE"] .leaf-type { color: var(--fail); }
1001
1522
  .rail-thin {
1002
1523
  display: none;
1003
1524
  width: 1.75rem;
@@ -1022,6 +1543,12 @@ details:not(.rail-acc):not([open]) .chev { transform: rotate(-90deg); }
1022
1543
  .rail-check:not(:checked) ~ .rail-thin { display: flex; }
1023
1544
  @media (prefers-color-scheme: light) {
1024
1545
  .leaf.is-on { background: oklch(0.967 0.001 286.375 / 0.7); }
1546
+ .leaf[data-method="GET"] .leaf-type,
1547
+ .leaf[data-method="QUERY"] .leaf-type { color: oklch(0.55 0.14 163); }
1548
+ .leaf[data-method="POST"] .leaf-type { color: oklch(0.5 0.12 230); }
1549
+ .leaf[data-method="PUT"] .leaf-type { color: oklch(0.55 0.14 75); }
1550
+ .leaf[data-method="PATCH"] .leaf-type { color: oklch(0.48 0.16 300); }
1551
+ .leaf[data-method="DELETE"] .leaf-type { color: oklch(0.55 0.2 22); }
1025
1552
  }
1026
1553
  pre {
1027
1554
  margin: 0;
@@ -1051,45 +1578,91 @@ pre {
1051
1578
  </style>
1052
1579
  </head>
1053
1580
  <body>
1054
- <main class="page" data-slot="json-code-block" data-state="complete" data-view="${compact ? "raw" : "pretty"}" data-status="${options.status}" data-method="${escapeHtml(options.method)}" data-path="${escapeHtml(options.path)}">
1581
+ <main class="page" data-slot="json-code-block" data-state="complete" data-view="${view}" data-code-view="${compact ? "raw" : "pretty"}" data-status="${options.status}" data-method="${escapeHtml(options.method)}" data-path="${escapeHtml(options.path)}">
1055
1582
  <header class="strip">
1056
1583
  <span class="title">${escapeHtml(options.app)}</span>
1057
- <span class="count">${options.status}</span>
1584
+ <span class="count st-${statusTone(options.status)}" data-slot="json-code-status">${GLYPH.dot}${options.status}</span>
1058
1585
  ${latencyHtml(options.latencyMs)}
1059
1586
  ${cacheHtml(options.cache)}
1060
1587
  ${authHtml(options.auth)}
1061
1588
  <span class="grow"></span>
1062
- <span class="sep" aria-hidden="true"></span>
1063
- <a
1064
- class="token is-on"
1065
- data-nav
1066
- data-slot="json-code-view-toggle"
1067
- href="${escapeHtml(compact ? options.prettyHref : options.rawHref)}"
1068
- aria-pressed="${compact ? "true" : "false"}"
1069
- title="${compact ? "Show pretty JSON" : "Show compact JSON"}"
1070
- >${compact ? "Raw" : "Pretty"}</a>
1589
+ <button type="button" class="token" data-slot="json-code-global-auth" aria-expanded="false" aria-controls="json-code-global-panel" title="Global authentication"><span class="tok-ico">${GLYPH.key}</span>Auth</button>
1590
+ <button type="button" class="token" data-slot="json-code-global-headers" aria-expanded="false" aria-controls="json-code-global-panel" title="Global headers"><span class="tok-ico">${GLYPH.list}</span>Headers</button>
1071
1591
  </header>
1592
+ <div class="global-pop" id="json-code-global-panel" data-slot="json-code-global-panel" data-pane="auth" hidden>
1593
+ <div class="global-line" data-global-pane="auth" data-auth-scope="global">
1594
+ ${authEditorHtml("global")}
1595
+ </div>
1596
+ <div class="global-line" data-global-pane="headers" hidden>
1597
+ <div class="kv-editor">
1598
+ <div class="kv-rows" data-slot="json-code-kv-rows" data-kv="headers-global"></div>
1599
+ </div>
1600
+ </div>
1601
+ </div>
1072
1602
  <header class="strip">
1073
- <span class="icon" aria-hidden="true">
1074
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none"><path d="M7 3.5h7.2L19 8.2V20a.5.5 0 0 1-.5.5h-11A.5.5 0 0 1 7 20V3.5Z" stroke="currentColor" stroke-width="1.5"/><path d="M14 3.5V8h5" stroke="currentColor" stroke-width="1.5"/></svg>
1075
- </span>
1076
- <span class="file">${escapeHtml(title)}</span>
1077
- <span class="head">json</span>
1603
+ <span class="icon meth meth-${methodTone(options.method)}" data-slot="json-code-method-icon" aria-hidden="true">${methodGlyph(options.method)}</span>
1604
+ <span class="file"><span class="verb meth-${methodTone(options.method)}" data-slot="json-code-method">${escapeHtml(options.method)}</span><span class="file-path" data-slot="json-code-path">${escapeHtml(options.path)}</span></span>
1605
+ <span class="head kind">json</span>
1078
1606
  <span class="state">${ok ? "Ready" : options.status}</span>
1607
+ ${responseViewToggleHtml(hasFields, compact)}
1079
1608
  <button class="copy" type="button" data-slot="json-code-copy" aria-label="Copy code" title="Copy code">
1080
1609
  <svg data-copy width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true"><rect x="8" y="8" width="11" height="11" rx="1.5" stroke="currentColor" stroke-width="1.5"/><path d="M5 16V5.5A1.5 1.5 0 0 1 6.5 4H16" stroke="currentColor" stroke-width="1.5"/></svg>
1081
1610
  <svg data-done hidden width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M5 12.5 9.2 17 19 7" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>
1082
1611
  </button>
1083
1612
  </header>
1084
1613
  <div class="body">
1085
- <div class="view"><pre>${rows}</pre></div>
1614
+ <div class="view">${responseHtml || `<pre>${rows}</pre>`}</div>
1086
1615
  ${navHtml(options.nav)}
1087
1616
  </div>
1088
1617
  </main>
1089
1618
  <textarea id="payload" hidden>${escapeHtml(code)}</textarea>
1090
1619
  <script>
1091
1620
  (() => {
1621
+ const METHOD_GLYPH = ${JSON.stringify({
1622
+ get: GLYPH.eye,
1623
+ query: GLYPH.search,
1624
+ post: GLYPH.plus,
1625
+ put: GLYPH.pencil,
1626
+ patch: GLYPH.pencil,
1627
+ delete: GLYPH.trash,
1628
+ head: GLYPH.file,
1629
+ options: GLYPH.sliders,
1630
+ other: GLYPH.file,
1631
+ })};
1632
+ function methodToneName(method) {
1633
+ switch (String(method || "").toUpperCase()) {
1634
+ case "GET": return "get";
1635
+ case "QUERY": return "query";
1636
+ case "POST": return "post";
1637
+ case "PUT": return "put";
1638
+ case "PATCH": return "patch";
1639
+ case "DELETE": return "delete";
1640
+ case "HEAD": return "head";
1641
+ case "OPTIONS": return "options";
1642
+ default: return "other";
1643
+ }
1644
+ }
1645
+ function paintRequestLine(method, path) {
1646
+ const tone = methodToneName(method);
1647
+ const verb = document.querySelector("[data-slot=json-code-method]");
1648
+ const icon = document.querySelector("[data-slot=json-code-method-icon]");
1649
+ const pathEl = document.querySelector("[data-slot=json-code-path]");
1650
+ if (verb) {
1651
+ verb.textContent = method;
1652
+ verb.className = "verb meth-" + tone;
1653
+ }
1654
+ if (icon) {
1655
+ icon.className = "icon meth meth-" + tone;
1656
+ icon.innerHTML = METHOD_GLYPH[tone] || METHOD_GLYPH.other;
1657
+ }
1658
+ if (pathEl) pathEl.textContent = path;
1659
+ }
1660
+
1092
1661
  const HEADERS_KEY = "oke:json-code:headers";
1662
+ const HEADERS_MODE_KEY = "oke:json-code:headers-mode";
1663
+ const HEADERS_GLOBAL_KEY = "oke:json-code:headers-global";
1664
+ const AUTH_KEY = "oke:json-code:auth";
1665
+ const AUTH_GLOBAL_KEY = "oke:json-code:auth-global";
1093
1666
  const QUERY_KEY = "oke:json-code:query";
1094
1667
  const PATH_KEY = "oke:json-code:path";
1095
1668
  const PATH_TEMPLATE_KEY = "oke:json-code:path-template";
@@ -1099,7 +1672,7 @@ pre {
1099
1672
  const BODY_JSON_KEY = "oke:json-code:body-json";
1100
1673
  const SECTION_KEY = "oke:json-code:rail-section";
1101
1674
  const ACCEPT = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
1102
- const SECTIONS = ["query", "body", "cookies", "headers", "path"];
1675
+ const SECTIONS = ["query", "body", "cookies", "headers", "auth", "path"];
1103
1676
  const page = document.querySelector("[data-slot=json-code-block]");
1104
1677
  const railCheck = document.getElementById("json-code-rail");
1105
1678
  const resetBtn = document.querySelector("[data-slot=json-code-reset]");
@@ -1113,6 +1686,35 @@ pre {
1113
1686
  const bodyModeBtns = document.querySelectorAll("[data-slot=json-code-body-mode]");
1114
1687
  const btn = document.querySelector("[data-slot=json-code-copy]");
1115
1688
  const payload = document.getElementById("payload");
1689
+ document.querySelectorAll("[data-response-view]").forEach((el) => {
1690
+ el.addEventListener("click", () => {
1691
+ const next = el.getAttribute("data-response-view");
1692
+ if (!next || !page) return;
1693
+ page.setAttribute(
1694
+ "data-view",
1695
+ next === "fields" ? "fields" : page.getAttribute("data-code-view") || "pretty",
1696
+ );
1697
+ document.querySelectorAll("[data-response-view]").forEach((other) => {
1698
+ const on = other.getAttribute("data-response-view") === next;
1699
+ other.classList.toggle("is-on", on);
1700
+ other.setAttribute("aria-pressed", on ? "true" : "false");
1701
+ });
1702
+ });
1703
+ });
1704
+ document.addEventListener("click", (event) => {
1705
+ const copy = event.target instanceof Element ? event.target.closest("[data-field-copy]") : null;
1706
+ if (!copy || !payload) return;
1707
+ event.preventDefault();
1708
+ event.stopPropagation();
1709
+ let value;
1710
+ try {
1711
+ const path = JSON.parse(copy.getAttribute("data-field-copy") || "[]");
1712
+ value = JSON.parse(payload.value);
1713
+ for (const seg of path) value = value == null ? value : value[seg];
1714
+ } catch { return; }
1715
+ const text = typeof value === "string" ? value : JSON.stringify(value);
1716
+ navigator.clipboard.writeText(text).catch(() => {});
1717
+ });
1116
1718
  const copyIcon = btn?.querySelector("[data-copy]");
1117
1719
  const doneIcon = btn?.querySelector("[data-done]");
1118
1720
  if (!page || !sendBtn) return;
@@ -1364,7 +1966,8 @@ pre {
1364
1966
  section.open = true;
1365
1967
  section.scrollIntoView({ block: "nearest" });
1366
1968
  }
1367
- if (name === "cookies" || name === "headers" || name === "query" || name === "path" || name === "body") {
1969
+ if (name === "auth") fillAuth("request");
1970
+ else if (name === "cookies" || name === "headers" || name === "query" || name === "path" || name === "body") {
1368
1971
  if (name === "body") syncBodyMode(bodyMode());
1369
1972
  else renderKv(name);
1370
1973
  }
@@ -1449,7 +2052,7 @@ pre {
1449
2052
  const key = (row.querySelector(".kv-key")?.value || "").trim();
1450
2053
  const value = row.querySelector(".kv-val")?.value || "";
1451
2054
  if (!key && !value.trim()) continue;
1452
- if (kind === "headers" && key.toLowerCase() === "authorization") continue;
2055
+ if ((kind === "headers" || kind === "headers-global") && key.toLowerCase() === "authorization") continue;
1453
2056
  out.push({ key, value });
1454
2057
  }
1455
2058
  return out;
@@ -1473,6 +2076,8 @@ pre {
1473
2076
  } else if (kind === "headers") {
1474
2077
  pairs = loadPairs(HEADERS_KEY).filter((p) => String(p.key).toLowerCase() !== "authorization");
1475
2078
  if (pairs.length === 0) pairs = [{ key: "accept", value: ACCEPT }];
2079
+ } else if (kind === "headers-global") {
2080
+ pairs = loadPairs(HEADERS_GLOBAL_KEY).filter((p) => String(p.key).toLowerCase() !== "authorization");
1476
2081
  } else if (kind === "body") {
1477
2082
  pairs = loadPairs(BODY_FORM_KEY);
1478
2083
  } else {
@@ -1491,6 +2096,12 @@ pre {
1491
2096
  if (!sessionStorage.getItem(HEADERS_KEY)) {
1492
2097
  savePairs(HEADERS_KEY, [{ key: "accept", value: ACCEPT }]);
1493
2098
  }
2099
+ if (sessionStorage.getItem(HEADERS_MODE_KEY) !== "custom") {
2100
+ sessionStorage.setItem(HEADERS_MODE_KEY, "inherit");
2101
+ }
2102
+ if (!sessionStorage.getItem(HEADERS_GLOBAL_KEY)) savePairs(HEADERS_GLOBAL_KEY, []);
2103
+ if (!sessionStorage.getItem(AUTH_KEY)) sessionStorage.setItem(AUTH_KEY, JSON.stringify(emptyAuth()));
2104
+ if (!sessionStorage.getItem(AUTH_GLOBAL_KEY)) sessionStorage.setItem(AUTH_GLOBAL_KEY, JSON.stringify(emptyAuth()));
1494
2105
  if (!sessionStorage.getItem(QUERY_KEY)) {
1495
2106
  const pairs = [];
1496
2107
  for (const [key, value] of new URLSearchParams(location.search)) pairs.push({ key, value });
@@ -1528,15 +2139,193 @@ pre {
1528
2139
  }
1529
2140
  }
1530
2141
 
1531
- function requestHeaders(contentType) {
1532
- const headers = { accept: ACCEPT };
1533
- for (const p of loadPairs(HEADERS_KEY)) {
2142
+ function emptyAuth() {
2143
+ return { type: "none", token: "", username: "", password: "", key: "", value: "", in: "header", mode: "inherit" };
2144
+ }
2145
+
2146
+ function authStorageKey(scope) {
2147
+ return scope === "global" ? AUTH_GLOBAL_KEY : AUTH_KEY;
2148
+ }
2149
+
2150
+ function loadAuth(key) {
2151
+ try {
2152
+ const raw = sessionStorage.getItem(key);
2153
+ if (!raw) return emptyAuth();
2154
+ const parsed = JSON.parse(raw);
2155
+ const type = parsed && parsed.type;
2156
+ if (type !== "bearer" && type !== "basic" && type !== "apikey" && type !== "none") return emptyAuth();
2157
+ return {
2158
+ type,
2159
+ token: typeof parsed.token === "string" ? parsed.token : "",
2160
+ username: typeof parsed.username === "string" ? parsed.username : "",
2161
+ password: typeof parsed.password === "string" ? parsed.password : "",
2162
+ key: typeof parsed.key === "string" ? parsed.key : "",
2163
+ value: typeof parsed.value === "string" ? parsed.value : "",
2164
+ in: parsed.in === "query" ? "query" : "header",
2165
+ mode: parsed.mode === "custom" ? "custom" : "inherit",
2166
+ };
2167
+ } catch {
2168
+ return emptyAuth();
2169
+ }
2170
+ }
2171
+
2172
+ function readAuth(scope) {
2173
+ const root = document.querySelector('[data-auth-scope="' + scope + '"]');
2174
+ if (!root) return loadAuth(authStorageKey(scope));
2175
+ const on = root.querySelector("[data-auth-type].is-on");
2176
+ const type = on ? on.getAttribute("data-auth-type") : "none";
2177
+ const field = (name) => {
2178
+ const input = root.querySelector('[data-auth-field="' + name + '"]');
2179
+ return input ? input.value : "";
2180
+ };
2181
+ const inBtn = root.querySelector("[data-auth-in].is-on");
2182
+ const modeBtn = root.querySelector("[data-auth-mode].is-on");
2183
+ const mode = modeBtn && modeBtn.getAttribute("data-auth-mode") === "custom" ? "custom" : "inherit";
2184
+ return {
2185
+ type: type === "bearer" || type === "basic" || type === "apikey" ? type : "none",
2186
+ token: field("token"),
2187
+ username: field("username"),
2188
+ password: field("password"),
2189
+ key: field("key"),
2190
+ value: field("value"),
2191
+ in: inBtn && inBtn.getAttribute("data-auth-in") === "query" ? "query" : "header",
2192
+ mode,
2193
+ };
2194
+ }
2195
+
2196
+ function authLabel(auth) {
2197
+ if (auth.type === "bearer") return "Bearer";
2198
+ if (auth.type === "basic") return "Basic";
2199
+ if (auth.type === "apikey") return "API";
2200
+ return "No";
2201
+ }
2202
+
2203
+ function paintAuth(scope, auth) {
2204
+ const root = document.querySelector('[data-auth-scope="' + scope + '"]');
2205
+ if (root) {
2206
+ for (const btn of root.querySelectorAll("[data-auth-type]")) {
2207
+ const on = btn.getAttribute("data-auth-type") === auth.type;
2208
+ btn.classList.toggle("is-on", on);
2209
+ btn.setAttribute("aria-pressed", on ? "true" : "false");
2210
+ }
2211
+ for (const pane of root.querySelectorAll("[data-auth-pane]")) {
2212
+ pane.hidden = pane.getAttribute("data-auth-pane") !== auth.type;
2213
+ }
2214
+ for (const btn of root.querySelectorAll("[data-auth-in]")) {
2215
+ const on = (btn.getAttribute("data-auth-in") || "header") === auth.in;
2216
+ btn.classList.toggle("is-on", on);
2217
+ btn.setAttribute("aria-pressed", on ? "true" : "false");
2218
+ }
2219
+ const mode = auth.mode === "custom" ? "custom" : "inherit";
2220
+ for (const btn of root.querySelectorAll("[data-auth-mode]")) {
2221
+ const on = btn.getAttribute("data-auth-mode") === mode;
2222
+ btn.classList.toggle("is-on", on);
2223
+ btn.setAttribute("aria-pressed", on ? "true" : "false");
2224
+ }
2225
+ const custom = root.querySelector("[data-auth-custom]");
2226
+ if (custom) custom.hidden = mode !== "custom";
2227
+ }
2228
+ if (scope === "global") {
2229
+ const btn = document.querySelector("[data-slot=json-code-global-auth]");
2230
+ if (btn) {
2231
+ const active = auth.type !== "none";
2232
+ btn.classList.toggle("is-on", active);
2233
+ btn.title = active ? "Global authentication · " + authLabel(auth) : "Global authentication";
2234
+ }
2235
+ }
2236
+ }
2237
+
2238
+ function fillAuth(scope) {
2239
+ const auth = loadAuth(authStorageKey(scope));
2240
+ const root = document.querySelector('[data-auth-scope="' + scope + '"]');
2241
+ if (root) {
2242
+ const set = (name, value) => {
2243
+ const input = root.querySelector('[data-auth-field="' + name + '"]');
2244
+ if (input) input.value = value;
2245
+ };
2246
+ set("token", auth.token);
2247
+ set("username", auth.username);
2248
+ set("password", auth.password);
2249
+ set("key", auth.key);
2250
+ set("value", auth.value);
2251
+ }
2252
+ paintAuth(scope, auth);
2253
+ }
2254
+
2255
+ function storedAuth(auth) {
2256
+ return auth;
2257
+ }
2258
+
2259
+ function forgetSecretsOnReload() {
2260
+ try {
2261
+ const nav = performance.getEntriesByType("navigation")[0];
2262
+ if (!nav || nav.type !== "reload") return;
2263
+ const mark = String(nav.startTime);
2264
+ if (sessionStorage.getItem("oke:json-code:reload-seen") === mark) return;
2265
+ sessionStorage.setItem("oke:json-code:reload-seen", mark);
2266
+ for (const key of [AUTH_KEY, AUTH_GLOBAL_KEY]) {
2267
+ const auth = loadAuth(key);
2268
+ sessionStorage.setItem(key, JSON.stringify({ ...auth, token: "", password: "", value: "" }));
2269
+ }
2270
+ } catch {}
2271
+ }
2272
+
2273
+ function persistAuth(scope) {
2274
+ const auth = readAuth(scope);
2275
+ sessionStorage.setItem(authStorageKey(scope), JSON.stringify(storedAuth(auth)));
2276
+ paintAuth(scope, auth);
2277
+ }
2278
+
2279
+ function effectiveAuth() {
2280
+ const request = readAuth("request");
2281
+ if (request.mode === "custom") return request;
2282
+ return readAuth("global");
2283
+ }
2284
+
2285
+ function basicAuthorization(user, pass) {
2286
+ const bytes = new TextEncoder().encode(String(user) + ":" + String(pass));
2287
+ let bin = "";
2288
+ for (const b of bytes) bin += String.fromCharCode(b);
2289
+ return "Basic " + btoa(bin);
2290
+ }
2291
+
2292
+ function headersMode() {
2293
+ return sessionStorage.getItem(HEADERS_MODE_KEY) === "custom" ? "custom" : "inherit";
2294
+ }
2295
+
2296
+ function paintHeadersMode(mode) {
2297
+ const next = mode === "custom" ? "custom" : "inherit";
2298
+ const root = document.querySelector('[data-rail-section="headers"]');
2299
+ if (!root) return;
2300
+ for (const btn of root.querySelectorAll("[data-headers-mode]")) {
2301
+ const on = btn.getAttribute("data-headers-mode") === next;
2302
+ btn.classList.toggle("is-on", on);
2303
+ btn.setAttribute("aria-pressed", on ? "true" : "false");
2304
+ }
2305
+ const custom = root.querySelector("[data-headers-custom]");
2306
+ if (custom) custom.hidden = next !== "custom";
2307
+ }
2308
+
2309
+ function applyHeaderPairs(headers, pairs) {
2310
+ for (const p of pairs) {
1534
2311
  const k = (p.key || "").trim();
1535
- if (!k) continue;
1536
- const lower = k.toLowerCase();
1537
- if (lower === "authorization") continue;
2312
+ if (!k || k.toLowerCase() === "authorization") continue;
1538
2313
  headers[k] = p.value ?? "";
1539
2314
  }
2315
+ }
2316
+
2317
+ function requestHeaders(contentType) {
2318
+ const headers = { accept: ACCEPT };
2319
+ applyHeaderPairs(headers, loadPairs(HEADERS_GLOBAL_KEY));
2320
+ if (headersMode() === "custom") applyHeaderPairs(headers, loadPairs(HEADERS_KEY));
2321
+ const auth = effectiveAuth();
2322
+ if (auth.type === "bearer" && auth.token.trim()) {
2323
+ headers.authorization = "Bearer " + auth.token.trim();
2324
+ } else if (auth.type === "basic" && (auth.username || auth.password)) {
2325
+ headers.authorization = basicAuthorization(auth.username, auth.password);
2326
+ } else if (auth.type === "apikey" && auth.in !== "query" && auth.key.trim()) {
2327
+ headers[auth.key.trim()] = auth.value ?? "";
2328
+ }
1540
2329
  if (contentType) {
1541
2330
  const hasCt = Object.keys(headers).some((k) => k.toLowerCase() === "content-type");
1542
2331
  if (!hasCt) headers["content-type"] = contentType;
@@ -1551,6 +2340,10 @@ pre {
1551
2340
  if (!k) continue;
1552
2341
  params.set(k, p.value ?? "");
1553
2342
  }
2343
+ const auth = effectiveAuth();
2344
+ if (auth.type === "apikey" && auth.in === "query" && auth.key.trim()) {
2345
+ params.set(auth.key.trim(), auth.value ?? "");
2346
+ }
1554
2347
  const q = params.toString();
1555
2348
  return q ? pathname + "?" + q : pathname;
1556
2349
  }
@@ -1625,6 +2418,12 @@ pre {
1625
2418
  HEADERS_KEY,
1626
2419
  collectKv("headers").filter((p) => p.key.toLowerCase() !== "authorization"),
1627
2420
  );
2421
+ savePairs(
2422
+ HEADERS_GLOBAL_KEY,
2423
+ collectKv("headers-global").filter((p) => p.key.toLowerCase() !== "authorization"),
2424
+ );
2425
+ persistAuth("request");
2426
+ persistAuth("global");
1628
2427
  savePairs(QUERY_KEY, collectKv("query"));
1629
2428
  savePairs(BODY_FORM_KEY, collectKv("body"));
1630
2429
  if (bodyRaw) sessionStorage.setItem(BODY_JSON_KEY, bodyRaw.value);
@@ -1688,6 +2487,8 @@ pre {
1688
2487
 
1689
2488
  function resetOptions() {
1690
2489
  savePairs(HEADERS_KEY, [{ key: "accept", value: ACCEPT }]);
2490
+ sessionStorage.setItem(HEADERS_MODE_KEY, "inherit");
2491
+ sessionStorage.setItem(AUTH_KEY, JSON.stringify(emptyAuth()));
1691
2492
  const pairs = [];
1692
2493
  for (const [key, value] of new URLSearchParams(location.search)) pairs.push({ key, value });
1693
2494
  savePairs(QUERY_KEY, pairs);
@@ -1699,6 +2500,8 @@ pre {
1699
2500
  savePairs(PATH_KEY, pathNames(template).map((n) => ({ key: n, value: "" })));
1700
2501
  }
1701
2502
  for (const kind of ["cookies", "headers", "query", "path", "body"]) renderKv(kind);
2503
+ paintHeadersMode("inherit");
2504
+ fillAuth("request");
1702
2505
  syncBodyMode("form");
1703
2506
  syncPathChapter();
1704
2507
  }
@@ -1710,10 +2513,47 @@ pre {
1710
2513
  return names.every((n) => (stored[n] || "").trim().length > 0);
1711
2514
  }
1712
2515
 
2516
+ function authHasCredentials(auth) {
2517
+ if (auth.type === "bearer") return auth.token.trim().length > 0;
2518
+ if (auth.type === "basic") return Boolean(auth.username || auth.password);
2519
+ if (auth.type === "apikey") return auth.key.trim().length > 0;
2520
+ return false;
2521
+ }
2522
+
2523
+ /** Address-bar loads omit sessionStorage credentials. Replay a 401 once. */
2524
+ function replayStoredAuth() {
2525
+ const status = Number(page.getAttribute("data-status") || "0");
2526
+ if (status !== 401) {
2527
+ try { sessionStorage.removeItem("oke:json-code:auth-replay"); } catch {}
2528
+ return;
2529
+ }
2530
+ const auth = effectiveAuth();
2531
+ if (!authHasCredentials(auth)) return;
2532
+ const method = (page.getAttribute("data-method") || "GET").toUpperCase();
2533
+ const path = page.getAttribute("data-path") || location.pathname;
2534
+ const stamp = [
2535
+ method, path, location.search, auth.type, auth.token, auth.username,
2536
+ auth.password, auth.key, auth.value, auth.in,
2537
+ ].join("\\0");
2538
+ try {
2539
+ if (sessionStorage.getItem("oke:json-code:auth-replay") === stamp) return;
2540
+ sessionStorage.setItem("oke:json-code:auth-replay", stamp);
2541
+ } catch {
2542
+ return;
2543
+ }
2544
+ rememberMethod(method, path);
2545
+ void sendRequest();
2546
+ }
2547
+
1713
2548
  ensureSeeds();
1714
- for (const kind of ["cookies", "headers", "query", "path", "body"]) renderKv(kind);
2549
+ forgetSecretsOnReload();
2550
+ for (const kind of ["cookies", "headers", "headers-global", "query", "path", "body"]) renderKv(kind);
2551
+ paintHeadersMode(headersMode());
2552
+ fillAuth("request");
2553
+ fillAuth("global");
1715
2554
  syncBodyMode(bodyMode());
1716
2555
  syncPathChapter();
2556
+ replayStoredAuth();
1717
2557
  const saved = sessionStorage.getItem(SECTION_KEY) || "";
1718
2558
  if (SECTIONS.includes(saved)) openSection(saved);
1719
2559
 
@@ -1732,6 +2572,63 @@ pre {
1732
2572
  syncBodyMode(mode);
1733
2573
  });
1734
2574
  }
2575
+ for (const el of document.querySelectorAll("[data-auth-reveal]")) {
2576
+ el.addEventListener("click", (e) => {
2577
+ e.preventDefault();
2578
+ e.stopPropagation();
2579
+ const field = el.closest(".auth-field");
2580
+ const input = field ? field.querySelector("input") : null;
2581
+ if (!input) return;
2582
+ const show = input.type === "password";
2583
+ input.type = show ? "text" : "password";
2584
+ el.setAttribute("aria-pressed", show ? "true" : "false");
2585
+ el.setAttribute("aria-label", show ? "Hide secret" : "Show secret");
2586
+ });
2587
+ }
2588
+ for (const el of document.querySelectorAll("[data-headers-mode]")) {
2589
+ el.addEventListener("click", (e) => {
2590
+ e.preventDefault();
2591
+ e.stopPropagation();
2592
+ const mode = el.getAttribute("data-headers-mode") === "custom" ? "custom" : "inherit";
2593
+ sessionStorage.setItem(HEADERS_MODE_KEY, mode);
2594
+ paintHeadersMode(mode);
2595
+ if (mode === "custom") renderKv("headers");
2596
+ });
2597
+ }
2598
+ for (const el of document.querySelectorAll("[data-auth-mode]")) {
2599
+ el.addEventListener("click", (e) => {
2600
+ e.preventDefault();
2601
+ e.stopPropagation();
2602
+ const root = el.closest("[data-auth-scope]");
2603
+ if (!root) return;
2604
+ for (const btn of root.querySelectorAll("[data-auth-mode]")) {
2605
+ const on = btn === el;
2606
+ btn.classList.toggle("is-on", on);
2607
+ btn.setAttribute("aria-pressed", on ? "true" : "false");
2608
+ }
2609
+ const custom = root.querySelector("[data-auth-custom]");
2610
+ if (custom) custom.hidden = el.getAttribute("data-auth-mode") !== "custom";
2611
+ persistAuth(root.getAttribute("data-auth-scope") || "request");
2612
+ });
2613
+ }
2614
+ for (const el of document.querySelectorAll("[data-auth-type]")) {
2615
+ el.addEventListener("click", (e) => {
2616
+ e.preventDefault();
2617
+ e.stopPropagation();
2618
+ const root = el.closest("[data-auth-scope]");
2619
+ if (!root) return;
2620
+ for (const btn of root.querySelectorAll("[data-auth-type]")) {
2621
+ const on = btn === el;
2622
+ btn.classList.toggle("is-on", on);
2623
+ btn.setAttribute("aria-pressed", on ? "true" : "false");
2624
+ }
2625
+ const type = el.getAttribute("data-auth-type") || "none";
2626
+ for (const pane of root.querySelectorAll("[data-auth-pane]")) {
2627
+ pane.hidden = pane.getAttribute("data-auth-pane") !== type;
2628
+ }
2629
+ persistAuth(root.getAttribute("data-auth-scope") || "request");
2630
+ });
2631
+ }
1735
2632
  if (bodyRaw) {
1736
2633
  bodyRaw.addEventListener("input", () => {
1737
2634
  sessionStorage.setItem(BODY_JSON_KEY, bodyRaw.value);
@@ -1750,13 +2647,63 @@ pre {
1750
2647
  const name = section.getAttribute("data-rail-section");
1751
2648
  if (!name) return;
1752
2649
  sessionStorage.setItem(SECTION_KEY, name);
1753
- if (name === "cookies" || name === "headers" || name === "query" || name === "path" || name === "body") {
2650
+ if (name === "auth") fillAuth("request");
2651
+ else if (name === "cookies" || name === "headers" || name === "query" || name === "path" || name === "body") {
1754
2652
  if (name === "body") syncBodyMode(bodyMode());
1755
2653
  else renderKv(name);
1756
2654
  }
1757
2655
  });
1758
2656
  }
1759
2657
 
2658
+ const globalPop = document.querySelector("[data-slot=json-code-global-panel]");
2659
+ const globalAuthBtn = document.querySelector("[data-slot=json-code-global-auth]");
2660
+ const globalHeadersBtn = document.querySelector("[data-slot=json-code-global-headers]");
2661
+
2662
+ function paintGlobalHeaders() {
2663
+ if (!globalHeadersBtn) return;
2664
+ const n = loadPairs(HEADERS_GLOBAL_KEY).filter((p) => (p.key || "").trim() && String(p.key).toLowerCase() !== "authorization").length;
2665
+ globalHeadersBtn.classList.toggle("is-on", n > 0);
2666
+ globalHeadersBtn.title = n > 0 ? "Global headers · " + n : "Global headers";
2667
+ }
2668
+
2669
+ function closeGlobal() {
2670
+ persistAuth("global");
2671
+ if (globalPop) globalPop.hidden = true;
2672
+ if (globalAuthBtn) globalAuthBtn.setAttribute("aria-expanded", "false");
2673
+ if (globalHeadersBtn) globalHeadersBtn.setAttribute("aria-expanded", "false");
2674
+ }
2675
+
2676
+ function openGlobal(pane) {
2677
+ if (!globalPop) return;
2678
+ const same = !globalPop.hidden && globalPop.getAttribute("data-pane") === pane;
2679
+ if (same) {
2680
+ closeGlobal();
2681
+ return;
2682
+ }
2683
+ globalPop.hidden = false;
2684
+ globalPop.setAttribute("data-pane", pane);
2685
+ const authPane = globalPop.querySelector('[data-global-pane="auth"]');
2686
+ const headersPane = globalPop.querySelector('[data-global-pane="headers"]');
2687
+ if (authPane) authPane.hidden = pane !== "auth";
2688
+ if (headersPane) headersPane.hidden = pane !== "headers";
2689
+ if (globalAuthBtn) globalAuthBtn.setAttribute("aria-expanded", pane === "auth" ? "true" : "false");
2690
+ if (globalHeadersBtn) globalHeadersBtn.setAttribute("aria-expanded", pane === "headers" ? "true" : "false");
2691
+ if (pane === "headers") renderKv("headers-global");
2692
+ else fillAuth("global");
2693
+ }
2694
+
2695
+ paintGlobalHeaders();
2696
+ if (globalAuthBtn) globalAuthBtn.addEventListener("click", (e) => {
2697
+ e.preventDefault();
2698
+ e.stopPropagation();
2699
+ openGlobal("auth");
2700
+ });
2701
+ if (globalHeadersBtn) globalHeadersBtn.addEventListener("click", (e) => {
2702
+ e.preventDefault();
2703
+ e.stopPropagation();
2704
+ openGlobal("headers");
2705
+ });
2706
+
1760
2707
  document.addEventListener("keydown", (e) => {
1761
2708
  if (e.key !== "Enter") return;
1762
2709
  const t = e.target;
@@ -1773,8 +2720,7 @@ pre {
1773
2720
  leaf.classList.add("is-on");
1774
2721
  const method = (leaf.getAttribute("data-method") || currentMethod()).toUpperCase();
1775
2722
  const path = leaf.getAttribute("data-path") || leaf.getAttribute("data-param-template") || currentPath();
1776
- const file = document.querySelector(".file");
1777
- if (file) file.textContent = method + " " + path;
2723
+ paintRequestLine(method, path);
1778
2724
  const title = document.querySelector("title");
1779
2725
  if (title) {
1780
2726
  const app = page.querySelector(".title")?.textContent || "";
@@ -1782,10 +2728,43 @@ pre {
1782
2728
  }
1783
2729
  }
1784
2730
 
2731
+ document.addEventListener("input", (e) => {
2732
+ const t = e.target;
2733
+ if (!(t instanceof HTMLInputElement)) return;
2734
+ const authRoot = t.closest("[data-auth-scope]");
2735
+ if (authRoot) {
2736
+ persistAuth(authRoot.getAttribute("data-auth-scope") || "request");
2737
+ return;
2738
+ }
2739
+ if (t.closest('[data-kv="headers-global"]')) {
2740
+ savePairs(
2741
+ HEADERS_GLOBAL_KEY,
2742
+ collectKv("headers-global").filter((p) => p.key.toLowerCase() !== "authorization"),
2743
+ );
2744
+ paintGlobalHeaders();
2745
+ }
2746
+ });
2747
+
1785
2748
  document.addEventListener("click", (e) => {
1786
2749
  const t = e.target;
1787
2750
  const el = t instanceof Element ? t : t && t.parentElement;
1788
2751
  if (!el) return;
2752
+ const authIn = el.closest("[data-auth-in]");
2753
+ if (authIn) {
2754
+ e.preventDefault();
2755
+ const root = authIn.closest("[data-auth-scope]");
2756
+ if (!root) return;
2757
+ for (const btn of root.querySelectorAll("[data-auth-in]")) {
2758
+ const on = btn === authIn;
2759
+ btn.classList.toggle("is-on", on);
2760
+ btn.setAttribute("aria-pressed", on ? "true" : "false");
2761
+ }
2762
+ persistAuth(root.getAttribute("data-auth-scope") || "request");
2763
+ return;
2764
+ }
2765
+ if (globalPop && !globalPop.hidden && !el.closest("[data-slot=json-code-global-panel]") && !el.closest("[data-slot=json-code-global-auth]") && !el.closest("[data-slot=json-code-global-headers]")) {
2766
+ closeGlobal();
2767
+ }
1789
2768
  const paramLeaf = el.closest("button.leaf[data-param-template]");
1790
2769
  if (paramLeaf) {
1791
2770
  e.preventDefault();
@@ -1906,6 +2885,7 @@ export async function asBrowserJsonCodeBlock(
1906
2885
  latencyMs,
1907
2886
  cache,
1908
2887
  auth,
2888
+ headers: jsonCodeResponseHeaders(response.headers),
1909
2889
  });
1910
2890
  const headers = new Headers(response.headers);
1911
2891
  headers.set("content-type", "text/html; charset=utf-8");
@@ -1916,11 +2896,121 @@ export async function asBrowserJsonCodeBlock(
1916
2896
  return new Response(html, { status: response.status, headers });
1917
2897
  }
1918
2898
 
2899
+ /** Stroke icon, 14px, currentColor. */
2900
+ function glyph(body: string, size = 14): string {
2901
+ return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" aria-hidden="true">${body}</svg>`;
2902
+ }
2903
+
2904
+ const GLYPH = {
2905
+ clock: glyph(
2906
+ `<circle cx="12" cy="12" r="7.5" stroke="currentColor" stroke-width="1.5"/><path d="M12 8v4.5l2.5 1.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>`,
2907
+ ),
2908
+ dot: glyph(`<circle cx="12" cy="12" r="3.25" fill="currentColor"/>`, 12),
2909
+ eye: glyph(
2910
+ `<path d="M2.5 12S6 6.5 12 6.5 21.5 12 21.5 12 18 17.5 12 17.5 2.5 12 2.5 12Z" stroke="currentColor" stroke-width="1.5"/><circle cx="12" cy="12" r="2.25" stroke="currentColor" stroke-width="1.5"/>`,
2911
+ ),
2912
+ plus: glyph(
2913
+ `<path d="M12 5.5v13M5.5 12h13" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/>`,
2914
+ ),
2915
+ pencil: glyph(
2916
+ `<path d="M4 16.5V20h3.5L19 8.5 15.5 5 4 16.5Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>`,
2917
+ ),
2918
+ trash: glyph(
2919
+ `<path d="M5 7.5h14M9.5 7.5V5.5h5v2M8 7.5l.7 12h6.6L16 7.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>`,
2920
+ ),
2921
+ file: glyph(
2922
+ `<path d="M7 3.5h7.2L19 8.2V20a.5.5 0 0 1-.5.5h-11A.5.5 0 0 1 7 20V3.5Z" stroke="currentColor" stroke-width="1.5"/><path d="M14 3.5V8h5" stroke="currentColor" stroke-width="1.5"/>`,
2923
+ ),
2924
+ sliders: glyph(
2925
+ `<path d="M4 8h16M4 16h16M9 6v4M16 14v4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>`,
2926
+ ),
2927
+ search: glyph(
2928
+ `<circle cx="11" cy="11" r="5.5" stroke="currentColor" stroke-width="1.5"/><path d="M15.5 15.5 19.5 19.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>`,
2929
+ ),
2930
+ braces: glyph(
2931
+ `<path d="M9 5.5c-1.6 0-2.5 1-2.5 2.6v2.2c0 1-.5 1.5-1.7 1.7 1.2.2 1.7.7 1.7 1.7v2.2c0 1.6.9 2.6 2.5 2.6M15 5.5c1.6 0 2.5 1 2.5 2.6v2.2c0 1 .5 1.5 1.7 1.7-1.2.2-1.7.7-1.7 1.7v2.2c0 1.6-.9 2.6-2.5 2.6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>`,
2932
+ ),
2933
+ cookie: glyph(
2934
+ `<circle cx="12" cy="12" r="7.5" stroke="currentColor" stroke-width="1.5"/><circle cx="9" cy="10" r=".9" fill="currentColor"/><circle cx="14" cy="9.5" r=".9" fill="currentColor"/><circle cx="13" cy="14" r=".9" fill="currentColor"/>`,
2935
+ ),
2936
+ list: glyph(
2937
+ `<path d="M9 7h10M9 12h10M9 17h10M5 7h.01M5 12h.01M5 17h.01" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/>`,
2938
+ ),
2939
+ key: glyph(
2940
+ `<circle cx="8" cy="14" r="3.25" stroke="currentColor" stroke-width="1.5"/><path d="M11 12.2 19 4.5M16.2 4.5H19V7.2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>`,
2941
+ ),
2942
+ route: glyph(
2943
+ `<path d="M5 19V7.5A2 2 0 0 1 7 5.5h5M12 5.5 15.5 9 12 12.5M15 12h4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>`,
2944
+ ),
2945
+ } as const;
2946
+
2947
+ /** Accent class for an HTTP method. Matches Console traces. */
2948
+ function methodTone(method: string): string {
2949
+ switch (method.toUpperCase()) {
2950
+ case "GET":
2951
+ return "get";
2952
+ case "QUERY":
2953
+ return "query";
2954
+ case "POST":
2955
+ return "post";
2956
+ case "PUT":
2957
+ return "put";
2958
+ case "PATCH":
2959
+ return "patch";
2960
+ case "DELETE":
2961
+ return "delete";
2962
+ case "HEAD":
2963
+ return "head";
2964
+ case "OPTIONS":
2965
+ return "options";
2966
+ default:
2967
+ return "other";
2968
+ }
2969
+ }
2970
+
2971
+ /** Glyph for an HTTP method. */
2972
+ function methodGlyph(method: string): string {
2973
+ switch (method.toUpperCase()) {
2974
+ case "GET":
2975
+ return GLYPH.eye;
2976
+ case "QUERY":
2977
+ return GLYPH.search;
2978
+ case "POST":
2979
+ return GLYPH.plus;
2980
+ case "PUT":
2981
+ case "PATCH":
2982
+ return GLYPH.pencil;
2983
+ case "DELETE":
2984
+ return GLYPH.trash;
2985
+ case "OPTIONS":
2986
+ return GLYPH.sliders;
2987
+ default:
2988
+ return GLYPH.file;
2989
+ }
2990
+ }
2991
+
2992
+ const SECTION_GLYPH: Readonly<Record<string, string>> = {
2993
+ query: GLYPH.sliders,
2994
+ body: GLYPH.braces,
2995
+ cookies: GLYPH.cookie,
2996
+ headers: GLYPH.list,
2997
+ auth: GLYPH.key,
2998
+ path: GLYPH.route,
2999
+ };
3000
+
3001
+ /** Status accent: ok, redirect, client error, server error. */
3002
+ function statusTone(status: number): "ok" | "info" | "warn" | "err" {
3003
+ if (status >= 500) return "err";
3004
+ if (status >= 400) return "warn";
3005
+ if (status >= 300) return "info";
3006
+ return "ok";
3007
+ }
3008
+
1919
3009
  function latencyHtml(ms: number | undefined): string {
1920
3010
  if (ms === undefined) return "";
1921
3011
  const tone = jsonCodeLatencyTone(ms);
1922
3012
  const label = formatJsonCodeLatency(ms);
1923
- return `<span class="count lat-${tone}" data-slot="json-code-latency" data-tone="${tone}" title="Latency">${escapeHtml(label)}</span>`;
3013
+ return `<span class="count lat-${tone}" data-slot="json-code-latency" data-tone="${tone}" title="Latency">${GLYPH.clock}${escapeHtml(label)}</span>`;
1924
3014
  }
1925
3015
 
1926
3016
  const CACHE_MARK: Readonly<
@@ -1992,13 +3082,57 @@ function authHtml(auth: JsonCodeAuth | undefined): string {
1992
3082
  return `<span class="count auth auth-${value.kind}" data-slot="json-code-auth" data-auth="${value.kind}" title="${escapeHtml(title)}">${mark.icon}<span class="auth-label">${escapeHtml(label)}</span></span>`;
1993
3083
  }
1994
3084
 
3085
+ function authSwitchHtml(): string {
3086
+ const opt = (type: string, text: string) =>
3087
+ `<button type="button" class="token${type === "none" ? " is-on" : ""}" data-auth-type="${type}" aria-pressed="${type === "none" ? "true" : "false"}">${text}</button>`;
3088
+ return `<span class="body-mode-strip" role="group" aria-label="Authorization">${opt("none", "No")}${opt("bearer", "Bearer")}${opt("basic", "Basic")}${opt("apikey", "API")}</span>`;
3089
+ }
3090
+
3091
+ const AUTH_EYE = `<svg class="eye-open" width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M2.5 12S6 6.5 12 6.5 21.5 12 21.5 12 18 17.5 12 17.5 2.5 12 2.5 12Z" stroke="currentColor" stroke-width="1.5"/><circle cx="12" cy="12" r="2.5" stroke="currentColor" stroke-width="1.5"/></svg><svg class="eye-shut" width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M3 4.5 21 19.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M9.2 6.9A11 11 0 0 1 12 6.5c6 0 9.5 5.5 9.5 5.5a17 17 0 0 1-3.4 3.7M6.1 8.2C3.9 9.7 2.5 12 2.5 12S6 17.5 12 17.5c1.1 0 2.1-.2 3.1-.7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>`;
3092
+
3093
+ /** Masked credential input. The value stays for this tab; a refresh clears it. */
3094
+ function secretInput(name: string, placeholder: string): string {
3095
+ return `<input class="kv-val" type="password" data-auth-field="${name}" spellcheck="false" autocomplete="off" placeholder="${placeholder}" /><button type="button" class="auth-reveal" data-auth-reveal aria-pressed="false" aria-label="Show secret">${AUTH_EYE}</button>`;
3096
+ }
3097
+
3098
+ function authEditorHtml(scope: "request" | "global"): string {
3099
+ const none =
3100
+ scope === "global"
3101
+ ? "No global credentials. Each request uses its own Authorization."
3102
+ : "No credentials on this request.";
3103
+ const switchRow =
3104
+ scope === "global" ? authSwitchHtml() : `<div class="strip">${authSwitchHtml()}</div>`;
3105
+ return `<div class="auth-editor" data-slot="json-code-auth-editor">
3106
+ ${switchRow}
3107
+ <p class="kv-empty" data-auth-pane="none">${none}</p>
3108
+ <div class="auth-pane" data-auth-pane="bearer" hidden>
3109
+ <label class="auth-field"><span class="auth-lab">Token</span>${secretInput("token", "Token")}</label>
3110
+ </div>
3111
+ <div class="auth-pane" data-auth-pane="basic" hidden>
3112
+ <label class="auth-field"><span class="auth-lab">Username</span><input class="kv-val" data-auth-field="username" spellcheck="false" autocomplete="off" placeholder="Username" /></label>
3113
+ <label class="auth-field"><span class="auth-lab">Password</span>${secretInput("password", "Password")}</label>
3114
+ </div>
3115
+ <div class="auth-pane" data-auth-pane="apikey" hidden>
3116
+ <label class="auth-field"><span class="auth-lab">Key</span><input class="kv-val" data-auth-field="key" spellcheck="false" autocomplete="off" placeholder="X-Api-Key" /></label>
3117
+ <label class="auth-field"><span class="auth-lab">Value</span>${secretInput("value", "Value")}</label>
3118
+ <div class="auth-field">
3119
+ <span class="auth-lab">Add to</span>
3120
+ <span class="auth-in" role="group" aria-label="API key location">
3121
+ <button type="button" class="token is-on" data-auth-in="header" aria-pressed="true">Header</button>
3122
+ <button type="button" class="token" data-auth-in="query" aria-pressed="false">Query</button>
3123
+ </span>
3124
+ </div>
3125
+ </div>
3126
+ </div>`;
3127
+ }
3128
+
1995
3129
  function navHtml(nav: readonly JsonCodeNavGroup[] | undefined): string {
1996
3130
  const groups = (nav ?? []).map(navGroupHtml).join("");
1997
3131
  const chev = `<span class="chev" aria-hidden="true"><svg width="12" height="12" viewBox="0 0 24 24" fill="none"><path d="M6 9.5 12 15.5 18 9.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></span>`;
1998
3132
  const play = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M8 5.5v13l11-6.5L8 5.5Z" fill="currentColor"/></svg>`;
1999
3133
  const kvSection = (kind: string, label: string) =>
2000
3134
  `<details class="rail-acc" data-rail-section="${kind}">
2001
- <summary class="rail-acc-sum">${chev}<span>${label}</span></summary>
3135
+ <summary class="rail-acc-sum">${chev}<span class="sec-ico">${SECTION_GLYPH[kind] ?? ""}</span><span>${label}</span></summary>
2002
3136
  <div class="rail-acc-body">
2003
3137
  <div class="kv-editor">
2004
3138
  <div class="kv-rows" data-slot="json-code-kv-rows" data-kv="${kind}"></div>
@@ -2013,10 +3147,6 @@ function navHtml(nav: readonly JsonCodeNavGroup[] | undefined): string {
2013
3147
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M9 5.5 15.5 12 9 18.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
2014
3148
  </label>
2015
3149
  </header>
2016
- <div class="rail-nav" data-rail-section="routes">
2017
- <p class="rail-sec-label">Routes</p>
2018
- <ul class="rail-list">${groups}</ul>
2019
- </div>
2020
3150
  <div class="rail-dock" data-slot="json-code-request-dock">
2021
3151
  <div class="strip rail-dock-strip">
2022
3152
  <span class="head">${play}<span>Request</span></span>
@@ -2027,9 +3157,10 @@ function navHtml(nav: readonly JsonCodeNavGroup[] | undefined): string {
2027
3157
  </div>
2028
3158
  <div class="rail-dock-body">
2029
3159
  ${kvSection("query", "Params")}
3160
+ ${kvSection("path", "Path")}
2030
3161
  <details class="rail-acc" data-rail-section="body">
2031
3162
  <summary class="rail-acc-sum">
2032
- ${chev}<span>Body</span>
3163
+ ${chev}<span class="sec-ico">${SECTION_GLYPH.body}</span><span>Body</span>
2033
3164
  <span class="grow"></span>
2034
3165
  <span class="body-mode-strip" role="group" aria-label="Body format">
2035
3166
  <button type="button" class="token is-on" data-slot="json-code-body-mode" data-mode="form" aria-pressed="true">Form</button>
@@ -2049,11 +3180,43 @@ function navHtml(nav: readonly JsonCodeNavGroup[] | undefined): string {
2049
3180
  </div>
2050
3181
  </div>
2051
3182
  </details>
3183
+ <details class="rail-acc" data-rail-section="headers">
3184
+ <summary class="rail-acc-sum">
3185
+ ${chev}<span class="sec-ico">${SECTION_GLYPH.headers}</span><span>Headers</span>
3186
+ <span class="grow"></span>
3187
+ <span class="body-mode-strip" role="group" aria-label="Request headers">
3188
+ <button type="button" class="token is-on" data-headers-mode="inherit" aria-pressed="true">Inherit</button>
3189
+ <button type="button" class="token" data-headers-mode="custom" aria-pressed="false">Custom</button>
3190
+ </span>
3191
+ </summary>
3192
+ <div class="rail-acc-body" data-headers-custom hidden>
3193
+ <div class="kv-editor">
3194
+ <div class="kv-rows" data-slot="json-code-kv-rows" data-kv="headers"></div>
3195
+ </div>
3196
+ </div>
3197
+ </details>
3198
+ <details class="rail-acc" data-rail-section="auth" data-auth-scope="request">
3199
+ <summary class="rail-acc-sum">${chev}<span class="sec-ico">${SECTION_GLYPH.auth}</span><span>Auth</span><span class="grow"></span><span class="body-mode-strip" role="group" aria-label="Request auth"><button type="button" class="token is-on" data-auth-mode="inherit" aria-pressed="true">Inherit</button><button type="button" class="token" data-auth-mode="custom" aria-pressed="false">Custom</button></span></summary>
3200
+ <div class="rail-acc-body" data-auth-custom hidden>
3201
+ ${authEditorHtml("request")}
3202
+ </div>
3203
+ </details>
2052
3204
  ${kvSection("cookies", "Cookies")}
2053
- ${kvSection("headers", "Headers")}
2054
- ${kvSection("path", "Path")}
2055
3205
  </div>
2056
3206
  </div>
3207
+ <div class="rail-nav" data-rail-section="routes">
3208
+ <input class="routes-check" type="checkbox" id="json-code-routes" checked>
3209
+ <p class="rail-sec-label">
3210
+ <span>Routes</span>
3211
+ <label class="copy routes-collapse" for="json-code-routes" aria-label="Collapse routes" title="Collapse routes">
3212
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M6 9.5 12 15.5 18 9.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
3213
+ </label>
3214
+ <label class="copy routes-expand" for="json-code-routes" aria-label="Expand routes" title="Expand routes">
3215
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M6 14.5 12 8.5 18 14.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
3216
+ </label>
3217
+ </p>
3218
+ <ul class="rail-list">${groups}</ul>
3219
+ </div>
2057
3220
  </aside>
2058
3221
  <label class="rail-thin" for="json-code-rail" aria-label="Expand request" title="Expand request">
2059
3222
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M15 5.5 8.5 12 15 18.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>