okengine 0.14.1 → 0.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/package.json +3 -4
  2. package/site/content/docs/elements/ai.mdx +2 -1
  3. package/site/content/docs/elements/channel.mdx +1 -0
  4. package/site/content/docs/elements/clock.mdx +3 -0
  5. package/site/content/docs/elements/flow.mdx +3 -0
  6. package/site/content/docs/elements/gate.mdx +25 -22
  7. package/site/content/docs/elements/signal.mdx +4 -4
  8. package/site/content/docs/elements/store.mdx +86 -27
  9. package/site/content/docs/elements/vault.mdx +1 -0
  10. package/site/content/docs/get-started/basic-usage.mdx +22 -13
  11. package/site/content/docs/get-started/index.mdx +5 -0
  12. package/site/content/docs/get-started/introduction.mdx +11 -3
  13. package/site/content/docs/get-started/meta.json +1 -1
  14. package/site/content/docs/get-started/project-structure.mdx +932 -0
  15. package/site/content/docs/reference/cli.md +13 -10
  16. package/site/content/docs/reference/client.mdx +25 -21
  17. package/site/content/docs/reference/security.md +1 -1
  18. package/src/auth/bindings.ts +14 -5
  19. package/src/auth/cross-plane.ts +10 -3
  20. package/src/cli/ai-setup/ai-setup.test.ts +62 -0
  21. package/src/cli/ai-setup/apply.ts +32 -1
  22. package/src/cli/attach-host-console.test.ts +3 -2
  23. package/src/cli/attach-host-console.ts +3 -0
  24. package/src/cli/build.test.ts +4 -2
  25. package/src/cli/db-seed.ts +48 -15
  26. package/src/cli/db.test.ts +104 -0
  27. package/src/cli/db.ts +122 -10
  28. package/src/cli/dev-controls.test.ts +20 -0
  29. package/src/cli/dev-controls.ts +13 -0
  30. package/src/cli/dev.test.ts +15 -4
  31. package/src/cli/dev.ts +102 -29
  32. package/src/cli/ensure-drizzle-config.ts +2 -1
  33. package/src/cli/tui/DevLive.tsx +12 -2
  34. package/src/cli/tui/keys.test.ts +2 -1
  35. package/src/cli/tui/keys.ts +3 -2
  36. package/src/compiler/aot.test.ts +1 -1
  37. package/src/compiler/extract.test.ts +409 -33
  38. package/src/compiler/extract.ts +318 -67
  39. package/src/compiler/flow-path.test.ts +96 -0
  40. package/src/compiler/flow-path.ts +173 -0
  41. package/src/compiler/generate-adopt.test.ts +112 -3
  42. package/src/compiler/generate-adopt.ts +280 -26
  43. package/src/compiler/schema-from-ast.ts +609 -0
  44. package/src/console/server/bind.ts +2 -2
  45. package/src/console/server/console.test.ts +3 -0
  46. package/src/console/server/flows.ts +120 -40
  47. package/src/console/server/invoke-user-flow.test.ts +3 -1
  48. package/src/console/server/invoke-user-flow.ts +17 -2
  49. package/src/console/server/iso-at.test.ts +9 -0
  50. package/src/console/server/iso-at.ts +11 -0
  51. package/src/console/server/security-headers.ts +6 -3
  52. package/src/console/server/spa-proxy.test.ts +1 -0
  53. package/src/console/server/sql-catalog.ts +147 -7
  54. package/src/console/server/sql-rls.test.ts +16 -0
  55. package/src/console/server/state.ts +8 -0
  56. package/src/console/server/store.test.ts +7 -4
  57. package/src/console/server/store.ts +142 -58
  58. package/src/console/ui-next/dist/assets/agent-disclosure-C0X1fbWF.js +1 -0
  59. package/src/console/ui-next/dist/assets/{cache-glyph-BanhLsEY.js → cache-glyph-F1FI122b.js} +1 -1
  60. package/src/console/ui-next/dist/assets/{call-pii-button-bqkxMrJH.js → call-pii-button-DEDCl_j3.js} +1 -1
  61. package/src/console/ui-next/dist/assets/collapsible-LPqGvfoz.js +1 -0
  62. package/src/console/ui-next/dist/assets/{confirm-sheet-DDCRmG62.js → confirm-sheet-3ptDzXbz.js} +1 -1
  63. package/src/console/ui-next/dist/assets/{duration-tone-oiRxPVsZ.js → duration-tone-sC3lGABz.js} +1 -1
  64. package/src/console/ui-next/dist/assets/{explorer-empty-HjTnVQoR.js → explorer-empty-BvYrygyO.js} +1 -1
  65. package/src/console/ui-next/dist/assets/flows-page-Dluo1Bd7.js +1 -0
  66. package/src/console/ui-next/dist/assets/{highlighted-json-CvDPvveV.js → highlighted-json-CS_O8L-r.js} +1 -1
  67. package/src/console/ui-next/dist/assets/http-method--sWDdXSB.js +1 -0
  68. package/src/console/ui-next/dist/assets/index-Ca3HZMVq.js +66 -0
  69. package/src/console/ui-next/dist/assets/index-UtTDRQpS.css +2 -0
  70. package/src/console/ui-next/dist/assets/{link-DF7SZ9Ek.js → link-COeyggt-.js} +1 -1
  71. package/src/console/ui-next/dist/assets/{observability-page-BEZDzyYh.js → observability-page-HK9-BO8z.js} +2 -2
  72. package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +1 -0
  73. package/src/console/ui-next/dist/assets/replica-lag-DBIFFf7d.js +18 -0
  74. package/src/console/ui-next/dist/assets/sheet-form-Dnwa5oOR.js +1 -0
  75. package/src/console/ui-next/dist/assets/shortcut-keys-CoqwEIi0.js +1 -0
  76. package/src/console/ui-next/dist/assets/skeleton-BHcfDYcb.js +1 -0
  77. package/src/console/ui-next/dist/assets/store-page-BSohH3wM.js +41 -0
  78. package/src/console/ui-next/dist/assets/trace-detail-sheet-CFIGRnnA.js +2 -0
  79. package/src/console/ui-next/dist/assets/tree-expand-toggle-BgLl34w7.js +54 -0
  80. package/src/console/ui-next/dist/assets/units-page-B0cFE76A.js +1 -0
  81. package/src/console/ui-next/dist/assets/{use-vault-list-Cl79j_ku.js → use-vault-list-BLbZhzyF.js} +1 -1
  82. package/src/console/ui-next/dist/assets/vault-page-B1SbYe10.js +2 -0
  83. package/src/console/ui-next/dist/index.html +6 -6
  84. package/src/console/ui-next/src/client.ts +35 -7
  85. package/src/console/ui-next/src/components/motion/table/index.tsx +16 -0
  86. package/src/console/ui-next/src/components/motion/table/types.ts +4 -0
  87. package/src/console/ui-next/src/components/ui/kbd.tsx +2 -2
  88. package/src/console/ui-next/src/features/flows/traces/http-method.ts +19 -0
  89. package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +19 -1
  90. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +10 -3
  91. package/src/console/ui-next/src/features/store/detail/browse-section.tsx +45 -1
  92. package/src/console/ui-next/src/features/store/detail/resource-panel.tsx +78 -16
  93. package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +155 -14
  94. package/src/console/ui-next/src/features/store/explorer/store-tree.tsx +33 -133
  95. package/src/console/ui-next/src/features/store/files/file-preview.tsx +4 -1
  96. package/src/console/ui-next/src/features/store/grid/rls-policy-sheet.tsx +326 -165
  97. package/src/console/ui-next/src/features/store/grid/sql-insert-sheet.tsx +6 -0
  98. package/src/console/ui-next/src/features/store/grid/store-data-grid.tsx +50 -3
  99. package/src/console/ui-next/src/features/store/lib/files-meta.test.ts +3 -0
  100. package/src/console/ui-next/src/features/store/lib/files-meta.ts +12 -0
  101. package/src/console/ui-next/src/features/store/lib/query-defaults.test.ts +25 -0
  102. package/src/console/ui-next/src/features/store/lib/query-defaults.ts +85 -3
  103. package/src/console/ui-next/src/features/store/lib/query-gate.ts +30 -4
  104. package/src/console/ui-next/src/features/store/lib/rls-policy.test.ts +139 -0
  105. package/src/console/ui-next/src/features/store/lib/rls-policy.ts +223 -21
  106. package/src/console/ui-next/src/features/store/lib/schema-graph.ts +4 -0
  107. package/src/console/ui-next/src/features/store/lib/sql-catalog.test.ts +24 -0
  108. package/src/console/ui-next/src/features/store/lib/sql-catalog.ts +19 -0
  109. package/src/console/ui-next/src/features/store/lib/store-tree.test.ts +6 -0
  110. package/src/console/ui-next/src/features/store/lib/store-tree.ts +13 -0
  111. package/src/console/ui-next/src/features/store/query/query-console.tsx +48 -14
  112. package/src/console/ui-next/src/features/store/query/query-gate-menu.tsx +19 -20
  113. package/src/console/ui-next/src/features/store/query/query-gate-parts.tsx +2 -1
  114. package/src/console/ui-next/src/features/store/query/query-results.tsx +110 -83
  115. package/src/console/ui-next/src/features/store/schema/schema-table-node.tsx +17 -0
  116. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +112 -72
  117. package/src/console/ui-next/src/features/units/lib/contract-input.test.ts +206 -1
  118. package/src/console/ui-next/src/features/units/lib/contract-input.ts +55 -0
  119. package/src/console/ui-next/src/features/vault/lib/types.ts +1 -1
  120. package/src/docker/compose-health.test.ts +27 -0
  121. package/src/docker/compose-health.ts +19 -3
  122. package/src/docker/docker.test.ts +3 -0
  123. package/src/docker/recipes/pgdog.ts +2 -0
  124. package/src/drivers/clock-postgres.ts +13 -9
  125. package/src/drivers/instances-postgres.ts +4 -6
  126. package/src/drivers/journal-postgres.ts +13 -9
  127. package/src/drivers/pg-rls.test.ts +87 -0
  128. package/src/drivers/pg-rls.ts +233 -7
  129. package/src/drivers/pglite.ts +26 -3
  130. package/src/drivers/postgres.test.ts +23 -0
  131. package/src/drivers/postgres.ts +133 -9
  132. package/src/drivers/types.ts +10 -0
  133. package/src/elements/clock/declare.ts +26 -1
  134. package/src/elements/gate/boot.ts +5 -5
  135. package/src/elements/gate/declare.ts +36 -4
  136. package/src/elements/gate/flatten.ts +1 -1
  137. package/src/elements/gate/runtime.ts +1 -1
  138. package/src/elements/store/declare.ts +9 -6
  139. package/src/elements/store/emit-drizzle.ts +44 -8
  140. package/src/elements/store/resource.test.ts +1 -1
  141. package/src/elements/store/rls-identity.test.ts +95 -0
  142. package/src/elements/store/rls-identity.ts +116 -0
  143. package/src/elements/store/runtime.ts +4 -0
  144. package/src/elements/store/schema-decl.test.ts +37 -0
  145. package/src/elements/store/schema-decl.ts +161 -0
  146. package/src/elements/store/seed.ts +1 -1
  147. package/src/elements/store/sql-condition.test.ts +12 -0
  148. package/src/elements/store/sql-condition.ts +26 -2
  149. package/src/elements/store/sql-rls-isolation.test.ts +165 -0
  150. package/src/elements/store/sql-rls-stamp.test.ts +117 -0
  151. package/src/elements/store/sql-session.ts +81 -3
  152. package/src/elements/store/upsert-app.test.ts +1 -1
  153. package/src/elements/store.ts +14 -0
  154. package/src/http.ts +20 -3
  155. package/src/index.ts +10 -1
  156. package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
  157. package/src/kernel/adopt-routes.ts +30 -2
  158. package/src/kernel/app.ts +131 -14
  159. package/src/kernel/auto-cache.test.ts +8 -8
  160. package/src/kernel/auto-registry.test.ts +49 -9
  161. package/src/kernel/boot.test.ts +4 -4
  162. package/src/kernel/boot.ts +2 -2
  163. package/src/kernel/effects-stamping.test.ts +3 -3
  164. package/src/kernel/element-registries.ts +12 -5
  165. package/src/kernel/errors.ts +24 -0
  166. package/src/kernel/flow-units.ts +64 -0
  167. package/src/kernel/flow.ts +29 -17
  168. package/src/kernel/fx.ts +23 -0
  169. package/src/kernel/horizontal-child.ts +4 -4
  170. package/src/kernel/http-path-pending.ts +20 -0
  171. package/src/kernel/http-resource.ts +4 -0
  172. package/src/kernel/http-stream.test.ts +4 -4
  173. package/src/kernel/pipeline.test.ts +2 -2
  174. package/src/kernel/plugin-elements.test.ts +1 -1
  175. package/src/kernel/plugin-needs.test.ts +1 -1
  176. package/src/kernel/ready.test.ts +2 -2
  177. package/src/kernel/resource-mount.test.ts +14 -0
  178. package/src/kernel/run-duration.test.ts +2 -2
  179. package/src/kernel/stamp-http.test.ts +79 -0
  180. package/src/kernel/stamp-http.ts +75 -0
  181. package/src/kernel/triggers.ts +52 -27
  182. package/src/manifest/types.ts +13 -0
  183. package/src/release/measure.ts +2 -2
  184. package/src/runtime/json-code-block.test.ts +2 -2
  185. package/src/runtime/serve.test.ts +4 -4
  186. package/src/term.test.ts +52 -4
  187. package/src/term.ts +125 -17
  188. package/src/test/reset-element-registries.ts +8 -3
  189. package/src/cli/ask-seed.test.ts +0 -96
  190. package/src/cli/ask-seed.ts +0 -82
  191. package/src/console/ui-next/dist/assets/collapsible-DYb0xU8C.js +0 -1
  192. package/src/console/ui-next/dist/assets/flows-page-DxDsOd4f.js +0 -1
  193. package/src/console/ui-next/dist/assets/http-method-CJCBYL2j.js +0 -1
  194. package/src/console/ui-next/dist/assets/index-Ce6WKWKM.js +0 -66
  195. package/src/console/ui-next/dist/assets/index-D4Ldtj79.css +0 -2
  196. package/src/console/ui-next/dist/assets/query-gate-parts-1m8m1iNp.js +0 -2
  197. package/src/console/ui-next/dist/assets/react-B1ML8gxg.js +0 -1
  198. package/src/console/ui-next/dist/assets/replica-lag-C8_BRt2x.js +0 -18
  199. package/src/console/ui-next/dist/assets/sheet-form-D-ata7jy.js +0 -1
  200. package/src/console/ui-next/dist/assets/shortcut-keys-DO4IsVqv.js +0 -1
  201. package/src/console/ui-next/dist/assets/skeleton-CL_X0GCj.js +0 -1
  202. package/src/console/ui-next/dist/assets/store-page-v3LXdYpr.js +0 -45
  203. package/src/console/ui-next/dist/assets/trace-detail-sheet-DFLFfUUX.js +0 -2
  204. package/src/console/ui-next/dist/assets/units-page-C0gW6Kdo.js +0 -1
  205. package/src/console/ui-next/dist/assets/vault-page-DuKzqwzW.js +0 -2
