okengine 0.17.2 → 0.18.4

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 (209) hide show
  1. package/AGENTS.md +5 -3
  2. package/README.md +14 -10
  3. package/manifest.v1.schema.json +61 -2
  4. package/package.json +19 -5
  5. package/site/content/docs/ai/index.mdx +1 -1
  6. package/site/content/docs/ai/mcp.mdx +10 -1
  7. package/site/content/docs/ai/skills.mdx +9 -9
  8. package/site/content/docs/elements/ai.mdx +1 -1
  9. package/site/content/docs/elements/clock.mdx +1 -1
  10. package/site/content/docs/elements/flow.mdx +25 -1
  11. package/site/content/docs/elements/gate.mdx +3 -2
  12. package/site/content/docs/elements/store.mdx +289 -341
  13. package/site/content/docs/elements/vault.mdx +5 -5
  14. package/site/content/docs/get-started/basic-usage.mdx +3 -10
  15. package/site/content/docs/get-started/index.mdx +1 -1
  16. package/site/content/docs/get-started/installation.mdx +2 -3
  17. package/site/content/docs/get-started/introduction.mdx +58 -121
  18. package/site/content/docs/get-started/meta.json +9 -1
  19. package/site/content/docs/get-started/project-structure.mdx +4 -11
  20. package/site/content/docs/get-started/testing.mdx +328 -0
  21. package/site/content/docs/get-started/why.mdx +93 -71
  22. package/site/content/docs/index.mdx +44 -11
  23. package/site/content/docs/meta.json +8 -5
  24. package/site/content/docs/plugins/apple.mdx +151 -0
  25. package/site/content/docs/plugins/discord.mdx +139 -0
  26. package/site/content/docs/plugins/facebook.mdx +134 -0
  27. package/site/content/docs/plugins/figma.mdx +138 -0
  28. package/site/content/docs/plugins/github.mdx +138 -0
  29. package/site/content/docs/plugins/google.mdx +153 -0
  30. package/site/content/docs/plugins/index.mdx +47 -1
  31. package/site/content/docs/plugins/meta.json +10 -0
  32. package/site/content/docs/plugins/microsoft.mdx +151 -0
  33. package/site/content/docs/plugins/oauth.mdx +188 -0
  34. package/site/content/docs/plugins/x.mdx +125 -0
  35. package/site/content/docs/providers/index.mdx +2 -0
  36. package/site/content/docs/recipes/index.mdx +2 -0
  37. package/site/content/docs/reference/cli.md +3 -2
  38. package/site/content/docs/reference/client.mdx +58 -1
  39. package/site/content/docs/reference/configuration.mdx +2 -4
  40. package/site/content/docs/reference/fx.mdx +3 -1
  41. package/site/content/docs/reference/index.mdx +0 -5
  42. package/site/content/docs/reference/meta.json +2 -2
  43. package/site/content/docs/reference/okid.mdx +137 -0
  44. package/src/auth/bindings.ts +1 -1
  45. package/src/auth/config.ts +9 -0
  46. package/src/auth/identity-sql.ts +314 -0
  47. package/src/auth/identity.ts +140 -2
  48. package/src/auth/index.ts +17 -1
  49. package/src/auth/method-context.ts +3 -0
  50. package/src/auth/oauth-as/cimd.ts +132 -0
  51. package/src/auth/oauth-as/crypto.test.ts +101 -0
  52. package/src/auth/oauth-as/crypto.ts +393 -0
  53. package/src/auth/oauth-as/errors.ts +68 -0
  54. package/src/auth/oauth-as/http.test.ts +419 -0
  55. package/src/auth/oauth-as/http.ts +842 -0
  56. package/src/auth/oauth-as/stores.ts +61 -0
  57. package/src/auth/oauth-as/tables.ts +142 -0
  58. package/src/auth/tables.ts +0 -11
  59. package/src/bench/README.md +83 -0
  60. package/src/bench/REPORT.md +176 -0
  61. package/src/bench/g01-rls-stamp.bench.ts +194 -0
  62. package/src/bench/g02-clock-per-tenant.bench.ts +158 -0
  63. package/src/bench/g03-signal-once.bench.ts +157 -0
  64. package/src/bench/g03-signal-reconnect.bench.ts +254 -0
  65. package/src/bench/g03-signal-sse-memory.bench.ts +191 -0
  66. package/src/bench/g04-auth-vault-hotpath.bench.ts +170 -0
  67. package/src/bench/g05-sustained-full.bench.ts +265 -0
  68. package/src/bench/g06-mixed-load.bench.ts +260 -0
  69. package/src/bench/g07-vault-crypto.bench.ts +100 -0
  70. package/src/bench/g07-vault-rotate-under-read.bench.ts +285 -0
  71. package/src/bench/g08-conn-oversubscribe.bench.ts +194 -0
  72. package/src/bench/g08-store-kv-durable.bench.ts +133 -0
  73. package/src/bench/g08-store-sql.bench.ts +178 -0
  74. package/src/bench/g09-journal-sustained.bench.ts +203 -0
  75. package/src/bench/g10-observability-contention.bench.ts +246 -0
  76. package/src/bench/g11-cold-start-cycle.bench.ts +164 -0
  77. package/src/bench/g13-elements.bench.ts +427 -0
  78. package/src/bench/g14-graceful-shutdown.bench.ts +244 -0
  79. package/src/bench/g15-postgres-degradation.bench.ts +264 -0
  80. package/src/bench/g16-live-query-fanout.bench.ts +206 -0
  81. package/src/bench/lib/event-loop-lag.ts +26 -0
  82. package/src/bench/lib/infra.ts +60 -0
  83. package/src/bench/lib/report.ts +52 -0
  84. package/src/bench/lib/rss-sampler.ts +61 -0
  85. package/src/bench/lib/signal-pg.ts +88 -0
  86. package/src/bench/load-app.ts +337 -0
  87. package/src/bench/load-child.ts +108 -0
  88. package/src/bench/smoke.bench.ts +43 -0
  89. package/src/cli/competitor-mention-removal.test.ts +28 -0
  90. package/src/cli/doctor-fd.ts +117 -0
  91. package/src/cli/doctor.test.ts +192 -0
  92. package/src/cli/doctor.ts +129 -1
  93. package/src/client/create.ts +95 -1
  94. package/src/client/index.ts +9 -2
  95. package/src/client/transport.ts +11 -4
  96. package/src/client/use-live-query.ts +154 -0
  97. package/src/client-react/index.ts +15 -1
  98. package/src/client-react/live-resource.ts +246 -0
  99. package/src/client-react/use-live-query.test.ts +475 -0
  100. package/src/client-react/use-live-query.ts +530 -0
  101. package/src/compiler/extract.test.ts +518 -0
  102. package/src/compiler/extract.ts +386 -19
  103. package/src/console/server/invoke-user-flow.ts +2 -1
  104. package/src/console/ui-next/dist/assets/{access-page-DnWbnGzq.js → access-page-De7Lc2JC.js} +1 -1
  105. package/src/console/ui-next/dist/assets/{flows-page-BiZ4-6yQ.js → flows-page-RGy7VEA_.js} +1 -1
  106. package/src/console/ui-next/dist/assets/{index-C8NRK2R-.js → index-_rgpdVzo.js} +3 -3
  107. package/src/console/ui-next/dist/assets/{observability-page-CrB6vd1T.js → observability-page-Ds6pcnh-.js} +1 -1
  108. package/src/console/ui-next/dist/assets/{store-page-CS5-aETQ.js → store-page-02xOiqIK.js} +3 -3
  109. package/src/console/ui-next/dist/assets/{units-page-CjtdlW8l.js → units-page-4rHOePuE.js} +1 -1
  110. package/src/console/ui-next/dist/assets/{vault-page-C6Xxm9SA.js → vault-page-DISPgxLM.js} +1 -1
  111. package/src/console/ui-next/dist/index.html +1 -1
  112. package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +36 -2
  113. package/src/drivers/cdc-outbox.ts +389 -0
  114. package/src/drivers/memory.ts +20 -0
  115. package/src/drivers/oauth-apple.ts +156 -0
  116. package/src/drivers/oauth-discord.ts +79 -0
  117. package/src/drivers/oauth-facebook.ts +80 -0
  118. package/src/drivers/oauth-figma.ts +116 -0
  119. package/src/drivers/oauth-github.ts +92 -0
  120. package/src/drivers/oauth-google.ts +142 -0
  121. package/src/drivers/oauth-microsoft.ts +174 -0
  122. package/src/drivers/oauth-oidc.ts +293 -0
  123. package/src/drivers/oauth-shared.ts +326 -0
  124. package/src/drivers/oauth-types.ts +159 -0
  125. package/src/drivers/oauth-x.ts +77 -0
  126. package/src/drivers/oauth2-common.ts +95 -0
  127. package/src/drivers/oauth2-token.ts +61 -0
  128. package/src/drivers/pg-rls-row-passes.ts +251 -0
  129. package/src/drivers/pg-rls.ts +2 -0
  130. package/src/drivers/postgres.ts +45 -2
  131. package/src/drivers/signal-postgres.ts +2 -1
  132. package/src/elements/channel/runtime.ts +29 -2
  133. package/src/elements/channel.test.ts +52 -0
  134. package/src/elements/gate/boot.ts +29 -2
  135. package/src/elements/store/emit-drizzle.ts +147 -14
  136. package/src/elements/store/field-ddl.test.ts +118 -0
  137. package/src/elements/store/field-types.test.ts +455 -0
  138. package/src/elements/store/list-query.golden.json +777 -0
  139. package/src/elements/store/list-query.parity.test.ts +396 -0
  140. package/src/elements/store/list-query.ts +792 -0
  141. package/src/elements/store/live-default.test.ts +136 -0
  142. package/src/elements/store/live-http.test.ts +160 -0
  143. package/src/elements/store/live-isolation.test.ts +291 -0
  144. package/src/elements/store/live-query-runtime.test.ts +323 -0
  145. package/src/elements/store/live-query-runtime.ts +403 -0
  146. package/src/elements/store/live-query-server.test.ts +377 -0
  147. package/src/elements/store/live-query-server.ts +102 -0
  148. package/src/elements/store/live-query.ts +97 -0
  149. package/src/elements/store/resource.ts +189 -680
  150. package/src/elements/store/rls-row-passes-policies.parity.test.ts +665 -0
  151. package/src/elements/store/schema-decl.ts +539 -41
  152. package/src/elements/store/sql-rls-stamp.test.ts +27 -0
  153. package/src/elements/store/sql-session.ts +297 -35
  154. package/src/elements/store/table.ts +102 -21
  155. package/src/elements/store.test.ts +3 -1
  156. package/src/elements/store.ts +12 -1
  157. package/src/elements/vault/chaos-child.ts +74 -1
  158. package/src/elements/vault/chaos.test.ts +4 -2
  159. package/src/elements/vault/storage.ts +4 -2
  160. package/src/index.ts +5 -2
  161. package/src/kernel/app-auth.ts +1 -0
  162. package/src/kernel/app.ts +116 -2
  163. package/src/kernel/auth-sharing.test.ts +196 -0
  164. package/src/kernel/boot.test.ts +3 -3
  165. package/src/kernel/errors.ts +8 -0
  166. package/src/kernel/fx.test.ts +1 -0
  167. package/src/kernel/fx.ts +14 -2
  168. package/src/kernel/horizontal-child.ts +2 -1
  169. package/src/kernel/http-resource.ts +33 -7
  170. package/src/kernel/identity-host-persist.test.ts +119 -0
  171. package/src/kernel/instance-id.ts +4 -2
  172. package/src/kernel/journal.ts +2 -1
  173. package/src/kernel/mcp-tool.test.ts +95 -0
  174. package/src/kernel/on.ts +9 -0
  175. package/src/kernel/realtime-bind.ts +326 -0
  176. package/src/kernel/resource-live.ts +117 -0
  177. package/src/kernel/triggers.ts +86 -4
  178. package/src/manifest/diff.ts +37 -0
  179. package/src/manifest/types.ts +64 -2
  180. package/src/okid.bench.test.ts +64 -0
  181. package/src/okid.test.ts +338 -0
  182. package/src/okid.ts +245 -0
  183. package/src/plugins/anonymous.ts +19 -1
  184. package/src/plugins/auth/shared.ts +15 -0
  185. package/src/plugins/index.ts +2 -0
  186. package/src/plugins/magic-link.ts +10 -8
  187. package/src/plugins/mcp-oauth.ts +208 -0
  188. package/src/plugins/oauth/flow-store.ts +117 -0
  189. package/src/plugins/oauth/link.ts +69 -0
  190. package/src/plugins/oauth/shared.ts +108 -0
  191. package/src/plugins/oauth/token-vault.ts +100 -0
  192. package/src/plugins/oauth.security.test.ts +535 -0
  193. package/src/plugins/oauth.ts +532 -0
  194. package/src/plugins/otp.ts +48 -6
  195. package/src/plugins/passkey.ts +20 -1
  196. package/src/plugins/two-factor.ts +11 -0
  197. package/src/plugins/username.ts +40 -7
  198. package/src/release/build-lib.ts +7 -1
  199. package/src/release/measure.ts +1 -0
  200. package/src/release/official-plugins.ts +4 -1
  201. package/src/runs/collect.ts +2 -1
  202. package/src/runs/drivers/files.ts +2 -1
  203. package/src/test/create-test-app.ts +114 -5
  204. package/src/test/export-bundle.test.ts +33 -0
  205. package/src/test/live-signals.test.ts +83 -0
  206. package/src/test/tenant-isolation.test.ts +175 -0
  207. package/src/testing.ts +26 -0
  208. package/src/upgrade/codemods.ts +1 -1
  209. package/site/content/docs/reference/migrating-environments.mdx +0 -158
