okengine 0.15.2 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/manifest.v1.schema.json +21 -2
  2. package/package.json +1 -1
  3. package/site/content/docs/elements/clock.mdx +7 -0
  4. package/site/content/docs/elements/flow.mdx +20 -12
  5. package/site/content/docs/elements/gate.mdx +108 -3
  6. package/site/content/docs/elements/signal.mdx +46 -17
  7. package/site/content/docs/elements/store.mdx +36 -32
  8. package/site/content/docs/elements/vault.mdx +26 -0
  9. package/site/content/docs/get-started/project-structure.mdx +4 -4
  10. package/site/content/docs/reference/cli.md +1 -1
  11. package/site/content/docs/reference/client.mdx +75 -17
  12. package/site/content/docs/reference/configuration.mdx +7 -4
  13. package/site/content/docs/reference/errors.mdx +24 -17
  14. package/site/content/docs/reference/fx.mdx +39 -22
  15. package/site/content/docs/reference/security.md +1 -1
  16. package/src/auth/api-key-sql.test.ts +47 -0
  17. package/src/auth/api-key-sql.ts +261 -0
  18. package/src/auth/api-keys.ts +276 -14
  19. package/src/auth/auth.test.ts +56 -0
  20. package/src/auth/config.ts +24 -5
  21. package/src/auth/index.ts +62 -1
  22. package/src/auth/plugin.ts +6 -1
  23. package/src/auth/schema.ts +8 -0
  24. package/src/auth/sessions.ts +18 -0
  25. package/src/auth/tables.ts +32 -0
  26. package/src/auth/tenant-config.ts +74 -0
  27. package/src/auth/tenant-tables.ts +11 -0
  28. package/src/auth/tenants.test.ts +63 -0
  29. package/src/auth/tenants.ts +360 -0
  30. package/src/cli/attach-host-console.test.ts +37 -0
  31. package/src/cli/attach-host-console.ts +6 -0
  32. package/src/cli/build.ts +2 -2
  33. package/src/client/budget.test.ts +1 -1
  34. package/src/client/create.ts +75 -5
  35. package/src/client/index.ts +13 -0
  36. package/src/client/live.test.ts +422 -0
  37. package/src/client/live.ts +389 -0
  38. package/src/client/notes-contract.test.ts +41 -0
  39. package/src/client/types.ts +85 -6
  40. package/src/client-react/index.ts +85 -1
  41. package/src/client-react/use-live.test.ts +129 -0
  42. package/src/compiler/effects-infer.ts +37 -2
  43. package/src/compiler/extract.test.ts +143 -11
  44. package/src/compiler/extract.ts +210 -30
  45. package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +1 -2
  46. package/src/compiler/fixtures/skyport.expected.json +2 -3
  47. package/src/compiler/response.ts +41 -11
  48. package/src/console/server/access.test.ts +116 -8
  49. package/src/console/server/access.ts +70 -23
  50. package/src/console/server/app.ts +45 -11
  51. package/src/console/server/console.test.ts +6 -8
  52. package/src/console/server/dev-identities.test.ts +127 -0
  53. package/src/console/server/dev-identities.ts +229 -0
  54. package/src/console/server/flows-invoke.test.ts +21 -4
  55. package/src/console/server/flows.ts +113 -58
  56. package/src/console/server/gates.test.ts +24 -0
  57. package/src/console/server/gates.ts +10 -30
  58. package/src/console/server/index.ts +1 -0
  59. package/src/console/server/live.test.ts +26 -1
  60. package/src/console/server/live.ts +4 -2
  61. package/src/console/server/operator-db.ts +2 -0
  62. package/src/console/server/state.ts +29 -55
  63. package/src/console/server/store.test.ts +17 -0
  64. package/src/console/server/store.ts +43 -1
  65. package/src/console/ui-next/dist/assets/access-page-CXVWWMmD.js +4 -0
  66. package/src/console/ui-next/dist/assets/{agent-disclosure-C0X1fbWF.js → agent-disclosure-BP0Y0Sux.js} +1 -1
  67. package/src/console/ui-next/dist/assets/cache-glyph-B5X-NM0-.js +1 -0
  68. package/src/console/ui-next/dist/assets/call-pii-button-ChAUiLo9.js +1 -0
  69. package/src/console/ui-next/dist/assets/collapsible-DGnOM2ph.js +1 -0
  70. package/src/console/ui-next/dist/assets/copy-inline-button-DBHhgHKP.js +1 -0
  71. package/src/console/ui-next/dist/assets/detail-header-DhHM1iaZ.js +1 -0
  72. package/src/console/ui-next/dist/assets/dropdown-menu-_NjTEo5_.js +1 -0
  73. package/src/console/ui-next/dist/assets/element-icons-BVXtRyd3.js +1 -0
  74. package/src/console/ui-next/dist/assets/explorer-empty-2uIhBu0_.js +1 -0
  75. package/src/console/ui-next/dist/assets/flows-page-XDpAJO8f.js +1 -0
  76. package/src/console/ui-next/dist/assets/{highlighted-json-CS_O8L-r.js → highlighted-json-Awq7gYdu.js} +1 -1
  77. package/src/console/ui-next/dist/assets/http-method-_UHM2ODJ.js +1 -0
  78. package/src/console/ui-next/dist/assets/index-BBj2QJCu.js +66 -0
  79. package/src/console/ui-next/dist/assets/index-Ck88Jmv8.css +2 -0
  80. package/src/console/ui-next/dist/assets/link-BX6Vqztd.js +1 -0
  81. package/src/console/ui-next/dist/assets/observability-page-BFaay44m.js +4 -0
  82. package/src/console/ui-next/dist/assets/replica-lag-DKRrbvdo.js +18 -0
  83. package/src/console/ui-next/dist/assets/request-meta-DV0ywz7t.js +1 -0
  84. package/src/console/ui-next/dist/assets/shortcut-JQIZlWfm.js +1 -0
  85. package/src/console/ui-next/dist/assets/shortcut-keys-DKxNTe_m.js +1 -0
  86. package/src/console/ui-next/dist/assets/skeleton-D-czQJT6.js +1 -0
  87. package/src/console/ui-next/dist/assets/store-page-CS5-aETQ.js +41 -0
  88. package/src/console/ui-next/dist/assets/trace-detail-sheet-B09O8rA6.js +2 -0
  89. package/src/console/ui-next/dist/assets/tree-expand-toggle-BtyhmWb4.js +55 -0
  90. package/src/console/ui-next/dist/assets/units-page-Ca2Z-E52.js +1 -0
  91. package/src/console/ui-next/dist/assets/{use-vault-list-BLbZhzyF.js → use-vault-list-uk4WVboC.js} +1 -1
  92. package/src/console/ui-next/dist/assets/useMutation-B8EO02Ej.js +1 -0
  93. package/src/console/ui-next/dist/assets/useRender-BE2A9BWC.js +1 -0
  94. package/src/console/ui-next/dist/assets/vault-page-BmOeAFwg.js +2 -0
  95. package/src/console/ui-next/dist/index.html +11 -9
  96. package/src/console/ui-next/index.html +3 -3
  97. package/src/console/ui-next/src/client.ts +167 -0
  98. package/src/console/ui-next/src/components/shell/app-sidebar.tsx +2 -0
  99. package/src/console/ui-next/src/components/shell/command-items.test.ts +1 -0
  100. package/src/console/ui-next/src/components/shell/command-items.ts +8 -0
  101. package/src/console/ui-next/src/components/shell/shell-layout.tsx +2 -1
  102. package/src/console/ui-next/src/components/ui/sheet-form.tsx +8 -3
  103. package/src/console/ui-next/src/features/access/access-page.tsx +347 -0
  104. package/src/console/ui-next/src/features/access/data/use-access.ts +23 -0
  105. package/src/console/ui-next/src/features/access/detail/access-detail.tsx +552 -0
  106. package/src/console/ui-next/src/features/access/explorer/access-list.tsx +125 -0
  107. package/src/console/ui-next/src/features/access/lib/format-when.test.ts +139 -0
  108. package/src/console/ui-next/src/features/access/lib/format-when.ts +296 -0
  109. package/src/console/ui-next/src/features/access/lib/scope-groups.test.ts +34 -0
  110. package/src/console/ui-next/src/features/access/lib/scope-groups.ts +40 -0
  111. package/src/console/ui-next/src/features/access/sheets/access-allow-fields.tsx +236 -0
  112. package/src/console/ui-next/src/features/access/sheets/access-create-sheet.tsx +316 -0
  113. package/src/console/ui-next/src/features/access/sheets/access-edit-sheet.tsx +240 -0
  114. package/src/console/ui-next/src/features/access/sheets/access-expiry-fields.tsx +66 -0
  115. package/src/console/ui-next/src/features/access/sheets/access-refresh-sheet.tsx +104 -0
  116. package/src/console/ui-next/src/features/access/sheets/access-scope-field.tsx +115 -0
  117. package/src/console/ui-next/src/features/access/sheets/access-secret-sheet.tsx +89 -0
  118. package/src/console/ui-next/src/features/access/sheets/access-sheet-search.tsx +49 -0
  119. package/src/console/ui-next/src/features/access/state/access-selection.ts +84 -0
  120. package/src/console/ui-next/src/features/auth/auth-redirect.test.ts +5 -0
  121. package/src/console/ui-next/src/features/auth/auth-redirect.ts +18 -2
  122. package/src/console/ui-next/src/features/flows/fixture.ts +0 -1
  123. package/src/console/ui-next/src/features/not-found/not-found-page.tsx +1 -1
  124. package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +5 -1
  125. package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +15 -4
  126. package/src/console/ui-next/src/lib/document-meta.test.ts +1 -0
  127. package/src/console/ui-next/src/lib/document-meta.ts +2 -1
  128. package/src/console/ui-next/src/lib/last-module-search.test.ts +1 -0
  129. package/src/console/ui-next/src/lib/last-module-search.ts +2 -1
  130. package/src/console/ui-next/src/lib/shortcut.test.ts +2 -0
  131. package/src/console/ui-next/src/lib/shortcut.ts +3 -0
  132. package/src/console/ui-next/src/router.tsx +10 -1
  133. package/src/console/ui-next/ui-next-seed-app-schema.ts +88 -37
  134. package/src/console/ui-next/ui-next-seed-manifest-surface.ts +2 -9
  135. package/src/console/ui-next/ui-next-seed-manifest.ts +0 -1
  136. package/src/console/ui-next/ui-next-seed-store.ts +43 -15
  137. package/src/console/xss.gate.test.ts +1 -0
  138. package/src/drivers/index.ts +2 -0
  139. package/src/drivers/journal-postgres.ts +12 -3
  140. package/src/drivers/pg-rls.ts +26 -2
  141. package/src/drivers/pg-vault-rls.ts +68 -0
  142. package/src/drivers/signal-engine.ts +69 -15
  143. package/src/drivers/signal-live-iter.ts +65 -0
  144. package/src/drivers/signal-nats.ts +2 -1
  145. package/src/drivers/signal-postgres.ts +95 -12
  146. package/src/drivers/signal-redis.ts +2 -1
  147. package/src/drivers/signal-retention.ts +64 -0
  148. package/src/drivers/signal-types.ts +35 -5
  149. package/src/elements/clock/declare.ts +64 -2
  150. package/src/elements/clock/reconcile.ts +98 -25
  151. package/src/elements/clock/runtime.ts +13 -2
  152. package/src/elements/clock.test.ts +28 -0
  153. package/src/elements/clock.ts +9 -1
  154. package/src/elements/gate/declare.ts +2 -0
  155. package/src/elements/gate/permissions.ts +12 -0
  156. package/src/elements/gate.ts +6 -1
  157. package/src/elements/signal/declare.ts +44 -10
  158. package/src/elements/signal/delivery-modes.test.ts +90 -4
  159. package/src/elements/signal/order-lifecycle.test.ts +20 -4
  160. package/src/elements/signal/runtime.ts +42 -0
  161. package/src/elements/signal.test.ts +21 -8
  162. package/src/elements/signal.ts +1 -1
  163. package/src/elements/store/cache.ts +1 -1
  164. package/src/elements/store/declare.ts +7 -0
  165. package/src/elements/store/rls-identity.test.ts +29 -0
  166. package/src/elements/store/rls-identity.ts +3 -0
  167. package/src/elements/store/runtime.ts +14 -0
  168. package/src/elements/store/schema-decl.ts +162 -44
  169. package/src/elements/store/schema-tenant.ts +41 -0
  170. package/src/elements/store/sql-rls-isolation.test.ts +39 -0
  171. package/src/elements/store/sql-select.types.test.ts +71 -0
  172. package/src/elements/store/sql-session.ts +54 -32
  173. package/src/elements/store.ts +5 -0
  174. package/src/elements/vault/builtin-adapter.ts +15 -2
  175. package/src/elements/vault/declare.ts +8 -0
  176. package/src/elements/vault/runtime.ts +7 -0
  177. package/src/elements/vault/sql-rls-isolation.test.ts +145 -0
  178. package/src/elements/vault/storage.ts +9 -0
  179. package/src/elements/vault/test-helpers.ts +2 -1
  180. package/src/i18n/catalogs/ar.ts +19 -0
  181. package/src/i18n/catalogs/en.ts +19 -0
  182. package/src/index.ts +2 -0
  183. package/src/kernel/adopt-routes.ts +56 -8
  184. package/src/kernel/api-key-host-persist.test.ts +143 -0
  185. package/src/kernel/app-tenant.ts +122 -0
  186. package/src/kernel/app.ts +196 -57
  187. package/src/kernel/auth-resolve.ts +13 -5
  188. package/src/kernel/boot-bind/clock.ts +9 -3
  189. package/src/kernel/boot.test.ts +4 -4
  190. package/src/kernel/boot.ts +2 -0
  191. package/src/kernel/budget.test.ts +1 -1
  192. package/src/kernel/clock-durable.ts +8 -0
  193. package/src/kernel/clock-per-tenant-name.ts +5 -0
  194. package/src/kernel/clock-reconcile.ts +8 -0
  195. package/src/kernel/errors-live-resume.ts +15 -0
  196. package/src/kernel/errors-tenant.ts +29 -0
  197. package/src/kernel/errors.registry.test.ts +31 -3
  198. package/src/kernel/errors.ts +43 -3
  199. package/src/kernel/flow.ts +26 -5
  200. package/src/kernel/fx-auth-keys.test.ts +88 -0
  201. package/src/kernel/fx-auth-keys.ts +203 -0
  202. package/src/kernel/fx-auth-tenants.test.ts +87 -0
  203. package/src/kernel/fx-auth-tenants.ts +286 -0
  204. package/src/kernel/fx-live-stream.ts +149 -0
  205. package/src/kernel/fx-live.test.ts +157 -0
  206. package/src/kernel/fx-runtime.ts +15 -0
  207. package/src/kernel/fx-tenant-store.ts +213 -0
  208. package/src/kernel/fx.test.ts +108 -1
  209. package/src/kernel/fx.ts +206 -28
  210. package/src/kernel/hooks.ts +2 -2
  211. package/src/kernel/http-resource.ts +9 -18
  212. package/src/kernel/index.ts +2 -0
  213. package/src/kernel/journal.ts +12 -0
  214. package/src/kernel/live-http.test.ts +78 -0
  215. package/src/kernel/live-http.ts +114 -0
  216. package/src/kernel/live-resume.test.ts +125 -0
  217. package/src/kernel/on.ts +51 -0
  218. package/src/kernel/pipeline-api-key.test.ts +192 -7
  219. package/src/kernel/pipeline-tenant.ts +49 -0
  220. package/src/kernel/pipeline.test.ts +1 -1
  221. package/src/kernel/pipeline.ts +41 -4
  222. package/src/kernel/resource-mount.test.ts +10 -27
  223. package/src/kernel/tenant-resolve.test.ts +101 -0
  224. package/src/kernel/tenant-resolve.ts +124 -0
  225. package/src/kernel/tenant-roles.test.ts +87 -0
  226. package/src/kernel/triggers.ts +59 -21
  227. package/src/manifest/diff.test.ts +11 -2
  228. package/src/manifest/diff.ts +53 -11
  229. package/src/manifest/fixtures/skyport.excerpt.json +1 -1
  230. package/src/manifest/fixtures/skyport.manifest.json +0 -1
  231. package/src/manifest/types.ts +58 -6
  232. package/src/release/build-lib.ts +14 -1
  233. package/src/release/limits.ts +2 -2
  234. package/src/release/measure.ts +55 -1
  235. package/src/client/live-gap.test.ts +0 -35
  236. package/src/console/ui-next/dist/assets/cache-glyph-F1FI122b.js +0 -1
  237. package/src/console/ui-next/dist/assets/call-pii-button-DEDCl_j3.js +0 -1
  238. package/src/console/ui-next/dist/assets/collapsible-LPqGvfoz.js +0 -1
  239. package/src/console/ui-next/dist/assets/confirm-sheet-3ptDzXbz.js +0 -1
  240. package/src/console/ui-next/dist/assets/explorer-empty-BvYrygyO.js +0 -1
  241. package/src/console/ui-next/dist/assets/flows-page-Dluo1Bd7.js +0 -1
  242. package/src/console/ui-next/dist/assets/http-method--sWDdXSB.js +0 -1
  243. package/src/console/ui-next/dist/assets/index-Ca3HZMVq.js +0 -66
  244. package/src/console/ui-next/dist/assets/index-UtTDRQpS.css +0 -2
  245. package/src/console/ui-next/dist/assets/link-COeyggt-.js +0 -1
  246. package/src/console/ui-next/dist/assets/observability-page-HK9-BO8z.js +0 -4
  247. package/src/console/ui-next/dist/assets/replica-lag-DBIFFf7d.js +0 -18
  248. package/src/console/ui-next/dist/assets/sheet-form-Dnwa5oOR.js +0 -1
  249. package/src/console/ui-next/dist/assets/shortcut-Cgzuv4k1.js +0 -1
  250. package/src/console/ui-next/dist/assets/shortcut-keys-CoqwEIi0.js +0 -1
  251. package/src/console/ui-next/dist/assets/skeleton-BHcfDYcb.js +0 -1
  252. package/src/console/ui-next/dist/assets/store-page-BSohH3wM.js +0 -41
  253. package/src/console/ui-next/dist/assets/trace-detail-sheet-CFIGRnnA.js +0 -2
  254. package/src/console/ui-next/dist/assets/tree-expand-toggle-BgLl34w7.js +0 -54
  255. package/src/console/ui-next/dist/assets/units-page-B0cFE76A.js +0 -1
  256. package/src/console/ui-next/dist/assets/vault-page-B1SbYe10.js +0 -2
