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.
Files changed (197) hide show
  1. package/README.md +9 -5
  2. package/manifest.v1.schema.json +61 -2
  3. package/package.json +18 -4
  4. package/site/content/docs/elements/clock.mdx +1 -1
  5. package/site/content/docs/elements/flow.mdx +25 -1
  6. package/site/content/docs/elements/store.mdx +287 -341
  7. package/site/content/docs/elements/vault.mdx +5 -5
  8. package/site/content/docs/get-started/installation.mdx +1 -2
  9. package/site/content/docs/get-started/introduction.mdx +54 -110
  10. package/site/content/docs/get-started/meta.json +9 -1
  11. package/site/content/docs/get-started/testing.mdx +328 -0
  12. package/site/content/docs/get-started/why.mdx +94 -70
  13. package/site/content/docs/index.mdx +1 -1
  14. package/site/content/docs/plugins/apple.mdx +151 -0
  15. package/site/content/docs/plugins/discord.mdx +139 -0
  16. package/site/content/docs/plugins/facebook.mdx +134 -0
  17. package/site/content/docs/plugins/figma.mdx +138 -0
  18. package/site/content/docs/plugins/github.mdx +138 -0
  19. package/site/content/docs/plugins/google.mdx +153 -0
  20. package/site/content/docs/plugins/index.mdx +47 -1
  21. package/site/content/docs/plugins/meta.json +10 -0
  22. package/site/content/docs/plugins/microsoft.mdx +151 -0
  23. package/site/content/docs/plugins/oauth.mdx +188 -0
  24. package/site/content/docs/plugins/x.mdx +125 -0
  25. package/site/content/docs/reference/cli.md +3 -2
  26. package/site/content/docs/reference/client.mdx +58 -1
  27. package/site/content/docs/reference/configuration.mdx +2 -4
  28. package/site/content/docs/reference/fx.mdx +3 -1
  29. package/site/content/docs/reference/index.mdx +0 -5
  30. package/site/content/docs/reference/meta.json +2 -2
  31. package/site/content/docs/reference/okid.mdx +137 -0
  32. package/src/auth/bindings.ts +1 -1
  33. package/src/auth/config.ts +9 -0
  34. package/src/auth/identity-sql.ts +314 -0
  35. package/src/auth/identity.ts +140 -2
  36. package/src/auth/index.ts +17 -1
  37. package/src/auth/method-context.ts +3 -0
  38. package/src/auth/oauth-as/cimd.ts +132 -0
  39. package/src/auth/oauth-as/crypto.test.ts +101 -0
  40. package/src/auth/oauth-as/crypto.ts +393 -0
  41. package/src/auth/oauth-as/errors.ts +68 -0
  42. package/src/auth/oauth-as/http.test.ts +419 -0
  43. package/src/auth/oauth-as/http.ts +842 -0
  44. package/src/auth/oauth-as/stores.ts +61 -0
  45. package/src/auth/oauth-as/tables.ts +142 -0
  46. package/src/auth/tables.ts +0 -11
  47. package/src/bench/README.md +83 -0
  48. package/src/bench/REPORT.md +176 -0
  49. package/src/bench/g01-rls-stamp.bench.ts +194 -0
  50. package/src/bench/g02-clock-per-tenant.bench.ts +158 -0
  51. package/src/bench/g03-signal-once.bench.ts +157 -0
  52. package/src/bench/g03-signal-reconnect.bench.ts +254 -0
  53. package/src/bench/g03-signal-sse-memory.bench.ts +191 -0
  54. package/src/bench/g04-auth-vault-hotpath.bench.ts +170 -0
  55. package/src/bench/g05-sustained-full.bench.ts +265 -0
  56. package/src/bench/g06-mixed-load.bench.ts +260 -0
  57. package/src/bench/g07-vault-crypto.bench.ts +100 -0
  58. package/src/bench/g07-vault-rotate-under-read.bench.ts +285 -0
  59. package/src/bench/g08-conn-oversubscribe.bench.ts +194 -0
  60. package/src/bench/g08-store-kv-durable.bench.ts +133 -0
  61. package/src/bench/g08-store-sql.bench.ts +178 -0
  62. package/src/bench/g09-journal-sustained.bench.ts +203 -0
  63. package/src/bench/g10-observability-contention.bench.ts +246 -0
  64. package/src/bench/g11-cold-start-cycle.bench.ts +164 -0
  65. package/src/bench/g13-elements.bench.ts +427 -0
  66. package/src/bench/g14-graceful-shutdown.bench.ts +244 -0
  67. package/src/bench/g15-postgres-degradation.bench.ts +264 -0
  68. package/src/bench/g16-live-query-fanout.bench.ts +206 -0
  69. package/src/bench/lib/event-loop-lag.ts +26 -0
  70. package/src/bench/lib/infra.ts +60 -0
  71. package/src/bench/lib/report.ts +52 -0
  72. package/src/bench/lib/rss-sampler.ts +61 -0
  73. package/src/bench/lib/signal-pg.ts +88 -0
  74. package/src/bench/load-app.ts +337 -0
  75. package/src/bench/load-child.ts +108 -0
  76. package/src/bench/smoke.bench.ts +43 -0
  77. package/src/cli/competitor-mention-removal.test.ts +28 -0
  78. package/src/cli/doctor-fd.ts +117 -0
  79. package/src/cli/doctor.test.ts +192 -0
  80. package/src/cli/doctor.ts +129 -1
  81. package/src/client/create.ts +95 -1
  82. package/src/client/index.ts +9 -2
  83. package/src/client/transport.ts +11 -4
  84. package/src/client/use-live-query.ts +154 -0
  85. package/src/client-react/index.ts +15 -1
  86. package/src/client-react/live-resource.ts +246 -0
  87. package/src/client-react/use-live-query.test.ts +475 -0
  88. package/src/client-react/use-live-query.ts +530 -0
  89. package/src/compiler/extract.test.ts +518 -0
  90. package/src/compiler/extract.ts +386 -19
  91. package/src/console/server/invoke-user-flow.ts +2 -1
  92. package/src/console/ui-next/dist/assets/{access-page-DnWbnGzq.js → access-page-DY4N6nnk.js} +1 -1
  93. package/src/console/ui-next/dist/assets/{flows-page-BiZ4-6yQ.js → flows-page-CsPDMrVM.js} +1 -1
  94. package/src/console/ui-next/dist/assets/{index-C8NRK2R-.js → index-CcTDXHuz.js} +3 -3
  95. package/src/console/ui-next/dist/assets/{observability-page-CrB6vd1T.js → observability-page-CKR595wP.js} +1 -1
  96. package/src/console/ui-next/dist/assets/{store-page-CS5-aETQ.js → store-page-02xOiqIK.js} +3 -3
  97. package/src/console/ui-next/dist/assets/{units-page-CjtdlW8l.js → units-page-CpPFFKyE.js} +1 -1
  98. package/src/console/ui-next/dist/assets/{vault-page-C6Xxm9SA.js → vault-page-BsMf-9_W.js} +1 -1
  99. package/src/console/ui-next/dist/index.html +1 -1
  100. package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +36 -2
  101. package/src/drivers/cdc-outbox.ts +389 -0
  102. package/src/drivers/memory.ts +20 -0
  103. package/src/drivers/oauth-apple.ts +156 -0
  104. package/src/drivers/oauth-discord.ts +79 -0
  105. package/src/drivers/oauth-facebook.ts +80 -0
  106. package/src/drivers/oauth-figma.ts +116 -0
  107. package/src/drivers/oauth-github.ts +92 -0
  108. package/src/drivers/oauth-google.ts +142 -0
  109. package/src/drivers/oauth-microsoft.ts +174 -0
  110. package/src/drivers/oauth-oidc.ts +293 -0
  111. package/src/drivers/oauth-shared.ts +326 -0
  112. package/src/drivers/oauth-types.ts +159 -0
  113. package/src/drivers/oauth-x.ts +77 -0
  114. package/src/drivers/oauth2-common.ts +95 -0
  115. package/src/drivers/oauth2-token.ts +61 -0
  116. package/src/drivers/pg-rls-row-passes.ts +251 -0
  117. package/src/drivers/pg-rls.ts +2 -0
  118. package/src/drivers/postgres.ts +45 -2
  119. package/src/drivers/signal-postgres.ts +2 -1
  120. package/src/elements/channel/runtime.ts +29 -2
  121. package/src/elements/channel.test.ts +52 -0
  122. package/src/elements/gate/boot.ts +29 -2
  123. package/src/elements/store/emit-drizzle.ts +147 -14
  124. package/src/elements/store/field-ddl.test.ts +118 -0
  125. package/src/elements/store/field-types.test.ts +455 -0
  126. package/src/elements/store/list-query.golden.json +777 -0
  127. package/src/elements/store/list-query.parity.test.ts +396 -0
  128. package/src/elements/store/list-query.ts +792 -0
  129. package/src/elements/store/live-default.test.ts +136 -0
  130. package/src/elements/store/live-http.test.ts +160 -0
  131. package/src/elements/store/live-isolation.test.ts +291 -0
  132. package/src/elements/store/live-query-runtime.test.ts +323 -0
  133. package/src/elements/store/live-query-runtime.ts +403 -0
  134. package/src/elements/store/live-query-server.test.ts +377 -0
  135. package/src/elements/store/live-query-server.ts +102 -0
  136. package/src/elements/store/live-query.ts +97 -0
  137. package/src/elements/store/resource.ts +189 -680
  138. package/src/elements/store/rls-row-passes-policies.parity.test.ts +665 -0
  139. package/src/elements/store/schema-decl.ts +539 -41
  140. package/src/elements/store/sql-rls-stamp.test.ts +27 -0
  141. package/src/elements/store/sql-session.ts +297 -35
  142. package/src/elements/store/table.ts +102 -21
  143. package/src/elements/store.test.ts +3 -1
  144. package/src/elements/store.ts +12 -1
  145. package/src/elements/vault/chaos-child.ts +74 -1
  146. package/src/elements/vault/chaos.test.ts +4 -2
  147. package/src/elements/vault/storage.ts +4 -2
  148. package/src/index.ts +4 -1
  149. package/src/kernel/app-auth.ts +1 -0
  150. package/src/kernel/app.ts +116 -2
  151. package/src/kernel/auth-sharing.test.ts +196 -0
  152. package/src/kernel/boot.test.ts +3 -3
  153. package/src/kernel/errors.ts +8 -0
  154. package/src/kernel/fx.test.ts +1 -0
  155. package/src/kernel/fx.ts +14 -2
  156. package/src/kernel/horizontal-child.ts +2 -1
  157. package/src/kernel/http-resource.ts +33 -7
  158. package/src/kernel/identity-host-persist.test.ts +119 -0
  159. package/src/kernel/instance-id.ts +4 -2
  160. package/src/kernel/journal.ts +2 -1
  161. package/src/kernel/mcp-tool.test.ts +95 -0
  162. package/src/kernel/on.ts +9 -0
  163. package/src/kernel/realtime-bind.ts +326 -0
  164. package/src/kernel/resource-live.ts +117 -0
  165. package/src/kernel/triggers.ts +86 -4
  166. package/src/manifest/diff.ts +37 -0
  167. package/src/manifest/types.ts +64 -2
  168. package/src/okid.bench.test.ts +64 -0
  169. package/src/okid.test.ts +338 -0
  170. package/src/okid.ts +245 -0
  171. package/src/plugins/anonymous.ts +19 -1
  172. package/src/plugins/auth/shared.ts +15 -0
  173. package/src/plugins/index.ts +2 -0
  174. package/src/plugins/magic-link.ts +10 -8
  175. package/src/plugins/mcp-oauth.ts +208 -0
  176. package/src/plugins/oauth/flow-store.ts +117 -0
  177. package/src/plugins/oauth/link.ts +69 -0
  178. package/src/plugins/oauth/shared.ts +108 -0
  179. package/src/plugins/oauth/token-vault.ts +100 -0
  180. package/src/plugins/oauth.security.test.ts +535 -0
  181. package/src/plugins/oauth.ts +532 -0
  182. package/src/plugins/otp.ts +48 -6
  183. package/src/plugins/passkey.ts +20 -1
  184. package/src/plugins/two-factor.ts +11 -0
  185. package/src/plugins/username.ts +40 -7
  186. package/src/release/build-lib.ts +7 -1
  187. package/src/release/measure.ts +1 -0
  188. package/src/release/official-plugins.ts +4 -1
  189. package/src/runs/collect.ts +2 -1
  190. package/src/runs/drivers/files.ts +2 -1
  191. package/src/test/create-test-app.ts +114 -5
  192. package/src/test/export-bundle.test.ts +33 -0
  193. package/src/test/live-signals.test.ts +83 -0
  194. package/src/test/tenant-isolation.test.ts +175 -0
  195. package/src/testing.ts +26 -0
  196. package/src/upgrade/codemods.ts +1 -1
  197. 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
