okengine 0.15.2 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/manifest.v1.schema.json +21 -2
  2. package/package.json +1 -1
  3. package/site/content/docs/elements/clock.mdx +7 -0
  4. package/site/content/docs/elements/flow.mdx +20 -12
  5. package/site/content/docs/elements/gate.mdx +108 -3
  6. package/site/content/docs/elements/signal.mdx +46 -17
  7. package/site/content/docs/elements/store.mdx +36 -32
  8. package/site/content/docs/elements/vault.mdx +26 -0
  9. package/site/content/docs/get-started/project-structure.mdx +4 -4
  10. package/site/content/docs/reference/cli.md +1 -1
  11. package/site/content/docs/reference/client.mdx +75 -17
  12. package/site/content/docs/reference/configuration.mdx +7 -4
  13. package/site/content/docs/reference/errors.mdx +24 -17
  14. package/site/content/docs/reference/fx.mdx +39 -22
  15. package/site/content/docs/reference/security.md +1 -1
  16. package/src/auth/api-key-sql.test.ts +47 -0
  17. package/src/auth/api-key-sql.ts +261 -0
  18. package/src/auth/api-keys.ts +276 -14
  19. package/src/auth/auth.test.ts +56 -0
  20. package/src/auth/config.ts +24 -5
  21. package/src/auth/index.ts +62 -1
  22. package/src/auth/plugin.ts +6 -1
  23. package/src/auth/schema.ts +8 -0
  24. package/src/auth/sessions.ts +18 -0
  25. package/src/auth/tables.ts +32 -0
  26. package/src/auth/tenant-config.ts +74 -0
  27. package/src/auth/tenant-tables.ts +11 -0
  28. package/src/auth/tenants.test.ts +63 -0
  29. package/src/auth/tenants.ts +360 -0
  30. package/src/cli/attach-host-console.test.ts +37 -0
  31. package/src/cli/attach-host-console.ts +6 -0
  32. package/src/cli/build.ts +2 -2
  33. package/src/client/budget.test.ts +1 -1
  34. package/src/client/create.ts +75 -5
  35. package/src/client/index.ts +13 -0
  36. package/src/client/live.test.ts +422 -0
  37. package/src/client/live.ts +389 -0
  38. package/src/client/notes-contract.test.ts +41 -0
  39. package/src/client/types.ts +85 -6
  40. package/src/client-react/index.ts +85 -1
  41. package/src/client-react/use-live.test.ts +129 -0
  42. package/src/compiler/effects-infer.ts +37 -2
  43. package/src/compiler/extract.test.ts +143 -11
  44. package/src/compiler/extract.ts +210 -30
  45. package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +1 -2
  46. package/src/compiler/fixtures/skyport.expected.json +2 -3
  47. package/src/compiler/response.ts +41 -11
  48. package/src/console/server/access.test.ts +116 -8
  49. package/src/console/server/access.ts +70 -23
  50. package/src/console/server/app.ts +45 -11
  51. package/src/console/server/console.test.ts +6 -8
  52. package/src/console/server/dev-identities.test.ts +127 -0
  53. package/src/console/server/dev-identities.ts +229 -0
  54. package/src/console/server/flows-invoke.test.ts +21 -4
  55. package/src/console/server/flows.ts +113 -58
  56. package/src/console/server/gates.test.ts +24 -0
  57. package/src/console/server/gates.ts +10 -30
  58. package/src/console/server/index.ts +1 -0
  59. package/src/console/server/live.test.ts +26 -1
  60. package/src/console/server/live.ts +4 -2
  61. package/src/console/server/operator-db.ts +2 -0
  62. package/src/console/server/state.ts +29 -55
  63. package/src/console/server/store.test.ts +17 -0
  64. package/src/console/server/store.ts +43 -1
  65. package/src/console/ui-next/dist/assets/access-page-CXVWWMmD.js +4 -0
  66. package/src/console/ui-next/dist/assets/{agent-disclosure-C0X1fbWF.js → agent-disclosure-BP0Y0Sux.js} +1 -1
  67. package/src/console/ui-next/dist/assets/cache-glyph-B5X-NM0-.js +1 -0
  68. package/src/console/ui-next/dist/assets/call-pii-button-ChAUiLo9.js +1 -0
  69. package/src/console/ui-next/dist/assets/collapsible-DGnOM2ph.js +1 -0
  70. package/src/console/ui-next/dist/assets/copy-inline-button-DBHhgHKP.js +1 -0
  71. package/src/console/ui-next/dist/assets/detail-header-DhHM1iaZ.js +1 -0
  72. package/src/console/ui-next/dist/assets/dropdown-menu-_NjTEo5_.js +1 -0
  73. package/src/console/ui-next/dist/assets/element-icons-BVXtRyd3.js +1 -0
  74. package/src/console/ui-next/dist/assets/explorer-empty-2uIhBu0_.js +1 -0
  75. package/src/console/ui-next/dist/assets/flows-page-XDpAJO8f.js +1 -0
  76. package/src/console/ui-next/dist/assets/{highlighted-json-CS_O8L-r.js → highlighted-json-Awq7gYdu.js} +1 -1
  77. package/src/console/ui-next/dist/assets/http-method-_UHM2ODJ.js +1 -0
  78. package/src/console/ui-next/dist/assets/index-BBj2QJCu.js +66 -0
  79. package/src/console/ui-next/dist/assets/index-Ck88Jmv8.css +2 -0
  80. package/src/console/ui-next/dist/assets/link-BX6Vqztd.js +1 -0
  81. package/src/console/ui-next/dist/assets/observability-page-BFaay44m.js +4 -0
  82. package/src/console/ui-next/dist/assets/replica-lag-DKRrbvdo.js +18 -0
  83. package/src/console/ui-next/dist/assets/request-meta-DV0ywz7t.js +1 -0
  84. package/src/console/ui-next/dist/assets/shortcut-JQIZlWfm.js +1 -0
  85. package/src/console/ui-next/dist/assets/shortcut-keys-DKxNTe_m.js +1 -0
  86. package/src/console/ui-next/dist/assets/skeleton-D-czQJT6.js +1 -0
  87. package/src/console/ui-next/dist/assets/store-page-CS5-aETQ.js +41 -0
  88. package/src/console/ui-next/dist/assets/trace-detail-sheet-B09O8rA6.js +2 -0
  89. package/src/console/ui-next/dist/assets/tree-expand-toggle-BtyhmWb4.js +55 -0
  90. package/src/console/ui-next/dist/assets/units-page-Ca2Z-E52.js +1 -0
  91. package/src/console/ui-next/dist/assets/{use-vault-list-BLbZhzyF.js → use-vault-list-uk4WVboC.js} +1 -1
  92. package/src/console/ui-next/dist/assets/useMutation-B8EO02Ej.js +1 -0
  93. package/src/console/ui-next/dist/assets/useRender-BE2A9BWC.js +1 -0
  94. package/src/console/ui-next/dist/assets/vault-page-BmOeAFwg.js +2 -0
  95. package/src/console/ui-next/dist/index.html +11 -9
  96. package/src/console/ui-next/index.html +3 -3
  97. package/src/console/ui-next/src/client.ts +167 -0
  98. package/src/console/ui-next/src/components/shell/app-sidebar.tsx +2 -0
  99. package/src/console/ui-next/src/components/shell/command-items.test.ts +1 -0
  100. package/src/console/ui-next/src/components/shell/command-items.ts +8 -0
  101. package/src/console/ui-next/src/components/shell/shell-layout.tsx +2 -1
  102. package/src/console/ui-next/src/components/ui/sheet-form.tsx +8 -3
  103. package/src/console/ui-next/src/features/access/access-page.tsx +347 -0
  104. package/src/console/ui-next/src/features/access/data/use-access.ts +23 -0
  105. package/src/console/ui-next/src/features/access/detail/access-detail.tsx +552 -0
  106. package/src/console/ui-next/src/features/access/explorer/access-list.tsx +125 -0
  107. package/src/console/ui-next/src/features/access/lib/format-when.test.ts +139 -0
  108. package/src/console/ui-next/src/features/access/lib/format-when.ts +296 -0
  109. package/src/console/ui-next/src/features/access/lib/scope-groups.test.ts +34 -0
  110. package/src/console/ui-next/src/features/access/lib/scope-groups.ts +40 -0
  111. package/src/console/ui-next/src/features/access/sheets/access-allow-fields.tsx +236 -0
  112. package/src/console/ui-next/src/features/access/sheets/access-create-sheet.tsx +316 -0
  113. package/src/console/ui-next/src/features/access/sheets/access-edit-sheet.tsx +240 -0
  114. package/src/console/ui-next/src/features/access/sheets/access-expiry-fields.tsx +66 -0
  115. package/src/console/ui-next/src/features/access/sheets/access-refresh-sheet.tsx +104 -0
  116. package/src/console/ui-next/src/features/access/sheets/access-scope-field.tsx +115 -0
  117. package/src/console/ui-next/src/features/access/sheets/access-secret-sheet.tsx +89 -0
  118. package/src/console/ui-next/src/features/access/sheets/access-sheet-search.tsx +49 -0
  119. package/src/console/ui-next/src/features/access/state/access-selection.ts +84 -0
  120. package/src/console/ui-next/src/features/auth/auth-redirect.test.ts +5 -0
  121. package/src/console/ui-next/src/features/auth/auth-redirect.ts +18 -2
  122. package/src/console/ui-next/src/features/flows/fixture.ts +0 -1
  123. package/src/console/ui-next/src/features/not-found/not-found-page.tsx +1 -1
  124. package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +5 -1
  125. package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +15 -4
  126. package/src/console/ui-next/src/lib/document-meta.test.ts +1 -0
  127. package/src/console/ui-next/src/lib/document-meta.ts +2 -1
  128. package/src/console/ui-next/src/lib/last-module-search.test.ts +1 -0
  129. package/src/console/ui-next/src/lib/last-module-search.ts +2 -1
  130. package/src/console/ui-next/src/lib/shortcut.test.ts +2 -0
  131. package/src/console/ui-next/src/lib/shortcut.ts +3 -0
  132. package/src/console/ui-next/src/router.tsx +10 -1
  133. package/src/console/ui-next/ui-next-seed-app-schema.ts +88 -37
  134. package/src/console/ui-next/ui-next-seed-manifest-surface.ts +2 -9
  135. package/src/console/ui-next/ui-next-seed-manifest.ts +0 -1
  136. package/src/console/ui-next/ui-next-seed-store.ts +43 -15
  137. package/src/console/xss.gate.test.ts +1 -0
  138. package/src/drivers/index.ts +2 -0
  139. package/src/drivers/journal-postgres.ts +12 -3
  140. package/src/drivers/pg-rls.ts +26 -2
  141. package/src/drivers/pg-vault-rls.ts +68 -0
  142. package/src/drivers/signal-engine.ts +69 -15
  143. package/src/drivers/signal-live-iter.ts +65 -0
  144. package/src/drivers/signal-nats.ts +2 -1
  145. package/src/drivers/signal-postgres.ts +95 -12
  146. package/src/drivers/signal-redis.ts +2 -1
  147. package/src/drivers/signal-retention.ts +64 -0
  148. package/src/drivers/signal-types.ts +35 -5
  149. package/src/elements/clock/declare.ts +64 -2
  150. package/src/elements/clock/reconcile.ts +98 -25
  151. package/src/elements/clock/runtime.ts +13 -2
  152. package/src/elements/clock.test.ts +28 -0
  153. package/src/elements/clock.ts +9 -1
  154. package/src/elements/gate/declare.ts +2 -0
  155. package/src/elements/gate/permissions.ts +12 -0
  156. package/src/elements/gate.ts +6 -1
  157. package/src/elements/signal/declare.ts +44 -10
  158. package/src/elements/signal/delivery-modes.test.ts +90 -4
  159. package/src/elements/signal/order-lifecycle.test.ts +20 -4
  160. package/src/elements/signal/runtime.ts +42 -0
  161. package/src/elements/signal.test.ts +21 -8
  162. package/src/elements/signal.ts +1 -1
  163. package/src/elements/store/cache.ts +1 -1
  164. package/src/elements/store/declare.ts +7 -0
  165. package/src/elements/store/rls-identity.test.ts +29 -0
  166. package/src/elements/store/rls-identity.ts +3 -0
  167. package/src/elements/store/runtime.ts +14 -0
  168. package/src/elements/store/schema-decl.ts +162 -44
  169. package/src/elements/store/schema-tenant.ts +41 -0
  170. package/src/elements/store/sql-rls-isolation.test.ts +39 -0
  171. package/src/elements/store/sql-select.types.test.ts +71 -0
  172. package/src/elements/store/sql-session.ts +54 -32
  173. package/src/elements/store.ts +5 -0
  174. package/src/elements/vault/builtin-adapter.ts +15 -2
  175. package/src/elements/vault/declare.ts +8 -0
  176. package/src/elements/vault/runtime.ts +7 -0
  177. package/src/elements/vault/sql-rls-isolation.test.ts +145 -0
  178. package/src/elements/vault/storage.ts +9 -0
  179. package/src/elements/vault/test-helpers.ts +2 -1
  180. package/src/i18n/catalogs/ar.ts +19 -0
  181. package/src/i18n/catalogs/en.ts +19 -0
  182. package/src/index.ts +2 -0
  183. package/src/kernel/adopt-routes.ts +56 -8
  184. package/src/kernel/api-key-host-persist.test.ts +143 -0
  185. package/src/kernel/app-tenant.ts +122 -0
  186. package/src/kernel/app.ts +196 -57
  187. package/src/kernel/auth-resolve.ts +13 -5
  188. package/src/kernel/boot-bind/clock.ts +9 -3
  189. package/src/kernel/boot.test.ts +4 -4
  190. package/src/kernel/boot.ts +2 -0
  191. package/src/kernel/budget.test.ts +1 -1
  192. package/src/kernel/clock-durable.ts +8 -0
  193. package/src/kernel/clock-per-tenant-name.ts +5 -0
  194. package/src/kernel/clock-reconcile.ts +8 -0
  195. package/src/kernel/errors-live-resume.ts +15 -0
  196. package/src/kernel/errors-tenant.ts +29 -0
  197. package/src/kernel/errors.registry.test.ts +31 -3
  198. package/src/kernel/errors.ts +43 -3
  199. package/src/kernel/flow.ts +26 -5
  200. package/src/kernel/fx-auth-keys.test.ts +88 -0
  201. package/src/kernel/fx-auth-keys.ts +203 -0
  202. package/src/kernel/fx-auth-tenants.test.ts +87 -0
  203. package/src/kernel/fx-auth-tenants.ts +286 -0
  204. package/src/kernel/fx-live-stream.ts +149 -0
  205. package/src/kernel/fx-live.test.ts +157 -0
  206. package/src/kernel/fx-runtime.ts +15 -0
  207. package/src/kernel/fx-tenant-store.ts +213 -0
  208. package/src/kernel/fx.test.ts +108 -1
  209. package/src/kernel/fx.ts +206 -28
  210. package/src/kernel/hooks.ts +2 -2
  211. package/src/kernel/http-resource.ts +9 -18
  212. package/src/kernel/index.ts +2 -0
  213. package/src/kernel/journal.ts +12 -0
  214. package/src/kernel/live-http.test.ts +78 -0
  215. package/src/kernel/live-http.ts +114 -0
  216. package/src/kernel/live-resume.test.ts +125 -0
  217. package/src/kernel/on.ts +51 -0
  218. package/src/kernel/pipeline-api-key.test.ts +192 -7
  219. package/src/kernel/pipeline-tenant.ts +49 -0
  220. package/src/kernel/pipeline.test.ts +1 -1
  221. package/src/kernel/pipeline.ts +41 -4
  222. package/src/kernel/resource-mount.test.ts +10 -27
  223. package/src/kernel/tenant-resolve.test.ts +101 -0
  224. package/src/kernel/tenant-resolve.ts +124 -0
  225. package/src/kernel/tenant-roles.test.ts +87 -0
  226. package/src/kernel/triggers.ts +59 -21
  227. package/src/manifest/diff.test.ts +11 -2
  228. package/src/manifest/diff.ts +53 -11
  229. package/src/manifest/fixtures/skyport.excerpt.json +1 -1
  230. package/src/manifest/fixtures/skyport.manifest.json +0 -1
  231. package/src/manifest/types.ts +58 -6
  232. package/src/release/build-lib.ts +14 -1
  233. package/src/release/limits.ts +2 -2
  234. package/src/release/measure.ts +55 -1
  235. package/src/client/live-gap.test.ts +0 -35
  236. package/src/console/ui-next/dist/assets/cache-glyph-F1FI122b.js +0 -1
  237. package/src/console/ui-next/dist/assets/call-pii-button-DEDCl_j3.js +0 -1
  238. package/src/console/ui-next/dist/assets/collapsible-LPqGvfoz.js +0 -1
  239. package/src/console/ui-next/dist/assets/confirm-sheet-3ptDzXbz.js +0 -1
  240. package/src/console/ui-next/dist/assets/explorer-empty-BvYrygyO.js +0 -1
  241. package/src/console/ui-next/dist/assets/flows-page-Dluo1Bd7.js +0 -1
  242. package/src/console/ui-next/dist/assets/http-method--sWDdXSB.js +0 -1
  243. package/src/console/ui-next/dist/assets/index-Ca3HZMVq.js +0 -66
  244. package/src/console/ui-next/dist/assets/index-UtTDRQpS.css +0 -2
  245. package/src/console/ui-next/dist/assets/link-COeyggt-.js +0 -1
  246. package/src/console/ui-next/dist/assets/observability-page-HK9-BO8z.js +0 -4
  247. package/src/console/ui-next/dist/assets/replica-lag-DBIFFf7d.js +0 -18
  248. package/src/console/ui-next/dist/assets/sheet-form-Dnwa5oOR.js +0 -1
  249. package/src/console/ui-next/dist/assets/shortcut-Cgzuv4k1.js +0 -1
  250. package/src/console/ui-next/dist/assets/shortcut-keys-CoqwEIi0.js +0 -1
  251. package/src/console/ui-next/dist/assets/skeleton-BHcfDYcb.js +0 -1
  252. package/src/console/ui-next/dist/assets/store-page-BSohH3wM.js +0 -41
  253. package/src/console/ui-next/dist/assets/trace-detail-sheet-CFIGRnnA.js +0 -2
  254. package/src/console/ui-next/dist/assets/tree-expand-toggle-BgLl34w7.js +0 -54
  255. package/src/console/ui-next/dist/assets/units-page-B0cFE76A.js +0 -1
  256. package/src/console/ui-next/dist/assets/vault-page-B1SbYe10.js +0 -2