package/src/kernel/app.ts CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  type RoutesFromNamespace,
18
18
  type RuntimeRouteMap,
19
19
  } from "./adopt-routes.ts";
20
+ import { liveExposureKey, liveGatesKey, liveMatchKeyFromPath } from "./live-http.ts";
20
21
  // `./boot.ts` pulls in every element + driver module (vault, store, signal,
21
22
  // clock, gate, channel, ai, runs) — a type-only import here keeps that whole
22
23
  // graph out of the cold-start path; `doBoot` below loads it lazily, only
@@ -24,6 +25,7 @@ import {
24
25
  import type { BootOptions, BootResult, ElementRuntimes } from "./boot.ts";
25
26
  import type { CapabilityToken } from "./capability.ts";
26
27
  import type { AppAuthBinding } from "./auth-resolve.ts";
28
+ import type { ApiKeyStore } from "../auth/api-keys.ts";
27
29
  import type { AuthHttpMaterialization } from "../auth/bindings.ts";
28
30
  import type { WiredGateAuth } from "./app-auth.ts";
29
31
  import {
@@ -32,25 +34,24 @@ import {
32
34
  type ResolvedGateConfig,
33
35
  } from "../elements/gate/config.ts";
34
36
  import { requirePackageModule } from "../shared/lazy-src.ts";
37
+ import { lazyRequire } from "./lazy-require.ts";
38
+ import type { DurableResult, RunDurableOptions } from "../elements/clock/durable.ts";
35
39
  import type { TemplateCatalog } from "../elements/channel/runtime.ts";
36
40
  import { parseAcceptLanguage } from "../elements/channel/locale.ts";
37
41
  import { runWithLocale } from "../i18n/locale-context.ts";
38
- import { runDurable } from "../elements/clock/durable.ts";
39
42
  import { isFlow, type AnyFlowDef } from "./flow.ts";
40
43
  import { failureFromUnknown, runCompensationPhase } from "./compensate.ts";
41
44
  import { withAbortSignal } from "./abort-scope.ts";
42
45
  import { fxRetry } from "./concurrency.ts";
43
- import {
44
- createFxContext,
45
- freezePrincipal,
46
- isJsonStreamResult,
47
- resolveName,
48
- type CreateFxOptions,
49
- type Fx,
50
- type FxAuth,
51
- type FxOperator,
52
- type FxPrincipal,
53
- type NamedRef,
46
+ import type {
47
+ CreateFxOptions,
48
+ Fx,
49
+ FxAuth,
50
+ FxContext,
51
+ FxOperator,
52
+ FxPrincipal,
53
+ JsonStreamResult,
54
+ NamedRef,
54
55
  } from "./fx.ts";
55
56
  import {
56
57
  currentDevSurface,
@@ -151,6 +152,31 @@ function loadMessages(): typeof import("../i18n/messages.ts") {
151
152
  return requirePackageModule("i18n/messages", "messages");
152
153
  }
153
154
 
155
+ /**
156
+ * `createFx` / JSON result helpers — loaded on first execute, not on Store-only
157
+ * `oke()` construction (same boundary as `boot.ts`).
158
+ */
159
+ function loadFx(): {
160
+ createFxContext: (options: CreateFxOptions) => FxContext;
161
+ freezePrincipal: (p: FxPrincipal) => FxPrincipal;
162
+ isJsonStreamResult: (value: unknown) => value is JsonStreamResult;
163
+ resolveName: (ref: NamedRef) => string;
164
+ } {
165
+ return lazyRequire(import.meta.dir, ["fx", "runtime"].join("-"));
166
+ }
167
+
168
+ /** App-shell tenancy wiring — loaded only when `gate.auth.tenant` is on. */
169
+ function loadAppTenant(): { w: (kind: number, ...args: unknown[]) => unknown } {
170
+ return lazyRequire(import.meta.dir, ["app", "tenant"].join("-"));
171
+ }
172
+
173
+ /** Durable runner — statically imports `createFx`; keep it off Store-only `oke()`. */
174
+ function loadDurable(): {
175
+ runDurable: (opts: RunDurableOptions) => Promise<DurableResult>;
176
+ } {
177
+ return lazyRequire(import.meta.dir, ["clock", "durable"].join("-"));
178
+ }
179
+
154
180
  /** Options for {@link oke}. */
155
181
  export interface OkeOptions {
156
182
  /** Application name (Manifest `app`). */
@@ -371,6 +397,8 @@ export interface OkeApp<D extends Record<string, unknown> = {}, R extends AppRou
371
397
  * Builtin auth binding after boot (session store + secret), when configured.
372
398
  */
373
399
  readonly authBinding: AppAuthBinding | undefined;
400
+ /** Shared API key store when `gate.auth` is enabled. */
401
+ readonly apiKeys: ApiKeyStore | undefined;
374
402
  /**
375
403
  * Attach a plugin app-wide. Scope is the attachment point.
376
404
  * Types accumulate — decorations are visible on downstream handlers.
@@ -462,6 +490,8 @@ export interface OkeApp<D extends Record<string, unknown> = {}, R extends AppRou
462
490
  readonly parentId?: string;
463
491
  /** Explicit run / WideEvent id (defaults to a new UUID). */
464
492
  readonly runId?: string;
493
+ /** Tenant identity for cron / `fx.call` (propagated, unlike auth). */
494
+ readonly tenant?: { readonly id: string | null };
465
495
  },
466
496
  ): Promise<ExecuteResult>;
467
497
  /**
@@ -495,8 +525,12 @@ export interface OkeApp<D extends Record<string, unknown> = {}, R extends AppRou
495
525
  * Invoke all flows bound to an `every` interval.
496
526
  *
497
527
  * @param interval - Interval string (e.g. `"1h"`)
528
+ * @param extras - Optional tenant / parent for per-tenant clocks
498
529
  */
499
- dispatchEvery(interval: string): Promise<ExecuteResult[]>;
530
+ dispatchEvery(
531
+ interval: string,
532
+ extras?: { readonly tenant?: { readonly id: string | null } },
533
+ ): Promise<ExecuteResult[]>;
500
534
  /**
501
535
  * Invoke all flows bound to a CDC table change.
502
536
  *
@@ -607,6 +641,57 @@ function assertHttpBindingReady(binding: Binding): void {
607
641
  }
608
642
  }
609
643
 
644
+ /**
645
+ * Register one HTTP binding: unique method+path, unique live exposure key.
646
+ *
647
+ * @param binding - HTTP binding
648
+ * @param seenHttpRoutes - `METHOD path` keys
649
+ * @param seenLiveExposures - `(signal, gates, match)` keys
650
+ * @param smart - Router
651
+ * @param compiled - Compiled route map
652
+ * @param aot - AoT compile flag
653
+ */
654
+ function registerHttpRoute(
655
+ binding: Binding,
656
+ seenHttpRoutes: Set<string>,
657
+ seenLiveExposures: Map<string, string>,
658
+ smart: { add(method: string, path: string, value: Binding): void },
659
+ compiled: WeakMap<Binding, CompiledRoute>,
660
+ aot: boolean,
661
+ ): void {
662
+ const trigger = binding.trigger;
663
+ if (trigger.kind !== "http") return;
664
+ const routeKey = `${trigger.method} ${trigger.path}`;
665
+ if (seenHttpRoutes.has(routeKey)) {
666
+ throwOke("HTTP_ROUTE_DUPLICATE", {
667
+ method: trigger.method,
668
+ path: trigger.path,
669
+ flow: binding.flow.name || "(unnamed)",
670
+ });
671
+ }
672
+ seenHttpRoutes.add(routeKey);
673
+ if (trigger.liveSignal !== undefined) {
674
+ const matchKey = binding.flow.liveCustomMatch
675
+ ? `custom:${binding.flow.name}`
676
+ : liveMatchKeyFromPath(trigger.path);
677
+ const exposure = liveExposureKey(
678
+ trigger.liveSignal.name,
679
+ liveGatesKey(trigger.gates),
680
+ matchKey,
681
+ );
682
+ if (seenLiveExposures.has(exposure)) {
683
+ throwOke("LIVE_EXPOSURE_DUPLICATE", {
684
+ signal: trigger.liveSignal.name,
685
+ gates: liveGatesKey(trigger.gates) || "(none)",
686
+ match: matchKey || "(firehose)",
687
+ });
688
+ }
689
+ seenLiveExposures.set(exposure, binding.flow.name);
690
+ }
691
+ smart.add(trigger.method, trigger.path, binding);
692
+ compiled.set(binding, compileHttpBinding(binding, aot));
693
+ }
694
+
610
695
  /**
611
696
  * Fold `generated.ts` units into `$routes` and `flowsByName`.
612
697
  *
@@ -787,20 +872,11 @@ export function oke(options: OkeOptions): OkeApp {
787
872
 
788
873
  const smart = createRouter<Binding>(options.router ?? "default");
789
874
  const seenHttpRoutes = new Set<string>();
875
+ const seenLiveExposures = new Map<string, string>();
790
876
  for (const b of adopted) {
791
877
  if (b.trigger.kind === "http") {
792
878
  assertHttpBindingReady(b);
793
- const key = `${b.trigger.method} ${b.trigger.path}`;
794
- if (seenHttpRoutes.has(key)) {
795
- throwOke("HTTP_ROUTE_DUPLICATE", {
796
- method: b.trigger.method,
797
- path: b.trigger.path,
798
- flow: b.flow.name || "(unnamed)",
799
- });
800
- }
801
- seenHttpRoutes.add(key);
802
- smart.add(b.trigger.method, b.trigger.path, b);
803
- compiled.set(b, compileHttpBinding(b, aot));
879
+ registerHttpRoute(b, seenHttpRoutes, seenLiveExposures, smart, compiled, aot);
804
880
  }
805
881
  }
806
882
  // Defer SmartRouter selection until first match so `.plug()` can still
@@ -812,17 +888,7 @@ export function oke(options: OkeOptions): OkeApp {
812
888
  registerFlow(b.flow);
813
889
  if (b.trigger.kind === "http") {
814
890
  assertHttpBindingReady(b);
815
- const key = `${b.trigger.method} ${b.trigger.path}`;
816
- if (seenHttpRoutes.has(key)) {
817
- throwOke("HTTP_ROUTE_DUPLICATE", {
818
- method: b.trigger.method,
819
- path: b.trigger.path,
820
- flow: b.flow.name || "(unnamed)",
821
- });
822
- }
823
- seenHttpRoutes.add(key);
824
- smart.add(b.trigger.method, b.trigger.path, b);
825
- compiled.set(b, compileHttpBinding(b, aot));
891
+ registerHttpRoute(b, seenHttpRoutes, seenLiveExposures, smart, compiled, aot);
826
892
  }
827
893
  }
828
894
 
@@ -941,12 +1007,15 @@ export function oke(options: OkeOptions): OkeApp {
941
1007
  }
942
1008
 
943
1009
  async function handleCronFire(name: string): Promise<void> {
944
- await app.dispatchEvery(name);
1010
+ const extras = gateConfig.auth?.tenant
1011
+ ? (loadAppTenant().w(0, name) as { readonly tenant: { readonly id: string } } | undefined)
1012
+ : undefined;
1013
+ await app.dispatchEvery(name, extras);
945
1014
  // Named clocks (e.g. `clock("expire-stale", { every: "1h" })`) reconcile
946
1015
  // to a store row whose effective interval may differ from the cron name.
947
1016
  const row = await bootResult?.clock?.store.get(name);
948
1017
  if (row?.effectiveEvery && row.effectiveEvery !== name) {
949
- await app.dispatchEvery(row.effectiveEvery);
1018
+ await app.dispatchEvery(row.effectiveEvery, extras);
950
1019
  }
951
1020
  }
952
1021
 
@@ -1001,15 +1070,20 @@ export function oke(options: OkeOptions): OkeApp {
1001
1070
  return;
1002
1071
  }
1003
1072
  const { store, instanceId, leaseMs } = activeJournal();
1073
+ const existing = await store.get(runId);
1004
1074
  try {
1005
- await runDurable({
1075
+ await loadDurable().runDurable({
1006
1076
  flow: flowDef,
1007
1077
  input,
1008
1078
  journalStore: store,
1009
1079
  runId,
1010
1080
  ...(hasJournalLease(store) ? { lease: { instanceId, leaseMs } } : {}),
1011
1081
  now,
1012
- fx: durableResumeFx(),
1082
+ fx: {
1083
+ ...durableResumeFx(),
1084
+ ...(gateConfig.auth?.tenant ? (loadAppTenant().w(5, gateConfig.auth) as object) : {}),
1085
+ ...(existing?.tenant ? { tenant: { id: existing.tenant } } : {}),
1086
+ },
1013
1087
  });
1014
1088
  } catch (err) {
1015
1089
  if (isJournalLeaseBusy(err)) return;
@@ -1173,9 +1247,19 @@ export function oke(options: OkeOptions): OkeApp {
1173
1247
  onCronFire: overrides?.onCronFire ?? handleCronFire,
1174
1248
  onSignal: overrides?.onSignal ?? handleSignalFire,
1175
1249
  onDurableResume: overrides?.onDurableResume ?? handleDurableResume,
1250
+ ...(gateConfig.auth?.tenantStore
1251
+ ? (loadAppTenant().w(1, gateConfig.auth.tenantStore) as object)
1252
+ : {}),
1176
1253
  };
1177
1254
  const result = await bootApplication(merged);
1178
1255
  bootResult = result;
1256
+ if (gateConfig.auth?.tenantStore && result.clock) {
1257
+ loadAppTenant().w(2, gateConfig.auth.tenantStore, result.clock.store, merged.clocks ?? []);
1258
+ }
1259
+ if (gateConfig.auth?.apiKeyStore && result.store) {
1260
+ const { bindHostApiKeySqlFromStore } = await import("../auth/api-key-sql.ts");
1261
+ await bindHostApiKeySqlFromStore(result.store, gateConfig.auth.apiKeyStore);
1262
+ }
1179
1263
  // otp() provider / app mode capability — fail loud at boot, never silent downgrade.
1180
1264
  if (result.channel) {
1181
1265
  const { assertOtpPluginCapability } = await import("../auth/otp-capability.ts");
@@ -1259,6 +1343,8 @@ export function oke(options: OkeOptions): OkeApp {
1259
1343
  readonly parentId?: string;
1260
1344
  /** Explicit run / WideEvent id (defaults to a new UUID). */
1261
1345
  readonly runId?: string;
1346
+ /** Tenant identity for cron / `fx.call` (propagated, unlike auth). */
1347
+ readonly tenant?: { readonly id: string | null };
1262
1348
  },
1263
1349
  ): Promise<ExecuteResult> {
1264
1350
  registerFlow(flowDef);
@@ -1283,8 +1369,8 @@ export function oke(options: OkeOptions): OkeApp {
1283
1369
  resolvedLocale,
1284
1370
  defaultLocale,
1285
1371
  });
1286
- const signal = extras?.request?.signal;
1287
- return signal ? withAbortSignal(signal, run) : run();
1372
+ const abort = extras?.request?.signal;
1373
+ return abort ? withAbortSignal(abort, run) : run();
1288
1374
  });