- VaultBootError: 1 secret(s) missing:
177
- STRIPE_KEY Payments gateway 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` uses SKIP LOCKED + lease TTL (one holder; crashed holders reclaim). Audit appends
246
- under a transactional row lock. Backups use temp fsync rename with end marker + checksum —
247
- restore rejects incomplete files before decrypt.
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
- See [Migrating environments](/docs/reference/migrating-environments).
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
- A booking API, a nightly cleanup job, a receipt email, a row-change hook — in
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
- <Callout title="The one rule">
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
- ## Quick start
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
- <Steps>
15
+ ## Quick Start
20
16
 
21
- <Step>
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 title="flows/main/health.ts"
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
- </Step>
36
+ ### Change Only the Trigger
42
37
 
43
- <Step>
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")` | a request arrives | endpoint · handler |
49
- | `every("10m")` | time passes | cron job |
50
- | `orderPlaced` (a signal) | another flow emits | queue consumer |
51
- | `db.table(users).changed("email")` | a row changes | CDC pipeline |
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
- | Consequence | Follows from |
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
- **All world access goes through `fx`.** A Flow that imports `node:fs`, calls
97
- `fetch` directly, or uses `Date.now()` is a defect.
69
+ ## The `fx` Rule
98
70
 
99
- | Inferred from `fx` touches | Without hand annotations |
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
- ## What the Manifest derives
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
- At build time OKE extracts a **Manifest** — a machine-readable description of
110
- your system. You do not maintain a second source of truth.
81
+ ## What the Manifest Derives
111
82
 
