okengine 0.18.5 → 0.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 (510) hide show
  1. package/AGENTS.md +18 -10
  2. package/README.md +2 -2
  3. package/package.json +51 -50
  4. package/site/content/docs/ai/llms-txt.mdx +1 -1
  5. package/site/content/docs/ai/meta.json +1 -1
  6. package/site/content/docs/ai/skills.mdx +4 -1
  7. package/site/content/docs/ai/try-it.mdx +58 -0
  8. package/site/content/docs/client/auth.mdx +208 -0
  9. package/site/content/docs/client/calling.mdx +451 -0
  10. package/site/content/docs/client/index.mdx +209 -0
  11. package/site/content/docs/client/live.mdx +234 -0
  12. package/site/content/docs/client/meta.json +5 -0
  13. package/site/content/docs/client/react.mdx +249 -0
  14. package/site/content/docs/elements/ai/agents.mdx +232 -0
  15. package/site/content/docs/elements/ai/index.mdx +341 -0
  16. package/site/content/docs/elements/ai/mcp.mdx +275 -0
  17. package/site/content/docs/elements/ai/meta.json +5 -0
  18. package/site/content/docs/elements/ai/models.mdx +283 -0
  19. package/site/content/docs/elements/ai/prompts.mdx +256 -0
  20. package/site/content/docs/elements/channel/email.mdx +266 -0
  21. package/site/content/docs/elements/channel/index.mdx +344 -0
  22. package/site/content/docs/elements/channel/meta.json +5 -0
  23. package/site/content/docs/elements/channel/push.mdx +218 -0
  24. package/site/content/docs/elements/channel/receipts.mdx +206 -0
  25. package/site/content/docs/elements/channel/sms.mdx +264 -0
  26. package/site/content/docs/elements/channel/whatsapp.mdx +220 -0
  27. package/site/content/docs/elements/clock/index.mdx +300 -0
  28. package/site/content/docs/elements/clock/meta.json +5 -0
  29. package/site/content/docs/elements/clock/schedules.mdx +508 -0
  30. package/site/content/docs/elements/clock/sleep.mdx +445 -0
  31. package/site/content/docs/elements/flow/consumers.mdx +707 -0
  32. package/site/content/docs/elements/flow/http.mdx +1080 -0
  33. package/site/content/docs/elements/flow/index.mdx +618 -0
  34. package/site/content/docs/elements/flow/meta.json +5 -0
  35. package/site/content/docs/elements/flow/routing.mdx +559 -0
  36. package/site/content/docs/elements/flow/workflows.mdx +640 -0
  37. package/site/content/docs/elements/gate/auth.mdx +371 -0
  38. package/site/content/docs/elements/gate/authorization.mdx +298 -0
  39. package/site/content/docs/elements/gate/index.mdx +418 -0
  40. package/site/content/docs/elements/gate/meta.json +5 -0
  41. package/site/content/docs/elements/gate/rate-limits.mdx +365 -0
  42. package/site/content/docs/elements/gate/rls.mdx +303 -0
  43. package/site/content/docs/elements/gate/tenancy.mdx +372 -0
  44. package/site/content/docs/elements/index.mdx +20 -10
  45. package/site/content/docs/elements/signal/broadcast.mdx +517 -0
  46. package/site/content/docs/elements/signal/index.mdx +352 -0
  47. package/site/content/docs/elements/signal/live.mdx +590 -0
  48. package/site/content/docs/elements/signal/meta.json +5 -0
  49. package/site/content/docs/elements/signal/once.mdx +596 -0
  50. package/site/content/docs/elements/store/files.mdx +659 -0
  51. package/site/content/docs/elements/store/index.mdx +310 -0
  52. package/site/content/docs/elements/store/kv.mdx +629 -0
  53. package/site/content/docs/elements/store/meta.json +5 -0
  54. package/site/content/docs/elements/store/search.mdx +946 -0
  55. package/site/content/docs/elements/store/sql.mdx +937 -0
  56. package/site/content/docs/elements/vault/config.mdx +256 -0
  57. package/site/content/docs/elements/vault/index.mdx +356 -0
  58. package/site/content/docs/elements/vault/meta.json +5 -0
  59. package/site/content/docs/elements/vault/rotation.mdx +273 -0
  60. package/site/content/docs/elements/vault/secrets.mdx +285 -0
  61. package/site/content/docs/index.mdx +63 -36
  62. package/site/content/docs/meta.json +8 -7
  63. package/site/content/docs/plugins/cors.mdx +2 -1
  64. package/site/content/docs/plugins/csrf.mdx +4 -1
  65. package/site/content/docs/plugins/headers.mdx +2 -1
  66. package/site/content/docs/plugins/ip-allowlist.mdx +2 -1
  67. package/site/content/docs/plugins/magic-link.mdx +6 -0
  68. package/site/content/docs/plugins/maintenance-mode.mdx +2 -1
  69. package/site/content/docs/plugins/oauth.mdx +1 -1
  70. package/site/content/docs/plugins/otp.mdx +12 -1
  71. package/site/content/docs/plugins/passkey.mdx +39 -15
  72. package/site/content/docs/plugins/two-factor.mdx +91 -34
  73. package/site/content/docs/plugins/username.mdx +1 -1
  74. package/site/content/docs/recipes/caddy.mdx +3 -3
  75. package/site/content/docs/recipes/index.mdx +18 -28
  76. package/site/content/docs/recipes/mailpit.mdx +1 -1
  77. package/site/content/docs/recipes/meilisearch.mdx +3 -3
  78. package/site/content/docs/recipes/meta.json +1 -4
  79. package/site/content/docs/recipes/nginx.mdx +2 -2
  80. package/site/content/docs/recipes/openrouter.mdx +283 -0
  81. package/site/content/docs/recipes/pgdog.mdx +3 -3
  82. package/site/content/docs/recipes/postgres.mdx +1 -2
  83. package/site/content/docs/recipes/rustfs.mdx +2 -2
  84. package/site/content/docs/recipes/traefik.mdx +3 -3
  85. package/site/content/docs/reference/cli.mdx +231 -0
  86. package/site/content/docs/reference/configuration.mdx +30 -10
  87. package/site/content/docs/reference/environment-variables.mdx +93 -20
  88. package/site/content/docs/reference/errors.mdx +132 -29
  89. package/site/content/docs/reference/fx.mdx +137 -73
  90. package/site/content/docs/reference/index.mdx +13 -21
  91. package/site/content/docs/reference/meta.json +4 -5
  92. package/site/content/docs/reference/okid.mdx +42 -17
  93. package/site/content/docs/reference/plugins.mdx +4 -3
  94. package/site/content/docs/reference/security.mdx +197 -0
  95. package/site/content/docs/understand/meta.json +5 -0
  96. package/site/content/docs/understand/the-anatomy.mdx +132 -0
  97. package/site/content/docs/understand/the-model.mdx +32 -0
  98. package/site/content/docs/understand/the-problem.mdx +74 -0
  99. package/site/content/docs/understand/the-vocabulary.mdx +26 -0
  100. package/src/auth/api-keys.ts +2 -1
  101. package/src/auth/bindings.ts +64 -16
  102. package/src/auth/gate-auth.test.ts +6 -1
  103. package/src/auth/identity.ts +152 -5
  104. package/src/auth/index.ts +29 -0
  105. package/src/auth/invites.ts +2 -1
  106. package/src/auth/method-context.ts +41 -0
  107. package/src/auth/oauth-as/crypto.ts +2 -1
  108. package/src/auth/oauth-as/stores.ts +2 -1
  109. package/src/auth/operator.ts +2 -1
  110. package/src/auth/sessions-jwt.test.ts +77 -0
  111. package/src/auth/sessions.ts +20 -4
  112. package/src/auth/tenants.ts +3 -2
  113. package/src/auth/two-factor-challenge.test.ts +61 -0
  114. package/src/auth/two-factor-challenge.ts +284 -0
  115. package/src/auth/verification.ts +5 -0
  116. package/src/bench/REPORT.md +49 -0
  117. package/src/bench/g03-signal-once.bench.ts +1 -1
  118. package/src/bench/g10-observability-contention.bench.ts +1 -1
  119. package/src/bench/g17-hybrid-search.bench.ts +572 -0
  120. package/src/bench/load-app.ts +2 -10
  121. package/src/cli/ai-setup/ai-setup.test.ts +331 -42
  122. package/src/cli/ai-setup/apply.ts +294 -52
  123. package/src/cli/ai-setup/catalog.ts +238 -1342
  124. package/src/cli/ai-setup/index.ts +14 -99
  125. package/src/cli/ai-setup/prompts.ts +60 -443
  126. package/src/cli/ask-seed.test.ts +96 -0
  127. package/src/cli/ask-seed.ts +82 -0
  128. package/src/cli/ask-vault-gaps.test.ts +84 -0
  129. package/src/cli/ask-vault-gaps.ts +146 -0
  130. package/src/cli/build.ts +2 -2
  131. package/src/cli/client-add.test.ts +26 -1
  132. package/src/cli/client-add.ts +106 -27
  133. package/src/cli/db-seed.ts +2 -0
  134. package/src/cli/db.ts +150 -11
  135. package/src/cli/dev.ts +119 -211
  136. package/src/cli/docker-clean.ts +2 -2
  137. package/src/cli/docker-cli.test.ts +1 -1
  138. package/src/cli/doctor-diff.ts +4 -2
  139. package/src/cli/doctor-pii.test.ts +1 -1
  140. package/src/cli/doctor.ts +14 -1
  141. package/src/cli/hero-meta.test.ts +4 -2
  142. package/src/cli/load-config.images.test.ts +8 -10
  143. package/src/cli/load-config.ts +1 -1
  144. package/src/cli/registry.ts +22 -10
  145. package/src/cli/replay.ts +3 -1
  146. package/src/cli/start.ts +1 -1
  147. package/src/cli/tui/keys.ts +2 -3
  148. package/src/client/auth/cookies.ts +74 -0
  149. package/src/client/auth/create-auth-client.ts +636 -0
  150. package/src/client/auth/denials.ts +99 -0
  151. package/src/client/auth/session.ts +283 -0
  152. package/src/client/auth.test.ts +251 -0
  153. package/src/client/auth.ts +39 -114
  154. package/src/client/create-with-session.ts +245 -0
  155. package/src/client/create.ts +51 -5
  156. package/src/client/index.ts +11 -1
  157. package/src/client/live.ts +11 -79
  158. package/src/client/notes-contract.test.ts +5 -10
  159. package/src/client/sse.ts +137 -0
  160. package/src/client/stream.ts +147 -0
  161. package/src/client/transport.test.ts +32 -0
  162. package/src/client/transport.ts +123 -26
  163. package/src/client/types.ts +118 -14
  164. package/src/client-react/index.ts +185 -24
  165. package/src/client-react/use-live-query.ts +13 -3
  166. package/src/compiler/aot.test.ts +7 -4
  167. package/src/compiler/effects-embed.test.ts +56 -0
  168. package/src/compiler/effects-fetch.test.ts +43 -0
  169. package/src/compiler/effects-infer.ts +58 -2
  170. package/src/compiler/extract.test.ts +257 -43
  171. package/src/compiler/extract.ts +566 -48
  172. package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +7 -4
  173. package/src/compiler/fixtures/skyport/src/flows/bookings/signals.ts +2 -8
  174. package/src/compiler/fixtures/skyport.expected.json +4 -4
  175. package/src/compiler/fixtures/triggers/five-triggers.ts +14 -10
  176. package/src/compiler/response.ts +2 -2
  177. package/src/compiler/search-writer-isolation.test.ts +40 -0
  178. package/src/config/index.ts +11 -0
  179. package/src/console/server/app.ts +17 -23
  180. package/src/console/server/bind.ts +4 -0
  181. package/src/console/server/console.test.ts +2 -2
  182. package/src/console/server/flows-invoke.test.ts +50 -34
  183. package/src/console/server/flows.ts +436 -255
  184. package/src/console/server/runs-ingest.test.ts +6 -3
  185. package/src/console/server/serve.ts +1 -1
  186. package/src/console/server/signals.test.ts +1 -5
  187. package/src/console/server/signals.ts +8 -6
  188. package/src/console/server/state.ts +6 -1
  189. package/src/console/server/store.ts +1 -1
  190. package/src/console/ui-next/dist/assets/FileExportIcon-Ck-5od4R.js +1 -0
  191. package/src/console/ui-next/dist/assets/MoreHorizontalCircle01Icon-gMNGsE37.js +1 -0
  192. package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +1 -0
  193. package/src/console/ui-next/dist/assets/UnavailableIcon-D9cvHVPr.js +1 -0
  194. package/src/console/ui-next/dist/assets/UserIcon-DaE2PB5_.js +1 -0
  195. package/src/console/ui-next/dist/assets/access-page-DFeymU07.js +4 -0
  196. package/src/console/ui-next/dist/assets/agent-disclosure-U1rdfblp.js +1 -0
  197. package/src/console/ui-next/dist/assets/cache-glyph-BeFJeqBG.js +1 -0
  198. package/src/console/ui-next/dist/assets/call-pii-button-CVAONPii.js +1 -0
  199. package/src/console/ui-next/dist/assets/collapsible-D2A6NJ-3.js +1 -0
  200. package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +1 -0
  201. package/src/console/ui-next/dist/assets/dagre.esm-B1_XeuLP.js +1 -0
  202. package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +1 -0
  203. package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +1 -0
  204. package/src/console/ui-next/dist/assets/duration-tone-Cgk_h5ja.js +9 -0
  205. package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +1 -0
  206. package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +1 -0
  207. package/src/console/ui-next/dist/assets/flows-page-Dss7941e.js +1 -0
  208. package/src/console/ui-next/dist/assets/highlighted-json-MYZQtRnw.js +154 -0
  209. package/src/console/ui-next/dist/assets/http-method-Jrh39p7A.js +1 -0
  210. package/src/console/ui-next/dist/assets/index-D0zS5rKO.css +2 -0
  211. package/src/console/ui-next/dist/assets/index-DXP2dBIF.js +63 -0
  212. package/src/console/ui-next/dist/assets/observability-page-HvolXxTI.js +4 -0
  213. package/src/console/ui-next/dist/assets/react-dom-Ddte4I-Q.js +9 -0
  214. package/src/console/ui-next/dist/assets/replica-lag-CSh2dzrb.js +18 -0
  215. package/src/console/ui-next/dist/assets/request-meta-BatF8KrK.js +1 -0
  216. package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +1 -0
  217. package/src/console/ui-next/dist/assets/{sql-BskegiFM.js → sql-BsFa4tDR.js} +1 -1
  218. package/src/console/ui-next/dist/assets/store-page-eiKiHnNe.js +41 -0
  219. package/src/console/ui-next/dist/assets/trace-detail-sheet-CZkMeKS-.js +2 -0
  220. package/src/console/ui-next/dist/assets/tree-expand-toggle-CW8y5A2h.js +55 -0
  221. package/src/console/ui-next/dist/assets/units-page-B_RWJrEO.js +1 -0
  222. package/src/console/ui-next/dist/assets/{use-vault-list-uk4WVboC.js → use-vault-list-CT4-gajj.js} +1 -1
  223. package/src/console/ui-next/dist/assets/vault-page-CWrg-A68.js +2 -0
  224. package/src/console/ui-next/dist/assets/xyflow-CSyC6ryz.css +1 -0
  225. package/src/console/ui-next/dist/assets/xyflow-yApv7D4e.js +7 -0
  226. package/src/console/ui-next/dist/index.html +6 -13
  227. package/src/console/ui-next/seed-invoke-host.ts +21 -18
  228. package/src/console/ui-next/src/client.ts +7 -2
  229. package/src/console/ui-next/src/features/flows/graph/element-map.ts +2 -0
  230. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +12 -2
  231. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +4 -0
  232. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +8 -0
  233. package/src/console/ui-next/src/features/flows/traces/waterfall-bars.ts +4 -0
  234. package/src/console/ui-next/src/features/flows/traces/waterfall-tooltip.ts +9 -3
  235. package/src/console/ui-next/src/features/store/detail/reveal-cell.tsx +7 -11
  236. package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +2 -2
  237. package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +2 -0
  238. package/src/console/ui-next/src/features/store/lib/grid-model.test.ts +25 -1
  239. package/src/console/ui-next/src/features/store/lib/grid-model.ts +36 -1
  240. package/src/console/ui-next/src/features/store/query/query-results.tsx +3 -3
  241. package/src/console/ui-next/src/features/units/detail/effects-summary.tsx +2 -0
  242. package/src/console/ui-next/src/features/units/lib/call-read-safe.ts +3 -1
  243. package/src/console/ui-next/src/features/units/lib/fields-from-schema.ts +6 -0
  244. package/src/docker/ai-model-status.test.ts +1 -12
  245. package/src/docker/ai-model-status.ts +10 -38
  246. package/src/docker/compose-up.test.ts +100 -0
  247. package/src/docker/compose-up.ts +292 -0
  248. package/src/docker/compose.ts +4 -14
  249. package/src/docker/derive.ts +9 -34
  250. package/src/docker/docker.test.ts +16 -261
  251. package/src/docker/images-config.test.ts +13 -25
  252. package/src/docker/index.ts +1 -24
  253. package/src/docker/recipes/index.ts +0 -23
  254. package/src/docker/recipes/pgdog.ts +6 -4
  255. package/src/docker/stack-id.test.ts +2 -2
  256. package/src/docker/stack-id.ts +0 -1
  257. package/src/docker/types.ts +2 -2
  258. package/src/drivers/ai-anthropic.ts +10 -0
  259. package/src/drivers/ai-openai-compatible.ts +34 -7
  260. package/src/drivers/ai-providers.test.ts +0 -107
  261. package/src/drivers/ai-stream.test.ts +0 -34
  262. package/src/drivers/ai-types.ts +17 -8
  263. package/src/drivers/channel-fcm.ts +6 -1
  264. package/src/drivers/channel-msegat.ts +6 -1
  265. package/src/drivers/channel-resend.ts +5 -1
  266. package/src/drivers/channel-smtp.ts +5 -1
  267. package/src/drivers/channel-sndr.ts +10 -1
  268. package/src/drivers/channel-taqnyat-mail.ts +5 -1
  269. package/src/drivers/channel-taqnyat-whatsapp.ts +6 -1
  270. package/src/drivers/channel-taqnyat.ts +6 -1
  271. package/src/drivers/channel-types.ts +10 -0
  272. package/src/drivers/channel-unifonic.ts +6 -1
  273. package/src/drivers/channel-wa-cloud.ts +6 -1
  274. package/src/drivers/channel-webpush.ts +6 -1
  275. package/src/drivers/external.ts +33 -0
  276. package/src/drivers/index.ts +0 -10
  277. package/src/drivers/meilisearch.ts +6 -0
  278. package/src/drivers/oauth-types.ts +4 -0
  279. package/src/drivers/postgres.test.ts +74 -3
  280. package/src/drivers/postgres.ts +175 -6
  281. package/src/drivers/signal-types.ts +5 -5
  282. package/src/drivers/types.ts +7 -0
  283. package/src/drivers/vault-types.ts +5 -0
  284. package/src/elements/ai/declare.ts +17 -4
  285. package/src/elements/ai/eval.ts +1 -1
  286. package/src/elements/ai/mcp-http.ts +1 -1
  287. package/src/elements/ai/pii.ts +1 -1
  288. package/src/elements/ai/providers.test.ts +289 -0
  289. package/src/elements/ai/providers.ts +169 -0
  290. package/src/elements/ai/runtime.ts +68 -5
  291. package/src/elements/ai/schema.ts +2 -2
  292. package/src/elements/ai.test.ts +7 -7
  293. package/src/elements/ai.ts +17 -1
  294. package/src/elements/channel/runtime.ts +105 -12
  295. package/src/elements/clock/cron-fields.test.ts +144 -0
  296. package/src/elements/clock/cron-fields.ts +185 -0
  297. package/src/elements/clock/declare.ts +247 -3
  298. package/src/elements/clock.ts +21 -1
  299. package/src/elements/index.ts +15 -0
  300. package/src/elements/signal/chaos-child.ts +1 -2
  301. package/src/elements/signal/declare.ts +65 -37
  302. package/src/elements/signal/delivery-modes.test.ts +11 -31
  303. package/src/elements/signal/dry-run-replay.test.ts +1 -7
  304. package/src/elements/signal/dry-run-write-isolation.test.ts +1 -7
  305. package/src/elements/signal/key-ordering.test.ts +6 -26
  306. package/src/elements/signal/lease-reclaim.test.ts +2 -6
  307. package/src/elements/signal/optional-emit.test.ts +6 -9
  308. package/src/elements/signal/order-lifecycle.test.ts +4 -17
  309. package/src/elements/signal/orphan-messages.test.ts +4 -15
  310. package/src/elements/signal/reconcile.test.ts +2 -2
  311. package/src/elements/signal/runtime.ts +1 -1
  312. package/src/elements/signal/schema-emit.test.ts +4 -8
  313. package/src/elements/signal.test.ts +24 -49
  314. package/src/elements/signal.ts +9 -2
  315. package/src/elements/store/domain-ddl.test.ts +1 -1
  316. package/src/elements/store/live-http.test.ts +9 -4
  317. package/src/elements/store/prepare-row.test.ts +123 -0
  318. package/src/elements/store/resource.ts +43 -12
  319. package/src/elements/store/schema-decl.ts +117 -0
  320. package/src/elements/store/search-backfill.ts +205 -0
  321. package/src/elements/store/search-bind.ts +66 -0
  322. package/src/elements/store/search-bm25.ts +78 -0
  323. package/src/elements/store/search-ddl.ts +156 -0
  324. package/src/elements/store/search-embed-flow.ts +141 -0
  325. package/src/elements/store/search-errors.ts +34 -0
  326. package/src/elements/store/search-fusion.ts +112 -0
  327. package/src/elements/store/search-lsh.ts +179 -0
  328. package/src/elements/store/search-runtime.ts +300 -0
  329. package/src/elements/store/search.test.ts +144 -0
  330. package/src/elements/store/sql-session.test.ts +1 -0
  331. package/src/elements/store/sql-session.ts +83 -3
  332. package/src/elements/store/table.ts +23 -2
  333. package/src/elements/store/upsert-app.test.ts +1 -3
  334. package/src/elements/store.ts +50 -0
  335. package/src/full.ts +4 -2
  336. package/src/http.ts +4 -1
  337. package/src/i18n/catalogs/ar.ts +14 -6
  338. package/src/i18n/catalogs/en.ts +14 -6
  339. package/src/index.ts +6 -1
  340. package/src/kernel/adopt-barrel-fresh.test.ts +7 -7
  341. package/src/kernel/adopt-routes.ts +9 -1
  342. package/src/kernel/app-auth.ts +5 -0
  343. package/src/kernel/app.ts +102 -9
  344. package/src/kernel/auto-cache.test.ts +6 -12
  345. package/src/kernel/auto-registry.test.ts +3 -3
  346. package/src/kernel/boot-bind/ai.test.ts +22 -19
  347. package/src/kernel/boot-bind/ai.ts +5 -43
  348. package/src/kernel/boot-bind/clock.ts +23 -5
  349. package/src/kernel/boot-bind/honor-config.test.ts +4 -4
  350. package/src/kernel/boot.test.ts +11 -6
  351. package/src/kernel/boot.ts +66 -16
  352. package/src/kernel/boundary-contract.ts +93 -0
  353. package/src/kernel/budget.test.ts +1 -1
  354. package/src/kernel/call.ts +89 -0
  355. package/src/kernel/capability.ts +6 -0
  356. package/src/kernel/client-descriptor.ts +123 -0
  357. package/src/kernel/clock-timezone.test.ts +80 -0
  358. package/src/kernel/correlation.test.ts +1 -1
  359. package/src/kernel/dry-run.ts +11 -8
  360. package/src/kernel/effects-stamping.test.ts +42 -9
  361. package/src/kernel/effects.test.ts +4 -2
  362. package/src/kernel/effects.ts +51 -9
  363. package/src/kernel/errors-channel.ts +17 -0
  364. package/src/kernel/errors-live-resume.ts +3 -2
  365. package/src/kernel/errors-tenant.ts +7 -4
  366. package/src/kernel/errors.registry-helpers.ts +96 -0
  367. package/src/kernel/errors.registry.test.ts +139 -25
  368. package/src/kernel/errors.ts +102 -23
  369. package/src/kernel/external-effects.test.ts +196 -0
  370. package/src/kernel/flow.test.ts +13 -4
  371. package/src/kernel/flow.ts +69 -71
  372. package/src/kernel/fx-ask-telemetry.test.ts +2 -2
  373. package/src/kernel/fx-dead-letters.test.ts +3 -3
  374. package/src/kernel/fx-fetch.ts +55 -0
  375. package/src/kernel/fx-live-stream.ts +2 -2
  376. package/src/kernel/fx-live.test.ts +8 -8
  377. package/src/kernel/fx.test.ts +17 -11
  378. package/src/kernel/fx.ts +180 -57
  379. package/src/kernel/horizontal-child.ts +1 -1
  380. package/src/kernel/http-query.test.ts +3 -6
  381. package/src/kernel/index.ts +13 -3
  382. package/src/kernel/instance-id.ts +1 -1
  383. package/src/kernel/instances.test.ts +40 -0
  384. package/src/kernel/instances.ts +47 -9
  385. package/src/kernel/live-http.test.ts +6 -7
  386. package/src/kernel/live-http.ts +6 -2
  387. package/src/kernel/live-resume.test.ts +1 -1
  388. package/src/kernel/mcp-tool.test.ts +3 -3
  389. package/src/kernel/on.ts +98 -4
  390. package/src/kernel/pipeline.test.ts +12 -15
  391. package/src/kernel/plugin-elements.test.ts +1 -1
  392. package/src/kernel/stamp-http.test.ts +3 -3
  393. package/src/kernel/triggers.ts +210 -95
  394. package/src/kernel-entry.ts +0 -1
  395. package/src/manifest/diff.ts +11 -1
  396. package/src/manifest/fixtures/skyport.excerpt.json +1 -1
  397. package/src/manifest/types.ts +46 -2
  398. package/src/mcp/docs-index.ts +3 -3
  399. package/src/mcp/docs-mcp.test.ts +2 -2
  400. package/src/mcp/docs-tools.ts +1 -1
  401. package/src/okid.test.ts +56 -0
  402. package/src/okid.ts +55 -11
  403. package/src/plugins/anonymous.ts +8 -4
  404. package/src/plugins/auth/shared.ts +27 -3
  405. package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
  406. package/src/plugins/auth-methods.security.test.ts +561 -23
  407. package/src/plugins/config-source.ts +1 -1
  408. package/src/plugins/csrf.test.ts +65 -0
  409. package/src/plugins/index.ts +2 -0
  410. package/src/plugins/magic-link.ts +36 -16
  411. package/src/plugins/oauth/shared.ts +3 -1
  412. package/src/plugins/oauth.ts +22 -14
  413. package/src/plugins/otp.ts +37 -21
  414. package/src/plugins/passkey-webauthn.ts +9 -6
  415. package/src/plugins/passkey.ts +98 -26
  416. package/src/plugins/pre-account-hijack.test.ts +223 -0
  417. package/src/plugins/two-factor.ts +466 -37
  418. package/src/plugins/username.ts +19 -7
  419. package/src/release/build-lib.ts +1 -0
  420. package/src/release/limits.ts +2 -2
  421. package/src/release/measure.ts +1 -1
  422. package/src/runs/collect.test.ts +0 -2
  423. package/src/runtime/json-code-block.test.ts +250 -20
  424. package/src/runtime/json-code-block.ts +1261 -41
  425. package/src/term.test.ts +1 -1
  426. package/src/test/create-test-app.test.ts +12 -12
  427. package/src/test/live-signals.test.ts +6 -6
  428. package/src/test/provisions.integration.test.ts +10 -14
  429. package/src/test/reset-element-registries.ts +1 -1
  430. package/src/test/tenant-isolation.test.ts +12 -6
  431. package/site/content/docs/deployment/docker-swarm.mdx +0 -164
  432. package/site/content/docs/deployment/docker.mdx +0 -227
  433. package/site/content/docs/deployment/index.mdx +0 -83
  434. package/site/content/docs/deployment/kubernetes.mdx +0 -176
  435. package/site/content/docs/deployment/meta.json +0 -5
  436. package/site/content/docs/deployment/reverse-proxy.mdx +0 -234
  437. package/site/content/docs/elements/ai.mdx +0 -385
  438. package/site/content/docs/elements/channel.mdx +0 -346
  439. package/site/content/docs/elements/clock.mdx +0 -244
  440. package/site/content/docs/elements/flow.mdx +0 -420
  441. package/site/content/docs/elements/gate.mdx +0 -436
  442. package/site/content/docs/elements/signal.mdx +0 -380
  443. package/site/content/docs/elements/store.mdx +0 -1099
  444. package/site/content/docs/elements/vault.mdx +0 -405
  445. package/site/content/docs/get-started/basic-usage.mdx +0 -173
  446. package/site/content/docs/get-started/index.mdx +0 -43
  447. package/site/content/docs/get-started/installation.mdx +0 -220
  448. package/site/content/docs/get-started/introduction.mdx +0 -144
  449. package/site/content/docs/get-started/meta.json +0 -13
  450. package/site/content/docs/get-started/project-structure.mdx +0 -925
  451. package/site/content/docs/get-started/testing.mdx +0 -328
  452. package/site/content/docs/get-started/why.mdx +0 -114
  453. package/site/content/docs/recipes/llama-cpp.mdx +0 -151
  454. package/site/content/docs/recipes/ollama.mdx +0 -142
  455. package/site/content/docs/recipes/sglang.mdx +0 -105
  456. package/site/content/docs/recipes/vllm.mdx +0 -106
  457. package/site/content/docs/reference/cli.md +0 -232
  458. package/site/content/docs/reference/client.mdx +0 -469
  459. package/site/content/docs/reference/security.md +0 -74
  460. package/src/cli/ai-setup/detect-ollama.ts +0 -213
  461. package/src/cli/ai-setup/recommend.test.ts +0 -225
  462. package/src/cli/ai-setup/recommend.ts +0 -225
  463. package/src/cli/dev-controls.test.ts +0 -122
  464. package/src/cli/dev-controls.ts +0 -164
  465. package/src/cli/tui/DevLive.tsx +0 -126
  466. package/src/console/ui-next/dist/assets/access-page-3-EFj-2G.js +0 -4
  467. package/src/console/ui-next/dist/assets/agent-disclosure-BP0Y0Sux.js +0 -1
  468. package/src/console/ui-next/dist/assets/cache-glyph-B5X-NM0-.js +0 -1
  469. package/src/console/ui-next/dist/assets/call-pii-button-ChAUiLo9.js +0 -1
  470. package/src/console/ui-next/dist/assets/collapsible-DGnOM2ph.js +0 -1
  471. package/src/console/ui-next/dist/assets/copy-inline-button-DBHhgHKP.js +0 -1
  472. package/src/console/ui-next/dist/assets/dagre.esm-ZwcdTuZZ.js +0 -1
  473. package/src/console/ui-next/dist/assets/detail-header-DhHM1iaZ.js +0 -1
  474. package/src/console/ui-next/dist/assets/dropdown-menu-_NjTEo5_.js +0 -1
  475. package/src/console/ui-next/dist/assets/duration-tone-sC3lGABz.js +0 -9
  476. package/src/console/ui-next/dist/assets/element-icons-BVXtRyd3.js +0 -1
  477. package/src/console/ui-next/dist/assets/explorer-empty-2uIhBu0_.js +0 -1
  478. package/src/console/ui-next/dist/assets/flows-page-cVFnA4HH.js +0 -1
  479. package/src/console/ui-next/dist/assets/highlighted-json-Awq7gYdu.js +0 -154
  480. package/src/console/ui-next/dist/assets/http-method-_UHM2ODJ.js +0 -1
  481. package/src/console/ui-next/dist/assets/index-CMIgUbD0.js +0 -66
  482. package/src/console/ui-next/dist/assets/index-Ck88Jmv8.css +0 -2
  483. package/src/console/ui-next/dist/assets/link-BX6Vqztd.js +0 -1
  484. package/src/console/ui-next/dist/assets/observability-page-CQ3p34ip.js +0 -4
  485. package/src/console/ui-next/dist/assets/preload-helper-oH4irX4C.js +0 -1
  486. package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +0 -1
  487. package/src/console/ui-next/dist/assets/react-dom-Bph1y7z7.js +0 -9
  488. package/src/console/ui-next/dist/assets/replica-lag-DKRrbvdo.js +0 -18
  489. package/src/console/ui-next/dist/assets/request-meta-DV0ywz7t.js +0 -1
  490. package/src/console/ui-next/dist/assets/shortcut-JQIZlWfm.js +0 -1
  491. package/src/console/ui-next/dist/assets/shortcut-keys-DKxNTe_m.js +0 -1
  492. package/src/console/ui-next/dist/assets/skeleton-D-czQJT6.js +0 -1
  493. package/src/console/ui-next/dist/assets/store-page-02xOiqIK.js +0 -41
  494. package/src/console/ui-next/dist/assets/trace-detail-sheet-B09O8rA6.js +0 -2
  495. package/src/console/ui-next/dist/assets/tree-expand-toggle-BtyhmWb4.js +0 -55
  496. package/src/console/ui-next/dist/assets/units-page-l8FeKfnP.js +0 -1
  497. package/src/console/ui-next/dist/assets/useMutation-B8EO02Ej.js +0 -1
  498. package/src/console/ui-next/dist/assets/useRender-BE2A9BWC.js +0 -1
  499. package/src/console/ui-next/dist/assets/vault-page-CL-d_mLE.js +0 -2
  500. package/src/console/ui-next/dist/assets/xyflow-D7n4g6go.js +0 -7
  501. package/src/console/ui-next/dist/assets/xyflow-DZ0Ws1xk.css +0 -1
  502. package/src/docker/ollama-pull.ts +0 -232
  503. package/src/docker/recipes/llama-cpp.ts +0 -298
  504. package/src/docker/recipes/ollama.ts +0 -44
  505. package/src/docker/recipes/sglang.ts +0 -55
  506. package/src/docker/recipes/vllm.ts +0 -44
  507. package/src/drivers/ai-ollama-tools.integration.test.ts +0 -109
  508. package/src/drivers/ai-ollama.integration.test.ts +0 -184
  509. package/src/drivers/ai-ollama.ts +0 -389
  510. package/src/drivers/ollama.ts +0 -14