@@ -172,7 +172,7 @@ const { data } = await api.auth.signInEmail({ email, password });
172
172
  if (data) session.set(data);
173
173
  ```
174
174
 
175
- React: `useSession(api, session)` from `okengine/client-react`.
175
+ React: `useSession(api, session)` and `useLive(api, signal, input?)` from `okengine/client-react`.
176
176
 
177
177
  | Step | What happens |
178
178
  | ---------------------- | ---------------------------------------------------------------- |
@@ -180,6 +180,9 @@ React: `useSession(api, session)` from `okengine/client-react`.
180
180
  | HTTP **401** | `refresh()` runs **once**, then the same call retries |
181
181
  | HTTP **403** / **429** | No refresh — decode the failure envelope as usual |
182
182
 
183
+ `getToken` may return a session access token or an API key secret — both are Bearer.
184
+ See [Gate](/docs/elements/gate#api-keys). `refresh` applies to sessions only.
185
+
183
186
  **Consequence:** `refresh` must mutate whatever `getToken` reads. Returning a new string alone does
184
187
  nothing if storage was not updated.
185
188
 
@@ -201,6 +204,7 @@ A 401 with no `{ data, error }` body becomes `TransportError` with `data.status:
201
204
  | `memorySession` | `okengine/client/auth` | In-memory access/refresh bag for `auth.getToken` |