@@ -574,6 +574,7 @@ describe("fx — wholesale swap", () => {
574
574
  get signal() {
575
575
  return new AbortController().signal;
576
576
  },
577
+ rlsIdentity: undefined,
577
578
  all(thunks) {
578
579
  return Promise.all(thunks.map((t) => t())) as never;
579
580
  },
package/src/kernel/fx.ts CHANGED
@@ -72,6 +72,7 @@ import type { RunsRuntime } from "../runs/runtime.ts";
72
72
  import type { RunsRow, WideEvent } from "../runs/types.ts";
73
73
  import type { RunWindowStats, SloBreach } from "../runs/window.ts";
74
74
  import type { MessageCatalogs } from "../i18n/messages.ts";
75
+ import { okid } from "../okid.ts";
75
76
  import { lazyRequire } from "./lazy-require.ts";
76
77
  import type { AppMessageKey, MessageValues } from "../i18n/types.ts";
77
78
 
@@ -853,6 +854,14 @@ export interface Fx {
853
854
  * Outside `all` / `race`, a never-aborted signal.
854
855
  */
855
856
  readonly signal: AbortSignal;
857
+ /**
858
+ * Resolved RLS identity for this invocation (`undefined` when bypassed,
859
+ * operator-plane, or unauthenticated on an un-gated trigger). The live
860
+ * query stack subscribes SSE streams under this exact stamp so per-event
861
+ * visibility re-checks are Postgres-authoritative — identical to what a
862
+ * read through {@link Fx.store} would see.
863
+ */
864
+ readonly rlsIdentity: import("../drivers/pg-rls.ts").RlsIdentity | undefined;
856
865
  /**
857
866
  * Run thunks in parallel. On first rejection, abort sibling branches and
858
867
  * rethrow. Pass thunks (not started Promises) so abort scopes exist first.
@@ -1197,7 +1206,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1197
1206
  },
1198
1207
  insert(row: Record<string, unknown>): Promise<{ id: string }> {
1199
1208
  return gated("write", ref, () => {
1200
- const id = typeof row.id === "string" ? row.id : crypto.randomUUID();
1209
+ const id = typeof row.id === "string" ? row.id : okid();
1201
1210
  table().set(id, { ...row, id });
1202
1211
  return { id };
1203
1212
  });
@@ -2088,7 +2097,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
2088
2097
  },
2089
2098
  locale,
2090
2099
  id() {
2091
- return crypto.randomUUID();
2100
+ return okid();
2092
2101
  },
2093
2102
  auth,
2094
2103
  operator,
@@ -2130,6 +2139,9 @@ export function createFxContext(options: CreateFxOptions): FxContext {
2130
2139
  get signal(): AbortSignal {
2131
2140
  return currentAbortSignal();
2132
2141
  },
2142
+ get rlsIdentity() {
2143
+ return rlsInvokeContext().rls;
2144
+ },
2133
2145
  all(thunks) {
2134
2146
  return fxAll(thunks);
2135
2147
  },
@@ -38,6 +38,7 @@ import { http } from "./triggers.ts";
38
38
  import { GATE_KV_NAMESPACE } from "./boot-bind/gate.ts";
39
39
  import { createBunRuntime } from "../runtime/bun.ts";
40
40
  import { installGracefulShutdown } from "./graceful-shutdown.ts";
41
+ import { okid } from "../okid.ts";
41
42
 
42
43
  const mode = process.argv[2];
43
44
  if (mode !== "serve") {
@@ -139,7 +140,7 @@ const rate = flow("horizontal.rate", {
139
140
  });
140
141
  const write = flow("horizontal.write", {
141
142
  do: async (_input, fx) => {
142
- const id = crypto.randomUUID();
143
+ const id = okid();
143
144
  await fx
144
145
  .store(db)
145
146
  .insert(writesTable)
@@ -13,7 +13,9 @@ import {
13
13
  type HttpTrigger,
14
14
  type ResourceFlowBag,
15
15
  type ResourceMount,
16
+ type SignalSource,
16
17
  } from "./triggers.ts";
18
+ import { registerPendingResourceLiveMount } from "./resource-live.ts";
17
19
 
18
20
  /**
19
21
  * Mount a CRUD resource at `path`: `list`/`create` on the base, `get` /
@@ -31,16 +33,40 @@ export function httpResource<P extends string>(
31
33
  const id = `${path}/:id` as `${P}/:id`;
32
34
  const verb = <M extends HttpMethod>(method: M, p: P | `${P}/:id`): HttpTrigger<M> =>
33
35
  createHttpTrigger(method, p, gates);
36
+ // Live exposure — synthesized from the resource's live surface so SSE
37
+ // rides the same gates as the CRUD verbs.
38
+ const liveFlow = (ops as { readonly live?: { readonly signal: string; readonly flow: unknown } })
39
+ .live;
40
+ // `store.resource(…, { live: omitted })` puts a pending surface on the ops
41
+ // bag. `http.resource` records the mount slot; `oke()` drains it only when
42
+ // the project-wide `store.live` flag is on.
43
+ const pendingSignal = (ops as { readonly pendingLive?: { readonly signalName: string } })
44
+ .pendingLive?.signalName;
45
+ if (pendingSignal !== undefined && liveFlow === undefined) {
46
+ registerPendingResourceLiveMount({ path, gates, signalName: pendingSignal });
47
+ }
48
+ const mounts = [
49
+ { trigger: verb("GET", path), flow: ops.list },
50
+ { trigger: verb("POST", path), flow: ops.create },
51
+ { trigger: verb("GET", id), flow: ops.get },
52
+ { trigger: verb("PATCH", id), flow: ops.update },
53
+ { trigger: verb("DELETE", id), flow: ops.remove },
54
+ ...(liveFlow !== undefined
55
+ ? [
56
+ {
57
+ trigger: createHttpTrigger("GET" as const, `${path}/live`, gates, {
58
+ name: liveFlow.signal,
59
+ } as SignalSource),
60
+ flow: liveFlow.flow,
61
+ },
62
+ ]
63
+ : []),
64
+ ];
34
65
  const mount: ResourceMount = {
35
66
  [resourceMountBrand]: true,
36
67
  gates,
37
- mounts: [
38
- { trigger: verb("GET", path), flow: ops.list },
39
- { trigger: verb("POST", path), flow: ops.create },
40
- { trigger: verb("GET", id), flow: ops.get },
41
- { trigger: verb("PATCH", id), flow: ops.update },
42
- { trigger: verb("DELETE", id), flow: ops.remove },
43
- ],
68
+ ...(liveFlow !== undefined ? { live: { signal: liveFlow.signal, flow: liveFlow.flow } } : {}),
69
+ mounts,
44
70
  gate: createGateAttach((next) => httpResource(path, ops, next), gates),
45
71
  public() {
46
72
  return httpResource(path, ops, [...gates, GATE_PUBLIC_NAME]);
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Host identity durability — standalone `oke()` (no Console).
3
+ *
4
+ * Creates a user via the real `/auth/sign-up/email` path, stops, boots a fresh
5
+ * app against the same SQL URL, and resolves the same user on `/auth/sign-in/email`.
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 { resetFlowSeq } from "./flow.ts";
19
+ import { resetBindings } from "./on.ts";
20
+
21
+ const PEPPER = "oke-host-identity-persist-pepper";
22
+ const member = gate.policy("member", ({ auth }) => !!auth.verified);
23
+
24
+ function resetIsolated(): void {
25
+ resetBindings();
26
+ resetFlowSeq();
27
+ resetStores();
28
+ }
29
+
30
+ afterEach(() => {
31
+ resetIsolated();
32
+ });
33
+
34
+ describe("host identity persist via store.sql()", () => {
35
+ test("standalone oke() persists users+credentials across restart; sign-in resolves on a fresh store", async () => {
36
+ const dir = await mkdtemp(join(tmpdir(), "oke-host-identity-"));
37
+ const sqlUrl = join(dir, "pgdata");
38
+ const prevDb = process.env.DATABASE_URL;
39
+ const prevPglite = process.env.OKE_PGLITE_URL;
40
+ process.env.DATABASE_URL = sqlUrl;
41
+ process.env.OKE_PGLITE_URL = sqlUrl;
42
+
43
+ try {
44
+ resetIsolated();
45
+ const db1 = store.sql("app", { schema: {} });
46
+ const first = oke({
47
+ name: "host-identity-first",
48
+ stores: [db1],
49
+ gate: {
50
+ auth: { secret: PEPPER, emailAndPassword: { enabled: true } },
51
+ policies: [member],
52
+ },
53
+ env: "test",
54
+ startScheduler: false,
55
+ });
56
+ await first.boot({ env: "test", startScheduler: false });
57
+
58
+ const signUp = await first.fetch(
59
+ new Request("http://localhost/auth/sign-up/email", {
60
+ method: "POST",
61
+ headers: { "content-type": "application/json" },
62
+ body: JSON.stringify({ email: "persist@example.com", password: "CorrectHorse1!" }),
63
+ }),
64
+ );
65
+ expect(signUp.status).toBe(200);
66
+ const signedUp = (await signUp.json()) as {
67
+ data: { accessToken: string; userId: string };
68
+ };
69
+ expect(signedUp.data.userId).toBeTruthy();
70
+
71
+ const firstStore = first.bootResult?.store;
72
+ await first.stop();
73
+ await firstStore?.close();
74
+
75
+ resetIsolated();
76
+ const db2 = store.sql("app", { schema: {} });
77
+ const second = oke({
78
+ name: "host-identity-second",
79
+ stores: [db2],
80
+ gate: {
81
+ auth: { secret: PEPPER, emailAndPassword: { enabled: true } },
82
+ policies: [member],
83
+ },
84
+ env: "test",
85
+ startScheduler: false,
86
+ });
87
+ await second.boot({ env: "test", startScheduler: false });
88
+
89
+ const signIn = await second.fetch(
90
+ new Request("http://localhost/auth/sign-in/email", {
91
+ method: "POST",
92
+ headers: { "content-type": "application/json" },
93
+ body: JSON.stringify({ email: "persist@example.com", password: "CorrectHorse1!" }),
94
+ }),
95
+ );
96
+ expect(signIn.status).toBe(200);
97
+ const session = (await signIn.json()) as { data: { userId: string } };
98
+ expect(session.data.userId).toBe(signedUp.data.userId);
99
+
100
+ const conn = await second.bootResult?.store?.primarySql();
101
+ const identities = await conn?.query(`SELECT id, email FROM ${AUTH_TABLES.identities}`);
102
+ expect(identities?.some((r) => r.email === "persist@example.com")).toBe(true);
103
+ const credentials = await conn?.query(
104
+ `SELECT user_id, provider, provider_account_id FROM ${AUTH_TABLES.credentials}`,
105
+ );
106
+ expect(credentials?.some((r) => r.provider === "credential")).toBe(true);
107
+
108
+ const secondStore = second.bootResult?.store;
109
+ await second.stop();
110
+ await secondStore?.close();
111
+ } finally {
112
+ if (prevDb === undefined) delete process.env.DATABASE_URL;
113
+ else process.env.DATABASE_URL = prevDb;
114
+ if (prevPglite === undefined) delete process.env.OKE_PGLITE_URL;
115
+ else process.env.OKE_PGLITE_URL = prevPglite;
116
+ await rm(dir, { recursive: true, force: true });
117
+ }
118
+ }, 20_000);
119
+ });
@@ -3,14 +3,16 @@
3
3
  * and the fleet registry.
4
4
  */
5
5
 
6
+ import { okid } from "../okid.ts";
7
+
6
8
  /** Prefix for process instance ids minted at boot. */
7
9
  export const INSTANCE_ID_PREFIX = "inst-";
8
10
 
9
11
  /**
10
- * Mint a process instance id (`inst-<uuid>`).
12
+ * Mint a process instance id (`inst-<okid>`).
11
13
  */
12
14
  export function mintInstanceId(): string {
13
- return `${INSTANCE_ID_PREFIX}${crypto.randomUUID()}`;
15
+ return `${INSTANCE_ID_PREFIX}${okid()}`;
14
16
  }
15
17
 
16
18
  /**
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { mkdir } from "node:fs/promises";
11
11
  import { dirname } from "node:path";
12
+ import { okid } from "../okid.ts";
12
13
  import { JournalSuspend } from "./journal-suspend.ts";
13
14
 
14
15
  export { JournalSuspend, isJournalSuspend } from "./journal-suspend.ts";
@@ -452,7 +453,7 @@ export interface Journal {
452
453
  */
453
454
  export function createJournal(options: CreateJournalOptions): Journal {
454
455
  const now = options.now ?? (() => Date.now());
455
- const newId = options.id ?? (() => crypto.randomUUID());
456
+ const newId = options.id ?? (() => okid());
456
457
  const lease = options.lease;
457
458
  const coordinated = lease !== undefined && hasJournalLease(options.store);
458
459
 
@@ -0,0 +1,95 @@
1
+ /**
2
+ * MCP tool trigger — name validation, gated registration lookup,
3
+ * duplicate-name boot failure (OKE1018), gate posture, and Manifest stamping.
4
+ */
5
+
6
+ import { describe, expect, test, beforeEach } from "bun:test";
7
+ import { GateBootError } from "../elements/gate/boot.ts";
8
+ import { extractFromSources } from "../compiler/extract.ts";
9
+ import { gate } from "../elements/gate.ts";
10
+ import { oke } from "./app.ts";
11
+ import { flow, resetFlowSeq } from "./flow.ts";
12
+ import { on, resetBindings } from "./on.ts";
13
+ import { mcp } from "./triggers.ts";
14
+
15
+ const member = gate.policy("member", ({ auth }) => !!auth.verified);
16
+
17
+ beforeEach(() => {
18
+ resetBindings();
19
+ resetFlowSeq();
20
+ });
21
+
22
+ describe("mcp.tool(name)", () => {
23
+ test("empty or missing name throws TypeError", () => {
24
+ expect(() => mcp.tool("")).toThrow(/name is required/);
25
+ expect(() => mcp.tool(" ")).toThrow(/name is required/);
26
+ expect(() => (mcp.tool as (name?: string) => ReturnType<typeof mcp.tool>)()).toThrow(
27
+ /name is required/,
28
+ );
29
+ });
30
+
31
+ test("gated registration is indexed by exact tool name", () => {
32
+ const toolFlow = on(
33
+ mcp.tool("unit.action").gate(member),
34
+ flow("unit.action", {
35
+ plane: "user",
36
+ do: () => ({ ok: true }),
37
+ }),
38
+ );
39
+
40
+ expect(toolFlow.$trigger?.kind).toBe("mcp");
41
+ expect(toolFlow.$trigger && toolFlow.$trigger.kind === "mcp" && toolFlow.$trigger.name).toBe(
42
+ "unit.action",
43
+ );
44
+
45
+ const app = oke({ name: "t", autoBoot: false });
46
+ expect(app.resolveMcpTool("unit.action")).toBe(toolFlow);
47
+ expect(app.resolveMcpTool("other.action")).toBeUndefined();
48
+ });
49
+
50
+ test("duplicate tool name fails OKE1018 at construction", () => {
51
+ on(
52
+ mcp.tool("bookings.create").gate(member),
53
+ flow("bookings.create", { plane: "user", do: () => ({ ok: true }) }),
54
+ );
55
+ on(
56
+ mcp.tool("bookings.create").gate(member),
57
+ flow("bookings.also", { plane: "user", do: () => ({ ok: true }) }),
58
+ );
59
+ expect(() => oke({ name: "t", autoBoot: false })).toThrow(/OKE1018/);
60
+ });
61
+
62
+ test("ungated mcp.tool passes on() but fails gate posture at boot", async () => {
63
+ on(
64
+ mcp.tool("bookings.create"),
65
+ flow("bookings.create", { plane: "user", do: () => ({ ok: true }) }),
66
+ );
67
+
68
+ const app = oke({ name: "t", autoBoot: false });
69
+ expect(app.resolveMcpTool("bookings.create")).toBeUndefined();
70
+ await expect(app.boot()).rejects.toThrow(GateBootError);
71
+ });
72
+ });
73
+
74
+ describe("mcp.tool — Manifest", () => {
75
+ test("trigger.mcp.name and gates are stamped on the flow", async () => {
76
+ const source = `
77
+ import { on, flow, gate, mcp } from "okengine";
78
+
79
+ export const member = gate.policy("member", ({ auth }) => !!auth.verified);
80
+
81
+ export const createBooking = on(
82
+ mcp.tool("bookings.create").gate(member),
83
+ flow("bookings.create", {
84
+ plane: "user",
85
+ do: () => ({ ok: true }),
86
+ }),
87
+ );
88
+ `;
89
+ const manifest = await extractFromSources({ "src/flows/bookings/index.ts": source });
90
+ const flowDef = manifest.flows?.["bookings.create"];
91
+ expect(flowDef?.trigger).toEqual({ mcp: { name: "bookings.create" } });
92
+ expect(flowDef?.gates).toEqual(["member"]);
93
+ expect(flowDef?.plane).toBe("user");
94
+ });
95
+ });
package/src/kernel/on.ts CHANGED
@@ -91,6 +91,9 @@ export function on(
91
91
  };
92
92
  const ops: Record<string, unknown> = {};
93
93
  for (const { trigger, flow } of triggerOrMount.mounts) {
94
+ // Live SSE exposure binds through the normal `on()` path below (its
95
+ // trigger carries a liveSignal) — it is not one of the five CRUD ops.
96
+ if (trigger.liveSignal !== undefined) continue;
94
97
  const key = trigger.path.endsWith("/:id")
95
98
  ? byIdVerb[trigger.method]
96
99
  : baseVerb[trigger.method];
@@ -100,6 +103,12 @@ export function on(
100
103
  on(trigger, flow as FlowDef<any, any, any, any, Trigger | undefined>);
101
104
  ops[key] = flow;
102
105
  }
106
+ // Live surface — the `<path>/live` mount carries a liveSignal; bind it
107
+ // through `on()` so SSE synthesis + exposure-uniqueness checks run once.
108
+ const liveMount = triggerOrMount.mounts.find((m) => m.trigger.liveSignal !== undefined);
109
+ if (liveMount !== undefined && isFlow(liveMount.flow)) {
110
+ on(liveMount.trigger, liveMount.flow as FlowDef<any, any, any, any, Trigger | undefined>);
111
+ }
103
112
  return ops as unknown as ResourceFlowBag;
104
113
  }
105
114
  const asHttp =