@@ -47,7 +47,7 @@ const CreateOut = z.object({ ok: z.boolean() });
47
47
 
48
48
  function buildUnannotatedCreateFlow(db: ReturnType<typeof store.sql>) {
49
49
  return on(
50
- http.post("/notes").gate.public,
50
+ http.post("/notes").public(),
51
51
  flow("notes.create", {
52
52
  in: CreateIn,
53
53
  out: CreateOut,
@@ -140,7 +140,7 @@ describe("boot-level: table-ref resolution stays backward compatible", () => {
140
140
  resetFlowSeq();
141
141
  const db = store.sql("app", { schema: { notes } });
142
142
  const create = on(
143
- http.post("/notes").gate.public,
143
+ http.post("/notes").public(),
144
144
  flow("notes.create", {
145
145
  in: CreateIn,
146
146
  out: CreateOut,
@@ -193,7 +193,7 @@ import { on, flow, http, gate } from "okengine";
193
193
  const db = {} as any;
194
194
  const notes = {} as any;
195
195
  export const create = on(
196
- http.post("/notes").gate.public,
196
+ http.post("/notes").public(),
197
197
  flow("notes.create", {
198
198
  do: async (input, fx) => {
199
199
  await fx.store(db).insert(notes).values({ id: input.id, title: input.title, createdAt: 1 });
@@ -1,8 +1,9 @@
1
1
  /**
2
- * Module-evaluation registries for `store.sql` / `store.files`, `vault.secret`,
3
- * `vault.env.required`, `signal()`, `channel.<medium>().template()`, and
4
- * `ai.model` / `.prompt` / `ai.embed` / `ai.agent` / `ai.mcpServer` — the plain arrays behind
5
- * each declare module's `listX()` / `resetX()` pair (mirrors `on.ts`'s
2
+ * Module-evaluation registries for `store.*`, `vault.secret`,
3
+ * `vault.env.required`, `signal()`, `clock()`, `gate.policy` / `.scope` /
4
+ * `.rate`, `channel.<medium>().template()`, and `ai.model` / `.prompt` /
5
+ * `ai.embed` / `ai.agent` / `ai.mcpServer` the plain arrays behind each
6
+ * declare module's `listX()` / `resetX()` pair (mirrors `on.ts`'s
6
7
  * trigger-drain `bindings` array).
7
8
  *
8
9
  * Lives here, not in the declare modules themselves, so {@link oke} can read
@@ -24,8 +25,10 @@ import type { StoreDecl } from "../elements/store/declare.ts";
24
25
  import type { VaultSecretDecl } from "../elements/vault/declare.ts";
25
26
  import type { SignalDecl } from "../elements/signal/declare.ts";
26
27
  import type { ChannelTemplateDecl } from "../elements/channel/declare.ts";
28
+ import type { ClockDecl } from "../elements/clock/declare.ts";
29
+ import type { GateDecl } from "../elements/gate/declare.ts";
27
30
 
28
- /** `store.sql` / `store.files` declarations since the last reset. */
31
+ /** `store.sql` / `store.kv` / `store.files` / `store.index` declarations since the last reset. */
29
32
  export const storeRegistry: StoreDecl[] = [];
30
33
  /** `vault.secret` declarations since the last reset. */
31
34
  export const secretRegistry: VaultSecretDecl[] = [];
@@ -33,6 +36,10 @@ export const secretRegistry: VaultSecretDecl[] = [];
33
36
  export const requiredEnvRegistry: string[] = [];
34
37
  /** `signal()` declarations since the last reset. */
35
38
  export const signalRegistry: SignalDecl[] = [];
39
+ /** `clock()` declarations since the last reset. */
40
+ export const clockRegistry: ClockDecl[] = [];
41
+ /** `gate.policy` / `gate.scope` / `gate.rate` declarations since the last reset. */
42
+ export const gateRegistry: GateDecl[] = [];
36
43
  /** Medium-binder `.template()` declarations since the last reset. */
37
44
  export const channelTemplateRegistry: ChannelTemplateDecl[] = [];
38
45
  /** `ai.model` declarations since the last reset. */
@@ -183,6 +183,30 @@ export const OKE_ERRORS = {
183
183
  cause: "src/flows/{unit} exists on disk but adopted no flows — the .adopt() barrel is stale.",
184
184
  fix: "Run `oke dev` or `oke build` to regenerate `src/flows/generated.ts`.",
185
185
  },
186
+ /**
187
+ * `http.get()` was never stamped from the file tree — refuse a silent `/`.
188
+ */
189
+ HTTP_PATH_UNRESOLVED: {
190
+ code: 1010,
191
+ cause: 'Flow "{flow}" bound {method} with no path — the file-tree stamp never ran.',
192
+ fix: 'Put the file under `src/flows/<unit>/` and import `@/flows/generated`, or pass an explicit path to `http.{method}("/…")`.',
193
+ },
194
+ /**
195
+ * Two HTTP bindings share method + path — last-add-wins is the opposite of this DX.
196
+ */
197
+ HTTP_ROUTE_DUPLICATE: {
198
+ code: 1011,
199
+ cause: '{method} {path} is bound twice (flow "{flow}").',
200
+ fix: "Give each HTTP flow a unique method + path.",
201
+ },
202
+ /**
203
+ * Adopted HTTP flow still has no name (`flow({ do })` outside a unit).
204
+ */
205
+ HTTP_FLOW_UNNAMED: {
206
+ code: 1012,
207
+ cause: "An HTTP flow on {method} {path} has no name.",
208
+ fix: 'Use `flow("unit.export", {…})` or export it from a `src/flows/<unit>/` file so the tree can stamp `unit.export`.',
209
+ },
186
210
  /**
187
211
  * Emit target has no subscriber (unified-theory §21).
188
212
  * Thrown at emit when `optional` is false and nobody is subscribed.
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Module-evaluation registry for `src/flows/generated.ts`.
3
+ *
4
+ * Same consume/ignore posture as `on()` / `listBindings()`: generated.ts
5
+ * calls {@link registerFlowUnits}; {@link oke} drains the bag into `$routes`
6
+ * unless `registry: "ignore"`.
7
+ */
8
+
9
+ import type { AnyFlowDef } from "./flow.ts";
10
+ import { isFlow } from "./flow.ts";
11
+
12
+ /** Unit name → export name → Flow. */
13
+ export type FlowUnitBag = {
14
+ readonly [exportName: string]: unknown;
15
+ };
16
+
17
+ const pending: Record<string, FlowUnitBag> = {};
18
+
19
+ /**
20
+ * Record generated flow units for the next {@link oke} construction.
21
+ *
22
+ * @param units - `{ notes, main, … }` from `generated.ts`
23
+ */
24
+ export function registerFlowUnits(units: Record<string, FlowUnitBag>): void {
25
+ Object.assign(pending, units);
26
+ }
27
+
28
+ /**
29
+ * Drain registered units (consume). Clears the registry.
30
+ */
31
+ export function consumeRegisteredFlowUnits(): Record<string, FlowUnitBag> {
32
+ const snap: Record<string, FlowUnitBag> = { ...pending };
33
+ resetRegisteredFlowUnits();
34
+ return snap;
35
+ }
36
+
37
+ /**
38
+ * Snapshot without clearing (keep / tests).
39
+ */
40
+ export function peekRegisteredFlowUnits(): Record<string, FlowUnitBag> {
41
+ return { ...pending };
42
+ }
43
+
44
+ /**
45
+ * Clear the unit registry (tests / `registry: "consume"` after drain).
46
+ */
47
+ export function resetRegisteredFlowUnits(): void {
48
+ for (const key of Object.keys(pending)) {
49
+ delete pending[key];
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Collect FlowDefs from a unit bag (skip non-flow exports).
55
+ *
56
+ * @param bag - Namespace object
57
+ */
58
+ export function flowsInUnitBag(bag: FlowUnitBag): AnyFlowDef[] {
59
+ const found: AnyFlowDef[] = [];
60
+ for (const value of Object.values(bag)) {
61
+ if (isFlow(value)) found.push(value);
62
+ }
63
+ return found;
64
+ }
@@ -239,13 +239,34 @@ export const flowBrand: unique symbol = Symbol("oke.flow");
239
239
  * Input / output types are inferred from Standard Schema `in` / `out` when
240
240
  * present; otherwise from the `do` handler signature.
241
241
  *
242
+ * A nameless `flow({ do })` is stamped `unit.export` by the file-tree
243
+ * generator, {@link unit}, or `.adopt()`. Explicit `flow("notes.get", {…})`
244
+ * still wins.
245
+ *
246
+ * @param options - Contract and handler (name stamped later)
247
+ */
248
+ export function flow<Opts extends FlowOptions<any, any, any>>(
249
+ options: Opts,
250
+ ): FlowDef<InferFlowIn<Opts>, InferFlowOut<Opts>, InferFlowErrors<Opts>>;
251
+ /**
242
252
  * @param name - Stable name (used by `fx.call` and the Manifest)
243
253
  * @param options - Contract and handler
244
254
  */
245
255
  export function flow<Opts extends FlowOptions<any, any, any>>(
246
256
  name: string,
247
257
  options: Opts & { readonly name?: never },
248
- ): FlowDef<InferFlowIn<Opts>, InferFlowOut<Opts>, InferFlowErrors<Opts>> {
258
+ ): FlowDef<InferFlowIn<Opts>, InferFlowOut<Opts>, InferFlowErrors<Opts>>;
259
+ export function flow(
260
+ nameOrOptions: string | FlowOptions<any, any, any>,
261
+ maybeOptions?: FlowOptions<any, any, any> & { readonly name?: never },
262
+ ): FlowDef {
263
+ const named = typeof nameOrOptions === "string";
264
+ const name = named ? nameOrOptions : "";
265
+ const options = (named ? maybeOptions : nameOrOptions) as FlowOptions<any, any, any> | undefined;
266
+ if (!options || typeof options.do !== "function") {
267
+ throw new TypeError("flow() expected an options bag with a do handler");
268
+ }
269
+
249
270
  const triggers: Trigger[] = [];
250
271
  const hooks: Partial<Record<HookStage, HookFn[]>> = {};
251
272
  const pendingPlugins: PluginDef[] = [];
@@ -253,13 +274,13 @@ export function flow<Opts extends FlowOptions<any, any, any>>(
253
274
  const dot = name.indexOf(".");
254
275
  const unit = dot > 0 ? name.slice(0, dot) : undefined;
255
276
 
256
- const def: FlowDef<InferFlowIn<Opts>, InferFlowOut<Opts>, InferFlowErrors<Opts>> = {
277
+ const def: FlowDef = {
257
278
  [flowBrand]: true,
258
279
  name,
259
280
  unit,
260
281
  in: options.in,
261
282
  out: options.out,
262
- errors: (options.errors ?? undefined) as InferFlowErrors<Opts> | undefined,
283
+ errors: (options.errors ?? undefined) as FlowErrorMap | undefined,
263
284
  effects: options.effects,
264
285
  durable: options.durable ?? false,
265
286
  retry: options.retry,
@@ -268,17 +289,8 @@ export function flow<Opts extends FlowOptions<any, any, any>>(
268
289
  slo: options.slo,
269
290
  plane: options.plane,
270
291
  breaking: options.breaking ?? false,
271
- compensate: options.compensate as
272
- | ((
273
- ctx: {
274
- readonly input: InferFlowIn<Opts>;
275
- readonly error: unknown;
276
- readonly completedSteps: readonly string[];
277
- },
278
- fx: import("./fx.ts").Fx,
279
- ) => unknown | Promise<unknown>)
280
- | undefined,
281
- do: options.do as FlowHandler<InferFlowIn<Opts>, InferFlowOut<Opts>>,
292
+ compensate: options.compensate as FlowDef["compensate"],
293
+ do: options.do as FlowHandler,
282
294
  triggers,
283
295
  $trigger: undefined,
284
296
  hooks,
@@ -320,9 +332,9 @@ export function isFlow(value: unknown): value is AnyFlowDef {
320
332
  }
321
333
 
322
334
  /**
323
- * No-op. `name` is a required positional argument now, so there is no
324
- * auto-name sequence left to reset — kept so existing test `beforeEach`
325
- * blocks don't need touching.
335
+ * No-op. Nameless flows start as `""` and are stamped by unit / adopt /
336
+ * the file-tree generator — kept so existing test `beforeEach` blocks
337
+ * don't need touching.
326
338
  *
327
339
  * @internal
328
340
  */
package/src/kernel/fx.ts CHANGED
@@ -27,6 +27,8 @@ import type {
27
27
  StoreRuntime,
28
28
  SqlStoreHandle,
29
29
  } from "../elements/store.ts";
30
+ import { rlsIdentityFromAuth } from "../elements/store.ts";
31
+ import type { RlsIdentity } from "../drivers/pg-rls.ts";
30
32
  import type { SqlRow } from "../drivers/types.ts";
31
33
  import type { SignalDecl, SignalRuntime } from "../elements/signal.ts";
32
34
  import type { DeadLetter, SignalEmitOptions } from "../drivers/signal-types.ts";
@@ -911,6 +913,12 @@ export interface CreateFxOptions {
911
913
  readonly runId?: string;
912
914
  /** Reveal PII through the store runtime (requires `pii:reveal` upstream). */
913
915
  readonly revealPii?: boolean;
916
+ /** Trigger gate names for RLS (`oke.gate` = first policy/public). */
917
+ readonly rlsGateNames?: readonly string[];
918
+ /** Skip RLS stamp (Operator / `bypassGates` / operator-plane flows). */
919
+ readonly rlsBypass?: boolean;
920
+ /** Forced bag (Console / Call API). Wins over {@link rlsGateNames}. */
921
+ readonly rls?: import("../drivers/pg-rls.ts").RlsIdentity;
914
922
  /**
915
923
  * Active journal session when the flow is durable. Every `fx` call is
916
924
  * recorded; `step` / `sleep` replay from the journal on resume.
@@ -1025,6 +1033,19 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1025
1033
  } satisfies FxPrincipal);
1026
1034
  const journal = options.journal;
1027
1035
 
1036
+ function rlsInvokeContext(): { rls?: RlsIdentity } {
1037
+ if (options.rlsBypass === true) return {};
1038
+ if (options.rls) return { rls: options.rls };
1039
+ const identity = rlsIdentityFromAuth({
1040
+ userId: auth.userId,
1041
+ scopes: auth.scopes,
1042
+ gateNames: options.rlsGateNames ?? [],
1043
+ bypass: false,
1044
+ operator: false,
1045
+ });
1046
+ return identity ? { rls: identity } : {};
1047
+ }
1048
+
1028
1049
  async function gated<T>(
1029
1050
  kind: Parameters<CapabilityToken["assert"]>[0],
1030
1051
  resource: string,
@@ -1342,6 +1363,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1342
1363
  cache.handle = await runtime.open(decl, {
1343
1364
  effects: options.effects ?? {},
1344
1365
  revealPii: options.revealPii,
1366
+ ...rlsInvokeContext(),
1345
1367
  });
1346
1368
  }
1347
1369
  return cache.handle;
@@ -1361,6 +1383,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1361
1383
  {
1362
1384
  effects: options.effects ?? {},
1363
1385
  revealPii: options.revealPii,
1386
+ ...rlsInvokeContext(),
1364
1387
  },
1365
1388
  {
1366
1389
  gate: gated,
@@ -190,11 +190,11 @@ const charge = flow("horizontal.charge", {
190
190
  });
191
191
 
192
192
  const bindings: Binding[] = [
193
- { trigger: http.get("/ping").gate.public, flow: ping as AnyFlowDef },
193
+ { trigger: http.get("/ping").public(), flow: ping as AnyFlowDef },
194
194
  { trigger: http.get("/rate").gate(rateGate), flow: rate as AnyFlowDef },
195
- { trigger: http.post("/write").gate.public, flow: write as AnyFlowDef },
196
- { trigger: http.post("/emit").gate.public, flow: emit as AnyFlowDef },
197
- { trigger: http.post("/charge").gate.public, flow: charge as AnyFlowDef },
195
+ { trigger: http.post("/write").public(), flow: write as AnyFlowDef },
196
+ { trigger: http.post("/emit").public(), flow: emit as AnyFlowDef },
197
+ { trigger: http.post("/charge").public(), flow: charge as AnyFlowDef },
198
198
  ];
199
199
 
200
200
  const app = oke({
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Branded sentinel for `http.get()` with no path — never `"/"`.
3
+ * Lives in a leaf module so `oke()` can refuse leftovers without pulling
4
+ * the HTTP trigger constructors onto a Store-only graph.
5
+ */
6
+
7
+ /** Unresolved path token. {@link stampHttpPath} replaces it from the file tree. */
8
+ export const HTTP_PATH_PENDING = "__oke_http_path_pending__" as const;
9
+
10
+ /** Type of {@link HTTP_PATH_PENDING}. */
11
+ export type HttpPathPending = typeof HTTP_PATH_PENDING;
12
+
13
+ /**
14
+ * True when an HTTP trigger still carries the unresolved path sentinel.
15
+ *
16
+ * @param path - Trigger path
17
+ */
18
+ export function isPendingHttpPath(path: string): boolean {
19
+ return path === HTTP_PATH_PENDING;
20
+ }
@@ -3,6 +3,7 @@
3
3
  * apps do not pay for the five-verb CRUD helper.
4
4
  */
5
5
 
6
+ import { GATE_PUBLIC_NAME } from "../elements/gate/flatten.ts";
6
7
  import {
7
8
  createGateAttach,
8
9
  createHttpTrigger,
@@ -47,6 +48,9 @@ export function httpResource<P extends string>(
47
48
  { trigger: verb("DELETE", id, false), flow: ops.remove },
48
49
  ],
49
50
  gate: createGateAttach((next) => httpResource(path, ops, next, isLive), gates),
51
+ public() {
52
+ return httpResource(path, ops, [...gates, GATE_PUBLIC_NAME], isLive);
53
+ },
50
54
  live() {
51
55
  return httpResource(path, ops, gates, true);
52
56
  },
@@ -83,12 +83,12 @@ describe("fx.json.stream HTTP SSE", () => {
83
83
  test("encodes token chunks as SSE and leaves JSON flows buffered", async () => {
84
84
  const smart = ai.model("smart");
85
85
  on(
86
- http.get("/chat").gate.public,
86
+ http.get("/chat").public(),
87
87
  flow("chat.stream", {
88
88
  do: (_input, fx) => fx.json.stream(fx.stream(smart, { prompt: "hi" })),
89
89
  }),
90
90
  );
91
- on(http.get("/ping").gate.public, flow("ping", { do: () => ({ ok: true as const }) }));
91
+ on(http.get("/ping").public(), flow("ping", { do: () => ({ ok: true as const }) }));
92
92
 
93
93
  const app = oke({
94
94
  name: "http-stream",
@@ -111,7 +111,7 @@ describe("fx.json.stream HTTP SSE", () => {
111
111
  let providerAborted = false;
112
112
  const smart = ai.model("smart");
113
113
  on(
114
- http.get("/slow").gate.public,
114
+ http.get("/slow").public(),
115
115
  flow("chat.slow", {
116
116
  do: (_input, fx) => fx.json.stream(fx.stream(smart, { prompt: "long" })),
117
117
  }),
@@ -139,7 +139,7 @@ describe("fx.json.stream HTTP SSE", () => {
139
139
  await runs.open();
140
140
  const smart = ai.model("smart");
141
141
  on(
142
- http.get("/chat").gate.public,
142
+ http.get("/chat").public(),
143
143
  flow("chat.stream", {
144
144
  do: (_input, fx) => fx.json.stream(fx.stream(smart, { prompt: "hi" })),
145
145
  }),
@@ -65,9 +65,9 @@ describe("gateDenialFailure", () => {
65
65
  });
66
66
  });
67
67
 
68
- describe("http trigger — .gate.public", () => {
68
+ describe("http trigger — .public()", () => {
69
69
  test("attaches the public sentinel without calling gate()", () => {
70
- const trigger = http.get("/health").gate.public;
70
+ const trigger = http.get("/health").public();
71
71
  expect(trigger.gates.map((g) => (typeof g === "string" ? g : g.name))).toEqual(["public"]);
72
72
  expect(trigger.live().isLive).toBe(true);
73
73
  });
@@ -32,7 +32,7 @@ describe("plugin element contributions", () => {
32
32
  .table("otp_codes")
33
33
  .needs("store.sql");
34
34
 
35
- on(http.get("/ok").gate.public, flow("ok", { do: () => ({ ok: true }) }));
35
+ on(http.get("/ok").public(), flow("ok", { do: () => ({ ok: true }) }));
36
36
 
37
37
  const app = oke({
38
38
  name: "plugin-elements",
@@ -75,7 +75,7 @@ describe("plugin .needs() resolution", () => {
75
75
  test("boot fails when plugin needs missing peer plugin", async () => {
76
76
  resetBindings();
77
77
  resetFlowSeq();
78
- on(http.get("/x").gate.public, flow("x", { do: () => ({ ok: true }) }));
78
+ on(http.get("/x").public(), flow("x", { do: () => ({ ok: true }) }));
79
79
  const dependent = plugin("needs-auth", { version: "0.0.1" }).needs("auth");
80
80
  const app = oke({ name: "needs-boot", gate: { policies: [gate.public] } }).plug(dependent);
81
81
  await expect(app.boot({ env: "test" })).rejects.toThrow(PluginNeedsError);
@@ -38,7 +38,7 @@ describe("GET /_/ready", () => {
38
38
  });
39
39
  const bindings: Binding[] = [
40
40
  {
41
- trigger: http.post("/charge").gate.public,
41
+ trigger: http.post("/charge").public(),
42
42
  flow: charge as AnyFlowDef,
43
43
  },
44
44
  ];
@@ -82,7 +82,7 @@ describe("GET /_oke/client.json", () => {
82
82
  startScheduler: false,
83
83
  env: "test",
84
84
  gate: { unguardedHttp: "allow" },
85
- bindings: [{ trigger: http.get("/ping").gate.public, flow: ping as AnyFlowDef }],
85
+ bindings: [{ trigger: http.get("/ping").public(), flow: ping as AnyFlowDef }],
86
86
  });
87
87
  await app.boot();
88
88
  const res = await app.fetch(new Request("http://127.0.0.1/_oke/client.json"));
@@ -63,6 +63,20 @@ describe("http.resource — gate / live", () => {
63
63
  expect(remove?.isLive).toBe(false);
64
64
  });
65
65
 
66
+ test(".public().live() stamps the sentinel on every verb", () => {
67
+ resetBindings();
68
+ resetFlowSeq();
69
+ on(http.resource("/notes", bag()).public().live());
70
+ expect(httpOf("/notes", "GET")?.gates.map((g) => (typeof g === "string" ? g : g.name))).toEqual(
71
+ ["public"],
72
+ );
73
+ expect(
74
+ httpOf("/notes", "POST")?.gates.map((g) => (typeof g === "string" ? g : g.name)),
75
+ ).toEqual(["public"]);
76
+ expect(httpOf("/notes", "GET")?.isLive).toBe(true);
77
+ expect(httpOf("/notes", "POST")?.isLive).toBe(false);
78
+ });
79
+
66
80
  test(".live().gate(member) order does not matter", () => {
67
81
  resetBindings();
68
82
  resetFlowSeq();
@@ -23,7 +23,7 @@ describe("execute run duration", () => {
23
23
  await runs.open();
24
24
 
25
25
  on(
26
- http.get("/ping").gate.public,
26
+ http.get("/ping").public(),
27
27
  flow("demo.ping", {
28
28
  do: () => ({ ok: true }),
29
29
  }),
@@ -68,7 +68,7 @@ describe("execute run duration", () => {
68
68
  await runs.open();
69
69
  try {
70
70
  on(
71
- http.get("/ping").gate.public,
71
+ http.get("/ping").public(),
72
72
  flow("demo.ping", {
73
73
  do: () => ({ ok: true }),
74
74
  }),
@@ -0,0 +1,79 @@
1
+ /**
2
+ * In-place path / name stamps and fail-loud HTTP boot.
3
+ */
4
+
5
+ import { describe, expect, test, beforeEach } from "bun:test";
6
+ import type { RuntimeRouteMap } from "./adopt-routes.ts";
7
+ import { oke } from "./app.ts";
8
+ import { flow, resetFlowSeq } from "./flow.ts";
9
+ import { resetRegisteredFlowUnits, registerFlowUnits } from "./flow-units.ts";
10
+ import { listBindings, on, resetBindings } from "./on.ts";
11
+ import { stampFlowName, stampHttpPath } from "./stamp-http.ts";
12
+ import { HTTP_PATH_PENDING } from "./http-path-pending.ts";
13
+ import { http } from "./triggers.ts";
14
+
15
+ beforeEach(() => {
16
+ resetBindings();
17
+ resetFlowSeq();
18
+ resetRegisteredFlowUnits();
19
+ });
20
+
21
+ describe("stampHttpPath", () => {
22
+ test("mutates the on() binding in place — listBindings sees the stamped path", () => {
23
+ const f = on(http.get().public(), flow({ do: () => ({ ok: true }) }));
24
+ expect(f.$trigger && f.$trigger.kind === "http" && f.$trigger.path).toBe(HTTP_PATH_PENDING);
25
+ stampHttpPath(f, "/notes/:id");
26
+ expect(f.$trigger && f.$trigger.kind === "http" && f.$trigger.path).toBe("/notes/:id");
27
+ const bound = listBindings()[0];
28
+ expect(bound?.trigger.kind === "http" && bound.trigger.path).toBe("/notes/:id");
29
+ });
30
+
31
+ test("explicit http.get('/x') wins — stamp does not overwrite", () => {
32
+ const f = on(http.get("/me/tasks").public(), flow("notes.get", { do: () => 1 }));
33
+ stampHttpPath(f, "/notes/:id");
34
+ expect(f.$trigger && f.$trigger.kind === "http" && f.$trigger.path).toBe("/me/tasks");
35
+ });
36
+ });
37
+
38
+ describe("stampFlowName", () => {
39
+ test("fills a nameless flow as unit.export", () => {
40
+ const f = flow({ do: () => 1 });
41
+ expect(f.name).toBe("");
42
+ stampFlowName(f, "notes.get");
43
+ expect(f.name).toBe("notes.get");
44
+ expect(f.unit).toBe("notes");
45
+ });
46
+
47
+ test("explicit flow('notes.get') wins", () => {
48
+ const f = flow("notes.get", { do: () => 1 });
49
+ stampFlowName(f, "notes.list");
50
+ expect(f.name).toBe("notes.get");
51
+ });
52
+ });
53
+
54
+ describe("oke — unresolved sentinel and duplicate routes", () => {
55
+ test("unresolved http.get() fails construction", () => {
56
+ on(http.get().public(), flow("notes.get", { do: () => 1 }));
57
+ expect(() => oke({ name: "t", autoBoot: false })).toThrow(/OKE1010/);
58
+ });
59
+
60
+ test("duplicate GET /notes fails construction", () => {
61
+ on(http.get("/notes").public(), flow("notes.list", { do: () => 1 }));
62
+ on(http.get("/notes").public(), flow("notes.also", { do: () => 1 }));
63
+ expect(() => oke({ name: "t", autoBoot: false })).toThrow(/OKE1011/);
64
+ });
65
+
66
+ test("nameless HTTP flow fails construction", () => {
67
+ on(http.get("/notes").public(), flow({ do: () => 1 }));
68
+ expect(() => oke({ name: "t", autoBoot: false })).toThrow(/OKE1012/);
69
+ });
70
+
71
+ test("registerFlowUnits drains into $routes; registry ignore does not", () => {
72
+ const get = on(http.get("/notes/:id").public(), flow("notes.get", { do: () => 1 }));
73
+ registerFlowUnits({ notes: { get } });
74
+ const ignored = oke({ name: "t", autoBoot: false, registry: "ignore" });
75
+ expect(ignored.$routes).toEqual({});
76
+ const app = oke({ name: "t", autoBoot: false });
77
+ expect((app.$routes as RuntimeRouteMap).notes?.get?.path).toBe("/notes/:id");
78
+ });
79
+ });
@@ -0,0 +1,75 @@
1
+ /**
2
+ * In-place HTTP path + Flow name stamps.
3
+ *
4
+ * `on()` already registered the trigger (possibly still on the pending-path
5
+ * sentinel). A wrapper that returned a new trigger would leave `listBindings()`
6
+ * on the sentinel — mutate the existing object.
7
+ */
8
+
9
+ import { isFlow, type AnyFlowDef, type FlowDef } from "./flow.ts";
10
+ import { isPendingHttpPath, type HttpPathPending } from "./http-path-pending.ts";
11
+ import type { HttpTrigger, Trigger } from "./triggers.ts";
12
+
13
+ /**
14
+ * Replace a pending HTTP path on {@link FlowDef} with the file-tree stamp.
15
+ *
16
+ * Explicit `http.get("/x")` wins — the pending sentinel is the only path
17
+ * this function overwrites. Returns the same object with a tighter trigger
18
+ * type when the original path was pending.
19
+ *
20
+ * @param flow - Bound flow (already passed through `on()`)
21
+ * @param path - Inferred URL (`/notes/:id`)
22
+ */
23
+ export function stampHttpPath<F extends AnyFlowDef, P extends string>(
24
+ flow: F,
25
+ path: P,
26
+ ): StampHttpPath<F, P> {
27
+ if (!isFlow(flow)) return flow as StampHttpPath<F, P>;
28
+ const apply = (trigger: Trigger): void => {
29
+ if (trigger.kind !== "http") return;
30
+ if (!isPendingHttpPath(trigger.path)) return;
31
+ (trigger as { path: string }).path = path;
32
+ };
33
+ if (flow.$trigger) apply(flow.$trigger);
34
+ for (const trigger of flow.triggers) apply(trigger);
35
+ return flow as StampHttpPath<F, P>;
36
+ }
37
+
38
+ /**
39
+ * Stamp `unit.export` onto a nameless (or `flow_*`) Flow.
40
+ *
41
+ * Explicit `flow("notes.get")` wins. Fills `unit` when missing.
42
+ *
43
+ * @param flow - Flow definition
44
+ * @param name - `notes.get`
45
+ */
46
+ export function stampFlowName<F extends AnyFlowDef>(flow: F, name: string): F {
47
+ if (!isFlow(flow)) return flow;
48
+ const f = flow as { name: string; unit: string | undefined };
49
+ if (!f.name || f.name.startsWith("flow_")) {
50
+ f.name = name;
51
+ }
52
+ if (!f.unit) {
53
+ const resolved = f.name || name;
54
+ const dot = resolved.indexOf(".");
55
+ f.unit = dot > 0 ? resolved.slice(0, dot) : resolved;
56
+ }
57
+ return flow;
58
+ }
59
+
60
+ /**
61
+ * Type-level path replacement — only when the bound trigger is still pending.
62
+ *
63
+ * @typeParam F - Flow definition
64
+ * @typeParam P - Stamped path
65
+ */
66
+ export type StampHttpPath<F, P extends string> =
67
+ F extends FlowDef<infer I, infer O, infer E, infer D, infer T>
68
+ ? T extends HttpTrigger<infer M, infer Cur>
69
+ ? [Cur] extends [HttpPathPending]
70
+ ? FlowDef<I, O, E, D, HttpTrigger<M, P>>
71
+ : F
72
+ : F
73
+ : F;
74
+
75
+ export { HTTP_PATH_PENDING, isPendingHttpPath } from "./http-path-pending.ts";