202
205
  | `AUTH_ERROR_CODES` | `okengine/client/auth` | Common auth Flow / gate codes |
203
206
  | `useSession(api, session?)` | `okengine/client-react` | React status + `auth.me` |
207
+ | `useLive(api, signal, …)` | `okengine/client-react` | React `events` / `latest` / `isConnected` |
204
208
 
205
209
  Core `okengine/client` stays under the size budget — helpers are separate exports. Not in
206
210
  core today: cookie jars or plugin `.client()` decorations. Browser apps: also see
@@ -218,7 +222,7 @@ core today: cookie jars or plugin `.client()` decorations. Browser apps: also se
218
222
  | [Flow](/docs/elements/flow) | Direct | `api.unit.flow(input)` — the only public surface |
219
223
  | [Gate](/docs/elements/gate) | Indirect | Bearer via `auth`; denials as `Unauthorized` / `Forbidden` / `RateLimited` |
220
224
  | [Store](/docs/elements/store) | Via Flows | `fx.store` inside Flows; `store.resource` + `on(http.resource…)` → five Flows on `$routes` |
221
- | [Signal](/docs/elements/signal) | Via Flows | Emit/consume server-side; no subscribe API on `okengine/client` yet |
225
+ | [Signal](/docs/elements/signal) | Live SSE | `api.live(signal, input?, { onEvent })` HTTP GET, callback + unsubscribe |
222
226
  | [Clock](/docs/elements/clock) | Via Flows | Schedules fire on the server — the client never ticks a clock |
