okengine 0.13.0 → 0.15.2
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/manifest.v1.schema.json +7 -1
- package/package.json +4 -5
- package/site/content/docs/ai/mcp.mdx +27 -2
- package/site/content/docs/elements/ai.mdx +59 -11
- package/site/content/docs/elements/channel.mdx +1 -0
- package/site/content/docs/elements/clock.mdx +30 -12
- package/site/content/docs/elements/flow.mdx +9 -3
- package/site/content/docs/elements/gate.mdx +25 -22
- package/site/content/docs/elements/signal.mdx +71 -25
- package/site/content/docs/elements/store.mdx +87 -28
- package/site/content/docs/elements/vault.mdx +1 -0
- package/site/content/docs/get-started/basic-usage.mdx +22 -13
- package/site/content/docs/get-started/index.mdx +5 -0
- package/site/content/docs/get-started/installation.mdx +2 -2
- package/site/content/docs/get-started/introduction.mdx +11 -3
- package/site/content/docs/get-started/meta.json +1 -1
- package/site/content/docs/get-started/project-structure.mdx +932 -0
- package/site/content/docs/reference/cli.md +13 -10
- package/site/content/docs/reference/client.mdx +25 -21
- package/site/content/docs/reference/fx.mdx +27 -22
- package/site/content/docs/reference/security.md +1 -1
- package/src/auth/bindings.ts +14 -5
- package/src/auth/cross-plane.ts +10 -3
- package/src/cli/ai-setup/ai-setup.test.ts +62 -0
- package/src/cli/ai-setup/apply.ts +32 -1
- package/src/cli/ai-setup/recommend.test.ts +25 -0
- package/src/cli/ai-setup/recommend.ts +8 -3
- package/src/cli/attach-host-console.test.ts +3 -2
- package/src/cli/attach-host-console.ts +3 -0
- package/src/cli/build.test.ts +4 -2
- package/src/cli/db-seed.ts +48 -15
- package/src/cli/db.test.ts +104 -0
- package/src/cli/db.ts +122 -10
- package/src/cli/dev-controls.test.ts +20 -0
- package/src/cli/dev-controls.ts +13 -0
- package/src/cli/dev.test.ts +15 -4
- package/src/cli/dev.ts +102 -29
- package/src/cli/ensure-drizzle-config.ts +2 -1
- package/src/cli/tui/DevLive.tsx +12 -2
- package/src/cli/tui/keys.test.ts +2 -1
- package/src/cli/tui/keys.ts +3 -2
- package/src/compiler/aot.test.ts +1 -1
- package/src/compiler/effects-infer.ts +58 -3
- package/src/compiler/extract.test.ts +458 -29
- package/src/compiler/extract.ts +387 -69
- package/src/compiler/flow-path.test.ts +96 -0
- package/src/compiler/flow-path.ts +173 -0
- package/src/compiler/generate-adopt.test.ts +112 -3
- package/src/compiler/generate-adopt.ts +280 -26
- package/src/compiler/response.ts +45 -1
- package/src/compiler/schema-from-ast.ts +609 -0
- package/src/console/server/ai.ts +5 -2
- package/src/console/server/bind.ts +2 -2
- package/src/console/server/console.test.ts +3 -0
- package/src/console/server/flows.ts +121 -40
- package/src/console/server/invoke-user-flow.test.ts +3 -1
- package/src/console/server/invoke-user-flow.ts +17 -2
- package/src/console/server/iso-at.test.ts +9 -0
- package/src/console/server/iso-at.ts +11 -0
- package/src/console/server/security-headers.ts +6 -3
- package/src/console/server/serve.ts +17 -2
- package/src/console/server/spa-proxy.test.ts +1 -0
- package/src/console/server/sql-catalog.ts +147 -7
- package/src/console/server/sql-rls.test.ts +16 -0
- package/src/console/server/state.ts +8 -0
- package/src/console/server/store.test.ts +7 -4
- package/src/console/server/store.ts +142 -58
- package/src/console/ui-next/dist/assets/agent-disclosure-C0X1fbWF.js +1 -0
- package/src/console/ui-next/dist/assets/cache-glyph-F1FI122b.js +1 -0
- package/src/console/ui-next/dist/assets/{call-pii-button-bqkxMrJH.js → call-pii-button-DEDCl_j3.js} +1 -1
- package/src/console/ui-next/dist/assets/collapsible-LPqGvfoz.js +1 -0
- package/src/console/ui-next/dist/assets/{confirm-sheet-DDCRmG62.js → confirm-sheet-3ptDzXbz.js} +1 -1
- package/src/console/ui-next/dist/assets/{duration-tone-oiRxPVsZ.js → duration-tone-sC3lGABz.js} +1 -1
- package/src/console/ui-next/dist/assets/{explorer-empty-HjTnVQoR.js → explorer-empty-BvYrygyO.js} +1 -1
- package/src/console/ui-next/dist/assets/flows-page-Dluo1Bd7.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-CvDPvveV.js → highlighted-json-CS_O8L-r.js} +1 -1
- package/src/console/ui-next/dist/assets/http-method--sWDdXSB.js +1 -0
- package/src/console/ui-next/dist/assets/index-Ca3HZMVq.js +66 -0
- package/src/console/ui-next/dist/assets/index-UtTDRQpS.css +2 -0
- package/src/console/ui-next/dist/assets/{link-DF7SZ9Ek.js → link-COeyggt-.js} +1 -1
- package/src/console/ui-next/dist/assets/observability-page-HK9-BO8z.js +4 -0
- package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +1 -0
- package/src/console/ui-next/dist/assets/replica-lag-DBIFFf7d.js +18 -0
- package/src/console/ui-next/dist/assets/sheet-form-Dnwa5oOR.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-CoqwEIi0.js +1 -0
- package/src/console/ui-next/dist/assets/skeleton-BHcfDYcb.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-BSohH3wM.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-CFIGRnnA.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-BgLl34w7.js +54 -0
- package/src/console/ui-next/dist/assets/units-page-B0cFE76A.js +1 -0
- package/src/console/ui-next/dist/assets/{use-vault-list-Cl79j_ku.js → use-vault-list-BLbZhzyF.js} +1 -1
- package/src/console/ui-next/dist/assets/vault-page-B1SbYe10.js +2 -0
- package/src/console/ui-next/dist/index.html +6 -6
- package/src/console/ui-next/seed-invoke-host.ts +2 -0
- package/src/console/ui-next/src/client.ts +35 -7
- package/src/console/ui-next/src/components/motion/table/index.tsx +16 -0
- package/src/console/ui-next/src/components/motion/table/types.ts +4 -0
- package/src/console/ui-next/src/components/ui/kbd.tsx +2 -2
- package/src/console/ui-next/src/features/flows/graph/build-flow-graph.test.ts +18 -0
- package/src/console/ui-next/src/features/flows/graph/build-flow-graph.ts +14 -1
- package/src/console/ui-next/src/features/flows/graph/neighborhood.test.ts +17 -0
- package/src/console/ui-next/src/features/flows/graph/neighborhood.ts +16 -3
- package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +3 -1
- package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/http-method.ts +19 -0
- package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +9 -3
- package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +29 -2
- package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +10 -3
- package/src/console/ui-next/src/features/observability/lib/ask-count.test.ts +25 -0
- package/src/console/ui-next/src/features/observability/lib/ask-count.ts +4 -1
- package/src/console/ui-next/src/features/store/detail/browse-section.tsx +45 -1
- package/src/console/ui-next/src/features/store/detail/resource-panel.tsx +78 -16
- package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +155 -14
- package/src/console/ui-next/src/features/store/explorer/store-tree.tsx +33 -133
- package/src/console/ui-next/src/features/store/files/file-preview.tsx +4 -1
- package/src/console/ui-next/src/features/store/grid/rls-policy-sheet.tsx +326 -165
- package/src/console/ui-next/src/features/store/grid/sql-insert-sheet.tsx +6 -0
- package/src/console/ui-next/src/features/store/grid/store-data-grid.tsx +50 -3
- package/src/console/ui-next/src/features/store/lib/files-meta.test.ts +3 -0
- package/src/console/ui-next/src/features/store/lib/files-meta.ts +12 -0
- package/src/console/ui-next/src/features/store/lib/query-defaults.test.ts +25 -0
- package/src/console/ui-next/src/features/store/lib/query-defaults.ts +85 -3
- package/src/console/ui-next/src/features/store/lib/query-gate.ts +30 -4
- package/src/console/ui-next/src/features/store/lib/rls-policy.test.ts +139 -0
- package/src/console/ui-next/src/features/store/lib/rls-policy.ts +223 -21
- package/src/console/ui-next/src/features/store/lib/schema-graph.ts +4 -0
- package/src/console/ui-next/src/features/store/lib/sql-catalog.test.ts +24 -0
- package/src/console/ui-next/src/features/store/lib/sql-catalog.ts +19 -0
- package/src/console/ui-next/src/features/store/lib/store-tree.test.ts +6 -0
- package/src/console/ui-next/src/features/store/lib/store-tree.ts +13 -0
- package/src/console/ui-next/src/features/store/query/query-console.tsx +48 -14
- package/src/console/ui-next/src/features/store/query/query-gate-menu.tsx +19 -20
- package/src/console/ui-next/src/features/store/query/query-gate-parts.tsx +2 -1
- package/src/console/ui-next/src/features/store/query/query-results.tsx +110 -83
- package/src/console/ui-next/src/features/store/schema/schema-table-node.tsx +17 -0
- package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +112 -72
- package/src/console/ui-next/src/features/units/detail/effects-summary.tsx +12 -4
- package/src/console/ui-next/src/features/units/lib/contract-input.test.ts +206 -1
- package/src/console/ui-next/src/features/units/lib/contract-input.ts +55 -0
- package/src/console/ui-next/src/features/vault/lib/types.ts +1 -1
- package/src/docker/compose-health.test.ts +27 -0
- package/src/docker/compose-health.ts +19 -3
- package/src/docker/docker.test.ts +4 -1
- package/src/docker/dockerfile.ts +1 -1
- package/src/docker/recipes/pgdog.ts +2 -0
- package/src/drivers/ai-anthropic.ts +5 -0
- package/src/drivers/ai-ollama.ts +49 -30
- package/src/drivers/ai-openai-compatible.ts +57 -46
- package/src/drivers/ai-providers.test.ts +3 -0
- package/src/drivers/bun-native-completeness.test.ts +7 -9
- package/src/drivers/clock-postgres.ts +13 -9
- package/src/drivers/instances-postgres.ts +4 -6
- package/src/drivers/journal-postgres.ts +13 -9
- package/src/drivers/pg-rls.test.ts +87 -0
- package/src/drivers/pg-rls.ts +233 -7
- package/src/drivers/pglite.ts +26 -3
- package/src/drivers/postgres.test.ts +23 -0
- package/src/drivers/postgres.ts +133 -9
- package/src/drivers/redis.ts +11 -4
- package/src/drivers/signal-redis.ts +24 -14
- package/src/drivers/signal-types.ts +2 -1
- package/src/drivers/types.ts +10 -0
- package/src/elements/ai/declare.ts +109 -0
- package/src/elements/ai/errors.test.ts +5 -1
- package/src/elements/ai/errors.ts +30 -2
- package/src/elements/ai/eval.ts +4 -6
- package/src/elements/ai/mcp-client.test.ts +206 -0
- package/src/elements/ai/mcp-client.ts +362 -0
- package/src/elements/ai/mcp-http.ts +159 -0
- package/src/elements/ai/mcp-mock.ts +134 -0
- package/src/elements/ai/mcp-protocol.ts +234 -0
- package/src/elements/ai/mcp-stdio.test.ts +50 -0
- package/src/elements/ai/mcp-stdio.ts +212 -0
- package/src/elements/ai/mcp-transport.ts +70 -0
- package/src/elements/ai/runtime.ts +159 -29
- package/src/elements/ai.test.ts +139 -0
- package/src/elements/ai.ts +16 -0
- package/src/elements/clock/declare.ts +26 -1
- package/src/elements/clock/health.test.ts +43 -0
- package/src/elements/clock/runtime.ts +55 -2
- package/src/elements/clock/schedule.ts +71 -142
- package/src/elements/clock.test.ts +9 -40
- package/src/elements/clock.ts +6 -1
- package/src/elements/gate/boot.ts +5 -5
- package/src/elements/gate/declare.ts +36 -4
- package/src/elements/gate/flatten.ts +1 -1
- package/src/elements/gate/runtime.ts +1 -1
- package/src/elements/index.ts +2 -0
- package/src/elements/signal/declare.ts +2 -1
- package/src/elements/signal/runtime.ts +16 -1
- package/src/elements/signal.ts +1 -0
- package/src/elements/store/cache.test.ts +2 -0
- package/src/elements/store/cache.ts +3 -3
- package/src/elements/store/declare.ts +9 -6
- package/src/elements/store/emit-drizzle.ts +44 -8
- package/src/elements/store/resource.test.ts +1 -1
- package/src/elements/store/rls-identity.test.ts +95 -0
- package/src/elements/store/rls-identity.ts +116 -0
- package/src/elements/store/runtime.ts +4 -0
- package/src/elements/store/schema-decl.test.ts +37 -0
- package/src/elements/store/schema-decl.ts +161 -0
- package/src/elements/store/seed.ts +1 -1
- package/src/elements/store/sql-condition.test.ts +12 -0
- package/src/elements/store/sql-condition.ts +26 -2
- package/src/elements/store/sql-rls-isolation.test.ts +165 -0
- package/src/elements/store/sql-rls-stamp.test.ts +117 -0
- package/src/elements/store/sql-session.ts +81 -3
- package/src/elements/store/upsert-app.test.ts +1 -1
- package/src/elements/store.ts +14 -0
- package/src/elements/vault/builtin-adapter.ts +17 -0
- package/src/full.ts +2 -0
- package/src/http.ts +20 -3
- package/src/index.ts +14 -1
- package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
- package/src/kernel/adopt-routes.ts +30 -2
- package/src/kernel/app.ts +227 -77
- package/src/kernel/auto-cache.test.ts +8 -8
- package/src/kernel/auto-registry.test.ts +54 -9
- package/src/kernel/boot-bind/ai.ts +24 -0
- package/src/kernel/boot-bind/clock.ts +2 -0
- package/src/kernel/boot.test.ts +4 -4
- package/src/kernel/boot.ts +5 -3
- package/src/kernel/effects-stamping.test.ts +3 -3
- package/src/kernel/element-registries.ts +15 -5
- package/src/kernel/errors.ts +24 -0
- package/src/kernel/flow-units.ts +64 -0
- package/src/kernel/flow.ts +29 -17
- package/src/kernel/fx-dead-letters.test.ts +77 -0
- package/src/kernel/fx.test.ts +22 -0
- package/src/kernel/fx.ts +135 -6
- package/src/kernel/horizontal-child.ts +4 -4
- package/src/kernel/http-path-pending.ts +20 -0
- package/src/kernel/http-resource.ts +4 -0
- package/src/kernel/http-stream.test.ts +174 -0
- package/src/kernel/index.ts +2 -0
- package/src/kernel/pipeline.test.ts +2 -2
- package/src/kernel/plugin-elements.test.ts +1 -1
- package/src/kernel/plugin-needs.test.ts +1 -1
- package/src/kernel/ready.test.ts +2 -2
- package/src/kernel/resource-mount.test.ts +14 -0
- package/src/kernel/run-duration.test.ts +2 -2
- package/src/kernel/stamp-http.test.ts +79 -0
- package/src/kernel/stamp-http.ts +75 -0
- package/src/kernel/triggers.ts +52 -27
- package/src/manifest/mcp-ref.ts +88 -0
- package/src/manifest/types.ts +41 -2
- package/src/manifest/validate.test.ts +20 -0
- package/src/mcp/docs-server.ts +1 -1
- package/src/mcp/server.ts +1 -1
- package/src/plugins/compression.test.ts +21 -0
- package/src/plugins/compression.ts +1 -0
- package/src/release/measure.ts +2 -2
- package/src/runtime/bun.ts +41 -4
- package/src/runtime/json-code-block.test.ts +2 -2
- package/src/runtime/serve.test.ts +4 -4
- package/src/term.test.ts +52 -4
- package/src/term.ts +125 -17
- package/src/test/reset-element-registries.ts +10 -3
- package/src/cli/ask-seed.test.ts +0 -96
- package/src/cli/ask-seed.ts +0 -82
- package/src/console/ui-next/dist/assets/cache-glyph-CLPBqZeb.js +0 -1
- package/src/console/ui-next/dist/assets/collapsible-DYb0xU8C.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-Dg8CTE29.js +0 -1
- package/src/console/ui-next/dist/assets/http-method-BJ92Z_ke.js +0 -1
- package/src/console/ui-next/dist/assets/index-Bp-R7jtM.js +0 -66
- package/src/console/ui-next/dist/assets/index-D4Ldtj79.css +0 -2
- package/src/console/ui-next/dist/assets/observability-page-DAnpEaq1.js +0 -4
- package/src/console/ui-next/dist/assets/query-gate-parts-1m8m1iNp.js +0 -2
- package/src/console/ui-next/dist/assets/react-B1ML8gxg.js +0 -1
- package/src/console/ui-next/dist/assets/replica-lag-C8_BRt2x.js +0 -18
- package/src/console/ui-next/dist/assets/sheet-form-D-ata7jy.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-DO4IsVqv.js +0 -1
- package/src/console/ui-next/dist/assets/skeleton-CL_X0GCj.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-v3LXdYpr.js +0 -45
- package/src/console/ui-next/dist/assets/trace-detail-sheet-D16lWQMt.js +0 -2
- package/src/console/ui-next/dist/assets/units-page-BtQ0bqMe.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-Ca-MvcmJ.js +0 -2
package/src/kernel/fx.ts
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* for deterministic tests (§7.6).
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import type { Effects, ResourceRef } from "../manifest/types.ts";
|
|
12
|
+
import type { Effects, ResourceRef, SignalResourceRef } from "../manifest/types.ts";
|
|
13
|
+
import { isMcpToolRef } from "../manifest/mcp-ref.ts";
|
|
13
14
|
import type { QueryPageSpec } from "./list-page.ts";
|
|
14
15
|
import { schemaTableName, sqlTableRef } from "../manifest/sql-resource.ts";
|
|
15
16
|
import type {
|
|
@@ -26,9 +27,11 @@ import type {
|
|
|
26
27
|
StoreRuntime,
|
|
27
28
|
SqlStoreHandle,
|
|
28
29
|
} from "../elements/store.ts";
|
|
30
|
+
import { rlsIdentityFromAuth } from "../elements/store.ts";
|
|
31
|
+
import type { RlsIdentity } from "../drivers/pg-rls.ts";
|
|
29
32
|
import type { SqlRow } from "../drivers/types.ts";
|
|
30
|
-
import type { SignalRuntime } from "../elements/signal.ts";
|
|
31
|
-
import type { SignalEmitOptions } from "../drivers/signal-types.ts";
|
|
33
|
+
import type { SignalDecl, SignalRuntime } from "../elements/signal.ts";
|
|
34
|
+
import type { DeadLetter, SignalEmitOptions } from "../drivers/signal-types.ts";
|
|
32
35
|
import type { VaultActor, VaultAdapter, VaultRuntime } from "../elements/vault.ts";
|
|
33
36
|
import type { ChannelRuntime } from "../elements/channel.ts";
|
|
34
37
|
import type { AiRuntime } from "../elements/ai.ts";
|
|
@@ -73,6 +76,15 @@ async function loadRunsWindow(): Promise<typeof import("../runs/window.ts")> {
|
|
|
73
76
|
/** Resource ref Flows declare to read the Runs store via {@link Fx.runs}. */
|
|
74
77
|
export const RUNS_RESOURCE = "runs";
|
|
75
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Capability ref for {@link Fx.deadLetters} — `signal:<name>`, never a store facet.
|
|
81
|
+
*
|
|
82
|
+
* @param name - Signal name
|
|
83
|
+
*/
|
|
84
|
+
export function signalReadRef(name: string): SignalResourceRef {
|
|
85
|
+
return `signal:${name}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
76
88
|
export type { FxRetryOptions, FxThunk } from "./concurrency.ts";
|
|
77
89
|
|
|
78
90
|
/** Named ref: plain string or `{ name }` element handle (optional pin). */
|
|
@@ -140,6 +152,27 @@ export interface FxTenant {
|
|
|
140
152
|
export interface FxClock {
|
|
141
153
|
/** Current epoch-ms (injectable via {@link CreateFxOptions.now}). */
|
|
142
154
|
now(): number;
|
|
155
|
+
/**
|
|
156
|
+
* Instant `duration` before {@link FxClock.now} (`"30d"` → now − 30 days).
|
|
157
|
+
* Uses the same duration strings as {@link FxClock.sleep} / `every()`.
|
|
158
|
+
*
|
|
159
|
+
* @param duration - Duration string (e.g. `"30d"`, `"2h"`)
|
|
160
|
+
*/
|
|
161
|
+
ago(duration: string): number;
|
|
162
|
+
/**
|
|
163
|
+
* Instant `duration` after {@link FxClock.now} (`"14d"` → now + 14 days).
|
|
164
|
+
* Uses the same duration strings as {@link FxClock.sleep} / `every()`.
|
|
165
|
+
*
|
|
166
|
+
* @param duration - Duration string (e.g. `"14d"`, `"15m"`)
|
|
167
|
+
*/
|
|
168
|
+
fromNow(duration: string): number;
|
|
169
|
+
/**
|
|
170
|
+
* Span in milliseconds (`"30d"` → `2_592_000_000`). Compose with a stored
|
|
171
|
+
* instant: `createdAt + fx.clock.duration("7d")`. Unknown strings are `0`.
|
|
172
|
+
*
|
|
173
|
+
* @param duration - Duration string (e.g. `"30d"`, `"200ms"`)
|
|
174
|
+
*/
|
|
175
|
+
duration(duration: string): number;
|
|
143
176
|
/**
|
|
144
177
|
* Durable sleep — when the flow is `durable`, journals the wake time and
|
|
145
178
|
* survives restart / deploy. Without a journal, resolves immediately
|
|
@@ -323,12 +356,36 @@ export interface JsonResult<T = unknown> {
|
|
|
323
356
|
readonly status: number;
|
|
324
357
|
readonly value?: T;
|
|
325
358
|
readonly meta?: Record<string, unknown>;
|
|
359
|
+
readonly kind?: undefined;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** SSE carrier from {@link FxJson.stream}. */
|
|
363
|
+
export interface JsonStreamResult {
|
|
364
|
+
readonly [jsonResultBrand]: true;
|
|
365
|
+
readonly kind: "stream";
|
|
366
|
+
readonly status: 200;
|
|
367
|
+
readonly chunks: AsyncIterable<string>;
|
|
368
|
+
/** Set by the kernel to commit journal / Runs after the stream settles. */
|
|
369
|
+
finalize?: () => Promise<void>;
|
|
326
370
|
}
|
|
327
371
|
|
|
328
|
-
/** True when `value` is an {@link FxJson} carrier. */
|
|
372
|
+
/** True when `value` is an {@link FxJson} JSON-envelope carrier. */
|
|
329
373
|
export function isJsonResult(value: unknown): value is JsonResult {
|
|
330
374
|
return (
|
|
331
|
-
typeof value === "object" &&
|
|
375
|
+
typeof value === "object" &&
|
|
376
|
+
value !== null &&
|
|
377
|
+
(value as JsonResult)[jsonResultBrand] === true &&
|
|
378
|
+
(value as JsonStreamResult).kind !== "stream"
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** True when `value` is an SSE stream carrier from {@link FxJson.stream}. */
|
|
383
|
+
export function isJsonStreamResult(value: unknown): value is JsonStreamResult {
|
|
384
|
+
return (
|
|
385
|
+
typeof value === "object" &&
|
|
386
|
+
value !== null &&
|
|
387
|
+
(value as JsonStreamResult)[jsonResultBrand] === true &&
|
|
388
|
+
(value as JsonStreamResult).kind === "stream"
|
|
332
389
|
);
|
|
333
390
|
}
|
|
334
391
|
|
|
@@ -363,6 +420,11 @@ export interface FxJson {
|
|
|
363
420
|
* every string field; extra keys auto-eq except `id`.
|
|
364
421
|
*/
|
|
365
422
|
withQuery<T>(rows: readonly T[], input: unknown, spec?: QueryPageSpec<T>): JsonResult<T[]>;
|
|
423
|
+
/**
|
|
424
|
+
* 200 — `text/event-stream` of JSON `data:` frames, then `data: [DONE]`.
|
|
425
|
+
* Pass {@link Fx.stream} or any async iterable of token strings.
|
|
426
|
+
*/
|
|
427
|
+
stream(chunks: AsyncIterable<string>): JsonStreamResult;
|
|
366
428
|
}
|
|
367
429
|
|
|
368
430
|
/**
|
|
@@ -567,6 +629,15 @@ export interface Fx {
|
|
|
567
629
|
* @param options - Optional emit options (`key` for per-key once ordering)
|
|
568
630
|
*/
|
|
569
631
|
emit(signal: NamedRef, payload?: unknown, options?: SignalEmitOptions): Promise<void>;
|
|
632
|
+
/**
|
|
633
|
+
* Query dead-lettered messages for one signal (records `read` on `signal:<name>`).
|
|
634
|
+
*
|
|
635
|
+
* Requires a bound signal runtime and `effects.reads` including `signal:<name>`.
|
|
636
|
+
*
|
|
637
|
+
* @param signal - Signal name or handle
|
|
638
|
+
*/
|
|
639
|
+
deadLetters<T>(signal: SignalDecl<T>): Promise<readonly DeadLetter<T>[]>;
|
|
640
|
+
deadLetters(signal: NamedRef): Promise<readonly DeadLetter[]>;
|
|
570
641
|
/**
|
|
571
642
|
* Call another flow (records `call`). Stub returns `undefined`.
|
|
572
643
|
*
|
|
@@ -651,7 +722,11 @@ export interface Fx {
|
|
|
651
722
|
*/
|
|
652
723
|
stream(
|
|
653
724
|
model: NamedRef,
|
|
654
|
-
opts?: {
|
|
725
|
+
opts?: {
|
|
726
|
+
readonly prompt?: string;
|
|
727
|
+
readonly data?: unknown;
|
|
728
|
+
readonly via?: readonly NamedRef[];
|
|
729
|
+
},
|
|
655
730
|
): AsyncIterable<string>;
|
|
656
731
|
/** Logger. */
|
|
657
732
|
readonly log: FxLog;
|
|
@@ -838,6 +913,12 @@ export interface CreateFxOptions {
|
|
|
838
913
|
readonly runId?: string;
|
|
839
914
|
/** Reveal PII through the store runtime (requires `pii:reveal` upstream). */
|
|
840
915
|
readonly revealPii?: boolean;
|
|
916
|
+
/** Trigger gate names for RLS (`oke.gate` = first policy/public). */
|
|
917
|
+
readonly rlsGateNames?: readonly string[];
|
|
918
|
+
/** Skip RLS stamp (Operator / `bypassGates` / operator-plane flows). */
|
|
919
|
+
readonly rlsBypass?: boolean;
|
|
920
|
+
/** Forced bag (Console / Call API). Wins over {@link rlsGateNames}. */
|
|
921
|
+
readonly rls?: import("../drivers/pg-rls.ts").RlsIdentity;
|
|
841
922
|
/**
|
|
842
923
|
* Active journal session when the flow is durable. Every `fx` call is
|
|
843
924
|
* recorded; `step` / `sleep` replay from the journal on resume.
|
|
@@ -952,6 +1033,19 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
952
1033
|
} satisfies FxPrincipal);
|
|
953
1034
|
const journal = options.journal;
|
|
954
1035
|
|
|
1036
|
+
function rlsInvokeContext(): { rls?: RlsIdentity } {
|
|
1037
|
+
if (options.rlsBypass === true) return {};
|
|
1038
|
+
if (options.rls) return { rls: options.rls };
|
|
1039
|
+
const identity = rlsIdentityFromAuth({
|
|
1040
|
+
userId: auth.userId,
|
|
1041
|
+
scopes: auth.scopes,
|
|
1042
|
+
gateNames: options.rlsGateNames ?? [],
|
|
1043
|
+
bypass: false,
|
|
1044
|
+
operator: false,
|
|
1045
|
+
});
|
|
1046
|
+
return identity ? { rls: identity } : {};
|
|
1047
|
+
}
|
|
1048
|
+
|
|
955
1049
|
async function gated<T>(
|
|
956
1050
|
kind: Parameters<CapabilityToken["assert"]>[0],
|
|
957
1051
|
resource: string,
|
|
@@ -1269,6 +1363,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1269
1363
|
cache.handle = await runtime.open(decl, {
|
|
1270
1364
|
effects: options.effects ?? {},
|
|
1271
1365
|
revealPii: options.revealPii,
|
|
1366
|
+
...rlsInvokeContext(),
|
|
1272
1367
|
});
|
|
1273
1368
|
}
|
|
1274
1369
|
return cache.handle;
|
|
@@ -1288,6 +1383,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1288
1383
|
{
|
|
1289
1384
|
effects: options.effects ?? {},
|
|
1290
1385
|
revealPii: options.revealPii,
|
|
1386
|
+
...rlsInvokeContext(),
|
|
1291
1387
|
},
|
|
1292
1388
|
{
|
|
1293
1389
|
gate: gated,
|
|
@@ -1336,6 +1432,15 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1336
1432
|
|
|
1337
1433
|
const clock: FxClock = {
|
|
1338
1434
|
now,
|
|
1435
|
+
ago(duration: string): number {
|
|
1436
|
+
return now() - parseDurationMs(duration);
|
|
1437
|
+
},
|
|
1438
|
+
fromNow(duration: string): number {
|
|
1439
|
+
return now() + parseDurationMs(duration);
|
|
1440
|
+
},
|
|
1441
|
+
duration(duration: string): number {
|
|
1442
|
+
return parseDurationMs(duration);
|
|
1443
|
+
},
|
|
1339
1444
|
async sleep(label: string, duration: string): Promise<void> {
|
|
1340
1445
|
if (journal) {
|
|
1341
1446
|
await journal.sleep(label, duration, parseDurationMs);
|
|
@@ -1591,9 +1696,24 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1591
1696
|
}
|
|
1592
1697
|
});
|
|
1593
1698
|
},
|
|
1699
|
+
deadLetters(signal: NamedRef) {
|
|
1700
|
+
const name = resolveName(signal);
|
|
1701
|
+
return gated("read", signalReadRef(name), async () => {
|
|
1702
|
+
if (!options.signalRuntime) {
|
|
1703
|
+
throw new Error("fx.deadLetters requires a bound signal runtime");
|
|
1704
|
+
}
|
|
1705
|
+
return options.signalRuntime.deadLetters(name);
|
|
1706
|
+
});
|
|
1707
|
+
},
|
|
1594
1708
|
call(flow, input) {
|
|
1595
1709
|
const name = resolveName(flow);
|
|
1596
1710
|
return gated("call", name, async () => {
|
|
1711
|
+
if (isMcpToolRef(name)) {
|
|
1712
|
+
if (!options.aiRuntime) {
|
|
1713
|
+
throw new Error(`fx.call: AI runtime is not configured for MCP tool "${name}"`);
|
|
1714
|
+
}
|
|
1715
|
+
return options.aiRuntime.callMcp(name, input, currentAbortSignal());
|
|
1716
|
+
}
|
|
1597
1717
|
if (options.callHandler) {
|
|
1598
1718
|
return options.callHandler(name, input);
|
|
1599
1719
|
}
|
|
@@ -1785,6 +1905,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1785
1905
|
prompt: opts?.prompt,
|
|
1786
1906
|
data: opts?.data,
|
|
1787
1907
|
signal: local.signal,
|
|
1908
|
+
via: opts?.via?.map(resolveName),
|
|
1788
1909
|
})) {
|
|
1789
1910
|
if (local.signal.aborted) break;
|
|
1790
1911
|
yield text;
|
|
@@ -1832,6 +1953,14 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1832
1953
|
},
|
|
1833
1954
|
with: jsonWith,
|
|
1834
1955
|
withQuery: jsonWithQuery,
|
|
1956
|
+
stream(chunks) {
|
|
1957
|
+
return {
|
|
1958
|
+
[jsonResultBrand]: true,
|
|
1959
|
+
kind: "stream" as const,
|
|
1960
|
+
status: 200 as const,
|
|
1961
|
+
chunks,
|
|
1962
|
+
};
|
|
1963
|
+
},
|
|
1835
1964
|
},
|
|
1836
1965
|
async step<T>(name: string, fn: () => T | Promise<T>, opts?: StepOptions<T>): Promise<T> {
|
|
1837
1966
|
if (journal) {
|
|
@@ -190,11 +190,11 @@ const charge = flow("horizontal.charge", {
|
|
|
190
190
|
});
|
|
191
191
|
|
|
192
192
|
const bindings: Binding[] = [
|
|
193
|
-
{ trigger: http.get("/ping").
|
|
193
|
+
{ trigger: http.get("/ping").public(), flow: ping as AnyFlowDef },
|
|
194
194
|
{ trigger: http.get("/rate").gate(rateGate), flow: rate as AnyFlowDef },
|
|
195
|
-
{ trigger: http.post("/write").
|
|
196
|
-
{ trigger: http.post("/emit").
|
|
197
|
-
{ trigger: http.post("/charge").
|
|
195
|
+
{ trigger: http.post("/write").public(), flow: write as AnyFlowDef },
|
|
196
|
+
{ trigger: http.post("/emit").public(), flow: emit as AnyFlowDef },
|
|
197
|
+
{ trigger: http.post("/charge").public(), flow: charge as AnyFlowDef },
|
|
198
198
|
];
|
|
199
199
|
|
|
200
200
|
const app = oke({
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Branded sentinel for `http.get()` with no path — never `"/"`.
|
|
3
|
+
* Lives in a leaf module so `oke()` can refuse leftovers without pulling
|
|
4
|
+
* the HTTP trigger constructors onto a Store-only graph.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Unresolved path token. {@link stampHttpPath} replaces it from the file tree. */
|
|
8
|
+
export const HTTP_PATH_PENDING = "__oke_http_path_pending__" as const;
|
|
9
|
+
|
|
10
|
+
/** Type of {@link HTTP_PATH_PENDING}. */
|
|
11
|
+
export type HttpPathPending = typeof HTTP_PATH_PENDING;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* True when an HTTP trigger still carries the unresolved path sentinel.
|
|
15
|
+
*
|
|
16
|
+
* @param path - Trigger path
|
|
17
|
+
*/
|
|
18
|
+
export function isPendingHttpPath(path: string): boolean {
|
|
19
|
+
return path === HTTP_PATH_PENDING;
|
|
20
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* apps do not pay for the five-verb CRUD helper.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { GATE_PUBLIC_NAME } from "../elements/gate/flatten.ts";
|
|
6
7
|
import {
|
|
7
8
|
createGateAttach,
|
|
8
9
|
createHttpTrigger,
|
|
@@ -47,6 +48,9 @@ export function httpResource<P extends string>(
|
|
|
47
48
|
{ trigger: verb("DELETE", id, false), flow: ops.remove },
|
|
48
49
|
],
|
|
49
50
|
gate: createGateAttach((next) => httpResource(path, ops, next, isLive), gates),
|
|
51
|
+
public() {
|
|
52
|
+
return httpResource(path, ops, [...gates, GATE_PUBLIC_NAME], isLive);
|
|
53
|
+
},
|
|
50
54
|
live() {
|
|
51
55
|
return httpResource(path, ops, gates, true);
|
|
52
56
|
},
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP SSE via `fx.json.stream` + client-disconnect abort of the provider fetch.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
6
|
+
import { ai } from "../elements/ai.ts";
|
|
7
|
+
import type { AiDriver } from "../drivers/ai-types.ts";
|
|
8
|
+
import { createRunsRuntime } from "../runs/runtime.ts";
|
|
9
|
+
import { createBunRuntime } from "../runtime/bun.ts";
|
|
10
|
+
import type { ServerHandle } from "../runtime/types.ts";
|
|
11
|
+
import { oke } from "./app.ts";
|
|
12
|
+
import { flow, resetFlowSeq } from "./flow.ts";
|
|
13
|
+
import { on, resetBindings } from "./on.ts";
|
|
14
|
+
import { http } from "./triggers.ts";
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
resetBindings();
|
|
18
|
+
resetFlowSeq();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function tokenDriver(chunks: readonly string[], delayMs = 0): AiDriver {
|
|
22
|
+
return {
|
|
23
|
+
id: "mock",
|
|
24
|
+
async open() {
|
|
25
|
+
return {
|
|
26
|
+
driverId: "mock" as const,
|
|
27
|
+
model: "smart",
|
|
28
|
+
async complete() {
|
|
29
|
+
return { text: "", model: "smart", driverId: "mock" as const };
|
|
30
|
+
},
|
|
31
|
+
async *stream() {
|
|
32
|
+
for (const text of chunks) {
|
|
33
|
+
if (delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
|
|
34
|
+
yield { text };
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function hangingDriver(onAbort: () => void): AiDriver {
|
|
43
|
+
return {
|
|
44
|
+
id: "mock",
|
|
45
|
+
async open() {
|
|
46
|
+
return {
|
|
47
|
+
driverId: "mock" as const,
|
|
48
|
+
model: "smart",
|
|
49
|
+
async complete() {
|
|
50
|
+
return { text: "", model: "smart", driverId: "mock" as const };
|
|
51
|
+
},
|
|
52
|
+
async *stream(opts) {
|
|
53
|
+
yield { text: "start" };
|
|
54
|
+
await new Promise<void>((resolve, reject) => {
|
|
55
|
+
const t = setTimeout(resolve, 8_000);
|
|
56
|
+
opts.signal?.addEventListener(
|
|
57
|
+
"abort",
|
|
58
|
+
() => {
|
|
59
|
+
onAbort();
|
|
60
|
+
clearTimeout(t);
|
|
61
|
+
const err = new Error("aborted");
|
|
62
|
+
err.name = "AbortError";
|
|
63
|
+
reject(err);
|
|
64
|
+
},
|
|
65
|
+
{ once: true },
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
yield { text: "never" };
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe("fx.json.stream HTTP SSE", () => {
|
|
76
|
+
let handle: ServerHandle | undefined;
|
|
77
|
+
|
|
78
|
+
afterEach(() => {
|
|
79
|
+
handle?.stop(true);
|
|
80
|
+
handle = undefined;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("encodes token chunks as SSE and leaves JSON flows buffered", async () => {
|
|
84
|
+
const smart = ai.model("smart");
|
|
85
|
+
on(
|
|
86
|
+
http.get("/chat").public(),
|
|
87
|
+
flow("chat.stream", {
|
|
88
|
+
do: (_input, fx) => fx.json.stream(fx.stream(smart, { prompt: "hi" })),
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
on(http.get("/ping").public(), flow("ping", { do: () => ({ ok: true as const }) }));
|
|
92
|
+
|
|
93
|
+
const app = oke({
|
|
94
|
+
name: "http-stream",
|
|
95
|
+
ai: { models: [smart], defaultDriver: tokenDriver(["Hel", "lo"]) },
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const streamed = await app.fetch(new Request("http://localhost/chat"));
|
|
99
|
+
expect(streamed.headers.get("content-type")).toMatch(/text\/event-stream/);
|
|
100
|
+
const body = await streamed.text();
|
|
101
|
+
expect(body).toContain('data: "Hel"');
|
|
102
|
+
expect(body).toContain('data: "lo"');
|
|
103
|
+
expect(body).toContain("data: [DONE]");
|
|
104
|
+
|
|
105
|
+
const json = await app.fetch(new Request("http://localhost/ping"));
|
|
106
|
+
expect(json.headers.get("content-type")).toMatch(/application\/json/);
|
|
107
|
+
expect(await json.json()).toEqual({ data: { ok: true }, error: null });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("client abort mid-stream aborts the upstream provider fetch", async () => {
|
|
111
|
+
let providerAborted = false;
|
|
112
|
+
const smart = ai.model("smart");
|
|
113
|
+
on(
|
|
114
|
+
http.get("/slow").public(),
|
|
115
|
+
flow("chat.slow", {
|
|
116
|
+
do: (_input, fx) => fx.json.stream(fx.stream(smart, { prompt: "long" })),
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const app = oke({
|
|
121
|
+
name: "http-stream-abort",
|
|
122
|
+
ai: { models: [smart], defaultDriver: hangingDriver(() => (providerAborted = true)) },
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
handle = createBunRuntime().serve(app, { port: 0, hostname: "127.0.0.1" });
|
|
126
|
+
const ac = new AbortController();
|
|
127
|
+
const res = await fetch(`http://127.0.0.1:${handle.port}/slow`, { signal: ac.signal });
|
|
128
|
+
expect(res.headers.get("content-type")).toMatch(/text\/event-stream/);
|
|
129
|
+
const reader = res.body!.getReader();
|
|
130
|
+
const first = await reader.read();
|
|
131
|
+
expect(first.done).toBe(false);
|
|
132
|
+
ac.abort();
|
|
133
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
134
|
+
expect(providerAborted).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("Traces stay empty until stream close; duration covers the open stream", async () => {
|
|
138
|
+
const runs = createRunsRuntime({ driver: "memory" });
|
|
139
|
+
await runs.open();
|
|
140
|
+
const smart = ai.model("smart");
|
|
141
|
+
on(
|
|
142
|
+
http.get("/chat").public(),
|
|
143
|
+
flow("chat.stream", {
|
|
144
|
+
do: (_input, fx) => fx.json.stream(fx.stream(smart, { prompt: "hi" })),
|
|
145
|
+
}),
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const app = oke({
|
|
149
|
+
name: "http-stream-traces",
|
|
150
|
+
ai: { models: [smart], defaultDriver: tokenDriver(["Hel", "lo"], 25) },
|
|
151
|
+
runs,
|
|
152
|
+
env: "test",
|
|
153
|
+
});
|
|
154
|
+
await app.boot({ env: "test", runs, startScheduler: false });
|
|
155
|
+
|
|
156
|
+
const streamed = await app.fetch(new Request("http://localhost/chat"));
|
|
157
|
+
expect(streamed.headers.get("content-type")).toMatch(/text\/event-stream/);
|
|
158
|
+
await runs.flush();
|
|
159
|
+
expect(await runs.all()).toHaveLength(0);
|
|
160
|
+
|
|
161
|
+
const body = await streamed.text();
|
|
162
|
+
expect(body).toContain("data: [DONE]");
|
|
163
|
+
|
|
164
|
+
await runs.flush();
|
|
165
|
+
const events = await runs.all();
|
|
166
|
+
expect(events).toHaveLength(1);
|
|
167
|
+
expect(events[0]!.error ?? null).toBeNull();
|
|
168
|
+
expect(events[0]!.output).toEqual({ streamed: true });
|
|
169
|
+
expect(events[0]!.durationMs).toBeGreaterThanOrEqual(40);
|
|
170
|
+
|
|
171
|
+
await app.stop();
|
|
172
|
+
await runs.close();
|
|
173
|
+
});
|
|
174
|
+
});
|
package/src/kernel/index.ts
CHANGED
|
@@ -137,6 +137,7 @@ export {
|
|
|
137
137
|
createFxContext,
|
|
138
138
|
freezePrincipal,
|
|
139
139
|
isJsonResult,
|
|
140
|
+
isJsonStreamResult,
|
|
140
141
|
jsonResultBrand,
|
|
141
142
|
resolveName,
|
|
142
143
|
resolveStoreRef,
|
|
@@ -160,6 +161,7 @@ export {
|
|
|
160
161
|
type FxThunk,
|
|
161
162
|
type JsonPage,
|
|
162
163
|
type JsonResult,
|
|
164
|
+
type JsonStreamResult,
|
|
163
165
|
type NamedRef,
|
|
164
166
|
type StepOptions,
|
|
165
167
|
} from "./fx.ts";
|
|
@@ -65,9 +65,9 @@ describe("gateDenialFailure", () => {
|
|
|
65
65
|
});
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
describe("http trigger — .
|
|
68
|
+
describe("http trigger — .public()", () => {
|
|
69
69
|
test("attaches the public sentinel without calling gate()", () => {
|
|
70
|
-
const trigger = http.get("/health").
|
|
70
|
+
const trigger = http.get("/health").public();
|
|
71
71
|
expect(trigger.gates.map((g) => (typeof g === "string" ? g : g.name))).toEqual(["public"]);
|
|
72
72
|
expect(trigger.live().isLive).toBe(true);
|
|
73
73
|
});
|
|
@@ -32,7 +32,7 @@ describe("plugin element contributions", () => {
|
|
|
32
32
|
.table("otp_codes")
|
|
33
33
|
.needs("store.sql");
|
|
34
34
|
|
|
35
|
-
on(http.get("/ok").
|
|
35
|
+
on(http.get("/ok").public(), flow("ok", { do: () => ({ ok: true }) }));
|
|
36
36
|
|
|
37
37
|
const app = oke({
|
|
38
38
|
name: "plugin-elements",
|
|
@@ -75,7 +75,7 @@ describe("plugin .needs() resolution", () => {
|
|
|
75
75
|
test("boot fails when plugin needs missing peer plugin", async () => {
|
|
76
76
|
resetBindings();
|
|
77
77
|
resetFlowSeq();
|
|
78
|
-
on(http.get("/x").
|
|
78
|
+
on(http.get("/x").public(), flow("x", { do: () => ({ ok: true }) }));
|
|
79
79
|
const dependent = plugin("needs-auth", { version: "0.0.1" }).needs("auth");
|
|
80
80
|
const app = oke({ name: "needs-boot", gate: { policies: [gate.public] } }).plug(dependent);
|
|
81
81
|
await expect(app.boot({ env: "test" })).rejects.toThrow(PluginNeedsError);
|
package/src/kernel/ready.test.ts
CHANGED
|
@@ -38,7 +38,7 @@ describe("GET /_/ready", () => {
|
|
|
38
38
|
});
|
|
39
39
|
const bindings: Binding[] = [
|
|
40
40
|
{
|
|
41
|
-
trigger: http.post("/charge").
|
|
41
|
+
trigger: http.post("/charge").public(),
|
|
42
42
|
flow: charge as AnyFlowDef,
|
|
43
43
|
},
|
|
44
44
|
];
|
|
@@ -82,7 +82,7 @@ describe("GET /_oke/client.json", () => {
|
|
|
82
82
|
startScheduler: false,
|
|
83
83
|
env: "test",
|
|
84
84
|
gate: { unguardedHttp: "allow" },
|
|
85
|
-
bindings: [{ trigger: http.get("/ping").
|
|
85
|
+
bindings: [{ trigger: http.get("/ping").public(), flow: ping as AnyFlowDef }],
|
|
86
86
|
});
|
|
87
87
|
await app.boot();
|
|
88
88
|
const res = await app.fetch(new Request("http://127.0.0.1/_oke/client.json"));
|
|
@@ -63,6 +63,20 @@ describe("http.resource — gate / live", () => {
|
|
|
63
63
|
expect(remove?.isLive).toBe(false);
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
+
test(".public().live() stamps the sentinel on every verb", () => {
|
|
67
|
+
resetBindings();
|
|
68
|
+
resetFlowSeq();
|
|
69
|
+
on(http.resource("/notes", bag()).public().live());
|
|
70
|
+
expect(httpOf("/notes", "GET")?.gates.map((g) => (typeof g === "string" ? g : g.name))).toEqual(
|
|
71
|
+
["public"],
|
|
72
|
+
);
|
|
73
|
+
expect(
|
|
74
|
+
httpOf("/notes", "POST")?.gates.map((g) => (typeof g === "string" ? g : g.name)),
|
|
75
|
+
).toEqual(["public"]);
|
|
76
|
+
expect(httpOf("/notes", "GET")?.isLive).toBe(true);
|
|
77
|
+
expect(httpOf("/notes", "POST")?.isLive).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
66
80
|
test(".live().gate(member) order does not matter", () => {
|
|
67
81
|
resetBindings();
|
|
68
82
|
resetFlowSeq();
|
|
@@ -23,7 +23,7 @@ describe("execute run duration", () => {
|
|
|
23
23
|
await runs.open();
|
|
24
24
|
|
|
25
25
|
on(
|
|
26
|
-
http.get("/ping").
|
|
26
|
+
http.get("/ping").public(),
|
|
27
27
|
flow("demo.ping", {
|
|
28
28
|
do: () => ({ ok: true }),
|
|
29
29
|
}),
|
|
@@ -68,7 +68,7 @@ describe("execute run duration", () => {
|
|
|
68
68
|
await runs.open();
|
|
69
69
|
try {
|
|
70
70
|
on(
|
|
71
|
-
http.get("/ping").
|
|
71
|
+
http.get("/ping").public(),
|
|
72
72
|
flow("demo.ping", {
|
|
73
73
|
do: () => ({ ok: true }),
|
|
74
74
|
}),
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-place path / name stamps and fail-loud HTTP boot.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test, beforeEach } from "bun:test";
|
|
6
|
+
import type { RuntimeRouteMap } from "./adopt-routes.ts";
|
|
7
|
+
import { oke } from "./app.ts";
|
|
8
|
+
import { flow, resetFlowSeq } from "./flow.ts";
|
|
9
|
+
import { resetRegisteredFlowUnits, registerFlowUnits } from "./flow-units.ts";
|
|
10
|
+
import { listBindings, on, resetBindings } from "./on.ts";
|
|
11
|
+
import { stampFlowName, stampHttpPath } from "./stamp-http.ts";
|
|
12
|
+
import { HTTP_PATH_PENDING } from "./http-path-pending.ts";
|
|
13
|
+
import { http } from "./triggers.ts";
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
resetBindings();
|
|
17
|
+
resetFlowSeq();
|
|
18
|
+
resetRegisteredFlowUnits();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("stampHttpPath", () => {
|
|
22
|
+
test("mutates the on() binding in place — listBindings sees the stamped path", () => {
|
|
23
|
+
const f = on(http.get().public(), flow({ do: () => ({ ok: true }) }));
|
|
24
|
+
expect(f.$trigger && f.$trigger.kind === "http" && f.$trigger.path).toBe(HTTP_PATH_PENDING);
|
|
25
|
+
stampHttpPath(f, "/notes/:id");
|
|
26
|
+
expect(f.$trigger && f.$trigger.kind === "http" && f.$trigger.path).toBe("/notes/:id");
|
|
27
|
+
const bound = listBindings()[0];
|
|
28
|
+
expect(bound?.trigger.kind === "http" && bound.trigger.path).toBe("/notes/:id");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("explicit http.get('/x') wins — stamp does not overwrite", () => {
|
|
32
|
+
const f = on(http.get("/me/tasks").public(), flow("notes.get", { do: () => 1 }));
|
|
33
|
+
stampHttpPath(f, "/notes/:id");
|
|
34
|
+
expect(f.$trigger && f.$trigger.kind === "http" && f.$trigger.path).toBe("/me/tasks");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("stampFlowName", () => {
|
|
39
|
+
test("fills a nameless flow as unit.export", () => {
|
|
40
|
+
const f = flow({ do: () => 1 });
|
|
41
|
+
expect(f.name).toBe("");
|
|
42
|
+
stampFlowName(f, "notes.get");
|
|
43
|
+
expect(f.name).toBe("notes.get");
|
|
44
|
+
expect(f.unit).toBe("notes");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("explicit flow('notes.get') wins", () => {
|
|
48
|
+
const f = flow("notes.get", { do: () => 1 });
|
|
49
|
+
stampFlowName(f, "notes.list");
|
|
50
|
+
expect(f.name).toBe("notes.get");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("oke — unresolved sentinel and duplicate routes", () => {
|
|
55
|
+
test("unresolved http.get() fails construction", () => {
|
|
56
|
+
on(http.get().public(), flow("notes.get", { do: () => 1 }));
|
|
57
|
+
expect(() => oke({ name: "t", autoBoot: false })).toThrow(/OKE1010/);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("duplicate GET /notes fails construction", () => {
|
|
61
|
+
on(http.get("/notes").public(), flow("notes.list", { do: () => 1 }));
|
|
62
|
+
on(http.get("/notes").public(), flow("notes.also", { do: () => 1 }));
|
|
63
|
+
expect(() => oke({ name: "t", autoBoot: false })).toThrow(/OKE1011/);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("nameless HTTP flow fails construction", () => {
|
|
67
|
+
on(http.get("/notes").public(), flow({ do: () => 1 }));
|
|
68
|
+
expect(() => oke({ name: "t", autoBoot: false })).toThrow(/OKE1012/);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("registerFlowUnits drains into $routes; registry ignore does not", () => {
|
|
72
|
+
const get = on(http.get("/notes/:id").public(), flow("notes.get", { do: () => 1 }));
|
|
73
|
+
registerFlowUnits({ notes: { get } });
|
|
74
|
+
const ignored = oke({ name: "t", autoBoot: false, registry: "ignore" });
|
|
75
|
+
expect(ignored.$routes).toEqual({});
|
|
76
|
+
const app = oke({ name: "t", autoBoot: false });
|
|
77
|
+
expect((app.$routes as RuntimeRouteMap).notes?.get?.path).toBe("/notes/:id");
|
|
78
|
+
});
|
|
79
|
+
});
|