@@ -20,7 +20,6 @@ export interface JsonCodeBlockRenderOptions {
20
20
  readonly method: string;
21
21
  readonly path: string;
22
22
  readonly app: string;
23
- readonly consoleUrl: string;
24
23
  readonly rawHref: string;
25
24
  readonly prettyHref: string;
26
25
  /** Compact one-line JSON (`?raw=1`). Default pretty. */
@@ -31,18 +30,121 @@ export interface JsonCodeBlockRenderOptions {
31
30
  readonly latencyMs?: number;
32
31
  /** Wide-event cache dimension from the invocation. */
33
32
  readonly cache?: JsonCodeCache;
33
+ /** Auth principal that handled the request (traces language). */
34
+ readonly auth?: JsonCodeAuth;
34
35
  }
35
36
 
36
37
  /** Wide-event cache dimension on the browser JSON page. */
37
38
  export type JsonCodeCache = "hit" | "miss" | "none";
38
39
 
40
+ /**
41
+ * Auth mark on the browser JSON page — who handled the request.
42
+ *
43
+ * `public` is the intentional unauthenticated sentinel (`.public()` /
44
+ * `gate.public`). `none` means no credentials and no public gate.
45
+ */
46
+ export type JsonCodeAuthKind = "none" | "public" | "user" | "key" | "operator";
47
+
48
+ /** Auth principal projection for {@link renderJsonCodeBlockHtml}. */
49
+ export interface JsonCodeAuth {
50
+ readonly kind: JsonCodeAuthKind;
51
+ /** Subject id when known (user / key / operator). */
52
+ readonly id?: string | null;
53
+ }
54
+
55
+ /**
56
+ * Build the auth mark from the resolved principal + optional public gate.
57
+ *
58
+ * @param input - Identity + whether the route is `.public()`
59
+ */
60
+ export function jsonCodeAuthFrom(input: {
61
+ readonly userId?: string | null;
62
+ readonly apiKeyId?: string | null;
63
+ readonly operatorId?: string | null;
64
+ readonly publicGate?: boolean;
65
+ }): JsonCodeAuth {
66
+ if (input.operatorId) return { kind: "operator", id: input.operatorId };
67
+ if (input.apiKeyId) return { kind: "key", id: input.apiKeyId };
68
+ if (input.userId) return { kind: "user", id: input.userId };
69
+ if (input.publicGate) return { kind: "public" };
70
+ return { kind: "none" };
71
+ }
39
72
  /** One HTTP route under a {@link JsonCodeNavGroup}. */
40
73
  export interface JsonCodeNavRoute {
41
74
  readonly method: string;
42
75
  readonly path: string;
43
- /** Set for static GET — other verbs / params are listed only. */
76
+ /** Set for static GET — parameterized GET uses Path tab values. */
44
77
  readonly href: string | null;
45
78
  readonly current: boolean;
79
+ /** `:name` segments from {@link path} (empty when static). */
80
+ readonly paramNames: readonly string[];
81
+ }
82
+
83
+ /**
84
+ * Collect `:param` names from an HTTP path template.
85
+ *
86
+ * @param path - e.g. `/notes/:id/archive`
87
+ */
88
+ export function pathParamNames(path: string): string[] {
89
+ const names: string[] = [];
90
+ for (const seg of path.split("/")) {
91
+ if (seg.startsWith(":") && seg.length > 1) names.push(seg.slice(1));
92
+ }
93
+ return names;
94
+ }
95
+
96
+ /**
97
+ * Match a pathname against a `:param` template.
98
+ *
99
+ * @param template - e.g. `/notes/:id`
100
+ * @param pathname - e.g. `/notes/abc`
101
+ */
102
+ export function matchPathTemplate(
103
+ template: string,
104
+ pathname: string,
105
+ ): Record<string, string> | null {
106
+ const tParts = template.split("/");
107
+ const pParts = pathname.split("/");
108
+ if (tParts.length !== pParts.length) return null;
109
+ const out: Record<string, string> = {};
110
+ for (let i = 0; i < tParts.length; i += 1) {
111
+ const t = tParts[i]!;
112
+ const p = pParts[i]!;
113
+ if (t.startsWith(":") && t.length > 1) {
114
+ try {
115
+ out[t.slice(1)] = decodeURIComponent(p);
116
+ } catch {
117
+ out[t.slice(1)] = p;
118
+ }
119
+ continue;
120
+ }
121
+ if (t !== p) return null;
122
+ }
123
+ return out;
124
+ }
125
+
126
+ /**
127
+ * Fill a path template with values. Returns null when any param is empty.
128
+ *
129
+ * @param template - e.g. `/notes/:id`
130
+ * @param values - param name → value
131
+ */
132
+ export function fillPathTemplate(
133
+ template: string,
134
+ values: Readonly<Record<string, string>>,
135
+ ): string | null {
136
+ const parts = template.split("/");
137
+ const out: string[] = [];
138
+ for (const seg of parts) {
139
+ if (seg.startsWith(":") && seg.length > 1) {
140
+ const v = (values[seg.slice(1)] ?? "").trim();
141
+ if (!v) return null;
142
+ out.push(encodeURIComponent(v));
143
+ continue;
144
+ }
145
+ out.push(seg);
146
+ }
147
+ return out.join("/");
46
148
  }
47
149
 
48
150
  /** First-segment group in the Routes rail. */
@@ -293,13 +395,16 @@ export function httpNavGroups(
293
395
  if (!method || !path || path.startsWith("/_/")) continue;
294
396
  const name = navGroupName(path);
295
397
  const key = `${method} ${path}`;
296
- const navigable = method === "GET" && !path.includes(":");
398
+ const paramNames = pathParamNames(path);
399
+ const navigable = method === "GET" && paramNames.length === 0;
400
+ const matched = paramNames.length > 0 ? matchPathTemplate(path, url.pathname) : null;
297
401
  const group = byGroup.get(name) ?? new Map<string, JsonCodeNavRoute>();
298
402
  group.set(key, {
299
403
  method,
300
404
  path,
301
405
  href: navigable ? (raw ? `${path}?raw=1` : path) : null,
302
- current: method === "GET" && url.pathname === path,
406
+ current: method === "GET" && (url.pathname === path || matched !== null),
407
+ paramNames,
303
408
  });
304
409
  byGroup.set(name, group);
305
410
  }
@@ -442,6 +547,7 @@ export function renderJsonCodeBlockHtml(options: JsonCodeBlockRenderOptions): st
442
547
  .lat-critical { color: oklch(0.514 0.222 16.935); }
443
548
  .cache-hit { color: oklch(0.685 0.169 237.323); }
444
549
  .cache-miss { color: oklch(0.666 0.179 58.318); }
550
+ .auth-user, .auth-key, .auth-operator { color: oklch(0.685 0.169 237.323); }
445
551
  }
