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
@@ -0,0 +1,129 @@
1
+ /**
2
+ * useLive — mount/unmount and typed event state.
3
+ */
4
+
5
+ import { describe, expect, test, beforeEach, afterEach } from "bun:test";
6
+ import { Window } from "happy-dom";
7
+ import { act, createElement } from "react";
8
+ import { createRoot, type Root } from "react-dom/client";
9
+ import { createClient } from "../client/create.ts";
10
+ import type { AppOf, LiveSignalHandle } from "../client/types.ts";
11
+ import { useLive } from "./index.ts";
12
+
13
+ type EventsApp = AppOf<{
14
+ orders: {
15
+ events: {
16
+ in: { orderId: string };
17
+ out: { orderId: string; status: string };
18
+ method: "GET";
19
+ path: "/orders/:orderId/events";
20
+ live: "order-status";
21
+ matchKey: ["orderId"];
22
+ stream: true;
23
+ };
24
+ };
25
+ }>;
26
+
27
+ type Payload = { orderId: string; status: string };
28
+
29
+ const orderStatus: LiveSignalHandle<Payload> = {
30
+ name: "order-status",
31
+ };
32
+
33
+ function sseResponse(frames: readonly unknown[], ids?: readonly string[]): Response {
34
+ const lines: string[] = [];
35
+ frames.forEach((payload, i) => {
36
+ const id = ids?.[i];
37
+ if (id !== undefined) lines.push(`id: ${id}`);
38
+ lines.push(`data: ${JSON.stringify(payload)}`);
39
+ lines.push("");
40
+ });
41
+ lines.push("data: [DONE]");
42
+ lines.push("");
43
+ return new Response(lines.join("\n"), {
44
+ status: 200,
45
+ headers: { "content-type": "text/event-stream" },
46
+ });
47
+ }
48
+
49
+ async function waitFor(pred: () => boolean, ms = 1_000): Promise<void> {
50
+ const start = Date.now();
51
+ while (!pred()) {
52
+ if (Date.now() - start > ms) throw new Error("timeout");
53
+ await new Promise((r) => setTimeout(r, 5));
54
+ }
55
+ }
56
+
57
+ describe("useLive", () => {
58
+ let happy: Window;
59
+
60
+ beforeEach(() => {
61
+ (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
62
+ happy = new Window({ url: "http://app.test/" });
63
+ Object.defineProperty(globalThis, "window", {
64
+ value: happy,
65
+ configurable: true,
66
+ writable: true,
67
+ });
68
+ Object.defineProperty(globalThis, "document", {
69
+ value: happy.document,
70
+ configurable: true,
71
+ writable: true,
72
+ });
73
+ });
74
+
75
+ afterEach(() => {
76
+ happy.close();
77
+ });
78
+
79
+ test("collects events and latest; unmount stops fetches", async () => {
80
+ let calls = 0;
81
+ const payload = { orderId: "ord_1", status: "placed" as const };
82
+ const api = createClient<EventsApp>("http://app.test", {
83
+ fetch: async () => {
84
+ calls += 1;
85
+ return sseResponse([payload], ["evt-1"]);
86
+ },
87
+ $routes: {
88
+ orders: {
89
+ events: {
90
+ method: "GET",
91
+ path: "/orders/:orderId/events",
92
+ live: "order-status",
93
+ matchKey: ["orderId"],
94
+ stream: true,
95
+ },
96
+ },
97
+ },
98
+ });
99
+
100
+ const host = happy.document.createElement("div");
101
+ happy.document.body.appendChild(host);
102
+ const seen: { latest: Payload | null; events: Payload[] } = { latest: null, events: [] };
103
+
104
+ function Probe(): null {
105
+ const state = useLive(api, orderStatus, { orderId: "ord_1" });
106
+ seen.latest = state.latest;
107
+ seen.events = state.events;
108
+ return null;
109
+ }
110
+
111
+ let root: Root | undefined;
112
+ await act(async () => {
113
+ root = createRoot(host as unknown as Element);
114
+ root.render(createElement(Probe));
115
+ });
116
+ await waitFor(() => seen.events.length === 1);
117
+ expect(seen.latest).toEqual(payload);
118
+ expect(seen.events).toEqual([payload]);
119
+ expect(calls).toBe(1);
120
+
121
+ await act(async () => {
122
+ root?.unmount();
123
+ });
124
+ const after = calls;
125
+ await new Promise((r) => setTimeout(r, 200));
126
+ expect(calls).toBe(after);
127
+ host.remove();
128
+ });
129
+ });
@@ -7,6 +7,8 @@
7
7
  */
8
8
 
9
9
  import type {
10
+ AuthApiKeysResourceRef,
11
+ AuthTenantsResourceRef,
10
12
  Effects,
11
13
  PromptRef,
12
14
  ResourceRef,
@@ -145,8 +147,10 @@ const TABLE_ARG_METHODS = new Set([
145
147
  * @param options - Handler AST, bindings, and annotation flag
146
148
  */
147
149
  export function inferEffects(options: InferEffectsOptions): InferredEffects {
148
- const reads = new Set<ResourceRef | SignalResourceRef>();
149
- const writes = new Set<ResourceRef>();
150
+ const reads = new Set<
151
+ ResourceRef | SignalResourceRef | AuthApiKeysResourceRef | AuthTenantsResourceRef
152
+ >();
153
+ const writes = new Set<ResourceRef | AuthApiKeysResourceRef | AuthTenantsResourceRef>();
150
154
  const emits = new Set<SignalRef>();
151
155
  const sends = new Set<TemplateRef>();
152
156
  const asks = new Set<PromptRef>();
@@ -182,6 +186,12 @@ export function inferEffects(options: InferEffectsOptions): InferredEffects {
182
186
  continue;
183
187
  }
184
188
 
189
+ if (chain.rootMethod === "live" && call === chain.rootCall) {
190
+ const ref = resolveNamed(call.arguments[0], options.bindings, "signal");
191
+ if (ref) reads.add(`signal:${ref}`);
192
+ continue;
193
+ }
194
+
185
195
  if (chain.rootMethod === "send" && call === chain.rootCall) {
186
196
  const ref = resolveNamed(call.arguments[0], options.bindings, "template");
187
197
  if (ref) sends.add(ref);
@@ -217,6 +227,31 @@ export function inferEffects(options: InferEffectsOptions): InferredEffects {
217
227
  continue;
218
228
  }
219
229
 
230
+ if (chain.rootMethod === "auth" && call === chain.rootCall) {
231
+ const method = chain.methods[1];
232
+ if (method === "listApiKeys") reads.add("auth:api-keys");
233
+ if (method === "listTenants" || method === "listMembers") reads.add("auth:tenants");
234
+ if (
235
+ method === "createApiKey" ||
236
+ method === "revokeApiKey" ||
237
+ method === "rotateApiKey" ||
238
+ method === "updateApiKey"
239
+ ) {
240
+ writes.add("auth:api-keys");
241
+ }
242
+ if (
243
+ method === "switchTenant" ||
244
+ method === "createTenant" ||
245
+ method === "deleteTenant" ||
246
+ method === "addMember" ||
247
+ method === "removeMember" ||
248
+ method === "upsertTenantRole"
249
+ ) {
250
+ writes.add("auth:tenants");
251
+ }
252
+ continue;
253
+ }
254
+
220
255
  if (chain.rootMethod === "search" && call === chain.rootCall) {
221
256
  const ref = resolveNamed(call.arguments[0], options.bindings, "embed");
222
257
  if (ref) {
@@ -337,6 +337,23 @@ export const stripeKey = vault.secret("STRIPE_KEY", {
337
337
  expect(manifest.vault?.STRIPE_KEY?.description).toBe("Payments gateway key");
338
338
  });
339
339
 
340
+ test("extracts live signal retention", async () => {
341
+ const manifest = await extractFromSources({
342
+ "src/signals.ts": `
343
+ import { signal } from "okengine";
344
+ export const orderStatus = signal("order-status", {
345
+ delivery: "live",
346
+ optional: true,
347
+ retention: { maxAge: "24h", maxCount: 500 },
348
+ });
349
+ `,
350
+ });
351
+ expect(manifest.signals?.["order-status"]?.retention).toEqual({
352
+ maxAge: "24h",
353
+ maxCount: 500,
354
+ });
355
+ });
356
+
340
357
  test("extracts store.kv durable onto the Manifest store", async () => {
341
358
  const manifest = await extractFromSources({
342
359
  "src/kv.ts": `
@@ -362,7 +379,6 @@ export const { list, create, get, update, remove } = bindNamedTableCrud({
362
379
  unit: "cycles",
363
380
  path: "/cycles",
364
381
  table: cycles,
365
- liveList: true,
366
382
  });
367
383
  `;
368
384
  const helper = `
@@ -379,7 +395,7 @@ export function bindNamedTableCrud(spec) {
379
395
  expect(manifest.flows?.["cycles.list"]?.trigger).toEqual({
380
396
  http: { method: "GET", path: "/cycles" },
381
397
  });
382
- expect(manifest.flows?.["cycles.list"]?.live).toBe(true);
398
+ expect(manifest.flows?.["cycles.list"]?.live).toBeUndefined();
383
399
  expect(manifest.flows?.["cycles.create"]?.trigger).toEqual({
384
400
  http: { method: "POST", path: "/cycles" },
385
401
  });
@@ -469,7 +485,7 @@ export const remove = mounted.remove;
469
485
  expect(manifest.flows?.list?.breaking).toBe(true);
470
486
  });
471
487
 
472
- test("chains .gate(...) and .live() onto the five verbs", async () => {
488
+ test("chains .gate(...) onto the five verbs", async () => {
473
489
  const source = `
474
490
  import { on, http, store, gate } from "okengine";
475
491
 
@@ -478,7 +494,7 @@ export const db = store.sql("notes", { schema: {} });
478
494
 
479
495
  const notesR = store.resource(db, {}, {});
480
496
 
481
- const mounted = on(http.resource("/notes", notesR.all()).gate(member).live());
497
+ const mounted = on(http.resource("/notes", notesR.all()).gate(member));
482
498
 
483
499
  export const list = mounted.list;
484
500
  export const create = mounted.create;
@@ -493,20 +509,18 @@ export const remove = mounted.remove;
493
509
  expect(manifest.flows?.get?.gates).toEqual(["member"]);
494
510
  expect(manifest.flows?.update?.gates).toEqual(["member"]);
495
511
  expect(manifest.flows?.remove?.gates).toEqual(["member"]);
496
- expect(manifest.flows?.list?.live).toBe(true);
497
- expect(manifest.flows?.get?.live).toBe(true);
512
+ expect(manifest.flows?.list?.live).toBeUndefined();
513
+ expect(manifest.flows?.get?.live).toBeUndefined();
498
514
  expect(manifest.flows?.create?.live).toBeUndefined();
499
- expect(manifest.flows?.update?.live).toBeUndefined();
500
- expect(manifest.flows?.remove?.live).toBeUndefined();
501
515
  });
502
516
 
503
- test("chains .public().live() onto the five verbs", async () => {
517
+ test("chains .public() onto the five verbs", async () => {
504
518
  const source = `
505
519
  import { on, http, store } from "okengine";
506
520
 
507
521
  export const db = store.sql("notes", { schema: {} });
508
522
  const notesR = store.resource(db, {}, {});
509
- const mounted = on(http.resource("/notes", notesR.all()).public().live());
523
+ const mounted = on(http.resource("/notes", notesR.all()).public());
510
524
 
511
525
  export const list = mounted.list;
512
526
  export const create = mounted.create;
@@ -517,7 +531,7 @@ export const remove = mounted.remove;
517
531
  const manifest = await extractFromSources({ "src/flows/notes.ts": source });
518
532
  expect(manifest.flows?.list?.gates).toEqual(["public"]);
519
533
  expect(manifest.flows?.create?.gates).toEqual(["public"]);
520
- expect(manifest.flows?.get?.live).toBe(true);
534
+ expect(manifest.flows?.get?.live).toBeUndefined();
521
535
  expect(manifest.flows?.create?.live).toBeUndefined();
522
536
  });
523
537
  });
@@ -672,6 +686,67 @@ export const failed = on(
672
686
  });
673
687
  });
674
688
 
689
+ describe("extractManifest — fx.live / .live(signal)", () => {
690
+ test("one-arg on(http.get(path).live(signal)) stamps live + signal read", async () => {
691
+ const source = `
692
+ import { on, http, gate, signal } from "okengine";
693
+ import { z } from "zod";
694
+
695
+ export const member = gate.policy("member", ({ auth }) => !!auth.verified);
696
+ export const orderStatus = signal("order-status", {
697
+ delivery: "live",
698
+ optional: true,
699
+ schema: z.object({ orderId: z.string(), status: z.string() }),
700
+ });
701
+
702
+ export const events = on(http.get("/orders/:orderId/events").gate(member).live(orderStatus));
703
+ `;
704
+ const manifest = await extractFromSources({ "src/flows/orders/index.ts": source });
705
+ const flow = manifest.flows?.["orders.events"];
706
+ expect(flow?.live).toBe("order-status");
707
+ expect(flow?.trigger).toEqual({
708
+ http: { method: "GET", path: "/orders/:orderId/events" },
709
+ });
710
+ expect(flow?.gates).toEqual(["member"]);
711
+ expect(flow?.effects?.reads).toEqual(["signal:order-status"]);
712
+ });
713
+
714
+ test("http.live(signal) uses GET /_oke/live/{name}", async () => {
715
+ const source = `
716
+ import { on, http, gate, signal } from "okengine";
717
+
718
+ export const member = gate.policy("member", ({ auth }) => !!auth.verified);
719
+ export const orderStatus = signal("order-status", { delivery: "live", optional: true });
720
+
721
+ export const firehose = on(http.live(orderStatus).gate(member));
722
+ `;
723
+ const manifest = await extractFromSources({ "src/flows/orders/index.ts": source });
724
+ const flow = manifest.flows?.["orders.firehose"];
725
+ expect(flow?.live).toBe("order-status");
726
+ expect(flow?.trigger).toEqual({
727
+ http: { method: "GET", path: "/_oke/live/order-status" },
728
+ });
729
+ });
730
+
731
+ test("fx.live(signal) infers reads: [signal:<name>]", async () => {
732
+ const source = `
733
+ import { on, flow, http, signal } from "okengine";
734
+
735
+ export const orderStatus = signal("order-status", { delivery: "live", optional: true });
736
+
737
+ export const events = on(
738
+ http.get("/feed").public().live(orderStatus),
739
+ flow("orders.events", {
740
+ do: (_input, fx) => fx.live(orderStatus),
741
+ }),
742
+ );
743
+ `;
744
+ const manifest = await extractFromSources({ "src/flows/orders.ts": source });
745
+ expect(manifest.flows?.["orders.events"]?.live).toBe("order-status");
746
+ expect(manifest.flows?.["orders.events"]?.effects?.reads).toEqual(["signal:order-status"]);
747
+ });
748
+ });
749
+
675
750
  describe("extractManifest — channel medium binder aliasing", () => {
676
751
  test("mail.template(...) resolves through `const mail = channel.email(...)`", async () => {
677
752
  const source = `
@@ -1205,3 +1280,60 @@ export const create = flow("views.create", {
1205
1280
  });
1206
1281
  });
1207
1282
  });
1283
+
1284
+ describe("extractManifest — tenancy", () => {
1285
+ test("oke({ gate: { auth: { tenant: true }}}) stamps tenancy.enabled", async () => {
1286
+ const source = `
1287
+ import { oke, on, flow, http, store, field } from "okengine";
1288
+
1289
+ export const bookings = store.schema.table("bookings", {
1290
+ id: field.text().primaryKey(),
1291
+ tenantId: field.text().notNull(),
1292
+ }, [
1293
+ store.schema.policy.tenant("tenant_id"),
1294
+ ]);
1295
+
1296
+ export const db = store.sql("app", { schema: { bookings } });
1297
+
1298
+ export const ping = on(
1299
+ http.get("/ping").public(),
1300
+ flow("health.ping", { do: () => ({ ok: true }) }),
1301
+ );
1302
+
1303
+ export const app = oke({
1304
+ name: "shop",
1305
+ gate: { auth: { tenant: true } },
1306
+ });
1307
+ `;
1308
+ const manifest = await extractFromSources({ "src/app.ts": source });
1309
+ expect(manifest.tenancy).toMatchObject({
1310
+ enabled: true,
1311
+ isolation: "row",
1312
+ membershipRequired: false,
1313
+ });
1314
+ expect(manifest.flows?.["health.ping"]?.tenantScoped).toBe(true);
1315
+ expect(manifest.stores?.app?.tables?.bookings?.policies?.tenant_tenant_id_all?.using).toContain(
1316
+ "oke.tenant()",
1317
+ );
1318
+ });
1319
+
1320
+ test("SQL table without tenant policy or unscoped() fails loud", async () => {
1321
+ const source = `
1322
+ import { oke, store, field } from "okengine";
1323
+
1324
+ export const notes = store.schema.table("notes", {
1325
+ id: field.text().primaryKey(),
1326
+ });
1327
+
1328
+ export const db = store.sql("app", { schema: { notes } });
1329
+
1330
+ export const app = oke({
1331
+ name: "shop",
1332
+ gate: { auth: { tenant: true } },
1333
+ });
1334
+ `;
1335
+ await expect(extractFromSources({ "src/app.ts": source })).rejects.toThrow(
1336
+ /needs store.schema.policy.tenant/,
1337
+ );
1338
+ });
1339
+ });