okengine 0.17.2 → 0.18.3
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/README.md +9 -5
- package/manifest.v1.schema.json +61 -2
- package/package.json +18 -4
- package/site/content/docs/elements/clock.mdx +1 -1
- package/site/content/docs/elements/flow.mdx +25 -1
- package/site/content/docs/elements/store.mdx +287 -341
- package/site/content/docs/elements/vault.mdx +5 -5
- package/site/content/docs/get-started/installation.mdx +1 -2
- package/site/content/docs/get-started/introduction.mdx +54 -110
- package/site/content/docs/get-started/meta.json +9 -1
- package/site/content/docs/get-started/testing.mdx +328 -0
- package/site/content/docs/get-started/why.mdx +94 -70
- package/site/content/docs/index.mdx +1 -1
- package/site/content/docs/plugins/apple.mdx +151 -0
- package/site/content/docs/plugins/discord.mdx +139 -0
- package/site/content/docs/plugins/facebook.mdx +134 -0
- package/site/content/docs/plugins/figma.mdx +138 -0
- package/site/content/docs/plugins/github.mdx +138 -0
- package/site/content/docs/plugins/google.mdx +153 -0
- package/site/content/docs/plugins/index.mdx +47 -1
- package/site/content/docs/plugins/meta.json +10 -0
- package/site/content/docs/plugins/microsoft.mdx +151 -0
- package/site/content/docs/plugins/oauth.mdx +188 -0
- package/site/content/docs/plugins/x.mdx +125 -0
- package/site/content/docs/reference/cli.md +3 -2
- package/site/content/docs/reference/client.mdx +58 -1
- package/site/content/docs/reference/configuration.mdx +2 -4
- package/site/content/docs/reference/fx.mdx +3 -1
- package/site/content/docs/reference/index.mdx +0 -5
- package/site/content/docs/reference/meta.json +2 -2
- package/site/content/docs/reference/okid.mdx +137 -0
- package/src/auth/bindings.ts +1 -1
- package/src/auth/config.ts +9 -0
- package/src/auth/identity-sql.ts +314 -0
- package/src/auth/identity.ts +140 -2
- package/src/auth/index.ts +17 -1
- package/src/auth/method-context.ts +3 -0
- package/src/auth/oauth-as/cimd.ts +132 -0
- package/src/auth/oauth-as/crypto.test.ts +101 -0
- package/src/auth/oauth-as/crypto.ts +393 -0
- package/src/auth/oauth-as/errors.ts +68 -0
- package/src/auth/oauth-as/http.test.ts +419 -0
- package/src/auth/oauth-as/http.ts +842 -0
- package/src/auth/oauth-as/stores.ts +61 -0
- package/src/auth/oauth-as/tables.ts +142 -0
- package/src/auth/tables.ts +0 -11
- package/src/bench/README.md +83 -0
- package/src/bench/REPORT.md +176 -0
- package/src/bench/g01-rls-stamp.bench.ts +194 -0
- package/src/bench/g02-clock-per-tenant.bench.ts +158 -0
- package/src/bench/g03-signal-once.bench.ts +157 -0
- package/src/bench/g03-signal-reconnect.bench.ts +254 -0
- package/src/bench/g03-signal-sse-memory.bench.ts +191 -0
- package/src/bench/g04-auth-vault-hotpath.bench.ts +170 -0
- package/src/bench/g05-sustained-full.bench.ts +265 -0
- package/src/bench/g06-mixed-load.bench.ts +260 -0
- package/src/bench/g07-vault-crypto.bench.ts +100 -0
- package/src/bench/g07-vault-rotate-under-read.bench.ts +285 -0
- package/src/bench/g08-conn-oversubscribe.bench.ts +194 -0
- package/src/bench/g08-store-kv-durable.bench.ts +133 -0
- package/src/bench/g08-store-sql.bench.ts +178 -0
- package/src/bench/g09-journal-sustained.bench.ts +203 -0
- package/src/bench/g10-observability-contention.bench.ts +246 -0
- package/src/bench/g11-cold-start-cycle.bench.ts +164 -0
- package/src/bench/g13-elements.bench.ts +427 -0
- package/src/bench/g14-graceful-shutdown.bench.ts +244 -0
- package/src/bench/g15-postgres-degradation.bench.ts +264 -0
- package/src/bench/g16-live-query-fanout.bench.ts +206 -0
- package/src/bench/lib/event-loop-lag.ts +26 -0
- package/src/bench/lib/infra.ts +60 -0
- package/src/bench/lib/report.ts +52 -0
- package/src/bench/lib/rss-sampler.ts +61 -0
- package/src/bench/lib/signal-pg.ts +88 -0
- package/src/bench/load-app.ts +337 -0
- package/src/bench/load-child.ts +108 -0
- package/src/bench/smoke.bench.ts +43 -0
- package/src/cli/competitor-mention-removal.test.ts +28 -0
- package/src/cli/doctor-fd.ts +117 -0
- package/src/cli/doctor.test.ts +192 -0
- package/src/cli/doctor.ts +129 -1
- package/src/client/create.ts +95 -1
- package/src/client/index.ts +9 -2
- package/src/client/transport.ts +11 -4
- package/src/client/use-live-query.ts +154 -0
- package/src/client-react/index.ts +15 -1
- package/src/client-react/live-resource.ts +246 -0
- package/src/client-react/use-live-query.test.ts +475 -0
- package/src/client-react/use-live-query.ts +530 -0
- package/src/compiler/extract.test.ts +518 -0
- package/src/compiler/extract.ts +386 -19
- package/src/console/server/invoke-user-flow.ts +2 -1
- package/src/console/ui-next/dist/assets/{access-page-DnWbnGzq.js → access-page-DY4N6nnk.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-BiZ4-6yQ.js → flows-page-CsPDMrVM.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-C8NRK2R-.js → index-CcTDXHuz.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-CrB6vd1T.js → observability-page-CKR595wP.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-CS5-aETQ.js → store-page-02xOiqIK.js} +3 -3
- package/src/console/ui-next/dist/assets/{units-page-CjtdlW8l.js → units-page-CpPFFKyE.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-C6Xxm9SA.js → vault-page-BsMf-9_W.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +36 -2
- package/src/drivers/cdc-outbox.ts +389 -0
- package/src/drivers/memory.ts +20 -0
- package/src/drivers/oauth-apple.ts +156 -0
- package/src/drivers/oauth-discord.ts +79 -0
- package/src/drivers/oauth-facebook.ts +80 -0
- package/src/drivers/oauth-figma.ts +116 -0
- package/src/drivers/oauth-github.ts +92 -0
- package/src/drivers/oauth-google.ts +142 -0
- package/src/drivers/oauth-microsoft.ts +174 -0
- package/src/drivers/oauth-oidc.ts +293 -0
- package/src/drivers/oauth-shared.ts +326 -0
- package/src/drivers/oauth-types.ts +159 -0
- package/src/drivers/oauth-x.ts +77 -0
- package/src/drivers/oauth2-common.ts +95 -0
- package/src/drivers/oauth2-token.ts +61 -0
- package/src/drivers/pg-rls-row-passes.ts +251 -0
- package/src/drivers/pg-rls.ts +2 -0
- package/src/drivers/postgres.ts +45 -2
- package/src/drivers/signal-postgres.ts +2 -1
- package/src/elements/channel/runtime.ts +29 -2
- package/src/elements/channel.test.ts +52 -0
- package/src/elements/gate/boot.ts +29 -2
- package/src/elements/store/emit-drizzle.ts +147 -14
- package/src/elements/store/field-ddl.test.ts +118 -0
- package/src/elements/store/field-types.test.ts +455 -0
- package/src/elements/store/list-query.golden.json +777 -0
- package/src/elements/store/list-query.parity.test.ts +396 -0
- package/src/elements/store/list-query.ts +792 -0
- package/src/elements/store/live-default.test.ts +136 -0
- package/src/elements/store/live-http.test.ts +160 -0
- package/src/elements/store/live-isolation.test.ts +291 -0
- package/src/elements/store/live-query-runtime.test.ts +323 -0
- package/src/elements/store/live-query-runtime.ts +403 -0
- package/src/elements/store/live-query-server.test.ts +377 -0
- package/src/elements/store/live-query-server.ts +102 -0
- package/src/elements/store/live-query.ts +97 -0
- package/src/elements/store/resource.ts +189 -680
- package/src/elements/store/rls-row-passes-policies.parity.test.ts +665 -0
- package/src/elements/store/schema-decl.ts +539 -41
- package/src/elements/store/sql-rls-stamp.test.ts +27 -0
- package/src/elements/store/sql-session.ts +297 -35
- package/src/elements/store/table.ts +102 -21
- package/src/elements/store.test.ts +3 -1
- package/src/elements/store.ts +12 -1
- package/src/elements/vault/chaos-child.ts +74 -1
- package/src/elements/vault/chaos.test.ts +4 -2
- package/src/elements/vault/storage.ts +4 -2
- package/src/index.ts +4 -1
- package/src/kernel/app-auth.ts +1 -0
- package/src/kernel/app.ts +116 -2
- package/src/kernel/auth-sharing.test.ts +196 -0
- package/src/kernel/boot.test.ts +3 -3
- package/src/kernel/errors.ts +8 -0
- package/src/kernel/fx.test.ts +1 -0
- package/src/kernel/fx.ts +14 -2
- package/src/kernel/horizontal-child.ts +2 -1
- package/src/kernel/http-resource.ts +33 -7
- package/src/kernel/identity-host-persist.test.ts +119 -0
- package/src/kernel/instance-id.ts +4 -2
- package/src/kernel/journal.ts +2 -1
- package/src/kernel/mcp-tool.test.ts +95 -0
- package/src/kernel/on.ts +9 -0
- package/src/kernel/realtime-bind.ts +326 -0
- package/src/kernel/resource-live.ts +117 -0
- package/src/kernel/triggers.ts +86 -4
- package/src/manifest/diff.ts +37 -0
- package/src/manifest/types.ts +64 -2
- package/src/okid.bench.test.ts +64 -0
- package/src/okid.test.ts +338 -0
- package/src/okid.ts +245 -0
- package/src/plugins/anonymous.ts +19 -1
- package/src/plugins/auth/shared.ts +15 -0
- package/src/plugins/index.ts +2 -0
- package/src/plugins/magic-link.ts +10 -8
- package/src/plugins/mcp-oauth.ts +208 -0
- package/src/plugins/oauth/flow-store.ts +117 -0
- package/src/plugins/oauth/link.ts +69 -0
- package/src/plugins/oauth/shared.ts +108 -0
- package/src/plugins/oauth/token-vault.ts +100 -0
- package/src/plugins/oauth.security.test.ts +535 -0
- package/src/plugins/oauth.ts +532 -0
- package/src/plugins/otp.ts +48 -6
- package/src/plugins/passkey.ts +20 -1
- package/src/plugins/two-factor.ts +11 -0
- package/src/plugins/username.ts +40 -7
- package/src/release/build-lib.ts +7 -1
- package/src/release/measure.ts +1 -0
- package/src/release/official-plugins.ts +4 -1
- package/src/runs/collect.ts +2 -1
- package/src/runs/drivers/files.ts +2 -1
- package/src/test/create-test-app.ts +114 -5
- package/src/test/export-bundle.test.ts +33 -0
- package/src/test/live-signals.test.ts +83 -0
- package/src/test/tenant-isolation.test.ts +175 -0
- package/src/testing.ts +26 -0
- package/src/upgrade/codemods.ts +1 -1
- package/site/content/docs/reference/migrating-environments.mdx +0 -158
|
@@ -173,8 +173,8 @@ A value already in the vault / managed backend wins over a leftover env pin.
|
|
|
173
173
|
If every layer misses a contract, boot **fails** before any request is served:
|
|
174
174
|
|
|
175
175
|
```text
|
|
176
|
-
|
|
177
|
-
STRIPE_KEY
|
|
176
|
+
vault boot failed — 1 missing secret(s):
|
|
177
|
+
- STRIPE_KEY: Payments gateway key
|
|
178
178
|
```
|
|
179
179
|
|
|
180
180
|
<Callout title="Good to know">
|
|
@@ -242,9 +242,9 @@ Backup stays on the CLI (`oke vault backup` / `restore`). The Console never
|
|
|
242
242
|
accepts a master key in the HTTP body — unseal via `OKE_VAULT_MASTER_KEY`.
|
|
243
243
|
|
|
244
244
|
<Callout title="Builtin concurrency (rotate / audit / backup)">
|
|
245
|
-
`rotate-master`
|
|
246
|
-
|
|
247
|
-
|
|
245
|
+
`rotate-master` holds a lease (`FOR UPDATE` + lease TTL) so exactly one batch runs at a time; a
|
|
246
|
+
busy audit append briefly waits rather than being misread as "lease held". Audit appends serialize
|
|
247
|
+
under a transactional write lock; backups use temp → fsync → rename with end marker + checksum.
|
|
248
248
|
</Callout>
|
|
249
249
|
|
|
250
250
|
<Callout title="Builtin master keys and expired rows">
|
|
@@ -146,7 +146,7 @@ Uncommented supported controls in the generated compose env are preserved when
|
|
|
146
146
|
`oke dev` regenerates the file.
|
|
147
147
|
|
|
148
148
|
Migrating from `local` / `docker` maps or `oke mode`?
|
|
149
|
-
|
|
149
|
+
Rename the env keys in `oke.config.ts` to `dev` / `test` / `prod`.
|
|
150
150
|
|
|
151
151
|
## Troubleshooting
|
|
152
152
|
|
|
@@ -198,7 +198,6 @@ See [Migrating environments](/docs/reference/migrating-environments).
|
|
|
198
198
|
- [CLI Reference](/docs/reference/cli) — `oke`, `create-oke`, `oke test`, and db commands
|
|
199
199
|
- [Environment variables](/docs/reference/environment-variables) — protocol-shaped env
|
|
200
200
|
- [Configuration](/docs/reference/configuration) — `oke.config.ts` drivers and `db.autoPush`
|
|
201
|
-
- [Migrating environments](/docs/reference/migrating-environments) — four-key maps → three
|
|
202
201
|
|
|
203
202
|
## Next
|
|
204
203
|
|
|
@@ -5,25 +5,20 @@ source: docs/spec/unified-theory.md
|
|
|
5
5
|
icon: BookOpen
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
most stacks those are four frameworks. In OKE they are **one species** with one
|
|
10
|
-
shape. Learn the shape once; only the trigger changes.
|
|
8
|
+
## The One Idea
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
**Every backend behavior is a Flow:** `on(Trigger) → Effects`. There are no separate species
|
|
14
|
-
called endpoints, handlers, consumers, jobs, or workflows.
|
|
15
|
-
</Callout>
|
|
10
|
+
A booking API, a nightly cleanup job, a receipt email, a row-change hook — in most stacks those are four frameworks. In OKE they are **one species** with one shape. Learn the shape once; only the trigger changes.
|
|
16
11
|
|
|
17
|
-
|
|
12
|
+
> **The one rule**
|
|
13
|
+
> Every backend behavior is a **Flow**: `on(Trigger) → Effects`. There are no separate species called endpoints, handlers, consumers, jobs, or workflows.
|
|
18
14
|
|
|
19
|
-
|
|
15
|
+
## Quick Start
|
|
20
16
|
|
|
21
|
-
|
|
22
|
-
### Write one Flow
|
|
17
|
+
### Write One Flow
|
|
23
18
|
|
|
24
19
|
Four contracts plus a `do`. This is the standard starter's health check:
|
|
25
20
|
|
|
26
|
-
```typescript
|
|
21
|
+
```typescript
|
|
27
22
|
import { on, flow, http } from "okengine";
|
|
28
23
|
import { z } from "zod";
|
|
29
24
|
|
|
@@ -38,45 +33,29 @@ export const health = on(
|
|
|
38
33
|
|
|
39
34
|
`main` omits the URL prefix, so this file is `GET /health` named `main.health`. See [Project structure](/docs/get-started/project-structure). Explicit `http.get("/health")` and `flow("main.health")` still win.
|
|
40
35
|
|
|
41
|
-
|
|
36
|
+
### Change Only the Trigger
|
|
42
37
|
|
|
43
|
-
|
|
44
|
-
### Change only the trigger
|
|
38
|
+
Triggers define what starts the flow. Changing the trigger replaces the entire behavior while keeping the same structure:
|
|
45
39
|
|
|
46
40
|
| Trigger | Starts when | Replaces |
|
|
47
41
|
| ---------------------------------- | ------------------ | ------------------ |
|
|
48
|
-
| `http.post("/bookings")` |
|
|
49
|
-
| `every("10m")` |
|
|
50
|
-
| `orderPlaced` (a signal) |
|
|
51
|
-
| `db.table(users).changed("email")` |
|
|
42
|
+
| `http.post("/bookings")` | A request arrives | Endpoint · handler |
|
|
43
|
+
| `every("10m")` | Time passes | Cron job |
|
|
44
|
+
| `orderPlaced` (a signal) | Another flow emits | Queue consumer |
|
|
45
|
+
| `db.table(users).changed("email")` | A row changes | CDC pipeline |
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { on, flow, http } from "okengine";
|
|
52
49
|
|
|
53
|
-
```typescript title="triggers"
|
|
54
50
|
on(http.post("/bookings"), createBooking);
|
|
55
51
|
on(every("10m"), expireStale);
|
|
56
52
|
on(orderPlaced, sendReceipt);
|
|
57
53
|
on(db.table(users).changed("email"), reverify);
|
|
58
54
|
```
|
|
59
55
|
|
|
60
|
-
</Step>
|
|
61
|
-
|
|
62
|
-
<Step>
|
|
63
|
-
### See what was derived
|
|
64
|
-
|
|
65
|
-
Run `oke dev`. Four surfaces come up together — nothing configured by hand in
|
|
66
|
-
a separate dashboard.
|
|
67
|
-
|
|
68
|
-
<Surfaces />
|
|
69
|
-
|
|
70
|
-
</Step>
|
|
71
|
-
|
|
72
|
-
</Steps>
|
|
73
|
-
|
|
74
56
|
## Anatomy
|
|
75
57
|
|
|
76
|
-
One pipeline. Only the trigger changes between an endpoint, a job, a consumer,
|
|
77
|
-
and a row hook.
|
|
78
|
-
|
|
79
|
-
<FlowShape />
|
|
58
|
+
One pipeline. Only the trigger changes between an endpoint, a job, a consumer, and a row hook.
|
|
80
59
|
|
|
81
60
|
| Piece | Role |
|
|
82
61
|
| ------------- | -------------------------------------------------------------- |
|
|
@@ -85,33 +64,25 @@ and a row hook.
|
|
|
85
64
|
| **`do`** | The body — every read, write, emit, and call goes through `fx` |
|
|
86
65
|
| **Effects** | Inferred from those `fx` calls — not hand-annotated |
|
|
87
66
|
|
|
88
|
-
|
|
89
|
-
| ---------------------------------- | -------------- |
|
|
90
|
-
| One documentation path | One species |
|
|
91
|
-
| One trace shape | One Flow model |
|
|
92
|
-
| One thing for an AI agent to learn | One law |
|
|
93
|
-
|
|
94
|
-
## The `fx` rule
|
|
67
|
+
Consequence: One documentation path, one trace shape, one Flow model, one thing for an AI agent to learn.
|
|
95
68
|
|
|
96
|
-
|
|
97
|
-
`fetch` directly, or uses `Date.now()` is a defect.
|
|
69
|
+
## The `fx` Rule
|
|
98
70
|
|
|
99
|
-
|
|
100
|
-
| -------------------------- | ------------------------ |
|
|
101
|
-
| Cache invalidation keys | Yes |
|
|
102
|
-
| Live queries | Yes |
|
|
103
|
-
| Least-privilege tokens | Yes |
|
|
104
|
-
| Deterministic tests | Yes |
|
|
105
|
-
| Manifest Diff | Yes |
|
|
71
|
+
**All world access goes through `fx`.** A Flow that imports `node:fs`, calls `fetch` directly, or uses `Date.now()` is a defect.
|
|
106
72
|
|
|
107
|
-
|
|
73
|
+
| Inferred From | With `fx` |
|
|
74
|
+
| ----------------------- | --------- |
|
|
75
|
+
| Cache invalidation keys | Yes |
|
|
76
|
+
| Live queries | Yes |
|
|
77
|
+
| Least-privilege tokens | Yes |
|
|
78
|
+
| Deterministic tests | Yes |
|
|
79
|
+
| Manifest Diff | Yes |
|
|
108
80
|
|
|
109
|
-
|
|
110
|
-
your system. You do not maintain a second source of truth.
|
|
81
|
+
## What the Manifest Derives
|
|
111
82
|
|
|
112
|
-
|
|
83
|
+
At build time OKE extracts a **Manifest** — a machine-readable description of your system. You do not maintain a second source of truth.
|
|
113
84
|
|
|
114
|
-
| Surface | Port /
|
|
85
|
+
| Surface | Port / Place | You Maintain? |
|
|
115
86
|
| --------------------------------- | ------------------ | ----------------------------- |
|
|
116
87
|
| Typed client (`okengine/client`) | your app code | No separate codegen |
|
|
117
88
|
| Console panels, traces, explorers | `:6533` | No |
|
|
@@ -119,47 +90,38 @@ your system. You do not maintain a second source of truth.
|
|
|
119
90
|
| Architecture diagram | Console | No — it _is_ the effect graph |
|
|
120
91
|
| Capability matrix + cache keys | compiler / runtime | No — from `fx` |
|
|
121
92
|
|
|
122
|
-
## Eight
|
|
93
|
+
## The Eight Elements
|
|
123
94
|
|
|
124
|
-
An element earns its place only if it has **irreducible physics**. New
|
|
125
|
-
infrastructure becomes a new **driver** — never a ninth element.
|
|
95
|
+
An element earns its place only if it has **irreducible physics**. New infrastructure becomes a new **driver** — never a ninth element.
|
|
126
96
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
| Element | Essence | Replaces the zoo of |
|
|
97
|
+
| Element | Essence | Replaces the Zoo |
|
|
130
98
|
| ----------- | ----------------------------- | --------------------------------------------------------- |
|
|
131
|
-
| **Flow** |
|
|
132
|
-
| **Signal** |
|
|
133
|
-
| **Store** |
|
|
134
|
-
| **Clock** |
|
|
135
|
-
| **Gate** |
|
|
136
|
-
| **Vault** |
|
|
137
|
-
| **Channel** |
|
|
138
|
-
| **AI** |
|
|
99
|
+
| **Flow** | Behavior | endpoint · handler · consumer · job · workflow · webhook |
|
|
100
|
+
| **Signal** | Data in motion | queue · pub/sub · stream · websocket · SSE · event bus |
|
|
101
|
+
| **Store** | Data at rest | database · cache · KV · file storage · search index |
|
|
102
|
+
| **Clock** | Time | cron · delay · timeout · durable sleep · TTL |
|
|
103
|
+
| **Gate** | Permission to act | auth · session · ABAC · rate limit · quota · feature flag |
|
|
104
|
+
| **Vault** | Protected knowledge | secrets · config · environment |
|
|
105
|
+
| **Channel** | Reaching humans | email · SMS · WhatsApp · push |
|
|
106
|
+
| **AI** | Reaching machine intelligence | model calls · prompts · embeddings · agents · RAG |
|
|
139
107
|
|
|
140
|
-
## Ten
|
|
108
|
+
## Ten Exports
|
|
141
109
|
|
|
142
|
-
The entire public vocabulary fits in one import. Everything else in the docs is
|
|
143
|
-
derived from these ten names:
|
|
110
|
+
The entire public vocabulary fits in one import. Everything else in the docs is derived from these ten names:
|
|
144
111
|
|
|
145
|
-
```ts
|
|
112
|
+
```ts
|
|
146
113
|
import { on, flow, signal, store, clock, gate, vault, channel, ai, plugin } from "okengine";
|
|
147
114
|
```
|
|
148
115
|
|
|
149
|
-
For HTTP-only apps, prefer the slim entry so the cold graph stays on the edge
|
|
150
|
-
router:
|
|
116
|
+
For HTTP-only apps, prefer the slim entry so the cold graph stays on the edge:
|
|
151
117
|
|
|
152
|
-
```ts
|
|
118
|
+
```ts
|
|
153
119
|
import { on, flow, http, gate, oke, createBunRuntime } from "okengine/http";
|
|
154
120
|
```
|
|
155
121
|
|
|
156
|
-
Heavy surfaces (
|
|
157
|
-
mega-barrel) live on `okengine/runs`, `okengine/journal`, `okengine/compiler`,
|
|
158
|
-
and `okengine/full`.
|
|
159
|
-
|
|
160
|
-
<Vocabulary />
|
|
122
|
+
Heavy surfaces (runs, journal factories, compiler extract, the legacy mega-barrel) live on `okengine/runs`, `okengine/journal`, `okengine/compiler`, and `okengine/full`.
|
|
161
123
|
|
|
162
|
-
## Learn
|
|
124
|
+
## Learn More
|
|
163
125
|
|
|
164
126
|
| Topic | Page |
|
|
165
127
|
| ------- | ------------------------------------------------------------------------------- |
|
|
@@ -169,27 +131,9 @@ and `okengine/full`.
|
|
|
169
131
|
| `fx` | [fx](/docs/reference/fx) |
|
|
170
132
|
| Agents | [MCP](/docs/ai/mcp) · [Skills](/docs/ai/skills) · [llms.txt](/docs/ai/llms-txt) |
|
|
171
133
|
|
|
172
|
-
## Next
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
href="/docs/get-started/why"
|
|
179
|
-
/>
|
|
180
|
-
<Card
|
|
181
|
-
title="Installation"
|
|
182
|
-
description="Scaffold with create-oke and open the Console."
|
|
183
|
-
href="/docs/get-started/installation"
|
|
184
|
-
/>
|
|
185
|
-
<Card
|
|
186
|
-
title="Basic Usage"
|
|
187
|
-
description="Health Flow, typed client, and bun:test."
|
|
188
|
-
href="/docs/get-started/basic-usage"
|
|
189
|
-
/>
|
|
190
|
-
<Card
|
|
191
|
-
title="Project structure"
|
|
192
|
-
description="Folders are the URL; unit plus export is the name."
|
|
193
|
-
href="/docs/get-started/project-structure"
|
|
194
|
-
/>
|
|
195
|
-
</Cards>
|
|
134
|
+
## Next Steps
|
|
135
|
+
|
|
136
|
+
- [Why OKE](/docs/get-started/why)
|
|
137
|
+
- [Installation](/docs/get-started/installation)
|
|
138
|
+
- [Basic Usage](/docs/get-started/basic-usage)
|
|
139
|
+
- [Project Structure](/docs/get-started/project-structure)
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"title": "Get Started",
|
|
3
3
|
"icon": "Rocket",
|
|
4
|
-
"pages": [
|
|
4
|
+
"pages": [
|
|
5
|
+
"index",
|
|
6
|
+
"introduction",
|
|
7
|
+
"why",
|
|
8
|
+
"installation",
|
|
9
|
+
"basic-usage",
|
|
10
|
+
"testing",
|
|
11
|
+
"project-structure"
|
|
12
|
+
]
|
|
5
13
|
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Testing
|
|
3
|
+
description: Write deterministic tests for your Flows with createTestApp — freeze the clock, mock AI, drain signals, and prove side effects.
|
|
4
|
+
icon: FlaskConical
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
`createTestApp` boots the **same app** you deploy, but with memory drivers and a
|
|
8
|
+
frozen clock. You drive it through `t.api`, `t.auth`, `t.signals`, `t.clock`,
|
|
9
|
+
and `t.channels` — every assertion is deterministic.
|
|
10
|
+
|
|
11
|
+
<Callout title="The one rule">
|
|
12
|
+
Boot the same `app` your server boots. Assert only through the harness surfaces (`t.api`,
|
|
13
|
+
`t.auth`, `t.signals`, `t.clock`, `t.channels`, `t.ai`, `t.effects`) — never reach into internal
|
|
14
|
+
runtimes.
|
|
15
|
+
</Callout>
|
|
16
|
+
|
|
17
|
+
## Import
|
|
18
|
+
|
|
19
|
+
The harness ships as a separate subpath so production bundles stay small:
|
|
20
|
+
|
|
21
|
+
```typescript title="tests/orders.test.ts"
|
|
22
|
+
import { describe, expect, test } from "bun:test";
|
|
23
|
+
import { createTestApp } from "okengine/testing"; // or "okengine/test"
|
|
24
|
+
import { app } from "../src/app";
|
|
25
|
+
|
|
26
|
+
test("orders flow", async () => {
|
|
27
|
+
const t = await createTestApp(app);
|
|
28
|
+
// ...
|
|
29
|
+
await t.close();
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Both `okengine/testing` and `okengine/test` are aliases. Use `okengine/testing`
|
|
34
|
+
(→ `dist/testing.js`) for published packages and hoisted monorepos; `okengine/test`
|
|
35
|
+
is the source-only path older starters use.
|
|
36
|
+
|
|
37
|
+
## Quick start
|
|
38
|
+
|
|
39
|
+
<Steps>
|
|
40
|
+
|
|
41
|
+
<Step>
|
|
42
|
+
### Adopt your Flows
|
|
43
|
+
|
|
44
|
+
Every behavior on the app is reachable. No separate test database or server.
|
|
45
|
+
|
|
46
|
+
```typescript title="src/flows/orders/index.ts"
|
|
47
|
+
import { on, flow, http } from "okengine";
|
|
48
|
+
import { z } from "zod";
|
|
49
|
+
|
|
50
|
+
export const create = on(
|
|
51
|
+
http.post("/orders").gate(member),
|
|
52
|
+
flow("orders.create", {
|
|
53
|
+
in: z.object({ sku: z.string(), qty: z.number() }),
|
|
54
|
+
out: z.object({ id: z.string() }),
|
|
55
|
+
do: async (input, fx) => {
|
|
56
|
+
const id = fx.id();
|
|
57
|
+
await fx.emit(orderPlaced, { orderId: id, ...input });
|
|
58
|
+
return { id };
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
</Step>
|
|
65
|
+
|
|
66
|
+
<Step>
|
|
67
|
+
### Boot the harness
|
|
68
|
+
|
|
69
|
+
```typescript title="tests/orders.test.ts"
|
|
70
|
+
const t = await createTestApp(app, { gates: [member] });
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
</Step>
|
|
74
|
+
|
|
75
|
+
<Step>
|
|
76
|
+
### Login and call
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
const u = await t.auth.loginAs({ scopes: ["order:create"] });
|
|
80
|
+
const { data, error } = await t.api.orders.create({ sku: "COFFEE", qty: 2 }, { as: u });
|
|
81
|
+
expect(error).toBeNull();
|
|
82
|
+
expect(data).toEqual({ id: expect.any(String) });
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
</Step>
|
|
86
|
+
|
|
87
|
+
<Step>
|
|
88
|
+
### Advance time and drain signals
|
|
89
|
+
|
|
90
|
+
Signal handlers and Clocks are processed deterministically:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
await t.signals.drain(); // run queued signal work
|
|
94
|
+
await t.clock.advance("2m"); // tick scheduled Clocks forward
|
|
95
|
+
expect(t.channels.sent()).toContainEqual(expect.objectContaining({ template: "order-confirmed" }));
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
</Step>
|
|
99
|
+
|
|
100
|
+
</Steps>
|
|
101
|
+
|
|
102
|
+
## Harness surface
|
|
103
|
+
|
|
104
|
+
| Property | What it does |
|
|
105
|
+
| ------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
106
|
+
| `t.api.<unit>.<flow>(input, opts?)` | Invoke an adopted Flow directly with a `{ as }` principal and optional `{ tenant }` |
|
|
107
|
+
| `t.auth.loginAs(options)` | Returns a verified `TestUser` with `id`, `scopes`, `verified`, and optional `tenantId` |
|
|
108
|
+
| `t.clock.advance(by)` / `t.clock.now()` | Freeze and move the harness clock (used by durable sleeps and Clocks) |
|
|
109
|
+
| `t.cron.run(name)` | Run a named Clock / `every()` interval now, still leader-elected |
|
|
110
|
+
| `t.signals.drain()` | Drain the signal bus until idle (deterministic queued work) |
|
|
111
|
+
| `t.signals.subscribeLive(signal, handlers)` | Subscribe to a `delivery: "live"` signal's event stream (deterministic) |
|
|
112
|
+
| `t.signals.waitForLive(signal, predicate)` | Resolve once a live event matching `predicate` arrives |
|
|
113
|
+
| `t.channels.sent()` | Receipts / inbox entries sent during the test |
|
|
114
|
+
| `t.ai.mock(prompt, output)` | Register a canned mock response for a Prompt |
|
|
115
|
+
| `t.ai.cost()` | Accumulated AI cost across `fx.ask` calls |
|
|
116
|
+
| `t.effects.of(runId)` | Effect ledger for a recorded run (reads / writes / emits / sends) |
|
|
117
|
+
| `t.runs()` | All wide events recorded during the test |
|
|
118
|
+
| `t.close()` | Close booted element runtimes |
|
|
119
|
+
|
|
120
|
+
## Multi-tenancy & RLS
|
|
121
|
+
|
|
122
|
+
With `gate.auth: { tenant: true }` you can test tenant isolation with real
|
|
123
|
+
row-level security.
|
|
124
|
+
|
|
125
|
+
```typescript title="src/db/documents.ts"
|
|
126
|
+
import { store, field } from "okengine";
|
|
127
|
+
|
|
128
|
+
export const documents = store.schema.table(
|
|
129
|
+
"documents",
|
|
130
|
+
{
|
|
131
|
+
id: field.text().primaryKey(),
|
|
132
|
+
tenant_id: field.text().notNull(),
|
|
133
|
+
title: field.text().notNull(),
|
|
134
|
+
},
|
|
135
|
+
[store.schema.policy.tenant("tenant_id", { for: "all" })],
|
|
136
|
+
);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Give each test user a tenant, and drive distinct tenants against the same
|
|
140
|
+
|
|
141
|
+
Flow:
|
|
142
|
+
|
|
143
|
+
```typescript title="tests/tenant.test.ts"
|
|
144
|
+
const t = await createTestApp(app, {
|
|
145
|
+
gates: [member],
|
|
146
|
+
boot: { config: { drivers: { store: { sql: { test: "pglite" } } } } },
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const alice = await t.auth.loginAs({ id: "alice", tenantId: "acme" });
|
|
150
|
+
const bob = await t.auth.loginAs({ id: "bob", tenantId: "globex" });
|
|
151
|
+
|
|
152
|
+
await t.api.docs.create({ title: "Acme Roadmap" }, { as: alice });
|
|
153
|
+
await t.api.docs.create({ title: "Globex Secrets" }, { as: bob });
|
|
154
|
+
|
|
155
|
+
const acmeDocs = await t.api.docs.list(undefined, { as: alice });
|
|
156
|
+
expect(acmeDocs.data).toHaveLength(1); // only Acme Roadmap
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Re-scope a single call to another tenant with `{ tenant }`, independent of the
|
|
160
|
+
principal's own tenant:
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
const carol = await t.auth.loginAs({ id: "carol", tenantId: "acme" });
|
|
164
|
+
const { data } = await t.api.docs.create(
|
|
165
|
+
{ title: "Globex Carol" },
|
|
166
|
+
{ as: carol, tenant: "globex" },
|
|
167
|
+
);
|
|
168
|
+
expect(data.tenantId).toBe("globex");
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
RLS policies are row-level: tenant-scoped tables need `store.schema.policy.tenant`
|
|
172
|
+
(or `store.schema.unscoped()` for genuinely global tables). Harness boots do not
|
|
173
|
+
run migrations — in a real app `oke db push` installs those policies.
|
|
174
|
+
|
|
175
|
+
## Deterministic live signals
|
|
176
|
+
|
|
177
|
+
Signals with `delivery: "live"` back client feeds and SSE streams. The harness
|
|
178
|
+
exposes the retained event stream so you can assert both sides deterministically:
|
|
179
|
+
|
|
180
|
+
```typescript title="tests/live.test.ts"
|
|
181
|
+
const orderStatus = signal("order-status", {
|
|
182
|
+
delivery: "live",
|
|
183
|
+
retention: { maxCount: 10 },
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
on(
|
|
187
|
+
http.post("/orders/status").public(),
|
|
188
|
+
flow("orders.updateStatus", {
|
|
189
|
+
in: z.object({ orderId: z.string(), status: z.string() }),
|
|
190
|
+
effects: { emits: ["order-status"] },
|
|
191
|
+
do: async (input, fx) => {
|
|
192
|
+
await fx.emit(orderStatus, input);
|
|
193
|
+
return { ok: true };
|
|
194
|
+
},
|
|
195
|
+
}),
|
|
196
|
+
);
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
const seen: Array<{ orderId: string; status: string }> = [];
|
|
201
|
+
|
|
202
|
+
const unsubscribe = t.signals.subscribeLive(orderStatus, {
|
|
203
|
+
onEvent: (event) => seen.push(event),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
await t.api.orders.updateStatus({ orderId: "o1", status: "placed" });
|
|
207
|
+
await t.signals.drain();
|
|
208
|
+
expect(seen.map((e) => e.status)).toEqual(["placed"]);
|
|
209
|
+
|
|
210
|
+
// Resolve a promise the moment a matching event arrives.
|
|
211
|
+
const deliveredPromise = t.signals.waitForLive(
|
|
212
|
+
orderStatus,
|
|
213
|
+
(event) => event.status === "delivered",
|
|
214
|
+
);
|
|
215
|
+
await t.api.orders.updateStatus({ orderId: "o1", status: "delivered" });
|
|
216
|
+
await t.signals.drain();
|
|
217
|
+
const delivered = await deliveredPromise;
|
|
218
|
+
expect(delivered.status).toBe("delivered");
|
|
219
|
+
|
|
220
|
+
unsubscribe(); // no further events observed
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Call `t.signals.drain()` after each emit — live events are staged until the bus
|
|
224
|
+
drains. `retention` caps the replayed tape (`maxCount` / `maxAge`); it is only
|
|
225
|
+
valid on `delivery: "live"` signals.
|
|
226
|
+
|
|
227
|
+
## Time travel
|
|
228
|
+
|
|
229
|
+
`t.clock` freezes time. `t.clock.advance("2m")` moves the clock forward and
|
|
230
|
+
resumes any durable sleeps that become due. `t.cron.run("expire-stale")` runs a
|
|
231
|
+
named Clock now:
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
await t.clock.advance("30s"); // durable sleeps resume
|
|
235
|
+
await t.cron.run("expire-stale"); // named Clock fires
|
|
236
|
+
expect(t.channels.sent()).toHaveLength(1); // side effect observed
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## AI & channels
|
|
240
|
+
|
|
241
|
+
`fx.ask` routes to a mock driver. Register canned outputs and read accumulated
|
|
242
|
+
cost:
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
t.ai.mock(summarizePrompt, { title: "Mock summary" });
|
|
246
|
+
const { data } = await t.api.posts.summarize({ postId: "p_1" }, { as: u });
|
|
247
|
+
expect(data).toEqual({ title: "Mock summary" });
|
|
248
|
+
expect(t.ai.cost()).toBe(0);
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Channels write to an inbox instead of a transport:
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
expect(t.channels.sent()).toContainEqual(
|
|
255
|
+
expect.objectContaining({ template: "order-confirmed", to: u.id }),
|
|
256
|
+
);
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Troubleshooting
|
|
260
|
+
|
|
261
|
+
<Accordions>
|
|
262
|
+
|
|
263
|
+
<Accordion title="createTestApp fails to boot — missing secrets or stores">
|
|
264
|
+
The starter patches test drivers and fallback secrets on the app options. If
|
|
265
|
+
your `oke({ ... })` references a Vault secret or Store that the template does
|
|
266
|
+
not define, pass them explicitly:
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
await createTestApp(app, {
|
|
270
|
+
secrets: [openAiKey],
|
|
271
|
+
vaultSecrets: { OPENAI_KEY: "sk-test" },
|
|
272
|
+
});
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
</Accordion>
|
|
276
|
+
|
|
277
|
+
<Accordion title="My live subscription never receives events">
|
|
278
|
+
- Live events are staged until `t.signals.drain()` — await `drain()` after
|
|
279
|
+
every emitting call.
|
|
280
|
+
- Register `subscribeLive` / `waitForLive` **before** you emit.
|
|
281
|
+
- A signal with a schema drops violating emits — assert `res.error`.
|
|
282
|
+
|
|
283
|
+
</Accordion>
|
|
284
|
+
|
|
285
|
+
<Accordion title="Cross-tenant reads return a stale cached row">
|
|
286
|
+
Tier-1 read caching keys by flow, input, and user id — tenant is a row-level
|
|
287
|
+
RLS scope, not a cache dimension. For reads under a `{ tenant }` override, use
|
|
288
|
+
a distinct call principal (or write first) so the cache key differs.
|
|
289
|
+
|
|
290
|
+
</Accordion>
|
|
291
|
+
|
|
292
|
+
<Accordion title="PGlite RLS policies aren't applied in the harness">
|
|
293
|
+
Test boots never run migrations — `oke db push` installs DDL and
|
|
294
|
+
`store.schema.policy.tenant` policies in real apps.
|
|
295
|
+
|
|
296
|
+
Assert real RLS by installing the policy + `oke.*` helpers on the booted
|
|
297
|
+
primary connection (as the repo's `sql-rls-isolation.test.ts` does).
|
|
298
|
+
|
|
299
|
+
</Accordion>
|
|
300
|
+
|
|
301
|
+
</Accordions>
|
|
302
|
+
|
|
303
|
+
## Learn more
|
|
304
|
+
|
|
305
|
+
- [Flow](/docs/elements/flow) — triggers, contracts, effects, and composition
|
|
306
|
+
- [Store](/docs/elements/store) — SQL, KV, files, index, and RLS policies
|
|
307
|
+
- [Signal](/docs/elements/signal) — delivery physics, retention, and live feeds
|
|
308
|
+
- [Client](/docs/reference/client) — the typed client for real HTTP calls
|
|
309
|
+
|
|
310
|
+
## Next
|
|
311
|
+
|
|
312
|
+
<Cards>
|
|
313
|
+
<Card
|
|
314
|
+
title="Project structure"
|
|
315
|
+
description="Folders are the URL; unit plus export is the name."
|
|
316
|
+
href="/docs/get-started/project-structure"
|
|
317
|
+
/>
|
|
318
|
+
<Card
|
|
319
|
+
title="Flow"
|
|
320
|
+
description="Triggers, contracts, effects, and composition."
|
|
321
|
+
href="/docs/elements/flow"
|
|
322
|
+
/>
|
|
323
|
+
<Card
|
|
324
|
+
title="Signal"
|
|
325
|
+
description="Delivery physics, retention, and deterministic live feeds."
|
|
326
|
+
href="/docs/elements/signal"
|
|
327
|
+
/>
|
|
328
|
+
</Cards>
|