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
@@ -8,7 +8,8 @@
8
8
  import type { InferSchemaOutput } from "../validation/standard-schema.ts";
9
9
  import type { AnyFlowDef, FlowDef, FlowErrorMap } from "./flow.ts";
10
10
  import { isFlow } from "./flow.ts";
11
- import type { HttpTrigger, Trigger } from "./triggers.ts";
11
+ import type { HttpTrigger, LiveHttpTrigger, Trigger } from "./triggers.ts";
12
+ import { httpPathParams } from "./live-http.ts";
12
13
 
13
14
  /**
14
15
  * One flow's route contract on `app.$routes` (compatible with client
@@ -52,6 +53,10 @@ export type AppRouteMap = {
52
53
  readonly errors?: unknown;
53
54
  readonly method?: string;
54
55
  readonly path?: string;
56
+ readonly live?: string;
57
+ readonly matchKey?: readonly string[];
58
+ readonly gates?: readonly string[];
59
+ readonly stream?: true;
55
60
  };
56
61
  };
57
62
  };
@@ -73,6 +78,28 @@ export type ErrorDataOf<E extends FlowErrorMap> = {
73
78
  [K in keyof E]: InferSchemaOutput<E[K]>;
74
79
  };
75
80
 
81
+ /**
82
+ * HTTP live exposure on `$routes` so `createClient(app)` types
83
+ * `api.unit.flow(input, { onEvent })` as subscribe, not JSON RPC.
84
+ *
85
+ * @typeParam I - Input
86
+ * @typeParam O - Output
87
+ * @typeParam E - Error map
88
+ * @typeParam M - HTTP method
89
+ * @typeParam P - Path
90
+ */
91
+ export type LiveAppFlowRoute<
92
+ I = unknown,
93
+ O = unknown,
94
+ E extends Record<string, unknown> = Record<string, unknown>,
95
+ M extends string = string,
96
+ P extends string = string,
97
+ > = AppFlowRoute<I, O, E, M, P> & {
98
+ readonly live: string;
99
+ readonly stream: true;
100
+ readonly matchKey: readonly string[];
101
+ };
102
+
76
103
  /**
77
104
  * Derive a client route contract from a {@link FlowDef} (trigger → REST).
78
105
  *
@@ -80,9 +107,11 @@ export type ErrorDataOf<E extends FlowErrorMap> = {
80
107
  */
81
108
  export type RouteFromFlow<F> =
82
109
  F extends FlowDef<infer I, infer O, infer E, any, infer T>
83
- ? [T] extends [HttpTrigger<infer M, infer P>]
84
- ? AppFlowRoute<I, O, ErrorDataOf<E>, M, P>
85
- : AppFlowRoute<I, O, ErrorDataOf<E>>
110
+ ? [T] extends [LiveHttpTrigger<infer M, infer P>]
111
+ ? LiveAppFlowRoute<I, O, ErrorDataOf<E>, M, P>
112
+ : [T] extends [HttpTrigger<infer M, infer P>]
113
+ ? AppFlowRoute<I, O, ErrorDataOf<E>, M, P>
114
+ : AppFlowRoute<I, O, ErrorDataOf<E>>
86
115
  : never;
87
116
 
88
117
  /**
@@ -130,6 +159,14 @@ export type RoutesFromAdoptArgs<Args extends readonly unknown[]> = Args extends
130
159
  export interface RuntimeFlowRoute {
131
160
  readonly method?: string;
132
161
  readonly path?: string;
162
+ /** SSE live signal name when the trigger used `.live(signal)`. */
163
+ readonly live?: string;
164
+ /** Sorted auto-match path-param names (empty = firehose). */
165
+ readonly matchKey?: readonly string[];
166
+ /** Flattened gate names on the HTTP trigger. */
167
+ readonly gates?: readonly string[];
168
+ /** True when this flow returns `text/event-stream`. */
169
+ readonly stream?: true;
133
170
  }
134
171
 
135
172
  /** Runtime `$routes` table (values; types live on {@link OkeApp}). */
