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
@@ -41,6 +41,20 @@ const SEED_SPACES = [
41
41
  ] as const;
42
42
 
43
43
  const SEED_MEMBERS = [
44
+ {
45
+ id: "mem_demo",
46
+ space_id: "space_eng",
47
+ name: "Demo User",
48
+ email: "demo@example.com",
49
+ role: "owner",
50
+ },
51
+ {
52
+ id: "mem_admin",
53
+ space_id: "space_eng",
54
+ name: "Avery Cole",
55
+ email: "admin@keel.dev",
56
+ role: "admin",
57
+ },
44
58
  {
45
59
  id: "mem_aria",
46
60
  space_id: "space_eng",
@@ -48,6 +62,13 @@ const SEED_MEMBERS = [
48
62
  email: "aria@keel.dev",
49
63
  role: "project_manager",
50
64
  },
65
+ {
66
+ id: "mem_lead",
67
+ space_id: "space_eng",
68
+ name: "Jules Hart",
69
+ email: "jules@keel.dev",
70
+ role: "lead",
71
+ },
51
72
  {
52
73
  id: "mem_ben",
53
74
  space_id: "space_eng",
@@ -55,6 +76,13 @@ const SEED_MEMBERS = [
55
76
  email: "ben@keel.dev",
56
77
  role: "developer",
57
78
  },
79
+ {
80
+ id: "mem_eli",
81
+ space_id: "space_eng",
82
+ name: "Eli Park",
83
+ email: "eli@keel.dev",
84
+ role: "contributor",
85
+ },
58
86
  {
59
87
  id: "mem_cai",
60
88
  space_id: "space_des",
@@ -62,6 +90,20 @@ const SEED_MEMBERS = [
62
90
  email: "cai@keel.dev",
63
91
  role: "member",
64
92
  },
93
+ {
94
+ id: "mem_member",
95
+ space_id: "space_des",
96
+ name: "Member",
97
+ email: "member@example.com",
98
+ role: "member",
99
+ },
100
+ {
101
+ id: "mem_commenter",
102
+ space_id: "space_gtm",
103
+ name: "Kit Rowe",
104
+ email: "kit@keel.dev",
105
+ role: "commenter",
106
+ },
65
107
  {
66
108
  id: "mem_dia",
67
109
  space_id: "space_gtm",
@@ -69,13 +111,6 @@ const SEED_MEMBERS = [
69
111
  email: "dia@keel.dev",
70
112
  role: "guest",
71
113
  },
72
- {
73
- id: "mem_eli",
74
- space_id: "space_eng",
75
- name: "Eli Park",
76
- email: "eli@keel.dev",
77
- role: "developer",
78
- },
79
114
  ] as const;
80
115
 
81
116
  const SEED_GOALS = [
@@ -640,14 +675,7 @@ function generateKeelVolume(): {
640
675
  email: `${p.id}@keel.dev`,
641
676
  role: "member",
642
677
  }));
643
- const emails = [
644
- "aria@keel.dev",
645
- "ben@keel.dev",
646
- "cai@keel.dev",
647
- "dia@keel.dev",
648
- "eli@keel.dev",
649
- ...members.map((m) => m.email),
650
- ];
678
+ const emails = [...SEED_MEMBERS.map((m) => m.email), ...members.map((m) => m.email)];
651
679
 
652
680
  const projects = Array.from({ length: GENERATED.projects }, (_, i) => ({
653
681
  id: `proj_g_${String(i + 1).padStart(2, "0")}`,
@@ -21,6 +21,7 @@ const FEATURES_ROOT = `${UI_ROOT}/features`;
21
21
  * already covers it; the list makes coverage explicit in CI output.
22
22
  */
23
23
  export const CONSOLE_UI_FEATURE_DIRS = [
24
+ "access",
24
25
  "auth",
25
26
  "flows",
26
27
  "not-found",
@@ -40,6 +40,8 @@ export type {
40
40
  SignalDriverId,
41
41
  SignalDriver,
42
42
  SignalBus,
43
+ LiveEvent,
44
+ LiveSubscribeOptions,
43
45
  SignalMessage,
44
46
  SignalEmitOptions,
45
47
  SignalFailureReason,
@@ -36,6 +36,7 @@ interface JournalDbRow {
36
36
  lease_expires_at: number | null;
37
37
  created_at: number;
38
38
  updated_at: number;
39
+ tenant: string | null;
39
40
  }
40
41
 
41
42
  /** Minimal SQL + transaction surface for the postgres journal store. */
@@ -151,6 +152,9 @@ function rowToRun(row: Record<string, unknown>): JournalRun {
151
152
  if (r.locked_by !== null && r.locked_by !== undefined) run.lockedBy = String(r.locked_by);
152
153
  const leaseExpiresAt = numOrNull(r.lease_expires_at);
153
154
  if (leaseExpiresAt !== null) run.leaseExpiresAt = leaseExpiresAt;
155
+ if (r.tenant !== null && r.tenant !== undefined && String(r.tenant).length > 0) {
156
+ run.tenant = String(r.tenant);
157
+ }
154
158
  return run;
155
159
  }
156
160
 
@@ -168,6 +172,7 @@ function runToParams(run: JournalRun): unknown[] {
168
172
  run.leaseExpiresAt ?? null,
169
173
  run.createdAt,
170
174
  run.updatedAt,
175
+ run.tenant ?? null,
171
176
  ];
172
177
  }
173
178
 
@@ -184,8 +189,10 @@ async function ensureSchema(sql: PostgresJournalSql): Promise<void> {
184
189
  locked_by TEXT,
185
190
  lease_expires_at BIGINT,
186
191
  created_at BIGINT NOT NULL,
187
- updated_at BIGINT NOT NULL
192
+ updated_at BIGINT NOT NULL,
193
+ tenant TEXT
188
194
  )`);
195
+ await sql.exec(`ALTER TABLE oke_journal_runs ADD COLUMN IF NOT EXISTS tenant TEXT`);
189
196
  await sql.exec(
190
197
  `CREATE INDEX IF NOT EXISTS oke_journal_runs_wake ON oke_journal_runs (status, wake_at)`,
191
198
  );
@@ -308,7 +315,8 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
308
315
  const text = sql.trim();
309
316
  const state = view();
310
317
 
311
- if (/^CREATE\s+(TABLE|INDEX)/i.test(text)) return { changes: 0 };
318
+ if (/^CREATE\s+(TABLE|INDEX)/i.test(text) || /^ALTER\s+TABLE/i.test(text))
319
+ return { changes: 0 };
312
320
 
313
321
  const upsert =
314
322
  /^INSERT\s+INTO\s+oke_journal_runs\s*\(([^)]+)\)\s*VALUES\s*\(([^)]+)\)\s*ON\s+CONFLICT\s*\(\s*id\s*\)\s*DO\s+UPDATE\s+SET\s+.+$/i.exec(
@@ -337,6 +345,7 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
337
345
  : Number(record.lease_expires_at),
338
346
  created_at: Number(record.created_at ?? 0),
339
347
  updated_at: Number(record.updated_at ?? 0),
348
+ tenant: (record.tenant as string | null) ?? null,
340
349
  };
341
350
  const idx = state.rows.findIndex((r) => r.id === next.id);
342
351
  if (idx >= 0) state.rows[idx] = next;
@@ -423,7 +432,7 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
423
432
  return api;
424
433
  }
425
434
 
426
- const UPSERT_SQL = `INSERT INTO oke_journal_runs (id, flow, input, status, entries, wake_at, error, output, locked_by, lease_expires_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET flow = EXCLUDED.flow, input = EXCLUDED.input, status = EXCLUDED.status, entries = EXCLUDED.entries, wake_at = EXCLUDED.wake_at, error = EXCLUDED.error, output = EXCLUDED.output, locked_by = EXCLUDED.locked_by, lease_expires_at = EXCLUDED.lease_expires_at, created_at = EXCLUDED.created_at, updated_at = EXCLUDED.updated_at`;
435
+ const UPSERT_SQL = `INSERT INTO oke_journal_runs (id, flow, input, status, entries, wake_at, error, output, locked_by, lease_expires_at, created_at, updated_at, tenant) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET flow = EXCLUDED.flow, input = EXCLUDED.input, status = EXCLUDED.status, entries = EXCLUDED.entries, wake_at = EXCLUDED.wake_at, error = EXCLUDED.error, output = EXCLUDED.output, locked_by = EXCLUDED.locked_by, lease_expires_at = EXCLUDED.lease_expires_at, created_at = EXCLUDED.created_at, updated_at = EXCLUDED.updated_at, tenant = EXCLUDED.tenant`;
427
436
 
428
437
  /** Postgres journal store with run-level lease coordination. */
429
438
  export type PostgresJournalStore = JournalStore &
@@ -3,6 +3,8 @@
3
3
  * Drizzle emit, and Gate identity helpers (`oke.gate` / `oke.user` / `oke.has_scope`).
4
4
  */
5
5
 
6
+ import { lazyRequire } from "../kernel/lazy-require.ts";
7
+
6
8
  /** Policy command (`FOR`). */
7
9
  export type SqlPolicyCommand = "ALL" | "SELECT" | "INSERT" | "UPDATE" | "DELETE";
8
10
 
@@ -49,6 +51,11 @@ export type RlsIdentity = {
49
51
  readonly gate: string;
50
52
  readonly userId: string;
51
53
  readonly scopes: readonly string[];
54
+ /**
55
+ * When set, the prelude stamps `oke.tenant` (fourth GUC).
56
+ * Omit entirely when tenancy is off so the prelude stays three params.
57
+ */
58
+ readonly tenantId?: string;
52
59
  };
53
60
 
54
61
  /**
@@ -98,6 +105,8 @@ LANGUAGE sql STABLE AS $$
98
105
  ELSE jsonb_exists(current_setting('oke.scopes', true)::jsonb, p_scope)
99
106
  END
100
107
  $$`,
108
+ `CREATE OR REPLACE FUNCTION oke.tenant() RETURNS text
109
+ LANGUAGE sql STABLE AS $$ SELECT current_setting('oke.tenant', true) $$`,
101
110
  `DO $oke_rls_role$
102
111
  BEGIN
103
112
  CREATE ROLE ${OKE_RLS_ROLE} NOSUPERUSER NOBYPASSRLS NOLOGIN;
@@ -122,6 +131,7 @@ $oke_app_search_path$`,
122
131
 
123
132
  /**
124
133
  * All {@link OKE_RLS_HELPER_STATEMENTS} as one script (PGlite `exec` / `oke db push`).
134
+ * Vault table RLS is attached by {@link installOkeRlsHelpers} after helpers exist.
125
135
  */
126
136
  export const OKE_RLS_HELPER_SQL = `${OKE_RLS_HELPER_STATEMENTS.join(";\n")};`;
127
137
 
@@ -134,6 +144,10 @@ export async function installOkeRlsHelpers(exec: (sql: string) => Promise<unknow
134
144
  for (const stmt of OKE_RLS_HELPER_STATEMENTS) {
135
145
  await exec(stmt);
136
146
  }
147
+ const { attach } = lazyRequire<{
148
+ attach: (exec: (sql: string) => Promise<unknown>) => Promise<void>;
149
+ }>(import.meta.dir, ["pg", "vault", "rls"].join("-"));
150
+ await attach(exec);
137
151
  }
138
152
 
139
153
  /**
@@ -449,12 +463,22 @@ export type RlsPreludeStatement = {
449
463
  * @param identity - Gate principal
450
464
  */
451
465
  export function buildRlsIdentityPreludeSql(identity: RlsIdentity): readonly RlsPreludeStatement[] {
466
+ const configs = [
467
+ "set_config('oke.gate', ?, true)",
468
+ "set_config('oke.user', ?, true)",
469
+ "set_config('oke.scopes', ?, true)",
470
+ ];
471
+ const params: string[] = [identity.gate, identity.userId, rlsScopesJson(identity.scopes)];
472
+ if (identity.tenantId !== undefined) {
473
+ configs.push("set_config('oke.tenant', ?, true)");
474
+ params.push(identity.tenantId);
475
+ }
452
476
  return [
453
477
  { sql: `SET LOCAL ROLE ${OKE_RLS_ROLE}` },
454
478
  { sql: "SET LOCAL row_security = on" },
455
479
  {
456
- sql: `SELECT set_config('oke.gate', ?, true), set_config('oke.user', ?, true), set_config('oke.scopes', ?, true)`,
457
- params: [identity.gate, identity.userId, rlsScopesJson(identity.scopes)],
480
+ sql: `SELECT ${configs.join(", ")}`,
481
+ params,
458
482
  },
459
483
  ];
460
484
  }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Tenant RLS on `oke_vault_secrets` — kept off the Store-only `oke()` graph.
3
+ *
4
+ * Same `oke.tenant()` helper as domain tables. Loaded from helper install
5
+ * and Vault DDL via a computed require.
6
+ */
7
+
8
+ /** Same role as `OKE_RLS_ROLE` (`oke_app`) — duplicated so this module stays off `pg-rls`. */
9
+ const VAULT_RLS_ROLE = "oke_app";
10
+
11
+ /**
12
+ * Built-in Vault ciphertext table. Global rows (`tenant_id IS NULL`) stay
13
+ * visible; per-tenant rows are isolated. No-op until `oke_vault_secrets`
14
+ * and `oke.tenant()` exist.
15
+ *
16
+ * ENABLE without FORCE: the Vault adapter (table owner) still sees every
17
+ * row; Console / `oke_app` stamped SQL cannot list another tenant's
18
+ * ciphertext.
19
+ */
20
+ export const OKE_VAULT_SECRETS_TENANT_RLS_SQL = `DO $oke_vault_rls$
21
+ BEGIN
22
+ IF to_regclass('public.oke_vault_secrets') IS NULL THEN
23
+ RETURN;
24
+ END IF;
25
+ IF NOT EXISTS (
26
+ SELECT 1 FROM pg_proc p
27
+ JOIN pg_namespace n ON n.oid = p.pronamespace
28
+ WHERE n.nspname = 'oke' AND p.proname = 'tenant' AND p.pronargs = 0
29
+ ) THEN
30
+ RETURN;
31
+ END IF;
32
+ EXECUTE 'ALTER TABLE oke_vault_secrets ENABLE ROW LEVEL SECURITY';
33
+ BEGIN
34
+ EXECUTE 'GRANT SELECT, INSERT, UPDATE, DELETE ON oke_vault_secrets TO ${VAULT_RLS_ROLE}';
35
+ EXCEPTION
36
+ WHEN undefined_object THEN NULL;
37
+ END;
38
+ IF NOT EXISTS (
39
+ SELECT 1 FROM pg_policies
40
+ WHERE schemaname = 'public'
41
+ AND tablename = 'oke_vault_secrets'
42
+ AND policyname = 'oke_vault_secrets_tenant'
43
+ ) THEN
44
+ EXECUTE $p$
45
+ CREATE POLICY oke_vault_secrets_tenant ON oke_vault_secrets
46
+ AS PERMISSIVE FOR ALL TO public
47
+ USING (tenant_id = oke.tenant() OR tenant_id IS NULL)
48
+ WITH CHECK (tenant_id = oke.tenant() OR tenant_id IS NULL)
49
+ $p$;
50
+ END IF;
51
+ END
52
+ $oke_vault_rls$`;
53
+
54
+ /**
55
+ * Attach tenant RLS to `oke_vault_secrets` when the table and `oke.tenant()`
56
+ * are both present. Safe to call from Vault DDL and from helper install
57
+ * (either boot order).
58
+ *
59
+ * @param exec - Statement runner
60
+ */
61
+ export async function ensureVaultSecretsTenantRls(
62
+ exec: (sql: string) => Promise<unknown>,
63
+ ): Promise<void> {
64
+ await exec(OKE_VAULT_SECRETS_TENANT_RLS_SQL);
65
+ }
66
+
67
+ /** Short name for the Store-only `oke()` helper-install path. */
68
+ export { ensureVaultSecretsTenantRls as attach };
@@ -12,10 +12,12 @@ import type { SignalDecl } from "../elements/signal/declare.ts";
12
12
  import type { SignalDelivery } from "../manifest/types.ts";
13
13
  import { DryRunWriteIsolationError, setDryRunMessageId, withDryRun } from "../kernel/dry-run.ts";
14
14
  import { OkeError, OKE_ERRORS } from "../kernel/errors.ts";
15
+ import { LIVE_RESUME_GAP } from "../kernel/errors-live-resume.ts";
15
16
  import {
16
17
  SIGNAL_DEFAULT_LEASE_MS,
17
18
  validateSignalEmitPayload,
18
19
  type DeadLetter,
20
+ type LiveEvent,
19
21
  type LiveHandler,
20
22
  type SignalBus,
21
23
  type SignalDiscardOptions,
@@ -30,7 +32,10 @@ import {
30
32
  type SignalStats,
31
33
  type SignalTransaction,
32
34
  type SignalUnsubscribe,
35
+ type LiveSubscribeOptions,
33
36
  } from "./signal-types.ts";
37
+ import { createLiveIterable } from "./signal-live-iter.ts";
38
+ import { liveIdsToPrune, skipAfterId } from "./signal-retention.ts";
34
39
 
35
40
  /** Internal mutable message. */
36
41
  interface MutMessage {
@@ -313,26 +318,68 @@ export async function createSignalEngine(
313
318
  };
314
319
  }
315
320
 
316
- async function live(signal: string, handler: LiveHandler): Promise<SignalUnsubscribe> {
321
+ function live(signal: string, opts?: LiveSubscribeOptions): AsyncIterable<LiveEvent> {
317
322
  const decl = requireDecl(signal);
318
323
  if (decl.delivery !== "live") {
319
324
  throw new Error(`signal "${signal}" is not delivery: "live"`);
320
325
  }
321
- let set = liveHandlers.get(signal);
322
- if (!set) {
323
- set = new Set();
324
- liveHandlers.set(signal, set);
325
- }
326
- set.add(handler);
327
- // Replay retained live messages to late subscribers.
328
- for (const m of messages) {
329
- if (m.signal === signal && m.delivery === "live") {
330
- await handler(m.payload);
326
+ const afterId = opts?.afterId;
327
+ return createLiveIterable((emit) => {
328
+ pruneLive(signal);
329
+ const history: LiveEvent[] = [];
330
+ for (const m of messages) {
331
+ if (m.signal === signal && m.delivery === "live") {
332
+ history.push({ id: m.id, payload: m.payload });
333
+ }
334
+ }
335
+ const skipped = skipAfterId(history, afterId);
336
+ if (afterId !== undefined && afterId.length > 0 && !skipped.found) {
337
+ throw new OkeError(LIVE_RESUME_GAP, { signal, afterId });
338
+ }
339
+ let set = liveHandlers.get(signal);
340
+ if (!set) {
341
+ set = new Set();
342
+ liveHandlers.set(signal, set);
331
343
  }
344
+ const handler: LiveHandler = (event) => {
345
+ emit(event);
346
+ };
347
+ set.add(handler);
348
+ for (const event of skipped.rest) {
349
+ emit(event);
350
+ }
351
+ return () => {
352
+ set.delete(handler);
353
+ };
354
+ });
355
+ }
356
+
357
+ async function checkLiveResume(signal: string, afterId: string): Promise<void> {
358
+ requireDecl(signal);
359
+ pruneLive(signal);
360
+ const found = messages.some(
361
+ (m) => m.signal === signal && m.delivery === "live" && m.id === afterId,
362
+ );
363
+ if (!found) {
364
+ throw new OkeError(LIVE_RESUME_GAP, { signal, afterId });
365
+ }
366
+ }
367
+
368
+ function pruneLive(signal: string): void {
369
+ const decl = signals.get(signal);
370
+ const drop = liveIdsToPrune(
371
+ messages
372
+ .filter((m) => m.signal === signal && m.delivery === "live")
373
+ .map((m) => ({ id: m.id, createdAt: m.createdAt })),
374
+ decl?.retention,
375
+ now(),
376
+ );
377
+ if (drop.length === 0) return;
378
+ const dropSet = new Set(drop);
379
+ for (let i = messages.length - 1; i >= 0; i--) {
380
+ const m = messages[i]!;
381
+ if (m.delivery === "live" && dropSet.has(m.id)) messages.splice(i, 1);
332
382
  }
333
- return () => {
334
- set!.delete(handler);
335
- };
336
383
  }
337
384
 
338
385
  function toPublic(m: MutMessage): SignalMessage {
@@ -469,8 +516,9 @@ export async function createSignalEngine(
469
516
  if (m.delivery !== "live" || m.status !== "pending") continue;
470
517
  const handlers = liveHandlers.get(m.signal);
471
518
  if (handlers && handlers.size > 0) {
519
+ const event: LiveEvent = { id: m.id, payload: m.payload };
472
520
  for (const h of handlers) {
473
- await h(m.payload);
521
+ await h(event);
474
522
  }
475
523
  }
476
524
  pushLive(m.signal, m.payload);
@@ -478,6 +526,11 @@ export async function createSignalEngine(
478
526
  noteDelivered();
479
527
  progress = true;
480
528
  }
529
+ const touched = new Set<string>();
530
+ for (const m of messages) {
531
+ if (m.delivery === "live") touched.add(m.signal);
532
+ }
533
+ for (const name of touched) pruneLive(name);
481
534
  return progress;
482
535
  }
483
536
 
@@ -717,6 +770,7 @@ export async function createSignalEngine(
717
770
  begin,
718
771
  subscribe,
719
772
  live,
773
+ checkLiveResume,
720
774
  drain,
721
775
  deadLetters,
722
776
  inspect,
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Callback-set → {@link AsyncIterable} adapter for {@link SignalBus.live}.
3
+ */
4
+
5
+ import type { LiveEvent } from "./signal-types.ts";
6
+
7
+ /**
8
+ * Turn a live subscriber (replay-then-push) into an async iterable.
9
+ *
10
+ * `start` must register `emit` and replay retained events into it before
11
+ * resolving; `drain` later pushes new events through the same `emit`.
12
+ *
13
+ * @param start - Register + replay; return unsubscribe
14
+ */
15
+ export function createLiveIterable(
16
+ start: (emit: (event: LiveEvent) => void) => (() => void) | Promise<() => void>,
17
+ ): AsyncIterable<LiveEvent> {
18
+ return {
19
+ [Symbol.asyncIterator]() {
20
+ const queue: LiveEvent[] = [];
21
+ let notify: (() => void) | undefined;
22
+ let finished = false;
23
+ let unsub: (() => void) | undefined;
24
+ let started: Promise<void> | undefined;
25
+
26
+ const ensure = (): Promise<void> => {
27
+ if (started) return started;
28
+ const emit = (event: LiveEvent): void => {
29
+ queue.push(event);
30
+ notify?.();
31
+ };
32
+ const result = start(emit);
33
+ if (typeof result === "function") {
34
+ unsub = result;
35
+ started = Promise.resolve();
36
+ return started;
37
+ }
38
+ started = result.then((fn) => {
39
+ unsub = fn;
40
+ });
41
+ return started;
42
+ };
43
+
44
+ return {
45
+ async next() {
46
+ await ensure();
47
+ for (;;) {
48
+ const nextEvent = queue.shift();
49
+ if (nextEvent !== undefined) return { done: false, value: nextEvent };
50
+ if (finished) return { done: true, value: undefined };
51
+ await new Promise<void>((resolve) => {
52
+ notify = resolve;
53
+ });
54
+ }
55
+ },
56
+ async return() {
57
+ finished = true;
58
+ unsub?.();
59
+ notify?.();
60
+ return { done: true, value: undefined };
61
+ },
62
+ };
63
+ },
64
+ };
65
+ }
@@ -108,7 +108,8 @@ export async function openNatsSignal(options: SignalOpenOptions): Promise<Signal
108
108
  };
109
109
  },
110
110
  subscribe: (signal, subscriberId, handler) => outbox.subscribe(signal, subscriberId, handler),
111
- live: (signal, handler) => outbox.live(signal, handler),
111
+ live: (signal, opts) => outbox.live(signal, opts),
112
+ checkLiveResume: (signal, afterId) => outbox.checkLiveResume(signal, afterId),
112
113
  drain: () => outbox.drain(),
113
114
  deadLetters: (s) => outbox.deadLetters(s),
114
115
  inspect: (s) => outbox.inspect(s),