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.
- package/AGENTS.md +18 -10
- package/README.md +2 -2
- package/package.json +51 -50
- package/site/content/docs/ai/llms-txt.mdx +1 -1
- package/site/content/docs/ai/meta.json +1 -1
- package/site/content/docs/ai/skills.mdx +4 -1
- package/site/content/docs/ai/try-it.mdx +58 -0
- package/site/content/docs/client/auth.mdx +208 -0
- package/site/content/docs/client/calling.mdx +451 -0
- package/site/content/docs/client/index.mdx +209 -0
- package/site/content/docs/client/live.mdx +234 -0
- package/site/content/docs/client/meta.json +5 -0
- package/site/content/docs/client/react.mdx +249 -0
- package/site/content/docs/elements/ai/agents.mdx +232 -0
- package/site/content/docs/elements/ai/index.mdx +341 -0
- package/site/content/docs/elements/ai/mcp.mdx +275 -0
- package/site/content/docs/elements/ai/meta.json +5 -0
- package/site/content/docs/elements/ai/models.mdx +283 -0
- package/site/content/docs/elements/ai/prompts.mdx +256 -0
- package/site/content/docs/elements/channel/email.mdx +266 -0
- package/site/content/docs/elements/channel/index.mdx +344 -0
- package/site/content/docs/elements/channel/meta.json +5 -0
- package/site/content/docs/elements/channel/push.mdx +218 -0
- package/site/content/docs/elements/channel/receipts.mdx +206 -0
- package/site/content/docs/elements/channel/sms.mdx +264 -0
- package/site/content/docs/elements/channel/whatsapp.mdx +220 -0
- package/site/content/docs/elements/clock/index.mdx +300 -0
- package/site/content/docs/elements/clock/meta.json +5 -0
- package/site/content/docs/elements/clock/schedules.mdx +508 -0
- package/site/content/docs/elements/clock/sleep.mdx +445 -0
- package/site/content/docs/elements/flow/consumers.mdx +707 -0
- package/site/content/docs/elements/flow/http.mdx +1080 -0
- package/site/content/docs/elements/flow/index.mdx +618 -0
- package/site/content/docs/elements/flow/meta.json +5 -0
- package/site/content/docs/elements/flow/routing.mdx +559 -0
- package/site/content/docs/elements/flow/workflows.mdx +640 -0
- package/site/content/docs/elements/gate/auth.mdx +371 -0
- package/site/content/docs/elements/gate/authorization.mdx +298 -0
- package/site/content/docs/elements/gate/index.mdx +418 -0
- package/site/content/docs/elements/gate/meta.json +5 -0
- package/site/content/docs/elements/gate/rate-limits.mdx +365 -0
- package/site/content/docs/elements/gate/rls.mdx +303 -0
- package/site/content/docs/elements/gate/tenancy.mdx +372 -0
- package/site/content/docs/elements/index.mdx +20 -10
- package/site/content/docs/elements/signal/broadcast.mdx +517 -0
- package/site/content/docs/elements/signal/index.mdx +352 -0
- package/site/content/docs/elements/signal/live.mdx +590 -0
- package/site/content/docs/elements/signal/meta.json +5 -0
- package/site/content/docs/elements/signal/once.mdx +596 -0
- package/site/content/docs/elements/store/files.mdx +659 -0
- package/site/content/docs/elements/store/index.mdx +310 -0
- package/site/content/docs/elements/store/kv.mdx +629 -0
- package/site/content/docs/elements/store/meta.json +5 -0
- package/site/content/docs/elements/store/search.mdx +946 -0
- package/site/content/docs/elements/store/sql.mdx +937 -0
- package/site/content/docs/elements/vault/config.mdx +256 -0
- package/site/content/docs/elements/vault/index.mdx +356 -0
- package/site/content/docs/elements/vault/meta.json +5 -0
- package/site/content/docs/elements/vault/rotation.mdx +273 -0
- package/site/content/docs/elements/vault/secrets.mdx +285 -0
- package/site/content/docs/index.mdx +63 -36
- package/site/content/docs/meta.json +8 -7
- package/site/content/docs/plugins/cors.mdx +2 -1
- package/site/content/docs/plugins/csrf.mdx +4 -1
- package/site/content/docs/plugins/headers.mdx +2 -1
- package/site/content/docs/plugins/ip-allowlist.mdx +2 -1
- package/site/content/docs/plugins/magic-link.mdx +6 -0
- package/site/content/docs/plugins/maintenance-mode.mdx +2 -1
- package/site/content/docs/plugins/oauth.mdx +1 -1
- package/site/content/docs/plugins/otp.mdx +12 -1
- package/site/content/docs/plugins/passkey.mdx +39 -15
- package/site/content/docs/plugins/two-factor.mdx +91 -34
- package/site/content/docs/plugins/username.mdx +1 -1
- package/site/content/docs/recipes/caddy.mdx +3 -3
- package/site/content/docs/recipes/index.mdx +18 -28
- package/site/content/docs/recipes/mailpit.mdx +1 -1
- package/site/content/docs/recipes/meilisearch.mdx +3 -3
- package/site/content/docs/recipes/meta.json +1 -4
- package/site/content/docs/recipes/nginx.mdx +2 -2
- package/site/content/docs/recipes/openrouter.mdx +283 -0
- package/site/content/docs/recipes/pgdog.mdx +3 -3
- package/site/content/docs/recipes/postgres.mdx +1 -2
- package/site/content/docs/recipes/rustfs.mdx +2 -2
- package/site/content/docs/recipes/traefik.mdx +3 -3
- package/site/content/docs/reference/cli.mdx +231 -0
- package/site/content/docs/reference/configuration.mdx +30 -10
- package/site/content/docs/reference/environment-variables.mdx +93 -20
- package/site/content/docs/reference/errors.mdx +132 -29
- package/site/content/docs/reference/fx.mdx +137 -73
- package/site/content/docs/reference/index.mdx +13 -21
- package/site/content/docs/reference/meta.json +4 -5
- package/site/content/docs/reference/okid.mdx +42 -17
- package/site/content/docs/reference/plugins.mdx +4 -3
- package/site/content/docs/reference/security.mdx +197 -0
- package/site/content/docs/understand/meta.json +5 -0
- package/site/content/docs/understand/the-anatomy.mdx +132 -0
- package/site/content/docs/understand/the-model.mdx +32 -0
- package/site/content/docs/understand/the-problem.mdx +74 -0
- package/site/content/docs/understand/the-vocabulary.mdx +26 -0
- package/src/auth/api-keys.ts +2 -1
- package/src/auth/bindings.ts +64 -16
- package/src/auth/gate-auth.test.ts +6 -1
- package/src/auth/identity.ts +152 -5
- package/src/auth/index.ts +29 -0
- package/src/auth/invites.ts +2 -1
- package/src/auth/method-context.ts +41 -0
- package/src/auth/oauth-as/crypto.ts +2 -1
- package/src/auth/oauth-as/stores.ts +2 -1
- package/src/auth/operator.ts +2 -1
- package/src/auth/sessions-jwt.test.ts +77 -0
- package/src/auth/sessions.ts +20 -4
- package/src/auth/tenants.ts +3 -2
- package/src/auth/two-factor-challenge.test.ts +61 -0
- package/src/auth/two-factor-challenge.ts +284 -0
- package/src/auth/verification.ts +5 -0
- package/src/bench/REPORT.md +49 -0
- package/src/bench/g03-signal-once.bench.ts +1 -1
- package/src/bench/g10-observability-contention.bench.ts +1 -1
- package/src/bench/g17-hybrid-search.bench.ts +572 -0
- package/src/bench/load-app.ts +2 -10
- package/src/cli/ai-setup/ai-setup.test.ts +331 -42
- package/src/cli/ai-setup/apply.ts +294 -52
- package/src/cli/ai-setup/catalog.ts +238 -1342
- package/src/cli/ai-setup/index.ts +14 -99
- package/src/cli/ai-setup/prompts.ts +60 -443
- package/src/cli/ask-seed.test.ts +96 -0
- package/src/cli/ask-seed.ts +82 -0
- package/src/cli/ask-vault-gaps.test.ts +84 -0
- package/src/cli/ask-vault-gaps.ts +146 -0
- package/src/cli/build.ts +2 -2
- package/src/cli/client-add.test.ts +26 -1
- package/src/cli/client-add.ts +106 -27
- package/src/cli/db-seed.ts +2 -0
- package/src/cli/db.ts +150 -11
- package/src/cli/dev.ts +119 -211
- package/src/cli/docker-clean.ts +2 -2
- package/src/cli/docker-cli.test.ts +1 -1
- package/src/cli/doctor-diff.ts +4 -2
- package/src/cli/doctor-pii.test.ts +1 -1
- package/src/cli/doctor.ts +14 -1
- package/src/cli/hero-meta.test.ts +4 -2
- package/src/cli/load-config.images.test.ts +8 -10
- package/src/cli/load-config.ts +1 -1
- package/src/cli/registry.ts +22 -10
- package/src/cli/replay.ts +3 -1
- package/src/cli/start.ts +1 -1
- package/src/cli/tui/keys.ts +2 -3
- package/src/client/auth/cookies.ts +74 -0
- package/src/client/auth/create-auth-client.ts +636 -0
- package/src/client/auth/denials.ts +99 -0
- package/src/client/auth/session.ts +283 -0
- package/src/client/auth.test.ts +251 -0
- package/src/client/auth.ts +39 -114
- package/src/client/create-with-session.ts +245 -0
- package/src/client/create.ts +51 -5
- package/src/client/index.ts +11 -1
- package/src/client/live.ts +11 -79
- package/src/client/notes-contract.test.ts +5 -10
- package/src/client/sse.ts +137 -0
- package/src/client/stream.ts +147 -0
- package/src/client/transport.test.ts +32 -0
- package/src/client/transport.ts +123 -26
- package/src/client/types.ts +118 -14
- package/src/client-react/index.ts +185 -24
- package/src/client-react/use-live-query.ts +13 -3
- package/src/compiler/aot.test.ts +7 -4
- package/src/compiler/effects-embed.test.ts +56 -0
- package/src/compiler/effects-fetch.test.ts +43 -0
- package/src/compiler/effects-infer.ts +58 -2
- package/src/compiler/extract.test.ts +257 -43
- package/src/compiler/extract.ts +566 -48
- package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +7 -4
- package/src/compiler/fixtures/skyport/src/flows/bookings/signals.ts +2 -8
- package/src/compiler/fixtures/skyport.expected.json +4 -4
- package/src/compiler/fixtures/triggers/five-triggers.ts +14 -10
- package/src/compiler/response.ts +2 -2
- package/src/compiler/search-writer-isolation.test.ts +40 -0
- package/src/config/index.ts +11 -0
- package/src/console/server/app.ts +17 -23
- package/src/console/server/bind.ts +4 -0
- package/src/console/server/console.test.ts +2 -2
- package/src/console/server/flows-invoke.test.ts +50 -34
- package/src/console/server/flows.ts +436 -255
- package/src/console/server/runs-ingest.test.ts +6 -3
- package/src/console/server/serve.ts +1 -1
- package/src/console/server/signals.test.ts +1 -5
- package/src/console/server/signals.ts +8 -6
- package/src/console/server/state.ts +6 -1
- package/src/console/server/store.ts +1 -1
- package/src/console/ui-next/dist/assets/FileExportIcon-Ck-5od4R.js +1 -0
- package/src/console/ui-next/dist/assets/MoreHorizontalCircle01Icon-gMNGsE37.js +1 -0
- package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +1 -0
- package/src/console/ui-next/dist/assets/UnavailableIcon-D9cvHVPr.js +1 -0
- package/src/console/ui-next/dist/assets/UserIcon-DaE2PB5_.js +1 -0
- package/src/console/ui-next/dist/assets/access-page-DFeymU07.js +4 -0
- package/src/console/ui-next/dist/assets/agent-disclosure-U1rdfblp.js +1 -0
- package/src/console/ui-next/dist/assets/cache-glyph-BeFJeqBG.js +1 -0
- package/src/console/ui-next/dist/assets/call-pii-button-CVAONPii.js +1 -0
- package/src/console/ui-next/dist/assets/collapsible-D2A6NJ-3.js +1 -0
- package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +1 -0
- package/src/console/ui-next/dist/assets/dagre.esm-B1_XeuLP.js +1 -0
- package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +1 -0
- package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +1 -0
- package/src/console/ui-next/dist/assets/duration-tone-Cgk_h5ja.js +9 -0
- package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +1 -0
- package/src/console/ui-next/dist/assets/flows-page-Dss7941e.js +1 -0
- package/src/console/ui-next/dist/assets/highlighted-json-MYZQtRnw.js +154 -0
- package/src/console/ui-next/dist/assets/http-method-Jrh39p7A.js +1 -0
- package/src/console/ui-next/dist/assets/index-D0zS5rKO.css +2 -0
- package/src/console/ui-next/dist/assets/index-DXP2dBIF.js +63 -0
- package/src/console/ui-next/dist/assets/observability-page-HvolXxTI.js +4 -0
- package/src/console/ui-next/dist/assets/react-dom-Ddte4I-Q.js +9 -0
- package/src/console/ui-next/dist/assets/replica-lag-CSh2dzrb.js +18 -0
- package/src/console/ui-next/dist/assets/request-meta-BatF8KrK.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +1 -0
- package/src/console/ui-next/dist/assets/{sql-BskegiFM.js → sql-BsFa4tDR.js} +1 -1
- package/src/console/ui-next/dist/assets/store-page-eiKiHnNe.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-CZkMeKS-.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CW8y5A2h.js +55 -0
- package/src/console/ui-next/dist/assets/units-page-B_RWJrEO.js +1 -0
- package/src/console/ui-next/dist/assets/{use-vault-list-uk4WVboC.js → use-vault-list-CT4-gajj.js} +1 -1
- package/src/console/ui-next/dist/assets/vault-page-CWrg-A68.js +2 -0
- package/src/console/ui-next/dist/assets/xyflow-CSyC6ryz.css +1 -0
- package/src/console/ui-next/dist/assets/xyflow-yApv7D4e.js +7 -0
- package/src/console/ui-next/dist/index.html +6 -13
- package/src/console/ui-next/seed-invoke-host.ts +21 -18
- package/src/console/ui-next/src/client.ts +7 -2
- package/src/console/ui-next/src/features/flows/graph/element-map.ts +2 -0
- package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +12 -2
- package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +4 -0
- package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +8 -0
- package/src/console/ui-next/src/features/flows/traces/waterfall-bars.ts +4 -0
- package/src/console/ui-next/src/features/flows/traces/waterfall-tooltip.ts +9 -3
- package/src/console/ui-next/src/features/store/detail/reveal-cell.tsx +7 -11
- package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +2 -2
- package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +2 -0
- package/src/console/ui-next/src/features/store/lib/grid-model.test.ts +25 -1
- package/src/console/ui-next/src/features/store/lib/grid-model.ts +36 -1
- package/src/console/ui-next/src/features/store/query/query-results.tsx +3 -3
- package/src/console/ui-next/src/features/units/detail/effects-summary.tsx +2 -0
- package/src/console/ui-next/src/features/units/lib/call-read-safe.ts +3 -1
- package/src/console/ui-next/src/features/units/lib/fields-from-schema.ts +6 -0
- package/src/docker/ai-model-status.test.ts +1 -12
- package/src/docker/ai-model-status.ts +10 -38
- package/src/docker/compose-up.test.ts +100 -0
- package/src/docker/compose-up.ts +292 -0
- package/src/docker/compose.ts +4 -14
- package/src/docker/derive.ts +9 -34
- package/src/docker/docker.test.ts +16 -261
- package/src/docker/images-config.test.ts +13 -25
- package/src/docker/index.ts +1 -24
- package/src/docker/recipes/index.ts +0 -23
- package/src/docker/recipes/pgdog.ts +6 -4
- package/src/docker/stack-id.test.ts +2 -2
- package/src/docker/stack-id.ts +0 -1
- package/src/docker/types.ts +2 -2
- package/src/drivers/ai-anthropic.ts +10 -0
- package/src/drivers/ai-openai-compatible.ts +34 -7
- package/src/drivers/ai-providers.test.ts +0 -107
- package/src/drivers/ai-stream.test.ts +0 -34
- package/src/drivers/ai-types.ts +17 -8
- package/src/drivers/channel-fcm.ts +6 -1
- package/src/drivers/channel-msegat.ts +6 -1
- package/src/drivers/channel-resend.ts +5 -1
- package/src/drivers/channel-smtp.ts +5 -1
- package/src/drivers/channel-sndr.ts +10 -1
- package/src/drivers/channel-taqnyat-mail.ts +5 -1
- package/src/drivers/channel-taqnyat-whatsapp.ts +6 -1
- package/src/drivers/channel-taqnyat.ts +6 -1
- package/src/drivers/channel-types.ts +10 -0
- package/src/drivers/channel-unifonic.ts +6 -1
- package/src/drivers/channel-wa-cloud.ts +6 -1
- package/src/drivers/channel-webpush.ts +6 -1
- package/src/drivers/external.ts +33 -0
- package/src/drivers/index.ts +0 -10
- package/src/drivers/meilisearch.ts +6 -0
- package/src/drivers/oauth-types.ts +4 -0
- package/src/drivers/postgres.test.ts +74 -3
- package/src/drivers/postgres.ts +175 -6
- package/src/drivers/signal-types.ts +5 -5
- package/src/drivers/types.ts +7 -0
- package/src/drivers/vault-types.ts +5 -0
- package/src/elements/ai/declare.ts +17 -4
- package/src/elements/ai/eval.ts +1 -1
- package/src/elements/ai/mcp-http.ts +1 -1
- package/src/elements/ai/pii.ts +1 -1
- package/src/elements/ai/providers.test.ts +289 -0
- package/src/elements/ai/providers.ts +169 -0
- package/src/elements/ai/runtime.ts +68 -5
- package/src/elements/ai/schema.ts +2 -2
- package/src/elements/ai.test.ts +7 -7
- package/src/elements/ai.ts +17 -1
- package/src/elements/channel/runtime.ts +105 -12
- package/src/elements/clock/cron-fields.test.ts +144 -0
- package/src/elements/clock/cron-fields.ts +185 -0
- package/src/elements/clock/declare.ts +247 -3
- package/src/elements/clock.ts +21 -1
- package/src/elements/index.ts +15 -0
- package/src/elements/signal/chaos-child.ts +1 -2
- package/src/elements/signal/declare.ts +65 -37
- package/src/elements/signal/delivery-modes.test.ts +11 -31
- package/src/elements/signal/dry-run-replay.test.ts +1 -7
- package/src/elements/signal/dry-run-write-isolation.test.ts +1 -7
- package/src/elements/signal/key-ordering.test.ts +6 -26
- package/src/elements/signal/lease-reclaim.test.ts +2 -6
- package/src/elements/signal/optional-emit.test.ts +6 -9
- package/src/elements/signal/order-lifecycle.test.ts +4 -17
- package/src/elements/signal/orphan-messages.test.ts +4 -15
- package/src/elements/signal/reconcile.test.ts +2 -2
- package/src/elements/signal/runtime.ts +1 -1
- package/src/elements/signal/schema-emit.test.ts +4 -8
- package/src/elements/signal.test.ts +24 -49
- package/src/elements/signal.ts +9 -2
- package/src/elements/store/domain-ddl.test.ts +1 -1
- package/src/elements/store/live-http.test.ts +9 -4
- package/src/elements/store/prepare-row.test.ts +123 -0
- package/src/elements/store/resource.ts +43 -12
- package/src/elements/store/schema-decl.ts +117 -0
- package/src/elements/store/search-backfill.ts +205 -0
- package/src/elements/store/search-bind.ts +66 -0
- package/src/elements/store/search-bm25.ts +78 -0
- package/src/elements/store/search-ddl.ts +156 -0
- package/src/elements/store/search-embed-flow.ts +141 -0
- package/src/elements/store/search-errors.ts +34 -0
- package/src/elements/store/search-fusion.ts +112 -0
- package/src/elements/store/search-lsh.ts +179 -0
- package/src/elements/store/search-runtime.ts +300 -0
- package/src/elements/store/search.test.ts +144 -0
- package/src/elements/store/sql-session.test.ts +1 -0
- package/src/elements/store/sql-session.ts +83 -3
- package/src/elements/store/table.ts +23 -2
- package/src/elements/store/upsert-app.test.ts +1 -3
- package/src/elements/store.ts +50 -0
- package/src/full.ts +4 -2
- package/src/http.ts +4 -1
- package/src/i18n/catalogs/ar.ts +14 -6
- package/src/i18n/catalogs/en.ts +14 -6
- package/src/index.ts +6 -1
- package/src/kernel/adopt-barrel-fresh.test.ts +7 -7
- package/src/kernel/adopt-routes.ts +9 -1
- package/src/kernel/app-auth.ts +5 -0
- package/src/kernel/app.ts +102 -9
- package/src/kernel/auto-cache.test.ts +6 -12
- package/src/kernel/auto-registry.test.ts +3 -3
- package/src/kernel/boot-bind/ai.test.ts +22 -19
- package/src/kernel/boot-bind/ai.ts +5 -43
- package/src/kernel/boot-bind/clock.ts +23 -5
- package/src/kernel/boot-bind/honor-config.test.ts +4 -4
- package/src/kernel/boot.test.ts +11 -6
- package/src/kernel/boot.ts +66 -16
- package/src/kernel/boundary-contract.ts +93 -0
- package/src/kernel/budget.test.ts +1 -1
- package/src/kernel/call.ts +89 -0
- package/src/kernel/capability.ts +6 -0
- package/src/kernel/client-descriptor.ts +123 -0
- package/src/kernel/clock-timezone.test.ts +80 -0
- package/src/kernel/correlation.test.ts +1 -1
- package/src/kernel/dry-run.ts +11 -8
- package/src/kernel/effects-stamping.test.ts +42 -9
- package/src/kernel/effects.test.ts +4 -2
- package/src/kernel/effects.ts +51 -9
- package/src/kernel/errors-channel.ts +17 -0
- package/src/kernel/errors-live-resume.ts +3 -2
- package/src/kernel/errors-tenant.ts +7 -4
- package/src/kernel/errors.registry-helpers.ts +96 -0
- package/src/kernel/errors.registry.test.ts +139 -25
- package/src/kernel/errors.ts +102 -23
- package/src/kernel/external-effects.test.ts +196 -0
- package/src/kernel/flow.test.ts +13 -4
- package/src/kernel/flow.ts +69 -71
- package/src/kernel/fx-ask-telemetry.test.ts +2 -2
- package/src/kernel/fx-dead-letters.test.ts +3 -3
- package/src/kernel/fx-fetch.ts +55 -0
- package/src/kernel/fx-live-stream.ts +2 -2
- package/src/kernel/fx-live.test.ts +8 -8
- package/src/kernel/fx.test.ts +17 -11
- package/src/kernel/fx.ts +180 -57
- package/src/kernel/horizontal-child.ts +1 -1
- package/src/kernel/http-query.test.ts +3 -6
- package/src/kernel/index.ts +13 -3
- package/src/kernel/instance-id.ts +1 -1
- package/src/kernel/instances.test.ts +40 -0
- package/src/kernel/instances.ts +47 -9
- package/src/kernel/live-http.test.ts +6 -7
- package/src/kernel/live-http.ts +6 -2
- package/src/kernel/live-resume.test.ts +1 -1
- package/src/kernel/mcp-tool.test.ts +3 -3
- package/src/kernel/on.ts +98 -4
- package/src/kernel/pipeline.test.ts +12 -15
- package/src/kernel/plugin-elements.test.ts +1 -1
- package/src/kernel/stamp-http.test.ts +3 -3
- package/src/kernel/triggers.ts +210 -95
- package/src/kernel-entry.ts +0 -1
- package/src/manifest/diff.ts +11 -1
- package/src/manifest/fixtures/skyport.excerpt.json +1 -1
- package/src/manifest/types.ts +46 -2
- package/src/mcp/docs-index.ts +3 -3
- package/src/mcp/docs-mcp.test.ts +2 -2
- package/src/mcp/docs-tools.ts +1 -1
- package/src/okid.test.ts +56 -0
- package/src/okid.ts +55 -11
- package/src/plugins/anonymous.ts +8 -4
- package/src/plugins/auth/shared.ts +27 -3
- package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
- package/src/plugins/auth-methods.security.test.ts +561 -23
- package/src/plugins/config-source.ts +1 -1
- package/src/plugins/csrf.test.ts +65 -0
- package/src/plugins/index.ts +2 -0
- package/src/plugins/magic-link.ts +36 -16
- package/src/plugins/oauth/shared.ts +3 -1
- package/src/plugins/oauth.ts +22 -14
- package/src/plugins/otp.ts +37 -21
- package/src/plugins/passkey-webauthn.ts +9 -6
- package/src/plugins/passkey.ts +98 -26
- package/src/plugins/pre-account-hijack.test.ts +223 -0
- package/src/plugins/two-factor.ts +466 -37
- package/src/plugins/username.ts +19 -7
- package/src/release/build-lib.ts +1 -0
- package/src/release/limits.ts +2 -2
- package/src/release/measure.ts +1 -1
- package/src/runs/collect.test.ts +0 -2
- package/src/runtime/json-code-block.test.ts +250 -20
- package/src/runtime/json-code-block.ts +1261 -41
- package/src/term.test.ts +1 -1
- package/src/test/create-test-app.test.ts +12 -12
- package/src/test/live-signals.test.ts +6 -6
- package/src/test/provisions.integration.test.ts +10 -14
- package/src/test/reset-element-registries.ts +1 -1
- package/src/test/tenant-isolation.test.ts +12 -6
- package/site/content/docs/deployment/docker-swarm.mdx +0 -164
- package/site/content/docs/deployment/docker.mdx +0 -227
- package/site/content/docs/deployment/index.mdx +0 -83
- package/site/content/docs/deployment/kubernetes.mdx +0 -176
- package/site/content/docs/deployment/meta.json +0 -5
- package/site/content/docs/deployment/reverse-proxy.mdx +0 -234
- package/site/content/docs/elements/ai.mdx +0 -385
- package/site/content/docs/elements/channel.mdx +0 -346
- package/site/content/docs/elements/clock.mdx +0 -244
- package/site/content/docs/elements/flow.mdx +0 -420
- package/site/content/docs/elements/gate.mdx +0 -436
- package/site/content/docs/elements/signal.mdx +0 -380
- package/site/content/docs/elements/store.mdx +0 -1099
- package/site/content/docs/elements/vault.mdx +0 -405
- package/site/content/docs/get-started/basic-usage.mdx +0 -173
- package/site/content/docs/get-started/index.mdx +0 -43
- package/site/content/docs/get-started/installation.mdx +0 -220
- package/site/content/docs/get-started/introduction.mdx +0 -144
- package/site/content/docs/get-started/meta.json +0 -13
- package/site/content/docs/get-started/project-structure.mdx +0 -925
- package/site/content/docs/get-started/testing.mdx +0 -328
- package/site/content/docs/get-started/why.mdx +0 -114
- package/site/content/docs/recipes/llama-cpp.mdx +0 -151
- package/site/content/docs/recipes/ollama.mdx +0 -142
- package/site/content/docs/recipes/sglang.mdx +0 -105
- package/site/content/docs/recipes/vllm.mdx +0 -106
- package/site/content/docs/reference/cli.md +0 -232
- package/site/content/docs/reference/client.mdx +0 -469
- package/site/content/docs/reference/security.md +0 -74
- package/src/cli/ai-setup/detect-ollama.ts +0 -213
- package/src/cli/ai-setup/recommend.test.ts +0 -225
- package/src/cli/ai-setup/recommend.ts +0 -225
- package/src/cli/dev-controls.test.ts +0 -122
- package/src/cli/dev-controls.ts +0 -164
- package/src/cli/tui/DevLive.tsx +0 -126
- package/src/console/ui-next/dist/assets/access-page-3-EFj-2G.js +0 -4
- package/src/console/ui-next/dist/assets/agent-disclosure-BP0Y0Sux.js +0 -1
- package/src/console/ui-next/dist/assets/cache-glyph-B5X-NM0-.js +0 -1
- package/src/console/ui-next/dist/assets/call-pii-button-ChAUiLo9.js +0 -1
- package/src/console/ui-next/dist/assets/collapsible-DGnOM2ph.js +0 -1
- package/src/console/ui-next/dist/assets/copy-inline-button-DBHhgHKP.js +0 -1
- package/src/console/ui-next/dist/assets/dagre.esm-ZwcdTuZZ.js +0 -1
- package/src/console/ui-next/dist/assets/detail-header-DhHM1iaZ.js +0 -1
- package/src/console/ui-next/dist/assets/dropdown-menu-_NjTEo5_.js +0 -1
- package/src/console/ui-next/dist/assets/duration-tone-sC3lGABz.js +0 -9
- package/src/console/ui-next/dist/assets/element-icons-BVXtRyd3.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-2uIhBu0_.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-cVFnA4HH.js +0 -1
- package/src/console/ui-next/dist/assets/highlighted-json-Awq7gYdu.js +0 -154
- package/src/console/ui-next/dist/assets/http-method-_UHM2ODJ.js +0 -1
- package/src/console/ui-next/dist/assets/index-CMIgUbD0.js +0 -66
- package/src/console/ui-next/dist/assets/index-Ck88Jmv8.css +0 -2
- package/src/console/ui-next/dist/assets/link-BX6Vqztd.js +0 -1
- package/src/console/ui-next/dist/assets/observability-page-CQ3p34ip.js +0 -4
- package/src/console/ui-next/dist/assets/preload-helper-oH4irX4C.js +0 -1
- package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +0 -1
- package/src/console/ui-next/dist/assets/react-dom-Bph1y7z7.js +0 -9
- package/src/console/ui-next/dist/assets/replica-lag-DKRrbvdo.js +0 -18
- package/src/console/ui-next/dist/assets/request-meta-DV0ywz7t.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-JQIZlWfm.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-DKxNTe_m.js +0 -1
- package/src/console/ui-next/dist/assets/skeleton-D-czQJT6.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-02xOiqIK.js +0 -41
- package/src/console/ui-next/dist/assets/trace-detail-sheet-B09O8rA6.js +0 -2
- package/src/console/ui-next/dist/assets/tree-expand-toggle-BtyhmWb4.js +0 -55
- package/src/console/ui-next/dist/assets/units-page-l8FeKfnP.js +0 -1
- package/src/console/ui-next/dist/assets/useMutation-B8EO02Ej.js +0 -1
- package/src/console/ui-next/dist/assets/useRender-BE2A9BWC.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-CL-d_mLE.js +0 -2
- package/src/console/ui-next/dist/assets/xyflow-D7n4g6go.js +0 -7
- package/src/console/ui-next/dist/assets/xyflow-DZ0Ws1xk.css +0 -1
- package/src/docker/ollama-pull.ts +0 -232
- package/src/docker/recipes/llama-cpp.ts +0 -298
- package/src/docker/recipes/ollama.ts +0 -44
- package/src/docker/recipes/sglang.ts +0 -55
- package/src/docker/recipes/vllm.ts +0 -44
- package/src/drivers/ai-ollama-tools.integration.test.ts +0 -109
- package/src/drivers/ai-ollama.integration.test.ts +0 -184
- package/src/drivers/ai-ollama.ts +0 -389
- 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 —
|
|
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
|
|
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:
|
|
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-
|
|
576
|
-
|
|
577
|
-
|
|
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]) .
|
|
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
|
-
<
|
|
705
|
-
<a
|
|
706
|
-
|
|
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
|
|
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 (!
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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, "&")
|
|
1276
|
+
.replace(/</g, "<")
|
|
1277
|
+
.replace(/>/g, ">");
|
|
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, "&")
|
|
1376
|
+
.replace(/"/g, """)
|
|
1377
|
+
.replace(/</g, "<")
|
|
1378
|
+
.replace(/>/g, ">");
|
|
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, "&")
|
|
1585
|
+
.replace(/</g, "<")
|
|
1586
|
+
.replace(/>/g, ">");
|
|
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
|
-
|
|
831
|
-
const
|
|
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="
|
|
834
|
-
<header class="strip rail-
|
|
835
|
-
<p class="head">
|
|
836
|
-
<label class="copy" for="json-code-rail" aria-label="Collapse
|
|
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
|
-
<
|
|
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
|
|
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>
|
|
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><
|
|
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 {
|