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
package/src/kernel/fx.ts CHANGED
@@ -20,6 +20,7 @@ import type {
20
20
  IndexStoreFxHandle,
21
21
  KvStoreDecl,
22
22
  KvStoreFxHandle,
23
+ SelectFromBuilder,
23
24
  SelectOrderBuilder,
24
25
  SqlStoreDecl,
25
26
  StoreDecl,
@@ -36,6 +37,12 @@ import type { VaultActor, VaultAdapter, VaultRuntime } from "../elements/vault.t
36
37
  import type { ChannelRuntime } from "../elements/channel.ts";
37
38
  import type { AiRuntime } from "../elements/ai.ts";
38
39
  import { parseDurationMs } from "../elements/clock/duration.ts";
40
+ import type { ApiKeyStore } from "../auth/api-keys.ts";
41
+ import type { FxAuthIdentity, FxAuthKeyMethods } from "./fx-auth-keys.ts";
42
+ import type { FxAuthTenantMethods } from "./fx-auth-tenants.ts";
43
+ import type { TenantStore } from "../auth/tenants.ts";
44
+ import type { SessionCrypto, SessionStore } from "../auth/sessions.ts";
45
+ import type { Manifest } from "../manifest/types.ts";
39
46
  import { createCapabilityToken, type CapabilityToken } from "./capability.ts";
40
47
  import { createEffectLedger, recordEffect, reversibilityOf, type EffectLedger } from "./effects.ts";
41
48
  import { resolveDurationMs } from "./elapsed.ts";
@@ -73,11 +80,27 @@ async function loadRunsWindow(): Promise<typeof import("../runs/window.ts")> {
73
80
  return import("../runs/window.ts");
74
81
  }
75
82
 
83
+ /**
84
+ * Sync-load live SSE + Last-Event-ID resume only when `fx.live` runs.
85
+ * A static import would pin `checkLiveResume` / 410 encoding on every
86
+ * createFx — including Store-only `oke()` graphs.
87
+ */
88
+ function loadFxLiveStream(): typeof import("./fx-live-stream.ts") {
89
+ return lazyRequire(import.meta.dir, ["fx", "live", "stream"].join("-"));
90
+ }
91
+
76
92
  /** Resource ref Flows declare to read the Runs store via {@link Fx.runs}. */
77
93
  export const RUNS_RESOURCE = "runs";
78
94
 
79
95
  /**
80
- * Capability ref for {@link Fx.deadLetters} `signal:<name>`, never a store facet.
96
+ * Resource ref Flows declare for {@link Fx.auth} key management.
97
+ * Same string as `src/auth/api-keys.ts` — defined here so the HMAC / key-store
98
+ * module stays off the edge and Store-only graphs.
99
+ */
100
+ export const AUTH_API_KEYS_RESOURCE = "auth:api-keys";
101
+
102
+ /**
103
+ * Capability ref for {@link Fx.deadLetters} / {@link Fx.live} — `signal:<name>`, never a store facet.
81
104
  *
82
105
  * @param name - Signal name
83
106
  */
@@ -103,13 +126,10 @@ export function resolveStoreRef(ref: NamedRef | { readonly ref: ResourceRef }):
103
126
  return ref.name as ResourceRef;
104
127
  }
105
128
 
129
+ export type { FxAuthIdentity } from "./fx-auth-keys.ts";
130
+
106
131
  /** Auth principal on the user plane. */
107
- export interface FxAuth {
108
- readonly userId: string | null;
109
- readonly scopes: ReadonlySet<string>;
110
- /** Whether the identity has completed verification (email / MFA). */
111
- readonly verified?: boolean;
112
- }
132
+ export interface FxAuth extends FxAuthIdentity, FxAuthKeyMethods, FxAuthTenantMethods {}
113
133
 
114
134
  /** Operator principal on the Console plane. */