223
227
  | [Vault](/docs/elements/vault) | Via Flows | Secrets stay server-side; never ship them to the browser package |
224
228
  | [Channel](/docs/elements/channel) | Via Flows | `fx.send` in a Flow — the client does not send email/SMS/push |
@@ -230,7 +234,7 @@ Mount a resource, adopt the returned ops, then call the five Flows like any othe
230
234
 
231
235
  ```typescript
232
236
  const notesR = store.resource(db, notes, {/* in, out, list */});
233
- const mounted = on(http.resource("/notes", notesR.all()).public().live());
237
+ const mounted = on(http.resource("/notes", notesR.all()).public());
234
238
  // .adopt({ notes: mounted }) →
235
239
  const page = await api.notes.list({ limit: 20 });
236
240
  const more = await page.next();
@@ -256,24 +260,63 @@ See [Store](/docs/elements/store) for the list query language and schemas. Handw
256
260
  `fx.json.withQuery(rows, input)` for the same envelope. Auth posture for HTTP triggers is
257
261
  covered under [Gate](/docs/elements/gate).
258
262
 
259
- ### Signal and live queries
263
+ ### Live signals
264
+
265
+ `delivery: "live"` is HTTP SSE. Expose with `.live(signal)` on GET (or `http.live(signal)` for
266
+ `GET /_oke/live/{name}`), then subscribe with a callback. `for await` stays on the server.
267
+
268
+ ```typescript
269
+ const stop = api.live(
270
+ orderStatus,
271
+ { orderId: "ord_1" },
272
+ {
273
+ onEvent: (event) => {
274
+ /* event: { orderId, status } */
275
+ },
276
+ onError: (err) => {
277
+ /* optional — 4xx, envelope, network drop */
278
+ },
279
+ autoResubscribe: false, // default — true reopens after a drop (500ms…30s backoff)
280
+ },
281
+ );
282
+ stop(); // useEffect cleanup
283
+ ```
260
284
 
261
- `delivery: "live"` and `http.get(…).live()` are Manifest / driver flags today. `okengine/client`
262
- does **not** expose WebSocket, SSE, or `api.*.subscribe`. Until that ships, poll or call an HTTP
263
- Flow that returns the current state.
285
+ `api.orders.events({ orderId }, { onEvent })` is the same shape on the exposing flow.
286
+
287
+ The client picks the unique exposure whose `matchKey` fields are a subset of the input, preferring
288
+ the largest match (`{ orderId }` beats firehose). A tie needs `via: "unit.flow"`.
289
+
290
+ Reconnects send `Last-Event-ID` from the last `id:` the client actually received.
291
+
292
+ A **410** `LiveResumeGap` (**OKE1014**) means that cursor is gone — `onError` fires, the
293
+ cursor is dropped, and `autoResubscribe` replays the remaining tape after backoff.
294
+
295
+ ```typescript
296
+ import { useLive } from "okengine/client-react";
297
+
298
+ const { events, latest, error, isConnected } = useLive(
299
+ api,
300
+ orderStatus,
301
+ { orderId: "ord_1" },
302
+ {
303
+ autoResubscribe: true,
304
+ },
305
+ );
306
+ ```
264
307
 
265
308
  ## Exports
266
309
 
267
- | Export | Kind | Role |
268
- | ----------------------------------------- | --------- | -------------------------------------------------- |
269
- | `createClient` | function | Typed proxy `api.unit.flow(input?)` |
270
- | `flattenRoutes` | function | `$routes` → flat `unit.flow` REST table |
271
- | `createTransport` | function | Low-level HTTP transport (timeout / retry / auth) |
272
- | `isOk` / `isFail` | function | Envelope predicates |
273
- | `isErrorCode` / `isTransportError` | function | Error narrowing |
274
- | `Client`, `ClientCall`, `ClientResult`, … | types | Contracts, `page.next()` / `for await` of `list()` |
275
- | `Register` | interface | Module-augmentation slot for ambient App types |
276
- | `AppOf` | type | Brand a bare route map as an App |
310
+ | Export | Kind | Role |
311
+ | ----------------------------------------- | --------- | --------------------------------------------------- |
312
+ | `createClient` | function | Typed proxy `api.unit.flow(input?)` plus `api.live` |
313
+ | `flattenRoutes` | function | `$routes` → flat `unit.flow` REST table |
314
+ | `createTransport` | function | Low-level HTTP transport (timeout / retry / auth) |
315
+ | `isOk` / `isFail` | function | Envelope predicates |
316
+ | `isErrorCode` / `isTransportError` | function | Error narrowing |
317
+ | `Client`, `ClientCall`, `ClientResult`, … | types | Contracts, `page.next()` / `for await` of `list()` |
318
+ | `Register` | interface | Module-augmentation slot for ambient App types |
319
+ | `AppOf` | type | Brand a bare route map as an App |
277
320
 
278
321
  Budget: the `./client` export stays under the measured client-runtime cap (hard gate in CI).
279
322
 
@@ -320,6 +363,20 @@ calls `api.auth.refresh({ refreshToken })`. Re-login when rotation fails or no r
320
363
 
321
364
  </Accordion>
322
365
 
366
+ <Accordion title="api.live throws Multiple live exposures">
367
+
368
+ Two routes share the same match shape. Pass `via: "unit.flow"` or call the exposing flow
369
+ (`api.orders.events(input, { onEvent })`).
370
+
371
+ </Accordion>
372
+
373
+ <Accordion title="onError sees LiveResumeGap / HTTP 410">
374
+
375
+ The last `id:` is not on the server tape. The client drops the cursor. With `autoResubscribe: true`
376
+ the next request omits `Last-Event-ID` and replays what remains.
377
+
378
+ </Accordion>
379
+
323
380
  </Accordions>
324
381
 
325
382
  ## Learn more
@@ -329,6 +386,7 @@ calls `api.auth.refresh({ refreshToken })`. Re-login when rotation fails or no r
329
386
  - [Gate](/docs/elements/gate) — policies, `gate.public`, denials
330
387
  - [Store](/docs/elements/store) — `store.resource` and list query language
331
388
  - [Flow](/docs/elements/flow) — `in` / `out` / `errors` and `fx.fail`
389
+ - [Signal](/docs/elements/signal) — `api.live` for `delivery: "live"` SSE
332
390
  - [Errors](/docs/reference/errors) — framework codes vs failure values
333
391
  - [CORS](/docs/plugins/cors) · [CSRF](/docs/plugins/csrf) — browser callers
334
392
  - [CLI Reference](/docs/reference/cli) — `oke client add`, `oke dev`
@@ -181,10 +181,13 @@ i18n: { locales: ["en"], default: "en" },
181
181
 
182
182
  ## tenancy
183
183
 
184
- | Option | Type | Meaning |
185
- | ----------- | --------------------------------- | ---------------------------------------- |
186
- | `isolation` | `"row" \| "schema" \| "database"` | How tenants are separated in the store |
187
- | `resolve` | string \| function | Tenant resolver — reads `ctx.auth.orgId` |
184
+ `oke.config.ts` `tenancy` is **isolation posture** (how rows are separated). Identity —
185
+ who the tenant is is `gate.auth.tenant` on [Gate](/docs/elements/gate#tenants-identity-dimension).
186
+
187
+ | Option | Type | Meaning |
188
+ | ----------- | --------------------------------- | ------------------------------------------------- |
189
+ | `isolation` | `"row" \| "schema" \| "database"` | How tenants are separated in the store |
190
+ | `resolve` | string \| function | Observational resolver (isolation-only manifests) |
188
191
 
189
192
  ## privacy
190
193
 
@@ -29,18 +29,25 @@ string. Custom app codes stay message-less until registered.
29
29
 
30
30
  ## OKE numeric codes
31
31
 
32
- | Code | Name | Cause | Fix |
33
- | ------ | ----------------- | ----------------------------------------------- | -------------------------------------------------- |
34
- | `1001` | undeclared read | Flow reads a resource not in `effects.reads` | Add it to the flow's `effects.reads` |
35
- | `1002` | undeclared write | Flow writes a resource not in `effects.writes` | Add it to the flow's `effects.writes` |
36
- | `1003` | undeclared emit | Flow emits a signal not in `effects.emits` | Add it to the flow's `effects.emits` |
37
- | `1004` | undeclared send | Flow sends a template not in `effects.sends` | Add it to the flow's `effects.sends` |
38
- | `1005` | undeclared ask | Flow asks a prompt not in `effects.asks` | Add it to the flow's `effects.asks` |
39
- | `1006` | undeclared secret | Flow reads a secret not in `effects.secrets` | Add it to the flow's `effects.secrets` |
40
- | `1007` | undeclared call | Flow calls a flow not in `effects.calls` | Add it to the flow's `effects.calls` |
41
- | `1042` | orphan emit | Emit with zero subscribers and `optional` false | Add `on(signal, …)` or declare `optional: true` |
42
- | `1043` | signal schema | Emit payload failed the signal's `schema` | Pass a payload that matches `schema`, or remove it |
43
- | `1101` | schema missing | Domain table absent in `prod` no auto-DDL | Run `oke db migrate` against this environment |
32
+ | Code | Name | Cause | Fix |
33
+ | ------ | -------------------- | ------------------------------------------------- | ---------------------------------------------------- |
34
+ | `1001` | undeclared read | Flow reads a resource not in `effects.reads` | Add it to the flow's `effects.reads` |
35
+ | `1002` | undeclared write | Flow writes a resource not in `effects.writes` | Add it to the flow's `effects.writes` |
36
+ | `1003` | undeclared emit | Flow emits a signal not in `effects.emits` | Add it to the flow's `effects.emits` |
37
+ | `1004` | undeclared send | Flow sends a template not in `effects.sends` | Add it to the flow's `effects.sends` |
38
+ | `1005` | undeclared ask | Flow asks a prompt not in `effects.asks` | Add it to the flow's `effects.asks` |
39
+ | `1006` | undeclared secret | Flow reads a secret not in `effects.secrets` | Add it to the flow's `effects.secrets` |
40
+ | `1007` | undeclared call | Flow calls a flow not in `effects.calls` | Add it to the flow's `effects.calls` |
41
+ | `1011` | HTTP route clash | Two HTTP flows share the same method + path | Give each flow a unique method + path |
42
+ | `1012` | HTTP flow unnamed | Adopted HTTP flow still has no `unit.export` | Export from `flows/<unit>/` or pass a named `flow` |
43
+ | `1013` | live exposure dup | Same signal, gates, and match on two GET routes | Change the gate or path-param filter |
44
+ | `1014` | live resume gap | `Last-Event-ID` is not on the retained tape | Reconnect without the cursor; remaining tape replays |
45
+ | `1015` | tenant required | Tenant-scoped op with no `fx.tenant.id` | `switchTenant`, signed `tid`, or tenant header |
46
+ | `1016` | tenant not member | Client-supplied tenant id is not a membership | Pick from `listTenants` or add the user as a member |
47
+ | `1017` | tenant unknown scope | Tenant role used an invented or `console:*` scope | Use a declared application scope |
48
+ | `1042` | orphan emit | Emit with zero subscribers and `optional` false | Add `on(signal, …)` or declare `optional: true` |
49
+ | `1043` | signal schema | Emit payload failed the signal's `schema` | Pass a payload that matches `schema`, or remove it |
50
+ | `1101` | schema missing | Domain table absent in `prod` — no auto-DDL | Run `oke db migrate` against this environment |
44
51
 
45
52
  <Callout title="Effects are usually inferred">
46
53
  The 1001–1007 family exists for flows that declare effects explicitly. Most apps never write an
@@ -52,11 +59,11 @@ string. Custom app codes stay message-less until registered.
52
59
 
53
60
  Returned, not thrown — the request never reached `do`:
54
61
 
55
- | Code | When | Payload |
56
- | -------------- | -------------------------------------------- | ---------------- |
57
- | `Unauthorized` | Policy denied, request not authenticated | — |
58
- | `Forbidden` | Policy denied, authenticated but not allowed | `gate`, `reason` |
59
- | `RateLimited` | Rate gate budget exhausted | `retryAfterMs` |
62
+ | Code | When | Payload |
63
+ | -------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------ |
64
+ | `Unauthorized` | Policy denied, request not authenticated | — |
65
+ | `Forbidden` | Policy denied, authenticated but not allowed | `gate`, `reason` (`tenant_required` · `not_member` · `unknown_scope` · `session_only` · …) |
66
+ | `RateLimited` | Rate gate budget exhausted | `retryAfterMs` |
60
67
 
61
68
  ## Framework validation failures
62
69
 
@@ -14,11 +14,16 @@ flow("orders.example", {
14
14
  });
15
15
  ```