1289
1375
  }
1290
1376
 
@@ -1307,12 +1393,15 @@ export function oke(options: OkeOptions): OkeApp {
1307
1393
  readonly locale?: string;
1308
1394
  readonly parentId?: string;
1309
1395
  readonly runId?: string;
1396
+ readonly tenant?: { readonly id: string | null };
1310
1397
  };
1311
1398
  readonly resolvedLocale: string;
1312
1399
  readonly defaultLocale: string;
1313
1400
  }): Promise<ExecuteResult> {
1314
1401
  const { flowDef, input, trigger, extras, resolvedLocale, defaultLocale } = args;
1315
1402
  const booted = await ensureBoot();
1403
+ const tenantEnabled = gateConfig.auth?.tenant !== undefined;
1404
+ const flowTenantScoped = flowDef.tenantScoped ?? tenantEnabled;
1316
1405
 
1317
1406
  const unitBag = flowDef.unit !== undefined ? unitHooks.get(flowDef.unit) : undefined;
1318
1407
  // app (hooks + plugs) → unit (hooks + plugs) → flow (hooks + plugs)
@@ -1365,8 +1454,10 @@ export function oke(options: OkeOptions): OkeApp {
1365
1454
  userId: options.fx?.auth?.userId ?? null,
1366
1455
  scopes: new Set(options.fx?.auth?.scopes ?? []),
1367
1456
  verified: options.fx?.auth?.verified,
1457
+ apiKeyId: options.fx?.auth?.apiKeyId,
1368
1458
  },
1369
1459
  operator: { id: options.fx?.operator?.id ?? null },
1460
+ tenant: { id: extras?.tenant?.id ?? options.fx?.tenant?.id ?? null },
1370
1461
  };