446
552
  * { box-sizing: border-box; }
447
553
  html, body {
@@ -457,7 +563,7 @@ body {
457
563
  flex-direction: column;
458
564
  font: 13px/1.45 ui-sans-serif, system-ui, sans-serif;
459
565
  }
460
- .page { display: flex; flex-direction: column; height: 100%; }
566
+ .page { display: flex; flex-direction: column; height: 100%; position: relative; }
461
567
  .strip {
462
568
  display: flex;
463
569
  align-items: stretch;
@@ -494,6 +600,16 @@ body {
494
600
  .cache-hit { color: oklch(0.746 0.16 232.661); }
495
601
  .cache-miss { color: oklch(0.828 0.189 84.429); }
496
602
  .cache-none { color: color-mix(in oklab, var(--mute) 40%, transparent); }
603
+ .auth { gap: .25rem; max-width: 9rem; }
604
+ .auth-user, .auth-key, .auth-operator { color: oklch(0.746 0.16 232.661); }
605
+ .auth-public { color: var(--mute); }
606
+ .auth-none { color: color-mix(in oklab, var(--mute) 40%, transparent); }
607
+ .auth-label {
608
+ overflow: hidden;
609
+ text-overflow: ellipsis;
610
+ white-space: nowrap;
611
+ max-width: 6.5rem;
612
+ }
497
613
  .grow { flex: 1; min-width: 0; }
498
614
  .token {
499
615
  display: inline-flex;
@@ -505,9 +621,21 @@ body {
505
621
  text-transform: uppercase;
506
622
  text-decoration: none;
507
623
  color: var(--mute);
624
+ appearance: none;
625
+ border: 0;
626
+ background: transparent;
627
+ cursor: pointer;
628
+ font-family: inherit;
629
+ height: 100%;
508
630
  }
509
631
  .token:hover { background: var(--hover); color: var(--ink); }
510
632
  .token.is-on { color: var(--ink); }
633
+ .sep {
634
+ width: 1px;
635
+ align-self: stretch;
636
+ flex-shrink: 0;
637
+ background: var(--line);
638
+ }
511
639
  .icon {
512
640
  display: inline-flex;
513
641
  align-items: center;
@@ -561,20 +689,242 @@ body {
561
689
  }
562
690
  .copy:hover { background: var(--hover); color: var(--ink); }
563
691
  .copy:focus-visible { outline: 2px solid var(--ink); outline-offset: -2px; }
692
+ svg[hidden] { display: none !important; }
564
693
  .body { display: flex; flex: 1; min-height: 0; }
565
694
  .view { flex: 1; min-width: 0; min-height: 0; overflow: auto; }
566
695
  .rail-check { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
567
696
  .rail {
568
697
  display: flex;
569
- width: 14rem;
698
+ width: 22rem;
570
699
  flex-shrink: 0;
571
700
  flex-direction: column;
572
701
  overflow: hidden;
573
702
  border-left: 1px solid var(--line);
574
703
  }
575
- .rail-head { justify-content: space-between; padding: 0 .5rem; }
576
- .rail-head .head { padding: 0; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
577
- .rail-list { flex: 1; min-height: 0; margin: 0; padding: 0; overflow: auto; list-style: none; }
704
+ .rail-toolbar {
705
+ justify-content: space-between;
706
+ padding: 0 .25rem 0 .5rem;
707
+ flex-shrink: 0;
708
+ }
709
+ .rail-toolbar .head {
710
+ padding: 0;
711
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
712
+ }
713
+ .rail-nav {
714
+ flex: 1;
715
+ min-height: 0;
716
+ overflow: auto;
717
+ display: flex;
718
+ flex-direction: column;
719
+ }
720
+ .rail-sec-label {
721
+ display: flex;
722
+ align-items: center;
723
+ height: 2.5rem;
724
+ margin: 0;
725
+ padding: 0 .5rem;
726
+ flex-shrink: 0;
727
+ border-bottom: 1px solid var(--line);
728
+ font-size: 10px;
729
+ font-weight: 600;
730
+ letter-spacing: .08em;
731
+ text-transform: uppercase;
732
+ color: var(--mute);
733
+ }
734
+ .rail-dock {
735
+ display: flex;
736
+ flex-direction: column;
737
+ flex-shrink: 0;
738
+ max-height: 55%;
739
+ min-height: 0;
740
+ border-top: 1px solid var(--line);
741
+ }
742
+ .rail-dock-strip {
743
+ justify-content: flex-start;
744
+ gap: 0;
745
+ border-bottom: 1px solid var(--line);
746
+ }
747
+ .rail-dock-strip .head {
748
+ gap: .375rem;
749
+ color: var(--ink);
750
+ }
751
+ .rail-dock-strip .head svg { color: var(--mute); }
752
+ .rail-dock-body {
753
+ flex: 1;
754
+ min-height: 0;
755
+ overflow: auto;
756
+ display: flex;
757
+ flex-direction: column;
758
+ }
759
+ .dock-send {
760
+ appearance: none;
761
+ border: 0;
762
+ margin: 0;
763
+ height: 100%;
764
+ padding: 0 .75rem;
765
+ display: inline-flex;
766
+ align-items: center;
767
+ gap: .375rem;
768
+ font: 600 10px/1 ui-sans-serif, system-ui, sans-serif;
769
+ letter-spacing: .08em;
770
+ text-transform: uppercase;
771
+ background: color-mix(in oklab, var(--mute) 18%, transparent);
772
+ color: var(--ink);
773
+ cursor: pointer;
774
+ flex-shrink: 0;
775
+ }
776
+ .dock-send:hover { background: var(--hover); }
777
+ .dock-send:focus-visible { outline: 2px solid var(--ink); outline-offset: -2px; }
778
+ .dock-send:disabled {
779
+ opacity: 0.4;
780
+ cursor: default;
781
+ }
782
+ .rail-acc {
783
+ border-bottom: 1px solid var(--line);
784
+ flex-shrink: 0;
785
+ }
786
+ .rail-acc[hidden] { display: none; }
787
+ .rail-acc-sum {
788
+ display: flex;
789
+ align-items: center;
790
+ gap: .25rem;
791
+ height: 2.5rem;
792
+ padding: 0 .5rem 0 .25rem;
793
+ cursor: pointer;
794
+ list-style: none;
795
+ font-size: 10px;
796
+ font-weight: 600;
797
+ letter-spacing: .08em;
798
+ text-transform: uppercase;
799
+ color: var(--mute);
800
+ user-select: none;
801
+ }
802
+ .rail-acc-sum::-webkit-details-marker { display: none; }
803
+ .rail-acc-sum:hover { background: var(--hover); color: var(--ink); }
804
+ .rail-acc[open] > .rail-acc-sum { color: var(--ink); }
805
+ .rail-acc-sum .chev {
806
+ display: grid;
807
+ width: 1.5rem;
808
+ height: 1.5rem;
809
+ place-items: center;
810
+ color: var(--mute);
811
+ }
812
+ .rail-acc:not([open]) > .rail-acc-sum .chev { transform: rotate(-90deg); }
813
+ .rail-acc-body {
814
+ display: flex;
815
+ flex-direction: column;
816
+ min-height: 0;
817
+ }
818
+ .kv-editor {
819
+ display: flex;
820
+ flex-direction: column;
821
+ min-height: 0;
822
+ }
823
+ .kv-editor[hidden],
824
+ [data-slot="json-code-body-form"][hidden] {
825
+ display: none;
826
+ }
827
+ .kv-rows {
828
+ min-height: 0;
829
+ max-height: 10rem;
830
+ overflow: auto;
831
+ }
832
+ .kv-row {
833
+ display: flex;
834
+ align-items: stretch;
835
+ height: 2rem;
836
+ border-bottom: 1px solid var(--line);
837
+ }
838
+ .kv-key, .kv-val {
839
+ flex: 1;
840
+ min-width: 0;
841
+ appearance: none;
842
+ border: 0;
843
+ background: transparent;
844
+ color: var(--ink);
845
+ font: 11px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
846
+ padding: 0 .5rem;
847
+ outline: none;
848
+ }
849
+ .kv-key { border-right: 1px solid var(--line); max-width: 42%; }
850
+ .kv-key:focus-visible, .kv-val:focus-visible { background: var(--hover); }
851
+ .kv-empty {
852
+ margin: 0;
853
+ padding: .75rem .5rem;
854
+ font-size: 11px;
855
+ color: var(--mute);
856
+ }
857
+ .body-mode-strip {
858
+ display: inline-flex;
859
+ align-items: stretch;
860
+ align-self: stretch;
861
+ height: 100%;
862
+ flex-shrink: 0;
863
+ margin-inline-end: -.5rem;
864
+ }
865
+ .body-mode-strip .token {
866
+ height: 100%;
867
+ }
868
+ .body-json-wrap {
869
+ display: flex;
870
+ flex-direction: column;
871
+ min-height: 0;
872
+ }
873
+ .body-json-wrap[hidden] { display: none; }
874
+ .body-json-editor {
875
+ position: relative;
876
+ max-height: 14rem;
877
+ overflow: auto;
878
+ border-bottom: 1px solid var(--line);
879
+ }
880
+ .body-json-editor:focus-within { background: var(--hover); }
881
+ .body-json-editor.is-bad { box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--fail) 55%, transparent); }
882
+ .body-json-hi {
883
+ position: absolute;
884
+ inset: 0 auto auto 0;
885
+ width: 100%;
886
+ margin: 0;
887
+ padding: .5rem;
888
+ border: 0;
889
+ font: 11px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace;
890
+ white-space: pre-wrap;
891
+ overflow-wrap: anywhere;
892
+ word-break: break-word;
893
+ color: var(--mute);
894
+ pointer-events: none;
895
+ overflow: hidden;
896
+ }
897
+ .body-json {
898
+ position: relative;
899
+ z-index: 1;
900
+ display: block;
901
+ appearance: none;
902
+ border: 0;
903
+ margin: 0;
904
+ min-height: 8rem;
905
+ width: 100%;
906
+ resize: vertical;
907
+ background: transparent;
908
+ color: transparent;
909
+ caret-color: var(--ink);
910
+ font: 11px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace;
911
+ padding: .5rem;
912
+ outline: none;
913
+ overflow: hidden;
914
+ white-space: pre-wrap;
915
+ overflow-wrap: anywhere;
916
+ word-break: break-word;
917
+ }
918
+ .body-json::placeholder { color: var(--mute); }
919
+ .body-json-error {
920
+ margin: 0;
921
+ padding: .375rem .5rem .5rem;
922
+ font-size: 11px;
923
+ color: var(--fail);
924
+ border-top: 1px solid var(--line);
925
+ }
926
+ .body-json-error[hidden] { display: none; }
927
+ .rail-list { margin: 0; padding: 0; overflow: auto; list-style: none; flex: 1; min-height: 0; }
578
928
  .band {
579
929
  display: flex;
580
930
  align-items: center;
@@ -593,7 +943,8 @@ body {
593
943
  place-items: center;
594
944
  color: var(--mute);
595
945
  }
596
- details:not([open]) .chev { transform: rotate(-90deg); }
946
+ details.rail-acc:not([open]) > .rail-acc-sum .chev,
947
+ details:not(.rail-acc):not([open]) .chev { transform: rotate(-90deg); }
597
948
  .band-name {
598
949
  min-width: 0;
599
950
  overflow: hidden;
@@ -616,6 +967,12 @@ details:not([open]) .chev { transform: rotate(-90deg); }
616
967
  padding: .25rem .5rem;
617
968
  text-decoration: none;
618
969
  color: var(--mute);
970
+ appearance: none;
971
+ border: 0;
972
+ background: transparent;
973
+ cursor: pointer;
974
+ font: inherit;
975
+ text-align: left;
619
976
  }
620
977
  .leaf:hover { background: var(--hover); color: var(--ink); }
621
978
  .leaf.is-on { background: oklch(0.25 0.006 286.033 / 0.7); color: var(--ink); }
@@ -694,16 +1051,23 @@ pre {
694
1051
  </style>
695
1052
  </head>
696
1053
  <body>
697
- <main class="page" data-slot="json-code-block" data-state="complete" data-view="${compact ? "raw" : "pretty"}">
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)}">
698
1055
  <header class="strip">
699
1056
  <span class="title">${escapeHtml(options.app)}</span>
700
1057
  <span class="count">${options.status}</span>
701
1058
  ${latencyHtml(options.latencyMs)}
702
1059
  ${cacheHtml(options.cache)}
1060
+ ${authHtml(options.auth)}
703
1061
  <span class="grow"></span>
704
- <a class="token" href="${escapeHtml(options.consoleUrl)}">Console</a>
705
- <a class="token${compact ? "" : " is-on"}" href="${escapeHtml(options.prettyHref)}">Pretty</a>
706
- <a class="token${compact ? " is-on" : ""}" href="${escapeHtml(options.rawHref)}">Raw</a>
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>
707
1071
  </header>
708
1072
  <header class="strip">
709
1073
  <span class="icon" aria-hidden="true">
@@ -712,7 +1076,7 @@ pre {
712
1076
  <span class="file">${escapeHtml(title)}</span>
713
1077
  <span class="head">json</span>
714
1078
  <span class="state">${ok ? "Ready" : options.status}</span>
715
- <button class="copy" type="button" aria-label="Copy code" title="Copy code">
1079
+ <button class="copy" type="button" data-slot="json-code-copy" aria-label="Copy code" title="Copy code">
716
1080
  <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>
717
1081
  <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>
718
1082
  </button>
@@ -725,24 +1089,780 @@ pre {
725
1089
  <textarea id="payload" hidden>${escapeHtml(code)}</textarea>
726
1090
  <script>
727
1091
  (() => {
728
- const btn = document.querySelector(".copy");
1092
+ const HEADERS_KEY = "oke:json-code:headers";
1093
+ const QUERY_KEY = "oke:json-code:query";
1094
+ const PATH_KEY = "oke:json-code:path";
1095
+ const PATH_TEMPLATE_KEY = "oke:json-code:path-template";
1096
+ const METHOD_KEY = "oke:json-code:method";
1097
+ const BODY_MODE_KEY = "oke:json-code:body-mode";
1098
+ const BODY_FORM_KEY = "oke:json-code:body-form";
1099
+ const BODY_JSON_KEY = "oke:json-code:body-json";
1100
+ const SECTION_KEY = "oke:json-code:rail-section";
1101
+ const ACCEPT = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
1102
+ const SECTIONS = ["query", "body", "cookies", "headers", "path"];
1103
+ const page = document.querySelector("[data-slot=json-code-block]");
1104
+ const railCheck = document.getElementById("json-code-rail");
1105
+ const resetBtn = document.querySelector("[data-slot=json-code-reset]");
1106
+ const sendBtn = document.querySelector("[data-slot=json-code-send]");
1107
+ const bodyFormHost = document.querySelector("[data-slot=json-code-body-form]");
1108
+ const bodyJsonHost = document.querySelector("[data-slot=json-code-body-json]");
1109
+ const bodyEditor = document.querySelector("[data-slot=json-code-body-editor]");
1110
+ const bodyHi = document.querySelector("[data-slot=json-code-body-hi]");
1111
+ const bodyRaw = document.querySelector("[data-slot=json-code-body-raw]");
1112
+ const bodyError = document.querySelector("[data-slot=json-code-body-error]");
1113
+ const bodyModeBtns = document.querySelectorAll("[data-slot=json-code-body-mode]");
1114
+ const btn = document.querySelector("[data-slot=json-code-copy]");
729
1115
  const payload = document.getElementById("payload");
730
1116
  const copyIcon = btn?.querySelector("[data-copy]");
731
1117
  const doneIcon = btn?.querySelector("[data-done]");
732
- if (!btn || !payload) return;
733
- btn.addEventListener("click", async () => {
734
- try { await navigator.clipboard.writeText(payload.value); } catch { payload.select(); document.execCommand("copy"); }
735
- btn.setAttribute("aria-label", "Copied");
736
- btn.title = "Copied";
737
- if (copyIcon) copyIcon.hidden = true;
738
- if (doneIcon) doneIcon.hidden = false;
739
- setTimeout(() => {
740
- btn.setAttribute("aria-label", "Copy code");
741
- btn.title = "Copy code";
742
- if (copyIcon) copyIcon.hidden = false;
743
- if (doneIcon) doneIcon.hidden = true;
744
- }, 1600);
1118
+ if (!page || !sendBtn) return;
1119
+
1120
+ function currentPath() {
1121
+ return page.getAttribute("data-path") || location.pathname;
1122
+ }
1123
+
1124
+ function rememberMethod(method, path) {
1125
+ const m = String(method || "").toUpperCase();
1126
+ if (m) sessionStorage.setItem(METHOD_KEY, m);
1127
+ if (path) page.setAttribute("data-path", path);
1128
+ if (m) page.setAttribute("data-method", m);
1129
+ }
1130
+
1131
+ function currentMethod() {
1132
+ return (
1133
+ sessionStorage.getItem(METHOD_KEY) ||
1134
+ page.getAttribute("data-method") ||
1135
+ "GET"
1136
+ ).toUpperCase();
1137
+ }
1138
+
1139
+ function methodAllowsBody(method) {
1140
+ switch (String(method || "").toUpperCase()) {
1141
+ case "GET":
1142
+ case "HEAD":
1143
+ case "DELETE":
1144
+ return false;
1145
+ default:
1146
+ return true;
1147
+ }
1148
+ }
1149
+
1150
+ function bodyMode() {
1151
+ const mode = sessionStorage.getItem(BODY_MODE_KEY);
1152
+ return mode === "json" ? "json" : "form";
1153
+ }
1154
+
1155
+ function setBodyError(message) {
1156
+ if (!bodyError || !bodyRaw) return;
1157
+ if (!message) {
1158
+ bodyError.hidden = true;
1159
+ bodyError.textContent = "";
1160
+ if (bodyEditor) bodyEditor.classList.remove("is-bad");
1161
+ bodyRaw.classList.remove("is-bad");
1162
+ return;
1163
+ }
1164
+ bodyError.hidden = false;
1165
+ bodyError.textContent = message;
1166
+ if (bodyEditor) bodyEditor.classList.add("is-bad");
1167
+ bodyRaw.classList.add("is-bad");
1168
+ }
1169
+
1170
+ function isJsonIdentChar(ch) {
1171
+ return Boolean(ch) && ((ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z") || (ch >= "0" && ch <= "9") || ch === "_");
1172
+ }
1173
+
1174
+ function scanBodyString(source, start) {
1175
+ let i = start + 1;
1176
+ while (i < source.length) {
1177
+ const ch = source[i];
1178
+ if (ch === "\\\\") { i += 2; continue; }
1179
+ if (ch === '"') return i + 1;
1180
+ i += 1;
1181
+ }
1182
+ return source.length;
1183
+ }
1184
+
1185
+ function scanBodyNumber(source, start) {
1186
+ let i = start;
1187
+ if (source[i] === "-") i += 1;
1188
+ while (i < source.length && source[i] >= "0" && source[i] <= "9") i += 1;
1189
+ if (source[i] === ".") {
1190
+ i += 1;
1191
+ while (i < source.length && source[i] >= "0" && source[i] <= "9") i += 1;
1192
+ }
1193
+ const exp = source[i];
1194
+ if (exp === "e" || exp === "E") {
1195
+ i += 1;
1196
+ if (source[i] === "+" || source[i] === "-") i += 1;
1197
+ while (i < source.length && source[i] >= "0" && source[i] <= "9") i += 1;
1198
+ }
1199
+ return i;
1200
+ }
1201
+
1202
+ function tokenizeBodyJson(source) {
1203
+ const tokens = [];
1204
+ let i = 0;
1205
+ while (i < source.length) {
1206
+ const ch = source[i];
1207
+ if (ch === " " || ch === "\\t" || ch === "\\n" || ch === "\\r") {
1208
+ let end = i + 1;
1209
+ while (end < source.length) {
1210
+ const next = source[end];
1211
+ if (next !== " " && next !== "\\t" && next !== "\\n" && next !== "\\r") break;
1212
+ end += 1;
1213
+ }
1214
+ tokens.push({ kind: "space", text: source.slice(i, end) });
1215
+ i = end;
1216
+ continue;
1217
+ }
1218
+ if (ch === "{" || ch === "}" || ch === "[" || ch === "]" || ch === ":" || ch === ",") {
1219
+ tokens.push({ kind: "punct", text: ch });
1220
+ i += 1;
1221
+ continue;
1222
+ }
1223
+ if (ch === '"') {
1224
+ const end = scanBodyString(source, i);
1225
+ tokens.push({ kind: "string", text: source.slice(i, end) });
1226
+ i = end;
1227
+ continue;
1228
+ }
1229
+ if (ch === "-" || (ch >= "0" && ch <= "9")) {
1230
+ const end = scanBodyNumber(source, i);
1231
+ tokens.push({ kind: "number", text: source.slice(i, end) });
1232
+ i = end;
1233
+ continue;
1234
+ }
1235
+ if (source.startsWith("true", i) && !isJsonIdentChar(source[i + 4])) {
1236
+ tokens.push({ kind: "literal", text: "true" });
1237
+ i += 4;
1238
+ continue;
1239
+ }
1240
+ if (source.startsWith("false", i) && !isJsonIdentChar(source[i + 5])) {
1241
+ tokens.push({ kind: "literal", text: "false" });
1242
+ i += 5;
1243
+ continue;
1244
+ }
1245
+ if (source.startsWith("null", i) && !isJsonIdentChar(source[i + 4])) {
1246
+ tokens.push({ kind: "literal", text: "null" });
1247
+ i += 4;
1248
+ continue;
1249
+ }
1250
+ tokens.push({ kind: "space", text: ch });
1251
+ i += 1;
1252
+ }
1253
+ const out = [];
1254
+ for (let t = 0; t < tokens.length; t += 1) {
1255
+ const token = tokens[t];
1256
+ if (token.kind !== "string") {
1257
+ out.push(token);
1258
+ continue;
1259
+ }
1260
+ let j = t + 1;
1261
+ while (j < tokens.length && tokens[j].kind === "space") j += 1;
1262
+ out.push(
1263
+ tokens[j] && tokens[j].kind === "punct" && tokens[j].text === ":"
1264
+ ? { kind: "key", text: token.text }
1265
+ : token,
1266
+ );
1267
+ }
1268
+ return out;
1269
+ }
1270
+
1271
+ function highlightBodyJson(source) {
1272
+ if (!source) return "";
1273
+ return tokenizeBodyJson(source).map((token) => {
1274
+ const text = String(token.text)
1275
+ .replace(/&/g, "&amp;")
1276
+ .replace(/</g, "&lt;")
1277
+ .replace(/>/g, "&gt;");
1278
+ if (token.kind === "key") return '<span class="k">' + text + "</span>";
1279
+ if (token.kind === "string") return '<span class="s">' + text + "</span>";
1280
+ if (token.kind === "number") return '<span class="m">' + text + "</span>";
1281
+ if (token.kind === "literal") return '<span class="l">' + text + "</span>";
1282
+ if (token.kind === "punct") return '<span class="p">' + text + "</span>";
1283
+ return text;
1284
+ }).join("");
1285
+ }
1286
+
1287
+ function refreshBodyHighlight(opts) {
1288
+ if (!bodyRaw || !bodyHi) return;
1289
+ const soft = !opts || opts.soft !== false;
1290
+ const text = bodyRaw.value;
1291
+ bodyHi.innerHTML = highlightBodyJson(text);
1292
+ bodyRaw.style.height = "auto";
1293
+ bodyRaw.style.height = Math.max(bodyRaw.scrollHeight, 128) + "px";
1294
+ const trim = text.trim();
1295
+ if (!trim) {
1296
+ setBodyError("");
1297
+ return;
1298
+ }
1299
+ try {
1300
+ JSON.parse(trim);
1301
+ setBodyError("");
1302
+ } catch {
1303
+ if (soft) {
1304
+ if (bodyEditor) bodyEditor.classList.add("is-bad");
1305
+ bodyRaw.classList.add("is-bad");
1306
+ if (bodyError) {
1307
+ bodyError.hidden = true;
1308
+ bodyError.textContent = "";
1309
+ }
1310
+ } else {
1311
+ setBodyError("Body must be valid JSON.");
1312
+ }
1313
+ }
1314
+ }
1315
+
1316
+ function prettyBodyJson() {
1317
+ if (!bodyRaw) return;
1318
+ const trim = bodyRaw.value.trim();
1319
+ if (!trim) return;
1320
+ try {
1321
+ bodyRaw.value = JSON.stringify(JSON.parse(trim), null, 2);
1322
+ sessionStorage.setItem(BODY_JSON_KEY, bodyRaw.value);
1323
+ refreshBodyHighlight({ soft: true });
1324
+ } catch {
1325
+ refreshBodyHighlight({ soft: false });
1326
+ }
1327
+ }
1328
+
1329
+ function syncBodyMode(mode) {
1330
+ const next = mode === "json" ? "json" : "form";
1331
+ sessionStorage.setItem(BODY_MODE_KEY, next);
1332
+ for (const el of bodyModeBtns) {
1333
+ const on = el.getAttribute("data-mode") === next;
1334
+ el.classList.toggle("is-on", on);
1335
+ el.setAttribute("aria-pressed", on ? "true" : "false");
1336
+ }
1337
+ if (bodyFormHost) bodyFormHost.hidden = next !== "form";
1338
+ if (bodyJsonHost) bodyJsonHost.hidden = next !== "json";
1339
+ if (next === "form") {
1340
+ setBodyError("");
1341
+ renderKv("body");
1342
+ } else if (bodyRaw) {
1343
+ bodyRaw.value = sessionStorage.getItem(BODY_JSON_KEY) || "";
1344
+ refreshBodyHighlight({ soft: true });
1345
+ }
1346
+ }
1347
+
1348
+ function syncPathChapter() {
1349
+ const pathSection = document.querySelector('[data-rail-section="path"]');
1350
+ if (!(pathSection instanceof HTMLElement)) return;
1351
+ const template = sessionStorage.getItem(PATH_TEMPLATE_KEY) || "";
1352
+ const show = pathNames(template).length > 0;
1353
+ pathSection.hidden = !show;
1354
+ if (!show && pathSection instanceof HTMLDetailsElement) pathSection.open = false;
1355
+ }
1356
+
1357
+ function openSection(name) {
1358
+ if (!SECTIONS.includes(name)) return;
1359
+ sessionStorage.setItem(SECTION_KEY, name);
1360
+ if (railCheck) railCheck.checked = true;
1361
+ const section = document.querySelector('[data-rail-section="' + name + '"]');
1362
+ if (section instanceof HTMLDetailsElement) {
1363
+ section.hidden = false;
1364
+ section.open = true;
1365
+ section.scrollIntoView({ block: "nearest" });
1366
+ }
1367
+ if (name === "cookies" || name === "headers" || name === "query" || name === "path" || name === "body") {
1368
+ if (name === "body") syncBodyMode(bodyMode());
1369
+ else renderKv(name);
1370
+ }
1371
+ }
1372
+
1373
+ function escAttr(value) {
1374
+ return String(value)
1375
+ .replace(/&/g, "&amp;")
1376
+ .replace(/"/g, "&quot;")
1377
+ .replace(/</g, "&lt;")
1378
+ .replace(/>/g, "&gt;");
1379
+ }
1380
+
1381
+ function loadPairs(key) {
1382
+ try {
1383
+ const raw = sessionStorage.getItem(key);
1384
+ if (!raw) return [];
1385
+ const parsed = JSON.parse(raw);
1386
+ if (!Array.isArray(parsed)) return [];
1387
+ return parsed.filter((p) => p && typeof p.key === "string");
1388
+ } catch {
1389
+ return [];
1390
+ }
1391
+ }
1392
+
1393
+ function savePairs(key, pairs) {
1394
+ sessionStorage.setItem(key, JSON.stringify(pairs));
1395
+ }
1396
+
1397
+ function readCookiePairs() {
1398
+ const raw = document.cookie || "";
1399
+ if (!raw.trim()) return [];
1400
+ return raw.split(";").map((part) => {
1401
+ const eq = part.indexOf("=");
1402
+ if (eq === -1) return { key: part.trim(), value: "" };
1403
+ let value = part.slice(eq + 1).trim();
1404
+ try { value = decodeURIComponent(value); } catch {}
1405
+ return { key: part.slice(0, eq).trim(), value };
1406
+ }).filter((p) => p.key);
1407
+ }
1408
+
1409
+ function writeCookies(pairs) {
1410
+ const existing = new Set(readCookiePairs().map((p) => p.key));
1411
+ const next = new Set();
1412
+ for (const p of pairs) {
1413
+ const k = (p.key || "").trim();
1414
+ if (!k) continue;
1415
+ next.add(k);
1416
+ document.cookie = encodeURIComponent(k) + "=" + encodeURIComponent((p.value || "").trim()) + "; path=/";
1417
+ }
1418
+ for (const k of existing) {
1419
+ if (!next.has(k)) {
1420
+ document.cookie = encodeURIComponent(k) + "=; path=/; max-age=0";
1421
+ }
1422
+ }
1423
+ }
1424
+
1425
+ function pathNames(template) {
1426
+ return String(template || "").split("/").filter((s) => s.startsWith(":") && s.length > 1).map((s) => s.slice(1));
1427
+ }
1428
+
1429
+ function fillPath(template, values) {
1430
+ const parts = String(template || "").split("/");
1431
+ const out = [];
1432
+ for (const seg of parts) {
1433
+ if (seg.startsWith(":") && seg.length > 1) {
1434
+ const v = (values[seg.slice(1)] ?? "").trim();
1435
+ if (!v) return null;
1436
+ out.push(encodeURIComponent(v));
1437
+ continue;
1438
+ }
1439
+ out.push(seg);
1440
+ }
1441
+ return out.join("/");
1442
+ }
1443
+
1444
+ function collectKv(kind) {
1445
+ const host = document.querySelector("[data-slot=json-code-kv-rows][data-kv=" + kind + "]");
1446
+ if (!host) return [];
1447
+ const out = [];
1448
+ for (const row of host.querySelectorAll(".kv-row")) {
1449
+ const key = (row.querySelector(".kv-key")?.value || "").trim();
1450
+ const value = row.querySelector(".kv-val")?.value || "";
1451
+ if (!key && !value.trim()) continue;
1452
+ if (kind === "headers" && key.toLowerCase() === "authorization") continue;
1453
+ out.push({ key, value });
1454
+ }
1455
+ return out;
1456
+ }
1457
+
1458
+ function renderKv(kind) {
1459
+ const host = document.querySelector("[data-slot=json-code-kv-rows][data-kv=" + kind + "]");
1460
+ if (!host) return;
1461
+ let pairs;
1462
+ if (kind === "cookies") {
1463
+ pairs = readCookiePairs();
1464
+ } else if (kind === "path") {
1465
+ const template = sessionStorage.getItem(PATH_TEMPLATE_KEY) || "";
1466
+ const names = pathNames(template);
1467
+ if (names.length === 0) {
1468
+ host.innerHTML = '<p class="kv-empty">Select a parameterized GET route</p>';
1469
+ return;
1470
+ }
1471
+ const stored = Object.fromEntries(loadPairs(PATH_KEY).map((p) => [p.key, p.value]));
1472
+ pairs = names.map((n) => ({ key: n, value: stored[n] || "" }));
1473
+ } else if (kind === "headers") {
1474
+ pairs = loadPairs(HEADERS_KEY).filter((p) => String(p.key).toLowerCase() !== "authorization");
1475
+ if (pairs.length === 0) pairs = [{ key: "accept", value: ACCEPT }];
1476
+ } else if (kind === "body") {
1477
+ pairs = loadPairs(BODY_FORM_KEY);
1478
+ } else {
1479
+ pairs = loadPairs(QUERY_KEY);
1480
+ }
1481
+ pairs = pairs.concat([{ key: "", value: "" }]);
1482
+ host.innerHTML = pairs.map((p) =>
1483
+ '<div class="kv-row">' +
1484
+ '<input class="kv-key" spellcheck="false" autocomplete="off" placeholder="Key" value="' + escAttr(p.key) + '" />' +
1485
+ '<input class="kv-val" spellcheck="false" autocomplete="off" placeholder="Value" value="' + escAttr(p.value ?? "") + '" />' +
1486
+ "</div>"
1487
+ ).join("");
1488
+ }
1489
+
1490
+ function ensureSeeds() {
1491
+ if (!sessionStorage.getItem(HEADERS_KEY)) {
1492
+ savePairs(HEADERS_KEY, [{ key: "accept", value: ACCEPT }]);
1493
+ }
1494
+ if (!sessionStorage.getItem(QUERY_KEY)) {
1495
+ const pairs = [];
1496
+ for (const [key, value] of new URLSearchParams(location.search)) pairs.push({ key, value });
1497
+ savePairs(QUERY_KEY, pairs);
1498
+ }
1499
+ if (!sessionStorage.getItem(BODY_MODE_KEY)) sessionStorage.setItem(BODY_MODE_KEY, "form");
1500
+ if (!sessionStorage.getItem(BODY_FORM_KEY)) savePairs(BODY_FORM_KEY, []);
1501
+ if (!sessionStorage.getItem(BODY_JSON_KEY)) sessionStorage.setItem(BODY_JSON_KEY, "");
1502
+ if (!sessionStorage.getItem(METHOD_KEY)) {
1503
+ const m = (page.getAttribute("data-method") || "GET").toUpperCase();
1504
+ sessionStorage.setItem(METHOD_KEY, m);
1505
+ }
1506
+ const pathAttr = page.getAttribute("data-path") || location.pathname;
1507
+ const template = sessionStorage.getItem(PATH_TEMPLATE_KEY);
1508
+ if (template) {
1509
+ const matched = (() => {
1510
+ const tParts = template.split("/");
1511
+ const pParts = pathAttr.split("/");
1512
+ if (tParts.length !== pParts.length) return null;
1513
+ const out = {};
1514
+ for (let i = 0; i < tParts.length; i += 1) {
1515
+ const t = tParts[i];
1516
+ const p = pParts[i];
1517
+ if (t.startsWith(":") && t.length > 1) {
1518
+ try { out[t.slice(1)] = decodeURIComponent(p); } catch { out[t.slice(1)] = p; }
1519
+ continue;
1520
+ }
1521
+ if (t !== p) return null;
1522
+ }
1523
+ return out;
1524
+ })();
1525
+ if (matched) {
1526
+ savePairs(PATH_KEY, Object.entries(matched).map(([key, value]) => ({ key, value })));
1527
+ }
1528
+ }
1529
+ }
1530
+
1531
+ function requestHeaders(contentType) {
1532
+ const headers = { accept: ACCEPT };
1533
+ for (const p of loadPairs(HEADERS_KEY)) {
1534
+ const k = (p.key || "").trim();
1535
+ if (!k) continue;
1536
+ const lower = k.toLowerCase();
1537
+ if (lower === "authorization") continue;
1538
+ headers[k] = p.value ?? "";
1539
+ }
1540
+ if (contentType) {
1541
+ const hasCt = Object.keys(headers).some((k) => k.toLowerCase() === "content-type");
1542
+ if (!hasCt) headers["content-type"] = contentType;
1543
+ }
1544
+ return headers;
1545
+ }
1546
+
1547
+ function buildQueryUrl(pathname) {
1548
+ const params = new URLSearchParams();
1549
+ for (const p of loadPairs(QUERY_KEY)) {
1550
+ const k = (p.key || "").trim();
1551
+ if (!k) continue;
1552
+ params.set(k, p.value ?? "");
1553
+ }
1554
+ const q = params.toString();
1555
+ return q ? pathname + "?" + q : pathname;
1556
+ }
1557
+
1558
+ function buildRequestBody() {
1559
+ const mode = bodyMode();
1560
+ if (mode === "json") {
1561
+ const raw = (bodyRaw?.value ?? sessionStorage.getItem(BODY_JSON_KEY) ?? "").trim();
1562
+ if (!raw) return { ok: true, empty: true };
1563
+ try {
1564
+ JSON.parse(raw);
1565
+ } catch {
1566
+ return { ok: false, error: "Body must be valid JSON." };
1567
+ }
1568
+ return { ok: true, empty: false, body: raw, contentType: "application/json" };
1569
+ }
1570
+ const pairs = collectKv("body");
1571
+ if (pairs.length === 0) return { ok: true, empty: true };
1572
+ const params = new URLSearchParams();
1573
+ for (const p of pairs) params.set(p.key, p.value ?? "");
1574
+ return {
1575
+ ok: true,
1576
+ empty: false,
1577
+ body: params.toString(),
1578
+ contentType: "application/x-www-form-urlencoded",
1579
+ };
1580
+ }
1581
+
1582
+ function escHtmlText(value) {
1583
+ return String(value)
1584
+ .replace(/&/g, "&amp;")
1585
+ .replace(/</g, "&lt;")
1586
+ .replace(/>/g, "&gt;");
1587
+ }
1588
+
1589
+ async function navigate(url, headers, init) {
1590
+ const opts = init && typeof init === "object" ? init : {};
1591
+ try {
1592
+ const res = await fetch(url, {
1593
+ credentials: "same-origin",
1594
+ ...opts,
1595
+ headers,
1596
+ });
1597
+ const text = await res.text();
1598
+ const ct = (res.headers.get("content-type") || "").toLowerCase();
1599
+ if (ct.includes("text/html")) {
1600
+ document.open();
1601
+ document.write(text);
1602
+ document.close();
1603
+ return;
1604
+ }
1605
+ let display = text;
1606
+ try { display = JSON.stringify(JSON.parse(text), null, 2); } catch {}
1607
+ document.open();
1608
+ document.write(
1609
+ "<!doctype html><html lang=\\"en\\"><head><meta charset=\\"utf-8\\"><meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1\\"><title>" +
1610
+ escHtmlText(String(res.status)) +
1611
+ "</title><style>body{margin:0;background:#111;color:#eee;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace}pre{margin:0;padding:1rem;white-space:pre-wrap;overflow-wrap:anywhere}</style></head><body><pre>" +
1612
+ escHtmlText(display) +
1613
+ "</pre></body></html>",
1614
+ );
1615
+ document.close();
1616
+ } catch {
1617
+ const method = String(opts.method || "GET").toUpperCase();
1618
+ if (method === "GET" || method === "HEAD") location.assign(url);
1619
+ }
1620
+ }
1621
+
1622
+ function persistOptions() {
1623
+ writeCookies(collectKv("cookies"));
1624
+ savePairs(
1625
+ HEADERS_KEY,
1626
+ collectKv("headers").filter((p) => p.key.toLowerCase() !== "authorization"),
1627
+ );
1628
+ savePairs(QUERY_KEY, collectKv("query"));
1629
+ savePairs(BODY_FORM_KEY, collectKv("body"));
1630
+ if (bodyRaw) sessionStorage.setItem(BODY_JSON_KEY, bodyRaw.value);
1631
+ sessionStorage.setItem(BODY_MODE_KEY, bodyMode());
1632
+ const pathPairs = collectKv("path");
1633
+ if (pathPairs.length > 0) savePairs(PATH_KEY, pathPairs);
1634
+ }
1635
+
1636
+ function resolveSendUrl() {
1637
+ const template = sessionStorage.getItem(PATH_TEMPLATE_KEY) || "";
1638
+ const names = pathNames(template);
1639
+ if (names.length > 0) {
1640
+ const values = Object.fromEntries(loadPairs(PATH_KEY).map((p) => [p.key, p.value]));
1641
+ const filled = fillPath(template, values);
1642
+ if (!filled) return null;
1643
+ return buildQueryUrl(filled);
1644
+ }
1645
+ const path = page.getAttribute("data-path") || location.pathname;
1646
+ return buildQueryUrl(path);
1647
+ }
1648
+
1649
+ async function sendRequest() {
1650
+ persistOptions();
1651
+ const url = resolveSendUrl();
1652
+ if (!url) {
1653
+ openSection("path");
1654
+ renderKv("path");
1655
+ return;
1656
+ }
1657
+ const payloadBody = buildRequestBody();
1658
+ if (!payloadBody.ok) {
1659
+ setBodyError(payloadBody.error || "Invalid body");
1660
+ syncBodyMode("json");
1661
+ openSection("body");
1662
+ return;
1663
+ }
1664
+ setBodyError("");
1665
+ let method = currentMethod();
1666
+ if (!payloadBody.empty && !methodAllowsBody(method)) method = "POST";
1667
+ if (payloadBody.empty && !methodAllowsBody(method)) {
1668
+ // GET-style send — no body on the wire
1669
+ sendBtn.disabled = true;
1670
+ try {
1671
+ await navigate(url, requestHeaders());
1672
+ } finally {
1673
+ sendBtn.disabled = false;
1674
+ }
1675
+ return;
1676
+ }
1677
+ sendBtn.disabled = true;
1678
+ try {
1679
+ const headers = requestHeaders(payloadBody.empty ? undefined : payloadBody.contentType);
1680
+ await navigate(url, headers, {
1681
+ method,
1682
+ ...(payloadBody.empty ? {} : { body: payloadBody.body }),
1683
+ });
1684
+ } finally {
1685
+ sendBtn.disabled = false;
1686
+ }
1687
+ }
1688
+
1689
+ function resetOptions() {
1690
+ savePairs(HEADERS_KEY, [{ key: "accept", value: ACCEPT }]);
1691
+ const pairs = [];
1692
+ for (const [key, value] of new URLSearchParams(location.search)) pairs.push({ key, value });
1693
+ savePairs(QUERY_KEY, pairs);
1694
+ savePairs(BODY_FORM_KEY, []);
1695
+ sessionStorage.setItem(BODY_JSON_KEY, "");
1696
+ sessionStorage.setItem(BODY_MODE_KEY, "form");
1697
+ const template = sessionStorage.getItem(PATH_TEMPLATE_KEY) || "";
1698
+ if (pathNames(template).length > 0) {
1699
+ savePairs(PATH_KEY, pathNames(template).map((n) => ({ key: n, value: "" })));
1700
+ }
1701
+ for (const kind of ["cookies", "headers", "query", "path", "body"]) renderKv(kind);
1702
+ syncBodyMode("form");
1703
+ syncPathChapter();
1704
+ }
1705
+
1706
+ function pathValuesFilled(template) {
1707
+ const names = pathNames(template);
1708
+ if (names.length === 0) return false;
1709
+ const stored = Object.fromEntries(loadPairs(PATH_KEY).map((p) => [p.key, p.value]));
1710
+ return names.every((n) => (stored[n] || "").trim().length > 0);
1711
+ }
1712
+
1713
+ ensureSeeds();
1714
+ for (const kind of ["cookies", "headers", "query", "path", "body"]) renderKv(kind);
1715
+ syncBodyMode(bodyMode());
1716
+ syncPathChapter();
1717
+ const saved = sessionStorage.getItem(SECTION_KEY) || "";
1718
+ if (SECTIONS.includes(saved)) openSection(saved);
1719
+
1720
+ if (resetBtn) resetBtn.addEventListener("click", () => resetOptions());
1721
+ sendBtn.addEventListener("click", () => void sendRequest());
1722
+ for (const el of bodyModeBtns) {
1723
+ el.addEventListener("click", (e) => {
1724
+ e.preventDefault();
1725
+ e.stopPropagation();
1726
+ const mode = el.getAttribute("data-mode") || "form";
1727
+ if (mode === "json" && bodyRaw) {
1728
+ savePairs(BODY_FORM_KEY, collectKv("body"));
1729
+ } else if (bodyRaw) {
1730
+ sessionStorage.setItem(BODY_JSON_KEY, bodyRaw.value);
1731
+ }
1732
+ syncBodyMode(mode);
1733
+ });
1734
+ }
1735
+ if (bodyRaw) {
1736
+ bodyRaw.addEventListener("input", () => {
1737
+ sessionStorage.setItem(BODY_JSON_KEY, bodyRaw.value);
1738
+ refreshBodyHighlight({ soft: true });
1739
+ });
1740
+ bodyRaw.addEventListener("blur", () => prettyBodyJson());
1741
+ bodyRaw.addEventListener("scroll", () => {
1742
+ if (!bodyHi) return;
1743
+ bodyHi.style.transform = "translate(" + (-bodyRaw.scrollLeft) + "px," + (-bodyRaw.scrollTop) + "px)";
1744
+ });
1745
+ }
1746
+
1747
+ for (const section of document.querySelectorAll(".rail-dock-body details[data-rail-section]")) {
1748
+ section.addEventListener("toggle", () => {
1749
+ if (!(section instanceof HTMLDetailsElement) || !section.open) return;
1750
+ const name = section.getAttribute("data-rail-section");
1751
+ if (!name) return;
1752
+ sessionStorage.setItem(SECTION_KEY, name);
1753
+ if (name === "cookies" || name === "headers" || name === "query" || name === "path" || name === "body") {
1754
+ if (name === "body") syncBodyMode(bodyMode());
1755
+ else renderKv(name);
1756
+ }
1757
+ });
1758
+ }
1759
+
1760
+ document.addEventListener("keydown", (e) => {
1761
+ if (e.key !== "Enter") return;
1762
+ const t = e.target;
1763
+ if (!(t instanceof HTMLInputElement)) return;
1764
+ if (!t.closest(".rail-dock")) return;
1765
+ e.preventDefault();
1766
+ void sendRequest();
1767
+ });
1768
+
1769
+ function selectRouteLeaf(leaf) {
1770
+ for (const node of document.querySelectorAll("a.leaf.is-on, button.leaf.is-on")) {
1771
+ node.classList.remove("is-on");
1772
+ }
1773
+ leaf.classList.add("is-on");
1774
+ const method = (leaf.getAttribute("data-method") || currentMethod()).toUpperCase();
1775
+ 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;
1778
+ const title = document.querySelector("title");
1779
+ if (title) {
1780
+ const app = page.querySelector(".title")?.textContent || "";
1781
+ title.textContent = method + " " + path + (app ? " · " + app : "");
1782
+ }
1783
+ }
1784
+
1785
+ document.addEventListener("click", (e) => {
1786
+ const t = e.target;
1787
+ const el = t instanceof Element ? t : t && t.parentElement;
1788
+ if (!el) return;
1789
+ const paramLeaf = el.closest("button.leaf[data-param-template]");
1790
+ if (paramLeaf) {
1791
+ e.preventDefault();
1792
+ const template = paramLeaf.getAttribute("data-param-template") || "";
1793
+ rememberMethod(
1794
+ paramLeaf.getAttribute("data-method") || "GET",
1795
+ template,
1796
+ );
1797
+ sessionStorage.setItem(PATH_TEMPLATE_KEY, template);
1798
+ const names = pathNames(template);
1799
+ const stored = Object.fromEntries(loadPairs(PATH_KEY).map((p) => [p.key, p.value]));
1800
+ const next = names.map((n) => ({ key: n, value: stored[n] || "" }));
1801
+ savePairs(PATH_KEY, next);
1802
+ syncPathChapter();
1803
+ selectRouteLeaf(paramLeaf);
1804
+ openSection(pathValuesFilled(template) ? "query" : "path");
1805
+ return;
1806
+ }
1807
+ const optionsLeaf = el.closest("button.leaf[data-route-options]");
1808
+ if (optionsLeaf) {
1809
+ e.preventDefault();
1810
+ rememberMethod(
1811
+ optionsLeaf.getAttribute("data-method") || "POST",
1812
+ optionsLeaf.getAttribute("data-path") || page.getAttribute("data-path") || location.pathname,
1813
+ );
1814
+ sessionStorage.removeItem(PATH_TEMPLATE_KEY);
1815
+ syncPathChapter();
1816
+ selectRouteLeaf(optionsLeaf);
1817
+ openSection(methodAllowsBody(optionsLeaf.getAttribute("data-method")) ? "body" : "query");
1818
+ return;
1819
+ }
1820
+ const routeLeaf = el.closest("a.leaf[data-route-leaf][href]");
1821
+ if (routeLeaf) {
1822
+ e.preventDefault();
1823
+ rememberMethod(
1824
+ routeLeaf.getAttribute("data-method") || "GET",
1825
+ routeLeaf.getAttribute("data-path") || "",
1826
+ );
1827
+ sessionStorage.removeItem(PATH_TEMPLATE_KEY);
1828
+ syncPathChapter();
1829
+ selectRouteLeaf(routeLeaf);
1830
+ openSection(methodAllowsBody(routeLeaf.getAttribute("data-method")) ? "body" : "query");
1831
+ return;
1832
+ }
1833
+ const a = el.closest("a[data-nav][href]");
1834
+ if (!a || !a.getAttribute("href")) return;
1835
+ if (a.target === "_blank") return;
1836
+ e.preventDefault();
1837
+ const href = a.getAttribute("href") || "";
1838
+ let url = href;
1839
+ try {
1840
+ const u = new URL(href, location.origin);
1841
+ url = buildQueryUrl(u.pathname);
1842
+ if (href.includes("raw=1") || u.searchParams.has("raw")) {
1843
+ const params = new URLSearchParams(url.includes("?") ? url.slice(url.indexOf("?") + 1) : "");
1844
+ params.set("raw", "1");
1845
+ url = u.pathname + "?" + params.toString();
1846
+ }
1847
+ } catch {}
1848
+ void navigate(url, requestHeaders());
745
1849
  });
1850
+
1851
+ if (btn && payload) {
1852
+ btn.addEventListener("click", async () => {
1853
+ try { await navigator.clipboard.writeText(payload.value); } catch { payload.select(); document.execCommand("copy"); }
1854
+ btn.setAttribute("aria-label", "Copied");
1855
+ btn.title = "Copied";
1856
+ if (copyIcon) copyIcon.hidden = true;
1857
+ if (doneIcon) doneIcon.hidden = false;
1858
+ setTimeout(() => {
1859
+ btn.setAttribute("aria-label", "Copy code");
1860
+ btn.title = "Copy code";
1861
+ if (copyIcon) copyIcon.hidden = false;
1862
+ if (doneIcon) doneIcon.hidden = true;
1863
+ }, 1600);
1864
+ });
1865
+ }
746
1866
  })();
747
1867
  </script>
748
1868
  </body>
@@ -759,6 +1879,7 @@ pre {
759
1879
  * @param nav - Route groups for the right-rail tree
760
1880
  * @param latencyMs - Handler elapsed time
761
1881
  * @param cache - Wide-event cache dimension
1882
+ * @param auth - Auth principal that handled the request
762
1883
  */
763
1884
  export async function asBrowserJsonCodeBlock(
764
1885
  request: Request,
@@ -767,6 +1888,7 @@ export async function asBrowserJsonCodeBlock(
767
1888
  nav: readonly JsonCodeNavGroup[] = [],
768
1889
  latencyMs?: number,
769
1890
  cache: JsonCodeCache = "none",
1891
+ auth: JsonCodeAuth = { kind: "none" },
770
1892
  ): Promise<Response> {
771
1893
  if (!shouldRenderJsonCodeBlock(request, response)) return response;
772
1894
  const json = await response.text();
@@ -777,13 +1899,13 @@ export async function asBrowserJsonCodeBlock(
777
1899
  method: request.method.toUpperCase(),
778
1900
  path: url.pathname,
779
1901
  app,
780
- consoleUrl: consoleUrlFromRequest(request),
781
1902
  rawHref: rawHrefFromRequest(request),
782
1903
  prettyHref: prettyHrefFromRequest(request),
783
1904
  compact: url.searchParams.has("raw"),
784
1905
  nav,
785
1906
  latencyMs,
786
1907
  cache,
1908
+ auth,
787
1909
  });
788
1910
  const headers = new Headers(response.headers);
789
1911
  headers.set("content-type", "text/html; charset=utf-8");
@@ -826,22 +1948,116 @@ function cacheHtml(cache: JsonCodeCache | undefined): string {
826
1948
  return `<span class="count cache cache-${cache ?? "none"}" data-slot="json-code-cache" data-cache="${cache ?? "none"}" title="${mark.title}">${mark.icon}${escapeHtml(mark.text)}</span>`;
827
1949
  }
828
1950
 
1951
+ const AUTH_MARK: Readonly<
1952
+ Record<JsonCodeAuthKind, { readonly title: string; readonly text: string; readonly icon: string }>
1953
+ > = {
1954
+ none: {
1955
+ title: "Unauthenticated",
1956
+ text: "None",
1957
+ icon: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle cx="12" cy="8" r="3.5" stroke="currentColor" stroke-width="1.5"/><path d="M5 19.5c1.8-3.2 4-4.8 7-4.8s5.2 1.6 7 4.8M4 4l16 16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>`,
1958
+ },
1959
+ public: {
1960
+ title: "Public — intentionally unauthenticated",
1961
+ text: "Public",
1962
+ icon: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle cx="12" cy="12" r="8" stroke="currentColor" stroke-width="1.5"/><path d="M4 12h16M12 4c2.5 2.8 2.5 13.2 0 16M12 4c-2.5 2.8-2.5 13.2 0 16" stroke="currentColor" stroke-width="1.5"/></svg>`,
1963
+ },
1964
+ user: {
1965
+ title: "Authenticated user",
1966
+ text: "User",
1967
+ icon: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle cx="12" cy="8" r="3.5" stroke="currentColor" stroke-width="1.5"/><path d="M5 19.5c1.8-3.2 4-4.8 7-4.8s5.2 1.6 7 4.8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>`,
1968
+ },
1969
+ key: {
1970
+ title: "API key",
1971
+ text: "Key",
1972
+ icon: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle cx="8" cy="14" r="3.5" stroke="currentColor" stroke-width="1.5"/><path d="M11 12.5 19 4.5M16.5 4.5h3v3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
1973
+ },
1974
+ operator: {
1975
+ title: "Operator",
1976
+ text: "Operator",
1977
+ icon: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M12 3.5 19 7v5c0 4.5-2.9 7.8-7 9-4.1-1.2-7-4.5-7-9V7l7-3.5Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>`,
1978
+ },
1979
+ };
1980
+
1981
+ function authHtml(auth: JsonCodeAuth | undefined): string {
1982
+ const value = auth ?? { kind: "none" as const };
1983
+ const mark = AUTH_MARK[value.kind];
1984
+ const label =
1985
+ value.kind === "user" || value.kind === "key" || value.kind === "operator"
1986
+ ? value.id?.trim() || mark.text
1987
+ : mark.text;
1988
+ const title =
1989
+ value.id && (value.kind === "user" || value.kind === "key" || value.kind === "operator")
1990
+ ? `${mark.title} · ${value.id}`
1991
+ : mark.title;
1992
+ 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
+ }
1994
+
829
1995
  function navHtml(nav: readonly JsonCodeNavGroup[] | undefined): string {
830
- if (!nav || nav.length === 0) return "";
831
- const groups = nav.map(navGroupHtml).join("");
1996
+ const groups = (nav ?? []).map(navGroupHtml).join("");
1997
+ 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
+ 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
+ const kvSection = (kind: string, label: string) =>
2000
+ `<details class="rail-acc" data-rail-section="${kind}">
2001
+ <summary class="rail-acc-sum">${chev}<span>${label}</span></summary>
2002
+ <div class="rail-acc-body">
2003
+ <div class="kv-editor">
2004
+ <div class="kv-rows" data-slot="json-code-kv-rows" data-kv="${kind}"></div>
2005
+ </div>
2006
+ </div>
2007
+ </details>`;
832
2008
  return `<input class="rail-check" type="checkbox" id="json-code-rail">
833
- <aside class="rail" data-slot="json-code-nav-panel" aria-label="Routes">
834
- <header class="strip rail-head">
835
- <p class="head">Routes</p>
836
- <label class="copy" for="json-code-rail" aria-label="Collapse routes" title="Collapse routes">
2009
+ <aside class="rail" data-slot="json-code-nav-panel" aria-label="Request">
2010
+ <header class="strip rail-toolbar">
2011
+ <p class="head">Request</p>
2012
+ <label class="copy" for="json-code-rail" aria-label="Collapse request" title="Collapse request">
837
2013
  <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>
838
2014
  </label>
839
2015
  </header>
840
- <ul class="rail-list">${groups}</ul>
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
+ <div class="rail-dock" data-slot="json-code-request-dock">
2021
+ <div class="strip rail-dock-strip">
2022
+ <span class="head">${play}<span>Request</span></span>
2023
+ <span class="grow"></span>
2024
+ <button type="button" class="token" data-slot="json-code-reset" title="Reset options to defaults">Reset</button>
2025
+ <span class="sep" aria-hidden="true"></span>
2026
+ <button type="button" class="dock-send" data-slot="json-code-send" title="Send request">${play}Send</button>
2027
+ </div>
2028
+ <div class="rail-dock-body">
2029
+ ${kvSection("query", "Params")}
2030
+ <details class="rail-acc" data-rail-section="body">
2031
+ <summary class="rail-acc-sum">
2032
+ ${chev}<span>Body</span>
2033
+ <span class="grow"></span>
2034
+ <span class="body-mode-strip" role="group" aria-label="Body format">
2035
+ <button type="button" class="token is-on" data-slot="json-code-body-mode" data-mode="form" aria-pressed="true">Form</button>
2036
+ <button type="button" class="token" data-slot="json-code-body-mode" data-mode="json" aria-pressed="false">JSON</button>
2037
+ </span>
2038
+ </summary>
2039
+ <div class="rail-acc-body">
2040
+ <div class="kv-editor" data-slot="json-code-body-form">
2041
+ <div class="kv-rows" data-slot="json-code-kv-rows" data-kv="body"></div>
2042
+ </div>
2043
+ <div class="body-json-wrap" data-slot="json-code-body-json" hidden>
2044
+ <div class="body-json-editor" data-slot="json-code-body-editor">
2045
+ <pre class="body-json-hi" data-slot="json-code-body-hi" aria-hidden="true"></pre>
2046
+ <textarea class="body-json" data-slot="json-code-body-raw" spellcheck="false" autocomplete="off" aria-label="JSON body" placeholder='{"key":"value"}'></textarea>
2047
+ </div>
2048
+ <p class="body-json-error" data-slot="json-code-body-error" hidden role="alert"></p>
2049
+ </div>
2050
+ </div>
2051
+ </details>
2052
+ ${kvSection("cookies", "Cookies")}
2053
+ ${kvSection("headers", "Headers")}
2054
+ ${kvSection("path", "Path")}
2055
+ </div>
2056
+ </div>
841
2057
  </aside>
842
- <label class="rail-thin" for="json-code-rail" aria-label="Expand routes" title="Expand routes">
2058
+ <label class="rail-thin" for="json-code-rail" aria-label="Expand request" title="Expand request">
843
2059
  <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>
844
- <span>Routes</span>
2060
+ <span>Request</span>
845
2061
  </label>`;
846
2062
  }
847
2063
 
@@ -861,11 +2077,15 @@ function navGroupHtml(group: JsonCodeNavGroup): string {
861
2077
 
862
2078
  function navRouteHtml(route: JsonCodeNavRoute): string {
863
2079
  const on = route.current ? " is-on" : "";
2080
+ const meta = `data-route-leaf data-method="${escapeHtml(route.method)}" data-path="${escapeHtml(route.path)}"`;
864
2081
  const inner = `<span class="leaf-path">${escapeHtml(route.path)}</span><span class="leaf-type">${escapeHtml(route.method)}</span>`;
865
2082
  if (route.href) {
866
- return `<li><a class="leaf${on}" href="${escapeHtml(route.href)}">${inner}</a></li>`;
2083
+ return `<li><a class="leaf${on}" ${meta} href="${escapeHtml(route.href)}">${inner}</a></li>`;
2084
+ }
2085
+ if (route.paramNames.length > 0) {
2086
+ return `<li><button type="button" class="leaf${on}" ${meta} data-param-template="${escapeHtml(route.path)}">${inner}</button></li>`;
867
2087
  }
868
- return `<li><span class="leaf${on}">${inner}</span></li>`;
2088
+ return `<li><button type="button" class="leaf${on}" ${meta} data-route-options="1">${inner}</button></li>`;
869
2089
  }
870
2090
 
871
2091
  function isJsonIdent(ch: string | undefined): boolean {