okengine 0.22.0 → 0.23.1
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 +7 -0
- package/manifest.v1.schema.json +44 -0
- package/package.json +32 -19
- package/site/content/docs/ai/mcp.mdx +14 -10
- package/site/content/docs/ai/skills.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/index.mdx +4 -4
- package/site/content/docs/elements/channel/receipts.mdx +7 -5
- package/site/content/docs/elements/channel/sms.mdx +2 -1
- package/site/content/docs/elements/flow/index.mdx +22 -18
- package/site/content/docs/elements/gate/auth.mdx +4 -1
- package/site/content/docs/elements/signal/index.mdx +15 -14
- package/site/content/docs/elements/store/index.mdx +1 -1
- package/site/content/docs/elements/store/kv.mdx +5 -5
- package/site/content/docs/plugins/otp.mdx +3 -3
- package/site/content/docs/recipes/dragonfly.mdx +5 -5
- package/site/content/docs/recipes/index.mdx +6 -6
- 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 +9 -7
- package/site/content/docs/reference/fx.mdx +19 -10
- 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/auth/tenants.ts +17 -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/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/doctor-diff.test.ts +29 -1
- package/src/cli/doctor-diff.ts +35 -0
- package/src/cli/eval.ts +142 -1
- package/src/cli/index.ts +5 -0
- package/src/cli/load-config.images.test.ts +2 -2
- package/src/cli/load-config.ts +1 -1
- package/src/cli/manifest-pr-diff.ts +16 -0
- package/src/cli/mcp-from-console.ts +48 -0
- package/src/cli/registry.ts +35 -1
- package/src/client/agent.ts +307 -0
- package/src/client/transport.test.ts +20 -0
- package/src/client/transport.ts +2 -1
- package/src/client/types.ts +1 -1
- 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-fetch.test.ts +77 -0
- package/src/compiler/effects-infer.ts +114 -17
- package/src/compiler/effects-join.test.ts +48 -0
- package/src/compiler/extract.test.ts +28 -0
- package/src/compiler/extract.ts +279 -13
- package/src/compiler/fx-index.ts +58 -4
- package/src/compiler/response.ts +35 -2
- package/src/config/index.ts +21 -3
- package/src/console/index.ts +1 -1
- 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.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-C_N4qvai.js +4 -0
- package/src/console/ui-next/dist/assets/{agent-disclosure-Bkohc_8X.js → agent-disclosure-Cq1TxniW.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-rL0lHxat.js → cache-glyph-BaI4uxZz.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-Deo4PP18.js → call-pii-button-DNyK7IIY.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-CoJ6amHf.js → collapsible-CRhBJMZR.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-D9twj8Xy.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-CsHWlVPU.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-ZaTJAhR9.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-CPYBzDh0.js → highlighted-json-DQ-EMAfz.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-DTPevKmM.js → http-method-Cn_Fl79s.js} +1 -1
- package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
- package/src/console/ui-next/dist/assets/index-CmRFeWdT.js +58 -0
- package/src/console/ui-next/dist/assets/observability-page-Cn6R_dXz.js +8 -0
- package/src/console/ui-next/dist/assets/{replica-lag-BM27lXld.js → replica-lag-Bx6EZhSe.js} +7 -7
- package/src/console/ui-next/dist/assets/request-meta-DmdiUrfh.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-8P6vGXnn.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-BfGvpFSv.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CWdUHbc0.js +55 -0
- package/src/console/ui-next/dist/assets/units-page-CZOFZzay.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-ki_ishmX.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 +6 -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/recipes/dragonfly.ts +3 -2
- package/src/docker/recipes/redis.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.test.ts +22 -1
- package/src/drivers/journal-postgres.ts +511 -13
- 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/signal-compete.test.ts +234 -0
- package/src/drivers/signal-postgres.ts +39 -19
- package/src/drivers/signal-redis.ts +167 -11
- package/src/drivers/signal-types.ts +12 -0
- 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/mcp-protocol.ts +5 -6
- 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 +1187 -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/channel/runtime.ts +11 -0
- package/src/elements/channel/sql-ledger.ts +251 -0
- package/src/elements/clock/durable.ts +27 -1
- package/src/elements/signal/runtime.ts +9 -0
- package/src/elements/signal.test.ts +1 -0
- package/src/elements/store/cache.test.ts +120 -15
- package/src/elements/store/cache.ts +200 -39
- package/src/elements/store/prepare-row.test.ts +6 -2
- package/src/elements/store/sql-session.test.ts +53 -0
- package/src/elements/store/sql-session.ts +206 -10
- package/src/elements/store.ts +1 -0
- package/src/i18n/catalogs/ar.ts +5 -0
- package/src/i18n/catalogs/en.ts +5 -0
- package/src/index.ts +2 -0
- package/src/kernel/agent-event-store.ts +448 -0
- package/src/kernel/app.ts +135 -17
- package/src/kernel/auto-cache.test.ts +228 -0
- package/src/kernel/boot-bind/ai.ts +3 -0
- package/src/kernel/boot-bind/channel.ts +63 -2
- package/src/kernel/boot-bind/honor-config.test.ts +40 -10
- package/src/kernel/boot-bind/signal.ts +61 -25
- package/src/kernel/boot-bind/store.test.ts +26 -0
- package/src/kernel/boot-bind/store.ts +82 -6
- package/src/kernel/boot.ts +31 -6
- package/src/kernel/builtin-errors.ts +2 -0
- package/src/kernel/capability.ts +3 -0
- package/src/kernel/decision-label-store.ts +441 -0
- 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 +11 -0
- package/src/kernel/errors-text.ts +20 -0
- package/src/kernel/errors.ts +10 -0
- package/src/kernel/flow.test.ts +2 -3
- package/src/kernel/flow.ts +33 -3
- package/src/kernel/fx-call-types.test.ts +32 -0
- package/src/kernel/fx-decide.ts +765 -0
- package/src/kernel/fx-sql-handle.ts +46 -7
- package/src/kernel/fx.test.ts +13 -8
- package/src/kernel/fx.ts +339 -45
- package/src/kernel/http-frame.test.ts +94 -0
- package/src/kernel/http-frame.ts +203 -0
- package/src/kernel/idempotency.test.ts +2 -1
- package/src/kernel/journal.test.ts +114 -0
- package/src/kernel/journal.ts +471 -51
- package/src/kernel/json-result.ts +10 -0
- package/src/kernel/pipeline-tenant.ts +5 -1
- package/src/kernel/pipeline.test.ts +45 -0
- package/src/kernel/pipeline.ts +5 -0
- package/src/kernel/signal-tx.ts +28 -0
- package/src/kernel/sse-id.ts +32 -0
- package/src/kernel/store-transaction.test.ts +95 -0
- package/src/manifest/diff.ts +1 -0
- package/src/manifest/types.ts +29 -2
- package/src/mcp/ai-tools.test.ts +202 -0
- package/src/mcp/authorization.ts +66 -0
- package/src/mcp/docs-server.ts +19 -2
- package/src/mcp/mcp.test.ts +83 -0
- package/src/mcp/protocol.ts +8 -2
- package/src/mcp/server.ts +23 -2
- package/src/mcp/session.ts +3 -0
- package/src/mcp/tools.ts +168 -0
- package/src/mcp/versions.ts +60 -0
- package/src/plugins/index.ts +1 -0
- package/src/plugins/mena.ts +56 -0
- package/src/release/http-graph.test.ts +2 -0
- package/src/release/http-graph.ts +5 -0
- package/src/release/measure.ts +39 -14
- 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/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
- package/src/console/ui-next/dist/assets/access-page-Ze6ckjdd.js +0 -4
- package/src/console/ui-next/dist/assets/copy-inline-button-CWG5_ZOh.js +0 -1
- package/src/console/ui-next/dist/assets/detail-header-b_ZzIsmU.js +0 -1
- package/src/console/ui-next/dist/assets/dropdown-menu-nwXEO1Ac.js +0 -1
- package/src/console/ui-next/dist/assets/duration-tone-BbQ_8z50.js +0 -9
- package/src/console/ui-next/dist/assets/element-icons-CwyLpVXz.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-C8sSCqYT.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-D2E7BtKK.js +0 -1
- package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
- package/src/console/ui-next/dist/assets/index-vTuwmeQz.js +0 -58
- package/src/console/ui-next/dist/assets/observability-page-SJTSzHFJ.js +0 -4
- package/src/console/ui-next/dist/assets/request-meta-BjFT7DNl.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-CUNegEV4.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-Qj9ThRE-.js +0 -41
- package/src/console/ui-next/dist/assets/trace-detail-sheet-Cmitq3da.js +0 -2
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CbDIB-7x.js +0 -55
- package/src/console/ui-next/dist/assets/units-page-_PuYqFty.js +0 -1
- package/src/console/ui-next/dist/assets/use-vault-list-Cs45Czkt.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-BOMr0go9.js +0 -2
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP wire frame stored on a wide event — query, headers, status.
|
|
3
|
+
*
|
|
4
|
+
* Credential headers are replaced before the row is written. The body stays
|
|
5
|
+
* on `input` / `output`; this frame is the rest of the message.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { RunHttpFrame } from "../runs/types.ts";
|
|
9
|
+
|
|
10
|
+
/** Replacement for credential header and query values. */
|
|
11
|
+
export const HTTP_FRAME_REDACTED = "[redacted]";
|
|
12
|
+
|
|
13
|
+
const EXACT_HEADERS = new Set([
|
|
14
|
+
"authorization",
|
|
15
|
+
"proxy-authorization",
|
|
16
|
+
"cookie",
|
|
17
|
+
"set-cookie",
|
|
18
|
+
"x-api-key",
|
|
19
|
+
"dpop",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Build the Request a Console invoke would have sent for an HTTP flow.
|
|
24
|
+
*
|
|
25
|
+
* Call API runs `execute` directly, so there is no socket request. The frame
|
|
26
|
+
* still needs a method, path, query, and the headers the invoke actually
|
|
27
|
+
* uses (`accept`, `x-oke-client: console`, and `content-type` when there is a body).
|
|
28
|
+
* The operator's browser user-agent is not copied onto the flow.
|
|
29
|
+
*
|
|
30
|
+
* @param trigger - HTTP method and path template
|
|
31
|
+
* @param options - Path params and the assembled flow input
|
|
32
|
+
*/
|
|
33
|
+
export function requestForHttpInvoke(
|
|
34
|
+
trigger: { readonly method: string; readonly path: string },
|
|
35
|
+
options: {
|
|
36
|
+
readonly pathParams?: Readonly<Record<string, string>>;
|
|
37
|
+
readonly input?: unknown;
|
|
38
|
+
} = {},
|
|
39
|
+
): Request {
|
|
40
|
+
let path = trigger.path;
|
|
41
|
+
const params = options.pathParams ?? {};
|
|
42
|
+
for (const [key, value] of Object.entries(params)) {
|
|
43
|
+
path = path.replaceAll(`:${key}`, encodeURIComponent(value));
|
|
44
|
+
}
|
|
45
|
+
const url = new URL(path, "http://127.0.0.1");
|
|
46
|
+
const method = trigger.method.toUpperCase();
|
|
47
|
+
const headers = new Headers({ accept: "application/json", "x-oke-client": "console" });
|
|
48
|
+
if (!methodCarriesBody(method)) {
|
|
49
|
+
appendScalarQuery(url, options.input, new Set(Object.keys(params)));
|
|
50
|
+
return new Request(url.href, { method, headers });
|
|
51
|
+
}
|
|
52
|
+
headers.set("content-type", "application/json");
|
|
53
|
+
const body = options.input === undefined ? undefined : JSON.stringify(options.input);
|
|
54
|
+
return new Request(url.href, {
|
|
55
|
+
method,
|
|
56
|
+
headers,
|
|
57
|
+
...(body !== undefined ? { body } : {}),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Build the wire frame for an HTTP run.
|
|
63
|
+
*
|
|
64
|
+
* Does not read the body. Safe to call after the handler has consumed it,
|
|
65
|
+
* including streamed responses.
|
|
66
|
+
*
|
|
67
|
+
* @param request - Incoming request
|
|
68
|
+
* @param response - Final response after `onResponse`, when one exists
|
|
69
|
+
*/
|
|
70
|
+
export function captureHttpFrame(request: Request, response: Response | undefined): RunHttpFrame {
|
|
71
|
+
const url = new URL(request.url);
|
|
72
|
+
const query: Record<string, string> = {};
|
|
73
|
+
for (const [key, value] of url.searchParams) {
|
|
74
|
+
query[key] = sensitiveQuery(key) ? HTTP_FRAME_REDACTED : value;
|
|
75
|
+
}
|
|
76
|
+
return redactHttpFrame({
|
|
77
|
+
request: {
|
|
78
|
+
method: request.method,
|
|
79
|
+
path: url.pathname,
|
|
80
|
+
query,
|
|
81
|
+
headers: headerMap(request.headers),
|
|
82
|
+
},
|
|
83
|
+
...(response !== undefined
|
|
84
|
+
? { response: { status: response.status, headers: headerMap(response.headers) } }
|
|
85
|
+
: {}),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Mask credential headers and query values on a stored frame.
|
|
91
|
+
*
|
|
92
|
+
* Applied again at Console projection so a row that skipped capture still
|
|
93
|
+
* does not show secrets.
|
|
94
|
+
*
|
|
95
|
+
* @param frame - Stored HTTP frame
|
|
96
|
+
*/
|
|
97
|
+
export function redactHttpFrame(frame: RunHttpFrame): RunHttpFrame {
|
|
98
|
+
return {
|
|
99
|
+
request: {
|
|
100
|
+
method: frame.request.method,
|
|
101
|
+
path: frame.request.path,
|
|
102
|
+
query: redactQuery(frame.request.query),
|
|
103
|
+
headers: redactHeaders(frame.request.headers),
|
|
104
|
+
},
|
|
105
|
+
...(frame.response !== undefined
|
|
106
|
+
? {
|
|
107
|
+
response: {
|
|
108
|
+
status: frame.response.status,
|
|
109
|
+
headers: redactHeaders(frame.response.headers),
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
: {}),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* True when a header name carries a credential.
|
|
118
|
+
*
|
|
119
|
+
* @param name - Lower-case header name
|
|
120
|
+
*/
|
|
121
|
+
export function sensitiveHeader(name: string): boolean {
|
|
122
|
+
const key = name.toLowerCase();
|
|
123
|
+
if (EXACT_HEADERS.has(key)) return true;
|
|
124
|
+
if (key === "idempotency-key") return false;
|
|
125
|
+
return (
|
|
126
|
+
key.includes("token") ||
|
|
127
|
+
key.includes("secret") ||
|
|
128
|
+
key.includes("api-key") ||
|
|
129
|
+
key.endsWith("-key") ||
|
|
130
|
+
key.endsWith("_key")
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function methodCarriesBody(method: string): boolean {
|
|
135
|
+
switch (method) {
|
|
136
|
+
case "GET":
|
|
137
|
+
case "HEAD":
|
|
138
|
+
case "DELETE":
|
|
139
|
+
return false;
|
|
140
|
+
default:
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function appendScalarQuery(url: URL, input: unknown, skip: ReadonlySet<string>): void {
|
|
146
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) return;
|
|
147
|
+
for (const [key, value] of Object.entries(input as Record<string, unknown>)) {
|
|
148
|
+
if (skip.has(key)) continue;
|
|
149
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
150
|
+
url.searchParams.set(key, String(value));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function sensitiveQuery(name: string): boolean {
|
|
156
|
+
const key = name.toLowerCase();
|
|
157
|
+
return (
|
|
158
|
+
key === "token" ||
|
|
159
|
+
key.endsWith("_token") ||
|
|
160
|
+
key.endsWith("-token") ||
|
|
161
|
+
key === "api_key" ||
|
|
162
|
+
key === "apikey" ||
|
|
163
|
+
key === "api-key" ||
|
|
164
|
+
key === "secret" ||
|
|
165
|
+
key === "password" ||
|
|
166
|
+
key === "access_token" ||
|
|
167
|
+
key === "refresh_token"
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function headerMap(headers: Headers): Record<string, string> {
|
|
172
|
+
const out: Record<string, string> = {};
|
|
173
|
+
headers.forEach((value, key) => {
|
|
174
|
+
const name = key.toLowerCase();
|
|
175
|
+
if (sensitiveHeader(name)) {
|
|
176
|
+
out[name] = HTTP_FRAME_REDACTED;
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const prev = out[name];
|
|
180
|
+
out[name] = prev === undefined ? value : `${prev}, ${value}`;
|
|
181
|
+
});
|
|
182
|
+
return sortRecord(out);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function redactHeaders(headers: Readonly<Record<string, string>>): Record<string, string> {
|
|
186
|
+
const out: Record<string, string> = {};
|
|
187
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
188
|
+
out[key.toLowerCase()] = sensitiveHeader(key) ? HTTP_FRAME_REDACTED : value;
|
|
189
|
+
}
|
|
190
|
+
return sortRecord(out);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function redactQuery(query: Readonly<Record<string, string>>): Record<string, string> {
|
|
194
|
+
const out: Record<string, string> = {};
|
|
195
|
+
for (const [key, value] of Object.entries(query)) {
|
|
196
|
+
out[key] = sensitiveQuery(key) ? HTTP_FRAME_REDACTED : value;
|
|
197
|
+
}
|
|
198
|
+
return sortRecord(out);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function sortRecord(record: Record<string, string>): Record<string, string> {
|
|
202
|
+
return Object.fromEntries(Object.entries(record).sort(([a], [b]) => a.localeCompare(b)));
|
|
203
|
+
}
|
|
@@ -353,7 +353,8 @@ describe("HTTP idempotency", () => {
|
|
|
353
353
|
expect(step1).toEqual(["step1"]);
|
|
354
354
|
blockers[0]!();
|
|
355
355
|
blockers[1]!();
|
|
356
|
-
|
|
356
|
+
// The forfeited holder kept the old lease token. Its later write is rejected.
|
|
357
|
+
await expect(hung).rejects.toThrow(/OKE1074/);
|
|
357
358
|
expect((await resumed).status).toBe(200);
|
|
358
359
|
expect(step1).toEqual(["step1"]);
|
|
359
360
|
}, 30_000);
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
import { describe, expect, test } from "bun:test";
|
|
7
7
|
|
|
8
|
+
import { runDurable } from "../elements/clock/durable.ts";
|
|
9
|
+
import { flow } from "./flow.ts";
|
|
8
10
|
import {
|
|
9
11
|
createJournal,
|
|
10
12
|
createMemoryJournalStore,
|
|
@@ -13,6 +15,7 @@ import {
|
|
|
13
15
|
isJournalSuspend,
|
|
14
16
|
JOURNAL_DEFAULT_LEASE_MS,
|
|
15
17
|
} from "./journal.ts";
|
|
18
|
+
import { OkeError } from "./errors.ts";
|
|
16
19
|
|
|
17
20
|
describe("journal lease (memory store)", () => {
|
|
18
21
|
test("built-in stores expose the lease surface", () => {
|
|
@@ -139,4 +142,115 @@ describe("journal lease (memory store)", () => {
|
|
|
139
142
|
const again = await journal.resume(session.runId);
|
|
140
143
|
expect(await again.step("s", () => 2)).toBe(1);
|
|
141
144
|
});
|
|
145
|
+
|
|
146
|
+
test("a stale lease holder cannot append", async () => {
|
|
147
|
+
let t = 1_000;
|
|
148
|
+
const store = createMemoryJournalStore();
|
|
149
|
+
const a = createJournal({
|
|
150
|
+
store,
|
|
151
|
+
now: () => t,
|
|
152
|
+
lease: { instanceId: "a", leaseMs: 100 },
|
|
153
|
+
codeVersion: "v1",
|
|
154
|
+
});
|
|
155
|
+
const session = await a.start("charge");
|
|
156
|
+
await session.step("create-intent", () => ({ id: "pi_1" }));
|
|
157
|
+
t = 1_200;
|
|
158
|
+
const b = createJournal({
|
|
159
|
+
store,
|
|
160
|
+
now: () => t,
|
|
161
|
+
lease: { instanceId: "b", leaseMs: 100 },
|
|
162
|
+
codeVersion: "v1",
|
|
163
|
+
});
|
|
164
|
+
await b.resume(session.runId);
|
|
165
|
+
try {
|
|
166
|
+
await session.step("capture", () => ({ ok: true }));
|
|
167
|
+
expect.unreachable("stale holder must be rejected");
|
|
168
|
+
} catch (err) {
|
|
169
|
+
expect(err).toBeInstanceOf(OkeError);
|
|
170
|
+
expect((err as OkeError).code).toBe(1074);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("replay throws when the next call does not match the journal", async () => {
|
|
175
|
+
const store = createMemoryJournalStore();
|
|
176
|
+
const journal = createJournal({ store, now: () => 1, codeVersion: "v1" });
|
|
177
|
+
const session = await journal.start("charge");
|
|
178
|
+
await session.step("a", () => 1);
|
|
179
|
+
const resumed = await journal.resume(session.runId);
|
|
180
|
+
try {
|
|
181
|
+
await resumed.step("b", () => 2);
|
|
182
|
+
expect.unreachable("divergent replay must throw");
|
|
183
|
+
} catch (err) {
|
|
184
|
+
expect(err).toBeInstanceOf(OkeError);
|
|
185
|
+
expect((err as OkeError).code).toBe(1075);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("a non-JSON journal value is OKE1077", async () => {
|
|
190
|
+
const store = createMemoryJournalStore();
|
|
191
|
+
const session = await createJournal({ store, now: () => 1 }).start("charge");
|
|
192
|
+
try {
|
|
193
|
+
await session.effect("call", "charge", () => 1n);
|
|
194
|
+
expect.unreachable("bigint must not journal");
|
|
195
|
+
} catch (err) {
|
|
196
|
+
expect(err).toBeInstanceOf(OkeError);
|
|
197
|
+
expect((err as OkeError).code).toBe(1077);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("resume rejects a run stamped with another code version", async () => {
|
|
202
|
+
const store = createMemoryJournalStore();
|
|
203
|
+
const started = await createJournal({ store, now: () => 1, codeVersion: "v1" }).start("charge");
|
|
204
|
+
try {
|
|
205
|
+
await createJournal({ store, now: () => 2, codeVersion: "v2" }).resume(started.runId);
|
|
206
|
+
expect.unreachable("version skew must throw");
|
|
207
|
+
} catch (err) {
|
|
208
|
+
expect(err).toBeInstanceOf(OkeError);
|
|
209
|
+
expect((err as OkeError).code).toBe(1076);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe("journal fetch snapshot", () => {
|
|
215
|
+
test("a durable flow replays fx.fetch without calling the network again", async () => {
|
|
216
|
+
const store = createMemoryJournalStore();
|
|
217
|
+
let calls = 0;
|
|
218
|
+
const original = globalThis.fetch;
|
|
219
|
+
globalThis.fetch = (async () => {
|
|
220
|
+
calls += 1;
|
|
221
|
+
return new Response("hello", { status: 201 });
|
|
222
|
+
}) as unknown as typeof fetch;
|
|
223
|
+
const load = flow("load", {
|
|
224
|
+
durable: true,
|
|
225
|
+
do: async (_input, fx) => {
|
|
226
|
+
const res = await fx.step("load", () => fx.fetch("https://example.com/data"));
|
|
227
|
+
return { status: res.status, text: await res.text() };
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
try {
|
|
231
|
+
const first = await runDurable({
|
|
232
|
+
flow: load,
|
|
233
|
+
input: {},
|
|
234
|
+
journalStore: store,
|
|
235
|
+
now: () => 1,
|
|
236
|
+
});
|
|
237
|
+
expect(first).toMatchObject({ status: "completed", output: { status: 201, text: "hello" } });
|
|
238
|
+
expect(calls).toBe(1);
|
|
239
|
+
const row = await store.get(first.runId);
|
|
240
|
+
expect(row?.entries.some((entry) => entry.kind === "step" && entry.name === "load")).toBe(
|
|
241
|
+
true,
|
|
242
|
+
);
|
|
243
|
+
const replay = await createJournal({ store, now: () => 2 }).resume(first.runId);
|
|
244
|
+
const again = await replay.step("load", () => {
|
|
245
|
+
calls += 1;
|
|
246
|
+
return new Response("nope", { status: 500 });
|
|
247
|
+
});
|
|
248
|
+
expect(again).toBeInstanceOf(Response);
|
|
249
|
+
expect(again.status).toBe(201);
|
|
250
|
+
expect(await again.text()).toBe("hello");
|
|
251
|
+
expect(calls).toBe(1);
|
|
252
|
+
} finally {
|
|
253
|
+
globalThis.fetch = original;
|
|
254
|
+
}
|
|
255
|
+
});
|
|
142
256
|
});
|