16
16
 
17
+ <Callout title="Extracted helpers">
18
+ Type a helper's `fx` parameter as `Fx` (`import type {Fx} from "okengine"`). A narrower structural
19
+ type will not match `store()` overloads. See [Flow](/docs/elements/flow).
20
+ </Callout>
21
+
17
22
  ## Stores
18
23
 
19
24
  | Signature | Records | Returns |
20
25
  | ------------------------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------- |
21
- | `fx.store(sqlDecl).select().from(t)…` | `read` | rows (`where` · `orderBy` · `limit` · `offset` chainable) |
26
+ | `fx.store(sqlDecl).select().from(t)…` | `read` | inferred rows (`where` · `orderBy` · `limit` · `offset` chainable) |
22
27
  | `fx.store(sqlDecl).insert(t).values(v)` | `write` | `Promise<void>` |
23
28
  | `fx.store(sqlDecl).update(t).set(v).where(…)` | `write` | `Promise<void>` |
24
29
  | `fx.store(sqlDecl).delete(t).where(…)` | `write` | `Promise<void>` |
@@ -37,6 +42,7 @@ See [Store](/docs/elements/store) for the query-builder surface.
37
42
  | ------------------------------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
38
43
  | `fx.emit(signal, payload?, { key? })` | `emit` | Commits the signal outbox when the call resolves; optional `key` serializes `once` per key; stamps producer run id as `parentRunId` for trace chains; throws **OKE1042** (orphan) or **OKE1043** (schema) |