115
135
  export interface FxOperator {
@@ -359,16 +379,46 @@ export interface JsonResult<T = unknown> {
359
379
  readonly kind?: undefined;
360
380
  }
361
381
 
362
- /** SSE carrier from {@link FxJson.stream}. */
382
+ /** SSE carrier from {@link FxJson.stream} / {@link Fx.live}. */
363
383
  export interface JsonStreamResult {
364
384
  readonly [jsonResultBrand]: true;
365
385
  readonly kind: "stream";
366
386
  readonly status: 200;
367
- readonly chunks: AsyncIterable<string>;
387
+ readonly chunks: AsyncIterable<unknown>;
388
+ /** Awaited before the 200 SSE body; throws OKE1014 on a missing resume cursor. */
389
+ ready?: () => Promise<void>;
368
390
  /** Set by the kernel to commit journal / Runs after the stream settles. */
369
391
  finalize?: () => Promise<void>;
370
392
  }
371
393
 
394
+ const sseFrameBrand: unique symbol = Symbol.for("oke.sse.frame");
395
+
396
+ /** One SSE frame — optional `id:` plus JSON `data:`. */
397
+ export interface SseFrame {
398
+ readonly [sseFrameBrand]: true;
399
+ readonly data: unknown;
400
+ readonly id?: string;
401
+ }
402
+
403
+ /**
404
+ * Brand a stream chunk so {@link encodeSseStream} can emit `id:`.
405
+ *
406
+ * @param data - JSON payload
407
+ * @param id - Optional SSE id
408
+ */
409
+ export function sseFrame(data: unknown, id?: string): SseFrame {
410
+ return id !== undefined ? { [sseFrameBrand]: true, data, id } : { [sseFrameBrand]: true, data };
411
+ }
412
+
413
+ /**
414
+ * True when `value` is a branded SSE frame.
415
+ *
416
+ * @param value - Unknown
417
+ */
418
+ export function isSseFrame(value: unknown): value is SseFrame {
419
+ return typeof value === "object" && value !== null && (value as SseFrame)[sseFrameBrand] === true;
420
+ }
421
+
372
422
  /** True when `value` is an {@link FxJson} JSON-envelope carrier. */
373
423
  export function isJsonResult(value: unknown): value is JsonResult {
374
424
  return (
@@ -422,9 +472,9 @@ export interface FxJson {
422
472
  withQuery<T>(rows: readonly T[], input: unknown, spec?: QueryPageSpec<T>): JsonResult<T[]>;
423
473
  /**
424
474
  * 200 — `text/event-stream` of JSON `data:` frames, then `data: [DONE]`.
425
- * Pass {@link Fx.stream} or any async iterable of token strings.
475
+ * Pass {@link Fx.stream} or any async iterable of chunks.
426
476
  */
427
- stream(chunks: AsyncIterable<string>): JsonStreamResult;
477
+ stream(chunks: AsyncIterable<unknown>): JsonStreamResult;
428
478
  }
429
479
 
430
480
  /**
@@ -638,6 +688,29 @@ export interface Fx {
638
688
  */
639
689
  deadLetters<T>(signal: SignalDecl<T>): Promise<readonly DeadLetter<T>[]>;
640
690
  deadLetters(signal: NamedRef): Promise<readonly DeadLetter[]>;
691
+ /**
692
+ * Stream a `delivery: "live"` signal as SSE (records `read` on `signal:<name>`).
693
+ *
694
+ * HTTP `Last-Event-ID` is applied when `opts.afterId` is omitted. A missing
695
+ * cursor throws OKE1014; `JsonStreamResult.ready` turns that into HTTP 410.
696
+ *
697
+ * @param signal - Signal name or handle
698
+ * @param opts - Payload filter and optional resume cursor
699
+ */
700
+ live<T>(
701
+ signal: SignalDecl<T>,
702
+ opts?: {
703
+ readonly match?: (payload: T) => boolean;
704
+ readonly afterId?: string;
705
+ },
706
+ ): JsonStreamResult;
707
+ live(
708
+ signal: NamedRef,
709
+ opts?: {
710
+ readonly match?: (payload: unknown) => boolean;
711
+ readonly afterId?: string;
712
+ },
713
+ ): JsonStreamResult;
641
714
  /**
642
715
  * Call another flow (records `call`). Stub returns `undefined`.
643
716
  *
@@ -837,8 +910,27 @@ export interface CreateFxOptions {
837
910
  readonly capability?: CapabilityToken;
838
911
  /** Injectable clock for timestamps / `fx.clock.now`. */
839
912
  readonly now?: () => number;
840
- /** Auth principal. */
841
- readonly auth?: FxAuth;
913
+ /** Auth principal bag (key methods attach at create time). */
914
+ readonly auth?: FxAuthIdentity;
915
+ /** Shared API key store for {@link Fx.auth} key methods. */
916
+ readonly apiKeyStore?: ApiKeyStore;
917
+ /** Tenant registry (when `gate.auth.tenant` is on). */
918
+ readonly tenantStore?: TenantStore;
919
+ /** Session store for {@link Fx.auth.switchTenant}. */
920
+ readonly sessions?: SessionStore;
921
+ /** Session crypto for {@link Fx.auth.switchTenant}. */
922
+ readonly sessionCrypto?: SessionCrypto;
923
+ /** Manifest for tenant-role catalog validation. */
924
+ readonly manifest?: Manifest | null;
925
+ /** When true, tenant-scoped KV / vault defaults apply. */
926
+ readonly tenantEnabled?: boolean;
927
+ /**
928
+ * When false, skip tenant-role scope union (tenant-unaware flow).
929
+ * Default true when {@link tenantEnabled}.
930
+ */
931
+ readonly flowTenantScoped?: boolean;
932
+ /** Current flow plane (tenant-role union is user-plane only). */
933
+ readonly flowPlane?: "user" | "operator";
842
934
  /** Operator principal. */
843
935
  readonly operator?: FxOperator;
844
936
  /**
@@ -911,6 +1003,8 @@ export interface CreateFxOptions {
911
1003
  * as `parentRunId` so consuming Flows can join the trace chain.
912
1004
  */
913
1005
  readonly runId?: string;
1006
+ /** HTTP `Last-Event-ID` for {@link Fx.live} resume (tests may pass `opts.afterId`). */
1007
+ readonly lastEventId?: string;
914
1008
  /** Reveal PII through the store runtime (requires `pii:reveal` upstream). */
915
1009
  readonly revealPii?: boolean;
916
1010
  /** Trigger gate names for RLS (`oke.gate` = first policy/public). */
@@ -1001,12 +1095,20 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1001
1095
  }
1002
1096
  const cacheStore = new Map<string, unknown>();
1003
1097
 
1004
- const auth: FxAuth = options.auth ?? {
1005
- userId: null,
1006
- scopes: new Set(),
1007
- };
1098
+ // Computed stem a static import would pin HMAC / api-keys on every createFx.
1099
+ const authBag = options.auth ?? { userId: null, scopes: new Set() };
1100
+ const keys = lazyRequire<typeof import("./fx-auth-keys.ts")>(
1101
+ import.meta.dir,
1102
+ ["fx", "auth", "keys"].join("-"),
1103
+ ).attach({
1104
+ auth: authBag,
1105
+ store: options.apiKeyStore,
1106
+ now,
1107
+ gated,
1108
+ });
1109
+ const auth: FxAuth = keys as FxAuth;
1008
1110
  const operator: FxOperator = options.operator ?? { id: null };
1009
- const tenant: FxTenant = options.tenant ?? { id: null };
1111
+ const tenant: { id: string | null } = options.tenant ?? { id: null };
1010
1112
  const defaultLocale = options.i18n?.defaultLocale ?? "en";
1011
1113
  const locale = options.i18n?.locale ?? defaultLocale;
1012
1114
  const catalogs = options.i18n?.catalogs ?? {};
@@ -1042,6 +1144,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1042
1144
  gateNames: options.rlsGateNames ?? [],
1043
1145
  bypass: false,
1044
1146
  operator: false,
1147
+ ...(options.tenantEnabled === true ? { tenantId: tenant.id } : {}),
1045
1148
  });