@@ -146,10 +183,21 @@ export type RuntimeRouteMap = {
146
183
  */
147
184
  export function runtimeRouteFromFlow(flowDef: AnyFlowDef): RuntimeFlowRoute {
148
185
  const trigger: Trigger | undefined = flowDef.$trigger ?? flowDef.triggers[0];
149
- if (trigger?.kind === "http") {
150
- return { method: trigger.method, path: trigger.path };
151
- }
152
- return {};
186
+ if (trigger?.kind !== "http") return {};
187
+ const liveName = trigger.liveSignal?.name ?? flowDef.live;
188
+ const gates = trigger.gates.map((g) => (typeof g === "string" ? g : g.name));
189
+ return {
190
+ method: trigger.method,
191
+ path: trigger.path,
192
+ ...(liveName !== undefined
193
+ ? {
194
+ live: liveName,
195
+ stream: true as const,
196
+ matchKey: httpPathParams(trigger.path),
197
+ ...(gates.length > 0 ? { gates } : {}),
198
+ }
199
+ : {}),
200
+ };
153
201
  }
154
202
 
155
203
  /**
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Host API-key durability — standalone `oke()` (no Console).
3
+ *
4
+ * Creates a key via `fx.auth.createApiKey`, stops, boots a fresh app against
5
+ * the same SQL URL, and verifies Bearer on the new `gate.auth.apiKeyStore`.
6
+ * Production uses postgres + `DATABASE_URL`; this suite uses file-backed
7
+ * PGlite at that same URL so the restart is hermetic.
8
+ */
9
+
10
+ import { afterEach, describe, expect, test } from "bun:test";
11
+ import { mkdtemp, rm } from "node:fs/promises";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+ import { AUTH_TABLES } from "../auth/tables.ts";
15
+ import { gate } from "../elements/gate.ts";
16
+ import { resetStores, store } from "../elements/store/declare.ts";
17
+ import { oke } from "./app.ts";
18
+ import { flow, resetFlowSeq } from "./flow.ts";
19
+ import { on, resetBindings } from "./on.ts";
20
+ import { http } from "./triggers.ts";
21
+
22
+ const PEPPER = "oke-host-api-key-persist-pepper";
23
+ const member = gate.policy("member", ({ auth }) => !!auth.verified);
24
+
25
+ function resetIsolated(): void {
26
+ resetBindings();
27
+ resetFlowSeq();
28
+ resetStores();
29
+ }
30
+
31
+ afterEach(() => {
32
+ resetIsolated();
33
+ });
34
+
35
+ describe("host API key persist via store.sql()", () => {
36
+ test("standalone oke() persists keys across restart; Bearer verifies on a fresh store", async () => {
37
+ const dir = await mkdtemp(join(tmpdir(), "oke-host-keys-"));
38
+ const sqlUrl = join(dir, "pgdata");
39
+ const prevDb = process.env.DATABASE_URL;
40
+ const prevPglite = process.env.OKE_PGLITE_URL;
41
+ process.env.DATABASE_URL = sqlUrl;
42
+ process.env.OKE_PGLITE_URL = sqlUrl;
43
+
44
+ try {
45
+ resetIsolated();
46
+ const db1 = store.sql("app", { schema: {} });
47
+ on(
48
+ http.post("/keys").gate(member),
49
+ flow("keys.create", {
50
+ effects: { writes: ["auth:api-keys"] },
51
+ do: (_input, fx) => fx.auth.createApiKey({ name: "prod-ci", scopes: ["member"] }),
52
+ }),
53
+ );
54
+ on(
55
+ http.get("/secure").gate(member),
56
+ flow("secure.ping", {
57
+ effects: {},
58
+ do: () => ({ ok: true }),
59
+ }),
60
+ );
61
+
62
+ const first = oke({
63
+ name: "host-keys-first",
64
+ stores: [db1],
65
+ gate: {
66
+ auth: { secret: PEPPER, http: false },
67
+ policies: [member],
68
+ },
69
+ env: "test",
70
+ startScheduler: false,
71
+ });
72
+ await first.boot({ env: "test", startScheduler: false });
73
+ expect(first.apiKeys?.persist).toBeTypeOf("function");
74
+
75
+ const created = await first.execute(
76
+ first.flow("keys.create")!,
77
+ {},
78
+ http.post("/keys").gate(member),
79
+ {
80
+ principal: {
81
+ plane: "user",
82
+ userId: "u_issuer",
83
+ scopes: new Set(["member"]),
84
+ verified: true,
85
+ },
86
+ },
87
+ );
88
+ expect(created.failure).toBeUndefined();
89
+ const secret = (created.output as { secret?: string } | undefined)?.secret;
90
+ expect(secret?.startsWith("oke_")).toBe(true);
91
+
92
+ const firstStore = first.bootResult?.store;
93
+ await first.stop();
94
+ await firstStore?.close();
95
+
96
+ resetIsolated();
97
+ const db2 = store.sql("app", { schema: {} });
98
+ on(
99
+ http.get("/secure").gate(member),
100
+ flow("secure.ping", {
101
+ effects: {},
102
+ do: () => ({ ok: true }),
103
+ }),
104
+ );
105
+ const second = oke({
106
+ name: "host-keys-second",
107
+ stores: [db2],
108
+ gate: {
109
+ auth: { secret: PEPPER, http: false },
110
+ policies: [member],
111
+ },
112
+ env: "test",
113
+ startScheduler: false,
114
+ });
115
+ await second.boot({ env: "test", startScheduler: false });
116
+ expect(second.apiKeys).not.toBe(first.apiKeys);
117
+ expect(second.apiKeys?.keys.size).toBeGreaterThan(0);
118
+
119
+ const conn = await second.bootResult?.store?.primarySql();
120
+ const rows = await conn?.query(`SELECT id, name FROM ${AUTH_TABLES.apiKeys}`);
121
+ expect(rows?.some((r) => r.name === "prod-ci")).toBe(true);
122
+
123
+ const res = await second.fetch(
124
+ new Request("http://localhost/secure", {
125
+ method: "GET",
126
+ headers: { authorization: `Bearer ${secret}` },
127
+ }),
128
+ );
129
+ expect(res.status).toBe(200);
130
+ expect(await res.json()).toEqual({ data: { ok: true }, error: null });
131
+
132
+ const secondStore = second.bootResult?.store;
133
+ await second.stop();
134
+ await secondStore?.close();
135
+ } finally {
136
+ if (prevDb === undefined) delete process.env.DATABASE_URL;
137
+ else process.env.DATABASE_URL = prevDb;
138
+ if (prevPglite === undefined) delete process.env.OKE_PGLITE_URL;
139
+ else process.env.OKE_PGLITE_URL = prevPglite;
140
+ await rm(dir, { recursive: true, force: true });
141
+ }
142
+ }, 20_000);
143
+ });
@@ -0,0 +1,122 @@
1
+ /**
2
+ * App-shell tenancy wiring — lazy chunk.
3
+ *
4
+ * One `w` dispatcher so Store-only `oke()` graphs do not spell Clock /
5
+ * pipeline tenant field names.
6
+ */
7
+
8
+ import type { ResolvedTenantAuth } from "../auth/tenant-config.ts";
9
+ import type { TenantStore } from "../auth/tenants.ts";
10
+ import type { ClockDecl } from "../elements/clock/declare.ts";
11
+ import { parsePerTenantCronName } from "./clock-per-tenant-name.ts";
12
+ import { orphanPerTenantCronRows, putPerTenantCronRows } from "./clock-reconcile.ts";
13
+ import type { AnyFlowDef } from "./flow.ts";
14
+ import type { PipelineTenantDeps } from "./pipeline.ts";
15
+
16
+ function cron(name: string): { readonly tenant: { readonly id: string } } | undefined {
17
+ const parsed = parsePerTenantCronName(name);
18
+ return parsed ? { tenant: { id: parsed.tenantId } } : undefined;
19
+ }
20
+
21
+ function boot(store: TenantStore): { readonly tenantIds: () => string[] } {
22
+ return { tenantIds: () => [...store.tenants.keys()] };
23
+ }
24
+
25
+ function clock(
26
+ tenantStore: TenantStore,
27
+ clockStore: Parameters<typeof putPerTenantCronRows>[0],
28
+ templates: readonly ClockDecl[],
29
+ ): void {
30
+ tenantStore.hooks = {
31
+ onCreate: (row) => putPerTenantCronRows(clockStore, templates, row.id),
32
+ onDelete: (row) => orphanPerTenantCronRows(clockStore, templates, row.id),
33
+ };
34
+ }
35
+
36
+ function resume(auth: { readonly tenant?: unknown; readonly tenantStore?: TenantStore }): {
37
+ readonly tenantEnabled: boolean;
38
+ readonly tenantStore: TenantStore | undefined;
39
+ } {
40
+ return {
41
+ tenantEnabled: auth.tenant !== undefined,
42
+ tenantStore: auth.tenantStore,
43
+ };
44
+ }
45
+
46
+ function pipe(
47
+ tenant: ResolvedTenantAuth,
48
+ store: TenantStore,
49
+ flowTenantScoped: boolean,
50
+ flowPlane: AnyFlowDef["plane"],
51
+ ): { readonly tenant: PipelineTenantDeps } {
52
+ return {
53
+ tenant: {
54
+ config: tenant,
55
+ store,
56
+ flowTenantScoped,
57
+ flowPlane,
58
+ },
59
+ };
60
+ }
61
+
62
+ function fx(args: {
63
+ readonly enabled: boolean;
64
+ readonly store: TenantStore | undefined;
65
+ readonly scoped: boolean;
66
+ readonly plane: AnyFlowDef["plane"];
67
+ readonly tenant: { readonly id: string | null } | undefined;
68
+ readonly bind?: { readonly secret: string; readonly now: () => number };
69
+ }): {
70
+ readonly tenantStore: TenantStore | undefined;
71
+ readonly tenantEnabled: boolean;
72
+ readonly flowTenantScoped: boolean;
73
+ readonly flowPlane: AnyFlowDef["plane"];
74
+ readonly sessionCrypto?: { readonly secret: string; readonly now: () => number };
75
+ readonly tenant?: { readonly id: string | null };
76
+ } {
77
+ return {
78
+ tenantStore: args.store,
79
+ tenantEnabled: args.enabled,
80
+ flowTenantScoped: args.scoped,
81
+ flowPlane: args.plane,
82
+ ...(args.bind ? { sessionCrypto: { secret: args.bind.secret, now: args.bind.now } } : {}),
83
+ ...(args.tenant ? { tenant: args.tenant } : {}),
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Dispatch tenancy wiring.
89
+ *
90
+ * 0 cron · 1 boot · 2 clock · 3 pipe · 4 fx · 5 resume
91
+ *
92
+ * @param kind - Operation
93
+ * @param args - Kind-specific arguments
94
+ */
95
+ export function w(kind: number, ...args: unknown[]): unknown {
96
+ switch (kind) {
97
+ case 0:
98
+ return cron(args[0] as string);
99
+ case 1:
100
+ return boot(args[0] as TenantStore);
101
+ case 2:
102
+ clock(
103
+ args[0] as TenantStore,
104
+ args[1] as Parameters<typeof putPerTenantCronRows>[0],
105
+ args[2] as readonly ClockDecl[],
106
+ );
107
+ return undefined;
108
+ case 3:
109
+ return pipe(
110
+ args[0] as ResolvedTenantAuth,
111
+ args[1] as TenantStore,
112
+ args[2] as boolean,
113
+ args[3] as AnyFlowDef["plane"],
114
+ );
115
+ case 4:
116
+ return fx(args[0] as Parameters<typeof fx>[0]);
117
+ case 5:
118
+ return resume(args[0] as Parameters<typeof resume>[0]);
119
+ default:
120
+ return undefined;
121
+ }
122
+ }