1371
1462
 
1372
1463
  // Principal injection: test harness (`env: "test"`) OR console-trusted
@@ -1383,6 +1474,8 @@ export function oke(options: OkeOptions): OkeApp {
1383
1474
  const cookieOpts = gateConfig.auth?.cookies;
1384
1475
  const cookieToken = wiredAuth?.tokenFromCookieHeader;
1385
1476
  const apiKeyStore = gateConfig.auth?.apiKeyStore;
1477
+ const tenantEnabled = gateConfig.auth?.tenant !== undefined;
1478
+ const flowTenantScoped = flowDef.tenantScoped ?? tenantEnabled;
1386
1479
  const elementHooks = createElementPipelineHooks({
1387
1480
  gates: booted.gate,
1388
1481
  principals,
@@ -1393,7 +1486,8 @@ export function oke(options: OkeOptions): OkeApp {
1393
1486
  : {}),
1394
1487
  verifyBearer:
1395
1488
  binding && wiredAuth
1396
- ? async (token) => wiredAuth.verifyBearerOrApiKey(binding, token, apiKeyStore)
1489
+ ? (token, request) =>
1490
+ wiredAuth.verifyBearerOrApiKey(binding, token, apiKeyStore, request)
1397
1491
  : undefined,
1398
1492
  // Phase 1a: opt-in cookie → Bearer when Authorization is absent.
1399
1493
  resolveToken:
@@ -1406,13 +1500,29 @@ export function oke(options: OkeOptions): OkeApp {
1406
1500
  return cookieToken(request.headers.get("cookie"), cookieOpts);
1407
1501
  }
1408
1502
  : undefined,
1503
+ ...(gateConfig.auth?.tenant && gateConfig.auth.tenantStore
1504
+ ? (loadAppTenant().w(
1505
+ 3,
1506
+ gateConfig.auth.tenant,
1507
+ gateConfig.auth.tenantStore,
1508
+ flowTenantScoped,
1509
+ flowDef.plane,
1510
+ ) as object)
1511
+ : {}),
1409
1512
  });
1410
1513
 
1411
1514
  // Element hooks run first in the app-level onAuth/beforeHandle chain —
1412
1515
  // principal resolution and gate checks precede user-registered hooks.
1413
1516
  hooks = mergeHooks(
1414
1517
  {
1415
- onAuth: [elementHooks.onAuth],
1518
+ onAuth: [
1519
+ elementHooks.onAuth,
1520
+ async () => {
1521
+ if (journalSession && principals?.tenant.id) {
1522
+ await journalSession.stampTenant(principals.tenant.id);
1523
+ }
1524
+ },
1525
+ ],
1416
1526
  beforeHandle: [elementHooks.beforeHandle],
1417
1527
  },
1418
1528
  composedHooks,
@@ -1437,12 +1547,13 @@ export function oke(options: OkeOptions): OkeApp {
1437
1547
  }
1438
1548
 
1439
1549
  const effects = flowDef.effects ?? capability?.declared;
1440
- const { fx, ledger } = createFxContext({
1550
+ const { fx, ledger } = loadFx().createFxContext({
1441
1551
  ...options.fx,
1442
1552
  flow: flowDef.name,
1443
1553
  effects,
1444
1554
  runTelemetry: telemetry,
1445
1555
  runId,
1556
+ lastEventId: extras?.request?.headers.get("last-event-id") || undefined,
1446
1557
  now,
1447
1558
  i18n: {
1448
1559
  locale: resolvedLocale,
@@ -1450,7 +1561,28 @@ export function oke(options: OkeOptions): OkeApp {
1450
1561
  catalogs: loadMessages().getMessageCatalogs(),
1451
1562
  ...options.fx?.i18n,
1452
1563
  },
1453
- ...(principals ? { auth: principals.auth as FxAuth, operator: principals.operator } : {}),
1564
+ ...(principals
1565
+ ? {
1566
+ auth: principals.auth,
1567
+ operator: principals.operator,
1568
+ tenant: principals.tenant,
1569
+ }
1570
+ : extras?.tenant
1571
+ ? { tenant: { id: extras.tenant.id } }
1572
+ : {}),
1573
+ apiKeyStore: gateConfig.auth?.apiKeyStore,
1574
+ sessions: authBinding?.sessions,
1575
+ manifest: options.manifest,
1576
+ ...(tenantEnabled || extras?.tenant
1577
+ ? (loadAppTenant().w(4, {
1578
+ enabled: tenantEnabled,
1579
+ store: gateConfig.auth?.tenantStore,
1580
+ scoped: flowTenantScoped,
1581
+ plane: flowDef.plane,
1582
+ tenant: principals?.tenant ?? (extras?.tenant ? { id: extras.tenant.id } : undefined),
1583
+ bind: authBinding,
1584
+ }) as object)
1585
+ : {}),
1454
1586
  ...(extras?.originPrincipal ? { principal: extras.originPrincipal } : {}),
1455
1587
  ...(extras?.trustedInvoke === true && extras.revealPii === true ? { revealPii: true } : {}),
1456
1588
  rlsGateNames: gateNamesOf(trigger),
@@ -1478,9 +1610,10 @@ export function oke(options: OkeOptions): OkeApp {
1478
1610
  callInput,
1479
1611
  { kind: "internal" } satisfies InternalTrigger,
1480
1612
  {
1481
- originPrincipal: freezePrincipal(fx.principal),
1613
+ originPrincipal: loadFx().freezePrincipal(fx.principal),
1482
1614
  locale: resolvedLocale,
1483
1615
  parentId: runId,
1616
+ ...(tenantEnabled ? { tenant: { id: fx.tenant.id } } : {}),
1484
1617
  ...(extras?.trustedInvoke === true && extras.revealPii === true
1485
1618
  ? { trustedInvoke: true, revealPii: true }
1486
1619
  : {}),
@@ -1582,7 +1715,7 @@ export function oke(options: OkeOptions): OkeApp {
1582
1715
  durable: flowDef.durable,
1583
1716
  effects: putEffects,
1584
1717
  });
1585
- if (storeAfter && output !== undefined && !isJsonStreamResult(output)) {
1718
+ if (storeAfter && output !== undefined && !loadFx().isJsonStreamResult(output)) {
1586
1719
  const ttlMs =
1587
1720
  typeof flowDef.cache === "string" ? cache.parseTtlMs(flowDef.cache) : undefined;
1588
1721
  storeRt.putTier1(
@@ -1604,6 +1737,7 @@ export function oke(options: OkeOptions): OkeApp {
1604
1737
  }
1605
1738
  return await invoke(ctx.input);
1606
1739
  } catch (err) {
1740
+ if (isFlowFailure(err)) return err;
1607
1741
  // A durable sleep that has not yet elapsed suspends the run — this
1608
1742
  // is a park, not a pipeline failure, so it must not throw upward.
1609
1743
  if (flowDef.durable && journalSession && isJournalSuspend(err)) {
@@ -1630,7 +1764,7 @@ export function oke(options: OkeOptions): OkeApp {
1630
1764
 
1631
1765
  const endedAt = now();
1632
1766
  const durationMs = resolveDurationMs(endedAt - startedAt, performance.now() - startedHr);
1633
- const streamOut = isJsonStreamResult(result.output) ? result.output : undefined;
1767
+ const streamOut = loadFx().isJsonStreamResult(result.output) ? result.output : undefined;
1634
1768
 
1635
1769
  const finalizeRun = async (): Promise<void> => {
1636
1770
  // Stream rows land in Traces only after close. Measure wall time then
@@ -1699,7 +1833,11 @@ export function oke(options: OkeOptions): OkeApp {
1699
1833
  }
1700
1834
  };
1701
1835
 
1702
- if (streamOut && !isTerminalFailure(result)) {
1836
+ if (
1837
+ streamOut &&
1838
+ !isTerminalFailure(result) &&
1839
+ result.response?.headers.get("content-type")?.includes("text/event-stream")
1840
+ ) {
1703
1841
  streamOut.finalize = finalizeRun;
1704
1842
  } else {
1705
1843
  await finalizeRun();
@@ -1780,6 +1918,7 @@ export function oke(options: OkeOptions): OkeApp {
1780
1918
  get authBinding() {
1781
1919
  return authBinding;
1782
1920
  },
1921
+ apiKeys: gateConfig.auth?.apiKeyStore,
1783
1922
  async resumeDurable(now) {
1784
1923
  const t = now ?? bootResult?.clock?.now() ?? options.fx?.now?.() ?? Date.now();
1785
1924
  const { store, instanceId, leaseMs } = activeJournal();
@@ -1805,7 +1944,7 @@ export function oke(options: OkeOptions): OkeApp {
1805
1944
  for (const [runId, sleeper] of [...sleepingRuns.entries()]) {
1806
1945
  if (t < sleeper.wakeAt) continue;
1807
1946
  sleepingRuns.delete(runId);
1808
- const result = await runDurable({
1947
+ const result = await loadDurable().runDurable({
1809
1948
  flow: sleeper.flow,
1810
1949
  input: sleeper.input,
1811
1950
  journalStore: store,
@@ -1861,7 +2000,7 @@ export function oke(options: OkeOptions): OkeApp {
1861
2000
  },
1862
2001
  flow(ref) {
1863
2002
  if (isFlow(ref)) return flowsByName.get(ref.name) ?? ref;
1864
- return flowsByName.get(resolveName(ref as NamedRef));
2003
+ return flowsByName.get(loadFx().resolveName(ref as NamedRef));
1865
2004
  },
1866
2005
  adopt(...args: readonly unknown[]) {
1867
2006
  const flows = accumulateAdoptArgs(args, routes);
@@ -1957,7 +2096,7 @@ export function oke(options: OkeOptions): OkeApp {
1957
2096
  );
1958
2097
  runLabel = internalResult.runId;
1959
2098
  cacheLabel = internalResult.cache;
1960
- return respond(encodeExecuteResult(internalResult));
2099
+ return respond(await encodeExecuteResult(internalResult));
1961
2100
  }
1962
2101
  }
1963
2102
 
@@ -2036,10 +2175,10 @@ export function oke(options: OkeOptions): OkeApp {
2036
2175
  runLabel = result.runId;
2037
2176
  cacheLabel = result.cache;
2038
2177
 
2039
- return respond(encodeExecuteResult(result));
2178
+ return respond(await encodeExecuteResult(result));
2040
2179
  },
2041
2180
  async dispatchSignal(signal, payload, meta) {
2042
- const name = resolveName(signal);
2181
+ const name = loadFx().resolveName(signal);
2043
2182
  const results: ExecuteResult[] = [];
2044
2183
  for (const b of adopted) {
2045
2184
  if (b.trigger.kind === "signal" && b.trigger.name === name) {
@@ -2052,11 +2191,11 @@ export function oke(options: OkeOptions): OkeApp {
2052
2191
  }
2053
2192
  return results;
2054
2193
  },
2055
- async dispatchEvery(interval) {
2194
+ async dispatchEvery(interval, extras) {
2056
2195
  const results: ExecuteResult[] = [];
2057
2196
  for (const b of adopted) {
2058
2197
  if (b.trigger.kind === "every" && b.trigger.interval === interval) {
2059
- results.push(await execute(b.flow, undefined, b.trigger as EveryTrigger));
2198
+ results.push(await execute(b.flow, undefined, b.trigger as EveryTrigger, extras));
2060
2199
  }
2061
2200
  }
2062
2201
  return results;
@@ -2077,7 +2216,7 @@ export function oke(options: OkeOptions): OkeApp {
2077
2216
  async call(ref, input) {
2078
2217
  const flowDef = isFlow(ref)
2079
2218
  ? (flowsByName.get(ref.name) ?? ref)
2080
- : flowsByName.get(resolveName(ref as NamedRef));
2219
+ : flowsByName.get(loadFx().resolveName(ref as NamedRef));
2081
2220
  if (!flowDef) {
2082
2221
  return undefined;
2083
2222
  }
@@ -6,7 +6,7 @@
6
6
  * separately in the pipeline (`allowTestPrincipals`).
7
7
  */
8
8
 
9
- import { authenticateApiKey, type ApiKeyStore } from "../auth/api-keys.ts";
9
+ import { authenticateApiKey, clientIpFromRequest, type ApiKeyStore } from "../auth/api-keys.ts";
10
10
  import type { ApiKeyRow } from "../auth/tables.ts";
11
11
  import {
12
12
  createSessionStore,
@@ -84,12 +84,15 @@ export function claimsToPrincipal(claims: AccessClaims): ResolvedPrincipal {
84
84
  userId: claims.sub,
85
85
  scopes: claims.scopes,
86
86
  verified: true,
87
+ ...(claims.tid !== undefined ? { tenantId: claims.tid } : {}),
87
88
  };
88
89
  }
89
90
 
90
91
  /**
91
92
  * Map an authenticated API key row to the pipeline principal shape.
92
- * Uses the bare key id so Access `runTouchesKey` matches `WideEvent.principal`.
93
+ *
94
+ * The key is the issuer with fewer gates: `userId` / `operatorId` is
95
+ * {@link ApiKeyRow.creatorId}. The credential id is {@link ResolvedPrincipal.apiKeyId}.
93
96
  *
94
97
  * @param row - Authenticated key
95
98
  */
@@ -97,19 +100,21 @@ export function apiKeyRowToPrincipal(row: ApiKeyRow): ResolvedPrincipal {
97
100
  if (row.plane === "operator") {
98
101
  return {
99
102
  plane: "operator",
100
- operatorId: row.id,
103
+ operatorId: row.creatorId,
101
104
  userId: null,
102
105
  scopes: row.scopes,
103
106
  verified: true,
104
107
  apiKeyId: row.id,
108
+ ...(row.tenantId !== undefined ? { tenantId: row.tenantId } : {}),
105
109
  };
106
110
  }
107
111
  return {
108
112
  plane: "user",
109
- userId: row.id,
113
+ userId: row.creatorId,
110
114
  scopes: row.scopes,
111
115
  verified: true,
112
116
  apiKeyId: row.id,
117
+ ...(row.tenantId !== undefined ? { tenantId: row.tenantId } : {}),
113
118
  };
114
119
  }
115
120
 
@@ -119,17 +124,20 @@ export function apiKeyRowToPrincipal(row: ApiKeyRow): ResolvedPrincipal {
119
124
  * @param auth - Session binding
120
125
  * @param token - Raw Bearer token
121
126
  * @param apiKeys - Optional key store
127
+ * @param request - Incoming request (allowlist IP is read here, not in `oke()`)
122
128
  */
123
129
  export async function verifyBearerOrApiKey(
124
130
  auth: AppAuthBinding,
125
131
  token: string,
126
132
  apiKeys?: ApiKeyStore,
133
+ request?: Request,
127
134
  ): Promise<ResolvedPrincipal> {
128
135
  try {
129
136
  return await verifyBearerToken(auth, token);
130
137
  } catch (err) {
131
138
  if (apiKeys) {
132
- const row = await authenticateApiKey(apiKeys, token, auth.now);
139
+ const ip = request ? clientIpFromRequest(request) : undefined;
140
+ const row = await authenticateApiKey(apiKeys, token, { now: auth.now, ip });
133
141
  if (row) return apiKeyRowToPrincipal(row);
134
142
  }
135
143
  throw err;
@@ -60,12 +60,16 @@ export async function bindClock(
60
60
  if (prebuilt) {
61
61
  clock = prebuilt;
62
62
  } else if (clockDriver === "frozen" || env === "test") {
63
- clock = createTestClockRuntime(now(), { instanceId: options.instanceId });
63
+ clock = createTestClockRuntime(now(), {
64
+ instanceId: options.instanceId,
65
+ ...(options.tenantIds ? { tenantIds: options.tenantIds } : {}),
66
+ });
64
67
  } else if (clockDriver === "memory") {
65
68
  clock = createClockRuntime({
66
69
  instanceId: options.instanceId,
67
70
  now,
68
71
  store: createMemoryCronStore(),
72
+ ...(options.tenantIds ? { tenantIds: options.tenantIds } : {}),
69
73
  });
70
74
  } else if (clockDriver === "file") {
71
75
  const path = resolve(process.cwd(), DEFAULT_FILE_CRON_PATH);
@@ -74,6 +78,7 @@ export async function bindClock(
74
78
  instanceId: options.instanceId,
75
79
  now,
76
80
  store: createFileCronStore(path),
81
+ ...(options.tenantIds ? { tenantIds: options.tenantIds } : {}),
77
82
  });
78
83
  } else if (clockDriver === "postgres") {
79
84
  const url = process.env.DATABASE_URL ?? process.env.OKE_STORE_SQL_URL ?? undefined;
@@ -89,6 +94,7 @@ export async function bindClock(
89
94
  instanceId: options.instanceId,
90
95
  now,
91
96
  store,
97
+ ...(options.tenantIds ? { tenantIds: options.tenantIds } : {}),
92
98
  });
93
99
  } else {
94
100
  throw new Error(
@@ -118,8 +124,8 @@ export async function bindClock(
118
124
  if (options.onCronFire) {
119
125
  const fire = options.onCronFire;
120
126
  for (const name of clockDecls.keys()) {
121
- clock.onCron(name, async () => {
122
- await fire(name);
127
+ clock.onCron(name, async (row) => {
128
+ await fire(row.name);
123
129
  });
124
130
  }
125
131
  }
@@ -79,7 +79,7 @@ describe("boot — lazy element needs", () => {
79
79
  expect(needs.signal).toBe(false);
80
80
  });
81
81
 
82
- test("oke() Store-only graph stays under the prior 48 kB baseline", async () => {
82
+ test("oke() Store-only graph stays under the prior 50 kB baseline", async () => {
83
83
  const dir = await mkdtemp(join(tmpdir(), "oke-store-only-"));
84
84
  const entry = join(dir, "entry.ts");
85
85
  const appPath = join(import.meta.dir, "app.ts");
@@ -116,9 +116,9 @@ describe("boot — lazy element needs", () => {
116
116
  if (raw.byteLength === 0) continue;
117
117
  total += Bun.gzipSync(new Uint8Array(raw)).byteLength;
118
118
  }
119
- // Rebased after clock / gate / kv / index auto-drain on `oke()`.
120
- // (~48.3 kB gzip with export externals).
121
- expect(total).toBeLessThan(49_000);
119
+ // Rebased after host API-key persist + allowlist on `oke()`.
120
+ // (~50.3 kB gzip with export externals).
121
+ expect(total).toBeLessThan(51_000);
122
122
  } finally {
123
123
  await rm(dir, { recursive: true, force: true });
124
124
  }
@@ -152,6 +152,8 @@ export interface BootOptions {
152
152
  * @param name - Cron name or every-interval
153
153
  */
154
154
  readonly onCronFire?: (name: string) => void | Promise<void>;
155
+ /** Tenant ids for per-tenant clock expansion at reconcile. */
156
+ readonly tenantIds?: () => readonly string[] | Promise<readonly string[]>;
155
157
  /**
156
158
  * Dispatch a signal message to bound flows.
157
159
  *