okengine 0.21.1 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +11 -4
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +15 -9
- package/manifest.v1.schema.json +54 -0
- package/package.json +53 -38
- package/site/content/docs/ai/mcp.mdx +13 -9
- package/site/content/docs/ai/skills.mdx +2 -2
- package/site/content/docs/client/calling.mdx +27 -19
- package/site/content/docs/client/index.mdx +1 -1
- package/site/content/docs/elements/ai/agents.mdx +61 -13
- package/site/content/docs/elements/ai/decide.mdx +536 -0
- package/site/content/docs/elements/ai/events.mdx +373 -0
- package/site/content/docs/elements/ai/index.mdx +9 -3
- package/site/content/docs/elements/ai/meta.json +1 -1
- package/site/content/docs/elements/ai/prompts.mdx +4 -2
- package/site/content/docs/elements/channel/email.mdx +1 -1
- package/site/content/docs/elements/channel/index.mdx +1 -1
- package/site/content/docs/elements/flow/http.mdx +4 -4
- package/site/content/docs/elements/flow/index.mdx +3 -3
- package/site/content/docs/elements/flow/meta.json +1 -1
- package/site/content/docs/elements/gate/auth.mdx +4 -1
- package/site/content/docs/elements/gate/tenancy.mdx +1 -1
- package/site/content/docs/elements/store/index.mdx +1 -1
- package/site/content/docs/elements/store/kv.mdx +5 -5
- package/site/content/docs/elements/vault/rotation.mdx +15 -0
- package/site/content/docs/plugins/otp.mdx +31 -31
- package/site/content/docs/recipes/dragonfly.mdx +5 -5
- package/site/content/docs/recipes/index.mdx +6 -6
- package/site/content/docs/recipes/mailpit.mdx +1 -1
- package/site/content/docs/recipes/meilisearch.mdx +1 -1
- package/site/content/docs/recipes/pgdog.mdx +1 -1
- package/site/content/docs/recipes/postgres.mdx +12 -8
- package/site/content/docs/recipes/redis.mdx +4 -4
- package/site/content/docs/reference/cli.mdx +2 -1
- package/site/content/docs/reference/configuration.mdx +11 -9
- package/site/content/docs/reference/errors.mdx +12 -2
- package/site/content/docs/reference/fx.mdx +35 -21
- package/site/content/docs/reference/idempotency.mdx +192 -0
- package/site/content/docs/reference/index.mdx +6 -1
- package/site/content/docs/reference/meta.json +2 -1
- package/site/content/docs/understand/meta.json +1 -1
- package/site/content/docs/{elements/flow → understand}/routing.mdx +11 -10
- package/site/content/docs/understand/the-architecture.mdx +14 -12
- package/site/content/docs/understand/try-it.mdx +3 -3
- package/src/auth/api-key-sql.test.ts +44 -0
- package/src/auth/api-key-sql.ts +1 -0
- package/src/auth/api-keys.ts +30 -12
- package/src/auth/config.ts +6 -2
- package/src/auth/gate-auth.test.ts +20 -0
- package/src/cli/agents-md.test.ts +25 -0
- package/src/cli/ask-vault-gaps.test.ts +38 -1
- package/src/cli/ask-vault-gaps.ts +35 -2
- package/src/cli/competitor-mention-removal.test.ts +3 -3
- package/src/cli/decide.ts +174 -0
- package/src/cli/decision-lock-watch.test.ts +45 -0
- package/src/cli/decision-lock-watch.ts +51 -0
- package/src/cli/dev-app-runner.ts +34 -8
- package/src/cli/dev-auth-secret.test.ts +49 -0
- package/src/cli/dev-auth-secret.ts +49 -0
- package/src/cli/dev-hot.test.ts +41 -0
- package/src/cli/dev-hot.ts +48 -0
- package/src/cli/dev.test.ts +1 -1
- package/src/cli/dev.ts +24 -6
- package/src/cli/docker-cli.test.ts +1 -1
- package/src/cli/eval.ts +142 -1
- package/src/cli/index.ts +14 -1
- package/src/cli/load-config.images.test.ts +12 -12
- package/src/cli/load-config.ts +2 -2
- package/src/cli/mcp-from-console.ts +48 -0
- package/src/cli/registry.ts +35 -1
- package/src/cli/vault-cmd.test.ts +23 -0
- package/src/cli/vault-cmd.ts +7 -0
- package/src/client/agent.ts +307 -0
- package/src/client/create.ts +18 -1
- package/src/client/transport.test.ts +245 -0
- package/src/client/transport.ts +133 -19
- package/src/client/types.ts +29 -3
- package/src/client-react/index.ts +7 -1
- package/src/client-react/use-agent-run.test.ts +271 -0
- package/src/client-react/use-agent-run.ts +193 -0
- package/src/compiler/ai-approval.test.ts +36 -0
- package/src/compiler/ai-repair.test.ts +22 -0
- package/src/compiler/decisions.extract.test.ts +164 -0
- package/src/compiler/effects-infer.ts +753 -17
- package/src/compiler/extract.test.ts +37 -0
- package/src/compiler/extract.ts +381 -24
- package/src/compiler/fixtures/skyport.expected.json +24 -0
- package/src/compiler/fx-follow.test.ts +164 -0
- package/src/compiler/fx-index.ts +399 -0
- package/src/compiler/response.ts +35 -2
- package/src/config/index.ts +21 -3
- package/src/console/server/ai-runs-flows.ts +269 -0
- package/src/console/server/ai-runs.test.ts +323 -0
- package/src/console/server/ai-runs.ts +562 -0
- package/src/console/server/ai.ts +12 -1
- package/src/console/server/app.ts +10 -1
- package/src/console/server/decisions-flows.ts +157 -0
- package/src/console/server/decisions.test.ts +66 -0
- package/src/console/server/decisions.ts +229 -0
- package/src/console/server/flows-invoke.test.ts +2 -2
- package/src/console/server/flows.ts +50 -2
- package/src/console/server/invoke-user-flow.ts +9 -0
- package/src/console/server/runs-ingest.ts +16 -0
- package/src/console/server/serve.ts +5 -0
- package/src/console/server/state.ts +25 -1
- package/src/console/server/store-stats.test.ts +47 -0
- package/src/console/server/store-stats.ts +92 -18
- package/src/console/ui-next/dist/assets/access-page-DDKkhT9v.js +4 -0
- package/src/console/ui-next/dist/assets/agent-disclosure-62Xts8Xi.js +1 -0
- package/src/console/ui-next/dist/assets/{cache-glyph-92uM5MO7.js → cache-glyph-DyeoKHKA.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-DtGVPtcs.js → call-pii-button-DvfpLXsU.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-DN7l6zmC.js → collapsible-BY03SeCg.js} +1 -1
- package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
- package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
- package/src/console/ui-next/dist/assets/decisions-page-CgkKlA56.js +1 -0
- package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
- package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
- package/src/console/ui-next/dist/assets/duration-tone-Y6HLjyJ5.js +9 -0
- package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
- package/src/console/ui-next/dist/assets/flows-page-DLPfNy-_.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-BlAEVgNW.js → highlighted-json-D7nNzpgB.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-BDf7OAHv.js → http-method-DdL19zzh.js} +1 -1
- package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
- package/src/console/ui-next/dist/assets/index-C0lc_s9d.js +58 -0
- package/src/console/ui-next/dist/assets/observability-page-CRlcyLCC.js +8 -0
- package/src/console/ui-next/dist/assets/{replica-lag-B3GLNVfF.js → replica-lag-Bb3FWUu9.js} +7 -7
- package/src/console/ui-next/dist/assets/request-meta-DYBMWhX8.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-CcE-SXC8.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-CKMSEQ4s.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-QOfk0M0H.js +55 -0
- package/src/console/ui-next/dist/assets/units-page-DKC1CDDd.js +1 -0
- package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
- package/src/console/ui-next/dist/assets/vault-page-B54v4Yit.js +2 -0
- package/src/console/ui-next/dist/index.html +4 -3
- package/src/console/ui-next/seed-parked-approval.ts +126 -0
- package/src/console/ui-next/src/client.ts +224 -1
- package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
- package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
- package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
- package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
- package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
- package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
- package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
- package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
- package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
- package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
- package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
- package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
- package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
- package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
- package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
- package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
- package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
- package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
- package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
- package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
- package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
- package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
- package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
- package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
- package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
- package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
- package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
- package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
- package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +15 -2
- package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
- package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
- package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
- package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
- package/src/console/ui-next/src/router.tsx +17 -0
- package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
- package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
- package/src/console/ui-next/vite.config.ts +9 -0
- package/src/docker/docker.test.ts +13 -13
- package/src/docker/images-config.test.ts +12 -12
- package/src/docker/recipes/dragonfly.ts +3 -2
- package/src/docker/recipes/redis.ts +1 -1
- package/src/docker/stack-id.test.ts +1 -1
- package/src/drivers/ai-anthropic.ts +212 -21
- package/src/drivers/ai-mock.ts +34 -1
- package/src/drivers/ai-openai-compatible.ts +67 -26
- package/src/drivers/ai-preconnect.ts +18 -0
- package/src/drivers/ai-stream.test.ts +62 -0
- package/src/drivers/ai-types.ts +20 -0
- package/src/drivers/journal-postgres.ts +455 -3
- package/src/drivers/meilisearch.ts +2 -0
- package/src/drivers/memory-ddl.test.ts +18 -0
- package/src/drivers/memory.ts +21 -1
- package/src/drivers/pg-rls.test.ts +31 -0
- package/src/drivers/pg-rls.ts +15 -0
- package/src/drivers/postgres.test.ts +9 -0
- package/src/drivers/postgres.ts +49 -11
- package/src/drivers/vault-builtin.test.ts +10 -0
- package/src/drivers/vault-builtin.ts +13 -1
- package/src/drivers/vault-remote-bag.ts +5 -1
- package/src/drivers/vault-types.ts +6 -1
- package/src/elements/ai/agent-event-slot.ts +27 -0
- package/src/elements/ai/approval-http.ts +207 -0
- package/src/elements/ai/approval.test.ts +741 -0
- package/src/elements/ai/approval.ts +228 -0
- package/src/elements/ai/decisions/bind.ts +163 -0
- package/src/elements/ai/decisions/certificate.test.ts +291 -0
- package/src/elements/ai/decisions/certificate.ts +377 -0
- package/src/elements/ai/decisions/certify.ts +314 -0
- package/src/elements/ai/decisions/decide.test.ts +953 -0
- package/src/elements/ai/decisions/e2e.test.ts +399 -0
- package/src/elements/ai/decisions/export.test.ts +120 -0
- package/src/elements/ai/decisions/export.ts +146 -0
- package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
- package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
- package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
- package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
- package/src/elements/ai/decisions/http.test.ts +154 -0
- package/src/elements/ai/decisions/http.ts +240 -0
- package/src/elements/ai/decisions/labels.test.ts +282 -0
- package/src/elements/ai/decisions/labels.ts +257 -0
- package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
- package/src/elements/ai/decisions/openrouter.ts +36 -0
- package/src/elements/ai/decisions/provider.ts +102 -0
- package/src/elements/ai/decisions/typesafe.ts +36 -0
- package/src/elements/ai/declare.ts +252 -1
- package/src/elements/ai/events.test.ts +379 -0
- package/src/elements/ai/events.ts +138 -0
- package/src/elements/ai/run-events.test.ts +461 -0
- package/src/elements/ai/run-events.ts +522 -0
- package/src/elements/ai/runtime.ts +1186 -105
- package/src/elements/ai/stream-turn.ts +214 -0
- package/src/elements/ai/stream.live.test.ts +76 -0
- package/src/elements/ai/subagent.test.ts +136 -0
- package/src/elements/ai.test.ts +497 -3
- package/src/elements/ai.ts +16 -1
- package/src/elements/clock/durable.ts +27 -1
- package/src/elements/store/emit-drizzle.ts +2 -2
- package/src/elements/store/live-default.test.ts +2 -0
- package/src/elements/store/live-http.test.ts +1 -0
- package/src/elements/store/resource-list-docs.test.ts +2 -2
- package/src/elements/store/resource.test.ts +3 -3
- package/src/elements/store/schema-decl.test.ts +1 -0
- package/src/elements/store/sql-condition.ts +24 -7
- package/src/elements/store/sql-session.test.ts +51 -0
- package/src/elements/store/sql-session.ts +111 -1
- package/src/elements/store/upsert-app.test.ts +1 -1
- package/src/elements/vault/audit.test.ts +137 -0
- package/src/elements/vault/audit.ts +106 -13
- package/src/elements/vault/boot-chain.ts +9 -1
- package/src/elements/vault/builtin-adapter.ts +33 -3
- package/src/i18n/catalogs/ar.ts +9 -0
- package/src/i18n/catalogs/en.ts +9 -0
- package/src/index.ts +2 -0
- package/src/kernel/abort-scope.ts +33 -2
- package/src/kernel/agent-event-store.ts +448 -0
- package/src/kernel/app.ts +294 -19
- package/src/kernel/auto-cache.test.ts +6 -6
- package/src/kernel/boot-bind/ai.ts +3 -0
- package/src/kernel/boot-bind/store.test.ts +26 -0
- package/src/kernel/boot-bind/store.ts +82 -6
- package/src/kernel/boot-bind/vault.ts +1 -0
- package/src/kernel/boot.ts +34 -5
- package/src/kernel/builtin-errors.ts +10 -0
- package/src/kernel/capability.ts +3 -0
- package/src/kernel/client-descriptor.ts +1 -0
- package/src/kernel/concurrency.test.ts +33 -0
- package/src/kernel/decision-label-store.ts +441 -0
- package/src/kernel/effects-stamping.test.ts +2 -2
- package/src/kernel/effects.test.ts +2 -1
- package/src/kernel/effects.ts +13 -1
- package/src/kernel/element-registries.ts +3 -0
- package/src/kernel/errors-compiler.ts +20 -0
- package/src/kernel/errors-text.ts +4 -0
- package/src/kernel/errors.ts +2 -0
- package/src/kernel/external-effects.test.ts +36 -0
- package/src/kernel/flow.test.ts +2 -3
- package/src/kernel/flow.ts +24 -0
- package/src/kernel/fx-decide.ts +761 -0
- package/src/kernel/fx-fetch.ts +5 -1
- package/src/kernel/fx.test.ts +12 -5
- package/src/kernel/fx.ts +316 -29
- package/src/kernel/http-frame.test.ts +94 -0
- package/src/kernel/http-frame.ts +203 -0
- package/src/kernel/idempotency-store.ts +578 -0
- package/src/kernel/idempotency.test.ts +556 -0
- package/src/kernel/idempotency.ts +304 -0
- package/src/kernel/index.ts +1 -0
- package/src/kernel/journal.ts +160 -0
- package/src/kernel/json-result.ts +10 -0
- package/src/kernel/pipeline.test.ts +45 -0
- package/src/kernel/sse-id.ts +32 -0
- package/src/manifest/diff.ts +1 -0
- package/src/manifest/types.ts +37 -2
- package/src/mcp/ai-tools.test.ts +202 -0
- package/src/mcp/authorization.ts +66 -0
- package/src/mcp/session.ts +3 -0
- package/src/mcp/tools.ts +168 -0
- package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
- package/src/plugins/otp.test.ts +65 -1
- package/src/plugins/otp.ts +59 -7
- package/src/release/absolute-regression.test.ts +63 -0
- package/src/release/http-graph.test.ts +27 -0
- package/src/release/http-graph.ts +95 -0
- package/src/release/index.ts +3 -0
- package/src/release/limits.ts +17 -2
- package/src/release/measure.ts +144 -28
- package/src/runs/collect.ts +4 -1
- package/src/runs/parquet.test.ts +27 -1
- package/src/runs/parquet.ts +19 -0
- package/src/runs/types.ts +31 -0
- package/src/runtime/json-code-block.test.ts +106 -47
- package/src/runtime/json-code-block.ts +1209 -46
- package/src/term.test.ts +13 -0
- package/src/term.ts +81 -49
- package/src/test/create-test-app.test.ts +3 -1
- package/src/test/create-test-app.ts +40 -0
- package/src/test/live-signals.test.ts +1 -0
- package/src/test/provisions.integration.test.ts +1 -0
- package/src/test/tenant-isolation.test.ts +1 -0
- package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
- package/src/console/ui-next/dist/assets/access-page-Bgt9bq2r.js +0 -4
- package/src/console/ui-next/dist/assets/agent-disclosure-B43CXZZR.js +0 -1
- package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +0 -1
- package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +0 -1
- package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +0 -1
- package/src/console/ui-next/dist/assets/duration-tone-BmIR9FV8.js +0 -9
- package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-BMHs-IzK.js +0 -1
- package/src/console/ui-next/dist/assets/index-BKpaes3n.js +0 -63
- package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
- package/src/console/ui-next/dist/assets/observability-page-WnVLI-0j.js +0 -4
- package/src/console/ui-next/dist/assets/request-meta-DMbnAe3f.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-KvFDingJ.js +0 -41
- package/src/console/ui-next/dist/assets/trace-detail-sheet-Htk8Cm9t.js +0 -2
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CFMPWX4f.js +0 -55
- package/src/console/ui-next/dist/assets/units-page-C4NdNuxP.js +0 -1
- package/src/console/ui-next/dist/assets/use-vault-list-CT4-gajj.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-CBYl0LW_.js +0 -2
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client facts derived from a stored HTTP frame.
|
|
3
|
+
*
|
|
4
|
+
* Port of the sacp user-agent classifier. Nothing is stored: the trace sheet
|
|
5
|
+
* reads the headers it already has. `unknown`, `other`, and `none` stay off
|
|
6
|
+
* the card.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Device class from the user-agent and client hints. */
|
|
10
|
+
export type DeviceType = "unknown" | "desktop" | "mobile" | "tablet" | "bot";
|
|
11
|
+
|
|
12
|
+
/** Operating system family. */
|
|
13
|
+
export type Platform =
|
|
14
|
+
| "windows"
|
|
15
|
+
| "macos"
|
|
16
|
+
| "linux"
|
|
17
|
+
| "ios"
|
|
18
|
+
| "ipados"
|
|
19
|
+
| "android"
|
|
20
|
+
| "chromeos"
|
|
21
|
+
| "freebsd"
|
|
22
|
+
| "openbsd"
|
|
23
|
+
| "other";
|
|
24
|
+
|
|
25
|
+
/** Browser family. `none` is a non-browser client. */
|
|
26
|
+
export type Browser =
|
|
27
|
+
| "chrome"
|
|
28
|
+
| "firefox"
|
|
29
|
+
| "safari"
|
|
30
|
+
| "edge"
|
|
31
|
+
| "arc"
|
|
32
|
+
| "opera"
|
|
33
|
+
| "brave"
|
|
34
|
+
| "vivaldi"
|
|
35
|
+
| "other"
|
|
36
|
+
| "none";
|
|
37
|
+
|
|
38
|
+
/** Who sent the request. */
|
|
39
|
+
export type ClientType = "browser" | "api_client" | "cli" | "bot" | "other";
|
|
40
|
+
|
|
41
|
+
/** Named HTTP client when the user-agent is not a browser. */
|
|
42
|
+
export type ApiClient =
|
|
43
|
+
| "postmanruntime"
|
|
44
|
+
| "newman"
|
|
45
|
+
| "insomnia"
|
|
46
|
+
| "hoppscotch"
|
|
47
|
+
| "curl"
|
|
48
|
+
| "httpie"
|
|
49
|
+
| "wget"
|
|
50
|
+
| "python-requests"
|
|
51
|
+
| "okhttp"
|
|
52
|
+
| "axios"
|
|
53
|
+
| "node-fetch"
|
|
54
|
+
| "undici"
|
|
55
|
+
| "other"
|
|
56
|
+
| "none";
|
|
57
|
+
|
|
58
|
+
/** Facts the Request card can draw. Hidden kinds are `null`. */
|
|
59
|
+
export type RequestClient = {
|
|
60
|
+
/** Device, or `null` when the guess is `unknown`. */
|
|
61
|
+
readonly deviceType: DeviceType | null;
|
|
62
|
+
/** Platform, or `null` when the guess is `other`. */
|
|
63
|
+
readonly platform: Platform | null;
|
|
64
|
+
/** Browser, or `null` when the guess is `other` or `none`. */
|
|
65
|
+
readonly browser: Browser | null;
|
|
66
|
+
/** Major, or major.minor when the minor is not zero. */
|
|
67
|
+
readonly browserVersion: string | null;
|
|
68
|
+
/** Client kind, or `null` when the guess is `other`. */
|
|
69
|
+
readonly clientType: ClientType | null;
|
|
70
|
+
/** API or CLI client, or `null` when the guess is `other` or `none`. */
|
|
71
|
+
readonly apiClient: ApiClient | null;
|
|
72
|
+
/** Version token for {@link apiClient}. */
|
|
73
|
+
readonly apiClientVersion: string | null;
|
|
74
|
+
/** Call API invoke stamped `x-oke-client: console`. */
|
|
75
|
+
readonly console: boolean;
|
|
76
|
+
/** Client IP from forwarding headers, when one is stored. */
|
|
77
|
+
readonly ip: string | null;
|
|
78
|
+
/** Raw `user-agent`, when the header is present. */
|
|
79
|
+
readonly userAgent: string | null;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
type Detection = {
|
|
83
|
+
deviceType: DeviceType;
|
|
84
|
+
platform: Platform;
|
|
85
|
+
browser: Browser;
|
|
86
|
+
clientType: ClientType;
|
|
87
|
+
apiClient: ApiClient;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const PLATFORM_HINT: Record<string, Platform> = {
|
|
91
|
+
windows: "windows",
|
|
92
|
+
macos: "macos",
|
|
93
|
+
"mac os": "macos",
|
|
94
|
+
linux: "linux",
|
|
95
|
+
android: "android",
|
|
96
|
+
ios: "ios",
|
|
97
|
+
ipados: "ipados",
|
|
98
|
+
"chrome os": "chromeos",
|
|
99
|
+
chromeos: "chromeos",
|
|
100
|
+
cros: "chromeos",
|
|
101
|
+
freebsd: "freebsd",
|
|
102
|
+
openbsd: "openbsd",
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Classify the request headers already stored on a run.
|
|
107
|
+
*
|
|
108
|
+
* @param headers - Request header map. Names may be any case.
|
|
109
|
+
*/
|
|
110
|
+
export function requestClientFromHeaders(headers: Readonly<Record<string, string>>): RequestClient {
|
|
111
|
+
const userAgent = headerValue(headers, "user-agent")?.trim() ?? "";
|
|
112
|
+
const detected = detectFromUserAgent(userAgent);
|
|
113
|
+
const hinted = applyClientHints(detected, userAgent.toLowerCase(), headers);
|
|
114
|
+
const version = productVersion(userAgent, hinted.browser, hinted.apiClient);
|
|
115
|
+
const ip = clientIp(headers);
|
|
116
|
+
return {
|
|
117
|
+
deviceType: hinted.deviceType === "unknown" ? null : hinted.deviceType,
|
|
118
|
+
platform: hinted.platform === "other" ? null : hinted.platform,
|
|
119
|
+
browser: hinted.browser === "other" || hinted.browser === "none" ? null : hinted.browser,
|
|
120
|
+
browserVersion: hinted.browser === "other" || hinted.browser === "none" ? null : version,
|
|
121
|
+
clientType: hinted.clientType === "other" ? null : hinted.clientType,
|
|
122
|
+
apiClient:
|
|
123
|
+
hinted.apiClient === "other" || hinted.apiClient === "none" ? null : hinted.apiClient,
|
|
124
|
+
apiClientVersion: hinted.apiClient === "other" || hinted.apiClient === "none" ? null : version,
|
|
125
|
+
console: headerValue(headers, "x-oke-client")?.trim().toLowerCase() === "console",
|
|
126
|
+
ip,
|
|
127
|
+
userAgent: userAgent.length > 0 ? userAgent : null,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Best-effort user-agent classification.
|
|
133
|
+
*
|
|
134
|
+
* Non-browser clients match first so Postman and curl are not read as Chrome.
|
|
135
|
+
* Arc and Brave often still look like Chrome from the user-agent alone.
|
|
136
|
+
*
|
|
137
|
+
* @param userAgent - Raw user-agent, possibly empty
|
|
138
|
+
*/
|
|
139
|
+
export function detectFromUserAgent(userAgent?: string): Detection {
|
|
140
|
+
const ua = (userAgent ?? "").trim();
|
|
141
|
+
const ual = ua.toLowerCase();
|
|
142
|
+
|
|
143
|
+
let clientType: ClientType = "other";
|
|
144
|
+
let apiClient: ApiClient = "none";
|
|
145
|
+
let browser: Browser = "other";
|
|
146
|
+
const platform: Platform = "other";
|
|
147
|
+
let deviceType: DeviceType = "unknown";
|
|
148
|
+
|
|
149
|
+
if (/\bpostmanruntime\/\d+/.test(ual)) {
|
|
150
|
+
return finalize({
|
|
151
|
+
ua: ual,
|
|
152
|
+
clientType: "api_client",
|
|
153
|
+
apiClient: "postmanruntime",
|
|
154
|
+
browser: "none",
|
|
155
|
+
platform,
|
|
156
|
+
deviceType,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
if (/\bnewman\/\d+/.test(ual)) {
|
|
160
|
+
return finalize({
|
|
161
|
+
ua: ual,
|
|
162
|
+
clientType: "cli",
|
|
163
|
+
apiClient: "newman",
|
|
164
|
+
browser: "none",
|
|
165
|
+
platform,
|
|
166
|
+
deviceType,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
if (/\binsomnia\/\d+/.test(ual)) {
|
|
170
|
+
return finalize({
|
|
171
|
+
ua: ual,
|
|
172
|
+
clientType: "api_client",
|
|
173
|
+
apiClient: "insomnia",
|
|
174
|
+
browser: "none",
|
|
175
|
+
platform,
|
|
176
|
+
deviceType,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
if (/\bhoppscotch\b/.test(ual)) {
|
|
180
|
+
return finalize({
|
|
181
|
+
ua: ual,
|
|
182
|
+
clientType: "api_client",
|
|
183
|
+
apiClient: "hoppscotch",
|
|
184
|
+
browser: "none",
|
|
185
|
+
platform,
|
|
186
|
+
deviceType,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
if (/\bcurl\/\d+/.test(ual)) {
|
|
190
|
+
return finalize({
|
|
191
|
+
ua: ual,
|
|
192
|
+
clientType: "cli",
|
|
193
|
+
apiClient: "curl",
|
|
194
|
+
browser: "none",
|
|
195
|
+
platform,
|
|
196
|
+
deviceType,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
if (/\bhttpie\/\d+/.test(ual)) {
|
|
200
|
+
return finalize({
|
|
201
|
+
ua: ual,
|
|
202
|
+
clientType: "cli",
|
|
203
|
+
apiClient: "httpie",
|
|
204
|
+
browser: "none",
|
|
205
|
+
platform,
|
|
206
|
+
deviceType,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
if (/\bwget\/\d+/.test(ual)) {
|
|
210
|
+
return finalize({
|
|
211
|
+
ua: ual,
|
|
212
|
+
clientType: "cli",
|
|
213
|
+
apiClient: "wget",
|
|
214
|
+
browser: "none",
|
|
215
|
+
platform,
|
|
216
|
+
deviceType,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
if (/\bpython-requests\/\d+/.test(ual)) {
|
|
220
|
+
return finalize({
|
|
221
|
+
ua: ual,
|
|
222
|
+
clientType: "api_client",
|
|
223
|
+
apiClient: "python-requests",
|
|
224
|
+
browser: "none",
|
|
225
|
+
platform,
|
|
226
|
+
deviceType,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
if (/\bokhttp\/\d+/.test(ual)) {
|
|
230
|
+
return finalize({
|
|
231
|
+
ua: ual,
|
|
232
|
+
clientType: "api_client",
|
|
233
|
+
apiClient: "okhttp",
|
|
234
|
+
browser: "none",
|
|
235
|
+
platform,
|
|
236
|
+
deviceType,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
if (/\baxios\/\d+/.test(ual)) {
|
|
240
|
+
return finalize({
|
|
241
|
+
ua: ual,
|
|
242
|
+
clientType: "api_client",
|
|
243
|
+
apiClient: "axios",
|
|
244
|
+
browser: "none",
|
|
245
|
+
platform,
|
|
246
|
+
deviceType,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
if (/\bnode-fetch\/\d+/.test(ual)) {
|
|
250
|
+
return finalize({
|
|
251
|
+
ua: ual,
|
|
252
|
+
clientType: "api_client",
|
|
253
|
+
apiClient: "node-fetch",
|
|
254
|
+
browser: "none",
|
|
255
|
+
platform,
|
|
256
|
+
deviceType,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
if (/\bundici\b/.test(ual)) {
|
|
260
|
+
return finalize({
|
|
261
|
+
ua: ual,
|
|
262
|
+
clientType: "api_client",
|
|
263
|
+
apiClient: "undici",
|
|
264
|
+
browser: "none",
|
|
265
|
+
platform,
|
|
266
|
+
deviceType,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (/\bbot\b|\bcrawler\b|\bspider\b/.test(ual)) {
|
|
271
|
+
return finalize({
|
|
272
|
+
ua: ual,
|
|
273
|
+
clientType: "bot",
|
|
274
|
+
apiClient: "none",
|
|
275
|
+
browser: "none",
|
|
276
|
+
platform,
|
|
277
|
+
deviceType: "bot",
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
clientType = "browser";
|
|
282
|
+
apiClient = "none";
|
|
283
|
+
if (/\bedg\//.test(ual)) browser = "edge";
|
|
284
|
+
else if (/\barc\//.test(ual)) browser = "arc";
|
|
285
|
+
else if (/\bvivaldi\//.test(ual)) browser = "vivaldi";
|
|
286
|
+
else if (/\bopr\//.test(ual) || /\bopera\b/.test(ual)) browser = "opera";
|
|
287
|
+
else if (/\bfirefox\//.test(ual)) browser = "firefox";
|
|
288
|
+
else if (/\bchrome\//.test(ual) || /\bcrios\//.test(ual)) browser = "chrome";
|
|
289
|
+
else if (/\bsafari\//.test(ual)) browser = "safari";
|
|
290
|
+
else if (/\bbrave\//.test(ual)) browser = "brave";
|
|
291
|
+
else browser = "other";
|
|
292
|
+
|
|
293
|
+
return finalize({ ua: ual, clientType, apiClient, browser, platform, deviceType });
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function finalize(input: {
|
|
297
|
+
ua: string;
|
|
298
|
+
clientType: ClientType;
|
|
299
|
+
apiClient: ApiClient;
|
|
300
|
+
browser: Browser;
|
|
301
|
+
platform: Platform;
|
|
302
|
+
deviceType: DeviceType;
|
|
303
|
+
}): Detection {
|
|
304
|
+
let platform = input.platform;
|
|
305
|
+
let deviceType = input.deviceType;
|
|
306
|
+
const ual = input.ua;
|
|
307
|
+
|
|
308
|
+
if (/\bwindows nt\b/.test(ual)) platform = "windows";
|
|
309
|
+
else if (/\bipad\b/.test(ual)) platform = "ipados";
|
|
310
|
+
else if (/\biphone\b|\bipod\b|\bcpu iphone os\b/.test(ual)) platform = "ios";
|
|
311
|
+
else if (/\bandroid\b/.test(ual)) platform = "android";
|
|
312
|
+
else if (/\bcros\b|\bchromeos\b/.test(ual)) platform = "chromeos";
|
|
313
|
+
else if (/\bmac os x\b/.test(ual)) platform = "macos";
|
|
314
|
+
else if (/\bfreebsd\b/.test(ual)) platform = "freebsd";
|
|
315
|
+
else if (/\bopenbsd\b/.test(ual)) platform = "openbsd";
|
|
316
|
+
else if (/\blinux\b/.test(ual)) platform = "linux";
|
|
317
|
+
|
|
318
|
+
deviceType = deviceFromPlatform(platform, ual, deviceType);
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
clientType: input.clientType,
|
|
322
|
+
apiClient: input.apiClient,
|
|
323
|
+
browser: input.browser,
|
|
324
|
+
platform,
|
|
325
|
+
deviceType,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function deviceFromPlatform(platform: Platform, ua: string, current: DeviceType): DeviceType {
|
|
330
|
+
if (current === "bot") return "bot";
|
|
331
|
+
if (platform === "ipados" || /\btablet\b/.test(ua)) return "tablet";
|
|
332
|
+
if (platform === "ios" || platform === "android" || /\bmobile\b/.test(ua)) return "mobile";
|
|
333
|
+
if (
|
|
334
|
+
platform === "windows" ||
|
|
335
|
+
platform === "macos" ||
|
|
336
|
+
platform === "linux" ||
|
|
337
|
+
platform === "chromeos" ||
|
|
338
|
+
platform === "freebsd" ||
|
|
339
|
+
platform === "openbsd"
|
|
340
|
+
) {
|
|
341
|
+
return "desktop";
|
|
342
|
+
}
|
|
343
|
+
return current === "unknown" ? "unknown" : current;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function applyClientHints(
|
|
347
|
+
detected: Detection,
|
|
348
|
+
ua: string,
|
|
349
|
+
headers: Readonly<Record<string, string>>,
|
|
350
|
+
): Detection {
|
|
351
|
+
const platformHint = unquote(headerValue(headers, "sec-ch-ua-platform"));
|
|
352
|
+
const platform = platformHint
|
|
353
|
+
? (PLATFORM_HINT[platformHint.toLowerCase()] ?? detected.platform)
|
|
354
|
+
: detected.platform;
|
|
355
|
+
let deviceType =
|
|
356
|
+
platform === detected.platform
|
|
357
|
+
? detected.deviceType
|
|
358
|
+
: deviceFromPlatform(platform, ua, detected.deviceType);
|
|
359
|
+
const mobile = unquote(headerValue(headers, "sec-ch-ua-mobile"));
|
|
360
|
+
if (deviceType !== "bot" && mobile === "?1") deviceType = "mobile";
|
|
361
|
+
if (deviceType !== "bot" && mobile === "?0") {
|
|
362
|
+
deviceType = platform === "ipados" || /\btablet\b/.test(ua) ? "tablet" : "desktop";
|
|
363
|
+
}
|
|
364
|
+
return { ...detected, platform, deviceType };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function clientIp(headers: Readonly<Record<string, string>>): string | null {
|
|
368
|
+
const cf = headerValue(headers, "cf-connecting-ip")?.trim();
|
|
369
|
+
if (cf) return cf;
|
|
370
|
+
const forwarded = headerValue(headers, "x-forwarded-for")?.split(",")[0]?.trim();
|
|
371
|
+
if (forwarded) return forwarded;
|
|
372
|
+
const real = headerValue(headers, "x-real-ip")?.trim();
|
|
373
|
+
return real && real.length > 0 ? real : null;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function productVersion(userAgent: string, browser: Browser, apiClient: ApiClient): string | null {
|
|
377
|
+
const token =
|
|
378
|
+
browser === "safari"
|
|
379
|
+
? matchVersion(userAgent, /version\/(\d+(?:\.\d+)*)/i)
|
|
380
|
+
: browser === "edge"
|
|
381
|
+
? matchVersion(userAgent, /\bedg\/(\d+(?:\.\d+)*)/i)
|
|
382
|
+
: browser === "opera"
|
|
383
|
+
? matchVersion(userAgent, /\b(?:opr|opera)\/(\d+(?:\.\d+)*)/i)
|
|
384
|
+
: browser === "chrome"
|
|
385
|
+
? matchVersion(userAgent, /\b(?:chrome|crios)\/(\d+(?:\.\d+)*)/i)
|
|
386
|
+
: browser !== "other" && browser !== "none"
|
|
387
|
+
? matchVersion(userAgent, new RegExp(`\\b${browser}\\/(\\d+(?:\\.\\d+)*)`, "i"))
|
|
388
|
+
: apiClient !== "other" && apiClient !== "none"
|
|
389
|
+
? matchVersion(userAgent, new RegExp(`\\b${apiClient}\\/(\\d+(?:\\.\\d+)*)`, "i"))
|
|
390
|
+
: null;
|
|
391
|
+
return token ? displayVersion(token) : null;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function matchVersion(userAgent: string, pattern: RegExp): string | null {
|
|
395
|
+
return pattern.exec(userAgent)?.[1] ?? null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** Major only when the minor is zero (`131.0.0.0` → `131`, `8.7.1` → `8.7`). */
|
|
399
|
+
function displayVersion(raw: string): string {
|
|
400
|
+
const [major, minor] = raw.split(".");
|
|
401
|
+
if (!major) return raw;
|
|
402
|
+
if (!minor || minor === "0") return major;
|
|
403
|
+
return `${major}.${minor}`;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function headerValue(headers: Readonly<Record<string, string>>, name: string): string | undefined {
|
|
407
|
+
const direct = headers[name];
|
|
408
|
+
if (direct !== undefined) return direct;
|
|
409
|
+
const lower = name.toLowerCase();
|
|
410
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
411
|
+
if (key.toLowerCase() === lower) return value;
|
|
412
|
+
}
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function unquote(value: string | undefined): string | null {
|
|
417
|
+
if (!value) return null;
|
|
418
|
+
const trimmed = value.trim();
|
|
419
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length >= 2) {
|
|
420
|
+
return trimmed.slice(1, -1);
|
|
421
|
+
}
|
|
422
|
+
return trimmed;
|
|
423
|
+
}
|
|
@@ -8,8 +8,26 @@ import {
|
|
|
8
8
|
inputByteLabel,
|
|
9
9
|
inputFieldRows,
|
|
10
10
|
inputShapeHint,
|
|
11
|
+
payloadHasContent,
|
|
11
12
|
} from "./request-input-view.ts";
|
|
12
13
|
|
|
14
|
+
describe("payloadHasContent", () => {
|
|
15
|
+
test("treats an empty object, array, or missing value as no body", () => {
|
|
16
|
+
expect(payloadHasContent({})).toBe(false);
|
|
17
|
+
expect(payloadHasContent([])).toBe(false);
|
|
18
|
+
expect(payloadHasContent("")).toBe(false);
|
|
19
|
+
expect(payloadHasContent(null)).toBe(false);
|
|
20
|
+
expect(payloadHasContent(undefined)).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("keeps a payload that has fields or a scalar", () => {
|
|
24
|
+
expect(payloadHasContent({ limit: 20 })).toBe(true);
|
|
25
|
+
expect(payloadHasContent([1])).toBe(true);
|
|
26
|
+
expect(payloadHasContent(0)).toBe(true);
|
|
27
|
+
expect(payloadHasContent(false)).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
13
31
|
describe("inputFieldRows", () => {
|
|
14
32
|
test("projects plain objects into typed rows", () => {
|
|
15
33
|
expect(
|
|
@@ -20,22 +38,50 @@ describe("inputFieldRows", () => {
|
|
|
20
38
|
meta: { cabin: "economy" },
|
|
21
39
|
}),
|
|
22
40
|
).toEqual([
|
|
23
|
-
{ key: "bookingId", value: "bk_8f2a", display: "bk_8f2a", kind: "string" },
|
|
24
|
-
{ key: "seats", value: 2, display: "2", kind: "number" },
|
|
25
|
-
{ key: "ok", value: true, display: "true", kind: "boolean" },
|
|
41
|
+
{ key: "bookingId", value: "bk_8f2a", display: "bk_8f2a", kind: "string", children: null },
|
|
42
|
+
{ key: "seats", value: 2, display: "2", kind: "number", children: null },
|
|
43
|
+
{ key: "ok", value: true, display: "true", kind: "boolean", children: null },
|
|
26
44
|
{
|
|
27
45
|
key: "meta",
|
|
28
46
|
value: { cabin: "economy" },
|
|
29
|
-
display:
|
|
47
|
+
display: "cabin: economy",
|
|
30
48
|
kind: "object",
|
|
49
|
+
children: [
|
|
50
|
+
{ key: "cabin", value: "economy", display: "economy", kind: "string", children: null },
|
|
51
|
+
],
|
|
31
52
|
},
|
|
32
53
|
]);
|
|
33
54
|
});
|
|
34
55
|
|
|
35
|
-
test("
|
|
36
|
-
expect(inputFieldRows(["
|
|
56
|
+
test("projects arrays as indexed rows and nests objects", () => {
|
|
57
|
+
expect(inputFieldRows([{ id: "ENG-12", tags: ["sso"] }])).toEqual([
|
|
58
|
+
{
|
|
59
|
+
key: "0",
|
|
60
|
+
value: { id: "ENG-12", tags: ["sso"] },
|
|
61
|
+
display: "id: ENG-12",
|
|
62
|
+
kind: "object",
|
|
63
|
+
children: [
|
|
64
|
+
{ key: "id", value: "ENG-12", display: "ENG-12", kind: "string", children: null },
|
|
65
|
+
{
|
|
66
|
+
key: "tags",
|
|
67
|
+
value: ["sso"],
|
|
68
|
+
display: "1 item",
|
|
69
|
+
kind: "array",
|
|
70
|
+
children: [{ key: "0", value: "sso", display: "sso", kind: "string", children: null }],
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("returns null for scalars, null, and empty arrays", () => {
|
|
37
78
|
expect(inputFieldRows("x")).toBeNull();
|
|
38
79
|
expect(inputFieldRows(null)).toBeNull();
|
|
80
|
+
expect(inputFieldRows([])).toBeNull();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("previews nested containers by count when they have no short scalars", () => {
|
|
84
|
+
expect(inputFieldRows({ blob: { nested: { a: 1 } } })?.[0]?.display).toBe("1 field");
|
|
39
85
|
});
|
|
40
86
|
});
|
|
41
87
|
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* Helpers for rendering run.input in the trace Request section.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
/** Scalar / container kind for a
|
|
5
|
+
/** Scalar / container kind for a payload field. */
|
|
6
6
|
export type InputFieldKind = "string" | "number" | "boolean" | "null" | "object" | "array";
|
|
7
7
|
|
|
8
|
-
/** One
|
|
8
|
+
/** One field row in the Fields view, including nested object fields and array items. */
|
|
9
9
|
export type InputFieldRow = {
|
|
10
|
-
/** Object key. */
|
|
10
|
+
/** Object key, or array index. */
|
|
11
11
|
readonly key: string;
|
|
12
12
|
/** Raw value (for copy). */
|
|
13
13
|
readonly value: unknown;
|
|
@@ -15,25 +15,43 @@ export type InputFieldRow = {
|
|
|
15
15
|
readonly display: string;
|
|
16
16
|
/** Value kind chip. */
|
|
17
17
|
readonly kind: InputFieldKind;
|
|
18
|
+
/**
|
|
19
|
+
* Nested object fields or array items.
|
|
20
|
+
* `null` for scalars and empty containers — those rows do not expand.
|
|
21
|
+
*/
|
|
22
|
+
readonly children: readonly InputFieldRow[] | null;
|
|
18
23
|
};
|
|
19
24
|
|
|
20
25
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
26
|
+
* Project an object or array into field rows. Nested objects and arrays
|
|
27
|
+
* become expandable child rows. Returns `null` for scalars, `null`, and
|
|
28
|
+
* empty arrays.
|
|
24
29
|
*
|
|
25
|
-
* @param value - Stored run input
|
|
30
|
+
* @param value - Stored run input or output
|
|
26
31
|
*/
|
|
27
32
|
export function inputFieldRows(value: unknown): readonly InputFieldRow[] | null {
|
|
28
|
-
if (
|
|
29
|
-
return null;
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
if (value.length === 0) return null;
|
|
35
|
+
return value.map((item, index) => projectField(String(index), item));
|
|
30
36
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
if (value === null || typeof value !== "object") return null;
|
|
38
|
+
return Object.entries(value as Record<string, unknown>).map(([key, v]) => projectField(key, v));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Whether a stored payload is worth a Body panel.
|
|
43
|
+
*
|
|
44
|
+
* `null`, `undefined`, `""`, `{}`, and `[]` are empty. A scalar such as
|
|
45
|
+
* `0` or `false` still counts.
|
|
46
|
+
*
|
|
47
|
+
* @param value - Stored run input or output
|
|
48
|
+
*/
|
|
49
|
+
export function payloadHasContent(value: unknown): boolean {
|
|
50
|
+
if (value === null || value === undefined) return false;
|
|
51
|
+
if (typeof value === "string") return value.length > 0;
|
|
52
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
53
|
+
if (typeof value === "object") return Object.keys(value).length > 0;
|
|
54
|
+
return true;
|
|
37
55
|
}
|
|
38
56
|
|
|
39
57
|
/**
|
|
@@ -96,13 +114,61 @@ function fieldKind(value: unknown): InputFieldKind {
|
|
|
96
114
|
}
|
|
97
115
|
}
|
|
98
116
|
|
|
117
|
+
const PREVIEW_FIELDS = 3;
|
|
118
|
+
const PREVIEW_SCALAR_MAX = 42;
|
|
119
|
+
|
|
120
|
+
function projectField(key: string, value: unknown): InputFieldRow {
|
|
121
|
+
return {
|
|
122
|
+
key,
|
|
123
|
+
value,
|
|
124
|
+
display: formatFieldDisplay(value),
|
|
125
|
+
kind: fieldKind(value),
|
|
126
|
+
children: childRows(value),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function childRows(value: unknown): readonly InputFieldRow[] | null {
|
|
131
|
+
if (Array.isArray(value)) {
|
|
132
|
+
if (value.length === 0) return null;
|
|
133
|
+
return value.map((item, index) => projectField(String(index), item));
|
|
134
|
+
}
|
|
135
|
+
if (value !== null && typeof value === "object") {
|
|
136
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
137
|
+
if (entries.length === 0) return null;
|
|
138
|
+
return entries.map(([key, v]) => projectField(key, v));
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
99
143
|
function formatFieldDisplay(value: unknown): string {
|
|
100
144
|
if (value === null) return "null";
|
|
101
145
|
if (typeof value === "string") return value;
|
|
102
146
|
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
147
|
+
if (Array.isArray(value)) {
|
|
148
|
+
if (value.length === 0) return "[]";
|
|
149
|
+
return `${value.length} ${value.length === 1 ? "item" : "items"}`;
|
|
150
|
+
}
|
|
151
|
+
if (typeof value === "object") return objectPreview(value as Record<string, unknown>);
|
|
152
|
+
if (typeof value === "bigint") return value.toString();
|
|
153
|
+
return typeof value;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Short collapsed preview: up to three short scalar fields, otherwise a count.
|
|
158
|
+
*
|
|
159
|
+
* @param value - Plain object
|
|
160
|
+
*/
|
|
161
|
+
function objectPreview(value: Record<string, unknown>): string {
|
|
162
|
+
const keys = Object.keys(value);
|
|
163
|
+
if (keys.length === 0) return "{}";
|
|
164
|
+
const bits: string[] = [];
|
|
165
|
+
for (const [key, v] of Object.entries(value)) {
|
|
166
|
+
if (bits.length >= PREVIEW_FIELDS) break;
|
|
167
|
+
if (typeof v !== "string" && typeof v !== "number" && typeof v !== "boolean") continue;
|
|
168
|
+
const text = typeof v === "string" ? v : String(v);
|
|
169
|
+
if (text.length === 0 || text.length > PREVIEW_SCALAR_MAX) continue;
|
|
170
|
+
bits.push(`${key}: ${text}`);
|
|
107
171
|
}
|
|
172
|
+
if (bits.length > 0) return bits.join(" · ");
|
|
173
|
+
return `${keys.length} ${keys.length === 1 ? "field" : "fields"}`;
|
|
108
174
|
}
|