112
- <ManifestPipeline />
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 / place | You maintain? |
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 elements
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
- <Features />
128
-
129
- | Element | Essence | Replaces the zoo of |
97
+ | Element | Essence | Replaces the Zoo |
130
98
  | ----------- | ----------------------------- | --------------------------------------------------------- |
131
- | **Flow** | behavior | endpoint · handler · consumer · job · workflow · webhook |
132
- | **Signal** | data in motion | queue · pub/sub · stream · websocket · SSE · event bus |
133
- | **Store** | data at rest | database · cache · KV · file storage · search index |
134
- | **Clock** | time | cron · delay · timeout · durable sleep · TTL |
135
- | **Gate** | permission to act | auth · session · ABAC · rate limit · quota · feature flag |
136
- | **Vault** | protected knowledge | secrets · config · environment |
137
- | **Channel** | reaching humans | email · SMS · WhatsApp · push |
138
- | **AI** | reaching machine intelligence | model calls · prompts · embeddings · agents · RAG |
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 exports
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 title="okengine"
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 title="okengine/http"
118
+ ```ts
153
119
  import { on, flow, http, gate, oke, createBunRuntime } from "okengine/http";
154
120
  ```
155
121
 
156
- Heavy surfaces (`runs`, journal factories, compiler extract, the legacy
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 more
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
- <Cards>
175
- <Card
176
- title="Why OKE"
177
- description="Traditional backend pain — and what Manifest derivation closes."
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": ["index", "introduction", "why", "installation", "basic-usage", "project-structure"]
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>