1046
1149
  return identity ? { rls: identity } : {};
1047
1150
  }
@@ -1171,9 +1274,9 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1171
1274
  get driverId() {
1172
1275
  return cached?.driverId ?? "memory";
1173
1276
  },
1174
- select(columns?) {
1277
+ select: ((columns?: unknown) => {
1175
1278
  return {
1176
- from(table) {
1279
+ from(table: unknown) {
1177
1280
  const run = (plan: {
1178
1281
  where?: unknown;
1179
1282
  orders?: readonly unknown[];
@@ -1182,7 +1285,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1182
1285
  }): Promise<SqlRow[]> =>
1183
1286
  gatedTable("read", table, async () => {
1184
1287
  const h = await ensure();
1185
- const from = h.select(columns).from(table);
1288
+ const from = h.select(columns).from(table) as SelectFromBuilder;
1186
1289
  const filtered = plan.where === undefined ? from : from.where(plan.where);
1187
1290
  const ordered =
1188
1291
  plan.orders === undefined ? filtered : filtered.orderBy(...plan.orders);
@@ -1206,7 +1309,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1206
1309
  });
1207
1310
 
1208
1311
  return {
1209
- where(where) {
1312
+ where(where: unknown) {
1210
1313
  return {
1211
1314
  ...tail({ where }),
1212
1315
  orderBy: (...orders: readonly unknown[]) => tail({ where, orders }),
@@ -1219,13 +1322,16 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1219
1322
  offset(n: number) {
1220
1323
  return run({ offset: n });
1221
1324
  },
1222
- then(onfulfilled, onrejected) {
1325
+ then(
1326
+ onfulfilled: (value: SqlRow[]) => unknown,
1327
+ onrejected?: (reason: unknown) => unknown,
1328
+ ) {
1223
1329
  return run({}).then(onfulfilled, onrejected);
1224
1330
  },
1225
1331
  };
1226
1332
  },
1227
1333
  };
1228
- },
1334
+ }) as SqlStoreHandle["select"],
1229
1335
  insert(table) {
1230
1336
  return {
1231
1337
  values(row) {
@@ -1338,6 +1444,33 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1338
1444
  } as SqlStoreHandle;
1339
1445
  }
1340
1446
 
1447
+ function loadFxTenantStore(): {
1448
+ kv: (
1449
+ mode: "in" | "out",
1450
+ tenantId: string | null,
1451
+ decl: KvStoreDecl,
1452
+ enabled: boolean,
1453
+ prop: string | symbol,
1454
+ payload: unknown,
1455
+ ) => unknown;
1456
+ path: (
1457
+ tenantId: string | null,
1458
+ enabled: boolean,
1459
+ contracts: { get(name: string): { perTenant?: boolean } | undefined } | undefined,
1460
+ name: string,
1461
+ ) => string;
1462
+ missingVault: (storagePath: string) => Error;
1463
+ } {
1464
+ return lazyRequire(import.meta.dir, ["fx", "tenant", "store"].join("-"));
1465
+ }
1466
+
1467
+ const tenantOn = options.tenantEnabled === true;
1468
+
1469
+ function tenantVaultPath(name: string): string {
1470
+ if (!tenantOn) return name;
1471
+ return loadFxTenantStore().path(tenant.id, true, options.vaultRuntime?.contracts, name);
1472
+ }
1473
+
1341
1474
  function storeHandle(ref: SqlStoreDecl): SqlStoreHandle;
1342
1475
  function storeHandle(ref: KvStoreDecl): KvStoreFxHandle;
1343
1476
  function storeHandle(ref: FilesStoreDecl): FilesStoreFxHandle;
@@ -1589,7 +1722,19 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1589
1722
  const vaultSurface: FxVault = {
1590
1723
  get(secret) {
1591
1724
  const name = resolveName(secret);
1592
- return gatedSecret(name, () => {
1725
+ return gatedSecret(name, async () => {
1726
+ const path = tenantVaultPath(name);
1727
+ if (path !== name) {
1728
+ if (options.vaultAdapter) {
1729
+ const rec = await options.vaultAdapter.get(path);
1730
+ if (!rec) {
1731
+ throw loadFxTenantStore().missingVault(path);
1732
+ }
1733
+ return new Redacted(rec.value);
1734
+ }
1735
+ const value = secrets[path] ?? secrets[name] ?? `[secret:${path}]`;
1736
+ return new Redacted(value);
1737
+ }
1593
1738
  const value = options.vaultRuntime
1594
1739
  ? options.vaultRuntime.read(name)
1595
1740
  : (secrets[name] ?? `[secret:${name}]`);
@@ -1600,7 +1745,8 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1600
1745
  const name = resolveName(path);
1601
1746
  return gatedSecret(name, async () => {
1602
1747
  refuseDryRunVaultWrite("set", name);
1603
- const written = await vaultAdapterFor("set").set(name, value, {
1748
+ const storage = tenantVaultPath(name);
1749
+ const written = await vaultAdapterFor("set").set(storage, value, {
1604
1750
  ...(setOptions?.ttlMs !== undefined ? { ttlMs: setOptions.ttlMs } : {}),
1605
1751
  ...(setOptions?.metadata !== undefined ? { metadata: setOptions.metadata } : {}),
1606
1752
  actor: vaultActor,
@@ -1612,7 +1758,8 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1612
1758
  const name = resolveName(path);
1613
1759
  return gatedSecret(name, async () => {
1614
1760
  refuseDryRunVaultWrite("rotate", name);
1615
- const written = await vaultAdapterFor("rotate").rotate(name, value, {
1761
+ const storage = tenantVaultPath(name);
1762
+ const written = await vaultAdapterFor("rotate").rotate(storage, value, {
1616
1763
  actor: vaultActor,
1617
1764
  });
1618
1765
  return { path: written.path, version: written.version };
@@ -1622,7 +1769,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1622
1769
  const name = resolveName(path);
1623
1770
  return gatedSecret(name, async () => {
1624
1771
  refuseDryRunVaultWrite("delete", name);
1625
- return vaultAdapterFor("delete").delete(name, { actor: vaultActor });
1772
+ return vaultAdapterFor("delete").delete(tenantVaultPath(name), { actor: vaultActor });
1626
1773
  });
1627
1774
  },
1628
1775
  async list(prefix) {
@@ -1705,6 +1852,18 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1705
1852
  return options.signalRuntime.deadLetters(name);
1706
1853
  });
1707
1854
  },
1855
+ live(
1856
+ signal: NamedRef,
1857
+ opts?: { readonly match?: (payload: unknown) => boolean; readonly afterId?: string },
1858
+ ) {
1859
+ return loadFxLiveStream().createLiveStream({
1860
+ name: resolveName(signal),
1861
+ afterId: opts?.afterId ?? options.lastEventId,
1862
+ match: opts?.match,
1863
+ gated,
1864
+ signalRuntime: options.signalRuntime,
1865
+ }) as JsonStreamResult;
1866
+ },
1708
1867
  call(flow, input) {
1709
1868
  const name = resolveName(flow);
1710
1869
  return gated("call", name, async () => {
@@ -1985,5 +2144,24 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1985
2144
  },
1986
2145
  };
1987
2146
 
2147
+ if (options.tenantStore !== undefined || options.tenantEnabled === true) {
2148
+ lazyRequire<{
2149
+ install: (
2150
+ bag: { auth: FxAuthIdentity; store: (ref: never) => object },
2151
+ ctx: {
2152
+ readonly tenantId: string | null;
2153
+ readonly options: typeof options;
2154
+ readonly now: () => number;
2155
+ readonly gated: typeof gated;
2156
+ },
2157
+ ) => void;
2158
+ }>(import.meta.dir, ["fx", "tenant", "store"].join("-")).install(fx, {
2159
+ tenantId: tenant.id,
2160
+ options,
2161
+ now,
2162
+ gated,
2163
+ });
2164
+ }
2165
+
1988
2166
  return { fx, ledger, capability };
1989
2167
  }
@@ -156,7 +156,7 @@ export type PipelineEncoder = (result: {
156
156
  readonly failure?: FlowFailure | undefined;
157
157
  readonly output?: unknown;
158
158
  readonly error?: unknown;
159
- }) => Response;
159
+ }) => Response | Promise<Response>;
160
160
 
161
161
  /**
162
162
  * Run the seven-stage pipeline around `handler`.
@@ -234,7 +234,7 @@ export async function runPipeline(
234
234
  // (short-circuit wins; encoder input mirrors the app layer exactly).
235
235
  if (encode !== undefined && ctx.response === undefined) {
236
236
  const failure = isFlowFailure(ctx.error) ? ctx.error : undefined;
237
- ctx.response = encode({ failure, output: ctx.result, error: ctx.error });
237
+ ctx.response = await encode({ failure, output: ctx.result, error: ctx.error });
238
238
  }
239
239
 
240
240
  await runStage("onResponse", hooks, ctx, fx);
@@ -22,37 +22,28 @@ import {
22
22
  * @param path - Base path (`/notes`)
23
23
  * @param ops - The five FlowDefs (usually `resource.all()`)
24
24
  * @param gates - Shared gate chain
25
- * @param isLive - Live on GET list + get
26
25
  */
27
26
  export function httpResource<P extends string>(
28
27
  path: P,
29
28
  ops: ResourceFlowBag,
30
29
  gates: readonly GateRef[] = [],
31
- isLive = false,
32
30
  ): ResourceMount {
33
31
  const id = `${path}/:id` as `${P}/:id`;
34
- const verb = <M extends HttpMethod>(
35
- method: M,
36
- p: P | `${P}/:id`,
37
- live: boolean,
38
- ): HttpTrigger<M> => createHttpTrigger(method, p, gates, live);
32
+ const verb = <M extends HttpMethod>(method: M, p: P | `${P}/:id`): HttpTrigger<M> =>
33
+ createHttpTrigger(method, p, gates);
39
34
  const mount: ResourceMount = {
40
35
  [resourceMountBrand]: true,
41
36
  gates,
42
- isLive,
43
37
  mounts: [
44
- { trigger: verb("GET", path, isLive), flow: ops.list },
45
- { trigger: verb("POST", path, false), flow: ops.create },
46
- { trigger: verb("GET", id, isLive), flow: ops.get },
47
- { trigger: verb("PATCH", id, false), flow: ops.update },
48
- { trigger: verb("DELETE", id, false), flow: ops.remove },
38
+ { trigger: verb("GET", path), flow: ops.list },
39
+ { trigger: verb("POST", path), flow: ops.create },
40
+ { trigger: verb("GET", id), flow: ops.get },
41
+ { trigger: verb("PATCH", id), flow: ops.update },
42
+ { trigger: verb("DELETE", id), flow: ops.remove },
49
43
  ],
50
- gate: createGateAttach((next) => httpResource(path, ops, next, isLive), gates),
44
+ gate: createGateAttach((next) => httpResource(path, ops, next), gates),
51
45
  public() {
52
- return httpResource(path, ops, [...gates, GATE_PUBLIC_NAME], isLive);
53
- },
54
- live() {
55
- return httpResource(path, ops, gates, true);
46
+ return httpResource(path, ops, [...gates, GATE_PUBLIC_NAME]);
56
47
  },
57
48
  };
58
49
  return mount;
@@ -78,6 +78,7 @@ export type {
78
78
  AppFlowRoute,
79
79
  AppRouteMap,
80
80
  FlowNamespace,
81
+ LiveAppFlowRoute,
81
82
  RouteFromFlow,
82
83
  RoutesFromAdoptArgs,
83
84
  } from "./adopt-routes.ts";
@@ -359,6 +360,7 @@ export {
359
360
  type HttpMethod,
360
361
  type HttpTrigger,
361
362
  type InternalTrigger,
363
+ type LiveHttpTrigger,
362
364
  type ResourceFlowBag,
363
365
  type ResourceMount,
364
366
  type SignalAsTrigger,
@@ -104,6 +104,8 @@ export interface JournalRun {
104
104
  leaseExpiresAt?: number;
105
105
  readonly createdAt: number;
106
106
  updatedAt: number;
107
+ /** Isolation context for resume (`fx.tenant`). */
108
+ tenant?: string | null;
107
109
  }
108
110
 
109
111
  /**
@@ -366,6 +368,12 @@ export interface JournalSession {
366
368
  readonly runId: string;
367
369
  /** Underlying run snapshot (mutated as entries append). */
368
370
  readonly run: JournalRun;
371
+ /**
372
+ * Persist isolation context so resume restamps {@link Fx.tenant}.
373
+ *
374
+ * @param id - Tenant id (null clears)
375
+ */
376
+ stampTenant(id: string | null): Promise<void>;
369
377
  /**
370
378
  * Replay or execute a named step. Never re-runs `fn` when already journaled.
371
379
  *
@@ -607,6 +615,10 @@ export function createJournal(options: CreateJournalOptions): Journal {
607
615
  undoStack() {
608
616
  return undos;
609
617
  },
618
+ async stampTenant(id) {
619
+ run.tenant = id;
620
+ await persist();
621
+ },
610
622
  beginRegistrationPass() {
611
623
  registrationPass = true;
612
624
  },
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Live HTTP exposure uniqueness (OKE1013) and GET-only synthesis.
3
+ */
4
+
5
+ import { describe, expect, test, beforeEach } from "bun:test";
6
+ import { z } from "zod";
7
+ import { gate } from "../elements/gate.ts";
8
+ import { signal, resetSignals } from "../elements/signal/declare.ts";
9
+ import { oke } from "./app.ts";
10
+ import { resetFlowSeq } from "./flow.ts";
11
+ import { on, resetBindings } from "./on.ts";
12
+ import { stampFlowName } from "./stamp-http.ts";
13
+ import { http } from "./triggers.ts";
14
+
15
+ const member = gate.policy("member", ({ auth }) => !!auth.verified);
16
+ const admin = gate.policy("admin", ({ auth }) => !!auth.verified);
17
+ const partner = gate.policy("partner", ({ auth }) => !!auth.verified);
18
+
19
+ const orderStatus = () =>
20
+ signal("order-status", {
21
+ delivery: "live",
22
+ optional: true,
23
+ schema: z.object({
24
+ orderId: z.string(),
25
+ status: z.enum(["placed", "fulfilling", "shipped"]),
26
+ }),
27
+ });
28
+
29
+ beforeEach(() => {
30
+ resetBindings();
31
+ resetFlowSeq();
32
+ resetSignals();
33
+ });
34
+
35
+ describe("live HTTP uniqueness", () => {
36
+ test("member :orderId + admin firehose both boot", () => {
37
+ const sig = orderStatus();
38
+ const events = on(http.get("/orders/:orderId/events").gate(member).live(sig));
39
+ const feed = on(http.get("/admin/order-status").gate(admin).live(sig));
40
+ stampFlowName(events, "orders.events");
41
+ stampFlowName(feed, "admin.adminFeed");
42
+ expect(() => oke({ name: "t", autoBoot: false })).not.toThrow();
43
+ });
44
+
45
+ test("same match, different gates boot (via disambiguates on the client)", () => {
46
+ const sig = orderStatus();
47
+ const a = on(http.get("/orders/:orderId/events").gate(member).live(sig));
48
+ const b = on(http.get("/partners/:orderId/events").gate(partner).live(sig));
49
+ stampFlowName(a, "orders.events");
50
+ stampFlowName(b, "partners.events");
51
+ expect(() => oke({ name: "t", autoBoot: false })).not.toThrow();
52
+ });
53
+
54
+ test("two member firehoses on different paths fail OKE1013", () => {
55
+ const sig = orderStatus();
56
+ const a = on(http.get("/feed-a").gate(member).live(sig));
57
+ const b = on(http.get("/feed-b").gate(member).live(sig));
58
+ stampFlowName(a, "orders.feedA");
59
+ stampFlowName(b, "orders.feedB");
60
+ expect(() => oke({ name: "t", autoBoot: false })).toThrow(/OKE1013/);
61
+ });
62
+
63
+ test("duplicate GET path still OKE1011", () => {
64
+ const sig = orderStatus();
65
+ const a = on(http.get("/orders/:orderId/events").gate(member).live(sig));
66
+ const b = on(http.get("/orders/:orderId/events").gate(admin).live(sig));
67
+ stampFlowName(a, "orders.events");
68
+ stampFlowName(b, "admin.events");
69
+ expect(() => oke({ name: "t", autoBoot: false })).toThrow(/OKE1011/);
70
+ });
71
+
72
+ test("POST .live(signal) is rejected", () => {
73
+ const sig = orderStatus();
74
+ expect(() => on(http.post("/orders").gate(member).live(sig))).toThrow(
75
+ /live exposure must be GET/,
76
+ );
77
+ });
78
+ });