39
44
  | `fx.deadLetters(signal)` | `read` `signal:<name>` | Dead-lettered messages for that signal. Payload typed from `SignalDecl<T>`. Page with `fx.json.withQuery`. Cross-signal throws **OKE1001**. |
45
+ | `fx.live(signal, { match? })` | `read` `signal:<name>` | Live tape as SSE. Returns `JsonStreamResult` (object chunks, `id:` on the wire). Cross-signal throws **OKE1001**. Do not wrap with `fx.json.stream`. |
40
46
 
41
47
  ## Runs (observability read)
42
48
 
@@ -72,20 +78,29 @@ on(
72
78
 
73
79
  ## Flows
74
80
 
75
- | Signature | Records | Returns / notes |
76
- | ---------------------------- | ------- | ----------------------------------------------------- |
77
- | `fx.call(flow, input?)` | `call` | The callee's `out` — runs through the same pipeline |
78
- | `fx.step(name, fn)` | — | Durable step: replays from the journal, never re-runs |
79
- | `fx.all([...thunks])` | — | Parallel; first rejection aborts siblings |
80
- | `fx.race([...thunks])` | — | First settle wins; losers aborted |
81
- | `fx.retry(fn, opts?)` | — | Exponential backoff + jitter (plain Promise) |
82
- | `fx.using(acq, rel, use)` | — | `release` runs once on settle or ambient abort |
83
- | `fx.signal` | — | Ambient `AbortSignal` for the current branch |
84
- | `fx.fail(code, data, opts?)` | — | Typed failure value (`opts.message` overrides) |
85
-
86
- `fx.call` starts the callee with an **empty** `fx.auth` (fail-closed for authorization). For
87
- audit/attribution only, read `fx.principal` it propagates the originating identity without
88
- copying into `fx.auth`. Gates never consult `fx.principal`.
81
+ | Signature | Records | Returns / notes |
82
+ | ------------------------------------------------------------------------------ | ----------------------- | -------------------------------------------------------------------------------------------------- |
83
+ | `fx.call(flow, input?)` | `call` | The callee's `out` — runs through the same pipeline |
84
+ | `fx.step(name, fn)` | — | Durable step: replays from the journal, never re-runs |
85
+ | `fx.all([...thunks])` | — | Parallel; first rejection aborts siblings |
86
+ | `fx.race([...thunks])` | — | First settle wins; losers aborted |
87
+ | `fx.retry(fn, opts?)` | — | Exponential backoff + jitter (plain Promise) |
88
+ | `fx.using(acq, rel, use)` | — | `release` runs once on settle or ambient abort |
89
+ | `fx.signal` | — | Ambient `AbortSignal` for the current branch |
90
+ | `fx.fail(code, data, opts?)` | — | Typed failure value (`opts.message` overrides) |
91
+ | `fx.auth.createApiKey({ name, scopes, expiresIn?, ipAllowlist?, rateLimit? })` | `write` `auth:api-keys` | Secret once. Creator is live `userId` / `scopes`. Session only. `ipAllowlist` is IPs or hostnames. |
92
+ | `fx.auth.listApiKeys()` | `read` `auth:api-keys` | Keys this session minted |
93
+ | `fx.auth.revokeApiKey(id)` | `write` `auth:api-keys` | Owner only |
94
+ | `fx.auth.rotateApiKey(id)` | `write` `auth:api-keys` | New secret once. Owner only |
95
+ | `fx.auth.updateApiKey(id, …)` | `write` `auth:api-keys` | Name / scopes / expiry / allowlist / rate. Re-attenuates |
96
+ | `fx.auth.listTenants()` | `read` `auth:tenants` | Memberships for the live session. Session only |
97
+ | `fx.auth.switchTenant(id)` | `write` `auth:tenants` | New access+refresh, new family, `tid` on both. Never Set-Cookie. Session only |
98
+ | `fx.auth.createTenant({ name, slug?, id? })` | `write` `auth:tenants` | Creator becomes a member. Session only |
99
+ | `fx.auth.upsertTenantRole({ tenantId, roleName, scopes })` | `write` `auth:tenants` | Application scopes only — `console:*` is unknown_scope |
100
+
101
+ `fx.call` starts the callee with an **empty** `fx.auth` (fail-closed for authorization) and
102
+ propagates `fx.tenant.id`. For audit/attribution only, read `fx.principal` — it propagates the
103
+ originating identity without copying into `fx.auth`. Gates never consult `fx.principal`.
89
104
 
90
105
  ## Concurrency and retry
91
106
 
@@ -204,6 +219,7 @@ keys. Use `cache: false` to opt out, or `cache: "30s"` for a TTL.
204
219
  | `fx.json.with(page)` / `with(data, meta)` | 200 | `{ data, meta, error: null }` — already-built pager |
205
220
  | `fx.json.withQuery(rows, input, spec?)` | 200 | In-memory list page — zero-config `q` / auto-eq / PostgREST |
206
221
  | `fx.json.stream(chunks)` | 200 | `text/event-stream` — JSON `data:` frames, then `data: [DONE]` |
222
+ | `fx.live(signal)` | 200 | Same SSE carrier for a live signal (payload frames + `id:`) |
207
223
 
208
224
  Returning a plain value instead answers 200 with `{ data: value, error: null }` — the helpers exist for status and `meta` control. Pass `fx.stream(...)` into `fx.json.stream` to reach the HTTP client token-by-token.
209
225
 
@@ -265,15 +281,16 @@ Built-in `errors.*` / `oke.*` catalogs localize `fx.fail` messages and thrown
265
281
 
266
282
  ## Principals
267
283
 
268
- | Property | Shape |
269
- | -------------- | ------------------------------------------------------------------------- |
270
- | `fx.auth` | `{ userId: string \| null, scopes: Set<string>, verified?: boolean }` |
271
- | `fx.operator` | `{ id: string \| null }` — Console plane |
272
- | `fx.principal` | Read-only origin: `userId`, `operatorId`, `scopes`, `verified?`, `plane?` |
273
- | `fx.tenant` | `{ id: string \| null }` — active tenant |
284
+ | Property | Shape |
285
+ | -------------- | ------------------------------------------------------------------------------------- |
286
+ | `fx.auth` | `{ userId, scopes, verified?, apiKeyId? }` plus key and tenant methods (session only) |
287
+ | `fx.operator` | `{ id: string \| null }` — Console plane |
288
+ | `fx.principal` | Read-only origin: `userId`, `operatorId`, `scopes`, `verified?`, `plane?` |
289
+ | `fx.tenant` | `{ id: string \| null }` — active tenant (propagates on `fx.call`) |
274
290
 
275
291
  **Consequence:** use `fx.auth` / gates for authorization; use `fx.principal` only when a callee
276
- must log who started the call chain.
292
+ must log who started the call chain. A key Bearer sets `userId` to the issuer and `apiKeyId`
293
+ to the key — see [Gate](/docs/elements/gate#api-keys).
277
294
 
278
295
  ## Not on `fx`
279
296
 
@@ -47,7 +47,7 @@ Our path is concrete: a booking name containing "ignore previous instructions an
47
47
  - **Store KV performance** (`QUERY /console/store/kv/stats`) is Redis-wire `INFO` / `COMMANDSTATS` / `SLOWLOG` / `LATENCY` — not Store browse. INFO is instance-wide (`StoreKvStatsServerWideGap`). SLOWLOG args are keys and values — collapsed until `revealPii: true`, which writes `console.store.kv.stats.reveal` (operator, ref). Named limitation `StoreKvStatsSlowlogArgsGap`. `memory` returns `KvStatsUnsupported`. This surface does not run `MONITOR` and does not invent a hot-key table.
48
48
  - **Runs SQL** (`POST /console/runs/query`, Observability SQL tab) is operator-session, read-only, 5s timeout, 1000-row cap. DuckDB filesystem access is disabled for the statement. Masking is column-key only (`dim_*` and JSON blobs `input` / `output` / `logs` / `dimensions`). Aliases and expressions can leak classified values — named limitation `RunsQueryPiiProjectionGap`. This is **not** the `projectRun` guarantee. `revealPii: true` writes `console.runs.query.reveal`. Flow `fx.runs.query` stays unrestricted app SQL.
49
49
  - **Plugin panels** run in a sandboxed iframe without `allow-same-origin`, communicating only over a `postMessage` bridge with their own CSP, no access to the operator session token, and exposure limited to that plugin's declared flows.
50
- - **Session and framing:** `frame-ancestors 'none'`, `SameSite=Strict`, step-up authentication before destructive actions. An expired operator session redirects to Sign in with `?next=` set to the current `/overview`, `/flows`, `/store`, `/vault`, or `/observability` href (search included); after login the operator returns there. `/monitoring` is rewritten to `/observability`. Any other `next` (including `/units`) is dropped so the gate cannot be used as an open redirect. Unknown Console paths are 404.
50
+ - **Session and framing:** `frame-ancestors 'none'`, `SameSite=Strict`, step-up authentication before destructive actions. An expired operator session redirects to Sign in with `?next=` set to the current `/overview`, `/flows`, `/store`, `/vault`, `/access`, or `/observability` href (search included); after login the operator returns there. `/monitoring` is rewritten to `/observability`. Any other `next` (including `/units`) is dropped so the gate cannot be used as an open redirect. Unknown Console paths are 404.
51
51
  - **Secret write path:** TLS required, no autocomplete, never echoed, never logged, not retained in browser memory after submission. Vault set / rotate / rotate-master use typed confirm (`SET` / `ROTATE` / `ROTATE_MASTER`). The Console never accepts a master key in the HTTP body (`OKE_VAULT_MASTER_KEY` only). `GET /console/vault/audit/verify` is read-only (no typed phrase).
52
52
  - **The setup claim code** is rate-limited and compared in constant time. It is
53
53
  printed on the `oke dev` board (TTY only — never on `GET /console/setup/status`)
@@ -0,0 +1,47 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { createApiKey, createApiKeyStore } from "./api-keys.ts";
3
+ import { bindApiKeySqlPersist, hydrateApiKeyStore, type ApiKeySqlExec } from "./api-key-sql.ts";
4
+
5
+ describe("api-key SQL persist", () => {
6
+ test("hydrate + persist round-trip", async () => {
7
+ const rows = new Map<string, Record<string, unknown>>();
8
+ const sql: ApiKeySqlExec = {
9
+ async execute(_q, params) {
10
+ const id = String(params[0]);
11
+ rows.set(id, {
12
+ id,
13
+ plane: params[1],
14
+ hash: params[2],
15
+ name: params[3],
16
+ scopes: params[4],
17
+ expires_at: params[5],
18
+ rate_limit: params[6],
19
+ ip_allowlist: params[7],
20
+ creator_id: params[8],
21
+ creator_scopes: params[9],
22
+ created_at: params[10],
23
+ last_used_at: params[11],
24
+ revoked_at: params[12],
25
+ });
26
+ },
27
+ async all() {
28
+ return [...rows.values()];
29
+ },
30
+ };
31
+ const store = createApiKeyStore();
32
+ bindApiKeySqlPersist(store, sql);
33
+ const created = await createApiKey(store, {
34
+ plane: "user",
35
+ name: "sql",
36
+ scopes: ["member"],
37
+ creatorId: "u1",
38
+ creatorScopes: ["member"],
39
+ id: "key_sql",
40
+ });
41
+ const other = createApiKeyStore();
42
+ await hydrateApiKeyStore(sql, other);
43
+ expect(other.keys.get("key_sql")?.name).toBe("sql");
44
+ expect(other.keys.get("key_sql")?.creatorId).toBe("u1");
45
+ expect(other.keys.get("key_sql")?.hash).toBe(created.row.hash);
46
+ });
47
+ });